BPF List
 help / color / mirror / Atom feed
* [PATCH net-next 00/14] skb extension for BPF metadata
@ 2026-08-14  8:14 Jakub Sitnicki
  2026-08-14  8:14 ` [PATCH net-next 01/14] bpf: Introduce per-packet metadata storage for BPF programs Jakub Sitnicki
                   ` (13 more replies)
  0 siblings, 14 replies; 26+ messages in thread
From: Jakub Sitnicki @ 2026-08-14  8:14 UTC (permalink / raw)
  To: netdev, Alexei Starovoitov, Jakub Kicinski, Kuniyuki Iwashima,
	Paolo Abeni, Stanislav Fomichev
  Cc: bpf, kernel-team, Daniel Borkmann, John Fastabend,
	Andrii Nakryiko, Eduard Zingerman, Kumar Kartikeya Dwivedi,
	Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
	Emil Tsalapatis, David S. Miller, Eric Dumazet, Simon Horman,
	Jesper Dangaard Brouer, Willem de Bruijn

[I realize net-next closes soon. Posting only to collect more feedback.]

Please see the RFC cover letter for the overview [1].
I will focus here just on the latest developments.

1) Based on feedback from the RFC - the BPF skb extension is *no longer
scrubbed* on tunnel encap/decap or veth traversal. There is also *no
mechanism* to enable scrubbing as we don't seem to have a use case for it.

2) Since the RFC I've enabled access to BPF skb extension for the remaining
BPF program types which operate on skbs, namely netfilter, lwt family, and
- everyone's favorite - sk_skb.

Outside the patch set, two things happened:

3) At NetConf 2026 Kuniyuki presented effectively the same idea with a use
case in mind to attach metadata to packets on egress (IIRC). Hoping for
feedback if this meets your needs as well.

4) At BPF Summit 2026, Alexei asked - I'm paraphrasing: What percentage of
skbs will carry metadata in our workload? This determines if the cost of
attaching a tracing prog to consume_skb gets amortized. We've run
experiments and have some answers.

If bpf skb ext existed today, we would adopt it to attach metadata to
incoming TCP connections and ingress UDP packets to CDN to identify flows
that have been forwarded from one PoP to another.

Based on stats from a production node where we've been testing this patch
set, the fraction of skbs that would carry the metadata is <1% (~0.7%):

```
923m16$ sudo perf stat -a -r 10 \
            -e skb:consume_skb -e skb:kfree_skb \
            -e probe:skb_ext_add -- sleep 1

 Performance counter stats for 'system wide' (10 runs):

           334,256      skb:consume_skb                            ( +-  1.63% )
             1,525      skb:kfree_skb                              ( +-  4.09% )
             2,218      probe:skb_ext_add                          ( +- 18.58% )

           1.02305 +- 0.00243 seconds time elapsed  ( +-  0.24% )
```

I expect that even if we adopted bpf skb ext for other use cases we have in
mind, like labeling egress CDN packets with a customer identifier, we would
attach metadata only to around 5% of all skbs in flight.

We've run an experiment to evaluate this patch set - bpf skb extension -
against the alternative - using a BPF map keyed by &skb (LRU_HASH) to stash
metadata plus consume/kfree_skb tracepoint programs to clean up the
entries.

To do that we've modified the CDN component that labels the incoming
connections forwarded from another PoP:

- with bpf skb ext, BPF progs involved in processing consumed 7.5..10 CPU%,
- with BPF map + consume/kfree_skb tp, the overhead was between 15..20 CPU%

Not surprising considering the low fraction of skbs we attach metadata to.
(For graphs see slides 30 & 40 from our presentation at Netdev 0x1A [2].)

Note that we have not tried using RHASH as a BPF metadata stash, to see if
that could bridge the overhead gap, as that would involve deploying a
non-LTS kernel ATM.

Thanks,
-jkbs

[1] https://patch.msgid.link/20260714-bpf-meta-inside-skb-ext-v1-0-5871c07a8dd6@cloudflare.com
[2] https://github.com/jsitnicki/talks/blob/5d64c151f4dc0c38b2832fdcbce7284ec93fdca2/Netdev%200x1A%20-%20Thrice%20the%20charm%20-%20an%20skb%20extension%20for%20BPF%20metadata.pdf

Signed-off-by: Jakub Sitnicki <jakub@cloudflare.com>
---
Changes in v1:
- Don't scrub BPF skb extension. Remove F_NO_SCRUB flag. (Stan)
- Allow calling bpf_dynptr_from_skb_ext from NETFILTER, LWT_*, SK_SKB progs.
- Reorg tests into smaller commits. Add missing coverage.
- Link to RFC: https://patch.msgid.link/20260714-bpf-meta-inside-skb-ext-v1-0-5871c07a8dd6@cloudflare.com

---
Jakub Sitnicki (14):
      bpf: Introduce per-packet metadata storage for BPF programs
      bpf: Allow access to bpf_sock_ops_kern->skb
      bpf: Make BPF skb extension survive packet scrubbing
      selftests/bpf: Add tests for bpf_dynptr_from_skb_ext
      selftests/bpf: Test skb_ext on cloned skbs
      selftests/bpf: Test skb_ext survival across veth and GRE
      selftests/bpf: Test skb_ext read from cgroup_skb and sk_filter hooks
      selftests/bpf: Test skb_ext read from sock_ops and LSM hooks
      selftests/bpf: Test skb_ext read from kfree_skb tracepoint
      selftests/bpf: Test skb_ext read from netfilter hook
      selftests/bpf: Test skb_ext from LWT in, out, and xmit hooks
      selftests/bpf: Test skb_ext read from seg6local End.BPF hook
      selftests/bpf: Test skb_ext read from sk_skb stream verdict hook
      selftests/bpf: Use non-trivial test payload in xdp_context tests

 include/linux/bpf.h                                |  10 +
 include/linux/filter.h                             |  26 +
 include/linux/skbuff.h                             |   5 +
 include/uapi/linux/bpf.h                           |   5 +
 kernel/bpf/helpers.c                               |   7 +
 kernel/bpf/log.c                                   |   2 +
 kernel/bpf/verifier.c                              |  15 +-
 net/Kconfig                                        |  20 +
 net/core/filter.c                                  | 135 +++
 net/core/skbuff.c                                  |  81 +-
 net/ipv4/udp.c                                     |   9 +-
 tools/testing/selftests/bpf/config                 |   1 +
 .../selftests/bpf/prog_tests/socket_helpers.h      |   1 +
 .../bpf/prog_tests/xdp_context_test_run.c          | 988 ++++++++++++++++++++-
 tools/testing/selftests/bpf/progs/test_xdp_meta.c  | 489 +++++++++-
 15 files changed, 1772 insertions(+), 22 deletions(-)


^ permalink raw reply	[flat|nested] 26+ messages in thread

* [PATCH net-next 01/14] bpf: Introduce per-packet metadata storage for BPF programs
  2026-08-14  8:14 [PATCH net-next 00/14] skb extension for BPF metadata Jakub Sitnicki
@ 2026-08-14  8:14 ` Jakub Sitnicki
  2026-08-15  8:14   ` sashiko-bot
  2026-08-14  8:14 ` [PATCH net-next 02/14] bpf: Allow access to bpf_sock_ops_kern->skb Jakub Sitnicki
                   ` (12 subsequent siblings)
  13 siblings, 1 reply; 26+ messages in thread
From: Jakub Sitnicki @ 2026-08-14  8:14 UTC (permalink / raw)
  To: netdev, Alexei Starovoitov, Jakub Kicinski, Kuniyuki Iwashima,
	Paolo Abeni, Stanislav Fomichev
  Cc: bpf, kernel-team, Daniel Borkmann, John Fastabend,
	Andrii Nakryiko, Eduard Zingerman, Kumar Kartikeya Dwivedi,
	Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
	Emil Tsalapatis, David S. Miller, Eric Dumazet, Simon Horman,
	Jesper Dangaard Brouer, Willem de Bruijn

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        | 135 +++++++++++++++++++++++++++++++++++++++++++++++
 net/core/skbuff.c        |   3 ++
 10 files changed, 220 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 14acb2455746..6a2955487504 100644
--- a/include/linux/filter.h
+++ b/include/linux/filter.h
@@ -1914,4 +1914,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 22eda1d54a0e..a4f88107c2d5 100644
--- a/include/linux/skbuff.h
+++ b/include/linux/skbuff.h
@@ -5031,6 +5031,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 c18f1e16edee..aa28cc6a75f7 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 7aa47342dc65..1fab69fec478 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;
@@ -11036,6 +11037,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,
@@ -11116,6 +11118,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)
@@ -12219,7 +12226,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 11bb0d236822..a9a164738389 100644
--- a/net/core/filter.c
+++ b/net/core/filter.c
@@ -12259,6 +12259,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)
@@ -12276,6 +12318,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 (CONFIG_BPF_SKB_EXT_SIZE)
+ * @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
@@ -12576,6 +12683,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)
@@ -12603,6 +12716,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,
@@ -12640,6 +12760,21 @@ 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_SK_SKB, &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_LWT_OUT, &bpf_kfunc_set_skb_ext);
+	ret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_LWT_IN, &bpf_kfunc_set_skb_ext);
+	ret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_LWT_XMIT, &bpf_kfunc_set_skb_ext);
+	ret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_LWT_SEG6LOCAL, &bpf_kfunc_set_skb_ext);
+	ret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_NETFILTER, &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 c82a1472a5ea..9947d2ed6635 100644
--- a/net/core/skbuff.c
+++ b/net/core/skbuff.c
@@ -5155,6 +5155,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	[flat|nested] 26+ messages in thread

* [PATCH net-next 02/14] bpf: Allow access to bpf_sock_ops_kern->skb
  2026-08-14  8:14 [PATCH net-next 00/14] skb extension for BPF metadata Jakub Sitnicki
  2026-08-14  8:14 ` [PATCH net-next 01/14] bpf: Introduce per-packet metadata storage for BPF programs Jakub Sitnicki
@ 2026-08-14  8:14 ` Jakub Sitnicki
  2026-08-14  8:14 ` [PATCH net-next 03/14] bpf: Make BPF skb extension survive packet scrubbing Jakub Sitnicki
                   ` (11 subsequent siblings)
  13 siblings, 0 replies; 26+ messages in thread
From: Jakub Sitnicki @ 2026-08-14  8:14 UTC (permalink / raw)
  To: netdev, Alexei Starovoitov, Jakub Kicinski, Kuniyuki Iwashima,
	Paolo Abeni, Stanislav Fomichev
  Cc: bpf, kernel-team, Daniel Borkmann, John Fastabend,
	Andrii Nakryiko, Eduard Zingerman, Kumar Kartikeya Dwivedi,
	Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
	Emil Tsalapatis, David S. Miller, Eric Dumazet, Simon Horman,
	Jesper Dangaard Brouer, Willem de Bruijn

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 1fab69fec478..b0ddaccf61b3 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -5684,6 +5684,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)
@@ -5726,6 +5730,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	[flat|nested] 26+ messages in thread

* [PATCH net-next 03/14] bpf: Make BPF skb extension survive packet scrubbing
  2026-08-14  8:14 [PATCH net-next 00/14] skb extension for BPF metadata Jakub Sitnicki
  2026-08-14  8:14 ` [PATCH net-next 01/14] bpf: Introduce per-packet metadata storage for BPF programs Jakub Sitnicki
  2026-08-14  8:14 ` [PATCH net-next 02/14] bpf: Allow access to bpf_sock_ops_kern->skb Jakub Sitnicki
@ 2026-08-14  8:14 ` Jakub Sitnicki
  2026-08-15  8:14   ` sashiko-bot
  2026-08-14  8:14 ` [PATCH net-next 04/14] selftests/bpf: Add tests for bpf_dynptr_from_skb_ext Jakub Sitnicki
                   ` (10 subsequent siblings)
  13 siblings, 1 reply; 26+ messages in thread
From: Jakub Sitnicki @ 2026-08-14  8:14 UTC (permalink / raw)
  To: netdev, Alexei Starovoitov, Jakub Kicinski, Kuniyuki Iwashima,
	Paolo Abeni, Stanislav Fomichev
  Cc: bpf, kernel-team, Daniel Borkmann, John Fastabend,
	Andrii Nakryiko, Eduard Zingerman, Kumar Kartikeya Dwivedi,
	Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
	Emil Tsalapatis, David S. Miller, Eric Dumazet, Simon Horman,
	Jesper Dangaard Brouer, Willem de Bruijn

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().

Switch udp_try_make_stateless() to skb_ext_scrub() as well, so that the
metadata stays readable until the packet is queued on a UDP socket
receive queue (e.g. for a sockmap verdict program). Only mark the skb
stateless when no extension survives the scrub. Otherwise
skb_consume_udp() would take the __consume_stateless_skb() fast path,
which skips skb_release_head_state(), and leak the extension slab.

Finally, make BPF skb extension survive skb scrubbing.

Signed-off-by: Jakub Sitnicki <jakub@cloudflare.com>
---
 include/linux/skbuff.h |  2 ++
 net/core/skbuff.c      | 78 +++++++++++++++++++++++++++++++++++++++++++-------
 net/ipv4/udp.c         |  9 ++++--
 3 files changed, 76 insertions(+), 13 deletions(-)

