[PATCH 0/5] software node: provide support for fw_devlink

1 view
Skip to first unread message

Bartosz Golaszewski

unread,
Jun 29, 2026, 6:52:39 AMJun 29
to Brendan Higgins, David Gow, Rae Moar, Andy Shevchenko, Daniel Scally, Heikki Krogerus, Sakari Ailus, Bartosz Golaszewski, Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, Linus Walleij, Dmitry Torokhov, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org, Bartosz Golaszewski
Currently only devicetree systems have their devices' probe ordered
against their suppliers automatically by fw_devlink. Software nodes have
lately been used extensively treewide to describe references to resource
suppliers: most notably, the GPIO subsystem uses it in many places. Now
that the conversion of "dangling" GPIO chip software nodes to using real
links is almost done, it makes sense to ensure no needless probe
deferrals by implementing the add_links() callback from the fwnode
interface.

This series extends software node support to use fw_devlink and adds test
coverage for the new behaviour.

Patch 1 adds new kunit helpers that will be used later on for test
cases.

Patch 2 implements software_node_add_links() modelled on
of_fwnode_add_links(): for every reference property it resolves the
supplier and links to it. There's no allowlist like in DT - a software
node only carries a reference when its author explicitly wants one, so
every reference is an intentional supplier dependency. Graph
"remote-endpoint" references, unregistered supplier software nodes and
self-references are skipped. It also mirrors the device pointer onto a
secondary software node so fw_devlink can find the supplier device, and
purges the fwnode links on release.

Patches 3 and 5 add the tests: a kunit suite for the add_links() op
itself and GPIO tests for a real-life use-case: a GPIO consumer
referencing its provider via a software node.

Patch 4 proposes to add myself as a reviewer of software nodes.

Caveats: a supplier software node must be registered before the consumer
device is added, If the swnode is registered after the consumer was added,
add_links() has already run and set FWNODE_FLAG_LINKS_ADDED, so the late
supplier is missed. Graph/remote- endpoint ordering is left out for now as
well as there are no known users.

Signed-off-by: Bartosz Golaszewski <bartosz.g...@oss.qualcomm.com>
---
Bartosz Golaszewski (5):
kunit: provide a set of fwnode-oriented helpers
software node: add fw_devlink support
software node: add kunit tests for fw_devlink support
MAINTAINERS: add myself as reviewer of software node support
gpio: kunit: add test cases verifying swnode devlink support

MAINTAINERS | 2 +
drivers/base/swnode.c | 79 ++++++++
drivers/base/test/Kconfig | 5 +
drivers/base/test/Makefile | 3 +
drivers/base/test/swnode-devlink-test.c | 336 ++++++++++++++++++++++++++++++++
drivers/gpio/gpiolib-kunit.c | 272 +++++++++++++++++++++++++-
include/kunit/fwnode.h | 26 +++
lib/kunit/Makefile | 1 +
lib/kunit/fwnode.c | 116 +++++++++++
9 files changed, 833 insertions(+), 7 deletions(-)
---
base-commit: dc59e4fea9d83f03bad6bddf3fa2e52491777482
change-id: 20260609-swnode-fw-devlink-cdc8dc4e5cb4

Best regards,
--
Bartosz Golaszewski <bartosz.g...@oss.qualcomm.com>

Bartosz Golaszewski

unread,
Jun 29, 2026, 6:52:41 AMJun 29
to Brendan Higgins, David Gow, Rae Moar, Andy Shevchenko, Daniel Scally, Heikki Krogerus, Sakari Ailus, Bartosz Golaszewski, Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, Linus Walleij, Dmitry Torokhov, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org, Bartosz Golaszewski
Provide three new kunit-managed helpers for test cases that need to
register/create dynamic software nodes.

Signed-off-by: Bartosz Golaszewski <bartosz.g...@oss.qualcomm.com>
---
include/kunit/fwnode.h | 26 +++++++++++
lib/kunit/Makefile | 1 +
lib/kunit/fwnode.c | 116 +++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 143 insertions(+)

diff --git a/include/kunit/fwnode.h b/include/kunit/fwnode.h
new file mode 100644
index 0000000000000000000000000000000000000000..239bc71eb5072ccead0beb51fc0882bab69c6877
--- /dev/null
+++ b/include/kunit/fwnode.h
@@ -0,0 +1,26 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * KUnit resource management helpers for firmware nodes.
+ *
+ * Copyright (C) Qualcomm Technologies, Inc. and/or its subsidiaries
+ */
+
+#ifndef _KUNIT_FWNODE_H
+#define _KUNIT_FWNODE_H
+
+struct kunit;
+struct fwnode_handle;
+struct property_entry;
+struct software_node;
+
+struct fwnode_handle *
+kunit_fwnode_create_software_node(struct kunit *test,
+ const struct property_entry *properties,
+ const struct fwnode_handle *parent);
+struct fwnode_handle *
+kunit_software_node_register(struct kunit *test,
+ const struct software_node *node);
+int kunit_software_node_register_node_group(struct kunit *test,
+ const struct software_node *const *nodes);
+
+#endif /* _KUNIT_FWNODE_H */
diff --git a/lib/kunit/Makefile b/lib/kunit/Makefile
index 2e8a6b71a2ab07a738964a7ef0f442fd53e085b1..204e02b10eba1030c6d511991fe2f6271de64603 100644
--- a/lib/kunit/Makefile
+++ b/lib/kunit/Makefile
@@ -11,6 +11,7 @@ kunit-objs += test.o \
attributes.o \
device.o \
platform.o \
+ fwnode.o \
bug.o

ifeq ($(CONFIG_KUNIT_DEBUGFS),y)
diff --git a/lib/kunit/fwnode.c b/lib/kunit/fwnode.c
new file mode 100644
index 0000000000000000000000000000000000000000..332490f07fae78e0fbf2930f9c80da0cc7dce028
--- /dev/null
+++ b/lib/kunit/fwnode.c
@@ -0,0 +1,116 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) Qualcomm Technologies, Inc. and/or its subsidiaries
+ */
+
+#include <kunit/fwnode.h>
+#include <kunit/test.h>
+
+#include <linux/fwnode.h>
+#include <linux/property.h>
+
+KUNIT_DEFINE_ACTION_WRAPPER(fwnode_remove_software_node_wrapper,
+ fwnode_remove_software_node,
+ struct fwnode_handle *);
+
+/**
+ * kunit_fwnode_create_software_node() - Create a kunit-managed software node
+ * @test: Test context
+ * @properties: Properties to use to create the new software node
+ * @parent: Parent of this software node
+ *
+ * Create a test-managed software node and return its firmware node handle.
+ * The software node is removed after the test case completes.
+ *
+ * Returns:
+ * Firmware node handle of the newly created software node or IS_ERR() on
+ * failure.
+ */
+struct fwnode_handle *
+kunit_fwnode_create_software_node(struct kunit *test,
+ const struct property_entry *properties,
+ const struct fwnode_handle *parent)
+{
+ struct fwnode_handle *fwnode;
+ int ret;
+
+ fwnode = fwnode_create_software_node(properties, parent);
+ if (IS_ERR(fwnode))
+ return fwnode;
+
+ ret = kunit_add_action_or_reset(test, fwnode_remove_software_node_wrapper,
+ fwnode);
+ if (ret)
+ return ERR_PTR(ret);
+
+ return fwnode;
+}
+EXPORT_SYMBOL_GPL(kunit_fwnode_create_software_node);
+
+KUNIT_DEFINE_ACTION_WRAPPER(software_node_unregister_wrapper,
+ software_node_unregister,
+ const struct software_node *);
+
+/**
+ * kunit_software_node_register() - Register a kunit-managed software node
+ * @test: Test context
+ * @swnode: Software node to register
+ *
+ * Register a test-managed software node and return its firmware node handle.
+ * The software node is unregistered after the test case completes.
+ *
+ * Returns:
+ * Firmware node handle of the registered software node or IS_ERR() on failure.
+ */
+struct fwnode_handle *
+kunit_software_node_register(struct kunit *test,
+ const struct software_node *swnode)
+{
+ struct fwnode_handle *fwnode;
+ int ret;
+
+ ret = software_node_register(swnode);
+ if (ret)
+ return ERR_PTR(ret);
+
+ fwnode = software_node_fwnode(swnode);
+ if (WARN_ON(!fwnode))
+ return ERR_PTR(-ENOENT);
+
+ ret = kunit_add_action_or_reset(test, software_node_unregister_wrapper,
+ (void *)swnode);
+ if (ret)
+ return ERR_PTR(ret);
+
+ return fwnode;
+}
+EXPORT_SYMBOL_GPL(kunit_software_node_register);
+
+KUNIT_DEFINE_ACTION_WRAPPER(software_node_unregister_node_group_wrapper,
+ software_node_unregister_node_group,
+ const struct software_node *const *);
+
+/**
+ * kunit_software_node_register_node_group() - Register a kunit-managed software node group
+ * @test: Test context
+ * @nodes: Software node group to register
+ *
+ * Register a test-managed software node group. The nodes are unregistered
+ * after the test case completes.
+ *
+ * Returns:
+ * 0 on success, negative error number on failure.
+ */
+int kunit_software_node_register_node_group(struct kunit *test,
+ const struct software_node *const *nodes)
+{
+ int ret;
+
+ ret = software_node_register_node_group(nodes);
+ if (ret)
+ return ret;
+
+ return kunit_add_action_or_reset(test, software_node_unregister_node_group_wrapper,
+ (void *)nodes);
+}
+EXPORT_SYMBOL_GPL(kunit_software_node_register_node_group);

--
2.47.3

Bartosz Golaszewski

unread,
Jun 29, 2026, 6:52:43 AMJun 29
to Brendan Higgins, David Gow, Rae Moar, Andy Shevchenko, Daniel Scally, Heikki Krogerus, Sakari Ailus, Bartosz Golaszewski, Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, Linus Walleij, Dmitry Torokhov, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org, Bartosz Golaszewski
Software nodes can be used to describe supplier-consumer relationships
between devices they represent using reference property entries. Unlike
for OF-nodes, driver core cannot yet use these references to create a
probe order that avoids needless probe deferrals on missing providers.

Implement software_node_add_links() modelled on of_fwnode_add_links().
For every DEV_PROP_REF property we resolve each referenced supplier and
create an fwnode link from the node to it. The driver core later promotes
these to device links and defers the consumer until the suppliers are
ready.

There's no allowlist like the one DT needs - devicetree phandles appear
in plenty of non-supplier contexts, but a software node only carries a
reference property when its author explicitly points at another node, so
we treat every reference as an intentional supplier dependency and link
all of them. Graph "remote-endpoint" references are skipped for now: they
go 2-ways between endpoint nodes and would create graph cycles without
the port-parent lifting DT does via get_con_dev(). References to
suppliers that aren't registered yet and self-references are ignored.

fw_devlink resolves the supplier device through fwnode->dev but the core
only records the owning device on the primary fwnode. When the software
node is a device's secondary fwnode, mirror the device pointer onto it in
software_node_notify() so the consumer can actually find the supplier
instead of deferring forever.

While at it: purge the fwnode links in software_node_release() now that
software nodes can own them.

Signed-off-by: Bartosz Golaszewski <bartosz.g...@oss.qualcomm.com>
---
drivers/base/swnode.c | 79 +++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 79 insertions(+)

diff --git a/drivers/base/swnode.c b/drivers/base/swnode.c
index 869228a65cb365567ddac7db6ad7b8743e0dbca9..48eb67826f9e1917acc7a6a513c1536a7ece0961 100644
--- a/drivers/base/swnode.c
+++ b/drivers/base/swnode.c
@@ -699,6 +699,62 @@ software_node_graph_parse_endpoint(const struct fwnode_handle *fwnode,
return 0;
}

+static int software_node_add_links(struct fwnode_handle *fwnode)
+{
+ const struct software_node_ref_args *ref, *ref_array;
+ struct swnode *swnode = to_swnode(fwnode);
+ const struct property_entry *prop;
+ struct fwnode_handle *refnode;
+ unsigned int count, i;
+
+ if (!swnode || !swnode->node->properties)
+ return 0;
+
+ /*
+ * Unlike Device Tree, where phandles appear in many non-supplier
+ * contexts and a curated allowlist is required, a software node only
+ * carries a DEV_PROP_REF property when the author explicitly describes
+ * a reference to another node. Every such reference is therefore an
+ * intentional supplier dependency, so we create fwnode links for all
+ * of them.
+ */
+ for (prop = swnode->node->properties; prop->name; prop++) {
+ if (prop->type != DEV_PROP_REF || prop->is_inline)
+ continue;
+
+ /*
+ * TODO: Graph "remote-endpoint" references go both ways
+ * between endpoint child nodes and would create endpoint
+ * cycles. Let's leave it out for now until we have potential
+ * users.
+ */
+ if (!strcmp(prop->name, "remote-endpoint"))
+ continue;
+
+ ref_array = prop->pointer;
+ count = prop->length / sizeof(*ref_array);
+
+ for (i = 0; i < count; i++) {
+ ref = &ref_array[i];
+
+ if (ref->swnode)
+ refnode = software_node_fwnode(ref->swnode);
+ else if (ref->fwnode)
+ refnode = ref->fwnode;
+ else
+ continue;
+
+ /* Supplier not registered yet, or self-reference. */
+ if (!refnode || refnode == &swnode->fwnode)
+ continue;
+
+ fwnode_link_add(&swnode->fwnode, refnode, 0);
+ }
+ }
+
+ return 0;
+}
+
static const struct fwnode_operations software_node_ops = {
.get = software_node_get,
.put = software_node_put,
@@ -716,6 +772,7 @@ static const struct fwnode_operations software_node_ops = {
.graph_get_remote_endpoint = software_node_graph_get_remote_endpoint,
.graph_get_port_parent = software_node_graph_get_port_parent,
.graph_parse_endpoint = software_node_graph_parse_endpoint,
+ .add_links = software_node_add_links,
};

/* -------------------------------------------------------------------------- */
@@ -787,6 +844,8 @@ static void software_node_release(struct kobject *kobj)
{
struct swnode *swnode = kobj_to_swnode(kobj);

+ fwnode_links_purge(&swnode->fwnode);
+
if (swnode->parent) {
ida_free(&swnode->parent->child_ids, swnode->id);
list_del(&swnode->entry);
@@ -1105,6 +1164,17 @@ void software_node_notify(struct device *dev)
if (!swnode)
return;

+ /*
+ * When the software node is the device's secondary firmware node, the
+ * core only records the owning device on the primary fwnode (see
+ * device_add()). fw_devlink resolves a supplier device through
+ * fwnode->dev, so without this a consumer referencing the software
+ * node could never find the supplier device and would defer forever.
+ * Make fwnode.dev point to its owner in that case.
+ */
+ if (dev_fwnode(dev) != &swnode->fwnode && !swnode->fwnode.dev)
+ swnode->fwnode.dev = dev;
+
swnode_get(swnode);
ret = sysfs_create_link(&dev->kobj, &swnode->kobj, "software_node");
if (ret)
@@ -1127,6 +1197,15 @@ void software_node_notify_remove(struct device *dev)

sysfs_remove_link(&swnode->kobj, dev_name(dev));
sysfs_remove_link(&dev->kobj, "software_node");
+
+ /*
+ * Drop the device pointer mirrored onto a secondary software node in
+ * software_node_notify(). For a primary software node the core owns
+ * fwnode->dev and clears it in device_del().
+ */
+ if (dev_fwnode(dev) != &swnode->fwnode && swnode->fwnode.dev == dev)
+ swnode->fwnode.dev = NULL;
+
swnode_put(swnode);

if (swnode->managed) {

--
2.47.3

Bartosz Golaszewski

unread,
Jun 29, 2026, 6:52:44 AMJun 29
to Brendan Higgins, David Gow, Rae Moar, Andy Shevchenko, Daniel Scally, Heikki Krogerus, Sakari Ailus, Bartosz Golaszewski, Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, Linus Walleij, Dmitry Torokhov, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org, Bartosz Golaszewski
Add a kunit test suite for fw_devlink support for software nodes.

Most cases call add_links() directly and inspect the resulting fwnode
supplier/consumer lists: a single reference, multiple references, a
reference to an unregistered node, a "remote-endpoint" reference and a
reference array. The last case is end-to-end - it registers real consumer
and supplier platform devices together with their drivers, adds the
consumer first and checks that fw_devlink defers its probe until the
supplier has been bound.

Signed-off-by: Bartosz Golaszewski <bartosz.g...@oss.qualcomm.com>
---
drivers/base/test/Kconfig | 5 +
drivers/base/test/Makefile | 3 +
drivers/base/test/swnode-devlink-test.c | 336 ++++++++++++++++++++++++++++++++
3 files changed, 344 insertions(+)

diff --git a/drivers/base/test/Kconfig b/drivers/base/test/Kconfig
index 2756870615ccab67ec26d8671c1e4dba69342134..1ecf0791241a1b2eee7e1e787772217724abacb9 100644
--- a/drivers/base/test/Kconfig
+++ b/drivers/base/test/Kconfig
@@ -18,3 +18,8 @@ config DRIVER_PE_KUNIT_TEST
tristate "KUnit Tests for property entry API" if !KUNIT_ALL_TESTS
depends on KUNIT
default KUNIT_ALL_TESTS
+
+config DRIVER_SWNODE_KUNIT_TEST
+ tristate "KUnit Tests for software node fw_devlink links" if !KUNIT_ALL_TESTS
+ depends on KUNIT
+ default KUNIT_ALL_TESTS
diff --git a/drivers/base/test/Makefile b/drivers/base/test/Makefile
index e321dfc7e92266d2073d442f652cadb6e911dba5..1b78a705983c145e29bd166606f2c78682342735 100644
--- a/drivers/base/test/Makefile
+++ b/drivers/base/test/Makefile
@@ -6,3 +6,6 @@ obj-$(CONFIG_DM_KUNIT_TEST) += platform-device-test.o

obj-$(CONFIG_DRIVER_PE_KUNIT_TEST) += property-entry-test.o
CFLAGS_property-entry-test.o += $(DISABLE_STRUCTLEAK_PLUGIN)
+
+obj-$(CONFIG_DRIVER_SWNODE_KUNIT_TEST) += swnode-devlink-test.o
+CFLAGS_swnode-devlink-test.o += $(DISABLE_STRUCTLEAK_PLUGIN)
diff --git a/drivers/base/test/swnode-devlink-test.c b/drivers/base/test/swnode-devlink-test.c
new file mode 100644
index 0000000000000000000000000000000000000000..42816f8f7c1ee4572b6c87bc91b434c0e0086aa8
--- /dev/null
+++ b/drivers/base/test/swnode-devlink-test.c
@@ -0,0 +1,336 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (C) Qualcomm Technologies, Inc. and/or its subsidiaries
+ */
+
+#include <linux/device.h>
+#include <linux/fwnode.h>
+#include <linux/platform_device.h>
+#include <linux/property.h>
+#include <linux/types.h>
+#include <linux/wait.h>
+
+#include <kunit/fwnode.h>
+#include <kunit/platform_device.h>
+#include <kunit/test.h>
+
+static int swnode_count_suppliers(struct fwnode_handle *fwnode)
+{
+ struct fwnode_link *link;
+ int ret = 0;
+
+ /*
+ * The suppliers and consumers lists should typically only be accessed
+ * with the fwnode_link_lock taken but it's private to the driver core.
+ *
+ * These are tests and at this point nobody should be modifying them so
+ * let's just access the list.
+ */
+ list_for_each_entry(link, &fwnode->suppliers, c_hook)
+ ret++;
+
+ return ret;
+}
+
+/* True if a supplier link con->sup exists, checked from both list ends. */
+static bool swnode_has_link(struct fwnode_handle *consumer,
+ struct fwnode_handle *supplier)
+{
+ bool from_con = false, from_sup = false;
+ struct fwnode_link *link;
+
+ list_for_each_entry(link, &consumer->suppliers, c_hook) {
+ if (link->supplier == supplier && link->consumer == consumer)
+ from_con = true;
+ }
+
+ list_for_each_entry(link, &supplier->consumers, s_hook) {
+ if (link->supplier == supplier && link->consumer == consumer)
+ from_sup = true;
+ }
+
+ return from_con && from_sup;
+}
+
+/* A single reference creates exactly one supplier link, on both list ends. */
+static void swnode_devlink_test_single_ref(struct kunit *test)
+{
+ static const struct software_node supp_swnode = {
+ .name = "swnode-devlink-test-supplier"
+ };
+
+ struct fwnode_handle *cons_fwnode, *supp_fwnode;
+ int ret;
+
+ const struct property_entry props[] = {
+ PROPERTY_ENTRY_REF("supplier", &supp_swnode),
+ { }
+ };
+
+ supp_fwnode = kunit_software_node_register(test, &supp_swnode);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, supp_fwnode);
+
+ cons_fwnode = kunit_fwnode_create_software_node(test, props, NULL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, cons_fwnode);
+
+ ret = fwnode_call_int_op(cons_fwnode, add_links);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+
+ KUNIT_EXPECT_EQ(test, swnode_count_suppliers(cons_fwnode), 1);
+ KUNIT_EXPECT_TRUE(test, swnode_has_link(cons_fwnode, supp_fwnode));
+}
+
+/* Multiple distinct references create multiple supplier links. */
+static void swnode_devlink_test_multiple_refs(struct kunit *test)
+{
+ static const struct software_node supp1_swnode = {
+ .name = "swnode-devlink-test-supplier-1"
+ };
+ static const struct software_node supp2_swnode = {
+ .name = "swnode-devlink-test-supplier-2"
+ };
+ static const struct software_node *supp_nodes[] = {
+ &supp1_swnode, &supp2_swnode, NULL
+ };
+
+ const struct property_entry props[] = {
+ PROPERTY_ENTRY_REF("foo", &supp1_swnode),
+ PROPERTY_ENTRY_REF("bar", &supp2_swnode),
+ { }
+ };
+
+ struct fwnode_handle *fwnode;
+ int ret;
+
+ ret = kunit_software_node_register_node_group(test, supp_nodes);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ fwnode = kunit_fwnode_create_software_node(test, props, NULL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, fwnode);
+
+ ret = fwnode_call_int_op(fwnode, add_links);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+
+ KUNIT_EXPECT_EQ(test, swnode_count_suppliers(fwnode), 2);
+ KUNIT_EXPECT_TRUE(test, swnode_has_link(fwnode, software_node_fwnode(&supp1_swnode)));
+ KUNIT_EXPECT_TRUE(test, swnode_has_link(fwnode, software_node_fwnode(&supp2_swnode)));
+}
+
+/* A reference to an unregistered node creates no link (graceful skip). */
+static void swnode_devlink_test_unregistered_ref(struct kunit *test)
+{
+ static const struct software_node supp_swnode = {
+ .name = "swnode-devlink-test-supplier"
+ };
+
+ const struct property_entry props[] = {
+ PROPERTY_ENTRY_REF("supplier", &supp_swnode),
+ { }
+ };
+
+ struct fwnode_handle *fwnode;
+ int ret;
+
+ fwnode = kunit_fwnode_create_software_node(test, props, NULL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, fwnode);
+
+ ret = fwnode_call_int_op(fwnode, add_links);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, swnode_count_suppliers(fwnode), 0);
+}
+
+/* Graph "remote-endpoint" references are excluded. */
+static void swnode_devlink_test_remote_endpoint_excluded(struct kunit *test)
+{
+ static const struct software_node ep_swnode = {
+ .name = "swnode-devlink-test-end-point"
+ };
+
+ const struct property_entry props[] = {
+ PROPERTY_ENTRY_REF("remote-endpoint", &ep_swnode),
+ { }
+ };
+
+ struct fwnode_handle *cons_fwnode, *supp_fwnode;
+ int ret;
+
+ supp_fwnode = kunit_software_node_register(test, &ep_swnode);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, supp_fwnode);
+
+ cons_fwnode = kunit_fwnode_create_software_node(test, props, NULL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, cons_fwnode);
+
+ ret = fwnode_call_int_op(cons_fwnode, add_links);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, swnode_count_suppliers(cons_fwnode), 0);
+}
+
+/* A reference array creates one link per registered element. */
+static void swnode_devlink_test_ref_array(struct kunit *test)
+{
+ static const struct software_node supp1_swnode = {
+ .name = "swnode-devlink-test-supplier-1"
+ };
+ static const struct software_node supp2_swnode = {
+ .name = "swnode-devlink-test-supplier-2"
+ };
+ static const struct software_node *supp_nodes[] = {
+ &supp1_swnode, &supp2_swnode, NULL
+ };
+ static const struct software_node_ref_args refs[] = {
+ SOFTWARE_NODE_REFERENCE(&supp1_swnode),
+ SOFTWARE_NODE_REFERENCE(&supp2_swnode, 4, 2),
+ };
+
+ const struct property_entry props[] = {
+ PROPERTY_ENTRY_REF_ARRAY("suppliers", refs),
+ { }
+ };
+
+ struct fwnode_handle *fwnode;
+ int ret;
+
+ ret = kunit_software_node_register_node_group(test, supp_nodes);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ fwnode = kunit_fwnode_create_software_node(test, props, NULL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, fwnode);
+
+ ret = fwnode_call_int_op(fwnode, add_links);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+
+ KUNIT_EXPECT_EQ(test, swnode_count_suppliers(fwnode), 2);
+ KUNIT_EXPECT_TRUE(test, swnode_has_link(fwnode, software_node_fwnode(&supp1_swnode)));
+ KUNIT_EXPECT_TRUE(test, swnode_has_link(fwnode, software_node_fwnode(&supp2_swnode)));
+}
+
+/*
+ * End-to-end test: fw_devlink must defer a consumer's probe until its
+ * supplier has probed.
+ *
+ * The reference created by software_node_add_links() is only useful if the
+ * driver core promotes it to a real device_link and uses it to order probing.
+ * This test drives actual probing through the platform bus and asserts the
+ * supplier binds before the consumer.
+ */
+
+#define SWNODE_DEVLINK_TEST_SUPPLIER "swnode-link-supplier"
+#define SWNODE_DEVLINK_TEST_CONSUMER "swnode-link-consumer"
+#define SWNODE_DEVLINK_TEST_TIMEOUT_MS 2000
+
+struct swnode_test_probe_order {
+ /* Names in the order their drivers' .probe ran. */
+ const char *probed[2];
+ unsigned int count;
+ wait_queue_head_t wq;
+};
+
+static int swnode_test_record_probe(struct platform_device *pdev)
+{
+ struct swnode_test_probe_order *order = platform_get_drvdata(pdev);
+
+ if (order && order->count < ARRAY_SIZE(order->probed)) {
+ order->probed[order->count++] = dev_name(&pdev->dev);
+ wake_up_interruptible(&order->wq);
+ }
+
+ return 0;
+}
+
+static struct platform_driver swnode_test_supplier_driver = {
+ .probe = swnode_test_record_probe,
+ .driver = {
+ .name = SWNODE_DEVLINK_TEST_SUPPLIER,
+ },
+};
+
+static struct platform_driver swnode_test_consumer_driver = {
+ .probe = swnode_test_record_probe,
+ .driver = {
+ .name = SWNODE_DEVLINK_TEST_CONSUMER,
+ },
+};
+
+static void swnode_devlink_test_probe_order(struct kunit *test)
+{
+ static const struct software_node supplier_swnode = {
+ .name = "swnode-devlink-test-supplier",
+ };
+
+ const struct property_entry consumer_props[] = {
+ PROPERTY_ENTRY_REF("supplier-ref", &supplier_swnode),
+ { }
+ };
+
+ struct platform_device *supplier, *consumer;
+ struct swnode_test_probe_order *order;
+ struct fwnode_handle *fwnode;
+ int ret;
+
+ order = kunit_kzalloc(test, sizeof(*order), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, order);
+ init_waitqueue_head(&order->wq);
+
+ fwnode = kunit_software_node_register(test, &supplier_swnode);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, fwnode);
+
+ ret = kunit_platform_driver_register(test, &swnode_test_supplier_driver);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ret = kunit_platform_driver_register(test, &swnode_test_consumer_driver);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ supplier = kunit_platform_device_alloc(test, SWNODE_DEVLINK_TEST_SUPPLIER,
+ PLATFORM_DEVID_NONE);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, supplier);
+ consumer = kunit_platform_device_alloc(test, SWNODE_DEVLINK_TEST_CONSUMER,
+ PLATFORM_DEVID_NONE);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, consumer);
+
+ platform_set_drvdata(supplier, order);
+ platform_set_drvdata(consumer, order);
+
+ ret = device_add_software_node(&supplier->dev, &supplier_swnode);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ret = device_create_managed_software_node(&consumer->dev,
+ consumer_props, NULL);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ ret = kunit_platform_device_add(test, consumer);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ret = kunit_platform_device_add(test, supplier);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ ret = wait_event_interruptible_timeout(order->wq,
+ order->count == 2,
+ msecs_to_jiffies(SWNODE_DEVLINK_TEST_TIMEOUT_MS));
+ KUNIT_ASSERT_GT(test, ret, 0);
+
+ KUNIT_EXPECT_STREQ(test, order->probed[0], SWNODE_DEVLINK_TEST_SUPPLIER);
+ KUNIT_EXPECT_STREQ(test, order->probed[1], SWNODE_DEVLINK_TEST_CONSUMER);
+
+ /* Tear down the consumer (and its device link) before the supplier. */
+ kunit_platform_device_unregister(test, consumer);
+
+ device_remove_software_node(&supplier->dev);
+}
+
+static struct kunit_case swnode_test_cases[] = {
+ KUNIT_CASE(swnode_devlink_test_single_ref),
+ KUNIT_CASE(swnode_devlink_test_multiple_refs),
+ KUNIT_CASE(swnode_devlink_test_unregistered_ref),
+ KUNIT_CASE(swnode_devlink_test_remote_endpoint_excluded),
+ KUNIT_CASE(swnode_devlink_test_ref_array),
+ KUNIT_CASE(swnode_devlink_test_probe_order),
+ { }
+};
+
+static struct kunit_suite swnode_test_suite = {
+ .name = "software-node-links",
+ .test_cases = swnode_test_cases,
+};
+
+kunit_test_suite(swnode_test_suite);
+
+MODULE_DESCRIPTION("Test module for software node fw_devlink support");
+MODULE_AUTHOR("Bartosz Golaszewski <bartosz.g...@oss.qualcomm.com>");
+MODULE_LICENSE("GPL");

