Vulnerability Report: Double module_put / Refcount Underflow in sound/aoa/core/core.c
Summary
A double module_put() in the Apple Onboard Audio (AOA) driver core causes a module
reference-count underflow. When the reference count reaches zero the kernel may unload
the owning module; any subsequent access to that module’s code or data is a
use-after-free. This is exploitable for local privilege escalation on PowerPC Mac
hardware running an affected kernel.
Affected File
sound/aoa/core/core.c
Vulnerability Class
CWE-911 – Improper Update of Reference Count
(leads to CWE-416 – Use After Free)
Root Cause
Each time a codec is successfully attached to a fabric,
attach_codec_to_fabric() increments the module reference count exactly once:
/* core.c:27 */
if (!try_module_get(c->owner)) // +1 ref
return -EBUSY;
There are two independent code paths that each unconditionally call module_put() on
that same owner, but neither checks whether the other has already done so.
Path 1 – aoa_fabric_unlink_codec() (line 146)
void aoa_fabric_unlink_codec(struct aoa_codec *codec)
{
...
codec->fabric = NULL;
module_put(codec->owner); // <-- first put
}
Called from aoa_fabric_unregister() for every codec still linked to the fabric being
removed.
Path 2 – aoa_codec_unregister() (line 80)
void aoa_codec_unregister(struct aoa_codec *codec)
{
list_del(&codec->list);
if (codec->fabric && codec->exit) // fabric is already NULL here
codec->exit(codec);
if (fabric && fabric->remove_codec) // fabric global also NULL
fabric->remove_codec(codec);
codec->fabric = NULL;
module_put(codec->owner); // <-- second put
}
After aoa_fabric_unregister() sets codec->fabric = NULL and clears the global
fabric pointer, all the guard conditions in aoa_codec_unregister() evaluate to
false — so neither early-exit path fires — and module_put() is called a second time
on the same owner.
Exploit Scenario
-
Setup: A codec driver module (
codec->owner = THIS_MODULE) is registered while a fabric is present.try_module_getincrements the refcount to N+1. -
Trigger fabric removal:
aoa_fabric_unregister()iteratescodec_list, callsaoa_fabric_unlink_codec()for each attached codec →module_put()→ refcount drops to N. When N was 1, refcount is now 0 and the kernel considers the module eligible for unloading. -
Trigger codec unregister:
aoa_codec_unregister()is called for the same codec (normal driver teardown order).module_put()is called again → refcount underflows (wraps or goes negative depending on the atomic type). -
Use-after-free: If the module was freed between steps 2 and 3 (refcount hit 0), the
module_putin step 3 writes to freedstruct modulememory. An attacker who can reallocate that memory (e.g. viausercopygadgets, SLUB heap spray) controls what the write lands on, enabling kernel code execution.
Even without a race, a negative/wrapped refcount permanently prevents the module from being unloaded in future iterations, leaking kernel memory indefinitely and potentially enabling denial-of-service.
Proof-of-Concept (pseudo-code)
// 1. Load codec module → aoa_codec_register() [try_module_get, ref=1]
// 2. Load fabric module → aoa_fabric_register() [attaches codec]
// 3. Unload fabric → aoa_fabric_unregister() [module_put, ref=0]
// (module may be freed here by the kernel)
// 4. Unload codec → aoa_codec_unregister() [module_put on freed struct, ref=-1]
// ^ UAF / refcount underflow
Impact
| Property | Value |
|---|---|
| Attack vector | Local (requires ability to load/unload kernel modules, or trigger via device hotplug) |
| Privileges required | CAP_SYS_MODULE or physical device access |
| Impact | Kernel use-after-free → potential local privilege escalation / kernel crash |
| CVSS v3 (estimated) | 7.8 (AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H) |
Fix
Track whether module_put has already been called (e.g. via a bool owner_put flag on
struct aoa_codec), or centralise the put exclusively in aoa_codec_unregister() and
remove it from aoa_fabric_unlink_codec():
/* aoa_fabric_unlink_codec: remove the unconditional module_put */
- module_put(codec->owner);
/* aoa_codec_unregister: keep the single authoritative put */
module_put(codec->owner);
Additionally, the entire file lacks any locking on the global fabric pointer and
codec_list, which introduces TOCTOU races between register/unregister paths — a
secondary issue that should be addressed with a mutex.