diff --git a/include/linux/skbuff.h b/include/linux/skbuff.h
index a4f88107c2d5..286f2e4cdbb1 100644
--- a/include/linux/skbuff.h
+++ b/include/linux/skbuff.h
@@ -5061,6 +5061,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)
 {
@@ -5130,6 +5131,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/net/core/skbuff.c b/net/core/skbuff.c
index 9947d2ed6635..700bee045b5c 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>
@@ -6284,7 +6285,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);
 
@@ -7258,6 +7259,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
@@ -7269,18 +7284,61 @@ 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))
+		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 af9603217444..865655db2185 100644
--- a/net/ipv4/udp.c
+++ b/net/ipv4/udp.c
@@ -1530,7 +1530,10 @@ void udp_splice_eof(struct socket *sock)
  * We need to preserve secpath, if present, to eventually process
  * IP_CMSG_PASSSEC at recvmsg() time.
  *
- * Other extensions can be cleared.
+ * Other extensions get scrubbed. If any extension survives scrubbing
+ * (e.g. SKB_EXT_BPF), the skb must stay stateful, so that
+ * skb_consume_udp() releases it via skb_release_head_state() instead of
+ * leaking the extension slab on the __consume_stateless_skb() path.
  */
 static bool udp_try_make_stateless(struct sk_buff *skb)
 {
@@ -1538,8 +1541,8 @@ static bool udp_try_make_stateless(struct sk_buff *skb)
 		return true;
 
 	if (!secpath_exists(skb)) {
-		skb_ext_reset(skb);
-		return true;
+		skb_ext_scrub(skb);
+		return !skb_has_extensions(skb);
 	}
 
 	return false;

-- 
2.43.0


^ permalink raw reply related	[flat|nested] 26+ messages in thread

* [PATCH net-next 04/14] selftests/bpf: Add tests for bpf_dynptr_from_skb_ext
  2026-08-14  8:14 [PATCH net-next 00/14] skb extension for BPF metadata Jakub Sitnicki
                   ` (2 preceding siblings ...)
  2026-08-14  8:14 ` [PATCH net-next 03/14] bpf: Make BPF skb extension survive packet scrubbing Jakub Sitnicki
@ 2026-08-14  8:14 ` Jakub Sitnicki
  2026-08-15  8:14   ` sashiko-bot
  2026-08-14  8:14 ` [PATCH net-next 05/14] selftests/bpf: Test skb_ext on cloned skbs Jakub Sitnicki
                   ` (9 subsequent siblings)
  13 siblings, 1 reply; 26+ messages in thread
From: Jakub Sitnicki @ 2026-08-14  8:14 UTC (permalink / raw)
  To: netdev, Alexei Starovoitov, Jakub Kicinski, Kuniyuki Iwashima,
	Paolo Abeni, Stanislav Fomichev
  Cc: bpf, kernel-team, Daniel Borkmann, John Fastabend,
	Andrii Nakryiko, Eduard Zingerman, Kumar Kartikeya Dwivedi,
	Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
	Emil Tsalapatis, David S. Miller, Eric Dumazet, Simon Horman,
	Jesper Dangaard Brouer, Willem de Bruijn

Cover the bpf_dynptr_from_skb_ext() kfunc and the dynptr interface
to skb_ext 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

Signed-off-by: Jakub Sitnicki <jakub@cloudflare.com>
---
 tools/testing/selftests/bpf/config                 |   1 +
 .../bpf/prog_tests/xdp_context_test_run.c          |  52 ++++++
 tools/testing/selftests/bpf/progs/test_xdp_meta.c  | 185 +++++++++++++++++++++
 3 files changed, 238 insertions(+)

diff --git a/tools/testing/selftests/bpf/config b/tools/testing/selftests/bpf/config
index ea7044f30adc..502f4504bdba 100644
--- a/tools/testing/selftests/bpf/config
+++ b/tools/testing/selftests/bpf/config
@@ -7,6 +7,7 @@ CONFIG_BPF_JIT=y
 CONFIG_BPF_KPROBE_OVERRIDE=y
 CONFIG_BPF_LIRC_MODE2=y
 CONFIG_BPF_LSM=y
+CONFIG_BPF_SKB_EXT=y
 CONFIG_BPF_STREAM_PARSER=y
 CONFIG_BPF_SYSCALL=y
 # CONFIG_BPF_UNPRIV_DEFAULT_OFF is not set
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 448807676176..fd340e5538a3 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
@@ -693,3 +693,55 @@ void test_xdp_context_lwt_encap(void)
 
 	test_xdp_meta__destroy(skel);
 }
+
+void test_skb_ext_basic(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_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_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_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_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_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_rdonly"))
+		test_tuntap(NULL, /* xdp */
+			    skel->progs.tc_skb_ext_rdonly,
+			    NULL, /* tc prio 2 */
+			    &skel->bss->test_pass);
+	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);
+}
diff --git a/tools/testing/selftests/bpf/progs/test_xdp_meta.c b/tools/testing/selftests/bpf/progs/test_xdp_meta.c
index 08b03be0b891..43840ee32d35 100644
--- a/tools/testing/selftests/bpf/progs/test_xdp_meta.c
+++ b/tools/testing/selftests/bpf/progs/test_xdp_meta.c
@@ -6,6 +6,7 @@
 #include <errno.h>
 
 #include "bpf_kfuncs.h"
+#include "bpf_misc.h"
 #include "bpf_tracing_net.h"
 
 #define META_SIZE 32
@@ -689,4 +690,188 @@ 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;
+}
+
 char _license[] SEC("license") = "GPL";

-- 
2.43.0


^ permalink raw reply related	[flat|nested] 26+ messages in thread

* [PATCH net-next 05/14] selftests/bpf: Test skb_ext on cloned skbs
  2026-08-14  8:14 [PATCH net-next 00/14] skb extension for BPF metadata Jakub Sitnicki
                   ` (3 preceding siblings ...)
  2026-08-14  8:14 ` [PATCH net-next 04/14] selftests/bpf: Add tests for bpf_dynptr_from_skb_ext Jakub Sitnicki
@ 2026-08-14  8:14 ` Jakub Sitnicki
  2026-08-15  8:14   ` sashiko-bot
  2026-08-14  8:14 ` [PATCH net-next 06/14] selftests/bpf: Test skb_ext survival across veth and GRE Jakub Sitnicki
                   ` (8 subsequent siblings)
  13 siblings, 1 reply; 26+ messages in thread
From: Jakub Sitnicki @ 2026-08-14  8:14 UTC (permalink / raw)
  To: netdev, Alexei Starovoitov, Jakub Kicinski, Kuniyuki Iwashima,
	Paolo Abeni, Stanislav Fomichev
  Cc: bpf, kernel-team, Daniel Borkmann, John Fastabend,
	Andrii Nakryiko, Eduard Zingerman, Kumar Kartikeya Dwivedi,
	Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
	Emil Tsalapatis, David S. Miller, Eric Dumazet, Simon Horman,
	Jesper Dangaard Brouer, Willem de Bruijn

Cover skb_ext behavior when an skb is cloned by TC mirred
(mirror to a dummy device):

- clone_ext_read: the extension written at tap ingress is readable
  from the mirred clone at dummy ingress -- the clone shares the
  extension with the original
- clone_ext_cow: opening the extension with F_CREATE on the clone
  triggers copy-on-write, so overwriting the clone's data does not
  affect the original -- verified by a tp_btf/kfree_skb probe that
  checks the original skb still carries meta_want

Signed-off-by: Jakub Sitnicki <jakub@cloudflare.com>
---
 .../bpf/prog_tests/xdp_context_test_run.c          | 93 ++++++++++++++++++++++
 tools/testing/selftests/bpf/progs/test_xdp_meta.c  | 46 +++++++++++
 2 files changed, 139 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 fd340e5538a3..58b793cf14e5 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
@@ -694,6 +694,95 @@ void test_xdp_context_lwt_encap(void)
 	test_xdp_meta__destroy(skel);
 }
 
+/* Test if 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);
+}
+
+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_basic(void)
 {
 	struct test_xdp_meta *skel = NULL;
@@ -742,6 +831,10 @@ void test_skb_ext_basic(void)
 			    skel->progs.tc_skb_ext_double_alloc,
 			    NULL, /* tc prio 2 */
 			    &skel->bss->test_pass);
+	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 43840ee32d35..1b6bb20ee7e8 100644
--- a/tools/testing/selftests/bpf/progs/test_xdp_meta.c
+++ b/tools/testing/selftests/bpf/progs/test_xdp_meta.c
@@ -3,6 +3,7 @@
 
 #include <bpf/bpf_endian.h>
 #include <bpf/bpf_helpers.h>
+#include <bpf/bpf_tracing.h>
 #include <errno.h>
 
 #include "bpf_kfuncs.h"
@@ -874,4 +875,49 @@ int tc_skb_ext_double_alloc(struct __sk_buff *ctx)
 	return TC_ACT_UNSPEC;
 }
 
+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))
+		return 0;
+
+	test_pass = true;
+	return 0;
+}
+
 char _license[] SEC("license") = "GPL";

-- 
2.43.0


^ permalink raw reply related	[flat|nested] 26+ messages in thread

* [PATCH net-next 06/14] selftests/bpf: Test skb_ext survival across veth and GRE
  2026-08-14  8:14 [PATCH net-next 00/14] skb extension for BPF metadata Jakub Sitnicki
                   ` (4 preceding siblings ...)
  2026-08-14  8:14 ` [PATCH net-next 05/14] selftests/bpf: Test skb_ext on cloned skbs Jakub Sitnicki
@ 2026-08-14  8:14 ` Jakub Sitnicki
  2026-08-15  8:14   ` sashiko-bot
  2026-08-14  8:14 ` [PATCH net-next 07/14] selftests/bpf: Test skb_ext read from cgroup_skb and sk_filter hooks Jakub Sitnicki
                   ` (7 subsequent siblings)
  13 siblings, 1 reply; 26+ messages in thread
From: Jakub Sitnicki @ 2026-08-14  8:14 UTC (permalink / raw)
  To: netdev, Alexei Starovoitov, Jakub Kicinski, Kuniyuki Iwashima,
	Paolo Abeni, Stanislav Fomichev
  Cc: bpf, kernel-team, Daniel Borkmann, John Fastabend,
	Andrii Nakryiko, Eduard Zingerman, Kumar Kartikeya Dwivedi,
	Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
	Emil Tsalapatis, David S. Miller, Eric Dumazet, Simon Horman,
	Jesper Dangaard Brouer, Willem de Bruijn

Write skb_ext at TC egress and read it back at TC ingress after the
packet crosses device boundaries:

- survives_veth: veth forwarding between two netns
- survives_gre: GRE encap/decap between two gretap endpoints over
  loopback in a single netns

Signed-off-by: Jakub Sitnicki <jakub@cloudflare.com>
---
 .../bpf/prog_tests/xdp_context_test_run.c          | 154 +++++++++++++++++++++
 1 file changed, 154 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 58b793cf14e5..de6030302536 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
@@ -783,6 +783,156 @@ static void test_mirred_clone_ext_cow(struct test_xdp_meta *skel)
 	bpf_link__destroy(tp_link);
 }
 
+/* Test if skb_ext survives veth cross-netns forward */
+static void test_skb_ext_scrub_veth(struct test_xdp_meta *skel)
+{
+	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 = 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;
+
+	ASSERT_TRUE(skel->bss->test_pass, "test_pass");
+
+close:
+	close_netns(nstoken);
+	netns_free(rx_ns);
+	netns_free(tx_ns);
+}
+
+/* Test if skb_ext survives GRE tunnel encap+decap */
+static void test_skb_ext_scrub_gre(struct test_xdp_meta *skel)
+{
+	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 = 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;
+
+	ASSERT_TRUE(skel->bss->test_pass, "test_pass");
+
+close:
+	netns_free(ns);
+}
+
 void test_skb_ext_basic(void)
 {
 	struct test_xdp_meta *skel = NULL;
@@ -835,6 +985,10 @@ void test_skb_ext_basic(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("survives_veth"))
+		test_skb_ext_scrub_veth(skel);
+	if (test__start_subtest("survives_gre"))
+		test_skb_ext_scrub_gre(skel);
 
 	test_xdp_meta__destroy(skel);
 }

-- 
2.43.0


^ permalink raw reply related	[flat|nested] 26+ messages in thread

* [PATCH net-next 07/14] selftests/bpf: Test skb_ext read from cgroup_skb and sk_filter hooks
  2026-08-14  8:14 [PATCH net-next 00/14] skb extension for BPF metadata Jakub Sitnicki
                   ` (5 preceding siblings ...)
  2026-08-14  8:14 ` [PATCH net-next 06/14] selftests/bpf: Test skb_ext survival across veth and GRE Jakub Sitnicki
@ 2026-08-14  8:14 ` Jakub Sitnicki
  2026-08-15  8:14   ` sashiko-bot
  2026-08-14  8:14 ` [PATCH net-next 08/14] selftests/bpf: Test skb_ext read from sock_ops and LSM hooks Jakub Sitnicki
                   ` (6 subsequent siblings)
  13 siblings, 1 reply; 26+ messages in thread
