β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-5125

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5125 Β· 16 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download 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_MODULE misuse panicking module_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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.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
↓ fix.diffper-fix-DF-5125

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.