From 8d07874923907a19653855d1e68193972e27866a Mon Sep 17 00:00:00 2001 From: Daniel Golle Date: Sun, 22 Mar 2026 00:58:04 +0000 Subject: [PATCH 1/3] net: dsa: mxl862xx: map every allocated VBP in host bridge entries In tag_8021q mode, host FDB and MDB entries installed into a shared bridge FID carry a portmap of virtual bridge ports. The bridging engine ANDs that portmap with the ingress port's bridge_port_map, which holds only that port's own VBP, so the intersection selects the ingress port's VBP and with it the egress EVLAN that tags the frame with the management VID identifying the source port to DSA. Building the portmap from the current bridge members reads priv->ports[].bridge_port_cpu for each member. A member whose VBP has not been allocated yet reads back as 0, and mxl862xx_fw_portmap_set_bit() sets bit 0 for it -- bit 0 is a valid portmap bit belonging to another port, not a no-op, so the host entry is installed pointing at the wrong bridge port. The resync and drop-VBP callbacks run on bridge join and leave attempt to repair this after the fact, which leaves a window in which host-terminated traffic is misdirected, and depends on every reordering of DSA's setup sequence being covered. List every user port's VBP unconditionally instead, skipping ports whose VBP is not allocated. The hardware AND against bridge_port_map already encodes membership, so the wider portmap resolves to the same single VBP per ingress port while removing both the ordering dependency and the need to track membership changes in the entry itself. Signed-off-by: Daniel Golle [Mihai Ordean: extracted from a7fdaf365e22 ("net: dsa: add 802.1Q VLAN-based tag driver for MxL862xx") in https://github.com/dangowrt/linux, which revises the earlier version of that commit already carried in this tree; commit message and kernel-doc expanded to record the bit 0 aliasing, the code is unchanged from the original] Signed-off-by: Mihai Ordean --- drivers/net/dsa/mxl862xx/mxl862xx.c | 34 +++++++++++++++++++---------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/drivers/net/dsa/mxl862xx/mxl862xx.c b/drivers/net/dsa/mxl862xx/mxl862xx.c index 5bde382a8290..eb5914ff8644 100644 --- a/drivers/net/dsa/mxl862xx/mxl862xx.c +++ b/drivers/net/dsa/mxl862xx/mxl862xx.c @@ -2361,15 +2361,24 @@ static int mxl862xx_mac_portmap_del(struct mxl862xx_priv *priv, * @ds: DSA switch * @addr: MAC address * @vid: VLAN ID - * @bridge: bridge whose members' VBPs to include + * @bridge: bridge the entry is scoped to (used to pick the FID) * * In tag_8021q mode, host FDB/MDB entries in a shared bridge FID must use - * portmap mode targeting ALL bridge members' virtual bridge ports (VBPs). - * The firmware ANDs the entry's portmap with each ingress port's - * bridge_port_map, which contains only that port's own VBP. This - * selects the correct VBP per ingress port, ensuring frames exit - * through the right egress EVLAN (which inserts the per-port management - * VID that identifies the source port to DSA on the CPU side). + * portmap mode targeting virtual bridge ports (VBPs). The firmware ANDs + * the entry's portmap with the ingress port's bridge_port_map, which + * contains only that port's own VBP, so listing every user port's VBP + * unconditionally still leaves exactly the ingress port's VBP after the + * intersection. That selects the correct egress EVLAN, which inserts the + * per-port management VID identifying the source port to DSA on the CPU + * side. + * + * Listing all user ports rather than the current bridge members keeps the + * entry correct without tracking membership changes, and avoids depending + * on every member's VBP having been allocated by the time the entry is + * installed. A port whose VBP is not yet allocated reads back as 0, and + * mxl862xx_fw_portmap_set_bit() would then set bit 0 -- a valid portmap + * bit belonging to another port, not a no-op -- corrupting the portmap of + * an entry the host depends on. Skip unallocated VBPs explicitly. */ static int mxl862xx_mac_add_host_bridge(struct dsa_switch *ds, const unsigned char *addr, u16 vid, @@ -2378,11 +2387,14 @@ static int mxl862xx_mac_add_host_bridge(struct dsa_switch *ds, __le16 add_map[MXL862XX_FW_PORTMAP_WORDS] = {}; struct mxl862xx_priv *priv = ds->priv; u16 fid = priv->bridges[bridge->num]; - struct dsa_port *member_dp; + struct dsa_port *dp; + u16 vbp; - dsa_switch_for_each_bridge_member(member_dp, ds, bridge->dev) - mxl862xx_fw_portmap_set_bit(add_map, - priv->ports[member_dp->index].bridge_port_cpu); + dsa_switch_for_each_user_port(dp, ds) { + vbp = priv->ports[dp->index].bridge_port_cpu; + if (vbp) + mxl862xx_fw_portmap_set_bit(add_map, vbp); + } return mxl862xx_mac_portmap_add(priv, addr, fid, vid, add_map); } From cad493f7fc43e141c2ef78c03e47781f67ebd919 Mon Sep 17 00:00:00 2001 From: Daniel Golle Date: Tue, 24 Mar 2026 18:51:13 +0000 Subject: [PATCH 2/3] net: dsa: mxl862xx: never learn from CPU-trapped management frames The per-port PCE trap rules (IEEE 802.1D link-local, IGMP, MLDv1/v2) redirect matching frames to the CPU port with an alternative portmap and override the lookup FID to the dedicated snooping FID. The rule action leaves learning_action at 0, so the hardware keeps learning the source address of every trapped frame under the regular port and bridge configuration. Trapped management frames must not populate the MAC table: they are diverted from the normal forwarding path, and any entry created from them reflects the trap override rather than the bridge topology. On switch firmware 1.0.85, tag_8021q mode stops forwarding entirely (R3) while the native SpTag mode keeps working; in tag_8021q mode correct forwarding depends on static host FDB entries carrying virtual bridge port portmaps, which learned entries for the same {MAC, FID} can displace or shadow. This trap-rule learning is the last behavioural difference between this driver's trap path and the reference driver in the dangowrt/linux tree at 07c32ae88874, which sets LEARNING_FORCE_NOT here and passes bridge selftests in tag_8021q mode on firmware >= 1.0.84. Set learning_action to LEARNING_FORCE_NOT in the shared trap action fill helper so the source addresses of trapped frames are never learned, and add the mxl862xx_pce_action_learning enum, which this driver documented in prose but never defined. Signed-off-by: Daniel Golle [meehien: extracted from f82da48b6323 ("net: dsa: mxl862xx: trap link-local and multicast snooping frames to CPU") in https://github.com/dangowrt/linux, which revises the earlier version of that commit already carried in this tree; commit message rewritten to explain the effect on tag_8021q forwarding under firmware 1.0.85, the code is unchanged from the original] Signed-off-by: Mihai Ordean --- drivers/net/dsa/mxl862xx/mxl862xx-api.h | 25 ++++++++++++++++++++++--- drivers/net/dsa/mxl862xx/mxl862xx.c | 3 +++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/drivers/net/dsa/mxl862xx/mxl862xx-api.h b/drivers/net/dsa/mxl862xx/mxl862xx-api.h index 874d5126f1a4..d3fe124806ae 100644 --- a/drivers/net/dsa/mxl862xx/mxl862xx-api.h +++ b/drivers/net/dsa/mxl862xx/mxl862xx-api.h @@ -2131,6 +2131,26 @@ enum mxl862xx_pce_action_cross_state { MXL862XX_PCE_ACTION_CROSS_STATE_CROSS = 2, }; +/** + * enum mxl862xx_pce_action_learning - MAC learning action selector + * + * Controls source address learning for packets matching the rule. + * + * @MXL862XX_PCE_ACTION_LEARNING_DISABLE: Learning action is disabled + * @MXL862XX_PCE_ACTION_LEARNING_REGULAR: Enabled; learning follows the + * regular port and bridge configuration + * @MXL862XX_PCE_ACTION_LEARNING_FORCE_NOT: Enabled; the source address + * of matching packets is never learned + * @MXL862XX_PCE_ACTION_LEARNING_FORCE: Enabled; the source address of + * matching packets is always learned + */ +enum mxl862xx_pce_action_learning { + MXL862XX_PCE_ACTION_LEARNING_DISABLE = 0, + MXL862XX_PCE_ACTION_LEARNING_REGULAR = 1, + MXL862XX_PCE_ACTION_LEARNING_FORCE_NOT = 2, + MXL862XX_PCE_ACTION_LEARNING_FORCE = 3, +}; + /** * struct mxl862xx_pce_action - PCE rule action configuration * @@ -2161,9 +2181,8 @@ enum mxl862xx_pce_action_cross_state { * @traffic_class_action: Traffic class action selector * (0 = disable, 1 = regular CoS, 2 = alternative) * @snooping_type_action: IGMP snooping control selector - * @learning_action: MAC learning action selector - * (0 = disable, 1 = regular, 2 = force no learn, - * 3 = force learn) + * @learning_action: MAC learning action selector. + * See &enum mxl862xx_pce_action_learning * @irq_action: Interrupt action selector * (0 = disable, 1 = regular, 2 = generate interrupt) * @cross_state_action: Cross state action selector. diff --git a/drivers/net/dsa/mxl862xx/mxl862xx.c b/drivers/net/dsa/mxl862xx/mxl862xx.c index eb5914ff8644..86a65027cc97 100644 --- a/drivers/net/dsa/mxl862xx/mxl862xx.c +++ b/drivers/net/dsa/mxl862xx/mxl862xx.c @@ -721,6 +721,9 @@ static void mxl862xx_fill_cpu_trap_action(struct dsa_switch *ds, int port, rule->action.cross_state_action = cpu_to_le32(MXL862XX_PCE_ACTION_CROSS_STATE_CROSS); + rule->action.learning_action = + cpu_to_le32(MXL862XX_PCE_ACTION_LEARNING_FORCE_NOT); + rule->action.fid_enable = 1; rule->action.fid = priv->cpu_trap_fid; } From 43ab559c2df974e7db0923f5687a1210c302e46c Mon Sep 17 00:00:00 2001 From: Mihai Ordean Date: Sat, 8 Aug 2026 11:48:09 +0000 Subject: [PATCH 3/3] net: dsa: mxl862xx: always write the virtual bridge port map In tag_8021q mode every user port gets a virtual bridge port on the CPU RX path. mxl862xx_setup_virtual_bridge_port() populates its bridge port map with the single user port the VBP belongs to, and nothing writes that field again: mxl862xx_set_cpu_vbp() updates the same bridge port with a mask covering only the forwarding ID and the egress sub-meters, leaving bridge_port_map zeroed in the request. That relies on the firmware preserving a field its caller did not select. On MxL86252C firmware 1.0.85 it does not, and mxl862xx_complete_tag_8021q_setup() calls mxl862xx_set_cpu_vbp() for every port immediately after the VBPs are allocated, so the map is already gone before the first frame is forwarded. The map is how a CPU-originated frame reaches the wire: the CPU port's ingress rules reassign the frame onto the VBP, the destination lookup runs in the VBP's per-port FID where nothing was learned, and the resulting flood has no member to flood to. The symptom is that the switch forwards between its own ports normally while every frame the host originates is lost, in both directions and on every user port. Write the port map on every VBP update and select it in the mask, so the driver never depends on the firmware retaining an unselected field. This is correct on any firmware; where the field was being preserved the write is idempotent. Confirmed on a BPI-R4 Pro (MxL86252C, firmware 1.0.85 build 85): without this change host-originated traffic is lost entirely; with it host and switch-local traffic work in every direction, including DHCP and DHCPv6-PD. Selecting the mask bit while writing an empty map reproduces the failure exactly, so it is the map contents that matter and not the act of selecting the field. Signed-off-by: Mihai Ordean --- drivers/net/dsa/mxl862xx/mxl862xx.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/drivers/net/dsa/mxl862xx/mxl862xx.c b/drivers/net/dsa/mxl862xx/mxl862xx.c index 86a65027cc97..2f8ec42cbdb3 100644 --- a/drivers/net/dsa/mxl862xx/mxl862xx.c +++ b/drivers/net/dsa/mxl862xx/mxl862xx.c @@ -1494,6 +1494,12 @@ static int mxl862xx_configure_sp_tag_proto(struct dsa_switch *ds, int port, * Per-port host flood control is implemented via egress sub-meters on * the VBP. * + * The bridge port map is written on every update rather than only at + * allocation time. It contains just the user port the VBP belongs to, + * and it is the path by which a CPU-originated frame reassigned onto + * the VBP reaches the wire, so it must never be left to the firmware + * to preserve across an unrelated field update. + * * This is intentionally separate from mxl862xx_set_bridge_port() because * the VBP and the physical bridge port are independent firmware entities: * host flood changes (deferred from atomic context) only need the VBP @@ -1513,8 +1519,10 @@ static int mxl862xx_set_cpu_vbp(struct dsa_switch *ds, int port) vbp_cfg.bridge_port_id = cpu_to_le16(p->bridge_port_cpu); vbp_cfg.mask = cpu_to_le32( MXL862XX_BRIDGE_PORT_CONFIG_MASK_BRIDGE_ID | + MXL862XX_BRIDGE_PORT_CONFIG_MASK_BRIDGE_PORT_MAP | MXL862XX_BRIDGE_PORT_CONFIG_MASK_EGRESS_SUB_METER); vbp_cfg.bridge_id = cpu_to_le16(p->fid); + mxl862xx_fw_portmap_set_bit(vbp_cfg.bridge_port_map, port); for (i = 0; i < ARRAY_SIZE(mxl862xx_flood_meters); i++) { idx = mxl862xx_flood_meters[i];