All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH RFC net-next 0/6] skb extension for BPF metadata
@ 2026-07-14 17:48 Jakub Sitnicki
  2026-07-14 17:48 ` [PATCH RFC net-next 1/6] bpf: Introduce per-packet metadata storage for BPF programs Jakub Sitnicki
                   ` (5 more replies)
  0 siblings, 6 replies; 12+ messages in thread
From: Jakub Sitnicki @ 2026-07-14 17:48 UTC (permalink / raw)
  To: netdev; +Cc: bpf, kernel-team

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

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

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

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

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

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

Thanks,
-jkbs

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

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

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


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

* [PATCH RFC net-next 1/6] bpf: Introduce per-packet metadata storage for BPF programs
  2026-07-14 17:48 [PATCH RFC net-next 0/6] skb extension for BPF metadata Jakub Sitnicki
@ 2026-07-14 17:48 ` Jakub Sitnicki
  2026-07-16 12:10   ` Stanislav Fomichev
  2026-07-14 17:48 ` [PATCH RFC net-next 2/6] bpf: Allow access to bpf_sock_ops_kern->skb Jakub Sitnicki
                   ` (4 subsequent siblings)
  5 siblings, 1 reply; 12+ messages in thread
From: Jakub Sitnicki @ 2026-07-14 17:48 UTC (permalink / raw)
  To: netdev; +Cc: bpf, kernel-team

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

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

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

Guard the feature behind a new CONFIG_BPF_SKB_EXT option.

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

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

-- 
2.43.0


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

* [PATCH RFC net-next 2/6] bpf: Allow access to bpf_sock_ops_kern->skb
  2026-07-14 17:48 [PATCH RFC net-next 0/6] skb extension for BPF metadata Jakub Sitnicki
  2026-07-14 17:48 ` [PATCH RFC net-next 1/6] bpf: Introduce per-packet metadata storage for BPF programs Jakub Sitnicki
@ 2026-07-14 17:48 ` Jakub Sitnicki
  2026-07-14 17:48 ` [PATCH RFC net-next 3/6] bpf: Allow skb extensions to survive packet scrubbing Jakub Sitnicki
                   ` (3 subsequent siblings)
  5 siblings, 0 replies; 12+ messages in thread
From: Jakub Sitnicki @ 2026-07-14 17:48 UTC (permalink / raw)
  To: netdev; +Cc: bpf, kernel-team

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

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

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

-- 
2.43.0


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

* [PATCH RFC net-next 3/6] bpf: Allow skb extensions to survive packet scrubbing
  2026-07-14 17:48 [PATCH RFC net-next 0/6] skb extension for BPF metadata Jakub Sitnicki
  2026-07-14 17:48 ` [PATCH RFC net-next 1/6] bpf: Introduce per-packet metadata storage for BPF programs Jakub Sitnicki
  2026-07-14 17:48 ` [PATCH RFC net-next 2/6] bpf: Allow access to bpf_sock_ops_kern->skb Jakub Sitnicki
@ 2026-07-14 17:48 ` Jakub Sitnicki
  2026-07-16 12:11   ` Stanislav Fomichev
  2026-07-14 17:48 ` [PATCH RFC net-next 4/6] selftests/bpf: Add tests for bpf_dynptr_from_skb_ext Jakub Sitnicki
                   ` (2 subsequent siblings)
  5 siblings, 1 reply; 12+ messages in thread
From: Jakub Sitnicki @ 2026-07-14 17:48 UTC (permalink / raw)
  To: netdev; +Cc: bpf, kernel-team

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

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

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

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

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

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

-- 
2.43.0


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

* [PATCH RFC net-next 4/6] selftests/bpf: Add tests for bpf_dynptr_from_skb_ext
  2026-07-14 17:48 [PATCH RFC net-next 0/6] skb extension for BPF metadata Jakub Sitnicki
                   ` (2 preceding siblings ...)
  2026-07-14 17:48 ` [PATCH RFC net-next 3/6] bpf: Allow skb extensions to survive packet scrubbing Jakub Sitnicki
