Thread (3 messages) flat view 3 messages, 2 authors, 5d ago

Re: [PATCH net v3 1/1] llc: fix listener child socket leaks before passive open completes

From: Jakub Kicinski <kuba@kernel.org>
Date: 2026-08-13 00:19:20

This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
---
llc: fix listener child socket leaks before passive open completes

This adds a new llc_sock::incoming_pend flag plus an skb->cb bit so that a
passive-open child created by llc_conn_handler() can be rolled back out of
the SAP tables when the listener path exits before LLC_CONN_PRIM is emitted.
Rollback is done by a new helper, llc_release_incoming_sock(), called from
llc_conn_handler() and llc_backlog_rcv().

A few questions on the locking and on the paths the flag covers.
quoted hunk ↗ jump to hunk
diff --git a/include/net/llc_conn.h b/include/net/llc_conn.h
index e1a3026967234..ba40194899fda 100644
--- a/include/net/llc_conn.h
+++ b/include/net/llc_conn.h
@@ -72,6 +72,7 @@ struct llc_sock {
 					      received and caused sending FRMR.
 					      Used for resending FRMR */
 	u32		    cmsg_flags;
+	u8		    incoming_pend;
 	struct hlist_node   dev_hash_node;
 };
[Severity: Medium]
Which lock is meant to protect incoming_pend?

llc_create_incoming_sock() and llc_release_incoming_sock() write it under
bh_lock_sock() of the listener, but llc_conn_state_process() writes it on
whatever socket the skb belongs to:

	llc_sk(skb->sk)->incoming_pend = 0;

For a frame matched by __llc_lookup_established() that socket is the child,
and the caller holds the child's lock, not the listener's.

The readers in llc_conn_handler() and llc_backlog_rcv() hold (or own) only
the listener lock, so the set of locks common to all accesses is empty.
Is a plain u8 without READ_ONCE()/WRITE_ONCE() intended here, and is the

	if (newsk && llc_sk(newsk)->incoming_pend)
		llc_release_incoming_sock(newsk);

check-then-destroy sequence expected to be atomic with respect to the
clearing site?

[ ... ]
quoted hunk ↗ jump to hunk
diff --git a/net/llc/llc_conn.c b/net/llc/llc_conn.c
index 260460d50f54c..d2913b2164cd8 100644
--- a/net/llc/llc_conn.c
+++ b/net/llc/llc_conn.c
[ ... ]
quoted hunk ↗ jump to hunk
@@ -88,6 +89,7 @@ int llc_conn_state_process(struct sock *sk, struct sk_buff *skb)
 		 * skb->sk pointing to the newly created struct sock in
 		 * llc_conn_handler. -acme
 		 */
+		llc_sk(skb->sk)->incoming_pend = 0;
 		skb_get(skb);
 		skb_queue_tail(&sk->sk_receive_queue, skb);
 		sk->sk_state_change(sk);
[Severity: High]
Can this clear the flag on a child that no accept() will ever see, and so
defeat every rollback path the patch adds?

The child is hash-published by llc_create_incoming_sock() before the
handshake finishes, and __llc_lookup() prefers the established socket:

net/llc/llc_conn.c:__llc_lookup() {
	struct sock *sk = __llc_lookup_established(sap, daddr, laddr, net);

	return sk ? : llc_lookup_listener(sap, laddr, net);
}

So a second frame for the same address pair is delivered with sk == the
child. The child's sk_state is TCP_CLOSE, so llc_conn_handler() takes the
else branch, newsk stays NULL, and no rollback check runs for that call.
The child is in LLC_CONN_STATE_ADM, so a SABME hits llc_adm_state_trans_2,
whose action list contains llc_conn_ac_conn_ind, giving ind_prim ==
LLC_CONN_PRIM with sk == skb->sk == child:

	llc_sk(skb->sk)->incoming_pend = 0;
	skb_get(skb);
	skb_queue_tail(&sk->sk_receive_queue, skb);

The indication is queued on the child's own receive queue, not on a
listener's, yet incoming_pend is now 0.

