DF-2878 / verdict.json
{ "finding_id": "DF-2878", "status": "reproduced", "reproduced": 1, "impact": "leak", "confidence": "certain", "verdict": "REPRODUCED 3/3 (deterministic, fresh image). l32_setdisklabel (sys/kern/subr_disklabel32.c:249-315) installs an in-core disklabel32 via DIOCSDINFO32/DIOCWDINFO32 with no structural bounds validation: d_partitions[].p_offset is never checked against the slice (only p_size <= ds_size is, at :310-313) and d_npartitions is never capped. dscheck then translates partition-node I/O to (ds_offset + p_offset + secno) * secsize, so /dev/vn0s1a with p_offset=61796 on a 61696-sector slice read and wrote absolute sectors 63844/63845 OUTSIDE the slice (verified in the backing image). The label64 twin validates exactly this on the same ioctl (subr_disklabel64.c:275-306) and the label32 reader/writer enforce it (l32_fixlabel :593-609); the set path is the single unguarded twin. Impact enum caveat: 'leak' is the closest kernel-centric value; the demonstrated primitive is an out-of-slice READ+WRITE on the underlying device (data boundary escape), NOT a kernel-memory disclosure. Trigger requires a credential that can open the cooked slice node (SYSCAP_RESTRICTEDROOT, subr_disk.c:1072) i.e. root-class on a stock host; the boundary matters for delegated slice devices (jail/devfs, VM passthrough), hence severity Low per rubric.", "exploit_chain": "open /dev/vn0s1 O_RDWR (whole-slice partition, FWRITE) -> ioctl DIOCSDINFO32 with crafted 404-byte label {magic, dkcksum ok, d_npartitions=3, 'c'(RAW_PART): p_offset=0 p_size=61696, 'a': p_offset=61796 p_size=8 fstype=FS_OTHER} -> accepted unvalidated (subr_disklabel32.c:306-313 checks pass) -> open existing /dev/vn0s1a node -> pread(0) returns marker bytes planted at absolute sector 63844 (outside slice [2048,63744)) -> pwrite(512) lands at absolute sector 63845 in the backing image (verified via the backing file; sector zeroed before). No kernel memory corruption; the primitive is device-scope escape.", "evidence": [ "run.log / run.2.log / run.3.log: 'DIOCSDINFO32: accepted', 'READ-ESCAPE: PASS', 'WRITE-ESCAPE: PASS', 'DF-2878 REPRODUCED' (run 3 on a pristine image, escape sector verified zero beforehand)", "run.fixed.log: patched kernel #1 -> 'DIOCSDINFO32: REJECTED: No space left on device', no escape", "build.log + VERDICT.md fix section: make nativekernel RC=0 (14897 lines), installkernel RC=0, regress.c in-slice label still accepted on patched kernel", "mkimg.py: byte-exact crafted MBR + on-disk label32 (valid dkcksum32) accepted by the reader, proving the reader's fixlabel bounds are the intended invariant" ], "kernel_refs": [ "sys/kern/subr_disklabel32.c:306-313", "sys/kern/subr_disklabel32.c:264-266", "sys/kern/subr_disklabel32.c:123-135", "sys/kern/subr_disklabel32.c:593-609", "sys/kern/subr_diskslice.c:199-281", "sys/kern/subr_diskslice.c:561-618", "sys/kern/subr_disklabel64.c:275-306", "sys/kern/subr_disk.c:265-270", "sys/kern/subr_disk.c:1072" ], "poc_changes": "No seed was present; the entire pack was authored from scratch: mkimg.py builds a 64MiB vn image with an MBR slice [2048,63744), a VALID on-disk disklabel32 (byte-exact 404-byte layout, LOCARE-verified offsets, dkcksum32=0) whose 'a' partition is in-slice (so the probe creates /dev/vn0s1a), and an out-of-slice marker at absolute 63844; poc2878.c (_Static_assert on struct size/offsets) shows the before/after contrast through the same node and verifies the escaped write in the backing file.", "attempts": 3, "guest_uname": "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", "runtime_sec": 95, "guest_dirty": 0, "build_cmd": "cc -O -Wall -o poc2878 poc2878.c", "run_cmd": "vnconfig -c vn0 /root/df2878.img && sleep 1 && ./poc2878 /root/df2878.img; vnconfig -u vn0", "code_hash": "5c3ca077a8cfecf2b563e3daeecc5ad795d16c57632c8f4fef8001ebe3e3f5e9", "notes": "Phase V complete on the single-tenant guest: baseline 3/3 (run.log, run.2.log, run.3.log - third on a pristine image), fix validation on rebuilt kernel #1 (rejection + no regression via regress.c), guest reset with-src afterwards. Same-source precedent: DF-0134 (Medium, label64 reader, no-priv hostile-media trigger) validates these exact fields; this is the label32 set-path twin, root-class trigger -> filed Low per rubric. The d_npartitions cap in fix.diff additionally closes the DF-0106/0107 dkcksum32 OOB family on the set path.", "recommended_fix": "In l32_setdisklabel: reject nlp->d_npartitions > MAXPARTITIONS32 before dkcksum32(), and require (u_int64_t)p_offset + p_size <= sp->ds_size for every partition (mirroring l64_setdisklabel)", "fix_status": "fixed", "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Wed Sep 2 19:31:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "fix.diff applied to guest /usr/src (patch -p1, both hunks clean), make nativekernel RC=0, make installkernel RC=0, reboot into #1: the same PoC that escaped 3/3 on stock #0 now gets ENOSPC from DIOCSDINFO32 and performs no out-of-slice I/O; regress.c confirms in-slice labels are still accepted (no regression); guest healthy throughout.", "fix_evidence": [ "run.fixed.log (patched rejection, no escape)", "VERDICT.md 'Fix validation' section", "fix.diff (git-apply-able, authored against read-only sys/)" ] } |