--
2.47.3

Bartosz Golaszewski

unread,
Jun 29, 2026, 6:52:45 AMJun 29
to Brendan Higgins, David Gow, Rae Moar, Andy Shevchenko, Daniel Scally, Heikki Krogerus, Sakari Ailus, Bartosz Golaszewski, Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, Linus Walleij, Dmitry Torokhov, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org, Bartosz Golaszewski
I've been working extensively on software nodes lately and introduced
some changes. Add myself as reviewer so that I can help review any new
proposed changes.

Signed-off-by: Bartosz Golaszewski <bartosz.g...@oss.qualcomm.com>
---
MAINTAINERS | 2 ++
1 file changed, 2 insertions(+)

diff --git a/MAINTAINERS b/MAINTAINERS
index 15011f5752a994cf1b354f490d6c4e411588df88..e30aa3b63eff90bc0ccc39de774986ce17494071 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -25101,10 +25101,12 @@ R: Andy Shevchenko <andriy.s...@linux.intel.com>
R: Daniel Scally <djrs...@gmail.com>
R: Heikki Krogerus <heikki....@linux.intel.com>
R: Sakari Ailus <sakari...@linux.intel.com>
+R: Bartosz Golaszewski <br...@kernel.org>
L: linux...@vger.kernel.org
S: Maintained
F: drivers/base/property.c
F: drivers/base/swnode.c
+F: drivers/base/test/swnode-devlink-test.c
F: include/linux/fwnode.h
F: include/linux/property.h


--
2.47.3

Bartosz Golaszewski

unread,
Jun 29, 2026, 6:52:47 AMJun 29
to Brendan Higgins, David Gow, Rae Moar, Andy Shevchenko, Daniel Scally, Heikki Krogerus, Sakari Ailus, Bartosz Golaszewski, Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, Linus Walleij, Dmitry Torokhov, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org, Bartosz Golaszewski
The software node fw_devlink support already has its own kunit suite, but
that verifies the fwnode links in isolation. Add GPIO tests that prove
the ordering works in a real-life use-case: a GPIO consumer that
references its provider via a software node.

The first suite registers the provider's software node, adds the consumer
device first and checks that fw_devlink defers its probe until the
provider has been added and bound. The second covers the fallback:
with the provider's software node not yet registered no supplier link is
created, so the consumer probes, devm_gpiod_get() returns -EPROBE_DEFER
and the consumer only binds once the provider shows up.

While at it: the existing gpio_unbind_with_consumers() test keeps the
consumer bound while the provider goes away and then operates the orphaned
descriptor. With software nodes now being covered by fw_devlink that would
instead force-unbind the consumer along with the provider, so opt it out
by setting FWNODE_FLAG_LINKS_ADDED.

Signed-off-by: Bartosz Golaszewski <bartosz.g...@oss.qualcomm.com>
---
drivers/gpio/gpiolib-kunit.c | 272 +++++++++++++++++++++++++++++++++++++++++--
1 file changed, 265 insertions(+), 7 deletions(-)

diff --git a/drivers/gpio/gpiolib-kunit.c b/drivers/gpio/gpiolib-kunit.c
index 380b68f879e55433668353bb88067d561142a5bc..3def39f11ece46557cbdf8bae8642b2ad21232f0 100644
--- a/drivers/gpio/gpiolib-kunit.c
+++ b/drivers/gpio/gpiolib-kunit.c
@@ -12,11 +12,14 @@
#include <linux/platform_device.h>
#include <linux/property.h>

+#include <kunit/fwnode.h>
#include <kunit/platform_device.h>
#include <kunit/test.h>

#define GPIO_TEST_PROVIDER "gpio-test-provider"
#define GPIO_SWNODE_TEST_CONSUMER "gpio-swnode-test-consumer"
+#define GPIO_PROBE_ORDER_TEST_CONSUMER "gpio-probe-order-test-consumer"
+#define GPIO_PROBE_DEFER_TEST_CONSUMER "gpio-probe-defer-test-consumer"
#define GPIO_UNBIND_TEST_CONSUMER "gpio-unbind-test-consumer"

static int gpio_test_provider_get_direction(struct gpio_chip *gc, unsigned int offset)
@@ -213,6 +216,251 @@ static struct kunit_suite gpio_swnode_lookup_test_suite = {
.test_cases = gpio_swnode_lookup_tests,
};

+static void gpio_swnode_unregister_swnode(void *data)
+{
+ software_node_unregister(data);
+}
+
+struct gpio_probe_order_pdata {
+ int probe_count;
+ bool gpio_ok;
+};
+
+static const struct gpio_probe_order_pdata gpio_probe_order_pdata_template = {
+ .probe_count = 0,
+ .gpio_ok = false,
+};
+
+static int gpio_probe_order_consumer_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct gpio_probe_order_pdata *pdata = dev_get_platdata(dev);
+ struct gpio_desc *desc;
+
+ pdata->probe_count++;
+
+ desc = devm_gpiod_get(dev, "foo", GPIOD_OUT_HIGH);
+ if (IS_ERR(desc))
+ return PTR_ERR(desc);
+
+ pdata->gpio_ok = true;
+
+ return 0;
+}
+
+static struct platform_driver gpio_probe_order_consumer_driver = {
+ .probe = gpio_probe_order_consumer_probe,
+ .driver = {
+ .name = GPIO_PROBE_ORDER_TEST_CONSUMER,
+ },
+};
+
+/*
+ * Verify that fw_devlink orders the probe of a GPIO consumer after its
+ * provider. The consumer references the provider through a software node and
+ * is registered first. fw_devlink must defer it before its driver's probe()
+ * is ever entered, so the consumer probes exactly once - only after the
+ * provider is added and bound.
+ */
+static void gpio_swnode_probe_order(struct kunit *test)
+{
+ struct gpio_probe_order_pdata *pdata;
+ struct platform_device_info pdevinfo;
+ struct property_entry properties[2];
+ struct platform_device *prvd, *cons;
+ bool bound = false;
+ int ret;
+
+ ret = kunit_platform_driver_register(test, &gpio_test_provider_driver);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ ret = kunit_platform_driver_register(test, &gpio_probe_order_consumer_driver);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ ret = software_node_register(&gpio_test_provider_swnode);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ ret = kunit_add_action_or_reset(test, gpio_swnode_unregister_swnode,
+ (void *)&gpio_test_provider_swnode);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ properties[0] = PROPERTY_ENTRY_GPIO("foo-gpios",
+ &gpio_test_provider_swnode,
+ 0, GPIO_ACTIVE_HIGH);
+ properties[1] = (struct property_entry){ };
+
+ pdevinfo = (struct platform_device_info){
+ .name = GPIO_PROBE_ORDER_TEST_CONSUMER,
+ .id = PLATFORM_DEVID_NONE,
+ .data = &gpio_probe_order_pdata_template,
+ .size_data = sizeof(gpio_probe_order_pdata_template),
+ .properties = properties,
+ };
+
+ cons = kunit_platform_device_register_full(test, &pdevinfo);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, cons);
+
+ wait_for_device_probe();
+ scoped_guard(device, &cons->dev)
+ bound = device_is_bound(&cons->dev);
+
+ KUNIT_ASSERT_FALSE(test, bound);
+
+ pdata = dev_get_platdata(&cons->dev);
+ KUNIT_ASSERT_EQ(test, pdata->probe_count, 0);
+ KUNIT_ASSERT_FALSE(test, pdata->gpio_ok);
+
+ pdevinfo = (struct platform_device_info){
+ .name = GPIO_TEST_PROVIDER,
+ .id = PLATFORM_DEVID_NONE,
+ .swnode = &gpio_test_provider_swnode,
+ };
+
+ prvd = kunit_platform_device_register_full(test, &pdevinfo);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, prvd);
+
+ wait_for_device_probe();
+
+ scoped_guard(device, &prvd->dev)
+ bound = device_is_bound(&prvd->dev);
+ KUNIT_ASSERT_TRUE(test, bound);
+
+ scoped_guard(device, &cons->dev)
+ bound = device_is_bound(&cons->dev);
+ KUNIT_ASSERT_TRUE(test, bound);
+
+ pdata = dev_get_platdata(&cons->dev);
+ KUNIT_ASSERT_EQ(test, pdata->probe_count, 1);
+ KUNIT_ASSERT_TRUE(test, pdata->gpio_ok);
+}
+
+struct gpio_probe_defer_pdata {
+ int probe_count;
+ int gpio_err;
+};
+
+static const struct gpio_probe_defer_pdata gpio_probe_defer_pdata_template = {
+ .probe_count = 0,
+ .gpio_err = 0,
+};
+
+static int gpio_probe_defer_consumer_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct gpio_probe_defer_pdata *pdata = dev_get_platdata(dev);
+ struct gpio_desc *desc;
+
+ pdata->probe_count++;
+
+ desc = devm_gpiod_get(dev, "foo", GPIOD_OUT_HIGH);
+ if (IS_ERR(desc)) {
+ pdata->gpio_err = PTR_ERR(desc);
+ return pdata->gpio_err;
+ }
+
+ pdata->gpio_err = 0;
+
+ return 0;
+}
+
+static struct platform_driver gpio_probe_defer_consumer_driver = {
+ .probe = gpio_probe_defer_consumer_probe,
+ .driver = {
+ .name = GPIO_PROBE_DEFER_TEST_CONSUMER,
+ },
+};
+
+/*
+ * Verify that a GPIO consumer referencing a provider whose software node is
+ * not registered yet, defers its probe instead of failing.
+ *
+ * The provider software node is deliberately left unregistered when the
+ * consumer is added. fw_devlink cannot resolve the reference, so it creates no
+ * supplier link and does not order the consumer - the consumer's probe() runs
+ * and reaches devm_gpiod_get(). The swnode GPIO lookup returns -ENOTCONN for a
+ * reference to an unregistered node, which gpiolib maps to -EPROBE_DEFER. Once
+ * the provider software node and device appear, the deferred consumer probes
+ * again and binds.
+ */
+static void gpio_swnode_probe_defer_on_unregistered(struct kunit *test)
+{
+ struct gpio_probe_defer_pdata *pdata;
+ struct platform_device_info pdevinfo;
+ struct property_entry properties[2];
+ struct platform_device *prvd, *cons;
+ struct fwnode_handle *fwnode;
+ bool bound = false;
+ int ret;
+
+ ret = kunit_platform_driver_register(test, &gpio_test_provider_driver);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ ret = kunit_platform_driver_register(test, &gpio_probe_defer_consumer_driver);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ properties[0] = PROPERTY_ENTRY_GPIO("foo-gpios",
+ &gpio_test_provider_swnode,
+ 0, GPIO_ACTIVE_HIGH);
+ properties[1] = (struct property_entry){ };
+
+ pdevinfo = (struct platform_device_info){
+ .name = GPIO_PROBE_DEFER_TEST_CONSUMER,
+ .id = PLATFORM_DEVID_NONE,
+ .data = &gpio_probe_defer_pdata_template,
+ .size_data = sizeof(gpio_probe_defer_pdata_template),
+ .properties = properties,
+ };
+
+ cons = kunit_platform_device_register_full(test, &pdevinfo);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, cons);
+
+ wait_for_device_probe();
+ scoped_guard(device, &cons->dev)
+ bound = device_is_bound(&cons->dev);
+
+ KUNIT_ASSERT_FALSE(test, bound);
+
+ pdata = dev_get_platdata(&cons->dev);
+ KUNIT_ASSERT_GT(test, pdata->probe_count, 0);
+ KUNIT_ASSERT_EQ(test, pdata->gpio_err, -EPROBE_DEFER);
+
+ fwnode = kunit_software_node_register(test, &gpio_test_provider_swnode);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, fwnode);
+
+ pdevinfo = (struct platform_device_info){
+ .name = GPIO_TEST_PROVIDER,
+ .id = PLATFORM_DEVID_NONE,
+ .swnode = &gpio_test_provider_swnode,
+ };
+
+ prvd = kunit_platform_device_register_full(test, &pdevinfo);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, prvd);
+
+ wait_for_device_probe();
+
+ scoped_guard(device, &prvd->dev)
+ bound = device_is_bound(&prvd->dev);
+ KUNIT_ASSERT_TRUE(test, bound);
+
+ scoped_guard(device, &cons->dev)
+ bound = device_is_bound(&cons->dev);
+ KUNIT_ASSERT_TRUE(test, bound);
+
+ pdata = dev_get_platdata(&cons->dev);
+ KUNIT_ASSERT_EQ(test, pdata->gpio_err, 0);
+}
+
+static struct kunit_case gpio_swnode_probe_order_tests[] = {
+ KUNIT_CASE(gpio_swnode_probe_order),
+ KUNIT_CASE(gpio_swnode_probe_defer_on_unregistered),
+ { }
+};
+
+static struct kunit_suite gpio_swnode_probe_order_test_suite = {
+ .name = "gpio-swnode-probe-order",
+ .test_cases = gpio_swnode_probe_order_tests,
+};
+
static BLOCKING_NOTIFIER_HEAD(gpio_unbind_notifier);

struct gpio_unbind_consumer_drvdata {
@@ -310,15 +558,24 @@ static void gpio_unbind_with_consumers(struct kunit *test)
0, GPIO_ACTIVE_HIGH);
properties[1] = (struct property_entry){ };

- pdevinfo = (struct platform_device_info){
- .name = GPIO_UNBIND_TEST_CONSUMER,
- .id = PLATFORM_DEVID_NONE,
- .properties = properties,
- };
-
- cons = kunit_platform_device_register_full(test, &pdevinfo);
+ /*
+ * This test deliberately keeps the consumer bound while the provider
+ * is unregistered. fw_devlink would force-unbind the consumer before
+ * the provider so use the FWNODE_FLAG_LINKS_ADDED flag to opt out of
+ * it as a workaround.
+ */
+ cons = kunit_platform_device_alloc(test, GPIO_UNBIND_TEST_CONSUMER,
+ PLATFORM_DEVID_NONE);
KUNIT_ASSERT_NOT_ERR_OR_NULL(test, cons);

+ ret = device_create_managed_software_node(&cons->dev, properties, NULL);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ fwnode_set_flag(dev_fwnode(&cons->dev), FWNODE_FLAG_LINKS_ADDED);
+
+ ret = kunit_platform_device_add(test, cons);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
wait_for_device_probe();
scoped_guard(device, &cons->dev)
bound = device_is_bound(&cons->dev);
@@ -350,6 +607,7 @@ static struct kunit_suite gpio_unbind_with_consumers_test_suite = {

kunit_test_suites(
&gpio_swnode_lookup_test_suite,
+ &gpio_swnode_probe_order_test_suite,
&gpio_unbind_with_consumers_test_suite,
);


--
2.47.3

Andy Shevchenko

unread,
Jun 29, 2026, 7:23:59 AMJun 29
to Bartosz Golaszewski, Brendan Higgins, David Gow, Rae Moar, Daniel Scally, Heikki Krogerus, Sakari Ailus, Bartosz Golaszewski, Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, Linus Walleij, Dmitry Torokhov, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org
On Mon, Jun 29, 2026 at 12:52:09PM +0200, Bartosz Golaszewski wrote:
> I've been working extensively on software nodes lately and introduced
> some changes. Add myself as reviewer so that I can help review any new
> proposed changes.

Acked-by: Andy Shevchenko <andriy.s...@linux.intel.com>
Thanks, I appreciate the move!

...

> +F: drivers/base/test/swnode-devlink-test.c

This should be part of the patch that bring this file to life.

--
With Best Regards,
Andy Shevchenko


Andy Shevchenko

unread,
Jun 29, 2026, 11:42:37 AMJun 29
to Bartosz Golaszewski, Herve Codina, Brendan Higgins, David Gow, Rae Moar, Daniel Scally, Heikki Krogerus, Sakari Ailus, Bartosz Golaszewski, Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, Linus Walleij, Dmitry Torokhov, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org
+Cc: Herve, to just ask the situation with one series that adds nice helper.
...

> +static int software_node_add_links(struct fwnode_handle *fwnode)
> +{
> + const struct software_node_ref_args *ref, *ref_array;
> + struct swnode *swnode = to_swnode(fwnode);
> + const struct property_entry *prop;
> + struct fwnode_handle *refnode;
> + unsigned int count, i;

'i' is local to the loop.
Seems we are going to have more of a such, perhaps make a helper for counting
(which will do that division beneath).

> + for (i = 0; i < count; i++) {

for (unsigned int i = 0; i < count; i++) {

> +

Redundant blank line?

> + if (ref->swnode)
> + refnode = software_node_fwnode(ref->swnode);
> + else if (ref->fwnode)
> + refnode = ref->fwnode;
> + else
> + continue;
> +
> + /* Supplier not registered yet, or self-reference. */
> + if (!refnode || refnode == &swnode->fwnode)
> + continue;
> +
> + fwnode_link_add(&swnode->fwnode, refnode, 0);
> + }
> + }
> +
> + return 0;
> +}

...

> + /*
> + * When the software node is the device's secondary firmware node, the
> + * core only records the owning device on the primary fwnode (see
> + * device_add()). fw_devlink resolves a supplier device through
> + * fwnode->dev, so without this a consumer referencing the software
> + * node could never find the supplier device and would defer forever.
> + * Make fwnode.dev point to its owner in that case.

The below seems more balanced in terms of line lengths.

* When the software node is the device's secondary firmware node,
* the core only records the owning device on the primary fwnode
* (see device_add()). fw_devlink resolves a supplier device through
* fwnode->dev, so without this a consumer referencing the software
* node could never find the supplier device and would defer forever.
* Make fwnode.dev point to its owner in that case.

> + */
> + if (dev_fwnode(dev) != &swnode->fwnode && !swnode->fwnode.dev)
> + swnode->fwnode.dev = dev;

Doesn't Herve's patch(es) add some helpers for this?
https://lore.kernel.org/lkml/20260511155930.34...@bootlin.com/

Ah, still not applied :-( Perhaps it can be applied as just part that brings
fw_devlink_set_device()?

> + /*
> + * Drop the device pointer mirrored onto a secondary software node in
> + * software_node_notify(). For a primary software node the core owns
> + * fwnode->dev and clears it in device_del().
> + */
> + if (dev_fwnode(dev) != &swnode->fwnode && swnode->fwnode.dev == dev)
> + swnode->fwnode.dev = NULL;

Andy Shevchenko

unread,
Jun 29, 2026, 11:49:17 AMJun 29
to Bartosz Golaszewski, Brendan Higgins, David Gow, Rae Moar, Daniel Scally, Heikki Krogerus, Sakari Ailus, Bartosz Golaszewski, Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, Linus Walleij, Dmitry Torokhov, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org
On Mon, Jun 29, 2026 at 12:52:08PM +0200, Bartosz Golaszewski wrote:
> Add a kunit test suite for fw_devlink support for software nodes.
>
> Most cases call add_links() directly and inspect the resulting fwnode
> supplier/consumer lists: a single reference, multiple references, a
> reference to an unregistered node, a "remote-endpoint" reference and a
> reference array. The last case is end-to-end - it registers real consumer
> and supplier platform devices together with their drivers, adds the
> consumer first and checks that fw_devlink defers its probe until the
> supplier has been bound.

...

> ---
> drivers/base/test/Kconfig | 5 +
> drivers/base/test/Makefile | 3 +
> drivers/base/test/swnode-devlink-test.c | 336 ++++++++++++++++++++++++++++++++

+ MAINTAINERS.

...


> CFLAGS_property-entry-test.o += $(DISABLE_STRUCTLEAK_PLUGIN)
> +
> +obj-$(CONFIG_DRIVER_SWNODE_KUNIT_TEST) += swnode-devlink-test.o
> +CFLAGS_swnode-devlink-test.o += $(DISABLE_STRUCTLEAK_PLUGIN)

Is it the case for this?

...

> +#include <linux/device.h>
> +#include <linux/fwnode.h>

+ list.h

> +#include <linux/platform_device.h>
> +#include <linux/property.h>
> +#include <linux/types.h>
> +#include <linux/wait.h>

...

> +static int swnode_count_suppliers(struct fwnode_handle *fwnode)
> +{
> + struct fwnode_link *link;
> + int ret = 0;

Why signed? Also it's not 'ret' semantically it's 'count'.

> + /*
> + * The suppliers and consumers lists should typically only be accessed
> + * with the fwnode_link_lock taken but it's private to the driver core.
> + *
> + * These are tests and at this point nobody should be modifying them so
> + * let's just access the list.
> + */
> + list_for_each_entry(link, &fwnode->suppliers, c_hook)
> + ret++;
> +
> + return ret;
> +}

...

> +/* A single reference creates exactly one supplier link, on both list ends. */
> +static void swnode_devlink_test_single_ref(struct kunit *test)
> +{
> + static const struct software_node supp_swnode = {
> + .name = "swnode-devlink-test-supplier"

Keep trailing comma.

> + };

> +

Redundant blank line.

> + struct fwnode_handle *cons_fwnode, *supp_fwnode;
> + int ret;
> +
> + const struct property_entry props[] = {
> + PROPERTY_ENTRY_REF("supplier", &supp_swnode),
> + { }
> + };
> +
> + supp_fwnode = kunit_software_node_register(test, &supp_swnode);
> + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, supp_fwnode);
> +
> + cons_fwnode = kunit_fwnode_create_software_node(test, props, NULL);
> + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, cons_fwnode);
> +
> + ret = fwnode_call_int_op(cons_fwnode, add_links);
> + KUNIT_EXPECT_EQ(test, ret, 0);
> +
> + KUNIT_EXPECT_EQ(test, swnode_count_suppliers(cons_fwnode), 1);
> + KUNIT_EXPECT_TRUE(test, swnode_has_link(cons_fwnode, supp_fwnode));
> +}

