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.
Samsung's Wi-Fi chip has a serious flaw that researchers just discovered. Think of it like a security guard who's supposed to check that only one person enters a room at a time, but sometimes falls asleep on the job.
Here's what's happening: Samsung's processors have built-in Wi-Fi drivers—the software that makes your phone connect to Wi-Fi networks. These drivers have a variable (basically a sticky note) that multiple parts of the system write to and read from. When programmers forgot to add proper locks around this sticky note, bad things can happen.
An attacker can exploit this by sending lots of Wi-Fi commands at the exact same time from multiple threads (think of threads as separate workers in a kitchen). Because there's no coordination, two workers might both try to "clean up" the same piece of memory, creating what's called a "double free condition." It's like both trying to put the same dish away, which causes everything to break.
This could let a hacker take over your phone or crash it completely. The good news: no one's actively exploiting this in the wild yet, so you're not under attack right now.
Here's what to do: First, keep your Android phone updated. Samsung will patch this through regular security updates, so enable automatic updates in your settings. Second, be cautious about connecting to unfamiliar Wi-Fi networks, especially public ones—this is where attackers might try something like this. Third, if you own an older Samsung phone that's no longer receiving updates, consider upgrading to something more current. These three steps address the main ways you could actually get hurt by this flaw.
Want the full technical analysis? Click "Technical" above.
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.
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.