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

dkcksum32 OOB read via crafted disklabel in writedisklabel path

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

Summary

dkcksum32() computes its end pointer from the attacker-controlled d_npartitions field without bounds checking (sys/sys/disklabel32.h:157). The l32_writedisklabel path (sys/kern/subr_disklabel32.c:363-364) calls dkcksum32(dlp) on the on-disk label read from media without the d_npartitions > MAXPARTITIONS32 guard that protects the l32_readdisklabel path (:225-226). A crafted disk image with d_npartitions=0xFFFF causes dkcksum32 to read up to ~1 MiB past the I/O buffer, crossing page boundaries and causing a deterministic kernel panic.

Root cause

dkcksum32 in sys/sys/disklabel32.h:150-161:

static __inline u_int16_t
dkcksum32(struct disklabel32 *lp)
{
    u_int16_t *start, *end;
    u_int16_t sum = 0;
    start = (u_int16_t *)lp;
    end = (u_int16_t *)&lp->d_partitions[lp->d_npartitions]; // UNBOUNDED
    while (start < end)
        sum ^= *start++;
    return (sum);
}

d_partitions is a fixed array of MAXPARTITIONS32 (16) entries (disklabel32.h:145). Any d_npartitions > 16 causes end to point past the struct/containing buffer.

l32_readdisklabel guards correctly (:225-226):

} else if (dlp->d_npartitions > MAXPARTITIONS32 ||
           dkcksum32(dlp) != 0) {

The || short-circuits โ€” dkcksum32 is not called when d_npartitions exceeds the array bound.

l32_writedisklabel has no such guard (:363-364):

if (dlp->d_magic == DISKMAGIC32 &&
    dlp->d_magic2 == DISKMAGIC32 && dkcksum32(dlp) == 0) {

Both magic checks are attacker-matched (short-circuit of &&), so dkcksum32 IS evaluated. dlp points into bp->b_data โ€” a buffer of only lp->d_secsize bytes (the new label's sector size). The attacker controls the on-disk label's d_npartitions field.

Threat model & preconditions

  • Attacker position: Anyone who can supply a crafted storage device (USB stick, virtual disk, iSCSI LUN, filesystem image).
  • Impact: Kernel panic (local DoS). OOB read of kernel heap memory, but the data is XOR-folded into a 16-bit checksum โ€” negligible info leak.
  • Required config: Default kernel. The victim must trigger DIOCWDINFO on the device (e.g. running disklabel -w), which requires write access to the device node (root or operator).
  • Reachability: Insert crafted media โ†’ victim attempts to write a new disklabel โ†’ l32_writedisklabel reads existing on-disk label โ†’ dkcksum32 walks past buffer.

Proof of concept

PoC source: findings/poc/DF-0106/

Build & run

# On a DragonFlyBSD host, create a crafted disk image:
dd if=/dev/zero of=/tmp/crafted.img bs=1m count=10
mdconfig -a -t vnode -f /tmp/crafted.img  # returns mdN

# Write a malicious disklabel32 sector at LABELSECTOR32 (offset 0):
# Fields: d_magic=0x82564557, d_magic2=0x82564557,
#          d_npartitions=0xFFFF (65535), d_checksum=calculated
# Use the PoC C program to generate and write the label.

cc -o craft_label craft_label.c
sudo ./craft_label /dev/mdN

# Trigger: write a label to the device (reads existing malicious label first)
sudo disklabel -w /dev/mdN auto

Expected output

Fatal trap 12: page fault while in kernel mode
cpuid = 0
fault virtual address = 0xffff...  (past the I/O buffer)
...
panic: page fault

Impact

Local kernel panic via crafted disk media. The attacker needs social engineering (leave USB stick for a sysadmin) or direct operator access. The OOB read is limited to XOR-folded 16-bit output โ€” no meaningful information disclosure.

Mirror the readdisklabel guard in writedisklabel. Validate d_npartitions before calling dkcksum32:

--- a/sys/kern/subr_disklabel32.c
+++ b/sys/kern/subr_disklabel32.c
@@ -360,7 +360,8 @@
         dlp = (struct disklabel32 *)((char *)dlp + sizeof(long)))
    {
        if (dlp->d_magic == DISKMAGIC32 &&
-           dlp->d_magic2 == DISKMAGIC32 && dkcksum32(dlp) == 0) {
+           dlp->d_magic2 == DISKMAGIC32 &&
+           dlp->d_npartitions <= MAXPARTITIONS32 && dkcksum32(dlp) == 0) {

Defense-in-depth: also harden dkcksum32 itself to clamp end:

--- a/sys/sys/disklabel32.h
+++ b/sys/sys/disklabel32.h
@@ -154,7 +154,7 @@
    u_int16_t sum = 0;

    start = (u_int16_t *)lp;
-   end = (u_int16_t *)&lp->d_partitions[lp->d_npartitions];
+   end = (u_int16_t *)&lp->d_partitions[MIN(lp->d_npartitions, MAXPARTITIONS32)];
    while (start < end)
        sum ^= *start++;
    return (sum);

References

  • FreeBSD fixed a similar issue: dkcksum unbounded d_npartitions (historical advisory).
  • The read path (l32_readdisklabel) already has the guard at :225-226.

Timeline

  • 2026-06-30 Discovered during automated audit.

PoC verification

Evidence pack

findings/poc/DF-0106 ยท 10 files
FileTypeDescriptionSize
poc_writedisklabel.c trigger-source plant crafted on-disk label (d_npartitions=0xFFFF) at sector 1 + DIOCWDINFO32 with valid virgin label -> writedisklabel reads crafted sector -> dkcksum32 OOB 5.3 KB view raw
build.sh build-script cc -O0 -o poc_writedisklabel poc_writedisklabel.c 262 B view raw
run.sh run-script as root: vnconfig vn0 + plant crafted label + run trigger 1.7 KB view raw
build.log build-log final successful build, full output 76 B view raw
run.log run-log 10 representative iterations (ESRCH, no panic) + outcome note 4.1 KB view raw
panic.txt panic-signature no live panic on this path (getpbuf_mem buffer in ~24MiB wired region); points to sibling DF-0107 signature 1.6 KB view raw
env.txt environment uname, cc version, device perms, vfs.nbuf=6081, id maxx 648 B view raw
fix.diff suggested-fix root-cause: clamp dkcksum32 end pointer to MAXPARTITIONS32 (identical to DF-0107; closes both); git apply --check clean 383 B view raw
VERDICT.md verdict full narrative: source-confirmed bug, traced reason no panic, sibling-panic proof, fix 5.7 KB โ†“ raw
README.md readme build/run/expected + outcome explanation 3.8 KB โ†“ raw
README.md readme build/run/expected + outcome explanation
โ†“ download raw

DF-0106 โ€” dkcksum32 OOB read via crafted on-disk label in writedisklabel (PoC)

l32_writedisklabel() (sys/kern/subr_disklabel32.c:363-364) reads the existing on-disk label from media and calls dkcksum32(dlp) on it 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. A crafted disk with d_npartitions = 0xFFFF makes dkcksum32 walk ~1 MiB past the I/O buffer.

Build

On the guest, as the unprivileged build user:

cd poc/DF-0106
sh build.sh        # cc -O0 -o poc_writedisklabel poc_writedisklabel.c

Run

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

cd poc/DF-0106
sh run.sh

The PoC: 1. dd an 8 MiB image, vnconfig -c vn0 /tmp/oob.img โ†’ /dev/vn0, /dev/vn0s0. 2. pwrite a crafted on-disk disklabel32 at sector 1 (byte 512) of /dev/vn0: d_magic = d_magic2 = DISKMAGIC32, d_npartitions = 0xFFFF (sector-aligned 512-byte write). 3. open("/dev/vn0s0", O_RDWR), fetch a valid label via DIOCGDVIRGIN32, then ioctl(fd, DIOCWDINFO32, &virgin).

DIOCWDINFO32 first installs the valid label internally (DIOCSDINFO/l32_setdisklabel, passes), then calls op_writedisklabel = l32_writedisklabel, which READS sector 1 (the crafted label) and runs the unguarded dkcksum32(dlp) (subr_disklabel32.c:363-364).

Expected / observed outcome

The missing-guard bug is confirmed in source (subr_disklabel32.c:363-364 calls dkcksum32(dlp) with no d_npartitions bound, unlike the guarded read path at :225-226), and dkcksum32 provably executes on this path (the scan loop at :358-364 evaluates dkcksum32(dlp) at sector offset 0 where the crafted label's d_magic/d_magic2 both match).

However, on this master DEV kernel the writedisklabel read buffer is a getpbuf_mem buffer whose data area (bp->b_data = b_kvabase) lives in a single ~24 MiB contiguous, fully-wired region (swapbkva_mem, sys/vm/vm_pager.c:235,263-291). The ~1 MiB dkcksum32 walk therefore stays inside mapped memory for ~184 of the ~190 pbufs and returns nonzero (the loop then finds no "valid" on-disk label and returns ESRCH). It would fault only for the ~6 pbufs whose b_kvabase is in the last ~1 MiB of the region โ€” and those sit at the tails of the 16-bucket LIFO free list (getpbuf_mem, vm_pager.c:505-547), unreachable from userspace with only 4 vn devices.

In 400+ harness iterations (tight loops, 80-way concurrency across 4 vn devices, concurrent I/O churn) the writedisklabel path never panicked; every invocation returned ESRCH. So the claimed deterministic panic does not reproduce on this kernel's writedisklabel path.

The identical root-cause panic IS reproduced live via sibling DF-0107, where dkcksum32 is called from l32_setdisklabel on a kmalloc(404) buffer that reliably lands near a thread-stack guard page on fresh boots (panic: vm_fault: fault on stack guard ... l32_setdisklabel+0x57). Fixing dkcksum32 (the shared helper) closes both findings.

Files

file purpose
poc_writedisklabel.c trigger: plant crafted on-disk label + DIOCWDINFO32
build.sh / run.sh exact build / run invocations
build.log final successful build, full output
run.log 10 representative iterations (ESRCH, no panic) + outcome note
panic.txt no live panic on this path; points to sibling DF-0107's signature
env.txt guest uname, cc version, device permissions, ids
fix.diff git-apply-able root-cause fix (clamp dkcksum32) โ€” identical to DF-0107
VERDICT.md full narrative
manifest.json machine-readable catalog
VERDICT.md verdict full narrative: source-confirmed bug, traced reason no panic, sibling-panic proof, fix
โ†“ download raw

DF-0106 โ€” VERDICT: NOT REPRODUCED (claimed panic); bug CONFIRMED in source

One-line verdict

NOT REPRODUCED at runtime on the writedisklabel path: the missing d_npartitions guard at subr_disklabel32.c:363-364 is confirmed real in the audited master DEV source and dkcksum32 provably executes on the path, but the claimed kernel panic does not manifest on this kernel because the getpbuf_mem read buffer sits in a ~24 MiB contiguous wired region and the ~1 MiB OOB walk almost always stays mapped (returns ESRCH, no fault). The identical root-cause panic IS reproduced live via sibling DF-0107 (dkcksum32 from l32_setdisklabel).

Source confirmation (the bug is real)

l32_writedisklabel reads the on-disk label into bp->b_data and scans for a valid one (subr_disklabel32.c:358-364):

for (dlp = (struct disklabel32 *)bp->b_data;
     dlp <= (struct disklabel32 *)((char *)bp->b_data + lp->d_secsize - sizeof(*dlp));
     dlp = (struct disklabel32 *)((char *)dlp + sizeof(long)))
{
    if (dlp->d_magic == DISKMAGIC32 &&
        dlp->d_magic2 == DISKMAGIC32 && dkcksum32(dlp) == 0) {   /* NO GUARD */

There is no dlp->d_npartitions > MAXPARTITIONS32 check before dkcksum32(dlp), in contrast to the read path l32_readdisklabel (:225-226) which guards correctly. The PoC plants a crafted sector at byte 512 with d_magic = d_magic2 = DISKMAGIC32 and d_npartitions = 0xFFFF, so at sector offset 0 both magics match and the && short-circuit evaluates dkcksum32(dlp). That is the unbounded ~1 MiB walk (disklabel32.h:157: end = &lp->d_partitions[lp->d_npartitions]).

Why it does not panic on this kernel (the traced reason)

  1. The read buffer comes from getpbuf_mem(NULL) (subr_disklabel32.c:335), whose bp->b_data = bp->b_kvabase (sys/vm/vm_pager.c:271).
  2. Every pbuf_mem b_kvabase is i * MAXPHYS + swapbkva_mem (vm_pager.c:263), and swapbkva_mem is one contiguous kmem_alloc_pageable of nswbuf_mem * MAXPHYS = 190 * 128 KiB โ‰ˆ 24 MiB (vm_pager.c:235; nswbuf_mem = max(nbuf/32,8) with vfs.nbuf=6081 โ†’ 190; MAXPHYS = 128 KiB, sys/cpu/x86_64/include/param.h:122), backed by wired kernel pages (vm_pager.c:277-288).
  3. A 1 MiB walk from b_kvabase faults only if b_kvabase is in the last ~1 MiB of that 24 MiB region (i.e. pbuf indices โ‰ˆ 184-189). The other ~184 pbufs' walks stay fully inside mapped pages and return nonzero.
  4. Those last-region pbufs sit at the tails of the 16 per-bucket LIFO free lists (bswlist_mem[16], getpbuf_mem at vm_pager.c:505-547); reaching them would require ~184 pbufs checked out simultaneously. Only 4 vn devices exist (/dev/vn0..vn3; the vn driver does not auto-create vn4+ via devfs), and each device serializes its writedisklabel, so the harness can hold at most ~4 pbuf_mem buffers at once โ€” far short of what is needed to drain the free list to a faulting pbuf.

Empirical: 400+ iterations (tight sequential loops, 80-way concurrency across all 4 vn devices, plus concurrent dd I/O churn to rotate the free lists) produced zero panics; every invocation returned ESRCH (l32_writedisklabel:381, the loop found no valid on-disk label because the OOB-corrupted dkcksum32 result is nonzero). The kernel config (sys/config/X86_64_GENERIC) has INVARIANTS but no SLAB_DEBUG/ redzone, so there is no guard page around getpbuf_mem data to make the fault deterministic.

This is the (d) outcome in the procedure โ€” "genuinely not reachable for a panic on this kernel" for the writedisklabel call site specifically, even though the code bug is real and the OOB read executes. It is not a false positive: the guard is genuinely missing at :363-364, confirmed against the read-path guard at :225-226.

Why DF-0107 (same root cause) DOES panic

l32_setdisklabel operates on the ioctl label buffer, a kmalloc(404, M_IOCTLOPS) heap object (sys/kern/sys_generic.c:674-676). That buffer lands in the general kernel malloc arena, where thread-stack guard pages are interspersed, so the 1 MiB walk faults readily on fresh boots โ€” reproduced 3ร— (panic: vm_fault: fault on stack guard ... l32_setdisklabel+0x57). Same dkcksum32, same unbounded d_npartitions, different (luckier-for-attacker) buffer placement.

Impact

The writedisklabel-path OOB read executes on every trigger (CWE-125), but on this kernel it is non-faulting and its 16-bit XOR-folded result is internal only โ€” so there is no observable runtime impact (no panic, no extractable leak) on the writedisklabel path. The realistic, demonstrated DoS for the shared dkcksum32 root cause is via DF-0107.

PoC changes

Authored from scratch (the finding shipped with no PoC folder). First iteration failed (pwrite of the 404-byte label returned EINVAL because disk I/O must be sector-aligned); fixed by writing a full 512-byte sector with the label in its first sizeof(label) bytes. The valid "new" label is obtained from the kernel via DIOCGDVIRGIN32 so the DIOCSDINFO sub-step of DIOCWDINFO passes cleanly and execution reaches l32_writedisklabel.

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 single change closes DF-0106 and DF-0107 (and all other callers) at the helper. Defense-in-depth: also mirror the read-path guard in l32_writedisklabel (subr_disklabel32.c:363-364) with dlp->d_npartitions > MAXPARTITIONS32 ||. The standalone fix.diff implements the root-cause clamp (identical to DF-0107's fix.diff); it supersedes the finding markdown's writedisklabel-only caller patch by fixing the shared helper once. Verified with git apply --check (clean).

Confirmed kernel references

Detail

Exploit chain

n/a (no memory-corruption primitive on this path; the OOB read is XOR-folded into an internal 16-bit checksum that is never returned to userspace, and it does not fault on this kernel's writedisklabel buffer placement). The realistic, demonstrated DoS for the shared dkcksum32 root cause is via sibling DF-0107.

Evidence (decisive lines)

VERDICT.md (full source trace + traced no-panic reason); run.log (10 iterations all return ESRCH, guest stays up); panic.txt (no live panic on this path; cites DF-0107 signature); poc_writedisklabel.c (crafted on-disk label d_npartitions=0xFFFF + DIOCWDINFO32). build.log + build.sh + run.sh reproduce cleanly. fix.diff clamps dkcksum32 (git apply --check clean).

PoC changes

Authored the entire evidence pack from scratch (finding shipped with no PoC folder). First iteration failed (pwrite of the 404-byte label returned EINVAL: disk I/O must be sector-aligned) -> fixed by writing a full 512-byte sector with the label in its leading bytes. The valid 'new' label for the DIOCSDINFO sub-step of DIOCWDINFO is obtained from the kernel via DIOCGDVIRGIN32 so execution reliably reaches l32_writedisklabel.

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-0106 AND DF-0107 at the shared helper. Defense-in-depth: mirror the read-path guard in l32_writedisklabel (subr_disklabel32.c:363-364) with d_npartitions > MAXPARTITIONS32 ||. Full git-apply-able diff in findings/poc/DF-0106/fix.diff (supersedes the finding markdown's writedisklabel-only caller patch).

Verdict

Bug CONFIRMED in source but claimed panic NOT reproduced at runtime. l32_writedisklabel (sys/kern/subr_disklabel32.c:363-364) calls dkcksum32(dlp) on the on-disk label with NO d_npartitions > MAXPARTITIONS32 guard, unlike the read path l32_readdisklabel (:225-226); the scan loop (:358-364) provably evaluates dkcksum32 at sector offset 0 where the PoC's crafted d_magic/d_magic2 match, so the ~1MiB OOB walk executes. However, the read buffer is a getpbuf_mem buffer whose b_data lives in one ~24MiB contiguous wired region (swapbkva_mem, vm_pager.c:235/263); the walk stays mapped for ~184 of ~190 pbufs and returns ESRCH without faulting. The ~6 faulting pbufs sit at the tails of the 16-bucket LIFO free list and are unreachable from userspace with only 4 vn devices. 400+ iterations (incl. 80-way concurrency + I/O churn) produced ZERO panics. The IDENTICAL root-cause panic IS reproduced live via DF-0107 (dkcksum32 from l32_setdisklabel on a kmalloc buffer near a thread-stack guard page).