...

> +/* Multiple distinct references create multiple supplier links. */
> +static void swnode_devlink_test_multiple_refs(struct kunit *test)
> +{
> + static const struct software_node supp1_swnode = {
> + .name = "swnode-devlink-test-supplier-1"

Keep comma.

> + };
> + static const struct software_node supp2_swnode = {
> + .name = "swnode-devlink-test-supplier-2"

Ditto.

> + };
> + static const struct software_node *supp_nodes[] = {
> + &supp1_swnode, &supp2_swnode, NULL

Here it's fine.

> + };

> +

Redundant blank line.

> + const struct property_entry props[] = {
> + PROPERTY_ENTRY_REF("foo", &supp1_swnode),
> + PROPERTY_ENTRY_REF("bar", &supp2_swnode),
> + { }
> + };
> +
> + struct fwnode_handle *fwnode;
> + int ret;
> +
> + ret = kunit_software_node_register_node_group(test, supp_nodes);
> + KUNIT_ASSERT_EQ(test, ret, 0);
> +
> + fwnode = kunit_fwnode_create_software_node(test, props, NULL);
> + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, fwnode);
> +
> + ret = fwnode_call_int_op(fwnode, add_links);
> + KUNIT_EXPECT_EQ(test, ret, 0);
> +
> + KUNIT_EXPECT_EQ(test, swnode_count_suppliers(fwnode), 2);
> + KUNIT_EXPECT_TRUE(test, swnode_has_link(fwnode, software_node_fwnode(&supp1_swnode)));
> + KUNIT_EXPECT_TRUE(test, swnode_has_link(fwnode, software_node_fwnode(&supp2_swnode)));
> +}

...

> +static void swnode_devlink_test_unregistered_ref(struct kunit *test)

Same comments as per above.

...

> +static void swnode_devlink_test_remote_endpoint_excluded(struct kunit *test)

Ditto.

...

> +static void swnode_devlink_test_ref_array(struct kunit *test)

Ditto.

...

> +#define SWNODE_DEVLINK_TEST_TIMEOUT_MS 2000

(2 * MSEC_PER_SEC) ?
(will require time.h).

...

> +static int swnode_test_record_probe(struct platform_device *pdev)
> +{
> + struct swnode_test_probe_order *order = platform_get_drvdata(pdev);
> +
> + if (order && order->count < ARRAY_SIZE(order->probed)) {

+ array_size.h

> + order->probed[order->count++] = dev_name(&pdev->dev);
> + wake_up_interruptible(&order->wq);
> + }
> +
> + return 0;
> +}

...

> +static void swnode_devlink_test_probe_order(struct kunit *test)

As per above comments.

...

> +static struct kunit_suite swnode_test_suite = {
> + .name = "software-node-links",
> + .test_cases = swnode_test_cases,
> +};

> +

Redundant blank line.

> +kunit_test_suite(swnode_test_suite);

Andy Shevchenko

unread,
Jun 29, 2026, 11:58:27 AMJun 29
to Bartosz Golaszewski, Brendan Higgins, David Gow, Rae Moar, Daniel Scally, Heikki Krogerus, Sakari Ailus, Bartosz Golaszewski, Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, Linus Walleij, Dmitry Torokhov, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org
On Mon, Jun 29, 2026 at 12:52:10PM +0200, Bartosz Golaszewski wrote:
> The software node fw_devlink support already has its own kunit suite, but
> that verifies the fwnode links in isolation. Add GPIO tests that prove
> the ordering works in a real-life use-case: a GPIO consumer that
> references its provider via a software node.
>
> The first suite registers the provider's software node, adds the consumer
> device first and checks that fw_devlink defers its probe until the
> provider has been added and bound. The second covers the fallback:
> with the provider's software node not yet registered no supplier link is
> created, so the consumer probes, devm_gpiod_get() returns -EPROBE_DEFER
> and the consumer only binds once the provider shows up.
>
> While at it: the existing gpio_unbind_with_consumers() test keeps the
> consumer bound while the provider goes away and then operates the orphaned
> descriptor. With software nodes now being covered by fw_devlink that would
> instead force-unbind the consumer along with the provider, so opt it out
> by setting FWNODE_FLAG_LINKS_ADDED.

...

+ cleanup.h // guard()()
+ err.h // IS_ERR()

> #include <linux/platform_device.h>
> #include <linux/property.h>

+ types.h // bool

> +#include <kunit/fwnode.h>
> #include <kunit/platform_device.h>
> #include <kunit/test.h>

...

> + properties[1] = (struct property_entry){ };

Just zero the whole array at the definition time.

...

> + pdevinfo = (struct platform_device_info){

It's better to have a space after ).

> + .name = GPIO_PROBE_ORDER_TEST_CONSUMER,
> + .id = PLATFORM_DEVID_NONE,
> + .data = &gpio_probe_order_pdata_template,
> + .size_data = sizeof(gpio_probe_order_pdata_template),
> + .properties = properties,
> + };

...

> + pdevinfo = (struct platform_device_info){

Ditto.

> + .name = GPIO_TEST_PROVIDER,
> + .id = PLATFORM_DEVID_NONE,
> + .swnode = &gpio_test_provider_swnode,
> + };

...

> +struct gpio_probe_defer_pdata {
> + int probe_count;

Why is this signed?

> + int gpio_err;
> +};

...

> +static int gpio_probe_defer_consumer_probe(struct platform_device *pdev)
> +{
> + struct device *dev = &pdev->dev;
> + struct gpio_probe_defer_pdata *pdata = dev_get_platdata(dev);
> + struct gpio_desc *desc;

> + pdata->probe_count++;

Even in case of error?

> + desc = devm_gpiod_get(dev, "foo", GPIOD_OUT_HIGH);
> + if (IS_ERR(desc)) {
> + pdata->gpio_err = PTR_ERR(desc);
> + return pdata->gpio_err;
> + }
> +
> + pdata->gpio_err = 0;
> +
> + return 0;
> +}

...

> +static void gpio_swnode_probe_defer_on_unregistered(struct kunit *test)

As per above.

Herve Codina

unread,
Jun 30, 2026, 2:17:13 AMJun 30
to Andy Shevchenko, Bartosz Golaszewski, Brendan Higgins, David Gow, Rae Moar, Daniel Scally, Heikki Krogerus, Sakari Ailus, Bartosz Golaszewski, Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, Linus Walleij, Dmitry Torokhov, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org
Hi All,

On Mon, 29 Jun 2026 18:42:24 +0300
Andy Shevchenko <andriy.s...@linux.intel.com> wrote:

> +Cc: Herve, to just ask the situation with one series that adds nice helper.
>
...

>
> Doesn't Herve's patch(es) add some helpers for this?
> https://lore.kernel.org/lkml/20260511155930.34...@bootlin.com/
>
> Ah, still not applied :-( Perhaps it can be applied as just part that brings
> fw_devlink_set_device()?

Yes, still not applied.

I plan to send a new iteration of the series rebased on v7.2-rc1 and I Hope
it will be applied in this cycle.

Best regards,
Hervé

Bartosz Golaszewski

unread,
Jul 6, 2026, 7:34:33 AMJul 6
to Andy Shevchenko, Brendan Higgins, David Gow, Rae Moar, Daniel Scally, Heikki Krogerus, Sakari Ailus, Bartosz Golaszewski, Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, Linus Walleij, Dmitry Torokhov, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org, Bartosz Golaszewski, Herve Codina
Let's cross that bridge when we get there? I don't want to burden the series
with more changes than necessary.

>> + for (i = 0; i < count; i++) {
>
> for (unsigned int i = 0; i < count; i++) {
>
>> +
>
> Redundant blank line?

I think it's fine for readability.

I fixed the rest in v2.

Bart

Bartosz Golaszewski

unread,
Jul 6, 2026, 7:42:41 AMJul 6
to Andy Shevchenko, Brendan Higgins, David Gow, Rae Moar, Daniel Scally, Heikki Krogerus, Sakari Ailus, Bartosz Golaszewski, Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, Linus Walleij, Dmitry Torokhov, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org, Bartosz Golaszewski
On Mon, 29 Jun 2026 17:49:06 +0200, Andy Shevchenko
<andriy.s...@linux.intel.com> said:
> On Mon, Jun 29, 2026 at 12:52:08PM +0200, Bartosz Golaszewski wrote:
>> Add a kunit test suite for fw_devlink support for software nodes.
>>
>> Most cases call add_links() directly and inspect the resulting fwnode
>> supplier/consumer lists: a single reference, multiple references, a
>> reference to an unregistered node, a "remote-endpoint" reference and a
>> reference array. The last case is end-to-end - it registers real consumer
>> and supplier platform devices together with their drivers, adds the
>> consumer first and checks that fw_devlink defers its probe until the
>> supplier has been bound.
>
> ...
>
>> ---
>> drivers/base/test/Kconfig | 5 +
>> drivers/base/test/Makefile | 3 +
>> drivers/base/test/swnode-devlink-test.c | 336 ++++++++++++++++++++++++++++++++
>
> + MAINTAINERS.
>
> ...
>
>
>> +/* A single reference creates exactly one supplier link, on both list ends. */
>> +static void swnode_devlink_test_single_ref(struct kunit *test)
>> +{
>> + static const struct software_node supp_swnode = {
>> + .name = "swnode-devlink-test-supplier"
>
> Keep trailing comma.
>
>> + };
>
>> +
>
> Redundant blank line.
>

I actually leave a blank line between static, const and regular local variable
blocks for readability.

Bart

Bartosz Golaszewski

unread,
Jul 6, 2026, 8:32:33 AMJul 6
to Andy Shevchenko, Brendan Higgins, David Gow, Rae Moar, Daniel Scally, Heikki Krogerus, Sakari Ailus, Bartosz Golaszewski, Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, Linus Walleij, Dmitry Torokhov, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org, Bartosz Golaszewski
TBH most compound literals in the kernel seem to not add the space.
We're counting how many times we *enter* probe, so yes, I think it's right.

Bart

Bartosz Golaszewski

unread,
Jul 6, 2026, 8:54:37 AMJul 6
to Brendan Higgins, David Gow, Rae Moar, Andy Shevchenko, Daniel Scally, Heikki Krogerus, Sakari Ailus, Bartosz Golaszewski, Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, Linus Walleij, Dmitry Torokhov, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org, Bartosz Golaszewski
Merging strategy: with an Ack from Kunit maintainers, the entire series
can go through the driver core tree.

Signed-off-by: Bartosz Golaszewski <bartosz.g...@oss.qualcomm.com>
---
Changes in v2:
- Use loop-local variables where applicable
- Reformat a long comment
- Keep trailing commas on array initializations which may potentially be
extended in the future
- Add missing headers
- Update MAINTAINERS in commit adding a new file
- Initialize arrays at declaration
- Fix signedness of ints where needed
- Remove unneeded DISABLE_STRUCTLEAK_PLUGIN switch for swnode tests
- Link to v1: https://patch.msgid.link/20260629-swnode-fw-dev...@oss.qualcomm.com

---
Bartosz Golaszewski (5):
kunit: provide a set of fwnode-oriented helpers
software node: add fw_devlink support
software node: add kunit tests for fw_devlink support
MAINTAINERS: add myself as reviewer of software node support
gpio: kunit: add test cases verifying swnode devlink support

MAINTAINERS | 2 +
drivers/base/swnode.c | 79 ++++++++
drivers/base/test/Kconfig | 5 +
drivers/base/test/Makefile | 2 +
drivers/base/test/swnode-devlink-test.c | 339 ++++++++++++++++++++++++++++++++
drivers/gpio/gpiolib-kunit.c | 273 ++++++++++++++++++++++++-
include/kunit/fwnode.h | 26 +++
lib/kunit/Makefile | 1 +
lib/kunit/fwnode.c | 116 +++++++++++
9 files changed, 836 insertions(+), 7 deletions(-)
---
base-commit: 8cdeaa50eae8dad34885515f62559ee83e7e8dda

Bartosz Golaszewski

unread,
Jul 6, 2026, 8:54:40 AMJul 6
to Brendan Higgins, David Gow, Rae Moar, Andy Shevchenko, Daniel Scally, Heikki Krogerus, Sakari Ailus, Bartosz Golaszewski, Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, Linus Walleij, Dmitry Torokhov, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org, Bartosz Golaszewski
Provide three new kunit-managed helpers for test cases that need to
register/create dynamic software nodes.

Signed-off-by: Bartosz Golaszewski <bartosz.g...@oss.qualcomm.com>
---
include/kunit/fwnode.h | 26 +++++++++++
lib/kunit/Makefile | 1 +
lib/kunit/fwnode.c | 116 +++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 143 insertions(+)

diff --git a/include/kunit/fwnode.h b/include/kunit/fwnode.h
new file mode 100644
index 0000000000000000000000000000000000000000..239bc71eb5072ccead0beb51fc0882bab69c6877
--- /dev/null
+++ b/include/kunit/fwnode.h
@@ -0,0 +1,26 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * KUnit resource management helpers for firmware nodes.
+ *
+ * Copyright (C) Qualcomm Technologies, Inc. and/or its subsidiaries
+ */
+
new file mode 100644
index 0000000000000000000000000000000000000000..332490f07fae78e0fbf2930f9c80da0cc7dce028
--- /dev/null
+++ b/lib/kunit/fwnode.c
@@ -0,0 +1,116 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) Qualcomm Technologies, Inc. and/or its subsidiaries
+ */
+
+{
+ struct fwnode_handle *fwnode;
+ int ret;
+
+ fwnode = fwnode_create_software_node(properties, parent);
+ if (IS_ERR(fwnode))
+ return fwnode;
+
+{
+ struct fwnode_handle *fwnode;
+ int ret;
+
+ ret = software_node_register(swnode);
+ if (ret)
+ return ERR_PTR(ret);
+
+ fwnode = software_node_fwnode(swnode);
+ if (WARN_ON(!fwnode))
+ return ERR_PTR(-ENOENT);
+
+ int ret;
+
+ ret = software_node_register_node_group(nodes);
+ if (ret)
+ return ret;
+

Bartosz Golaszewski

unread,
Jul 6, 2026, 8:54:41 AMJul 6
to Brendan Higgins, David Gow, Rae Moar, Andy Shevchenko, Daniel Scally, Heikki Krogerus, Sakari Ailus, Bartosz Golaszewski, Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, Linus Walleij, Dmitry Torokhov, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org, Bartosz Golaszewski
Software nodes can be used to describe supplier-consumer relationships
between devices they represent using reference property entries. Unlike
for OF-nodes, driver core cannot yet use these references to create a
probe order that avoids needless probe deferrals on missing providers.

Implement software_node_add_links() modelled on of_fwnode_add_links().
For every DEV_PROP_REF property we resolve each referenced supplier and
create an fwnode link from the node to it. The driver core later promotes
these to device links and defers the consumer until the suppliers are
ready.

There's no allowlist like the one DT needs - devicetree phandles appear
in plenty of non-supplier contexts, but a software node only carries a
reference property when its author explicitly points at another node, so
we treat every reference as an intentional supplier dependency and link
all of them. Graph "remote-endpoint" references are skipped for now: they
go 2-ways between endpoint nodes and would create graph cycles without
the port-parent lifting DT does via get_con_dev(). References to
suppliers that aren't registered yet and self-references are ignored.

fw_devlink resolves the supplier device through fwnode->dev but the core
only records the owning device on the primary fwnode. When the software
node is a device's secondary fwnode, mirror the device pointer onto it in
software_node_notify() so the consumer can actually find the supplier
instead of deferring forever.

While at it: purge the fwnode links in software_node_release() now that
software nodes can own them.

Signed-off-by: Bartosz Golaszewski <bartosz.g...@oss.qualcomm.com>
---
drivers/base/swnode.c | 79 +++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 79 insertions(+)

diff --git a/drivers/base/swnode.c b/drivers/base/swnode.c
index 869228a65cb365567ddac7db6ad7b8743e0dbca9..e6e2d6926fef2db82eb1f2bf439e80aacf48668a 100644
--- a/drivers/base/swnode.c
+++ b/drivers/base/swnode.c
@@ -699,6 +699,62 @@ software_node_graph_parse_endpoint(const struct fwnode_handle *fwnode,
return 0;
}

+static int software_node_add_links(struct fwnode_handle *fwnode)
+{
+ const struct software_node_ref_args *ref, *ref_array;
+ struct swnode *swnode = to_swnode(fwnode);
+ const struct property_entry *prop;
+ struct fwnode_handle *refnode;
+ unsigned int count;
+
+
+ for (unsigned int i = 0; i < count; i++) {
+ ref = &ref_array[i];
+
+ if (ref->swnode)
+ refnode = software_node_fwnode(ref->swnode);
+ else if (ref->fwnode)
+ refnode = ref->fwnode;
+ else
+ continue;
+
+ /* Supplier not registered yet, or self-reference. */
+ if (!refnode || refnode == &swnode->fwnode)
+ continue;
+
+ fwnode_link_add(&swnode->fwnode, refnode, 0);
+ }
+ }
+
+ return 0;
+}
+
static const struct fwnode_operations software_node_ops = {
.get = software_node_get,
.put = software_node_put,
@@ -716,6 +772,7 @@ static const struct fwnode_operations software_node_ops = {
.graph_get_remote_endpoint = software_node_graph_get_remote_endpoint,
.graph_get_port_parent = software_node_graph_get_port_parent,
.graph_parse_endpoint = software_node_graph_parse_endpoint,
+ .add_links = software_node_add_links,
};

/* -------------------------------------------------------------------------- */
@@ -787,6 +844,8 @@ static void software_node_release(struct kobject *kobj)
{
struct swnode *swnode = kobj_to_swnode(kobj);

+ fwnode_links_purge(&swnode->fwnode);
+
if (swnode->parent) {
ida_free(&swnode->parent->child_ids, swnode->id);
list_del(&swnode->entry);
@@ -1105,6 +1164,17 @@ void software_node_notify(struct device *dev)
if (!swnode)
return;

+ /*
+ * When the software node is the device's secondary firmware node,
+ * the core only records the owning device on the primary fwnode
+ * (see device_add()). fw_devlink resolves a supplier device through
+ * fwnode->dev, so without this a consumer referencing the software
+ * node could never find the supplier device and would defer forever.
+ * Make fwnode.dev point to its owner in that case.
+ */
+ if (dev_fwnode(dev) != &swnode->fwnode && !swnode->fwnode.dev)
+ swnode->fwnode.dev = dev;
+
swnode_get(swnode);
ret = sysfs_create_link(&dev->kobj, &swnode->kobj, "software_node");
if (ret)
@@ -1127,6 +1197,15 @@ void software_node_notify_remove(struct device *dev)

sysfs_remove_link(&swnode->kobj, dev_name(dev));
sysfs_remove_link(&dev->kobj, "software_node");
+
+ /*
+ * Drop the device pointer mirrored onto a secondary software node in
+ * software_node_notify(). For a primary software node the core owns
+ * fwnode->dev and clears it in device_del().
+ */
+ if (dev_fwnode(dev) != &swnode->fwnode && swnode->fwnode.dev == dev)
+ swnode->fwnode.dev = NULL;

Bartosz Golaszewski

unread,
Jul 6, 2026, 8:54:44 AMJul 6
to Brendan Higgins, David Gow, Rae Moar, Andy Shevchenko, Daniel Scally, Heikki Krogerus, Sakari Ailus, Bartosz Golaszewski, Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, Linus Walleij, Dmitry Torokhov, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org, Bartosz Golaszewski
Add a kunit test suite for fw_devlink support for software nodes.

Most cases call add_links() directly and inspect the resulting fwnode
supplier/consumer lists: a single reference, multiple references, a
reference to an unregistered node, a "remote-endpoint" reference and a
reference array. The last case is end-to-end - it registers real consumer
and supplier platform devices together with their drivers, adds the
consumer first and checks that fw_devlink defers its probe until the
supplier has been bound.

Signed-off-by: Bartosz Golaszewski <bartosz.g...@oss.qualcomm.com>
---
MAINTAINERS | 1 +
drivers/base/test/Kconfig | 5 +
drivers/base/test/Makefile | 2 +
drivers/base/test/swnode-devlink-test.c | 339 ++++++++++++++++++++++++++++++++
4 files changed, 347 insertions(+)

diff --git a/MAINTAINERS b/MAINTAINERS
index 4a8b0fd665ce2447c3e89784b142d998f7384b95..d7741614c941ceed9e46e9caea72bad77cfb7618 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -25105,6 +25105,7 @@ L: linux...@vger.kernel.org
S: Maintained
F: drivers/base/property.c
F: drivers/base/swnode.c
+F: drivers/base/test/swnode-devlink-test.c
F: include/linux/fwnode.h
F: include/linux/property.h

diff --git a/drivers/base/test/Kconfig b/drivers/base/test/Kconfig
index 2756870615ccab67ec26d8671c1e4dba69342134..1ecf0791241a1b2eee7e1e787772217724abacb9 100644
--- a/drivers/base/test/Kconfig
+++ b/drivers/base/test/Kconfig
@@ -18,3 +18,8 @@ config DRIVER_PE_KUNIT_TEST
tristate "KUnit Tests for property entry API" if !KUNIT_ALL_TESTS
depends on KUNIT
default KUNIT_ALL_TESTS
+
+config DRIVER_SWNODE_KUNIT_TEST
+ tristate "KUnit Tests for software node fw_devlink links" if !KUNIT_ALL_TESTS
+ depends on KUNIT
+ default KUNIT_ALL_TESTS
diff --git a/drivers/base/test/Makefile b/drivers/base/test/Makefile
index e321dfc7e92266d2073d442f652cadb6e911dba5..9ced7bbd569fc49ba2719aa0cab57c7d8245dde1 100644
--- a/drivers/base/test/Makefile
+++ b/drivers/base/test/Makefile
@@ -6,3 +6,5 @@ obj-$(CONFIG_DM_KUNIT_TEST) += platform-device-test.o

obj-$(CONFIG_DRIVER_PE_KUNIT_TEST) += property-entry-test.o
CFLAGS_property-entry-test.o += $(DISABLE_STRUCTLEAK_PLUGIN)
+
+obj-$(CONFIG_DRIVER_SWNODE_KUNIT_TEST) += swnode-devlink-test.o
diff --git a/drivers/base/test/swnode-devlink-test.c b/drivers/base/test/swnode-devlink-test.c
new file mode 100644
index 0000000000000000000000000000000000000000..6f59f13214fcf39cebe02244bd0029470d3d104a
--- /dev/null
+++ b/drivers/base/test/swnode-devlink-test.c
@@ -0,0 +1,339 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (C) Qualcomm Technologies, Inc. and/or its subsidiaries
+ */
+
+#include <linux/array_size.h>
+#include <linux/device.h>
+#include <linux/fwnode.h>
+#include <linux/list.h>
+#include <linux/platform_device.h>
+#include <linux/property.h>
+#include <linux/time.h>
+#include <linux/types.h>
+#include <linux/wait.h>
+
+#include <kunit/fwnode.h>
+#include <kunit/platform_device.h>
+#include <kunit/test.h>
+
+static int swnode_count_suppliers(struct fwnode_handle *fwnode)
+{
+ struct fwnode_link *link;
+ unsigned int count = 0;
+
+ /*
+ * The suppliers and consumers lists should typically only be accessed
+ * with the fwnode_link_lock taken but it's private to the driver core.
+ *
+ * These are tests and at this point nobody should be modifying them so
+ * let's just access the list.
+ */
+ list_for_each_entry(link, &fwnode->suppliers, c_hook)
+ count++;
+
+ return count;
+}
+
+/* True if a supplier link con->sup exists, checked from both list ends. */
+static bool swnode_has_link(struct fwnode_handle *consumer,
+ struct fwnode_handle *supplier)
+{
+ bool from_con = false, from_sup = false;
+ struct fwnode_link *link;
+
+ list_for_each_entry(link, &consumer->suppliers, c_hook) {
+ if (link->supplier == supplier && link->consumer == consumer)
+ from_con = true;
+ }
+
+ list_for_each_entry(link, &supplier->consumers, s_hook) {
+ if (link->supplier == supplier && link->consumer == consumer)
+ from_sup = true;
+ }
+
+ return from_con && from_sup;
+}
+
+/* A single reference creates exactly one supplier link, on both list ends. */
+static void swnode_devlink_test_single_ref(struct kunit *test)
+{
+ static const struct software_node supp_swnode = {
+ .name = "swnode-devlink-test-supplier",
+ };
+
+ struct fwnode_handle *cons_fwnode, *supp_fwnode;
+ int ret;
+
+ const struct property_entry props[] = {
+ PROPERTY_ENTRY_REF("supplier", &supp_swnode),
+ { }
+ };
+
+ supp_fwnode = kunit_software_node_register(test, &supp_swnode);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, supp_fwnode);
+
+ cons_fwnode = kunit_fwnode_create_software_node(test, props, NULL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, cons_fwnode);
+
+ ret = fwnode_call_int_op(cons_fwnode, add_links);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+
+ KUNIT_EXPECT_EQ(test, swnode_count_suppliers(cons_fwnode), 1);
+ KUNIT_EXPECT_TRUE(test, swnode_has_link(cons_fwnode, supp_fwnode));
+}
+
+/* Multiple distinct references create multiple supplier links. */
+static void swnode_devlink_test_multiple_refs(struct kunit *test)
+{
+ static const struct software_node supp1_swnode = {
+ .name = "swnode-devlink-test-supplier-1",
+ };
+ static const struct software_node supp2_swnode = {
+ .name = "swnode-devlink-test-supplier-2",
+ };
+ static const struct software_node *supp_nodes[] = {
+ &supp1_swnode, &supp2_swnode, NULL
+ };
+
+ const struct property_entry props[] = {
+ PROPERTY_ENTRY_REF("foo", &supp1_swnode),
+ PROPERTY_ENTRY_REF("bar", &supp2_swnode),
+ { }
+ };
+
+ struct fwnode_handle *fwnode;
+ int ret;
+
+ ret = kunit_software_node_register_node_group(test, supp_nodes);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ fwnode = kunit_fwnode_create_software_node(test, props, NULL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, fwnode);
+
+ ret = fwnode_call_int_op(fwnode, add_links);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+
+ KUNIT_EXPECT_EQ(test, swnode_count_suppliers(fwnode), 2);
+ KUNIT_EXPECT_TRUE(test, swnode_has_link(fwnode, software_node_fwnode(&supp1_swnode)));
+ KUNIT_EXPECT_TRUE(test, swnode_has_link(fwnode, software_node_fwnode(&supp2_swnode)));
+}
+
+/* A reference to an unregistered node creates no link (graceful skip). */
+static void swnode_devlink_test_unregistered_ref(struct kunit *test)
+{
+ static const struct software_node supp_swnode = {
+ .name = "swnode-devlink-test-supplier",
+ };
+
+ const struct property_entry props[] = {
+ PROPERTY_ENTRY_REF("supplier", &supp_swnode),
+ { }
+ };
+
+ struct fwnode_handle *fwnode;
+ int ret;
+
+ fwnode = kunit_fwnode_create_software_node(test, props, NULL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, fwnode);
+
+ ret = fwnode_call_int_op(fwnode, add_links);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, swnode_count_suppliers(fwnode), 0);
+}
+
+/* Graph "remote-endpoint" references are excluded. */
+static void swnode_devlink_test_remote_endpoint_excluded(struct kunit *test)
+{
+ static const struct software_node ep_swnode = {
+ .name = "swnode-devlink-test-end-point"
+ };
+
+ const struct property_entry props[] = {
+ PROPERTY_ENTRY_REF("remote-endpoint", &ep_swnode),
+ { }
+ };
+
+ struct fwnode_handle *cons_fwnode, *supp_fwnode;
+ int ret;
+
+ supp_fwnode = kunit_software_node_register(test, &ep_swnode);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, supp_fwnode);
+
+ cons_fwnode = kunit_fwnode_create_software_node(test, props, NULL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, cons_fwnode);
+
+ ret = fwnode_call_int_op(cons_fwnode, add_links);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, swnode_count_suppliers(cons_fwnode), 0);
+}
+
+/* A reference array creates one link per registered element. */
+static void swnode_devlink_test_ref_array(struct kunit *test)
+{
+ static const struct software_node supp1_swnode = {
+ .name = "swnode-devlink-test-supplier-1",
+ };
+ static const struct software_node supp2_swnode = {
+ .name = "swnode-devlink-test-supplier-2",
+ };
+ static const struct software_node *supp_nodes[] = {
+ &supp1_swnode, &supp2_swnode, NULL
+ };
+ static const struct software_node_ref_args refs[] = {
+ SOFTWARE_NODE_REFERENCE(&supp1_swnode),
+ SOFTWARE_NODE_REFERENCE(&supp2_swnode, 4, 2),
+ };
+
+ const struct property_entry props[] = {
+ PROPERTY_ENTRY_REF_ARRAY("suppliers", refs),
+ { }
+ };
+
+ struct fwnode_handle *fwnode;
+ int ret;
+
+ ret = kunit_software_node_register_node_group(test, supp_nodes);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ fwnode = kunit_fwnode_create_software_node(test, props, NULL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, fwnode);
+
+ ret = fwnode_call_int_op(fwnode, add_links);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+
+ KUNIT_EXPECT_EQ(test, swnode_count_suppliers(fwnode), 2);
+ KUNIT_EXPECT_TRUE(test, swnode_has_link(fwnode, software_node_fwnode(&supp1_swnode)));
+ KUNIT_EXPECT_TRUE(test, swnode_has_link(fwnode, software_node_fwnode(&supp2_swnode)));
+}
+
+/*
+ * End-to-end test: fw_devlink must defer a consumer's probe until its
+ * supplier has probed.
+ *
+ * The reference created by software_node_add_links() is only useful if the
+ * driver core promotes it to a real device_link and uses it to order probing.
+ * This test drives actual probing through the platform bus and asserts the
+ * supplier binds before the consumer.
+ */
+
+#define SWNODE_DEVLINK_TEST_SUPPLIER "swnode-link-supplier"
+#define SWNODE_DEVLINK_TEST_CONSUMER "swnode-link-consumer"
+#define SWNODE_DEVLINK_TEST_TIMEOUT_MS (2 * MSEC_PER_SEC)
+
+struct swnode_test_probe_order {
+ /* Names in the order their drivers' .probe ran. */
+ const char *probed[2];
+ unsigned int count;
+ wait_queue_head_t wq;
+};
+
+static int swnode_test_record_probe(struct platform_device *pdev)
+{
+ struct swnode_test_probe_order *order = platform_get_drvdata(pdev);
+
+ if (order && order->count < ARRAY_SIZE(order->probed)) {
+ order->probed[order->count++] = dev_name(&pdev->dev);
+ wake_up_interruptible(&order->wq);
+ }
+
+ return 0;
+}
+
+static struct platform_driver swnode_test_supplier_driver = {
+ .probe = swnode_test_record_probe,
+ .driver = {
+ .name = SWNODE_DEVLINK_TEST_SUPPLIER,
+ },
+};
+
+static struct platform_driver swnode_test_consumer_driver = {
+ .probe = swnode_test_record_probe,
+ .driver = {
+ .name = SWNODE_DEVLINK_TEST_CONSUMER,
+ },
+};
+
+static void swnode_devlink_test_probe_order(struct kunit *test)
+{
+ static const struct software_node supplier_swnode = {
+ .name = "swnode-devlink-test-supplier",
+ };
+
+ const struct property_entry consumer_props[] = {
+ PROPERTY_ENTRY_REF("supplier-ref", &supplier_swnode),
+ { }
+ };
+
+ struct platform_device *supplier, *consumer;
+ struct swnode_test_probe_order *order;
+ struct fwnode_handle *fwnode;
+ int ret;
+
+ order = kunit_kzalloc(test, sizeof(*order), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, order);
+ init_waitqueue_head(&order->wq);
+
+ fwnode = kunit_software_node_register(test, &supplier_swnode);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, fwnode);
+
+ ret = kunit_platform_driver_register(test, &swnode_test_supplier_driver);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ret = kunit_platform_driver_register(test, &swnode_test_consumer_driver);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ supplier = kunit_platform_device_alloc(test, SWNODE_DEVLINK_TEST_SUPPLIER,
+ PLATFORM_DEVID_NONE);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, supplier);
+ consumer = kunit_platform_device_alloc(test, SWNODE_DEVLINK_TEST_CONSUMER,
+ PLATFORM_DEVID_NONE);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, consumer);
+
+ platform_set_drvdata(supplier, order);
+ platform_set_drvdata(consumer, order);
+
+ ret = device_add_software_node(&supplier->dev, &supplier_swnode);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ret = device_create_managed_software_node(&consumer->dev,
+ consumer_props, NULL);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ ret = kunit_platform_device_add(test, consumer);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ret = kunit_platform_device_add(test, supplier);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ ret = wait_event_interruptible_timeout(order->wq,
+ order->count == 2,
+ msecs_to_jiffies(SWNODE_DEVLINK_TEST_TIMEOUT_MS));
+ KUNIT_ASSERT_GT(test, ret, 0);
+
+ KUNIT_EXPECT_STREQ(test, order->probed[0], SWNODE_DEVLINK_TEST_SUPPLIER);
+ KUNIT_EXPECT_STREQ(test, order->probed[1], SWNODE_DEVLINK_TEST_CONSUMER);
+
+ /* Tear down the consumer (and its device link) before the supplier. */
+ kunit_platform_device_unregister(test, consumer);
+
+ device_remove_software_node(&supplier->dev);
+}
+
+static struct kunit_case swnode_test_cases[] = {
+ KUNIT_CASE(swnode_devlink_test_single_ref),
+ KUNIT_CASE(swnode_devlink_test_multiple_refs),
+ KUNIT_CASE(swnode_devlink_test_unregistered_ref),
+ KUNIT_CASE(swnode_devlink_test_remote_endpoint_excluded),
+ KUNIT_CASE(swnode_devlink_test_ref_array),
+ KUNIT_CASE(swnode_devlink_test_probe_order),
+ { }
+};
+
+static struct kunit_suite swnode_test_suite = {
+ .name = "software-node-links",
+ .test_cases = swnode_test_cases,
+};
+

