โฌข DragonFlyBSD Kernel Audit
โ† dashboard
DF-0107

dkcksum32 OOB read via DIOCSDINFO ioctl with crafted d_npartitions

Field Value
ID DF-0107
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H
CWE CWE-125 Out-of-bounds Read
File sys/kern/subr_disklabel32.c
Lines 264-265
Area kern
Confidence high
Discovered 2026-06-30
Reported pending

Summary

l32_setdisklabel calls dkcksum32(nlp) on a user-supplied label without checking d_npartitions first (sys/kern/subr_disklabel32.c:265). A user with write access to a disk device node can submit a DIOCSDINFO ioctl with d_npartitions > MAXPARTITIONS32, causing dkcksum32 (disklabel32.h:157) to walk past the label buffer and panic the kernel.

Root cause

l32_setdisklabel at :264-265:

if ((nlp->d_magic != DISKMAGIC32 ||
     nlp->d_magic2 != DISKMAGIC32 ||
     dkcksum32(nlp) != 0)

No check on nlp->d_npartitions before calling dkcksum32. Compare with l32_readdisklabel at :225-226 which guards with dlp->d_npartitions > MAXPARTITIONS32 ||.

nlp is the label passed from the DIOCSDINFO ioctl handler in subr_diskslice.c, allocated as sizeof(struct disklabel32) bytes. With d_npartitions = 0xFFFF, dkcksum32 computes an end pointer ~1 MiB past the struct, reading kernel heap memory until it crosses into an unmapped page โ†’ kernel panic.

Threat model & preconditions

  • Attacker position: Local user with write access to a disk device node (typically root or operator group).
  • Impact: Kernel panic (local DoS).
  • Required config: Default kernel.
  • Reachability: ioctl(fd, DIOCSDINFO32, &label) where the label has d_magic = d_magic2 = DISKMAGIC32, d_npartitions = 0xFFFF.

Proof of concept

PoC source: findings/poc/DF-0107/

Build & run

cc -o poc_diocsdinfo poc_diocsdinfo.c
sudo ./poc_diocsdinfo /dev/da0s1

Expected output

Fatal trap 12: page fault while in kernel mode
fault virtual address = 0xffff...
panic: page fault

Impact

Local kernel panic. Requires write access to a disk device node (root or operator). Simpler to trigger than DF-0106 (no need for crafted on-disk media โ€” the payload is in the ioctl argument).

--- a/sys/kern/subr_disklabel32.c
+++ b/sys/kern/subr_disklabel32.c
@@ -262,6 +262,7 @@
     * the spec says you can only change the partition table.
     */
    if ((nlp->d_magic != DISKMAGIC32 ||
+        nlp->d_npartitions > MAXPARTITIONS32 ||
         nlp->d_magic2 != DISKMAGIC32 ||
         dkcksum32(nlp) != 0)

References

  • Same root cause as DF-0106 (dkcksum32 unbounded d_npartitions).
  • The read path already has this guard at :225-226.

Timeline

  • 2026-06-30 Discovered during automated audit.

PoC verification

Evidence pack

findings/poc/DF-0107 ยท 10 files
FileTypeDescriptionSize
poc_diocsdinfo.c trigger-source open slice device O_RDWR, issue DIOCSDINFO32 with d_magic/d_magic2=DISKMAGIC32, d_npartitions=0xFFFF 3.0 KB view raw
build.sh build-script cc -O0 -o poc_diocsdinfo poc_diocsdinfo.c 243 B view raw
run.sh run-script as root: vnconfig vn0 + run trigger on /dev/vn0s0 1.3 KB view raw
build.log build-log final successful build, full output 72 B view raw
run.log run-log decisive run: ioctl issued, ssh dies on panic, signature appended 982 B view raw
panic.txt panic-signature fatal trap 12 in l32_setdisklabel+0x57 from dsioctl+0x721 (stack-guard page fault) 711 B view raw
env.txt environment uname, cc version, device perms (root:operator 0640), id maxx (not in operator) 648 B view raw
fix.diff suggested-fix root-cause: clamp dkcksum32 end pointer to MAXPARTITIONS32 in sys/sys/disklabel32.h (closes DF-0106 + DF-0107); git apply --check clean 383 B view raw
VERDICT.md verdict full narrative: trigger->primitive->effect, why probabilistic, fix 5.7 KB โ†“ raw
README.md readme build/run/expected + privilege model 3.7 KB โ†“ raw
README.md readme build/run/expected + privilege model
โ†“ download raw

DF-0107 โ€” dkcksum32 OOB read via DIOCSDINFO32 (PoC)

l32_setdisklabel() (sys/kern/subr_disklabel32.c:264-265) calls dkcksum32(nlp) on a user-supplied disklabel without the d_npartitions > MAXPARTITIONS32 guard that protects the read path (l32_readdisklabel, subr_disklabel32.c:225-226). dkcksum32() (sys/sys/disklabel32.h:150-161) computes its end pointer from the attacker-controlled d_npartitions field and XOR-walks u_int16_t values to it. With d_npartitions = 0xFFFF the walk goes ~1 MiB past the label buffer (0xFFFF * 16 = 1048560 bytes), reading kernel memory until it faults a thread-stack guard page โ†’ fatal trap 12 / kernel panic.

Privilege model

Opening the slice device O_RDWR is required (FWRITE, see subr_diskslice.c:583-584). On DragonFlyBSD, disk device nodes are crw-r----- root:operator (verified: /dev/md0, /dev/vn0s0). The maxx test user (uid 1001) is not in operator, so this is a root / operator group trigger โ€” exactly the threat model in the finding ("local user with write access to a disk device node"). The PoC must be run as root to open the device.

Build

On the guest, as the unprivileged build user:

cd poc/DF-0107
sh build.sh        # cc -O0 -o poc_diocsdinfo poc_diocsdinfo.c

Run

On the guest, as root (creates a memory disk + issues the ioctl):

cd poc/DF-0107
sh run.sh          # may need a few invocations -- the panic is heap-layout dependent
# or, loop until panic:
for i in $(seq 1 30); do sh run.sh >/dev/null 2>&1 || true; done

The PoC: 1. dd an 8 MiB image, vnconfig -c vn0 /tmp/oob.img โ†’ /dev/vn0s0. 2. Builds a disklabel32 with d_magic = d_magic2 = DISKMAGIC32, d_npartitions = 0xFFFF (everything else zero). 3. open("/dev/vn0s0", O_RDWR) then ioctl(fd, DIOCSDINFO32, &label).

Expected on a vulnerable kernel (reproduced)

The panic is probabilistic: the ioctl label is a kmalloc(404, M_IOCTLOPS) buffer (sys/kern/sys_generic.c:674-676, since sizeof(disklabel32)=404 > STK_PARAMS=128), and the ~1 MiB dkcksum32 walk faults only when that buffer lands within ~1 MiB before a thread-stack guard page in kernel VM. On a freshly booted guest it fires within the first few invocations; on a churned heap it may take more. Observed live twice (fresh-boot run 2 and run 3, then run 1 of a later fresh boot):

panic: vm_fault: fault on stack guard, addr: 0xfffff800ab221000
cpuid = 1
...
--- trap 000000000000000c, rip = ffffffff80694ff7, rsp = ..., rbp = ... ---
l32_setdisklabel() at l32_setdisklabel+0x57 0xffffffff80694ff7
dsioctl() at dsioctl+0x721 0xffffffff806976f1
Debugger("panic")
Stopped at Debugger+0x7c: ...
db>

trap 0xc = 12 = page fault while in kernel mode, inside dkcksum32 (inlined into l32_setdisklabel+0x57), called from the DIOCSDINFO32 handler dsioctl. The full signature is in panic.txt; the decisive loop output + panic is in run.log.

Fix

fix.diff clamps dkcksum32's end pointer to MAXPARTITIONS32 in sys/sys/disklabel32.h โ€” the root-cause fix that closes both DF-0106 and DF-0107 (and any other caller) at the source. Applies with git apply -p1.

Files

file purpose
poc_diocsdinfo.c minimal trigger (open slice device, DIOCSDINFO32 with d_npartitions=0xFFFF)
build.sh / run.sh exact build / run invocations
build.log final successful build, full output
run.log decisive run: ioctl issued, then kernel panic (signature appended)
panic.txt fatal-trap / panic / db> excerpt from dfbsd-qemu/boot.log
env.txt guest uname, cc version, device permissions, ids
fix.diff git-apply-able root-cause fix (clamp dkcksum32)
VERDICT.md full narrative
manifest.json machine-readable catalog
VERDICT.md verdict full narrative: trigger->primitive->effect, why probabilistic, fix
โ†“ download raw

DF-0107 โ€” VERDICT: REPRODUCED (kernel panic, probabilistic DoS)

One-line verdict

REPRODUCED. The missing d_npartitions > MAXPARTITIONS32 guard in l32_setdisklabel (sys/kern/subr_disklabel32.c:264-265) is confirmed in the audited master DEV source, and the resulting unbounded dkcksum32 walk (sys/sys/disklabel32.h:157) panics the kernel via DIOCSDINFO32 with d_npartitions=0xFFFF. The panic is heap-layout dependent (probabilistic) but reproduces cleanly on fresh boots.

Mechanism (trigger โ†’ primitive โ†’ effect)

  1. Trigger. Open a disk slice device O_RDWR (/dev/vn0s0; requires root or operator โ€” device nodes are crw-r----- root:operator, and the maxx test user is not in operator). Issue ioctl(fd, DIOCSDINFO32, &label) with a disklabel32 whose d_magic = d_magic2 = DISKMAGIC32 and d_npartitions = 0xFFFF.

  2. Dispatch. The DIOCSDINFO32 handler dsioctl() (subr_diskslice.c:561-609) passes the user label through. The gate checks pass: slice != WHOLE_DISK_SLICE (vn0s0 is the compatibility slice, slice=0) and part == WHOLE_SLICE_PART (part=255); FWRITE is set (:583). The user pointer is lptmp.opaque = data (:608).

  3. Where the label buffer lives. mapped_ioctl() copies the 404-byte label into a heap allocation (sys/kern/sys_generic.c:674-676) because sizeof(struct disklabel32)=404 > STK_PARAMS=128: c if ((com & IOC_VOID) == 0 && size > sizeof(ubuf.stkbuf)) memp = kmalloc(size, M_IOCTLOPS, M_WAITOK); /* <-- 404-byte heap buf */

  4. Missing guard (the bug). l32_setdisklabel checks only the magics and the checksum โ€” no d_npartitions bound (subr_disklabel32.c:264-266): c if (nlp->d_magic != DISKMAGIC32 || nlp->d_magic2 != DISKMAGIC32 || dkcksum32(nlp) != 0) return (EINVAL); Contrast the read path l32_readdisklabel (:225-226) which DOES guard: c } else if (dlp->d_npartitions > MAXPARTITIONS32 || dkcksum32(dlp) != 0) { Because d_magic/d_magic2 match, the || short-circuit reaches dkcksum32(nlp).

  5. OOB walk. dkcksum32 (disklabel32.h:150-161): c start = (u_int16_t *)lp; end = (u_int16_t *)&lp->d_partitions[lp->d_npartitions]; /* UNBOUNDED */ while (start < end) sum ^= *start++; With d_npartitions=0xFFFF, end = &d_partitions[65535] = offsetof(d_partitions)=148 + 65535*16 = 1048708 bytes from lp. The buffer is only 404 bytes, so the walk reads ~1 048 560 bytes of kernel heap past the buffer (XOR-folded into a 16-bit sum).

  6. Effect โ€” panic. When the 1 MiB walk crosses an unmapped page fault occurs. Empirically the faulting page is a kernel thread-stack guard page (the panic message names it explicitly): panic: vm_fault: fault on stack guard, addr: 0xfffff800ab221000 trap 0xc (12) page fault, rip = l32_setdisklabel+0x57 (dkcksum32 inlined) dsioctl+0x721

Why it is probabilistic (not deterministic)

The M_IOCTLOPS kmalloc(404) buffer sits in the kernel malloc arena. The 1 MiB walk faults iff a thread-stack guard page (or other unmapped page) lies within ~1 MiB forward of the buffer. On a fresh boot the arena is tightly laid out and the guard page is close โ†’ it fires within a few invocations (observed on fresh-boot run 2, run 3, and run 1 of separate tests). After the heap is churned (many allocations), the buffer may land in a region where the next 1 MiB is fully mapped โ†’ dkcksum32 returns nonzero (no fault) โ†’ EINVAL, no panic. The bug executes every time (the OOB read always happens); only the fault is layout-dependent. This is consistent with the INVARIANTS (but no SLAB_DEBUG/guard-page) kernel config (sys/config/X86_64_GENERIC): there is no kmalloc redzone that would make the fault deterministic.

Confirmation / stress

  • Reproduced 3ร— on fresh-boot heaps, identical RIP (l32_setdisklabel+0x57 = 0xffffffff80694ff7), two distinct fault addresses (0xfffff800ab1a8000, 0xfffff800ab221000) โ€” both stack-guard pages, confirming the OOB read reaches varying kernel addresses (real OOB, not a cosmetic artifact).
  • On a churned heap the same trigger returns EINVAL (no panic) โ€” the OOB read still executes, just through mapped memory.

Impact

Local kernel panic (DoS) by any principal with write access to a disk device node (root or operator). The XOR-folded 16-bit checksum result is never copied back to userspace on this IOC_IN path, so there is no extractable information disclosure โ€” the realistic worst case is denial of service. (The OOB read itself is a latent info-leak into an internal checksum, but it is not observable by the attacker.)

PoC changes

Authored from scratch (the finding shipped with no PoC folder). The trigger is a self-contained C program that opens /dev/vn0s0 and issues DIOCSDINFO32 with d_npartitions=0xFFFF; build.sh/run.sh wire up the vnconfig setup. The first compile attempt succeeded with no source fixes needed (syscall number, struct layout, and ioctl constants all match the audited headers).

Root-cause: clamp dkcksum32's end pointer in sys/sys/disklabel32.h:157:

end = (u_int16_t *)&lp->d_partitions[MIN(lp->d_npartitions, MAXPARTITIONS32)];

This closes DF-0107 and sibling DF-0106 (and every other caller) at the source. Defense-in-depth: also add the explicit nlp->d_npartitions > MAXPARTITIONS32 guard to l32_setdisklabel (subr_disklabel32.c:264) mirroring l32_readdisklabel:225. The standalone fix.diff implements the root-cause clamp; it supersedes the finding markdown's two-site caller-only proposal by fixing the shared helper once. Verified with git apply --check (clean).

Confirmed kernel references

Detail

Exploit chain

Primitive is an unbounded kernel heap read (~1MiB, attacker-controlled length via d_npartitions) starting from a kmalloc(404, M_IOCTLOPS) object, but its 16-bit XOR-folded result is never copied back to userspace on this IOC_IN path, so it is NOT an extractable info leak. Realistic weaponization ceiling = denial of service (panic). No memory-corruption write primitive; no escalation path.

Evidence (decisive lines)

panic.txt: 'panic: vm_fault: fault on stack guard, addr: 0xfffff800ab221000' trap 0xc rip=l32_setdisklabel+0x57 dsioctl+0x721. run.log: ioctl issued then ssh dies mid-loop on the panic (signature appended). Reproduced 3x with identical RIP (0xffffffff80694ff7) and two distinct fault addresses (0xfffff800ab1a8000, 0xfffff800ab221000) -- both stack-guard pages, confirming a genuine OOB read reaching varying kernel addresses. poc_diocsdinfo.c + build.sh + run.sh reproduce cleanly as root.

PoC changes

Authored the entire evidence pack from scratch (finding shipped with no PoC folder). The trigger C program compiled and ran correctly on the first attempt (DIOCSDINFO32 constant, struct disklabel32 layout, and DISKMAGIC32 all match the audited headers -- no source fixes needed). build.sh wires up cc; run.sh wires up the vnconfig memory-disk setup + root invocation on /dev/vn0s0.

Verified recommended fix

Root-cause: clamp dkcksum32 end pointer in sys/sys/disklabel32.h:157 to &lp->d_partitions[MIN(lp->d_npartitions, MAXPARTITIONS32)] -- closes DF-0107 AND DF-0106 at the shared helper. Defense-in-depth: add nlp->d_npartitions > MAXPARTITIONS32 to l32_setdisklabel (subr_disklabel32.c:264) mirroring :225. Full git-apply-able diff in findings/poc/DF-0107/fix.diff (supersedes the finding markdown's caller-only proposal).

Verdict

REPRODUCED. l32_setdisklabel (sys/kern/subr_disklabel32.c:264-265) calls dkcksum32(nlp) with NO d_npartitions > MAXPARTITIONS32 guard, unlike the read path (:225-226). DIOCSDINFO32 with d_magic=d_magic2=DISKMAGIC32 and d_npartitions=0xFFFF makes dkcksum32 (disklabel32.h:157) walk ~1MiB past the 404-byte kmalloc(404, M_IOCTLOPS) ioctl buffer. Reproduced 3x: panic 'vm_fault: fault on stack guard' (trap 12) in l32_setdisklabel+0x57 (dkcksum32 inlined), called from dsioctl+0x721. The panic is heap-layout dependent (the walk faults only when the kmalloc buffer lands within ~1MiB before a thread-stack guard page); it fires within the first few invocations on a fresh-boot heap and returns EINVAL on a churned heap (the OOB read always executes, only the fault is probabilistic). The INVARIANTS kernel has no SLAB_DEBUG redzone to make it deterministic.