* [PATCH net v3 0/3] net: hsr: fix shared-skb mutations in the forwarding path
@ 2026-08-08 0:45 Xin Xie
2026-08-08 0:45 ` [PATCH net v3 1/3] net: hsr: privatize interlink-bound skbs before address mutation Xin Xie
` (2 more replies)
0 siblings, 3 replies; 4+ messages in thread
From: Xin Xie @ 2026-08-08 0:45 UTC (permalink / raw)
To: davem, kuba, pabeni
Cc: edumazet, horms, shuah, lukma, m-karicheri2, fmaurer, luka.gejak,
bigeasy, ali, qingfang.deng, netdev, linux-kselftest, Xin Xie
Two existing HSR/PRP forwarding paths modify data shared by multiple
skbs. One changes RedBox interlink source addresses, and the other
updates per-egress HSR path or PRP LAN IDs. A later consumer can then
observe another consumer's value.
Patch 1 makes interlink address substitution private. Patch 2 uses one
clone/COW helper for tagged HSR/PRP frames and both hardware
tag-insertion branches. Patch 3 adds regressions for both corruptions.
The series is independent of the posted GRO/GSO v5 series:
https://lore.kernel.org/all/20260807140751.1351-1-xiexinet@gmail.com/
Both orders apply to current net. On current net-next, patch 3 needs
git am -3 because hsr_prp_redbox.sh is already in the same Makefile
block; the merged list remains sorted and validates.
Validation:
- the targeted test fails both cases on the base and passes on the
candidate
- the affected HSR/PRP regression tests pass
- base-versus-candidate W=1 allmodconfig and allyesconfig add no
diagnostics
- no TAG_INS hardware was available; those branches have source
review only
Changes since v2, including official Sashiko dispositions:
- use a shared clone/COW helper and cover both TAG_INS branches
- correct the helper comment and interlink eligibility wording
- fix HSR LSDU encoding, capture draining, and result aggregation
- defer the existing dev->stats races per Paolo
- leave VLAN/non-linear receive parsing and receive-side tap aliases
unchanged
Previous postings:
v2: https://lore.kernel.org/all/20260802202504.2962-1-xiexinet@gmail.com/
v1: https://lore.kernel.org/all/20260728143604.26-1-xiexinet@gmail.com/
Xin Xie (3):
net: hsr: privatize interlink-bound skbs before address mutation
net: hsr: return private clones from the tagged-frame helpers
selftests: net: hsr: add shared-mutation regression test
net/hsr/hsr_forward.c | 65 ++++-
tools/testing/selftests/net/hsr/Makefile | 1 +
.../selftests/net/hsr/hsr_shared_mutation.sh | 242 ++++++++++++++++++
3 files changed, 299 insertions(+), 9 deletions(-)
create mode 100755 tools/testing/selftests/net/hsr/hsr_shared_mutation.sh
base-commit: 594d905195024b228c962627ae5ae7c17bd582a4
--
2.43.0
^ permalink raw reply [flat|nested] 4+ messages in thread
* [PATCH net v3 1/3] net: hsr: privatize interlink-bound skbs before address mutation
2026-08-08 0:45 [PATCH net v3 0/3] net: hsr: fix shared-skb mutations in the forwarding path Xin Xie
@ 2026-08-08 0:45 ` Xin Xie
2026-08-08 0:45 ` [PATCH net v3 2/3] net: hsr: return private clones from the tagged-frame helpers Xin Xie
2026-08-08 0:45 ` [PATCH net v3 3/3] selftests: net: hsr: add shared-mutation regression test Xin Xie
2 siblings, 0 replies; 4+ messages in thread
From: Xin Xie @ 2026-08-08 0:45 UTC (permalink / raw)
To: davem, kuba, pabeni
Cc: edumazet, horms, shuah, lukma, m-karicheri2, fmaurer, luka.gejak,
bigeasy, ali, qingfang.deng, netdev, linux-kselftest, Xin Xie
An skb sent to a RedBox interlink can share data with master delivery
or the original TX skb. hsr_deliver_master() and hsr_xmit() then write
different source addresses, so one consumer can observe the other's
address.
Use skb_cow() before interlink address substitution when those paths
can share the data. Drop that egress on COW failure. Other interlink
traffic keeps its zero-copy behavior.
Fixes: 5055cccfc2d1 ("net: hsr: Provide RedBox support (HSR-SAN)")
Reviewed-by: Ali Ahmet Memis <ali@iusegentoo.com>
Tested-by: Ali Ahmet Memis <ali@iusegentoo.com>
Signed-off-by: Xin Xie <xiexinet@gmail.com>
---
net/hsr/hsr_forward.c | 16 ++++++++++++++++
1 file changed, 16 insertions(+)
diff --git a/net/hsr/hsr_forward.c b/net/hsr/hsr_forward.c
index 0774981a65c1..67aaf5a8622b 100644
--- a/net/hsr/hsr_forward.c
+++ b/net/hsr/hsr_forward.c
@@ -420,6 +420,22 @@ static void hsr_deliver_master(struct sk_buff *skb, struct net_device *dev,
static int hsr_xmit(struct sk_buff *skb, struct hsr_port *port,
struct hsr_frame_info *frame)
{
+ /* An interlink-bound skb from get_untagged_frame() can still alias
+ * another live consumer: for master-originated frames the clone
+ * shares the original TX skb (which taps or the TX path may still
+ * hold); for ring frames the master also consumes them when they
+ * are destined to the local node without being exclusive to it.
+ * Privatize before any address mutation.
+ */
+ if (port->type == HSR_PT_INTERLINK &&
+ (frame->port_rcv->type == HSR_PT_MASTER ||
+ (frame->is_local_dest && !frame->is_local_exclusive)) &&
+ skb_cow(skb, 0)) {
+ frame->port_rcv->dev->stats.rx_dropped++;
+ kfree_skb(skb);
+ return NET_XMIT_DROP;
+ }
+
if (frame->port_rcv->type == HSR_PT_MASTER) {
hsr_addr_subst_dest(frame->node_src, skb, port);
--
2.43.0
^ permalink raw reply related [flat|nested] 4+ messages in thread
* [PATCH net v3 2/3] net: hsr: return private clones from the tagged-frame helpers
2026-08-08 0:45 [PATCH net v3 0/3] net: hsr: fix shared-skb mutations in the forwarding path Xin Xie
2026-08-08 0:45 ` [PATCH net v3 1/3] net: hsr: privatize interlink-bound skbs before address mutation Xin Xie
@ 2026-08-08 0:45 ` Xin Xie
2026-08-08 0:45 ` [PATCH net v3 3/3] selftests: net: hsr: add shared-mutation regression test Xin Xie
2 siblings, 0 replies; 4+ messages in thread
From: Xin Xie @ 2026-08-08 0:45 UTC (permalink / raw)
To: davem, kuba, pabeni
Cc: edumazet, horms, shuah, lukma, m-karicheri2, fmaurer, luka.gejak,
bigeasy, ali, qingfang.deng, netdev, linux-kselftest, Xin Xie
The tagged-frame helpers can return skbs whose data is still shared.
Later path, LAN ID, or source-address updates may then modify another
queued clone or the original TX skb.
Add hsr_clone_private() and use it for HSR, PRP, and both hardware
tag-insertion branches. Reacquire tag and trailer pointers after COW;
the PRP RCT is in the copied linear area.
This adds one linear-data copy per affected egress. Untagged software
forwarding is unchanged.
Fixes: 451d8123f897 ("net: prp: add packet handling support")
Fixes: dcf0cd1cc58b ("net: hsr: add offloading support")
Signed-off-by: Xin Xie <xiexinet@gmail.com>
---
net/hsr/hsr_forward.c | 49 +++++++++++++++++++++++++++++++++++--------
1 file changed, 40 insertions(+), 9 deletions(-)
diff --git a/net/hsr/hsr_forward.c b/net/hsr/hsr_forward.c
index 67aaf5a8622b..efcbf3cf26f9 100644
--- a/net/hsr/hsr_forward.c
+++ b/net/hsr/hsr_forward.c
@@ -325,8 +325,29 @@ static struct sk_buff *hsr_fill_tag(struct sk_buff *skb,
return skb;
}
-/* If the original frame was an HSR tagged frame, just clone it to be sent
- * unchanged. Otherwise, create a private frame especially tagged for 'port'.
+/* Clone an skb and make the clone's data private, so that per-egress
+ * writes cannot corrupt the original skb or other clones of it.
+ * Returns NULL on allocation failure.
+ */
+static struct sk_buff *hsr_clone_private(struct sk_buff *skb)
+{
+ struct sk_buff *clone;
+
+ clone = skb_clone(skb, GFP_ATOMIC);
+ if (!clone)
+ return NULL;
+ if (skb_cow(clone, 0)) {
+ kfree_skb(clone);
+ return NULL;
+ }
+
+ return clone;
+}
+
+/* If the original frame was an HSR tagged frame, return a private clone
+ * of it with the path id updated for 'port'. Otherwise, return a private
+ * clone for hardware tag insertion, or create a private frame especially
+ * tagged for 'port'.
*/
struct sk_buff *hsr_create_tagged_frame(struct hsr_frame_info *frame,
struct hsr_port *port)
@@ -336,14 +357,18 @@ struct sk_buff *hsr_create_tagged_frame(struct hsr_frame_info *frame,
int movelen;
if (frame->skb_hsr) {
- struct hsr_ethhdr *hsr_ethhdr =
- (struct hsr_ethhdr *)skb_mac_header(frame->skb_hsr);
+ struct hsr_ethhdr *hsr_ethhdr;
+
+ skb = hsr_clone_private(frame->skb_hsr);
+ if (!skb)
+ return NULL;
/* set the lane id properly */
+ hsr_ethhdr = (struct hsr_ethhdr *)skb_mac_header(skb);
hsr_set_path_id(frame, hsr_ethhdr, port);
- return skb_clone(frame->skb_hsr, GFP_ATOMIC);
+ return skb;
} else if (port->dev->features & NETIF_F_HW_HSR_TAG_INS) {
- return skb_clone(frame->skb_std, GFP_ATOMIC);
+ return hsr_clone_private(frame->skb_std);
}
/* Create the new skb with enough headroom to fit the HSR tag */
@@ -377,17 +402,23 @@ struct sk_buff *prp_create_tagged_frame(struct hsr_frame_info *frame,
struct sk_buff *skb;
if (frame->skb_prp) {
- struct prp_rct *trailer = skb_get_PRP_rct(frame->skb_prp);
+ struct prp_rct *trailer;
+ skb = hsr_clone_private(frame->skb_prp);
+ if (!skb)
+ return NULL;
+
+ trailer = skb_get_PRP_rct(skb);
if (trailer) {
prp_set_lan_id(trailer, port);
} else {
WARN_ONCE(!trailer, "errored PRP skb");
+ kfree_skb(skb);
return NULL;
}
- return skb_clone(frame->skb_prp, GFP_ATOMIC);
+ return skb;
} else if (port->dev->features & NETIF_F_HW_HSR_TAG_INS) {
- return skb_clone(frame->skb_std, GFP_ATOMIC);
+ return hsr_clone_private(frame->skb_std);
}
skb = skb_copy_expand(frame->skb_std, skb_headroom(frame->skb_std),
--
2.43.0
^ permalink raw reply related [flat|nested] 4+ messages in thread
* [PATCH net v3 3/3] selftests: net: hsr: add shared-mutation regression test
2026-08-08 0:45 [PATCH net v3 0/3] net: hsr: fix shared-skb mutations in the forwarding path Xin Xie
2026-08-08 0:45 ` [PATCH net v3 1/3] net: hsr: privatize interlink-bound skbs before address mutation Xin Xie
2026-08-08 0:45 ` [PATCH net v3 2/3] net: hsr: return private clones from the tagged-frame helpers Xin Xie
@ 2026-08-08 0:45 ` Xin Xie
2 siblings, 0 replies; 4+ messages in thread
From: Xin Xie @ 2026-08-08 0:45 UTC (permalink / raw)
To: davem, kuba, pabeni
Cc: edumazet, horms, shuah, lukma, m-karicheri2, fmaurer, luka.gejak,
bigeasy, ali, qingfang.deng, netdev, linux-kselftest, Xin Xie
Add regression coverage for both shared-data corruptions. Delay one
PRP egress and verify that queued clones retain their LAN IDs. For HSR
RedBox, deliver a tagged multicast to the master and interlink and
verify each source address.
Drain captures while injecting and merge both subtest results so
packet-socket pressure or a skip cannot hide a failure.
Signed-off-by: Xin Xie <xiexinet@gmail.com>
---
tools/testing/selftests/net/hsr/Makefile | 1 +
.../selftests/net/hsr/hsr_shared_mutation.sh | 242 ++++++++++++++++++
2 files changed, 243 insertions(+)
create mode 100755 tools/testing/selftests/net/hsr/hsr_shared_mutation.sh
diff --git a/tools/testing/selftests/net/hsr/Makefile b/tools/testing/selftests/net/hsr/Makefile
index 31fb9326cf53..87fe34951b8b 100644
--- a/tools/testing/selftests/net/hsr/Makefile
+++ b/tools/testing/selftests/net/hsr/Makefile
@@ -5,6 +5,7 @@ top_srcdir = ../../../../..
TEST_PROGS := \
hsr_ping.sh \
hsr_redbox.sh \
+ hsr_shared_mutation.sh \
link_faults.sh \
prp_ping.sh \
# end of TEST_PROGS
diff --git a/tools/testing/selftests/net/hsr/hsr_shared_mutation.sh b/tools/testing/selftests/net/hsr/hsr_shared_mutation.sh
new file mode 100755
index 000000000000..0b8b8791190d
--- /dev/null
+++ b/tools/testing/selftests/net/hsr/hsr_shared_mutation.sh
@@ -0,0 +1,242 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# Verify that per-egress mutations of shared skb data are private:
+#
+# F2 (path/LAN ID): on an affected kernel the second slave's LAN-ID write
+# lands in the first slave's still-queued clone; with a netem delay on
+# slave A, injected frames leave A carrying B's LAN ID.
+#
+# F1 (RedBox source MAC): on an affected kernel an HSR-tagged multicast
+# frame received on a RedBox slave is cloned for master and interlink,
+# and the interlink's RedBox-MAC rewrite lands in the master clone's
+# buffer, so the local stack receives the RedBox MAC instead of the
+# originating node's MAC.
+
+ipv6=false
+
+source ./hsr_common.sh
+
+DUR=5
+
+require()
+{
+ command -v "$1" >/dev/null 2>&1 && return 0
+ echo "SKIP: $1 not available"
+ exit $ksft_skip
+}
+
+require ip
+require tc
+require python3
+
+trap cleanup_all_ns EXIT
+
+# ------------------------------------------------------- F2: LAN-ID isolation
+# PRP DANP (proto 1), AF_PACKET pre-tagged injection, netem on slave A.
+run_f2()
+{
+ setup_ns ns 2>/dev/null || return $ksft_skip
+ nsx() { ip netns exec "$ns" "$@"; }
+
+ # Probe sch_netem inside the disposable namespace only.
+ if ! nsx tc qdisc add dev lo root netem delay 1ms 2>/dev/null; then
+ echo "SKIP: sch_netem not available"
+ return $ksft_skip
+ fi
+ nsx tc qdisc del dev lo root 2>/dev/null
+
+ # Capability probes end here; setup or runtime failure below is FAIL.
+ nsx ip link add vA type veth peer name vAp ||
+ { echo "FAIL: veth A"; return 1; }
+ nsx ip link add vB type veth peer name vBp ||
+ { echo "FAIL: veth B"; return 1; }
+ for i in vA vB vAp vBp; do
+ nsx ip link set "$i" up || { echo "FAIL: $i up"; return 1; }
+ done
+ if ! nsx ip link add name prp0 type hsr slave1 vA slave2 vB \
+ supervision 45 proto 1 2>/dev/null; then
+ echo "SKIP: HSR/PRP not supported by this kernel"
+ return $ksft_skip
+ fi
+ nsx ip link set prp0 up || { echo "FAIL: prp0 up"; return 1; }
+ nsx tc qdisc add dev vA root netem delay 200ms ||
+ { echo "FAIL: netem"; return 1; }
+
+ nsx python3 /dev/stdin "$DUR" <<'PYF2'
+import socket, struct, select, sys, time
+
+dur = int(sys.argv[1])
+def lanid(pkt):
+ if len(pkt) < 20 or pkt[-2:] != b"\x88\xfb":
+ return None
+ return (pkt[-4] >> 4) & 0xF
+
+SRC = bytes.fromhex(open("/sys/class/net/prp0/address").read().replace(":", ""))
+DST = bytes.fromhex("02aabbccdd01")
+PAY = bytes(range(46))
+rct0 = struct.pack(">H", 0) + struct.pack(">H", 52 & 0x0FFF) + b"\x88\xfb"
+frame = DST + SRC + b"\x08\x00" + PAY + rct0
+
+tx = socket.socket(socket.AF_PACKET, socket.SOCK_RAW); tx.bind(("prp0", 0))
+sA = socket.socket(socket.AF_PACKET, socket.SOCK_RAW,
+ socket.ntohs(0x0003))
+sA.bind(("vAp", 0))
+sB = socket.socket(socket.AF_PACKET, socket.SOCK_RAW,
+ socket.ntohs(0x0003))
+sB.bind(("vBp", 0))
+sA.setblocking(False); sB.setblocking(False)
+
+a, b = [], []
+
+def match(pkt):
+ return (pkt[:6] == DST and pkt[6:12] == SRC
+ and pkt[12:14] == b"\x08\x00" and pkt[14:14 + len(PAY)] == PAY)
+
+def drain(timeout):
+ wait = timeout
+ while True:
+ r, _, _ = select.select([sA, sB], [], [], wait)
+ if not r:
+ return
+ for s in r:
+ pkt = s.recv(65535)
+ if not match(pkt):
+ continue
+ lid = lanid(pkt)
+ if lid is not None:
+ (a if s is sA else b).append(lid)
+ wait = 0
+
+# Drain while sending so the burst cannot overflow the capture sockets'
+# receive buffers; the netem-delayed A-side frames arrive afterwards and
+# are collected below.
+for _ in range(200):
+ tx.send(frame)
+ drain(0)
+ time.sleep(0.001)
+
+end = time.time() + dur
+while time.time() < end:
+ drain(0.3)
+
+print("A-side count=%d lan ids=%s" % (len(a), sorted(set(a))))
+print("B-side count=%d lan ids=%s" % (len(b), sorted(set(b))))
+if len(a) < 150 or len(b) < 150:
+ print("FAIL: too few injected frames captured (A=%d B=%d, sent 200)"
+ % (len(a), len(b)))
+ sys.exit(1)
+bad_a = [x for x in a if (x & 1) != 0]
+bad_b = [x for x in b if (x & 1) != 1]
+if bad_a or bad_b:
+ print("FAIL: shared-mutation corruption - A: %d/%d wrong-lan,"
+ " B: %d/%d wrong-lan"
+ % (len(bad_a), len(a), len(bad_b), len(b)))
+ sys.exit(1)
+print("PASS: per-egress LAN IDs isolated (A all bit0=0, B all bit0=1)")
+sys.exit(0)
+PYF2
+}
+
+# --------------------------------------------- F1: RedBox source-MAC privacy
+# HSR RedBox (proto 0), tagged multicast from a slave: master must keep
+# the node MAC, interlink must carry the RedBox MAC.
+run_f1()
+{
+ setup_ns ns 2>/dev/null || return $ksft_skip
+ nsx() { ip netns exec "$ns" "$@"; }
+
+ nsx ip link add vA type veth peer name vAp ||
+ { echo "FAIL: veth A"; return 1; }
+ nsx ip link add vB type veth peer name vBp ||
+ { echo "FAIL: veth B"; return 1; }
+ nsx ip link add vI type veth peer name vIp ||
+ { echo "FAIL: veth I"; return 1; }
+ for i in vA vB vI vAp vBp vIp; do
+ nsx ip link set "$i" up || { echo "FAIL: $i up"; return 1; }
+ done
+ if ! nsx ip link add name hsr0 type hsr slave1 vA slave2 vB \
+ interlink vI supervision 45 proto 0 2>/dev/null; then
+ echo "SKIP: HSR RedBox not supported by this kernel"
+ return $ksft_skip
+ fi
+ nsx ip link set hsr0 up || { echo "FAIL: hsr0 up"; return 1; }
+
+ nsx python3 /dev/stdin <<'PYF1'
+import socket, select, sys, time
+
+NODE = bytes.fromhex("021122334455")
+MCAST = bytes.fromhex("01005e000001")
+RB = bytes.fromhex(open("/sys/class/net/vI/address").read().replace(":", ""))
+PAY = bytes(range(46))
+
+def frame(seq):
+ # LSDU size = payload + HSR tag (HSR_HLEN), as hsr_fill_tag() computes it
+ tag = (((1 << 12) | (len(PAY) + 6)).to_bytes(2, "big")
+ + seq.to_bytes(2, "big") + b"\x08\x00")
+ return MCAST + NODE + b"\x89\x2f" + tag + PAY
+
+tx = socket.socket(socket.AF_PACKET, socket.SOCK_RAW); tx.bind(("vAp", 0))
+sm = socket.socket(socket.AF_PACKET, socket.SOCK_RAW,
+ socket.ntohs(0x0003))
+sm.bind(("hsr0", 0))
+si = socket.socket(socket.AF_PACKET, socket.SOCK_RAW,
+ socket.ntohs(0x0003))
+si.bind(("vIp", 0))
+sm.setblocking(False); si.setblocking(False)
+
+for i in range(3):
+ tx.send(frame(i + 1)); time.sleep(0.05)
+
+m_src = i_src = None
+end = time.time() + 4
+while time.time() < end and (m_src is None or i_src is None):
+ r, _, _ = select.select([sm, si], [], [], 0.3)
+ for s in r:
+ pkt = s.recv(65535)
+ # exact flow: dst, post-strip EtherType, exact payload, min length;
+ # h_source is the asserted value and must NOT be filtered on
+ if (len(pkt) < 60 or pkt[:6] != MCAST or pkt[12:14] != b"\x08\x00"
+ or pkt[14:14 + len(PAY)] != PAY):
+ continue
+ if s is sm and m_src is None:
+ m_src = pkt[6:12]
+ elif s is si and i_src is None:
+ i_src = pkt[6:12]
+
+print("master h_source =", m_src.hex() if m_src else None)
+print("node MAC =", NODE.hex())
+print("interlink h_source =", i_src.hex() if i_src else None)
+print("redbox MAC =", RB.hex())
+if i_src != RB:
+ print("FAIL: interlink did not carry the RedBox MAC")
+ sys.exit(1)
+if m_src != NODE:
+ print("FAIL: master received %s instead of the node MAC "
+ "(shared-mutation corruption)"
+ % (m_src.hex() if m_src else "nothing"))
+ sys.exit(1)
+print("PASS: master kept node MAC, interlink kept RedBox MAC")
+sys.exit(0)
+PYF1
+}
+
+rc=0
+
+run_f2
+ret=$?
+rc=$(ksft_status_merge "$rc" "$ret")
+
+run_f1
+ret=$?
+rc=$(ksft_status_merge "$rc" "$ret")
+
+if [ "$rc" -eq 0 ]; then
+ echo "hsr_shared_mutation: per-egress mutation isolation (F1+F2) [ OK ]"
+elif [ "$rc" -eq "$ksft_skip" ]; then
+ echo "hsr_shared_mutation: subtests skipped (capabilities missing)"
+else
+ echo "hsr_shared_mutation: per-egress mutation isolation [ FAIL ]" \
+ "rc=$rc" 1>&2
+fi
+exit "$rc"
--
2.43.0
^ permalink raw reply related [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-08-08 0:45 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-08 0:45 [PATCH net v3 0/3] net: hsr: fix shared-skb mutations in the forwarding path Xin Xie
2026-08-08 0:45 ` [PATCH net v3 1/3] net: hsr: privatize interlink-bound skbs before address mutation Xin Xie
2026-08-08 0:45 ` [PATCH net v3 2/3] net: hsr: return private clones from the tagged-frame helpers Xin Xie
2026-08-08 0:45 ` [PATCH net v3 3/3] selftests: net: hsr: add shared-mutation regression test Xin Xie
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox