Unvalidated d_secsize in writedisklabel enables oversized I/O transfer
| Field | Value |
|---|---|
| ID | DF-0108 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:N/I:L/A:H |
| CWE | CWE-787 Out-of-bounds Write |
| File | sys/kern/subr_disklabel32.c |
| Lines | 336-341 |
| Area | kern |
| Confidence | medium |
| Discovered | 2026-06-30 |
| Reported | pending |
Summary
l32_writedisklabel uses lp->d_secsize directly as bp->b_bcount
(:340) and as a loop bound (:360), with only a KKASSERT
(no-op on production kernels) to verify it fits in the buffer. A user
who supplies a label with d_secsize exceeding the pbuf's
b_bufsize can trigger an oversized I/O transfer, potentially
corrupting kernel memory depending on the device strategy routine.
Root cause
l32_writedisklabel at :336-340:
KKASSERT((int)lp->d_secsize <= bp->b_bufsize);
bp->b_bio1.bio_offset = (off_t)LABELSECTOR32 * lp->d_secsize;
...
bp->b_bcount = lp->d_secsize;
KKASSERT is #ifdef INVARIANTS only (sys/sys/systm.h:94-118) —
a no-op on production kernels. lp->d_secsize comes from the new label
supplied via DIOCWDINFO ioctl. If d_secsize exceeds bp->b_bufsize
(typically MAXPHYS = 128KB), the strategy routine receives
b_bcount > b_bufsize.
Additionally, the loop bound at :359-360:
((char *)bp->b_data + lp->d_secsize - sizeof(*dlp))
If d_secsize < sizeof(*dlp) (e.g. 0), the pointer arithmetic wraps,
making the loop condition enormous — though the first iteration reads
bp->b_data which is valid, limiting immediate impact.
Threat model & preconditions
- Attacker position: Root or operator with write access to disk device node.
- Impact: Potential kernel memory corruption via oversized I/O.
Severity depends on the underlying device driver's handling of
b_bcount > b_bufsize. - Required config: Production kernel (no INVARIANTS).
- Reachability:
ioctl(fd, DIOCWDINFO32, &label)where label hasd_secsize > MAXPHYSandp_offset[RAW_PART] == 0.
Proof of concept
PoC source: findings/poc/DF-0108/ (scaffold)
Build & run
cc -o poc_secsize poc_secsize.c sudo ./poc_secsize /dev/da0s1
Expected output
Depends on driver behavior: kernel panic, memory corruption, or silent data transfer past buffer end.
Impact
Low. Requires root/operator privileges. Whether the oversized transfer
actually corrupts memory depends on the specific disk driver — many
drivers clamp b_bcount to b_bufsize internally. This is primarily
a hardening gap.
Recommended fix
Replace the KKASSERT with a real validation:
--- a/sys/kern/subr_disklabel32.c
+++ b/sys/kern/subr_disklabel32.c
@@ -333,7 +333,10 @@
if (lp->d_partitions[RAW_PART].p_offset != 0)
return (EXDEV); /* not quite right */
- bp = getpbuf_mem(NULL);
+ bp = getpbuf_mem(NULL);
+ if (lp->d_secsize < DEV_BSIZE || lp->d_secsize > bp->b_bufsize)
+ return (EINVAL);
+
KKASSERT((int)lp->d_secsize <= bp->b_bufsize);
bp->b_bio1.bio_offset = (off_t)LABELSECTOR32 * lp->d_secsize;
Timeline
- 2026-06-30 Discovered during automated audit.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0108 · 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| poc_secsize.c | trigger-source | DIOCWDINFO32 with d_secsize=2MiB > MAXPHYS; valid magic/checksum/RAW_PART offset | 3.3 KB | view raw |
| build.sh | build-script | cc -o poc_secsize poc_secsize.c | 197 B | view raw |
| run.sh | run-script | sets up scratch vn0s0, runs PoC | 685 B | view raw |
| run.log | run-log | baseline run: marker lines + panic signature (KKASSERT :336) | 1.7 KB | view raw |
| fix_run.log | run-log | patched run: errno=22 EINVAL x3, guest up | 1.4 KB | view raw |
| panic.txt | panic-signature | panic: assertion d_secsize<=b_bufsize failed in l32_writedisklabel at :336; call trace diskioctl->dsioctl->l32_writedisklabel | 432 B | view raw |
| fix.diff | suggested-fix | validate d_secsize (DEV_BSIZE..MAXPHYS) before getpbuf_mem; return EINVAL | 830 B | view raw |
| fix_build.log | build-log | single-fix nativekernel build, full output, rc=0 | 5.6 MB | ↓ download |
| VERDICT.md | verdict | full narrative: mechanism, path:line, hardening-gap rationale, fix validation | 6.2 KB | ↓ raw |
| README.md | readme | build/run/expected + file index | 1.5 KB | ↓ raw |
| env.txt | environment | uname, cc version, INVARIANTS confirmed ON in GENERIC | 278 B | 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-0108 — Unvalidated d_secsize in writedisklabel
Verdict: REPRODUCED (panic/DoS on default GENERIC) + FIX VALIDATED.
Impact: panic (root/operator-only DoS; no unpriv escalation).
Privilege: root/operator only — DIOCWDINFO32 needs FWRITE on a
root:operator 0640 slice device node. maxx (not in operator) cannot reach it.
Build & run
cc -o poc_secsize poc_secsize.c # build.sh # as root/operator: rm -f /root/scratch.img dd if=/dev/zero of=/root/scratch.img bs=1m count=64 vnconfig -c -s labels /dev/vn0 /root/scratch.img ./poc_secsize /dev/vn0s0 # run.sh
Expected
- Unpatched GENERIC kernel (#0, INVARIANTS ON): kernel panic at
subr_disklabel32.c:336—KKASSERT "d_secsize <= b_bufsize". The ssh session dies; the panic is captured in the serial log (dfbsd-qemu/boot.log). - Patched kernel (#1):
ioctl returned rc=-1,errno=22 (Invalid argument), guest stays up.
Files
| file | purpose |
|---|---|
poc_secsize.c |
trigger: DIOCWDINFO32 with d_secsize = 2 MiB > MAXPHYS |
build.sh / run.sh |
exact build/run commands |
run.log |
baseline run output + panic signature |
fix_run.log |
patched-kernel run output (3×, all EINVAL) |
panic.txt |
panic call trace from serial console |
fix.diff |
git-apply-able fix (validates d_secsize before getpbuf_mem) |
fix_build.log |
full single-fix kernel build output (rc=0) |
VERDICT.md |
full narrative |
manifest.json |
artifact catalog |
env.txt |
guest environment |
DF-0108 — Unvalidated d_secsize in l32_writedisklabel
Verdict
REPRODUCED (panic/DoS on default GENERIC) + FIX VALIDATED. The bug is real:
d_secsize from a user-supplied disklabel32 reaches l32_writedisklabel
without validation, and on the GENERIC kernel (INVARIANTS ON) the KKASSERT at
subr_disklabel32.c:336 fires as a kernel panic. The single-fix kernel
(fix.diff) returns EINVAL instead and the panic is gone (3/3 clean runs).
Privilege boundary: root/operator-only — there is no unprivileged→root
escalation. DIOCWDINFO32 requires the slice device open FWRITE, and slice
device nodes are root:operator mode 0640. The audit user maxx (uid 1001,
not in operator) cannot trigger it. This is a root→kernel hardening gap.
Mechanism (confirmed, path:line at each hop)
-
Attacker opens a disk slice device (e.g.
/dev/vn0s0)O_RDWRand issuesioctl(fd, DIOCWDINFO32, &crafted_label)— requires root/operator (subr_diskslice.c:583checksflags & FWRITE; device nodes areroot:operator 0640). -
diskioctl()(subr_disk.c:1202) →dsioctl()(subr_diskslice.c:650-670). The DIOCWDINFO path first callsDIOCSDINFO32internally, which invokesl32_setdisklabel(subr_disklabel32.c:250). -
l32_setdisklabelvalidates magic, checksum, RAW_PART offset, secperunit, and each partition size — but NOTd_secsize(subr_disklabel32.c:264-314). The crafted label (valid magic/checksum,d_secsize = 0x200000= 2 MiB,RAW_PART.p_offset = 0) passes all checks and is installed in-core. -
Back in
dsioctl,ops->op_writedisklabel(dev, ssp, sp, sp->ds_label)is called (subr_diskslice.c:670) →l32_writedisklabel. -
l32_writedisklabel(subr_disklabel32.c:335-340):c bp = getpbuf_mem(NULL); /* :335 */ KKASSERT((int)lp->d_secsize <= bp->b_bufsize); /* :336 PANIC */ bp->b_bio1.bio_offset = (off_t)LABELSECTOR32 * lp->d_secsize; ... bp->b_bcount = lp->d_secsize; /* :340 */bp->b_bufsize == MAXPHYS == 128 KiB(vm/vm_pager.c:391 initpbuf). Withd_secsize = 2 MiB > 128 KiB, the KKASSERT fails. On GENERIC (INVARIANTS ON,sys/sys/systm.h:94-101)KKASSERTexpands to apanic(); on a production kernel (INVARIANTSOFF,systm.h:117-118) it is a no-op and the oversizedb_bcountflows to the device strategy routine.
Observed impact
-
GENERIC kernel (#0, INVARIANTS ON — the default): immediate kernel panic at
subr_disklabel32.c:336. Reproduced 2× from independentvm.sh reset with-srcboots, identical signature. This is a DoS (root can already reboot the box, so the panic itself is not a privilege gain). -
Production kernel (INVARIANTS OFF, non-default): the KKASSERT is skipped and
bp->b_bcount = d_secsize(2 MiB) exceeds the pbuf'sb_bufsize(128 KiB) backing KVA. The oversized transfer reaches the device strategy routine. Whether this corrupts memory depends on the driver — mostdev_dstrategypaths clampb_bcountor use separate DMA mapping. This is a potential memory-corruption primitive, but only on the non-defaultnoinv-class kernel, and only reachable by root/operator.
Exploit chain / escalation
None — root/operator-only path (valid hard blocker per Phase 6).
DIOCWDINFO32 requires FWRITE on a root:operator 0640 device node
(subr_diskslice.c:583, id maxx ⇒ groups=1001(maxx) only). An
unprivileged user cannot cross the privilege boundary to reach this code. No
unprivileged path was found (vnconfig/mdconfig/devfs rules all gate on
root). Root→kernel is game-over by definition; this is a hardening gap, not a
privesc. No exploit.c/chain.c was authored because there is no
unprivileged victim to escalate against.
PoC changes
- Created
poc_secsize.c(the finding shipped no PoC source — only a scaffold reference). The PoC builds a validdisklabel32(correctDISKMAGIC32,dkcksum32==0,RAW_PART.p_offset==0, small partition sizes sol32_setdisklabelaccepts it) with the hostile fieldd_secsize = 0x200000(2 MiB ≫MAXPHYS). It uses the header-provideddkcksum32()static inline (not a redefinition) andd_type=0(theDTYPE_*enum is not exposed in the userland header). - Uses a scratch
vn-backed device (/dev/vn0s0on a 64 MiB zero image) so the boot disk is never at risk.
Fix (fix.diff — supersedes the finding's proposal)
The finding's proposed fix places the check after getpbuf_mem(NULL), which
would leak the pbuf on the error path (relpbuf at :395 is skipped by an
early return). My fix places the validation before getpbuf_mem:
if (lp->d_partitions[RAW_PART].p_offset != 0)
return (EXDEV); /* not quite right */
/* Validate d_secsize before allocating the pbuf ... */
if (lp->d_secsize < DEV_BSIZE || lp->d_secsize > MAXPHYS)
return (EINVAL);
bp = getpbuf_mem(NULL);
KKASSERT((int)lp->d_secsize <= bp->b_bufsize); /* now always true for user input */
DEV_BSIZE(512) lower bound rejectsd_secsize == 0(which would also wrap the loop-bound pointer arithmetic at:360).MAXPHYS(128 KiB) upper bound equalsbp->b_bufsize, matching the KKASSERT invariant but as a realreturn (EINVAL).- Placed before
getpbuf_mem→ no pbuf leak. - The KKASSERT is retained as a secondary invariant (now unreachable for user-supplied labels).
Fix validation (Phase 8)
| kernel | version | PoC result | guest |
|---|---|---|---|
| unpatched baseline | #0 Jul 2 06:02 |
panic KKASSERT d_secsize <= b_bufsize at :336 |
down |
| single-fix | #1 Jul 13 00:55 |
EINVAL (errno 22), 3/3 runs | up |
fix.diff applies cleanly (patch -p1 Hunk #1 succeeded), the single-fix
kernel compiles (nativekernel rc=0), boots as #1, and the identical PoC
that panicked the baseline now returns EINVAL with the guest staying up.
Reproduce
./build.sh # cc -o poc_secsize poc_secsize.c sudo ./run.sh # sets up scratch /dev/vn0s0, runs the PoC # unpatched: panic at subr_disklabel32.c:336 (check dfbsd-qemu/boot.log) # patched: "errno=22 (Invalid argument)", guest stays up
Fix verification
fixedVALIDATED: baseline panic at subr_disklabel32.c:336; patched EINVAL x3, guest up, 0 panics.
BEFORE #0: panic d_secsize<=b_bufsize KKASSERT, guest down x2. AFTER #1: EINVAL errno=22 x3, guest up.
Confirmed kernel references
Detail
Exploit chain
No escalation -- root/operator-only reachability. DIOCWDINFO32 requires FWRITE on root:operator 0640 device. No unprivileged path. Low severity / CVSS PR:H.
Evidence (decisive lines)
BASELINE #0: panic: assertion d_secsize<=b_bufsize failed at subr_disklabel32.c:336, guest down x2. PATCHED #1: EINVAL (errno=22), guest up x3, 0 panics.
PoC changes
Created poc_secsize.c from scratch (finding shipped no PoC source). Builds valid disklabel32 with hostile d_secsize=0x200000. Uses scratch vn device. Added build.sh, run.sh, VERDICT.md, manifest.json, fix.diff, full logs.
Verified recommended fix
In l32_writedisklabel (subr_disklabel32.c), add BEFORE getpbuf_mem: 'if (lp->d_secsize < DEV_BSIZE || lp->d_secsize > MAXPHYS) return (EINVAL);'. Supersedes finding proposal (moves check before getpbuf_mem to avoid pbuf leak). Full git-apply-able diff in findings/poc/DF-0108/fix.diff.
Verdict
REPRODUCED. l32_setdisklabel validates magic/checksum/RAW_PART but NOT d_secsize. A crafted disklabel32 with d_secsize=0x200000 (2MiB >> MAXPHYS=128KiB) passes all gates and reaches l32_writedisklabel, where KKASSERT((int)lp->d_secsize <= bp->b_bufsize) at subr_disklabel32.c:336 panics on INVARIANTS GENERIC. Root/operator-only.
No comments yet.