<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>QEMU on Niels Provos</title>
        <link>https://www.provos.org/tags/qemu/</link>
        <description>Recent content in QEMU on Niels Provos</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>en</language>
        <lastBuildDate>Sun, 28 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://www.provos.org/tags/qemu/index.xml" rel="self" type="application/rss+xml" /><item>
        <title>Watching GLM 5.2 Escape QEMU</title>
        <link>https://www.provos.org/p/qemu-escape-glm-5-2/</link>
        <pubDate>Sun, 28 Jun 2026 00:00:00 +0000</pubDate>
        
        <guid>https://www.provos.org/p/qemu-escape-glm-5-2/</guid>
        <description>&lt;img src="https://www.provos.org/p/qemu-escape-glm-5-2/EscapeQemuCover.png" alt="Featured image of post Watching GLM 5.2 Escape QEMU" /&gt;&lt;p&gt;Effective AI-driven vulnerability discovery does not require a restricted frontier model. This is a concrete run that shows it, start to finish: I pointed IronCurtain&amp;rsquo;s &lt;code&gt;vuln-discovery&lt;/code&gt; workflow at QEMU, with every agent role driven by the open-weight GLM 5.2. The workflow had already found a set of out-of-bounds primitives in QEMU&amp;rsquo;s device emulation, the EDU device&amp;rsquo;s among them. I then set it the goal of turning those primitives into a maximal-impact proof of concept of host compromise: a reliable guest-to-host escape that needs no host-side knowledge, defeating ASLR from inside the guest and leaving a host-visible proof of execution, all against unmodified device source. It rejected two dead-end targets, pivoted to corrupting the EDU device&amp;rsquo;s own timer callback, and drove the chain to reproducible host code execution.&lt;/p&gt;
