AI Trajectory


Trajectory: (Expand/Collapse All)
Seq Timestamp Type Name Duration
0/0 2026/08/07 11:52 flow patch-triage
2m Args:
null

Results:
{
  "EnableConfigs": [
    "ZL3073X_I2C",
    "ZL3073X_SPI",
    "PTP_1588_CLOCK"
  ],
  "FocusSymbols": [
    "zl3073x_chan_tod_ready_wait",
    "zl3073x_chan_tod_read",
    "zl3073x_chan_tod_write",
    "zl3073x_chan_tod_adjust",
    "zl3073x_chan_df_offset_set",
    "zl3073x_chan_tie_write",
    "zl3073x_chan_phase_step"
  ],
  "KMSANReasoning": "The patch introduces PTP clock support for the Microchip Azurite DPLL driver. It adds functions to read/write Time of Day (ToD) registers, adjust frequency, and adjust phase, as well as registering a PTP clock device. All local variables, such as `struct timespec64` and `synth_mask`, are fully initialized before use. The `ptp_clock_info` structure is initialized from a static const template, ensuring all fields (including padding and the `name` buffer) are zero-initialized before `snprintf` is called. There are no kernel-to-user info-leaks or uninitialized memory usages introduced by these changes. Any potential memory corruption, out-of-bounds accesses, or use-after-free bugs would be effectively caught by KASAN. Therefore, a dedicated KMSAN fuzzing session is not required.",
  "NeedsKMSAN": false,
  "Reasoning": "The patch adds PTP clock support to the Microchip Azurite DPLL driver (zl3073x). The driver can be instantiated via I2C or SPI, and the PTP clock interface is exposed to userspace via `/dev/ptpX`. Syzkaller can fuzz this functionality using I2C/SPI emulation and PTP ioctls.",
  "WorthFuzzing": true
}

1/1 2026/08/07 11:52 action read-patch-diff
0m Args:
null