Bartosz Golaszewski

unread,
Jul 6, 2026, 8:54:44 AMJul 6
to Brendan Higgins, David Gow, Rae Moar, Andy Shevchenko, Daniel Scally, Heikki Krogerus, Sakari Ailus, Bartosz Golaszewski, Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, Linus Walleij, Dmitry Torokhov, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org, Bartosz Golaszewski
I've been working extensively on software nodes lately and introduced
some changes. Add myself as reviewer so that I can help review any new
proposed changes.

Acked-by: Andy Shevchenko <andriy.s...@linux.intel.com>
Signed-off-by: Bartosz Golaszewski <bartosz.g...@oss.qualcomm.com>
---
MAINTAINERS | 1 +
1 file changed, 1 insertion(+)

diff --git a/MAINTAINERS b/MAINTAINERS
index d7741614c941ceed9e46e9caea72bad77cfb7618..da886aa69f362f4cb9dc529a26f8dfc67649d7c6 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -25101,6 +25101,7 @@ R: Andy Shevchenko <andriy.s...@linux.intel.com>
R: Daniel Scally <djrs...@gmail.com>
R: Heikki Krogerus <heikki....@linux.intel.com>
R: Sakari Ailus <sakari...@linux.intel.com>
+R: Bartosz Golaszewski <br...@kernel.org>
L: linux...@vger.kernel.org
S: Maintained
F: drivers/base/property.c

--
2.47.3

Bartosz Golaszewski

unread,
Jul 6, 2026, 8:54:47 AMJul 6
to Brendan Higgins, David Gow, Rae Moar, Andy Shevchenko, Daniel Scally, Heikki Krogerus, Sakari Ailus, Bartosz Golaszewski, Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, Linus Walleij, Dmitry Torokhov, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org, Bartosz Golaszewski
The software node fw_devlink support already has its own kunit suite, but
that verifies the fwnode links in isolation. Add GPIO tests that prove
the ordering works in a real-life use-case: a GPIO consumer that
references its provider via a software node.

The first suite registers the provider's software node, adds the consumer
device first and checks that fw_devlink defers its probe until the
provider has been added and bound. The second covers the fallback:
with the provider's software node not yet registered no supplier link is
created, so the consumer probes, devm_gpiod_get() returns -EPROBE_DEFER
and the consumer only binds once the provider shows up.

While at it: the existing gpio_unbind_with_consumers() test keeps the
consumer bound while the provider goes away and then operates the orphaned
descriptor. With software nodes now being covered by fw_devlink that would
instead force-unbind the consumer along with the provider, so opt it out
by setting FWNODE_FLAG_LINKS_ADDED.

Signed-off-by: Bartosz Golaszewski <bartosz.g...@oss.qualcomm.com>
---
drivers/gpio/gpiolib-kunit.c | 273 +++++++++++++++++++++++++++++++++++++++++--
1 file changed, 266 insertions(+), 7 deletions(-)

diff --git a/drivers/gpio/gpiolib-kunit.c b/drivers/gpio/gpiolib-kunit.c
index 380b68f879e55433668353bb88067d561142a5bc..fe984a283318849df26ced1c1e572734f7e154ae 100644
--- a/drivers/gpio/gpiolib-kunit.c
+++ b/drivers/gpio/gpiolib-kunit.c
@@ -3,6 +3,8 @@
* Copyright (C) Qualcomm Technologies, Inc. and/or its subsidiaries
*/

+#include <linux/cleanup.h>
+#include <linux/err.h>
#include <linux/fwnode.h>
#include <linux/gpio/consumer.h>
#include <linux/gpio/driver.h>
@@ -11,12 +13,16 @@
#include <linux/notifier.h>
#include <linux/platform_device.h>
#include <linux/property.h>
+#include <linux/types.h>

+#include <kunit/fwnode.h>
#include <kunit/platform_device.h>
#include <kunit/test.h>

#define GPIO_TEST_PROVIDER "gpio-test-provider"
#define GPIO_SWNODE_TEST_CONSUMER "gpio-swnode-test-consumer"
+#define GPIO_PROBE_ORDER_TEST_CONSUMER "gpio-probe-order-test-consumer"
+#define GPIO_PROBE_DEFER_TEST_CONSUMER "gpio-probe-defer-test-consumer"
#define GPIO_UNBIND_TEST_CONSUMER "gpio-unbind-test-consumer"

static int gpio_test_provider_get_direction(struct gpio_chip *gc, unsigned int offset)
@@ -213,6 +219,249 @@ static struct kunit_suite gpio_swnode_lookup_test_suite = {
.test_cases = gpio_swnode_lookup_tests,
};

+static void gpio_swnode_unregister_swnode(void *data)
+{
+ software_node_unregister(data);
+}
+
+struct gpio_probe_order_pdata {
+ unsigned int probe_count;
+ bool gpio_ok;
+};
+
+static const struct gpio_probe_order_pdata gpio_probe_order_pdata_template = {
+ .probe_count = 0,
+ .gpio_ok = false,
+};
+
+static int gpio_probe_order_consumer_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct gpio_probe_order_pdata *pdata = dev_get_platdata(dev);
+ struct gpio_desc *desc;
+
+ pdata->probe_count++;
+
+ desc = devm_gpiod_get(dev, "foo", GPIOD_OUT_HIGH);
+ if (IS_ERR(desc))
+ return PTR_ERR(desc);
+
+ pdata->gpio_ok = true;
+
+ return 0;
+}
+
+static struct platform_driver gpio_probe_order_consumer_driver = {
+ .probe = gpio_probe_order_consumer_probe,
+ .driver = {
+ .name = GPIO_PROBE_ORDER_TEST_CONSUMER,
+ },
+};
+
+/*
+ * Verify that fw_devlink orders the probe of a GPIO consumer after its
+ * provider. The consumer references the provider through a software node and
+ * is registered first. fw_devlink must defer it before its driver's probe()
+ * is ever entered, so the consumer probes exactly once - only after the
+ * provider is added and bound.
+ */
+static void gpio_swnode_probe_order(struct kunit *test)
+{
+ struct property_entry properties[2] = { };
+ struct gpio_probe_order_pdata *pdata;
+ struct platform_device_info pdevinfo;
+ struct platform_device *prvd, *cons;
+ bool bound = false;
+ int ret;
+
+ ret = kunit_platform_driver_register(test, &gpio_test_provider_driver);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ ret = kunit_platform_driver_register(test, &gpio_probe_order_consumer_driver);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ ret = software_node_register(&gpio_test_provider_swnode);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ ret = kunit_add_action_or_reset(test, gpio_swnode_unregister_swnode,
+ (void *)&gpio_test_provider_swnode);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ properties[0] = PROPERTY_ENTRY_GPIO("foo-gpios",
+ &gpio_test_provider_swnode,
+ 0, GPIO_ACTIVE_HIGH);
+
+ pdevinfo = (struct platform_device_info){
+ .name = GPIO_PROBE_ORDER_TEST_CONSUMER,
+ .id = PLATFORM_DEVID_NONE,
+ .data = &gpio_probe_order_pdata_template,
+ .size_data = sizeof(gpio_probe_order_pdata_template),
+ .properties = properties,
+ };
+
+ cons = kunit_platform_device_register_full(test, &pdevinfo);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, cons);
+
+ wait_for_device_probe();
+ scoped_guard(device, &cons->dev)
+ bound = device_is_bound(&cons->dev);
+
+ KUNIT_ASSERT_FALSE(test, bound);
+
+ pdata = dev_get_platdata(&cons->dev);
+ KUNIT_ASSERT_EQ(test, pdata->probe_count, 0);
+ KUNIT_ASSERT_FALSE(test, pdata->gpio_ok);
+
+ pdevinfo = (struct platform_device_info){
+ .name = GPIO_TEST_PROVIDER,
+ .id = PLATFORM_DEVID_NONE,
+ .swnode = &gpio_test_provider_swnode,
+ };
+
+ prvd = kunit_platform_device_register_full(test, &pdevinfo);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, prvd);
+
+ wait_for_device_probe();
+
+ scoped_guard(device, &prvd->dev)
+ bound = device_is_bound(&prvd->dev);
+ KUNIT_ASSERT_TRUE(test, bound);
+
+ scoped_guard(device, &cons->dev)
+ bound = device_is_bound(&cons->dev);
+ KUNIT_ASSERT_TRUE(test, bound);
+
+ pdata = dev_get_platdata(&cons->dev);
+ KUNIT_ASSERT_EQ(test, pdata->probe_count, 1);
+ KUNIT_ASSERT_TRUE(test, pdata->gpio_ok);
+}
+
+struct gpio_probe_defer_pdata {
+ unsigned int probe_count;
+ int gpio_err;
+};
+
+static const struct gpio_probe_defer_pdata gpio_probe_defer_pdata_template = {
+ .probe_count = 0,
+ .gpio_err = 0,
+};
+
+static int gpio_probe_defer_consumer_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct gpio_probe_defer_pdata *pdata = dev_get_platdata(dev);
+ struct gpio_desc *desc;
+
+ pdata->probe_count++;
+
+ desc = devm_gpiod_get(dev, "foo", GPIOD_OUT_HIGH);
+ if (IS_ERR(desc)) {
+ pdata->gpio_err = PTR_ERR(desc);
+ return pdata->gpio_err;
+ }
+
+ pdata->gpio_err = 0;
+
+ return 0;
+}
+
+static struct platform_driver gpio_probe_defer_consumer_driver = {
+ .probe = gpio_probe_defer_consumer_probe,
+ .driver = {
+ .name = GPIO_PROBE_DEFER_TEST_CONSUMER,
+ },
+};
+
+/*
+ * Verify that a GPIO consumer referencing a provider whose software node is
+ * not registered yet, defers its probe instead of failing.
+ *
+ * The provider software node is deliberately left unregistered when the
+ * consumer is added. fw_devlink cannot resolve the reference, so it creates no
+ * supplier link and does not order the consumer - the consumer's probe() runs
+ * and reaches devm_gpiod_get(). The swnode GPIO lookup returns -ENOTCONN for a
+ * reference to an unregistered node, which gpiolib maps to -EPROBE_DEFER. Once
+ * the provider software node and device appear, the deferred consumer probes
+ * again and binds.
+ */
+static void gpio_swnode_probe_defer_on_unregistered(struct kunit *test)
+{
+ struct property_entry properties[2] = { };
+ struct gpio_probe_defer_pdata *pdata;
+ struct platform_device_info pdevinfo;
+ struct platform_device *prvd, *cons;
+ struct fwnode_handle *fwnode;
+ bool bound = false;
+ int ret;
+
+ ret = kunit_platform_driver_register(test, &gpio_test_provider_driver);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ ret = kunit_platform_driver_register(test, &gpio_probe_defer_consumer_driver);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ properties[0] = PROPERTY_ENTRY_GPIO("foo-gpios",
+ &gpio_test_provider_swnode,
+ 0, GPIO_ACTIVE_HIGH);
+
+ pdevinfo = (struct platform_device_info){
+ .name = GPIO_PROBE_DEFER_TEST_CONSUMER,
+ .id = PLATFORM_DEVID_NONE,
+ .data = &gpio_probe_defer_pdata_template,
+ .size_data = sizeof(gpio_probe_defer_pdata_template),
+ .properties = properties,
+ };
+
+ cons = kunit_platform_device_register_full(test, &pdevinfo);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, cons);
+
+ wait_for_device_probe();
+ scoped_guard(device, &cons->dev)
+ bound = device_is_bound(&cons->dev);
+
+ KUNIT_ASSERT_FALSE(test, bound);
+
+ pdata = dev_get_platdata(&cons->dev);
+ KUNIT_ASSERT_GT(test, pdata->probe_count, 0);
+ KUNIT_ASSERT_EQ(test, pdata->gpio_err, -EPROBE_DEFER);
+
+ fwnode = kunit_software_node_register(test, &gpio_test_provider_swnode);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, fwnode);
+
+ pdevinfo = (struct platform_device_info){
+ .name = GPIO_TEST_PROVIDER,
+ .id = PLATFORM_DEVID_NONE,
+ .swnode = &gpio_test_provider_swnode,
+ };
+
+ prvd = kunit_platform_device_register_full(test, &pdevinfo);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, prvd);
+
+ wait_for_device_probe();
+
+ scoped_guard(device, &prvd->dev)
+ bound = device_is_bound(&prvd->dev);
+ KUNIT_ASSERT_TRUE(test, bound);
+
+ scoped_guard(device, &cons->dev)
+ bound = device_is_bound(&cons->dev);
+ KUNIT_ASSERT_TRUE(test, bound);
+
+ pdata = dev_get_platdata(&cons->dev);
+ KUNIT_ASSERT_EQ(test, pdata->gpio_err, 0);
+}
+
+static struct kunit_case gpio_swnode_probe_order_tests[] = {
+ KUNIT_CASE(gpio_swnode_probe_order),
+ KUNIT_CASE(gpio_swnode_probe_defer_on_unregistered),
+ { }
+};
+
+static struct kunit_suite gpio_swnode_probe_order_test_suite = {
+ .name = "gpio-swnode-probe-order",
+ .test_cases = gpio_swnode_probe_order_tests,
+};
+
static BLOCKING_NOTIFIER_HEAD(gpio_unbind_notifier);

