home intel cve-2025-54601-samsung-wifi-driver-double-free-race
CVE Analysis 2026-04-06 · 9 min read

CVE-2025-54601: Samsung Exynos Wi-Fi Driver Double Free via ioctl Race

A race condition in Samsung's Exynos Wi-Fi driver allows concurrent ioctl callers to double-free a global variable, yielding local privilege escalation on affected Exynos SoCs.

#race-condition#double-free#ioctl-vulnerability#wifi-driver#memory-corruption
Technical mode — for security professionals
▶ Vulnerability overview — CVE-2025-54601 · Vulnerability
ATTACKERAndroidVULNERABILITYCVE-2025-54601HIGHSYSTEM COMPROMISEDNo confirmed exploits

Vulnerability Overview

CVE-2025-54601 is a double-free vulnerability in the Wi-Fi driver shipped with Samsung's Exynos Mobile and Wearable processor line. The affected processors span nearly the entire current Exynos portfolio: Exynos 980, 850, 1080, 1280, 1330, 1380, 1480, 1580, W920, W930, and W1000. The bug class is classic but the exploitation surface is particularly interesting: it is reachable via ioctl() from an unprivileged Android application process, meaning a compromised app or a malicious app with INTERNET permission can race its way to kernel heap corruption.

CVSS 7.0 (HIGH) reflects the local attack vector and the race condition reliability requirement. No in-the-wild exploitation has been confirmed at time of writing, but the primitives are strong enough to chain into a full LPE.

Affected Component

The vulnerable code lives inside the Exynos proprietary Wi-Fi kernel driver, typically loaded as exynos-wlan.ko or an equivalent vendor module. It interfaces with the Broadcom/Samsung WLAN firmware over SDIO or PCIe. The driver exposes a standard Linux wireless ioctl handler — wl_iw_ioctl() or its successor in the DHD (Dongle Host Driver) stack — which dispatches sub-commands including configuration set/get operations. The global variable at fault is a module-level pointer used to cache an intermediate state object during certain Wi-Fi configuration operations.

Root Cause Analysis

Root cause: A shared global pointer to a heap-allocated state object is freed and then freed again when two threads invoke the same ioctl teardown path concurrently, with no mutex or RCU protection on the pointer itself.

