AI Trajectory


Trajectory: (Expand/Collapse All)
Seq Timestamp Type Name Duration
0/0 2026/08/14 08:38 flow patch-triage
0m Args:
null

Results:
{
  "EnableConfigs": [
    "BPF_SKB_EXT"
  ],
  "FocusSymbols": [
    "bpf_dynptr_from_skb_ext",
    "__bpf_skb_ext_load_bytes",
    "__bpf_skb_ext_store_bytes",
    "bpf_skb_ext_pointer",
    "skb_ext_scrub"
  ],
  "KMSANReasoning": "The patch introduces a new BPF skb extension (`struct bpf_skb_ext`) to store metadata. The extension memory is explicitly zero-initialized upon creation (`memset(ext, 0, sizeof(*ext))`) in `bpf_dynptr_from_skb_ext()`. BPF programs can read and write to this extension via dynptr helpers. Since the memory is fully initialized before being accessed by BPF programs, and the extension is not directly copied to user space, there is no risk of uninitialized memory reads or info-leaks. Any potential memory safety issues (like out-of-bounds accesses or use-after-free) would be caught by standard KASAN. Therefore, a dedicated KMSAN fuzzing session is not required.",
  "NeedsKMSAN": false,
  "Reasoning": "The patch introduces a new BPF feature allowing BPF programs to store and retrieve metadata in an `sk_buff` extension (`struct bpf_skb_ext`). This includes new Kfuncs (`bpf_dynptr_from_skb_ext`), a new dynptr type (`BPF_DYNPTR_TYPE_SKB_EXT`), and modifications to the core network stack to preserve this extension across certain network layers (e.g., `skb_ext_scrub`). This is a functional change in the core networking and BPF subsystems that is reachable from userspace via BPF programs, making it highly relevant for fuzzing.",
  "WorthFuzzing": true
}

1/1 2026/08/14 08:38 action read-patch-diff
0m Args:
null