Results:
{
  "PatchDiff": "commit 591dec83a85d707455260e918e741257e45f9291\nAuthor: syz-cluster \u003ctriage@syzkaller.com\u003e\nDate:   Fri Aug 7 11:52:46 2026 +0000\n\n    syz-cluster: applied patch under review\n\ndiff --git a/drivers/dpll/zl3073x/Kconfig b/drivers/dpll/zl3073x/Kconfig\nindex 5bbca14005813..da1731ace15f0 100644\n--- a/drivers/dpll/zl3073x/Kconfig\n+++ b/drivers/dpll/zl3073x/Kconfig\n@@ -2,7 +2,7 @@\n \n config ZL3073X\n \ttristate \"Microchip Azurite DPLL/PTP/SyncE devices\" if COMPILE_TEST\n-\tdepends on NET\n+\tdepends on NET \u0026\u0026 PTP_1588_CLOCK\n \tselect DPLL\n \tselect NET_DEVLINK\n \tselect REGMAP\n@@ -16,7 +16,7 @@ config ZL3073X\n \n config ZL3073X_I2C\n \ttristate \"I2C bus implementation for Microchip Azurite devices\"\n-\tdepends on I2C \u0026\u0026 NET\n+\tdepends on I2C \u0026\u0026 NET \u0026\u0026 PTP_1588_CLOCK\n \tselect REGMAP_I2C\n \tselect ZL3073X\n \thelp\n@@ -28,7 +28,7 @@ config ZL3073X_I2C\n \n config ZL3073X_SPI\n \ttristate \"SPI bus implementation for Microchip Azurite devices\"\n-\tdepends on NET \u0026\u0026 SPI\n+\tdepends on NET \u0026\u0026 SPI \u0026\u0026 PTP_1588_CLOCK\n \tselect REGMAP_SPI\n \tselect ZL3073X\n \thelp\ndiff --git a/drivers/dpll/zl3073x/chan.c b/drivers/dpll/zl3073x/chan.c\nindex 4ec2cf53dad46..af6ca43c33590 100644\n--- a/drivers/dpll/zl3073x/chan.c\n+++ b/drivers/dpll/zl3073x/chan.c\n@@ -3,6 +3,7 @@\n #include \u003clinux/cleanup.h\u003e\n #include \u003clinux/delay.h\u003e\n #include \u003clinux/dev_printk.h\u003e\n+#include \u003clinux/ptp_clock_kernel.h\u003e\n #include \u003clinux/string.h\u003e\n #include \u003clinux/types.h\u003e\n \n@@ -162,8 +163,8 @@ int zl3073x_chan_nco_mode_set(struct zl3073x_dev *zldev, u8 index)\n  * @zldev: pointer to zl3073x_dev structure\n  * @index: DPLL channel index to fetch state for\n  *\n- * Reads the mode_refsel register and reference priority registers for\n- * the given DPLL channel and stores the raw values for later use.\n+ * Reads the mode_refsel, status and reference priority registers for\n+ * the given DPLL channel and stores the values for later use.\n  *\n  * Return: 0 on success, \u003c0 on error\n  */\n@@ -234,6 +235,322 @@ const struct zl3073x_chan *zl3073x_chan_state_get(struct zl3073x_dev *zldev,\n \treturn \u0026zldev-\u003echan[index];\n }\n \n+/**\n+ * zl3073x_chan_tod_ready_wait - wait for ToD semaphore to clear\n+ * @zldev: pointer to zl3073x device\n+ * @ch: DPLL channel index\n+ *\n+ * Polls the ToD control register until the semaphore bit is cleared,\n+ * indicating the device has completed the previous ToD operation.\n+ *\n+ * Return: 0 on success, -EBUSY if semaphore not cleared, \u003c0 on error\n+ */\n+int zl3073x_chan_tod_ready_wait(struct zl3073x_dev *zldev, u8 ch)\n+{\n+\tunsigned int timeout;\n+\tu8 tod_ctrl;\n+\tint rc;\n+\n+\trc = zl3073x_read_u8(zldev, ZL_REG_DPLL_TOD_CTRL(ch), \u0026tod_ctrl);\n+\tif (rc)\n+\t\treturn rc;\n+\n+\tswitch (FIELD_GET(ZL_DPLL_TOD_CTRL_CMD, tod_ctrl)) {\n+\tcase 0:\n+\t\t/* No operation pending (power-on/reset default) */\n+\t\treturn 0;\n+\tcase ZL_DPLL_TOD_CTRL_CMD_WR_NEXT_1HZ:\n+\t\ttimeout = ZL_POLL_TOD_WR_TIMEOUT_US;\n+\t\tbreak;\n+\tcase ZL_DPLL_TOD_CTRL_CMD_RD_CURRENT:\n+\tcase ZL_DPLL_TOD_CTRL_CMD_RD_NEXT_1HZ:\n+\t\ttimeout = ZL_POLL_TOD_RD_TIMEOUT_US;\n+\t\tbreak;\n+\tdefault:\n+\t\t/* FW never writes cmd bits; only the driver sets them */\n+\t\tWARN_ON(1);\n+\t\treturn -EOPNOTSUPP;\n+\t}\n+\n+\trc = zl3073x_poll_zero_u8(zldev, ZL_REG_DPLL_TOD_CTRL(ch),\n+\t\t\t\t  ZL_DPLL_TOD_CTRL_SEM, timeout);\n+\n+\treturn rc == -ETIMEDOUT ? -EBUSY : rc;\n+}\n+\n+/**\n+ * zl3073x_chan_tod_ctrl - issue ToD command\n+ * @zldev: pointer to zl3073x device\n+ * @ch: DPLL channel index\n+ * @cmd: ToD command to execute\n+ *\n+ * Writes the semaphore and command to dpll_tod_ctrl. The caller must\n+ * ensure the device is ready (semaphore clear) before calling and\n+ * must wait for completion if needed.\n+ *\n+ * Return: 0 on success, \u003c0 on error\n+ */\n+static int zl3073x_chan_tod_ctrl(struct zl3073x_dev *zldev, u8 ch, u8 cmd)\n+{\n+\treturn zl3073x_write_u8(zldev, ZL_REG_DPLL_TOD_CTRL(ch),\n+\t\t\t\tZL_DPLL_TOD_CTRL_SEM | cmd);\n+}\n+\n+/**\n+ * zl3073x_chan_tod_read - read ToD registers after issuing a command\n+ * @zldev: pointer to zl3073x device\n+ * @ch: DPLL channel index\n+ * @next_hz: if true, read predicted ToD at next 1 Hz; otherwise read current\n+ * @ts: timespec to store the result\n+ * @sts: optional system timestamp pair for cross-timestamping\n+ *\n+ * Context: Caller must serialize all zl3073x_chan_tod_* calls externally.\n+ * Return: 0 on success, \u003c0 on error\n+ */\n+int zl3073x_chan_tod_read(struct zl3073x_dev *zldev, u8 ch,\n+\t\t\t  bool next_hz, struct timespec64 *ts,\n+\t\t\t  struct ptp_system_timestamp *sts)\n+{\n+\tu32 nsec;\n+\tu64 sec;\n+\tu8 cmd;\n+\tint rc;\n+\n+\tif (next_hz)\n+\t\tcmd = ZL_DPLL_TOD_CTRL_CMD_RD_NEXT_1HZ;\n+\telse\n+\t\tcmd = ZL_DPLL_TOD_CTRL_CMD_RD_CURRENT;\n+\n+\t/* Wait for any previous ToD operation to complete */\n+\trc = zl3073x_chan_tod_ready_wait(zldev, ch);\n+\tif (rc)\n+\t\treturn rc;\n+\n+\tptp_read_system_prets(sts);\n+\trc = zl3073x_chan_tod_ctrl(zldev, ch, cmd);\n+\tif (rc)\n+\t\treturn rc;\n+\n+\trc = zl3073x_chan_tod_ready_wait(zldev, ch);\n+\tif (rc)\n+\t\treturn rc;\n+\tptp_read_system_postts(sts);\n+\n+\trc = zl3073x_read_u48(zldev, ZL_REG_DPLL_TOD_SEC(ch), \u0026sec);\n+\tif (rc)\n+\t\treturn rc;\n+\n+\t/* HW nanoseconds are always in [0, NSEC_PER_SEC) range */\n+\trc = zl3073x_read_u32(zldev, ZL_REG_DPLL_TOD_NS(ch), \u0026nsec);\n+\tif (rc)\n+\t\treturn rc;\n+\n+\tts-\u003etv_sec = sec;\n+\tts-\u003etv_nsec = nsec;\n+\n+\treturn 0;\n+}\n+\n+/**\n+ * zl3073x_chan_tod_write - write ToD registers and trigger 1 Hz update\n+ * @zldev: pointer to zl3073x device\n+ * @ch: DPLL channel index\n+ * @ts: time to set\n+ *\n+ * Context: Caller must serialize all zl3073x_chan_tod_* calls externally.\n+ * Return: 0 on success, \u003c0 on error\n+ */\n+int zl3073x_chan_tod_write(struct zl3073x_dev *zldev, u8 ch,\n+\t\t\t   struct timespec64 ts)\n+{\n+\tint rc;\n+\n+\t/* Wait for any previous ToD operation to complete */\n+\trc = zl3073x_chan_tod_ready_wait(zldev, ch);\n+\tif (rc)\n+\t\treturn rc;\n+\n+\trc = zl3073x_write_u48(zldev, ZL_REG_DPLL_TOD_SEC(ch), ts.tv_sec);\n+\tif (rc)\n+\t\treturn rc;\n+\n+\trc = zl3073x_write_u32(zldev, ZL_REG_DPLL_TOD_NS(ch), ts.tv_nsec);\n+\tif (rc)\n+\t\treturn rc;\n+\n+\treturn zl3073x_chan_tod_ctrl(zldev, ch,\n+\t\t\t\t     ZL_DPLL_TOD_CTRL_CMD_WR_NEXT_1HZ);\n+}\n+\n+/**\n+ * zl3073x_chan_tod_adjust - atomic ToD read-modify-write with rollover guard\n+ * @zldev: pointer to zl3073x device\n+ * @ch: DPLL channel index\n+ * @delta: time adjustment to apply\n+ *\n+ * Reads the next-Hz ToD and current ToD, then checks whether enough time\n+ * remains before the next 1 Hz rollover to safely complete the write.\n+ * Re-reads if the 1 Hz tick crossed between the two reads or if less\n+ * than 20 ms remains before the next rollover. Applies @delta and writes\n+ * the result back.\n+ *\n+ * Context: Caller must serialize all zl3073x_chan_tod_* calls externally.\n+ * Return: 0 on success, \u003c0 on error\n+ */\n+#define ZL_TOD_MAX_RETRIES\t20\n+int zl3073x_chan_tod_adjust(struct zl3073x_dev *zldev, u8 ch,\n+\t\t\t    struct timespec64 delta)\n+{\n+\tstatic const long threshold_ns = 20 * NSEC_PER_MSEC;\n+\tstruct timespec64 ts_next, ts_cur, diff;\n+\tint rc, i;\n+\n+\tfor (i = 0; i \u003c ZL_TOD_MAX_RETRIES; i++) {\n+\t\trc = zl3073x_chan_tod_read(zldev, ch, true, \u0026ts_next, NULL);\n+\t\tif (rc)\n+\t\t\treturn rc;\n+\n+\t\trc = zl3073x_chan_tod_read(zldev, ch, false, \u0026ts_cur, NULL);\n+\t\tif (rc)\n+\t\t\treturn rc;\n+\n+\t\t/* Ensure the 1 Hz tick did not cross between the two reads\n+\t\t * and that enough margin remains to complete the write.\n+\t\t */\n+\t\tdiff = timespec64_sub(ts_next, ts_cur);\n+\t\tif (diff.tv_sec \u003e 0 ||\n+\t\t    (!diff.tv_sec \u0026\u0026 diff.tv_nsec \u003e= threshold_ns))\n+\t\t\tbreak;\n+\t}\n+\tif (i == ZL_TOD_MAX_RETRIES) {\n+\t\tdev_warn(zldev-\u003edev,\n+\t\t\t \"DPLL%u ToD adjust failed to get stable margin\\n\",\n+\t\t\t ch);\n+\t\treturn -EBUSY;\n+\t}\n+\n+\t/* Apply delta to the next-Hz ToD */\n+\tts_next = timespec64_add(ts_next, delta);\n+\tif (!timespec64_valid(\u0026ts_next))\n+\t\treturn -EINVAL;\n+\n+\treturn zl3073x_chan_tod_write(zldev, ch, ts_next);\n+}\n+\n+/**\n+ * zl3073x_chan_df_offset_set - write delta frequency offset to hardware\n+ * @zldev: pointer to zl3073x device\n+ * @ch: DPLL channel index\n+ * @offset: frequency offset in 2^-48 steps\n+ *\n+ * Context: Caller must hold the per-DPLL lock.\n+ * Return: 0 on success, \u003c0 on error\n+ */\n+int zl3073x_chan_df_offset_set(struct zl3073x_dev *zldev, u8 ch, s64 offset)\n+{\n+\tint rc;\n+\n+\trc = zl3073x_write_u48(zldev, ZL_REG_DPLL_DF_OFFSET(ch), offset);\n+\tif (!rc)\n+\t\tzldev-\u003echan[ch].df_offset = offset;\n+\n+\treturn rc;\n+}\n+\n+/**\n+ * zl3073x_chan_tie_write - adjust DPLL phase using TIE write\n+ * @zldev: pointer to zl3073x device\n+ * @ch: DPLL channel index\n+ * @delta_ns: phase adjustment in nanoseconds (must be within +-1s)\n+ *\n+ * Converts nanoseconds to TIE units (0.01 ps) and writes TIE data\n+ * to the specified channel.\n+ *\n+ * Return: 0 on success, \u003c0 on error\n+ */\n+int zl3073x_chan_tie_write(struct zl3073x_dev *zldev, u8 ch, s64 delta_ns)\n+{\n+\ts64 tie_data;\n+\tint rc;\n+\n+\tif (WARN_ON(delta_ns \u003c= -NSEC_PER_SEC || delta_ns \u003e= NSEC_PER_SEC))\n+\t\treturn -ERANGE;\n+\n+\tguard(mutex)(\u0026zldev-\u003etie_lock);\n+\n+\t/* Wait for any previous TIE operation to complete */\n+\trc = zl3073x_poll_zero_u8(zldev, ZL_REG_DPLL_TIE_CTRL,\n+\t\t\t\t  ZL_DPLL_TIE_CTRL_OP,\n+\t\t\t\t  ZL_POLL_TIE_WR_TIMEOUT_US);\n+\tif (rc)\n+\t\treturn rc;\n+\n+\t/* Convert ns to TIE units (0.01 ps = 10^-14 s) */\n+\ttie_data = delta_ns * 100000LL;\n+\n+\trc = zl3073x_write_u48(zldev, ZL_REG_DPLL_TIE_DATA(ch), tie_data);\n+\tif (rc)\n+\t\treturn rc;\n+\n+\trc = zl3073x_write_u8(zldev, ZL_REG_DPLL_TIE_CTRL_MASK, BIT(ch));\n+\tif (rc)\n+\t\treturn rc;\n+\n+\treturn zl3073x_write_u8(zldev, ZL_REG_DPLL_TIE_CTRL,\n+\t\t\t\tZL_DPLL_TIE_CTRL_OP_WR);\n+}\n+\n+/**\n+ * zl3073x_chan_phase_step - execute one output phase step operation\n+ * @zldev: pointer to zl3073x device\n+ * @ch: DPLL channel index\n+ * @out_mask: bitmask of outputs to step\n+ * @step_cycles: phase step in synthesizer clock cycles\n+ * @tod_step: also step the ToD counter\n+ *\n+ * All masked outputs must use synthesizers of the same frequency since\n+ * the step value is in synthesizer clock cycles.\n+ *\n+ * Return: 0 on success, \u003c0 on error\n+ */\n+int zl3073x_chan_phase_step(struct zl3073x_dev *zldev, u8 ch,\n+\t\t\t    u16 out_mask, s32 step_cycles,\n+\t\t\t    bool tod_step)\n+{\n+\tu8 ctrl;\n+\tint rc;\n+\n+\tguard(mutex)(\u0026zldev-\u003ephase_step_lock);\n+\n+\t/* Wait for any previous phase step operation to complete */\n+\trc = zl3073x_poll_zero_u8(zldev, ZL_REG_OUTPUT_PHASE_STEP_CTRL,\n+\t\t\t\t  ZL_OUTPUT_PHASE_STEP_CTRL_OP,\n+\t\t\t\t  ZL_POLL_PHASE_STEP_TIMEOUT_US);\n+\tif (rc)\n+\t\treturn rc;\n+\n+\trc = zl3073x_write_u32(zldev, ZL_REG_OUTPUT_PHASE_STEP_DATA,\n+\t\t\t       step_cycles);\n+\tif (rc)\n+\t\treturn rc;\n+\n+\trc = zl3073x_write_u16(zldev, ZL_REG_OUTPUT_PHASE_STEP_MASK, out_mask);\n+\tif (rc)\n+\t\treturn rc;\n+\n+\trc = zl3073x_write_u8(zldev, ZL_REG_OUTPUT_PHASE_STEP_NUMBER, 1);\n+\tif (rc)\n+\t\treturn rc;\n+\n+\tctrl = FIELD_PREP(ZL_OUTPUT_PHASE_STEP_CTRL_DPLL, ch) |\n+\t       FIELD_PREP(ZL_OUTPUT_PHASE_STEP_CTRL_OP,\n+\t\t\t  ZL_OUTPUT_PHASE_STEP_CTRL_OP_WRITE);\n+\tif (tod_step)\n+\t\tctrl |= ZL_OUTPUT_PHASE_STEP_CTRL_TOD_STEP;\n+\n+\treturn zl3073x_write_u8(zldev, ZL_REG_OUTPUT_PHASE_STEP_CTRL, ctrl);\n+}\n+\n /**\n  * zl3073x_chan_state_set - commit DPLL channel state changes to hardware\n  * @zldev: pointer to zl3073x_dev structure\ndiff --git a/drivers/dpll/zl3073x/chan.h b/drivers/dpll/zl3073x/chan.h\nindex dc9c6d95bdee7..99c3b7088f678 100644\n--- a/drivers/dpll/zl3073x/chan.h\n+++ b/drivers/dpll/zl3073x/chan.h\n@@ -5,10 +5,12 @@\n \n #include \u003clinux/bitfield.h\u003e\n #include \u003clinux/stddef.h\u003e\n+#include \u003clinux/time64.h\u003e\n #include \u003clinux/types.h\u003e\n \n #include \"regs.h\"\n \n+struct ptp_system_timestamp;\n struct zl3073x_dev;\n \n /**\n@@ -42,6 +44,21 @@ int zl3073x_chan_state_set(struct zl3073x_dev *zldev, u8 index,\n int zl3073x_chan_state_update(struct zl3073x_dev *zldev, u8 index);\n int zl3073x_chan_nco_mode_set(struct zl3073x_dev *zldev, u8 index);\n \n+int zl3073x_chan_tod_ready_wait(struct zl3073x_dev *zldev, u8 ch);\n+int zl3073x_chan_tod_read(struct zl3073x_dev *zldev, u8 ch,\n+\t\t\t  bool next_hz, struct timespec64 *ts,\n+\t\t\t  struct ptp_system_timestamp *sts);\n+int zl3073x_chan_tod_write(struct zl3073x_dev *zldev, u8 ch,\n+\t\t\t   struct timespec64 ts);\n+int zl3073x_chan_tod_adjust(struct zl3073x_dev *zldev, u8 ch,\n+\t\t\t    struct timespec64 delta);\n+int zl3073x_chan_phase_step(struct zl3073x_dev *zldev, u8 ch,\n+\t\t\t    u16 out_mask, s32 step_cycles, bool tod_step);\n+\n+int zl3073x_chan_df_offset_set(struct zl3073x_dev *zldev, u8 ch, s64 offset);\n+\n+int zl3073x_chan_tie_write(struct zl3073x_dev *zldev, u8 ch, s64 delta_ns);\n+\n /**\n  * zl3073x_chan_df_offset_get - get cached df_offset vs tracked reference\n  * @chan: pointer to channel state\n@@ -200,6 +217,21 @@ static inline bool zl3073x_chan_mode_is_reflock(const struct zl3073x_chan *chan)\n \treturn zl3073x_chan_mode_get(chan) == ZL_DPLL_MODE_REFSEL_MODE_REFLOCK;\n }\n \n+/**\n+ * zl3073x_chan_mode_supports_tie - check if channel mode supports TIE write\n+ * @chan: pointer to channel state\n+ *\n+ * TIE write is supported in AUTO and REFLOCK modes regardless of lock state.\n+ *\n+ * Return: true if TIE write is supported, false otherwise\n+ */\n+static inline bool\n+zl3073x_chan_mode_supports_tie(const struct zl3073x_chan *chan)\n+{\n+\treturn zl3073x_chan_mode_is_auto(chan) ||\n+\t\tzl3073x_chan_mode_is_reflock(chan);\n+}\n+\n /**\n  * zl3073x_chan_is_ho_ready - check if holdover is ready\n  * @chan: pointer to channel state\ndiff --git a/drivers/dpll/zl3073x/core.c b/drivers/dpll/zl3073x/core.c\nindex 5b2d77f2c2288..230df08e27cd1 100644\n--- a/drivers/dpll/zl3073x/core.c\n+++ b/drivers/dpll/zl3073x/core.c\n@@ -322,7 +322,7 @@ int zl3073x_write_u48(struct zl3073x_dev *zldev, unsigned int reg, u64 val)\n int zl3073x_poll_zero_u8(struct zl3073x_dev *zldev, unsigned int reg,\n \t\t\t u8 mask, unsigned int timeout_us)\n {\n-#define ZL_POLL_SLEEP_US 10\n+\tunsigned int sleep_us = timeout_us / 50;\n \tunsigned int val;\n \n \t/* Check the register is 8bit */\n@@ -336,7 +336,7 @@ int zl3073x_poll_zero_u8(struct zl3073x_dev *zldev, unsigned int reg,\n \treg = ZL_REG_ADDR(reg) + ZL_RANGE_OFFSET;\n \n \treturn regmap_read_poll_timeout(zldev-\u003eregmap, reg, val, !(val \u0026 mask),\n-\t\t\t\t\tZL_POLL_SLEEP_US, timeout_us);\n+\t\t\t\t\tsleep_us, timeout_us);\n }\n \n int zl3073x_mb_op(struct zl3073x_dev *zldev, unsigned int op_reg, u8 op_val,\n@@ -511,6 +511,11 @@ zl3073x_dev_state_fetch(struct zl3073x_dev *zldev)\n \tint rc;\n \tu8 i;\n \n+\trc = zl3073x_read_u16(zldev, ZL_REG_OUTPUT_STEP_TIME_MASK,\n+\t\t\t      \u0026zldev-\u003eout_step_time_mask);\n+\tif (rc)\n+\t\treturn rc;\n+\n \tfor (i = 0; i \u003c ZL3073X_NUM_REFS; i++) {\n \t\trc = zl3073x_ref_state_fetch(zldev, i);\n \t\tif (rc) {\n@@ -1034,6 +1039,14 @@ int zl3073x_dev_probe(struct zl3073x_dev *zldev)\n \t * and/or polls are required to be done atomically.\n \t */\n \trc = devm_mutex_init(zldev-\u003edev, \u0026zldev-\u003emultiop_lock);\n+\tif (rc)\n+\t\treturn dev_err_probe(zldev-\u003edev, rc,\n+\t\t\t\t     \"Failed to initialize mutex\\n\");\n+\trc = devm_mutex_init(zldev-\u003edev, \u0026zldev-\u003ephase_step_lock);\n+\tif (rc)\n+\t\treturn dev_err_probe(zldev-\u003edev, rc,\n+\t\t\t\t     \"Failed to initialize mutex\\n\");\n+\trc = devm_mutex_init(zldev-\u003edev, \u0026zldev-\u003etie_lock);\n \tif (rc)\n \t\treturn dev_err_probe(zldev-\u003edev, rc,\n \t\t\t\t     \"Failed to initialize mutex\\n\");\ndiff --git a/drivers/dpll/zl3073x/core.h b/drivers/dpll/zl3073x/core.h\nindex 78dc208f3eea2..67c10e2595118 100644\n--- a/drivers/dpll/zl3073x/core.h\n+++ b/drivers/dpll/zl3073x/core.h\n@@ -26,6 +26,10 @@ struct zl3073x_dpll;\n #define ZL_POLL_HWREG_TIMEOUT_US\t(50 * USEC_PER_MSEC)\n #define ZL_POLL_MB_TIMEOUT_US\t\t(30 * USEC_PER_MSEC)\n #define ZL_POLL_PHASE_ERR_TIMEOUT_US\t(50 * USEC_PER_MSEC)\n+#define ZL_POLL_PHASE_STEP_TIMEOUT_US\t(3000 * USEC_PER_MSEC)\n+#define ZL_POLL_TIE_WR_TIMEOUT_US\t(1000 * USEC_PER_MSEC)\n+#define ZL_POLL_TOD_RD_TIMEOUT_US\t(30 * USEC_PER_MSEC)\n+#define ZL_POLL_TOD_WR_TIMEOUT_US\t(1000 * USEC_PER_MSEC)\n \n enum zl3073x_flags {\n \tZL3073X_FLAG_REF_PHASE_COMP_32_BIT,\n@@ -55,6 +59,8 @@ struct zl3073x_chip_info {\n  * @regmap: regmap to access device registers\n  * @info: detected chip info\n  * @multiop_lock: to serialize multiple register operations\n+ * @tie_lock: to serialize TIE write operations\n+ * @phase_step_lock: to serialize output phase step operations\n  * @ref: array of input references' invariants\n  * @out: array of outs' invariants\n  * @synth: array of synths' invariants\n@@ -63,6 +69,7 @@ struct zl3073x_chip_info {\n  * @kworker: thread for periodic work\n  * @work: periodic work\n  * @clock_id: clock id of the device\n+ * @out_step_time_mask: output step-time mask (device-global)\n  * @phase_avg_factor: phase offset measurement averaging factor\n  * @freq_monitor: is frequency monitor enabled\n  */\n@@ -71,6 +78,8 @@ struct zl3073x_dev {\n \tstruct regmap\t\t\t*regmap;\n \tconst struct zl3073x_chip_info\t*info;\n \tstruct mutex\t\t\tmultiop_lock;\n+\tstruct mutex\t\t\ttie_lock;\n+\tstruct mutex\t\t\tphase_step_lock;\n \n \t/* Invariants */\n \tstruct zl3073x_ref\tref[ZL3073X_NUM_REFS];\n@@ -87,6 +96,7 @@ struct zl3073x_dev {\n \n \t/* Per-chip parameters */\n \tu64\t\t\tclock_id;\n+\tu16\t\t\tout_step_time_mask;\n \tu8\t\t\tphase_avg_factor;\n \tbool\t\t\tfreq_monitor;\n };\n@@ -308,6 +318,19 @@ zl3073x_dev_out_is_enabled(struct zl3073x_dev *zldev, u8 index)\n \treturn zl3073x_synth_is_enabled(synth) \u0026\u0026 zl3073x_out_is_enabled(out);\n }\n \n+/**\n+ * zl3073x_dev_out_is_stepped - check if output is in step-time mask\n+ * @zldev: pointer to zl3073x device\n+ * @index: output index\n+ *\n+ * Return: true if output is affected by step-time operations\n+ */\n+static inline bool\n+zl3073x_dev_out_is_stepped(struct zl3073x_dev *zldev, u8 index)\n+{\n+\treturn !!(zldev-\u003eout_step_time_mask \u0026 BIT(index));\n+}\n+\n /**\n  * zl3073x_dev_out_dpll_get - get DPLL ID the output is driven by\n  * @zldev: pointer to zl3073x device\ndiff --git a/drivers/dpll/zl3073x/dpll.c b/drivers/dpll/zl3073x/dpll.c\nindex 0488ae6ac486c..a686dd52bfac9 100644\n--- a/drivers/dpll/zl3073x/dpll.c\n+++ b/drivers/dpll/zl3073x/dpll.c\n@@ -2,6 +2,7 @@\n \n #include \u003clinux/bits.h\u003e\n #include \u003clinux/bitfield.h\u003e\n+#include \u003clinux/cleanup.h\u003e\n #include \u003clinux/bug.h\u003e\n #include \u003clinux/container_of.h\u003e\n #include \u003clinux/dev_printk.h\u003e\n@@ -13,6 +14,7 @@\n #include \u003clinux/netlink.h\u003e\n #include \u003clinux/platform_device.h\u003e\n #include \u003clinux/property.h\u003e\n+#include \u003clinux/ptp_clock_kernel.h\u003e\n #include \u003clinux/slab.h\u003e\n #include \u003clinux/sprintf.h\u003e\n \n@@ -2275,45 +2277,375 @@ zl3073x_dpll_init_fine_phase_adjust(struct zl3073x_dev *zldev)\n \treturn zl3073x_write_u8(zldev, ZL_REG_SYNTH_PHASE_SHIFT_CTRL, 0x01);\n }\n \n+/* Maximum frequency adjustment: +-1% of nominal in ppb */\n+#define ZL3073X_DPLL_PTP_MAX_ADJ\t10000000\n+\n /**\n- * zl3073x_dpll_alloc - allocate DPLL device\n- * @zldev: pointer to zl3073x device\n- * @ch: DPLL channel number\n+ * zl3073x_dpll_ptp_gettimex64 - read current time from ToD counters\n+ * @info: PTP clock info\n+ * @ts: timespec to store current time\n+ * @sts: optional system timestamp pair for cross-timestamping\n  *\n- * Allocates DPLL device structure for given DPLL channel.\n+ * Return: 0 on success, \u003c0 on error\n+ */\n+static int zl3073x_dpll_ptp_gettimex64(struct ptp_clock_info *info,\n+\t\t\t\t       struct timespec64 *ts,\n+\t\t\t\t       struct ptp_system_timestamp *sts)\n+{\n+\tstruct zl3073x_dpll *zldpll = container_of(info, struct zl3073x_dpll,\n+\t\t\t\t\t\t   ptp_info);\n+\n+\tguard(mutex)(\u0026zldpll-\u003elock);\n+\n+\treturn zl3073x_chan_tod_read(zldpll-\u003edev, zldpll-\u003eid, false, ts, sts);\n+}\n+\n+/**\n+ * zl3073x_dpll_ptp_settime64 - set ToD counters to given time\n+ * @info: PTP clock info\n+ * @ts: timespec with time to set\n  *\n- * Return: pointer to DPLL device on success, error pointer on error\n+ * Return: 0 on success, \u003c0 on error\n  */\n-struct zl3073x_dpll *\n-zl3073x_dpll_alloc(struct zl3073x_dev *zldev, u8 ch)\n+static int zl3073x_dpll_ptp_settime64(struct ptp_clock_info *info,\n+\t\t\t\t      const struct timespec64 *ts)\n {\n-\tstruct zl3073x_dpll *zldpll;\n+\tstruct zl3073x_dpll *zldpll = container_of(info, struct zl3073x_dpll,\n+\t\t\t\t\t\t   ptp_info);\n \n-\tzldpll = kzalloc_obj(*zldpll);\n-\tif (!zldpll)\n-\t\treturn ERR_PTR(-ENOMEM);\n+\tguard(mutex)(\u0026zldpll-\u003elock);\n \n-\tzldpll-\u003edev = zldev;\n-\tzldpll-\u003eid = ch;\n-\tmutex_init(\u0026zldpll-\u003elock);\n-\tINIT_LIST_HEAD(\u0026zldpll-\u003epins);\n+\treturn zl3073x_chan_tod_write(zldpll-\u003edev, zldpll-\u003eid, *ts);\n+}\n \n-\treturn zldpll;\n+/**\n+ * zl3073x_dpll_ptp_adjtime_phase_step - adjust sub-second time via phase step\n+ * @zldpll: DPLL channel\n+ * @delta: time adjustment in nanoseconds (must be within (-NSEC_PER_SEC,\n+ *         NSEC_PER_SEC))\n+ *\n+ * Uses the output phase step mechanism with tod_step=1 to adjust both\n+ * the output clock phase and the ToD counter simultaneously. This keeps\n+ * outputs and ToD coherent. Only valid for NCO.\n+ *\n+ * Outputs are grouped by synthesizer since the phase step value is in\n+ * synthesizer clock cycles. The first synth group with enabled outputs\n+ * uses tod_step to adjust both outputs and the ToD counter. Remaining\n+ * groups step outputs only. If no synth has enabled outputs, the ToD\n+ * counter is stepped alone using an empty output mask (the FW uses\n+ * the first enabled synth's period for the conversion).\n+ *\n+ * Return: 0 on success, -EOPNOTSUPP if no synths available, \u003c0 on error\n+ */\n+static int zl3073x_dpll_ptp_adjtime_phase_step(struct zl3073x_dpll *zldpll,\n+\t\t\t\t\t       s64 delta)\n+{\n+\tu16 synth_mask[ZL3073X_NUM_SYNTHS] = {};\n+\tstruct zl3073x_dev *zldev = zldpll-\u003edev;\n+\tconst struct zl3073x_synth *synth;\n+\tstruct zl3073x_dpll_pin *pin;\n+\tu32 first_synth_freq = 0;\n+\tbool tod_stepped = false;\n+\ts32 step_cycles;\n+\tu32 synth_freq;\n+\tint rc;\n+\tu8 i;\n+\n+\t/* Build per-synth output masks from registered output pins */\n+\tlist_for_each_entry(pin, \u0026zldpll-\u003epins, list) {\n+\t\tu8 out_id, synth_id;\n+\n+\t\tif (zl3073x_dpll_is_input_pin(pin))\n+\t\t\tcontinue;\n+\n+\t\tout_id = zl3073x_output_pin_out_get(pin-\u003eid);\n+\n+\t\tif (!zl3073x_dev_out_is_stepped(zldev, out_id))\n+\t\t\tcontinue;\n+\n+\t\tsynth_id = zl3073x_dev_out_synth_get(zldev, out_id);\n+\t\tif (synth_id \u003e= ZL3073X_NUM_SYNTHS) {\n+\t\t\tdev_warn(zldev-\u003edev, \"Unexpected synth id for OUT%u\\n\",\n+\t\t\t\t out_id);\n+\t\t\tcontinue;\n+\t\t}\n+\t\tsynth_mask[synth_id] |= BIT(out_id);\n+\t}\n+\n+\t/* Process each synth group */\n+\tfor (i = 0; i \u003c ZL3073X_NUM_SYNTHS; i++) {\n+\t\tsynth = zl3073x_synth_state_get(zldev, i);\n+\t\tif (!zl3073x_synth_is_enabled(synth) ||\n+\t\t    zl3073x_synth_dpll_get(synth) != zldpll-\u003eid)\n+\t\t\tcontinue;\n+\n+\t\tsynth_freq = zl3073x_synth_freq_get(synth);\n+\n+\t\t/* Remember first enabled synth freq for ToD-only fallback */\n+\t\tif (!first_synth_freq)\n+\t\t\tfirst_synth_freq = synth_freq;\n+\n+\t\tif (!synth_mask[i])\n+\t\t\tcontinue;\n+\n+\t\t/* Safe for s32: max synth freq is 750 MHz */\n+\t\tstep_cycles = div_s64(delta * synth_freq, NSEC_PER_SEC);\n+\n+\t\trc = zl3073x_chan_phase_step(zldev, zldpll-\u003eid,\n+\t\t\t\t\t     synth_mask[i], step_cycles,\n+\t\t\t\t\t     !tod_stepped);\n+\t\tif (rc) {\n+\t\t\tif (tod_stepped) {\n+\t\t\t\tdev_warn(zldev-\u003edev,\n+\t\t\t\t\t \"Partial phase step failure\\n\");\n+\t\t\t\treturn 0;\n+\t\t\t}\n+\t\t\treturn rc;\n+\t\t}\n+\t\ttod_stepped = true;\n+\t}\n+\n+\tif (!first_synth_freq)\n+\t\treturn -EOPNOTSUPP;\n+\n+\t/* No enabled outputs found; step ToD counter only using the\n+\t * first enabled synth's period (empty output mask).\n+\t */\n+\tif (!tod_stepped) {\n+\t\tstep_cycles = div_s64(delta * first_synth_freq, NSEC_PER_SEC);\n+\t\treturn zl3073x_chan_phase_step(zldev, zldpll-\u003eid, 0,\n+\t\t\t\t\t       step_cycles, true);\n+\t}\n+\n+\treturn 0;\n }\n \n /**\n- * zl3073x_dpll_free - free DPLL device\n- * @zldpll: pointer to zl3073x_dpll structure\n+ * zl3073x_dpll_ptp_adjtime - adjust PTP clock time\n+ * @info: PTP clock info\n+ * @delta: time adjustment in nanoseconds\n  *\n- * Deallocates given DPLL device previously allocated by @zl3073x_dpll_alloc.\n+ * For NCO, large deltas (\u003e= 1 second) are split into a ToD\n+ * read-modify-write for the seconds part and an output phase step for\n+ * the sub-second remainder. Sub-second deltas use phase step directly,\n+ * falling back to ToD read-modify-write if phase step is unavailable.\n+ * In AUTO/REFLOCK modes, large deltas are split into ToD\n+ * read-modify-write for seconds and TIE write for the sub-second\n+ * remainder. Sub-second deltas use TIE write directly.\n+ *\n+ * Return: 0 on success, \u003c0 on error\n  */\n-void\n-zl3073x_dpll_free(struct zl3073x_dpll *zldpll)\n+static int zl3073x_dpll_ptp_adjtime(struct ptp_clock_info *info, s64 delta)\n {\n-\tWARN(zldpll-\u003edpll_dev, \"DPLL device is still registered\\n\");\n+\tstruct zl3073x_dpll *zldpll = container_of(info, struct zl3073x_dpll,\n+\t\t\t\t\t\t   ptp_info);\n+\tstruct zl3073x_dev *zldev = zldpll-\u003edev;\n+\tconst struct zl3073x_chan *chan;\n+\tbool sec_adjusted = false;\n+\tstruct timespec64 ts;\n+\tint rc;\n \n-\tmutex_destroy(\u0026zldpll-\u003elock);\n-\tkfree(zldpll);\n+\tif (!delta)\n+\t\treturn 0;\n+\n+\tguard(mutex)(\u0026zldpll-\u003elock);\n+\n+\t/* Modes without phase step or TIE use plain ToD adjust */\n+\tchan = zl3073x_chan_state_get(zldev, zldpll-\u003eid);\n+\tif (!zl3073x_chan_mode_is_nco(chan) \u0026\u0026\n+\t    !zl3073x_chan_mode_supports_tie(chan))\n+\t\treturn zl3073x_chan_tod_adjust(zldev, zldpll-\u003eid,\n+\t\t\t\t\t       ns_to_timespec64(delta));\n+\n+\t/* Split off seconds via ToD read-modify-write so the sub-second\n+\t * remainder can be applied through the output-coherent mechanism\n+\t * (phase step or TIE write).\n+\t */\n+\tif (delta \u003e= NSEC_PER_SEC || delta \u003c= -NSEC_PER_SEC) {\n+\t\ts32 remainder;\n+\n+\t\tts.tv_sec = div_s64_rem(delta, NSEC_PER_SEC, \u0026remainder);\n+\t\tts.tv_nsec = 0;\n+\t\tdelta = remainder;\n+\n+\t\trc = zl3073x_chan_tod_adjust(zldev, zldpll-\u003eid, ts);\n+\t\tif (rc)\n+\t\t\treturn rc;\n+\n+\t\t/* No sub-second remainder, done */\n+\t\tif (!delta)\n+\t\t\treturn 0;\n+\n+\t\t/* Wait for the ToD write to be applied at the 1 Hz edge\n+\t\t * before issuing phase step or TIE write, so the pending\n+\t\t * WR_NEXT_1HZ does not overwrite the sub-second adjustment.\n+\t\t */\n+\t\trc = zl3073x_chan_tod_ready_wait(zldev, zldpll-\u003eid);\n+\t\tif (rc)\n+\t\t\treturn rc;\n+\n+\t\tsec_adjusted = true;\n+\t}\n+\n+\t/* Apply sub-second delta via phase step (NCO) or TIE write */\n+\tif (zl3073x_chan_mode_is_nco(chan)) {\n+\t\trc = zl3073x_dpll_ptp_adjtime_phase_step(zldpll, delta);\n+\t\tif (!rc)\n+\t\t\treturn 0;\n+\t} else {\n+\t\trc = zl3073x_chan_tie_write(zldev, zldpll-\u003eid, delta);\n+\t\tif (!rc)\n+\t\t\treturn 0;\n+\t}\n+\n+\t/* Phase step or TIE write failed, fall back to ToD adjust */\n+\trc = zl3073x_chan_tod_adjust(zldev, zldpll-\u003eid,\n+\t\t\t\t     ns_to_timespec64(delta));\n+\n+\t/* In the unlikely event that both phase step/TIE write and fallback\n+\t * ToD adjust fail after seconds were already committed, return\n+\t * success to prevent the PTP servo from retrying the full delta and\n+\t * applying seconds again. The sub-second residual will self-correct\n+\t * in the next servo cycle.\n+\t */\n+\tif (rc \u0026\u0026 sec_adjusted) {\n+\t\tdev_warn(zldev-\u003edev,\n+\t\t\t \"Sub-second adjustment failed after seconds applied\\n\");\n+\t\treturn 0;\n+\t}\n+\n+\treturn rc;\n+}\n+\n+/**\n+ * zl3073x_dpll_ptp_adjfine - adjust PTP clock frequency\n+ * @info: PTP clock info\n+ * @scaled_ppm: frequency adjustment in scaled ppm (ppm * 2^16)\n+ *\n+ * Only supported for NCO. Writes the delta frequency offset register.\n+ *\n+ * Return:\n+ * * %0\t\t\t- success or @scaled_ppm is zero (no-op)\n+ * * %-EOPNOTSUPP\t- NCO pin is not connected and @scaled_ppm is non-zero\n+ * * negative\t\t- other error\n+ */\n+static int\n+zl3073x_dpll_ptp_adjfine(struct ptp_clock_info *info, long scaled_ppm)\n+{\n+\tstruct zl3073x_dpll *zldpll = container_of(info, struct zl3073x_dpll,\n+\t\t\t\t\t\t   ptp_info);\n+\tconst struct zl3073x_chan *chan;\n+\ts64 offset;\n+\n+\t/* Convert scaled_ppm to df_offset in 2^-48 steps:\n+\t *   df_offset = -(scaled_ppm * 2^32) / 10^6\n+\t *\n+\t * Simplify to avoid overflow:\n+\t *   df_offset = -(scaled_ppm * 2^26) / 5^6\n+\t *   df_offset = -(scaled_ppm * 67108864) / 15625\n+\t */\n+\toffset = -div_s64((s64)scaled_ppm * 67108864LL, 15625);\n+\n+\tguard(mutex)(\u0026zldpll-\u003elock);\n+\n+\tchan = zl3073x_chan_state_get(zldpll-\u003edev, zldpll-\u003eid);\n+\tif (!zl3073x_chan_mode_is_nco(chan))\n+\t\treturn scaled_ppm ? -EOPNOTSUPP : 0;\n+\tif (offset == chan-\u003edf_offset)\n+\t\treturn 0;\n+\n+\treturn zl3073x_chan_df_offset_set(zldpll-\u003edev, zldpll-\u003eid, offset);\n+}\n+\n+/**\n+ * zl3073x_dpll_ptp_adjphase - adjust PTP clock phase\n+ * @info: PTP clock info\n+ * @delta: phase adjustment in nanoseconds\n+ *\n+ * Only supported in AUTO and REFLOCK modes. Uses TIE write for\n+ * nanosecond resolution phase adjustment.\n+ *\n+ * Return:\n+ * * %0\t\t\t- success or @delta is zero (no-op)\n+ * * %-EOPNOTSUPP\t- mode does not support TIE and @delta is non-zero\n+ * * negative\t\t- other error\n+ */\n+static int zl3073x_dpll_ptp_adjphase(struct ptp_clock_info *info, s32 delta)\n+{\n+\tstruct zl3073x_dpll *zldpll = container_of(info, struct zl3073x_dpll,\n+\t\t\t\t\t\t   ptp_info);\n+\tstruct zl3073x_dev *zldev = zldpll-\u003edev;\n+\tconst struct zl3073x_chan *chan;\n+\n+\tif (!delta)\n+\t\treturn 0;\n+\n+\tguard(mutex)(\u0026zldpll-\u003elock);\n+\n+\tchan = zl3073x_chan_state_get(zldev, zldpll-\u003eid);\n+\n+\tif (!zl3073x_chan_mode_supports_tie(chan))\n+\t\treturn -EOPNOTSUPP;\n+\n+\treturn zl3073x_chan_tie_write(zldev, zldpll-\u003eid, delta);\n+}\n+\n+static s32\n+zl3073x_dpll_ptp_getmaxphase(struct ptp_clock_info *info __always_unused)\n+{\n+\t/* HW limits TIE write to +-1 second. Return the constant HW\n+\t * limit and let adjphase handle mode-specific checks.\n+\t */\n+\treturn NSEC_PER_SEC - 1;\n+}\n+\n+static const struct ptp_clock_info zl3073x_dpll_ptp_clock_info = {\n+\t.owner\t\t= THIS_MODULE,\n+\t.max_adj\t= ZL3073X_DPLL_PTP_MAX_ADJ,\n+\t.gettimex64\t= zl3073x_dpll_ptp_gettimex64,\n+\t.settime64\t= zl3073x_dpll_ptp_settime64,\n+\t.adjtime\t= zl3073x_dpll_ptp_adjtime,\n+\t.adjfine\t= zl3073x_dpll_ptp_adjfine,\n+\t.adjphase\t= zl3073x_dpll_ptp_adjphase,\n+\t.getmaxphase\t= zl3073x_dpll_ptp_getmaxphase,\n+};\n+\n+/**\n+ * zl3073x_dpll_ptp_register - register PTP clock for a DPLL channel\n+ * @zldpll: DPLL channel to register PTP clock for\n+ *\n+ * Return: 0 on success, \u003c0 on error\n+ */\n+static int zl3073x_dpll_ptp_register(struct zl3073x_dpll *zldpll)\n+{\n+\tstruct zl3073x_dev *zldev = zldpll-\u003edev;\n+\tstruct ptp_clock *ptp_clock;\n+\n+\tsnprintf(zldpll-\u003eptp_info.name, sizeof(zldpll-\u003eptp_info.name),\n+\t\t \"%s-dpll%u\", dev_name(zldev-\u003edev), zldpll-\u003eid);\n+\n+\tptp_clock = ptp_clock_register(\u0026zldpll-\u003eptp_info, zldev-\u003edev);\n+\tif (IS_ERR(ptp_clock)) {\n+\t\tdev_err(zldev-\u003edev, \"Failed to register PTP clock for DPLL%u\\n\",\n+\t\t\tzldpll-\u003eid);\n+\t\treturn PTR_ERR(ptp_clock);\n+\t}\n+\n+\tzldpll-\u003eptp_clock = ptp_clock;\n+\n+\treturn 0;\n+}\n+\n+/**\n+ * zl3073x_dpll_ptp_unregister - unregister PTP clock for a DPLL channel\n+ * @zldpll: DPLL channel to unregister PTP clock for\n+ */\n+static void zl3073x_dpll_ptp_unregister(struct zl3073x_dpll *zldpll)\n+{\n+\tif (!IS_ERR_OR_NULL(zldpll-\u003eptp_clock)) {\n+\t\tptp_clock_unregister(zldpll-\u003eptp_clock);\n+\t\tzldpll-\u003eptp_clock = NULL;\n+\t}\n }\n \n /**\n@@ -2398,6 +2730,48 @@ zl3073x_dpll_ref_sync_pairs_register(struct zl3073x_dpll *zldpll)\n \treturn 0;\n }\n \n+/**\n+ * zl3073x_dpll_alloc - allocate DPLL device\n+ * @zldev: pointer to zl3073x device\n+ * @ch: DPLL channel number\n+ *\n+ * Allocates DPLL device structure for given DPLL channel.\n+ *\n+ * Return: pointer to DPLL device on success, error pointer on error\n+ */\n+struct zl3073x_dpll *\n+zl3073x_dpll_alloc(struct zl3073x_dev *zldev, u8 ch)\n+{\n+\tstruct zl3073x_dpll *zldpll;\n+\n+\tzldpll = kzalloc_obj(*zldpll);\n+\tif (!zldpll)\n+\t\treturn ERR_PTR(-ENOMEM);\n+\n+\tzldpll-\u003edev = zldev;\n+\tzldpll-\u003eid = ch;\n+\tzldpll-\u003eptp_info = zl3073x_dpll_ptp_clock_info;\n+\tmutex_init(\u0026zldpll-\u003elock);\n+\tINIT_LIST_HEAD(\u0026zldpll-\u003epins);\n+\n+\treturn zldpll;\n+}\n+\n+/**\n+ * zl3073x_dpll_free - free DPLL device\n+ * @zldpll: pointer to zl3073x_dpll structure\n+ *\n+ * Deallocates given DPLL device previously allocated by @zl3073x_dpll_alloc.\n+ */\n+void\n+zl3073x_dpll_free(struct zl3073x_dpll *zldpll)\n+{\n+\tWARN(zldpll-\u003edpll_dev, \"DPLL device is still registered\\n\");\n+\n+\tmutex_destroy(\u0026zldpll-\u003elock);\n+\tkfree(zldpll);\n+}\n+\n /**\n  * zl3073x_dpll_register - register DPLL device and all its pins\n  * @zldpll: pointer to zl3073x_dpll structure\n@@ -2428,6 +2802,13 @@ zl3073x_dpll_register(struct zl3073x_dpll *zldpll)\n \t\treturn rc;\n \t}\n \n+\trc = zl3073x_dpll_ptp_register(zldpll);\n+\tif (rc) {\n+\t\tzl3073x_dpll_pins_unregister(zldpll);\n+\t\tzl3073x_dpll_device_unregister(zldpll);\n+\t\treturn rc;\n+\t}\n+\n \treturn 0;\n }\n \n@@ -2441,7 +2822,7 @@ zl3073x_dpll_register(struct zl3073x_dpll *zldpll)\n void\n zl3073x_dpll_unregister(struct zl3073x_dpll *zldpll)\n {\n-\t/* Unregister all pins and dpll */\n+\tzl3073x_dpll_ptp_unregister(zldpll);\n \tzl3073x_dpll_pins_unregister(zldpll);\n \tzl3073x_dpll_device_unregister(zldpll);\n }\ndiff --git a/drivers/dpll/zl3073x/dpll.h b/drivers/dpll/zl3073x/dpll.h\nindex faebc402ba1b7..993221dc63249 100644\n--- a/drivers/dpll/zl3073x/dpll.h\n+++ b/drivers/dpll/zl3073x/dpll.h\n@@ -5,6 +5,7 @@\n \n #include \u003clinux/dpll.h\u003e\n #include \u003clinux/list.h\u003e\n+#include \u003clinux/ptp_clock_kernel.h\u003e\n \n #include \"core.h\"\n \n@@ -22,6 +23,8 @@\n  * @type: DPLL type (PPS or EEC)\n  * @lock_status: last saved DPLL lock status\n  * @pins: list of pins\n+ * @ptp_info: PTP clock info\n+ * @ptp_clock: registered PTP clock (or NULL)\n  */\n struct zl3073x_dpll {\n \tstruct list_head\t\tlist;\n@@ -36,6 +39,8 @@ struct zl3073x_dpll {\n \tenum dpll_type\t\t\ttype;\n \tenum dpll_lock_status\t\tlock_status;\n \tstruct list_head\t\tpins;\n+\tstruct ptp_clock_info\t\tptp_info;\n+\tstruct ptp_clock\t\t*ptp_clock;\n };\n \n struct zl3073x_dpll *zl3073x_dpll_alloc(struct zl3073x_dev *zldev, u8 ch);\ndiff --git a/drivers/dpll/zl3073x/regs.h b/drivers/dpll/zl3073x/regs.h\nindex b70ead7d4495b..f3a5e1215aa36 100644\n--- a/drivers/dpll/zl3073x/regs.h\n+++ b/drivers/dpll/zl3073x/regs.h\n@@ -179,6 +179,20 @@\n #define ZL_DPLL_DF_READ_CMD\t\t\tGENMASK(2, 0)\n #define ZL_DPLL_DF_READ_CMD_ACC_I\t\t4\n \n+#define ZL_REG_DPLL_TIE_CTRL\t\t\tZL_REG(5, 0x30, 1)\n+#define ZL_DPLL_TIE_CTRL_OP\t\t\tGENMASK(2, 0)\n+#define ZL_DPLL_TIE_CTRL_OP_WR\t\t\t4\n+\n+#define ZL_REG_DPLL_TIE_CTRL_MASK\t\tZL_REG(5, 0x31, 1)\n+\n+#define ZL_REG_DPLL_TOD_CTRL(_idx)\t\t\t\t\t\\\n+\tZL_REG_IDX(_idx, 5, 0x38, 1, ZL3073X_MAX_CHANNELS, 1)\n+#define ZL_DPLL_TOD_CTRL_SEM\t\t\tBIT(4)\n+#define ZL_DPLL_TOD_CTRL_CMD\t\t\tGENMASK(3, 0)\n+#define ZL_DPLL_TOD_CTRL_CMD_WR_NEXT_1HZ\t1\n+#define ZL_DPLL_TOD_CTRL_CMD_RD_CURRENT\t\t8\n+#define ZL_DPLL_TOD_CTRL_CMD_RD_NEXT_1HZ\t9\n+\n #define ZL_REG_DPLL_MEAS_CTRL\t\t\tZL_REG(5, 0x50, 1)\n #define ZL_DPLL_MEAS_CTRL_EN\t\t\tBIT(0)\n #define ZL_DPLL_MEAS_CTRL_AVG_FACTOR\t\tGENMASK(7, 4)\n@@ -193,6 +207,9 @@\n \n /*******************************\n  * Register Pages 6-7, DPLL Data\n+ *\n+ * Per-channel registers with stride 0x20. Channels 0-3 reside on page 6,\n+ * channel 4 on page 7.\n  *******************************/\n \n #define ZL_REG_DPLL_DF_OFFSET_03(_idx)\t\t\t\t\t\\\n@@ -202,6 +219,24 @@\n \t((_idx) \u003c 4 ? ZL_REG_DPLL_DF_OFFSET_03(_idx) : ZL_REG_DPLL_DF_OFFSET_4)\n #define ZL_DPLL_DF_OFFSET_UNKNOWN\tS64_MIN\n \n+#define ZL_REG_DPLL_TIE_DATA_03(_idx)\t\t\t\t\t\\\n+\tZL_REG_IDX(_idx, 6, 0x0C, 6, 4, 0x20)\n+#define ZL_REG_DPLL_TIE_DATA_4\t\t\tZL_REG(7, 0x0C, 6)\n+#define ZL_REG_DPLL_TIE_DATA(_idx)\t\t\t\t\t\\\n+\t((_idx) \u003c 4 ? ZL_REG_DPLL_TIE_DATA_03(_idx) : ZL_REG_DPLL_TIE_DATA_4)\n+\n+#define ZL_REG_DPLL_TOD_SEC_03(_idx)\t\t\t\t\t\\\n+\tZL_REG_IDX(_idx, 6, 0x12, 6, 4, 0x20)\n+#define ZL_REG_DPLL_TOD_SEC_4\t\t\tZL_REG(7, 0x12, 6)\n+#define ZL_REG_DPLL_TOD_SEC(_idx)\t\t\t\t\t\\\n+\t((_idx) \u003c 4 ? ZL_REG_DPLL_TOD_SEC_03(_idx) : ZL_REG_DPLL_TOD_SEC_4)\n+\n+#define ZL_REG_DPLL_TOD_NS_03(_idx)\t\t\t\t\t\\\n+\tZL_REG_IDX(_idx, 6, 0x18, 4, 4, 0x20)\n+#define ZL_REG_DPLL_TOD_NS_4\t\t\tZL_REG(7, 0x18, 4)\n+#define ZL_REG_DPLL_TOD_NS(_idx)\t\t\t\t\t\\\n+\t((_idx) \u003c 4 ? ZL_REG_DPLL_TOD_NS_03(_idx) : ZL_REG_DPLL_TOD_NS_4)\n+\n /***********************************\n  * Register Page 9, Synth and Output\n  ***********************************/\n@@ -221,6 +256,23 @@\n #define ZL_OUTPUT_CTRL_EN\t\t\tBIT(0)\n #define ZL_OUTPUT_CTRL_SYNTH_SEL\t\tGENMASK(6, 4)\n \n+#define ZL_REG_OUTPUT_STEP_TIME_MASK\t\tZL_REG(9, 0x36, 2)\n+\n+#define ZL_REG_OUTPUT_PHASE_STEP_CTRL\t\tZL_REG(9, 0x38, 1)\n+#define ZL_OUTPUT_PHASE_STEP_CTRL_DPLL\t\tGENMASK(6, 4)\n+#define ZL_OUTPUT_PHASE_STEP_CTRL_TOD_STEP\tBIT(3)\n+#define ZL_OUTPUT_PHASE_STEP_CTRL_OP\t\tGENMASK(1, 0)\n+#define ZL_OUTPUT_PHASE_STEP_CTRL_OP_NONE\t0\n+#define ZL_OUTPUT_PHASE_STEP_CTRL_OP_RESET\t1\n+#define ZL_OUTPUT_PHASE_STEP_CTRL_OP_READ\t2\n+#define ZL_OUTPUT_PHASE_STEP_CTRL_OP_WRITE\t3\n+\n+#define ZL_REG_OUTPUT_PHASE_STEP_NUMBER\t\tZL_REG(9, 0x39, 1)\n+\n+#define ZL_REG_OUTPUT_PHASE_STEP_MASK\t\tZL_REG(9, 0x3a, 2)\n+\n+#define ZL_REG_OUTPUT_PHASE_STEP_DATA\t\tZL_REG(9, 0x3c, 4)\n+\n /*******************************\n  * Register Page 10, Ref Mailbox\n  *******************************/\n"
}

