[PATCH AUTOSEL 6.19-5.10] atm: lec: fix use-after-free in sock_def_readable()

Sasha Levin
2026-03-24T11:19:45Z

From: Deepanshu Kartikey <kartikey406@gmail.com>

[ Upstream commit 922814879542c2e397b0e9641fd36b8202a8e555 ]

A race condition exists between lec_atm_close() setting priv->lecd
to NULL and concurrent access to priv->lecd in send_to_lecd(),
lec_handle_bridge(), and lec_atm_send(). When the socket is freed
via RCU while another thread is still using it, a use-after-free
occurs in sock_def_readable() when accessing the socket's wait queue.

The root cause is that lec_atm_close() clears priv->lecd without
any synchronization, while callers dereference priv->lecd without
any protection against concurrent teardown.

Fix this by converting priv->lecd to an RCU-protected pointer:
- Mark priv->lecd as __rcu in lec.h
- Use rcu_assign_pointer() in lec_atm_close() and lecd_attach()
for safe pointer assignment
- Use rcu_access_pointer() for NULL checks that do not dereference
the pointer in lec_start_xmit(), lec_push(), send_to_lecd() and
lecd_attach()
- Use rcu_read_lock/rcu_dereference/rcu_read_unlock in send_to_lecd(),
lec_handle_bridge() and lec_atm_send() to safely access lecd
- Use rcu_assign_pointer() followed by synchronize_rcu() in
lec_atm_close() to ensure all readers have completed before
proceeding. This is safe since lec_atm_close() is called from
vcc_release() which holds lock_sock(), a sleeping lock.
- Remove the manual sk_receive_queue drain from lec_atm_close()
since vcc_destroy_socket() already drains it after lec_atm_close()
returns.

v2: Switch from spinlock + sock_hold/put approach to RCU to properly
fix the race. The v1 spinlock approach had two issues pointed out
by Eric Dumazet:
1. priv->lecd was still accessed directly after releasing the
lock instead of using a local copy.
2. The spinlock did not prevent packets being queued after
lec_atm_close() drains sk_receive_queue since timer and
workqueue paths bypass netif_stop_queue().

Note: Syzbot patch testing was attempted but the test VM terminated
unexpectedly with "Connection to localhost closed by remote host",
likely due to a QEMU AHCI emulation issue unrelated to this fix.
Compile testing with "make W=1 net/atm/lec.o" passes cleanly.

Reported-by: syzbot+f50072212ab792c86925@syzkaller.appspotmail.com
Closes: https://syzkaller.appspot.com/bug?extid=f50072212ab792c86925
Link: https://lore.kernel.org/all/20260309093614.502094-1-kartikey406@gmail.com/T/ [v1]
Signed-off-by: Deepanshu Kartikey <kartikey406@gmail.com>
Reviewed-by: Eric Dumazet <edumazet@google.com>
Link: https://patch.msgid.link/20260309155908.508768-1-kartikey406@gmail.com
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
Signed-off-by: Sasha Levin <sashal@kernel.org>
---

LLM Generated explanations, may be completely bogus:

None of these are specific to the ATM LEC `priv->lecd` race. No prior
fix for this specific bug exists.

Record: No prior fix for this specific bug in stable.

## PHASE 7: SUBSYSTEM AND MAINTAINER CONTEXT

### Step 7.1: IDENTIFY THE SUBSYSTEM AND ITS CRITICALITY
- **Subsystem:** net/atm (ATM networking, LAN Emulation Client)
- **Criticality:** PERIPHERAL — ATM is a legacy networking technology,
but it's still used in some DSL/broadband environments and the code is
compiled into many kernel configs

### Step 7.2: ASSESS SUBSYSTEM ACTIVITY
Very low activity (legacy subsystem), but bugs still get fixed when
found. The fact that syzbot can trigger it means the code is reachable
and exercised by kernel fuzzing.

Record: [net/atm] [PERIPHERAL but still reachable and fuzzed] [Legacy
but compiled into many configs]

## PHASE 8: IMPACT AND RISK ASSESSMENT

### Step 8.1: DETERMINE WHO IS AFFECTED
Users with ATM/LEC networking configured (CONFIG_ATM_LANE). This is a
legacy technology but still compiled in many distribution kernels.

Record: Users with ATM LANE support configured. Narrower user base but
still present in many distro configs.

### Step 8.2: DETERMINE THE TRIGGER CONDITIONS
- Race between closing the ATM LEC daemon socket and concurrent network
operations (transmit, bridge handling, ARP)
- Syzbot triggers it via IPv6 MLD workqueue → packet transmission path
- Can be triggered from userspace (syzbot confirms reproducibility)
- Timing-dependent race but with a real race window

Record: Userspace-triggerable race condition. Reproducible by syzbot
fuzzer.

### Step 8.3: DETERMINE THE FAILURE MODE SEVERITY
- **Use-after-free** detected by KASAN
- Can cause: kernel crash/oops, potential memory corruption, potentially
exploitable
- UAFs are among the most dangerous bug classes — they can lead to
privilege escalation
- **Severity: CRITICAL** (UAF with userspace trigger)

Record: [KASAN slab-use-after-free] [Severity: CRITICAL — UAF with
userspace trigger, crash/corruption/potential exploit]

### Step 8.4: CALCULATE RISK-BENEFIT RATIO
**BENEFIT:** High
- Fixes a syzbot-confirmed UAF that affects all stable trees (5.4+)
- Prevents kernel crash/corruption
- Userspace-triggerable = security relevant
- Well-reviewed by top networking expert

**RISK:** Low-Medium
- ~50 lines changed, but all within a single well-contained pattern (RCU
conversion)
- Textbook RCU pattern, well-understood
- Reviewed by Eric Dumazet
- synchronize_rcu() is safe in the calling context (sleeping lock held)
- Low file churn means clean backport likely

Record: [Benefit: HIGH — UAF fix, all stable trees affected, security-
relevant] [Risk: LOW — textbook RCU, expert-reviewed, well-contained]
[Ratio: Strongly favorable]

## PHASE 9: FINAL SYNTHESIS

### Step 9.1: COMPILE THE EVIDENCE

**Evidence FOR backporting:**
1. Fixes a confirmed use-after-free (KASAN slab-use-after-free in
sock_def_readable)
2. Reported by syzbot — reproducible with concrete trigger
3. Affects ALL active stable trees (5.4, 5.10, 5.15, 6.1, 6.6)
4. Userspace-triggerable race condition = security-relevant
5. Reviewed-by Eric Dumazet (top networking expert)
6. Committed by Jakub Kicinski (net maintainer)
7. v2 after expert review iteration — fix quality is high
8. Textbook RCU pattern — well-understood, low regression risk
9. Well-contained to single file + header
10. No prerequisites or dependencies
11. Low-churn file — clean backport expected
12. send_to_lecd() has 11 call sites — wide impact surface

**Evidence AGAINST backporting:**
1. ~50 lines of change (moderate but not huge)
2. ATM/LEC is a legacy subsystem with narrower user base
3. Syzbot VM testing was inconclusive (QEMU issue, not fix issue)
4. No explicit Cc: stable (expected for candidates under review)

**UNRESOLVED QUESTIONS:**
- None significant. All claims verified through git blame, syzbot
report, and mailing list discussion.

### Step 9.2: APPLY THE STABLE RULES CHECKLIST
1. **Obviously correct and tested?** YES — textbook RCU conversion,
reviewed by Eric Dumazet, compile-tested
2. **Fixes a real bug that affects users?** YES — syzbot-confirmed UAF,
all stable trees affected
3. **Important issue?** YES — UAF (crash, corruption, potential security
exploit)
4. **Small and contained?** YES — 2 files, ~50 lines, single pattern
conversion
5. **No new features or APIs?** CORRECT — pure bug fix
6. **Can apply to stable trees?** YES — low-churn file, should apply
cleanly

### Step 9.3: CHECK FOR EXCEPTION CATEGORIES
Not an exception category — this is a straightforward bug fix, the
primary stable material.

### Step 9.4: DECISION

This is a clear YES. A syzbot-confirmed use-after-free affecting all
active stable trees, with a well-reviewed RCU-based fix from expert
networking developers. The fix is textbook, contained, and has no
dependencies.

## Verification