Results:
{
  "PatchDiff": "commit 8f1ceadd3ec88f91a907c36c2180c18013845199\nAuthor: syz-cluster \u003ctriage@syzkaller.com\u003e\nDate:   Fri Aug 14 08:38:04 2026 +0000\n\n    syz-cluster: applied patch under review\n\ndiff --git a/include/linux/bpf.h b/include/linux/bpf.h\nindex 7719f65284456..6b918a5b61bfe 100644\n--- a/include/linux/bpf.h\n+++ b/include/linux/bpf.h\n@@ -1484,6 +1484,8 @@ enum bpf_dynptr_type {\n \tBPF_DYNPTR_TYPE_SKB_META,\n \t/* Underlying data is a file */\n \tBPF_DYNPTR_TYPE_FILE,\n+\t/* Underlying data is a bpf_skb_ext chunk */\n+\tBPF_DYNPTR_TYPE_SKB_EXT,\n };\n \n int bpf_dynptr_check_size(u64 size);\n@@ -4209,4 +4211,12 @@ static inline int bpf_map_check_op_flags(struct bpf_map *map, u64 flags, u64 all\n \treturn 0;\n }\n \n+#ifdef CONFIG_BPF_SKB_EXT\n+\n+struct bpf_skb_ext {\n+\tu8 buf[CONFIG_BPF_SKB_EXT_SIZE] __aligned(8);\n+};\n+\n+#endif /* CONFIG_BPF_SKB_EXT */\n+\n #endif /* _LINUX_BPF_H */\ndiff --git a/include/linux/filter.h b/include/linux/filter.h\nindex 14acb2455746f..6a2955487504d 100644\n--- a/include/linux/filter.h\n+++ b/include/linux/filter.h\n@@ -1914,4 +1914,30 @@ static inline void *bpf_skb_meta_pointer(struct sk_buff *skb, u32 offset)\n }\n #endif /* CONFIG_NET */\n \n+#ifdef CONFIG_BPF_SKB_EXT\n+void *bpf_skb_ext_pointer(struct sk_buff *skb, u32 offset);\n+int __bpf_skb_ext_load_bytes(const struct sk_buff *skb, u32 offset, void *to,\n+\t\t\t     u32 len);\n+int __bpf_skb_ext_store_bytes(struct sk_buff *skb, u32 offset, const void *from,\n+\t\t\t      u32 len, u64 flags);\n+#else /* CONFIG_BPF_SKB_EXT */\n+static inline void *bpf_skb_ext_pointer(struct sk_buff *skb, u32 offset)\n+{\n+\treturn NULL;\n+}\n+\n+static inline int __bpf_skb_ext_load_bytes(const struct sk_buff *skb,\n+\t\t\t\t\t   u32 offset, void *to, u32 len)\n+{\n+\treturn -EOPNOTSUPP;\n+}\n+\n+static inline int __bpf_skb_ext_store_bytes(struct sk_buff *skb, u32 offset,\n+\t\t\t\t\t    const void *from, u32 len,\n+\t\t\t\t\t    u64 flags)\n+{\n+\treturn -EOPNOTSUPP;\n+}\n+#endif /* CONFIG_BPF_SKB_EXT */\n+\n #endif /* __LINUX_FILTER_H__ */\ndiff --git a/include/linux/skbuff.h b/include/linux/skbuff.h\nindex 22eda1d54a0e8..286f2e4cdbb1a 100644\n--- a/include/linux/skbuff.h\n+++ b/include/linux/skbuff.h\n@@ -5031,6 +5031,9 @@ enum skb_ext_id {\n #endif\n #if IS_ENABLED(CONFIG_CAN)\n \tSKB_EXT_CAN,\n+#endif\n+#if IS_ENABLED(CONFIG_BPF_SKB_EXT)\n+\tSKB_EXT_BPF,\n #endif\n \tSKB_EXT_NUM, /* must be last */\n };\n@@ -5058,6 +5061,7 @@ void *__skb_ext_set(struct sk_buff *skb, enum skb_ext_id id,\n void *skb_ext_add(struct sk_buff *skb, enum skb_ext_id id);\n void __skb_ext_del(struct sk_buff *skb, enum skb_ext_id id);\n void __skb_ext_put(struct skb_ext *ext);\n+void skb_ext_scrub(struct sk_buff *skb);\n \n static inline void skb_ext_put(struct sk_buff *skb)\n {\n@@ -5127,6 +5131,7 @@ static inline bool skb_has_extensions(struct sk_buff *skb)\n static inline void __skb_ext_put(struct skb_ext *ext) {}\n static inline void skb_ext_put(struct sk_buff *skb) {}\n static inline void skb_ext_reset(struct sk_buff *skb) {}\n+static inline void skb_ext_scrub(struct sk_buff *skb) {}\n static inline void skb_ext_del(struct sk_buff *skb, int unused) {}\n static inline void __skb_ext_copy(struct sk_buff *d, const struct sk_buff *s) {}\n static inline void skb_ext_copy(struct sk_buff *dst, const struct sk_buff *s) {}\ndiff --git a/include/uapi/linux/bpf.h b/include/uapi/linux/bpf.h\nindex 89b36de5fdbb6..3eee4467422db 100644\n--- a/include/uapi/linux/bpf.h\n+++ b/include/uapi/linux/bpf.h\n@@ -7732,4 +7732,9 @@ struct bpf_insn_array_value {\n \t__u32 :32;\n };\n \n+/* Flags to control bpf_dynptr_from_skb_ext() behavior. */\n+enum {\n+\tBPF_SKB_EXT_F_CREATE = (1ULL \u003c\u003c 0),\n+};\n+\n #endif /* _UAPI__LINUX_BPF_H__ */\ndiff --git a/kernel/bpf/helpers.c b/kernel/bpf/helpers.c\nindex c18f1e16edee4..aa28cc6a75f76 100644\n--- a/kernel/bpf/helpers.c\n+++ b/kernel/bpf/helpers.c\n@@ -1926,6 +1926,8 @@ static int __bpf_dynptr_read(void *dst, u64 len, const struct bpf_dynptr_kern *s\n \t\treturn 0;\n \tcase BPF_DYNPTR_TYPE_FILE:\n \t\treturn bpf_file_fetch_bytes(src-\u003edata, offset, dst, len);\n+\tcase BPF_DYNPTR_TYPE_SKB_EXT:\n+\t\treturn __bpf_skb_ext_load_bytes(src-\u003edata, src-\u003eoffset + offset, dst, len);\n \tdefault:\n \t\tWARN_ONCE(true, \"bpf_dynptr_read: unknown dynptr type %d\\n\", type);\n \t\treturn -EFAULT;\n@@ -1985,6 +1987,8 @@ int __bpf_dynptr_write(const struct bpf_dynptr_kern *dst, u64 offset, void *src,\n \tcase BPF_DYNPTR_TYPE_SKB_META:\n \t\treturn __bpf_skb_meta_store_bytes(dst-\u003edata, dst-\u003eoffset + offset, src,\n \t\t\t\t\t\t  len, flags);\n+\tcase BPF_DYNPTR_TYPE_SKB_EXT:\n+\t\treturn __bpf_skb_ext_store_bytes(dst-\u003edata, dst-\u003eoffset + offset, src, len, flags);\n \tdefault:\n \t\tWARN_ONCE(true, \"bpf_dynptr_write: unknown dynptr type %d\\n\", type);\n \t\treturn -EFAULT;\n@@ -2032,6 +2036,7 @@ BPF_CALL_3(bpf_dynptr_data, const struct bpf_dynptr_kern *, ptr, u64, offset, u6\n \tcase BPF_DYNPTR_TYPE_SKB:\n \tcase BPF_DYNPTR_TYPE_XDP:\n \tcase BPF_DYNPTR_TYPE_SKB_META:\n+\tcase BPF_DYNPTR_TYPE_SKB_EXT:\n \t\t/* skb and xdp dynptrs should use bpf_dynptr_slice / bpf_dynptr_slice_rdwr */\n \t\treturn 0;\n \tdefault:\n@@ -3087,6 +3092,8 @@ __bpf_kfunc void *bpf_dynptr_slice(const struct bpf_dynptr *p, u64 offset,\n \t}\n \tcase BPF_DYNPTR_TYPE_SKB_META:\n \t\treturn bpf_skb_meta_pointer(ptr-\u003edata, ptr-\u003eoffset + offset);\n+\tcase BPF_DYNPTR_TYPE_SKB_EXT:\n+\t\treturn bpf_skb_ext_pointer(ptr-\u003edata, ptr-\u003eoffset + offset);\n \tcase BPF_DYNPTR_TYPE_FILE:\n \t\terr = bpf_file_fetch_bytes(ptr-\u003edata, offset, buffer__nullable, buffer__szk);\n \t\treturn err ? NULL : buffer__nullable;\ndiff --git a/kernel/bpf/log.c b/kernel/bpf/log.c\nindex b740fa73ee26d..d66e154649e0d 100644\n--- a/kernel/bpf/log.c\n+++ b/kernel/bpf/log.c\n@@ -462,6 +462,8 @@ const char *dynptr_type_str(enum bpf_dynptr_type type)\n \t\treturn \"skb_meta\";\n \tcase BPF_DYNPTR_TYPE_FILE:\n \t\treturn \"file\";\n+\tcase BPF_DYNPTR_TYPE_SKB_EXT:\n+\t\treturn \"skb_ext\";\n \tcase BPF_DYNPTR_TYPE_INVALID:\n \t\treturn \"\u003cinvalid\u003e\";\n \tdefault:\ndiff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c\nindex 7aa47342dc659..b0ddaccf61b30 100644\n--- a/kernel/bpf/verifier.c\n+++ b/kernel/bpf/verifier.c\n@@ -635,6 +635,7 @@ static enum bpf_type_flag get_dynptr_type_flag(enum bpf_dynptr_type type)\n \tcase BPF_DYNPTR_TYPE_XDP:\n \t\treturn DYNPTR_TYPE_XDP;\n \tcase BPF_DYNPTR_TYPE_SKB_META:\n+\tcase BPF_DYNPTR_TYPE_SKB_EXT:\n \t\treturn DYNPTR_TYPE_SKB_META;\n \tcase BPF_DYNPTR_TYPE_FILE:\n \t\treturn DYNPTR_TYPE_FILE;\n@@ -5683,6 +5684,10 @@ BTF_TYPE_SAFE_TRUSTED_OR_NULL(struct vm_area_struct) {\n \tstruct file *vm_file;\n };\n \n+BTF_TYPE_SAFE_TRUSTED_OR_NULL(struct bpf_sock_ops_kern) {\n+\tstruct sk_buff *skb;\n+};\n+\n static bool type_is_rcu(struct bpf_verifier_env *env,\n \t\t\tstruct bpf_reg_state *reg,\n \t\t\tconst char *field_name, u32 btf_id)\n@@ -5725,6 +5730,7 @@ static bool type_is_trusted_or_null(struct bpf_verifier_env *env,\n \tBTF_TYPE_EMIT(BTF_TYPE_SAFE_TRUSTED_OR_NULL(struct socket));\n \tBTF_TYPE_EMIT(BTF_TYPE_SAFE_TRUSTED_OR_NULL(struct dentry));\n \tBTF_TYPE_EMIT(BTF_TYPE_SAFE_TRUSTED_OR_NULL(struct vm_area_struct));\n+\tBTF_TYPE_EMIT(BTF_TYPE_SAFE_TRUSTED_OR_NULL(struct bpf_sock_ops_kern));\n \n \treturn btf_nested_type_is_trusted(\u0026env-\u003elog, reg, field_name, btf_id,\n \t\t\t\t\t  \"__safe_trusted_or_null\");\n@@ -11036,6 +11042,7 @@ enum special_kfunc_type {\n \tKF_bpf_dynptr_from_xdp,\n \tKF_bpf_dynptr_from_skb_meta,\n \tKF_bpf_xdp_pull_data,\n+\tKF_bpf_dynptr_from_skb_ext,\n \tKF_bpf_dynptr_slice,\n \tKF_bpf_dynptr_slice_rdwr,\n \tKF_bpf_dynptr_clone,\n@@ -11116,6 +11123,11 @@ BTF_ID_UNUSED\n BTF_ID_UNUSED\n BTF_ID_UNUSED\n #endif\n+#ifdef CONFIG_BPF_SKB_EXT\n+BTF_ID(func, bpf_dynptr_from_skb_ext)\n+#else\n+BTF_ID_UNUSED\n+#endif\n BTF_ID(func, bpf_dynptr_slice)\n BTF_ID(func, bpf_dynptr_slice_rdwr)\n BTF_ID(func, bpf_dynptr_clone)\n@@ -12219,7 +12231,8 @@ static int check_kfunc_args(struct bpf_verifier_env *env, struct bpf_kfunc_call_\n \t\t\t\tdynptr_arg_type |= DYNPTR_TYPE_SKB;\n \t\t\t} else if (meta-\u003efunc_id == special_kfunc_list[KF_bpf_dynptr_from_xdp]) {\n \t\t\t\tdynptr_arg_type |= DYNPTR_TYPE_XDP;\n-\t\t\t} else if (meta-\u003efunc_id == special_kfunc_list[KF_bpf_dynptr_from_skb_meta]) {\n+\t\t\t} else if (meta-\u003efunc_id == special_kfunc_list[KF_bpf_dynptr_from_skb_meta] ||\n+\t\t\t\t   meta-\u003efunc_id == special_kfunc_list[KF_bpf_dynptr_from_skb_ext]) {\n \t\t\t\tdynptr_arg_type |= DYNPTR_TYPE_SKB_META;\n \t\t\t} else if (meta-\u003efunc_id == special_kfunc_list[KF_bpf_dynptr_from_file]) {\n \t\t\t\tdynptr_arg_type |= DYNPTR_TYPE_FILE;\ndiff --git a/net/Kconfig b/net/Kconfig\nindex e384773935515..6d57320dfea39 100644\n--- a/net/Kconfig\n+++ b/net/Kconfig\n@@ -540,4 +540,24 @@ config NET_TEST\n \n \t  If unsure, say N.\n \n+config BPF_SKB_EXT\n+\tbool \"skb extension for BPF metadata\"\n+\tdepends on BPF_SYSCALL\n+\tselect SKB_EXTENSIONS\n+\thelp\n+\t  Enable an sk_buff extension for storing BPF metadata. This allows BPF\n+\t  programs to associate arbitrary data with individual packets as they\n+\t  traverse the network stack. The storage is automatically freed when\n+\t  the sk_buff is freed.\n+\n+config BPF_SKB_EXT_SIZE\n+\tint \"Size of the BPF skb extension metadata buffer\"\n+\tdepends on BPF_SKB_EXT\n+\trange 1 256\n+\tdefault 64\n+\thelp\n+\t  Configures the size of the inline metadata buffer in struct\n+\t  bpf_skb_ext, which is the maximum amount of data a BPF program can\n+\t  store or retrieve with bpf_dynptr_from_skb_ext().\n+\n endif   # if NET\ndiff --git a/net/core/filter.c b/net/core/filter.c\nindex 11bb0d236822a..a9a164738389c 100644\n--- a/net/core/filter.c\n+++ b/net/core/filter.c\n@@ -12259,6 +12259,48 @@ int __bpf_skb_meta_store_bytes(struct sk_buff *skb, u32 offset,\n \treturn 0;\n }\n \n+#ifdef CONFIG_BPF_SKB_EXT\n+void *bpf_skb_ext_pointer(struct sk_buff *skb, u32 offset)\n+{\n+\tstruct bpf_skb_ext *ext;\n+\n+\text = skb_ext_find(skb, SKB_EXT_BPF);\n+\tif (!ext)\n+\t\treturn NULL;\n+\n+\treturn ext-\u003ebuf + offset;\n+}\n+\n+int __bpf_skb_ext_load_bytes(const struct sk_buff *skb, u32 offset, void *to,\n+\t\t\t     u32 len)\n+{\n+\tstruct bpf_skb_ext *ext;\n+\n+\text = skb_ext_find(skb, SKB_EXT_BPF);\n+\tif (!ext)\n+\t\treturn -ENOENT;\n+\n+\tmemmove(to, ext-\u003ebuf + offset, len);\n+\treturn 0;\n+}\n+\n+int __bpf_skb_ext_store_bytes(struct sk_buff *skb, u32 offset,\n+\t\t\t      const void *from, u32 len, u64 flags)\n+{\n+\tstruct bpf_skb_ext *ext;\n+\n+\tif (unlikely(flags))\n+\t\treturn -EINVAL;\n+\n+\text = skb_ext_find(skb, SKB_EXT_BPF);\n+\tif (!ext)\n+\t\treturn -ENOENT;\n+\n+\tmemmove(ext-\u003ebuf + offset, from, len);\n+\treturn 0;\n+}\n+#endif /* CONFIG_BPF_SKB_EXT */\n+\n __bpf_kfunc_start_defs();\n __bpf_kfunc int bpf_dynptr_from_skb(struct __sk_buff *s, u64 flags,\n \t\t\t\t    struct bpf_dynptr *ptr__uninit)\n@@ -12276,6 +12318,71 @@ __bpf_kfunc int bpf_dynptr_from_skb(struct __sk_buff *s, u64 flags,\n \treturn 0;\n }\n \n+#ifdef CONFIG_BPF_SKB_EXT\n+/**\n+ * bpf_dynptr_from_skb_ext() - Initialize a dynptr to the skb_ext BPF area.\n+ * @skb_: socket buffer to attach the extension to\n+ * @size: dynptr size in bytes, 0 for maximum (CONFIG_BPF_SKB_EXT_SIZE)\n+ * @flags: BPF_SKB_EXT_F_CREATE to create/COW (read-write), 0 to find (read-only)\n+ * @ptr__uninit: dynptr to initialize\n+ *\n+ * Return:\n+ * * %0         - dynptr ready to use\n+ * * %-ENOENT   - extension not found (when not creating)\n+ * * %-ENOMEM   - allocation failed\n+ * * %-EINVAL   - invalid flags\n+ * * %-E2BIG    - size exceeds CONFIG_BPF_SKB_EXT_SIZE\n+ */\n+__bpf_kfunc int bpf_dynptr_from_skb_ext(struct __sk_buff *skb_, u32 size,\n+\t\t\t\t\tu64 flags,\n+\t\t\t\t\tstruct bpf_dynptr *ptr__uninit)\n+{\n+\tstruct bpf_dynptr_kern *ptr = (struct bpf_dynptr_kern *)ptr__uninit;\n+\tstruct sk_buff *skb = (struct sk_buff *)skb_;\n+\tbool create = flags \u0026 BPF_SKB_EXT_F_CREATE;\n+\tstruct bpf_skb_ext *ext;\n+\tbool exists;\n+\tint err;\n+\n+\tif (flags \u0026 ~BPF_SKB_EXT_F_CREATE) {\n+\t\terr = -EINVAL;\n+\t\tgoto error;\n+\t}\n+\n+\tif (size \u003e ARRAY_SIZE(ext-\u003ebuf)) {\n+\t\terr = -E2BIG;\n+\t\tgoto error;\n+\t}\n+\tif (!size)\n+\t\tsize = ARRAY_SIZE(ext-\u003ebuf);\n+\n+\texists = skb_ext_exist(skb, SKB_EXT_BPF);\n+\tif (!create) {\n+\t\tif (!exists) {\n+\t\t\terr = -ENOENT;\n+\t\t\tgoto error;\n+\t\t}\n+\t\tgoto out;\n+\t}\n+\n+\text = skb_ext_add(skb, SKB_EXT_BPF);\n+\tif (!ext) {\n+\t\terr = -ENOMEM;\n+\t\tgoto error;\n+\t}\n+\tif (!exists)\n+\t\tmemset(ext, 0, sizeof(*ext));\n+out:\n+\tbpf_dynptr_init(ptr, skb, BPF_DYNPTR_TYPE_SKB_EXT, 0, size);\n+\tif (!create)\n+\t\tbpf_dynptr_set_rdonly(ptr);\n+\treturn 0;\n+error:\n+\tbpf_dynptr_set_null(ptr);\n+\treturn err;\n+}\n+#endif /* CONFIG_BPF_SKB_EXT */\n+\n /**\n  * bpf_dynptr_from_skb_meta() - Initialize a dynptr to the skb metadata area.\n  * @skb_: socket buffer carrying the metadata\n@@ -12576,6 +12683,12 @@ BTF_KFUNCS_START(bpf_kfunc_check_set_skb_meta)\n BTF_ID_FLAGS(func, bpf_dynptr_from_skb_meta)\n BTF_KFUNCS_END(bpf_kfunc_check_set_skb_meta)\n \n+#ifdef CONFIG_BPF_SKB_EXT\n+BTF_KFUNCS_START(bpf_kfunc_check_set_skb_ext)\n+BTF_ID_FLAGS(func, bpf_dynptr_from_skb_ext)\n+BTF_KFUNCS_END(bpf_kfunc_check_set_skb_ext)\n+#endif\n+\n BTF_KFUNCS_START(bpf_kfunc_check_set_xdp)\n BTF_ID_FLAGS(func, bpf_dynptr_from_xdp)\n BTF_ID_FLAGS(func, bpf_xdp_pull_data)\n@@ -12603,6 +12716,13 @@ static const struct btf_kfunc_id_set bpf_kfunc_set_skb_meta = {\n \t.set = \u0026bpf_kfunc_check_set_skb_meta,\n };\n \n+#ifdef CONFIG_BPF_SKB_EXT\n+static const struct btf_kfunc_id_set bpf_kfunc_set_skb_ext = {\n+\t.owner = THIS_MODULE,\n+\t.set = \u0026bpf_kfunc_check_set_skb_ext,\n+};\n+#endif\n+\n static const struct btf_kfunc_id_set bpf_kfunc_set_xdp = {\n \t.owner = THIS_MODULE,\n \t.set = \u0026bpf_kfunc_check_set_xdp,\n@@ -12640,6 +12760,21 @@ static int __init bpf_kfunc_init(void)\n \tret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_TRACING, \u0026bpf_kfunc_set_skb);\n \tret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_SCHED_CLS, \u0026bpf_kfunc_set_skb_meta);\n \tret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_SCHED_ACT, \u0026bpf_kfunc_set_skb_meta);\n+#ifdef CONFIG_BPF_SKB_EXT\n+\tret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_SCHED_CLS, \u0026bpf_kfunc_set_skb_ext);\n+\tret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_SCHED_ACT, \u0026bpf_kfunc_set_skb_ext);\n+\tret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_CGROUP_SKB, \u0026bpf_kfunc_set_skb_ext);\n+\tret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_SOCK_OPS, \u0026bpf_kfunc_set_skb_ext);\n+\tret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_SK_SKB, \u0026bpf_kfunc_set_skb_ext);\n+\tret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_SOCKET_FILTER, \u0026bpf_kfunc_set_skb_ext);\n+\tret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_LWT_OUT, \u0026bpf_kfunc_set_skb_ext);\n+\tret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_LWT_IN, \u0026bpf_kfunc_set_skb_ext);\n+\tret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_LWT_XMIT, \u0026bpf_kfunc_set_skb_ext);\n+\tret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_LWT_SEG6LOCAL, \u0026bpf_kfunc_set_skb_ext);\n+\tret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_NETFILTER, \u0026bpf_kfunc_set_skb_ext);\n+\tret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_LSM, \u0026bpf_kfunc_set_skb_ext);\n+\tret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_TRACING, \u0026bpf_kfunc_set_skb_ext);\n+#endif\n \tret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_XDP, \u0026bpf_kfunc_set_xdp);\n \tret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_CGROUP_SOCK_ADDR,\n \t\t\t\t\t       \u0026bpf_kfunc_set_sock_addr);\ndiff --git a/net/core/skbuff.c b/net/core/skbuff.c\nindex c82a1472a5ea8..700bee045b5c5 100644\n--- a/net/core/skbuff.c\n+++ b/net/core/skbuff.c\n@@ -83,6 +83,7 @@\n #include \u003cnet/page_pool/helpers.h\u003e\n #include \u003cnet/psp/types.h\u003e\n #include \u003cnet/dropreason.h\u003e\n+#include \u003clinux/bpf.h\u003e\n #include \u003cnet/xdp_sock.h\u003e\n \n #include \u003clinux/uaccess.h\u003e\n@@ -5155,6 +5156,9 @@ static const u8 skb_ext_type_len[] = {\n #if IS_ENABLED(CONFIG_CAN)\n \t[SKB_EXT_CAN] = SKB_EXT_CHUNKSIZEOF(struct can_skb_ext),\n #endif\n+#if IS_ENABLED(CONFIG_BPF_SKB_EXT)\n+\t[SKB_EXT_BPF] = SKB_EXT_CHUNKSIZEOF(struct bpf_skb_ext),\n+#endif\n };\n \n static __always_inline __no_profile unsigned int skb_ext_total_length(void)\n@@ -6281,7 +6285,7 @@ void skb_scrub_packet(struct sk_buff *skb, bool xnet)\n \tskb-\u003eskb_iif = 0;\n \tskb-\u003eignore_df = 0;\n \tskb_dst_drop(skb);\n-\tskb_ext_reset(skb);\n+\tskb_ext_scrub(skb);\n \tnf_reset_ct(skb);\n \tnf_reset_trace(skb);\n \n@@ -7255,6 +7259,20 @@ void __skb_ext_del(struct sk_buff *skb, enum skb_ext_id id)\n }\n EXPORT_SYMBOL(__skb_ext_del);\n \n+static void skb_ext_put_each(struct skb_ext *ext, unsigned int skip)\n+{\n+#ifdef CONFIG_XFRM\n+\tif (!(skip \u0026 (1 \u003c\u003c SKB_EXT_SEC_PATH)) \u0026\u0026\n+\t    __skb_ext_exist(ext, SKB_EXT_SEC_PATH))\n+\t\tskb_ext_put_sp(skb_ext_get_ptr(ext, SKB_EXT_SEC_PATH));\n+#endif\n+#ifdef CONFIG_MCTP_FLOWS\n+\tif (!(skip \u0026 (1 \u003c\u003c SKB_EXT_MCTP)) \u0026\u0026\n+\t    __skb_ext_exist(ext, SKB_EXT_MCTP))\n+\t\tskb_ext_put_mctp(skb_ext_get_ptr(ext, SKB_EXT_MCTP));\n+#endif\n+}\n+\n void __skb_ext_put(struct skb_ext *ext)\n {\n \t/* If this is last clone, nothing can increment\n@@ -7266,18 +7284,61 @@ void __skb_ext_put(struct skb_ext *ext)\n \tif (!refcount_dec_and_test(\u0026ext-\u003erefcnt))\n \t\treturn;\n free_now:\n-#ifdef CONFIG_XFRM\n-\tif (__skb_ext_exist(ext, SKB_EXT_SEC_PATH))\n-\t\tskb_ext_put_sp(skb_ext_get_ptr(ext, SKB_EXT_SEC_PATH));\n-#endif\n-#ifdef CONFIG_MCTP_FLOWS\n-\tif (__skb_ext_exist(ext, SKB_EXT_MCTP))\n-\t\tskb_ext_put_mctp(skb_ext_get_ptr(ext, SKB_EXT_MCTP));\n-#endif\n-\n+\tskb_ext_put_each(ext, 0);\n \tkmem_cache_free(skbuff_ext_cache, ext);\n }\n EXPORT_SYMBOL(__skb_ext_put);\n+\n+static unsigned int skb_ext_no_scrub(struct skb_ext *ext)\n+{\n+\tunsigned int keep = 0;\n+\n+#if IS_ENABLED(CONFIG_BPF_SKB_EXT)\n+\tif (__skb_ext_exist(ext, SKB_EXT_BPF))\n+\t\tkeep |= (1 \u003c\u003c SKB_EXT_BPF);\n+#endif\n+\treturn keep;\n+}\n+\n+static int __skb_ext_scrub(struct sk_buff *skb, unsigned int keep)\n+{\n+\tstruct skb_ext *old = skb-\u003eextensions;\n+\tstruct skb_ext *ext;\n+\tint i;\n+\n+\tif (refcount_read(\u0026old-\u003erefcnt) == 1) {\n+\t\tskb_ext_put_each(old, keep);\n+\t\text = old;\n+\t} else {\n+\t\text = skb_ext_maybe_cow(old, keep);\n+\t\tif (!ext)\n+\t\t\treturn -ENOMEM;\n+\t\tskb-\u003eextensions = ext;\n+\t}\n+\n+\tfor (i = 0; i \u003c SKB_EXT_NUM; i++) {\n+\t\tif (!(keep \u0026 (1 \u003c\u003c i)))\n+\t\t\text-\u003eoffset[i] = 0;\n+\t}\n+\tskb-\u003eactive_extensions = keep;\n+\treturn 0;\n+}\n+\n+void skb_ext_scrub(struct sk_buff *skb)\n+{\n+\tunsigned int keep;\n+\n+\tif (likely(!skb-\u003eactive_extensions))\n+\t\treturn;\n+\n+\tkeep = skb_ext_no_scrub(skb-\u003eextensions);\n+\tif (keep \u0026\u0026 !__skb_ext_scrub(skb, keep))\n+\t\treturn;\n+\n+\tskb_ext_reset(skb);\n+}\n+EXPORT_SYMBOL(skb_ext_scrub);\n+\n #endif /* CONFIG_SKB_EXTENSIONS */\n \n static void kfree_skb_napi_cache(struct sk_buff *skb)\ndiff --git a/net/ipv4/udp.c b/net/ipv4/udp.c\nindex af96032174441..865655db21857 100644\n--- a/net/ipv4/udp.c\n+++ b/net/ipv4/udp.c\n@@ -1530,7 +1530,10 @@ void udp_splice_eof(struct socket *sock)\n  * We need to preserve secpath, if present, to eventually process\n  * IP_CMSG_PASSSEC at recvmsg() time.\n  *\n- * Other extensions can be cleared.\n+ * Other extensions get scrubbed. If any extension survives scrubbing\n+ * (e.g. SKB_EXT_BPF), the skb must stay stateful, so that\n+ * skb_consume_udp() releases it via skb_release_head_state() instead of\n+ * leaking the extension slab on the __consume_stateless_skb() path.\n  */\n static bool udp_try_make_stateless(struct sk_buff *skb)\n {\n@@ -1538,8 +1541,8 @@ static bool udp_try_make_stateless(struct sk_buff *skb)\n \t\treturn true;\n \n \tif (!secpath_exists(skb)) {\n-\t\tskb_ext_reset(skb);\n-\t\treturn true;\n+\t\tskb_ext_scrub(skb);\n+\t\treturn !skb_has_extensions(skb);\n \t}\n \n \treturn false;\ndiff --git a/tools/testing/selftests/bpf/config b/tools/testing/selftests/bpf/config\nindex ea7044f30adc3..502f4504bdba9 100644\n--- a/tools/testing/selftests/bpf/config\n+++ b/tools/testing/selftests/bpf/config\n@@ -7,6 +7,7 @@ CONFIG_BPF_JIT=y\n CONFIG_BPF_KPROBE_OVERRIDE=y\n CONFIG_BPF_LIRC_MODE2=y\n CONFIG_BPF_LSM=y\n+CONFIG_BPF_SKB_EXT=y\n CONFIG_BPF_STREAM_PARSER=y\n CONFIG_BPF_SYSCALL=y\n # CONFIG_BPF_UNPRIV_DEFAULT_OFF is not set\ndiff --git a/tools/testing/selftests/bpf/prog_tests/socket_helpers.h b/tools/testing/selftests/bpf/prog_tests/socket_helpers.h\nindex 0d59503a0c73e..eb114a6ea6f92 100644\n--- a/tools/testing/selftests/bpf/prog_tests/socket_helpers.h\n+++ b/tools/testing/selftests/bpf/prog_tests/socket_helpers.h\n@@ -3,6 +3,7 @@\n #ifndef __SOCKET_HELPERS__\n #define __SOCKET_HELPERS__\n \n+#include \u003cerror.h\u003e\n #include \u003csys/un.h\u003e\n #include \u003clinux/vm_sockets.h\u003e\n \ndiff --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\nindex 448807676176c..8bf586b5af32f 100644\n--- a/tools/testing/selftests/bpf/prog_tests/xdp_context_test_run.c\n+++ b/tools/testing/selftests/bpf/prog_tests/xdp_context_test_run.c\n@@ -2,7 +2,9 @@\n #include \u003ctest_progs.h\u003e\n #include \u003cnetwork_helpers.h\u003e\n #include \u003clinux/ipv6.h\u003e\n+#include \u003clinux/netfilter.h\u003e\n #include \u003carpa/inet.h\u003e\n+#include \"socket_helpers.h\"\n #include \"test_xdp_context_test_run.skel.h\"\n #include \"test_xdp_meta.skel.h\"\n \n@@ -19,10 +21,10 @@\n \n #define TEST_PAYLOAD_LEN 32\n static const __u8 test_payload[TEST_PAYLOAD_LEN] = {\n-\t0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,\n-\t0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,\n-\t0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28,\n-\t0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38,\n+\t0x49, 0x20, 0x63, 0x72, 0x6f, 0x73, 0x73, 0x65,\n+\t0x64, 0x20, 0x68, 0x6f, 0x6f, 0x6b, 0x73, 0x20,\n+\t0x26, 0x20, 0x6c, 0x69, 0x76, 0x65, 0x64, 0x20,\n+\t0x74, 0x6f, 0x20, 0x74, 0x65, 0x6c, 0x6c, 0x21,\n };\n \n void test_xdp_context_error(int prog_fd, struct bpf_test_run_opts opts,\n@@ -693,3 +695,981 @@ void test_xdp_context_lwt_encap(void)\n \n \ttest_xdp_meta__destroy(skel);\n }\n+\n+/* Test if skb_ext survives skb clone (via tc mirred).\n+ * dummy_prog runs on the clone (dummy ingress).\n+ */\n+static void test_mirred_clone_ext(struct test_xdp_meta *skel,\n+\t\t\t\t  struct bpf_program *dummy_prog)\n+{\n+\tLIBBPF_OPTS(bpf_tc_hook, tc_hook, .attach_point = BPF_TC_INGRESS);\n+\tLIBBPF_OPTS(bpf_tc_opts, tc_opts, .handle = 1, .priority = 1);\n+\tstruct netns_obj *ns = NULL;\n+\tint dummy_ifindex;\n+\tint tap_ifindex;\n+\tint tap_fd = -1;\n+\tint ret;\n+\n+\tskel-\u003ebss-\u003etest_pass = false;\n+\n+\tns = netns_new(\"mirred_clone\", true);\n+\tif (!ASSERT_OK_PTR(ns, \"netns_new\"))\n+\t\treturn;\n+\n+\t/* Dummy dev: attach reader */\n+\tSYS(close, \"ip link add name \" DUMMY_NAME \" type dummy\");\n+\tSYS(close, \"ip link set dev \" DUMMY_NAME \" up\");\n+\n+\tdummy_ifindex = if_nametoindex(DUMMY_NAME);\n+\tif (!ASSERT_GE(dummy_ifindex, 0, \"dummy_ifindex\"))\n+\t\tgoto close;\n+\n+\ttc_hook.ifindex = dummy_ifindex;\n+\tret = bpf_tc_hook_create(\u0026tc_hook);\n+\tif (!ASSERT_OK(ret, \"dummy_hook_create\"))\n+\t\tgoto close;\n+\n+\ttc_opts.prog_fd = bpf_program__fd(dummy_prog);\n+\tret = bpf_tc_attach(\u0026tc_hook, \u0026tc_opts);\n+\tif (!ASSERT_OK(ret, \"dummy_attach\"))\n+\t\tgoto close;\n+\n+\t/* TAP dev: attach writer + mirred to dummy */\n+\ttap_fd = open_tuntap(TAP_NAME, true);\n+\tif (!ASSERT_GE(tap_fd, 0, \"open_tuntap\"))\n+\t\tgoto close;\n+\n+\tSYS(close, \"ip link set dev \" TAP_NAME \" up\");\n+\n+\ttap_ifindex = if_nametoindex(TAP_NAME);\n+\tif (!ASSERT_GE(tap_ifindex, 0, \"tap_ifindex\"))\n+\t\tgoto close;\n+\n+\ttc_hook.ifindex = tap_ifindex;\n+\tret = bpf_tc_hook_create(\u0026tc_hook);\n+\tif (!ASSERT_OK(ret, \"tap_hook_create\"))\n+\t\tgoto close;\n+\n+\ttc_opts.prog_id = 0;\n+\ttc_opts.prog_fd = bpf_program__fd(skel-\u003eprogs.tc_skb_ext_write);\n+\tret = bpf_tc_attach(\u0026tc_hook, \u0026tc_opts);\n+\tif (!ASSERT_OK(ret, \"tap_attach\"))\n+\t\tgoto close;\n+\n+\tSYS(close, \"tc filter add dev \" TAP_NAME \" ingress \"\n+\t\t   \"protocol all matchall \"\n+\t\t   \"action mirred ingress mirror dev \" DUMMY_NAME);\n+\n+\tret = write_test_packet(tap_fd);\n+\tif (!ASSERT_OK(ret, \"write_test_packet\"))\n+\t\tgoto close;\n+\n+\tASSERT_TRUE(skel-\u003ebss-\u003etest_pass, \"test_pass\");\n+\n+close:\n+\tif (tap_fd \u003e= 0)\n+\t\tclose(tap_fd);\n+\tnetns_free(ns);\n+}\n+\n+static void test_mirred_clone_ext_cow(struct test_xdp_meta *skel)\n+{\n+\tstruct bpf_link *tp_link;\n+\n+\tskel-\u003ebss-\u003eclone_cow_done = false;\n+\ttp_link = bpf_program__attach(skel-\u003eprogs.tp_kfree_skb_cow_check);\n+\tif (!ASSERT_OK_PTR(tp_link, \"attach_tp\"))\n+\t\treturn;\n+\n+\ttest_mirred_clone_ext(skel, skel-\u003eprogs.tc_skb_ext_clone_redir_cow);\n+\tbpf_link__destroy(tp_link);\n+}\n+\n+/* Test if skb_ext survives veth cross-netns forward */\n+static void test_skb_ext_scrub_veth(struct test_xdp_meta *skel)\n+{\n+\tLIBBPF_OPTS(bpf_tc_hook, tx_hook, .attach_point = BPF_TC_EGRESS);\n+\tLIBBPF_OPTS(bpf_tc_opts, tx_opts, .handle = 1, .priority = 1);\n+\tLIBBPF_OPTS(bpf_tc_hook, rx_hook, .attach_point = BPF_TC_INGRESS);\n+\tLIBBPF_OPTS(bpf_tc_opts, rx_opts, .handle = 1, .priority = 1);\n+\tstruct netns_obj *rx_ns = NULL, *tx_ns = NULL;\n+\tstruct nstoken *nstoken = NULL;\n+\tint rx_ifindex, tx_ifindex;\n+\tint ret;\n+\n+\ttx_ns = netns_new(TX_NETNS, false);\n+\tif (!ASSERT_OK_PTR(tx_ns, \"create tx_ns\"))\n+\t\treturn;\n+\n+\trx_ns = netns_new(RX_NETNS, false);\n+\tif (!ASSERT_OK_PTR(rx_ns, \"create rx_ns\"))\n+\t\tgoto close;\n+\n+\tSYS(close, \"ip link add \" RX_NAME \" netns \" RX_NETNS\n+\t    \" type veth peer name \" TX_NAME \" netns \" TX_NETNS);\n+\n+\t/* Setup RX side: TC ingress reader */\n+\tnstoken = open_netns(RX_NETNS);\n+\tif (!ASSERT_OK_PTR(nstoken, \"setns rx_ns\"))\n+\t\tgoto close;\n+\n+\tSYS(close, \"ip link set dev \" RX_NAME \" up\");\n+\n+\trx_ifindex = if_nametoindex(RX_NAME);\n+\tif (!ASSERT_GE(rx_ifindex, 0, \"if_nametoindex rx\"))\n+\t\tgoto close;\n+\n+\trx_hook.ifindex = rx_ifindex;\n+\tret = bpf_tc_hook_create(\u0026rx_hook);\n+\tif (!ASSERT_OK(ret, \"bpf_tc_hook_create rx\"))\n+\t\tgoto close;\n+\n+\trx_opts.prog_fd = bpf_program__fd(skel-\u003eprogs.tc_skb_ext_read);\n+\tret = bpf_tc_attach(\u0026rx_hook, \u0026rx_opts);\n+\tif (!ASSERT_OK(ret, \"bpf_tc_attach rx\"))\n+\t\tgoto close;\n+\n+\tclose_netns(nstoken);\n+\n+\t/* Setup TX side: TC egress writer */\n+\tnstoken = open_netns(TX_NETNS);\n+\tif (!ASSERT_OK_PTR(nstoken, \"setns tx_ns\"))\n+\t\tgoto close;\n+\n+\tSYS(close, \"ip link set dev \" TX_NAME \" up\");\n+\n+\ttx_ifindex = if_nametoindex(TX_NAME);\n+\tif (!ASSERT_GE(tx_ifindex, 0, \"if_nametoindex tx\"))\n+\t\tgoto close;\n+\n+\ttx_hook.ifindex = tx_ifindex;\n+\tret = bpf_tc_hook_create(\u0026tx_hook);\n+\tif (!ASSERT_OK(ret, \"bpf_tc_hook_create tx\"))\n+\t\tgoto close;\n+\n+\ttx_opts.prog_fd = bpf_program__fd(skel-\u003eprogs.tc_skb_ext_write);\n+\tret = bpf_tc_attach(\u0026tx_hook, \u0026tx_opts);\n+\tif (!ASSERT_OK(ret, \"bpf_tc_attach tx\"))\n+\t\tgoto close;\n+\n+\tskel-\u003ebss-\u003etest_pass = false;\n+\n+\tret = send_test_packet(tx_ifindex);\n+\tif (!ASSERT_OK(ret, \"send_test_packet\"))\n+\t\tgoto close;\n+\n+\tASSERT_TRUE(skel-\u003ebss-\u003etest_pass, \"test_pass\");\n+\n+close:\n+\tclose_netns(nstoken);\n+\tnetns_free(rx_ns);\n+\tnetns_free(tx_ns);\n+}\n+\n+/* Test if skb_ext survives GRE tunnel encap+decap */\n+static void test_skb_ext_scrub_gre(struct test_xdp_meta *skel)\n+{\n+\tLIBBPF_OPTS(bpf_tc_hook, tx_hook, .attach_point = BPF_TC_EGRESS);\n+\tLIBBPF_OPTS(bpf_tc_opts, tx_opts, .handle = 1, .priority = 1);\n+\tLIBBPF_OPTS(bpf_tc_hook, rx_hook, .attach_point = BPF_TC_INGRESS);\n+\tLIBBPF_OPTS(bpf_tc_opts, rx_opts, .handle = 1, .priority = 1);\n+\tstruct netns_obj *ns = NULL;\n+\tint tx_ifindex;\n+\tint rx_ifindex;\n+\tint ret;\n+\n+\tskel-\u003ebss-\u003etest_pass = false;\n+\n+\tns = netns_new(\"gre_test\", true);\n+\tif (!ASSERT_OK_PTR(ns, \"netns_new\"))\n+\t\treturn;\n+\n+\t/* Setup: gre_tx -\u003e lo -\u003e gre_rx */\n+\tSYS(close, \"ip link set lo up\");\n+\tSYS(close, \"ip link add gre_tx type gretap\"\n+\t    \" local 127.0.0.1 remote 127.0.0.2\");\n+\tSYS(close, \"ip link set gre_tx up\");\n+\tSYS(close, \"ip addr add 127.0.0.2/8 dev lo\");\n+\tSYS(close, \"ip link add gre_rx type gretap\"\n+\t    \" local 127.0.0.2 remote 127.0.0.1\");\n+\tSYS(close, \"ip link set gre_rx up\");\n+\n+\t/* Write skb_ext on TC egress on GRE tx */\n+\ttx_ifindex = if_nametoindex(\"gre_tx\");\n+\tif (!ASSERT_GE(tx_ifindex, 0, \"tx_ifindex\"))\n+\t\tgoto close;\n+\n+\ttx_hook.ifindex = tx_ifindex;\n+\tret = bpf_tc_hook_create(\u0026tx_hook);\n+\tif (!ASSERT_OK(ret, \"tx_hook_create\"))\n+\t\tgoto close;\n+\n+\ttx_opts.prog_fd = bpf_program__fd(skel-\u003eprogs.tc_skb_ext_write);\n+\tret = bpf_tc_attach(\u0026tx_hook, \u0026tx_opts);\n+\tif (!ASSERT_OK(ret, \"tx_attach\"))\n+\t\tgoto close;\n+\n+\t/* Read skb_ext on TC ingress on GRE rx */\n+\trx_ifindex = if_nametoindex(\"gre_rx\");\n+\tif (!ASSERT_GE(rx_ifindex, 0, \"rx_ifindex\"))\n+\t\tgoto close;\n+\n+\trx_hook.ifindex = rx_ifindex;\n+\tret = bpf_tc_hook_create(\u0026rx_hook);\n+\tif (!ASSERT_OK(ret, \"rx_hook_create\"))\n+\t\tgoto close;\n+\n+\trx_opts.prog_fd = bpf_program__fd(skel-\u003eprogs.tc_skb_ext_read);\n+\tret = bpf_tc_attach(\u0026rx_hook, \u0026rx_opts);\n+\tif (!ASSERT_OK(ret, \"rx_attach\"))\n+\t\tgoto close;\n+\n+\t/* Then use send_test_packet on GRE tx */\n+\tret = send_test_packet(tx_ifindex);\n+\tif (!ASSERT_OK(ret, \"send_test_packet\"))\n+\t\tgoto close;\n+\n+\tASSERT_TRUE(skel-\u003ebss-\u003etest_pass, \"test_pass\");\n+\n+close:\n+\tnetns_free(ns);\n+}\n+\n+void test_skb_ext_basic(void)\n+{\n+\tstruct test_xdp_meta *skel = NULL;\n+\n+\tskel = test_xdp_meta__open_and_load();\n+\tif (!ASSERT_OK_PTR(skel, \"open and load skeleton\"))\n+\t\treturn;\n+\n+\tif (test__start_subtest(\"tc_write_read\"))\n+\t\ttest_tuntap(NULL, /* xdp */\n+\t\t\t    skel-\u003eprogs.tc_skb_ext_write,\n+\t\t\t    skel-\u003eprogs.tc_skb_ext_read,\n+\t\t\t    \u0026skel-\u003ebss-\u003etest_pass);\n+\tif (test__start_subtest(\"tc_write_clone_read\"))\n+\t\ttest_tuntap(NULL, /* xdp */\n+\t\t\t    skel-\u003eprogs.tc_skb_ext_write,\n+\t\t\t    skel-\u003eprogs.tc_skb_ext_clone_read,\n+\t\t\t    \u0026skel-\u003ebss-\u003etest_pass);\n+\tif (test__start_subtest(\"tc_write_slice_read\"))\n+\t\ttest_tuntap(NULL, /* xdp */\n+\t\t\t    skel-\u003eprogs.tc_skb_ext_write,\n+\t\t\t    skel-\u003eprogs.tc_skb_ext_slice_read,\n+\t\t\t    \u0026skel-\u003ebss-\u003etest_pass);\n+\tif (test__start_subtest(\"tc_slice_write_read\"))\n+\t\ttest_tuntap(NULL, /* xdp */\n+\t\t\t    skel-\u003eprogs.tc_skb_ext_slice_write,\n+\t\t\t    skel-\u003eprogs.tc_skb_ext_read,\n+\t\t\t    \u0026skel-\u003ebss-\u003etest_pass);\n+\tif (test__start_subtest(\"tc_no_alloc\"))\n+\t\ttest_tuntap(NULL, /* xdp */\n+\t\t\t    skel-\u003eprogs.tc_skb_ext_no_alloc,\n+\t\t\t    NULL, /* tc prio 2 */\n+\t\t\t    \u0026skel-\u003ebss-\u003etest_pass);\n+\tif (test__start_subtest(\"tc_invalid_flags\"))\n+\t\ttest_tuntap(NULL, /* xdp */\n+\t\t\t    skel-\u003eprogs.tc_skb_ext_invalid_flags,\n+\t\t\t    NULL, /* tc prio 2 */\n+\t\t\t    \u0026skel-\u003ebss-\u003etest_pass);\n+\tif (test__start_subtest(\"tc_rdonly\"))\n+\t\ttest_tuntap(NULL, /* xdp */\n+\t\t\t    skel-\u003eprogs.tc_skb_ext_rdonly,\n+\t\t\t    NULL, /* tc prio 2 */\n+\t\t\t    \u0026skel-\u003ebss-\u003etest_pass);\n+\tif (test__start_subtest(\"tc_double_alloc\"))\n+\t\ttest_tuntap(NULL, /* xdp */\n+\t\t\t    skel-\u003eprogs.tc_skb_ext_double_alloc,\n+\t\t\t    NULL, /* tc prio 2 */\n+\t\t\t    \u0026skel-\u003ebss-\u003etest_pass);\n+\tif (test__start_subtest(\"clone_ext_read\"))\n+\t\ttest_mirred_clone_ext(skel, skel-\u003eprogs.tc_skb_ext_read);\n+\tif (test__start_subtest(\"clone_ext_cow\"))\n+\t\ttest_mirred_clone_ext_cow(skel);\n+\tif (test__start_subtest(\"survives_veth\"))\n+\t\ttest_skb_ext_scrub_veth(skel);\n+\tif (test__start_subtest(\"survives_gre\"))\n+\t\ttest_skb_ext_scrub_gre(skel);\n+\n+\ttest_xdp_meta__destroy(skel);\n+}\n+\n+/* Send test_payload over loopback UDP to recv_fd */\n+static int send_loopback_udp(int recv_fd)\n+{\n+\tstruct sockaddr_in addr = {\n+\t\t.sin_family = AF_INET,\n+\t\t.sin_addr.s_addr = htonl(INADDR_LOOPBACK),\n+\t};\n+\tchar buf[TEST_PAYLOAD_LEN];\n+\tint ret = -1;\n+\tint fd = -1;\n+\t__be16 port;\n+\n+\tport = get_socket_local_port(recv_fd);\n+\tif (!ASSERT_GE(port, 0, \"get_port\"))\n+\t\tgoto out;\n+\n+\tfd = socket(AF_INET, SOCK_DGRAM, 0);\n+\tif (!ASSERT_GE(fd, 0, \"socket\"))\n+\t\tgoto out;\n+\n+\taddr.sin_port = port;\n+\tsendto(fd, test_payload, TEST_PAYLOAD_LEN, 0,\n+\t       (void *)\u0026addr, sizeof(addr));\n+\trecvfrom(recv_fd, buf, sizeof(buf), 0, NULL, NULL);\n+\tret = 0;\n+out:\n+\tif (fd \u003e= 0)\n+\t\tclose(fd);\n+\treturn ret;\n+}\n+\n+enum udp_reader_type {\n+\tREADER_CGRP_SKB,\n+\tREADER_SK_FILTER,\n+};\n+\n+/* Test skb_ext survival across TC ingress -\u003e UDP reader hook */\n+static void test_skb_ext_udp(struct test_xdp_meta *skel, const char *name,\n+\t\t\t     enum udp_reader_type reader)\n+{\n+\tLIBBPF_OPTS(bpf_tc_hook, tc_hook,\n+\t\t    .ifindex = 1 /* IFINDEX_LO */,\n+\t\t    .attach_point = BPF_TC_INGRESS);\n+\tLIBBPF_OPTS(bpf_tc_opts, tc_opts, .handle = 1, .priority = 1);\n+\tstruct bpf_link *reader_link = NULL;\n+\tstruct netns_obj *ns = NULL;\n+\tint server_fd = -1;\n+\tint cgroup_fd = -1;\n+\tint filter_fd;\n+\tint ret;\n+\n+\tns = netns_new(name, true);\n+\tif (!ASSERT_OK_PTR(ns, \"netns_new\"))\n+\t\treturn;\n+\n+\tcgroup_fd = test__join_cgroup(name);\n+\tif (!ASSERT_GE(cgroup_fd, 0, \"join_cgroup\"))\n+\t\tgoto cleanup;\n+\n+\tserver_fd = start_server(AF_INET, SOCK_DGRAM, \"127.0.0.1\", 0, 0);\n+\tif (!ASSERT_GE(server_fd, 0, \"start_server\"))\n+\t\tgoto cleanup;\n+\n+\tskel-\u003ebss-\u003etest_pass = false;\n+\n+\tret = bpf_tc_hook_create(\u0026tc_hook);\n+\tif (!ASSERT_OK(ret, \"bpf_tc_hook_create\"))\n+\t\tgoto cleanup;\n+\n+\ttc_opts.prog_fd = bpf_program__fd(skel-\u003eprogs.tc_skb_ext_write);\n+\tret = bpf_tc_attach(\u0026tc_hook, \u0026tc_opts);\n+\tif (!ASSERT_OK(ret, \"bpf_tc_attach\"))\n+\t\tgoto cleanup;\n+\n+\tswitch (reader) {\n+\tcase READER_CGRP_SKB:\n+\t\treader_link = bpf_program__attach_cgroup(skel-\u003eprogs.cgrp_skb_ext_read,\n+\t\t\t\t\t\t\t cgroup_fd);\n+\t\tif (!ASSERT_OK_PTR(reader_link, \"attach_cgroup\"))\n+\t\t\tgoto cleanup;\n+\t\tbreak;\n+\tcase READER_SK_FILTER:\n+\t\tfilter_fd = bpf_program__fd(skel-\u003eprogs.sk_filter_skb_ext_read);\n+\t\tret = setsockopt(server_fd, SOL_SOCKET, SO_ATTACH_BPF,\n+\t\t\t\t \u0026filter_fd, sizeof(filter_fd));\n+\t\tif (!ASSERT_OK(ret, \"attach_socket_filter\"))\n+\t\t\tgoto cleanup;\n+\t\tbreak;\n+\t}\n+\n+\tif (send_loopback_udp(server_fd))\n+\t\tgoto cleanup;\n+\n+\tASSERT_TRUE(skel-\u003ebss-\u003etest_pass, \"test_pass\");\n+\n+cleanup:\n+\tbpf_link__destroy(reader_link);\n+\tbpf_tc_hook_destroy(\u0026tc_hook);\n+\tif (server_fd \u003e= 0)\n+\t\tclose(server_fd);\n+\tif (cgroup_fd \u003e= 0)\n+\t\tclose(cgroup_fd);\n+\tnetns_free(ns);\n+}\n+\n+enum tcp_reader_type {\n+\tREADER_SKOPS,\n+\tREADER_LSM,\n+};\n+\n+/* Test skb_ext survival across TC ingress -\u003e TCP reader hook */\n+static void test_skb_ext_tcp(struct test_xdp_meta *skel, const char *name,\n+\t\t\t     enum tcp_reader_type reader)\n+{\n+\tLIBBPF_OPTS(bpf_tc_hook, tc_hook,\n+\t\t    .ifindex = 1 /* IFINDEX_LO */,\n+\t\t    .attach_point = BPF_TC_INGRESS);\n+\tLIBBPF_OPTS(bpf_tc_opts, tc_opts, .handle = 1, .priority = 1);\n+\tstruct bpf_link *reader_link = NULL;\n+\tstruct netns_obj *ns = NULL;\n+\tint server_fd = -1;\n+\tint cgroup_fd = -1;\n+\tint client_fd = -1;\n+\tint conn_fd = -1;\n+\t__be16 port;\n+\tint ret;\n+\n+\tns = netns_new(name, true);\n+\tif (!ASSERT_OK_PTR(ns, \"netns_new\"))\n+\t\treturn;\n+\n+\tcgroup_fd = test__join_cgroup(name);\n+\tif (!ASSERT_GE(cgroup_fd, 0, \"join_cgroup\"))\n+\t\tgoto cleanup;\n+\n+\tserver_fd = start_server(AF_INET, SOCK_STREAM, \"127.0.0.1\", 0, 0);\n+\tif (!ASSERT_GE(server_fd, 0, \"start_server\"))\n+\t\tgoto cleanup;\n+\n+\tport = get_socket_local_port(server_fd);\n+\tif (!ASSERT_GE(port, 0, \"get_port\"))\n+\t\tgoto cleanup;\n+\n+\tskel-\u003ebss-\u003etarget_port = port;\n+\tskel-\u003ebss-\u003etest_pass = false;\n+\n+\tret = bpf_tc_hook_create(\u0026tc_hook);\n+\tif (!ASSERT_OK(ret, \"bpf_tc_hook_create\"))\n+\t\tgoto cleanup;\n+\n+\ttc_opts.prog_fd = bpf_program__fd(skel-\u003eprogs.tc_skb_ext_write_port);\n+\tret = bpf_tc_attach(\u0026tc_hook, \u0026tc_opts);\n+\tif (!ASSERT_OK(ret, \"bpf_tc_attach\"))\n+\t\tgoto cleanup;\n+\n+\tswitch (reader) {\n+\tcase READER_SKOPS:\n+\t\treader_link = bpf_program__attach_cgroup(skel-\u003eprogs.skops_skb_ext_read,\n+\t\t\t\t\t\t\t cgroup_fd);\n+\t\tif (!ASSERT_OK_PTR(reader_link, \"attach_skops\"))\n+\t\t\tgoto cleanup;\n+\t\tbreak;\n+\tcase READER_LSM:\n+\t\treader_link = bpf_program__attach_lsm(skel-\u003eprogs.lsm_skb_ext_read);\n+\t\tif (!ASSERT_OK_PTR(reader_link, \"attach_lsm\"))\n+\t\t\tgoto cleanup;\n+\t\tbreak;\n+\t}\n+\n+\tclient_fd = connect_to_fd(server_fd, 0);\n+\tif (!ASSERT_GE(client_fd, 0, \"connect\"))\n+\t\tgoto cleanup;\n+\n+\tconn_fd = accept(server_fd, NULL, NULL);\n+\tif (!ASSERT_GE(conn_fd, 0, \"accept\"))\n+\t\tgoto cleanup;\n+\n+\tASSERT_TRUE(skel-\u003ebss-\u003etest_pass, \"test_pass\");\n+\n+cleanup:\n+\tif (conn_fd \u003e= 0)\n+\t\tclose(conn_fd);\n+\tif (client_fd \u003e= 0)\n+\t\tclose(client_fd);\n+\tbpf_link__destroy(reader_link);\n+\tbpf_tc_hook_destroy(\u0026tc_hook);\n+\tif (server_fd \u003e= 0)\n+\t\tclose(server_fd);\n+\tif (cgroup_fd \u003e= 0)\n+\t\tclose(cgroup_fd);\n+\tnetns_free(ns);\n+}\n+\n+/* Test skb_ext survival until skb free: cgroup/skb egress -\u003e kfree_skb.\n+ * Send UDP to a closed port. The packet is dropped, triggering kfree_skb.\n+ */\n+static void test_cgrp_egress_to_kfree_skb(struct test_xdp_meta *skel)\n+{\n+\tstruct sockaddr_in addr = {\n+\t\t.sin_family = AF_INET,\n+\t\t.sin_port = htons(4321),\n+\t\t.sin_addr.s_addr = htonl(INADDR_LOOPBACK),\n+\t};\n+\tstruct bpf_link *cg_link = NULL;\n+\tstruct bpf_link *tp_link = NULL;\n+\tstruct netns_obj *ns = NULL;\n+\tint cgroup_fd = -1;\n+\tchar buf[1];\n+\tint fd = -1;\n+\tint ret;\n+\n+\tcgroup_fd = test__join_cgroup(\"/cgrp_to_kfree\");\n+\tif (!ASSERT_GE(cgroup_fd, 0, \"join_cgroup\"))\n+\t\treturn;\n+\n+\tns = netns_new(\"cgrp_to_kfree\", true);\n+\tif (!ASSERT_OK_PTR(ns, \"netns_new\"))\n+\t\tgoto cleanup;\n+\n+\tskel-\u003ebss-\u003etest_pass = false;\n+\n+\tcg_link = bpf_program__attach_cgroup(skel-\u003eprogs.cgrp_skb_ext_write,\n+\t\t\t\t\t     cgroup_fd);\n+\tif (!ASSERT_OK_PTR(cg_link, \"attach_cgroup\"))\n+\t\tgoto cleanup;\n+\n+\ttp_link = bpf_program__attach_trace(skel-\u003eprogs.tp_kfree_skb_ext_read);\n+\tif (!ASSERT_OK_PTR(tp_link, \"attach_tp\"))\n+\t\tgoto cleanup;\n+\n+\tfd = socket(AF_INET, SOCK_DGRAM, 0);\n+\tif (!ASSERT_GE(fd, 0, \"socket\"))\n+\t\tgoto cleanup;\n+\n+\tret = connect(fd, (void *)\u0026addr, sizeof(addr));\n+\tif (!ASSERT_OK(ret, \"connect\"))\n+\t\tgoto cleanup;\n+\n+\tsend(fd, test_payload, TEST_PAYLOAD_LEN, 0);\n+\n+\t/* Wait for ICMP error -- confirms the packet was freed */\n+\tret = recv(fd, buf, sizeof(buf), 0);\n+\tASSERT_EQ(errno, ECONNREFUSED, \"recv_econnrefused\");\n+\n+\tASSERT_TRUE(skel-\u003ebss-\u003etest_pass, \"test_pass\");\n+\n+cleanup:\n+\tif (fd \u003e= 0)\n+\t\tclose(fd);\n+\tbpf_link__destroy(tp_link);\n+\tbpf_link__destroy(cg_link);\n+\tnetns_free(ns);\n+\tif (cgroup_fd \u003e= 0)\n+\t\tclose(cgroup_fd);\n+}\n+\n+/* Test skb_ext survival across TC ingress -\u003e netfilter hook */\n+static void test_skb_ext_nf(struct test_xdp_meta *skel, const char *name)\n+{\n+\tLIBBPF_OPTS(bpf_tc_hook, tc_hook,\n+\t\t    .ifindex = 1 /* IFINDEX_LO */,\n+\t\t    .attach_point = BPF_TC_INGRESS);\n+\tLIBBPF_OPTS(bpf_tc_opts, tc_opts, .handle = 1, .priority = 1);\n+\tLIBBPF_OPTS(bpf_netfilter_opts, nf_opts,\n+\t\t    .pf = NFPROTO_IPV4,\n+\t\t    .hooknum = NF_INET_LOCAL_IN,\n+\t\t    .priority = 1);\n+\tstruct bpf_link *reader_link = NULL;\n+\tstruct netns_obj *ns = NULL;\n+\tint server_fd = -1;\n+\tint ret;\n+\n+\tns = netns_new(name, true);\n+\tif (!ASSERT_OK_PTR(ns, \"netns_new\"))\n+\t\treturn;\n+\n+\tserver_fd = start_server(AF_INET, SOCK_DGRAM, \"127.0.0.1\", 0, 0);\n+\tif (!ASSERT_GE(server_fd, 0, \"start_server\"))\n+\t\tgoto cleanup;\n+\n+\tskel-\u003ebss-\u003etest_pass = false;\n+\n+\tret = bpf_tc_hook_create(\u0026tc_hook);\n+\tif (!ASSERT_OK(ret, \"bpf_tc_hook_create\"))\n+\t\tgoto cleanup;\n+\n+\ttc_opts.prog_fd = bpf_program__fd(skel-\u003eprogs.tc_skb_ext_write);\n+\tret = bpf_tc_attach(\u0026tc_hook, \u0026tc_opts);\n+\tif (!ASSERT_OK(ret, \"bpf_tc_attach\"))\n+\t\tgoto cleanup;\n+\n+\treader_link = bpf_program__attach_netfilter(skel-\u003eprogs.nf_skb_ext_read,\n+\t\t\t\t\t\t    \u0026nf_opts);\n+\tif (!ASSERT_OK_PTR(reader_link, \"attach_nf\"))\n+\t\tgoto cleanup;\n+\n+\tif (send_loopback_udp(server_fd))\n+\t\tgoto cleanup;\n+\n+\tASSERT_TRUE(skel-\u003ebss-\u003etest_pass, \"test_pass\");\n+\n+cleanup:\n+\tbpf_link__destroy(reader_link);\n+\tbpf_tc_hook_destroy(\u0026tc_hook);\n+\tif (server_fd \u003e= 0)\n+\t\tclose(server_fd);\n+\tnetns_free(ns);\n+}\n+\n+#define LWT_EXT_PIN_PATH \"/sys/fs/bpf/skb_ext_lwt\"\n+\n+/* Test skb_ext across LWT hooks on loopback.\n+ *\n+ * @lwt_prog:  BPF program to pin and attach via ip route encap\n+ * @encap_dir: \"in\", \"out\", or \"xmit\"\n+ * @writer:    true if lwt_prog writes skb_ext (reader on TC ingress),\n+ *             false if lwt_prog reads skb_ext (writer on TC ingress)\n+ */\n+static void test_skb_ext_lwt(struct test_xdp_meta *skel, const char *name,\n+\t\t\t     struct bpf_program *lwt_prog,\n+\t\t\t     const char *encap_dir, bool writer)\n+{\n+\tLIBBPF_OPTS(bpf_tc_hook, tc_hook,\n+\t\t    .ifindex = 1 /* IFINDEX_LO */,\n+\t\t    .attach_point = BPF_TC_INGRESS);\n+\tLIBBPF_OPTS(bpf_tc_opts, tc_opts, .handle = 1, .priority = 1);\n+\tstruct sockaddr_in addr = {\n+\t\t.sin_family = AF_INET,\n+\t};\n+\tstruct bpf_program *tc_prog;\n+\tchar buf[TEST_PAYLOAD_LEN];\n+\tstruct netns_obj *ns = NULL;\n+\tbool pinned = false;\n+\tint server_fd = -1;\n+\tint fd = -1;\n+\t__be16 port;\n+\tint ret;\n+\n+\tunlink(LWT_EXT_PIN_PATH);\n+\tret = bpf_program__pin(lwt_prog, LWT_EXT_PIN_PATH);\n+\tif (!ASSERT_OK(ret, \"pin lwt\"))\n+\t\treturn;\n+\tpinned = true;\n+\n+\tns = netns_new(name, true);\n+\tif (!ASSERT_OK_PTR(ns, \"netns_new\"))\n+\t\tgoto cleanup;\n+\n+\tSYS(cleanup, \"ip addr add 10.0.0.1/32 dev lo\");\n+\tSYS(cleanup, \"ip route replace table local local 10.0.0.1 \"\n+\t\t     \"encap bpf %s pinned \" LWT_EXT_PIN_PATH \" dev lo\",\n+\t\t     encap_dir);\n+\n+\tserver_fd = start_server(AF_INET, SOCK_DGRAM, \"10.0.0.1\", 0, 0);\n+\tif (!ASSERT_GE(server_fd, 0, \"start_server\"))\n+\t\tgoto cleanup;\n+\n+\tskel-\u003ebss-\u003etest_pass = false;\n+\n+\tret = bpf_tc_hook_create(\u0026tc_hook);\n+\tif (!ASSERT_OK(ret, \"bpf_tc_hook_create\"))\n+\t\tgoto cleanup;\n+\n+\t/* When LWT writes, TC ingress reads; when LWT reads, TC ingress writes */\n+\ttc_prog = writer ? skel-\u003eprogs.tc_skb_ext_read\n+\t\t\t : skel-\u003eprogs.tc_skb_ext_write;\n+\ttc_opts.prog_fd = bpf_program__fd(tc_prog);\n+\tret = bpf_tc_attach(\u0026tc_hook, \u0026tc_opts);\n+\tif (!ASSERT_OK(ret, \"bpf_tc_attach\"))\n+\t\tgoto cleanup;\n+\n+\tport = get_socket_local_port(server_fd);\n+\tif (!ASSERT_GE(port, 0, \"get_port\"))\n+\t\tgoto cleanup;\n+\n+\tfd = socket(AF_INET, SOCK_DGRAM, 0);\n+\tif (!ASSERT_GE(fd, 0, \"socket\"))\n+\t\tgoto cleanup;\n+\n+\tinet_pton(AF_INET, \"10.0.0.1\", \u0026addr.sin_addr);\n+\taddr.sin_port = port;\n+\tsendto(fd, test_payload, TEST_PAYLOAD_LEN, 0,\n+\t       (void *)\u0026addr, sizeof(addr));\n+\trecvfrom(server_fd, buf, sizeof(buf), 0, NULL, NULL);\n+\n+\tASSERT_TRUE(skel-\u003ebss-\u003etest_pass, \"test_pass\");\n+\n+cleanup:\n+\tif (fd \u003e= 0)\n+\t\tclose(fd);\n+\tbpf_tc_hook_destroy(\u0026tc_hook);\n+\tif (server_fd \u003e= 0)\n+\t\tclose(server_fd);\n+\tnetns_free(ns);\n+\tif (pinned)\n+\t\tunlink(LWT_EXT_PIN_PATH);\n+}\n+\n+#define SEG6_PIN_PATH \"/sys/fs/bpf/skb_ext_seg6local\"\n+\n+/*\n+ * Test skb_ext survival across TC egress -\u003e seg6local End.BPF hook.\n+ *\n+ * Topology:\n+ *\n+ *     NS1           NS2            NS3\n+ *   lo veth1 \u003c-\u003e veth2 veth3 \u003c-\u003e veth4 lo\n+ *\n+ * NS1 encaps fb01::2 with SRH (segments fd01::1,fd01::2) on output and\n+ * writes skb_ext at TC egress on veth1. NS2 runs End.BPF for fd01::1,\n+ * which reads skb_ext, then forwards to NS3 (seg6_lookup_nexthop\n+ * rejects loopback routes with local_delivery=false, so End.BPF\n+ * cannot deliver locally). NS3 has fd01::2 local, decapsulates, and\n+ * delivers the inner UDP (dst=fb01::2) to a local socket for test\n+ * synchronization.\n+ *\n+ * The TC program drops non-test packets, so NDP is bypassed with a\n+ * static neighbor entry on the NS1-\u003eNS2 hop.\n+ */\n+static void test_skb_ext_seg6local(struct test_xdp_meta *skel)\n+{\n+\tLIBBPF_OPTS(bpf_tc_hook, tc_hook, .attach_point = BPF_TC_EGRESS);\n+\tLIBBPF_OPTS(bpf_tc_opts, tc_opts, .handle = 1, .priority = 1);\n+\tstruct nstoken *nstoken = NULL;\n+\tstruct sockaddr_in6 dst = {};\n+\tchar buf[TEST_PAYLOAD_LEN];\n+\tbool pinned = false;\n+\tint client_fd = -1;\n+\tint server_fd = -1;\n+\tssize_t bytes;\n+\tint ret;\n+\n+\tunlink(SEG6_PIN_PATH);\n+\tret = bpf_program__pin(skel-\u003eprogs.seg6local_skb_ext_read,\n+\t\t\t       SEG6_PIN_PATH);\n+\tif (!ASSERT_OK(ret, \"pin seg6local\"))\n+\t\treturn;\n+\tpinned = true;\n+\n+\tskel-\u003ebss-\u003etest_pass = false;\n+\n+\tSYS(cleanup, \"ip netns add seg6_1\");\n+\tSYS(cleanup, \"ip netns add seg6_2\");\n+\tSYS(cleanup, \"ip netns add seg6_3\");\n+\n+\t/* NS1 \u003c-\u003e NS2 veth pair; static NDP since TC drops NS/NA */\n+\tSYS(cleanup, \"ip -n seg6_1 link add veth1 type veth peer name veth2 netns seg6_2\");\n+\tSYS(cleanup, \"ip -n seg6_1 link set dev veth1 up\");\n+\tSYS(cleanup, \"ip -n seg6_2 link set dev veth2 address 02:00:00:00:00:02\");\n+\tSYS(cleanup, \"ip -n seg6_2 link set dev veth2 up\");\n+\tSYS(cleanup, \"ip -n seg6_1 -6 addr add fb00::12/16 dev veth1 scope link nodad\");\n+\tSYS(cleanup, \"ip -n seg6_2 -6 addr add fb00::21/16 dev veth2 scope link nodad\");\n+\tSYS(cleanup, \"ip -n seg6_1 -6 neigh add fb00::21 lladdr 02:00:00:00:00:02 \"\n+\t\t     \"nud permanent dev veth1\");\n+\n+\t/* NS2 \u003c-\u003e NS3 veth pair */\n+\tSYS(cleanup, \"ip -n seg6_2 link add veth3 type veth peer name veth4 netns seg6_3\");\n+\tSYS(cleanup, \"ip -n seg6_2 link set dev veth3 up\");\n+\tSYS(cleanup, \"ip -n seg6_3 link set dev veth4 up\");\n+\tSYS(cleanup, \"ip -n seg6_2 -6 addr add fb00::34/16 dev veth3 scope link nodad\");\n+\tSYS(cleanup, \"ip -n seg6_3 -6 addr add fb00::43/16 dev veth4 scope link nodad\");\n+\n+\t/* NS1: source address + SRH encap on output + route for fd01::/16 */\n+\tSYS(cleanup, \"ip -n seg6_1 link set dev lo up\");\n+\tSYS(cleanup, \"ip -n seg6_1 -6 addr add fb01::1/128 dev lo nodad\");\n+\tSYS(cleanup, \"ip -n seg6_1 -6 route add fb01::2 encap seg6 mode encap \"\n+\t\t     \"segs fd01::1,fd01::2 dev veth1 via fb00::21\");\n+\tSYS(cleanup, \"ip -n seg6_1 -6 route add fd01::/16 dev veth1 via fb00::21\");\n+\n+\t/* NS2: seg6local End.BPF for fd01::1, then forward to NS3.\n+\t * Use open_netns so the pinned prog on bpffs is accessible.\n+\t */\n+\tnstoken = open_netns(\"seg6_2\");\n+\tif (!ASSERT_OK_PTR(nstoken, \"open seg6_2\"))\n+\t\tgoto cleanup;\n+\n+\tSYS(cleanup, \"ip link set dev lo up\");\n+\tSYS(cleanup, \"sysctl -wq net.ipv6.conf.all.forwarding=1\");\n+\tSYS(cleanup, \"ip -6 route add fd01::2/128 dev veth3 via fb00::43\");\n+\tSYS(cleanup, \"ip -6 route add fd01::1 encap seg6local \"\n+\t\t     \"action End.BPF endpoint pinned \" SEG6_PIN_PATH \" dev veth2\");\n+\n+\tclose_netns(nstoken);\n+\tnstoken = NULL;\n+\n+\t/* NS3: decap point + UDP server.\n+\t * fd01::2 local triggers SRH decap (segments_left=0).\n+\t * fb01::2 local delivers the inner UDP to the socket.\n+\t */\n+\tnstoken = open_netns(\"seg6_3\");\n+\tif (!ASSERT_OK_PTR(nstoken, \"open seg6_3\"))\n+\t\tgoto cleanup;\n+\n+\tSYS(cleanup, \"ip link set dev lo up\");\n+\tSYS(cleanup, \"sysctl -wq net.ipv6.conf.all.seg6_enabled=1\");\n+\tSYS(cleanup, \"sysctl -wq net.ipv6.conf.lo.seg6_enabled=1\");\n+\tSYS(cleanup, \"sysctl -wq net.ipv6.conf.veth4.seg6_enabled=1\");\n+\tSYS(cleanup, \"ip -6 addr add fd01::2/128 dev lo nodad\");\n+\tSYS(cleanup, \"ip -6 addr add fb01::2/128 dev lo nodad\");\n+\n+\tserver_fd = start_server(AF_INET6, SOCK_DGRAM, \"fb01::2\", 1234, 0);\n+\tif (!ASSERT_GE(server_fd, 0, \"start_server\"))\n+\t\tgoto cleanup;\n+\n+\tclose_netns(nstoken);\n+\tnstoken = NULL;\n+\n+\t/* NS1: write skb_ext at TC egress on veth1, then send.\n+\t * TC egress fires after seg6 encap, on the encapped packet.\n+\t */\n+\tnstoken = open_netns(\"seg6_1\");\n+\tif (!ASSERT_OK_PTR(nstoken, \"open seg6_1\"))\n+\t\tgoto cleanup;\n+\n+\ttc_hook.ifindex = if_nametoindex(\"veth1\");\n+\tif (!ASSERT_GE(tc_hook.ifindex, 0, \"ifindex veth1\"))\n+\t\tgoto cleanup;\n+\n+\tret = bpf_tc_hook_create(\u0026tc_hook);\n+\tif (!ASSERT_OK(ret, \"bpf_tc_hook_create\"))\n+\t\tgoto cleanup;\n+\n+\ttc_opts.prog_fd = bpf_program__fd(skel-\u003eprogs.tc_skb_ext_write);\n+\tret = bpf_tc_attach(\u0026tc_hook, \u0026tc_opts);\n+\tif (!ASSERT_OK(ret, \"bpf_tc_attach\"))\n+\t\tgoto cleanup;\n+\n+\tclient_fd = socket(AF_INET6, SOCK_DGRAM, 0);\n+\tif (!ASSERT_GE(client_fd, 0, \"socket\"))\n+\t\tgoto cleanup;\n+\n+\tdst.sin6_family = AF_INET6;\n+\tdst.sin6_port = htons(1234);\n+\tinet_pton(AF_INET6, \"fb01::2\", \u0026dst.sin6_addr);\n+\n+\tbytes = sendto(client_fd, test_payload, TEST_PAYLOAD_LEN, 0,\n+\t\t       (void *)\u0026dst, sizeof(dst));\n+\n+\tclose_netns(nstoken);\n+\tnstoken = NULL;\n+\n+\tif (!ASSERT_EQ(bytes, TEST_PAYLOAD_LEN, \"sendto\"))\n+\t\tgoto cleanup;\n+\n+\t/* Receive decapsulated packet -- synchronizes with seg6local */\n+\tnstoken = open_netns(\"seg6_3\");\n+\tif (!ASSERT_OK_PTR(nstoken, \"open seg6_3\"))\n+\t\tgoto cleanup;\n+\n+\tbytes = recvfrom(server_fd, buf, sizeof(buf), 0, NULL, NULL);\n+\tASSERT_EQ(bytes, TEST_PAYLOAD_LEN, \"recvfrom\");\n+\tASSERT_TRUE(skel-\u003ebss-\u003etest_pass, \"test_pass\");\n+\n+cleanup:\n+\tclose_netns(nstoken);\n+\tif (server_fd \u003e= 0)\n+\t\tclose(server_fd);\n+\tif (client_fd \u003e= 0)\n+\t\tclose(client_fd);\n+\tSYS_NOFAIL(\"ip netns del seg6_3\");\n+\tSYS_NOFAIL(\"ip netns del seg6_2\");\n+\tSYS_NOFAIL(\"ip netns del seg6_1\");\n+\tif (pinned)\n+\t\tunlink(SEG6_PIN_PATH);\n+}\n+\n+/* Test skb_ext survival across TC ingress -\u003e sk_skb verdict hook.\n+ * TC ingress writes skb_ext on loopback; the verdict program reads it\n+ * on skb delivery to a socket in the sockmap.\n+ */\n+static void test_skb_ext_sk_skb(struct test_xdp_meta *skel)\n+{\n+\tLIBBPF_OPTS(bpf_tc_hook, tc_hook,\n+\t\t    .ifindex = 1 /* IFINDEX_LO */,\n+\t\t    .attach_point = BPF_TC_INGRESS);\n+\tLIBBPF_OPTS(bpf_tc_opts, tc_opts, .handle = 1, .priority = 1);\n+\tstruct bpf_link *verdict_link = NULL;\n+\tstruct sockaddr_in addr;\n+\tsocklen_t addr_len = sizeof(addr);\n+\tstruct netns_obj *ns = NULL;\n+\tchar buf[TEST_PAYLOAD_LEN];\n+\tint c1 = -1, p1 = -1;\n+\tint map = -1, zero = 0;\n+\tssize_t n;\n+\tint ret;\n+\n+\tns = netns_new(\"tc_to_sk_skb\", true);\n+\tif (!ASSERT_OK_PTR(ns, \"netns_new\"))\n+\t\treturn;\n+\n+\tskel-\u003ebss-\u003etest_pass = false;\n+\n+\tret = bpf_tc_hook_create(\u0026tc_hook);\n+\tif (!ASSERT_OK(ret, \"bpf_tc_hook_create\"))\n+\t\tgoto cleanup;\n+\n+\ttc_opts.prog_fd = bpf_program__fd(skel-\u003eprogs.tc_skb_ext_write_port);\n+\tret = bpf_tc_attach(\u0026tc_hook, \u0026tc_opts);\n+\tif (!ASSERT_OK(ret, \"bpf_tc_attach\"))\n+\t\tgoto cleanup;\n+\n+\tmap = bpf_map_create(BPF_MAP_TYPE_SOCKMAP, NULL, sizeof(int),\n+\t\t\t     sizeof(int), 1, NULL);\n+\tif (!ASSERT_GE(map, 0, \"bpf_map_create\"))\n+\t\tgoto cleanup;\n+\n+\tverdict_link = bpf_program__attach_sockmap(skel-\u003eprogs.sk_skb_skb_ext_read,\n+\t\t\t\t\t\t   map);\n+\tif (!ASSERT_OK_PTR(verdict_link, \"attach_sockmap\"))\n+\t\tgoto cleanup;\n+\n+\tif (!ASSERT_OK(create_pair(AF_INET, SOCK_STREAM, \u0026c1, \u0026p1), \"create_pair\"))\n+\t\tgoto cleanup;\n+\n+\tif (!ASSERT_OK(bpf_map_update_elem(map, \u0026zero, \u0026c1, BPF_NOEXIST), \"map_update\"))\n+\t\tgoto cleanup;\n+\n+\tif (!ASSERT_OK(getsockname(c1, (struct sockaddr *)\u0026addr, \u0026addr_len),\n+\t\t       \"getsockname\"))\n+\t\tgoto cleanup;\n+\tskel-\u003ebss-\u003etarget_port = addr.sin_port;\n+\n+\tn = send(p1, test_payload, TEST_PAYLOAD_LEN, 0);\n+\tif (!ASSERT_EQ(n, TEST_PAYLOAD_LEN, \"send\"))\n+\t\tgoto cleanup;\n+\n+\tn = recv(c1, buf, sizeof(buf), 0);\n+\tASSERT_EQ(n, TEST_PAYLOAD_LEN, \"recv\");\n+\tASSERT_TRUE(skel-\u003ebss-\u003etest_pass, \"test_pass\");\n+\n+cleanup:\n+\tif (c1 \u003e= 0)\n+\t\tclose(c1);\n+\tif (p1 \u003e= 0)\n+\t\tclose(p1);\n+\tbpf_link__destroy(verdict_link);\n+\tif (map \u003e= 0)\n+\t\tclose(map);\n+\tbpf_tc_hook_destroy(\u0026tc_hook);\n+\tnetns_free(ns);\n+}\n+\n+void test_skb_ext_cross_hook(void)\n+{\n+\tstruct test_xdp_meta *skel = NULL;\n+\n+\tskel = test_xdp_meta__open_and_load();\n+\tif (!ASSERT_OK_PTR(skel, \"open and load skeleton\"))\n+\t\treturn;\n+\n+\tif (test__start_subtest(\"tc_to_cgrp_ingress\"))\n+\t\ttest_skb_ext_udp(skel, \"tc_to_cgrp_ingress\", READER_CGRP_SKB);\n+\tif (test__start_subtest(\"tc_to_sk_filter\"))\n+\t\ttest_skb_ext_udp(skel, \"tc_to_sk_filter\", READER_SK_FILTER);\n+\tif (test__start_subtest(\"tc_to_lsm\"))\n+\t\ttest_skb_ext_tcp(skel, \"tc_to_lsm\", READER_LSM);\n+\tif (test__start_subtest(\"tc_to_skops\"))\n+\t\ttest_skb_ext_tcp(skel, \"tc_to_skops\", READER_SKOPS);\n+\tif (test__start_subtest(\"cgrp_egress_to_kfree_skb\"))\n+\t\ttest_cgrp_egress_to_kfree_skb(skel);\n+\tif (test__start_subtest(\"tc_to_nf\"))\n+\t\ttest_skb_ext_nf(skel, \"tc_to_nf\");\n+\tif (test__start_subtest(\"tc_to_lwt_in\"))\n+\t\ttest_skb_ext_lwt(skel, \"tc_to_lwt_in\",\n+\t\t\t\t skel-\u003eprogs.lwt_in_skb_ext_read, \"in\", false);\n+\tif (test__start_subtest(\"lwt_out_to_tc\"))\n+\t\ttest_skb_ext_lwt(skel, \"lwt_out_to_tc\",\n+\t\t\t\t skel-\u003eprogs.lwt_out_skb_ext_write, \"out\", true);\n+\tif (test__start_subtest(\"lwt_xmit_to_tc\"))\n+\t\ttest_skb_ext_lwt(skel, \"lwt_xmit_to_tc\",\n+\t\t\t\t skel-\u003eprogs.lwt_xmit_skb_ext_write, \"xmit\", true);\n+\tif (test__start_subtest(\"tc_to_seg6local\"))\n+\t\ttest_skb_ext_seg6local(skel);\n+\tif (test__start_subtest(\"tc_to_sk_skb\"))\n+\t\ttest_skb_ext_sk_skb(skel);\n+\n+\ttest_xdp_meta__destroy(skel);\n+}\ndiff --git a/tools/testing/selftests/bpf/progs/test_xdp_meta.c b/tools/testing/selftests/bpf/progs/test_xdp_meta.c\nindex 08b03be0b891c..b0ab7f4f7a1b1 100644\n--- a/tools/testing/selftests/bpf/progs/test_xdp_meta.c\n+++ b/tools/testing/selftests/bpf/progs/test_xdp_meta.c\n@@ -3,9 +3,11 @@\n \n #include \u003cbpf/bpf_endian.h\u003e\n #include \u003cbpf/bpf_helpers.h\u003e\n+#include \u003cbpf/bpf_tracing.h\u003e\n #include \u003cerrno.h\u003e\n \n #include \"bpf_kfuncs.h\"\n+#include \"bpf_misc.h\"\n #include \"bpf_tracing_net.h\"\n \n #define META_SIZE 32\n@@ -22,10 +24,10 @@\n bool test_pass;\n \n static const __u8 meta_want[META_SIZE] = {\n-\t0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,\n-\t0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,\n-\t0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28,\n-\t0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38,\n+\t0x49, 0x20, 0x63, 0x72, 0x6f, 0x73, 0x73, 0x65,\n+\t0x64, 0x20, 0x68, 0x6f, 0x6f, 0x6b, 0x73, 0x20,\n+\t0x26, 0x20, 0x6c, 0x69, 0x76, 0x65, 0x64, 0x20,\n+\t0x74, 0x6f, 0x20, 0x74, 0x65, 0x6c, 0x6c, 0x21,\n };\n \n static bool check_metadata(const char *file, int line, __u8 *meta_have)\n@@ -689,4 +691,483 @@ int helper_skb_change_proto(struct __sk_buff *ctx)\n \treturn TC_ACT_SHOT;\n }\n \n+/* Write to skb_ext using bpf_dynptr_write helper */\n+SEC(\"tc\")\n+int tc_skb_ext_write(struct __sk_buff *ctx)\n+{\n+\tstruct bpf_dynptr meta;\n+\n+\tif (!is_test_packet_tc(ctx))\n+\t\treturn TC_ACT_SHOT;\n+\tif (bpf_dynptr_from_skb_ext(ctx, 0, BPF_SKB_EXT_F_CREATE, \u0026meta))\n+\t\treturn TC_ACT_SHOT;\n+\tif (bpf_dynptr_write(\u0026meta, 0, (void *)meta_want, ARRAY_SIZE(meta_want), 0))\n+\t\treturn TC_ACT_SHOT;\n+\n+\treturn TC_ACT_UNSPEC;\n+}\n+\n+/* Read from skb-ext metadata using bpf_dynptr_read helper */\n+SEC(\"tc\")\n+int tc_skb_ext_read(struct __sk_buff *ctx)\n+{\n+\t__u8 meta_have[META_SIZE];\n+\tstruct bpf_dynptr meta;\n+\n+\tif (bpf_dynptr_from_skb_ext(ctx, 0, 0, \u0026meta))\n+\t\treturn TC_ACT_SHOT;\n+\tif (bpf_dynptr_read(meta_have, ARRAY_SIZE(meta_have), \u0026meta, 0, 0))\n+\t\treturn TC_ACT_SHOT;\n+\tif (!check_metadata(meta_have))\n+\t\treturn TC_ACT_SHOT;\n+\n+\ttest_pass = true;\n+\treturn TC_ACT_UNSPEC;\n+}\n+\n+/* Read from a cloned skb_ext dynptr */\n+SEC(\"tc\")\n+int tc_skb_ext_clone_read(struct __sk_buff *ctx)\n+{\n+\tstruct bpf_dynptr meta, clone;\n+\t__u8 meta_have[META_SIZE];\n+\n+\tif (bpf_dynptr_from_skb_ext(ctx, 0, 0, \u0026meta))\n+\t\treturn TC_ACT_SHOT;\n+\tif (bpf_dynptr_clone(\u0026meta, \u0026clone))\n+\t\treturn TC_ACT_SHOT;\n+\tif (bpf_dynptr_read(meta_have, ARRAY_SIZE(meta_have), \u0026clone, 0, 0))\n+\t\treturn TC_ACT_SHOT;\n+\tif (!check_metadata(meta_have))\n+\t\treturn TC_ACT_SHOT;\n+\n+\ttest_pass = true;\n+\treturn TC_ACT_UNSPEC;\n+}\n+\n+/* Read from skb_ext using bpf_dynptr_slice */\n+SEC(\"tc\")\n+int tc_skb_ext_slice_read(struct __sk_buff *ctx)\n+{\n+\tstruct bpf_dynptr meta;\n+\t__u8 *meta_have;\n+\n+\tif (bpf_dynptr_from_skb_ext(ctx, 0, 0, \u0026meta))\n+\t\treturn TC_ACT_SHOT;\n+\tmeta_have = bpf_dynptr_slice(\u0026meta, 0, NULL, META_SIZE);\n+\tif (!meta_have)\n+\t\treturn TC_ACT_SHOT;\n+\tif (!check_metadata(meta_have))\n+\t\treturn TC_ACT_SHOT;\n+\n+\ttest_pass = true;\n+\treturn TC_ACT_UNSPEC;\n+}\n+\n+/* Write to skb_ext using bpf_dynptr_slice_rdwr */\n+SEC(\"tc\")\n+int tc_skb_ext_slice_write(struct __sk_buff *ctx)\n+{\n+\tstruct bpf_dynptr meta;\n+\t__u8 *dst;\n+\n+\tif (!is_test_packet_tc(ctx))\n+\t\treturn TC_ACT_SHOT;\n+\tif (bpf_dynptr_from_skb_ext(ctx, 0, BPF_SKB_EXT_F_CREATE, \u0026meta))\n+\t\treturn TC_ACT_SHOT;\n+\tdst = bpf_dynptr_slice_rdwr(\u0026meta, 0, NULL, META_SIZE);\n+\tif (!dst)\n+\t\treturn TC_ACT_SHOT;\n+\t__builtin_memcpy(dst, meta_want, META_SIZE);\n+\n+\treturn TC_ACT_UNSPEC;\n+}\n+\n+/* Opening skb_ext without F_CREATE on a fresh skb should fail */\n+SEC(\"tc\")\n+int tc_skb_ext_no_alloc(struct __sk_buff *ctx)\n+{\n+\tstruct bpf_dynptr meta;\n+\n+\tif (!is_test_packet_tc(ctx))\n+\t\treturn TC_ACT_SHOT;\n+\tif (bpf_dynptr_from_skb_ext(ctx, 0, 0, \u0026meta) != -ENOENT)\n+\t\treturn TC_ACT_SHOT;\n+\n+\ttest_pass = true;\n+\treturn TC_ACT_UNSPEC;\n+}\n+\n+/* Invalid flags are rejected */\n+SEC(\"tc\")\n+int tc_skb_ext_invalid_flags(struct __sk_buff *ctx)\n+{\n+\tstruct bpf_dynptr meta;\n+\n+\tif (!is_test_packet_tc(ctx))\n+\t\treturn TC_ACT_SHOT;\n+\tif (bpf_dynptr_from_skb_ext(ctx, 0, ~0ULL, \u0026meta) != -EINVAL)\n+\t\treturn TC_ACT_SHOT;\n+\n+\ttest_pass = true;\n+\treturn TC_ACT_UNSPEC;\n+}\n+\n+/* Without F_CREATE the dynptr is read-only */\n+SEC(\"tc\")\n+int tc_skb_ext_rdonly(struct __sk_buff *ctx)\n+{\n+\t__u8 meta_have[META_SIZE];\n+\tstruct bpf_dynptr meta;\n+\n+\tif (!is_test_packet_tc(ctx))\n+\t\treturn TC_ACT_SHOT;\n+\n+\t/* Create and populate the ext */\n+\tif (bpf_dynptr_from_skb_ext(ctx, 0, BPF_SKB_EXT_F_CREATE, \u0026meta))\n+\t\treturn TC_ACT_SHOT;\n+\tif (bpf_dynptr_write(\u0026meta, 0, (void *)meta_want, META_SIZE, 0))\n+\t\treturn TC_ACT_SHOT;\n+\n+\t/* Reopen without F_CREATE -- should be read-only */\n+\tif (bpf_dynptr_from_skb_ext(ctx, 0, 0, \u0026meta))\n+\t\treturn TC_ACT_SHOT;\n+\n+\t/* Verify read-only: writes must fail, reads must work */\n+\tif (!bpf_dynptr_is_rdonly(\u0026meta))\n+\t\treturn TC_ACT_SHOT;\n+\tif (!bpf_dynptr_write(\u0026meta, 0, (void *)meta_want, META_SIZE, 0))\n+\t\treturn TC_ACT_SHOT;\n+\tif (bpf_dynptr_read(meta_have, META_SIZE, \u0026meta, 0, 0))\n+\t\treturn TC_ACT_SHOT;\n+\tif (!check_metadata(meta_have))\n+\t\treturn TC_ACT_SHOT;\n+\n+\ttest_pass = true;\n+\treturn TC_ACT_UNSPEC;\n+}\n+\n+/* Double alloc: data from first alloc survives second skb_ext_add */\n+SEC(\"tc\")\n+int tc_skb_ext_double_alloc(struct __sk_buff *ctx)\n+{\n+\t__u8 meta_have[META_SIZE];\n+\tstruct bpf_dynptr meta;\n+\n+\tif (!is_test_packet_tc(ctx))\n+\t\treturn TC_ACT_SHOT;\n+\n+\t/* First alloc + write */\n+\tif (bpf_dynptr_from_skb_ext(ctx, 0, BPF_SKB_EXT_F_CREATE, \u0026meta))\n+\t\treturn TC_ACT_SHOT;\n+\tif (bpf_dynptr_write(\u0026meta, 0, (void *)meta_want, META_SIZE, 0))\n+\t\treturn TC_ACT_SHOT;\n+\n+\t/* Second alloc -- skb_ext_add returns existing ext */\n+\tif (bpf_dynptr_from_skb_ext(ctx, 0, BPF_SKB_EXT_F_CREATE, \u0026meta))\n+\t\treturn TC_ACT_SHOT;\n+\tif (bpf_dynptr_read(meta_have, META_SIZE, \u0026meta, 0, 0))\n+\t\treturn TC_ACT_SHOT;\n+\tif (!check_metadata(meta_have))\n+\t\treturn TC_ACT_SHOT;\n+\n+\ttest_pass = true;\n+\treturn TC_ACT_UNSPEC;\n+}\n+\n+static const __u8 meta_zero[META_SIZE] = {};\n+\n+volatile bool clone_cow_done;\n+\n+/* Overwrite skb_ext on the clone via F_CREATE (COW) -- must not affect original.\n+ * Runs on the dummy ingress (clone side), synchronously during tc mirred.\n+ */\n+SEC(\"tc\")\n+int tc_skb_ext_clone_redir_cow(struct __sk_buff *ctx)\n+{\n+\tstruct bpf_dynptr meta;\n+\n+\tif (bpf_dynptr_from_skb_ext(ctx, 0, BPF_SKB_EXT_F_CREATE, \u0026meta))\n+\t\treturn TC_ACT_SHOT;\n+\n+\t/* Zero out the clone's ext -- must not affect original */\n+\tbpf_dynptr_write(\u0026meta, 0, (void *)meta_zero, META_SIZE, 0);\n+\n+\tclone_cow_done = true;\n+\treturn TC_ACT_SHOT;\n+}\n+\n+/* Verify COW isolation at kfree_skb time: once clone_cow_done is set,\n+ * check that the original skb still has meta_want.\n+ */\n+SEC(\"tp_btf/kfree_skb\")\n+int BPF_PROG(tp_kfree_skb_cow_check, struct sk_buff *skb)\n+{\n+\t__u8 meta_have[META_SIZE];\n+\tstruct bpf_dynptr meta;\n+\n+\tif (!clone_cow_done)\n+\t\treturn 0;\n+\n+\tif (bpf_dynptr_from_skb_ext((struct __sk_buff *)skb, 0, 0, \u0026meta))\n+\t\treturn 0;\n+\tif (bpf_dynptr_read(meta_have, META_SIZE, \u0026meta, 0, 0))\n+\t\treturn 0;\n+\tif (!check_metadata(meta_have))\n+\t\treturn 0;\n+\n+\ttest_pass = true;\n+\treturn 0;\n+}\n+\n+/* Read skb_ext from cgroup/skb ingress -- tests cross-hook survival */\n+SEC(\"cgroup_skb/ingress\")\n+int cgrp_skb_ext_read(struct __sk_buff *ctx)\n+{\n+\t__u8 meta_have[META_SIZE];\n+\tstruct bpf_dynptr meta;\n+\n+\tif (bpf_dynptr_from_skb_ext(ctx, 0, 0, \u0026meta))\n+\t\treturn 1;\n+\tif (bpf_dynptr_read(meta_have, META_SIZE, \u0026meta, 0, 0))\n+\t\treturn 1;\n+\tif (!check_metadata(meta_have))\n+\t\treturn 1;\n+\n+\ttest_pass = true;\n+\treturn 1;\n+}\n+\n+volatile __be16 target_port;\n+\n+#define TCPV4_HDR_OFF\t(sizeof(struct ethhdr) + sizeof(struct iphdr))\n+#define TCPV4_SPORT_OFF\t(TCPV4_HDR_OFF + offsetof(struct tcphdr, source))\n+#define TCPV4_DPORT_OFF\t(TCPV4_HDR_OFF + offsetof(struct tcphdr, dest))\n+\n+/* Write skb_ext on TCP packets to/from target_port */\n+SEC(\"tc\")\n+int tc_skb_ext_write_port(struct __sk_buff *ctx)\n+{\n+\tstruct bpf_dynptr meta;\n+\t__be16 sport, dport;\n+\n+\tif (ctx-\u003eprotocol != __bpf_constant_htons(ETH_P_IP))\n+\t\treturn TC_ACT_UNSPEC;\n+\tif (bpf_skb_load_bytes(ctx, TCPV4_SPORT_OFF, \u0026sport, sizeof(sport)))\n+\t\treturn TC_ACT_UNSPEC;\n+\tif (bpf_skb_load_bytes(ctx, TCPV4_DPORT_OFF, \u0026dport, sizeof(dport)))\n+\t\treturn TC_ACT_UNSPEC;\n+\tif (sport != target_port \u0026\u0026 dport != target_port)\n+\t\treturn TC_ACT_UNSPEC;\n+\n+\tif (bpf_dynptr_from_skb_ext(ctx, 0, BPF_SKB_EXT_F_CREATE, \u0026meta))\n+\t\treturn TC_ACT_UNSPEC;\n+\tbpf_dynptr_write(\u0026meta, 0, (void *)meta_want, META_SIZE, 0);\n+\n+\treturn TC_ACT_UNSPEC;\n+}\n+\n+/* Read skb_ext from sock_ops passive established -- tests TC -\u003e sock_ops path */\n+SEC(\"sockops\")\n+int skops_skb_ext_read(struct bpf_sock_ops *ctx)\n+{\n+\tstruct bpf_sock_ops_kern *kctx;\n+\t__u8 meta_have[META_SIZE];\n+\tstruct bpf_dynptr meta;\n+\tstruct sk_buff *skb;\n+\n+\tif (ctx-\u003eop != BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB)\n+\t\treturn 1;\n+\n+\tkctx = bpf_cast_to_kern_ctx(ctx);\n+\tskb = kctx-\u003eskb;\n+\tif (!skb)\n+\t\treturn 1;\n+\n+\tif (bpf_dynptr_from_skb_ext((struct __sk_buff *)skb, 0, 0, \u0026meta))\n+\t\treturn 1;\n+\tif (bpf_dynptr_read(meta_have, META_SIZE, \u0026meta, 0, 0))\n+\t\treturn 1;\n+\tif (!check_metadata(meta_have))\n+\t\treturn 1;\n+\n+\ttest_pass = true;\n+\treturn 1;\n+}\n+\n+/* Read skb_ext from LSM inet_conn_established -- tests TC -\u003e LSM path */\n+SEC(\"lsm/inet_conn_established\")\n+int BPF_PROG(lsm_skb_ext_read, struct sock *sk, struct sk_buff *skb)\n+{\n+\t__u8 meta_have[META_SIZE];\n+\tstruct bpf_dynptr meta;\n+\n+\tif (bpf_dynptr_from_skb_ext((struct __sk_buff *)skb, 0, 0, \u0026meta))\n+\t\treturn 0;\n+\tif (bpf_dynptr_read(meta_have, META_SIZE, \u0026meta, 0, 0))\n+\t\treturn 0;\n+\tif (!check_metadata(meta_have))\n+\t\treturn 0;\n+\n+\ttest_pass = true;\n+\treturn 0;\n+}\n+\n+/* Read skb_ext from socket filter -- tests TC -\u003e sk_filter path */\n+SEC(\"socket\")\n+int sk_filter_skb_ext_read(struct __sk_buff *ctx)\n+{\n+\t__u8 meta_have[META_SIZE];\n+\tstruct bpf_dynptr meta;\n+\n+\tif (bpf_dynptr_from_skb_ext(ctx, 0, 0, \u0026meta))\n+\t\tgoto out;\n+\tif (bpf_dynptr_read(meta_have, META_SIZE, \u0026meta, 0, 0))\n+\t\tgoto out;\n+\tif (!check_metadata(meta_have))\n+\t\tgoto out;\n+\n+\ttest_pass = true;\n+out:\n+\treturn ctx-\u003elen;\n+}\n+\n+/* Write skb_ext from cgroup/skb egress */\n+SEC(\"cgroup_skb/egress\")\n+int cgrp_skb_ext_write(struct __sk_buff *ctx)\n+{\n+\tstruct bpf_dynptr meta;\n+\n+\tif (!is_test_packet_tc(ctx))\n+\t\treturn 1;\n+\n+\tif (bpf_dynptr_from_skb_ext(ctx, 0, BPF_SKB_EXT_F_CREATE, \u0026meta))\n+\t\treturn 1;\n+\tbpf_dynptr_write(\u0026meta, 0, (void *)meta_want, META_SIZE, 0);\n+\n+\treturn 1;\n+}\n+\n+/* Read skb_ext from tp_btf/kfree_skb -- tests survival until skb free */\n+SEC(\"tp_btf/kfree_skb\")\n+int BPF_PROG(tp_kfree_skb_ext_read, struct sk_buff *skb)\n+{\n+\t__u8 meta_have[META_SIZE];\n+\tstruct bpf_dynptr meta;\n+\n+\tif (bpf_dynptr_from_skb_ext((struct __sk_buff *)skb, 0, 0, \u0026meta))\n+\t\treturn 0;\n+\tif (bpf_dynptr_read(meta_have, META_SIZE, \u0026meta, 0, 0))\n+\t\treturn 0;\n+\tif (!check_metadata(meta_have))\n+\t\treturn 0;\n+\n+\ttest_pass = true;\n+\treturn 0;\n+}\n+\n+#define NF_ACCEPT 1\n+\n+SEC(\"netfilter\")\n+int nf_skb_ext_read(struct bpf_nf_ctx *ctx)\n+{\n+\tstruct __sk_buff *skb = (struct __sk_buff *)ctx-\u003eskb;\n+\t__u8 meta_have[META_SIZE];\n+\tstruct bpf_dynptr meta;\n+\n+\tif (!skb)\n+\t\treturn NF_ACCEPT;\n+\n+\tif (bpf_dynptr_from_skb_ext(skb, 0, 0, \u0026meta))\n+\t\treturn NF_ACCEPT;\n+\tif (bpf_dynptr_read(meta_have, META_SIZE, \u0026meta, 0, 0))\n+\t\treturn NF_ACCEPT;\n+\tif (!check_metadata(meta_have))\n+\t\treturn NF_ACCEPT;\n+\n+\ttest_pass = true;\n+\treturn NF_ACCEPT;\n+}\n+\n+SEC(\"lwt_in\")\n+int lwt_in_skb_ext_read(struct __sk_buff *ctx)\n+{\n+\t__u8 meta_have[META_SIZE];\n+\tstruct bpf_dynptr meta;\n+\n+\tif (bpf_dynptr_from_skb_ext(ctx, 0, 0, \u0026meta))\n+\t\treturn BPF_OK;\n+\tif (bpf_dynptr_read(meta_have, META_SIZE, \u0026meta, 0, 0))\n+\t\treturn BPF_OK;\n+\tif (!check_metadata(meta_have))\n+\t\treturn BPF_OK;\n+\n+\ttest_pass = true;\n+\treturn BPF_OK;\n+}\n+\n+SEC(\"lwt_out\")\n+int lwt_out_skb_ext_write(struct __sk_buff *ctx)\n+{\n+\tstruct bpf_dynptr meta;\n+\n+\tif (!is_test_packet_tc(ctx))\n+\t\treturn BPF_OK;\n+\tif (bpf_dynptr_from_skb_ext(ctx, 0, BPF_SKB_EXT_F_CREATE, \u0026meta))\n+\t\treturn BPF_OK;\n+\tbpf_dynptr_write(\u0026meta, 0, (void *)meta_want, ARRAY_SIZE(meta_want), 0);\n+\n+\treturn BPF_OK;\n+}\n+\n+SEC(\"lwt_xmit\")\n+int lwt_xmit_skb_ext_write(struct __sk_buff *ctx)\n+{\n+\tstruct bpf_dynptr meta;\n+\n+\tif (!is_test_packet_tc(ctx))\n+\t\treturn BPF_OK;\n+\tif (bpf_dynptr_from_skb_ext(ctx, 0, BPF_SKB_EXT_F_CREATE, \u0026meta))\n+\t\treturn BPF_OK;\n+\tbpf_dynptr_write(\u0026meta, 0, (void *)meta_want, ARRAY_SIZE(meta_want), 0);\n+\n+\treturn BPF_OK;\n+}\n+\n+/* Read skb_ext from seg6local End.BPF -- tests TC -\u003e seg6local path */\n+SEC(\"lwt_seg6local\")\n+int seg6local_skb_ext_read(struct __sk_buff *ctx)\n+{\n+\t__u8 meta_have[META_SIZE];\n+\tstruct bpf_dynptr meta;\n+\n+\tif (bpf_dynptr_from_skb_ext(ctx, 0, 0, \u0026meta))\n+\t\treturn BPF_OK;\n+\tif (bpf_dynptr_read(meta_have, META_SIZE, \u0026meta, 0, 0))\n+\t\treturn BPF_OK;\n+\tif (!check_metadata(meta_have))\n+\t\treturn BPF_OK;\n+\n+\ttest_pass = true;\n+\treturn BPF_OK;\n+}\n+\n+/* Read skb_ext from sk_skb stream verdict hook */\n+SEC(\"sk_skb/stream_verdict\")\n+int sk_skb_skb_ext_read(struct __sk_buff *ctx)\n+{\n+\t__u8 meta_have[META_SIZE];\n+\tstruct bpf_dynptr meta;\n+\n+\tif (bpf_dynptr_from_skb_ext(ctx, 0, 0, \u0026meta))\n+\t\treturn SK_PASS;\n+\tif (bpf_dynptr_read(meta_have, META_SIZE, \u0026meta, 0, 0))\n+\t\treturn SK_PASS;\n+\tif (!check_metadata(meta_have))\n+\t\treturn SK_PASS;\n+\n+\ttest_pass = true;\n+\treturn SK_PASS;\n+}\n+\n char _license[] SEC(\"license\") = \"GPL\";\n"
}

2/1 2026/08/14 08:38 agent patch-evaluator
0m Model:
best-expensive

Args:
null

Results:
{
  "EnableConfigs": [
    "BPF_SKB_EXT"
  ],
  "FocusSymbols": [
    "bpf_dynptr_from_skb_ext",
    "__bpf_skb_ext_load_bytes",
    "__bpf_skb_ext_store_bytes",
    "bpf_skb_ext_pointer",
    "skb_ext_scrub"
  ],
  "Reasoning": "The patch introduces a new BPF feature allowing BPF programs to store and retrieve metadata in an `sk_buff` extension (`struct bpf_skb_ext`). This includes new Kfuncs (`bpf_dynptr_from_skb_ext`), a new dynptr type (`BPF_DYNPTR_TYPE_SKB_EXT`), and modifications to the core network stack to preserve this extension across certain network layers (e.g., `skb_ext_scrub`). This is a functional change in the core networking and BPF subsystems that is reachable from userspace via BPF programs, making it highly relevant for fuzzing.",
  "WorthFuzzing": true
}

Instruction:
You are an expert Linux kernel maintainer.
Your job is to review a provided patch series and determine
if it makes functional changes to the kernel that should be fuzzed.

IMPORTANT: The changes have ALREADY been applied and committed as the HEAD commit in
your workspace. Do NOT rely on your internal knowledge of the kernel. You must actively
use your code access tools to examine the actual source code and confirm any assumptions.

