DF-3879 / verdict.json
{ "finding_id": "DF-3879", "status": "reproduced", "reproduced": 1, "impact": "panic", "confidence": "certain", "verdict": "REPRODUCED on the live kernel after making the path executable by attaching QEMU emulated Intel-HDA audio (-device intel-hda -device hda-duplex) to the audit guest: DragonFlys snd_hda.ko binds the QEMU ICH6 controller (8086:2668) + hda-duplex codec (0x1af40022), creating pcm0 and /dev/dsp, /dev/dsp0 both 0666 root:wheel. As unprivileged uid=1001: (1) the filed PoC (mmap off=0xffffffff10000000 size=0xf0000000) deterministically PANICS the running INVARIANTS kernel DURING the mmap syscall with panic: sndbuf_getbufofs: ofs invalid 268435456 (268435456 = 0x10000000 = exactly the attacker-chosen ofs) and the backtrace kern_mmap -> vm_mmap -> dev_dmmap_single -> dsp_mmap_single -> sndbuf_getbufofs \u2014 the findings chain word for word (panic.txt). (2) A second variant (off=0xffffffff00000000 = -2^32, size=4GiB) proves the check bypass reaches the pager sink on a path with ZERO INVARIANTS code: the old-style device pager ctor (device_pager.c old_dev_pager_ctor) validates every page via dev_dmmap -> dsp_mmap -> vtophys, and the walk faults in pmap_kextract on a garbage PDE (Fatal trap 12, fault VA 0xffff80403e005000, Stopped at pmap_kextract.part.23+0x16) \u2014 deterministic across boots, fires identically on production non-INVARIANTS builds (panic.pmap.txt). MECHANISM CORRECTION discovered during verification: vm_ooffset_t is SIGNED (typedef __int64_t, sys/cpu/x86_64/include/types.h:44) \u2014 the attack is an undetected NEGATIVE mmap offset (0xffffffff10000000 == -0xF0000000), not a 2^64 unsigned wrap; kern_mmap does no sign/range check for VCHR offsets. All PoC numbers and both panics unchanged. FIX: the originally-proposed fix.diff (v1, signed compares) was built and PROVEN INSUFFICIENT \u2014 variant B still panicked on the v1 module (signed jg compare lets negative offsets through); the corrected v2 fix compares unsigned ((uint64_t)*offset > (uint64_t)sndbuf_getsize(...) || size > (uint64_t)sndbuf_getsize(...) - (uint64_t)*offset) and was kernel-build-validated on the guest (sound.ko module \u2014 note dsp.c lives in the module, not the kernel binary): variant B -> EINVAL (was panic), variant A2 -> EINVAL (was pmap_kextract fatal trap), legit mmap(off=0, size=bufsize) still works RW (feature preserved), guest stays up across all runs. RW-alias ceiling (arbitrary kernel memory within 4GB of the sound soft buffer, layout-dependent: aliasable window is [b->buf+4GB-S, b->buf+4GB) for mapping size S, every ctor-validated page must be mapped KVA) documented in VERDICT.md with the root-side /dev/kmem oracle plan (kmemprobe.c); left as follow-up. uid=0 not demonstrated.", "exploit_chain": "unprivileged user -> open /dev/dsp O_RDWR (0666) -> OSS SETFMT/CHANNELS/SPEED (allocates bufsoft, bufsize 65536 observed) -> mmap(NULL, size=S, PROT_READ|PROT_WRITE, MAP_SHARED, fd, off = t-S [NEGATIVE, t<S<=bufsize]) -> kern_mmap (no VCHR offset sign/range check, vm_mmap.c:195-197) -> dev_dmmap_single -> dsp_mmap_single: signed sum (*offset+size)=t <= bufsize so dsp.c:2342 PASSES -> sndbuf_getbufofs(c->bufsoft, *offset) truncates to u32 ofs = 2^32-S+t (buffer.c:424, KASSERT-only bound) -> wild = b->buf + ofs -> dev_pager_alloc old-style pager (dsp.c:2357; ctor validates every page via dev_dmmap -> dsp_mmap -> vtophys and never rejects because dsp_mmap always returns 0, dsp.c:2287-2294). Outcome attacker-shaped by S/t: (a) any page of [wild, wild+S) unmapped -> pmap_kextract walks a garbage PDE -> unrecoverable kernel page fault DURING mmap (INVARIANTS-independent, reproduced twice, deterministic) \u2014 local DoS; (b) INVARIANTS kernels with ofs >= bufsize -> KASSERT panic sndbuf_getbufofs: ofs invalid (reproduced); (c) all pages mapped (heap-layout dependent, window [b->buf+4GB-S, b->buf+4GB)) -> mmap succeeds and old_dev_pager_fault enters pmap_kextract(wild+n*PAGE) frames read/write into user PTEs -> arbitrary kernel-memory R/W within 4GB of b->buf -> spray/groom a victim object (e.g. cred-bearing structure) -> uid=0. (c) not demonstrated on this guest; (a)+(b) proven.", "evidence": "panic.txt (KASSERT panic + full backtrace from unprivileged run); panic.pmap.txt (Fatal trap 12 in pmap_kextract reached through the pager during mmap, INVARIANTS-independent); run.log (narrative of all baseline runs); env.audio.txt (pcm0 attached via QEMU intel-hda + hda-duplex, /dev/dsp 0666); fix_run_variantB.log + fix_run_variantA_sanity.log (fix-v2 matrix: both attack variants -> EINVAL, legit mmap still RW, guest up); fix_env.txt + fix_build.log (kernel #1 + fixed sound.ko md5 6f5280a1ded4e7b516549eefdc8f1735); fix.diff (v2 unsigned-compare fix, git-apply clean); VERDICT.md (full story incl. signed-type correction, v1-insufficiency proof with objdump jg-vs-ja, attack geometry, /dev/kmem oracle plan)", "kernel_refs": [ "sys/dev/sound/pcm/dsp.c:2342", "sys/dev/sound/pcm/dsp.c:2355", "sys/dev/sound/pcm/dsp.c:2357", "sys/dev/sound/pcm/dsp.c:2287", "sys/dev/sound/pcm/dsp.c:2506", "sys/dev/sound/pcm/buffer.c:424", "sys/dev/sound/pcm/channel.c:1247", "sys/dev/sound/pcm/channel.h:402", "sys/cpu/x86_64/include/types.h:44", "sys/vm/vm_mmap.c:195", "sys/vm/vm_mmap.c:1377", "sys/kern/kern_device.c:286", "sys/vm/device_pager.c:174", "sys/vm/device_pager.c:326", "sys/vm/device_pager.c:351", "sys/platform/pc64/x86_64/pmap.c:1865", "sys/dev/sound/pcm/sound.c:1420" ], "poc_changes": "mmap_ofs_overflow.c unchanged from prior pass (its userspace replica + live tail; the live tail now actually executes on the audio-attached guest). ADDED: mmap_alias_rw.c (wrapped -2^32 offset + OSS setup + cross-view alias proof attempt; on this guest its two baseline runs proved the pmap_kextract ctor-walk panic instead), mmap_onepage.c (small-S attack shape for follow-up RW-alias work), kmemprobe.c (root-side /dev/kmem mapped-ness oracle), sanity_mmap.c (legit-feature regression check for fix validation). The QEMU guest was booted with -audiodev none,id=snd0 -device intel-hda -device hda-duplex,audiodev=snd0 via a temporary launcher (dfbsd-qemu/vm.sh untouched); snd_hda kldloaded as root; all attack runs as uid=1001.", "attempts": 7, "guest_uname": "baseline: DragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (INVARIANTS); fix validation: DragonFly 6.5-DEVELOPMENT #1: Wed Sep 16 19:37:17 UTC 2026 + fixed sound.ko", "runtime_sec": 4800, "guest_dirty": 1, "build_cmd": "guest: cc -O2 -Wall -o /tmp/mmap_ofs /tmp/mmap_ofs_overflow.c && cc -O2 -Wall -o /tmp/mmap_alias_rw /tmp/mmap_alias_rw.c && cc -O2 -Wall -o /tmp/sanity_mmap /tmp/sanity_mmap.c (fix kernel: cd /usr/src && make -j 6 nativekernel KERNCONF=X86_64_GENERIC)", "run_cmd": "dfbsd-qemu/vm.sh run_user /tmp/mmap_ofs; echo exit=$? (baseline: ssh dies, serial console shows panic; fix-v2: mmap: Invalid argument (errno=22), guest up)", "code_hash": "67b16bbdcc1e993d98edfccafe2a7e988aa2176d740f3ba3e9917c999ab455f1", "notes": "Two verification discoveries that supersede parts of the filed record: (1) vm_ooffset_t is SIGNED (types.h:44) so the bypass is a negative mmap offset, not a u64 wrap \u2014 same PoC numbers; (2) the originally-filed fix (v1) compiles clean yet does NOT block the attack (signed compare jg in objdump; variant B still panicked on the v1 module \u2014 proven empirically), so the authoritative fix is the v2 unsigned-compare form in fix.diff, kernel-validated. Also: the old-style device pager CTOR validates every page of the mapping upfront (device_pager.c:326-333), which shapes the attack geometry (window [b->buf+4GB-S, b->buf+4GB), all S/PAGE pages must be mapped) and makes large-S attacks deterministically panic in pmap_kextract during mmap \u2014 an INVARIANTS-independent production-kernel DoS. dsp.c lives in sound.ko (module), not the kernel binary \u2014 fix validation required installing the rebuilt module; a bare kernel rebuild changes nothing. QEMU audio attach recipe documented in README.md for reproducibility. Guest left needing reset (kernel #1 + fixed module installed; /tmp artifacts) \u2014 reset with-src performed at end of run.", "recommended_fix": "Unsigned bounds check in dsp_mmap_single (dsp.c:2342): (uint64_t)*offset > (uint64_t)sndbuf_getsize(c->bufsoft) || size > (uint64_t)sndbuf_getsize(c->bufsoft) - (uint64_t)*offset \u2014 rejects negative/wrapped offsets; kernel-build-validated (v1 signed form proven insufficient); optionally also bounds-check inside sndbuf_getbufofs without relying on KASSERT", "fix_status": "fixed", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "v2 fix validated at kernel level on the guest: baseline (stock X86_64_GENERIC INVARIANTS + QEMU HDA pcm0) reproduces both panic signatures from uid=1001; patched (kernel #1 + rebuilt sound.ko md5 6f5280a1ded4e7b516549eefdc8f1735 with the unsigned-compare check, ja/jb confirmed by objdump) rejects both attack variants with EINVAL and preserves the legitimate mmap(off=0, size=bufsize) feature (RW verified), guest stable across all runs. IMPORTANT: the originally-filed v1 fix was built and empirically proven INSUFFICIENT (signed compare lets the negative offset through; variant B still panicked) \u2014 v2 supersedes it.", "fix_evidence": "fix.diff (v2); fix_run_variantB.log; fix_run_variantA_sanity.log; fix_env.txt; fix_build.log; VERDICT.md (objdump jg-vs-ja analysis of v1 vs v2)" } |