struct gpio_unbind_consumer_drvdata {
@@ -310,15 +559,24 @@ static void gpio_unbind_with_consumers(struct kunit *test)
0, GPIO_ACTIVE_HIGH);
properties[1] = (struct property_entry){ };

- pdevinfo = (struct platform_device_info){
- .name = GPIO_UNBIND_TEST_CONSUMER,
- .id = PLATFORM_DEVID_NONE,
- .properties = properties,
- };
-
- cons = kunit_platform_device_register_full(test, &pdevinfo);
+ /*
+ * This test deliberately keeps the consumer bound while the provider
+ * is unregistered. fw_devlink would force-unbind the consumer before
+ * the provider so use the FWNODE_FLAG_LINKS_ADDED flag to opt out of
+ * it as a workaround.
+ */
+ cons = kunit_platform_device_alloc(test, GPIO_UNBIND_TEST_CONSUMER,
+ PLATFORM_DEVID_NONE);
KUNIT_ASSERT_NOT_ERR_OR_NULL(test, cons);

+ ret = device_create_managed_software_node(&cons->dev, properties, NULL);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ fwnode_set_flag(dev_fwnode(&cons->dev), FWNODE_FLAG_LINKS_ADDED);
+
+ ret = kunit_platform_device_add(test, cons);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
wait_for_device_probe();
scoped_guard(device, &cons->dev)
bound = device_is_bound(&cons->dev);
@@ -350,6 +608,7 @@ static struct kunit_suite gpio_unbind_with_consumers_test_suite = {

Andy Shevchenko

unread,
Jul 6, 2026, 1:25:07 PMJul 6
to Bartosz Golaszewski, Brendan Higgins, David Gow, Rae Moar, Daniel Scally, Heikki Krogerus, Sakari Ailus, Bartosz Golaszewski, Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, Linus Walleij, Dmitry Torokhov, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org
...

> + /*
> + * When the software node is the device's secondary firmware node,
> + * the core only records the owning device on the primary fwnode
> + * (see device_add()). fw_devlink resolves a supplier device through
> + * fwnode->dev, so without this a consumer referencing the software
> + * node could never find the supplier device and would defer forever.
> + * Make fwnode.dev point to its owner in that case.
> + */
> + if (dev_fwnode(dev) != &swnode->fwnode && !swnode->fwnode.dev)

Can we use device_match_fwnode() here?

> + swnode->fwnode.dev = dev;
> +

...

> + /*
> + * Drop the device pointer mirrored onto a secondary software node in
> + * software_node_notify(). For a primary software node the core owns
> + * fwnode->dev and clears it in device_del().
> + */
> + if (dev_fwnode(dev) != &swnode->fwnode && swnode->fwnode.dev == dev)
> + swnode->fwnode.dev = NULL;

Ditto.

Note, it does check passed fwnode pointer against NULL, but looking at
the above &swnode->fwnode must never be NULL, so basically device_match_node()
will be an equivalent replacement.

Andy Shevchenko

unread,
Jul 6, 2026, 4:05:51 PMJul 6
to Bartosz Golaszewski, Brendan Higgins, David Gow, Rae Moar, Daniel Scally, Heikki Krogerus, Sakari Ailus, Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, Linus Walleij, Dmitry Torokhov, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org, Bartosz Golaszewski
On Mon, Jul 06, 2026 at 07:42:36AM -0400, Bartosz Golaszewski wrote:
> On Mon, 29 Jun 2026 17:49:06 +0200, Andy Shevchenko
> <andriy.s...@linux.intel.com> said:
> > On Mon, Jun 29, 2026 at 12:52:08PM +0200, Bartosz Golaszewski wrote:

...

> >> + static const struct software_node supp_swnode = {
> >> + .name = "swnode-devlink-test-supplier"
> >
> > Keep trailing comma.
> >
> >> + };
> >
> >> +
> >
> > Redundant blank line.
> >
>
> I actually leave a blank line between static, const and regular local variable
> blocks for readability.

As far as I know this is against the style.

Andy Shevchenko

unread,
Jul 6, 2026, 4:10:09 PMJul 6
to Bartosz Golaszewski, Brendan Higgins, David Gow, Rae Moar, Daniel Scally, Heikki Krogerus, Sakari Ailus, Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, Linus Walleij, Dmitry Torokhov, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org, Bartosz Golaszewski
Perhaps a (short) comment on top of this line?

/* Count probes independently on being successful or unsuccessful */

Bartosz Golaszewski

unread,
Jul 7, 2026, 3:25:47 AMJul 7
to Andy Shevchenko, Brendan Higgins, David Gow, Rae Moar, Daniel Scally, Heikki Krogerus, Sakari Ailus, Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, Linus Walleij, Dmitry Torokhov, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org, Bartosz Golaszewski, Bartosz Golaszewski
On Mon, 6 Jul 2026 22:09:39 +0200, Andy Shevchenko
<andriy.s...@linux.intel.com> said:
> On Mon, Jul 06, 2026 at 08:32:28AM -0400, Bartosz Golaszewski wrote:
>> On Mon, 29 Jun 2026 17:58:16 +0200, Andy Shevchenko
>> <andriy.s...@linux.intel.com> said:
>> > On Mon, Jun 29, 2026 at 12:52:10PM +0200, Bartosz Golaszewski wrote:
>
> ...
>
>> >> +static int gpio_probe_defer_consumer_probe(struct platform_device *pdev)
>> >> +{
>> >> + struct device *dev = &pdev->dev;
>> >> + struct gpio_probe_defer_pdata *pdata = dev_get_platdata(dev);
>> >> + struct gpio_desc *desc;
>> >
>> >> + pdata->probe_count++;
>> >
>> > Even in case of error?
>>
>> We're counting how many times we *enter* probe, so yes, I think it's right.
>
> Perhaps a (short) comment on top of this line?
>
> /* Count probes independently on being successful or unsuccessful */
>
> --
> With Best Regards,
> Andy Shevchenko
>
>
>

I mean: sure, I can, but it already says probe_count, not probe_ok_count. IMO
it's overkill but I don't want to bikeshed over a comment.

Bart

Bartosz Golaszewski

unread,
Jul 7, 2026, 3:27:06 AMJul 7
to Andy Shevchenko, Brendan Higgins, David Gow, Rae Moar, Daniel Scally, Heikki Krogerus, Sakari Ailus, Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, Linus Walleij, Dmitry Torokhov, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org, Bartosz Golaszewski, Bartosz Golaszewski
On Mon, 6 Jul 2026 22:05:43 +0200, Andy Shevchenko
<andriy.s...@linux.intel.com> said:
> On Mon, Jul 06, 2026 at 07:42:36AM -0400, Bartosz Golaszewski wrote:
>> On Mon, 29 Jun 2026 17:49:06 +0200, Andy Shevchenko
>> <andriy.s...@linux.intel.com> said:
>> > On Mon, Jun 29, 2026 at 12:52:08PM +0200, Bartosz Golaszewski wrote:
>
> ...
>
>> >> + static const struct software_node supp_swnode = {
>> >> + .name = "swnode-devlink-test-supplier"
>> >
>> > Keep trailing comma.
>> >
>> >> + };
>> >
>> >> +
>> >
>> > Redundant blank line.
>> >
>>
>> I actually leave a blank line between static, const and regular local variable
>> blocks for readability.
>
> As far as I know this is against the style.
>

I don't think the docs mention this particular situation - correct me if I'm
wrong. Since I wrote it, I'll likely maintain this file and my personal
preference is to keep the newlines. I hope it's fine.

Bart

Andy Shevchenko

unread,
Jul 8, 2026, 7:40:35 AMJul 8
to Bartosz Golaszewski, Brendan Higgins, David Gow, Rae Moar, Daniel Scally, Heikki Krogerus, Sakari Ailus, Bartosz Golaszewski, Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, Linus Walleij, Dmitry Torokhov, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org
On Mon, Jul 06, 2026 at 02:54:10PM +0200, Bartosz Golaszewski wrote:
> Add a kunit test suite for fw_devlink support for software nodes.
>
> Most cases call add_links() directly and inspect the resulting fwnode
> supplier/consumer lists: a single reference, multiple references, a
> reference to an unregistered node, a "remote-endpoint" reference and a
> reference array. The last case is end-to-end - it registers real consumer
> and supplier platform devices together with their drivers, adds the
> consumer first and checks that fw_devlink defers its probe until the
> supplier has been bound.

Always in favour for the new test cases!
Acked-by: Andy Shevchenko <andriy.s...@linux.intel.com>

Andy Shevchenko

unread,
Jul 8, 2026, 9:44:25 AMJul 8
to Bartosz Golaszewski, Brendan Higgins, David Gow, Rae Moar, Daniel Scally, Heikki Krogerus, Sakari Ailus, Bartosz Golaszewski, Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, Linus Walleij, Dmitry Torokhov, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org
On Mon, Jul 06, 2026 at 02:54:12PM +0200, Bartosz Golaszewski wrote:
> The software node fw_devlink support already has its own kunit suite, but
> that verifies the fwnode links in isolation. Add GPIO tests that prove
> the ordering works in a real-life use-case: a GPIO consumer that
> references its provider via a software node.
>
> The first suite registers the provider's software node, adds the consumer
> device first and checks that fw_devlink defers its probe until the
> provider has been added and bound. The second covers the fallback:
> with the provider's software node not yet registered no supplier link is
> created, so the consumer probes, devm_gpiod_get() returns -EPROBE_DEFER
> and the consumer only binds once the provider shows up.
>
> While at it: the existing gpio_unbind_with_consumers() test keeps the
> consumer bound while the provider goes away and then operates the orphaned
> descriptor. With software nodes now being covered by fw_devlink that would
> instead force-unbind the consumer along with the provider, so opt it out
> by setting FWNODE_FLAG_LINKS_ADDED.

Bartosz Golaszewski

unread,
Jul 10, 2026, 9:51:53 AMJul 10
to Brendan Higgins, David Gow, Rae Moar, Andy Shevchenko, Daniel Scally, Heikki Krogerus, Sakari Ailus, Bartosz Golaszewski, Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, Linus Walleij, Dmitry Torokhov, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org, Bartosz Golaszewski
Signed-off-by: Bartosz Golaszewski <bartosz.g...@oss.qualcomm.com>
---
Changes in v3:
- Use device_match_fwnode() where applicable
- Fix a race between the actions on the device_link_mq and the GPIO
test cases' swnode management
- Link to v2: https://patch.msgid.link/20260706-swnode-fw-dev...@oss.qualcomm.com

Changes in v2:
- Use loop-local variables where applicable
- Reformat a long comment
- Keep trailing commas on array initializations which may potentially be
extended in the future
- Add missing headers
- Update MAINTAINERS in commit adding a new file
- Initialize arrays at declaration
- Fix signedness of ints where needed
- Remove unneeded DISABLE_STRUCTLEAK_PLUGIN switch for swnode tests
- Link to v1: https://patch.msgid.link/20260629-swnode-fw-dev...@oss.qualcomm.com

---
Bartosz Golaszewski (5):
kunit: provide a set of fwnode-oriented helpers
software node: add fw_devlink support
software node: add kunit tests for fw_devlink support
MAINTAINERS: add myself as reviewer of software node support
gpio: kunit: add test cases verifying swnode devlink support

MAINTAINERS | 2 +
drivers/base/swnode.c | 79 ++++++++
drivers/base/test/Kconfig | 5 +
drivers/base/test/Makefile | 2 +
drivers/base/test/swnode-devlink-test.c | 339 ++++++++++++++++++++++++++++++++
drivers/gpio/gpiolib-kunit.c | 286 ++++++++++++++++++++++++++-
include/kunit/fwnode.h | 26 +++
lib/kunit/Makefile | 1 +
lib/kunit/fwnode.c | 116 +++++++++++
9 files changed, 849 insertions(+), 7 deletions(-)

Bartosz Golaszewski

unread,
Jul 10, 2026, 9:51:53 AMJul 10
to Brendan Higgins, David Gow, Rae Moar, Andy Shevchenko, Daniel Scally, Heikki Krogerus, Sakari Ailus, Bartosz Golaszewski, Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, Linus Walleij, Dmitry Torokhov, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org, Bartosz Golaszewski
Provide three new kunit-managed helpers for test cases that need to
register/create dynamic software nodes.

Signed-off-by: Bartosz Golaszewski <bartosz.g...@oss.qualcomm.com>
---
include/kunit/fwnode.h | 26 +++++++++++
lib/kunit/Makefile | 1 +
lib/kunit/fwnode.c | 116 +++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 143 insertions(+)

diff --git a/include/kunit/fwnode.h b/include/kunit/fwnode.h
new file mode 100644
new file mode 100644
+ struct fwnode_handle *fwnode;
+ int ret;
+
+ fwnode = fwnode_create_software_node(properties, parent);
+ if (IS_ERR(fwnode))
+ return fwnode;
+
+ struct fwnode_handle *fwnode;
+ int ret;
+
+ ret = software_node_register(swnode);
+ if (ret)
+ return ERR_PTR(ret);
+
+ fwnode = software_node_fwnode(swnode);
+ if (WARN_ON(!fwnode))
+ return ERR_PTR(-ENOENT);
+
+ int ret;
+

Bartosz Golaszewski

unread,
Jul 10, 2026, 9:51:59 AMJul 10
to Brendan Higgins, David Gow, Rae Moar, Andy Shevchenko, Daniel Scally, Heikki Krogerus, Sakari Ailus, Bartosz Golaszewski, Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, Linus Walleij, Dmitry Torokhov, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org, Bartosz Golaszewski
Add a kunit test suite for fw_devlink support for software nodes.

Most cases call add_links() directly and inspect the resulting fwnode
supplier/consumer lists: a single reference, multiple references, a
reference to an unregistered node, a "remote-endpoint" reference and a
reference array. The last case is end-to-end - it registers real consumer
and supplier platform devices together with their drivers, adds the
consumer first and checks that fw_devlink defers its probe until the
supplier has been bound.

Acked-by: Andy Shevchenko <andriy.s...@linux.intel.com>
Signed-off-by: Bartosz Golaszewski <bartosz.g...@oss.qualcomm.com>
---
MAINTAINERS | 1 +
drivers/base/test/Kconfig | 5 +
drivers/base/test/Makefile | 2 +
drivers/base/test/swnode-devlink-test.c | 339 ++++++++++++++++++++++++++++++++
4 files changed, 347 insertions(+)

diff --git a/MAINTAINERS b/MAINTAINERS
index 4a8b0fd665ce2447c3e89784b142d998f7384b95..d7741614c941ceed9e46e9caea72bad77cfb7618 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -25105,6 +25105,7 @@ L: linux...@vger.kernel.org
S: Maintained
F: drivers/base/property.c
new file mode 100644
index 0000000000000000000000000000000000000000..6f59f13214fcf39cebe02244bd0029470d3d104a
--- /dev/null
+++ b/drivers/base/test/swnode-devlink-test.c
@@ -0,0 +1,339 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (C) Qualcomm Technologies, Inc. and/or its subsidiaries
+ */
+
+ static const struct software_node supp_swnode = {
+ .name = "swnode-devlink-test-supplier",
+ };
+
+ struct fwnode_handle *cons_fwnode, *supp_fwnode;
+ int ret;
+
+
+ struct fwnode_handle *fwnode;
+ int ret;
+
+ ret = kunit_software_node_register_node_group(test, supp_nodes);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ fwnode = kunit_fwnode_create_software_node(test, props, NULL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, fwnode);
+
+ ret = fwnode_call_int_op(fwnode, add_links);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+
+ KUNIT_EXPECT_EQ(test, swnode_count_suppliers(fwnode), 2);
+ KUNIT_EXPECT_TRUE(test, swnode_has_link(fwnode, software_node_fwnode(&supp1_swnode)));
+ KUNIT_EXPECT_TRUE(test, swnode_has_link(fwnode, software_node_fwnode(&supp2_swnode)));
+}
+
+/* A reference to an unregistered node creates no link (graceful skip). */
+static void swnode_devlink_test_unregistered_ref(struct kunit *test)
+{
+ static const struct software_node supp_swnode = {
+ .name = "swnode-devlink-test-supplier",
+ };
+
+ const struct property_entry props[] = {
+ PROPERTY_ENTRY_REF("supplier", &supp_swnode),
+ { }
+ };
+
+ struct fwnode_handle *fwnode;
+ int ret;
+
+ fwnode = kunit_fwnode_create_software_node(test, props, NULL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, fwnode);
+
+ ret = fwnode_call_int_op(fwnode, add_links);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, swnode_count_suppliers(fwnode), 0);
+}
+
+/* Graph "remote-endpoint" references are excluded. */
+static void swnode_devlink_test_remote_endpoint_excluded(struct kunit *test)
+{
+ static const struct software_node ep_swnode = {
+ .name = "swnode-devlink-test-end-point"
+ };
+
+ const struct property_entry props[] = {
+ PROPERTY_ENTRY_REF("remote-endpoint", &ep_swnode),
+ { }
+ };
+
+ struct fwnode_handle *cons_fwnode, *supp_fwnode;
+ int ret;
+
+ supp_fwnode = kunit_software_node_register(test, &ep_swnode);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, supp_fwnode);
+
+ cons_fwnode = kunit_fwnode_create_software_node(test, props, NULL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, cons_fwnode);
+
+ ret = fwnode_call_int_op(cons_fwnode, add_links);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, swnode_count_suppliers(cons_fwnode), 0);
+}
+
+/* A reference array creates one link per registered element. */
+static void swnode_devlink_test_ref_array(struct kunit *test)
+{
+ static const struct software_node supp1_swnode = {
+ .name = "swnode-devlink-test-supplier-1",
+ };
+ static const struct software_node supp2_swnode = {
+ .name = "swnode-devlink-test-supplier-2",
+ };
+ static const struct software_node *supp_nodes[] = {
+ &supp1_swnode, &supp2_swnode, NULL
+ };
+ static const struct software_node_ref_args refs[] = {
+ SOFTWARE_NODE_REFERENCE(&supp1_swnode),
+ SOFTWARE_NODE_REFERENCE(&supp2_swnode, 4, 2),
+ };
+
+ const struct property_entry props[] = {
+ PROPERTY_ENTRY_REF_ARRAY("suppliers", refs),
+ { }
+ };
+
+ struct fwnode_handle *fwnode;
+ int ret;
+
+ ret = kunit_software_node_register_node_group(test, supp_nodes);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ fwnode = kunit_fwnode_create_software_node(test, props, NULL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, fwnode);
+
+ ret = fwnode_call_int_op(fwnode, add_links);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+
+ KUNIT_EXPECT_EQ(test, swnode_count_suppliers(fwnode), 2);
+ KUNIT_EXPECT_TRUE(test, swnode_has_link(fwnode, software_node_fwnode(&supp1_swnode)));
+ KUNIT_EXPECT_TRUE(test, swnode_has_link(fwnode, software_node_fwnode(&supp2_swnode)));
+}
+
+/*
+ * End-to-end test: fw_devlink must defer a consumer's probe until its
+ * supplier has probed.
+ *
+ * The reference created by software_node_add_links() is only useful if the
+ * driver core promotes it to a real device_link and uses it to order probing.
+ * This test drives actual probing through the platform bus and asserts the
+ * supplier binds before the consumer.
+ */
+
+#define SWNODE_DEVLINK_TEST_SUPPLIER "swnode-link-supplier"
+#define SWNODE_DEVLINK_TEST_CONSUMER "swnode-link-consumer"
+#define SWNODE_DEVLINK_TEST_TIMEOUT_MS (2 * MSEC_PER_SEC)
+
+struct swnode_test_probe_order {
+ /* Names in the order their drivers' .probe ran. */
+ const char *probed[2];
+ unsigned int count;
+ wait_queue_head_t wq;
+};
+
+static int swnode_test_record_probe(struct platform_device *pdev)
+{
+ struct swnode_test_probe_order *order = platform_get_drvdata(pdev);
+
+ if (order && order->count < ARRAY_SIZE(order->probed)) {
+ order->probed[order->count++] = dev_name(&pdev->dev);
+ wake_up_interruptible(&order->wq);
+ }
+
+ return 0;
+}
+
+static struct platform_driver swnode_test_supplier_driver = {
+ .probe = swnode_test_record_probe,
+ .driver = {
+ .name = SWNODE_DEVLINK_TEST_SUPPLIER,
+ },
+};
+
+static struct platform_driver swnode_test_consumer_driver = {
+ .probe = swnode_test_record_probe,
+ .driver = {
+ .name = SWNODE_DEVLINK_TEST_CONSUMER,
+ },
+};
+
+static void swnode_devlink_test_probe_order(struct kunit *test)
+{
+ static const struct software_node supplier_swnode = {
+ .name = "swnode-devlink-test-supplier",
+ };
+
+ const struct property_entry consumer_props[] = {
+ PROPERTY_ENTRY_REF("supplier-ref", &supplier_swnode),
+ { }
+ };
+
+ struct platform_device *supplier, *consumer;
+ struct swnode_test_probe_order *order;
+ struct fwnode_handle *fwnode;
+ int ret;
+
+ order = kunit_kzalloc(test, sizeof(*order), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, order);
+ init_waitqueue_head(&order->wq);
+
+ fwnode = kunit_software_node_register(test, &supplier_swnode);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, fwnode);
+
+ ret = kunit_platform_driver_register(test, &swnode_test_supplier_driver);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ret = kunit_platform_driver_register(test, &swnode_test_consumer_driver);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ supplier = kunit_platform_device_alloc(test, SWNODE_DEVLINK_TEST_SUPPLIER,
+ PLATFORM_DEVID_NONE);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, supplier);
+ consumer = kunit_platform_device_alloc(test, SWNODE_DEVLINK_TEST_CONSUMER,
+ PLATFORM_DEVID_NONE);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, consumer);
+
+ platform_set_drvdata(supplier, order);
+ platform_set_drvdata(consumer, order);
+
+ ret = device_add_software_node(&supplier->dev, &supplier_swnode);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ret = device_create_managed_software_node(&consumer->dev,
+ consumer_props, NULL);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ ret = kunit_platform_device_add(test, consumer);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ret = kunit_platform_device_add(test, supplier);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ ret = wait_event_interruptible_timeout(order->wq,
+ order->count == 2,
+ msecs_to_jiffies(SWNODE_DEVLINK_TEST_TIMEOUT_MS));
+ KUNIT_ASSERT_GT(test, ret, 0);
+
+ KUNIT_EXPECT_STREQ(test, order->probed[0], SWNODE_DEVLINK_TEST_SUPPLIER);
+ KUNIT_EXPECT_STREQ(test, order->probed[1], SWNODE_DEVLINK_TEST_CONSUMER);
+
+ /* Tear down the consumer (and its device link) before the supplier. */
+ kunit_platform_device_unregister(test, consumer);
+
+ device_remove_software_node(&supplier->dev);
+}
+
+static struct kunit_case swnode_test_cases[] = {
+ KUNIT_CASE(swnode_devlink_test_single_ref),
+ KUNIT_CASE(swnode_devlink_test_multiple_refs),
+ KUNIT_CASE(swnode_devlink_test_unregistered_ref),
+ KUNIT_CASE(swnode_devlink_test_remote_endpoint_excluded),
+ KUNIT_CASE(swnode_devlink_test_ref_array),
+ KUNIT_CASE(swnode_devlink_test_probe_order),
+ { }
+};
+

Bartosz Golaszewski

unread,
Jul 10, 2026, 9:52:00 AMJul 10
to Brendan Higgins, David Gow, Rae Moar, Andy Shevchenko, Daniel Scally, Heikki Krogerus, Sakari Ailus, Bartosz Golaszewski, Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, Linus Walleij, Dmitry Torokhov, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org, Bartosz Golaszewski
Software nodes can be used to describe supplier-consumer relationships
between devices they represent using reference property entries. Unlike
for OF-nodes, driver core cannot yet use these references to create a
probe order that avoids needless probe deferrals on missing providers.

