# VERDICT — DF-4811 (aps.c off-by-one I/O port window)

**finding_id:** DF-4811
**status:** untested (static proof only; runtime verification not possible on the audit guest)
**impact:** none (hygiene/hardening — out-of-bounds I/O *port* access, not kernel memory)

## Was it reproduced?

No, and it cannot be on this guest, for a hardware reason (not a code reason):

* The bug lives behind `aps_probe()` → `aps_do_io()` (aps.c:315), which requires
  a Renesas H8S embedded controller answering on the LPC window at 0x1600.
* The QEMU/KVM audit guest has no such EC. On it, port 0x1604 (APS_STR3) reads
  0xFF; the arbitration loop drains and `aps_do_io()` returns 1; `aps_probe()`
  returns ENXIO (aps.c:203-206, 315-318) and the driver never attaches.
  (`vm.sh` guest = QEMU x86_64, no ThinkPad EC device model exists.)
* The driver is additionally not loaded by default (GENERIC has it commented:
  sys/config/X86_64_GENERIC:178; module is `optional aps isa`, sys/conf/files:2600).

Per the audit contract, PoC verification is skipped for Info-severity
hardening findings; this pack documents the static proof instead.

## Static proof (why the finding is nonetheless certain)

Arithmetic, no hardware needed:

1. `APS_ADDR_BASE = 0x1600`, `APS_ADDR_SIZE = 0x1f` (aps.c:101-102) →
   `bus_set_resource()` (aps.c:284-285) allocates ports 0x1600..0x161E
   (31 ports; rman ranges are [start, start+count)).
2. `APS_TWR_RET = 0x1f` (aps.c:61); `APS_TWR_BASE = 0x10` (aps.c:60).
3. `aps_do_io()` reads offset 0x1f directly at aps.c:193, and both the write
   loop (aps.c:213-215, n max 15 → offset 0x10+0x0f = 0x1f; bit 15 forced by
   `wmask |= APS_READ_RET` at aps.c:210) and the read loop (aps.c:236-238,
   bit 15 forced at aps.c:233) touch offset 0x1f on every invocation.
4. 0x1600 + 0x1f = 0x161F ∉ [0x1600, 0x161E] ⇒ exactly one port past the
   allocated window, in every EC transaction (probe/attach/poll/suspend/resume).
5. The register file itself (TWR0..TWR15, aps.c:47-48 comment) legitimately
   extends to offset 0x1F, so the driver *needs* a 0x20-byte window; the size
   constant is the off-by-one, inherited from OpenBSD aps.c rev 1.19
   (`$OpenBSD: aps.c,v 1.19` header at aps.c:1).

## Why impact = none (what an attacker cannot get)

* The OOB access is a fixed single port (0x161F), fixed direction set
  (read + one write of constant 0x01 at aps.c:211/215), no attacker-controlled
  value, count, or timing beyond "driver loaded + EC transaction happens".
* No kernel memory is involved; `bus_space_*_1` on x86 is a bare `inb/outb`.
* Loading the driver requires root (`kldload` / kernel config).
* No path from this to privilege escalation, info leak, or kernel memory
  corruption exists on the affected (ThinkPad) hardware — the EC decodes 0x161F
  anyway, which is why this has been harmless upstream since 2005.

## Classes considered and killed in aps.c (pass 2)

* **detach vs poll UAF (DF-4647/4773 family):** `sensor_task_unregister()`
  (kern_sensors.c:238-249) needs `thr->lock`; the per-CPU sensor thread holds
  that lock *across* `st->func(st->arg)` (kern_sensors.c:307,326-336), so
  unregister cannot return while `aps_refresh` is executing, and a
  running=0 task is never executed again (freed at kern_sensors.c:328-330).
  `aps_detach` (aps.c:405-414) deinstalls sysctls first, then unregisters,
  then releases the I/O resource — no window.
* **sysctl strings (DF-4766 family):** `sysctl_handle_sensor`
  (kern_sensors.c:474-497) copies into an M_ZERO'd buffer via bounded memcpy;
  `desc[]` filled with `ksnprintf(..., sizeof(...))` (aps.c:361-389). No
  unterminated-string shape.
* **iobuf[16] uninit / OOB:** all `aps_do_io` accesses are n ∈ [0,15]; every
  byte written to the EC is initialized (cmd at aps.c:196, args at :215, RET=1
  at :211); all bytes consumed afterwards are device-read (aps.c:236-241).
* **busy-wait DoS:** `DRIVERSLEEP` is microseconds (sys/platform/pc64/isa/clock.c:485);
  worst case 100×500 µs = 50 ms per transaction — not a stall. (The sensor
  thread holding its lock across callback sleeps is a kern_sensors.c design
  property, out of scope for this file.)
* **sensor counts vs arrays:** loops bounded by `APS_NUM_SENSORS` (9) against
  `sensors[APS_NUM_SENSORS]` (aps.c:116,134,394-395,496,525).
* **resume double-register:** `sensor_task_register` at aps.c:556 is paired
  with the unregister at aps.c:567 by ACPI suspend/resume ordering.
* **attach unwinding:** failure paths (aps.c:349-358) precede any
  registration — clean.

## Fix validation

Not performed (runtime verification impossible: guest lacks the EC; Info
severity). `fix.diff` is a static, one-line derivation
(`APS_ADDR_SIZE 0x1f` → `0x20`, making the window 0x1600..0x161F, which is
exactly what aps_do_io touches). No code under `sys/` was modified.

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