mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Simon Horman <horms@kernel.org>
To: zihanx@nebusec.ai
Cc: Simon Horman <horms@kernel.org>,
	netdev@vger.kernel.org, davem@davemloft.net, edumazet@google.com,
	kuba@kernel.org, pabeni@redhat.com, linux-kernel@vger.kernel.org,
	stable@vger.kernel.org, vega@nebusec.ai
Subject: Re: [PATCH net v5 1/2] llc: fix listener child socket leaks before passive open completes
Date: Wed, 26 Aug 2026 09:34:41 +0100	[thread overview]
Message-ID: <20260826083441.307129-1-horms@kernel.org> (raw)
In-Reply-To: <20260822082354.3109-2-zihanx@nebusec.ai>

This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
Full review at: https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260822082354.3109-2-zihanx@nebusec.ai?part=1
---
llc: fix listener child socket leaks before passive open completes

This restricts passive-open child creation to SABME commands, answers other
listener-directed commands with a directly generated DM response, and adds
an incoming_state lifecycle (PENDING/QUEUED/NONE) so that children which
never reach accept() are unhashed and torn down from a work item.

> diff --git a/net/llc/af_llc.c b/net/llc/af_llc.c
> index b0447c33dbf0..7d246d4d281b 100644
> --- a/net/llc/af_llc.c
> +++ b/net/llc/af_llc.c

[ ... ]

> @@ -722,6 +728,7 @@ static int llc_ui_accept(struct socket *sock, struct socket *newsock,
>  		goto frees;
>  	rc = 0;
>  	newsk = skb->sk;
> +	llc_accept_incoming_sock(newsk);
>  	/* attach connection to a new socket. */
>  	llc_ui_sk_init(newsock, newsk);
>  	sock_reset_flag(newsk, SOCK_ZAPPED);

[Severity: High]

llc_accept_incoming_sock() performs the QUEUED to LLC_INCOMING_NONE
transition, and the paired sock_put(llc->incoming_listener), with only the
listener's owner lock held (the lock_sock(sk) at the top of
llc_ui_accept()).

Every other transition site holds the child's slock instead:
bh_lock_sock()/bh_lock_sock_nested(newsk) in llc_conn_handler(),
local_bh_disable() plus bh_lock_sock_nested(newsk) in llc_backlog_rcv(),
and bh_lock_sock_nested(newsk) in llc_release_incoming_children().

Since those two lock sets are disjoint, can a softirq-side release win the
atomic_xchg while accept() sits between skb_dequeue() and the graft?

CPU0 (softirq) llc_conn_handler() or llc_backlog_rcv()
	bh_lock_sock_nested(newsk);
	llc_release_incoming_sock(newsk);  /* xchg -> NONE, work queued */

CPU1 (process) llc_ui_accept()
	newsk = skb->sk;
	llc_accept_incoming_sock(newsk);   /* xchg returns NONE, no fixup */
	llc_ui_sk_init(newsock, newsk);
	newsk->sk_state		= TCP_ESTABLISHED;

llc_accept_incoming_sock() returns void, so llc_ui_accept() has no way to
notice that it lost the transition, and it takes no reference on newsk and
never holds the child's lock.

llc_incoming_sock_work() only waits on lock_sock(listener), which accept()
drops on return, so the final sock_put() pair can land immediately after
newsock->sk has been published to userspace.

Would it work for llc_accept_incoming_sock() to return whether it won the
transition, taken under the child's lock, so accept() can fail instead of
grafting a socket whose teardown is already scheduled?

> diff --git a/net/llc/llc_conn.c b/net/llc/llc_conn.c
> index 260460d50f54..8120ecd46ace 100644
> --- a/net/llc/llc_conn.c
> +++ b/net/llc/llc_conn.c

[ ... ]

> @@ -88,6 +89,11 @@ 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
>  		 */
> +		if (sk != skb->sk &&
> +		    atomic_read(&llc_sk(skb->sk)->incoming_state) ==
> +		    LLC_INCOMING_PENDING)
> +			atomic_set(&llc_sk(skb->sk)->incoming_state,
> +				   LLC_INCOMING_QUEUED);
>  		skb_get(skb);
>  		skb_queue_tail(&sk->sk_receive_queue, skb);
>  		sk->sk_state_change(sk);

