β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-2878

l32_setdisklabel installs partition tables with no slice-bounds validation β€” DIOCSDINFO32/DIOCWDINFO32 partition devices read/write OUTSIDE the slice (label64 twin validates the same fields)

Field Value
ID DF-2878
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:N
CWE CWE-20 (missing structural validation β†’ access-control bypass)
File sys/kern/subr_disklabel32.c
Lines 249-315 (read-path bound :593-609; label64 twin :275-306)
Area kern
Confidence certain
Discovered 2026-09-02
Pass 2 (GLM 5.3 second pass)
Bucket base:kern
Reported pending
Known CVE none
CVE match novel

Summary

l32_setdisklabel validates a user-supplied disklabel32 only for magic+dkcksum, RAW_PART.p_offset==0 and per-partition p_size<=ds_size. d_partitions[].p_offset is never bounded to the slice and d_npartitions never capped. Once installed in-core, l32_getpartbounds hands the attacker's p_offset/p_size to dscheck, which translates partition-device I/O to (ds_offset + p_offset + secno)*secsize β€” up to ~2TiB past the slice start. The kernel enforces exactly this invariant everywhere else: the label32 reader bounds every partition in l32_fixlabel (incl. the u32 wrap case), and the label64 twin l64_setdisklabel validates the same ioctl (d_npartitions cap, p_boffset/p_bsize slice bounds β†’ ENOSPC). Trigger requires opening the cooked slice node (SYSCAP_RESTRICTEDROOT β€” root-class on stock, hence Low); the boundary is security-relevant wherever a slice device is delegated below host root (jail/devfs rulesets, VM slice passthrough).

Proof of contest

VERIFIED 3/3 (findings/poc/DF-2878/): DIOCSDINFO32 with partition 'a' p_offset=61796/p_size=8 on a 61696-sector slice accepted; after it, pread(vn0s1a,0) returned the out-of-slice marker and pwrite landed at absolute sector 63845 in the backing image β€” READ-ESCAPE: PASS + WRITE-ESCAPE: PASS. Fix (cap d_npartitions BEFORE dkcksum32 β€” also closes the DF-0106/0107 dkcksum family on this path β€” and bound p_offset+p_size to the slice with 64-bit arithmetic) validated on a rebuilt kernel: rejected ENOSPC, no escape, in-slice labels accepted.

Validated fix.diff in findings/poc/DF-2878/.

Timeline

  • 2026-09-02 Discovered during pass-2 audit of subr_disklabel32.c (GLM 5.3); deterministic R/W escape reproduced + fix validated same run.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2878 Β· 15 files
FileTypeDescriptionSize
README.md β€” 3.0 KB ↓ raw
VERDICT.md β€” 5.7 KB ↓ raw
poc2878.c β€” 6.0 KB view raw
mkimg.py β€” 4.4 KB view raw
regress.c β€” 1.4 KB view raw
build.sh β€” 76 B view raw
run.sh β€” 318 B view raw
build.log β€” 765 B view raw
run.log β€” 590 B view raw
run.2.log β€” 590 B view raw
run.3.log β€” 590 B view raw
run.fixed.log β€” 286 B view raw
env.txt β€” 562 B view raw
fix.diff β€” 837 B view raw
verdict.json β€” 5.5 KB view raw

DF-2878 β€” l32_setdisklabel installs partition tables without slice-bounds validation β†’ slice escape (R/W outside the slice via partition device)

Impact

DIOCSDINFO32 / DIOCWDINFO32 on a cooked disk slice accept an in-core disklabel32 whose non-raw partitions have arbitrary p_offset (u32, up to ~2 TiB past the slice start at 512 B/sector). Every subsequent I/O through the partition device node (e.g. /dev/vn0s1a) is translated by dscheck() to (ds_offset + p_offset + secno) * secsize β€” outside the slice β€” giving reads and writes of disk data the slice device was never supposed to reach (other slices of the same disk; past-slice-end areas of the media).

The label64 twin validates exactly this on the same ioctl path (l64_setdisklabel, sys/kern/subr_disklabel64.c:275-306: p_boffset < d_pbase β†’ ENOSPC, p_boffset + p_bsize > d_total_size β†’ ENOSPC, plus d_npartitions > MAXPARTITIONS64 β†’ EINVAL). The label32 read path enforces it too (l32_fixlabel, sys/kern/subr_disklabel32.c:593-609 bounds every partition within the slice). The label32 set path is the one unguarded twin: it checks only p_size > sp->ds_size (subr_disklabel32.c:310-313) and RAW_PART.p_offset != 0 (:306).

