Netdev List
 help / color / mirror / Atom feed
* [PATCH net] net: mpls: keep transport offsets valid across label changes
@ 2026-10-11  4:14 Weiming Shi
  2026-10-12  4:17 ` netdev-bot+sashiko
  0 siblings, 1 reply; 2+ messages in thread
From: Weiming Shi @ 2026-10-11  4:14 UTC (permalink / raw)
  To: David S . Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
	Simon Horman
  Cc: netdev, Xiang Mei, zjamg, Weiming Shi, stable,
	co+b3e1928dd33a1bd6

skb_mpls_push() and skb_mpls_pop() can leave a transport header at or
inside the moved MPLS header behind. Repeated pops then make
skb_transport_offset() negative; when a GSO skb is redirected to egress,
qdisc_pkt_len_segs_init() widens that offset, wraps its pull length, and
dereferences about 4 GiB past skb->data.

For tunnel GSO, qdisc_pkt_len_segs_init() uses the inner transport header
instead. skb_mpls_push() rejects encapsulated skbs because the networking
stack does not support simultaneous tunnel and MPLS GSO, but
skb_mpls_pop() did not. This leaves the same negative-offset path reachable
through stale inner metadata.

Align outer transport headers at or before the affected network header
with its new position while preserving valid later L4 offsets. Reject
encapsulated skbs from skb_mpls_pop(), matching skb_mpls_push(), and
refresh mac_len after a pop.

BUG: unable to handle page fault for address: ffffed10408b8e0b
Oops: Oops: 0000 [#1] SMP KASAN NOPTI
RIP: qdisc_pkt_len_segs_init (include/linux/tcp.h:31 net/core/dev.c:4193)
Call Trace:
 __dev_queue_xmit (net/core/dev.c:4835)
 tcf_mirred_to_dev (net/sched/act_mirred.c:242 net/sched/act_mirred.c:237 net/sched/act_mirred.c:328)
 tcf_mirred_act (net/sched/act_mirred.c:489)
 tcf_action_exec (include/net/tc_wrapper.h:59 net/sched/act_api.c:1162 net/sched/act_api.c:1136)
 tcf_classify (include/net/tc_wrapper.h:178 net/sched/cls_api.c:1779 net/sched/cls_api.c:1875)
 tc_run (net/core/dev.c:4463)
 __netif_receive_skb_core.constprop.0 (net/core/dev.c:4538 net/core/dev.c:6115)
 __netif_receive_skb_one_core (net/core/dev.c:6262)
 netif_receive_skb (net/core/dev.c:6377 net/core/dev.c:6463 net/core/dev.c:6522)
 tun_get_user (drivers/net/tun.c:1595 drivers/net/tun.c:2045)
 tun_chr_write_iter (drivers/net/tun.c:2091)
 vfs_write (fs/read_write.c:595 fs/read_write.c:687)
 ksys_write (fs/read_write.c:739)
 do_syscall_64 (arch/x86/entry/syscall_64.c:61 arch/x86/entry/syscall_64.c:84)
 entry_SYSCALL_64_after_hwframe (arch/x86/entry/entry_64.S:121)
Kernel panic - not syncing: Fatal exception in interrupt

Cc: stable@vger.kernel.org
Fixes: 48d2ab609b6b ("net: mpls: Fixups for GSO")
Reported-by: <co+b3e1928dd33a1bd6@bugs.sh>
Assisted-by: LLM
Signed-off-by: Weiming Shi <bestswngs@gmail.com>
---
 net/core/skbuff.c | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/net/core/skbuff.c b/net/core/skbuff.c
index cc3b4b702..c09bb8ac2 100644
--- a/net/core/skbuff.c
+++ b/net/core/skbuff.c
@@ -6612,6 +6612,7 @@ int skb_mpls_push(struct sk_buff *skb, __be32 mpls_lse, __be16 mpls_proto,
 		  int mac_len, bool ethernet)
 {
 	struct mpls_shim_hdr *lse;
+	bool trans_update;
 	int err;
 
 	if (unlikely(!eth_p_mpls(mpls_proto)))
@@ -6630,11 +6631,15 @@ int skb_mpls_push(struct sk_buff *skb, __be32 mpls_lse, __be16 mpls_proto,
 		skb_set_inner_protocol(skb, skb->protocol);
 	}
 
+	trans_update = skb_transport_header_was_set(skb) &&
+		       skb->transport_header <= skb->network_header;
 	skb_push(skb, MPLS_HLEN);
 	memmove(skb_mac_header(skb) - MPLS_HLEN, skb_mac_header(skb),
 		mac_len);
 	skb_reset_mac_header(skb);
 	skb_set_network_header(skb, mac_len);
+	if (trans_update)
+		skb->transport_header = skb->network_header;
 	skb_reset_mac_len(skb);
 
 	lse = mpls_hdr(skb);
@@ -6669,6 +6674,10 @@ int skb_mpls_pop(struct sk_buff *skb, __be16 next_proto, int mac_len,
 	if (unlikely(!eth_p_mpls(skb->protocol)))
 		return 0;
 
+	/* Networking stack does not allow simultaneous Tunnel and MPLS GSO. */
+	if (skb->encapsulation)
+		return -EINVAL;
+
 	err = skb_ensure_writable(skb, mac_len + MPLS_HLEN);
 	if (unlikely(err))
 		return err;
@@ -6680,6 +6689,10 @@ int skb_mpls_pop(struct sk_buff *skb, __be16 next_proto, int mac_len,
 	__skb_pull(skb, MPLS_HLEN);
 	skb_reset_mac_header(skb);
 	skb_set_network_header(skb, mac_len);
+	if (skb_transport_header_was_set(skb) &&
+	    skb->transport_header <= skb->network_header)
+		skb->transport_header = skb->network_header;
+	skb_reset_mac_len(skb);
 
 	if (ethernet && mac_len >= ETH_HLEN) {
 		struct ethhdr *hdr;

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

* Re: [PATCH net] net: mpls: keep transport offsets valid across label changes
  2026-10-11  4:14 [PATCH net] net: mpls: keep transport offsets valid across label changes Weiming Shi
@ 2026-10-12  4:17 ` netdev-bot+sashiko
  0 siblings, 0 replies; 2+ messages in thread
