hci_call() leaks the ACPI_ALLOCATE_BUFFER result buffer on a malformed GHCI return package: early 'return (ENXIO)' skips the AcpiOsFree that every other exit performs β unbounded M_ACPICA kernel heap leak per unprivileged sysctl read
Summary
The !ACPI_PKG_VALID branch returns ENXIO before the end: label that frees the buffer - the only pointer to the ACPICA allocation is dropped, and every hw.acpi.toshiba.* sysctl read reaches hci_call. DYNAMICALLY REPRODUCED on guest with a crafted SSDT (TOS6200 whose GHCI returns Package(3)): stock driver leaked 469 objs/46K -> 118K objs/11.0MB (~190B per invocation) via a silent unprivileged sysctl loop (DF sysctl swallows failed leaf reads, exit 0). One-line AcpiOsFree fix validated live on same guest: pool exactly flat under identical 40k-read load with the path marker still firing.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4812 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 1.9 KB | β raw | |
| VERDICT.md | β | 5.0 KB | β raw | |
| verdict.json | β | 4.2 KB | view raw | |
| run.sh | β | 187 B | view raw | |
| ssdt_tosh.dsl | β | 1.3 KB | β download | |
| ssdt_tosh.aml | β | 90 B | β download | |
| run.log | β | 4.0 KB | view raw | |
| fix_build.log | β | 8.2 KB | view raw | |
| env.txt | β | 384 B | view raw | |
| fix.diff | β | 502 B | view raw |
DF-4812 β acpi_toshiba(4): hci_call() leaks the ACPI result buffer on a malformed GHCI return package (missing AcpiOsFree before early return).
POC artifacts
ssdt_tosh.dsl / ssdt_tosh.aml crafted SSDT defining a TOS6200 device whose GHCI returns Package(3) (HCI_WORDS=6 expected) and whose ENAB succeeds. run.sh full reproduction procedure (below)
Build
Host side (one-time): scp ssdt_tosh.dsl dfbsd:/tmp/df4812/ ssh dfbsd 'cd /tmp/df4812 && /usr/sbin/iasl ssdt_tosh.dsl' scp dfbsd:/tmp/df4812/ssdt_tosh.aml . Guest side (needs reboot with the table): ./dfbsd-qemu/vm.sh down ( cd dfbsd-qemu && QEMU_EXTRA="-acpitable file=$PWD/ssdt_tosh.aml" ./vm.sh up )
Run
# kldload acpi_toshiba # real in-tree driver attaches to \TSH0 # vmstat -m | grep acpica # baseline $ i=0; while [ $i -lt 20000 ]; do # AS UNPRIVILEGED USER sysctl hw.acpi.toshiba.force_fan >/dev/null 2>&1; i=$((i+1)); done # vmstat -m | grep acpica # compare
Expected (observed on DF 6.5-DEVELOPMENT guest, KVM): baseline : acpica 469 46.0K ... 221K after : acpica 39.5K 3.71M ... 572K (+3.66 MB / 20k reads) dmesg : "toshiba: invalid package!" repeated per read Note: sysctl(8) exits 0 and prints NOTHING (DF sysctl(8) silently swallows failed leaf reads) β the trigger is silent for the invoking user.
Fix validation
apply fix.diff in guest /usr/src, rebuild the module: cd /usr/src/sys/dev/acpica/acpi_toshiba && make obj && make kldunload acpi_toshiba kldload /usr/obj/usr/src/sys/dev/acpica/acpi_toshiba/acpi_toshiba.ko re-run the same 40,000-read load -> M_ACPICA Count/MemUse FLAT (118K/11.0M before and after), markers still present. [fix_build.log / run.log]
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():
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 witholdptr=NULL, then the real read; sbin/sysctl/sysctl.cshow_var()), and DFsysctl(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_calls),
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:
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.
Fix verification
fixedOne-line AcpiOsFree(results.Pointer) before the line-440 return; applied to guest /usr/src, module rebuilt clean (-Werror), swapped live. Baseline module: +7.3MB M_ACPICA under 40k reads; patched module under the identical load: exactly 0 growth with path markers still firing. Bad behavior GONE.
findings/poc/DF-4812/fix.diff; findings/poc/DF-4812/fix_build.log; findings/poc/DF-4812/run.log (RUN 3)
Confirmed kernel references
Detail
Exploit chain
unprivileged user loop: while :; sysctl hw.acpi.toshiba.force_fan (silent, exit 0) -> 2x hci_call -> 2x leaked ACPI_OBJECT buffers per iteration -> linear M_ACPICA exhaustion -> kernel memory starvation/DoS (no info disclosure; contents are AML-built package objects)
Evidence (decisive lines)
findings/poc/DF-4812/run.log (before/after vmstat -m rows for baseline runs 1-2 and patched run 3, dmesg marker count 40283); VERDICT.md (full path trace path:line + fix validation); ssdt_tosh.dsl/.aml (crafted table); fix_build.log (clean -Werror module build); env.txt (uname, kldstat, SSDT present, final pool state)
PoC changes
No seed PoC (pass-2 static finding). Verification built from scratch: crafted SSDT compiled with the guest's /usr/sbin/iasl, injected via QEMU_EXTRA=-acpitable (vm.sh supports the env var); unprivileged read loop via vm.sh run_user.
Verified recommended fix
Add AcpiOsFree(results.Pointer) before the 'return (ENXIO)' in hci_call()'s invalid-package branch (acpi_toshiba.c:440)
Verdict
REPRODUCED dynamically on the audit guest with the stock in-tree driver: a crafted SSDT (QEMU -acpitable) supplies the TOS6200 device whose GHCI answers Package(3) instead of the required HCI_WORDS=6; acpi_toshiba.c hci_call() (sys/dev/acpica/acpi_toshiba/acpi_toshiba.c:433-441) evaluates GHCI with ACPI_ALLOCATE_BUFFER, ACPI_PKG_VALID fails, and the function returns ENXIO at line 440 WITHOUT AcpiOsFree(results.Pointer) β every other exit frees at the end: label (460-462). Unprivileged sysctl READS of hw.acpi.toshiba.* then leak the ACPICA result buffer (M_ACPICA, Osd/OsdMemory.c:62): measured 469 objs/46.0K -> 39.5K/3.71M after 20k reads and -> 118K/11.0M after 40k more (~190B and ~1 object per hci_call; sysctl(8) makes 2 handler calls per invocation and silently swallows the error, exit 0). Unbounded -> kernel heap exhaustion DoS on any unit whose GHCI firmware returns a short package. Fix validated on the same guest: one-line AcpiOsFree before the return, module rebuilt and swapped; identical 40k-read load leaves M_ACPICA exactly flat (118K/11.0M before and after) while the 'invalid package!' marker and allocation counters prove the same path still executes.
No comments yet.