Reachability gate (same as DF-2741): opening a cooked disk node requires SYSCAP_RESTRICTEDROOT (sys/kern/subr_disk.c:1072), so the trigger is root-class on a stock host. The kernel nevertheless guarantees slice-relative bounding of partition devices everywhere else; this is the missing enforcement (defence-in-depth + jail/devfs scenarios where a slice device is delegated).

Bonus divergence proof: on the write path, l32_writedisklabel β†’ l32_fixlabel(TRUE) silently bzeroes out-of-bounds partitions in the buffer copy it writes to media (:609), so the on-disk label stays bounded while the in-core one is not β€” confirming the intended invariant the set path forgets.

Reproduce (guest, as root)

# host:  python3 mkimg.py df2878.img && scp df2878.img poc2878.c root@guest:/root/
# guest:
cd /root && cc -O -Wall -o poc2878 poc2878.c
vnconfig -c vn0 /root/df2878.img && sleep 1     # probe creates vn0s1a (in-slice 'a')
./poc2878 /root/df2878.img
vnconfig -u vn0

Expected output (stock kernel)

BEFORE ioctl, vn0s1a@0  ... : df2878-inside-slice
BEFORE ioctl, vn0s1a@61796 ...: read=-1 errno=22     <- boundary normally holds
DIOCSDINFO32: accepted ...
AFTER  ioctl, vn0s1a@0  ... : DF2878-OUTSIDE-SLICE-SECRET
READ-ESCAPE: PASS ...
backing image @63845 ... : DF2878-ESCAPED-WRITE
WRITE-ESCAPE: PASS ...
DF-2878 REPRODUCED

Expected output (patched kernel, fix.diff)

DIOCSDINFO32: REJECTED: No space left on device (ENOSPC) β€” the kernel refuses the out-of-slice label; vn0s1a keeps serving in-slice data.

Fix

fix.diff β€” in l32_setdisklabel: cap d_npartitions before dkcksum32() (also closes the DF-0106/0107 dkcksum OOB family on this path) and require p_offset + p_size <= ds_size for every partition, mirroring l64_setdisklabel.

VERDICT.md
↓ download raw

DF-2878 β€” VERDICT

Finding: l32_setdisklabel() (sys/kern/subr_disklabel32.c:249-315) installs an in-core disklabel32 via DIOCSDINFO32/DIOCWDINFO32 with no structural bounds validation of the partition table β€” d_partitions[].p_offset is never checked against the slice (the final loop at :310-313 checks only p_size > sp->ds_size), d_npartitions is never capped (that half is the DF-0106/0107 dkcksum family), and there is no p_offset + p_size bounds/overflow check.

Verdict: REPRODUCED (deterministic, 3/3 runs, fresh image)

Primitive: slice-boundary escape (out-of-slice read and write on the underlying device, through an ordinary cooked partition node).

How it works, end to end

  1. Hostile/delegated label via ioctl(vn0s1_fd, DIOCSDINFO32, &label): dsioctl() (sys/kern/subr_diskslice.c:561-618) requires slice != WHOLE_DISK_SLICE && part == WHOLE_SLICE_PART, FWRITE, and β€” if a label already exists β€” same label type (:590-591). It hands the user label to ops->op_setdisklabel = l32_setdisklabel.
  2. l32_setdisklabel verifies only magic + dkcksum32 (:264-265), the open-partition compat loop (:272-301, no partitions open β‡’ skipped), copies the label into the in-core slot (:304), and checks RAW_PART.p_offset == 0 (:306) and p_size <= ds_size per partition (:310-313). No p_offset bound, no d_npartitions cap. The temp label becomes the installed in-core label (subr_diskslice.c:616-617).
  3. dsopen/dscheck on the (already existing) partition node /dev/vn0s1a: l32_getpartbounds (subr_disklabel32.c:123-135) returns the attacker's p_offset/p_size; dscheck (subr_diskslice.c:206-212, 280-281) translates every I/O to (sp->ds_offset + p_offset + secno) * dss_secsize β€” absolute sectors outside the slice.
  4. Observed on the guest (64 MiB vn image, MBR slice = sectors [2048, 63744), marker at absolute 63844): - BEFORE: vn0s1a returns in-slice filler; out-of-slice offset β†’ EINVAL (the boundary holds with the on-disk label). - DIOCSDINFO32 accepted with 'a' at slice-relative 61796 (= 100 sectors past the slice end), p_size 8. - AFTER: pread(vn0s1a, 0) returns DF2878-OUTSIDE-SLICE-SECRET (planted at absolute 63844, outside the slice). - pwrite(vn0s1a, 512) landed at absolute 63845 in the backing image (verified by reading the backing file; sector was zero before).