From: netdev-bot+sashiko @ 2026-10-12  4:17 UTC (permalink / raw)
  To: bestswngs
  Cc: davem, edumazet, kuba, pabeni, horms, netdev, xmei5, ndaugoing,
	stable, co+b3e1928dd33a1bd6

Thank you for your contribution! Sashiko AI review found 2 potential
issue(s) to consider.

Critical: 0 · High: 1 (1) · Medium: 0 · Low: 1

If you believe none of the issues are legitimate, please reply to each
issue of Medium severity or higher and explain why the AI is wrong.
If any of them are legitimate and you plan to respin, please let patchwork
know by including "pw-bot: cr" as a separate line at the end of your reply
(one such reply per series is enough).

- [High] The fix is incomplete.
- [Low] The new `if (skb->encapsulation) return -EINVAL;` in
  skb_mpls_pop() (net/core/skbuff.c) rejects every encapsulated MPLS…

Pre-existing issues:
- [High] This problem wasn't introduced by this patch, but nsh_pop()
  (net/nsh/nsh.c) leaves transport_header behind skb->data in the same…

--

net: mpls: keep transport offsets valid across label changes

This commit changes skb_mpls_push() and skb_mpls_pop(). After a label push
or pop, a transport header that sat at or before the old network header is
moved to the new network header. The commit also makes skb_mpls_pop() reject
encapsulated skbs and refresh mac_len after a pop. The goal is to prevent the
negative skb_transport_offset() that makes qdisc_pkt_len_segs_init()
dereference about 4 GiB past skb->data.