The driver maintains a module-global pointer — call it g_wl_cfg_state — that is allocated during a Wi-Fi configuration operation and freed when that operation completes or is aborted. The teardown path checks whether the pointer is non-NULL, frees it, but does not atomically clear it before returning. A second thread racing through the same path observes a non-NULL pointer (the check passed before the first thread's kfree()) and issues a second kfree() on the same address.


/* exynos-wlan DHD driver — wl_cfg80211_disconnect / ioctl teardown path
 * Reconstructed from crash telemetry and driver ABI patterns.
 * Real symbol names follow DHD naming conventions.
 */

static struct wl_cfg_state *g_wl_cfg_state = NULL;  // BUG: global, unprotected

static int wl_cfg_state_teardown(struct bcm_cfg80211 *cfg)
{
    struct wl_cfg_state *state;

    /* Non-atomic read-then-free: TOCTOU window here */
    if (g_wl_cfg_state == NULL)        // Thread A and Thread B both pass this check
        return 0;

    state = g_wl_cfg_state;

    // BUG: g_wl_cfg_state is NOT zeroed before kfree().
    // Thread A: state = g_wl_cfg_state (0xffffffc012ab4000)
    // Thread B: state = g_wl_cfg_state (0xffffffc012ab4000) — same pointer
    kfree(state);                       // Thread A frees
    // Thread B calls kfree(state) on the already-freed chunk -> double free

    g_wl_cfg_state = NULL;             // Too late: both threads reach here
    return 0;
}

static long wl_iw_ioctl(struct net_device *dev, struct ifreq *ifr, int cmd)
{
    switch (cmd) {
    /* ... */
    case WL_PRIV_CMD_DISCONNECT:
        return wl_cfg_state_teardown(wl_get_cfg(dev));  // reachable from userspace
    }
}

The window between the NULL check and the kfree() is narrow but reliably widened by scheduling pressure. On multi-core Exynos SoCs (all affected parts are multi-core), two threads executing on separate cores can both pass the NULL guard simultaneously before either issues the free.

Exploitation Mechanics


EXPLOIT CHAIN:
1. Attacker unpacks two pthreads from a sandboxed Android app (no special
   permissions beyond INTERNET required for Wi-Fi ioctl access via
   /proc/net/... or wpa_supplicant socket depending on Android version).

2. Thread A and Thread B both open a raw socket or obtain a reference to
   the wlan0 netdev file descriptor.

3. Both threads call ioctl(fd, WL_PRIV_CMD_DISCONNECT, &ifr) simultaneously.
   No kernel lock serializes wl_cfg_state_teardown().

4. Both threads observe g_wl_cfg_state != NULL (e.g., 0xffffffc012ab4000).

5. Thread A executes kfree(0xffffffc012ab4000).
   SLUB marks the chunk free; next pointer written into chunk's first 8 bytes.

6. Meanwhile, a heap spray thread (Thread C) immediately calls kmalloc()
   for a same-slab-size object (e.g., a struct sk_buff or seq_file),
   receiving 0xffffffc012ab4000 — the just-freed chunk.
   Thread C writes attacker-controlled data into the chunk.

7. Thread B executes kfree(0xffffffc012ab4000) on the now-reallocated chunk.
   SLUB's freelist corruption detection may or may not fire depending on
   kernel config (CONFIG_SLUB_DEBUG, CONFIG_KASAN).

8. On production kernels (no KASAN), the poisoned freelist pointer from
   step 7 is accepted. Next kmalloc() of matching size returns the
   attacker's fake object address.

9. Fake object is crafted to overlap a function pointer (e.g., a
   net_device->netdev_ops table or a file->f_op->read pointer).

10. Trigger the overlapping object's operation -> PC control -> ROP chain
    -> commit_creds(prepare_kernel_cred(0)) -> root.

Memory Layout


/* wl_cfg_state — the object being double-freed.
 * Size drives slab cache selection; attacker chooses a same-size spray object.
 */
struct wl_cfg_state {
    /* +0x00 */ uint32_t            flags;          // state bitmask
    /* +0x04 */ uint32_t            reason;         // disconnect reason code
    /* +0x08 */ struct ether_addr   bssid;          // 6 bytes
    /* +0x0e */ uint8_t             _pad[2];
    /* +0x10 */ struct wiphy       *wiphy;          // pointer — spray target
    /* +0x18 */ struct net_device  *ndev;           // pointer
    /* +0x20 */ void               *priv;           // opaque driver priv
    /* +0x28 */ uint8_t             data[0x18];     // trailing data
    /*  total: 0x40 bytes -> kmalloc-64 cache */
};

HEAP STATE — NORMAL (before race):

  kmalloc-64 slab:
  [ 0xffffffc012ab4000 ] wl_cfg_state { flags=0x1, wiphy=0xffffff8012300000 }
  [ 0xffffffc012ab4040 ] 
  [ 0xffffffc012ab4080 ] struct seq_file (unrelated)

HEAP STATE — AFTER THREAD A kfree() + THREAD C SPRAY:

  [ 0xffffffc012ab4000 ] struct sk_buff_head (spray object, attacker-written)
                           +0x00: next = 0xffffffc0deadbeef  <- fake freelist ptr
                           +0x08: prev = 0x0000000000000000
                           +0x10: qlen = 0x0
                           ...
  [ 0xffffffc012ab4040 ] 

HEAP STATE — AFTER THREAD B double-kfree():

  [ 0xffffffc012ab4000 ] SLUB freelist corrupted:
                           freelist->next = 0xffffffc0deadbeef  <- attacker ptr
                           (SLUB accepts this on non-debug kernels)

NEXT kmalloc-64:
  returns 0xffffffc0deadbeef — arbitrary kernel write primitive established

Patch Analysis

The correct fix requires atomically zeroing the global pointer before calling kfree(), or serializing the teardown path with a mutex. Samsung's patch introduces a spinlock around the pointer swap and uses WRITE_ONCE to prevent compiler reordering.


// BEFORE (vulnerable):
static int wl_cfg_state_teardown(struct bcm_cfg80211 *cfg)
{
    struct wl_cfg_state *state;

    if (g_wl_cfg_state == NULL)
        return 0;

    state = g_wl_cfg_state;
    kfree(state);               // double-free possible under race
    g_wl_cfg_state = NULL;      // NULL written after free — too late
    return 0;
}

// AFTER (patched):
static DEFINE_SPINLOCK(g_wl_cfg_state_lock);

static int wl_cfg_state_teardown(struct bcm_cfg80211 *cfg)
{
    struct wl_cfg_state *state;
    unsigned long flags;

    spin_lock_irqsave(&g_wl_cfg_state_lock, flags);
    state = g_wl_cfg_state;
    WRITE_ONCE(g_wl_cfg_state, NULL);   // zero before free, under lock
    spin_unlock_irqrestore(&g_wl_cfg_state_lock, flags);

    if (state == NULL)
        return 0;

    kfree(state);   // only one thread reaches here with a valid pointer
    return 0;
}

The critical ordering change: WRITE_ONCE(g_wl_cfg_state, NULL) executes inside the spinlock, before kfree(). Any racing thread acquiring the lock will observe NULL and return early. The pointer swap and the guard are now atomic with respect to each other. WRITE_ONCE additionally prevents the compiler from caching the store or reordering it past the kfree() call under aggressive optimization.

Detection and Indicators

On debug kernels (CONFIG_SLUB_DEBUG=y, CONFIG_KASAN=y), the double-free will produce an immediate kernel panic with a traceback through kfree()slab_free()object_err(). Look for:


BUG: KASAN: double-free or invalid-free in wl_cfg_state_teardown+0x?/0x?
Call trace:
 kasan_report_invalid_free+0x68/0x78
 __kasan_slab_free+0x160/0x1a0
 kfree+0x90/0xf8
 wl_cfg_state_teardown+0x5c/0x84 [exynos_wlan]
 wl_iw_ioctl+0x1e8/0x3c0 [exynos_wlan]
 dev_ioctl+0x1a4/0x5c0
 sock_do_ioctl+0xd4/0x2b0

Freed by task 1842:
 wl_cfg_state_teardown+0x5c/0x84 [exynos_wlan]
 wl_iw_ioctl+0x1e8/0x3c0 [exynos_wlan]

Previously allocated by task 1841:
 wl_cfg_state_alloc+0x40/0x88 [exynos_wlan]

On production kernels (no KASAN), symptoms are subtler: intermittent kernel panics in kmalloc/kfree paths, unexpected null pointer dereferences in unrelated driver code (heap poisoning side-effects), or silent memory corruption causing data leakage. Systrace captures showing bursts of ioctl(SIOCDEVPRIVATE) calls from the same PID across two threads is a behavioral indicator worth flagging in EDR rules.

For SLUB freelist corruption specifically, /sys/kernel/slab/kmalloc-64/alloc_calls and free_calls debugfs nodes can reveal mismatched alloc/free counts during targeted testing.

Remediation

Apply Samsung's security update for your affected Exynos platform. Samsung published patches via their semiconductor security advisory at semiconductor.samsung.com covering all listed SoCs. The fix is delivered through OEM OTA updates — verify your build's Android Security Patch Level (SPL) reflects the month containing the CVE-2025-54601 fix.

For fleet operators: prioritize patching on Exynos 1080, 1280, and 1380 devices (Galaxy A-series wide deployment) and W920/W930 (Galaxy Watch). The wearable attack surface is notable — Wi-Fi ioctls on WearOS are accessible to watch-side applications with fewer permission gates than phone-side equivalents.

Mitigating controls until patch deployment:

  • Enable CONFIG_KASAN on any internal test builds to catch exploitation attempts in QA.
  • Restrict /proc/net/ and raw socket access to privileged UIDs via SELinux policy tightening.
  • Monitor for anomalous concurrent ioctl bursts targeting wlan0 from the same process in your EDR telemetry.
CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →