β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-4812

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4812 Β· 10 files
FileTypeDescriptionSize
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]

VERDICT.md
↓ download raw

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 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_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

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

fixed
baseline reproduced→ patch + rebuild →patched clean

One-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)
↓ fix.diffper-fix-DF-4812

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.