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.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0107 Β· 15 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 | 8.0 KB | β raw |
| README.md | readme | build/run/expected + privilege model | 3.7 KB | β raw |
| boot_baseline_panic.log | panic-signature | full boot.log around baseline #0 panic (iter 1, fresh boot): vm_fault fault on stack guard, trap 12 in l32_setdisklabel+0x57 from dsioctl+0x721 | 13.5 KB | view raw |
| fix_build.log | build-log | full nativekernel output of the single-fix build (patched disklabel32.h), NK_DONE rc=0, 0 errors | 5.6 MB | β download |
| fix_run.log | run-log | patched #1 kernel: 50/50 PoC iterations survived, ioctl returns EINVAL, DMESG_NO_PANIC | 1.0 KB | view raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view 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.
Fix validation (Phase 8 β built + booted single-fix kernel)
The fix.diff was validated end-to-end on a single-fix kernel, not just
git apply --check.
Setup. vm.sh reset with-src (clean /usr/src + warm obj + unpatched
6.5-DEVELOPMENT #0), then patch -p1 < fix.diff (hunk #1 succeeded at line
154 of sys/sys/disklabel32.h). A userland include-test confirmed MIN is
available where the static __inline dkcksum32 is compiled outside the kernel
(sbin/disklabel32/disklabel.c:38 includes <sys/param.h> first; MIN is
sys/sys/param.h:429). Built make -j6 nativekernel KERNCONF=X86_64_GENERIC
β NK_DONE rc=0, 0 errors (full log: fix_build.log). Installed
kernel.stripped β /boot/kernel/kernel (sha256
38b6940b2560f4802c0eac68d5a4ed5d6bf8ef420eb5fb322770e84b86f5041f),
rebooted β kern.version: DragonFly 6.5-DEVELOPMENT #1: Thu Jul 2 16:12:50 UTC 2026.
Before (unpatched #0, fresh boot). The same PoC (vnconfig vn0 +
ioctl(DIOCSDINFO32, d_npartitions=0xFFFF) on /dev/vn0s0) panicked on
iter 1:
panic: vm_fault: fault on stack guard, addr: 0xfffff80117a88000
--- trap 0xc, rip = l32_setdisklabel+0x57 0xffffffff806958e7 ---
l32_setdisklabel() at l32_setdisklabel+0x57
dsioctl() at dsioctl+0x721
Debugger("panic") -> db>
(fault address differs run-to-run β 0xfffff80117a88000 here,
0xfffff800ab221000 / 0xfffff800ab1a8000 in prior sessions β confirming the
real OOB read reaches varying kernel addresses.) Guest died β vm.sh reset.
After (single-fix #1, same fresh boot). 50 consecutive invocations:
=== iter 1 === [*] issuing DIOCSDINFO32 with d_npartitions=0xffff on /dev/vn0s0 DIOCSDINFO32 ioctl returned (kernel NOT panicked): Invalid argument ... (49 more, identical) ... PATCHED_STRESS: ok=45 panics=0 (of 45; total 50 iters this boot) DMESG_NO_PANIC
The ioctl now returns EINVAL (the clamped dkcksum32 checksum is
nonzero, so l32_setdisklabel:265-266 rejects the label) instead of walking
OOB. 0 panics across 50 runs, guest stayed up, no fault in boot.log or
dmesg.
Verdict: FIXED. Baseline panic (iter 1) is gone on the single-fix kernel
across 50Γ the baseline trigger count. The clamp at disklabel32.h:157
eliminates the OOB read at the source, so the fault is structurally impossible
(not merely masked). fix_status: fixed.
Fix verification
fixedVALIDATED the fix. The same PoC (vnconfig vn0 + ioctl DIOCSDINFO32 d_npartitions=0xFFFF on /dev/vn0s0) that panicked the unpatched #0 baseline on iter 1 ('vm_fault: fault on stack guard', trap 12 in l32_setdisklabel+0x57 from dsioctl+0x721) does NOT panic the single-fix #1 kernel across 50 consecutive invocations on a fresh boot; each ioctl now returns EINVAL cleanly and dmesg/boot.log show no fault. The clamp at disklabel32.h:157 eliminates the OOB read at the source, so the fault is structurally impossible, not merely masked. fix_status=fixed.
BEFORE (#0, iter 1): panic: vm_fault: fault on stack guard, addr: 0xfffff80117a88000 | trap 0xc rip=l32_setdisklabel+0x57 | dsioctl+0x721 | db>. AFTER (#1, 50 iters): '[*] issuing DIOCSDINFO32 with d_npartitions=0xffff' -> 'DIOCSDINFO32 ioctl returned (kernel NOT panicked): Invalid argument' x50 | PATCHED_STRESS: ok=45 panics=0 | DMESG_NO_PANIC. nativekernel build: === NK_DONE rc=0 === (0 errors).
Confirmed kernel references
Detail
Exploit chain
Local DoS (kernel panic) by root/operator via open(slice,O_RDWR) + ioctl(DIOCSDINFO32, d_npartitions=0xFFFF) -> dkcksum32 1MiB OOB read -> page fault on a thread-stack guard page. Probabilistic (heap-layout dependent; fires within a few fresh-boot invocations). The XOR-folded 16-bit checksum result is never copied back to userspace on this IOC_IN path, so no extractable info disclosure; realistic worst case is denial of service. Not a memory-corruption primitive amenable to further escalation (read-only OOB into a discarded checksum).
Evidence (decisive lines)
BASELINE #0 (iter 1, fresh boot): panic: vm_fault: fault on stack guard, addr: 0xfffff80117a88000 | --- trap 000000000000000c, rip = ffffffff806958e7 --- | l32_setdisklabel() at l32_setdisklabel+0x57 | dsioctl() at dsioctl+0x721 | Debugger("panic") db>. PATCHED #1: PATCHED_STRESS: ok=45 panics=0 (of 45; total 50 iters this boot); each ioctl returns 'DIOCSDINFO32 ... Invalid argument' (EINVAL); DMESG_NO_PANIC; guest stayed up. Full logs in boot_baseline_panic.log and fix_run.log.
PoC changes
No PoC source changes needed this session (trigger unchanged: cc -O0 -o poc_diocsdinfo poc_diocsdinfo.c compiled first try on #0). fix.diff kept as-is (root-cause clamp at disklabel32.h:157); verified it applies cleanly (hunk #1 at line 154) and that MIN is safe for the userland static __inline (sbin/disklabel32/disklabel.c:38 includes
Verified recommended fix
Clamp dkcksum32's end pointer in sys/sys/disklabel32.h:157 to &lp->d_partitions[MIN(lp->d_npartitions, MAXPARTITIONS32)] -- the root-cause fix that bounds the walk at the source, closing DF-0107 and sibling DF-0106 (and every caller). Verified: built+booted single-fix #1 kernel, baseline iter-1 panic is gone across 50 runs. This SUPERSEDES the finding markdown's caller-only two-site proposal (subr_disklabel32.c:264 guard) by fixing the shared helper once; the caller guard is optional defense-in-depth. Full git-apply-able diff in findings/poc/DF-0107/fix.diff.
Verdict
REPRODUCED. The missing d_npartitions guard in l32_setdisklabel (sys/kern/subr_disklabel32.c:264-265) is confirmed: it calls dkcksum32(nlp) whose end pointer is computed from the attacker-controlled d_npartitions without bounding (sys/sys/disklabel32.h:157). On the unpatched #0 kernel a DIOCSDINFO32 with d_npartitions=0xFFFF panicked on iter 1 of a fresh boot: 'panic: vm_fault: fault on stack guard, addr: 0xfffff80117a88000' trap 0xc (12) in l32_setdisklabel+0x57 (dkcksum32 inlined) called from dsioctl+0x721. The ~1 MiB XOR walk read past the 404-byte kmalloc ioctl label buffer until it crossed a thread-stack guard page. fault address varies run-to-run (0xfffff80117a88000 / 0xfffff800ab221000 / 0xfffff800ab1a8000) confirming a genuine OOB read, not a cosmetic artifact.
No comments yet.