@ 2026-07-14 17:48 ` Jakub Sitnicki
  2026-07-14 17:48 ` [PATCH RFC net-next 5/6] selftests/bpf: Test skb_ext scrubbing across tunnels and veths Jakub Sitnicki
  2026-07-14 17:48 ` [PATCH RFC net-next 6/6] selftests/bpf: Use non-trivial test payload in xdp_context tests Jakub Sitnicki
  5 siblings, 0 replies; 12+ messages in thread
From: Jakub Sitnicki @ 2026-07-14 17:48 UTC (permalink / raw)
  To: netdev; +Cc: bpf, kernel-team

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

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

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

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

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

Cover skb clone isolation:

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

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

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

-- 
2.43.0


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

* [PATCH RFC net-next 5/6] selftests/bpf: Test skb_ext scrubbing across tunnels and veths
  2026-07-14 17:48 [PATCH RFC net-next 0/6] skb extension for BPF metadata Jakub Sitnicki
                   ` (3 preceding siblings ...)
  2026-07-14 17:48 ` [PATCH RFC net-next 4/6] selftests/bpf: Add tests for bpf_dynptr_from_skb_ext Jakub Sitnicki
@ 2026-07-14 17:48 ` Jakub Sitnicki
  2026-07-14 17:48 ` [PATCH RFC net-next 6/6] selftests/bpf: Use non-trivial test payload in xdp_context tests Jakub Sitnicki
  5 siblings, 0 replies; 12+ messages in thread
From: Jakub Sitnicki @ 2026-07-14 17:48 UTC (permalink / raw)
  To: netdev; +Cc: bpf, kernel-team

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

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

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

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

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

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

-- 
2.43.0


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

* [PATCH RFC net-next 6/6] selftests/bpf: Use non-trivial test payload in xdp_context tests
  2026-07-14 17:48 [PATCH RFC net-next 0/6] skb extension for BPF metadata Jakub Sitnicki
                   ` (4 preceding siblings ...)
  2026-07-14 17:48 ` [PATCH RFC net-next 5/6] selftests/bpf: Test skb_ext scrubbing across tunnels and veths Jakub Sitnicki
@ 2026-07-14 17:48 ` Jakub Sitnicki
  5 siblings, 0 replies; 12+ messages in thread
From: Jakub Sitnicki @ 2026-07-14 17:48 UTC (permalink / raw)
  To: netdev; +Cc: bpf, kernel-team

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

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

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

-- 
2.43.0


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

* Re: [PATCH RFC net-next 1/6] bpf: Introduce per-packet metadata storage for BPF programs
  2026-07-14 17:48 ` [PATCH RFC net-next 1/6] bpf: Introduce per-packet metadata storage for BPF programs Jakub Sitnicki
@ 2026-07-16 12:10   ` Stanislav Fomichev
  2026-07-16 12:35     ` Jakub Sitnicki
  0 siblings, 1 reply; 12+ messages in thread
From: Stanislav Fomichev @ 2026-07-16 12:10 UTC (permalink / raw)
  To: Jakub Sitnicki; +Cc: netdev, bpf, kernel-team

On 07/14, Jakub Sitnicki wrote:
> BPF programs attached at different points in the network stack have no way
> to pass data between each other on a per-packet basis, other than by
> stashing it into a shared BPF map. xdp/skb->data_meta works for XDP-to-TC
> handoff, but is not available to programs running at later hooks like
> cgroup/skb, sock_ops, socket filters, tracing or LSM.
> 
> Add a new skb extension (struct bpf_skb_ext) that provides up to 256 bytes
> of per-packet storage. Size is configurable at build time though the
> CONFIG_BPF_SKB_EXT_SIZE option. The storage is embedded inside the
> extension chunk itself.
> 
> Expose the storage to BPF programs via bpf_dynptr_from_skb_ext() kfunc.
> The caller passes BPF_SKB_EXT_F_CREATE to allocate or COW (unshare) the
> extension and get a read-write dynptr. Without the flag, it gets a
> read-only dynptr to the existing extension, or -ENOENT if none exists.
> 
> Guard the feature behind a new CONFIG_BPF_SKB_EXT option.
> 
> Signed-off-by: Jakub Sitnicki <jakub@cloudflare.com>
> ---
>  include/linux/bpf.h      |  10 ++++
>  include/linux/filter.h   |  26 ++++++++++
>  include/linux/skbuff.h   |   3 ++
>  include/uapi/linux/bpf.h |   5 ++
>  kernel/bpf/helpers.c     |   7 +++
>  kernel/bpf/log.c         |   2 +
>  kernel/bpf/verifier.c    |  10 +++-
>  net/Kconfig              |  20 ++++++++
>  net/core/filter.c        | 129 +++++++++++++++++++++++++++++++++++++++++++++++
>  net/core/skbuff.c        |   3 ++
>  10 files changed, 214 insertions(+), 1 deletion(-)
> 
> diff --git a/include/linux/bpf.h b/include/linux/bpf.h
> index 7719f6528445..6b918a5b61bf 100644
> --- a/include/linux/bpf.h
> +++ b/include/linux/bpf.h
> @@ -1484,6 +1484,8 @@ enum bpf_dynptr_type {
>  	BPF_DYNPTR_TYPE_SKB_META,
>  	/* Underlying data is a file */
>  	BPF_DYNPTR_TYPE_FILE,
> +	/* Underlying data is a bpf_skb_ext chunk */
> +	BPF_DYNPTR_TYPE_SKB_EXT,
>  };
>  
>  int bpf_dynptr_check_size(u64 size);
> @@ -4209,4 +4211,12 @@ static inline int bpf_map_check_op_flags(struct bpf_map *map, u64 flags, u64 all
>  	return 0;
>  }
>  
> +#ifdef CONFIG_BPF_SKB_EXT
> +
> +struct bpf_skb_ext {
> +	u8 buf[CONFIG_BPF_SKB_EXT_SIZE] __aligned(8);
> +};

Can we do a dynamic size from the start? Say, some sysfs knob, 0 by
default. Once written, it's locks in the size and can't be changed.
Then your new 'flags' field can be used to indicate whether the area
actually has been allocated or not?

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

* Re: [PATCH RFC net-next 3/6] bpf: Allow skb extensions to survive packet scrubbing
  2026-07-14 17:48 ` [PATCH RFC net-next 3/6] bpf: Allow skb extensions to survive packet scrubbing Jakub Sitnicki