&lt;p&gt;The EDU device is QEMU&amp;rsquo;s educational PCI device, not compiled by default and attached only with an explicit &lt;code&gt;-device edu&lt;/code&gt;. It is tutorial code, present on no default machine and in no production build. Xchg Labs published the complete EDU escape in &lt;a class=&#34;link&#34; href=&#34;https://xchglabs.com/blog/escaping-qemu.html&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;Escaping QEMU&lt;/a&gt; on May 22 and noted, correctly, that there is nothing to disclose, because the file exists to teach new contributors how device emulation works. So I can show this chain in full. Everything else the same investigation surfaced, in devices that ship in real configurations, stays withheld pending coordination, exactly as in my &lt;a class=&#34;link&#34; href=&#34;https://www.provos.org/p/finding-zero-days-with-any-model/&#34; &gt;earlier work&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&#34;one-model-drove-every-role&#34;&gt;One model drove every role&lt;/h2&gt;
&lt;p&gt;The workflow is the same hub-and-spoke state machine I described &lt;a class=&#34;link&#34; href=&#34;https://www.provos.org/p/finding-zero-days-with-any-model/&#34; &gt;before&lt;/a&gt;. An orchestrator that is not itself an LLM routes specialized agents (analyze, harness design and build, validate, discover, triage, conclude) through a fixed set of states, each rehydrating a fresh context window from the journal on disk. I will not re-explain the mechanics here. What matters for this post is the model wiring and one design rule.&lt;/p&gt;
&lt;div class=&#34;mermaid fsm-slide&#34;&gt;flowchart TB
analyzeNode[analyze]
orchestratorNode[orchestrator]
harnessNode[&#34;harness pipeline&#34;]
discoverNode[discover]
triageNode[triage]
escalationNode[&#34;human escalation&#34;]
concludeNode[conclude]

analyzeNode --&gt; orchestratorNode
orchestratorNode -- reanalyze --&gt; analyzeNode
orchestratorNode -- harness_design --&gt; harnessNode
orchestratorNode -- discover --&gt; discoverNode
orchestratorNode -- triage --&gt; triageNode
orchestratorNode -- escalate --&gt; escalationNode
orchestratorNode -- complete --&gt; concludeNode&lt;/div&gt;

&lt;p&gt;The model wiring is deliberately boring. The workflow pins a single model alias for all eleven agent roles, with no per-state override, so one model drives analysis, harness design, fuzzing, exploitation, and triage uniformly. A LiteLLM gateway re-routed every model call to GLM 5.2, so GLM 5.2 served every role, and I changed nothing else in the workflow.&lt;/p&gt;
&lt;p&gt;The rule that keeps the run honest is that nothing counts as a finding until execution proves it. A sanitizer hit, a crash, or a static match is a lead, not a result; the discover state has to demonstrate adversary-maximal impact by execution before the orchestrator routes it to triage. When discover does prove a vulnerability the orchestrator triages that one, and whether it then concludes or keeps going is set by the task it was given. That gate is why the run reads as real engineering rather than a lucky first hit: it was not allowed to stop at the first plausible crash, so it did not.&lt;/p&gt;
&lt;h2 id=&#34;the-escape-as-it-happened&#34;&gt;The escape, as it happened&lt;/h2&gt;
&lt;p&gt;The run is exploitation, not discovery. The bug was already in hand, and the open question was where to point it to prove impact. The EDU primitive reads and writes host memory relative to the device&amp;rsquo;s DMA buffer, so turning memory corruption into code execution meant finding a function pointer the write could reach and the program would later call. The workflow spent its first several rounds on the obvious answer, a pointer in a neighboring object, and rejected two candidates in turn. Each time it built a harness, ran it, and confirmed that the corrupting write landed in an isolated region with no reachable dereference. Neither dead-end was argued away on paper. Each was killed by an executed harness that failed to produce a usable hijack.&lt;/p&gt;
&lt;p&gt;At round eight the orchestrator pivoted to the primitive it had not yet tried: the EDU device&amp;rsquo;s own memory. The insight, recorded in the discover state&amp;rsquo;s &lt;a class=&#34;link&#34; href=&#34;https://www.provos.org/p/qemu-escape-glm-5-2/artifacts/findings-h3.txt&#34; &gt;notes&lt;/a&gt;, is that the device&amp;rsquo;s embedded timer sits immediately before its DMA buffer in the same allocation:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;QEMUTimer dma_timer&lt;/code&gt; is embedded inline in &lt;code&gt;EduState&lt;/code&gt; and sits immediately BEFORE &lt;code&gt;char dma_buf[]&lt;/code&gt;, so edu&amp;rsquo;s OWN &lt;code&gt;dma_timer.cb&lt;/code&gt; is backward of &lt;code&gt;dma_buf&lt;/code&gt;. A wrapped negative offset reaches &lt;code&gt;dma_timer.cb&lt;/code&gt;. Corrupting &lt;code&gt;dma_timer.cb&lt;/code&gt; then re-arming and firing the timer calls the attacker-chosen gadget as &lt;code&gt;ts-&amp;gt;cb(ts-&amp;gt;opaque)&lt;/code&gt;.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;Instead of hunting for a neighbor with a usable function pointer, the workflow targeted the function pointer inside the overflowed object itself, which carries its own built-in, guest-paced dereference: the timer fire. That collapses the whole escape onto one self-contained, already-public bug. It is the elegant move, and it is the same one Xchg Labs landed on independently.&lt;/p&gt;
&lt;p&gt;The arc did not jump straight to victory. The first run reached full control of the host instruction pointer by round eleven but aimed it at an address that was not mapped, so the process faulted on the instruction fetch. That is a meaningful milestone: program-counter control proven across ten of ten ASLR runs, but not yet a working call. A continuation (&lt;a class=&#34;link&#34; href=&#34;https://www.provos.org/p/qemu-escape-glm-5-2/artifacts/ace-task.txt&#34; &gt;its task directive&lt;/a&gt;) closed the gap, characterizing the call site and selecting a real libc target, &lt;code&gt;system&lt;/code&gt;, and by round thirteen the validator&amp;rsquo;s own execution produced host code execution. The two runs together span thirteen rounds, each agent building and running its own harnesses. My part was the task definition and the approvals, not the exploit engineering.&lt;/p&gt;
&lt;h2 id=&#34;the-bug-and-the-exploit&#34;&gt;The bug and the exploit&lt;/h2&gt;
&lt;p&gt;QEMU&amp;rsquo;s EDU device exposes its DMA engine through BAR0 registers. The guest writes a source, a destination, and a count, then sets a command bit that arms a timer. When the timer fires, &lt;code&gt;edu_dma_timer()&lt;/code&gt; runs the transfer. The bounds check on that path, &lt;code&gt;edu_check_range()&lt;/code&gt;, returns on the in-bounds case and merely logs on the out-of-bounds one, with no clamp and no abort (&lt;code&gt;hw/misc/edu.c:106-125&lt;/code&gt;, verbatim):&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt; 1
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 2
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 3
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 4
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 5
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 6
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 7
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 8
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 9
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;10
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;11
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;12
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;13
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;14
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;15
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;16
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;17
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;18
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;19
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;20
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-c&#34; data-lang=&#34;c&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;static&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;void&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;edu_check_range&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;uint64_t&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;xfer_start&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;uint64_t&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;xfer_size&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;kt&#34;&gt;uint64_t&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;dma_start&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;uint64_t&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;dma_size&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kt&#34;&gt;uint64_t&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;xfer_end&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;xfer_start&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;xfer_size&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kt&#34;&gt;uint64_t&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;dma_end&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;dma_start&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;dma_size&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;cm&#34;&gt;/*
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cm&#34;&gt;     * 1. ensure we aren&amp;#39;t overflowing
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cm&#34;&gt;     * 2. ensure that xfer is within dma address range
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cm&#34;&gt;     */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;dma_end&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;dma_start&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;xfer_end&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;xfer_start&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;xfer_start&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;dma_start&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;xfer_end&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;lt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;dma_end&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nf&#34;&gt;qemu_log_mask&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;LOG_GUEST_ERROR&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                  &lt;span class=&#34;s&#34;&gt;&amp;#34;EDU: DMA range 0x%016&amp;#34;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;PRIx64&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;-0x%016&amp;#34;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;PRIx64&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                  &lt;span class=&#34;s&#34;&gt;&amp;#34; out of bounds (0x%016&amp;#34;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;PRIx64&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;-0x%016&amp;#34;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;PRIx64&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;)!&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                  &lt;span class=&#34;n&#34;&gt;xfer_start&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;xfer_end&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;dma_start&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;dma_end&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Its result is &lt;code&gt;void&lt;/code&gt;, and both callers ignore it. The transfer then computes a host buffer offset by subtracting the DMA window base (&lt;code&gt;edu_dma_timer&lt;/code&gt;, &lt;code&gt;hw/misc/edu.c:149-161&lt;/code&gt;, verbatim):&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt; 1
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 2
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 3
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 4
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 5
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 6
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 7
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 8
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 9
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;10
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;11
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;12
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;13
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-c&#34; data-lang=&#34;c&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;EDU_DMA_DIR&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;edu&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;dma&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;cmd&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;EDU_DMA_FROM_PCI&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kt&#34;&gt;uint64_t&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;dst&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;edu&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;dma&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;dst&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nf&#34;&gt;edu_check_range&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;dst&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;edu&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;dma&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;cnt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;DMA_START&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;DMA_SIZE&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;dst&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;DMA_START&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nf&#34;&gt;pci_dma_read&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;edu&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pdev&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;edu_clamp_addr&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;edu&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;edu&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;dma&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;src&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;n&#34;&gt;edu&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;dma_buf&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;dst&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;edu&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;dma&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;cnt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kt&#34;&gt;uint64_t&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;src&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;edu&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;dma&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;src&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nf&#34;&gt;edu_check_range&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;src&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;edu&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;dma&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;cnt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;DMA_START&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;DMA_SIZE&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;src&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;DMA_START&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nf&#34;&gt;pci_dma_write&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;edu&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pdev&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;edu_clamp_addr&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;edu&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;edu&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;dma&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;dst&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;n&#34;&gt;edu&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;dma_buf&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;src&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;edu&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;dma&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;cnt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;dst&lt;/code&gt; and &lt;code&gt;src&lt;/code&gt; are full 64-bit, guest-controlled values. After the log-only check, &lt;code&gt;dst -= DMA_START&lt;/code&gt; underflows on &lt;code&gt;uint64_t&lt;/code&gt; for any guest offset below &lt;code&gt;DMA_START&lt;/code&gt; (&lt;code&gt;0x40000&lt;/code&gt;), so &lt;code&gt;edu-&amp;gt;dma_buf + dst&lt;/code&gt; points before &lt;code&gt;dma_buf&lt;/code&gt; for a backward reach and arbitrarily far after it for a forward one. The companion masking function clamps only the guest-side address against the device&amp;rsquo;s DMA mask. It never touches the host offset. The guest DMA target stays a legal guest physical address while the host pointer goes wild. One bug, read in two DMA directions, gives both an out-of-bounds read (a leak) and an out-of-bounds write.&lt;/p&gt;
&lt;p&gt;What makes the timer-callback target reliable is the struct layout, which gdb resolved on the built object:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;2
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;3
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;offsetof(EduState, dma_timer) = 0xc88
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;offsetof(EduState, dma_buf)   = 0xcb8
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sizeof(QEMUTimer) = 0x30 ; offsetof(QEMUTimer, cb) = 0x10
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;CB_OFF = 0xcb8 - 0xc88 - 0x10 = 0x20   =&amp;gt; cb lives at  dma_buf - 0x20
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;dma_timer.cb&lt;/code&gt; sits at a fixed &lt;code&gt;-0x20&lt;/code&gt; from &lt;code&gt;dma_buf&lt;/code&gt;, and the object&amp;rsquo;s class pointer at &lt;code&gt;-0xcb8&lt;/code&gt;, both inside the same allocation with no page boundary or sanitizer redzone between them. Every address the exploit needs is a fixed offset from &lt;code&gt;dma_buf&lt;/code&gt;. It never needs an absolute host address, so it never reads &lt;code&gt;/proc/self/maps&lt;/code&gt; or calls &lt;code&gt;process_vm_readv&lt;/code&gt; on the exploit path. That is the &amp;ldquo;no host knowledge&amp;rdquo; constraint from the task, satisfied by the structure of the bug.&lt;/p&gt;
&lt;div class=&#34;edu-memlayout&#34;&gt;
&lt;svg viewBox=&#34;0 0 720 460&#34; role=&#34;img&#34; aria-label=&#34;EduState host-heap allocation. dma_timer.cb sits 0x20 bytes before dma_buf, and the class pointer 0xcb8 bytes before it. A guest DMA destination that underflows below DMA_START wraps backward onto dma_timer.cb, which is overwritten with system() and called when the timer re-fires.&#34;&gt;
  &lt;text class=&#34;edu-ml-frame-label&#34; x=&#34;44&#34; y=&#34;34&#34; font-size=&#34;15&#34;&gt;EduState allocation (host heap)&lt;/text&gt;
  &lt;rect class=&#34;edu-ml-frame&#34; x=&#34;28&#34; y=&#34;48&#34; width=&#34;400&#34; height=&#34;388&#34; rx=&#34;14&#34;/&gt;

  &lt;rect x=&#34;58&#34; y=&#34;72&#34; width=&#34;340&#34; height=&#34;66&#34; rx=&#34;10&#34; fill=&#34;#3a3a3a&#34;/&gt;
  &lt;text class=&#34;edu-ml-title&#34; x=&#34;228&#34; y=&#34;101&#34; font-size=&#34;19&#34; text-anchor=&#34;middle&#34;&gt;class pointer&lt;/text&gt;
  &lt;text class=&#34;edu-ml-sub&#34; x=&#34;228&#34; y=&#34;123&#34; font-size=&#34;13&#34; text-anchor=&#34;middle&#34;&gt;offset -0xcb8 from dma_buf&lt;/text&gt;

  &lt;rect x=&#34;58&#34; y=&#34;192&#34; width=&#34;340&#34; height=&#34;86&#34; rx=&#34;10&#34; fill=&#34;#7c2a1a&#34;/&gt;
  &lt;text class=&#34;edu-ml-title&#34; x=&#34;228&#34; y=&#34;221&#34; font-size=&#34;19&#34; text-anchor=&#34;middle&#34;&gt;dma_timer.cb&lt;/text&gt;
  &lt;text class=&#34;edu-ml-sub&#34; x=&#34;228&#34; y=&#34;242&#34; font-size=&#34;13&#34; text-anchor=&#34;middle&#34;&gt;offset -0x20 from dma_buf&lt;/text&gt;
  &lt;text class=&#34;edu-ml-sys&#34; x=&#34;228&#34; y=&#34;262&#34; font-size=&#34;13&#34; text-anchor=&#34;middle&#34;&gt;overwritten with system()&lt;/text&gt;

  &lt;rect x=&#34;58&#34; y=&#34;336&#34; width=&#34;340&#34; height=&#34;72&#34; rx=&#34;10&#34; fill=&#34;#0f6a5a&#34;/&gt;
  &lt;text class=&#34;edu-ml-title&#34; x=&#34;228&#34; y=&#34;367&#34; font-size=&#34;19&#34; text-anchor=&#34;middle&#34;&gt;dma_buf[]&lt;/text&gt;
  &lt;text class=&#34;edu-ml-sub&#34; x=&#34;228&#34; y=&#34;389&#34; font-size=&#34;13&#34; text-anchor=&#34;middle&#34;&gt;intended DMA target, offset 0&lt;/text&gt;

  &lt;line class=&#34;edu-ml-arrow&#34; x1=&#34;150&#34; y1=&#34;334&#34; x2=&#34;150&#34; y2=&#34;286&#34;/&gt;
  &lt;polygon class=&#34;edu-ml-arrowhead&#34; points=&#34;150,278 144,290 156,290&#34;/&gt;
  &lt;text class=&#34;edu-ml-arrow-label&#34; x=&#34;166&#34; y=&#34;313&#34; font-size=&#34;12&#34;&gt;backward write&lt;/text&gt;

  &lt;text class=&#34;edu-ml-outside&#34; x=&#34;58&#34; y=&#34;427&#34; font-size=&#34;13&#34;&gt;guest writes here when dst = DMA_START&lt;/text&gt;

  &lt;line class=&#34;edu-ml-dash&#34; x1=&#34;398&#34; y1=&#34;226&#34; x2=&#34;442&#34; y2=&#34;226&#34;/&gt;
  &lt;text class=&#34;edu-ml-note&#34; x=&#34;448&#34; y=&#34;222&#34; font-size=&#34;14&#34;&gt;re-armed timer later&lt;/text&gt;
  &lt;text class=&#34;edu-ml-note&#34; x=&#34;448&#34; y=&#34;241&#34; font-size=&#34;14&#34;&gt;calls this pointer&lt;/text&gt;

  &lt;text class=&#34;edu-ml-note&#34; x=&#34;448&#34; y=&#34;312&#34; font-size=&#34;14&#34;&gt;underflow: small dst&lt;/text&gt;
  &lt;text class=&#34;edu-ml-note&#34; x=&#34;448&#34; y=&#34;331&#34; font-size=&#34;14&#34;&gt;wraps backward 0x20 bytes&lt;/text&gt;
&lt;/svg&gt;
&lt;/div&gt;

&lt;p&gt;The exploit chain reads three static constants from the target&amp;rsquo;s glibc once, offline (the &lt;code&gt;&amp;amp;main_arena+96&lt;/code&gt; offset, the &lt;code&gt;system&lt;/code&gt; offset, and an arena page offset), then runs entirely from guest-side operations:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Leak and defeat ASLR.&lt;/strong&gt; Sweep the out-of-bounds read forward of &lt;code&gt;dma_buf&lt;/code&gt; and pull host heap bytes into guest memory. Free glibc chunks store a pointer to &lt;code&gt;&amp;amp;main_arena+96&lt;/code&gt;, so that qword recurs. Tallying canonical leaked values at the right page offset surfaces the arena pointer, which dominated the runner-up by roughly 380 to 1, and from it &lt;code&gt;libc_base&lt;/code&gt;, then &lt;code&gt;system&lt;/code&gt;. The selector is statistical and maps-free.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Snapshot&lt;/strong&gt; the device object backward of &lt;code&gt;dma_buf&lt;/code&gt;, preserving its fields.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Plant the command string&lt;/strong&gt; into the front of the object and write the snapshot back, corrupting the class pointer but leaving the timer callback intact, so the device keeps working.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Plant &lt;code&gt;system&lt;/code&gt; onto &lt;code&gt;dma_timer.cb&lt;/code&gt;&lt;/strong&gt; with an eight-byte write at &lt;code&gt;dst = DMA_START - 0x20&lt;/code&gt;, which underflows to &lt;code&gt;-0x20&lt;/code&gt; and lands exactly on the callback.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fire.&lt;/strong&gt; Re-arm the timer and advance virtual time. The main loop calls &lt;code&gt;cb(opaque)&lt;/code&gt;, now &lt;code&gt;system(EduState)&lt;/code&gt;, and &lt;code&gt;system&lt;/code&gt; reads its command from the bytes planted in step 3.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The workflow chose &lt;code&gt;system&lt;/code&gt; over a return-oriented chain for a concrete reason it read off &lt;a class=&#34;link&#34; href=&#34;https://www.provos.org/p/qemu-escape-glm-5-2/artifacts/callsite-edu.txt&#34; &gt;the call site&lt;/a&gt;. At the dereference, only &lt;code&gt;rdi&lt;/code&gt; is attacker-controlled:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;2
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;3
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;4
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;5
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;===== edu_dma_timer cb(opaque) CALL-SITE (the pivot target) =====
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;rdi(opaque=EduState)=0x635177d99ff0    &amp;lt;- ONLY attacker-controlled register
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;r13(cb=edu_dma_timer)=0x63516c2b8be0   &amp;lt;- the slot the write corrupts -&amp;gt; becomes the gadget
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;rsi=0x0 rdx=0x2000000000 ...
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;rsp=0x7ffdbb4119b0 rbp=0x7ffdbb4119f0  &amp;lt;- real stack, NOT attacker-controlled
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;PIE base = rip - 0x1391533 = 0x63516b68c000
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;A return-oriented program needs control of the stack or a pivot, and the dump shows neither: &lt;code&gt;rsp&lt;/code&gt; and &lt;code&gt;rbp&lt;/code&gt; are the real stack. But &lt;code&gt;system&lt;/code&gt; needs only &lt;code&gt;rdi&lt;/code&gt; to point at a command string, and the guest fully owns the bytes at &lt;code&gt;EduState&lt;/code&gt;, so the direct call is the whole exploit. The timing works out cleanly too. The corrupting fire has already cleared the run bit, and the timer machinery reads &lt;code&gt;cb&lt;/code&gt; into a local before calling it, so overwriting &lt;code&gt;cb&lt;/code&gt; mid-callback does not recurse. The next fire picks up &lt;code&gt;cb = system&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The workflow does not take the chain on faith. A separate validator state re-runs the harness rather than trusting the discover state&amp;rsquo;s account, and its &lt;a class=&#34;link&#34; href=&#34;https://www.provos.org/p/qemu-escape-glm-5-2/artifacts/ace-gdb-evidence.txt&#34; &gt;gdb capture&lt;/a&gt; shows &lt;code&gt;system&lt;/code&gt; reached through the hijacked callback, inside QEMU&amp;rsquo;s own timer machinery:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;2
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;3
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;4
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;Thread 1 hit Breakpoint 1, __libc_system (line=&amp;#34;echo edu-ace &amp;gt; /tmp/edu_ace_marker&amp;#34;) at ../sysdeps/posix/system.c:202
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;#0  __libc_system (line=...) at ../sysdeps/posix/system.c:202
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;#1  0x...96 in timerlist_run_timers (timer_list=...) at ../util/qemu-timer.c:593
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;#2  ... qemu_clock_run_timers (type=QEMU_CLOCK_VIRTUAL) at ../util/qemu-timer.c:607
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;#3  qemu_clock_advance_virtual_time (dest=1000000000) at ../util/qemu-timer.c:713
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;And it &lt;a class=&#34;link&#34; href=&#34;https://www.provos.org/p/qemu-escape-glm-5-2/artifacts/ace-reliability-matrix.txt&#34; &gt;reproduced across address-space randomization&lt;/a&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;2
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;3
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;4
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;5
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;6
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;7
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;#   leak        : 10/10  (100%)
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;#   cb_planted  : 10/10  (100%)
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;#   cmd_planted : 10/10  (100%)
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;#   ace_full    : 10/10  (100%)
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;# decision: FULL ACE: the host QEMU process executed the attacker-chosen action
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;#   system(&amp;#34;echo edu-ace &amp;gt; /tmp/edu_ace_marker&amp;#34;) in 10/10 ASLR runs -- marker created,
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;#   gadget addr leak-derived, cmd guest-planted. Reproducible.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Ten of ten, with the marker file written by the host process each time. The device source was unmodified throughout. The proof-of-concept wrapper asserts a clean &lt;code&gt;git status&lt;/code&gt; on the device files before each run, so the only thing driving the host process is guest-reachable register and DMA traffic against stock tutorial code. The point for this post is not that the callback target exists. It is that the workflow found a usable one only after execution rejected the easy ones, and that a separate validator, not the agent that found it, signed off. The workflow&amp;rsquo;s &lt;a class=&#34;link&#34; href=&#34;https://www.provos.org/p/qemu-escape-glm-5-2/artifacts/report-h3.txt&#34; &gt;complete write-up of the finding&lt;/a&gt; records the chain, the call-site analysis, and the evidence in full.&lt;/p&gt;
&lt;h2 id=&#34;the-argument-made-concrete&#34;&gt;The argument, made concrete&lt;/h2&gt;
&lt;p&gt;This is the part that matters for the larger argument. In my &lt;a class=&#34;link&#34; href=&#34;https://www.provos.org/p/finding-zero-days-with-any-model/&#34; &gt;earlier run&lt;/a&gt;, a frontier model found a serious bug and then stopped when exploit development crossed its acceptable-use boundary. I had to break the work into a seven-step plan, and the model completed two steps before declining the rest. Here the same workflow ran from analysis through a working host exploit without a refusal, because the model&amp;rsquo;s policy is mine. For legitimate defensive research, that is the difference between a finding and a stall. A maintainer cannot rank a vulnerability from a static report, and most static findings are false positives; the maximal-impact proof of concept is what separates the real bug from the noise. Producing it is defensive work.&lt;/p&gt;
&lt;p&gt;The target makes the point safe to make. EDU is tutorial code, already public, in no production build, so a full proof of concept against it costs no one anything. The capability to carry an exploit end to end now rides on an open-weight model that no vendor can refuse on your behalf, recall by export order, reprice after you depend on it, or &lt;a class=&#34;link&#34; href=&#34;https://www.provos.org/p/case-for-open-weight-models/&#34; &gt;silently degrade&lt;/a&gt; under a classifier you cannot inspect. A well-resourced adversary already works this way. A defender who depends on a frontier API accepts a policy and availability handicap the adversary does not.&lt;/p&gt;
&lt;p&gt;Everything technical above is QEMU&amp;rsquo;s EDU device: tutorial code, attached only with an explicit &lt;code&gt;-device edu&lt;/code&gt;, on no default machine and in no production build, and already published in full by Xchg Labs. There is no CVE and nothing to report upstream. The same investigation also turned up out-of-bounds read and write primitives in devices that do ship in real configurations; those were harder to drive to a full host exploit than the EDU chain, and they stay withheld while coordination runs. I keep this approach throughout this article: show the published, harmless case in full, and say nothing concrete about the rest until it is fixed.&lt;/p&gt;
&lt;p&gt;The final result: an open-weight model, inside a trust boundary I control, drove a set of confirmed primitives to a reproducible proof-of-concept exploit with the workflow unchanged and the device source untouched. The artifacts track rounds and verdicts rather than wall-clock time, so the token meter is the best measure of effort: roughly 789 million tokens over five days for the whole QEMU investigation, the figure in the chart below, low enough at open-weight rates to make broad, repeated auditing routine.&lt;/p&gt;


&lt;img src=&#34;https://www.provos.org/p/qemu-escape-glm-5-2/z_ai_token_burn_hu_f5c4d4021a72d40f.jpg&#34; width=&#34;800&#34; height=&#34;425&#34;&gt;
&lt;p&gt;The model still matters. It has to be good enough to form real hypotheses and engineer a chain, and GLM 5.2 is. Even so, the model found a working exploit, not the most elegant one: where it recovered the &lt;code&gt;system&lt;/code&gt; address the long way, through a statistical sweep of heap pointers, Xchg Labs derived it straight from the code pointer the same overflow already leaked. On this device, a careful human still beat the harness-driven model to the cleaner line. What the workflow adds is long-horizon coherence and a refusal to cut corners, which is what lets a model that is not a restricted frontier model finish the job. GLM 5.1 already found vulnerabilities; GLM 5.2, more capable, finds them better. Both are open-weight, so the model that did this is one you can audit, pin, and keep. The earlier posts on &lt;a class=&#34;link&#34; href=&#34;https://www.provos.org/p/finding-zero-days-with-any-model/&#34; &gt;orchestration&lt;/a&gt;, &lt;a class=&#34;link&#34; href=&#34;https://www.provos.org/p/case-for-open-weight-models/&#34; &gt;open weights&lt;/a&gt;, and &lt;a class=&#34;link&#34; href=&#34;https://www.provos.org/p/day-after-the-zero-days/&#34; &gt;structural invariants&lt;/a&gt; made the case; this run is the demonstration.&lt;/p&gt;
&lt;p&gt;&lt;a class=&#34;link&#34; href=&#34;https://github.com/provos/ironcurtain&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;IronCurtain&lt;/a&gt; is open source. The workflow, the gateway shim, and the evidence gates are in the repository, and the run above used them unchanged.&lt;/p&gt;
</description>
        </item>
        
    </channel>
</rss>
