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
DIOCWDINFOon the device (e.g. runningdisklabel -w), which requires write access to the device node (root or operator). - Reachability: Insert crafted media โ victim attempts to write a
new disklabel โ
l32_writedisklabelreads 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.
Recommended fix
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:
dkcksumunbounded 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| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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 |
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)
- The read buffer comes from
getpbuf_mem(NULL)(subr_disklabel32.c:335), whosebp->b_data = bp->b_kvabase(sys/vm/vm_pager.c:271). - Every
pbuf_memb_kvabaseisi * MAXPHYS + swapbkva_mem(vm_pager.c:263), andswapbkva_memis one contiguouskmem_alloc_pageableofnswbuf_mem * MAXPHYS=190 * 128 KiB โ 24 MiB(vm_pager.c:235;nswbuf_mem = max(nbuf/32,8)withvfs.nbuf=6081โ 190;MAXPHYS = 128 KiB,sys/cpu/x86_64/include/param.h:122), backed by wired kernel pages (vm_pager.c:277-288). - A 1 MiB walk from
b_kvabasefaults only ifb_kvabaseis 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. - Those last-region pbufs sit at the tails of the 16 per-bucket LIFO free
lists (
bswlist_mem[16],getpbuf_mematvm_pager.c:505-547); reaching them would require ~184 pbufs checked out simultaneously. Only 4vndevices exist (/dev/vn0..vn3; thevndriver does not auto-createvn4+via devfs), and each device serializes itswritedisklabel, so the harness can hold at most ~4pbuf_membuffers 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.
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 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
- sys/sys/disklabel32.h:157
- sys/kern/subr_disklabel32.c:358
- sys/kern/subr_disklabel32.c:363
- sys/kern/subr_disklabel32.c:364
- sys/kern/subr_disklabel32.c:225
- sys/kern/subr_disklabel32.c:335
- sys/vm/vm_pager.c:235
- sys/vm/vm_pager.c:263
- sys/vm/vm_pager.c:271
- sys/vm/vm_pager.c:505
- sys/platform/pc64/x86_64/machdep.c:460
- sys/config/X86_64_GENERIC
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).