When the listener's backlog is drained afterwards, a listener's llc->state
is LLC_CONN_STATE_ADM == 1 (llc_ui_listen() never changes llc->state), so
llc_backlog_rcv() takes out_kfree_skb, and the new rollback there is skipped
because incoming_pend is already 0. The child then stays in
sap->sk_laddr_hash / sk_dev_hash with the dev_hold() taken by
llc_create_incoming_sock(), keeps answering the peer, and is unreachable
from accept().

Would restricting the clear to the case where the indication really goes to
a different (listening) socket, e.g. only when sk != skb->sk, close this?

[Severity: High]
This isn't a bug introduced by this patch, but the flag is cleared the
instant LLC_CONN_PRIM is emitted, which is before accept() has taken
ownership of the child, and from that point llc_release_incoming_sock()
returns immediately:

	if (!llc->incoming_pend)
		return;

If userspace never calls accept() and just closes the listener,
llc_ui_release() unhashes and frees only the listener. The child is reached
only through the queued skb, and llc_sk_free() does:

	skb_queue_purge(&sk->sk_receive_queue);

which runs each skb's sock_rfree destructor (installed by skb_set_owner_r()
in llc_conn_handler()). That un-charges rmem, but never does sock_put() on
skb->sk, never calls llc_sap_remove_socket(), and never drops the dev_hold()
from llc_create_incoming_sock(). There is no netdevice notifier or other
sweeper in net/llc/, so the child stays hashed with a permanent netdev
reference.

Given the subject "llc: fix listener child socket leaks before passive open
completes" and Cc: stable, could the commit message state that the
listen/SABME/close-without-accept case is still left as is?

[ ... ]
quoted hunk ↗ jump to hunk
@@ -765,16 +767,32 @@ static struct sock *llc_create_incoming_sock(struct sock *sk,
 	memcpy(&newllc->laddr, daddr, sizeof(newllc->laddr));
 	memcpy(&newllc->daddr, saddr, sizeof(newllc->daddr));
 	newllc->dev = dev;
+	newllc->incoming_pend = 1;
 	dev_hold(dev);
 	llc_sap_add_socket(llc->sap, newsk);
 out:
 	return newsk;
 }
 
+static void llc_release_incoming_sock(struct sock *sk)
+{
+	struct llc_sock *llc = llc_sk(sk);
+
+	if (!llc->incoming_pend)
+		return;
+
+	llc->incoming_pend = 0;
+	llc_sap_remove_socket(llc->sap, sk);
+	dev_put(llc->dev);
+	sock_orphan(sk);
+	llc_sk_free(sk);
+}
[Severity: High]
Is any lock held on the socket being destroyed here?

The callers hold bh_lock_sock() of the listener, but the child was already
made globally visible by llc_sap_add_socket():

net/llc/llc_conn.c:llc_sap_add_socket() {
	sock_set_flag(sk, SOCK_RCU_FREE);
	sap->sk_count++;
	sk_nulls_add_node_rcu(sk, laddr_hb);
	hlist_add_head(&llc->dev_hash_node, dev_hb);
}

so another CPU can find it in __llc_lookup_established():

		if (unlikely(!refcount_inc_not_zero(&rc->sk_refcnt)))
			goto again;

and then run llc_conn_rcv(child, skb) under bh_lock_sock(child), which is a
lock this path does not take:

CPU0 (softirq, frame A, listener branch)
llc_conn_handler()
  bh_lock_sock(listener)
  llc_create_incoming_sock()      /* child published */
  llc_conn_rcv(listener, skbA)    /* no LLC_CONN_PRIM */
  llc_release_incoming_sock(child)
    llc_sap_remove_socket()
    dev_put(llc->dev)
    sock_orphan(child)
    llc_sk_free(child)            /* stops timers, purges queues, sock_put */

CPU1 (softirq, frame B, same address pair)
llc_conn_handler()
  __llc_lookup_established() -> child
  bh_lock_sock(child)
  llc_conn_rcv(child, skbB)       /* mod_timer(), tx using llc->dev, ... */

