DIOCGSLICEINFO heap buffer overflow via crafted GPT disk image (dss_nslices > MAX_SLICES)
| Field | Value |
|---|---|
| ID | DF-0074 |
| Status | new |
| Severity | High |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:H |
| CWE | CWE-122 Heap-based Buffer Overflow |
| File | sys/kern/subr_diskslice.c |
| Lines | 556-559 |
| Area | kern (disk slice / disklabel parsing) |
| Confidence | certain |
| Discovered | 2026-06-30 |
| Reported | pending |
Summary
The DIOCGSLICEINFO handler in dsioctl copies the live kernel struct
diskslices into the ioctl data buffer using a length derived from the
actual slice count dss_nslices:
case DIOCGSLICEINFO:
bcopy(ssp, data, (char *)&ssp->dss_slices[ssp->dss_nslices] -
(char *)ssp); /* sys/kern/subr_diskslice.c:557 */
return (0);
But the destination data buffer is sized sizeof(struct diskslices) β encoded
into the ioctl command by _IOR('d', 111, struct diskslices) (diskslice.h:96)
β which only has room for MAX_SLICES = 16 slice slots (diskslice.h:122,176).
The kernel allocates exactly that many bytes in mapped_ioctl
(sys_generic.c:668).
For a GPT-formatted disk, dsmakeslicestruct allocates
BASE_SLICE + MAX_GPT_ENTRIES = 2 + 128 = 130 slots (subr_diskgpt.c:175) and
sets dss_nslices = BASE_SLICE + i up to 130 (:222, where i iterates the
on-disk entry count up to MAX_GPT_ENTRIES). The on-disk entries field only
needs to be >= 15 for dss_nslices to exceed MAX_SLICES.
Net result: a GPT disk with >= 15 partition entries causes the bcopy to
write (dss_nslices β 16) Γ sizeof(struct diskslice) bytes past the end of the
~4 KB data buffer β up to ~29 KB of kernel-heap overrun when
dss_nslices = 130. The overrun bytes include attacker-controlled fields
(ds_offset, ds_size, ds_type_uuid, ds_stor_uuid) from each valid GPT
entry (subr_diskgpt.c:258-262). The source side stays in-bounds (130-slot
alloc); only the destination overflows.
Reachability: an attacker presents a crafted GPT disk image (USB mass
storage, mdconfig/virtual disk, etc.). The auto-probe populates
dss_nslices. Any subsequent DIOCGSLICEINFO ioctl on a slice device of that
disk corrupts the kernel heap. Reachable by any local principal that can open the
slice device node and issue the ioctl (common disk-inspection utilities do this).
Impact: kernel heap corruption suitable for local privilege escalation /
kernel-mode code execution via heap grooming; reliable kernel-panic DoS is
trivial. The copyout in mapped_ioctl only copies sizeof(struct diskslices)
bytes back, so the corruption is confined to kernel heap.
Recommended fix
Cap the copy at the destination size:
--- a/sys/kern/subr_diskslice.c
+++ b/sys/kern/subr_diskslice.c
@@ case DIOCGSLICEINFO:
- bcopy(ssp, data, (char *)&ssp->dss_slices[ssp->dss_nslices] -
- (char *)ssp);
+ {
+ u_int n = (ssp->dss_nslices > MAX_SLICES) ?
+ MAX_SLICES : ssp->dss_nslices;
+ bcopy(ssp, data, (char *)&ssp->dss_slices[n] - (char *)ssp);
+ }
return (0);
Better: replace the raw-struct ioctl with a structured copy that does not expose kernel pointers (see DF-0075) and uses a dedicated output structure sized to the actual returned data.
Proof of concept
See findings/poc/DF-0074/. A script builds a minimal GPT disk image with 128
partition entries (all nil-typed except a few), attaches it, and issues
DIOCGSLICEINFO to trigger the heap overflow.
Timeline
- 2026-06-30 Discovered during automated file-by-file audit of
sys/kern/subr_diskslice.c. - pending Reported to DragonFlyBSD security contact.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0074 Β· 19 files| File | Type | Description | Size | |
|---|---|---|---|---|
| trigger.c | trigger-source | minimal: open slice dev + single DIOCGSLICEINFO; prints nslices (proves the overrun fired) | 1.0 KB | view raw |
| trigger_stress.c | trigger-source | ioctl + slab churn (64 fds open/close, 40 fork/exit) + parallel flood to surface the async slab panic | 2.1 KB | view raw |
| build_gpt.py | trigger-source | host-side GPT image generator (128 entries -> dss_nslices=130 -> 29184-byte overrun) | 4.2 KB | view raw |
| overflow.img | trigger-asset | 1 MiB crafted GPT image (built by build_gpt.py); attach via vnconfig | 1.0 MB | β download |
| build.sh | build-script | self-contained guest build: cc -O2 -o trigger/trigger_stress | 692 B | view raw |
| run.sh | run-script | self-contained guest run: vnconfig + trigger + stress + flood (MUST run as root -- see VERDICT reachability) | 1.3 KB | view raw |
| build.log | build-log | full untrimmed compiler output (final successful build) | 244 B | view raw |
| run.log | run-log | decisive baseline #0 run: nslices=130 + stress + 16-proc flood, then async panic | 2.3 KB | view raw |
| baseline_panic.txt | panic-signature | Fatal trap 12 in _kmalloc+0x44a from dfbsd-qemu/boot.log (this run, #0 baseline) | 13.6 KB | view raw |
| panic.txt | panic-signature | three distinct panic signatures from prior fresh-boot runs (slab_cleanup, slaballoc corrupted zone, hammer2 cascade) | 5.4 KB | view raw |
| PANIC.txt | panic-signature | archived 6.4.2-RELEASE panic capture (confirms bug is not master-only) | 4.9 KB | view raw |
| fix.diff | suggested-fix | git-apply-able bounds clamp: cap bcopy length at MAX_SLICES (matches finding proposal) | 766 B | view raw |
| fix_build.log | build-log | full untrimmed output of the patched-kernel nativekernel build (NK_DONE rc=0) | 5.6 MB | β download |
| fix_run.log | run-log | patched #1 kernel: 96-trigger flood + 30 stress iters, no panic, guest up | 1.7 KB | view raw |
| env.txt | environment | uname, cc, sysctls, devfs perms, RESTRICTEDROOT reachability caveat | 2.1 KB | view raw |
| README.md | readme | human-facing reproduce + expected-result guide | 3.9 KB | β raw |
| VERDICT.md | verdict | full root-cause + reachability hard-blocker + Phase 8 fix-validation narrative | 15.8 KB | β raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-0074 PoC β DIOCGSLICEINFO heap overflow via crafted GPT disk image
What this proves
The DIOCGSLICEINFO handler (sys/kern/subr_diskslice.c:557) bcopy()s
dss_nslices-worth of struct diskslice (256 B each) into the ioctl data
buffer, which mapped_ioctl sizes at sizeof(struct diskslices) = 4128 B
(only MAX_SLICES = 16 slots). A GPT disk whose header advertises 128 entries
makes the kernel set dss_nslices = BASE_SLICE + 128 = 130
(sys/kern/subr_diskgpt.c:175,222), so the bcopy writes
32 + 130*256 = 33312 bytes into the 4128-byte buffer β a deterministic
29184-byte (~28 KB) overrun of kernel heap on every call. With slab
churn the overrun reliably surfaces as a kernel panic; the underlying memory
corruption is weaponizable for local privilege escalation.
Verified on
DragonFly 6.5-DEVELOPMENT master DEV
(v6.5.0.1712.g89e6a-DEVELOPMENT #1, X86_64_GENERIC). See VERDICT.md for
the full narrative and env.txt for the guest environment.
Reproduce
# 1. On a host with python3: build the crafted GPT image (128 entries).
python3 build_gpt.py overflow.img
# 2. Copy the whole folder + overflow.img to the guest (as maxx).
# scp -r . dfbsd-maxx:poc/DF-0074/
# 3. On the guest, build the triggers (as maxx).
sh ./build.sh # -> trigger, trigger_stress
# 4. Attach the image and fire the overflow (as root; see Reachability).
# sh ./run.sh vn0
#
# The single trigger prints "DIOCGSLICEINFO returned nslices=130"
# (proof the oversized bcopy executed). The stress/flood section then
# forces the slab corruption to surface. The panic is ASYNCHRONOUS and
# may land a few seconds after the script returns; capture it from the
# serial console log (dfbsd-qemu/boot.log on the QEMU host).
run.sh must run as root (or a principal in the operator group / a
devfs class that exposes slice devices). On the default devfs ruleset,
/dev/vn0* are root:operator crw-r----- and operator contains only
root, so an unprivileged user gets EACCES.
Expected result (bug present)
- The trigger returns
nslices=130(the 28 KB heap overflow executes). - The guest panics within the run (asynchronous, heap-layout dependent).
Observed signatures on master DEV (all in
panic.txt): panic: slaballoc: corrupted zonein_kmalloc <- fork1Fatal trap 12 ... slab_cleanup+0x1c9(NULL deref, idle reclaimer)panic: ... hammer2_chain_createpreceded bydscheck(vbd0s1d): slice too large(the overrun corrupted the root disk's slice metadata)
Expected result (bug fixed)
With fix.diff applied, the bcopy is capped at MAX_SLICES; the trigger
returns nslices=130 (the live count is unchanged) but no heap overrun
occurs and no panic follows under any amount of churn.
Files
| File | Purpose |
|---|---|
build_gpt.py |
host-side GPT image generator (128 entries) |
trigger.c |
minimal proof: single DIOCGSLICEINFO, prints nslices |
trigger_stress.c |
ioctl + slab churn + fork/flood to surface the panic |
build.sh / run.sh |
self-contained reproduce scripts |
build.log / run.log |
full untrimmed build / decisive-run output |
panic.txt |
the three panic signatures from dfbsd-qemu/boot.log |
env.txt |
guest uname, cc, devfs perms, reachability caveat |
VERDICT.md |
full root-cause + reproduction narrative |
fix.diff |
git apply-able bounds fix (cap copy at MAX_SLICES) |
manifest.json |
machine-readable artifact catalog |
PANIC.txt |
archived capture from the prior 6.4.2-RELEASE run |
Notes
python3is not on the DragonFly guest by default; generateoverflow.imgon a host and ship it.- Only
>= 15GPT entries are needed fordss_nslices > MAX_SLICES(dss_nslices = 2 + i); the PoC uses the maximum 128 for the largest overrun. - The overflow source (
dsmakeslicestruct, 130 slots) is correctly allocated; only theDIOCGSLICEINFOdestination is undersized.
DF-0074 β VERDICT
Verdict: REPRODUCED (kernel heap overflow -> panic) on master DEV
The DIOCGSLICEINFO heap-buffer overflow described in the finding is real,
present in the current DragonFlyBSD master DEV kernel, and was reproduced as a
kernel panic on every fresh-boot attempt. The overflow is mathematically
deterministic on every invocation; the resulting crash site is heap-layout
dependent (four distinct panic signatures observed across fresh-boot runs, all
from the same ~28 KB overrun).
Guest tested: DragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026
(the audit's master DEV build, X86_64_GENERIC, INVARIANTS ON β the default).
TL;DR β escalation analysis (THIS run, 2026-07-04)
The escalation to uid=0 is blocked by a valid hard blocker: the bug's
only trigger path (open(/dev/<disk>s<N>) + ioctl(DIOCGSLICEINFO)) is
gated by caps_priv_check_self(SYSCAP_RESTRICTEDROOT) in diskopen(),
which on a default DragonFly kernel requires cr_uid == 0. The userland
principal maxx (uid 1001) cannot open the device even when the devfs node is
chowned to maxx with mode 666 AND vfs.usermount=1 is set β the caps check
overrides the file permission. The same RESTRICTEDROOT gate protects
vnconfig/mdconfig (sys/dev/disk/vn/vn.c:434). The kernel's own GPT
auto-probe path (USB mass storage, etc.) does NOT issue DIOCGSLICEINFO β
it calls dsmakeslicestruct/subr_diskgpt directly, so the only way to
fire the buggy bcopy is via the user-issued ioctl, which requires uid=0.
This is the Phase-6 "root-only reachability" valid hard blocker: rootβkernel is game-over by definition, so there is no privilege boundary to cross. The demonstrable impact on the default kernel is therefore panic (root-triggerable kernel heap corruption / DoS). The underlying primitive (a deterministic ~28 KB attacker-controlled heap write) is real and would be a strong LPE candidate if any unprivileged trigger path existed, but none does on this kernel.
This analysis supersedes the "Reachability" section of the prior verdict,
which treated the devfs crw-r----- permissions as the only gate and
speculated about operator-group / permissive devfs rule workarounds. The
actual gate is one layer deeper, at the disk-layer caps_priv_check, and is
not bypassable by any realistic userspace-only setup.
Root cause (confirmed line-by-line in sys/)
The DIOCGSLICEINFO handler copies the live kernel struct diskslices
into the ioctl data buffer using a length derived from the actual slice
count, but the destination buffer is only sized for MAX_SLICES = 16 slots:
/* sys/kern/subr_diskslice.c:556-559 */
case DIOCGSLICEINFO:
bcopy(ssp, data, (char *)&ssp->dss_slices[ssp->dss_nslices] -
(char *)ssp);
return (0);
- Destination size:
DIOCGSLICEINFOis_IOR('d', 111, struct diskslices)(sys/sys/diskslice.h:96).mapped_ioctl(sys/kern/sys_generic.c:675) allocatessize = IOCPARM_LEN(cmd)=sizeof(struct diskslices)=offsetof(dss_slices) + MAX_SLICES * sizeof(struct diskslice)=32 + 16 * 256 = 4128bytes, typeM_IOCTLOPS. - Source size for a GPT disk:
subr_diskgpt.c:175callsdsmakeslicestruct(BASE_SLICE + MAX_GPT_ENTRIES, info)= 130 slots (BASE_SLICE = 2,MAX_GPT_ENTRIES = 128), andsubr_diskgpt.c:222setsssp->dss_nslices = BASE_SLICE + i(= 130 for any GPT whose headerentries >= 128).dsmakeslicestruct(subr_diskslice.c:720-723)kmallocs the full 130-slot object, so the source is in-bounds β only the destination overflows. - The overflow: the
bcopylength becomesoffsetof(dss_slices) + dss_nslices * sizeof(struct diskslice)=32 + 130 * 256 = 33312bytes, written into the 4128-bytedatabuffer β 29184 bytes (~28 KB) of overrun past the end of anM_IOCTLOPSslab object, on every call. The overrun bytes are attacker-influenced (ds_offset,ds_size,ds_type_uuid,ds_stor_uuidcome straight from the crafted GPT entries βsubr_diskgpt.c:237+). - The trailing
copyout(sys_generic.c:730) only copies the declaredsizeof(struct diskslices)back, so the corruption is confined to kernel heap (the user merely observesdss_nslices = 130).
Struct sizes were verified on the guest with a small probe:
sizeof(struct diskslice) = 256, sizeof(struct diskslices) = 4128,
offsetof(dss_slices) = 32, overflow = (130-16)*256 = 29184 (~28 KB).
Trigger & evidence (this run, 2026-07-04, #0 baseline)
A crafted 1 MiB GPT image (build_gpt.py, header entries = 128, 17 non-nil
entries) is attached with vnconfig -c vn0 overflow.img. The kernel
auto-probes the GPT and creates /dev/vn0s0../dev/vn0s16 (already proving
dss_nslices > 16). Issuing DIOCGSLICEINFO on /dev/vn0s1 returns
nslices=130 β proof the oversized bcopy executed β and writes 28 KB past
the data buffer into adjacent kernel heap. A subsequent stress run (5
ioctl+fd-churn iters + 16-proc flood) reliably surfaces the panic within the
run. From dfbsd-qemu/boot.log (saved as baseline_panic.txt):
Fatal user address access from kernel mode from trigger_stress at ffffffff8065720a Fatal trap 12: page fault while in kernel mode cpuid = 0; lapic id = 0 fault virtual address = 0x0 current process = 931 current thread = pri 6 (CRIT) kernel: type 12 trap, code=0 Stopped at _kmalloc+0x44a: movq (%rax),%rdx db>
_kmalloc is the slab allocator itself walking zone metadata; the page fault
on a NULL/corrupted c_Next is direct evidence that the 28 KB overrun
corrupted an adjacent slab zone's free-list / chunk header. vm.sh status =>
down. The single-shot trigger does not synchronously panic (the corrupted
neighbor isn't exercised in-band); the stress + flood forces a corrupted
neighbor to be allocated/freed/validated.
The crash is asynchronous and probabilistic in exact site/timing (the
overrun corrupts whatever slab object happens to be adjacent at runtime), but
the underlying 28 KB heap overflow is 100% deterministic on every call β
nslices=130 is returned on every single invocation.
Reachability β hard-blocker analysis (NEW in this run)
This is the central new finding of the 2026-07-04 verification. The
escalation to uid=0 was the assigned deliverable. After confirming the
primitive, this section documents why that escalation is blocked.
The gate
diskopen() (sys/kern/subr_disk.c:1051) is the cdevsw .d_open for ALL
disk devices (registered at subr_disk.c:138 and :151 β vn, md, vbd, serno,
etc. all inherit it via the disk-layer wrapper). It opens with:
/* sys/kern/subr_disk.c:1072 */
if (caps_priv_check_self(SYSCAP_RESTRICTEDROOT))
return (EPERM);
caps_priv_check_self -> caps_priv_check_td -> caps_priv_check
(sys/kern/kern_caps.c:311) does:
/* sys/kern/kern_caps.c:328-331 */
if (cred->cr_uid != 0 && (cap & __SYSCAP_NOROOTTEST) == 0) {
if ((cap & __SYSCAP_WHEELOK) == 0 || !groupmember(0, cred))
return EPERM;
}
SYSCAP_RESTRICTEDROOT = (__SYSCAP_GROUP_0 | 1) = 1
(sys/sys/caps.h:132) β it has neither the __SYSCAP_NOROOTTEST bit
(0x00040000) nor the __SYSCAP_WHEELOK bit (0x00080000). So the check
reduces to cr_uid == 0 or EPERM, with no group/cap bypass. The check
fires at the disk layer, BEFORE the underlying device's d_open
(vnopen, mdopen, ...) is ever called, so no per-device quirk bypasses it.
The same RESTRICTEDROOT gate protects vnconfig/VNIOCATTACH
(sys/dev/disk/vn/vn.c:434) and md's attach ioctl. So an unprivileged
user cannot even create their own vnode-backed disk to attack.
Empirical confirmation (on #0 baseline)
$ sysctl vfs.usermount=1 $ vnconfig -c vn0 /home/maxx/poc/DF-0074/overflow.img $ chown maxx:maxx /dev/vn0 /dev/vn0s1 $ chmod 666 /dev/vn0 /dev/vn0s1 $ ls -la /dev/vn0s1 crw-rw-rw- 1 maxx maxx 8, 0x1e120007 Jul 4 16:30 /dev/vn0s1 $ su maxx -c '/home/maxx/poc/DF-0074/trigger /dev/vn0s1' open: Operation not permitted <-- EPERM, not EACCES rc=1 $ su maxx -c 'vnconfig -c vn1 /home/maxx/poc/DF-0074/overflow.img' vnconfig: /dev/vn1: Permission denied <-- same RESTRICTEDROOT gate
The EPERM (not EACCES) is the smoking gun: it is diskopen's caps check
failing, not the devfs file-mode check. Chowning the devfs node and granting
666 permissions makes no difference β the kernel inspects cr_uid, not
the file's owner.
Options exhausted
| Path | Result |
|---|---|
Default devfs (root:operator crw-r-----) |
EACCES (devfs layer) |
| chown vn0s1 -> maxx + chmod 666 | EPERM (caps) |
vfs.usermount=1 + chown |
EPERM (caps) |
maxx in operator group (still uid 1001) |
EPERM (no WHEELOK) |
maxx in wheel group (still uid 1001) |
EPERM (no WHEELOK) |
vnconfig / mdconfig as maxx |
EPERM (vn.c:434) |
| USB mass-storage auto-probe of crafted GPT | populates dss_nslices but does NOT issue DIOCGSLICEINFO (uses internal dsmakeslicestruct/subr_diskgpt path); the buggy bcopy is never reached |
kldload a kernel module |
forbidden by the bright-line rule (root-only; would also make the chain circular) |
| setuid-root helper | forbidden by the bright-line rule |
non-default INVARIANTS-OFF (noinv) kernel |
moot β the blocker is reachability, not INVARIANTS |
Every userspace-only path to issuing DIOCGSLICEINFO requires cr_uid == 0.
There is no unprivileged path. This is the Phase-6 valid hard blocker
("the write is reachable only from an already-root context").
Implication
Because the bug is uid=0-to-trigger, rootβkernel corruption is by definition game-over (the triggering principal is already uid=0). The bug's demonstrable impact on the default kernel is therefore panic / DoS (reliable, root-triggerable). The 28 KB controlled-write primitive IS real and WOULD be a strong LPE candidate if any unprivileged trigger existed β but none does. The proper severity calibration is "root can panic the kernel", which is a defense-in-depth / hardening gap, not a Critical unprivβroot LPE.
Phase 8 β Fix validation on a single-fix kernel (this run, 2026-07-04)
Verified end-to-end: baseline overflow/panic on #0 β fix builds β patched
#1 kernel is clean.
8a. Baseline re-confirm on unpatched #0: guest reset to with-src,
kern.version = DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026,
sha256(/boot/kernel/kernel) = 5dc83dac19ad09effd6241c33e0c0669d41b6497ee92d87d3a2e45f287bc22ad.
Attached overflow.img to vn0, kernel auto-probed GPT (created
/dev/vn0s0../dev/vn0s16), ./trigger /dev/vn0s1 returned nslices=130
(the 28 KB overrun executed). Stress + 16-proc flood then panicked the guest
asynchronously (Stopped at _kmalloc+0x44a, vm.sh status => down). Saved as
baseline_panic.txt and run.log.
8b. Apply fix.diff: cd /usr/src && patch -p1 --forward < /root/fix.diff
β Hunk #1 succeeded at 554, PATCH_RC=0. Verified the patched source now
reads:
case DIOCGSLICEINFO:
/*
* The ioctl data buffer is sized sizeof(struct diskslices)
* (i.e. MAX_SLICES slots) via _IOR in diskslice.h. GPT disks
* allocate up to BASE_SLICE + MAX_GPT_ENTRIES (130) slots, so
* cap the copy at MAX_SLICES to avoid overrunning the buffer.
*/
{
u_int ncap = (ssp->dss_nslices > MAX_SLICES) ?
MAX_SLICES : ssp->dss_nslices;
bcopy(ssp, data, (char *)&ssp->dss_slices[ncap] -
(char *)ssp);
}
return (0);
8c. Build: make -j6 nativekernel KERNCONF=X86_64_GENERIC β
=== NK_DONE rc=0 === Sat Jul 4 16:51:33 UTC 2026.
subr_diskslice.c recompiled cleanly under -Werror; kernel linked +
objcopy --strip-debug produced kernel.stripped (15.7 MB). Full log in
fix_build.log.
8d. Install + reboot: overwrote bare /boot/kernel/kernel (loader boots
the bare name) + kernel.debug, sync, vm.sh down && vm.sh up.
kern.version now reads
DragonFly 6.5-DEVELOPMENT #1: Sat Jul 4 16:48:34 UTC 2026
(#0β#1, today's build ts); sha256(/boot/kernel/kernel) =
58d7f052637e8b048f6b8edd7c9be7e66f5d6b26a7067a6d435856bc237716cd.
8e. Re-run the SAME PoC on #1 (HEAVIER workload than the unpatched
case, which panicked inside the first 16-proc flood):
- ./trigger /dev/vn0s1 β nslices=130, rc=0
- ./trigger_stress /dev/vn0s1 10 β all 10 iters nslices=130, rc=0
- 32-process parallel flood, 3 rounds (96 concurrent triggers) β all complete, rc=0
- ./trigger_stress /dev/vn0s1 20 β all 20 iters nslices=130, rc=0
vm.sh status => up. No panic in boot.log, no slab warning in dmesg.
The live dss_nslices is unchanged at 130 (so userspace still observes the
real GPT slice count β the count reporting path is unaffected); only the
bcopy length is clamped at MAX_SLICES = 16, so the destination is never
overrun. Full output in fix_run.log.
8f. Classification: fixed. Clean before/after:
- before (#0): single stress + 16-proc flood β panic _kmalloc+0x44a,
guest down.
- after (#1): 96+ DIOCGSLICEINFO calls + flood + 30 stress iters β no panic,
guest up, no slab warnings.
The fix is deterministic and sufficient: the previously-crashing workload
now completes cleanly, while the live-slice-count reporting (nslices=130)
is preserved (no behavior regression for legitimate consumers).
Recommended fix
fix.diff (validates with git apply --check and patch -p1 --forward):
cap the bcopy length at MAX_SLICES so it never exceeds the destination
buffer size. This matches the finding markdown's ## Recommended fix
proposal (the same one-line clamp), with an added explanatory comment and a
local u_int ncap to keep the bcopy expression readable. The deeper fix
(replacing the raw-struct ioctl with a structured, pointer-free output β see
DF-0075) remains desirable but is out of scope for this minimal bounds fix.
PoC changes made during verification (cumulative across all sessions)
trigger_stress.c: the originaltrigger.conly issues a singleDIOCGSLICEINFO. On master DEV the single-shot overflow does not synchronously panic (the corrupted neighbor isn't exercised in-band).trigger_stress.cissues the ioctl, then churns the slab allocator (open/close 64 fds, fork/exit 40 children) and a parallel flood to force a corrupted neighbor to be allocated/freed/validated, reliably surfacing the panic within a run.trigger.cis retained as the minimal proof.build.sh/run.sh: self-contained, runnable reproduce scripts.- Image build clarified:
python3is not on the DragonFly guest, sobuild_gpt.pyruns on a host with python3 and the resultingoverflow.imgis shipped to the guest. - Device naming: DragonFly uses
/dev/vn0s1+vnconfig -c vn0(the original PoC text referenced NetBSD-style names β fixed). - Reachability re-analysis (NEW 2026-07-04): the prior verdict's
"operator group / permissive devfs rule" reachability story was wrong.
The actual gate is
diskopen()'scaps_priv_check_self(SYSCAP_RESTRICTEDROOT)check atsubr_disk.c:1072, which requirescr_uid == 0regardless of devfs file permissions. Empirically verified: chown + 666 + usermount=1 still yields EPERM. This downgrades the realistic impact from "unpriv LPE with realistic preconditions" to "root-triggerable panic / DoS" β still a High-severity hardening gap, but not a Critical unprivβroot escalation.
Fix verification
fixedVALIDATED: ./trigger + ./trigger_stress + 16-proc flood PANICKED unpatched #0 baseline (Stopped at _kmalloc+0x44a, vm.sh status => down) within ~30s; SAME and HEAVIER workload (96-trigger flood + 30 stress iters) on single-fix #1 kernel all rc=0 with nslices=130 preserved, NO panic in boot.log, NO slab warning in dmesg, vm.sh status => up. Clean before/after.
baseline #0 (BAD): DIOCGSLICEINFO returned nslices=130 -> stress+flood -> 'Fatal trap 12: page fault while in kernel mode' / 'Stopped at _kmalloc+0x44a: movq (%rax),%rdx' / 'db>' / vm.sh status => down. patched #1 (GOOD): trigger nslices=130 rc=0; trigger_stress x10 all rc=0; 32-proc flood x3 rounds (96 concurrent) all rc=0; trigger_stress x20 all rc=0; vm.sh status => up; dmesg slab-clean.
Confirmed kernel references
Detail
Exploit chain
BLOCKED by valid Phase-6 hard blocker 'write reachable only from already-root context' (sys/kern/subr_disk.c:1072 caps_priv_check_self(SYSCAP_RESTRICTEDROOT) requires cr_uid==0). No uid=0 chain developed because no unprivileged trigger path exists. Characterization (had a chain been possible): M_IOCTLOPS destination 4128 B rounded by zoneindex() to 4608 B chunk in slab zone 56; the 29184-byte overrun plows through ~6-7 adjacent same-zone chunks plus possibly crosses into the next zone's SLZone header. Overrun bytes are attacker-controlled diskslice fields. On noinv (INVARIANTS OFF) cleanly weaponizable: groom zone 56 with a victim carrying a function pointer or ucred*, overwrite, redirect at forged userspace ucred (no SMAP) or userspace shellcode (no SMEP) -> commit_creds(prepare_kernel_cred(0)). On GENERIC (INVARIANTS ON) the same chain would trip chunk_mark_allocated/KKASSERT during grooming. But neither case is reachable as maxx. Reachability paths exhausted (see VERDICT.md table). No exploit.c written because there is no unprivileged trigger. Honest deliverable: panic (root-only trigger) + documented hard blocker.
Evidence (decisive lines)
baseline #0 (panic): trigger returned nslices=130 (28 KB bcopy fired); stress+16-proc flood -> 'Fatal user address access from kernel mode from trigger_stress at ffffffff8065720a' / 'Fatal trap 12: page fault while in kernel mode' / 'Stopped at _kmalloc+0x44a: movq (%rax),%rdx' / 'db>' (vm.sh status => down). Reachability blocker: chown vn0s1 maxx:maxx + chmod 666 + vfs.usermount=1 -> su maxx -c './trigger /dev/vn0s1' still yields 'open: Operation not permitted' (EPERM not EACCES). Patched #1 kernel (sha256 58d7f052...) under SAME and heavier workload (96-trigger flood + 30 stress iters): all rc=0, nslices=130 preserved, vm.sh status => up, no slab warning in dmesg.
PoC changes
No source changes to trigger C files this run (build_gpt.py, trigger.c, trigger_stress.c, build.sh, run.sh retained). NEW: rewrote VERDICT.md to add reachability hard-blocker analysis (prior verdict reachability story was wrong - real gate is diskopen caps_priv_check at subr_disk.c:1072, not devfs file mode) and the 2026-07-04 Phase-8 fix-validation section; rewrote env.txt; rewrote run.log/fix_run.log with fresh baseline-panic and patched-clean captures; recomputed manifest.json. fix.diff unchanged (matches finding proposal).
Verified recommended fix
fix.diff clamps the DIOCGSLICEINFO bcopy length at MAX_SLICES (16) so it can never exceed the destination buffer (sizeof(struct diskslices) = 4128 B). At sys/kern/subr_diskslice.c:556-559 replace the bcopy length with a capped ncap = min(dss_nslices, MAX_SLICES). Live dss_nslices count preserved in copied header (regression-free). MATCHES the finding markdown proposal. Full git-apply-able diff in findings/poc/DF-0074/fix.diff.
Verdict
REPRODUCED as a deterministic ~28 KB attacker-controlled kernel-heap overwrite that surfaces as a slab-allocator panic on the default GENERIC (#0) kernel. Confirmed end-to-end: single ./trigger returns nslices=130 (the oversized bcopy at sys/kern/subr_diskslice.c:557 fired), and a stress+16-proc-flood workload asynchronously panics in _kmalloc+0x44a (slab zone metadata corrupted by the overrun), guest down. The primitive is real and large (32 + 130*256 - 4128 = 29184 bytes of overrun into adjacent slab chunks). The assigned deliverable of escalating this to uid=0 on GENERIC is BLOCKED by a valid hard blocker: diskopen() at sys/kern/subr_disk.c:1072 enforces caps_priv_check_self(SYSCAP_RESTRICTEDROOT), which requires cr_uid == 0 regardless of devfs file permissions or group membership. Empirically verified: maxx with /dev/vn0s1 chowned to maxx + mode 666 + vfs.usermount=1 STILL gets EPERM on open() (EPERM not EACCES = caps check overriding file mode). vnconfig gated identically at sys/dev/disk/vn/vn.c:434. The kernel's GPT auto-probe path does NOT issue DIOCGSLICEINFO. Every userspace-only path to firing DIOCGSLICEINFO requires uid=0; root->kernel corruption is game-over by definition. The honest demonstrable impact on the default kernel is panic/DoS (root-triggerable). The prior verdict's reachability story was wrong (stopped at devfs file mode, missed the deeper caps gate).
No comments yet.