All of lore.kernel.org
 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, linux-afs@lists.infradead.org,
	dhowells@redhat.com, marc.dionne@auristor.com,
	davem@davemloft.net, edumazet@google.com, kuba@kernel.org,
	pabeni@redhat.com, linux-kernel@vger.kernel.org,
	stable@vger.kernel.org, vega@nebusec.ai, root@tr0jan.top
Subject: Re: [PATCH net v4 1/1] rxrpc: fix encap_rcv skb accounting exhaustion
Date: Sat, 12 Sep 2026 11:26:59 +0100	[thread overview]
Message-ID: <20260912102659.174546-1-horms@kernel.org> (raw)
In-Reply-To: <ca1d010bc963319d4167b8e985b5afac5912f6da.1788878590.git.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/ca1d010bc963319d4167b8e985b5afac5912f6da.1788878590.git.zihanx@nebusec.ai?part=1
---
rxrpc: fix encap_rcv skb accounting exhaustion

This charges encapsulated packets against the UDP tunnel socket's receive
buffer in rxrpc_encap_rcv() and drops them once sk_rcvbuf is reached, then
orphans PACKET skbs when the I/O thread dequeues them.  It also moves the
sk_user_data clearing under RCU and defers sock_release() until after the
local queues are purged.

Link: https://lore.kernel.org/all/cover.1788539302.git.zihanx@nebusec.ai/
Link: https://lore.kernel.org/all/cover.1785339953.git.zihanx@nebusec.ai/
Link: https://lore.kernel.org/all/cover.1784742007.git.zihanx@nebusec.ai/

