Negative port/portsize/maddr/msize hints accepted: poisoned resource-list entries (inverted start>end ranges, sign-extended UINT64_MAX bases) and rman double-claims of already-allocated I/O ports
Summary
The guards accept start>0 with negative count and count>0 with negative start (the -1 disabled sentinel only half-honored): sign-extended UINT64_MAX start/counts and wrapped ends land in resource lists - live-proven inverted ranges in kernel memory (port 0x60-0x5e, iomem 0xd0000-0xcfffe, base 0xffffffffffffffff). At default-range allocation rman's wrap-satisfying check GRANTS the inverted region, the split-in-three duplicates the boundary byte and breaks free-list ordering, and atkbdc1's alloc double-claimed port 0x60 already owned by atkbdc0. Loader/root-gated permanent resource-bookkeeping corruption: hardware double-claims rather than adjacent-object memory corruption. reject-negative-geometry fix validated (poisoned devices fail to probe, compiled hints unregressed).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5647 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.2 KB | β raw | |
| VERDICT.md | β | 5.9 KB | β raw | |
| trigger.hints | β | 167 B | β download | |
| build.sh | β | 86 B | view raw | |
| run.sh | β | 174 B | view raw | |
| run.log | β | 39.2 KB | view raw | |
| env.txt | β | 444 B | view raw | |
| fix.diff | β | 973 B | view raw | |
| verdict.json | β | 4.6 KB | view raw |
DF-5647 β hint.<dev>.N.port/portsize|maddr/msize accepted with negative values β poisoned rle (inverted start>end ranges, UINT64_MAX bases) and rman bookkeeping corruption
Where
sys/bus/isa/isahint.c:59-71 (isahint_add_device), sink at
sys/bus/isa/isa_common.c:910 (end = start + count - 1), consumed by
sys/kern/subr_bus.c:2698-2701 (count = ulmax(count, rle->count)) into
sys/kern/subr_rman.c:205-291.
Build
Nothing to compile β pure loader-configuration trigger.
Run (the exact repro)
# on the guest, as root:
cat > /boot/loader.conf.local <<EOF
boot_verbose="YES"
hint.atkbdc.0.portsize="-1" # negative count (port stays compiled 0x60)
hint.wbsio.0.portsize="-1" # negative count, clean control = wbsio1
hint.lm.0.maddr="0xd0000"
hint.lm.0.msize="-1" # negative msize β inverted MEM range
hint.lm.0.port="-1" # negative start β sign-extended base
hint.lm.0.portsize="0x10"
EOF
shutdown -r now
# after boot:
dmesg | grep "failed to probe"
Expected (observed 2026-10-09, guest boots to ssh, no panic)
atkbdc1 [tentative] failed to probe at port 0x60-0x5e,0x64 on isa1 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 wbsio1 [tentative] failed to probe at port 0x4e-0x4f on isa1 <- clean control it0 [tentative] failed to probe at port 0x290-0x297 on isa1 <- clean control
start > end ranges (0x60-0x5e, 0x2e-0x2c, 0xd0000-0xcfffe,
0xffffffffffffffff-0xe) are live kernel state: the resource_list entries
isa_set_resource() stored with wrapped end = start + count - 1 and
sign-extended start/count. lm0's inverted iomem 0xd0000-0xcfffe entry is
never even allocated β it sits poisoned in the child's rle for the whole
uptime.
atkbdc1's line additionally proves the poisoned entry was GRANTED by rman:
rid1 (0x64) is only set by atkbdc_probe() after the rid0 default-range
alloc returned non-NULL β i.e. rman_reserve_resource(0x60, 0x5e, 0xffff..ff)
succeeded and double-claimed port 0x60 already owned by atkbdc0 (transient
during probe; released on probe failure).
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)
-
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 barestrtol, kenv consulted BEFORE the compiled devtab, so they override everything). -
isahint.c:61-71 fetches them as
int start/countand guards onlystart > 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. -
bus_set_resource()βisa_set_resource()(isa_common.c:884) stores the rle withend = start + count - 1in 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. -
Sink A (persistent, no alloc needed): the poisoned rle sits in the child's
isa_device.id_resourcesforever. lm0'siomem 0xd0000-0xcfffeis never allocated β dmesg proves it exists purely as stored kernel state. -
Sink B (rman, at any default-range allocation): a driver calling
bus_alloc_resource(dev, SYS_RES_{IOPORT,MEMORY}, &rid=0, 0, ~0, n, ...)hitsresource_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) >= countis true for an inverted candidate (both sides = 2^64-1); - subr_rman.c:276-291: split-in-three insertsrv=[start, start-2]and writesr->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; - foratkbdc1(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 resourcefields (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
kenvinjection 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_unitcannot return attacker-controlled units;resource_kenvformats (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_addreplaces 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/flagshints: 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.
Fix verification
fixedSame fix.diff as DF-5646 (shared isahint.c patch, kernel #1 Fri Oct 9, BUILD_OK): reboot with the identical poison set armed shows bare 'failed to probe on isa1' for atkbdc1/lm0/wbsio0 (no ranges stored) while compiled hints still work (wbsio1 0x4e-0x4f, it0-3, sio 0x3f8/0x2f8-0x2ff) β inverted ranges gone, no regression.
run.fixed.log (patched-kernel current-boot dmesg segment); build.log (kernel build + installkernel + BUILD_OK tail); diff in fix.diff
Confirmed kernel references
Detail
Exploit chain
loader.conf.local negative hints -> resource_kenv strtol (no range check, kenv overrides compiled devtab) -> isahint_add_device guard misses negative count/start -> bus_set_resource -> isa_set_resource end=start+count-1 wraps (isa_common.c:910) -> poisoned rle persists in child idev->id_resources -> on any default-range alloc, resource_list_alloc's count=ulmax(count,rle->count) (subr_bus.c:2698-2701) feeds 2^64-1 into rman_reserve_resource -> inverted region granted, split-in-three duplicates the boundary byte into two free regions and breaks list ordering (subr_rman.c:276-291); allocations can overlap already-claimed ports (atkbdc1 over atkbdc0's 0x60).
Evidence (decisive lines)
run.log (full verbose dmesg of the poisoned boot; decisive lines: 'atkbdc1 ... port 0x60-0x5e,0x64', 'lm0 ... port 0xffffffffffffffff-0xe iomem 0xd0000-0xcfffe', 'wbsio0 ... 0x2e-0x2c' vs controls 'wbsio1 ... 0x4e-0x4f', it0-3); run.fixed.log (patched kernel: poisoned devices print bare 'failed to probe on isa1', controls unchanged); VERDICT.md (full math + negative results)
PoC changes
Replaced seeded idea with loader.conf kenv hints (proven pattern): the isahint value path only reads kenv for COMPILED-IN devtab names (atkbdc/lm/wbsio/it/sio/adv/vga in X86_64_GENERIC), so the hints target those names; boot_verbose=YES added so isa_probe_nomatch prints the live rle ranges.
Verified recommended fix
Reject negative geometry at isahint.c:63/70: require start >= 0 && count >= 0 in addition to the existing start>0||count>0 (keeps compiled port-only hints with count=0 working).
Verdict
Negative hint values (portsize/msize = -1 with positive base, or port/maddr = -1 with positive size) pass isahint.c:63/70's start>0||count>0 guard; isa_set_resource then stores rle entries with sign-extended UINT64_MAX start/count and wrapped end = start+count-1 < start. Live-verified in guest kernel memory via isa_probe_nomatch prints: atkbdc1 'port 0x60-0x5e', wbsio0 'port 0x2e-0x2c', lm0 'iomem 0xd0000-0xcfffe' and 'port 0xffffffffffffffff-0xe' (inverted ranges), against clean controls (wbsio1 0x4e-0x4f, it0-3). The atkbdc1 line proves rman GRANTED the inverted region: rid1 (0x64) is only added after the rid0 default alloc returned non-NULL, i.e. rman_reserve_resource(0x60,0x5e,2^64-1) succeeded via subr_rman.c:236-238's wrap-satisfying (rend-rstart+1)>=count and double-claimed port 0x60 already allocated to atkbdc0. Persistent poisoned rle (lm0 iomem) plus transient rman free-list corruption (duplicated boundary byte, unsorted list) per subr_rman.c:276-291. Bookkeeping/hardware-claim corruption inside properly-allocated structures β no adjacent-object memory-safety violation demonstrated. Root/loader-gated: Low severity.
No comments yet.