Re: [net-next,10/10] net: dsa: microchip: add periodic output support for the KSZ8463
From: netdev-bot+sashiko@kernel.org
Date: 2026-09-02 16:27:10
Also in:
lkml
Thank you for your contribution! Sashiko AI review found 7 potential issue(s) to consider: Critical: 0 · High: 1 (1) · Medium: 4 (1) · Low: 2 - [High] KSZ8463 advertises 12 periodic-output channels (.n_pins = 12 / .n_per_out = 12) but the driver keeps only one set of software state… - [Medium] Nothing in the new KSZ8463 path ever clears the per-unit enable bit in KSZ8463_TOU_EN (0x206). - [Medium] Build break with CONFIG_NET_DSA_MICROCHIP_KSZ_PTP=n: ksz8463_setup() (drivers/net/dsa/microchip/ksz8.c:2481) now calls… - [Medium] ksz8463_ptp_enable_perout() performs destructive actions before validating the request and never reconciles state on failure. - [Medium] ksz8463_ptp_configure_perout() (ksz_ptp.c:184) directly dereferences dev->ptp_data.pin_config[index], reading pin->chan to… - [Low] Stale comment in the shared helper ksz_ptp_compute_perout_pulse(): 'Use a duty cycle of 50%. - [Low] ksz8463_ptp_set_caps() (ksz_ptp.c:1335) does not set caps.supported_perout_flags, yet the KSZ8463 perout implementation… Pre-existing issues: - [High] A non-normalized periodic-output period is accepted and stored as zero nanoseconds, and the restart path then divides by it. - [Medium] ksz_ptp_prepare_restart_perout() stores the 64-bit quotient div_u64(now_ns - first_ns, period_ns) in an 'unsigned int count'… -- net: dsa: microchip: add periodic output support for the KSZ8463 This adds KSZ8463-specific periodic output support to the microchip DSA driver, with per-unit trigger configuration register blocks, a smaller maximum pulse width, and 12 advertised outputs instead of 2. A few questions on the new code below.
quoted hunk ↗ jump to hunk
diff --git a/drivers/net/dsa/microchip/ksz8.c b/drivers/net/dsa/microchip/ksz8.c index f2a23e2e474ac..6b08bb8fed5b7 100644 --- a/drivers/net/dsa/microchip/ksz8.c +++ b/drivers/net/dsa/microchip/ksz8.c@@ -2478,7 +2478,7 @@ static int ksz8463_setup(struct dsa_switch *ds) if (ret) goto free_girq; - ksz_ptp_set_caps(ds); + ksz8463_ptp_set_caps(ds);
[Severity: Medium]
Does this build with CONFIG_NET_DSA_MICROCHIP_KSZ_PTP=n?
ksz8.o is built unconditionally, while the Makefile only adds ksz_ptp.o
inside "ifdef CONFIG_NET_DSA_MICROCHIP_KSZ_PTP". In ksz_ptp.h, the new
declaration lives in the enabled branch:
#if IS_ENABLED(CONFIG_NET_DSA_MICROCHIP_KSZ_PTP)
...
void ksz8463_ptp_set_caps(struct dsa_switch *ds);
and the #else branch only provides the old stub:
static inline void ksz_ptp_set_caps(struct dsa_switch *ds) { }
Every other KSZ8463-specific PTP entry point (ksz8463_ptp_irq_setup,
ksz8463_ptp_irq_free, ksz8463_get_ts_info, ksz8463_hwtstamp_set) does have
a stub or #define in the #else branch. Should ksz8463_ptp_set_caps() get
one too, otherwise ksz8463_setup() is compiled with no declaration?
quoted hunk ↗ jump to hunk
diff --git a/drivers/net/dsa/microchip/ksz_common.c b/drivers/net/dsa/microchip/ksz_common.c index 194c88cdd00bc..111f3014e5a97 100644 --- a/drivers/net/dsa/microchip/ksz_common.c +++ b/drivers/net/dsa/microchip/ksz_common.c@@ -1161,6 +1161,8 @@ const struct ksz_chip_data ksz_switch_chips[] = { .supports_mii = {false, false, true}, .supports_rmii = {false, false, true}, .internal_phy = {true, true, false}, + .n_pins = 12, + .n_per_out = 12, },
quoted hunk ↗ jump to hunk
diff --git a/drivers/net/dsa/microchip/ksz_ptp.c b/drivers/net/dsa/microchip/ksz_ptp.c index 0a77b46cfad25..d33034892a8f6 100644 --- a/drivers/net/dsa/microchip/ksz_ptp.c +++ b/drivers/net/dsa/microchip/ksz_ptp.c
[ ... ]
quoted hunk ↗ jump to hunk
@@ -63,6 +64,17 @@ static int ksz_ptp_tou_gpio(struct ksz_device *dev) LED_SRC_PTP_GPIO_1 | LED_SRC_PTP_GPIO_2); } +static int ksz8463_ptp_tou_reset(struct ksz_device *dev, u8 unit) +{ + int ret; + + ret = ksz_rmw16(dev, KSZ8463_TOU_SW_RST, BIT(unit), BIT(unit)); + if (ret) + return ret; + + return ksz_rmw16(dev, KSZ8463_TOU_SW_RST, BIT(unit), 0); +} +
[Severity: Medium]
Should this also clear BIT(unit) in KSZ8463_TOU_EN?
The generic counterpart just below deliberately ends by clearing
TRIG_ENABLE (and write-1-clears the per-unit status bits):
/* Clear reset and set GPIO direction */
return ksz_rmw32(dev, REG_PTP_CTRL_STAT__4, (TRIG_RESET | TRIG_ENABLE),
0);
Here only KSZ8463_TOU_SW_RST is pulsed, and the disable path in
ksz8463_ptp_enable_perout() returns right after it:
ret = ksz8463_ptp_tou_reset(dev, request->index);
if (ret)
return ret;
if (!on) {
ptp_data->tou_mode = KSZ_PTP_TOU_IDLE;
return 0;
}
If asserting the software reset bit does not itself clear TOU_EN, does the
unit keep driving the pin after userspace disabled it while the driver
reports KSZ_PTP_TOU_IDLE?
There is a second consequence on the re-arm path, see the note on
ksz8463_ptp_tou_start() below.
static int ksz_ptp_tou_reset(struct ksz_device *dev, u8 unit)
{
u32 data;[ ... ]
quoted hunk ↗ jump to hunk
@@ -120,6 +132,28 @@ static int ksz_ptp_tou_target_time_set(struct ksz_device *dev, return 0; } +static int ksz8463_ptp_tou_start(struct ksz_device *dev, u8 unit) +{ + u16 data; + int ret; + + ret = ksz_rmw16(dev, KSZ8463_TOU_EN, BIT(unit), BIT(unit)); + if (ret) + return ret; + + ret = ksz_read16(dev, KSZ8463_TOU_ACTIVE, &data); + if (ret) + return ret; + + if (!(data & BIT(unit))) { + dev_err(dev->dev, "%s: Trigger unit%d error!\n", __func__, + unit); + return -EIO; + } + + return 0; +} +
[Severity: Medium]
Continuing the note above: if BIT(unit) is still set in KSZ8463_TOU_EN when
the restart path reaches here, does ksz_rmw16() actually emit a write?
The KSZ8463 regmap config uses .cache_type = REGCACHE_NONE with no
reg_update_bits, and _regmap_update_bits() only writes when the value
changes:
drivers/base/regmap/regmap.c:_regmap_update_bits() {
...
if (force_write || (tmp != orig) || map->force_write_field) {
ret = _regmap_write(map, reg, tmp);
...
}
So no 0->1 edge would be generated to latch the freshly written
TARGET_SEC/TARGET_NSEC, and the KSZ8463_TOU_ACTIVE check right after would
pass on the stale ACTIVE bit, making
ksz8463_ptp_settime()/ksz8463_ptp_adjtime() return success while the unit
still runs against the pre-step target time.
Also on the -EIO exit here, BIT(unit) is left set in KSZ8463_TOU_EN while
tou_mode is never advanced to KSZ_PTP_TOU_PEROUT. Should the enable bit be
cleared before returning the error?
quoted hunk ↗ jump to hunk
static int ksz_ptp_tou_start(struct ksz_device *dev, u8 unit) { u32 data;@@ -147,6 +181,56 @@ static int ksz_ptp_tou_start(struct ksz_device *dev, u8 unit) return 0; } +static int ksz8463_ptp_configure_perout(struct ksz_device *dev, + u32 cycle_width_ns, u32 pulse_width_ns, + struct timespec64 const *target_time, + u8 index) +{ + struct ptp_pin_desc *pin = &dev->ptp_data.pin_config[index]; + u16 cfg_base = KSZ8463_TRIG1_CFG + KSZ8463_TRIGN_CFG_SIZE * pin->chan;
[Severity: Medium]
Is pin_config[] safe to dereference here on the settime/adjtime path?
pin_config[] is owned by the PTP core and protected by
ptp_clock::pincfg_mux, which the core holds only around ->enable():
drivers/ptp/ptp_chardev.c:ptp_perout_request() {
...
scoped_cond_guard(mutex_intr, return -ERESTARTSYS, &ptp->pincfg_mux)
return ops->enable(ops, &req, perout->period.sec || perout->period.nsec);
}
The new path ksz8463_ptp_settime()/ksz8463_ptp_adjtime() ->
ksz8463_ptp_restart_perout() -> ksz8463_ptp_enable_perout() ->
ksz8463_ptp_configure_perout() only holds the driver-private
ptp_data->lock. Can a concurrent PTP_PIN_SETFUNC change pin->chan
underneath, so that cfg_base points at a different trigger unit than
BIT(request->index) which ksz8463_ptp_tou_start() enables?
Note that simply taking pincfg_mux here would invert the order used by the
->enable() path (pincfg_mux then ptp_data->lock); caching pin/chan under
pincfg_mux at enable() time would avoid that.
+ u16 data; + int ret; + + /* Hardware has only 32 bit */ + if ((target_time->tv_sec & 0xffffffff) != target_time->tv_sec) + return -EINVAL; + + data = KSZ8463_NOTIFY_BIT | + FIELD_PREP(KSZ8463_PATTERN_M, TRIG_POS_PERIOD) | + pin->index; + ret = ksz_write16(dev, cfg_base + KSZ8463_PATTERN_OFF, data); + if (ret) + return ret;
[ ... ]
+ return 0; +} + static int ksz_ptp_configure_perout(struct ksz_device *dev, u32 cycle_width_ns, u32 pulse_width_ns, struct timespec64 const *target_time,
[ ... ]
quoted hunk ↗ jump to hunk
@@ -241,6 +325,58 @@ static u64 ksz_ptp_compute_perout_pulse(struct ksz_device *dev, return min_t(u64, req_pulse_width_ns, max_pulse_width); } +static int ksz8463_ptp_enable_perout(struct ksz_device *dev, + struct ptp_perout_request const *request, + int on) +{ + struct ksz_ptp_data *ptp_data = &dev->ptp_data; + u64 cycle_width_ns; + u64 pulse_width_ns; + int pin; + int ret; + + pin = ksz_ptp_get_pin(dev, request); + if (pin < 0) + return pin; + + ret = ksz8463_ptp_tou_reset(dev, request->index); + if (ret) + return ret; + + if (!on) { + ptp_data->tou_mode = KSZ_PTP_TOU_IDLE; + return 0; + } + ret = ksz_ptp_compute_perout_cycle(dev, request, &cycle_width_ns); + if (ret) + return ret;
[Severity: Medium]
Can a rejected request leave the driver and hardware out of sync here?
The unit is reset before any of the timing values are validated, and
ksz_ptp_compute_perout_cycle() stores the requested period before deciding
it is out of range:
ptp_data->perout_period.tv_sec = request->period.sec;
ptp_data->perout_period.tv_nsec = request->period.nsec;
*cycle_width_ns = timespec64_to_ns(&ptp_data->perout_period);
if ((*cycle_width_ns & TRIG_CYCLE_WIDTH_M) != *cycle_width_ns) {
*cycle_width_ns = 0;
return -EINVAL;
}
So with a perout already active (tou_mode == KSZ_PTP_TOU_PEROUT), a
PTP_PEROUT_REQUEST with an out-of-range period stops the running unit,
overwrites perout_period with the rejected value, and returns while
tou_mode still says PEROUT. Every later
ksz8463_ptp_settime()/ksz8463_ptp_adjtime() then rebuilds the request from
that stored period and fails again, after the RTC has already been
written. An SPI error in one of the six writes in
ksz8463_ptp_configure_perout() gives the same half-programmed state.
Should validation happen before the unit reset and before the state is
committed?
+ pulse_width_ns = ksz_ptp_compute_perout_pulse(dev, request, + KSZ8463_MAX_PULSE_WIDTH);
[Severity: Low] This isn't a bug, but the comment in ksz_ptp_compute_perout_pulse() is now stale for this caller: /* Use a duty cycle of 50%. Maximum pulse width supported by the * hardware is a little bit more than 125 ms. */ The maximum is a parameter now, and this call passes KSZ8463_MAX_PULSE_WIDTH = 500000LL, i.e. 0.5 ms. Could the comment be made generic or moved to the callers?
+ + ret = ksz_ptp_tou_pulse_verify(pulse_width_ns, + KSZ8463_TRIG_PULSE_WIDTH_M); + if (ret) + return ret; + + ret = ksz8463_ptp_configure_perout(dev, cycle_width_ns, pulse_width_ns, + &ptp_data->perout_target_time_first, + pin); + if (ret) + return ret; + + ret = ksz_ptp_tou_gpio(dev); + if (ret) + return ret; + + ret = ksz8463_ptp_tou_start(dev, request->index); + if (ret) + return ret; + + ptp_data->tou_mode = KSZ_PTP_TOU_PEROUT; + + return 0; +} + static int ksz_ptp_enable_perout(struct ksz_device *dev, struct ptp_perout_request const *request, int on)
[ ... ]
quoted hunk ↗ jump to hunk
@@ -845,6 +981,18 @@ static int ksz_ptp_restart_perout(struct ksz_device *dev) return ksz_ptp_enable_perout(dev, &request, 1); } +static int ksz8463_ptp_restart_perout(struct ksz_device *dev) +{ + struct ptp_perout_request request; + int ret; + + ret = ksz_ptp_prepare_restart_perout(dev, &request); + if (ret) + return ret; + + return ksz8463_ptp_enable_perout(dev, &request, 1); +} +
[Severity: High]
Does this work for channels other than 0?
ksz_ptp_prepare_restart_perout() hardcodes the channel and drops the flags:
request->index = 0;
request->flags = 0;
but this chip now advertises .n_pins = 12 and .n_per_out = 12, and
struct ksz_ptp_data keeps a single tou_mode / perout_target_time_first /
perout_period for all of them.
With a perout armed on, say, channel 3 (PTP_PIN_SETFUNC accepts it,
ksz_ptp_verify_pin() only filters on func and the core only checks
chan < n_per_out), the restart path becomes:
ksz8463_ptp_adjtime()
ksz8463_ptp_restart_perout()
ksz_ptp_prepare_restart_perout() /* index = 0 */
ksz8463_ptp_enable_perout()
ksz_ptp_get_pin()
ptp_find_pin(clock, PTP_PF_PEROUT, 0) /* no such pin */
so -EINVAL is returned, and this happens after __ksz_ptp_adjtime() has
already stepped the RTC:
ret = __ksz_ptp_adjtime(dev, delta);
if (ret)
goto unlock;
...
case KSZ_PTP_TOU_PEROUT:
ret = ksz8463_ptp_restart_perout(dev);
if (ret)
goto unlock;
The "goto unlock" also skips the ptp_data->clock_time update. Since
ptp4l/phc2sys call adjtime/adjfine continuously, would every PHC adjust
fail for 11 of the 12 channels the commit advertises? And if some pin does
happen to be mapped to channel 0, unit 0 gets reprogrammed with whatever
timing is stored in the single set of fields while the genuinely active
unit keeps its pre-step target.
Related to the single set of state fields: units 1..11 are never re-armed,
"request->flags = 0" discards PTP_PEROUT_DUTY_CYCLE on every restart,
enabling a second channel overwrites the first channel's stored timing, and
disabling any one channel sets the global tou_mode to KSZ_PTP_TOU_IDLE
while the other units stay enabled in hardware. Should the per-unit state
be tracked per channel, and should the restart carry the real index?
[Severity: High]
This isn't a bug introduced by this patch, but the new caller of
ksz_ptp_prepare_restart_perout() adds a second path to it, so it is worth
raising: can a stored period of zero nanoseconds divide by zero here?
struct ptp_clock_time has a signed sec and an unsigned nsec, and
ptp_perout_request() validates flags, reserved fields, duty
cycle/phase and "index < n_per_out", but never normalization or
positivity; it decides "on" from "period.sec || period.nsec". A period of
{sec = -1, nsec = 1000000000} is therefore treated as an enable, while
timespec64_to_ns() evaluates it to 0, which passes both checks:
*cycle_width_ns = timespec64_to_ns(&ptp_data->perout_period);
if ((*cycle_width_ns & TRIG_CYCLE_WIDTH_M) != *cycle_width_ns) {
and ksz_ptp_tou_pulse_verify(0) passes too. A later settime/adjtime then
reaches:
period_ns = timespec64_to_ns(&ptp_data->perout_period);
if (first_ns < now_ns) {
count = div_u64(now_ns - first_ns, period_ns);
...
while (next_ns < now_ns + 100000000)
next_ns += period_ns;
which is either a divide-error with a zero divisor, or a loop that never
advances, both with ptp_data->lock held. Should non-normalized times be
rejected and the converted period required to be strictly positive before
it is stored?
[Severity: Medium]
This is a pre-existing issue and not introduced by this patch, but this new
function adds another caller: ksz_ptp_prepare_restart_perout() stores the
64-bit quotient in an unsigned int:
unsigned int count;
...
if (first_ns < now_ns) {
count = div_u64(now_ns - first_ns, period_ns);
next_ns = first_ns + count * period_ns;
...
/* Ensure 100 ms guard time prior next event */
while (next_ns < now_ns + 100000000)
next_ns += period_ns;
Once more than UINT_MAX periods have elapsed (about 72 minutes with a 1 us
period, about 50 days with 1 ms), count truncates and the guard-time loop
has to catch up one period at a time, up to roughly 4.29e9 iterations with
ptp_data->lock held. Would a u64 count avoid that stall?
static int __ksz_ptp_settime(struct ksz_device *dev,
const struct timespec64 *ts)
{[ ... ]
quoted hunk ↗ jump to hunk
@@ -1095,6 +1332,27 @@ static int ksz_ptp_start_clock(struct ksz_device *dev) return 0; } +void ksz8463_ptp_set_caps(struct dsa_switch *ds) +{ + struct ksz_device *dev = ds->priv; + struct ksz_ptp_data *ptp_data; + + ptp_data = &dev->ptp_data; + + ptp_data->caps.owner = THIS_MODULE; + snprintf(ptp_data->caps.name, 16, "Microchip Clock"); + ptp_data->caps.max_adj = KSZ_MAX_DRIFT_CORR; + ptp_data->caps.gettime64 = ksz_ptp_gettime; + ptp_data->caps.settime64 = ksz8463_ptp_settime; + ptp_data->caps.adjfine = ksz_ptp_adjfine; + ptp_data->caps.adjtime = ksz8463_ptp_adjtime; + ptp_data->caps.do_aux_work = ksz_ptp_do_aux_work; + ptp_data->caps.enable = ksz8463_ptp_enable; + ptp_data->caps.verify = ksz_ptp_verify_pin; + ptp_data->caps.n_pins = dev->info->n_pins; + ptp_data->caps.n_per_out = dev->info->n_per_out; +} +
[Severity: Low]
Should caps.supported_perout_flags be set to PTP_PEROUT_DUTY_CYCLE here?
The implementation accepts that flag:
ksz_ptp_get_pin():
if (request->flags & ~PTP_PEROUT_DUTY_CYCLE)
return -EOPNOTSUPP;
and ksz_ptp_compute_perout_pulse() consumes request->on when it is set, but
the core rejects it first:
drivers/ptp/ptp_chardev.c:ptp_perout_request() {
...
if (perout->flags & ~ops->supported_perout_flags)
return -EOPNOTSUPP;
}
so with supported_perout_flags left at 0 the duty-cycle handling is
unreachable. The same omission exists in ksz_ptp_set_caps().
quoted hunk ↗ jump to hunk
void ksz_ptp_set_caps(struct dsa_switch *ds) { struct ksz_device *dev = ds->priv;diff --git a/drivers/net/dsa/microchip/ksz_ptp.h b/drivers/net/dsa/microchip/ksz_ptp.h index 89716b31596b5..92589cf373ac2 100644 --- a/drivers/net/dsa/microchip/ksz_ptp.h +++ b/drivers/net/dsa/microchip/ksz_ptp.h@@ -12,7 +12,7 @@ #include <linux/ptp_clock_kernel.h> -#define KSZ_PTP_N_GPIO 2 +#define KSZ_PTP_N_GPIO 12 enum ksz_ptp_tou_mode { KSZ_PTP_TOU_IDLE,@@ -34,6 +34,7 @@ struct ksz_ptp_data { }; void ksz_ptp_set_caps(struct dsa_switch *ds); +void ksz8463_ptp_set_caps(struct dsa_switch *ds); int ksz_ptp_clock_register(struct dsa_switch *ds); void ksz_ptp_clock_unregister(struct dsa_switch *ds);
-- Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260831-ksz-perout-v1-0-14202db763b3%40bootlin.com