β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-5647

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5647 Β· 9 files
FileTypeDescriptionSize
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).

VERDICT.md
↓ download raw

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.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Same 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
↓ fix.diffper-fix-DF-5647

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.