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

smbus_acpi_detach does not quiesce in-flight GSBUS address-space handler dispatches: AcpiRemoveAddressSpaceHandler frees the handler object and its held ContextMutex, and newbus frees the softc/parent device underneath the running handler (UAF, panic/heap corruption)

Summary

GSBUS handlers run with the interpreter exited while holding the ContextMutex; detach only calls AcpiRemoveAddressSpaceHandler, which has no in-flight tracking: it unlinks and frees the HandlerObj + the ContextMutex the dispatch still holds, then newbus frees the softc (the HandlerContext) and parent device. Rig with the REAL driver under a fake ig4iic + planted SSDT driven through real ACPICA field machinery: dispatch parked mid-transfer, teardown run underneath -> stock kernel panicked 3/3 (2x spin_lock_ex indefinite wait on the freed semaphore, 1x slab chunk_mark_free assertion - heap metadata corruption from the write through freed memory); busy/detaching drain fix validated (parked_at_detach=0, no panic). SMBus sibling of DF-5518/5521/5563. Privileged trigger, memory-corruption DoS.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5582 Β· 18 files
FileTypeDescriptionSize
README.md β€” 5.7 KB ↓ raw
VERDICT.md β€” 9.8 KB ↓ raw
verdict.json β€” 5.4 KB view raw
manifest.json β€” 1.1 KB view raw
df5582.dsl β€” 3.0 KB ↓ download
df5582_rig.c β€” 19.5 KB view raw
Makefile.rig β€” 459 B ↓ download
trigger.c β€” 580 B view raw
build.sh β€” 130 B view raw
run.sh β€” 95 B view raw
build.log β€” 6.7 KB view raw
run.control.log β€” 350 B view raw
run.race.stock.log β€” 859 B view raw
run.race.fixed.log β€” 916 B view raw
panic.txt β€” 1.4 KB view raw
panic.spin.txt β€” 5.2 KB view raw
fix.diff β€” 2.9 KB view raw
env.txt β€” 414 B view raw

DF-5582 β€” smbacpi: driver detach does not quiesce in-flight GSBUS

address-space handler dispatches (UAF: freed handler-object/ContextMutex/ softc under the running handler)

  • File under audit: sys/bus/smbus/smbacpi/smbacpi.c (pass 2, GLM 5.3)
  • Class: kernel use-after-free / lifecycle race (memcorrupt)
  • Severity: Low (root-gated trigger: device teardown / kldunload of the ig4/smbacpi stack racing a concurrent AML GSBUS access; the SMBus sibling of the verified DF-5518 (gpio_acpi) and DF-5521/DF-5563 (iic) no-quiesce findings)
  • Verdict: REPRODUCED β€” 3 natural kernel panics across 3 boots, two distinct signatures; fix validated (drain witness flips 1 β†’ 0, guest survives the identical schedule).

What the code does

smbus_acpi_space_handler() (smbacpi.c:120-242) is the ACPI GenericSerialBus (GSBUS) operation-region handler that smbacpi installs on its ig4iic parent's ACPI node (smbacpi.c:89-103). Every AML GSBUS field access is dispatched to it by ACPICA with the AML interpreter exited (AcpiEvAddressSpaceDispatch, evregion.c:399-401) and with the handler-object's ContextMutex held across the call (evregion.c:429-447) β€” the handler sleeps inside the SMBus transfer (SMBUS_* on ig4 sleeps in wait_status).

