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

1. `sys/kern/subr_diskgpt.c:175` — `gptinit()` replaces the minimal slice
   struct with `dsmakeslicestruct(BASE_SLICE + MAX_GPT_ENTRIES, info)` =
   **130** slice slots (BASE_SLICE=2, MAX_GPT_ENTRIES=128), and
   `sys/kern/subr_diskgpt.c:222` sets `ssp->dss_nslices = BASE_SLICE + i`
   (up to 130).
2. `sys/kern/subr_diskgpt.c:201-204` — entry #0 maps to
   `dss_slices[COMPATIBILITY_SLICE]` (=0); entry #N (N≥1) maps to
   `dss_slices[BASE_SLICE+N-1]`, so **entry #127 maps to slice index 128**.
3. `sys/sys/diskslice.h:107` — `DKMAXSLICES 128` (slice indices 0..127);
   `sys/sys/diskslice.h:252-253` — `dkmakeminor()` encodes the slice field
   with masks `slice & 0x0f` (→bits 16-19) and `slice & 0x70` (→bits 29-31):
   **7 bits; bit 7 of slice (0x80) is silently dropped.**
4. `sys/kern/subr_disk.c:448-451` — `disk_probe()` creates the whole-slice
   device for every sized slice via
   `make_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)`.
5. `sys/sys/diskslice.h:327-333` — every consumer (`dsopen` subr_diskslice.c:771,
   `dscheck` :380/:107, `dssize`, `dsioctl` :380) decodes the device with
   `dkslice(dev)` → **0** — so the node *named* `adXs127` actually addresses
   `dss_slices[0]`, the compatibility slice holding **GPT entry 0's extent**.
6. `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 identical `si_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/vn0s127` **not 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.
