DragonFlyBSD Kernel Audit
← triage · dashboard
DF-0108

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 has d_secsize > MAXPHYS and p_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.

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0108 · 13 files
FileTypeDescriptionSize
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
README.md readme build/run/expected + file index
↓ download 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:336KKASSERT "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
VERDICT.md verdict full narrative: mechanism, path:line, hardening-gap rationale, fix validation
↓ download raw

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)

  1. Attacker opens a disk slice device (e.g. /dev/vn0s0) O_RDWR and issues ioctl(fd, DIOCWDINFO32, &crafted_label) — requires root/operator (subr_diskslice.c:583 checks flags & FWRITE; device nodes are root:operator 0640).

  2. diskioctl() (subr_disk.c:1202) → dsioctl() (subr_diskslice.c:650-670). The DIOCWDINFO path first calls DIOCSDINFO32 internally, which invokes l32_setdisklabel (subr_disklabel32.c:250).

  3. l32_setdisklabel validates magic, checksum, RAW_PART offset, secperunit, and each partition size — but NOT d_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.

  4. Back in dsioctl, ops->op_writedisklabel(dev, ssp, sp, sp->ds_label) is called (subr_diskslice.c:670) → l32_writedisklabel.

  5. 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). With d_secsize = 2 MiB > 128 KiB, the KKASSERT fails. On GENERIC (INVARIANTS ON, sys/sys/systm.h:94-101) KKASSERT expands to a panic(); on a production kernel (INVARIANTS OFF, systm.h:117-118) it is a no-op and the oversized b_bcount flows 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 independent vm.sh reset with-src boots, 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's b_bufsize (128 KiB) backing KVA. The oversized transfer reaches the device strategy routine. Whether this corrupts memory depends on the driver — most dev_dstrategy paths clamp b_bcount or use separate DMA mapping. This is a potential memory-corruption primitive, but only on the non-default noinv-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 maxxgroups=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 valid disklabel32 (correct DISKMAGIC32, dkcksum32==0, RAW_PART.p_offset==0, small partition sizes so l32_setdisklabel accepts it) with the hostile field d_secsize = 0x200000 (2 MiB ≫ MAXPHYS). It uses the header-provided dkcksum32() static inline (not a redefinition) and d_type=0 (the DTYPE_* enum is not exposed in the userland header).
  • Uses a scratch vn-backed device (/dev/vn0s0 on 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 rejects d_secsize == 0 (which would also wrap the loop-bound pointer arithmetic at :360).
  • MAXPHYS (128 KiB) upper bound equals bp->b_bufsize, matching the KKASSERT invariant but as a real return (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

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: 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.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Mon Jul 13 00:55:22 UTC 2026 (SHA256 687b30b9dc3c11d55dc8b958b3e325b9c95a4251e8589649f920bc7cd80480d6)

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.