* [PATCH v4 0/2] bpf: clear stale IPv4 options after LWT encapsulation
@ 2026-09-21 17:39 Weiming Shi
2026-09-21 17:39 ` [PATCH v4 1/2] " Weiming Shi
2026-09-21 17:39 ` [PATCH v4 2/2] selftests/bpf: cover stale CB after LWT IP encapsulation Weiming Shi
0 siblings, 2 replies; 4+ messages in thread
From: Weiming Shi @ 2026-09-21 17:39 UTC (permalink / raw)
To: Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
Eduard Zingerman, Kumar Kartikeya Dwivedi, Martin KaFai Lau,
Song Liu, Yonghong Song, Jiri Olsa, Emil Tsalapatis,
Ihor Solodrai, John Fastabend, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Shuah Khan
Cc: bpf, linux-kernel, netdev, linux-kselftest, Xiang Mei, Weiming Shi
bpf_lwt_push_ip_encap() rebases the network header after prepending an
outer IP header, but can leave IPCB(skb)->opt describing the inner IPv4
header. An ingress LWT route can then make an ICMP error consume stale
option offsets.
Patch 1 marks completed LWT IP encapsulation and clears the stale metadata
after bpf_prog_run_save_cb() has restored the protocol control block. For
same-family IPv4 encapsulation it clears only IPCB(skb)->opt. For a family
change it resets the new family's control block following
seg6_do_srh_encap(). Patch 2 adds compact coverage to the existing LWT IP
encapsulation selftest.
Changes:
v4:
- Follow Alexei Starovoitov's suggestion to drop the independent freplace
patch and keep cb_access as one bit.
- Clear only IPv4 options when the family is unchanged; use skb->protocol
and the SEG6 precedent when it changes.
- Rebase on bpf-next and replace the standalone selftest with compact
coverage in the existing LWT IP encapsulation test.
v3: https://lore.kernel.org/bpf/20260920163211.795547-1-bestswngs@gmail.com/
- Follow Daniel Borkmann's suggestion to reset the CB after
bpf_prog_run_save_cb() restores it, avoiding an extra CB copy.
- Split out freplace handling and selftests, and cover CB access, VRF,
nested runs, and already-encapsulated packets.
v2: https://lore.kernel.org/bpf/20260916170406.1280954-2-bestswngs@gmail.com/
- Preserve the protocol CB for programs without ctx->cb[] access and restore
the proper layout for the final packet consumer.
v1: https://lore.kernel.org/bpf/20260915170147.3943392-2-bestswngs@gmail.com/
Weiming Shi (2):
bpf: clear stale IPv4 options after LWT encapsulation
selftests/bpf: cover stale CB after LWT IP encapsulation
include/linux/filter.h | 1 +
net/core/lwt_bpf.c | 30 ++++++++++++
.../selftests/bpf/prog_tests/lwt_ip_encap.c | 45 ++++++++++++++++++
.../selftests/bpf/progs/test_lwt_ip_encap.c | 47 +++++++++++++++++++
4 files changed, 123 insertions(+)
--
2.55.0
^ permalink raw reply [flat|nested] 4+ messages in thread
* [PATCH v4 1/2] bpf: clear stale IPv4 options after LWT encapsulation
2026-09-21 17:39 [PATCH v4 0/2] bpf: clear stale IPv4 options after LWT encapsulation Weiming Shi
@ 2026-09-21 17:39 ` Weiming Shi
2026-09-21 17:39 ` [PATCH v4 2/2] selftests/bpf: cover stale CB after LWT IP encapsulation Weiming Shi
1 sibling, 0 replies; 4+ messages in thread
From: Weiming Shi @ 2026-09-21 17:39 UTC (permalink / raw)
To: Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
Eduard Zingerman, Kumar Kartikeya Dwivedi, Martin KaFai Lau,
Song Liu, Yonghong Song, Jiri Olsa, Emil Tsalapatis,
Ihor Solodrai, John Fastabend, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Shuah Khan
Cc: bpf, linux-kernel, netdev, linux-kselftest, Xiang Mei,
Weiming Shi, stable, Alexei Starovoitov
bpf_lwt_push_ip_encap() rebases the network header after prepending an IP
header, but leaves IPCB(skb)->opt describing the inner IPv4 header. An
ingress LWT route can consequently make an ICMP error use stale option
offsets when constructing its reply.
Mark completed LWT IP encapsulation in the run's BPF network context and
clear stale metadata after bpf_prog_run_save_cb() restores the protocol
control block. For same-family IPv4 encapsulation, clear only the cached
options, as the IP tunnel transmit paths do. When the family changes,
reset the destination family's control block and initialize its ingress
interface, following seg6_do_srh_encap(). Save and restore the marker
around nested LWT runs.
Cc: stable@vger.kernel.org
Fixes: 52f278774e79 ("bpf: implement BPF_LWT_ENCAP_IP mode in bpf_lwt_push_encap")
Reported-by: Xiang Mei <xmei5@asu.edu>
Suggested-by: Daniel Borkmann <daniel@iogearbox.net>
Suggested-by: Alexei Starovoitov <alexei.starovoitov@gmail.com>
Assisted-by: LLM
Signed-off-by: Weiming Shi <bestswngs@gmail.com>
---
include/linux/filter.h | 1 +
net/core/lwt_bpf.c | 30 ++++++++++++++++++++++++++++++
2 files changed, 31 insertions(+)
diff --git a/include/linux/filter.h b/include/linux/filter.h
index 39decde7fc73..195898af9528 100644
--- a/include/linux/filter.h
+++ b/include/linux/filter.h
@@ -848,6 +848,7 @@ struct bpf_nh_params {
#define BPF_RI_F_CPU_MAP_INIT BIT(2)
#define BPF_RI_F_DEV_MAP_INIT BIT(3)
#define BPF_RI_F_XSK_MAP_INIT BIT(4)
+#define BPF_RI_F_LWT_IP_ENCAP BIT(5)
struct bpf_redirect_info {
u64 tgt_index;
diff --git a/net/core/lwt_bpf.c b/net/core/lwt_bpf.c
index da49364ec63d..88664382ca44 100644
--- a/net/core/lwt_bpf.c
+++ b/net/core/lwt_bpf.c
@@ -36,10 +36,30 @@ static inline struct bpf_lwt *bpf_lwt_lwtunnel(struct lwtunnel_state *lwt)
#define NO_REDIRECT false
#define CAN_REDIRECT true
+static void bpf_lwt_reset_cb(struct sk_buff *skb, __be16 orig_proto)
+{
+ if (skb->protocol == orig_proto) {
+ if (skb->protocol == htons(ETH_P_IP))
+ memset(&IPCB(skb)->opt, 0, sizeof(IPCB(skb)->opt));
+ return;
+ }
+
+ if (skb->protocol == htons(ETH_P_IP)) {
+ memset(IPCB(skb), 0, sizeof(*IPCB(skb)));
+ IPCB(skb)->iif = skb->skb_iif;
+ } else if (skb->protocol == htons(ETH_P_IPV6)) {
+ memset(IP6CB(skb), 0, sizeof(*IP6CB(skb)));
+ IP6CB(skb)->iif = skb->skb_iif;
+ }
+}
+
static int run_lwt_bpf(struct sk_buff *skb, struct bpf_lwt_prog *lwt,
struct dst_entry *dst, bool can_redirect)
{
struct bpf_net_context __bpf_net_ctx, *bpf_net_ctx;
+ struct bpf_redirect_info *ri;
+ bool lwt_ip_encap, saved_lwt_ip_encap;
+ __be16 orig_proto = skb->protocol;
int ret;
/* Disabling BH is needed to protect per-CPU bpf_redirect_info between
@@ -47,8 +67,17 @@ static int run_lwt_bpf(struct sk_buff *skb, struct bpf_lwt_prog *lwt,
*/
local_bh_disable();
bpf_net_ctx = bpf_net_ctx_set(&__bpf_net_ctx);
+ ri = bpf_net_ctx_get_ri();
+ saved_lwt_ip_encap = ri->kern_flags & BPF_RI_F_LWT_IP_ENCAP;
+ ri->kern_flags &= ~BPF_RI_F_LWT_IP_ENCAP;
bpf_compute_data_pointers(skb);
ret = bpf_prog_run_save_cb(lwt->prog, skb);
+ lwt_ip_encap = ri->kern_flags & BPF_RI_F_LWT_IP_ENCAP;
+ ri->kern_flags &= ~BPF_RI_F_LWT_IP_ENCAP;
+ if (saved_lwt_ip_encap)
+ ri->kern_flags |= BPF_RI_F_LWT_IP_ENCAP;
+ if (lwt_ip_encap)
+ bpf_lwt_reset_cb(skb, orig_proto);
switch (ret) {
case BPF_OK:
@@ -668,6 +697,7 @@ int bpf_lwt_push_ip_encap(struct sk_buff *skb, void *hdr, u32 len, bool ingress)
} else {
skb->protocol = htons(ETH_P_IPV6);
}
+ bpf_net_ctx_get_ri()->kern_flags |= BPF_RI_F_LWT_IP_ENCAP;
if (skb_is_gso(skb))
return handle_gso_encap(skb, ipv4, len);
--
2.55.0
^ permalink raw reply [flat|nested] 4+ messages in thread
* [PATCH v4 2/2] selftests/bpf: cover stale CB after LWT IP encapsulation
2026-09-21 17:39 [PATCH v4 0/2] bpf: clear stale IPv4 options after LWT encapsulation Weiming Shi
2026-09-21 17:39 ` [PATCH v4 1/2] " Weiming Shi
@ 2026-09-21 17:39 ` Weiming Shi
2026-09-21 18:38 ` bot+bpf-ci
1 sibling, 1 reply; 4+ messages in thread
From: Weiming Shi @ 2026-09-21 17:39 UTC (permalink / raw)
To: Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
Eduard Zingerman, Kumar Kartikeya Dwivedi, Martin KaFai Lau,
Song Liu, Yonghong Song, Jiri Olsa, Emil Tsalapatis,
Ihor Solodrai, John Fastabend, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Shuah Khan
Cc: bpf, linux-kernel, netdev, linux-kselftest, Xiang Mei, Weiming Shi
Add compact regression coverage to the existing LWT IP encapsulation
test. Send an IPv4 ping with a Record Route option through an ingress LWT
program which prepends a TTL-one IPv4 header, then observe __icmp_send()
with fentry and verify that the restored protocol control block no longer
contains the inner header's cached option offsets.
Read ctx->cb[0] in the LWT program so the test exercises
bpf_prog_run_save_cb()'s save/restore path before the post-run check.
Link: https://lore.kernel.org/bpf/DLKB3UXOH0IS.1UL8NV9HSJFXG@gmail.com/
Signed-off-by: Weiming Shi <bestswngs@gmail.com>
---
.../selftests/bpf/prog_tests/lwt_ip_encap.c | 45 ++++++++++++++++++
.../selftests/bpf/progs/test_lwt_ip_encap.c | 47 +++++++++++++++++++
2 files changed, 92 insertions(+)
diff --git a/tools/testing/selftests/bpf/prog_tests/lwt_ip_encap.c b/tools/testing/selftests/bpf/prog_tests/lwt_ip_encap.c
index 39e8a3b8b6af..14ba33eaeb96 100644
--- a/tools/testing/selftests/bpf/prog_tests/lwt_ip_encap.c
+++ b/tools/testing/selftests/bpf/prog_tests/lwt_ip_encap.c
@@ -686,3 +686,48 @@ void test_lwt_ip_encap_vxlan_ipv6(void)
{
lwt_ip_encap_vxlan(IPV6_ENCAP);
}
+
+void test_lwt_ip_encap_stale_cb(void)
+{
+ char ns1[NETNS_NAME_SIZE] = NETNS_BASE "-1-";
+ char ns2[NETNS_NAME_SIZE] = NETNS_BASE "-2-";
+ char ns3[NETNS_NAME_SIZE] = NETNS_BASE "-3-";
+ struct test_lwt_ip_encap *skel = NULL;
+
+ if (!ASSERT_OK(create_ns(ns1, NETNS_NAME_SIZE), "create ns1"))
+ goto out;
+ if (!ASSERT_OK(create_ns(ns2, NETNS_NAME_SIZE), "create ns2"))
+ goto out;
+ if (!ASSERT_OK(create_ns(ns3, NETNS_NAME_SIZE), "create ns3"))
+ goto out;
+ if (!ASSERT_OK(setup_network(ns1, ns2, ns3, ""), "setup network"))
+ goto out;
+
+ skel = test_lwt_ip_encap__open();
+ if (!ASSERT_OK_PTR(skel, "open"))
+ goto out;
+ bpf_program__set_autoload(skel->progs.bpf_lwt_encap_gre, false);
+ bpf_program__set_autoload(skel->progs.bpf_lwt_encap_gre6, false);
+ bpf_program__set_autoload(skel->progs.bpf_lwt_encap_vxlan, false);
+ bpf_program__set_autoload(skel->progs.bpf_lwt_encap_vxlan6, false);
+ bpf_program__set_autoload(skel->progs.bpf_lwt_encap_stale, false);
+ if (!ASSERT_OK(test_lwt_ip_encap__load(skel), "load"))
+ goto out;
+ if (!ASSERT_OK(test_lwt_ip_encap__attach(skel), "attach"))
+ goto out;
+
+ SYS(out, "ip -n %s route replace %s/32 encap bpf in obj %s sec encap_stale dev veth3",
+ ns2, IP4_ADDR_DST, BPF_FILE);
+ skel->bss->stale_cb_seen = false;
+ skel->bss->stale_cb_cleared = false;
+ SYS_NOFAIL("ip netns exec %s ping -q -R -c 1 -W 1 -I veth1 %s >/dev/null 2>&1",
+ ns1, IP4_ADDR_DST);
+ ASSERT_TRUE(skel->bss->stale_cb_seen, "stale_cb_seen");
+ ASSERT_TRUE(skel->bss->stale_cb_cleared, "stale_cb_cleared");
+
+out:
+ test_lwt_ip_encap__destroy(skel);
+ SYS_NOFAIL("ip netns del %s", ns1);
+ SYS_NOFAIL("ip netns del %s", ns2);
+ SYS_NOFAIL("ip netns del %s", ns3);
+}
diff --git a/tools/testing/selftests/bpf/progs/test_lwt_ip_encap.c b/tools/testing/selftests/bpf/progs/test_lwt_ip_encap.c
index 4a934fccf8f5..8b57c7fb7fa8 100644
--- a/tools/testing/selftests/bpf/progs/test_lwt_ip_encap.c
+++ b/tools/testing/selftests/bpf/progs/test_lwt_ip_encap.c
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: GPL-2.0
#include "vmlinux.h"
#include <string.h>
+#include <bpf/bpf_core_read.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
#include <bpf/bpf_tracing.h>
@@ -10,6 +11,8 @@ struct grehdr {
__be16 protocol;
};
+#define ICMP_TIME_EXCEEDED 11
+
SEC("encap_gre")
int bpf_lwt_encap_gre(struct __sk_buff *skb)
{
@@ -80,6 +83,29 @@ int bpf_lwt_encap_gre6(struct __sk_buff *skb)
return BPF_LWT_REROUTE;
}
+SEC("encap_stale")
+int bpf_lwt_encap_stale(struct __sk_buff *skb)
+{
+ struct iphdr iph = {};
+
+ /* Exercise CB restore before post-run invalidation. */
+ if (skb->cb[0])
+ return BPF_DROP;
+
+ iph.version = 4;
+ iph.ihl = 5;
+ iph.ttl = 1;
+ iph.protocol = IPPROTO_IPIP;
+ iph.tot_len = bpf_htons(skb->len + sizeof(iph));
+ iph.saddr = bpf_htonl(0xac100264); /* 172.16.2.100 */
+ iph.daddr = bpf_htonl(0xac100464); /* 172.16.4.100 */
+
+ if (bpf_lwt_push_encap(skb, BPF_LWT_ENCAP_IP, &iph, sizeof(iph)))
+ return BPF_DROP;
+
+ return BPF_OK;
+}
+
#define VXLAN_PORT 4789
#define VXLAN_FLAGS 0x08000000
#define VXLAN_VNI 1
@@ -194,6 +220,27 @@ volatile const int tgt_ip_version;
__u16 transport_hdr = 0;
__u16 network_hdr = 0;
bool fexit_triggered = false;
+bool stale_cb_cleared = false;
+bool stale_cb_seen = false;
+
+SEC("fentry/__icmp_send")
+int BPF_PROG(fentry_icmp_send, struct sk_buff *skb, int type, int code,
+ __be32 info, const struct inet_skb_parm *parm)
+{
+ struct iphdr *iph;
+
+ if (type != ICMP_TIME_EXCEEDED)
+ return 0;
+
+ iph = (void *)BPF_CORE_READ(skb, head) + BPF_CORE_READ(skb, network_header);
+ if (BPF_CORE_READ(iph, daddr) != bpf_htonl(0xac100464))
+ return 0;
+
+ stale_cb_seen = true;
+ stale_cb_cleared = !BPF_CORE_READ(parm, opt.optlen) &&
+ !BPF_CORE_READ(parm, opt.rr);
+ return 0;
+}
SEC("?fexit/bpf_lwt_push_ip_encap")
int BPF_PROG(fexit_lwt_push_ip_encap, struct sk_buff *skb, void *hdr, u32 len, bool ingress,
--
2.55.0
^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [PATCH v4 2/2] selftests/bpf: cover stale CB after LWT IP encapsulation
2026-09-21 17:39 ` [PATCH v4 2/2] selftests/bpf: cover stale CB after LWT IP encapsulation Weiming Shi
@ 2026-09-21 18:38 ` bot+bpf-ci
0 siblings, 0 replies; 4+ messages in thread
From: bot+bpf-ci @ 2026-09-21 18:38 UTC (permalink / raw)
To: bestswngs, ast, daniel, andrii, eddyz87, memxor, martin.lau,
song, yonghong.song, jolsa, emil, ihor.solodrai, john.fastabend,
davem, edumazet, kuba, pabeni, horms, shuah
Cc: bpf, linux-kernel, netdev, linux-kselftest, xmei5, bestswngs,
ast, andrii, daniel, martin.lau, eddyz87, yonghong.song, mason,
ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 2488 bytes --]
> diff --git a/tools/testing/selftests/bpf/prog_tests/lwt_ip_encap.c b/tools/testing/selftests/bpf/prog_tests/lwt_ip_encap.c
> index 5c5560d45c5b4..ef90dd8d4b87c 100644
> --- a/tools/testing/selftests/bpf/prog_tests/lwt_ip_encap.c
> +++ b/tools/testing/selftests/bpf/prog_tests/lwt_ip_encap.c
[ ... ]
> @@ -739,3 +739,48 @@ void test_lwt_ip_encap_vxlan_ipv6(void)
> {
> lwt_ip_encap_vxlan(IPV6_ENCAP);
> }
> +
> +void test_lwt_ip_encap_stale_cb(void)
> +{
[ ... ]
> + SYS_NOFAIL("ip netns exec %s ping -q -R -c 1 -W 1 -I veth1 %s >/dev/null 2>&1",
> + ns1, IP4_ADDR_DST);
> + ASSERT_TRUE(skel->bss->stale_cb_seen, "stale_cb_seen");
> + ASSERT_TRUE(skel->bss->stale_cb_cleared, "stale_cb_cleared");
Could the test confirm that the probe packet actually carries an IP option?
Looking at ip_rcv_core() in net/ipv4/ip_input.c, IPCB(skb) is zeroed
unconditionally on receive:
memset(IPCB(skb), 0, sizeof(struct inet_skb_parm));
The opt.optlen and opt.rr fields are only written by ip_rcv_options(),
which ip_rcv_finish_core() calls only when the IP header has options:
if (iph->ihl > 5) {
drop_reason = ip_rcv_options(skb, dev);
If ping -R does not place a Record Route option on the wire (a ping
implementation that accepts but ignores -R, an option stripped en route,
or a non-iputils ping), the inner header has ihl == 5, IPCB(skb)->opt
stays all-zero through the LWT run, and the fentry hook records
stale_cb_cleared = true whether or not bpf_lwt_reset_cb() exists.
The stale_cb_seen flag still becomes true, because the ICMP_TIME_EXCEEDED
is produced by the TTL-one outer header the BPF program prepends and does
not depend on options at all, so neither assertion catches the missing
precondition and the test silently stops guarding the fix.
Could a pre-encapsulation check record whether opt.optlen was non-zero,
so the test fails loudly when the probe packet is not what it expects?
For example, an fentry or fexit on bpf_lwt_push_ip_encap that reads
IPCB(skb)->opt.optlen before the reset, then asserts that value was
non-zero.
> +
> +out:
> + test_lwt_ip_encap__destroy(skel);
> + SYS_NOFAIL("ip netns del %s", ns1);
> + SYS_NOFAIL("ip netns del %s", ns2);
> + SYS_NOFAIL("ip netns del %s", ns3);
> +}
[ ... ]
---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md
CI run summary: https://github.com/kernel-patches/bpf/actions/runs/35634260486
^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-09-21 18:38 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-21 17:39 [PATCH v4 0/2] bpf: clear stale IPv4 options after LWT encapsulation Weiming Shi
2026-09-21 17:39 ` [PATCH v4 1/2] " Weiming Shi
2026-09-21 17:39 ` [PATCH v4 2/2] selftests/bpf: cover stale CB after LWT IP encapsulation Weiming Shi
2026-09-21 18:38 ` bot+bpf-ci
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®