Can CPU1's state-machine actions re-arm the child's timers with mod_timer()
after CPU0's llc_sk_stop_all_timers(child, true) has already returned? The
timers hold no socket reference, so once CPU1 drops its lookup reference the
sock is freed with a timer still armed.

Similarly, dev_put(llc->dev) does not clear llc->dev, so CPU1's transmit
actions keep using it after the reference was dropped, and sock_orphan()
clears sk_socket while llc_conn_state_process() dereferences
sk->sk_socket->state.

Compare llc_ui_release(), which holds lock_sock() of the socket it destroys
across llc_sap_remove_socket()/netdev_put()/llc_sk_free().

[Severity: High]
Is llc_sk_free() safe to call from here?

llc_sk_free() hardcodes the synchronous variant:

net/llc/llc_conn.c:llc_sk_free() {
	llc->state = LLC_CONN_OUT_OF_SVC;
	llc_sk_stop_all_timers(sk, true);

which expands to four timer_delete_sync() calls. Both new call sites run in
NET_RX softirq with the listener's bh_lock_sock(sk) (sk->sk_lock.slock)
held, and the LLC timers are set up with flags == 0 in llc_sk_init(), so
they are not TIMER_IRQSAFE. timer_delete_sync()'s comment says the caller
must not hold locks that are held in interrupt context, even unrelated ones.

sk_lock.slock is exactly such a lock, taken from softirq by the timer
callbacks themselves:

net/llc/llc_c_ac.c:llc_conn_tmr_common_cb() {
	bh_lock_sock(sk);

__timer_delete_sync() does lock_map_acquire(&timer->lockdep_map)
unconditionally, before checking whether the timer is pending, so on
CONFIG_PROVE_LOCKING this records slock-AF_LLC -> timer lockdep_map, while
call_timer_fn() plus llc_conn_tmr_common_cb() records the reverse edge.

The existing BH-context helper uses the async variant:

net/llc/llc_c_ac.c:llc_conn_ac_stop_all_timers() {
	llc_sk_stop_all_timers(sk, false);

Before this patch llc_sk_free() had a single caller, llc_ui_release(), in
process context. Should the rollback path use the sync == false form, or
defer the free?

[ ... ]
quoted hunk ↗ jump to hunk
@@ -812,14 +830,22 @@ void llc_conn_handler(struct llc_sap *sap, struct sk_buff *skb)
 		sock_hold(sk);
 		skb->sk = sk;
 		skb->destructor = sock_efree;
+		llc_set_incoming_flag(skb, false);
 	}
-	if (!sock_owned_by_user(sk))
+	if (!sock_owned_by_user(sk)) {
 		llc_conn_rcv(sk, skb);
-	else {
+		if (newsk && llc_sk(newsk)->incoming_pend)
+			llc_release_incoming_sock(newsk);
+	} else {
 		dprintk("%s: adding to backlog...\n", __func__);
 		llc_set_backlog_type(skb, LLC_PACKET);
-		if (sk_add_backlog(sk, skb, READ_ONCE(sk->sk_rcvbuf)))
+		if (sk_add_backlog(sk, skb, READ_ONCE(sk->sk_rcvbuf))) {
+			if (newsk) {
+				skb_orphan(skb);
+				llc_release_incoming_sock(newsk);
+			}
 			goto drop_unlock;
+		}
 	}
 out:
 	bh_unlock_sock(sk);
[ ... ]
quoted hunk ↗ jump to hunk
@@ -868,10 +895,14 @@ static int llc_backlog_rcv(struct sock *sk, struct sk_buff *skb)
 		printk(KERN_ERR "%s: invalid skb in backlog\n", __func__);
 		goto out_kfree_skb;
 	}
+	if (newsk && llc_sk(newsk)->incoming_pend)
+		llc_release_incoming_sock(newsk);
 out:
 	return rc;
 out_kfree_skb:
 	kfree_skb(skb);
+	if (newsk && llc_sk(newsk)->incoming_pend)
+		llc_release_incoming_sock(newsk);
 	goto out;
 }
-- 
pw-bot: cr
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help