# DF-5647 — VERDICT

**Status: reproduced** (kernel resource-bookkeeping corruption live in guest
kernel memory, two independent sinks demonstrated, plus controls)
**Impact: corrupted kernel resource state — inverted (start>end) resource
ranges, sign-extended UINT64_MAX bases, rman double-claims of allocated I/O
ports.** No memory-safety violation of adjacent objects demonstrated; the
corrupted values live inside properly-allocated rman/rle structures. Severity
Low (loader.conf/root-gated).

## Root cause chain (each step live-verified or code-cited)

1. Hints: `hint.atkbdc.0.portsize="-1"`, `hint.lm.0.port="-1"` +
   `portsize="0x10"`, `hint.lm.0.maddr="0xd0000"` + `msize="-1"`,
   `hint.wbsio.0.portsize="-1"` (all documented loader.conf syntax;
   resource_kenv (subr_bus.c:2306-2326) parses them with bare `strtol`,
   kenv consulted BEFORE the compiled devtab, so they override everything).

2. **isahint.c:61-71** fetches them as `int start/count` and guards only
   `start > 0 || count > 0`:
   - `port=0x60, portsize=-1` → start=0x60>0 passes **with count=-1**;
   - `port=-1, portsize=0x10` → count>0 passes **with start=-1**;
   - same for maddr/msize.

3. `bus_set_resource()` → `isa_set_resource()` (isa_common.c:884) stores the
   rle with **`end = start + count - 1` in u_long, no wrap guard**
   (isa_common.c:910):
   - count=-1 → `count=0xFFFFFFFFFFFFFFFF`, `end=start-2` → **inverted range**;
   - start=-1 → `start=0xFFFFFFFFFFFFFFFF`, `end=0xe` → **UINT64_MAX base**.

4. Sink A (persistent, no alloc needed): the poisoned rle sits in the child's
   `isa_device.id_resources` forever. lm0's `iomem 0xd0000-0xcfffe` is never
   allocated — dmesg proves it exists purely as stored kernel state.

5. Sink B (rman, at any default-range allocation): a driver calling
   `bus_alloc_resource(dev, SYS_RES_{IOPORT,MEMORY}, &rid=0, 0, ~0, n, ...)`
   hits `resource_list_alloc()`'s isdefault path
   (subr_bus.c:2698-2701): `count = ulmax(count, rle->count)` = 2^64-1,
   `end = ulmax(rle->end, start+count-1)` = wrapped →
   `rman_reserve_resource(rm, start, end, 2^64-1)`:
   - subr_rman.c:236-238: `(rend - rstart + 1) >= count` is *true* for an
     inverted candidate (both sides = 2^64-1);
   - subr_rman.c:276-291: split-in-three inserts `rv=[start, start-2]` and
     writes `r->r_start = rv->r_end+1; s->r_end = rv->r_start-1` — the
     boundary byte ends up in **two** free regions, and the list order
     invariant (sorted by r_start) is broken;
   - for `atkbdc1` (port 0x60 already allocated to ACPI's atkbdc0) the scan
     still "fits" the inverted request → **rman grants a region overlapping
     an allocated port** (double-claim).

## Live evidence (findings/poc/DF-5647/run.log, captured 2026-10-09)

```
atkbdc1 [tentative] failed to probe at port 0x60-0x5e,0x64 on isa1   <- inverted + proof rid0 alloc SUCCEEDED (rid1 only set after port0 != NULL, atkbdc_isa.c:158-168)
lm0 [tentative] failed to probe at port 0xffffffffffffffff-0xe iomem 0xd0000-0xcfffe on isa1
wbsio0 [tentative] failed to probe at port 0x2e-0x2c on isa1          <- poisoned
wbsio1 [tentative] failed to probe at port 0x4e-0x4f on isa1          <- clean control
it0..it3 failed to probe at port 0x290-0x297/0xc00-0xc07/0xd00-0xd07/0x228-0x22f  <- clean controls
```

Two boots (guest reset in between) reproduced identical output; the guest
boots to ssh each time (the corruption is silent — which is itself the
problem: nothing rejects these ranges).

`atkbdc1` double-claim detail: atkbdc_probe (atkbdc_isa.c) allocs rid0 with
defaults → gets the inverted `[0x60,0x5e]` region granted over atkbdc0's
claimed 0x60, then sets rid1=0x64 (this only happens when port0 != NULL),
allocs rid1 (0x64 busy → NULL) → ENXIO → releases. The `port 0x60-0x5e,0x64`
line is printed by `isa_probe_nomatch()` from the live rle.

## What this is NOT

- Not a heap/stack overwrite: the corrupted values stay inside
  `struct resource_list_entry` / `struct resource` fields (u_longs), so this
  is bookkeeping corruption + hardware-resource double-claims, not an
  adjacent-object write primitive.
- Not unprivileged: requires loader.conf (root/loader). Runtime `kenv`
  injection does not re-run isahint (identify early-returns once the bus is
  attached, isahint.c:99-102).

## Fix

`fix.diff` (shared with DF-5646): `start >= 0 && count >= 0` added to both
the port and maddr guards (keeps stock port-only hints working: compiled
`port=0x60` without portsize still yields count=0 entries, which the
isdefault `ulmax(count, rle->count)` path already handles safely).
Fix-validated: with the patched kernel the same poison boot shows clean
ranges only (see fix_evidence).

## Negative results recorded (pass-2 checklist)

- Unit parsing: devtab units come from config(8) only (mkioconf.c:150-158);
  kenv cannot inject new devtab entries, so `resource_query_unit` cannot
  return attacker-controlled units; `resource_kenv` formats (not parses)
  units. No overflow. (subr_bus.c:2440-2444)
- Duplicate/ordering: both identify loops can't double-process one devtab
  entry (single "at" value); `resource_list_add` replaces by (type,rid)
  (subr_bus.c:2619-2633) — no duplicate accumulation.
- `static char buf[] = "isaXXX"` ksprintf (isahint.c:92,107): needs isa bus
  unit ≥ 1000; bus units are kernel-assigned (one legacy isab → isa0/isa1 on
  this guest). Unreachable from hints.
- drq path (isahint.c:78-79): out-of-range values fail allocation gracefully
  in drq_rman [0,7] (nexus.c:197-198); no ABI call with the raw value. Benign.
- `disabled`/`sensitive`/`flags` hints: honored as documented (verified live:
  "sio3/sio4: not probed (disabled)"); flags steer driver behavior by design.

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