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

P packet out-of-bounds stack write via NUM_REGS/struct mismatch and missing lower bound

Summary

P (set one register) packet validates register index only with regno<NUM_REGS where NUM_REGS hardcoded 22 but struct x86_64regs only 152 bytes = 19 eight-byte slots. Indices 19 20 21 write 8 bytes each past end of registers stack local. hexToInt() produces signed int with no sign rejection so parsed value 0xffffffff yields regno=-1 which passes regno<22 and writes 8 bytes at &registers-8. P handler hex2mem(ptr (vm_offset_t)&registers+regno*8 8). Result kernel-stack OOB write of attacker-controlled bytes at attacker-controlled offsets. Corrupts adjacent locals saved rbp return address. Attacker: GDB debug console holder (serial sio 0x80 flag dcons/FireWire) while kernel in GDB stub. [VERIFIED pass2: reproduced against the REAL stock stub in the shipped kernel over a serial debug console - g reply proves sizeof registers 152 vs NUM_REGS 22, P13-P15 land exact attacker bytes at +152 +160 +168, Pffffffff passes as regno -1 writing at registers-8, Pffffffe0 reaches registers-0x100, saved-register slot write at +152 kills the VM on resume twice; RIP control itself out of reach on this gcc -O2 layout (retaddr at +208) and kernel image 127TiB above kstack; console-holder reach only, no unpriv network path; bounds fix-validated]

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2361 Β· 15 files
FileTypeDescriptionSize
poc2361.py β€” 8.8 KB view raw
poc2361rip.py β€” 3.0 KB view raw
rig236.py β€” 5.1 KB view raw
rsp.py β€” 6.1 KB view raw
rig-attach.diff β€” 972 B view raw
fix.diff β€” 1.7 KB view raw
run.log β€” 34.8 KB view raw
run.raw.log β€” 124.1 KB view raw
run.fixed.log β€” 34.9 KB view raw
rip.log β€” 18.6 KB view raw
rip.raw.log β€” 17.9 KB view raw
env.txt β€” 409 B view raw
probe.py β€” 1.4 KB view raw
kmemv.c β€” 1.3 KB view raw
exp_p_oob.py β€” 6.8 KB view raw

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff (regno >= 0 && regno < sizeof(registers)/8) applied in-guest, kernel rebuilt and installed: P13/P14/P15, Pffffffff and Pffffffe0 all rejected with P01 and no memory change (run.fixed.log), in-bounds P12 still returns OK with exact bytes, protocol healthy, clean detach. Baseline and fixed runs on otherwise identical kernels.

run.fixed.log [S2]-[S5] blocks; fix.diff in this pack applies clean to the read-only sys/ tree (git apply --check)
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #2: Mon Oct 19 01:25:11 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

console access -> sysctl debug.enter_debugger=gdb (or ddb gdb / panic with RB_GDB) -> stub T05 handshake -> P packet with regno 19/20/21 or negative regno -> 8 attacker-chosen bytes at &registers+152..168 or &registers+negative_offset (up to 16 GiB below the kernel stack, 8-byte aligned) -> saved-register corruption -> D -> kernel does not resume, VM dies (controlled-RIP not achievable on this build: return address at +208 is out of positive-regno reach)

Evidence (decisive lines)

run.log lines [S0]-[S6] (g=304 hex chars proving sizeof 152; P12 in-bounds control; P13/P14/P15 exact OOB bytes at +152/+160/+168 with struct untouched; Pffffffff OK proving regno=-1 accepted; Pffffffe0 exact marker at A-0x100); rip.log + rip.raw.log (P13=4141414141414141 + D -> VM death, frame autopsy showing saved regs and retaddr at +208); run.fixed.log (all OOB P packets rejected with P01, in-bounds still OK after fix.diff)

PoC changes

Rewrote the seed exp_p_oob.py into a single-session rig (rig236.py + poc2361.py): fixed the T05 field endianness (mem2hex emits little-endian register bytes; int(hex,16) mis-decoded rsp and read unmapped memory), added the P25-rejected control (hex 19 = 25 dec, proving the upper bound works), re-based the in-bounds control to slot 18 dec (P12; the seed used hex 18 = 24 dec which is rejected), documented A-8/A-64/A-128 as per-packet transient spill slots (the marker writes land but the handler re-spills ptr/locals there every packet), and added poc2361rip.py for the destructive saved-register corruption. rig-attach.diff (tty_cons.c only) attaches gdb_tab because hint.sio.0.flags=0x90 demonstrably does not on this ACPI guest.

Verified recommended fix

P handler: require regno >= 0 && regno < (int)(sizeof(registers)/8) (19 slots), replacing the unbounded regno < NUM_REGS(22) check

Verdict

Reproduced on the live stock kernel: the P handler guard regno < NUM_REGS(22) vs the 152-byte (19-slot) struct x86_64regs plus the missing regno >= 0 lower bound give an attacker on the kernel debug console an arbitrary-offset fully controlled 8-byte kernel-stack write: regno 19/20/21 write at &registers+152/+160/+168 (0-24 bytes past the object, struct untouched, exact attacker bytes verified), hexToInt of ffffffff yields -1 which passes regno < 22 (stub replies OK; write issued at &registers-8), and regno -32 lands an exact marker 256 bytes below the object. Destructive proof: 0x4141414141414141 into the saved-register slot at +152 then detach (D) - the kernel never resumes and the VM dies (twice reproduced); the identical session without the corruption resumes cleanly. On this gcc -O2 build the return address sits at &registers+208, outside regno 19-21 reach, and the kernel image lies ~127 TiB above the kstack region, so the negative direction covers the 16 GiB of kernel-map memory below the stack rather than the image. Reach requires the debug console (sio flags 0x80/0x90, dcons, or DDB access): operator/physical, no unprivileged or network path, hence Medium/dos with a genuine memory-corruption primitive inside that boundary.