Implement software_node_add_links() modelled on of_fwnode_add_links().
For every DEV_PROP_REF property we resolve each referenced supplier and
create an fwnode link from the node to it. The driver core later promotes
these to device links and defers the consumer until the suppliers are
ready.

There's no allowlist like the one DT needs - devicetree phandles appear
in plenty of non-supplier contexts, but a software node only carries a
reference property when its author explicitly points at another node, so
we treat every reference as an intentional supplier dependency and link
all of them. Graph "remote-endpoint" references are skipped for now: they
go 2-ways between endpoint nodes and would create graph cycles without
the port-parent lifting DT does via get_con_dev(). References to
suppliers that aren't registered yet and self-references are ignored.

fw_devlink resolves the supplier device through fwnode->dev but the core
only records the owning device on the primary fwnode. When the software
node is a device's secondary fwnode, mirror the device pointer onto it in
software_node_notify() so the consumer can actually find the supplier
instead of deferring forever.

While at it: purge the fwnode links in software_node_release() now that
software nodes can own them.

Signed-off-by: Bartosz Golaszewski <bartosz.g...@oss.qualcomm.com>
---
drivers/base/swnode.c | 79 +++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 79 insertions(+)

diff --git a/drivers/base/swnode.c b/drivers/base/swnode.c
index 869228a65cb365567ddac7db6ad7b8743e0dbca9..1f2315858cc3e04c7ff197324f1862c32a2ffa31 100644
--- a/drivers/base/swnode.c
+++ b/drivers/base/swnode.c
@@ -699,6 +699,62 @@ software_node_graph_parse_endpoint(const struct fwnode_handle *fwnode,
return 0;
}

+static int software_node_add_links(struct fwnode_handle *fwnode)
+{
+ const struct software_node_ref_args *ref, *ref_array;
+ struct swnode *swnode = to_swnode(fwnode);
+ const struct property_entry *prop;
+ struct fwnode_handle *refnode;
+ unsigned int count;
+
+ if (!swnode || !swnode->node->properties)
+ return 0;
+
+ /*
+ * Unlike Device Tree, where phandles appear in many non-supplier
+ * contexts and a curated allowlist is required, a software node only
+ * carries a DEV_PROP_REF property when the author explicitly describes
+ * a reference to another node. Every such reference is therefore an
+ * intentional supplier dependency, so we create fwnode links for all
+ * of them.
+ */
+ for (prop = swnode->node->properties; prop->name; prop++) {
+ if (prop->type != DEV_PROP_REF || prop->is_inline)
+ continue;
+
+ /*
+ }
+
+ return 0;
+}
+
static const struct fwnode_operations software_node_ops = {
.get = software_node_get,
.put = software_node_put,
@@ -716,6 +772,7 @@ static const struct fwnode_operations software_node_ops = {
.graph_get_remote_endpoint = software_node_graph_get_remote_endpoint,
.graph_get_port_parent = software_node_graph_get_port_parent,
.graph_parse_endpoint = software_node_graph_parse_endpoint,
+ .add_links = software_node_add_links,
};

/* -------------------------------------------------------------------------- */
@@ -787,6 +844,8 @@ static void software_node_release(struct kobject *kobj)
{
struct swnode *swnode = kobj_to_swnode(kobj);

+ fwnode_links_purge(&swnode->fwnode);
+
if (swnode->parent) {
ida_free(&swnode->parent->child_ids, swnode->id);
list_del(&swnode->entry);
@@ -1105,6 +1164,17 @@ void software_node_notify(struct device *dev)
if (!swnode)
return;

+ /*
+ * When the software node is the device's secondary firmware node,
+ * the core only records the owning device on the primary fwnode
+ * (see device_add()). fw_devlink resolves a supplier device through
+ * fwnode->dev, so without this a consumer referencing the software
+ * node could never find the supplier device and would defer forever.
+ * Make fwnode.dev point to its owner in that case.
+ */
+ if (!device_match_fwnode(dev, &swnode->fwnode) && !swnode->fwnode.dev)
+ swnode->fwnode.dev = dev;
+
swnode_get(swnode);
ret = sysfs_create_link(&dev->kobj, &swnode->kobj, "software_node");
if (ret)
@@ -1127,6 +1197,15 @@ void software_node_notify_remove(struct device *dev)

sysfs_remove_link(&swnode->kobj, dev_name(dev));
sysfs_remove_link(&dev->kobj, "software_node");
+
+ /*
+ * Drop the device pointer mirrored onto a secondary software node in
+ * software_node_notify(). For a primary software node the core owns
+ * fwnode->dev and clears it in device_del().
+ */
+ if (!device_match_fwnode(dev, &swnode->fwnode) && swnode->fwnode.dev == dev)
+ swnode->fwnode.dev = NULL;

Bartosz Golaszewski

unread,
Jul 10, 2026, 9:52:00 AMJul 10
to Brendan Higgins, David Gow, Rae Moar, Andy Shevchenko, Daniel Scally, Heikki Krogerus, Sakari Ailus, Bartosz Golaszewski, Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, Linus Walleij, Dmitry Torokhov, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org, Bartosz Golaszewski
I've been working extensively on software nodes lately and introduced
some changes. Add myself as reviewer so that I can help review any new
proposed changes.

Acked-by: Andy Shevchenko <andriy.s...@linux.intel.com>
Signed-off-by: Bartosz Golaszewski <bartosz.g...@oss.qualcomm.com>
---
MAINTAINERS | 1 +
1 file changed, 1 insertion(+)

diff --git a/MAINTAINERS b/MAINTAINERS
index d7741614c941ceed9e46e9caea72bad77cfb7618..da886aa69f362f4cb9dc529a26f8dfc67649d7c6 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -25101,6 +25101,7 @@ R: Andy Shevchenko <andriy.s...@linux.intel.com>
R: Daniel Scally <djrs...@gmail.com>
R: Heikki Krogerus <heikki....@linux.intel.com>
R: Sakari Ailus <sakari...@linux.intel.com>
+R: Bartosz Golaszewski <br...@kernel.org>
L: linux...@vger.kernel.org
S: Maintained
F: drivers/base/property.c

--
2.47.3

Bartosz Golaszewski

unread,
Jul 10, 2026, 9:52:03 AMJul 10
to Brendan Higgins, David Gow, Rae Moar, Andy Shevchenko, Daniel Scally, Heikki Krogerus, Sakari Ailus, Bartosz Golaszewski, Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, Linus Walleij, Dmitry Torokhov, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org, Bartosz Golaszewski
Signed-off-by: Bartosz Golaszewski <bartosz.g...@oss.qualcomm.com>
---
drivers/gpio/gpiolib-kunit.c | 286 +++++++++++++++++++++++++++++++++++++++++--
1 file changed, 279 insertions(+), 7 deletions(-)

diff --git a/drivers/gpio/gpiolib-kunit.c b/drivers/gpio/gpiolib-kunit.c
index 380b68f879e55433668353bb88067d561142a5bc..ad961cf97aee7b1b6e3fb64c9f5d52be25486253 100644
--- a/drivers/gpio/gpiolib-kunit.c
+++ b/drivers/gpio/gpiolib-kunit.c
@@ -3,6 +3,8 @@
* Copyright (C) Qualcomm Technologies, Inc. and/or its subsidiaries
*/

+#include <linux/cleanup.h>
+#include <linux/err.h>
#include <linux/fwnode.h>
#include <linux/gpio/consumer.h>
#include <linux/gpio/driver.h>
@@ -11,12 +13,16 @@
#include <linux/notifier.h>
#include <linux/platform_device.h>
#include <linux/property.h>
+#include <linux/types.h>

+#include <kunit/fwnode.h>
#include <kunit/platform_device.h>
#include <kunit/test.h>

#define GPIO_TEST_PROVIDER "gpio-test-provider"
#define GPIO_SWNODE_TEST_CONSUMER "gpio-swnode-test-consumer"
+#define GPIO_PROBE_ORDER_TEST_CONSUMER "gpio-probe-order-test-consumer"
+#define GPIO_PROBE_DEFER_TEST_CONSUMER "gpio-probe-defer-test-consumer"
#define GPIO_UNBIND_TEST_CONSUMER "gpio-unbind-test-consumer"

static int gpio_test_provider_get_direction(struct gpio_chip *gc, unsigned int offset)
@@ -213,6 +219,262 @@ static struct kunit_suite gpio_swnode_lookup_test_suite = {
.test_cases = gpio_swnode_lookup_tests,
};

+static void gpio_swnode_unregister_swnode(void *data)
+{
+ software_node_unregister(data);
+}
+
+struct gpio_probe_order_pdata {
+ unsigned int probe_count;
+ bool gpio_ok;
+};
+
+static const struct gpio_probe_order_pdata gpio_probe_order_pdata_template = {
+ .probe_count = 0,
+ .gpio_ok = false,
+};
+
+static int gpio_probe_order_consumer_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct gpio_probe_order_pdata *pdata = dev_get_platdata(dev);
+ struct gpio_desc *desc;
+
+ pdata->probe_count++;
+
+ desc = devm_gpiod_get(dev, "foo", GPIOD_OUT_HIGH);
+ if (IS_ERR(desc))
+ return PTR_ERR(desc);
+
+ pdata->gpio_ok = true;
+
+ return 0;
+}
+
+static struct platform_driver gpio_probe_order_consumer_driver = {
+ .probe = gpio_probe_order_consumer_probe,
+ .driver = {
+ .name = GPIO_PROBE_ORDER_TEST_CONSUMER,
+ },
+};
+
+/*
+ * Verify that fw_devlink orders the probe of a GPIO consumer after its
+ * provider. The consumer references the provider through a software node and
+ * is registered first. fw_devlink must defer it before its driver's probe()
+ * is ever entered, so the consumer probes exactly once - only after the
+ * provider is added and bound.
+ */
+static void gpio_swnode_probe_order(struct kunit *test)
+{
+ struct property_entry properties[2] = { };
+ struct gpio_probe_order_pdata *pdata;
+ struct platform_device_info pdevinfo;
+ struct platform_device *prvd, *cons;
+ bool bound = false;
+ int ret;
+
+ ret = kunit_platform_driver_register(test, &gpio_test_provider_driver);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ ret = kunit_platform_driver_register(test, &gpio_probe_order_consumer_driver);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ ret = software_node_register(&gpio_test_provider_swnode);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ ret = kunit_add_action_or_reset(test, gpio_swnode_unregister_swnode,
+ (void *)&gpio_test_provider_swnode);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+static int gpio_probe_defer_consumer_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct gpio_probe_defer_pdata *pdata = dev_get_platdata(dev);
+ struct gpio_desc *desc;
+
+ pdata->probe_count++;
+
+ desc = devm_gpiod_get(dev, "foo", GPIOD_OUT_HIGH);
+ if (IS_ERR(desc)) {
+ pdata->gpio_err = PTR_ERR(desc);
+ return pdata->gpio_err;
+ }
+
+ pdata->gpio_err = 0;
+
+ return 0;
+}
+
+static struct platform_driver gpio_probe_defer_consumer_driver = {
+ .probe = gpio_probe_defer_consumer_probe,
+ .driver = {
+ .name = GPIO_PROBE_DEFER_TEST_CONSUMER,
+ },
+};
+
+/*
+ * Verify that a GPIO consumer referencing a provider whose software node is
+ * not registered yet, defers its probe instead of failing.
+ *
+ * The provider software node is deliberately left unregistered when the
+ * consumer is added. fw_devlink cannot resolve the reference, so it creates no
+ * supplier link and does not order the consumer - the consumer's probe() runs
+ * and reaches devm_gpiod_get(). The swnode GPIO lookup returns -ENOTCONN for a
+ * reference to an unregistered node, which gpiolib maps to -EPROBE_DEFER. Once
+ * the provider software node and device appear, the deferred consumer probes
+ * again and binds.
+ */
+static void gpio_swnode_probe_defer_on_unregistered(struct kunit *test)
+{
+ struct property_entry properties[2] = { };
+ struct gpio_probe_defer_pdata *pdata;
+ struct platform_device_info pdevinfo;
+ struct platform_device *prvd, *cons;
+ struct fwnode_handle *fwnode;
+ bool bound = false;
+ int ret;
+
+ ret = kunit_platform_driver_register(test, &gpio_test_provider_driver);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ ret = kunit_platform_driver_register(test, &gpio_probe_defer_consumer_driver);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ properties[0] = PROPERTY_ENTRY_GPIO("foo-gpios",
+ &gpio_test_provider_swnode,
+ 0, GPIO_ACTIVE_HIGH);
+
+ pdevinfo = (struct platform_device_info){
+ .name = GPIO_PROBE_DEFER_TEST_CONSUMER,
+ .id = PLATFORM_DEVID_NONE,
+ .data = &gpio_probe_defer_pdata_template,
+ .size_data = sizeof(gpio_probe_defer_pdata_template),
+ .properties = properties,
+ };
+
+ cons = kunit_platform_device_register_full(test, &pdevinfo);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, cons);
+
+ wait_for_device_probe();
+ scoped_guard(device, &cons->dev)
+ bound = device_is_bound(&cons->dev);
+
+ KUNIT_ASSERT_FALSE(test, bound);
+
+ pdata = dev_get_platdata(&cons->dev);
+ KUNIT_ASSERT_GT(test, pdata->probe_count, 0);
+ KUNIT_ASSERT_EQ(test, pdata->gpio_err, -EPROBE_DEFER);
+
+ fwnode = kunit_software_node_register(test, &gpio_test_provider_swnode);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, fwnode);
+
+ pdevinfo = (struct platform_device_info){
+ .name = GPIO_TEST_PROVIDER,
+ .id = PLATFORM_DEVID_NONE,
+ .swnode = &gpio_test_provider_swnode,
+ };
+
+ prvd = kunit_platform_device_register_full(test, &pdevinfo);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, prvd);
+
+ wait_for_device_probe();
+
+ scoped_guard(device, &prvd->dev)
+ bound = device_is_bound(&prvd->dev);
+ KUNIT_ASSERT_TRUE(test, bound);
+
+ scoped_guard(device, &cons->dev)
+ bound = device_is_bound(&cons->dev);
+ KUNIT_ASSERT_TRUE(test, bound);
+
+ pdata = dev_get_platdata(&cons->dev);
+ KUNIT_ASSERT_EQ(test, pdata->gpio_err, 0);
+}
+
+static int gpio_swnode_probe_order_test_init(struct kunit *test)
+{
+ /*
+ * A prior test may have left a managed device link teardown queued on
+ * the device_link_mq. Flush it so that software_node_register()
+ * doesn't spuriously see the node as registered and fail with -EEXIST.
+ */
+ device_link_wait_removal();
+
+ return 0;
+}
+
+static struct kunit_case gpio_swnode_probe_order_tests[] = {
+ KUNIT_CASE(gpio_swnode_probe_order),
+ KUNIT_CASE(gpio_swnode_probe_defer_on_unregistered),
+ { }
+};
+
+static struct kunit_suite gpio_swnode_probe_order_test_suite = {
+ .name = "gpio-swnode-probe-order",
+ .test_cases = gpio_swnode_probe_order_tests,
+ .init = gpio_swnode_probe_order_test_init,
+};
+
static BLOCKING_NOTIFIER_HEAD(gpio_unbind_notifier);

struct gpio_unbind_consumer_drvdata {
@@ -310,15 +572,24 @@ static void gpio_unbind_with_consumers(struct kunit *test)
0, GPIO_ACTIVE_HIGH);
properties[1] = (struct property_entry){ };

- pdevinfo = (struct platform_device_info){
- .name = GPIO_UNBIND_TEST_CONSUMER,
- .id = PLATFORM_DEVID_NONE,
- .properties = properties,
- };
-
- cons = kunit_platform_device_register_full(test, &pdevinfo);
+ /*
+ * This test deliberately keeps the consumer bound while the provider
+ * is unregistered. fw_devlink would force-unbind the consumer before
+ * the provider so use the FWNODE_FLAG_LINKS_ADDED flag to opt out of
+ * it as a workaround.
+ */
+ cons = kunit_platform_device_alloc(test, GPIO_UNBIND_TEST_CONSUMER,
+ PLATFORM_DEVID_NONE);
KUNIT_ASSERT_NOT_ERR_OR_NULL(test, cons);

