# DF-5554 VERDICT — cam_calc_geometry() 64→32-bit truncation

**Status: REPRODUCED (baseline) / FIXED (patched kernel, validated in-guest).**
**Impact: none beyond wrong kernel-reported geometry (logic bug, Low).**
**Confidence: certain.**

## Root cause

`struct ccb_calc_geometry.volume_size` is `u_int64_t`
(sys/bus/cam/cam_ccb.h:779 — DragonFly widened it for >2TB volumes;
FreeBSD's counterpart is u32), but `cam_calc_geometry()`
(sys/bus/cam/cam.c:375-400) still does all math in `uint32_t`
locals (cam.c:378):

```c
uint32_t size_mb, secs_per_cylinder;                 /* cam.c:378 */
size_mb = (1024L * 1024L) / ccg->block_size;         /* cam.c:384 */
size_mb = ccg->volume_size / size_mb;                /* cam.c:389  u64→u32 WRAP #1 */
if (size_mb > 1024 && extended) { heads=255; spt=63; } else { heads=64; spt=32; }
secs_per_cylinder = ccg->heads * ccg->secs_per_track;
ccg->cylinders = ccg->volume_size / secs_per_cylinder; /* cam.c:398  u64→u32 WRAP #2 */
```

Both quotients wrap mod 2^32 when `volume_size ≥ 2^32 × secs_per_cylinder`
(≈31 PB with 512B sectors in extended mode; ≈4 PiB in 64/32 mode). Wrap #1
silently flips the extended-translation decision; wrap #2 returns an
arbitrary modular cylinder count, including exactly 0.

## Reachability (attacker-controlled input)

`volume_size` comes from the **device**, not from anywhere local:

- scsi_da.c:1849-1851: `maxsector = scsi_8btou64(rcaplong->addr)` — READ
  CAPACITY(16) answer, 64 bits, fully chosen by the target;
- scsi_da.c:2287 `dasetgeom()` → scsi_da.c:2303 `ccg->volume_size = dp->sectors`;
- xpt_action routes XPT_CALC_GEOMETRY to the SIM, and every SIM in the
  tree calls this same function: ahci_cam.c:981, sili_cam.c:936,
  virtio_scsi.c:802, umass.c:2482, atapi-cam.c:470, mpr/mps/amr/ciss/... .

A malicious USB gadget, iSCSI/NBD target, or virtio-scsi backend answers
RC(10) with 0xFFFFFFFF then RC(16) with e.g. 0xFFFFFFFFFFFFFFFF. No sanity
check exists on `maxsector` before dasetgeom.

## What was run (evidence class: KLD harness on the production function)

`calcgeo.c` builds `df5554.ko` against the running kernel (kmod.mk) and
calls the **production `cam_calc_geometry()` symbol** with crafted
volume_size values, printing kernel-vs-true-64-bit math. No kernel source
is copied into the harness.

### Baseline (stock kernel #0, Jul 2 2026) — run.log

| input volume_size | kernel result | true 64-bit result |
|---|---|---|
| 15,628,000,000 (8TB disk) | H=255 S=63 C=972798 — ok | same (fidelity check) |
| 2^43 (4PiB @512B) | **H=64 S=32 C=0** | H=255 S=63 C=547531467 |
| 2^32·16065 | **C=0** | C=2^32 |
| 2^32·16065+16064 | **C=0** | C=2^32 |
| 0xFFFF...FFFF | **C=1500097198** | C=1148256711715502 |
| block_size=0 | CAM_REQ_CMP_ERR (div-guard OK) | — |

Both wraps demonstrated: the 2^43 case flips geometry to 64/32 **and**
returns cylinders=0 for a >1MB volume (dasetgeom's error path explicitly
exists to avoid cylinders-based divide-by-zero scenarios — the success path
can now produce the same degenerate values).

### Fix validation (kernel #1, fix.diff applied in-guest) — run.patched.log

fix.diff (in pack): compute `size_mb`/`secs_per_cylinder` in `uint64_t`,
clamp `cylinders` to `UINT32_MAX` when the true quotient cannot fit.

- 2^43 case → `H=255 S=63 C=547531467` — **exactly the true value** (the
  flip AND the zero-wrap are gone; 2^43/16065 fits u32);
- 2^32·16065 (+16064) and max-u64 → `C=4294967295` (deliberate saturation,
  never a silent modular wrap);
- sane-8TB / zero-volume / block_size=0 guards byte-identical to baseline.

Baseline bad behaviors GONE on the patched kernel. Build:
`make nativekernel KERNCONF=X86_64_GENERIC` (fix_build.log, rc=0; 56 cc
lines for cam.c in log), installkernel, reboot into #1, same harness
re-run.

## Why impact stays Low

The geometry is advisory metadata: it lands in
`disk_info.d_ncylinders/d_nheads/d_secpertrack/d_secpercyl`
(scsi_da.c:1866-1870, 2153-2157) and the dmesg announce line
(scsi_da.c:1857-1861). In-kernel consumers never divide by these fields
unguarded (subr_diskmbr.c:268/352 guard `secpercyl != 0`; subr_disklabel32
recomputes from 64-bit slice sizes; capacity logic uses d_media_blocks).
No OOB, no leak, no panic — wrong numbers only, for crafted-capacity
devices. Not a memory-safety bug; filed for correctness and because the
same crafted-capacity path feeds genuinely dangerous consumers elsewhere.

## Attempts, environment

3 attempts total: (1) cam.h needs a stub opt_cam.h for standalone KLD
builds — fixed in build.sh; (2) baseline run captured, guest reset
with-src; (3) fix build first failed `buildkernel` (no buildworld in
snapshot) → `nativekernel` succeeded. Guest reset with-src after the
patched run. Guest uname lines in env.txt.

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