From: Jakub Sitnicki @ 2026-08-14  8:14 UTC (permalink / raw)
  To: netdev, Alexei Starovoitov, Jakub Kicinski, Kuniyuki Iwashima,
	Paolo Abeni, Stanislav Fomichev
  Cc: bpf, kernel-team, Daniel Borkmann, John Fastabend,
	Andrii Nakryiko, Eduard Zingerman, Kumar Kartikeya Dwivedi,
	Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
	Emil Tsalapatis, David S. Miller, Eric Dumazet, Simon Horman,
	Jesper Dangaard Brouer, Willem de Bruijn

Extend skb_ext cross-hook coverage to UDP readers: attach a TC
ingress program on loopback that writes the extension, send a UDP
datagram over loopback, and read the extension back from a
cgroup_skb/ingress program and from a socket filter attached with
SO_ATTACH_BPF.

Signed-off-by: Jakub Sitnicki <jakub@cloudflare.com>
---
 .../bpf/prog_tests/xdp_context_test_run.c          | 121 +++++++++++++++++++++
 tools/testing/selftests/bpf/progs/test_xdp_meta.c  |  37 +++++++
 2 files changed, 158 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 de6030302536..808eb158fe0f 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
@@ -992,3 +992,124 @@ void test_skb_ext_basic(void)
 
 	test_xdp_meta__destroy(skel);
 }
+
+/* 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);
+}
+
+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_ingress"))
+		test_skb_ext_udp(skel, "tc_to_cgrp_ingress", READER_CGRP_SKB);
+	if (test__start_subtest("tc_to_sk_filter"))
+		test_skb_ext_udp(skel, "tc_to_sk_filter", READER_SK_FILTER);
+
+	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 1b6bb20ee7e8..0a3afb5c3461 100644
--- a/tools/testing/selftests/bpf/progs/test_xdp_meta.c
+++ b/tools/testing/selftests/bpf/progs/test_xdp_meta.c
@@ -920,4 +920,41 @@ int BPF_PROG(tp_kfree_skb_cow_check, struct sk_buff *skb)
 	return 0;
 }
 
+/* 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;
+}
+
+/* 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;
+}
+
 char _license[] SEC("license") = "GPL";

-- 
2.43.0


^ permalink raw reply related	[flat|nested] 26+ messages in thread

* [PATCH net-next 08/14] selftests/bpf: Test skb_ext read from sock_ops and LSM hooks
  2026-08-14  8:14 [PATCH net-next 00/14] skb extension for BPF metadata Jakub Sitnicki
                   ` (6 preceding siblings ...)
  2026-08-14  8:14 ` [PATCH net-next 07/14] selftests/bpf: Test skb_ext read from cgroup_skb and sk_filter hooks Jakub Sitnicki
@ 2026-08-14  8:14 ` Jakub Sitnicki
  2026-08-15  8:14   ` sashiko-bot
  2026-08-14  8:14 ` [PATCH net-next 09/14] selftests/bpf: Test skb_ext read from kfree_skb tracepoint Jakub Sitnicki
                   ` (5 subsequent siblings)
  13 siblings, 1 reply; 26+ messages in thread
From: Jakub Sitnicki @ 2026-08-14  8:14 UTC (permalink / raw)
  To: netdev, Alexei Starovoitov, Jakub Kicinski, Kuniyuki Iwashima,
	Paolo Abeni, Stanislav Fomichev
  Cc: bpf, kernel-team, Daniel Borkmann, John Fastabend,
	Andrii Nakryiko, Eduard Zingerman, Kumar Kartikeya Dwivedi,
	Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
	Emil Tsalapatis, David S. Miller, Eric Dumazet, Simon Horman,
	Jesper Dangaard Brouer, Willem de Bruijn

Extend skb_ext test coverage to hook on TCP ingress path: attach a TC
ingress program on loopback that writes the metadata to packets, and read
it back from a sock_ops program (BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB,
reaching the skb via bpf_sock_ops_kern->skb) and from the LSM
inet_conn_established hook.

Signed-off-by: Jakub Sitnicki <jakub@cloudflare.com>
---
 .../bpf/prog_tests/xdp_context_test_run.c          | 92 ++++++++++++++++++++++
 tools/testing/selftests/bpf/progs/test_xdp_meta.c  | 75 ++++++++++++++++++
 2 files changed, 167 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 808eb158fe0f..e0ab0fccf9d7 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
@@ -1098,6 +1098,94 @@ static void test_skb_ext_udp(struct test_xdp_meta *skel, const char *name,
 	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);
+}
+
 void test_skb_ext_cross_hook(void)
 {
 	struct test_xdp_meta *skel = NULL;
@@ -1110,6 +1198,10 @@ void test_skb_ext_cross_hook(void)
 		test_skb_ext_udp(skel, "tc_to_cgrp_ingress", 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);
 
 	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 0a3afb5c3461..8f0a0fea5d84 100644
--- a/tools/testing/selftests/bpf/progs/test_xdp_meta.c
+++ b/tools/testing/selftests/bpf/progs/test_xdp_meta.c
@@ -938,6 +938,81 @@ int cgrp_skb_ext_read(struct __sk_buff *ctx)
 	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)

-- 
2.43.0


^ permalink raw reply related	[flat|nested] 26+ messages in thread

* [PATCH net-next 09/14] selftests/bpf: Test skb_ext read from kfree_skb tracepoint
  2026-08-14  8:14 [PATCH net-next 00/14] skb extension for BPF metadata Jakub Sitnicki
                   ` (7 preceding siblings ...)
  2026-08-14  8:14 ` [PATCH net-next 08/14] selftests/bpf: Test skb_ext read from sock_ops and LSM hooks Jakub Sitnicki
@ 2026-08-14  8:14 ` Jakub Sitnicki
  2026-08-15  8:14   ` sashiko-bot
  2026-08-14  8:14 ` [PATCH net-next 10/14] selftests/bpf: Test skb_ext read from netfilter hook Jakub Sitnicki
                   ` (4 subsequent siblings)
  13 siblings, 1 reply; 26+ messages in thread
From: Jakub Sitnicki @ 2026-08-14  8:14 UTC (permalink / raw)
  To: netdev, Alexei Starovoitov, Jakub Kicinski, Kuniyuki Iwashima,
	Paolo Abeni, Stanislav Fomichev
  Cc: bpf, kernel-team, Daniel Borkmann, John Fastabend,
	Andrii Nakryiko, Eduard Zingerman, Kumar Kartikeya Dwivedi,
	Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
	Emil Tsalapatis, David S. Miller, Eric Dumazet, Simon Horman,
	Jesper Dangaard Brouer, Willem de Bruijn

Write skb_ext at cgroup/skb egress, send UDP to a closed loopback port so
the packet is dropped, and read the extension back from a tp_btf/kfree_skb
program.

Signed-off-by: Jakub Sitnicki <jakub@cloudflare.com>
---
 .../bpf/prog_tests/xdp_context_test_run.c          | 65 ++++++++++++++++++++++
 tools/testing/selftests/bpf/progs/test_xdp_meta.c  | 34 +++++++++++
 2 files changed, 99 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 e0ab0fccf9d7..1e4e12d2b041 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
@@ -1186,6 +1186,69 @@ static void test_skb_ext_tcp(struct test_xdp_meta *skel, const char *name,
 	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_skb_ext_cross_hook(void)
 {
 	struct test_xdp_meta *skel = NULL;
@@ -1202,6 +1265,8 @@ void test_skb_ext_cross_hook(void)
 		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);
 
 	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 8f0a0fea5d84..415b33b32ef5 100644
--- a/tools/testing/selftests/bpf/progs/test_xdp_meta.c
+++ b/tools/testing/selftests/bpf/progs/test_xdp_meta.c
@@ -1032,4 +1032,38 @@ int sk_filter_skb_ext_read(struct __sk_buff *ctx)
 	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;
+}
+
+/* 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	[flat|nested] 26+ messages in thread

* [PATCH net-next 10/14] selftests/bpf: Test skb_ext read from netfilter hook
  2026-08-14  8:14 [PATCH net-next 00/14] skb extension for BPF metadata Jakub Sitnicki
                   ` (8 preceding siblings ...)
  2026-08-14  8:14 ` [PATCH net-next 09/14] selftests/bpf: Test skb_ext read from kfree_skb tracepoint Jakub Sitnicki
@ 2026-08-14  8:14 ` Jakub Sitnicki
  2026-08-14  8:14 ` [PATCH net-next 11/14] selftests/bpf: Test skb_ext from LWT in, out, and xmit hooks Jakub Sitnicki
                   ` (3 subsequent siblings)
  13 siblings, 0 replies; 26+ messages in thread
From: Jakub Sitnicki @ 2026-08-14  8:14 UTC (permalink / raw)
  To: netdev, Alexei Starovoitov, Jakub Kicinski, Kuniyuki Iwashima,
	Paolo Abeni, Stanislav Fomichev
  Cc: bpf, kernel-team, Daniel Borkmann, John Fastabend,
	Andrii Nakryiko, Eduard Zingerman, Kumar Kartikeya Dwivedi,
	Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
	Emil Tsalapatis, David S. Miller, Eric Dumazet, Simon Horman,
	Jesper Dangaard Brouer, Willem de Bruijn

Add a TC -> netfilter cross-hook test for bpf_dynptr_from_skb_ext(). A TC
ingress program writes skb_ext on loopback, and a netfilter program at
NF_INET_LOCAL_IN reads it back.

Signed-off-by: Jakub Sitnicki <jakub@cloudflare.com>
---
 .../bpf/prog_tests/xdp_context_test_run.c          | 56 ++++++++++++++++++++++
 tools/testing/selftests/bpf/progs/test_xdp_meta.c  | 23 +++++++++
 2 files changed, 79 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 1e4e12d2b041..82c57ea3957e 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
@@ -2,6 +2,7 @@
 #include <test_progs.h>
 #include <network_helpers.h>
 #include <linux/ipv6.h>
+#include <linux/netfilter.h>
 #include <arpa/inet.h>
 #include "test_xdp_context_test_run.skel.h"
 #include "test_xdp_meta.skel.h"
@@ -1249,6 +1250,59 @@ static void test_cgrp_egress_to_kfree_skb(struct test_xdp_meta *skel)
 		close(cgroup_fd);
 }
 
+/* Test skb_ext survival across TC ingress -> netfilter hook */
+static void test_skb_ext_nf(struct test_xdp_meta *skel, const char *name)
+{
+	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);
+	LIBBPF_OPTS(bpf_netfilter_opts, nf_opts,
+		    .pf = NFPROTO_IPV4,
+		    .hooknum = NF_INET_LOCAL_IN,
+		    .priority = 1);
+	struct bpf_link *reader_link = NULL;
+	struct netns_obj *ns = NULL;
+	int server_fd = -1;
+	int ret;
+
+	ns = netns_new(name, true);
+	if (!ASSERT_OK_PTR(ns, "netns_new"))
+		return;
+
+	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;
+
+	reader_link = bpf_program__attach_netfilter(skel->progs.nf_skb_ext_read,
+						    &nf_opts);
+	if (!ASSERT_OK_PTR(reader_link, "attach_nf"))
+		goto cleanup;
+
+	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);
+	netns_free(ns);
+}
+
 void test_skb_ext_cross_hook(void)
 {
 	struct test_xdp_meta *skel = NULL;
@@ -1267,6 +1321,8 @@ void test_skb_ext_cross_hook(void)
 		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("tc_to_nf"))
+		test_skb_ext_nf(skel, "tc_to_nf");
 
 	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 415b33b32ef5..0688120f3d40 100644
--- a/tools/testing/selftests/bpf/progs/test_xdp_meta.c
+++ b/tools/testing/selftests/bpf/progs/test_xdp_meta.c
@@ -1066,4 +1066,27 @@ int BPF_PROG(tp_kfree_skb_ext_read, struct sk_buff *skb)
 	return 0;
 }
 
+#define NF_ACCEPT 1
+
+SEC("netfilter")
+int nf_skb_ext_read(struct bpf_nf_ctx *ctx)
+{
+	struct __sk_buff *skb = (struct __sk_buff *)ctx->skb;
+	__u8 meta_have[META_SIZE];
+	struct bpf_dynptr meta;
+
+	if (!skb)
+		return NF_ACCEPT;
+
+	if (bpf_dynptr_from_skb_ext(skb, 0, 0, &meta))
+		return NF_ACCEPT;
+	if (bpf_dynptr_read(meta_have, META_SIZE, &meta, 0, 0))
+		return NF_ACCEPT;
+	if (!check_metadata(meta_have))
+		return NF_ACCEPT;
+
+	test_pass = true;
+	return NF_ACCEPT;
+}
+
 char _license[] SEC("license") = "GPL";

-- 
2.43.0


^ permalink raw reply related	[flat|nested] 26+ messages in thread

* [PATCH net-next 11/14] selftests/bpf: Test skb_ext from LWT in, out, and xmit hooks
  2026-08-14  8:14 [PATCH net-next 00/14] skb extension for BPF metadata Jakub Sitnicki
                   ` (9 preceding siblings ...)
  2026-08-14  8:14 ` [PATCH net-next 10/14] selftests/bpf: Test skb_ext read from netfilter hook Jakub Sitnicki