> @@ -765,27 +771,153 @@ static struct sock *llc_create_incoming_sock(struct sock *sk,

[ ... ]

> +static void llc_incoming_sock_work(struct work_struct *work)
> +{
> +	struct llc_sock *llc = container_of(work, struct llc_sock,
> +						    incoming_work);
> +	struct sock *sk = &llc->sk;
> +	struct sock *listener = llc->incoming_listener;
> +
> +	lock_sock(listener);
> +	lock_sock_nested(sk, SINGLE_DEPTH_NESTING);
> +	llc_sk_free(sk, false);
> +	sock_orphan(sk);
> +	release_sock(sk);
> +	llc_sk_stop_all_timers(sk, true);
> +	release_sock(listener);
> +	dev_put(llc->dev);
> +	llc->dev = NULL;
> +	sock_put(sk);
> +	sock_put(listener);
> +}
> +
> +void llc_release_incoming_sock(struct sock *sk)
> +{
> +	struct llc_sock *llc = llc_sk(sk);
> +
> +	if (atomic_xchg(&llc->incoming_state, LLC_INCOMING_NONE) ==
> +	    LLC_INCOMING_NONE)
> +		return;
> +
> +	WRITE_ONCE(llc->state, LLC_CONN_OUT_OF_SVC);
> +	sock_hold(sk);
> +	llc_sap_remove_socket(llc->sap, sk);
> +	schedule_work(&llc->incoming_work);
> +}

[ ... ]

> @@ -813,18 +960,46 @@ void llc_conn_handler(struct llc_sap *sap, struct sk_buff *skb)
>  		skb->sk = sk;
>  		skb->destructor = sock_efree;
>  	}
> -	if (!sock_owned_by_user(sk))
> +	if (unlikely(llc_sk(skb->sk)->state < LLC_CONN_STATE_ADM)) {
> +		if (newsk)
> +			llc_release_incoming_sock(newsk);
> +		else if (atomic_read(&llc_sk(sk)->incoming_state) !=
> +			 LLC_INCOMING_NONE)
> +			llc_release_incoming_sock(sk);
> +		goto drop_unlock;
> +	}

[Severity: Critical]

Can this free a child that is already sitting on the listener's accept
queue?

Once llc_conn_state_process() has flipped the child to
LLC_INCOMING_QUEUED, the connection indication skb is on the listener's
sk_receive_queue holding a raw skb->sk pointer to the child;
skb_set_owner_r() takes no socket reference. This gate releases the child
found directly by established lookup whenever incoming_state is not
LLC_INCOMING_NONE, which includes QUEUED children.

A purely remote frame sequence against a listening socket reaches it:

  SABME  -> llc_create_incoming_sock() publishes the child, PENDING
         -> llc_conn_state_process() LLC_CONN_PRIM: QUEUED, skb queued
  DISC   -> __llc_lookup() selects the child, and llc_common_state_trans_4
            in net/llc/llc_c_st.c moves it from NORMAL to
            LLC_CONN_STATE_ADM
  command frame -> llc_adm_state_trans_5 sets LLC_CONN_OUT_OF_SVC
  one more frame -> this gate sees state < LLC_CONN_STATE_ADM and
            incoming_state == LLC_INCOMING_QUEUED

llc_release_incoming_sock() then drops the SAP hash reference through
llc_sap_remove_socket() and schedules llc_incoming_sock_work(), which drops
the llc_sk_free() sock_put plus the explicit sock_put, so the child is
freed. Nothing dequeues or invalidates the indication skb.

Does a later accept() then work on freed memory?

net/llc/af_llc.c:llc_ui_accept() {
	newsk = skb->sk;
	llc_accept_incoming_sock(newsk);
	llc_ui_sk_init(newsock, newsk);
	sock_reset_flag(newsk, SOCK_ZAPPED);
	newsk->sk_state		= TCP_ESTABLISHED;
	...
frees:
	kfree_skb(skb);
}

The trailing kfree_skb() also runs sock_rfree() on that sock, and listener
close in llc_release_incoming_children() reads
llc_sk(newsk)->incoming_state and takes bh_lock_sock_nested(newsk) on the
same pointer.

The QUEUED branch and the retry: block of llc_backlog_rcv() call
llc_release_incoming_sock() on an already-indicated child in the same way.

Should the release paths be limited to LLC_INCOMING_PENDING children, or
should the queued indication skb be removed from the listener's receive
queue as part of the release?

> +	if (!sock_owned_by_user(sk)) {
>  		llc_conn_rcv(sk, skb);
> -	else {
> +		if (newsk &&
> +		    atomic_read(&llc_sk(newsk)->incoming_state) ==
> +		    LLC_INCOMING_PENDING)
> +			llc_release_incoming_sock(newsk);
> +	} else {

[ ... ]

> @@ -852,12 +1027,53 @@ static int llc_backlog_rcv(struct sock *sk, struct sk_buff *skb)

[ ... ]

> +			if (sock_flag(sk, SOCK_DEAD) ||
> +			    sk->sk_state != TCP_LISTEN ||
> +			    llc_sk(newsk)->state < LLC_CONN_STATE_ADM) {
> +				llc_release_incoming_sock(newsk);
> +				bh_unlock_sock(newsk);
> +				local_bh_enable();
> +				goto out_kfree_skb;
> +			}

[ ... ]

> +		} else if (newsk &&
> +			   atomic_read(&llc_sk(newsk)->incoming_state) ==
> +			   LLC_INCOMING_QUEUED) {
> +			local_bh_disable();
> +			bh_lock_sock_nested(newsk);
> +			if (llc_sk(newsk)->state <= LLC_CONN_STATE_ADM) {
> +				llc_release_incoming_sock(newsk);
> +				bh_unlock_sock(newsk);
> +				local_bh_enable();
> +				goto out_kfree_skb;
> +			}
                                            ^^

[Severity: Medium]

Is the <= bound intended here? The PENDING branch just above and the gate
in llc_conn_handler() both use state < LLC_CONN_STATE_ADM, and the v5
changelog describes the new check as an LLC_CONN_OUT_OF_SVC lower bound.

LLC_CONN_STATE_ADM (1) is a live state with its own transition table
(llc_adm_state_transitions), and a queued child legitimately returns to it
after a remote DISC:

net/llc/llc_c_st.c
static const struct llc_conn_state_trans llc_common_state_trans_4 = {
	.ev	       = llc_conn_ev_rx_disc_cmd_pbit_set_x,
	.next_state    = LLC_CONN_STATE_ADM,

So the same frame is dispatched when it arrives on the direct path in
llc_conn_handler(), but here it is dropped and the child is destroyed via
llc_release_incoming_sock(). The retry: block below applies the same
asymmetric bound with state > LLC_CONN_STATE_ADM.

Since llc_qualify_conn_ev() indexes llc_conn_state_table[llc->state - 1],
would excluding only LLC_CONN_OUT_OF_SVC (0) be enough?

[ ... ]

  reply	other threads:[~2026-08-26  8:34 UTC|newest]

Thread overview: 5+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-08-22  8:23 [PATCH net v5 0/2] " Zihan Xi
2026-08-22  8:23 ` [PATCH net v5 1/2] " Zihan Xi
2026-08-26  8:34   ` Simon Horman [this message]
2026-08-26 11:50     ` zihan xi
2026-08-22  8:23 ` [PATCH net v5 2/2] llc: reject out-of-service state before state lookup Zihan Xi

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=20260826083441.307129-1-horms@kernel.org \
    --to=horms@kernel.org \
    --cc=davem@davemloft.net \
    --cc=edumazet@google.com \
    --cc=kuba@kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=netdev@vger.kernel.org \
    --cc=pabeni@redhat.com \
    --cc=stable@vger.kernel.org \
    --cc=vega@nebusec.ai \
    --cc=zihanx@nebusec.ai \
    /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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®