Re: [PATCH net-next 1/6] dpll: zl3073x: notify sibling pin when shared output config changes

From: Ivan Vecera

Date: Wed Sep 30 2026 - 12:57:28 EST


On 9/30/26 9:49 AM, Ivan Vecera wrote:
[Severity: High]
Can sibling->dpll_pin be NULL, or the sibling already freed, by the time
it is dereferenced here?

zl3073x_dpll_output_pin_sibling_get() returns any output pin on
zldpll->pins with the matching id. It does not check that
sibling->dpll_pin is still set. The lookup runs under zldpll->lock, but
the dereference happens after mutex_unlock().

The teardown path does not take zldpll->lock:

drivers/dpll/zl3073x/dpll.c:zl3073x_dpll_pins_unregister() {
    list_for_each_entry_safe(pin, next, &zldpll->pins, list) {
        zl3073x_dpll_pin_unregister(pin);
        list_del(&pin->list);
        zl3073x_dpll_pin_free(pin);
    }
}

zl3073x_dpll_pin_unregister() ends with:

    dpll_pin_put(pin->dpll_pin, &pin->tracker);
    pin->dpll_pin = NULL;

and dpll_pin_put() has already dropped dpll_lock at that point.

zl3073x_dpll_pins_register() uses list_add(), so the N pin (2k+1) sits
before its P pin (2k) on the list. That means N is torn down while P is
still registered and reachable over netlink.

One possible interleaving:

CPU1 (devlink reload, flash_prepare or unbind)
zl3073x_dev_stop()
   zl3073x_dpll_unregister()
     zl3073x_dpll_pins_unregister()
       dpll_pin_put(N)          /* dpll_lock released */

CPU0 (DPLL_CMD_PIN_SET on P, dpll_lock held)
dpll_pin_freq_set()
   zl3073x_dpll_output_pin_frequency_set(P)
     mutex_lock(&zldpll->lock)
     sibling = zl3073x_dpll_output_pin_sibling_get(P)  /* finds N */
     mutex_unlock(&zldpll->lock)

CPU1
       N->dpll_pin = NULL
       list_del(&N->list)
       zl3073x_dpll_pin_free(N)

CPU0
     __dpll_pin_change_ntf(sibling->dpll_pin)

In that case, would __dpll_pin_change_ntf() get NULL and oops on
pin->id and pin->clock_id in dpll_pin_notify()? Or would CPU0 read the
freed zl3073x_dpll_pin?

The list walk in zl3073x_dpll_output_pin_sibling_get() can also run
while list_del() is in progress, because the writer never takes
zldpll->lock.

zl3073x_dpll_output_pin_frequency_set() and
zl3073x_dpll_output_pin_phase_adjust_set() have the same pattern.

The later patch "dpll: zl3073x: add PTP periodic output support" also
calls zl3073x_dpll_output_pin_sibling_get() from
zl3073x_dpll_ptp_enable() without holding zldpll->lock at all.

This cannot happen; the notification is already serialized against pin
teardown.

The output setters (esync_set / frequency_set / phase_adjust_set) run
with dpll_lock held: the dpll core invokes the pin ops under dpll_lock,
and they use __dpll_pin_change_ntf(), whose contract is exactly "caller
must hold dpll_lock" (dpll_netlink.c: lockdep_assert_held(&dpll_lock),
"suitable for use inside pin callbacks which are already invoked under
dpll_lock"). So sibling_get() and the __dpll_pin_change_ntf() call - even
though it runs after mutex_unlock(&zldpll->lock) - are all covered by
dpll_lock.

Pin teardown frees the sibling via zl3073x_dpll_pin_unregister() ->
dpll_pin_unregister(), which takes dpll_lock. While a setter holds
dpll_lock for the whole callback, teardown cannot even reach
dpll_pin_unregister(N), let alone the following list_del()/kfree(). The
proposed interleaving (CPU1 freeing N while CPU0 notifies) is therefore
impossible: CPU0 holds dpll_lock throughout.

The PTP enable() path (zl3073x_dpll_ptp_enable(), added in patch 6) does
call sibling_get() outside dpll_lock, but it is serialized differently:
zl3073x_dpll_unregister() unregisters the PTP clock *before* the pins
(zl3073x_dpll_ptp_unregister() then zl3073x_dpll_pins_unregister()), and
ptp_clock_unregister() quiesces in-flight enable() callbacks. No
enable() can run while the pins are being freed.


More thinking about it... and yes this can happen :-(
The pin removal from the list must be performed prior its unregistration
and also the list management has to be protected by zldpll->lock.

Will fix and send as bugfix to net branch with proper Fixes: tags.

Thanks,
Ivan