Netdev List
 help / color / mirror / Atom feed
* [PATCH v3 net-next 0/8] net: dsa: netc: add PTP support for NETC switch
@ 2026-08-31  8:23 wei.fang
  2026-08-31  8:23 ` [PATCH v3 net-next 1/8] ptp: netc: use ioread64_lo_hi/iowrite64_lo_hi for 64-bit register access wei.fang
                   ` (8 more replies)
  0 siblings, 9 replies; 16+ messages in thread
From: wei.fang @ 2026-08-31  8:23 UTC (permalink / raw)
  To: xiaoning.wang, andrew, olteanv, andrew+netdev, davem, edumazet,
	kuba, pabeni, horms, richardcochran
  Cc: wei.fang, imx, netdev, linux-kernel, linuxppc-dev,
	linux-arm-kernel

From: Wei Fang <wei.fang@nxp.com>

This patch set adds PTP timestamping support to the NETC switch, so that
the switch ports can be used for IEEE 1588 PTP synchronization.

The NETC switch has no time registers of its own and instead shares the
PTP time of the NETC Timer, which is a separate PCIe function driven by
its own driver. The first 3 patches prepare the Timer driver for this
role: tidy up its 64-bit register access, drop an ineffective reset, and
export netc_timer_get_current_time() so the switch driver can read the
current PTP time across the driver boundary in a race-free way.

The switch redirects PTP frames to the CPU through the ingress port
filter table (IPFT). A few patches harden and simplify the existing IPFT
and host flood handling first, then enable the ingress port filtering
lookup by default so PTP filter rules take effect without extra software
tracking.

On top of that groundwork, the switch driver gains RX timestamping and
both flavours of TX timestamping. Timestamps are carried between the
hardware and the driver through dedicated switch tags: the ingress and
transmit timestamps ride in To_Host tags, and per-frame requests ride in
To_Port tags. Two-step TX matches a queued skb clone against the
timestamp response echoed back by the hardware. One-step TX defers the
Sync frame to a per-port work, where the driver reads a fresh PTP time
and writes it into the Sync frame's timestamp field, then programs the
single-step register and transmits the frame so the hardware can add the
residence time to the correction field on the wire.

Together these changes let standard user-space PTP stacks drive hardware
timestamping on the NETC switch ports.

---
v3:
1. Change the prototype of netc_timer_get_current_time(), and check the
   pdev driver before accessing the driver data
2. Patch 5 is a new patch
3. Add netc_port_tstamp_timeout_work()
4. Check the return value of ntmp_ipft_delete_entry(), and in
   netc_port_add_ipft_ptp_entry(), check whether the entry already exists
   in IPFT before creating a new entry
5. Remove netc_port_purge_onestep_queue() from netc_port_disable()
6. Add netc_port_purge_onestep_queue() to netc_onestep_destroy_work()
7. Add try_module_get() to netc_port_onestep_alloc() and add module_put()
   to netc_onestep_destroy_work()
8. Use consume_skb() instead of the err_free_skb label calls kfree_skb()
   for NETC_TAG_TH_SUBTYPE2 frames
9. Add subtype check in netc_flow_dissect() and update the comments
10. Check onestep queue length before enqueue
11. Update commit messages
v2 link: https://lore.kernel.org/imx/20260808032146.2335723-1-wei.fang@oss.nxp.com/
v1 link: https://lore.kernel.org/imx/20260728104548.3301214-1-wei.fang@oss.nxp.com/
---

Wei Fang (8):
  ptp: netc: use ioread64_lo_hi/iowrite64_lo_hi for 64-bit register
    access
  ptp: netc: remove unnecessary pcie_flr() call in probe
  ptp: netc: export netc_timer_get_current_time() for cross-driver use
  net: dsa: netc: use entry ID instead of pointer to track host flood
    rule
  net: dsa: netc: check return value of ntmp_ipft_delete_entry()
  net: dsa: netc: enable ingress port filtering lookup by default
  net: dsa: netc: add PTP two-step timestamping support
  net: dsa: netc: add PTP one-step timestamping support

 drivers/net/dsa/netc/Kconfig          |   2 +
 drivers/net/dsa/netc/Makefile         |   3 +-
 drivers/net/dsa/netc/netc_main.c      | 252 +++++--
 drivers/net/dsa/netc/netc_platform.c  |   1 +
 drivers/net/dsa/netc/netc_ptp.c       | 904 ++++++++++++++++++++++++++
 drivers/net/dsa/netc/netc_switch.h    |  93 ++-
 drivers/net/dsa/netc/netc_switch_hw.h |   5 +
 drivers/ptp/ptp_netc.c                | 125 ++--
 include/linux/dsa/tag_netc.h          |  43 ++
 include/linux/fsl/netc_global.h       |  10 +
 net/dsa/tag_netc.c                    | 236 ++++++-
 11 files changed, 1551 insertions(+), 123 deletions(-)
 create mode 100644 drivers/net/dsa/netc/netc_ptp.c

-- 
2.34.1


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

* [PATCH v3 net-next 1/8] ptp: netc: use ioread64_lo_hi/iowrite64_lo_hi for 64-bit register access
  2026-08-31  8:23 [PATCH v3 net-next 0/8] net: dsa: netc: add PTP support for NETC switch wei.fang
@ 2026-08-31  8:23 ` wei.fang
       [not found]   ` <20260901082024.E6F931F000E9@smtp.kernel.org>
  2026-08-31  8:23 ` [PATCH v3 net-next 2/8] ptp: netc: remove unnecessary pcie_flr() call in probe wei.fang
                   ` (7 subsequent siblings)
  8 siblings, 1 reply; 16+ messages in thread
From: wei.fang @ 2026-08-31  8:23 UTC (permalink / raw)
  To: xiaoning.wang, andrew, olteanv, andrew+netdev, davem, edumazet,
	kuba, pabeni, horms, richardcochran
  Cc: wei.fang, imx, netdev, linux-kernel, linuxppc-dev,
	linux-arm-kernel

From: Wei Fang <wei.fang@nxp.com>

Replace the open-coded 64-bit register read/write sequences with
ioread64_lo_hi() and iowrite64_lo_hi() helpers. Introduce two new macros
netc_timer_rd64() and netc_timer_wr64() that wrap these helpers and use
them throughout the driver. This reduces boilerplate and makes the intent
of each operation clearer.

The high-half register defines (NETC_TMR_*_H) are kept to document the
register map; they are not used directly since netc_timer_rd/wr64()
address the 64-bit register pair via the low-half offset, relying on the
hardware layout where H is always at L + 4.

Signed-off-by: Wei Fang <wei.fang@nxp.com>
---
 drivers/ptp/ptp_netc.c | 72 ++++++++++++------------------------------
 1 file changed, 21 insertions(+), 51 deletions(-)