Return WorthFuzzing=false if the patch only contains:
- Modifications to Documentation/, Kconfig files, or code comments.
- Purely decorative changes, such as logging (e.g., pr_err, printk) or tracepoints.
- Changes to numeric constants or macros that do not functionally alter execution flow.
- Code paths that are impossible to reach in virtualized environments like GCE or QEMU,
  even when utilizing software-emulated hardware (e.g., usb gadget, mac80211_hwsim).
- Code in vendor-specific PCIe switch, SmartNIC, or GPU drivers (e.g., mlxsw, pds_core, qed,
  ionic, amdgpu) that require physical PCIe hardware cards not emulated in standard QEMU.
- Driver .remove, .shutdown, or pci_unregister_driver teardown callbacks (e.g., igb_remove)
  that are executed only during PCI hot-unplug or sysfs driver unbind operations.

If it modifies reachable core kernel logic, drivers, or architectures, use your code search
tools to verify the code can be executed, then return WorthFuzzing=true.

When returning WorthFuzzing=true, you MUST ALSO:
1. Extract any specific kernel functions that should be heavily fuzzed into FocusSymbols.
   Avoid listing generic hot-path functions to prevent skewed test distributions.
   Prefer non-static, non-inlined API entrypoint functions over internal static helper functions
   (which are inlined by the compiler and do not have distinct symbol addresses).
