* [PATCH net-next 1/6] uapi: if_ether: add ETH_P_RTAG for IEEE 802.1CB R-TAG
From: Xiaoliang Yang @ 2026-07-17 9:55 UTC (permalink / raw)
To: netdev, linux-kernel, linux-kselftest
Cc: davem, edumazet, kuba, pabeni, jhs, jiri, horms, shuah,
vladimir.oltean, vinicius.gomes, fejes, xiaoliang.yang_1
In-Reply-To: <20260717095549.10565-1-xiaoliang.yang_1@nxp.com>
The IEEE 802.1CB-2017 standard defines the Redundancy Tag (R-TAG) with
EtherType 0xF1C1. Add ETH_P_RTAG to the kernel's EtherType definitions
so that it can be used by tc classifiers (e.g. cls_flower) and the FRER
tc action for stream identification on the ingress path.
Signed-off-by: Xiaoliang Yang <xiaoliang.yang_1@nxp.com>
---
include/uapi/linux/if_ether.h | 1 +
1 file changed, 1 insertion(+)
diff --git a/include/uapi/linux/if_ether.h b/include/uapi/linux/if_ether.h
index fb5efc8e06cc..2d909078cde1 100644
--- a/include/uapi/linux/if_ether.h
+++ b/include/uapi/linux/if_ether.h
@@ -122,6 +122,7 @@
#define ETH_P_DSA_8021Q 0xDADB /* Fake VLAN Header for DSA [ NOT AN OFFICIALLY REGISTERED ID ] */
#define ETH_P_DSA_A5PSW 0xE001 /* A5PSW Tag Value [ NOT AN OFFICIALLY REGISTERED ID ] */
#define ETH_P_IFE 0xED3E /* ForCES inter-FE LFB type */
+#define ETH_P_RTAG 0xF1C1 /* Redundancy Tag (IEEE 802.1CB) */
#define ETH_P_AF_IUCV 0xFBFB /* IBM af_iucv [ NOT AN OFFICIALLY REGISTERED ID ] */
#define ETH_P_NXP_NETC 0xFD3A /* NXP NETC DSA [ NOT AN OFFICIALLY REGISTERED ID ] */
--
2.17.1
^ permalink raw reply related
* [PATCH net-next 2/6] uapi: pkt_cls: add TCA_ID_FRER action identifier
From: Xiaoliang Yang @ 2026-07-17 9:55 UTC (permalink / raw)
To: netdev, linux-kernel, linux-kselftest
Cc: davem, edumazet, kuba, pabeni, jhs, jiri, horms, shuah,
vladimir.oltean, vinicius.gomes, fejes, xiaoliang.yang_1
In-Reply-To: <20260717095549.10565-1-xiaoliang.yang_1@nxp.com>
Register TCA_ID_FRER in the global tc action ID enum so that the FRER
tc action can be identified uniquely among all tc actions.
Signed-off-by: Xiaoliang Yang <xiaoliang.yang_1@nxp.com>
---
include/uapi/linux/pkt_cls.h | 1 +
1 file changed, 1 insertion(+)
diff --git a/include/uapi/linux/pkt_cls.h b/include/uapi/linux/pkt_cls.h
index 28d94b11d1aa..9b87f0455110 100644
--- a/include/uapi/linux/pkt_cls.h
+++ b/include/uapi/linux/pkt_cls.h
@@ -139,6 +139,7 @@ enum tca_id {
TCA_ID_MPLS,
TCA_ID_CT,
TCA_ID_GATE,
+ TCA_ID_FRER,
/* other actions go here */
__TCA_ID_MAX = 255
};
--
2.17.1
^ permalink raw reply related
* [PATCH net-next 3/6] uapi: tc_act: add tc_frer UAPI header
From: Xiaoliang Yang @ 2026-07-17 9:55 UTC (permalink / raw)
To: netdev, linux-kernel, linux-kselftest
Cc: davem, edumazet, kuba, pabeni, jhs, jiri, horms, shuah,
vladimir.oltean, vinicius.gomes, fejes, xiaoliang.yang_1
In-Reply-To: <20260717095549.10565-1-xiaoliang.yang_1@nxp.com>
Define the netlink attribute layout and enumerations for the FRER tc
action (IEEE 802.1CB Frame Replication and Elimination for Reliability).
The action is split into two functional sub-commands selected by the
TCA_FRER_FUNC attribute:
TCA_FRER_FUNC_PUSH - Egress: sequence number generation and R-TAG
insertion. The action inserts an R-TAG with
the current sequence number into the frame
before passing it on. When chained with
"action mirred egress mirror", the mirrored
copy already carries the R-TAG, so all
replicated frames on different egress paths
carry the same sequence number without any
additional shared state.
TCA_FRER_FUNC_RECOVER - Ingress: duplicate detection and elimination.
Multiple ingress filters can share the same
recovery state by referencing the same action
index, implementing Sequence Recovery across
ports (IEEE 802.1CB Section 7.4.2).
When TCA_FRER_RCVY_INDIVIDUAL flag is set,
the action uses private per-action state
(Individual Recovery, Section 7.5).
Statistics attributes map directly to the managed objects defined in
IEEE 802.1CB Table 10-1.
Signed-off-by: Xiaoliang Yang <xiaoliang.yang_1@nxp.com>
---
include/uapi/linux/tc_act/tc_frer.h | 89 +++++++++++++++++++++++++++++
1 file changed, 89 insertions(+)
create mode 100644 include/uapi/linux/tc_act/tc_frer.h
diff --git a/include/uapi/linux/tc_act/tc_frer.h b/include/uapi/linux/tc_act/tc_frer.h
new file mode 100644
index 000000000000..241e90827e26
--- /dev/null
+++ b/include/uapi/linux/tc_act/tc_frer.h
@@ -0,0 +1,89 @@
+/* SPDX-License-Identifier: GPL-2.0+ WITH Linux-syscall-note */
+/* Copyright 2026 NXP */
+
+#ifndef __LINUX_TC_FRER_H
+#define __LINUX_TC_FRER_H
+
+#include <linux/pkt_cls.h>
+
+/* Base parameters passed in TCA_FRER_PARMS */
+struct tc_frer {
+ tc_gen;
+};
+
+/**
+ * enum TCA_FRER_* - netlink attributes for the FRER tc action
+ *
+ * @TCA_FRER_FUNC: Functional sub-command (tc_frer_func).
+ * Mandatory.
+ * @TCA_FRER_TAG_TYPE: Redundancy tag type (tc_frer_tag_type).
+ * Mandatory.
+ *
+ * Push-specific attributes (TCA_FRER_FUNC_PUSH):
+ * Recover-specific attributes (TCA_FRER_FUNC_RECOVER):
+ * @TCA_FRER_RCVY_INDIVIDUAL: Flag. Force Individual Recovery.
+ * @TCA_FRER_RCVY_ALG: u8. Recovery algorithm (tc_frer_rcvy_alg).
+ * @TCA_FRER_RCVY_HISTORY_LEN: u8. SequenceHistory window size (1-32).
+ * Maps to frerSeqRcvyHistoryLength.
+ * @TCA_FRER_RCVY_RESET_MSEC: u32. Reset timer in milliseconds.
+ * 0 disables the timer.
+ * Maps to frerSeqRcvyResetMSec.
+ * @TCA_FRER_RCVY_TAKE_NO_SEQ: Flag. Accept frames without a redundancy
+ * tag and pass them unconditionally.
+ * Maps to frerSeqRcvyTakeNoSeq.
+ * @TCA_FRER_RCVY_TAG_POP: Flag. Remove the redundancy tag from
+ * frames that pass the recovery function.
+ *
+ * Read-only statistics (filled on dump, IEEE 802.1CB Table 10-1):
+ * @TCA_FRER_STATS_TAGLESS_PKTS: frerCpsSeqRcvyTaglessPackets
+ * @TCA_FRER_STATS_OUT_OF_ORDER_PKTS: frerCpsSeqRcvyOutOfOrderPackets
+ * @TCA_FRER_STATS_ROGUE_PKTS: frerCpsSeqRcvyRoguePackets
+ * @TCA_FRER_STATS_LOST_PKTS: frerCpsSeqRcvyLostPackets
+ * @TCA_FRER_STATS_RESETS: frerCpsSeqRcvyResets
+ * @TCA_FRER_STATS_PASSED_PKTS: frerCpsSeqRcvyPassedPackets
+ * @TCA_FRER_STATS_DISCARDED_PKTS: frerCpsSeqRcvyDiscardedPackets
+ * @TCA_FRER_STATS_SEQGEN_PKTS: frerCpsSeqGenPackets
+ */
+enum {
+ TCA_FRER_UNSPEC,
+ TCA_FRER_TM, /* struct tcf_t */
+ TCA_FRER_PARMS, /* struct tc_frer */
+ TCA_FRER_PAD,
+ TCA_FRER_FUNC, /* u8: tc_frer_func */
+ TCA_FRER_TAG_TYPE, /* u8: tc_frer_tag_type */
+ TCA_FRER_RCVY_INDIVIDUAL, /* NLA_FLAG */
+ TCA_FRER_RCVY_ALG, /* u8: tc_frer_rcvy_alg */
+ TCA_FRER_RCVY_HISTORY_LEN, /* u8: 1-32 */
+ TCA_FRER_RCVY_RESET_MSEC, /* u32 */
+ TCA_FRER_RCVY_TAKE_NO_SEQ, /* NLA_FLAG */
+ TCA_FRER_RCVY_TAG_POP, /* NLA_FLAG */
+ TCA_FRER_STATS_TAGLESS_PKTS, /* u64 */
+ TCA_FRER_STATS_OUT_OF_ORDER_PKTS, /* u64 */
+ TCA_FRER_STATS_ROGUE_PKTS, /* u64 */
+ TCA_FRER_STATS_LOST_PKTS, /* u64 */
+ TCA_FRER_STATS_RESETS, /* u64 */
+ TCA_FRER_STATS_PASSED_PKTS, /* u64 */
+ TCA_FRER_STATS_DISCARDED_PKTS, /* u64 */
+ TCA_FRER_STATS_SEQGEN_PKTS, /* u64 */
+ __TCA_FRER_MAX,
+};
+
+#define TCA_FRER_MAX (__TCA_FRER_MAX - 1)
+
+enum tc_frer_func {
+ TCA_FRER_FUNC_PUSH = 1,
+ TCA_FRER_FUNC_RECOVER = 2,
+};
+
+enum tc_frer_tag_type {
+ TCA_FRER_TAG_RTAG = 1,
+ TCA_FRER_TAG_HSR,
+ TCA_FRER_TAG_PRP,
+};
+
+enum tc_frer_rcvy_alg {
+ TCA_FRER_RCVY_VECTOR_ALG = 0, /* IEEE 802.1CB 7.4.3.4 */
+ TCA_FRER_RCVY_MATCH_ALG = 1, /* IEEE 802.1CB 7.4.3.5 */
+};
+
+#endif /* __LINUX_TC_FRER_H */
--
2.17.1
^ permalink raw reply related
* [PATCH net-next 4/6] net: sched: act_frer: add FRER tc action
From: Xiaoliang Yang @ 2026-07-17 9:55 UTC (permalink / raw)
To: netdev, linux-kernel, linux-kselftest
Cc: davem, edumazet, kuba, pabeni, jhs, jiri, horms, shuah,
vladimir.oltean, vinicius.gomes, fejes, xiaoliang.yang_1
In-Reply-To: <20260717095549.10565-1-xiaoliang.yang_1@nxp.com>
Introduce the FRER tc action for IEEE 802.1CB. This patch adds the
module skeleton, the shared sequence-generator infrastructure, the
TCA_FRER_FUNC_PUSH data path, and the TCA_FRER_FUNC_RECOVER data path.
Sequence generation (IEEE 802.1CB Section 7.4.1):
Each push action embeds a struct frer_seqgen directly in tcf_frer,
protected by a per-action spinlock. The sequence counter wraps at
65536 (16-bit R-TAG field). When a Talker chains "action frer push"
with "action mirred egress mirror", both the primary and the mirrored
frame carry the same R-TAG because mirred copies the already-modified
skb. No changes to act_mirred are required (Split function,
Section 7.7).
Sequence Recovery vs. Individual Recovery (IEEE 802.1CB Section 7.5):
Sequence Recovery (cross-port deduplication):
Multiple ingress filters on different ports share one recover
action by referencing the same action index. They all operate on
the same struct frer_rcvy embedded in that tcf_frer instance and
protected by a spinlock. A frame arriving on any port is checked
against the shared sequence history; the first copy passes and all
later copies with the same sequence number are discarded.
Individual Recovery (per-port independent deduplication):
Each action uses its own frer_rcvy embedded directly in tcf_frer.
Selected when the user sets the "individual" flag.
Recovery algorithms:
Vector (7.4.3.4, default): 32-bit history bit-vector, handles
out-of-order delivery within the window.
Match (7.4.3.5): remembers only the last accepted sequence number.
Reset timer:
An hrtimer fires after frerSeqRcvyResetMSec ms of inactivity.
CLOCK_MONOTONIC is used throughout. The reset runs in a workqueue
to avoid holding the spinlock in the hrtimer callback.
R-TAG wire format (IEEE 802.1CB 7.8, EtherType 0xF1C1):
[Dst MAC 6B][Src MAC 6B][Optional 802.1Q tag 4B][0xF1C1 2B]
[Reserved 2B][Sequence Number 2B][Encapsulated EtherType 2B][Payload]
Signed-off-by: Xiaoliang Yang <xiaoliang.yang_1@nxp.com>
---
include/net/flow_offload.h | 11 +
include/net/tc_act/tc_frer.h | 71 +++
net/sched/Kconfig | 16 +
net/sched/Makefile | 1 +
net/sched/act_frer.c | 835 +++++++++++++++++++++++++++++++++++
5 files changed, 934 insertions(+)
create mode 100644 include/net/tc_act/tc_frer.h
create mode 100644 net/sched/act_frer.c
diff --git a/include/net/flow_offload.h b/include/net/flow_offload.h
index 70a02ee14308..8d97a5f293e6 100644
--- a/include/net/flow_offload.h
+++ b/include/net/flow_offload.h
@@ -184,6 +184,7 @@ enum flow_action_id {
FLOW_ACTION_VLAN_PUSH_ETH,
FLOW_ACTION_VLAN_POP_ETH,
FLOW_ACTION_CONTINUE,
+ FLOW_ACTION_FRER,
NUM_FLOW_ACTIONS,
};
@@ -329,6 +330,16 @@ struct flow_action_entry {
struct { /* FLOW_ACTION_PPPOE_PUSH */
u16 sid;
} pppoe;
+ struct { /* FLOW_ACTION_FRER */
+ u8 func;
+ u8 tag_type;
+ bool individual;
+ u8 rcvy_alg;
+ u8 rcvy_history_len;
+ u32 rcvy_reset_msec;
+ bool tag_pop;
+ bool take_no_seq;
+ } frer;
};
struct flow_action_cookie *user_cookie; /* user defined action cookie */
};
diff --git a/include/net/tc_act/tc_frer.h b/include/net/tc_act/tc_frer.h
new file mode 100644
index 000000000000..5f6f8ca70813
--- /dev/null
+++ b/include/net/tc_act/tc_frer.h
@@ -0,0 +1,71 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright 2026 NXP */
+
+#ifndef __NET_TC_FRER_H
+#define __NET_TC_FRER_H
+
+#include <net/act_api.h>
+#include <linux/tc_act/tc_frer.h>
+
+/**
+ * struct frer_seqgen - sequence number generator state (embedded in tcf_frer)
+ */
+struct frer_seqgen {
+ u32 gen_seq_num;
+ u64 seq_space; /* 1 << 16 */
+ spinlock_t lock; /* protects frer_seqgen state */
+ u64 stats_pkts; /* frerCpsSeqGenPackets */
+};
+
+/**
+ * struct frer_rcvy - sequence recovery state (embedded in tcf_frer)
+ *
+ */
+struct frer_rcvy {
+ u8 alg;
+ u8 history_len; /* 1-32 */
+ u32 reset_msec;
+ u64 seq_space;
+ u32 rcvy_seq_num;
+ u32 seq_history;
+ bool take_any;
+ bool take_no_seq;
+ struct hrtimer hrtimer;
+ spinlock_t lock; /* protects frer_rcvy state */
+ /* statistics */
+ u64 stats_tagless_pkts;
+ u64 stats_out_of_order_pkts;
+ u64 stats_rogue_pkts;
+ u64 stats_lost_pkts;
+ u64 stats_resets;
+ u64 stats_passed_pkts;
+ u64 stats_discarded_pkts;
+};
+
+/**
+ * struct tcf_frer - per tc_action FRER private data
+ */
+struct tcf_frer {
+ struct tc_action common;
+ u8 func;
+ u8 tag_type;
+ bool tag_pop;
+ bool individual; /* Individual Recovery flag */
+ /* push path */
+ struct frer_seqgen seqgen;
+ /* recover path */
+ struct frer_rcvy rcvy;
+};
+
+#define to_frer(a) ((struct tcf_frer *)(a))
+
+static inline bool is_tcf_frer(const struct tc_action *a)
+{
+#ifdef CONFIG_NET_CLS_ACT
+ if (a->ops && a->ops->id == TCA_ID_FRER)
+ return true;
+#endif
+ return false;
+}
+
+#endif /* __NET_TC_FRER_H */
diff --git a/net/sched/Kconfig b/net/sched/Kconfig
index 6ddff028b81a..7ca79b3eb5b3 100644
--- a/net/sched/Kconfig
+++ b/net/sched/Kconfig
@@ -939,6 +939,22 @@ config NET_ACT_GATE
To compile this code as a module, choose M here: the
module will be called act_gate.
+config NET_ACT_FRER
+ tristate "IEEE 802.1CB FRER tc action"
+ depends on NET_CLS_ACT
+ help
+ Say Y here to enable the IEEE 802.1CB FRER tc action. The action
+ implements the Sequence Generation Function (egress R-TAG insertion
+ with shared per-stream sequence counter) and the Sequence Recovery
+ Function (ingress duplicate detection and elimination) described in
+ IEEE 802.1CB-2017.
+
+ Both Sequence Recovery (cross-port shared state via rcvy-id) and
+ Individual Recovery (per-port independent state) are supported.
+
+ To compile this code as a module, choose M here: the
+ module will be called act_frer.
+
config NET_IFE_SKBMARK
tristate "Support to encoding decoding skb mark on IFE action"
depends on NET_ACT_IFE
diff --git a/net/sched/Makefile b/net/sched/Makefile
index 5078ea84e6ad..d9f60434e7d7 100644
--- a/net/sched/Makefile
+++ b/net/sched/Makefile
@@ -31,6 +31,7 @@ obj-$(CONFIG_NET_IFE_SKBTCINDEX) += act_meta_skbtcindex.o
obj-$(CONFIG_NET_ACT_TUNNEL_KEY)+= act_tunnel_key.o
obj-$(CONFIG_NET_ACT_CT) += act_ct.o
obj-$(CONFIG_NET_ACT_GATE) += act_gate.o
+obj-$(CONFIG_NET_ACT_FRER) += act_frer.o
obj-$(CONFIG_NET_SCH_FIFO) += sch_fifo.o
obj-$(CONFIG_NET_SCH_HTB) += sch_htb.o
obj-$(CONFIG_NET_SCH_HFSC) += sch_hfsc.o
diff --git a/net/sched/act_frer.c b/net/sched/act_frer.c
new file mode 100644
index 000000000000..7b6db643788d
--- /dev/null
+++ b/net/sched/act_frer.c
@@ -0,0 +1,835 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/* Copyright 2026 NXP */
+
+#include <linux/module.h>
+#include <linux/types.h>
+#include <linux/kernel.h>
+#include <linux/skbuff.h>
+#include <linux/rtnetlink.h>
+#include <linux/slab.h>
+#include <linux/spinlock.h>
+#include <linux/if_vlan.h>
+#include <linux/hrtimer.h>
+#include <linux/workqueue.h>
+#include <net/act_api.h>
+#include <net/netlink.h>
+#include <net/pkt_cls.h>
+#include <net/tc_act/tc_frer.h>
+
+/* ------------------------------------------------------------------ */
+/* R-TAG wire structures (IEEE 802.1CB 7.8) */
+/* ------------------------------------------------------------------ */
+
+struct r_tag {
+ __be16 reserved;
+ __be16 sequence_nr;
+ __be16 encap_proto;
+} __packed;
+
+static struct tc_action_ops act_frer_ops;
+
+/* ------------------------------------------------------------------ */
+/* Recovery reset machinery */
+/* ------------------------------------------------------------------ */
+
+struct frer_rcvy_work {
+ struct work_struct work;
+ struct frer_rcvy *rcvy;
+};
+
+static void frer_rcvy_reset(struct frer_rcvy *rcvy)
+{
+ if (rcvy->alg == TCA_FRER_RCVY_VECTOR_ALG) {
+ rcvy->rcvy_seq_num = (u32)(rcvy->seq_space - 1);
+ rcvy->seq_history = 0;
+ }
+ rcvy->take_any = true;
+ rcvy->stats_resets++;
+}
+
+static void frer_rcvy_reset_work_fn(struct work_struct *work)
+{
+ struct frer_rcvy_work *rw =
+ container_of(work, struct frer_rcvy_work, work);
+ struct frer_rcvy *rcvy = rw->rcvy;
+
+ spin_lock_bh(&rcvy->lock);
+ frer_rcvy_reset(rcvy);
+ spin_unlock_bh(&rcvy->lock);
+ kfree(rw);
+}
+
+static enum hrtimer_restart frer_rcvy_hrtimer_fn(struct hrtimer *timer)
+{
+ struct frer_rcvy *rcvy =
+ container_of(timer, struct frer_rcvy, hrtimer);
+ struct frer_rcvy_work *rw;
+
+ /* Allocate in GFP_ATOMIC context; if it fails the state is not
+ * reset this cycle - the next frame will attempt again.
+ */
+ rw = kmalloc_obj(*rw);
+ if (rw) {
+ INIT_WORK(&rw->work, frer_rcvy_reset_work_fn);
+ rw->rcvy = rcvy;
+ schedule_work(&rw->work);
+ }
+ return HRTIMER_NORESTART;
+}
+
+static void frer_rcvy_timer_restart(struct frer_rcvy *rcvy)
+{
+ if (rcvy->reset_msec)
+ hrtimer_start(&rcvy->hrtimer,
+ ms_to_ktime(rcvy->reset_msec),
+ HRTIMER_MODE_REL_SOFT);
+}
+
+static void frer_rcvy_init_state(struct frer_rcvy *rcvy, u8 alg,
+ u8 history_len, u32 reset_msec,
+ bool take_no_seq)
+{
+ rcvy->alg = alg;
+ rcvy->history_len = history_len;
+ rcvy->reset_msec = reset_msec;
+ rcvy->seq_space = 1 << 16;
+ rcvy->take_no_seq = take_no_seq;
+ rcvy->take_any = true;
+ rcvy->rcvy_seq_num = (u32)(rcvy->seq_space - 1);
+ rcvy->seq_history = 0;
+ spin_lock_init(&rcvy->lock);
+ hrtimer_setup(&rcvy->hrtimer, frer_rcvy_hrtimer_fn, CLOCK_MONOTONIC,
+ HRTIMER_MODE_REL_SOFT);
+}
+
+/* ------------------------------------------------------------------ */
+/* R-TAG helpers */
+/* ------------------------------------------------------------------ */
+
+static int frer_rtag_push(struct sk_buff *skb, u16 seq_num)
+{
+ unsigned char *new_mac_header;
+ unsigned int data_offset;
+ unsigned int head_len;
+ struct vlan_ethhdr *vh;
+ struct ethhdr *eh;
+ struct r_tag *rtag;
+ __be16 *proto_ptr;
+ __be16 saved_proto;
+
+ if (!skb_mac_header_was_set(skb))
+ return -EINVAL;
+
+ data_offset = skb->data - skb_mac_header(skb);
+
+ if (skb_cow_head(skb, data_offset + sizeof(*rtag)))
+ return -ENOMEM;
+
+ if (data_offset > 0)
+ skb_push(skb, data_offset);
+
+ eh = eth_hdr(skb);
+ if (eth_type_vlan(eh->h_proto)) {
+ if (!pskb_may_pull(skb, sizeof(*vh)))
+ return -EINVAL;
+ eh = eth_hdr(skb);
+ vh = (struct vlan_ethhdr *)eh;
+ proto_ptr = &vh->h_vlan_encapsulated_proto;
+ head_len = sizeof(*vh);
+ } else {
+ if (!pskb_may_pull(skb, sizeof(*eh)))
+ return -EINVAL;
+ eh = eth_hdr(skb);
+ proto_ptr = &eh->h_proto;
+ head_len = sizeof(*eh);
+ }
+
+ saved_proto = *proto_ptr;
+ *proto_ptr = htons(ETH_P_RTAG);
+
+ skb_push(skb, sizeof(*rtag));
+ skb_reset_mac_header(skb);
+
+ new_mac_header = skb_mac_header(skb);
+ memmove(new_mac_header, (unsigned char *)eh, head_len);
+
+ skb->protocol = htons(ETH_P_RTAG);
+ skb_set_network_header(skb, head_len);
+ if (data_offset > 0)
+ skb_pull(skb, data_offset);
+
+ /* Write R-TAG after the Ethernet / VLAN header */
+ rtag = (struct r_tag *)(new_mac_header + head_len);
+ rtag->reserved = 0;
+ rtag->sequence_nr = htons(seq_num);
+ rtag->encap_proto = saved_proto;
+
+ return 0;
+}
+
+static void frer_rtag_pop(struct sk_buff *skb)
+{
+ unsigned char *new_mac_header;
+ unsigned int data_offset;
+ unsigned int head_len;
+ struct vlan_ethhdr *vh;
+ struct ethhdr *eh;
+ struct r_tag *rtag;
+ __be16 *proto_ptr;
+
+ data_offset = skb->data - skb_mac_header(skb);
+ if (data_offset > 0)
+ skb_push(skb, data_offset);
+
+ eh = eth_hdr(skb);
+ if (eth_type_vlan(eh->h_proto)) {
+ vh = (struct vlan_ethhdr *)eh;
+ proto_ptr = &vh->h_vlan_encapsulated_proto;
+ head_len = sizeof(*vh);
+ } else {
+ proto_ptr = &eh->h_proto;
+ head_len = sizeof(*eh);
+ }
+
+ if (*proto_ptr != htons(ETH_P_RTAG))
+ return;
+
+ rtag = (struct r_tag *)((unsigned char *)eh + head_len);
+ *proto_ptr = rtag->encap_proto;
+
+ skb->protocol = rtag->encap_proto;
+
+ skb_postpull_rcsum(skb, rtag, sizeof(struct r_tag));
+ skb_pull(skb, sizeof(*rtag));
+ skb_reset_mac_header(skb);
+
+ new_mac_header = skb_mac_header(skb);
+ memmove(new_mac_header, (unsigned char *)eh, head_len);
+
+ skb_set_network_header(skb, head_len);
+ if (data_offset > 0)
+ skb_pull(skb, data_offset);
+}
+
+static int frer_rtag_decode(struct sk_buff *skb, int *seq)
+{
+ unsigned int data_offset;
+ struct vlan_ethhdr *vh;
+ unsigned int head_len;
+ struct ethhdr *eh;
+ struct r_tag *rtag;
+ __be16 *proto_ptr;
+
+ if (!skb_mac_header_was_set(skb))
+ return -EINVAL;
+
+ data_offset = skb->data - skb_mac_header(skb);
+
+ if (skb_cow_head(skb, data_offset))
+ return -ENOMEM;
+
+ if (data_offset > 0)
+ skb_push(skb, data_offset);
+
+ eh = eth_hdr(skb);
+ if (eth_type_vlan(eh->h_proto)) {
+ if (!pskb_may_pull(skb, sizeof(*vh) + sizeof(*rtag)))
+ return -EINVAL;
+ eh = eth_hdr(skb);
+ vh = (struct vlan_ethhdr *)eh;
+ proto_ptr = &vh->h_vlan_encapsulated_proto;
+ head_len = sizeof(*vh);
+ } else {
+ if (!pskb_may_pull(skb, sizeof(*eh) + sizeof(*rtag)))
+ return -EINVAL;
+ eh = eth_hdr(skb);
+ proto_ptr = &eh->h_proto;
+ head_len = sizeof(*eh);
+ }
+
+ if (data_offset > 0)
+ skb_pull(skb, data_offset);
+
+ if (*proto_ptr != htons(ETH_P_RTAG)) {
+ *seq = -1;
+ return 0;
+ }
+
+ rtag = (struct r_tag *)((unsigned char *)eh + head_len);
+
+ *seq = (int)ntohs(rtag->sequence_nr);
+
+ return 0;
+}
+
+/* ------------------------------------------------------------------ */
+/* Recovery algorithms (called with rcvy->lock held) */
+/* ------------------------------------------------------------------ */
+
+/* Returns true = pass frame, false = discard frame.
+ * @individual: when true, restart the reset timer even on discarded frames
+ * (rogue/duplicate), as required for Individual Recovery (IEEE 802.1CB 7.5).
+ */
+static bool frer_vector_alg(struct frer_rcvy *rcvy, int seq, bool individual)
+{
+ int delta;
+ bool restart_timer = false;
+ bool pass;
+
+ if (seq < 0) {
+ /* No R-TAG present */
+ rcvy->stats_tagless_pkts++;
+ if (rcvy->take_no_seq) {
+ restart_timer = true;
+ pass = true;
+ } else {
+ pass = false;
+ }
+ goto out;
+ }
+
+ if (rcvy->take_any) {
+ /* First frame after reset: accept unconditionally */
+ rcvy->take_any = false;
+ rcvy->rcvy_seq_num = (u32)seq;
+ rcvy->seq_history = BIT(0);
+ restart_timer = true;
+ pass = true;
+ goto out;
+ }
+
+ delta = (seq - (int)rcvy->rcvy_seq_num) &
+ (int)(rcvy->seq_space - 1);
+ /* Map delta > seq_space/2 to negative (signed wrap) */
+ if ((u32)delta & (u32)(rcvy->seq_space / 2))
+ delta -= (int)rcvy->seq_space;
+
+ if (delta >= (int)rcvy->history_len ||
+ delta <= -(int)rcvy->history_len) {
+ /* Packet is out-of-range (rogue). */
+ rcvy->stats_rogue_pkts++;
+ if (individual)
+ restart_timer = true;
+ pass = false;
+ goto out;
+ }
+
+ if (delta <= 0) {
+ /* Packet is old: check whether already seen. */
+ if (rcvy->seq_history & BIT(-delta)) {
+ if (individual)
+ restart_timer = true;
+ /* Already received */
+ pass = false;
+ } else {
+ /* Out-of-order but not yet seen */
+ rcvy->seq_history |= BIT(-delta);
+ rcvy->stats_out_of_order_pkts++;
+ restart_timer = true;
+ pass = true;
+ }
+ goto out;
+ }
+
+ /* delta > 0: frame is newer than expected */
+ if (delta != 1)
+ rcvy->stats_out_of_order_pkts++;
+
+ /* Shift history forward, counting any gaps as lost */
+ while (--delta) {
+ if (!(rcvy->seq_history & BIT(rcvy->history_len - 1)))
+ rcvy->stats_lost_pkts++;
+ rcvy->seq_history <<= 1;
+ }
+ if (!(rcvy->seq_history & BIT(rcvy->history_len - 1)))
+ rcvy->stats_lost_pkts++;
+ rcvy->seq_history = (rcvy->seq_history << 1) | BIT(0);
+ rcvy->rcvy_seq_num = (u32)seq;
+ restart_timer = true;
+ pass = true;
+
+out:
+ if (restart_timer)
+ frer_rcvy_timer_restart(rcvy);
+ return pass;
+}
+
+static bool frer_match_alg(struct frer_rcvy *rcvy, int seq, bool individual)
+{
+ if (seq < 0) {
+ /* No R-TAG: Match alg cannot deduplicate, always pass. */
+ rcvy->stats_tagless_pkts++;
+ return true;
+ }
+
+ if (rcvy->take_any) {
+ rcvy->take_any = false;
+ rcvy->rcvy_seq_num = (u32)seq;
+ frer_rcvy_timer_restart(rcvy);
+ return true;
+ }
+
+ if ((u32)seq == rcvy->rcvy_seq_num) {
+ /* Duplicate */
+ if (individual)
+ frer_rcvy_timer_restart(rcvy);
+ return false;
+ }
+
+ /* New sequence number: accept and update */
+ if ((u32)seq != ((rcvy->rcvy_seq_num + 1) % rcvy->seq_space))
+ rcvy->stats_out_of_order_pkts++;
+ rcvy->rcvy_seq_num = (u32)seq;
+ frer_rcvy_timer_restart(rcvy);
+ return true;
+}
+
+/* ------------------------------------------------------------------ */
+/* Netlink policy */
+/* ------------------------------------------------------------------ */
+
+static const struct nla_policy frer_policy[TCA_FRER_MAX + 1] = {
+ [TCA_FRER_PARMS] = NLA_POLICY_EXACT_LEN(sizeof(struct tc_frer)),
+ [TCA_FRER_FUNC] = { .type = NLA_U8 },
+ [TCA_FRER_TAG_TYPE] = { .type = NLA_U8 },
+ [TCA_FRER_RCVY_INDIVIDUAL] = { .type = NLA_FLAG },
+ [TCA_FRER_RCVY_ALG] = { .type = NLA_U8 },
+ [TCA_FRER_RCVY_HISTORY_LEN] = NLA_POLICY_RANGE(NLA_U8, 1, 32),
+ [TCA_FRER_RCVY_RESET_MSEC] = { .type = NLA_U32 },
+ [TCA_FRER_RCVY_TAKE_NO_SEQ] = { .type = NLA_FLAG },
+ [TCA_FRER_RCVY_TAG_POP] = { .type = NLA_FLAG },
+};
+
+/* ------------------------------------------------------------------ */
+/* Action init */
+/* ------------------------------------------------------------------ */
+
+static int tcf_frer_init(struct net *net, struct nlattr *nla,
+ struct nlattr *est, struct tc_action **a,
+ struct tcf_proto *tp, u32 flags,
+ struct netlink_ext_ack *extack)
+{
+ struct tc_action_net *tn = net_generic(net, act_frer_ops.net_id);
+ bool bind = flags & TCA_ACT_FLAGS_BIND;
+ struct nlattr *tb[TCA_FRER_MAX + 1];
+ struct tcf_chain *goto_ch = NULL;
+ struct tcf_frer *f;
+ struct tc_frer *parm;
+ bool exists = false;
+ int ret = 0, err, index;
+ u8 func, tag_type;
+
+ if (!nla) {
+ NL_SET_ERR_MSG_MOD(extack, "frer: attributes required");
+ return -EINVAL;
+ }
+
+ err = nla_parse_nested(tb, TCA_FRER_MAX, nla, frer_policy, extack);
+ if (err < 0)
+ return err;
+
+ if (!tb[TCA_FRER_PARMS]) {
+ NL_SET_ERR_MSG_MOD(extack, "frer: TCA_FRER_PARMS missing");
+ return -EINVAL;
+ }
+ if (!tb[TCA_FRER_FUNC]) {
+ NL_SET_ERR_MSG_MOD(extack, "frer: TCA_FRER_FUNC missing");
+ return -EINVAL;
+ }
+ if (!tb[TCA_FRER_TAG_TYPE]) {
+ NL_SET_ERR_MSG_MOD(extack, "frer: TCA_FRER_TAG_TYPE missing");
+ return -EINVAL;
+ }
+
+ func = nla_get_u8(tb[TCA_FRER_FUNC]);
+ tag_type = nla_get_u8(tb[TCA_FRER_TAG_TYPE]);
+
+ if (func != TCA_FRER_FUNC_PUSH && func != TCA_FRER_FUNC_RECOVER) {
+ NL_SET_ERR_MSG_MOD(extack, "frer: unknown func");
+ return -EINVAL;
+ }
+ if (tag_type != TCA_FRER_TAG_RTAG) {
+ NL_SET_ERR_MSG_MOD(extack, "frer: only rtag supported");
+ return -EOPNOTSUPP;
+ }
+
+ parm = nla_data(tb[TCA_FRER_PARMS]);
+ index = parm->index;
+
+ err = tcf_idr_check_alloc(tn, &index, a, bind);
+ if (err < 0)
+ return err;
+ exists = err;
+
+ if (exists && bind)
+ return ACT_P_BOUND;
+
+ if (!exists) {
+ ret = tcf_idr_create_from_flags(tn, index, est, a,
+ &act_frer_ops, bind, flags);
+ if (ret) {
+ tcf_idr_cleanup(tn, index);
+ return ret;
+ }
+ ret = ACT_P_CREATED;
+ } else if (!(flags & TCA_ACT_FLAGS_REPLACE)) {
+ tcf_idr_release(*a, bind);
+ return -EEXIST;
+ }
+
+ err = tcf_action_check_ctrlact(parm->action, tp, &goto_ch, extack);
+ if (err < 0)
+ goto release_idr;
+
+ f = to_frer(*a);
+
+ spin_lock_bh(&f->tcf_lock);
+ goto_ch = tcf_action_set_ctrlact(*a, parm->action, goto_ch);
+ f->func = func;
+ f->tag_type = tag_type;
+ f->tag_pop = !!tb[TCA_FRER_RCVY_TAG_POP];
+
+ if (func == TCA_FRER_FUNC_PUSH) {
+ if (ret == ACT_P_CREATED) {
+ spin_lock_init(&f->seqgen.lock);
+ f->seqgen.seq_space = 1 << 16;
+ }
+ /* gen_seq_num starts at 0 on creation; preserved on replace */
+ } else {
+ u8 alg = tb[TCA_FRER_RCVY_ALG] ?
+ nla_get_u8(tb[TCA_FRER_RCVY_ALG]) :
+ TCA_FRER_RCVY_VECTOR_ALG;
+ u8 history_len = tb[TCA_FRER_RCVY_HISTORY_LEN] ?
+ nla_get_u8(tb[TCA_FRER_RCVY_HISTORY_LEN]) : 32;
+ u32 reset_msec = tb[TCA_FRER_RCVY_RESET_MSEC] ?
+ nla_get_u32(tb[TCA_FRER_RCVY_RESET_MSEC]) : 0;
+ bool take_no_seq = !!tb[TCA_FRER_RCVY_TAKE_NO_SEQ];
+
+ if (alg != TCA_FRER_RCVY_VECTOR_ALG &&
+ alg != TCA_FRER_RCVY_MATCH_ALG) {
+ spin_unlock_bh(&f->tcf_lock);
+ NL_SET_ERR_MSG_MOD(extack, "frer: unknown recovery algorithm");
+ err = -EINVAL;
+ goto release_idr;
+ }
+
+ f->individual = !!tb[TCA_FRER_RCVY_INDIVIDUAL];
+
+ /* Cancel any running reset timer before re-initialising. */
+ if (ret != ACT_P_CREATED && f->rcvy.reset_msec) {
+ spin_unlock_bh(&f->tcf_lock);
+ hrtimer_cancel(&f->rcvy.hrtimer);
+ spin_lock_bh(&f->tcf_lock);
+ }
+
+ frer_rcvy_init_state(&f->rcvy, alg, history_len,
+ reset_msec, take_no_seq);
+ }
+
+ spin_unlock_bh(&f->tcf_lock);
+
+ if (goto_ch)
+ tcf_chain_put_by_act(goto_ch);
+
+ return ret;
+
+release_idr:
+ tcf_idr_release(*a, bind);
+ return err;
+}
+
+/* ------------------------------------------------------------------ */
+/* Data path */
+/* ------------------------------------------------------------------ */
+
+static int tcf_frer_act(struct sk_buff *skb, const struct tc_action *a,
+ struct tcf_result *res)
+{
+ struct tcf_frer *f = to_frer(a);
+ int retval;
+
+ tcf_lastuse_update(&f->tcf_tm);
+ tcf_action_update_bstats(&f->common, skb);
+ retval = READ_ONCE(f->tcf_action);
+
+ if (f->func == TCA_FRER_FUNC_PUSH) {
+ struct frer_seqgen *sg = &f->seqgen;
+ u16 seq;
+
+ spin_lock(&sg->lock);
+ seq = (u16)sg->gen_seq_num;
+ if (++sg->gen_seq_num >= sg->seq_space)
+ sg->gen_seq_num = 0;
+ sg->stats_pkts++;
+ spin_unlock(&sg->lock);
+
+ if (frer_rtag_push(skb, seq) < 0) {
+ tcf_action_inc_drop_qstats(&f->common);
+ return TC_ACT_SHOT;
+ }
+ } else {
+ struct frer_rcvy *rcvy = &f->rcvy;
+ bool pass;
+ int seq;
+
+ if (frer_rtag_decode(skb, &seq) < 0) {
+ tcf_action_inc_drop_qstats(&f->common);
+ return TC_ACT_SHOT;
+ }
+
+ spin_lock(&rcvy->lock);
+ if (rcvy->alg == TCA_FRER_RCVY_VECTOR_ALG)
+ pass = frer_vector_alg(rcvy, seq, f->individual);
+ else
+ pass = frer_match_alg(rcvy, seq, f->individual);
+
+ if (pass) {
+ rcvy->stats_passed_pkts++;
+ spin_unlock(&rcvy->lock);
+ if (f->tag_pop)
+ frer_rtag_pop(skb);
+ return retval;
+ }
+
+ rcvy->stats_discarded_pkts++;
+ spin_unlock(&rcvy->lock);
+ return TC_ACT_SHOT;
+ }
+
+ return retval;
+}
+
+/* ------------------------------------------------------------------ */
+/* Dump */
+/* ------------------------------------------------------------------ */
+
+static int tcf_frer_dump(struct sk_buff *skb, struct tc_action *a,
+ int bind, int ref)
+{
+ unsigned char *b = skb_tail_pointer(skb);
+ struct tcf_frer *f = to_frer(a);
+ struct tc_frer opt = {
+ .index = f->tcf_index,
+ .refcnt = refcount_read(&f->tcf_refcnt) - ref,
+ .bindcnt = atomic_read(&f->tcf_bindcnt) - bind,
+ };
+ struct tcf_t t;
+
+ spin_lock_bh(&f->tcf_lock);
+ opt.action = f->tcf_action;
+
+ if (nla_put(skb, TCA_FRER_PARMS, sizeof(opt), &opt))
+ goto nla_put_failure;
+ if (nla_put_u8(skb, TCA_FRER_FUNC, f->func))
+ goto nla_put_failure;
+ if (nla_put_u8(skb, TCA_FRER_TAG_TYPE, f->tag_type))
+ goto nla_put_failure;
+ if (f->tag_pop && nla_put_flag(skb, TCA_FRER_RCVY_TAG_POP))
+ goto nla_put_failure;
+
+ if (f->func == TCA_FRER_FUNC_PUSH) {
+ spin_lock(&f->seqgen.lock);
+ if (nla_put_u64_64bit(skb, TCA_FRER_STATS_SEQGEN_PKTS,
+ f->seqgen.stats_pkts, TCA_FRER_PAD)) {
+ spin_unlock(&f->seqgen.lock);
+ goto nla_put_failure;
+ }
+ spin_unlock(&f->seqgen.lock);
+ } else {
+ u64 tagless, ooo, rogue, lost, resets, passed, discarded;
+ struct frer_rcvy *rcvy = &f->rcvy;
+
+ spin_lock(&rcvy->lock);
+ tagless = rcvy->stats_tagless_pkts;
+ ooo = rcvy->stats_out_of_order_pkts;
+ rogue = rcvy->stats_rogue_pkts;
+ lost = rcvy->stats_lost_pkts;
+ resets = rcvy->stats_resets;
+ passed = rcvy->stats_passed_pkts;
+ discarded = rcvy->stats_discarded_pkts;
+ spin_unlock(&rcvy->lock);
+
+ if (f->individual && nla_put_flag(skb, TCA_FRER_RCVY_INDIVIDUAL))
+ goto nla_put_failure;
+ if (nla_put_u8(skb, TCA_FRER_RCVY_ALG, rcvy->alg))
+ goto nla_put_failure;
+ if (nla_put_u8(skb, TCA_FRER_RCVY_HISTORY_LEN, rcvy->history_len))
+ goto nla_put_failure;
+ if (nla_put_u32(skb, TCA_FRER_RCVY_RESET_MSEC, rcvy->reset_msec))
+ goto nla_put_failure;
+ if (rcvy->take_no_seq && nla_put_flag(skb, TCA_FRER_RCVY_TAKE_NO_SEQ))
+ goto nla_put_failure;
+ if (nla_put_u64_64bit(skb, TCA_FRER_STATS_TAGLESS_PKTS,
+ tagless, TCA_FRER_PAD))
+ goto nla_put_failure;
+ if (nla_put_u64_64bit(skb, TCA_FRER_STATS_OUT_OF_ORDER_PKTS,
+ ooo, TCA_FRER_PAD))
+ goto nla_put_failure;
+ if (nla_put_u64_64bit(skb, TCA_FRER_STATS_ROGUE_PKTS,
+ rogue, TCA_FRER_PAD))
+ goto nla_put_failure;
+ if (nla_put_u64_64bit(skb, TCA_FRER_STATS_LOST_PKTS,
+ lost, TCA_FRER_PAD))
+ goto nla_put_failure;
+ if (nla_put_u64_64bit(skb, TCA_FRER_STATS_RESETS,
+ resets, TCA_FRER_PAD))
+ goto nla_put_failure;
+ if (nla_put_u64_64bit(skb, TCA_FRER_STATS_PASSED_PKTS,
+ passed, TCA_FRER_PAD))
+ goto nla_put_failure;
+ if (nla_put_u64_64bit(skb, TCA_FRER_STATS_DISCARDED_PKTS,
+ discarded, TCA_FRER_PAD))
+ goto nla_put_failure;
+ }
+
+ tcf_tm_dump(&t, &f->tcf_tm);
+ if (nla_put_64bit(skb, TCA_FRER_TM, sizeof(t), &t, TCA_FRER_PAD))
+ goto nla_put_failure;
+
+ spin_unlock_bh(&f->tcf_lock);
+ return skb->len;
+
+nla_put_failure:
+ spin_unlock_bh(&f->tcf_lock);
+ nlmsg_trim(skb, b);
+ return -1;
+}
+
+/* ------------------------------------------------------------------ */
+/* Cleanup */
+/* ------------------------------------------------------------------ */
+
+static void tcf_frer_cleanup(struct tc_action *a)
+{
+ struct tcf_frer *f = to_frer(a);
+
+ if (f->func == TCA_FRER_FUNC_RECOVER)
+ hrtimer_cancel(&f->rcvy.hrtimer);
+}
+
+/* ------------------------------------------------------------------ */
+/* Walker / search / stats / fill-size / offload */
+/* ------------------------------------------------------------------ */
+
+static int tcf_frer_walker(struct net *net, struct sk_buff *skb,
+ struct netlink_callback *cb, int type,
+ const struct tc_action_ops *ops,
+ struct netlink_ext_ack *extack)
+{
+ struct tc_action_net *tn = net_generic(net, act_frer_ops.net_id);
+
+ return tcf_generic_walker(tn, skb, cb, type, ops, extack);
+}
+
+static void tcf_frer_stats_update(struct tc_action *a, u64 bytes, u64 packets,
+ u64 drops, u64 lastuse, bool hw)
+{
+ struct tcf_frer *f = to_frer(a);
+ struct tcf_t *tm = &f->tcf_tm;
+
+ tcf_action_update_stats(a, bytes, packets, drops, hw);
+ tm->lastuse = max_t(u64, tm->lastuse, lastuse);
+}
+
+static size_t tcf_frer_get_fill_size(const struct tc_action *act)
+{
+ return nla_total_size(sizeof(struct tc_frer)) /* TCA_FRER_PARMS */
+ + nla_total_size(sizeof(u8)) /* TCA_FRER_FUNC */
+ + nla_total_size(sizeof(u8)) /* TCA_FRER_TAG_TYPE */
+ + nla_total_size(0) /* TCA_FRER_RCVY_TAG_POP (flag) */
+ + nla_total_size(0) /* TCA_FRER_RCVY_INDIVIDUAL (flag) */
+ + nla_total_size(sizeof(u8)) /* TCA_FRER_RCVY_ALG */
+ + nla_total_size(sizeof(u8)) /* TCA_FRER_RCVY_HISTORY_LEN */
+ + nla_total_size(sizeof(u32)) /* TCA_FRER_RCVY_RESET_MSEC */
+ + nla_total_size(0) /* TCA_FRER_RCVY_TAKE_NO_SEQ (flag) */
+ + nla_total_size_64bit(sizeof(u64)) /* TCA_FRER_STATS_TAGLESS_PKTS */
+ + nla_total_size_64bit(sizeof(u64)) /* TCA_FRER_STATS_OUT_OF_ORDER_PKTS */
+ + nla_total_size_64bit(sizeof(u64)) /* TCA_FRER_STATS_ROGUE_PKTS */
+ + nla_total_size_64bit(sizeof(u64)) /* TCA_FRER_STATS_LOST_PKTS */
+ + nla_total_size_64bit(sizeof(u64)) /* TCA_FRER_STATS_RESETS */
+ + nla_total_size_64bit(sizeof(u64)) /* TCA_FRER_STATS_PASSED_PKTS */
+ + nla_total_size_64bit(sizeof(u64)) /* TCA_FRER_STATS_DISCARDED_PKTS */
+ + nla_total_size_64bit(sizeof(struct tcf_t)); /* TCA_FRER_TM */
+}
+
+static int tcf_frer_offload_act_setup(struct tc_action *act, void *entry_data,
+ u32 *index_inc, bool bind,
+ struct netlink_ext_ack *extack)
+{
+ if (bind) {
+ struct flow_action_entry *entry = entry_data;
+ struct tcf_frer *f = to_frer(act);
+
+ entry->id = FLOW_ACTION_FRER;
+ entry->frer.func = f->func;
+ entry->frer.tag_type = f->tag_type;
+ entry->frer.tag_pop = f->tag_pop;
+
+ if (f->func != TCA_FRER_FUNC_PUSH) {
+ entry->frer.individual = f->individual;
+ entry->frer.rcvy_alg = f->rcvy.alg;
+ entry->frer.rcvy_history_len = f->rcvy.history_len;
+ entry->frer.rcvy_reset_msec = f->rcvy.reset_msec;
+ entry->frer.take_no_seq = f->rcvy.take_no_seq;
+ }
+ *index_inc = 1;
+ } else {
+ struct flow_offload_action *fl_action = entry_data;
+
+ fl_action->id = FLOW_ACTION_FRER;
+ }
+ return 0;
+}
+
+/* ------------------------------------------------------------------ */
+/* Module glue */
+/* ------------------------------------------------------------------ */
+
+static struct tc_action_ops act_frer_ops = {
+ .kind = "frer",
+ .id = TCA_ID_FRER,
+ .owner = THIS_MODULE,
+ .act = tcf_frer_act,
+ .init = tcf_frer_init,
+ .cleanup = tcf_frer_cleanup,
+ .dump = tcf_frer_dump,
+ .walk = tcf_frer_walker,
+ .stats_update = tcf_frer_stats_update,
+ .get_fill_size = tcf_frer_get_fill_size,
+ .offload_act_setup = tcf_frer_offload_act_setup,
+ .size = sizeof(struct tcf_frer),
+};
+
+static __net_init int frer_init_net(struct net *net)
+{
+ struct tc_action_net *tn = net_generic(net, act_frer_ops.net_id);
+
+ return tc_action_net_init(net, tn, &act_frer_ops);
+}
+
+static void __net_exit frer_exit_net(struct list_head *net_list)
+{
+ tc_action_net_exit(net_list, act_frer_ops.net_id);
+}
+
+static struct pernet_operations frer_net_ops = {
+ .init = frer_init_net,
+ .exit_batch = frer_exit_net,
+ .id = &act_frer_ops.net_id,
+ .size = sizeof(struct tc_action_net),
+};
+
+static int __init frer_init_module(void)
+{
+ return tcf_register_action(&act_frer_ops, &frer_net_ops);
+}
+
+static void __exit frer_cleanup_module(void)
+{
+ tcf_unregister_action(&act_frer_ops, &frer_net_ops);
+}
+
+module_init(frer_init_module);
+module_exit(frer_cleanup_module);
+MODULE_LICENSE("GPL");
+MODULE_DESCRIPTION("IEEE 802.1CB FRER tc action");
--
2.17.1
^ permalink raw reply related
* [PATCH net-next 5/6] selftest: add tc-testing JSON test cases for act_frer
From: Xiaoliang Yang @ 2026-07-17 9:55 UTC (permalink / raw)
To: netdev, linux-kernel, linux-kselftest
Cc: davem, edumazet, kuba, pabeni, jhs, jiri, horms, shuah,
vladimir.oltean, vinicius.gomes, fejes, xiaoliang.yang_1
In-Reply-To: <20260717095549.10565-1-xiaoliang.yang_1@nxp.com>
Add a tc-testing JSON file covering the FRER (IEEE 802.1CB Frame
Replication and Elimination for Reliability) tc action (act_frer).
The test suite contains 32 test cases and exercises:
- Creating push and recover actions with default and explicit parameters
(tag-type, alg vector/match, history-length, reset-time, tag-pop,
individual, take-no-seq)
- Boundary values for history-length (1 and 32) and reset-time (0)
- Combining multiple flags (frer_0011, frer_0012)
- Statistics output format for push (SeqGen) and recover (passed,
discarded, tagless, out-of-order, rogue, lost, resets)
- Replace and delete operations
- Flush all actions
- Duplicate-index failure (expExitCode 255)
- Control actions (continue, pipe) placed after the index token
- Binding push and recover actions to egress/ingress clsact filters
- Sharing a recover action across two filters and verifying the
reference count increments
- not_in_hw flag present in show output
Signed-off-by: Xiaoliang Yang <xiaoliang.yang_1@nxp.com>
---
.../tc-testing/tc-tests/actions/frer.json | 785 ++++++++++++++++++
1 file changed, 785 insertions(+)
create mode 100644 tools/testing/selftests/tc-testing/tc-tests/actions/frer.json
diff --git a/tools/testing/selftests/tc-testing/tc-tests/actions/frer.json b/tools/testing/selftests/tc-testing/tc-tests/actions/frer.json
new file mode 100644
index 000000000000..d5be6ae156f7
--- /dev/null
+++ b/tools/testing/selftests/tc-testing/tc-tests/actions/frer.json
@@ -0,0 +1,785 @@
+[
+ {
+ "id": "frer_0001",
+ "name": "Create frer push action with default parameters",
+ "category": [
+ "actions",
+ "frer"
+ ],
+ "setup": [
+ [
+ "modprobe act_frer",
+ 0,
+ 1
+ ]
+ ],
+ "cmdUnderTest": "$TC actions add action frer push index 1",
+ "expExitCode": "0",
+ "verifyCmd": "$TC actions show action frer index 1",
+ "matchPattern": "action order [0-9]+: frer push tag-type rtag index 1",
+ "matchCount": "1",
+ "teardown": [
+ "$TC actions flush action frer"
+ ]
+ },
+ {
+ "id": "frer_0002",
+ "name": "Create frer push action with explicit tag-type rtag",
+ "category": [
+ "actions",
+ "frer"
+ ],
+ "setup": [
+ [
+ "modprobe act_frer",
+ 0,
+ 1
+ ]
+ ],
+ "cmdUnderTest": "$TC actions add action frer push tag-type rtag index 2",
+ "expExitCode": "0",
+ "verifyCmd": "$TC actions show action frer index 2",
+ "matchPattern": "action order [0-9]+: frer push tag-type rtag index 2",
+ "matchCount": "1",
+ "teardown": [
+ "$TC actions flush action frer"
+ ]
+ },
+ {
+ "id": "frer_0003",
+ "name": "Create frer recover action with default parameters",
+ "category": [
+ "actions",
+ "frer"
+ ],
+ "setup": [
+ [
+ "modprobe act_frer",
+ 0,
+ 1
+ ]
+ ],
+ "cmdUnderTest": "$TC actions add action frer recover index 10",
+ "expExitCode": "0",
+ "verifyCmd": "$TC actions show action frer index 10",
+ "matchPattern": "action order [0-9]+: frer recover tag-type rtag index 10 alg vector history-length [0-9]+ reset-time [0-9]+",
+ "matchCount": "1",
+ "teardown": [
+ "$TC actions flush action frer"
+ ]
+ },
+ {
+ "id": "frer_0004",
+ "name": "Create frer recover action with vector algorithm explicit",
+ "category": [
+ "actions",
+ "frer"
+ ],
+ "setup": [
+ [
+ "modprobe act_frer",
+ 0,
+ 1
+ ]
+ ],
+ "cmdUnderTest": "$TC actions add action frer recover alg vector index 11",
+ "expExitCode": "0",
+ "verifyCmd": "$TC actions show action frer index 11",
+ "matchPattern": "action order [0-9]+: frer recover tag-type rtag index 11 alg vector",
+ "matchCount": "1",
+ "teardown": [
+ "$TC actions flush action frer"
+ ]
+ },
+ {
+ "id": "frer_0005",
+ "name": "Create frer recover action with match algorithm",
+ "category": [
+ "actions",
+ "frer"
+ ],
+ "setup": [
+ [
+ "modprobe act_frer",
+ 0,
+ 1
+ ]
+ ],
+ "cmdUnderTest": "$TC actions add action frer recover alg match index 12",
+ "expExitCode": "0",
+ "verifyCmd": "$TC actions show action frer index 12",
+ "matchPattern": "action order [0-9]+: frer recover tag-type rtag index 12 alg match",
+ "matchCount": "1",
+ "teardown": [
+ "$TC actions flush action frer"
+ ]
+ },
+ {
+ "id": "frer_0006",
+ "name": "Create frer recover action with history-length 16",
+ "category": [
+ "actions",
+ "frer"
+ ],
+ "setup": [
+ [
+ "modprobe act_frer",
+ 0,
+ 1
+ ]
+ ],
+ "cmdUnderTest": "$TC actions add action frer recover alg vector history-length 16 index 13",
+ "expExitCode": "0",
+ "verifyCmd": "$TC actions show action frer index 13",
+ "matchPattern": "action order [0-9]+: frer recover tag-type rtag index 13 alg vector history-length 16",
+ "matchCount": "1",
+ "teardown": [
+ "$TC actions flush action frer"
+ ]
+ },
+ {
+ "id": "frer_0007",
+ "name": "Create frer recover action with reset-time 2000",
+ "category": [
+ "actions",
+ "frer"
+ ],
+ "setup": [
+ [
+ "modprobe act_frer",
+ 0,
+ 1
+ ]
+ ],
+ "cmdUnderTest": "$TC actions add action frer recover alg vector reset-time 2000 index 14",
+ "expExitCode": "0",
+ "verifyCmd": "$TC actions show action frer index 14",
+ "matchPattern": "action order [0-9]+: frer recover tag-type rtag index 14 alg vector history-length [0-9]+ reset-time 2000",
+ "matchCount": "1",
+ "teardown": [
+ "$TC actions flush action frer"
+ ]
+ },
+ {
+ "id": "frer_0008",
+ "name": "Create frer recover action with tag-pop flag",
+ "category": [
+ "actions",
+ "frer"
+ ],
+ "setup": [
+ [
+ "modprobe act_frer",
+ 0,
+ 1
+ ]
+ ],
+ "cmdUnderTest": "$TC actions add action frer recover tag-pop index 15",
+ "expExitCode": "0",
+ "verifyCmd": "$TC actions show action frer index 15",
+ "matchPattern": "action order [0-9]+: frer recover tag-type rtag index 15.*tag-pop",
+ "matchCount": "1",
+ "teardown": [
+ "$TC actions flush action frer"
+ ]
+ },
+ {
+ "id": "frer_0009",
+ "name": "Create frer recover action with individual flag",
+ "category": [
+ "actions",
+ "frer"
+ ],
+ "setup": [
+ [
+ "modprobe act_frer",
+ 0,
+ 1
+ ]
+ ],
+ "cmdUnderTest": "$TC actions add action frer recover individual index 16",
+ "expExitCode": "0",
+ "verifyCmd": "$TC actions show action frer index 16",
+ "matchPattern": "action order [0-9]+: frer recover tag-type rtag index 16.*individual",
+ "matchCount": "1",
+ "teardown": [
+ "$TC actions flush action frer"
+ ]
+ },
+ {
+ "id": "frer_0010",
+ "name": "Create frer recover action with take-no-seq flag",
+ "category": [
+ "actions",
+ "frer"
+ ],
+ "setup": [
+ [
+ "modprobe act_frer",
+ 0,
+ 1
+ ]
+ ],
+ "cmdUnderTest": "$TC actions add action frer recover take-no-seq index 17",
+ "expExitCode": "0",
+ "verifyCmd": "$TC actions show action frer index 17",
+ "matchPattern": "action order [0-9]+: frer recover tag-type rtag index 17.*take-no-seq",
+ "matchCount": "1",
+ "teardown": [
+ "$TC actions flush action frer"
+ ]
+ },
+ {
+ "id": "frer_0011",
+ "name": "Create frer recover action with all parameters combined",
+ "category": [
+ "actions",
+ "frer"
+ ],
+ "setup": [
+ [
+ "modprobe act_frer",
+ 0,
+ 1
+ ]
+ ],
+ "cmdUnderTest": "$TC actions add action frer recover alg vector history-length 16 reset-time 1000 tag-pop individual index 20",
+ "expExitCode": "0",
+ "verifyCmd": "$TC actions show action frer index 20",
+ "matchPattern": "action order [0-9]+: frer recover tag-type rtag index 20 individual alg vector history-length 16 reset-time 1000 tag-pop",
+ "matchCount": "1",
+ "teardown": [
+ "$TC actions flush action frer"
+ ]
+ },
+ {
+ "id": "frer_0012",
+ "name": "Create frer recover action with match alg and all flags",
+ "category": [
+ "actions",
+ "frer"
+ ],
+ "setup": [
+ [
+ "modprobe act_frer",
+ 0,
+ 1
+ ]
+ ],
+ "cmdUnderTest": "$TC actions add action frer recover alg match take-no-seq tag-pop individual index 21",
+ "expExitCode": "0",
+ "verifyCmd": "$TC actions show action frer index 21",
+ "matchPattern": "action order [0-9]+: frer recover tag-type rtag index 21 individual alg match history-length [0-9]+ reset-time [0-9]+ tag-pop take-no-seq",
+ "matchCount": "1",
+ "teardown": [
+ "$TC actions flush action frer"
+ ]
+ },
+ {
+ "id": "frer_0013",
+ "name": "Show frer push action SeqGen statistics (zero after create)",
+ "category": [
+ "actions",
+ "frer"
+ ],
+ "setup": [
+ [
+ "modprobe act_frer",
+ 0,
+ 1
+ ],
+ "$TC actions add action frer push index 1"
+ ],
+ "cmdUnderTest": "$TC -s actions show action frer index 1",
+ "expExitCode": "0",
+ "verifyCmd": "$TC -s actions show action frer index 1",
+ "matchPattern": "SeqGen packets: 0",
+ "matchCount": "1",
+ "teardown": [
+ "$TC actions flush action frer"
+ ]
+ },
+ {
+ "id": "frer_0014",
+ "name": "Show frer recover action Statistics line (zero after create)",
+ "category": [
+ "actions",
+ "frer"
+ ],
+ "setup": [
+ [
+ "modprobe act_frer",
+ 0,
+ 1
+ ],
+ "$TC actions add action frer recover alg vector history-length 16 reset-time 1000 tag-pop index 10"
+ ],
+ "cmdUnderTest": "$TC -s actions show action frer index 10",
+ "expExitCode": "0",
+ "verifyCmd": "$TC -s actions show action frer index 10",
+ "matchPattern": "Statistics: passed=0 discarded=0 tagless=0 out-of-order=0 rogue=0 lost=0 resets=0",
+ "matchCount": "1",
+ "teardown": [
+ "$TC actions flush action frer"
+ ]
+ },
+ {
+ "id": "frer_0015",
+ "name": "Show frer recover action Statistics fields present",
+ "category": [
+ "actions",
+ "frer"
+ ],
+ "setup": [
+ [
+ "modprobe act_frer",
+ 0,
+ 1
+ ],
+ "$TC actions add action frer recover index 10"
+ ],
+ "cmdUnderTest": "$TC -s actions show action frer index 10",
+ "expExitCode": "0",
+ "verifyCmd": "$TC -s actions show action frer index 10",
+ "matchPattern": "Statistics: passed=[0-9]+ discarded=[0-9]+ tagless=[0-9]+ out-of-order=[0-9]+ rogue=[0-9]+ lost=[0-9]+ resets=[0-9]+",
+ "matchCount": "1",
+ "teardown": [
+ "$TC actions flush action frer"
+ ]
+ },
+ {
+ "id": "frer_0016",
+ "name": "Replace frer push action (same index)",
+ "category": [
+ "actions",
+ "frer"
+ ],
+ "setup": [
+ [
+ "modprobe act_frer",
+ 0,
+ 1
+ ],
+ "$TC actions add action frer push index 1"
+ ],
+ "cmdUnderTest": "$TC actions replace action frer push index 1",
+ "expExitCode": "0",
+ "verifyCmd": "$TC actions show action frer index 1",
+ "matchPattern": "action order [0-9]+: frer push tag-type rtag index 1",
+ "matchCount": "1",
+ "teardown": [
+ "$TC actions flush action frer"
+ ]
+ },
+ {
+ "id": "frer_0017",
+ "name": "Replace frer recover action changing algorithm from vector to match",
+ "category": [
+ "actions",
+ "frer"
+ ],
+ "setup": [
+ [
+ "modprobe act_frer",
+ 0,
+ 1
+ ],
+ "$TC actions add action frer recover alg vector index 10"
+ ],
+ "cmdUnderTest": "$TC actions replace action frer recover alg match index 10",
+ "expExitCode": "0",
+ "verifyCmd": "$TC actions show action frer index 10",
+ "matchPattern": "action order [0-9]+: frer recover tag-type rtag index 10 alg match",
+ "matchCount": "1",
+ "teardown": [
+ "$TC actions flush action frer"
+ ]
+ },
+ {
+ "id": "frer_0018",
+ "name": "Delete frer push action by index",
+ "category": [
+ "actions",
+ "frer"
+ ],
+ "setup": [
+ [
+ "modprobe act_frer",
+ 0,
+ 1
+ ],
+ "$TC actions add action frer push index 1"
+ ],
+ "cmdUnderTest": "$TC actions del action frer index 1",
+ "expExitCode": "0",
+ "verifyCmd": "$TC actions show action frer",
+ "matchPattern": "frer push tag-type rtag index 1",
+ "matchCount": "0",
+ "teardown": [
+ "$TC actions flush action frer"
+ ]
+ },
+ {
+ "id": "frer_0019",
+ "name": "Flush all frer actions",
+ "category": [
+ "actions",
+ "frer"
+ ],
+ "setup": [
+ [
+ "modprobe act_frer",
+ 0,
+ 1
+ ],
+ "$TC actions add action frer push index 1",
+ "$TC actions add action frer recover index 10",
+ "$TC actions add action frer recover index 11"
+ ],
+ "cmdUnderTest": "$TC actions flush action frer",
+ "expExitCode": "0",
+ "verifyCmd": "$TC actions show action frer",
+ "matchPattern": "action order [0-9]+: frer",
+ "matchCount": "0",
+ "teardown": [
+ "$TC actions flush action frer 2>/dev/null || true"
+ ]
+ },
+ {
+ "id": "frer_0020",
+ "name": "Add duplicate frer action index fails without replace flag",
+ "category": [
+ "actions",
+ "frer"
+ ],
+ "setup": [
+ [
+ "modprobe act_frer",
+ 0,
+ 1
+ ],
+ "$TC actions add action frer push index 1"
+ ],
+ "cmdUnderTest": "$TC actions add action frer push index 1",
+ "expExitCode": "255",
+ "verifyCmd": "$TC actions show action frer index 1",
+ "matchPattern": "action order [0-9]+: frer push tag-type rtag index 1",
+ "matchCount": "1",
+ "teardown": [
+ "$TC actions flush action frer"
+ ]
+ },
+ {
+ "id": "frer_0021",
+ "name": "Create frer push action with continue control action",
+ "category": [
+ "actions",
+ "frer"
+ ],
+ "setup": [
+ [
+ "modprobe act_frer",
+ 0,
+ 1
+ ]
+ ],
+ "cmdUnderTest": "$TC actions add action frer push index 1 continue",
+ "expExitCode": "0",
+ "verifyCmd": "$TC actions show action frer index 1",
+ "matchPattern": "action order [0-9]+: frer push tag-type rtag index 1.*control continue",
+ "matchCount": "1",
+ "teardown": [
+ "$TC actions flush action frer"
+ ]
+ },
+ {
+ "id": "frer_0022",
+ "name": "Create frer recover action with pipe control action",
+ "category": [
+ "actions",
+ "frer"
+ ],
+ "setup": [
+ [
+ "modprobe act_frer",
+ 0,
+ 1
+ ]
+ ],
+ "cmdUnderTest": "$TC actions add action frer recover index 10 pipe",
+ "expExitCode": "0",
+ "verifyCmd": "$TC actions show action frer index 10",
+ "matchPattern": "action order [0-9]+: frer recover tag-type rtag index 10.*control pipe",
+ "matchCount": "1",
+ "teardown": [
+ "$TC actions flush action frer"
+ ]
+ },
+ {
+ "id": "frer_0023",
+ "name": "Create frer recover action history-length minimum boundary (1)",
+ "category": [
+ "actions",
+ "frer"
+ ],
+ "setup": [
+ [
+ "modprobe act_frer",
+ 0,
+ 1
+ ]
+ ],
+ "cmdUnderTest": "$TC actions add action frer recover alg vector history-length 1 index 30",
+ "expExitCode": "0",
+ "verifyCmd": "$TC actions show action frer index 30",
+ "matchPattern": "action order [0-9]+: frer recover tag-type rtag index 30 alg vector history-length 1",
+ "matchCount": "1",
+ "teardown": [
+ "$TC actions flush action frer"
+ ]
+ },
+ {
+ "id": "frer_0024",
+ "name": "Create frer recover action history-length maximum boundary (32)",
+ "category": [
+ "actions",
+ "frer"
+ ],
+ "setup": [
+ [
+ "modprobe act_frer",
+ 0,
+ 1
+ ]
+ ],
+ "cmdUnderTest": "$TC actions add action frer recover alg vector history-length 32 index 31",
+ "expExitCode": "0",
+ "verifyCmd": "$TC actions show action frer index 31",
+ "matchPattern": "action order [0-9]+: frer recover tag-type rtag index 31 alg vector history-length 32",
+ "matchCount": "1",
+ "teardown": [
+ "$TC actions flush action frer"
+ ]
+ },
+ {
+ "id": "frer_0025",
+ "name": "Create frer recover action with reset-time 0 (timer disabled)",
+ "category": [
+ "actions",
+ "frer"
+ ],
+ "setup": [
+ [
+ "modprobe act_frer",
+ 0,
+ 1
+ ]
+ ],
+ "cmdUnderTest": "$TC actions add action frer recover alg vector reset-time 0 index 32",
+ "expExitCode": "0",
+ "verifyCmd": "$TC actions show action frer index 32",
+ "matchPattern": "action order [0-9]+: frer recover tag-type rtag index 32 alg vector history-length [0-9]+ reset-time 0",
+ "matchCount": "1",
+ "teardown": [
+ "$TC actions flush action frer"
+ ]
+ },
+ {
+ "id": "frer_0026",
+ "name": "List all frer actions shows correct count",
+ "category": [
+ "actions",
+ "frer"
+ ],
+ "setup": [
+ [
+ "modprobe act_frer",
+ 0,
+ 1
+ ],
+ "$TC actions add action frer push index 1",
+ "$TC actions add action frer recover alg vector index 10",
+ "$TC actions add action frer recover alg match tag-pop index 11"
+ ],
+ "cmdUnderTest": "$TC actions show action frer",
+ "expExitCode": "0",
+ "verifyCmd": "$TC actions show action frer",
+ "matchPattern": "action order [0-9]+: frer",
+ "matchCount": "3",
+ "teardown": [
+ "$TC actions flush action frer"
+ ]
+ },
+ {
+ "id": "frer_0027",
+ "name": "Bind frer push action to egress clsact filter",
+ "category": [
+ "actions",
+ "frer"
+ ],
+ "setup": [
+ [
+ "modprobe act_frer",
+ 0,
+ 1
+ ],
+ "ip link del frer_dummy 2>/dev/null || true",
+ "ip link add frer_dummy type dummy",
+ "ip link set frer_dummy up",
+ "$TC qdisc add dev frer_dummy clsact"
+ ],
+ "cmdUnderTest": "$TC filter add dev frer_dummy egress protocol ip flower skip_hw action frer push index 1",
+ "expExitCode": "0",
+ "verifyCmd": "$TC filter show dev frer_dummy egress",
+ "matchPattern": "frer push tag-type rtag index 1",
+ "matchCount": "1",
+ "teardown": [
+ "$TC qdisc del dev frer_dummy clsact",
+ "$TC actions flush action frer",
+ "ip link del frer_dummy"
+ ]
+ },
+ {
+ "id": "frer_0028",
+ "name": "Bind frer recover action to ingress clsact filter",
+ "category": [
+ "actions",
+ "frer"
+ ],
+ "setup": [
+ [
+ "modprobe act_frer",
+ 0,
+ 1
+ ],
+ "ip link del frer_dummy 2>/dev/null || true",
+ "ip link add frer_dummy type dummy",
+ "ip link set frer_dummy up",
+ "$TC qdisc add dev frer_dummy clsact"
+ ],
+ "cmdUnderTest": "$TC filter add dev frer_dummy ingress protocol all flower skip_hw action frer recover alg vector history-length 16 reset-time 1000 tag-pop index 10",
+ "expExitCode": "0",
+ "verifyCmd": "$TC filter show dev frer_dummy ingress",
+ "matchPattern": "frer recover tag-type rtag index 10 alg vector history-length 16 reset-time 1000",
+ "matchCount": "1",
+ "teardown": [
+ "$TC qdisc del dev frer_dummy clsact",
+ "$TC actions flush action frer",
+ "ip link del frer_dummy"
+ ]
+ },
+ {
+ "id": "frer_0029",
+ "name": "Share frer recover action across two ingress filters (refcount check)",
+ "category": [
+ "actions",
+ "frer"
+ ],
+ "setup": [
+ [
+ "modprobe act_frer",
+ 0,
+ 1
+ ],
+ "ip link del frer_a 2>/dev/null || true",
+ "ip link del frer_b 2>/dev/null || true",
+ "ip link add frer_a type dummy",
+ "ip link add frer_b type dummy",
+ "ip link set frer_a up",
+ "ip link set frer_b up",
+ "$TC qdisc add dev frer_a clsact",
+ "$TC qdisc add dev frer_b clsact",
+ "$TC filter add dev frer_a ingress protocol all flower skip_hw action frer recover alg vector history-length 16 tag-pop index 10"
+ ],
+ "cmdUnderTest": "$TC filter add dev frer_b ingress protocol all flower skip_hw action frer recover index 10",
+ "expExitCode": "0",
+ "verifyCmd": "$TC -s actions show action frer index 10",
+ "matchPattern": "ref [2-9][0-9]*",
+ "matchCount": "1",
+ "teardown": [
+ "$TC qdisc del dev frer_a clsact",
+ "$TC qdisc del dev frer_b clsact",
+ "$TC actions flush action frer",
+ "ip link del frer_a",
+ "ip link del frer_b"
+ ]
+ },
+ {
+ "id": "frer_0030",
+ "name": "frer push action refcount increments when bound to filter",
+ "category": [
+ "actions",
+ "frer"
+ ],
+ "setup": [
+ [
+ "modprobe act_frer",
+ 0,
+ 1
+ ],
+ "ip link del frer_dummy 2>/dev/null || true",
+ "ip link add frer_dummy type dummy",
+ "ip link set frer_dummy up",
+ "$TC qdisc add dev frer_dummy clsact",
+ "$TC actions add action frer push index 1"
+ ],
+ "cmdUnderTest": "$TC filter add dev frer_dummy egress protocol ip flower skip_hw action frer push index 1",
+ "expExitCode": "0",
+ "verifyCmd": "$TC -s actions show action frer index 1",
+ "matchPattern": "ref [2-9][0-9]*",
+ "matchCount": "1",
+ "teardown": [
+ "$TC qdisc del dev frer_dummy clsact",
+ "$TC actions flush action frer",
+ "ip link del frer_dummy"
+ ]
+ },
+ {
+ "id": "frer_0031",
+ "name": "frer push output shows not_in_hw flag",
+ "category": [
+ "actions",
+ "frer"
+ ],
+ "setup": [
+ [
+ "modprobe act_frer",
+ 0,
+ 1
+ ],
+ "$TC actions add action frer push index 1"
+ ],
+ "cmdUnderTest": "$TC actions show action frer index 1",
+ "expExitCode": "0",
+ "verifyCmd": "$TC actions show action frer index 1",
+ "matchPattern": "not_in_hw",
+ "matchCount": "1",
+ "teardown": [
+ "$TC actions flush action frer"
+ ]
+ },
+ {
+ "id": "frer_0032",
+ "name": "frer recover output shows not_in_hw flag",
+ "category": [
+ "actions",
+ "frer"
+ ],
+ "setup": [
+ [
+ "modprobe act_frer",
+ 0,
+ 1
+ ],
+ "$TC actions add action frer recover index 10"
+ ],
+ "cmdUnderTest": "$TC actions show action frer index 10",
+ "expExitCode": "0",
+ "verifyCmd": "$TC actions show action frer index 10",
+ "matchPattern": "not_in_hw",
+ "matchCount": "1",
+ "teardown": [
+ "$TC actions flush action frer"
+ ]
+ }
+]
--
2.17.1
^ permalink raw reply related
* [PATCH net-next 6/6] selftests: net: add kselftest for IEEE 802.1CB FRER tc action
From: Xiaoliang Yang @ 2026-07-17 9:55 UTC (permalink / raw)
To: netdev, linux-kernel, linux-kselftest
Cc: davem, edumazet, kuba, pabeni, jhs, jiri, horms, shuah,
vladimir.oltean, vinicius.gomes, fejes, xiaoliang.yang_1
In-Reply-To: <20260717095549.10565-1-xiaoliang.yang_1@nxp.com>
Add frer_test.sh, a TAP-format kselftest script covering the FRER
(IEEE 802.1CB Frame Replication and Elimination for Reliability)
tc action (act_frer).
Tests 1-4 use a bond-based two-namespace topology:
ns_talker
+---------------------------+
| bond0 (IP_SRC, balance-rr)|
| slave: veth_a0 (frer push + mirror to veth_b0)|
| slave: veth_b0 (frer push + mirror to veth_a0)|
+-------+---------------+--+
| |
veth_a0 veth_b0
| |
veth_a1 veth_b1
| |
+-------+---------------+--+
| bond1 (IP_DST, balance-rr)|
| slave: veth_a1 (frer recover ingress) |
| slave: veth_b1 (frer recover ingress) |
+---------------------------+
ns_listener
IP_SRC is assigned to bond0; IP_DST is assigned to bond1. FRER push
is configured on both veth_a0 and veth_b0 egress with cross-mirroring
so every frame sent by either bond slave carries an R-TAG and a
mirrored copy reaches the peer slave. Tests 1-4 exercise shared and
individual recover modes on the listener side.
Test 5 uses a self-contained single-path (no bond) topology:
ns_p2p_src ns_p2p_dst
+----------------------+ +----------------------+
| frer_p2p_a0 (IP_P2P_SRC)| <---> | frer_p2p_a1 (IP_P2P_DST)|
| egress: frer push | | ingress: frer recover |
+----------------------+ +----------------------+
Test 6 uses a four-namespace relay topology:
ns_talker -- bridge0 (br_r0) -+- path A -+- bridge1 (br_r1) -- ns_listener
\- path B -/
bridge0 acts as sequence generator (frer push + replicate to both
redundant paths); bridge1 acts as eliminator (frer shared recover with
tag-pop on both ingress ports).
Six functional test cases are included:
1. push verify - confirm that the frer push action inserts
an R-TAG (EtherType 0xF1C1) on egress;
tcpdump on both veth_a1 and veth_b1 must
capture at least one R-TAG frame each.
2. shared recover e2e - veth_a1 and veth_b1 share one recover
action; the action passes exactly one copy
and discards the duplicate; verified via
ping success, tcpdump frame count on bond1,
and tc stats (passed >= PING_COUNT,
discarded >= PING_COUNT).
3. individual recover - veth_a1 and veth_b1 use independent recover
actions so both copies are passed without
cross-port deduplication; verified via
per-slave tcpdump and tc stats
(discarded = 0 on each port).
4. no tag-pop - shared recover without tag-pop leaves the
R-TAG on passed frames; verified by
capturing EtherType 0xF1C1 (expect >= 1)
and plain ICMP (expect 0) on bond1.
5. simple point-to-point - single-path push + individual recover (with
tag-pop) end-to-end ping test; no bond.
6. relay e2e - four-namespace bridge relay topology; bridge0
pushes R-TAG and replicates to two paths;
bridge1 recovers (shared, tag-pop) and
forwards deduplicated frames to listener;
verified via ping success, tcpdump frame
count on listener, and bridge1 tc stats.
The script conforms to the kselftest framework (TAP output, KSFT_PASS /
KSFT_FAIL / KSFT_SKIP exit codes). It loads kselftest/lib.sh when
available and falls back to a minimal inline implementation otherwise.
All tests are skipped gracefully when act_frer is not available in the
running kernel.
Signed-off-by: Xiaoliang Yang <xiaoliang.yang_1@nxp.com>
---
tools/testing/selftests/net/Makefile | 1 +
tools/testing/selftests/net/frer_test.sh | 1013 ++++++++++++++++++++++
2 files changed, 1014 insertions(+)
create mode 100755 tools/testing/selftests/net/frer_test.sh
diff --git a/tools/testing/selftests/net/Makefile b/tools/testing/selftests/net/Makefile
index 6a190a525a39..67b896611f08 100644
--- a/tools/testing/selftests/net/Makefile
+++ b/tools/testing/selftests/net/Makefile
@@ -38,6 +38,7 @@ TEST_PROGS := \
fib_rule_tests.sh \
fib_tests.sh \
fin_ack_lat.sh \
+ frer_test.sh \
fq_band_pktlimit.sh \
gre_gso.sh \
gre_ipv6_lladdr.sh \
diff --git a/tools/testing/selftests/net/frer_test.sh b/tools/testing/selftests/net/frer_test.sh
new file mode 100755
index 000000000000..ecd88952f495
--- /dev/null
+++ b/tools/testing/selftests/net/frer_test.sh
@@ -0,0 +1,1013 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright 2026 NXP
+#
+# frer_test.sh - IEEE 802.1CB FRER tc action kselftest
+#
+# Topology for tests 1-4:
+#
+# ns_talker bond0 (veth_a0 + veth_b0) <---> bond1 (veth_a1 + veth_b1) ns_listener
+#
+# IP_SRC assigned to bond0; IP_DST assigned to bond1
+#
+# bond mode: balance-rr (round-robin), so frames are distributed across
+# both slaves. FRER push is configured on both veth_a0 and
+# veth_b0 egress with cross-mirror so every frame sent by either
+# slave carries an R-TAG and a mirrored copy reaches the peer.
+# FRER recover: veth_a1/veth_b1 ingress, shared or individual recover per test
+#
+# Ping runs from bond0 to bond1; tcpdump captures on bond1 (or on individual
+# slave interfaces for tests where both copies must be observable).
+#
+# Test 5: simple point-to-point, self-contained topology (no bond).
+# Test 6: relay system, self-contained topology.
+#
+# All namespaces, veth pairs, bond interfaces, tc rules and addresses are
+# created and destroyed within this script. External dependencies:
+# - kernel with CONFIG_NET_ACT_FRER and CONFIG_BONDING
+# - iproute2 tc with frer action support
+# - tcpdump, ping
+# - root privileges
+
+# ----------------------------------------------------------------------------
+# kselftest library: TAP output + exit-code constants
+# ----------------------------------------------------------------------------
+ksft_lib="${KSFT_LIB:-$(dirname "$0")/../kselftest/lib.sh}"
+if [ -f "$ksft_lib" ]; then
+ # shellcheck source=/dev/null
+ . "$ksft_lib"
+else
+ # Minimal fallback when run outside the kselftest tree
+ KSFT_PASS=0
+ KSFT_FAIL=1
+ KSFT_SKIP=4
+ _ksft_count=0
+ _ksft_pass=0
+ _ksft_fail=0
+ _ksft_skip=0
+
+ ksft_print_header() { echo "TAP version 13"; }
+ ksft_set_plan() { echo "1..$1"; }
+ ksft_test_result_pass() {
+ _ksft_count=$((_ksft_count + 1)); _ksft_pass=$((_ksft_pass + 1))
+ echo "ok $_ksft_count - $*"
+ }
+ ksft_test_result_fail() {
+ _ksft_count=$((_ksft_count + 1)); _ksft_fail=$((_ksft_fail + 1))
+ echo "not ok $_ksft_count - $*"
+ }
+ ksft_test_result_skip() {
+ _ksft_count=$((_ksft_count + 1)); _ksft_skip=$((_ksft_skip + 1))
+ echo "ok $_ksft_count - $* # SKIP"
+ }
+ ksft_print_cnts() {
+ echo "# Totals: pass=$_ksft_pass fail=$_ksft_fail skip=$_ksft_skip"
+ }
+ ksft_exit_pass() { exit $KSFT_PASS; }
+ ksft_exit_fail() { exit $KSFT_FAIL; }
+ ksft_exit_fail_msg() { echo "# FATAL: $*" >&2; exit $KSFT_FAIL; }
+fi
+
+# ----------------------------------------------------------------------------
+# Configuration (override via environment)
+# ----------------------------------------------------------------------------
+TC="${TC:-tc}"
+PING="${PING:-ping}"
+TCPDUMP="${TCPDUMP:-tcpdump}"
+PING_COUNT="${PING_COUNT:-5}"
+PING_TIMEOUT="${PING_TIMEOUT:-2}"
+SKIP_MODPROBE="${SKIP_MODPROBE:-0}"
+
+# Bond topology interfaces (tests 1-4)
+readonly VETH_A0="frer_a0"
+readonly VETH_A1="frer_a1"
+readonly VETH_B0="frer_b0"
+readonly VETH_B1="frer_b1"
+readonly BOND0="frer_bond0"
+readonly BOND1="frer_bond1"
+
+readonly NS_TALKER="frer_ns_talker"
+readonly NS_LISTENER="frer_ns_listener"
+
+readonly IP_SRC="10.0.0.1"
+readonly IP_DST="10.0.0.2"
+
+# Point-to-point topology interfaces (test 5)
+readonly P2P_NS_SRC="frer_p2p_src"
+readonly P2P_NS_DST="frer_p2p_dst"
+readonly P2P_VETH_A0="frer_p2p_a0"
+readonly P2P_VETH_A1="frer_p2p_a1"
+readonly IP_P2P_SRC="10.0.1.1"
+readonly IP_P2P_DST="10.0.1.2"
+
+# Relay topology interfaces (test 6)
+#
+# ns_talker (talker_eth.100) -- talker_eth/br0_uplink -- bridge0 (br_r0)
+# |-- br0_swp0/br1_swp0 --\
+# \-- br0_swp1/br1_swp1 --+--\
+# bridge1 (br_r1) -- br1_downlink/listener_eth -- ns_listener
+#
+# bridge0 acts as sequence generator (frer push + replicate to both paths).
+# bridge1 acts as eliminator (frer recover, shared, tag-pop).
+readonly R_NS_TALKER="frer_r_talker"
+readonly R_NS_BRIDGE0="frer_r_bridge0"
+readonly R_NS_BRIDGE1="frer_r_bridge1"
+readonly R_NS_LISTENER="frer_r_listener"
+readonly R_TALKER_ETH="r_tlk_eth" # talker-side physical port
+readonly R_BR0_UPLINK="r_br0_uplink" # bridge0 uplink facing talker
+readonly R_BR0_SWP0="r_br0_swp0" # bridge0 redundant path port 0
+readonly R_BR0_SWP1="r_br0_swp1" # bridge0 redundant path port 1
+readonly R_BR1_SWP0="r_br1_swp0" # bridge1 redundant path port 0
+readonly R_BR1_SWP1="r_br1_swp1" # bridge1 redundant path port 1
+readonly R_BR1_DOWNLINK="r_br1_dwnlnk" # bridge1 downlink facing listener
+readonly R_LISTENER_ETH="r_lst_eth" # listener-side physical port
+readonly R_BR0="br_r0"
+readonly R_BR1="br_r1"
+readonly R_VLAN=100
+readonly R_IP_TALKER="10.1.0.1"
+readonly R_IP_LISTENER="10.1.0.2"
+
+# FRER action index constants
+readonly IDX_PUSH=1
+readonly IDX_SHARED_RCVY=10
+readonly IDX_INDV_RCVY_A=20
+readonly IDX_INDV_RCVY_B=21
+readonly IDX_NO_POP=30
+readonly IDX_P2P_RCVY=40
+readonly IDX_RELAY_PUSH=50
+readonly IDX_RELAY_RCVY=60
+
+readonly NUM_TESTS=6
+
+# ----------------------------------------------------------------------------
+# Prerequisite check
+# ----------------------------------------------------------------------------
+check_prerequisites()
+{
+ local missing=0
+
+ [ "$(id -u)" -eq 0 ] || { echo "# Must be run as root" >&2; missing=1; }
+
+ for cmd in ip "$TC" "$TCPDUMP" "$PING"; do
+ command -v "$cmd" >/dev/null 2>&1 || {
+ echo "# Missing command: $cmd" >&2
+ missing=1
+ }
+ done
+
+ if [ "$missing" -ne 0 ]; then
+ ksft_set_plan "$NUM_TESTS"
+ for i in $(seq 1 "$NUM_TESTS"); do
+ ksft_test_result_skip "prerequisites not met (test $i)"
+ done
+ ksft_print_cnts
+ exit "$KSFT_SKIP"
+ fi
+}
+
+load_module()
+{
+ [ "$SKIP_MODPROBE" = "1" ] && return
+ if ! modprobe act_frer 2>/dev/null; then
+ echo "# modprobe act_frer failed - may be built-in or unavailable" >&2
+ fi
+ if ! modprobe bonding 2>/dev/null; then
+ echo "# modprobe bonding failed - may be built-in or unavailable" >&2
+ fi
+}
+
+check_frer_action()
+{
+ ip netns exec "$NS_TALKER" \
+ $TC actions add action frer push index 999 2>/dev/null || return 1
+ ip netns exec "$NS_TALKER" \
+ $TC actions del action frer index 999 2>/dev/null || true
+ return 0
+}
+
+# ----------------------------------------------------------------------------
+# Bond topology setup / teardown (used by tests 1-4)
+# ----------------------------------------------------------------------------
+setup_topology()
+{
+ for n in "$NS_TALKER" "$NS_LISTENER"; do
+ ip netns add "$n"
+ done
+
+ ip link add "$VETH_A0" type veth peer name "$VETH_A1"
+ ip link set "$VETH_A0" netns "$NS_TALKER"
+ ip link set "$VETH_A1" netns "$NS_LISTENER"
+
+ ip link add "$VETH_B0" type veth peer name "$VETH_B1"
+ ip link set "$VETH_B0" netns "$NS_TALKER"
+ ip link set "$VETH_B1" netns "$NS_LISTENER"
+
+ # ns_talker: create bond0 (balance-rr), frames round-robin across both slaves.
+ ip netns exec "$NS_TALKER" ip link set lo up
+ ip netns exec "$NS_TALKER" ip link add "$BOND0" type bond mode balance-rr miimon 100
+ ip netns exec "$NS_TALKER" ip link set "$VETH_A0" master "$BOND0"
+ ip netns exec "$NS_TALKER" ip link set "$VETH_B0" master "$BOND0"
+ ip netns exec "$NS_TALKER" ip link set "$VETH_A0" up
+ ip netns exec "$NS_TALKER" ip link set "$VETH_B0" up
+ ip netns exec "$NS_TALKER" ip link set "$BOND0" up
+ ip netns exec "$NS_TALKER" ip addr add "${IP_SRC}/24" dev "$BOND0"
+
+ # ns_listener: create bond1 (balance-rr).
+ ip netns exec "$NS_LISTENER" ip link set lo up
+ ip netns exec "$NS_LISTENER" ip link add "$BOND1" type bond mode balance-rr miimon 100
+ ip netns exec "$NS_LISTENER" ip link set "$VETH_A1" master "$BOND1"
+ ip netns exec "$NS_LISTENER" ip link set "$VETH_B1" master "$BOND1"
+ ip netns exec "$NS_LISTENER" ip link set "$VETH_A1" up
+ ip netns exec "$NS_LISTENER" ip link set "$VETH_B1" up
+ ip netns exec "$NS_LISTENER" ip link set "$BOND1" up
+ ip netns exec "$NS_LISTENER" ip addr add "${IP_DST}/24" dev "$BOND1"
+
+ # Static ARP so L2 forwarding works without ARP broadcasts.
+ # With balance-rr both slaves share the bond MAC.
+ local mac_bond0 mac_bond1
+ mac_bond0=$(ip netns exec "$NS_TALKER" cat /sys/class/net/"$BOND0"/address)
+ mac_bond1=$(ip netns exec "$NS_LISTENER" cat /sys/class/net/"$BOND1"/address)
+ ip netns exec "$NS_TALKER" ip neigh add "$IP_DST" lladdr "$mac_bond1" dev "$BOND0"
+ ip netns exec "$NS_LISTENER" ip neigh add "$IP_SRC" lladdr "$mac_bond0" dev "$BOND1"
+}
+
+cleanup()
+{
+ for n in "$NS_TALKER" "$NS_LISTENER" \
+ "$P2P_NS_SRC" "$P2P_NS_DST" \
+ "$R_NS_TALKER" "$R_NS_BRIDGE0" "$R_NS_BRIDGE1" "$R_NS_LISTENER"; do
+ ip netns del "$n" 2>/dev/null || true
+ done
+}
+trap cleanup EXIT
+
+# ----------------------------------------------------------------------------
+# TC rule helpers
+# ----------------------------------------------------------------------------
+
+# Push on both veth_a0 and veth_b0 egress using the same shared frer push
+# action (IDX_PUSH). Each slave also mirrors to the other so that every
+# outgoing frame is replicated onto both paths regardless of which slave the
+# bond currently selects. This prevents packet loss during bond link changes.
+setup_push_mirror()
+{
+ ip netns exec "$NS_TALKER" $TC qdisc add dev "$VETH_A0" clsact
+ ip netns exec "$NS_TALKER" $TC filter add dev "$VETH_A0" egress \
+ protocol ip flower skip_hw \
+ action frer push index $IDX_PUSH \
+ action mirred egress mirror dev "$VETH_B0"
+
+ ip netns exec "$NS_TALKER" $TC qdisc add dev "$VETH_B0" clsact
+ ip netns exec "$NS_TALKER" $TC filter add dev "$VETH_B0" egress \
+ protocol ip flower skip_hw \
+ action frer push index $IDX_PUSH \
+ action mirred egress mirror dev "$VETH_A0"
+}
+
+teardown_tc()
+{
+ for dev in "$VETH_A0" "$VETH_B0"; do
+ ip netns exec "$NS_TALKER" $TC qdisc del dev "$dev" clsact \
+ 2>/dev/null || true
+ done
+ for dev in "$VETH_A1" "$VETH_B1"; do
+ ip netns exec "$NS_LISTENER" $TC qdisc del dev "$dev" clsact \
+ 2>/dev/null || true
+ done
+ ip netns exec "$NS_TALKER" $TC actions flush action frer 2>/dev/null || true
+ ip netns exec "$NS_LISTENER" $TC actions flush action frer 2>/dev/null || true
+}
+
+# ----------------------------------------------------------------------------
+# Packet-capture helpers
+#
+# capture_start_on NS IFACE PCAP [BPF_FILTER]
+# Starts tcpdump in namespace NS on IFACE, writing to PCAP.
+# Stores PID in _CAP_PID.
+#
+# capture_stop
+# Waits for tcpdump (stored in _CAP_PID) to finish.
+#
+# capture_count_on NS PCAP
+# Prints the number of captured packets.
+#
+# Convenience wrappers capture_start / capture_count target bond1 in
+# NS_LISTENER (the primary observation point for tests 2 and 4).
+# ----------------------------------------------------------------------------
+_CAP_PID=""
+
+capture_start_on()
+{
+ local ns="$1" iface="$2" pcap="$3" filter="${4:-}"
+
+ if [ -n "$filter" ]; then
+ ip netns exec "$ns" timeout 4 \
+ $TCPDUMP -i "$iface" -w "$pcap" \
+ --immediate-mode -Z root -y EN10MB \
+ $filter >/dev/null 2>&1 &
+ else
+ ip netns exec "$ns" timeout 4 \
+ $TCPDUMP -i "$iface" -w "$pcap" \
+ --immediate-mode -Z root -y EN10MB \
+ >/dev/null 2>&1 &
+ fi
+ _CAP_PID=$!
+
+ # Wait until tcpdump opens a packet socket (max ~2.5 s).
+ local tries=0
+ while [ $tries -lt 50 ]; do
+ ip netns exec "$ns" grep -q "$iface" /proc/net/packet 2>/dev/null && break
+ sleep 0.05
+ tries=$((tries + 1))
+ done
+}
+
+capture_stop()
+{
+ [ -n "$_CAP_PID" ] || return 0
+ wait "$_CAP_PID" 2>/dev/null || true
+ _CAP_PID=""
+}
+
+capture_count_on()
+{
+ local ns="$1" pcap="$2"
+ ip netns exec "$ns" \
+ $TCPDUMP -r "$pcap" --no-promiscuous-mode 2>/dev/null \
+ | grep -c "^[0-9]" || true
+}
+
+# Convenience wrappers: default to bond1 in NS_LISTENER
+capture_start() { capture_start_on "$NS_LISTENER" "$BOND1" "$@"; }
+capture_count() { capture_count_on "$NS_LISTENER" "$1"; }
+
+# ----------------------------------------------------------------------------
+# Ping helper
+# ----------------------------------------------------------------------------
+do_ping()
+{
+ local rc=0
+ ip netns exec "$NS_TALKER" \
+ $PING -c "$PING_COUNT" -W "$PING_TIMEOUT" -i 0.2 -q \
+ "$IP_DST" >/dev/null 2>&1 || rc=$?
+ return $rc
+}
+
+# ----------------------------------------------------------------------------
+# tc statistics parser
+# ----------------------------------------------------------------------------
+tc_stat()
+{
+ local dump="$1" field="$2"
+ echo "$dump" | awk -F"${field}=" 'NF>1{split($2,a," ");print a[1];exit}' || echo "0"
+}
+
+# ----------------------------------------------------------------------------
+# TEST 1: PUSH VERIFY (bond topology)
+#
+# Only push is configured on the talker side; no recover on the listener.
+# The push action on veth_a0 egress inserts an R-TAG and mirrors a copy to
+# veth_b0, so both listener slaves (veth_a1 and veth_b1) receive a frame
+# with EtherType 0xF1C1. Captures run sequentially on each slave to verify
+# that both paths carry R-TAG frames.
+#
+# Pass criteria:
+# - veth_a1 captures >= 1 R-TAG frame
+# - veth_b1 captures >= 1 R-TAG frame
+# ----------------------------------------------------------------------------
+test_push_verify_bond()
+{
+ local pcap_a pcap_b cap_a cap_b
+ local result="pass"
+
+ setup_push_mirror
+
+ # Capture 1: R-TAG frames on veth_a1 (path A)
+ pcap_a=$(mktemp /tmp/frer_bond_push_a_XXXXXX.pcap)
+ capture_start_on "$NS_LISTENER" "$VETH_A1" "$pcap_a" "ether proto 0xf1c1"
+ ip netns exec "$NS_TALKER" \
+ $PING -c 3 -W 1 -i 0.2 -q "$IP_DST" >/dev/null 2>&1 || true
+ capture_stop
+ cap_a=$(capture_count_on "$NS_LISTENER" "$pcap_a")
+ rm -f "$pcap_a"
+
+ # Capture 2: R-TAG frames on veth_b1 (path B, mirrored copy)
+ pcap_b=$(mktemp /tmp/frer_bond_push_b_XXXXXX.pcap)
+ capture_start_on "$NS_LISTENER" "$VETH_B1" "$pcap_b" "ether proto 0xf1c1"
+ ip netns exec "$NS_TALKER" \
+ $PING -c 3 -W 1 -i 0.2 -q "$IP_DST" >/dev/null 2>&1 || true
+ capture_stop
+ cap_b=$(capture_count_on "$NS_LISTENER" "$pcap_b")
+ rm -f "$pcap_b"
+
+ teardown_tc
+
+ echo "# bond push verify: veth_a1 R-TAG=$cap_a veth_b1 R-TAG=$cap_b"
+
+ [ "$cap_a" -ge 1 ] || result="fail"
+ [ "$cap_b" -ge 1 ] || result="fail"
+
+ if [ "$result" = "pass" ]; then
+ ksft_test_result_pass \
+ "bond push verify: R-TAG on both paths (a1=$cap_a b1=$cap_b)"
+ else
+ ksft_test_result_fail \
+ "bond push verify: expected R-TAG on both paths (a1=$cap_a b1=$cap_b)"
+ fi
+}
+
+# ----------------------------------------------------------------------------
+# TEST 2: SHARED RECOVER E2E (bond topology)
+#
+# veth_a1 and veth_b1 ingress share one recover action (idx=10) with tag-pop.
+# The listener receives two R-TAG copies per request; the shared recover passes
+# exactly one and discards the other. The recovered plain ICMP reaches bond1's
+# IP stack and a reply is sent, making ping succeed.
+#
+# Pass criteria:
+# - ping succeeds (rc=0)
+# - tcpdump on bond1 captures exactly PING_COUNT ICMP echo-request frames
+# (filter is restricted to type=8 to exclude echo replies, which would
+# double the count since bond1 also originates the reply packets)
+# - tc stats on veth_a1: passed >= PING_COUNT, discarded >= PING_COUNT
+# ----------------------------------------------------------------------------
+test_shared_recover_bond()
+{
+ local pcap cap_count ping_rc=0
+ local dump_a
+ local total_passed total_discarded tagless
+ local result="pass"
+
+ setup_push_mirror
+
+ # veth_a1 ingress: create shared recover action with tag-pop
+ ip netns exec "$NS_LISTENER" $TC qdisc add dev "$VETH_A1" clsact
+ ip netns exec "$NS_LISTENER" $TC filter add dev "$VETH_A1" ingress \
+ protocol all flower skip_hw \
+ action frer recover alg vector history-length 16 \
+ reset-time 2000 tag-pop index $IDX_SHARED_RCVY
+
+ # veth_b1 ingress: bind to the same shared action by index
+ ip netns exec "$NS_LISTENER" $TC qdisc add dev "$VETH_B1" clsact
+ ip netns exec "$NS_LISTENER" $TC filter add dev "$VETH_B1" ingress \
+ protocol all flower skip_hw \
+ action frer recover index $IDX_SHARED_RCVY
+
+ pcap=$(mktemp /tmp/frer_bond_shared_XXXXXX.pcap)
+ capture_start "$pcap" "icmp[icmptype] == icmp-echo"
+
+ do_ping || ping_rc=$?
+
+ capture_stop
+
+ cap_count=$(capture_count "$pcap")
+ rm -f "$pcap"
+
+ dump_a=$(ip netns exec "$NS_LISTENER" \
+ $TC -s filter show dev "$VETH_A1" ingress 2>/dev/null)
+
+ teardown_tc
+
+ total_passed=$(tc_stat "$dump_a" "passed")
+ total_discarded=$(tc_stat "$dump_a" "discarded")
+ tagless=$(tc_stat "$dump_a" "tagless")
+ total_discarded=$((total_discarded - tagless))
+
+ echo "# bond shared recover: ping_rc=$ping_rc cap=$cap_count" \
+ "passed=$total_passed discarded=$total_discarded"
+
+ [ "$ping_rc" -eq 0 ] || result="fail"
+ [ "$cap_count" -eq "$PING_COUNT" ] || result="fail"
+ [ "$total_passed" -ge "$PING_COUNT" ] || result="fail"
+ [ "$total_discarded" -ge "$PING_COUNT" ] || result="fail"
+
+ if [ "$result" = "pass" ]; then
+ ksft_test_result_pass \
+ "bond shared recover: ping OK, cap=$cap_count" \
+ "passed=$total_passed discarded=$total_discarded"
+ else
+ ksft_test_result_fail \
+ "bond shared recover: ping_rc=$ping_rc cap=$cap_count" \
+ "passed=$total_passed discarded=$total_discarded" \
+ "(expected ping OK, cap=$PING_COUNT," \
+ "passed>=$PING_COUNT, discarded>=$PING_COUNT)"
+ fi
+}
+
+# ----------------------------------------------------------------------------
+# TEST 3: INDIVIDUAL RECOVER (bond topology)
+#
+# veth_a1 and veth_b1 use independent recover actions (idx=20 and idx=21).
+# Each port maintains its own sequence history so both copies of every frame
+# are passed (no cross-port deduplication). With active-backup bond1, only
+# the active slave's (veth_a1) recovered frame reaches bond1's IP stack, so
+# ping succeeds. The absence of deduplication is verified via per-slave
+# tcpdump (each slave should capture PING_COUNT ICMP frames) and tc stats.
+#
+# Pass criteria:
+# - ping succeeds
+# - veth_a1 captures PING_COUNT ICMP frames (passed, not discarded)
+# - veth_b1 captures PING_COUNT ICMP frames (passed independently)
+# - tc stats: veth_a1 passed=PING_COUNT discarded=0
+# veth_b1 passed=PING_COUNT discarded=0
+# ----------------------------------------------------------------------------
+test_individual_recover_bond()
+{
+ local pcap_a pcap_b cap_a cap_b ping_rc=0
+ local dump_a dump_b
+ local passed_a discarded_a passed_b discarded_b tagless_a tagless_b
+ local result="pass"
+
+ setup_push_mirror
+
+ # veth_a1 ingress: individual recover idx=20 (independent state)
+ ip netns exec "$NS_LISTENER" $TC qdisc add dev "$VETH_A1" clsact
+ ip netns exec "$NS_LISTENER" $TC filter add dev "$VETH_A1" ingress \
+ protocol all flower skip_hw \
+ action frer recover individual alg vector history-length 16 \
+ reset-time 2000 tag-pop index $IDX_INDV_RCVY_A
+
+ # veth_b1 ingress: individual recover idx=21 (separate independent state)
+ ip netns exec "$NS_LISTENER" $TC qdisc add dev "$VETH_B1" clsact
+ ip netns exec "$NS_LISTENER" $TC filter add dev "$VETH_B1" ingress \
+ protocol all flower skip_hw \
+ action frer recover individual alg vector history-length 16 \
+ reset-time 2000 tag-pop index $IDX_INDV_RCVY_B
+
+ # Per-slave capture A: verify veth_a1 passes frames; also use this run
+ # for the overall ping_rc check (do_ping targets bond0->bond1).
+ pcap_a=$(mktemp /tmp/frer_bond_indv_a_XXXXXX.pcap)
+ capture_start_on "$NS_LISTENER" "$VETH_A1" "$pcap_a" "icmp"
+ do_ping || ping_rc=$?
+ capture_stop
+ cap_a=$(capture_count_on "$NS_LISTENER" "$pcap_a")
+ rm -f "$pcap_a"
+
+ # Per-slave capture B: verify veth_b1 also passes frames (balance-rr
+ # distributes egress across both slaves, so both paths carry traffic).
+ pcap_b=$(mktemp /tmp/frer_bond_indv_b_XXXXXX.pcap)
+ capture_start_on "$NS_LISTENER" "$VETH_B1" "$pcap_b" "icmp"
+ do_ping || true
+ capture_stop
+ cap_b=$(capture_count_on "$NS_LISTENER" "$pcap_b")
+ rm -f "$pcap_b"
+
+ dump_a=$(ip netns exec "$NS_LISTENER" \
+ $TC -s filter show dev "$VETH_A1" ingress 2>/dev/null)
+ dump_b=$(ip netns exec "$NS_LISTENER" \
+ $TC -s filter show dev "$VETH_B1" ingress 2>/dev/null)
+
+ teardown_tc
+
+ passed_a=$(tc_stat "$dump_a" "passed")
+ discarded_a=$(tc_stat "$dump_a" "discarded")
+ tagless_a=$(tc_stat "$dump_a" "tagless")
+ passed_b=$(tc_stat "$dump_b" "passed")
+ discarded_b=$(tc_stat "$dump_b" "discarded")
+ tagless_b=$(tc_stat "$dump_b" "tagless")
+ discarded_a=$((discarded_a - tagless_a))
+ discarded_b=$((discarded_b - tagless_b))
+
+ echo "# bond individual recover: ping_rc=$ping_rc" \
+ "a1: cap=$cap_a passed=$passed_a discarded=$discarded_a" \
+ "b1: cap=$cap_b passed=$passed_b discarded=$discarded_b"
+
+ [ "$ping_rc" -eq 0 ] || result="fail"
+ [ "$cap_a" -ge "$PING_COUNT" ] || result="fail"
+ [ "$cap_b" -ge "$PING_COUNT" ] || result="fail"
+ [ "$passed_a" -ge "$PING_COUNT" ] || result="fail"
+ [ "$passed_b" -ge "$PING_COUNT" ] || result="fail"
+ [ "$discarded_a" -eq 0 ] || result="fail"
+ [ "$discarded_b" -eq 0 ] || result="fail"
+
+ if [ "$result" = "pass" ]; then
+ ksft_test_result_pass \
+ "bond individual recover: ping OK" \
+ "a1: cap=$cap_a passed=$passed_a/0" \
+ "b1: cap=$cap_b passed=$passed_b/0"
+ else
+ ksft_test_result_fail \
+ "bond individual recover: ping_rc=$ping_rc" \
+ "a1: cap=$cap_a passed=$passed_a discarded=$discarded_a" \
+ "b1: cap=$cap_b passed=$passed_b discarded=$discarded_b"
+ fi
+}
+
+# ----------------------------------------------------------------------------
+# TEST 4: NO TAG-POP (bond topology)
+#
+# Shared recover runs without tag-pop; passed frames still carry the R-TAG
+# when they reach bond1.
+#
+# Pass criteria:
+# - tcpdump on bond1 with "ether proto 0xf1c1" captures >= 1 R-TAG frame
+# - tcpdump on bond1 with "icmp" captures 0 frames (outer EtherType is
+# 0xF1C1, not 0x0800, so plain-IP ICMP filter does not match)
+# ----------------------------------------------------------------------------
+test_no_tag_pop_bond()
+{
+ local pcap_rtag pcap_icmp rtag_count icmp_count
+ local result="pass"
+
+ setup_push_mirror
+
+ # veth_a1 ingress: shared recover WITHOUT tag-pop
+ ip netns exec "$NS_LISTENER" $TC qdisc add dev "$VETH_A1" clsact
+ ip netns exec "$NS_LISTENER" $TC filter add dev "$VETH_A1" ingress \
+ protocol all flower skip_hw \
+ action frer recover alg vector history-length 16 \
+ reset-time 2000 index $IDX_NO_POP
+
+ # veth_b1 ingress: bind to the same shared action
+ ip netns exec "$NS_LISTENER" $TC qdisc add dev "$VETH_B1" clsact
+ ip netns exec "$NS_LISTENER" $TC filter add dev "$VETH_B1" ingress \
+ protocol all flower skip_hw \
+ action frer recover index $IDX_NO_POP
+
+ # Capture 1: frames with R-TAG EtherType on bond1 (expect >= 1)
+ pcap_rtag=$(mktemp /tmp/frer_bond_nopop_rtag_XXXXXX.pcap)
+ capture_start "$pcap_rtag" "ether proto 0xf1c1"
+ ip netns exec "$NS_TALKER" \
+ $PING -c 3 -W 1 -i 0.2 -q "$IP_DST" >/dev/null 2>&1 || true
+ capture_stop
+ rtag_count=$(capture_count "$pcap_rtag")
+ rm -f "$pcap_rtag"
+
+ # Capture 2: plain ICMP frames on bond1 (expect 0)
+ pcap_icmp=$(mktemp /tmp/frer_bond_nopop_icmp_XXXXXX.pcap)
+ capture_start "$pcap_icmp" "icmp"
+ ip netns exec "$NS_TALKER" \
+ $PING -c 3 -W 1 -i 0.2 -q "$IP_DST" >/dev/null 2>&1 || true
+ capture_stop
+ icmp_count=$(capture_count "$pcap_icmp")
+ rm -f "$pcap_icmp"
+
+ teardown_tc
+
+ echo "# bond no tag-pop: rtag=$rtag_count (expected >=1) icmp=$icmp_count (expected 0)"
+
+ [ "$rtag_count" -ge 1 ] || result="fail"
+ [ "$icmp_count" -eq 0 ] || result="fail"
+
+ if [ "$result" = "pass" ]; then
+ ksft_test_result_pass \
+ "bond no tag-pop: R-TAG present on bond1 " \
+ "(rtag=$rtag_count), ICMP absent (icmp=$icmp_count)"
+ else
+ ksft_test_result_fail \
+ "bond no tag-pop: rtag=$rtag_count icmp=$icmp_count " \
+ "(expected rtag>=1 icmp=0)"
+ fi
+}
+
+# ----------------------------------------------------------------------------
+# TEST 5: SIMPLE POINT-TO-POINT (no bond)
+#
+# Self-contained single-path topology: push on p2p_a0 egress, individual
+# recover (with tag-pop) on p2p_a1 ingress. IP is assigned directly to the
+# veth interfaces (no bond).
+#
+# Pass criteria:
+# - ping succeeds (rc=0)
+# - veth_a1 recover stats: passed >= PING_COUNT, discarded = 0
+# ----------------------------------------------------------------------------
+test_simple_point_to_point()
+{
+ local ping_rc=0
+ local dump_a1 passed discarded
+ local result="pass"
+
+ # Create self-contained p2p namespaces
+ ip netns add "$P2P_NS_SRC"
+ ip netns add "$P2P_NS_DST"
+
+ ip link add "$P2P_VETH_A0" type veth peer name "$P2P_VETH_A1"
+ ip link set "$P2P_VETH_A0" netns "$P2P_NS_SRC"
+ ip link set "$P2P_VETH_A1" netns "$P2P_NS_DST"
+
+ ip netns exec "$P2P_NS_SRC" ip link set lo up
+ ip netns exec "$P2P_NS_SRC" ip link set "$P2P_VETH_A0" up
+ ip netns exec "$P2P_NS_SRC" ip addr add "${IP_P2P_SRC}/24" dev "$P2P_VETH_A0"
+
+ ip netns exec "$P2P_NS_DST" ip link set lo up
+ ip netns exec "$P2P_NS_DST" ip link set "$P2P_VETH_A1" up
+ ip netns exec "$P2P_NS_DST" ip addr add "${IP_P2P_DST}/24" dev "$P2P_VETH_A1"
+
+ local mac_a0 mac_a1
+ mac_a0=$(ip netns exec "$P2P_NS_SRC" cat /sys/class/net/"$P2P_VETH_A0"/address)
+ mac_a1=$(ip netns exec "$P2P_NS_DST" cat /sys/class/net/"$P2P_VETH_A1"/address)
+ ip netns exec "$P2P_NS_SRC" ip neigh add "$IP_P2P_DST" lladdr "$mac_a1" dev "$P2P_VETH_A0"
+ ip netns exec "$P2P_NS_DST" ip neigh add "$IP_P2P_SRC" lladdr "$mac_a0" dev "$P2P_VETH_A1"
+
+ # veth_a0 egress: push R-TAG
+ ip netns exec "$P2P_NS_SRC" $TC qdisc add dev "$P2P_VETH_A0" clsact
+ ip netns exec "$P2P_NS_SRC" $TC filter add dev "$P2P_VETH_A0" egress \
+ protocol ip flower skip_hw \
+ action frer push index $IDX_PUSH
+
+ # veth_a1 ingress: individual recover with tag-pop
+ ip netns exec "$P2P_NS_DST" $TC qdisc add dev "$P2P_VETH_A1" clsact
+ ip netns exec "$P2P_NS_DST" $TC filter add dev "$P2P_VETH_A1" ingress \
+ protocol all flower skip_hw \
+ action frer recover individual alg vector history-length 16 \
+ reset-time 2000 tag-pop index $IDX_P2P_RCVY
+
+ ip netns exec "$P2P_NS_SRC" \
+ $PING -c "$PING_COUNT" -W "$PING_TIMEOUT" -i 0.2 -q \
+ "$IP_P2P_DST" >/dev/null 2>&1 || ping_rc=$?
+
+ dump_a1=$(ip netns exec "$P2P_NS_DST" \
+ $TC -s filter show dev "$P2P_VETH_A1" ingress 2>/dev/null)
+
+ # Teardown p2p topology
+ for dev in "$P2P_VETH_A0"; do
+ ip netns exec "$P2P_NS_SRC" $TC qdisc del dev "$dev" clsact \
+ 2>/dev/null || true
+ done
+ for dev in "$P2P_VETH_A1"; do
+ ip netns exec "$P2P_NS_DST" $TC qdisc del dev "$dev" clsact \
+ 2>/dev/null || true
+ done
+ ip netns exec "$P2P_NS_SRC" $TC actions flush action frer 2>/dev/null || true
+ ip netns exec "$P2P_NS_DST" $TC actions flush action frer 2>/dev/null || true
+ ip netns del "$P2P_NS_SRC" 2>/dev/null || true
+ ip netns del "$P2P_NS_DST" 2>/dev/null || true
+
+ passed=$(tc_stat "$dump_a1" "passed")
+ discarded=$(tc_stat "$dump_a1" "discarded")
+ local tagless
+ tagless=$(tc_stat "$dump_a1" "tagless")
+ discarded=$((discarded - tagless))
+
+ echo "# p2p: ping_rc=$ping_rc passed=$passed discarded=$discarded"
+
+ [ "$ping_rc" -eq 0 ] || result="fail"
+ [ "$passed" -ge "$PING_COUNT" ] || result="fail"
+ [ "$discarded" -eq 0 ] || result="fail"
+
+ if [ "$result" = "pass" ]; then
+ ksft_test_result_pass \
+ "simple p2p: ping OK, passed=$passed discarded=$discarded"
+ else
+ ksft_test_result_fail \
+ "simple p2p: ping_rc=$ping_rc passed=$passed discarded=$discarded"
+ fi
+}
+
+# ----------------------------------------------------------------------------
+# TEST 6: RELAY E2E (self-contained, no bond)
+#
+# Talker sends VLAN-100 frames into bridge0 (sequence generator). Bridge0
+# pushes an R-TAG and replicates to two redundant paths. Bridge1 (eliminator)
+# recovers (shared, tag-pop) on both paths and forwards the deduplicated frame
+# to the listener.
+#
+# Topology:
+# ns_talker (talker_eth.100) -- talker_eth/br0_uplink
+# -- bridge0 (br_r0) -+- br0_swp0/br1_swp0 -+
+# \- br0_swp1/br1_swp1 -+
+# -- bridge1 (br_r1) -- br1_downlink/listener_eth -- ns_listener
+#
+# FRER rules:
+# bridge0 / br0_uplink ingress : push idx=50, redirect br0_swp0, mirror br0_swp1
+# bridge1 / br1_swp0 ingress : recover (shared, tag-pop) idx=60, redirect br1_downlink
+# bridge1 / br1_swp1 ingress : recover idx=60 (bind same), redirect br1_downlink
+# bridge1 / br1_downlink ingress: redirect br1_swp0 (reply path, bypass FDB)
+#
+# Pass criteria:
+# - ping from ns_talker to ns_listener succeeds (rc=0)
+# - tcpdump on listener captures exactly PING_COUNT ICMP echo-request frames
+# - br1_swp0 tc stats: passed >= PING_COUNT, discarded >= PING_COUNT
+# ----------------------------------------------------------------------------
+teardown_relay_tc()
+{
+ for dev in "$R_BR0_UPLINK"; do
+ ip netns exec "$R_NS_BRIDGE0" $TC qdisc del dev "$dev" clsact \
+ 2>/dev/null || true
+ done
+ for dev in "$R_BR1_SWP0" "$R_BR1_SWP1" "$R_BR1_DOWNLINK"; do
+ ip netns exec "$R_NS_BRIDGE1" $TC qdisc del dev "$dev" clsact \
+ 2>/dev/null || true
+ done
+ ip netns exec "$R_NS_BRIDGE0" $TC actions flush action frer 2>/dev/null || true
+ ip netns exec "$R_NS_BRIDGE1" $TC actions flush action frer 2>/dev/null || true
+}
+
+test_relay_e2e()
+{
+ local ping_rc=0
+ local dump_r1swp0
+ local total_passed total_discarded
+ local result="pass"
+ local ns
+
+ for ns in "$R_NS_TALKER" "$R_NS_BRIDGE0" "$R_NS_BRIDGE1" "$R_NS_LISTENER"; do
+ ip netns add "$ns" || {
+ echo "# relay e2e: failed to create netns $ns" >&2
+ ksft_test_result_skip "relay e2e: netns setup failed"
+ return
+ }
+ done
+
+ ip link add "$R_TALKER_ETH" type veth peer name "$R_BR0_UPLINK"
+ ip link add "$R_BR0_SWP0" type veth peer name "$R_BR1_SWP0"
+ ip link add "$R_BR0_SWP1" type veth peer name "$R_BR1_SWP1"
+ ip link add "$R_BR1_DOWNLINK" type veth peer name "$R_LISTENER_ETH"
+
+ ip link set "$R_TALKER_ETH" netns "$R_NS_TALKER"
+ ip link set "$R_BR0_UPLINK" netns "$R_NS_BRIDGE0"
+ ip link set "$R_BR0_SWP0" netns "$R_NS_BRIDGE0"
+ ip link set "$R_BR0_SWP1" netns "$R_NS_BRIDGE0"
+ ip link set "$R_BR1_SWP0" netns "$R_NS_BRIDGE1"
+ ip link set "$R_BR1_SWP1" netns "$R_NS_BRIDGE1"
+ ip link set "$R_BR1_DOWNLINK" netns "$R_NS_BRIDGE1"
+ ip link set "$R_LISTENER_ETH" netns "$R_NS_LISTENER"
+
+ local ns_dev
+ for ns_dev in \
+ "$R_NS_TALKER:$R_TALKER_ETH" \
+ "$R_NS_BRIDGE0:$R_BR0_UPLINK" "$R_NS_BRIDGE0:$R_BR0_SWP0" \
+ "$R_NS_BRIDGE0:$R_BR0_SWP1" \
+ "$R_NS_BRIDGE1:$R_BR1_SWP0" "$R_NS_BRIDGE1:$R_BR1_SWP1" \
+ "$R_NS_BRIDGE1:$R_BR1_DOWNLINK" \
+ "$R_NS_LISTENER:$R_LISTENER_ETH"; do
+ local _ns="${ns_dev%%:*}"
+ local _dev="${ns_dev##*:}"
+ ip netns exec "$_ns" ip link set lo up
+ ip netns exec "$_ns" ip link set "$_dev" up
+ done
+
+ # bridge0: sequence generator, VLAN filtering
+ ip netns exec "$R_NS_BRIDGE0" ip link add name "$R_BR0" type bridge vlan_filtering 1
+ ip netns exec "$R_NS_BRIDGE0" ip link set "$R_BR0" up
+ ip netns exec "$R_NS_BRIDGE0" ip link set "$R_BR0_UPLINK" master "$R_BR0"
+ ip netns exec "$R_NS_BRIDGE0" ip link set "$R_BR0_SWP0" master "$R_BR0"
+ ip netns exec "$R_NS_BRIDGE0" ip link set "$R_BR0_SWP1" master "$R_BR0"
+
+ ip netns exec "$R_NS_BRIDGE0" bridge vlan add dev "$R_BR0_UPLINK" vid "$R_VLAN"
+ ip netns exec "$R_NS_BRIDGE0" bridge vlan add dev "$R_BR0_SWP0" vid "$R_VLAN"
+ ip netns exec "$R_NS_BRIDGE0" bridge vlan del dev "$R_BR0_SWP1" vid 1
+ ip netns exec "$R_NS_BRIDGE0" bridge vlan add dev "$R_BR0_SWP1" \
+ vid "$R_VLAN" pvid untagged
+ ip netns exec "$R_NS_BRIDGE0" bridge link set dev "$R_BR0_SWP0" learning off
+ ip netns exec "$R_NS_BRIDGE0" bridge link set dev "$R_BR0_SWP1" learning off
+ ip netns exec "$R_NS_BRIDGE0" bridge vlan set dev "$R_BR0_SWP0" vid "$R_VLAN" noflood
+ ip netns exec "$R_NS_BRIDGE0" bridge vlan set dev "$R_BR0_SWP1" vid "$R_VLAN" noflood
+
+ # bridge1: eliminator, VLAN filtering
+ ip netns exec "$R_NS_BRIDGE1" ip link add name "$R_BR1" type bridge vlan_filtering 1
+ ip netns exec "$R_NS_BRIDGE1" ip link set "$R_BR1" up
+ ip netns exec "$R_NS_BRIDGE1" ip link set "$R_BR1_SWP0" master "$R_BR1"
+ ip netns exec "$R_NS_BRIDGE1" ip link set "$R_BR1_SWP1" master "$R_BR1"
+ ip netns exec "$R_NS_BRIDGE1" ip link set "$R_BR1_DOWNLINK" master "$R_BR1"
+
+ ip netns exec "$R_NS_BRIDGE1" bridge vlan add dev "$R_BR1_SWP0" vid "$R_VLAN"
+ ip netns exec "$R_NS_BRIDGE1" bridge vlan del dev "$R_BR1_SWP1" vid 1
+ ip netns exec "$R_NS_BRIDGE1" bridge vlan add dev "$R_BR1_SWP1" \
+ vid "$R_VLAN" pvid untagged
+ ip netns exec "$R_NS_BRIDGE1" bridge vlan add dev "$R_BR1_DOWNLINK" vid "$R_VLAN"
+ ip netns exec "$R_NS_BRIDGE1" bridge link set dev "$R_BR1_SWP0" learning off
+ ip netns exec "$R_NS_BRIDGE1" bridge link set dev "$R_BR1_SWP1" learning off
+ ip netns exec "$R_NS_BRIDGE1" bridge vlan set dev "$R_BR1_SWP0" vid "$R_VLAN" noflood
+ ip netns exec "$R_NS_BRIDGE1" bridge vlan set dev "$R_BR1_SWP1" vid "$R_VLAN" noflood
+
+ # ns_talker: VLAN sub-interface
+ ip netns exec "$R_NS_TALKER" ip link add link "$R_TALKER_ETH" \
+ name "${R_TALKER_ETH}.${R_VLAN}" type vlan id "$R_VLAN"
+ ip netns exec "$R_NS_TALKER" ip link set "${R_TALKER_ETH}.${R_VLAN}" up
+ ip netns exec "$R_NS_TALKER" ip addr add "${R_IP_TALKER}/24" \
+ dev "${R_TALKER_ETH}.${R_VLAN}"
+
+ # ns_listener: VLAN sub-interface
+ ip netns exec "$R_NS_LISTENER" ip link add link "$R_LISTENER_ETH" \
+ name "${R_LISTENER_ETH}.${R_VLAN}" type vlan id "$R_VLAN"
+ ip netns exec "$R_NS_LISTENER" ip link set "${R_LISTENER_ETH}.${R_VLAN}" up
+ ip netns exec "$R_NS_LISTENER" ip addr add "${R_IP_LISTENER}/24" \
+ dev "${R_LISTENER_ETH}.${R_VLAN}"
+
+ # Static ARP (VLAN 100 flooding is disabled)
+ local mac_talker mac_listener
+ mac_talker=$(ip netns exec "$R_NS_TALKER" \
+ cat /sys/class/net/"${R_TALKER_ETH}.${R_VLAN}"/address)
+ mac_listener=$(ip netns exec "$R_NS_LISTENER" \
+ cat /sys/class/net/"${R_LISTENER_ETH}.${R_VLAN}"/address)
+ ip netns exec "$R_NS_TALKER" ip neigh add "$R_IP_LISTENER" \
+ lladdr "$mac_listener" dev "${R_TALKER_ETH}.${R_VLAN}"
+ ip netns exec "$R_NS_LISTENER" ip neigh add "$R_IP_TALKER" \
+ lladdr "$mac_talker" dev "${R_LISTENER_ETH}.${R_VLAN}"
+
+ # bridge0 / br0_uplink ingress: push R-TAG then replicate to both redundant paths.
+ # mirror must come before redirect because redirect is a terminating action.
+ ip netns exec "$R_NS_BRIDGE0" $TC qdisc add dev "$R_BR0_UPLINK" clsact
+ ip netns exec "$R_NS_BRIDGE0" $TC filter add dev "$R_BR0_UPLINK" ingress \
+ protocol 802.1Q flower skip_hw vlan_id "$R_VLAN" \
+ action frer push index $IDX_RELAY_PUSH \
+ action mirred egress mirror dev "$R_BR0_SWP1" \
+ action mirred egress redirect dev "$R_BR0_SWP0"
+
+ # bridge1 / br1_swp0 ingress: create shared recover action (tag-pop)
+ ip netns exec "$R_NS_BRIDGE1" $TC qdisc add dev "$R_BR1_SWP0" clsact
+ ip netns exec "$R_NS_BRIDGE1" $TC filter add dev "$R_BR1_SWP0" ingress \
+ protocol all flower skip_hw \
+ action frer recover alg vector history-length 16 \
+ reset-time 2000 tag-pop index $IDX_RELAY_RCVY \
+ action mirred egress redirect dev "$R_BR1_DOWNLINK"
+
+ # bridge1 / br1_swp1 ingress: bind to the same shared recover action
+ ip netns exec "$R_NS_BRIDGE1" $TC qdisc add dev "$R_BR1_SWP1" clsact
+ ip netns exec "$R_NS_BRIDGE1" $TC filter add dev "$R_BR1_SWP1" ingress \
+ protocol all flower skip_hw \
+ action frer recover index $IDX_RELAY_RCVY \
+ action mirred egress redirect dev "$R_BR1_DOWNLINK"
+
+ # bridge1 / br1_downlink ingress: redirect VLAN 100 replies directly to br1_swp0
+ ip netns exec "$R_NS_BRIDGE1" $TC qdisc add dev "$R_BR1_DOWNLINK" clsact
+ ip netns exec "$R_NS_BRIDGE1" $TC filter add dev "$R_BR1_DOWNLINK" ingress \
+ protocol 802.1Q flower skip_hw vlan_id "$R_VLAN" \
+ action mirred egress redirect dev "$R_BR1_SWP0"
+
+ # Capture ICMP echo-requests on listener_eth.VLAN to verify exactly
+ # PING_COUNT deduplicated frames reach the listener after recovery.
+ local pcap cap_count
+ pcap=$(mktemp /tmp/frer_relay_XXXXXX.pcap)
+ capture_start_on "$R_NS_LISTENER" "${R_LISTENER_ETH}.${R_VLAN}" \
+ "$pcap" "icmp[icmptype] == icmp-echo"
+
+ ip netns exec "$R_NS_TALKER" \
+ $PING -c "$PING_COUNT" -W "$PING_TIMEOUT" -i 0.2 -q \
+ "$R_IP_LISTENER" >/dev/null 2>&1 || ping_rc=$?
+
+ capture_stop
+ cap_count=$(capture_count_on "$R_NS_LISTENER" "$pcap")
+ rm -f "$pcap"
+
+ dump_br1_swp0=$(ip netns exec "$R_NS_BRIDGE1" \
+ $TC -s filter show dev "$R_BR1_SWP0" ingress 2>/dev/null)
+
+ teardown_relay_tc
+ for ns in "$R_NS_TALKER" "$R_NS_BRIDGE0" "$R_NS_BRIDGE1" "$R_NS_LISTENER"; do
+ ip netns del "$ns" 2>/dev/null || true
+ done
+
+ total_passed=$(tc_stat "$dump_br1_swp0" "passed")
+ total_discarded=$(tc_stat "$dump_br1_swp0" "discarded")
+ local tagless
+ tagless=$(tc_stat "$dump_br1_swp0" "tagless")
+ total_discarded=$((total_discarded - tagless))
+
+ echo "# relay e2e: ping_rc=$ping_rc cap=$cap_count" \
+ "passed=$total_passed discarded=$total_discarded"
+
+ [ "$ping_rc" -eq 0 ] || result="fail"
+ [ "$cap_count" -eq "$PING_COUNT" ] || result="fail"
+ [ "$total_passed" -ge "$PING_COUNT" ] || result="fail"
+ [ "$total_discarded" -ge "$PING_COUNT" ] || result="fail"
+
+ if [ "$result" = "pass" ]; then
+ ksft_test_result_pass \
+ "relay e2e: ping OK, cap=$cap_count " \
+ "passed=$total_passed discarded=$total_discarded"
+ else
+ ksft_test_result_fail \
+ "relay e2e: ping_rc=$ping_rc cap=$cap_count " \
+ "passed=$total_passed discarded=$total_discarded" \
+ "(expected ping OK, cap=$PING_COUNT," \
+ "passed>=$PING_COUNT, discarded>=$PING_COUNT)"
+ fi
+}
+
+# ----------------------------------------------------------------------------
+# Main
+# ----------------------------------------------------------------------------
+main()
+{
+ ksft_print_header
+ check_prerequisites
+ load_module
+ setup_topology
+
+ if ! check_frer_action; then
+ ksft_set_plan "$NUM_TESTS"
+ for i in $(seq 1 "$NUM_TESTS"); do
+ ksft_test_result_skip \
+ "frer action not available in this kernel (test $i)"
+ done
+ ksft_print_cnts
+ exit "$KSFT_SKIP"
+ fi
+
+ ksft_set_plan "$NUM_TESTS"
+
+ test_push_verify_bond # TEST 1: push on a0/b0, no recover, R-TAG on both paths
+ test_shared_recover_bond # TEST 2: shared recover, dedup, ping succeeds
+ test_individual_recover_bond # TEST 3: individual recover, no dedup, double frames
+ test_no_tag_pop_bond # TEST 4: shared recover without tag-pop, R-TAG preserved
+ test_simple_point_to_point # TEST 5: single-path p2p, no bond
+ test_relay_e2e # TEST 6: relay bridge topology
+
+ ksft_print_cnts
+
+ [ "$_ksft_fail" -eq 0 ] && ksft_exit_pass || ksft_exit_fail
+}
+
+main "$@"
--
2.17.1
^ permalink raw reply related
* Re: [PATCH bpf] veth: convert frag_list skbs before running XDP
From: Toke Høiland-Jørgensen @ 2026-07-17 9:56 UTC (permalink / raw)
To: Matt Fleming, Alexei Starovoitov, Daniel Borkmann
Cc: Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni, Simon Horman, Jesper Dangaard Brouer, John Fastabend,
Stanislav Fomichev, Lorenzo Bianconi, bpf, netdev, stable,
kernel-team, Matt Fleming
In-Reply-To: <20260716100605.939654-1-matt@readmodwrite.com>
Matt Fleming <matt@readmodwrite.com> writes:
> From: Matt Fleming <mfleming@cloudflare.com>
>
> A frag_list skb can reach veth with data_len set but nr_frags zero.
> veth_convert_skb_to_xdp_buff() only converts skbs that are shared,
> locked, have frags[], or do not have enough headroom. It later uses
> skb_is_nonlinear() to decide whether to set XDP_FLAGS_HAS_FRAGS and
> xdp_frags_size.
>
> That exposes frag_list data to XDP as if it were stored in frags[], but
> frags[] is empty. AF_XDP copy mode can then trust the bogus XDP fragment
> metadata, walk an empty fragment entry, and crash in memcpy() from
> __xsk_rcv().
>
> Route frag_list skbs through skb_pp_cow_data() before exposing them to
> XDP, and only advertise XDP frags when the resulting skb has frags[].
> skb_copy_bits() already handles frag_list input, and skb_pp_cow_data()
> builds frags[] output with skb_add_rx_frag(), which is the representation
> XDP multi-buffer expects.
>
> Fixes: 718a18a0c8a6 ("veth: Rework veth_xdp_rcv_skb in order to accept non-linear skb")
> Cc: stable@vger.kernel.org
> Signed-off-by: Matt Fleming <mfleming@cloudflare.com>
Seems reasonable, but a few nits below:
> ---
> drivers/net/veth.c | 4 ++--
> net/core/skbuff.c | 9 ++++-----
> 2 files changed, 6 insertions(+), 7 deletions(-)
>
> diff --git a/drivers/net/veth.c b/drivers/net/veth.c
> index 1c5142149175..efb24aae1f26 100644
> --- a/drivers/net/veth.c
> +++ b/drivers/net/veth.c
> @@ -756,7 +756,7 @@ static int veth_convert_skb_to_xdp_buff(struct veth_rq *rq,
> u32 frame_sz;
>
> if (skb_shared(skb) || skb_head_is_locked(skb) ||
> - skb_shinfo(skb)->nr_frags ||
> + skb_shinfo(skb)->nr_frags || skb_has_frag_list(skb) ||
Isn't 'skb_shinfo(skb)->nr_frags || skb_has_frag_list(skb)' basically
the same as 'skb_is_nonlinear(skb)'? Which, incidentally, is what
generic XDP uses in the check that guards calling into the
skb_pp_cow_data() path.
Looking at those two places, generic XDP checks for 'skb_cloned(skb)',
while veth checks 'skb_shared(skb) || skb_head_is_locked(skb)'. AFAICT,
the latter is stricter; should we update the generic XDP check?
> skb_headroom(skb) < XDP_PACKET_HEADROOM) {
> if (skb_pp_cow_data(rq->page_pool, pskb, XDP_PACKET_HEADROOM))
> goto drop;
> @@ -771,7 +771,7 @@ static int veth_convert_skb_to_xdp_buff(struct veth_rq *rq,
> xdp_prepare_buff(xdp, skb->head, skb_headroom(skb),
> skb_headlen(skb), true);
>
> - if (skb_is_nonlinear(skb)) {
> + if (skb_shinfo(skb)->nr_frags) {
> skb_shinfo(skb)->xdp_frags_size = skb->data_len;
> xdp_buff_set_frags_flag(xdp);
> } else {
> diff --git a/net/core/skbuff.c b/net/core/skbuff.c
> index 18dabb4e9cfa..1e837d01a908 100644
> --- a/net/core/skbuff.c
> +++ b/net/core/skbuff.c
> @@ -936,12 +936,11 @@ int skb_pp_cow_data(struct page_pool *pool, struct sk_buff **pskb,
> int err, i, head_off;
> void *data;
>
> - /* XDP does not support fraglist so we need to linearize
> - * the skb.
> + /*
> + * skb_copy_bits() handles both frags[] and frag_list input. If the
> + * copied skb remains non-linear, it uses frags[], which is the
> + * representation used by XDP multi-buffer.
> */
This comment sorta reads like a function documentation comment, but it
ends up sitting weirdly in the middle of the function body. The comment
you're replacing was tied to the statement below, but this one isn't,
really. Should we turn it into an actual function doc comment instead?
-Toke
^ permalink raw reply
* Re: [PATCH net] tcp: fix TIME_WAIT socket reference leak on PSP policy failure
From: patchwork-bot+netdevbpf @ 2026-07-17 10:00 UTC (permalink / raw)
To: Eric Dumazet
Cc: davem, kuba, pabeni, horms, ncardwell, kuniyu, daniel.zahka,
willemdebruijn.kernel, netdev, eric.dumazet
In-Reply-To: <20260710181317.4060230-1-edumazet@google.com>
Hello:
This patch was applied to netdev/net.git (main)
by Paolo Abeni <pabeni@redhat.com>:
On Fri, 10 Jul 2026 18:13:17 +0000 you wrote:
> Release the TIME_WAIT socket reference and jump to discard_it
> upon PSP policy failure in both IPv4 and IPv6 receive paths.
> This prevents a memory leak of tcp_tw_bucket structures.
>
> Fixes: 659a2899a57d ("tcp: add datapath logic for PSP with inline key exchange")
> Signed-off-by: Eric Dumazet <edumazet@google.com>
>
> [...]
Here is the summary with links:
- [net] tcp: fix TIME_WAIT socket reference leak on PSP policy failure
https://git.kernel.org/netdev/net/c/2c1931a81122
You are awesome, thank you!
--
Deet-doot-dot, I am a bot.
https://korg.docs.kernel.org/patchwork/pwbot.html
^ permalink raw reply
* Re: [PATCH net v2] net/mlx5: free mlx5_st_idx_data on final dealloc
From: patchwork-bot+netdevbpf @ 2026-07-17 10:00 UTC (permalink / raw)
To: Zhiping Zhang
Cc: jgg, leon, saeedm, tariqt, mbloch, michaelgur, netdev, linux-rdma,
linux-kernel, stable
In-Reply-To: <20260702222507.1234467-1-zhipingz@meta.com>
Hello:
This patch was applied to netdev/net.git (main)
by Paolo Abeni <pabeni@redhat.com>:
On Thu, 2 Jul 2026 15:24:58 -0700 you wrote:
> Workloads that repeatedly allocate and release mkeys carrying TPH
> steering-tag hints (e.g. churning RDMA MRs) leak one
> struct mlx5_st_idx_data per cycle; kmemleak flags it as unreferenced
> and the kmalloc slab grows over time.
>
> When the last reference to an ST table entry is dropped,
> mlx5_st_dealloc_index() removed the entry from idx_xa but the backing
> mlx5_st_idx_data allocation was never freed.
>
> [...]
Here is the summary with links:
- [net,v2] net/mlx5: free mlx5_st_idx_data on final dealloc
https://git.kernel.org/netdev/net/c/df6134b527a8
You are awesome, thank you!
--
Deet-doot-dot, I am a bot.
https://korg.docs.kernel.org/patchwork/pwbot.html
^ permalink raw reply
* Re: [PATCH net v2] net: dpaa: fix mode setting
From: Paolo Abeni @ 2026-07-17 10:01 UTC (permalink / raw)
To: Michael Walle, Sean Anderson, Madalin Bucur, Andrew Lunn,
David S . Miller, Eric Dumazet, Jakub Kicinski, Russell King
Cc: netdev, linux-kernel
In-Reply-To: <DJUZ1VG8U9SL.2W0CJM7NSMK2@kernel.org>
On 7/10/26 4:47 PM, Michael Walle wrote:
> On Fri Jul 10, 2026 at 4:39 PM CEST, Sean Anderson wrote:
>> On 7/10/26 10:22, Michael Walle wrote:
>>> Before converting to the phylink interface, the init function would have
>>> set the correct I/F mode depending on the maximum link speed of an
>>> interface. After converting to phylink, the established link speed
>>> is used to determine this setting and is set in the .link_up()
>>> callback. The callback isn't called because the link is never
>>> established between the PCS and a connected SGMII PHY.
>>> To fix it, don't use the current speed, but set the mode depending on
>>> the interface (which implies the maximum speed) in .mac_config().
>>>
>>> Fixes: 5d93cfcf7360 ("net: dpaa: Convert to phylink")
>>> Suggested-by: Sean Anderson <sean.anderson@linux.dev>
>>> Signed-off-by: Michael Walle <mwalle@kernel.org>
>>> ---
>>> FWIW, I dropped setting a non-reserved mode in init(). The hardware
>>> default is 0 and the mac_config() will set a valid mode anyway.
>>>
>>> Changes in v2:
>>> - the setting is/was based on the maximum speed, not the current
>>> speed. thus, move the setting into mac_config().
>>> - Link to v1: https://lore.kernel.org/r/20260706121011.1948906-1-mwalle@kernel.org/
>>>
>>> .../net/ethernet/freescale/fman/fman_dtsec.c | 26 ++++++++++---------
>>> 1 file changed, 14 insertions(+), 12 deletions(-)
>>>
>>> diff --git a/drivers/net/ethernet/freescale/fman/fman_dtsec.c b/drivers/net/ethernet/freescale/fman/fman_dtsec.c
>>> index fe35703c509e..7075f93bab49 100644
>>> --- a/drivers/net/ethernet/freescale/fman/fman_dtsec.c
>>> +++ b/drivers/net/ethernet/freescale/fman/fman_dtsec.c
>>> @@ -900,22 +900,28 @@ static void dtsec_mac_config(struct phylink_config *config, unsigned int mode,
>>> {
>>> struct mac_device *mac_dev = fman_config_to_mac(config);
>>> struct dtsec_regs __iomem *regs = mac_dev->fman_mac->regs;
>>> - u32 tmp;
>>> + u32 ecntrl, maccfg2;
>>> +
>>> + maccfg2 = ioread32be(®s->maccfg2);
>>> + maccfg2 &= ~(MACCFG2_NIBBLE_MODE | MACCFG2_BYTE_MODE);
>>>
>>> switch (state->interface) {
>>> case PHY_INTERFACE_MODE_RMII:
>>> - tmp = DTSEC_ECNTRL_RMM;
>>> + ecntrl = DTSEC_ECNTRL_RMM;
>>> + maccfg2 |= MACCFG2_NIBBLE_MODE;
>>> break;
>>> case PHY_INTERFACE_MODE_RGMII:
>>> case PHY_INTERFACE_MODE_RGMII_ID:
>>> case PHY_INTERFACE_MODE_RGMII_RXID:
>>> case PHY_INTERFACE_MODE_RGMII_TXID:
>>> - tmp = DTSEC_ECNTRL_GMIIM | DTSEC_ECNTRL_RPM;
>>> + ecntrl = DTSEC_ECNTRL_GMIIM | DTSEC_ECNTRL_RPM;
>>> + maccfg2 |= MACCFG2_BYTE_MODE;
>>> break;
>>> case PHY_INTERFACE_MODE_SGMII:
>>> case PHY_INTERFACE_MODE_1000BASEX:
>>> case PHY_INTERFACE_MODE_2500BASEX:
>>> - tmp = DTSEC_ECNTRL_TBIM | DTSEC_ECNTRL_SGMIIM;
>>> + ecntrl = DTSEC_ECNTRL_TBIM | DTSEC_ECNTRL_SGMIIM;
>>> + maccfg2 |= MACCFG2_BYTE_MODE;
>>> break;
>>> default:
>>> dev_warn(mac_dev->dev, "cannot configure dTSEC for %s\n",
>>> @@ -923,7 +929,8 @@ static void dtsec_mac_config(struct phylink_config *config, unsigned int mode,
>>> return;
>>> }
>>>
>>> - iowrite32be(tmp, ®s->ecntrl);
>>> + iowrite32be(ecntrl, ®s->ecntrl);
>>> + iowrite32be(maccfg2, ®s->maccfg2);
>>> }
>>>
>>> static void dtsec_link_up(struct phylink_config *config, struct phy_device *phy,
>>> @@ -948,15 +955,10 @@ static void dtsec_link_up(struct phylink_config *config, struct phy_device *phy,
>>> iowrite32be(tmp, ®s->ecntrl);
>>>
>>> tmp = ioread32be(®s->maccfg2);
>>> - tmp &= ~(MACCFG2_NIBBLE_MODE | MACCFG2_BYTE_MODE | MACCFG2_FULL_DUPLEX);
>>> - if (speed >= SPEED_1000)
>>> - tmp |= MACCFG2_BYTE_MODE;
>>> - else
>>> - tmp |= MACCFG2_NIBBLE_MODE;
>>> -
>>> if (duplex == DUPLEX_FULL)
>>> tmp |= MACCFG2_FULL_DUPLEX;
>>> -
>>> + else
>>> + tmp &= ~MACCFG2_FULL_DUPLEX;
>>
>> Did you test this when forcing 10/100 speed?
>
> No I didn't. Well I can't. I have a very weird board which only
> supports 1000base-X (and copper SFPs in 1000basex autoneg mode). On
> top of that there is a Marvell 88E1112 in between the SFP and the
> MAC, for which the PHY driver is completely broken. Long story
> short, I'm not able to test that (yet/at all? Not sure).
FTR, sashiko suspect this patch will broke such setup:
https://sashiko.dev/#/patchset/20260710143430.2276141-1-mwalle%40kernel.org
I think we need either an explicit test from a 3rd party or a new revision.
/P
^ permalink raw reply
* Re: [PATCH net] dpll: fix NULL pointer dereference in dpll_msg_add_pin_ref_sync()
From: patchwork-bot+netdevbpf @ 2026-07-17 10:10 UTC (permalink / raw)
To: Ivan Vecera
Cc: netdev, vadim.fedorenko, arkadiusz.kubalewski, jiri,
przemyslaw.kitszel, milena.olech, kuba, linux-kernel
In-Reply-To: <20260710193625.1378822-1-ivecera@redhat.com>
Hello:
This patch was applied to netdev/net.git (main)
by Paolo Abeni <pabeni@redhat.com>:
On Fri, 10 Jul 2026 21:36:25 +0200 you wrote:
> When a dpll_pin is shared across multiple dpll_device instances and
> those devices are being unregistered (e.g. during driver module removal),
> a NULL pointer dereference can occur in dpll_msg_add_pin_ref_sync().
>
> This happens under the following conditions:
> - A pin is registered with two or more dpll devices (dpll_A, dpll_B)
> - The pin has ref_sync pairs with other pins
> - During unregistration of dpll_A's pins, a ref_sync partner pin is
> unregistered first, removing it from dpll_A->pin_refs
> - But since the partner pin is still registered with dpll_B, its
> dpll_refs is not empty, so dpll_pin_ref_sync_pair_del() does NOT
> run and the partner stays in the pin's ref_sync_pins xarray
> - When the pin itself is then unregistered from dpll_A, the delete
> notification calls dpll_msg_add_pin_ref_sync() which finds the
> partner in ref_sync_pins, passes dpll_pin_available() (partner is
> still registered with dpll_B), but dpll_pin_on_dpll_priv(dpll_A,
> partner) returns NULL because partner was already removed from
> dpll_A->pin_refs
> - The NULL priv pointer is passed to the driver's ref_sync_get
> callback, which dereferences it
>
> [...]
Here is the summary with links:
- [net] dpll: fix NULL pointer dereference in dpll_msg_add_pin_ref_sync()
https://git.kernel.org/netdev/net/c/d2e914a4a0d0
You are awesome, thank you!
--
Deet-doot-dot, I am a bot.
https://korg.docs.kernel.org/patchwork/pwbot.html
^ permalink raw reply
* Re: [PATCH net v2] net: dpaa: fix mode setting
From: Michael Walle @ 2026-07-17 10:13 UTC (permalink / raw)
To: Paolo Abeni, Sean Anderson, Madalin Bucur, Andrew Lunn,
David S . Miller, Eric Dumazet, Jakub Kicinski, Russell King
Cc: netdev, linux-kernel
In-Reply-To: <2c8f63f5-f04a-4abc-9508-4c47a80da543@redhat.com>
[-- Attachment #1: Type: text/plain, Size: 5121 bytes --]
On Fri Jul 17, 2026 at 12:01 PM CEST, Paolo Abeni wrote:
> On 7/10/26 4:47 PM, Michael Walle wrote:
>> On Fri Jul 10, 2026 at 4:39 PM CEST, Sean Anderson wrote:
>>> On 7/10/26 10:22, Michael Walle wrote:
>>>> Before converting to the phylink interface, the init function would have
>>>> set the correct I/F mode depending on the maximum link speed of an
>>>> interface. After converting to phylink, the established link speed
>>>> is used to determine this setting and is set in the .link_up()
>>>> callback. The callback isn't called because the link is never
>>>> established between the PCS and a connected SGMII PHY.
>>>> To fix it, don't use the current speed, but set the mode depending on
>>>> the interface (which implies the maximum speed) in .mac_config().
>>>>
>>>> Fixes: 5d93cfcf7360 ("net: dpaa: Convert to phylink")
>>>> Suggested-by: Sean Anderson <sean.anderson@linux.dev>
>>>> Signed-off-by: Michael Walle <mwalle@kernel.org>
>>>> ---
>>>> FWIW, I dropped setting a non-reserved mode in init(). The hardware
>>>> default is 0 and the mac_config() will set a valid mode anyway.
>>>>
>>>> Changes in v2:
>>>> - the setting is/was based on the maximum speed, not the current
>>>> speed. thus, move the setting into mac_config().
>>>> - Link to v1: https://lore.kernel.org/r/20260706121011.1948906-1-mwalle@kernel.org/
>>>>
>>>> .../net/ethernet/freescale/fman/fman_dtsec.c | 26 ++++++++++---------
>>>> 1 file changed, 14 insertions(+), 12 deletions(-)
>>>>
>>>> diff --git a/drivers/net/ethernet/freescale/fman/fman_dtsec.c b/drivers/net/ethernet/freescale/fman/fman_dtsec.c
>>>> index fe35703c509e..7075f93bab49 100644
>>>> --- a/drivers/net/ethernet/freescale/fman/fman_dtsec.c
>>>> +++ b/drivers/net/ethernet/freescale/fman/fman_dtsec.c
>>>> @@ -900,22 +900,28 @@ static void dtsec_mac_config(struct phylink_config *config, unsigned int mode,
>>>> {
>>>> struct mac_device *mac_dev = fman_config_to_mac(config);
>>>> struct dtsec_regs __iomem *regs = mac_dev->fman_mac->regs;
>>>> - u32 tmp;
>>>> + u32 ecntrl, maccfg2;
>>>> +
>>>> + maccfg2 = ioread32be(®s->maccfg2);
>>>> + maccfg2 &= ~(MACCFG2_NIBBLE_MODE | MACCFG2_BYTE_MODE);
>>>>
>>>> switch (state->interface) {
>>>> case PHY_INTERFACE_MODE_RMII:
>>>> - tmp = DTSEC_ECNTRL_RMM;
>>>> + ecntrl = DTSEC_ECNTRL_RMM;
>>>> + maccfg2 |= MACCFG2_NIBBLE_MODE;
>>>> break;
>>>> case PHY_INTERFACE_MODE_RGMII:
>>>> case PHY_INTERFACE_MODE_RGMII_ID:
>>>> case PHY_INTERFACE_MODE_RGMII_RXID:
>>>> case PHY_INTERFACE_MODE_RGMII_TXID:
>>>> - tmp = DTSEC_ECNTRL_GMIIM | DTSEC_ECNTRL_RPM;
>>>> + ecntrl = DTSEC_ECNTRL_GMIIM | DTSEC_ECNTRL_RPM;
>>>> + maccfg2 |= MACCFG2_BYTE_MODE;
>>>> break;
>>>> case PHY_INTERFACE_MODE_SGMII:
>>>> case PHY_INTERFACE_MODE_1000BASEX:
>>>> case PHY_INTERFACE_MODE_2500BASEX:
>>>> - tmp = DTSEC_ECNTRL_TBIM | DTSEC_ECNTRL_SGMIIM;
>>>> + ecntrl = DTSEC_ECNTRL_TBIM | DTSEC_ECNTRL_SGMIIM;
>>>> + maccfg2 |= MACCFG2_BYTE_MODE;
>>>> break;
>>>> default:
>>>> dev_warn(mac_dev->dev, "cannot configure dTSEC for %s\n",
>>>> @@ -923,7 +929,8 @@ static void dtsec_mac_config(struct phylink_config *config, unsigned int mode,
>>>> return;
>>>> }
>>>>
>>>> - iowrite32be(tmp, ®s->ecntrl);
>>>> + iowrite32be(ecntrl, ®s->ecntrl);
>>>> + iowrite32be(maccfg2, ®s->maccfg2);
>>>> }
>>>>
>>>> static void dtsec_link_up(struct phylink_config *config, struct phy_device *phy,
>>>> @@ -948,15 +955,10 @@ static void dtsec_link_up(struct phylink_config *config, struct phy_device *phy,
>>>> iowrite32be(tmp, ®s->ecntrl);
>>>>
>>>> tmp = ioread32be(®s->maccfg2);
>>>> - tmp &= ~(MACCFG2_NIBBLE_MODE | MACCFG2_BYTE_MODE | MACCFG2_FULL_DUPLEX);
>>>> - if (speed >= SPEED_1000)
>>>> - tmp |= MACCFG2_BYTE_MODE;
>>>> - else
>>>> - tmp |= MACCFG2_NIBBLE_MODE;
>>>> -
>>>> if (duplex == DUPLEX_FULL)
>>>> tmp |= MACCFG2_FULL_DUPLEX;
>>>> -
>>>> + else
>>>> + tmp &= ~MACCFG2_FULL_DUPLEX;
>>>
>>> Did you test this when forcing 10/100 speed?
>>
>> No I didn't. Well I can't. I have a very weird board which only
>> supports 1000base-X (and copper SFPs in 1000basex autoneg mode). On
>> top of that there is a Marvell 88E1112 in between the SFP and the
>> MAC, for which the PHY driver is completely broken. Long story
>> short, I'm not able to test that (yet/at all? Not sure).
> FTR, sashiko suspect this patch will broke such setup:
> https://sashiko.dev/#/patchset/20260710143430.2276141-1-mwalle%40kernel.org
I've seen that, but.. that was the actual change between v1 and v2
as suggested by Sean. It does not depend on the actual link speed,
but the maximum link speed. So it is not relevant if the link
negotiates to a slower speed or not. At least that now matches the
behavior prior to the phylink conversion. If that was working -
that I can't tell you.
> I think we need either an explicit test from a 3rd party or a new revision.
I'm not sure what you mean with a v3. Going back to v1?
-michael
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 297 bytes --]
^ permalink raw reply
* Re: [PATCH net 8/9] ipvs: fix more places with wrong ipv6 transport offsets
From: Pablo Neira Ayuso @ 2026-07-17 10:13 UTC (permalink / raw)
To: Paolo Abeni
Cc: Florian Westphal, netdev, David S. Miller, Eric Dumazet,
Jakub Kicinski, netfilter-devel, ja
In-Reply-To: <e7ed31d3-a3aa-427a-8e82-aa65b4388368@redhat.com>
Hi,
On Fri, Jul 17, 2026 at 11:17:15AM +0200, Paolo Abeni wrote:
> Hi,
>
> On 7/10/26 4:37 PM, Florian Westphal wrote:
> > From: Julian Anastasov <ja@ssi.bg>
> >
> > Sashiko reports for more incorrect IPv6 transport offsets.
> >
> > The app code for TCP was assuming IPv4 network header
> > even after the ipvsh argument was provided. This can
> > cause problems with apps over IPv6. As for the only
> > official app in the kernel tree (FTP) this problem is
> > harmless because we use Netfilter to mangle the FTP
> > ports and we do not adjust the TCP seq numbers.
> >
> > Also, provide correct offset of the ICMPV6 header in
> > ip_vs_out_icmp_v6() for correct checksum checks when
> > the IPv6 packet has extension headers.
> >
> > Fixes: d12e12299a69 ("ipvs: add ipv6 support to ftp")
> > Fixes: 2a3b791e6e11 ("IPVS: Add/adjust Netfilter hook functions and helpers for v6")
> > Cc: stable@vger.kernel.org
> > Link: https://sashiko.dev/#/patchset/20260706101624.69471-1-zhaoyz24%40mails.tsinghua.edu.cn
> > Signed-off-by: Julian Anastasov <ja@ssi.bg>
> > Signed-off-by: Florian Westphal <fw@strlen.de>
> > ---
> > net/netfilter/ipvs/ip_vs_app.c | 10 ++++------
> > net/netfilter/ipvs/ip_vs_core.c | 3 +--
> > 2 files changed, 5 insertions(+), 8 deletions(-)
> >
> > diff --git a/net/netfilter/ipvs/ip_vs_app.c b/net/netfilter/ipvs/ip_vs_app.c
> > index d54d7da58334..b0e00be85cb1 100644
> > --- a/net/netfilter/ipvs/ip_vs_app.c
> > +++ b/net/netfilter/ipvs/ip_vs_app.c
> > @@ -361,14 +361,13 @@ static inline int app_tcp_pkt_out(struct ip_vs_conn *cp, struct sk_buff *skb,
> > struct ip_vs_iphdr *ipvsh)
> > {
> > int diff;
> > - const unsigned int tcp_offset = ip_hdrlen(skb);
> > struct tcphdr *th;
> > __u32 seq;
> >
> > - if (skb_ensure_writable(skb, tcp_offset + sizeof(*th)))
> > + if (skb_ensure_writable(skb, ipvsh->len + sizeof(*th)))
> > return 0;
> >
> > - th = (struct tcphdr *)(skb_network_header(skb) + tcp_offset);
> > + th = (struct tcphdr *)(skb_network_header(skb) + ipvsh->len);
>
> Beyond the usual set of pre-existing issues, sashiko-gemini noted this
> patch may need a follow-up:
>
> https://sashiko.dev/#/patchset/20260710143733.29741-2-fw%40strlen.de
Cc'ing Julian, it seems he is not on Cc, in case he can help us with this.
^ permalink raw reply
* Re: [PATCH] net/sched: act_tunnel_key: Defer dst_release to RCU callback
From: patchwork-bot+netdevbpf @ 2026-07-17 10:20 UTC (permalink / raw)
To: Jamal Hadi Salim
Cc: netdev, davem, edumazet, kuba, pabeni, horms, dcaratti,
zdi-disclosures, security, victor, jiri
In-Reply-To: <20260711150537.7946-1-jhs@mojatatu.com>
Hello:
This patch was applied to netdev/net.git (main)
by Paolo Abeni <pabeni@redhat.com>:
On Sat, 11 Jul 2026 11:05:37 -0400 you wrote:
> Fix a race-condition use-after-free in tunnel_key_release_params().
>
> The function releases the metadata_dst of the old params synchronously
> via dst_release() while deferring the params struct free with
> kfree_rcu(). A concurrent tunnel_key_act() reader on the datapath may
> still hold the old params pointer (under rcu_read_lock_bh) and proceed
> to call dst_clone(¶ms->tcft_enc_metadata->dst) after the writer's
> dst_release has already pushed the dst's rcuref to RCUREF_DEAD.
>
> [...]
Here is the summary with links:
- net/sched: act_tunnel_key: Defer dst_release to RCU callback
https://git.kernel.org/netdev/net/c/f1f5c8a3955f
You are awesome, thank you!
--
Deet-doot-dot, I am a bot.
https://korg.docs.kernel.org/patchwork/pwbot.html
^ permalink raw reply
* Re: [PATCH net-next v3 0/9] netconsole: stop charging netpoll users for netconsole-only data
From: patchwork-bot+netdevbpf @ 2026-07-17 10:20 UTC (permalink / raw)
To: Breno Leitao
Cc: davem, edumazet, kuba, pabeni, horms, andrew+netdev, netdev,
asantostc, gustavold, linux-kernel, kernel-team
In-Reply-To: <20260710-netconsole_move_more-v3-0-6f63f76b28bc@debian.org>
Hello:
This series was applied to netdev/net-next.git (main)
by Paolo Abeni <pabeni@redhat.com>:
On Fri, 10 Jul 2026 04:38:52 -0700 you wrote:
> This work continue to untangle netconsole and netpoll, improving
> memory usage for netpoll users that are not netconsole.
>
> struct netpoll is shared by every netpoll consumer in the tree:
> bonding, bridge, team, vlan, macvlan, dsa and netconsole. The non-
> netconsole users only need a per-port handle that lets them transmit
> SKBs.
>
> [...]
Here is the summary with links:
- [net-next,v3,1/9] netconsole: clean up released targets dropped before the cleanup worker
https://git.kernel.org/netdev/net-next/c/a620ff84d42c
- [net-next,v3,2/9] netpoll: export refill_skbs(), refill_skbs_work_handler(), skb_pool_flush()
https://git.kernel.org/netdev/net-next/c/ede59d06c28f
- [net-next,v3,3/9] netconsole: take over skb pool lifecycle from netpoll
https://git.kernel.org/netdev/net-next/c/1fee9a9c5904
- [net-next,v3,4/9] netconsole: move refill_skbs_work_handler() from netpoll
https://git.kernel.org/netdev/net-next/c/28511289088c
- [net-next,v3,5/9] netconsole: move refill_skbs() and skb-pool sizing macros from netpoll
https://git.kernel.org/netdev/net-next/c/2fbebfbe2953
- [net-next,v3,6/9] netconsole: move skb_pool_flush() from netpoll
https://git.kernel.org/netdev/net-next/c/67fb2038e1f6
- [net-next,v3,7/9] netconsole: move skb_pool / refill_wq from struct netpoll to netconsole_target
https://git.kernel.org/netdev/net-next/c/ba520084dc3b
- [net-next,v3,8/9] netconsole: move local_port / remote_port from struct netpoll to netconsole_target
https://git.kernel.org/netdev/net-next/c/3b247a595663
- [net-next,v3,9/9] netconsole: move remote_mac from struct netpoll to netconsole_target
https://git.kernel.org/netdev/net-next/c/49e03ca58334
You are awesome, thank you!
--
Deet-doot-dot, I am a bot.
https://korg.docs.kernel.org/patchwork/pwbot.html
^ permalink raw reply
* RE: [PATCH net V3] net: phy: motorcomm: read EEE abilities in yt8521_get_features()
From: Clark Wang @ 2026-07-17 10:21 UTC (permalink / raw)
To: Clark Wang (OSS), Frank.Sae@motor-comm.com, andrew@lunn.ch,
leitao@debian.org, hkallweit1@gmail.com, linux@armlinux.org.uk,
davem@davemloft.net, edumazet@google.com, kuba@kernel.org,
pabeni@redhat.com
Cc: netdev@vger.kernel.org, linux-kernel@vger.kernel.org,
imx@lists.linux.dev
In-Reply-To: <20260717091652.230617-1-xiaoning.wang@oss.nxp.com>
> From: Clark Wang <xiaoning.wang@nxp.com>
>
> In phy_probe(), genphy_c45_read_eee_abilities() is only called when a
> driver uses phydrv->features. Drivers that implement .get_features are
> responsible for reading the EEE abilities themselves.
>
> yt8521_get_features() does not do this, so phydev->supported_eee stays
> empty for YT8521/YT8531S and "ethtool --show-eee" reports "EEE status:
> not supported", even though the PHY has the standard EEE capability
> registers.
>
> Call genphy_c45_read_eee_abilities() at the end of yt8521_get_features()
> to populate supported_eee.
>
> Fixes: 70479a40954c ("net: phy: Add driver for Motorcomm yt8521 gigabit
> ethernet phy")
> Reviewed-by: Breno Leitao <leitao@debian.org>
> Signed-off-by: Clark Wang <xiaoning.wang@nxp.com>
> ---
> V3 changes:
> - Replace the uncommon "ret ? : ..." expression with the usual
> check, and move the check right after the first
> yt8521_get_features_paged()
> call, which was the only path missing it.
> - Collect Reviewed tag from Breno.
>
> V2 changes:
> - Return the value from genphy_c45_read_eee_abilities.
> ---
Sorry for the noise.
I think sashiko's discovery on V2 makes sense. I will fix the issues mentioned in V4.
https://sashiko.dev/#/patchset/20260710080018.1070789-1-xiaoning.wang@oss.nxp.com?part=1
Best regards,
Clark Wang
^ permalink raw reply
* Re: ipv4: icmp: icmp_route_lookup() relookups pick wrong netdev with policy routing + strict rp_filter
From: Muhammad Ziad @ 2026-07-17 10:28 UTC (permalink / raw)
To: Eric Dumazet
Cc: netdev, David Ahern, Jakub Kicinski, Paolo Abeni, David S. Miller,
linux-kernel
In-Reply-To: <CANn89iLziS_XE8oz03VWU47NRzP-G8OexL=-C0mxcMMnOia=Rw@mail.gmail.com>
Seems ANYSRC flag is required for ip_route_output_key() to not fail
with an error:
diff --git a/net/ipv4/icmp.c b/net/ipv4/icmp.c
index 23e921d313b3..c2428fa21b07 100644
--- a/net/ipv4/icmp.c
+++ b/net/ipv4/icmp.c
@@ -548,10 +548,15 @@ static struct rtable *icmp_route_lookup(struct
net *net, struct flowi4 *fl4,
if (IS_ERR(rt2))
err = PTR_ERR(rt2);
} else {
- struct flowi4 fl4_2 = {};
+ struct flowi4 fl4_2 = fl4_dec;
unsigned long orefdst;
- fl4_2.daddr = fl4_dec.saddr;
+ swap(fl4_2.daddr, fl4_2.saddr);
+ swap(fl4_2.fl4_sport, fl4_2.fl4_dport);
+
+ fl4_2.flowi4_oif = l3mdev_master_ifindex(route_lookup_dev);
+ fl4_2.flowi4_flags |= FLOWI_FLAG_ANYSRC;
+
rt2 = ip_route_output_key(net, &fl4_2);
if (IS_ERR(rt2)) {
err = PTR_ERR(rt2);
On Thu, Jul 16, 2026 at 2:29 PM Eric Dumazet <edumazet@google.com> wrote:
>
> On Thu, Jul 16, 2026 at 1:22 PM Eric Dumazet <edumazet@google.com> wrote:
> >
> > On Thu, Jul 16, 2026 at 12:40 PM Muhammad Ziad <muhzi100@gmail.com> wrote:
> > >
> > > Thank you for the fix, Eric. Applying the other selectors from skb_in
> > > makes sense to me, but I'm not sure if instead we should copy them
> > > from fl4_dec?
> > >
> > > The mark param e.g. is gated by IP4_REPLY_MARK() on fwmark_reflect,
> > > which could be different from skb_in->mark.
> > >
> >
> > Yes, sashiko had some remarks.
> >
> > I have been playing with:
> >
> > diff --git a/net/ipv4/icmp.c b/net/ipv4/icmp.c
> > index 23e921d313b36b00d8ae5e14846527220c9db32b..b0eb4f8ff9867499dc3a96c92a414440b2d3a115
> > 100644
> > --- a/net/ipv4/icmp.c
> > +++ b/net/ipv4/icmp.c
> > @@ -548,10 +548,12 @@ static struct rtable *icmp_route_lookup(struct
> > net *net, struct flowi4 *fl4,
> > if (IS_ERR(rt2))
> > err = PTR_ERR(rt2);
> > } else {
> > - struct flowi4 fl4_2 = {};
> > + struct flowi4 fl4_2 = fl4_dec;
> > unsigned long orefdst;
> >
> > fl4_2.daddr = fl4_dec.saddr;
> > + fl4_2.saddr = fl4_dec.daddr;
> > + fl4_2.flowi4_oif = l3mdev_master_ifindex(route_lookup_dev);
> > rt2 = ip_route_output_key(net, &fl4_2);
> > if (IS_ERR(rt2)) {
> > err = PTR_ERR(rt2);
> >
> >
>
> Or even better:
>
> diff --git a/net/ipv4/icmp.c b/net/ipv4/icmp.c
> index 23e921d313b36b00d8ae5e14846527220c9db32b..aaaa0e347702c4412ff515a9d0589c79c69c62d1
> 100644
> --- a/net/ipv4/icmp.c
> +++ b/net/ipv4/icmp.c
> @@ -548,10 +548,13 @@ static struct rtable *icmp_route_lookup(struct
> net *net, struct flowi4 *fl4,
> if (IS_ERR(rt2))
> err = PTR_ERR(rt2);
> } else {
> - struct flowi4 fl4_2 = {};
> + struct flowi4 fl4_2 = fl4_dec;
> unsigned long orefdst;
>
> - fl4_2.daddr = fl4_dec.saddr;
> + swap(fl4_2.daddr, fl4_2.saddr);
> + swap(fl4_2.fl4_sport, fl4_2.fl4_dport);
> +
> + fl4_2.flowi4_oif = l3mdev_master_ifindex(route_lookup_dev);
> rt2 = ip_route_output_key(net, &fl4_2);
> if (IS_ERR(rt2)) {
> err = PTR_ERR(rt2);
^ permalink raw reply related
* [PATCH 02/13] 8139cp: handle cp_init_rings() failure in cp_tx_timeout()
From: Yun Lu @ 2026-07-17 10:37 UTC (permalink / raw)
To: andrew+netdev, davem, edumazet, kuba, pabeni; +Cc: netdev
In-Reply-To: <20260717103724.13971-1-luyun_611@163.com>
From: Yun Lu <luyun@kylinos.cn>
cp_tx_timeout() ignores the return value of cp_init_rings(). If the
RX buffer refill fails there, cp_clean_rings() has already dropped
all rx_skb entries, yet the driver restarts the hardware and
schedules NAPI unconditionally. cp_rx_poll() then dereferences a
NULL rx_skb slot and hits BUG_ON(!skb), crashing the kernel; the RX
ring also stays empty until the next close/open cycle.
On refill failure, keep the queue stopped and leave the device down
until the next open instead of restarting into a guaranteed BUG.
Fixes: 9030c0d24dbb ("8139cp: implement the missing dev->tx_timeout")
Signed-off-by: Yun Lu <luyun@kylinos.cn>
---
drivers/net/ethernet/realtek/8139cp.c | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/drivers/net/ethernet/realtek/8139cp.c b/drivers/net/ethernet/realtek/8139cp.c
index 03929bb0bd79..968bc0a2086e 100644
--- a/drivers/net/ethernet/realtek/8139cp.c
+++ b/drivers/net/ethernet/realtek/8139cp.c
@@ -1273,7 +1273,13 @@ static void cp_tx_timeout(struct net_device *dev, unsigned int txqueue)
cp_stop_hw(cp);
cp_clean_rings(cp);
- cp_init_rings(cp);
+ if (cp_init_rings(cp)) {
+ netif_err(cp, tx_err, dev,
+ "Failed to allocate RX buffers, not restarting\n");
+ netif_stop_queue(dev);
+ spin_unlock_irqrestore(&cp->lock, flags);
+ return;
+ }
cp_start_hw(cp);
__cp_set_rx_mode(dev);
cpw16_f(IntrMask, cp_norx_intr_mask);
--
2.25.1
^ permalink raw reply related
* [PATCH 01/13] 8139cp: fix DMA mapping unwind on TX frag map failure
From: Yun Lu @ 2026-07-17 10:37 UTC (permalink / raw)
To: andrew+netdev, davem, edumazet, kuba, pabeni; +Cc: netdev
In-Reply-To: <20260717103724.13971-1-luyun_611@163.com>
From: Yun Lu <luyun@kylinos.cn>
When dma_map_single() fails for one of the TX fragments in
cp_start_xmit(), unwind_tx_frag_mapping() is supposed to drop the
DMA mappings that have been established so far. The current code
gets this wrong in three ways:
- it unmaps the DMA address read from the descriptor of the head
slot, but the head descriptor is only written after the fragment
loop, so a stale address of a previously completed (and already
unmapped) packet gets unmapped again;
- the unmap sizes are off by one slot: slot first_entry + 1 + k
holds frags[k], but is unmapped with the size of frags[k + 1];
- the head buffer mapping (first_mapping) is never unmapped at all,
leaking one DMA mapping on every failure.
Pass the head mapping and its length to the unwind helper, unmap it
explicitly, and unmap the successfully mapped fragments with their
matching sizes.
Fixes: cf3c4c03060b ("8139cp: Add dma_mapping_error checking")
Signed-off-by: Yun Lu <luyun@kylinos.cn>
---
drivers/net/ethernet/realtek/8139cp.c | 23 ++++++++++++++++++-----
1 file changed, 18 insertions(+), 5 deletions(-)
diff --git a/drivers/net/ethernet/realtek/8139cp.c b/drivers/net/ethernet/realtek/8139cp.c
index 5652da8a178c..03929bb0bd79 100644
--- a/drivers/net/ethernet/realtek/8139cp.c
+++ b/drivers/net/ethernet/realtek/8139cp.c
@@ -713,13 +713,24 @@ static inline u32 cp_tx_vlan_tag(struct sk_buff *skb)
}
static void unwind_tx_frag_mapping(struct cp_private *cp, struct sk_buff *skb,
+ dma_addr_t first_mapping, u32 first_len,
int first, int entry_last)
{
int frag, index;
- struct cp_desc *txd;
- skb_frag_t *this_frag;
- for (frag = 0; frag+first < entry_last; frag++) {
- index = first+frag;
+
+ /* The head mapping was never written to the ring, unmap it
+ * using the saved address.
+ */
+ dma_unmap_single(&cp->pdev->dev, first_mapping, first_len,
+ DMA_TO_DEVICE);
+ cp->tx_skb[first] = NULL;
+
+ /* Unmap the frags that were successfully mapped. */
+ for (frag = 0; first + 1 + frag < entry_last; frag++) {
+ struct cp_desc *txd;
+ skb_frag_t *this_frag;
+
+ index = first + 1 + frag;
cp->tx_skb[index] = NULL;
txd = &cp->tx_ring[index];
this_frag = &skb_shinfo(skb)->frags[frag];
@@ -828,7 +839,9 @@ static netdev_tx_t cp_start_xmit (struct sk_buff *skb,
skb_frag_address(this_frag),
len, DMA_TO_DEVICE);
if (dma_mapping_error(&cp->pdev->dev, mapping)) {
- unwind_tx_frag_mapping(cp, skb, first_entry, entry);
+ unwind_tx_frag_mapping(cp, skb, first_mapping,
+ first_len, first_entry,
+ entry);
goto out_dma_error;
}
--
2.25.1
^ permalink raw reply related
* [PATCH net 0/2] 8139cp: fix two TX error handling bugs
From: Yun Lu @ 2026-07-17 10:37 UTC (permalink / raw)
To: andrew+netdev, davem, edumazet, kuba, pabeni; +Cc: netdev
From: Yun Lu <luyun@kylinos.cn>
Hi,
This short series fixes two bugs in the transmit error handling of
the 8139cp driver. Both are only reachable when DMA mapping or
memory allocation fails, but each can take the kernel down when
triggered.
The first patch rewrites the DMA mapping unwind in cp_start_xmit().
When mapping a TX fragment fails, the current helper unmaps the
stale address found in the not-yet-written head descriptor slot,
which is a double unmap of a previously completed mapping; the
fragment unmaps use sizes shifted by one slot; and the head buffer
mapping is never unmapped at all, leaking one DMA mapping per
failure.
The second patch makes cp_tx_timeout() handle cp_init_rings()
failure. If the RX buffer refill fails there, the ring is left
empty, but the driver restarts the hardware and schedules NAPI
anyway, so cp_rx_poll() dereferences a NULL rx_skb slot and hits
BUG_ON(!skb). Bail out and leave the device down until the next
open instead.
Thanks,
Yun
Yun Lu (2):
8139cp: fix DMA mapping unwind on TX frag map failure
8139cp: handle cp_init_rings() failure in cp_tx_timeout()
drivers/net/ethernet/realtek/8139cp.c | 31 +++++++++++++++++++++------
1 file changed, 25 insertions(+), 6 deletions(-)
--
2.25.1
^ permalink raw reply
* [PATCH] netconsole configfs target release NULL dereference
From: David Lee @ 2026-07-17 10:39 UTC (permalink / raw)
To: Breno Leitao
Cc: David Lee, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, netdev,
Dominik 'Disconnect3d' Czarnota, linux-kernel
configfs target release can call netpoll cleanup with missing netpoll state.
Mark targets as disabled before releasing their netpoll state and skip
cleanup when no netpoll device is present. This prevents teardown races
from reaching netpoll cleanup after nt->np.dev has already been cleared.
Fixes: 97714695ef90 ("net: netconsole: Defer netpoll cleanup to avoid lock release during list traversal")
Signed-off-by: David Lee <david.lee@trailofbits.com>
Assisted-by: Codex:gpt-5.5
---
drivers/net/netconsole.c | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/drivers/net/netconsole.c b/drivers/net/netconsole.c
index 862001d..ad30e2e 100644
--- a/drivers/net/netconsole.c
+++ b/drivers/net/netconsole.c
@@ -287,6 +287,8 @@ static bool bound_by_mac(struct netconsole_target *nt)
static void netcons_release_dev(struct netconsole_target *nt)
{
+ if (!nt->np.dev)
+ return;
do_netpoll_cleanup(&nt->np);
if (bound_by_mac(nt))
memset(&nt->np.dev_name, 0, IFNAMSIZ);
@@ -1490,10 +1492,12 @@ static void drop_netconsole_target(struct config_group *group,
*/
needs_cleanup = nt->state == STATE_ENABLED ||
nt->state == STATE_DEACTIVATED;
- /* Disable deactivated target to prevent races between resume attempt
- * and target removal.
+ /* Disable targets that still own netpoll state before removal. This
+ * prevents races between open configfs writers, resume attempts, and
+ * target removal from observing STATE_ENABLED after netpoll_cleanup()
+ * has cleared nt->np.dev.
*/
- if (nt->state == STATE_DEACTIVATED)
+ if (needs_cleanup)
nt->state = STATE_DISABLED;
list_del(&nt->list);
spin_unlock_irqrestore(&target_list_lock, flags);
^ permalink raw reply related
* Re: [PATCH nf-next] ipvs: use type-safe allocation helpers in ip_vs_rht_alloc
From: Phil Sutter @ 2026-07-17 10:39 UTC (permalink / raw)
To: Subasri S
Cc: Simon Horman, Julian Anastasov, Pablo Neira Ayuso,
Florian Westphal, David S. Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni, Simon Horman, netdev, lvs-devel, netfilter-devel,
coreteam, linux-kernel
In-Reply-To: <20260716-nf-next-ipvs-deprecated-v1-1-4aad24f87cb8@gmail.com>
On Thu, Jul 16, 2026 at 07:37:10PM +0530, Subasri S wrote:
> As per Documentation/process/deprecated.rst, open-coded kmalloc
> assignments for struct objects are deprecated. Replace
> kzalloc(sizeof(*ptr), GFP_KERNEL) with kzalloc_obj() and
> kvmalloc_array(n, sizeof(*ptr), GFP_KERNEL) with kvmalloc_objs()
> in ip_vs_rht_alloc().
>
> Compile tested with CONFIG_IP_VS=y and runtime tested using
> tools/testing/selftests/net/netfilter/ipvs.sh on x86_64/QEMU.
>
> Signed-off-by: Subasri S <subasris1210@gmail.com>
Reviewed-by: Phil Sutter <phil@nwl.cc>
^ permalink raw reply
* Re: [PATCH net] sctp: fix auth_hmacs array size in struct sctp_cookie
From: patchwork-bot+netdevbpf @ 2026-07-17 10:40 UTC (permalink / raw)
To: Xin Long
Cc: netdev, linux-sctp, davem, kuba, edumazet, pabeni, horms,
marcelo.leitner, yuantan098, dstsmallbird, xizh2024, enjou1224z
In-Reply-To: <634a0de0d5de29532915e6d47c92a0cbc206e03f.1783707155.git.lucien.xin@gmail.com>
Hello:
This patch was applied to netdev/net.git (main)
by Paolo Abeni <pabeni@redhat.com>:
On Fri, 10 Jul 2026 14:12:35 -0400 you wrote:
> The auth_hmacs array in struct sctp_cookie is supposed to store a complete
> SCTP_AUTH_HMAC_ALGO parameter, which consists of a struct sctp_paramhdr
> followed by N HMAC identifiers.
>
> However, the array size was calculated using an extra 2 bytes instead of
> sizeof(struct sctp_paramhdr), which is 4 bytes. When four HMAC identifiers
> are configured, the HMAC-ALGO parameter stored in the endpoint is larger
> than the auth_hmacs buffer in the cookie.
>
> [...]
Here is the summary with links:
- [net] sctp: fix auth_hmacs array size in struct sctp_cookie
https://git.kernel.org/netdev/net/c/e0b5252a5938
You are awesome, thank you!
--
Deet-doot-dot, I am a bot.
https://korg.docs.kernel.org/patchwork/pwbot.html
^ permalink raw reply
* Re: Please backport bridge multicast exponential field encoding fix series to 6.1.y/6.6.y/6.12.y/6.18.y/7.0.y
From: Ujjal Roy @ 2026-07-17 10:42 UTC (permalink / raw)
To: Andrew Lunn
Cc: Greg KH, Linux Stable, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Nikolay Aleksandrov,
Ido Schimmel, David Ahern, Shuah Khan, Andy Roulin, Yong Wang,
Petr Machata, Ujjal Roy, bridge, netdev, linux-kernel,
linux-kselftest
In-Reply-To: <797773e2-b155-451b-ae97-e8a005fd2d86@lunn.ch>
> It does not force the users to use a value < 128. You would need to
> return EINVAL for that, which i'm not proposing. Returning an error
> could break user space.
>
While I understand that clamping avoids breaking user space, silently
modifying the value might lead to unexpected behavior that is
difficult for users to debug.
Because of these side effects, I am hesitant to proceed without the
Multicast maintainer's input. We should raise an open issue on this
topic to get their official decision, otherwise my preference is to
ignore patching this for the stable kernel versions entirely.
> By clamping to 127, we don't break user space, but we do avoid the
> kernel bug, and at least to my superficial reading of the RFC, we are
> "language lawyer" compliant with the RFC.
>
> Andrew
^ permalink raw reply
* Re: [PATCH net v2] bnxt_en: Handle partially initialized auxiliary devices
From: patchwork-bot+netdevbpf @ 2026-07-17 10:50 UTC (permalink / raw)
To: Ruoyu Wang
Cc: michael.chan, pavan.chebbi, andrew+netdev, davem, edumazet, kuba,
pabeni, jacob.e.keller, andrew.gospodarek, vikas.gupta, netdev,
linux-kernel
In-Reply-To: <20260711163716.3996929-1-ruoyuw560@gmail.com>
Hello:
This patch was applied to netdev/net.git (main)
by Paolo Abeni <pabeni@redhat.com>:
On Sun, 12 Jul 2026 00:37:16 +0800 you wrote:
> bnxt_aux_devices_init() calls auxiliary_device_init() before all fields
> used by bnxt_aux_dev_release() are initialized. After
> auxiliary_device_init() succeeds, later errors must unwind with
> auxiliary_device_uninit(), which invokes the release callback.
>
> The release callback assumes that aux_priv->id, aux_priv->edev,
> edev->net and edev->ulp_tbl are all populated. If allocation fails
> after auxiliary_device_init(), the release path can otherwise dereference
> or clear partially initialized state.
>
> [...]
Here is the summary with links:
- [net,v2] bnxt_en: Handle partially initialized auxiliary devices
https://git.kernel.org/netdev/net/c/1cb8553c02e9
You are awesome, thank you!
--
Deet-doot-dot, I am a bot.
https://korg.docs.kernel.org/patchwork/pwbot.html
^ permalink raw reply
page: next (older) | prev (newer) | latest
- recent:[subjects (threaded)|topics (new)|topics (active)]
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox