* [PATCH net-next 00/10] dpll: do not serialize driver callbacks under global dpll_lock
@ 2026-11-06 12:09 Ivan Vecera
2026-11-06 12:09 ` [PATCH net-next 01/10] ice: dpll: notify TSPLL lock status from the periodic work only Ivan Vecera
` (9 more replies)
0 siblings, 10 replies; 27+ messages in thread
From: Ivan Vecera @ 2026-11-06 12:09 UTC (permalink / raw)
To: netdev
Cc: Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
Jonathan Corbet, Shuah Khan, Randy Dunlap, Min Li, Tony Nguyen,
Przemek Kitszel, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Richard Cochran, linux-doc,
linux-kernel, intel-wired-lan, Grzegorz Nitka, Karol Kolacinski,
Saeed Mahameed, Tariq Toukan, Petr Oros
dpll_lock protects topology and object lifetime, but it also serializes
all driver callbacks in the system, so one slow device stalls every other
DPLL device, whichever driver it belongs to. mlx5 callbacks issue firmware
commands with a 60 s default timeout, and zl3073x callbacks can wait
seconds on a driver lock held across ToD writes and phase steps.
Make dpll_lock an rwsem for topology and lifetime and add a mutex per
domain for callbacks. A domain is all objects created with one (module,
clock_id), in practice one chip or card, so channels of one chip stay
serialized as drivers expect, and they still block each other. Device
callbacks run under the domain of the device, pin callbacks under the
domain of the pin, whichever driver registered them. Only one domain lock
is ever held, so domains have no lock order. A lock per device would not
work, because pins are N:M with devices and callbacks notify sibling
channels.
The driver API does not change, but a callback can now notify only
objects of its own domain. Patch 1 changes the one in-tree callback that
does not follow this.
Registrations across domains exist in the tree only on E825, where ice
registers fwnode pins of an external DPLL with its TXC and TSPLL DPLLs.
These callbacks are no longer serialized with TXC and TSPLL, they rely on
pf->dplls.lock, and requests about these pins wait while the external
DPLL is busy. Intel folks, please check that this is fine.
Each complex of a dual complex E825 has its own DSN and so its own
domain, but both use the CGU of complex 0. Patch 2 adds a lock for the
registers they share. Without it a test lost updates of them.
Patch 5 is the only patch that lets callbacks run concurrently. Patch 6
adds KUnit tests. I also tested each commit with a mock driver and YNL in
QEMU, with lockdep and KASAN, and the tip with lockdep on two systems:
- A board with ZL30732A: every notification the driver sends from a
callback, concurrent traffic with PTP, driver reload and rebind.
- GNR-D with dual complex E825 and zl3073x: callbacks of the pins shared
by both drivers, TX clock switches, RCLK pins of both complexes, a port
going down and up, and both drivers unloaded and loaded in either order
under load.
mlx5 is build tested only.
On GNR-D, with ToD writes running on the zl3073x and requests waiting for
them, requests to an ice DPLL took 1448 ms in median and 3.9 s at most
before this series, 0.2 ms and 0.3 ms with it, the same as when idle.
Requests about the pins shared by both drivers still wait for the zl3073x.
Ivan Vecera (10):
ice: dpll: notify TSPLL lock status from the periodic work only
ice: dpll: serialize changes of the SyncE CGU registers
dpll: add domain object
dpll: take domain locks around driver callbacks
dpll: convert dpll_lock to rwsem
dpll: add KUnit tests for core locking
dpll: reject measured_freq_get for foreign and on-pin registrations
dpll: zl3073x: update locking comments
ice: dpll: update locking comments
dpll: document locking rules
Documentation/driver-api/dpll.rst | 44 +
drivers/dpll/.kunitconfig | 5 +
drivers/dpll/Kconfig | 13 +
drivers/dpll/Makefile | 1 +
drivers/dpll/dpll_core.c | 187 ++-
drivers/dpll/dpll_core.h | 34 +-
drivers/dpll/dpll_core_kunit.c | 1573 ++++++++++++++++++
drivers/dpll/dpll_netlink.c | 117 +-
drivers/dpll/zl3073x/dpll.c | 9 +-
drivers/net/ethernet/intel/ice/ice_adapter.c | 2 +
drivers/net/ethernet/intel/ice/ice_adapter.h | 4 +
drivers/net/ethernet/intel/ice/ice_dpll.c | 29 +-
drivers/net/ethernet/intel/ice/ice_dpll.h | 7 +
drivers/net/ethernet/intel/ice/ice_ptp.c | 2 +
drivers/net/ethernet/intel/ice/ice_tspll.c | 4 +-
15 files changed, 1915 insertions(+), 116 deletions(-)
create mode 100644 drivers/dpll/.kunitconfig
create mode 100644 drivers/dpll/dpll_core_kunit.c
base-commit: 6584acd42820478910a771efb28a355a707e9cb0
--
2.56.0
^ permalink raw reply [flat|nested] 27+ messages in thread
* [PATCH net-next 01/10] ice: dpll: notify TSPLL lock status from the periodic work only
2026-11-06 12:09 [PATCH net-next 00/10] dpll: do not serialize driver callbacks under global dpll_lock Ivan Vecera
@ 2026-11-06 12:09 ` Ivan Vecera
2026-11-10 12:20 ` Petr Oros
2026-11-10 17:53 ` netdev-bot+sashiko
2026-11-06 12:09 ` [PATCH net-next 02/10] ice: dpll: serialize changes of the SyncE CGU registers Ivan Vecera
` (8 subsequent siblings)
9 siblings, 2 replies; 27+ messages in thread
From: Ivan Vecera @ 2026-11-06 12:09 UTC (permalink / raw)
To: netdev
Cc: Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
Jonathan Corbet, Shuah Khan, Randy Dunlap, Min Li, Tony Nguyen,
Przemek Kitszel, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Richard Cochran, linux-doc,
linux-kernel, intel-wired-lan, Grzegorz Nitka, Karol Kolacinski,
Saeed Mahameed, Tariq Toukan, Petr Oros
ice_dpll_tspll_state_on_dpll_set() notifies the TSPLL device from a
callback of the TIME_REF pin. This pin belongs to an external DPLL and the
DPLL core is going to run its callbacks under a lock of the pin's owner,
which does not cover the TSPLL device.
Only update dpll_state in the callback, so a device get reports the new
state right away, and leave prev_dpll_state to
ice_dpll_periodic_work_e825(), which then sends the notification. It now
arrives up to 500 ms later and after the reply to the request.
Refresh dpll_state on every run of the work, as the state written by the
callback can be transient and the lock status can return to
prev_dpll_state before the next run. No notification is sent in this case.
Signed-off-by: Ivan Vecera <ivecera@redhat.com>
---
drivers/net/ethernet/intel/ice/ice_dpll.c | 14 +++++---------
1 file changed, 5 insertions(+), 9 deletions(-)
diff --git a/drivers/net/ethernet/intel/ice/ice_dpll.c b/drivers/net/ethernet/intel/ice/ice_dpll.c
index ade1fb3dd06743..bdbcf7af296eb4 100644
--- a/drivers/net/ethernet/intel/ice/ice_dpll.c
+++ b/drivers/net/ethernet/intel/ice/ice_dpll.c
@@ -2914,7 +2914,6 @@ ice_dpll_tspll_state_on_dpll_set(const struct dpll_pin *pin, void *pin_priv,
struct ice_dpll_pin *p = pin_priv;
struct ice_pf *pf = p->pf;
enum ice_clk_src clk_src;
- bool changed = false;
struct ice_dpll *tp;
int ret;
@@ -2947,15 +2946,12 @@ ice_dpll_tspll_state_on_dpll_set(const struct dpll_pin *pin, void *pin_priv,
WRITE_ONCE(pf->ptp.tspll_locked,
new_state == DPLL_LOCK_STATUS_LOCKED);
- if (tp->prev_dpll_state != new_state) {
- tp->dpll_state = new_state;
- tp->prev_dpll_state = new_state;
- changed = true;
- }
+ /* ice_dpll_periodic_work_e825() compares the state with
+ * prev_dpll_state and sends the notification.
+ */
+ tp->dpll_state = new_state;
}
mutex_unlock(&pf->dplls.lock);
- if (changed)
- __dpll_device_change_ntf(tp->dpll);
/* TSPLL reconfiguration may complete before lock is reacquired.
* PTP periodic monitoring tracks and restores the final lock state.
*/
@@ -3372,8 +3368,8 @@ static void ice_dpll_periodic_work_e825(struct ice_pf *pf)
mutex_lock(&pf->dplls.lock);
new_state = ice_dpll_tspll_lock_status_get(pf, true);
+ tp->dpll_state = new_state;
if (tp->prev_dpll_state != new_state) {
- tp->dpll_state = new_state;
tp->prev_dpll_state = new_state;
changed = true;
}
--
2.56.0
^ permalink raw reply [flat|nested] 27+ messages in thread
* [PATCH net-next 02/10] ice: dpll: serialize changes of the SyncE CGU registers
2026-11-06 12:09 [PATCH net-next 00/10] dpll: do not serialize driver callbacks under global dpll_lock Ivan Vecera
2026-11-06 12:09 ` [PATCH net-next 01/10] ice: dpll: notify TSPLL lock status from the periodic work only Ivan Vecera
@ 2026-11-06 12:09 ` Ivan Vecera
2026-11-10 17:53 ` netdev-bot+sashiko
2026-11-06 12:09 ` [PATCH net-next 03/10] dpll: add domain object Ivan Vecera
` (7 subsequent siblings)
9 siblings, 1 reply; 27+ messages in thread
From: Ivan Vecera @ 2026-11-06 12:09 UTC (permalink / raw)
To: netdev
Cc: Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
Jonathan Corbet, Shuah Khan, Randy Dunlap, Min Li, Tony Nguyen,
Przemek Kitszel, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Richard Cochran, linux-doc,
linux-kernel, intel-wired-lan, Grzegorz Nitka, Karol Kolacinski,
Saeed Mahameed, Tariq Toukan, Petr Oros
On dual complex E825 only complex 0 has a functional CGU and the second
complex reaches it through cgu_peer, so PFs of both complexes share the
CGU registers R10 and R11. They read, modify and write them when the
state of an RCLK pin changes, and ice_ptp_link_change() rewrites R10 when
a link changes, under the pf->dplls.lock of their own PF only.
ice_ptp_link_change() does not hold dpll_lock, so it can already overwrite
an update of another PF. RCLK callbacks are serialized by dpll_lock, but
the DPLL core is going to serialize callbacks per (module, clock_id), and
each complex has its own DSN and so its own clock_id. With that change, a
test toggling RCLK pins of both complexes lost an update.
Add a mutex to struct ice_adapter, which all E825C PFs share, and hold it
around these sequences.
Signed-off-by: Ivan Vecera <ivecera@redhat.com>
---
drivers/net/ethernet/intel/ice/ice_adapter.c | 2 ++
drivers/net/ethernet/intel/ice/ice_adapter.h | 4 ++++
drivers/net/ethernet/intel/ice/ice_dpll.c | 11 ++++++-----
drivers/net/ethernet/intel/ice/ice_dpll.h | 4 ++++
drivers/net/ethernet/intel/ice/ice_ptp.c | 2 ++
drivers/net/ethernet/intel/ice/ice_tspll.c | 4 ++--
6 files changed, 20 insertions(+), 7 deletions(-)
diff --git a/drivers/net/ethernet/intel/ice/ice_adapter.c b/drivers/net/ethernet/intel/ice/ice_adapter.c
index 536923b6ae979d..aa2d8addae5bef 100644
--- a/drivers/net/ethernet/intel/ice/ice_adapter.c
+++ b/drivers/net/ethernet/intel/ice/ice_adapter.c
@@ -72,6 +72,7 @@ static struct ice_adapter *ice_adapter_new(struct pci_dev *pdev)
spin_lock_init(&adapter->txq_ctx_lock);
for (int i = 0; i < ARRAY_SIZE(adapter->cpi_phy_lock); i++)
mutex_init(&adapter->cpi_phy_lock[i]);
+ mutex_init(&adapter->cgu_synce_lock);
refcount_set(&adapter->refcount, 1);
spin_lock_init(&adapter->ports.lock);
@@ -85,6 +86,7 @@ static void ice_adapter_free(struct ice_adapter *adapter)
WARN_ON(!list_empty(&adapter->ports.list));
for (int i = 0; i < ARRAY_SIZE(adapter->cpi_phy_lock); i++)
mutex_destroy(&adapter->cpi_phy_lock[i]);
+ mutex_destroy(&adapter->cgu_synce_lock);
cleanup_srcu_struct(&adapter->ports.srcu);
diff --git a/drivers/net/ethernet/intel/ice/ice_adapter.h b/drivers/net/ethernet/intel/ice/ice_adapter.h
index c61bd645e460de..fd5585005d91ba 100644
--- a/drivers/net/ethernet/intel/ice/ice_adapter.h
+++ b/drivers/net/ethernet/intel/ice/ice_adapter.h
@@ -41,6 +41,8 @@ struct ice_port_list {
* @txq_ctx_lock: Spinlock protecting access to the GLCOMM_QTX_CNTX_CTL register
* @cpi_phy_lock: Per-PHY mutex serializing CPI REQ/ACK transactions.
* Index 0 = PHY0, index 1 = PHY1. Used on E825C devices.
+ * @cgu_synce_lock: Mutex serializing changes of the CGU registers R10 and R11,
+ * which all PFs reach. Used on E825C devices.
* @ctrl_pf: Control PF of the adapter
* @ports: Ports list
* @index: 64-bit index cached for collision detection on 32bit systems
@@ -53,6 +55,8 @@ struct ice_adapter {
spinlock_t txq_ctx_lock;
/* Serialize CPI REQ/ACK transactions per PHY (E825C only) */
struct mutex cpi_phy_lock[ICE_E825_MAX_PHYS];
+ /* Serialize changes of the SyncE CGU registers (E825C only) */
+ struct mutex cgu_synce_lock;
struct ice_pf *ctrl_pf;
struct ice_port_list ports;
diff --git a/drivers/net/ethernet/intel/ice/ice_dpll.c b/drivers/net/ethernet/intel/ice/ice_dpll.c
index bdbcf7af296eb4..d6302409a2dacf 100644
--- a/drivers/net/ethernet/intel/ice/ice_dpll.c
+++ b/drivers/net/ethernet/intel/ice/ice_dpll.c
@@ -2027,7 +2027,7 @@ ice_dpll_phase_offset_get(const struct dpll_pin *pin, void *pin_priv,
*
* DPLL subsystem callback. Set proper signals to recover clock from port.
*
- * Context: Called under pf->dplls.lock
+ * Context: Called under pf->dplls.lock, acquires adapter->cgu_synce_lock
* Return:
* * 0 - success
* * negative - error
@@ -2035,14 +2035,15 @@ ice_dpll_phase_offset_get(const struct dpll_pin *pin, void *pin_priv,
static int ice_dpll_synce_update_e825c(struct ice_hw *hw, bool ena,
u32 port_num, enum ice_synce_clk output)
{
+ struct ice_pf *pf = hw->back;
int err;
+ mutex_lock(&pf->adapter->cgu_synce_lock);
/* configure the mux to deliver proper signal to DPLL from the MUX */
err = ice_tspll_cfg_bypass_mux_e825c(hw, ena, port_num, output);
- if (err)
- return err;
-
- err = ice_tspll_cfg_synce_ethdiv_e825c(hw, output);
+ if (!err)
+ err = ice_tspll_cfg_synce_ethdiv_e825c(hw, output);
+ mutex_unlock(&pf->adapter->cgu_synce_lock);
if (err)
return err;
diff --git a/drivers/net/ethernet/intel/ice/ice_dpll.h b/drivers/net/ethernet/intel/ice/ice_dpll.h
index bce0bea9a664bc..4ba76e2e2e0b01 100644
--- a/drivers/net/ethernet/intel/ice/ice_dpll.h
+++ b/drivers/net/ethernet/intel/ice/ice_dpll.h
@@ -158,6 +158,7 @@ struct ice_dpll {
* txclk_notify_rwsem (read)
* -> pf->dplls.lock
* -> ctrl_pf->dplls.lock
+ * -> adapter->cgu_synce_lock
*
* - @lock serializes all DPLL state mutations on this PF. When the
* controlling PF's lock must also be taken (e.g. updating the shared
@@ -169,6 +170,9 @@ struct ice_dpll {
* dpll_*_change_ntf() calls. ice_dpll_deinit() takes the write side
* standalone (not nested under any other lock) to drain in-flight
* readers before pins and the TXC DPLL device are freed.
+ * - adapter->cgu_synce_lock serializes changes of the CGU registers R10 and
+ * R11. All PFs of a dual complex E825 reach one CGU, so @lock does not
+ * cover them.
*/
struct ice_dplls {
struct kthread_worker *kworker;
diff --git a/drivers/net/ethernet/intel/ice/ice_ptp.c b/drivers/net/ethernet/intel/ice/ice_ptp.c
index 3df08714510da5..4d4b6aaaf30c29 100644
--- a/drivers/net/ethernet/intel/ice/ice_ptp.c
+++ b/drivers/net/ethernet/intel/ice/ice_ptp.c
@@ -1395,7 +1395,9 @@ void ice_ptp_link_change(struct ice_pf *pf, bool linkup)
break;
}
+ mutex_lock(&pf->adapter->cgu_synce_lock);
err = ice_tspll_cfg_synce_ethdiv_e825c(hw, clk_pin);
+ mutex_unlock(&pf->adapter->cgu_synce_lock);
if (active && err) {
dev_err_once(ice_pf_to_dev(pf),
"Failed to configure SyncE ETH divider for pin %d, err %d\n",
diff --git a/drivers/net/ethernet/intel/ice/ice_tspll.c b/drivers/net/ethernet/intel/ice/ice_tspll.c
index 85bacb7c3a7903..b99e02cfe559bd 100644
--- a/drivers/net/ethernet/intel/ice/ice_tspll.c
+++ b/drivers/net/ethernet/intel/ice/ice_tspll.c
@@ -792,7 +792,7 @@ int ice_tspll_bypass_mux_active_e825c(struct ice_hw *hw, u8 port, bool *active,
*
* Set reference clock source and output clock selection.
*
- * Context: Called under pf->dplls.lock
+ * Context: Called under pf->dplls.lock and adapter->cgu_synce_lock
* Return:
* * 0 - success
* * negative - error
@@ -898,7 +898,7 @@ static int ice_tspll_get_div_e825c(u16 link_speed, unsigned int *divider)
*
* Set the correct CGU divider for RCLKA or RCLKB.
*
- * Context: Called under pf->dplls.lock
+ * Context: Called under pf->dplls.lock and adapter->cgu_synce_lock
* Return:
* * 0 - success
* * negative - error
--
2.56.0
^ permalink raw reply [flat|nested] 27+ messages in thread
* [PATCH net-next 03/10] dpll: add domain object
2026-11-06 12:09 [PATCH net-next 00/10] dpll: do not serialize driver callbacks under global dpll_lock Ivan Vecera
2026-11-06 12:09 ` [PATCH net-next 01/10] ice: dpll: notify TSPLL lock status from the periodic work only Ivan Vecera
2026-11-06 12:09 ` [PATCH net-next 02/10] ice: dpll: serialize changes of the SyncE CGU registers Ivan Vecera
@ 2026-11-06 12:09 ` Ivan Vecera
2026-11-10 12:23 ` Petr Oros
2026-11-10 17:53 ` netdev-bot+sashiko
2026-11-06 12:09 ` [PATCH net-next 04/10] dpll: take domain locks around driver callbacks Ivan Vecera
` (6 subsequent siblings)
9 siblings, 2 replies; 27+ messages in thread
From: Ivan Vecera @ 2026-11-06 12:09 UTC (permalink / raw)
To: netdev
Cc: Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
Jonathan Corbet, Shuah Khan, Randy Dunlap, Min Li, Tony Nguyen,
Przemek Kitszel, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Richard Cochran, linux-doc,
linux-kernel, intel-wired-lan, Grzegorz Nitka, Karol Kolacinski,
Saeed Mahameed, Tariq Toukan, Petr Oros
All dpll device and pin callbacks are serialized by the global dpll_lock,
so a slow device stalls all others, whichever driver they belong to.
Add struct dpll_domain as a preparation for serializing callbacks per
driver instance. A domain groups all devices and pins created with the
same (module, clock_id) pair, which is in practice one chip or card. The
same pair already identifies the owner of a pin in
dpll_pin_own_dpll_ref_first().
Domains are kept in an xarray, like devices and pins, and they live only as
long as their objects.
No functional change.
Signed-off-by: Ivan Vecera <ivecera@redhat.com>
---
drivers/dpll/dpll_core.c | 61 ++++++++++++++++++++++++++++++++++++++++
drivers/dpll/dpll_core.h | 27 ++++++++++++++++++
2 files changed, 88 insertions(+)
diff --git a/drivers/dpll/dpll_core.c b/drivers/dpll/dpll_core.c
index 8389983f425695..d39c4905172f2d 100644
--- a/drivers/dpll/dpll_core.c
+++ b/drivers/dpll/dpll_core.c
@@ -24,6 +24,7 @@ DEFINE_MUTEX(dpll_lock);
DEFINE_XARRAY_FLAGS(dpll_device_xa, XA_FLAGS_ALLOC);
DEFINE_XARRAY_FLAGS(dpll_pin_xa, XA_FLAGS_ALLOC);
+static DEFINE_XARRAY_FLAGS(dpll_domain_xa, XA_FLAGS_ALLOC);
static RAW_NOTIFIER_HEAD(dpll_notifier_chain);
static DEFINE_IDA(dpll_pin_idx_ida);
@@ -88,6 +89,50 @@ void dpll_pin_notify(struct dpll_pin *pin, u64 src_clock_id,
call_dpll_notifiers(action, &info);
}
+static struct dpll_domain *dpll_domain_get(u64 clock_id, struct module *module)
+{
+ struct dpll_domain *domain;
+ unsigned long i;
+ int ret;
+
+ lockdep_assert_held(&dpll_lock);
+
+ xa_for_each(&dpll_domain_xa, i, domain) {
+ if (domain->clock_id == clock_id && domain->module == module) {
+ refcount_inc(&domain->refcount);
+ return domain;
+ }
+ }
+
+ domain = kzalloc_obj(*domain);
+ if (!domain)
+ return ERR_PTR(-ENOMEM);
+ domain->clock_id = clock_id;
+ domain->module = module;
+ refcount_set(&domain->refcount, 1);
+ mutex_init(&domain->lock);
+ ret = xa_alloc(&dpll_domain_xa, &domain->id, domain, xa_limit_32b,
+ GFP_KERNEL);
+ if (ret) {
+ mutex_destroy(&domain->lock);
+ kfree(domain);
+ return ERR_PTR(ret);
+ }
+
+ return domain;
+}
+
+static void dpll_domain_put(struct dpll_domain *domain)
+{
+ lockdep_assert_held(&dpll_lock);
+
+ if (refcount_dec_and_test(&domain->refcount)) {
+ xa_erase(&dpll_domain_xa, domain->id);
+ mutex_destroy(&domain->lock);
+ kfree(domain);
+ }
+}
+
static void dpll_device_tracker_alloc(struct dpll_device *dpll,
dpll_tracker *tracker)
{
@@ -120,6 +165,7 @@ static void __dpll_device_put(struct dpll_device *dpll, dpll_tracker *tracker)
xa_erase(&dpll_device_xa, dpll->id);
WARN_ON(!list_empty(&dpll->registration_list));
ref_tracker_dir_exit(&dpll->refcnt_tracker);
+ dpll_domain_put(dpll->domain);
kfree(dpll);
}
}
@@ -159,6 +205,7 @@ static void __dpll_pin_put(struct dpll_pin *pin, dpll_tracker *tracker)
fwnode_handle_put(pin->fwnode);
dpll_pin_idx_free(pin->pin_idx);
ref_tracker_dir_exit(&pin->refcnt_tracker);
+ dpll_domain_put(pin->domain);
kfree_rcu(pin, rcu);
}
}
@@ -366,6 +413,12 @@ dpll_device_alloc(const u64 clock_id, u32 device_idx, struct module *module)
dpll = kzalloc_obj(*dpll);
if (!dpll)
return ERR_PTR(-ENOMEM);
+ dpll->domain = dpll_domain_get(clock_id, module);
+ if (IS_ERR(dpll->domain)) {
+ ret = PTR_ERR(dpll->domain);
+ kfree(dpll);
+ return ERR_PTR(ret);
+ }
refcount_set(&dpll->refcount, 1);
INIT_LIST_HEAD(&dpll->registration_list);
dpll->device_idx = device_idx;
@@ -374,6 +427,7 @@ dpll_device_alloc(const u64 clock_id, u32 device_idx, struct module *module)
ret = xa_alloc_cyclic(&dpll_device_xa, &dpll->id, dpll, xa_limit_32b,
&dpll_device_xa_id, GFP_KERNEL);
if (ret < 0) {
+ dpll_domain_put(dpll->domain);
kfree(dpll);
return ERR_PTR(ret);
}
@@ -664,6 +718,11 @@ dpll_pin_alloc(u64 clock_id, u32 pin_idx, struct module *module,
ret = dpll_pin_prop_dup(prop, &pin->prop);
if (ret)
goto err_pin_prop;
+ pin->domain = dpll_domain_get(clock_id, module);
+ if (IS_ERR(pin->domain)) {
+ ret = PTR_ERR(pin->domain);
+ goto err_domain;
+ }
refcount_set(&pin->refcount, 1);
xa_init_flags(&pin->dpll_refs, XA_FLAGS_ALLOC);
xa_init_flags(&pin->parent_refs, XA_FLAGS_ALLOC);
@@ -678,6 +737,8 @@ dpll_pin_alloc(u64 clock_id, u32 pin_idx, struct module *module,
xa_destroy(&pin->dpll_refs);
xa_destroy(&pin->parent_refs);
xa_destroy(&pin->ref_sync_pins);
+ dpll_domain_put(pin->domain);
+err_domain:
dpll_pin_prop_free(&pin->prop);
err_pin_prop:
kfree(pin);
diff --git a/drivers/dpll/dpll_core.h b/drivers/dpll/dpll_core.h
index da8a369556ed11..6705e81bc11de9 100644
--- a/drivers/dpll/dpll_core.h
+++ b/drivers/dpll/dpll_core.h
@@ -9,18 +9,42 @@
#include <linux/dpll.h>
#include <linux/list.h>
+#include <linux/mutex.h>
#include <linux/refcount.h>
#include <linux/ref_tracker.h>
#include "dpll_nl.h"
#define DPLL_REGISTERED XA_MARK_1
+/**
+ * struct dpll_domain - group of dpll objects sharing a lock
+ * @id: index of the domain in dpll_domain_xa
+ * @clock_id: clock_id of the objects in the domain
+ * @module: module of the objects in the domain
+ * @refcount: number of dpll devices and pins in the domain
+ * @lock: lock of the domain
+ *
+ * All dpll devices and pins created with the same (module, clock_id) pair
+ * belong to one domain, in practice to one chip or card.
+ **/
+struct dpll_domain {
+ u32 id;
+ u64 clock_id;
+ struct module *module;
+ refcount_t refcount;
+ /* Serializes the driver callbacks of all DPLL devices and pins
+ * that share one (module, clock_id) pair.
+ */
+ struct mutex lock;
+};
+
/**
* struct dpll_device - stores DPLL device internal data
* @id: unique id number for device given by dpll subsystem
* @device_idx: id given by dev driver
* @clock_id: unique identifier (clock_id) of a dpll
* @module: module of creator
+ * @domain: domain the device belongs to
* @type: type of a dpll
* @pin_refs: stores pins registered within a dpll
* @refcount: refcount
@@ -32,6 +56,7 @@ struct dpll_device {
u32 device_idx;
u64 clock_id;
struct module *module;
+ struct dpll_domain *domain;
enum dpll_type type;
struct xarray pin_refs;
refcount_t refcount;
@@ -46,6 +71,7 @@ struct dpll_device {
* @clock_id: clock_id of creator
* @module: module of creator
* @module_name: module name of creator
+ * @domain: domain the pin belongs to
* @fwnode: optional reference to firmware node
* @dpll_refs: hold referencees to dplls pin was registered with
* @parent_refs: hold references to parent pins pin was registered with
@@ -61,6 +87,7 @@ struct dpll_pin {
u64 clock_id;
struct module *module;
char module_name[MODULE_NAME_LEN];
+ struct dpll_domain *domain;
struct fwnode_handle *fwnode;
struct xarray dpll_refs;
struct xarray parent_refs;
--
2.56.0
^ permalink raw reply [flat|nested] 27+ messages in thread
* [PATCH net-next 04/10] dpll: take domain locks around driver callbacks
2026-11-06 12:09 [PATCH net-next 00/10] dpll: do not serialize driver callbacks under global dpll_lock Ivan Vecera
` (2 preceding siblings ...)
2026-11-06 12:09 ` [PATCH net-next 03/10] dpll: add domain object Ivan Vecera
@ 2026-11-06 12:09 ` Ivan Vecera
2026-11-10 12:23 ` Petr Oros
2026-11-06 12:10 ` [PATCH net-next 05/10] dpll: convert dpll_lock to rwsem Ivan Vecera
` (5 subsequent siblings)
9 siblings, 1 reply; 27+ messages in thread
From: Ivan Vecera @ 2026-11-06 12:09 UTC (permalink / raw)
To: netdev
Cc: Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
Jonathan Corbet, Shuah Khan, Randy Dunlap, Min Li, Tony Nguyen,
Przemek Kitszel, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Richard Cochran, linux-doc,
linux-kernel, intel-wired-lan, Grzegorz Nitka, Karol Kolacinski,
Saeed Mahameed, Tariq Toukan, Petr Oros
Hold the domain lock of the addressed object wherever driver callbacks can
run:
- netlink requests addressing a device or a pin, from pre_doit to post_doit
- dumps, for each object
- notifications, while the message is being built and sent
Callbacks of a device run under the domain of the device. Callbacks of a
pin run under the domain of the pin, also when another driver registered
the pin with its dpll or under its parent pin. All callbacks a message
about a pin needs are thus covered by one lock, and only one domain lock
is ever held. Domains have no lock order, so registrations can connect
them in any way.
A request is atomic against other requests addressing objects of that
domain, and messages of one object are sent in the order they were built.
Requests resolving an id run no callbacks, so they do not take the lock.
__dpll_{device,pin}_change_ntf() now also require the domain lock of the
object. A callback can therefore only notify objects of the domain it runs
under.
Callbacks stay serialized by dpll_lock for now.
Signed-off-by: Ivan Vecera <ivecera@redhat.com>
---
drivers/dpll/dpll_core.c | 18 +++++----
drivers/dpll/dpll_core.h | 6 ++-
drivers/dpll/dpll_netlink.c | 78 ++++++++++++++++++++++++++++---------
3 files changed, 76 insertions(+), 26 deletions(-)
diff --git a/drivers/dpll/dpll_core.c b/drivers/dpll/dpll_core.c
index d39c4905172f2d..dd09f528170994 100644
--- a/drivers/dpll/dpll_core.c
+++ b/drivers/dpll/dpll_core.c
@@ -520,7 +520,7 @@ dpll_device_registration_find(struct dpll_device *dpll,
*
* Make dpll device available for user space.
*
- * Context: Acquires a lock (dpll_lock)
+ * Context: Acquires dpll_lock and the domain lock of @dpll
* Return:
* * 0 on success
* * negative - error value
@@ -579,7 +579,7 @@ EXPORT_SYMBOL_GPL(dpll_device_register);
*
* Unregister device, make it unavailable for userspace.
* Note: It does not free the memory
- * Context: Acquires a lock (dpll_lock)
+ * Context: Acquires dpll_lock and the domain lock of @dpll
*/
void dpll_device_unregister(struct dpll_device *dpll,
const struct dpll_device_ops *ops, void *priv)
@@ -928,7 +928,7 @@ __dpll_pin_register(struct dpll_device *dpll, struct dpll_pin *pin,
* @ops: ops for a dpll pin ops
* @priv: pointer to private information of owner
*
- * Context: Acquires a lock (dpll_lock)
+ * Context: Acquires dpll_lock and domain locks
* Return:
* * 0 on success
* * negative - error value
@@ -989,7 +989,9 @@ static void dpll_pin_ref_sync_pair_del(u32 ref_sync_pin_id)
ref_sync_pin = xa_load(&pin->ref_sync_pins, ref_sync_pin_id);
if (ref_sync_pin) {
xa_erase(&pin->ref_sync_pins, ref_sync_pin_id);
+ mutex_lock(&pin->domain->lock);
__dpll_pin_change_ntf(pin);
+ mutex_unlock(&pin->domain->lock);
}
}
}
@@ -1016,7 +1018,7 @@ __dpll_pin_unregister(struct dpll_device *dpll, struct dpll_pin *pin,
* @priv: pointer to private information of owner
*
* Note: It does not free the memory
- * Context: Acquires a lock (dpll_lock)
+ * Context: Acquires dpll_lock and domain locks
*/
void dpll_pin_unregister(struct dpll_device *dpll, struct dpll_pin *pin,
const struct dpll_pin_ops *ops, void *priv)
@@ -1042,7 +1044,7 @@ EXPORT_SYMBOL_GPL(dpll_pin_unregister);
* Register a pin with a parent pin, create references between them and
* between newly registered pin and dplls connected with a parent pin.
*
- * Context: Acquires a lock (dpll_lock)
+ * Context: Acquires dpll_lock and domain locks
* Return:
* * 0 on success
* * negative - error value
@@ -1099,7 +1101,7 @@ EXPORT_SYMBOL_GPL(dpll_pin_on_pin_register);
* @ops: ops for a dpll pin
* @priv: pointer to private information of owner
*
- * Context: Acquires a lock (dpll_lock)
+ * Context: Acquires dpll_lock and domain locks
* Note: It does not free the memory
*/
void dpll_pin_on_pin_unregister(struct dpll_pin *parent, struct dpll_pin *pin,
@@ -1129,7 +1131,7 @@ EXPORT_SYMBOL_GPL(dpll_pin_on_pin_unregister);
*
* Once pins are paired, the user-space configuration of reference sync pair
* is possible.
- * Context: Acquires a lock (dpll_lock)
+ * Context: Acquires dpll_lock and domain locks
* Return:
* * 0 on success
* * negative - error value
@@ -1142,7 +1144,9 @@ int dpll_pin_ref_sync_pair_add(struct dpll_pin *pin,
mutex_lock(&dpll_lock);
ret = xa_insert(&pin->ref_sync_pins, ref_sync_pin->id,
ref_sync_pin, GFP_KERNEL);
+ mutex_lock(&pin->domain->lock);
__dpll_pin_change_ntf(pin);
+ mutex_unlock(&pin->domain->lock);
mutex_unlock(&dpll_lock);
return ret;
diff --git a/drivers/dpll/dpll_core.h b/drivers/dpll/dpll_core.h
index 6705e81bc11de9..630c5a1ca572c2 100644
--- a/drivers/dpll/dpll_core.h
+++ b/drivers/dpll/dpll_core.h
@@ -22,10 +22,14 @@
* @clock_id: clock_id of the objects in the domain
* @module: module of the objects in the domain
* @refcount: number of dpll devices and pins in the domain
- * @lock: lock of the domain
+ * @lock: serializes driver callbacks of the domain
*
* All dpll devices and pins created with the same (module, clock_id) pair
* belong to one domain, in practice to one chip or card.
+ *
+ * Callbacks of a device run under the domain of the device and callbacks of a
+ * pin under the domain of the pin, whichever driver registered them. Only one
+ * domain lock is held at a time, so domains have no lock order.
**/
struct dpll_domain {
u32 id;
diff --git a/drivers/dpll/dpll_netlink.c b/drivers/dpll/dpll_netlink.c
index e5380d95f238b3..1b4e3a0166a744 100644
--- a/drivers/dpll/dpll_netlink.c
+++ b/drivers/dpll/dpll_netlink.c
@@ -861,27 +861,39 @@ dpll_device_event_send(enum dpll_cmd event, struct dpll_device *dpll)
int dpll_device_create_ntf(struct dpll_device *dpll)
{
+ int ret;
+
+ mutex_lock(&dpll->domain->lock);
dpll_device_notify(dpll, DPLL_DEVICE_CREATED);
- return dpll_device_event_send(DPLL_CMD_DEVICE_CREATE_NTF, dpll);
+ ret = dpll_device_event_send(DPLL_CMD_DEVICE_CREATE_NTF, dpll);
+ mutex_unlock(&dpll->domain->lock);
+
+ return ret;
}
int dpll_device_delete_ntf(struct dpll_device *dpll)
{
+ int ret;
+
+ mutex_lock(&dpll->domain->lock);
dpll_device_notify(dpll, DPLL_DEVICE_DELETED);
- return dpll_device_event_send(DPLL_CMD_DEVICE_DELETE_NTF, dpll);
+ ret = dpll_device_event_send(DPLL_CMD_DEVICE_DELETE_NTF, dpll);
+ mutex_unlock(&dpll->domain->lock);
+
+ return ret;
}
/**
* __dpll_device_change_ntf - notify that the dpll device has been changed
* @dpll: registered dpll pointer
*
- * Context: caller must hold dpll_lock. Suitable for use inside device
- * callbacks which are already invoked under dpll_lock.
+ * Context: caller must hold dpll_lock and the domain lock of @dpll.
+ * Suitable for use inside callbacks running under these locks.
* Return: 0 if succeeds, error code otherwise.
*/
int __dpll_device_change_ntf(struct dpll_device *dpll)
{
- lockdep_assert_held(&dpll_lock);
+ lockdep_assert_held(&dpll->domain->lock);
dpll_device_notify(dpll, DPLL_DEVICE_CHANGED);
return dpll_device_event_send(DPLL_CMD_DEVICE_CHANGE_NTF, dpll);
}
@@ -891,7 +903,7 @@ EXPORT_SYMBOL_GPL(__dpll_device_change_ntf);
* dpll_device_change_ntf - notify that the dpll device has been changed
* @dpll: registered dpll pointer
*
- * Context: acquires and holds a dpll_lock.
+ * Context: acquires and holds dpll_lock and the domain lock of @dpll.
* Return: 0 if succeeds, error code otherwise.
*/
int dpll_device_change_ntf(struct dpll_device *dpll)
@@ -899,7 +911,9 @@ int dpll_device_change_ntf(struct dpll_device *dpll)
int ret;
mutex_lock(&dpll_lock);
+ mutex_lock(&dpll->domain->lock);
ret = __dpll_device_change_ntf(dpll);
+ mutex_unlock(&dpll->domain->lock);
mutex_unlock(&dpll_lock);
return ret;
@@ -941,27 +955,39 @@ dpll_pin_event_send(enum dpll_cmd event, struct dpll_pin *pin)
int dpll_pin_create_ntf(struct dpll_pin *pin, u64 src_clock_id)
{
+ int ret;
+
+ mutex_lock(&pin->domain->lock);
dpll_pin_notify(pin, src_clock_id, DPLL_PIN_CREATED);
- return dpll_pin_event_send(DPLL_CMD_PIN_CREATE_NTF, pin);
+ ret = dpll_pin_event_send(DPLL_CMD_PIN_CREATE_NTF, pin);
+ mutex_unlock(&pin->domain->lock);
+
+ return ret;
}
int dpll_pin_delete_ntf(struct dpll_pin *pin, u64 src_clock_id)
{
+ int ret;
+
+ mutex_lock(&pin->domain->lock);
dpll_pin_notify(pin, src_clock_id, DPLL_PIN_DELETED);
- return dpll_pin_event_send(DPLL_CMD_PIN_DELETE_NTF, pin);
+ ret = dpll_pin_event_send(DPLL_CMD_PIN_DELETE_NTF, pin);
+ mutex_unlock(&pin->domain->lock);
+
+ return ret;
}
/**
* __dpll_pin_change_ntf - notify that the pin has been changed
* @pin: registered pin pointer
*
- * Context: caller must hold dpll_lock. Suitable for use inside pin
- * callbacks which are already invoked under dpll_lock.
+ * Context: caller must hold dpll_lock and the domain lock of @pin.
+ * Suitable for use inside callbacks running under these locks.
* Return: 0 if succeeds, error code otherwise.
*/
int __dpll_pin_change_ntf(struct dpll_pin *pin)
{
- lockdep_assert_held(&dpll_lock);
+ lockdep_assert_held(&pin->domain->lock);
dpll_pin_notify(pin, pin->clock_id, DPLL_PIN_CHANGED);
return dpll_pin_event_send(DPLL_CMD_PIN_CHANGE_NTF, pin);
}
@@ -971,7 +997,7 @@ EXPORT_SYMBOL_GPL(__dpll_pin_change_ntf);
* dpll_pin_change_ntf - notify that the pin has been changed
* @pin: registered pin pointer
*
- * Context: acquires and holds a dpll_lock.
+ * Context: acquires and holds dpll_lock and the domain lock of @pin.
* Return: 0 if succeeds, error code otherwise.
*/
int dpll_pin_change_ntf(struct dpll_pin *pin)
@@ -979,7 +1005,9 @@ int dpll_pin_change_ntf(struct dpll_pin *pin)
int ret;
mutex_lock(&dpll_lock);
+ mutex_lock(&pin->domain->lock);
ret = __dpll_pin_change_ntf(pin);
+ mutex_unlock(&pin->domain->lock);
mutex_unlock(&dpll_lock);
return ret;
@@ -1780,7 +1808,9 @@ int dpll_nl_pin_get_dumpit(struct sk_buff *skb, struct netlink_callback *cb)
ret = -EMSGSIZE;
break;
}
+ mutex_lock(&pin->domain->lock);
ret = dpll_cmd_pin_get_one(skb, pin, cb->extack);
+ mutex_unlock(&pin->domain->lock);
if (ret) {
genlmsg_cancel(skb, hdr);
if (ret == -ENODEV) {
@@ -1999,7 +2029,9 @@ int dpll_nl_device_get_dumpit(struct sk_buff *skb, struct netlink_callback *cb)
ret = -EMSGSIZE;
break;
}
+ mutex_lock(&dpll->domain->lock);
ret = dpll_device_get_one(dpll, skb, cb->extack);
+ mutex_unlock(&dpll->domain->lock);
if (ret) {
genlmsg_cancel(skb, hdr);
break;
@@ -2018,6 +2050,7 @@ int dpll_nl_device_get_dumpit(struct sk_buff *skb, struct netlink_callback *cb)
int dpll_pre_doit(const struct genl_split_ops *ops, struct sk_buff *skb,
struct genl_info *info)
{
+ struct dpll_device *dpll;
u32 id;
if (GENL_REQ_ATTR_CHECK(info, DPLL_A_ID))
@@ -2025,11 +2058,13 @@ int dpll_pre_doit(const struct genl_split_ops *ops, struct sk_buff *skb,
mutex_lock(&dpll_lock);
id = nla_get_u32(info->attrs[DPLL_A_ID]);
- info->user_ptr[0] = dpll_device_get_by_id(id);
- if (!info->user_ptr[0]) {
+ dpll = dpll_device_get_by_id(id);
+ if (!dpll) {
NL_SET_ERR_MSG(info->extack, "device not found");
goto unlock;
}
+ info->user_ptr[0] = dpll;
+ mutex_lock(&dpll->domain->lock);
return 0;
unlock:
mutex_unlock(&dpll_lock);
@@ -2039,6 +2074,9 @@ int dpll_pre_doit(const struct genl_split_ops *ops, struct sk_buff *skb,
void dpll_post_doit(const struct genl_split_ops *ops, struct sk_buff *skb,
struct genl_info *info)
{
+ struct dpll_device *dpll = info->user_ptr[0];
+
+ mutex_unlock(&dpll->domain->lock);
mutex_unlock(&dpll_lock);
}
@@ -2061,6 +2099,7 @@ dpll_unlock_doit(const struct genl_split_ops *ops, struct sk_buff *skb,
int dpll_pin_pre_doit(const struct genl_split_ops *ops, struct sk_buff *skb,
struct genl_info *info)
{
+ struct dpll_pin *pin;
int ret;
mutex_lock(&dpll_lock);
@@ -2068,14 +2107,14 @@ int dpll_pin_pre_doit(const struct genl_split_ops *ops, struct sk_buff *skb,
ret = -EINVAL;
goto unlock_dev;
}
- info->user_ptr[0] = xa_load(&dpll_pin_xa,
- nla_get_u32(info->attrs[DPLL_A_PIN_ID]));
- if (!info->user_ptr[0] ||
- !dpll_pin_available(info->user_ptr[0])) {
+ pin = xa_load(&dpll_pin_xa, nla_get_u32(info->attrs[DPLL_A_PIN_ID]));
+ if (!pin || !dpll_pin_available(pin)) {
NL_SET_ERR_MSG(info->extack, "pin not found");
ret = -ENODEV;
goto unlock_dev;
}
+ info->user_ptr[0] = pin;
+ mutex_lock(&pin->domain->lock);
return 0;
@@ -2087,5 +2126,8 @@ int dpll_pin_pre_doit(const struct genl_split_ops *ops, struct sk_buff *skb,
void dpll_pin_post_doit(const struct genl_split_ops *ops, struct sk_buff *skb,
struct genl_info *info)
{
+ struct dpll_pin *pin = info->user_ptr[0];
+
+ mutex_unlock(&pin->domain->lock);
mutex_unlock(&dpll_lock);
}
--
2.56.0
^ permalink raw reply [flat|nested] 27+ messages in thread
* [PATCH net-next 05/10] dpll: convert dpll_lock to rwsem
2026-11-06 12:09 [PATCH net-next 00/10] dpll: do not serialize driver callbacks under global dpll_lock Ivan Vecera
` (3 preceding siblings ...)
2026-11-06 12:09 ` [PATCH net-next 04/10] dpll: take domain locks around driver callbacks Ivan Vecera
@ 2026-11-06 12:10 ` Ivan Vecera
2026-11-10 12:24 ` Petr Oros
2026-11-10 17:53 ` netdev-bot+sashiko
2026-11-06 12:10 ` [PATCH net-next 06/10] dpll: add KUnit tests for core locking Ivan Vecera
` (4 subsequent siblings)
9 siblings, 2 replies; 27+ messages in thread
From: Ivan Vecera @ 2026-11-06 12:10 UTC (permalink / raw)
To: netdev
Cc: Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
Jonathan Corbet, Shuah Khan, Randy Dunlap, Min Li, Tony Nguyen,
Przemek Kitszel, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Richard Cochran, linux-doc,
linux-kernel, intel-wired-lan, Grzegorz Nitka, Karol Kolacinski,
Saeed Mahameed, Tariq Toukan, Petr Oros
Driver callbacks are now serialized by domain locks, so dpll_lock only has
to protect the topology and lifetime of devices and pins.
Convert it to an rwsem. Core functions changing the topology take it for
write. Netlink requests, each part of a dump and change notifications take
it for read, for their whole duration, so objects found by a lookup stay
valid. This is the patch that lets callbacks of different domains run
concurrently.
A callback running for long still blocks topology changes, and a waiting
writer blocks new readers, so a stuck callback stalls the subsystem while
a device is being probed or removed. This is no worse than the global
serialization it replaces.
Signed-off-by: Ivan Vecera <ivecera@redhat.com>
---
drivers/dpll/dpll_core.c | 93 +++++++++++++++++--------------------
drivers/dpll/dpll_core.h | 3 +-
drivers/dpll/dpll_netlink.c | 32 ++++++-------
3 files changed, 61 insertions(+), 67 deletions(-)
diff --git a/drivers/dpll/dpll_core.c b/drivers/dpll/dpll_core.c
index dd09f528170994..8eb74682343156 100644
--- a/drivers/dpll/dpll_core.c
+++ b/drivers/dpll/dpll_core.c
@@ -19,8 +19,12 @@
#include "dpll_core.h"
#include "dpll_netlink.h"
-/* Mutex lock to protect DPLL subsystem devices and pins */
-DEFINE_MUTEX(dpll_lock);
+/* Protects the topology and lifetime of DPLL devices and pins. Taken for write
+ * to change them and for read for the whole netlink request, part of a dump or
+ * change notification that uses them. Driver callbacks are serialized by domain
+ * locks, which are only taken under this lock.
+ */
+DECLARE_RWSEM(dpll_lock);
DEFINE_XARRAY_FLAGS(dpll_device_xa, XA_FLAGS_ALLOC);
DEFINE_XARRAY_FLAGS(dpll_pin_xa, XA_FLAGS_ALLOC);
@@ -459,7 +463,7 @@ dpll_device_get(u64 clock_id, u32 device_idx, struct module *module,
struct dpll_device *dpll, *ret = NULL;
unsigned long index;
- mutex_lock(&dpll_lock);
+ down_write(&dpll_lock);
xa_for_each(&dpll_device_xa, index, dpll) {
if (dpll->clock_id == clock_id &&
dpll->device_idx == device_idx &&
@@ -475,7 +479,7 @@ dpll_device_get(u64 clock_id, u32 device_idx, struct module *module,
dpll_device_tracker_alloc(ret, tracker);
}
- mutex_unlock(&dpll_lock);
+ up_write(&dpll_lock);
return ret;
}
@@ -492,9 +496,9 @@ EXPORT_SYMBOL_GPL(dpll_device_get);
*/
void dpll_device_put(struct dpll_device *dpll, dpll_tracker *tracker)
{
- mutex_lock(&dpll_lock);
+ down_write(&dpll_lock);
__dpll_device_put(dpll, tracker);
- mutex_unlock(&dpll_lock);
+ up_write(&dpll_lock);
}
EXPORT_SYMBOL_GPL(dpll_device_put);
@@ -540,16 +544,16 @@ int dpll_device_register(struct dpll_device *dpll, enum dpll_type type,
if (WARN_ON(type < DPLL_TYPE_PPS || type > DPLL_TYPE_MAX))
return -EINVAL;
- mutex_lock(&dpll_lock);
+ down_write(&dpll_lock);
reg = dpll_device_registration_find(dpll, ops, priv);
if (reg) {
- mutex_unlock(&dpll_lock);
+ up_write(&dpll_lock);
return -EEXIST;
}
reg = kzalloc_obj(*reg);
if (!reg) {
- mutex_unlock(&dpll_lock);
+ up_write(&dpll_lock);
return -ENOMEM;
}
reg->ops = ops;
@@ -559,13 +563,13 @@ int dpll_device_register(struct dpll_device *dpll, enum dpll_type type,
first_registration = list_empty(&dpll->registration_list);
list_add_tail(®->list, &dpll->registration_list);
if (!first_registration) {
- mutex_unlock(&dpll_lock);
+ up_write(&dpll_lock);
return 0;
}
xa_set_mark(&dpll_device_xa, dpll->id, DPLL_REGISTERED);
dpll_device_create_ntf(dpll);
- mutex_unlock(&dpll_lock);
+ up_write(&dpll_lock);
return 0;
}
@@ -586,12 +590,12 @@ void dpll_device_unregister(struct dpll_device *dpll,
{
struct dpll_device_registration *reg;
- mutex_lock(&dpll_lock);
+ down_write(&dpll_lock);
ASSERT_DPLL_REGISTERED(dpll);
dpll_device_delete_ntf(dpll);
reg = dpll_device_registration_find(dpll, ops, priv);
if (WARN_ON(!reg)) {
- mutex_unlock(&dpll_lock);
+ up_write(&dpll_lock);
return;
}
list_del(®->list);
@@ -599,11 +603,11 @@ void dpll_device_unregister(struct dpll_device *dpll,
kfree(reg);
if (!list_empty(&dpll->registration_list)) {
- mutex_unlock(&dpll_lock);
+ up_write(&dpll_lock);
return;
}
xa_clear_mark(&dpll_device_xa, dpll->id, DPLL_REGISTERED);
- mutex_unlock(&dpll_lock);
+ up_write(&dpll_lock);
}
EXPORT_SYMBOL_GPL(dpll_device_unregister);
@@ -771,9 +775,9 @@ int register_dpll_notifier(struct notifier_block *nb)
{
int ret;
- mutex_lock(&dpll_lock);
+ down_write(&dpll_lock);
ret = raw_notifier_chain_register(&dpll_notifier_chain, nb);
- mutex_unlock(&dpll_lock);
+ up_write(&dpll_lock);
return ret;
}
EXPORT_SYMBOL_GPL(register_dpll_notifier);
@@ -782,9 +786,9 @@ int unregister_dpll_notifier(struct notifier_block *nb)
{
int ret;
- mutex_lock(&dpll_lock);
+ down_write(&dpll_lock);
ret = raw_notifier_chain_unregister(&dpll_notifier_chain, nb);
- mutex_unlock(&dpll_lock);
+ up_write(&dpll_lock);
return ret;
}
EXPORT_SYMBOL_GPL(unregister_dpll_notifier);
@@ -812,7 +816,7 @@ dpll_pin_get(u64 clock_id, u32 pin_idx, struct module *module,
struct dpll_pin *pos, *ret = NULL;
unsigned long i;
- mutex_lock(&dpll_lock);
+ down_write(&dpll_lock);
xa_for_each(&dpll_pin_xa, i, pos) {
if (pos->clock_id == clock_id &&
pos->pin_idx == pin_idx &&
@@ -827,7 +831,7 @@ dpll_pin_get(u64 clock_id, u32 pin_idx, struct module *module,
if (!IS_ERR(ret))
dpll_pin_tracker_alloc(ret, tracker);
}
- mutex_unlock(&dpll_lock);
+ up_write(&dpll_lock);
return ret;
}
@@ -844,9 +848,9 @@ EXPORT_SYMBOL_GPL(dpll_pin_get);
*/
void dpll_pin_put(struct dpll_pin *pin, dpll_tracker *tracker)
{
- mutex_lock(&dpll_lock);
+ down_write(&dpll_lock);
__dpll_pin_put(pin, tracker);
- mutex_unlock(&dpll_lock);
+ up_write(&dpll_lock);
}
EXPORT_SYMBOL_GPL(dpll_pin_put);
@@ -859,10 +863,10 @@ EXPORT_SYMBOL_GPL(dpll_pin_put);
*/
void dpll_pin_fwnode_set(struct dpll_pin *pin, struct fwnode_handle *fwnode)
{
- mutex_lock(&dpll_lock);
+ down_write(&dpll_lock);
fwnode_handle_put(pin->fwnode); /* Drop fwnode previously set */
pin->fwnode = fwnode_handle_get(fwnode);
- mutex_unlock(&dpll_lock);
+ up_write(&dpll_lock);
}
EXPORT_SYMBOL_GPL(dpll_pin_fwnode_set);
@@ -885,7 +889,7 @@ struct dpll_pin *fwnode_dpll_pin_find(struct fwnode_handle *fwnode,
struct dpll_pin *pin, *ret = NULL;
unsigned long index;
- mutex_lock(&dpll_lock);
+ down_write(&dpll_lock);
xa_for_each(&dpll_pin_xa, index, pin) {
if (pin->fwnode == fwnode) {
__dpll_pin_hold(pin, tracker);
@@ -893,7 +897,7 @@ struct dpll_pin *fwnode_dpll_pin_find(struct fwnode_handle *fwnode,
break;
}
}
- mutex_unlock(&dpll_lock);
+ up_write(&dpll_lock);
return ret;
}
@@ -950,7 +954,7 @@ dpll_pin_register(struct dpll_device *dpll, struct dpll_pin *pin,
DPLL_PIN_CAPABILITIES_STATE_CAN_CHANGE)))
return -EINVAL;
- mutex_lock(&dpll_lock);
+ down_write(&dpll_lock);
dev_ops = dpll_device_ops(dpll);
if (WARN_ON(ops->measured_freq_get &&
@@ -974,7 +978,7 @@ dpll_pin_register(struct dpll_device *dpll, struct dpll_pin *pin,
ret = __dpll_pin_register(dpll, pin, ops, priv, NULL);
out_unlock:
- mutex_unlock(&dpll_lock);
+ up_write(&dpll_lock);
return ret;
}
@@ -1028,9 +1032,9 @@ void dpll_pin_unregister(struct dpll_device *dpll, struct dpll_pin *pin,
if (WARN_ON(!xa_empty(&pin->parent_refs)))
return;
- mutex_lock(&dpll_lock);
+ down_write(&dpll_lock);
__dpll_pin_unregister(dpll, pin, ops, priv, NULL);
- mutex_unlock(&dpll_lock);
+ up_write(&dpll_lock);
}
EXPORT_SYMBOL_GPL(dpll_pin_unregister);
@@ -1064,7 +1068,7 @@ int dpll_pin_on_pin_register(struct dpll_pin *parent, struct dpll_pin *pin,
WARN_ON(!ops->direction_get))
return -EINVAL;
- mutex_lock(&dpll_lock);
+ down_write(&dpll_lock);
ret = dpll_xa_ref_pin_add(&pin->parent_refs, parent, ops, priv, pin);
if (ret)
goto unlock;
@@ -1076,7 +1080,7 @@ int dpll_pin_on_pin_register(struct dpll_pin *parent, struct dpll_pin *pin,
}
dpll_pin_create_ntf(pin, parent->clock_id);
}
- mutex_unlock(&dpll_lock);
+ up_write(&dpll_lock);
return ret;
@@ -1089,7 +1093,7 @@ int dpll_pin_on_pin_register(struct dpll_pin *parent, struct dpll_pin *pin,
}
dpll_xa_ref_pin_del(&pin->parent_refs, parent, ops, priv, pin);
unlock:
- mutex_unlock(&dpll_lock);
+ up_write(&dpll_lock);
return ret;
}
EXPORT_SYMBOL_GPL(dpll_pin_on_pin_register);
@@ -1111,7 +1115,7 @@ void dpll_pin_on_pin_unregister(struct dpll_pin *parent, struct dpll_pin *pin,
struct dpll_pin_ref *ref;
unsigned long i;
- mutex_lock(&dpll_lock);
+ down_write(&dpll_lock);
xa_for_each(&pin->dpll_refs, i, ref) {
reg = dpll_pin_registration_find(ref, ops, priv, parent);
if (!reg)
@@ -1120,7 +1124,7 @@ void dpll_pin_on_pin_unregister(struct dpll_pin *parent, struct dpll_pin *pin,
__dpll_pin_unregister(ref->dpll, pin, ops, priv, parent);
}
dpll_xa_ref_pin_del(&pin->parent_refs, parent, ops, priv, pin);
- mutex_unlock(&dpll_lock);
+ up_write(&dpll_lock);
}
EXPORT_SYMBOL_GPL(dpll_pin_on_pin_unregister);
@@ -1141,13 +1145,13 @@ int dpll_pin_ref_sync_pair_add(struct dpll_pin *pin,
{
int ret;
- mutex_lock(&dpll_lock);
+ down_write(&dpll_lock);
ret = xa_insert(&pin->ref_sync_pins, ref_sync_pin->id,
ref_sync_pin, GFP_KERNEL);
mutex_lock(&pin->domain->lock);
__dpll_pin_change_ntf(pin);
mutex_unlock(&pin->domain->lock);
- mutex_unlock(&dpll_lock);
+ up_write(&dpll_lock);
return ret;
}
@@ -1254,23 +1258,12 @@ const struct dpll_pin_ops *dpll_pin_ops(struct dpll_pin_ref *ref)
static int __init dpll_init(void)
{
- int ret;
-
- ret = genl_register_family(&dpll_nl_family);
- if (ret)
- goto error;
-
- return 0;
-
-error:
- mutex_destroy(&dpll_lock);
- return ret;
+ return genl_register_family(&dpll_nl_family);
}
static void __exit dpll_exit(void)
{
genl_unregister_family(&dpll_nl_family);
- mutex_destroy(&dpll_lock);
}
subsys_initcall(dpll_init);
diff --git a/drivers/dpll/dpll_core.h b/drivers/dpll/dpll_core.h
index 630c5a1ca572c2..e212e53528e33b 100644
--- a/drivers/dpll/dpll_core.h
+++ b/drivers/dpll/dpll_core.h
@@ -12,6 +12,7 @@
#include <linux/mutex.h>
#include <linux/refcount.h>
#include <linux/ref_tracker.h>
+#include <linux/rwsem.h>
#include "dpll_nl.h"
#define DPLL_REGISTERED XA_MARK_1
@@ -129,7 +130,7 @@ const struct dpll_pin_ops *dpll_pin_ops(struct dpll_pin_ref *ref);
struct dpll_pin_ref *dpll_xa_ref_dpll_first(struct xarray *xa_refs);
extern struct xarray dpll_device_xa;
extern struct xarray dpll_pin_xa;
-extern struct mutex dpll_lock;
+extern struct rw_semaphore dpll_lock;
void dpll_device_notify(struct dpll_device *dpll, unsigned long action);
void dpll_pin_notify(struct dpll_pin *pin, u64 src_clock_id,
diff --git a/drivers/dpll/dpll_netlink.c b/drivers/dpll/dpll_netlink.c
index 1b4e3a0166a744..9d8f5d1ce97248 100644
--- a/drivers/dpll/dpll_netlink.c
+++ b/drivers/dpll/dpll_netlink.c
@@ -910,11 +910,11 @@ int dpll_device_change_ntf(struct dpll_device *dpll)
{
int ret;
- mutex_lock(&dpll_lock);
+ down_read(&dpll_lock);
mutex_lock(&dpll->domain->lock);
ret = __dpll_device_change_ntf(dpll);
mutex_unlock(&dpll->domain->lock);
- mutex_unlock(&dpll_lock);
+ up_read(&dpll_lock);
return ret;
}
@@ -1004,11 +1004,11 @@ int dpll_pin_change_ntf(struct dpll_pin *pin)
{
int ret;
- mutex_lock(&dpll_lock);
+ down_read(&dpll_lock);
mutex_lock(&pin->domain->lock);
ret = __dpll_pin_change_ntf(pin);
mutex_unlock(&pin->domain->lock);
- mutex_unlock(&dpll_lock);
+ up_read(&dpll_lock);
return ret;
}
@@ -1795,7 +1795,7 @@ int dpll_nl_pin_get_dumpit(struct sk_buff *skb, struct netlink_callback *cb)
unsigned long i;
int ret = 0;
- mutex_lock(&dpll_lock);
+ down_read(&dpll_lock);
xa_for_each_marked_start(&dpll_pin_xa, i, pin, DPLL_REGISTERED,
ctx->idx) {
if (!dpll_pin_available(pin))
@@ -1821,7 +1821,7 @@ int dpll_nl_pin_get_dumpit(struct sk_buff *skb, struct netlink_callback *cb)
}
genlmsg_end(skb, hdr);
}
- mutex_unlock(&dpll_lock);
+ up_read(&dpll_lock);
if (ret == -EMSGSIZE) {
ctx->idx = i;
@@ -2019,7 +2019,7 @@ int dpll_nl_device_get_dumpit(struct sk_buff *skb, struct netlink_callback *cb)
unsigned long i;
int ret = 0;
- mutex_lock(&dpll_lock);
+ down_read(&dpll_lock);
xa_for_each_marked_start(&dpll_device_xa, i, dpll, DPLL_REGISTERED,
ctx->idx) {
hdr = genlmsg_put(skb, NETLINK_CB(cb->skb).portid,
@@ -2038,7 +2038,7 @@ int dpll_nl_device_get_dumpit(struct sk_buff *skb, struct netlink_callback *cb)
}
genlmsg_end(skb, hdr);
}
- mutex_unlock(&dpll_lock);
+ up_read(&dpll_lock);
if (ret == -EMSGSIZE) {
ctx->idx = i;
@@ -2056,7 +2056,7 @@ int dpll_pre_doit(const struct genl_split_ops *ops, struct sk_buff *skb,
if (GENL_REQ_ATTR_CHECK(info, DPLL_A_ID))
return -EINVAL;
- mutex_lock(&dpll_lock);
+ down_read(&dpll_lock);
id = nla_get_u32(info->attrs[DPLL_A_ID]);
dpll = dpll_device_get_by_id(id);
if (!dpll) {
@@ -2067,7 +2067,7 @@ int dpll_pre_doit(const struct genl_split_ops *ops, struct sk_buff *skb,
mutex_lock(&dpll->domain->lock);
return 0;
unlock:
- mutex_unlock(&dpll_lock);
+ up_read(&dpll_lock);
return -ENODEV;
}
@@ -2077,14 +2077,14 @@ void dpll_post_doit(const struct genl_split_ops *ops, struct sk_buff *skb,
struct dpll_device *dpll = info->user_ptr[0];
mutex_unlock(&dpll->domain->lock);
- mutex_unlock(&dpll_lock);
+ up_read(&dpll_lock);
}
int
dpll_lock_doit(const struct genl_split_ops *ops, struct sk_buff *skb,
struct genl_info *info)
{
- mutex_lock(&dpll_lock);
+ down_read(&dpll_lock);
return 0;
}
@@ -2093,7 +2093,7 @@ void
dpll_unlock_doit(const struct genl_split_ops *ops, struct sk_buff *skb,
struct genl_info *info)
{
- mutex_unlock(&dpll_lock);
+ up_read(&dpll_lock);
}
int dpll_pin_pre_doit(const struct genl_split_ops *ops, struct sk_buff *skb,
@@ -2102,7 +2102,7 @@ int dpll_pin_pre_doit(const struct genl_split_ops *ops, struct sk_buff *skb,
struct dpll_pin *pin;
int ret;
- mutex_lock(&dpll_lock);
+ down_read(&dpll_lock);
if (GENL_REQ_ATTR_CHECK(info, DPLL_A_PIN_ID)) {
ret = -EINVAL;
goto unlock_dev;
@@ -2119,7 +2119,7 @@ int dpll_pin_pre_doit(const struct genl_split_ops *ops, struct sk_buff *skb,
return 0;
unlock_dev:
- mutex_unlock(&dpll_lock);
+ up_read(&dpll_lock);
return ret;
}
@@ -2129,5 +2129,5 @@ void dpll_pin_post_doit(const struct genl_split_ops *ops, struct sk_buff *skb,
struct dpll_pin *pin = info->user_ptr[0];
mutex_unlock(&pin->domain->lock);
- mutex_unlock(&dpll_lock);
+ up_read(&dpll_lock);
}
--
2.56.0
^ permalink raw reply [flat|nested] 27+ messages in thread
* [PATCH net-next 06/10] dpll: add KUnit tests for core locking
2026-11-06 12:09 [PATCH net-next 00/10] dpll: do not serialize driver callbacks under global dpll_lock Ivan Vecera
` (4 preceding siblings ...)
2026-11-06 12:10 ` [PATCH net-next 05/10] dpll: convert dpll_lock to rwsem Ivan Vecera
@ 2026-11-06 12:10 ` Ivan Vecera
2026-11-10 12:24 ` Petr Oros
2026-11-10 17:53 ` netdev-bot+sashiko
2026-11-06 12:10 ` [PATCH net-next 07/10] dpll: reject measured_freq_get for foreign and on-pin registrations Ivan Vecera
` (3 subsequent siblings)
9 siblings, 2 replies; 27+ messages in thread
From: Ivan Vecera @ 2026-11-06 12:10 UTC (permalink / raw)
To: netdev
Cc: Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
Jonathan Corbet, Shuah Khan, Randy Dunlap, Min Li, Tony Nguyen,
Przemek Kitszel, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Richard Cochran, linux-doc,
linux-kernel, intel-wired-lan, Grzegorz Nitka, Karol Kolacinski,
Saeed Mahameed, Tariq Toukan, Petr Oros
Add KUnit tests using a mock driver with unrelated domains, a pin of one
domain registered with a dpll of another and a pin of one domain under a
MUX pin of another. They check that:
- objects with the same (module, clock_id) share a refcounted domain
- callbacks of a device run under its domain and callbacks of a pin under
the domain of the pin alone, whichever driver registered them
- a callback blocked in one domain does not stop unrelated domains
- callbacks of one domain are serialized, also across its devices
- a stuck callback of a foreign registration stops only the domain of the
pin, and a stuck foreign device or domain of the pin does not stop the
other side
- a pin and its parent pin of another domain do not block each other
- registrations can connect two domains in both directions
- a topology change waits until running requests finish
- a request setting several attributes of a pin is atomic against other
requests about the pin, also when it spans two domains
- notifications from callbacks keep their order, also when a callback
fails
Requests that can block run in kthreads, so a test can report a locking
bug instead of blocking itself. A real deadlock cannot be undone, the
stuck request keeps its locks, so the teardown then hangs until the run
times out. The handlers are called directly with a hand-built message,
so they are exported for KUnit.
Only lockdep can tell which locks a callback holds. Without it the checks
of held locks cannot fail, so the test that checks nothing else is
skipped. The tests of what blocks what do not depend on it.
The tests do not cover what needs a netlink socket, for example the order
of notifications against the ACK or the content of messages.
Run them with:
tools/testing/kunit/kunit.py run --kunitconfig=drivers/dpll
Signed-off-by: Ivan Vecera <ivecera@redhat.com>
---
drivers/dpll/.kunitconfig | 5 +
drivers/dpll/Kconfig | 13 +
drivers/dpll/Makefile | 1 +
drivers/dpll/dpll_core_kunit.c | 1471 ++++++++++++++++++++++++++++++++
drivers/dpll/dpll_netlink.c | 7 +
5 files changed, 1497 insertions(+)
create mode 100644 drivers/dpll/.kunitconfig
create mode 100644 drivers/dpll/dpll_core_kunit.c
diff --git a/drivers/dpll/.kunitconfig b/drivers/dpll/.kunitconfig
new file mode 100644
index 00000000000000..017c9d5b593a4f
--- /dev/null
+++ b/drivers/dpll/.kunitconfig
@@ -0,0 +1,5 @@
+CONFIG_KUNIT=y
+CONFIG_NET=y
+CONFIG_DPLL_KUNIT_TEST=y
+CONFIG_DEBUG_KERNEL=y
+CONFIG_PROVE_LOCKING=y
diff --git a/drivers/dpll/Kconfig b/drivers/dpll/Kconfig
index be98969f040ab6..12bcf706a15e14 100644
--- a/drivers/dpll/Kconfig
+++ b/drivers/dpll/Kconfig
@@ -23,6 +23,19 @@ config DPLL_REFCNT_TRACKER
If unsure, say N.
+config DPLL_KUNIT_TEST
+ tristate "KUnit tests for DPLL core" if !KUNIT_ALL_TESTS
+ depends on KUNIT && NET
+ select DPLL
+ default KUNIT_ALL_TESTS
+ help
+ KUnit tests for locking in the DPLL core. They use a mock driver
+ to check that callbacks run under the locks of their domains and
+ that unrelated domains do not block each other. The checks of held
+ locks need PROVE_LOCKING.
+
+ If unsure, say N.
+
source "drivers/dpll/zl3073x/Kconfig"
endmenu
diff --git a/drivers/dpll/Makefile b/drivers/dpll/Makefile
index 9e7a3a3e592e81..75ee1608c76a93 100644
--- a/drivers/dpll/Makefile
+++ b/drivers/dpll/Makefile
@@ -8,4 +8,5 @@ dpll-y += dpll_core.o
dpll-y += dpll_netlink.o
dpll-y += dpll_nl.o
+obj-$(CONFIG_DPLL_KUNIT_TEST) += dpll_core_kunit.o
obj-$(CONFIG_ZL3073X) += zl3073x/
diff --git a/drivers/dpll/dpll_core_kunit.c b/drivers/dpll/dpll_core_kunit.c
new file mode 100644
index 00000000000000..a58800eda5ff51
--- /dev/null
+++ b/drivers/dpll/dpll_core_kunit.c
@@ -0,0 +1,1471 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KUnit tests for locking in the DPLL core.
+ *
+ * Each test starts with two unrelated domains:
+ *
+ * Domain A: devices A0 and A1
+ * pin P on A0 and A1, setting its state on A0 notifies pin S
+ * pin S on A0 and A1
+ * Domain B: device B0
+ * pin BP on B0
+ *
+ * Tests of objects connected by a pin add these domains:
+ *
+ * Domain E: device E0, pin X on E0 (the owner), pin M (MUX) on E0
+ * Domain F: device F0, pin X from E is also registered with F0
+ * Domain G: pin C registered under pin M from E
+ *
+ * Requests that can block run in kthreads, so a test can report a locking bug
+ * instead of blocking itself. A real deadlock cannot be undone, the stuck
+ * request keeps its locks, so the teardown then hangs until the run times out.
+ */
+
+#include <kunit/test.h>
+#include <linux/completion.h>
+#include <linux/debug_locks.h>
+#include <linux/dpll.h>
+#include <linux/kthread.h>
+#include <linux/lockdep.h>
+#include <linux/notifier.h>
+#include <linux/property.h>
+#include <linux/wait.h>
+#include <net/genetlink.h>
+#include <net/netlink.h>
+
+#include "dpll_core.h"
+
+MODULE_IMPORT_NS("EXPORTED_FOR_KUNIT_TESTING");
+
+#define DT_CLOCK_ID_A 0xd0110a
+#define DT_CLOCK_ID_B 0xd0110b
+#define DT_CLOCK_ID_E 0xd0110e
+#define DT_CLOCK_ID_F 0xd0110f
+#define DT_CLOCK_ID_G 0xd01107
+
+#define DT_PIN_IDX_P 0
+#define DT_PIN_IDX_S 1
+#define DT_PIN_IDX_BP 2
+#define DT_PIN_IDX_M 3
+#define DT_PIN_IDX_X 4
+#define DT_PIN_IDX_C 5
+#define DT_PIN_IDX_Y 6
+#define DT_PIN_IDX_EXTRA 7
+
+#define DT_MAX_EVENTS 8
+#define DT_TIMEOUT msecs_to_jiffies(5000)
+#define DT_SHORT_TIMEOUT msecs_to_jiffies(100)
+
+struct dt_ctx;
+
+/* Counters and blocking of the callbacks of one driver instance */
+struct dt_dom {
+ struct dt_ctx *ctx;
+ struct completion entered;
+ atomic_t cur;
+ atomic_t peak;
+ bool block;
+};
+
+struct dt_dev {
+ struct dt_dom *dom;
+ struct dpll_device *dpll;
+ dpll_tracker tracker;
+ struct dt_dev *notify;
+ enum dpll_mode mode;
+ bool block;
+};
+
+struct dt_pin {
+ struct dpll_pin *pin;
+ dpll_tracker tracker;
+};
+
+struct dt_reg {
+ struct dt_dom *dom;
+ struct dt_pin *pin;
+ struct dt_dev *dev;
+ struct dt_pin *parent;
+ struct dt_pin *notify;
+ struct dpll_domain *forbid;
+ enum dpll_pin_state state;
+ enum dpll_pin_direction direction;
+ atomic_t freq_gets;
+ u32 prio;
+ bool block_prio_set;
+};
+
+struct dt_ctx {
+ struct kunit *test;
+ struct dt_dom dom_a, dom_b, dom_e, dom_f, dom_g;
+ struct dt_dev a0, a1, b0, e0, f0;
+ struct dt_pin p, s, bp, x, y, m, c, extra;
+ struct dt_reg reg_p_a0, reg_p_a1, reg_s_a0, reg_s_a1, reg_bp_b0;
+ struct dt_reg reg_x_e0, reg_x_f0, reg_y_e0, reg_y_f0, reg_m_e0, reg_c_m;
+ struct dt_reg *atomic_regs[2];
+ struct notifier_block nb;
+ wait_queue_head_t wq;
+ atomic_t cur_total;
+ atomic_t peak_total;
+ atomic_t wrong_lock;
+ atomic_t bad_ntf;
+ atomic_t torn;
+ struct task_struct *set_task;
+ void *events[DT_MAX_EVENTS];
+ int num_events;
+ bool record;
+ bool fail_after_ntf;
+ bool debug_locks_at_start;
+};
+
+struct dt_req {
+ struct dt_ctx *ctx;
+ int (*fn)(void *arg);
+ void *arg;
+ int ret;
+ struct completion done;
+ bool finished;
+};
+
+/* A nested attribute of a pin set request */
+struct dt_nest {
+ int type;
+ u32 parent_id;
+ int attr;
+ u32 val;
+};
+
+/* Allocated with kunit_kzalloc(), a failed assertion must not free them */
+struct dt_pin_set_args {
+ struct dt_ctx *ctx;
+ struct dpll_pin *pin;
+ struct dt_nest nests[2];
+ int num;
+};
+
+struct dt_mode_set_args {
+ struct dpll_device *dpll;
+ enum dpll_mode mode;
+};
+
+/*
+ * The lock may be held by another task, so mutex_is_locked() is not enough.
+ * Without lockdep no check of a held lock can fail.
+ */
+static bool dt_held(struct dpll_domain *domain)
+{
+#ifdef CONFIG_LOCKDEP
+ return lockdep_is_held(&domain->lock) != 0;
+#else
+ return true;
+#endif
+}
+
+static bool dt_surely_held(struct dpll_domain *domain)
+{
+#ifdef CONFIG_LOCKDEP
+ return lockdep_is_held(&domain->lock) > 0;
+#else
+ return false;
+#endif
+}
+
+static void dt_peak(atomic_t *peak, int val)
+{
+ int old = atomic_read(peak);
+
+ while (val > old && !atomic_try_cmpxchg(peak, &old, val))
+ ;
+}
+
+static void dt_block(struct dt_dom *dom, bool *block)
+{
+ complete(&dom->entered);
+ wait_event(dom->ctx->wq, !READ_ONCE(*block));
+}
+
+static void dt_enter(struct dt_dom *dom)
+{
+ struct dt_ctx *ctx = dom->ctx;
+
+ dt_peak(&ctx->peak_total, atomic_inc_return(&ctx->cur_total));
+ dt_peak(&dom->peak, atomic_inc_return(&dom->cur));
+
+ if (READ_ONCE(dom->block))
+ dt_block(dom, &dom->block);
+}
+
+static void dt_leave(struct dt_dom *dom)
+{
+ atomic_dec(&dom->cur);
+ atomic_dec(&dom->ctx->cur_total);
+}
+
+static void dt_dev_enter(struct dt_dev *dev, const struct dpll_device *dpll)
+{
+ if (!dt_held(dpll->domain))
+ atomic_inc(&dev->dom->ctx->wrong_lock);
+ dt_enter(dev->dom);
+ if (READ_ONCE(dev->block))
+ dt_block(dev->dom, &dev->block);
+}
+
+/* A callback of a pin runs under the domain of the pin alone */
+static void dt_pin_enter(struct dt_reg *reg, const struct dpll_pin *pin)
+{
+ struct dt_ctx *ctx = reg->dom->ctx;
+
+ if (!dt_held(pin->domain) ||
+ (reg->forbid && dt_surely_held(reg->forbid)))
+ atomic_inc(&ctx->wrong_lock);
+ dt_enter(reg->dom);
+}
+
+static int dt_mode_get(const struct dpll_device *dpll, void *priv,
+ enum dpll_mode *mode, struct netlink_ext_ack *extack)
+{
+ struct dt_dev *dev = priv;
+
+ dt_dev_enter(dev, dpll);
+ *mode = dev->mode;
+ dt_leave(dev->dom);
+
+ return 0;
+}
+
+static int dt_mode_set(const struct dpll_device *dpll, void *priv,
+ enum dpll_mode mode, struct netlink_ext_ack *extack)
+{
+ struct dt_dev *dev = priv;
+
+ dt_dev_enter(dev, dpll);
+ dev->mode = mode;
+ dt_leave(dev->dom);
+
+ if (dev->notify)
+ __dpll_device_change_ntf(dev->notify->dpll);
+
+ return 0;
+}
+
+static int dt_supported_modes_get(const struct dpll_device *dpll, void *priv,
+ unsigned long *modes,
+ struct netlink_ext_ack *extack)
+{
+ struct dt_dev *dev = priv;
+
+ dt_dev_enter(dev, dpll);
+ __set_bit(DPLL_MODE_AUTOMATIC, modes);
+ __set_bit(DPLL_MODE_MANUAL, modes);
+ dt_leave(dev->dom);
+
+ return 0;
+}
+
+static int dt_lock_status_get(const struct dpll_device *dpll, void *priv,
+ enum dpll_lock_status *status,
+ enum dpll_lock_status_error *status_error,
+ struct netlink_ext_ack *extack)
+{
+ struct dt_dev *dev = priv;
+
+ dt_dev_enter(dev, dpll);
+ *status = DPLL_LOCK_STATUS_LOCKED;
+ dt_leave(dev->dom);
+
+ return 0;
+}
+
+static const struct dpll_device_ops dt_dev_ops = {
+ .mode_get = dt_mode_get,
+ .mode_set = dt_mode_set,
+ .supported_modes_get = dt_supported_modes_get,
+ .lock_status_get = dt_lock_status_get,
+};
+
+static int dt_frequency_get(const struct dpll_pin *pin, void *pin_priv,
+ const struct dpll_device *dpll, void *dpll_priv,
+ u64 *frequency, struct netlink_ext_ack *extack)
+{
+ struct dt_reg *reg = pin_priv;
+
+ dt_pin_enter(reg, pin);
+ atomic_inc(®->freq_gets);
+ *frequency = 1;
+ dt_leave(reg->dom);
+
+ return 0;
+}
+
+static int dt_state_on_dpll_get(const struct dpll_pin *pin, void *pin_priv,
+ const struct dpll_device *dpll, void *dpll_priv,
+ enum dpll_pin_state *state,
+ struct netlink_ext_ack *extack)
+{
+ struct dt_reg *reg = pin_priv;
+
+ dt_pin_enter(reg, pin);
+ *state = reg->state;
+ dt_leave(reg->dom);
+
+ return 0;
+}
+
+static int dt_state_on_dpll_set(const struct dpll_pin *pin, void *pin_priv,
+ const struct dpll_device *dpll, void *dpll_priv,
+ const enum dpll_pin_state state,
+ struct netlink_ext_ack *extack)
+{
+ struct dt_reg *reg = pin_priv;
+
+ dt_pin_enter(reg, pin);
+ reg->state = state;
+ dt_leave(reg->dom);
+
+ if (reg->notify)
+ __dpll_pin_change_ntf(reg->notify->pin);
+
+ return reg->dom->ctx->fail_after_ntf ? -EIO : 0;
+}
+
+static int dt_state_on_pin_get(const struct dpll_pin *pin, void *pin_priv,
+ const struct dpll_pin *parent_pin,
+ void *parent_pin_priv,
+ enum dpll_pin_state *state,
+ struct netlink_ext_ack *extack)
+{
+ struct dt_reg *reg = pin_priv;
+
+ dt_pin_enter(reg, pin);
+ *state = reg->state;
+ dt_leave(reg->dom);
+
+ return 0;
+}
+
+static int dt_state_on_pin_set(const struct dpll_pin *pin, void *pin_priv,
+ const struct dpll_pin *parent_pin,
+ void *parent_pin_priv,
+ const enum dpll_pin_state state,
+ struct netlink_ext_ack *extack)
+{
+ struct dt_reg *reg = pin_priv;
+
+ dt_pin_enter(reg, pin);
+ reg->state = state;
+ dt_leave(reg->dom);
+
+ return 0;
+}
+
+static int dt_prio_get(const struct dpll_pin *pin, void *pin_priv,
+ const struct dpll_device *dpll, void *dpll_priv,
+ u32 *prio, struct netlink_ext_ack *extack)
+{
+ struct dt_reg *reg = pin_priv;
+ struct dt_ctx *ctx = reg->dom->ctx;
+
+ dt_pin_enter(reg, pin);
+ /* Only the request doing the set may see it half done */
+ if (READ_ONCE(ctx->set_task) && current != ctx->set_task &&
+ ctx->atomic_regs[0]->prio != ctx->atomic_regs[1]->prio)
+ atomic_inc(&ctx->torn);
+ *prio = reg->prio;
+ dt_leave(reg->dom);
+
+ return 0;
+}
+
+static int dt_prio_set(const struct dpll_pin *pin, void *pin_priv,
+ const struct dpll_device *dpll, void *dpll_priv,
+ const u32 prio, struct netlink_ext_ack *extack)
+{
+ struct dt_reg *reg = pin_priv;
+
+ dt_pin_enter(reg, pin);
+ if (READ_ONCE(reg->block_prio_set))
+ dt_block(reg->dom, ®->block_prio_set);
+ reg->prio = prio;
+ dt_leave(reg->dom);
+
+ return 0;
+}
+
+static int dt_direction_get(const struct dpll_pin *pin, void *pin_priv,
+ const struct dpll_device *dpll, void *dpll_priv,
+ enum dpll_pin_direction *direction,
+ struct netlink_ext_ack *extack)
+{
+ struct dt_reg *reg = pin_priv;
+
+ dt_pin_enter(reg, pin);
+ *direction = reg->direction;
+ dt_leave(reg->dom);
+
+ return 0;
+}
+
+static int dt_direction_set(const struct dpll_pin *pin, void *pin_priv,
+ const struct dpll_device *dpll, void *dpll_priv,
+ const enum dpll_pin_direction direction,
+ struct netlink_ext_ack *extack)
+{
+ struct dt_reg *reg = pin_priv;
+
+ dt_pin_enter(reg, pin);
+ reg->direction = direction;
+ dt_leave(reg->dom);
+
+ return 0;
+}
+
+static int dt_ref_sync_get(const struct dpll_pin *pin, void *pin_priv,
+ const struct dpll_pin *ref_sync_pin,
+ void *ref_sync_pin_priv, enum dpll_pin_state *state,
+ struct netlink_ext_ack *extack)
+{
+ struct dt_reg *reg = pin_priv;
+
+ dt_pin_enter(reg, pin);
+ *state = DPLL_PIN_STATE_DISCONNECTED;
+ dt_leave(reg->dom);
+
+ return 0;
+}
+
+static const struct dpll_pin_ops dt_pin_ops = {
+ .frequency_get = dt_frequency_get,
+ .state_on_dpll_get = dt_state_on_dpll_get,
+ .state_on_dpll_set = dt_state_on_dpll_set,
+ .state_on_pin_get = dt_state_on_pin_get,
+ .state_on_pin_set = dt_state_on_pin_set,
+ .prio_get = dt_prio_get,
+ .prio_set = dt_prio_set,
+ .direction_get = dt_direction_get,
+ .direction_set = dt_direction_set,
+ .ref_sync_get = dt_ref_sync_get,
+};
+
+static const struct dpll_pin_properties dt_prop = {
+ .type = DPLL_PIN_TYPE_EXT,
+ .capabilities = DPLL_PIN_CAPABILITIES_STATE_CAN_CHANGE |
+ DPLL_PIN_CAPABILITIES_PRIORITY_CAN_CHANGE |
+ DPLL_PIN_CAPABILITIES_DIRECTION_CAN_CHANGE,
+};
+
+static const struct dpll_pin_properties dt_prop_mux = {
+ .type = DPLL_PIN_TYPE_MUX,
+ .capabilities = DPLL_PIN_CAPABILITIES_STATE_CAN_CHANGE,
+};
+
+/* Other dpll drivers in the system can send notifications at any time */
+static bool dt_own_clock_id(u64 clock_id)
+{
+ switch (clock_id) {
+ case DT_CLOCK_ID_A:
+ case DT_CLOCK_ID_B:
+ case DT_CLOCK_ID_E:
+ case DT_CLOCK_ID_F:
+ case DT_CLOCK_ID_G:
+ return true;
+ default:
+ return false;
+ }
+}
+
+static int dt_notifier(struct notifier_block *nb, unsigned long action,
+ void *data)
+{
+ struct dt_ctx *ctx = container_of(nb, struct dt_ctx, nb);
+ struct dpll_device_notifier_info *dev_info = data;
+ struct dpll_pin_notifier_info *pin_info = data;
+ struct dpll_domain *domain;
+ u64 clock_id;
+ void *obj;
+
+ switch (action) {
+ case DPLL_DEVICE_CHANGED:
+ clock_id = dev_info->clock_id;
+ obj = dev_info->dpll;
+ domain = dev_info->dpll->domain;
+ break;
+ case DPLL_PIN_CHANGED:
+ clock_id = pin_info->clock_id;
+ obj = pin_info->pin;
+ domain = pin_info->pin->domain;
+ break;
+ default:
+ return NOTIFY_DONE;
+ }
+
+ if (!dt_own_clock_id(clock_id) || !ctx->record)
+ return NOTIFY_DONE;
+
+ if (!dt_held(domain))
+ atomic_inc(&ctx->bad_ntf);
+ if (ctx->num_events < DT_MAX_EVENTS)
+ ctx->events[ctx->num_events] = obj;
+ ctx->num_events++;
+
+ return NOTIFY_OK;
+}
+
+static void dt_final_check(void *data)
+{
+ struct dt_ctx *ctx = data;
+
+ KUNIT_EXPECT_EQ(ctx->test, atomic_read(&ctx->wrong_lock), 0);
+ if (ctx->debug_locks_at_start)
+ KUNIT_EXPECT_TRUE(ctx->test, debug_locks);
+}
+
+static void dt_dev_release(void *data)
+{
+ struct dt_dev *dev = data;
+
+ dpll_device_unregister(dev->dpll, &dt_dev_ops, dev);
+ dpll_device_put(dev->dpll, &dev->tracker);
+}
+
+static void dt_dev_add(struct kunit *test, struct dt_dev *dev,
+ struct dt_dom *dom, u64 clock_id, u32 idx,
+ enum dpll_type type)
+{
+ int ret;
+
+ dev->dom = dom;
+ dev->mode = DPLL_MODE_AUTOMATIC;
+ dev->dpll = dpll_device_get(clock_id, idx, THIS_MODULE, &dev->tracker);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, dev->dpll);
+
+ ret = dpll_device_register(dev->dpll, type, &dt_dev_ops, dev);
+ if (ret)
+ dpll_device_put(dev->dpll, &dev->tracker);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test, dt_dev_release,
+ dev), 0);
+}
+
+static void dt_pin_release(void *data)
+{
+ struct dt_pin *pin = data;
+
+ if (!IS_ERR_OR_NULL(pin->pin))
+ dpll_pin_put(pin->pin, &pin->tracker);
+}
+
+static void dt_pin_add(struct kunit *test, struct dt_pin *pin, u64 clock_id,
+ u32 idx, const struct dpll_pin_properties *prop)
+{
+ pin->pin = dpll_pin_get(clock_id, idx, THIS_MODULE, prop,
+ &pin->tracker);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, pin->pin);
+ KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test, dt_pin_release,
+ pin), 0);
+}
+
+static void dt_reg_release(void *data)
+{
+ struct dt_reg *reg = data;
+
+ if (reg->parent)
+ dpll_pin_on_pin_unregister(reg->parent->pin, reg->pin->pin,
+ &dt_pin_ops, reg);
+ else
+ dpll_pin_unregister(reg->dev->dpll, reg->pin->pin, &dt_pin_ops,
+ reg);
+}
+
+static void dt_reg_add(struct kunit *test, struct dt_reg *reg,
+ struct dt_dom *dom, struct dt_pin *pin,
+ struct dt_dev *dev, struct dt_pin *parent)
+{
+ int ret;
+
+ reg->dom = dom;
+ reg->pin = pin;
+ reg->dev = dev;
+ reg->parent = parent;
+ reg->state = DPLL_PIN_STATE_SELECTABLE;
+ reg->direction = DPLL_PIN_DIRECTION_INPUT;
+
+ if (parent)
+ ret = dpll_pin_on_pin_register(parent->pin, pin->pin,
+ &dt_pin_ops, reg);
+ else
+ ret = dpll_pin_register(dev->dpll, pin->pin, &dt_pin_ops, reg);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test, dt_reg_release,
+ reg), 0);
+}
+
+static void dt_fwnode_release(void *data)
+{
+ fwnode_remove_software_node(data);
+}
+
+/* Creates a pin with a fwnode so that it can be registered with another dpll */
+static void dt_fwnode_pin_add(struct kunit *test, struct dt_pin *pin,
+ u64 clock_id, u32 idx)
+{
+ struct fwnode_handle *fwnode;
+
+ fwnode = fwnode_create_software_node(NULL, NULL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, fwnode);
+ KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test, dt_fwnode_release,
+ fwnode), 0);
+ dt_pin_add(test, pin, clock_id, idx, &dt_prop);
+ dpll_pin_fwnode_set(pin->pin, fwnode);
+}
+
+/* Builds domains E and F connected by pin X */
+static void dt_foreign_pin_add(struct kunit *test)
+{
+ struct dt_ctx *ctx = test->priv;
+
+ dt_dev_add(test, &ctx->e0, &ctx->dom_e, DT_CLOCK_ID_E, 0,
+ DPLL_TYPE_PPS);
+ dt_dev_add(test, &ctx->f0, &ctx->dom_f, DT_CLOCK_ID_F, 0,
+ DPLL_TYPE_PPS);
+ dt_fwnode_pin_add(test, &ctx->x, DT_CLOCK_ID_E, DT_PIN_IDX_X);
+ dt_reg_add(test, &ctx->reg_x_e0, &ctx->dom_e, &ctx->x, &ctx->e0, NULL);
+ dt_reg_add(test, &ctx->reg_x_f0, &ctx->dom_f, &ctx->x, &ctx->f0, NULL);
+
+ /* No callback of the pin takes the lock of the other domain */
+ ctx->reg_x_e0.forbid = ctx->f0.dpll->domain;
+ ctx->reg_x_f0.forbid = ctx->f0.dpll->domain;
+}
+
+/*
+ * Builds domains E and G connected by pin C under pin M. The pins are added
+ * first, so that domain G is freed only after the registrations are gone.
+ */
+static void dt_on_pin_add(struct kunit *test)
+{
+ struct dt_ctx *ctx = test->priv;
+
+ dt_dev_add(test, &ctx->e0, &ctx->dom_e, DT_CLOCK_ID_E, 0,
+ DPLL_TYPE_PPS);
+ dt_pin_add(test, &ctx->m, DT_CLOCK_ID_E, DT_PIN_IDX_M, &dt_prop_mux);
+ dt_pin_add(test, &ctx->c, DT_CLOCK_ID_G, DT_PIN_IDX_C, &dt_prop);
+ dt_reg_add(test, &ctx->reg_m_e0, &ctx->dom_e, &ctx->m, &ctx->e0, NULL);
+ dt_reg_add(test, &ctx->reg_c_m, &ctx->dom_g, &ctx->c, NULL, &ctx->m);
+
+ ctx->reg_c_m.forbid = ctx->e0.dpll->domain;
+ ctx->reg_m_e0.forbid = ctx->c.pin->domain;
+}
+
+static void dt_notifier_release(void *data)
+{
+ struct dt_ctx *ctx = data;
+
+ unregister_dpll_notifier(&ctx->nb);
+}
+
+static void dt_dom_init(struct dt_dom *dom, struct dt_ctx *ctx)
+{
+ dom->ctx = ctx;
+ init_completion(&dom->entered);
+}
+
+static int dt_init(struct kunit *test)
+{
+ struct dt_ctx *ctx;
+
+ ctx = kunit_kzalloc(test, sizeof(*ctx), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, ctx);
+ test->priv = ctx;
+
+ ctx->test = test;
+ dt_dom_init(&ctx->dom_a, ctx);
+ dt_dom_init(&ctx->dom_b, ctx);
+ dt_dom_init(&ctx->dom_e, ctx);
+ dt_dom_init(&ctx->dom_f, ctx);
+ dt_dom_init(&ctx->dom_g, ctx);
+ init_waitqueue_head(&ctx->wq);
+ ctx->debug_locks_at_start = debug_locks;
+
+ /* Runs last, after the deletion of all objects */
+ KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test, dt_final_check,
+ ctx), 0);
+
+ dt_dev_add(test, &ctx->a0, &ctx->dom_a, DT_CLOCK_ID_A, 0,
+ DPLL_TYPE_PPS);
+ dt_dev_add(test, &ctx->a1, &ctx->dom_a, DT_CLOCK_ID_A, 1,
+ DPLL_TYPE_EEC);
+ dt_dev_add(test, &ctx->b0, &ctx->dom_b, DT_CLOCK_ID_B, 0,
+ DPLL_TYPE_PPS);
+ ctx->a0.notify = &ctx->a1;
+
+ dt_pin_add(test, &ctx->p, DT_CLOCK_ID_A, DT_PIN_IDX_P, &dt_prop);
+ dt_reg_add(test, &ctx->reg_p_a0, &ctx->dom_a, &ctx->p, &ctx->a0, NULL);
+ dt_reg_add(test, &ctx->reg_p_a1, &ctx->dom_a, &ctx->p, &ctx->a1, NULL);
+ ctx->reg_p_a0.notify = &ctx->s;
+
+ dt_pin_add(test, &ctx->s, DT_CLOCK_ID_A, DT_PIN_IDX_S, &dt_prop);
+ dt_reg_add(test, &ctx->reg_s_a0, &ctx->dom_a, &ctx->s, &ctx->a0, NULL);
+ dt_reg_add(test, &ctx->reg_s_a1, &ctx->dom_a, &ctx->s, &ctx->a1, NULL);
+
+ dt_pin_add(test, &ctx->bp, DT_CLOCK_ID_B, DT_PIN_IDX_BP, &dt_prop);
+ dt_reg_add(test, &ctx->reg_bp_b0, &ctx->dom_b, &ctx->bp, &ctx->b0,
+ NULL);
+
+ ctx->nb.notifier_call = dt_notifier;
+ KUNIT_ASSERT_EQ(test, register_dpll_notifier(&ctx->nb), 0);
+ KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test,
+ dt_notifier_release,
+ ctx), 0);
+
+ atomic_set(&ctx->peak_total, 0);
+ atomic_set(&ctx->dom_a.peak, 0);
+ atomic_set(&ctx->dom_b.peak, 0);
+
+ return 0;
+}
+
+static int dt_device_ntf(void *arg)
+{
+ struct dt_dev *dev = arg;
+
+ return dpll_device_change_ntf(dev->dpll);
+}
+
+static int dt_pin_ntf(void *arg)
+{
+ struct dt_pin *pin = arg;
+
+ return dpll_pin_change_ntf(pin->pin);
+}
+
+static int dt_extra_pin_get(void *arg)
+{
+ struct dt_pin *pin = arg;
+
+ pin->pin = dpll_pin_get(DT_CLOCK_ID_B, DT_PIN_IDX_EXTRA, THIS_MODULE,
+ &dt_prop, &pin->tracker);
+
+ return PTR_ERR_OR_ZERO(pin->pin);
+}
+
+static void dt_unblock(struct dt_ctx *ctx)
+{
+ WRITE_ONCE(ctx->dom_a.block, false);
+ WRITE_ONCE(ctx->dom_b.block, false);
+ WRITE_ONCE(ctx->dom_e.block, false);
+ WRITE_ONCE(ctx->dom_f.block, false);
+ WRITE_ONCE(ctx->dom_g.block, false);
+ WRITE_ONCE(ctx->f0.block, false);
+ WRITE_ONCE(ctx->reg_p_a0.block_prio_set, false);
+ WRITE_ONCE(ctx->reg_x_e0.block_prio_set, false);
+ WRITE_ONCE(ctx->reg_x_f0.block_prio_set, false);
+ wake_up_all(&ctx->wq);
+}
+
+static int dt_req_thread(void *data)
+{
+ struct dt_req *req = data;
+
+ req->ret = req->fn(req->arg);
+ kthread_complete_and_exit(&req->done, 0);
+}
+
+/* Runs before the objects used by the request are released */
+static void dt_req_release(void *data)
+{
+ struct dt_req *req = data;
+
+ dt_unblock(req->ctx);
+ if (!req->finished)
+ wait_for_completion(&req->done);
+}
+
+static struct dt_req *dt_req_start(struct kunit *test, int (*fn)(void *arg),
+ void *arg)
+{
+ struct task_struct *task;
+ struct dt_req *req;
+
+ req = kunit_kzalloc(test, sizeof(*req), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, req);
+
+ req->ctx = test->priv;
+ req->fn = fn;
+ req->arg = arg;
+ init_completion(&req->done);
+
+ task = kthread_run(dt_req_thread, req, "dpll-test");
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, task);
+ KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test, dt_req_release,
+ req), 0);
+
+ return req;
+}
+
+/*
+ * Consumes the completion, so it must not be waited for again. Assert it before
+ * reading what the request wrote.
+ */
+static bool dt_done(struct dt_req *req, unsigned long timeout)
+{
+ if (!req->finished && wait_for_completion_timeout(&req->done, timeout))
+ req->finished = true;
+
+ return req->finished;
+}
+
+/* Blocks a request of @dom in a callback, which holds the domain lock */
+static struct dt_req *dt_block_in(struct kunit *test, struct dt_dom *dom,
+ int (*fn)(void *arg), void *arg)
+{
+ struct dt_req *req;
+
+ WRITE_ONCE(dom->block, true);
+ req = dt_req_start(test, fn, arg);
+ KUNIT_ASSERT_TRUE(test, wait_for_completion_timeout(&dom->entered,
+ DT_TIMEOUT));
+
+ return req;
+}
+
+/* Build a set request and pass it to the handlers genetlink would call */
+static int dt_pin_set_fn(void *arg)
+{
+ struct dt_pin_set_args *args = arg;
+ struct nlattr *attrs[DPLL_A_PIN_MAX + 1] = {};
+ struct genl_info info = {};
+ struct nlmsghdr *nlh;
+ struct sk_buff *skb;
+ struct nlattr *nest;
+ int i, ret;
+
+ if (args->ctx)
+ WRITE_ONCE(args->ctx->set_task, current);
+
+ skb = nlmsg_new(NLMSG_DEFAULT_SIZE, GFP_KERNEL);
+ if (!skb)
+ return -ENOMEM;
+ ret = -EMSGSIZE;
+ nlh = nlmsg_put(skb, 0, 0, NLMSG_MIN_TYPE, GENL_HDRLEN, 0);
+ if (!nlh || nla_put_u32(skb, DPLL_A_PIN_ID, args->pin->id))
+ goto out;
+ for (i = 0; i < args->num; i++) {
+ const struct dt_nest *n = &args->nests[i];
+
+ nest = nla_nest_start(skb, n->type);
+ if (!nest ||
+ nla_put_u32(skb, DPLL_A_PIN_PARENT_ID, n->parent_id) ||
+ nla_put_u32(skb, n->attr, n->val))
+ goto out;
+ nla_nest_end(skb, nest);
+ }
+ nlmsg_end(skb, nlh);
+
+ info.genlhdr = nlmsg_data(nlh);
+ info.attrs = attrs;
+ attrs[DPLL_A_PIN_ID] = nla_find(genlmsg_data(info.genlhdr),
+ genlmsg_len(info.genlhdr),
+ DPLL_A_PIN_ID);
+
+ ret = dpll_pin_pre_doit(NULL, skb, &info);
+ if (!ret) {
+ ret = dpll_nl_pin_set_doit(skb, &info);
+ dpll_pin_post_doit(NULL, skb, &info);
+ }
+out:
+ nlmsg_free(skb);
+
+ return ret;
+}
+
+static int dt_mode_set_fn(void *arg)
+{
+ struct dt_mode_set_args *args = arg;
+ struct nlattr *attrs[DPLL_A_MAX + 1] = {};
+ struct genl_info info = {};
+ struct nlmsghdr *nlh;
+ struct sk_buff *skb;
+ int ret;
+
+ skb = nlmsg_new(NLMSG_DEFAULT_SIZE, GFP_KERNEL);
+ if (!skb)
+ return -ENOMEM;
+ ret = -EMSGSIZE;
+ nlh = nlmsg_put(skb, 0, 0, NLMSG_MIN_TYPE, GENL_HDRLEN, 0);
+ if (!nlh || nla_put_u32(skb, DPLL_A_ID, args->dpll->id) ||
+ nla_put_u32(skb, DPLL_A_MODE, args->mode))
+ goto out;
+ nlmsg_end(skb, nlh);
+
+ info.genlhdr = nlmsg_data(nlh);
+ info.attrs = attrs;
+ attrs[DPLL_A_ID] = nla_find(genlmsg_data(info.genlhdr),
+ genlmsg_len(info.genlhdr), DPLL_A_ID);
+
+ ret = dpll_pre_doit(NULL, skb, &info);
+ if (!ret) {
+ ret = dpll_nl_device_set_doit(skb, &info);
+ dpll_post_doit(NULL, skb, &info);
+ }
+out:
+ nlmsg_free(skb);
+
+ return ret;
+}
+
+/* Runs a request that should not block in a kthread and returns its result */
+static int dt_run(struct kunit *test, int (*fn)(void *arg), void *arg)
+{
+ struct dt_req *req = dt_req_start(test, fn, arg);
+
+ KUNIT_ASSERT_TRUE(test, dt_done(req, DT_TIMEOUT));
+
+ return req->ret;
+}
+
+static struct dt_pin_set_args *dt_pin_set_args_alloc(struct kunit *test,
+ struct dpll_pin *pin,
+ int num)
+{
+ struct dt_pin_set_args *args;
+
+ args = kunit_kzalloc(test, sizeof(*args), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, args);
+ args->pin = pin;
+ args->num = num;
+
+ return args;
+}
+
+static struct dt_pin_set_args *dt_pin_set_args_one(struct kunit *test,
+ struct dpll_pin *pin,
+ int type, u32 parent_id,
+ int attr, u32 val)
+{
+ struct dt_pin_set_args *args = dt_pin_set_args_alloc(test, pin, 1);
+
+ args->nests[0].type = type;
+ args->nests[0].parent_id = parent_id;
+ args->nests[0].attr = attr;
+ args->nests[0].val = val;
+
+ return args;
+}
+
+static int dt_pin_set_one(struct kunit *test, struct dpll_pin *pin, int type,
+ u32 parent_id, int attr, u32 val)
+{
+ return dt_run(test, dt_pin_set_fn,
+ dt_pin_set_args_one(test, pin, type, parent_id, attr,
+ val));
+}
+
+static void dt_expect_events(struct kunit *test, void * const *expected,
+ int num)
+{
+ struct dt_ctx *ctx = test->priv;
+ int i;
+
+ KUNIT_ASSERT_EQ(test, ctx->num_events, num);
+ for (i = 0; i < num; i++)
+ KUNIT_EXPECT_PTR_EQ(test, ctx->events[i], expected[i]);
+ KUNIT_EXPECT_EQ(test, atomic_read(&ctx->bad_ntf), 0);
+}
+
+/* Objects with the same (module, clock_id) share a refcounted domain */
+static void dt_test_domain_sharing(struct kunit *test)
+{
+ struct dt_ctx *ctx = test->priv;
+ struct dpll_domain *a = ctx->a0.dpll->domain;
+ struct dpll_domain *b = ctx->b0.dpll->domain;
+
+ KUNIT_EXPECT_PTR_EQ(test, ctx->a1.dpll->domain, a);
+ KUNIT_EXPECT_PTR_EQ(test, ctx->p.pin->domain, a);
+ KUNIT_EXPECT_PTR_EQ(test, ctx->bp.pin->domain, b);
+ KUNIT_EXPECT_PTR_NE(test, a, b);
+
+ KUNIT_EXPECT_EQ(test, refcount_read(&a->refcount), 4);
+ KUNIT_EXPECT_EQ(test, refcount_read(&b->refcount), 2);
+
+ KUNIT_ASSERT_EQ(test, dt_extra_pin_get(&ctx->extra), 0);
+ KUNIT_EXPECT_PTR_EQ(test, ctx->extra.pin->domain, b);
+ KUNIT_EXPECT_EQ(test, refcount_read(&b->refcount), 3);
+ dpll_pin_put(ctx->extra.pin, &ctx->extra.tracker);
+ KUNIT_EXPECT_EQ(test, refcount_read(&b->refcount), 2);
+}
+
+/* The same clock_id used by another module means another domain */
+static void dt_test_domain_per_module(struct kunit *test)
+{
+ struct dt_ctx *ctx = test->priv;
+ static char other_module;
+ struct dpll_device *dpll;
+ dpll_tracker tracker;
+
+ /* Never registered, so the core does not dereference the module */
+ dpll = dpll_device_get(DT_CLOCK_ID_A, 0, (struct module *)&other_module,
+ &tracker);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, dpll);
+ KUNIT_EXPECT_PTR_NE(test, dpll, ctx->a0.dpll);
+ KUNIT_EXPECT_PTR_NE(test, dpll->domain, ctx->a0.dpll->domain);
+ dpll_device_put(dpll, &tracker);
+}
+
+/* A pin on a dpll of another domain stays in its domain */
+static void dt_test_foreign_registration_domain(struct kunit *test)
+{
+ struct dt_ctx *ctx = test->priv;
+ struct dpll_domain *e, *f;
+
+ dt_foreign_pin_add(test);
+ e = ctx->e0.dpll->domain;
+ f = ctx->f0.dpll->domain;
+
+ KUNIT_EXPECT_PTR_NE(test, e, f);
+ KUNIT_EXPECT_PTR_EQ(test, ctx->x.pin->domain, e);
+ KUNIT_EXPECT_EQ(test, refcount_read(&e->refcount), 2);
+ KUNIT_EXPECT_EQ(test, refcount_read(&f->refcount), 1);
+}
+
+/* A pin under a parent pin of another domain stays in its domain */
+static void dt_test_on_pin_registration_domain(struct kunit *test)
+{
+ struct dt_ctx *ctx = test->priv;
+ struct dpll_domain *e, *g;
+
+ dt_on_pin_add(test);
+ e = ctx->e0.dpll->domain;
+ g = ctx->c.pin->domain;
+
+ KUNIT_EXPECT_PTR_NE(test, e, g);
+ KUNIT_EXPECT_PTR_EQ(test, ctx->m.pin->domain, e);
+ KUNIT_EXPECT_EQ(test, refcount_read(&e->refcount), 2);
+ KUNIT_EXPECT_EQ(test, refcount_read(&g->refcount), 1);
+}
+
+/* A callback blocked in one domain does not stop other domains */
+static void dt_test_domains_run_in_parallel(struct kunit *test)
+{
+ struct dt_ctx *ctx = test->priv;
+ struct dt_req *blocked, *other;
+
+ blocked = dt_block_in(test, &ctx->dom_a, dt_device_ntf, &ctx->a0);
+
+ other = dt_req_start(test, dt_device_ntf, &ctx->b0);
+ KUNIT_EXPECT_TRUE(test, dt_done(other, DT_TIMEOUT));
+ KUNIT_EXPECT_FALSE(test, completion_done(&blocked->done));
+ KUNIT_EXPECT_EQ(test, atomic_read(&ctx->peak_total), 2);
+
+ dt_unblock(ctx);
+ KUNIT_EXPECT_TRUE(test, dt_done(blocked, DT_TIMEOUT));
+}
+
+/* Callbacks of one domain are serialized, across its devices too */
+static void dt_test_domain_is_serialized(struct kunit *test)
+{
+ struct dt_ctx *ctx = test->priv;
+ struct dt_req *first, *second;
+
+ first = dt_block_in(test, &ctx->dom_a, dt_device_ntf, &ctx->a0);
+
+ second = dt_req_start(test, dt_device_ntf, &ctx->a1);
+ KUNIT_EXPECT_FALSE(test, dt_done(second, DT_SHORT_TIMEOUT));
+
+ dt_unblock(ctx);
+ KUNIT_EXPECT_TRUE(test, dt_done(first, DT_TIMEOUT));
+ KUNIT_EXPECT_TRUE(test, dt_done(second, DT_TIMEOUT));
+ KUNIT_EXPECT_EQ(test, atomic_read(&ctx->dom_a.peak), 1);
+}
+
+/* Callbacks of all refs of a pin run under the domain of the pin alone */
+static void dt_test_foreign_get_locks(struct kunit *test)
+{
+ struct dt_ctx *ctx = test->priv;
+
+ if (!IS_ENABLED(CONFIG_LOCKDEP) || !debug_locks)
+ kunit_skip(test, "needs lockdep");
+
+ dt_foreign_pin_add(test);
+
+ KUNIT_EXPECT_EQ(test, dt_run(test, dt_pin_ntf, &ctx->x), 0);
+ KUNIT_EXPECT_EQ(test, atomic_read(&ctx->wrong_lock), 0);
+}
+
+/* Setters of a foreign ref run under the domain of the pin alone */
+static void dt_test_foreign_set_locks(struct kunit *test)
+{
+ static const struct {
+ int attr;
+ u32 val;
+ } sets[] = {
+ { DPLL_A_PIN_STATE, DPLL_PIN_STATE_CONNECTED },
+ { DPLL_A_PIN_PRIO, 7 },
+ { DPLL_A_PIN_DIRECTION, DPLL_PIN_DIRECTION_OUTPUT },
+ };
+ struct dt_ctx *ctx = test->priv;
+ int i;
+
+ dt_foreign_pin_add(test);
+
+ for (i = 0; i < ARRAY_SIZE(sets); i++) {
+ int ret;
+
+ ctx->num_events = 0;
+ ctx->record = true;
+ ret = dt_pin_set_one(test, ctx->x.pin, DPLL_A_PIN_PARENT_DEVICE,
+ ctx->f0.dpll->id, sets[i].attr,
+ sets[i].val);
+ ctx->record = false;
+
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ dt_expect_events(test, (void *[]){ ctx->x.pin }, 1);
+ }
+ KUNIT_EXPECT_EQ(test, ctx->reg_x_f0.state, DPLL_PIN_STATE_CONNECTED);
+ KUNIT_EXPECT_EQ(test, ctx->reg_x_f0.prio, 7);
+ KUNIT_EXPECT_EQ(test, ctx->reg_x_f0.direction,
+ DPLL_PIN_DIRECTION_OUTPUT);
+ KUNIT_EXPECT_EQ(test, ctx->reg_x_e0.prio, 0);
+}
+
+/* The callback of a foreign ref can notify objects of the domain of the pin */
+static void dt_test_foreign_callback_notifies_pin(struct kunit *test)
+{
+ struct dt_ctx *ctx = test->priv;
+ int ret;
+
+ dt_foreign_pin_add(test);
+ ctx->reg_x_f0.notify = &ctx->x;
+
+ ctx->record = true;
+ ret = dt_pin_set_one(test, ctx->x.pin, DPLL_A_PIN_PARENT_DEVICE,
+ ctx->f0.dpll->id, DPLL_A_PIN_STATE,
+ DPLL_PIN_STATE_CONNECTED);
+ ctx->record = false;
+
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ dt_expect_events(test, (void *[]){ ctx->x.pin, ctx->x.pin }, 2);
+}
+
+/* Without an owner the pin-level attributes come from the foreign ref */
+static void dt_test_pin_without_owner(struct kunit *test)
+{
+ struct dt_ctx *ctx = test->priv;
+
+ dt_dev_add(test, &ctx->f0, &ctx->dom_f, DT_CLOCK_ID_F, 0,
+ DPLL_TYPE_PPS);
+ dt_fwnode_pin_add(test, &ctx->x, DT_CLOCK_ID_E, DT_PIN_IDX_X);
+ dt_reg_add(test, &ctx->reg_x_f0, &ctx->dom_f, &ctx->x, &ctx->f0, NULL);
+ KUNIT_ASSERT_PTR_NE(test, ctx->x.pin->domain, ctx->f0.dpll->domain);
+ ctx->reg_x_f0.forbid = ctx->f0.dpll->domain;
+
+ atomic_set(&ctx->reg_x_f0.freq_gets, 0);
+ KUNIT_EXPECT_EQ(test, dt_run(test, dt_pin_ntf, &ctx->x), 0);
+ KUNIT_EXPECT_GT(test, atomic_read(&ctx->reg_x_f0.freq_gets), 0);
+ KUNIT_EXPECT_EQ(test, atomic_read(&ctx->wrong_lock), 0);
+}
+
+/* A stuck callback of a foreign ref stops only the domain of the pin */
+static void dt_test_stuck_foreign_callback(struct kunit *test)
+{
+ struct dt_req *blocked, *owner, *foreign, *other;
+ struct dt_ctx *ctx = test->priv;
+
+ dt_foreign_pin_add(test);
+
+ WRITE_ONCE(ctx->reg_x_f0.block_prio_set, true);
+ blocked = dt_req_start(test, dt_pin_set_fn,
+ dt_pin_set_args_one(test, ctx->x.pin,
+ DPLL_A_PIN_PARENT_DEVICE,
+ ctx->f0.dpll->id,
+ DPLL_A_PIN_PRIO, 7));
+ KUNIT_ASSERT_TRUE(test, wait_for_completion_timeout(&ctx->dom_f.entered,
+ DT_TIMEOUT));
+
+ owner = dt_req_start(test, dt_device_ntf, &ctx->e0);
+ KUNIT_EXPECT_FALSE(test, dt_done(owner, DT_SHORT_TIMEOUT));
+ foreign = dt_req_start(test, dt_device_ntf, &ctx->f0);
+ KUNIT_EXPECT_TRUE(test, dt_done(foreign, DT_TIMEOUT));
+ other = dt_req_start(test, dt_device_ntf, &ctx->b0);
+ KUNIT_EXPECT_TRUE(test, dt_done(other, DT_TIMEOUT));
+
+ dt_unblock(ctx);
+ KUNIT_EXPECT_TRUE(test, dt_done(blocked, DT_TIMEOUT));
+ KUNIT_EXPECT_TRUE(test, dt_done(owner, DT_TIMEOUT));
+}
+
+/* A stuck device callback does not stop a foreign pin registered with it */
+static void dt_test_stuck_foreign_device(struct kunit *test)
+{
+ struct dt_ctx *ctx = test->priv;
+ struct dt_req *blocked, *pin;
+
+ dt_foreign_pin_add(test);
+
+ WRITE_ONCE(ctx->f0.block, true);
+ blocked = dt_req_start(test, dt_device_ntf, &ctx->f0);
+ KUNIT_ASSERT_TRUE(test, wait_for_completion_timeout(&ctx->dom_f.entered,
+ DT_TIMEOUT));
+
+ pin = dt_req_start(test, dt_pin_ntf, &ctx->x);
+ KUNIT_EXPECT_TRUE(test, dt_done(pin, DT_TIMEOUT));
+
+ dt_unblock(ctx);
+ KUNIT_EXPECT_TRUE(test, dt_done(blocked, DT_TIMEOUT));
+}
+
+/* A stuck owner stops requests for its pins, but not the foreign device */
+static void dt_test_stuck_owner_domain(struct kunit *test)
+{
+ struct dt_ctx *ctx = test->priv;
+ struct dt_req *blocked, *foreign, *pin;
+
+ dt_foreign_pin_add(test);
+
+ blocked = dt_block_in(test, &ctx->dom_e, dt_device_ntf, &ctx->e0);
+
+ foreign = dt_req_start(test, dt_device_ntf, &ctx->f0);
+ KUNIT_EXPECT_TRUE(test, dt_done(foreign, DT_TIMEOUT));
+ pin = dt_req_start(test, dt_pin_ntf, &ctx->x);
+ KUNIT_EXPECT_FALSE(test, dt_done(pin, DT_SHORT_TIMEOUT));
+
+ dt_unblock(ctx);
+ KUNIT_EXPECT_TRUE(test, dt_done(blocked, DT_TIMEOUT));
+ KUNIT_EXPECT_TRUE(test, dt_done(pin, DT_TIMEOUT));
+}
+
+/* A pin and its parent pin of another domain do not block each other */
+static void dt_test_on_pin_domains_are_independent(struct kunit *test)
+{
+ struct dt_ctx *ctx = test->priv;
+ struct dt_req *blocked, *other;
+
+ dt_on_pin_add(test);
+
+ blocked = dt_block_in(test, &ctx->dom_e, dt_device_ntf, &ctx->e0);
+ other = dt_req_start(test, dt_pin_ntf, &ctx->c);
+ KUNIT_EXPECT_TRUE(test, dt_done(other, DT_TIMEOUT));
+ dt_unblock(ctx);
+ KUNIT_EXPECT_TRUE(test, dt_done(blocked, DT_TIMEOUT));
+
+ blocked = dt_block_in(test, &ctx->dom_g, dt_pin_ntf, &ctx->c);
+ other = dt_req_start(test, dt_pin_ntf, &ctx->m);
+ KUNIT_EXPECT_TRUE(test, dt_done(other, DT_TIMEOUT));
+ dt_unblock(ctx);
+ KUNIT_EXPECT_TRUE(test, dt_done(blocked, DT_TIMEOUT));
+}
+
+/* Registrations in both directions between two domains are fine */
+static void dt_test_registrations_both_directions(struct kunit *test)
+{
+ struct dt_ctx *ctx = test->priv;
+
+ dt_foreign_pin_add(test);
+ dt_fwnode_pin_add(test, &ctx->y, DT_CLOCK_ID_F, DT_PIN_IDX_Y);
+ dt_reg_add(test, &ctx->reg_y_f0, &ctx->dom_f, &ctx->y, &ctx->f0, NULL);
+ dt_reg_add(test, &ctx->reg_y_e0, &ctx->dom_e, &ctx->y, &ctx->e0, NULL);
+ ctx->reg_y_f0.forbid = ctx->e0.dpll->domain;
+ ctx->reg_y_e0.forbid = ctx->e0.dpll->domain;
+
+ KUNIT_EXPECT_EQ(test, dt_run(test, dt_pin_ntf, &ctx->x), 0);
+ KUNIT_EXPECT_EQ(test, dt_run(test, dt_pin_ntf, &ctx->y), 0);
+}
+
+/* A topology change waits until running requests finish */
+static void dt_test_topology_change_waits(struct kunit *test)
+{
+ struct dt_ctx *ctx = test->priv;
+ struct dt_req *blocked, *writer;
+
+ blocked = dt_block_in(test, &ctx->dom_a, dt_device_ntf, &ctx->a0);
+
+ /* Added before the request starts, so it runs after waiting for it */
+ KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test, dt_pin_release,
+ &ctx->extra), 0);
+ writer = dt_req_start(test, dt_extra_pin_get, &ctx->extra);
+ KUNIT_EXPECT_FALSE(test, dt_done(writer, DT_SHORT_TIMEOUT));
+
+ dt_unblock(ctx);
+ KUNIT_EXPECT_TRUE(test, dt_done(blocked, DT_TIMEOUT));
+ KUNIT_ASSERT_TRUE(test, dt_done(writer, DT_TIMEOUT));
+ KUNIT_EXPECT_NOT_ERR_OR_NULL(test, ctx->extra.pin);
+}
+
+/* Other requests about a pin cannot see a set of two attributes half done */
+static void dt_check_request_atomic(struct kunit *test, struct dt_pin *pin,
+ struct dt_dev *dev0, struct dt_reg *reg0,
+ struct dt_dev *dev1, struct dt_reg *reg1)
+{
+ struct dt_pin_set_args *args = dt_pin_set_args_alloc(test, pin->pin, 2);
+ struct dt_ctx *ctx = test->priv;
+ struct dt_req *set, *other;
+ int i;
+
+ args->ctx = ctx;
+ for (i = 0; i < args->num; i++) {
+ args->nests[i].type = DPLL_A_PIN_PARENT_DEVICE;
+ args->nests[i].parent_id = i ? dev1->dpll->id : dev0->dpll->id;
+ args->nests[i].attr = DPLL_A_PIN_PRIO;
+ args->nests[i].val = 11;
+ }
+
+ ctx->atomic_regs[0] = reg0;
+ ctx->atomic_regs[1] = reg1;
+
+ /* The other request starts to wait before the first attribute is set */
+ WRITE_ONCE(reg0->block_prio_set, true);
+ set = dt_req_start(test, dt_pin_set_fn, args);
+ KUNIT_ASSERT_TRUE(test, wait_for_completion_timeout(®0->dom->entered,
+ DT_TIMEOUT));
+ other = dt_req_start(test, dt_pin_ntf, pin);
+ KUNIT_EXPECT_FALSE(test, dt_done(other, DT_SHORT_TIMEOUT));
+
+ dt_unblock(ctx);
+ KUNIT_ASSERT_TRUE(test, dt_done(set, DT_TIMEOUT));
+ KUNIT_ASSERT_TRUE(test, dt_done(other, DT_TIMEOUT));
+ KUNIT_EXPECT_EQ(test, set->ret, 0);
+ KUNIT_EXPECT_EQ(test, reg0->prio, 11);
+ KUNIT_EXPECT_EQ(test, reg1->prio, 11);
+ KUNIT_EXPECT_EQ(test, atomic_read(&ctx->torn), 0);
+}
+
+static void dt_test_request_is_atomic(struct kunit *test)
+{
+ struct dt_ctx *ctx = test->priv;
+
+ dt_check_request_atomic(test, &ctx->p, &ctx->a0, &ctx->reg_p_a0,
+ &ctx->a1, &ctx->reg_p_a1);
+}
+
+static void dt_test_spanning_request_is_atomic(struct kunit *test)
+{
+ struct dt_ctx *ctx = test->priv;
+
+ dt_foreign_pin_add(test);
+ dt_check_request_atomic(test, &ctx->x, &ctx->e0, &ctx->reg_x_e0,
+ &ctx->f0, &ctx->reg_x_f0);
+}
+
+/* Notifications from a callback come first, under the domain lock */
+static void dt_test_ntf_order(struct kunit *test)
+{
+ struct dt_ctx *ctx = test->priv;
+ int ret;
+
+ ctx->record = true;
+ ret = dt_pin_set_one(test, ctx->p.pin, DPLL_A_PIN_PARENT_DEVICE,
+ ctx->a0.dpll->id, DPLL_A_PIN_STATE,
+ DPLL_PIN_STATE_CONNECTED);
+ ctx->record = false;
+
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, ctx->reg_p_a0.state, DPLL_PIN_STATE_CONNECTED);
+ dt_expect_events(test, (void *[]){ ctx->s.pin, ctx->p.pin }, 2);
+}
+
+/* A notification from a callback that fails is still sent */
+static void dt_test_ntf_on_failure(struct kunit *test)
+{
+ struct dt_ctx *ctx = test->priv;
+ int ret;
+
+ ctx->fail_after_ntf = true;
+ ctx->record = true;
+ ret = dt_pin_set_one(test, ctx->p.pin, DPLL_A_PIN_PARENT_DEVICE,
+ ctx->a0.dpll->id, DPLL_A_PIN_STATE,
+ DPLL_PIN_STATE_CONNECTED);
+ ctx->record = false;
+
+ KUNIT_EXPECT_EQ(test, ret, -EIO);
+ dt_expect_events(test, (void *[]){ ctx->s.pin }, 1);
+}
+
+/* A set on a pin under a parent of another domain needs only its domain */
+static void dt_test_set_on_pin(struct kunit *test)
+{
+ struct dt_ctx *ctx = test->priv;
+ int ret;
+
+ dt_on_pin_add(test);
+
+ ctx->record = true;
+ ret = dt_pin_set_one(test, ctx->c.pin, DPLL_A_PIN_PARENT_PIN,
+ ctx->m.pin->id, DPLL_A_PIN_STATE,
+ DPLL_PIN_STATE_CONNECTED);
+ ctx->record = false;
+
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, ctx->reg_c_m.state, DPLL_PIN_STATE_CONNECTED);
+ dt_expect_events(test, (void *[]){ ctx->c.pin }, 1);
+}
+
+/* A device callback notifies a sibling device of its domain */
+static void dt_test_device_set_ntf(struct kunit *test)
+{
+ struct dt_ctx *ctx = test->priv;
+ struct dt_mode_set_args *args;
+ int ret;
+
+ args = kunit_kzalloc(test, sizeof(*args), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, args);
+ args->dpll = ctx->a0.dpll;
+ args->mode = DPLL_MODE_MANUAL;
+
+ ctx->record = true;
+ ret = dt_run(test, dt_mode_set_fn, args);
+ ctx->record = false;
+
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, ctx->a0.mode, DPLL_MODE_MANUAL);
+ dt_expect_events(test, (void *[]){ ctx->a1.dpll }, 1);
+}
+
+/* A change of a reference sync pair notifies under the domain of the pin */
+static void dt_test_ref_sync_pair(struct kunit *test)
+{
+ struct dt_ctx *ctx = test->priv;
+ int ret;
+
+ ctx->record = true;
+ ret = dpll_pin_ref_sync_pair_add(ctx->p.pin, ctx->s.pin);
+ ctx->record = false;
+
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ dt_expect_events(test, (void *[]){ ctx->p.pin }, 1);
+}
+
+static struct kunit_case dt_cases[] = {
+ KUNIT_CASE(dt_test_domain_sharing),
+ KUNIT_CASE(dt_test_domain_per_module),
+ KUNIT_CASE(dt_test_foreign_registration_domain),
+ KUNIT_CASE(dt_test_on_pin_registration_domain),
+ KUNIT_CASE(dt_test_domains_run_in_parallel),
+ KUNIT_CASE(dt_test_domain_is_serialized),
+ KUNIT_CASE(dt_test_foreign_get_locks),
+ KUNIT_CASE(dt_test_foreign_set_locks),
+ KUNIT_CASE(dt_test_foreign_callback_notifies_pin),
+ KUNIT_CASE(dt_test_pin_without_owner),
+ KUNIT_CASE(dt_test_stuck_foreign_callback),
+ KUNIT_CASE(dt_test_stuck_foreign_device),
+ KUNIT_CASE(dt_test_stuck_owner_domain),
+ KUNIT_CASE(dt_test_on_pin_domains_are_independent),
+ KUNIT_CASE(dt_test_registrations_both_directions),
+ KUNIT_CASE(dt_test_topology_change_waits),
+ KUNIT_CASE(dt_test_request_is_atomic),
+ KUNIT_CASE(dt_test_spanning_request_is_atomic),
+ KUNIT_CASE(dt_test_ntf_order),
+ KUNIT_CASE(dt_test_ntf_on_failure),
+ KUNIT_CASE(dt_test_set_on_pin),
+ KUNIT_CASE(dt_test_device_set_ntf),
+ KUNIT_CASE(dt_test_ref_sync_pair),
+ {}
+};
+
+static struct kunit_suite dt_suite = {
+ .name = "dpll_core",
+ .init = dt_init,
+ .test_cases = dt_cases,
+};
+
+kunit_test_suite(dt_suite);
+
+MODULE_DESCRIPTION("KUnit tests for locking in the DPLL core");
+MODULE_LICENSE("GPL");
diff --git a/drivers/dpll/dpll_netlink.c b/drivers/dpll/dpll_netlink.c
index 9d8f5d1ce97248..223cd1de905c1a 100644
--- a/drivers/dpll/dpll_netlink.c
+++ b/drivers/dpll/dpll_netlink.c
@@ -6,6 +6,7 @@
* Copyright (c) 2023 Intel and affiliates
*
*/
+#include <kunit/visibility.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/netdevice.h>
@@ -1836,6 +1837,7 @@ int dpll_nl_pin_set_doit(struct sk_buff *skb, struct genl_info *info)
return dpll_pin_set_from_nlattr(pin, info);
}
+EXPORT_SYMBOL_IF_KUNIT(dpll_nl_pin_set_doit);
static struct dpll_device *
dpll_device_find(u64 clock_id, struct nlattr *mod_name_attr,
@@ -2010,6 +2012,7 @@ int dpll_nl_device_set_doit(struct sk_buff *skb, struct genl_info *info)
return dpll_set_from_nlattr(dpll, info);
}
+EXPORT_SYMBOL_IF_KUNIT(dpll_nl_device_set_doit);
int dpll_nl_device_get_dumpit(struct sk_buff *skb, struct netlink_callback *cb)
{
@@ -2070,6 +2073,7 @@ int dpll_pre_doit(const struct genl_split_ops *ops, struct sk_buff *skb,
up_read(&dpll_lock);
return -ENODEV;
}
+EXPORT_SYMBOL_IF_KUNIT(dpll_pre_doit);
void dpll_post_doit(const struct genl_split_ops *ops, struct sk_buff *skb,
struct genl_info *info)
@@ -2079,6 +2083,7 @@ void dpll_post_doit(const struct genl_split_ops *ops, struct sk_buff *skb,
mutex_unlock(&dpll->domain->lock);
up_read(&dpll_lock);
}
+EXPORT_SYMBOL_IF_KUNIT(dpll_post_doit);
int
dpll_lock_doit(const struct genl_split_ops *ops, struct sk_buff *skb,
@@ -2122,6 +2127,7 @@ int dpll_pin_pre_doit(const struct genl_split_ops *ops, struct sk_buff *skb,
up_read(&dpll_lock);
return ret;
}
+EXPORT_SYMBOL_IF_KUNIT(dpll_pin_pre_doit);
void dpll_pin_post_doit(const struct genl_split_ops *ops, struct sk_buff *skb,
struct genl_info *info)
@@ -2131,3 +2137,4 @@ void dpll_pin_post_doit(const struct genl_split_ops *ops, struct sk_buff *skb,
mutex_unlock(&pin->domain->lock);
up_read(&dpll_lock);
}
+EXPORT_SYMBOL_IF_KUNIT(dpll_pin_post_doit);
--
2.56.0
^ permalink raw reply [flat|nested] 27+ messages in thread
* [PATCH net-next 07/10] dpll: reject measured_freq_get for foreign and on-pin registrations
2026-11-06 12:09 [PATCH net-next 00/10] dpll: do not serialize driver callbacks under global dpll_lock Ivan Vecera
` (5 preceding siblings ...)
2026-11-06 12:10 ` [PATCH net-next 06/10] dpll: add KUnit tests for core locking Ivan Vecera
@ 2026-11-06 12:10 ` Ivan Vecera
2026-11-10 12:24 ` Petr Oros
2026-11-06 12:10 ` [PATCH net-next 08/10] dpll: zl3073x: update locking comments Ivan Vecera
` (2 subsequent siblings)
9 siblings, 1 reply; 27+ messages in thread
From: Ivan Vecera @ 2026-11-06 12:10 UTC (permalink / raw)
To: netdev
Cc: Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
Jonathan Corbet, Shuah Khan, Randy Dunlap, Min Li, Tony Nguyen,
Przemek Kitszel, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Richard Cochran, linux-doc,
linux-kernel, intel-wired-lan, Grzegorz Nitka, Karol Kolacinski,
Saeed Mahameed, Tariq Toukan, Petr Oros
A message about a pin with .measured_freq_get calls .freq_monitor_get of
a dpll of the pin. For a pin of another domain, this device callback
would run outside the domain of the dpll, so dpll_pin_register() now
rejects such ops.
dpll_pin_on_pin_register() checks nothing about them. The message would
call .freq_monitor_get with no NULL check, on a dpll of the parent pin
that can belong to another driver, so reject them there as well.
No in-tree driver passes such ops. Add KUnit tests: the ops work on a dpll
of the domain of the pin, and are rejected on a dpll of another domain and
under a parent pin.
Signed-off-by: Ivan Vecera <ivecera@redhat.com>
---
drivers/dpll/dpll_core.c | 6 +-
drivers/dpll/dpll_core_kunit.c | 112 +++++++++++++++++++++++++++++++--
2 files changed, 111 insertions(+), 7 deletions(-)
diff --git a/drivers/dpll/dpll_core.c b/drivers/dpll/dpll_core.c
index 8eb74682343156..84377f8ba16036 100644
--- a/drivers/dpll/dpll_core.c
+++ b/drivers/dpll/dpll_core.c
@@ -958,7 +958,8 @@ dpll_pin_register(struct dpll_device *dpll, struct dpll_pin *pin,
dev_ops = dpll_device_ops(dpll);
if (WARN_ON(ops->measured_freq_get &&
- (!dev_ops || !dev_ops->freq_monitor_get ||
+ (pin->domain != dpll->domain || !dev_ops ||
+ !dev_ops->freq_monitor_get ||
!dev_ops->freq_monitor_set))) {
ret = -EINVAL;
goto out_unlock;
@@ -1065,7 +1066,8 @@ int dpll_pin_on_pin_register(struct dpll_pin *parent, struct dpll_pin *pin,
if (WARN_ON(!ops) ||
WARN_ON(!ops->state_on_pin_get) ||
- WARN_ON(!ops->direction_get))
+ WARN_ON(!ops->direction_get) ||
+ WARN_ON(ops->measured_freq_get))
return -EINVAL;
down_write(&dpll_lock);
diff --git a/drivers/dpll/dpll_core_kunit.c b/drivers/dpll/dpll_core_kunit.c
index a58800eda5ff51..84c405f855d8cb 100644
--- a/drivers/dpll/dpll_core_kunit.c
+++ b/drivers/dpll/dpll_core_kunit.c
@@ -87,6 +87,7 @@ struct dt_reg {
struct dt_dev *dev;
struct dt_pin *parent;
struct dt_pin *notify;
+ const struct dpll_pin_ops *ops;
struct dpll_domain *forbid;
enum dpll_pin_state state;
enum dpll_pin_direction direction;
@@ -276,11 +277,33 @@ static int dt_lock_status_get(const struct dpll_device *dpll, void *priv,
return 0;
}
+static int dt_freq_monitor_set(const struct dpll_device *dpll, void *priv,
+ enum dpll_feature_state state,
+ struct netlink_ext_ack *extack)
+{
+ return 0;
+}
+
+static int dt_freq_monitor_get(const struct dpll_device *dpll, void *priv,
+ enum dpll_feature_state *state,
+ struct netlink_ext_ack *extack)
+{
+ struct dt_dev *dev = priv;
+
+ dt_dev_enter(dev, dpll);
+ *state = DPLL_FEATURE_STATE_ENABLE;
+ dt_leave(dev->dom);
+
+ return 0;
+}
+
static const struct dpll_device_ops dt_dev_ops = {
.mode_get = dt_mode_get,
.mode_set = dt_mode_set,
.supported_modes_get = dt_supported_modes_get,
.lock_status_get = dt_lock_status_get,
+ .freq_monitor_set = dt_freq_monitor_set,
+ .freq_monitor_get = dt_freq_monitor_get,
};
static int dt_frequency_get(const struct dpll_pin *pin, void *pin_priv,
@@ -433,6 +456,21 @@ static int dt_ref_sync_get(const struct dpll_pin *pin, void *pin_priv,
return 0;
}
+static int dt_measured_freq_get(const struct dpll_pin *pin, void *pin_priv,
+ const struct dpll_device *dpll, void *dpll_priv,
+ u64 *measured_freq,
+ struct netlink_ext_ack *extack)
+{
+ struct dt_reg *reg = pin_priv;
+
+ dt_pin_enter(reg, pin);
+ atomic_inc(®->freq_gets);
+ *measured_freq = 1;
+ dt_leave(reg->dom);
+
+ return 0;
+}
+
static const struct dpll_pin_ops dt_pin_ops = {
.frequency_get = dt_frequency_get,
.state_on_dpll_get = dt_state_on_dpll_get,
@@ -446,6 +484,13 @@ static const struct dpll_pin_ops dt_pin_ops = {
.ref_sync_get = dt_ref_sync_get,
};
+static const struct dpll_pin_ops dt_pin_ops_measured = {
+ .measured_freq_get = dt_measured_freq_get,
+ .state_on_dpll_get = dt_state_on_dpll_get,
+ .state_on_pin_get = dt_state_on_pin_get,
+ .direction_get = dt_direction_get,
+};
+
static const struct dpll_pin_properties dt_prop = {
.type = DPLL_PIN_TYPE_EXT,
.capabilities = DPLL_PIN_CAPABILITIES_STATE_CAN_CHANGE |
@@ -570,9 +615,9 @@ static void dt_reg_release(void *data)
if (reg->parent)
dpll_pin_on_pin_unregister(reg->parent->pin, reg->pin->pin,
- &dt_pin_ops, reg);
+ reg->ops, reg);
else
- dpll_pin_unregister(reg->dev->dpll, reg->pin->pin, &dt_pin_ops,
+ dpll_pin_unregister(reg->dev->dpll, reg->pin->pin, reg->ops,
reg);
}
@@ -588,12 +633,14 @@ static void dt_reg_add(struct kunit *test, struct dt_reg *reg,
reg->parent = parent;
reg->state = DPLL_PIN_STATE_SELECTABLE;
reg->direction = DPLL_PIN_DIRECTION_INPUT;
+ if (!reg->ops)
+ reg->ops = &dt_pin_ops;
if (parent)
- ret = dpll_pin_on_pin_register(parent->pin, pin->pin,
- &dt_pin_ops, reg);
+ ret = dpll_pin_on_pin_register(parent->pin, pin->pin, reg->ops,
+ reg);
else
- ret = dpll_pin_register(dev->dpll, pin->pin, &dt_pin_ops, reg);
+ ret = dpll_pin_register(dev->dpll, pin->pin, reg->ops, reg);
KUNIT_ASSERT_EQ(test, ret, 0);
KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test, dt_reg_release,
reg), 0);
@@ -1432,6 +1479,59 @@ static void dt_test_ref_sync_pair(struct kunit *test)
dt_expect_events(test, (void *[]){ ctx->p.pin }, 1);
}
+/* A pin with a measured frequency must be in the domain of its dpll */
+static void dt_test_measured_freq_foreign(struct kunit *test)
+{
+ struct kunit_suppressed_warning *w;
+ struct dt_ctx *ctx = test->priv;
+ struct dt_reg *reg;
+ int ret;
+
+ dt_dev_add(test, &ctx->e0, &ctx->dom_e, DT_CLOCK_ID_E, 0,
+ DPLL_TYPE_PPS);
+ dt_dev_add(test, &ctx->f0, &ctx->dom_f, DT_CLOCK_ID_F, 0,
+ DPLL_TYPE_PPS);
+ dt_fwnode_pin_add(test, &ctx->x, DT_CLOCK_ID_E, DT_PIN_IDX_X);
+
+ reg = &ctx->reg_x_e0;
+ reg->ops = &dt_pin_ops_measured;
+ dt_reg_add(test, reg, &ctx->dom_e, &ctx->x, &ctx->e0, NULL);
+ atomic_set(®->freq_gets, 0);
+ KUNIT_EXPECT_EQ(test, dt_run(test, dt_pin_ntf, &ctx->x), 0);
+ KUNIT_EXPECT_GT(test, atomic_read(®->freq_gets), 0);
+
+ w = kunit_start_suppress_warning(test);
+ ret = dpll_pin_register(ctx->f0.dpll, ctx->x.pin, reg->ops, reg);
+ kunit_end_suppress_warning(test, w);
+
+ KUNIT_EXPECT_EQ(test, ret, -EINVAL);
+ if (!ret)
+ dpll_pin_unregister(ctx->f0.dpll, ctx->x.pin, reg->ops, reg);
+}
+
+/* A pin under a parent pin cannot have a measured frequency */
+static void dt_test_measured_freq_on_pin(struct kunit *test)
+{
+ struct kunit_suppressed_warning *w;
+ struct dt_ctx *ctx = test->priv;
+ struct dt_reg *reg;
+ int ret;
+
+ dt_on_pin_add(test);
+ dt_pin_add(test, &ctx->y, DT_CLOCK_ID_G, DT_PIN_IDX_Y, &dt_prop);
+ reg = &ctx->reg_c_m;
+
+ w = kunit_start_suppress_warning(test);
+ ret = dpll_pin_on_pin_register(ctx->m.pin, ctx->y.pin,
+ &dt_pin_ops_measured, reg);
+ kunit_end_suppress_warning(test, w);
+
+ KUNIT_EXPECT_EQ(test, ret, -EINVAL);
+ if (!ret)
+ dpll_pin_on_pin_unregister(ctx->m.pin, ctx->y.pin,
+ &dt_pin_ops_measured, reg);
+}
+
static struct kunit_case dt_cases[] = {
KUNIT_CASE(dt_test_domain_sharing),
KUNIT_CASE(dt_test_domain_per_module),
@@ -1456,6 +1556,8 @@ static struct kunit_case dt_cases[] = {
KUNIT_CASE(dt_test_set_on_pin),
KUNIT_CASE(dt_test_device_set_ntf),
KUNIT_CASE(dt_test_ref_sync_pair),
+ KUNIT_CASE(dt_test_measured_freq_foreign),
+ KUNIT_CASE(dt_test_measured_freq_on_pin),
{}
};
--
2.56.0
^ permalink raw reply [flat|nested] 27+ messages in thread
* [PATCH net-next 08/10] dpll: zl3073x: update locking comments
2026-11-06 12:09 [PATCH net-next 00/10] dpll: do not serialize driver callbacks under global dpll_lock Ivan Vecera
` (6 preceding siblings ...)
2026-11-06 12:10 ` [PATCH net-next 07/10] dpll: reject measured_freq_get for foreign and on-pin registrations Ivan Vecera
@ 2026-11-06 12:10 ` Ivan Vecera
2026-11-10 12:24 ` Petr Oros
2026-11-06 12:10 ` [PATCH net-next 09/10] ice: dpll: " Ivan Vecera
2026-11-06 12:10 ` [PATCH net-next 10/10] dpll: document locking rules Ivan Vecera
9 siblings, 1 reply; 27+ messages in thread
From: Ivan Vecera @ 2026-11-06 12:10 UTC (permalink / raw)
To: netdev
Cc: Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
Jonathan Corbet, Shuah Khan, Randy Dunlap, Min Li, Tony Nguyen,
Przemek Kitszel, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Richard Cochran, linux-doc,
linux-kernel, intel-wired-lan, Grzegorz Nitka, Karol Kolacinski,
Saeed Mahameed, Tariq Toukan, Petr Oros
Callbacks are serialized by the DPLL core domain lock instead of dpll_lock.
All channels of a chip belong to one domain, so what the comments rely on
still holds.
Signed-off-by: Ivan Vecera <ivecera@redhat.com>
---
drivers/dpll/zl3073x/dpll.c | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/drivers/dpll/zl3073x/dpll.c b/drivers/dpll/zl3073x/dpll.c
index 1cdadb8e46fb2d..6e5e2bb41f55d6 100644
--- a/drivers/dpll/zl3073x/dpll.c
+++ b/drivers/dpll/zl3073x/dpll.c
@@ -349,9 +349,10 @@ zl3073x_dpll_input_pin_ref_sync_set(const struct dpll_pin *dpll_pin,
if (rc)
goto unlock;
- /* All code paths accessing per-channel reference priorities are
- * serialized by the subsystem dpll_lock, so it is safe to release
- * our lock here before iterating over the other channels.
+ /* All callbacks accessing per-channel reference priorities are
+ * serialized by the DPLL core domain lock shared by all channels of
+ * the chip, so it is safe to release our lock here before iterating
+ * over the other channels.
*/
mutex_unlock(&zldpll->lock);
@@ -2401,7 +2402,7 @@ zl3073x_dpll_changes_check(struct zl3073x_dpll *zldpll)
mutex_unlock(&zldpll->lock);
/* Send notifications outside the lock to avoid ABBA deadlock
- * with dpll_lock taken by notification functions.
+ * with the DPLL core domain lock taken by notification functions.
*/
if (dev_changed)
dpll_device_change_ntf(zldpll->dpll_dev);
--
2.56.0
^ permalink raw reply [flat|nested] 27+ messages in thread
* [PATCH net-next 09/10] ice: dpll: update locking comments
2026-11-06 12:09 [PATCH net-next 00/10] dpll: do not serialize driver callbacks under global dpll_lock Ivan Vecera
` (7 preceding siblings ...)
2026-11-06 12:10 ` [PATCH net-next 08/10] dpll: zl3073x: update locking comments Ivan Vecera
@ 2026-11-06 12:10 ` Ivan Vecera
2026-11-10 15:31 ` Petr Oros
2026-11-10 17:53 ` netdev-bot+sashiko
2026-11-06 12:10 ` [PATCH net-next 10/10] dpll: document locking rules Ivan Vecera
9 siblings, 2 replies; 27+ messages in thread
From: Ivan Vecera @ 2026-11-06 12:10 UTC (permalink / raw)
To: netdev
Cc: Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
Jonathan Corbet, Shuah Khan, Randy Dunlap, Min Li, Tony Nguyen,
Przemek Kitszel, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Richard Cochran, linux-doc,
linux-kernel, intel-wired-lan, Grzegorz Nitka, Karol Kolacinski,
Saeed Mahameed, Tariq Toukan, Petr Oros
Callbacks run under the DPLL core domain locks instead of dpll_lock. Note
that callbacks of pins of an external DPLL run under the domain of that
DPLL, so only pf->dplls.lock serializes them with the rest of the driver.
Signed-off-by: Ivan Vecera <ivecera@redhat.com>
---
drivers/net/ethernet/intel/ice/ice_dpll.c | 4 ++--
drivers/net/ethernet/intel/ice/ice_dpll.h | 3 +++
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/drivers/net/ethernet/intel/ice/ice_dpll.c b/drivers/net/ethernet/intel/ice/ice_dpll.c
index d6302409a2dacf..07cbd4cf0160ca 100644
--- a/drivers/net/ethernet/intel/ice/ice_dpll.c
+++ b/drivers/net/ethernet/intel/ice/ice_dpll.c
@@ -1178,8 +1178,8 @@ ice_dpll_input_state_get(const struct dpll_pin *pin, void *pin_priv,
* peer via dpll netlink learn about the update.
*
* Context: Called from dpll_pin_ops callbacks after pf->dplls.lock is
- * released. Uses __dpll_pin_change_ntf() because dpll_lock is
- * still held by the dpll netlink layer.
+ * released. Uses __dpll_pin_change_ntf() because the domain lock
+ * is still held by the dpll core.
*/
static void ice_dpll_sw_pin_notify_peer(struct ice_dplls *d,
struct ice_dpll_pin *changed)
diff --git a/drivers/net/ethernet/intel/ice/ice_dpll.h b/drivers/net/ethernet/intel/ice/ice_dpll.h
index 4ba76e2e2e0b01..39e041a30090d0 100644
--- a/drivers/net/ethernet/intel/ice/ice_dpll.h
+++ b/drivers/net/ethernet/intel/ice/ice_dpll.h
@@ -165,6 +165,9 @@ struct ice_dpll {
* tx_refclks usage map), acquire pf->dplls.lock first, then
* ctrl_pf->dplls.lock. Skip the second acquire when pf == ctrl_pf
* to avoid recursive locking.
+ * - Callbacks of the SYNCE and TIME_REF pins of an external DPLL run under
+ * the DPLL core domain lock of that DPLL, not of the TXC and TSPLL DPLLs,
+ * so only @lock serializes them with the callbacks of these DPLLs.
* - @txclk_notify_rwsem is held for read across
* ice_txclk_update_and_notify(), including the out-of-lock
* dpll_*_change_ntf() calls. ice_dpll_deinit() takes the write side
--
2.56.0
^ permalink raw reply [flat|nested] 27+ messages in thread
* [PATCH net-next 10/10] dpll: document locking rules
2026-11-06 12:09 [PATCH net-next 00/10] dpll: do not serialize driver callbacks under global dpll_lock Ivan Vecera
` (8 preceding siblings ...)
2026-11-06 12:10 ` [PATCH net-next 09/10] ice: dpll: " Ivan Vecera
@ 2026-11-06 12:10 ` Ivan Vecera
2026-11-10 12:25 ` Petr Oros
2026-11-10 17:53 ` netdev-bot+sashiko
9 siblings, 2 replies; 27+ messages in thread
From: Ivan Vecera @ 2026-11-06 12:10 UTC (permalink / raw)
To: netdev
Cc: Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
Jonathan Corbet, Shuah Khan, Randy Dunlap, Min Li, Tony Nguyen,
Przemek Kitszel, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Richard Cochran, linux-doc,
linux-kernel, intel-wired-lan, Grzegorz Nitka, Karol Kolacinski,
Saeed Mahameed, Tariq Toukan, Petr Oros
Describe the lock levels of the subsystem, which domain a callback runs
under and the rules device drivers have to follow.
Signed-off-by: Ivan Vecera <ivecera@redhat.com>
---
Documentation/driver-api/dpll.rst | 44 +++++++++++++++++++++++++++++++
drivers/dpll/dpll_core.c | 9 +++++++
2 files changed, 53 insertions(+)
diff --git a/Documentation/driver-api/dpll.rst b/Documentation/driver-api/dpll.rst
index 7c117ae37cc1bc..40d5037ff3bb0f 100644
--- a/Documentation/driver-api/dpll.rst
+++ b/Documentation/driver-api/dpll.rst
@@ -675,6 +675,50 @@ In the error path we have to rewind every allocation in the reverse order:
More complex example can be found in Intel's ICE driver or nVidia's mlx5 driver.
+Locking
+=======
+
+The subsystem uses two levels of locks, always taken in this order:
+
+- ``dpll_lock`` protects the topology of dpll devices and pins and their
+ lifetime. Functions changing the topology, for example dpll_pin_register(),
+ take it for write and send the create and delete notifications under it.
+ Netlink requests, each part of a dump and change notifications take it for
+ read for their whole duration.
+- A domain lock serializes driver callbacks. All dpll devices and pins created
+ with the same module and ``clock_id`` belong to one domain, in practice to
+ one chip or card.
+
+A netlink request addressing a device or a pin by its ID holds the domain lock
+of that object from start to end. A notification holds it while its message is
+built and sent, so messages of one object are sent in the order they were
+built. Only one domain lock is held at a time, so domains have no lock order,
+and callbacks of different domains can run concurrently.
+
+Callbacks of a dpll device run under the domain of the device. Callbacks of a
+pin run under the domain of the pin, whichever driver registered them:
+
+- A driver registering a pin of another driver with its dpll device has these
+ callbacks serialized with the driver that created the pin, not with the other
+ callbacks of its device, so it has to protect the state they share by itself.
+ A callback stuck in such a registration stops the domain of the pin.
+- The parent pin of a pin registered with dpll_pin_on_pin_register() and its
+ dpll devices can belong to another driver, so the callback must not
+ dereference their private data.
+- The frequency monitor belongs to the dpll device, so only callbacks of a pin
+ of the same domain registered with dpll_pin_register() can include
+ ``.measured_freq_get``.
+
+Device drivers have to follow these rules:
+
+- A callback must not call dpll_device_change_ntf(), dpll_pin_change_ntf() or
+ any function changing the topology.
+- A callback notifies about objects of the domain it runs under with
+ __dpll_device_change_ntf() or __dpll_pin_change_ntf(). Other objects have to
+ be notified from a context that holds no domain lock, for example a work item.
+- A driver must not call dpll_device_change_ntf(), dpll_pin_change_ntf() or any
+ function changing the topology while holding a lock that its callbacks take.
+
SyncE enablement
================
For SyncE enablement it is required to allow control over dpll device
diff --git a/drivers/dpll/dpll_core.c b/drivers/dpll/dpll_core.c
index 84377f8ba16036..6ad40fb9eee855 100644
--- a/drivers/dpll/dpll_core.c
+++ b/drivers/dpll/dpll_core.c
@@ -932,6 +932,10 @@ __dpll_pin_register(struct dpll_device *dpll, struct dpll_pin *pin,
* @ops: ops for a dpll pin ops
* @priv: pointer to private information of owner
*
+ * The callbacks run under the domain of @pin. If @pin belongs to another domain
+ * than @dpll, they are not serialized with the callbacks of @dpll and @ops must
+ * not include .measured_freq_get.
+ *
* Context: Acquires dpll_lock and domain locks
* Return:
* * 0 on success
@@ -1049,6 +1053,11 @@ EXPORT_SYMBOL_GPL(dpll_pin_unregister);
* Register a pin with a parent pin, create references between them and
* between newly registered pin and dplls connected with a parent pin.
*
+ * The callbacks run under the domain of @pin. The parent pin and its dplls can
+ * belong to another driver, so @ops must not dereference their private data.
+ * The frequency monitor belongs to a dpll, so @ops must not include
+ * .measured_freq_get.
+ *
* Context: Acquires dpll_lock and domain locks
* Return:
* * 0 on success
--
2.56.0
^ permalink raw reply [flat|nested] 27+ messages in thread
* Re: [PATCH net-next 01/10] ice: dpll: notify TSPLL lock status from the periodic work only
2026-11-06 12:09 ` [PATCH net-next 01/10] ice: dpll: notify TSPLL lock status from the periodic work only Ivan Vecera
@ 2026-11-10 12:20 ` Petr Oros
2026-11-10 17:53 ` netdev-bot+sashiko
1 sibling, 0 replies; 27+ messages in thread
From: Petr Oros @ 2026-11-10 12:20 UTC (permalink / raw)
To: Ivan Vecera, netdev
Cc: Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
Jonathan Corbet, Shuah Khan, Randy Dunlap, Min Li, Tony Nguyen,
Przemek Kitszel, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Richard Cochran, linux-doc,
linux-kernel, intel-wired-lan, Grzegorz Nitka, Karol Kolacinski,
Saeed Mahameed, Tariq Toukan
On 11/6/26 1:09 PM, Ivan Vecera wrote:
> ice_dpll_tspll_state_on_dpll_set() notifies the TSPLL device from a
> callback of the TIME_REF pin. This pin belongs to an external DPLL and the
> DPLL core is going to run its callbacks under a lock of the pin's owner,
> which does not cover the TSPLL device.
>
> Only update dpll_state in the callback, so a device get reports the new
> state right away, and leave prev_dpll_state to
> ice_dpll_periodic_work_e825(), which then sends the notification. It now
> arrives up to 500 ms later and after the reply to the request.
>
> Refresh dpll_state on every run of the work, as the state written by the
> callback can be transient and the lock status can return to
> prev_dpll_state before the next run. No notification is sent in this case.
>
> Signed-off-by: Ivan Vecera <ivecera@redhat.com>
> ---
> drivers/net/ethernet/intel/ice/ice_dpll.c | 14 +++++---------
> 1 file changed, 5 insertions(+), 9 deletions(-)
>
> diff --git a/drivers/net/ethernet/intel/ice/ice_dpll.c b/drivers/net/ethernet/intel/ice/ice_dpll.c
> index ade1fb3dd06743..bdbcf7af296eb4 100644
> --- a/drivers/net/ethernet/intel/ice/ice_dpll.c
> +++ b/drivers/net/ethernet/intel/ice/ice_dpll.c
> @@ -2914,7 +2914,6 @@ ice_dpll_tspll_state_on_dpll_set(const struct dpll_pin *pin, void *pin_priv,
> struct ice_dpll_pin *p = pin_priv;
> struct ice_pf *pf = p->pf;
> enum ice_clk_src clk_src;
> - bool changed = false;
> struct ice_dpll *tp;
> int ret;
>
> @@ -2947,15 +2946,12 @@ ice_dpll_tspll_state_on_dpll_set(const struct dpll_pin *pin, void *pin_priv,
> WRITE_ONCE(pf->ptp.tspll_locked,
> new_state == DPLL_LOCK_STATUS_LOCKED);
>
> - if (tp->prev_dpll_state != new_state) {
> - tp->dpll_state = new_state;
> - tp->prev_dpll_state = new_state;
> - changed = true;
> - }
> + /* ice_dpll_periodic_work_e825() compares the state with
> + * prev_dpll_state and sends the notification.
> + */
> + tp->dpll_state = new_state;
> }
> mutex_unlock(&pf->dplls.lock);
> - if (changed)
> - __dpll_device_change_ntf(tp->dpll);
> /* TSPLL reconfiguration may complete before lock is reacquired.
> * PTP periodic monitoring tracks and restores the final lock state.
> */
> @@ -3372,8 +3368,8 @@ static void ice_dpll_periodic_work_e825(struct ice_pf *pf)
> mutex_lock(&pf->dplls.lock);
> new_state = ice_dpll_tspll_lock_status_get(pf, true);
>
> + tp->dpll_state = new_state;
> if (tp->prev_dpll_state != new_state) {
> - tp->dpll_state = new_state;
> tp->prev_dpll_state = new_state;
> changed = true;
> }
Reviewed-by: Petr Oros <poros@redhat.com>
^ permalink raw reply [flat|nested] 27+ messages in thread
* Re: [PATCH net-next 03/10] dpll: add domain object
2026-11-06 12:09 ` [PATCH net-next 03/10] dpll: add domain object Ivan Vecera
@ 2026-11-10 12:23 ` Petr Oros
2026-11-10 17:53 ` netdev-bot+sashiko
1 sibling, 0 replies; 27+ messages in thread
From: Petr Oros @ 2026-11-10 12:23 UTC (permalink / raw)
To: Ivan Vecera, netdev
Cc: Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
Jonathan Corbet, Shuah Khan, Randy Dunlap, Min Li, Tony Nguyen,
Przemek Kitszel, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Richard Cochran, linux-doc,
linux-kernel, intel-wired-lan, Grzegorz Nitka, Karol Kolacinski,
Saeed Mahameed, Tariq Toukan
On 11/6/26 1:09 PM, Ivan Vecera wrote:
> All dpll device and pin callbacks are serialized by the global dpll_lock,
> so a slow device stalls all others, whichever driver they belong to.
>
> Add struct dpll_domain as a preparation for serializing callbacks per
> driver instance. A domain groups all devices and pins created with the
> same (module, clock_id) pair, which is in practice one chip or card. The
> same pair already identifies the owner of a pin in
> dpll_pin_own_dpll_ref_first().
>
> Domains are kept in an xarray, like devices and pins, and they live only as
> long as their objects.
>
> No functional change.
>
> Signed-off-by: Ivan Vecera <ivecera@redhat.com>
> ---
> drivers/dpll/dpll_core.c | 61 ++++++++++++++++++++++++++++++++++++++++
> drivers/dpll/dpll_core.h | 27 ++++++++++++++++++
> 2 files changed, 88 insertions(+)
>
> diff --git a/drivers/dpll/dpll_core.c b/drivers/dpll/dpll_core.c
> index 8389983f425695..d39c4905172f2d 100644
> --- a/drivers/dpll/dpll_core.c
> +++ b/drivers/dpll/dpll_core.c
> @@ -24,6 +24,7 @@ DEFINE_MUTEX(dpll_lock);
>
> DEFINE_XARRAY_FLAGS(dpll_device_xa, XA_FLAGS_ALLOC);
> DEFINE_XARRAY_FLAGS(dpll_pin_xa, XA_FLAGS_ALLOC);
> +static DEFINE_XARRAY_FLAGS(dpll_domain_xa, XA_FLAGS_ALLOC);
>
> static RAW_NOTIFIER_HEAD(dpll_notifier_chain);
> static DEFINE_IDA(dpll_pin_idx_ida);
> @@ -88,6 +89,50 @@ void dpll_pin_notify(struct dpll_pin *pin, u64 src_clock_id,
> call_dpll_notifiers(action, &info);
> }
>
> +static struct dpll_domain *dpll_domain_get(u64 clock_id, struct module *module)
> +{
> + struct dpll_domain *domain;
> + unsigned long i;
> + int ret;
> +
> + lockdep_assert_held(&dpll_lock);
> +
> + xa_for_each(&dpll_domain_xa, i, domain) {
> + if (domain->clock_id == clock_id && domain->module == module) {
> + refcount_inc(&domain->refcount);
> + return domain;
> + }
> + }
> +
> + domain = kzalloc_obj(*domain);
> + if (!domain)
> + return ERR_PTR(-ENOMEM);
> + domain->clock_id = clock_id;
> + domain->module = module;
> + refcount_set(&domain->refcount, 1);
> + mutex_init(&domain->lock);
> + ret = xa_alloc(&dpll_domain_xa, &domain->id, domain, xa_limit_32b,
> + GFP_KERNEL);
> + if (ret) {
> + mutex_destroy(&domain->lock);
> + kfree(domain);
> + return ERR_PTR(ret);
> + }
> +
> + return domain;
> +}
> +
> +static void dpll_domain_put(struct dpll_domain *domain)
> +{
> + lockdep_assert_held(&dpll_lock);
> +
> + if (refcount_dec_and_test(&domain->refcount)) {
> + xa_erase(&dpll_domain_xa, domain->id);
> + mutex_destroy(&domain->lock);
> + kfree(domain);
> + }
> +}
> +
> static void dpll_device_tracker_alloc(struct dpll_device *dpll,
> dpll_tracker *tracker)
> {
> @@ -120,6 +165,7 @@ static void __dpll_device_put(struct dpll_device *dpll, dpll_tracker *tracker)
> xa_erase(&dpll_device_xa, dpll->id);
> WARN_ON(!list_empty(&dpll->registration_list));
> ref_tracker_dir_exit(&dpll->refcnt_tracker);
> + dpll_domain_put(dpll->domain);
> kfree(dpll);
> }
> }
> @@ -159,6 +205,7 @@ static void __dpll_pin_put(struct dpll_pin *pin, dpll_tracker *tracker)
> fwnode_handle_put(pin->fwnode);
> dpll_pin_idx_free(pin->pin_idx);
> ref_tracker_dir_exit(&pin->refcnt_tracker);
> + dpll_domain_put(pin->domain);
> kfree_rcu(pin, rcu);
> }
> }
> @@ -366,6 +413,12 @@ dpll_device_alloc(const u64 clock_id, u32 device_idx, struct module *module)
> dpll = kzalloc_obj(*dpll);
> if (!dpll)
> return ERR_PTR(-ENOMEM);
> + dpll->domain = dpll_domain_get(clock_id, module);
> + if (IS_ERR(dpll->domain)) {
> + ret = PTR_ERR(dpll->domain);
> + kfree(dpll);
> + return ERR_PTR(ret);
> + }
> refcount_set(&dpll->refcount, 1);
> INIT_LIST_HEAD(&dpll->registration_list);
> dpll->device_idx = device_idx;
> @@ -374,6 +427,7 @@ dpll_device_alloc(const u64 clock_id, u32 device_idx, struct module *module)
> ret = xa_alloc_cyclic(&dpll_device_xa, &dpll->id, dpll, xa_limit_32b,
> &dpll_device_xa_id, GFP_KERNEL);
> if (ret < 0) {
> + dpll_domain_put(dpll->domain);
> kfree(dpll);
> return ERR_PTR(ret);
> }
> @@ -664,6 +718,11 @@ dpll_pin_alloc(u64 clock_id, u32 pin_idx, struct module *module,
> ret = dpll_pin_prop_dup(prop, &pin->prop);
> if (ret)
> goto err_pin_prop;
> + pin->domain = dpll_domain_get(clock_id, module);
> + if (IS_ERR(pin->domain)) {
> + ret = PTR_ERR(pin->domain);
> + goto err_domain;
> + }
> refcount_set(&pin->refcount, 1);
> xa_init_flags(&pin->dpll_refs, XA_FLAGS_ALLOC);
> xa_init_flags(&pin->parent_refs, XA_FLAGS_ALLOC);
> @@ -678,6 +737,8 @@ dpll_pin_alloc(u64 clock_id, u32 pin_idx, struct module *module,
> xa_destroy(&pin->dpll_refs);
> xa_destroy(&pin->parent_refs);
> xa_destroy(&pin->ref_sync_pins);
> + dpll_domain_put(pin->domain);
> +err_domain:
> dpll_pin_prop_free(&pin->prop);
> err_pin_prop:
> kfree(pin);
> diff --git a/drivers/dpll/dpll_core.h b/drivers/dpll/dpll_core.h
> index da8a369556ed11..6705e81bc11de9 100644
> --- a/drivers/dpll/dpll_core.h
> +++ b/drivers/dpll/dpll_core.h
> @@ -9,18 +9,42 @@
>
> #include <linux/dpll.h>
> #include <linux/list.h>
> +#include <linux/mutex.h>
> #include <linux/refcount.h>
> #include <linux/ref_tracker.h>
> #include "dpll_nl.h"
>
> #define DPLL_REGISTERED XA_MARK_1
>
> +/**
> + * struct dpll_domain - group of dpll objects sharing a lock
> + * @id: index of the domain in dpll_domain_xa
> + * @clock_id: clock_id of the objects in the domain
> + * @module: module of the objects in the domain
> + * @refcount: number of dpll devices and pins in the domain
> + * @lock: lock of the domain
> + *
> + * All dpll devices and pins created with the same (module, clock_id) pair
> + * belong to one domain, in practice to one chip or card.
> + **/
> +struct dpll_domain {
> + u32 id;
> + u64 clock_id;
> + struct module *module;
> + refcount_t refcount;
> + /* Serializes the driver callbacks of all DPLL devices and pins
> + * that share one (module, clock_id) pair.
> + */
> + struct mutex lock;
> +};
> +
> /**
> * struct dpll_device - stores DPLL device internal data
> * @id: unique id number for device given by dpll subsystem
> * @device_idx: id given by dev driver
> * @clock_id: unique identifier (clock_id) of a dpll
> * @module: module of creator
> + * @domain: domain the device belongs to
> * @type: type of a dpll
> * @pin_refs: stores pins registered within a dpll
> * @refcount: refcount
> @@ -32,6 +56,7 @@ struct dpll_device {
> u32 device_idx;
> u64 clock_id;
> struct module *module;
> + struct dpll_domain *domain;
> enum dpll_type type;
> struct xarray pin_refs;
> refcount_t refcount;
> @@ -46,6 +71,7 @@ struct dpll_device {
> * @clock_id: clock_id of creator
> * @module: module of creator
> * @module_name: module name of creator
> + * @domain: domain the pin belongs to
> * @fwnode: optional reference to firmware node
> * @dpll_refs: hold referencees to dplls pin was registered with
> * @parent_refs: hold references to parent pins pin was registered with
> @@ -61,6 +87,7 @@ struct dpll_pin {
> u64 clock_id;
> struct module *module;
> char module_name[MODULE_NAME_LEN];
> + struct dpll_domain *domain;
> struct fwnode_handle *fwnode;
> struct xarray dpll_refs;
> struct xarray parent_refs;
Reviewed-by: Petr Oros <poros@redhat.com>
^ permalink raw reply [flat|nested] 27+ messages in thread
* Re: [PATCH net-next 04/10] dpll: take domain locks around driver callbacks
2026-11-06 12:09 ` [PATCH net-next 04/10] dpll: take domain locks around driver callbacks Ivan Vecera
@ 2026-11-10 12:23 ` Petr Oros
0 siblings, 0 replies; 27+ messages in thread
From: Petr Oros @ 2026-11-10 12:23 UTC (permalink / raw)
To: Ivan Vecera, netdev
Cc: Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
Jonathan Corbet, Shuah Khan, Randy Dunlap, Min Li, Tony Nguyen,
Przemek Kitszel, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Richard Cochran, linux-doc,
linux-kernel, intel-wired-lan, Grzegorz Nitka, Karol Kolacinski,
Saeed Mahameed, Tariq Toukan
On 11/6/26 1:09 PM, Ivan Vecera wrote:
> Hold the domain lock of the addressed object wherever driver callbacks can
> run:
>
> - netlink requests addressing a device or a pin, from pre_doit to post_doit
> - dumps, for each object
> - notifications, while the message is being built and sent
>
> Callbacks of a device run under the domain of the device. Callbacks of a
> pin run under the domain of the pin, also when another driver registered
> the pin with its dpll or under its parent pin. All callbacks a message
> about a pin needs are thus covered by one lock, and only one domain lock
> is ever held. Domains have no lock order, so registrations can connect
> them in any way.
>
> A request is atomic against other requests addressing objects of that
> domain, and messages of one object are sent in the order they were built.
>
> Requests resolving an id run no callbacks, so they do not take the lock.
>
> __dpll_{device,pin}_change_ntf() now also require the domain lock of the
> object. A callback can therefore only notify objects of the domain it runs
> under.
>
> Callbacks stay serialized by dpll_lock for now.
>
> Signed-off-by: Ivan Vecera <ivecera@redhat.com>
> ---
> drivers/dpll/dpll_core.c | 18 +++++----
> drivers/dpll/dpll_core.h | 6 ++-
> drivers/dpll/dpll_netlink.c | 78 ++++++++++++++++++++++++++++---------
> 3 files changed, 76 insertions(+), 26 deletions(-)
>
> diff --git a/drivers/dpll/dpll_core.c b/drivers/dpll/dpll_core.c
> index d39c4905172f2d..dd09f528170994 100644
> --- a/drivers/dpll/dpll_core.c
> +++ b/drivers/dpll/dpll_core.c
> @@ -520,7 +520,7 @@ dpll_device_registration_find(struct dpll_device *dpll,
> *
> * Make dpll device available for user space.
> *
> - * Context: Acquires a lock (dpll_lock)
> + * Context: Acquires dpll_lock and the domain lock of @dpll
> * Return:
> * * 0 on success
> * * negative - error value
> @@ -579,7 +579,7 @@ EXPORT_SYMBOL_GPL(dpll_device_register);
> *
> * Unregister device, make it unavailable for userspace.
> * Note: It does not free the memory
> - * Context: Acquires a lock (dpll_lock)
> + * Context: Acquires dpll_lock and the domain lock of @dpll
> */
> void dpll_device_unregister(struct dpll_device *dpll,
> const struct dpll_device_ops *ops, void *priv)
> @@ -928,7 +928,7 @@ __dpll_pin_register(struct dpll_device *dpll, struct dpll_pin *pin,
> * @ops: ops for a dpll pin ops
> * @priv: pointer to private information of owner
> *
> - * Context: Acquires a lock (dpll_lock)
> + * Context: Acquires dpll_lock and domain locks
> * Return:
> * * 0 on success
> * * negative - error value
> @@ -989,7 +989,9 @@ static void dpll_pin_ref_sync_pair_del(u32 ref_sync_pin_id)
> ref_sync_pin = xa_load(&pin->ref_sync_pins, ref_sync_pin_id);
> if (ref_sync_pin) {
> xa_erase(&pin->ref_sync_pins, ref_sync_pin_id);
> + mutex_lock(&pin->domain->lock);
> __dpll_pin_change_ntf(pin);
> + mutex_unlock(&pin->domain->lock);
> }
> }
> }
> @@ -1016,7 +1018,7 @@ __dpll_pin_unregister(struct dpll_device *dpll, struct dpll_pin *pin,
> * @priv: pointer to private information of owner
> *
> * Note: It does not free the memory
> - * Context: Acquires a lock (dpll_lock)
> + * Context: Acquires dpll_lock and domain locks
> */
> void dpll_pin_unregister(struct dpll_device *dpll, struct dpll_pin *pin,
> const struct dpll_pin_ops *ops, void *priv)
> @@ -1042,7 +1044,7 @@ EXPORT_SYMBOL_GPL(dpll_pin_unregister);
> * Register a pin with a parent pin, create references between them and
> * between newly registered pin and dplls connected with a parent pin.
> *
> - * Context: Acquires a lock (dpll_lock)
> + * Context: Acquires dpll_lock and domain locks
> * Return:
> * * 0 on success
> * * negative - error value
> @@ -1099,7 +1101,7 @@ EXPORT_SYMBOL_GPL(dpll_pin_on_pin_register);
> * @ops: ops for a dpll pin
> * @priv: pointer to private information of owner
> *
> - * Context: Acquires a lock (dpll_lock)
> + * Context: Acquires dpll_lock and domain locks
> * Note: It does not free the memory
> */
> void dpll_pin_on_pin_unregister(struct dpll_pin *parent, struct dpll_pin *pin,
> @@ -1129,7 +1131,7 @@ EXPORT_SYMBOL_GPL(dpll_pin_on_pin_unregister);
> *
> * Once pins are paired, the user-space configuration of reference sync pair
> * is possible.
> - * Context: Acquires a lock (dpll_lock)
> + * Context: Acquires dpll_lock and domain locks
> * Return:
> * * 0 on success
> * * negative - error value
> @@ -1142,7 +1144,9 @@ int dpll_pin_ref_sync_pair_add(struct dpll_pin *pin,
> mutex_lock(&dpll_lock);
> ret = xa_insert(&pin->ref_sync_pins, ref_sync_pin->id,
> ref_sync_pin, GFP_KERNEL);
> + mutex_lock(&pin->domain->lock);
> __dpll_pin_change_ntf(pin);
> + mutex_unlock(&pin->domain->lock);
> mutex_unlock(&dpll_lock);
>
> return ret;
> diff --git a/drivers/dpll/dpll_core.h b/drivers/dpll/dpll_core.h
> index 6705e81bc11de9..630c5a1ca572c2 100644
> --- a/drivers/dpll/dpll_core.h
> +++ b/drivers/dpll/dpll_core.h
> @@ -22,10 +22,14 @@
> * @clock_id: clock_id of the objects in the domain
> * @module: module of the objects in the domain
> * @refcount: number of dpll devices and pins in the domain
> - * @lock: lock of the domain
> + * @lock: serializes driver callbacks of the domain
> *
> * All dpll devices and pins created with the same (module, clock_id) pair
> * belong to one domain, in practice to one chip or card.
> + *
> + * Callbacks of a device run under the domain of the device and callbacks of a
> + * pin under the domain of the pin, whichever driver registered them. Only one
> + * domain lock is held at a time, so domains have no lock order.
> **/
> struct dpll_domain {
> u32 id;
> diff --git a/drivers/dpll/dpll_netlink.c b/drivers/dpll/dpll_netlink.c
> index e5380d95f238b3..1b4e3a0166a744 100644
> --- a/drivers/dpll/dpll_netlink.c
> +++ b/drivers/dpll/dpll_netlink.c
> @@ -861,27 +861,39 @@ dpll_device_event_send(enum dpll_cmd event, struct dpll_device *dpll)
>
> int dpll_device_create_ntf(struct dpll_device *dpll)
> {
> + int ret;
> +
> + mutex_lock(&dpll->domain->lock);
> dpll_device_notify(dpll, DPLL_DEVICE_CREATED);
> - return dpll_device_event_send(DPLL_CMD_DEVICE_CREATE_NTF, dpll);
> + ret = dpll_device_event_send(DPLL_CMD_DEVICE_CREATE_NTF, dpll);
> + mutex_unlock(&dpll->domain->lock);
> +
> + return ret;
> }
>
> int dpll_device_delete_ntf(struct dpll_device *dpll)
> {
> + int ret;
> +
> + mutex_lock(&dpll->domain->lock);
> dpll_device_notify(dpll, DPLL_DEVICE_DELETED);
> - return dpll_device_event_send(DPLL_CMD_DEVICE_DELETE_NTF, dpll);
> + ret = dpll_device_event_send(DPLL_CMD_DEVICE_DELETE_NTF, dpll);
> + mutex_unlock(&dpll->domain->lock);
> +
> + return ret;
> }
>
> /**
> * __dpll_device_change_ntf - notify that the dpll device has been changed
> * @dpll: registered dpll pointer
> *
> - * Context: caller must hold dpll_lock. Suitable for use inside device
> - * callbacks which are already invoked under dpll_lock.
> + * Context: caller must hold dpll_lock and the domain lock of @dpll.
> + * Suitable for use inside callbacks running under these locks.
> * Return: 0 if succeeds, error code otherwise.
> */
> int __dpll_device_change_ntf(struct dpll_device *dpll)
> {
> - lockdep_assert_held(&dpll_lock);
> + lockdep_assert_held(&dpll->domain->lock);
> dpll_device_notify(dpll, DPLL_DEVICE_CHANGED);
> return dpll_device_event_send(DPLL_CMD_DEVICE_CHANGE_NTF, dpll);
> }
> @@ -891,7 +903,7 @@ EXPORT_SYMBOL_GPL(__dpll_device_change_ntf);
> * dpll_device_change_ntf - notify that the dpll device has been changed
> * @dpll: registered dpll pointer
> *
> - * Context: acquires and holds a dpll_lock.
> + * Context: acquires and holds dpll_lock and the domain lock of @dpll.
> * Return: 0 if succeeds, error code otherwise.
> */
> int dpll_device_change_ntf(struct dpll_device *dpll)
> @@ -899,7 +911,9 @@ int dpll_device_change_ntf(struct dpll_device *dpll)
> int ret;
>
> mutex_lock(&dpll_lock);
> + mutex_lock(&dpll->domain->lock);
> ret = __dpll_device_change_ntf(dpll);
> + mutex_unlock(&dpll->domain->lock);
> mutex_unlock(&dpll_lock);
>
> return ret;
> @@ -941,27 +955,39 @@ dpll_pin_event_send(enum dpll_cmd event, struct dpll_pin *pin)
>
> int dpll_pin_create_ntf(struct dpll_pin *pin, u64 src_clock_id)
> {
> + int ret;
> +
> + mutex_lock(&pin->domain->lock);
> dpll_pin_notify(pin, src_clock_id, DPLL_PIN_CREATED);
> - return dpll_pin_event_send(DPLL_CMD_PIN_CREATE_NTF, pin);
> + ret = dpll_pin_event_send(DPLL_CMD_PIN_CREATE_NTF, pin);
> + mutex_unlock(&pin->domain->lock);
> +
> + return ret;
> }
>
> int dpll_pin_delete_ntf(struct dpll_pin *pin, u64 src_clock_id)
> {
> + int ret;
> +
> + mutex_lock(&pin->domain->lock);
> dpll_pin_notify(pin, src_clock_id, DPLL_PIN_DELETED);
> - return dpll_pin_event_send(DPLL_CMD_PIN_DELETE_NTF, pin);
> + ret = dpll_pin_event_send(DPLL_CMD_PIN_DELETE_NTF, pin);
> + mutex_unlock(&pin->domain->lock);
> +
> + return ret;
> }
>
> /**
> * __dpll_pin_change_ntf - notify that the pin has been changed
> * @pin: registered pin pointer
> *
> - * Context: caller must hold dpll_lock. Suitable for use inside pin
> - * callbacks which are already invoked under dpll_lock.
> + * Context: caller must hold dpll_lock and the domain lock of @pin.
> + * Suitable for use inside callbacks running under these locks.
> * Return: 0 if succeeds, error code otherwise.
> */
> int __dpll_pin_change_ntf(struct dpll_pin *pin)
> {
> - lockdep_assert_held(&dpll_lock);
> + lockdep_assert_held(&pin->domain->lock);
> dpll_pin_notify(pin, pin->clock_id, DPLL_PIN_CHANGED);
> return dpll_pin_event_send(DPLL_CMD_PIN_CHANGE_NTF, pin);
> }
> @@ -971,7 +997,7 @@ EXPORT_SYMBOL_GPL(__dpll_pin_change_ntf);
> * dpll_pin_change_ntf - notify that the pin has been changed
> * @pin: registered pin pointer
> *
> - * Context: acquires and holds a dpll_lock.
> + * Context: acquires and holds dpll_lock and the domain lock of @pin.
> * Return: 0 if succeeds, error code otherwise.
> */
> int dpll_pin_change_ntf(struct dpll_pin *pin)
> @@ -979,7 +1005,9 @@ int dpll_pin_change_ntf(struct dpll_pin *pin)
> int ret;
>
> mutex_lock(&dpll_lock);
> + mutex_lock(&pin->domain->lock);
> ret = __dpll_pin_change_ntf(pin);
> + mutex_unlock(&pin->domain->lock);
> mutex_unlock(&dpll_lock);
>
> return ret;
> @@ -1780,7 +1808,9 @@ int dpll_nl_pin_get_dumpit(struct sk_buff *skb, struct netlink_callback *cb)
> ret = -EMSGSIZE;
> break;
> }
> + mutex_lock(&pin->domain->lock);
> ret = dpll_cmd_pin_get_one(skb, pin, cb->extack);
> + mutex_unlock(&pin->domain->lock);
> if (ret) {
> genlmsg_cancel(skb, hdr);
> if (ret == -ENODEV) {
> @@ -1999,7 +2029,9 @@ int dpll_nl_device_get_dumpit(struct sk_buff *skb, struct netlink_callback *cb)
> ret = -EMSGSIZE;
> break;
> }
> + mutex_lock(&dpll->domain->lock);
> ret = dpll_device_get_one(dpll, skb, cb->extack);
> + mutex_unlock(&dpll->domain->lock);
> if (ret) {
> genlmsg_cancel(skb, hdr);
> break;
> @@ -2018,6 +2050,7 @@ int dpll_nl_device_get_dumpit(struct sk_buff *skb, struct netlink_callback *cb)
> int dpll_pre_doit(const struct genl_split_ops *ops, struct sk_buff *skb,
> struct genl_info *info)
> {
> + struct dpll_device *dpll;
> u32 id;
>
> if (GENL_REQ_ATTR_CHECK(info, DPLL_A_ID))
> @@ -2025,11 +2058,13 @@ int dpll_pre_doit(const struct genl_split_ops *ops, struct sk_buff *skb,
>
> mutex_lock(&dpll_lock);
> id = nla_get_u32(info->attrs[DPLL_A_ID]);
> - info->user_ptr[0] = dpll_device_get_by_id(id);
> - if (!info->user_ptr[0]) {
> + dpll = dpll_device_get_by_id(id);
> + if (!dpll) {
> NL_SET_ERR_MSG(info->extack, "device not found");
> goto unlock;
> }
> + info->user_ptr[0] = dpll;
> + mutex_lock(&dpll->domain->lock);
> return 0;
> unlock:
> mutex_unlock(&dpll_lock);
> @@ -2039,6 +2074,9 @@ int dpll_pre_doit(const struct genl_split_ops *ops, struct sk_buff *skb,
> void dpll_post_doit(const struct genl_split_ops *ops, struct sk_buff *skb,
> struct genl_info *info)
> {
> + struct dpll_device *dpll = info->user_ptr[0];
> +
> + mutex_unlock(&dpll->domain->lock);
> mutex_unlock(&dpll_lock);
> }
>
> @@ -2061,6 +2099,7 @@ dpll_unlock_doit(const struct genl_split_ops *ops, struct sk_buff *skb,
> int dpll_pin_pre_doit(const struct genl_split_ops *ops, struct sk_buff *skb,
> struct genl_info *info)
> {
> + struct dpll_pin *pin;
> int ret;
>
> mutex_lock(&dpll_lock);
> @@ -2068,14 +2107,14 @@ int dpll_pin_pre_doit(const struct genl_split_ops *ops, struct sk_buff *skb,
> ret = -EINVAL;
> goto unlock_dev;
> }
> - info->user_ptr[0] = xa_load(&dpll_pin_xa,
> - nla_get_u32(info->attrs[DPLL_A_PIN_ID]));
> - if (!info->user_ptr[0] ||
> - !dpll_pin_available(info->user_ptr[0])) {
> + pin = xa_load(&dpll_pin_xa, nla_get_u32(info->attrs[DPLL_A_PIN_ID]));
> + if (!pin || !dpll_pin_available(pin)) {
> NL_SET_ERR_MSG(info->extack, "pin not found");
> ret = -ENODEV;
> goto unlock_dev;
> }
> + info->user_ptr[0] = pin;
> + mutex_lock(&pin->domain->lock);
>
> return 0;
>
> @@ -2087,5 +2126,8 @@ int dpll_pin_pre_doit(const struct genl_split_ops *ops, struct sk_buff *skb,
> void dpll_pin_post_doit(const struct genl_split_ops *ops, struct sk_buff *skb,
> struct genl_info *info)
> {
> + struct dpll_pin *pin = info->user_ptr[0];
> +
> + mutex_unlock(&pin->domain->lock);
> mutex_unlock(&dpll_lock);
> }
Reviewed-by: Petr Oros <poros@redhat.com>
^ permalink raw reply [flat|nested] 27+ messages in thread
* Re: [PATCH net-next 05/10] dpll: convert dpll_lock to rwsem
2026-11-06 12:10 ` [PATCH net-next 05/10] dpll: convert dpll_lock to rwsem Ivan Vecera
@ 2026-11-10 12:24 ` Petr Oros
2026-11-10 17:53 ` netdev-bot+sashiko
1 sibling, 0 replies; 27+ messages in thread
From: Petr Oros @ 2026-11-10 12:24 UTC (permalink / raw)
To: Ivan Vecera, netdev
Cc: Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
Jonathan Corbet, Shuah Khan, Randy Dunlap, Min Li, Tony Nguyen,
Przemek Kitszel, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Richard Cochran, linux-doc,
linux-kernel, intel-wired-lan, Grzegorz Nitka, Karol Kolacinski,
Saeed Mahameed, Tariq Toukan
On 11/6/26 1:10 PM, Ivan Vecera wrote:
> Driver callbacks are now serialized by domain locks, so dpll_lock only has
> to protect the topology and lifetime of devices and pins.
>
> Convert it to an rwsem. Core functions changing the topology take it for
> write. Netlink requests, each part of a dump and change notifications take
> it for read, for their whole duration, so objects found by a lookup stay
> valid. This is the patch that lets callbacks of different domains run
> concurrently.
>
> A callback running for long still blocks topology changes, and a waiting
> writer blocks new readers, so a stuck callback stalls the subsystem while
> a device is being probed or removed. This is no worse than the global
> serialization it replaces.
>
> Signed-off-by: Ivan Vecera <ivecera@redhat.com>
> ---
> drivers/dpll/dpll_core.c | 93 +++++++++++++++++--------------------
> drivers/dpll/dpll_core.h | 3 +-
> drivers/dpll/dpll_netlink.c | 32 ++++++-------
> 3 files changed, 61 insertions(+), 67 deletions(-)
>
> diff --git a/drivers/dpll/dpll_core.c b/drivers/dpll/dpll_core.c
> index dd09f528170994..8eb74682343156 100644
> --- a/drivers/dpll/dpll_core.c
> +++ b/drivers/dpll/dpll_core.c
> @@ -19,8 +19,12 @@
> #include "dpll_core.h"
> #include "dpll_netlink.h"
>
> -/* Mutex lock to protect DPLL subsystem devices and pins */
> -DEFINE_MUTEX(dpll_lock);
> +/* Protects the topology and lifetime of DPLL devices and pins. Taken for write
> + * to change them and for read for the whole netlink request, part of a dump or
> + * change notification that uses them. Driver callbacks are serialized by domain
> + * locks, which are only taken under this lock.
> + */
> +DECLARE_RWSEM(dpll_lock);
>
> DEFINE_XARRAY_FLAGS(dpll_device_xa, XA_FLAGS_ALLOC);
> DEFINE_XARRAY_FLAGS(dpll_pin_xa, XA_FLAGS_ALLOC);
> @@ -459,7 +463,7 @@ dpll_device_get(u64 clock_id, u32 device_idx, struct module *module,
> struct dpll_device *dpll, *ret = NULL;
> unsigned long index;
>
> - mutex_lock(&dpll_lock);
> + down_write(&dpll_lock);
> xa_for_each(&dpll_device_xa, index, dpll) {
> if (dpll->clock_id == clock_id &&
> dpll->device_idx == device_idx &&
> @@ -475,7 +479,7 @@ dpll_device_get(u64 clock_id, u32 device_idx, struct module *module,
> dpll_device_tracker_alloc(ret, tracker);
> }
>
> - mutex_unlock(&dpll_lock);
> + up_write(&dpll_lock);
>
> return ret;
> }
> @@ -492,9 +496,9 @@ EXPORT_SYMBOL_GPL(dpll_device_get);
> */
> void dpll_device_put(struct dpll_device *dpll, dpll_tracker *tracker)
> {
> - mutex_lock(&dpll_lock);
> + down_write(&dpll_lock);
> __dpll_device_put(dpll, tracker);
> - mutex_unlock(&dpll_lock);
> + up_write(&dpll_lock);
> }
> EXPORT_SYMBOL_GPL(dpll_device_put);
>
> @@ -540,16 +544,16 @@ int dpll_device_register(struct dpll_device *dpll, enum dpll_type type,
> if (WARN_ON(type < DPLL_TYPE_PPS || type > DPLL_TYPE_MAX))
> return -EINVAL;
>
> - mutex_lock(&dpll_lock);
> + down_write(&dpll_lock);
> reg = dpll_device_registration_find(dpll, ops, priv);
> if (reg) {
> - mutex_unlock(&dpll_lock);
> + up_write(&dpll_lock);
> return -EEXIST;
> }
>
> reg = kzalloc_obj(*reg);
> if (!reg) {
> - mutex_unlock(&dpll_lock);
> + up_write(&dpll_lock);
> return -ENOMEM;
> }
> reg->ops = ops;
> @@ -559,13 +563,13 @@ int dpll_device_register(struct dpll_device *dpll, enum dpll_type type,
> first_registration = list_empty(&dpll->registration_list);
> list_add_tail(®->list, &dpll->registration_list);
> if (!first_registration) {
> - mutex_unlock(&dpll_lock);
> + up_write(&dpll_lock);
> return 0;
> }
>
> xa_set_mark(&dpll_device_xa, dpll->id, DPLL_REGISTERED);
> dpll_device_create_ntf(dpll);
> - mutex_unlock(&dpll_lock);
> + up_write(&dpll_lock);
>
> return 0;
> }
> @@ -586,12 +590,12 @@ void dpll_device_unregister(struct dpll_device *dpll,
> {
> struct dpll_device_registration *reg;
>
> - mutex_lock(&dpll_lock);
> + down_write(&dpll_lock);
> ASSERT_DPLL_REGISTERED(dpll);
> dpll_device_delete_ntf(dpll);
> reg = dpll_device_registration_find(dpll, ops, priv);
> if (WARN_ON(!reg)) {
> - mutex_unlock(&dpll_lock);
> + up_write(&dpll_lock);
> return;
> }
> list_del(®->list);
> @@ -599,11 +603,11 @@ void dpll_device_unregister(struct dpll_device *dpll,
> kfree(reg);
>
> if (!list_empty(&dpll->registration_list)) {
> - mutex_unlock(&dpll_lock);
> + up_write(&dpll_lock);
> return;
> }
> xa_clear_mark(&dpll_device_xa, dpll->id, DPLL_REGISTERED);
> - mutex_unlock(&dpll_lock);
> + up_write(&dpll_lock);
> }
> EXPORT_SYMBOL_GPL(dpll_device_unregister);
>
> @@ -771,9 +775,9 @@ int register_dpll_notifier(struct notifier_block *nb)
> {
> int ret;
>
> - mutex_lock(&dpll_lock);
> + down_write(&dpll_lock);
> ret = raw_notifier_chain_register(&dpll_notifier_chain, nb);
> - mutex_unlock(&dpll_lock);
> + up_write(&dpll_lock);
> return ret;
> }
> EXPORT_SYMBOL_GPL(register_dpll_notifier);
> @@ -782,9 +786,9 @@ int unregister_dpll_notifier(struct notifier_block *nb)
> {
> int ret;
>
> - mutex_lock(&dpll_lock);
> + down_write(&dpll_lock);
> ret = raw_notifier_chain_unregister(&dpll_notifier_chain, nb);
> - mutex_unlock(&dpll_lock);
> + up_write(&dpll_lock);
> return ret;
> }
> EXPORT_SYMBOL_GPL(unregister_dpll_notifier);
> @@ -812,7 +816,7 @@ dpll_pin_get(u64 clock_id, u32 pin_idx, struct module *module,
> struct dpll_pin *pos, *ret = NULL;
> unsigned long i;
>
> - mutex_lock(&dpll_lock);
> + down_write(&dpll_lock);
> xa_for_each(&dpll_pin_xa, i, pos) {
> if (pos->clock_id == clock_id &&
> pos->pin_idx == pin_idx &&
> @@ -827,7 +831,7 @@ dpll_pin_get(u64 clock_id, u32 pin_idx, struct module *module,
> if (!IS_ERR(ret))
> dpll_pin_tracker_alloc(ret, tracker);
> }
> - mutex_unlock(&dpll_lock);
> + up_write(&dpll_lock);
>
> return ret;
> }
> @@ -844,9 +848,9 @@ EXPORT_SYMBOL_GPL(dpll_pin_get);
> */
> void dpll_pin_put(struct dpll_pin *pin, dpll_tracker *tracker)
> {
> - mutex_lock(&dpll_lock);
> + down_write(&dpll_lock);
> __dpll_pin_put(pin, tracker);
> - mutex_unlock(&dpll_lock);
> + up_write(&dpll_lock);
> }
> EXPORT_SYMBOL_GPL(dpll_pin_put);
>
> @@ -859,10 +863,10 @@ EXPORT_SYMBOL_GPL(dpll_pin_put);
> */
> void dpll_pin_fwnode_set(struct dpll_pin *pin, struct fwnode_handle *fwnode)
> {
> - mutex_lock(&dpll_lock);
> + down_write(&dpll_lock);
> fwnode_handle_put(pin->fwnode); /* Drop fwnode previously set */
> pin->fwnode = fwnode_handle_get(fwnode);
> - mutex_unlock(&dpll_lock);
> + up_write(&dpll_lock);
> }
> EXPORT_SYMBOL_GPL(dpll_pin_fwnode_set);
>
> @@ -885,7 +889,7 @@ struct dpll_pin *fwnode_dpll_pin_find(struct fwnode_handle *fwnode,
> struct dpll_pin *pin, *ret = NULL;
> unsigned long index;
>
> - mutex_lock(&dpll_lock);
> + down_write(&dpll_lock);
> xa_for_each(&dpll_pin_xa, index, pin) {
> if (pin->fwnode == fwnode) {
> __dpll_pin_hold(pin, tracker);
> @@ -893,7 +897,7 @@ struct dpll_pin *fwnode_dpll_pin_find(struct fwnode_handle *fwnode,
> break;
> }
> }
> - mutex_unlock(&dpll_lock);
> + up_write(&dpll_lock);
>
> return ret;
> }
> @@ -950,7 +954,7 @@ dpll_pin_register(struct dpll_device *dpll, struct dpll_pin *pin,
> DPLL_PIN_CAPABILITIES_STATE_CAN_CHANGE)))
> return -EINVAL;
>
> - mutex_lock(&dpll_lock);
> + down_write(&dpll_lock);
>
> dev_ops = dpll_device_ops(dpll);
> if (WARN_ON(ops->measured_freq_get &&
> @@ -974,7 +978,7 @@ dpll_pin_register(struct dpll_device *dpll, struct dpll_pin *pin,
>
> ret = __dpll_pin_register(dpll, pin, ops, priv, NULL);
> out_unlock:
> - mutex_unlock(&dpll_lock);
> + up_write(&dpll_lock);
>
> return ret;
> }
> @@ -1028,9 +1032,9 @@ void dpll_pin_unregister(struct dpll_device *dpll, struct dpll_pin *pin,
> if (WARN_ON(!xa_empty(&pin->parent_refs)))
> return;
>
> - mutex_lock(&dpll_lock);
> + down_write(&dpll_lock);
> __dpll_pin_unregister(dpll, pin, ops, priv, NULL);
> - mutex_unlock(&dpll_lock);
> + up_write(&dpll_lock);
> }
> EXPORT_SYMBOL_GPL(dpll_pin_unregister);
>
> @@ -1064,7 +1068,7 @@ int dpll_pin_on_pin_register(struct dpll_pin *parent, struct dpll_pin *pin,
> WARN_ON(!ops->direction_get))
> return -EINVAL;
>
> - mutex_lock(&dpll_lock);
> + down_write(&dpll_lock);
> ret = dpll_xa_ref_pin_add(&pin->parent_refs, parent, ops, priv, pin);
> if (ret)
> goto unlock;
> @@ -1076,7 +1080,7 @@ int dpll_pin_on_pin_register(struct dpll_pin *parent, struct dpll_pin *pin,
> }
> dpll_pin_create_ntf(pin, parent->clock_id);
> }
> - mutex_unlock(&dpll_lock);
> + up_write(&dpll_lock);
>
> return ret;
>
> @@ -1089,7 +1093,7 @@ int dpll_pin_on_pin_register(struct dpll_pin *parent, struct dpll_pin *pin,
> }
> dpll_xa_ref_pin_del(&pin->parent_refs, parent, ops, priv, pin);
> unlock:
> - mutex_unlock(&dpll_lock);
> + up_write(&dpll_lock);
> return ret;
> }
> EXPORT_SYMBOL_GPL(dpll_pin_on_pin_register);
> @@ -1111,7 +1115,7 @@ void dpll_pin_on_pin_unregister(struct dpll_pin *parent, struct dpll_pin *pin,
> struct dpll_pin_ref *ref;
> unsigned long i;
>
> - mutex_lock(&dpll_lock);
> + down_write(&dpll_lock);
> xa_for_each(&pin->dpll_refs, i, ref) {
> reg = dpll_pin_registration_find(ref, ops, priv, parent);
> if (!reg)
> @@ -1120,7 +1124,7 @@ void dpll_pin_on_pin_unregister(struct dpll_pin *parent, struct dpll_pin *pin,
> __dpll_pin_unregister(ref->dpll, pin, ops, priv, parent);
> }
> dpll_xa_ref_pin_del(&pin->parent_refs, parent, ops, priv, pin);
> - mutex_unlock(&dpll_lock);
> + up_write(&dpll_lock);
> }
> EXPORT_SYMBOL_GPL(dpll_pin_on_pin_unregister);
>
> @@ -1141,13 +1145,13 @@ int dpll_pin_ref_sync_pair_add(struct dpll_pin *pin,
> {
> int ret;
>
> - mutex_lock(&dpll_lock);
> + down_write(&dpll_lock);
> ret = xa_insert(&pin->ref_sync_pins, ref_sync_pin->id,
> ref_sync_pin, GFP_KERNEL);
> mutex_lock(&pin->domain->lock);
> __dpll_pin_change_ntf(pin);
> mutex_unlock(&pin->domain->lock);
> - mutex_unlock(&dpll_lock);
> + up_write(&dpll_lock);
>
> return ret;
> }
> @@ -1254,23 +1258,12 @@ const struct dpll_pin_ops *dpll_pin_ops(struct dpll_pin_ref *ref)
>
> static int __init dpll_init(void)
> {
> - int ret;
> -
> - ret = genl_register_family(&dpll_nl_family);
> - if (ret)
> - goto error;
> -
> - return 0;
> -
> -error:
> - mutex_destroy(&dpll_lock);
> - return ret;
> + return genl_register_family(&dpll_nl_family);
> }
>
> static void __exit dpll_exit(void)
> {
> genl_unregister_family(&dpll_nl_family);
> - mutex_destroy(&dpll_lock);
> }
>
> subsys_initcall(dpll_init);
> diff --git a/drivers/dpll/dpll_core.h b/drivers/dpll/dpll_core.h
> index 630c5a1ca572c2..e212e53528e33b 100644
> --- a/drivers/dpll/dpll_core.h
> +++ b/drivers/dpll/dpll_core.h
> @@ -12,6 +12,7 @@
> #include <linux/mutex.h>
> #include <linux/refcount.h>
> #include <linux/ref_tracker.h>
> +#include <linux/rwsem.h>
> #include "dpll_nl.h"
>
> #define DPLL_REGISTERED XA_MARK_1
> @@ -129,7 +130,7 @@ const struct dpll_pin_ops *dpll_pin_ops(struct dpll_pin_ref *ref);
> struct dpll_pin_ref *dpll_xa_ref_dpll_first(struct xarray *xa_refs);
> extern struct xarray dpll_device_xa;
> extern struct xarray dpll_pin_xa;
> -extern struct mutex dpll_lock;
> +extern struct rw_semaphore dpll_lock;
>
> void dpll_device_notify(struct dpll_device *dpll, unsigned long action);
> void dpll_pin_notify(struct dpll_pin *pin, u64 src_clock_id,
> diff --git a/drivers/dpll/dpll_netlink.c b/drivers/dpll/dpll_netlink.c
> index 1b4e3a0166a744..9d8f5d1ce97248 100644
> --- a/drivers/dpll/dpll_netlink.c
> +++ b/drivers/dpll/dpll_netlink.c
> @@ -910,11 +910,11 @@ int dpll_device_change_ntf(struct dpll_device *dpll)
> {
> int ret;
>
> - mutex_lock(&dpll_lock);
> + down_read(&dpll_lock);
> mutex_lock(&dpll->domain->lock);
> ret = __dpll_device_change_ntf(dpll);
> mutex_unlock(&dpll->domain->lock);
> - mutex_unlock(&dpll_lock);
> + up_read(&dpll_lock);
>
> return ret;
> }
> @@ -1004,11 +1004,11 @@ int dpll_pin_change_ntf(struct dpll_pin *pin)
> {
> int ret;
>
> - mutex_lock(&dpll_lock);
> + down_read(&dpll_lock);
> mutex_lock(&pin->domain->lock);
> ret = __dpll_pin_change_ntf(pin);
> mutex_unlock(&pin->domain->lock);
> - mutex_unlock(&dpll_lock);
> + up_read(&dpll_lock);
>
> return ret;
> }
> @@ -1795,7 +1795,7 @@ int dpll_nl_pin_get_dumpit(struct sk_buff *skb, struct netlink_callback *cb)
> unsigned long i;
> int ret = 0;
>
> - mutex_lock(&dpll_lock);
> + down_read(&dpll_lock);
> xa_for_each_marked_start(&dpll_pin_xa, i, pin, DPLL_REGISTERED,
> ctx->idx) {
> if (!dpll_pin_available(pin))
> @@ -1821,7 +1821,7 @@ int dpll_nl_pin_get_dumpit(struct sk_buff *skb, struct netlink_callback *cb)
> }
> genlmsg_end(skb, hdr);
> }
> - mutex_unlock(&dpll_lock);
> + up_read(&dpll_lock);
>
> if (ret == -EMSGSIZE) {
> ctx->idx = i;
> @@ -2019,7 +2019,7 @@ int dpll_nl_device_get_dumpit(struct sk_buff *skb, struct netlink_callback *cb)
> unsigned long i;
> int ret = 0;
>
> - mutex_lock(&dpll_lock);
> + down_read(&dpll_lock);
> xa_for_each_marked_start(&dpll_device_xa, i, dpll, DPLL_REGISTERED,
> ctx->idx) {
> hdr = genlmsg_put(skb, NETLINK_CB(cb->skb).portid,
> @@ -2038,7 +2038,7 @@ int dpll_nl_device_get_dumpit(struct sk_buff *skb, struct netlink_callback *cb)
> }
> genlmsg_end(skb, hdr);
> }
> - mutex_unlock(&dpll_lock);
> + up_read(&dpll_lock);
>
> if (ret == -EMSGSIZE) {
> ctx->idx = i;
> @@ -2056,7 +2056,7 @@ int dpll_pre_doit(const struct genl_split_ops *ops, struct sk_buff *skb,
> if (GENL_REQ_ATTR_CHECK(info, DPLL_A_ID))
> return -EINVAL;
>
> - mutex_lock(&dpll_lock);
> + down_read(&dpll_lock);
> id = nla_get_u32(info->attrs[DPLL_A_ID]);
> dpll = dpll_device_get_by_id(id);
> if (!dpll) {
> @@ -2067,7 +2067,7 @@ int dpll_pre_doit(const struct genl_split_ops *ops, struct sk_buff *skb,
> mutex_lock(&dpll->domain->lock);
> return 0;
> unlock:
> - mutex_unlock(&dpll_lock);
> + up_read(&dpll_lock);
> return -ENODEV;
> }
>
> @@ -2077,14 +2077,14 @@ void dpll_post_doit(const struct genl_split_ops *ops, struct sk_buff *skb,
> struct dpll_device *dpll = info->user_ptr[0];
>
> mutex_unlock(&dpll->domain->lock);
> - mutex_unlock(&dpll_lock);
> + up_read(&dpll_lock);
> }
>
> int
> dpll_lock_doit(const struct genl_split_ops *ops, struct sk_buff *skb,
> struct genl_info *info)
> {
> - mutex_lock(&dpll_lock);
> + down_read(&dpll_lock);
>
> return 0;
> }
> @@ -2093,7 +2093,7 @@ void
> dpll_unlock_doit(const struct genl_split_ops *ops, struct sk_buff *skb,
> struct genl_info *info)
> {
> - mutex_unlock(&dpll_lock);
> + up_read(&dpll_lock);
> }
>
> int dpll_pin_pre_doit(const struct genl_split_ops *ops, struct sk_buff *skb,
> @@ -2102,7 +2102,7 @@ int dpll_pin_pre_doit(const struct genl_split_ops *ops, struct sk_buff *skb,
> struct dpll_pin *pin;
> int ret;
>
> - mutex_lock(&dpll_lock);
> + down_read(&dpll_lock);
> if (GENL_REQ_ATTR_CHECK(info, DPLL_A_PIN_ID)) {
> ret = -EINVAL;
> goto unlock_dev;
> @@ -2119,7 +2119,7 @@ int dpll_pin_pre_doit(const struct genl_split_ops *ops, struct sk_buff *skb,
> return 0;
>
> unlock_dev:
> - mutex_unlock(&dpll_lock);
> + up_read(&dpll_lock);
> return ret;
> }
>
> @@ -2129,5 +2129,5 @@ void dpll_pin_post_doit(const struct genl_split_ops *ops, struct sk_buff *skb,
> struct dpll_pin *pin = info->user_ptr[0];
>
> mutex_unlock(&pin->domain->lock);
> - mutex_unlock(&dpll_lock);
> + up_read(&dpll_lock);
> }
Reviewed-by: Petr Oros <poros@redhat.com>
^ permalink raw reply [flat|nested] 27+ messages in thread
* Re: [PATCH net-next 06/10] dpll: add KUnit tests for core locking
2026-11-06 12:10 ` [PATCH net-next 06/10] dpll: add KUnit tests for core locking Ivan Vecera
@ 2026-11-10 12:24 ` Petr Oros
2026-11-10 17:53 ` netdev-bot+sashiko
1 sibling, 0 replies; 27+ messages in thread
From: Petr Oros @ 2026-11-10 12:24 UTC (permalink / raw)
To: Ivan Vecera, netdev
Cc: Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
Jonathan Corbet, Shuah Khan, Randy Dunlap, Min Li, Tony Nguyen,
Przemek Kitszel, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Richard Cochran, linux-doc,
linux-kernel, intel-wired-lan, Grzegorz Nitka, Karol Kolacinski,
Saeed Mahameed, Tariq Toukan
On 11/6/26 1:10 PM, Ivan Vecera wrote:
> Add KUnit tests using a mock driver with unrelated domains, a pin of one
> domain registered with a dpll of another and a pin of one domain under a
> MUX pin of another. They check that:
>
> - objects with the same (module, clock_id) share a refcounted domain
> - callbacks of a device run under its domain and callbacks of a pin under
> the domain of the pin alone, whichever driver registered them
> - a callback blocked in one domain does not stop unrelated domains
> - callbacks of one domain are serialized, also across its devices
> - a stuck callback of a foreign registration stops only the domain of the
> pin, and a stuck foreign device or domain of the pin does not stop the
> other side
> - a pin and its parent pin of another domain do not block each other
> - registrations can connect two domains in both directions
> - a topology change waits until running requests finish
> - a request setting several attributes of a pin is atomic against other
> requests about the pin, also when it spans two domains
> - notifications from callbacks keep their order, also when a callback
> fails
>
> Requests that can block run in kthreads, so a test can report a locking
> bug instead of blocking itself. A real deadlock cannot be undone, the
> stuck request keeps its locks, so the teardown then hangs until the run
> times out. The handlers are called directly with a hand-built message,
> so they are exported for KUnit.
>
> Only lockdep can tell which locks a callback holds. Without it the checks
> of held locks cannot fail, so the test that checks nothing else is
> skipped. The tests of what blocks what do not depend on it.
>
> The tests do not cover what needs a netlink socket, for example the order
> of notifications against the ACK or the content of messages.
>
> Run them with:
>
> tools/testing/kunit/kunit.py run --kunitconfig=drivers/dpll
>
> Signed-off-by: Ivan Vecera <ivecera@redhat.com>
> ---
> drivers/dpll/.kunitconfig | 5 +
> drivers/dpll/Kconfig | 13 +
> drivers/dpll/Makefile | 1 +
> drivers/dpll/dpll_core_kunit.c | 1471 ++++++++++++++++++++++++++++++++
> drivers/dpll/dpll_netlink.c | 7 +
> 5 files changed, 1497 insertions(+)
> create mode 100644 drivers/dpll/.kunitconfig
> create mode 100644 drivers/dpll/dpll_core_kunit.c
>
> diff --git a/drivers/dpll/.kunitconfig b/drivers/dpll/.kunitconfig
> new file mode 100644
> index 00000000000000..017c9d5b593a4f
> --- /dev/null
> +++ b/drivers/dpll/.kunitconfig
> @@ -0,0 +1,5 @@
> +CONFIG_KUNIT=y
> +CONFIG_NET=y
> +CONFIG_DPLL_KUNIT_TEST=y
> +CONFIG_DEBUG_KERNEL=y
> +CONFIG_PROVE_LOCKING=y
> diff --git a/drivers/dpll/Kconfig b/drivers/dpll/Kconfig
> index be98969f040ab6..12bcf706a15e14 100644
> --- a/drivers/dpll/Kconfig
> +++ b/drivers/dpll/Kconfig
> @@ -23,6 +23,19 @@ config DPLL_REFCNT_TRACKER
>
> If unsure, say N.
>
> +config DPLL_KUNIT_TEST
> + tristate "KUnit tests for DPLL core" if !KUNIT_ALL_TESTS
> + depends on KUNIT && NET
> + select DPLL
> + default KUNIT_ALL_TESTS
> + help
> + KUnit tests for locking in the DPLL core. They use a mock driver
> + to check that callbacks run under the locks of their domains and
> + that unrelated domains do not block each other. The checks of held
> + locks need PROVE_LOCKING.
> +
> + If unsure, say N.
> +
> source "drivers/dpll/zl3073x/Kconfig"
>
> endmenu
> diff --git a/drivers/dpll/Makefile b/drivers/dpll/Makefile
> index 9e7a3a3e592e81..75ee1608c76a93 100644
> --- a/drivers/dpll/Makefile
> +++ b/drivers/dpll/Makefile
> @@ -8,4 +8,5 @@ dpll-y += dpll_core.o
> dpll-y += dpll_netlink.o
> dpll-y += dpll_nl.o
>
> +obj-$(CONFIG_DPLL_KUNIT_TEST) += dpll_core_kunit.o
> obj-$(CONFIG_ZL3073X) += zl3073x/
> diff --git a/drivers/dpll/dpll_core_kunit.c b/drivers/dpll/dpll_core_kunit.c
> new file mode 100644
> index 00000000000000..a58800eda5ff51
> --- /dev/null
> +++ b/drivers/dpll/dpll_core_kunit.c
> @@ -0,0 +1,1471 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * KUnit tests for locking in the DPLL core.
> + *
> + * Each test starts with two unrelated domains:
> + *
> + * Domain A: devices A0 and A1
> + * pin P on A0 and A1, setting its state on A0 notifies pin S
> + * pin S on A0 and A1
> + * Domain B: device B0
> + * pin BP on B0
> + *
> + * Tests of objects connected by a pin add these domains:
> + *
> + * Domain E: device E0, pin X on E0 (the owner), pin M (MUX) on E0
> + * Domain F: device F0, pin X from E is also registered with F0
> + * Domain G: pin C registered under pin M from E
> + *
> + * Requests that can block run in kthreads, so a test can report a locking bug
> + * instead of blocking itself. A real deadlock cannot be undone, the stuck
> + * request keeps its locks, so the teardown then hangs until the run times out.
> + */
> +
> +#include <kunit/test.h>
> +#include <linux/completion.h>
> +#include <linux/debug_locks.h>
> +#include <linux/dpll.h>
> +#include <linux/kthread.h>
> +#include <linux/lockdep.h>
> +#include <linux/notifier.h>
> +#include <linux/property.h>
> +#include <linux/wait.h>
> +#include <net/genetlink.h>
> +#include <net/netlink.h>
> +
> +#include "dpll_core.h"
> +
> +MODULE_IMPORT_NS("EXPORTED_FOR_KUNIT_TESTING");
> +
> +#define DT_CLOCK_ID_A 0xd0110a
> +#define DT_CLOCK_ID_B 0xd0110b
> +#define DT_CLOCK_ID_E 0xd0110e
> +#define DT_CLOCK_ID_F 0xd0110f
> +#define DT_CLOCK_ID_G 0xd01107
> +
> +#define DT_PIN_IDX_P 0
> +#define DT_PIN_IDX_S 1
> +#define DT_PIN_IDX_BP 2
> +#define DT_PIN_IDX_M 3
> +#define DT_PIN_IDX_X 4
> +#define DT_PIN_IDX_C 5
> +#define DT_PIN_IDX_Y 6
> +#define DT_PIN_IDX_EXTRA 7
> +
> +#define DT_MAX_EVENTS 8
> +#define DT_TIMEOUT msecs_to_jiffies(5000)
> +#define DT_SHORT_TIMEOUT msecs_to_jiffies(100)
> +
> +struct dt_ctx;
> +
> +/* Counters and blocking of the callbacks of one driver instance */
> +struct dt_dom {
> + struct dt_ctx *ctx;
> + struct completion entered;
> + atomic_t cur;
> + atomic_t peak;
> + bool block;
> +};
> +
> +struct dt_dev {
> + struct dt_dom *dom;
> + struct dpll_device *dpll;
> + dpll_tracker tracker;
> + struct dt_dev *notify;
> + enum dpll_mode mode;
> + bool block;
> +};
> +
> +struct dt_pin {
> + struct dpll_pin *pin;
> + dpll_tracker tracker;
> +};
> +
> +struct dt_reg {
> + struct dt_dom *dom;
> + struct dt_pin *pin;
> + struct dt_dev *dev;
> + struct dt_pin *parent;
> + struct dt_pin *notify;
> + struct dpll_domain *forbid;
> + enum dpll_pin_state state;
> + enum dpll_pin_direction direction;
> + atomic_t freq_gets;
> + u32 prio;
> + bool block_prio_set;
> +};
> +
> +struct dt_ctx {
> + struct kunit *test;
> + struct dt_dom dom_a, dom_b, dom_e, dom_f, dom_g;
> + struct dt_dev a0, a1, b0, e0, f0;
> + struct dt_pin p, s, bp, x, y, m, c, extra;
> + struct dt_reg reg_p_a0, reg_p_a1, reg_s_a0, reg_s_a1, reg_bp_b0;
> + struct dt_reg reg_x_e0, reg_x_f0, reg_y_e0, reg_y_f0, reg_m_e0, reg_c_m;
> + struct dt_reg *atomic_regs[2];
> + struct notifier_block nb;
> + wait_queue_head_t wq;
> + atomic_t cur_total;
> + atomic_t peak_total;
> + atomic_t wrong_lock;
> + atomic_t bad_ntf;
> + atomic_t torn;
> + struct task_struct *set_task;
> + void *events[DT_MAX_EVENTS];
> + int num_events;
> + bool record;
> + bool fail_after_ntf;
> + bool debug_locks_at_start;
> +};
> +
> +struct dt_req {
> + struct dt_ctx *ctx;
> + int (*fn)(void *arg);
> + void *arg;
> + int ret;
> + struct completion done;
> + bool finished;
> +};
> +
> +/* A nested attribute of a pin set request */
> +struct dt_nest {
> + int type;
> + u32 parent_id;
> + int attr;
> + u32 val;
> +};
> +
> +/* Allocated with kunit_kzalloc(), a failed assertion must not free them */
> +struct dt_pin_set_args {
> + struct dt_ctx *ctx;
> + struct dpll_pin *pin;
> + struct dt_nest nests[2];
> + int num;
> +};
> +
> +struct dt_mode_set_args {
> + struct dpll_device *dpll;
> + enum dpll_mode mode;
> +};
> +
> +/*
> + * The lock may be held by another task, so mutex_is_locked() is not enough.
> + * Without lockdep no check of a held lock can fail.
> + */
> +static bool dt_held(struct dpll_domain *domain)
> +{
> +#ifdef CONFIG_LOCKDEP
> + return lockdep_is_held(&domain->lock) != 0;
> +#else
> + return true;
> +#endif
> +}
> +
> +static bool dt_surely_held(struct dpll_domain *domain)
> +{
> +#ifdef CONFIG_LOCKDEP
> + return lockdep_is_held(&domain->lock) > 0;
> +#else
> + return false;
> +#endif
> +}
> +
> +static void dt_peak(atomic_t *peak, int val)
> +{
> + int old = atomic_read(peak);
> +
> + while (val > old && !atomic_try_cmpxchg(peak, &old, val))
> + ;
> +}
> +
> +static void dt_block(struct dt_dom *dom, bool *block)
> +{
> + complete(&dom->entered);
> + wait_event(dom->ctx->wq, !READ_ONCE(*block));
> +}
> +
> +static void dt_enter(struct dt_dom *dom)
> +{
> + struct dt_ctx *ctx = dom->ctx;
> +
> + dt_peak(&ctx->peak_total, atomic_inc_return(&ctx->cur_total));
> + dt_peak(&dom->peak, atomic_inc_return(&dom->cur));
> +
> + if (READ_ONCE(dom->block))
> + dt_block(dom, &dom->block);
> +}
> +
> +static void dt_leave(struct dt_dom *dom)
> +{
> + atomic_dec(&dom->cur);
> + atomic_dec(&dom->ctx->cur_total);
> +}
> +
> +static void dt_dev_enter(struct dt_dev *dev, const struct dpll_device *dpll)
> +{
> + if (!dt_held(dpll->domain))
> + atomic_inc(&dev->dom->ctx->wrong_lock);
> + dt_enter(dev->dom);
> + if (READ_ONCE(dev->block))
> + dt_block(dev->dom, &dev->block);
> +}
> +
> +/* A callback of a pin runs under the domain of the pin alone */
> +static void dt_pin_enter(struct dt_reg *reg, const struct dpll_pin *pin)
> +{
> + struct dt_ctx *ctx = reg->dom->ctx;
> +
> + if (!dt_held(pin->domain) ||
> + (reg->forbid && dt_surely_held(reg->forbid)))
> + atomic_inc(&ctx->wrong_lock);
> + dt_enter(reg->dom);
> +}
> +
> +static int dt_mode_get(const struct dpll_device *dpll, void *priv,
> + enum dpll_mode *mode, struct netlink_ext_ack *extack)
> +{
> + struct dt_dev *dev = priv;
> +
> + dt_dev_enter(dev, dpll);
> + *mode = dev->mode;
> + dt_leave(dev->dom);
> +
> + return 0;
> +}
> +
> +static int dt_mode_set(const struct dpll_device *dpll, void *priv,
> + enum dpll_mode mode, struct netlink_ext_ack *extack)
> +{
> + struct dt_dev *dev = priv;
> +
> + dt_dev_enter(dev, dpll);
> + dev->mode = mode;
> + dt_leave(dev->dom);
> +
> + if (dev->notify)
> + __dpll_device_change_ntf(dev->notify->dpll);
> +
> + return 0;
> +}
> +
> +static int dt_supported_modes_get(const struct dpll_device *dpll, void *priv,
> + unsigned long *modes,
> + struct netlink_ext_ack *extack)
> +{
> + struct dt_dev *dev = priv;
> +
> + dt_dev_enter(dev, dpll);
> + __set_bit(DPLL_MODE_AUTOMATIC, modes);
> + __set_bit(DPLL_MODE_MANUAL, modes);
> + dt_leave(dev->dom);
> +
> + return 0;
> +}
> +
> +static int dt_lock_status_get(const struct dpll_device *dpll, void *priv,
> + enum dpll_lock_status *status,
> + enum dpll_lock_status_error *status_error,
> + struct netlink_ext_ack *extack)
> +{
> + struct dt_dev *dev = priv;
> +
> + dt_dev_enter(dev, dpll);
> + *status = DPLL_LOCK_STATUS_LOCKED;
> + dt_leave(dev->dom);
> +
> + return 0;
> +}
> +
> +static const struct dpll_device_ops dt_dev_ops = {
> + .mode_get = dt_mode_get,
> + .mode_set = dt_mode_set,
> + .supported_modes_get = dt_supported_modes_get,
> + .lock_status_get = dt_lock_status_get,
> +};
> +
> +static int dt_frequency_get(const struct dpll_pin *pin, void *pin_priv,
> + const struct dpll_device *dpll, void *dpll_priv,
> + u64 *frequency, struct netlink_ext_ack *extack)
> +{
> + struct dt_reg *reg = pin_priv;
> +
> + dt_pin_enter(reg, pin);
> + atomic_inc(®->freq_gets);
> + *frequency = 1;
> + dt_leave(reg->dom);
> +
> + return 0;
> +}
> +
> +static int dt_state_on_dpll_get(const struct dpll_pin *pin, void *pin_priv,
> + const struct dpll_device *dpll, void *dpll_priv,
> + enum dpll_pin_state *state,
> + struct netlink_ext_ack *extack)
> +{
> + struct dt_reg *reg = pin_priv;
> +
> + dt_pin_enter(reg, pin);
> + *state = reg->state;
> + dt_leave(reg->dom);
> +
> + return 0;
> +}
> +
> +static int dt_state_on_dpll_set(const struct dpll_pin *pin, void *pin_priv,
> + const struct dpll_device *dpll, void *dpll_priv,
> + const enum dpll_pin_state state,
> + struct netlink_ext_ack *extack)
> +{
> + struct dt_reg *reg = pin_priv;
> +
> + dt_pin_enter(reg, pin);
> + reg->state = state;
> + dt_leave(reg->dom);
> +
> + if (reg->notify)
> + __dpll_pin_change_ntf(reg->notify->pin);
> +
> + return reg->dom->ctx->fail_after_ntf ? -EIO : 0;
> +}
> +
> +static int dt_state_on_pin_get(const struct dpll_pin *pin, void *pin_priv,
> + const struct dpll_pin *parent_pin,
> + void *parent_pin_priv,
> + enum dpll_pin_state *state,
> + struct netlink_ext_ack *extack)
> +{
> + struct dt_reg *reg = pin_priv;
> +
> + dt_pin_enter(reg, pin);
> + *state = reg->state;
> + dt_leave(reg->dom);
> +
> + return 0;
> +}
> +
> +static int dt_state_on_pin_set(const struct dpll_pin *pin, void *pin_priv,
> + const struct dpll_pin *parent_pin,
> + void *parent_pin_priv,
> + const enum dpll_pin_state state,
> + struct netlink_ext_ack *extack)
> +{
> + struct dt_reg *reg = pin_priv;
> +
> + dt_pin_enter(reg, pin);
> + reg->state = state;
> + dt_leave(reg->dom);
> +
> + return 0;
> +}
> +
> +static int dt_prio_get(const struct dpll_pin *pin, void *pin_priv,
> + const struct dpll_device *dpll, void *dpll_priv,
> + u32 *prio, struct netlink_ext_ack *extack)
> +{
> + struct dt_reg *reg = pin_priv;
> + struct dt_ctx *ctx = reg->dom->ctx;
> +
> + dt_pin_enter(reg, pin);
> + /* Only the request doing the set may see it half done */
> + if (READ_ONCE(ctx->set_task) && current != ctx->set_task &&
> + ctx->atomic_regs[0]->prio != ctx->atomic_regs[1]->prio)
> + atomic_inc(&ctx->torn);
> + *prio = reg->prio;
> + dt_leave(reg->dom);
> +
> + return 0;
> +}
> +
> +static int dt_prio_set(const struct dpll_pin *pin, void *pin_priv,
> + const struct dpll_device *dpll, void *dpll_priv,
> + const u32 prio, struct netlink_ext_ack *extack)
> +{
> + struct dt_reg *reg = pin_priv;
> +
> + dt_pin_enter(reg, pin);
> + if (READ_ONCE(reg->block_prio_set))
> + dt_block(reg->dom, ®->block_prio_set);
> + reg->prio = prio;
> + dt_leave(reg->dom);
> +
> + return 0;
> +}
> +
> +static int dt_direction_get(const struct dpll_pin *pin, void *pin_priv,
> + const struct dpll_device *dpll, void *dpll_priv,
> + enum dpll_pin_direction *direction,
> + struct netlink_ext_ack *extack)
> +{
> + struct dt_reg *reg = pin_priv;
> +
> + dt_pin_enter(reg, pin);
> + *direction = reg->direction;
> + dt_leave(reg->dom);
> +
> + return 0;
> +}
> +
> +static int dt_direction_set(const struct dpll_pin *pin, void *pin_priv,
> + const struct dpll_device *dpll, void *dpll_priv,
> + const enum dpll_pin_direction direction,
> + struct netlink_ext_ack *extack)
> +{
> + struct dt_reg *reg = pin_priv;
> +
> + dt_pin_enter(reg, pin);
> + reg->direction = direction;
> + dt_leave(reg->dom);
> +
> + return 0;
> +}
> +
> +static int dt_ref_sync_get(const struct dpll_pin *pin, void *pin_priv,
> + const struct dpll_pin *ref_sync_pin,
> + void *ref_sync_pin_priv, enum dpll_pin_state *state,
> + struct netlink_ext_ack *extack)
> +{
> + struct dt_reg *reg = pin_priv;
> +
> + dt_pin_enter(reg, pin);
> + *state = DPLL_PIN_STATE_DISCONNECTED;
> + dt_leave(reg->dom);
> +
> + return 0;
> +}
> +
> +static const struct dpll_pin_ops dt_pin_ops = {
> + .frequency_get = dt_frequency_get,
> + .state_on_dpll_get = dt_state_on_dpll_get,
> + .state_on_dpll_set = dt_state_on_dpll_set,
> + .state_on_pin_get = dt_state_on_pin_get,
> + .state_on_pin_set = dt_state_on_pin_set,
> + .prio_get = dt_prio_get,
> + .prio_set = dt_prio_set,
> + .direction_get = dt_direction_get,
> + .direction_set = dt_direction_set,
> + .ref_sync_get = dt_ref_sync_get,
> +};
> +
> +static const struct dpll_pin_properties dt_prop = {
> + .type = DPLL_PIN_TYPE_EXT,
> + .capabilities = DPLL_PIN_CAPABILITIES_STATE_CAN_CHANGE |
> + DPLL_PIN_CAPABILITIES_PRIORITY_CAN_CHANGE |
> + DPLL_PIN_CAPABILITIES_DIRECTION_CAN_CHANGE,
> +};
> +
> +static const struct dpll_pin_properties dt_prop_mux = {
> + .type = DPLL_PIN_TYPE_MUX,
> + .capabilities = DPLL_PIN_CAPABILITIES_STATE_CAN_CHANGE,
> +};
> +
> +/* Other dpll drivers in the system can send notifications at any time */
> +static bool dt_own_clock_id(u64 clock_id)
> +{
> + switch (clock_id) {
> + case DT_CLOCK_ID_A:
> + case DT_CLOCK_ID_B:
> + case DT_CLOCK_ID_E:
> + case DT_CLOCK_ID_F:
> + case DT_CLOCK_ID_G:
> + return true;
> + default:
> + return false;
> + }
> +}
> +
> +static int dt_notifier(struct notifier_block *nb, unsigned long action,
> + void *data)
> +{
> + struct dt_ctx *ctx = container_of(nb, struct dt_ctx, nb);
> + struct dpll_device_notifier_info *dev_info = data;
> + struct dpll_pin_notifier_info *pin_info = data;
> + struct dpll_domain *domain;
> + u64 clock_id;
> + void *obj;
> +
> + switch (action) {
> + case DPLL_DEVICE_CHANGED:
> + clock_id = dev_info->clock_id;
> + obj = dev_info->dpll;
> + domain = dev_info->dpll->domain;
> + break;
> + case DPLL_PIN_CHANGED:
> + clock_id = pin_info->clock_id;
> + obj = pin_info->pin;
> + domain = pin_info->pin->domain;
> + break;
> + default:
> + return NOTIFY_DONE;
> + }
> +
> + if (!dt_own_clock_id(clock_id) || !ctx->record)
> + return NOTIFY_DONE;
> +
> + if (!dt_held(domain))
> + atomic_inc(&ctx->bad_ntf);
> + if (ctx->num_events < DT_MAX_EVENTS)
> + ctx->events[ctx->num_events] = obj;
> + ctx->num_events++;
> +
> + return NOTIFY_OK;
> +}
> +
> +static void dt_final_check(void *data)
> +{
> + struct dt_ctx *ctx = data;
> +
> + KUNIT_EXPECT_EQ(ctx->test, atomic_read(&ctx->wrong_lock), 0);
> + if (ctx->debug_locks_at_start)
> + KUNIT_EXPECT_TRUE(ctx->test, debug_locks);
> +}
> +
> +static void dt_dev_release(void *data)
> +{
> + struct dt_dev *dev = data;
> +
> + dpll_device_unregister(dev->dpll, &dt_dev_ops, dev);
> + dpll_device_put(dev->dpll, &dev->tracker);
> +}
> +
> +static void dt_dev_add(struct kunit *test, struct dt_dev *dev,
> + struct dt_dom *dom, u64 clock_id, u32 idx,
> + enum dpll_type type)
> +{
> + int ret;
> +
> + dev->dom = dom;
> + dev->mode = DPLL_MODE_AUTOMATIC;
> + dev->dpll = dpll_device_get(clock_id, idx, THIS_MODULE, &dev->tracker);
> + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, dev->dpll);
> +
> + ret = dpll_device_register(dev->dpll, type, &dt_dev_ops, dev);
> + if (ret)
> + dpll_device_put(dev->dpll, &dev->tracker);
> + KUNIT_ASSERT_EQ(test, ret, 0);
> + KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test, dt_dev_release,
> + dev), 0);
> +}
> +
> +static void dt_pin_release(void *data)
> +{
> + struct dt_pin *pin = data;
> +
> + if (!IS_ERR_OR_NULL(pin->pin))
> + dpll_pin_put(pin->pin, &pin->tracker);
> +}
> +
> +static void dt_pin_add(struct kunit *test, struct dt_pin *pin, u64 clock_id,
> + u32 idx, const struct dpll_pin_properties *prop)
> +{
> + pin->pin = dpll_pin_get(clock_id, idx, THIS_MODULE, prop,
> + &pin->tracker);
> + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, pin->pin);
> + KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test, dt_pin_release,
> + pin), 0);
> +}
> +
> +static void dt_reg_release(void *data)
> +{
> + struct dt_reg *reg = data;
> +
> + if (reg->parent)
> + dpll_pin_on_pin_unregister(reg->parent->pin, reg->pin->pin,
> + &dt_pin_ops, reg);
> + else
> + dpll_pin_unregister(reg->dev->dpll, reg->pin->pin, &dt_pin_ops,
> + reg);
> +}
> +
> +static void dt_reg_add(struct kunit *test, struct dt_reg *reg,
> + struct dt_dom *dom, struct dt_pin *pin,
> + struct dt_dev *dev, struct dt_pin *parent)
> +{
> + int ret;
> +
> + reg->dom = dom;
> + reg->pin = pin;
> + reg->dev = dev;
> + reg->parent = parent;
> + reg->state = DPLL_PIN_STATE_SELECTABLE;
> + reg->direction = DPLL_PIN_DIRECTION_INPUT;
> +
> + if (parent)
> + ret = dpll_pin_on_pin_register(parent->pin, pin->pin,
> + &dt_pin_ops, reg);
> + else
> + ret = dpll_pin_register(dev->dpll, pin->pin, &dt_pin_ops, reg);
> + KUNIT_ASSERT_EQ(test, ret, 0);
> + KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test, dt_reg_release,
> + reg), 0);
> +}
> +
> +static void dt_fwnode_release(void *data)
> +{
> + fwnode_remove_software_node(data);
> +}
> +
> +/* Creates a pin with a fwnode so that it can be registered with another dpll */
> +static void dt_fwnode_pin_add(struct kunit *test, struct dt_pin *pin,
> + u64 clock_id, u32 idx)
> +{
> + struct fwnode_handle *fwnode;
> +
> + fwnode = fwnode_create_software_node(NULL, NULL);
> + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, fwnode);
> + KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test, dt_fwnode_release,
> + fwnode), 0);
> + dt_pin_add(test, pin, clock_id, idx, &dt_prop);
> + dpll_pin_fwnode_set(pin->pin, fwnode);
> +}
> +
> +/* Builds domains E and F connected by pin X */
> +static void dt_foreign_pin_add(struct kunit *test)
> +{
> + struct dt_ctx *ctx = test->priv;
> +
> + dt_dev_add(test, &ctx->e0, &ctx->dom_e, DT_CLOCK_ID_E, 0,
> + DPLL_TYPE_PPS);
> + dt_dev_add(test, &ctx->f0, &ctx->dom_f, DT_CLOCK_ID_F, 0,
> + DPLL_TYPE_PPS);
> + dt_fwnode_pin_add(test, &ctx->x, DT_CLOCK_ID_E, DT_PIN_IDX_X);
> + dt_reg_add(test, &ctx->reg_x_e0, &ctx->dom_e, &ctx->x, &ctx->e0, NULL);
> + dt_reg_add(test, &ctx->reg_x_f0, &ctx->dom_f, &ctx->x, &ctx->f0, NULL);
> +
> + /* No callback of the pin takes the lock of the other domain */
> + ctx->reg_x_e0.forbid = ctx->f0.dpll->domain;
> + ctx->reg_x_f0.forbid = ctx->f0.dpll->domain;
> +}
> +
> +/*
> + * Builds domains E and G connected by pin C under pin M. The pins are added
> + * first, so that domain G is freed only after the registrations are gone.
> + */
> +static void dt_on_pin_add(struct kunit *test)
> +{
> + struct dt_ctx *ctx = test->priv;
> +
> + dt_dev_add(test, &ctx->e0, &ctx->dom_e, DT_CLOCK_ID_E, 0,
> + DPLL_TYPE_PPS);
> + dt_pin_add(test, &ctx->m, DT_CLOCK_ID_E, DT_PIN_IDX_M, &dt_prop_mux);
> + dt_pin_add(test, &ctx->c, DT_CLOCK_ID_G, DT_PIN_IDX_C, &dt_prop);
> + dt_reg_add(test, &ctx->reg_m_e0, &ctx->dom_e, &ctx->m, &ctx->e0, NULL);
> + dt_reg_add(test, &ctx->reg_c_m, &ctx->dom_g, &ctx->c, NULL, &ctx->m);
> +
> + ctx->reg_c_m.forbid = ctx->e0.dpll->domain;
> + ctx->reg_m_e0.forbid = ctx->c.pin->domain;
> +}
> +
> +static void dt_notifier_release(void *data)
> +{
> + struct dt_ctx *ctx = data;
> +
> + unregister_dpll_notifier(&ctx->nb);
> +}
> +
> +static void dt_dom_init(struct dt_dom *dom, struct dt_ctx *ctx)
> +{
> + dom->ctx = ctx;
> + init_completion(&dom->entered);
> +}
> +
> +static int dt_init(struct kunit *test)
> +{
> + struct dt_ctx *ctx;
> +
> + ctx = kunit_kzalloc(test, sizeof(*ctx), GFP_KERNEL);
> + KUNIT_ASSERT_NOT_NULL(test, ctx);
> + test->priv = ctx;
> +
> + ctx->test = test;
> + dt_dom_init(&ctx->dom_a, ctx);
> + dt_dom_init(&ctx->dom_b, ctx);
> + dt_dom_init(&ctx->dom_e, ctx);
> + dt_dom_init(&ctx->dom_f, ctx);
> + dt_dom_init(&ctx->dom_g, ctx);
> + init_waitqueue_head(&ctx->wq);
> + ctx->debug_locks_at_start = debug_locks;
> +
> + /* Runs last, after the deletion of all objects */
> + KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test, dt_final_check,
> + ctx), 0);
> +
> + dt_dev_add(test, &ctx->a0, &ctx->dom_a, DT_CLOCK_ID_A, 0,
> + DPLL_TYPE_PPS);
> + dt_dev_add(test, &ctx->a1, &ctx->dom_a, DT_CLOCK_ID_A, 1,
> + DPLL_TYPE_EEC);
> + dt_dev_add(test, &ctx->b0, &ctx->dom_b, DT_CLOCK_ID_B, 0,
> + DPLL_TYPE_PPS);
> + ctx->a0.notify = &ctx->a1;
> +
> + dt_pin_add(test, &ctx->p, DT_CLOCK_ID_A, DT_PIN_IDX_P, &dt_prop);
> + dt_reg_add(test, &ctx->reg_p_a0, &ctx->dom_a, &ctx->p, &ctx->a0, NULL);
> + dt_reg_add(test, &ctx->reg_p_a1, &ctx->dom_a, &ctx->p, &ctx->a1, NULL);
> + ctx->reg_p_a0.notify = &ctx->s;
> +
> + dt_pin_add(test, &ctx->s, DT_CLOCK_ID_A, DT_PIN_IDX_S, &dt_prop);
> + dt_reg_add(test, &ctx->reg_s_a0, &ctx->dom_a, &ctx->s, &ctx->a0, NULL);
> + dt_reg_add(test, &ctx->reg_s_a1, &ctx->dom_a, &ctx->s, &ctx->a1, NULL);
> +
> + dt_pin_add(test, &ctx->bp, DT_CLOCK_ID_B, DT_PIN_IDX_BP, &dt_prop);
> + dt_reg_add(test, &ctx->reg_bp_b0, &ctx->dom_b, &ctx->bp, &ctx->b0,
> + NULL);
> +
> + ctx->nb.notifier_call = dt_notifier;
> + KUNIT_ASSERT_EQ(test, register_dpll_notifier(&ctx->nb), 0);
> + KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test,
> + dt_notifier_release,
> + ctx), 0);
> +
> + atomic_set(&ctx->peak_total, 0);
> + atomic_set(&ctx->dom_a.peak, 0);
> + atomic_set(&ctx->dom_b.peak, 0);
> +
> + return 0;
> +}
> +
> +static int dt_device_ntf(void *arg)
> +{
> + struct dt_dev *dev = arg;
> +
> + return dpll_device_change_ntf(dev->dpll);
> +}
> +
> +static int dt_pin_ntf(void *arg)
> +{
> + struct dt_pin *pin = arg;
> +
> + return dpll_pin_change_ntf(pin->pin);
> +}
> +
> +static int dt_extra_pin_get(void *arg)
> +{
> + struct dt_pin *pin = arg;
> +
> + pin->pin = dpll_pin_get(DT_CLOCK_ID_B, DT_PIN_IDX_EXTRA, THIS_MODULE,
> + &dt_prop, &pin->tracker);
> +
> + return PTR_ERR_OR_ZERO(pin->pin);
> +}
> +
> +static void dt_unblock(struct dt_ctx *ctx)
> +{
> + WRITE_ONCE(ctx->dom_a.block, false);
> + WRITE_ONCE(ctx->dom_b.block, false);
> + WRITE_ONCE(ctx->dom_e.block, false);
> + WRITE_ONCE(ctx->dom_f.block, false);
> + WRITE_ONCE(ctx->dom_g.block, false);
> + WRITE_ONCE(ctx->f0.block, false);
> + WRITE_ONCE(ctx->reg_p_a0.block_prio_set, false);
> + WRITE_ONCE(ctx->reg_x_e0.block_prio_set, false);
> + WRITE_ONCE(ctx->reg_x_f0.block_prio_set, false);
> + wake_up_all(&ctx->wq);
> +}
> +
> +static int dt_req_thread(void *data)
> +{
> + struct dt_req *req = data;
> +
> + req->ret = req->fn(req->arg);
> + kthread_complete_and_exit(&req->done, 0);
> +}
> +
> +/* Runs before the objects used by the request are released */
> +static void dt_req_release(void *data)
> +{
> + struct dt_req *req = data;
> +
> + dt_unblock(req->ctx);
> + if (!req->finished)
> + wait_for_completion(&req->done);
> +}
> +
> +static struct dt_req *dt_req_start(struct kunit *test, int (*fn)(void *arg),
> + void *arg)
> +{
> + struct task_struct *task;
> + struct dt_req *req;
> +
> + req = kunit_kzalloc(test, sizeof(*req), GFP_KERNEL);
> + KUNIT_ASSERT_NOT_NULL(test, req);
> +
> + req->ctx = test->priv;
> + req->fn = fn;
> + req->arg = arg;
> + init_completion(&req->done);
> +
> + task = kthread_run(dt_req_thread, req, "dpll-test");
> + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, task);
> + KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test, dt_req_release,
> + req), 0);
> +
> + return req;
> +}
> +
> +/*
> + * Consumes the completion, so it must not be waited for again. Assert it before
> + * reading what the request wrote.
> + */
> +static bool dt_done(struct dt_req *req, unsigned long timeout)
> +{
> + if (!req->finished && wait_for_completion_timeout(&req->done, timeout))
> + req->finished = true;
> +
> + return req->finished;
> +}
> +
> +/* Blocks a request of @dom in a callback, which holds the domain lock */
> +static struct dt_req *dt_block_in(struct kunit *test, struct dt_dom *dom,
> + int (*fn)(void *arg), void *arg)
> +{
> + struct dt_req *req;
> +
> + WRITE_ONCE(dom->block, true);
> + req = dt_req_start(test, fn, arg);
> + KUNIT_ASSERT_TRUE(test, wait_for_completion_timeout(&dom->entered,
> + DT_TIMEOUT));
> +
> + return req;
> +}
> +
> +/* Build a set request and pass it to the handlers genetlink would call */
> +static int dt_pin_set_fn(void *arg)
> +{
> + struct dt_pin_set_args *args = arg;
> + struct nlattr *attrs[DPLL_A_PIN_MAX + 1] = {};
> + struct genl_info info = {};
> + struct nlmsghdr *nlh;
> + struct sk_buff *skb;
> + struct nlattr *nest;
> + int i, ret;
> +
> + if (args->ctx)
> + WRITE_ONCE(args->ctx->set_task, current);
> +
> + skb = nlmsg_new(NLMSG_DEFAULT_SIZE, GFP_KERNEL);
> + if (!skb)
> + return -ENOMEM;
> + ret = -EMSGSIZE;
> + nlh = nlmsg_put(skb, 0, 0, NLMSG_MIN_TYPE, GENL_HDRLEN, 0);
> + if (!nlh || nla_put_u32(skb, DPLL_A_PIN_ID, args->pin->id))
> + goto out;
> + for (i = 0; i < args->num; i++) {
> + const struct dt_nest *n = &args->nests[i];
> +
> + nest = nla_nest_start(skb, n->type);
> + if (!nest ||
> + nla_put_u32(skb, DPLL_A_PIN_PARENT_ID, n->parent_id) ||
> + nla_put_u32(skb, n->attr, n->val))
> + goto out;
> + nla_nest_end(skb, nest);
> + }
> + nlmsg_end(skb, nlh);
> +
> + info.genlhdr = nlmsg_data(nlh);
> + info.attrs = attrs;
> + attrs[DPLL_A_PIN_ID] = nla_find(genlmsg_data(info.genlhdr),
> + genlmsg_len(info.genlhdr),
> + DPLL_A_PIN_ID);
> +
> + ret = dpll_pin_pre_doit(NULL, skb, &info);
> + if (!ret) {
> + ret = dpll_nl_pin_set_doit(skb, &info);
> + dpll_pin_post_doit(NULL, skb, &info);
> + }
> +out:
> + nlmsg_free(skb);
> +
> + return ret;
> +}
> +
> +static int dt_mode_set_fn(void *arg)
> +{
> + struct dt_mode_set_args *args = arg;
> + struct nlattr *attrs[DPLL_A_MAX + 1] = {};
> + struct genl_info info = {};
> + struct nlmsghdr *nlh;
> + struct sk_buff *skb;
> + int ret;
> +
> + skb = nlmsg_new(NLMSG_DEFAULT_SIZE, GFP_KERNEL);
> + if (!skb)
> + return -ENOMEM;
> + ret = -EMSGSIZE;
> + nlh = nlmsg_put(skb, 0, 0, NLMSG_MIN_TYPE, GENL_HDRLEN, 0);
> + if (!nlh || nla_put_u32(skb, DPLL_A_ID, args->dpll->id) ||
> + nla_put_u32(skb, DPLL_A_MODE, args->mode))
> + goto out;
> + nlmsg_end(skb, nlh);
> +
> + info.genlhdr = nlmsg_data(nlh);
> + info.attrs = attrs;
> + attrs[DPLL_A_ID] = nla_find(genlmsg_data(info.genlhdr),
> + genlmsg_len(info.genlhdr), DPLL_A_ID);
> +
> + ret = dpll_pre_doit(NULL, skb, &info);
> + if (!ret) {
> + ret = dpll_nl_device_set_doit(skb, &info);
> + dpll_post_doit(NULL, skb, &info);
> + }
> +out:
> + nlmsg_free(skb);
> +
> + return ret;
> +}
> +
> +/* Runs a request that should not block in a kthread and returns its result */
> +static int dt_run(struct kunit *test, int (*fn)(void *arg), void *arg)
> +{
> + struct dt_req *req = dt_req_start(test, fn, arg);
> +
> + KUNIT_ASSERT_TRUE(test, dt_done(req, DT_TIMEOUT));
> +
> + return req->ret;
> +}
> +
> +static struct dt_pin_set_args *dt_pin_set_args_alloc(struct kunit *test,
> + struct dpll_pin *pin,
> + int num)
> +{
> + struct dt_pin_set_args *args;
> +
> + args = kunit_kzalloc(test, sizeof(*args), GFP_KERNEL);
> + KUNIT_ASSERT_NOT_NULL(test, args);
> + args->pin = pin;
> + args->num = num;
> +
> + return args;
> +}
> +
> +static struct dt_pin_set_args *dt_pin_set_args_one(struct kunit *test,
> + struct dpll_pin *pin,
> + int type, u32 parent_id,
> + int attr, u32 val)
> +{
> + struct dt_pin_set_args *args = dt_pin_set_args_alloc(test, pin, 1);
> +
> + args->nests[0].type = type;
> + args->nests[0].parent_id = parent_id;
> + args->nests[0].attr = attr;
> + args->nests[0].val = val;
> +
> + return args;
> +}
> +
> +static int dt_pin_set_one(struct kunit *test, struct dpll_pin *pin, int type,
> + u32 parent_id, int attr, u32 val)
> +{
> + return dt_run(test, dt_pin_set_fn,
> + dt_pin_set_args_one(test, pin, type, parent_id, attr,
> + val));
> +}
> +
> +static void dt_expect_events(struct kunit *test, void * const *expected,
> + int num)
> +{
> + struct dt_ctx *ctx = test->priv;
> + int i;
> +
> + KUNIT_ASSERT_EQ(test, ctx->num_events, num);
> + for (i = 0; i < num; i++)
> + KUNIT_EXPECT_PTR_EQ(test, ctx->events[i], expected[i]);
> + KUNIT_EXPECT_EQ(test, atomic_read(&ctx->bad_ntf), 0);
> +}
> +
> +/* Objects with the same (module, clock_id) share a refcounted domain */
> +static void dt_test_domain_sharing(struct kunit *test)
> +{
> + struct dt_ctx *ctx = test->priv;
> + struct dpll_domain *a = ctx->a0.dpll->domain;
> + struct dpll_domain *b = ctx->b0.dpll->domain;
> +
> + KUNIT_EXPECT_PTR_EQ(test, ctx->a1.dpll->domain, a);
> + KUNIT_EXPECT_PTR_EQ(test, ctx->p.pin->domain, a);
> + KUNIT_EXPECT_PTR_EQ(test, ctx->bp.pin->domain, b);
> + KUNIT_EXPECT_PTR_NE(test, a, b);
> +
> + KUNIT_EXPECT_EQ(test, refcount_read(&a->refcount), 4);
> + KUNIT_EXPECT_EQ(test, refcount_read(&b->refcount), 2);
> +
> + KUNIT_ASSERT_EQ(test, dt_extra_pin_get(&ctx->extra), 0);
> + KUNIT_EXPECT_PTR_EQ(test, ctx->extra.pin->domain, b);
> + KUNIT_EXPECT_EQ(test, refcount_read(&b->refcount), 3);
> + dpll_pin_put(ctx->extra.pin, &ctx->extra.tracker);
> + KUNIT_EXPECT_EQ(test, refcount_read(&b->refcount), 2);
> +}
> +
> +/* The same clock_id used by another module means another domain */
> +static void dt_test_domain_per_module(struct kunit *test)
> +{
> + struct dt_ctx *ctx = test->priv;
> + static char other_module;
> + struct dpll_device *dpll;
> + dpll_tracker tracker;
> +
> + /* Never registered, so the core does not dereference the module */
> + dpll = dpll_device_get(DT_CLOCK_ID_A, 0, (struct module *)&other_module,
> + &tracker);
> + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, dpll);
> + KUNIT_EXPECT_PTR_NE(test, dpll, ctx->a0.dpll);
> + KUNIT_EXPECT_PTR_NE(test, dpll->domain, ctx->a0.dpll->domain);
> + dpll_device_put(dpll, &tracker);
> +}
> +
> +/* A pin on a dpll of another domain stays in its domain */
> +static void dt_test_foreign_registration_domain(struct kunit *test)
> +{
> + struct dt_ctx *ctx = test->priv;
> + struct dpll_domain *e, *f;
> +
> + dt_foreign_pin_add(test);
> + e = ctx->e0.dpll->domain;
> + f = ctx->f0.dpll->domain;
> +
> + KUNIT_EXPECT_PTR_NE(test, e, f);
> + KUNIT_EXPECT_PTR_EQ(test, ctx->x.pin->domain, e);
> + KUNIT_EXPECT_EQ(test, refcount_read(&e->refcount), 2);
> + KUNIT_EXPECT_EQ(test, refcount_read(&f->refcount), 1);
> +}
> +
> +/* A pin under a parent pin of another domain stays in its domain */
> +static void dt_test_on_pin_registration_domain(struct kunit *test)
> +{
> + struct dt_ctx *ctx = test->priv;
> + struct dpll_domain *e, *g;
> +
> + dt_on_pin_add(test);
> + e = ctx->e0.dpll->domain;
> + g = ctx->c.pin->domain;
> +
> + KUNIT_EXPECT_PTR_NE(test, e, g);
> + KUNIT_EXPECT_PTR_EQ(test, ctx->m.pin->domain, e);
> + KUNIT_EXPECT_EQ(test, refcount_read(&e->refcount), 2);
> + KUNIT_EXPECT_EQ(test, refcount_read(&g->refcount), 1);
> +}
> +
> +/* A callback blocked in one domain does not stop other domains */
> +static void dt_test_domains_run_in_parallel(struct kunit *test)
> +{
> + struct dt_ctx *ctx = test->priv;
> + struct dt_req *blocked, *other;
> +
> + blocked = dt_block_in(test, &ctx->dom_a, dt_device_ntf, &ctx->a0);
> +
> + other = dt_req_start(test, dt_device_ntf, &ctx->b0);
> + KUNIT_EXPECT_TRUE(test, dt_done(other, DT_TIMEOUT));
> + KUNIT_EXPECT_FALSE(test, completion_done(&blocked->done));
> + KUNIT_EXPECT_EQ(test, atomic_read(&ctx->peak_total), 2);
> +
> + dt_unblock(ctx);
> + KUNIT_EXPECT_TRUE(test, dt_done(blocked, DT_TIMEOUT));
> +}
> +
> +/* Callbacks of one domain are serialized, across its devices too */
> +static void dt_test_domain_is_serialized(struct kunit *test)
> +{
> + struct dt_ctx *ctx = test->priv;
> + struct dt_req *first, *second;
> +
> + first = dt_block_in(test, &ctx->dom_a, dt_device_ntf, &ctx->a0);
> +
> + second = dt_req_start(test, dt_device_ntf, &ctx->a1);
> + KUNIT_EXPECT_FALSE(test, dt_done(second, DT_SHORT_TIMEOUT));
> +
> + dt_unblock(ctx);
> + KUNIT_EXPECT_TRUE(test, dt_done(first, DT_TIMEOUT));
> + KUNIT_EXPECT_TRUE(test, dt_done(second, DT_TIMEOUT));
> + KUNIT_EXPECT_EQ(test, atomic_read(&ctx->dom_a.peak), 1);
> +}
> +
> +/* Callbacks of all refs of a pin run under the domain of the pin alone */
> +static void dt_test_foreign_get_locks(struct kunit *test)
> +{
> + struct dt_ctx *ctx = test->priv;
> +
> + if (!IS_ENABLED(CONFIG_LOCKDEP) || !debug_locks)
> + kunit_skip(test, "needs lockdep");
> +
> + dt_foreign_pin_add(test);
> +
> + KUNIT_EXPECT_EQ(test, dt_run(test, dt_pin_ntf, &ctx->x), 0);
> + KUNIT_EXPECT_EQ(test, atomic_read(&ctx->wrong_lock), 0);
> +}
> +
> +/* Setters of a foreign ref run under the domain of the pin alone */
> +static void dt_test_foreign_set_locks(struct kunit *test)
> +{
> + static const struct {
> + int attr;
> + u32 val;
> + } sets[] = {
> + { DPLL_A_PIN_STATE, DPLL_PIN_STATE_CONNECTED },
> + { DPLL_A_PIN_PRIO, 7 },
> + { DPLL_A_PIN_DIRECTION, DPLL_PIN_DIRECTION_OUTPUT },
> + };
> + struct dt_ctx *ctx = test->priv;
> + int i;
> +
> + dt_foreign_pin_add(test);
> +
> + for (i = 0; i < ARRAY_SIZE(sets); i++) {
> + int ret;
> +
> + ctx->num_events = 0;
> + ctx->record = true;
> + ret = dt_pin_set_one(test, ctx->x.pin, DPLL_A_PIN_PARENT_DEVICE,
> + ctx->f0.dpll->id, sets[i].attr,
> + sets[i].val);
> + ctx->record = false;
> +
> + KUNIT_EXPECT_EQ(test, ret, 0);
> + dt_expect_events(test, (void *[]){ ctx->x.pin }, 1);
> + }
> + KUNIT_EXPECT_EQ(test, ctx->reg_x_f0.state, DPLL_PIN_STATE_CONNECTED);
> + KUNIT_EXPECT_EQ(test, ctx->reg_x_f0.prio, 7);
> + KUNIT_EXPECT_EQ(test, ctx->reg_x_f0.direction,
> + DPLL_PIN_DIRECTION_OUTPUT);
> + KUNIT_EXPECT_EQ(test, ctx->reg_x_e0.prio, 0);
> +}
> +
> +/* The callback of a foreign ref can notify objects of the domain of the pin */
> +static void dt_test_foreign_callback_notifies_pin(struct kunit *test)
> +{
> + struct dt_ctx *ctx = test->priv;
> + int ret;
> +
> + dt_foreign_pin_add(test);
> + ctx->reg_x_f0.notify = &ctx->x;
> +
> + ctx->record = true;
> + ret = dt_pin_set_one(test, ctx->x.pin, DPLL_A_PIN_PARENT_DEVICE,
> + ctx->f0.dpll->id, DPLL_A_PIN_STATE,
> + DPLL_PIN_STATE_CONNECTED);
> + ctx->record = false;
> +
> + KUNIT_EXPECT_EQ(test, ret, 0);
> + dt_expect_events(test, (void *[]){ ctx->x.pin, ctx->x.pin }, 2);
> +}
> +
> +/* Without an owner the pin-level attributes come from the foreign ref */
> +static void dt_test_pin_without_owner(struct kunit *test)
> +{
> + struct dt_ctx *ctx = test->priv;
> +
> + dt_dev_add(test, &ctx->f0, &ctx->dom_f, DT_CLOCK_ID_F, 0,
> + DPLL_TYPE_PPS);
> + dt_fwnode_pin_add(test, &ctx->x, DT_CLOCK_ID_E, DT_PIN_IDX_X);
> + dt_reg_add(test, &ctx->reg_x_f0, &ctx->dom_f, &ctx->x, &ctx->f0, NULL);
> + KUNIT_ASSERT_PTR_NE(test, ctx->x.pin->domain, ctx->f0.dpll->domain);
> + ctx->reg_x_f0.forbid = ctx->f0.dpll->domain;
> +
> + atomic_set(&ctx->reg_x_f0.freq_gets, 0);
> + KUNIT_EXPECT_EQ(test, dt_run(test, dt_pin_ntf, &ctx->x), 0);
> + KUNIT_EXPECT_GT(test, atomic_read(&ctx->reg_x_f0.freq_gets), 0);
> + KUNIT_EXPECT_EQ(test, atomic_read(&ctx->wrong_lock), 0);
> +}
> +
> +/* A stuck callback of a foreign ref stops only the domain of the pin */
> +static void dt_test_stuck_foreign_callback(struct kunit *test)
> +{
> + struct dt_req *blocked, *owner, *foreign, *other;
> + struct dt_ctx *ctx = test->priv;
> +
> + dt_foreign_pin_add(test);
> +
> + WRITE_ONCE(ctx->reg_x_f0.block_prio_set, true);
> + blocked = dt_req_start(test, dt_pin_set_fn,
> + dt_pin_set_args_one(test, ctx->x.pin,
> + DPLL_A_PIN_PARENT_DEVICE,
> + ctx->f0.dpll->id,
> + DPLL_A_PIN_PRIO, 7));
> + KUNIT_ASSERT_TRUE(test, wait_for_completion_timeout(&ctx->dom_f.entered,
> + DT_TIMEOUT));
> +
> + owner = dt_req_start(test, dt_device_ntf, &ctx->e0);
> + KUNIT_EXPECT_FALSE(test, dt_done(owner, DT_SHORT_TIMEOUT));
> + foreign = dt_req_start(test, dt_device_ntf, &ctx->f0);
> + KUNIT_EXPECT_TRUE(test, dt_done(foreign, DT_TIMEOUT));
> + other = dt_req_start(test, dt_device_ntf, &ctx->b0);
> + KUNIT_EXPECT_TRUE(test, dt_done(other, DT_TIMEOUT));
> +
> + dt_unblock(ctx);
> + KUNIT_EXPECT_TRUE(test, dt_done(blocked, DT_TIMEOUT));
> + KUNIT_EXPECT_TRUE(test, dt_done(owner, DT_TIMEOUT));
> +}
> +
> +/* A stuck device callback does not stop a foreign pin registered with it */
> +static void dt_test_stuck_foreign_device(struct kunit *test)
> +{
> + struct dt_ctx *ctx = test->priv;
> + struct dt_req *blocked, *pin;
> +
> + dt_foreign_pin_add(test);
> +
> + WRITE_ONCE(ctx->f0.block, true);
> + blocked = dt_req_start(test, dt_device_ntf, &ctx->f0);
> + KUNIT_ASSERT_TRUE(test, wait_for_completion_timeout(&ctx->dom_f.entered,
> + DT_TIMEOUT));
> +
> + pin = dt_req_start(test, dt_pin_ntf, &ctx->x);
> + KUNIT_EXPECT_TRUE(test, dt_done(pin, DT_TIMEOUT));
> +
> + dt_unblock(ctx);
> + KUNIT_EXPECT_TRUE(test, dt_done(blocked, DT_TIMEOUT));
> +}
> +
> +/* A stuck owner stops requests for its pins, but not the foreign device */
> +static void dt_test_stuck_owner_domain(struct kunit *test)
> +{
> + struct dt_ctx *ctx = test->priv;
> + struct dt_req *blocked, *foreign, *pin;
> +
> + dt_foreign_pin_add(test);
> +
> + blocked = dt_block_in(test, &ctx->dom_e, dt_device_ntf, &ctx->e0);
> +
> + foreign = dt_req_start(test, dt_device_ntf, &ctx->f0);
> + KUNIT_EXPECT_TRUE(test, dt_done(foreign, DT_TIMEOUT));
> + pin = dt_req_start(test, dt_pin_ntf, &ctx->x);
> + KUNIT_EXPECT_FALSE(test, dt_done(pin, DT_SHORT_TIMEOUT));
> +
> + dt_unblock(ctx);
> + KUNIT_EXPECT_TRUE(test, dt_done(blocked, DT_TIMEOUT));
> + KUNIT_EXPECT_TRUE(test, dt_done(pin, DT_TIMEOUT));
> +}
> +
> +/* A pin and its parent pin of another domain do not block each other */
> +static void dt_test_on_pin_domains_are_independent(struct kunit *test)
> +{
> + struct dt_ctx *ctx = test->priv;
> + struct dt_req *blocked, *other;
> +
> + dt_on_pin_add(test);
> +
> + blocked = dt_block_in(test, &ctx->dom_e, dt_device_ntf, &ctx->e0);
> + other = dt_req_start(test, dt_pin_ntf, &ctx->c);
> + KUNIT_EXPECT_TRUE(test, dt_done(other, DT_TIMEOUT));
> + dt_unblock(ctx);
> + KUNIT_EXPECT_TRUE(test, dt_done(blocked, DT_TIMEOUT));
> +
> + blocked = dt_block_in(test, &ctx->dom_g, dt_pin_ntf, &ctx->c);
> + other = dt_req_start(test, dt_pin_ntf, &ctx->m);
> + KUNIT_EXPECT_TRUE(test, dt_done(other, DT_TIMEOUT));
> + dt_unblock(ctx);
> + KUNIT_EXPECT_TRUE(test, dt_done(blocked, DT_TIMEOUT));
> +}
> +
> +/* Registrations in both directions between two domains are fine */
> +static void dt_test_registrations_both_directions(struct kunit *test)
> +{
> + struct dt_ctx *ctx = test->priv;
> +
> + dt_foreign_pin_add(test);
> + dt_fwnode_pin_add(test, &ctx->y, DT_CLOCK_ID_F, DT_PIN_IDX_Y);
> + dt_reg_add(test, &ctx->reg_y_f0, &ctx->dom_f, &ctx->y, &ctx->f0, NULL);
> + dt_reg_add(test, &ctx->reg_y_e0, &ctx->dom_e, &ctx->y, &ctx->e0, NULL);
> + ctx->reg_y_f0.forbid = ctx->e0.dpll->domain;
> + ctx->reg_y_e0.forbid = ctx->e0.dpll->domain;
> +
> + KUNIT_EXPECT_EQ(test, dt_run(test, dt_pin_ntf, &ctx->x), 0);
> + KUNIT_EXPECT_EQ(test, dt_run(test, dt_pin_ntf, &ctx->y), 0);
> +}
> +
> +/* A topology change waits until running requests finish */
> +static void dt_test_topology_change_waits(struct kunit *test)
> +{
> + struct dt_ctx *ctx = test->priv;
> + struct dt_req *blocked, *writer;
> +
> + blocked = dt_block_in(test, &ctx->dom_a, dt_device_ntf, &ctx->a0);
> +
> + /* Added before the request starts, so it runs after waiting for it */
> + KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test, dt_pin_release,
> + &ctx->extra), 0);
> + writer = dt_req_start(test, dt_extra_pin_get, &ctx->extra);
> + KUNIT_EXPECT_FALSE(test, dt_done(writer, DT_SHORT_TIMEOUT));
> +
> + dt_unblock(ctx);
> + KUNIT_EXPECT_TRUE(test, dt_done(blocked, DT_TIMEOUT));
> + KUNIT_ASSERT_TRUE(test, dt_done(writer, DT_TIMEOUT));
> + KUNIT_EXPECT_NOT_ERR_OR_NULL(test, ctx->extra.pin);
> +}
> +
> +/* Other requests about a pin cannot see a set of two attributes half done */
> +static void dt_check_request_atomic(struct kunit *test, struct dt_pin *pin,
> + struct dt_dev *dev0, struct dt_reg *reg0,
> + struct dt_dev *dev1, struct dt_reg *reg1)
> +{
> + struct dt_pin_set_args *args = dt_pin_set_args_alloc(test, pin->pin, 2);
> + struct dt_ctx *ctx = test->priv;
> + struct dt_req *set, *other;
> + int i;
> +
> + args->ctx = ctx;
> + for (i = 0; i < args->num; i++) {
> + args->nests[i].type = DPLL_A_PIN_PARENT_DEVICE;
> + args->nests[i].parent_id = i ? dev1->dpll->id : dev0->dpll->id;
> + args->nests[i].attr = DPLL_A_PIN_PRIO;
> + args->nests[i].val = 11;
> + }
> +
> + ctx->atomic_regs[0] = reg0;
> + ctx->atomic_regs[1] = reg1;
> +
> + /* The other request starts to wait before the first attribute is set */
> + WRITE_ONCE(reg0->block_prio_set, true);
> + set = dt_req_start(test, dt_pin_set_fn, args);
> + KUNIT_ASSERT_TRUE(test, wait_for_completion_timeout(®0->dom->entered,
> + DT_TIMEOUT));
> + other = dt_req_start(test, dt_pin_ntf, pin);
> + KUNIT_EXPECT_FALSE(test, dt_done(other, DT_SHORT_TIMEOUT));
> +
> + dt_unblock(ctx);
> + KUNIT_ASSERT_TRUE(test, dt_done(set, DT_TIMEOUT));
> + KUNIT_ASSERT_TRUE(test, dt_done(other, DT_TIMEOUT));
> + KUNIT_EXPECT_EQ(test, set->ret, 0);
> + KUNIT_EXPECT_EQ(test, reg0->prio, 11);
> + KUNIT_EXPECT_EQ(test, reg1->prio, 11);
> + KUNIT_EXPECT_EQ(test, atomic_read(&ctx->torn), 0);
> +}
> +
> +static void dt_test_request_is_atomic(struct kunit *test)
> +{
> + struct dt_ctx *ctx = test->priv;
> +
> + dt_check_request_atomic(test, &ctx->p, &ctx->a0, &ctx->reg_p_a0,
> + &ctx->a1, &ctx->reg_p_a1);
> +}
> +
> +static void dt_test_spanning_request_is_atomic(struct kunit *test)
> +{
> + struct dt_ctx *ctx = test->priv;
> +
> + dt_foreign_pin_add(test);
> + dt_check_request_atomic(test, &ctx->x, &ctx->e0, &ctx->reg_x_e0,
> + &ctx->f0, &ctx->reg_x_f0);
> +}
> +
> +/* Notifications from a callback come first, under the domain lock */
> +static void dt_test_ntf_order(struct kunit *test)
> +{
> + struct dt_ctx *ctx = test->priv;
> + int ret;
> +
> + ctx->record = true;
> + ret = dt_pin_set_one(test, ctx->p.pin, DPLL_A_PIN_PARENT_DEVICE,
> + ctx->a0.dpll->id, DPLL_A_PIN_STATE,
> + DPLL_PIN_STATE_CONNECTED);
> + ctx->record = false;
> +
> + KUNIT_EXPECT_EQ(test, ret, 0);
> + KUNIT_EXPECT_EQ(test, ctx->reg_p_a0.state, DPLL_PIN_STATE_CONNECTED);
> + dt_expect_events(test, (void *[]){ ctx->s.pin, ctx->p.pin }, 2);
> +}
> +
> +/* A notification from a callback that fails is still sent */
> +static void dt_test_ntf_on_failure(struct kunit *test)
> +{
> + struct dt_ctx *ctx = test->priv;
> + int ret;
> +
> + ctx->fail_after_ntf = true;
> + ctx->record = true;
> + ret = dt_pin_set_one(test, ctx->p.pin, DPLL_A_PIN_PARENT_DEVICE,
> + ctx->a0.dpll->id, DPLL_A_PIN_STATE,
> + DPLL_PIN_STATE_CONNECTED);
> + ctx->record = false;
> +
> + KUNIT_EXPECT_EQ(test, ret, -EIO);
> + dt_expect_events(test, (void *[]){ ctx->s.pin }, 1);
> +}
> +
> +/* A set on a pin under a parent of another domain needs only its domain */
> +static void dt_test_set_on_pin(struct kunit *test)
> +{
> + struct dt_ctx *ctx = test->priv;
> + int ret;
> +
> + dt_on_pin_add(test);
> +
> + ctx->record = true;
> + ret = dt_pin_set_one(test, ctx->c.pin, DPLL_A_PIN_PARENT_PIN,
> + ctx->m.pin->id, DPLL_A_PIN_STATE,
> + DPLL_PIN_STATE_CONNECTED);
> + ctx->record = false;
> +
> + KUNIT_EXPECT_EQ(test, ret, 0);
> + KUNIT_EXPECT_EQ(test, ctx->reg_c_m.state, DPLL_PIN_STATE_CONNECTED);
> + dt_expect_events(test, (void *[]){ ctx->c.pin }, 1);
> +}
> +
> +/* A device callback notifies a sibling device of its domain */
> +static void dt_test_device_set_ntf(struct kunit *test)
> +{
> + struct dt_ctx *ctx = test->priv;
> + struct dt_mode_set_args *args;
> + int ret;
> +
> + args = kunit_kzalloc(test, sizeof(*args), GFP_KERNEL);
> + KUNIT_ASSERT_NOT_NULL(test, args);
> + args->dpll = ctx->a0.dpll;
> + args->mode = DPLL_MODE_MANUAL;
> +
> + ctx->record = true;
> + ret = dt_run(test, dt_mode_set_fn, args);
> + ctx->record = false;
> +
> + KUNIT_EXPECT_EQ(test, ret, 0);
> + KUNIT_EXPECT_EQ(test, ctx->a0.mode, DPLL_MODE_MANUAL);
> + dt_expect_events(test, (void *[]){ ctx->a1.dpll }, 1);
> +}
> +
> +/* A change of a reference sync pair notifies under the domain of the pin */
> +static void dt_test_ref_sync_pair(struct kunit *test)
> +{
> + struct dt_ctx *ctx = test->priv;
> + int ret;
> +
> + ctx->record = true;
> + ret = dpll_pin_ref_sync_pair_add(ctx->p.pin, ctx->s.pin);
> + ctx->record = false;
> +
> + KUNIT_EXPECT_EQ(test, ret, 0);
> + dt_expect_events(test, (void *[]){ ctx->p.pin }, 1);
> +}
> +
> +static struct kunit_case dt_cases[] = {
> + KUNIT_CASE(dt_test_domain_sharing),
> + KUNIT_CASE(dt_test_domain_per_module),
> + KUNIT_CASE(dt_test_foreign_registration_domain),
> + KUNIT_CASE(dt_test_on_pin_registration_domain),
> + KUNIT_CASE(dt_test_domains_run_in_parallel),
> + KUNIT_CASE(dt_test_domain_is_serialized),
> + KUNIT_CASE(dt_test_foreign_get_locks),
> + KUNIT_CASE(dt_test_foreign_set_locks),
> + KUNIT_CASE(dt_test_foreign_callback_notifies_pin),
> + KUNIT_CASE(dt_test_pin_without_owner),
> + KUNIT_CASE(dt_test_stuck_foreign_callback),
> + KUNIT_CASE(dt_test_stuck_foreign_device),
> + KUNIT_CASE(dt_test_stuck_owner_domain),
> + KUNIT_CASE(dt_test_on_pin_domains_are_independent),
> + KUNIT_CASE(dt_test_registrations_both_directions),
> + KUNIT_CASE(dt_test_topology_change_waits),
> + KUNIT_CASE(dt_test_request_is_atomic),
> + KUNIT_CASE(dt_test_spanning_request_is_atomic),
> + KUNIT_CASE(dt_test_ntf_order),
> + KUNIT_CASE(dt_test_ntf_on_failure),
> + KUNIT_CASE(dt_test_set_on_pin),
> + KUNIT_CASE(dt_test_device_set_ntf),
> + KUNIT_CASE(dt_test_ref_sync_pair),
> + {}
> +};
> +
> +static struct kunit_suite dt_suite = {
> + .name = "dpll_core",
> + .init = dt_init,
> + .test_cases = dt_cases,
> +};
> +
> +kunit_test_suite(dt_suite);
> +
> +MODULE_DESCRIPTION("KUnit tests for locking in the DPLL core");
> +MODULE_LICENSE("GPL");
> diff --git a/drivers/dpll/dpll_netlink.c b/drivers/dpll/dpll_netlink.c
> index 9d8f5d1ce97248..223cd1de905c1a 100644
> --- a/drivers/dpll/dpll_netlink.c
> +++ b/drivers/dpll/dpll_netlink.c
> @@ -6,6 +6,7 @@
> * Copyright (c) 2023 Intel and affiliates
> *
> */
> +#include <kunit/visibility.h>
> #include <linux/module.h>
> #include <linux/kernel.h>
> #include <linux/netdevice.h>
> @@ -1836,6 +1837,7 @@ int dpll_nl_pin_set_doit(struct sk_buff *skb, struct genl_info *info)
>
> return dpll_pin_set_from_nlattr(pin, info);
> }
> +EXPORT_SYMBOL_IF_KUNIT(dpll_nl_pin_set_doit);
>
> static struct dpll_device *
> dpll_device_find(u64 clock_id, struct nlattr *mod_name_attr,
> @@ -2010,6 +2012,7 @@ int dpll_nl_device_set_doit(struct sk_buff *skb, struct genl_info *info)
>
> return dpll_set_from_nlattr(dpll, info);
> }
> +EXPORT_SYMBOL_IF_KUNIT(dpll_nl_device_set_doit);
>
> int dpll_nl_device_get_dumpit(struct sk_buff *skb, struct netlink_callback *cb)
> {
> @@ -2070,6 +2073,7 @@ int dpll_pre_doit(const struct genl_split_ops *ops, struct sk_buff *skb,
> up_read(&dpll_lock);
> return -ENODEV;
> }
> +EXPORT_SYMBOL_IF_KUNIT(dpll_pre_doit);
>
> void dpll_post_doit(const struct genl_split_ops *ops, struct sk_buff *skb,
> struct genl_info *info)
> @@ -2079,6 +2083,7 @@ void dpll_post_doit(const struct genl_split_ops *ops, struct sk_buff *skb,
> mutex_unlock(&dpll->domain->lock);
> up_read(&dpll_lock);
> }
> +EXPORT_SYMBOL_IF_KUNIT(dpll_post_doit);
>
> int
> dpll_lock_doit(const struct genl_split_ops *ops, struct sk_buff *skb,
> @@ -2122,6 +2127,7 @@ int dpll_pin_pre_doit(const struct genl_split_ops *ops, struct sk_buff *skb,
> up_read(&dpll_lock);
> return ret;
> }
> +EXPORT_SYMBOL_IF_KUNIT(dpll_pin_pre_doit);
>
> void dpll_pin_post_doit(const struct genl_split_ops *ops, struct sk_buff *skb,
> struct genl_info *info)
> @@ -2131,3 +2137,4 @@ void dpll_pin_post_doit(const struct genl_split_ops *ops, struct sk_buff *skb,
> mutex_unlock(&pin->domain->lock);
> up_read(&dpll_lock);
> }
> +EXPORT_SYMBOL_IF_KUNIT(dpll_pin_post_doit);
Reviewed-by: Petr Oros <poros@redhat.com>
^ permalink raw reply [flat|nested] 27+ messages in thread
* Re: [PATCH net-next 07/10] dpll: reject measured_freq_get for foreign and on-pin registrations
2026-11-06 12:10 ` [PATCH net-next 07/10] dpll: reject measured_freq_get for foreign and on-pin registrations Ivan Vecera
@ 2026-11-10 12:24 ` Petr Oros
0 siblings, 0 replies; 27+ messages in thread
From: Petr Oros @ 2026-11-10 12:24 UTC (permalink / raw)
To: Ivan Vecera, netdev
Cc: Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
Jonathan Corbet, Shuah Khan, Randy Dunlap, Min Li, Tony Nguyen,
Przemek Kitszel, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Richard Cochran, linux-doc,
linux-kernel, intel-wired-lan, Grzegorz Nitka, Karol Kolacinski,
Saeed Mahameed, Tariq Toukan
On 11/6/26 1:10 PM, Ivan Vecera wrote:
> A message about a pin with .measured_freq_get calls .freq_monitor_get of
> a dpll of the pin. For a pin of another domain, this device callback
> would run outside the domain of the dpll, so dpll_pin_register() now
> rejects such ops.
>
> dpll_pin_on_pin_register() checks nothing about them. The message would
> call .freq_monitor_get with no NULL check, on a dpll of the parent pin
> that can belong to another driver, so reject them there as well.
>
> No in-tree driver passes such ops. Add KUnit tests: the ops work on a dpll
> of the domain of the pin, and are rejected on a dpll of another domain and
> under a parent pin.
>
> Signed-off-by: Ivan Vecera <ivecera@redhat.com>
> ---
> drivers/dpll/dpll_core.c | 6 +-
> drivers/dpll/dpll_core_kunit.c | 112 +++++++++++++++++++++++++++++++--
> 2 files changed, 111 insertions(+), 7 deletions(-)
>
> diff --git a/drivers/dpll/dpll_core.c b/drivers/dpll/dpll_core.c
> index 8eb74682343156..84377f8ba16036 100644
> --- a/drivers/dpll/dpll_core.c
> +++ b/drivers/dpll/dpll_core.c
> @@ -958,7 +958,8 @@ dpll_pin_register(struct dpll_device *dpll, struct dpll_pin *pin,
>
> dev_ops = dpll_device_ops(dpll);
> if (WARN_ON(ops->measured_freq_get &&
> - (!dev_ops || !dev_ops->freq_monitor_get ||
> + (pin->domain != dpll->domain || !dev_ops ||
> + !dev_ops->freq_monitor_get ||
> !dev_ops->freq_monitor_set))) {
> ret = -EINVAL;
> goto out_unlock;
> @@ -1065,7 +1066,8 @@ int dpll_pin_on_pin_register(struct dpll_pin *parent, struct dpll_pin *pin,
>
> if (WARN_ON(!ops) ||
> WARN_ON(!ops->state_on_pin_get) ||
> - WARN_ON(!ops->direction_get))
> + WARN_ON(!ops->direction_get) ||
> + WARN_ON(ops->measured_freq_get))
> return -EINVAL;
>
> down_write(&dpll_lock);
> diff --git a/drivers/dpll/dpll_core_kunit.c b/drivers/dpll/dpll_core_kunit.c
> index a58800eda5ff51..84c405f855d8cb 100644
> --- a/drivers/dpll/dpll_core_kunit.c
> +++ b/drivers/dpll/dpll_core_kunit.c
> @@ -87,6 +87,7 @@ struct dt_reg {
> struct dt_dev *dev;
> struct dt_pin *parent;
> struct dt_pin *notify;
> + const struct dpll_pin_ops *ops;
> struct dpll_domain *forbid;
> enum dpll_pin_state state;
> enum dpll_pin_direction direction;
> @@ -276,11 +277,33 @@ static int dt_lock_status_get(const struct dpll_device *dpll, void *priv,
> return 0;
> }
>
> +static int dt_freq_monitor_set(const struct dpll_device *dpll, void *priv,
> + enum dpll_feature_state state,
> + struct netlink_ext_ack *extack)
> +{
> + return 0;
> +}
> +
> +static int dt_freq_monitor_get(const struct dpll_device *dpll, void *priv,
> + enum dpll_feature_state *state,
> + struct netlink_ext_ack *extack)
> +{
> + struct dt_dev *dev = priv;
> +
> + dt_dev_enter(dev, dpll);
> + *state = DPLL_FEATURE_STATE_ENABLE;
> + dt_leave(dev->dom);
> +
> + return 0;
> +}
> +
> static const struct dpll_device_ops dt_dev_ops = {
> .mode_get = dt_mode_get,
> .mode_set = dt_mode_set,
> .supported_modes_get = dt_supported_modes_get,
> .lock_status_get = dt_lock_status_get,
> + .freq_monitor_set = dt_freq_monitor_set,
> + .freq_monitor_get = dt_freq_monitor_get,
> };
>
> static int dt_frequency_get(const struct dpll_pin *pin, void *pin_priv,
> @@ -433,6 +456,21 @@ static int dt_ref_sync_get(const struct dpll_pin *pin, void *pin_priv,
> return 0;
> }
>
> +static int dt_measured_freq_get(const struct dpll_pin *pin, void *pin_priv,
> + const struct dpll_device *dpll, void *dpll_priv,
> + u64 *measured_freq,
> + struct netlink_ext_ack *extack)
> +{
> + struct dt_reg *reg = pin_priv;
> +
> + dt_pin_enter(reg, pin);
> + atomic_inc(®->freq_gets);
> + *measured_freq = 1;
> + dt_leave(reg->dom);
> +
> + return 0;
> +}
> +
> static const struct dpll_pin_ops dt_pin_ops = {
> .frequency_get = dt_frequency_get,
> .state_on_dpll_get = dt_state_on_dpll_get,
> @@ -446,6 +484,13 @@ static const struct dpll_pin_ops dt_pin_ops = {
> .ref_sync_get = dt_ref_sync_get,
> };
>
> +static const struct dpll_pin_ops dt_pin_ops_measured = {
> + .measured_freq_get = dt_measured_freq_get,
> + .state_on_dpll_get = dt_state_on_dpll_get,
> + .state_on_pin_get = dt_state_on_pin_get,
> + .direction_get = dt_direction_get,
> +};
> +
> static const struct dpll_pin_properties dt_prop = {
> .type = DPLL_PIN_TYPE_EXT,
> .capabilities = DPLL_PIN_CAPABILITIES_STATE_CAN_CHANGE |
> @@ -570,9 +615,9 @@ static void dt_reg_release(void *data)
>
> if (reg->parent)
> dpll_pin_on_pin_unregister(reg->parent->pin, reg->pin->pin,
> - &dt_pin_ops, reg);
> + reg->ops, reg);
> else
> - dpll_pin_unregister(reg->dev->dpll, reg->pin->pin, &dt_pin_ops,
> + dpll_pin_unregister(reg->dev->dpll, reg->pin->pin, reg->ops,
> reg);
> }
>
> @@ -588,12 +633,14 @@ static void dt_reg_add(struct kunit *test, struct dt_reg *reg,
> reg->parent = parent;
> reg->state = DPLL_PIN_STATE_SELECTABLE;
> reg->direction = DPLL_PIN_DIRECTION_INPUT;
> + if (!reg->ops)
> + reg->ops = &dt_pin_ops;
>
> if (parent)
> - ret = dpll_pin_on_pin_register(parent->pin, pin->pin,
> - &dt_pin_ops, reg);
> + ret = dpll_pin_on_pin_register(parent->pin, pin->pin, reg->ops,
> + reg);
> else
> - ret = dpll_pin_register(dev->dpll, pin->pin, &dt_pin_ops, reg);
> + ret = dpll_pin_register(dev->dpll, pin->pin, reg->ops, reg);
> KUNIT_ASSERT_EQ(test, ret, 0);
> KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test, dt_reg_release,
> reg), 0);
> @@ -1432,6 +1479,59 @@ static void dt_test_ref_sync_pair(struct kunit *test)
> dt_expect_events(test, (void *[]){ ctx->p.pin }, 1);
> }
>
> +/* A pin with a measured frequency must be in the domain of its dpll */
> +static void dt_test_measured_freq_foreign(struct kunit *test)
> +{
> + struct kunit_suppressed_warning *w;
> + struct dt_ctx *ctx = test->priv;
> + struct dt_reg *reg;
> + int ret;
> +
> + dt_dev_add(test, &ctx->e0, &ctx->dom_e, DT_CLOCK_ID_E, 0,
> + DPLL_TYPE_PPS);
> + dt_dev_add(test, &ctx->f0, &ctx->dom_f, DT_CLOCK_ID_F, 0,
> + DPLL_TYPE_PPS);
> + dt_fwnode_pin_add(test, &ctx->x, DT_CLOCK_ID_E, DT_PIN_IDX_X);
> +
> + reg = &ctx->reg_x_e0;
> + reg->ops = &dt_pin_ops_measured;
> + dt_reg_add(test, reg, &ctx->dom_e, &ctx->x, &ctx->e0, NULL);
> + atomic_set(®->freq_gets, 0);
> + KUNIT_EXPECT_EQ(test, dt_run(test, dt_pin_ntf, &ctx->x), 0);
> + KUNIT_EXPECT_GT(test, atomic_read(®->freq_gets), 0);
> +
> + w = kunit_start_suppress_warning(test);
> + ret = dpll_pin_register(ctx->f0.dpll, ctx->x.pin, reg->ops, reg);
> + kunit_end_suppress_warning(test, w);
> +
> + KUNIT_EXPECT_EQ(test, ret, -EINVAL);
> + if (!ret)
> + dpll_pin_unregister(ctx->f0.dpll, ctx->x.pin, reg->ops, reg);
> +}
> +
> +/* A pin under a parent pin cannot have a measured frequency */
> +static void dt_test_measured_freq_on_pin(struct kunit *test)
> +{
> + struct kunit_suppressed_warning *w;
> + struct dt_ctx *ctx = test->priv;
> + struct dt_reg *reg;
> + int ret;
> +
> + dt_on_pin_add(test);
> + dt_pin_add(test, &ctx->y, DT_CLOCK_ID_G, DT_PIN_IDX_Y, &dt_prop);
> + reg = &ctx->reg_c_m;
> +
> + w = kunit_start_suppress_warning(test);
> + ret = dpll_pin_on_pin_register(ctx->m.pin, ctx->y.pin,
> + &dt_pin_ops_measured, reg);
> + kunit_end_suppress_warning(test, w);
> +
> + KUNIT_EXPECT_EQ(test, ret, -EINVAL);
> + if (!ret)
> + dpll_pin_on_pin_unregister(ctx->m.pin, ctx->y.pin,
> + &dt_pin_ops_measured, reg);
> +}
> +
> static struct kunit_case dt_cases[] = {
> KUNIT_CASE(dt_test_domain_sharing),
> KUNIT_CASE(dt_test_domain_per_module),
> @@ -1456,6 +1556,8 @@ static struct kunit_case dt_cases[] = {
> KUNIT_CASE(dt_test_set_on_pin),
> KUNIT_CASE(dt_test_device_set_ntf),
> KUNIT_CASE(dt_test_ref_sync_pair),
> + KUNIT_CASE(dt_test_measured_freq_foreign),
> + KUNIT_CASE(dt_test_measured_freq_on_pin),
> {}
> };
>
Reviewed-by: Petr Oros <poros@redhat.com>
^ permalink raw reply [flat|nested] 27+ messages in thread
* Re: [PATCH net-next 08/10] dpll: zl3073x: update locking comments
2026-11-06 12:10 ` [PATCH net-next 08/10] dpll: zl3073x: update locking comments Ivan Vecera
@ 2026-11-10 12:24 ` Petr Oros
0 siblings, 0 replies; 27+ messages in thread
From: Petr Oros @ 2026-11-10 12:24 UTC (permalink / raw)
To: Ivan Vecera, netdev
Cc: Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
Jonathan Corbet, Shuah Khan, Randy Dunlap, Min Li, Tony Nguyen,
Przemek Kitszel, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Richard Cochran, linux-doc,
linux-kernel, intel-wired-lan, Grzegorz Nitka, Karol Kolacinski,
Saeed Mahameed, Tariq Toukan
On 11/6/26 1:10 PM, Ivan Vecera wrote:
> Callbacks are serialized by the DPLL core domain lock instead of dpll_lock.
> All channels of a chip belong to one domain, so what the comments rely on
> still holds.
>
> Signed-off-by: Ivan Vecera <ivecera@redhat.com>
> ---
> drivers/dpll/zl3073x/dpll.c | 9 +++++----
> 1 file changed, 5 insertions(+), 4 deletions(-)
>
> diff --git a/drivers/dpll/zl3073x/dpll.c b/drivers/dpll/zl3073x/dpll.c
> index 1cdadb8e46fb2d..6e5e2bb41f55d6 100644
> --- a/drivers/dpll/zl3073x/dpll.c
> +++ b/drivers/dpll/zl3073x/dpll.c
> @@ -349,9 +349,10 @@ zl3073x_dpll_input_pin_ref_sync_set(const struct dpll_pin *dpll_pin,
> if (rc)
> goto unlock;
>
> - /* All code paths accessing per-channel reference priorities are
> - * serialized by the subsystem dpll_lock, so it is safe to release
> - * our lock here before iterating over the other channels.
> + /* All callbacks accessing per-channel reference priorities are
> + * serialized by the DPLL core domain lock shared by all channels of
> + * the chip, so it is safe to release our lock here before iterating
> + * over the other channels.
> */
> mutex_unlock(&zldpll->lock);
>
> @@ -2401,7 +2402,7 @@ zl3073x_dpll_changes_check(struct zl3073x_dpll *zldpll)
> mutex_unlock(&zldpll->lock);
>
> /* Send notifications outside the lock to avoid ABBA deadlock
> - * with dpll_lock taken by notification functions.
> + * with the DPLL core domain lock taken by notification functions.
> */
> if (dev_changed)
> dpll_device_change_ntf(zldpll->dpll_dev);
Reviewed-by: Petr Oros <poros@redhat.com>
^ permalink raw reply [flat|nested] 27+ messages in thread
* Re: [PATCH net-next 10/10] dpll: document locking rules
2026-11-06 12:10 ` [PATCH net-next 10/10] dpll: document locking rules Ivan Vecera
@ 2026-11-10 12:25 ` Petr Oros
2026-11-10 17:53 ` netdev-bot+sashiko
1 sibling, 0 replies; 27+ messages in thread
From: Petr Oros @ 2026-11-10 12:25 UTC (permalink / raw)
To: Ivan Vecera, netdev
Cc: Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
Jonathan Corbet, Shuah Khan, Randy Dunlap, Min Li, Tony Nguyen,
Przemek Kitszel, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Richard Cochran, linux-doc,
linux-kernel, intel-wired-lan, Grzegorz Nitka, Karol Kolacinski,
Saeed Mahameed, Tariq Toukan
On 11/6/26 1:10 PM, Ivan Vecera wrote:
> Describe the lock levels of the subsystem, which domain a callback runs
> under and the rules device drivers have to follow.
>
> Signed-off-by: Ivan Vecera <ivecera@redhat.com>
> ---
> Documentation/driver-api/dpll.rst | 44 +++++++++++++++++++++++++++++++
> drivers/dpll/dpll_core.c | 9 +++++++
> 2 files changed, 53 insertions(+)
>
> diff --git a/Documentation/driver-api/dpll.rst b/Documentation/driver-api/dpll.rst
> index 7c117ae37cc1bc..40d5037ff3bb0f 100644
> --- a/Documentation/driver-api/dpll.rst
> +++ b/Documentation/driver-api/dpll.rst
> @@ -675,6 +675,50 @@ In the error path we have to rewind every allocation in the reverse order:
>
> More complex example can be found in Intel's ICE driver or nVidia's mlx5 driver.
>
> +Locking
> +=======
> +
> +The subsystem uses two levels of locks, always taken in this order:
> +
> +- ``dpll_lock`` protects the topology of dpll devices and pins and their
> + lifetime. Functions changing the topology, for example dpll_pin_register(),
> + take it for write and send the create and delete notifications under it.
> + Netlink requests, each part of a dump and change notifications take it for
> + read for their whole duration.
> +- A domain lock serializes driver callbacks. All dpll devices and pins created
> + with the same module and ``clock_id`` belong to one domain, in practice to
> + one chip or card.
> +
> +A netlink request addressing a device or a pin by its ID holds the domain lock
> +of that object from start to end. A notification holds it while its message is
> +built and sent, so messages of one object are sent in the order they were
> +built. Only one domain lock is held at a time, so domains have no lock order,
> +and callbacks of different domains can run concurrently.
> +
> +Callbacks of a dpll device run under the domain of the device. Callbacks of a
> +pin run under the domain of the pin, whichever driver registered them:
> +
> +- A driver registering a pin of another driver with its dpll device has these
> + callbacks serialized with the driver that created the pin, not with the other
> + callbacks of its device, so it has to protect the state they share by itself.
> + A callback stuck in such a registration stops the domain of the pin.
> +- The parent pin of a pin registered with dpll_pin_on_pin_register() and its
> + dpll devices can belong to another driver, so the callback must not
> + dereference their private data.
> +- The frequency monitor belongs to the dpll device, so only callbacks of a pin
> + of the same domain registered with dpll_pin_register() can include
> + ``.measured_freq_get``.
> +
> +Device drivers have to follow these rules:
> +
> +- A callback must not call dpll_device_change_ntf(), dpll_pin_change_ntf() or
> + any function changing the topology.
> +- A callback notifies about objects of the domain it runs under with
> + __dpll_device_change_ntf() or __dpll_pin_change_ntf(). Other objects have to
> + be notified from a context that holds no domain lock, for example a work item.
> +- A driver must not call dpll_device_change_ntf(), dpll_pin_change_ntf() or any
> + function changing the topology while holding a lock that its callbacks take.
> +
> SyncE enablement
> ================
> For SyncE enablement it is required to allow control over dpll device
> diff --git a/drivers/dpll/dpll_core.c b/drivers/dpll/dpll_core.c
> index 84377f8ba16036..6ad40fb9eee855 100644
> --- a/drivers/dpll/dpll_core.c
> +++ b/drivers/dpll/dpll_core.c
> @@ -932,6 +932,10 @@ __dpll_pin_register(struct dpll_device *dpll, struct dpll_pin *pin,
> * @ops: ops for a dpll pin ops
> * @priv: pointer to private information of owner
> *
> + * The callbacks run under the domain of @pin. If @pin belongs to another domain
> + * than @dpll, they are not serialized with the callbacks of @dpll and @ops must
> + * not include .measured_freq_get.
> + *
> * Context: Acquires dpll_lock and domain locks
> * Return:
> * * 0 on success
> @@ -1049,6 +1053,11 @@ EXPORT_SYMBOL_GPL(dpll_pin_unregister);
> * Register a pin with a parent pin, create references between them and
> * between newly registered pin and dplls connected with a parent pin.
> *
> + * The callbacks run under the domain of @pin. The parent pin and its dplls can
> + * belong to another driver, so @ops must not dereference their private data.
> + * The frequency monitor belongs to a dpll, so @ops must not include
> + * .measured_freq_get.
> + *
> * Context: Acquires dpll_lock and domain locks
> * Return:
> * * 0 on success
Reviewed-by: Petr Oros <poros@redhat.com>
^ permalink raw reply [flat|nested] 27+ messages in thread
* Re: [PATCH net-next 09/10] ice: dpll: update locking comments
2026-11-06 12:10 ` [PATCH net-next 09/10] ice: dpll: " Ivan Vecera
@ 2026-11-10 15:31 ` Petr Oros
2026-11-10 17:53 ` netdev-bot+sashiko
1 sibling, 0 replies; 27+ messages in thread
From: Petr Oros @ 2026-11-10 15:31 UTC (permalink / raw)
To: Ivan Vecera, netdev
Cc: Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
Jonathan Corbet, Shuah Khan, Randy Dunlap, Min Li, Tony Nguyen,
Przemek Kitszel, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Richard Cochran, linux-doc,
linux-kernel, intel-wired-lan, Grzegorz Nitka, Karol Kolacinski,
Saeed Mahameed, Tariq Toukan
On 11/6/26 1:10 PM, Ivan Vecera wrote:
> Callbacks run under the DPLL core domain locks instead of dpll_lock. Note
> that callbacks of pins of an external DPLL run under the domain of that
> DPLL, so only pf->dplls.lock serializes them with the rest of the driver.
>
> Signed-off-by: Ivan Vecera <ivecera@redhat.com>
> ---
> drivers/net/ethernet/intel/ice/ice_dpll.c | 4 ++--
> drivers/net/ethernet/intel/ice/ice_dpll.h | 3 +++
> 2 files changed, 5 insertions(+), 2 deletions(-)
>
> diff --git a/drivers/net/ethernet/intel/ice/ice_dpll.c b/drivers/net/ethernet/intel/ice/ice_dpll.c
> index d6302409a2dacf..07cbd4cf0160ca 100644
> --- a/drivers/net/ethernet/intel/ice/ice_dpll.c
> +++ b/drivers/net/ethernet/intel/ice/ice_dpll.c
> @@ -1178,8 +1178,8 @@ ice_dpll_input_state_get(const struct dpll_pin *pin, void *pin_priv,
> * peer via dpll netlink learn about the update.
> *
> * Context: Called from dpll_pin_ops callbacks after pf->dplls.lock is
> - * released. Uses __dpll_pin_change_ntf() because dpll_lock is
> - * still held by the dpll netlink layer.
> + * released. Uses __dpll_pin_change_ntf() because the domain lock
> + * is still held by the dpll core.
> */
> static void ice_dpll_sw_pin_notify_peer(struct ice_dplls *d,
> struct ice_dpll_pin *changed)
> diff --git a/drivers/net/ethernet/intel/ice/ice_dpll.h b/drivers/net/ethernet/intel/ice/ice_dpll.h
> index 4ba76e2e2e0b01..39e041a30090d0 100644
> --- a/drivers/net/ethernet/intel/ice/ice_dpll.h
> +++ b/drivers/net/ethernet/intel/ice/ice_dpll.h
> @@ -165,6 +165,9 @@ struct ice_dpll {
> * tx_refclks usage map), acquire pf->dplls.lock first, then
> * ctrl_pf->dplls.lock. Skip the second acquire when pf == ctrl_pf
> * to avoid recursive locking.
> + * - Callbacks of the SYNCE and TIME_REF pins of an external DPLL run under
> + * the DPLL core domain lock of that DPLL, not of the TXC and TSPLL DPLLs,
> + * so only @lock serializes them with the callbacks of these DPLLs.
> * - @txclk_notify_rwsem is held for read across
> * ice_txclk_update_and_notify(), including the out-of-lock
> * dpll_*_change_ntf() calls. ice_dpll_deinit() takes the write side
Reviewed-by: Petr Oros <poros@redhat.com>
^ permalink raw reply [flat|nested] 27+ messages in thread
* Re: [PATCH net-next 01/10] ice: dpll: notify TSPLL lock status from the periodic work only
2026-11-06 12:09 ` [PATCH net-next 01/10] ice: dpll: notify TSPLL lock status from the periodic work only Ivan Vecera
2026-11-10 12:20 ` Petr Oros
@ 2026-11-10 17:53 ` netdev-bot+sashiko
1 sibling, 0 replies; 27+ messages in thread
From: netdev-bot+sashiko @ 2026-11-10 17:53 UTC (permalink / raw)
To: ivecera
Cc: netdev, vadim.fedorenko, arkadiusz.kubalewski, jiri, corbet,
skhan, rdunlap, min.li, anthony.l.nguyen, przemyslaw.kitszel,
andrew+netdev, davem, edumazet, kuba, pabeni, richardcochran,
linux-doc, linux-kernel, intel-wired-lan, grzegorz.nitka,
karol.kolacinski, saeedm, tariqt, poros
Thank you for your contribution! Sashiko AI review found more severe
issue(s) in other patches of this series. This mail is only an FYI, in case
you want to address the nit picks below when you respin.
Low priority / nit picks for your consideration:
- [Low] This patch leaves the comments in
ice_dpll_tspll_state_on_dpll_set()…
--
ice: dpll: notify TSPLL lock status from the periodic work only
This patch stops ice_dpll_tspll_state_on_dpll_set() from sending a TSPLL
device change notification from the TIME_REF pin callback. The callback
now only updates dpll_state. ice_dpll_periodic_work_e825() refreshes
dpll_state on every run and sends the notification when the state differs
from prev_dpll_state.
> diff --git a/drivers/net/ethernet/intel/ice/ice_dpll.c b/drivers/net/ethernet/intel/ice/ice_dpll.c
> index ade1fb3dd0674..bdbcf7af296eb 100644
> --- a/drivers/net/ethernet/intel/ice/ice_dpll.c
> +++ b/drivers/net/ethernet/intel/ice/ice_dpll.c
[ ... ]
> @@ -2947,15 +2946,12 @@ ice_dpll_tspll_state_on_dpll_set(const struct dpll_pin *pin, void *pin_priv,
> WRITE_ONCE(pf->ptp.tspll_locked,
> new_state == DPLL_LOCK_STATUS_LOCKED);
>
> - if (tp->prev_dpll_state != new_state) {
> - tp->dpll_state = new_state;
> - tp->prev_dpll_state = new_state;
> - changed = true;
> - }
> + /* ice_dpll_periodic_work_e825() compares the state with
> + * prev_dpll_state and sends the notification.
> + */
> + tp->dpll_state = new_state;
[Severity: Low]
This isn't a bug, but do the comments in ice_dpll_tspll_state_on_dpll_set()
still match how the notification path works after this change?
The new comment reads as if the periodic work compares the tp->dpll_state
written here. In ice_dpll_periodic_work_e825() that value is never read.
The work recomputes the state from the cache and overwrites dpll_state
before it compares:
ice_dpll_periodic_work_e825() {
...
new_state = ice_dpll_tspll_lock_status_get(pf, true);
tp->dpll_state = new_state;
if (tp->prev_dpll_state != new_state) {
...
}
The older comment just above the WRITE_ONCE() was not changed:
/* Sync the cached lock state to what we just observed from HW
* so ice_dpll_periodic_work_e825() (cached path) does not emit
* a spurious UNLOCKED notification between now and the next
* PTP monitor tick.
*/
That reason applied when the callback also set prev_dpll_state. Now that
it doesn't, the WRITE_ONCE() to pf->ptp.tspll_locked is the only way the
callback's HW reading gets to the periodic work.
Without it, take a switch from TCXO (prev_dpll_state is UNLOCKED) to
TIME_REF where the HW reports LOCKED but the cache still holds false. The
next periodic run would compute UNLOCKED, which matches prev_dpll_state.
No notification would be sent, and dpll_state would go back to UNLOCKED.
The result would be a lost notification, not a spurious one.
Could both comments say that the periodic work picks up the new state
through pf->ptp.tspll_locked? Then a later change is less likely to drop
the WRITE_ONCE() as just a way to avoid spurious notifications. As far as
I can tell, the comments are still unchanged at the end of the series,
after "ice: dpll: update locking comments".
> }
> mutex_unlock(&pf->dplls.lock);
> - if (changed)
> - __dpll_device_change_ntf(tp->dpll);
[ ... ]
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261106121005.1327603-1-ivecera%40redhat.com
^ permalink raw reply [flat|nested] 27+ messages in thread
* Re: [PATCH net-next 02/10] ice: dpll: serialize changes of the SyncE CGU registers
2026-11-06 12:09 ` [PATCH net-next 02/10] ice: dpll: serialize changes of the SyncE CGU registers Ivan Vecera
@ 2026-11-10 17:53 ` netdev-bot+sashiko
0 siblings, 0 replies; 27+ messages in thread
From: netdev-bot+sashiko @ 2026-11-10 17:53 UTC (permalink / raw)
To: ivecera
Cc: netdev, vadim.fedorenko, arkadiusz.kubalewski, jiri, corbet,
skhan, rdunlap, min.li, anthony.l.nguyen, przemyslaw.kitszel,
andrew+netdev, davem, edumazet, kuba, pabeni, richardcochran,
linux-doc, linux-kernel, intel-wired-lan, grzegorz.nitka,
karol.kolacinski, saeedm, tariqt, poros
Thank you for your contribution! Sashiko AI review found more severe
issue(s) in other patches of this series. This mail is only an FYI, in case
you want to address the nit picks below when you respin.
Low priority / nit picks for your consideration:
- [Low] The commit fixes a race that already exists in the tree, but it
has no Fixes: tag and no stable Cc.
--
ice: dpll: serialize changes of the SyncE CGU registers
This patch adds a cgu_synce_lock mutex to struct ice_adapter, which all
E825C PFs share. It holds that lock around the CGU R10/R11
read-modify-write sequences in ice_dpll_synce_update_e825c(), and around
the ice_tspll_cfg_synce_ethdiv_e825c() call in ice_ptp_link_change().
> ice_ptp_link_change() does not hold dpll_lock, so it can already overwrite
> an update of another PF. RCLK callbacks are serialized by dpll_lock, but
> the DPLL core is going to serialize callbacks per (module, clock_id), and
> each complex has its own DSN and so its own clock_id. With that change, a
> test toggling RCLK pins of both complexes lost an update.
[ ... ]
> Signed-off-by: Ivan Vecera <ivecera@redhat.com>
[Severity: Low]
Should this patch have a Fixes: tag?
The commit message says this race already exists in the tree. Before
this patch, ice_ptp_link_change() on one PF calls
ice_tspll_cfg_synce_ethdiv_e825c(). That function reads ICE_CGU_R10 once
and writes the whole register back twice.
At the same time, on another PF, the path
ice_dpll_rclk_state_on_pin_set()->ice_dpll_synce_update_e825c() does a
read-modify-write of R10 and R11. Each of these paths holds only its own
PF's pf->dplls.lock.
Both paths reach the same CGU. All E825C PFs share one ice_adapter, and
the secondary complex reaches the primary CGU through
ice_sbq_dev_cgu_peer. So a link flap on one port can write back a stale
R10. That undoes a mux or divider change that another PF just made
(S_REF_CLK, ETHCLKO_SEL, CLKO_SEL, *_DIV_M1).
Both racing sequences seem to come from commit ad1df4f2d591 ("ice: dpll:
Support E825-C SyncE and dynamic pin discovery"). A related fix,
bf6dbadb72b9 ("ice: fix PTP timestamping broken by SyncE code on
E825C"), already has a Fixes: tag for that commit.
Without the tag, stable kernels that carry ad1df4f2d591 would probably
not get the fix for this race. Could you add the following tag?
Fixes: ad1df4f2d591 ("ice: dpll: Support E825-C SyncE and dynamic pin discovery")
Another option is to split this patch out of the series and send it to
net.
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261106121005.1327603-1-ivecera%40redhat.com
^ permalink raw reply [flat|nested] 27+ messages in thread
* Re: [PATCH net-next 03/10] dpll: add domain object
2026-11-06 12:09 ` [PATCH net-next 03/10] dpll: add domain object Ivan Vecera
2026-11-10 12:23 ` Petr Oros
@ 2026-11-10 17:53 ` netdev-bot+sashiko
1 sibling, 0 replies; 27+ messages in thread
From: netdev-bot+sashiko @ 2026-11-10 17:53 UTC (permalink / raw)
To: ivecera
Cc: netdev, vadim.fedorenko, arkadiusz.kubalewski, jiri, corbet,
skhan, rdunlap, min.li, anthony.l.nguyen, przemyslaw.kitszel,
andrew+netdev, davem, edumazet, kuba, pabeni, richardcochran,
linux-doc, linux-kernel, intel-wired-lan, grzegorz.nitka,
karol.kolacinski, saeedm, tariqt, poros
Thank you for your contribution! Sashiko AI review found more severe
issue(s) in other patches of this series. This mail is only an FYI, in case
you want to address the nit picks below when you respin.
Low priority / nit picks for your consideration:
- [Low] The comment on `lock` in the new struct dpll_domain
(drivers/dpll/dpll_core.h) says it "Serializes the driver callbacks of…
--
dpll: add domain object
This commit adds struct dpll_domain, which groups all dpll devices and
pins created with the same (module, clock_id) pair. Domains are
refcounted, stored in an xarray and freed along with their last object.
It prepares for serializing driver callbacks per driver instance instead
of under the global dpll_lock, and states no functional change.
> diff --git a/drivers/dpll/dpll_core.h b/drivers/dpll/dpll_core.h
> index da8a369556ed1..6705e81bc11de 100644
> --- a/drivers/dpll/dpll_core.h
> +++ b/drivers/dpll/dpll_core.h
[ ... ]
> +struct dpll_domain {
> + u32 id;
> + u64 clock_id;
> + struct module *module;
> + refcount_t refcount;
> + /* Serializes the driver callbacks of all DPLL devices and pins
> + * that share one (module, clock_id) pair.
> + */
> + struct mutex lock;
[Severity: Low]
This isn't a bug, but is this comment accurate at this point in the
series?
In this commit, domain->lock is only set up and torn down:
dpll_domain_get() calls mutex_init(), and its error path and
dpll_domain_put() call mutex_destroy(). Nothing acquires it, so driver
callbacks are still serialized only by the global dpll_lock.
The later patch "dpll: take domain locks around driver callbacks" adds
the mutex_lock() calls on dpll->domain->lock and pin->domain->lock that
make the comment true. So the mismatch exists only in this intermediate
commit.
Could the comment say the lock will serialize the callbacks, or could it
be added in the patch that starts taking the lock?
> +};
[ ... ]
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261106121005.1327603-1-ivecera%40redhat.com
^ permalink raw reply [flat|nested] 27+ messages in thread
* Re: [PATCH net-next 05/10] dpll: convert dpll_lock to rwsem
2026-11-06 12:10 ` [PATCH net-next 05/10] dpll: convert dpll_lock to rwsem Ivan Vecera
2026-11-10 12:24 ` Petr Oros
@ 2026-11-10 17:53 ` netdev-bot+sashiko
1 sibling, 0 replies; 27+ messages in thread
From: netdev-bot+sashiko @ 2026-11-10 17:53 UTC (permalink / raw)
To: ivecera
Cc: netdev, vadim.fedorenko, arkadiusz.kubalewski, jiri, corbet,
skhan, rdunlap, min.li, anthony.l.nguyen, przemyslaw.kitszel,
andrew+netdev, davem, edumazet, kuba, pabeni, richardcochran,
linux-doc, linux-kernel, intel-wired-lan, grzegorz.nitka,
karol.kolacinski, saeedm, tariqt, poros
Thank you for your contribution! Sashiko AI review found more severe
issue(s) in other patches of this series. This mail is only an FYI, in case
you want to address the nit picks below when you respin.
Low priority / nit picks for your consideration:
- [Low] This patch turns dpll_lock from a mutex into an rwsem.
--
dpll: convert dpll_lock to rwsem
This commit converts dpll_lock from a mutex to an rwsem. Core functions
that change the topology take it for write. Netlink requests, dump parts
and change notifications take it for read, so callbacks of different
domains can run concurrently.
> diff --git a/drivers/dpll/dpll_core.c b/drivers/dpll/dpll_core.c
> index dd09f52817099..8eb7468234315 100644
> --- a/drivers/dpll/dpll_core.c
> +++ b/drivers/dpll/dpll_core.c
> @@ -19,8 +19,12 @@
> #include "dpll_core.h"
> #include "dpll_netlink.h"
>
> -/* Mutex lock to protect DPLL subsystem devices and pins */
> -DEFINE_MUTEX(dpll_lock);
> +/* Protects the topology and lifetime of DPLL devices and pins. Taken for write
> + * to change them and for read for the whole netlink request, part of a dump or
> + * change notification that uses them. Driver callbacks are serialized by domain
> + * locks, which are only taken under this lock.
> + */
> +DECLARE_RWSEM(dpll_lock);
[Severity: Low]
Should the assertions in dpll_domain_get() and dpll_domain_put() be changed
to lockdep_assert_held_write() along with this conversion?
Both helpers still use the plain form:
drivers/dpll/dpll_core.c:dpll_domain_get() {
lockdep_assert_held(&dpll_lock);
xa_for_each(&dpll_domain_xa, i, domain) {
if (domain->clock_id == clock_id && domain->module == module) {
refcount_inc(&domain->refcount);
return domain;
}
}
...
ret = xa_alloc(&dpll_domain_xa, &domain->id, domain, xa_limit_32b,
GFP_KERNEL);
...
}
drivers/dpll/dpll_core.c:dpll_domain_put() {
lockdep_assert_held(&dpll_lock);
if (refcount_dec_and_test(&domain->refcount)) {
xa_erase(&dpll_domain_xa, domain->id);
mutex_destroy(&domain->lock);
kfree(domain);
}
}
On the old mutex, lockdep_assert_held() checked for exclusive ownership. On
an rwsem it also passes when only the read lock is held.
Every current caller holds dpll_lock for write: dpll_device_get() and
dpll_pin_get() via the alloc helpers, and dpll_device_put(), dpll_pin_put()
and the register/unregister paths via the __put helpers. So this is not a
runtime problem today.
However, the read-side holders added here (dpll_pre_doit(),
dpll_pin_pre_doit(), the dumpits, dpll_device_change_ntf() and
dpll_pin_change_ntf()) can now run concurrently. If one of them reached a
domain lookup-or-create, or a final put, lockdep would no longer warn about
the race on dpll_domain_xa or on the domain refcount.
call_dpll_notifiers() can keep the plain lockdep_assert_held(), because it
legitimately runs under the read lock.
These assertions are still unchanged at the end of the series.
[ ... ]
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261106121005.1327603-1-ivecera%40redhat.com
^ permalink raw reply [flat|nested] 27+ messages in thread
* Re: [PATCH net-next 06/10] dpll: add KUnit tests for core locking
2026-11-06 12:10 ` [PATCH net-next 06/10] dpll: add KUnit tests for core locking Ivan Vecera
2026-11-10 12:24 ` Petr Oros
@ 2026-11-10 17:53 ` netdev-bot+sashiko
1 sibling, 0 replies; 27+ messages in thread
From: netdev-bot+sashiko @ 2026-11-10 17:53 UTC (permalink / raw)
To: ivecera
Cc: netdev, vadim.fedorenko, arkadiusz.kubalewski, jiri, corbet,
skhan, rdunlap, min.li, anthony.l.nguyen, przemyslaw.kitszel,
andrew+netdev, davem, edumazet, kuba, pabeni, richardcochran,
linux-doc, linux-kernel, intel-wired-lan, grzegorz.nitka,
karol.kolacinski, saeedm, tariqt, poros
Thank you for your contribution! Sashiko AI review found 1 potential
issue(s) to consider.
Critical: 0 · High: 0 · Medium: 1 · Low: 0
If you believe none of the issues are legitimate, please reply to each
issue of Medium severity or higher and explain why the AI is wrong.
If any of them are legitimate and you plan to respin, please let patchwork
know by including "pw-bot: cr" as a separate line at the end of your reply
(one such reply per series is enough).
- [Medium] In drivers/dpll/dpll_core_kunit.c, some tests set a blocking
flag before dt_req_start() has registered dt_req_release().
--
dpll: add KUnit tests for core locking
This adds KUnit tests for DPLL core locking. They use a mock driver with
several domains to check domain sharing, which locks callbacks hold,
independence and serialization of domains, atomicity of multi-attribute
pin sets, and notification ordering. The netlink pre/post doit and set
handlers are exported for KUnit so the tests can call them directly.
> diff --git a/drivers/dpll/dpll_core_kunit.c b/drivers/dpll/dpll_core_kunit.c
> new file mode 100644
> index 0000000000000..a58800eda5ff5
> --- /dev/null
> +++ b/drivers/dpll/dpll_core_kunit.c
> @@ -0,0 +1,1471 @@
[ ... ]
> +static void dt_block(struct dt_dom *dom, bool *block)
> +{
> + complete(&dom->entered);
> + wait_event(dom->ctx->wq, !READ_ONCE(*block));
> +}
[ ... ]
> +static struct dt_req *dt_req_start(struct kunit *test, int (*fn)(void *arg),
> + void *arg)
> +{
> + struct task_struct *task;
> + struct dt_req *req;
> +
> + req = kunit_kzalloc(test, sizeof(*req), GFP_KERNEL);
> + KUNIT_ASSERT_NOT_NULL(test, req);
> +
> + req->ctx = test->priv;
> + req->fn = fn;
> + req->arg = arg;
> + init_completion(&req->done);
> +
> + task = kthread_run(dt_req_thread, req, "dpll-test");
> + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, task);
> + KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test, dt_req_release,
> + req), 0);
> +
> + return req;
> +}
[ ... ]
> +/* Blocks a request of @dom in a callback, which holds the domain lock */
> +static struct dt_req *dt_block_in(struct kunit *test, struct dt_dom *dom,
> + int (*fn)(void *arg), void *arg)
> +{
> + struct dt_req *req;
> +
> + WRITE_ONCE(dom->block, true);
> + req = dt_req_start(test, fn, arg);
[Severity: Medium]
Can the teardown hang here if dt_req_start() fails one of its assertions?
dom->block is set before dt_req_start() registers dt_req_release(), and
dt_req_release() is the only cleanup action that calls dt_unblock(). If
kunit_kzalloc() or kthread_run() in dt_req_start() fails, the
KUNIT_ASSERT_NOT_NULL() or KUNIT_ASSERT_NOT_ERR_OR_NULL() check aborts the
test and the flag stays set.
The cleanup actions that were already registered then run, for example:
dt_reg_release()
dpll_pin_unregister()
down_write(&dpll_lock)
__dpll_pin_unregister()
dpll_pin_delete_ntf()
mutex_lock(&pin->domain->lock)
dpll_pin_event_send()
dpll_cmd_pin_get_one()
pin get callbacks
dt_pin_enter()->dt_enter()->dt_block()
dt_block() then waits with:
wait_event(dom->ctx->wq, !READ_ONCE(*block));
Nothing is left that can clear the flag. The KUnit cleanup thread would
sleep uninterruptibly while holding dpll_lock for write and a domain
mutex. Every other DPLL user in the system would then block behind it.
dt_test_stuck_foreign_device() has the same problem:
WRITE_ONCE(ctx->f0.block, true);
blocked = dt_req_start(test, dt_device_ntf, &ctx->f0);
There, dt_dev_release()->dpll_device_unregister()->dpll_device_delete_ntf()
->dpll_device_get_one()->dt_mode_get()->dt_dev_enter() would reach
dt_block() on f0.block.
One option is to register a cleanup action that calls dt_unblock() from
dt_init(), before any flag can be set. Another is to set the flags only
after dt_req_release() has been registered. Would either of these work?
Setting block_prio_set early looks harmless, since no teardown path calls
prio_set.
> + KUNIT_ASSERT_TRUE(test, wait_for_completion_timeout(&dom->entered,
> + DT_TIMEOUT));
> +
> + return req;
> +}
[ ... ]
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261106121005.1327603-1-ivecera%40redhat.com
^ permalink raw reply [flat|nested] 27+ messages in thread
* Re: [PATCH net-next 09/10] ice: dpll: update locking comments
2026-11-06 12:10 ` [PATCH net-next 09/10] ice: dpll: " Ivan Vecera
2026-11-10 15:31 ` Petr Oros
@ 2026-11-10 17:53 ` netdev-bot+sashiko
1 sibling, 0 replies; 27+ messages in thread
From: netdev-bot+sashiko @ 2026-11-10 17:53 UTC (permalink / raw)
To: ivecera
Cc: netdev, vadim.fedorenko, arkadiusz.kubalewski, jiri, corbet,
skhan, rdunlap, min.li, anthony.l.nguyen, przemyslaw.kitszel,
andrew+netdev, davem, edumazet, kuba, pabeni, richardcochran,
linux-doc, linux-kernel, intel-wired-lan, grzegorz.nitka,
karol.kolacinski, saeedm, tariqt, poros
Thank you for your contribution! Sashiko AI review found more severe
issue(s) in other patches of this series. This mail is only an FYI, in case
you want to address the nit picks below when you respin.
Low priority / nit picks for your consideration:
- [Low] The locking note this patch adds to the struct ice_dplls kernel-
doc (drivers/net/ethernet/intel/ice/ice_dpll.h, lines 168-170) is…
--
ice: dpll: update locking comments
This commit updates the ice DPLL locking comments now that callbacks run
under the DPLL core domain locks instead of dpll_lock. It also adds a note
saying that callbacks of the external DPLL's pins run under that DPLL's
domain, so only pf->dplls.lock serializes them with the rest of the
driver.
> diff --git a/drivers/net/ethernet/intel/ice/ice_dpll.h b/drivers/net/ethernet/intel/ice/ice_dpll.h
> index 4ba76e2e2e0b0..39e041a30090d 100644
> --- a/drivers/net/ethernet/intel/ice/ice_dpll.h
> +++ b/drivers/net/ethernet/intel/ice/ice_dpll.h
> @@ -165,6 +165,9 @@ struct ice_dpll {
> * tx_refclks usage map), acquire pf->dplls.lock first, then
> * ctrl_pf->dplls.lock. Skip the second acquire when pf == ctrl_pf
> * to avoid recursive locking.
> + * - Callbacks of the SYNCE and TIME_REF pins of an external DPLL run under
> + * the DPLL core domain lock of that DPLL, not of the TXC and TSPLL DPLLs,
> + * so only @lock serializes them with the callbacks of these DPLLs.
[Severity: Low]
Could this note be clearer that TXC and TSPLL are not in the same core
domain either?
As written, it presents the external fwnode pins as the special case. A
reader could take that to mean the other ice objects share one core
domain, which would serialize their callbacks against each other.
Looking at ice_generate_dpll_clock_id(), each PF gets three different
clock_ids:
drivers/net/ethernet/intel/ice/ice_dpll.c:ice_generate_dpll_clock_id() {
...
if (d->dpll_idx >= E825_DPLL_TXCLK_BASE_IDX)
return mac_clock_id;
if (d->dpll_idx == E825_DPLL_TSPLL_BASE_IDX)
return mac_clock_id | BIT_ULL(63);
}
return pf->dplls.clock_id;
}
dpll_domain_get() keys domains on (clock_id, module). That gives three
separate domains on one PF:
- TXC, plus the EXT_EREF0 pin, which ice_dpll_init_txclk_pins() creates
with the TXC clock_id
- TSPLL
- the DSN-based objects, such as the rclk pin
Each PF's TXC and TSPLL domains are also separate from those of other PFs.
The struct dpll_domain comment in dpll_core.h says only one domain lock is
held at a time.
So the core does not serialize TXC callbacks against TSPLL callbacks, or
either of them against rclk callbacks. pf->dplls.lock (with
ctrl_pf->dplls.lock and cgu_synce_lock) is what does that, and the fwnode
pins are one more case of the same thing.
The new sentence is accurate per DPLL, and the first bullet still says
@lock serializes all DPLL state mutations. Since this patch exists to
document the domain-lock rules, though, would it make sense to say that
TXC, TSPLL and the DSN-domain objects are each in their own domain?
> * - @txclk_notify_rwsem is held for read across
> * ice_txclk_update_and_notify(), including the out-of-lock
> * dpll_*_change_ntf() calls. ice_dpll_deinit() takes the write side
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261106121005.1327603-1-ivecera%40redhat.com
^ permalink raw reply [flat|nested] 27+ messages in thread
* Re: [PATCH net-next 10/10] dpll: document locking rules
2026-11-06 12:10 ` [PATCH net-next 10/10] dpll: document locking rules Ivan Vecera
2026-11-10 12:25 ` Petr Oros
@ 2026-11-10 17:53 ` netdev-bot+sashiko
1 sibling, 0 replies; 27+ messages in thread
From: netdev-bot+sashiko @ 2026-11-10 17:53 UTC (permalink / raw)
To: ivecera
Cc: netdev, vadim.fedorenko, arkadiusz.kubalewski, jiri, corbet,
skhan, rdunlap, min.li, anthony.l.nguyen, przemyslaw.kitszel,
andrew+netdev, davem, edumazet, kuba, pabeni, richardcochran,
linux-doc, linux-kernel, intel-wired-lan, grzegorz.nitka,
karol.kolacinski, saeedm, tariqt, poros
Thank you for your contribution! Sashiko AI review found more severe
issue(s) in other patches of this series. This mail is only an FYI, in case
you want to address the nit picks below when you respin.
Low priority / nit picks for your consideration:
- [Low] The new Locking section in Documentation/driver-api/dpll.rst
covers three cases where a pin callback reaches private data owned by…
- [Low] The new driver rule in Documentation/driver-api/dpll.rst (lines
716-718) says notifications for objects outside the callback's domain…
--
dpll: document locking rules
This commit adds a Locking section to the dpll driver API documentation.
The section describes the dpll_lock and per-domain lock levels, the domain
each driver callback runs under, and the rules drivers have to follow. It
also adds matching notes to the kernel-doc of dpll_pin_register() and
dpll_pin_on_pin_register().
> diff --git a/Documentation/driver-api/dpll.rst b/Documentation/driver-api/dpll.rst
> index 7c117ae37cc1b..40d5037ff3bb0 100644
> --- a/Documentation/driver-api/dpll.rst
> +++ b/Documentation/driver-api/dpll.rst
> @@ -675,6 +675,50 @@ In the error path we have to rewind every allocation in the reverse order:
[ ... ]
> +Callbacks of a dpll device run under the domain of the device. Callbacks of a
> +pin run under the domain of the pin, whichever driver registered them:
> +
> +- A driver registering a pin of another driver with its dpll device has these
> + callbacks serialized with the driver that created the pin, not with the other
> + callbacks of its device, so it has to protect the state they share by itself.
> + A callback stuck in such a registration stops the domain of the pin.
> +- The parent pin of a pin registered with dpll_pin_on_pin_register() and its
> + dpll devices can belong to another driver, so the callback must not
> + dereference their private data.
> +- The frequency monitor belongs to the dpll device, so only callbacks of a pin
> + of the same domain registered with dpll_pin_register() can include
> + ``.measured_freq_get``.
[Severity: Low]
Should reference-sync pairs be in this list too?
dpll_pin_ref_sync_pair_add() only does an xa_insert() into
pin->ref_sync_pins. It never checks that pin->domain matches
ref_sync_pin->domain, so a pair can span two domains.
When the ref-sync callbacks run, only the domain lock of the pin is held,
but they get the private data of the paired pin:
drivers/dpll/dpll_netlink.c:dpll_msg_add_pin_ref_sync() {
...
ref_sync_pin_priv = dpll_pin_on_dpll_priv(dpll, ref_sync_pin);
...
ret = ops->ref_sync_get(pin, pin_priv, ref_sync_pin,
ref_sync_pin_priv, &state, extack);
...
}
dpll_pin_ref_sync_state_set() does the same for ref_sync_get() and
ref_sync_set(). It then calls __dpll_pin_change_ntf() only for the pin, not
for ref_sync_pin.
Under the rules above, the callbacks from ref_sync_pin's own registration
run under ref_sync_pin's domain. If the two domains differ, nothing
serializes these two sets of callbacks.
The in-tree implementers zl3073x_dpll_input_pin_ref_sync_get() and
zl3073x_dpll_input_pin_ref_sync_set() do dereference this data:
struct zl3073x_dpll_pin *sync_pin = ref_sync_pin_priv;
No in-tree driver creates a cross-domain pair today, since zl3073x
registers only its own chip's pins. The first bullet partly covers the case
where one driver registered both pins. It does not cover pins that
different drivers registered on one dpll.
Could the documentation say whether a ref-sync pair may cross domains, and
whether ref_sync_pin_priv may be dereferenced? The parent pin case in the
dpll_pin_on_pin_register() bullet already has an explicit rule like this.
> +
> +Device drivers have to follow these rules:
> +
> +- A callback must not call dpll_device_change_ntf(), dpll_pin_change_ntf() or
> + any function changing the topology.
> +- A callback notifies about objects of the domain it runs under with
> + __dpll_device_change_ntf() or __dpll_pin_change_ntf(). Other objects have to
> + be notified from a context that holds no domain lock, for example a work item.
> +- A driver must not call dpll_device_change_ntf(), dpll_pin_change_ntf() or any
> + function changing the topology while holding a lock that its callbacks take.
[Severity: Low]
Can a driver follow the work item suggestion safely without two more rules?
The first rule is about waiting on the work. The deferred work ends up in
dpll_pin_change_ntf() or dpll_device_change_ntf(). Both take dpll_lock for
read and then the domain mutex:
drivers/dpll/dpll_netlink.c:dpll_pin_change_ntf() {
...
down_read(&dpll_lock);
mutex_lock(&pin->domain->lock);
ret = __dpll_pin_change_ntf(pin);
...
}
Callbacks already run with dpll_lock held for read, for example from
dpll_pin_pre_doit() or dpll_pre_doit(). A callback, or code holding a lock
the callbacks take, might wait for the work with flush_work() or
cancel_work_sync() while a writer is queued on dpll_lock. Could that
deadlock?
callback writer work
down_read(&dpll_lock)
dpll_pin_register()
down_write(&dpll_lock)
dpll_pin_change_ntf()
down_read(&dpll_lock)
flush_work()
Here the writer waits for the callback's read hold. The work's down_read()
blocks behind the queued writer, and the callback waits for the work.
The last rule above only bans direct calls to dpll_device_change_ntf() and
dpll_pin_change_ntf() while holding such a lock. It does not cover waiting
for a work item that makes those calls.
The second rule is about lifetime. The text does not say that the work has
to be cancelled or flushed before the target pin or device is unregistered
and released with dpll_pin_put() or dpll_device_put(). __dpll_device_put()
ends in kfree(dpll). A work item that keeps a struct dpll_device or struct
dpll_pin pointer and runs after the final put would touch freed memory.
Would it make sense to add both rules next to the work item suggestion?
> +
> SyncE enablement
> ================
> For SyncE enablement it is required to allow control over dpll device
[ ... ]
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261106121005.1327603-1-ivecera%40redhat.com
^ permalink raw reply [flat|nested] 27+ messages in thread
end of thread, other threads:[~2026-11-10 17:53 UTC | newest]
Thread overview: 27+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-11-06 12:09 [PATCH net-next 00/10] dpll: do not serialize driver callbacks under global dpll_lock Ivan Vecera
2026-11-06 12:09 ` [PATCH net-next 01/10] ice: dpll: notify TSPLL lock status from the periodic work only Ivan Vecera
2026-11-10 12:20 ` Petr Oros
2026-11-10 17:53 ` netdev-bot+sashiko
2026-11-06 12:09 ` [PATCH net-next 02/10] ice: dpll: serialize changes of the SyncE CGU registers Ivan Vecera
2026-11-10 17:53 ` netdev-bot+sashiko
2026-11-06 12:09 ` [PATCH net-next 03/10] dpll: add domain object Ivan Vecera
2026-11-10 12:23 ` Petr Oros
2026-11-10 17:53 ` netdev-bot+sashiko
2026-11-06 12:09 ` [PATCH net-next 04/10] dpll: take domain locks around driver callbacks Ivan Vecera
2026-11-10 12:23 ` Petr Oros
2026-11-06 12:10 ` [PATCH net-next 05/10] dpll: convert dpll_lock to rwsem Ivan Vecera
2026-11-10 12:24 ` Petr Oros
2026-11-10 17:53 ` netdev-bot+sashiko
2026-11-06 12:10 ` [PATCH net-next 06/10] dpll: add KUnit tests for core locking Ivan Vecera
2026-11-10 12:24 ` Petr Oros
2026-11-10 17:53 ` netdev-bot+sashiko
2026-11-06 12:10 ` [PATCH net-next 07/10] dpll: reject measured_freq_get for foreign and on-pin registrations Ivan Vecera
2026-11-10 12:24 ` Petr Oros
2026-11-06 12:10 ` [PATCH net-next 08/10] dpll: zl3073x: update locking comments Ivan Vecera
2026-11-10 12:24 ` Petr Oros
2026-11-06 12:10 ` [PATCH net-next 09/10] ice: dpll: " Ivan Vecera
2026-11-10 15:31 ` Petr Oros
2026-11-10 17:53 ` netdev-bot+sashiko
2026-11-06 12:10 ` [PATCH net-next 10/10] dpll: document locking rules Ivan Vecera
2026-11-10 12:25 ` Petr Oros
2026-11-10 17:53 ` netdev-bot+sashiko
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®