- [Phase 1] Parsed tags: Reported-by: syzbot, Reviewed-by: Eric Dumazet,
Signed-off-by: Jakub Kicinski (net maintainer). v2 after expert
review.
- [Phase 2] Diff analysis: +48/-26 lines across 7 functions in lec.c + 1
line in lec.h. All changes convert bare priv->lecd access to RCU-
protected access (rcu_read_lock/rcu_dereference/rcu_access_pointer/rcu
_assign_pointer/synchronize_rcu).
- [Phase 3] git blame: Buggy code dates to initial git import (2005,
^1da177e4c3f41) and 2006 ATM rework (d44f77466cfdc6). Present in ALL
stable trees.
- [Phase 3] git log: File has only 15 commits since 2020, very low
churn. No prerequisites needed.
- [Phase 3] Author has 10+ bug-fix commits across multiple subsystems.
Fix endorsed by Eric Dumazet and Jakub Kicinski.
- [Phase 4] Syzbot report confirmed: KASAN slab-use-after-free in
sock_def_readable. Affected versions: 5.4, 5.10, 5.15, 6.1, 6.6.
- [Phase 4] Lore discussion: v2 patch, no NAKs, Eric Dumazet gave
Reviewed-by after v1 issues fixed.
- [Phase 5] send_to_lecd() has 11 call sites in lec.c.
lec_handle_bridge() called from transmit path. Bug is userspace-
reachable (confirmed by syzbot IPv6 MLD trigger).
- [Phase 6] Bug exists in all active stable trees. No prior fix for this
specific race. Clean backport expected.
- [Phase 7] net/atm is legacy but compiled in many distro configs and
exercised by syzbot.
- [Phase 8] Failure mode: UAF → crash/corruption/potential exploit.
Severity: CRITICAL. Benefit: HIGH, Risk: LOW.

**YES**

net/atm/lec.c | 72 +++++++++++++++++++++++++++++++++------------------
net/atm/lec.h | 2 +-
2 files changed, 48 insertions(+), 26 deletions(-)

diff --git a/net/atm/lec.c b/net/atm/lec.c
index c39dc5d367979..b6f764e524f7c 100644
--- a/net/atm/lec.c
+++ b/net/atm/lec.c
@@ -154,10 +154,19 @@ static void lec_handle_bridge(struct sk_buff *skb, struct net_device *dev)
/* 0x01 is topology change */

