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 hasd_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).
Recommended fix
--- 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| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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 |
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)
-
Trigger. Open a disk slice device
O_RDWR(/dev/vn0s0; requires root oroperatorโ device nodes arecrw-r----- root:operator, and themaxxtest user is not inoperator). Issueioctl(fd, DIOCSDINFO32, &label)with adisklabel32whosed_magic = d_magic2 = DISKMAGIC32andd_npartitions = 0xFFFF. -
Dispatch. The
DIOCSDINFO32handlerdsioctl()(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) andpart == WHOLE_SLICE_PART(part=255);FWRITEis set (:583). The user pointer islptmp.opaque = data(:608). -
Where the label buffer lives.
mapped_ioctl()copies the 404-byte label into a heap allocation (sys/kern/sys_generic.c:674-676) becausesizeof(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 */ -
Missing guard (the bug).
l32_setdisklabelchecks only the magics and the checksum โ nod_npartitionsbound (subr_disklabel32.c:264-266):c if (nlp->d_magic != DISKMAGIC32 || nlp->d_magic2 != DISKMAGIC32 || dkcksum32(nlp) != 0) return (EINVAL);Contrast the read pathl32_readdisklabel(:225-226) which DOES guard:c } else if (dlp->d_npartitions > MAXPARTITIONS32 || dkcksum32(dlp) != 0) {Becaused_magic/d_magic2match, the||short-circuit reachesdkcksum32(nlp). -
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++;Withd_npartitions=0xFFFF,end = &d_partitions[65535]=offsetof(d_partitions)=148 + 65535*16 = 1048708bytes fromlp. 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). -
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).
Recommended fix
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
- sys/sys/disklabel32.h:150
- sys/sys/disklabel32.h:157
- sys/kern/subr_disklabel32.c:264
- sys/kern/subr_disklabel32.c:265
- sys/kern/subr_disklabel32.c:225
- sys/kern/subr_disklabel32.c:226
- sys/kern/subr_diskslice.c:561
- sys/kern/subr_diskslice.c:583
- sys/kern/subr_diskslice.c:608
- sys/kern/subr_diskslice.c:609
- sys/kern/sys_generic.c:674
- sys/kern/sys_generic.c:675
- sys/config/X86_64_GENERIC
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.