Netdev List
 help / color / mirror / Atom feed
* Please backport: DSA taggers OOB read on PACKET_QDISC_BYPASS TX
From: Doruk Tan Ozturk @ 2026-07-14 16:46 UTC (permalink / raw)
  To: stable; +Cc: olteanv, andrew, f.fainelli, davem, edumazet, kuba, pabeni,
	netdev

Please backport the following mainline commits to the stable trees.

Reason: the ocelot, ksz and sja1105 DSA taggers dereference
eth_hdr(skb)/skb_mac_header(skb) on their TX paths. skb->mac_header is
not set on the AF_PACKET SOCK_RAW + PACKET_QDISC_BYPASS transmit path
(packet_direct_xmit() -> netdev_start_xmit(), which bypasses the
dev_hard_start_xmit() reset from 6d1ccff62780), so eth_hdr(skb) resolves
~64 KB out of bounds -> out-of-bounds read. The fixes below make these
taggers read the header from skb->data instead. Reproducible with an
unmodified CONFIG_NET_DSA_LOOP=y kernel by sending on a raw packet
socket with PACKET_QDISC_BYPASS set.

These commits went into v6.4 without a Cc: stable tag because they were
made as preparation for reverting 6d1ccff62780 and the bug was assumed
to be future-only; it is not -- the bypass path was always unaffected by
that reset. 6.6.y and 6.12.y already carry them.

Prerequisite (helper, not a fix on its own; needed or the ocelot and
sja1105 fixes will not build on pre-v6.4 trees):

  1f5020acb33f ("net: vlan: introduce skb_vlan_eth_hdr()")

Fixes, in mainline order:

  eabb1494c9f2 ("net: dsa: tag_ocelot: do not rely on skb_mac_header() for VLAN xmit")
  499b2491d550 ("net: dsa: tag_ksz: do not rely on skb_mac_header() in TX paths")
  f9346f00b5af ("net: dsa: tag_sja1105: don't rely on skb_mac_header() in TX paths")
  0bcf2e4aca6c ("net: dsa: tag_ocelot: call only the relevant portion of __skb_vlan_pop() on TX")

Not all fixes apply to all trees (the vulnerable code was introduced at
different times). Per tree:

  6.1.y:  1f5020acb33f, eabb1494c9f2, 499b2491d550, f9346f00b5af, 0bcf2e4aca6c
  5.15.y: 1f5020acb33f, 499b2491d550, f9346f00b5af
          (tag_ocelot has no ocelot_xmit_get_vlan_info() before v5.16)
  5.10.y: 499b2491d550
          (sja1105_pvid_tag_control_pkt() is v5.15+; ocelot is v5.16+;
           skb_eth_hdr() already present, so no prerequisite needed)

Ordering: apply 1f5020acb33f before eabb1494c9f2/f9346f00b5af, and
eabb1494c9f2 before 0bcf2e4aca6c.

5.4.y is EOL and also lacks skb_eth_hdr(); not requested.

Thanks,
Doruk Ozturk

^ permalink raw reply

* Re: [PATCH net v3] tun/tap & vhost-net: make qdisc backpressure opt-in via IFF_BACKPRESSURE
From: Simon Schippers @ 2026-07-14 16:50 UTC (permalink / raw)
  To: Michael S. Tsirkin
  Cc: Willem de Bruijn, Jason Wang, David S . Miller, Eric Dumazet,
	Jakub Kicinski, Paolo Abeni, netdev, Simon Horman,
	Jonathan Corbet, Shuah Khan, Andrew Lunn, Tim Gebauer,
	Brett Sheffield, linux-doc, linux-kernel
In-Reply-To: <20260714092506-mutt-send-email-mst@kernel.org>

