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.

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

  1. Setup: A codec driver module (codec->owner = THIS_MODULE) is registered while a fabric is present. try_module_get increments the refcount to N+1.

  2. Trigger fabric removal: aoa_fabric_unregister() iterates codec_list, calls aoa_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.

  3. 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).

  4. Use-after-free: If the module was freed between steps 2 and 3 (refcount hit 0), the module_put in step 3 writes to freed struct module memory. An attacker who can reallocate that memory (e.g. via usercopy gadgets, 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.