synchronize_rcu() expands to zero instructions β zero-length grace period lets drm callers kfree objects under live readers (UAF β kernel RIP control, demonstrated)
Summary
The shim defines synchronize_rcu() away entirely (rcupdate.h:94) - the canonical rcu_assign_pointer/synchronize_rcu/kfree teardown frees immediately while readers are provably in-section. Verified with the SHIPPED drm.ko symbols: witness compiles to a single retq; handshake-proven reader saw freed content change mid-section 4-5/5; controlled slab reclaim + function-pointer call through the freed object; rip_panic run trapped at the attacker-chosen RIP (Fatal trap 12, ip=0xffffffff52170000). In-tree victims i915_gem.c:3713/:3731. Full uid0 chain blocked on this guest only by absent GPU hardware. Real-grace-protocol fix validated in-guest (every hit to 0).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5125 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| rcu_race.c | β | 11.3 KB | view raw | |
| Makefile | β | 217 B | β download | |
| build.sh | β | 89 B | view raw | |
| run.sh | β | 187 B | view raw | |
| run_fixed.sh | β | 523 B | view raw | |
| build.log | β | 5.6 KB | view raw | |
| run.log | β | 966 B | view raw | |
| run.2.log | β | 547 B | view raw | |
| run.fixed.log | β | 1.3 KB | view raw | |
| panic.txt | β | 1.1 KB | view raw | |
| env.txt | β | 514 B | view raw | |
| dmesg.txt | β | 7.3 KB | view raw | |
| drmbuild.log | β | 25.0 KB | view raw | |
| fix.diff | β | 2.5 KB | view raw | |
| VERDICT.md | β | 6.0 KB | β raw | |
| verdict.json | β | 4.7 KB | view raw |
DF-5125 VERDICT β synchronize_rcu() empty macro
Status: REPRODUCED (UAF + kernel RIP control; fix validated)
Root cause (source-certain)
The DragonFly drm LinuxKPI implements Linux RCU as a per-CPU delay line
(sys/dev/drm/linux_rcu.c): call_rcu()/__kfree_rcu() queue elements that
rcu_timer() executes after delta >= hz (linux_rcu.c:81-83). There is no
synchronize_rcu() implementation anywhere in linux_rcu.c; instead the
header defines it away:
sys/dev/drm/include/linux/rcupdate.h:94:
#define synchronize_rcu()
rcu_read_lock()/rcu_read_unlock() are preempt_disable()/preempt_enable()
(rcupdate.h:46-56) which are cpu_ccfence() β a compiler fence with no
runtime semantics (sys/dev/drm/include/linux/preempt.h:37-38,
sys/cpu/x86_64/include/cpufunc.h:219-227).
Consequence: the canonical Linux teardown contract β "after
synchronize_rcu() returns, no pre-existing reader can still hold the old
pointer" β is vacuously violated: the call returns instantly and the caller
typically kfrees the old object on the next line, racing every reader on every
other CPU that is mid-critical-section. In-tree callers of the broken macro:
sys/dev/drm/i915/i915_gem.c:3713 and :3731 (the shim's
get_state_synchronize_rcu()/cond_synchronize_rcu() pair used by
i915_request.c:761 is likewise a no-op/mfence).
Mechanical proof
The harness ships a witness function df_sync_witness(void){ synchronize_rcu();
kfree((void*)0); }. Compiled against the shipped headers it is a single
retq (build.log, objdump):
0000000000000970 <df_sync_witness>: 970: c3 retq
After fix.diff it contains a real call (jmpq to the external
synchronize_rcu).
Runtime proof (stock /boot/kernel/drm.ko, guest 6.5-DEVELOPMENT, 6 CPUs)
Stage A (DF-5125 contract test): reader thread pinned to CPU2 runs the exact
canonical read-side pattern; handshake proves it holds the pointer inside its
critical section; writer on CPU0 then runs rcu_assign_pointer(gp,v2);
synchronize_rcu(); kfree(v1); and reclaims v1's slot with poisoned content.
All RCU primitives are the shipped shim's (headers + drm.ko symbols
__kfree_rcu/call_rcu); only the loops are local.
Results (3 full runs + extra rounds):
| run | stageA uaf hits | stageB uaf hits | poison reads | canary fp-calls |
|---|---|---|---|---|
| 1 | 4/5 | 3/3 | 3 | 3 |
| 2 | 5/5 | 3/3 | 3 | 3 |
| 3 | 5/5 (8-spray) | 3/3 | 4 | 4 |
| extra rounds | 10/10 | β | β | β |
The reader observed the freed object's contents change while provably inside its read-side critical section in every single round.
Impact demonstration β kernel RIP control through the freed object
With rcu_race.rip_panic=1 the reclaimed content sets the freed object's
function pointer to 0xffffffff52170000 (canonical, unmapped). The reader's
v->v_fp(v) β the drm idiom (e.g. fence release paths) β jumped there:
Fatal trap 12: page fault while in kernel mode cpuid = 2; lapic id = 2 fault virtual address = 0xffffffff52170000 instruction pointer = 0x8:0xffffffff52170000 fault code = supervisor read instruction, page not present panic: page fault --- trap 000000000000000c, rip = ffffffff52170000 ---
That is arbitrary kernel instruction-pointer control from the UAF: the kernel executed an address that existed only as data written into reclaimed freed memory.
Exploit chain / ceiling
Demonstrated chain: reader in CS -> zero-instruction synchronize_rcu() ->
immediate kfree under reader -> slab reclaim with controlled content ->
reader dereference (info read) and indirect call (RIP control). With a real
DRM victim object (the pattern is used by i915/amdgpu/scheduler/reservation
code compiled into the shipped drm.ko and the gpu driver modules) and
user-controlled reclamation content this is the classic path to uid=0. On
this QEMU guest there is no GPU and no render node, so the shipped call
sites are unreachable end-to-end; that is a hard environment blocker
(documented), not a mitigation β the primitive itself (UAF + RIP control with
the shipped shim) is fully reproduced here.
Fix validation
fix.diff (in this pack): declares extern void synchronize_rcu(void) in
rcupdate.h, implements it in linux_rcu.c (waits >= 2 delay periods AND until
rcu_readers == 0), counts read-side critical sections
(rcu_read_acquire/rcu_read_release wired into rcu_read_lock/unlock), and
makes rcu_timer() defer reclamation while any reader is in flight.
Applied to the guest's /usr/src, drm.ko rebuilt
(/usr/src/sys/dev/drm/drm), harness rebuilt against the fixed headers, exact
same PoC re-run:
| baseline (stock) | patched (fix.diff) | |
|---|---|---|
| stageA uaf | 4-5/5 (10/10 extra) | 0 (10 rounds) |
| stageB uaf | 3/3 | 0 (6 rounds) |
| canary / RIP control | 3-4 (fp-call), fatal trap | 0, guest stays up under rip_panic=1 |
Bad behavior gone; fix validated.
Harness notes / honest caveats
- Two earlier harness iterations produced null/false results (a
DECLARE_MODULEmisuse panickingmodule_register's strcmp; same-CPU pinning + spin handshake starving the reader). Both were harness bugs, diagnosed from the DDB backtrace and fixed; the final harness (this pack) pins writer=cpu0/reader=cpu2 and tsleep-polls handshakes. - Stage A's exact-poison reclaim is probabilistic (~1/5 with 1 block, ~4/5 with 8-spray); stage B's is deterministic (3/3 in every run). UAF detection (contents changed under the reader) was 100% in runs 2 and 3.
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
fixedfix.diff applied to guest /usr/src; drm.ko rebuilt (nm: synchronize_rcu/rcu_read_acquire/rcu_read_release present; df_sync_witness now calls synchronize_rcu instead of bare ret); identical harness rebuilt against fixed headers: stageA_uaf 0 (10 rounds), stageB_uaf 0 (6 rounds), canary 0, guest survived rip_panic=1 triggers. Previously-observed UAF/RIP behavior GONE.
run.fixed.log; drmbuild.log; build.log (fixed witness disassembly); fix.diff
Confirmed kernel references
Detail
Exploit chain
reader pinned cpu2: rcu_read_lock(); v=rcu_dereference(gp) [handshake proves pointer held mid-CS] -> writer cpu0: rcu_assign_pointer(gp,v2); synchronize_rcu() [compiles to nothing]; kfree(v1) -> same-cpu slab LIFO reclaim of v1 with controlled content (magic+fp) -> reader reads poisoned magic (UAF read) and calls v->v_fp (kernel indirect call through data in freed+reclaimed memory) -> with fp=0xffffffff52170000: Fatal trap 12, instruction pointer = 0xffffffff52170000 (arbitrary kernel RIP control from the UAF). Full uid=0 chain blocked on this guest only by absent GPU hardware (no render node -> shipped i915/amdgpu call sites unreachable end-to-end); primitive itself fully reproduced.
Evidence (decisive lines)
run.log/run.2.log (baseline counters: stageA_uaf 4/5 then 5/5, stageB_uaf 3/3, canary 3), panic.txt (Fatal trap 12, rip=0xffffffff52170000), build.log (df_sync_witness = single retq), run.fixed.log (post-fix: all 0, guest up under rip_panic triggers), fix.diff
PoC changes
Rewrote the seeded concept: (1) out-of-tree kmod (SYSDIR=/usr/src/sys, drm include path added AFTER kmod.mk include because kmod.mk resets CFLAGS); (2) fixed DECLARE_MODULE misuse (moduledata_t struct, not handler fn -- the misuse itself panics module_register's strcmp); (3) pinned writer=cpu0 reader=cpu2 and replaced spin handshakes with tsleep-poll (same-CPU spin starved the reader via LWKT cooperative scheduling); (4) removed kprintf from CS hot paths; (5) 8/32-block poison reclaim sprays; (6) sysctl triggers (more/moreb/rip_panic) for reruns and the wild-fp run.
Verified recommended fix
Implement a real synchronize_rcu() in linux_rcu.c (wait >=2 delay periods AND until the new read-side nesting counter is 0), count read-side critical sections in rcu_read_lock/unlock, and defer rcu_timer reclamation while readers are in flight -- see fix.diff.
Verdict
synchronize_rcu() in the DragonFly drm LinuxKPI expands to zero instructions (rcupdate.h:94; linux_rcu.c provides no implementation), so the canonical teardown (swap pointer; synchronize_rcu(); kfree(old)) frees objects immediately while readers on other CPUs are provably still inside their read-side critical sections. Reproduced on the 6-CPU QEMU guest with the shipped drm.ko shim: UAF detection in 4-5/5 rounds (10/10 in extra rounds), controlled content read through the freed object, a function pointer written into the reclaimed freed memory invoked by the kernel (canary x3), and a fatal trap with RIP = the attacker-written pointer value (0xffffffff52170000). In-tree callers include i915_gem.c:3713/3731. fix.diff (real synchronize_rcu + read-side accounting + grace deferral) drives all hits to 0 on the identical harness.
No comments yet.