Why this is a real invariant violation (not just "root being root")

  • The reader enforces slice bounds for every partition: l32_fixlabel (subr_disklabel32.c:593-609) bounds each on-media partition to [ds_offset, ds_offset+ds_size) (including the u32 wraparound case, :596) and zeroes offenders.
  • The label64 twin enforces it on the same set ioctl: l64_setdisklabel (subr_disklabel64.c:275-306): d_npartitions > MAXPARTITIONS64 β†’ EINVAL, p_boffset < d_pbase β†’ ENOSPC, p_boffset + p_bsize > d_total_size β†’ ENOSPC, plus alignment checks.
  • The writer enforces it too: l32_writedisklabel β†’ l32_fixlabel(TRUE) silently bzeroes out-of-bounds partitions in the copy it writes to media (:609) β€” so the on-disk label stays bounded while the in-core one is not: the kernel itself proves the invariant the set path forgets.
  • Reachability gate (verified during DF-2741, same guest): opening cooked disk nodes requires SYSCAP_RESTRICTEDROOT (sys/kern/subr_disk.c:1072), so on a stock host the trigger is root-class. The boundary matters wherever a slice device is delegated below the host root (jail/devfs rulesets, VMs with a slice passthrough): the kernel promises partition nodes stay inside their slice, and everywhere except l32_setdisklabel it keeps that promise.

Severity filed Low per project rubric ("already-privileged user" on a stock host); consequence class identical to DF-0134 (Medium, label64 reader) which needs no privileges at all, and the fix mirrors label64 exactly.

Fix validation

fix.diff adds to l32_setdisklabel: - nlp->d_npartitions > MAXPARTITIONS32 β†’ EINVAL before dkcksum32() (also closes the DF-0106/0107 dkcksum32 OOB-read family on this path), and - p_offset + p_size > sp->ds_size β†’ ENOSPC for every partition (u64 add, no wrap), replacing the p_size-only check.

Applied to the guest's /usr/src, make nativekernel (RC=0, 14,897 lines), make installkernel (RC=0), reboot into kernel #1: Wed Sep 2 19:31:54 UTC 2026, exact PoC re-run on a pristine image (run.fixed.log):