2/1 2026/08/07 11:52 agent patch-evaluator
1m Model:
best-expensive

Args:
null

Results:
{
  "EnableConfigs": [
    "ZL3073X_I2C",
    "ZL3073X_SPI",
    "PTP_1588_CLOCK"
  ],
  "FocusSymbols": [
    "zl3073x_chan_tod_ready_wait",
    "zl3073x_chan_tod_read",
    "zl3073x_chan_tod_write",
    "zl3073x_chan_tod_adjust",
    "zl3073x_chan_df_offset_set",
    "zl3073x_chan_tie_write",
    "zl3073x_chan_phase_step"
  ],
  "Reasoning": "The patch adds PTP clock support to the Microchip Azurite DPLL driver (zl3073x). The driver can be instantiated via I2C or SPI, and the PTP clock interface is exposed to userspace via `/dev/ptpX`. Syzkaller can fuzz this functionality using I2C/SPI emulation and PTP ioctls.",
  "WorthFuzzing": true
}

Instruction:
You are an expert Linux kernel maintainer.
Your job is to review a provided patch series and determine
if it makes functional changes to the kernel that should be fuzzed.

IMPORTANT: The changes have ALREADY been applied and committed as the HEAD commit in
your workspace. Do NOT rely on your internal knowledge of the kernel. You must actively
use your code access tools to examine the actual source code and confirm any assumptions.

Return WorthFuzzing=false if the patch only contains:
- Modifications to Documentation/, Kconfig files, or code comments.
- Purely decorative changes, such as logging (e.g., pr_err, printk) or tracepoints.
- Changes to numeric constants or macros that do not functionally alter execution flow.
- Code paths that are impossible to reach in virtualized environments like GCE or QEMU,
  even when utilizing software-emulated hardware (e.g., usb gadget, mac80211_hwsim).
- Code in vendor-specific PCIe switch, SmartNIC, or GPU drivers (e.g., mlxsw, pds_core, qed,
  ionic, amdgpu) that require physical PCIe hardware cards not emulated in standard QEMU.
- Driver .remove, .shutdown, or pci_unregister_driver teardown callbacks (e.g., igb_remove)
  that are executed only during PCI hot-unplug or sysfs driver unbind operations.

If it modifies reachable core kernel logic, drivers, or architectures, use your code search
tools to verify the code can be executed, then return WorthFuzzing=true.

When returning WorthFuzzing=true, you MUST ALSO:
1. Extract any specific kernel functions that should be heavily fuzzed into FocusSymbols.
   Avoid listing generic hot-path functions to prevent skewed test distributions.
   Prefer non-static, non-inlined API entrypoint functions over internal static helper functions
   (which are inlined by the compiler and do not have distinct symbol addresses).
2. Identify any specific CONFIG_ options required to properly test this new/modified feature.
   Go and look into the Kconfig files and check for ifdefs around the code, do not make assumptions.
   Also check "depends on" lines in Kconfig to include any non-standard parent subsystem configs
   needed for Kbuild to compile the code statically into vmlinux. List them in the EnableConfigs
   output array, and DO NOT add a 'CONFIG_' prefix (e.g., return "NET_IPV4" instead of "CONFIG_NET_IPV4").
Prefer calling several tools at the same time to save round-trips.


Use set-results tool to provide results of the analysis.
It must be called exactly once before the final reply.
Ignore results of this tool.

Prompt:
For your convenience, here is the diff of the changes:
commit 591dec83a85d707455260e918e741257e45f9291
Author: syz-cluster <triage@syzkaller.com>
Date:   Fri Aug 7 11:52:46 2026 +0000

    syz-cluster: applied patch under review

diff --git a/drivers/dpll/zl3073x/Kconfig b/drivers/dpll/zl3073x/Kconfig
index 5bbca14005813..da1731ace15f0 100644
--- a/drivers/dpll/zl3073x/Kconfig
+++ b/drivers/dpll/zl3073x/Kconfig
@@ -2,7 +2,7 @@
 
 config ZL3073X
 	tristate "Microchip Azurite DPLL/PTP/SyncE devices" if COMPILE_TEST
-	depends on NET
+	depends on NET && PTP_1588_CLOCK
 	select DPLL
 	select NET_DEVLINK
 	select REGMAP
@@ -16,7 +16,7 @@ config ZL3073X
 
 config ZL3073X_I2C
 	tristate "I2C bus implementation for Microchip Azurite devices"
-	depends on I2C && NET
+	depends on I2C && NET && PTP_1588_CLOCK
 	select REGMAP_I2C
 	select ZL3073X
 	help
@@ -28,7 +28,7 @@ config ZL3073X_I2C
 
 config ZL3073X_SPI
 	tristate "SPI bus implementation for Microchip Azurite devices"
-	depends on NET && SPI
+	depends on NET && SPI && PTP_1588_CLOCK
 	select REGMAP_SPI
 	select ZL3073X
 	help
diff --git a/drivers/dpll/zl3073x/chan.c b/drivers/dpll/zl3073x/chan.c
index 4ec2cf53dad46..af6ca43c33590 100644
--- a/drivers/dpll/zl3073x/chan.c
+++ b/drivers/dpll/zl3073x/chan.c
@@ -3,6 +3,7 @@
 #include <linux/cleanup.h>
 #include <linux/delay.h>
 #include <linux/dev_printk.h>
+#include <linux/ptp_clock_kernel.h>
 #include <linux/string.h>
 #include <linux/types.h>
 
@@ -162,8 +163,8 @@ int zl3073x_chan_nco_mode_set(struct zl3073x_dev *zldev, u8 index)
  * @zldev: pointer to zl3073x_dev structure
  * @index: DPLL channel index to fetch state for
  *
- * Reads the mode_refsel register and reference priority registers for
- * the given DPLL channel and stores the raw values for later use.
+ * Reads the mode_refsel, status and reference priority registers for
+ * the given DPLL channel and stores the values for later use.
  *
  * Return: 0 on success, <0 on error
  */
@@ -234,6 +235,322 @@ const struct zl3073x_chan *zl3073x_chan_state_get(struct zl3073x_dev *zldev,
 	return &zldev->chan[index];
 }
 