@ 2026-08-14  8:14 ` Jakub Sitnicki
  2026-08-15  8:14   ` sashiko-bot
  2026-08-14  8:14 ` [PATCH net-next 12/14] selftests/bpf: Test skb_ext read from seg6local End.BPF hook Jakub Sitnicki
                   ` (2 subsequent siblings)
  13 siblings, 1 reply; 26+ messages in thread
From: Jakub Sitnicki @ 2026-08-14  8:14 UTC (permalink / raw)
  To: netdev, Alexei Starovoitov, Jakub Kicinski, Kuniyuki Iwashima,
	Paolo Abeni, Stanislav Fomichev
  Cc: bpf, kernel-team, Daniel Borkmann, John Fastabend,
	Andrii Nakryiko, Eduard Zingerman, Kumar Kartikeya Dwivedi,
	Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
	Emil Tsalapatis, David S. Miller, Eric Dumazet, Simon Horman,
	Jesper Dangaard Brouer, Willem de Bruijn

Add cross-hook tests for bpf_dynptr_from_skb_ext() covering all
generic LWT program types. For lwt_in, a TC ingress program writes
skb_ext on loopback and the LWT program reads it back. For lwt_out
and lwt_xmit, the LWT programs write skb_ext on the output path and a
TC ingress program reads it back after loopback.

Signed-off-by: Jakub Sitnicki <jakub@cloudflare.com>
---
 .../bpf/prog_tests/xdp_context_test_run.c          | 98 ++++++++++++++++++++++
 tools/testing/selftests/bpf/progs/test_xdp_meta.c  | 45 ++++++++++
 2 files changed, 143 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 82c57ea3957e..7a7537ef80e7 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
@@ -1303,6 +1303,95 @@ static void test_skb_ext_nf(struct test_xdp_meta *skel, const char *name)
 	netns_free(ns);
 }
 
+#define LWT_EXT_PIN_PATH "/sys/fs/bpf/skb_ext_lwt"
+
+/* Test skb_ext across LWT hooks on loopback.
+ *
+ * @lwt_prog:  BPF program to pin and attach via ip route encap
+ * @encap_dir: "in", "out", or "xmit"
+ * @writer:    true if lwt_prog writes skb_ext (reader on TC ingress),
+ *             false if lwt_prog reads skb_ext (writer on TC ingress)
+ */
+static void test_skb_ext_lwt(struct test_xdp_meta *skel, const char *name,
+			     struct bpf_program *lwt_prog,
+			     const char *encap_dir, bool writer)
+{
+	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 sockaddr_in addr = {
+		.sin_family = AF_INET,
+	};
+	struct bpf_program *tc_prog;
+	char buf[TEST_PAYLOAD_LEN];
+	struct netns_obj *ns = NULL;
+	bool pinned = false;
+	int server_fd = -1;
+	int fd = -1;
+	__be16 port;
+	int ret;
+
+	unlink(LWT_EXT_PIN_PATH);
+	ret = bpf_program__pin(lwt_prog, LWT_EXT_PIN_PATH);
+	if (!ASSERT_OK(ret, "pin lwt"))
+		return;
+	pinned = true;
+
+	ns = netns_new(name, true);
+	if (!ASSERT_OK_PTR(ns, "netns_new"))
+		goto cleanup;
+
+	SYS(cleanup, "ip addr add 10.0.0.1/32 dev lo");
+	SYS(cleanup, "ip route replace table local local 10.0.0.1 "
+		     "encap bpf %s pinned " LWT_EXT_PIN_PATH " dev lo",
+		     encap_dir);
+
+	server_fd = start_server(AF_INET, SOCK_DGRAM, "10.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;
+
+	/* When LWT writes, TC ingress reads; when LWT reads, TC ingress writes */
+	tc_prog = writer ? skel->progs.tc_skb_ext_read
+			 : skel->progs.tc_skb_ext_write;
+	tc_opts.prog_fd = bpf_program__fd(tc_prog);
+	ret = bpf_tc_attach(&tc_hook, &tc_opts);
+	if (!ASSERT_OK(ret, "bpf_tc_attach"))
+		goto cleanup;
+
+	port = get_socket_local_port(server_fd);
+	if (!ASSERT_GE(port, 0, "get_port"))
+		goto cleanup;
+
+	fd = socket(AF_INET, SOCK_DGRAM, 0);
+	if (!ASSERT_GE(fd, 0, "socket"))
+		goto cleanup;
+
+	inet_pton(AF_INET, "10.0.0.1", &addr.sin_addr);
+	addr.sin_port = port;
+	sendto(fd, test_payload, TEST_PAYLOAD_LEN, 0,
+	       (void *)&addr, sizeof(addr));
+	recvfrom(server_fd, buf, sizeof(buf), 0, NULL, NULL);
+
+	ASSERT_TRUE(skel->bss->test_pass, "test_pass");
+
+cleanup:
+	if (fd >= 0)
+		close(fd);
+	bpf_tc_hook_destroy(&tc_hook);
+	if (server_fd >= 0)
+		close(server_fd);
+	netns_free(ns);
+	if (pinned)
+		unlink(LWT_EXT_PIN_PATH);
+}
+
 void test_skb_ext_cross_hook(void)
 {
 	struct test_xdp_meta *skel = NULL;
@@ -1323,6 +1412,15 @@ void test_skb_ext_cross_hook(void)
 		test_cgrp_egress_to_kfree_skb(skel);
 	if (test__start_subtest("tc_to_nf"))
 		test_skb_ext_nf(skel, "tc_to_nf");
+	if (test__start_subtest("tc_to_lwt_in"))
+		test_skb_ext_lwt(skel, "tc_to_lwt_in",
+				 skel->progs.lwt_in_skb_ext_read, "in", false);
+	if (test__start_subtest("lwt_out_to_tc"))
+		test_skb_ext_lwt(skel, "lwt_out_to_tc",
+				 skel->progs.lwt_out_skb_ext_write, "out", true);
+	if (test__start_subtest("lwt_xmit_to_tc"))
+		test_skb_ext_lwt(skel, "lwt_xmit_to_tc",
+				 skel->progs.lwt_xmit_skb_ext_write, "xmit", true);
 
 	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 0688120f3d40..97c1760fbd6d 100644
--- a/tools/testing/selftests/bpf/progs/test_xdp_meta.c
+++ b/tools/testing/selftests/bpf/progs/test_xdp_meta.c
@@ -1089,4 +1089,49 @@ int nf_skb_ext_read(struct bpf_nf_ctx *ctx)
 	return NF_ACCEPT;
 }
 
+SEC("lwt_in")
+int lwt_in_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 BPF_OK;
+	if (bpf_dynptr_read(meta_have, META_SIZE, &meta, 0, 0))
+		return BPF_OK;
+	if (!check_metadata(meta_have))
+		return BPF_OK;
+
+	test_pass = true;
+	return BPF_OK;
+}
+
+SEC("lwt_out")
+int lwt_out_skb_ext_write(struct __sk_buff *ctx)
+{
+	struct bpf_dynptr meta;
+
+	if (!is_test_packet_tc(ctx))
+		return BPF_OK;
+	if (bpf_dynptr_from_skb_ext(ctx, 0, BPF_SKB_EXT_F_CREATE, &meta))
+		return BPF_OK;
+	bpf_dynptr_write(&meta, 0, (void *)meta_want, ARRAY_SIZE(meta_want), 0);
+
+	return BPF_OK;
+}
+
+SEC("lwt_xmit")
+int lwt_xmit_skb_ext_write(struct __sk_buff *ctx)
+{
+	struct bpf_dynptr meta;
+
+	if (!is_test_packet_tc(ctx))
+		return BPF_OK;
+	if (bpf_dynptr_from_skb_ext(ctx, 0, BPF_SKB_EXT_F_CREATE, &meta))
+		return BPF_OK;
+	bpf_dynptr_write(&meta, 0, (void *)meta_want, ARRAY_SIZE(meta_want), 0);
+
+	return BPF_OK;
+}
+
 char _license[] SEC("license") = "GPL";

-- 
2.43.0


^ permalink raw reply related	[flat|nested] 26+ messages in thread

* [PATCH net-next 12/14] selftests/bpf: Test skb_ext read from seg6local End.BPF hook
  2026-08-14  8:14 [PATCH net-next 00/14] skb extension for BPF metadata Jakub Sitnicki
                   ` (10 preceding siblings ...)
  2026-08-14  8:14 ` [PATCH net-next 11/14] selftests/bpf: Test skb_ext from LWT in, out, and xmit hooks Jakub Sitnicki
@ 2026-08-14  8:14 ` Jakub Sitnicki
  2026-08-15  8:14   ` sashiko-bot
  2026-08-14  8:14 ` [PATCH net-next 13/14] selftests/bpf: Test skb_ext read from sk_skb stream verdict hook Jakub Sitnicki
  2026-08-14  8:14 ` [PATCH net-next 14/14] selftests/bpf: Use non-trivial test payload in xdp_context tests Jakub Sitnicki
  13 siblings, 1 reply; 26+ messages in thread
From: Jakub Sitnicki @ 2026-08-14  8:14 UTC (permalink / raw)
  To: netdev, Alexei Starovoitov, Jakub Kicinski, Kuniyuki Iwashima,
	Paolo Abeni, Stanislav Fomichev
  Cc: bpf, kernel-team, Daniel Borkmann, John Fastabend,
	Andrii Nakryiko, Eduard Zingerman, Kumar Kartikeya Dwivedi,
	Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
	Emil Tsalapatis, David S. Miller, Eric Dumazet, Simon Horman,
	Jesper Dangaard Brouer, Willem de Bruijn

Test bpf_dynptr_from_skb_ext() from lwt_seg6local context:
- TC writes skb_ext on the encapped packet leaving NS1,
- NS2 runs End.BPF for fd01::1 which reads it back,
- NS3 receives the packet and decapsulates it.

Note that End.BPF cannot deliver locally since seg6_lookup_nexthop rejects
loopback routes, hence a three netns setup is necessary.

Signed-off-by: Jakub Sitnicki <jakub@cloudflare.com>
---
 .../bpf/prog_tests/xdp_context_test_run.c          | 170 +++++++++++++++++++++
 tools/testing/selftests/bpf/progs/test_xdp_meta.c  |  18 +++
 2 files changed, 188 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 7a7537ef80e7..5f9247941aa0 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
@@ -1392,6 +1392,174 @@ static void test_skb_ext_lwt(struct test_xdp_meta *skel, const char *name,
 		unlink(LWT_EXT_PIN_PATH);
 }
 