+ ret = device_create_managed_software_node(&cons->dev, properties, NULL);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ fwnode_set_flag(dev_fwnode(&cons->dev), FWNODE_FLAG_LINKS_ADDED);
+
+ ret = kunit_platform_device_add(test, cons);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
wait_for_device_probe();
scoped_guard(device, &cons->dev)
bound = device_is_bound(&cons->dev);
@@ -350,6 +621,7 @@ static struct kunit_suite gpio_unbind_with_consumers_test_suite = {

David Gow

unread,
Jul 11, 2026, 3:54:53 AMJul 11
to Bartosz Golaszewski, Brendan Higgins, Rae Moar, Andy Shevchenko, Daniel Scally, Heikki Krogerus, Sakari Ailus, Bartosz Golaszewski, Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, Linus Walleij, Dmitry Torokhov, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org
Le 10/07/2026 à 21:51, Bartosz Golaszewski a écrit :
> Provide three new kunit-managed helpers for test cases that need to
> register/create dynamic software nodes.
>
> Signed-off-by: Bartosz Golaszewski <bartosz.g...@oss.qualcomm.com>
> ---

Thanks: these look good to me. I'm assuming they'll head in with the
rest of the series, rather than separately via the kselftest/kunit tree.

Reviewed-by: David Gow <da...@davidgow.net>

Cheers,
-- David

David Gow

unread,
Jul 11, 2026, 3:54:55 AMJul 11
to Bartosz Golaszewski, Brendan Higgins, Rae Moar, Andy Shevchenko, Daniel Scally, Heikki Krogerus, Sakari Ailus, Bartosz Golaszewski, Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, Linus Walleij, Dmitry Torokhov, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org


Le 10/07/2026 à 21:51, Bartosz Golaszewski a écrit :
> Add a kunit test suite for fw_devlink support for software nodes.
>
> Most cases call add_links() directly and inspect the resulting fwnode
> supplier/consumer lists: a single reference, multiple references, a
> reference to an unregistered node, a "remote-endpoint" reference and a
> reference array. The last case is end-to-end - it registers real consumer
> and supplier platform devices together with their drivers, adds the
> consumer first and checks that fw_devlink defers its probe until the
> supplier has been bound.
>
> Acked-by: Andy Shevchenko <andriy.s...@linux.intel.com>
> Signed-off-by: Bartosz Golaszewski <bartosz.g...@oss.qualcomm.com>
> ---

Work fine here, thanks!

Tested-by: David Gow <da...@davidgow.net>

Cheers,
-- David
Given there's a comment above in swnode_count_suppliers about these
lists needing (normally) to be behind fwnode_link_lock, is it worth
duplicating it for these loops here?

David Gow

unread,
Jul 11, 2026, 3:54:59 AMJul 11
to Bartosz Golaszewski, Brendan Higgins, Rae Moar, Andy Shevchenko, Daniel Scally, Heikki Krogerus, Sakari Ailus, Bartosz Golaszewski, Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, Linus Walleij, Dmitry Torokhov, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org
Le 10/07/2026 à 21:51, Bartosz Golaszewski a écrit :
> The software node fw_devlink support already has its own kunit suite, but
> that verifies the fwnode links in isolation. Add GPIO tests that prove
> the ordering works in a real-life use-case: a GPIO consumer that
> references its provider via a software node.
>
> The first suite registers the provider's software node, adds the consumer
> device first and checks that fw_devlink defers its probe until the
> provider has been added and bound. The second covers the fallback:
> with the provider's software node not yet registered no supplier link is
> created, so the consumer probes, devm_gpiod_get() returns -EPROBE_DEFER
> and the consumer only binds once the provider shows up.
>
> While at it: the existing gpio_unbind_with_consumers() test keeps the
> consumer bound while the provider goes away and then operates the orphaned
> descriptor. With software nodes now being covered by fw_devlink that would
> instead force-unbind the consumer along with the provider, so opt it out
> by setting FWNODE_FLAG_LINKS_ADDED.
>
> Acked-by: Andy Shevchenko <andriy.s...@linux.intel.com>
> Signed-off-by: Bartosz Golaszewski <bartosz.g...@oss.qualcomm.com>
> ---

Working well under my test harness. Thanks!

Tested-by: David Gow <da...@davidgow.net>

Cheers,
-- David

Andy Shevchenko

unread,
Jul 11, 2026, 4:36:29 AMJul 11
to Bartosz Golaszewski, Brendan Higgins, David Gow, Rae Moar, Daniel Scally, Heikki Krogerus, Sakari Ailus, Bartosz Golaszewski, Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, Linus Walleij, Dmitry Torokhov, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org

Danilo Krummrich

unread,
Jul 12, 2026, 12:59:13 PMJul 12
to Bartosz Golaszewski, Brendan Higgins, David Gow, Rae Moar, Andy Shevchenko, Daniel Scally, Heikki Krogerus, Sakari Ailus, Bartosz Golaszewski, Greg Kroah-Hartman, Rafael J. Wysocki, Linus Walleij, Dmitry Torokhov, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org
On Fri Jul 10, 2026 at 3:51 PM CEST, Bartosz Golaszewski wrote:
> Bartosz Golaszewski (5):
> kunit: provide a set of fwnode-oriented helpers
> software node: add fw_devlink support
> software node: add kunit tests for fw_devlink support
> MAINTAINERS: add myself as reviewer of software node support
> gpio: kunit: add test cases verifying swnode devlink support

Overall looks good, but I think the two issues pointed out by Sashiko are valid.
In addition, I also found a memory leak in the gpiolib kunit test:

unreferenced object 0xffff88810296b2c0 (size 32):
comm "kunit_try_catch", pid 1096, jiffies 4294694235
hex dump (first 32 bytes):
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
66 6f 6f 00 00 00 00 00 00 00 00 00 00 00 00 00 foo.............
backtrace (crc a62f59c2):
__kmalloc_noprof+0x216/0x510
desc_set_label+0x46/0xc0
gpiod_request_commit+0xd5/0x140
gpiod_request+0x49/0x170
gpiod_find_and_request+0x2be/0x520
gpiod_get_index+0x56/0x70
devm_gpiod_get+0x1b/0x90
0xffffffffc0401ff8
platform_probe+0x58/0x90
really_probe+0x1bc/0x490
__driver_probe_device+0xa2/0x140
driver_probe_device+0x1e/0x110
__device_attach_driver+0xc2/0x140
bus_for_each_drv+0x117/0x170
__device_attach+0xd5/0x1c0
device_initial_probe+0x34/0x50

I came up with the following diff to resolve those issues.

diff --git a/drivers/base/test/swnode-devlink-test.c b/drivers/base/test/swnode-devlink-test.c
index 6f59f13214fc..143b9c4047a0 100644
--- a/drivers/base/test/swnode-devlink-test.c
+++ b/drivers/base/test/swnode-devlink-test.c
@@ -17,6 +17,10 @@
#include <kunit/platform_device.h>
#include <kunit/test.h>

+KUNIT_DEFINE_ACTION_WRAPPER(device_remove_software_node_wrapper,
+ device_remove_software_node,
+ struct device *);
+
static int swnode_count_suppliers(struct fwnode_handle *fwnode)
{
struct fwnode_link *link;
@@ -294,6 +298,9 @@ static void swnode_devlink_test_probe_order(struct kunit *test)

ret = device_add_software_node(&supplier->dev, &supplier_swnode);
KUNIT_ASSERT_EQ(test, ret, 0);
+ ret = kunit_add_action_or_reset(test, device_remove_software_node_wrapper,
+ &supplier->dev);
+ KUNIT_ASSERT_EQ(test, ret, 0);
ret = device_create_managed_software_node(&consumer->dev,
consumer_props, NULL);
KUNIT_ASSERT_EQ(test, ret, 0);
@@ -313,8 +320,6 @@ static void swnode_devlink_test_probe_order(struct kunit *test)

/* Tear down the consumer (and its device link) before the supplier. */
kunit_platform_device_unregister(test, consumer);
-
- device_remove_software_node(&supplier->dev);
}

static struct kunit_case swnode_test_cases[] = {
diff --git a/drivers/gpio/gpiolib-kunit.c b/drivers/gpio/gpiolib-kunit.c
index ad961cf97aee..7798f8a8e602 100644
--- a/drivers/gpio/gpiolib-kunit.c
+++ b/drivers/gpio/gpiolib-kunit.c
@@ -449,6 +449,9 @@ static void gpio_swnode_probe_defer_on_unregistered(struct kunit *test)

pdata = dev_get_platdata(&cons->dev);
KUNIT_ASSERT_EQ(test, pdata->gpio_err, 0);
+
+ /* Tear down the consumer before the provider to free the GPIO. */
+ kunit_platform_device_unregister(test, cons);
}

static int gpio_swnode_probe_order_test_init(struct kunit *test)
@@ -614,9 +617,17 @@ static struct kunit_case gpio_unbind_with_consumers_tests[] = {
{ }
};

+static int gpio_unbind_test_init(struct kunit *test)
+{
+ device_link_wait_removal();
+
+ return 0;
+}
+
static struct kunit_suite gpio_unbind_with_consumers_test_suite = {
.name = "gpio-unbind-with-consumers",
.test_cases = gpio_unbind_with_consumers_tests,
+ .init = gpio_unbind_test_init,
};

kunit_test_suites(

Bartosz Golaszewski

unread,
Jul 13, 2026, 5:45:23 AM (14 days ago) Jul 13
to Danilo Krummrich, Brendan Higgins, David Gow, Rae Moar, Andy Shevchenko, Daniel Scally, Heikki Krogerus, Sakari Ailus, Bartosz Golaszewski, Greg Kroah-Hartman, Rafael J. Wysocki, Linus Walleij, Dmitry Torokhov, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org, Bartosz Golaszewski
Interestingly I noticed this in the GPIO test cases but not here...

I also think that we should call device_link_wait_removal() *before* and
*after* these cases to not affect other test suites - as pointed out by
sashiko too.

Let me send a v4.

Bart

Bartosz Golaszewski

unread,
Jul 13, 2026, 6:20:23 AM (14 days ago) Jul 13
to Bartosz Golaszewski, Danilo Krummrich, Brendan Higgins, David Gow, Rae Moar, Andy Shevchenko, Daniel Scally, Heikki Krogerus, Sakari Ailus, Greg Kroah-Hartman, Rafael J. Wysocki, Linus Walleij, Dmitry Torokhov, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org, Bartosz Golaszewski
Huh, that won't work because suite->exit() runs *before* kunit_cleanup(). Isn't
it wrong? We do suite->init(), then schedule actions inside the test, it would
make sense to then first call the release callbacks for resources and only
after that call suite->exit()?

Anyway, that's material for another series.

Bart

Bartosz Golaszewski

unread,
Jul 13, 2026, 7:15:05 AM (14 days ago) Jul 13
to Brendan Higgins, David Gow, Rae Moar, Andy Shevchenko, Daniel Scally, Heikki Krogerus, Sakari Ailus, Bartosz Golaszewski, Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, Linus Walleij, Dmitry Torokhov, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org, Bartosz Golaszewski
Signed-off-by: Bartosz Golaszewski <bartosz.g...@oss.qualcomm.com>
---
Changes in v4:
- Extend patch 1 with an additional kunit helper:
kunit_device_add_software_node() and use it in patch 3/5 to fix the
release ordering
- Fix a memory leak in GPIO test cases pointed out by Danilo
- Fix more possible device_link_mq races: extend the coverage of
device_link_wait_removal()
- Link to v3: https://patch.msgid.link/20260710-swnode-fw-dev...@oss.qualcomm.com

Changes in v3:
- Use device_match_fwnode() where applicable
- Fix a race between the actions on the device_link_mq and the GPIO
test cases' swnode management
- Link to v2: https://patch.msgid.link/20260706-swnode-fw-dev...@oss.qualcomm.com

Changes in v2:
- Use loop-local variables where applicable
- Reformat a long comment
- Keep trailing commas on array initializations which may potentially be
extended in the future
- Add missing headers
- Update MAINTAINERS in commit adding a new file
- Initialize arrays at declaration
- Fix signedness of ints where needed
- Remove unneeded DISABLE_STRUCTLEAK_PLUGIN switch for swnode tests
- Link to v1: https://patch.msgid.link/20260629-swnode-fw-dev...@oss.qualcomm.com

---
Bartosz Golaszewski (5):
kunit: provide a set of fwnode-oriented helpers
software node: add fw_devlink support
software node: add kunit tests for fw_devlink support
MAINTAINERS: add myself as reviewer of software node support
gpio: kunit: add test cases verifying swnode devlink support

MAINTAINERS | 2 +
drivers/base/swnode.c | 79 ++++++++
drivers/base/test/Kconfig | 5 +
drivers/base/test/Makefile | 2 +
drivers/base/test/swnode-devlink-test.c | 337 ++++++++++++++++++++++++++++++++
drivers/gpio/gpiolib-kunit.c | 291 ++++++++++++++++++++++++++-
include/kunit/fwnode.h | 29 +++
lib/kunit/Makefile | 1 +
lib/kunit/fwnode.c | 146 ++++++++++++++
9 files changed, 885 insertions(+), 7 deletions(-)
---
base-commit: a13c140cc289c0b7b3770bce5b3ad42ab35074aa

Bartosz Golaszewski

unread,
Jul 13, 2026, 7:15:09 AM (14 days ago) Jul 13
to Brendan Higgins, David Gow, Rae Moar, Andy Shevchenko, Daniel Scally, Heikki Krogerus, Sakari Ailus, Bartosz Golaszewski, Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, Linus Walleij, Dmitry Torokhov, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org, Bartosz Golaszewski
Provide three new kunit-managed helpers for test cases that need to
register/create dynamic software nodes.

Reviewed-by: David Gow <da...@davidgow.net>
Signed-off-by: Bartosz Golaszewski <bartosz.g...@oss.qualcomm.com>
---
include/kunit/fwnode.h | 29 ++++++++++
lib/kunit/Makefile | 1 +
lib/kunit/fwnode.c | 146 +++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 176 insertions(+)

diff --git a/include/kunit/fwnode.h b/include/kunit/fwnode.h
new file mode 100644
index 0000000000000000000000000000000000000000..e250bb87e144c42b1a7e439947a224f50a253179
--- /dev/null
+++ b/include/kunit/fwnode.h
@@ -0,0 +1,29 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * KUnit resource management helpers for firmware nodes.
+ *
+ * Copyright (C) Qualcomm Technologies, Inc. and/or its subsidiaries
+ */
+
+#ifndef _KUNIT_FWNODE_H
+#define _KUNIT_FWNODE_H
+
+struct device;
+struct fwnode_handle;
+struct kunit;
+struct property_entry;
+struct software_node;
+
+struct fwnode_handle *
+kunit_fwnode_create_software_node(struct kunit *test,
+ const struct property_entry *properties,
+ const struct fwnode_handle *parent);
+struct fwnode_handle *
+kunit_software_node_register(struct kunit *test,
+ const struct software_node *node);
+int kunit_software_node_register_node_group(struct kunit *test,
+ const struct software_node *const *nodes);
+int kunit_device_add_software_node(struct kunit *test, struct device *dev,
+ const struct software_node *node);
+
+#endif /* _KUNIT_FWNODE_H */
diff --git a/lib/kunit/Makefile b/lib/kunit/Makefile
index 2e8a6b71a2ab07a738964a7ef0f442fd53e085b1..204e02b10eba1030c6d511991fe2f6271de64603 100644
--- a/lib/kunit/Makefile
+++ b/lib/kunit/Makefile
@@ -11,6 +11,7 @@ kunit-objs += test.o \
attributes.o \
device.o \
platform.o \
+ fwnode.o \
bug.o

ifeq ($(CONFIG_KUNIT_DEBUGFS),y)
diff --git a/lib/kunit/fwnode.c b/lib/kunit/fwnode.c
new file mode 100644
index 0000000000000000000000000000000000000000..a58ce0984d76bae1072d77abc26f4cd516e487ea
--- /dev/null
+++ b/lib/kunit/fwnode.c
@@ -0,0 +1,146 @@
+ struct fwnode_handle *fwnode;
+ int ret;
+
+ fwnode = fwnode_create_software_node(properties, parent);
+ if (IS_ERR(fwnode))
+ return fwnode;
+
+ struct fwnode_handle *fwnode;
+ int ret;
+
+ ret = software_node_register(swnode);
+ if (ret)
+ return ERR_PTR(ret);
+
+ fwnode = software_node_fwnode(swnode);
+ if (WARN_ON(!fwnode))
+ return ERR_PTR(-ENOENT);
+
+ int ret;
+
+ ret = software_node_register_node_group(nodes);
+ if (ret)
+ return ret;
+
+ return kunit_add_action_or_reset(test, software_node_unregister_node_group_wrapper,
+ (void *)nodes);
+}
+EXPORT_SYMBOL_GPL(kunit_software_node_register_node_group);
+
+KUNIT_DEFINE_ACTION_WRAPPER(device_remove_software_node_wrapper,
+ device_remove_software_node,
+ struct device *);
+
+/**
+ * kunit_device_add_software_node() - Assign a kunit-managed software node to a device
+ * @test: Test context
+ * @dev: Device to assign the software node for
+ * @node: The software node to assign
+ *
+ * Make @node the secondary firmware node of @dev. If @dev has no primary
+ * firmware node, @node will become the primary node. The software node will
+ * be automatically removed from @dev when the test case completes.
+ *
+ * Returns:
+ * 0 on success, negative error number on failure.
+ */
+int kunit_device_add_software_node(struct kunit *test, struct device *dev,
+ const struct software_node *node)
+{
+ int ret;
+
+ ret = device_add_software_node(dev, node);
+ if (ret)
+ return ret;
+
+ return kunit_add_action_or_reset(test, device_remove_software_node_wrapper, dev);
+}
+EXPORT_SYMBOL_GPL(kunit_device_add_software_node);

--
2.47.3

Bartosz Golaszewski

unread,
Jul 13, 2026, 7:15:10 AM (14 days ago) Jul 13
to Brendan Higgins, David Gow, Rae Moar, Andy Shevchenko, Daniel Scally, Heikki Krogerus, Sakari Ailus, Bartosz Golaszewski, Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, Linus Walleij, Dmitry Torokhov, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org, Bartosz Golaszewski
Signed-off-by: Bartosz Golaszewski <bartosz.g...@oss.qualcomm.com>
---
drivers/base/swnode.c | 79 +++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 79 insertions(+)

diff --git a/drivers/base/swnode.c b/drivers/base/swnode.c
index 869228a65cb365567ddac7db6ad7b8743e0dbca9..1f2315858cc3e04c7ff197324f1862c32a2ffa31 100644
--- a/drivers/base/swnode.c
+++ b/drivers/base/swnode.c
@@ -699,6 +699,62 @@ software_node_graph_parse_endpoint(const struct fwnode_handle *fwnode,
return 0;
}

+static int software_node_add_links(struct fwnode_handle *fwnode)
+{
+ const struct software_node_ref_args *ref, *ref_array;
+ struct swnode *swnode = to_swnode(fwnode);
+ const struct property_entry *prop;
+ struct fwnode_handle *refnode;
+ unsigned int count;
+
+ if (!swnode || !swnode->node->properties)
+ return 0;
+
+ /*
+ * Unlike Device Tree, where phandles appear in many non-supplier
+ * contexts and a curated allowlist is required, a software node only
+ * carries a DEV_PROP_REF property when the author explicitly describes
+ * a reference to another node. Every such reference is therefore an
+ * intentional supplier dependency, so we create fwnode links for all
+ * of them.
+ */
+ for (prop = swnode->node->properties; prop->name; prop++) {
+ if (prop->type != DEV_PROP_REF || prop->is_inline)
+ continue;
+
+ /*
+
+ return 0;
+}
+
+
+ /*

Bartosz Golaszewski

unread,
Jul 13, 2026, 7:15:16 AM (14 days ago) Jul 13
to Brendan Higgins, David Gow, Rae Moar, Andy Shevchenko, Daniel Scally, Heikki Krogerus, Sakari Ailus, Bartosz Golaszewski, Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, Linus Walleij, Dmitry Torokhov, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org, Bartosz Golaszewski
Add a kunit test suite for fw_devlink support for software nodes.

Most cases call add_links() directly and inspect the resulting fwnode
supplier/consumer lists: a single reference, multiple references, a
reference to an unregistered node, a "remote-endpoint" reference and a
reference array. The last case is end-to-end - it registers real consumer
and supplier platform devices together with their drivers, adds the
consumer first and checks that fw_devlink defers its probe until the
supplier has been bound.

Acked-by: Andy Shevchenko <andriy.s...@linux.intel.com>
Tested-by: David Gow <da...@davidgow.net>
Signed-off-by: Bartosz Golaszewski <bartosz.g...@oss.qualcomm.com>
---
MAINTAINERS | 1 +
drivers/base/test/Kconfig | 5 +
drivers/base/test/Makefile | 2 +
drivers/base/test/swnode-devlink-test.c | 337 ++++++++++++++++++++++++++++++++
4 files changed, 345 insertions(+)

diff --git a/MAINTAINERS b/MAINTAINERS
index 806bd2d80d153537393761df322165f683954d50..f9dfb14b9d477fc0331441f96c158b0474f63026 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -25106,6 +25106,7 @@ L: linux...@vger.kernel.org
new file mode 100644
index 0000000000000000000000000000000000000000..f53549e926118fd4a1e109ece67d58d45d52193d
--- /dev/null
+++ b/drivers/base/test/swnode-devlink-test.c
@@ -0,0 +1,337 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (C) Qualcomm Technologies, Inc. and/or its subsidiaries
+ */
+
+#include <linux/array_size.h>
+#include <linux/device.h>
+#include <linux/fwnode.h>
+#include <linux/list.h>
+#include <linux/platform_device.h>
+#include <linux/property.h>
+#include <linux/time.h>
+#include <linux/types.h>
+#include <linux/wait.h>
+
+#include <kunit/fwnode.h>
+#include <kunit/platform_device.h>
+#include <kunit/test.h>
+
+static int swnode_count_suppliers(struct fwnode_handle *fwnode)
+{
+ struct fwnode_link *link;
+ unsigned int count = 0;
+
+ /*
+ * The suppliers and consumers lists should typically only be accessed
+ * with the fwnode_link_lock taken but it's private to the driver core.
+ *
+ * These are tests and at this point nobody should be modifying them so
+ * let's just access the list.
+ */
+ list_for_each_entry(link, &fwnode->suppliers, c_hook)
+ count++;
+
+ return count;
+}
+
+/* True if a supplier link con->sup exists, checked from both list ends. */
+static bool swnode_has_link(struct fwnode_handle *consumer,
+ struct fwnode_handle *supplier)
+{
+ bool from_con = false, from_sup = false;
+ struct fwnode_link *link;
+
+ list_for_each_entry(link, &consumer->suppliers, c_hook) {
+ if (link->supplier == supplier && link->consumer == consumer)
+ from_con = true;
+ }
+
+ list_for_each_entry(link, &supplier->consumers, s_hook) {
+ if (link->supplier == supplier && link->consumer == consumer)
+ from_sup = true;
+ }
+
+ return from_con && from_sup;
+}
+
+/* A single reference creates exactly one supplier link, on both list ends. */
+static void swnode_devlink_test_single_ref(struct kunit *test)
+{
+ static const struct software_node supp_swnode = {
+ .name = "swnode-devlink-test-supplier",
+ };
+
+ struct fwnode_handle *cons_fwnode, *supp_fwnode;
+ int ret;
+
+ const struct property_entry props[] = {
+ PROPERTY_ENTRY_REF("supplier", &supp_swnode),
+ { }
+ };
+
+ supp_fwnode = kunit_software_node_register(test, &supp_swnode);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, supp_fwnode);
+
+ cons_fwnode = kunit_fwnode_create_software_node(test, props, NULL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, cons_fwnode);
+
+ ret = fwnode_call_int_op(cons_fwnode, add_links);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+
+ KUNIT_EXPECT_EQ(test, swnode_count_suppliers(cons_fwnode), 1);
+ KUNIT_EXPECT_TRUE(test, swnode_has_link(cons_fwnode, supp_fwnode));
+}
+
+/* Multiple distinct references create multiple supplier links. */
+static void swnode_devlink_test_multiple_refs(struct kunit *test)
+{
+ static const struct software_node supp1_swnode = {
+ .name = "swnode-devlink-test-supplier-1",
+ };
+ static const struct software_node supp2_swnode = {
+ .name = "swnode-devlink-test-supplier-2",
+ };
+ static const struct software_node *supp_nodes[] = {
+ &supp1_swnode, &supp2_swnode, NULL
+ };
+
+ const struct property_entry props[] = {
+ PROPERTY_ENTRY_REF("foo", &supp1_swnode),
+ PROPERTY_ENTRY_REF("bar", &supp2_swnode),
+ { }
+ };
+
+ struct fwnode_handle *fwnode;
+ int ret;
+
+ ret = kunit_software_node_register_node_group(test, supp_nodes);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ fwnode = kunit_fwnode_create_software_node(test, props, NULL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, fwnode);
+
+ ret = fwnode_call_int_op(fwnode, add_links);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+
+ KUNIT_EXPECT_EQ(test, swnode_count_suppliers(fwnode), 2);
+ KUNIT_EXPECT_TRUE(test, swnode_has_link(fwnode, software_node_fwnode(&supp1_swnode)));
+ KUNIT_EXPECT_TRUE(test, swnode_has_link(fwnode, software_node_fwnode(&supp2_swnode)));
+}
+
+/* A reference to an unregistered node creates no link (graceful skip). */
+static void swnode_devlink_test_unregistered_ref(struct kunit *test)
+{
+ static const struct software_node supp_swnode = {
+ .name = "swnode-devlink-test-supplier",
+ };
+
+ const struct property_entry props[] = {
+ PROPERTY_ENTRY_REF("supplier", &supp_swnode),
+ { }
+ };
+
+ struct fwnode_handle *fwnode;
+ int ret;
+
+ fwnode = kunit_fwnode_create_software_node(test, props, NULL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, fwnode);
+
+ ret = fwnode_call_int_op(fwnode, add_links);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, swnode_count_suppliers(fwnode), 0);
+}
+
+/* Graph "remote-endpoint" references are excluded. */
+static void swnode_devlink_test_remote_endpoint_excluded(struct kunit *test)
+{
+ static const struct software_node ep_swnode = {
+ .name = "swnode-devlink-test-end-point"
+ };
+
+ const struct property_entry props[] = {
+ PROPERTY_ENTRY_REF("remote-endpoint", &ep_swnode),
+ { }
+ };
+
+ struct fwnode_handle *cons_fwnode, *supp_fwnode;
+ int ret;
+
+ supp_fwnode = kunit_software_node_register(test, &ep_swnode);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, supp_fwnode);
+
+ cons_fwnode = kunit_fwnode_create_software_node(test, props, NULL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, cons_fwnode);
+
+ ret = fwnode_call_int_op(cons_fwnode, add_links);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, swnode_count_suppliers(cons_fwnode), 0);
+}
+
+/* A reference array creates one link per registered element. */
+static void swnode_devlink_test_ref_array(struct kunit *test)
+{
+ static const struct software_node supp1_swnode = {
+ .name = "swnode-devlink-test-supplier-1",
+ };
+ static const struct software_node supp2_swnode = {
+ .name = "swnode-devlink-test-supplier-2",
+ };
+ static const struct software_node *supp_nodes[] = {
+ &supp1_swnode, &supp2_swnode, NULL
+ };
+ static const struct software_node_ref_args refs[] = {
+ SOFTWARE_NODE_REFERENCE(&supp1_swnode),
+ SOFTWARE_NODE_REFERENCE(&supp2_swnode, 4, 2),
+ };
+
+ const struct property_entry props[] = {
+ PROPERTY_ENTRY_REF_ARRAY("suppliers", refs),
+ { }
+ };
+
+ struct fwnode_handle *fwnode;
+ int ret;
+
+ ret = kunit_software_node_register_node_group(test, supp_nodes);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ fwnode = kunit_fwnode_create_software_node(test, props, NULL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, fwnode);
+
+ ret = fwnode_call_int_op(fwnode, add_links);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+
+ KUNIT_EXPECT_EQ(test, swnode_count_suppliers(fwnode), 2);
+ KUNIT_EXPECT_TRUE(test, swnode_has_link(fwnode, software_node_fwnode(&supp1_swnode)));
+ KUNIT_EXPECT_TRUE(test, swnode_has_link(fwnode, software_node_fwnode(&supp2_swnode)));
+}
+
+/*
+ * End-to-end test: fw_devlink must defer a consumer's probe until its
+ * supplier has probed.
+ *
+ * The reference created by software_node_add_links() is only useful if the
+ * driver core promotes it to a real device_link and uses it to order probing.
+ * This test drives actual probing through the platform bus and asserts the
+ * supplier binds before the consumer.
+ */
+
+#define SWNODE_DEVLINK_TEST_SUPPLIER "swnode-link-supplier"
+#define SWNODE_DEVLINK_TEST_CONSUMER "swnode-link-consumer"
+#define SWNODE_DEVLINK_TEST_TIMEOUT_MS (2 * MSEC_PER_SEC)
+
+struct swnode_test_probe_order {
+ /* Names in the order their drivers' .probe ran. */
+ const char *probed[2];
+ unsigned int count;
+ wait_queue_head_t wq;
+};
+
+static int swnode_test_record_probe(struct platform_device *pdev)
+{
+ struct swnode_test_probe_order *order = platform_get_drvdata(pdev);
+
+ if (order && order->count < ARRAY_SIZE(order->probed)) {
+ order->probed[order->count++] = dev_name(&pdev->dev);
+ wake_up_interruptible(&order->wq);
+ }
+
+ return 0;
+}
+
+static struct platform_driver swnode_test_supplier_driver = {
+ .probe = swnode_test_record_probe,
+ .driver = {
+ .name = SWNODE_DEVLINK_TEST_SUPPLIER,
+ },
+};
+
+static struct platform_driver swnode_test_consumer_driver = {
+ .probe = swnode_test_record_probe,
+ .driver = {
+ .name = SWNODE_DEVLINK_TEST_CONSUMER,
+ },
+};
+
+static void swnode_devlink_test_probe_order(struct kunit *test)
+{
+ static const struct software_node supplier_swnode = {
+ .name = "swnode-devlink-test-supplier",
+ };
+
+ const struct property_entry consumer_props[] = {
+ PROPERTY_ENTRY_REF("supplier-ref", &supplier_swnode),
+ { }
+ };
+
+ struct platform_device *supplier, *consumer;
+ struct swnode_test_probe_order *order;
+ struct fwnode_handle *fwnode;
+ int ret;
+
+ order = kunit_kzalloc(test, sizeof(*order), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, order);
+ init_waitqueue_head(&order->wq);
+
+ fwnode = kunit_software_node_register(test, &supplier_swnode);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, fwnode);
+
+ ret = kunit_platform_driver_register(test, &swnode_test_supplier_driver);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ret = kunit_platform_driver_register(test, &swnode_test_consumer_driver);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ supplier = kunit_platform_device_alloc(test, SWNODE_DEVLINK_TEST_SUPPLIER,
+ PLATFORM_DEVID_NONE);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, supplier);
+ consumer = kunit_platform_device_alloc(test, SWNODE_DEVLINK_TEST_CONSUMER,
+ PLATFORM_DEVID_NONE);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, consumer);
+
+ platform_set_drvdata(supplier, order);
+ platform_set_drvdata(consumer, order);
+
+ ret = kunit_device_add_software_node(test, &supplier->dev, &supplier_swnode);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ret = device_create_managed_software_node(&consumer->dev,
+ consumer_props, NULL);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ ret = kunit_platform_device_add(test, consumer);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ret = kunit_platform_device_add(test, supplier);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ ret = wait_event_interruptible_timeout(order->wq,
+ order->count == 2,
+ msecs_to_jiffies(SWNODE_DEVLINK_TEST_TIMEOUT_MS));
+ KUNIT_ASSERT_GT(test, ret, 0);
+
+ KUNIT_EXPECT_STREQ(test, order->probed[0], SWNODE_DEVLINK_TEST_SUPPLIER);
+ KUNIT_EXPECT_STREQ(test, order->probed[1], SWNODE_DEVLINK_TEST_CONSUMER);
+
+ /* Tear down the consumer (and its device link) before the supplier. */
+ kunit_platform_device_unregister(test, consumer);
+}
+
+static struct kunit_case swnode_test_cases[] = {
+ KUNIT_CASE(swnode_devlink_test_single_ref),
+ KUNIT_CASE(swnode_devlink_test_multiple_refs),
+ KUNIT_CASE(swnode_devlink_test_unregistered_ref),
+ KUNIT_CASE(swnode_devlink_test_remote_endpoint_excluded),
+ KUNIT_CASE(swnode_devlink_test_ref_array),
+ KUNIT_CASE(swnode_devlink_test_probe_order),
+ { }
+};
+
+static struct kunit_suite swnode_test_suite = {
+ .name = "software-node-links",
+ .test_cases = swnode_test_cases,
+};
+
+kunit_test_suite(swnode_test_suite);
+
+MODULE_DESCRIPTION("Test module for software node fw_devlink support");
+MODULE_AUTHOR("Bartosz Golaszewski <bartosz.g...@oss.qualcomm.com>");
+MODULE_LICENSE("GPL");