2. Identify any specific CONFIG_ options required to properly test this new/modified feature.
   Go and look into the Kconfig files and check for ifdefs around the code, do not make assumptions.
   Also check "depends on" lines in Kconfig to include any non-standard parent subsystem configs
   needed for Kbuild to compile the code statically into vmlinux. List them in the EnableConfigs
   output array, and DO NOT add a 'CONFIG_' prefix (e.g., return "NET_IPV4" instead of "CONFIG_NET_IPV4").
Prefer calling several tools at the same time to save round-trips.


Use set-results tool to provide results of the analysis.
It must be called exactly once before the final reply.
Ignore results of this tool.

Prompt:
For your convenience, here is the diff of the changes:
commit 8f1ceadd3ec88f91a907c36c2180c18013845199
Author: syz-cluster <triage@syzkaller.com>
Date:   Fri Aug 14 08:38:04 2026 +0000

    syz-cluster: applied patch under review

diff --git a/include/linux/bpf.h b/include/linux/bpf.h
index 7719f65284456..6b918a5b61bfe 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 14acb2455746f..6a2955487504d 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 22eda1d54a0e8..286f2e4cdbb1a 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 */
 };
@@ -5058,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)
 {
@@ -5127,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/include/uapi/linux/bpf.h b/include/uapi/linux/bpf.h
index 89b36de5fdbb6..3eee4467422db 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 c18f1e16edee4..aa28cc6a75f76 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 b740fa73ee26d..d66e154649e0d 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 7aa47342dc659..b0ddaccf61b30 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;
@@ -5683,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)
@@ -5725,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");
@@ -11036,6 +11042,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 +11123,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 +12231,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 e384773935515..6d57320dfea39 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 11bb0d236822a..a9a164738389c 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 c82a1472a5ea8..700bee045b5c5 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>
@@ -5155,6 +5156,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)
@@ -6281,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);
 