On 7/14/26 15:40, Michael S. Tsirkin wrote:
> On Thu, Jul 09, 2026 at 11:55:11AM +0200, Simon Schippers wrote:
>> Commit 1d6e569b7d0c ("tun/tap & vhost-net: avoid ptr_ring tail-drop
>> when a qdisc is present") did not show a relevant performance regression
>> in my testing but on Brett Sheffield's librecast testbed it shows a
>> significant performance drop in a IPv6 multicast testcase. The regression
>> can be pinpointed when multiple iperf3 TCP threads are sending. For 8
>> threads the performance dropped from 13.5 Gbit/s to 9.13 Gbit/s. This is
>> the reason why this patch makes the qdisc backpressure behavior opt-in.
>>
>> One option to accomplish the opt-in would be to set the default qdisc to
>> noqueue at init. However this may also break userspace as users might
>> have chosen a custom qdisc even though most of the qdiscs did nothing
>> for tun/tap in the past due to missing backpressure...
>>
>> This is the reason why in this patch, the flag IFF_BACKPRESSURE is
>> introduced instead which is required to enable the backpressure logic.
>> This means the stopping logic in tun_net_xmit() and the waking logic in
>> __tun_wake_queue() are skipped if the flag is disabled. Setting
>> IFF_BACKPRESSURE makes an attached qdisc effective by stopping the queue
>> instead of tail-dropping when the internal ring is full.
>>
>> To avoid a possible stall due to disabling IFF_BACKPRESSURE, the new
>> helper tun_force_wake_queue() is implemented. The helper safely wakes the
>> respective netdev queue and resets cons_cnt while the consumer_lock and
>> the producer_lock of the ring are held. The helper is run in tun_attach()
>> when a queue (re)attaches, in tun_set_iff() for attached tfiles, and
>> in tun_queue_resize().
>>
>> The documentation in tuntap.rst is updated accordingly.
>>
>> Fixes: 1d6e569b7d0c ("tun/tap & vhost-net: avoid ptr_ring tail-drop when a qdisc is present")
>> Reported-by: Brett Sheffield <brett@librecast.net>
>> Closes: https://lore.kernel.org/netdev/akVnoOYQOrt8k-Gu@karahi.librecast.net/T/#u
>> Signed-off-by: Simon Schippers <simon.schippers@tu-dortmund.de>
> 
> 
> The patch itself is fine:
> 
> Acked-by: Michael S. Tsirkin <mst@redhat.com>
> 
> What bothers me is the timing: we
> are past freeze and this new interface hasn't been tested
> much.
> 
> We can either apply this, or revert the original patch for now
> and reapply with this for next.

I prefer applying it, but you maintainers must decide that.

> 
>> ---
>> V2 -> V3:
>> - As suggested by MST: Clarify in tuntap.rst and the UAPI header what
>>   enabling IFF_BACKPRESSURE opts into: an attached qdisc becomes effective
>>   instead of the driver tail-dropping when the internal ring is full.
>> - Avoid lines over 75 characters.
>> - Update comment in tun_net_xmit() to include IFF_BACKPRESSURE.
>> - Brett: Update in commit message that the referenced tests were TCP.
>>
>> V1 -> V2:
>> - Sashiko: Ensure detached queues are woken on re-attach by calling the
>>   new tun_force_wake_queue() helper from tun_attach(), and reuse it
>>   across the existing wake paths.
>> - Specify the failing test case in the commit message.
>>
>> V1: https://lore.kernel.org/netdev/20260704112058.95421-1-simon.schippers@tu-dortmund.de/T/#u
>> V2: https://lore.kernel.org/netdev/20260706094242.115992-1-simon.schippers@tu-dortmund.de/T/#u
>>
>>  Documentation/networking/tuntap.rst | 22 +++++++++++++
>>  drivers/net/tun.c                   | 51 ++++++++++++++++++++---------
>>  include/uapi/linux/if_tun.h         |  4 +++
>>  tools/include/uapi/linux/if_tun.h   |  1 +
>>  4 files changed, 62 insertions(+), 16 deletions(-)
>>
>> diff --git a/Documentation/networking/tuntap.rst b/Documentation/networking/tuntap.rst
>> index 4d7087f727be..5921a924c2ae 100644
>> --- a/Documentation/networking/tuntap.rst
>> +++ b/Documentation/networking/tuntap.rst
>> @@ -206,6 +206,28 @@ enable is true we enable it, otherwise we disable it::
>>        return ioctl(fd, TUNSETQUEUE, (void *)&ifr);
>>    }
>>  
>> +3.4 qdisc backpressure
>> +----------------------
>> +
>> +Starting with Linux 7.2, IFF_BACKPRESSURE can be set to enable qdisc
>> +backpressure. Without it, TX drops occur when the internal ring buffer
>> +is full, so any attached qdisc is effectively bypassed and applications
>> +only learn about congestion through those drops.
>> +
>> +With it, the kernel stops instead, letting the qdisc hold and schedule
>> +packets, so its AQM, shaping and fairness actually apply. This helps
>> +protocols like TCP, which cut throughput in reaction to packet drops.
>> +With IFF_BACKPRESSURE, drops then only occur as a rare race. Backpressure
>> +requires a qdisc to be attached and has no effect with noqueue.
>> +
>> +The txqueuelen can be reduced alongside this flag to further shift
>> +buffering into the qdisc and reduce bufferbloat, but comes at possible
>> +performance cost.
>> +
>> +When running multiple network streams in parallel through a single
>> +TUN/TAP queue, the flag may reduce performance due to the extra overhead
>> +of the backpressure mechanism.
>> +
>>  Universal TUN/TAP device driver Frequently Asked Question
>>  =========================================================
>>  
>> diff --git a/drivers/net/tun.c b/drivers/net/tun.c
>> index ffbe6f13fb1f..5941e8f302ea 100644
>> --- a/drivers/net/tun.c
>> +++ b/drivers/net/tun.c
>> @@ -98,7 +98,8 @@ static void tun_default_link_ksettings(struct net_device *dev,
>>  #define TUN_FASYNC	IFF_ATTACH_QUEUE
>>  
>>  #define TUN_FEATURES (IFF_NO_PI | IFF_ONE_QUEUE | IFF_VNET_HDR | \
>> -		      IFF_MULTI_QUEUE | IFF_NAPI | IFF_NAPI_FRAGS)
>> +		      IFF_MULTI_QUEUE | IFF_NAPI | IFF_NAPI_FRAGS | \
>> +		      IFF_BACKPRESSURE)
>>  
>>  #define GOODCOPY_LEN 128
>>  
>> @@ -694,6 +695,20 @@ static void tun_detach_all(struct net_device *dev)
>>  		module_put(THIS_MODULE);
>>  }
>>  
>> +static void tun_force_wake_queue(struct tun_struct *tun,
>> +				 struct tun_file *tfile)
>> +{
>> +	/* Ensure that the producer can not stop the
>> +	 * queue concurrently by taking locks.
>> +	 */
>> +	spin_lock_bh(&tfile->tx_ring.consumer_lock);
>> +	spin_lock(&tfile->tx_ring.producer_lock);
>> +	netif_wake_subqueue(tun->dev, tfile->queue_index);
>> +	tfile->cons_cnt = 0;
>> +	spin_unlock(&tfile->tx_ring.producer_lock);
>> +	spin_unlock_bh(&tfile->tx_ring.consumer_lock);
>> +}
>> +
>>  static int tun_attach(struct tun_struct *tun, struct file *file,
>>  		      bool skip_filter, bool napi, bool napi_frags,
>>  		      bool publish_tun)
>> @@ -737,11 +752,9 @@ static int tun_attach(struct tun_struct *tun, struct file *file,
>>  		goto out;
>>  	}
>>  
>> -	spin_lock(&tfile->tx_ring.consumer_lock);
>> -	tfile->cons_cnt = 0;
>> -	spin_unlock(&tfile->tx_ring.consumer_lock);
>>  	tfile->queue_index = tun->numqueues;
>>  	tfile->socket.sk->sk_shutdown &= ~RCV_SHUTDOWN;
>> +	tun_force_wake_queue(tun, tfile);
>>  
>>  	if (tfile->detached) {
>>  		/* Re-attach detached tfile, updating XDP queue_index */
>> @@ -1077,7 +1090,8 @@ static netdev_tx_t tun_net_xmit(struct sk_buff *skb, struct net_device *dev)
>>  
>>  	spin_lock(&tfile->tx_ring.producer_lock);
>>  	ret = __ptr_ring_produce(&tfile->tx_ring, skb);
>> -	if (!qdisc_txq_has_no_queue(queue) &&
>> +	if ((tun->flags & IFF_BACKPRESSURE) &&
>> +	    !qdisc_txq_has_no_queue(queue) &&
>>  	    __ptr_ring_check_produce(&tfile->tx_ring) == -ENOSPC) {
>>  		netif_tx_stop_queue(queue);
>>  		/* Paired with smp_mb() in __tun_wake_queue() */
>> @@ -1088,8 +1102,8 @@ static netdev_tx_t tun_net_xmit(struct sk_buff *skb, struct net_device *dev)
>>  	spin_unlock(&tfile->tx_ring.producer_lock);
>>  
>>  	if (ret) {
>> -		/* This should be a rare case if a qdisc is present, but
>> -		 * can happen due to lltx.
>> +		/* This should be a rare case if IFF_BACKPRESSURE is enabled and
>> +		 * a qdisc is present, but can happen due to lltx.
>>  		 * Since skb_tx_timestamp(), skb_orphan(),
>>  		 * run_ebpf_filter() and pskb_trim() could have tinkered
>>  		 * with the SKB, returning NETDEV_TX_BUSY is unsafe and
>> @@ -2151,8 +2165,12 @@ static ssize_t tun_put_user(struct tun_struct *tun,
>>  static void __tun_wake_queue(struct tun_struct *tun,
>>  			     struct tun_file *tfile, int consumed)
>>  {
>> -	struct netdev_queue *txq = netdev_get_tx_queue(tun->dev,
>> -						tfile->queue_index);
>> +	struct netdev_queue *txq;
>> +
>> +	if (!(tun->flags & IFF_BACKPRESSURE))
>> +		return;
>> +
>> +	txq = netdev_get_tx_queue(tun->dev, tfile->queue_index);
>>  
>>  	/* Paired with smp_mb__after_atomic() in tun_net_xmit() */
>>  	smp_mb();
>> @@ -2764,7 +2782,7 @@ static int tun_set_iff(struct net *net, struct file *file, struct ifreq *ifr)
>>  	struct tun_struct *tun;
>>  	struct tun_file *tfile = file->private_data;
>>  	struct net_device *dev;
>> -	int err;
>> +	int err, i;
>>  
>>  	if (tfile->detached)
>>  		return -EINVAL;
>> @@ -2893,8 +2911,12 @@ static int tun_set_iff(struct net *net, struct file *file, struct ifreq *ifr)
>>  	/* Make sure persistent devices do not get stuck in
>>  	 * xoff state.
>>  	 */
>> -	if (netif_running(tun->dev))
>> -		netif_tx_wake_all_queues(tun->dev);
>> +	if (netif_running(tun->dev)) {
>> +		for (i = 0; i < tun->numqueues; i++) {
>> +			tfile = rtnl_dereference(tun->tfiles[i]);
>> +			tun_force_wake_queue(tun, tfile);
>> +		}
>> +	}
>>  
>>  	strscpy(ifr->ifr_name, tun->dev->name);
>>  	return 0;
>> @@ -3693,10 +3715,7 @@ static int tun_queue_resize(struct tun_struct *tun)
>>  	if (!ret) {
>>  		for (i = 0; i < tun->numqueues; i++) {
>>  			tfile = rtnl_dereference(tun->tfiles[i]);
>> -			spin_lock(&tfile->tx_ring.consumer_lock);
>> -			netif_wake_subqueue(tun->dev, tfile->queue_index);
>> -			tfile->cons_cnt = 0;
>> -			spin_unlock(&tfile->tx_ring.consumer_lock);
>> +			tun_force_wake_queue(tun, tfile);
>>  		}
>>  	}
>>  
>> diff --git a/include/uapi/linux/if_tun.h b/include/uapi/linux/if_tun.h
>> index 79d53c7a1ebd..a0ddc50a7534 100644
>> --- a/include/uapi/linux/if_tun.h
>> +++ b/include/uapi/linux/if_tun.h
>> @@ -69,6 +69,10 @@
>>  #define IFF_NAPI_FRAGS	0x0020
>>  /* Used in TUNSETIFF to bring up tun/tap without carrier */
>>  #define IFF_NO_CARRIER	0x0040
>> +/* Stop the queue instead of dropping when the internal ring is full, so an
>> + * attached qdisc applies backpressure instead of being bypassed.
>> + */
>> +#define IFF_BACKPRESSURE	0x0080
>>  #define IFF_NO_PI	0x1000
>>  /* This flag has no real effect */
>>  #define IFF_ONE_QUEUE	0x2000
>> diff --git a/tools/include/uapi/linux/if_tun.h b/tools/include/uapi/linux/if_tun.h
>> index 2ec07de1d73b..97b670f5bc0a 100644
>> --- a/tools/include/uapi/linux/if_tun.h
>> +++ b/tools/include/uapi/linux/if_tun.h
>> @@ -67,6 +67,7 @@
>>  #define IFF_TAP		0x0002
>>  #define IFF_NAPI	0x0010
>>  #define IFF_NAPI_FRAGS	0x0020
>> +#define IFF_BACKPRESSURE	0x0080
>>  #define IFF_NO_PI	0x1000
>>  /* This flag has no real effect */
>>  #define IFF_ONE_QUEUE	0x2000
>> -- 
>> 2.43.0
> 

^ permalink raw reply

* Re: [PATCH net] tipc: cap number of nodes per net namespace
From: Ibrahim Hashimov @ 2026-07-14 17:03 UTC (permalink / raw)
  To: tung.quang.nguyen, jmaloy, davem, edumazet, kuba, pabeni
  Cc: horms, netdev, tipc-discussion, linux-kernel
In-Reply-To: <GV1P189MB19881AB5829FF8B0B00EF158C6FA2@GV1P189MB1988.EURP189.PROD.OUTLOOK.COM>

> I do not see any issue with current code that requires this patch.

tipc_node_create() allocates a struct tipc_node (plus a broadcast rcv
link, a unicast link and a keepalive timer) for every (addr, node_id)
it hasn't seen before, and both come straight off the discovery frame
(msg_prevnode / msg_node_id). There's no cap and no rate limit on that
path, and a link-less spoofed node isn't reclaimed until
NODE_CLEANUP_AFTER (300s). So an unauthenticated peer on an enabled
bearer can pin memory just by handing out fresh identities.

I measured it: 2000 discovery frames each with a distinct (addr,node_id)
-> 2000 live nodes (Slab +~15MB); the same 2000 frames sharing one
identity -> 1 node. The only variable is uniqueness, so it's the missing
cap and not frame volume.

Still the case on net HEAD: tipc_net.num_nodes is declared but never
read or written anywhere in net/tipc/, and the create path has no bounds
check. The patch just wires up that dead counter, the same way
neigh_alloc() checks gc_thresh3 before adding a struct neighbour for
unauthenticated on-link input.

> Can you provide your C reproducer and the stack trace you observed
> (on latest net-tree) ?

There's no stack trace - this is resource exhaustion, not corruption.
The alloc is GFP_ATOMIC and NULL-checked, so it just grows memory; KASAN
doesn't fire and there's nothing to paste. I should have written that in
the changelog instead of "KASAN build", which was misleading - sorry.

The reproducer is python (genetlink + raw AF_PACKET injection of a
captured DSC_REQ over a veth pair, no userspace tipc needed), not C.
Happy to send it or port it to C if that's useful. It ran on a v6.19
stand rather than net; since node.c is unchanged there bar the
kzalloc_obj rename I don't expect a difference, but I can re-run on
net-next. One honest caveat: the single-VM harness is softirq-drain
limited (a 10000-frame flood only reached ~3042 nodes before the cap),
so "unbounded at line rate" is extrapolation from the 2000-vs-1 result,
not something I clocked at line rate.

That said, the bearer is a trusted-cluster segment by design. If your
position is that on-bearer discovery peers are inside the trust
boundary, that's fair enough and I'm happy to drop this. I only sent it
because neigh bounds the equivalent ARP/ND case, so it seemed worth
doing the same here.

Thanks,
Ibrahim

^ permalink raw reply

* Re: [PATCH bpf-next v4 1/6] netlink: specs: Add XDP RX checksum capability to XDP metadata specs
From: Lorenzo Bianconi @ 2026-07-14 17:08 UTC (permalink / raw)
  To: Stanislav Fomichev
  Cc: Vladimir Vdovin, Donald Hunter, Jakub Kicinski, David S . Miller,
	Eric Dumazet, Paolo Abeni, Simon Horman, Alexei Starovoitov,
	Daniel Borkmann, Jesper Dangaard Brouer, John Fastabend,
	Stanislav Fomichev, Andrew Lunn, Tony Nguyen, Przemek Kitszel,
	Alexander Lobakin, Andrii Nakryiko, Martin KaFai Lau,
	Eduard Zingerman, Song Liu, Yonghong Song, KP Singh, Hao Luo,
	Jiri Olsa, Shuah Khan, Maciej Fijalkowski, Jakub Sitnicki,
	Aleksandr Loktionov, netdev, bpf, intel-wired-lan,
	linux-kselftest
In-Reply-To: <alE8oBxkrak8n602@devvm7509.cco0.facebook.com>

[-- Attachment #1: Type: text/plain, Size: 3985 bytes --]

> On 07/08, Vladimir Vdovin wrote:
> > From: Lorenzo Bianconi <lorenzo@kernel.org>
> > 
> > Introduce XDP RX checksum capability to XDP metadata specs. XDP RX
> > checksum will be use by devices capable of exposing receive checksum
> > result via bpf_xdp_metadata_rx_checksum().
> > Moreover, introduce xmo_rx_checksum netdev callback in order to allow
> > the eBPF program bound to the device to retrieve the RX checksum result
> > computed by the hw NIC and reported via DMA descriptors.
> > 
> > Signed-off-by: Lorenzo Bianconi <lorenzo@kernel.org>
> > Signed-off-by: Vladimir Vdovin <deliran@verdict.gg>
> > ---
> >  Documentation/netlink/specs/netdev.yaml |  5 ++++
> >  include/net/xdp.h                       | 18 ++++++++++++++
> >  include/uapi/linux/netdev.h             |  3 +++
> >  net/core/xdp.c                          | 32 +++++++++++++++++++++++++
> >  tools/include/uapi/linux/netdev.h       |  3 +++
> >  5 files changed, 61 insertions(+)
> > 
> > diff --git a/Documentation/netlink/specs/netdev.yaml b/Documentation/netlink/specs/netdev.yaml
> > index 5f143da7458c..6d0d90d3a614 100644
> > --- a/Documentation/netlink/specs/netdev.yaml
> > +++ b/Documentation/netlink/specs/netdev.yaml
> > @@ -61,6 +61,11 @@ definitions:
> >          doc: |
> >            Device is capable of exposing receive packet VLAN tag via
> >            bpf_xdp_metadata_rx_vlan_tag().
> > +      -
> > +        name: checksum
> > +        doc: |
> > +          Device is capable of exposing receive checksum result via
> > +          bpf_xdp_metadata_rx_checksum().
> >    -
> >      type: flags
> >      name: xsk-flags
> > diff --git a/include/net/xdp.h b/include/net/xdp.h
> > index aa742f413c35..e255ff786131 100644
> > --- a/include/net/xdp.h
> > +++ b/include/net/xdp.h
> > @@ -586,6 +586,10 @@ void xdp_attachment_setup(struct xdp_attachment_info *info,
> >  			   NETDEV_XDP_RX_METADATA_VLAN_TAG, \
> >  			   bpf_xdp_metadata_rx_vlan_tag, \
> >  			   xmo_rx_vlan_tag) \
> > +	XDP_METADATA_KFUNC(XDP_METADATA_KFUNC_RX_CHECKSUM, \
> > +			   NETDEV_XDP_RX_METADATA_CHECKSUM, \
> > +			   bpf_xdp_metadata_rx_checksum, \
> > +			   xmo_rx_checksum)
> >  
> >  enum xdp_rx_metadata {
> >  #define XDP_METADATA_KFUNC(name, _, __, ___) name,
> > @@ -643,12 +647,26 @@ enum xdp_rss_hash_type {
> >  	XDP_RSS_TYPE_L4_IPV6_SCTP_EX = XDP_RSS_TYPE_L4_IPV6_SCTP | XDP_RSS_L3_DYNHDR,
> >  };
> 
> [..]
>  
> > +/* Please note the driver is required to invalidate the checksum if the NIC
> > + * reports CHECKSUM_UNNECESSARY or CHECKSUM_COMPLETE and the eBPF program
> > + * modifies the packet since it can change some fields validated by the
> > + * checksum.
> > + */
> 
> Sorry, a bit confused about this part. IIUC this is in response to this
> discussion https://lore.kernel.org/bpf/aaLYfWnuuf_ne72u@lore-desk/ ?
> 
> Looking at a few drivers:
> - bnxt (bnxt_rx_pkt) does UNNECESSARY - ok
> - mlx5 (mlx5e_handle_csum) does UNNECESSARY and skips COMPLETE if there is
>   bpf prog attached
> - fbnic (fbnic_rx_csum) - can do COMPLETE even with xdp attached?
> - gve (gve_rx) - can do COMPLETE even with xdp attached?
> 
> So none of the drivers invalidate anything (and nether the bpf machinery), some
> choose to (correctly?) not report COMPLETE when there is XDP. So was the
> discussion about documenting this and giving some guidance? This probably
> belongs to that `DOC: skb checksums` section of include/linux/skbuff.h?

Hi Stanislav,

re-reading the previous upstream discussion, I guess we should just document the
expected driver behaviour (invalidate the cksum for both COMPLETE and
UNNECESSARY cases when the XDP program running on the NIC returns XDP_PASS) and
add a given kselftest for it. Agree?

> 
> Also, can you add your new rx_checksum to
> Documentation/networking/xdp-rx-metadata.rst's list of supported kfuncs?

ack, I will do it in the next iteration.

Regards,
Lorenzo

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 228 bytes --]

^ permalink raw reply

* Re: [PATCH net-next 9/9] net: document RFC6724 rule 5.5 implementation
From: David 'equinox' Lamparter @ 2026-07-14 17:16 UTC (permalink / raw)
  To: Jonathan Corbet; +Cc: netdev, linux-doc
In-Reply-To: <87zeztg1ye.fsf@trenco.lwn.net>

On Tue, Jul 14, 2026 at 07:01:13AM -0600, Jonathan Corbet wrote:
> David 'equinox' Lamparter <equinox@diac24.net> writes:
> >  create mode 100644 Documentation/networking/ipv6-addrsel.rst
>
> You need to add this new document to the index.rst file or it won't be
> part of the docs build...you should have seen a warning when you built
> the docs.

Indeed... I missed it between the other warnings.  Downside of doing
clean runs without a sphinx cache, sigh.

> > +====================================
> > +IPv6 source address selection trivia
> > +====================================
> > +
> > +
> > +RFC6724 rule 5.5 support
> > +------------------------
>
> Please stick with the section markup guidelines in
> Documentation/doc-guide/sphinx.rst.

Oh, you mean I shouldn't skip the "Chapter" level?

Now that you mention it, maybe I should include the file from ipv6.rst,
and remove the document title entirely - it looks quite weird in the TOC
with the direct reference from index.rst.  (Or rather, I've made weird
choices with the headings.)  Does that sound good to you?

(Or, I could just put the entire text into ipv6.rst...)

> > +RFC6724 rule 5.5 is a very short paragraph in a complex RFC that has turned
> > +out quite tricky, but also immensely useful in multihoming scenarios.  For
> > +reference, it says:
> > +
> > +::
> 
> You'll likely get more visually pleasing results if you just leave out
> the "::" here; you don't need literal formatting.

I did that because RFCs are historically shipped & cited monospaced, and
that's direct from the RFC.  My tendency is to keep it this way for
"established convention" reasons, but I don't care that much - does
anyone have stronger feelings?

> > diff --git a/MAINTAINERS b/MAINTAINERS
> > index f3218abefd0c..4edf48362a07 100644
> > --- a/MAINTAINERS
> > +++ b/MAINTAINERS
> > @@ -18912,6 +18912,7 @@ F:	Documentation/netlink/specs/rt-addr.yaml
> >  F:	Documentation/netlink/specs/rt-neigh.yaml
> >  F:	Documentation/netlink/specs/rt-route.yaml
> >  F:	Documentation/netlink/specs/rt-rule.yaml
> > +F:	Documentation/networking/ipv6-addrsel.rst
> 
> It seems weird to add this one file here - I wonder why there isn't just
> an entry for Documentation/networking ?

There is, the difference is "NETWORKING [GENERAL]" vs "NETWORKING
[IPv4/IPv6]".  I wasn't sure how much this matters & decided to err on
the side of caution (and also because checkpatch complained.)  I'll go
drop the line.

Thanks,


-equi

^ permalink raw reply

* Re: [PATCH RFC v2 5/9] leds: Add trigger_may_offload attribute
From: Rong Zhang @ 2026-07-14 17:23 UTC (permalink / raw)
  To: Thomas Weißschuh
  Cc: Lee Jones, Pavel Machek, Jonathan Corbet, Shuah Khan,
	Benson Leung, Guenter Roeck, Marek Behún, Mark Pearson,
	Derek J. Clark, Hans de Goede, Ilpo Järvinen, Ike Panhc,
	Andrew Lunn, Jakub Kicinski, Vishnu Sankar, Vishnu Sankar,
	linux-leds, netdev, linux-doc, linux-kernel, chrome-platform,
	platform-driver-x86
In-Reply-To: <b36aa937-b100-4fa3-bbc9-631c4c39ba40@t-8ch.de>

Hi Thomas,

Thanks for reviewing the series!

On Mon, 2026-07-13 at 22:36 +0200, Thomas Weißschuh wrote:
> On 2026-06-18 00:47:59+0800, Rong Zhang wrote:
> > There are multiple triggers implementing hardware control. Only "netdev"
> > provides a custom attribute to determine if it's offloaded to hardware
> > (i.e., in hardware control). For other triggers, there is no obvious way
> > for userspace to determine the trigger state programmatically. Moreover,
> > userspace can't query if an LED device supports hardware control or
> > identifies these triggers.
> > 
> > Add a new attribute "trigger_may_offload" to the LED core, so that
> > userspace can determine:
> > 
> > - if the LED device supports hardware control (supported => visible)
> > - which trigger is the hardware control trigger selected by the LED
> >   device
> > - if the trigger is selected ("<foo_trigger>")
> > - if the trigger is offloaded ("[foo_trigger]")
> > 
> > Note: the documentation describes the attribute as "returning a list"
> > despite the LED core currently only supports one hardware control
> > trigger per LED device. This is intentional to make the attribute
> > extensible in the future without breaking userspace.
> 
> Maybe also mention that the old 'offloaded' attribute is deprecated.
> However, does it really need to be deprecated?

Hmm, retaining the old one is indeed harmless.

I will leave the "offloaded" attribute as is, and add a short note to
the documentation of "trigger_may_offload" to briefly introduce the
relation between them.

> 
> > Signed-off-by: Rong Zhang <i@rong.moe>
> > ---
> >  .../ABI/obsolete/sysfs-class-led-trigger-netdev    | 16 ++++++++
> >  Documentation/ABI/testing/sysfs-class-led          | 22 +++++++++++
> >  .../ABI/testing/sysfs-class-led-trigger-netdev     | 13 -------
> >  Documentation/leds/leds-class.rst                  |  8 ++++
> >  drivers/leds/led-class.c                           | 23 +++++++++++
> >  drivers/leds/led-triggers.c                        | 45 ++++++++++++++++++++++
> >  drivers/leds/leds.h                                |  2 +
> >  drivers/leds/trigger/ledtrig-netdev.c              |  2 +
> >  8 files changed, 118 insertions(+), 13 deletions(-)
> > 
> > diff --git a/Documentation/ABI/obsolete/sysfs-class-led-trigger-netdev b/Documentation/ABI/obsolete/sysfs-class-led-trigger-netdev
> > new file mode 100644
> > index 000000000000..8d2fbfaf50c3
> > --- /dev/null
> > +++ b/Documentation/ABI/obsolete/sysfs-class-led-trigger-netdev
> > @@ -0,0 +1,16 @@
> > +What:		/sys/class/leds/<led>/offloaded
> > +Date:		June 2026
> > +KernelVersion:	7.3
> > +Contact:	linux-leds@vger.kernel.org
> > +Description:
> > +		Communicate whether the LED trigger modes are offloaded to
> > +		hardware or whether software fallback is used.
> > +
> > +		If 0, the LED is using software fallback to blink.
> > +
> > +		If 1, the LED blinking in requested mode is offloaded to
> > +		hardware.
> > +
> > +		/sys/class/leds/<led>/trigger_may_offload provides a generic
> > +		method to query the offloaded state of supported triggers,
> > +		superseding this attribute.
> > diff --git a/Documentation/ABI/testing/sysfs-class-led b/Documentation/ABI/testing/sysfs-class-led
> > index 0313b82644f2..edd5a9a74dfd 100644
> > --- a/Documentation/ABI/testing/sysfs-class-led
> > +++ b/Documentation/ABI/testing/sysfs-class-led
> > @@ -78,6 +78,28 @@ Description:
> >  		(which would often be configured in the device tree for the
> >  		hardware).
> >  
> > +What:		/sys/class/leds/<led>/trigger_may_offload
> > +Date:		June 2026
> > +KernelVersion:	7.3
> > +Contact:	linux-leds@vger.kernel.org
> > +Description:
> > +		Names and states of triggers that may be offloaded to hardware.
> > +		Such triggers are also called "hw control trigger" in some
> > +		context.
> > +
> > +		Only exists when the LED supports trigger offload.
> > +
> > +		Reading this file returns a list of triggers that are capable to
> > +		be offloaded. The optional brackets around the trigger name
> > +		indicate the state of the current trigger:
> > +
> > +		- `foo_trigger`: the trigger is not selected.
> > +		- `<foo_trigger>`: the trigger is selected, but falls back to
> > +		  software blink for some reason (e.g., incompatible trigger
> > +		  parameters)
> > +		- `[foo_trigger]`: the trigger is selected and offloaded to
> > +		  hardware.
> > +
> >  What:		/sys/class/leds/<led>/inverted
> >  Date:		January 2011
> >  KernelVersion:	2.6.38
> > diff --git a/Documentation/ABI/testing/sysfs-class-led-trigger-netdev b/Documentation/ABI/testing/sysfs-class-led-trigger-netdev
> > index ed46b37ab8a2..396d37a4b820 100644
> > --- a/Documentation/ABI/testing/sysfs-class-led-trigger-netdev
> > +++ b/Documentation/ABI/testing/sysfs-class-led-trigger-netdev
> > @@ -62,19 +62,6 @@ Description:
> >  		When offloaded is true, the blink interval is controlled by
> >  		hardware and won't reflect the value set in interval.
> >  
> > -What:		/sys/class/leds/<led>/offloaded
> > -Date:		Jun 2023
> > -KernelVersion:	6.5
> > -Contact:	linux-leds@vger.kernel.org
> > -Description:
> > -		Communicate whether the LED trigger modes are offloaded to
> > -		hardware or whether software fallback is used.
> > -
> > -		If 0, the LED is using software fallback to blink.
> > -
> > -		If 1, the LED blinking in requested mode is offloaded to
> > -		hardware.
> > -
> >  What:		/sys/class/leds/<led>/link_10
> >  Date:		Jun 2023
> >  KernelVersion:	6.5
> > diff --git a/Documentation/leds/leds-class.rst b/Documentation/leds/leds-class.rst
> > index 84665200a88d..41342ecb5f6b 100644
> > --- a/Documentation/leds/leds-class.rst
> > +++ b/Documentation/leds/leds-class.rst
> > @@ -179,6 +179,9 @@ ops and needs to declare specific support for the supported triggers.
> >  
> >  With hw control we refer to the LED driven by hardware.
> >  
> > +A sysfs attribute `trigger_may_offload` is provided for userspace to
> > +query supported triggers and their states.
> > +
> >  LED driver must define the following value to support hw control:
> >  
> >      - hw_control_trigger:
> > @@ -240,6 +243,11 @@ LED trigger must implement the following API to support hw control:
> >                  return a boolean indicating if the trigger is offloaded to
> >                  hardware.
> >  
> > +                If an LED driver specifies a hw control trigger but the
> > +                latter doesn't implement this callback, a dev_err_once will
> > +                be emitted and the LED trigger will be assumed to be not
> > +                offloaded.
> 
> Not sure if this needs to be documented.
> I would make this optional initally and enforce it after all the drivers
> have been changed in the series.

Makes sense. Will remove the paragraph and rearrange the series so that
the attribute is introduced first while driver implementations are added
later.

ACK on all remaining comments as well as those in other replies. And
thanks a lot for your tag for cros_ec ;-)

Thanks,
Rong

> 
> > +
> >  LED driver can activate additional modes by default to workaround the
> >  impossibility of supporting each different mode on the supported trigger.
> >  Examples are hardcoding the blink speed to a set interval, enable special
> > diff --git a/drivers/leds/led-class.c b/drivers/leds/led-class.c
> > index 9e14ae588f78..0ac80b93b8b5 100644
> > --- a/drivers/leds/led-class.c
> > +++ b/drivers/leds/led-class.c
> > @@ -90,8 +90,31 @@ static const struct bin_attribute *const led_trigger_bin_attrs[] = {
> >  	&bin_attr_trigger,
> >  	NULL,
> >  };
> > +
> > +static DEVICE_ATTR(trigger_may_offload, 0444, led_trigger_may_offload_show, NULL);
> 
> DEVICE_ATTR_RO()
> 
> > +static struct attribute *led_trigger_attrs[] = {
> > +	&dev_attr_trigger_may_offload.attr,
> > +	NULL,
> 
> No comma after the sentinel.
> 
> > +};
> > +
> > +static umode_t led_trigger_is_visible(struct kobject *kobj,
> > +				      struct attribute *attr,
> > +				      int idx)
> > +{
> > +	struct device *dev = kobj_to_dev(kobj);
> > +	struct led_classdev *led_cdev = dev_get_drvdata(dev);
> > +
> > +	if (attr == &dev_attr_trigger_may_offload.attr &&
> > +	    !led_cdev->hw_control_trigger)
> 
> Can be a single line.
> 
> > +		return 0;
> > +
> > +	return attr->mode;
> > +}
> > +
> >  static const struct attribute_group led_trigger_group = {
> >  	.bin_attrs = led_trigger_bin_attrs,
> > +	.attrs = led_trigger_attrs,
> > +	.is_visible = led_trigger_is_visible,
> >  };
> >  #endif
> >  
> > diff --git a/drivers/leds/led-triggers.c b/drivers/leds/led-triggers.c
> > index b1223218bda1..c43229d9c4c1 100644
> > --- a/drivers/leds/led-triggers.c
> > +++ b/drivers/leds/led-triggers.c
> > @@ -313,6 +313,51 @@ void led_trigger_set_default(struct led_classdev *led_cdev)
> >  }
> >  EXPORT_SYMBOL_GPL(led_trigger_set_default);
> >  
> > +/*
> > + * Caller must ensure led_cdev->trigger_lock held,
> 
> Use lockdep_assert_held() to document and validate locking invariants.
> 
> > + * and led_cdev->trigger->name must match led_cdev->hw_control_trigger.
> > + */
> > +static bool led_trigger_get_offloaded(struct led_classdev *led_cdev)
> > +{
> > +	if (likely(led_cdev->trigger->offloaded))
> > +		return led_cdev->trigger->offloaded(led_cdev);
> > +
> > +	dev_err_once(led_cdev->dev,
> > +		     "hw control trigger %s doesn't implement offloaded(), this is a bug\n",
> > +		     led_cdev->trigger->name);
> > +	return false;
> > +}
> > +
> > +ssize_t led_trigger_may_offload_show(struct device *dev,
> > +				     struct device_attribute *attr, char *buf)
> > +{
> > +	struct led_classdev *led_cdev = dev_get_drvdata(dev);
> > +	bool hit, offloaded = false;
> > +	struct led_trigger *trig;
> > +	int len;
> > +
> > +	mutex_lock(&led_cdev->led_access);
> > +	down_read(&led_cdev->trigger_lock);
> 
> guard()?
> 
> > +
> > +	trig = led_cdev->trigger;
> > +
> > +	hit = trig && !strcmp(led_cdev->hw_control_trigger, trig->name);
> > +	if (hit)
> > +		offloaded = led_trigger_get_offloaded(led_cdev);
> > +
> > +	/* [offloaded] <active_but_not_offloaded> inactive */
> > +	len = sysfs_emit(buf, "%s%s%s\n",
> > +			 offloaded ? "[" : (hit ? "<" : ""),
> > +			 led_cdev->hw_control_trigger,
> > +			 offloaded ? "]" : (hit ? ">" : ""));
> > +
> > +	up_read(&led_cdev->trigger_lock);
> > +	mutex_unlock(&led_cdev->led_access);
> > +
> > +	return len;
> > +}
> > +EXPORT_SYMBOL_GPL(led_trigger_may_offload_show);
> > +
> >  /* LED Trigger Interface */
> >  
> >  int led_trigger_register(struct led_trigger *trig)
> > diff --git a/drivers/leds/leds.h b/drivers/leds/leds.h
> > index bee46651e068..9177e098989b 100644
> > --- a/drivers/leds/leds.h
> > +++ b/drivers/leds/leds.h
> > @@ -27,6 +27,8 @@ ssize_t led_trigger_read(struct file *filp, struct kobject *kobj,
> >  ssize_t led_trigger_write(struct file *filp, struct kobject *kobj,
> >  			const struct bin_attribute *bin_attr, char *buf,
> >  			loff_t pos, size_t count);
> > +ssize_t led_trigger_may_offload_show(struct device *dev,
> > +				     struct device_attribute *attr, char *buf);
> >  
> >  extern struct rw_semaphore leds_list_lock;
> >  extern struct list_head leds_list;
> > diff --git a/drivers/leds/trigger/ledtrig-netdev.c b/drivers/leds/trigger/ledtrig-netdev.c
> > index a26109ca4b1c..21f22eea4ab8 100644
> > --- a/drivers/leds/trigger/ledtrig-netdev.c
> > +++ b/drivers/leds/trigger/ledtrig-netdev.c
> > @@ -487,6 +487,8 @@ static ssize_t offloaded_show(struct device *dev,
> >  {
> >  	struct led_netdev_data *trigger_data = led_trigger_get_drvdata(dev);
> >  
> > +	dev_warn_once(dev, "offloaded attribute has been deprecated, see trigger_may_offload.\n");
> > +
> >  	return sprintf(buf, "%d\n", trigger_data->hw_control);
> >  }
> >  
> > 
> > -- 
> > 2.53.0
> > 

^ permalink raw reply

* Re: [PATCH net-next 13/15] net/mlx5e: psp: Make PSP steering config dynamic
From: Simon Horman @ 2026-07-14 17:30 UTC (permalink / raw)
  To: tariqt
  Cc: Simon Horman, andrew+netdev, davem, edumazet, kuba, netdev,
	pabeni, aleksandr.loktionov, borisp, cmi, cratiu, daniel.zahka,
	dtatulea, gal, jacob.e.keller, jianbol, lkayal, leon,
	linux-kernel, linux-rdma, mbloch, raeds, rrameshbabu, saeedm, sdf,
	sdf.kernel, willemdebruijn.kernel
In-Reply-To: <20260707130858.969928-14-tariqt@nvidia.com>

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://sashiko.dev
---
--- Patch [13]: [PATCH net-next 13/15] net/mlx5e: psp: Make PSP steering config dynamic ---
commit 133d9a32cbf9c1b939ae4ca8473c41624c313951
Author: Cosmin Ratiu <cratiu@nvidia.com>

net/mlx5e: psp: Make PSP steering config dynamic

This commit changes PSP steering tables to only be created when PSP
configuration is enabled on a device, instead of at device initialization
time. This avoids imposing steering overhead on all UDP packets when PSP
is not being used.

> diff --git a/drivers/net/ethernet/mellanox/mlx5/core/en_accel/en_accel.h b/drivers/net/ethernet/mellanox/mlx5/core/en_accel/en_accel.h
> index b526b3898c22e..3f212e46fc2f9 100644
> --- a/drivers/net/ethernet/mellanox/mlx5/core/en_accel/en_accel.h
> +++ b/drivers/net/ethernet/mellanox/mlx5/core/en_accel/en_accel.h
> @@ -220,18 +220,7 @@ static inline void mlx5e_accel_tx_finish(struct mlx5e_txqsq *sq,
>  
>  static inline int mlx5e_accel_init_rx(struct mlx5e_priv *priv)
>  {
> -	int err;
> -
> -	err = mlx5_accel_psp_fs_init_rx_tables(priv);
> -	if (err)
> -		goto out;
> -
> -	err = mlx5e_ktls_init_rx(priv);
> -	if (err)
> -		mlx5_accel_psp_fs_cleanup_rx_tables(priv);
> -
> -out:
> -	return err;
> +	return mlx5e_ktls_init_rx(priv);
>  }
>  
>  static inline void mlx5e_accel_cleanup_rx(struct mlx5e_priv *priv)
> @@ -242,12 +231,6 @@ static inline void mlx5e_accel_cleanup_rx(struct mlx5e_priv *priv)
>  
>  static inline int mlx5e_accel_init_tx(struct mlx5e_priv *priv)
>  {
> -	int err;
> -
> -	err = mlx5_accel_psp_fs_init_tx_tables(priv);
> -	if (err)
> -		return err;
> -
>  	return mlx5e_ktls_init_tx(priv);
>  }

[Severity: Critical]
Does removing the unconditional table creation here prevent PSP tables
from being restored on device attach or resume?

Since mlx5_accel_psp_fs_cleanup_tx_tables() is still called during device
detach or suspend, the PSP steering tables are destroyed. However, because
mlx5e_accel_init_tx() no longer recreates them, the tables appear to remain
missing after a suspend/resume or detach/attach cycle.

Because software psp_assoc structures remain bound to active sockets, could
this leave the device in a state where the hardware TX steering table (which
contains the CRYPTO_ENCRYPT action) is missing?

If so, would this cause egress traffic to bypass encryption and be
transmitted in plaintext?

^ permalink raw reply

* Re: [PATCH net-next 9/9] net: document RFC6724 rule 5.5 implementation
From: Jonathan Corbet @ 2026-07-14 17:31 UTC (permalink / raw)
  To: David 'equinox' Lamparter; +Cc: netdev, linux-doc
In-Reply-To: <alZvBEPH86WqH56m@eidolon.nox.tf>

David 'equinox' Lamparter <equinox@diac24.net> writes:

> On Tue, Jul 14, 2026 at 07:01:13AM -0600, Jonathan Corbet wrote:
>> David 'equinox' Lamparter <equinox@diac24.net> writes:
>> >  create mode 100644 Documentation/networking/ipv6-addrsel.rst
>>
>> You need to add this new document to the index.rst file or it won't be
>> part of the docs build...you should have seen a warning when you built
>> the docs.
>
> Indeed... I missed it between the other warnings.  Downside of doing
> clean runs without a sphinx cache, sigh.

*Sigh* ... one of these years we'll clean up all those warnings, and
have them actually stay cleaned...

>> > +====================================
>> > +IPv6 source address selection trivia
>> > +====================================
>> > +
>> > +
>> > +RFC6724 rule 5.5 support
>> > +------------------------
>>
>> Please stick with the section markup guidelines in
>> Documentation/doc-guide/sphinx.rst.
>
> Oh, you mean I shouldn't skip the "Chapter" level?
>
> Now that you mention it, maybe I should include the file from ipv6.rst,
> and remove the document title entirely - it looks quite weird in the TOC
> with the direct reference from index.rst.  (Or rather, I've made weird
> choices with the headings.)  Does that sound good to you?
>
> (Or, I could just put the entire text into ipv6.rst...)

I would take that latter approach myself.  But others may differ :)

>> > +RFC6724 rule 5.5 is a very short paragraph in a complex RFC that has turned
>> > +out quite tricky, but also immensely useful in multihoming scenarios.  For
>> > +reference, it says:
>> > +
>> > +::
>> 
>> You'll likely get more visually pleasing results if you just leave out
>> the "::" here; you don't need literal formatting.
>
> I did that because RFCs are historically shipped & cited monospaced, and
> that's direct from the RFC.  My tendency is to keep it this way for
> "established convention" reasons, but I don't care that much - does
> anyone have stronger feelings?

You're writing the document, it's up to you in the end.

(But if you keep it that way, you can end the previous paragraph with
"it says::" and drop the separate "::" line).

Thanks,

jon

^ permalink raw reply

* Re: [PATCH] phonet: check register_netdevice_notifier() error in phonet_device_init()
From: Rémi Denis-Courmont @ 2026-07-13 19:22 UTC (permalink / raw)
  To: netdev; +Cc: Minhong He
In-Reply-To: <20260713075212.431455-1-heminhong@kylinos.cn>

Le maanantaina 13. heinäkuuta 2026, 10.52.12 Itä-Euroopan kesäaika Minhong He 
a écrit :
> phonet_device_init() registers a netdevice notifier before calling
> phonet_netlink_register(), but does not check whether notifier
> registration succeeded. On failure, netlink setup still proceeds and
> init may return success without the notifier in place.

> Check the notifier registration error and unwind through
> phonet_device_exit() on failure.

That will unregister a notifier that is not registered. While I believe that 
would fail safe, I am not sure if that is really considered proper by 
contemporary kernel coding standards.

-- 
Rémi Denis-Courmont
https://www.remlab.net/




^ permalink raw reply

* Re: [PATCH net-next 15/15] net/mlx5e: psp: Report PSP dev registration errors
From: Simon Horman @ 2026-07-14 17:31 UTC (permalink / raw)
  To: tariqt
  Cc: Simon Horman, andrew+netdev, davem, edumazet, kuba, netdev,
	pabeni, aleksandr.loktionov, borisp, cmi, cratiu, daniel.zahka,
	dtatulea, gal, jacob.e.keller, jianbol, lkayal, leon,
	linux-kernel, linux-rdma, mbloch, raeds, rrameshbabu, saeedm, sdf,
	sdf.kernel, willemdebruijn.kernel
In-Reply-To: <20260707130858.969928-16-tariqt@nvidia.com>

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://sashiko.dev
---
--- Patch [15]: [PATCH net-next 15/15] net/mlx5e: psp: Report PSP dev registration errors ---
commit 76afe7de4d1e708e7c01d267ef1fb9cf946de937
Author: Cosmin Ratiu <cratiu@nvidia.com>

net/mlx5e: psp: Report PSP dev registration errors

This commit updates mlx5e_psp_register() to return an error code instead
of void when psp_dev_create() fails. The caller, mlx5e_nic_enable(), is
also updated to check this return value and abort if PSP registration fails.

> diff --git a/drivers/net/ethernet/mellanox/mlx5/core/en_main.c b/drivers/net/ethernet/mellanox/mlx5/core/en_main.c
> --- a/drivers/net/ethernet/mellanox/mlx5/core/en_main.c
> +++ b/drivers/net/ethernet/mellanox/mlx5/core/en_main.c
> @@ -6199,7 +6199,9 @@ static int mlx5e_nic_enable(struct mlx5e_priv *priv)
>  
>  	mlx5e_fs_init_l2_addr(priv->fs, netdev);
>  	mlx5e_ipsec_init(priv);
> -	mlx5e_psp_register(priv);
> +	err = mlx5e_psp_register(priv);
> +	if (err)
> +		goto out_ipsec_cleanup;

[Severity: High]
Could propagating this error cause the entire network interface to fail if
the optional PSP offload fails to register?

If psp_dev_create() fails (e.g., due to memory pressure or ID exhaustion
in xa_alloc_cyclic), mlx5e_nic_enable() will now abort, resulting in a
complete loss of networking capability for the device.

Notice how the code immediately following this handles MACsec initialization
failures by logging the error and allowing the NIC to continue operating:

	err = mlx5e_macsec_init(priv);
	if (err)
		mlx5_core_err(mdev, "MACsec initialization failed, %d\n", err);

Should PSP registration failure fall back gracefully and allow the core NIC
to function without the offload, similar to how MACsec is handled?

>  
>  	err = mlx5e_macsec_init(priv);
>  	if (err)
>  		mlx5_core_err(mdev, "MACsec initialization failed, %d\n", err);

^ permalink raw reply

* [PATCH net v3 0/2] tipc: fix NULL deref in tipc_named_node_up() on empty publication list
From: Weiming Shi @ 2026-07-14 17:41 UTC (permalink / raw)
  To: Jon Maloy, netdev, tipc-discussion
  Cc: David S . Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
	Simon Horman, Hoang Huu Le, Xiang Mei, Tung Quang Nguyen,
	linux-kernel, Weiming Shi

named_distribute() stamps the last_bulk flag on the tail skb of the
publication list. When the list is empty no skb is enqueued and the tail
access dereferences NULL. tipc_named_node_up() hits this on an empty
cluster_scope, which happens with a node-id configuration where
cluster_scope is populated only later by tipc_net_finalize(). It is
reachable by an unprivileged user over a UDP bearer in a user+net
namespace.

Patch 1 guards tipc_node_xmit() against an empty list, so the allocation-
failure path in named_distribute() cannot pass an empty list down to
tipc_lxc_xmit(), which has the same tail-of-empty-list deref.

Patch 2 emits an item-less bulk when the publication list is empty, so the
peer still receives the last_bulk flag and does not stall this node's later
name updates.

Changes in v3:
 - Split into two patches; add patch 1 to guard tipc_node_xmit() against
   the tipc_lxc_xmit() deref reachable via the allocation-failure path.
 - Break out of the build loop on allocation failure instead of returning,
   so the last_bulk flag is still applied to the last queued skb.

Changes in v2:
 - Instead of returning early on an empty cluster_scope (which would stall
   the peer, since it waits for a bulk with the last_bulk flag before
   accepting later name updates), emit an item-less bulk so the peer opens
   normally.

Thanks to Tung Quang Nguyen for the review of v1.

Weiming Shi (2):
  tipc: guard against empty list in tipc_node_xmit()
  tipc: fix NULL deref in tipc_named_node_up() on empty publication list

 net/tipc/name_distr.c | 16 +++++++++++++++-
 net/tipc/node.c       |  3 +++
 2 files changed, 18 insertions(+), 1 deletion(-)


base-commit: 4595d2c77ea4bfe4ae0efa38770a59ee5d2f06d3
-- 
2.43.0


^ permalink raw reply

* [PATCH net v3 1/2] tipc: guard against empty list in tipc_node_xmit()
From: Weiming Shi @ 2026-07-14 17:41 UTC (permalink / raw)
  To: Jon Maloy, netdev, tipc-discussion
  Cc: David S . Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
	Simon Horman, Hoang Huu Le, Xiang Mei, Tung Quang Nguyen,
	linux-kernel, Weiming Shi
In-Reply-To: <20260714174110.1571033-1-bestswngs@gmail.com>

tipc_node_xmit() passes @list to tipc_lxc_xmit(), which dereferences
buf_msg(skb_peek(list)) without checking, so an empty list causes a NULL
pointer dereference. named_distribute() can hand it an empty list when a
bulk allocation fails. tipc_link_xmit() was already guarded in commit
b77413446408 ("tipc: fix NULL deref in tipc_link_xmit()"); guard
tipc_node_xmit() itself so the tipc_lxc_xmit() path is covered too.

Fixes: f73b12812a3d ("tipc: improve throughput between nodes in netns")
Reported-by: Xiang Mei <xmei5@asu.edu>
Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Weiming Shi <bestswngs@gmail.com>
---
 net/tipc/node.c | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/net/tipc/node.c b/net/tipc/node.c
index 97aa970a0d83..fc241e7b0c1f 100644
--- a/net/tipc/node.c
+++ b/net/tipc/node.c
@@ -1695,6 +1695,9 @@ int tipc_node_xmit(struct net *net, struct sk_buff_head *list,
 	int bearer_id;
 	int rc;
 
+	if (skb_queue_empty(list))
+		return 0;
+
 	if (in_own_node(net, dnode)) {
 		tipc_loopback_trace(net, list);
 		spin_lock_init(&list->lock);
-- 
2.43.0


^ permalink raw reply related

* [PATCH net v3 2/2] tipc: fix NULL deref in tipc_named_node_up() on empty publication list
From: Weiming Shi @ 2026-07-14 17:41 UTC (permalink / raw)
  To: Jon Maloy, netdev, tipc-discussion
  Cc: David S . Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
	Simon Horman, Hoang Huu Le, Xiang Mei, Tung Quang Nguyen,
	linux-kernel, Weiming Shi
In-Reply-To: <20260714174110.1571033-1-bestswngs@gmail.com>

named_distribute() ends by stamping the last_bulk flag on the tail skb via
buf_msg(skb_peek_tail(list)). When the publication list is empty no skb is
enqueued, skb_peek_tail() returns NULL, and buf_msg(NULL) is dereferenced.

tipc_named_node_up() runs this on &nt->cluster_scope. With a node-id
configuration cluster_scope is populated only later by tipc_net_finalize(),
so a peer link that comes up first reaches named_distribute() with an empty
list. It is reachable by an unprivileged user (TIPC genl ops use
GENL_UNS_ADMIN_PERM) over a UDP bearer in a user+net namespace:

 KASAN: null-ptr-deref in range [0x00000000000000d8-0x00000000000000df]
 RIP: 0010:tipc_named_node_up (net/tipc/name_distr.c:196)
  tipc_named_node_up (net/tipc/name_distr.c:196 net/tipc/name_distr.c:221)
  tipc_node_write_unlock (net/tipc/node.c:428)
  tipc_rcv (net/tipc/node.c:2185)
  tipc_udp_recv (net/tipc/udp_media.c:392)
 Kernel panic - not syncing: Fatal exception in interrupt

The peer holds back this node's later name updates until it sees a bulk
with the last_bulk flag, so simply skipping the empty bulk would stall it.
Emit an item-less bulk when the list is empty, and break out of the build
loop on allocation failure instead of returning, so the last_bulk flag is
applied to the last queued skb.

Fixes: cad2929dc432 ("tipc: update a binding service via broadcast")
Reported-by: Xiang Mei <xmei5@asu.edu>
Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Weiming Shi <bestswngs@gmail.com>
---
 net/tipc/name_distr.c | 16 +++++++++++++++-
 1 file changed, 15 insertions(+), 1 deletion(-)

diff --git a/net/tipc/name_distr.c b/net/tipc/name_distr.c
index ba4f4906e13b..dbcfa965de34 100644
--- a/net/tipc/name_distr.c
+++ b/net/tipc/name_distr.c
@@ -165,7 +165,7 @@ static void named_distribute(struct net *net, struct sk_buff_head *list,
 						dnode);
 			if (!skb) {
 				pr_warn("Bulk publication failure\n");
-				return;
+				break;
 			}
 			hdr = buf_msg(skb);
 			msg_set_bc_ack_invalid(hdr, true);
@@ -192,6 +192,20 @@ static void named_distribute(struct net *net, struct sk_buff_head *list,
 		skb_trim(skb, INT_H_SIZE + (msg_dsz - msg_rem));
 		__skb_queue_tail(list, skb);
 	}
+
+	if (skb_queue_empty(list)) {
+		skb = named_prepare_buf(net, PUBLICATION, 0, dnode);
+		if (!skb) {
+			pr_warn("Bulk publication failure\n");
+			return;
+		}
+		hdr = buf_msg(skb);
+		msg_set_bc_ack_invalid(hdr, true);
+		msg_set_bulk(hdr);
+		msg_set_non_legacy(hdr);
+		__skb_queue_tail(list, skb);
+	}
+
 	hdr = buf_msg(skb_peek_tail(list));
 	msg_set_last_bulk(hdr);
 	msg_set_named_seqno(hdr, seqno);
-- 
2.43.0


^ permalink raw reply related

* Re: [PATCH net-next 0/3] net: nexthop: per-nexthop UDP dst port for fdb (VXLAN) nexthops
From: Ido Schimmel @ 2026-07-14 17:42 UTC (permalink / raw)
  To: Jack Ma
  Cc: netdev, David Ahern, David S . Miller, Eric Dumazet,
	Jakub Kicinski, Paolo Abeni, Simon Horman, Shuah Khan,
	linux-kselftest, linux-kernel
In-Reply-To: <20260714034309.2226-1-jack4it@gmail.com>

On Tue, Jul 14, 2026 at 03:43:09AM +0000, Jack Ma wrote:
> On Mon, Jul 13, 2026 at 03:23:39PM +0300, Ido Schimmel wrote:
> > > Some deployments pack several receivers behind a single underlay IP and tell
> > > them apart by UDP destination port.  To spread flows across such receivers they
> >
> > I don't understand the "tell them apart" phrasing. Aren't all of these
> > receivers interchangeable given you are load balancing between them?
> 
> Sorry, that wording conflated two separate things. The load-balancing
> targets are interchangeable; the UDP port is not what makes them
> distinct services, it is how the underlay addresses each one:
> 
>   - Addressing: several VTEPs are packed behind one underlay IP, each
>     reachable at a distinct UDP destination port. The port is the
>     underlay demux that delivers an encapsulated packet to the right
>     VTEP -- it selects *where*, not *which service*.
>   - Load-balancing: when those VTEPs form an HA set (shared inner
>     MAC/IP), a sender spreads flows across them with an fdb nexthop
>     group. The legs are interchangeable, but each one lives at a
>     different (underlay IP, UDP port).
> 
> So the legs are interchangeable endpoints that happen to sit at
> different (IP, port) addresses. A group can already carry a distinct IP
> per leg but not a distinct port, which is the gap here. I'll reword the
> cover and patch 1 to say this explicitly.
> 
> > I think you will need to provide more details about the use case [...]
> > In your use case, why can't the load balancing happen at the target
> > host (e.g., using XDP / TC / SO_REUSEPORT / flow classification)?
> 
> I think the key difference is that there isn't really a shared
> host-side datapath to load-balance in, and the UDP port is doing demux
> rather than load-balancing. The shape is a bit unusual, so maybe a
> couple of pictures help.
> 
> First, how forwarders sit on a receiver node. A forwarder here is
> just the pod that terminates the overlay tunnel (it hosts the VXLAN
> VTEP) and relays traffic to and from one tenant's workload. Many such
> pods share one mesh-routable underlay IP, each demuxed by UDP port:
> 
>   receiver node -- one mesh-routable underlay IP (NodeIP_A)
>   +----------------------------------------------------+
>   |  host netns: stateless outer-UDP demux by dst port |
>   |              (host does NOT terminate the tunnel)  |
>   |                                                    |
>   |     dst :40000        dst :40001        dst :40002 |
>   |         |                 |                 |      |
>   |   +-----v----+      +-----v----+      +-----v----+ |
>   |   | pod0 ns  |      | pod1 ns  |      | pod2 ns  | |
>   |   | vxlan    |      | vxlan    |      | vxlan    | |
>   |   | VTEP     |      | VTEP     |      | VTEP     | |
>   |   | decap    |      | decap    |      | decap    | |
>   |   +----------+      +----------+      +----------+ |
>   +----------------------------------------------------+
>   (up to ~10 forwarder pods packed per node)
> 
>   These packed pods are unrelated -- different tenants on different
>   VNIs (below) -- so the per-pod UDP port is node-level demux, not an
>   HA construct. A single forwarder's HA replicas are the orthogonal
>   axis: anti-affinity spreads them across nodes (never co-resident),
>   so one member's nexthop group has its legs on distinct node IPs.
>   But each leg is still reachable only at (node IP, that pod's UDP
>   port), so within one group the legs differ in IP *and* port. A
>   group can already carry a distinct IP per leg, but it takes the UDP
>   port from the device (a single value), so it can't send each leg to
>   its own port. That last part is really the gap we keep running into.
> 
> The pods sharing a node need not be related: each one belongs to a
> separate tenant on its own VXLAN VNI, and a node can pack forwarders
> for many of them, each on its own UDP port. That per-pod port is what
> lets different tenants co-locate without colliding, and the host --
> which only demuxes outer UDP -- never has to reason about tenancy.

It's unclear to me why you need the UDP port based demux. On a single
node, do you have multiple pods that belong to the same tenant and
therefore share the same VNI? From the above, my understanding is that
the answer is "no".

Assuming the answer is "no", instead of having multiple VXLAN sockets in
the host netns (each bound to a different port), why not have a single
VXLAN socket bound to the standard port (4789) and let the VXLAN driver
perform the demux based on the VNI? You can achieve that by creating
multiple VXLAN devices in the host netns that only differ in their VNI
and then move each to the relevant pod netns.

Given the anti-affinity constraint, you will only need to load balance
between pods on different nodes that use the standard port and that is
already supported by the FDB nexthop groups.

^ permalink raw reply

* Re: [PATCH net] amt: refresh cached IP header pointers after MLD/IGMP checks
From: Xiang Mei @ 2026-07-14 17:43 UTC (permalink / raw)
  To: Taehee Yoo
  Cc: Jakub Kicinski, Andrew Lunn, David S . Miller, Eric Dumazet,
	Paolo Abeni, netdev, linux-kernel, AutonomousCodeSecurity,
	tgopinath, kys
In-Reply-To: <CAMArcTVGfCzcnOrrKFRqD0a6Q2LgC19Yge1Lh2fvKMx2oqwtmw@mail.gmail.com>

Thanks for the reminder.

Xiang

On Tue, Jul 14, 2026 at 6:16 AM Taehee Yoo <ap420073@gmail.com> wrote:
>
> On Fri, Jul 10, 2026 at 10:41 AM Xiang Mei (Microsoft) <xmei5@asu.edu> wrote:
> >
>
> Hi Xiang Mei,
> Sorry for the late reply.
>
> > amt_dev_xmit() and amt_update_handler() cache iph/ip6h before calling
> > ip_mc_check_igmp() / ipv6_mc_check_mld(), which may pskb_may_pull() a
> > non-linear skb and reallocate skb->head. The stale pointers are then
> > read (amt_dev_xmit: group.ip{4,6} = iph/ip6h->daddr; amt_update_handler:
> > ip{,v6}_eth_mc_map()), a slab-use-after-free. tx is reachable by an
> > unprivileged user whose amt device has SG enabled; rx by a tunnel peer
> > sending a membership update fragmented across several IP fragments.
> >
> > Reload iph/ip6h after the check returns, like commit f0e42f0c4337
> > ("ipv6: sit: reload inner IPv6 header after GSO offloads").
> >
> > tx path:
> >   BUG: KASAN: slab-use-after-free in amt_dev_xmit (drivers/net/amt.c:1238)
> >   Read of size 16 by task exploit
> >    amt_dev_xmit (drivers/net/amt.c:1238)
> >    dev_hard_start_xmit (net/core/dev.c:3905)
> >    __dev_queue_xmit (net/core/dev.c:4872)
> >    packet_sendmsg (net/packet/af_packet.c:3114)
> >
> > rx path:
> >   BUG: KASAN: slab-use-after-free in amt_rcv (drivers/net/amt.c:2530)
> >   Read of size 4 by task exploit
> >    amt_rcv (drivers/net/amt.c:2530)
> >    udp_queue_rcv_one_skb (net/ipv4/udp.c:2388)
> >    ip_local_deliver (net/ipv4/ip_input.c:262)
> >    ip_rcv (net/ipv4/ip_input.c:612)
> >
> > Fixes: cbc21dc1cfe9 ("amt: add data plane of amt interface")
> > Fixes: bc54e49c140b ("amt: add multicast(IGMP) report message handler")
> > Fixes: b75f7095d4d4 ("amt: add mld report message handler")
> > Reported-by: AutonomousCodeSecurity@microsoft.com
> > Signed-off-by: Xiang Mei (Microsoft) <xmei5@asu.edu>
>
> Thanks a lot for this work.
> However Michael Bommarito is already working on this.
> https://lore.kernel.org/netdev/20260711151934.2955226-2-michael.bommarito@gmail.com/
>
> Thanks,
> Taehee Yoo
>
> > ---
> >  drivers/net/amt.c | 4 ++++
> >  1 file changed, 4 insertions(+)
> >
> > diff --git a/drivers/net/amt.c b/drivers/net/amt.c
> > index 951dd10e192b..8bc61c539b0a 100644
> > --- a/drivers/net/amt.c
> > +++ b/drivers/net/amt.c
> > @@ -1210,6 +1210,7 @@ static netdev_tx_t amt_dev_xmit(struct sk_buff *skb, struct net_device *dev)
> >                 } else {
> >                         data = true;
> >                 }
> > +               iph = ip_hdr(skb);
> >                 v6 = false;
> >                 group.ip4 = iph->daddr;
> >  #if IS_ENABLED(CONFIG_IPV6)
> > @@ -1234,6 +1235,7 @@ static netdev_tx_t amt_dev_xmit(struct sk_buff *skb, struct net_device *dev)
> >                 } else {
> >                         data = true;
> >                 }
> > +               ip6h = ipv6_hdr(skb);
> >                 v6 = true;
> >                 group.ip6 = ip6h->daddr;
> >  #endif
> > @@ -2498,6 +2500,7 @@ static bool amt_update_handler(struct amt_dev *amt, struct sk_buff *skb)
> >                         netdev_dbg(amt->dev, "Invalid IGMP\n");
> >                         return true;
> >                 }
> > +               iph = ip_hdr(skb);
> >
> >                 spin_lock_bh(&tunnel->lock);
> >                 amt_igmp_report_handler(amt, skb, tunnel);
> > @@ -2517,6 +2520,7 @@ static bool amt_update_handler(struct amt_dev *amt, struct sk_buff *skb)
> >                         netdev_dbg(amt->dev, "Invalid MLD\n");
> >                         return true;
> >                 }
> > +               ip6h = ipv6_hdr(skb);
> >
> >                 spin_lock_bh(&tunnel->lock);
> >                 amt_mld_report_handler(amt, skb, tunnel);
> > --
> > 2.43.0
> >

^ permalink raw reply

* Re: [PATCH net v2] tipc: fix NULL deref in tipc_named_node_up() on empty publication list
From: Weiming Shi @ 2026-07-14 17:43 UTC (permalink / raw)
  To: Simon Horman
  Cc: jmaloy, netdev, tipc-discussion, davem, edumazet, kuba, pabeni,
	hoang.h.le, xmei5, linux-kernel, tung.quang.nguyen
In-Reply-To: <20260713134610.1779498-1-horms@kernel.org>

Simon Horman <horms@kernel.org> 于2026年7月13日周一 21:46写道:
>
> 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://sashiko.dev
> ---
> tipc: fix NULL deref in tipc_named_node_up() on empty publication list
>
> This commit fixes a NULL pointer dereference in tipc_named_node_up() that
> occurs when the cluster_scope publication list is empty. The fix introduces
> an early return to avoid calling named_distribute() with an empty list.
>
> > diff --git a/net/tipc/name_distr.c b/net/tipc/name_distr.c
> > index ba4f4906e13b7..60ccaa862162c 100644
> > --- a/net/tipc/name_distr.c
> > +++ b/net/tipc/name_distr.c
> > @@ -218,6 +218,10 @@ void tipc_named_node_up(struct net *net, u32 dnode, u16 capabilities)
> >       spin_unlock_bh(&tn->nametbl_lock);
> >
> >       read_lock_bh(&nt->cluster_scope_lock);
> > +     if (list_empty(&nt->cluster_scope)) {
> > +             read_unlock_bh(&nt->cluster_scope_lock);
> > +             return;
> > +     }
>
> [Severity: Critical]
> Does this early return prevent the transmission of the last_bulk
> synchronization message to the peer node?
>
> In tipc_named_dequeue(), the peer node sets *open = true only when receiving
> a message where msg_is_last_bulk(hdr) is true:
>
> net/tipc/name_distr.c:tipc_named_dequeue() {
>         ...
>         if (*open && (*rcv_nxt == seqno)) {
>                 (*rcv_nxt)++;
>                 __skb_unlink(skb, namedq);
>                 ...
>                 return skb;
>         }
>         ...
> }
>
> If *open is false, all subsequent non-bulk name publications will fail this
> sequence check and remain in the namedq indefinitely.
>
> Without sending the last_bulk message, could this cause a permanent stall
> of name table updates and an unbounded memory leak of socket buffers on the
> peer node?
>
> >       named_distribute(net, &head, dnode, &nt->cluster_scope, seqno);
>
> [Severity: High]
> This isn't a bug introduced by this patch, but looking at named_distribute()
> called here, what happens if buffer allocation fails during the loop?
>
> Inside named_distribute():
>
> net/tipc/name_distr.c:named_distribute() {
>         ...
>         if (!skb) {
>                 skb = named_prepare_buf(net, PUBLICATION, msg_rem, dnode);
>                 if (!skb) {
>                         pr_warn("Bulk publication failure\n");
>                         return;
>                 }
>         ...
> }
>
> If the function returns early due to allocation failure, the messages
> already added to the queue are transmitted by tipc_node_xmit(), but none
> of them will have the last_bulk flag set since that happens at the very end
> of the function.
>
> Could this also prevent the receiving node from transitioning its named_open
> state to true, leading to the exact same permanent stall and memory leak of
> subsequent normal name publications?
>
> >       tipc_node_xmit(net, &head, dnode, 0);
> >       read_unlock_bh(&nt->cluster_scope_lock);
> --
> pw-bot: changes-requested


Hi ,
Thank you for the review and feedback. v3 sent.

^ permalink raw reply

* [PATCH RFC net-next 0/6] skb extension for BPF metadata
From: Jakub Sitnicki @ 2026-07-14 17:48 UTC (permalink / raw)
  To: netdev; +Cc: bpf, kernel-team

BPF programs attached at different points in the network stack have no way
to pass data between each other on a per-packet basis. xdp/skb->data_meta
covers the XDP-to-TC handoff, but is not available to programs at later
hooks like cgroup/skb, sock_ops, socket filters, tracing, or LSM.

This series adds a per-packet metadata byte buffer for BPF, carried inline
in a new skb extension (struct bpf_skb_ext). Programs access it through a
bpf_dynptr returned by the bpf_dynptr_from_skb_ext() kfunc.

Pass BPF_SKB_EXT_F_CREATE to allocate (or COW) the extension and get a
read-write dynptr; omit the flag to get a read-only view of existing
metadata, or -ENOENT if none has been attached. The inline buffer is up to
256 bytes, sized at build time via CONFIG_BPF_SKB_EXT_SIZE (default 64).

Metadata survives across hooks (TC -> socket filter) and, when opted in
with BPF_SKB_EXT_F_NO_SCRUB, across tunnel encap/decap and cross-netns
forwarding. Without the flag the extension is scrubbed at the usual
points (skb_scrub_packet).

This is our third attempt at the problem. We first tried to make XDP/skb
metadata persist across network layers and expose it as a dynptr [1], then
turned to BPF local storage for skb's [2]. Based on the feedback from both,
we converged on the simpler skb extension + dynptr approach.

We are posting this RFC to accompany a Netdev talk on the topic [3]. We are
currently running an experiment in a production environment to evaluate the
performance of this approach and will follow up with results and a
comparison to alternatives.

Thanks,
-jkbs

[1] https://lore.kernel.org/all/20260107-skb-meta-safeproof-netdevs-rx-only-v3-0-0d461c5e4764@cloudflare.com/
[2] https://lore.kernel.org/all/20260226-skb-local-storage-v1-0-4ca44f0dd9d1@cloudflare.com/
[3] https://www.netdevconf.info/0x1A/sessions/talk/thrice-the-charm-an-skb-extension-for-bpf-metadata.html

Signed-off-by: Jakub Sitnicki <jakub@cloudflare.com>
---
Jakub Sitnicki (6):
      bpf: Introduce per-packet metadata storage for BPF programs
      bpf: Allow access to bpf_sock_ops_kern->skb
      bpf: Allow skb extensions to survive packet scrubbing
      selftests/bpf: Add tests for bpf_dynptr_from_skb_ext
      selftests/bpf: Test skb_ext scrubbing across tunnels and veths
      selftests/bpf: Use non-trivial test payload in xdp_context tests

 include/linux/bpf.h                                |  11 +
 include/linux/filter.h                             |  26 +
 include/linux/skbuff.h                             |   5 +
 include/uapi/linux/bpf.h                           |   6 +
 kernel/bpf/helpers.c                               |   7 +
 kernel/bpf/log.c                                   |   2 +
 kernel/bpf/verifier.c                              |  15 +-
 net/Kconfig                                        |  20 +
 net/core/filter.c                                  | 134 +++++
 net/core/skbuff.c                                  |  85 ++-
 net/ipv4/udp.c                                     |   2 +-
 .../bpf/prog_tests/xdp_context_test_run.c          | 592 ++++++++++++++++++++-
 tools/testing/selftests/bpf/progs/test_xdp_meta.c  | 418 ++++++++++++++-
 13 files changed, 1303 insertions(+), 20 deletions(-)


^ permalink raw reply

* [PATCH RFC net-next 2/6] bpf: Allow access to bpf_sock_ops_kern->skb
From: Jakub Sitnicki @ 2026-07-14 17:48 UTC (permalink / raw)
  To: netdev; +Cc: bpf, kernel-team
In-Reply-To: <20260714-bpf-meta-inside-skb-ext-v1-0-5871c07a8dd6@cloudflare.com>

sock_ops programs receive bpf_sock_ops_kern as their kernel context, which
holds a pointer to the sk_buff being processed. Mark bpf_sock_ops_kern->skb
as BTF_TYPE_SAFE_TRUSTED_OR_NULL so that BPF programs can dereference it
and pass it to kfuncs expecting a trusted sk_buff pointer, such as
bpf_skb_storage_get().

Signed-off-by: Jakub Sitnicki <jakub@cloudflare.com>
---
 kernel/bpf/verifier.c | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index a78ef1e37f24..e10886ca5089 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -5675,6 +5675,10 @@ BTF_TYPE_SAFE_TRUSTED_OR_NULL(struct vm_area_struct) {
 	struct file *vm_file;
 };
 
+BTF_TYPE_SAFE_TRUSTED_OR_NULL(struct bpf_sock_ops_kern) {
+	struct sk_buff *skb;
+};
+
 static bool type_is_rcu(struct bpf_verifier_env *env,
 			struct bpf_reg_state *reg,
 			const char *field_name, u32 btf_id)
@@ -5717,6 +5721,7 @@ static bool type_is_trusted_or_null(struct bpf_verifier_env *env,
 	BTF_TYPE_EMIT(BTF_TYPE_SAFE_TRUSTED_OR_NULL(struct socket));
 	BTF_TYPE_EMIT(BTF_TYPE_SAFE_TRUSTED_OR_NULL(struct dentry));
 	BTF_TYPE_EMIT(BTF_TYPE_SAFE_TRUSTED_OR_NULL(struct vm_area_struct));
+	BTF_TYPE_EMIT(BTF_TYPE_SAFE_TRUSTED_OR_NULL(struct bpf_sock_ops_kern));
 
 	return btf_nested_type_is_trusted(&env->log, reg, field_name, btf_id,
 					  "__safe_trusted_or_null");

-- 
2.43.0


^ permalink raw reply related

* [PATCH RFC net-next 1/6] bpf: Introduce per-packet metadata storage for BPF programs
From: Jakub Sitnicki @ 2026-07-14 17:48 UTC (permalink / raw)
  To: netdev; +Cc: bpf, kernel-team
In-Reply-To: <20260714-bpf-meta-inside-skb-ext-v1-0-5871c07a8dd6@cloudflare.com>

BPF programs attached at different points in the network stack have no way
to pass data between each other on a per-packet basis, other than by
stashing it into a shared BPF map. xdp/skb->data_meta works for XDP-to-TC
handoff, but is not available to programs running at later hooks like
cgroup/skb, sock_ops, socket filters, tracing or LSM.

Add a new skb extension (struct bpf_skb_ext) that provides up to 256 bytes
of per-packet storage. Size is configurable at build time though the
CONFIG_BPF_SKB_EXT_SIZE option. The storage is embedded inside the
extension chunk itself.

Expose the storage to BPF programs via bpf_dynptr_from_skb_ext() kfunc.
The caller passes BPF_SKB_EXT_F_CREATE to allocate or COW (unshare) the
extension and get a read-write dynptr. Without the flag, it gets a
read-only dynptr to the existing extension, or -ENOENT if none exists.

Guard the feature behind a new CONFIG_BPF_SKB_EXT option.

Signed-off-by: Jakub Sitnicki <jakub@cloudflare.com>
---
 include/linux/bpf.h      |  10 ++++
 include/linux/filter.h   |  26 ++++++++++
 include/linux/skbuff.h   |   3 ++
 include/uapi/linux/bpf.h |   5 ++
 kernel/bpf/helpers.c     |   7 +++
 kernel/bpf/log.c         |   2 +
 kernel/bpf/verifier.c    |  10 +++-
 net/Kconfig              |  20 ++++++++
 net/core/filter.c        | 129 +++++++++++++++++++++++++++++++++++++++++++++++
 net/core/skbuff.c        |   3 ++
 10 files changed, 214 insertions(+), 1 deletion(-)

diff --git a/include/linux/bpf.h b/include/linux/bpf.h
index 7719f6528445..6b918a5b61bf 100644
--- a/include/linux/bpf.h
+++ b/include/linux/bpf.h
@@ -1484,6 +1484,8 @@ enum bpf_dynptr_type {
 	BPF_DYNPTR_TYPE_SKB_META,
 	/* Underlying data is a file */
 	BPF_DYNPTR_TYPE_FILE,
+	/* Underlying data is a bpf_skb_ext chunk */
+	BPF_DYNPTR_TYPE_SKB_EXT,
 };
 
 int bpf_dynptr_check_size(u64 size);
@@ -4209,4 +4211,12 @@ static inline int bpf_map_check_op_flags(struct bpf_map *map, u64 flags, u64 all
 	return 0;
 }
 
+#ifdef CONFIG_BPF_SKB_EXT
+
+struct bpf_skb_ext {
+	u8 buf[CONFIG_BPF_SKB_EXT_SIZE] __aligned(8);
+};
+
+#endif /* CONFIG_BPF_SKB_EXT */
+
 #endif /* _LINUX_BPF_H */
diff --git a/include/linux/filter.h b/include/linux/filter.h
index 67d337ede91b..839e56708594 100644
--- a/include/linux/filter.h
+++ b/include/linux/filter.h
@@ -1903,4 +1903,30 @@ static inline void *bpf_skb_meta_pointer(struct sk_buff *skb, u32 offset)
 }
 #endif /* CONFIG_NET */
 
+#ifdef CONFIG_BPF_SKB_EXT
+void *bpf_skb_ext_pointer(struct sk_buff *skb, u32 offset);
+int __bpf_skb_ext_load_bytes(const struct sk_buff *skb, u32 offset, void *to,
+			     u32 len);
+int __bpf_skb_ext_store_bytes(struct sk_buff *skb, u32 offset, const void *from,
+			      u32 len, u64 flags);
+#else /* CONFIG_BPF_SKB_EXT */
+static inline void *bpf_skb_ext_pointer(struct sk_buff *skb, u32 offset)
+{
+	return NULL;
+}
+
+static inline int __bpf_skb_ext_load_bytes(const struct sk_buff *skb,
+					   u32 offset, void *to, u32 len)
+{
+	return -EOPNOTSUPP;
+}
+
+static inline int __bpf_skb_ext_store_bytes(struct sk_buff *skb, u32 offset,
+					    const void *from, u32 len,
+					    u64 flags)
+{
+	return -EOPNOTSUPP;
+}
+#endif /* CONFIG_BPF_SKB_EXT */
+
 #endif /* __LINUX_FILTER_H__ */
diff --git a/include/linux/skbuff.h b/include/linux/skbuff.h
index 115db8c44db2..584d8440d352 100644
--- a/include/linux/skbuff.h
+++ b/include/linux/skbuff.h
@@ -5033,6 +5033,9 @@ enum skb_ext_id {
 #endif
 #if IS_ENABLED(CONFIG_CAN)
 	SKB_EXT_CAN,
+#endif
+#if IS_ENABLED(CONFIG_BPF_SKB_EXT)
+	SKB_EXT_BPF,
 #endif
 	SKB_EXT_NUM, /* must be last */
 };
diff --git a/include/uapi/linux/bpf.h b/include/uapi/linux/bpf.h
index 89b36de5fdbb..3eee4467422d 100644
--- a/include/uapi/linux/bpf.h
+++ b/include/uapi/linux/bpf.h
@@ -7732,4 +7732,9 @@ struct bpf_insn_array_value {
 	__u32 :32;
 };
 
+/* Flags to control bpf_dynptr_from_skb_ext() behavior. */
+enum {
+	BPF_SKB_EXT_F_CREATE = (1ULL << 0),
+};
+
 #endif /* _UAPI__LINUX_BPF_H__ */
diff --git a/kernel/bpf/helpers.c b/kernel/bpf/helpers.c
index 8e196c9b7c50..c7a823586225 100644
--- a/kernel/bpf/helpers.c
+++ b/kernel/bpf/helpers.c
@@ -1926,6 +1926,8 @@ static int __bpf_dynptr_read(void *dst, u64 len, const struct bpf_dynptr_kern *s
 		return 0;
 	case BPF_DYNPTR_TYPE_FILE:
 		return bpf_file_fetch_bytes(src->data, offset, dst, len);
+	case BPF_DYNPTR_TYPE_SKB_EXT:
+		return __bpf_skb_ext_load_bytes(src->data, src->offset + offset, dst, len);
 	default:
 		WARN_ONCE(true, "bpf_dynptr_read: unknown dynptr type %d\n", type);
 		return -EFAULT;
@@ -1985,6 +1987,8 @@ int __bpf_dynptr_write(const struct bpf_dynptr_kern *dst, u64 offset, void *src,
 	case BPF_DYNPTR_TYPE_SKB_META:
 		return __bpf_skb_meta_store_bytes(dst->data, dst->offset + offset, src,
 						  len, flags);
+	case BPF_DYNPTR_TYPE_SKB_EXT:
+		return __bpf_skb_ext_store_bytes(dst->data, dst->offset + offset, src, len, flags);
 	default:
 		WARN_ONCE(true, "bpf_dynptr_write: unknown dynptr type %d\n", type);
 		return -EFAULT;
@@ -2032,6 +2036,7 @@ BPF_CALL_3(bpf_dynptr_data, const struct bpf_dynptr_kern *, ptr, u64, offset, u6
 	case BPF_DYNPTR_TYPE_SKB:
 	case BPF_DYNPTR_TYPE_XDP:
 	case BPF_DYNPTR_TYPE_SKB_META:
+	case BPF_DYNPTR_TYPE_SKB_EXT:
 		/* skb and xdp dynptrs should use bpf_dynptr_slice / bpf_dynptr_slice_rdwr */
 		return 0;
 	default:
@@ -3087,6 +3092,8 @@ __bpf_kfunc void *bpf_dynptr_slice(const struct bpf_dynptr *p, u64 offset,
 	}
 	case BPF_DYNPTR_TYPE_SKB_META:
 		return bpf_skb_meta_pointer(ptr->data, ptr->offset + offset);
+	case BPF_DYNPTR_TYPE_SKB_EXT:
+		return bpf_skb_ext_pointer(ptr->data, ptr->offset + offset);
 	case BPF_DYNPTR_TYPE_FILE:
 		err = bpf_file_fetch_bytes(ptr->data, offset, buffer__nullable, buffer__szk);
 		return err ? NULL : buffer__nullable;
diff --git a/kernel/bpf/log.c b/kernel/bpf/log.c
index b740fa73ee26..d66e154649e0 100644
--- a/kernel/bpf/log.c
+++ b/kernel/bpf/log.c
@@ -462,6 +462,8 @@ const char *dynptr_type_str(enum bpf_dynptr_type type)
 		return "skb_meta";
 	case BPF_DYNPTR_TYPE_FILE:
 		return "file";
+	case BPF_DYNPTR_TYPE_SKB_EXT:
+		return "skb_ext";
 	case BPF_DYNPTR_TYPE_INVALID:
 		return "<invalid>";
 	default:
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 2abc79dbf281..a78ef1e37f24 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -635,6 +635,7 @@ static enum bpf_type_flag get_dynptr_type_flag(enum bpf_dynptr_type type)
 	case BPF_DYNPTR_TYPE_XDP:
 		return DYNPTR_TYPE_XDP;
 	case BPF_DYNPTR_TYPE_SKB_META:
+	case BPF_DYNPTR_TYPE_SKB_EXT:
 		return DYNPTR_TYPE_SKB_META;
 	case BPF_DYNPTR_TYPE_FILE:
 		return DYNPTR_TYPE_FILE;
@@ -11014,6 +11015,7 @@ enum special_kfunc_type {
 	KF_bpf_dynptr_from_xdp,
 	KF_bpf_dynptr_from_skb_meta,
 	KF_bpf_xdp_pull_data,
+	KF_bpf_dynptr_from_skb_ext,
 	KF_bpf_dynptr_slice,
 	KF_bpf_dynptr_slice_rdwr,
 	KF_bpf_dynptr_clone,
@@ -11094,6 +11096,11 @@ BTF_ID_UNUSED
 BTF_ID_UNUSED
 BTF_ID_UNUSED
 #endif
+#ifdef CONFIG_BPF_SKB_EXT
+BTF_ID(func, bpf_dynptr_from_skb_ext)
+#else
+BTF_ID_UNUSED
+#endif
 BTF_ID(func, bpf_dynptr_slice)
 BTF_ID(func, bpf_dynptr_slice_rdwr)
 BTF_ID(func, bpf_dynptr_clone)
@@ -12192,7 +12199,8 @@ static int check_kfunc_args(struct bpf_verifier_env *env, struct bpf_kfunc_call_
 				dynptr_arg_type |= DYNPTR_TYPE_SKB;
 			} else if (meta->func_id == special_kfunc_list[KF_bpf_dynptr_from_xdp]) {
 				dynptr_arg_type |= DYNPTR_TYPE_XDP;
-			} else if (meta->func_id == special_kfunc_list[KF_bpf_dynptr_from_skb_meta]) {
+			} else if (meta->func_id == special_kfunc_list[KF_bpf_dynptr_from_skb_meta] ||
+				   meta->func_id == special_kfunc_list[KF_bpf_dynptr_from_skb_ext]) {
 				dynptr_arg_type |= DYNPTR_TYPE_SKB_META;
 			} else if (meta->func_id == special_kfunc_list[KF_bpf_dynptr_from_file]) {
 				dynptr_arg_type |= DYNPTR_TYPE_FILE;
diff --git a/net/Kconfig b/net/Kconfig
index e38477393551..6d57320dfea3 100644
--- a/net/Kconfig
+++ b/net/Kconfig
@@ -540,4 +540,24 @@ config NET_TEST
 
 	  If unsure, say N.
 
+config BPF_SKB_EXT
+	bool "skb extension for BPF metadata"
+	depends on BPF_SYSCALL
+	select SKB_EXTENSIONS
+	help
+	  Enable an sk_buff extension for storing BPF metadata. This allows BPF
+	  programs to associate arbitrary data with individual packets as they
+	  traverse the network stack. The storage is automatically freed when
+	  the sk_buff is freed.
+
+config BPF_SKB_EXT_SIZE
+	int "Size of the BPF skb extension metadata buffer"
+	depends on BPF_SKB_EXT
+	range 1 256
+	default 64
+	help
+	  Configures the size of the inline metadata buffer in struct
+	  bpf_skb_ext, which is the maximum amount of data a BPF program can
+	  store or retrieve with bpf_dynptr_from_skb_ext().
+
 endif   # if NET
diff --git a/net/core/filter.c b/net/core/filter.c
index 2e96b4b847ce..b0b039c64b02 100644
--- a/net/core/filter.c
+++ b/net/core/filter.c
@@ -12254,6 +12254,48 @@ int __bpf_skb_meta_store_bytes(struct sk_buff *skb, u32 offset,
 	return 0;
 }
 
+#ifdef CONFIG_BPF_SKB_EXT
+void *bpf_skb_ext_pointer(struct sk_buff *skb, u32 offset)
+{
+	struct bpf_skb_ext *ext;
+
+	ext = skb_ext_find(skb, SKB_EXT_BPF);
+	if (!ext)
+		return NULL;
+
+	return ext->buf + offset;
+}
+
+int __bpf_skb_ext_load_bytes(const struct sk_buff *skb, u32 offset, void *to,
+			     u32 len)
+{
+	struct bpf_skb_ext *ext;
+
+	ext = skb_ext_find(skb, SKB_EXT_BPF);
+	if (!ext)
+		return -ENOENT;
+
+	memmove(to, ext->buf + offset, len);
+	return 0;
+}
+
+int __bpf_skb_ext_store_bytes(struct sk_buff *skb, u32 offset,
+			      const void *from, u32 len, u64 flags)
+{
+	struct bpf_skb_ext *ext;
+
+	if (unlikely(flags))
+		return -EINVAL;
+
+	ext = skb_ext_find(skb, SKB_EXT_BPF);
+	if (!ext)
+		return -ENOENT;
+
+	memmove(ext->buf + offset, from, len);
+	return 0;
+}
+#endif /* CONFIG_BPF_SKB_EXT */
+
 __bpf_kfunc_start_defs();
 __bpf_kfunc int bpf_dynptr_from_skb(struct __sk_buff *s, u64 flags,
 				    struct bpf_dynptr *ptr__uninit)
@@ -12271,6 +12313,71 @@ __bpf_kfunc int bpf_dynptr_from_skb(struct __sk_buff *s, u64 flags,
 	return 0;
 }
 
+#ifdef CONFIG_BPF_SKB_EXT
+/**
+ * bpf_dynptr_from_skb_ext() - Initialize a dynptr to the skb_ext BPF area.
+ * @skb_: socket buffer to attach the extension to
+ * @size: dynptr size in bytes, 0 for maximum (BPF_SKB_EXT_SIZE_MAX)
+ * @flags: BPF_SKB_EXT_F_CREATE to create/COW (read-write), 0 to find (read-only)
+ * @ptr__uninit: dynptr to initialize
+ *
+ * Return:
+ * * %0         - dynptr ready to use
+ * * %-ENOENT   - extension not found (when not creating)
+ * * %-ENOMEM   - allocation failed
+ * * %-EINVAL   - invalid flags
+ * * %-E2BIG    - size exceeds CONFIG_BPF_SKB_EXT_SIZE
+ */
+__bpf_kfunc int bpf_dynptr_from_skb_ext(struct __sk_buff *skb_, u32 size,
+					u64 flags,
+					struct bpf_dynptr *ptr__uninit)
+{
+	struct bpf_dynptr_kern *ptr = (struct bpf_dynptr_kern *)ptr__uninit;
+	struct sk_buff *skb = (struct sk_buff *)skb_;
+	bool create = flags & BPF_SKB_EXT_F_CREATE;
+	struct bpf_skb_ext *ext;
+	bool exists;
+	int err;
+
+	if (flags & ~BPF_SKB_EXT_F_CREATE) {
+		err = -EINVAL;
+		goto error;
+	}
+
+	if (size > ARRAY_SIZE(ext->buf)) {
+		err = -E2BIG;
+		goto error;
+	}
+	if (!size)
+		size = ARRAY_SIZE(ext->buf);
+
+	exists = skb_ext_exist(skb, SKB_EXT_BPF);
+	if (!create) {
+		if (!exists) {
+			err = -ENOENT;
+			goto error;
+		}
+		goto out;
+	}
+
+	ext = skb_ext_add(skb, SKB_EXT_BPF);
+	if (!ext) {
+		err = -ENOMEM;
+		goto error;
+	}
+	if (!exists)
+		memset(ext, 0, sizeof(*ext));
+out:
+	bpf_dynptr_init(ptr, skb, BPF_DYNPTR_TYPE_SKB_EXT, 0, size);
+	if (!create)
+		bpf_dynptr_set_rdonly(ptr);
+	return 0;
+error:
+	bpf_dynptr_set_null(ptr);
+	return err;
+}
+#endif /* CONFIG_BPF_SKB_EXT */
+
 /**
  * bpf_dynptr_from_skb_meta() - Initialize a dynptr to the skb metadata area.
  * @skb_: socket buffer carrying the metadata
@@ -12571,6 +12678,12 @@ BTF_KFUNCS_START(bpf_kfunc_check_set_skb_meta)
 BTF_ID_FLAGS(func, bpf_dynptr_from_skb_meta)
 BTF_KFUNCS_END(bpf_kfunc_check_set_skb_meta)
 
+#ifdef CONFIG_BPF_SKB_EXT
+BTF_KFUNCS_START(bpf_kfunc_check_set_skb_ext)
+BTF_ID_FLAGS(func, bpf_dynptr_from_skb_ext)
+BTF_KFUNCS_END(bpf_kfunc_check_set_skb_ext)
+#endif
+
 BTF_KFUNCS_START(bpf_kfunc_check_set_xdp)
 BTF_ID_FLAGS(func, bpf_dynptr_from_xdp)
 BTF_ID_FLAGS(func, bpf_xdp_pull_data)
@@ -12598,6 +12711,13 @@ static const struct btf_kfunc_id_set bpf_kfunc_set_skb_meta = {
 	.set = &bpf_kfunc_check_set_skb_meta,
 };
 
+#ifdef CONFIG_BPF_SKB_EXT
+static const struct btf_kfunc_id_set bpf_kfunc_set_skb_ext = {
+	.owner = THIS_MODULE,
+	.set = &bpf_kfunc_check_set_skb_ext,
+};
+#endif
+
 static const struct btf_kfunc_id_set bpf_kfunc_set_xdp = {
 	.owner = THIS_MODULE,
 	.set = &bpf_kfunc_check_set_xdp,
@@ -12635,6 +12755,15 @@ static int __init bpf_kfunc_init(void)
 	ret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_TRACING, &bpf_kfunc_set_skb);
 	ret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_SCHED_CLS, &bpf_kfunc_set_skb_meta);
 	ret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_SCHED_ACT, &bpf_kfunc_set_skb_meta);
+#ifdef CONFIG_BPF_SKB_EXT
+	ret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_SCHED_CLS, &bpf_kfunc_set_skb_ext);
+	ret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_SCHED_ACT, &bpf_kfunc_set_skb_ext);
+	ret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_CGROUP_SKB, &bpf_kfunc_set_skb_ext);
+	ret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_SOCK_OPS, &bpf_kfunc_set_skb_ext);
+	ret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_SOCKET_FILTER, &bpf_kfunc_set_skb_ext);
+	ret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_LSM, &bpf_kfunc_set_skb_ext);
+	ret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_TRACING, &bpf_kfunc_set_skb_ext);
+#endif
 	ret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_XDP, &bpf_kfunc_set_xdp);
 	ret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_CGROUP_SOCK_ADDR,
 					       &bpf_kfunc_set_sock_addr);
diff --git a/net/core/skbuff.c b/net/core/skbuff.c
index 18dabb4e9cfa..4efc06c426b2 100644
--- a/net/core/skbuff.c
+++ b/net/core/skbuff.c
@@ -5149,6 +5149,9 @@ static const u8 skb_ext_type_len[] = {
 #if IS_ENABLED(CONFIG_CAN)
 	[SKB_EXT_CAN] = SKB_EXT_CHUNKSIZEOF(struct can_skb_ext),
 #endif
+#if IS_ENABLED(CONFIG_BPF_SKB_EXT)
+	[SKB_EXT_BPF] = SKB_EXT_CHUNKSIZEOF(struct bpf_skb_ext),
+#endif
 };
 
 static __always_inline __no_profile unsigned int skb_ext_total_length(void)

-- 
2.43.0


^ permalink raw reply related

* [PATCH RFC net-next 3/6] bpf: Allow skb extensions to survive packet scrubbing
From: Jakub Sitnicki @ 2026-07-14 17:48 UTC (permalink / raw)
  To: netdev; +Cc: bpf, kernel-team
In-Reply-To: <20260714-bpf-meta-inside-skb-ext-v1-0-5871c07a8dd6@cloudflare.com>

skb_scrub_packet() drops all skb extensions unconditionally via
skb_ext_reset(). It runs on tunnel encap/decap (ip_tunnel_rcv,
vxlan_rcv, etc.) and cross-netns forwarding (dev_forward_skb).

This makes it impossible for a BPF program to pass metadata via
bpf_skb_ext through a tunnel or across a netns boundary. The extension
is always lost at the scrub point.

Introduce skb_ext_scrub() which consults each active extension before
discarding it. Extensions that request preservation are kept while the
rest are torn down. When the extension slab is shared with clones, COW
ensures isolation. Replace the skb_ext_reset() call in
skb_scrub_packet() with skb_ext_scrub().

Expose the opt-in mechanism to BPF via the BPF_SKB_EXT_F_NO_SCRUB flag
for bpf_dynptr_from_skb_ext(). A program that sets this flag when
creating the extension signals that its metadata should survive
scrubbing.

Signed-off-by: Jakub Sitnicki <jakub@cloudflare.com>
---
 include/linux/bpf.h      |  1 +
 include/linux/skbuff.h   |  2 ++
 include/uapi/linux/bpf.h |  3 +-
 net/core/filter.c        | 11 +++++--
 net/core/skbuff.c        | 82 ++++++++++++++++++++++++++++++++++++++++++------
 net/ipv4/udp.c           |  2 +-
 6 files changed, 86 insertions(+), 15 deletions(-)

diff --git a/include/linux/bpf.h b/include/linux/bpf.h
index 6b918a5b61bf..a46ca53c5b27 100644
--- a/include/linux/bpf.h
+++ b/include/linux/bpf.h
@@ -4214,6 +4214,7 @@ static inline int bpf_map_check_op_flags(struct bpf_map *map, u64 flags, u64 all
 #ifdef CONFIG_BPF_SKB_EXT
 
 struct bpf_skb_ext {
+	u64 flags;
 	u8 buf[CONFIG_BPF_SKB_EXT_SIZE] __aligned(8);
 };
 
diff --git a/include/linux/skbuff.h b/include/linux/skbuff.h
index 584d8440d352..66afa5489007 100644
--- a/include/linux/skbuff.h
+++ b/include/linux/skbuff.h
@@ -5063,6 +5063,7 @@ void *__skb_ext_set(struct sk_buff *skb, enum skb_ext_id id,
 void *skb_ext_add(struct sk_buff *skb, enum skb_ext_id id);
 void __skb_ext_del(struct sk_buff *skb, enum skb_ext_id id);
 void __skb_ext_put(struct skb_ext *ext);
+void skb_ext_scrub(struct sk_buff *skb);
 
 static inline void skb_ext_put(struct sk_buff *skb)
 {
@@ -5132,6 +5133,7 @@ static inline bool skb_has_extensions(struct sk_buff *skb)
 static inline void __skb_ext_put(struct skb_ext *ext) {}
 static inline void skb_ext_put(struct sk_buff *skb) {}
 static inline void skb_ext_reset(struct sk_buff *skb) {}
+static inline void skb_ext_scrub(struct sk_buff *skb) {}
 static inline void skb_ext_del(struct sk_buff *skb, int unused) {}
 static inline void __skb_ext_copy(struct sk_buff *d, const struct sk_buff *s) {}
 static inline void skb_ext_copy(struct sk_buff *dst, const struct sk_buff *s) {}
diff --git a/include/uapi/linux/bpf.h b/include/uapi/linux/bpf.h
index 3eee4467422d..02da170205de 100644
--- a/include/uapi/linux/bpf.h
+++ b/include/uapi/linux/bpf.h
@@ -7734,7 +7734,8 @@ struct bpf_insn_array_value {
 
 /* Flags to control bpf_dynptr_from_skb_ext() behavior. */
 enum {
-	BPF_SKB_EXT_F_CREATE = (1ULL << 0),
+	BPF_SKB_EXT_F_CREATE	= (1ULL << 0),
+	BPF_SKB_EXT_F_NO_SCRUB	= (1ULL << 1),
 };
 
 #endif /* _UAPI__LINUX_BPF_H__ */
diff --git a/net/core/filter.c b/net/core/filter.c
index b0b039c64b02..eca07976ab1c 100644
--- a/net/core/filter.c
+++ b/net/core/filter.c
@@ -12319,6 +12319,7 @@ __bpf_kfunc int bpf_dynptr_from_skb(struct __sk_buff *s, u64 flags,
  * @skb_: socket buffer to attach the extension to
  * @size: dynptr size in bytes, 0 for maximum (BPF_SKB_EXT_SIZE_MAX)
  * @flags: BPF_SKB_EXT_F_CREATE to create/COW (read-write), 0 to find (read-only)
+ *	   BPF_SKB_EXT_F_NO_SCRUB to keep the extension across skb_scrub_packet()
  * @ptr__uninit: dynptr to initialize
  *
  * Return:
@@ -12335,14 +12336,16 @@ __bpf_kfunc int bpf_dynptr_from_skb_ext(struct __sk_buff *skb_, u32 size,
 	struct bpf_dynptr_kern *ptr = (struct bpf_dynptr_kern *)ptr__uninit;
 	struct sk_buff *skb = (struct sk_buff *)skb_;
 	bool create = flags & BPF_SKB_EXT_F_CREATE;
+	bool no_scrub = flags & BPF_SKB_EXT_F_NO_SCRUB;
 	struct bpf_skb_ext *ext;
 	bool exists;
 	int err;
 
-	if (flags & ~BPF_SKB_EXT_F_CREATE) {
-		err = -EINVAL;
+	err = -EINVAL;
+	if (flags & ~(BPF_SKB_EXT_F_CREATE | BPF_SKB_EXT_F_NO_SCRUB))
+		goto error;
+	if (!create && no_scrub)
 		goto error;
-	}
 
 	if (size > ARRAY_SIZE(ext->buf)) {
 		err = -E2BIG;
@@ -12367,6 +12370,8 @@ __bpf_kfunc int bpf_dynptr_from_skb_ext(struct __sk_buff *skb_, u32 size,
 	}
 	if (!exists)
 		memset(ext, 0, sizeof(*ext));
+	if (no_scrub)
+		ext->flags |= BPF_SKB_EXT_F_NO_SCRUB;
 out:
 	bpf_dynptr_init(ptr, skb, BPF_DYNPTR_TYPE_SKB_EXT, 0, size);
 	if (!create)
diff --git a/net/core/skbuff.c b/net/core/skbuff.c
index 4efc06c426b2..46b5bd49a110 100644
--- a/net/core/skbuff.c
+++ b/net/core/skbuff.c
@@ -83,6 +83,7 @@
 #include <net/page_pool/helpers.h>
 #include <net/psp/types.h>
 #include <net/dropreason.h>
+#include <linux/bpf.h>
 #include <net/xdp_sock.h>
 
 #include <linux/uaccess.h>
@@ -6278,7 +6279,7 @@ void skb_scrub_packet(struct sk_buff *skb, bool xnet)
 	skb->skb_iif = 0;
 	skb->ignore_df = 0;
 	skb_dst_drop(skb);
-	skb_ext_reset(skb);
+	skb_ext_scrub(skb);
 	nf_reset_ct(skb);
 	nf_reset_trace(skb);
 
@@ -7252,6 +7253,20 @@ void __skb_ext_del(struct sk_buff *skb, enum skb_ext_id id)
 }
 EXPORT_SYMBOL(__skb_ext_del);
 
+static void skb_ext_put_each(struct skb_ext *ext, unsigned int skip)
+{
+#ifdef CONFIG_XFRM
+	if (!(skip & (1 << SKB_EXT_SEC_PATH)) &&
+	    __skb_ext_exist(ext, SKB_EXT_SEC_PATH))
+		skb_ext_put_sp(skb_ext_get_ptr(ext, SKB_EXT_SEC_PATH));
+#endif
+#ifdef CONFIG_MCTP_FLOWS
+	if (!(skip & (1 << SKB_EXT_MCTP)) &&
+	    __skb_ext_exist(ext, SKB_EXT_MCTP))
+		skb_ext_put_mctp(skb_ext_get_ptr(ext, SKB_EXT_MCTP));
+#endif
+}
+
 void __skb_ext_put(struct skb_ext *ext)
 {
 	/* If this is last clone, nothing can increment
@@ -7263,18 +7278,65 @@ void __skb_ext_put(struct skb_ext *ext)
 	if (!refcount_dec_and_test(&ext->refcnt))
 		return;
 free_now:
-#ifdef CONFIG_XFRM
-	if (__skb_ext_exist(ext, SKB_EXT_SEC_PATH))
-		skb_ext_put_sp(skb_ext_get_ptr(ext, SKB_EXT_SEC_PATH));
-#endif
-#ifdef CONFIG_MCTP_FLOWS
-	if (__skb_ext_exist(ext, SKB_EXT_MCTP))
-		skb_ext_put_mctp(skb_ext_get_ptr(ext, SKB_EXT_MCTP));
-#endif
-
+	skb_ext_put_each(ext, 0);
 	kmem_cache_free(skbuff_ext_cache, ext);
 }
 EXPORT_SYMBOL(__skb_ext_put);
+
+static unsigned int skb_ext_no_scrub(struct skb_ext *ext)
+{
+	unsigned int keep = 0;
+
+#if IS_ENABLED(CONFIG_BPF_SKB_EXT)
+	if (__skb_ext_exist(ext, SKB_EXT_BPF)) {
+		struct bpf_skb_ext *bpf = skb_ext_get_ptr(ext, SKB_EXT_BPF);
+
+		if (bpf->flags & BPF_SKB_EXT_F_NO_SCRUB)
+			keep |= (1 << SKB_EXT_BPF);
+	}
+#endif
+	return keep;
+}
+
+static int __skb_ext_scrub(struct sk_buff *skb, unsigned int keep)
+{
+	struct skb_ext *old = skb->extensions;
+	struct skb_ext *ext;
+	int i;
+
+	if (refcount_read(&old->refcnt) == 1) {
+		skb_ext_put_each(old, keep);
+		ext = old;
+	} else {
+		ext = skb_ext_maybe_cow(old, keep);
+		if (!ext)
+			return -ENOMEM;
+		skb->extensions = ext;
+	}
+
+	for (i = 0; i < SKB_EXT_NUM; i++) {
+		if (!(keep & (1 << i)))
+			ext->offset[i] = 0;
+	}
+	skb->active_extensions = keep;
+	return 0;
+}
+
+void skb_ext_scrub(struct sk_buff *skb)
+{
+	unsigned int keep;
+
+	if (likely(!skb->active_extensions))
+		return;
+
+	keep = skb_ext_no_scrub(skb->extensions);
+	if (keep && !__skb_ext_scrub(skb, keep))
+		return;
+
+	skb_ext_reset(skb);
+}
+EXPORT_SYMBOL(skb_ext_scrub);
+
 #endif /* CONFIG_SKB_EXTENSIONS */
 
 static void kfree_skb_napi_cache(struct sk_buff *skb)
diff --git a/net/ipv4/udp.c b/net/ipv4/udp.c
index 70f6cbd4ef73..08357f2621da 100644
--- a/net/ipv4/udp.c
+++ b/net/ipv4/udp.c
@@ -1536,7 +1536,7 @@ static bool udp_try_make_stateless(struct sk_buff *skb)
 		return true;
 
 	if (!secpath_exists(skb)) {
-		skb_ext_reset(skb);
+		skb_ext_scrub(skb);
 		return true;
 	}
 

-- 
2.43.0


^ permalink raw reply related

* [PATCH RFC net-next 4/6] selftests/bpf: Add tests for bpf_dynptr_from_skb_ext
From: Jakub Sitnicki @ 2026-07-14 17:48 UTC (permalink / raw)
  To: netdev; +Cc: bpf, kernel-team
In-Reply-To: <20260714-bpf-meta-inside-skb-ext-v1-0-5871c07a8dd6@cloudflare.com>

Test the bpf_dynptr_from_skb_ext() kfunc and skb_ext survival across
network stack hooks.

Cover the dynptr interface with TC-to-TC tests on a tuntap device:

- write/read via bpf_dynptr_read and bpf_dynptr_write
- write/read via bpf_dynptr_slice and bpf_dynptr_slice_rdwr
- clone read via bpf_dynptr_clone
- error paths: no allocation without F_CREATE, invalid flags, read-only
  enforcement without F_CREATE
- double allocation: data from first alloc survives second skb_ext_add

Cover cross-hook survival by writing skb_ext at TC ingress and reading
it at later hooks:

- TC -> cgroup/skb ingress (UDP loopback)
- TC -> socket filter (UDP loopback)
- TC -> sock_ops passive established (TCP loopback)
- TC -> LSM inet_conn_established (TCP loopback)
- cgroup/skb egress -> tp_btf/kfree_skb (survival until skb free)

Cover skb clone isolation:

- clone_ext_read: ext read from a mirred clone shares data with
  original
- clone_ext_cow: F_CREATE on clone triggers COW, writes to clone do
  not affect original

Signed-off-by: Jakub Sitnicki <jakub@cloudflare.com>
---
 .../bpf/prog_tests/xdp_context_test_run.c          | 412 +++++++++++++++++++++
 tools/testing/selftests/bpf/progs/test_xdp_meta.c  | 392 ++++++++++++++++++++
 2 files changed, 804 insertions(+)

diff --git a/tools/testing/selftests/bpf/prog_tests/xdp_context_test_run.c b/tools/testing/selftests/bpf/prog_tests/xdp_context_test_run.c
index 26159e0499c7..c603f2484803 100644
--- a/tools/testing/selftests/bpf/prog_tests/xdp_context_test_run.c
+++ b/tools/testing/selftests/bpf/prog_tests/xdp_context_test_run.c
@@ -427,6 +427,338 @@ static void test_tuntap_mirred(struct bpf_program *xdp_prog,
 	netns_free(ns);
 }
 
+/* Send test_payload over loopback UDP to recv_fd */
+static int send_loopback_udp(int recv_fd)
+{
+	struct sockaddr_in addr = {
+		.sin_family = AF_INET,
+		.sin_addr.s_addr = htonl(INADDR_LOOPBACK),
+	};
+	char buf[TEST_PAYLOAD_LEN];
+	int ret = -1;
+	int fd = -1;
+	__be16 port;
+
+	port = get_socket_local_port(recv_fd);
+	if (!ASSERT_GE(port, 0, "get_port"))
+		goto out;
+
+	fd = socket(AF_INET, SOCK_DGRAM, 0);
+	if (!ASSERT_GE(fd, 0, "socket"))
+		goto out;
+
+	addr.sin_port = port;
+	sendto(fd, test_payload, TEST_PAYLOAD_LEN, 0,
+	       (void *)&addr, sizeof(addr));
+	recvfrom(recv_fd, buf, sizeof(buf), 0, NULL, NULL);
+	ret = 0;
+out:
+	if (fd >= 0)
+		close(fd);
+	return ret;
+}
+
+enum udp_reader_type {
+	READER_CGRP_SKB,
+	READER_SK_FILTER,
+};
+
+/* Test skb_ext survival across TC ingress -> UDP reader hook */
+static void test_skb_ext_udp(struct test_xdp_meta *skel, const char *name,
+			     enum udp_reader_type reader)
+{
+	LIBBPF_OPTS(bpf_tc_hook, tc_hook,
+		    .ifindex = 1 /* IFINDEX_LO */,
+		    .attach_point = BPF_TC_INGRESS);
+	LIBBPF_OPTS(bpf_tc_opts, tc_opts, .handle = 1, .priority = 1);
+	struct bpf_link *reader_link = NULL;
+	struct netns_obj *ns = NULL;
+	int server_fd = -1;
+	int cgroup_fd = -1;
+	int filter_fd;
+	int ret;
+
+	ns = netns_new(name, true);
+	if (!ASSERT_OK_PTR(ns, "netns_new"))
+		return;
+
+	cgroup_fd = test__join_cgroup(name);
+	if (!ASSERT_GE(cgroup_fd, 0, "join_cgroup"))
+		goto cleanup;
+
+	server_fd = start_server(AF_INET, SOCK_DGRAM, "127.0.0.1", 0, 0);
+	if (!ASSERT_GE(server_fd, 0, "start_server"))
+		goto cleanup;
+
+	skel->bss->test_pass = false;
+
+	ret = bpf_tc_hook_create(&tc_hook);
+	if (!ASSERT_OK(ret, "bpf_tc_hook_create"))
+		goto cleanup;
+
+	tc_opts.prog_fd = bpf_program__fd(skel->progs.tc_skb_ext_write);
+	ret = bpf_tc_attach(&tc_hook, &tc_opts);
+	if (!ASSERT_OK(ret, "bpf_tc_attach"))
+		goto cleanup;
+
+	switch (reader) {
+	case READER_CGRP_SKB:
+		reader_link = bpf_program__attach_cgroup(skel->progs.cgrp_skb_ext_read,
+							 cgroup_fd);
+		if (!ASSERT_OK_PTR(reader_link, "attach_cgroup"))
+			goto cleanup;
+		break;
+	case READER_SK_FILTER:
+		filter_fd = bpf_program__fd(skel->progs.sk_filter_skb_ext_read);
+		ret = setsockopt(server_fd, SOL_SOCKET, SO_ATTACH_BPF,
+				 &filter_fd, sizeof(filter_fd));
+		if (!ASSERT_OK(ret, "attach_socket_filter"))
+			goto cleanup;
+		break;
+	}
+
+	if (send_loopback_udp(server_fd))
+		goto cleanup;
+
+	ASSERT_TRUE(skel->bss->test_pass, "test_pass");
+
+cleanup:
+	bpf_link__destroy(reader_link);
+	bpf_tc_hook_destroy(&tc_hook);
+	if (server_fd >= 0)
+		close(server_fd);
+	if (cgroup_fd >= 0)
+		close(cgroup_fd);
+	netns_free(ns);
+}
+
+enum tcp_reader_type {
+	READER_SKOPS,
+	READER_LSM,
+};
+
+/* Test skb_ext survival across TC ingress -> TCP reader hook */
+static void test_skb_ext_tcp(struct test_xdp_meta *skel, const char *name,
+			     enum tcp_reader_type reader)
+{
+	LIBBPF_OPTS(bpf_tc_hook, tc_hook,
+		    .ifindex = 1 /* IFINDEX_LO */,
+		    .attach_point = BPF_TC_INGRESS);
+	LIBBPF_OPTS(bpf_tc_opts, tc_opts, .handle = 1, .priority = 1);
+	struct bpf_link *reader_link = NULL;
+	struct netns_obj *ns = NULL;
+	int server_fd = -1;
+	int cgroup_fd = -1;
+	int client_fd = -1;
+	int conn_fd = -1;
+	__be16 port;
+	int ret;
+
+	ns = netns_new(name, true);
+	if (!ASSERT_OK_PTR(ns, "netns_new"))
+		return;
+
+	cgroup_fd = test__join_cgroup(name);
+	if (!ASSERT_GE(cgroup_fd, 0, "join_cgroup"))
+		goto cleanup;
+
+	server_fd = start_server(AF_INET, SOCK_STREAM, "127.0.0.1", 0, 0);
+	if (!ASSERT_GE(server_fd, 0, "start_server"))
+		goto cleanup;
+
+	port = get_socket_local_port(server_fd);
+	if (!ASSERT_GE(port, 0, "get_port"))
+		goto cleanup;
+
+	skel->bss->target_port = port;
+	skel->bss->test_pass = false;
+
+	ret = bpf_tc_hook_create(&tc_hook);
+	if (!ASSERT_OK(ret, "bpf_tc_hook_create"))
+		goto cleanup;
+
+	tc_opts.prog_fd = bpf_program__fd(skel->progs.tc_skb_ext_write_port);
+	ret = bpf_tc_attach(&tc_hook, &tc_opts);
+	if (!ASSERT_OK(ret, "bpf_tc_attach"))
+		goto cleanup;
+
+	switch (reader) {
+	case READER_SKOPS:
+		reader_link = bpf_program__attach_cgroup(skel->progs.skops_skb_ext_read,
+							 cgroup_fd);
+		if (!ASSERT_OK_PTR(reader_link, "attach_skops"))
+			goto cleanup;
+		break;
+	case READER_LSM:
+		reader_link = bpf_program__attach_lsm(skel->progs.lsm_skb_ext_read);
+		if (!ASSERT_OK_PTR(reader_link, "attach_lsm"))
+			goto cleanup;
+		break;
+	}
+
+	client_fd = connect_to_fd(server_fd, 0);
+	if (!ASSERT_GE(client_fd, 0, "connect"))
+		goto cleanup;
+
+	conn_fd = accept(server_fd, NULL, NULL);
+	if (!ASSERT_GE(conn_fd, 0, "accept"))
+		goto cleanup;
+
+	ASSERT_TRUE(skel->bss->test_pass, "test_pass");
+
+cleanup:
+	if (conn_fd >= 0)
+		close(conn_fd);
+	if (client_fd >= 0)
+		close(client_fd);
+	bpf_link__destroy(reader_link);
+	bpf_tc_hook_destroy(&tc_hook);
+	if (server_fd >= 0)
+		close(server_fd);
+	if (cgroup_fd >= 0)
+		close(cgroup_fd);
+	netns_free(ns);
+}
+
+/* Test skb_ext survives skb clone (via tc mirred).
+ * dummy_prog runs on the clone (dummy ingress).
+ */
+static void test_mirred_clone_ext(struct test_xdp_meta *skel,
+				  struct bpf_program *dummy_prog)
+{
+	LIBBPF_OPTS(bpf_tc_hook, tc_hook, .attach_point = BPF_TC_INGRESS);
+	LIBBPF_OPTS(bpf_tc_opts, tc_opts, .handle = 1, .priority = 1);
+	struct netns_obj *ns = NULL;
+	int dummy_ifindex;
+	int tap_ifindex;
+	int tap_fd = -1;
+	int ret;
+
+	skel->bss->test_pass = false;
+
+	ns = netns_new("mirred_clone", true);
+	if (!ASSERT_OK_PTR(ns, "netns_new"))
+		return;
+
+	/* Dummy dev: attach reader */
+	SYS(close, "ip link add name " DUMMY_NAME " type dummy");
+	SYS(close, "ip link set dev " DUMMY_NAME " up");
+
+	dummy_ifindex = if_nametoindex(DUMMY_NAME);
+	if (!ASSERT_GE(dummy_ifindex, 0, "dummy_ifindex"))
+		goto close;
+
+	tc_hook.ifindex = dummy_ifindex;
+	ret = bpf_tc_hook_create(&tc_hook);
+	if (!ASSERT_OK(ret, "dummy_hook_create"))
+		goto close;
+
+	tc_opts.prog_fd = bpf_program__fd(dummy_prog);
+	ret = bpf_tc_attach(&tc_hook, &tc_opts);
+	if (!ASSERT_OK(ret, "dummy_attach"))
+		goto close;
+
+	/* TAP dev: attach writer + mirred to dummy */
+	tap_fd = open_tuntap(TAP_NAME, true);
+	if (!ASSERT_GE(tap_fd, 0, "open_tuntap"))
+		goto close;
+
+	SYS(close, "ip link set dev " TAP_NAME " up");
+
+	tap_ifindex = if_nametoindex(TAP_NAME);
+	if (!ASSERT_GE(tap_ifindex, 0, "tap_ifindex"))
+		goto close;
+
+	tc_hook.ifindex = tap_ifindex;
+	ret = bpf_tc_hook_create(&tc_hook);
+	if (!ASSERT_OK(ret, "tap_hook_create"))
+		goto close;
+
+	tc_opts.prog_id = 0;
+	tc_opts.prog_fd = bpf_program__fd(skel->progs.tc_skb_ext_write);
+	ret = bpf_tc_attach(&tc_hook, &tc_opts);
+	if (!ASSERT_OK(ret, "tap_attach"))
+		goto close;
+
+	SYS(close, "tc filter add dev " TAP_NAME " ingress "
+		   "protocol all matchall "
+		   "action mirred ingress mirror dev " DUMMY_NAME);
+
+	ret = write_test_packet(tap_fd);
+	if (!ASSERT_OK(ret, "write_test_packet"))
+		goto close;
+
+	ASSERT_TRUE(skel->bss->test_pass, "test_pass");
+
+close:
+	if (tap_fd >= 0)
+		close(tap_fd);
+	netns_free(ns);
+}
+
+/* Test skb_ext survival until skb free: cgroup/skb egress -> kfree_skb.
+ * Send UDP to a closed port. The packet is dropped, triggering kfree_skb.
+ */
+static void test_cgrp_egress_to_kfree_skb(struct test_xdp_meta *skel)
+{
+	struct sockaddr_in addr = {
+		.sin_family = AF_INET,
+		.sin_port = htons(4321),
+		.sin_addr.s_addr = htonl(INADDR_LOOPBACK),
+	};
+	struct bpf_link *cg_link = NULL;
+	struct bpf_link *tp_link = NULL;
+	struct netns_obj *ns = NULL;
+	int cgroup_fd = -1;
+	char buf[1];
+	int fd = -1;
+	int ret;
+
+	cgroup_fd = test__join_cgroup("/cgrp_to_kfree");
+	if (!ASSERT_GE(cgroup_fd, 0, "join_cgroup"))
+		return;
+
+	ns = netns_new("cgrp_to_kfree", true);
+	if (!ASSERT_OK_PTR(ns, "netns_new"))
+		goto cleanup;
+
+	skel->bss->test_pass = false;
+
+	cg_link = bpf_program__attach_cgroup(skel->progs.cgrp_skb_ext_write,
+					     cgroup_fd);
+	if (!ASSERT_OK_PTR(cg_link, "attach_cgroup"))
+		goto cleanup;
+
+	tp_link = bpf_program__attach_trace(skel->progs.tp_kfree_skb_ext_read);
+	if (!ASSERT_OK_PTR(tp_link, "attach_tp"))
+		goto cleanup;
+
+	fd = socket(AF_INET, SOCK_DGRAM, 0);
+	if (!ASSERT_GE(fd, 0, "socket"))
+		goto cleanup;
+
+	ret = connect(fd, (void *)&addr, sizeof(addr));
+	if (!ASSERT_OK(ret, "connect"))
+		goto cleanup;
+
+	send(fd, test_payload, TEST_PAYLOAD_LEN, 0);
+
+	/* Wait for ICMP error -- confirms the packet was freed */
+	ret = recv(fd, buf, sizeof(buf), 0);
+	ASSERT_EQ(errno, ECONNREFUSED, "recv_econnrefused");
+
+	ASSERT_TRUE(skel->bss->test_pass, "test_pass");
+
+cleanup:
+	if (fd >= 0)
+		close(fd);
+	bpf_link__destroy(tp_link);
+	bpf_link__destroy(cg_link);
+	netns_free(ns);
+	if (cgroup_fd >= 0)
+		close(cgroup_fd);
+}
+
 void test_xdp_context_tuntap(void)
 {
 	struct test_xdp_meta *skel = NULL;
@@ -515,6 +847,86 @@ void test_xdp_context_tuntap(void)
 			    skel->progs.helper_skb_change_proto,
 			    NULL, /* tc prio 2 */
 			    &skel->bss->test_pass);
+	/* Tests for BPF dynptr from skb_ext */
+	if (test__start_subtest("tc_skb_ext__write_read"))
+		test_tuntap(NULL, /* xdp */
+			    skel->progs.tc_skb_ext_write,
+			    skel->progs.tc_skb_ext_read,
+			    &skel->bss->test_pass);
+	if (test__start_subtest("tc_skb_ext__write_clone_read"))
+		test_tuntap(NULL, /* xdp */
+			    skel->progs.tc_skb_ext_write,
+			    skel->progs.tc_skb_ext_clone_read,
+			    &skel->bss->test_pass);
+	if (test__start_subtest("tc_skb_ext__write_slice_read"))
+		test_tuntap(NULL, /* xdp */
+			    skel->progs.tc_skb_ext_write,
+			    skel->progs.tc_skb_ext_slice_read,
+			    &skel->bss->test_pass);
+	if (test__start_subtest("tc_skb_ext__slice_write_read"))
+		test_tuntap(NULL, /* xdp */
+			    skel->progs.tc_skb_ext_slice_write,
+			    skel->progs.tc_skb_ext_read,
+			    &skel->bss->test_pass);
+	if (test__start_subtest("tc_skb_ext__no_alloc"))
+		test_tuntap(NULL, /* xdp */
+			    skel->progs.tc_skb_ext_no_alloc,
+			    NULL, /* tc prio 2 */
+			    &skel->bss->test_pass);
+	if (test__start_subtest("tc_skb_ext__invalid_flags"))
+		test_tuntap(NULL, /* xdp */
+			    skel->progs.tc_skb_ext_invalid_flags,
+			    NULL, /* tc prio 2 */
+			    &skel->bss->test_pass);
+	if (test__start_subtest("tc_skb_ext__rdonly"))
+		test_tuntap(NULL, /* xdp */
+			    skel->progs.tc_skb_ext_rdonly,
+			    NULL, /* tc prio 2 */
+			    &skel->bss->test_pass);
+	if (test__start_subtest("tc_skb_ext__double_alloc"))
+		test_tuntap(NULL, /* xdp */
+			    skel->progs.tc_skb_ext_double_alloc,
+			    NULL, /* tc prio 2 */
+			    &skel->bss->test_pass);
+
+	test_xdp_meta__destroy(skel);
+}
+
+static void test_mirred_clone_ext_cow(struct test_xdp_meta *skel)
+{
+	struct bpf_link *tp_link;
+
+	skel->bss->clone_cow_done = false;
+	tp_link = bpf_program__attach(skel->progs.tp_kfree_skb_cow_check);
+	if (!ASSERT_OK_PTR(tp_link, "attach_tp"))
+		return;
+
+	test_mirred_clone_ext(skel, skel->progs.tc_skb_ext_clone_redir_cow);
+	bpf_link__destroy(tp_link);
+}
+
+void test_skb_ext_cross_hook(void)
+{
+	struct test_xdp_meta *skel = NULL;
+
+	skel = test_xdp_meta__open_and_load();
+	if (!ASSERT_OK_PTR(skel, "open and load skeleton"))
+		return;
+
+	if (test__start_subtest("tc_to_cgrp"))
+		test_skb_ext_udp(skel, "tc_to_cgrp", READER_CGRP_SKB);
+	if (test__start_subtest("tc_to_sk_filter"))
+		test_skb_ext_udp(skel, "tc_to_sk_filter", READER_SK_FILTER);
+	if (test__start_subtest("tc_to_lsm"))
+		test_skb_ext_tcp(skel, "tc_to_lsm", READER_LSM);
+	if (test__start_subtest("tc_to_skops"))
+		test_skb_ext_tcp(skel, "tc_to_skops", READER_SKOPS);
+	if (test__start_subtest("cgrp_egress_to_kfree_skb"))
+		test_cgrp_egress_to_kfree_skb(skel);
+	if (test__start_subtest("clone_ext_read"))
+		test_mirred_clone_ext(skel, skel->progs.tc_skb_ext_read);
+	if (test__start_subtest("clone_ext_cow"))
+		test_mirred_clone_ext_cow(skel);
 
 	test_xdp_meta__destroy(skel);
 }
diff --git a/tools/testing/selftests/bpf/progs/test_xdp_meta.c b/tools/testing/selftests/bpf/progs/test_xdp_meta.c
index fa73b17cb999..25fbdc00657c 100644
--- a/tools/testing/selftests/bpf/progs/test_xdp_meta.c
+++ b/tools/testing/selftests/bpf/progs/test_xdp_meta.c
@@ -3,9 +3,11 @@
 
 #include <bpf/bpf_endian.h>
 #include <bpf/bpf_helpers.h>
+#include <bpf/bpf_tracing.h>
 #include <errno.h>
 
 #include "bpf_kfuncs.h"
+#include "bpf_misc.h"
 #include "bpf_tracing_net.h"
 
 #define META_SIZE 32
@@ -64,6 +66,21 @@ static bool check_skb_metadata(const char *file, int line, struct __sk_buff *skb
 
 #define check_skb_metadata(skb) check_skb_metadata(__FILE__, __LINE__, skb)
 
+/* Test packets carry test metadata pattern as payload. */
+static bool is_test_packet_tc(struct __sk_buff *ctx)
+{
+	__u8 meta_have[META_SIZE];
+
+	if (ctx->len < META_SIZE)
+		return false;
+	if (bpf_skb_load_bytes(ctx, ctx->len - META_SIZE, meta_have, META_SIZE))
+		return false;
+	if (__builtin_memcmp(meta_have, meta_want, META_SIZE))
+		return false;
+
+	return true;
+}
+
 SEC("tc")
 int ing_cls(struct __sk_buff *ctx)
 {
@@ -670,4 +687,379 @@ int helper_skb_change_proto(struct __sk_buff *ctx)
 	return TC_ACT_SHOT;
 }
 
+/* Write to skb_ext using bpf_dynptr_write helper */
+SEC("tc")
+int tc_skb_ext_write(struct __sk_buff *ctx)
+{
+	struct bpf_dynptr meta;
+
+	if (!is_test_packet_tc(ctx))
+		return TC_ACT_SHOT;
+	if (bpf_dynptr_from_skb_ext(ctx, 0, BPF_SKB_EXT_F_CREATE, &meta))
+		return TC_ACT_SHOT;
+	if (bpf_dynptr_write(&meta, 0, (void *)meta_want, ARRAY_SIZE(meta_want), 0))
+		return TC_ACT_SHOT;
+
+	return TC_ACT_UNSPEC;
+}
+
+/* Read from skb-ext metadata using bpf_dynptr_read helper */
+SEC("tc")
+int tc_skb_ext_read(struct __sk_buff *ctx)
+{
+	__u8 meta_have[META_SIZE];
+	struct bpf_dynptr meta;
+
+	if (bpf_dynptr_from_skb_ext(ctx, 0, 0, &meta))
+		return TC_ACT_SHOT;
+	if (bpf_dynptr_read(meta_have, ARRAY_SIZE(meta_have), &meta, 0, 0))
+		return TC_ACT_SHOT;
+	if (!check_metadata(meta_have))
+		return TC_ACT_SHOT;
+
+	test_pass = true;
+	return TC_ACT_UNSPEC;
+}
+
+/* Read from a cloned skb_ext dynptr */
+SEC("tc")
+int tc_skb_ext_clone_read(struct __sk_buff *ctx)
+{
+	struct bpf_dynptr meta, clone;
+	__u8 meta_have[META_SIZE];
+
+	if (bpf_dynptr_from_skb_ext(ctx, 0, 0, &meta))
+		return TC_ACT_SHOT;
+	if (bpf_dynptr_clone(&meta, &clone))
+		return TC_ACT_SHOT;
+	if (bpf_dynptr_read(meta_have, ARRAY_SIZE(meta_have), &clone, 0, 0))
+		return TC_ACT_SHOT;
+	if (!check_metadata(meta_have))
+		return TC_ACT_SHOT;
+
+	test_pass = true;
+	return TC_ACT_UNSPEC;
+}
+
+/* Read from skb_ext using bpf_dynptr_slice */
+SEC("tc")
+int tc_skb_ext_slice_read(struct __sk_buff *ctx)
+{
+	struct bpf_dynptr meta;
+	__u8 *meta_have;
+
+	if (bpf_dynptr_from_skb_ext(ctx, 0, 0, &meta))
+		return TC_ACT_SHOT;
+	meta_have = bpf_dynptr_slice(&meta, 0, NULL, META_SIZE);
+	if (!meta_have)
+		return TC_ACT_SHOT;
+	if (!check_metadata(meta_have))
+		return TC_ACT_SHOT;
+
+	test_pass = true;
+	return TC_ACT_UNSPEC;
+}
+
+/* Write to skb_ext using bpf_dynptr_slice_rdwr */
+SEC("tc")
+int tc_skb_ext_slice_write(struct __sk_buff *ctx)
+{
+	struct bpf_dynptr meta;
+	__u8 *dst;
+
+	if (!is_test_packet_tc(ctx))
+		return TC_ACT_SHOT;
+	if (bpf_dynptr_from_skb_ext(ctx, 0, BPF_SKB_EXT_F_CREATE, &meta))
+		return TC_ACT_SHOT;
+	dst = bpf_dynptr_slice_rdwr(&meta, 0, NULL, META_SIZE);
+	if (!dst)
+		return TC_ACT_SHOT;
+	__builtin_memcpy(dst, meta_want, META_SIZE);
+
+	return TC_ACT_UNSPEC;
+}
+
+/* Opening skb_ext without F_CREATE on a fresh skb should fail */
+SEC("tc")
+int tc_skb_ext_no_alloc(struct __sk_buff *ctx)
+{
+	struct bpf_dynptr meta;
+
+	if (!is_test_packet_tc(ctx))
+		return TC_ACT_SHOT;
+	if (bpf_dynptr_from_skb_ext(ctx, 0, 0, &meta) != -ENOENT)
+		return TC_ACT_SHOT;
+
+	test_pass = true;
+	return TC_ACT_UNSPEC;
+}
+
+/* Invalid flags are rejected */
+SEC("tc")
+int tc_skb_ext_invalid_flags(struct __sk_buff *ctx)
+{
+	struct bpf_dynptr meta;
+
+	if (!is_test_packet_tc(ctx))
+		return TC_ACT_SHOT;
+	if (bpf_dynptr_from_skb_ext(ctx, 0, ~0ULL, &meta) != -EINVAL)
+		return TC_ACT_SHOT;
+
+	test_pass = true;
+	return TC_ACT_UNSPEC;
+}
+
+/* Without F_CREATE the dynptr is read-only */
+SEC("tc")
+int tc_skb_ext_rdonly(struct __sk_buff *ctx)
+{
+	__u8 meta_have[META_SIZE];
+	struct bpf_dynptr meta;
+
+	if (!is_test_packet_tc(ctx))
+		return TC_ACT_SHOT;
+
+	/* Create and populate the ext */
+	if (bpf_dynptr_from_skb_ext(ctx, 0, BPF_SKB_EXT_F_CREATE, &meta))
+		return TC_ACT_SHOT;
+	if (bpf_dynptr_write(&meta, 0, (void *)meta_want, META_SIZE, 0))
+		return TC_ACT_SHOT;
+
+	/* Reopen without F_CREATE -- should be read-only */
+	if (bpf_dynptr_from_skb_ext(ctx, 0, 0, &meta))
+		return TC_ACT_SHOT;
+
+	/* Verify read-only: writes must fail, reads must work */
+	if (!bpf_dynptr_is_rdonly(&meta))
+		return TC_ACT_SHOT;
+	if (!bpf_dynptr_write(&meta, 0, (void *)meta_want, META_SIZE, 0))
+		return TC_ACT_SHOT;
+	if (bpf_dynptr_read(meta_have, META_SIZE, &meta, 0, 0))
+		return TC_ACT_SHOT;
+	if (!check_metadata(meta_have))
+		return TC_ACT_SHOT;
+
+	test_pass = true;
+	return TC_ACT_UNSPEC;
+}
+
+/* Double alloc: data from first alloc survives second skb_ext_add */
+SEC("tc")
+int tc_skb_ext_double_alloc(struct __sk_buff *ctx)
+{
+	__u8 meta_have[META_SIZE];
+	struct bpf_dynptr meta;
+
+	if (!is_test_packet_tc(ctx))
+		return TC_ACT_SHOT;
+
+	/* First alloc + write */
+	if (bpf_dynptr_from_skb_ext(ctx, 0, BPF_SKB_EXT_F_CREATE, &meta))
+		return TC_ACT_SHOT;
+	if (bpf_dynptr_write(&meta, 0, (void *)meta_want, META_SIZE, 0))
+		return TC_ACT_SHOT;
+
+	/* Second alloc -- skb_ext_add returns existing ext */
+	if (bpf_dynptr_from_skb_ext(ctx, 0, BPF_SKB_EXT_F_CREATE, &meta))
+		return TC_ACT_SHOT;
+	if (bpf_dynptr_read(meta_have, META_SIZE, &meta, 0, 0))
+		return TC_ACT_SHOT;
+	if (!check_metadata(meta_have))
+		return TC_ACT_SHOT;
+
+	test_pass = true;
+	return TC_ACT_UNSPEC;
+}
+
+/* Read skb_ext from cgroup/skb ingress -- tests cross-hook survival */
+SEC("cgroup_skb/ingress")
+int cgrp_skb_ext_read(struct __sk_buff *ctx)
+{
+	__u8 meta_have[META_SIZE];
+	struct bpf_dynptr meta;
+
+	if (bpf_dynptr_from_skb_ext(ctx, 0, 0, &meta))
+		return 1;
+	if (bpf_dynptr_read(meta_have, META_SIZE, &meta, 0, 0))
+		return 1;
+	if (!check_metadata(meta_have))
+		return 1;
+
+	test_pass = true;
+	return 1;
+}
+
+volatile __be16 target_port;
+
+#define TCPV4_HDR_OFF	(sizeof(struct ethhdr) + sizeof(struct iphdr))
+#define TCPV4_SPORT_OFF	(TCPV4_HDR_OFF + offsetof(struct tcphdr, source))
+#define TCPV4_DPORT_OFF	(TCPV4_HDR_OFF + offsetof(struct tcphdr, dest))
+
+/* Write skb_ext on TCP packets to/from target_port */
+SEC("tc")
+int tc_skb_ext_write_port(struct __sk_buff *ctx)
+{
+	struct bpf_dynptr meta;
+	__be16 sport, dport;
+
+	if (ctx->protocol != __bpf_constant_htons(ETH_P_IP))
+		return TC_ACT_UNSPEC;
+	if (bpf_skb_load_bytes(ctx, TCPV4_SPORT_OFF, &sport, sizeof(sport)))
+		return TC_ACT_UNSPEC;
+	if (bpf_skb_load_bytes(ctx, TCPV4_DPORT_OFF, &dport, sizeof(dport)))
+		return TC_ACT_UNSPEC;
+	if (sport != target_port && dport != target_port)
+		return TC_ACT_UNSPEC;
+
+	if (bpf_dynptr_from_skb_ext(ctx, 0, BPF_SKB_EXT_F_CREATE, &meta))
+		return TC_ACT_UNSPEC;
+	bpf_dynptr_write(&meta, 0, (void *)meta_want, META_SIZE, 0);
+
+	return TC_ACT_UNSPEC;
+}
+
+/* Read skb_ext from sock_ops passive established -- tests TC -> sock_ops path */
+SEC("sockops")
+int skops_skb_ext_read(struct bpf_sock_ops *ctx)
+{
+	struct bpf_sock_ops_kern *kctx;
+	__u8 meta_have[META_SIZE];
+	struct bpf_dynptr meta;
+	struct sk_buff *skb;
+
+	if (ctx->op != BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB)
+		return 1;
+
+	kctx = bpf_cast_to_kern_ctx(ctx);
+	skb = kctx->skb;
+	if (!skb)
+		return 1;
+
+	if (bpf_dynptr_from_skb_ext((struct __sk_buff *)skb, 0, 0, &meta))
+		return 1;
+	if (bpf_dynptr_read(meta_have, META_SIZE, &meta, 0, 0))
+		return 1;
+	if (!check_metadata(meta_have))
+		return 1;
+
+	test_pass = true;
+	return 1;
+}
+
+/* Read skb_ext from LSM inet_conn_established -- tests TC -> LSM path */
+SEC("lsm/inet_conn_established")
+int BPF_PROG(lsm_skb_ext_read, struct sock *sk, struct sk_buff *skb)
+{
+	__u8 meta_have[META_SIZE];
+	struct bpf_dynptr meta;
+
+	if (bpf_dynptr_from_skb_ext((struct __sk_buff *)skb, 0, 0, &meta))
+		return 0;
+	if (bpf_dynptr_read(meta_have, META_SIZE, &meta, 0, 0))
+		return 0;
+	if (!check_metadata(meta_have))
+		return 0;
+
+	test_pass = true;
+	return 0;
+}
+
+/* Read skb_ext from socket filter -- tests TC -> sk_filter path */
+SEC("socket")
+int sk_filter_skb_ext_read(struct __sk_buff *ctx)
+{
+	__u8 meta_have[META_SIZE];
+	struct bpf_dynptr meta;
+
+	if (bpf_dynptr_from_skb_ext(ctx, 0, 0, &meta))
+		goto out;
+	if (bpf_dynptr_read(meta_have, META_SIZE, &meta, 0, 0))
+		goto out;
+	if (!check_metadata(meta_have))
+		goto out;
+
+	test_pass = true;
+out:
+	return ctx->len;
+}
+
+/* Write skb_ext from cgroup/skb egress */
+SEC("cgroup_skb/egress")
+int cgrp_skb_ext_write(struct __sk_buff *ctx)
+{
+	struct bpf_dynptr meta;
+
+	if (!is_test_packet_tc(ctx))
+		return 1;
+
+	if (bpf_dynptr_from_skb_ext(ctx, 0, BPF_SKB_EXT_F_CREATE, &meta))
+		return 1;
+	bpf_dynptr_write(&meta, 0, (void *)meta_want, META_SIZE, 0);
+
+	return 1;
+}
+
+static const __u8 meta_zero[META_SIZE] = {};
+
+volatile bool clone_cow_done;
+
+/* Overwrite skb_ext on the clone via F_CREATE (COW) -- must not affect original.
+ * Runs on the dummy ingress (clone side), synchronously during tc mirred.
+ */
+SEC("tc")
+int tc_skb_ext_clone_redir_cow(struct __sk_buff *ctx)
+{
+	struct bpf_dynptr meta;
+
+	if (bpf_dynptr_from_skb_ext(ctx, 0, BPF_SKB_EXT_F_CREATE, &meta))
+		return TC_ACT_SHOT;
+
+	/* Zero out the clone's ext -- must not affect original */
+	bpf_dynptr_write(&meta, 0, (void *)meta_zero, META_SIZE, 0);
+
+	clone_cow_done = true;
+	return TC_ACT_SHOT;
+}
+
+/* Verify COW isolation at kfree_skb time: once clone_cow_done is set,
+ * check that the original skb still has meta_want.
+ */
+SEC("tp_btf/kfree_skb")
+int BPF_PROG(tp_kfree_skb_cow_check, struct sk_buff *skb)
+{
+	__u8 meta_have[META_SIZE];
+	struct bpf_dynptr meta;
+
+	if (!clone_cow_done)
+		return 0;
+
+	if (bpf_dynptr_from_skb_ext((struct __sk_buff *)skb, 0, 0, &meta))
+		return 0;
+	if (bpf_dynptr_read(meta_have, META_SIZE, &meta, 0, 0))
+		return 0;
+
+	if (check_metadata(meta_have))
+		test_pass = true;
+
+	return 0;
+}
+
+/* Read skb_ext from tp_btf/kfree_skb -- tests survival until skb free */
+SEC("tp_btf/kfree_skb")
+int BPF_PROG(tp_kfree_skb_ext_read, struct sk_buff *skb)
+{
+	__u8 meta_have[META_SIZE];
+	struct bpf_dynptr meta;
+
+	if (bpf_dynptr_from_skb_ext((struct __sk_buff *)skb, 0, 0, &meta))
+		return 0;
+	if (bpf_dynptr_read(meta_have, META_SIZE, &meta, 0, 0))
+		return 0;
+	if (!check_metadata(meta_have))
+		return 0;
+
+	test_pass = true;
+	return 0;
+}
+
 char _license[] SEC("license") = "GPL";

-- 
2.43.0


^ permalink raw reply related

* [PATCH RFC net-next 6/6] selftests/bpf: Use non-trivial test payload in xdp_context tests
From: Jakub Sitnicki @ 2026-07-14 17:48 UTC (permalink / raw)
  To: netdev; +Cc: bpf, kernel-team
In-Reply-To: <20260714-bpf-meta-inside-skb-ext-v1-0-5871c07a8dd6@cloudflare.com>

Replace the sequential test payload pattern (0x01..0x38) with
higher-entropy values that don't alias common protocol header fields
such as IP version/IHL or ethertype LSBs. This reduces the chance of
false positives from stray packets.

Signed-off-by: Jakub Sitnicki <jakub@cloudflare.com>
---
 tools/testing/selftests/bpf/prog_tests/xdp_context_test_run.c | 8 ++++----
 tools/testing/selftests/bpf/progs/test_xdp_meta.c             | 8 ++++----
 2 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/tools/testing/selftests/bpf/prog_tests/xdp_context_test_run.c b/tools/testing/selftests/bpf/prog_tests/xdp_context_test_run.c
index a697e8f63722..0a73035dd855 100644
--- a/tools/testing/selftests/bpf/prog_tests/xdp_context_test_run.c
+++ b/tools/testing/selftests/bpf/prog_tests/xdp_context_test_run.c
@@ -14,10 +14,10 @@
 
 #define TEST_PAYLOAD_LEN 32
 static const __u8 test_payload[TEST_PAYLOAD_LEN] = {
-	0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
-	0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
-	0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28,
-	0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38,
+	0x49, 0x20, 0x63, 0x72, 0x6f, 0x73, 0x73, 0x65,
+	0x64, 0x20, 0x68, 0x6f, 0x6f, 0x6b, 0x73, 0x20,
+	0x26, 0x20, 0x6c, 0x69, 0x76, 0x65, 0x64, 0x20,
+	0x74, 0x6f, 0x20, 0x74, 0x65, 0x6c, 0x6c, 0x21,
 };
 
 void test_xdp_context_error(int prog_fd, struct bpf_test_run_opts opts,
diff --git a/tools/testing/selftests/bpf/progs/test_xdp_meta.c b/tools/testing/selftests/bpf/progs/test_xdp_meta.c
index a1a3cbdb85bc..7e5a9d14b572 100644
--- a/tools/testing/selftests/bpf/progs/test_xdp_meta.c
+++ b/tools/testing/selftests/bpf/progs/test_xdp_meta.c
@@ -28,10 +28,10 @@ static const __u8 smac_want[ETH_ALEN] = {
 };
 
 static const __u8 meta_want[META_SIZE] = {
-	0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
-	0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
-	0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28,
-	0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38,
+	0x49, 0x20, 0x63, 0x72, 0x6f, 0x73, 0x73, 0x65,
+	0x64, 0x20, 0x68, 0x6f, 0x6f, 0x6b, 0x73, 0x20,
+	0x26, 0x20, 0x6c, 0x69, 0x76, 0x65, 0x64, 0x20,
+	0x74, 0x6f, 0x20, 0x74, 0x65, 0x6c, 0x6c, 0x21,
 };
 
 static bool check_smac(const struct ethhdr *eth)

-- 
2.43.0


^ permalink raw reply related

* [PATCH RFC net-next 5/6] selftests/bpf: Test skb_ext scrubbing across tunnels and veths
From: Jakub Sitnicki @ 2026-07-14 17:48 UTC (permalink / raw)
  To: netdev; +Cc: bpf, kernel-team
In-Reply-To: <20260714-bpf-meta-inside-skb-ext-v1-0-5871c07a8dd6@cloudflare.com>

Add tests for BPF_SKB_EXT_F_NO_SCRUB covering extension survival and
scrubbing across veth cross-netns forwarding and GRE tunnel
encap/decap:

- no_scrub_veth: ext with F_NO_SCRUB survives veth forwarding
- scrubbed_veth: ext without the flag is scrubbed on veth forwarding
- no_scrub_gre: ext with F_NO_SCRUB survives GRE encap+decap
- scrubbed_gre: ext without the flag is scrubbed on GRE decap

The GRE tests use a single netns with two gretap endpoints over
loopback. The veth tests use a cross-netns veth pair.

Move skb_ext subtests from xdp_context_tuntap into a dedicated
test_skb_ext test group.

Signed-off-by: Jakub Sitnicki <jakub@cloudflare.com>
---
 .../bpf/prog_tests/xdp_context_test_run.c          | 240 ++++++++++++++++++---
 tools/testing/selftests/bpf/progs/test_xdp_meta.c  |  18 ++
 2 files changed, 224 insertions(+), 34 deletions(-)

diff --git a/tools/testing/selftests/bpf/prog_tests/xdp_context_test_run.c b/tools/testing/selftests/bpf/prog_tests/xdp_context_test_run.c
index c603f2484803..a697e8f63722 100644
--- a/tools/testing/selftests/bpf/prog_tests/xdp_context_test_run.c
+++ b/tools/testing/selftests/bpf/prog_tests/xdp_context_test_run.c
@@ -847,72 +847,236 @@ void test_xdp_context_tuntap(void)
 			    skel->progs.helper_skb_change_proto,
 			    NULL, /* tc prio 2 */
 			    &skel->bss->test_pass);
-	/* Tests for BPF dynptr from skb_ext */
-	if (test__start_subtest("tc_skb_ext__write_read"))
+
+	test_xdp_meta__destroy(skel);
+}
+
+static void test_mirred_clone_ext_cow(struct test_xdp_meta *skel)
+{
+	struct bpf_link *tp_link;
+
+	skel->bss->clone_cow_done = false;
+	tp_link = bpf_program__attach(skel->progs.tp_kfree_skb_cow_check);
+	if (!ASSERT_OK_PTR(tp_link, "attach_tp"))
+		return;
+
+	test_mirred_clone_ext(skel, skel->progs.tc_skb_ext_clone_redir_cow);
+	bpf_link__destroy(tp_link);
+}
+
+/* Test skb_ext across veth cross-netns forwarding.
+ * Writer on TX_NAME egress, reader on RX_NAME ingress.
+ * When no_scrub is true, ext should survive. Otherwise it should be scrubbed.
+ */
+static void test_skb_ext_scrub_veth(struct test_xdp_meta *skel, bool no_scrub)
+{
+	LIBBPF_OPTS(bpf_tc_hook, tx_hook, .attach_point = BPF_TC_EGRESS);
+	LIBBPF_OPTS(bpf_tc_opts, tx_opts, .handle = 1, .priority = 1);
+	LIBBPF_OPTS(bpf_tc_hook, rx_hook, .attach_point = BPF_TC_INGRESS);
+	LIBBPF_OPTS(bpf_tc_opts, rx_opts, .handle = 1, .priority = 1);
+	struct netns_obj *rx_ns = NULL, *tx_ns = NULL;
+	struct nstoken *nstoken = NULL;
+	int rx_ifindex, tx_ifindex;
+	int ret;
+
+	tx_ns = netns_new(TX_NETNS, false);
+	if (!ASSERT_OK_PTR(tx_ns, "create tx_ns"))
+		return;
+
+	rx_ns = netns_new(RX_NETNS, false);
+	if (!ASSERT_OK_PTR(rx_ns, "create rx_ns"))
+		goto close;
+
+	SYS(close, "ip link add " RX_NAME " netns " RX_NETNS
+	    " type veth peer name " TX_NAME " netns " TX_NETNS);
+
+	/* Setup RX side: TC ingress reader */
+	nstoken = open_netns(RX_NETNS);
+	if (!ASSERT_OK_PTR(nstoken, "setns rx_ns"))
+		goto close;
+
+	SYS(close, "ip link set dev " RX_NAME " up");
+
+	rx_ifindex = if_nametoindex(RX_NAME);
+	if (!ASSERT_GE(rx_ifindex, 0, "if_nametoindex rx"))
+		goto close;
+
+	rx_hook.ifindex = rx_ifindex;
+	ret = bpf_tc_hook_create(&rx_hook);
+	if (!ASSERT_OK(ret, "bpf_tc_hook_create rx"))
+		goto close;
+
+	rx_opts.prog_fd = bpf_program__fd(skel->progs.tc_skb_ext_read);
+	ret = bpf_tc_attach(&rx_hook, &rx_opts);
+	if (!ASSERT_OK(ret, "bpf_tc_attach rx"))
+		goto close;
+
+	close_netns(nstoken);
+
+	/* Setup TX side: TC egress writer */
+	nstoken = open_netns(TX_NETNS);
+	if (!ASSERT_OK_PTR(nstoken, "setns tx_ns"))
+		goto close;
+
+	SYS(close, "ip link set dev " TX_NAME " up");
+
+	tx_ifindex = if_nametoindex(TX_NAME);
+	if (!ASSERT_GE(tx_ifindex, 0, "if_nametoindex tx"))
+		goto close;
+
+	tx_hook.ifindex = tx_ifindex;
+	ret = bpf_tc_hook_create(&tx_hook);
+	if (!ASSERT_OK(ret, "bpf_tc_hook_create tx"))
+		goto close;
+
+	tx_opts.prog_fd = no_scrub
+		? bpf_program__fd(skel->progs.tc_skb_ext_write_no_scrub)
+		: bpf_program__fd(skel->progs.tc_skb_ext_write);
+	ret = bpf_tc_attach(&tx_hook, &tx_opts);
+	if (!ASSERT_OK(ret, "bpf_tc_attach tx"))
+		goto close;
+
+	skel->bss->test_pass = false;
+
+	ret = send_test_packet(tx_ifindex);
+	if (!ASSERT_OK(ret, "send_test_packet"))
+		goto close;
+
+	if (no_scrub)
+		ASSERT_TRUE(skel->bss->test_pass, "test_pass");
+	else
+		ASSERT_FALSE(skel->bss->test_pass, "ext_scrubbed");
+
+close:
+	close_netns(nstoken);
+	netns_free(rx_ns);
+	netns_free(tx_ns);
+}
+
+/* Test skb_ext across GRE tunnel encap+decap.
+ * When no_scrub is true, ext should survive. Otherwise it should be scrubbed.
+ */
+static void test_skb_ext_scrub_gre(struct test_xdp_meta *skel, bool no_scrub)
+{
+	LIBBPF_OPTS(bpf_tc_hook, tx_hook, .attach_point = BPF_TC_EGRESS);
+	LIBBPF_OPTS(bpf_tc_opts, tx_opts, .handle = 1, .priority = 1);
+	LIBBPF_OPTS(bpf_tc_hook, rx_hook, .attach_point = BPF_TC_INGRESS);
+	LIBBPF_OPTS(bpf_tc_opts, rx_opts, .handle = 1, .priority = 1);
+	struct netns_obj *ns = NULL;
+	int tx_ifindex;
+	int rx_ifindex;
+	int ret;
+
+	skel->bss->test_pass = false;
+
+	ns = netns_new("gre_test", true);
+	if (!ASSERT_OK_PTR(ns, "netns_new"))
+		return;
+
+	/* Setup: gre_tx -> lo -> gre_rx */
+	SYS(close, "ip link set lo up");
+	SYS(close, "ip link add gre_tx type gretap"
+	    " local 127.0.0.1 remote 127.0.0.2");
+	SYS(close, "ip link set gre_tx up");
+	SYS(close, "ip addr add 127.0.0.2/8 dev lo");
+	SYS(close, "ip link add gre_rx type gretap"
+	    " local 127.0.0.2 remote 127.0.0.1");
+	SYS(close, "ip link set gre_rx up");
+
+	/* Write skb_ext on TC egress on GRE tx */
+	tx_ifindex = if_nametoindex("gre_tx");
+	if (!ASSERT_GE(tx_ifindex, 0, "tx_ifindex"))
+		goto close;
+
+	tx_hook.ifindex = tx_ifindex;
+	ret = bpf_tc_hook_create(&tx_hook);
+	if (!ASSERT_OK(ret, "tx_hook_create"))
+		goto close;
+
+	tx_opts.prog_fd = no_scrub
+		? bpf_program__fd(skel->progs.tc_skb_ext_write_no_scrub)
+		: bpf_program__fd(skel->progs.tc_skb_ext_write);
+	ret = bpf_tc_attach(&tx_hook, &tx_opts);
+	if (!ASSERT_OK(ret, "tx_attach"))
+		goto close;
+
+	/* Read skb_ext on TC ingress on GRE rx */
+	rx_ifindex = if_nametoindex("gre_rx");
+	if (!ASSERT_GE(rx_ifindex, 0, "rx_ifindex"))
+		goto close;
+
+	rx_hook.ifindex = rx_ifindex;
+	ret = bpf_tc_hook_create(&rx_hook);
+	if (!ASSERT_OK(ret, "rx_hook_create"))
+		goto close;
+
+	rx_opts.prog_fd = bpf_program__fd(skel->progs.tc_skb_ext_read);
+	ret = bpf_tc_attach(&rx_hook, &rx_opts);
+	if (!ASSERT_OK(ret, "rx_attach"))
+		goto close;
+
+	/* Then use send_test_packet on GRE tx */
+	ret = send_test_packet(tx_ifindex);
+	if (!ASSERT_OK(ret, "send_test_packet"))
+		goto close;
+
+	if (no_scrub)
+		ASSERT_TRUE(skel->bss->test_pass, "test_pass");
+	else
+		ASSERT_FALSE(skel->bss->test_pass, "ext_scrubbed");
+
+close:
+	netns_free(ns);
+}
+
+void test_skb_ext(void)
+{
+	struct test_xdp_meta *skel = NULL;
+
+	skel = test_xdp_meta__open_and_load();
+	if (!ASSERT_OK_PTR(skel, "open and load skeleton"))
+		return;
+
+	if (test__start_subtest("tc_write_read"))
 		test_tuntap(NULL, /* xdp */
 			    skel->progs.tc_skb_ext_write,
 			    skel->progs.tc_skb_ext_read,
 			    &skel->bss->test_pass);
-	if (test__start_subtest("tc_skb_ext__write_clone_read"))
+	if (test__start_subtest("tc_write_clone_read"))
 		test_tuntap(NULL, /* xdp */
 			    skel->progs.tc_skb_ext_write,
 			    skel->progs.tc_skb_ext_clone_read,
 			    &skel->bss->test_pass);
-	if (test__start_subtest("tc_skb_ext__write_slice_read"))
+	if (test__start_subtest("tc_write_slice_read"))
 		test_tuntap(NULL, /* xdp */
 			    skel->progs.tc_skb_ext_write,
 			    skel->progs.tc_skb_ext_slice_read,
 			    &skel->bss->test_pass);
-	if (test__start_subtest("tc_skb_ext__slice_write_read"))
+	if (test__start_subtest("tc_slice_write_read"))
 		test_tuntap(NULL, /* xdp */
 			    skel->progs.tc_skb_ext_slice_write,
 			    skel->progs.tc_skb_ext_read,
 			    &skel->bss->test_pass);
-	if (test__start_subtest("tc_skb_ext__no_alloc"))
+	if (test__start_subtest("tc_no_alloc"))
 		test_tuntap(NULL, /* xdp */
 			    skel->progs.tc_skb_ext_no_alloc,
 			    NULL, /* tc prio 2 */
 			    &skel->bss->test_pass);
-	if (test__start_subtest("tc_skb_ext__invalid_flags"))
+	if (test__start_subtest("tc_invalid_flags"))
 		test_tuntap(NULL, /* xdp */
 			    skel->progs.tc_skb_ext_invalid_flags,
 			    NULL, /* tc prio 2 */
 			    &skel->bss->test_pass);
-	if (test__start_subtest("tc_skb_ext__rdonly"))
+	if (test__start_subtest("tc_rdonly"))
 		test_tuntap(NULL, /* xdp */
 			    skel->progs.tc_skb_ext_rdonly,
 			    NULL, /* tc prio 2 */
 			    &skel->bss->test_pass);
-	if (test__start_subtest("tc_skb_ext__double_alloc"))
+	if (test__start_subtest("tc_double_alloc"))
 		test_tuntap(NULL, /* xdp */
 			    skel->progs.tc_skb_ext_double_alloc,
 			    NULL, /* tc prio 2 */
 			    &skel->bss->test_pass);
-
-	test_xdp_meta__destroy(skel);
-}
-
-static void test_mirred_clone_ext_cow(struct test_xdp_meta *skel)
-{
-	struct bpf_link *tp_link;
-
-	skel->bss->clone_cow_done = false;
-	tp_link = bpf_program__attach(skel->progs.tp_kfree_skb_cow_check);
-	if (!ASSERT_OK_PTR(tp_link, "attach_tp"))
-		return;
-
-	test_mirred_clone_ext(skel, skel->progs.tc_skb_ext_clone_redir_cow);
-	bpf_link__destroy(tp_link);
-}
-
-void test_skb_ext_cross_hook(void)
-{
-	struct test_xdp_meta *skel = NULL;
-
-	skel = test_xdp_meta__open_and_load();
-	if (!ASSERT_OK_PTR(skel, "open and load skeleton"))
-		return;
-
 	if (test__start_subtest("tc_to_cgrp"))
 		test_skb_ext_udp(skel, "tc_to_cgrp", READER_CGRP_SKB);
 	if (test__start_subtest("tc_to_sk_filter"))
@@ -927,6 +1091,14 @@ void test_skb_ext_cross_hook(void)
 		test_mirred_clone_ext(skel, skel->progs.tc_skb_ext_read);
 	if (test__start_subtest("clone_ext_cow"))
 		test_mirred_clone_ext_cow(skel);
+	if (test__start_subtest("no_scrub_veth"))
+		test_skb_ext_scrub_veth(skel, true);
+	if (test__start_subtest("scrubbed_veth"))
+		test_skb_ext_scrub_veth(skel, false);
+	if (test__start_subtest("no_scrub_gre"))
+		test_skb_ext_scrub_gre(skel, true);
+	if (test__start_subtest("scrubbed_gre"))
+		test_skb_ext_scrub_gre(skel, false);
 
 	test_xdp_meta__destroy(skel);
 }
diff --git a/tools/testing/selftests/bpf/progs/test_xdp_meta.c b/tools/testing/selftests/bpf/progs/test_xdp_meta.c
index 25fbdc00657c..a1a3cbdb85bc 100644
--- a/tools/testing/selftests/bpf/progs/test_xdp_meta.c
+++ b/tools/testing/selftests/bpf/progs/test_xdp_meta.c
@@ -703,6 +703,24 @@ int tc_skb_ext_write(struct __sk_buff *ctx)
 	return TC_ACT_UNSPEC;
 }
 
+/* Write to skb_ext with NO_SCRUB flag to survive scrubbing */
+SEC("tc")
+int tc_skb_ext_write_no_scrub(struct __sk_buff *ctx)
+{
+	struct bpf_dynptr meta;
+
+	if (!is_test_packet_tc(ctx))
+		return TC_ACT_SHOT;
+	if (bpf_dynptr_from_skb_ext(ctx, 0,
+				    BPF_SKB_EXT_F_CREATE | BPF_SKB_EXT_F_NO_SCRUB,
+				    &meta))
+		return TC_ACT_SHOT;
+	if (bpf_dynptr_write(&meta, 0, (void *)meta_want, ARRAY_SIZE(meta_want), 0))
+		return TC_ACT_SHOT;
+
+	return TC_ACT_UNSPEC;
+}
+
 /* Read from skb-ext metadata using bpf_dynptr_read helper */
 SEC("tc")
 int tc_skb_ext_read(struct __sk_buff *ctx)

-- 
2.43.0


^ permalink raw reply related

* Re: [PATCH v2 net-next 1/3] geneve: pass geneve_config pointer to helper functions
From: Simon Horman @ 2026-07-14 18:03 UTC (permalink / raw)
  To: Eric Dumazet
  Cc: David S . Miller, Jakub Kicinski, Paolo Abeni, Kuniyuki Iwashima,
	Andrew Lunn, netdev, eric.dumazet
In-Reply-To: <20260707145331.3717941-2-edumazet@google.com>

On Tue, Jul 07, 2026 at 02:53:28PM +0000, Eric Dumazet wrote:
> In preparation for converting geneve->cfg to an RCU-protected pointer,
> update helper functions to explicitly accept a const struct geneve_config
> pointer instead of dereferencing geneve->cfg directly.
> 
> Signed-off-by: Eric Dumazet <edumazet@google.com>
> Suggested-by: Paolo Abeni <pabeni@redhat.com>
> ---
>  drivers/net/geneve.c | 140 +++++++++++++++++++++++--------------------
>  1 file changed, 76 insertions(+), 64 deletions(-)
> 
> diff --git a/drivers/net/geneve.c b/drivers/net/geneve.c

...

> @@ -1306,14 +1313,14 @@ static int geneve_build_skb(struct dst_entry *dst, struct sk_buff *skb,
>  }
>  
>  static u8 geneve_get_dsfield(struct sk_buff *skb, struct net_device *dev,
> +			     const struct geneve_config *cfg,
>  			     const struct ip_tunnel_info *info,
>  			     bool *use_cache)
>  {
> -	struct geneve_dev *geneve = netdev_priv(dev);
>  	u8 dsfield;
>  
>  	dsfield = info->key.tos;
> -	if (dsfield == 1 && !geneve->cfg.collect_md) {
> +	if (cfg && dsfield == 1 && !cfg->collect_md) {

Hi Eric,

Sashiko.dev seems to think that the cfg guard is purely defensive.
Which does seem to be the case.
Perhaps it is an artifact from the development of v2?

>  		dsfield = ip_tunnel_get_dsfield(ip_hdr(skb), skb);
>  		*use_cache = false;
>  	}

...

^ permalink raw reply

* Re: (subset) [PATCH v9 00/14] firmware: qcom: Add OP-TEE PAS service support
From: Jeff Johnson @ 2026-07-14 18:07 UTC (permalink / raw)
  To: andersson, konradybcio, Sumit Garg
  Cc: linux-arm-msm, devicetree, dri-devel, freedreno, linux-media,
	netdev, linux-wireless, ath12k, linux-remoteproc, robh, krzk+dt,
	conor+dt, robin.clark, sean, akhilpo, lumag, abhinav.kumar,
	jesszhan0024, marijn.suijten, airlied, simona, vikash.garodia,
	bod, mchehab, elder, andrew+netdev, davem, edumazet, kuba, pabeni,
	jjohnson, mathieu.poirier, trilokkumar.soni, mukesh.ojha,
	pavan.kondeti, jorge.ramirez, tonyh, vignesh.viswanathan,
	srinivas.kandagatla, amirreza.zarrabi, jenswi, op-tee, apurupa,
	skare, linux-kernel, Sumit Garg
In-Reply-To: <20260702115835.167602-1-sumit.garg@kernel.org>


On Thu, 02 Jul 2026 17:28:16 +0530, Sumit Garg wrote:
> From: Sumit Garg <sumit.garg@oss.qualcomm.com>
> 
> Qcom platforms has the legacy of using non-standard SCM calls
> splintered over the various kernel drivers. These SCM calls aren't
> compliant with the standard SMC calling conventions which is a
> prerequisite to enable migration to the FF-A specifications from Arm.
> 
> [...]

Applied, thanks!

[12/14] wifi: ath12k: Switch to generic PAS TZ APIs
        commit: d418509383b0c884b70814ae85d3ef105a63b940

Best regards,
-- 
Jeff Johnson <jeff.johnson@oss.qualcomm.com>


^ permalink raw reply


This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox