# DF-4852 — VERDICT

## Bottom line

**Reproduced** on the guest (DragonFly 6.5-DEVELOPMENT, kernel build
2026-07-02, cc 8.3): the striped-target chunk-size validation in
`dm_target_stripe_init()` is bypassable via signed integer overflow, and
the kernel accepted and stored chunk sizes of 4194304 (2^22) and
2147483647 (INT_MAX) blocks while correctly rejecting the non-overflow
control value 1048576 with EINVAL. This is a **validation/integrity bug,
not memory corruption** (verified below), so impact is `none` beyond the
integrity break; severity Low, confidence certain.

## Root cause

`sys/dev/disk/dm/striped/dm_target_striped.c:98-99`:

```c
chunksize = atoi64(argv[1]);                              /* int */
if (chunksize < 1 || chunksize * DEV_BSIZE > MAXPHYS) {   /* int*int */
```

* `atoi64` (device-mapper.c:584-596) parses digits into a `uint64_t`;
  assignment truncates into `int chunksize` (this truncation itself is
  harmless here: values that truncate to [1,2^31) are all representable).
* The product `chunksize * DEV_BSIZE` is `int` arithmetic:
  * 4194304 * 512 == 2^31 → wraps to INT_MIN; `INT_MIN > MAXPHYS(131072)`
    is false → **accepted**.
  * 2147483647 * 512 == 2^40-512 → low 32 bits 0xFFFFFF00 → signed -256 →
    **accepted**.
  * 1048576 * 512 == 2^29 (no wrap, positive) → `> MAXPHYS` → EINVAL
    (the control proving the check works absent overflow).
* The accepted value lands in `tsc->stripe_chunksize` (u64) and is used by
  `dm_target_stripe_strategy()` (:313-322) and `dm_target_stripe_dump()`
  (:409-417).

## Why it is NOT memory corruption (checked, killed)

With `stripe_chunksize` up to 2^31-1:
* `stripe = blkno / chunksize`, `blknr = stripe / stripe_num` — u64 math,
  no truncation (chunksize < 2^31 keeps every intermediate < 2^63 for any
  media addressable by the bio path).
* `stripe_off = blkno % chunksize` (u32) — result is < chunksize < 2^31,
  fits.
* `stripe_rest = chunksize - stripe_off` (u32) — < 2^31, fits.
* `issue_blks = MIN(stripe_rest, num_blks)` — bounded by the request size
  in blocks (≤ MAXPHYS/512 = 256 via physio), never by the huge chunk.
* `devnr = stripe % stripe_num` ∈ [0, 32) — table index in bounds.
So the only effect is **mis-striping** (all I/O maps to stripe device 0
for devices smaller than one chunk) and acceptance of configurations the
check exists to reject.

## Reproduction (run.log, dmesg_run.log)

```
[case A] chunk=4194304     reload rv=0 (accepted)  EXPECTED: ACCEPT
[case B] chunk=1048576     reload rv=22 (Invalid argument)  EXPECTED: EINVAL
[case C] chunk=2147483647  reload rv=0 (accepted)  EXPECTED: ACCEPT
[case D] chunk=128         reload rv=0 (accepted)  EXPECTED: ACCEPT
[*] inactive table params: "2 4194304 0:504430599 0 0:504430599 0"
[*] CONFIRMED: kernel accepted+stored chunk size 4194304 blocks (2 GiB chunks)
[PASS] 0 unexpected case results
```

Kernel dmesg (decisive):

```
dm: Stripe 2 devices chunk size 4194304KB      <- case A accepted (BUG)
dm: Error unsupported chunk size 524288KB      <- case B rejected (control)
dm: Stripe 2 devices chunk size 2147483647KB   <- case C accepted (BUG)
dm: Stripe 2 devices chunk size 128KB          <- case D accepted (legit)
```

(The "KB" in the kprintf is itself mislabeled — it prints the raw block
count — cosmetic only.)

Privilege: /dev/mapper/control is `make_dev(..., UID_ROOT, GID_OPERATOR,
0640, "mapper/control")` (device-mapper.c:181) — root/operator only;
this bounds severity at Low.

## Fix

`fix.diff`: cast to `uintmax_t` before the multiply (`(uintmax_t)chunksize
* DEV_BSIZE > MAXPHYS`). Compile-validated by a full `dm_target_striped.ko`
rebuild on the guest (fix_build.log). Behavioral re-validation of the
patched module was not performed in-guest (loading a replacement target
module requires unloading the in-use one); the change is a one-token type
widening with no control-flow effect on the accepted path, and the
unpatched behavior is fully characterized above.

## PoC fixes vs seed

No seed existed (new pass-2 finding). Two iterations were needed:
DragonFly's `prop_dictionary_send_ioctl()` is one-way (unlike NetBSD's),
so the "table" readback had to use `prop_dictionary_sendrecv_ioctl()`;
and a leftover device from the first (segfaulting) run required a
`remove_all()` preamble.

## Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest `make -j6 nativekernel KERNCONF=X86_64_GENERIC` completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