@@ -7255,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
@@ -7266,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 af96032174441..865655db21857 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;
diff --git a/tools/testing/selftests/bpf/config b/tools/testing/selftests/bpf/config
index ea7044f30adc3..502f4504bdba9 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/socket_helpers.h b/tools/testing/selftests/bpf/prog_tests/socket_helpers.h
index 0d59503a0c73e..eb114a6ea6f92 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 448807676176c..8bf586b5af32f 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,7 +2,9 @@
 #include <test_progs.h>
 #include <network_helpers.h>
 #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"
 
@@ -19,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,
@@ -693,3 +695,981 @@ 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);
+}
+
+/* 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;
+
+	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);
+	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);
+	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);
+}
+
+/* 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 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);
+}
+
+/* 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);
+}
+
+#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);
+}
+
+#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);
+}
+
+/* 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;
+
+	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);
+	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("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);
+	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 08b03be0b891c..b0ab7f4f7a1b1 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
@@ -22,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)
@@ -689,4 +691,483 @@ 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;
+}
+
+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;
+}
+
+/* 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;
+}
+
+/* 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;
+}
+
+#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;
+}
+
+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;
+}
+
+/* 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;
+}
+
+/* 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";

3/2 2026/08/14 08:38 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 31251
							output: 241
							thoughts: 1469
						

Thoughts:
**Fuzzing Analysis: BPF SKB Extension Patch Series**

Okay, let's break this down. The first thing I'm assessing is whether this patch series introduces functionality that warrants fuzzing. From what I'm seeing, it definitely does. It's adding a new `BPF_DYNPTR_TYPE_SKB_EXT`, allowing BPF programs to manage metadata within `sk_buff` extensions. This is enabled through a new `CONFIG_BPF_SKB_EXT` Kconfig option.

This means we have new kfuncs, specifically `bpf_dynptr_from_skb_ext`, and code modifications in `skb_ext_scrub` and related functions to preserve the extension across different network layers. Since this introduces new, reachable code paths from BPF programs that affect the networking and BPF subsystems, it's a clear 'WorthFuzzing=true' situation.

Now, let's identify the key kernel functions that require targeted fuzzing (`FocusSymbols`). I'm going to list the externally visible functions from this patch, as those are directly accessible and represent the primary attack surface:

*   `bpf_dynptr_from_skb_ext`
*   `__bpf_skb_ext_load_bytes`
*   `__bpf_skb_ext_store_bytes`
*   `bpf_skb_ext_pointer`
*   `skb_ext_scrub`

I'm skipping the static helper functions since these are likely inlined and fuzzed via the main entrypoints.

Next, we need the `EnableConfigs`. This is pretty straightforward:

*   `BPF_SKB_EXT` is the primary one, as it's the core enabler.
*   `BPF_SYSCALL` (implied by BPF_SKB_EXT, but worth including to make sure everything is properly included).
*   `SKB_EXTENSIONS` which is automatically selected by `BPF_SKB_EXT`.

I'm confident `BPF_SKB_EXT` is the core.

A quick check to confirm the visibility of these symbols is in order.

*   `bpf_dynptr_from_skb_ext` is a `__bpf_kfunc`, thus exported.
*   `__bpf_skb_ext_load_bytes` is exported.
*   `__bpf_skb_ext_store_bytes` is exported.
*   `bpf_skb_ext_pointer` is exported.
*   `skb_ext_scrub` is already exported.

I should also ensure `skb_ext_scrub` is exported, that `bpf_dynptr_from_skb_ext` is a kfunc and so on. Looks good!

Here's the final reasoning: This patch introduces `BPF_DYNPTR_TYPE_SKB_EXT`, new kfuncs, and network stack modifications for `sk_buff` extensions. Because this introduces new functionality that can be accessed by userspace via BPF programs, it needs to be fuzzed.

I'm ready to use the `set-results` tool with these values.



4/2 2026/08/14 08:38 tool set-results
0m Args:
{
  "EnableConfigs": [
    "BPF_SKB_EXT"
  ],
  "FocusSymbols": [
    "bpf_dynptr_from_skb_ext",
    "__bpf_skb_ext_load_bytes",
    "__bpf_skb_ext_store_bytes",
    "bpf_skb_ext_pointer",
    "skb_ext_scrub"
  ],
  "Reasoning": "The patch introduces a new BPF feature allowing BPF programs to store and retrieve metadata in an `sk_buff` extension (`struct bpf_skb_ext`). This includes new Kfuncs (`bpf_dynptr_from_skb_ext`), a new dynptr type (`BPF_DYNPTR_TYPE_SKB_EXT`), and modifications to the core network stack to preserve this extension across certain network layers (e.g., `skb_ext_scrub`). This is a functional change in the core networking and BPF subsystems that is reachable from userspace via BPF programs, making it highly relevant for fuzzing.",
  "WorthFuzzing": true
}

Results:
{
  "EnableConfigs": [
    "BPF_SKB_EXT"
  ],
  "FocusSymbols": [
    "bpf_dynptr_from_skb_ext",
    "__bpf_skb_ext_load_bytes",
    "__bpf_skb_ext_store_bytes",
    "bpf_skb_ext_pointer",
    "skb_ext_scrub"
  ],
  "Reasoning": "The patch introduces a new BPF feature allowing BPF programs to store and retrieve metadata in an `sk_buff` extension (`struct bpf_skb_ext`). This includes new Kfuncs (`bpf_dynptr_from_skb_ext`), a new dynptr type (`BPF_DYNPTR_TYPE_SKB_EXT`), and modifications to the core network stack to preserve this extension across certain network layers (e.g., `skb_ext_scrub`). This is a functional change in the core networking and BPF subsystems that is reachable from userspace via BPF programs, making it highly relevant for fuzzing.",
  "WorthFuzzing": true
}

5/1 2026/08/14 08:38 action If
0m Args:
{
  "WorthFuzzing": true
}

Results:
null

6/2 2026/08/14 08:38 agent kmsan-evaluator
0m Model:
best-expensive

Args:
null

Results:
{
  "KMSANReasoning": "The patch introduces a new BPF skb extension (`struct bpf_skb_ext`) to store metadata. The extension memory is explicitly zero-initialized upon creation (`memset(ext, 0, sizeof(*ext))`) in `bpf_dynptr_from_skb_ext()`. BPF programs can read and write to this extension via dynptr helpers. Since the memory is fully initialized before being accessed by BPF programs, and the extension is not directly copied to user space, there is no risk of uninitialized memory reads or info-leaks. Any potential memory safety issues (like out-of-bounds accesses or use-after-free) would be caught by standard KASAN. Therefore, a dedicated KMSAN fuzzing session is not required.",
  "NeedsKMSAN": false
}

Instruction:
You are an expert Linux kernel security engineer specializing in kernel memory
error detectors (KASAN and KMSAN). Your job is to review the provided patch series and
determine if the code changes justify spawning a dedicated KMSAN (KernelMemorySanitizer)
fuzzing session in addition to standard KASAN fuzzing.

CRITICAL DISTINCTION BETWEEN KASAN AND KMSAN:
- Standard KASAN kernel builds (upstream-apparmor-kasan.config) already enable
  a comprehensive suite of debugging tools and sanitizers, including KASAN
  (out-of-bounds accesses, use-after-free, double free, invalid free), LOCKDEP
  (locking bugs and deadlocks), UB-sanitizers, and memory corruption checks.
- KMSAN (KernelMemorySanitizer) detects reads of UNINITIALIZED memory (stack, heap,
  or page allocations) and kernel-to-user memory info-leaks.

Rule: THERE IS NO SENSE IN RUNNING A KMSAN SESSION IF A BUG CAN BE CAUGHT BY KASAN,
LOCKDEP, OR OTHER STANDARD BUG DETECTORS.
A dedicated KMSAN fuzzing session incurs significant resource costs. You must ONLY
set NeedsKMSAN=true if the code changes introduce or expose UNINITIALIZED MEMORY risks
that are detected ONLY by KMSAN.

Look holistically at the patch series and surrounding code. Even if no direct
uninitialized field accesses or new buffer allocations are added in the diff itself,
a patch may alter control flow, bounds checking, or data length calculations in ways
that change how the rest of the code operates on existing buffers (e.g. allowing
uninitialized stack/heap memory to be read, copied to user space, or used in control
flow). Do not hesitate to use your code access tools to inspect the surrounding code,
called functions, and callers.

Set NeedsKMSAN=true ONLY IF the patch introduces or modifies:
1. Kernel structures sent to user space (via copy_to_user, put_user, netlink skb
   attributes, ioctl output arguments, socket options, or BPF buffers) where fields
   or structure padding might not be fully initialized/zeroed.
2. Conditional logic or branching that depends on potentially uninitialized variables
   or struct fields.
3. Allocation or initialization of complex data structures where uninitialized fields
   could be read later in reachable code paths.
4. Bounds checks, lengths, or logic in a way that allows surrounding code to access
   uninitialized bytes of existing buffers.

Set NeedsKMSAN=false IF:
- The code changes primarily risk out-of-bounds access, array overflows, NULL pointer
  dereferences, locking deadlocks, or use-after-free bugs (these are already caught
  by KASAN, LOCKDEP, or standard bug detectors).
- All stack/heap structures touched or introduced by the patch are fully zeroed
  or initialized (e.g. using = {0}, memset, kzalloc) before being read or copied.
- The patch does not introduce any risk of uninitialized memory usage or info-leaks.

Use your code access tools to inspect the surrounding code if necessary, then provide
detailed KMSANReasoning contrasting KASAN vs KMSAN applicability for this patch.
Prefer calling several tools at the same time to save round-trips.


Use set-results tool to provide results of the analysis.
It must be called exactly once before the final reply.
Ignore results of this tool.

Prompt:
For your convenience, here is the diff of the changes:
commit 8f1ceadd3ec88f91a907c36c2180c18013845199
Author: syz-cluster <triage@syzkaller.com>
Date:   Fri Aug 14 08:38:04 2026 +0000

    syz-cluster: applied patch under review

diff --git a/include/linux/bpf.h b/include/linux/bpf.h
index 7719f65284456..6b918a5b61bfe 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 14acb2455746f..6a2955487504d 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 22eda1d54a0e8..286f2e4cdbb1a 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 */
 };
