pnp_check_quirks PNP_QUIRK_EXTRA_IO writes ic_port[] past ISA_NPORT without bounds check
Summary
pnp_check_quirks PNP_QUIRK_EXTRA_IO unconditionally increments config->ic_nport and writes config->ic_port[ic_nport-1] without verifying room in ic_port[ISA_NPORT] array. ISA_NPORT=50. Card spoofing SoundBlaster vendor ID with 50 IO_RANGE descriptors drives ic_nport to limit; quirk writes ic_port[50]/[51] aliasing ic_irqmask/ic_drqmask/ic_nmem inside same struct isa_config corrupting IRQ/DMA/memory programming. Parser itself guards before incrementing but pnp_check_quirks called AFTER ic_nport++ so can exceed. OOB stays inside kmallocd isa_config not heap corruption but causes wrong IRQ programming (DoS). [VERIFIED pass2: reproduced with offsetof proof - ic_port 50 51 alias ic_irqmask ic_drqmask ic_nmem exactly as claimed, 17-tag stream corrupts masks that escape into the device config, 200 tags reach ic_port 599 past the 8640-byte kmalloc - linear attacker-paced heap overwrite ceiling quantified; bounds fix-validated, legal extra-port behavior preserved]
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2315 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| VERDICT.md | verdict | full narrative: layout proof, direct + end-to-end results, heap escape, fix validation | 5.3 KB | β raw |
| run.log | run-log | baseline decisive output: before/after config dumps for all scenarios | 4.1 KB | view raw |
| fix_run.log | run-log | fixed-kernel A/B: canaries intact, nport clamped at 50 | 2.5 KB | view raw |
| pnp_harness.c | source | kernel harness: verbatim pnp.c TU-compiled + /dev/pnph0 scenarios | 23.8 KB | view raw |
| pnphctl.c | source | userland scenario driver | 2.9 KB | view raw |
| pnph.h | source | shared ioctl definitions | 1.9 KB | view raw |
| prep.sh | build-script | guest prep with verbatim sha256 gate | 1.9 KB | view raw |
| build.sh | build-script | nativekernel KERNCONF=PNPH + installkernel | 126 B | view raw |
| run.sh | run-script | baseline scenario sequence | 136 B | view raw |
| build.log | build-log | baseline harness-kernel build evidence | 1.4 KB | view raw |
| fix_build.log | build-log | fixed-kernel build evidence | 1.4 KB | view raw |
| fix.diff | suggested-fix | validated two-guard bounds fix (git-apply-able, hunks verified) | 1.1 KB | view raw |
| env.txt | environment | guest uname, build, gates | 1.2 KB | view raw |
DF-2315 β pnp_check_quirks PNP_QUIRK_EXTRA_IO writes ic_port[] past ISA_NPORT without a bounds check
Verdict: REPRODUCED (mechanically, direct and end-to-end through the real parser). Impact: struct isa_config field corruption (IRQ/DMA masks, all four resource counters) + unbounded ic_port[] index writes that escape the configs heap allocation at scale. Confidence: certain.
What was run
Same verbatim-interposition PNPH kernel harness as DF-2314 (pnp.c TU-compiled unmodified, sha256-gated). Two drive modes:
- direct: the REAL
pnp_check_quirks()(pnp.c:320, non-static) on a fabricatedstruct isa_configwithic_port[0] = {0x220,0x22F,0x10,0x10}, canaries in the masks/counters, and the SoundBlaster quirk identity (vendor 0xc1008c0e SB64 / logical 0x22008c0e β quirk table pnp.c:91-92, arg1=0x400, arg2=0x800). - end-to-end: the REAL stock
pnp_parse_resources()(pnpparse.c) on a crafted card stream of N IO_RANGE tags (tag 0x47, base 0x220) with the SB64 identity set on a real isa0 child β the parser callspnp_check_quirks(..., config)after every IO_RANGE tag (pnpparse.c:169), each tag netting +3 ports (parser +1, quirk +2).
Results (baseline, kernel #5 15:58:28)
Layout proof (runtime offsetof): ic_port[50] sits at +928 =
ic_irqmask[0] (its 16 bytes alias ic_irqmask[0..1] + ic_drqmask[0..1]);
ic_port[51] at +944 = ic_nmem (aliases ic_nmem/ic_nport/ic_nirq/ic_ndrq).
ISA_NPORT = 50 (isavar.h:63); struct is 960 bytes (isavar.h:86-95).
-
ic_nport=49 (direct): arg1 β port[49] in bounds (nport 49β50); arg2 β port[50] written β ic_irqmask[0]=0xa20, ic_irqmask[1]=0xa2f, ic_drqmask[0]=0x10, ic_drqmask[1]=0x10 β the canaries {0xcafe0001..} are overwritten with port-derived values (port0+0x800 start/end and its size/align). nport ends at 51.
-
ic_nport=50 (direct): arg1 β port[50] β masks = {0x620,0x62f,0x10,0x10}; arg2 β port[51] β counters clobbered: ic_nmem=0xa20, ic_nport=0xa2f (2607), ic_nirq=0x10, ic_ndrq=0x10. The config now claims 2607 IO ports in a 50-entry array β downstream isa resource allocation / pnp_set_config iterates
i < ic_nportoveric_port[i](pnp.c:274-278) β mass OOB reads and wrong card programming (IRQ/DMA/memory) β the wrong-IRQ/DMA/mem DoS the finding claims. -
Real path, 17 IO tags: the stored config that escapes into the device (isa_config_entry via the real ISA_ADD_CONFIG, isa_common.c:939) shows nport=51 and irq={0xa20,0xa2f} drq={0x10,0x10} β the aliasing corruption demonstrably reaches the device record on the real parser path (the parser's own
== ISA_NPORTcheck at pnpparse.c:137 is equality-only and never fires on this +3/tag cadence). -
Real path, 18 IO tags: the parser itself writes port[51] (its equality check misses 51) β ic_nmem=0x220, ic_nirq/ic_ndrq=0x10 in the stored config; subsequent quirk writes re-read the wild counter and land at offsets 960/976 β i.e. inside configs[1] of the 9Γ960-byte alternatives array.
-
Real path, 200 IO tags: stored config nport=0x258 (600); last quirk write at ic_port[599] = configs+9712, past the end of the 8640-byte kmalloc β with card-controlled field bytes (ir_start = 16-bit card-supplied base, ir_size/ir_align = card bytes). Silent on this guest (the 8640-byte request lands in a larger malloc size class that absorbs the slack), but it is a linear, attacker-paced heap overwrite with semi-controlled data once tags exceed ~178. No panic, guest stayed up through all runs.
Reachability / attacker model (honest)
Same as DF-2314: parsed only at boot from ISA-PnP card resource data β malicious physical card or VM firmware (AV:P). A genuine SoundBlaster card never reaches 49+ ports legitimately; the overflow needs the attacker-controlled stream. Low severity stands (hardware-attacker DoS / config corruption, with a heap-overflow ceiling at scale).
Fix validation
fix.diff bounds both EXTRA_IO increments:
- if (qp->arg1 != 0) {
+ if (qp->arg1 != 0 && config->ic_nport < ISA_NPORT) {
...
- if (qp->arg2 != 0) {
+ if (qp->arg2 != 0 && config->ic_nport < ISA_NPORT) {
Applied to the guest tree, kernel rebuilt and booted: direct runs leave ALL canaries intact (nport 49β50 with arg1 legally applied; nport stays 50 at start 50 β both writes blocked); 17/18/200-tag real-path runs all store a clean config (nport=50, masks/counters untouched). No regression to the legitimate quirk behavior. fix_status: fixed.
Reproduce
# on the audit host (guest up): sh prep.sh && sh build.sh && reboot # inside guest /root/pnp sh run.sh # baseline # apply fix.diff to guest /usr/src/sys/bus/isa/pnp.c, sh build.sh, reboot sh run.sh # A/B after fix
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
fixedBoth fixes (DF-2314 trim guard and DF-2315 quirk bounds, same file) applied to guest /usr/src, kernel rebuilt and booted: baseline symptoms gone, legal quirk behavior intact.
fix_run.log: scenario 4 nport 49 to 50 with all canaries intact (arg2 blocked); scenario 5 nport stays 50; scenarios 6, 7 and 8 all store a clean config with nport=50 and untouched masks and counters
Confirmed kernel references
Detail
Evidence (decisive lines)
run.log: layout line ic_port[50] at +928 (ic_irqmask[0]) ic_port[51] at +944 (ic_nmem); direct after-dumps nport=51 irq={0xa20,0xa2f} drq={0x10,0x10} and nport=2607 cnt={0xa20,0xa2f,0x10,0x10}; parse(17 tags) stored ice_config nport=0x33 irq={0xa20,0xa2f}; parse(200 tags) last quirk write ic_port[599]=configs+9712 escape=YES; fix_run.log: canaries intact and nport=0x32 at every scenario
PoC changes
seed had no runnable harness; reused the verbatim-interposition PNPH kernel harness built for DF-2314 and added the quirk scenarios: direct pnp_check_quirks on a fabricated isa_config with canaries, a crafted IO_RANGE tag stream builder, a real isa0 child carrying the SB64 vendor and logical ids driven through the REAL stock pnp_parse_resources, and a dump of the isa_config_entry stored on the device via DEVTOISA and the id_configs tailq.
Verified recommended fix
bound both EXTRA_IO increments: if (qp->arg1 != 0 && config->ic_nport < ISA_NPORT) and likewise for arg2
Verdict
Reproduced mechanically through verbatim pnp.c compiled into the PNPH harness kernel (guest pnp.c sha256-gated PRISTINE). Runtime offsetof proof: ic_port[50] sits at +928 on ic_irqmask[0] (its 16 bytes alias ic_irqmask[0..1] and ic_drqmask[0..1]) and ic_port[51] at +944 on ic_nmem (aliasing ic_nmem, ic_nport, ic_nirq, ic_ndrq). The REAL pnp_check_quirks with a SoundBlaster quirk identity (vendor 0xc1008c0e, logical 0x22008c0e) on a fabricated isa_config: at ic_nport=49 the arg2 write lands on ic_port[50] and overwrites the IRQ and DMA mask canaries with port-derived values 0xa20, 0xa2f, 0x10, 0x10; at ic_nport=50 both writes go out of bounds and the counters end up 0xa20, 0xa2f (2607), 0x10, 0x10 - a config claiming 2607 IO ports in a 50-entry array that pnp_set_config and isa allocation then iterate (pnp.c:274-278). End-to-end through the REAL stock pnp_parse_resources with a crafted 17-tag SB64 IO stream: the stored config that escapes into the device via the real ISA_ADD_CONFIG carries the corrupted masks; at 18 tags the parser itself writes ic_port[51] (its equality-only ISA_NPORT check never fires on the +3-per-tag cadence) and the counters clobber; at 200 tags the unbounded index reaches ic_port[599] = configs+9712, past the end of the 8640-byte kmalloc, with card-controlled ir_start (16-bit card base) and card-supplied size and align bytes - silent on this guest only because the malloc size class absorbs the slack. Impact ceiling: wrong IRQ, DMA and memory programming of the card at set-config time (local DoS of the device or bus resources) plus a linear attacker-paced heap overwrite given enough tags. Attacker model: malicious physical ISA-PnP card or VM firmware at boot (AV:P); no local or remote unprivileged trigger. Fix validated on a rebuilt kernel: with the ic_nport < ISA_NPORT bounds on both EXTRA_IO increments all canaries stay intact, nport clamps at 50 for 49, 50, 17, 18 and 200 tag runs, and the legal extra-port quirk behavior (arg1 applied while room remains) still works.
No comments yet.