priv = netdev_priv(dev);
- atm_force_charge(priv->lecd, skb2->truesize);
- sk = sk_atm(priv->lecd);
- skb_queue_tail(&sk->sk_receive_queue, skb2);
- sk->sk_data_ready(sk);
+ struct atm_vcc *vcc;
+
+ rcu_read_lock();
+ vcc = rcu_dereference(priv->lecd);
+ if (vcc) {
+ atm_force_charge(vcc, skb2->truesize);
+ sk = sk_atm(vcc);
+ skb_queue_tail(&sk->sk_receive_queue, skb2);
+ sk->sk_data_ready(sk);
+ } else {
+ dev_kfree_skb(skb2);
+ }
+ rcu_read_unlock();
}
}
#endif /* IS_ENABLED(CONFIG_BRIDGE) */
@@ -216,7 +225,7 @@ static netdev_tx_t lec_start_xmit(struct sk_buff *skb,
int is_rdesc;

pr_debug("called\n");
- if (!priv->lecd) {
+ if (!rcu_access_pointer(priv->lecd)) {
pr_info("%s:No lecd attached\n", dev->name);
dev->stats.tx_errors++;
netif_stop_queue(dev);
@@ -449,10 +458,19 @@ static int lec_atm_send(struct atm_vcc *vcc, struct sk_buff *skb)
break;
skb2->len = sizeof(struct atmlec_msg);
skb_copy_to_linear_data(skb2, mesg, sizeof(*mesg));
- atm_force_charge(priv->lecd, skb2->truesize);
- sk = sk_atm(priv->lecd);
- skb_queue_tail(&sk->sk_receive_queue, skb2);
- sk->sk_data_ready(sk);
+ struct atm_vcc *vcc;
+
+ rcu_read_lock();
+ vcc = rcu_dereference(priv->lecd);
+ if (vcc) {
+ atm_force_charge(vcc, skb2->truesize);
+ sk = sk_atm(vcc);
+ skb_queue_tail(&sk->sk_receive_queue, skb2);
+ sk->sk_data_ready(sk);
+ } else {
+ dev_kfree_skb(skb2);
+ }
+ rcu_read_unlock();
}
}
#endif /* IS_ENABLED(CONFIG_BRIDGE) */
@@ -468,23 +486,16 @@ static int lec_atm_send(struct atm_vcc *vcc, struct sk_buff *skb)

static void lec_atm_close(struct atm_vcc *vcc)
{
- struct sk_buff *skb;
struct net_device *dev = (struct net_device *)vcc->proto_data;
struct lec_priv *priv = netdev_priv(dev);

- priv->lecd = NULL;
+ rcu_assign_pointer(priv->lecd, NULL);
+ synchronize_rcu();
/* Do something needful? */

netif_stop_queue(dev);
lec_arp_destroy(priv);

- if (skb_peek(&sk_atm(vcc)->sk_receive_queue))
- pr_info("%s closing with messages pending\n", dev->name);
- while ((skb = skb_dequeue(&sk_atm(vcc)->sk_receive_queue))) {
- atm_return(vcc, skb->truesize);
- dev_kfree_skb(skb);
- }
-
pr_info("%s: Shut down!\n", dev->name);
module_put(THIS_MODULE);
}
@@ -510,12 +521,14 @@ send_to_lecd(struct lec_priv *priv, atmlec_msg_type type,
const unsigned char *mac_addr, const unsigned char *atm_addr,
struct sk_buff *data)
{
+ struct atm_vcc *vcc;
struct sock *sk;
struct sk_buff *skb;
struct atmlec_msg *mesg;

- if (!priv || !priv->lecd)
+ if (!priv || !rcu_access_pointer(priv->lecd))
return -1;
+
skb = alloc_skb(sizeof(struct atmlec_msg), GFP_ATOMIC);
if (!skb)
return -1;
@@ -532,18 +545,27 @@ send_to_lecd(struct lec_priv *priv, atmlec_msg_type type,
if (atm_addr)
memcpy(&mesg->content.normal.atm_addr, atm_addr, ATM_ESA_LEN);

- atm_force_charge(priv->lecd, skb->truesize);
- sk = sk_atm(priv->lecd);
+ rcu_read_lock();
+ vcc = rcu_dereference(priv->lecd);
+ if (!vcc) {
+ rcu_read_unlock();
+ kfree_skb(skb);
+ return -1;
+ }
+
+ atm_force_charge(vcc, skb->truesize);
+ sk = sk_atm(vcc);
skb_queue_tail(&sk->sk_receive_queue, skb);
sk->sk_data_ready(sk);

if (data != NULL) {
pr_debug("about to send %d bytes of data\n", data->len);
- atm_force_charge(priv->lecd, data->truesize);
+ atm_force_charge(vcc, data->truesize);
skb_queue_tail(&sk->sk_receive_queue, data);
sk->sk_data_ready(sk);
}

+ rcu_read_unlock();
return 0;
}

@@ -618,7 +640,7 @@ static void lec_push(struct atm_vcc *vcc, struct sk_buff *skb)

atm_return(vcc, skb->truesize);
if (*(__be16 *) skb->data == htons(priv->lecid) ||
- !priv->lecd || !(dev->flags & IFF_UP)) {
+ !rcu_access_pointer(priv->lecd) || !(dev->flags & IFF_UP)) {
/*
* Probably looping back, or if lecd is missing,
* lecd has gone down
@@ -753,12 +775,12 @@ static int lecd_attach(struct atm_vcc *vcc, int arg)
priv = netdev_priv(dev_lec[i]);
} else {
priv = netdev_priv(dev_lec[i]);
- if (priv->lecd)
+ if (rcu_access_pointer(priv->lecd))
return -EADDRINUSE;
}
lec_arp_init(priv);
priv->itfnum = i; /* LANE2 addition */
- priv->lecd = vcc;
+ rcu_assign_pointer(priv->lecd, vcc);
vcc->dev = &lecatm_dev;
vcc_insert_socket(sk_atm(vcc));

diff --git a/net/atm/lec.h b/net/atm/lec.h
index be0e2667bd8c3..ec85709bf8185 100644
--- a/net/atm/lec.h
+++ b/net/atm/lec.h
@@ -91,7 +91,7 @@ struct lec_priv {
*/
spinlock_t lec_arp_lock;
struct atm_vcc *mcast_vcc; /* Default Multicast Send VCC */
- struct atm_vcc *lecd;
+ struct atm_vcc __rcu *lecd;
struct delayed_work lec_arp_work; /* C10 */
unsigned int maximum_unknown_frame_count;
/*
--
2.51.0