BEFORE ioctl, vn0s1a@0  ... : df2878-inside-slice      <- cooked access intact
DIOCSDINFO32: REJECTED: No space left on device        <- ENOSPC, label64 semantics
=> kernel enforces slice bounds on the set path (patched?)
  • fix_status: fixed β€” the out-of-slice label is rejected (ENOSPC, exactly l64_setdisklabel's behavior at subr_disklabel64.c:304-305); the previously observed READ-ESCAPE/WRITE-ESCAPE are gone; the guest stayed healthy.
  • No regression: regress.c installs an equivalent IN-SLICE label ('a' at slice-relative 2048) on the patched kernel β€” accepted (in-slice label accepted, RC=0). Normal labeling still works.
  • Guest reset to the clean-source snapshot after validation.

Baseline (stock kernel #0): see run.log, run.2.log, run.3.log β€” READ-ESCAPE PASS + WRITE-ESCAPE PASS, exit 0, 3/3.

Kernel references

sys/kern/subr_disklabel32.c:264-266, 306-313 (bug), :123-135 (consumer), :593-609 (reader enforces the invariant), :592-614 (writer sanitizes); sys/kern/subr_diskslice.c:561-618 (DIOCSDINFO glue), :199-212+279-281 (dscheck offset translation); sys/kern/subr_disklabel64.c:275-306 (validating twin); sys/kern/subr_disk.c:228-270 (partition node creation), :1072 (SYSCAP_RESTRICTEDROOT open gate).

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff applied to guest /usr/src (patch -p1, both hunks clean), make nativekernel RC=0, make installkernel RC=0, reboot into #1: the same PoC that escaped 3/3 on stock #0 now gets ENOSPC from DIOCSDINFO32 and performs no out-of-slice I/O; regress.c confirms in-slice labels are still accepted (no regression); guest healthy throughout.

['run.fixed.log (patched rejection, no escape)', "VERDICT.md 'Fix validation' section", 'fix.diff (git-apply-able, authored against read-only sys/)']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Wed Sep 2 19:31:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

open /dev/vn0s1 O_RDWR (whole-slice partition, FWRITE) -> ioctl DIOCSDINFO32 with crafted 404-byte label {magic, dkcksum ok, d_npartitions=3, 'c'(RAW_PART): p_offset=0 p_size=61696, 'a': p_offset=61796 p_size=8 fstype=FS_OTHER} -> accepted unvalidated (subr_disklabel32.c:306-313 checks pass) -> open existing /dev/vn0s1a node -> pread(0) returns marker bytes planted at absolute sector 63844 (outside slice [2048,63744)) -> pwrite(512) lands at absolute sector 63845 in the backing image (verified via the backing file; sector zeroed before). No kernel memory corruption; the primitive is device-scope escape.

Evidence (decisive lines)

["run.log / run.2.log / run.3.log: 'DIOCSDINFO32: accepted', 'READ-ESCAPE: PASS', 'WRITE-ESCAPE: PASS', 'DF-2878 REPRODUCED' (run 3 on a pristine image, escape sector verified zero beforehand)", "run.fixed.log: patched kernel #1 -> 'DIOCSDINFO32: REJECTED: No space left on device', no escape", 'build.log + VERDICT.md fix section: make nativekernel RC=0 (14897 lines), installkernel RC=0, regress.c in-slice label still accepted on patched kernel', "mkimg.py: byte-exact crafted MBR + on-disk label32 (valid dkcksum32) accepted by the reader, proving the reader's fixlabel bounds are the intended invariant"]

PoC changes

No seed was present; the entire pack was authored from scratch: mkimg.py builds a 64MiB vn image with an MBR slice [2048,63744), a VALID on-disk disklabel32 (byte-exact 404-byte layout, LOCARE-verified offsets, dkcksum32=0) whose 'a' partition is in-slice (so the probe creates /dev/vn0s1a), and an out-of-slice marker at absolute 63844; poc2878.c (_Static_assert on struct size/offsets) shows the before/after contrast through the same node and verifies the escaped write in the backing file.

Verified recommended fix

In l32_setdisklabel: reject nlp->d_npartitions > MAXPARTITIONS32 before dkcksum32(), and require (u_int64_t)p_offset + p_size <= sp->ds_size for every partition (mirroring l64_setdisklabel)

Verdict

REPRODUCED 3/3 (deterministic, fresh image). l32_setdisklabel (sys/kern/subr_disklabel32.c:249-315) installs an in-core disklabel32 via DIOCSDINFO32/DIOCWDINFO32 with no structural bounds validation: d_partitions[].p_offset is never checked against the slice (only p_size <= ds_size is, at :310-313) and d_npartitions is never capped. dscheck then translates partition-node I/O to (ds_offset + p_offset + secno) * secsize, so /dev/vn0s1a with p_offset=61796 on a 61696-sector slice read and wrote absolute sectors 63844/63845 OUTSIDE the slice (verified in the backing image). The label64 twin validates exactly this on the same ioctl (subr_disklabel64.c:275-306) and the label32 reader/writer enforce it (l32_fixlabel :593-609); the set path is the single unguarded twin. Impact enum caveat: 'leak' is the closest kernel-centric value; the demonstrated primitive is an out-of-slice READ+WRITE on the underlying device (data boundary escape), NOT a kernel-memory disclosure. Trigger requires a credential that can open the cooked slice node (SYSCAP_RESTRICTEDROOT, subr_disk.c:1072) i.e. root-class on a stock host; the boundary matters for delegated slice devices (jail/devfs, VM passthrough), hence severity Low per rubric.