# DF-4811 — aps(4): every EC transaction touches I/O port 0x161F, one byte outside the driver's allocated resource window

**File:** `sys/dev/powermng/aps/aps.c`
**Severity:** Info (hardening / correctness — no memory-safety impact)
**Status:** untested (static proof; not runtime-verifiable — see VERDICT.md)

## What the driver is

`aps.c` is the DragonFly port of the OpenBSD ThinkPad **Active Protection System**
driver (sudden-motion sensor + EC thermometer on the Renesas H8S embedded
controller, LPC I/O window at 0x1600). Despite the directory name
(`sys/dev/powermng/`), it is not an Apple-SMC driver; it is ISA/LPC, probed via
`aps_identify()` which adds a fixed-port child.

## The bug (pure arithmetic)

```
aps.c:101  #define APS_ADDR_BASE   0x1600
aps.c:102  #define APS_ADDR_SIZE   0x1f        /* 31 ports: 0x1600..0x161E */
aps.c:61   #define APS_TWR_RET     0x1f        /* last TWR register offset  */
```

`aps_identify()` allocates the resource with that size:

```
aps.c:284  bus_set_resource(child, SYS_RES_IOPORT, 0,
aps.c:285      APS_ADDR_BASE, APS_ADDR_SIZE, -1);
```

A size of 0x1f covers offsets 0x00..0x1E (ports 0x1600..0x161E). But the EC's
register file is 16 TWR registers at offsets 0x10..0x1F (comment at aps.c:47-48:
"EC uses LPC Channel 3 registers TWR0..15"), and the driver accesses offset 0x1F
in **every** `aps_do_io()` transaction:

```
aps.c:193  bus_space_read_1(iot, ioh, APS_TWR_RET);        /* read @ 0x161F */
aps.c:210  wmask |= APS_READ_RET;                          /* force bit 15   */
aps.c:211  buf[APS_RET] = 0x01;
aps.c:213  for (n = 1, bp = 2; n < 16; bp <<= 1, n++)
aps.c:214      if (wmask & bp)
aps.c:215          bus_space_write_1(iot, ioh,             /* write @ 0x161F */
aps.c:215              APS_TWR_BASE + n, buf[n]);          /* n=15 → 0x10+15=0x1f */
aps.c:233  rmask |= APS_READ_RET;
aps.c:236  for (n = 0, bp = 1; n < 16; bp <<= 1, n++)
aps.c:238      buf[n] = bus_space_read_1(iot, ioh,         /* read @ 0x161F */
aps.c:238          APS_TWR_BASE + n);
```

Port 0x1600 + 0x1f = **0x161F** is exactly one past the end of the
rman-allocated window `[0x1600, 0x161E]`. The size constant should be 0x20.

## Reachability

* Driver is optional: `sys/conf/files:2600` (`dev/powermng/aps/aps.c optional aps isa`);
  commented out in GENERIC (`sys/config/X86_64_GENERIC:178`), enabled via
  `device aps0 at isa? port 0x1600` (LINT64:1677) or `kldload aps`.
* Once loaded, the OOB access happens on every EC transaction (probe, poll,
  suspend/resume re-init) — i.e., the normal operation of the driver on the
  ThinkPads it supports.
* On non-APS machines, `aps_do_io()`'s arbitration drain path (aps.c:192-194)
  still reads port 0x161F repeatedly during probe, then returns ENXIO.

## Impact ceiling

None demonstrated beyond hygiene: a single inb/outb to a port outside the
driver's rman allocation. No kernel memory safety, no info leak, no privilege
boundary. Worst theoretical case is interference with an unrelated device that
decodes 0x161F on non-ThinkPad hardware (where the driver fails probe anyway).

## Reproduce (on ThinkPad hardware with the EC)

```
kldload aps                 # or boot a kernel with `device aps0 at isa? port 0x1600`
sysctl hw.sensors.aps0      # each refresh = aps_do_io = OOB touch at 0x161F
```

Not reproducible in the QEMU audit guest (no H8S EC behind port 0x1600; the
driver never attaches) — see VERDICT.md.

## Fix

One line: `APS_ADDR_SIZE 0x20` (see `fix.diff`; static derivation, not
runtime-validated).