--
2.47.3

Bartosz Golaszewski

unread,
Jul 13, 2026, 7:15:18 AM (14 days ago) Jul 13
to Brendan Higgins, David Gow, Rae Moar, Andy Shevchenko, Daniel Scally, Heikki Krogerus, Sakari Ailus, Bartosz Golaszewski, Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, Linus Walleij, Dmitry Torokhov, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org, Bartosz Golaszewski
I've been working extensively on software nodes lately and introduced
some changes. Add myself as reviewer so that I can help review any new
proposed changes.

Acked-by: Andy Shevchenko <andriy.s...@linux.intel.com>
Signed-off-by: Bartosz Golaszewski <bartosz.g...@oss.qualcomm.com>
---
MAINTAINERS | 1 +
1 file changed, 1 insertion(+)

diff --git a/MAINTAINERS b/MAINTAINERS
index f9dfb14b9d477fc0331441f96c158b0474f63026..a7a5d81d30658700b3744e0b2994211d75074883 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -25102,6 +25102,7 @@ R: Andy Shevchenko <andriy.s...@linux.intel.com>
R: Daniel Scally <djrs...@gmail.com>
R: Heikki Krogerus <heikki....@linux.intel.com>
R: Sakari Ailus <sakari...@linux.intel.com>
+R: Bartosz Golaszewski <br...@kernel.org>
L: linux...@vger.kernel.org
S: Maintained
F: drivers/base/property.c

--
2.47.3

Bartosz Golaszewski

unread,
Jul 13, 2026, 7:15:21 AM (14 days ago) Jul 13
to Brendan Higgins, David Gow, Rae Moar, Andy Shevchenko, Daniel Scally, Heikki Krogerus, Sakari Ailus, Bartosz Golaszewski, Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, Linus Walleij, Dmitry Torokhov, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org, Bartosz Golaszewski
The software node fw_devlink support already has its own kunit suite, but
that verifies the fwnode links in isolation. Add GPIO tests that prove
the ordering works in a real-life use-case: a GPIO consumer that
references its provider via a software node.

The first suite registers the provider's software node, adds the consumer
device first and checks that fw_devlink defers its probe until the
provider has been added and bound. The second covers the fallback:
with the provider's software node not yet registered no supplier link is
created, so the consumer probes, devm_gpiod_get() returns -EPROBE_DEFER
and the consumer only binds once the provider shows up.

While at it: the existing gpio_unbind_with_consumers() test keeps the
consumer bound while the provider goes away and then operates the orphaned
descriptor. With software nodes now being covered by fw_devlink that would
instead force-unbind the consumer along with the provider, so opt it out
by setting FWNODE_FLAG_LINKS_ADDED.

Acked-by: Andy Shevchenko <andriy.s...@linux.intel.com>
Tested-by: David Gow <da...@davidgow.net>
Signed-off-by: Bartosz Golaszewski <bartosz.g...@oss.qualcomm.com>
---
drivers/gpio/gpiolib-kunit.c | 291 +++++++++++++++++++++++++++++++++++++++++--
1 file changed, 284 insertions(+), 7 deletions(-)

diff --git a/drivers/gpio/gpiolib-kunit.c b/drivers/gpio/gpiolib-kunit.c
index 380b68f879e55433668353bb88067d561142a5bc..6c25561c1e9b104b4049965b70fb5c044051570b 100644
--- a/drivers/gpio/gpiolib-kunit.c
+++ b/drivers/gpio/gpiolib-kunit.c
@@ -3,6 +3,8 @@
* Copyright (C) Qualcomm Technologies, Inc. and/or its subsidiaries
*/

+#include <linux/cleanup.h>
+#include <linux/err.h>
#include <linux/fwnode.h>
#include <linux/gpio/consumer.h>
#include <linux/gpio/driver.h>
@@ -11,12 +13,16 @@
#include <linux/notifier.h>
#include <linux/platform_device.h>
#include <linux/property.h>
+#include <linux/types.h>

+#include <kunit/fwnode.h>
#include <kunit/platform_device.h>
#include <kunit/test.h>

#define GPIO_TEST_PROVIDER "gpio-test-provider"
#define GPIO_SWNODE_TEST_CONSUMER "gpio-swnode-test-consumer"
+#define GPIO_PROBE_ORDER_TEST_CONSUMER "gpio-probe-order-test-consumer"
+#define GPIO_PROBE_DEFER_TEST_CONSUMER "gpio-probe-defer-test-consumer"
#define GPIO_UNBIND_TEST_CONSUMER "gpio-unbind-test-consumer"

static int gpio_test_provider_get_direction(struct gpio_chip *gc, unsigned int offset)
@@ -213,6 +219,265 @@ static struct kunit_suite gpio_swnode_lookup_test_suite = {
.test_cases = gpio_swnode_lookup_tests,
};

+static void gpio_swnode_unregister_swnode(void *data)
+{
+ software_node_unregister(data);
+}
+
+struct gpio_probe_order_pdata {
+ unsigned int probe_count;
+ bool gpio_ok;
+};
+
+static const struct gpio_probe_order_pdata gpio_probe_order_pdata_template = {
+ .probe_count = 0,
+ .gpio_ok = false,
+};
+
+static int gpio_probe_order_consumer_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct gpio_probe_order_pdata *pdata = dev_get_platdata(dev);
+ struct gpio_desc *desc;
+
+ pdata->probe_count++;
+
+ desc = devm_gpiod_get(dev, "foo", GPIOD_OUT_HIGH);
+ if (IS_ERR(desc))
+ return PTR_ERR(desc);
+
+ pdata->gpio_ok = true;
+
+ return 0;
+}
+
+static struct platform_driver gpio_probe_order_consumer_driver = {
+ .probe = gpio_probe_order_consumer_probe,
+ .driver = {
+ .name = GPIO_PROBE_ORDER_TEST_CONSUMER,
+ },
+};
+
+/*
+ * Verify that fw_devlink orders the probe of a GPIO consumer after its
+ * provider. The consumer references the provider through a software node and
+ * is registered first. fw_devlink must defer it before its driver's probe()
+ * is ever entered, so the consumer probes exactly once - only after the
+ * provider is added and bound.
+ */
+static void gpio_swnode_probe_order(struct kunit *test)
+{
+ struct property_entry properties[2] = { };
+ struct gpio_probe_order_pdata *pdata;
+ struct platform_device_info pdevinfo;
+ struct platform_device *prvd, *cons;
+ bool bound = false;
+ int ret;
+
+ ret = kunit_platform_driver_register(test, &gpio_test_provider_driver);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ ret = kunit_platform_driver_register(test, &gpio_probe_order_consumer_driver);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ ret = software_node_register(&gpio_test_provider_swnode);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ ret = kunit_add_action_or_reset(test, gpio_swnode_unregister_swnode,
+ (void *)&gpio_test_provider_swnode);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+static int gpio_probe_defer_consumer_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct gpio_probe_defer_pdata *pdata = dev_get_platdata(dev);
+ struct gpio_desc *desc;
+
+ pdata->probe_count++;
+
+ desc = devm_gpiod_get(dev, "foo", GPIOD_OUT_HIGH);
+ if (IS_ERR(desc)) {
+ pdata->gpio_err = PTR_ERR(desc);
+ return pdata->gpio_err;
+ }
+
+ pdata->gpio_err = 0;
+
+ return 0;
+}
+
+static struct platform_driver gpio_probe_defer_consumer_driver = {
+ .probe = gpio_probe_defer_consumer_probe,
+ .driver = {
+ .name = GPIO_PROBE_DEFER_TEST_CONSUMER,
+ },
+};
+
+/*
+ * Verify that a GPIO consumer referencing a provider whose software node is
+ * not registered yet, defers its probe instead of failing.
+ *
+ * The provider software node is deliberately left unregistered when the
+ * consumer is added. fw_devlink cannot resolve the reference, so it creates no
+ * supplier link and does not order the consumer - the consumer's probe() runs
+ * and reaches devm_gpiod_get(). The swnode GPIO lookup returns -ENOTCONN for a
+ * reference to an unregistered node, which gpiolib maps to -EPROBE_DEFER. Once
+ * the provider software node and device appear, the deferred consumer probes
+ * again and binds.
+ */
+static void gpio_swnode_probe_defer_on_unregistered(struct kunit *test)
+{
+ struct property_entry properties[2] = { };
+ struct gpio_probe_defer_pdata *pdata;
+ struct platform_device_info pdevinfo;
+ struct platform_device *prvd, *cons;
+ struct fwnode_handle *fwnode;
+ bool bound = false;
+ int ret;
+
+ ret = kunit_platform_driver_register(test, &gpio_test_provider_driver);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ ret = kunit_platform_driver_register(test, &gpio_probe_defer_consumer_driver);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ properties[0] = PROPERTY_ENTRY_GPIO("foo-gpios",
+ &gpio_test_provider_swnode,
+ 0, GPIO_ACTIVE_HIGH);
+
+ pdevinfo = (struct platform_device_info){
+ .name = GPIO_PROBE_DEFER_TEST_CONSUMER,
+ .id = PLATFORM_DEVID_NONE,
+ .data = &gpio_probe_defer_pdata_template,
+ .size_data = sizeof(gpio_probe_defer_pdata_template),
+ .properties = properties,
+ };
+
+ cons = kunit_platform_device_register_full(test, &pdevinfo);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, cons);
+
+ wait_for_device_probe();
+ scoped_guard(device, &cons->dev)
+ bound = device_is_bound(&cons->dev);
+
+ KUNIT_ASSERT_FALSE(test, bound);
+
+ pdata = dev_get_platdata(&cons->dev);
+ KUNIT_ASSERT_GT(test, pdata->probe_count, 0);
+ KUNIT_ASSERT_EQ(test, pdata->gpio_err, -EPROBE_DEFER);
+
+ fwnode = kunit_software_node_register(test, &gpio_test_provider_swnode);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, fwnode);
+
+ pdevinfo = (struct platform_device_info){
+ .name = GPIO_TEST_PROVIDER,
+ .id = PLATFORM_DEVID_NONE,
+ .swnode = &gpio_test_provider_swnode,
+ };
+
+ prvd = kunit_platform_device_register_full(test, &pdevinfo);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, prvd);
+
+ wait_for_device_probe();
+
+ scoped_guard(device, &prvd->dev)
+ bound = device_is_bound(&prvd->dev);
+ KUNIT_ASSERT_TRUE(test, bound);
+
+ scoped_guard(device, &cons->dev)
+ bound = device_is_bound(&cons->dev);
+ KUNIT_ASSERT_TRUE(test, bound);
+
+ pdata = dev_get_platdata(&cons->dev);
+ KUNIT_ASSERT_EQ(test, pdata->gpio_err, 0);
+
+ /* Tear down the consumer before the provider to free the GPIO. */
+ kunit_platform_device_unregister(test, cons);
+}
+
+static int gpio_swnode_probe_order_test_init(struct kunit *test)
+{
+ /*
+ * A prior test may have left a managed device link teardown queued on
+ * the device_link_mq. Flush it so that software_node_register()
+ * doesn't spuriously see the node as registered and fail with -EEXIST.
+ */
+ device_link_wait_removal();
+
+ return 0;
+}
+
+static struct kunit_case gpio_swnode_probe_order_tests[] = {
+ KUNIT_CASE(gpio_swnode_probe_order),
+ KUNIT_CASE(gpio_swnode_probe_defer_on_unregistered),
+ { }
+};
+
+static struct kunit_suite gpio_swnode_probe_order_test_suite = {
+ .name = "gpio-swnode-probe-order",
+ .test_cases = gpio_swnode_probe_order_tests,
+ .init = gpio_swnode_probe_order_test_init,
+};
+
static BLOCKING_NOTIFIER_HEAD(gpio_unbind_notifier);

struct gpio_unbind_consumer_drvdata {
@@ -310,15 +575,24 @@ static void gpio_unbind_with_consumers(struct kunit *test)
0, GPIO_ACTIVE_HIGH);
properties[1] = (struct property_entry){ };

- pdevinfo = (struct platform_device_info){
- .name = GPIO_UNBIND_TEST_CONSUMER,
- .id = PLATFORM_DEVID_NONE,
- .properties = properties,
- };
-
- cons = kunit_platform_device_register_full(test, &pdevinfo);
+ /*
+ * This test deliberately keeps the consumer bound while the provider
+ * is unregistered. fw_devlink would force-unbind the consumer before
+ * the provider so use the FWNODE_FLAG_LINKS_ADDED flag to opt out of
+ * it as a workaround.
+ */
+ cons = kunit_platform_device_alloc(test, GPIO_UNBIND_TEST_CONSUMER,
+ PLATFORM_DEVID_NONE);
KUNIT_ASSERT_NOT_ERR_OR_NULL(test, cons);

+ ret = device_create_managed_software_node(&cons->dev, properties, NULL);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ fwnode_set_flag(dev_fwnode(&cons->dev), FWNODE_FLAG_LINKS_ADDED);
+
+ ret = kunit_platform_device_add(test, cons);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
wait_for_device_probe();
scoped_guard(device, &cons->dev)
bound = device_is_bound(&cons->dev);
@@ -346,10 +620,13 @@ static struct kunit_case gpio_unbind_with_consumers_tests[] = {
static struct kunit_suite gpio_unbind_with_consumers_test_suite = {
.name = "gpio-unbind-with-consumers",
.test_cases = gpio_unbind_with_consumers_tests,
+ /* We need this here too to clean any left over links. */
+ .init = gpio_swnode_probe_order_test_init,
};

kunit_test_suites(
&gpio_swnode_lookup_test_suite,
+ &gpio_swnode_probe_order_test_suite,
&gpio_unbind_with_consumers_test_suite,
);


--
2.47.3

Bartosz Golaszewski

unread,
Jul 13, 2026, 7:46:16 AM (14 days ago) Jul 13
to Bartosz Golaszewski, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org, Brendan Higgins, David Gow, Rae Moar, Andy Shevchenko, Daniel Scally, Heikki Krogerus, Sakari Ailus, Bartosz Golaszewski, Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, Linus Walleij, Dmitry Torokhov
Just a clarification: patch 5/5 will conflict with current gpio/for-next so
this one would have to go through the GPIO tree. An immutable branch with
commits 1-4 would be great.

Bart

Danilo Krummrich

unread,
Jul 15, 2026, 5:28:46 PM (11 days ago) Jul 15
to Bartosz Golaszewski, Brendan Higgins, David Gow, Rae Moar, Andy Shevchenko, Daniel Scally, Heikki Krogerus, Sakari Ailus, Bartosz Golaszewski, Greg Kroah-Hartman, Rafael J . Wysocki, Danilo Krummrich, Linus Walleij, Dmitry Torokhov, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org
On Mon, 13 Jul 2026 13:14:43 +0200, Bartosz Golaszewski wrote:
> [PATCH v4 0/5] software node: provide support for fw_devlink

Applied, thanks!

Branch: topic/swnode
Tree: git://git.kernel.org/pub/scm/linux/kernel/git/driver-core/driver-core.git

[1/5] kunit: provide a set of fwnode-oriented helpers
commit: 9fdbdb528488
[2/5] software node: add fw_devlink support
commit: 37e586675993
[3/5] software node: add kunit tests for fw_devlink support
commit: eecba5978729
[4/5] MAINTAINERS: add myself as reviewer of software node support
commit: 122630406245

The patches are in the topic/swnode topic branch and will be merged into the
corresponding target branch soon.

Danilo Krummrich

unread,
Jul 15, 2026, 5:30:33 PM (11 days ago) Jul 15
to Bartosz Golaszewski, Brendan Higgins, David Gow, Rae Moar, Andy Shevchenko, Daniel Scally, Heikki Krogerus, Sakari Ailus, Bartosz Golaszewski, Greg Kroah-Hartman, Rafael J . Wysocki, Linus Walleij, Dmitry Torokhov, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org
I will provide a signed tag once I have the 0-day result.

Thanks,
Danilo

Danilo Krummrich

unread,
Jul 17, 2026, 9:50:02 AM (9 days ago) Jul 17
to Bartosz Golaszewski, Brendan Higgins, David Gow, Rae Moar, Andy Shevchenko, Daniel Scally, Heikki Krogerus, Sakari Ailus, Bartosz Golaszewski, Greg Kroah-Hartman, Rafael J . Wysocki, Linus Walleij, Dmitry Torokhov, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org

Bartosz Golaszewski

unread,
Jul 20, 2026, 3:15:14 AM (7 days ago) Jul 20
to Danilo Krummrich, Brendan Higgins, David Gow, Rae Moar, Andy Shevchenko, Daniel Scally, Heikki Krogerus, Sakari Ailus, Bartosz Golaszewski, Greg Kroah-Hartman, Rafael J . Wysocki, Linus Walleij, Dmitry Torokhov, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org, Bartosz Golaszewski
Pulled, thanks!

Bartosz

Bartosz Golaszewski

unread,
Jul 20, 2026, 4:15:51 AM (7 days ago) Jul 20
to Brendan Higgins, David Gow, Rae Moar, Andy Shevchenko, Daniel Scally, Heikki Krogerus, Sakari Ailus, Bartosz Golaszewski, Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, Linus Walleij, Dmitry Torokhov, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org, Bartosz Golaszewski
The software node fw_devlink support already has its own kunit suite, but
that verifies the fwnode links in isolation. Add GPIO tests that prove
the ordering works in a real-life use-case: a GPIO consumer that
references its provider via a software node.

The first suite registers the provider's software node, adds the consumer
device first and checks that fw_devlink defers its probe until the
provider has been added and bound. The second covers the fallback:
with the provider's software node not yet registered no supplier link is
created, so the consumer probes, devm_gpiod_get() returns -EPROBE_DEFER
and the consumer only binds once the provider shows up.

While at it: the existing gpio_unbind_with_consumers() test keeps the
consumer bound while the provider goes away and then operates the orphaned
descriptor. With software nodes now being covered by fw_devlink that would
instead force-unbind the consumer along with the provider, so opt it out
by setting FWNODE_FLAG_LINKS_ADDED.

Acked-by: Andy Shevchenko <andriy.s...@linux.intel.com>
Tested-by: David Gow <da...@davidgow.net>
Signed-off-by: Bartosz Golaszewski <bartosz.g...@oss.qualcomm.com>
---
Currently only devicetree systems have their devices' probe ordered
against their suppliers automatically by fw_devlink. Software nodes have
lately been used extensively treewide to describe references to resource
suppliers: most notably, the GPIO subsystem uses it in many places. Now
that the conversion of "dangling" GPIO chip software nodes to using real
links is almost done, it makes sense to ensure no needless probe
deferrals by implementing the add_links() callback from the fwnode
interface.

This series extends software node support to use fw_devlink and adds test
coverage for the new behaviour.

This is the remaining GPIO patch (originally patch 5/5) that had some
conflicts with my gpio/for-next branch. It's rebased on top of the
changes Danilo queued in the driver core tree.
---
Changes in v5:
- Drop patches that went into linux-next and rebase the remaining GPIO
patch on top of the already queued kunit test cases
- Link to v4: https://patch.msgid.link/20260713-swnode-fw-dev...@oss.qualcomm.com

Changes in v4:
- Extend patch 1 with an additional kunit helper:
kunit_device_add_software_node() and use it in patch 3/5 to fix the
release ordering
- Fix a memory leak in GPIO test cases pointed out by Danilo
- Fix more possible device_link_mq races: extend the coverage of
device_link_wait_removal()
- Link to v3: https://patch.msgid.link/20260710-swnode-fw-dev...@oss.qualcomm.com

Changes in v3:
- Use device_match_fwnode() where applicable
- Fix a race between the actions on the device_link_mq and the GPIO
test cases' swnode management
- Link to v2: https://patch.msgid.link/20260706-swnode-fw-dev...@oss.qualcomm.com

Changes in v2:
- Use loop-local variables where applicable
- Reformat a long comment
- Keep trailing commas on array initializations which may potentially be
extended in the future
- Add missing headers
- Update MAINTAINERS in commit adding a new file
- Initialize arrays at declaration
- Fix signedness of ints where needed
- Remove unneeded DISABLE_STRUCTLEAK_PLUGIN switch for swnode tests
- Link to v1: https://patch.msgid.link/20260629-swnode-fw-dev...@oss.qualcomm.com
---
drivers/gpio/gpiolib-kunit.c | 291 +++++++++++++++++++++++++++++++++++++++++--
1 file changed, 284 insertions(+), 7 deletions(-)

diff --git a/drivers/gpio/gpiolib-kunit.c b/drivers/gpio/gpiolib-kunit.c
index c9c5b437482012cf9dbf8d593fa686e248ea1c88..e6cb43a8df5b8ea3f1904ecfca4d0bc4fe2d0e83 100644
--- a/drivers/gpio/gpiolib-kunit.c
+++ b/drivers/gpio/gpiolib-kunit.c
@@ -3,6 +3,8 @@
* Copyright (C) Qualcomm Technologies, Inc. and/or its subsidiaries
*/

+#include <linux/cleanup.h>
+#include <linux/err.h>
#include <linux/fwnode.h>
#include <linux/gpio/consumer.h>
#include <linux/gpio/driver.h>
@@ -11,12 +13,16 @@
#include <linux/notifier.h>
#include <linux/platform_device.h>
#include <linux/property.h>
+#include <linux/types.h>

+#include <kunit/fwnode.h>
#include <kunit/platform_device.h>
#include <kunit/test.h>

#define GPIO_TEST_PROVIDER "gpio-test-provider"
#define GPIO_SWNODE_TEST_CONSUMER "gpio-swnode-test-consumer"
+#define GPIO_PROBE_ORDER_TEST_CONSUMER "gpio-probe-order-test-consumer"
+#define GPIO_PROBE_DEFER_TEST_CONSUMER "gpio-probe-defer-test-consumer"
#define GPIO_UNBIND_TEST_CONSUMER "gpio-unbind-test-consumer"
#define GPIO_CONSUMER_NAME "gpio-swnode-consumer-test-device"

@@ -275,6 +281,265 @@ static struct kunit_suite gpio_swnode_lookup_test_suite = {
.init = gpio_swnode_register_drivers,
@@ -372,15 +637,24 @@ static void gpio_unbind_with_consumers(struct kunit *test)
@@ -408,6 +682,8 @@ static struct kunit_case gpio_unbind_with_consumers_tests[] = {
static struct kunit_suite gpio_unbind_with_consumers_test_suite = {
.name = "gpio-unbind-with-consumers",
.test_cases = gpio_unbind_with_consumers_tests,
+ /* We need this here too to clean any left over links. */
+ .init = gpio_swnode_probe_order_test_init,
};

/*
@@ -593,6 +869,7 @@ static struct kunit_suite gpio_swnode_hog_test_suite = {

kunit_test_suites(
&gpio_swnode_lookup_test_suite,
+ &gpio_swnode_probe_order_test_suite,
&gpio_unbind_with_consumers_test_suite,
&gpio_swnode_hog_test_suite,
);

---
base-commit: bc289224b9a1b0a7045c81272172342811b5c0fd

Bartosz Golaszewski

unread,
Jul 20, 2026, 6:15:01 AM (7 days ago) Jul 20
to Brendan Higgins, David Gow, Rae Moar, Andy Shevchenko, Daniel Scally, Heikki Krogerus, Sakari Ailus, Bartosz Golaszewski, Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich, Linus Walleij, Dmitry Torokhov, Bartosz Golaszewski, linux-...@vger.kernel.org, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux...@vger.kernel.org, drive...@lists.linux.dev, linux...@vger.kernel.org

On Mon, 20 Jul 2026 10:15:31 +0200, Bartosz Golaszewski wrote:
> The software node fw_devlink support already has its own kunit suite, but
> that verifies the fwnode links in isolation. Add GPIO tests that prove
> the ordering works in a real-life use-case: a GPIO consumer that
> references its provider via a software node.
>
> The first suite registers the provider's software node, adds the consumer
> device first and checks that fw_devlink defers its probe until the
> provider has been added and bound. The second covers the fallback:
> with the provider's software node not yet registered no supplier link is
> created, so the consumer probes, devm_gpiod_get() returns -EPROBE_DEFER
> and the consumer only binds once the provider shows up.
>
> [...]

Applied, thanks!

[1/1] gpio: kunit: add test cases verifying swnode devlink support
https://git.kernel.org/brgl/c/9672edf75bc7ba3fb5b7d529936548de38b5908e
Reply all
Reply to author
Forward
0 new messages