smbus_acpi_detach() (smbacpi.c:268-276) only calls AcpiRemoveAddressSpaceHandler() and returns 0. Nothing waits for in-flight dispatches:

  • AcpiRemoveAddressSpaceHandler (evxfregn.c:290-408) takes only ACPI_MTX_NAMESPACE, unlinks the HandlerObj and drops its last reference β†’ the handler object and its ContextMutex β€” which the in-flight dispatch still holds β€” are freed underneath the running handler;
  • newbus then frees the smbacpi softc (the handler's HandlerContext, &sc->space_handler_data) and, as the parent detaches, the ig4iic device (data->dev) that the handler's SMBUS_* calls dispatch on;
  • with smbacpi built as a module, kldunload additionally frees the module text the in-flight handler will resume into (the DF-5521 / DF-5563 freed-text mechanism).

On the stock kernel the resumed handler releases the freed ContextMutex: AcpiEvAddressSpaceDispatch β†’ AcpiOsReleaseMutex β†’ AcpiOsSignalSemaphore on struct acpi_semaphore memory already returned to the M_ACPISEM zone β€” observed as (a) an indefinite spin-lock wait panic and (b) slab metadata corruption.

Rig

df5582_rig.ko brings the REAL production handler up and drives it through the REAL ACPICA field machinery:

nexus -> df5582rig0 (rig bus; bus_read_ivar answers ACPI_IVAR_HANDLE
                      with \_SB.DFS β€” what ig4_acpi gives ig4iic on
                      real hardware)
           -> ig4iic0  (rig: fake ig4iic SMBus controller; implements
                        the smbus_if methods with a controllable park)
              -> smbacpi0 (REAL smbacpi driver, unmodified kernel)

A crafted SSDT (df5582.dsl) is loaded at rig attach with AcpiLoadTable():

\_SB.DFS : OpRegion(SMB0, GenericSerialBus) + I2cSerialBus Connection
           Field FLD0 (AttribBytes(16))  -> BYTES read -> SMBUS_TRANS
           Method TST0 (read FLD0), TST1 (BLOCK write, len 0xFF)

hw.df5582.go=1 evaluates TST0; the fake ig4iic's trans method parks (tsleep) while the REAL smbus_acpi_space_handler is on the stack, interpreter exited, ContextMutex held. ./trigger (ioctl DF5582_IOC_DETACH on /dev/df5582) then tears the fake ig4iic down, running smbus_acpi_detach() underneath the parked handler.

Note: the guest's X86_64_GENERIC has device smbacpi compiled in (sys/config/X86_64_GENERIC:336), so the production code under test is the in-kernel copy; the fix validation therefore rebuilds the kernel, not just the module.

Reproduce

# stage this directory to the guest as /root/df5582, then (as root):
sh build.sh                     # iasl SSDT + rig kld + trigger(1)
sh run.sh control               # sanity: real handler dispatches (count>0, AE_OK)
sh run.sh race                  # stock kernel: panic (see below)
# after vm.sh reset with-src + staging:
#   apply fix.diff in /usr/src, make nativekernel+installkernel, reboot
sh run.sh race                  # fixed kernel: drain witness 0, STILL ALIVE

Expected output

Control (run.control.log):

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

Race, stock kernel (run.race.stock.log + panic.txt / panic.spin.txt):

-- parked=1 (handler in-flight, interpreter exited)
-- detached=2 parked_at_detach=1     <- teardown returned while parked
panic: spin_lock_ex: AcpiOsSignalSemaphore, indefinite wait!      (2 boots)
panic: assertion "(((intptr_t)chunk ^ (intptr_t)z) & ZoneMask) == 0"
       failed in chunk_mark_free at kern_slaballoc.c:1675          (1 boot)

Race, fixed kernel (run.race.fixed.log):

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

parked_at_detach is sampled in-kernel at teardown completion (console prints reorder across CPUs on DF, so this is the authoritative ordering witness): 1 = teardown finished while the handler was still in-flight (stock), 0 = teardown waited for it (fixed).

Bonus (not a new finding)

sh run.sh bonus (TST1) exercises the KNOWN pass-1 finding DF-1112/DF-2332: a GSBUS BLOCK write whose buffer length byte is 0xFF drives memcpy(buf, gsb->data, gsb->len) (smbacpi.c:207-208) with buf[32] on the handler's stack β€” a 255-byte stack smash fully controlled by AML. Not re-reported here; the rig field exists so the known class can be re-demonstrated live if wanted.

VERDICT.md
↓ download raw

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:

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.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Baseline (stock kernel): parked_at_detach=1 and panic in 3/3 boots (AcpiOsSignalSemaphore indefinite-wait x2, chunk_mark_free slab assertion x1). Patched (fix.diff applied to guest /usr/src, kernel rebuilt as #1 Oct 8 02:57:28 UTC 2026, rebooted): identical rig schedule, parked_at_detach=0 β€” smbus_acpi_detach waited for the parked dispatch to complete before removing the handler β€” no panic, guest alive, eval returned AE_OK.

run.race.stock.log vs run.race.fixed.log (parked_at_detach 1 -> 0); panic.txt + panic.spin.txt (baseline panics); fix.diff; VERDICT.md 'Fixed kernel' section
↓ fix.diffper-fix-DF-5582

Confirmed kernel references

Detail

Evidence (decisive lines)

panic.txt (slab-corruption panic: chunk_mark_free assertion, kern_slaballoc.c:1675); panic.spin.txt (spin panic: 'panic: spin_lock_ex: AcpiOsSignalSemaphore, indefinite wait!' with the AcpiExReadSerialBus->AcpiEvAddressSpaceDispatch->AcpiOsSignalSemaphore trace); run.race.stock.log ('-- detached=2 parked_at_detach=1'); run.race.fixed.log ('-- detached=2 parked_at_detach=0' + 'STILL ALIVE'); run.control.log (real-handler dispatch proof: 'TRANS slave=0x10 cmd=0 op=0xc rcount=16'); VERDICT.md (full trace + negative results); fix.diff (drain patch)

PoC changes

Replaced the pass-1-style sketch entirely: rig kld instantiates the REAL smbacpi under a fake 'ig4iic' device whose bus_read_ivar answers ACPI_IVAR_HANDLE (what ig4_acpi provides on hardware), loads a crafted SSDT via AcpiLoadTable (GSBUS opregion + I2cSerialBus Connection + AttribBytes(16)/AttribBlock fields + TST0/TST1), evaluates TST0 from a kthread, parks the fake SMBUS_TRANS mid-dispatch, and tears the device down via /dev/df5582 ioctl (sysctl-triggered teardown deadlocks on DF's per-CPU sysctl locks β€” rig artifact). In-kernel parked_at_detach witness added because DF console prints reorder across CPUs. Fix validation required a kernel rebuild (guest X86_64_GENERIC has device smbacpi compiled in; a module-only install silently changed nothing β€” caught by the witness).

Verified recommended fix

Drain in-flight GSBUS dispatches in smbus_acpi_detach before removing the handler: busy/detaching accounting in struct acpi_i2c_handler_data, handler refuses new dispatches once detaching, detach waits for busy==0 then calls AcpiRemoveAddressSpaceHandler (fix.diff).

Verdict

smbus_acpi_detach() (sys/bus/smbus/smbacpi/smbacpi.c:268-276) removes the GSBUS address-space handler without quiescing in-flight smbus_acpi_space_handler() dispatches, which ACPICA runs with the interpreter exited and the handler-object ContextMutex held. AcpiRemoveAddressSpaceHandler frees the HandlerObj and that ContextMutex under the running dispatch, and newbus then frees the softc (the handler Context) and the parent ig4iic device. Reproduced 3/3 stock boots with a rig driving the real production path (fake ACPI-ivared ig4iic parent + AcpiLoadTable SSDT + AcpiEvaluateObject): two AcpiOsSignalSemaphore indefinite-wait panics (freed ContextMutex release, trace through AcpiEvAddressSpaceDispatch/AcpiExReadSerialBus) and one chunk_mark_free slab assertion (heap metadata corruption from the write through the freed semaphore). Root-gated trigger (ig4/smbacpi teardown racing concurrent AML), so Low severity, memcorrupt family β€” same defect class as verified DF-5518 (gpio_acpi) and DF-5521/DF-5563 (iic). fix.diff (busy/detaching drain before handler removal) validated with an in-kernel ordering witness: stock parked_at_detach=1 + panic; fixed kernel parked_at_detach=0, drain held, guest alive, identical schedule.