+#define SEG6_PIN_PATH "/sys/fs/bpf/skb_ext_seg6local"
+
+/*
+ * Test skb_ext survival across TC egress -> seg6local End.BPF hook.
+ *
+ * Topology:
+ *
+ *     NS1           NS2            NS3
+ *   lo veth1 <-> veth2 veth3 <-> veth4 lo
+ *
+ * NS1 encaps fb01::2 with SRH (segments fd01::1,fd01::2) on output and
+ * writes skb_ext at TC egress on veth1. NS2 runs End.BPF for fd01::1,
+ * which reads skb_ext, then forwards to NS3 (seg6_lookup_nexthop
+ * rejects loopback routes with local_delivery=false, so End.BPF
+ * cannot deliver locally). NS3 has fd01::2 local, decapsulates, and
+ * delivers the inner UDP (dst=fb01::2) to a local socket for test
+ * synchronization.
+ *
+ * The TC program drops non-test packets, so NDP is bypassed with a
+ * static neighbor entry on the NS1->NS2 hop.
+ */
+static void test_skb_ext_seg6local(struct test_xdp_meta *skel)
+{
+	LIBBPF_OPTS(bpf_tc_hook, tc_hook, .attach_point = BPF_TC_EGRESS);
+	LIBBPF_OPTS(bpf_tc_opts, tc_opts, .handle = 1, .priority = 1);
+	struct nstoken *nstoken = NULL;
+	struct sockaddr_in6 dst = {};
+	char buf[TEST_PAYLOAD_LEN];
+	bool pinned = false;
+	int client_fd = -1;
+	int server_fd = -1;
+	ssize_t bytes;
+	int ret;
+
+	unlink(SEG6_PIN_PATH);
+	ret = bpf_program__pin(skel->progs.seg6local_skb_ext_read,
+			       SEG6_PIN_PATH);
+	if (!ASSERT_OK(ret, "pin seg6local"))
+		return;
+	pinned = true;
+
+	skel->bss->test_pass = false;
+
+	SYS(cleanup, "ip netns add seg6_1");
+	SYS(cleanup, "ip netns add seg6_2");
+	SYS(cleanup, "ip netns add seg6_3");
+
+	/* NS1 <-> NS2 veth pair; static NDP since TC drops NS/NA */
+	SYS(cleanup, "ip -n seg6_1 link add veth1 type veth peer name veth2 netns seg6_2");
+	SYS(cleanup, "ip -n seg6_1 link set dev veth1 up");
+	SYS(cleanup, "ip -n seg6_2 link set dev veth2 address 02:00:00:00:00:02");
+	SYS(cleanup, "ip -n seg6_2 link set dev veth2 up");
+	SYS(cleanup, "ip -n seg6_1 -6 addr add fb00::12/16 dev veth1 scope link nodad");
+	SYS(cleanup, "ip -n seg6_2 -6 addr add fb00::21/16 dev veth2 scope link nodad");
+	SYS(cleanup, "ip -n seg6_1 -6 neigh add fb00::21 lladdr 02:00:00:00:00:02 "
+		     "nud permanent dev veth1");
+
+	/* NS2 <-> NS3 veth pair */
+	SYS(cleanup, "ip -n seg6_2 link add veth3 type veth peer name veth4 netns seg6_3");
+	SYS(cleanup, "ip -n seg6_2 link set dev veth3 up");
+	SYS(cleanup, "ip -n seg6_3 link set dev veth4 up");
+	SYS(cleanup, "ip -n seg6_2 -6 addr add fb00::34/16 dev veth3 scope link nodad");
+	SYS(cleanup, "ip -n seg6_3 -6 addr add fb00::43/16 dev veth4 scope link nodad");
+
+	/* NS1: source address + SRH encap on output + route for fd01::/16 */
+	SYS(cleanup, "ip -n seg6_1 link set dev lo up");
+	SYS(cleanup, "ip -n seg6_1 -6 addr add fb01::1/128 dev lo nodad");
+	SYS(cleanup, "ip -n seg6_1 -6 route add fb01::2 encap seg6 mode encap "
+		     "segs fd01::1,fd01::2 dev veth1 via fb00::21");
+	SYS(cleanup, "ip -n seg6_1 -6 route add fd01::/16 dev veth1 via fb00::21");
+
+	/* NS2: seg6local End.BPF for fd01::1, then forward to NS3.
+	 * Use open_netns so the pinned prog on bpffs is accessible.
+	 */
+	nstoken = open_netns("seg6_2");
+	if (!ASSERT_OK_PTR(nstoken, "open seg6_2"))
+		goto cleanup;
+
+	SYS(cleanup, "ip link set dev lo up");
+	SYS(cleanup, "sysctl -wq net.ipv6.conf.all.forwarding=1");
+	SYS(cleanup, "ip -6 route add fd01::2/128 dev veth3 via fb00::43");
+	SYS(cleanup, "ip -6 route add fd01::1 encap seg6local "
+		     "action End.BPF endpoint pinned " SEG6_PIN_PATH " dev veth2");
+
+	close_netns(nstoken);
+	nstoken = NULL;
+
+	/* NS3: decap point + UDP server.
+	 * fd01::2 local triggers SRH decap (segments_left=0).
+	 * fb01::2 local delivers the inner UDP to the socket.
+	 */
+	nstoken = open_netns("seg6_3");
+	if (!ASSERT_OK_PTR(nstoken, "open seg6_3"))
+		goto cleanup;
+
+	SYS(cleanup, "ip link set dev lo up");
+	SYS(cleanup, "sysctl -wq net.ipv6.conf.all.seg6_enabled=1");
+	SYS(cleanup, "sysctl -wq net.ipv6.conf.lo.seg6_enabled=1");
+	SYS(cleanup, "sysctl -wq net.ipv6.conf.veth4.seg6_enabled=1");
+	SYS(cleanup, "ip -6 addr add fd01::2/128 dev lo nodad");
+	SYS(cleanup, "ip -6 addr add fb01::2/128 dev lo nodad");
+
+	server_fd = start_server(AF_INET6, SOCK_DGRAM, "fb01::2", 1234, 0);
+	if (!ASSERT_GE(server_fd, 0, "start_server"))
+		goto cleanup;
+
+	close_netns(nstoken);
+	nstoken = NULL;
+
+	/* NS1: write skb_ext at TC egress on veth1, then send.
+	 * TC egress fires after seg6 encap, on the encapped packet.
+	 */
+	nstoken = open_netns("seg6_1");
+	if (!ASSERT_OK_PTR(nstoken, "open seg6_1"))
+		goto cleanup;
+
+	tc_hook.ifindex = if_nametoindex("veth1");
+	if (!ASSERT_GE(tc_hook.ifindex, 0, "ifindex veth1"))
+		goto cleanup;
+
+	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;
+
+	client_fd = socket(AF_INET6, SOCK_DGRAM, 0);
+	if (!ASSERT_GE(client_fd, 0, "socket"))
+		goto cleanup;
+
+	dst.sin6_family = AF_INET6;
+	dst.sin6_port = htons(1234);
+	inet_pton(AF_INET6, "fb01::2", &dst.sin6_addr);
+
+	bytes = sendto(client_fd, test_payload, TEST_PAYLOAD_LEN, 0,
+		       (void *)&dst, sizeof(dst));
+
+	close_netns(nstoken);
+	nstoken = NULL;
+
+	if (!ASSERT_EQ(bytes, TEST_PAYLOAD_LEN, "sendto"))
+		goto cleanup;
+
+	/* Receive decapsulated packet -- synchronizes with seg6local */
+	nstoken = open_netns("seg6_3");
+	if (!ASSERT_OK_PTR(nstoken, "open seg6_3"))
+		goto cleanup;
+
+	bytes = recvfrom(server_fd, buf, sizeof(buf), 0, NULL, NULL);
+	ASSERT_EQ(bytes, TEST_PAYLOAD_LEN, "recvfrom");
+	ASSERT_TRUE(skel->bss->test_pass, "test_pass");
+
+cleanup:
+	close_netns(nstoken);
+	if (server_fd >= 0)
+		close(server_fd);
+	if (client_fd >= 0)
+		close(client_fd);
+	SYS_NOFAIL("ip netns del seg6_3");
+	SYS_NOFAIL("ip netns del seg6_2");
+	SYS_NOFAIL("ip netns del seg6_1");
+	if (pinned)
+		unlink(SEG6_PIN_PATH);
+}
+
 void test_skb_ext_cross_hook(void)
 {
 	struct test_xdp_meta *skel = NULL;
@@ -1421,6 +1589,8 @@ void test_skb_ext_cross_hook(void)
 	if (test__start_subtest("lwt_xmit_to_tc"))
 		test_skb_ext_lwt(skel, "lwt_xmit_to_tc",
 				 skel->progs.lwt_xmit_skb_ext_write, "xmit", true);
+	if (test__start_subtest("tc_to_seg6local"))
+		test_skb_ext_seg6local(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 97c1760fbd6d..c4ea38d79533 100644
--- a/tools/testing/selftests/bpf/progs/test_xdp_meta.c
+++ b/tools/testing/selftests/bpf/progs/test_xdp_meta.c
@@ -1134,4 +1134,22 @@ int lwt_xmit_skb_ext_write(struct __sk_buff *ctx)
 	return BPF_OK;
 }
 
+/* Read skb_ext from seg6local End.BPF -- tests TC -> seg6local path */
+SEC("lwt_seg6local")
+int seg6local_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 BPF_OK;
+	if (bpf_dynptr_read(meta_have, META_SIZE, &meta, 0, 0))
+		return BPF_OK;
+	if (!check_metadata(meta_have))
+		return BPF_OK;
+
+	test_pass = true;
+	return BPF_OK;
+}
+
 char _license[] SEC("license") = "GPL";

-- 
2.43.0


^ permalink raw reply related	[flat|nested] 26+ messages in thread

* [PATCH net-next 13/14] selftests/bpf: Test skb_ext read from sk_skb stream verdict hook
  2026-08-14  8:14 [PATCH net-next 00/14] skb extension for BPF metadata Jakub Sitnicki
                   ` (11 preceding siblings ...)
  2026-08-14  8:14 ` [PATCH net-next 12/14] selftests/bpf: Test skb_ext read from seg6local End.BPF hook Jakub Sitnicki
@ 2026-08-14  8:14 ` Jakub Sitnicki
  2026-08-15  8:14   ` sashiko-bot
  2026-08-14  8:14 ` [PATCH net-next 14/14] selftests/bpf: Use non-trivial test payload in xdp_context tests Jakub Sitnicki
  13 siblings, 1 reply; 26+ messages in thread
From: Jakub Sitnicki @ 2026-08-14  8:14 UTC (permalink / raw)
  To: netdev, Alexei Starovoitov, Jakub Kicinski, Kuniyuki Iwashima,
	Paolo Abeni, Stanislav Fomichev
  Cc: bpf, kernel-team, Daniel Borkmann, John Fastabend,
	Andrii Nakryiko, Eduard Zingerman, Kumar Kartikeya Dwivedi,
	Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
	Emil Tsalapatis, David S. Miller, Eric Dumazet, Simon Horman,
	Jesper Dangaard Brouer, Willem de Bruijn

Test bpf_dynptr_from_skb_ext() from sk_skb context. Write to skb_ext from
TC on loopback; read from skb_ext from the sockmap verdict program.

Signed-off-by: Jakub Sitnicki <jakub@cloudflare.com>
---
 .../selftests/bpf/prog_tests/socket_helpers.h      |  1 +
 .../bpf/prog_tests/xdp_context_test_run.c          | 79 ++++++++++++++++++++++
 tools/testing/selftests/bpf/progs/test_xdp_meta.c  | 18 +++++
 3 files changed, 98 insertions(+)

diff --git a/tools/testing/selftests/bpf/prog_tests/socket_helpers.h b/tools/testing/selftests/bpf/prog_tests/socket_helpers.h
index 0d59503a0c73..eb114a6ea6f9 100644
--- a/tools/testing/selftests/bpf/prog_tests/socket_helpers.h
+++ b/tools/testing/selftests/bpf/prog_tests/socket_helpers.h
@@ -3,6 +3,7 @@
 #ifndef __SOCKET_HELPERS__
 #define __SOCKET_HELPERS__
 
+#include <error.h>
 #include <sys/un.h>
 #include <linux/vm_sockets.h>
 
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 5f9247941aa0..f5fbc19757e3 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
@@ -4,6 +4,7 @@
 #include <linux/ipv6.h>
 #include <linux/netfilter.h>
 #include <arpa/inet.h>
+#include "socket_helpers.h"
 #include "test_xdp_context_test_run.skel.h"
 #include "test_xdp_meta.skel.h"
 
@@ -1560,6 +1561,82 @@ static void test_skb_ext_seg6local(struct test_xdp_meta *skel)
 		unlink(SEG6_PIN_PATH);
 }
 
+/* Test skb_ext survival across TC ingress -> sk_skb verdict hook.
+ * TC ingress writes skb_ext on loopback; the verdict program reads it
+ * on skb delivery to a socket in the sockmap.
+ */
+static void test_skb_ext_sk_skb(struct test_xdp_meta *skel)
+{
+	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 *verdict_link = NULL;
+	struct sockaddr_in addr;
+	socklen_t addr_len = sizeof(addr);
+	struct netns_obj *ns = NULL;
+	char buf[TEST_PAYLOAD_LEN];
+	int c1 = -1, p1 = -1;
+	int map = -1, zero = 0;
+	ssize_t n;
+	int ret;
+
+	ns = netns_new("tc_to_sk_skb", true);
+	if (!ASSERT_OK_PTR(ns, "netns_new"))
+		return;
+
+	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;
+
+	map = bpf_map_create(BPF_MAP_TYPE_SOCKMAP, NULL, sizeof(int),
+			     sizeof(int), 1, NULL);
+	if (!ASSERT_GE(map, 0, "bpf_map_create"))
+		goto cleanup;
+
+	verdict_link = bpf_program__attach_sockmap(skel->progs.sk_skb_skb_ext_read,
+						   map);
+	if (!ASSERT_OK_PTR(verdict_link, "attach_sockmap"))
+		goto cleanup;
+
+	if (!ASSERT_OK(create_pair(AF_INET, SOCK_STREAM, &c1, &p1), "create_pair"))
+		goto cleanup;
+
+	if (!ASSERT_OK(bpf_map_update_elem(map, &zero, &c1, BPF_NOEXIST), "map_update"))
+		goto cleanup;
+
+	if (!ASSERT_OK(getsockname(c1, (struct sockaddr *)&addr, &addr_len),
+		       "getsockname"))
+		goto cleanup;
+	skel->bss->target_port = addr.sin_port;
+
+	n = send(p1, test_payload, TEST_PAYLOAD_LEN, 0);
+	if (!ASSERT_EQ(n, TEST_PAYLOAD_LEN, "send"))
+		goto cleanup;
+
+	n = recv(c1, buf, sizeof(buf), 0);
+	ASSERT_EQ(n, TEST_PAYLOAD_LEN, "recv");
+	ASSERT_TRUE(skel->bss->test_pass, "test_pass");
+
+cleanup:
+	if (c1 >= 0)
+		close(c1);
+	if (p1 >= 0)
+		close(p1);
+	bpf_link__destroy(verdict_link);
+	if (map >= 0)
+		close(map);
+	bpf_tc_hook_destroy(&tc_hook);
+	netns_free(ns);
+}
+
 void test_skb_ext_cross_hook(void)
 {
 	struct test_xdp_meta *skel = NULL;
@@ -1591,6 +1668,8 @@ void test_skb_ext_cross_hook(void)
 				 skel->progs.lwt_xmit_skb_ext_write, "xmit", true);
 	if (test__start_subtest("tc_to_seg6local"))
 		test_skb_ext_seg6local(skel);
+	if (test__start_subtest("tc_to_sk_skb"))
+		test_skb_ext_sk_skb(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 c4ea38d79533..4c224d5fe1b2 100644
--- a/tools/testing/selftests/bpf/progs/test_xdp_meta.c
+++ b/tools/testing/selftests/bpf/progs/test_xdp_meta.c
@@ -1152,4 +1152,22 @@ int seg6local_skb_ext_read(struct __sk_buff *ctx)
 	return BPF_OK;
 }
 
+/* Read skb_ext from sk_skb stream verdict hook */
+SEC("sk_skb/stream_verdict")
+int sk_skb_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 SK_PASS;
+	if (bpf_dynptr_read(meta_have, META_SIZE, &meta, 0, 0))
+		return SK_PASS;
+	if (!check_metadata(meta_have))
+		return SK_PASS;
+
+	test_pass = true;
+	return SK_PASS;
+}
+
 char _license[] SEC("license") = "GPL";

-- 
2.43.0


^ permalink raw reply related	[flat|nested] 26+ messages in thread

* [PATCH net-next 14/14] selftests/bpf: Use non-trivial test payload in xdp_context tests
  2026-08-14  8:14 [PATCH net-next 00/14] skb extension for BPF metadata Jakub Sitnicki
                   ` (12 preceding siblings ...)
  2026-08-14  8:14 ` [PATCH net-next 13/14] selftests/bpf: Test skb_ext read from sk_skb stream verdict hook Jakub Sitnicki
@ 2026-08-14  8:14 ` Jakub Sitnicki
  13 siblings, 0 replies; 26+ messages in thread