> diff --git a/net/rxrpc/io_thread.c b/net/rxrpc/io_thread.c
> index dc5184a2fa9d..415b05f5e2b4 100644
> --- a/net/rxrpc/io_thread.c
> +++ b/net/rxrpc/io_thread.c
> @@ -13,6 +13,22 @@ static int rxrpc_input_packet_on_conn(struct rxrpc_connection *conn,
>  				      struct sockaddr_rxrpc *peer_srx,
>  				      struct sk_buff *skb);
>  
> +/*
> + * Drop UDP rmem ownership for packets charged in encap_rcv().
> + * sk_forward_alloc is serialised by sk_receive_queue.lock.
> + */
> +static void rxrpc_skb_orphan_udp(struct sk_buff *skb)
> +{
> +	struct sock *sk = skb->sk;
> +
> +	if (!sk)
> +		return;
> +
> +	spin_lock_bh(&sk->sk_receive_queue.lock);
> +	skb_orphan(skb);
> +	spin_unlock_bh(&sk->sk_receive_queue.lock);
> +}
> +
>  /*
>   * handle data received on the local endpoint
>   * - may be called in interrupt context

[ ... ]

> @@ -52,6 +66,22 @@ int rxrpc_encap_rcv(struct sock *udp_sk, struct sk_buff *skb)
>  	}
>  #endif
>  
> +	spin_lock(&udp_sk->sk_receive_queue.lock);
> +	if (atomic_read(&udp_sk->sk_rmem_alloc) >= READ_ONCE(udp_sk->sk_rcvbuf) ||
> +	    !sk_rmem_schedule(udp_sk, skb, skb->truesize)) {

[Severity: High]

Which sk_rcvbuf value does this end up comparing against?  Nothing in
net/rxrpc/ sizes the tunnel socket's receive buffer.  rxrpc_open_socket()
calls udp_sock_create() and setup_udp_tunnel_sock(), neither of which touches
it, so it stays at what sock_init_data() assigned:

net/core/sock.c:sock_init_data() {
	...
	sk->sk_rcvbuf		=	READ_ONCE(sysctl_rmem_default);
	...
}

That is roughly 208 KiB of truesize for the entire local endpoint, and since
this is a kernel socket with no fd, the only way to change it is the global
net.core.rmem_default.

Meanwhile rxrpc keeps advertising a 255 packet receive window per call:

net/rxrpc/misc.c:
unsigned int rxrpc_rx_window_size = 255;

net/rxrpc/conn_event.c:rxrpc_conn_retransmit_call() {
	...
	trailer.rwind		= htonl(rxrpc_rx_window_size);
	...
}

with rxrpc_rx_mtu at RXRPC_JUMBO(46).  Both are sysctl-tunable; the new cap
is not.

The test is a hard >= with no headroom, and the charge is only released one
skb at a time when the MIN_NICE krxrpcio thread dequeues.  At 2-4 KiB truesize
per MTU-sized skb, 50-90 queued skbs exhaust the budget, which a single
softirq round can deliver before the I/O thread runs.

Can a compliant AFS peer that simply fills the window rxrpc advertised to it
get dropped here, with no EXCEEDS_WINDOW ACK and recovery only via RTO?  The
resulting threshold, how it was derived, and the disagreement with the
advertised window aren't mentioned in the commit message or the v2/v3/v4
changelog.

> +		spin_unlock(&udp_sk->sk_receive_queue.lock);
> +		sk_drops_inc(udp_sk);
> +		kfree_skb(skb);
> +		return 0;
> +	}

[Severity: Low]

The drop returns 0, which UDP counts as a delivered datagram:

net/ipv4/udp.c:udp_queue_rcv_one_skb() {
	...
		ret = encap_rcv(sk, skb);
		if (ret <= 0) {
			__UDP_INC_STATS(net, UDP_MIB_INDATAGRAMS);
			return -ret;
		}
	...
}

So each dropped datagram bumps UDP_MIB_INDATAGRAMS and neither
UDP_MIB_RCVBUFERRORS nor UDP_MIB_INERRORS is incremented, unlike UDP's own
rcvbuf-overflow path for the same socket type
(__udp_enqueue_schedule_skb() -> -ENOMEM -> RCVBUFERRORS + INERRORS).
Under the flood this patch bounds, netstat -su shows InDatagrams climbing and
zero receive-buffer errors while sk_drops rises.

The hook return value can't express this (a negative return makes
udp_queue_rcv_one_skb() resubmit to the protocol and still counts
INDATAGRAMS), so would an explicit __UDP_INC_STATS(net, UDP_MIB_RCVBUFERRORS)
plus UDP_MIB_INERRORS here be appropriate, along with a specific
skb_drop_reason instead of the bare kfree_skb()?

> +
> +	skb->dev = NULL;
> +	skb_set_owner_r(skb, udp_sk);

[Severity: Medium]

This consumes the same counter and the same limit that the socket error queue
is gated on:

net/core/skbuff.c:sock_queue_err_skb() {
	if (atomic_read(&sk->sk_rmem_alloc) + skb->truesize >=
	    (unsigned int)READ_ONCE(sk->sk_rcvbuf))
		return -ENOMEM;
	...
}

rxrpc's only ICMP ingress is rxrpc_encap_err_rcv() ->
ip_icmp_error()/ipv6_icmp_error() -> sock_queue_err_skb().  On -ENOMEM the
notification is kfree_skb()'d, sk_error_report() is never called, so
rxrpc_error_report() never queues an RXRPC_SKB_MARK_ERROR skb and
rxrpc_input_error() never runs.

Does that silently drop port-unreachable (peer-dead) detection and
ICMP_FRAG_NEEDED path-MTU input for the whole endpoint for as long as the rx
queue is near full, i.e. exactly the state this patch holds during a flood?

Before this change the encap path never touched sk_rmem_alloc, so the budget
was effectively reserved for error skbs.  Error skbs also stay charged after
rxrpc_error_report() moves them onto local->rx_queue, since the patch
deliberately does not orphan them, which widens the window further.

> +	spin_unlock(&udp_sk->sk_receive_queue.lock);
> +	skb_dst_force(skb);

[Severity: Low]

What consumes the forced dst reference, and why is skb->dev cleared above?

A git grep for skb_dst and skb->dev in net/rxrpc/ matches only these two new
lines, and the only dst dereference in rxrpc (dst->dev->gso_max_segs in
peer_object.c) works on an output route lookup.

Before this change the queued skb carried a SKB_DST_NOREF dst, which
refdst_drop() skips on free; skb_dst_force() promotes it to a refcounted dst
held until the skb is finally freed.  UDP's own queueing path drops the dst
instead:

net/ipv4/udp.c:udp_queue_rcv_one_skb() {
	...
	ipv4_pktinfo_prepare(sk, skb, true);
	return __udp_queue_rcv_skb(sk, skb);
}

Would skb_dst_drop() be what is wanted here?  rxrpc_skb_orphan_udp() releases
the rmem charge at dequeue but leaves the dst in place, so DATA packets handed
to a call's recvmsg queue keep the reference for a
remote-sender-plus-slow-reader controlled interval that is not covered by the
new sk_rcvbuf cap, because dst size is not part of skb->truesize.

Neither skb->dev = NULL nor skb_dst_force() is described in the commit message
or the v2/v3/v4 changelog.

> +
> +	skb->mark = RXRPC_SKB_MARK_PACKET;
> +	rxrpc_new_skb(skb, rxrpc_skb_new_encap_rcv);
>  	skb_queue_tail(rx_queue, skb);
>  	wake_up_process(io_thread);
>  	return 0;
> @@ -471,6 +501,9 @@ int rxrpc_io_thread(void *data)
>  		/* Distribute packets and errors. */
>  		while ((skb = __skb_dequeue(&rx_queue))) {
>  			struct rxrpc_skb_priv *sp = rxrpc_skb(skb);
> +
> +			if (skb->mark == RXRPC_SKB_MARK_PACKET)
> +				rxrpc_skb_orphan_udp(skb);
>  			switch (skb->mark) {
>  			case RXRPC_SKB_MARK_PACKET:
>  				skb->priority = 0;

[ ... ]

  reply	other threads:[~2026-09-12 10:28 UTC|newest]

Thread overview: 4+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-09  7:44 [PATCH net v4 0/1] rxrpc: fix encap_rcv skb accounting exhaustion Zihan Xi
2026-09-09  7:44 ` [PATCH net v4 1/1] " Zihan Xi
2026-09-12 10:26   ` Simon Horman [this message]
2026-09-13  7:27     ` 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=20260912102659.174546-1-horms@kernel.org \
    --to=horms@kernel.org \
    --cc=davem@davemloft.net \
    --cc=dhowells@redhat.com \
    --cc=edumazet@google.com \
    --cc=kuba@kernel.org \
    --cc=linux-afs@lists.infradead.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=marc.dionne@auristor.com \
    --cc=netdev@vger.kernel.org \
    --cc=pabeni@redhat.com \
    --cc=root@tr0jan.top \
    --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 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.