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

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.

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0074 Β· 19 files
FileTypeDescriptionSize
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
README.md readme human-facing reproduce + expected-result guide
↓ download 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 zone in _kmalloc <- fork1
  • Fatal trap 12 ... slab_cleanup+0x1c9 (NULL deref, idle reclaimer)
  • panic: ... hammer2_chain_create preceded by dscheck(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

  • python3 is not on the DragonFly guest by default; generate overflow.img on a host and ship it.
  • Only >= 15 GPT entries are needed for dss_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 the DIOCGSLICEINFO destination is undersized.
VERDICT.md verdict full root-cause + reachability hard-blocker + Phase 8 fix-validation narrative
↓ download raw

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: DIOCGSLICEINFO is _IOR('d', 111, struct diskslices) (sys/sys/diskslice.h:96). mapped_ioctl (sys/kern/sys_generic.c:675) allocates size = IOCPARM_LEN(cmd) = sizeof(struct diskslices) = offsetof(dss_slices) + MAX_SLICES * sizeof(struct diskslice) = 32 + 16 * 256 = 4128 bytes, type M_IOCTLOPS.
  • Source size for a GPT disk: subr_diskgpt.c:175 calls dsmakeslicestruct(BASE_SLICE + MAX_GPT_ENTRIES, info) = 130 slots (BASE_SLICE = 2, MAX_GPT_ENTRIES = 128), and subr_diskgpt.c:222 sets ssp->dss_nslices = BASE_SLICE + i (= 130 for any GPT whose header entries >= 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 bcopy length becomes offsetof(dss_slices) + dss_nslices * sizeof(struct diskslice) = 32 + 130 * 256 = 33312 bytes, written into the 4128-byte data buffer β†’ 29184 bytes (~28 KB) of overrun past the end of an M_IOCTLOPS slab object, on every call. The overrun bytes are attacker-influenced (ds_offset, ds_size, ds_type_uuid, ds_stor_uuid come straight from the crafted GPT entries β€” subr_diskgpt.c:237+).
  • The trailing copyout (sys_generic.c:730) only copies the declared sizeof(struct diskslices) back, so the corruption is confined to kernel heap (the user merely observes dss_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).

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)

  1. trigger_stress.c: the original trigger.c only issues a single DIOCGSLICEINFO. On master DEV the single-shot overflow does not synchronously panic (the corrupted neighbor isn't exercised in-band). trigger_stress.c issues 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.c is retained as the minimal proof.
  2. build.sh / run.sh: self-contained, runnable reproduce scripts.
  3. Image build clarified: python3 is not on the DragonFly guest, so build_gpt.py runs on a host with python3 and the resulting overflow.img is shipped to the guest.
  4. Device naming: DragonFly uses /dev/vn0s1 + vnconfig -c vn0 (the original PoC text referenced NetBSD-style names β€” fixed).
  5. 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()'s caps_priv_check_self(SYSCAP_RESTRICTEDROOT) check at subr_disk.c:1072, which requires cr_uid == 0 regardless 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

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: ./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.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sat Jul 4 16:48:34 UTC 2026 (sha256 58d7f052637e8b048f6b8edd7c9be7e66f5d6b26a7067a6d435856bc237716cd)

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).