DragonFlyBSD Kernel Audit
DF-2878 / verdict.json
← back to finding ↓ download raw
{
  "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/)"
  ]
}