@ 2026-07-16 12:11   ` Stanislav Fomichev
  2026-07-16 12:59     ` Jakub Sitnicki
  0 siblings, 1 reply; 12+ messages in thread
From: Stanislav Fomichev @ 2026-07-16 12:11 UTC (permalink / raw)
  To: Jakub Sitnicki; +Cc: netdev, bpf, kernel-team

On 07/14, Jakub Sitnicki wrote:
> skb_scrub_packet() drops all skb extensions unconditionally via
> skb_ext_reset(). It runs on tunnel encap/decap (ip_tunnel_rcv,
> vxlan_rcv, etc.) and cross-netns forwarding (dev_forward_skb).
> 
> This makes it impossible for a BPF program to pass metadata via
> bpf_skb_ext through a tunnel or across a netns boundary. The extension
> is always lost at the scrub point.
> 
> Introduce skb_ext_scrub() which consults each active extension before
> discarding it. Extensions that request preservation are kept while the
> rest are torn down. When the extension slab is shared with clones, COW
> ensures isolation. Replace the skb_ext_reset() call in
> skb_scrub_packet() with skb_ext_scrub().
> 
> Expose the opt-in mechanism to BPF via the BPF_SKB_EXT_F_NO_SCRUB flag
> for bpf_dynptr_from_skb_ext(). A program that sets this flag when
> creating the extension signals that its metadata should survive
> scrubbing.
> 
> Signed-off-by: Jakub Sitnicki <jakub@cloudflare.com>
> ---
>  include/linux/bpf.h      |  1 +
>  include/linux/skbuff.h   |  2 ++
>  include/uapi/linux/bpf.h |  3 +-
>  net/core/filter.c        | 11 +++++--
>  net/core/skbuff.c        | 82 ++++++++++++++++++++++++++++++++++++++++++------
>  net/ipv4/udp.c           |  2 +-
>  6 files changed, 86 insertions(+), 15 deletions(-)
> 
> diff --git a/include/linux/bpf.h b/include/linux/bpf.h
> index 6b918a5b61bf..a46ca53c5b27 100644
> --- a/include/linux/bpf.h
> +++ b/include/linux/bpf.h
> @@ -4214,6 +4214,7 @@ static inline int bpf_map_check_op_flags(struct bpf_map *map, u64 flags, u64 all
>  #ifdef CONFIG_BPF_SKB_EXT
>  
>  struct bpf_skb_ext {
> +	u64 flags;
>  	u8 buf[CONFIG_BPF_SKB_EXT_SIZE] __aligned(8);
>  };
>  
> diff --git a/include/linux/skbuff.h b/include/linux/skbuff.h
> index 584d8440d352..66afa5489007 100644
> --- a/include/linux/skbuff.h
> +++ b/include/linux/skbuff.h
> @@ -5063,6 +5063,7 @@ void *__skb_ext_set(struct sk_buff *skb, enum skb_ext_id id,
>  void *skb_ext_add(struct sk_buff *skb, enum skb_ext_id id);
>  void __skb_ext_del(struct sk_buff *skb, enum skb_ext_id id);
>  void __skb_ext_put(struct skb_ext *ext);
> +void skb_ext_scrub(struct sk_buff *skb);
>  
>  static inline void skb_ext_put(struct sk_buff *skb)
>  {
> @@ -5132,6 +5133,7 @@ static inline bool skb_has_extensions(struct sk_buff *skb)
>  static inline void __skb_ext_put(struct skb_ext *ext) {}
>  static inline void skb_ext_put(struct sk_buff *skb) {}
>  static inline void skb_ext_reset(struct sk_buff *skb) {}
> +static inline void skb_ext_scrub(struct sk_buff *skb) {}
>  static inline void skb_ext_del(struct sk_buff *skb, int unused) {}
>  static inline void __skb_ext_copy(struct sk_buff *d, const struct sk_buff *s) {}
>  static inline void skb_ext_copy(struct sk_buff *dst, const struct sk_buff *s) {}
> diff --git a/include/uapi/linux/bpf.h b/include/uapi/linux/bpf.h
> index 3eee4467422d..02da170205de 100644
> --- a/include/uapi/linux/bpf.h
> +++ b/include/uapi/linux/bpf.h
> @@ -7734,7 +7734,8 @@ struct bpf_insn_array_value {
>  
>  /* Flags to control bpf_dynptr_from_skb_ext() behavior. */
>  enum {
> -	BPF_SKB_EXT_F_CREATE = (1ULL << 0),
> +	BPF_SKB_EXT_F_CREATE	= (1ULL << 0),
> +	BPF_SKB_EXT_F_NO_SCRUB	= (1ULL << 1),

Do I understand correctly that you do prefer the NO_SCRUB mode? Any reason
we need to have scrub mode? If it's all produced/consumed by bpf, maybe
we can just carry this data unconditionally instead of having a SCRUB/NO_SCRUB
option?

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

* Re: [PATCH RFC net-next 1/6] bpf: Introduce per-packet metadata storage for BPF programs
  2026-07-16 12:10   ` Stanislav Fomichev
@ 2026-07-16 12:35     ` Jakub Sitnicki
  0 siblings, 0 replies; 12+ messages in thread
From: Jakub Sitnicki @ 2026-07-16 12:35 UTC (permalink / raw)
  To: Stanislav Fomichev; +Cc: netdev, bpf, kernel-team

On Thu, Jul 16, 2026 at 05:10 AM -07, Stanislav Fomichev wrote:
> On 07/14, Jakub Sitnicki wrote:
>> BPF programs attached at different points in the network stack have no way
>> to pass data between each other on a per-packet basis, other than by
>> stashing it into a shared BPF map. xdp/skb->data_meta works for XDP-to-TC
>> handoff, but is not available to programs running at later hooks like
>> cgroup/skb, sock_ops, socket filters, tracing or LSM.
>> 
>> Add a new skb extension (struct bpf_skb_ext) that provides up to 256 bytes
>> of per-packet storage. Size is configurable at build time though the
>> CONFIG_BPF_SKB_EXT_SIZE option. The storage is embedded inside the
>> extension chunk itself.
>> 
>> Expose the storage to BPF programs via bpf_dynptr_from_skb_ext() kfunc.
>> The caller passes BPF_SKB_EXT_F_CREATE to allocate or COW (unshare) the
>> extension and get a read-write dynptr. Without the flag, it gets a
>> read-only dynptr to the existing extension, or -ENOENT if none exists.
>> 
>> Guard the feature behind a new CONFIG_BPF_SKB_EXT option.
>> 
>> Signed-off-by: Jakub Sitnicki <jakub@cloudflare.com>
>> ---
>>  include/linux/bpf.h      |  10 ++++
>>  include/linux/filter.h   |  26 ++++++++++
>>  include/linux/skbuff.h   |   3 ++
>>  include/uapi/linux/bpf.h |   5 ++
>>  kernel/bpf/helpers.c     |   7 +++
>>  kernel/bpf/log.c         |   2 +
>>  kernel/bpf/verifier.c    |  10 +++-
>>  net/Kconfig              |  20 ++++++++
>>  net/core/filter.c        | 129 +++++++++++++++++++++++++++++++++++++++++++++++
>>  net/core/skbuff.c        |   3 ++
>>  10 files changed, 214 insertions(+), 1 deletion(-)
>> 
>> diff --git a/include/linux/bpf.h b/include/linux/bpf.h
>> index 7719f6528445..6b918a5b61bf 100644
>> --- a/include/linux/bpf.h
>> +++ b/include/linux/bpf.h
>> @@ -1484,6 +1484,8 @@ enum bpf_dynptr_type {
>>  	BPF_DYNPTR_TYPE_SKB_META,
>>  	/* Underlying data is a file */
>>  	BPF_DYNPTR_TYPE_FILE,
>> +	/* Underlying data is a bpf_skb_ext chunk */
>> +	BPF_DYNPTR_TYPE_SKB_EXT,
>>  };
>>  
>>  int bpf_dynptr_check_size(u64 size);
>> @@ -4209,4 +4211,12 @@ static inline int bpf_map_check_op_flags(struct bpf_map *map, u64 flags, u64 all
>>  	return 0;
>>  }
>>  
>> +#ifdef CONFIG_BPF_SKB_EXT
>> +
>> +struct bpf_skb_ext {
>> +	u8 buf[CONFIG_BPF_SKB_EXT_SIZE] __aligned(8);
>> +};
>
> Can we do a dynamic size from the start? Say, some sysfs knob, 0 by
> default. Once written, it's locks in the size and can't be changed.
> Then your new 'flags' field can be used to indicate whether the area
> actually has been allocated or not?

Thanks for feedback. Dynamic size would be ideal.

We're limited by skb extensions implementation here, which needs to know
the (maximum) size of each skb extension chunk at init time [1].

That said, there's been hallway discussion at Netdev, that we should
look into making skb_ext allocate memory just for activated extensions
and realloc as needed, as extensions are gaining more users.

Current implementation (allocate space for every available extension) is
wasteful, as you have extension combos which are not possible (won't be
active at the same time), like IPsec and MPTCP and CAN.

IOW, build-time config is what is doable today. Having a sysctl knob
doesn't buy us much. But we have a path to lift that constraint later
(if we don't mess up the BPF API).

[1] https://elixir.bootlin.com/linux/v7.2-rc3/source/net/core/skbuff.c#L5171

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

* Re: [PATCH RFC net-next 3/6] bpf: Allow skb extensions to survive packet scrubbing
  2026-07-16 12:11   ` Stanislav Fomichev
@ 2026-07-16 12:59     ` Jakub Sitnicki
  2026-07-16 15:06       ` Jason Xing
  0 siblings, 1 reply; 12+ messages in thread
From: Jakub Sitnicki @ 2026-07-16 12:59 UTC (permalink / raw)
  To: Stanislav Fomichev, Daniel Borkmann, John Fastabend
  Cc: netdev, bpf, kernel-team, Jakub Kicinski, Kuniyuki Iwashima

On Thu, Jul 16, 2026 at 05:11 AM -07, Stanislav Fomichev wrote:
> On 07/14, Jakub Sitnicki wrote:
>> skb_scrub_packet() drops all skb extensions unconditionally via
>> skb_ext_reset(). It runs on tunnel encap/decap (ip_tunnel_rcv,
>> vxlan_rcv, etc.) and cross-netns forwarding (dev_forward_skb).
>> 
>> This makes it impossible for a BPF program to pass metadata via
>> bpf_skb_ext through a tunnel or across a netns boundary. The extension
>> is always lost at the scrub point.
>> 
>> Introduce skb_ext_scrub() which consults each active extension before
>> discarding it. Extensions that request preservation are kept while the
>> rest are torn down. When the extension slab is shared with clones, COW
>> ensures isolation. Replace the skb_ext_reset() call in
>> skb_scrub_packet() with skb_ext_scrub().
>> 
>> Expose the opt-in mechanism to BPF via the BPF_SKB_EXT_F_NO_SCRUB flag
>> for bpf_dynptr_from_skb_ext(). A program that sets this flag when
>> creating the extension signals that its metadata should survive
>> scrubbing.
>> 
>> Signed-off-by: Jakub Sitnicki <jakub@cloudflare.com>
>> ---
>>  include/linux/bpf.h      |  1 +
>>  include/linux/skbuff.h   |  2 ++
>>  include/uapi/linux/bpf.h |  3 +-
>>  net/core/filter.c        | 11 +++++--
>>  net/core/skbuff.c        | 82 ++++++++++++++++++++++++++++++++++++++++++------
>>  net/ipv4/udp.c           |  2 +-
>>  6 files changed, 86 insertions(+), 15 deletions(-)
>> 
>> diff --git a/include/linux/bpf.h b/include/linux/bpf.h
>> index 6b918a5b61bf..a46ca53c5b27 100644
>> --- a/include/linux/bpf.h
>> +++ b/include/linux/bpf.h
>> @@ -4214,6 +4214,7 @@ static inline int bpf_map_check_op_flags(struct bpf_map *map, u64 flags, u64 all
>>  #ifdef CONFIG_BPF_SKB_EXT
>>  
>>  struct bpf_skb_ext {
>> +	u64 flags;
>>  	u8 buf[CONFIG_BPF_SKB_EXT_SIZE] __aligned(8);
>>  };
>>  
>> diff --git a/include/linux/skbuff.h b/include/linux/skbuff.h
>> index 584d8440d352..66afa5489007 100644
>> --- a/include/linux/skbuff.h
>> +++ b/include/linux/skbuff.h
>> @@ -5063,6 +5063,7 @@ void *__skb_ext_set(struct sk_buff *skb, enum skb_ext_id id,
>>  void *skb_ext_add(struct sk_buff *skb, enum skb_ext_id id);
>>  void __skb_ext_del(struct sk_buff *skb, enum skb_ext_id id);
>>  void __skb_ext_put(struct skb_ext *ext);
>> +void skb_ext_scrub(struct sk_buff *skb);
>>  
>>  static inline void skb_ext_put(struct sk_buff *skb)
>>  {
>> @@ -5132,6 +5133,7 @@ static inline bool skb_has_extensions(struct sk_buff *skb)
>>  static inline void __skb_ext_put(struct skb_ext *ext) {}
>>  static inline void skb_ext_put(struct sk_buff *skb) {}
>>  static inline void skb_ext_reset(struct sk_buff *skb) {}
>> +static inline void skb_ext_scrub(struct sk_buff *skb) {}
>>  static inline void skb_ext_del(struct sk_buff *skb, int unused) {}
>>  static inline void __skb_ext_copy(struct sk_buff *d, const struct sk_buff *s) {}
>>  static inline void skb_ext_copy(struct sk_buff *dst, const struct sk_buff *s) {}
>> diff --git a/include/uapi/linux/bpf.h b/include/uapi/linux/bpf.h
>> index 3eee4467422d..02da170205de 100644
>> --- a/include/uapi/linux/bpf.h
>> +++ b/include/uapi/linux/bpf.h
>> @@ -7734,7 +7734,8 @@ struct bpf_insn_array_value {
>>  
>>  /* Flags to control bpf_dynptr_from_skb_ext() behavior. */
>>  enum {
>> -	BPF_SKB_EXT_F_CREATE = (1ULL << 0),
>> +	BPF_SKB_EXT_F_CREATE	= (1ULL << 0),
>> +	BPF_SKB_EXT_F_NO_SCRUB	= (1ULL << 1),
>
> Do I understand correctly that you do prefer the NO_SCRUB mode? Any reason
> we need to have scrub mode? If it's all produced/consumed by bpf, maybe
> we can just carry this data unconditionally instead of having a SCRUB/NO_SCRUB
> option?

Yes, that's correct. We definitely need NO_SCRUB but I don't have a use
case that relies on metadata scrubbing. I believe Kuniyuki also would
like the no-scrub to be the only/default behavior for Google's egress
use case.

I've made it an opt-out mostly because that the existing metadata
(skb->mark, skb->data_meta) gets scrubbed. Although as Jakub K has
pointed out to me - you can circumvent it by using bpf_redirect into the
target netns. So I guess we have a precendent?

I could use input from folks operating in K8S-like environments, if
no-scrub-only mode would be acceptable there? Daniel, John, any opinion?


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

* Re: [PATCH RFC net-next 3/6] bpf: Allow skb extensions to survive packet scrubbing
  2026-07-16 12:59     ` Jakub Sitnicki
@ 2026-07-16 15:06       ` Jason Xing
  0 siblings, 0 replies; 12+ messages in thread
From: Jason Xing @ 2026-07-16 15:06 UTC (permalink / raw)
  To: Jakub Sitnicki
  Cc: Stanislav Fomichev, Daniel Borkmann, John Fastabend, netdev, bpf,
	kernel-team, Jakub Kicinski, Kuniyuki Iwashima

On Thu, Jul 16, 2026 at 3:00 PM Jakub Sitnicki <jakub@cloudflare.com> wrote:
>
> On Thu, Jul 16, 2026 at 05:11 AM -07, Stanislav Fomichev wrote:
> > On 07/14, Jakub Sitnicki wrote:
> >> skb_scrub_packet() drops all skb extensions unconditionally via
> >> skb_ext_reset(). It runs on tunnel encap/decap (ip_tunnel_rcv,
> >> vxlan_rcv, etc.) and cross-netns forwarding (dev_forward_skb).
> >>
> >> This makes it impossible for a BPF program to pass metadata via
> >> bpf_skb_ext through a tunnel or across a netns boundary. The extension
> >> is always lost at the scrub point.
> >>
> >> Introduce skb_ext_scrub() which consults each active extension before
> >> discarding it. Extensions that request preservation are kept while the
> >> rest are torn down. When the extension slab is shared with clones, COW
> >> ensures isolation. Replace the skb_ext_reset() call in
> >> skb_scrub_packet() with skb_ext_scrub().
> >>
> >> Expose the opt-in mechanism to BPF via the BPF_SKB_EXT_F_NO_SCRUB flag
> >> for bpf_dynptr_from_skb_ext(). A program that sets this flag when
> >> creating the extension signals that its metadata should survive
> >> scrubbing.
> >>
> >> Signed-off-by: Jakub Sitnicki <jakub@cloudflare.com>
> >> ---
> >>  include/linux/bpf.h      |  1 +
> >>  include/linux/skbuff.h   |  2 ++
> >>  include/uapi/linux/bpf.h |  3 +-
> >>  net/core/filter.c        | 11 +++++--
> >>  net/core/skbuff.c        | 82 ++++++++++++++++++++++++++++++++++++++++++------
> >>  net/ipv4/udp.c           |  2 +-
> >>  6 files changed, 86 insertions(+), 15 deletions(-)
> >>
> >> diff --git a/include/linux/bpf.h b/include/linux/bpf.h
> >> index 6b918a5b61bf..a46ca53c5b27 100644
> >> --- a/include/linux/bpf.h
> >> +++ b/include/linux/bpf.h
> >> @@ -4214,6 +4214,7 @@ static inline int bpf_map_check_op_flags(struct bpf_map *map, u64 flags, u64 all
> >>  #ifdef CONFIG_BPF_SKB_EXT
> >>
> >>  struct bpf_skb_ext {
> >> +    u64 flags;
> >>      u8 buf[CONFIG_BPF_SKB_EXT_SIZE] __aligned(8);
> >>  };
> >>
> >> diff --git a/include/linux/skbuff.h b/include/linux/skbuff.h
> >> index 584d8440d352..66afa5489007 100644
> >> --- a/include/linux/skbuff.h
> >> +++ b/include/linux/skbuff.h
> >> @@ -5063,6 +5063,7 @@ void *__skb_ext_set(struct sk_buff *skb, enum skb_ext_id id,
> >>  void *skb_ext_add(struct sk_buff *skb, enum skb_ext_id id);
> >>  void __skb_ext_del(struct sk_buff *skb, enum skb_ext_id id);
> >>  void __skb_ext_put(struct skb_ext *ext);
> >> +void skb_ext_scrub(struct sk_buff *skb);
> >>
> >>  static inline void skb_ext_put(struct sk_buff *skb)
> >>  {
> >> @@ -5132,6 +5133,7 @@ static inline bool skb_has_extensions(struct sk_buff *skb)
> >>  static inline void __skb_ext_put(struct skb_ext *ext) {}
> >>  static inline void skb_ext_put(struct sk_buff *skb) {}
> >>  static inline void skb_ext_reset(struct sk_buff *skb) {}
> >> +static inline void skb_ext_scrub(struct sk_buff *skb) {}
> >>  static inline void skb_ext_del(struct sk_buff *skb, int unused) {}
> >>  static inline void __skb_ext_copy(struct sk_buff *d, const struct sk_buff *s) {}
> >>  static inline void skb_ext_copy(struct sk_buff *dst, const struct sk_buff *s) {}
> >> diff --git a/include/uapi/linux/bpf.h b/include/uapi/linux/bpf.h
> >> index 3eee4467422d..02da170205de 100644
> >> --- a/include/uapi/linux/bpf.h
> >> +++ b/include/uapi/linux/bpf.h
> >> @@ -7734,7 +7734,8 @@ struct bpf_insn_array_value {
> >>
> >>  /* Flags to control bpf_dynptr_from_skb_ext() behavior. */
> >>  enum {
> >> -    BPF_SKB_EXT_F_CREATE = (1ULL << 0),
> >> +    BPF_SKB_EXT_F_CREATE    = (1ULL << 0),
> >> +    BPF_SKB_EXT_F_NO_SCRUB  = (1ULL << 1),
> >
> > Do I understand correctly that you do prefer the NO_SCRUB mode? Any reason
> > we need to have scrub mode? If it's all produced/consumed by bpf, maybe
> > we can just carry this data unconditionally instead of having a SCRUB/NO_SCRUB
> > option?
>
> Yes, that's correct. We definitely need NO_SCRUB but I don't have a use
> case that relies on metadata scrubbing. I believe Kuniyuki also would
> like the no-scrub to be the only/default behavior for Google's egress
> use case.
>
> I've made it an opt-out mostly because that the existing metadata
> (skb->mark, skb->data_meta) gets scrubbed. Although as Jakub K has
> pointed out to me - you can circumvent it by using bpf_redirect into the
> target netns. So I guess we have a precendent?
>
> I could use input from folks operating in K8S-like environments, if

Sorry, I don't quite follow it here. Why don't we need the NO_SCRUB
here? The crucial point here is to allow skb traverse across different
containers, which is also what I expect. We do need to support this
case to make this feature standalone, which means it is independent
and not affected by other components.

And one part of what I'm doing for BPF timestamping v2 is to address
the issue in the container scenario as well, FYI.

Thanks,
Jason

> no-scrub-only mode would be acceptable there? Daniel, John, any opinion?
>
>

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

end of thread, other threads:[~2026-07-16 15:07 UTC | newest]

Thread overview: 12+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-14 17:48 [PATCH RFC net-next 0/6] skb extension for BPF metadata Jakub Sitnicki
2026-07-14 17:48 ` [PATCH RFC net-next 1/6] bpf: Introduce per-packet metadata storage for BPF programs Jakub Sitnicki
2026-07-16 12:10   ` Stanislav Fomichev
2026-07-16 12:35     ` Jakub Sitnicki
2026-07-14 17:48 ` [PATCH RFC net-next 2/6] bpf: Allow access to bpf_sock_ops_kern->skb Jakub Sitnicki
2026-07-14 17:48 ` [PATCH RFC net-next 3/6] bpf: Allow skb extensions to survive packet scrubbing Jakub Sitnicki
2026-07-16 12:11   ` Stanislav Fomichev
2026-07-16 12:59     ` Jakub Sitnicki
2026-07-16 15:06       ` Jason Xing
2026-07-14 17:48 ` [PATCH RFC net-next 4/6] selftests/bpf: Add tests for bpf_dynptr_from_skb_ext Jakub Sitnicki
2026-07-14 17:48 ` [PATCH RFC net-next 5/6] selftests/bpf: Test skb_ext scrubbing across tunnels and veths Jakub Sitnicki
2026-07-14 17:48 ` [PATCH RFC net-next 6/6] selftests/bpf: Use non-trivial test payload in xdp_context tests Jakub Sitnicki

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.