From: Jakub Sitnicki @ 2026-08-14  8:14 UTC (permalink / raw)
  To: netdev, Alexei Starovoitov, Jakub Kicinski, Kuniyuki Iwashima,
	Paolo Abeni, Stanislav Fomichev
  Cc: bpf, kernel-team, Daniel Borkmann, John Fastabend,
	Andrii Nakryiko, Eduard Zingerman, Kumar Kartikeya Dwivedi,
	Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
	Emil Tsalapatis, David S. Miller, Eric Dumazet, Simon Horman,
	Jesper Dangaard Brouer, Willem de Bruijn

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 f5fbc19757e3..8bf586b5af32 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
@@ -21,10 +21,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 4c224d5fe1b2..b0ab7f4f7a1b 100644
--- a/tools/testing/selftests/bpf/progs/test_xdp_meta.c
+++ b/tools/testing/selftests/bpf/progs/test_xdp_meta.c
@@ -24,10 +24,10 @@
 bool test_pass;
 
 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_metadata(const char *file, int line, __u8 *meta_have)

-- 
2.43.0


^ permalink raw reply related	[flat|nested] 26+ messages in thread

* Re: [PATCH net-next 04/14] selftests/bpf: Add tests for bpf_dynptr_from_skb_ext
  2026-08-14  8:14 ` [PATCH net-next 04/14] selftests/bpf: Add tests for bpf_dynptr_from_skb_ext Jakub Sitnicki
@ 2026-08-15  8:14   ` sashiko-bot
  0 siblings, 0 replies; 26+ messages in thread
From: sashiko-bot @ 2026-08-15  8:14 UTC (permalink / raw)
  To: Jakub Sitnicki; +Cc: bpf

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Critical] A Use-After-Free (UAF) read/write vulnerability exists because `bpf_dynptr_from_skb_ext` with `BPF_SKB_EXT_F_CREATE` can force a copy-on-write (COW) reallocation of `skb->extensions`. This drops the reference to the old extension buffer, potentially freeing it. However, the verifier does not invalidate existing `PTR_TO_MEM` pointers to the old extension buffer obtained via `bpf_dynptr_slice()`, allowing BPF programs to access freed kernel memory.
- [Medium] The new test `test_skb_ext_basic` implicitly uses hardcoded network namespace and interface names (`TAP_NETNS` and `TAP_NAME`) by calling the `test_tuntap` helper. This will clash with the existing `test_xdp_context_tuntap` test when run concurrently via `test_progs -j`.
--

--- Patch [4]: [PATCH net-next 04/14] selftests/bpf: Add tests for bpf_dynptr_from_skb_ext ---
commit 4d32fa867b7073dd6ac590aca746b7f46b659dce
Author: Jakub Sitnicki <jakub@cloudflare.com>

selftests/bpf: Add tests for bpf_dynptr_from_skb_ext

Cover the bpf_dynptr_from_skb_ext() kfunc and the dynptr interface
to skb_ext 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

> 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 448807676176c..fd340e5538a34 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
> @@ -693,3 +693,55 @@ void test_xdp_context_lwt_encap(void)
>  
>  	test_xdp_meta__destroy(skel);
>  }
> +
> +void test_skb_ext_basic(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 */

[Severity: Medium]
Might this new test cause a race condition when run concurrently?

The test_tuntap() helper creates a network namespace using a hardcoded
string:

tools/testing/selftests/bpf/prog_tests/xdp_context_test_run.c:test_tuntap() {
    ...
	ns = netns_new(TAP_NETNS, true);
    ...
}

If test_skb_ext_basic() and other tests like test_xdp_context_tuntap() are
executed in parallel worker processes via test_progs -j, will they clash by
attempting to create and delete the identically named network namespace,
causing spurious test failures?

[ ... ]

> diff --git a/tools/testing/selftests/bpf/progs/test_xdp_meta.c b/tools/testing/selftests/bpf/progs/test_xdp_meta.c
> index 08b03be0b891c..43840ee32d359 100644
> --- a/tools/testing/selftests/bpf/progs/test_xdp_meta.c
> +++ b/tools/testing/selftests/bpf/progs/test_xdp_meta.c
[ ... ]
> @@ -689,4 +690,188 @@ 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))

[Severity: Critical]
Can the use of BPF_SKB_EXT_F_CREATE lead to a use-after-free regression?

When BPF_SKB_EXT_F_CREATE is used, bpf_dynptr_from_skb_ext() can force a
copy-on-write reallocation of skb->extensions:

net/core/filter.c:bpf_dynptr_from_skb_ext() {
    ...
	ext = skb_ext_add(skb, SKB_EXT_BPF);
    ...
}

This drops the reference to the old extension buffer, potentially freeing it.
However, the verifier does not currently treat bpf_dynptr_from_skb_ext() as a
packet-changing kfunc:

kernel/bpf/verifier.c:bpf_is_kfunc_pkt_changing() {
	return meta->func_id == special_kfunc_list[KF_bpf_xdp_pull_data];
}

If a BPF program obtains a direct pointer slice to skb->extensions via
bpf_dynptr_slice(), then calls bpf_dynptr_from_skb_ext() with
BPF_SKB_EXT_F_CREATE on a cloned skb, the old extension buffer could be freed.

Will the verifier fail to invalidate the existing PTR_TO_MEM pointers to the
old extension buffer, allowing the BPF program to access freed kernel memory?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260814-bpf-meta-inside-skb-ext-v1-0-767edd862656@cloudflare.com?part=4

^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: [PATCH net-next 05/14] selftests/bpf: Test skb_ext on cloned skbs
  2026-08-14  8:14 ` [PATCH net-next 05/14] selftests/bpf: Test skb_ext on cloned skbs Jakub Sitnicki
@ 2026-08-15  8:14   ` sashiko-bot
  0 siblings, 0 replies; 26+ messages in thread
From: sashiko-bot @ 2026-08-15  8:14 UTC (permalink / raw)
  To: Jakub Sitnicki; +Cc: bpf

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] Flawed assertion for `if_nametoindex` return value silently ignores errors.
- [Low] Multi-line comments do not follow the BPF subsystem comment style guidelines.
--

--- Patch [5]: [PATCH net-next 05/14] selftests/bpf: Test skb_ext on cloned skbs ---
commit 52378eb340683567339caa62c46774dede8267b1
Author: Jakub Sitnicki <jakub@cloudflare.com>

selftests/bpf: Test skb_ext on cloned skbs

This patch covers skb_ext behavior when an skb is cloned by TC mirred. It adds
tests to verify that the extension written at tap ingress is readable from the
mirred clone and that COW works when the extension is modified.

> 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 fd340e5538a34..58b793cf14e59 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
> @@ -694,6 +694,95 @@ void test_xdp_context_lwt_encap(void)
>  	test_xdp_meta__destroy(skel);
>  }
>  
> +/* Test if skb_ext survives skb clone (via tc mirred).
> + * dummy_prog runs on the clone (dummy ingress).
> + */

[Severity: Low]
This isn't a bug, but the multi-line comment does not follow the BPF subsystem
style guidelines. Should the opening /* be placed on its own line?

> +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"))

[Severity: Medium]
Since if_nametoindex() returns 0 on failure, will ASSERT_GE(..., 0) incorrectly
evaluate 0 as a success? If the interface lookup fails, won't the test proceed
with an invalid interface index (0) instead of catching the failure here?

> +		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"))

[Severity: Medium]
Similarly, will this assertion accept 0 as a valid interface index on failure
and result in confusing downstream errors during bpf_tc_hook_create()?

> +		goto close;

[ ... ]

> diff --git a/tools/testing/selftests/bpf/progs/test_xdp_meta.c b/tools/testing/selftests/bpf/progs/test_xdp_meta.c
> index 43840ee32d359..1b6bb20ee7e85 100644
> --- a/tools/testing/selftests/bpf/progs/test_xdp_meta.c
> +++ b/tools/testing/selftests/bpf/progs/test_xdp_meta.c
> @@ -874,4 +875,49 @@ int tc_skb_ext_double_alloc(struct __sk_buff *ctx)
>  	return TC_ACT_UNSPEC;
>  }
>  
> +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.
> + */

[Severity: Low]
This isn't a bug, but could the opening /* be placed on its own line to adhere
to the preferred kernel multi-line comment style used in the BPF subsystem?

> +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.
> + */

[Severity: Low]
This isn't a bug, but could the opening /* be placed on its own line to adhere
to the preferred kernel multi-line comment style used in the BPF subsystem?

> +SEC("tp_btf/kfree_skb")
> +int BPF_PROG(tp_kfree_skb_cow_check, struct sk_buff *skb)
> +{

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260814-bpf-meta-inside-skb-ext-v1-0-767edd862656@cloudflare.com?part=5

^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: [PATCH net-next 01/14] bpf: Introduce per-packet metadata storage for BPF programs
  2026-08-14  8:14 ` [PATCH net-next 01/14] bpf: Introduce per-packet metadata storage for BPF programs Jakub Sitnicki
@ 2026-08-15  8:14   ` sashiko-bot
  0 siblings, 0 replies; 26+ messages in thread
From: sashiko-bot @ 2026-08-15  8:14 UTC (permalink / raw)
  To: Jakub Sitnicki; +Cc: bpf

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] bpf_dynptr_from_skb_ext() mutates skb->extensions without synchronization in TRACING and LSM programs, causing data races.
- [High] bpf_dynptr_from_skb_ext() COWs the SKB extension without invalidating existing dynptr slices in the verifier, leading to a Use-After-Free.
- [High] __bpf_skb_ext_store_bytes() and bpf_skb_ext_pointer() allow lockless mutation of shared SKB extensions on cloned SKBs.
--

--- Patch [1]: [PATCH net-next 01/14] bpf: Introduce per-packet metadata storage for BPF programs ---
commit e0c430f0cc32850ff4a67d41f0ec463446dca9ae
Author: Jakub Sitnicki <jakub@cloudflare.com>

bpf: Introduce per-packet metadata storage for BPF programs

This commit adds a new sk_buff extension (struct bpf_skb_ext) to provide
up to 256 bytes of per-packet storage for BPF programs. The storage is
exposed to BPF programs via the bpf_dynptr_from_skb_ext() kfunc.

> diff --git a/net/core/filter.c b/net/core/filter.c
> index 16845987b2449..2cbb40b392edd 100644
> --- a/net/core/filter.c
> +++ b/net/core/filter.c
> @@ -12259,6 +12259,48 @@ int __bpf_skb_meta_store_bytes(struct sk_buff *skb, u32 offset,

[ ... ]

> +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);

[Severity: High]
Is it safe to directly mutate the extension buffer here without checking if
the SKB is cloned?

If a BPF program creates a writable dynptr, clones the SKB (which increments
the extension's reference count), and then writes to the dynptr, this
memmove() will modify the shared extension block while the cloned SKB might
be executing concurrently on another CPU.

Should this use a mechanism like bpf_try_make_writable() or check
ext->refcnt before mutating to ensure exclusive ownership?

> +	return 0;
> +}
> +#endif /* CONFIG_BPF_SKB_EXT */

[ ... ]

