GPT entry #127 maps to slice index 128, which dkmakeminor() silently truncates to 7 bits: duplicate major:minor cdevs and s127<->s0 cross-slice data aliasing (reads AND writes)
| Field | Value |
|---|---|
| ID | DF-2963 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:N/I:H/A:N |
| CWE | CWE-197 |
| File | sys/kern/subr_diskgpt.c |
| Lines | 175, 201-204, 222 (minor: diskslice.h:252-253; probe: subr_disk.c:448-451) |
| Area | kern/disk |
| 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
gptinit() allocates 130 slice slots and maps GPT entry #N (Nβ₯1) to dss_slices[BASE_SLICE+N-1], so entry #127 occupies slice index 128 while dss_nslices=130. But DKMAXSLICES=128 and dkmakeminor() encodes the slice field with only 7 bits β slice 128's bit 7 is silently dropped. disk_probe() therefore creates the 'adXs127' whole-slice device with a minor bit-identical to slice 0's; every consumer decodes it back via dkslice() to slice 0. devfs performs no duplicate-(major,minor) check, so two cdevs with the same dev_t coexist. Any GPT with 128 entries β the gpt(8)/gdisk DEFAULT, so legitimate media, not just crafted images β makes the kernel publish /dev/adXs127 whose reads and writes silently address partition s0 (entry 0's extent). VERIFIED on the guest: st_rdev(vn0s127)== st_rdev(vn0s0)==0x1e100807 on distinct devfs inodes; read via vn0s127 returns entry-0 data; a 512-byte write via vn0s127 lands on absolute LBA 34, corrupting s0's partition; DIOCGPART on vn0s127 reports s0's extent. A user with permission on the s127 node (default root:operator 0640) corrupts a different partition than the one named; devfs/udev/audit keyed on dev_t see two devices with one identity. No memory corruption: all dss_slices accesses are bounds-checked β no uid=0 chain from this primitive; escalation ceiling is cross-partition data-integrity violation. Fix validated in-guest (bound to 127 representable entries + loud dmesg): vn0s127 not created, s0/s126 unregressed.
Timeline
- 2026-09-02 Discovered during pass-2 audit of subr_diskgpt.c (GLM 5.3); cross-slice aliasing reproduced + fix validated.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2963 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 1.8 KB | β raw | |
| VERDICT.md | β | 5.4 KB | β raw | |
| gen_gpt.c | β | 4.8 KB | view raw | |
| trigger.c | β | 2.9 KB | view raw | |
| build.sh | β | 218 B | view raw | |
| run.sh | β | 551 B | view raw | |
| build.log | β | 220 B | view raw | |
| run.log | β | 1.4 KB | view raw | |
| run.2.log | β | 705 B | view raw | |
| run.3.log | β | 347 B | view raw | |
| run.fixed.log | β | 1.5 KB | view raw | |
| env.txt | β | 994 B | view raw | |
| fix.diff | β | 1.3 KB | view raw | |
| manifest.json | β | 1.5 KB | view raw | |
| verdict.json | β | 4.5 KB | view raw |
DF-2963 β GPT entry #127 maps to slice index 128, which the devfs minor encoding silently truncates β duplicate major:minor and s127βs0 data aliasing
Build
Upload gen_gpt.c, trigger.c, build.sh, run.sh to /root/df2963/ on the
guest, then as root:
sh /root/df2963/build.sh # compiles gen_gpt + trigger, writes /root/df2963.img (64 MB)
Run
sh /root/df2963/run.sh # as root; attaches vn0, runs trigger, detaches
Expected (vulnerable kernel)
/dev/vn0s0 : st_ino=311 st_rdev=0x1e100807 /dev/vn0s126 : st_ino=317 st_rdev=0xfe1f0807 /dev/vn0s127 : st_ino=321 st_rdev=0x1e100807 <-- identical rdev to vn0s0, different inode data: vn0s127 first sector = "ENTRY0ZONE_compat_slice0" <-- slice 0's data, not entry 127's DIOCGPART vn0s127: media_offset=17408 media_blocks=30 <-- slice 0's extent, not 1000/100 raw LBA 1000 = "ENTRY127ZONE_real_s127_data" <-- where the name promises to point
Write demonstration (see run.3.log): a 512-byte write via /dev/vn0s127 lands on absolute LBA 34 (slice 0/entry 0's region), verified read-back via /dev/vn0s0 and the raw device; entry 127's true region (LBA 1000) is untouched.
Expected (kernel patched with fix.diff)
ls: /dev/vn0s127: No such file or directory vn0s0 / vn0s126 unchanged and correct dmesg: "disk: GPT has 128 entries; only 127 slice devices representable, ignoring entry 127"
Files
gen_gpt.c image generator (protective MBR + GPT header w/ CRCs + 128 entries + data tags) trigger.c stat()/read/DIOCGPART comparison across vn0s0, vn0s126, vn0s127 + raw controls build.sh compile + generate image run.sh attach β demonstrate β detach fix.diff one-hunk fix bounding GPT slice devices to representable minor indices
DF-2963 VERDICT β reproduced (deterministic), fix validated
Status: reproduced. Impact: silent cross-slice data aliasing + duplicate major:minor device numbers (integrity/confusion, not memory corruption). Confidence: certain. Fix: validated (fixed).
Root cause (code trace)
sys/kern/subr_diskgpt.c:175βgptinit()replaces the minimal slice struct withdsmakeslicestruct(BASE_SLICE + MAX_GPT_ENTRIES, info)= 130 slice slots (BASE_SLICE=2, MAX_GPT_ENTRIES=128), andsys/kern/subr_diskgpt.c:222setsssp->dss_nslices = BASE_SLICE + i(up to 130).sys/kern/subr_diskgpt.c:201-204β entry #0 maps todss_slices[COMPATIBILITY_SLICE](=0); entry #N (Nβ₯1) maps todss_slices[BASE_SLICE+N-1], so entry #127 maps to slice index 128.sys/sys/diskslice.h:107βDKMAXSLICES 128(slice indices 0..127);sys/sys/diskslice.h:252-253βdkmakeminor()encodes the slice field with masksslice & 0x0f(βbits 16-19) andslice & 0x70(βbits 29-31): 7 bits; bit 7 of slice (0x80) is silently dropped.sys/kern/subr_disk.c:448-451βdisk_probe()creates the whole-slice device for every sized slice viamake_dev_covering(..., dkmakewholeslice(dkunit(dev), i), ..., "%ss%d"). For i=128 the minor equals slice 0's minor bit-for-bit:dkmakeminor(unit, 128, 255) == dkmakeminor(unit, 0, 255).sys/sys/diskslice.h:327-333β every consumer (dsopensubr_diskslice.c:771,dscheck:380/:107,dssize,dsioctl:380) decodes the device withdkslice(dev)β 0 β so the node namedadXs127actually addressesdss_slices[0], the compatibility slice holding GPT entry 0's extent.sys/vfs/devfs/devfs_core.c:2439-2484, 2571-2578βdevfs_new_cdev()/devfs_link_dev()perform no duplicate-(major,minor) check, so both cdevs coexist as distinct devfs inodes with identicalsi_inode.
Reproduction (kernel #0, stock, baseline)
sh run.sh on the stock INVARIANTS kernel (full output in run.log):
/dev/vn0s0 : st_ino=311 st_rdev=0x1e100807 /dev/vn0s126 : st_ino=317 st_rdev=0xfe1f0807 /dev/vn0s127 : st_ino=321 st_rdev=0x1e100807 <-- duplicate dev_t, distinct inode vn0s127 data read -> "ENTRY0ZONE_compat_slice0" (slice 0's region, LBA 34) vn0s126 data read -> "ENTRY126ZONE" (correct, LBA 160) raw LBA 1000 -> "ENTRY127ZONE_real_s127_data" (where s127 *should* point) DIOCGPART vn0s127 -> media_offset=17408 media_blocks=30 (slice 0's extent)
Write aliasing (run.3.log): 512-byte write via /dev/vn0s127 β lands at
absolute LBA 34; read-back via /dev/vn0s0 and raw /dev/vn0 shows the
payload; entry 127's true region unchanged. Reprobe attach/detach cycles x3
survive without panic (stable confusion, not a crash).
Threat model: identical to the file's established crafted-media model
(vnconfig by root, supplied VM disk image, physical media swap) β AND hits
legitimate media: any spec-compliant 128-entry GPT (the gpt(8)/gdisk
default) attached to DragonFly gets an s127 device that reads and writes the
first partition's data. An unprivileged user needs only read/write permission
on the s127 node (default root:operator 0640) to corrupt entry 0's partition
through the wrong name.
Why this is not DF-0074/DF-2741 or DF-0228..0231
Those cover the DIOCGSLICEINFO heap overflow (dss_nslices=130 copied into a 16-slice ioctl buffer) and the four header-parsing defects. This finding is a different defect at a different layer: the slice-index-to-devfs-minor truncation (identity/aliasing), which exists even on kernels where none of the other five are exercised.
Exploit-chain assessment
No memory-corruption primitive: every dss_slices access is bounds-checked
against dss_nslices (subr_diskslice.c:115/:385), and the alias target is a
valid in-array slice. The impact ceiling is silent data-integrity violation
across partitions (write to partition named s127 corrupts partition s0) and
ambiguous device identity (two live cdevs with the same dev_t; udev events,
devfs_find_device_by_devid, and audit/devfs rules key on dev_t). uid=0 is
not reachable from this primitive alone.
Fix validation
fix.diff bounds created GPT slice devices to indices representable in the
minor encoding (MAX_GPT_SLICES = DKMAXSLICES - BASE_SLICE + 1 = 127
entries: #0..#126 β slice indices 0..127). Applied to clean guest /usr/src,
make -j6 nativekernel && make installkernel, reboot into kernel #1
(run.fixed.log):
/dev/vn0s127not created ("No such file or directory")- vn0s0 (offset 17408/30 blocks, correct data) and vn0s126 (81920/2, correct) unchanged β no regression on representable slices
- dmesg:
disk: GPT has 128 entries; only 127 slice devices representable, ignoring entry 127(loud, not silent) - entry 127's data reachable only via the raw device β no aliasing path remains
fix_status: fixed (baseline reproduced on #0, bad behavior absent on #1).
Alternatives considered
Widening the minor encoding is impossible without ABI surgery: bits 8-15 of
the kernel minor are reserved (rejected by makeudev, kern_conf.c:156) and
all other bits are allocated (see layout comment diskslice.h:233-240), so
DKMAXSLICES cannot grow for this minor format. Rejecting the whole 128-entry
table (entries > 127 at the :136 validation) would break every legitimate
gpt(8)-created disk; skipping only the unrepresentable entry is the
minimal-behavior-change fix.
Fix verification
fixedvm.sh reset with-src; fix.diff applied to guest /usr/src (patch -p0, both hunks clean); make -j6 nativekernel + make installkernel; reboot into kernel #1. Exact PoC re-run: /dev/vn0s127 not created, no duplicate st_rdev, vn0s0 (offset 17408/30, correct data) and vn0s126 (81920/2, correct data) unregressed, dmesg shows 'disk: GPT has 128 entries; only 127 slice devices representable, ignoring entry 127'. Bad behavior (aliasing + duplicate dev_t) fully absent.
run.fixed.log; dmesg line quoted in VERDICT.md
Confirmed kernel references
Detail
Exploit chain
crafted 128-entry GPT image -> vnconfig/boot-time auto-probe -> gptinit writes entry 127 into dss_slices[128] -> disk_probe creates 'vn0s127' with dkmakeminor(unit,128,255) == dkmakeminor(unit,0,255) -> dkslice() decodes 0 for the s127 cdev -> dscheck/dsopen/DIOCGPART all address dss_slices[0] (entry 0's extent) -> reads and writes through /dev/vn0s127 silently read/corrupt partition s0. Ceiling: cross-partition integrity violation + duplicate dev_t in devfs; no kernel memory corruption -> no uid=0.
Evidence (decisive lines)
["run.log: st_rdev vn0s0 == vn0s127 == 0x1e100807 with distinct st_ino (311 vs 321); vn0s127 reads 'ENTRY0ZONE_compat_slice0' while raw LBA 1000 holds 'ENTRY127ZONE_real_s127_data'; DIOCGPART vn0s127 reports offset 17408/30 blocks (slice 0's extent)", "run.3.log: 512-byte write via /dev/vn0s127 lands on abs LBA 34 (verified via /dev/vn0s0 and raw /dev/vn0); entry 127's true region untouched", 'run.2.log: three reprobe attach/detach cycles survive - stable confusion, no panic', "run.fixed.log + dmesg: on fix.diff kernel #1 '/dev/vn0s127: No such file or directory', 'disk: GPT has 128 entries; only 127 slice devices representable, ignoring entry 127', vn0s0/vn0s126 correct", 'fix.diff: bounds loop to MAX_GPT_SLICES (DKMAXSLICES-BASE_SLICE+1=127 entries)']
PoC changes
n/a (finding + PoC authored together in this run)
Verified recommended fix
Bound GPT slice-device creation to minor-representable indices: loop over min(entries, DKMAXSLICES-BASE_SLICE+1) in gptinit() and log when entry #127 is skipped (fix.diff).
Verdict
Deterministic reproduction on the stock kernel: a crafted (or any legitimate 128-entry) GPT makes the kernel create /dev/adXs127 with a minor identical to /dev/adXs0 because dkmakeminor() drops slice bit 7 for slice index 128 (gptinit maps entry #127 to BASE_SLICE+126=128, beyond DKMAXSLICES-1=127). Two live devfs cdevs share one major:minor, and every read AND write through the s127 node silently addresses slice 0's data (proven: read returns entry-0 pattern; 512-byte write lands on abs LBA 34). No memory corruption (all dss_slices accesses bounds-checked), so no uid0 chain; impact is cross-partition data-integrity violation and device-identity confusion. fix.diff (bound created GPT slices to representable minor indices, entries #0..#126) rebuilt and validated on kernel #1: vn0s127 no longer created, s0/s126 unregressed, loud dmesg warning.
No comments yet.