@@ -5058,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)
 {
@@ -5127,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/include/uapi/linux/bpf.h b/include/uapi/linux/bpf.h
index 89b36de5fdbb6..3eee4467422db 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 c18f1e16edee4..aa28cc6a75f76 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 b740fa73ee26d..d66e154649e0d 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 7aa47342dc659..b0ddaccf61b30 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;
@@ -5683,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)
@@ -5725,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");
@@ -11036,6 +11042,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 +11123,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 +12231,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 e384773935515..6d57320dfea39 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 11bb0d236822a..a9a164738389c 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 c82a1472a5ea8..700bee045b5c5 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>
@@ -5155,6 +5156,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)
@@ -6281,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);
 
@@ -7255,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
@@ -7266,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 af96032174441..865655db21857 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;
diff --git a/tools/testing/selftests/bpf/config b/tools/testing/selftests/bpf/config
index ea7044f30adc3..502f4504bdba9 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/socket_helpers.h b/tools/testing/selftests/bpf/prog_tests/socket_helpers.h
index 0d59503a0c73e..eb114a6ea6f92 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 448807676176c..8bf586b5af32f 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,7 +2,9 @@
 #include <test_progs.h>
 #include <network_helpers.h>
 #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"
 
@@ -19,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,
@@ -693,3 +695,981 @@ 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);
+}
+
+/* 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;
+
+	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);
+	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);
+	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);
+}
+
+/* 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 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);
+}
+
+/* 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);
+}
+
+#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);
+}
+
+#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);
+}
+
+/* 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;
+
+	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);
+	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("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);
+	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 08b03be0b891c..b0ab7f4f7a1b1 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
@@ -22,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)
@@ -689,4 +691,483 @@ 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;
+}
+
+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;
+}
+
+/* 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;
+}
+
+/* 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;
+}
+
+#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;
+}
+
+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;
+}
+
+/* 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;
+}
+
+/* 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";

7/3 2026/08/14 08:38 llm kmsan-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 31248
							output: 186
							thoughts: 1692
						

Thoughts:
**KMSAN Fuzzing Session Assessment**

Okay, let's break this down. We need to decide if this patch warrants a dedicated KMSAN fuzzing session. The core of this patch seems to be the introduction of a new BPF skb extension, `CONFIG_BPF_SKB_EXT`, allowing BPF programs to store arbitrary metadata via `bpf_dynptr_from_skb_ext()`. My immediate thought is, are we introducing any uninitialized memory exposure here?

First, I see the changes include the addition of `BPF_DYNPTR_TYPE_SKB_EXT` and `struct bpf_skb_ext`. Then, crucial functions like `bpf_dynptr_from_skb_ext`, `__bpf_skb_ext_load_bytes`, and `__bpf_skb_ext_store_bytes` are implemented. Importantly, `bpf_dynptr_from_skb_ext` calls `skb_ext_add` to add the extension.

Now, let's focus on `bpf_dynptr_from_skb_ext`: the critical line is where the extension's memory is zeroed out using `memset(ext, 0, sizeof(*ext))`. This is a *very* good sign. It explicitly prevents any uninitialized memory from potentially leaking to the BPF program.

So, where else is this `struct bpf_skb_ext` handled? Is it allocated or copied to userspace? Looks like no direct copies to userspace – it lives within the kernel as an `skb` extension, and BPF programs access it via dynptr helpers. Since it's zero-initialized during creation, BPF programs will only read zeros or what they write to it. This suggests a clean initialization path.

But wait, what if `skb_ext_add` reuses an existing extension? The code has a check, and if the extension already exists (`exists` is true), that means it was either already zeroed at creation or written to by a BPF program. So, that edge case is covered.

Let's also confirm that the `struct bpf_skb_ext` itself doesn't have any padding that might be a problem. Checking the struct definition, it's just a byte array which is cleared completely using the `memset`. So, we should be fine there.

Now, checking the `__bpf_skb_ext_load_bytes` function shows the data being loaded is initialized, and also checked `skb_ext_scrub` which confirms that the offsets are scrubbed when extensions are not kept.

So, overall, the data in `struct bpf_skb_ext` is *fully* zeroed upon creation. It's accessible *only* to BPF programs. No user space exposure directly. And KASAN will catch any out-of-bounds or use-after-free bugs if they were to occur, so we are good. Even if `skb_ext_maybe_cow` were to copy uninitialized memory, that old memory would have already been initialized when the extension was created. And we also confirmed there are no other places where `bpf_skb_ext` is used besides the new feature.

Therefore, my conclusion is clear: **NeedsKMSAN should be false**. KASAN is sufficient in this case.

Here's the reasoning to be crystal clear: The patch creates a new BPF skb extension (`struct bpf_skb_ext`) to hold metadata. The memory for this extension is explicitly zero-initialized during creation. BPF programs can read and write to this extension using dynptr helpers. Since all memory is fully initialized, and the extension isn't directly exposed to user space, uninitialized reads or information leaks are highly unlikely. Standard KASAN should handle any memory safety issues. No dedicated KMSAN fuzzing session is necessary.



8/3 2026/08/14 08:38 tool set-results
0m Args:
{
  "KMSANReasoning": "The patch introduces a new BPF skb extension (`struct bpf_skb_ext`) to store metadata. The extension memory is explicitly zero-initialized upon creation (`memset(ext, 0, sizeof(*ext))`) in `bpf_dynptr_from_skb_ext()`. BPF programs can read and write to this extension via dynptr helpers. Since the memory is fully initialized before being accessed by BPF programs, and the extension is not directly copied to user space, there is no risk of uninitialized memory reads or info-leaks. Any potential memory safety issues (like out-of-bounds accesses or use-after-free) would be caught by standard KASAN. Therefore, a dedicated KMSAN fuzzing session is not required.",
  "NeedsKMSAN": false
}

Results:
{
  "KMSANReasoning": "The patch introduces a new BPF skb extension (`struct bpf_skb_ext`) to store metadata. The extension memory is explicitly zero-initialized upon creation (`memset(ext, 0, sizeof(*ext))`) in `bpf_dynptr_from_skb_ext()`. BPF programs can read and write to this extension via dynptr helpers. Since the memory is fully initialized before being accessed by BPF programs, and the extension is not directly copied to user space, there is no risk of uninitialized memory reads or info-leaks. Any potential memory safety issues (like out-of-bounds accesses or use-after-free) would be caught by standard KASAN. Therefore, a dedicated KMSAN fuzzing session is not required.",
  "NeedsKMSAN": false
}

LLM Calls Summary:
Total Calls Total Tokens Avg Tokens Total Duration (Seconds) Avg Duration (Seconds)
Tool Calls Summary:
Total Calls Total Duration (Seconds) Avg Duration (Seconds)