> +__bpf_kfunc int bpf_dynptr_from_skb_ext(struct __sk_buff *skb_, u32 size,
> +					u64 flags,
> +					struct bpf_dynptr *ptr__uninit)
> +{

[ ... ]

> +	ext = skb_ext_add(skb, SKB_EXT_BPF);
> +	if (!ext) {
> +		err = -ENOMEM;
> +		goto error;
> +	}

[Severity: High]
Could this COW operation lead to a use-after-free if existing dynptr slices
are not invalidated?

If a BPF program obtains a direct slice to a read-only SKB extension via
bpf_dynptr_from_skb_ext() and bpf_dynptr_slice(), and then calls
bpf_dynptr_from_skb_ext() again with BPF_SKB_EXT_F_CREATE, this forces a
Copy-On-Write in skb_ext_add(), dropping the reference on the old block.

However, because KF_bpf_dynptr_from_skb_ext is not added to
bpf_is_kfunc_pkt_changing() in the verifier, clear_all_pkt_pointers() won't
be invoked. The previously obtained slice remains valid in the verifier
state, allowing the program to read or write the freed memory.

> +	if (!exists)
> +		memset(ext, 0, sizeof(*ext));
> +out:
> +	bpf_dynptr_init(ptr, skb, BPF_DYNPTR_TYPE_SKB_EXT, 0, size);

[ ... ]

> @@ -12640,6 +12760,21 @@ static int __init bpf_kfunc_init(void)

[ ... ]

> +	ret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_NETFILTER, &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);

[Severity: High]
Can calling bpf_dynptr_from_skb_ext() from TRACING or LSM programs cause
data races on the skb structure?

TRACING and LSM programs can intercept an SKB that is currently shared
(skb->users > 1) and being processed concurrently on multiple CPUs.

Since bpf_dynptr_from_skb_ext() unconditionally calls skb_ext_add(), which
performs lockless read-modify-write operations on skb->active_extensions,
concurrent execution could corrupt the bitfield or overwrite the extension
pointer, leaking the allocated extension block.

> +#endif
>  	ret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_XDP, &bpf_kfunc_set_xdp);

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260814-bpf-meta-inside-skb-ext-v1-0-767edd862656@cloudflare.com?part=1

^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: [PATCH net-next 03/14] bpf: Make BPF skb extension survive packet scrubbing
  2026-08-14  8:14 ` [PATCH net-next 03/14] bpf: Make BPF skb extension survive packet scrubbing Jakub Sitnicki
@ 2026-08-15  8:14   ` sashiko-bot
  0 siblings, 0 replies; 26+ messages in thread
From: sashiko-bot @ 2026-08-15  8:14 UTC (permalink / raw)
  To: Jakub Sitnicki; +Cc: bpf

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Critical] Scrubbing and later re-adding SKB extensions breaks the `skb_ext` bump allocator, leading to a slab out-of-bounds write.
- [High] A Time-Of-Check to Time-Of-Use (TOCTOU) race condition in `__skb_ext_scrub` causes a memory leak of inner SKB extension references (such as `sec_path` or `mctp_flow`).
--

--- Patch [3]: [PATCH net-next 03/14] bpf: Make BPF skb extension survive packet scrubbing ---
commit 5b9486076275b0379f7c87d595b7fafe3fc1ef2a
Author: Jakub Sitnicki <jakub@cloudflare.com>

bpf: Make BPF skb extension survive packet scrubbing

skb_scrub_packet() drops all skb extensions unconditionally via
skb_ext_reset(), making it impossible for a BPF program to pass metadata
via bpf_skb_ext through a tunnel or across a netns boundary. Introduce
skb_ext_scrub() which consults each active extension before discarding it,
allowing extensions that request preservation to be kept. Update
udp_try_make_stateless() to use this new scrub method.

> diff --git a/net/core/skbuff.c b/net/core/skbuff.c
> index 9947d2ed6635b..700bee045b5c5 100644
> --- a/net/core/skbuff.c
> +++ b/net/core/skbuff.c
[ ... ]
> @@ -7269,18 +7284,61 @@ void __skb_ext_put(struct skb_ext *ext)
[ ... ]
> +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);

[Severity: High]
Does this introduce a time-of-check to time-of-use (TOCTOU) race condition?

If an SKB with extensions is cloned, and one clone passes through
skb_ext_scrub() while another is concurrently freed, the refcount could drop
to 1 just before skb_ext_maybe_cow() is called.

Looking at skb_ext_maybe_cow():

	if (refcount_read(&old->refcnt) == 1)
		return old;

If skb_ext_maybe_cow() returns the old extension directly, skb_ext_put_each()
is bypassed, meaning unkept inner references are not released. Furthermore,
since the offsets are subsequently zeroed out below, the final __skb_ext_put()
will also fail to discover and free those leaked resources.

This seems to permanently leak state references like xfrm_state.

> +		if (!ext)
> +			return -ENOMEM;
> +		skb->extensions = ext;
> +	}
> +
> +	for (i = 0; i < SKB_EXT_NUM; i++) {
> +		if (!(keep & (1 << i)))
> +			ext->offset[i] = 0;

[Severity: Critical]
Can this cause a slab out-of-bounds write due to how the skb_ext bump
allocator works?

By zeroing the offset here, we leave ext->chunks at its previous size, which
permanently consumes bump allocator space. If a packet retains one extension
and later re-acquires a scrubbed extension, skb_ext_add() will append it at
the end of the chunk space because it appears non-existent:

	if (__skb_ext_exist(new, id))
		goto set_active;

	newoff = new->chunks;
	...
	newlen = newoff + skb_ext_type_len[id];
	new->chunks = newlen;

Repeated scrub and add cycles (for instance, crossing netns boundaries into
IPsec) could cause new->chunks to exceed the skbuff_ext_cache slab object
bounds.

> +	}
> +	skb->active_extensions = keep;
> +	return 0;
> +}

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260814-bpf-meta-inside-skb-ext-v1-0-767edd862656@cloudflare.com?part=3

^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: [PATCH net-next 07/14] selftests/bpf: Test skb_ext read from cgroup_skb and sk_filter hooks
  2026-08-14  8:14 ` [PATCH net-next 07/14] selftests/bpf: Test skb_ext read from cgroup_skb and sk_filter hooks Jakub Sitnicki
@ 2026-08-15  8:14   ` sashiko-bot
  0 siblings, 0 replies; 26+ messages in thread
From: sashiko-bot @ 2026-08-15  8:14 UTC (permalink / raw)
  To: Jakub Sitnicki; +Cc: bpf

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] Implicit truncation of error code to unsigned short masks get_socket_local_port() failure.
--

--- Patch [7]: [PATCH net-next 07/14] selftests/bpf: Test skb_ext read from cgroup_skb and sk_filter hooks ---
commit b524b9baad48611cd0f3fbe2f1b770ea85d7c5df
Author: Jakub Sitnicki <jakub@cloudflare.com>

selftests/bpf: Test skb_ext read from cgroup_skb and sk_filter hooks

This commit extends the skb_ext cross-hook coverage to UDP readers. It attaches
a TC ingress program on loopback to write the extension, sends a UDP datagram,
and reads the extension back from a cgroup_skb/ingress program and a socket
filter.

> 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 de60303025361..808eb158fe0f1 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
[ ... ]
> +/* 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;

[Severity: Medium]
Will this error check ever trigger if get_socket_local_port() fails?

Because port is declared as a __be16 (an unsigned 16-bit integer), a -1 return
value from get_socket_local_port() would be implicitly truncated and converted
to 65535.

The ASSERT_GE(port, 0, ...) check would then compare 65535 >= 0, which is always
true. This masks the failure and allows the test to proceed with an invalid
port.

Should the return value be captured in an int variable for the error check
before assigning it to the __be16 port?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260814-bpf-meta-inside-skb-ext-v1-0-767edd862656@cloudflare.com?part=7

^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: [PATCH net-next 06/14] selftests/bpf: Test skb_ext survival across veth and GRE
  2026-08-14  8:14 ` [PATCH net-next 06/14] selftests/bpf: Test skb_ext survival across veth and GRE Jakub Sitnicki
@ 2026-08-15  8:14   ` sashiko-bot
  0 siblings, 0 replies; 26+ messages in thread
From: sashiko-bot @ 2026-08-15  8:14 UTC (permalink / raw)
  To: Jakub Sitnicki; +Cc: bpf

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] Incorrect assertions for if_nametoindex() return values allow errors to pass silently.
--

--- Patch [6]: [PATCH net-next 06/14] selftests/bpf: Test skb_ext survival across veth and GRE ---
commit 9eef2d71b261cfaa465a5233213fb272dc34f416
Author: Jakub Sitnicki <jakub@cloudflare.com>

selftests/bpf: Test skb_ext survival across veth and GRE
    
This commit tests if skb_ext survives veth cross-netns forwarding and GRE
encap/decap. It writes skb_ext at TC egress and reads it back at TC ingress
after the packet crosses device boundaries.

> 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 58b793cf14e59..de60303025361 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

[ ... ]

> @@ -783,6 +783,156 @@ static void test_mirred_clone_ext_cow(struct test_xdp_meta *skel)
> +/* Test if skb_ext survives veth cross-netns forward */
> +static void test_skb_ext_scrub_veth(struct test_xdp_meta *skel)
> +{

[ ... ]

> +	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;

[Severity: Low]
Does this code correctly catch interface lookup failures?

Since if_nametoindex() returns 0 on failure, the ASSERT_GE(rx_ifindex, 0, ...)
check will evaluate 0 >= 0 as true, failing to catch the error. The test will
instead fail slightly later with a less descriptive error. 

Should this be using ASSERT_GT() or ASSERT_NEQ() instead?

[ ... ]

> +	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;

[Severity: Low]
Is this the same issue here? if_nametoindex() returning 0 would bypass this
ASSERT_GE check.

[ ... ]

> +/* Test if skb_ext survives GRE tunnel encap+decap */
> +static void test_skb_ext_scrub_gre(struct test_xdp_meta *skel)
> +{

[ ... ]

> +	/* 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;

[Severity: Low]
Is this the same issue here as well?

[ ... ]

> +	/* 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;

[Severity: Low]
And one more instance here where ASSERT_GE allows if_nametoindex() failure
to pass silently.

[ ... ]

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260814-bpf-meta-inside-skb-ext-v1-0-767edd862656@cloudflare.com?part=6

^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: [PATCH net-next 08/14] selftests/bpf: Test skb_ext read from sock_ops and LSM hooks
  2026-08-14  8:14 ` [PATCH net-next 08/14] selftests/bpf: Test skb_ext read from sock_ops and LSM hooks Jakub Sitnicki
@ 2026-08-15  8:14   ` sashiko-bot
  0 siblings, 0 replies; 26+ messages in thread
From: sashiko-bot @ 2026-08-15  8:14 UTC (permalink / raw)
  To: Jakub Sitnicki; +Cc: bpf

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] Invalid error check for `get_socket_local_port()` due to assignment to an unsigned `__be16` type.
--

--- Patch [8]: [PATCH net-next 08/14] selftests/bpf: Test skb_ext read from sock_ops and LSM hooks ---
commit e995e7626c7fc8d98cbc259f8c506f6ec68e6a00
Author: Jakub Sitnicki <jakub@cloudflare.com>

selftests/bpf: Test skb_ext read from sock_ops and LSM hooks

This commit extends skb_ext test coverage to the TCP ingress path. It adds
a new test that attaches a TC ingress program to write metadata to packets
and reads it back using sock_ops and LSM hooks.

> 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 808eb158fe0f1..e0ab0fccf9d7e 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
[ ... ]
> +/* 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);
[ ... ]
> +	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"))

[Severity: Medium]
Does assigning the result of get_socket_local_port() to the unsigned __be16
port variable defeat this error check?

get_socket_local_port() returns an int, which can be a negative error code.
If the function fails and returns a negative value, storing it in an
unsigned __be16 will result in a positive value. This would cause the
ASSERT_GE(port, 0, "get_port") condition to always pass, ignoring the
failure and potentially leading to unexpected test behavior.

> +		goto cleanup;
> +
> +	skel->bss->target_port = port;
> +	skel->bss->test_pass = false;

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260814-bpf-meta-inside-skb-ext-v1-0-767edd862656@cloudflare.com?part=8

^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: [PATCH net-next 09/14] selftests/bpf: Test skb_ext read from kfree_skb tracepoint
  2026-08-14  8:14 ` [PATCH net-next 09/14] selftests/bpf: Test skb_ext read from kfree_skb tracepoint Jakub Sitnicki
@ 2026-08-15  8:14   ` sashiko-bot
  0 siblings, 0 replies; 26+ messages in thread
From: sashiko-bot @ 2026-08-15  8:14 UTC (permalink / raw)
  To: Jakub Sitnicki; +Cc: bpf

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] Missing validation of `recv()` failure before reading `errno`.
- [Medium] Missing return value check for `send()` can lead to infinite test hang.
--

--- Patch [9]: [PATCH net-next 09/14] selftests/bpf: Test skb_ext read from kfree_skb tracepoint ---
commit 8897bd2269141881d412c939eda870a1cef5354d
Author: Jakub Sitnicki <jakub@cloudflare.com>

selftests/bpf: Test skb_ext read from kfree_skb tracepoint

This commit adds a test that writes skb_ext at cgroup/skb egress, sends UDP
to a closed loopback port so the packet is dropped, and reads the extension
back from a tp_btf/kfree_skb program.

> 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 e0ab0fccf9d7e..1e4e12d2b041c 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
[ ... ]
> @@ -1186,6 +1186,69 @@ static void test_skb_ext_tcp(struct test_xdp_meta *skel, const char *name,
[ ... ]
> +static void test_cgrp_egress_to_kfree_skb(struct test_xdp_meta *skel)
> +{
[ ... ]
> +	ret = connect(fd, (void *)&addr, sizeof(addr));
> +	if (!ASSERT_OK(ret, "connect"))
> +		goto cleanup;
> +
> +	send(fd, test_payload, TEST_PAYLOAD_LEN, 0);

[Severity: Medium]
Is there a risk of an infinite hang here if send() fails locally?
If send() fails (for example, due to local buffer exhaustion or routing
issues), the packet is never sent, and the kernel will not generate the
expected ICMP error. Consequently, the test might hang indefinitely in the
subsequent blocking recv() call.

> +
> +	/* Wait for ICMP error -- confirms the packet was freed */
> +	ret = recv(fd, buf, sizeof(buf), 0);
> +	ASSERT_EQ(errno, ECONNREFUSED, "recv_econnrefused");

[Severity: Medium]
Should ret be checked for -1 before evaluating errno?
According to POSIX semantics, errno is only meaningful if a system call
returns an error. If recv() happens to succeed (for example, by receiving
a stray UDP packet), errno will contain a stale value, which could cause the
test to spuriously pass or fail.

> +
> +	ASSERT_TRUE(skel->bss->test_pass, "test_pass");
> +
> +cleanup:
[ ... ]

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260814-bpf-meta-inside-skb-ext-v1-0-767edd862656@cloudflare.com?part=9

^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: [PATCH net-next 11/14] selftests/bpf: Test skb_ext from LWT in, out, and xmit hooks
  2026-08-14  8:14 ` [PATCH net-next 11/14] selftests/bpf: Test skb_ext from LWT in, out, and xmit hooks Jakub Sitnicki
@ 2026-08-15  8:14   ` sashiko-bot
  0 siblings, 0 replies; 26+ messages in thread
From: sashiko-bot @ 2026-08-15  8:14 UTC (permalink / raw)
  To: Jakub Sitnicki; +Cc: bpf

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Medium] The error check for `get_socket_local_port()` is rendered completely ineffective due to assigning the result to an unsigned 16-bit integer before comparison.
- [Medium] Unconditional execution of `bpf_tc_hook_destroy(&tc_hook)` on the error path can delete the `clsact` qdisc from the base network namespace.
- [Low] The multi-line comment for `test_skb_ext_lwt` violates the BPF subsystem comment style guidelines.
--

--- Patch [11]: [PATCH net-next 11/14] selftests/bpf: Test skb_ext from LWT in, out, and xmit hooks ---
commit 7845f94d2569f8018cf274a4a851280db3c38a3a
Author: Jakub Sitnicki <jakub@cloudflare.com>

selftests/bpf: Test skb_ext from LWT in, out, and xmit hooks

Add cross-hook tests for bpf_dynptr_from_skb_ext() covering all generic
LWT program types. For lwt_in, a TC ingress program writes skb_ext on
loopback and the LWT program reads it back. For lwt_out and lwt_xmit,
the LWT programs write skb_ext on the output path and a TC ingress
program reads it back after loopback.

> 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 82c57ea3957e3..7a7537ef80e75 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
> @@ -1303,6 +1303,95 @@ static void test_skb_ext_nf(struct test_xdp_meta *skel, const char *name)
>  	netns_free(ns);
>  }
>  
> +#define LWT_EXT_PIN_PATH "/sys/fs/bpf/skb_ext_lwt"
> +
> +/* Test skb_ext across LWT hooks on loopback.

[Severity: Low]
Does this violate the BPF subsystem multi-line comment style? The opening /*
usually needs to be on its own line before the text begins.

> + *
> + * @lwt_prog:  BPF program to pin and attach via ip route encap
> + * @encap_dir: "in", "out", or "xmit"
> + * @writer:    true if lwt_prog writes skb_ext (reader on TC ingress),
> + *             false if lwt_prog reads skb_ext (writer on TC ingress)
> + */
> +static void test_skb_ext_lwt(struct test_xdp_meta *skel, const char *name,
> +			     struct bpf_program *lwt_prog,
> +			     const char *encap_dir, bool writer)
> +{
> +	LIBBPF_OPTS(bpf_tc_hook, tc_hook,
> +		    .ifindex = 1 /* IFINDEX_LO */,
> +		    .attach_point = BPF_TC_INGRESS);

[ ... ]

> +	__be16 port;
> +	int ret;
> +
> +	unlink(LWT_EXT_PIN_PATH);

[ ... ]

> +	port = get_socket_local_port(server_fd);
> +	if (!ASSERT_GE(port, 0, "get_port"))
> +		goto cleanup;

[Severity: Medium]
Will this error check work as intended? The get_socket_local_port() function
returns an int, which is -1 on error. Since port is declared as an unsigned
16-bit integer (__be16), the -1 gets converted to 65535, causing the
ASSERT_GE(port, 0) check to always evaluate to true.

[ ... ]

> +cleanup:
> +	if (fd >= 0)
> +		close(fd);
> +	bpf_tc_hook_destroy(&tc_hook);

[Severity: Medium]
Could this unconditionally tear down the clsact qdisc on the base namespace's
loopback interface if an error occurs early?

If a failure happens before the network namespace is created or before
bpf_tc_hook_create() is called, the thread is still in the original
namespace. Since tc_hook is statically initialized with .ifindex = 1,
will this destroy the hook in the wrong namespace?

> +	if (server_fd >= 0)
> +		close(server_fd);
> +	netns_free(ns);
> +	if (pinned)
> +		unlink(LWT_EXT_PIN_PATH);
> +}

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260814-bpf-meta-inside-skb-ext-v1-0-767edd862656@cloudflare.com?part=11

