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.
Recommended fix
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)
PoC verification
Evidence pack
findings/poc/DF-2878 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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.
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
- Hostile/delegated label via
ioctl(vn0s1_fd, DIOCSDINFO32, &label):dsioctl()(sys/kern/subr_diskslice.c:561-618) requiresslice != WHOLE_DISK_SLICE && part == WHOLE_SLICE_PART, FWRITE, and β if a label already exists β same label type (:590-591). It hands the user label toops->op_setdisklabel=l32_setdisklabel. l32_setdisklabelverifies 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 checksRAW_PART.p_offset == 0(:306) andp_size <= ds_sizeper partition (:310-313). Nop_offsetbound, nod_npartitionscap. The temp label becomes the installed in-core label (subr_diskslice.c:616-617).dsopen/dscheckon the (already existing) partition node/dev/vn0s1a:l32_getpartbounds(subr_disklabel32.c:123-135) returns the attacker'sp_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.- Observed on the guest (64 MiB vn image, MBR slice = sectors [2048, 63744),
marker at absolute 63844):
- BEFORE:
vn0s1areturns in-slice filler; out-of-slice offset βEINVAL(the boundary holds with the on-disk label). -DIOCSDINFO32accepted with 'a' at slice-relative 61796 (= 100 sectors past the slice end), p_size 8. - AFTER:pread(vn0s1a, 0)returnsDF2878-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 exceptl32_setdisklabelit 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.cinstalls 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
fixedfix.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/)']
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.
No comments yet.