# DF-4812 — VERDICT

**Status: REPRODUCED (dynamic, full kernel path) — impact: kernel heap leak
(unbounded, unprivileged, silent) — fix validated on the same guest.**

## Root cause

`sys/dev/acpica/acpi_toshiba/acpi_toshiba.c`, `hci_call()`:

```c
412	ACPI_OBJECT_LIST args;
413	ACPI_BUFFER    results;
...
433	results.Pointer = NULL;
434	results.Length = ACPI_ALLOCATE_BUFFER;
435	if (ACPI_FAILURE(AcpiEvaluateObject(h, METHOD_HCI, &args, &results)))   /* GHCI */
436		goto end;
437	res = (ACPI_OBJECT *)results.Pointer;
438	if (!ACPI_PKG_VALID(res, HCI_WORDS)) {
439		kprintf("toshiba: invalid package!\n");
440		return (ENXIO);            /* <-- BUG: results.Pointer never AcpiOsFree'd */
441	}
```

`results.Length = ACPI_ALLOCATE_BUFFER` makes ACPICA `AcpiOsAllocate()` the
return object into `results.Pointer` (Osd/OsdMemory.c:62 — `M_ACPICA` pool).
Every other exit path frees it at the `end:` label (lines 460-462), but the
invalid-package path returns at line 440 before the label, dropping the only
pointer to the buffer. `HCI_WORDS` is 6 (line 116); any GHCI implementation
that answers with a package of fewer than 6 elements (or a non-package —
then `AcpiEvaluateObject` still succeeds with a buffer? no: non-package
returns a buffer object too, `ACPI_PKG_VALID` fails on Type) leaks the whole
ACPI_OBJECT buffer.

## Trigger / threat model

- Any local user: a plain **read** of any `hw.acpi.toshiba.*` sysctl
  (`acpi_toshiba_sysctl()` → `handler(sc->handle, HCI_GET, &arg)` →
  `hci_call()`). Reads are not privilege-gated (only writes are, and even
  those are CTLFLAG_ANYBODY — DF-1892). `sysctl -a`, monitoring daemons, etc.
  all walk the tree.
- Each `sysctl(8)` invocation performs **two** handler calls (size-estimate
  read with `oldptr=NULL`, then the real read; sbin/sysctl/sysctl.c
  `show_var()`), and DF `sysctl(8)` **silently swallows** the failed leaf
  read (`if (i || !len) { error = 1; goto done; }` — no message, exit 0), so
  the trigger leaves no user-visible trace.
- Requires a Toshiba HCI firmware variant whose GHCI returns a short
  package. This is exactly the class of firmware divergence this defensive
  branch exists for (it was added because some models return malformed
  packages); on such a machine every sysctl tree walk leaks.

## Dynamic reproduction on the audit guest (full kernel path, real driver)

The audit QEMU guest has no Toshiba hardware, so the missing precondition was
supplied the way an auditor legitimately can: a **crafted SSDT** (QEMU
`-acpitable`) defining `\TSH0 { _HID="TOS6200", ENAB→0, GHCI→Package(3){...} }`.
The stock in-tree `acpi_toshiba.ko` then attaches to the real ACPI device:

```
acpi_toshiba0: <Toshiba HCI Extras> on acpi0
    acpi_toshiba0 pnpinfo _HID=TOS6200 _UID=0 at handle=\TSH0
```

Measurements (`vmstat -m` row `acpica` = the M_ACPICA pool used by
AcpiOsAllocate):

| run | load (unprivileged user) | count | MemUse |
|-----|---------------------------|-------|--------|
| 1 before | — | 469 | 46.0K |
| 1 after | 20,000 reads force_fan | 39.5K | 3.71M |
| 2 after | 40,000 reads (2 nodes) | 118K | 11.0M |
| 3 (patched module) before | — | 118K | 11.0M |
| 3 (patched module) after | same 40,000 reads | **118K** | **11.0M (flat)** |

≈190 bytes / ~2 M_ACPICA objects per `sysctl(8)` invocation (two `hci_call`s),
linear and unbounded; ~1 object per `hci_call()` — exactly the dropped
`results.Pointer`. `dmesg` shows the matching `toshiba: invalid package!`
marker per call (40,283 retained in the msgbuf ring). After >100k reads the
pool held 11 MB; the loop is unbounded → kernel memory exhaustion (DoS).

## Fix validation

`fix.diff` adds the one missing free before the early return:

```diff
 	if (!ACPI_PKG_VALID(res, HCI_WORDS)) {
 		kprintf("toshiba: invalid package!\n");
+		AcpiOsFree(results.Pointer);
 		return (ENXIO);
 	}
```

Applied inside the guest `/usr/src`, module rebuilt with `make` (clean,
`-Werror`, see fix_build.log), swapped in via `kldunload`/`kldload`. Under the
identical 40,000-read load the M_ACPICA pool stayed **exactly flat** while the
Requests counter still climbed by ~680K and the `invalid package!` markers
still appeared — i.e. the same path executed, now correctly freeing.
`fix_status: fixed` (baseline reproduced / patched clean on the same guest).

## Classification

- severity: **Low** (hardware-firmware-dependent trigger; consequence is
  unbounded kernel heap exhaustion = local DoS on affected laptops)
- CWE-401 (Missing Release of Memory after Effective Lifetime)
- bucket: base:dev (kernel memory *loss*, not an info leak)
- kernel_refs: sys/dev/acpica/acpi_toshiba/acpi_toshiba.c:433-441,
  sys/dev/acpica/Osd/OsdMemory.c:62-76, sys/dev/acpica/acpivar.h:491-493

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