# DF-5582 — VERDICT

**Status: reproduced (panic, 3/3 stock boots). Fix validated (A/B on the
same rig schedule).**

## The finding (pass-2 lifecycle class)

`smbus_acpi_detach()` (`sys/bus/smbus/smbacpi/smbacpi.c:268-276`) is the
entire detach method:

```c
static int
smbus_acpi_detach(device_t dev)
{
	struct smbus_acpi_softc *sc = device_get_softc(dev);

	smbus_acpi_remove_address_space_handler(sc);

	return (0);
}
```

It never quiesces in-flight `smbus_acpi_space_handler()` dispatches. The
dispatch context is what makes this a use-after-free rather than a
benign race:

1. `AcpiEvAddressSpaceDispatch` (`contrib .../events/evregion.c:399-401`)
   **exits the AML interpreter** before calling non-default address-space
   handlers — because they may block. The GSBUS handler therefore runs
   unlocked and sleeping (the `SMBUS_*` calls into ig4 sleep in
   `wait_status`), while ordinary AML and the detach path continue.
2. The same dispatcher fills the shared handler context
   (`info->Connection/Length/AccessLength`, `evregion.c:429-444`) under
   `HandlerObj->AddressSpace.ContextMutex` and **holds that mutex across
   the handler call** (`evregion.c:447`). ACPICA protects the context
   this way, but nothing protects the *mutex itself* from removal.
3. `AcpiRemoveAddressSpaceHandler` (`.../events/evxfregn.c:290-408`)
   takes only ACPI_MTX_NAMESPACE (free, the interpreter is exited),
   detaches the regions, unlinks the HandlerObj and drops its last
   reference: `AcpiUtRemoveReference` frees the HandlerObj, and its
   destructor deletes the ContextMutex — **while the in-flight dispatch
   still holds that mutex**. (Verified in-tree: no in-flight tracking
   exists anywhere on that path.)
4. The moment detach returns, newbus frees the smbacpi softc — which IS
   the handler's `HandlerContext` (`&sc->space_handler_data`,
   `smbacpi.c:68,96-98`) — and the parent ig4iic teardown frees
   `data->dev`, the device every remaining `SMBUS_*` macro in the
   handler would dispatch on.

So an in-flight dispatch resumes holding a freed mutex, with its context
and bus device freed or about to be. That is exactly the
detach-without-drain defect family already verified for the siblings:
DF-5518 (`gpio_acpi`, same handler architecture), DF-5521/DF-5563 (iic
stack, freed-text variant).

## Why the trigger is realistic (and why severity is Low)

* In-tree, only `ig4iic` parents get a smbacpi child
  (`DRIVER_MODULE(smbacpi, ig4iic, ...)` at `smbacpi.c:295`, ig4 adds
  the child at `ichiic/ig4_iic.c:607`). ig4 detach / kldunload of the
  stack (or PCI hot-remove) is a root/privileged operation.
* The concurrent AML side needs no attacker: on real ig4-connected
  hardware, GSBUS fields are read by thermal zones, battery/`_DSM`
  methods and event notifications at arbitrary times. ACPI notifies are
  precisely the concurrent-AML source (ACPICA worker threads evaluate
  them with the ACPI lock model, not the caller's).
* Consequence observed: kernel panic (two signatures) / heap-metadata
  corruption — a privileged-trigger memory-corruption DoS. Not an
  unprivileged escalation path: Low, consistent with the family
  precedent (DF-5518/DF-5521/DF-5563 all Low).

## Reproduction (rig, real production code path)

The rig (`df5582_rig.c` + `df5582.dsl`) instantiates the REAL smbacpi
driver under a fake ig4iic parent whose ACPI ivar points at a planted
`\_SB.DFS` device, loads the crafted SSDT via `AcpiLoadTable()`, and
evaluates `TST0` — a GSBUS `AttribBytes(16)` field read — via
`AcpiEvaluateObject` from a kernel thread. The control run proves the
production path end-to-end:

```
[df5582] TRANS slave=0x10 cmd=0 op=0xc wcount=0 rcount=16 (#1)
[df5582] eval done: AE_OK
```

(`op=0xc` = `SMB_TRANS_NOCNT|SMB_TRANS_7BIT`, `rcount=16` = the field's
AccessLength — exactly `smbacpi.c:217-219`.)

The race parks the fake controller's `trans` method mid-dispatch, tears
the fake ig4iic down (running the real `smbus_acpi_detach` underneath),
and lets the dispatch resume.

### Stock kernel: reproduced 3/3 boots, two panic signatures

`run.race.stock.log`, `panic.txt`, `panic.spin.txt`:

```
-- parked=1 (handler in-flight, interpreter exited)
-- detached=2 parked_at_detach=1
```

Boot A and B (freed-ContextMutex release, deterministic):

```
panic with 1 spinlocks held
panic: spin_lock_ex: AcpiOsSignalSemaphore, indefinite wait!
cpuid = 5
 _spin_lock_contested
 _spin_lock_contested
 AcpiOsSignalSemaphore          <- signalling the FREED ContextMutex
 AcpiEvAddressSpaceDispatch     <- evregion.c:447 (ContextMutex release)
 AcpiExAccessRegion
 AcpiExReadSerialBus            <- the GSBUS field read (TST0)
 ...
 AcpiEvaluateObject
 df5582_eval_thread
```

Boot C (heap metadata corruption — the UAF write into freed M_ACPISEM
memory tripping the slab garbage collector):

```
panic: assertion "(((intptr_t)chunk ^ (intptr_t)z) & ZoneMask) == 0"
       failed in chunk_mark_free at /usr/src/sys/kern/kern_slaballoc.c:1675
 chunk_mark_free -> slab_cleanup -> slotimer_callback -> softclock_handler
```

(On boot C the freed semaphore's memory had been reused, so
`AcpiOsSignalSemaphore` wrote through the dangling pointer into a live
slab chunk — the corruption was detected asynchronously. Two independent
manifestations of the same underlying free-under-running-handler.)

### Fixed kernel: validated

`fix.diff` (authored after reproduction, never applied to the read-only
`sys/` tree) adds the DF-5518-style drain to smbacpi: a `busy` count +
`detaching` flag in `struct acpi_i2c_handler_data`; the handler refuses
new dispatches once detaching and accounts in-flight ones; detach sets
the flag, **drains in-flight dispatches, and only then** removes the
handler (the order matters: `AcpiRemoveAddressSpaceHandler` itself frees
the ContextMutex).

Applied in the guest `/usr/src`, kernel rebuilt
(`make nativekernel KERNCONF=X86_64_GENERIC`, kern.version
`#1: Thu Oct 8 02:57:28 UTC 2026`), rebooted, identical rig schedule:

```
-- parked=1 (handler in-flight, interpreter exited)
-- detached=2 parked_at_detach=0     <- teardown WAITED for the handler
-- STILL ALIVE after race (expected on the FIXED module)
```

`parked_at_detach` is sampled in-kernel at teardown completion (immune
to DF's cross-CPU console reordering): stock = 1 (teardown returned
while the handler was still parked → the frees happened under it →
panic), fixed = 0 (teardown blocked until the dispatch completed on live
objects → nothing to corrupt). Guest survived; the same eval returned
AE_OK afterwards.

Rig caveats documented for honesty: (a) DF's `SYSCTL_XLOCK` takes every
per-CPU `gd_sysctllock` (kern_sysctl.c:1642-1650), so the teardown must
be triggered from `/dev/df5582` ioctl, not a sysctl handler — doing it
from a sysctl wedges the sysctl tree (rig artifact, unrelated to the
finding; production kldunload holds no sysctl locks); (b) the guest
kernel has `device smbacpi` compiled in, so the fix had to be validated
with a kernel rebuild — an earlier module-only install silently changed
nothing (caught by the in-kernel ordering witness).

## Exploit chain (why this is not uid0)

Trigger is privileged (driver teardown) → this is a privileged-user
memory-corruption/panic, not an unpriv→root path. The AML-controlled
inputs that reach this handler (connection templates, lengths) are all
bounded by ACPICA's fixed per-protocol buffers (see negative results) —
the only AML-driven memory-safety bug in this file remains the known
DF-1112/DF-2332 `buf[32]` class.

## Negative results (classes hunted and killed)

* **GSBUS destination writes**: ACPICA sizes the Value buffer per
  protocol (`AcpiProtocolLengths[]` + 2: SEND_RECEIVE/BYTE=3, WORD=4,
  BLOCK/BYTES=257; `exfield.c:170-188`, `exserial.c:320-355,466-500`).
  Every `gsb->...` write in the handler (`smbacpi.c:169,180,192,204-205,
  221,236`) lands inside those fixed bounds — no AML-declared
  field-size dependence. Killed.
* **BLOCK read OOB**: ig4 (the only in-tree parent) caps the device
  count byte at the requested 32 (`ichiic/ig4_iic.c:474-475`:
  `if (rcount > (u_char)last) rcount = (u_char)last;` with rcount
  preloaded 32) — no in-tree read-side overflow. Killed.
* **Unsupported protocol attributes** (QUICK/PROCESS_CALL/
  BLOCK_PROCESS_CALL/RAW_BYTES/RAW_PROCESS): rejected with
  AE_BAD_PARAMETER (`smbacpi.c:229-233`). Killed.
* **WORD byte-order**: ig4 assembles SMBus words LSB-first and the
  handler stores LE (`ig4_iic.c:888` / `smbacpi.c:187-196`) — wire-order
  round-trip is consistent; no swap bug. Killed.
* **Shared-context race**: concurrent dispatches are serialized by
  ACPICA's ContextMutex (always created at install,
  `evhandler.c:680-695`) — no cross-field info race. Killed.
* **iiconf ownership classes (DF-5563/5564) applied to this consumer**:
  smbacpi issues only single-shot `SMBUS_*` method calls; ig4 serializes
  each internally (`lwkt_serialize_enter(&sc->slz)`); no multi-call bus
  ownership sequences, no waiters parked on any bus lock. Not
  applicable — clean.
* **`Address`/`BitWidth`**: u64→`char cmd` truncation only, no memory
  effect (`smbacpi.c:177-226`); BitWidth unused. Killed.
* **Install-failure paths**: attach ignores install failure (cosmetic);
  `AcpiBufferToResource` failure paths free correctly (`smbacpi.c:143-
  149,238-239`). Killed.

## Fix

`fix.diff` — drain in-flight GSBUS dispatches in `smbus_acpi_detach`
before `AcpiRemoveAddressSpaceHandler` (busy/detaching accounting in
`struct acpi_i2c_handler_data`, 30 s bounded wait; handler aborts new
dispatches once detaching). Validated live (witness 1→0, panic gone,
guest alive).

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