diff --git a/drivers/ptp/ptp_netc.c b/drivers/ptp/ptp_netc.c
index 19ca99d80e95..376b3b0d1a38 100644
--- a/drivers/ptp/ptp_netc.c
+++ b/drivers/ptp/ptp_netc.c
@@ -127,6 +127,17 @@ struct netc_timer {
 
 #define netc_timer_rd(p, o)		netc_read((p)->base + (o))
 #define netc_timer_wr(p, o, v)		netc_write((p)->base + (o), v)
+
+/* The 64-bit timer registers consist of a low (L) and high (H) register pair.
+ * Hardware requires a strict access order: for writes, TMR_xxx_L must be
+ * written first, which latches the value into a shadow register; the write
+ * to TMR_xxx_H then atomically transfers both shadow registers into the live
+ * counter. For reads, TMR_xxx_L must be read first to capture a coherent
+ * snapshot. iowrite64_lo_hi() and ioread64_lo_hi() enforce this L-before-H
+ * ordering.
+ */
+#define netc_timer_rd64(p, o)		ioread64_lo_hi((p)->base + (o))
+#define netc_timer_wr64(p, o, v)	iowrite64_lo_hi(v, (p)->base + (o))
 #define ptp_to_netc_timer(ptp)		container_of((ptp), struct netc_timer, caps)
 
 static const char *const timer_clk_src[] = {
@@ -136,66 +147,28 @@ static const char *const timer_clk_src[] = {
 
 static void netc_timer_cnt_write(struct netc_timer *priv, u64 ns)
 {
-	u32 tmr_cnt_h = upper_32_bits(ns);
-	u32 tmr_cnt_l = lower_32_bits(ns);
-
-	/* Writes to the TMR_CNT_L register copies the written value
-	 * into the shadow TMR_CNT_L register. Writes to the TMR_CNT_H
-	 * register copies the values written into the shadow TMR_CNT_H
-	 * register. Contents of the shadow registers are copied into
-	 * the TMR_CNT_L and TMR_CNT_H registers following a write into
-	 * the TMR_CNT_H register. So the user must writes to TMR_CNT_L
-	 * register first. Other H/L registers should have the same
-	 * behavior.
-	 */
-	netc_timer_wr(priv, NETC_TMR_CNT_L, tmr_cnt_l);
-	netc_timer_wr(priv, NETC_TMR_CNT_H, tmr_cnt_h);
+	netc_timer_wr64(priv, NETC_TMR_CNT_L, ns);
 }
 
 static u64 netc_timer_offset_read(struct netc_timer *priv)
 {
-	u32 tmr_off_l, tmr_off_h;
-	u64 offset;
-
-	tmr_off_l = netc_timer_rd(priv, NETC_TMR_OFF_L);
-	tmr_off_h = netc_timer_rd(priv, NETC_TMR_OFF_H);
-	offset = (((u64)tmr_off_h) << 32) | tmr_off_l;
-
-	return offset;
+	return netc_timer_rd64(priv, NETC_TMR_OFF_L);
 }
 
 static void netc_timer_offset_write(struct netc_timer *priv, u64 offset)
 {
-	u32 tmr_off_h = upper_32_bits(offset);
-	u32 tmr_off_l = lower_32_bits(offset);
-
-	netc_timer_wr(priv, NETC_TMR_OFF_L, tmr_off_l);
-	netc_timer_wr(priv, NETC_TMR_OFF_H, tmr_off_h);
+	netc_timer_wr64(priv, NETC_TMR_OFF_L, offset);
 }
 
 static u64 netc_timer_cur_time_read(struct netc_timer *priv)
 {
-	u32 time_h, time_l;
-	u64 ns;
-
-	/* The user should read NETC_TMR_CUR_TIME_L first to
-	 * get correct current time.
-	 */
-	time_l = netc_timer_rd(priv, NETC_TMR_CUR_TIME_L);
-	time_h = netc_timer_rd(priv, NETC_TMR_CUR_TIME_H);
-	ns = (u64)time_h << 32 | time_l;
-
-	return ns;
+	return netc_timer_rd64(priv, NETC_TMR_CUR_TIME_L);
 }
 
 static void netc_timer_alarm_write(struct netc_timer *priv,
 				   u64 alarm, int index)
 {
-	u32 alarm_h = upper_32_bits(alarm);
-	u32 alarm_l = lower_32_bits(alarm);
-
-	netc_timer_wr(priv, NETC_TMR_ALARM_L(index), alarm_l);
-	netc_timer_wr(priv, NETC_TMR_ALARM_H(index), alarm_h);
+	netc_timer_wr64(priv, NETC_TMR_ALARM_L(index), alarm);
 }
 
 static u32 netc_timer_get_integral_period(struct netc_timer *priv)
@@ -500,22 +473,19 @@ static void netc_timer_handle_etts_event(struct netc_timer *priv, int index,
 					 bool update_event)
 {
 	struct ptp_clock_event event;
-	u32 etts_l = 0, etts_h = 0;
+	u64 etts = 0;
 
-	while (netc_timer_rd(priv, NETC_TMR_STAT) & TMR_STAT_ETS_VLD(index)) {
-		etts_l = netc_timer_rd(priv, NETC_TMR_ETTS_L(index));
-		etts_h = netc_timer_rd(priv, NETC_TMR_ETTS_H(index));
-	}
+	while (netc_timer_rd(priv, NETC_TMR_STAT) & TMR_STAT_ETS_VLD(index))
+		etts = netc_timer_rd64(priv, NETC_TMR_ETTS_L(index));
 
 	/* Invalid time stamp */
-	if (!etts_l && !etts_h)
+	if (!etts)
 		return;
 
 	if (update_event) {
 		event.type = PTP_CLOCK_EXTTS;
 		event.index = index;
-		event.timestamp = (u64)etts_h << 32;
-		event.timestamp |= etts_l;
+		event.timestamp = etts;
 		ptp_clock_event(priv->clock, &event);
 	}
 }
-- 
2.34.1


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

* [PATCH v3 net-next 2/8] ptp: netc: remove unnecessary pcie_flr() call in probe
  2026-08-31  8:23 [PATCH v3 net-next 0/8] net: dsa: netc: add PTP support for NETC switch wei.fang
  2026-08-31  8:23 ` [PATCH v3 net-next 1/8] ptp: netc: use ioread64_lo_hi/iowrite64_lo_hi for 64-bit register access wei.fang
@ 2026-08-31  8:23 ` wei.fang
  2026-08-31  8:23 ` [PATCH v3 net-next 3/8] ptp: netc: export netc_timer_get_current_time() for cross-driver use wei.fang
                   ` (6 subsequent siblings)
  8 siblings, 0 replies; 16+ messages in thread
From: wei.fang @ 2026-08-31  8:23 UTC (permalink / raw)
  To: xiaoning.wang, andrew, olteanv, andrew+netdev, davem, edumazet,
	kuba, pabeni, horms, richardcochran
  Cc: wei.fang, imx, netdev, linux-kernel, linuxppc-dev,
	linux-arm-kernel

From: Wei Fang <wei.fang@nxp.com>

According to the NETC reference manual, function level reset is not
applicable to the timer as a supporting function. Remove the pcie_flr()
call from netc_timer_pci_probe() as it has no effect.

Signed-off-by: Wei Fang <wei.fang@nxp.com>
---
 drivers/ptp/ptp_netc.c | 1 -
 1 file changed, 1 deletion(-)

diff --git a/drivers/ptp/ptp_netc.c b/drivers/ptp/ptp_netc.c
index 376b3b0d1a38..aa9be8e2a630 100644
--- a/drivers/ptp/ptp_netc.c
+++ b/drivers/ptp/ptp_netc.c
@@ -777,7 +777,6 @@ static int netc_timer_pci_probe(struct pci_dev *pdev)
 	if (!priv)
 		return -ENOMEM;
 
-	pcie_flr(pdev);
 	err = pci_enable_device_mem(pdev);
 	if (err)
 		return dev_err_probe(dev, err, "Failed to enable device\n");
-- 
2.34.1


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

* [PATCH v3 net-next 3/8] ptp: netc: export netc_timer_get_current_time() for cross-driver use
  2026-08-31  8:23 [PATCH v3 net-next 0/8] net: dsa: netc: add PTP support for NETC switch wei.fang
  2026-08-31  8:23 ` [PATCH v3 net-next 1/8] ptp: netc: use ioread64_lo_hi/iowrite64_lo_hi for 64-bit register access wei.fang
  2026-08-31  8:23 ` [PATCH v3 net-next 2/8] ptp: netc: remove unnecessary pcie_flr() call in probe wei.fang
@ 2026-08-31  8:23 ` wei.fang
       [not found]   ` <20260901082025.AF94B1F00A3D@smtp.kernel.org>
  2026-08-31  8:23 ` [PATCH v3 net-next 4/8] net: dsa: netc: use entry ID instead of pointer to track host flood rule wei.fang
                   ` (5 subsequent siblings)
  8 siblings, 1 reply; 16+ messages in thread
From: wei.fang @ 2026-08-31  8:23 UTC (permalink / raw)
  To: xiaoning.wang, andrew, olteanv, andrew+netdev, davem, edumazet,
	kuba, pabeni, horms, richardcochran
  Cc: wei.fang, imx, netdev, linux-kernel, linuxppc-dev,
	linux-arm-kernel

From: Wei Fang <wei.fang@nxp.com>

The NETC Switch does not have its own time registers and must obtain
the current PTP time from the NETC Timer bound to it. Since the two
are separate PCIe functions with independent drivers, add
netc_timer_get_current_time() to the Timer driver and export it via
EXPORT_SYMBOL_GPL().

The function takes the Timer's pci_dev pointer, acquires the per-device
spinlock to protect against concurrent register access, and reads
TMR_CUR_TIME via netc_timer_cur_time_read(). It returns an error if the
Timer driver has not yet probed or has already been removed, allowing
the caller to handle the unavailable case gracefully.

Hold the device lock around pci_get_drvdata() and the register read to
serialize against concurrent driver unbind. The remove() callback runs
under the same device lock, so this guarantees that priv and the MMIO
mapping remain valid for the entire duration of the read.

Move spin_lock_init() from netc_timer_probe() to netc_timer_pci_probe(),
before pci_set_drvdata(), so that the spinlock is fully initialized
before the driver data becomes visible to other drivers.

Signed-off-by: Wei Fang <wei.fang@nxp.com>
---
 drivers/ptp/ptp_netc.c          | 52 ++++++++++++++++++++++++++++++++-
 include/linux/fsl/netc_global.h | 10 +++++++
 2 files changed, 61 insertions(+), 1 deletion(-)

diff --git a/drivers/ptp/ptp_netc.c b/drivers/ptp/ptp_netc.c
index aa9be8e2a630..854a206b6c5b 100644
--- a/drivers/ptp/ptp_netc.c
+++ b/drivers/ptp/ptp_netc.c
@@ -798,6 +798,7 @@ static int netc_timer_pci_probe(struct pci_dev *pdev)
 		goto release_mem_regions;
 	}
 
+	spin_lock_init(&priv->lock);
 	pci_set_drvdata(pdev, priv);
 
 	return 0;
@@ -968,7 +969,6 @@ static int netc_timer_probe(struct pci_dev *pdev,
 	priv->caps = netc_timer_ptp_caps;
 	priv->oclk_prsc = NETC_TMR_DEFAULT_PRSC;
 	priv->pps_channel = NETC_TMR_INVALID_CHANNEL;
-	spin_lock_init(&priv->lock);
 	snprintf(priv->irq_name, sizeof(priv->irq_name), "ptp-netc %s",
 		 pci_name(pdev));
 
@@ -1021,5 +1021,55 @@ static struct pci_driver netc_timer_driver = {
 };
 module_pci_driver(netc_timer_driver);
 
+/**
+ * netc_timer_get_current_time - read the current PTP time from the NETC Timer
+ * @pdev: PCI device of the NETC Timer
+ * @ns: The current PTP clock time in nanoseconds, returned to the caller
+ *
+ * Reads the 64-bit current time register (TMR_CUR_TIME) from the NETC Timer
+ * device associated with @pdev. Returns an error if the Timer driver has not
+ * yet probed or has already been removed.
+ *
+ * Context: Process context only. Acquires the device mutex via device_lock(),
+ *          which may sleep. Must not be called from atomic context, softirq,
+ *          BH, or while holding a spinlock.
+ *
+ * Return: 0 on success, otherwise a negative error code.
+ */
+int netc_timer_get_current_time(struct pci_dev *pdev, u64 *ns)
+{
+	struct device *dev = &pdev->dev;
+	struct netc_timer *priv;
+	unsigned long flags;
+	int err = 0;
+
+	/* Serialize against driver unbind: the remove() callback runs under
+	 * the device lock, so holding it here ensures that priv remains valid
+	 * for the entire duration of the register read.
+	 */
+	device_lock(dev);
+
+	if (pci_dev_driver(pdev) != &netc_timer_driver) {
+		err = -EINVAL;
+		goto unlock_device;
+	}
+
+	priv = pci_get_drvdata(pdev);
+	if (!priv) {
+		err = -ENOMEM;
+		goto unlock_device;
+	}
+
+	spin_lock_irqsave(&priv->lock, flags);
+	*ns = netc_timer_cur_time_read(priv);
+	spin_unlock_irqrestore(&priv->lock, flags);
+
+unlock_device:
+	device_unlock(dev);
+
+	return err;
+}
+EXPORT_SYMBOL_GPL(netc_timer_get_current_time);
+
 MODULE_DESCRIPTION("NXP NETC Timer PTP Driver");
 MODULE_LICENSE("Dual BSD/GPL");
diff --git a/include/linux/fsl/netc_global.h b/include/linux/fsl/netc_global.h
index 5b8ff528d369..d4a26c17f99a 100644
--- a/include/linux/fsl/netc_global.h
+++ b/include/linux/fsl/netc_global.h
@@ -6,6 +6,7 @@
 
 #include <linux/io.h>
 #include <linux/io-64-nonatomic-lo-hi.h>
+#include <linux/pci.h>
 
 static inline u32 netc_read(void __iomem *reg)
 {
@@ -22,4 +23,13 @@ static inline u64 netc_read64(void __iomem *reg)
 	return ioread64(reg);
 }
 
+#if IS_REACHABLE(CONFIG_PTP_NETC_V4_TIMER)
+int netc_timer_get_current_time(struct pci_dev *pdev, u64 *ns);
+#else
+static inline int netc_timer_get_current_time(struct pci_dev *pdev, u64 *ns)
+{
+	return -ENODEV;
+}
+#endif
+
 #endif
-- 
2.34.1


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

* [PATCH v3 net-next 4/8] net: dsa: netc: use entry ID instead of pointer to track host flood rule
  2026-08-31  8:23 [PATCH v3 net-next 0/8] net: dsa: netc: add PTP support for NETC switch wei.fang
                   ` (2 preceding siblings ...)
  2026-08-31  8:23 ` [PATCH v3 net-next 3/8] ptp: netc: export netc_timer_get_current_time() for cross-driver use wei.fang
@ 2026-08-31  8:23 ` wei.fang
  2026-08-31  8:23 ` [PATCH v3 net-next 5/8] net: dsa: netc: check return value of ntmp_ipft_delete_entry() wei.fang
                   ` (4 subsequent siblings)
  8 siblings, 0 replies; 16+ messages in thread
From: wei.fang @ 2026-08-31  8:23 UTC (permalink / raw)
  To: xiaoning.wang, andrew, olteanv, andrew+netdev, davem, edumazet,
	kuba, pabeni, horms, richardcochran
  Cc: wei.fang, imx, netdev, linux-kernel, linuxppc-dev,
	linux-arm-kernel

From: Wei Fang <wei.fang@nxp.com>

Replace the struct ipft_entry_data pointer in struct netc_port with a
plain u32 entry ID (ipft_hf_eid), using NTMP_NULL_ENTRY_ID as the
sentinel value. The ipft_entry_data allocation is now freed immediately
inside netc_port_add_host_flood_rule() after the hardware entry is
committed, so no heap memory survives beyond that function. As a result,
netc_free_host_flood_rules() is no longer needed and can be removed.

Signed-off-by: Wei Fang <wei.fang@nxp.com>
---
 drivers/net/dsa/netc/netc_main.c   | 63 ++++++++++++------------------
 drivers/net/dsa/netc/netc_switch.h |  6 ++-
 2 files changed, 29 insertions(+), 40 deletions(-)

diff --git a/drivers/net/dsa/netc/netc_main.c b/drivers/net/dsa/netc/netc_main.c
index 77077352c1a5..d326a00104e1 100644
--- a/drivers/net/dsa/netc/netc_main.c
+++ b/drivers/net/dsa/netc/netc_main.c
@@ -286,6 +286,12 @@ static int netc_init_all_ports(struct netc_switch *priv)
 				dev_err(dev, "Failed to create MDIO bus\n");
 				return err;
 			}
+
+			/* The ipft_hf_eid is initialized to an invalid entry
+			 * ID because the host flood rule (IPFT entry) has not
+			 * been created.
+			 */
+			np->ipft_hf_eid = NTMP_NULL_ENTRY_ID;
 		}
 	}
 
@@ -938,30 +944,12 @@ static void netc_destroy_all_lists(struct netc_switch *priv)
 	mutex_destroy(&priv->vft_lock);
 }
 
-static void netc_free_host_flood_rules(struct netc_switch *priv)
-{
-	struct dsa_port *dp;
-
-	dsa_switch_for_each_user_port(dp, priv->ds) {
-		struct netc_port *np = priv->ports[dp->index];
-
-		/* No need to clear the hardware IPFT entry. Because PCIe
-		 * FLR will be performed when the switch is re-registered,
-		 * it will reset hardware state. So only need to free the
-		 * memory to avoid memory leak.
-		 */
-		kfree(np->host_flood);
-		np->host_flood = NULL;
-	}
-}
-
 static void netc_teardown(struct dsa_switch *ds)
 {
 	struct netc_switch *priv = ds->priv;
 
 	disable_delayed_work_sync(&priv->fdbt_ageing_work);
 	netc_destroy_all_lists(priv);
-	netc_free_host_flood_rules(priv);
 	netc_free_ntmp_user(priv);
 }
 
@@ -1759,37 +1747,36 @@ static int netc_port_add_host_flood_rule(struct netc_port *np,
 	cfge->cfg = cpu_to_le32(cfg);
 
 	err = ntmp_ipft_add_entry(&priv->ntmp, host_flood);
-	if (err) {
-		kfree(host_flood);
-		return err;
-	}
+	if (err)
+		goto free_host_flood;
 
 	np->uc = uc;
 	np->mc = mc;
-	np->host_flood = host_flood;
+	np->ipft_hf_eid = host_flood->entry_id;
 	/* Enable ingress port filter table lookup */
 	netc_port_wr(np, NETC_PIPFCR, PIPFCR_EN);
 
-	return 0;
+free_host_flood:
+	kfree(host_flood);
+
+	return err;
 }
 
-static void netc_port_remove_host_flood(struct netc_port *np,
-					struct ipft_entry_data *host_flood)
+static void netc_port_remove_host_flood(struct netc_port *np, u32 entry_id)
 {
 	struct netc_switch *priv = np->switch_priv;
 	bool disable_host_flood = false;
 
-	if (!host_flood)
+	if (entry_id == NTMP_NULL_ENTRY_ID)
 		return;
 
-	if (np->host_flood == host_flood)
+	if (np->ipft_hf_eid == entry_id)
 		disable_host_flood = true;
 
-	ntmp_ipft_delete_entry(&priv->ntmp, host_flood->entry_id);
-	kfree(host_flood);
+	ntmp_ipft_delete_entry(&priv->ntmp, entry_id);
 
 	if (disable_host_flood) {
-		np->host_flood = NULL;
+		np->ipft_hf_eid = NTMP_NULL_ENTRY_ID;
 		np->uc = false;
 		np->mc = false;
 		netc_port_wr(np, NETC_PIPFCR, 0);
@@ -1800,7 +1787,7 @@ static void netc_port_set_host_flood(struct dsa_switch *ds, int port,
 				     bool uc, bool mc)
 {
 	struct netc_port *np = NETC_PORT(ds, port);
-	struct ipft_entry_data *old_host_flood;
+	u32 old_entry_id;
 
 	/* Do not add host flood rule to ingress port filter table when
 	 * the port has joined a bridge. Otherwise, the ingress frames
@@ -1808,7 +1795,7 @@ static void netc_port_set_host_flood(struct dsa_switch *ds, int port,
 	 * will be redirected directly to the CPU port.
 	 */
 	if (dsa_port_bridge_dev_get(np->dp)) {
-		netc_port_remove_host_flood(np, np->host_flood);
+		netc_port_remove_host_flood(np, np->ipft_hf_eid);
 
 		return;
 	}
@@ -1818,20 +1805,18 @@ static void netc_port_set_host_flood(struct dsa_switch *ds, int port,
 
 	/* IPFT does not support in-place updates to the KEYE element,
 	 * we need to add a new entry and then delete the old one. So
-	 * save the old entry first.
+	 * save the old entry ID first.
 	 */
-	old_host_flood = np->host_flood;
-	np->host_flood = NULL;
+	old_entry_id = np->ipft_hf_eid;
 
 	if (netc_port_add_host_flood_rule(np, uc, mc)) {
-		np->host_flood = old_host_flood;
 		dev_err(ds->dev, "Failed to add host flood rule on port %d\n",
 			port);
 		return;
 	}
 
 	/* Remove the old host flood entry */
-	netc_port_remove_host_flood(np, old_host_flood);
+	netc_port_remove_host_flood(np, old_entry_id);
 }
 
 static int netc_single_vlan_aware_bridge(struct dsa_switch *ds,
@@ -2020,7 +2005,7 @@ static int netc_port_bridge_join(struct dsa_switch *ds, int port,
 	netc_port_set_pvid(np, vlan_unaware_pvid);
 
 out:
-	netc_port_remove_host_flood(np, np->host_flood);
+	netc_port_remove_host_flood(np, np->ipft_hf_eid);
 
 	if (atomic_inc_return(&priv->br_cnt) == 1)
 		schedule_delayed_work(&priv->fdbt_ageing_work,
diff --git a/drivers/net/dsa/netc/netc_switch.h b/drivers/net/dsa/netc/netc_switch.h
index 305f2a92e2f9..020e3661b866 100644
--- a/drivers/net/dsa/netc/netc_switch.h
+++ b/drivers/net/dsa/netc/netc_switch.h
@@ -84,7 +84,11 @@ struct netc_port {
 	u16 uc:1;
 	u16 mc:1;
 	u16 pvid;
-	struct ipft_entry_data *host_flood;
+	/* ipft_hf_eid applies only to user ports and should be initialized
+	 * to NTMP_NULL_ENTRY_ID. Other ports (such as CPU ports) do not
+	 * require initialization.
+	 */
+	u32 ipft_hf_eid;
 };
 
 struct netc_switch_regs {
-- 
2.34.1


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

* [PATCH v3 net-next 5/8] net: dsa: netc: check return value of ntmp_ipft_delete_entry()
  2026-08-31  8:23 [PATCH v3 net-next 0/8] net: dsa: netc: add PTP support for NETC switch wei.fang
                   ` (3 preceding siblings ...)
  2026-08-31  8:23 ` [PATCH v3 net-next 4/8] net: dsa: netc: use entry ID instead of pointer to track host flood rule wei.fang
@ 2026-08-31  8:23 ` wei.fang
  2026-08-31  8:23 ` [PATCH v3 net-next 6/8] net: dsa: netc: enable ingress port filtering lookup by default wei.fang
                   ` (3 subsequent siblings)
  8 siblings, 0 replies; 16+ messages in thread
From: wei.fang @ 2026-08-31  8:23 UTC (permalink / raw)
  To: xiaoning.wang, andrew, olteanv, andrew+netdev, davem, edumazet,
	kuba, pabeni, horms, richardcochran
  Cc: wei.fang, imx, netdev, linux-kernel, linuxppc-dev,
	linux-arm-kernel

From: Wei Fang <wei.fang@nxp.com>

ntmp_ipft_delete_entry() can fail, e.g. when DMA memory allocation fails.
The driver previously ignored the return value, so a failed deletion
would leave the IPFT entry untracked and impossible to clean up later.

Convert netc_port_remove_host_flood() from void to int and propagate the
error to all callers. In netc_port_add_host_flood_rule(), delete the old
entry before adding the new one, and guard against adding a new entry
when the old one still exists. In netc_port_set_host_flood() and
netc_port_bridge_join(), handle deletion failures and add rollback paths
to restore the host flood entry on error.

Note that the rollback itself may also fail, in which case there is
nothing more the driver can do. This is an acceptable limitation because
such failures only occur under resource exhaustion conditions that are
already abnormal, and the host flood feature is not critical to basic
packet forwarding.

Signed-off-by: Wei Fang <wei.fang@nxp.com>
---
 drivers/net/dsa/netc/netc_main.c | 93 ++++++++++++++++++++++----------
 1 file changed, 64 insertions(+), 29 deletions(-)

diff --git a/drivers/net/dsa/netc/netc_main.c b/drivers/net/dsa/netc/netc_main.c
index d326a00104e1..2171c83f3640 100644
--- a/drivers/net/dsa/netc/netc_main.c
+++ b/drivers/net/dsa/netc/netc_main.c
@@ -1707,15 +1707,23 @@ static int netc_port_add_host_flood_rule(struct netc_port *np,
 	u32 cfg;
 	int err;
 
-	if (!uc && !mc) {
-		/* Disable ingress port filter table lookup */
-		netc_port_wr(np, NETC_PIPFCR, 0);
-		np->uc = false;
-		np->mc = false;
+	if (np->ipft_hf_eid != NTMP_NULL_ENTRY_ID) {
+		/* The old rule is the same as the new rule, return directly.
+		 * But if the existing rule differs from the requested one,
+		 * return an error.
+		 */
+		if (np->uc == uc && np->mc == mc)
+			return 0;
 
-		return 0;
+		dev_err(priv->dev,
+			"The old host flood rule has not been removed from port %u\n",
+			np->dp->index);
+		return -EINVAL;
 	}
 
+	if (!uc && !mc)
+		return 0;
+
 	host_flood = kzalloc_obj(*host_flood);
 	if (!host_flood)
 		return -ENOMEM;
@@ -1762,32 +1770,33 @@ static int netc_port_add_host_flood_rule(struct netc_port *np,
 	return err;
 }
 
-static void netc_port_remove_host_flood(struct netc_port *np, u32 entry_id)
+static int netc_port_remove_host_flood(struct netc_port *np)
 {
 	struct netc_switch *priv = np->switch_priv;
-	bool disable_host_flood = false;
+	u32 entry_id = np->ipft_hf_eid;
+	int err;
 
 	if (entry_id == NTMP_NULL_ENTRY_ID)
-		return;
+		return 0;
 
-	if (np->ipft_hf_eid == entry_id)
-		disable_host_flood = true;
+	err = ntmp_ipft_delete_entry(&priv->ntmp, entry_id);
+	if (err)
+		return err;
 
-	ntmp_ipft_delete_entry(&priv->ntmp, entry_id);
+	np->ipft_hf_eid = NTMP_NULL_ENTRY_ID;
+	np->uc = false;
+	np->mc = false;
+	netc_port_wr(np, NETC_PIPFCR, 0);
 
-	if (disable_host_flood) {
-		np->ipft_hf_eid = NTMP_NULL_ENTRY_ID;
-		np->uc = false;
-		np->mc = false;
-		netc_port_wr(np, NETC_PIPFCR, 0);
-	}
+	return 0;
 }
 
 static void netc_port_set_host_flood(struct dsa_switch *ds, int port,
 				     bool uc, bool mc)
 {
 	struct netc_port *np = NETC_PORT(ds, port);
-	u32 old_entry_id;
+	bool old_uc = np->uc;
+	bool old_mc = np->mc;
 
 	/* Do not add host flood rule to ingress port filter table when
 	 * the port has joined a bridge. Otherwise, the ingress frames
@@ -1795,7 +1804,12 @@ static void netc_port_set_host_flood(struct dsa_switch *ds, int port,
 	 * will be redirected directly to the CPU port.
 	 */
 	if (dsa_port_bridge_dev_get(np->dp)) {
-		netc_port_remove_host_flood(np, np->ipft_hf_eid);
+		if (!netc_port_remove_host_flood(np))
+			return;
+
+		dev_err(ds->dev,
+			"Failed to delete host flood rule on bridge port %u\n",
+			port);
 
 		return;
 	}
@@ -1804,19 +1818,29 @@ static void netc_port_set_host_flood(struct dsa_switch *ds, int port,
 		return;
 
 	/* IPFT does not support in-place updates to the KEYE element,
-	 * we need to add a new entry and then delete the old one. So
-	 * save the old entry ID first.
+	 * we need to delete the old one and then add the new rule. If
+	 * the deletion fails, return immediately.
 	 */
-	old_entry_id = np->ipft_hf_eid;
+	if (netc_port_remove_host_flood(np)) {
+		dev_err(ds->dev,
+			"Failed to delete old host flood rule on port %u\n",
+			port);
+
+		return;
+	}
 
 	if (netc_port_add_host_flood_rule(np, uc, mc)) {
 		dev_err(ds->dev, "Failed to add host flood rule on port %d\n",
 			port);
-		return;
-	}
 
-	/* Remove the old host flood entry */
-	netc_port_remove_host_flood(np, old_entry_id);
+		/* Try to restore the old flood rule, if the recovery fails,
+		 * there is nothing else we can do; this is a limitation.
+		 */
+		if (netc_port_add_host_flood_rule(np, old_uc, old_mc))
+			dev_err(ds->dev,
+				"Failed to restore host flood rule on port %d\n",
+				port);
+	}
 }
 
 static int netc_single_vlan_aware_bridge(struct dsa_switch *ds,
@@ -1981,6 +2005,8 @@ static int netc_port_bridge_join(struct dsa_switch *ds, int port,
 	struct netc_port *np = NETC_PORT(ds, port);
 	struct netc_switch *priv = ds->priv;
 	u16 vlan_unaware_pvid;
+	bool uc = np->uc;
+	bool mc = np->mc;
 	int err;
 
 	if (!bridge.num) {
@@ -1992,6 +2018,12 @@ static int netc_port_bridge_join(struct dsa_switch *ds, int port,
 	if (err)
 		return err;
 
+	err = netc_port_remove_host_flood(np);
+	if (err) {
+		NL_SET_ERR_MSG_MOD(extack, "Failed to delete host flood rule");
+		return err;
+	}
+
 	netc_port_set_mlo(np, MLO_NOT_OVERRIDE);
 
 	if (br_vlan_enabled(bridge.dev))
@@ -2005,8 +2037,6 @@ static int netc_port_bridge_join(struct dsa_switch *ds, int port,
 	netc_port_set_pvid(np, vlan_unaware_pvid);
 
 out:
-	netc_port_remove_host_flood(np, np->ipft_hf_eid);
-
 	if (atomic_inc_return(&priv->br_cnt) == 1)
 		schedule_delayed_work(&priv->fdbt_ageing_work,
 				      READ_ONCE(priv->fdbt_ageing_delay));
@@ -2016,6 +2046,11 @@ static int netc_port_bridge_join(struct dsa_switch *ds, int port,
 disable_mlo:
 	netc_port_set_mlo(np, MLO_DISABLE);
 
+	if (netc_port_add_host_flood_rule(np, uc, mc))
+		dev_err(ds->dev,
+			"Failed to restore host flood rule on port %u\n",
+			port);
+
 	return err;
 }
 
-- 
2.34.1


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

* [PATCH v3 net-next 6/8] net: dsa: netc: enable ingress port filtering lookup by default
  2026-08-31  8:23 [PATCH v3 net-next 0/8] net: dsa: netc: add PTP support for NETC switch wei.fang
                   ` (4 preceding siblings ...)
  2026-08-31  8:23 ` [PATCH v3 net-next 5/8] net: dsa: netc: check return value of ntmp_ipft_delete_entry() wei.fang
@ 2026-08-31  8:23 ` wei.fang
  2026-08-31  8:23 ` [PATCH v3 net-next 7/8] net: dsa: netc: add PTP two-step timestamping support wei.fang
                   ` (2 subsequent siblings)
  8 siblings, 0 replies; 16+ messages in thread
From: wei.fang @ 2026-08-31  8:23 UTC (permalink / raw)
  To: xiaoning.wang, andrew, olteanv, andrew+netdev, davem, edumazet,
	kuba, pabeni, horms, richardcochran
  Cc: wei.fang, imx, netdev, linux-kernel, linuxppc-dev,
	linux-arm-kernel

From: Wei Fang <wei.fang@nxp.com>

The ingress port filtering lookup function involves performing a lookup
against the ingress port filter table (IPFT). If the frame matches an
entry, subsequent frame processing functions will perform corresponding
operations based on the parameters specified in that entry. If no entry
matches the frame, the frame is allowed in, and passed to the next frame
processing function.

In the future, the IPFT will be used to filter PTP frames and support tc
flower. Tracking in software whether any IPFT entry exists for a port in
order to decide whether to enable the ingress port filtering lookup would
increase code complexity unnecessarily. Since enabling the lookup when
the IPFT is empty has no effect on RX frames, enable the ingress port
filtering lookup unconditionally during driver initialization to simplify
code logic.

The only concern is that when deleting an IPFT entry fails, the hardware
will continue filtering RX frames based on that entry. However, we have
no choice. We cannot disable ingress port filter lookup because this
would deactivate other IPFT entries, which is obviously not what we want.
Moreover, deletion failures are almost impossible under normal
circumstances, and we don't need to be bothered by this minor issue.

Signed-off-by: Wei Fang <wei.fang@nxp.com>
---
 drivers/net/dsa/netc/netc_main.c | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/drivers/net/dsa/netc/netc_main.c b/drivers/net/dsa/netc/netc_main.c
index 2171c83f3640..ae3dc487f5f8 100644
--- a/drivers/net/dsa/netc/netc_main.c
+++ b/drivers/net/dsa/netc/netc_main.c
@@ -555,6 +555,12 @@ static void netc_port_fixed_config(struct netc_port *np)
 	netc_port_rmw(np, NETC_PCR, PCR_L2DOSE | PCR_L3DOSE,
 		      PCR_L2DOSE | PCR_L3DOSE);
 
+	/* Enable ingress port filter table lookup, if no match is found,
+	 * the frame is allowed and passed to the next frame processing
+	 * function.
+	 */
+	netc_port_wr(np, NETC_PIPFCR, PIPFCR_EN);
+
 	/* Set the quanta value of TX PAUSE frame */
 	netc_mac_port_wr(np, NETC_PM_PAUSE_QUANTA(0), NETC_PAUSE_QUANTA);
 
@@ -1761,8 +1767,6 @@ static int netc_port_add_host_flood_rule(struct netc_port *np,
 	np->uc = uc;
 	np->mc = mc;
 	np->ipft_hf_eid = host_flood->entry_id;
-	/* Enable ingress port filter table lookup */
-	netc_port_wr(np, NETC_PIPFCR, PIPFCR_EN);
 
 free_host_flood:
 	kfree(host_flood);
@@ -1786,7 +1790,6 @@ static int netc_port_remove_host_flood(struct netc_port *np)
 	np->ipft_hf_eid = NTMP_NULL_ENTRY_ID;
 	np->uc = false;
 	np->mc = false;
-	netc_port_wr(np, NETC_PIPFCR, 0);
 
 	return 0;
 }
-- 
2.34.1


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

* [PATCH v3 net-next 7/8] net: dsa: netc: add PTP two-step timestamping support
  2026-08-31  8:23 [PATCH v3 net-next 0/8] net: dsa: netc: add PTP support for NETC switch wei.fang
                   ` (5 preceding siblings ...)
  2026-08-31  8:23 ` [PATCH v3 net-next 6/8] net: dsa: netc: enable ingress port filtering lookup by default wei.fang
@ 2026-08-31  8:23 ` wei.fang
       [not found]   ` <20260901082026.7F3911F00A3E@smtp.kernel.org>
  2026-08-31  8:23 ` [PATCH v3 net-next 8/8] net: dsa: netc: add PTP one-step " wei.fang
  2026-09-01 15:09 ` [PATCH v3 net-next 0/8] net: dsa: netc: add PTP support for NETC switch Jakub Kicinski
  8 siblings, 1 reply; 16+ messages in thread
From: wei.fang @ 2026-08-31  8:23 UTC (permalink / raw)
  To: xiaoning.wang, andrew, olteanv, andrew+netdev, davem, edumazet,
	kuba, pabeni, horms, richardcochran
  Cc: wei.fang, imx, netdev, linux-kernel, linuxppc-dev,
	linux-arm-kernel

From: Wei Fang <wei.fang@nxp.com>

Add two-step TX timestamping and RX timestamping for the NETC switch.

For RX, install ingress port filter table (IPFT) rules that redirect PTP
frames to the CPU port. Support L2, L4 over IPv4 and L4 over IPv6, for
both event and general messages, selected through the hwtstamp rx_filter.
The hardware inserts a To_Host subtype 1 tag carrying the 64-bit ingress
timestamp after the source MAC address. The tagger extracts it into the
skb control buffer, and netc_port_rxtstamp() copies it into
skb_hwtstamps().

For two-step TX, clone the skb and record a 4-bit timestamp request ID
in the clone, then queue it on a per-port list. netc_xmit() carries the
same ID in the To_Port subtype 2 tag it inserts into the transmitted
frame. After the hardware sends the frame out, it generates a To_Host
subtype 2 response frame whose tag echoes back that ID together with the
64-bit transmit timestamp. The tagger dispatches the ID and timestamp to
the switch driver through the twostep_tstamp_handler callback registered
in netc_tagger_data. The driver matches the ID to the queued clone and
hands the clone back to the stack with the transmit timestamp via
skb_complete_tx_timestamp(), then frees the response skb. Non-PTP frames
keep using the To_Port subtype 0 tag on the xmit fast path.

The request IDs themselves are managed per port: each transmit takes the
first ID not currently in flight, and an ID becomes available again once
its clone leaves the queue. A per-port delayed work, tstamp_timeout_work,
drops any clone that stays unmatched for 5 seconds, which is far longer
than the hardware's normal response latency. A queued clone is thus
completed by the response handler, or dropped by tstamp_timeout_work on
timeout.

The two-step response frame carries no payload; its total length is
only 26 bytes (12 bytes of DMAC and SMAC plus a 14-byte switch tag).
eth_type_trans() pulls ETH_HLEN bytes, advancing skb->data past the
DMAC, SMAC, and the TPID that opens the switch tag, leaving skb->len
at 12. Since the tag pointer is at (skb->data - 2), the pskb_may_pull()
check must use (NETC_TAG_MAX_LEN - 2) rather than NETC_TAG_MAX_LEN.
Otherwise pskb_may_pull() drops the response frame and breaks PTP
synchronization.

Add the To_Port subtype 2 and To_Host subtype 1/2 tag structures, extend
netc_xmit() to select the tag based on ptp_flag in the skb control buffer,
and add netc_connect()/netc_disconnect() to manage the per-switch
netc_tagger_data allocation. Grab the PTP timer's pci_dev in netc_setup()
so get_ts_info() can report its PHC index, and release it in the teardown
and error paths.

Signed-off-by: Wei Fang <wei.fang@nxp.com>
---
 drivers/net/dsa/netc/Kconfig         |   1 +
 drivers/net/dsa/netc/Makefile        |   3 +-
 drivers/net/dsa/netc/netc_main.c     |  71 +++-
 drivers/net/dsa/netc/netc_platform.c |   1 +
 drivers/net/dsa/netc/netc_ptp.c      | 471 +++++++++++++++++++++++++++
 drivers/net/dsa/netc/netc_switch.h   |  42 +++
 include/linux/dsa/tag_netc.h         |  21 ++
 net/dsa/tag_netc.c                   | 168 +++++++++-
 8 files changed, 763 insertions(+), 15 deletions(-)
 create mode 100644 drivers/net/dsa/netc/netc_ptp.c

diff --git a/drivers/net/dsa/netc/Kconfig b/drivers/net/dsa/netc/Kconfig
index 793f7691a24f..8770b65d0f62 100644
--- a/drivers/net/dsa/netc/Kconfig
+++ b/drivers/net/dsa/netc/Kconfig
@@ -4,6 +4,7 @@ config NET_DSA_NETC_SWITCH
 	depends on ARM64 || COMPILE_TEST
 	depends on NET_DSA && PCI
 	depends on NET_VENDOR_FREESCALE
+	depends on PTP_1588_CLOCK_OPTIONAL
 	select NET_DSA_TAG_NETC
 	select FSL_ENETC_MDIO
 	select NXP_NTMP
diff --git a/drivers/net/dsa/netc/Makefile b/drivers/net/dsa/netc/Makefile
index f40b13c702e0..572b833ad80f 100644
--- a/drivers/net/dsa/netc/Makefile
+++ b/drivers/net/dsa/netc/Makefile
@@ -1,3 +1,4 @@
 # SPDX-License-Identifier: GPL-2.0-only
 obj-$(CONFIG_NET_DSA_NETC_SWITCH) += nxp-netc-switch.o
-nxp-netc-switch-objs := netc_main.o netc_platform.o netc_ethtool.o
+nxp-netc-switch-objs := netc_main.o netc_platform.o netc_ethtool.o \
+			netc_ptp.o
diff --git a/drivers/net/dsa/netc/netc_main.c b/drivers/net/dsa/netc/netc_main.c
index ae3dc487f5f8..cd2274d7c552 100644
--- a/drivers/net/dsa/netc/netc_main.c
+++ b/drivers/net/dsa/netc/netc_main.c
@@ -65,6 +65,20 @@ netc_get_tag_protocol(struct dsa_switch *ds, int port,
 	return DSA_TAG_PROTO_NETC;
 }
 
+static int netc_connect_tag_protocol(struct dsa_switch *ds,
+				     enum dsa_tag_protocol proto)
+{
+	struct netc_tagger_data *tagger_data;
+
+	if (proto != DSA_TAG_PROTO_NETC)
+		return -EPROTONOSUPPORT;
+
+	tagger_data = ds->tagger_data;
+	tagger_data->twostep_tstamp_handler = netc_port_twostep_tstamp_handler;
+
+	return 0;
+}
+
 static void netc_port_rmw(struct netc_port *np, u32 reg,
 			  u32 mask, u32 val)
 {
@@ -292,6 +306,15 @@ static int netc_init_all_ports(struct netc_switch *priv)
 			 * been created.
 			 */
 			np->ipft_hf_eid = NTMP_NULL_ENTRY_ID;
+
+			/* Only the user port needs to support PTP feature, so
+			 * PTP-related resources, such as skb_txtstamp_queue,
+			 * tstamp_lock, etc., are initialized only for the user
+			 * port.
+			 */
+			err = netc_port_ptp_init(np);
+			if (err)
+				return err;
 		}
 	}
 
@@ -881,6 +904,15 @@ static int netc_switch_bpt_default_config(struct netc_switch *priv)
 	return 0;
 }
 
+static struct pci_dev *netc_get_ptp_timer(struct netc_switch *priv)
+{
+	struct pci_bus *bus = priv->pdev->bus;
+	u32 devfn = priv->info->tmr_devfn;
+
+	return pci_get_domain_bus_and_slot(pci_domain_nr(bus),
+					   bus->number, devfn);
+}
+
 static int netc_setup(struct dsa_switch *ds)
 {
 	struct netc_switch *priv = ds->priv;
@@ -893,13 +925,23 @@ static int netc_setup(struct dsa_switch *ds)
 
 	netc_get_switch_capabilities(priv);
 
+	/* The PTP timer sits on the same PCI bus as the switch. PCI creates
+	 * every function's pci_dev during bus enumeration, before any driver
+	 * probes, so we can grab the timer's pci_dev here even if the timer
+	 * driver has not probed yet.
+	 */
+	priv->tmr_dev = netc_get_ptp_timer(priv);
+	if (!priv->tmr_dev)
+		dev_info(priv->dev,
+			 "PTP timer PCI device not found\n");
+
 	err = netc_init_all_ports(priv);
 	if (err)
-		return err;
+		goto put_ptp_timer;
 
 	err = netc_init_ntmp_user(priv);
 	if (err)
-		return err;
+		goto put_ptp_timer;
 
 	INIT_HLIST_HEAD(&priv->fdb_list);
 	mutex_init(&priv->fdbt_lock);
@@ -938,6 +980,8 @@ static int netc_setup(struct dsa_switch *ds)
 	mutex_destroy(&priv->fdbt_lock);
 	mutex_destroy(&priv->vft_lock);
 	netc_free_ntmp_user(priv);
+put_ptp_timer:
+	pci_dev_put(priv->tmr_dev);
 
 	return err;
 }
@@ -950,6 +994,21 @@ static void netc_destroy_all_lists(struct netc_switch *priv)
 	mutex_destroy(&priv->vft_lock);
 }
 
+static void netc_free_ports_resources(struct netc_switch *priv)
+{
+	struct dsa_port *dp;
+
+	dsa_switch_for_each_available_port(dp, priv->ds) {
+		struct netc_port *np = priv->ports[dp->index];
+
+		if (!dsa_port_is_user(dp))
+			continue;
+
+		disable_delayed_work_sync(&np->tstamp_timeout_work);
+		netc_port_purge_txtstamp_queue(np);
+	}
+}
+
 static void netc_teardown(struct dsa_switch *ds)
 {
 	struct netc_switch *priv = ds->priv;
@@ -957,6 +1016,8 @@ static void netc_teardown(struct dsa_switch *ds)
 	disable_delayed_work_sync(&priv->fdbt_ageing_work);
 	netc_destroy_all_lists(priv);
 	netc_free_ntmp_user(priv);
+	netc_free_ports_resources(priv);
+	pci_dev_put(priv->tmr_dev);
 }
 
 static bool netc_port_is_emdio_consumer(struct device_node *node)
@@ -2425,6 +2486,7 @@ static const struct phylink_mac_ops netc_phylink_mac_ops = {
 
 static const struct dsa_switch_ops netc_switch_ops = {
 	.get_tag_protocol		= netc_get_tag_protocol,
+	.connect_tag_protocol		= netc_connect_tag_protocol,
 	.setup				= netc_setup,
 	.teardown			= netc_teardown,
 	.phylink_get_caps		= netc_phylink_get_caps,
@@ -2453,6 +2515,11 @@ static const struct dsa_switch_ops netc_switch_ops = {
 	.get_sset_count			= netc_port_get_sset_count,
 	.get_strings			= netc_port_get_strings,
 	.get_ethtool_stats		= netc_port_get_ethtool_stats,
+	.get_ts_info			= netc_get_ts_info,
+	.port_hwtstamp_set		= netc_port_hwtstamp_set,
+	.port_hwtstamp_get		= netc_port_hwtstamp_get,
+	.port_rxtstamp			= netc_port_rxtstamp,
+	.port_txtstamp			= netc_port_txtstamp,
 };
 
 static int netc_switch_probe(struct pci_dev *pdev,
diff --git a/drivers/net/dsa/netc/netc_platform.c b/drivers/net/dsa/netc/netc_platform.c
index 34aeb6fceb3c..4fd0ce6770c3 100644
--- a/drivers/net/dsa/netc/netc_platform.c
+++ b/drivers/net/dsa/netc/netc_platform.c
@@ -50,6 +50,7 @@ static void imx94_switch_phylink_get_caps(int port,
 
 static const struct netc_switch_info imx94_info = {
 	.num_ports = 4,
+	.tmr_devfn = PCI_DEVFN(0, 1),
 	.phylink_get_caps = imx94_switch_phylink_get_caps,
 };
 
diff --git a/drivers/net/dsa/netc/netc_ptp.c b/drivers/net/dsa/netc/netc_ptp.c
new file mode 100644
index 000000000000..f55010f46819
--- /dev/null
+++ b/drivers/net/dsa/netc/netc_ptp.c
@@ -0,0 +1,471 @@
+// SPDX-License-Identifier: (GPL-2.0+ OR BSD-3-Clause)
+/*
+ * NXP NETC switch driver
+ * Copyright 2025-2026 NXP
+ */
+
+#include <linux/ptp_classify.h>
+#include <linux/ptp_clock_kernel.h>
+
+#include "netc_switch.h"
+
+#define NETC_NUM_TS_REQ_ID		16
+#define NETC_TXTSTAMP_TIMEOUT		(5 * HZ)
+
+static void netc_port_tstamp_timeout_work(struct work_struct *work)
+{
+	struct netc_port *np = container_of(work, struct netc_port,
+					    tstamp_timeout_work.work);
+	struct sk_buff_head free_list;
+	struct sk_buff *skb, *skb_tmp;
+
+	__skb_queue_head_init(&free_list);
+
+	spin_lock_bh(&np->tstamp_lock);
+	skb_queue_walk_safe(&np->skb_txtstamp_queue, skb, skb_tmp) {
+		if (time_before(jiffies, NETC_SKB_CB(skb)->ptp_tx_time +
+			       NETC_TXTSTAMP_TIMEOUT))
+			continue;
+
+		dev_dbg_ratelimited(np->switch_priv->dev,
+				    "Port %d ts_req_id %u which seems lost\n",
+				    np->dp->index, NETC_SKB_CB(skb)->ts_req_id);
+
+		__skb_unlink(skb, &np->skb_txtstamp_queue);
+		__skb_queue_tail(&free_list, skb);
+	}
+
+	/* Reschedule if there are still pending clones that have not
+	 * timed out yet.
+	 */
+	if (!skb_queue_empty(&np->skb_txtstamp_queue))
+		schedule_delayed_work(&np->tstamp_timeout_work,
+				      NETC_TXTSTAMP_TIMEOUT);
+
+	spin_unlock_bh(&np->tstamp_lock);
+	__skb_queue_purge(&free_list);
+}
+
+int netc_port_ptp_init(struct netc_port *np)
+{
+	/* Initialize to invalid entry IDs */
+	for (int i = 0; i < NETC_PTP_MAX; i++)
+		np->ptp_ipft_eid[i] = NTMP_NULL_ENTRY_ID;
+
+	spin_lock_init(&np->tstamp_lock);
+	__skb_queue_head_init(&np->skb_txtstamp_queue);
+	INIT_DELAYED_WORK(&np->tstamp_timeout_work,
+			  netc_port_tstamp_timeout_work);
+
+	return 0;
+}
+
+void netc_port_purge_txtstamp_queue(struct netc_port *np)
+{
+	struct sk_buff_head free_list;
+
+	__skb_queue_head_init(&free_list);
+
+	spin_lock_bh(&np->tstamp_lock);
+	skb_queue_splice_init(&np->skb_txtstamp_queue, &free_list);
+	spin_unlock_bh(&np->tstamp_lock);
+
+	__skb_queue_purge(&free_list);
+}
+
+static int netc_get_phc_index(struct netc_switch *priv)
+{
+	if (!priv->tmr_dev)
+		return -1;
+
+	return ptp_clock_index_by_dev(&priv->tmr_dev->dev);
+}
+
+int netc_get_ts_info(struct dsa_switch *ds, int port,
+		     struct kernel_ethtool_ts_info *info)
+{
+	struct netc_switch *priv = ds->priv;
+
+	info->phc_index = netc_get_phc_index(priv);
+	if (info->phc_index < 0)
+		return 0;
+
+	info->so_timestamping |= SOF_TIMESTAMPING_TX_HARDWARE |
+				 SOF_TIMESTAMPING_RX_HARDWARE |
+				 SOF_TIMESTAMPING_RAW_HARDWARE;
+
+	info->tx_types = BIT(HWTSTAMP_TX_OFF) | BIT(HWTSTAMP_TX_ON);
+
+	info->rx_filters = BIT(HWTSTAMP_FILTER_NONE) |
+			   BIT(HWTSTAMP_FILTER_PTP_V2_EVENT) |
+			   BIT(HWTSTAMP_FILTER_PTP_V2_L2_EVENT) |
+			   BIT(HWTSTAMP_FILTER_PTP_V2_L4_EVENT);
+
+	return 0;
+}
+
+static void netc_port_del_ptp_filter(struct netc_port *np)
+{
+	struct netc_switch *priv = np->switch_priv;
+	u32 entry_id;
+	int i;
+
+	for (i = 0; i < NETC_PTP_MAX; i++) {
+		entry_id = np->ptp_ipft_eid[i];
+		if (entry_id != NTMP_NULL_ENTRY_ID) {
+			if (!ntmp_ipft_delete_entry(&priv->ntmp, entry_id)) {
+				np->ptp_ipft_eid[i] = NTMP_NULL_ENTRY_ID;
+				continue;
+			}
+
+			dev_err(priv->dev,
+				"Deleting PTP entry 0x%x (type %d) on port %d failed\n",
+				entry_id, i, np->dp->index);
+		}
+	}
+}
+
+static int netc_build_ptp_ipft_keye(struct ipft_keye_data *keye, int port,
+				    enum netc_ptp_type type)
+{
+	u16 src_port, frm_attr_flags;
+
+	keye->precedence = cpu_to_le16(NETC_IPFT_PTP_PRECEDENCE);
+	src_port = FIELD_PREP(IPFT_SRC_PORT, port);
+	src_port |= IPFT_SRC_PORT_MASK;
+	keye->src_port = cpu_to_le16(src_port);
+
+	switch (type) {
+	case NETC_PTP_L2:
+		keye->ethertype = htons(ETH_P_1588);
+		keye->ethertype_mask = htons(0xffff);
+		break;
+	case NETC_PTP_L4_IPV4_EVENT:
+	case NETC_PTP_L4_IPV4_GENERAL:
+	case NETC_PTP_L4_IPV6_EVENT:
+	case NETC_PTP_L4_IPV6_GENERAL:
+		frm_attr_flags = IPFT_FAF_IP_HDR | FIELD_PREP(IPFT_FAF_L4_CODE,
+				 IPFT_FAF_UDP_HDR);
+		if (type == NETC_PTP_L4_IPV6_EVENT ||
+		    type == NETC_PTP_L4_IPV6_GENERAL)
+			frm_attr_flags |= IPFT_FAF_IP_VER6;
+
+		keye->frm_attr_flags = cpu_to_le16(frm_attr_flags);
+
+		/* Set IP version bit in flags_mask to match IPv4 or IPv6
+		 * packets
+		 */
+		frm_attr_flags |= IPFT_FAF_IP_VER6;
+		keye->frm_attr_flags_mask = cpu_to_le16(frm_attr_flags);
+		keye->ip_protocol = IPPROTO_UDP;
+		keye->ip_protocol_mask = 0xff;
+
+		if (type == NETC_PTP_L4_IPV4_EVENT ||
+		    type == NETC_PTP_L4_IPV6_EVENT)
+			keye->l4_dst_port = htons(PTP_EV_PORT);
+		else
+			keye->l4_dst_port = htons(PTP_GEN_PORT);
+
+		keye->l4_dst_port_mask = htons(0xffff);
+		break;
+	default:
+		return -ERANGE;
+	}
+
+	return 0;
+}
+
+static int netc_port_add_ipft_ptp_entry(struct netc_port *np,
+					enum netc_ptp_type type)
+{
+	struct netc_switch *priv = np->switch_priv;
+	struct ipft_entry_data *entry;
+	struct ipft_keye_data *keye;
+	u32 cfg;
+	int err;
+
+	/* The previously configured PTP entry may not have been successfully
+	 * deleted, so there is no need to configure it again.
+	 */
+	if (np->ptp_ipft_eid[type] != NTMP_NULL_ENTRY_ID)
+		return 0;
+
+	entry = kzalloc_obj(*entry);
+	if (!entry)
+		return -ENOMEM;
+
+	keye = &entry->keye;
+	err = netc_build_ptp_ipft_keye(keye, np->dp->index, type);
+	if (err)
+		goto free_entry;
+
+	cfg = FIELD_PREP(IPFT_FLTFA, IPFT_FLTFA_REDIRECT);
+	cfg |= FIELD_PREP(IPFT_HR, NETC_HR_PTP_TRAP);
+	cfg |= IPFT_TIMECAPE | IPFT_RRT;
+	entry->cfge.cfg = cpu_to_le32(cfg);
+
+	err = ntmp_ipft_add_entry(&priv->ntmp, entry);
+	if (err)
+		goto free_entry;
+
+	np->ptp_ipft_eid[type] = entry->entry_id;
+
+free_entry:
+	kfree(entry);
+
+	return err;
+}
+
+static int netc_port_add_l2_ptp_filter(struct netc_port *np)
+{
+	return netc_port_add_ipft_ptp_entry(np, NETC_PTP_L2);
+}
+
+static int netc_port_add_l4_ptp_filter(struct netc_port *np)
+{
+	int err;
+
+	err = netc_port_add_ipft_ptp_entry(np, NETC_PTP_L4_IPV4_EVENT);
+	if (err)
+		return err;
+
+	err = netc_port_add_ipft_ptp_entry(np, NETC_PTP_L4_IPV4_GENERAL);
+	if (err)
+		goto del_ptp_filter;
+
+	err = netc_port_add_ipft_ptp_entry(np, NETC_PTP_L4_IPV6_EVENT);
+	if (err)
+		goto del_ptp_filter;
+
+	err = netc_port_add_ipft_ptp_entry(np, NETC_PTP_L4_IPV6_GENERAL);
+	if (err)
+		goto del_ptp_filter;
+
+	return 0;
+
+del_ptp_filter:
+	netc_port_del_ptp_filter(np);
+
+	return err;
+}
+
+static int netc_port_add_l2_l4_ptp_filter(struct netc_port *np)
+{
+	int err;
+
+	err = netc_port_add_l2_ptp_filter(np);
+	if (err)
+		return err;
+
+	err = netc_port_add_l4_ptp_filter(np);
+	if (err)
+		goto del_ptp_filter;
+
+	return 0;
+
+del_ptp_filter:
+	netc_port_del_ptp_filter(np);
+
+	return err;
+}
+
+static int netc_port_set_ptp_filter(struct netc_port *np, int rx_filter)
+{
+	int err = 0;
+
+	netc_port_del_ptp_filter(np);
+	np->ptp_rx_filter = HWTSTAMP_FILTER_NONE;
+
+	switch (rx_filter) {
+	case HWTSTAMP_FILTER_NONE:
+		break;
+	case HWTSTAMP_FILTER_PTP_V2_L2_EVENT:
+		err = netc_port_add_l2_ptp_filter(np);
+		break;
+	case HWTSTAMP_FILTER_PTP_V2_L4_EVENT:
+		err = netc_port_add_l4_ptp_filter(np);
+		break;
+	case HWTSTAMP_FILTER_PTP_V2_EVENT:
+		err = netc_port_add_l2_l4_ptp_filter(np);
+		break;
+	default:
+		err = -ERANGE;
+	}
+
+	if (err)
+		return err;
+
+	np->ptp_rx_filter = rx_filter;
+
+	return 0;
+}
+
+int netc_port_hwtstamp_set(struct dsa_switch *ds, int port,
+			   struct kernel_hwtstamp_config *config,
+			   struct netlink_ext_ack *extack)
+{
+	struct netc_port *np = NETC_PORT(ds, port);
+	struct netc_switch *priv = ds->priv;
+	int rx_filter, err;
+
+	if ((config->tx_type != HWTSTAMP_TX_OFF ||
+	     config->rx_filter != HWTSTAMP_FILTER_NONE) &&
+	    !priv->tmr_dev)
+		return -EOPNOTSUPP;
+
+	switch (config->tx_type) {
+	case HWTSTAMP_TX_ON:
+	case HWTSTAMP_TX_OFF:
+		break;
+	default:
+		return -ERANGE;
+	}
+
+	switch (config->rx_filter) {
+	case HWTSTAMP_FILTER_NONE:
+		rx_filter = HWTSTAMP_FILTER_NONE;
+		break;
+	case HWTSTAMP_FILTER_PTP_V2_L4_EVENT:
+	case HWTSTAMP_FILTER_PTP_V2_L4_SYNC:
+	case HWTSTAMP_FILTER_PTP_V2_L4_DELAY_REQ:
+		rx_filter = HWTSTAMP_FILTER_PTP_V2_L4_EVENT;
+		break;
+	case HWTSTAMP_FILTER_PTP_V2_L2_EVENT:
+	case HWTSTAMP_FILTER_PTP_V2_L2_SYNC:
+	case HWTSTAMP_FILTER_PTP_V2_L2_DELAY_REQ:
+		rx_filter = HWTSTAMP_FILTER_PTP_V2_L2_EVENT;
+		break;
+	case HWTSTAMP_FILTER_PTP_V2_EVENT:
+	case HWTSTAMP_FILTER_PTP_V2_SYNC:
+	case HWTSTAMP_FILTER_PTP_V2_DELAY_REQ:
+		rx_filter = HWTSTAMP_FILTER_PTP_V2_EVENT;
+		break;
+	default:
+		return -ERANGE;
+	}
+
+	err = netc_port_set_ptp_filter(np, rx_filter);
+	if (err) {
+		NL_SET_ERR_MSG_MOD(extack, "Failed to set PTP filter");
+		return err;
+	}
+
+	WRITE_ONCE(np->ptp_tx_type, config->tx_type);
+
+	config->rx_filter = rx_filter;
+
+	return 0;
+}
+
+int netc_port_hwtstamp_get(struct dsa_switch *ds, int port,
+			   struct kernel_hwtstamp_config *config)
+{
+	struct netc_port *np = NETC_PORT(ds, port);
+
+	config->tx_type = READ_ONCE(np->ptp_tx_type);
+	config->rx_filter = np->ptp_rx_filter;
+
+	return 0;
+}
+
+static void netc_port_txtstamp_twostep(struct netc_port *np,
+				       struct sk_buff *nskb)
+{
+	DECLARE_BITMAP(ts_req_id_bitmap, NETC_NUM_TS_REQ_ID);
+	struct sk_buff *clone = skb_clone_sk(nskb);
+	struct sk_buff *skb, *skb_tmp;
+	unsigned long ts_req_id;
+
+	if (unlikely(!clone))
+		return;
+
+	bitmap_zero(ts_req_id_bitmap, NETC_NUM_TS_REQ_ID);
+	spin_lock_bh(&np->tstamp_lock);
+
+	skb_queue_walk_safe(&np->skb_txtstamp_queue, skb, skb_tmp)
+		__set_bit(NETC_SKB_CB(skb)->ts_req_id, ts_req_id_bitmap);
+
+	ts_req_id = find_first_zero_bit(ts_req_id_bitmap, NETC_NUM_TS_REQ_ID);
+	if (ts_req_id == NETC_NUM_TS_REQ_ID) {
+		dev_dbg_ratelimited(np->switch_priv->dev,
+				    "Port %d has no available ts_req_id\n",
+				    np->dp->index);
+		spin_unlock_bh(&np->tstamp_lock);
+		kfree_skb(clone);
+		return;
+	}
+
+	NETC_SKB_CB(nskb)->ptp_flag = NETC_PTP_FLAG_TWOSTEP;
+	NETC_SKB_CB(nskb)->ts_req_id = ts_req_id;
+	NETC_SKB_CB(clone)->ts_req_id = ts_req_id;
+	NETC_SKB_CB(clone)->ptp_tx_time = jiffies;
+	skb_shinfo(clone)->tx_flags |= SKBTX_IN_PROGRESS;
+	__skb_queue_tail(&np->skb_txtstamp_queue, clone);
+	if (!delayed_work_pending(&np->tstamp_timeout_work))
+		schedule_delayed_work(&np->tstamp_timeout_work,
+				      NETC_TXTSTAMP_TIMEOUT);
+
+	spin_unlock_bh(&np->tstamp_lock);
+}
+
+void netc_port_twostep_tstamp_handler(struct dsa_switch *ds, int port,
+				      u8 ts_req_id, u64 ts)
+{
+	struct sk_buff *skb, *skb_tmp, *skb_match = NULL;
+	struct netc_port *np = NETC_PORT(ds, port);
+	struct skb_shared_hwtstamps hwtstamps;
+	struct netc_switch *priv = ds->priv;
+
+	spin_lock_bh(&np->tstamp_lock);
+	skb_queue_walk_safe(&np->skb_txtstamp_queue, skb, skb_tmp) {
+		if (NETC_SKB_CB(skb)->ts_req_id != ts_req_id)
+			continue;
+
+		__skb_unlink(skb, &np->skb_txtstamp_queue);
+		skb_match = skb;
+		break;
+	}
+	spin_unlock_bh(&np->tstamp_lock);
+
+	if (!skb_match) {
+		dev_dbg_ratelimited(priv->dev,
+				    "Port %d ts_req_id %u which seems lost\n",
+				    port, ts_req_id);
+		return;
+	}
+
+	hwtstamps.hwtstamp = ns_to_ktime(ts);
+	skb_complete_tx_timestamp(skb_match, &hwtstamps);
+}
+
+bool netc_port_rxtstamp(struct dsa_switch *ds, int port, struct sk_buff *skb,
+			unsigned int type)
+{
+	struct skb_shared_hwtstamps *hwtstamps = skb_hwtstamps(skb);
+	u64 ts = NETC_SKB_CB(skb)->tstamp;
+
+	/* ts == 0 indicates the hardware did not capture the RX timestamp
+	 * of the frame.
+	 */
+	if (!ts)
+		return false;
+
+	hwtstamps->hwtstamp = ns_to_ktime(ts);
+
+	return false;
+}
+
+void netc_port_txtstamp(struct dsa_switch *ds, int port, struct sk_buff *skb)
+{
+	struct netc_port *np = NETC_PORT(ds, port);
+	int tx_type = READ_ONCE(np->ptp_tx_type);
+	u32 ptp_class;
+
+	NETC_SKB_CB(skb)->ptp_flag = 0;
+	ptp_class = ptp_classify_raw(skb);
+	if (ptp_class == PTP_CLASS_NONE)
+		return;
+
+	if (tx_type == HWTSTAMP_TX_ON)
+		netc_port_txtstamp_twostep(np, skb);
+}
diff --git a/drivers/net/dsa/netc/netc_switch.h b/drivers/net/dsa/netc/netc_switch.h
index 020e3661b866..b214eeb202b0 100644
--- a/drivers/net/dsa/netc/netc_switch.h
+++ b/drivers/net/dsa/netc/netc_switch.h
@@ -53,10 +53,16 @@
 #define NETC_FDBT_AGEING_DELAY		(3 * HZ)
 #define NETC_FDBT_AGEING_THRESH		100
 
+/* PTP frames have a higher priority, so a higher priority is defined to
+ * prioritize matching (The higher the value, the higher the priority).
+ */
+#define NETC_IPFT_PTP_PRECEDENCE	0xf000
+
 struct netc_switch;
 
 struct netc_switch_info {
 	u32 num_ports;
+	u32 tmr_devfn;
 	void (*phylink_get_caps)(int port, struct phylink_config *config);
 };
 
@@ -66,9 +72,19 @@ struct netc_port_caps {
 	u32 pseudo_link:1;
 };
 
+enum netc_ptp_type {
+	NETC_PTP_L2,
+	NETC_PTP_L4_IPV4_EVENT,
+	NETC_PTP_L4_IPV4_GENERAL,
+	NETC_PTP_L4_IPV6_EVENT,
+	NETC_PTP_L4_IPV6_GENERAL,
+	NETC_PTP_MAX,
+};
+
 enum netc_host_reason {
 	/* Software defined host reasons */
 	NETC_HR_HOST_FLOOD = 8,
+	NETC_HR_PTP_TRAP   = 9,
 };
 
 struct netc_port {
@@ -89,6 +105,15 @@ struct netc_port {
 	 * require initialization.
 	 */
 	u32 ipft_hf_eid;
+
+	/* Serialize access to skb_txtstamp_queue */
+	spinlock_t tstamp_lock;
+	/* skb queue for two-step timestamp frames */
+	struct sk_buff_head skb_txtstamp_queue;
+	struct delayed_work tstamp_timeout_work;
+	int ptp_tx_type;
+	int ptp_rx_filter;
+	u32 ptp_ipft_eid[NETC_PTP_MAX];
 };
 
 struct netc_switch_regs {
@@ -143,6 +168,7 @@ struct netc_switch {
 	u32 num_bp;
 
 	struct bpt_cfge_data *bpt_list;
+	struct pci_dev *tmr_dev; /* The PTP Timer PCI device */
 };
 
 #define NETC_PRIV(ds)			((struct netc_switch *)((ds)->priv))
@@ -207,4 +233,20 @@ void netc_port_get_strings(struct dsa_switch *ds, int port,
 			   u32 sset, u8 *data);
 void netc_port_get_ethtool_stats(struct dsa_switch *ds, int port, u64 *data);
 
+/* PTP APIs */
+int netc_port_ptp_init(struct netc_port *np);
+int netc_get_ts_info(struct dsa_switch *ds, int port,
+		     struct kernel_ethtool_ts_info *info);
+void netc_port_purge_txtstamp_queue(struct netc_port *np);
+int netc_port_hwtstamp_set(struct dsa_switch *ds, int port,
+			   struct kernel_hwtstamp_config *config,
+			   struct netlink_ext_ack *extack);
+int netc_port_hwtstamp_get(struct dsa_switch *ds, int port,
+			   struct kernel_hwtstamp_config *config);
+void netc_port_twostep_tstamp_handler(struct dsa_switch *ds, int port,
+				      u8 ts_req_id, u64 ts);
+bool netc_port_rxtstamp(struct dsa_switch *ds, int port, struct sk_buff *skb,
+			unsigned int type);
+void netc_port_txtstamp(struct dsa_switch *ds, int port, struct sk_buff *skb);
+
 #endif
diff --git a/include/linux/dsa/tag_netc.h b/include/linux/dsa/tag_netc.h
index fe964722e5b0..da200e3ba8ad 100644
--- a/include/linux/dsa/tag_netc.h
+++ b/include/linux/dsa/tag_netc.h
@@ -10,5 +10,26 @@
 #include <net/dsa.h>
 
 #define NETC_TAG_MAX_LEN			14
+#define NETC_PTP_FLAG_TWOSTEP			BIT(1)
+
+struct netc_skb_cb {
+	unsigned long ptp_tx_time;
+	u64 tstamp;
+	u8 ptp_flag;
+	u8 ts_req_id;
+};
+
+#define NETC_SKB_CB(skb)	((struct netc_skb_cb *)((skb)->cb))
+
+/**
+ * struct netc_tagger_data - NETC tagger/switch-driver shared operations
+ * @twostep_tstamp_handler: Called by the tagger when a two-step transmit
+ *	timestamp response is received, to deliver the timestamp to the
+ *	switch driver.
+ */
+struct netc_tagger_data {
+	void (*twostep_tstamp_handler)(struct dsa_switch *ds, int port,
+				       u8 ts_req_id, u64 ts);
+};
 
 #endif
diff --git a/net/dsa/tag_netc.c b/net/dsa/tag_netc.c
index df72a61796ad..d46576ae2f75 100644
--- a/net/dsa/tag_netc.c
+++ b/net/dsa/tag_netc.c
@@ -16,6 +16,8 @@
 #define NETC_TAG_TO_PORT		1
 /* SubType0: No request to perform timestamping */
 #define NETC_TAG_TP_SUBTYPE0		0
+/* SubType2: Request to perform two-step timestamping */
+#define NETC_TAG_TP_SUBTYPE2		2
 
 /* To_Host NXP switch tag */
 #define NETC_TAG_TO_HOST		2
@@ -29,6 +31,7 @@
 /* NETC switch tag lengths */
 #define NETC_TAG_FORWARD_LEN		6
 #define NETC_TAG_TP_SUBTYPE0_LEN	6
+#define NETC_TAG_TP_SUBTYPE2_LEN	6
 #define NETC_TAG_TH_SUBTYPE0_LEN	6
 #define NETC_TAG_TH_SUBTYPE1_LEN	14
 #define NETC_TAG_TH_SUBTYPE2_LEN	14
@@ -40,6 +43,7 @@
 #define NETC_TAG_IPV			GENMASK(4, 2)
 #define NETC_TAG_SWITCH			GENMASK(2, 0)
 #define NETC_TAG_PORT			GENMASK(7, 3)
+#define NETC_TAG_TS_REQ_ID		GENMASK(3, 0)
 
 struct netc_tag_cmn {
 	__be16 tpid;
@@ -48,6 +52,23 @@ struct netc_tag_cmn {
 	u8 switch_port;
 } __packed;
 
+struct netc_tag_tp_subtype2 {
+	struct netc_tag_cmn cmn;
+	u8 ts_req_id;
+} __packed;
+
+struct netc_tag_th_subtype1 {
+	struct netc_tag_cmn cmn;
+	u8 host_reason;
+	__be64 timestamp;
+} __packed;
+
+struct netc_tag_th_subtype2 {
+	struct netc_tag_cmn cmn;
+	u8 hr_tsreq_id;
+	__be64 timestamp;
+} __packed;
+
 static void netc_fill_common_tag(struct netc_tag_cmn *tag, u8 type,
 				 u8 subtype, u8 sw_id, u8 port, u8 ipv)
 {
@@ -97,15 +118,60 @@ static void netc_fill_tp_tag_subtype0(struct sk_buff *skb,
 				NETC_TAG_TP_SUBTYPE0_LEN);
 }
 
-/* Currently only support To_Port tag, subtype 0 */
+static void netc_fill_tp_tag_subtype2(struct sk_buff *skb,
+				      struct net_device *ndev)
+{
+	u8 ts_req_id = NETC_SKB_CB(skb)->ts_req_id;
+	struct netc_tag_tp_subtype2 *tag;
+
+	tag = netc_fill_common_tp_tag(skb, ndev, NETC_TAG_TP_SUBTYPE2,
+				      NETC_TAG_TP_SUBTYPE2_LEN);
+	tag->ts_req_id = FIELD_PREP(NETC_TAG_TS_REQ_ID, ts_req_id);
+}
+
 static struct sk_buff *netc_xmit(struct sk_buff *skb,
 				 struct net_device *ndev)
 {
-	netc_fill_tp_tag_subtype0(skb, ndev);
+	u8 ptp_flag = NETC_SKB_CB(skb)->ptp_flag;
+
+	/* Fast path: the overwhelming majority of frames are not PTP frames */
+	if (likely(!ptp_flag)) {
+		netc_fill_tp_tag_subtype0(skb, ndev);
+		return skb;
+	} else if (ptp_flag == NETC_PTP_FLAG_TWOSTEP) {
+		netc_fill_tp_tag_subtype2(skb, ndev);
+	} else {
+		kfree_skb(skb);
+		return NULL;
+	}
 
 	return skb;
 }
 
+static void netc_rx_tstamp_process(struct netc_tag_th_subtype1 *tag,
+				   struct sk_buff *skb)
+{
+	u64 ts = get_unaligned_be64(&tag->timestamp);
+
+	NETC_SKB_CB(skb)->tstamp = ts;
+}
+
+static void netc_twostep_tstamp_process(struct netc_tag_th_subtype2 *tag,
+					struct sk_buff *skb)
+{
+	u8 ts_req_id = FIELD_GET(NETC_TAG_TS_REQ_ID, tag->hr_tsreq_id);
+	struct dsa_port *dp = dsa_user_to_port(skb->dev);
+	u64 ts = get_unaligned_be64(&tag->timestamp);
+	struct netc_tagger_data *tagger_data;
+	struct dsa_switch *ds = dp->ds;
+
+	tagger_data = ds->tagger_data;
+	if (unlikely(!tagger_data->twostep_tstamp_handler))
+		return;
+
+	tagger_data->twostep_tstamp_handler(ds, dp->index, ts_req_id, ts);
+}
+
 static int netc_get_rx_tag_len(int type, int subtype)
 {
 	/* Only NETC_TAG_TO_HOST and NETC_TAG_FORWARD are expected in RX,
@@ -129,11 +195,22 @@ static struct sk_buff *netc_rcv(struct sk_buff *skb,
 	struct netc_tag_cmn *tag_cmn;
 	int tag_len, sw_id, port;
 	int type, subtype;
+	void *tag;
 
-	if (unlikely(!pskb_may_pull(skb, NETC_TAG_MAX_LEN)))
+	/* eth_type_trans() pulled ETH_HLEN bytes, so skb->data sits 2 bytes
+	 * past the start of the switch tag (past the TPID) and skb->len is
+	 * ETH_HLEN bytes shorter than the original frame length. The longest
+	 * switch tag is NETC_TAG_MAX_LEN (14) bytes, but since 2 of those
+	 * bytes are already behind skb->data, only NETC_TAG_MAX_LEN - 2 bytes
+	 * need to be in the linear buffer. For the To_Host subtype 2 response
+	 * frame, whose total length is only 26 bytes with no payload after the
+	 * tag, this check is the only guard against a too-short frame.
+	 */
+	if (unlikely(!pskb_may_pull(skb, NETC_TAG_MAX_LEN - 2)))
 		goto err_free_skb;
 
-	tag_cmn = dsa_etype_header_pos_rx(skb);
+	tag = dsa_etype_header_pos_rx(skb);
+	tag_cmn = tag;
 	if (ntohs(tag_cmn->tpid) != ETH_P_NXP_NETC) {
 		dev_warn_ratelimited(&ndev->dev, "Unknown TPID 0x%04x\n",
 				     ntohs(tag_cmn->tpid));
@@ -156,17 +233,50 @@ static struct sk_buff *netc_rcv(struct sk_buff *skb,
 	if (!skb->dev)
 		goto err_free_skb;
 
+	/* skb->cb may be used to store hardware RX timestamp, so it must
+	 * be cleared before processing to avoid data pollution from the
+	 * previous layer.
+	 */
+	NETC_SKB_CB(skb)->tstamp = 0;
+
 	type = FIELD_GET(NETC_TAG_TYPE, tag_cmn->type);
 	subtype = FIELD_GET(NETC_TAG_SUBTYPE, tag_cmn->type);
 	if (type == NETC_TAG_FORWARD) {
 		dsa_default_offload_fwd_mark(skb);
 	} else if (type == NETC_TAG_TO_HOST) {
-		/* Currently only subtype0 supported */
-		if (subtype != NETC_TAG_TH_SUBTYPE0)
+		switch (subtype) {
+		case NETC_TAG_TH_SUBTYPE0:
+			break;
+		case NETC_TAG_TH_SUBTYPE1:
+			/* To_Host Subtype 1 tag is 14 bytes, ensure it and the
+			 * EtherType behind it are fully present in the linear
+			 * area before netc_rcv() calls dsa_strip_etype_header()
+			 * to strip the tag.
+			 */
+			if (unlikely(!pskb_may_pull(skb,
+						    NETC_TAG_TH_SUBTYPE1_LEN)))
+				goto err_free_skb;
+
+			tag = dsa_etype_header_pos_rx(skb);
+			netc_rx_tstamp_process(tag, skb);
+			break;
+		case NETC_TAG_TH_SUBTYPE2:
+			/* This skb is a hardware-generated response to a
+			 * two-step transmit timestamp request. The tag
+			 * driver must free the skb after processing.
+			 */
+			netc_twostep_tstamp_process(tag, skb);
+			consume_skb(skb);
+			return NULL;
+		default:
+			dev_warn_ratelimited(&ndev->dev,
+					     "Unsupported To_Host subtype: %d\n",
+					     subtype);
 			goto err_free_skb;
+		}
 	} else {
 		dev_warn_ratelimited(&ndev->dev,
-				     "Unexpected  tag type %d\n", type);
+				     "Unexpected tag type %d\n", type);
 		goto err_free_skb;
 	}
 
@@ -190,16 +300,48 @@ static void netc_flow_dissect(const struct sk_buff *skb, __be16 *proto,
 	int type = FIELD_GET(NETC_TAG_TYPE, tag_cmn->type);
 	int tag_len = netc_get_rx_tag_len(type, subtype);
 
-	/* The RX minimum frame length of the NETC switch port is 64 bytes,
-	 * and the frame is received by the ENETC driver. From the hardware
-	 * perspective, the receive buffer of RX BD is at least 128 bytes,
-	 * so the switch tag header is guaranteed to be in the linear region
-	 * of the skb.
+	/* The CPU port of the switch is connected to the ENETC, so the frame
+	 * is received by the ENETC driver. From the hardware perspective, the
+	 * receive buffer of RX BD is at least 128 bytes, so the switch tag
+	 * header is guaranteed to be in the linear region of the skb.
+	 *
+	 * When the subtype of the frame is NETC_TAG_TH_SUBTYPE2, it indicates
+	 * the frame is a hardware generated timestamp response, which is only
+	 * 26 bytes (DMAC + SMAC + tag), so the frame has no payload after the
+	 * tag. Therefore, there is no need to parse the protocol and offset.
+	 * For other types of the frames, they are all received from the switch
+	 * ports, and the RX minimum frame length of the port is 64 bytes,
+	 * frames shorter than 64 bytes will be discarded by the hardware and
+	 * will not be received by the software.
 	 */
+	if (subtype == NETC_TAG_TH_SUBTYPE2)
+		return;
+
 	*offset = tag_len;
 	*proto = ((__be16 *)skb->data)[(tag_len / 2) - 1];
 }
 
+static int netc_connect(struct dsa_switch *ds)
+{
+	struct netc_tagger_data *tagger_data;
+
+	tagger_data = kzalloc_obj(*tagger_data);
+	if (!tagger_data)
+		return -ENOMEM;
+
+	ds->tagger_data = tagger_data;
+
+	return 0;
+}
+
+static void netc_disconnect(struct dsa_switch *ds)
+{
+	struct netc_tagger_data *tagger_data = ds->tagger_data;
+
+	kfree(tagger_data);
+	ds->tagger_data = NULL;
+}
+
 static const struct dsa_device_ops netc_netdev_ops = {
 	.name			= NETC_NAME,
 	.proto			= DSA_TAG_PROTO_NETC,
@@ -207,6 +349,8 @@ static const struct dsa_device_ops netc_netdev_ops = {
 	.rcv			= netc_rcv,
 	.needed_headroom	= NETC_TAG_MAX_LEN,
 	.flow_dissect		= netc_flow_dissect,
+	.connect		= netc_connect,
+	.disconnect		= netc_disconnect,
 };
 
 MODULE_DESCRIPTION("DSA tag driver for NXP NETC switch family");
-- 
2.34.1


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

* [PATCH v3 net-next 8/8] net: dsa: netc: add PTP one-step timestamping support
  2026-08-31  8:23 [PATCH v3 net-next 0/8] net: dsa: netc: add PTP support for NETC switch wei.fang
                   ` (6 preceding siblings ...)
  2026-08-31  8:23 ` [PATCH v3 net-next 7/8] net: dsa: netc: add PTP two-step timestamping support wei.fang
@ 2026-08-31  8:23 ` wei.fang
       [not found]   ` <20260901082027.461281F00A3F@smtp.kernel.org>
  2026-09-01 15:09 ` [PATCH v3 net-next 0/8] net: dsa: netc: add PTP support for NETC switch Jakub Kicinski
  8 siblings, 1 reply; 16+ messages in thread
From: wei.fang @ 2026-08-31  8:23 UTC (permalink / raw)
  To: xiaoning.wang, andrew, olteanv, andrew+netdev, davem, edumazet,
	kuba, pabeni, horms, richardcochran
  Cc: wei.fang, imx, netdev, linux-kernel, linuxppc-dev,
	linux-arm-kernel

From: Wei Fang <wei.fang@nxp.com>

The NETC switch supports one-step TX timestamping for PTP Sync frames.
When transmitting a Sync frame, the driver reads the current PTP time
and writes it into the frame's originTimestamp field, and inserts a
To_Port subtype 1 tag carrying the low 30 bits of that same PTP time.
As the frame leaves the wire, the MAC captures the SFD transmit time
and derives the residence time from the timestamp in the tag. It then
reads the correction field at the offset given by
PM_SINGLE_STEP[OFFSET], adds the residence time to it, and writes the
result back.

PM_SINGLE_STEP is a per-port register that can describe only one
in-flight frame at a time, and programming it requires reading the
current PTP time, which may sleep. Both constraints rule out handling
one-step Sync on the xmit path.

Instead, defer transmission to a per-port process-context work. The
xmit path classifies the frame in netc_port_txtstamp(): a genuine
one-step Sync (twoStepFlag cleared) has its PTP header offsets cached
in the skb control block; frames that cannot be handled as one-step
fall back to the two-step path or are sent as normal frames. netc_xmit()
hands the classified frame to the switch driver via the
onestep_sync_enqueue tagger callback, which queues it and kicks the work
if no frame is currently in flight.

The work dequeues one frame at a time, reads a fresh PTP time, programs
PM_SINGLE_STEP, updates the originTimestamp field, and transmits the
frame directly to the conduit via the onestep_sync_xmit tagger callback,
bypassing dsa_user_xmit() to avoid double-counting TX stats. Only one
frame is in flight at a time: the frame carries a TX-completion
destructor that reschedules the work when the conduit frees the skb,
keeping PM_SINGLE_STEP always matched to the frame being transmitted.

The one-step context is reference-counted and its lifetime is decoupled
from the devm-allocated netc_port. In-flight skbs hold a reference via
their destructor, so the context outlives port disable until the conduit
frees the last in-flight skb. Port disable clears onestep->active under
work_lock; a work that runs afterwards observes active cleared will
purge the onestep->queue and return without touching the freed port
resources.

Assisted-by: Wchat:claude-opus-4-8
Signed-off-by: Wei Fang <wei.fang@nxp.com>
---
 drivers/net/dsa/netc/Kconfig          |   1 +
 drivers/net/dsa/netc/netc_main.c      |  60 +++-
 drivers/net/dsa/netc/netc_ptp.c       | 439 +++++++++++++++++++++++++-
 drivers/net/dsa/netc/netc_switch.h    |  45 +++
 drivers/net/dsa/netc/netc_switch_hw.h |   5 +
 include/linux/dsa/tag_netc.h          |  22 ++
 net/dsa/tag_netc.c                    |  70 +++-
 7 files changed, 633 insertions(+), 9 deletions(-)

diff --git a/drivers/net/dsa/netc/Kconfig b/drivers/net/dsa/netc/Kconfig
index 8770b65d0f62..7c930579f1c9 100644
--- a/drivers/net/dsa/netc/Kconfig
+++ b/drivers/net/dsa/netc/Kconfig
@@ -5,6 +5,7 @@ config NET_DSA_NETC_SWITCH
 	depends on NET_DSA && PCI
 	depends on NET_VENDOR_FREESCALE
 	depends on PTP_1588_CLOCK_OPTIONAL
+	depends on PTP_NETC_V4_TIMER || PTP_NETC_V4_TIMER=n
 	select NET_DSA_TAG_NETC
 	select FSL_ENETC_MDIO
 	select NXP_NTMP
diff --git a/drivers/net/dsa/netc/netc_main.c b/drivers/net/dsa/netc/netc_main.c
index cd2274d7c552..88e358e1f7e2 100644
--- a/drivers/net/dsa/netc/netc_main.c
+++ b/drivers/net/dsa/netc/netc_main.c
@@ -74,6 +74,7 @@ static int netc_connect_tag_protocol(struct dsa_switch *ds,
 		return -EPROTONOSUPPORT;
 
 	tagger_data = ds->tagger_data;
+	tagger_data->onestep_sync_enqueue = netc_port_onestep_sync_enqueue;
 	tagger_data->twostep_tstamp_handler = netc_port_twostep_tstamp_handler;
 
 	return 0;
@@ -94,7 +95,7 @@ static void netc_port_rmw(struct netc_port *np, u32 reg,
 	netc_port_wr(np, reg, new);
 }
 
-static void netc_mac_port_wr(struct netc_port *np, u32 reg, u32 val)
+void netc_mac_port_wr(struct netc_port *np, u32 reg, u32 val)
 {
 	if (is_netc_pseudo_port(np))
 		return;
@@ -252,6 +253,21 @@ static void netc_get_switch_capabilities(struct netc_switch *priv)
 	priv->num_bp = FIELD_GET(BPCAPR_NUM_BP, val);
 }
 
+static void netc_free_user_ports(struct netc_switch *priv)
+{
+	struct dsa_switch *ds = priv->ds;
+	struct dsa_port *dp;
+
+	dsa_switch_for_each_user_port(dp, ds) {
+		struct netc_port *np = NETC_PORT(ds, dp->index);
+
+		if (np->onestep) {
+			netc_onestep_put(np->onestep);
+			np->onestep = NULL;
+		}
+	}
+}
+
 static int netc_init_all_ports(struct netc_switch *priv)
 {
 	struct device *dev = priv->dev;
@@ -292,13 +308,13 @@ static int netc_init_all_ports(struct netc_switch *priv)
 
 		err = netc_port_get_info_from_dt(np, dp->dn, dev);
 		if (err)
-			return err;
+			goto free_user_ports;
 
 		if (dsa_port_is_user(dp)) {
 			err = netc_port_create_mdio_bus(np, dp->dn);
 			if (err) {
 				dev_err(dev, "Failed to create MDIO bus\n");
-				return err;
+				goto free_user_ports;
 			}
 
 			/* The ipft_hf_eid is initialized to an invalid entry
@@ -314,11 +330,16 @@ static int netc_init_all_ports(struct netc_switch *priv)
 			 */
 			err = netc_port_ptp_init(np);
 			if (err)
-				return err;
+				goto free_user_ports;
 		}
 	}
 
 	return 0;
+
+free_user_ports:
+	netc_free_user_ports(priv);
+
+	return err;
 }
 
 static void netc_init_ntmp_tbl_versions(struct netc_switch *priv)
@@ -941,7 +962,7 @@ static int netc_setup(struct dsa_switch *ds)
 
 	err = netc_init_ntmp_user(priv);
 	if (err)
-		goto put_ptp_timer;
+		goto free_user_ports;
 
 	INIT_HLIST_HEAD(&priv->fdb_list);
 	mutex_init(&priv->fdbt_lock);
@@ -980,6 +1001,8 @@ static int netc_setup(struct dsa_switch *ds)
 	mutex_destroy(&priv->fdbt_lock);
 	mutex_destroy(&priv->vft_lock);
 	netc_free_ntmp_user(priv);
+free_user_ports:
+	netc_free_user_ports(priv);
 put_ptp_timer:
 	pci_dev_put(priv->tmr_dev);
 
@@ -1006,6 +1029,19 @@ static void netc_free_ports_resources(struct netc_switch *priv)
 
 		disable_delayed_work_sync(&np->tstamp_timeout_work);
 		netc_port_purge_txtstamp_queue(np);
+
+		/* dsa_tree_teardown() calls dsa_tree_teardown_ports() before
+		 * dsa_tree_teardown_switches(), so netc_port_disable() is
+		 * executed before netc_teardown() and onestep->active is set
+		 * to false. In-flight one-step skbs still hold references via
+		 * the destructor; the context (and its work) is freed only
+		 * after the conduit frees the last in-flight skb. By then np
+		 * may be gone, but the work no longer dereferences np because
+		 * onestep->active has been cleared. And onestep->queue will be
+		 * cleared by netc_onestep_work()/netc_onestep_destroy_work().
+		 */
+		netc_onestep_put(np->onestep);
+		np->onestep = NULL;
 	}
 }
 
@@ -1560,6 +1596,7 @@ static int netc_port_enable(struct dsa_switch *ds, int port,
 			    struct phy_device *phy)
 {
 	struct netc_port *np = NETC_PORT(ds, port);
+	struct netc_onestep *onestep = np->onestep;
 	int err;
 
 	if (np->enable)
@@ -1572,6 +1609,12 @@ static int netc_port_enable(struct dsa_switch *ds, int port,
 		return err;
 	}
 
+	if (onestep) {
+		mutex_lock(&onestep->work_lock);
+		onestep->active = true;
+		mutex_unlock(&onestep->work_lock);
+	}
+
 	np->enable = true;
 
 	return 0;
@@ -1580,6 +1623,7 @@ static int netc_port_enable(struct dsa_switch *ds, int port,
 static void netc_port_disable(struct dsa_switch *ds, int port)
 {
 	struct netc_port *np = NETC_PORT(ds, port);
+	struct netc_onestep *onestep = np->onestep;
 
 	/* When .port_disable() is called, .port_enable() may not have been
 	 * called. In this case, both the prepare_count and enable_count of
@@ -1589,6 +1633,12 @@ static void netc_port_disable(struct dsa_switch *ds, int port)
 	if (!np->enable)
 		return;
 
+	if (onestep) {
+		mutex_lock(&onestep->work_lock);
+		onestep->active = false;
+		mutex_unlock(&onestep->work_lock);
+	}
+
 	clk_disable_unprepare(np->ref_clk);
 	np->enable = false;
 }
diff --git a/drivers/net/dsa/netc/netc_ptp.c b/drivers/net/dsa/netc/netc_ptp.c
index f55010f46819..8e6ab4b06580 100644
--- a/drivers/net/dsa/netc/netc_ptp.c
+++ b/drivers/net/dsa/netc/netc_ptp.c
@@ -4,13 +4,307 @@
  * Copyright 2025-2026 NXP
  */
 
+#include <linux/kref.h>
 #include <linux/ptp_classify.h>
 #include <linux/ptp_clock_kernel.h>
+#include <linux/slab.h>
 
 #include "netc_switch.h"
 
 #define NETC_NUM_TS_REQ_ID		16
 #define NETC_TXTSTAMP_TIMEOUT		(5 * HZ)
+#define NETC_MAX_STEP_OFFSET		0x1ff
+#define NETC_ONESTEP_QTH		1024
+
+static void netc_port_set_onestep_control(struct netc_port *np,
+					  bool csum_update, int offset)
+{
+	u32 val;
+
+	val = PM_SINGLE_STEP_EN | FIELD_PREP(PM_SINGLE_STEP_OFFSET, offset);
+	if (csum_update)
+		val |= PM_SINGLE_STEP_CH;
+	netc_mac_port_wr(np, NETC_PM_SINGLE_STEP(0), val);
+}
+
+static void netc_port_purge_onestep_queue(struct netc_onestep *onestep,
+					  bool clear_flight)
+{
+	struct sk_buff_head free_list;
+
+	__skb_queue_head_init(&free_list);
+
+	spin_lock_bh(&onestep->queue_lock);
+	skb_queue_splice_init(&onestep->queue, &free_list);
+	if (clear_flight)
+		onestep->in_flight = false;
+	spin_unlock_bh(&onestep->queue_lock);
+
+	__skb_queue_purge(&free_list);
+}
+
+static void netc_onestep_destroy_work(struct work_struct *work)
+{
+	struct netc_onestep *onestep = container_of(work, struct netc_onestep,
+						    destroy_work);
+
+	/* refcnt reaching zero does not by itself mean onestep->work has
+	 * stopped: the last in-flight skb destructor calls schedule_work(&work)
+	 * *before* the netc_onestep_put() that drops the final reference, so at
+	 * the moment refcnt hits zero onestep->work may still be pending or
+	 * running on another CPU. destroy_work and work are distinct work_structs
+	 * and can run concurrently, so cancel_work_sync() is required to drain
+	 * onestep->work before mutex_destroy()/kfree() below, otherwise a
+	 * still-running work would touch freed memory. No new schedule_work(&work)
+	 * can occur after this point because no references remain, so this
+	 * cancel is final.
+	 */
+	cancel_work_sync(&onestep->work);
+	netc_port_purge_onestep_queue(onestep, true);
+	mutex_destroy(&onestep->work_lock);
+	kfree(onestep);
+	module_put(THIS_MODULE);
+}
+
+static void netc_onestep_release(struct kref *ref)
+{
+	struct netc_onestep *onestep = container_of(ref, struct netc_onestep,
+						    refcnt);
+
+	/* This may be called from the skb destructor in softirq context
+	 * (napi_consume_skb()), where cancel_work_sync() must not be used.
+	 * Defer the final teardown to process context.
+	 */
+	schedule_work(&onestep->destroy_work);
+}
+
+static void netc_onestep_get(struct netc_onestep *onestep)
+{
+	kref_get(&onestep->refcnt);
+}
+
+void netc_onestep_put(struct netc_onestep *onestep)
+{
+	kref_put(&onestep->refcnt, netc_onestep_release);
+}
+
+static void netc_onestep_skb_destructor(struct sk_buff *skb)
+{
+	struct netc_onestep *onestep = skb_shinfo(skb)->destructor_arg;
+
+	/* skb has been transmitted by hardware. Schedule work to send the next
+	 * queued one-step Sync packet, then release this skb's reference on the
+	 * context. If the port has already been torn down and this is the last
+	 * reference, the context is freed via netc_onestep_release().
+	 */
+	schedule_work(&onestep->work);
+	netc_onestep_put(onestep);
+}
+
+static void netc_port_program_onestep(struct netc_port *np,
+				      struct netc_onestep *onestep,
+				      struct sk_buff *skb,
+				      u64 tstamp)
+{
+	u16 correction_offset = NETC_SKB_CB(skb)->correction_offset;
+	u16 tstamp_offset = NETC_SKB_CB(skb)->timestamp_offset;
+	u8 *hdr = skb_mac_header(skb);
+	bool csum_update = false;
+	__be32 new_sec_l, new_ns;
+	__be16 new_sec_h;
+	u64 sec;
+	u32 ns;
+
+	NETC_SKB_CB(skb)->tstamp = tstamp;
+	NETC_SKB_CB(skb)->ptp_flag = NETC_PTP_FLAG_ONESTEP;
+
+	/* Update originTimestamp field of Sync packet
+	 * - 48 bits seconds field
+	 * - 32 bits nanoseconds field
+	 */
+	sec = div_u64_rem(tstamp, NSEC_PER_SEC, &ns);
+	new_sec_h = htons((sec >> 32) & 0xffff);
+	new_sec_l = htonl(sec & 0xffffffff);
+	new_ns = htonl(ns);
+
+	if (NETC_SKB_CB(skb)->is_udp) {
+		__be32 old_sec_l, old_ns;
+		struct udphdr *uh;
+		__be16 old_sec_h;
+
+		if (skb->ip_summed == CHECKSUM_PARTIAL) {
+			csum_update = true;
+			goto update_timestamp;
+		}
+
+		if (unlikely(!skb_transport_header_was_set(skb)))
+			uh = (struct udphdr *)(hdr + tstamp_offset -
+					       sizeof(struct ptp_header) -
+					       sizeof(struct udphdr));
+		else
+			uh = udp_hdr(skb);
+
+		/* For IPv4, a UDP checksum of zero on the wire means "no
+		 * checksum". For IPv6, its UDP checksum is mandatory and
+		 * never zero.
+		 */
+		if (!uh->check)
+			goto update_timestamp;
+
+		old_sec_h = __get_unaligned_t(__be16, hdr + tstamp_offset);
+		old_sec_l = __get_unaligned_t(__be32, hdr + tstamp_offset + 2);
+		old_ns = __get_unaligned_t(__be32, hdr + tstamp_offset + 6);
+		inet_proto_csum_replace2(&uh->check, skb, old_sec_h,
+					 new_sec_h, false);
+		inet_proto_csum_replace4(&uh->check, skb, old_sec_l,
+					 new_sec_l, false);
+		inet_proto_csum_replace4(&uh->check, skb, old_ns,
+					 new_ns, false);
+		csum_update = true;
+	}
+
+update_timestamp:
+	__put_unaligned_t(__be16, new_sec_h, hdr + tstamp_offset);
+	__put_unaligned_t(__be32, new_sec_l, hdr + tstamp_offset + 2);
+	__put_unaligned_t(__be32, new_ns, hdr + tstamp_offset + 6);
+
+	netc_port_set_onestep_control(np, csum_update, correction_offset);
+
+	/* Orphan the skb to release the socket send buffer quota immediately.
+	 * This is safe because sock_wfree() does not access skb->data or any
+	 * frame content. After skb_orphan(), we install our own destructor so
+	 * that when the conduit driver frees the skb after TX completion, we
+	 * get notified to send the next queued Sync packet.
+	 */
+	skb_orphan(skb);
+	netc_onestep_get(onestep); /* in-flight reference */
+	skb_shinfo(skb)->destructor_arg = onestep;
+	skb->destructor = netc_onestep_skb_destructor;
+}
+
+static int netc_get_phc_time(struct netc_switch *priv, u64 *ns)
+{
+	if (unlikely(!priv->tmr_dev))
+		return -ENODEV;
+
+	return netc_timer_get_current_time(priv->tmr_dev, ns);
+}
+
+static void netc_onestep_work(struct work_struct *work)
+{
+	struct netc_onestep *onestep = container_of(work, struct netc_onestep,
+						    work);
+	struct netc_tagger_data *tagger_data;
+	struct netc_switch *priv;
+	struct netc_port *np;
+	struct sk_buff *skb;
+	u64 tstamp;
+
+	/* Serialize the whole hardware access against port disable. work_lock
+	 * is a mutex (this runs in process context and netc_get_phc_time() may
+	 * sleep). If the port has been disabled, bail out immediately; np and
+	 * priv are only dereferenced after the @active check passes, so they
+	 * are always valid here.
+	 */
+	mutex_lock(&onestep->work_lock);
+	if (unlikely(!onestep->active)) {
+		netc_port_purge_onestep_queue(onestep, true);
+		goto unlock_work;
+	}
+
+	/* Send only one queued Sync per run. The shared SINGLE_STEP register
+	 * must match the frame currently being transmitted, so the next frame
+	 * is programmed only after this one completes TX, when its skb
+	 * destructor reschedules this work. Dequeue under onestep->queue_lock,
+	 * and if the queue has drained, release the in-flight slot so a later
+	 * frame from the xmit path kicks the work again.
+	 */
+	spin_lock_bh(&onestep->queue_lock);
+	skb = __skb_dequeue(&onestep->queue);
+	if (!skb) {
+		onestep->in_flight = false;
+		spin_unlock_bh(&onestep->queue_lock);
+		goto unlock_work;
+	}
+	spin_unlock_bh(&onestep->queue_lock);
+
+	np = onestep->np;
+	priv = np->switch_priv;
+	if (unlikely(netc_get_phc_time(priv, &tstamp))) {
+		/* The PTP timer is not available, so there is no correct
+		 * timestamp to program. Drop this frame and re-kick to process
+		 * the remaining queued frames.
+		 *
+		 * netc_port_program_onestep() has not run for this skb yet, so
+		 * netc_onestep_skb_destructor() is not installed on it. Freeing
+		 * it therefore does not reschedule the work, so the work must be
+		 * rescheduled explicitly to keep draining the queue.
+		 */
+		dev_dbg_ratelimited(priv->dev,
+				    "Port %d PTP timer unavailable, drop Sync\n",
+				    np->dp->index);
+		kfree_skb(skb);
+		schedule_work(&onestep->work);
+		goto unlock_work;
+	}
+
+	/* Reuse the offsets cached at enqueue time; only the timestamp is
+	 * read fresh so it reflects the actual TX moment.
+	 */
+	netc_port_program_onestep(np, onestep, skb, tstamp);
+
+	/* Tag and hand the frame directly to the conduit via the tagger,
+	 * bypassing dsa_user_xmit() so the TX stats are not counted twice.
+	 * And there is no need to check if tagger_data is NULL, because
+	 * dsa_tree_teardown_ports() executes before
+	 * dsa_switch_teardown_tag_protocol(), so tagger_data cannot be
+	 * NULL when onestep->active is set.
+	 */
+	tagger_data = priv->ds->tagger_data;
+	tagger_data->onestep_sync_xmit(skb, np->dp->user);
+
+unlock_work:
+	mutex_unlock(&onestep->work_lock);
+}
+
+static int netc_port_onestep_alloc(struct netc_port *np)
+{
+	struct netc_onestep *onestep;
+	int err;
+
+	/* Hold a module reference until the last in-flight one-step Sync skb
+	 * is freed by the conduit. Without this, the module could be unloaded
+	 * before netc_onestep_skb_destructor() returns, causing a panic.
+	 * Released in netc_onestep_destroy_work() after all cleanup is done.
+	 */
+	if (!try_module_get(THIS_MODULE)) {
+		dev_err(np->switch_priv->dev,
+			"Failed to get the driver module\n");
+		return -ENODEV;
+	}
+
+	onestep = kzalloc_obj(*onestep);
+	if (!onestep) {
+		err = -ENOMEM;
+		goto put_module;
+	}
+
+	kref_init(&onestep->refcnt); /* port (owner) reference */
+	np->onestep = onestep;
+	onestep->np = np;
+	mutex_init(&onestep->work_lock);
+	spin_lock_init(&onestep->queue_lock);
+	__skb_queue_head_init(&onestep->queue);
+	INIT_WORK(&onestep->work, netc_onestep_work);
+	INIT_WORK(&onestep->destroy_work, netc_onestep_destroy_work);
+
+	return 0;
+
+put_module:
+	module_put(THIS_MODULE);
+
+	return err;
+}
 
 static void netc_port_tstamp_timeout_work(struct work_struct *work)
 {
@@ -57,7 +351,7 @@ int netc_port_ptp_init(struct netc_port *np)
 	INIT_DELAYED_WORK(&np->tstamp_timeout_work,
 			  netc_port_tstamp_timeout_work);
 
-	return 0;
+	return netc_port_onestep_alloc(np);
 }
 
 void netc_port_purge_txtstamp_queue(struct netc_port *np)
@@ -94,7 +388,8 @@ int netc_get_ts_info(struct dsa_switch *ds, int port,
 				 SOF_TIMESTAMPING_RX_HARDWARE |
 				 SOF_TIMESTAMPING_RAW_HARDWARE;
 
-	info->tx_types = BIT(HWTSTAMP_TX_OFF) | BIT(HWTSTAMP_TX_ON);
+	info->tx_types = BIT(HWTSTAMP_TX_OFF) | BIT(HWTSTAMP_TX_ON) |
+			 BIT(HWTSTAMP_TX_ONESTEP_SYNC);
 
 	info->rx_filters = BIT(HWTSTAMP_FILTER_NONE) |
 			   BIT(HWTSTAMP_FILTER_PTP_V2_EVENT) |
@@ -316,6 +611,7 @@ int netc_port_hwtstamp_set(struct dsa_switch *ds, int port,
 	switch (config->tx_type) {
 	case HWTSTAMP_TX_ON:
 	case HWTSTAMP_TX_OFF:
+	case HWTSTAMP_TX_ONESTEP_SYNC:
 		break;
 	default:
 		return -ERANGE;
@@ -352,6 +648,9 @@ int netc_port_hwtstamp_set(struct dsa_switch *ds, int port,
 
 	WRITE_ONCE(np->ptp_tx_type, config->tx_type);
 
+	if (config->tx_type != HWTSTAMP_TX_ONESTEP_SYNC)
+		netc_port_purge_onestep_queue(np->onestep, false);
+
 	config->rx_filter = rx_filter;
 
 	return 0;
@@ -455,10 +754,100 @@ bool netc_port_rxtstamp(struct dsa_switch *ds, int port, struct sk_buff *skb,
 	return false;
 }
 
+static void netc_port_prepare_onestep_sync(struct netc_port *np,
+					   struct sk_buff *skb,
+					   u32 ptp_class, bool *twostep)
+{
+	struct netc_switch *priv = np->switch_priv;
+	u16 correction_offset, tstamp_offset;
+	struct ptp_header *ptp_hdr;
+	u8 msg_type, twostep_flag;
+	bool is_udp = false;
+	u32 pkt_type;
+	u8 *pkt_hdr;
+
+	if (unlikely(skb_linearize(skb))) {
+		NETC_SKB_CB(skb)->ptp_flag = NETC_PTP_FLAG_DROP;
+		return;
+	}
+
+	ptp_hdr = ptp_parse_header(skb, ptp_class);
+	if (unlikely(!ptp_hdr)) {
+		NETC_SKB_CB(skb)->ptp_flag = NETC_PTP_FLAG_DROP;
+		dev_dbg_ratelimited(priv->dev,
+				    "Port %d failed to parse Sync header\n",
+				    np->dp->index);
+		return;
+	}
+
+	msg_type = ptp_get_msgtype(ptp_hdr, ptp_class);
+	twostep_flag = ptp_hdr->flag_field[0] & 0x2;
+
+	pkt_hdr = skb_mac_header(skb);
+	correction_offset = (u8 *)&ptp_hdr->correction - pkt_hdr;
+	tstamp_offset = (u8 *)ptp_hdr + sizeof(*ptp_hdr) - pkt_hdr;
+
+	/* Ensure that the entire originTimestamp field is present in the
+	 * linear buffer of the skb and the correction_offset must be within
+	 * the hardware capability.
+	 */
+	if (unlikely(tstamp_offset + 10 > skb_headlen(skb) ||
+		     correction_offset > NETC_MAX_STEP_OFFSET)) {
+		NETC_SKB_CB(skb)->ptp_flag = NETC_PTP_FLAG_DROP;
+		dev_dbg_ratelimited(priv->dev,
+				    "Port %d PTP offset check error\n",
+				    np->dp->index);
+		return;
+	}
+
+	/* Only a Sync frame with the twoStepFlag cleared can use one-step
+	 * timestamping. A frame that requests two-step (or is not a Sync)
+	 * carries different on-wire fields, so this is a real classification;
+	 * report it through *twostep so the caller falls back to the two-step
+	 * path.
+	 */
+	if (msg_type != PTP_MSGTYPE_SYNC || twostep_flag != 0) {
+		*twostep = true;
+		return;
+	}
+
+	/* This is a genuine one-step Sync frame. skb_shinfo()->destructor_arg
+	 * is later used to pass the np->onestep pointer to
+	 * netc_onestep_skb_destructor() for TX completion notification.
+	 * MSG_ZEROCOPY also uses destructor_arg (via skb_zcopy_init()) to
+	 * track user-space page references. Overwriting it in that case would
+	 * leak the ubuf_info reference and prevent user pages from being
+	 * released. PTP applications do not use MSG_ZEROCOPY, but guard
+	 * against it defensively.
+	 */
+	if (skb_zcopy(skb)) {
+		NETC_SKB_CB(skb)->ptp_flag = NETC_PTP_FLAG_DROP;
+		dev_dbg_ratelimited(priv->dev,
+				    "Port %d one-step Sync not supported on zerocopy skb\n",
+				    np->dp->index);
+		return;
+	}
+
+	pkt_type = ptp_class & PTP_CLASS_PMASK;
+	if (pkt_type == PTP_CLASS_IPV4 || pkt_type == PTP_CLASS_IPV6)
+		is_udp = true;
+
+	/* Cache the parsing results so the tagger xmit path and the deferred
+	 * work do not need to re-parse the PTP header, and so that
+	 * netc_port_program_onestep() can derive these parameters from the
+	 * skb.
+	 */
+	NETC_SKB_CB(skb)->correction_offset = correction_offset;
+	NETC_SKB_CB(skb)->timestamp_offset = tstamp_offset;
+	NETC_SKB_CB(skb)->is_udp = is_udp;
+	NETC_SKB_CB(skb)->ptp_flag = NETC_PTP_FLAG_ONESTEP;
+}
+
 void netc_port_txtstamp(struct dsa_switch *ds, int port, struct sk_buff *skb)
 {
 	struct netc_port *np = NETC_PORT(ds, port);
 	int tx_type = READ_ONCE(np->ptp_tx_type);
+	bool twostep = false;
 	u32 ptp_class;
 
 	NETC_SKB_CB(skb)->ptp_flag = 0;
@@ -466,6 +855,50 @@ void netc_port_txtstamp(struct dsa_switch *ds, int port, struct sk_buff *skb)
 	if (ptp_class == PTP_CLASS_NONE)
 		return;
 
-	if (tx_type == HWTSTAMP_TX_ON)
+	if (tx_type == HWTSTAMP_TX_ONESTEP_SYNC)
+		netc_port_prepare_onestep_sync(np, skb, ptp_class, &twostep);
+
+	if (tx_type == HWTSTAMP_TX_ON || twostep)
 		netc_port_txtstamp_twostep(np, skb);
 }
+
+void netc_port_onestep_sync_enqueue(struct dsa_switch *ds, int port,
+				    struct sk_buff *skb)
+{
+	struct netc_port *np = NETC_PORT(ds, port);
+	struct netc_onestep *onestep = np->onestep;
+	bool kick = false;
+
+	/* This runs in the xmit path (softirq / BH-disabled), so it must not
+	 * sleep: only queue the frame here and let netc_onestep_work()
+	 * program the SINGLE_STEP register and transmit it from process
+	 * context. The shared SINGLE_STEP register can describe only one frame
+	 * at a time, so at most one one-step Sync may be in flight. Track that
+	 * with @in_flight under onestep->queue_lock.
+	 *
+	 * Enqueue the frame and, only if no frame is currently in flight, claim
+	 * the in-flight slot and kick the work. When a frame is already in
+	 * flight, just queue: its skb destructor will kick the work to send the
+	 * next one once it completes TX, so the frames are transmitted strictly
+	 * one at a time in order.
+	 */
+	spin_lock_bh(&onestep->queue_lock);
+	if (unlikely(skb_queue_len(&onestep->queue) >= NETC_ONESTEP_QTH)) {
+		spin_unlock_bh(&onestep->queue_lock);
+		kfree_skb(skb);
+		return;
+	}
+
+	__skb_queue_tail(&onestep->queue, skb);
+	if (!onestep->in_flight) {
+		onestep->in_flight = true;
+		kick = true;
+	}
+	spin_unlock_bh(&onestep->queue_lock);
+
+	/* Ownership is transferred to the queue; netc_xmit() stops processing
+	 * this skb. The work will program and transmit it.
+	 */
+	if (kick)
+		schedule_work(&onestep->work);
+}
diff --git a/drivers/net/dsa/netc/netc_switch.h b/drivers/net/dsa/netc/netc_switch.h
index b214eeb202b0..7de7132cd0c0 100644
--- a/drivers/net/dsa/netc/netc_switch.h
+++ b/drivers/net/dsa/netc/netc_switch.h
@@ -9,6 +9,7 @@
 #include <linux/dsa/tag_netc.h>
 #include <linux/fsl/netc_global.h>
 #include <linux/fsl/ntmp.h>
+#include <linux/mutex.h>
 #include <linux/of_device.h>
 #include <linux/of_net.h>
 #include <linux/pci.h>
@@ -87,6 +88,44 @@ enum netc_host_reason {
 	NETC_HR_PTP_TRAP   = 9,
 };
 
+/* One-step Sync serialization context.
+ *
+ * Its lifetime is decoupled from the devm-allocated netc_port. An in-flight
+ * one-step Sync skb keeps a reference on this context via its skb destructor,
+ * so the context outlives the port teardown until the conduit frees the last
+ * in-flight skb after TX completion. Once the port is disabled, @active is
+ * cleared and the work stops touching any devm memory (netc_port/netc_switch)
+ * or the unregistered user netdev or the tagger_data.
+ */
+struct netc_onestep {
+	struct netc_port *np;
+	struct kref refcnt;
+	/* Process-context lock: serializes the deferred TX work against port
+	 * teardown, so the work never touches the devm-allocated netc_port /
+	 * netc_switch or the unregistered user netdev after teardown. Held
+	 * across netc_get_phc_time(), which may sleep, hence a mutex.
+	 */
+	struct mutex work_lock;
+	/* Serialize access to in_flight and queue */
+	spinlock_t queue_lock;
+	bool active;	/* set when port is enabled, under @work_lock */
+	/* In-flight slot: true while one one-step Sync frame is programmed
+	 * into the shared SINGLE_STEP register and being transmitted. Only one
+	 * frame may be in flight at a time, so the next queued frame is sent
+	 * only after the current one completes TX (its skb destructor kicks
+	 * the work). Accessed under queue_lock, from both the softirq xmit
+	 * path and the process-context work.
+	 */
+	bool in_flight;
+	/* Pending one-step Sync frames. Enqueued from the softirq xmit path and
+	 * dequeued by the process-context work; the list is serialized by
+	 * queue_lock together with @in_flight.
+	 */
+	struct sk_buff_head queue;
+	struct work_struct work;	/* drains @queue */
+	struct work_struct destroy_work; /* frees the context in process ctx */
+};
+
 struct netc_port {
 	void __iomem *iobase;
 	struct netc_switch *switch_priv;
@@ -111,6 +150,8 @@ struct netc_port {
 	/* skb queue for two-step timestamp frames */
 	struct sk_buff_head skb_txtstamp_queue;
 	struct delayed_work tstamp_timeout_work;
+	/* one-step Sync serialization context */
+	struct netc_onestep *onestep;
 	int ptp_tx_type;
 	int ptp_rx_filter;
 	u32 ptp_ipft_eid[NETC_PTP_MAX];
@@ -217,6 +258,7 @@ static inline void netc_del_vlan_entry(struct netc_vlan_entry *entry)
 }
 
 int netc_switch_platform_probe(struct netc_switch *priv);
+void netc_mac_port_wr(struct netc_port *np, u32 reg, u32 val);
 
 /* ethtool APIs */
 void netc_port_get_pause_stats(struct dsa_switch *ds, int port,
@@ -248,5 +290,8 @@ void netc_port_twostep_tstamp_handler(struct dsa_switch *ds, int port,
 bool netc_port_rxtstamp(struct dsa_switch *ds, int port, struct sk_buff *skb,
 			unsigned int type);
 void netc_port_txtstamp(struct dsa_switch *ds, int port, struct sk_buff *skb);
+void netc_onestep_put(struct netc_onestep *onestep);
+void netc_port_onestep_sync_enqueue(struct dsa_switch *ds, int port,
+				    struct sk_buff *skb);
 
 #endif
diff --git a/drivers/net/dsa/netc/netc_switch_hw.h b/drivers/net/dsa/netc/netc_switch_hw.h
index 1404ae41c7bc..37d1dd7ec2c7 100644
--- a/drivers/net/dsa/netc/netc_switch_hw.h
+++ b/drivers/net/dsa/netc/netc_switch_hw.h
@@ -203,6 +203,11 @@ enum netc_stg_stage {
 #define   SSP_10M			1
 #define   SSP_1G			2
 
+#define NETC_PM_SINGLE_STEP(a)		(0x10c0 + (a) * 0x400)
+#define  PM_SINGLE_STEP_CH		BIT(6)
+#define  PM_SINGLE_STEP_OFFSET		GENMASK(15, 7)
+#define  PM_SINGLE_STEP_EN		BIT(31)
+
 /* Port MAC 0/1 Receive Ethernet Octets Counter */
 #define NETC_PM_REOCT(a)		(0x1100 + (a) * 0x400)
 
diff --git a/include/linux/dsa/tag_netc.h b/include/linux/dsa/tag_netc.h
index da200e3ba8ad..5ac5e2e72dff 100644
--- a/include/linux/dsa/tag_netc.h
+++ b/include/linux/dsa/tag_netc.h
@@ -10,13 +10,23 @@
 #include <net/dsa.h>
 
 #define NETC_TAG_MAX_LEN			14
+#define NETC_PTP_FLAG_ONESTEP			BIT(0)
 #define NETC_PTP_FLAG_TWOSTEP			BIT(1)
+#define NETC_PTP_FLAG_DROP			BIT(2)
 
 struct netc_skb_cb {
 	unsigned long ptp_tx_time;
 	u64 tstamp;
 	u8 ptp_flag;
 	u8 ts_req_id;
+	/* One-step Sync parsing results, computed in netc_port_txtstamp()
+	 * and reused in the tagger xmit path and the deferred work, to avoid
+	 * re-parsing the PTP header. Valid only while
+	 * ptp_flag == NETC_PTP_FLAG_ONESTEP.
+	 */
+	u16 correction_offset;
+	u16 timestamp_offset;
+	bool is_udp;
 };
 
 #define NETC_SKB_CB(skb)	((struct netc_skb_cb *)((skb)->cb))
@@ -26,10 +36,22 @@ struct netc_skb_cb {
  * @twostep_tstamp_handler: Called by the tagger when a two-step transmit
  *	timestamp response is received, to deliver the timestamp to the
  *	switch driver.
+ * @onestep_sync_enqueue: Called from the tagger xmit path for a one-step Sync
+ *	frame. The switch driver takes ownership of the skb and queues it for
+ *	deferred transmission from process context, where the shared
+ *	PM_SINGLE_STEP register can be programmed and the PTP timer read
+ *	(which may sleep). The tagger must not touch the skb after this call
+ *	and returns NULL to dsa_user_xmit().
+ * @onestep_sync_xmit: Called by the switch driver to transmit a deferred
+ *	one-step Sync frame directly to the conduit, bypassing dsa_user_xmit().
  */
 struct netc_tagger_data {
 	void (*twostep_tstamp_handler)(struct dsa_switch *ds, int port,
 				       u8 ts_req_id, u64 ts);
+	void (*onestep_sync_enqueue)(struct dsa_switch *ds, int port,
+				     struct sk_buff *skb);
+	netdev_tx_t (*onestep_sync_xmit)(struct sk_buff *skb,
+					 struct net_device *ndev);
 };
 
 #endif
diff --git a/net/dsa/tag_netc.c b/net/dsa/tag_netc.c
index d46576ae2f75..d9bc4a5e8ae9 100644
--- a/net/dsa/tag_netc.c
+++ b/net/dsa/tag_netc.c
@@ -16,6 +16,8 @@
 #define NETC_TAG_TO_PORT		1
 /* SubType0: No request to perform timestamping */
 #define NETC_TAG_TP_SUBTYPE0		0
+/* SubType1: Request to perform one-step timestamping */
+#define NETC_TAG_TP_SUBTYPE1		1
 /* SubType2: Request to perform two-step timestamping */
 #define NETC_TAG_TP_SUBTYPE2		2
 
@@ -31,6 +33,7 @@
 /* NETC switch tag lengths */
 #define NETC_TAG_FORWARD_LEN		6
 #define NETC_TAG_TP_SUBTYPE0_LEN	6
+#define NETC_TAG_TP_SUBTYPE1_LEN	10
 #define NETC_TAG_TP_SUBTYPE2_LEN	6
 #define NETC_TAG_TH_SUBTYPE0_LEN	6
 #define NETC_TAG_TH_SUBTYPE1_LEN	14
@@ -44,6 +47,7 @@
 #define NETC_TAG_SWITCH			GENMASK(2, 0)
 #define NETC_TAG_PORT			GENMASK(7, 3)
 #define NETC_TAG_TS_REQ_ID		GENMASK(3, 0)
+#define NETC_TAG_TIMESTAMP		GENMASK(29, 0)
 
 struct netc_tag_cmn {
 	__be16 tpid;
@@ -52,6 +56,12 @@ struct netc_tag_cmn {
 	u8 switch_port;
 } __packed;
 
+struct netc_tag_tp_subtype1 {
+	struct netc_tag_cmn cmn;
+	u8 resv;
+	__be32 timestamp;
+} __packed;
+
 struct netc_tag_tp_subtype2 {
 	struct netc_tag_cmn cmn;
 	u8 ts_req_id;
@@ -118,6 +128,17 @@ static void netc_fill_tp_tag_subtype0(struct sk_buff *skb,
 				NETC_TAG_TP_SUBTYPE0_LEN);
 }
 
+static void netc_fill_tp_tag_subtype1(struct sk_buff *skb,
+				      struct net_device *ndev)
+{
+	u32 ts = FIELD_PREP(NETC_TAG_TIMESTAMP, NETC_SKB_CB(skb)->tstamp);
+	struct netc_tag_tp_subtype1 *tag;
+
+	tag = netc_fill_common_tp_tag(skb, ndev, NETC_TAG_TP_SUBTYPE1,
+				      NETC_TAG_TP_SUBTYPE1_LEN);
+	tag->timestamp = htonl(ts);
+}
+
 static void netc_fill_tp_tag_subtype2(struct sk_buff *skb,
 				      struct net_device *ndev)
 {
@@ -129,6 +150,42 @@ static void netc_fill_tp_tag_subtype2(struct sk_buff *skb,
 	tag->ts_req_id = FIELD_PREP(NETC_TAG_TS_REQ_ID, ts_req_id);
 }
 
+static netdev_tx_t netc_onestep_sync_xmit(struct sk_buff *skb,
+					  struct net_device *dev)
+{
+	/* This deferred one-step Sync frame already went through
+	 * dsa_user_xmit()'s skb_ensure_writable_head_tail() before it was
+	 * queued in netc_xmit(), and nothing has cloned it or shrunk its
+	 * head room since. So the head room is still guaranteed and the
+	 * skb is still writable; only the tag needs to be pushed before
+	 * handing it directly to the conduit, bypassing dsa_user_xmit()
+	 * so that dev_sw_netstats_tx_add() is not invoked a second time
+	 * for the same frame.
+	 */
+	netc_fill_tp_tag_subtype1(skb, dev);
+
+	return dsa_enqueue_skb(skb, dev);
+}
+
+static void netc_onestep_sync_enqueue(struct sk_buff *skb,
+				      struct net_device *ndev)
+{
+	struct dsa_port *dp = dsa_user_to_port(ndev);
+	struct netc_tagger_data *tagger_data;
+
+	tagger_data = dp->ds->tagger_data;
+	if (unlikely(!tagger_data->onestep_sync_enqueue)) {
+		kfree_skb(skb);
+		return;
+	}
+
+	/* Hand the one-step Sync to the switch driver, which takes ownership
+	 * and queues it for deferred transmission from its work. The tagger
+	 * must not touch the skb after this point.
+	 */
+	tagger_data->onestep_sync_enqueue(dp->ds, dp->index, skb);
+}
+
 static struct sk_buff *netc_xmit(struct sk_buff *skb,
 				 struct net_device *ndev)
 {
@@ -138,9 +195,19 @@ static struct sk_buff *netc_xmit(struct sk_buff *skb,
 	if (likely(!ptp_flag)) {
 		netc_fill_tp_tag_subtype0(skb, ndev);
 		return skb;
+	}
+
+	if (ptp_flag == NETC_PTP_FLAG_ONESTEP) {
+		/* The switch driver takes ownership of the one-step Sync and
+		 * queues it for deferred TX; the deferred work tags it subtype 1
+		 * and transmits it directly to the conduit. Return NULL so
+		 * dsa_user_xmit() stops processing this skb.
+		 */
+		netc_onestep_sync_enqueue(skb, ndev);
+		return NULL;
 	} else if (ptp_flag == NETC_PTP_FLAG_TWOSTEP) {
 		netc_fill_tp_tag_subtype2(skb, ndev);
-	} else {
+	} else { /* NETC_PTP_FLAG_DROP */
 		kfree_skb(skb);
 		return NULL;
 	}
@@ -329,6 +396,7 @@ static int netc_connect(struct dsa_switch *ds)
 	if (!tagger_data)
 		return -ENOMEM;
 
+	tagger_data->onestep_sync_xmit = netc_onestep_sync_xmit;
 	ds->tagger_data = tagger_data;
 
 	return 0;
-- 
2.34.1


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

* Re: [PATCH v3 net-next 0/8] net: dsa: netc: add PTP support for NETC switch
  2026-08-31  8:23 [PATCH v3 net-next 0/8] net: dsa: netc: add PTP support for NETC switch wei.fang
                   ` (7 preceding siblings ...)
  2026-08-31  8:23 ` [PATCH v3 net-next 8/8] net: dsa: netc: add PTP one-step " wei.fang
@ 2026-09-01 15:09 ` Jakub Kicinski
  2026-09-02  1:43   ` Wei Fang
  8 siblings, 1 reply; 16+ messages in thread
From: Jakub Kicinski @ 2026-09-01 15:09 UTC (permalink / raw)
  To: wei.fang
  Cc: xiaoning.wang, andrew, olteanv, andrew+netdev, davem, edumazet,
	pabeni, horms, richardcochran, wei.fang, imx, netdev,
	linux-kernel, linuxppc-dev, linux-arm-kernel

On Mon, 31 Aug 2026 16:23:27 +0800 wei.fang@oss.nxp.com wrote:
> Subject: [PATCH v3 net-next 0/8] net: dsa: netc: add PTP support for NETC switch

Please be advised that the limit of 15 patches applies across 
the series. You posted 2 large series to net-next at once. 

See "Limit patches outstanding on mailing list" in the docs.
If this happens again we'll be forced to start discarding your patches.

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

* RE: [PATCH v3 net-next 0/8] net: dsa: netc: add PTP support for NETC switch
  2026-09-01 15:09 ` [PATCH v3 net-next 0/8] net: dsa: netc: add PTP support for NETC switch Jakub Kicinski
@ 2026-09-02  1:43   ` Wei Fang
  0 siblings, 0 replies; 16+ messages in thread
From: Wei Fang @ 2026-09-02  1:43 UTC (permalink / raw)
  To: Jakub Kicinski, Wei Fang (OSS)
  Cc: Clark Wang, andrew@lunn.ch, olteanv@gmail.com,
	andrew+netdev@lunn.ch, davem@davemloft.net, edumazet@google.com,
	pabeni@redhat.com, horms@kernel.org, richardcochran@gmail.com,
	imx@lists.linux.dev, netdev@vger.kernel.org,
	linux-kernel@vger.kernel.org, linuxppc-dev@lists.ozlabs.org,
	linux-arm-kernel@lists.infradead.org

> On Mon, 31 Aug 2026 16:23:27 +0800 wei.fang@oss.nxp.com wrote:
> > Subject: [PATCH v3 net-next 0/8] net: dsa: netc: add PTP support for NETC
> switch
> 
> Please be advised that the limit of 15 patches applies across
> the series. You posted 2 large series to net-next at once.
> 
> See "Limit patches outstanding on mailing list" in the docs.
> If this happens again we'll be forced to start discarding your patches.

Oh, sorry, I was not aware of the rule has been updated. I will repost
this series when the ENETC VF series is applied to net-next tree, thanks.


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

* RE: [PATCH v3 net-next 1/8] ptp: netc: use ioread64_lo_hi/iowrite64_lo_hi for 64-bit register access
       [not found]   ` <20260901082024.E6F931F000E9@smtp.kernel.org>
@ 2026-09-02  1:56     ` Wei Fang (OSS)
  0 siblings, 0 replies; 16+ messages in thread
From: Wei Fang (OSS) @ 2026-09-02  1:56 UTC (permalink / raw)
  To: sashiko-reviews@lists.linux.dev
  Cc: imx@lists.linux.dev, Frank.Li@kernel.org,
	Linux Kernel Network Developers

> > @@ -500,22 +473,19 @@ static void netc_timer_handle_etts_event(struct
> netc_timer *priv, int index,
> >  					 bool update_event)
> >  {
> >  	struct ptp_clock_event event;
> > -	u32 etts_l = 0, etts_h = 0;
> > +	u64 etts = 0;
> >
> > -	while (netc_timer_rd(priv, NETC_TMR_STAT) & TMR_STAT_ETS_VLD(index))
> {
> > -		etts_l = netc_timer_rd(priv, NETC_TMR_ETTS_L(index));
> > -		etts_h = netc_timer_rd(priv, NETC_TMR_ETTS_H(index));
> > -	}
> > +	while (netc_timer_rd(priv, NETC_TMR_STAT) & TMR_STAT_ETS_VLD(index))
> > +		etts = netc_timer_rd64(priv, NETC_TMR_ETTS_L(index));
> >
> >  	/* Invalid time stamp */
> > -	if (!etts_l && !etts_h)
> > +	if (!etts)
> >  		return;
> >
> >  	if (update_event) {
> >  		event.type = PTP_CLOCK_EXTTS;
> >  		event.index = index;
> > -		event.timestamp = (u64)etts_h << 32;
> > -		event.timestamp |= etts_l;
> > +		event.timestamp = etts;
> >  		ptp_clock_event(priv->clock, &event);
> >  	}
> >  }
> 
> [Severity: High]
> This is a pre-existing issue, but does this loop discard all but the last
> timestamp from the hardware FIFO?
> 
> The while loop successfully drains multiple timestamps from the TMR_ETTS
> FIFO
> if they accumulate before the interrupt is handled. However, because etts is
> overwritten on each iteration and ptp_clock_event() is only called once
> after the loop completes, it appears any prior external events in the batch
> are silently lost.
> 

This is a false positive, the intention is to get the last timestamp.


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

* RE: [PATCH v3 net-next 3/8] ptp: netc: export netc_timer_get_current_time() for cross-driver use
       [not found]   ` <20260901082025.AF94B1F00A3D@smtp.kernel.org>
@ 2026-09-02  2:02     ` Wei Fang (OSS)
  0 siblings, 0 replies; 16+ messages in thread
From: Wei Fang (OSS) @ 2026-09-02  2:02 UTC (permalink / raw)
  To: sashiko-reviews@lists.linux.dev
  Cc: Frank.Li@kernel.org, imx@lists.linux.dev,
	Linux Kernel Network Developers

> > @@ -1021,5 +1021,55 @@ static struct pci_driver netc_timer_driver = {
> >  };
> >  module_pci_driver(netc_timer_driver);
> >
> > +/**
> > + * netc_timer_get_current_time - read the current PTP time from the NETC
> Timer
> > + * @pdev: PCI device of the NETC Timer
> > + * @ns: The current PTP clock time in nanoseconds, returned to the caller
> > + *
> > + * Reads the 64-bit current time register (TMR_CUR_TIME) from the NETC
> Timer
> > + * device associated with @pdev. Returns an error if the Timer driver has not
> > + * yet probed or has already been removed.
> > + *
> > + * Context: Process context only. Acquires the device mutex via
> device_lock(),
> > + *          which may sleep. Must not be called from atomic context,
> softirq,
> > + *          BH, or while holding a spinlock.
> > + *
> > + * Return: 0 on success, otherwise a negative error code.
> > + */
> > +int netc_timer_get_current_time(struct pci_dev *pdev, u64 *ns)
> > +{
> > +	struct device *dev = &pdev->dev;
> > +	struct netc_timer *priv;
> > +	unsigned long flags;
> > +	int err = 0;
> > +
> > +	/* Serialize against driver unbind: the remove() callback runs under
> > +	 * the device lock, so holding it here ensures that priv remains valid
> > +	 * for the entire duration of the register read.
> > +	 */
> > +	device_lock(dev);
> 
> [Severity: High]
> Does acquiring the timer's device lock here create a circular lock dependency
> with the switch's remove path?
> 
> When the switch driver unbinds, it acquires the switch device's dev->mutex,
> then rtnl_lock() via dsa_unregister_switch(), and then onestep->work_lock
> via netc_port_disable().
> 
> In the datapath workqueue, netc_onestep_work() acquires
> onestep->work_lock,
> and calls this function, which acquires the timer's dev->mutex.
> 
> Because lockdep tracks all dev->mutex locks initialized by device_initialize()
> as a single lock class, wouldn't this form a dev->mutex -> work_lock ->
> dev->mutex circular dependency?
> 
> Could we prevent the timer from unbinding while the switch is active by
> creating a device link with device_link_add() instead, which would allow
> lock-free reads?
> 

This is a false positive. The premise that lockdep treats all dev->mutex locks
as a single lock class is incorrect — device_initialize() marks every dev->mutex
as a novalidate class, so device_lock() is deliberately excluded from lockdep's
dependency checking. The dev->mutex -> work_lock -> dev->mutex chain is
never built or reported.

There's also no real deadlock: the two dev->mutex instances are different
objects (the switch's device vs. the timer's device — separate PCIe functions).
The remove path never takes the timer's lock, and the datapath work never
takes the switch's lock, so there's no ABBA ordering.

device_link_add() isn't needed — it would address a non-existent problem
and still wouldn't make the read lock-free. 


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

* RE: [PATCH v3 net-next 7/8] net: dsa: netc: add PTP two-step timestamping support
       [not found]   ` <20260901082026.7F3911F00A3E@smtp.kernel.org>
@ 2026-09-02  2:12     ` Wei Fang (OSS)
  0 siblings, 0 replies; 16+ messages in thread
From: Wei Fang (OSS) @ 2026-09-02  2:12 UTC (permalink / raw)
  To: sashiko-reviews@lists.linux.dev
  Cc: Frank.Li@kernel.org, imx@lists.linux.dev,
	Linux Kernel Network Developers

> > +static void netc_twostep_tstamp_process(struct netc_tag_th_subtype2 *tag,
> > +					struct sk_buff *skb)
> > +{
> > +	u8 ts_req_id = FIELD_GET(NETC_TAG_TS_REQ_ID, tag->hr_tsreq_id);
> > +	struct dsa_port *dp = dsa_user_to_port(skb->dev);
> > +	u64 ts = get_unaligned_be64(&tag->timestamp);
> > +	struct netc_tagger_data *tagger_data;
> > +	struct dsa_switch *ds = dp->ds;
> > +
> > +	tagger_data = ds->tagger_data;
> > +	if (unlikely(!tagger_data->twostep_tstamp_handler))
> 
> [Severity: High]
> Can tagger_data be NULL here, leading to a kernel panic?
> 
> In the DSA teardown sequence (dsa_tree_teardown()),
> dsa_switch_teardown_tag_protocol() is called first, invoking
> netc_disconnect() which frees ds->tagger_data and sets it to NULL.
> 
> However, the conduit device's rx_handler is not removed until
> dsa_tree_teardown_conduits() is called later. If a hardware-generated To_Host
> subtype 2 response arrives in this window, the conduit's rx_handler will still
> invoke netc_rcv(), eventually calling netc_twostep_tstamp_process().
> This would result in a NULL pointer dereference when
> evaluating !tagger_data->twostep_tstamp_handler.
> 
> Note that a follow-up patch (8/8) introduces a similar unchecked dereference
> in netc_onestep_sync_enqueue(), so this issue might still be present at the end
> of the series.

False positive — the teardown ordering is reversed.

In dsa_tree_teardown(), the conduit is torn down before the switch.
netc_disconnect() (which frees tagger_data and sets it NULL) runs during
switch teardown, which happens after the conduit is already shut down.

DSA RX into the tagger is gated by the conduit's dsa_ptr. Conduit teardown
clears dsa_ptr (with a barrier), so no further frames reach netc_rcv() from
that point on. By the time tagger_data is freed, the RX path is already stopped,
so the window you describe ("tagger_data NULL but frames still arriving") never
occurs. No NULL dereference is possible, and the same ordering covers the 8/8
case too. No check needed.


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

* RE: [PATCH v3 net-next 8/8] net: dsa: netc: add PTP one-step timestamping support
       [not found]   ` <20260901082027.461281F00A3F@smtp.kernel.org>
@ 2026-09-02  3:00     ` Wei Fang (OSS)
  2026-09-02  3:05     ` Wei Fang (OSS)
  1 sibling, 0 replies; 16+ messages in thread
From: Wei Fang (OSS) @ 2026-09-02  3:00 UTC (permalink / raw)
  To: sashiko-reviews@lists.linux.dev
  Cc: imx@lists.linux.dev, Frank.Li@kernel.org, Netdev

> diff --git a/drivers/net/dsa/netc/netc_ptp.c b/drivers/net/dsa/netc/netc_ptp.c
> --- a/drivers/net/dsa/netc/netc_ptp.c
> +++ b/drivers/net/dsa/netc/netc_ptp.c
> [ ... ]
> > +static void netc_onestep_destroy_work(struct work_struct *work) {
> > +   struct netc_onestep *onestep = container_of(work, struct netc_onestep,
> > +                                               destroy_work);
> > +
> > +   /* refcnt reaching zero does not by itself mean onestep->work has
> > +    * stopped: the last in-flight skb destructor calls schedule_work(&work)
> > +    * *before* the netc_onestep_put() that drops the final reference, so at
> > +    * the moment refcnt hits zero onestep->work may still be pending or
> > +    * running on another CPU. destroy_work and work are distinct
> work_structs
> > +    * and can run concurrently, so cancel_work_sync() is required to drain
> > +    * onestep->work before mutex_destroy()/kfree() below, otherwise a
> > +    * still-running work would touch freed memory. No new
> schedule_work(&work)
> > +    * can occur after this point because no references remain, so this
> > +    * cancel is final.
> > +    */
> > +   cancel_work_sync(&onestep->work);
> > +   netc_port_purge_onestep_queue(onestep, true);
> > +   mutex_destroy(&onestep->work_lock);
> > +   kfree(onestep);
> > +   module_put(THIS_MODULE);
> > +}
>
> [Severity: High]
> Can calling module_put(THIS_MODULE) at the end of an asynchronous work
> item lead to a use-after-free panic?
>
> If this drops the final module reference, a concurrent rmmod could proceed to
> unload the module and free the module text memory. The worker thread,
> which is still running and needs to return to the workqueue core, would then
> execute unmapped memory.
>

Acknowledged as a theoretical race, but not worth addressing. Triggering it
requires an extremely precise interleaving of rmmod with the async
destroy_work's final module_put, and module unload is not a routine
operation in production. The practical risk is negligible, so adding
synchronization to close this window would add complexity for no
meaningful benefit.

> [ ... ]
> > +static void netc_onestep_skb_destructor(struct sk_buff *skb) {
> > +   struct netc_onestep *onestep = skb_shinfo(skb)->destructor_arg;
> > +
> > +   /* skb has been transmitted by hardware. Schedule work to send the next
> > +    * queued one-step Sync packet, then release this skb's reference on the
> > +    * context. If the port has already been torn down and this is the last
> > +    * reference, the context is freed via netc_onestep_release().
> > +    */
> > +   schedule_work(&onestep->work);
> > +   netc_onestep_put(onestep);
> > +}
>
> [Severity: High]
> Is it safe to use the skb destructor as a proxy for the switch hardware's
> transmission completion?
>
> The skb destructor fires when the host NIC completes DMA to the switch,
> which happens before the switch MAC transmits the frame onto the wire. If
> the switch egress queue is not empty, the first frame is still buffered inside the
> switch when the destructor runs.
>
> This would wake up the workqueue, which might then program the
> PM_SINGLE_STEP register for a second frame. By the time the first frame
> finally leaves the switch MAC, it could use the wrong register values,
> potentially corrupting the PTP timestamps or UDP checksums on the wire.
>

This is an inherent hardware limitation (PM_SINGLE_STEP is single-frame
and the conduit TX completion is decoupled from the switch egress MAC),
not something software can fully eliminate. In practice the window is closed
by three effects:

(1) the host NIC reclaims TX BDs via NAPI, so the destructor fires well after
the frame has been DMA'd into the switch, not immediately;
(2) the switch driver serializes via the workqueue and only schedules the
next frame after the current one's TX completion, adding further delay
before PM_SINGLE_STEP is reprogrammed;
(3) PTP Sync is low-rate, so overlap is highly unlikely.

The interval from "frame handed to the switch" to "PM_SINGLE_STEP
reprogrammed" is more than enough for the switch to transmit the small
frame. Even in the extremely rare case it does occur, only that one Sync
frame is corrupted; the receiver drops it on checksum failure and PTP
sync recovers on the next cycle, so clock synchronization is not affected.

> [ ... ]
> > +static int netc_port_onestep_alloc(struct netc_port *np) {
> > +   struct netc_onestep *onestep;
> > +   int err;
> > +
> > +   /* Hold a module reference until the last in-flight one-step Sync skb
> > +    * is freed by the conduit. Without this, the module could be unloaded
> > +    * before netc_onestep_skb_destructor() returns, causing a panic.
> > +    * Released in netc_onestep_destroy_work() after all cleanup is done.
> > +    */
> > +   if (!try_module_get(THIS_MODULE)) {
> > +           dev_err(np->switch_priv->dev,
> > +                   "Failed to get the driver module\n");
> > +           return -ENODEV;
> > +   }
> > +
>
> [Severity: High]
> Does this permanently pin the module in memory?
>
> Since this is called during port initialization, acquiring a module reference here
> will make the reference count non-zero for the lifetime of the port. As a result,
> the module can never be unloaded via rmmod, because rmmod requires the
> module reference count to be zero before it will unbind the driver and tear
> down the ports.
>
> If the goal is only to prevent the module from unloading while packets are
> in-flight, would it be better to acquire the reference dynamically when the
> packet is queued, rather than globally at initialization?
>

The analysis doesn't hold. This reference only blocks a direct rmmod; it does
not block driver unbind or port teardown. The module can still be unloaded
normally with unbind first, then rmmod: unbind releases the reference during
teardown, so by the time rmmod runs the refcount is back to zero and it
succeeds. This is intended behavior, not a bug — no change needed.


NXP Confidential

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

* RE: [PATCH v3 net-next 8/8] net: dsa: netc: add PTP one-step timestamping support
       [not found]   ` <20260901082027.461281F00A3F@smtp.kernel.org>
  2026-09-02  3:00     ` Wei Fang (OSS)
@ 2026-09-02  3:05     ` Wei Fang (OSS)
  1 sibling, 0 replies; 16+ messages in thread
From: Wei Fang (OSS) @ 2026-09-02  3:05 UTC (permalink / raw)
  To: sashiko-reviews@lists.linux.dev
  Cc: imx@lists.linux.dev, Frank.Li@kernel.org,
	Linux Kernel Network Developers

Remove NXP label,sorry for the noise.

> > +static void netc_onestep_destroy_work(struct work_struct *work) {
> > +	struct netc_onestep *onestep = container_of(work, struct netc_onestep,
> > +						    destroy_work);
> > +
> > +	/* refcnt reaching zero does not by itself mean onestep->work has
> > +	 * stopped: the last in-flight skb destructor calls schedule_work(&work)
> > +	 * *before* the netc_onestep_put() that drops the final reference, so at
> > +	 * the moment refcnt hits zero onestep->work may still be pending or
> > +	 * running on another CPU. destroy_work and work are distinct
> work_structs
> > +	 * and can run concurrently, so cancel_work_sync() is required to drain
> > +	 * onestep->work before mutex_destroy()/kfree() below, otherwise a
> > +	 * still-running work would touch freed memory. No new
> schedule_work(&work)
> > +	 * can occur after this point because no references remain, so this
> > +	 * cancel is final.
> > +	 */
> > +	cancel_work_sync(&onestep->work);
> > +	netc_port_purge_onestep_queue(onestep, true);
> > +	mutex_destroy(&onestep->work_lock);
> > +	kfree(onestep);
> > +	module_put(THIS_MODULE);
> > +}
> 
> [Severity: High]
> Can calling module_put(THIS_MODULE) at the end of an asynchronous work
> item lead to a use-after-free panic?
> 
> If this drops the final module reference, a concurrent rmmod could proceed to
> unload the module and free the module text memory. The worker thread,
> which is still running and needs to return to the workqueue core, would then
> execute unmapped memory.
> 

Acknowledged as a theoretical race, but not worth addressing. Triggering it
requires an extremely precise interleaving of rmmod with the async
destroy_work's final module_put, and module unload is not a routine
operation in production. The practical risk is negligible, so adding
synchronization to close this window would add complexity for no
meaningful benefit.

> [ ... ]
> > +static void netc_onestep_skb_destructor(struct sk_buff *skb) {
> > +	struct netc_onestep *onestep = skb_shinfo(skb)->destructor_arg;
> > +
> > +	/* skb has been transmitted by hardware. Schedule work to send the next
> > +	 * queued one-step Sync packet, then release this skb's reference on the
> > +	 * context. If the port has already been torn down and this is the last
> > +	 * reference, the context is freed via netc_onestep_release().
> > +	 */
> > +	schedule_work(&onestep->work);
> > +	netc_onestep_put(onestep);
> > +}
> 
> [Severity: High]
> Is it safe to use the skb destructor as a proxy for the switch hardware's
> transmission completion?
> 
> The skb destructor fires when the host NIC completes DMA to the switch,
> which happens before the switch MAC transmits the frame onto the wire. If
> the switch egress queue is not empty, the first frame is still buffered inside the
> switch when the destructor runs.
> 
> This would wake up the workqueue, which might then program the
> PM_SINGLE_STEP register for a second frame. By the time the first frame
> finally leaves the switch MAC, it could use the wrong register values,
> potentially corrupting the PTP timestamps or UDP checksums on the wire.
> 

This is an inherent hardware limitation (PM_SINGLE_STEP is single-frame
and the conduit TX completion is decoupled from the switch egress MAC),
not something software can fully eliminate. In practice the window is closed
by three effects:

(1) the host NIC reclaims TX BDs via NAPI, so the destructor fires well after
the frame has been DMA'd into the switch, not immediately;
(2) the switch driver serializes via the workqueue and only schedules the
next frame after the current one's TX completion, adding further delay
before PM_SINGLE_STEP is reprogrammed;
(3) PTP Sync is low-rate, so overlap is highly unlikely.

The interval from "frame handed to the switch" to "PM_SINGLE_STEP
reprogrammed" is more than enough for the switch to transmit the small
frame. Even in the extremely rare case it does occur, only that one Sync
frame is corrupted; the receiver drops it on checksum failure and PTP
sync recovers on the next cycle, so clock synchronization is not affected.

> [ ... ]
> > +static int netc_port_onestep_alloc(struct netc_port *np) {
> > +	struct netc_onestep *onestep;
> > +	int err;
> > +
> > +	/* Hold a module reference until the last in-flight one-step Sync skb
> > +	 * is freed by the conduit. Without this, the module could be unloaded
> > +	 * before netc_onestep_skb_destructor() returns, causing a panic.
> > +	 * Released in netc_onestep_destroy_work() after all cleanup is done.
> > +	 */
> > +	if (!try_module_get(THIS_MODULE)) {
> > +		dev_err(np->switch_priv->dev,
> > +			"Failed to get the driver module\n");
> > +		return -ENODEV;
> > +	}
> > +
> 
> [Severity: High]
> Does this permanently pin the module in memory?
> 
> Since this is called during port initialization, acquiring a module reference here
> will make the reference count non-zero for the lifetime of the port. As a result,
> the module can never be unloaded via rmmod, because rmmod requires the
> module reference count to be zero before it will unbind the driver and tear
> down the ports.
> 
> If the goal is only to prevent the module from unloading while packets are
> in-flight, would it be better to acquire the reference dynamically when the
> packet is queued, rather than globally at initialization?
> 

The analysis doesn't hold. This reference only blocks a direct rmmod; it does
not block driver unbind or port teardown. The module can still be unloaded
normally with unbind first, then rmmod: unbind releases the reference during
teardown, so by the time rmmod runs the refcount is back to zero and it
succeeds. This is intended behavior, not a bug — no change needed.


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

end of thread, other threads:[~2026-09-02  3:05 UTC | newest]

Thread overview: 16+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-31  8:23 [PATCH v3 net-next 0/8] net: dsa: netc: add PTP support for NETC switch wei.fang
2026-08-31  8:23 ` [PATCH v3 net-next 1/8] ptp: netc: use ioread64_lo_hi/iowrite64_lo_hi for 64-bit register access wei.fang
     [not found]   ` <20260901082024.E6F931F000E9@smtp.kernel.org>
2026-09-02  1:56     ` Wei Fang (OSS)
2026-08-31  8:23 ` [PATCH v3 net-next 2/8] ptp: netc: remove unnecessary pcie_flr() call in probe wei.fang
2026-08-31  8:23 ` [PATCH v3 net-next 3/8] ptp: netc: export netc_timer_get_current_time() for cross-driver use wei.fang
     [not found]   ` <20260901082025.AF94B1F00A3D@smtp.kernel.org>
2026-09-02  2:02     ` Wei Fang (OSS)
2026-08-31  8:23 ` [PATCH v3 net-next 4/8] net: dsa: netc: use entry ID instead of pointer to track host flood rule wei.fang
2026-08-31  8:23 ` [PATCH v3 net-next 5/8] net: dsa: netc: check return value of ntmp_ipft_delete_entry() wei.fang
2026-08-31  8:23 ` [PATCH v3 net-next 6/8] net: dsa: netc: enable ingress port filtering lookup by default wei.fang
2026-08-31  8:23 ` [PATCH v3 net-next 7/8] net: dsa: netc: add PTP two-step timestamping support wei.fang
     [not found]   ` <20260901082026.7F3911F00A3E@smtp.kernel.org>
2026-09-02  2:12     ` Wei Fang (OSS)
2026-08-31  8:23 ` [PATCH v3 net-next 8/8] net: dsa: netc: add PTP one-step " wei.fang
     [not found]   ` <20260901082027.461281F00A3F@smtp.kernel.org>
2026-09-02  3:00     ` Wei Fang (OSS)
2026-09-02  3:05     ` Wei Fang (OSS)
2026-09-01 15:09 ` [PATCH v3 net-next 0/8] net: dsa: netc: add PTP support for NETC switch Jakub Kicinski
2026-09-02  1:43   ` Wei Fang

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