> diff --git a/net/core/skbuff.c b/net/core/skbuff.c
> index 41beaf625421a..eb49201534fff 100644
> --- a/net/core/skbuff.c
> +++ b/net/core/skbuff.c

[ ... ]

> @@ -6676,6 +6681,10 @@ int skb_mpls_pop(struct sk_buff *skb, __be16 next_proto, int mac_len,
>  	if (unlikely(!eth_p_mpls(skb->protocol)))
>  		return 0;
>  
> +	/* Networking stack does not allow simultaneous Tunnel and MPLS GSO. */
> +	if (skb->encapsulation)
> +		return -EINVAL;

[Severity: Low]
Will this drop non-GSO encapsulated MPLS packets that were popped and
forwarded before this patch?

The crash only happens with GSO skbs. qdisc_pkt_len_segs_init() returns
before it reads either transport offset when gso_size is 0. Also, a pop only
moves bytes up to the MAC header, so the absolute inner header positions
stay the same.

Encapsulated MPLS skbs can exist in tree. Take a locally generated VXLAN or
GRE packet with an inner CHECKSUM_PARTIAL that is not GSO: it keeps
skb->encapsulation set, because iptunnel_handle_offloads() only clears the
flag when ip_summed != CHECKSUM_PARTIAL. If the outer route uses an MPLS
lwtunnel, mpls_xmit() does:

	skb->dev = out_dev;
	skb->protocol = htons(ETH_P_MPLS_UC);

and does not clear skb->encapsulation. skb_scrub_packet() does not clear it
either when the skb crosses a veth into another netns.

If tc act_mpls pop or OVS pop_mpls then handles that packet, skb_mpls_pop()
now returns -EINVAL, and tcf_mpls_act() drops the packet:

	case TCA_MPLS_ACT_POP:
		if (skb_mpls_pop(skb, p->tcfm_proto, mac_len,
				 skb->dev && skb->dev->type == ARPHRD_ETHER))
			goto drop;

The commit message says this is deliberate, to match skb_mpls_push(), which
already rejects all encapsulated skbs. This topology is also unusual.

Since this is going to stable, would a narrower check such as
skb_is_gso(skb) && skb->encapsulation close the crash without these drops?

> +
>  	err = skb_ensure_writable(skb, mac_len + MPLS_HLEN);
>  	if (unlikely(err))
>  		return err;
> @@ -6687,6 +6696,10 @@ int skb_mpls_pop(struct sk_buff *skb, __be16 next_proto, int mac_len,
>  	__skb_pull(skb, MPLS_HLEN);
>  	skb_reset_mac_header(skb);
>  	skb_set_network_header(skb, mac_len);
> +	if (skb_transport_header_was_set(skb) &&
> +	    skb->transport_header <= skb->network_header)
> +		skb->transport_header = skb->network_header;
> +	skb_reset_mac_len(skb);

[Severity: High]
Is this fix complete? The subject says transport offsets stay valid across
label changes. However, mpls_forward() in net/mpls/af_mpls.c also pops a
label without moving skb->transport_header or checking skb->encapsulation:

net/mpls/af_mpls.c:mpls_forward() {
    ...
	/* Pop the label */
	skb_pull(skb, sizeof(*hdr));
	skb_reset_network_header(skb);
    ...
		skb_push(skb, new_header_size);
		skb_reset_network_header(skb);
    ...
}

If the route has no outgoing labels and the label is not BOS, nothing is
pushed back. The skb then goes to neigh_xmit() with transport_header behind
network_header.

Here is a single-hop sequence that looks reachable:

1. A tap writer sends a VID 0 tagged MPLS frame with a vnet header that has
   NEEDS_CSUM, gso_type TCPV4, csum_start=20 and csum_offset=16.
   __virtio_net_hdr_to_skb() does not check the protocol, so
   transport_header ends up at mac+20.

2. skb_vlan_untag() moves network_header to mac+18 but keeps the
   transport_header that is already set. transport_header now sits inside
   the first label.

3. mpls_forward() pops that label. network_header moves to 22 and
   transport_header stays at 20.

4. The route is "ip -f mpls route add L dev tunX", with no via and no
   labels. neigh_xmit(NEIGH_LINK_TABLE) calls dev_hard_header(), which does
   nothing for an IFF_TUN device, and then dev_queue_xmit().

5. __dev_queue_xmit() resets mac_header to data. Then, in
   qdisc_pkt_len_segs_init():

	hdr_len = skb_transport_offset(skb);
	...
		if (!pskb_may_pull(skb, hdr_len + sizeof(struct tcphdr)))
			return SKB_DROP_REASON_SKB_BAD_GSO;

		th = (const struct tcphdr *)(skb->data + hdr_len);

   hdr_len is (unsigned)-2, so the pskb_may_pull() length wraps to 18 and
   the check passes. th then points about 4 GiB past skb->data, which is the
   same oops as in the commit message.

mpls_pkt_too_big() does not catch this, because it only validates GSO skbs
when skb->len > mtu.

Multi-hop paths over veth reach the same state, since each hop pops another
4 bytes. A clsact sch_handle_ingress() on the peer also calls
qdisc_pkt_len_segs_init().

An encapsulated skb in mpls_forward() also keeps stale
inner_transport_header metadata.

The tun device, MPLS routes and the related sysctls only need CAP_NET_ADMIN
in a netns owned by a user namespace.

Could mpls_forward() get the same adjustment? Alternatively,
qdisc_pkt_len_segs_init() could reject a negative transport offset itself.

[Severity: High]
This is a pre-existing issue and was not introduced by this patch. nsh_pop()
in net/nsh/nsh.c also leaves transport_header behind skb->data, and the skb
reaches the same qdisc_pkt_len_segs_init() consumer:

net/nsh/nsh.c:nsh_pop() {
    ...
	skb_pull_rcsum(skb, length);
	skb_reset_mac_header(skb);
	skb_reset_network_header(skb);
	skb_reset_mac_len(skb);
    ...
}

Here is a possible trigger:

1. A tap writer sends an ETH_P_NSH frame with a vnet header that has
   NEEDS_CSUM, gso_type TCPV4, csum_start=20 and csum_offset=16.
   __virtio_net_hdr_to_skb() does not check the protocol on the NEEDS_CSUM
   path, so transport_header lands inside the 8-byte NSH header.

2. The tap is an OVS netdev vport. key_extract() leaves transport_header
   alone for non-IP eth types.

3. A flow runs with pop_eth, pop_nsh (np = TUN_P_ETHERNET) and output to an
   Ethernet netdev vport. pop_nsh() only invalidates the flow key, and
   do_output() -> ovs_vport_send() -> dev_queue_xmit() runs without
   extracting the key again.

4. qdisc_pkt_len_segs_init() sees skb_transport_offset() == -2 and reads
   about 4 GiB past skb->data, as described above.

OVS genl commands use GENL_UNS_ADMIN_PERM, so this can also be triggered
from inside a user namespace.

This patch fixes producers of the bad offset one at a time. Would a check
in qdisc_pkt_len_segs_init() cover all of them? It could drop skbs with a
negative transport or inner transport offset, using
SKB_DROP_REASON_SKB_BAD_GSO.

>  
>  	if (ethernet && mac_len >= ETH_HLEN) {
>  		struct ethhdr *hdr;

-- 
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261011041436.1424222-2-bestswngs%40gmail.com

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

end of thread, other threads:[~2026-10-12  4:17 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-11  4:14 [PATCH net] net: mpls: keep transport offsets valid across label changes Weiming Shi
2026-10-12  4:17 ` netdev-bot+sashiko

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