From: Hillf Danton <hdanton@sina.com>
To: Deepanshu Kartikey <kartikey406@gmail.com>
Cc: edumazet@google.com, netdev@vger.kernel.org,
linux-kernel@vger.kernel.org,
syzbot+f50072212ab792c86925@syzkaller.appspotmail.com
Subject: Re: [PATCH v2] atm: lec: fix use-after-free in sock_def_readable()
Date: Tue, 10 Mar 2026 11:18:04 +0800 [thread overview]
Message-ID: <20260310031805.752-1-hdanton@sina.com> (raw)
In-Reply-To: <20260309155908.508768-1-kartikey406@gmail.com>
On Mon, 9 Mar 2026 21:29:08 +0530 Deepanshu Kartikey wrote:
> 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>
> ---
> net/atm/lec.c | 70 +++++++++++++++++++++++++++++++++------------------
> net/atm/lec.h | 2 +-
> 2 files changed, 46 insertions(+), 26 deletions(-)
>
> diff --git a/net/atm/lec.c b/net/atm/lec.c
> index fb93c6e1c329..32adf28663a0 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 +459,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? */
>
At this point priv->lecd is no longer used, so why not make lecd valid
throughout the lifespan of priv and free it after stopping dev queue,
instead of the tedious rcu trick?
> 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 be0e2667bd8c..ec85709bf818 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.43.0
next prev parent reply other threads:[~2026-03-10 3:27 UTC|newest]
Thread overview: 7+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-03-09 15:59 [PATCH v2] atm: lec: fix use-after-free in sock_def_readable() Deepanshu Kartikey
2026-03-10 3:18 ` Hillf Danton [this message]
2026-03-12 1:03 ` Deepanshu Kartikey
2026-03-14 2:53 ` Hillf Danton
2026-03-14 12:00 ` Deepanshu Kartikey
2026-03-14 12:10 ` Eric Dumazet
2026-03-14 15:40 ` patchwork-bot+netdevbpf
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260310031805.752-1-hdanton@sina.com \
--to=hdanton@sina.com \
--cc=edumazet@google.com \
--cc=kartikey406@gmail.com \
--cc=linux-kernel@vger.kernel.org \
--cc=netdev@vger.kernel.org \
--cc=syzbot+f50072212ab792c86925@syzkaller.appspotmail.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.