[PATCH RFC net 2/3] openvswitch: preserve the L3 header after stacked VLAN pushes
From: Gabriel Goller
Date: Wed Nov 11 2026 - 12:54:44 EST
A second VLAN push moves network_header to the in-band VLAN header.
Subsequent OVS set actions still expect it to point at IP, so they can
modify the wrong bytes and corrupt the packet.
OVS needs to retain the L3 header immediately after the push. Repairing
it only in validate_xmit_skb() is too late for intervening actions and
would not cover packets without CHECKSUM_PARTIAL. Keep this fix separate
from TX validation because it protects action execution, not just offload
preparation. Conversely, fixing OVS alone does not protect packets tagged
by tc or BPF before transmit.
The generic skb_vlan_push() layout is retained for tc classification.
The OVS push-then-set selftest fails without this fix and passes with it
in QEMU.
Fixes: 938863727076 ("tc: adjust network header after 2nd vlan push")
Signed-off-by: Gabriel Goller <g.goller@xxxxxxxxxxx>
---
net/openvswitch/actions.c | 6 +++-
.../selftests/net/openvswitch/openvswitch.sh | 34 ++++++++++++++++++++++
2 files changed, 39 insertions(+), 1 deletion(-)
diff --git a/net/openvswitch/actions.c b/net/openvswitch/actions.c
index 12e339cbf791..bf0bb5e6de2c 100644
--- a/net/openvswitch/actions.c
+++ b/net/openvswitch/actions.c
@@ -197,9 +197,10 @@ static int pop_vlan(struct sk_buff *skb, struct sw_flow_key *key)
static int push_vlan(struct sk_buff *skb, struct sw_flow_key *key,
const struct ovs_action_push_vlan *vlan)
{
+ bool tag_present = skb_vlan_tag_present(skb);
int err;
- if (skb_vlan_tag_present(skb)) {
+ if (tag_present) {
invalidate_flow_key(key);
} else {
key->eth.vlan.tci = vlan->vlan_tci;
@@ -207,6 +208,9 @@ static int push_vlan(struct sk_buff *skb, struct sw_flow_key *key,
}
err = skb_vlan_push(skb, vlan->vlan_tpid,
ntohs(vlan->vlan_tci) & ~VLAN_CFI_MASK);
+ /* OVS actions and TX offloads expect network_header to stay at L3. */
+ if (!err && tag_present)
+ skb->network_header += VLAN_HLEN;
skb_reset_mac_len(skb);
return err;
}
diff --git a/tools/testing/selftests/net/openvswitch/openvswitch.sh b/tools/testing/selftests/net/openvswitch/openvswitch.sh
index 21444657a040..339e3e09e643 100755
--- a/tools/testing/selftests/net/openvswitch/openvswitch.sh
+++ b/tools/testing/selftests/net/openvswitch/openvswitch.sh
@@ -28,6 +28,7 @@ tests="
tunnel_metadata ovs: test extraction of tunnel metadata
drop_reason drop: test drop reasons are emitted
pop_vlan vlan: POP_VLAN action strips tag
+ push_vlan_set vlan: SET after stacked VLAN pushes
dec_ttl ttl: dec_ttl decrements IP TTL
flow_set flow-set: Flow modify
action_set set: SET action rewrites fields
@@ -1501,6 +1502,39 @@ test_tunnel_metadata() {
return 0
}
+test_push_vlan_set() {
+ local sbx="test_push_vlan_set"
+ local actions
+
+ sbx_add "$sbx" || return $?
+ ovs_add_dp "$sbx" vlandp || return 1
+ ovs_add_netns_and_veths "$sbx" vlandp \
+ ns1 veth1 ns1veth 192.0.2.1/24 || return 1
+ ovs_add_netns_and_veths "$sbx" vlandp \
+ ns2 veth2 ns2veth 192.0.2.2/24 || return 1
+
+ ovs_add_flow "$sbx" vlandp \
+ 'in_port(1),eth(),eth_type(0x0806),arp()' '2' || return 1
+ ovs_add_flow "$sbx" vlandp \
+ 'in_port(2),eth(),eth_type(0x0806),arp()' '1' || return 1
+ ovs_add_flow "$sbx" vlandp \
+ 'in_port(2),eth(),eth_type(0x0800),ipv4()' '1' || return 1
+
+ # SET must address the IP header while both VLAN tags are present.
+ # Pop them afterwards so the receiver needs no VLAN configuration.
+ actions='push_vlan(vid=200,pcp=0,tpid=0x8100),'
+ actions+='push_vlan(vid=100,pcp=0,tpid=0x8100),'
+ actions+='set(ipv4(src=192.0.2.1,dst=192.0.2.2,'
+ actions+='proto=1,tos=0,ttl=63,frag=0)),pop_vlan,pop_vlan,2'
+ ovs_add_flow "$sbx" vlandp \
+ 'in_port(1),eth(),eth_type(0x0800),ipv4(proto=1),icmp()' \
+ "$actions" || return 1
+ ovs_sbx "$sbx" ip netns exec ns1 ping -c 3 -W 2 \
+ 192.0.2.2 || return 1
+
+ return 0
+}
+
test_pop_vlan() {
local sbx="test_pop_vlan"
sbx_add "$sbx" || return $?
--
2.47.3