+/**
+ * zl3073x_chan_tod_ready_wait - wait for ToD semaphore to clear
+ * @zldev: pointer to zl3073x device
+ * @ch: DPLL channel index
+ *
+ * Polls the ToD control register until the semaphore bit is cleared,
+ * indicating the device has completed the previous ToD operation.
+ *
+ * Return: 0 on success, -EBUSY if semaphore not cleared, <0 on error
+ */
+int zl3073x_chan_tod_ready_wait(struct zl3073x_dev *zldev, u8 ch)
+{
+	unsigned int timeout;
+	u8 tod_ctrl;
+	int rc;
+
+	rc = zl3073x_read_u8(zldev, ZL_REG_DPLL_TOD_CTRL(ch), &tod_ctrl);
+	if (rc)
+		return rc;
+
+	switch (FIELD_GET(ZL_DPLL_TOD_CTRL_CMD, tod_ctrl)) {
+	case 0:
+		/* No operation pending (power-on/reset default) */
+		return 0;
+	case ZL_DPLL_TOD_CTRL_CMD_WR_NEXT_1HZ:
+		timeout = ZL_POLL_TOD_WR_TIMEOUT_US;
+		break;
+	case ZL_DPLL_TOD_CTRL_CMD_RD_CURRENT:
+	case ZL_DPLL_TOD_CTRL_CMD_RD_NEXT_1HZ:
+		timeout = ZL_POLL_TOD_RD_TIMEOUT_US;
+		break;
+	default:
+		/* FW never writes cmd bits; only the driver sets them */
+		WARN_ON(1);
+		return -EOPNOTSUPP;
+	}
+
+	rc = zl3073x_poll_zero_u8(zldev, ZL_REG_DPLL_TOD_CTRL(ch),
+				  ZL_DPLL_TOD_CTRL_SEM, timeout);
+
+	return rc == -ETIMEDOUT ? -EBUSY : rc;
+}
+
+/**
+ * zl3073x_chan_tod_ctrl - issue ToD command
+ * @zldev: pointer to zl3073x device
+ * @ch: DPLL channel index
+ * @cmd: ToD command to execute
+ *
+ * Writes the semaphore and command to dpll_tod_ctrl. The caller must
+ * ensure the device is ready (semaphore clear) before calling and
+ * must wait for completion if needed.
+ *
+ * Return: 0 on success, <0 on error
+ */
+static int zl3073x_chan_tod_ctrl(struct zl3073x_dev *zldev, u8 ch, u8 cmd)
+{
+	return zl3073x_write_u8(zldev, ZL_REG_DPLL_TOD_CTRL(ch),
+				ZL_DPLL_TOD_CTRL_SEM | cmd);
+}
+
+/**
+ * zl3073x_chan_tod_read - read ToD registers after issuing a command
+ * @zldev: pointer to zl3073x device
+ * @ch: DPLL channel index
+ * @next_hz: if true, read predicted ToD at next 1 Hz; otherwise read current
+ * @ts: timespec to store the result
+ * @sts: optional system timestamp pair for cross-timestamping
+ *
+ * Context: Caller must serialize all zl3073x_chan_tod_* calls externally.
+ * Return: 0 on success, <0 on error
+ */
+int zl3073x_chan_tod_read(struct zl3073x_dev *zldev, u8 ch,
+			  bool next_hz, struct timespec64 *ts,
+			  struct ptp_system_timestamp *sts)
+{
+	u32 nsec;
+	u64 sec;
+	u8 cmd;
+	int rc;
+
+	if (next_hz)
+		cmd = ZL_DPLL_TOD_CTRL_CMD_RD_NEXT_1HZ;
+	else
+		cmd = ZL_DPLL_TOD_CTRL_CMD_RD_CURRENT;
+
+	/* Wait for any previous ToD operation to complete */
+	rc = zl3073x_chan_tod_ready_wait(zldev, ch);
+	if (rc)
+		return rc;
+
+	ptp_read_system_prets(sts);
+	rc = zl3073x_chan_tod_ctrl(zldev, ch, cmd);
+	if (rc)
+		return rc;
+
+	rc = zl3073x_chan_tod_ready_wait(zldev, ch);
+	if (rc)
+		return rc;
+	ptp_read_system_postts(sts);
+
+	rc = zl3073x_read_u48(zldev, ZL_REG_DPLL_TOD_SEC(ch), &sec);
+	if (rc)
+		return rc;
+
+	/* HW nanoseconds are always in [0, NSEC_PER_SEC) range */
+	rc = zl3073x_read_u32(zldev, ZL_REG_DPLL_TOD_NS(ch), &nsec);
+	if (rc)
+		return rc;
+
+	ts->tv_sec = sec;
+	ts->tv_nsec = nsec;
+
+	return 0;
+}
+
+/**
+ * zl3073x_chan_tod_write - write ToD registers and trigger 1 Hz update
+ * @zldev: pointer to zl3073x device
+ * @ch: DPLL channel index
+ * @ts: time to set
+ *
+ * Context: Caller must serialize all zl3073x_chan_tod_* calls externally.
+ * Return: 0 on success, <0 on error
+ */
+int zl3073x_chan_tod_write(struct zl3073x_dev *zldev, u8 ch,
+			   struct timespec64 ts)
+{
+	int rc;
+
+	/* Wait for any previous ToD operation to complete */
+	rc = zl3073x_chan_tod_ready_wait(zldev, ch);
+	if (rc)
+		return rc;
+
+	rc = zl3073x_write_u48(zldev, ZL_REG_DPLL_TOD_SEC(ch), ts.tv_sec);
+	if (rc)
+		return rc;
+
+	rc = zl3073x_write_u32(zldev, ZL_REG_DPLL_TOD_NS(ch), ts.tv_nsec);
+	if (rc)
+		return rc;
+
+	return zl3073x_chan_tod_ctrl(zldev, ch,
+				     ZL_DPLL_TOD_CTRL_CMD_WR_NEXT_1HZ);
+}
+
+/**
+ * zl3073x_chan_tod_adjust - atomic ToD read-modify-write with rollover guard
+ * @zldev: pointer to zl3073x device
+ * @ch: DPLL channel index
+ * @delta: time adjustment to apply
+ *
+ * Reads the next-Hz ToD and current ToD, then checks whether enough time
+ * remains before the next 1 Hz rollover to safely complete the write.
+ * Re-reads if the 1 Hz tick crossed between the two reads or if less
+ * than 20 ms remains before the next rollover. Applies @delta and writes
+ * the result back.
+ *
+ * Context: Caller must serialize all zl3073x_chan_tod_* calls externally.
+ * Return: 0 on success, <0 on error
+ */
+#define ZL_TOD_MAX_RETRIES	20
+int zl3073x_chan_tod_adjust(struct zl3073x_dev *zldev, u8 ch,
+			    struct timespec64 delta)
+{
+	static const long threshold_ns = 20 * NSEC_PER_MSEC;
+	struct timespec64 ts_next, ts_cur, diff;
+	int rc, i;
+
+	for (i = 0; i < ZL_TOD_MAX_RETRIES; i++) {
+		rc = zl3073x_chan_tod_read(zldev, ch, true, &ts_next, NULL);
+		if (rc)
+			return rc;
+
+		rc = zl3073x_chan_tod_read(zldev, ch, false, &ts_cur, NULL);
+		if (rc)
+			return rc;
+
+		/* Ensure the 1 Hz tick did not cross between the two reads
+		 * and that enough margin remains to complete the write.
+		 */
+		diff = timespec64_sub(ts_next, ts_cur);
+		if (diff.tv_sec > 0 ||
+		    (!diff.tv_sec && diff.tv_nsec >= threshold_ns))
+			break;
+	}
+	if (i == ZL_TOD_MAX_RETRIES) {
+		dev_warn(zldev->dev,
+			 "DPLL%u ToD adjust failed to get stable margin\n",
+			 ch);
+		return -EBUSY;
+	}
+
+	/* Apply delta to the next-Hz ToD */
+	ts_next = timespec64_add(ts_next, delta);
+	if (!timespec64_valid(&ts_next))
+		return -EINVAL;
+
+	return zl3073x_chan_tod_write(zldev, ch, ts_next);
+}
+
+/**
+ * zl3073x_chan_df_offset_set - write delta frequency offset to hardware
+ * @zldev: pointer to zl3073x device
+ * @ch: DPLL channel index
+ * @offset: frequency offset in 2^-48 steps
+ *
+ * Context: Caller must hold the per-DPLL lock.
+ * Return: 0 on success, <0 on error
+ */
+int zl3073x_chan_df_offset_set(struct zl3073x_dev *zldev, u8 ch, s64 offset)
+{
+	int rc;
+
+	rc = zl3073x_write_u48(zldev, ZL_REG_DPLL_DF_OFFSET(ch), offset);
+	if (!rc)
+		zldev->chan[ch].df_offset = offset;
+
+	return rc;
+}
+
+/**
+ * zl3073x_chan_tie_write - adjust DPLL phase using TIE write
+ * @zldev: pointer to zl3073x device
+ * @ch: DPLL channel index
+ * @delta_ns: phase adjustment in nanoseconds (must be within +-1s)
+ *
+ * Converts nanoseconds to TIE units (0.01 ps) and writes TIE data
+ * to the specified channel.
+ *
+ * Return: 0 on success, <0 on error
+ */
+int zl3073x_chan_tie_write(struct zl3073x_dev *zldev, u8 ch, s64 delta_ns)
+{
+	s64 tie_data;
+	int rc;
+
+	if (WARN_ON(delta_ns <= -NSEC_PER_SEC || delta_ns >= NSEC_PER_SEC))
+		return -ERANGE;
+
+	guard(mutex)(&zldev->tie_lock);
+
+	/* Wait for any previous TIE operation to complete */
+	rc = zl3073x_poll_zero_u8(zldev, ZL_REG_DPLL_TIE_CTRL,
+				  ZL_DPLL_TIE_CTRL_OP,
+				  ZL_POLL_TIE_WR_TIMEOUT_US);
+	if (rc)
+		return rc;
+
+	/* Convert ns to TIE units (0.01 ps = 10^-14 s) */
+	tie_data = delta_ns * 100000LL;
+
+	rc = zl3073x_write_u48(zldev, ZL_REG_DPLL_TIE_DATA(ch), tie_data);
+	if (rc)
+		return rc;
+
+	rc = zl3073x_write_u8(zldev, ZL_REG_DPLL_TIE_CTRL_MASK, BIT(ch));
+	if (rc)
+		return rc;
+
+	return zl3073x_write_u8(zldev, ZL_REG_DPLL_TIE_CTRL,
+				ZL_DPLL_TIE_CTRL_OP_WR);
+}
+
+/**
+ * zl3073x_chan_phase_step - execute one output phase step operation
+ * @zldev: pointer to zl3073x device
+ * @ch: DPLL channel index
+ * @out_mask: bitmask of outputs to step
+ * @step_cycles: phase step in synthesizer clock cycles
+ * @tod_step: also step the ToD counter
+ *
+ * All masked outputs must use synthesizers of the same frequency since
+ * the step value is in synthesizer clock cycles.
+ *
+ * Return: 0 on success, <0 on error
+ */
+int zl3073x_chan_phase_step(struct zl3073x_dev *zldev, u8 ch,
+			    u16 out_mask, s32 step_cycles,
+			    bool tod_step)
+{
+	u8 ctrl;
+	int rc;
+
+	guard(mutex)(&zldev->phase_step_lock);
+
+	/* Wait for any previous phase step operation to complete */
+	rc = zl3073x_poll_zero_u8(zldev, ZL_REG_OUTPUT_PHASE_STEP_CTRL,
+				  ZL_OUTPUT_PHASE_STEP_CTRL_OP,
+				  ZL_POLL_PHASE_STEP_TIMEOUT_US);
+	if (rc)
+		return rc;
+
+	rc = zl3073x_write_u32(zldev, ZL_REG_OUTPUT_PHASE_STEP_DATA,
+			       step_cycles);
+	if (rc)
+		return rc;
+
+	rc = zl3073x_write_u16(zldev, ZL_REG_OUTPUT_PHASE_STEP_MASK, out_mask);
+	if (rc)
+		return rc;
+
+	rc = zl3073x_write_u8(zldev, ZL_REG_OUTPUT_PHASE_STEP_NUMBER, 1);
+	if (rc)
+		return rc;
+
+	ctrl = FIELD_PREP(ZL_OUTPUT_PHASE_STEP_CTRL_DPLL, ch) |
+	       FIELD_PREP(ZL_OUTPUT_PHASE_STEP_CTRL_OP,
+			  ZL_OUTPUT_PHASE_STEP_CTRL_OP_WRITE);
+	if (tod_step)
+		ctrl |= ZL_OUTPUT_PHASE_STEP_CTRL_TOD_STEP;
+
+	return zl3073x_write_u8(zldev, ZL_REG_OUTPUT_PHASE_STEP_CTRL, ctrl);
+}
+
 /**
  * zl3073x_chan_state_set - commit DPLL channel state changes to hardware
  * @zldev: pointer to zl3073x_dev structure
diff --git a/drivers/dpll/zl3073x/chan.h b/drivers/dpll/zl3073x/chan.h
index dc9c6d95bdee7..99c3b7088f678 100644
--- a/drivers/dpll/zl3073x/chan.h
+++ b/drivers/dpll/zl3073x/chan.h
@@ -5,10 +5,12 @@
 
 #include <linux/bitfield.h>
 #include <linux/stddef.h>
+#include <linux/time64.h>
 #include <linux/types.h>
 
 #include "regs.h"
 
+struct ptp_system_timestamp;
 struct zl3073x_dev;
 
 /**
@@ -42,6 +44,21 @@ int zl3073x_chan_state_set(struct zl3073x_dev *zldev, u8 index,
 int zl3073x_chan_state_update(struct zl3073x_dev *zldev, u8 index);
 int zl3073x_chan_nco_mode_set(struct zl3073x_dev *zldev, u8 index);
 
+int zl3073x_chan_tod_ready_wait(struct zl3073x_dev *zldev, u8 ch);
+int zl3073x_chan_tod_read(struct zl3073x_dev *zldev, u8 ch,
+			  bool next_hz, struct timespec64 *ts,
+			  struct ptp_system_timestamp *sts);
+int zl3073x_chan_tod_write(struct zl3073x_dev *zldev, u8 ch,
+			   struct timespec64 ts);
+int zl3073x_chan_tod_adjust(struct zl3073x_dev *zldev, u8 ch,
+			    struct timespec64 delta);
+int zl3073x_chan_phase_step(struct zl3073x_dev *zldev, u8 ch,
+			    u16 out_mask, s32 step_cycles, bool tod_step);
+
+int zl3073x_chan_df_offset_set(struct zl3073x_dev *zldev, u8 ch, s64 offset);
+
+int zl3073x_chan_tie_write(struct zl3073x_dev *zldev, u8 ch, s64 delta_ns);
+
 /**
  * zl3073x_chan_df_offset_get - get cached df_offset vs tracked reference
  * @chan: pointer to channel state
@@ -200,6 +217,21 @@ static inline bool zl3073x_chan_mode_is_reflock(const struct zl3073x_chan *chan)
 	return zl3073x_chan_mode_get(chan) == ZL_DPLL_MODE_REFSEL_MODE_REFLOCK;
 }
 
+/**
+ * zl3073x_chan_mode_supports_tie - check if channel mode supports TIE write
+ * @chan: pointer to channel state
+ *
+ * TIE write is supported in AUTO and REFLOCK modes regardless of lock state.
+ *
+ * Return: true if TIE write is supported, false otherwise
+ */
+static inline bool
+zl3073x_chan_mode_supports_tie(const struct zl3073x_chan *chan)
+{
+	return zl3073x_chan_mode_is_auto(chan) ||
+		zl3073x_chan_mode_is_reflock(chan);
+}
+
 /**
  * zl3073x_chan_is_ho_ready - check if holdover is ready
  * @chan: pointer to channel state
diff --git a/drivers/dpll/zl3073x/core.c b/drivers/dpll/zl3073x/core.c
index 5b2d77f2c2288..230df08e27cd1 100644
--- a/drivers/dpll/zl3073x/core.c
+++ b/drivers/dpll/zl3073x/core.c
@@ -322,7 +322,7 @@ int zl3073x_write_u48(struct zl3073x_dev *zldev, unsigned int reg, u64 val)
 int zl3073x_poll_zero_u8(struct zl3073x_dev *zldev, unsigned int reg,
 			 u8 mask, unsigned int timeout_us)
 {
-#define ZL_POLL_SLEEP_US 10
+	unsigned int sleep_us = timeout_us / 50;
 	unsigned int val;
 
 	/* Check the register is 8bit */
@@ -336,7 +336,7 @@ int zl3073x_poll_zero_u8(struct zl3073x_dev *zldev, unsigned int reg,
 	reg = ZL_REG_ADDR(reg) + ZL_RANGE_OFFSET;
 
 	return regmap_read_poll_timeout(zldev->regmap, reg, val, !(val & mask),
-					ZL_POLL_SLEEP_US, timeout_us);
+					sleep_us, timeout_us);
 }
 
 int zl3073x_mb_op(struct zl3073x_dev *zldev, unsigned int op_reg, u8 op_val,
@@ -511,6 +511,11 @@ zl3073x_dev_state_fetch(struct zl3073x_dev *zldev)
 	int rc;
 	u8 i;
 
+	rc = zl3073x_read_u16(zldev, ZL_REG_OUTPUT_STEP_TIME_MASK,
+			      &zldev->out_step_time_mask);
+	if (rc)
+		return rc;
+
 	for (i = 0; i < ZL3073X_NUM_REFS; i++) {
 		rc = zl3073x_ref_state_fetch(zldev, i);
 		if (rc) {
@@ -1034,6 +1039,14 @@ int zl3073x_dev_probe(struct zl3073x_dev *zldev)
 	 * and/or polls are required to be done atomically.
 	 */
 	rc = devm_mutex_init(zldev->dev, &zldev->multiop_lock);
+	if (rc)
+		return dev_err_probe(zldev->dev, rc,
+				     "Failed to initialize mutex\n");
+	rc = devm_mutex_init(zldev->dev, &zldev->phase_step_lock);
+	if (rc)
+		return dev_err_probe(zldev->dev, rc,
+				     "Failed to initialize mutex\n");
+	rc = devm_mutex_init(zldev->dev, &zldev->tie_lock);
 	if (rc)
 		return dev_err_probe(zldev->dev, rc,
 				     "Failed to initialize mutex\n");
diff --git a/drivers/dpll/zl3073x/core.h b/drivers/dpll/zl3073x/core.h
index 78dc208f3eea2..67c10e2595118 100644
--- a/drivers/dpll/zl3073x/core.h
+++ b/drivers/dpll/zl3073x/core.h
@@ -26,6 +26,10 @@ struct zl3073x_dpll;
 #define ZL_POLL_HWREG_TIMEOUT_US	(50 * USEC_PER_MSEC)
 #define ZL_POLL_MB_TIMEOUT_US		(30 * USEC_PER_MSEC)
 #define ZL_POLL_PHASE_ERR_TIMEOUT_US	(50 * USEC_PER_MSEC)
+#define ZL_POLL_PHASE_STEP_TIMEOUT_US	(3000 * USEC_PER_MSEC)
+#define ZL_POLL_TIE_WR_TIMEOUT_US	(1000 * USEC_PER_MSEC)
+#define ZL_POLL_TOD_RD_TIMEOUT_US	(30 * USEC_PER_MSEC)
+#define ZL_POLL_TOD_WR_TIMEOUT_US	(1000 * USEC_PER_MSEC)
 
 enum zl3073x_flags {
 	ZL3073X_FLAG_REF_PHASE_COMP_32_BIT,
@@ -55,6 +59,8 @@ struct zl3073x_chip_info {
  * @regmap: regmap to access device registers
  * @info: detected chip info
  * @multiop_lock: to serialize multiple register operations
+ * @tie_lock: to serialize TIE write operations
+ * @phase_step_lock: to serialize output phase step operations
  * @ref: array of input references' invariants
  * @out: array of outs' invariants
  * @synth: array of synths' invariants
@@ -63,6 +69,7 @@ struct zl3073x_chip_info {
  * @kworker: thread for periodic work
  * @work: periodic work
  * @clock_id: clock id of the device
+ * @out_step_time_mask: output step-time mask (device-global)
  * @phase_avg_factor: phase offset measurement averaging factor
  * @freq_monitor: is frequency monitor enabled
  */
@@ -71,6 +78,8 @@ struct zl3073x_dev {
 	struct regmap			*regmap;
 	const struct zl3073x_chip_info	*info;
 	struct mutex			multiop_lock;
+	struct mutex			tie_lock;
+	struct mutex			phase_step_lock;
 
 	/* Invariants */
 	struct zl3073x_ref	ref[ZL3073X_NUM_REFS];
@@ -87,6 +96,7 @@ struct zl3073x_dev {
 
 	/* Per-chip parameters */
 	u64			clock_id;
+	u16			out_step_time_mask;
 	u8			phase_avg_factor;
 	bool			freq_monitor;
 };
@@ -308,6 +318,19 @@ zl3073x_dev_out_is_enabled(struct zl3073x_dev *zldev, u8 index)
 	return zl3073x_synth_is_enabled(synth) && zl3073x_out_is_enabled(out);
 }
 
+/**
+ * zl3073x_dev_out_is_stepped - check if output is in step-time mask
+ * @zldev: pointer to zl3073x device
+ * @index: output index
+ *
+ * Return: true if output is affected by step-time operations
+ */
+static inline bool
+zl3073x_dev_out_is_stepped(struct zl3073x_dev *zldev, u8 index)
+{
+	return !!(zldev->out_step_time_mask & BIT(index));
+}
+
 /**
  * zl3073x_dev_out_dpll_get - get DPLL ID the output is driven by
  * @zldev: pointer to zl3073x device
diff --git a/drivers/dpll/zl3073x/dpll.c b/drivers/dpll/zl3073x/dpll.c
index 0488ae6ac486c..a686dd52bfac9 100644
--- a/drivers/dpll/zl3073x/dpll.c
+++ b/drivers/dpll/zl3073x/dpll.c
@@ -2,6 +2,7 @@
 
 #include <linux/bits.h>
 #include <linux/bitfield.h>
+#include <linux/cleanup.h>
 #include <linux/bug.h>
 #include <linux/container_of.h>
 #include <linux/dev_printk.h>
@@ -13,6 +14,7 @@
 #include <linux/netlink.h>
 #include <linux/platform_device.h>
 #include <linux/property.h>
+#include <linux/ptp_clock_kernel.h>
 #include <linux/slab.h>
 #include <linux/sprintf.h>
 
@@ -2275,45 +2277,375 @@ zl3073x_dpll_init_fine_phase_adjust(struct zl3073x_dev *zldev)
 	return zl3073x_write_u8(zldev, ZL_REG_SYNTH_PHASE_SHIFT_CTRL, 0x01);
 }
 
+/* Maximum frequency adjustment: +-1% of nominal in ppb */
+#define ZL3073X_DPLL_PTP_MAX_ADJ	10000000
+
 /**
- * zl3073x_dpll_alloc - allocate DPLL device
- * @zldev: pointer to zl3073x device
- * @ch: DPLL channel number
+ * zl3073x_dpll_ptp_gettimex64 - read current time from ToD counters
+ * @info: PTP clock info
+ * @ts: timespec to store current time
+ * @sts: optional system timestamp pair for cross-timestamping
  *
- * Allocates DPLL device structure for given DPLL channel.
+ * Return: 0 on success, <0 on error
+ */
+static int zl3073x_dpll_ptp_gettimex64(struct ptp_clock_info *info,
+				       struct timespec64 *ts,
+				       struct ptp_system_timestamp *sts)
+{
+	struct zl3073x_dpll *zldpll = container_of(info, struct zl3073x_dpll,
+						   ptp_info);
+
+	guard(mutex)(&zldpll->lock);
+
+	return zl3073x_chan_tod_read(zldpll->dev, zldpll->id, false, ts, sts);
+}
+
+/**
+ * zl3073x_dpll_ptp_settime64 - set ToD counters to given time
+ * @info: PTP clock info
+ * @ts: timespec with time to set
  *
- * Return: pointer to DPLL device on success, error pointer on error
+ * Return: 0 on success, <0 on error
  */
-struct zl3073x_dpll *
-zl3073x_dpll_alloc(struct zl3073x_dev *zldev, u8 ch)
+static int zl3073x_dpll_ptp_settime64(struct ptp_clock_info *info,
+				      const struct timespec64 *ts)
 {
-	struct zl3073x_dpll *zldpll;
+	struct zl3073x_dpll *zldpll = container_of(info, struct zl3073x_dpll,
+						   ptp_info);
 
-	zldpll = kzalloc_obj(*zldpll);
-	if (!zldpll)
-		return ERR_PTR(-ENOMEM);
+	guard(mutex)(&zldpll->lock);
 
-	zldpll->dev = zldev;
-	zldpll->id = ch;
-	mutex_init(&zldpll->lock);
-	INIT_LIST_HEAD(&zldpll->pins);
+	return zl3073x_chan_tod_write(zldpll->dev, zldpll->id, *ts);
+}
 
-	return zldpll;
+/**
+ * zl3073x_dpll_ptp_adjtime_phase_step - adjust sub-second time via phase step
+ * @zldpll: DPLL channel
+ * @delta: time adjustment in nanoseconds (must be within (-NSEC_PER_SEC,
+ *         NSEC_PER_SEC))
+ *
+ * Uses the output phase step mechanism with tod_step=1 to adjust both
+ * the output clock phase and the ToD counter simultaneously. This keeps
+ * outputs and ToD coherent. Only valid for NCO.
+ *
+ * Outputs are grouped by synthesizer since the phase step value is in
+ * synthesizer clock cycles. The first synth group with enabled outputs
+ * uses tod_step to adjust both outputs and the ToD counter. Remaining
+ * groups step outputs only. If no synth has enabled outputs, the ToD
+ * counter is stepped alone using an empty output mask (the FW uses
+ * the first enabled synth's period for the conversion).
+ *
+ * Return: 0 on success, -EOPNOTSUPP if no synths available, <0 on error
+ */
+static int zl3073x_dpll_ptp_adjtime_phase_step(struct zl3073x_dpll *zldpll,
+					       s64 delta)
+{
+	u16 synth_mask[ZL3073X_NUM_SYNTHS] = {};
+	struct zl3073x_dev *zldev = zldpll->dev;
+	const struct zl3073x_synth *synth;
+	struct zl3073x_dpll_pin *pin;
+	u32 first_synth_freq = 0;
+	bool tod_stepped = false;
+	s32 step_cycles;
+	u32 synth_freq;
+	int rc;
+	u8 i;
+
+	/* Build per-synth output masks from registered output pins */
+	list_for_each_entry(pin, &zldpll->pins, list) {
+		u8 out_id, synth_id;
+
+		if (zl3073x_dpll_is_input_pin(pin))
+			continue;
+
+		out_id = zl3073x_output_pin_out_get(pin->id);
+
+		if (!zl3073x_dev_out_is_stepped(zldev, out_id))
+			continue;
+
+		synth_id = zl3073x_dev_out_synth_get(zldev, out_id);
+		if (synth_id >= ZL3073X_NUM_SYNTHS) {
+			dev_warn(zldev->dev, "Unexpected synth id for OUT%u\n",
+				 out_id);
+			continue;
+		}
+		synth_mask[synth_id] |= BIT(out_id);
+	}
+
+	/* Process each synth group */
+	for (i = 0; i < ZL3073X_NUM_SYNTHS; i++) {
+		synth = zl3073x_synth_state_get(zldev, i);
+		if (!zl3073x_synth_is_enabled(synth) ||
+		    zl3073x_synth_dpll_get(synth) != zldpll->id)
+			continue;
+
+		synth_freq = zl3073x_synth_freq_get(synth);
+
+		/* Remember first enabled synth freq for ToD-only fallback */
+		if (!first_synth_freq)
+			first_synth_freq = synth_freq;
+
+		if (!synth_mask[i])
+			continue;
+
+		/* Safe for s32: max synth freq is 750 MHz */
+		step_cycles = div_s64(delta * synth_freq, NSEC_PER_SEC);
+
+		rc = zl3073x_chan_phase_step(zldev, zldpll->id,
+					     synth_mask[i], step_cycles,
+					     !tod_stepped);
+		if (rc) {
+			if (tod_stepped) {
+				dev_warn(zldev->dev,
+					 "Partial phase step failure\n");
+				return 0;
+			}
+			return rc;
+		}
+		tod_stepped = true;
+	}
+
+	if (!first_synth_freq)
+		return -EOPNOTSUPP;
+
+	/* No enabled outputs found; step ToD counter only using the
+	 * first enabled synth's period (empty output mask).
+	 */
+	if (!tod_stepped) {
+		step_cycles = div_s64(delta * first_synth_freq, NSEC_PER_SEC);
+		return zl3073x_chan_phase_step(zldev, zldpll->id, 0,
+					       step_cycles, true);
+	}
+
+	return 0;
 }
 
 /**
- * zl3073x_dpll_free - free DPLL device
- * @zldpll: pointer to zl3073x_dpll structure
+ * zl3073x_dpll_ptp_adjtime - adjust PTP clock time
+ * @info: PTP clock info
+ * @delta: time adjustment in nanoseconds
  *
- * Deallocates given DPLL device previously allocated by @zl3073x_dpll_alloc.
+ * For NCO, large deltas (>= 1 second) are split into a ToD
+ * read-modify-write for the seconds part and an output phase step for
+ * the sub-second remainder. Sub-second deltas use phase step directly,
+ * falling back to ToD read-modify-write if phase step is unavailable.
+ * In AUTO/REFLOCK modes, large deltas are split into ToD
+ * read-modify-write for seconds and TIE write for the sub-second
+ * remainder. Sub-second deltas use TIE write directly.
+ *
+ * Return: 0 on success, <0 on error
  */
-void
-zl3073x_dpll_free(struct zl3073x_dpll *zldpll)
+static int zl3073x_dpll_ptp_adjtime(struct ptp_clock_info *info, s64 delta)
 {
-	WARN(zldpll->dpll_dev, "DPLL device is still registered\n");
+	struct zl3073x_dpll *zldpll = container_of(info, struct zl3073x_dpll,
+						   ptp_info);
+	struct zl3073x_dev *zldev = zldpll->dev;
+	const struct zl3073x_chan *chan;
+	bool sec_adjusted = false;
+	struct timespec64 ts;
+	int rc;
 
-	mutex_destroy(&zldpll->lock);
-	kfree(zldpll);
+	if (!delta)
+		return 0;
+
+	guard(mutex)(&zldpll->lock);
+
+	/* Modes without phase step or TIE use plain ToD adjust */
+	chan = zl3073x_chan_state_get(zldev, zldpll->id);
+	if (!zl3073x_chan_mode_is_nco(chan) &&
+	    !zl3073x_chan_mode_supports_tie(chan))
+		return zl3073x_chan_tod_adjust(zldev, zldpll->id,
+					       ns_to_timespec64(delta));
+
+	/* Split off seconds via ToD read-modify-write so the sub-second
+	 * remainder can be applied through the output-coherent mechanism
+	 * (phase step or TIE write).
+	 */
+	if (delta >= NSEC_PER_SEC || delta <= -NSEC_PER_SEC) {
+		s32 remainder;
+
+		ts.tv_sec = div_s64_rem(delta, NSEC_PER_SEC, &remainder);
+		ts.tv_nsec = 0;
+		delta = remainder;
+
+		rc = zl3073x_chan_tod_adjust(zldev, zldpll->id, ts);
+		if (rc)
+			return rc;
+
+		/* No sub-second remainder, done */
+		if (!delta)
+			return 0;
+
+		/* Wait for the ToD write to be applied at the 1 Hz edge
+		 * before issuing phase step or TIE write, so the pending
+		 * WR_NEXT_1HZ does not overwrite the sub-second adjustment.
+		 */
+		rc = zl3073x_chan_tod_ready_wait(zldev, zldpll->id);
+		if (rc)
+			return rc;
+
+		sec_adjusted = true;
+	}
+
+	/* Apply sub-second delta via phase step (NCO) or TIE write */
+	if (zl3073x_chan_mode_is_nco(chan)) {
+		rc = zl3073x_dpll_ptp_adjtime_phase_step(zldpll, delta);
+		if (!rc)
+			return 0;
+	} else {
+		rc = zl3073x_chan_tie_write(zldev, zldpll->id, delta);
+		if (!rc)
+			return 0;
+	}
+
+	/* Phase step or TIE write failed, fall back to ToD adjust */
+	rc = zl3073x_chan_tod_adjust(zldev, zldpll->id,
+				     ns_to_timespec64(delta));
+
+	/* In the unlikely event that both phase step/TIE write and fallback
+	 * ToD adjust fail after seconds were already committed, return
+	 * success to prevent the PTP servo from retrying the full delta and
+	 * applying seconds again. The sub-second residual will self-correct
+	 * in the next servo cycle.
+	 */
+	if (rc && sec_adjusted) {
+		dev_warn(zldev->dev,
+			 "Sub-second adjustment failed after seconds applied\n");
+		return 0;
+	}
+
+	return rc;
+}
+
+/**
+ * zl3073x_dpll_ptp_adjfine - adjust PTP clock frequency
+ * @info: PTP clock info
+ * @scaled_ppm: frequency adjustment in scaled ppm (ppm * 2^16)
+ *
+ * Only supported for NCO. Writes the delta frequency offset register.
+ *
+ * Return:
+ * * %0			- success or @scaled_ppm is zero (no-op)
+ * * %-EOPNOTSUPP	- NCO pin is not connected and @scaled_ppm is non-zero
+ * * negative		- other error
+ */
+static int
+zl3073x_dpll_ptp_adjfine(struct ptp_clock_info *info, long scaled_ppm)
+{
+	struct zl3073x_dpll *zldpll = container_of(info, struct zl3073x_dpll,
+						   ptp_info);
+	const struct zl3073x_chan *chan;
+	s64 offset;
+
+	/* Convert scaled_ppm to df_offset in 2^-48 steps:
+	 *   df_offset = -(scaled_ppm * 2^32) / 10^6
+	 *
+	 * Simplify to avoid overflow:
+	 *   df_offset = -(scaled_ppm * 2^26) / 5^6
+	 *   df_offset = -(scaled_ppm * 67108864) / 15625
+	 */
+	offset = -div_s64((s64)scaled_ppm * 67108864LL, 15625);
+
+	guard(mutex)(&zldpll->lock);
+
+	chan = zl3073x_chan_state_get(zldpll->dev, zldpll->id);
+	if (!zl3073x_chan_mode_is_nco(chan))
+		return scaled_ppm ? -EOPNOTSUPP : 0;
+	if (offset == chan->df_offset)
+		return 0;
+
+	return zl3073x_chan_df_offset_set(zldpll->dev, zldpll->id, offset);
+}
+
+/**
+ * zl3073x_dpll_ptp_adjphase - adjust PTP clock phase
+ * @info: PTP clock info
+ * @delta: phase adjustment in nanoseconds
+ *
+ * Only supported in AUTO and REFLOCK modes. Uses TIE write for
+ * nanosecond resolution phase adjustment.
+ *
+ * Return:
+ * * %0			- success or @delta is zero (no-op)
+ * * %-EOPNOTSUPP	- mode does not support TIE and @delta is non-zero
+ * * negative		- other error
+ */
+static int zl3073x_dpll_ptp_adjphase(struct ptp_clock_info *info, s32 delta)
+{
+	struct zl3073x_dpll *zldpll = container_of(info, struct zl3073x_dpll,
+						   ptp_info);
+	struct zl3073x_dev *zldev = zldpll->dev;
+	const struct zl3073x_chan *chan;
+
+	if (!delta)
+		return 0;
+
+	guard(mutex)(&zldpll->lock);
+
+	chan = zl3073x_chan_state_get(zldev, zldpll->id);
+
+	if (!zl3073x_chan_mode_supports_tie(chan))
+		return -EOPNOTSUPP;
+
+	return zl3073x_chan_tie_write(zldev, zldpll->id, delta);
+}
+
+static s32
+zl3073x_dpll_ptp_getmaxphase(struct ptp_clock_info *info __always_unused)
+{
+	/* HW limits TIE write to +-1 second. Return the constant HW
+	 * limit and let adjphase handle mode-specific checks.
+	 */
+	return NSEC_PER_SEC - 1;
+}
+
+static const struct ptp_clock_info zl3073x_dpll_ptp_clock_info = {
+	.owner		= THIS_MODULE,
+	.max_adj	= ZL3073X_DPLL_PTP_MAX_ADJ,
+	.gettimex64	= zl3073x_dpll_ptp_gettimex64,
+	.settime64	= zl3073x_dpll_ptp_settime64,
+	.adjtime	= zl3073x_dpll_ptp_adjtime,
+	.adjfine	= zl3073x_dpll_ptp_adjfine,
+	.adjphase	= zl3073x_dpll_ptp_adjphase,
+	.getmaxphase	= zl3073x_dpll_ptp_getmaxphase,
+};
+
+/**
+ * zl3073x_dpll_ptp_register - register PTP clock for a DPLL channel
+ * @zldpll: DPLL channel to register PTP clock for
+ *
+ * Return: 0 on success, <0 on error
+ */
+static int zl3073x_dpll_ptp_register(struct zl3073x_dpll *zldpll)
+{
+	struct zl3073x_dev *zldev = zldpll->dev;
+	struct ptp_clock *ptp_clock;
+
+	snprintf(zldpll->ptp_info.name, sizeof(zldpll->ptp_info.name),
+		 "%s-dpll%u", dev_name(zldev->dev), zldpll->id);
+
+	ptp_clock = ptp_clock_register(&zldpll->ptp_info, zldev->dev);
+	if (IS_ERR(ptp_clock)) {
+		dev_err(zldev->dev, "Failed to register PTP clock for DPLL%u\n",
+			zldpll->id);
+		return PTR_ERR(ptp_clock);
+	}
+
+	zldpll->ptp_clock = ptp_clock;
+
+	return 0;
+}
+
+/**
+ * zl3073x_dpll_ptp_unregister - unregister PTP clock for a DPLL channel
+ * @zldpll: DPLL channel to unregister PTP clock for
+ */
+static void zl3073x_dpll_ptp_unregister(struct zl3073x_dpll *zldpll)
+{
+	if (!IS_ERR_OR_NULL(zldpll->ptp_clock)) {
+		ptp_clock_unregister(zldpll->ptp_clock);
+		zldpll->ptp_clock = NULL;
+	}
 }
 
 /**
@@ -2398,6 +2730,48 @@ zl3073x_dpll_ref_sync_pairs_register(struct zl3073x_dpll *zldpll)
 	return 0;
 }
 
+/**
+ * zl3073x_dpll_alloc - allocate DPLL device
+ * @zldev: pointer to zl3073x device
+ * @ch: DPLL channel number
+ *
+ * Allocates DPLL device structure for given DPLL channel.
+ *
+ * Return: pointer to DPLL device on success, error pointer on error
+ */
+struct zl3073x_dpll *
+zl3073x_dpll_alloc(struct zl3073x_dev *zldev, u8 ch)
+{
+	struct zl3073x_dpll *zldpll;
+
+	zldpll = kzalloc_obj(*zldpll);
+	if (!zldpll)
+		return ERR_PTR(-ENOMEM);
+
+	zldpll->dev = zldev;
+	zldpll->id = ch;
+	zldpll->ptp_info = zl3073x_dpll_ptp_clock_info;
+	mutex_init(&zldpll->lock);
+	INIT_LIST_HEAD(&zldpll->pins);
+
+	return zldpll;
+}
+
+/**
+ * zl3073x_dpll_free - free DPLL device
+ * @zldpll: pointer to zl3073x_dpll structure
+ *
+ * Deallocates given DPLL device previously allocated by @zl3073x_dpll_alloc.
+ */
+void
+zl3073x_dpll_free(struct zl3073x_dpll *zldpll)
+{
+	WARN(zldpll->dpll_dev, "DPLL device is still registered\n");
+
+	mutex_destroy(&zldpll->lock);
+	kfree(zldpll);
+}
+
 /**
  * zl3073x_dpll_register - register DPLL device and all its pins
  * @zldpll: pointer to zl3073x_dpll structure
@@ -2428,6 +2802,13 @@ zl3073x_dpll_register(struct zl3073x_dpll *zldpll)
 		return rc;
 	}
 
+	rc = zl3073x_dpll_ptp_register(zldpll);
+	if (rc) {
+		zl3073x_dpll_pins_unregister(zldpll);
+		zl3073x_dpll_device_unregister(zldpll);
+		return rc;
+	}
+
 	return 0;
 }
 
@@ -2441,7 +2822,7 @@ zl3073x_dpll_register(struct zl3073x_dpll *zldpll)
 void
 zl3073x_dpll_unregister(struct zl3073x_dpll *zldpll)
 {
-	/* Unregister all pins and dpll */
+	zl3073x_dpll_ptp_unregister(zldpll);
 	zl3073x_dpll_pins_unregister(zldpll);
 	zl3073x_dpll_device_unregister(zldpll);
 }
diff --git a/drivers/dpll/zl3073x/dpll.h b/drivers/dpll/zl3073x/dpll.h
index faebc402ba1b7..993221dc63249 100644
--- a/drivers/dpll/zl3073x/dpll.h
+++ b/drivers/dpll/zl3073x/dpll.h
@@ -5,6 +5,7 @@
 
 #include <linux/dpll.h>
 #include <linux/list.h>
+#include <linux/ptp_clock_kernel.h>
 
 #include "core.h"
 
@@ -22,6 +23,8 @@
  * @type: DPLL type (PPS or EEC)
  * @lock_status: last saved DPLL lock status
  * @pins: list of pins
+ * @ptp_info: PTP clock info
+ * @ptp_clock: registered PTP clock (or NULL)
  */
 struct zl3073x_dpll {
 	struct list_head		list;
@@ -36,6 +39,8 @@ struct zl3073x_dpll {
 	enum dpll_type			type;
 	enum dpll_lock_status		lock_status;
 	struct list_head		pins;
+	struct ptp_clock_info		ptp_info;
+	struct ptp_clock		*ptp_clock;
 };
 
 struct zl3073x_dpll *zl3073x_dpll_alloc(struct zl3073x_dev *zldev, u8 ch);
diff --git a/drivers/dpll/zl3073x/regs.h b/drivers/dpll/zl3073x/regs.h
index b70ead7d4495b..f3a5e1215aa36 100644
--- a/drivers/dpll/zl3073x/regs.h
+++ b/drivers/dpll/zl3073x/regs.h
@@ -179,6 +179,20 @@
 #define ZL_DPLL_DF_READ_CMD			GENMASK(2, 0)
 #define ZL_DPLL_DF_READ_CMD_ACC_I		4
 
+#define ZL_REG_DPLL_TIE_CTRL			ZL_REG(5, 0x30, 1)
+#define ZL_DPLL_TIE_CTRL_OP			GENMASK(2, 0)
+#define ZL_DPLL_TIE_CTRL_OP_WR			4
+
+#define ZL_REG_DPLL_TIE_CTRL_MASK		ZL_REG(5, 0x31, 1)
+
+#define ZL_REG_DPLL_TOD_CTRL(_idx)					\
+	ZL_REG_IDX(_idx, 5, 0x38, 1, ZL3073X_MAX_CHANNELS, 1)
+#define ZL_DPLL_TOD_CTRL_SEM			BIT(4)
+#define ZL_DPLL_TOD_CTRL_CMD			GENMASK(3, 0)
+#define ZL_DPLL_TOD_CTRL_CMD_WR_NEXT_1HZ	1
+#define ZL_DPLL_TOD_CTRL_CMD_RD_CURRENT		8
+#define ZL_DPLL_TOD_CTRL_CMD_RD_NEXT_1HZ	9
+
 #define ZL_REG_DPLL_MEAS_CTRL			ZL_REG(5, 0x50, 1)
 #define ZL_DPLL_MEAS_CTRL_EN			BIT(0)
 #define ZL_DPLL_MEAS_CTRL_AVG_FACTOR		GENMASK(7, 4)
@@ -193,6 +207,9 @@
 
 /*******************************
  * Register Pages 6-7, DPLL Data
+ *
+ * Per-channel registers with stride 0x20. Channels 0-3 reside on page 6,
+ * channel 4 on page 7.
  *******************************/
 
 #define ZL_REG_DPLL_DF_OFFSET_03(_idx)					\
@@ -202,6 +219,24 @@
 	((_idx) < 4 ? ZL_REG_DPLL_DF_OFFSET_03(_idx) : ZL_REG_DPLL_DF_OFFSET_4)
 #define ZL_DPLL_DF_OFFSET_UNKNOWN	S64_MIN
 
+#define ZL_REG_DPLL_TIE_DATA_03(_idx)					\
+	ZL_REG_IDX(_idx, 6, 0x0C, 6, 4, 0x20)
+#define ZL_REG_DPLL_TIE_DATA_4			ZL_REG(7, 0x0C, 6)
+#define ZL_REG_DPLL_TIE_DATA(_idx)					\
+	((_idx) < 4 ? ZL_REG_DPLL_TIE_DATA_03(_idx) : ZL_REG_DPLL_TIE_DATA_4)
+
+#define ZL_REG_DPLL_TOD_SEC_03(_idx)					\
+	ZL_REG_IDX(_idx, 6, 0x12, 6, 4, 0x20)
+#define ZL_REG_DPLL_TOD_SEC_4			ZL_REG(7, 0x12, 6)
+#define ZL_REG_DPLL_TOD_SEC(_idx)					\
+	((_idx) < 4 ? ZL_REG_DPLL_TOD_SEC_03(_idx) : ZL_REG_DPLL_TOD_SEC_4)
+
+#define ZL_REG_DPLL_TOD_NS_03(_idx)					\
+	ZL_REG_IDX(_idx, 6, 0x18, 4, 4, 0x20)
+#define ZL_REG_DPLL_TOD_NS_4			ZL_REG(7, 0x18, 4)
+#define ZL_REG_DPLL_TOD_NS(_idx)					\
+	((_idx) < 4 ? ZL_REG_DPLL_TOD_NS_03(_idx) : ZL_REG_DPLL_TOD_NS_4)
+
 /***********************************
  * Register Page 9, Synth and Output
  ***********************************/
@@ -221,6 +256,23 @@
 #define ZL_OUTPUT_CTRL_EN			BIT(0)
 #define ZL_OUTPUT_CTRL_SYNTH_SEL		GENMASK(6, 4)
 
+#define ZL_REG_OUTPUT_STEP_TIME_MASK		ZL_REG(9, 0x36, 2)
+
+#define ZL_REG_OUTPUT_PHASE_STEP_CTRL		ZL_REG(9, 0x38, 1)
+#define ZL_OUTPUT_PHASE_STEP_CTRL_DPLL		GENMASK(6, 4)
+#define ZL_OUTPUT_PHASE_STEP_CTRL_TOD_STEP	BIT(3)
+#define ZL_OUTPUT_PHASE_STEP_CTRL_OP		GENMASK(1, 0)
+#define ZL_OUTPUT_PHASE_STEP_CTRL_OP_NONE	0
+#define ZL_OUTPUT_PHASE_STEP_CTRL_OP_RESET	1
+#define ZL_OUTPUT_PHASE_STEP_CTRL_OP_READ	2
+#define ZL_OUTPUT_PHASE_STEP_CTRL_OP_WRITE	3
+
+#define ZL_REG_OUTPUT_PHASE_STEP_NUMBER		ZL_REG(9, 0x39, 1)
+
+#define ZL_REG_OUTPUT_PHASE_STEP_MASK		ZL_REG(9, 0x3a, 2)
+
+#define ZL_REG_OUTPUT_PHASE_STEP_DATA		ZL_REG(9, 0x3c, 4)
+
 /*******************************
  * Register Page 10, Ref Mailbox
  *******************************/

3/2 2026/08/07 11:52 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 18112
							output: 30
							thoughts: 1604
						

Thoughts:
**Reaching the PTP Clock Support in the Microchip Azurite DPLL Driver**

Okay, so we have this patch adding PTP clock support to the Microchip Azurite DPLL driver (`drivers/dpll/zl3073x/`). My immediate thought is, can we actually *reach* this code and what's involved in getting it running?

First, the patch modifies `Kconfig` to depend on `PTP_1588_CLOCK`, which makes sense. It's adding functions to `chan.c` for Time of Day (ToD) read/write/adjust, TIE write, and phase stepping, and it registers a PTP clock in `dpll.c` using `ptp_clock_register`. That's a pretty significant addition – it's definitely something worth considering for fuzzing.

The driver is for Microchip Azurite DPLL/PTP/SyncE devices, and it uses I2C/SPI.  Can we get to this in QEMU, or any other virtual environment?  I2C/SPI drivers can usually be instantiated via device tree, sysfs, or even an emulator.  Even without an emulator, we can probe it via sysfs. I can, for example, instantiate the I2C device via `echo zl3073x 0x10 > /sys/bus/i2c/devices/i2c-0/new_device`. Wait a second! Syzkaller! Syzkaller can fuzz I2C and SPI devices using `syz_i2c` and `syz_spi` or by instantiating them!  That's perfect.  So, the added PTP clock interface is exposed to userspace via `/dev/ptpX`. Syzkaller can easily interact with that.  The driver itself leverages regmap over I2C/SPI.

Let's dive into the specifics of this PTP clock interface. The core functions I need to focus on are:

*   `zl3073x_dpll_ptp_gettimex64`
*   `zl3073x_dpll_ptp_settime64`
*   `zl3073x_dpll_ptp_adjtime`
*   `zl3073x_dpll_ptp_adjfine`
*   `zl3073x_dpll_ptp_adjphase`
*   `zl3073x_chan_tod_read`
*   `zl3073x_chan_tod_write`
*   `zl3073x_chan_tod_adjust`
*   `zl3073x_chan_df_offset_set`
*   `zl3073x_chan_tie_write`
*   `zl3073x_chan_phase_step`

These functions are hooked into `ptp_clock_info`, which is a standard approach.  The `ptp_clock_info` struct itself looks normal, standard PTP clock ops.

We need to make sure we've got all the necessary configs enabled. It looks like we need `ZL3073X_I2C`, `ZL3073X_SPI`, `PTP_1588_CLOCK`, `DPLL`, `I2C`, `SPI`, and `NET`.  A quick look at `drivers/dpll/zl3073x/Kconfig` confirms these dependencies. `ZL3073X` is selected by both `ZL3073X_I2C` and `ZL3073X_SPI`, so enabling either of them will enable the whole thing.

Given that Syzkaller can emulate I2C and SPI devices, we have a clear path to reach the code. The critical functions now are the PTP clock functions, as well as the channel functions that affect ToD, phase adjustments, and TIE writes. I'm going to run a quick grep to make sure those functions are definitely there and defined, just to be absolutely certain before going any further.



4/2 2026/08/07 11:53 tool grepper
0m Args:
{
  "Expression": "zl3073x_dpll_ptp_adjtime"
}

Results:
{
  "Output": "drivers/dpll/zl3073x/dpll.c=2310=static int zl3073x_dpll_ptp_settime64(struct ptp_clock_info *info,\n--\ndrivers/dpll/zl3073x/dpll.c-2321-/**\ndrivers/dpll/zl3073x/dpll.c:2322: * zl3073x_dpll_ptp_adjtime_phase_step - adjust sub-second time via phase step\ndrivers/dpll/zl3073x/dpll.c-2323- * @zldpll: DPLL channel\n--\ndrivers/dpll/zl3073x/dpll.c-2339- */\ndrivers/dpll/zl3073x/dpll.c:2340:static int zl3073x_dpll_ptp_adjtime_phase_step(struct zl3073x_dpll *zldpll,\ndrivers/dpll/zl3073x/dpll.c-2341-\t\t\t\t\t       s64 delta)\n--\ndrivers/dpll/zl3073x/dpll.c-2423-/**\ndrivers/dpll/zl3073x/dpll.c:2424: * zl3073x_dpll_ptp_adjtime - adjust PTP clock time\ndrivers/dpll/zl3073x/dpll.c-2425- * @info: PTP clock info\n--\ndrivers/dpll/zl3073x/dpll.c-2437- */\ndrivers/dpll/zl3073x/dpll.c:2438:static int zl3073x_dpll_ptp_adjtime(struct ptp_clock_info *info, s64 delta)\ndrivers/dpll/zl3073x/dpll.c-2439-{\n--\ndrivers/dpll/zl3073x/dpll.c-2491-\tif (zl3073x_chan_mode_is_nco(chan)) {\ndrivers/dpll/zl3073x/dpll.c:2492:\t\trc = zl3073x_dpll_ptp_adjtime_phase_step(zldpll, delta);\ndrivers/dpll/zl3073x/dpll.c-2493-\t\tif (!rc)\n--\ndrivers/dpll/zl3073x/dpll.c=2602=static const struct ptp_clock_info zl3073x_dpll_ptp_clock_info = {\n--\ndrivers/dpll/zl3073x/dpll.c-2606-\t.settime64\t= zl3073x_dpll_ptp_settime64,\ndrivers/dpll/zl3073x/dpll.c:2607:\t.adjtime\t= zl3073x_dpll_ptp_adjtime,\ndrivers/dpll/zl3073x/dpll.c-2608-\t.adjfine\t= zl3073x_dpll_ptp_adjfine,\n"
}

5/2 2026/08/07 11:53 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 21534
							output: 26
							thoughts: 24
						

Thoughts:
Okay, here's my take on those thoughts, framed as an expert's internal monologue:

**Checking the ZL3073X Channel Time-of-Day Adjustment Function**

Right, the building blocks are in place. The core functionalities I need are available. Now, let's drill down into the details. I need to examine the `zl3073x_chan_tod_adjust` function. This is where the time-of-day adjustments for each channel are handled, which is critical. I'll need to understand the algorithm, the data structures it manipulates, and the potential error conditions. I need to consider things like how it handles leap seconds, time zone offsets, and any clock synchronization issues that might impact the final TOD calculation. This is the heart of the timing solution. Let's dig in and make sure it is robust and precise.



6/2 2026/08/07 11:53 tool grepper
0m Args:
{
  "Expression": "zl3073x_chan_tod_adjust"
}

Results:
{
  "Output": "drivers/dpll/zl3073x/chan.c=363=int zl3073x_chan_tod_write(struct zl3073x_dev *zldev, u8 ch,\n--\ndrivers/dpll/zl3073x/chan.c-385-/**\ndrivers/dpll/zl3073x/chan.c:386: * zl3073x_chan_tod_adjust - atomic ToD read-modify-write with rollover guard\ndrivers/dpll/zl3073x/chan.c-387- * @zldev: pointer to zl3073x device\n--\ndrivers/dpll/zl3073x/chan.c-400-#define ZL_TOD_MAX_RETRIES\t20\ndrivers/dpll/zl3073x/chan.c:401:int zl3073x_chan_tod_adjust(struct zl3073x_dev *zldev, u8 ch,\ndrivers/dpll/zl3073x/chan.c-402-\t\t\t    struct timespec64 delta)\n--\ndrivers/dpll/zl3073x/chan.h=51=int zl3073x_chan_tod_write(struct zl3073x_dev *zldev, u8 ch,\ndrivers/dpll/zl3073x/chan.h-52-\t\t\t   struct timespec64 ts);\ndrivers/dpll/zl3073x/chan.h:53:int zl3073x_chan_tod_adjust(struct zl3073x_dev *zldev, u8 ch,\ndrivers/dpll/zl3073x/chan.h-54-\t\t\t    struct timespec64 delta);\n--\ndrivers/dpll/zl3073x/dpll.c=2438=static int zl3073x_dpll_ptp_adjtime(struct ptp_clock_info *info, s64 delta)\n--\ndrivers/dpll/zl3073x/dpll.c-2456-\t    !zl3073x_chan_mode_supports_tie(chan))\ndrivers/dpll/zl3073x/dpll.c:2457:\t\treturn zl3073x_chan_tod_adjust(zldev, zldpll-\u003eid,\ndrivers/dpll/zl3073x/dpll.c-2458-\t\t\t\t\t       ns_to_timespec64(delta));\n--\ndrivers/dpll/zl3073x/dpll.c-2470-\ndrivers/dpll/zl3073x/dpll.c:2471:\t\trc = zl3073x_chan_tod_adjust(zldev, zldpll-\u003eid, ts);\ndrivers/dpll/zl3073x/dpll.c-2472-\t\tif (rc)\n--\ndrivers/dpll/zl3073x/dpll.c-2501-\t/* Phase step or TIE write failed, fall back to ToD adjust */\ndrivers/dpll/zl3073x/dpll.c:2502:\trc = zl3073x_chan_tod_adjust(zldev, zldpll-\u003eid,\ndrivers/dpll/zl3073x/dpll.c-2503-\t\t\t\t     ns_to_timespec64(delta));\n"
}

7/2 2026/08/07 11:53 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 22676
							output: 22
							thoughts: 430
						

Thoughts:
**Analyzing the Microchip Azurite DPLL Driver Patch for PTP Clock Functionality**

Okay, so this patch introduces PTP clock support into the Microchip Azurite DPLL driver, interesting. The driver itself is fairly standard; it can be hooked up via either I2C or SPI, good to know. The key thing is that this PTP clock functionality is exposed to userspace, which means we can likely poke at it directly through `/dev/ptpX` devices and the associated ioctls. That opens up a whole avenue for fuzzing with syzkaller, of course.

I need to zero in on specific functions within this driver. Let's see... the most critical ones seem to be: `zl3073x_chan_tod_read` and `zl3073x_chan_tod_write` for Time of Day reads and writes; `zl3073x_chan_tod_adjust`, which is probably for corrections.  Then there's `zl3073x_chan_df_offset_set`, `zl3073x_chan_tie_write`, and `zl3073x_chan_phase_step` – those sound like they deal with finer-grained adjustments.

And, of course, the DPLL-specific PTP functions are crucial: `zl3073x_dpll_ptp_gettimex64`, `zl3073x_dpll_ptp_settime64`, `zl3073x_dpll_ptp_adjtime_phase_step`, `zl3073x_dpll_ptp_adjtime`, `zl3073x_dpll_ptp_adjfine`, and `zl3073x_dpll_ptp_adjphase`. These are all prime candidates for closer inspection and potential fuzzing targets.

Now, let's nail down the configuration options needed to enable this functionality. I see `ZL3073X_I2C`, `ZL3073X_SPI`, `PTP_1588_CLOCK`, and `DPLL`. Wait, hold on. It looks like `DPLL` is automatically selected if `ZL3073X` is selected. And `PTP_1588_CLOCK` is a dependency. Okay, so to get this all working, it looks like all that I need are:

`ZL3073X_I2C` or `ZL3073X_SPI`, and `PTP_1588_CLOCK`. Let me just double check the Kconfig file in `drivers/dpll/zl3073x/Kconfig` to be absolutely sure. Gotta be thorough with this stuff.



8/2 2026/08/07 11:53 tool grepper
0m Args:
{
  "Expression": "config ZL3073X"
}

Results:
{
  "Output": "drivers/dpll/zl3073x/Kconfig-2-\ndrivers/dpll/zl3073x/Kconfig:3:config ZL3073X\ndrivers/dpll/zl3073x/Kconfig-4-\ttristate \"Microchip Azurite DPLL/PTP/SyncE devices\" if COMPILE_TEST\n--\ndrivers/dpll/zl3073x/Kconfig-16-\ndrivers/dpll/zl3073x/Kconfig:17:config ZL3073X_I2C\ndrivers/dpll/zl3073x/Kconfig-18-\ttristate \"I2C bus implementation for Microchip Azurite devices\"\n--\ndrivers/dpll/zl3073x/Kconfig-28-\ndrivers/dpll/zl3073x/Kconfig:29:config ZL3073X_SPI\ndrivers/dpll/zl3073x/Kconfig-30-\ttristate \"SPI bus implementation for Microchip Azurite devices\"\n"
}

9/2 2026/08/07 11:53 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 24023
							output: 41
							thoughts: 23
						

Thoughts:
Okay, let's dive into this. Here's my thought process on checking the `drivers/dpll/zl3073x/Kconfig` dependencies:

**Analyzing the Kconfig Dependencies for zl3073x Driver**

Right, so I need to understand the configuration dependencies for the zl3073x driver. Specifically, I'm going to look at the `drivers/dpll/zl3073x/Kconfig` file. I'm assuming, as an expert in this, that this Kconfig file contains the configuration options and dependencies for enabling and disabling the zl3073x driver within the kernel's build system. My goal is to determine which other kernel features or hardware components must be enabled or configured before this particular driver can be selected. This is critical for understanding the overall system configuration and to ensure I don't introduce build errors or runtime conflicts. I'll need to identify: conditional dependencies (e.g., "if" statements), default values, select statements, and any other relevant configurations that relate to the proper building and use of the zl3073x driver.



10/2 2026/08/07 11:53 tool read-file
0m Args:
{
  "File": "drivers/dpll/zl3073x/Kconfig",
  "FirstLine": 1,
  "LineCount": 40
}

Results:
{
  "Contents": "   1:\t# SPDX-License-Identifier: GPL-2.0-only\n   2:\t\n   3:\tconfig ZL3073X\n   4:\t\ttristate \"Microchip Azurite DPLL/PTP/SyncE devices\" if COMPILE_TEST\n   5:\t\tdepends on NET \u0026\u0026 PTP_1588_CLOCK\n   6:\t\tselect DPLL\n   7:\t\tselect NET_DEVLINK\n   8:\t\tselect REGMAP\n   9:\t\thelp\n  10:\t\t  This driver supports Microchip Azurite family DPLL/PTP/SyncE\n  11:\t\t  devices that support up to 5 independent DPLL channels,\n  12:\t\t  10 input pins and up to 20 output pins.\n  13:\t\n  14:\t\t  To compile this driver as a module, choose M here. The module\n  15:\t\t  will be called zl3073x.\n  16:\t\n  17:\tconfig ZL3073X_I2C\n  18:\t\ttristate \"I2C bus implementation for Microchip Azurite devices\"\n  19:\t\tdepends on I2C \u0026\u0026 NET \u0026\u0026 PTP_1588_CLOCK\n  20:\t\tselect REGMAP_I2C\n  21:\t\tselect ZL3073X\n  22:\t\thelp\n  23:\t\t  This is I2C bus implementation for Microchip Azurite DPLL/PTP/SyncE\n  24:\t\t  devices.\n  25:\t\n  26:\t\t  To compile this driver as a module, choose M here: the module will\n  27:\t\t  be called zl3073x_i2c.\n  28:\t\n  29:\tconfig ZL3073X_SPI\n  30:\t\ttristate \"SPI bus implementation for Microchip Azurite devices\"\n  31:\t\tdepends on NET \u0026\u0026 SPI \u0026\u0026 PTP_1588_CLOCK\n  32:\t\tselect REGMAP_SPI\n  33:\t\tselect ZL3073X\n  34:\t\thelp\n  35:\t\t  This is SPI bus implementation for Microchip Azurite DPLL/PTP/SyncE\n  36:\t\t  devices.\n  37:\t\n  38:\t\t  To compile this driver as a module, choose M here: the module will\n  39:\t\t  be called zl3073x_spi.\n"
}

11/2 2026/08/07 11:53 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 24934
							output: 32
							thoughts: 309
						

Thoughts:
**My Focus: ZL3073X Clock Control**

Okay, so I need to enable the `ZL3073X_I2C` and `ZL3073X_SPI` interfaces. The `PTP_1588_CLOCK` feature is definitely necessary too. And since we're dealing with clock management, I'll go with the `DPLL` selection.

Now, let's nail down the key functions I should concentrate on for this implementation. I'm thinking:

*   `zl3073x_chan_tod_read`
*   `zl3073x_chan_tod_write`
*   `zl3073x_chan_tod_adjust`
*   `zl3073x_chan_df_offset_set`
*   `zl3073x_chan_tie_write`
*   `zl3073x_chan_phase_step`

Hold on a second...I see these static functions related to DPLL and PTP:

*   `zl3073x_dpll_ptp_adjtime_phase_step`
*   `zl3073x_dpll_ptp_adjtime`
*   `zl3073x_dpll_ptp_adjfine`
*   `zl3073x_dpll_ptp_adjphase`
*   `zl3073x_dpll_ptp_gettimex64`
*   `zl3073x_dpll_ptp_settime64`

Right, I'd better double-check that they are actually static - I need to know why they are written that way.



12/2 2026/08/07 11:53 tool grepper
0m Args:
{
  "Expression": "static int zl3073x_dpll_ptp_adjtime"
}

Results:
{
  "Output": "drivers/dpll/zl3073x/dpll.c=2310=static int zl3073x_dpll_ptp_settime64(struct ptp_clock_info *info,\n--\ndrivers/dpll/zl3073x/dpll.c-2339- */\ndrivers/dpll/zl3073x/dpll.c:2340:static int zl3073x_dpll_ptp_adjtime_phase_step(struct zl3073x_dpll *zldpll,\ndrivers/dpll/zl3073x/dpll.c-2341-\t\t\t\t\t       s64 delta)\n--\ndrivers/dpll/zl3073x/dpll.c-2437- */\ndrivers/dpll/zl3073x/dpll.c:2438:static int zl3073x_dpll_ptp_adjtime(struct ptp_clock_info *info, s64 delta)\ndrivers/dpll/zl3073x/dpll.c-2439-{\n"
}

13/2 2026/08/07 11:53 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 25978
							output: 27
							thoughts: 130
						

Thoughts:
**Focusing on Dynamic Channel Functions**

Okay, so I'm dealing with `chan.c`, and I've quickly confirmed that the majority of its functions are, as expected, static. That's good; it narrows down my focus. Now, I need to understand the dynamics, the parts that *do* change, or that are designed to be modified.

Therefore, I'm going to concentrate on these non-static functions within `chan.c`: `zl3073x_chan_tod_read`, `zl3073x_chan_tod_write`, `zl3073x_chan_tod_adjust`, `zl3073x_chan_df_offset_set`, `zl3073x_chan_tie_write`, and `zl3073x_chan_phase_step`. These seem to be the key areas for me to investigate, given the context.

My next step is to quickly verify whether these functions are properly exported and visible for the external interface, or if there's any subtlety in their definition that I need to account for. No point in wasting time if they're not even accessible.



14/2 2026/08/07 11:54 tool grepper
0m Args:
{
  "Expression": "int zl3073x_chan_tod_read"
}

Results:
{
  "Output": "drivers/dpll/zl3073x/chan.c=232=const struct zl3073x_chan *zl3073x_chan_state_get(struct zl3073x_dev *zldev,\n--\ndrivers/dpll/zl3073x/chan.c-247- */\ndrivers/dpll/zl3073x/chan.c:248:int zl3073x_chan_tod_ready_wait(struct zl3073x_dev *zldev, u8 ch)\ndrivers/dpll/zl3073x/chan.c-249-{\n--\ndrivers/dpll/zl3073x/chan.c=293=static int zl3073x_chan_tod_ctrl(struct zl3073x_dev *zldev, u8 ch, u8 cmd)\n--\ndrivers/dpll/zl3073x/chan.c-309- */\ndrivers/dpll/zl3073x/chan.c:310:int zl3073x_chan_tod_read(struct zl3073x_dev *zldev, u8 ch,\ndrivers/dpll/zl3073x/chan.c-311-\t\t\t  bool next_hz, struct timespec64 *ts,\n--\ndrivers/dpll/zl3073x/chan.h=45=int zl3073x_chan_nco_mode_set(struct zl3073x_dev *zldev, u8 index);\ndrivers/dpll/zl3073x/chan.h-46-\ndrivers/dpll/zl3073x/chan.h:47:int zl3073x_chan_tod_ready_wait(struct zl3073x_dev *zldev, u8 ch);\ndrivers/dpll/zl3073x/chan.h:48:int zl3073x_chan_tod_read(struct zl3073x_dev *zldev, u8 ch,\ndrivers/dpll/zl3073x/chan.h-49-\t\t\t  bool next_hz, struct timespec64 *ts,\n"
}

15/2 2026/08/07 11:54 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 26963
							output: 251
							thoughts: 765
						

Thoughts:
**My Analysis of the Azurite DPLL Driver Integration**

Okay, let's break this down. First, I need to confirm the focus symbols – the key functions we're interested in. I've got:

*   `zl3073x_chan_tod_ready_wait`
*   `zl3073x_chan_tod_read`
*   `zl3073x_chan_tod_write`
*   `zl3073x_chan_tod_adjust`
*   `zl3073x_chan_df_offset_set`
*   `zl3073x_chan_tie_write`
*   `zl3073x_chan_phase_step`

I quickly confirmed that `zl3073x_chan_tod_ready_wait` is non-static as required. I also confirmed the key configs. The code looks correct, so the focus symbols are correct.

Now, the configurations. I need to make sure we're covering all bases. We have:

*   `ZL3073X_I2C`
*   `ZL3073X_SPI`
*   `PTP_1588_CLOCK`

I checked `PTP_1588_CLOCK`, and it's a tristate, so that's good. It will enable the correct code path as intended.

I also considered `DPLL`. It seems `DPLL` is selected by `ZL3073X`, so it should be automatically enabled, but including it doesn't hurt. And, really, the `select` directive in the config handles it, so we don't need to specify it.

I did a quick check for other dependencies, and we've got `I2C`, `SPI`, and `NET`. `NET` is usually enabled, and `I2C` and `SPI` are required for this driver.

So, the config list is solid:

*   `ZL3073X_I2C`
*   `ZL3073X_SPI`
*   `PTP_1588_CLOCK`

I verified no other functions are relevant besides the focus symbols. `zl3073x_dpll_ptp_register` and `zl3073x_dpll_ptp_unregister` are static, so they're not part of the fuzzing surface, which is exactly what we want.

In essence, this patch is adding PTP clock functionality to the Microchip Azurite DPLL driver. It's accessible via I2C/SPI and exposed to userspace via the PTP clock subsystem (`/dev/ptpX`). Syzkaller will be able to fuzz this through I2C/SPI emulation. The focus symbols are correct, and all dependencies are appropriately covered. I think this is good to go. Time to `set-results`.



16/2 2026/08/07 11:54 tool set-results
0m Args:
{
  "EnableConfigs": [
    "ZL3073X_I2C",
    "ZL3073X_SPI",
    "PTP_1588_CLOCK"
  ],
  "FocusSymbols": [
    "zl3073x_chan_tod_ready_wait",
    "zl3073x_chan_tod_read",
    "zl3073x_chan_tod_write",
    "zl3073x_chan_tod_adjust",
    "zl3073x_chan_df_offset_set",
    "zl3073x_chan_tie_write",
    "zl3073x_chan_phase_step"
  ],
  "Reasoning": "The patch adds PTP clock support to the Microchip Azurite DPLL driver (zl3073x). The driver can be instantiated via I2C or SPI, and the PTP clock interface is exposed to userspace via `/dev/ptpX`. Syzkaller can fuzz this functionality using I2C/SPI emulation and PTP ioctls.",
  "WorthFuzzing": true
}

Results:
{
  "EnableConfigs": [
    "ZL3073X_I2C",
    "ZL3073X_SPI",
    "PTP_1588_CLOCK"
  ],
  "FocusSymbols": [
    "zl3073x_chan_tod_ready_wait",
    "zl3073x_chan_tod_read",
    "zl3073x_chan_tod_write",
    "zl3073x_chan_tod_adjust",
    "zl3073x_chan_df_offset_set",
    "zl3073x_chan_tie_write",
    "zl3073x_chan_phase_step"
  ],
  "Reasoning": "The patch adds PTP clock support to the Microchip Azurite DPLL driver (zl3073x). The driver can be instantiated via I2C or SPI, and the PTP clock interface is exposed to userspace via `/dev/ptpX`. Syzkaller can fuzz this functionality using I2C/SPI emulation and PTP ioctls.",
  "WorthFuzzing": true
}

17/1 2026/08/07 11:54 action If
0m Args:
{
  "WorthFuzzing": true
}

Results:
null

18/2 2026/08/07 11:54 agent kmsan-evaluator
0m Model:
best-expensive

Args:
null

Results:
{
  "KMSANReasoning": "The patch introduces PTP clock support for the Microchip Azurite DPLL driver. It adds functions to read/write Time of Day (ToD) registers, adjust frequency, and adjust phase, as well as registering a PTP clock device. All local variables, such as `struct timespec64` and `synth_mask`, are fully initialized before use. The `ptp_clock_info` structure is initialized from a static const template, ensuring all fields (including padding and the `name` buffer) are zero-initialized before `snprintf` is called. There are no kernel-to-user info-leaks or uninitialized memory usages introduced by these changes. Any potential memory corruption, out-of-bounds accesses, or use-after-free bugs would be effectively caught by KASAN. Therefore, a dedicated KMSAN fuzzing session is not required.",
  "NeedsKMSAN": false
}

Instruction:
You are an expert Linux kernel security engineer specializing in kernel memory
error detectors (KASAN and KMSAN). Your job is to review the provided patch series and
determine if the code changes justify spawning a dedicated KMSAN (KernelMemorySanitizer)
fuzzing session in addition to standard KASAN fuzzing.

CRITICAL DISTINCTION BETWEEN KASAN AND KMSAN:
- Standard KASAN kernel builds (upstream-apparmor-kasan.config) already enable
  a comprehensive suite of debugging tools and sanitizers, including KASAN
  (out-of-bounds accesses, use-after-free, double free, invalid free), LOCKDEP
  (locking bugs and deadlocks), UB-sanitizers, and memory corruption checks.
- KMSAN (KernelMemorySanitizer) detects reads of UNINITIALIZED memory (stack, heap,
  or page allocations) and kernel-to-user memory info-leaks.

Rule: THERE IS NO SENSE IN RUNNING A KMSAN SESSION IF A BUG CAN BE CAUGHT BY KASAN,
LOCKDEP, OR OTHER STANDARD BUG DETECTORS.
A dedicated KMSAN fuzzing session incurs significant resource costs. You must ONLY
set NeedsKMSAN=true if the code changes introduce or expose UNINITIALIZED MEMORY risks
that are detected ONLY by KMSAN.

Look holistically at the patch series and surrounding code. Even if no direct
uninitialized field accesses or new buffer allocations are added in the diff itself,
a patch may alter control flow, bounds checking, or data length calculations in ways
that change how the rest of the code operates on existing buffers (e.g. allowing
uninitialized stack/heap memory to be read, copied to user space, or used in control
flow). Do not hesitate to use your code access tools to inspect the surrounding code,
called functions, and callers.

Set NeedsKMSAN=true ONLY IF the patch introduces or modifies:
1. Kernel structures sent to user space (via copy_to_user, put_user, netlink skb
   attributes, ioctl output arguments, socket options, or BPF buffers) where fields
   or structure padding might not be fully initialized/zeroed.
2. Conditional logic or branching that depends on potentially uninitialized variables
   or struct fields.
3. Allocation or initialization of complex data structures where uninitialized fields
   could be read later in reachable code paths.
4. Bounds checks, lengths, or logic in a way that allows surrounding code to access
   uninitialized bytes of existing buffers.

Set NeedsKMSAN=false IF:
- The code changes primarily risk out-of-bounds access, array overflows, NULL pointer
  dereferences, locking deadlocks, or use-after-free bugs (these are already caught
  by KASAN, LOCKDEP, or standard bug detectors).
- All stack/heap structures touched or introduced by the patch are fully zeroed
  or initialized (e.g. using = {0}, memset, kzalloc) before being read or copied.
- The patch does not introduce any risk of uninitialized memory usage or info-leaks.

Use your code access tools to inspect the surrounding code if necessary, then provide
detailed KMSANReasoning contrasting KASAN vs KMSAN applicability for this patch.
Prefer calling several tools at the same time to save round-trips.


Use set-results tool to provide results of the analysis.
It must be called exactly once before the final reply.
Ignore results of this tool.

Prompt:
For your convenience, here is the diff of the changes:
commit 591dec83a85d707455260e918e741257e45f9291
Author: syz-cluster <triage@syzkaller.com>
Date:   Fri Aug 7 11:52:46 2026 +0000

    syz-cluster: applied patch under review

diff --git a/drivers/dpll/zl3073x/Kconfig b/drivers/dpll/zl3073x/Kconfig
index 5bbca14005813..da1731ace15f0 100644
--- a/drivers/dpll/zl3073x/Kconfig
+++ b/drivers/dpll/zl3073x/Kconfig
@@ -2,7 +2,7 @@
 
 config ZL3073X
 	tristate "Microchip Azurite DPLL/PTP/SyncE devices" if COMPILE_TEST
-	depends on NET
+	depends on NET && PTP_1588_CLOCK
 	select DPLL
 	select NET_DEVLINK
 	select REGMAP
@@ -16,7 +16,7 @@ config ZL3073X
 
 config ZL3073X_I2C
 	tristate "I2C bus implementation for Microchip Azurite devices"
-	depends on I2C && NET
+	depends on I2C && NET && PTP_1588_CLOCK
 	select REGMAP_I2C
 	select ZL3073X
 	help
@@ -28,7 +28,7 @@ config ZL3073X_I2C
 
 config ZL3073X_SPI
 	tristate "SPI bus implementation for Microchip Azurite devices"
-	depends on NET && SPI
+	depends on NET && SPI && PTP_1588_CLOCK
 	select REGMAP_SPI
 	select ZL3073X
 	help
diff --git a/drivers/dpll/zl3073x/chan.c b/drivers/dpll/zl3073x/chan.c
index 4ec2cf53dad46..af6ca43c33590 100644
--- a/drivers/dpll/zl3073x/chan.c
+++ b/drivers/dpll/zl3073x/chan.c
@@ -3,6 +3,7 @@
 #include <linux/cleanup.h>
 #include <linux/delay.h>
 #include <linux/dev_printk.h>
+#include <linux/ptp_clock_kernel.h>
 #include <linux/string.h>
 #include <linux/types.h>
 
@@ -162,8 +163,8 @@ int zl3073x_chan_nco_mode_set(struct zl3073x_dev *zldev, u8 index)
  * @zldev: pointer to zl3073x_dev structure
  * @index: DPLL channel index to fetch state for
  *
- * Reads the mode_refsel register and reference priority registers for
- * the given DPLL channel and stores the raw values for later use.
+ * Reads the mode_refsel, status and reference priority registers for
+ * the given DPLL channel and stores the values for later use.
  *
  * Return: 0 on success, <0 on error
  */
@@ -234,6 +235,322 @@ const struct zl3073x_chan *zl3073x_chan_state_get(struct zl3073x_dev *zldev,
 	return &zldev->chan[index];
 }
 
+/**
+ * zl3073x_chan_tod_ready_wait - wait for ToD semaphore to clear
+ * @zldev: pointer to zl3073x device
+ * @ch: DPLL channel index
+ *
+ * Polls the ToD control register until the semaphore bit is cleared,
+ * indicating the device has completed the previous ToD operation.
+ *
+ * Return: 0 on success, -EBUSY if semaphore not cleared, <0 on error
+ */
+int zl3073x_chan_tod_ready_wait(struct zl3073x_dev *zldev, u8 ch)
+{
+	unsigned int timeout;
+	u8 tod_ctrl;
+	int rc;
+
+	rc = zl3073x_read_u8(zldev, ZL_REG_DPLL_TOD_CTRL(ch), &tod_ctrl);
+	if (rc)
+		return rc;
+
+	switch (FIELD_GET(ZL_DPLL_TOD_CTRL_CMD, tod_ctrl)) {
+	case 0:
+		/* No operation pending (power-on/reset default) */
+		return 0;
+	case ZL_DPLL_TOD_CTRL_CMD_WR_NEXT_1HZ:
+		timeout = ZL_POLL_TOD_WR_TIMEOUT_US;
+		break;
+	case ZL_DPLL_TOD_CTRL_CMD_RD_CURRENT:
+	case ZL_DPLL_TOD_CTRL_CMD_RD_NEXT_1HZ:
+		timeout = ZL_POLL_TOD_RD_TIMEOUT_US;
+		break;
+	default:
+		/* FW never writes cmd bits; only the driver sets them */
+		WARN_ON(1);
+		return -EOPNOTSUPP;
+	}
+
+	rc = zl3073x_poll_zero_u8(zldev, ZL_REG_DPLL_TOD_CTRL(ch),
+				  ZL_DPLL_TOD_CTRL_SEM, timeout);
+
+	return rc == -ETIMEDOUT ? -EBUSY : rc;
+}
+
+/**
+ * zl3073x_chan_tod_ctrl - issue ToD command
+ * @zldev: pointer to zl3073x device
+ * @ch: DPLL channel index
+ * @cmd: ToD command to execute
+ *
+ * Writes the semaphore and command to dpll_tod_ctrl. The caller must
+ * ensure the device is ready (semaphore clear) before calling and
+ * must wait for completion if needed.
+ *
+ * Return: 0 on success, <0 on error
+ */
+static int zl3073x_chan_tod_ctrl(struct zl3073x_dev *zldev, u8 ch, u8 cmd)
+{
+	return zl3073x_write_u8(zldev, ZL_REG_DPLL_TOD_CTRL(ch),
+				ZL_DPLL_TOD_CTRL_SEM | cmd);
+}
+
+/**
+ * zl3073x_chan_tod_read - read ToD registers after issuing a command
+ * @zldev: pointer to zl3073x device
+ * @ch: DPLL channel index
+ * @next_hz: if true, read predicted ToD at next 1 Hz; otherwise read current
+ * @ts: timespec to store the result
+ * @sts: optional system timestamp pair for cross-timestamping
+ *
+ * Context: Caller must serialize all zl3073x_chan_tod_* calls externally.
+ * Return: 0 on success, <0 on error
+ */
+int zl3073x_chan_tod_read(struct zl3073x_dev *zldev, u8 ch,
+			  bool next_hz, struct timespec64 *ts,
+			  struct ptp_system_timestamp *sts)
+{
+	u32 nsec;
+	u64 sec;
+	u8 cmd;
+	int rc;
+
+	if (next_hz)
+		cmd = ZL_DPLL_TOD_CTRL_CMD_RD_NEXT_1HZ;
+	else
+		cmd = ZL_DPLL_TOD_CTRL_CMD_RD_CURRENT;
+
+	/* Wait for any previous ToD operation to complete */
+	rc = zl3073x_chan_tod_ready_wait(zldev, ch);
+	if (rc)
+		return rc;
+
+	ptp_read_system_prets(sts);
+	rc = zl3073x_chan_tod_ctrl(zldev, ch, cmd);
+	if (rc)
+		return rc;
+
+	rc = zl3073x_chan_tod_ready_wait(zldev, ch);
+	if (rc)
+		return rc;
+	ptp_read_system_postts(sts);
+
+	rc = zl3073x_read_u48(zldev, ZL_REG_DPLL_TOD_SEC(ch), &sec);
+	if (rc)
+		return rc;
+
+	/* HW nanoseconds are always in [0, NSEC_PER_SEC) range */
+	rc = zl3073x_read_u32(zldev, ZL_REG_DPLL_TOD_NS(ch), &nsec);
+	if (rc)
+		return rc;
+
+	ts->tv_sec = sec;
+	ts->tv_nsec = nsec;
+
+	return 0;
+}
+
+/**
+ * zl3073x_chan_tod_write - write ToD registers and trigger 1 Hz update
+ * @zldev: pointer to zl3073x device
+ * @ch: DPLL channel index
+ * @ts: time to set
+ *
+ * Context: Caller must serialize all zl3073x_chan_tod_* calls externally.
+ * Return: 0 on success, <0 on error
+ */
+int zl3073x_chan_tod_write(struct zl3073x_dev *zldev, u8 ch,
+			   struct timespec64 ts)
+{
+	int rc;
+
+	/* Wait for any previous ToD operation to complete */
+	rc = zl3073x_chan_tod_ready_wait(zldev, ch);
+	if (rc)
+		return rc;
+
+	rc = zl3073x_write_u48(zldev, ZL_REG_DPLL_TOD_SEC(ch), ts.tv_sec);
+	if (rc)
+		return rc;
+
+	rc = zl3073x_write_u32(zldev, ZL_REG_DPLL_TOD_NS(ch), ts.tv_nsec);
+	if (rc)
+		return rc;
+
+	return zl3073x_chan_tod_ctrl(zldev, ch,
+				     ZL_DPLL_TOD_CTRL_CMD_WR_NEXT_1HZ);
+}
+
+/**
+ * zl3073x_chan_tod_adjust - atomic ToD read-modify-write with rollover guard
+ * @zldev: pointer to zl3073x device
+ * @ch: DPLL channel index
+ * @delta: time adjustment to apply
+ *
+ * Reads the next-Hz ToD and current ToD, then checks whether enough time
+ * remains before the next 1 Hz rollover to safely complete the write.
+ * Re-reads if the 1 Hz tick crossed between the two reads or if less
+ * than 20 ms remains before the next rollover. Applies @delta and writes
+ * the result back.
+ *
+ * Context: Caller must serialize all zl3073x_chan_tod_* calls externally.
+ * Return: 0 on success, <0 on error
+ */
+#define ZL_TOD_MAX_RETRIES	20
+int zl3073x_chan_tod_adjust(struct zl3073x_dev *zldev, u8 ch,
+			    struct timespec64 delta)
+{
+	static const long threshold_ns = 20 * NSEC_PER_MSEC;
+	struct timespec64 ts_next, ts_cur, diff;
+	int rc, i;
+
+	for (i = 0; i < ZL_TOD_MAX_RETRIES; i++) {
+		rc = zl3073x_chan_tod_read(zldev, ch, true, &ts_next, NULL);
+		if (rc)
+			return rc;
+
+		rc = zl3073x_chan_tod_read(zldev, ch, false, &ts_cur, NULL);
+		if (rc)
+			return rc;
+
+		/* Ensure the 1 Hz tick did not cross between the two reads
+		 * and that enough margin remains to complete the write.
+		 */
+		diff = timespec64_sub(ts_next, ts_cur);
+		if (diff.tv_sec > 0 ||
+		    (!diff.tv_sec && diff.tv_nsec >= threshold_ns))
+			break;
+	}
+	if (i == ZL_TOD_MAX_RETRIES) {
+		dev_warn(zldev->dev,
+			 "DPLL%u ToD adjust failed to get stable margin\n",
+			 ch);
+		return -EBUSY;
+	}
+
+	/* Apply delta to the next-Hz ToD */
+	ts_next = timespec64_add(ts_next, delta);
+	if (!timespec64_valid(&ts_next))
+		return -EINVAL;
+
+	return zl3073x_chan_tod_write(zldev, ch, ts_next);
+}
+
+/**
+ * zl3073x_chan_df_offset_set - write delta frequency offset to hardware
+ * @zldev: pointer to zl3073x device
+ * @ch: DPLL channel index
+ * @offset: frequency offset in 2^-48 steps
+ *
+ * Context: Caller must hold the per-DPLL lock.
+ * Return: 0 on success, <0 on error
+ */
+int zl3073x_chan_df_offset_set(struct zl3073x_dev *zldev, u8 ch, s64 offset)
+{
+	int rc;
+
+	rc = zl3073x_write_u48(zldev, ZL_REG_DPLL_DF_OFFSET(ch), offset);
+	if (!rc)
+		zldev->chan[ch].df_offset = offset;
+
+	return rc;
+}
+
+/**
+ * zl3073x_chan_tie_write - adjust DPLL phase using TIE write
+ * @zldev: pointer to zl3073x device
+ * @ch: DPLL channel index
+ * @delta_ns: phase adjustment in nanoseconds (must be within +-1s)
+ *
+ * Converts nanoseconds to TIE units (0.01 ps) and writes TIE data
+ * to the specified channel.
+ *
+ * Return: 0 on success, <0 on error
+ */
+int zl3073x_chan_tie_write(struct zl3073x_dev *zldev, u8 ch, s64 delta_ns)
+{
+	s64 tie_data;
+	int rc;
+
+	if (WARN_ON(delta_ns <= -NSEC_PER_SEC || delta_ns >= NSEC_PER_SEC))
+		return -ERANGE;
+
+	guard(mutex)(&zldev->tie_lock);
+
+	/* Wait for any previous TIE operation to complete */
+	rc = zl3073x_poll_zero_u8(zldev, ZL_REG_DPLL_TIE_CTRL,
+				  ZL_DPLL_TIE_CTRL_OP,
+				  ZL_POLL_TIE_WR_TIMEOUT_US);
+	if (rc)
+		return rc;
+
+	/* Convert ns to TIE units (0.01 ps = 10^-14 s) */
+	tie_data = delta_ns * 100000LL;
+
+	rc = zl3073x_write_u48(zldev, ZL_REG_DPLL_TIE_DATA(ch), tie_data);
+	if (rc)
+		return rc;
+
+	rc = zl3073x_write_u8(zldev, ZL_REG_DPLL_TIE_CTRL_MASK, BIT(ch));
+	if (rc)
+		return rc;
+
+	return zl3073x_write_u8(zldev, ZL_REG_DPLL_TIE_CTRL,
+				ZL_DPLL_TIE_CTRL_OP_WR);
+}
+
+/**
+ * zl3073x_chan_phase_step - execute one output phase step operation
+ * @zldev: pointer to zl3073x device
+ * @ch: DPLL channel index
+ * @out_mask: bitmask of outputs to step
+ * @step_cycles: phase step in synthesizer clock cycles
+ * @tod_step: also step the ToD counter
+ *
+ * All masked outputs must use synthesizers of the same frequency since
+ * the step value is in synthesizer clock cycles.
+ *
+ * Return: 0 on success, <0 on error
+ */
+int zl3073x_chan_phase_step(struct zl3073x_dev *zldev, u8 ch,
+			    u16 out_mask, s32 step_cycles,
+			    bool tod_step)
+{
+	u8 ctrl;
+	int rc;
+
+	guard(mutex)(&zldev->phase_step_lock);
+
+	/* Wait for any previous phase step operation to complete */
+	rc = zl3073x_poll_zero_u8(zldev, ZL_REG_OUTPUT_PHASE_STEP_CTRL,
+				  ZL_OUTPUT_PHASE_STEP_CTRL_OP,
+				  ZL_POLL_PHASE_STEP_TIMEOUT_US);
+	if (rc)
+		return rc;
+
+	rc = zl3073x_write_u32(zldev, ZL_REG_OUTPUT_PHASE_STEP_DATA,
+			       step_cycles);
+	if (rc)
+		return rc;
+
+	rc = zl3073x_write_u16(zldev, ZL_REG_OUTPUT_PHASE_STEP_MASK, out_mask);
+	if (rc)
+		return rc;
+
+	rc = zl3073x_write_u8(zldev, ZL_REG_OUTPUT_PHASE_STEP_NUMBER, 1);
+	if (rc)
+		return rc;
+
+	ctrl = FIELD_PREP(ZL_OUTPUT_PHASE_STEP_CTRL_DPLL, ch) |
+	       FIELD_PREP(ZL_OUTPUT_PHASE_STEP_CTRL_OP,
+			  ZL_OUTPUT_PHASE_STEP_CTRL_OP_WRITE);
+	if (tod_step)
+		ctrl |= ZL_OUTPUT_PHASE_STEP_CTRL_TOD_STEP;
+
+	return zl3073x_write_u8(zldev, ZL_REG_OUTPUT_PHASE_STEP_CTRL, ctrl);
+}
+
 /**
  * zl3073x_chan_state_set - commit DPLL channel state changes to hardware
  * @zldev: pointer to zl3073x_dev structure
diff --git a/drivers/dpll/zl3073x/chan.h b/drivers/dpll/zl3073x/chan.h
index dc9c6d95bdee7..99c3b7088f678 100644
--- a/drivers/dpll/zl3073x/chan.h
+++ b/drivers/dpll/zl3073x/chan.h
@@ -5,10 +5,12 @@
 
 #include <linux/bitfield.h>
 #include <linux/stddef.h>
+#include <linux/time64.h>
 #include <linux/types.h>
 
 #include "regs.h"
 
+struct ptp_system_timestamp;
 struct zl3073x_dev;
 
 /**
@@ -42,6 +44,21 @@ int zl3073x_chan_state_set(struct zl3073x_dev *zldev, u8 index,
 int zl3073x_chan_state_update(struct zl3073x_dev *zldev, u8 index);
 int zl3073x_chan_nco_mode_set(struct zl3073x_dev *zldev, u8 index);
 
+int zl3073x_chan_tod_ready_wait(struct zl3073x_dev *zldev, u8 ch);
+int zl3073x_chan_tod_read(struct zl3073x_dev *zldev, u8 ch,
+			  bool next_hz, struct timespec64 *ts,
+			  struct ptp_system_timestamp *sts);
+int zl3073x_chan_tod_write(struct zl3073x_dev *zldev, u8 ch,
+			   struct timespec64 ts);
+int zl3073x_chan_tod_adjust(struct zl3073x_dev *zldev, u8 ch,
+			    struct timespec64 delta);
+int zl3073x_chan_phase_step(struct zl3073x_dev *zldev, u8 ch,
+			    u16 out_mask, s32 step_cycles, bool tod_step);
+
+int zl3073x_chan_df_offset_set(struct zl3073x_dev *zldev, u8 ch, s64 offset);
+
+int zl3073x_chan_tie_write(struct zl3073x_dev *zldev, u8 ch, s64 delta_ns);
+
 /**
  * zl3073x_chan_df_offset_get - get cached df_offset vs tracked reference
  * @chan: pointer to channel state
@@ -200,6 +217,21 @@ static inline bool zl3073x_chan_mode_is_reflock(const struct zl3073x_chan *chan)
 	return zl3073x_chan_mode_get(chan) == ZL_DPLL_MODE_REFSEL_MODE_REFLOCK;
 }
 
+/**
+ * zl3073x_chan_mode_supports_tie - check if channel mode supports TIE write
+ * @chan: pointer to channel state
+ *
+ * TIE write is supported in AUTO and REFLOCK modes regardless of lock state.
+ *
+ * Return: true if TIE write is supported, false otherwise
+ */
+static inline bool
+zl3073x_chan_mode_supports_tie(const struct zl3073x_chan *chan)
+{
+	return zl3073x_chan_mode_is_auto(chan) ||
+		zl3073x_chan_mode_is_reflock(chan);
+}
+
 /**
  * zl3073x_chan_is_ho_ready - check if holdover is ready
  * @chan: pointer to channel state
diff --git a/drivers/dpll/zl3073x/core.c b/drivers/dpll/zl3073x/core.c
index 5b2d77f2c2288..230df08e27cd1 100644
--- a/drivers/dpll/zl3073x/core.c
+++ b/drivers/dpll/zl3073x/core.c
@@ -322,7 +322,7 @@ int zl3073x_write_u48(struct zl3073x_dev *zldev, unsigned int reg, u64 val)
 int zl3073x_poll_zero_u8(struct zl3073x_dev *zldev, unsigned int reg,
 			 u8 mask, unsigned int timeout_us)
 {
-#define ZL_POLL_SLEEP_US 10
+	unsigned int sleep_us = timeout_us / 50;
 	unsigned int val;
 
 	/* Check the register is 8bit */
@@ -336,7 +336,7 @@ int zl3073x_poll_zero_u8(struct zl3073x_dev *zldev, unsigned int reg,
 	reg = ZL_REG_ADDR(reg) + ZL_RANGE_OFFSET;
 
 	return regmap_read_poll_timeout(zldev->regmap, reg, val, !(val & mask),
-					ZL_POLL_SLEEP_US, timeout_us);
+					sleep_us, timeout_us);
 }
 
 int zl3073x_mb_op(struct zl3073x_dev *zldev, unsigned int op_reg, u8 op_val,
@@ -511,6 +511,11 @@ zl3073x_dev_state_fetch(struct zl3073x_dev *zldev)
 	int rc;
 	u8 i;
 
+	rc = zl3073x_read_u16(zldev, ZL_REG_OUTPUT_STEP_TIME_MASK,
+			      &zldev->out_step_time_mask);
+	if (rc)
+		return rc;
+
 	for (i = 0; i < ZL3073X_NUM_REFS; i++) {
 		rc = zl3073x_ref_state_fetch(zldev, i);
 		if (rc) {
@@ -1034,6 +1039,14 @@ int zl3073x_dev_probe(struct zl3073x_dev *zldev)
 	 * and/or polls are required to be done atomically.
 	 */
 	rc = devm_mutex_init(zldev->dev, &zldev->multiop_lock);
+	if (rc)
+		return dev_err_probe(zldev->dev, rc,
+				     "Failed to initialize mutex\n");
+	rc = devm_mutex_init(zldev->dev, &zldev->phase_step_lock);
+	if (rc)
+		return dev_err_probe(zldev->dev, rc,
+				     "Failed to initialize mutex\n");
+	rc = devm_mutex_init(zldev->dev, &zldev->tie_lock);
 	if (rc)
 		return dev_err_probe(zldev->dev, rc,
 				     "Failed to initialize mutex\n");
diff --git a/drivers/dpll/zl3073x/core.h b/drivers/dpll/zl3073x/core.h
index 78dc208f3eea2..67c10e2595118 100644
--- a/drivers/dpll/zl3073x/core.h
+++ b/drivers/dpll/zl3073x/core.h
@@ -26,6 +26,10 @@ struct zl3073x_dpll;
 #define ZL_POLL_HWREG_TIMEOUT_US	(50 * USEC_PER_MSEC)
 #define ZL_POLL_MB_TIMEOUT_US		(30 * USEC_PER_MSEC)
 #define ZL_POLL_PHASE_ERR_TIMEOUT_US	(50 * USEC_PER_MSEC)
+#define ZL_POLL_PHASE_STEP_TIMEOUT_US	(3000 * USEC_PER_MSEC)
+#define ZL_POLL_TIE_WR_TIMEOUT_US	(1000 * USEC_PER_MSEC)
+#define ZL_POLL_TOD_RD_TIMEOUT_US	(30 * USEC_PER_MSEC)
+#define ZL_POLL_TOD_WR_TIMEOUT_US	(1000 * USEC_PER_MSEC)
 
 enum zl3073x_flags {
 	ZL3073X_FLAG_REF_PHASE_COMP_32_BIT,
@@ -55,6 +59,8 @@ struct zl3073x_chip_info {
  * @regmap: regmap to access device registers
  * @info: detected chip info
  * @multiop_lock: to serialize multiple register operations
+ * @tie_lock: to serialize TIE write operations
+ * @phase_step_lock: to serialize output phase step operations
  * @ref: array of input references' invariants
  * @out: array of outs' invariants
  * @synth: array of synths' invariants
@@ -63,6 +69,7 @@ struct zl3073x_chip_info {
  * @kworker: thread for periodic work
  * @work: periodic work
  * @clock_id: clock id of the device
+ * @out_step_time_mask: output step-time mask (device-global)
  * @phase_avg_factor: phase offset measurement averaging factor
  * @freq_monitor: is frequency monitor enabled
  */
@@ -71,6 +78,8 @@ struct zl3073x_dev {
 	struct regmap			*regmap;
 	const struct zl3073x_chip_info	*info;
 	struct mutex			multiop_lock;
+	struct mutex			tie_lock;
+	struct mutex			phase_step_lock;
 
 	/* Invariants */
 	struct zl3073x_ref	ref[ZL3073X_NUM_REFS];
@@ -87,6 +96,7 @@ struct zl3073x_dev {
 
 	/* Per-chip parameters */
 	u64			clock_id;
+	u16			out_step_time_mask;
 	u8			phase_avg_factor;
 	bool			freq_monitor;
 };
@@ -308,6 +318,19 @@ zl3073x_dev_out_is_enabled(struct zl3073x_dev *zldev, u8 index)
 	return zl3073x_synth_is_enabled(synth) && zl3073x_out_is_enabled(out);
 }
 
+/**
+ * zl3073x_dev_out_is_stepped - check if output is in step-time mask
+ * @zldev: pointer to zl3073x device
+ * @index: output index
+ *
+ * Return: true if output is affected by step-time operations
+ */
+static inline bool
+zl3073x_dev_out_is_stepped(struct zl3073x_dev *zldev, u8 index)
+{
+	return !!(zldev->out_step_time_mask & BIT(index));
+}
+
 /**
  * zl3073x_dev_out_dpll_get - get DPLL ID the output is driven by
  * @zldev: pointer to zl3073x device
diff --git a/drivers/dpll/zl3073x/dpll.c b/drivers/dpll/zl3073x/dpll.c
index 0488ae6ac486c..a686dd52bfac9 100644
--- a/drivers/dpll/zl3073x/dpll.c
+++ b/drivers/dpll/zl3073x/dpll.c
@@ -2,6 +2,7 @@
 
 #include <linux/bits.h>
 #include <linux/bitfield.h>
+#include <linux/cleanup.h>
 #include <linux/bug.h>
 #include <linux/container_of.h>
 #include <linux/dev_printk.h>
@@ -13,6 +14,7 @@
 #include <linux/netlink.h>
 #include <linux/platform_device.h>
 #include <linux/property.h>
+#include <linux/ptp_clock_kernel.h>
 #include <linux/slab.h>
 #include <linux/sprintf.h>
 
@@ -2275,45 +2277,375 @@ zl3073x_dpll_init_fine_phase_adjust(struct zl3073x_dev *zldev)
 	return zl3073x_write_u8(zldev, ZL_REG_SYNTH_PHASE_SHIFT_CTRL, 0x01);
 }
 
+/* Maximum frequency adjustment: +-1% of nominal in ppb */
+#define ZL3073X_DPLL_PTP_MAX_ADJ	10000000
+
 /**
- * zl3073x_dpll_alloc - allocate DPLL device
- * @zldev: pointer to zl3073x device
- * @ch: DPLL channel number
+ * zl3073x_dpll_ptp_gettimex64 - read current time from ToD counters
+ * @info: PTP clock info
+ * @ts: timespec to store current time
+ * @sts: optional system timestamp pair for cross-timestamping
  *
- * Allocates DPLL device structure for given DPLL channel.
+ * Return: 0 on success, <0 on error
+ */
+static int zl3073x_dpll_ptp_gettimex64(struct ptp_clock_info *info,
+				       struct timespec64 *ts,
+				       struct ptp_system_timestamp *sts)
+{
+	struct zl3073x_dpll *zldpll = container_of(info, struct zl3073x_dpll,
+						   ptp_info);
+
+	guard(mutex)(&zldpll->lock);
+
+	return zl3073x_chan_tod_read(zldpll->dev, zldpll->id, false, ts, sts);
+}
+
+/**
+ * zl3073x_dpll_ptp_settime64 - set ToD counters to given time
+ * @info: PTP clock info
+ * @ts: timespec with time to set
  *
- * Return: pointer to DPLL device on success, error pointer on error
+ * Return: 0 on success, <0 on error
  */
-struct zl3073x_dpll *
-zl3073x_dpll_alloc(struct zl3073x_dev *zldev, u8 ch)
+static int zl3073x_dpll_ptp_settime64(struct ptp_clock_info *info,
+				      const struct timespec64 *ts)
 {
-	struct zl3073x_dpll *zldpll;
+	struct zl3073x_dpll *zldpll = container_of(info, struct zl3073x_dpll,
+						   ptp_info);
 
-	zldpll = kzalloc_obj(*zldpll);
-	if (!zldpll)
-		return ERR_PTR(-ENOMEM);
+	guard(mutex)(&zldpll->lock);
 
-	zldpll->dev = zldev;
-	zldpll->id = ch;
-	mutex_init(&zldpll->lock);
-	INIT_LIST_HEAD(&zldpll->pins);
+	return zl3073x_chan_tod_write(zldpll->dev, zldpll->id, *ts);
+}
 
-	return zldpll;
+/**
+ * zl3073x_dpll_ptp_adjtime_phase_step - adjust sub-second time via phase step
+ * @zldpll: DPLL channel
+ * @delta: time adjustment in nanoseconds (must be within (-NSEC_PER_SEC,
+ *         NSEC_PER_SEC))
+ *
+ * Uses the output phase step mechanism with tod_step=1 to adjust both
+ * the output clock phase and the ToD counter simultaneously. This keeps
+ * outputs and ToD coherent. Only valid for NCO.
+ *
+ * Outputs are grouped by synthesizer since the phase step value is in
+ * synthesizer clock cycles. The first synth group with enabled outputs
+ * uses tod_step to adjust both outputs and the ToD counter. Remaining
+ * groups step outputs only. If no synth has enabled outputs, the ToD
+ * counter is stepped alone using an empty output mask (the FW uses
+ * the first enabled synth's period for the conversion).
+ *
+ * Return: 0 on success, -EOPNOTSUPP if no synths available, <0 on error
+ */
+static int zl3073x_dpll_ptp_adjtime_phase_step(struct zl3073x_dpll *zldpll,
+					       s64 delta)
+{
+	u16 synth_mask[ZL3073X_NUM_SYNTHS] = {};
+	struct zl3073x_dev *zldev = zldpll->dev;
+	const struct zl3073x_synth *synth;
+	struct zl3073x_dpll_pin *pin;
+	u32 first_synth_freq = 0;
+	bool tod_stepped = false;
+	s32 step_cycles;
+	u32 synth_freq;
+	int rc;
+	u8 i;
+
+	/* Build per-synth output masks from registered output pins */
+	list_for_each_entry(pin, &zldpll->pins, list) {
+		u8 out_id, synth_id;
+
+		if (zl3073x_dpll_is_input_pin(pin))
+			continue;
+
+		out_id = zl3073x_output_pin_out_get(pin->id);
+
+		if (!zl3073x_dev_out_is_stepped(zldev, out_id))
+			continue;
+
+		synth_id = zl3073x_dev_out_synth_get(zldev, out_id);
+		if (synth_id >= ZL3073X_NUM_SYNTHS) {
+			dev_warn(zldev->dev, "Unexpected synth id for OUT%u\n",
+				 out_id);
+			continue;
+		}
+		synth_mask[synth_id] |= BIT(out_id);
+	}
+
+	/* Process each synth group */
+	for (i = 0; i < ZL3073X_NUM_SYNTHS; i++) {
+		synth = zl3073x_synth_state_get(zldev, i);
+		if (!zl3073x_synth_is_enabled(synth) ||
+		    zl3073x_synth_dpll_get(synth) != zldpll->id)
+			continue;
+
+		synth_freq = zl3073x_synth_freq_get(synth);
+
+		/* Remember first enabled synth freq for ToD-only fallback */
+		if (!first_synth_freq)
+			first_synth_freq = synth_freq;
+
+		if (!synth_mask[i])
+			continue;
+
+		/* Safe for s32: max synth freq is 750 MHz */
+		step_cycles = div_s64(delta * synth_freq, NSEC_PER_SEC);
+
+		rc = zl3073x_chan_phase_step(zldev, zldpll->id,
+					     synth_mask[i], step_cycles,
+					     !tod_stepped);
+		if (rc) {
+			if (tod_stepped) {
+				dev_warn(zldev->dev,
+					 "Partial phase step failure\n");
+				return 0;
+			}
+			return rc;
+		}
+		tod_stepped = true;
+	}
+
+	if (!first_synth_freq)
+		return -EOPNOTSUPP;
+
+	/* No enabled outputs found; step ToD counter only using the
+	 * first enabled synth's period (empty output mask).
+	 */
+	if (!tod_stepped) {
+		step_cycles = div_s64(delta * first_synth_freq, NSEC_PER_SEC);
+		return zl3073x_chan_phase_step(zldev, zldpll->id, 0,
+					       step_cycles, true);
+	}
+
+	return 0;
 }
 
 /**
- * zl3073x_dpll_free - free DPLL device
- * @zldpll: pointer to zl3073x_dpll structure
+ * zl3073x_dpll_ptp_adjtime - adjust PTP clock time
+ * @info: PTP clock info
+ * @delta: time adjustment in nanoseconds
  *
- * Deallocates given DPLL device previously allocated by @zl3073x_dpll_alloc.
+ * For NCO, large deltas (>= 1 second) are split into a ToD
+ * read-modify-write for the seconds part and an output phase step for
+ * the sub-second remainder. Sub-second deltas use phase step directly,
+ * falling back to ToD read-modify-write if phase step is unavailable.
+ * In AUTO/REFLOCK modes, large deltas are split into ToD
+ * read-modify-write for seconds and TIE write for the sub-second
+ * remainder. Sub-second deltas use TIE write directly.
+ *
+ * Return: 0 on success, <0 on error
  */
-void
-zl3073x_dpll_free(struct zl3073x_dpll *zldpll)
+static int zl3073x_dpll_ptp_adjtime(struct ptp_clock_info *info, s64 delta)
 {
-	WARN(zldpll->dpll_dev, "DPLL device is still registered\n");
+	struct zl3073x_dpll *zldpll = container_of(info, struct zl3073x_dpll,
+						   ptp_info);
+	struct zl3073x_dev *zldev = zldpll->dev;
+	const struct zl3073x_chan *chan;
+	bool sec_adjusted = false;
+	struct timespec64 ts;
+	int rc;
 
-	mutex_destroy(&zldpll->lock);
-	kfree(zldpll);
+	if (!delta)
+		return 0;
+
+	guard(mutex)(&zldpll->lock);
+
+	/* Modes without phase step or TIE use plain ToD adjust */
+	chan = zl3073x_chan_state_get(zldev, zldpll->id);
+	if (!zl3073x_chan_mode_is_nco(chan) &&
+	    !zl3073x_chan_mode_supports_tie(chan))
+		return zl3073x_chan_tod_adjust(zldev, zldpll->id,
+					       ns_to_timespec64(delta));
+
+	/* Split off seconds via ToD read-modify-write so the sub-second
+	 * remainder can be applied through the output-coherent mechanism
+	 * (phase step or TIE write).
+	 */
+	if (delta >= NSEC_PER_SEC || delta <= -NSEC_PER_SEC) {
+		s32 remainder;
+
+		ts.tv_sec = div_s64_rem(delta, NSEC_PER_SEC, &remainder);
+		ts.tv_nsec = 0;
+		delta = remainder;
+
+		rc = zl3073x_chan_tod_adjust(zldev, zldpll->id, ts);
+		if (rc)
+			return rc;
+
+		/* No sub-second remainder, done */
+		if (!delta)
+			return 0;
+
+		/* Wait for the ToD write to be applied at the 1 Hz edge
+		 * before issuing phase step or TIE write, so the pending
+		 * WR_NEXT_1HZ does not overwrite the sub-second adjustment.
+		 */
+		rc = zl3073x_chan_tod_ready_wait(zldev, zldpll->id);
+		if (rc)
+			return rc;
+
+		sec_adjusted = true;
+	}
+
+	/* Apply sub-second delta via phase step (NCO) or TIE write */
+	if (zl3073x_chan_mode_is_nco(chan)) {
+		rc = zl3073x_dpll_ptp_adjtime_phase_step(zldpll, delta);
+		if (!rc)
+			return 0;
+	} else {
+		rc = zl3073x_chan_tie_write(zldev, zldpll->id, delta);
+		if (!rc)
+			return 0;
+	}
+
+	/* Phase step or TIE write failed, fall back to ToD adjust */
+	rc = zl3073x_chan_tod_adjust(zldev, zldpll->id,
+				     ns_to_timespec64(delta));
+
+	/* In the unlikely event that both phase step/TIE write and fallback
+	 * ToD adjust fail after seconds were already committed, return
+	 * success to prevent the PTP servo from retrying the full delta and
+	 * applying seconds again. The sub-second residual will self-correct
+	 * in the next servo cycle.
+	 */
+	if (rc && sec_adjusted) {
+		dev_warn(zldev->dev,
+			 "Sub-second adjustment failed after seconds applied\n");
+		return 0;
+	}
+
+	return rc;
+}
+
+/**
+ * zl3073x_dpll_ptp_adjfine - adjust PTP clock frequency
+ * @info: PTP clock info
+ * @scaled_ppm: frequency adjustment in scaled ppm (ppm * 2^16)
+ *
+ * Only supported for NCO. Writes the delta frequency offset register.
+ *
+ * Return:
+ * * %0			- success or @scaled_ppm is zero (no-op)
+ * * %-EOPNOTSUPP	- NCO pin is not connected and @scaled_ppm is non-zero
+ * * negative		- other error
+ */
+static int
+zl3073x_dpll_ptp_adjfine(struct ptp_clock_info *info, long scaled_ppm)
+{
+	struct zl3073x_dpll *zldpll = container_of(info, struct zl3073x_dpll,
+						   ptp_info);
+	const struct zl3073x_chan *chan;
+	s64 offset;
+
+	/* Convert scaled_ppm to df_offset in 2^-48 steps:
+	 *   df_offset = -(scaled_ppm * 2^32) / 10^6
+	 *
+	 * Simplify to avoid overflow:
+	 *   df_offset = -(scaled_ppm * 2^26) / 5^6
+	 *   df_offset = -(scaled_ppm * 67108864) / 15625
+	 */
+	offset = -div_s64((s64)scaled_ppm * 67108864LL, 15625);
+
+	guard(mutex)(&zldpll->lock);
+
+	chan = zl3073x_chan_state_get(zldpll->dev, zldpll->id);
+	if (!zl3073x_chan_mode_is_nco(chan))
+		return scaled_ppm ? -EOPNOTSUPP : 0;
+	if (offset == chan->df_offset)
+		return 0;
+
+	return zl3073x_chan_df_offset_set(zldpll->dev, zldpll->id, offset);
+}
+
+/**
+ * zl3073x_dpll_ptp_adjphase - adjust PTP clock phase
+ * @info: PTP clock info
+ * @delta: phase adjustment in nanoseconds
+ *
+ * Only supported in AUTO and REFLOCK modes. Uses TIE write for
+ * nanosecond resolution phase adjustment.
+ *
+ * Return:
+ * * %0			- success or @delta is zero (no-op)
+ * * %-EOPNOTSUPP	- mode does not support TIE and @delta is non-zero
+ * * negative		- other error
+ */
+static int zl3073x_dpll_ptp_adjphase(struct ptp_clock_info *info, s32 delta)
+{
+	struct zl3073x_dpll *zldpll = container_of(info, struct zl3073x_dpll,
+						   ptp_info);
+	struct zl3073x_dev *zldev = zldpll->dev;
+	const struct zl3073x_chan *chan;
+
+	if (!delta)
+		return 0;
+
+	guard(mutex)(&zldpll->lock);
+
+	chan = zl3073x_chan_state_get(zldev, zldpll->id);
+
+	if (!zl3073x_chan_mode_supports_tie(chan))
+		return -EOPNOTSUPP;
+
+	return zl3073x_chan_tie_write(zldev, zldpll->id, delta);
+}
+
+static s32
+zl3073x_dpll_ptp_getmaxphase(struct ptp_clock_info *info __always_unused)
+{
+	/* HW limits TIE write to +-1 second. Return the constant HW
+	 * limit and let adjphase handle mode-specific checks.
+	 */
+	return NSEC_PER_SEC - 1;
+}
+
+static const struct ptp_clock_info zl3073x_dpll_ptp_clock_info = {
+	.owner		= THIS_MODULE,
+	.max_adj	= ZL3073X_DPLL_PTP_MAX_ADJ,
+	.gettimex64	= zl3073x_dpll_ptp_gettimex64,
+	.settime64	= zl3073x_dpll_ptp_settime64,
+	.adjtime	= zl3073x_dpll_ptp_adjtime,
+	.adjfine	= zl3073x_dpll_ptp_adjfine,
+	.adjphase	= zl3073x_dpll_ptp_adjphase,
+	.getmaxphase	= zl3073x_dpll_ptp_getmaxphase,
+};
+
+/**
+ * zl3073x_dpll_ptp_register - register PTP clock for a DPLL channel
+ * @zldpll: DPLL channel to register PTP clock for
+ *
+ * Return: 0 on success, <0 on error
+ */
+static int zl3073x_dpll_ptp_register(struct zl3073x_dpll *zldpll)
+{
+	struct zl3073x_dev *zldev = zldpll->dev;
+	struct ptp_clock *ptp_clock;
+
+	snprintf(zldpll->ptp_info.name, sizeof(zldpll->ptp_info.name),
+		 "%s-dpll%u", dev_name(zldev->dev), zldpll->id);
+
+	ptp_clock = ptp_clock_register(&zldpll->ptp_info, zldev->dev);
+	if (IS_ERR(ptp_clock)) {
+		dev_err(zldev->dev, "Failed to register PTP clock for DPLL%u\n",
+			zldpll->id);
+		return PTR_ERR(ptp_clock);
+	}
+
+	zldpll->ptp_clock = ptp_clock;
+
+	return 0;
+}
+
+/**
+ * zl3073x_dpll_ptp_unregister - unregister PTP clock for a DPLL channel
+ * @zldpll: DPLL channel to unregister PTP clock for
+ */
+static void zl3073x_dpll_ptp_unregister(struct zl3073x_dpll *zldpll)
+{
+	if (!IS_ERR_OR_NULL(zldpll->ptp_clock)) {
+		ptp_clock_unregister(zldpll->ptp_clock);
+		zldpll->ptp_clock = NULL;
+	}
 }
 
 /**
@@ -2398,6 +2730,48 @@ zl3073x_dpll_ref_sync_pairs_register(struct zl3073x_dpll *zldpll)
 	return 0;
 }
 
+/**
+ * zl3073x_dpll_alloc - allocate DPLL device
+ * @zldev: pointer to zl3073x device
+ * @ch: DPLL channel number
+ *
+ * Allocates DPLL device structure for given DPLL channel.
+ *
+ * Return: pointer to DPLL device on success, error pointer on error
+ */
+struct zl3073x_dpll *
+zl3073x_dpll_alloc(struct zl3073x_dev *zldev, u8 ch)
+{
+	struct zl3073x_dpll *zldpll;
+
+	zldpll = kzalloc_obj(*zldpll);
+	if (!zldpll)
+		return ERR_PTR(-ENOMEM);
+
+	zldpll->dev = zldev;
+	zldpll->id = ch;
+	zldpll->ptp_info = zl3073x_dpll_ptp_clock_info;
+	mutex_init(&zldpll->lock);
+	INIT_LIST_HEAD(&zldpll->pins);
+
+	return zldpll;
+}
+
+/**
+ * zl3073x_dpll_free - free DPLL device
+ * @zldpll: pointer to zl3073x_dpll structure
+ *
+ * Deallocates given DPLL device previously allocated by @zl3073x_dpll_alloc.
+ */
+void
+zl3073x_dpll_free(struct zl3073x_dpll *zldpll)
+{
+	WARN(zldpll->dpll_dev, "DPLL device is still registered\n");
+
+	mutex_destroy(&zldpll->lock);
+	kfree(zldpll);
+}
+
 /**
  * zl3073x_dpll_register - register DPLL device and all its pins
  * @zldpll: pointer to zl3073x_dpll structure
@@ -2428,6 +2802,13 @@ zl3073x_dpll_register(struct zl3073x_dpll *zldpll)
 		return rc;
 	}
 
+	rc = zl3073x_dpll_ptp_register(zldpll);
+	if (rc) {
+		zl3073x_dpll_pins_unregister(zldpll);
+		zl3073x_dpll_device_unregister(zldpll);
+		return rc;
+	}
+
 	return 0;
 }
 
@@ -2441,7 +2822,7 @@ zl3073x_dpll_register(struct zl3073x_dpll *zldpll)
 void
 zl3073x_dpll_unregister(struct zl3073x_dpll *zldpll)
 {
-	/* Unregister all pins and dpll */
+	zl3073x_dpll_ptp_unregister(zldpll);
 	zl3073x_dpll_pins_unregister(zldpll);
 	zl3073x_dpll_device_unregister(zldpll);
 }
diff --git a/drivers/dpll/zl3073x/dpll.h b/drivers/dpll/zl3073x/dpll.h
index faebc402ba1b7..993221dc63249 100644
--- a/drivers/dpll/zl3073x/dpll.h
+++ b/drivers/dpll/zl3073x/dpll.h
@@ -5,6 +5,7 @@
 
 #include <linux/dpll.h>
 #include <linux/list.h>
+#include <linux/ptp_clock_kernel.h>
 
 #include "core.h"
 
@@ -22,6 +23,8 @@
  * @type: DPLL type (PPS or EEC)
  * @lock_status: last saved DPLL lock status
  * @pins: list of pins
+ * @ptp_info: PTP clock info
+ * @ptp_clock: registered PTP clock (or NULL)
  */
 struct zl3073x_dpll {
 	struct list_head		list;
@@ -36,6 +39,8 @@ struct zl3073x_dpll {
 	enum dpll_type			type;
 	enum dpll_lock_status		lock_status;
 	struct list_head		pins;
+	struct ptp_clock_info		ptp_info;
+	struct ptp_clock		*ptp_clock;
 };
 
 struct zl3073x_dpll *zl3073x_dpll_alloc(struct zl3073x_dev *zldev, u8 ch);
diff --git a/drivers/dpll/zl3073x/regs.h b/drivers/dpll/zl3073x/regs.h
index b70ead7d4495b..f3a5e1215aa36 100644
--- a/drivers/dpll/zl3073x/regs.h
+++ b/drivers/dpll/zl3073x/regs.h
@@ -179,6 +179,20 @@
 #define ZL_DPLL_DF_READ_CMD			GENMASK(2, 0)
 #define ZL_DPLL_DF_READ_CMD_ACC_I		4
 
+#define ZL_REG_DPLL_TIE_CTRL			ZL_REG(5, 0x30, 1)
+#define ZL_DPLL_TIE_CTRL_OP			GENMASK(2, 0)
+#define ZL_DPLL_TIE_CTRL_OP_WR			4
+
+#define ZL_REG_DPLL_TIE_CTRL_MASK		ZL_REG(5, 0x31, 1)
+
+#define ZL_REG_DPLL_TOD_CTRL(_idx)					\
+	ZL_REG_IDX(_idx, 5, 0x38, 1, ZL3073X_MAX_CHANNELS, 1)
+#define ZL_DPLL_TOD_CTRL_SEM			BIT(4)
+#define ZL_DPLL_TOD_CTRL_CMD			GENMASK(3, 0)
+#define ZL_DPLL_TOD_CTRL_CMD_WR_NEXT_1HZ	1
+#define ZL_DPLL_TOD_CTRL_CMD_RD_CURRENT		8
+#define ZL_DPLL_TOD_CTRL_CMD_RD_NEXT_1HZ	9
+
 #define ZL_REG_DPLL_MEAS_CTRL			ZL_REG(5, 0x50, 1)
 #define ZL_DPLL_MEAS_CTRL_EN			BIT(0)
 #define ZL_DPLL_MEAS_CTRL_AVG_FACTOR		GENMASK(7, 4)
@@ -193,6 +207,9 @@
 
 /*******************************
  * Register Pages 6-7, DPLL Data
+ *
+ * Per-channel registers with stride 0x20. Channels 0-3 reside on page 6,
+ * channel 4 on page 7.
  *******************************/
 
 #define ZL_REG_DPLL_DF_OFFSET_03(_idx)					\
@@ -202,6 +219,24 @@
 	((_idx) < 4 ? ZL_REG_DPLL_DF_OFFSET_03(_idx) : ZL_REG_DPLL_DF_OFFSET_4)
 #define ZL_DPLL_DF_OFFSET_UNKNOWN	S64_MIN
 
+#define ZL_REG_DPLL_TIE_DATA_03(_idx)					\
+	ZL_REG_IDX(_idx, 6, 0x0C, 6, 4, 0x20)
+#define ZL_REG_DPLL_TIE_DATA_4			ZL_REG(7, 0x0C, 6)
+#define ZL_REG_DPLL_TIE_DATA(_idx)					\
+	((_idx) < 4 ? ZL_REG_DPLL_TIE_DATA_03(_idx) : ZL_REG_DPLL_TIE_DATA_4)
+
+#define ZL_REG_DPLL_TOD_SEC_03(_idx)					\
+	ZL_REG_IDX(_idx, 6, 0x12, 6, 4, 0x20)
+#define ZL_REG_DPLL_TOD_SEC_4			ZL_REG(7, 0x12, 6)
+#define ZL_REG_DPLL_TOD_SEC(_idx)					\
+	((_idx) < 4 ? ZL_REG_DPLL_TOD_SEC_03(_idx) : ZL_REG_DPLL_TOD_SEC_4)
+
+#define ZL_REG_DPLL_TOD_NS_03(_idx)					\
+	ZL_REG_IDX(_idx, 6, 0x18, 4, 4, 0x20)
+#define ZL_REG_DPLL_TOD_NS_4			ZL_REG(7, 0x18, 4)
+#define ZL_REG_DPLL_TOD_NS(_idx)					\
+	((_idx) < 4 ? ZL_REG_DPLL_TOD_NS_03(_idx) : ZL_REG_DPLL_TOD_NS_4)
+
 /***********************************
  * Register Page 9, Synth and Output
  ***********************************/
@@ -221,6 +256,23 @@
 #define ZL_OUTPUT_CTRL_EN			BIT(0)
 #define ZL_OUTPUT_CTRL_SYNTH_SEL		GENMASK(6, 4)
 
+#define ZL_REG_OUTPUT_STEP_TIME_MASK		ZL_REG(9, 0x36, 2)
+
+#define ZL_REG_OUTPUT_PHASE_STEP_CTRL		ZL_REG(9, 0x38, 1)
+#define ZL_OUTPUT_PHASE_STEP_CTRL_DPLL		GENMASK(6, 4)
+#define ZL_OUTPUT_PHASE_STEP_CTRL_TOD_STEP	BIT(3)
+#define ZL_OUTPUT_PHASE_STEP_CTRL_OP		GENMASK(1, 0)
+#define ZL_OUTPUT_PHASE_STEP_CTRL_OP_NONE	0
+#define ZL_OUTPUT_PHASE_STEP_CTRL_OP_RESET	1
+#define ZL_OUTPUT_PHASE_STEP_CTRL_OP_READ	2
+#define ZL_OUTPUT_PHASE_STEP_CTRL_OP_WRITE	3
+
+#define ZL_REG_OUTPUT_PHASE_STEP_NUMBER		ZL_REG(9, 0x39, 1)
+
+#define ZL_REG_OUTPUT_PHASE_STEP_MASK		ZL_REG(9, 0x3a, 2)
+
+#define ZL_REG_OUTPUT_PHASE_STEP_DATA		ZL_REG(9, 0x3c, 4)
+
 /*******************************
  * Register Page 10, Ref Mailbox
  *******************************/

19/3 2026/08/07 11:54 llm kmsan-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 18109
							output: 208
							thoughts: 1584
						

Thoughts:
**KMSAN Assessment: PTP Clock Support for Microchip Azurite DPLL Driver**

Okay, so I'm looking at this patch that's adding PTP clock support to the Microchip Azurite DPLL driver, specifically the `drivers/dpll/zl3073x/` code. It looks like it's implementing functions to read, write, and adjust the Time of Day (ToD) registers, adjust the frequency and phase, and registering a PTP clock device for each DPLL channel. My immediate concern, as always, is uninitialized memory. Let's see if this code introduces any risks here.

First, I see the `ptp_clock_info` struct is defined and initialized statically. That's good. In `zl3073x_dpll_alloc`, it is copied to `zldpll->ptp_info`. Crucially, `zl3073x_dpll_ptp_clock_info` is `static const`, so any uninitialized fields get zero-initialized by the compiler. The `name` field of this struct is populated using `snprintf` in `zl3073x_dpll_ptp_register`. This means the beginning of `name` is overwritten, but the rest of `name` that's not overwritten is zero. So, this looks safe.

The `zl3073x_chan_tod_read` function looks straightforward. It reads hardware registers to populate the `ts->tv_sec` and `ts->tv_nsec` fields of a `struct timespec64`. Both values are initialized before returning.

The `zl3073x_dpll_ptp_adjtime_phase_step` uses `u16 synth_mask[ZL3073X_NUM_SYNTHS] = {};` which is zero initialized.

Next, I'm checking if there's any uninitialized memory passed to user space. The `ptp_clock_info` struct *is* passed to `ptp_clock_register`. I considered `name`, but as I said, the static `ptp_clock_info` struct is zero initialized, and the `snprintf` ensures at least the beginning of the `name` is valid and null terminated. The rest remains zero and is harmless.

Looking at `zl3073x_chan_tod_adjust`, `ts_next` and `ts_cur` are fully initialized within the function by `zl3073x_chan_tod_read`.

For `zl3073x_dpll_ptp_adjtime`, `ts` is also fully initialized with `tv_sec` and `tv_nsec`. No issues there.

This patch primarily deals with PTP clock registration and low-level hardware interactions. It's handling reads and writes to hardware registers and interacting with the PTP subsystem. I'm not seeing any complex structures or data being sent to user space with uninitialized fields. There also doesn't seem to be uninitialized variables being used in control flow. Given this, I don't believe KMSAN is necessary here. KASAN is already sufficient for memory safety concerns like out-of-bounds access and use-after-free, if present.

One last thing, the `ptp_system_timestamp *sts` parameter. In `zl3073x_chan_tod_read`, this is passed to `ptp_read_system_prets` and `ptp_read_system_postts`. This variable is supplied by the PTP core itself, and those functions are standard and will only write to sts if sts is not NULL. The PTP core handles its initialization. So no problem.

Therefore, my assessment is that KMSAN is not needed here.



20/3 2026/08/07 11:54 tool set-results
0m Args:
{
  "KMSANReasoning": "The patch introduces PTP clock support for the Microchip Azurite DPLL driver. It adds functions to read/write Time of Day (ToD) registers, adjust frequency, and adjust phase, as well as registering a PTP clock device. All local variables, such as `struct timespec64` and `synth_mask`, are fully initialized before use. The `ptp_clock_info` structure is initialized from a static const template, ensuring all fields (including padding and the `name` buffer) are zero-initialized before `snprintf` is called. There are no kernel-to-user info-leaks or uninitialized memory usages introduced by these changes. Any potential memory corruption, out-of-bounds accesses, or use-after-free bugs would be effectively caught by KASAN. Therefore, a dedicated KMSAN fuzzing session is not required.",
  "NeedsKMSAN": false
}

Results:
{
  "KMSANReasoning": "The patch introduces PTP clock support for the Microchip Azurite DPLL driver. It adds functions to read/write Time of Day (ToD) registers, adjust frequency, and adjust phase, as well as registering a PTP clock device. All local variables, such as `struct timespec64` and `synth_mask`, are fully initialized before use. The `ptp_clock_info` structure is initialized from a static const template, ensuring all fields (including padding and the `name` buffer) are zero-initialized before `snprintf` is called. There are no kernel-to-user info-leaks or uninitialized memory usages introduced by these changes. Any potential memory corruption, out-of-bounds accesses, or use-after-free bugs would be effectively caught by KASAN. Therefore, a dedicated KMSAN fuzzing session is not required.",
  "NeedsKMSAN": false
}

LLM Calls Summary:
Total Calls Total Tokens Avg Tokens Total Duration (Seconds) Avg Duration (Seconds)
Tool Calls Summary:
Total Calls Total Duration (Seconds) Avg Duration (Seconds)