From: Petr Oros <poros@redhat.com>
To: Ivan Vecera <ivecera@redhat.com>, netdev@vger.kernel.org
Cc: Vadim Fedorenko <vadim.fedorenko@linux.dev>,
Arkadiusz Kubalewski <arkadiusz.kubalewski@intel.com>,
Jiri Pirko <jiri@resnulli.us>, Jonathan Corbet <corbet@lwn.net>,
Shuah Khan <skhan@linuxfoundation.org>,
Randy Dunlap <rdunlap@infradead.org>,
Min Li <min.li@microchip.com>,
Tony Nguyen <anthony.l.nguyen@intel.com>,
Przemek Kitszel <przemyslaw.kitszel@intel.com>,
Andrew Lunn <andrew+netdev@lunn.ch>,
"David S. Miller" <davem@davemloft.net>,
Eric Dumazet <edumazet@kernel.org>,
Jakub Kicinski <kuba@kernel.org>, Paolo Abeni <pabeni@redhat.com>,
Richard Cochran <richardcochran@gmail.com>,
linux-doc@vger.kernel.org, linux-kernel@vger.kernel.org,
intel-wired-lan@lists.osuosl.org,
Grzegorz Nitka <grzegorz.nitka@intel.com>,
Karol Kolacinski <karol.kolacinski@intel.com>,
Saeed Mahameed <saeedm@nvidia.com>,
Tariq Toukan <tariqt@nvidia.com>
Subject: Re: [PATCH net-next 06/10] dpll: add KUnit tests for core locking
Date: Tue, 10 Nov 2026 13:24:26 +0100 [thread overview]
Message-ID: <cdf28801-4eba-44c3-a7e1-29334edc491b@redhat.com> (raw)
In-Reply-To: <20261106121005.1327603-7-ivecera@redhat.com>
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>
next prev parent reply other threads:[~2026-11-10 12:24 UTC|newest]
Thread overview: 19+ messages / expand[flat|nested] mbox.gz Atom feed top
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-06 12:09 ` [PATCH net-next 02/10] ice: dpll: serialize changes of the SyncE CGU registers Ivan Vecera
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-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-06 12:10 ` [PATCH net-next 06/10] dpll: add KUnit tests for core locking Ivan Vecera
2026-11-10 12:24 ` Petr Oros [this message]
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-06 12:10 ` [PATCH net-next 10/10] dpll: document locking rules Ivan Vecera
2026-11-10 12:25 ` Petr Oros
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=cdf28801-4eba-44c3-a7e1-29334edc491b@redhat.com \
--to=poros@redhat.com \
--cc=andrew+netdev@lunn.ch \
--cc=anthony.l.nguyen@intel.com \
--cc=arkadiusz.kubalewski@intel.com \
--cc=corbet@lwn.net \
--cc=davem@davemloft.net \
--cc=edumazet@kernel.org \
--cc=grzegorz.nitka@intel.com \
--cc=intel-wired-lan@lists.osuosl.org \
--cc=ivecera@redhat.com \
--cc=jiri@resnulli.us \
--cc=karol.kolacinski@intel.com \
--cc=kuba@kernel.org \
--cc=linux-doc@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=min.li@microchip.com \
--cc=netdev@vger.kernel.org \
--cc=pabeni@redhat.com \
--cc=przemyslaw.kitszel@intel.com \
--cc=rdunlap@infradead.org \
--cc=richardcochran@gmail.com \
--cc=saeedm@nvidia.com \
--cc=skhan@linuxfoundation.org \
--cc=tariqt@nvidia.com \
--cc=vadim.fedorenko@linux.dev \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
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®