^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: [PATCH net-next 12/14] selftests/bpf: Test skb_ext read from seg6local End.BPF hook
  2026-08-14  8:14 ` [PATCH net-next 12/14] selftests/bpf: Test skb_ext read from seg6local End.BPF hook Jakub Sitnicki
@ 2026-08-15  8:14   ` sashiko-bot
  0 siblings, 0 replies; 26+ messages in thread
From: sashiko-bot @ 2026-08-15  8:14 UTC (permalink / raw)
  To: Jakub Sitnicki; +Cc: bpf

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] The assertion `ASSERT_GE(tc_hook.ifindex, 0)` incorrectly accepts 0 as a valid interface index, masking `if_nametoindex()` failures.
--

--- Patch [12]: [PATCH net-next 12/14] selftests/bpf: Test skb_ext read from seg6local End.BPF hook ---
commit b36d673a00ed1b242e3b7537fce3622d564183f9
Author: Jakub Sitnicki <jakub@cloudflare.com>

selftests/bpf: Test skb_ext read from seg6local End.BPF hook

This patch adds a test for reading skb_ext from an lwt_seg6local context.
It sets up a three-namespace topology to verify that an skb_ext written
at TC egress can be successfully read back by a seg6local End.BPF hook
running in a different namespace.

> 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 7a7537ef80e75..5f9247941aa01 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
[ ... ]
> +	/* NS1: write skb_ext at TC egress on veth1, then send.
> +	 * TC egress fires after seg6 encap, on the encapped packet.
> +	 */
> +	nstoken = open_netns("seg6_1");
> +	if (!ASSERT_OK_PTR(nstoken, "open seg6_1"))
> +		goto cleanup;
> +
> +	tc_hook.ifindex = if_nametoindex("veth1");
> +	if (!ASSERT_GE(tc_hook.ifindex, 0, "ifindex veth1"))

[Severity: Medium]
Does this assertion properly detect an interface lookup failure? 

When if_nametoindex() fails to find the network interface, it returns 0. 
Using ASSERT_GE(..., 0) allows a return value of 0 to pass the check. If the 
interface is missing, the test will proceed and likely fail later in 
bpf_tc_hook_create(), which could obfuscate the true failure point.

Would it be better to ensure the index is strictly greater than 0 here?

> +		goto cleanup;
> +
> +	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);

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260814-bpf-meta-inside-skb-ext-v1-0-767edd862656@cloudflare.com?part=12

^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: [PATCH net-next 13/14] selftests/bpf: Test skb_ext read from sk_skb stream verdict hook
  2026-08-14  8:14 ` [PATCH net-next 13/14] selftests/bpf: Test skb_ext read from sk_skb stream verdict hook Jakub Sitnicki
@ 2026-08-15  8:14   ` sashiko-bot
  0 siblings, 0 replies; 26+ messages in thread
From: sashiko-bot @ 2026-08-15  8:14 UTC (permalink / raw)
  To: Jakub Sitnicki; +Cc: bpf

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] The multi-line comment preceding `test_skb_ext_sk_skb` violates the BPF subsystem comment style guidelines by placing text on the same line as the opening `/*`.
--

--- Patch [13]: [PATCH net-next 13/14] selftests/bpf: Test skb_ext read from sk_skb stream verdict hook ---
commit a5b31d7bdebf3ee8a7f99246938befdabdfff8e4
Author: Jakub Sitnicki <jakub@cloudflare.com>

selftests/bpf: Test skb_ext read from sk_skb stream verdict hook

This commit adds a selftest to verify that skb_ext survives across the TC
ingress to sk_skb verdict hook. It implements this by writing to skb_ext
from TC on loopback and reading from it in the sockmap verdict program.

> 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 5f9247941aa01..f5fbc19757e3e 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
[ ... ]
> @@ -1560,6 +1561,82 @@ static void test_skb_ext_seg6local(struct test_xdp_meta *skel)
>  		unlink(SEG6_PIN_PATH);
>  }
>  
> +/* Test skb_ext survival across TC ingress -> sk_skb verdict hook.
> + * TC ingress writes skb_ext on loopback; the verdict program reads it
> + * on skb delivery to a socket in the sockmap.
> + */

[Severity: Low]
This isn't a bug, but does this multi-line comment format follow the BPF
subsystem style guidelines? The preferred modern kernel style for BPF
subsystem files places the opening /* on its own line before the text begins.

> +static void test_skb_ext_sk_skb(struct test_xdp_meta *skel)
> +{
> +	LIBBPF_OPTS(bpf_tc_hook, tc_hook,
[ ... ]

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260814-bpf-meta-inside-skb-ext-v1-0-767edd862656@cloudflare.com?part=13

^ permalink raw reply	[flat|nested] 26+ messages in thread

end of thread, other threads:[~2026-08-15  8:14 UTC | newest]

Thread overview: 26+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-14  8:14 [PATCH net-next 00/14] skb extension for BPF metadata Jakub Sitnicki
2026-08-14  8:14 ` [PATCH net-next 01/14] bpf: Introduce per-packet metadata storage for BPF programs Jakub Sitnicki
2026-08-15  8:14   ` sashiko-bot
2026-08-14  8:14 ` [PATCH net-next 02/14] bpf: Allow access to bpf_sock_ops_kern->skb Jakub Sitnicki
2026-08-14  8:14 ` [PATCH net-next 03/14] bpf: Make BPF skb extension survive packet scrubbing Jakub Sitnicki
2026-08-15  8:14   ` sashiko-bot
2026-08-14  8:14 ` [PATCH net-next 04/14] selftests/bpf: Add tests for bpf_dynptr_from_skb_ext Jakub Sitnicki
2026-08-15  8:14   ` sashiko-bot
2026-08-14  8:14 ` [PATCH net-next 05/14] selftests/bpf: Test skb_ext on cloned skbs Jakub Sitnicki
2026-08-15  8:14   ` sashiko-bot
2026-08-14  8:14 ` [PATCH net-next 06/14] selftests/bpf: Test skb_ext survival across veth and GRE Jakub Sitnicki
2026-08-15  8:14   ` sashiko-bot
2026-08-14  8:14 ` [PATCH net-next 07/14] selftests/bpf: Test skb_ext read from cgroup_skb and sk_filter hooks Jakub Sitnicki
2026-08-15  8:14   ` sashiko-bot
2026-08-14  8:14 ` [PATCH net-next 08/14] selftests/bpf: Test skb_ext read from sock_ops and LSM hooks Jakub Sitnicki
2026-08-15  8:14   ` sashiko-bot
2026-08-14  8:14 ` [PATCH net-next 09/14] selftests/bpf: Test skb_ext read from kfree_skb tracepoint Jakub Sitnicki
2026-08-15  8:14   ` sashiko-bot
2026-08-14  8:14 ` [PATCH net-next 10/14] selftests/bpf: Test skb_ext read from netfilter hook Jakub Sitnicki
2026-08-14  8:14 ` [PATCH net-next 11/14] selftests/bpf: Test skb_ext from LWT in, out, and xmit hooks Jakub Sitnicki
2026-08-15  8:14   ` sashiko-bot
2026-08-14  8:14 ` [PATCH net-next 12/14] selftests/bpf: Test skb_ext read from seg6local End.BPF hook Jakub Sitnicki
2026-08-15  8:14   ` sashiko-bot
2026-08-14  8:14 ` [PATCH net-next 13/14] selftests/bpf: Test skb_ext read from sk_skb stream verdict hook Jakub Sitnicki
2026-08-15  8:14   ` sashiko-bot
2026-08-14  8:14 ` [PATCH net-next 14/14] selftests/bpf: Use non-trivial test payload in xdp_context tests Jakub Sitnicki

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