* [PATCH net-next v3 01/14] dpll: dpllsim: Add simulated DPLL device driver
2026-11-09 9:35 [PATCH net-next v3 00/14] dpll: add a simulated DPLL device driver Petr Oros
@ 2026-11-09 9:35 ` Petr Oros
2026-11-11 9:37 ` netdev-bot+sashiko
2026-11-09 9:35 ` [PATCH net-next v3 02/14] selftests: dpll: add test library and device lifecycle tests Petr Oros
` (12 subsequent siblings)
13 siblings, 1 reply; 31+ messages in thread
From: Petr Oros @ 2026-11-09 9:35 UTC (permalink / raw)
To: netdev
Cc: Petr Oros, Ivan Vecera, Jiri Pirko, Jonathan Corbet, Shuah Khan,
Shuah Khan, Randy Dunlap, Jani Nikula, Vadim Fedorenko,
Arkadiusz Kubalewski, Jiri Pirko, Andrew Lunn, David S. Miller,
Eric Dumazet, Jakub Kicinski, Paolo Abeni, linux-doc,
linux-kselftest
Add dpllsim, a driver providing simulated DPLL devices for testing the
DPLL subsystem and its userspace tooling without real hardware, in the
same spirit as netdevsim for network devices. The DPLL core identifies
a device by clock ID, index and module only, so the driver needs no bus
and no struct device at all.
Instances are created and configured at runtime through configfs,
following the gpio-sim precedent for objects that userspace creates on
demand. Creating a directory under /sys/kernel/config/dpllsim/ allocates
an instance, its clock_id attribute selects the clock ID, writing 1 to
live registers the DPLL device with the core and writing 0 unregisters
it again. Removing the directory tears a live instance down implicitly.
Keeping registration separate from creation lets a test register and
unregister the same device repeatedly, which is where the core bugs the
driver is meant to catch tend to live, and it allows the structural
attributes to be refused with EBUSY while the device is visible to
userspace. Unlike sysfs, configfs does not imply a stable ABI, which
suits a driver whose knobs exist only to drive tests.
Reading live reports whether the device is registered with the DPLL
subsystem, it turns 1 only once the whole live transition succeeded and
0 again as the first step of the teardown. The configuration is frozen
earlier, from the moment the transition obtains the device handle, so
that a configuration write racing with the transition is refused rather
than lost.
The device registers as a PPS type DPLL with the two mandatory callbacks
only. mode_get reports automatic mode and lock_status_get reports
unlocked with no error. Pins come with the following patches and the
simulation of lock acquisition with a later series.
Document the configfs layout under Documentation/driver-api and add a
MAINTAINERS entry.
Reviewed-by: Ivan Vecera <ivecera@redhat.com>
Acked-by: Jiri Pirko <jiri@nvidia.com>
Signed-off-by: Petr Oros <poros@redhat.com>
---
Documentation/driver-api/dpllsim.rst | 41 +++++
Documentation/driver-api/index.rst | 1 +
MAINTAINERS | 7 +
drivers/dpll/Kconfig | 1 +
drivers/dpll/Makefile | 1 +
drivers/dpll/dpllsim/Kconfig | 16 ++
drivers/dpll/dpllsim/Makefile | 4 +
drivers/dpll/dpllsim/core.c | 221 +++++++++++++++++++++++++++
drivers/dpll/dpllsim/dpll.c | 84 ++++++++++
drivers/dpll/dpllsim/dpllsim.h | 58 +++++++
10 files changed, 434 insertions(+)
create mode 100644 Documentation/driver-api/dpllsim.rst
create mode 100644 drivers/dpll/dpllsim/Kconfig
create mode 100644 drivers/dpll/dpllsim/Makefile
create mode 100644 drivers/dpll/dpllsim/core.c
create mode 100644 drivers/dpll/dpllsim/dpll.c
create mode 100644 drivers/dpll/dpllsim/dpllsim.h
diff --git a/Documentation/driver-api/dpllsim.rst b/Documentation/driver-api/dpllsim.rst
new file mode 100644
index 00000000000000..86a5d523534055
--- /dev/null
+++ b/Documentation/driver-api/dpllsim.rst
@@ -0,0 +1,41 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+==========================
+DPLL simulator (dpllsim)
+==========================
+
+The dpllsim driver provides simulated DPLL devices for testing the DPLL
+subsystem and its userspace tooling without real hardware, similar to
+what netdevsim provides for network devices.
+
+Configuration interface
+=======================
+
+Devices are described in configfs under ``/sys/kernel/config/dpllsim/``.
+A directory created there is a device instance. Attributes are written
+while the instance is not live, then ``live`` is set to 1 to register
+the device with the DPLL subsystem. Writing 0 unregisters it again.
+Structural attributes return ``EBUSY`` while the instance is live.
+
+Device directory ``/sys/kernel/config/dpllsim/<dev>/``:
+
+======================= =====================================================
+``clock_id`` Clock ID of the DPLL device, must be nonzero and
+ unique among live instances
+``live`` 1 registers the device, 0 unregisters it
+======================= =====================================================
+
+Example
+=======
+
+Create a device and make it live::
+
+ cd /sys/kernel/config/dpllsim
+ mkdir dev0 && cd dev0
+ echo 100 > clock_id
+ echo 1 > live
+
+ dpll device show
+
+To remove the device, write 0 to ``live`` and remove the device
+directory.
diff --git a/Documentation/driver-api/index.rst b/Documentation/driver-api/index.rst
index aaf5d76ed074ed..e2271889e33400 100644
--- a/Documentation/driver-api/index.rst
+++ b/Documentation/driver-api/index.rst
@@ -87,6 +87,7 @@ Subsystem-specific APIs
crypto/index
dmaengine/index
dpll
+ dpllsim
edac
extcon
firmware/index
diff --git a/MAINTAINERS b/MAINTAINERS
index 09713dc4c6a77d..147a5466902bc7 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -8070,6 +8070,13 @@ F: Documentation/networking/device_drivers/ethernet/freescale/dpaa2/switch-drive
F: drivers/net/ethernet/freescale/dpaa2/dpaa2-switch*
F: drivers/net/ethernet/freescale/dpaa2/dpsw*
+DPLL SIMULATOR DRIVER (dpllsim)
+M: Petr Oros <poros@redhat.com>
+L: netdev@vger.kernel.org
+S: Maintained
+F: Documentation/driver-api/dpllsim.rst
+F: drivers/dpll/dpllsim/
+
DPLL SUBSYSTEM
M: Vadim Fedorenko <vadim.fedorenko@linux.dev>
M: Arkadiusz Kubalewski <arkadiusz.kubalewski@intel.com>
diff --git a/drivers/dpll/Kconfig b/drivers/dpll/Kconfig
index be98969f040ab6..c6bd5392ce8753 100644
--- a/drivers/dpll/Kconfig
+++ b/drivers/dpll/Kconfig
@@ -24,5 +24,6 @@ config DPLL_REFCNT_TRACKER
If unsure, say N.
source "drivers/dpll/zl3073x/Kconfig"
+source "drivers/dpll/dpllsim/Kconfig"
endmenu
diff --git a/drivers/dpll/Makefile b/drivers/dpll/Makefile
index 9e7a3a3e592e81..1ee1daa3a46cc7 100644
--- a/drivers/dpll/Makefile
+++ b/drivers/dpll/Makefile
@@ -9,3 +9,4 @@ dpll-y += dpll_netlink.o
dpll-y += dpll_nl.o
obj-$(CONFIG_ZL3073X) += zl3073x/
+obj-$(CONFIG_DPLLSIM) += dpllsim/
diff --git a/drivers/dpll/dpllsim/Kconfig b/drivers/dpll/dpllsim/Kconfig
new file mode 100644
index 00000000000000..45ed93ba3dee5a
--- /dev/null
+++ b/drivers/dpll/dpllsim/Kconfig
@@ -0,0 +1,16 @@
+# SPDX-License-Identifier: GPL-2.0-only
+
+config DPLLSIM
+ tristate "DPLL simulator driver"
+ depends on NET
+ select DPLL
+ select CONFIGFS_FS
+ help
+ This driver provides simulated DPLL devices for testing and
+ development of the DPLL subsystem and its userspace tooling
+ without real hardware. Devices with arbitrary pin topologies
+ are created and configured at runtime through configfs,
+ similar to what gpio-sim provides for GPIO.
+
+ To compile this driver as a module, choose M here: the module
+ will be called dpllsim.
diff --git a/drivers/dpll/dpllsim/Makefile b/drivers/dpll/dpllsim/Makefile
new file mode 100644
index 00000000000000..905df2ee00874f
--- /dev/null
+++ b/drivers/dpll/dpllsim/Makefile
@@ -0,0 +1,4 @@
+# SPDX-License-Identifier: GPL-2.0-only
+
+obj-$(CONFIG_DPLLSIM) += dpllsim.o
+dpllsim-y := core.o dpll.o
diff --git a/drivers/dpll/dpllsim/core.c b/drivers/dpll/dpllsim/core.c
new file mode 100644
index 00000000000000..0772f409d013b7
--- /dev/null
+++ b/drivers/dpll/dpllsim/core.c
@@ -0,0 +1,221 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * DPLL simulator - configfs control plane
+ * Copyright (c) 2026 Red Hat, Inc.
+ */
+
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+
+#include <linux/cleanup.h>
+#include <linux/configfs.h>
+#include <linux/kstrtox.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/slab.h>
+#include <linux/sysfs.h>
+
+#include "dpllsim.h"
+
+static struct configfs_subsystem dpllsim_subsys;
+
+static struct dpllsim_dev *to_dpllsim_dev(struct config_item *item)
+{
+ return container_of(to_config_group(item), struct dpllsim_dev, group);
+}
+
+/* The clock id is checked at live=1, so both share the subsystem mutex */
+static ssize_t dpllsim_dev_clock_id_show(struct config_item *item, char *page)
+{
+ struct dpllsim_dev *sim = to_dpllsim_dev(item);
+
+ guard(mutex)(&dpllsim_subsys.su_mutex);
+ return sysfs_emit(page, "%llu\n", sim->clock_id);
+}
+
+static ssize_t dpllsim_dev_clock_id_store(struct config_item *item,
+ const char *page, size_t count)
+{
+ struct dpllsim_dev *sim = to_dpllsim_dev(item);
+ u64 val;
+ int err;
+
+ err = kstrtou64(page, 10, &val);
+ if (err)
+ return err;
+
+ guard(mutex)(&dpllsim_subsys.su_mutex);
+ if (dpllsim_dev_frozen(sim))
+ return -EBUSY;
+ sim->clock_id = val;
+ return count;
+}
+CONFIGFS_ATTR(dpllsim_dev_, clock_id);
+
+static ssize_t dpllsim_dev_live_show(struct config_item *item, char *page)
+{
+ struct dpllsim_dev *sim = to_dpllsim_dev(item);
+
+ guard(mutex)(&sim->lock);
+ return sysfs_emit(page, "%d\n", dpllsim_dev_live(sim));
+}
+
+/*
+ * Two live instances must not share a clock id, dpll_device_get() would
+ * hand out the same dpll_device to both of them instead of registering
+ * a new one.
+ */
+static bool dpllsim_clock_id_in_use(struct dpllsim_dev *sim)
+{
+ struct config_item *item;
+
+ lockdep_assert_held(&dpllsim_subsys.su_mutex);
+
+ list_for_each_entry(item, &dpllsim_subsys.su_group.cg_children,
+ ci_entry) {
+ struct dpllsim_dev *other = to_dpllsim_dev(item);
+
+ if (other != sim && dpllsim_dev_live(other) &&
+ other->clock_id == sim->clock_id)
+ return true;
+ }
+
+ return false;
+}
+
+/*
+ * The subsystem mutex excludes the live transitions of the other
+ * instances for the whole deploy sequence.
+ */
+static ssize_t dpllsim_dev_live_store(struct config_item *item,
+ const char *page, size_t count)
+{
+ struct dpllsim_dev *sim = to_dpllsim_dev(item);
+ bool live;
+ int err;
+
+ err = kstrtobool(page, &live);
+ if (err)
+ return err;
+
+ guard(mutex)(&dpllsim_subsys.su_mutex);
+
+ if (live == dpllsim_dev_live(sim))
+ return count;
+
+ if (!live) {
+ dpllsim_dev_undeploy(sim);
+ return count;
+ }
+
+ if (!sim->clock_id)
+ return -EINVAL;
+ if (dpllsim_clock_id_in_use(sim))
+ return -EEXIST;
+
+ err = dpllsim_dev_deploy(sim);
+ if (err)
+ return err;
+
+ return count;
+}
+CONFIGFS_ATTR(dpllsim_dev_, live);
+
+static struct configfs_attribute *dpllsim_dev_attrs[] = {
+ &dpllsim_dev_attr_clock_id,
+ &dpllsim_dev_attr_live,
+ NULL,
+};
+
+static void dpllsim_dev_release(struct config_item *item)
+{
+ struct dpllsim_dev *sim = to_dpllsim_dev(item);
+
+ mutex_destroy(&sim->lock);
+ kfree(sim);
+}
+
+static const struct configfs_item_operations dpllsim_dev_item_ops = {
+ .release = dpllsim_dev_release,
+};
+
+static const struct config_item_type dpllsim_dev_type = {
+ .ct_item_ops = &dpllsim_dev_item_ops,
+ .ct_attrs = dpllsim_dev_attrs,
+ .ct_owner = THIS_MODULE,
+};
+
+static struct config_group *dpllsim_make_group(struct config_group *group,
+ const char *name)
+{
+ struct dpllsim_dev *sim;
+
+ sim = kzalloc_obj(*sim);
+ if (!sim)
+ return ERR_PTR(-ENOMEM);
+
+ mutex_init(&sim->lock);
+ sim->mode = DPLL_MODE_AUTOMATIC;
+ sim->lock_status = DPLL_LOCK_STATUS_UNLOCKED;
+ sim->lock_status_error = DPLL_LOCK_STATUS_ERROR_NONE;
+
+ config_group_init_type_name(&sim->group, name, &dpllsim_dev_type);
+
+ return &sim->group;
+}
+
+/* Runs under the subsystem mutex, unlike release */
+static void dpllsim_drop_item(struct config_group *group,
+ struct config_item *item)
+{
+ struct dpllsim_dev *sim = to_dpllsim_dev(item);
+
+ if (dpllsim_dev_live(sim))
+ dpllsim_dev_undeploy(sim);
+ config_item_put(item);
+}
+
+static const struct configfs_group_operations dpllsim_group_ops = {
+ .make_group = dpllsim_make_group,
+ .drop_item = dpllsim_drop_item,
+};
+
+static const struct config_item_type dpllsim_type = {
+ .ct_group_ops = &dpllsim_group_ops,
+ .ct_owner = THIS_MODULE,
+};
+
+static struct configfs_subsystem dpllsim_subsys = {
+ .su_group = {
+ .cg_item = {
+ .ci_namebuf = "dpllsim",
+ .ci_type = &dpllsim_type,
+ },
+ },
+};
+
+static int __init dpllsim_init(void)
+{
+ int err;
+
+ config_group_init(&dpllsim_subsys.su_group);
+ mutex_init(&dpllsim_subsys.su_mutex);
+ err = configfs_register_subsystem(&dpllsim_subsys);
+ if (err) {
+ pr_err("failed to register the configfs subsystem: %d\n", err);
+ mutex_destroy(&dpllsim_subsys.su_mutex);
+ }
+
+ return err;
+}
+module_init(dpllsim_init);
+
+static void __exit dpllsim_exit(void)
+{
+ configfs_unregister_subsystem(&dpllsim_subsys);
+ mutex_destroy(&dpllsim_subsys.su_mutex);
+}
+module_exit(dpllsim_exit);
+
+MODULE_LICENSE("GPL");
+MODULE_DESCRIPTION("Simulated DPLL device driver");
+MODULE_AUTHOR("Petr Oros <poros@redhat.com>");
diff --git a/drivers/dpll/dpllsim/dpll.c b/drivers/dpll/dpllsim/dpll.c
new file mode 100644
index 00000000000000..ae2c8fcf238c68
--- /dev/null
+++ b/drivers/dpll/dpllsim/dpll.c
@@ -0,0 +1,84 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * DPLL simulator - DPLL subsystem interface
+ * Copyright (c) 2026 Red Hat, Inc.
+ */
+
+#include <linux/cleanup.h>
+#include <linux/dpll.h>
+#include <linux/module.h>
+
+#include "dpllsim.h"
+
+static int dpllsim_mode_get(const struct dpll_device *dpll, void *priv,
+ enum dpll_mode *mode,
+ struct netlink_ext_ack *extack)
+{
+ struct dpllsim_dev *sim = priv;
+
+ guard(mutex)(&sim->lock);
+ *mode = sim->mode;
+ return 0;
+}
+
+static int dpllsim_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 dpllsim_dev *sim = priv;
+
+ guard(mutex)(&sim->lock);
+ *status = sim->lock_status;
+ *status_error = sim->lock_status_error;
+ return 0;
+}
+
+static const struct dpll_device_ops dpllsim_dpll_ops = {
+ .mode_get = dpllsim_mode_get,
+ .lock_status_get = dpllsim_lock_status_get,
+};
+
+int dpllsim_dev_deploy(struct dpllsim_dev *sim)
+{
+ struct dpll_device *dpll;
+ int err;
+
+ dpll = dpll_device_get(sim->clock_id, 0, THIS_MODULE, &sim->tracker);
+ if (IS_ERR(dpll))
+ return PTR_ERR(dpll);
+
+ /* From here on the configuration is frozen */
+ mutex_lock(&sim->lock);
+ sim->dpll = dpll;
+ mutex_unlock(&sim->lock);
+
+ err = dpll_device_register(sim->dpll, DPLL_TYPE_PPS,
+ &dpllsim_dpll_ops, sim);
+ if (err)
+ goto err_put_dpll;
+
+ mutex_lock(&sim->lock);
+ sim->registered = true;
+ mutex_unlock(&sim->lock);
+
+ return 0;
+
+err_put_dpll:
+ dpll_device_put(sim->dpll, &sim->tracker);
+ mutex_lock(&sim->lock);
+ sim->dpll = NULL;
+ mutex_unlock(&sim->lock);
+ return err;
+}
+
+void dpllsim_dev_undeploy(struct dpllsim_dev *sim)
+{
+ scoped_guard(mutex, &sim->lock)
+ sim->registered = false;
+
+ dpll_device_unregister(sim->dpll, &dpllsim_dpll_ops, sim);
+ dpll_device_put(sim->dpll, &sim->tracker);
+ scoped_guard(mutex, &sim->lock)
+ sim->dpll = NULL;
+}
diff --git a/drivers/dpll/dpllsim/dpllsim.h b/drivers/dpll/dpllsim/dpllsim.h
new file mode 100644
index 00000000000000..5667f8827c98c1
--- /dev/null
+++ b/drivers/dpll/dpllsim/dpllsim.h
@@ -0,0 +1,58 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * DPLL simulator - internal header
+ * Copyright (c) 2026 Red Hat, Inc.
+ */
+
+#ifndef _DPLLSIM_H
+#define _DPLLSIM_H
+
+#include <linux/configfs.h>
+#include <linux/dpll.h>
+#include <linux/mutex.h>
+
+struct dpllsim_dev {
+ struct config_group group;
+
+ /*
+ * Set from the start of a live transition until the device is
+ * undeployed, written under both the subsystem mutex and @lock.
+ * Freezes the configuration, see dpllsim_dev_frozen().
+ */
+ struct dpll_device *dpll;
+ dpll_tracker tracker;
+ /*
+ * True while the device is registered with the DPLL subsystem,
+ * written under both the subsystem mutex and @lock. This is what
+ * the live attribute reports, see dpllsim_dev_live().
+ */
+ bool registered;
+
+ /* Guarded by the subsystem mutex */
+ u64 clock_id;
+
+ /* Simulated state, guarded by @lock */
+ enum dpll_mode mode;
+ enum dpll_lock_status lock_status;
+ enum dpll_lock_status_error lock_status_error;
+
+ /* Protects the simulated state */
+ struct mutex lock;
+};
+
+/* Configuration changes are refused from the start of a live transition */
+static inline bool dpllsim_dev_frozen(const struct dpllsim_dev *sim)
+{
+ return !!sim->dpll;
+}
+
+/* Live means registered with the DPLL subsystem */
+static inline bool dpllsim_dev_live(const struct dpllsim_dev *sim)
+{
+ return sim->registered;
+}
+
+int dpllsim_dev_deploy(struct dpllsim_dev *sim);
+void dpllsim_dev_undeploy(struct dpllsim_dev *sim);
+
+#endif /* _DPLLSIM_H */
--
2.56.0
^ permalink raw reply related [flat|nested] 31+ messages in thread* Re: [PATCH net-next v3 01/14] dpll: dpllsim: Add simulated DPLL device driver
2026-11-09 9:35 ` [PATCH net-next v3 01/14] dpll: dpllsim: Add " Petr Oros
@ 2026-11-11 9:37 ` netdev-bot+sashiko
0 siblings, 0 replies; 31+ messages in thread
From: netdev-bot+sashiko @ 2026-11-11 9:37 UTC (permalink / raw)
To: poros
Cc: netdev, ivecera, jiri, corbet, skhan, shuah, rdunlap, jani.nikula,
vadim.fedorenko, arkadiusz.kubalewski, jiri, andrew+netdev, davem,
edumazet, kuba, pabeni, linux-doc, linux-kselftest
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 message says configfs was chosen partly because
"Unlike sysfs, configfs does not imply a stable ABI, which suits a…
--
dpll: dpllsim: Add simulated DPLL device driver
This commit adds dpllsim, a driver that provides simulated DPLL devices
for testing the DPLL subsystem without real hardware. Userspace creates
instances through configfs under /sys/kernel/config/dpllsim/, sets a
clock_id and writes 1 to live to register the device with the DPLL core.
The device registers as a PPS type DPLL with only the mode_get and
lock_status_get callbacks.
> Keeping registration separate from creation lets a test register and
> unregister the same device repeatedly, which is where the core bugs the
> driver is meant to catch tend to live, and it allows the structural
> attributes to be refused with EBUSY while the device is visible to
> userspace. Unlike sysfs, configfs does not imply a stable ABI, which
> suits a driver whose knobs exist only to drive tests.
[Severity: Low]
Is this statement about configfs accurate?
The kernel has no ABI exemption for configfs. Its attributes are
user-visible interfaces and fall under the same no-regression rule as
sysfs.
The tree already documents configfs interfaces as ABI in:
Documentation/ABI/testing/configfs-most
Documentation/ABI/testing/configfs-thunderbolt_stream
Documentation/ABI/testing/configfs-tsm-report
Neither Documentation/filesystems/configfs.rst nor Documentation/ABI/README
makes an exception for configfs.
The selftests in this series will use the layout added here,
/sys/kernel/config/dpllsim/<dev>/{clock_id,live}, which later patches extend
with pins/<pin>/{type,frequency,direction,prio}. External tooling may use it
too. After that, renaming or changing these attributes would break
userspace.
The layout is currently documented only in
Documentation/driver-api/dpllsim.rst. There is no Documentation/ABI entry
and no note saying the interface is unstable or for testing only.
Could the claim be dropped from the commit message, or the interface be
documented under Documentation/ABI/testing/?
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261109093523.931201-1-poros%40redhat.com
^ permalink raw reply [flat|nested] 31+ messages in thread
* [PATCH net-next v3 02/14] selftests: dpll: add test library and device lifecycle tests
2026-11-09 9:35 [PATCH net-next v3 00/14] dpll: add a simulated DPLL device driver Petr Oros
2026-11-09 9:35 ` [PATCH net-next v3 01/14] dpll: dpllsim: Add " Petr Oros
@ 2026-11-09 9:35 ` Petr Oros
2026-11-09 13:56 ` Ivan Vecera
2026-11-11 9:37 ` netdev-bot+sashiko
2026-11-09 9:35 ` [PATCH net-next v3 03/14] dpll: dpllsim: Add configurable pins Petr Oros
` (11 subsequent siblings)
13 siblings, 2 replies; 31+ messages in thread
From: Petr Oros @ 2026-11-09 9:35 UTC (permalink / raw)
To: netdev
Cc: Petr Oros, Jonathan Corbet, Shuah Khan, Shuah Khan, Randy Dunlap,
Jani Nikula, Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
Ivan Vecera, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, linux-doc, linux-kselftest
Add a shell library for DPLL selftests with wrappers around the iproute2
dpll tool and around the dpllsim configfs interface, and the first two
test scripts built on it. The library resolves devices by the clock ID
chosen at creation time, since netlink ids depend on what else exists
on the system, and the dpllsim helpers restrict the lookup to their
module, so that a device of another driver with the same clock ID is
never picked up. Every query runs the tool first and jq on what it
printed, so that a failed query is told apart from a missing attribute.
The library mounts configfs when nothing else did, and the exit trap of
each script undoes that, removes the devices the script created and
runs on INT and TERM as well, so that a timeout imposed by the harness
still cleans up.
Two runs of scripts that create dpllsim devices would share the one
dpllsim directory and could draw the same clock IDs, so every such
script takes a lock before it mounts or creates anything and skips when
another run holds it. The lock file lives under /run rather than in
/tmp, since the scripts run as root and open the file for writing, and
a symlink planted in the world-writable /tmp would redirect that open.
dpll_basic.sh checks that a device reports the mandatory attributes
over netlink. It runs against the device named by DPLL_DEV_ID, the
first device of the driver named by DPLL_DRIVER, or the first device
that is not a dpllsim one, and creates a dpllsim device when there is
none or when DPLL_DRIVER asks for dpllsim, so the same script serves
the simulator and real drivers and picks one of several drivers on a
host. dpll_stress.sh is simulator only and exercises the lifecycle
paths of the core by creating and deleting devices in a loop and by
cycling one device through repeated register and unregister
transitions. The dpllsim MAINTAINERS entry covers the new directory.
Signed-off-by: Petr Oros <poros@redhat.com>
---
v3:
- move the selftests to tools/testing/selftests/drivers/dpll, next to
where the drivers live, with the library in that directory as a
TEST_FILES entry; the TARGETS and MAINTAINERS entries follow
- add DPLL_DRIVER, which picks the first device of the named driver
or, as dpllsim, a created simulated device on a system that has
hardware DPLL devices
- the Reviewed-by and Acked-by of v2 are not carried over, the
device selection changed
v2:
- sort the config file alphabetically (netdev CI check_selftest)
- quote the ksft_skip and ret expansions and add shellcheck directives
for the globals read by the scripts, for ksft_skip, which comes from
net/lib.sh, and for the optional argument of
dpllsim_setup_standard_device() (netdev CI shellcheck)
v2: https://lore.kernel.org/all/20261105151852.1844305-3-poros@redhat.com/
v1: https://lore.kernel.org/all/20261103093910.596225-3-poros@redhat.com/
---
MAINTAINERS | 1 +
tools/testing/selftests/Makefile | 1 +
tools/testing/selftests/drivers/dpll/Makefile | 15 +
tools/testing/selftests/drivers/dpll/config | 3 +
.../selftests/drivers/dpll/dpll_basic.sh | 94 +++++
.../selftests/drivers/dpll/dpll_lib.sh | 348 ++++++++++++++++++
.../selftests/drivers/dpll/dpll_stress.sh | 164 +++++++++
tools/testing/selftests/drivers/dpll/settings | 1 +
8 files changed, 627 insertions(+)
create mode 100644 tools/testing/selftests/drivers/dpll/Makefile
create mode 100644 tools/testing/selftests/drivers/dpll/config
create mode 100755 tools/testing/selftests/drivers/dpll/dpll_basic.sh
create mode 100644 tools/testing/selftests/drivers/dpll/dpll_lib.sh
create mode 100755 tools/testing/selftests/drivers/dpll/dpll_stress.sh
create mode 100644 tools/testing/selftests/drivers/dpll/settings
diff --git a/MAINTAINERS b/MAINTAINERS
index 147a5466902bc7..ab75cf8a6507c3 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -8076,6 +8076,7 @@ L: netdev@vger.kernel.org
S: Maintained
F: Documentation/driver-api/dpllsim.rst
F: drivers/dpll/dpllsim/
+F: tools/testing/selftests/drivers/dpll/
DPLL SUBSYSTEM
M: Vadim Fedorenko <vadim.fedorenko@linux.dev>
diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile
index 6c77beba0956ef..a67c3284ebbddc 100644
--- a/tools/testing/selftests/Makefile
+++ b/tools/testing/selftests/Makefile
@@ -20,6 +20,7 @@ TARGETS += devices/error_logs
TARGETS += devices/probe
TARGETS += dmabuf-heaps
TARGETS += drivers/dma-buf
+TARGETS += drivers/dpll
TARGETS += drivers/ntsync
TARGETS += drivers/s390x/uvdevice
TARGETS += drivers/net
diff --git a/tools/testing/selftests/drivers/dpll/Makefile b/tools/testing/selftests/drivers/dpll/Makefile
new file mode 100644
index 00000000000000..7e839ae71cd22f
--- /dev/null
+++ b/tools/testing/selftests/drivers/dpll/Makefile
@@ -0,0 +1,15 @@
+# SPDX-License-Identifier: GPL-2.0
+
+TEST_INCLUDES := \
+ $(wildcard ../../net/lib/sh/*.sh) \
+ ../../net/lib.sh \
+# end of TEST_INCLUDES
+
+TEST_FILES := dpll_lib.sh
+
+TEST_PROGS := \
+ dpll_basic.sh \
+ dpll_stress.sh \
+# end of TEST_PROGS
+
+include ../../lib.mk
diff --git a/tools/testing/selftests/drivers/dpll/config b/tools/testing/selftests/drivers/dpll/config
new file mode 100644
index 00000000000000..e2c90b6970bd43
--- /dev/null
+++ b/tools/testing/selftests/drivers/dpll/config
@@ -0,0 +1,3 @@
+CONFIG_CONFIGFS_FS=y
+CONFIG_DPLL=y
+CONFIG_DPLLSIM=m
diff --git a/tools/testing/selftests/drivers/dpll/dpll_basic.sh b/tools/testing/selftests/drivers/dpll/dpll_basic.sh
new file mode 100755
index 00000000000000..827c389fe1aa31
--- /dev/null
+++ b/tools/testing/selftests/drivers/dpll/dpll_basic.sh
@@ -0,0 +1,94 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# Basic DPLL subsystem test. Read-only validation of the device properties
+# exposed over netlink. Runs against a hardware DPLL device when one is
+# present (or when DPLL_DEV_ID is set), otherwise against a freshly
+# created dpllsim device. DPLL_DRIVER names the driver to take the
+# device from, dpllsim for a created one although hardware is present.
+#
+# Copyright (c) 2026 Red Hat, Inc.
+
+SCRIPTDIR=$(dirname "$(readlink -e "${BASH_SOURCE[0]}")")
+
+source "$SCRIPTDIR/../../net/lib.sh"
+source "$SCRIPTDIR/dpll_lib.sh"
+
+ALL_TESTS="
+ test_device_properties
+"
+
+test_device_properties()
+{
+ local json
+
+ RET=0
+
+ # A device that cannot be queried at all would be reported as one
+ # that misses every attribute, so bail out on that first.
+ if ! json=$(dpll_device_json "$DPLL_DEV_ID") || [ -z "$json" ]; then
+ check_err 1 "device query failed"
+ log_test "DPLL device properties"
+ return
+ fi
+
+ local field
+ for field in '.id' '.["module-name"]' '.["clock-id"]' '.type' \
+ '.mode' '.["lock-status"]'; do
+ if ! jq -e "$field" <<< "$json" &> /dev/null; then
+ check_err 1 "device is missing the $field attribute"
+ fi
+ done
+
+ log_test "DPLL device properties"
+}
+
+##############################################################################
+
+cleanup()
+{
+ # A second signal must not abort the teardown half way.
+ trap '' INT TERM
+
+ if [ -n "$CREATED_DEVICE" ]; then
+ dpllsim_delete_device "$CREATED_DEVICE" ||
+ log_info "cannot delete device $CREATED_DEVICE"
+ fi
+ # The device whose creation a signal interrupted is recorded by the
+ # library alone, a double delete of a tracked one is harmless.
+ if [ -n "$DPLLSIM_CREATED_NAME" ]; then
+ dpllsim_delete_device "$DPLLSIM_CREATED_NAME" ||
+ log_info "cannot delete device $DPLLSIM_CREATED_NAME"
+ fi
+ dpll_lib_cleanup
+}
+
+dpll_lib_check_prereqs
+# Another run would find the devices of this one left behind, see the
+# lock in the library.
+dpll_lib_lock
+dpll_lib_init
+
+CREATED_DEVICE=""
+# The module check may mount configfs, so the exit trap that unmounts it
+# again has to be in place before. INT and TERM exit through it as well,
+# so that a timeout imposed by the harness still removes the device.
+trap cleanup EXIT
+trap 'exit 1' INT TERM
+
+if [ -z "$DPLL_DEV_ID" ]; then
+ dpllsim_check_module
+ if ! dpllsim_setup_standard_device; then
+ echo "SKIP: cannot create dpllsim device"
+ exit "$ksft_skip"
+ fi
+ CREATED_DEVICE=$DPLLSIM_CREATED_NAME
+ DPLL_DEV_ID=$DPLLSIM_DEV_ID
+ DPLL_MODULE_NAME="dpllsim"
+fi
+
+log_info "testing DPLL device $DPLL_DEV_ID (module $DPLL_MODULE_NAME)"
+
+tests_run
+
+exit $EXIT_STATUS
diff --git a/tools/testing/selftests/drivers/dpll/dpll_lib.sh b/tools/testing/selftests/drivers/dpll/dpll_lib.sh
new file mode 100644
index 00000000000000..8d46d4cff06c58
--- /dev/null
+++ b/tools/testing/selftests/drivers/dpll/dpll_lib.sh
@@ -0,0 +1,348 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# Helpers for DPLL selftests. Provides wrappers around the iproute2 dpll
+# tool and around the dpllsim configfs interface for creating virtual DPLL
+# devices. Sourcing this file has no side effects; call
+# dpll_lib_check_prereqs and dpll_lib_init explicitly. net/lib.sh must be
+# sourced first, it provides ksft_skip, busywait and log_info.
+#
+# Copyright (c) 2026 Red Hat, Inc.
+#
+# shellcheck disable=SC2034,SC2154 # the globals set here are read by the
+# scripts and ksft_skip is defined by net/lib.sh
+
+CONFIGFS_DIR="/sys/kernel/config"
+DPLLSIM_CFG="$CONFIGFS_DIR/dpllsim"
+
+# Next clock-id handed out by dpllsim_alloc_clock_id. The lookups of the
+# dpllsim helpers match on the module name as well, which is what keeps
+# the tests away from the devices of other drivers whatever clock-id
+# those use. The random start only avoids collisions between runs, as
+# the devices are named after their clock-id in configfs.
+DPLLSIM_NEXT_CLOCK_ID=$((96000 + RANDOM))
+
+##############################################################################
+# Prerequisites and device selection
+
+dpll_lib_check_prereqs()
+{
+ if [ "$EUID" -ne 0 ]; then
+ echo "SKIP: must be run as root"
+ exit "$ksft_skip"
+ fi
+
+ local cmd
+ for cmd in jq dpll; do
+ if ! command -v "$cmd" &> /dev/null; then
+ echo "SKIP: $cmd not found"
+ exit "$ksft_skip"
+ fi
+ done
+
+ if ! dpll device help 2>&1 | grep -q "show"; then
+ echo "SKIP: dpll tool too old, missing 'device show'"
+ exit "$ksft_skip"
+ fi
+}
+
+# The scripts that create dpllsim devices share the one dpllsim directory
+# and cannot run alongside each other, so each takes a lock on
+# DPLL_LOCK_FILE before it mounts or creates anything and skips when
+# another run holds it. The file lives under /run rather than in the
+# world-writable /tmp, where a planted symlink could redirect the open.
+DPLL_RUN_DIR="/run/dpll-selftest"
+DPLL_LOCK_FILE="$DPLL_RUN_DIR/lock"
+DPLL_LOCK_FD=""
+
+# Create DPLL_RUN_DIR when it is missing, skip when that fails.
+dpll_lib_run_dir()
+{
+ mkdir -p "$DPLL_RUN_DIR" 2>/dev/null
+ if ! [ -d "$DPLL_RUN_DIR" ]; then
+ echo "SKIP: cannot create $DPLL_RUN_DIR"
+ exit "$ksft_skip"
+ fi
+}
+
+dpll_lib_lock()
+{
+ if ! command -v flock &> /dev/null; then
+ echo "SKIP: flock not found"
+ exit "$ksft_skip"
+ fi
+ dpll_lib_run_dir
+ if ! { exec {DPLL_LOCK_FD}> "$DPLL_LOCK_FILE"; } 2>/dev/null; then
+ echo "SKIP: cannot open $DPLL_LOCK_FILE"
+ exit "$ksft_skip"
+ fi
+ if ! flock -n "$DPLL_LOCK_FD"; then
+ echo "SKIP: another dpll selftest run is in progress"
+ exit "$ksft_skip"
+ fi
+}
+
+# Detect the DPLL device to test. DPLL_DEV_ID may be set by the user to
+# force a specific device. Otherwise DPLL_DRIVER names the driver whose
+# first device is taken, by the module name the device reports, dpllsim
+# standing for a simulated device the script creates, and a system
+# without a device of that driver is a skip. With neither set the first
+# device that is not a dpllsim one is taken, so that an instance of the
+# simulator left behind by an earlier run is not mistaken for hardware.
+# Sets DPLL_DEV_ID (may end up empty) and DPLL_MODULE_NAME.
+dpll_lib_init()
+{
+ DPLL_MODULE_NAME=""
+
+ if [ -n "${DPLL_DEV_ID:-}" ]; then
+ if ! dpll device show id "$DPLL_DEV_ID" &> /dev/null; then
+ echo "SKIP: DPLL device \"$DPLL_DEV_ID\" not found"
+ exit "$ksft_skip"
+ fi
+ elif [ "${DPLL_DRIVER:-}" = "dpllsim" ]; then
+ DPLL_DEV_ID=""
+ elif [ -n "${DPLL_DRIVER:-}" ]; then
+ DPLL_DEV_ID=$(dpll device show -j 2>/dev/null | \
+ jq -r --arg mod "$DPLL_DRIVER" '[.device[]? |
+ select(.["module-name"] == $mod)
+ ][0].id // empty')
+ if [ -z "$DPLL_DEV_ID" ]; then
+ echo "SKIP: no DPLL device of driver \"$DPLL_DRIVER\""
+ exit "$ksft_skip"
+ fi
+ else
+ DPLL_DEV_ID=$(dpll device show -j 2>/dev/null | \
+ jq -r '[.device[]? |
+ select(.["module-name"] != "dpllsim")
+ ][0].id // empty')
+ fi
+
+ if [ -n "$DPLL_DEV_ID" ]; then
+ DPLL_MODULE_NAME=$(dpll_device_get_module "$DPLL_DEV_ID")
+ fi
+}
+
+##############################################################################
+# Device queries
+#
+# "dpll device show id X -j" may print either a flat object or a dump-style
+# {"device":[...]} wrapper depending on the iproute2 version, so normalize
+# with (.device[0]? // .) before extracting fields. The tool is run first
+# and jq only on what it printed, so that a failed query returns its
+# status instead of the empty output a missing field produces too.
+
+dpll_device_json()
+{
+ local dev_id=$1
+ local out
+
+ out=$(dpll device show id "$dev_id" -j 2>/dev/null) || return 1
+ jq '(.device[0]? // .)' <<< "$out"
+}
+
+__dpll_device_field()
+{
+ local dev_id=$1; shift
+ local out
+
+ out=$(dpll device show id "$dev_id" -j 2>/dev/null) || return 1
+ jq -r "(.device[0]? // .) | $1 // empty" <<< "$out"
+}
+
+dpll_device_get_module()
+{
+ __dpll_device_field "$1" '.["module-name"]'
+}
+
+# Look up the netlink device id by the clock-id chosen at create time,
+# restricted to the devices of the given module when one is named. The
+# clock-id is the only reliable handle on a device the test created, as
+# netlink ids depend on what else exists, and the module name keeps a
+# lookup for a dpllsim device from returning a device of another driver
+# that happens to use the same clock-id. The clock-id is compared as a
+# string, so that it does not matter whether the tool prints it as a
+# JSON number or as a string. Return 1 when the dump itself failed, a
+# caller must not take that for a device that is gone.
+# $1: clock-id, $2: module name (optional)
+dpll_device_get_id_by_clock_id()
+{
+ local clock_id=$1
+ local module=${2:-}
+ local out
+
+ out=$(dpll device show -j 2>/dev/null) || return 1
+ jq -r --arg cid "$clock_id" --arg mod "$module" \
+ '[.device[]? |
+ select((.["clock-id"] | tostring) == $cid and
+ ($mod == "" or .["module-name"] == $mod))
+ ][0].id // empty' <<< "$out"
+}
+
+# The two checks below fail when the lookup itself failed, so that a
+# failed dump neither counts as a device that is present nor as one
+# that is gone, the wait helpers keep polling instead.
+__dpll_device_present_by_clock_id()
+{
+ local dev_id
+
+ dev_id=$(dpll_device_get_id_by_clock_id "$@") || return 1
+ [ -n "$dev_id" ] && echo "$dev_id"
+}
+
+__dpll_device_absent_by_clock_id()
+{
+ local dev_id
+
+ dev_id=$(dpll_device_get_id_by_clock_id "$@") || return 1
+ [ -z "$dev_id" ]
+}
+
+# Wait until a device with the given clock-id (and module, when given) is
+# visible on netlink and print its netlink id. Callers must check the
+# exit status rather than the output, busywait prints the output of the
+# last failed check when it times out.
+dpll_device_wait_by_clock_id()
+{
+ busywait "$BUSYWAIT_TIMEOUT" __dpll_device_present_by_clock_id "$@"
+}
+
+dpll_device_wait_gone_by_clock_id()
+{
+ busywait "$BUSYWAIT_TIMEOUT" __dpll_device_absent_by_clock_id "$@"
+}
+
+##############################################################################
+# dpllsim configfs interface
+#
+# Each directory under $DPLLSIM_CFG is one simulated device. Writing 1 to
+# <dev>/live registers the device with the DPLL core, writing 0
+# unregisters it. The structure (clock_id) can only be changed while the
+# device is not live.
+#
+# dpllsim_available mounts configfs when nothing else did, which is the
+# one side effect the library has outside the dpllsim directory. Scripts
+# call dpll_lib_cleanup last from their exit trap to undo it.
+
+DPLLSIM_MOUNTED_CONFIGFS=0
+
+dpllsim_available()
+{
+ [ -d "$DPLLSIM_CFG" ] && return 0
+
+ modprobe dpllsim &> /dev/null
+ if ! mountpoint -q "$CONFIGFS_DIR"; then
+ mount -t configfs none "$CONFIGFS_DIR" &> /dev/null &&
+ DPLLSIM_MOUNTED_CONFIGFS=1
+ fi
+ [ -d "$DPLLSIM_CFG" ]
+}
+
+# Unmount configfs again when dpllsim_available mounted it. Call after
+# the devices of the script are gone. A dpllsim device the script could
+# not remove keeps the mount, so that what is left can be looked at.
+dpll_lib_cleanup()
+{
+ local dev_path name
+
+ [ "$DPLLSIM_MOUNTED_CONFIGFS" -eq 1 ] || return 0
+ DPLLSIM_MOUNTED_CONFIGFS=0
+
+ for dev_path in "$DPLLSIM_CFG"/*/; do
+ [ -d "$dev_path" ] || continue
+ name=$(basename "$dev_path")
+ log_info "device $name left behind, $CONFIGFS_DIR kept mounted"
+ return
+ done
+ umount "$CONFIGFS_DIR" 2>/dev/null ||
+ log_info "cannot unmount $CONFIGFS_DIR"
+}
+
+dpllsim_check_module()
+{
+ if ! dpllsim_available; then
+ echo "SKIP: dpllsim module not available"
+ exit "$ksft_skip"
+ fi
+}
+
+# $1: device name, $2: clock-id. Sets DPLLSIM_CREATED_NAME as soon as the
+# directory exists, so that the cleanup handler of a script can remove a
+# device whose creation a signal interrupted before the script recorded
+# it. The name stays set once the device is complete.
+dpllsim_create_device()
+{
+ local name=$1
+ local clock_id=$2
+ local dev_path="$DPLLSIM_CFG/$name"
+
+ # Record the name before the directory exists, so that a cleanup
+ # run by a signal between the two still finds it.
+ DPLLSIM_CREATED_NAME=$name
+ if ! mkdir "$dev_path" 2>/dev/null; then
+ DPLLSIM_CREATED_NAME=""
+ return 1
+ fi
+ if ! echo "$clock_id" 2>/dev/null > "$dev_path/clock_id"; then
+ rmdir "$dev_path" 2>/dev/null
+ DPLLSIM_CREATED_NAME=""
+ return 1
+ fi
+}
+
+dpllsim_deploy()
+{
+ echo 1 2>/dev/null > "$DPLLSIM_CFG/$1/live"
+}
+
+dpllsim_undeploy()
+{
+ echo 0 2>/dev/null > "$DPLLSIM_CFG/$1/live"
+}
+
+# Take the device offline and remove it. Every step is attempted even
+# when an earlier one failed, the return value is non-zero when any did.
+# A device that does not exist counts as deleted.
+dpllsim_delete_device()
+{
+ local dev_path="$DPLLSIM_CFG/$1"
+ local ret=0
+
+ [ -d "$dev_path" ] || return 0
+
+ dpllsim_undeploy "$1" || ret=1
+ rmdir "$dev_path" || ret=1
+
+ return "$ret"
+}
+
+# Set DPLLSIM_CLOCK_ID to a clock-id unique within this test run. Must
+# not be called from a subshell (the counter increment would be lost);
+# derive per-worker id ranges in the parent instead.
+dpllsim_alloc_clock_id()
+{
+ DPLLSIM_CLOCK_ID=$((DPLLSIM_NEXT_CLOCK_ID++))
+}
+
+# Create a device, deploy it and resolve the netlink device id. Sets
+# DPLLSIM_CREATED_NAME, DPLLSIM_CLOCK_ID and DPLLSIM_DEV_ID.
+# shellcheck disable=SC2120 # the argument is optional
+dpllsim_setup_standard_device()
+{
+ local clock_id name
+
+ dpllsim_alloc_clock_id
+ clock_id=$DPLLSIM_CLOCK_ID
+ name="std$clock_id"
+ dpllsim_create_device "$name" "$clock_id" || return 1
+
+ if ! dpllsim_deploy "$name"; then
+ dpllsim_delete_device "$name"
+ return 1
+ fi
+
+ if ! DPLLSIM_DEV_ID=$(dpll_device_wait_by_clock_id "$clock_id" \
+ dpllsim); then
+ dpllsim_delete_device "$name"
+ return 1
+ fi
+}
diff --git a/tools/testing/selftests/drivers/dpll/dpll_stress.sh b/tools/testing/selftests/drivers/dpll/dpll_stress.sh
new file mode 100755
index 00000000000000..01d57ab0172b0f
--- /dev/null
+++ b/tools/testing/selftests/drivers/dpll/dpll_stress.sh
@@ -0,0 +1,164 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# DPLL subsystem stress test. Uses dpllsim to exercise rapid device
+# registration/unregistration, concurrent operations from multiple
+# processes, pin state change flooding and notification load, then
+# verifies via ref_tracker that no references leaked.
+#
+# Copyright (c) 2026 Red Hat, Inc.
+
+SCRIPTDIR=$(dirname "$(readlink -e "${BASH_SOURCE[0]}")")
+
+source "$SCRIPTDIR/../../net/lib.sh"
+source "$SCRIPTDIR/dpll_lib.sh"
+
+ALL_TESTS="
+ test_rapid_create_delete
+ test_deploy_cycle
+"
+
+STRESS_ITERATIONS=${STRESS_ITERATIONS:-20}
+
+CREATED_DEVICES=()
+
+track_device()
+{
+ CREATED_DEVICES+=("$1")
+}
+
+untrack_device()
+{
+ local name=$1 dev
+ local remaining=()
+
+ for dev in "${CREATED_DEVICES[@]}"; do
+ [ "$dev" = "$name" ] || remaining+=("$dev")
+ done
+ CREATED_DEVICES=("${remaining[@]}")
+}
+
+cleanup()
+{
+ local dev
+
+ # A second signal must not abort the teardown half way.
+ trap '' INT TERM
+
+ for dev in "${CREATED_DEVICES[@]}"; do
+ dpllsim_delete_device "$dev" ||
+ log_info "cannot delete device $dev"
+ done
+ # The device whose creation a signal interrupted is recorded by the
+ # library alone, a double delete of a tracked one is harmless.
+ if [ -n "$DPLLSIM_CREATED_NAME" ]; then
+ dpllsim_delete_device "$DPLLSIM_CREATED_NAME" ||
+ log_info "cannot delete device $DPLLSIM_CREATED_NAME"
+ fi
+ dpll_lib_cleanup
+}
+
+# Create a device named after the given clock-id and deploy it. Sets
+# DPLLSIM_CREATED_NAME.
+create_stress_device()
+{
+ local clock_id=$1
+ local name="st$clock_id"
+
+ dpllsim_create_device "$name" "$clock_id" || return 1
+
+ if ! dpllsim_deploy "$name"; then
+ dpllsim_delete_device "$name"
+ return 1
+ fi
+}
+
+##############################################################################
+
+test_rapid_create_delete()
+{
+ local i clock_id name
+
+ RET=0
+
+ for i in $(seq 1 "$STRESS_ITERATIONS"); do
+ dpllsim_alloc_clock_id; clock_id=$DPLLSIM_CLOCK_ID
+
+ if ! create_stress_device "$clock_id"; then
+ check_err 1 "failed to create device (iteration $i)"
+ break
+ fi
+ name=$DPLLSIM_CREATED_NAME
+ track_device "$name"
+
+ if ! dpllsim_delete_device "$name"; then
+ check_err 1 "failed to delete device (iteration $i)"
+ break
+ fi
+ untrack_device "$name"
+ done
+
+ log_test "DPLL rapid create/delete ($STRESS_ITERATIONS iterations)"
+}
+
+test_deploy_cycle()
+{
+ local clock_id name i
+
+ RET=0
+
+ dpllsim_alloc_clock_id; clock_id=$DPLLSIM_CLOCK_ID
+ name="cycle$clock_id"
+ if ! dpllsim_create_device "$name" "$clock_id"; then
+ check_err 1 "cannot create device"
+ log_test "DPLL deploy/undeploy cycling"
+ return
+ fi
+ track_device "$name"
+
+ for i in $(seq 1 "$STRESS_ITERATIONS"); do
+ if ! dpllsim_deploy "$name"; then
+ check_err 1 "deploy failed (iteration $i)"
+ break
+ fi
+ if ! dpll_device_wait_by_clock_id "$clock_id" dpllsim \
+ > /dev/null; then
+ check_err 1 "device not on netlink (iteration $i)"
+ break
+ fi
+ if ! dpllsim_undeploy "$name"; then
+ check_err 1 "undeploy failed (iteration $i)"
+ break
+ fi
+ if ! dpll_device_wait_gone_by_clock_id "$clock_id" dpllsim; then
+ check_err 1 "device still on netlink (iteration $i)"
+ break
+ fi
+ done
+
+ if dpllsim_delete_device "$name"; then
+ untrack_device "$name"
+ else
+ check_err 1 "cannot delete device $name"
+ fi
+
+ log_test "DPLL deploy/undeploy cycling"
+}
+
+##############################################################################
+
+dpll_lib_check_prereqs
+# Another run would find the devices of this one left behind, see the
+# lock in the library.
+dpll_lib_lock
+
+# The module check may mount configfs, so the exit trap that unmounts it
+# again has to be in place before. INT and TERM exit through it as well,
+# so that a timeout imposed by the harness still removes the devices.
+trap cleanup EXIT
+trap 'exit 1' INT TERM
+dpllsim_check_module
+
+tests_run
+
+exit $EXIT_STATUS
diff --git a/tools/testing/selftests/drivers/dpll/settings b/tools/testing/selftests/drivers/dpll/settings
new file mode 100644
index 00000000000000..a62d2fa1275c6b
--- /dev/null
+++ b/tools/testing/selftests/drivers/dpll/settings
@@ -0,0 +1 @@
+timeout=600
--
2.56.0
^ permalink raw reply related [flat|nested] 31+ messages in thread* Re: [PATCH net-next v3 02/14] selftests: dpll: add test library and device lifecycle tests
2026-11-09 9:35 ` [PATCH net-next v3 02/14] selftests: dpll: add test library and device lifecycle tests Petr Oros
@ 2026-11-09 13:56 ` Ivan Vecera
2026-11-11 9:37 ` netdev-bot+sashiko
1 sibling, 0 replies; 31+ messages in thread
From: Ivan Vecera @ 2026-11-09 13:56 UTC (permalink / raw)
To: Petr Oros, netdev
Cc: Jonathan Corbet, Shuah Khan, Shuah Khan, Randy Dunlap,
Jani Nikula, Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni, linux-doc, linux-kselftest
On 11/9/26 10:35, Petr Oros wrote:
> Add a shell library for DPLL selftests with wrappers around the iproute2
> dpll tool and around the dpllsim configfs interface, and the first two
> test scripts built on it. The library resolves devices by the clock ID
> chosen at creation time, since netlink ids depend on what else exists
> on the system, and the dpllsim helpers restrict the lookup to their
> module, so that a device of another driver with the same clock ID is
> never picked up. Every query runs the tool first and jq on what it
> printed, so that a failed query is told apart from a missing attribute.
> The library mounts configfs when nothing else did, and the exit trap of
> each script undoes that, removes the devices the script created and
> runs on INT and TERM as well, so that a timeout imposed by the harness
> still cleans up.
>
> Two runs of scripts that create dpllsim devices would share the one
> dpllsim directory and could draw the same clock IDs, so every such
> script takes a lock before it mounts or creates anything and skips when
> another run holds it. The lock file lives under /run rather than in
> /tmp, since the scripts run as root and open the file for writing, and
> a symlink planted in the world-writable /tmp would redirect that open.
>
> dpll_basic.sh checks that a device reports the mandatory attributes
> over netlink. It runs against the device named by DPLL_DEV_ID, the
> first device of the driver named by DPLL_DRIVER, or the first device
> that is not a dpllsim one, and creates a dpllsim device when there is
> none or when DPLL_DRIVER asks for dpllsim, so the same script serves
> the simulator and real drivers and picks one of several drivers on a
> host. dpll_stress.sh is simulator only and exercises the lifecycle
> paths of the core by creating and deleting devices in a loop and by
> cycling one device through repeated register and unregister
> transitions. The dpllsim MAINTAINERS entry covers the new directory.
>
> Signed-off-by: Petr Oros<poros@redhat.com>
> ---
> v3:
> - move the selftests to tools/testing/selftests/drivers/dpll, next to
> where the drivers live, with the library in that directory as a
> TEST_FILES entry; the TARGETS and MAINTAINERS entries follow
> - add DPLL_DRIVER, which picks the first device of the named driver
> or, as dpllsim, a created simulated device on a system that has
> hardware DPLL devices
> - the Reviewed-by and Acked-by of v2 are not carried over, the
> device selection changed
Reviewed-by: Ivan Vecera <ivecera@redhat.com>
^ permalink raw reply [flat|nested] 31+ messages in thread
* Re: [PATCH net-next v3 02/14] selftests: dpll: add test library and device lifecycle tests
2026-11-09 9:35 ` [PATCH net-next v3 02/14] selftests: dpll: add test library and device lifecycle tests Petr Oros
2026-11-09 13:56 ` Ivan Vecera
@ 2026-11-11 9:37 ` netdev-bot+sashiko
1 sibling, 0 replies; 31+ messages in thread
From: netdev-bot+sashiko @ 2026-11-11 9:37 UTC (permalink / raw)
To: poros
Cc: netdev, ivecera, jiri, corbet, skhan, shuah, rdunlap, jani.nikula,
vadim.fedorenko, arkadiusz.kubalewski, jiri, andrew+netdev, davem,
edumazet, kuba, pabeni, linux-doc, linux-kselftest
Thank you for your contribution! Sashiko AI review found 7 potential
issue(s) to consider.
Critical: 0 · High: 0 · Medium: 2 · Low: 5
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 dpll_lib_init()
(tools/testing/selftests/drivers/dpll/dpll_lib.sh), the DPLL_DRIVER…
- [Medium] dpll_basic.sh exits with ksft_skip whenever
dpllsim_setup_standard_device() fails (`echo "SKIP: cannot create…
- [Low] Several comments in dpll_lib.sh do not match what the code does.
- [Low] DPLLSIM_CREATED_NAME is never initialized at the top level of
dpll_lib.sh or by either script.
- [Low] dpllsim_create_device() sets DPLLSIM_CREATED_NAME=$name before
mkdir, so the cleanup handler counts the device as created by this run.
- [Low] dpll_lib_cleanup() unmounts /sys/kernel/config whenever this run
did the mount and no dpllsim item is left.
- [Low] In dpllsim_available(), the flag recording that this run mounted
configfs is set only after the mount: `mount -t configfs none…
--
selftests: dpll: add test library and device lifecycle tests
This commit adds a shell library for DPLL selftests that wraps the
iproute2 dpll tool and the dpllsim configfs interface. It also adds
dpll_basic.sh, which checks mandatory device attributes against hardware
or a created dpllsim device, and dpll_stress.sh, which cycles dpllsim
devices through create/delete and register/unregister. The scripts take
a lock under /run and clean up from an exit trap.
> diff --git a/tools/testing/selftests/drivers/dpll/dpll_basic.sh b/tools/testing/selftests/drivers/dpll/dpll_basic.sh
> new file mode 100755
> index 0000000000000..827c389fe1aa3
> --- /dev/null
> +++ b/tools/testing/selftests/drivers/dpll/dpll_basic.sh
> @@ -0,0 +1,94 @@
[ ... ]
> +cleanup()
> +{
> + # A second signal must not abort the teardown half way.
> + trap '' INT TERM
> +
> + if [ -n "$CREATED_DEVICE" ]; then
> + dpllsim_delete_device "$CREATED_DEVICE" ||
> + log_info "cannot delete device $CREATED_DEVICE"
> + fi
> + # The device whose creation a signal interrupted is recorded by the
> + # library alone, a double delete of a tracked one is harmless.
> + if [ -n "$DPLLSIM_CREATED_NAME" ]; then
> + dpllsim_delete_device "$DPLLSIM_CREATED_NAME" ||
> + log_info "cannot delete device $DPLLSIM_CREATED_NAME"
> + fi
[Severity: Low]
Is DPLLSIM_CREATED_NAME always empty here when this run never created a
device? It is only assigned inside dpllsim_create_device(). Neither
dpll_lib.sh nor the scripts initialize it at the top level, unlike
DPLLSIM_MOUNTED_CONFIGFS=0 and DPLL_LOCK_FD="" here, and
DPLL_CREATED_DEVICE="" and DPLLSIM_PROBE_DEVICE="" later in the series.
Bash imports environment variables as shell variables. If
DPLLSIM_CREATED_NAME is set in the inherited environment, this EXIT trap
undeploys and removes an existing $DPLLSIM_CFG/<name> device even on
paths that never create anything. Examples are a run against hardware,
the dpllsim_check_module() skip, or an early signal.
cleanup() in dpll_stress.sh has the same check, and the variable is
still uninitialized at the end of the series. Would a top-level
DPLLSIM_CREATED_NAME="" in dpll_lib.sh be enough?
> + dpll_lib_cleanup
> +}
[ ... ]
> +if [ -z "$DPLL_DEV_ID" ]; then
> + dpllsim_check_module
> + if ! dpllsim_setup_standard_device; then
> + echo "SKIP: cannot create dpllsim device"
> + exit "$ksft_skip"
> + fi
[Severity: Medium]
Should every failure of dpllsim_setup_standard_device() be reported as a
skip? That function returns 1 in three cases: the configfs mkdir fails,
the write of 1 to <dev>/live fails, or dpll_device_wait_by_clock_id()
times out.
The live write goes to dpllsim_dev_live_store(), which passes back the
DPLL core registration error:
drivers/dpll/dpllsim/core.c:dpllsim_dev_live_store() {
...
err = dpllsim_dev_deploy(sim);
if (err)
return err;
...
}
The timeout means the registered device never showed up in the netlink
dump. Both are failures of the core and netlink paths this test covers.
dpll_stress.sh treats the same condition as a failure in
test_deploy_cycle():
check_err 1 "device not on netlink (iteration $i)"
Could only environment problems be skipped here, with deploy and netlink
visibility failures reported as failures? This check is unchanged at the
end of the series.
> + CREATED_DEVICE=$DPLLSIM_CREATED_NAME
> + DPLL_DEV_ID=$DPLLSIM_DEV_ID
> + DPLL_MODULE_NAME="dpllsim"
> +fi
[ ... ]
> diff --git a/tools/testing/selftests/drivers/dpll/dpll_lib.sh b/tools/testing/selftests/drivers/dpll/dpll_lib.sh
> new file mode 100644
> index 0000000000000..8d46d4cff06c5
> --- /dev/null
> +++ b/tools/testing/selftests/drivers/dpll/dpll_lib.sh
> @@ -0,0 +1,348 @@
[ ... ]
> +dpll_lib_init()
> +{
> + DPLL_MODULE_NAME=""
> +
> + if [ -n "${DPLL_DEV_ID:-}" ]; then
> + if ! dpll device show id "$DPLL_DEV_ID" &> /dev/null; then
> + echo "SKIP: DPLL device \"$DPLL_DEV_ID\" not found"
> + exit "$ksft_skip"
> + fi
> + elif [ "${DPLL_DRIVER:-}" = "dpllsim" ]; then
> + DPLL_DEV_ID=""
> + elif [ -n "${DPLL_DRIVER:-}" ]; then
> + DPLL_DEV_ID=$(dpll device show -j 2>/dev/null | \
> + jq -r --arg mod "$DPLL_DRIVER" '[.device[]? |
> + select(.["module-name"] == $mod)
> + ][0].id // empty')
> + if [ -z "$DPLL_DEV_ID" ]; then
> + echo "SKIP: no DPLL device of driver \"$DPLL_DRIVER\""
> + exit "$ksft_skip"
> + fi
> + else
> + DPLL_DEV_ID=$(dpll device show -j 2>/dev/null | \
> + jq -r '[.device[]? |
> + select(.["module-name"] != "dpllsim")
> + ][0].id // empty')
> + fi
> +
> + if [ -n "$DPLL_DEV_ID" ]; then
> + DPLL_MODULE_NAME=$(dpll_device_get_module "$DPLL_DEV_ID")
> + fi
> +}
[Severity: Medium]
Can a failed dump be told apart from a dump with no matching device
here? Both the DPLL_DRIVER branch and the default branch use a pipeline,
and nothing sets pipefail, so the command substitution only returns the
status of jq.
If dpll device show -j fails, jq reads empty input, prints nothing and
exits 0, so DPLL_DEV_ID ends up empty.
With DPLL_DRIVER set to a hardware driver, the run then skips with "no
DPLL device of driver". With neither variable set, dpll_basic.sh decides
the host has no hardware and tests a dpllsim device instead:
dpll_basic.sh
dpll_lib_init()
dpll device show -j fails, jq prints nothing
DPLL_DEV_ID is empty
if [ -z "$DPLL_DEV_ID" ]; then
dpllsim_check_module
dpllsim_setup_standard_device
The commit message says:
Every query runs the tool first and jq on what it printed, so that a
failed query is told apart from a missing attribute.
The other helpers follow that with out=$(dpll ...) || return 1, but the
device selection path does not. The DPLL_MODULE_NAME lookup at the end
of dpll_lib_init() also ignores a failure of dpll_device_get_module().
This is unchanged at the end of the series. dpll_features.sh, added
later in the series, calls the same dpll_lib_init().
[ ... ]
> +# dpllsim_available mounts configfs when nothing else did, which is the
> +# one side effect the library has outside the dpllsim directory. Scripts
> +# call dpll_lib_cleanup last from their exit trap to undo it.
> +
> +DPLLSIM_MOUNTED_CONFIGFS=0
> +
> +dpllsim_available()
> +{
> + [ -d "$DPLLSIM_CFG" ] && return 0
> +
> + modprobe dpllsim &> /dev/null
[Severity: Low]
Is the "one side effect" comment above accurate? dpll_lib_run_dir() and
dpll_lib_lock() also create /run/dpll-selftest/ and its lock file, and
nothing removes them.
This modprobe is not recorded either, and dpll_lib_cleanup() never
unloads the module. A run on a system where dpllsim was not loaded
therefore leaves it loaded.
A few other comments also don't match the code. The file header says:
# devices. Sourcing this file has no side effects; call
# dpll_lib_check_prereqs and dpll_lib_init explicitly.
dpll_lib_lock is also required, though, and dpll_stress.sh never calls
dpll_lib_init.
dpllsim_setup_standard_device() has:
# shellcheck disable=SC2120 # the argument is optional
but the function uses no positional parameter.
The comment on dpllsim_alloc_clock_id() about per-worker id ranges
refers to workers that don't exist in this patch. That part is resolved
later in the series by "selftests: dpll: add concurrency stress tests".
At the end of the series, the header, the SC2120 directive and the side
effect comment are still there. The side effect comment now reads "which
are the only side effects the library has outside the dpllsim
directory", and it still leaves out the /run files and the module load.
> + if ! mountpoint -q "$CONFIGFS_DIR"; then
> + mount -t configfs none "$CONFIGFS_DIR" &> /dev/null &&
> + DPLLSIM_MOUNTED_CONFIGFS=1
[Severity: Low]
Can a signal leave configfs mounted while DPLLSIM_MOUNTED_CONFIGFS is
still 0? Both scripts install trap cleanup EXIT and trap 'exit 1' INT
TERM before dpllsim_check_module().
A SIGTERM or SIGINT that arrives while mount runs is handled after mount
returns, before the right-hand side of the && list runs:
mount -t configfs ... succeeds
pending trap: exit 1
cleanup()
dpll_lib_cleanup()
[ "$DPLLSIM_MOUNTED_CONFIGFS" -eq 1 ] || return 0
dpllsim_create_device() records its name before mkdir for this reason.
The mount flag doesn't get the same ordering, so "a timeout imposed by
the harness still cleans up" doesn't hold for the mount. The same
pattern is used for debugfs (DPLL_MOUNTED_DEBUGFS) later in the series.
> + fi
> + [ -d "$DPLLSIM_CFG" ]
> +}
> +
> +# Unmount configfs again when dpllsim_available mounted it. Call after
> +# the devices of the script are gone. A dpllsim device the script could
> +# not remove keeps the mount, so that what is left can be looked at.
> +dpll_lib_cleanup()
> +{
> + local dev_path name
> +
> + [ "$DPLLSIM_MOUNTED_CONFIGFS" -eq 1 ] || return 0
> + DPLLSIM_MOUNTED_CONFIGFS=0
> +
> + for dev_path in "$DPLLSIM_CFG"/*/; do
> + [ -d "$dev_path" ] || continue
> + name=$(basename "$dev_path")
> + log_info "device $name left behind, $CONFIGFS_DIR kept mounted"
> + return
> + done
> + umount "$CONFIGFS_DIR" 2>/dev/null ||
> + log_info "cannot unmount $CONFIGFS_DIR"
[Severity: Low]
Could this unmount configfs while another test is using it? The loop
above only looks at $DPLLSIM_CFG/*/. The flock in dpll_lib_lock() only
orders dpll selftests against each other.
Other configfs users, such as the gpio-sim, netconsole or USB gadget
tests, use the same check-then-mount pattern without a lock. Say one of
them starts while a dpll script holds the mount:
- It sees configfs already mounted and skips its own mount.
- It starts using /sys/kernel/config/<subsys>.
- This umount succeeds as long as that test has no open file or cwd in
the mount, and its paths disappear in the middle of the test.
dpll_lib.sh seems to be the only selftest that unmounts configfs. Later
in the series, "selftests: dpll: add reference leak tests" applies the
same mount and unmount handling to debugfs.
> +}
[ ... ]
> +dpllsim_create_device()
> +{
> + local name=$1
> + local clock_id=$2
> + local dev_path="$DPLLSIM_CFG/$name"
> +
> + # Record the name before the directory exists, so that a cleanup
> + # run by a signal between the two still finds it.
> + DPLLSIM_CREATED_NAME=$name
> + if ! mkdir "$dev_path" 2>/dev/null; then
> + DPLLSIM_CREATED_NAME=""
> + return 1
> + fi
[Severity: Low]
What happens when mkdir fails because a directory of that name already
exists? Leftover devices can exist: dpll_lib_cleanup() deliberately
keeps devices it could not remove, and the probe added later in the
series retries on exactly that case.
A SIGTERM or SIGINT that arrives while mkdir runs is handled after mkdir
returns, before DPLLSIM_CREATED_NAME is cleared:
DPLLSIM_CREATED_NAME=$name
mkdir "$dev_path" fails with EEXIST
pending trap: exit 1
cleanup()
dpllsim_delete_device "$DPLLSIM_CREATED_NAME"
In that case, cleanup() undeploys and removes a device this run did not
create.
> + if ! echo "$clock_id" 2>/dev/null > "$dev_path/clock_id"; then
> + rmdir "$dev_path" 2>/dev/null
> + DPLLSIM_CREATED_NAME=""
> + return 1
> + fi
> +}
[ ... ]
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261109093523.931201-1-poros%40redhat.com
^ permalink raw reply [flat|nested] 31+ messages in thread
* [PATCH net-next v3 03/14] dpll: dpllsim: Add configurable pins
2026-11-09 9:35 [PATCH net-next v3 00/14] dpll: add a simulated DPLL device driver Petr Oros
2026-11-09 9:35 ` [PATCH net-next v3 01/14] dpll: dpllsim: Add " Petr Oros
2026-11-09 9:35 ` [PATCH net-next v3 02/14] selftests: dpll: add test library and device lifecycle tests Petr Oros
@ 2026-11-09 9:35 ` Petr Oros
2026-11-11 9:37 ` netdev-bot+sashiko
2026-11-09 9:35 ` [PATCH net-next v3 04/14] selftests: dpll: add pin enumeration and property tests Petr Oros
` (10 subsequent siblings)
13 siblings, 1 reply; 31+ messages in thread
From: Petr Oros @ 2026-11-09 9:35 UTC (permalink / raw)
To: netdev
Cc: Petr Oros, Ivan Vecera, Jiri Pirko, Jonathan Corbet, Shuah Khan,
Shuah Khan, Randy Dunlap, Jani Nikula, Vadim Fedorenko,
Arkadiusz Kubalewski, Jiri Pirko, Andrew Lunn, David S. Miller,
Eric Dumazet, Jakub Kicinski, Paolo Abeni, linux-doc,
linux-kselftest
Pins are the unit of most DPLL netlink interaction, so let a device
instance carry an arbitrary set of them. Each directory created under
the pins/ group of an instance is one pin with type, frequency and
direction attributes. The type takes the names of the DPLL pin type
enum as iproute2 prints them, the direction is input or output and the
frequency must be one of the frequencies the driver advertises as
supported for the type. The pin directory name becomes the board label
of the pin, which gives tests a stable handle to find a pin by. The
attributes are writable only while the instance is not live and return
EBUSY otherwise.
At live=1 the pins are ordered as created and each pin records its
index in that order, which is the pin index it is registered under.
Their frequencies are validated against the per type tables, a MUX pin
configured as output is rejected since a MUX pin stands for an input
selector, and all pin handles are obtained up front, so that a failure
leaves nothing half visible. The device is registered first and the
pins after it, so that every pin registration event reaches userspace
with a valid device, and a failure unwinds exactly what was registered
in reverse order. While the instance is live the pin items are held
with configfs_depend_item_unlocked(), which makes rmdir of a pin fail
with EBUSY until the instance is taken down.
The pins register with the mandatory callbacks only, direction_get and
state_on_dpll_get, plus frequency_get since the frequency is known from
the configuration. The input and output ops tables are identical here
and diverge when input pins gain their setters in the next patches. The
state is always reported as disconnected for now, the settable pin
features follow in separate patches.
Reviewed-by: Ivan Vecera <ivecera@redhat.com>
Acked-by: Jiri Pirko <jiri@nvidia.com>
Signed-off-by: Petr Oros <poros@redhat.com>
---
Documentation/driver-api/dpllsim.rst | 40 +++-
drivers/dpll/dpllsim/core.c | 225 ++++++++++++++++++++-
drivers/dpll/dpllsim/dpll.c | 280 ++++++++++++++++++++++++++-
drivers/dpll/dpllsim/dpllsim.h | 32 +++
4 files changed, 565 insertions(+), 12 deletions(-)
diff --git a/Documentation/driver-api/dpllsim.rst b/Documentation/driver-api/dpllsim.rst
index 86a5d523534055..e7c5ee93392c21 100644
--- a/Documentation/driver-api/dpllsim.rst
+++ b/Documentation/driver-api/dpllsim.rst
@@ -12,10 +12,12 @@ Configuration interface
=======================
Devices are described in configfs under ``/sys/kernel/config/dpllsim/``.
-A directory created there is a device instance. Attributes are written
-while the instance is not live, then ``live`` is set to 1 to register
-the device with the DPLL subsystem. Writing 0 unregisters it again.
-Structural attributes return ``EBUSY`` while the instance is live.
+A directory created there is a device instance, a directory created in
+its ``pins/`` subdirectory is a pin. Attributes are written while the
+instance is not live, then ``live`` is set to 1 to register the device
+and its pins with the DPLL subsystem. Writing 0 unregisters everything
+again. Structural attributes return ``EBUSY`` while the instance is live
+and pins of a live instance cannot be removed.
Device directory ``/sys/kernel/config/dpllsim/<dev>/``:
@@ -25,17 +27,41 @@ Device directory ``/sys/kernel/config/dpllsim/<dev>/``:
``live`` 1 registers the device, 0 unregisters it
======================= =====================================================
+Pin directory ``/sys/kernel/config/dpllsim/<dev>/pins/<pin>/``:
+
+============= ===============================================================
+``type`` ``gnss``, ``int-oscillator``, ``ext``, ``synce-eth-port`` or
+ ``mux`` (default ``ext``)
+``frequency`` Frequency in Hz, must be supported by the pin type
+ (default 0, which is rejected)
+``direction`` ``input`` or ``output`` (default ``input``), a ``mux`` pin
+ must be an input
+============= ===============================================================
+
+The pin directory name becomes the board label of the pin. Pins are
+registered in the order the directories were created.
+
Example
=======
-Create a device and make it live::
+Create a device with a GNSS input, an external input and an external
+output, then make it live::
cd /sys/kernel/config/dpllsim
mkdir dev0 && cd dev0
echo 100 > clock_id
+ mkdir pins/gnss0
+ echo gnss > pins/gnss0/type
+ echo 1 > pins/gnss0/frequency
+ mkdir pins/ext0
+ echo 10000000 > pins/ext0/frequency
+ mkdir pins/out0
+ echo 10000000 > pins/out0/frequency
+ echo output > pins/out0/direction
echo 1 > live
dpll device show
+ dpll pin show
-To remove the device, write 0 to ``live`` and remove the device
-directory.
+To remove the device, write 0 to ``live``, remove the pin directories
+and finally the device directory.
diff --git a/drivers/dpll/dpllsim/core.c b/drivers/dpll/dpllsim/core.c
index 0772f409d013b7..97b3cf48772e7f 100644
--- a/drivers/dpll/dpllsim/core.c
+++ b/drivers/dpll/dpllsim/core.c
@@ -6,12 +6,14 @@
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+#include <linux/array_size.h>
#include <linux/cleanup.h>
#include <linux/configfs.h>
#include <linux/kstrtox.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/slab.h>
+#include <linux/string.h>
#include <linux/sysfs.h>
#include "dpllsim.h"
@@ -23,6 +25,182 @@ static struct dpllsim_dev *to_dpllsim_dev(struct config_item *item)
return container_of(to_config_group(item), struct dpllsim_dev, group);
}
+static struct dpllsim_pin *to_dpllsim_pin(struct config_item *item)
+{
+ return container_of(item, struct dpllsim_pin, item);
+}
+
+static const struct {
+ const char *name;
+ enum dpll_pin_type type;
+} dpllsim_pin_types[] = {
+ { "gnss", DPLL_PIN_TYPE_GNSS },
+ { "int-oscillator", DPLL_PIN_TYPE_INT_OSCILLATOR },
+ { "ext", DPLL_PIN_TYPE_EXT },
+ { "synce-eth-port", DPLL_PIN_TYPE_SYNCE_ETH_PORT },
+ { "mux", DPLL_PIN_TYPE_MUX },
+};
+
+static ssize_t dpllsim_pin_type_show(struct config_item *item, char *page)
+{
+ struct dpllsim_pin *pin = to_dpllsim_pin(item);
+ int i;
+
+ guard(mutex)(&pin->dev->lock);
+ for (i = 0; i < ARRAY_SIZE(dpllsim_pin_types); i++)
+ if (dpllsim_pin_types[i].type == pin->type)
+ return sysfs_emit(page, "%s\n",
+ dpllsim_pin_types[i].name);
+ return -EINVAL;
+}
+
+static ssize_t dpllsim_pin_type_store(struct config_item *item,
+ const char *page, size_t count)
+{
+ struct dpllsim_pin *pin = to_dpllsim_pin(item);
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(dpllsim_pin_types); i++)
+ if (sysfs_streq(page, dpllsim_pin_types[i].name))
+ break;
+ if (i == ARRAY_SIZE(dpllsim_pin_types))
+ return -EINVAL;
+
+ guard(mutex)(&pin->dev->lock);
+ if (dpllsim_dev_frozen(pin->dev))
+ return -EBUSY;
+ pin->type = dpllsim_pin_types[i].type;
+ return count;
+}
+CONFIGFS_ATTR(dpllsim_pin_, type);
+
+static ssize_t dpllsim_pin_direction_show(struct config_item *item, char *page)
+{
+ struct dpllsim_pin *pin = to_dpllsim_pin(item);
+
+ guard(mutex)(&pin->dev->lock);
+ return sysfs_emit(page, "%s\n",
+ pin->direction == DPLL_PIN_DIRECTION_INPUT ?
+ "input" : "output");
+}
+
+static ssize_t dpllsim_pin_direction_store(struct config_item *item,
+ const char *page, size_t count)
+{
+ struct dpllsim_pin *pin = to_dpllsim_pin(item);
+ enum dpll_pin_direction direction;
+
+ if (sysfs_streq(page, "input"))
+ direction = DPLL_PIN_DIRECTION_INPUT;
+ else if (sysfs_streq(page, "output"))
+ direction = DPLL_PIN_DIRECTION_OUTPUT;
+ else
+ return -EINVAL;
+
+ guard(mutex)(&pin->dev->lock);
+ if (dpllsim_dev_frozen(pin->dev))
+ return -EBUSY;
+ pin->direction = direction;
+ return count;
+}
+CONFIGFS_ATTR(dpllsim_pin_, direction);
+
+static ssize_t dpllsim_pin_frequency_show(struct config_item *item, char *page)
+{
+ struct dpllsim_pin *pin = to_dpllsim_pin(item);
+
+ guard(mutex)(&pin->dev->lock);
+ return sysfs_emit(page, "%llu\n", pin->frequency);
+}
+
+static ssize_t dpllsim_pin_frequency_store(struct config_item *item,
+ const char *page, size_t count)
+{
+ struct dpllsim_pin *pin = to_dpllsim_pin(item);
+ u64 val;
+ int err;
+
+ err = kstrtou64(page, 10, &val);
+ if (err)
+ return err;
+
+ guard(mutex)(&pin->dev->lock);
+ if (dpllsim_dev_frozen(pin->dev))
+ return -EBUSY;
+ pin->frequency = val;
+ return count;
+}
+CONFIGFS_ATTR(dpllsim_pin_, frequency);
+
+static struct configfs_attribute *dpllsim_pin_attrs[] = {
+ &dpllsim_pin_attr_type,
+ &dpllsim_pin_attr_direction,
+ &dpllsim_pin_attr_frequency,
+ NULL,
+};
+
+static void dpllsim_pin_release(struct config_item *item)
+{
+ struct dpllsim_pin *pin = to_dpllsim_pin(item);
+
+ kfree(pin);
+}
+
+static const struct configfs_item_operations dpllsim_pin_item_ops = {
+ .release = dpllsim_pin_release,
+};
+
+static const struct config_item_type dpllsim_pin_type = {
+ .ct_item_ops = &dpllsim_pin_item_ops,
+ .ct_attrs = dpllsim_pin_attrs,
+ .ct_owner = THIS_MODULE,
+};
+
+static struct config_item *dpllsim_pins_make_item(struct config_group *group,
+ const char *name)
+{
+ struct dpllsim_dev *sim = container_of(group, struct dpllsim_dev,
+ pins_group);
+ struct dpllsim_pin *pin;
+
+ lockdep_assert_held(&dpllsim_subsys.su_mutex);
+
+ if (dpllsim_dev_frozen(sim))
+ return ERR_PTR(-EBUSY);
+ if (list_count_nodes(&sim->pin_list) >= DPLLSIM_MAX_PINS)
+ return ERR_PTR(-ENOSPC);
+
+ pin = kzalloc_obj(*pin);
+ if (!pin)
+ return ERR_PTR(-ENOMEM);
+
+ config_item_init_type_name(&pin->item, name, &dpllsim_pin_type);
+ pin->dev = sim;
+ pin->type = DPLL_PIN_TYPE_EXT;
+ pin->direction = DPLL_PIN_DIRECTION_INPUT;
+ list_add_tail(&pin->list, &sim->pin_list);
+
+ return &pin->item;
+}
+
+/* Pin mkdir and rmdir run under the subsystem mutex like live transitions */
+static void dpllsim_pins_drop_item(struct config_group *group,
+ struct config_item *item)
+{
+ list_del(&to_dpllsim_pin(item)->list);
+ config_item_put(item);
+}
+
+static const struct configfs_group_operations dpllsim_pins_group_ops = {
+ .make_item = dpllsim_pins_make_item,
+ .drop_item = dpllsim_pins_drop_item,
+};
+
+static const struct config_item_type dpllsim_pins_type = {
+ .ct_group_ops = &dpllsim_pins_group_ops,
+ .ct_owner = THIS_MODULE,
+};
+
/* The clock id is checked at live=1, so both share the subsystem mutex */
static ssize_t dpllsim_dev_clock_id_show(struct config_item *item, char *page)
{
@@ -82,9 +260,39 @@ static bool dpllsim_clock_id_in_use(struct dpllsim_dev *sim)
return false;
}
+/* Pins of a live device cannot be removed */
+static void dpllsim_dev_undepend_pins(struct dpllsim_dev *sim,
+ struct dpllsim_pin *last)
+{
+ struct dpllsim_pin *pin;
+
+ list_for_each_entry(pin, &sim->pin_list, list) {
+ if (pin == last)
+ break;
+ configfs_undepend_item_unlocked(&pin->item);
+ }
+}
+
+static int dpllsim_dev_depend_pins(struct dpllsim_dev *sim)
+{
+ struct dpllsim_pin *pin;
+ int err;
+
+ list_for_each_entry(pin, &sim->pin_list, list) {
+ err = configfs_depend_item_unlocked(&dpllsim_subsys,
+ &pin->item);
+ if (err) {
+ dpllsim_dev_undepend_pins(sim, pin);
+ return err;
+ }
+ }
+
+ return 0;
+}
+
/*
- * The subsystem mutex excludes the live transitions of the other
- * instances for the whole deploy sequence.
+ * The subsystem mutex excludes pin mkdir and rmdir and the live
+ * transitions of the other instances for the whole deploy sequence.
*/
static ssize_t dpllsim_dev_live_store(struct config_item *item,
const char *page, size_t count)
@@ -104,6 +312,7 @@ static ssize_t dpllsim_dev_live_store(struct config_item *item,
if (!live) {
dpllsim_dev_undeploy(sim);
+ dpllsim_dev_undepend_pins(sim, NULL);
return count;
}
@@ -112,10 +321,16 @@ static ssize_t dpllsim_dev_live_store(struct config_item *item,
if (dpllsim_clock_id_in_use(sim))
return -EEXIST;
- err = dpllsim_dev_deploy(sim);
+ err = dpllsim_dev_depend_pins(sim);
if (err)
return err;
+ err = dpllsim_dev_deploy(sim);
+ if (err) {
+ dpllsim_dev_undepend_pins(sim, NULL);
+ return err;
+ }
+
return count;
}
CONFIGFS_ATTR(dpllsim_dev_, live);
@@ -154,11 +369,15 @@ static struct config_group *dpllsim_make_group(struct config_group *group,
return ERR_PTR(-ENOMEM);
mutex_init(&sim->lock);
+ INIT_LIST_HEAD(&sim->pin_list);
sim->mode = DPLL_MODE_AUTOMATIC;
sim->lock_status = DPLL_LOCK_STATUS_UNLOCKED;
sim->lock_status_error = DPLL_LOCK_STATUS_ERROR_NONE;
config_group_init_type_name(&sim->group, name, &dpllsim_dev_type);
+ config_group_init_type_name(&sim->pins_group, "pins",
+ &dpllsim_pins_type);
+ configfs_add_default_group(&sim->pins_group, &sim->group);
return &sim->group;
}
diff --git a/drivers/dpll/dpllsim/dpll.c b/drivers/dpll/dpllsim/dpll.c
index ae2c8fcf238c68..a1d5921fb8d017 100644
--- a/drivers/dpll/dpllsim/dpll.c
+++ b/drivers/dpll/dpllsim/dpll.c
@@ -4,12 +4,39 @@
* Copyright (c) 2026 Red Hat, Inc.
*/
+#include <linux/array_size.h>
#include <linux/cleanup.h>
#include <linux/dpll.h>
#include <linux/module.h>
+#include <linux/slab.h>
#include "dpllsim.h"
+static struct dpll_pin_frequency dpllsim_freq_1pps[] = {
+ DPLL_PIN_FREQUENCY_RANGE(1, 1),
+};
+
+static struct dpll_pin_frequency dpllsim_freq_synce[] = {
+ DPLL_PIN_FREQUENCY_RANGE(1544000, 1544000),
+ DPLL_PIN_FREQUENCY_RANGE(2048000, 2048000),
+ DPLL_PIN_FREQUENCY_RANGE(10000000, 10000000),
+ DPLL_PIN_FREQUENCY_RANGE(25000000, 25000000),
+};
+
+static struct dpll_pin_frequency dpllsim_freq_ext[] = {
+ DPLL_PIN_FREQUENCY_RANGE(1, 1),
+ DPLL_PIN_FREQUENCY_RANGE(10000000, 10000000),
+ DPLL_PIN_FREQUENCY_RANGE(19440000, 19440000),
+ DPLL_PIN_FREQUENCY_RANGE(25000000, 25000000),
+ DPLL_PIN_FREQUENCY_RANGE(77760000, 77760000),
+ DPLL_PIN_FREQUENCY_RANGE(125000000, 125000000),
+ DPLL_PIN_FREQUENCY_RANGE(156250000, 156250000),
+};
+
+static struct dpll_pin_frequency dpllsim_freq_int_osc[] = {
+ DPLL_PIN_FREQUENCY_RANGE(25000000, 25000000),
+};
+
static int dpllsim_mode_get(const struct dpll_device *dpll, void *priv,
enum dpll_mode *mode,
struct netlink_ext_ack *extack)
@@ -39,6 +66,229 @@ static const struct dpll_device_ops dpllsim_dpll_ops = {
.lock_status_get = dpllsim_lock_status_get,
};
+static int dpllsim_pin_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 dpllsim_pin *sim_pin = pin_priv;
+
+ guard(mutex)(&sim_pin->dev->lock);
+ *frequency = sim_pin->frequency;
+ return 0;
+}
+
+static int dpllsim_pin_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 dpllsim_pin *sim_pin = pin_priv;
+
+ *direction = sim_pin->direction;
+ return 0;
+}
+
+static int dpllsim_pin_state_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 dpllsim_pin *sim_pin = pin_priv;
+
+ guard(mutex)(&sim_pin->dev->lock);
+ *state = sim_pin->state;
+ return 0;
+}
+
+static const struct dpll_pin_ops dpllsim_input_pin_ops = {
+ .frequency_get = dpllsim_pin_frequency_get,
+ .direction_get = dpllsim_pin_direction_get,
+ .state_on_dpll_get = dpllsim_pin_state_get,
+};
+
+static const struct dpll_pin_ops dpllsim_output_pin_ops = {
+ .frequency_get = dpllsim_pin_frequency_get,
+ .direction_get = dpllsim_pin_direction_get,
+ .state_on_dpll_get = dpllsim_pin_state_get,
+};
+
+static const struct dpll_pin_ops *
+dpllsim_pin_select_ops(const struct dpllsim_pin *pin)
+{
+ if (pin->direction == DPLL_PIN_DIRECTION_INPUT)
+ return &dpllsim_input_pin_ops;
+ return &dpllsim_output_pin_ops;
+}
+
+static void dpllsim_pin_props(struct dpllsim_pin *pin,
+ struct dpll_pin_properties *prop)
+{
+ prop->type = pin->type;
+ prop->board_label = config_item_name(&pin->item);
+
+ switch (pin->type) {
+ case DPLL_PIN_TYPE_GNSS:
+ prop->freq_supported = dpllsim_freq_1pps;
+ prop->freq_supported_num = ARRAY_SIZE(dpllsim_freq_1pps);
+ break;
+ case DPLL_PIN_TYPE_INT_OSCILLATOR:
+ prop->freq_supported = dpllsim_freq_int_osc;
+ prop->freq_supported_num = ARRAY_SIZE(dpllsim_freq_int_osc);
+ break;
+ case DPLL_PIN_TYPE_MUX:
+ case DPLL_PIN_TYPE_SYNCE_ETH_PORT:
+ prop->freq_supported = dpllsim_freq_synce;
+ prop->freq_supported_num = ARRAY_SIZE(dpllsim_freq_synce);
+ break;
+ default:
+ prop->freq_supported = dpllsim_freq_ext;
+ prop->freq_supported_num = ARRAY_SIZE(dpllsim_freq_ext);
+ break;
+ }
+}
+
+static bool dpllsim_pin_frequency_valid(struct dpllsim_pin *pin)
+{
+ struct dpll_pin_properties prop = {};
+ int i;
+
+ dpllsim_pin_props(pin, &prop);
+ for (i = 0; i < prop.freq_supported_num; i++)
+ if (pin->frequency >= prop.freq_supported[i].min &&
+ pin->frequency <= prop.freq_supported[i].max)
+ return true;
+
+ return false;
+}
+
+/* A MUX pin stands for an input selector, so it is an input itself */
+static bool dpllsim_pin_direction_valid(const struct dpllsim_pin *pin)
+{
+ return pin->type != DPLL_PIN_TYPE_MUX ||
+ pin->direction == DPLL_PIN_DIRECTION_INPUT;
+}
+
+static void dpllsim_pin_reset(struct dpllsim_pin *pin)
+{
+ pin->ops = dpllsim_pin_select_ops(pin);
+ pin->state = DPLL_PIN_STATE_DISCONNECTED;
+}
+
+/* Build sim->pins in registration order, the order the pins were created */
+static int dpllsim_order_pins(struct dpllsim_dev *sim)
+{
+ struct dpllsim_pin *pin;
+ int count = 0, i;
+
+ list_for_each_entry(pin, &sim->pin_list, list) {
+ if (!dpllsim_pin_frequency_valid(pin) ||
+ !dpllsim_pin_direction_valid(pin))
+ return -EINVAL;
+ count++;
+ }
+
+ sim->pins = kzalloc_objs(*sim->pins, count);
+ if (!sim->pins)
+ return -ENOMEM;
+
+ sim->num_pins = 0;
+ list_for_each_entry(pin, &sim->pin_list, list) {
+ pin->idx = sim->num_pins;
+ sim->pins[sim->num_pins++] = pin;
+ }
+
+ for (i = 0; i < sim->num_pins; i++)
+ dpllsim_pin_reset(sim->pins[i]);
+
+ return 0;
+}
+
+static void dpllsim_put_pins(struct dpllsim_dev *sim)
+{
+ int i;
+
+ for (i = sim->num_pins - 1; i >= 0; i--) {
+ struct dpllsim_pin *pin = sim->pins[i];
+
+ if (pin->dpll_pin) {
+ dpll_pin_put(pin->dpll_pin, &pin->tracker);
+ pin->dpll_pin = NULL;
+ }
+ }
+}
+
+static int dpllsim_get_pins(struct dpllsim_dev *sim)
+{
+ int i, err;
+
+ for (i = 0; i < sim->num_pins; i++) {
+ struct dpllsim_pin *pin = sim->pins[i];
+ struct dpll_pin_properties prop = {};
+ struct dpll_pin *dpll_pin;
+
+ dpllsim_pin_props(pin, &prop);
+ dpll_pin = dpll_pin_get(sim->clock_id, pin->idx, THIS_MODULE,
+ &prop, &pin->tracker);
+ if (IS_ERR(dpll_pin)) {
+ err = PTR_ERR(dpll_pin);
+ goto err_put_pins;
+ }
+ pin->dpll_pin = dpll_pin;
+ }
+
+ return 0;
+
+err_put_pins:
+ dpllsim_put_pins(sim);
+ return err;
+}
+
+static void dpllsim_unregister_pins(struct dpllsim_dev *sim)
+{
+ int i;
+
+ for (i = sim->num_pins - 1; i >= 0; i--) {
+ struct dpllsim_pin *pin = sim->pins[i];
+
+ if (pin->registered)
+ dpll_pin_unregister(sim->dpll, pin->dpll_pin, pin->ops,
+ pin);
+ pin->registered = false;
+ }
+}
+
+static int dpllsim_register_pins(struct dpllsim_dev *sim)
+{
+ struct dpllsim_pin *pin;
+ int i, err;
+
+ for (i = 0; i < sim->num_pins; i++) {
+ pin = sim->pins[i];
+
+ err = dpll_pin_register(sim->dpll, pin->dpll_pin, pin->ops,
+ pin);
+ if (err)
+ goto err_unregister;
+ pin->registered = true;
+ }
+
+ return 0;
+
+err_unregister:
+ dpllsim_unregister_pins(sim);
+ return err;
+}
+
+/*
+ * Registration order matters: the device must be registered before pins
+ * so that pin registration events reach userspace with a valid device,
+ * while pin handles must be obtained up front so that a failure leaves
+ * nothing half-visible.
+ */
int dpllsim_dev_deploy(struct dpllsim_dev *sim)
{
struct dpll_device *dpll;
@@ -48,15 +298,27 @@ int dpllsim_dev_deploy(struct dpllsim_dev *sim)
if (IS_ERR(dpll))
return PTR_ERR(dpll);
- /* From here on the configuration is frozen */
+ /* From here on the pin configuration is frozen */
mutex_lock(&sim->lock);
sim->dpll = dpll;
mutex_unlock(&sim->lock);
+ err = dpllsim_order_pins(sim);
+ if (err)
+ goto err_put_dpll;
+
+ err = dpllsim_get_pins(sim);
+ if (err)
+ goto err_free_pins;
+
err = dpll_device_register(sim->dpll, DPLL_TYPE_PPS,
&dpllsim_dpll_ops, sim);
if (err)
- goto err_put_dpll;
+ goto err_put_pins;
+
+ err = dpllsim_register_pins(sim);
+ if (err)
+ goto err_unregister_device;
mutex_lock(&sim->lock);
sim->registered = true;
@@ -64,6 +326,14 @@ int dpllsim_dev_deploy(struct dpllsim_dev *sim)
return 0;
+err_unregister_device:
+ dpll_device_unregister(sim->dpll, &dpllsim_dpll_ops, sim);
+err_put_pins:
+ dpllsim_put_pins(sim);
+err_free_pins:
+ kfree(sim->pins);
+ sim->pins = NULL;
+ sim->num_pins = 0;
err_put_dpll:
dpll_device_put(sim->dpll, &sim->tracker);
mutex_lock(&sim->lock);
@@ -77,8 +347,14 @@ void dpllsim_dev_undeploy(struct dpllsim_dev *sim)
scoped_guard(mutex, &sim->lock)
sim->registered = false;
+ dpllsim_unregister_pins(sim);
dpll_device_unregister(sim->dpll, &dpllsim_dpll_ops, sim);
+ dpllsim_put_pins(sim);
dpll_device_put(sim->dpll, &sim->tracker);
scoped_guard(mutex, &sim->lock)
sim->dpll = NULL;
+
+ kfree(sim->pins);
+ sim->pins = NULL;
+ sim->num_pins = 0;
}
diff --git a/drivers/dpll/dpllsim/dpllsim.h b/drivers/dpll/dpllsim/dpllsim.h
index 5667f8827c98c1..73b846b66f79c7 100644
--- a/drivers/dpll/dpllsim/dpllsim.h
+++ b/drivers/dpll/dpllsim/dpllsim.h
@@ -9,10 +9,39 @@
#include <linux/configfs.h>
#include <linux/dpll.h>
+#include <linux/list.h>
#include <linux/mutex.h>
+#define DPLLSIM_MAX_PINS 128
+
+struct dpllsim_dev;
+
+struct dpllsim_pin {
+ struct config_item item;
+ struct list_head list;
+ struct dpllsim_dev *dev;
+
+ /* Configuration, writable only until dpllsim_dev_frozen() */
+ enum dpll_pin_type type;
+ enum dpll_pin_direction direction;
+ u64 frequency;
+
+ /* Resolved at live=1 */
+ int idx;
+ struct dpll_pin *dpll_pin;
+ const struct dpll_pin_ops *ops;
+ dpll_tracker tracker;
+ bool registered;
+
+ /* Simulated state, guarded by dev->lock */
+ enum dpll_pin_state state;
+};
+
struct dpllsim_dev {
struct config_group group;
+ struct config_group pins_group;
+ /* Pin items, guarded by the configfs subsystem mutex */
+ struct list_head pin_list;
/*
* Set from the start of a live transition until the device is
@@ -27,6 +56,9 @@ struct dpllsim_dev {
* the live attribute reports, see dpllsim_dev_live().
*/
bool registered;
+ /* Pins in registration order, valid while @dpll is set */
+ struct dpllsim_pin **pins;
+ int num_pins;
/* Guarded by the subsystem mutex */
u64 clock_id;
--
2.56.0
^ permalink raw reply related [flat|nested] 31+ messages in thread* Re: [PATCH net-next v3 03/14] dpll: dpllsim: Add configurable pins
2026-11-09 9:35 ` [PATCH net-next v3 03/14] dpll: dpllsim: Add configurable pins Petr Oros
@ 2026-11-11 9:37 ` netdev-bot+sashiko
0 siblings, 0 replies; 31+ messages in thread
From: netdev-bot+sashiko @ 2026-11-11 9:37 UTC (permalink / raw)
To: poros
Cc: netdev, ivecera, jiri, corbet, skhan, shuah, rdunlap, jani.nikula,
vadim.fedorenko, arkadiusz.kubalewski, jiri, andrew+netdev, davem,
edumazet, kuba, pabeni, linux-doc, linux-kselftest
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] A pin mkdir or rmdir that runs alongside `live=1` on the same
instance makes the live transition fail with -ENOENT.
- [Low] Two pin-creation limits are visible to users but not documented.
- [Low] dpllsim.rst describes the pin frequency attribute only as 'must
be supported by the pin type (default 0, which is rejected)' and never…
- [Low] Concern: dpllsim_pin_types[] maps only 5 of the 6 values of enum
dpll_pin_type and leaves out DPLL_PIN_TYPE_INT_NCO, so a simulated pin…
--
dpll: dpllsim: Add configurable pins
This patch lets a dpllsim device instance carry configurable pins. Each
pin is a configfs directory under pins/ with type, frequency and direction
attributes. At live=1 the pins are validated, ordered, obtained and
registered with the DPLL subsystem after the device, and while the
instance is live the pin items are held with
configfs_depend_item_unlocked().
> diff --git a/drivers/dpll/dpllsim/core.c b/drivers/dpll/dpllsim/core.c
> index 0772f409d013b..97b3cf48772e7 100644
> --- a/drivers/dpll/dpllsim/core.c
> +++ b/drivers/dpll/dpllsim/core.c
[ ... ]
> +static struct config_item *dpllsim_pins_make_item(struct config_group *group,
> + const char *name)
> +{
> + struct dpllsim_dev *sim = container_of(group, struct dpllsim_dev,
> + pins_group);
> + struct dpllsim_pin *pin;
> +
> + lockdep_assert_held(&dpllsim_subsys.su_mutex);
> +
> + if (dpllsim_dev_frozen(sim))
> + return ERR_PTR(-EBUSY);
> + if (list_count_nodes(&sim->pin_list) >= DPLLSIM_MAX_PINS)
> + return ERR_PTR(-ENOSPC);
[Severity: Low]
Should these two mkdir limits be described in
Documentation/driver-api/dpllsim.rst?
Once the instance is frozen, mkdir of a new pin fails with EBUSY. Once
the instance has DPLLSIM_MAX_PINS (128) pins, it fails with ENOSPC. The
documentation only says:
Structural attributes return EBUSY while the instance is live
and pins of a live instance cannot be removed.
It doesn't say that pins can't be added to a live instance, and it
doesn't mention the 128 pin cap. The commit message also says an instance
can carry "an arbitrary set of them".
Separately, a pin with an invalid frequency or direction makes live=1
fail with a bare -EINVAL from dpllsim_order_pins():
list_for_each_entry(pin, &sim->pin_list, list) {
if (!dpllsim_pin_frequency_valid(pin) ||
!dpllsim_pin_direction_valid(pin))
return -EINVAL;
Nothing tells the user which pin was rejected. Could that be reported?
> +
> + pin = kzalloc_obj(*pin);
[ ... ]
> +/* Pin mkdir and rmdir run under the subsystem mutex like live transitions */
> +static void dpllsim_pins_drop_item(struct config_group *group,
> + struct config_item *item)
> +{
> + list_del(&to_dpllsim_pin(item)->list);
> + config_item_put(item);
> +}
[ ... ]
> +static int dpllsim_dev_depend_pins(struct dpllsim_dev *sim)
> +{
> + struct dpllsim_pin *pin;
> + int err;
> +
> + list_for_each_entry(pin, &sim->pin_list, list) {
> + err = configfs_depend_item_unlocked(&dpllsim_subsys,
> + &pin->item);
[Severity: Low]
Can a pin mkdir or rmdir that runs at the same time as "echo 1 > live" on
the same instance make this return -ENOENT?
The comments above dpllsim_pins_drop_item() and dpllsim_dev_live_store()
say the subsystem mutex excludes pin mkdir and rmdir. However, configfs
only holds su_mutex around the make_item() and drop_item() callbacks.
For mkdir, dpllsim_pins_make_item() adds the pin to sim->pin_list under
su_mutex. The dirent is only attached and readied after the mutex has
been dropped:
fs/configfs/dir.c:configfs_mkdir() {
...
mutex_unlock(&subsys->su_mutex);
...
ret = configfs_attach_item(item, dentry, frag);
...
configfs_dir_set_ready(dentry->d_fsdata);
...
}
Suppose dpllsim_dev_live_store() takes su_mutex inside that window.
dpllsim_dev_depend_pins() then finds the half-created pin on pin_list.
configfs_depend_prep() doesn't find it, either because there is no
dirent yet or because it skips CONFIGFS_USET_CREATING entries. So
live=1 fails with -ENOENT.
rmdir has the same window. configfs_rmdir() marks the item as dropping
in configfs_detach_prep() and detaches the dirent without su_mutex. Only
after that does it take su_mutex for
client_drop_item()->dpllsim_pins_drop_item(), which does the list_del():
fs/configfs/dir.c:configfs_rmdir() {
...
ret = configfs_detach_prep(sd, &wait);
...
configfs_detach_item(dentry);
mutex_lock(&subsys->su_mutex);
...
client_drop_item(parent_item, item);
mutex_unlock(&subsys->su_mutex);
...
}
A live=1 in between still sees the pin on pin_list, and the depend fails
with -ENOENT.
Nothing is corrupted and a retry succeeds. Should the comments be
corrected, or should a pin that is still being created or removed be
handled here?
> + if (err) {
> + dpllsim_dev_undepend_pins(sim, pin);
> + return err;
> + }
> + }
> +
> + return 0;
> +}
[ ... ]
> diff --git a/drivers/dpll/dpllsim/dpll.c b/drivers/dpll/dpllsim/dpll.c
> index ae2c8fcf238c6..a1d5921fb8d01 100644
> --- a/drivers/dpll/dpllsim/dpll.c
> +++ b/drivers/dpll/dpllsim/dpll.c
[ ... ]
> +static void dpllsim_pin_props(struct dpllsim_pin *pin,
> + struct dpll_pin_properties *prop)
> +{
> + prop->type = pin->type;
> + prop->board_label = config_item_name(&pin->item);
> +
> + switch (pin->type) {
> + case DPLL_PIN_TYPE_GNSS:
> + prop->freq_supported = dpllsim_freq_1pps;
> + prop->freq_supported_num = ARRAY_SIZE(dpllsim_freq_1pps);
> + break;
> + case DPLL_PIN_TYPE_INT_OSCILLATOR:
> + prop->freq_supported = dpllsim_freq_int_osc;
> + prop->freq_supported_num = ARRAY_SIZE(dpllsim_freq_int_osc);
> + break;
> + case DPLL_PIN_TYPE_MUX:
> + case DPLL_PIN_TYPE_SYNCE_ETH_PORT:
> + prop->freq_supported = dpllsim_freq_synce;
> + prop->freq_supported_num = ARRAY_SIZE(dpllsim_freq_synce);
> + break;
> + default:
> + prop->freq_supported = dpllsim_freq_ext;
> + prop->freq_supported_num = ARRAY_SIZE(dpllsim_freq_ext);
> + break;
> + }
> +}
[Severity: Low]
Should dpllsim.rst list the frequencies each pin type accepts? The
frequency row only says:
Frequency in Hz, must be supported by the pin type
(default 0, which is rejected)
dpllsim_pin_frequency_valid() accepts only the fixed tables chosen here:
gnss: 1 Hz
int-oscillator: 25 MHz
synce-eth-port and mux: 1.544, 2.048, 10 and 25 MHz
ext: 1 Hz and 10, 19.44, 25, 77.76, 125, 156.25 MHz
The documentation also doesn't say that mux uses the SyncE table.
freq-supported can only be seen over netlink after live=1 has succeeded.
An unsupported frequency makes live=1 fail with a bare -EINVAL that
names neither the pin nor the value.
[ ... ]
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261109093523.931201-1-poros%40redhat.com
^ permalink raw reply [flat|nested] 31+ messages in thread
* [PATCH net-next v3 04/14] selftests: dpll: add pin enumeration and property tests
2026-11-09 9:35 [PATCH net-next v3 00/14] dpll: add a simulated DPLL device driver Petr Oros
` (2 preceding siblings ...)
2026-11-09 9:35 ` [PATCH net-next v3 03/14] dpll: dpllsim: Add configurable pins Petr Oros
@ 2026-11-09 9:35 ` Petr Oros
2026-11-11 9:37 ` netdev-bot+sashiko
2026-11-09 9:35 ` [PATCH net-next v3 05/14] dpll: dpllsim: Add support to set input pin state Petr Oros
` (9 subsequent siblings)
13 siblings, 1 reply; 31+ messages in thread
From: Petr Oros @ 2026-11-09 9:35 UTC (permalink / raw)
To: netdev
Cc: Petr Oros, Ivan Vecera, Jiri Pirko, Jonathan Corbet, Shuah Khan,
Shuah Khan, Randy Dunlap, Jani Nikula, Vadim Fedorenko,
Arkadiusz Kubalewski, Jiri Pirko, Andrew Lunn, David S. Miller,
Eric Dumazet, Jakub Kicinski, Paolo Abeni, linux-doc,
linux-kselftest
Extend the library with pin queries. Pins of a device are collected
from the full pin dump with jq, since older iproute2 lacks the device
filter of "pin show", and the fields that live in the parent-device
nest are read through a helper that selects the nest of the device
under test. The helpers that work on the full dump normalize its shape
the same way the id-based ones do, so that a tool printing a flat
object instead of the dump wrapper works with all of them. Like the
device queries, every pin query runs the tool first and jq on what it
printed, so that a query which fails returns the status of the tool
instead of the empty output a missing attribute produces too. The
prerequisite check now also requires a tool that knows "pin show", so
that a tool too old for the pin queries is a skip up front rather than
a failed query in every test. The dpllsim helpers gain
dpllsim_add_pin, which creates a pin directory and writes its type,
frequency and direction, and the standard device grows a GNSS input,
an external input and an external output.
dpll_basic.sh checks that the enumeration yields a numeric id for every
pin of the device, the ids are taken from the dump as they are and jq
prints null for a pin without one, that every pin has an id and a type
and carries the direction in the parent-device nest, and that an input
pin reports a valid state. The property test does not go on to query
pins by an id that is not one, and reports a pin it cannot query at all
as one failure rather than as one per attribute. The frequency is not
required, a pin without a frequency callback does not report one. The
device the script created has the standard pins, so an empty pin list
or a missing input pin on it means a failed query or broken tooling
rather than a pinless device. The library gains
dpll_log_test_skip_or_fail for such conditions, which fails on the
device the script created, recorded in DPLL_CREATED_DEVICE next to
DPLL_DEV_ID, and skips on a device the user provided. The stress script
creates its devices with a random set of pins of every type and both
directions, MUX pins always as inputs, and the deploy cycle test runs
with the standard pin set.
Reviewed-by: Ivan Vecera <ivecera@redhat.com>
Acked-by: Jiri Pirko <jiri@nvidia.com>
Signed-off-by: Petr Oros <poros@redhat.com>
---
v3:
- follow the move of the selftests to
tools/testing/selftests/drivers/dpll
v2:
- quote the ksft_skip and ret expansions (netdev CI shellcheck)
v2: https://lore.kernel.org/all/20261105151852.1844305-5-poros@redhat.com/
v1: https://lore.kernel.org/all/20261103093910.596225-5-poros@redhat.com/
---
.../selftests/drivers/dpll/dpll_basic.sh | 149 +++++++++++++-
.../selftests/drivers/dpll/dpll_lib.sh | 189 ++++++++++++++++--
.../selftests/drivers/dpll/dpll_stress.sh | 46 ++++-
3 files changed, 359 insertions(+), 25 deletions(-)
diff --git a/tools/testing/selftests/drivers/dpll/dpll_basic.sh b/tools/testing/selftests/drivers/dpll/dpll_basic.sh
index 827c389fe1aa31..eb5de2e22081e4 100755
--- a/tools/testing/selftests/drivers/dpll/dpll_basic.sh
+++ b/tools/testing/selftests/drivers/dpll/dpll_basic.sh
@@ -1,11 +1,12 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
#
-# Basic DPLL subsystem test. Read-only validation of the device properties
-# exposed over netlink. Runs against a hardware DPLL device when one is
-# present (or when DPLL_DEV_ID is set), otherwise against a freshly
-# created dpllsim device. DPLL_DRIVER names the driver to take the
-# device from, dpllsim for a created one although hardware is present.
+# Basic DPLL subsystem test. Read-only validation of device and pin
+# enumeration and of the properties exposed over netlink. Runs against a
+# hardware DPLL device when one is present (or when DPLL_DEV_ID is set),
+# otherwise against a freshly created dpllsim device. DPLL_DRIVER names
+# the driver to take the device from, dpllsim for a created one although
+# hardware is present.
#
# Copyright (c) 2026 Red Hat, Inc.
@@ -16,8 +17,29 @@ source "$SCRIPTDIR/dpll_lib.sh"
ALL_TESTS="
test_device_properties
+ test_pin_enumeration
+ test_pin_properties
+ test_pin_state
"
+# Fail on every entry of a pin id list that is not a number. The ids are
+# taken from the dump as they are, so a pin without one shows up as the
+# "null" jq prints for a missing field, not as an empty line. Return 1
+# when any entry is invalid.
+check_pin_ids()
+{
+ local id ret=0
+
+ for id in "$@"; do
+ if ! [[ "$id" =~ ^[0-9]+$ ]]; then
+ check_err 1 "invalid pin id '$id'"
+ ret=1
+ fi
+ done
+
+ return "$ret"
+}
+
test_device_properties()
{
local json
@@ -43,6 +65,114 @@ test_device_properties()
log_test "DPLL device properties"
}
+test_pin_enumeration()
+{
+ local pin_ids
+
+ RET=0
+
+ mapfile -t pin_ids < <(dpll_pin_get_ids "$DPLL_DEV_ID")
+ if [ "${#pin_ids[@]}" -eq 0 ]; then
+ dpll_log_test_skip_or_fail "DPLL pin enumeration" \
+ "no pins found"
+ return
+ fi
+ check_pin_ids "${pin_ids[@]}"
+
+ log_test "DPLL pin enumeration (${#pin_ids[@]} pins)"
+}
+
+check_pin_properties()
+{
+ local pin_id=$1
+ local json field
+
+ # A pin that cannot be queried at all would be reported as one
+ # that misses every attribute, so bail out on that first.
+ if ! json=$(dpll_pin_json "$pin_id") || [ -z "$json" ]; then
+ check_err 1 "pin $pin_id query failed"
+ return
+ fi
+
+ # Frequency is optional, a pin without a frequency callback such
+ # as a recovered clock pin does not report it.
+ for field in '.id' '.type'; do
+ if ! jq -e "$field" <<< "$json" &> /dev/null; then
+ check_err 1 \
+ "pin $pin_id is missing the $field attribute"
+ fi
+ done
+
+ # The parent-device nest is not present for all pin types, but when
+ # the device under test is in it, that entry must carry the
+ # direction. A pin can be registered on more than one device, so
+ # match on parent-id instead of taking the first entry.
+ if jq -e --argjson dev "$DPLL_DEV_ID" \
+ '[.["parent-device"][]? | select(.["parent-id"] == $dev)] |
+ length > 0' <<< "$json" &> /dev/null; then
+ if ! jq -e --argjson dev "$DPLL_DEV_ID" \
+ '.["parent-device"][]? |
+ select(.["parent-id"] == $dev) | .direction' \
+ <<< "$json" &> /dev/null; then
+ check_err 1 \
+ "pin $pin_id parent-device is missing direction"
+ fi
+ fi
+}
+
+test_pin_properties()
+{
+ local pin_ids pin_id
+
+ RET=0
+
+ mapfile -t pin_ids < <(dpll_pin_get_ids "$DPLL_DEV_ID")
+ if [ "${#pin_ids[@]}" -eq 0 ]; then
+ dpll_log_test_skip_or_fail "DPLL pin properties" "no pins found"
+ return
+ fi
+ if ! check_pin_ids "${pin_ids[@]}"; then
+ log_test "DPLL pin properties"
+ return
+ fi
+
+ for pin_id in "${pin_ids[@]}"; do
+ check_pin_properties "$pin_id"
+ done
+
+ log_test "DPLL pin properties (${#pin_ids[@]} pins)"
+}
+
+test_pin_state()
+{
+ local pin_id state
+
+ RET=0
+
+ pin_id=$(dpll_pin_find_by_direction "$DPLL_DEV_ID" input | head -1)
+ if [ -z "$pin_id" ]; then
+ dpll_log_test_skip_or_fail "DPLL pin state" "no input pin"
+ return
+ fi
+
+ # The pin was found through its parent-device nest for this device
+ # and the state is mandatory there, so an empty value is a failed
+ # query rather than a state.
+ state=$(dpll_pin_get_state "$DPLL_DEV_ID" "$pin_id")
+ case "$state" in
+ "")
+ check_err 1 "cannot read the state of pin $pin_id"
+ ;;
+ connected|disconnected|selectable)
+ ;;
+ *)
+ check_err 1 "pin $pin_id reports invalid state '$state'"
+ ;;
+ esac
+
+ log_test "DPLL pin state"
+}
+
##############################################################################
cleanup()
@@ -50,9 +180,9 @@ cleanup()
# A second signal must not abort the teardown half way.
trap '' INT TERM
- if [ -n "$CREATED_DEVICE" ]; then
- dpllsim_delete_device "$CREATED_DEVICE" ||
- log_info "cannot delete device $CREATED_DEVICE"
+ if [ -n "$DPLL_CREATED_DEVICE" ]; then
+ dpllsim_delete_device "$DPLL_CREATED_DEVICE" ||
+ log_info "cannot delete device $DPLL_CREATED_DEVICE"
fi
# The device whose creation a signal interrupted is recorded by the
# library alone, a double delete of a tracked one is harmless.
@@ -69,7 +199,6 @@ dpll_lib_check_prereqs
dpll_lib_lock
dpll_lib_init
-CREATED_DEVICE=""
# The module check may mount configfs, so the exit trap that unmounts it
# again has to be in place before. INT and TERM exit through it as well,
# so that a timeout imposed by the harness still removes the device.
@@ -82,8 +211,8 @@ if [ -z "$DPLL_DEV_ID" ]; then
echo "SKIP: cannot create dpllsim device"
exit "$ksft_skip"
fi
- CREATED_DEVICE=$DPLLSIM_CREATED_NAME
DPLL_DEV_ID=$DPLLSIM_DEV_ID
+ DPLL_CREATED_DEVICE=$DPLLSIM_CREATED_NAME
DPLL_MODULE_NAME="dpllsim"
fi
diff --git a/tools/testing/selftests/drivers/dpll/dpll_lib.sh b/tools/testing/selftests/drivers/dpll/dpll_lib.sh
index 8d46d4cff06c58..0db4a7e2e12fcf 100644
--- a/tools/testing/selftests/drivers/dpll/dpll_lib.sh
+++ b/tools/testing/selftests/drivers/dpll/dpll_lib.sh
@@ -44,6 +44,10 @@ dpll_lib_check_prereqs()
echo "SKIP: dpll tool too old, missing 'device show'"
exit "$ksft_skip"
fi
+ if ! dpll pin help 2>&1 | grep -q "show"; then
+ echo "SKIP: dpll tool too old, missing 'pin show'"
+ exit "$ksft_skip"
+ fi
}
# The scripts that create dpllsim devices share the one dpllsim directory
@@ -122,14 +126,39 @@ dpll_lib_init()
fi
}
+# Name of the dpllsim device a script created as the device under test,
+# set by the script next to DPLL_DEV_ID. Empty when the user provided
+# the device.
+DPLL_CREATED_DEVICE=""
+
+# The layout and the capabilities of the device a script created are
+# known, so a missing pin, attribute or capability on it can only mean a
+# failed query or broken tooling. Report such a condition as a failure
+# on the created device and as a skip on a device the user provided,
+# where it is a property of the hardware.
+# $1: test name, $2: reason
+dpll_log_test_skip_or_fail()
+{
+ local name=$1
+ local reason=$2
+
+ if [ -n "$DPLL_CREATED_DEVICE" ]; then
+ check_err 1 "$reason on the device created by the test"
+ log_test "$name"
+ else
+ log_test_skip "$name" "$reason"
+ fi
+}
+
##############################################################################
# Device queries
#
# "dpll device show id X -j" may print either a flat object or a dump-style
# {"device":[...]} wrapper depending on the iproute2 version, so normalize
-# with (.device[0]? // .) before extracting fields. The tool is run first
-# and jq only on what it printed, so that a failed query returns its
-# status instead of the empty output a missing field produces too.
+# with (.device[0]? // .) before extracting fields, same for pins. The
+# device helpers run the tool first and jq only on what it printed, so
+# that a failed query returns its status instead of the empty output a
+# missing field produces too.
dpll_device_json()
{
@@ -211,12 +240,110 @@ dpll_device_wait_gone_by_clock_id()
busywait "$BUSYWAIT_TIMEOUT" __dpll_device_absent_by_clock_id "$@"
}
+##############################################################################
+# Pin queries
+#
+# Attributes of the pin itself are read by pin id alone. Attributes that
+# live in the parent-device nest (state, prio, ...) take the device id in
+# addition, to select the nest of the device under test. The helpers
+# that work on the full pin dump normalize its shape like the id-based
+# ones do, with (.pin? // [.]), so that a flat object would work too.
+# Like the device queries, every helper runs the tool first and jq only
+# on what it printed, and returns 1 when the tool failed.
+
+dpll_pin_json()
+{
+ local pin_id=$1
+ local out
+
+ out=$(dpll pin show id "$pin_id" -j 2>/dev/null) || return 1
+ jq '(.pin[0]? // .)' <<< "$out"
+}
+
+__dpll_pin_field()
+{
+ local pin_id=$1; shift
+ local out
+
+ out=$(dpll pin show id "$pin_id" -j 2>/dev/null) || return 1
+ jq -r "(.pin[0]? // .) | $1 // empty" <<< "$out"
+}
+
+# Fields living in the parent-device nest matching the given device
+__dpll_pin_dev_field()
+{
+ local dev_id=$1
+ local pin_id=$2; shift 2
+ local out
+
+ out=$(dpll pin show id "$pin_id" -j 2>/dev/null) || return 1
+ jq -r --argjson dev "$dev_id" \
+ "(.pin[0]? // .) | .[\"parent-device\"][]? |
+ select(.[\"parent-id\"] == \$dev) | $1 // empty" <<< "$out"
+}
+
+# Print ids of all pins belonging to the device, one per line: pins
+# registered on the device directly plus pins registered on those pins
+# (MUX children), followed recursively so that children of children are
+# included too. Older iproute2 lacks the "pin show device <id>" filter,
+# so filter the full dump with jq.
+dpll_pin_get_ids()
+{
+ local dev_id=$1
+ local out
+
+ out=$(dpll pin show -j 2>/dev/null) || return 1
+ jq -r --argjson dev "$dev_id" '
+ (.pin? // [.]) as $pins |
+ def children($ids):
+ [$pins[] | select(any(.["parent-pin"][]?;
+ .["parent-id"] as $p |
+ $ids | index($p))) | .id];
+ def closure($ids):
+ (($ids + children($ids)) | unique) as $n |
+ if ($n | length) == ($ids | length) then $ids
+ else closure($n) end;
+ closure([$pins[] |
+ select(any(.["parent-device"][]?;
+ .["parent-id"] == $dev)) | .id])
+ as $all |
+ $pins[] | select(.id as $i | $all | index($i)) | .id' \
+ <<< "$out"
+}
+
+# Print ids of all non-MUX pins of the device with the given direction
+# (input/output), one per line. Direction lives in the parent-device nest.
+dpll_pin_find_by_direction()
+{
+ local dev_id=$1
+ local dir=$2
+ local out
+
+ out=$(dpll pin show -j 2>/dev/null) || return 1
+ jq -r --argjson dev "$dev_id" --arg dir "$dir" \
+ '(.pin? // [.])[] | select(.type != "mux") |
+ select(.["parent-device"][]? |
+ (.["parent-id"] == $dev and .direction == $dir)) |
+ .id' <<< "$out"
+}
+
+dpll_pin_get_frequency()
+{
+ __dpll_pin_field "$1" '.frequency'
+}
+
+dpll_pin_get_state()
+{
+ __dpll_pin_dev_field "$1" "$2" '.state'
+}
+
##############################################################################
# dpllsim configfs interface
#
-# Each directory under $DPLLSIM_CFG is one simulated device. Writing 1 to
-# <dev>/live registers the device with the DPLL core, writing 0
-# unregisters it. The structure (clock_id) can only be changed while the
+# Each directory under $DPLLSIM_CFG is one simulated device; its pins are
+# directories under <dev>/pins/. Writing 1 to <dev>/live registers the
+# device and its pins with the DPLL core, writing 0 unregisters them. The
+# structure (pins, clock_id, pin attributes) can only be changed while the
# device is not live.
#
# dpllsim_available mounts configfs when nothing else did, which is the
@@ -289,6 +416,28 @@ dpllsim_create_device()
fi
}
+# $1: device name, $2: pin name (becomes the board-label), $3: type (gnss,
+# int-oscillator, ext, synce-eth-port, mux), $4: frequency in Hz,
+# $5: direction (input/output)
+dpllsim_add_pin()
+{
+ local dev=$1
+ local pin=$2
+ local type=$3
+ local frequency=$4
+ local direction=$5
+ local pin_path="$DPLLSIM_CFG/$dev/pins/$pin"
+
+ mkdir "$pin_path" 2>/dev/null || return 1
+
+ if ! echo "$type" 2>/dev/null > "$pin_path/type" ||
+ ! echo "$frequency" 2>/dev/null > "$pin_path/frequency" ||
+ ! echo "$direction" 2>/dev/null > "$pin_path/direction"; then
+ rmdir "$pin_path" 2>/dev/null
+ return 1
+ fi
+}
+
dpllsim_deploy()
{
echo 1 2>/dev/null > "$DPLLSIM_CFG/$1/live"
@@ -299,17 +448,22 @@ dpllsim_undeploy()
echo 0 2>/dev/null > "$DPLLSIM_CFG/$1/live"
}
-# Take the device offline and remove it. Every step is attempted even
-# when an earlier one failed, the return value is non-zero when any did.
-# A device that does not exist counts as deleted.
+# Take the device offline and remove it together with all its pins.
+# Every step is attempted even when an earlier one failed, the return
+# value is non-zero when any did. A device that does not exist counts as
+# deleted.
dpllsim_delete_device()
{
local dev_path="$DPLLSIM_CFG/$1"
- local ret=0
+ local pin_path ret=0
[ -d "$dev_path" ] || return 0
dpllsim_undeploy "$1" || ret=1
+ for pin_path in "$dev_path"/pins/*/; do
+ [ -d "$pin_path" ] || continue
+ rmdir "$pin_path" || ret=1
+ done
rmdir "$dev_path" || ret=1
return "$ret"
@@ -323,8 +477,12 @@ dpllsim_alloc_clock_id()
DPLLSIM_CLOCK_ID=$((DPLLSIM_NEXT_CLOCK_ID++))
}
-# Create a device, deploy it and resolve the netlink device id. Sets
-# DPLLSIM_CREATED_NAME, DPLLSIM_CLOCK_ID and DPLLSIM_DEV_ID.
+# Create a device with the standard 3-pin layout, deploy it and resolve
+# the netlink device id. Sets DPLLSIM_CREATED_NAME, DPLLSIM_CLOCK_ID and
+# DPLLSIM_DEV_ID.
+# gnss0: GNSS, 1 Hz, input
+# ext0: EXT, 10 MHz, input
+# out0: EXT, 10 MHz, output
# shellcheck disable=SC2120 # the argument is optional
dpllsim_setup_standard_device()
{
@@ -335,6 +493,13 @@ dpllsim_setup_standard_device()
name="std$clock_id"
dpllsim_create_device "$name" "$clock_id" || return 1
+ if ! dpllsim_add_pin "$name" gnss0 gnss 1 input ||
+ ! dpllsim_add_pin "$name" ext0 ext 10000000 input ||
+ ! dpllsim_add_pin "$name" out0 ext 10000000 output; then
+ dpllsim_delete_device "$name"
+ return 1
+ fi
+
if ! dpllsim_deploy "$name"; then
dpllsim_delete_device "$name"
return 1
diff --git a/tools/testing/selftests/drivers/dpll/dpll_stress.sh b/tools/testing/selftests/drivers/dpll/dpll_stress.sh
index 01d57ab0172b0f..b21325d8368fb2 100755
--- a/tools/testing/selftests/drivers/dpll/dpll_stress.sh
+++ b/tools/testing/selftests/drivers/dpll/dpll_stress.sh
@@ -58,15 +58,47 @@ cleanup()
dpll_lib_cleanup
}
-# Create a device named after the given clock-id and deploy it. Sets
-# DPLLSIM_CREATED_NAME.
+# Pin type and a frequency the simulator supports for it.
+STRESS_PIN_KINDS=(
+ "gnss 1"
+ "ext 10000000"
+ "ext 25000000"
+ "synce-eth-port 10000000"
+ "int-oscillator 25000000"
+ "mux 2048000"
+)
+
+# Create a device named after the given clock-id with the given pin count
+# and deploy it. Sets DPLLSIM_CREATED_NAME.
create_stress_device()
{
local clock_id=$1
+ local num_pins=$2
local name="st$clock_id"
+ local i type freq dir
+ local dirs=(input output)
dpllsim_create_device "$name" "$clock_id" || return 1
+ # Always have at least one plain input pin, then random ones
+ if ! dpllsim_add_pin "$name" ext0 ext 10000000 input; then
+ dpllsim_delete_device "$name"
+ return 1
+ fi
+ for i in $(seq 1 $((num_pins - 1))); do
+ read -r type freq <<< \
+ "${STRESS_PIN_KINDS[RANDOM % ${#STRESS_PIN_KINDS[@]}]}"
+ # A MUX pin is an input selector, the simulator rejects an
+ # output one
+ dir=${dirs[RANDOM % 2]}
+ [ "$type" = mux ] && dir=input
+ if ! dpllsim_add_pin "$name" "pin$i" "$type" "$freq" \
+ "$dir"; then
+ dpllsim_delete_device "$name"
+ return 1
+ fi
+ done
+
if ! dpllsim_deploy "$name"; then
dpllsim_delete_device "$name"
return 1
@@ -84,7 +116,7 @@ test_rapid_create_delete()
for i in $(seq 1 "$STRESS_ITERATIONS"); do
dpllsim_alloc_clock_id; clock_id=$DPLLSIM_CLOCK_ID
- if ! create_stress_device "$clock_id"; then
+ if ! create_stress_device "$clock_id" 5; then
check_err 1 "failed to create device (iteration $i)"
break
fi
@@ -116,6 +148,14 @@ test_deploy_cycle()
fi
track_device "$name"
+ if ! dpllsim_add_pin "$name" gnss0 gnss 1 input ||
+ ! dpllsim_add_pin "$name" ext0 ext 10000000 input ||
+ ! dpllsim_add_pin "$name" out0 ext 10000000 output; then
+ check_err 1 "cannot add pins"
+ log_test "DPLL deploy/undeploy cycling"
+ return
+ fi
+
for i in $(seq 1 "$STRESS_ITERATIONS"); do
if ! dpllsim_deploy "$name"; then
check_err 1 "deploy failed (iteration $i)"
--
2.56.0
^ permalink raw reply related [flat|nested] 31+ messages in thread* Re: [PATCH net-next v3 04/14] selftests: dpll: add pin enumeration and property tests
2026-11-09 9:35 ` [PATCH net-next v3 04/14] selftests: dpll: add pin enumeration and property tests Petr Oros
@ 2026-11-11 9:37 ` netdev-bot+sashiko
0 siblings, 0 replies; 31+ messages in thread
From: netdev-bot+sashiko @ 2026-11-11 9:37 UTC (permalink / raw)
To: poros
Cc: netdev, ivecera, jiri, corbet, skhan, shuah, rdunlap, jani.nikula,
vadim.fedorenko, arkadiusz.kubalewski, jiri, andrew+netdev, davem,
edumazet, kuba, pabeni, linux-doc, linux-kselftest
Thank you for your contribution! Sashiko AI review found 2 potential
issue(s) to consider.
Critical: 0 · High: 0 · Medium: 1 · Low: 1
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 dpll_basic.sh, a failed pin dump is reported as SKIP
instead of FAIL on hardware or user-selected DPLL devices.
- [Low] The comment in check_pin_properties (dpll_basic.sh line 106) says
'The parent-device nest is not present for all pin types'.
--
selftests: dpll: add pin enumeration and property tests
This adds pin query helpers and dpllsim_add_pin() to the dpll selftest
library and gives the standard dpllsim device three pins. dpll_basic.sh
gains pin enumeration, pin property and pin state tests. The stress
script now creates its devices with a random set of pins.
> diff --git a/tools/testing/selftests/drivers/dpll/dpll_basic.sh b/tools/testing/selftests/drivers/dpll/dpll_basic.sh
> index 827c389fe1aa3..eb5de2e22081e 100755
> --- a/tools/testing/selftests/drivers/dpll/dpll_basic.sh
> +++ b/tools/testing/selftests/drivers/dpll/dpll_basic.sh
[ ... ]
> @@ -43,6 +65,114 @@ test_device_properties()
> log_test "DPLL device properties"
> }
>
> +test_pin_enumeration()
> +{
> + local pin_ids
> +
> + RET=0
> +
> + mapfile -t pin_ids < <(dpll_pin_get_ids "$DPLL_DEV_ID")
> + if [ "${#pin_ids[@]}" -eq 0 ]; then
> + dpll_log_test_skip_or_fail "DPLL pin enumeration" \
> + "no pins found"
> + return
> + fi
[Severity: Medium]
Can a failed pin dump get reported as a skip here?
dpll_pin_get_ids() returns 1 when the tool fails:
out=$(dpll pin show -j 2>/dev/null) || return 1
The exit status of a process substitution is lost, though, and mapfile
itself succeeds. So a failed query leaves pin_ids empty, which looks the
same as a device with no pins.
test_pin_properties() uses the same pattern:
mapfile -t pin_ids < <(dpll_pin_get_ids "$DPLL_DEV_ID")
test_pin_state() has the same problem. pipefail is not enabled there, so
the pipeline returns the status of head:
pin_id=$(dpll_pin_find_by_direction "$DPLL_DEV_ID" input | head -1)
All three then call dpll_log_test_skip_or_fail(), which calls
log_test_skip whenever DPLL_CREATED_DEVICE is empty. That happens for the
hardware device dpll_lib_init() picks by default, for a device chosen
through DPLL_DRIVER and for one set with DPLL_DEV_ID.
On such a device, would a pin dump that fails because of a driver or
netlink regression be logged as a skip with "no pins found" or "no input
pin", and not as a failure? On the created dpllsim device the test still
fails, but it gives "no pins found" as the reason and does not say that
the query failed.
The commit message says:
every pin query runs the tool first and jq on what it printed, so
that a query which fails returns the status of the tool instead of
the empty output a missing attribute produces too
None of these callers seem to check that status.
> + check_pin_ids "${pin_ids[@]}"
> +
> + log_test "DPLL pin enumeration (${#pin_ids[@]} pins)"
> +}
> +
> +check_pin_properties()
> +{
[ ... ]
> + # The parent-device nest is not present for all pin types, but when
> + # the device under test is in it, that entry must carry the
> + # direction. A pin can be registered on more than one device, so
> + # match on parent-id instead of taking the first entry.
[Severity: Low]
This isn't a bug, but is the first sentence of this comment accurate? In
the DPLL core, whether the parent-device nest is present depends on how
the pin was registered, not on its type:
drivers/dpll/dpll_netlink.c:dpll_cmd_pin_get_one() {
...
if (xa_empty(&pin->parent_refs))
ret = dpll_msg_add_pin_dplls(msg, pin, extack);
else
ret = dpll_msg_add_pin_parents(msg, pin, ref, extack);
...
}
A pin registered directly on a device always gets parent-device nests,
MUX pins included. A pin registered on a parent pin gets parent-pin nests
instead, whatever its type.
The comment above dpll_pin_get_ids() in dpll_lib.sh gets this right
("pins registered on those pins (MUX children)"). Could this comment say
the same thing, that the nest is missing for pins registered on a parent
pin? The check itself is not affected.
[ ... ]
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261109093523.931201-1-poros%40redhat.com
^ permalink raw reply [flat|nested] 31+ messages in thread
* [PATCH net-next v3 05/14] dpll: dpllsim: Add support to set input pin state
2026-11-09 9:35 [PATCH net-next v3 00/14] dpll: add a simulated DPLL device driver Petr Oros
` (3 preceding siblings ...)
2026-11-09 9:35 ` [PATCH net-next v3 04/14] selftests: dpll: add pin enumeration and property tests Petr Oros
@ 2026-11-09 9:35 ` Petr Oros
2026-11-09 13:59 ` Ivan Vecera
2026-11-09 9:35 ` [PATCH net-next v3 06/14] selftests: dpll: add pin state tests Petr Oros
` (8 subsequent siblings)
13 siblings, 1 reply; 31+ messages in thread
From: Petr Oros @ 2026-11-09 9:35 UTC (permalink / raw)
To: netdev
Cc: Petr Oros, Jonathan Corbet, Shuah Khan, Shuah Khan, Randy Dunlap,
Jani Nikula, Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
Ivan Vecera, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, linux-doc, linux-kselftest
Add support for setting the state of input pins on the device.
Implement the state_on_dpll_set callback for input pins and advertise
the state-can-change capability for them, so that userspace can move a
pin between the states valid for the device mode. The core checks the
capability only, the states a mode allows are up to the driver: in
automatic mode a pin is selectable or disconnected, in manual mode
connected or disconnected, as Documentation/driver-api/dpll.rst
describes them. The device reports automatic mode, so connected is
refused with -EINVAL and an extack message. The check is written for
both modes already, so that manual mode has nothing to add to it when
it arrives. The driver stores the requested state and reports it back
unchanged, it does not act on it yet. Output pins keep reporting their
state only.
Signed-off-by: Petr Oros <poros@redhat.com>
---
v3:
- refuse pin states that are not valid in the device mode, so that
connected is refused in automatic mode, the mode the device reports,
with an extack message (Ivan Vecera)
- the Acked-by of v2 is not carried over, the callback changed
v2: https://lore.kernel.org/all/20261105151852.1844305-6-poros@redhat.com/
v1: https://lore.kernel.org/all/20261103093910.596225-6-poros@redhat.com/
---
drivers/dpll/dpllsim/dpll.c | 42 +++++++++++++++++++++++++++++++++++++
1 file changed, 42 insertions(+)
diff --git a/drivers/dpll/dpllsim/dpll.c b/drivers/dpll/dpllsim/dpll.c
index a1d5921fb8d017..794020d0c112e8 100644
--- a/drivers/dpll/dpllsim/dpll.c
+++ b/drivers/dpll/dpllsim/dpll.c
@@ -104,10 +104,46 @@ static int dpllsim_pin_state_get(const struct dpll_pin *pin, void *pin_priv,
return 0;
}
+/*
+ * The states a user may request depend on the device mode: connected and
+ * disconnected select and deselect the reference in manual mode, selectable
+ * and disconnected offer a pin to or withdraw it from the automatic
+ * selection. The core checks the state-can-change capability only.
+ */
+static bool dpllsim_pin_state_valid(const struct dpllsim_dev *sim,
+ enum dpll_pin_state state)
+{
+ if (state == DPLL_PIN_STATE_DISCONNECTED)
+ return true;
+ if (sim->mode == DPLL_MODE_MANUAL)
+ return state == DPLL_PIN_STATE_CONNECTED;
+ return state == DPLL_PIN_STATE_SELECTABLE;
+}
+
+static int dpllsim_pin_state_set(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 dpllsim_pin *sim_pin = pin_priv;
+ struct dpllsim_dev *sim = sim_pin->dev;
+
+ guard(mutex)(&sim->lock);
+ if (!dpllsim_pin_state_valid(sim, state)) {
+ NL_SET_ERR_MSG_FMT(extack, "Pin state not valid in %s mode",
+ sim->mode == DPLL_MODE_MANUAL ?
+ "manual" : "automatic");
+ return -EINVAL;
+ }
+ sim_pin->state = state;
+ return 0;
+}
+
static const struct dpll_pin_ops dpllsim_input_pin_ops = {
.frequency_get = dpllsim_pin_frequency_get,
.direction_get = dpllsim_pin_direction_get,
.state_on_dpll_get = dpllsim_pin_state_get,
+ .state_on_dpll_set = dpllsim_pin_state_set,
};
static const struct dpll_pin_ops dpllsim_output_pin_ops = {
@@ -149,6 +185,12 @@ static void dpllsim_pin_props(struct dpllsim_pin *pin,
prop->freq_supported_num = ARRAY_SIZE(dpllsim_freq_ext);
break;
}
+
+ /* Capabilities follow the ops the pin is served by */
+ if (dpllsim_pin_select_ops(pin) == &dpllsim_input_pin_ops)
+ prop->capabilities = DPLL_PIN_CAPABILITIES_STATE_CAN_CHANGE;
+ else
+ prop->capabilities = 0;
}
static bool dpllsim_pin_frequency_valid(struct dpllsim_pin *pin)
--
2.56.0
^ permalink raw reply related [flat|nested] 31+ messages in thread* Re: [PATCH net-next v3 05/14] dpll: dpllsim: Add support to set input pin state
2026-11-09 9:35 ` [PATCH net-next v3 05/14] dpll: dpllsim: Add support to set input pin state Petr Oros
@ 2026-11-09 13:59 ` Ivan Vecera
0 siblings, 0 replies; 31+ messages in thread
From: Ivan Vecera @ 2026-11-09 13:59 UTC (permalink / raw)
To: Petr Oros, netdev
Cc: Jonathan Corbet, Shuah Khan, Shuah Khan, Randy Dunlap,
Jani Nikula, Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni, linux-doc, linux-kselftest
On 11/9/26 10:35, Petr Oros wrote:
> Add support for setting the state of input pins on the device.
> Implement the state_on_dpll_set callback for input pins and advertise
> the state-can-change capability for them, so that userspace can move a
> pin between the states valid for the device mode. The core checks the
> capability only, the states a mode allows are up to the driver: in
> automatic mode a pin is selectable or disconnected, in manual mode
> connected or disconnected, as Documentation/driver-api/dpll.rst
> describes them. The device reports automatic mode, so connected is
> refused with -EINVAL and an extack message. The check is written for
> both modes already, so that manual mode has nothing to add to it when
> it arrives. The driver stores the requested state and reports it back
> unchanged, it does not act on it yet. Output pins keep reporting their
> state only.
>
> Signed-off-by: Petr Oros<poros@redhat.com>
> ---
> v3:
> - refuse pin states that are not valid in the device mode, so that
> connected is refused in automatic mode, the mode the device reports,
> with an extack message (Ivan Vecera)
> - the Acked-by of v2 is not carried over, the callback changed
LGTM...
Reviewed-by: Ivan Vecera <ivecera@redhat.com>
^ permalink raw reply [flat|nested] 31+ messages in thread
* [PATCH net-next v3 06/14] selftests: dpll: add pin state tests
2026-11-09 9:35 [PATCH net-next v3 00/14] dpll: add a simulated DPLL device driver Petr Oros
` (4 preceding siblings ...)
2026-11-09 9:35 ` [PATCH net-next v3 05/14] dpll: dpllsim: Add support to set input pin state Petr Oros
@ 2026-11-09 9:35 ` Petr Oros
2026-11-09 14:00 ` Ivan Vecera
2026-11-11 9:37 ` netdev-bot+sashiko
2026-11-09 9:35 ` [PATCH net-next v3 07/14] dpll: dpllsim: Add support to get/set priority on input pins Petr Oros
` (7 subsequent siblings)
13 siblings, 2 replies; 31+ messages in thread
From: Petr Oros @ 2026-11-09 9:35 UTC (permalink / raw)
To: netdev
Cc: Petr Oros, Jonathan Corbet, Shuah Khan, Shuah Khan, Randy Dunlap,
Jani Nikula, Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
Ivan Vecera, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, linux-doc, linux-kselftest
Add dpll_features.sh, a script that exercises individual device and pin
callbacks with get/set roundtrips over netlink. It creates a dpllsim
device with the standard pin layout by default or runs against the
device named by DPLL_DEV_ID or the first device of the driver named by
DPLL_DRIVER, and picks the pin to reconfigure up front, the first input
pin that is not connected on the device, so that no test touches the
reference the device is locked to.
The first test moves that pin through the states of the device mode,
as Documentation/driver-api/dpll.rst defines them, reads the state back
after each step and restores the original state at the end. In
automatic mode these are disconnected and selectable, and on the
device the script created the test then checks that connected is
refused. Hardware drivers are not held to that, ice accepts connected
in automatic mode, and a pin with the state-connected-override
capability would take over the reference. In manual mode connected and
disconnected
are the states, connecting the pin makes it the reference, so the test
finds the pin connected before, connects it back at the end and checks
that it is the reference again.
A driver may refuse a state the capability promises, for a constraint
the interface does not tell. That is a skip on a device provided by
the user, with the reason the tool reported, and a failure on the
device the script created, whose simulator accepts both states of its
mode. The restores are deferred right after the original states are
read, and the cleanup handler of every script that defers runs the
pending restores before it removes the devices the script created, so
that an interrupted run puts the device back as well.
The stress script gains a test that floods one input pin of a fresh
device with changes between the states of automatic mode. The library
gains the state setter both use, which keeps the extack line of the
tool in DPLL_LAST_ERR so that a test can tell a refusal from a failure,
the device mode getter, and a check whether the pin set usage of the
tool knows a keyword, so that a test skips on a tool too old for it
rather than every script requiring a newer tool.
Signed-off-by: Petr Oros <poros@redhat.com>
---
v3:
- follow the mode of the device under test: disconnected and
selectable in automatic mode, where the simulator is checked to
refuse connected, connected and disconnected in manual mode, where
the pin connected before is connected back at the end
- cycle the state flooding test through the states of automatic mode
- add the device mode getter to the library
- honor DPLL_DRIVER like dpll_basic.sh does
- follow the move of the selftests to
tools/testing/selftests/drivers/dpll
- the Reviewed-by and Acked-by of v2 are not carried over, the
state test changed
v2:
- quote the ksft_skip expansion (netdev CI shellcheck)
v2: https://lore.kernel.org/all/20261105151852.1844305-7-poros@redhat.com/
v1: https://lore.kernel.org/all/20261103093910.596225-7-poros@redhat.com/
---
tools/testing/selftests/drivers/dpll/Makefile | 1 +
.../selftests/drivers/dpll/dpll_features.sh | 256 ++++++++++++++++++
.../selftests/drivers/dpll/dpll_lib.sh | 81 +++++-
.../selftests/drivers/dpll/dpll_stress.sh | 58 ++++
4 files changed, 395 insertions(+), 1 deletion(-)
create mode 100755 tools/testing/selftests/drivers/dpll/dpll_features.sh
diff --git a/tools/testing/selftests/drivers/dpll/Makefile b/tools/testing/selftests/drivers/dpll/Makefile
index 7e839ae71cd22f..1349d078fa2660 100644
--- a/tools/testing/selftests/drivers/dpll/Makefile
+++ b/tools/testing/selftests/drivers/dpll/Makefile
@@ -9,6 +9,7 @@ TEST_FILES := dpll_lib.sh
TEST_PROGS := \
dpll_basic.sh \
+ dpll_features.sh \
dpll_stress.sh \
# end of TEST_PROGS
diff --git a/tools/testing/selftests/drivers/dpll/dpll_features.sh b/tools/testing/selftests/drivers/dpll/dpll_features.sh
new file mode 100755
index 00000000000000..874007411bc5e7
--- /dev/null
+++ b/tools/testing/selftests/drivers/dpll/dpll_features.sh
@@ -0,0 +1,256 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# DPLL device and pin feature tests. Exercises individual dpll_device_ops
+# and dpll_pin_ops callbacks, verifying get/set roundtrips through the
+# netlink interface. By default a dpllsim device with a known pin layout
+# is created; set DPLL_DEV_ID or DPLL_DRIVER to run against an existing
+# device instead, the simulator-only tests then skip unless that device
+# is a dpllsim one.
+#
+# Copyright (c) 2026 Red Hat, Inc.
+
+SCRIPTDIR=$(dirname "$(readlink -e "${BASH_SOURCE[0]}")")
+
+source "$SCRIPTDIR/../../net/lib.sh"
+source "$SCRIPTDIR/dpll_lib.sh"
+
+ALL_TESTS="
+ test_pin_state_transitions
+"
+
+##############################################################################
+# Pin-level feature tests
+
+# Find the input pin connected on the device, the reference of a device in
+# manual mode. Prints nothing when there is none.
+find_connected_pin()
+{
+ local pin_id
+
+ for pin_id in "${input_pins[@]}"; do
+ if [ "$(dpll_pin_get_state "$DPLL_DEV_ID" "$pin_id")" = \
+ connected ]; then
+ echo "$pin_id"
+ return
+ fi
+ done
+}
+
+test_pin_state_transitions()
+{
+ local mode before state after reason ref_pin
+ local states
+
+ RET=0
+
+ if [ -z "$PIN_TUNE_ID" ]; then
+ dpll_log_test_skip_or_fail "DPLL pin state transitions" \
+ "no spare input pin"
+ return
+ fi
+ if ! dpll_pin_set_supports state; then
+ log_test_skip "DPLL pin state transitions" \
+ "dpll tool lacks pin set state"
+ return
+ fi
+ if ! dpll_pin_has_capability "$PIN_TUNE_ID" state-can-change; then
+ dpll_log_test_skip_or_fail "DPLL pin state transitions" \
+ "state cannot change"
+ return
+ fi
+
+ # The states a user may request depend on the device mode, see
+ # Documentation/driver-api/dpll.rst: connected and disconnected
+ # select and deselect the reference in manual mode, selectable and
+ # disconnected offer a pin to the automatic selection or withdraw
+ # it. The simulator runs in automatic mode.
+ mode=$(dpll_device_get_mode "$DPLL_DEV_ID")
+ case "$mode" in
+ automatic)
+ states="disconnected selectable"
+ ;;
+ manual)
+ states="connected disconnected"
+ ;;
+ *)
+ dpll_log_test_skip_or_fail "DPLL pin state transitions" \
+ "device mode '$mode' not handled"
+ return
+ ;;
+ esac
+
+ before=$(dpll_pin_get_state "$DPLL_DEV_ID" "$PIN_TUNE_ID")
+ if [ -z "$before" ]; then
+ dpll_log_test_skip_or_fail "DPLL pin state transitions" \
+ "state not reported"
+ return
+ fi
+ # Restore the original state even when the test aborts half way.
+ defer dpll_pin_set_state "$DPLL_DEV_ID" "$PIN_TUNE_ID" "$before"
+
+ # In manual mode connecting the pin makes it the reference and
+ # takes that over from the pin connected so far, so that one is
+ # connected back at the end, after the restore above has
+ # disconnected the test pin again. In automatic mode the pin is not
+ # connected, see how PIN_TUNE_ID is picked, and the test never
+ # connects it, the device keeps the reference it locked to.
+ ref_pin=""
+ if [ "$mode" = manual ]; then
+ ref_pin=$(find_connected_pin)
+ if [ -n "$ref_pin" ]; then
+ defer dpll_pin_set_state "$DPLL_DEV_ID" "$ref_pin" \
+ connected
+ fi
+ fi
+
+ # A driver may refuse a state the capability promises, for a
+ # constraint the interface does not tell. On a device provided by
+ # the user that is a skip with the reason the tool reported, on
+ # the created device a failure, the simulator accepts both states
+ # of its mode. The test ends there, whichever set was refused, a
+ # state that was not set has nothing to read back and the deferred
+ # restores put the pins back. A refused restore is treated the same
+ # way when nothing failed before it, afterwards it is one more
+ # failure, a skip would hide the earlier one.
+ for state in $states; do
+ if ! dpll_pin_set_state "$DPLL_DEV_ID" "$PIN_TUNE_ID" \
+ "$state"; then
+ reason=${DPLL_LAST_ERR%%$'\n'*}
+ dpll_log_test_skip_or_fail \
+ "DPLL pin state transitions" \
+ "cannot set pin state to $state${reason:+, $reason}"
+ return
+ fi
+ after=$(dpll_pin_get_state "$DPLL_DEV_ID" "$PIN_TUNE_ID")
+ if [ "$after" != "$state" ]; then
+ check_err 1 "pin not $state, state '$after'"
+ fi
+ done
+
+ # Connected is not a state of automatic mode and the simulator
+ # refuses it. Hardware drivers are not held to that, ice for one
+ # accepts it there, and a pin with the state-connected-override
+ # capability would take over the reference, so the check runs on
+ # the device the script created only.
+ if [ "$mode" = automatic ] && [ -n "$DPLL_CREATED_DEVICE" ]; then
+ if dpll_pin_set_state "$DPLL_DEV_ID" "$PIN_TUNE_ID" \
+ connected; then
+ check_err 1 "connected accepted in automatic mode"
+ fi
+ after=$(dpll_pin_get_state "$DPLL_DEV_ID" "$PIN_TUNE_ID")
+ if [ "$after" != "$state" ]; then
+ check_err 1 "refused state changed the pin to '$after'"
+ fi
+ fi
+
+ if ! dpll_pin_set_state "$DPLL_DEV_ID" "$PIN_TUNE_ID" "$before"; then
+ reason=${DPLL_LAST_ERR%%$'\n'*}
+ reason=${reason:+, $reason}
+ if [ "$RET" -eq 0 ]; then
+ dpll_log_test_skip_or_fail \
+ "DPLL pin state transitions" \
+ "cannot set pin state to $before$reason"
+ return
+ fi
+ check_err 1 "cannot restore pin state$reason"
+ fi
+ after=$(dpll_pin_get_state "$DPLL_DEV_ID" "$PIN_TUNE_ID")
+ if [ "$after" != "$before" ]; then
+ check_err 1 "pin state not restored, got '$after'"
+ fi
+
+ if [ -n "$ref_pin" ]; then
+ dpll_pin_set_state "$DPLL_DEV_ID" "$ref_pin" connected
+ check_err $? "cannot connect the reference pin $ref_pin back"
+ after=$(dpll_pin_get_state "$DPLL_DEV_ID" "$ref_pin")
+ if [ "$after" != connected ]; then
+ check_err 1 "reference pin $ref_pin is '$after'"
+ fi
+ fi
+
+ log_test "DPLL pin state transitions"
+}
+
+##############################################################################
+
+cleanup()
+{
+ # A second signal must not abort the teardown half way.
+ trap '' INT TERM
+
+ # Run the pending restores of an interrupted test first, the device
+ # they apply to may be one the user provided.
+ defer_scopes_cleanup
+ if [ -n "$DPLL_CREATED_DEVICE" ]; then
+ dpllsim_delete_device "$DPLL_CREATED_DEVICE" ||
+ log_info "cannot delete device $DPLL_CREATED_DEVICE"
+ fi
+ # The device whose creation a signal interrupted is recorded by the
+ # library alone, a double delete of a tracked one is harmless.
+ if [ -n "$DPLLSIM_CREATED_NAME" ]; then
+ dpllsim_delete_device "$DPLLSIM_CREATED_NAME" ||
+ log_info "cannot delete device $DPLLSIM_CREATED_NAME"
+ fi
+ dpll_lib_cleanup
+}
+
+dpll_lib_check_prereqs
+# Another run would find the devices of this one left behind, see the
+# lock in the library.
+dpll_lib_lock
+
+# The module check may mount configfs, so the exit trap that unmounts it
+# again has to be in place before. INT and TERM exit through it as well,
+# so that a timeout imposed by the harness still restores the device.
+trap cleanup EXIT
+trap 'exit 1' INT TERM
+
+if [ -n "${DPLL_DEV_ID:-}" ] || [ -n "${DPLL_DRIVER:-}" ]; then
+ dpll_lib_init
+fi
+if [ -z "${DPLL_DEV_ID:-}" ]; then
+ dpllsim_check_module
+ if ! dpllsim_setup_standard_device; then
+ echo "SKIP: cannot create dpllsim device"
+ exit "$ksft_skip"
+ fi
+ DPLL_DEV_ID=$DPLLSIM_DEV_ID
+ DPLL_CREATED_DEVICE=$DPLLSIM_CREATED_NAME
+ DPLL_MODULE_NAME="dpllsim"
+fi
+
+log_info "testing DPLL device $DPLL_DEV_ID (module $DPLL_MODULE_NAME)"
+
+# PIN_TUNE_ID is the input pin the tests reconfigure. It must not be the
+# reference the device is locked to, so it is the first input pin that
+# is not connected on the device. When the first dumped input pin and
+# another one both qualify the other one is taken, so that the first
+# pin, the one a simulated device locks to once it is selectable, is
+# left alone. Tests that need the pin skip when there is none rather
+# than reconfiguring the reference.
+pick_tune_pin()
+{
+ local pin_id state
+
+ PIN_TUNE_ID=""
+ for pin_id in "${input_pins[@]}"; do
+ state=$(dpll_pin_get_state "$DPLL_DEV_ID" "$pin_id")
+ case "$state" in
+ disconnected|selectable)
+ ;;
+ *)
+ continue
+ ;;
+ esac
+ PIN_TUNE_ID=$pin_id
+ [ "$pin_id" != "${input_pins[0]}" ] && return
+ done
+}
+
+mapfile -t input_pins < <(dpll_pin_find_by_direction "$DPLL_DEV_ID" input)
+pick_tune_pin
+
+tests_run
+
+exit $EXIT_STATUS
diff --git a/tools/testing/selftests/drivers/dpll/dpll_lib.sh b/tools/testing/selftests/drivers/dpll/dpll_lib.sh
index 0db4a7e2e12fcf..63c70a4aaab6e2 100644
--- a/tools/testing/selftests/drivers/dpll/dpll_lib.sh
+++ b/tools/testing/selftests/drivers/dpll/dpll_lib.sh
@@ -5,7 +5,8 @@
# tool and around the dpllsim configfs interface for creating virtual DPLL
# devices. Sourcing this file has no side effects; call
# dpll_lib_check_prereqs and dpll_lib_init explicitly. net/lib.sh must be
-# sourced first, it provides ksft_skip, busywait and log_info.
+# sourced first, it provides ksft_skip, busywait and log_info to the
+# library and defer to the scripts.
#
# Copyright (c) 2026 Red Hat, Inc.
#
@@ -86,6 +87,44 @@ dpll_lib_lock()
fi
}
+# Print the usage block of the given command of the given object from
+# the help of the dpll tool, "dpll pin set" for instance. The help lists
+# one usage per command, each starting with "Usage: dpll <obj> <cmd>" or
+# with "dpll <obj> <cmd>" indented under the first, possibly wrapped
+# over further lines. The block starts at the line that names the
+# command, with a space after the name so that a command whose name
+# merely begins the same way does not match, and ends before the next
+# line that names another command of the object, or at the end of the
+# help when none follows. Return 1 when the help has no usage for the
+# command, in which case nothing is printed.
+__dpll_usage_block()
+{
+ local obj=$1
+ local cmd=$2
+
+ dpll "$obj" help 2>&1 | \
+ awk -v start="[[:space:]]dpll $obj $cmd([[:space:]]|$)" \
+ -v other="[[:space:]]dpll $obj [a-z-]+([[:space:]]|$)" \
+ '$0 ~ start { s = 1; print; next }
+ s && $0 ~ other { exit }
+ s { print }
+ END { exit !s }'
+}
+
+# Return 0 when the dpll tool knows the given "pin set" keyword, such as
+# state or frequency. A test that sets something checks its keyword
+# first and skips on a tool that is too old for it. Only the "pin set"
+# usage is searched, a keyword that only "pin show" knows must not
+# count, and the keyword must stand on its own between blanks or
+# brackets, a hyphenated compound that contains it does not count.
+dpll_pin_set_supports()
+{
+ local usage
+
+ usage=$(__dpll_usage_block pin set) || return 1
+ grep -qE "(^|[[:space:][])$1([[:space:]]|$)" <<< "$usage"
+}
+
# Detect the DPLL device to test. DPLL_DEV_ID may be set by the user to
# force a specific device. Otherwise DPLL_DRIVER names the driver whose
# first device is taken, by the module name the device reports, dpllsim
@@ -183,6 +222,11 @@ dpll_device_get_module()
__dpll_device_field "$1" '.["module-name"]'
}
+dpll_device_get_mode()
+{
+ __dpll_device_field "$1" '.mode'
+}
+
# Look up the netlink device id by the clock-id chosen at create time,
# restricted to the devices of the given module when one is named. The
# clock-id is the only reliable handle on a device the test created, as
@@ -337,6 +381,41 @@ dpll_pin_get_state()
__dpll_pin_dev_field "$1" "$2" '.state'
}
+# Return 0 when the pin advertises the given capability, named as
+# iproute2 prints it (state-can-change, priority-can-change, ...).
+dpll_pin_has_capability()
+{
+ local pin_id=$1
+ local name=$2
+ local out
+
+ out=$(dpll pin show id "$pin_id" -j 2>/dev/null) || return 1
+ jq -e --arg name "$name" \
+ '(.pin[0]? // .) | .capabilities | index($name) != null' \
+ <<< "$out" &> /dev/null
+}
+
+##############################################################################
+# Pin setters
+#
+# Every setter returns the status of the tool and keeps what the tool
+# printed on stderr in DPLL_LAST_ERR, the "Error: ..." line that carries
+# the extack message of the kernel, so that a caller can tell a refusal
+# it may skip on from a failure.
+
+DPLL_LAST_ERR=""
+
+# $3: connected / disconnected / selectable
+dpll_pin_set_state()
+{
+ local dev_id=$1
+ local pin_id=$2
+ local state=$3
+
+ DPLL_LAST_ERR=$(dpll pin set id "$pin_id" parent-device "$dev_id" \
+ state "$state" 2>&1 >/dev/null)
+}
+
##############################################################################
# dpllsim configfs interface
#
diff --git a/tools/testing/selftests/drivers/dpll/dpll_stress.sh b/tools/testing/selftests/drivers/dpll/dpll_stress.sh
index b21325d8368fb2..526cb113807f72 100755
--- a/tools/testing/selftests/drivers/dpll/dpll_stress.sh
+++ b/tools/testing/selftests/drivers/dpll/dpll_stress.sh
@@ -15,6 +15,7 @@ source "$SCRIPTDIR/dpll_lib.sh"
ALL_TESTS="
test_rapid_create_delete
+ test_pin_state_flooding
test_deploy_cycle
"
@@ -45,6 +46,7 @@ cleanup()
# A second signal must not abort the teardown half way.
trap '' INT TERM
+ defer_scopes_cleanup
for dev in "${CREATED_DEVICES[@]}"; do
dpllsim_delete_device "$dev" ||
log_info "cannot delete device $dev"
@@ -133,6 +135,62 @@ test_rapid_create_delete()
log_test "DPLL rapid create/delete ($STRESS_ITERATIONS iterations)"
}
+test_pin_state_flooding()
+{
+ local clock_id name dev_id pin_id i last state fails=0
+ # The states of automatic mode, the mode the simulator runs in
+ local states=(disconnected selectable)
+
+ RET=0
+
+ if ! dpll_pin_set_supports state; then
+ log_test_skip "DPLL pin state flooding" \
+ "dpll tool lacks pin set state"
+ return
+ fi
+
+ dpllsim_alloc_clock_id; clock_id=$DPLLSIM_CLOCK_ID
+ if ! create_stress_device "$clock_id" 10; then
+ check_err 1 "cannot create device"
+ log_test "DPLL pin state flooding"
+ return
+ fi
+ name=$DPLLSIM_CREATED_NAME
+ track_device "$name"
+
+ if ! dev_id=$(dpll_device_wait_by_clock_id "$clock_id" dpllsim); then
+ check_err 1 "device not visible on netlink"
+ else
+ pin_id=$(dpll_pin_find_by_direction "$dev_id" input | head -1)
+ if [ -z "$pin_id" ]; then
+ check_err 1 "no input pin on stress device"
+ else
+ for i in $(seq 1 50); do
+ last=${states[i % 2]}
+ dpll_pin_set_state "$dev_id" "$pin_id" \
+ "$last" ||
+ fails=$((fails + 1))
+ done
+ if [ "$fails" -gt 0 ]; then
+ check_err 1 "$fails of 50 state changes failed"
+ fi
+ state=$(dpll_pin_get_state "$dev_id" "$pin_id")
+ if [ "$state" != "$last" ]; then
+ check_err 1 \
+ "state expected '$last', got '$state'"
+ fi
+ fi
+ fi
+
+ if dpllsim_delete_device "$name"; then
+ untrack_device "$name"
+ else
+ check_err 1 "cannot delete device $name"
+ fi
+
+ log_test "DPLL pin state flooding"
+}
+
test_deploy_cycle()
{
local clock_id name i
--
2.56.0
^ permalink raw reply related [flat|nested] 31+ messages in thread* Re: [PATCH net-next v3 06/14] selftests: dpll: add pin state tests
2026-11-09 9:35 ` [PATCH net-next v3 06/14] selftests: dpll: add pin state tests Petr Oros
@ 2026-11-09 14:00 ` Ivan Vecera
2026-11-11 9:37 ` netdev-bot+sashiko
1 sibling, 0 replies; 31+ messages in thread
From: Ivan Vecera @ 2026-11-09 14:00 UTC (permalink / raw)
To: Petr Oros, netdev
Cc: Jonathan Corbet, Shuah Khan, Shuah Khan, Randy Dunlap,
Jani Nikula, Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni, linux-doc, linux-kselftest
On 11/9/26 10:35, Petr Oros wrote:
> Add dpll_features.sh, a script that exercises individual device and pin
> callbacks with get/set roundtrips over netlink. It creates a dpllsim
> device with the standard pin layout by default or runs against the
> device named by DPLL_DEV_ID or the first device of the driver named by
> DPLL_DRIVER, and picks the pin to reconfigure up front, the first input
> pin that is not connected on the device, so that no test touches the
> reference the device is locked to.
>
> The first test moves that pin through the states of the device mode,
> as Documentation/driver-api/dpll.rst defines them, reads the state back
> after each step and restores the original state at the end. In
> automatic mode these are disconnected and selectable, and on the
> device the script created the test then checks that connected is
> refused. Hardware drivers are not held to that, ice accepts connected
> in automatic mode, and a pin with the state-connected-override
> capability would take over the reference. In manual mode connected and
> disconnected
> are the states, connecting the pin makes it the reference, so the test
> finds the pin connected before, connects it back at the end and checks
> that it is the reference again.
>
> A driver may refuse a state the capability promises, for a constraint
> the interface does not tell. That is a skip on a device provided by
> the user, with the reason the tool reported, and a failure on the
> device the script created, whose simulator accepts both states of its
> mode. The restores are deferred right after the original states are
> read, and the cleanup handler of every script that defers runs the
> pending restores before it removes the devices the script created, so
> that an interrupted run puts the device back as well.
>
> The stress script gains a test that floods one input pin of a fresh
> device with changes between the states of automatic mode. The library
> gains the state setter both use, which keeps the extack line of the
> tool in DPLL_LAST_ERR so that a test can tell a refusal from a failure,
> the device mode getter, and a check whether the pin set usage of the
> tool knows a keyword, so that a test skips on a tool too old for it
> rather than every script requiring a newer tool.
>
> Signed-off-by: Petr Oros<poros@redhat.com>
> ---
> v3:
> - follow the mode of the device under test: disconnected and
> selectable in automatic mode, where the simulator is checked to
> refuse connected, connected and disconnected in manual mode, where
> the pin connected before is connected back at the end
> - cycle the state flooding test through the states of automatic mode
> - add the device mode getter to the library
> - honor DPLL_DRIVER like dpll_basic.sh does
> - follow the move of the selftests to
> tools/testing/selftests/drivers/dpll
> - the Reviewed-by and Acked-by of v2 are not carried over, the
> state test changed
Reviewed-by: Ivan Vecera <ivecera@redhat.com>
^ permalink raw reply [flat|nested] 31+ messages in thread
* Re: [PATCH net-next v3 06/14] selftests: dpll: add pin state tests
2026-11-09 9:35 ` [PATCH net-next v3 06/14] selftests: dpll: add pin state tests Petr Oros
2026-11-09 14:00 ` Ivan Vecera
@ 2026-11-11 9:37 ` netdev-bot+sashiko
1 sibling, 0 replies; 31+ messages in thread
From: netdev-bot+sashiko @ 2026-11-11 9:37 UTC (permalink / raw)
To: poros
Cc: netdev, ivecera, jiri, corbet, skhan, shuah, rdunlap, jani.nikula,
vadim.fedorenko, arkadiusz.kubalewski, jiri, andrew+netdev, davem,
edumazet, kuba, pabeni, linux-doc, linux-kselftest
Thank you for your contribution! Sashiko AI review found 7 potential
issue(s) to consider.
Critical: 0 · High: 0 · Medium: 4 · Low: 3
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 manual mode, test_pin_state_transitions()
(tools/testing/selftests/drivers/dpll/dpll_features.sh) leaves the…
- [Medium] In automatic mode, test_pin_state_transitions() runs the pin
through states="disconnected selectable".
- [Medium] When a state set inside the loop of
test_pin_state_transitions() (lines 116-129) is refused, the test…
- [Medium] dpll_features.sh assumes that a pin whose state is not
'connected' cannot be the reference the device is locked to.
- [Low] The header of dpll_features.sh (lines 6-9) says that when
DPLL_DEV_ID or DPLL_DRIVER is set, 'the simulator-only tests then skip…
- [Low] The commit message says the script picks 'the first input pin
that is not connected on the device'.
- [Low] In test_pin_state_flooding() (dpll_stress.sh), the only readback
check cannot catch a simulator whose state setter does nothing.
--
selftests: dpll: add pin state tests
This adds dpll_features.sh. It moves a spare input pin through the pin
states of the device mode and checks each readback, either on a dpllsim
device it creates or on a device chosen through DPLL_DEV_ID or
DPLL_DRIVER. The stress script gains a pin state flooding test, and
dpll_lib.sh gains a state setter, a device mode getter and a check for
the pin set keywords the dpll tool supports.
> diff --git a/tools/testing/selftests/drivers/dpll/dpll_features.sh b/tools/testing/selftests/drivers/dpll/dpll_features.sh
> new file mode 100755
> index 0000000000000..874007411bc5e
> --- /dev/null
> +++ b/tools/testing/selftests/drivers/dpll/dpll_features.sh
> @@ -0,0 +1,256 @@
> +#!/bin/bash
> +# SPDX-License-Identifier: GPL-2.0
> +#
> +# DPLL device and pin feature tests. Exercises individual dpll_device_ops
> +# and dpll_pin_ops callbacks, verifying get/set roundtrips through the
> +# netlink interface. By default a dpllsim device with a known pin layout
> +# is created; set DPLL_DEV_ID or DPLL_DRIVER to run against an existing
> +# device instead, the simulator-only tests then skip unless that device
> +# is a dpllsim one.
[Severity: Low]
Is this description accurate? Nothing in the script seems to skip based
on whether the device is a dpllsim one.
The only simulator-specific check in test_pin_state_transitions()
depends on DPLL_CREATED_DEVICE, not on the module name:
if [ "$mode" = automatic ] && [ -n "$DPLL_CREATED_DEVICE" ]; then
So a dpllsim device passed through DPLL_DEV_ID silently loses that
check, and no skip line is logged.
Also, dpll_lib_init() clears DPLL_DEV_ID when DPLL_DRIVER=dpllsim:
elif [ "${DPLL_DRIVER:-}" = "dpllsim" ]; then
DPLL_DEV_ID=""
So that setting always creates a new simulator device instead of running
against an existing one. The header is still the same at the end of the
series.
[ ... ]
> + before=$(dpll_pin_get_state "$DPLL_DEV_ID" "$PIN_TUNE_ID")
> + if [ -z "$before" ]; then
> + dpll_log_test_skip_or_fail "DPLL pin state transitions" \
> + "state not reported"
> + return
> + fi
> + # Restore the original state even when the test aborts half way.
> + defer dpll_pin_set_state "$DPLL_DEV_ID" "$PIN_TUNE_ID" "$before"
> +
> + # In manual mode connecting the pin makes it the reference and
> + # takes that over from the pin connected so far, so that one is
> + # connected back at the end, after the restore above has
> + # disconnected the test pin again. In automatic mode the pin is not
> + # connected, see how PIN_TUNE_ID is picked, and the test never
> + # connects it, the device keeps the reference it locked to.
> + ref_pin=""
> + if [ "$mode" = manual ]; then
> + ref_pin=$(find_connected_pin)
> + if [ -n "$ref_pin" ]; then
> + defer dpll_pin_set_state "$DPLL_DEV_ID" "$ref_pin" \
> + connected
> + fi
> + fi
[Severity: Medium]
Do these two deferred commands run in the order the comment describes?
defer_scope_pop() in net/lib/sh/defer.sh runs deferred jobs in reverse
order, last in first out:
for ((defer_ix=$(__defer__ndefers d); defer_ix-->0; )); do
__defer__run d $defer_ix
done
tests_run() wraps every test in in_defer_scope(), so both commands run
after every test, including one that passed. In manual mode the
sequence would be:
test body restores PIN_TUNE_ID to $before and reconnects $ref_pin
test body finds $ref_pin connected, log_test reports a pass
defer_scope_pop() -> dpll_pin_set_state ... "$ref_pin" connected
defer_scope_pop() -> dpll_pin_set_state ... "$PIN_TUNE_ID" disconnected
On zl3073x, ZL_DPLL_MODE_REFSEL_MODE_REFLOCK is reported as manual
mode. In that mode, zl3073x_dpll_input_pin_state_on_dpll_set() handles a
disconnect request on any input pin by leaving the reference:
case ZL_DPLL_MODE_REFSEL_MODE_REFLOCK:
...
} else if (state == DPLL_PIN_STATE_DISCONNECTED) {
/* Choose new mode based on lock status */
switch (zldpll->lock_status) {
...
mode = ZL_DPLL_MODE_REFSEL_MODE_FREERUN;
The dpll core does not skip a set that matches the current state.
Could this last deferred disconnect of the test pin leave the device in
holdover or freerun after the test has already reported a pass?
That would also contradict the commit message:
"connects it back at the end and checks that it is the reference
again"
An interrupted run has the same ordering problem. The reconnect of
$ref_pin runs while PIN_TUNE_ID may still be the selected reference,
and PIN_TUNE_ID is disconnected afterwards.
Would registering the $ref_pin defer before the PIN_TUNE_ID one give
the intended order? This code is still the same at the end of the
series.
[ ... ]
> + for state in $states; do
> + if ! dpll_pin_set_state "$DPLL_DEV_ID" "$PIN_TUNE_ID" \
> + "$state"; then
> + reason=${DPLL_LAST_ERR%%$'\n'*}
> + dpll_log_test_skip_or_fail \
> + "DPLL pin state transitions" \
> + "cannot set pin state to $state${reason:+, $reason}"
> + return
> + fi
[Severity: Medium]
Can this skip hide a failure that an earlier iteration recorded?
Suppose the first state is set but reads back differently, so check_err
records a failure. If the second set is then refused on a user-provided
device, dpll_log_test_skip_or_fail() calls log_test_skip(), which does:
RET=$ksft_skip retmsg= log_test "$@"
The test then reports a skip and the earlier failure message is lost.
The restore path further down guards against this with
[ "$RET" -eq 0 ], and the comment above the loop says "a skip would
hide the earlier one". The loop path has no such check.
> + after=$(dpll_pin_get_state "$DPLL_DEV_ID" "$PIN_TUNE_ID")
> + if [ "$after" != "$state" ]; then
> + check_err 1 "pin not $state, state '$after'"
> + fi
> + done
[Severity: Medium]
In automatic mode, can the selectable step change the reference the
device is locked to?
When PIN_TUNE_ID starts out disconnected, setting it selectable puts it
back into hardware selection with its configured priority. ice enables
the input in ice_dpll_input_state_set():
bool enable = state == DPLL_PIN_STATE_SELECTABLE;
zl3073x restores pin->prio in the hardware priority table.
If the pin outranks the current reference and has a valid signal, the
DPLL would switch to it. The restore to disconnected would then switch
it back.
The commit message says the pin is picked "so that no test touches the
reference the device is locked to". However, pick_tune_pin() only looks
at the state, not at priority or operstate.
On ice, ice_dpll_pin_state_update() reports the active input as
connected:
pin->state[pf->dplls.eec.dpll_idx] =
pin->pin == pf->dplls.eec.active_input ?
DPLL_PIN_STATE_CONNECTED :
DPLL_PIN_STATE_SELECTABLE;
Would this readback then fail with "pin not selectable, state
'connected'", depending on how the board's priorities are set up?
[ ... ]
> +# PIN_TUNE_ID is the input pin the tests reconfigure. It must not be the
> +# reference the device is locked to, so it is the first input pin that
> +# is not connected on the device. When the first dumped input pin and
> +# another one both qualify the other one is taken, so that the first
> +# pin, the one a simulated device locks to once it is selectable, is
> +# left alone. Tests that need the pin skip when there is none rather
> +# than reconfiguring the reference.
> +pick_tune_pin()
> +{
> + local pin_id state
> +
> + PIN_TUNE_ID=""
> + for pin_id in "${input_pins[@]}"; do
> + state=$(dpll_pin_get_state "$DPLL_DEV_ID" "$pin_id")
> + case "$state" in
> + disconnected|selectable)
> + ;;
> + *)
> + continue
> + ;;
> + esac
[Severity: Medium]
On zl3073x, could this pick the reference the device is locked to?
In automatic mode, zl3073x_dpll_input_pin_state_on_dpll_get() never
reports connected:
case ZL_DPLL_MODE_REFSEL_MODE_AUTO:
if (zl3073x_chan_ref_is_selectable(chan, ref))
*state = DPLL_PIN_STATE_SELECTABLE;
else
*state = DPLL_PIN_STATE_DISCONNECTED;
So the active reference reads as selectable and passes this filter.
pick_tune_pin() prefers a qualifying pin other than input_pins[0], so it
would pick the locked reference whenever that reference is not the first
dumped pin.
test_pin_state_transitions() then sets the pin disconnected. The
ZL_DPLL_MODE_REFSEL_MODE_AUTO branch of
zl3073x_dpll_input_pin_state_on_dpll_set() handles that like this:
/* Set pin priority to none in HW */
zl3073x_chan_ref_prio_set(&chan, ref,
ZL_DPLL_REF_PRIO_NONE);
That drops the active reference.
Devices that do report the active input as connected have a related
gap. The state is checked only once here, before any test runs.
test_pin_state_transitions() reads $before again but does not reject
connected in automatic mode. A hardware reselection between
pick_tune_pin() and the test would therefore go unnoticed.
Would checking the pin operstate (active or standby), and checking again
right before the first set, avoid touching the locked reference? This
code is still the same at the end of the series.
> + PIN_TUNE_ID=$pin_id
> + [ "$pin_id" != "${input_pins[0]}" ] && return
> + done
> +}
[Severity: Low]
This isn't a bug, but the commit message and the comment above
pick_tune_pin() don't match this code.
The commit message says the script picks:
"the first input pin that is not connected on the device"
This early return, however, prefers any qualifying pin other than
input_pins[0].
The comment explains that choice with "the one a simulated device locks
to once it is selectable". dpllsim_pin_state_set() only stores the
state, though:
sim_pin->state = state;
return 0;
Nothing in dpllsim selects a reference or changes the lock status from
its initial unlocked value.
Could the comment and the commit message be updated?
[ ... ]
> diff --git a/tools/testing/selftests/drivers/dpll/dpll_stress.sh b/tools/testing/selftests/drivers/dpll/dpll_stress.sh
> index b21325d8368fb..526cb113807f7 100755
> --- a/tools/testing/selftests/drivers/dpll/dpll_stress.sh
> +++ b/tools/testing/selftests/drivers/dpll/dpll_stress.sh
[ ... ]
> @@ -133,6 +135,62 @@ test_rapid_create_delete()
> log_test "DPLL rapid create/delete ($STRESS_ITERATIONS iterations)"
> }
>
> +test_pin_state_flooding()
> +{
> + local clock_id name dev_id pin_id i last state fails=0
> + # The states of automatic mode, the mode the simulator runs in
> + local states=(disconnected selectable)
[ ... ]
> + for i in $(seq 1 50); do
> + last=${states[i % 2]}
> + dpll_pin_set_state "$dev_id" "$pin_id" \
> + "$last" ||
> + fails=$((fails + 1))
> + done
> + if [ "$fails" -gt 0 ]; then
> + check_err 1 "$fails of 50 state changes failed"
> + fi
> + state=$(dpll_pin_get_state "$dev_id" "$pin_id")
> + if [ "$state" != "$last" ]; then
> + check_err 1 \
> + "state expected '$last', got '$state'"
> + fi
[Severity: Low]
Can this readback catch a state setter that does nothing? With i running
from 1 to 50, the last request is states[50 % 2], which is disconnected.
dpllsim_pin_reset() initialises every pin to that same state:
pin->state = DPLL_PIN_STATE_DISCONNECTED;
On a freshly created device, a state_on_dpll_set that returned 0 without
storing anything would still pass. The fails counter only catches
non-zero returns. The test is still the same at the end of the series.
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261109093523.931201-1-poros%40redhat.com
^ permalink raw reply [flat|nested] 31+ messages in thread
* [PATCH net-next v3 07/14] dpll: dpllsim: Add support to get/set priority on input pins
2026-11-09 9:35 [PATCH net-next v3 00/14] dpll: add a simulated DPLL device driver Petr Oros
` (5 preceding siblings ...)
2026-11-09 9:35 ` [PATCH net-next v3 06/14] selftests: dpll: add pin state tests Petr Oros
@ 2026-11-09 9:35 ` Petr Oros
2026-11-11 9:37 ` netdev-bot+sashiko
2026-11-09 9:35 ` [PATCH net-next v3 08/14] selftests: dpll: add pin priority test Petr Oros
` (6 subsequent siblings)
13 siblings, 1 reply; 31+ messages in thread
From: Petr Oros @ 2026-11-09 9:35 UTC (permalink / raw)
To: netdev
Cc: Petr Oros, Ivan Vecera, Jiri Pirko, Jonathan Corbet, Shuah Khan,
Shuah Khan, Randy Dunlap, Jani Nikula, Vadim Fedorenko,
Arkadiusz Kubalewski, Jiri Pirko, Andrew Lunn, David S. Miller,
Eric Dumazet, Jakub Kicinski, Paolo Abeni, linux-doc,
linux-kselftest
Add support for getting and setting the priority of input pins.
Implement the prio_get and prio_set callbacks and advertise the
priority-can-change capability for input pins, which includes the MUX
type pins since those are always inputs.
The initial priority of a pin is configured through a new prio
attribute of the pin directory, which takes a value between 0 and 255
and defaults to 128, so that a test can lay out the reference order
before the device goes live. Like the other pin attributes it is
writable only while the device is not live. Priority changes made over
netlink are stored and reported back, they will drive the automatic
reference selection once the simulation loop exists. The setter
rejects a value above 255 with EINVAL and an extack message. A priority
is meaningful for input pins only, so live fails with EINVAL when an
output pin was given a priority other than the default, next to the
frequency validation.
Reviewed-by: Ivan Vecera <ivecera@redhat.com>
Acked-by: Jiri Pirko <jiri@nvidia.com>
Signed-off-by: Petr Oros <poros@redhat.com>
---
Documentation/driver-api/dpllsim.rst | 5 ++++
drivers/dpll/dpllsim/core.c | 31 +++++++++++++++++++
drivers/dpll/dpllsim/dpll.c | 45 ++++++++++++++++++++++++++--
drivers/dpll/dpllsim/dpllsim.h | 10 ++++++-
4 files changed, 88 insertions(+), 3 deletions(-)
diff --git a/Documentation/driver-api/dpllsim.rst b/Documentation/driver-api/dpllsim.rst
index e7c5ee93392c21..5109d67325f140 100644
--- a/Documentation/driver-api/dpllsim.rst
+++ b/Documentation/driver-api/dpllsim.rst
@@ -36,6 +36,9 @@ Pin directory ``/sys/kernel/config/dpllsim/<dev>/pins/<pin>/``:
(default 0, which is rejected)
``direction`` ``input`` or ``output`` (default ``input``), a ``mux`` pin
must be an input
+``prio`` Priority 0-255 used for automatic reference selection,
+ input pins only, output pins must keep the default
+ (default 128)
============= ===============================================================
The pin directory name becomes the board label of the pin. Pins are
@@ -53,8 +56,10 @@ output, then make it live::
mkdir pins/gnss0
echo gnss > pins/gnss0/type
echo 1 > pins/gnss0/frequency
+ echo 0 > pins/gnss0/prio
mkdir pins/ext0
echo 10000000 > pins/ext0/frequency
+ echo 1 > pins/ext0/prio
mkdir pins/out0
echo 10000000 > pins/out0/frequency
echo output > pins/out0/direction
diff --git a/drivers/dpll/dpllsim/core.c b/drivers/dpll/dpllsim/core.c
index 97b3cf48772e7f..ee728a7f0c21bc 100644
--- a/drivers/dpll/dpllsim/core.c
+++ b/drivers/dpll/dpllsim/core.c
@@ -132,10 +132,40 @@ static ssize_t dpllsim_pin_frequency_store(struct config_item *item,
}
CONFIGFS_ATTR(dpllsim_pin_, frequency);
+static ssize_t dpllsim_pin_prio_show(struct config_item *item, char *page)
+{
+ struct dpllsim_pin *pin = to_dpllsim_pin(item);
+
+ guard(mutex)(&pin->dev->lock);
+ return sysfs_emit(page, "%u\n", pin->prio);
+}
+
+static ssize_t dpllsim_pin_prio_store(struct config_item *item,
+ const char *page, size_t count)
+{
+ struct dpllsim_pin *pin = to_dpllsim_pin(item);
+ u32 val;
+ int err;
+
+ err = kstrtou32(page, 10, &val);
+ if (err)
+ return err;
+ if (val > DPLLSIM_PIN_PRIO_MAX)
+ return -EINVAL;
+
+ guard(mutex)(&pin->dev->lock);
+ if (dpllsim_dev_frozen(pin->dev))
+ return -EBUSY;
+ pin->prio = val;
+ return count;
+}
+CONFIGFS_ATTR(dpllsim_pin_, prio);
+
static struct configfs_attribute *dpllsim_pin_attrs[] = {
&dpllsim_pin_attr_type,
&dpllsim_pin_attr_direction,
&dpllsim_pin_attr_frequency,
+ &dpllsim_pin_attr_prio,
NULL,
};
@@ -178,6 +208,7 @@ static struct config_item *dpllsim_pins_make_item(struct config_group *group,
pin->dev = sim;
pin->type = DPLL_PIN_TYPE_EXT;
pin->direction = DPLL_PIN_DIRECTION_INPUT;
+ pin->prio = DPLLSIM_DEFAULT_PIN_PRIO;
list_add_tail(&pin->list, &sim->pin_list);
return &pin->item;
diff --git a/drivers/dpll/dpllsim/dpll.c b/drivers/dpll/dpllsim/dpll.c
index 794020d0c112e8..5e360596097616 100644
--- a/drivers/dpll/dpllsim/dpll.c
+++ b/drivers/dpll/dpllsim/dpll.c
@@ -139,11 +139,43 @@ static int dpllsim_pin_state_set(const struct dpll_pin *pin, void *pin_priv,
return 0;
}
+static int dpllsim_pin_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 dpllsim_pin *sim_pin = pin_priv;
+
+ guard(mutex)(&sim_pin->dev->lock);
+ *prio = sim_pin->prio;
+ return 0;
+}
+
+static int dpllsim_pin_prio_set(const struct dpll_pin *pin, void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv, u32 prio,
+ struct netlink_ext_ack *extack)
+{
+ struct dpllsim_pin *sim_pin = pin_priv;
+
+ if (prio > DPLLSIM_PIN_PRIO_MAX) {
+ NL_SET_ERR_MSG_FMT(extack, "Pin priority must be in range 0-%u",
+ DPLLSIM_PIN_PRIO_MAX);
+ return -EINVAL;
+ }
+
+ guard(mutex)(&sim_pin->dev->lock);
+ sim_pin->prio = prio;
+ return 0;
+}
+
static const struct dpll_pin_ops dpllsim_input_pin_ops = {
.frequency_get = dpllsim_pin_frequency_get,
.direction_get = dpllsim_pin_direction_get,
.state_on_dpll_get = dpllsim_pin_state_get,
.state_on_dpll_set = dpllsim_pin_state_set,
+ .prio_get = dpllsim_pin_prio_get,
+ .prio_set = dpllsim_pin_prio_set,
};
static const struct dpll_pin_ops dpllsim_output_pin_ops = {
@@ -188,7 +220,8 @@ static void dpllsim_pin_props(struct dpllsim_pin *pin,
/* Capabilities follow the ops the pin is served by */
if (dpllsim_pin_select_ops(pin) == &dpllsim_input_pin_ops)
- prop->capabilities = DPLL_PIN_CAPABILITIES_STATE_CAN_CHANGE;
+ prop->capabilities = DPLL_PIN_CAPABILITIES_STATE_CAN_CHANGE |
+ DPLL_PIN_CAPABILITIES_PRIORITY_CAN_CHANGE;
else
prop->capabilities = 0;
}
@@ -214,6 +247,13 @@ static bool dpllsim_pin_direction_valid(const struct dpllsim_pin *pin)
pin->direction == DPLL_PIN_DIRECTION_INPUT;
}
+/* Priority is meaningful for input pins only */
+static bool dpllsim_pin_prio_valid(const struct dpllsim_pin *pin)
+{
+ return pin->direction == DPLL_PIN_DIRECTION_INPUT ||
+ pin->prio == DPLLSIM_DEFAULT_PIN_PRIO;
+}
+
static void dpllsim_pin_reset(struct dpllsim_pin *pin)
{
pin->ops = dpllsim_pin_select_ops(pin);
@@ -228,7 +268,8 @@ static int dpllsim_order_pins(struct dpllsim_dev *sim)
list_for_each_entry(pin, &sim->pin_list, list) {
if (!dpllsim_pin_frequency_valid(pin) ||
- !dpllsim_pin_direction_valid(pin))
+ !dpllsim_pin_direction_valid(pin) ||
+ !dpllsim_pin_prio_valid(pin))
return -EINVAL;
count++;
}
diff --git a/drivers/dpll/dpllsim/dpllsim.h b/drivers/dpll/dpllsim/dpllsim.h
index 73b846b66f79c7..f9fced337dc881 100644
--- a/drivers/dpll/dpllsim/dpllsim.h
+++ b/drivers/dpll/dpllsim/dpllsim.h
@@ -12,6 +12,9 @@
#include <linux/list.h>
#include <linux/mutex.h>
+#define DPLLSIM_DEFAULT_PIN_PRIO 128
+#define DPLLSIM_PIN_PRIO_MAX 255
+
#define DPLLSIM_MAX_PINS 128
struct dpllsim_dev;
@@ -21,10 +24,15 @@ struct dpllsim_pin {
struct list_head list;
struct dpllsim_dev *dev;
- /* Configuration, writable only until dpllsim_dev_frozen() */
+ /*
+ * Configuration, set through configfs only until
+ * dpllsim_dev_frozen(). The netlink prio setter changes prio at
+ * runtime under dev->lock.
+ */
enum dpll_pin_type type;
enum dpll_pin_direction direction;
u64 frequency;
+ u32 prio;
/* Resolved at live=1 */
int idx;
--
2.56.0
^ permalink raw reply related [flat|nested] 31+ messages in thread* Re: [PATCH net-next v3 07/14] dpll: dpllsim: Add support to get/set priority on input pins
2026-11-09 9:35 ` [PATCH net-next v3 07/14] dpll: dpllsim: Add support to get/set priority on input pins Petr Oros
@ 2026-11-11 9:37 ` netdev-bot+sashiko
0 siblings, 0 replies; 31+ messages in thread
From: netdev-bot+sashiko @ 2026-11-11 9:37 UTC (permalink / raw)
To: poros
Cc: netdev, ivecera, jiri, corbet, skhan, shuah, rdunlap, jani.nikula,
vadim.fedorenko, arkadiusz.kubalewski, jiri, andrew+netdev, davem,
edumazet, kuba, pabeni, linux-doc, linux-kselftest
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] One field, `pin->prio`, holds both the configfs setting and the
live value.
- [Low] Documentation/driver-api/dpllsim.rst (line 39) describes the new
`prio` attribute as 'Priority 0-255 used for automatic reference…
--
dpll: dpllsim: Add support to get/set priority on input pins
This commit adds prio_get and prio_set callbacks for dpllsim input pins
and advertises the priority-can-change capability. It also adds a prio
configfs attribute that sets the initial priority before the device goes
live. Going live is rejected when an output pin has a non-default
priority.
> diff --git a/Documentation/driver-api/dpllsim.rst b/Documentation/driver-api/dpllsim.rst
> index e7c5ee93392c2..5109d67325f14 100644
> --- a/Documentation/driver-api/dpllsim.rst
> +++ b/Documentation/driver-api/dpllsim.rst
> @@ -36,6 +36,9 @@ Pin directory ``/sys/kernel/config/dpllsim/<dev>/pins/<pin>/``:
> (default 0, which is rejected)
> ``direction`` ``input`` or ``output`` (default ``input``), a ``mux`` pin
> must be an input
> +``prio`` Priority 0-255 used for automatic reference selection,
> + input pins only, output pins must keep the default
> + (default 128)
[Severity: Low]
Is "used for automatic reference selection" accurate for dpllsim at this
point?
As far as I can tell, only dpllsim_pin_prio_get(), dpllsim_pin_prio_valid()
and dpllsim_pin_prio_show() read pin->prio. The device ops have no
selection logic:
static const struct dpll_device_ops dpllsim_dpll_ops = {
.mode_get = dpllsim_mode_get,
.lock_status_get = dpllsim_lock_status_get,
};
sim->lock_status stays at the DPLL_LOCK_STATUS_UNLOCKED value set in
dpllsim_make_group(). Nothing changes a pin's state based on its priority.
The example further down sets gnss0 prio 0 and ext0 prio 1 as if that sets
up a reference order. The selftest library comment in
dpllsim_setup_standard_device() ("gnss0: ... prio 0 (active reference)")
gives the same impression.
The commit message says priorities "will drive the automatic reference
selection once the simulation loop exists". I couldn't find a later patch
in the series that adds that loop.
Could the documentation say that the priority is currently only stored and
reported back?
[ ... ]
> diff --git a/drivers/dpll/dpllsim/dpll.c b/drivers/dpll/dpllsim/dpll.c
> index 794020d0c112e..5e36059609761 100644
> --- a/drivers/dpll/dpllsim/dpll.c
> +++ b/drivers/dpll/dpllsim/dpll.c
[ ... ]
> +static int dpllsim_pin_prio_set(const struct dpll_pin *pin, void *pin_priv,
> + const struct dpll_device *dpll,
> + void *dpll_priv, u32 prio,
> + struct netlink_ext_ack *extack)
> +{
> + struct dpllsim_pin *sim_pin = pin_priv;
> +
> + if (prio > DPLLSIM_PIN_PRIO_MAX) {
> + NL_SET_ERR_MSG_FMT(extack, "Pin priority must be in range 0-%u",
> + DPLLSIM_PIN_PRIO_MAX);
> + return -EINVAL;
> + }
> +
> + guard(mutex)(&sim_pin->dev->lock);
> + sim_pin->prio = prio;
[Severity: Low]
Should the netlink setter write to the same field that holds the
priority configured through configfs?
dpllsim_pins_make_item() and dpllsim_pin_prio_store() set pin->prio, and
this function writes it at runtime. On each live=1, dpllsim_order_pins()
calls dpllsim_pin_reset(), which resets ops and state but not prio:
static void dpllsim_pin_reset(struct dpllsim_pin *pin)
{
pin->ops = dpllsim_pin_select_ops(pin);
pin->state = DPLL_PIN_STATE_DISCONNECTED;
}
dpllsim_dev_undeploy() doesn't restore the configured value either.
Consider this sequence:
echo 1 > pins/ext0/prio
echo 1 > live
dpll pin set ... prio 200
echo 0 > live
echo 1 > live
The second deployment starts with prio 200. Reading pins/ext0/prio through
dpllsim_pin_prio_show() also returns 200, which was never written through
configfs.
There is a second case. Take an input pin whose prio was changed over
netlink to something other than 128. Then write 0 to live, write output
to direction, and write 1 to live. The live write fails with EINVAL from
dpllsim_order_pins(), because of a priority that was only set at runtime:
static bool dpllsim_pin_prio_valid(const struct dpllsim_pin *pin)
{
return pin->direction == DPLL_PIN_DIRECTION_INPUT ||
pin->prio == DPLLSIM_DEFAULT_PIN_PRIO;
}
The commit message describes the attribute as setting the initial priority
"so that a test can lay out the reference order before the device goes
live". Neither the commit message nor dpllsim.rst says that netlink
changes carry into later deployments and replace that layout.
The later commit "dpll: dpllsim: Add support to set frequency on pins"
takes the same shared-field approach for frequency. No later patch in the
series separates the configured and runtime prio or resets it.
Would it make sense to keep the configured priority separate and copy it
into the runtime value in dpllsim_pin_reset()?
[ ... ]
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261109093523.931201-1-poros%40redhat.com
^ permalink raw reply [flat|nested] 31+ messages in thread
* [PATCH net-next v3 08/14] selftests: dpll: add pin priority test
2026-11-09 9:35 [PATCH net-next v3 00/14] dpll: add a simulated DPLL device driver Petr Oros
` (6 preceding siblings ...)
2026-11-09 9:35 ` [PATCH net-next v3 07/14] dpll: dpllsim: Add support to get/set priority on input pins Petr Oros
@ 2026-11-09 9:35 ` Petr Oros
2026-11-11 9:37 ` netdev-bot+sashiko
2026-11-09 9:35 ` [PATCH net-next v3 09/14] dpll: dpllsim: Add support to set frequency on pins Petr Oros
` (5 subsequent siblings)
13 siblings, 1 reply; 31+ messages in thread
From: Petr Oros @ 2026-11-09 9:35 UTC (permalink / raw)
To: netdev
Cc: Petr Oros, Ivan Vecera, Jiri Pirko, Jonathan Corbet, Shuah Khan,
Shuah Khan, Randy Dunlap, Jani Nikula, Vadim Fedorenko,
Arkadiusz Kubalewski, Jiri Pirko, Andrew Lunn, David S. Miller,
Eric Dumazet, Jakub Kicinski, Paolo Abeni, linux-doc,
linux-kselftest
Add a test that sets a new priority on an input pin, reads it back and
restores the original value. The range of priorities a device accepts is
not advertised over netlink, so the test tries one below the current
value and then the small values every driver accepts, since a large
current value may mark an unused pin outside the settable range, as the
ice driver does with 255. A driver may still refuse all of them for a
constraint the interface does not tell, so that is a skip on a device
provided by the user, with the reason the tool reported, and a failure
on the device the script created, whose simulator accepts every
priority. The test skips when the dpll tool lacks the prio keyword of
pin set, and the restore is deferred right after the original value is
read so that an interrupted run restores it too. A pin that does not
advertise the priority-can-change capability or does not report a
priority is a skip on a device provided by the user and a failure on the
device the script created, whose pins have both.
The dpllsim_add_pin helper gains an optional priority argument, which is
written only when it is given, so that a pin without one keeps the
default of the simulator. A priority is accepted for input pins only,
so the callers pass one for those alone.
The standard device lays its inputs out as gnss0 with priority 0 and
ext0 with priority 1, and the stress devices get random priorities on
their input pins.
Reviewed-by: Ivan Vecera <ivecera@redhat.com>
Acked-by: Jiri Pirko <jiri@nvidia.com>
Signed-off-by: Petr Oros <poros@redhat.com>
---
v3:
- follow the move of the selftests to
tools/testing/selftests/drivers/dpll
v2: https://lore.kernel.org/all/20261105151852.1844305-9-poros@redhat.com/
v1: https://lore.kernel.org/all/20261103093910.596225-9-poros@redhat.com/
---
.../selftests/drivers/dpll/dpll_features.sh | 65 +++++++++++++++++++
.../selftests/drivers/dpll/dpll_lib.sh | 32 +++++++--
.../selftests/drivers/dpll/dpll_stress.sh | 15 +++--
3 files changed, 100 insertions(+), 12 deletions(-)
diff --git a/tools/testing/selftests/drivers/dpll/dpll_features.sh b/tools/testing/selftests/drivers/dpll/dpll_features.sh
index 874007411bc5e7..b4b1eff322a219 100755
--- a/tools/testing/selftests/drivers/dpll/dpll_features.sh
+++ b/tools/testing/selftests/drivers/dpll/dpll_features.sh
@@ -17,6 +17,7 @@ source "$SCRIPTDIR/dpll_lib.sh"
ALL_TESTS="
test_pin_state_transitions
+ test_pin_priority_get_set
"
##############################################################################
@@ -37,6 +38,70 @@ find_connected_pin()
done
}
+test_pin_priority_get_set()
+{
+ local before target after reason candidate
+
+ RET=0
+
+ if [ -z "$PIN_TUNE_ID" ]; then
+ dpll_log_test_skip_or_fail "DPLL pin priority set/get" \
+ "no spare input pin"
+ return
+ fi
+ if ! dpll_pin_set_supports prio; then
+ log_test_skip "DPLL pin priority set/get" \
+ "dpll tool lacks pin set prio"
+ return
+ fi
+ if ! dpll_pin_has_capability "$PIN_TUNE_ID" priority-can-change; then
+ dpll_log_test_skip_or_fail "DPLL pin priority set/get" \
+ "priority cannot change"
+ return
+ fi
+
+ before=$(dpll_pin_get_prio "$DPLL_DEV_ID" "$PIN_TUNE_ID")
+ if [ -z "$before" ]; then
+ dpll_log_test_skip_or_fail "DPLL pin priority set/get" \
+ "prio not reported"
+ return
+ fi
+ defer dpll_pin_set_prio "$DPLL_DEV_ID" "$PIN_TUNE_ID" "$before"
+
+ # The accepted range is not advertised, so try one below the current
+ # value and then small values, a refusal of all of them is a skip on
+ # a device the user provided, the simulator accepts every priority.
+ target=""
+ for candidate in $((before - 1)) 1 7; do
+ [ "$candidate" -ge 0 ] && [ "$candidate" != "$before" ] ||
+ continue
+ if dpll_pin_set_prio "$DPLL_DEV_ID" "$PIN_TUNE_ID" \
+ "$candidate"; then
+ target=$candidate
+ break
+ fi
+ reason=${DPLL_LAST_ERR%%$'\n'*}
+ done
+ if [ -z "$target" ]; then
+ dpll_log_test_skip_or_fail "DPLL pin priority set/get" \
+ "cannot set a new prio${reason:+, $reason}"
+ return
+ fi
+ after=$(dpll_pin_get_prio "$DPLL_DEV_ID" "$PIN_TUNE_ID")
+ if [ "$after" != "$target" ]; then
+ check_err 1 "priority expected $target, got '$after'"
+ fi
+
+ dpll_pin_set_prio "$DPLL_DEV_ID" "$PIN_TUNE_ID" "$before"
+ check_err $? "cannot restore pin priority"
+ after=$(dpll_pin_get_prio "$DPLL_DEV_ID" "$PIN_TUNE_ID")
+ if [ "$after" != "$before" ]; then
+ check_err 1 "priority not restored, got '$after'"
+ fi
+
+ log_test "DPLL pin priority set/get"
+}
+
test_pin_state_transitions()
{
local mode before state after reason ref_pin
diff --git a/tools/testing/selftests/drivers/dpll/dpll_lib.sh b/tools/testing/selftests/drivers/dpll/dpll_lib.sh
index 63c70a4aaab6e2..6fe707c2609c55 100644
--- a/tools/testing/selftests/drivers/dpll/dpll_lib.sh
+++ b/tools/testing/selftests/drivers/dpll/dpll_lib.sh
@@ -395,6 +395,11 @@ dpll_pin_has_capability()
<<< "$out" &> /dev/null
}
+dpll_pin_get_prio()
+{
+ __dpll_pin_dev_field "$1" "$2" '.prio'
+}
+
##############################################################################
# Pin setters
#
@@ -416,6 +421,16 @@ dpll_pin_set_state()
state "$state" 2>&1 >/dev/null)
}
+dpll_pin_set_prio()
+{
+ local dev_id=$1
+ local pin_id=$2
+ local prio=$3
+
+ DPLL_LAST_ERR=$(dpll pin set id "$pin_id" parent-device "$dev_id" \
+ prio "$prio" 2>&1 >/dev/null)
+}
+
##############################################################################
# dpllsim configfs interface
#
@@ -497,7 +512,9 @@ dpllsim_create_device()
# $1: device name, $2: pin name (becomes the board-label), $3: type (gnss,
# int-oscillator, ext, synce-eth-port, mux), $4: frequency in Hz,
-# $5: direction (input/output)
+# $5: direction (input/output), $6: prio (optional, input pins only).
+# The priority is written only when one is given, so a pin without one
+# keeps the default of the simulator.
dpllsim_add_pin()
{
local dev=$1
@@ -505,13 +522,16 @@ dpllsim_add_pin()
local type=$3
local frequency=$4
local direction=$5
+ local prio=${6:-}
local pin_path="$DPLLSIM_CFG/$dev/pins/$pin"
mkdir "$pin_path" 2>/dev/null || return 1
if ! echo "$type" 2>/dev/null > "$pin_path/type" ||
! echo "$frequency" 2>/dev/null > "$pin_path/frequency" ||
- ! echo "$direction" 2>/dev/null > "$pin_path/direction"; then
+ ! echo "$direction" 2>/dev/null > "$pin_path/direction" ||
+ { [ -n "$prio" ] &&
+ ! echo "$prio" 2>/dev/null > "$pin_path/prio"; }; then
rmdir "$pin_path" 2>/dev/null
return 1
fi
@@ -559,8 +579,8 @@ dpllsim_alloc_clock_id()
# Create a device with the standard 3-pin layout, deploy it and resolve
# the netlink device id. Sets DPLLSIM_CREATED_NAME, DPLLSIM_CLOCK_ID and
# DPLLSIM_DEV_ID.
-# gnss0: GNSS, 1 Hz, input
-# ext0: EXT, 10 MHz, input
+# gnss0: GNSS, 1 Hz, input, prio 0 (active reference)
+# ext0: EXT, 10 MHz, input, prio 1
# out0: EXT, 10 MHz, output
# shellcheck disable=SC2120 # the argument is optional
dpllsim_setup_standard_device()
@@ -572,8 +592,8 @@ dpllsim_setup_standard_device()
name="std$clock_id"
dpllsim_create_device "$name" "$clock_id" || return 1
- if ! dpllsim_add_pin "$name" gnss0 gnss 1 input ||
- ! dpllsim_add_pin "$name" ext0 ext 10000000 input ||
+ if ! dpllsim_add_pin "$name" gnss0 gnss 1 input 0 ||
+ ! dpllsim_add_pin "$name" ext0 ext 10000000 input 1 ||
! dpllsim_add_pin "$name" out0 ext 10000000 output; then
dpllsim_delete_device "$name"
return 1
diff --git a/tools/testing/selftests/drivers/dpll/dpll_stress.sh b/tools/testing/selftests/drivers/dpll/dpll_stress.sh
index 526cb113807f72..3806fda2e2dfe0 100755
--- a/tools/testing/selftests/drivers/dpll/dpll_stress.sh
+++ b/tools/testing/selftests/drivers/dpll/dpll_stress.sh
@@ -77,13 +77,13 @@ create_stress_device()
local clock_id=$1
local num_pins=$2
local name="st$clock_id"
- local i type freq dir
+ local i type freq dir prio
local dirs=(input output)
dpllsim_create_device "$name" "$clock_id" || return 1
# Always have at least one plain input pin, then random ones
- if ! dpllsim_add_pin "$name" ext0 ext 10000000 input; then
+ if ! dpllsim_add_pin "$name" ext0 ext 10000000 input 1; then
dpllsim_delete_device "$name"
return 1
fi
@@ -94,8 +94,11 @@ create_stress_device()
# output one
dir=${dirs[RANDOM % 2]}
[ "$type" = mux ] && dir=input
- if ! dpllsim_add_pin "$name" "pin$i" "$type" "$freq" \
- "$dir"; then
+ # Only input pins accept a priority
+ prio=""
+ [ "$dir" = input ] && prio=$((RANDOM % 128))
+ if ! dpllsim_add_pin "$name" "pin$i" "$type" "$freq" "$dir" \
+ "$prio"; then
dpllsim_delete_device "$name"
return 1
fi
@@ -206,8 +209,8 @@ test_deploy_cycle()
fi
track_device "$name"
- if ! dpllsim_add_pin "$name" gnss0 gnss 1 input ||
- ! dpllsim_add_pin "$name" ext0 ext 10000000 input ||
+ if ! dpllsim_add_pin "$name" gnss0 gnss 1 input 0 ||
+ ! dpllsim_add_pin "$name" ext0 ext 10000000 input 1 ||
! dpllsim_add_pin "$name" out0 ext 10000000 output; then
check_err 1 "cannot add pins"
log_test "DPLL deploy/undeploy cycling"
--
2.56.0
^ permalink raw reply related [flat|nested] 31+ messages in thread* Re: [PATCH net-next v3 08/14] selftests: dpll: add pin priority test
2026-11-09 9:35 ` [PATCH net-next v3 08/14] selftests: dpll: add pin priority test Petr Oros
@ 2026-11-11 9:37 ` netdev-bot+sashiko
0 siblings, 0 replies; 31+ messages in thread
From: netdev-bot+sashiko @ 2026-11-11 9:37 UTC (permalink / raw)
To: poros
Cc: netdev, ivecera, jiri, corbet, skhan, shuah, rdunlap, jani.nikula,
vadim.fedorenko, arkadiusz.kubalewski, jiri, andrew+netdev, davem,
edumazet, kuba, pabeni, linux-doc, linux-kselftest
Thank you for your contribution! Sashiko AI review found 4 potential
issue(s) to consider.
Critical: 0 · High: 0 · Medium: 2 · Low: 2
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] pick_tune_pin() says PIN_TUNE_ID "must not be the reference
the device is locked to" and that tests skip "rather than…
- [Medium] The candidate loop in test_pin_priority_get_set() is built for
the case the commit message describes: "a large current value may mark…
- [Low] The comment in test_pin_priority_get_set()
(tools/testing/selftests/drivers/dpll/dpll_features.sh) and the commit…
- [Low] This patch adds a layout comment above
dpllsim_setup_standard_device() in…
--
selftests: dpll: add pin priority test
This adds test_pin_priority_get_set() to dpll_features.sh. The test sets a
new priority on a spare input pin, reads it back and restores the original
value. dpllsim_add_pin() gains an optional priority argument, and the
standard and stress simulator devices now set priorities on their input
pins.
> diff --git a/tools/testing/selftests/drivers/dpll/dpll_features.sh b/tools/testing/selftests/drivers/dpll/dpll_features.sh
> index 874007411bc5e..b4b1eff322a21 100755
> --- a/tools/testing/selftests/drivers/dpll/dpll_features.sh
> +++ b/tools/testing/selftests/drivers/dpll/dpll_features.sh
[ ... ]
> + before=$(dpll_pin_get_prio "$DPLL_DEV_ID" "$PIN_TUNE_ID")
> + if [ -z "$before" ]; then
> + dpll_log_test_skip_or_fail "DPLL pin priority set/get" \
> + "prio not reported"
> + return
> + fi
> + defer dpll_pin_set_prio "$DPLL_DEV_ID" "$PIN_TUNE_ID" "$before"
> +
> + # The accepted range is not advertised, so try one below the current
> + # value and then small values, a refusal of all of them is a skip on
> + # a device the user provided, the simulator accepts every priority.
[Severity: Low]
Is "the simulator accepts every priority" accurate? The commit message makes
the same claim. However, dpllsim_pin_prio_set() rejects anything above
DPLLSIM_PIN_PRIO_MAX, which is 255 in dpllsim.h:
drivers/dpll/dpllsim/dpll.c:dpllsim_pin_prio_set() {
...
if (prio > DPLLSIM_PIN_PRIO_MAX) {
NL_SET_ERR_MSG_FMT(extack, "Pin priority must be in range 0-%u",
DPLLSIM_PIN_PRIO_MAX);
return -EINVAL;
}
...
}
The configfs store dpllsim_pin_prio_store() applies the same limit.
The test still works today. Since before is capped at 255, the candidates
(before - 1, 1 and 7) are always in range.
This comment is the reason a refusal on the created device is a failure and
not a skip. Could it say "every priority in 0-255" or "every value tried
here"? The comment is still the same at the end of the series.
> + target=""
> + for candidate in $((before - 1)) 1 7; do
> + [ "$candidate" -ge 0 ] && [ "$candidate" != "$before" ] ||
> + continue
> + if dpll_pin_set_prio "$DPLL_DEV_ID" "$PIN_TUNE_ID" \
> + "$candidate"; then
[Severity: Medium]
Can this make a user-provided device switch its reference?
The comment above pick_tune_pin() says PIN_TUNE_ID "must not be the
reference the device is locked to". It also says tests skip "rather than
reconfiguring the reference". But pick_tune_pin() accepts selectable pins:
case "$state" in
disconnected|selectable)
;;
In automatic mode, a selectable input takes part in reference selection by
priority. This loop gives that pin a higher priority (a smaller value). It
never compares the new value with the priority of the current reference.
On a device set through DPLL_DEV_ID or DPLL_DRIVER, the new priority reaches
hardware straight away. zl3073x_dpll_input_pin_prio_set() writes REF_PRIO
through zl3073x_chan_state_set() when zl3073x_chan_ref_is_selectable()
holds. ice_dpll_hw_input_prio_set() sends it to the CGU firmware through
ice_aq_set_cgu_ref_prio().
Take a device in automatic, revertive mode with a valid signal on the spare
input. If before - 1 ties the locked reference's priority, or if 1 or 7
beats it, could the DPLL switch to the spare pin? Restoring the priority
could then switch it back. Each switch can disturb outputs feeding SyncE or
PTP.
Would it work to pick a target that does not outrank the connected
reference? Other options are to change the priority only on disconnected
pins, or to skip in automatic mode on user-provided devices.
> + target=$candidate
> + break
> + fi
> + reason=${DPLL_LAST_ERR%%$'\n'*}
> + done
[ ... ]
> + dpll_pin_set_prio "$DPLL_DEV_ID" "$PIN_TUNE_ID" "$before"
> + check_err $? "cannot restore pin priority"
[Severity: Medium]
What happens here in the case the candidate loop is built for? The commit
message says:
since a large current value may mark an unused pin outside the settable
range, as the ice driver does with 255.
In that case before itself cannot be set. Suppose before - 1 (254) is
refused and then 1 or 7 is accepted. The pin's hardware priority has
changed, and this restore to before would be refused for the same reason.
The test would then report a failure, not a skip, on a user-provided
device. The deferred restore sends the same value, so it also fails, and
without any message. The unused input stays at priority 1 or 7 on the
user's hardware, and in automatic mode it could then be picked as a
reference.
ice_dpll_input_prio_set() and ice_dpll_hw_input_prio_set() do no range
check of their own. They pass (u8)prio to firmware, so only firmware
decides whether 255 can be written back.
test_pin_state_transitions() treats a refused restore as a skip when RET is
0, but this test does not. Should it first check that before can be set,
for example by writing it back once, before trying a different value? The
code is still the same at the end of the series.
> + after=$(dpll_pin_get_prio "$DPLL_DEV_ID" "$PIN_TUNE_ID")
> + if [ "$after" != "$before" ]; then
> + check_err 1 "priority not restored, got '$after'"
> + fi
[ ... ]
> diff --git a/tools/testing/selftests/drivers/dpll/dpll_lib.sh b/tools/testing/selftests/drivers/dpll/dpll_lib.sh
> index 63c70a4aaab6e..6fe707c2609c5 100644
> --- a/tools/testing/selftests/drivers/dpll/dpll_lib.sh
> +++ b/tools/testing/selftests/drivers/dpll/dpll_lib.sh
[ ... ]
> @@ -559,8 +579,8 @@ dpllsim_alloc_clock_id()
> # Create a device with the standard 3-pin layout, deploy it and resolve
> # the netlink device id. Sets DPLLSIM_CREATED_NAME, DPLLSIM_CLOCK_ID and
> # DPLLSIM_DEV_ID.
> -# gnss0: GNSS, 1 Hz, input
> -# ext0: EXT, 10 MHz, input
> +# gnss0: GNSS, 1 Hz, input, prio 0 (active reference)
[Severity: Low]
Is "(active reference)" correct for the simulator? It does not seem to ever
select or lock to a reference.
dpllsim_pin_reset() starts every pin disconnected:
pin->ops = dpllsim_pin_select_ops(pin);
pin->state = DPLL_PIN_STATE_DISCONNECTED;
dpllsim_pin_prio_set() only stores sim_pin->prio, and nothing else reads
it. sim->lock_status is set once to DPLL_LOCK_STATUS_UNLOCKED in core.c and
never changes.
The device is always in automatic mode. dpllsim_pin_state_set() accepts
only disconnected and selectable there, so no pin is ever connected.
So gnss0 never becomes the active reference, and its prio 0 does not affect
which pin is used. That includes the tie test_pin_priority_get_set() can
create by setting ext0 to 0. The comment is still the same at the end of
the series, and no later patch adds selection by priority.
> +# ext0: EXT, 10 MHz, input, prio 1
> # out0: EXT, 10 MHz, output
[ ... ]
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261109093523.931201-1-poros%40redhat.com
^ permalink raw reply [flat|nested] 31+ messages in thread
* [PATCH net-next v3 09/14] dpll: dpllsim: Add support to set frequency on pins
2026-11-09 9:35 [PATCH net-next v3 00/14] dpll: add a simulated DPLL device driver Petr Oros
` (7 preceding siblings ...)
2026-11-09 9:35 ` [PATCH net-next v3 08/14] selftests: dpll: add pin priority test Petr Oros
@ 2026-11-09 9:35 ` Petr Oros
2026-11-11 9:37 ` netdev-bot+sashiko
2026-11-09 9:35 ` [PATCH net-next v3 10/14] selftests: dpll: add pin frequency test Petr Oros
` (4 subsequent siblings)
13 siblings, 1 reply; 31+ messages in thread
From: Petr Oros @ 2026-11-09 9:35 UTC (permalink / raw)
To: netdev
Cc: Petr Oros, Ivan Vecera, Jiri Pirko, Jonathan Corbet, Shuah Khan,
Shuah Khan, Randy Dunlap, Jani Nikula, Vadim Fedorenko,
Arkadiusz Kubalewski, Jiri Pirko, Andrew Lunn, David S. Miller,
Eric Dumazet, Jakub Kicinski, Paolo Abeni, linux-doc,
linux-kselftest
Add support to set the frequency of input and output pins. The DPLL
core validates the requested value against the supported frequency
list the pin advertises, so the frequency_set callback only has to
store it. The same per type tables that validate the configured
frequency at live time serve as the supported list. The configfs
attribute is rejected with EBUSY while the device is live, so only the
netlink setter changes the frequency of a registered pin and the two
cannot race.
Reviewed-by: Ivan Vecera <ivecera@redhat.com>
Acked-by: Jiri Pirko <jiri@nvidia.com>
Signed-off-by: Petr Oros <poros@redhat.com>
---
drivers/dpll/dpllsim/dpll.c | 15 +++++++++++++++
drivers/dpll/dpllsim/dpllsim.h | 4 ++--
2 files changed, 17 insertions(+), 2 deletions(-)
diff --git a/drivers/dpll/dpllsim/dpll.c b/drivers/dpll/dpllsim/dpll.c
index 5e360596097616..8590d2db808e45 100644
--- a/drivers/dpll/dpllsim/dpll.c
+++ b/drivers/dpll/dpllsim/dpll.c
@@ -79,6 +79,19 @@ static int dpllsim_pin_frequency_get(const struct dpll_pin *pin,
return 0;
}
+static int dpllsim_pin_frequency_set(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv, u64 frequency,
+ struct netlink_ext_ack *extack)
+{
+ struct dpllsim_pin *sim_pin = pin_priv;
+
+ guard(mutex)(&sim_pin->dev->lock);
+ sim_pin->frequency = frequency;
+ return 0;
+}
+
static int dpllsim_pin_direction_get(const struct dpll_pin *pin,
void *pin_priv,
const struct dpll_device *dpll,
@@ -171,6 +184,7 @@ static int dpllsim_pin_prio_set(const struct dpll_pin *pin, void *pin_priv,
static const struct dpll_pin_ops dpllsim_input_pin_ops = {
.frequency_get = dpllsim_pin_frequency_get,
+ .frequency_set = dpllsim_pin_frequency_set,
.direction_get = dpllsim_pin_direction_get,
.state_on_dpll_get = dpllsim_pin_state_get,
.state_on_dpll_set = dpllsim_pin_state_set,
@@ -180,6 +194,7 @@ static const struct dpll_pin_ops dpllsim_input_pin_ops = {
static const struct dpll_pin_ops dpllsim_output_pin_ops = {
.frequency_get = dpllsim_pin_frequency_get,
+ .frequency_set = dpllsim_pin_frequency_set,
.direction_get = dpllsim_pin_direction_get,
.state_on_dpll_get = dpllsim_pin_state_get,
};
diff --git a/drivers/dpll/dpllsim/dpllsim.h b/drivers/dpll/dpllsim/dpllsim.h
index f9fced337dc881..f59447d26a8668 100644
--- a/drivers/dpll/dpllsim/dpllsim.h
+++ b/drivers/dpll/dpllsim/dpllsim.h
@@ -26,8 +26,8 @@ struct dpllsim_pin {
/*
* Configuration, set through configfs only until
- * dpllsim_dev_frozen(). The netlink prio setter changes prio at
- * runtime under dev->lock.
+ * dpllsim_dev_frozen(). The netlink setters change prio and
+ * frequency at runtime under dev->lock.
*/
enum dpll_pin_type type;
enum dpll_pin_direction direction;
--
2.56.0
^ permalink raw reply related [flat|nested] 31+ messages in thread* Re: [PATCH net-next v3 09/14] dpll: dpllsim: Add support to set frequency on pins
2026-11-09 9:35 ` [PATCH net-next v3 09/14] dpll: dpllsim: Add support to set frequency on pins Petr Oros
@ 2026-11-11 9:37 ` netdev-bot+sashiko
0 siblings, 0 replies; 31+ messages in thread
From: netdev-bot+sashiko @ 2026-11-11 9:37 UTC (permalink / raw)
To: poros
Cc: netdev, ivecera, jiri, corbet, skhan, shuah, rdunlap, jani.nikula,
vadim.fedorenko, arkadiusz.kubalewski, jiri, andrew+netdev, davem,
edumazet, kuba, pabeni, linux-doc, linux-kselftest
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 adds dpllsim_pin_frequency_set()
(drivers/dpll/dpllsim/dpll.c).
--
dpll: dpllsim: Add support to set frequency on pins
This adds a netlink frequency_set callback to the dpllsim input and output
pin ops. The callback stores the value in sim_pin->frequency under
dev->lock. The DPLL core has already checked it against the pin's
supported frequency list.
> diff --git a/drivers/dpll/dpllsim/dpll.c b/drivers/dpll/dpllsim/dpll.c
> index 5e36059609761..8590d2db808e4 100644
> --- a/drivers/dpll/dpllsim/dpll.c
> +++ b/drivers/dpll/dpllsim/dpll.c
> @@ -79,6 +79,19 @@ static int dpllsim_pin_frequency_get(const struct dpll_pin *pin,
> return 0;
> }
>
> +static int dpllsim_pin_frequency_set(const struct dpll_pin *pin,
> + void *pin_priv,
> + const struct dpll_device *dpll,
> + void *dpll_priv, u64 frequency,
> + struct netlink_ext_ack *extack)
> +{
> + struct dpllsim_pin *sim_pin = pin_priv;
> +
> + guard(mutex)(&sim_pin->dev->lock);
> + sim_pin->frequency = frequency;
> + return 0;
> +}
[Severity: Low]
This isn't a bug, but should Documentation/driver-api/dpllsim.rst be
updated along with this?
dpllsim_pin_frequency_set() writes sim_pin->frequency. That is the same
field the configfs frequency attribute stores and shows through
dpllsim_pin_frequency_store() and dpllsim_pin_frequency_show() in core.c.
dpllsim_pin_frequency_valid() checks it again on the next live=1.
Nothing on the undeploy path resets it, so a frequency set over netlink:
- shows up when the configfs frequency attribute is read,
- is still there after live=0,
- is used on the next live=1 instead of the value written to configfs.
The rst describes pin attributes only as configuration written before
going live:
Attributes are written while the instance is not live, then ``live`` is
set to 1 to register the device and its pins with the DPLL subsystem.
``frequency`` Frequency in Hz, must be supported by the pin type
(default 0, which is rejected)
The struct dpllsim_pin comment in dpllsim.h was updated to say "The netlink
setters change prio and frequency at runtime under dev->lock", but the rst
still doesn't mention runtime changes or that they carry over to the next
deploy. Prio, added by the earlier "dpll: dpllsim: Add support to get/set
priority on input pins" commit, has the same gap.
Could a sentence be added to dpllsim.rst covering both prio and frequency?
[ ... ]
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261109093523.931201-1-poros%40redhat.com
^ permalink raw reply [flat|nested] 31+ messages in thread
* [PATCH net-next v3 10/14] selftests: dpll: add pin frequency test
2026-11-09 9:35 [PATCH net-next v3 00/14] dpll: add a simulated DPLL device driver Petr Oros
` (8 preceding siblings ...)
2026-11-09 9:35 ` [PATCH net-next v3 09/14] dpll: dpllsim: Add support to set frequency on pins Petr Oros
@ 2026-11-09 9:35 ` Petr Oros
2026-11-11 9:37 ` netdev-bot+sashiko
2026-11-09 9:35 ` [PATCH net-next v3 11/14] dpll: dpllsim: Add support to adjust phase Petr Oros
` (3 subsequent siblings)
13 siblings, 1 reply; 31+ messages in thread
From: Petr Oros @ 2026-11-09 9:35 UTC (permalink / raw)
To: netdev
Cc: Petr Oros, Ivan Vecera, Jiri Pirko, Jonathan Corbet, Shuah Khan,
Shuah Khan, Randy Dunlap, Jani Nikula, Vadim Fedorenko,
Arkadiusz Kubalewski, Jiri Pirko, Andrew Lunn, David S. Miller,
Eric Dumazet, Jakub Kicinski, Paolo Abeni, linux-doc,
linux-kselftest
Add a test that picks a supported frequency other than the current one
from the frequency-supported list of an input pin, sets it, reads it
back and restores the original value. Both ends of every advertised
range are candidates, so that a pin advertising a single range wider
than one value still has one. It skips when the dpll tool lacks the
frequency keyword of pin set, and the restore is deferred right after
the original value is read so that an interrupted run restores it too.
A pin that reports no frequency or advertises a single frequency only
is a skip on a device provided by the user and a failure on the device
the script created, whose pins advertise several.
The driver may refuse the candidate. A driver without a frequency
setter refuses every value with EOPNOTSUPP, and a driver with
constraints of its own refuses values the advertised ranges allow,
zl3073x for instance wants the frequencies of an output pair to divide
each other. On a device provided by the user a refused set is a skip
that names the reason the tool reported, which the setter keeps in
DPLL_LAST_ERR, on the created device the simulator accepts every
advertised value and the refusal is a failure. Once the set went
through, a refused restore is a failure on either.
Reviewed-by: Ivan Vecera <ivecera@redhat.com>
Acked-by: Jiri Pirko <jiri@nvidia.com>
Signed-off-by: Petr Oros <poros@redhat.com>
---
v3:
- follow the move of the selftests to
tools/testing/selftests/drivers/dpll
v2: https://lore.kernel.org/all/20261105151852.1844305-11-poros@redhat.com/
v1: https://lore.kernel.org/all/20261103093910.596225-11-poros@redhat.com/
---
.../selftests/drivers/dpll/dpll_features.sh | 64 +++++++++++++++++++
.../selftests/drivers/dpll/dpll_lib.sh | 5 ++
2 files changed, 69 insertions(+)
diff --git a/tools/testing/selftests/drivers/dpll/dpll_features.sh b/tools/testing/selftests/drivers/dpll/dpll_features.sh
index b4b1eff322a219..6649936c710e43 100755
--- a/tools/testing/selftests/drivers/dpll/dpll_features.sh
+++ b/tools/testing/selftests/drivers/dpll/dpll_features.sh
@@ -18,6 +18,7 @@ source "$SCRIPTDIR/dpll_lib.sh"
ALL_TESTS="
test_pin_state_transitions
test_pin_priority_get_set
+ test_pin_frequency_set_get
"
##############################################################################
@@ -237,6 +238,69 @@ test_pin_state_transitions()
log_test "DPLL pin state transitions"
}
+test_pin_frequency_set_get()
+{
+ local before target after reason
+
+ RET=0
+
+ if [ -z "$PIN_TUNE_ID" ]; then
+ dpll_log_test_skip_or_fail "DPLL pin frequency set/get" \
+ "no spare input pin"
+ return
+ fi
+ if ! dpll_pin_set_supports frequency; then
+ log_test_skip "DPLL pin frequency set/get" \
+ "dpll tool lacks pin set frequency"
+ return
+ fi
+
+ before=$(dpll_pin_get_frequency "$PIN_TUNE_ID")
+ if [ -z "$before" ]; then
+ dpll_log_test_skip_or_fail "DPLL pin frequency set/get" \
+ "frequency not reported"
+ return
+ fi
+ defer dpll_pin_set_frequency "$PIN_TUNE_ID" "$before"
+
+ # Pick a supported frequency different from the current one. Both
+ # ends of every supported range are candidates, so a pin with a
+ # single range still has an alternative unless it is one value wide.
+ target=$(dpll_pin_json "$PIN_TUNE_ID" | \
+ jq -r --argjson cur "$before" \
+ '[.["frequency-supported"][]? |
+ .["frequency-min"], .["frequency-max"] |
+ numbers | select(. != $cur)][0] // empty')
+ if [ -z "$target" ]; then
+ dpll_log_test_skip_or_fail "DPLL pin frequency set/get" \
+ "no alternative supported frequency"
+ return
+ fi
+
+ # A driver without a frequency setter or with constraints of its own
+ # may refuse the candidate, a skip on a device the user provided, the
+ # simulator accepts every advertised value.
+ if ! dpll_pin_set_frequency "$PIN_TUNE_ID" "$target"; then
+ reason=${DPLL_LAST_ERR%%$'\n'*}
+ dpll_log_test_skip_or_fail "DPLL pin frequency set/get" \
+ "cannot set frequency=$target${reason:+, $reason}"
+ return
+ fi
+ after=$(dpll_pin_get_frequency "$PIN_TUNE_ID")
+ if [ "$after" != "$target" ]; then
+ check_err 1 "frequency expected $target, got '$after'"
+ fi
+
+ dpll_pin_set_frequency "$PIN_TUNE_ID" "$before"
+ check_err $? "cannot restore frequency"
+ after=$(dpll_pin_get_frequency "$PIN_TUNE_ID")
+ if [ "$after" != "$before" ]; then
+ check_err 1 "frequency not restored, got '$after'"
+ fi
+
+ log_test "DPLL pin frequency set/get"
+}
+
##############################################################################
cleanup()
diff --git a/tools/testing/selftests/drivers/dpll/dpll_lib.sh b/tools/testing/selftests/drivers/dpll/dpll_lib.sh
index 6fe707c2609c55..2583a5aa3fe18d 100644
--- a/tools/testing/selftests/drivers/dpll/dpll_lib.sh
+++ b/tools/testing/selftests/drivers/dpll/dpll_lib.sh
@@ -431,6 +431,11 @@ dpll_pin_set_prio()
prio "$prio" 2>&1 >/dev/null)
}
+dpll_pin_set_frequency()
+{
+ DPLL_LAST_ERR=$(dpll pin set id "$1" frequency "$2" 2>&1 >/dev/null)
+}
+
##############################################################################
# dpllsim configfs interface
#
--
2.56.0
^ permalink raw reply related [flat|nested] 31+ messages in thread* Re: [PATCH net-next v3 10/14] selftests: dpll: add pin frequency test
2026-11-09 9:35 ` [PATCH net-next v3 10/14] selftests: dpll: add pin frequency test Petr Oros
@ 2026-11-11 9:37 ` netdev-bot+sashiko
0 siblings, 0 replies; 31+ messages in thread
From: netdev-bot+sashiko @ 2026-11-11 9:37 UTC (permalink / raw)
To: poros
Cc: netdev, ivecera, jiri, corbet, skhan, shuah, rdunlap, jani.nikula,
vadim.fedorenko, arkadiusz.kubalewski, jiri, andrew+netdev, davem,
edumazet, kuba, pabeni, linux-doc, linux-kselftest
Thank you for your contribution! Sashiko AI review found 2 potential
issue(s) to consider.
Critical: 0 · High: 0 · Medium: 2 · 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] test_pin_frequency_set_get() in
tools/testing/selftests/drivers/dpll/dpll_features.sh only checks that…
- [Medium] test_pin_frequency_set_get() reprograms the frequency of
PIN_TUNE_ID.
--
selftests: dpll: add pin frequency test
This adds test_pin_frequency_set_get() to dpll_features.sh. The test
takes a supported frequency other than the current one from an input
pin's frequency-supported list, sets it, reads it back and restores the
original value. It also adds a dpll_pin_set_frequency() helper to
dpll_lib.sh, which keeps the tool's error output in DPLL_LAST_ERR.
> diff --git a/tools/testing/selftests/drivers/dpll/dpll_features.sh b/tools/testing/selftests/drivers/dpll/dpll_features.sh
> index b4b1eff322a21..6649936c710e4 100755
> --- a/tools/testing/selftests/drivers/dpll/dpll_features.sh
> +++ b/tools/testing/selftests/drivers/dpll/dpll_features.sh
[ ... ]
> @@ -237,6 +238,69 @@ test_pin_state_transitions()
> log_test "DPLL pin state transitions"
> }
>
> +test_pin_frequency_set_get()
> +{
[ ... ]
> + before=$(dpll_pin_get_frequency "$PIN_TUNE_ID")
> + if [ -z "$before" ]; then
> + dpll_log_test_skip_or_fail "DPLL pin frequency set/get" \
> + "frequency not reported"
> + return
> + fi
> + defer dpll_pin_set_frequency "$PIN_TUNE_ID" "$before"
[Severity: Medium]
Can the original frequency always be written back here? The only check
on before is that it is not empty. The DPLL core refuses any value
outside the pin's frequency-supported ranges, and it does this before
it compares the value with the current frequency:
drivers/dpll/dpll_netlink.c:dpll_pin_freq_set() {
...
if (!dpll_pin_is_freq_supported(pin, freq)) {
NL_SET_ERR_MSG_ATTR(extack, a, "frequency is not supported by the device");
return -EINVAL;
}
...
}
zl3073x hits this case on purpose. It reads a sub-Hz reference back as
0 Hz and leaves 0 out of freq_supported:
drivers/dpll/zl3073x/prop.c:zl3073x_pin_props_get() {
...
/* Start with current frequency at index 0. A sub-Hz frequency is
* read back as 0 Hz and cannot be set, so it is not offered.
*/
j = 0;
if (curr_freq) {
...
}
Here is what happens on such a pin on a device the user provided:
- before=0 passes the -z check.
- The jq filter picks the first advertised frequency as target.
- Setting target succeeds and reprograms the hardware reference.
- The explicit restore and the deferred restore are both refused with
EINVAL.
Wouldn't this leave the user's device on a frequency the test picked?
The commit message says the test "restores the original value".
The phase adjust test later in the series bails out before it schedules
the restore when the current value could not be written back. Would a
similar check fit here, one that confirms before lies inside an
advertised frequency-supported range before the pin is touched?
> +
> + # Pick a supported frequency different from the current one. Both
> + # ends of every supported range are candidates, so a pin with a
> + # single range still has an alternative unless it is one value wide.
> + target=$(dpll_pin_json "$PIN_TUNE_ID" | \
> + jq -r --argjson cur "$before" \
> + '[.["frequency-supported"][]? |
> + .["frequency-min"], .["frequency-max"] |
> + numbers | select(. != $cur)][0] // empty')
[ ... ]
> + # A driver without a frequency setter or with constraints of its own
> + # may refuse the candidate, a skip on a device the user provided, the
> + # simulator accepts every advertised value.
> + if ! dpll_pin_set_frequency "$PIN_TUNE_ID" "$target"; then
[Severity: Medium]
Is it safe to reprogram this pin's frequency based only on its state on
DPLL_DEV_ID? pick_tune_pin() counts a pin as spare when it is
disconnected or selectable on that one device:
tools/testing/selftests/drivers/dpll/dpll_features.sh:pick_tune_pin() {
...
state=$(dpll_pin_get_state "$DPLL_DEV_ID" "$pin_id")
case "$state" in
disconnected|selectable)
...
}
State and priority are per parent device, but frequency belongs to the
pin itself. It is shared by every DPLL device the pin is registered
with.
On zl3073x, every channel gets the same dpll_pin for an input index
through dpll_pin_get(clock_id, index). The setter then writes the
chip-wide reference state:
drivers/dpll/zl3073x/dpll.c:zl3073x_dpll_input_pin_frequency_set() {
...
ref_id = zl3073x_input_pin_ref_get(pin->id);
ref = *zl3073x_ref_state_get(zldev, ref_id);
/* Update frequency */
zl3073x_ref_freq_set(&ref, frequency);
/* Commit reference state */
return zl3073x_ref_state_set(zldev, ref_id, &ref);
}
Say the chosen input is selectable on the EEC channel but is the locked
reference of the PPS channel. Could the PPS channel then lose lock, go
into holdover or switch references until the restore runs? ice also
registers its inputs with both its EEC and PPS DPLLs. The test never
checks that the other channel locks again.
Should the pin be checked against all of its parent devices before this
test changes its frequency?
> + reason=${DPLL_LAST_ERR%%$'\n'*}
> + dpll_log_test_skip_or_fail "DPLL pin frequency set/get" \
> + "cannot set frequency=$target${reason:+, $reason}"
> + return
> + fi
[ ... ]
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261109093523.931201-1-poros%40redhat.com
^ permalink raw reply [flat|nested] 31+ messages in thread
* [PATCH net-next v3 11/14] dpll: dpllsim: Add support to adjust phase
2026-11-09 9:35 [PATCH net-next v3 00/14] dpll: add a simulated DPLL device driver Petr Oros
` (9 preceding siblings ...)
2026-11-09 9:35 ` [PATCH net-next v3 10/14] selftests: dpll: add pin frequency test Petr Oros
@ 2026-11-09 9:35 ` Petr Oros
2026-11-09 9:35 ` [PATCH net-next v3 12/14] selftests: dpll: add pin phase adjust test Petr Oros
` (2 subsequent siblings)
13 siblings, 0 replies; 31+ messages in thread
From: Petr Oros @ 2026-11-09 9:35 UTC (permalink / raw)
To: netdev
Cc: Petr Oros, Ivan Vecera, Jiri Pirko, Jonathan Corbet, Shuah Khan,
Shuah Khan, Randy Dunlap, Jani Nikula, Vadim Fedorenko,
Arkadiusz Kubalewski, Jiri Pirko, Andrew Lunn, David S. Miller,
Eric Dumazet, Jakub Kicinski, Paolo Abeni, linux-doc,
linux-kselftest
Add support to get and set the phase adjustment of input and output
pins. The pins advertise a granularity of 100 ps and a symmetric range
whose ends are the largest multiples of the granularity that fit a
32-bit signed value, so that the advertised ends themselves are
settable, the core validates the requested value against both and the
driver stores it. The adjustment is reset to zero every time the device
goes live, so that repeated deploy cycles start from a known state. It
becomes the baseline of the simulated phase offset measurements once
those exist.
Reviewed-by: Ivan Vecera <ivecera@redhat.com>
Acked-by: Jiri Pirko <jiri@nvidia.com>
Signed-off-by: Petr Oros <poros@redhat.com>
---
drivers/dpll/dpllsim/dpll.c | 40 ++++++++++++++++++++++++++++++++++
drivers/dpll/dpllsim/dpllsim.h | 1 +
2 files changed, 41 insertions(+)
diff --git a/drivers/dpll/dpllsim/dpll.c b/drivers/dpll/dpllsim/dpll.c
index 8590d2db808e45..d9a0bbedf89423 100644
--- a/drivers/dpll/dpllsim/dpll.c
+++ b/drivers/dpll/dpllsim/dpll.c
@@ -12,6 +12,11 @@
#include "dpllsim.h"
+#define DPLLSIM_PHASE_ADJUST_GRAN 100 /* ps */
+/* Largest multiple of the granularity, so the range ends are settable */
+#define DPLLSIM_PHASE_ADJUST_MAX \
+ (S32_MAX - S32_MAX % DPLLSIM_PHASE_ADJUST_GRAN)
+
static struct dpll_pin_frequency dpllsim_freq_1pps[] = {
DPLL_PIN_FREQUENCY_RANGE(1, 1),
};
@@ -182,6 +187,33 @@ static int dpllsim_pin_prio_set(const struct dpll_pin *pin, void *pin_priv,
return 0;
}
+static int dpllsim_pin_phase_adjust_get(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv, s32 *phase_adjust,
+ struct netlink_ext_ack *extack)
+{
+ struct dpllsim_pin *sim_pin = pin_priv;
+
+ guard(mutex)(&sim_pin->dev->lock);
+ *phase_adjust = sim_pin->phase_adjust;
+ return 0;
+}
+
+static int dpllsim_pin_phase_adjust_set(const struct dpll_pin *pin,
+ void *pin_priv,
+ const struct dpll_device *dpll,
+ void *dpll_priv,
+ const s32 phase_adjust,
+ struct netlink_ext_ack *extack)
+{
+ struct dpllsim_pin *sim_pin = pin_priv;
+
+ guard(mutex)(&sim_pin->dev->lock);
+ sim_pin->phase_adjust = phase_adjust;
+ return 0;
+}
+
static const struct dpll_pin_ops dpllsim_input_pin_ops = {
.frequency_get = dpllsim_pin_frequency_get,
.frequency_set = dpllsim_pin_frequency_set,
@@ -190,6 +222,8 @@ static const struct dpll_pin_ops dpllsim_input_pin_ops = {
.state_on_dpll_set = dpllsim_pin_state_set,
.prio_get = dpllsim_pin_prio_get,
.prio_set = dpllsim_pin_prio_set,
+ .phase_adjust_get = dpllsim_pin_phase_adjust_get,
+ .phase_adjust_set = dpllsim_pin_phase_adjust_set,
};
static const struct dpll_pin_ops dpllsim_output_pin_ops = {
@@ -197,6 +231,8 @@ static const struct dpll_pin_ops dpllsim_output_pin_ops = {
.frequency_set = dpllsim_pin_frequency_set,
.direction_get = dpllsim_pin_direction_get,
.state_on_dpll_get = dpllsim_pin_state_get,
+ .phase_adjust_get = dpllsim_pin_phase_adjust_get,
+ .phase_adjust_set = dpllsim_pin_phase_adjust_set,
};
static const struct dpll_pin_ops *
@@ -212,6 +248,9 @@ static void dpllsim_pin_props(struct dpllsim_pin *pin,
{
prop->type = pin->type;
prop->board_label = config_item_name(&pin->item);
+ prop->phase_range.min = -DPLLSIM_PHASE_ADJUST_MAX;
+ prop->phase_range.max = DPLLSIM_PHASE_ADJUST_MAX;
+ prop->phase_gran = DPLLSIM_PHASE_ADJUST_GRAN;
switch (pin->type) {
case DPLL_PIN_TYPE_GNSS:
@@ -273,6 +312,7 @@ static void dpllsim_pin_reset(struct dpllsim_pin *pin)
{
pin->ops = dpllsim_pin_select_ops(pin);
pin->state = DPLL_PIN_STATE_DISCONNECTED;
+ pin->phase_adjust = 0;
}
/* Build sim->pins in registration order, the order the pins were created */
diff --git a/drivers/dpll/dpllsim/dpllsim.h b/drivers/dpll/dpllsim/dpllsim.h
index f59447d26a8668..886ac27f5f92d6 100644
--- a/drivers/dpll/dpllsim/dpllsim.h
+++ b/drivers/dpll/dpllsim/dpllsim.h
@@ -43,6 +43,7 @@ struct dpllsim_pin {
/* Simulated state, guarded by dev->lock */
enum dpll_pin_state state;
+ s32 phase_adjust;
};
struct dpllsim_dev {
--
2.56.0
^ permalink raw reply related [flat|nested] 31+ messages in thread* [PATCH net-next v3 12/14] selftests: dpll: add pin phase adjust test
2026-11-09 9:35 [PATCH net-next v3 00/14] dpll: add a simulated DPLL device driver Petr Oros
` (10 preceding siblings ...)
2026-11-09 9:35 ` [PATCH net-next v3 11/14] dpll: dpllsim: Add support to adjust phase Petr Oros
@ 2026-11-09 9:35 ` Petr Oros
2026-11-11 9:37 ` netdev-bot+sashiko
2026-11-09 9:35 ` [PATCH net-next v3 13/14] selftests: dpll: add concurrency stress tests Petr Oros
2026-11-09 9:35 ` [PATCH net-next v3 14/14] selftests: dpll: add reference leak tests Petr Oros
13 siblings, 1 reply; 31+ messages in thread
From: Petr Oros @ 2026-11-09 9:35 UTC (permalink / raw)
To: netdev
Cc: Petr Oros, Ivan Vecera, Jiri Pirko, Jonathan Corbet, Shuah Khan,
Shuah Khan, Randy Dunlap, Jani Nikula, Vadim Fedorenko,
Arkadiusz Kubalewski, Jiri Pirko, Andrew Lunn, David S. Miller,
Eric Dumazet, Jakub Kicinski, Paolo Abeni, linux-doc,
linux-kselftest
Add a test that reads the phase adjustment range and granularity a
pin advertises, picks the largest multiple of the granularity inside
the range, or the smallest one when that is already the current value,
sets it, reads it back and restores the original value. It skips when
the dpll tool lacks the phase-adjust keyword of pin set and when the
range is too narrow to pick a different value. A pin that reports no
phase adjustment or no range is a skip on a device provided by the user
and a failure on the device the script created, whose pins report
both.
The core refuses a value that is not a multiple of the granularity a
driver reports, the current value included, so a pin that sits at such
a value could not be put back. The test bails out on that before it
schedules the restore, which is deferred right after so that an
interrupted run restores the value too.
The granularity is only reported by drivers that have the core enforce
one. Without it the target is computed with a granularity of 1, and a
driver may refuse it for a granularity of its own or for any other
constraint the interface does not advertise. A refused set is therefore
a skip on a device provided by the user, with the reason the tool
reported, and a failure on the device the script created, whose
simulator accepts every value its range and granularity allow. The
test ends there, a value that was not set has nothing to read back.
Reviewed-by: Ivan Vecera <ivecera@redhat.com>
Acked-by: Jiri Pirko <jiri@nvidia.com>
Signed-off-by: Petr Oros <poros@redhat.com>
---
v3:
- follow the move of the selftests to
tools/testing/selftests/drivers/dpll
v2: https://lore.kernel.org/all/20261105151852.1844305-13-poros@redhat.com/
v1: https://lore.kernel.org/all/20261103093910.596225-13-poros@redhat.com/
---
.../selftests/drivers/dpll/dpll_features.sh | 85 +++++++++++++++++++
.../selftests/drivers/dpll/dpll_lib.sh | 10 +++
2 files changed, 95 insertions(+)
diff --git a/tools/testing/selftests/drivers/dpll/dpll_features.sh b/tools/testing/selftests/drivers/dpll/dpll_features.sh
index 6649936c710e43..b529b19e355dbb 100755
--- a/tools/testing/selftests/drivers/dpll/dpll_features.sh
+++ b/tools/testing/selftests/drivers/dpll/dpll_features.sh
@@ -19,6 +19,7 @@ ALL_TESTS="
test_pin_state_transitions
test_pin_priority_get_set
test_pin_frequency_set_get
+ test_pin_phase_adjust_set_get
"
##############################################################################
@@ -238,6 +239,90 @@ test_pin_state_transitions()
log_test "DPLL pin state transitions"
}
+test_pin_phase_adjust_set_get()
+{
+ local before target after json min max gran reason
+
+ RET=0
+
+ if [ -z "$PIN_TUNE_ID" ]; then
+ dpll_log_test_skip_or_fail "DPLL pin phase-adjust set/get" \
+ "no spare input pin"
+ return
+ fi
+ if ! dpll_pin_set_supports phase-adjust; then
+ log_test_skip "DPLL pin phase-adjust set/get" \
+ "dpll tool lacks pin set phase-adjust"
+ return
+ fi
+
+ before=$(dpll_pin_get_phase_adjust "$PIN_TUNE_ID")
+ if [ -z "$before" ]; then
+ dpll_log_test_skip_or_fail "DPLL pin phase-adjust set/get" \
+ "phase adjust not reported"
+ return
+ fi
+
+ # Pick a target inside the advertised range different from the
+ # current value, a multiple of the granularity, which is taken as 1
+ # when the driver reports none.
+ json=$(dpll_pin_json "$PIN_TUNE_ID")
+ min=$(jq -r '.["phase-adjust-min"] // empty' <<< "$json")
+ max=$(jq -r '.["phase-adjust-max"] // empty' <<< "$json")
+ gran=$(jq -r '.["phase-adjust-gran"] // empty' <<< "$json")
+ if [ -z "$min" ] || [ -z "$max" ]; then
+ dpll_log_test_skip_or_fail "DPLL pin phase-adjust set/get" \
+ "no adjust range"
+ return
+ fi
+ # A current value that is not a granularity multiple could not be
+ # written back, so bail out before the restore is scheduled.
+ if [ -n "$gran" ] && [ $((before % gran)) -ne 0 ]; then
+ dpll_log_test_skip_or_fail "DPLL pin phase-adjust set/get" \
+ "current phase adjust not a granularity multiple"
+ return
+ fi
+ [ -n "$gran" ] || gran=1
+ defer dpll_pin_set_phase_adjust "$PIN_TUNE_ID" "$before"
+
+ # Shell division truncates towards zero, so round the bounds towards
+ # the inside of the range explicitly, which matters for negative ones.
+ target=$((max / gran * gran))
+ [ "$target" -gt "$max" ] && target=$((target - gran))
+ if [ "$target" = "$before" ]; then
+ target=$((min / gran * gran))
+ [ "$target" -lt "$min" ] && target=$((target + gran))
+ fi
+ if [ "$target" = "$before" ] || [ "$target" -gt "$max" ] ||
+ [ "$target" -lt "$min" ]; then
+ log_test_skip "DPLL pin phase-adjust set/get" "range too narrow"
+ return
+ fi
+
+ # A driver may enforce a granularity it does not report or another
+ # constraint, a skip on a device the user provided, the simulator
+ # accepts every value its range and granularity allow.
+ if ! dpll_pin_set_phase_adjust "$PIN_TUNE_ID" "$target"; then
+ reason=${DPLL_LAST_ERR%%$'\n'*}
+ dpll_log_test_skip_or_fail "DPLL pin phase-adjust set/get" \
+ "cannot set phase-adjust=$target${reason:+, $reason}"
+ return
+ fi
+ after=$(dpll_pin_get_phase_adjust "$PIN_TUNE_ID")
+ if [ "$after" != "$target" ]; then
+ check_err 1 "phase-adjust expected $target, got '$after'"
+ fi
+
+ dpll_pin_set_phase_adjust "$PIN_TUNE_ID" "$before"
+ check_err $? "cannot restore phase-adjust"
+ after=$(dpll_pin_get_phase_adjust "$PIN_TUNE_ID")
+ if [ "$after" != "$before" ]; then
+ check_err 1 "phase-adjust not restored, got '$after'"
+ fi
+
+ log_test "DPLL pin phase-adjust set/get"
+}
+
test_pin_frequency_set_get()
{
local before target after reason
diff --git a/tools/testing/selftests/drivers/dpll/dpll_lib.sh b/tools/testing/selftests/drivers/dpll/dpll_lib.sh
index 2583a5aa3fe18d..69b66481394b7a 100644
--- a/tools/testing/selftests/drivers/dpll/dpll_lib.sh
+++ b/tools/testing/selftests/drivers/dpll/dpll_lib.sh
@@ -400,6 +400,11 @@ dpll_pin_get_prio()
__dpll_pin_dev_field "$1" "$2" '.prio'
}
+dpll_pin_get_phase_adjust()
+{
+ __dpll_pin_field "$1" '.["phase-adjust"]'
+}
+
##############################################################################
# Pin setters
#
@@ -436,6 +441,11 @@ dpll_pin_set_frequency()
DPLL_LAST_ERR=$(dpll pin set id "$1" frequency "$2" 2>&1 >/dev/null)
}
+dpll_pin_set_phase_adjust()
+{
+ DPLL_LAST_ERR=$(dpll pin set id "$1" phase-adjust "$2" 2>&1 >/dev/null)
+}
+
##############################################################################
# dpllsim configfs interface
#
--
2.56.0
^ permalink raw reply related [flat|nested] 31+ messages in thread* Re: [PATCH net-next v3 12/14] selftests: dpll: add pin phase adjust test
2026-11-09 9:35 ` [PATCH net-next v3 12/14] selftests: dpll: add pin phase adjust test Petr Oros
@ 2026-11-11 9:37 ` netdev-bot+sashiko
0 siblings, 0 replies; 31+ messages in thread
From: netdev-bot+sashiko @ 2026-11-11 9:37 UTC (permalink / raw)
To: poros
Cc: netdev, ivecera, jiri, corbet, skhan, shuah, rdunlap, jani.nikula,
vadim.fedorenko, arkadiusz.kubalewski, jiri, andrew+netdev, davem,
edumazet, kuba, pabeni, linux-doc, linux-kselftest
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 tools/testing/selftests/drivers/dpll/dpll_features.sh,
test_pin_phase_adjust_set_get() refuses to schedule a restore it…
--
selftests: dpll: add pin phase adjust test
This adds test_pin_phase_adjust_set_get() to the dpll selftests. The test
picks a granularity multiple inside the pin's advertised phase adjust
range, sets it, reads it back and then restores the original value. It
bails out before scheduling the restore when the current value is not a
granularity multiple.
> diff --git a/tools/testing/selftests/drivers/dpll/dpll_features.sh b/tools/testing/selftests/drivers/dpll/dpll_features.sh
> index 6649936c710e4..b529b19e355db 100755
> --- a/tools/testing/selftests/drivers/dpll/dpll_features.sh
> +++ b/tools/testing/selftests/drivers/dpll/dpll_features.sh
[ ... ]
> + json=$(dpll_pin_json "$PIN_TUNE_ID")
> + min=$(jq -r '.["phase-adjust-min"] // empty' <<< "$json")
> + max=$(jq -r '.["phase-adjust-max"] // empty' <<< "$json")
> + gran=$(jq -r '.["phase-adjust-gran"] // empty' <<< "$json")
> + if [ -z "$min" ] || [ -z "$max" ]; then
> + dpll_log_test_skip_or_fail "DPLL pin phase-adjust set/get" \
> + "no adjust range"
> + return
> + fi
> + # A current value that is not a granularity multiple could not be
> + # written back, so bail out before the restore is scheduled.
> + if [ -n "$gran" ] && [ $((before % gran)) -ne 0 ]; then
> + dpll_log_test_skip_or_fail "DPLL pin phase-adjust set/get" \
> + "current phase adjust not a granularity multiple"
> + return
> + fi
> + [ -n "$gran" ] || gran=1
> + defer dpll_pin_set_phase_adjust "$PIN_TUNE_ID" "$before"
[Severity: Medium]
Can the restore be refused when before lies outside [min, max]?
The granularity guard is the only check before the defer. The core also
checks the range, and it does that first, in dpll_pin_phase_adj_set():
drivers/dpll/dpll_netlink.c:dpll_pin_phase_adj_set() {
...
phase_adj = nla_get_s32(phase_adj_attr);
if (phase_adj > pin->prop.phase_range.max ||
phase_adj < pin->prop.phase_range.min) {
NL_SET_ERR_MSG_ATTR(extack, phase_adj_attr,
"phase adjust value of out range");
return -EINVAL;
}
if (pin->prop.phase_gran && phase_adj % (s32)pin->prop.phase_gran) {
...
}
So an out-of-range current value cannot be written back either, the same
as a value that is not a granularity multiple.
On such a pin, target is built inside [min, max] and the set succeeds.
After that, the explicit restore and the deferred one both fail with
-EINVAL. The pin on a user-supplied device then stays at the top (or
bottom) of the range, and the test reports a FAIL instead of a skip.
ice may hit this. ice_dpll_phase_range_set() derives the range from the
CGU abilities. The current value is read separately from firmware via
ice_aq_get_input_pin_cfg() in ice_dpll_pin_state_update(), and nothing
checks that the two agree. If the abilities report 0, the range is
[0, 0], and any nonzero firmware value is out of range.
Should the guard also bail out when before is less than min or greater
than max, before the restore is scheduled?
> +
> + # Shell division truncates towards zero, so round the bounds towards
> + # the inside of the range explicitly, which matters for negative ones.
[ ... ]
> + dpll_pin_set_phase_adjust "$PIN_TUNE_ID" "$before"
> + check_err $? "cannot restore phase-adjust"
[ ... ]
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261109093523.931201-1-poros%40redhat.com
^ permalink raw reply [flat|nested] 31+ messages in thread
* [PATCH net-next v3 13/14] selftests: dpll: add concurrency stress tests
2026-11-09 9:35 [PATCH net-next v3 00/14] dpll: add a simulated DPLL device driver Petr Oros
` (11 preceding siblings ...)
2026-11-09 9:35 ` [PATCH net-next v3 12/14] selftests: dpll: add pin phase adjust test Petr Oros
@ 2026-11-09 9:35 ` Petr Oros
2026-11-11 9:37 ` netdev-bot+sashiko
2026-11-09 9:35 ` [PATCH net-next v3 14/14] selftests: dpll: add reference leak tests Petr Oros
13 siblings, 1 reply; 31+ messages in thread
From: Petr Oros @ 2026-11-09 9:35 UTC (permalink / raw)
To: netdev
Cc: Petr Oros, Ivan Vecera, Jiri Pirko, Jonathan Corbet, Shuah Khan,
Shuah Khan, Randy Dunlap, Jani Nikula, Vadim Fedorenko,
Arkadiusz Kubalewski, Jiri Pirko, Andrew Lunn, David S. Miller,
Eric Dumazet, Jakub Kicinski, Paolo Abeni, linux-doc,
linux-kselftest
Extend the stress script with tests that register and unregister
devices from several processes at once and that keep a number of
devices with many pins live at the same time, checking that every one
of them is visible on netlink.
The workers run in subshells, so the parent cannot track the device a
worker has in flight. Each worker removes it from its own exit trap
instead, and the parent reserves a range of clock IDs for every worker,
so that the devices of a worker cannot collide with those of another
worker or of the parent and whatever a killed worker left behind can
be removed by name, at the end of the test and from the cleanup
handler. The handler stops the recorded workers together with the jobs
of the shell, gives them a few seconds to run their exit traps before
forcing the survivors, and goes on without a worker that does not die,
since one stuck in the kernel would hang the teardown. Deleting a
device treats a part of it that is gone by the time its step runs as
removed, so that a removal running concurrently with the one of a
worker does not turn into a spurious failure. The workers inherit the
open lock file of the library, so the lock is held until the script
and every worker have exited.
Reviewed-by: Ivan Vecera <ivecera@redhat.com>
Acked-by: Jiri Pirko <jiri@nvidia.com>
Signed-off-by: Petr Oros <poros@redhat.com>
---
v3:
- follow the move of the selftests to
tools/testing/selftests/drivers/dpll
v2:
- quote the failed and ret expansions (netdev CI shellcheck)
v2: https://lore.kernel.org/all/20261105151852.1844305-14-poros@redhat.com/
v1: https://lore.kernel.org/all/20261103093910.596225-14-poros@redhat.com/
---
.../selftests/drivers/dpll/dpll_lib.sh | 30 ++-
.../selftests/drivers/dpll/dpll_stress.sh | 219 +++++++++++++++++-
2 files changed, 241 insertions(+), 8 deletions(-)
diff --git a/tools/testing/selftests/drivers/dpll/dpll_lib.sh b/tools/testing/selftests/drivers/dpll/dpll_lib.sh
index 69b66481394b7a..7a0bb144af150c 100644
--- a/tools/testing/selftests/drivers/dpll/dpll_lib.sh
+++ b/tools/testing/selftests/drivers/dpll/dpll_lib.sh
@@ -54,8 +54,10 @@ dpll_lib_check_prereqs()
# The scripts that create dpllsim devices share the one dpllsim directory
# and cannot run alongside each other, so each takes a lock on
# DPLL_LOCK_FILE before it mounts or creates anything and skips when
-# another run holds it. The file lives under /run rather than in the
-# world-writable /tmp, where a planted symlink could redirect the open.
+# another run holds it. The workers a script forks inherit the open file
+# and keep the lock until they exit. The file lives under /run rather
+# than in the world-writable /tmp, where a planted symlink could
+# redirect the open.
DPLL_RUN_DIR="/run/dpll-selftest"
DPLL_LOCK_FILE="$DPLL_RUN_DIR/lock"
DPLL_LOCK_FD=""
@@ -564,8 +566,12 @@ dpllsim_undeploy()
# Take the device offline and remove it together with all its pins.
# Every step is attempted even when an earlier one failed, the return
-# value is non-zero when any did. A device that does not exist counts as
-# deleted.
+# value is non-zero when any did. A device that does not exist counts
+# as deleted, and so does a part of it that is gone by the time its
+# step runs, the live file of the undeploy and the pin or device
+# directory of an rmdir, so that a removal running concurrently, or an
+# earlier one whose later steps failed, does not turn into a spurious
+# failure here.
dpllsim_delete_device()
{
local dev_path="$DPLLSIM_CFG/$1"
@@ -573,12 +579,12 @@ dpllsim_delete_device()
[ -d "$dev_path" ] || return 0
- dpllsim_undeploy "$1" || ret=1
+ dpllsim_undeploy "$1" || [ ! -e "$dev_path/live" ] || ret=1
for pin_path in "$dev_path"/pins/*/; do
[ -d "$pin_path" ] || continue
- rmdir "$pin_path" || ret=1
+ rmdir "$pin_path" || [ ! -d "$pin_path" ] || ret=1
done
- rmdir "$dev_path" || ret=1
+ rmdir "$dev_path" || [ ! -d "$dev_path" ] || ret=1
return "$ret"
}
@@ -591,6 +597,16 @@ dpllsim_alloc_clock_id()
DPLLSIM_CLOCK_ID=$((DPLLSIM_NEXT_CLOCK_ID++))
}
+# Set DPLLSIM_CLOCK_ID to the first of $1 consecutive clock-ids unique
+# within this test run and move the allocator past them, for a worker
+# that derives the clock-ids of its devices from it. A device the parent
+# creates afterwards cannot collide with one of the worker.
+dpllsim_alloc_clock_id_range()
+{
+ DPLLSIM_CLOCK_ID=$DPLLSIM_NEXT_CLOCK_ID
+ DPLLSIM_NEXT_CLOCK_ID=$((DPLLSIM_NEXT_CLOCK_ID + $1))
+}
+
# Create a device with the standard 3-pin layout, deploy it and resolve
# the netlink device id. Sets DPLLSIM_CREATED_NAME, DPLLSIM_CLOCK_ID and
# DPLLSIM_DEV_ID.
diff --git a/tools/testing/selftests/drivers/dpll/dpll_stress.sh b/tools/testing/selftests/drivers/dpll/dpll_stress.sh
index 3806fda2e2dfe0..47b8f4bf55b269 100755
--- a/tools/testing/selftests/drivers/dpll/dpll_stress.sh
+++ b/tools/testing/selftests/drivers/dpll/dpll_stress.sh
@@ -15,13 +15,24 @@ source "$SCRIPTDIR/dpll_lib.sh"
ALL_TESTS="
test_rapid_create_delete
+ test_concurrent_workers
test_pin_state_flooding
+ test_max_devices
test_deploy_cycle
"
STRESS_ITERATIONS=${STRESS_ITERATIONS:-20}
+STRESS_DEVICES=${STRESS_DEVICES:-5}
+# The simulator accepts up to 128 pins per device (DPLLSIM_MAX_PINS)
+STRESS_PINS_PER_DEVICE=${STRESS_PINS_PER_DEVICE:-10}
+STRESS_CONCURRENT_PROCS=${STRESS_CONCURRENT_PROCS:-4}
+STRESS_WORKER_ITERATIONS=${STRESS_WORKER_ITERATIONS:-10}
CREATED_DEVICES=()
+BACKGROUND_PIDS=()
+# Base clock-ids of the workers in flight, their devices are named
+# st<base + iteration>
+WORKER_BASES=()
track_device()
{
@@ -39,18 +50,103 @@ untrack_device()
CREATED_DEVICES=("${remaining[@]}")
}
+# Remove whatever a worker started from the given base clock-id left
+# behind, which is the device in flight when the worker had to be killed.
+# A device that does not exist counts as deleted.
+delete_worker_devices()
+{
+ local base=$1
+ local i name
+
+ for i in $(seq 1 "$STRESS_WORKER_ITERATIONS"); do
+ name="st$((base + i))"
+ dpllsim_delete_device "$name" ||
+ log_info "cannot delete device $name"
+ done
+}
+
+# Return 0 while the process still runs. A process that has exited but
+# has not been reaped yet is a zombie, state Z in /proc/<pid>/stat, and
+# counts as gone, or a wait on it would spin until the forced kill.
+pid_running()
+{
+ local stat
+
+ kill -0 "$1" 2>/dev/null || return 1
+ # The state follows the parenthesized command name, which may
+ # contain spaces and parentheses. The ".*" is greedy, so the
+ # expression strips everything up to the last ") " of the line, and
+ # a ")" inside the name is harmless.
+ stat=$(sed 's/^.*) //' "/proc/$1/stat" 2>/dev/null) || return 1
+ [ "${stat%% *}" != "Z" ]
+}
+
+# Return 0 once none of the given processes runs any more, 1 when some
+# still does after about five seconds.
+pids_gone()
+{
+ local i pid alive
+
+ for i in $(seq 1 50); do
+ alive=0
+ for pid in "$@"; do
+ pid_running "$pid" && alive=1
+ done
+ [ "$alive" -eq 0 ] && return 0
+ sleep 0.1
+ done
+ return 1
+}
+
cleanup()
{
- local dev
+ local pid dev base
+ local pids=() shell_jobs=()
# A second signal must not abort the teardown half way.
trap '' INT TERM
+ # A worker forked right before the signal arrived may not be
+ # recorded yet, so the jobs of the shell are taken as well. The
+ # jobs builtin reports them from a subshell only when it is the
+ # whole command of that subshell.
+ mapfile -t shell_jobs < <(jobs -p)
+ mapfile -t pids < <(printf '%s\n' "${BACKGROUND_PIDS[@]}" \
+ "${shell_jobs[@]}" | grep . | sort -u)
+ for pid in "${pids[@]}"; do
+ kill "$pid" 2>/dev/null
+ done
+ # Give the workers a few seconds to run their exit traps, then
+ # force the survivors and give those a few seconds to die. The
+ # kernel cannot recycle the pid of a child until it is reaped,
+ # which the wait below does, so the forced kill cannot hit a
+ # process that reused one. A worker stuck in the kernel does not
+ # die of the forced kill before whatever it waits for returns, so
+ # only the workers that are gone are waited for, which reaps them,
+ # and the teardown goes on without the rest. The removal of what
+ # such a worker left behind can then fail or race with it, which
+ # is logged like any other failed removal.
+ pids_gone "${pids[@]}"
+ for pid in "${pids[@]}"; do
+ kill -KILL "$pid" 2>/dev/null
+ done
+ pids_gone "${pids[@]}"
+ for pid in "${pids[@]}"; do
+ if pid_running "$pid"; then
+ log_info "worker $pid did not exit, giving up"
+ else
+ wait "$pid" 2>/dev/null
+ fi
+ done
+
defer_scopes_cleanup
for dev in "${CREATED_DEVICES[@]}"; do
dpllsim_delete_device "$dev" ||
log_info "cannot delete device $dev"
done
+ for base in "${WORKER_BASES[@]}"; do
+ delete_worker_devices "$base"
+ done
# The device whose creation a signal interrupted is recorded by the
# library alone, a double delete of a tracked one is harmless.
if [ -n "$DPLLSIM_CREATED_NAME" ]; then
@@ -138,6 +234,83 @@ test_rapid_create_delete()
log_test "DPLL rapid create/delete ($STRESS_ITERATIONS iterations)"
}
+# The exit trap of a worker. The worker runs in a subshell, so
+# track_device() cannot reach CREATED_DEVICES of the parent. Remove the
+# device in flight from the exit trap instead, or killing the worker
+# leaks it. The trap saves the exit status first, so that the removal
+# cannot mask a failed iteration, and drops the traps, since bash runs
+# the EXIT trap again when a signal handler exits.
+worker_exit()
+{
+ local status=$?
+
+ trap - EXIT TERM INT
+ if [ -n "$DPLLSIM_CREATED_NAME" ]; then
+ dpllsim_delete_device "$DPLLSIM_CREATED_NAME"
+ DPLLSIM_CREATED_NAME=""
+ fi
+ exit "$status"
+}
+
+# Create and delete STRESS_WORKER_ITERATIONS devices with clock-ids
+# following the given base, in a subshell. Each worker derives its own
+# unique clock-ids from a range reserved for it, so its devices can never
+# be confused with those of another worker or of the parent. The parent
+# records the base of every worker, so that the device a killed worker
+# left behind can be removed by name.
+concurrent_worker()
+{
+ # The workers inherit the state of the generator from the parent
+ # and would draw the same pin layouts, so reseed it with the pid of
+ # the subshell first.
+ RANDOM=$BASHPID
+ local base=$1
+ local i name failed=0
+
+ trap worker_exit EXIT TERM INT
+
+ for i in $(seq 1 "$STRESS_WORKER_ITERATIONS"); do
+ if ! create_stress_device $((base + i)) 3; then
+ failed=$((failed + 1))
+ continue
+ fi
+ name=$DPLLSIM_CREATED_NAME
+ dpllsim_delete_device "$name" ||
+ failed=$((failed + 1))
+ done
+
+ [ "$failed" -eq 0 ]
+}
+
+test_concurrent_workers()
+{
+ local pid base failed=0
+
+ RET=0
+
+ for _ in $(seq 1 "$STRESS_CONCURRENT_PROCS"); do
+ dpllsim_alloc_clock_id_range $((STRESS_WORKER_ITERATIONS + 1))
+ base=$DPLLSIM_CLOCK_ID
+ WORKER_BASES+=("$base")
+ concurrent_worker "$base" &
+ BACKGROUND_PIDS+=($!)
+ done
+
+ for pid in "${BACKGROUND_PIDS[@]}"; do
+ wait "$pid" || failed=1
+ done
+ BACKGROUND_PIDS=()
+
+ check_err "$failed" "some concurrent workers failed"
+
+ for base in "${WORKER_BASES[@]}"; do
+ delete_worker_devices "$base"
+ done
+ WORKER_BASES=()
+
+ log_test "DPLL concurrent workers ($STRESS_CONCURRENT_PROCS processes)"
+}
+
test_pin_state_flooding()
{
local clock_id name dev_id pin_id i last state fails=0
@@ -194,6 +367,50 @@ test_pin_state_flooding()
log_test "DPLL pin state flooding"
}
+test_max_devices()
+{
+ local i clock_id created=0 visible
+ local test_devs=() clock_ids=()
+
+ RET=0
+
+ for i in $(seq 1 "$STRESS_DEVICES"); do
+ dpllsim_alloc_clock_id; clock_id=$DPLLSIM_CLOCK_ID
+ create_stress_device "$clock_id" \
+ "$STRESS_PINS_PER_DEVICE" || break
+ track_device "$DPLLSIM_CREATED_NAME"
+ test_devs+=("$DPLLSIM_CREATED_NAME")
+ clock_ids+=("$clock_id")
+ created=$((created + 1))
+ done
+
+ if [ "$created" -lt "$STRESS_DEVICES" ]; then
+ check_err 1 "created only $created of $STRESS_DEVICES devices"
+ fi
+
+ # Every created device must be visible on netlink.
+ visible=0
+ for clock_id in "${clock_ids[@]}"; do
+ if dpll_device_wait_by_clock_id "$clock_id" dpllsim \
+ > /dev/null; then
+ visible=$((visible + 1))
+ fi
+ done
+ if [ "$visible" -ne "$created" ]; then
+ check_err 1 "only $visible of $created devices visible"
+ fi
+
+ for i in "${test_devs[@]}"; do
+ if dpllsim_delete_device "$i"; then
+ untrack_device "$i"
+ else
+ check_err 1 "cannot delete device $i"
+ fi
+ done
+
+ log_test "DPLL maximum devices ($STRESS_DEVICES devices)"
+}
+
test_deploy_cycle()
{
local clock_id name i
--
2.56.0
^ permalink raw reply related [flat|nested] 31+ messages in thread* Re: [PATCH net-next v3 13/14] selftests: dpll: add concurrency stress tests
2026-11-09 9:35 ` [PATCH net-next v3 13/14] selftests: dpll: add concurrency stress tests Petr Oros
@ 2026-11-11 9:37 ` netdev-bot+sashiko
0 siblings, 0 replies; 31+ messages in thread
From: netdev-bot+sashiko @ 2026-11-11 9:37 UTC (permalink / raw)
To: poros
Cc: netdev, ivecera, jiri, corbet, skhan, shuah, rdunlap, jani.nikula,
vadim.fedorenko, arkadiusz.kubalewski, jiri, andrew+netdev, davem,
edumazet, kuba, pabeni, linux-doc, linux-kselftest
Thank you for your contribution! Sashiko AI review found 3 potential
issue(s) to consider.
Critical: 0 · High: 0 · Medium: 1 · Low: 2
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 tools/testing/selftests/drivers/dpll/dpll_stress.sh,
cleanup() can send SIGTERM and then SIGKILL to PIDs that bash has…
- [Low] test_max_devices() in
tools/testing/selftests/drivers/dpll/dpll_stress.sh reports itself as…
- [Low] cleanup() in tools/testing/selftests/drivers/dpll/dpll_stress.sh
signals only the PID of each worker subshell, never the external…
--
selftests: dpll: add concurrency stress tests
This adds two tests to dpll_stress.sh. One registers and unregisters
dpllsim devices from several background workers at once. The other
keeps several devices with many pins live and checks that each one is
visible on netlink.
The workers clean up from their own exit trap, and cleanup() stops
recorded workers and shell jobs before it removes their devices by name.
> diff --git a/tools/testing/selftests/drivers/dpll/dpll_stress.sh b/tools/testing/selftests/drivers/dpll/dpll_stress.sh
> index 3806fda2e2dfe..47b8f4bf55b26 100755
> --- a/tools/testing/selftests/drivers/dpll/dpll_stress.sh
> +++ b/tools/testing/selftests/drivers/dpll/dpll_stress.sh
[ ... ]
> @@ -39,18 +50,103 @@ untrack_device()
[ ... ]
> cleanup()
> {
> - local dev
> + local pid dev base
> + local pids=() shell_jobs=()
[ ... ]
> + mapfile -t shell_jobs < <(jobs -p)
> + mapfile -t pids < <(printf '%s\n' "${BACKGROUND_PIDS[@]}" \
> + "${shell_jobs[@]}" | grep . | sort -u)
> + for pid in "${pids[@]}"; do
> + kill "$pid" 2>/dev/null
> + done
> + # Give the workers a few seconds to run their exit traps, then
> + # force the survivors and give those a few seconds to die. The
> + # kernel cannot recycle the pid of a child until it is reaped,
> + # which the wait below does, so the forced kill cannot hit a
> + # process that reused one. A worker stuck in the kernel does not
> + # die of the forced kill before whatever it waits for returns, so
> + # only the workers that are gone are waited for, which reaps them,
> + # and the teardown goes on without the rest. The removal of what
> + # such a worker left behind can then fail or race with it, which
> + # is logged like any other failed removal.
> + pids_gone "${pids[@]}"
> + for pid in "${pids[@]}"; do
> + kill -KILL "$pid" 2>/dev/null
> + done
[Severity: Medium]
Does bash really leave the pid unreaped until the wait below? Bash reaps
exited background children itself. It does this from its SIGCHLD handler
and from waitpid(-1) whenever it waits for a foreground command. It keeps
their status in its bgpids table for a later wait.
pids_gone() runs seq, $(sed ...) and sleep 0.1 on every pass. A worker
that exits after the SIGTERM can therefore be reaped, and its pid freed,
before this loop sends SIGKILL to every entry of pids[]. The loop does
not check pid_running() first.
test_concurrent_workers() makes the window wider:
for pid in "${BACKGROUND_PIDS[@]}"; do
wait "$pid" || failed=1
done
BACKGROUND_PIDS=()
Workers that finish early are reaped right away, but they stay in
BACKGROUND_PIDS until the last wait returns. Suppose a harness timeout or
Ctrl-C arrives while the parent is still waiting on a slower worker.
cleanup() then sends SIGTERM and later SIGKILL, as root, to those stale
pids.
If one of those pids was handed out again after a pid-space wrap, could
this kill an unrelated process?
pid_running() relies on kill -0 and /proc/<pid>/stat, so a reused pid
also looks like a live worker. In that case pids_gone() would spin for
the full five seconds and log a false "worker N did not exit".
> + pids_gone "${pids[@]}"
> + for pid in "${pids[@]}"; do
> + if pid_running "$pid"; then
> + log_info "worker $pid did not exit, giving up"
> + else
> + wait "$pid" 2>/dev/null
> + fi
> + done
> +
> defer_scopes_cleanup
> for dev in "${CREATED_DEVICES[@]}"; do
> dpllsim_delete_device "$dev" ||
> log_info "cannot delete device $dev"
> done
> + for base in "${WORKER_BASES[@]}"; do
> + delete_worker_devices "$base"
> + done
[Severity: Low]
Does the forced kill above reach the external helpers that a worker runs?
In dpll_lib.sh, dpllsim_create_device() and dpllsim_add_pin() create the
configfs entries with an external mkdir. dpllsim_delete_device() removes
them with an external rmdir.
The commit message describes a worker "stuck in the kernel". Take the
case where a worker's mkdir blocks in configfs. Bash holds back the
worker's TERM trap until the foreground child returns. kill -KILL then
kills only the subshell, and the mkdir child keeps running as an orphan.
The second pids_gone() and pid_running() check only the subshell pid.
The worker therefore counts as gone and is reaped, and "worker $pid did
not exit, giving up" is never logged.
cleanup() then runs delete_worker_devices() and dpll_lib_cleanup() while
the orphan is still busy. If its mkdir completes afterwards, can a
dpllsim device or pin directory appear after the last removal pass and
be leaked?
The in-flight mkdir would also keep the configfs mount busy, so the
unmount in dpll_lib_cleanup() may fail.
This seems to contradict this part of the commit message:
"whatever a killed worker left behind can be removed by name, at the
end of the test and from the cleanup handler"
Would it help to start each worker in its own process group and signal
the whole group, or to track the helper processes?
[ ... ]
> @@ -194,6 +367,50 @@ test_pin_state_flooding()
> log_test "DPLL pin state flooding"
> }
>
> +test_max_devices()
> +{
[ ... ]
> + # Every created device must be visible on netlink.
> + visible=0
> + for clock_id in "${clock_ids[@]}"; do
> + if dpll_device_wait_by_clock_id "$clock_id" dpllsim \
> + > /dev/null; then
> + visible=$((visible + 1))
> + fi
> + done
[ ... ]
> + log_test "DPLL maximum devices ($STRESS_DEVICES devices)"
> +}
[Severity: Low]
This isn't a bug, but is "maximum devices" the right name for this test?
dpllsim has no limit on the number of devices. Its only per-device limit
is DPLLSIM_MAX_PINS:
drivers/dpll/dpllsim/core.c:dpllsim_pins_make_item() {
...
if (list_count_nodes(&sim->pin_list) >= DPLLSIM_MAX_PINS)
return ERR_PTR(-ENOSPC);
...
}
By default the test creates 5 devices with 10 pins each. It never
reaches that limit, and it never checks that a pin beyond the limit is
rejected. The comment above STRESS_PINS_PER_DEVICE mentions the 128 pin
limit, but the default is well below it.
The visibility check also only looks up the devices with
dpll_device_wait_by_clock_id(). It does not check their pins, so the
test would still pass if the pins of these many-pin devices did not
appear on netlink.
Could the test be renamed, or extended to cover the pin limit and to
check that the pins are visible?
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261109093523.931201-1-poros%40redhat.com
^ permalink raw reply [flat|nested] 31+ messages in thread
* [PATCH net-next v3 14/14] selftests: dpll: add reference leak tests
2026-11-09 9:35 [PATCH net-next v3 00/14] dpll: add a simulated DPLL device driver Petr Oros
` (12 preceding siblings ...)
2026-11-09 9:35 ` [PATCH net-next v3 13/14] selftests: dpll: add concurrency stress tests Petr Oros
@ 2026-11-09 9:35 ` Petr Oros
2026-11-09 14:00 ` Ivan Vecera
2026-11-11 9:37 ` netdev-bot+sashiko
13 siblings, 2 replies; 31+ messages in thread
From: Petr Oros @ 2026-11-09 9:35 UTC (permalink / raw)
To: netdev
Cc: Petr Oros, Jiri Pirko, Jonathan Corbet, Shuah Khan, Shuah Khan,
Randy Dunlap, Jani Nikula, Vadim Fedorenko, Arkadiusz Kubalewski,
Jiri Pirko, Ivan Vecera, Andrew Lunn, David S. Miller,
Eric Dumazet, Jakub Kicinski, Paolo Abeni, linux-doc,
linux-kselftest
With CONFIG_DPLL_REFCNT_TRACKER the DPLL core keeps the stack traces of
unreleased device and pin references in debugfs. The entries do not
name their owner, so add library helpers that snapshot the entries
present before a test creates its devices and fail when an entry that
is not in the snapshot still holds references afterwards. An entry in
the snapshot belongs to an object of another driver and is ignored
even when its content changed, since other drivers take and drop
references while the tests run. The teardown of a device releases its
references before the write to live returns, so the entries are
expected to be gone at the first look; the check still polls for a
while before it reports a leak, so that it does not fail on a single
read. Attributing
by name means that a leak of an earlier run goes unnoticed and that an
object reusing the address of a freed one from the snapshot can hide a
leak, which is accepted, the entries carry nothing better to go by. A
second run alongside would have its devices attributed to this one,
which is one more reason for the lock every script takes.
Whether the tracker works is probed with a throwaway device, which has
to show entries while it is live and to drain them once it is deleted,
so that a kernel without the tracker is told apart from a broken
simulator and from a leak. The probe runs as the first test of the
stress script, which gains a final leak check, and of the new
dpll_reftrack.sh, which registers a device, removes it and verifies
that no reference survived. The snapshot file lives next to the lock
file under /run, for the same reason the lock file does not live in
/tmp. The library mounts debugfs when nothing else did, like it does
for configfs, and the selftest config enables the tracker together
with CONFIG_DEBUG_KERNEL, which it depends on, and debugfs.
Acked-by: Jiri Pirko <jiri@nvidia.com>
Signed-off-by: Petr Oros <poros@redhat.com>
---
v3:
- say what the leak check waits for instead of a concurrent dump
holding references, which nothing does (Ivan Vecera)
- follow the move of the selftests to
tools/testing/selftests/drivers/dpll
v2:
- keep the config file sorted and quote the ret expansions (netdev CI
shellcheck)
v2: https://lore.kernel.org/all/20261105151852.1844305-15-poros@redhat.com/
v1: https://lore.kernel.org/all/20261103093910.596225-15-poros@redhat.com/
---
tools/testing/selftests/drivers/dpll/Makefile | 1 +
tools/testing/selftests/drivers/dpll/config | 3 +
.../selftests/drivers/dpll/dpll_lib.sh | 263 ++++++++++++++++--
.../selftests/drivers/dpll/dpll_reftrack.sh | 184 ++++++++++++
.../selftests/drivers/dpll/dpll_stress.sh | 91 +++++-
5 files changed, 520 insertions(+), 22 deletions(-)
create mode 100755 tools/testing/selftests/drivers/dpll/dpll_reftrack.sh
diff --git a/tools/testing/selftests/drivers/dpll/Makefile b/tools/testing/selftests/drivers/dpll/Makefile
index 1349d078fa2660..d292ea049edfa3 100644
--- a/tools/testing/selftests/drivers/dpll/Makefile
+++ b/tools/testing/selftests/drivers/dpll/Makefile
@@ -10,6 +10,7 @@ TEST_FILES := dpll_lib.sh
TEST_PROGS := \
dpll_basic.sh \
dpll_features.sh \
+ dpll_reftrack.sh \
dpll_stress.sh \
# end of TEST_PROGS
diff --git a/tools/testing/selftests/drivers/dpll/config b/tools/testing/selftests/drivers/dpll/config
index e2c90b6970bd43..71931de5cfdb2e 100644
--- a/tools/testing/selftests/drivers/dpll/config
+++ b/tools/testing/selftests/drivers/dpll/config
@@ -1,3 +1,6 @@
CONFIG_CONFIGFS_FS=y
+CONFIG_DEBUG_FS=y
+CONFIG_DEBUG_KERNEL=y
CONFIG_DPLL=y
+CONFIG_DPLL_REFCNT_TRACKER=y
CONFIG_DPLLSIM=m
diff --git a/tools/testing/selftests/drivers/dpll/dpll_lib.sh b/tools/testing/selftests/drivers/dpll/dpll_lib.sh
index 7a0bb144af150c..87686bb53d3553 100644
--- a/tools/testing/selftests/drivers/dpll/dpll_lib.sh
+++ b/tools/testing/selftests/drivers/dpll/dpll_lib.sh
@@ -15,6 +15,8 @@
CONFIGFS_DIR="/sys/kernel/config"
DPLLSIM_CFG="$CONFIGFS_DIR/dpllsim"
+DEBUGFS_DIR="/sys/kernel/debug"
+DPLL_REF_TRACKER_DIR="$DEBUGFS_DIR/ref_tracker"
# Next clock-id handed out by dpllsim_alloc_clock_id. The lookups of the
# dpllsim helpers match on the module name as well, which is what keeps
@@ -52,12 +54,12 @@ dpll_lib_check_prereqs()
}
# The scripts that create dpllsim devices share the one dpllsim directory
-# and cannot run alongside each other, so each takes a lock on
-# DPLL_LOCK_FILE before it mounts or creates anything and skips when
-# another run holds it. The workers a script forks inherit the open file
-# and keep the lock until they exit. The file lives under /run rather
-# than in the world-writable /tmp, where a planted symlink could
-# redirect the open.
+# and the leak check attributes every new ref_tracker entry to the run,
+# so each takes a lock on DPLL_LOCK_FILE before it mounts or creates
+# anything and skips when another run holds it. The workers a script
+# forks inherit the open file and keep the lock until they exit. The
+# files live under /run rather than in the world-writable /tmp, where a
+# planted symlink could redirect the open.
DPLL_RUN_DIR="/run/dpll-selftest"
DPLL_LOCK_FILE="$DPLL_RUN_DIR/lock"
DPLL_LOCK_FD=""
@@ -457,9 +459,10 @@ dpll_pin_set_phase_adjust()
# structure (pins, clock_id, pin attributes) can only be changed while the
# device is not live.
#
-# dpllsim_available mounts configfs when nothing else did, which is the
-# one side effect the library has outside the dpllsim directory. Scripts
-# call dpll_lib_cleanup last from their exit trap to undo it.
+# dpllsim_available mounts configfs when nothing else did, like
+# dpll_debugfs_available below does for debugfs, which are the only side
+# effects the library has outside the dpllsim directory. Scripts call
+# dpll_lib_cleanup last from their exit trap to undo them.
DPLLSIM_MOUNTED_CONFIGFS=0
@@ -475,24 +478,38 @@ dpllsim_available()
[ -d "$DPLLSIM_CFG" ]
}
-# Unmount configfs again when dpllsim_available mounted it. Call after
-# the devices of the script are gone. A dpllsim device the script could
-# not remove keeps the mount, so that what is left can be looked at.
+__dpll_lib_umount()
+{
+ umount "$1" 2>/dev/null || log_info "cannot unmount $1"
+}
+
+# Unmount configfs and debugfs again when the library mounted them. Call
+# after the devices of the script are gone. A dpllsim device the script
+# could not remove keeps both mounted, so that what is left can be
+# looked at, the device in configfs and the references it still holds
+# in the ref_tracker entries of debugfs.
dpll_lib_cleanup()
{
- local dev_path name
+ local dev_path name dir kept
+ local mounted=()
- [ "$DPLLSIM_MOUNTED_CONFIGFS" -eq 1 ] || return 0
+ [ "$DPLLSIM_MOUNTED_CONFIGFS" -eq 1 ] && mounted+=("$CONFIGFS_DIR")
+ [ "$DPLL_MOUNTED_DEBUGFS" -eq 1 ] && mounted+=("$DEBUGFS_DIR")
DPLLSIM_MOUNTED_CONFIGFS=0
+ DPLL_MOUNTED_DEBUGFS=0
+ [ "${#mounted[@]}" -gt 0 ] || return 0
for dev_path in "$DPLLSIM_CFG"/*/; do
[ -d "$dev_path" ] || continue
name=$(basename "$dev_path")
- log_info "device $name left behind, $CONFIGFS_DIR kept mounted"
+ kept=${mounted[*]}
+ kept="${kept/ / and } kept mounted"
+ log_info "device $name left behind, $kept"
return
done
- umount "$CONFIGFS_DIR" 2>/dev/null ||
- log_info "cannot unmount $CONFIGFS_DIR"
+ for dir in "${mounted[@]}"; do
+ __dpll_lib_umount "$dir"
+ done
}
dpllsim_check_module()
@@ -641,3 +658,215 @@ dpllsim_setup_standard_device()
return 1
fi
}
+
+##############################################################################
+# ref_tracker leak checking
+#
+# With CONFIG_DPLL_REFCNT_TRACKER=y the DPLL core creates regular files
+# named dpll_device@<ptr> / dpll_pin@<ptr> under the ref_tracker debugfs
+# dir. Reading one prints the stack traces of currently unreleased
+# references; an empty (or removed) file means no leaks.
+#
+# The entries do not name their owner, so leaks are attributed by
+# snapshot, a non-empty entry whose name is not in the snapshot taken
+# before the test created its devices is a leak, an entry that is in it
+# belongs to another driver and is ignored even when its content changed.
+
+DPLL_MOUNTED_DEBUGFS=0
+# The snapshot files are kept in DPLL_RUN_DIR, see the lock above for
+# why that is not /tmp, as refsnap.<pid>.<random>.
+DPLL_REF_SNAPSHOT_PREFIX="$DPLL_RUN_DIR/refsnap"
+# Name of the probe device while it exists, so that the cleanup handler
+# of a script can remove it when the probe could not.
+DPLLSIM_PROBE_DEVICE=""
+
+# Create the file a snapshot is recorded in and print its name. The
+# scripts take the lock first, which creates DPLL_RUN_DIR. A run that
+# is killed with SIGKILL cannot remove its file, so the files of earlier
+# runs whose process is gone are removed first, the pid of the run is
+# the component after the first dot of the name. A pid that was reused
+# by a process that lives long would keep such a file forever, so files
+# older than a day are removed whatever their pid is. Return 1 when the
+# file cannot be created.
+dpll_ref_tracker_snapshot_file()
+{
+ local file pid
+
+ find "$DPLL_RUN_DIR" -maxdepth 1 \
+ -name "${DPLL_REF_SNAPSHOT_PREFIX##*/}.*" -mmin +1440 -delete \
+ 2>/dev/null
+ for file in "$DPLL_REF_SNAPSHOT_PREFIX".*; do
+ [ -f "$file" ] || continue
+ pid=${file#"$DPLL_REF_SNAPSHOT_PREFIX".}
+ pid=${pid%%.*}
+ [[ "$pid" =~ ^[0-9]+$ ]] || continue
+ [ -d "/proc/$pid" ] || rm -f "$file"
+ done
+ mktemp "$DPLL_REF_SNAPSHOT_PREFIX.$$.XXXXXX"
+}
+
+# Mount debugfs when nothing else did, so that the ref_tracker entries
+# can be read, and remember that for dpll_lib_cleanup. Return 0 when
+# debugfs is mounted afterwards, whether the tracker dir exists is for
+# the caller to check.
+dpll_debugfs_available()
+{
+ if ! mountpoint -q "$DEBUGFS_DIR"; then
+ mount -t debugfs none "$DEBUGFS_DIR" &> /dev/null &&
+ DPLL_MOUNTED_DEBUGFS=1
+ fi
+ mountpoint -q "$DEBUGFS_DIR"
+}
+
+__dpll_ref_tracker_hash()
+{
+ printf '%s' "$1" | md5sum | cut -d' ' -f1
+}
+
+# Write "<name> <content hash>" for every existing dpll_device@* /
+# dpll_pin@* entry to the given file, one entry per line. The file ends
+# up empty when the tracker dir is missing.
+dpll_ref_tracker_snapshot()
+{
+ local file=$1
+ local entry content
+
+ for entry in "$DPLL_REF_TRACKER_DIR"/dpll_device@* \
+ "$DPLL_REF_TRACKER_DIR"/dpll_pin@*; do
+ [ -f "$entry" ] || continue
+ content=$(cat "$entry" 2>/dev/null)
+ echo "$(basename "$entry")" \
+ "$(__dpll_ref_tracker_hash "$content")"
+ done > "$file"
+}
+
+# Print the leak candidates, the non-empty entries whose name is not in
+# the snapshot, each followed by its content. Prints nothing when there
+# is no candidate.
+__dpll_ref_tracker_new_entries()
+{
+ local file=$1
+ local entry name content
+
+ for entry in "$DPLL_REF_TRACKER_DIR"/dpll_device@* \
+ "$DPLL_REF_TRACKER_DIR"/dpll_pin@*; do
+ [ -f "$entry" ] || continue
+ name=$(basename "$entry")
+ grep -q "^$name " "$file" && continue
+ content=$(cat "$entry" 2>/dev/null)
+ [ -n "$content" ] || continue
+ echo "Unreleased references in $name:"
+ echo "$content"
+ done
+}
+
+# Return 0 when there is no leak candidate. Otherwise print the
+# candidates and return 1, busywait hands the output of its last failed
+# check to the caller when it times out.
+__dpll_ref_tracker_settled()
+{
+ local leaks
+
+ leaks=$(__dpll_ref_tracker_new_entries "$1")
+ [ -z "$leaks" ] && return 0
+
+ echo "$leaks"
+ return 1
+}
+
+# Log the entries of the snapshot whose content changed since and record
+# the new content, so that the same change is logged once.
+__dpll_ref_tracker_note_changed()
+{
+ local file=$1
+ local entry name content hash changed=""
+
+ for entry in "$DPLL_REF_TRACKER_DIR"/dpll_device@* \
+ "$DPLL_REF_TRACKER_DIR"/dpll_pin@*; do
+ [ -f "$entry" ] || continue
+ name=$(basename "$entry")
+ grep -q "^$name " "$file" || continue
+ content=$(cat "$entry" 2>/dev/null)
+ hash=$(__dpll_ref_tracker_hash "$content")
+ grep -qx "$name $hash" "$file" && continue
+ sed -i "s/^$name .*/$name $hash/" "$file"
+ changed="$changed $name"
+ done
+
+ [ -n "$changed" ] && log_info \
+ "references of another driver changed (ignored):$changed"
+ return 0
+}
+
+# $1: snapshot file written by dpll_ref_tracker_snapshot before the
+# objects under test were created. Return 0 when no reference to an
+# object created since the snapshot is held any more. The teardown of a
+# device releases its references before the write to live returns, so
+# the entries are expected to be gone at the first look; the check still
+# polls for a while rather than failing on that look, so that a leak is
+# reported only when the references stay. A timeout means they never
+# drained, the candidates seen by the last check are then printed and
+# count as leaked.
+dpll_ref_tracker_check_leaks()
+{
+ local file=$1
+ local leaks ret=0
+
+ [ -d "$DPLL_REF_TRACKER_DIR" ] || return 0
+
+ if ! leaks=$(busywait "$BUSYWAIT_TIMEOUT" __dpll_ref_tracker_settled \
+ "$file"); then
+ echo "References not released within $BUSYWAIT_TIMEOUT ms:"
+ echo "$leaks"
+ ret=1
+ fi
+ __dpll_ref_tracker_note_changed "$file"
+
+ return "$ret"
+}
+
+# Probe whether the kernel tracks DPLL references with a throwaway
+# dpllsim device, the entries that exist before it are recorded in the
+# scratch file $1. Return 0 when the tracker works, 1 when no entry
+# appeared (no CONFIG_DPLL_REFCNT_TRACKER), 2 when the probe device
+# could not be created or deployed, 3 when its entries did not drain
+# after its deletion and 4 when it could not be deleted, its name then
+# stays in DPLLSIM_PROBE_DEVICE for the cleanup handler to retry. Must
+# not be called from a subshell, it allocates clock-ids.
+dpll_ref_tracker_probe()
+{
+ local file=$1
+ local name attempt ret=2
+
+ dpll_ref_tracker_snapshot "$file"
+
+ # A directory of that name left behind by an earlier run takes a
+ # fresh clock-id, anything else is a broken simulator.
+ for attempt in 1 2 3; do
+ dpllsim_alloc_clock_id
+ name="probe$DPLLSIM_CLOCK_ID"
+ dpllsim_create_device "$name" "$DPLLSIM_CLOCK_ID" && break
+ [ -d "$DPLLSIM_CFG/$name" ] || return 2
+ [ "$attempt" -lt 3 ] || return 2
+ done
+ DPLLSIM_PROBE_DEVICE=$name
+
+ if dpllsim_add_pin "$name" ext0 ext 10000000 input &&
+ dpllsim_deploy "$name"; then
+ ret=1
+ [ -n "$(__dpll_ref_tracker_new_entries "$file")" ] && ret=0
+ fi
+
+ if dpllsim_delete_device "$name"; then
+ DPLLSIM_PROBE_DEVICE=""
+ else
+ log_info "cannot delete device $name"
+ return 4
+ fi
+ if [ "$ret" -eq 0 ] &&
+ ! busywait "$BUSYWAIT_TIMEOUT" __dpll_ref_tracker_settled "$file" \
+ > /dev/null; then
+ ret=3
+ fi
+ return "$ret"
+}
diff --git a/tools/testing/selftests/drivers/dpll/dpll_reftrack.sh b/tools/testing/selftests/drivers/dpll/dpll_reftrack.sh
new file mode 100755
index 00000000000000..614020ac213485
--- /dev/null
+++ b/tools/testing/selftests/drivers/dpll/dpll_reftrack.sh
@@ -0,0 +1,184 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# DPLL reference tracking test. With CONFIG_DPLL_REFCNT_TRACKER=y the DPLL
+# core tracks device and pin references through ref_tracker; the debugfs
+# files dpll_device@<ptr> / dpll_pin@<ptr> list the stack traces of
+# currently unreleased references. This test records the entries that
+# exist before it starts, registers dpllsim devices and verifies that
+# every entry that appeared since is released again after unregistration.
+#
+# Copyright (c) 2026 Red Hat, Inc.
+
+SCRIPTDIR=$(dirname "$(readlink -e "${BASH_SOURCE[0]}")")
+
+source "$SCRIPTDIR/../../net/lib.sh"
+source "$SCRIPTDIR/dpll_lib.sh"
+
+ALL_TESTS="
+ test_ref_tracker_probe
+ test_basic_reftrack
+ test_final_leak_check
+"
+
+CREATED_DEVICE=""
+
+# The device stays recorded when its removal fails, so that the cleanup
+# handler tries again.
+destroy_device()
+{
+ [ -n "$CREATED_DEVICE" ] || return 0
+
+ dpllsim_delete_device "$CREATED_DEVICE" || return 1
+ CREATED_DEVICE=""
+}
+
+cleanup()
+{
+ # A second signal must not abort the teardown half way.
+ trap '' INT TERM
+
+ defer_scopes_cleanup
+ destroy_device || log_info "cannot delete device $CREATED_DEVICE"
+ # The device whose creation a signal interrupted is recorded by the
+ # library alone, a double delete of a tracked one is harmless.
+ if [ -n "$DPLLSIM_CREATED_NAME" ]; then
+ dpllsim_delete_device "$DPLLSIM_CREATED_NAME" ||
+ log_info "cannot delete device $DPLLSIM_CREATED_NAME"
+ fi
+ if [ -n "$DPLLSIM_PROBE_DEVICE" ]; then
+ dpllsim_delete_device "$DPLLSIM_PROBE_DEVICE" ||
+ log_info "cannot delete device $DPLLSIM_PROBE_DEVICE"
+ fi
+ [ -n "$REF_SNAPSHOT" ] && rm -f "$REF_SNAPSHOT"
+ dpll_lib_cleanup
+}
+
+##############################################################################
+
+# Probe for the reference tracker and record the entries that exist
+# before the first test that creates devices, so that every entry that
+# appears from here on is attributed to this script. A probe device
+# that cannot be created says nothing about the tracker, the simulator
+# is broken. A probe device whose references do not drain is the kind
+# of leak the tests look for, found before the first test. One that
+# cannot be deleted is a failure too, it stays recorded for the cleanup
+# handler to try again. The tests skip when REF_TRACKER is 0 afterwards.
+test_ref_tracker_probe()
+{
+ RET=0
+
+ REF_TRACKER=0
+ if ! dpll_debugfs_available || ! [ -d "$DPLL_REF_TRACKER_DIR" ]; then
+ log_test_skip "DPLL ref_tracker probe" \
+ "ref_tracker debugfs not available"
+ return
+ fi
+
+ # The snapshot file doubles as the scratch file of the probe, so
+ # it is needed before anything is recorded.
+ if ! REF_SNAPSHOT=$(dpll_ref_tracker_snapshot_file); then
+ check_err 1 "cannot create a temporary file for the snapshot"
+ log_test "DPLL ref_tracker probe"
+ return
+ fi
+
+ dpll_ref_tracker_probe "$REF_SNAPSHOT"
+ case $? in
+ 0)
+ REF_TRACKER=1
+ ;;
+ 1)
+ log_test_skip "DPLL ref_tracker probe" \
+ "CONFIG_DPLL_REFCNT_TRACKER not enabled"
+ return
+ ;;
+ 3)
+ REF_TRACKER=1
+ check_err 1 "references of the probe device not released"
+ ;;
+ 4)
+ check_err 1 "cannot delete the dpllsim probe device"
+ ;;
+ *)
+ check_err 1 "cannot create dpllsim probe device"
+ ;;
+ esac
+ dpll_ref_tracker_snapshot "$REF_SNAPSHOT"
+
+ log_test "DPLL ref_tracker probe"
+}
+
+test_basic_reftrack()
+{
+ RET=0
+
+ if [ "$REF_TRACKER" -eq 0 ]; then
+ log_test_skip "DPLL basic reference tracking" \
+ "DPLL reference tracker not available"
+ return
+ fi
+
+ if ! dpllsim_setup_standard_device; then
+ check_err 1 "cannot create dpllsim device"
+ log_test "DPLL basic reference tracking"
+ return
+ fi
+ CREATED_DEVICE=$DPLLSIM_CREATED_NAME
+
+ destroy_device
+ check_err $? "cannot delete device $CREATED_DEVICE"
+
+ if ! dpll_device_wait_gone_by_clock_id "$DPLLSIM_CLOCK_ID" dpllsim; then
+ check_err 1 "device still on netlink after delete"
+ fi
+
+ dpll_ref_tracker_check_leaks "$REF_SNAPSHOT"
+ check_err $? "leaked references after device removal"
+
+ log_test "DPLL basic reference tracking"
+}
+
+test_final_leak_check()
+{
+ RET=0
+
+ if [ "$REF_TRACKER" -eq 0 ]; then
+ log_test_skip "DPLL final ref_tracker check" \
+ "DPLL reference tracker not available"
+ return
+ fi
+ # A device an earlier test could not remove still holds its
+ # references, the failure to report is that one.
+ if [ -n "$CREATED_DEVICE" ]; then
+ log_test_skip "DPLL final ref_tracker check" \
+ "device from an earlier test still present"
+ return
+ fi
+
+ dpll_ref_tracker_check_leaks "$REF_SNAPSHOT"
+ check_err $? "leaked DPLL references at end of test"
+
+ log_test "DPLL final ref_tracker check"
+}
+
+##############################################################################
+
+dpll_lib_check_prereqs
+# Another run would find the devices of this one left behind and see
+# them as leaks, see the lock in the library.
+dpll_lib_lock
+
+REF_SNAPSHOT=""
+REF_TRACKER=0
+# The module check and the probe test may mount configfs and debugfs, so
+# the exit trap that unmounts them again has to be in place before. INT
+# and TERM exit through it as well, so that a timeout imposed by the
+# harness still removes the device.
+trap cleanup EXIT
+trap 'exit 1' INT TERM
+dpllsim_check_module
+
+tests_run
+
+exit $EXIT_STATUS
diff --git a/tools/testing/selftests/drivers/dpll/dpll_stress.sh b/tools/testing/selftests/drivers/dpll/dpll_stress.sh
index 47b8f4bf55b269..198a50ed678733 100755
--- a/tools/testing/selftests/drivers/dpll/dpll_stress.sh
+++ b/tools/testing/selftests/drivers/dpll/dpll_stress.sh
@@ -14,11 +14,13 @@ source "$SCRIPTDIR/../../net/lib.sh"
source "$SCRIPTDIR/dpll_lib.sh"
ALL_TESTS="
+ test_ref_tracker_probe
test_rapid_create_delete
test_concurrent_workers
test_pin_state_flooding
test_max_devices
test_deploy_cycle
+ test_ref_leaks
"
STRESS_ITERATIONS=${STRESS_ITERATIONS:-20}
@@ -153,6 +155,11 @@ cleanup()
dpllsim_delete_device "$DPLLSIM_CREATED_NAME" ||
log_info "cannot delete device $DPLLSIM_CREATED_NAME"
fi
+ if [ -n "$DPLLSIM_PROBE_DEVICE" ]; then
+ dpllsim_delete_device "$DPLLSIM_PROBE_DEVICE" ||
+ log_info "cannot delete device $DPLLSIM_PROBE_DEVICE"
+ fi
+ [ -n "$REF_SNAPSHOT" ] && rm -f "$REF_SNAPSHOT"
dpll_lib_cleanup
}
@@ -208,6 +215,60 @@ create_stress_device()
##############################################################################
+# Probe for the reference tracker and record the entries that exist
+# before the first test that creates devices, so that the final check
+# attributes every entry that appears from here on to this script. A
+# probe device that cannot be created says nothing about the tracker,
+# the simulator is broken. A probe device whose references do not drain
+# is the kind of leak the final check looks for, found before the first
+# test. One that cannot be deleted is a failure too, it stays recorded
+# for the cleanup handler to try again. REF_TRACKER tells the leak
+# check whether the tracker was seen at work.
+test_ref_tracker_probe()
+{
+ RET=0
+
+ REF_TRACKER=0
+ if ! dpll_debugfs_available || ! [ -d "$DPLL_REF_TRACKER_DIR" ]; then
+ log_test_skip "DPLL ref_tracker probe" \
+ "ref_tracker debugfs not available"
+ return
+ fi
+
+ # The snapshot file doubles as the scratch file of the probe, so
+ # it is needed before anything is recorded.
+ if ! REF_SNAPSHOT=$(dpll_ref_tracker_snapshot_file); then
+ check_err 1 "cannot create a temporary file for the snapshot"
+ log_test "DPLL ref_tracker probe"
+ return
+ fi
+
+ dpll_ref_tracker_probe "$REF_SNAPSHOT"
+ case $? in
+ 0)
+ REF_TRACKER=1
+ ;;
+ 1)
+ log_test_skip "DPLL ref_tracker probe" \
+ "CONFIG_DPLL_REFCNT_TRACKER not enabled"
+ return
+ ;;
+ 3)
+ REF_TRACKER=1
+ check_err 1 "references of the probe device not released"
+ ;;
+ 4)
+ check_err 1 "cannot delete the dpllsim probe device"
+ ;;
+ *)
+ check_err 1 "cannot create dpllsim probe device"
+ ;;
+ esac
+ dpll_ref_tracker_snapshot "$REF_SNAPSHOT"
+
+ log_test "DPLL ref_tracker probe"
+}
+
test_rapid_create_delete()
{
local i clock_id name
@@ -463,16 +524,36 @@ test_deploy_cycle()
log_test "DPLL deploy/undeploy cycling"
}
+test_ref_leaks()
+{
+ RET=0
+
+ # The probe test found the tracker inactive or could not run.
+ if [ "$REF_TRACKER" -eq 0 ]; then
+ log_test_skip "DPLL stress ref_tracker check" \
+ "DPLL reference tracker not available"
+ return
+ fi
+
+ dpll_ref_tracker_check_leaks "$REF_SNAPSHOT"
+ check_err $? "leaked DPLL references after stress"
+
+ log_test "DPLL stress ref_tracker check"
+}
+
##############################################################################
dpll_lib_check_prereqs
-# Another run would find the devices of this one left behind, see the
-# lock in the library.
+# Another run would find the devices of this one left behind and see
+# them as leaks, see the lock in the library.
dpll_lib_lock
-# The module check may mount configfs, so the exit trap that unmounts it
-# again has to be in place before. INT and TERM exit through it as well,
-# so that a timeout imposed by the harness still removes the devices.
+REF_SNAPSHOT=""
+REF_TRACKER=0
+# The module check and the probe test may mount configfs and debugfs, so
+# the exit trap that unmounts them again has to be in place before. INT
+# and TERM exit through it as well, so that a timeout imposed by the
+# harness still removes the devices.
trap cleanup EXIT
trap 'exit 1' INT TERM
dpllsim_check_module
--
2.56.0
^ permalink raw reply related [flat|nested] 31+ messages in thread* Re: [PATCH net-next v3 14/14] selftests: dpll: add reference leak tests
2026-11-09 9:35 ` [PATCH net-next v3 14/14] selftests: dpll: add reference leak tests Petr Oros
@ 2026-11-09 14:00 ` Ivan Vecera
2026-11-11 9:37 ` netdev-bot+sashiko
1 sibling, 0 replies; 31+ messages in thread
From: Ivan Vecera @ 2026-11-09 14:00 UTC (permalink / raw)
To: Petr Oros, netdev
Cc: Jiri Pirko, Jonathan Corbet, Shuah Khan, Shuah Khan, Randy Dunlap,
Jani Nikula, Vadim Fedorenko, Arkadiusz Kubalewski, Jiri Pirko,
Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni, linux-doc, linux-kselftest
On 11/9/26 10:35, Petr Oros wrote:
> With CONFIG_DPLL_REFCNT_TRACKER the DPLL core keeps the stack traces of
> unreleased device and pin references in debugfs. The entries do not
> name their owner, so add library helpers that snapshot the entries
> present before a test creates its devices and fail when an entry that
> is not in the snapshot still holds references afterwards. An entry in
> the snapshot belongs to an object of another driver and is ignored
> even when its content changed, since other drivers take and drop
> references while the tests run. The teardown of a device releases its
> references before the write to live returns, so the entries are
> expected to be gone at the first look; the check still polls for a
> while before it reports a leak, so that it does not fail on a single
> read. Attributing
> by name means that a leak of an earlier run goes unnoticed and that an
> object reusing the address of a freed one from the snapshot can hide a
> leak, which is accepted, the entries carry nothing better to go by. A
> second run alongside would have its devices attributed to this one,
> which is one more reason for the lock every script takes.
>
> Whether the tracker works is probed with a throwaway device, which has
> to show entries while it is live and to drain them once it is deleted,
> so that a kernel without the tracker is told apart from a broken
> simulator and from a leak. The probe runs as the first test of the
> stress script, which gains a final leak check, and of the new
> dpll_reftrack.sh, which registers a device, removes it and verifies
> that no reference survived. The snapshot file lives next to the lock
> file under /run, for the same reason the lock file does not live in
> /tmp. The library mounts debugfs when nothing else did, like it does
> for configfs, and the selftest config enables the tracker together
> with CONFIG_DEBUG_KERNEL, which it depends on, and debugfs.
>
> Acked-by: Jiri Pirko<jiri@nvidia.com>
> Signed-off-by: Petr Oros<poros@redhat.com>
> ---
> v3:
> - say what the leak check waits for instead of a concurrent dump
> holding references, which nothing does (Ivan Vecera)
> - follow the move of the selftests to
> tools/testing/selftests/drivers/dpll
Reviewed-by: Ivan Vecera <ivecera@redhat.com>
^ permalink raw reply [flat|nested] 31+ messages in thread
* Re: [PATCH net-next v3 14/14] selftests: dpll: add reference leak tests
2026-11-09 9:35 ` [PATCH net-next v3 14/14] selftests: dpll: add reference leak tests Petr Oros
2026-11-09 14:00 ` Ivan Vecera
@ 2026-11-11 9:37 ` netdev-bot+sashiko
1 sibling, 0 replies; 31+ messages in thread
From: netdev-bot+sashiko @ 2026-11-11 9:37 UTC (permalink / raw)
To: poros
Cc: netdev, ivecera, jiri, corbet, skhan, shuah, rdunlap, jani.nikula,
vadim.fedorenko, arkadiusz.kubalewski, jiri, andrew+netdev, davem,
edumazet, kuba, pabeni, linux-doc, linux-kselftest
Thank you for your contribution! Sashiko AI review found 5 potential
issue(s) to consider.
Critical: 0 · High: 0 · Medium: 1 · Low: 4
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] The snapshot taken right after the probe can record the probe
device's own dead ref_tracker entries.
- [Low] The leak check treats any non-empty dpll_device@*/dpll_pin@*
entry missing from the snapshot as a leak of this run.
- [Low] The two new final leak checks handle a leftover device
differently.
- [Low] dpll_ref_tracker_probe() returns 1 whenever the deployed probe
device produces no non-empty new entry.
- [Low] dpll_debugfs_available() mounts debugfs when /sys/kernel/debug is
not a mountpoint and sets DPLL_MOUNTED_DEBUGFS=1.
--
selftests: dpll: add reference leak tests
This adds helpers to dpll_lib.sh that snapshot the ref_tracker debugfs
entries before a test creates dpllsim devices. A test then fails when an
entry that is not in the snapshot still holds references afterwards. A
throwaway probe device checks that the tracker works; the probe runs first
in dpll_stress.sh, which also gains a final leak check, and in the new
dpll_reftrack.sh.
> diff --git a/tools/testing/selftests/drivers/dpll/dpll_lib.sh b/tools/testing/selftests/drivers/dpll/dpll_lib.sh
> index 7a0bb144af150..87686bb53d355 100644
> --- a/tools/testing/selftests/drivers/dpll/dpll_lib.sh
> +++ b/tools/testing/selftests/drivers/dpll/dpll_lib.sh
[ ... ]
> @@ -641,3 +658,215 @@ dpllsim_setup_standard_device()
[ ... ]
> +dpll_debugfs_available()
> +{
> + if ! mountpoint -q "$DEBUGFS_DIR"; then
> + mount -t debugfs none "$DEBUGFS_DIR" &> /dev/null &&
> + DPLL_MOUNTED_DEBUGFS=1
> + fi
> + mountpoint -q "$DEBUGFS_DIR"
> +}
[Severity: Low]
The only sign that this script owns the mount is that debugfs was not
mounted when it checked. If no dpllsim device is left behind,
dpll_lib_cleanup() unmounts it at exit:
[ "$DPLL_MOUNTED_DEBUGFS" -eq 1 ] && mounted+=("$DEBUGFS_DIR")
...
for dir in "${mounted[@]}"; do
__dpll_lib_umount "$dir"
done
During a long dpll_stress.sh run, another process (another selftest, a
tracing tool, a user) can find debugfs already mounted and start using it
without mounting it itself.
Would the umount at exit pull debugfs out from under that process? A plain
umount succeeds unless a file or cwd is held open at that moment, and the
other process would not remount it.
The configfs handling already works this way. This patch applies the same
pattern to debugfs, which is shared far more widely.
[ ... ]
> +__dpll_ref_tracker_new_entries()
> +{
> + local file=$1
> + local entry name content
> +
> + for entry in "$DPLL_REF_TRACKER_DIR"/dpll_device@* \
> + "$DPLL_REF_TRACKER_DIR"/dpll_pin@*; do
> + [ -f "$entry" ] || continue
> + name=$(basename "$entry")
> + grep -q "^$name " "$file" && continue
> + content=$(cat "$entry" 2>/dev/null)
> + [ -n "$content" ] || continue
> + echo "Unreleased references in $name:"
> + echo "$content"
> + done
> +}
[Severity: Low]
The only attribution here is snapshot membership plus non-empty content.
What happens when another DPLL driver (ice, zl3073x, ptp_ocp, ...)
registers a new dpll_device or dpll_pin after the snapshot? This could come
from a deferred or async probe, a driver bind, a module reload, or a driver
re-allocating its pins during the long dpll_stress.sh run.
A live object of that driver always holds tracked references, so its entry
is non-empty and not in the snapshot.
Would test_basic_reftrack(), test_final_leak_check() and test_ref_leaks()
then wait BUSYWAIT_TIMEOUT and fail, blaming dpllsim for references it does
not own?
dpll_lib_lock() is an flock on /run/dpll-selftest/lock, so it only excludes
other dpll selftest scripts. The library comment only covers objects of
other drivers that already exist when the snapshot is taken.
[ ... ]
> +dpll_ref_tracker_probe()
> +{
[ ... ]
> + if dpllsim_add_pin "$name" ext0 ext 10000000 input &&
> + dpllsim_deploy "$name"; then
> + ret=1
> + [ -n "$(__dpll_ref_tracker_new_entries "$file")" ] && ret=0
> + fi
[Severity: Low]
Can this tell a kernel without CONFIG_DPLL_REFCNT_TRACKER apart from one
where the tracker is enabled but does not record anything?
dpll_device_alloc() and dpll_pin_alloc() call ref_tracker_dir_init()
unconditionally:
ref_tracker_dir_init(&dpll->refcnt_tracker, 128, "dpll_device");
So the dpll_device@ and dpll_pin@ files exist whenever CONFIG_REF_TRACKER
and CONFIG_DEBUG_FS are set. Only dpll_device_tracker_alloc/free and
dpll_pin_tracker_alloc/free are under #ifdef CONFIG_DPLL_REFCNT_TRACKER, and
an empty dir->list prints nothing.
Suppose a regression makes the core stop calling ref_tracker_alloc(), or
pass a NULL tracker so that only no_tracker is bumped. The probe would then
return 1, and both scripts log:
log_test_skip "DPLL ref_tracker probe" \
"CONFIG_DPLL_REFCNT_TRACKER not enabled"
This happens even though the selftest config sets
CONFIG_DPLL_REFCNT_TRACKER=y. Would such a regression show up as a SKIP
with a misleading reason instead of a FAIL?
> +
> + if dpllsim_delete_device "$name"; then
> + DPLLSIM_PROBE_DEVICE=""
> + else
> + log_info "cannot delete device $name"
> + return 4
> + fi
> + if [ "$ret" -eq 0 ] &&
> + ! busywait "$BUSYWAIT_TIMEOUT" __dpll_ref_tracker_settled "$file" \
> + > /dev/null; then
> + ret=3
> + fi
> + return "$ret"
> +}
[Severity: Medium]
Can the snapshot taken right after the probe record the probe device's own
dead ref_tracker entries?
The commit message says:
The teardown of a device releases its references before the write to
live returns, so the entries are expected to be gone at the first look
In lib/ref_tracker.c only the references go away synchronously.
ref_tracker_dir_exit() calls ref_tracker_debugfs_mark(), which only marks
the dir dead and schedules the reaper:
__xa_set_mark(&debugfs_dentries, (unsigned long)dir, REF_TRACKER_DIR_DEAD);
...
schedule_work(&debugfs_reap_worker);
The dentry is removed later, in debugfs_reap_work(). Until then the file
still exists, and reading it returns -ENODATA from
ref_tracker_debugfs_show().
That allows this sequence:
dpllsim_delete_device(probe)
dpll_device_put() -> ref_tracker_dir_exit(), kfree(dpll)
busywait __dpll_ref_tracker_settled
cat of the dead entry fails, content is empty, entry is skipped
test_ref_tracker_probe()
dpll_ref_tracker_snapshot "$REF_SNAPSHOT"
[ -f "$entry" ] is still true, so the probe's names are recorded
The next test (test_basic_reftrack() or test_rapid_create_delete()) then
allocates a dpll_device right away. It can land on the probe's freed slab
slot, and the %p hash then gives the same entry name.
If the reaper has run by then, the new object's entry is in the snapshot,
and __dpll_ref_tracker_new_entries() skips it:
grep -q "^$name " "$file" && continue
If the reaper still has not run, ref_tracker_dir_debugfs() returns early:
/* No-op if already created */
dentry = xa_load(&debugfs_dentries, (unsigned long)dir);
if (dentry && !xa_is_err(dentry))
return;
The reaper later removes the old dentry, so the new object never gets a
debugfs file at all.
In either case, would a leak in the first device created after the probe go
unreported?
Would it help to skip unreadable entries in dpll_ref_tracker_snapshot(), or
to wait after deleting the probe until its entries are gone from the
ref_tracker dir before taking the snapshot again?
[ ... ]
> diff --git a/tools/testing/selftests/drivers/dpll/dpll_stress.sh b/tools/testing/selftests/drivers/dpll/dpll_stress.sh
> index 47b8f4bf55b26..198a50ed67873 100755
> --- a/tools/testing/selftests/drivers/dpll/dpll_stress.sh
> +++ b/tools/testing/selftests/drivers/dpll/dpll_stress.sh
[ ... ]
> @@ -463,16 +524,36 @@ test_deploy_cycle()
> log_test "DPLL deploy/undeploy cycling"
> }
>
> +test_ref_leaks()
> +{
> + RET=0
> +
> + # The probe test found the tracker inactive or could not run.
> + if [ "$REF_TRACKER" -eq 0 ]; then
> + log_test_skip "DPLL stress ref_tracker check" \
> + "DPLL reference tracker not available"
> + return
> + fi
> +
> + dpll_ref_tracker_check_leaks "$REF_SNAPSHOT"
> + check_err $? "leaked DPLL references after stress"
[Severity: Low]
Should this skip when CREATED_DEVICES is not empty, the way
test_final_leak_check() in dpll_reftrack.sh does?
# A device an earlier test could not remove still holds its
# references, the failure to report is that one.
if [ -n "$CREATED_DEVICE" ]; then
log_test_skip "DPLL final ref_tracker check" \
"device from an earlier test still present"
dpllsim_delete_device() can fail in test_pin_state_flooding(),
test_max_devices() or test_deploy_cycle() because the undeploy failed. The
device then stays live and stays in CREATED_DEVICES.
It still holds its dpll_device_get(..., &sim->tracker) and
dpll_pin_get(..., &pin->tracker) references, so its entries are non-empty
and not in the snapshot.
cleanup() only removes those devices at exit. Wouldn't test_ref_leaks()
spin for BUSYWAIT_TIMEOUT and then report "leaked DPLL references after
stress" for references the device legitimately holds?
> +
> + log_test "DPLL stress ref_tracker check"
> +}
[ ... ]
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261109093523.931201-1-poros%40redhat.com
^ permalink raw reply [flat|nested] 31+ messages in thread