All of lore.kernel.org
 help / color / mirror / Atom feed
From: Chad Aldreda <ca@modelmiser.com>
To: linux-kernel@vger.kernel.org
Cc: Greg Kroah-Hartman <gregkh@linuxfoundation.org>,
	"Rafael J. Wysocki" <rafael@kernel.org>,
	Danilo Krummrich <dakr@kernel.org>,
	Johan Hovold <johan@kernel.org>,
	driver-core@lists.linux.dev, linux-media@vger.kernel.org,
	Mauro Carvalho Chehab <mchehab@kernel.org>,
	Hans Verkuil <hverkuil+cisco@kernel.org>,
	Laurent Pinchart <laurent.pinchart@ideasonboard.com>,
	Nick Faro <yux50000@hotmail.com>
Subject: [RFC PATCH 4/4] drivers: base: test: add a vehicle for DEBUG_DEVRES_REFS
Date: Wed, 04 Nov 2026 22:00:57 +0000	[thread overview]
Message-ID: <20261104220041.3994559-3-ca@modelmiser.com> (raw)
In-Reply-To: <20261104215546.3990950-1-ca@modelmiser.com>

Register a platform device whose driver exposes two misc devices, one
character device and one debugfs file: /dev/devm_refs_bad keeps its
state, with the miscdevice embedded, in devm memory; /dev/devm_refs_good
keeps refcounted state freed on last close; /dev/devm_refs_cdev is a
struct cdev embedded in devm memory; the debugfs file bad_magic points
into the devm state and is removed by a devres action registered after
that allocation. The driver depends on DEBUG_FS for that file.

Unbinding the device with all four held open must warn exactly twice,
for the devm-backed misc node and for the cdev, and report the debugfs
file at info level after release; closing the bad node and the cdev
node afterwards are use-after-frees that KASAN reports independently
(the cdev close reads the freed cdev in cdev_put() and then writes its
kobject refcount in kobject_put(), so run this under KASAN only).
Rebinding and unbinding with nothing open must be silent.

Written with Claude (Anthropic) under my direction; see the cover letter
for the disclosure required by Documentation/process/generated-content.rst.

Signed-off-by: Chad Aldreda <ca@modelmiser.com>
---
 drivers/base/test/Kconfig          |  15 ++
 drivers/base/test/Makefile         |   2 +
 drivers/base/test/devm_refs_test.c | 323 +++++++++++++++++++++++++++++
 3 files changed, 340 insertions(+)
 create mode 100644 drivers/base/test/devm_refs_test.c

diff --git a/drivers/base/test/Kconfig b/drivers/base/test/Kconfig
index 1ecf079124..e04dfda988 100644
--- a/drivers/base/test/Kconfig
+++ b/drivers/base/test/Kconfig
@@ -23,3 +23,18 @@ 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
+
+config DEVM_REFS_TEST
+	tristate "Test driver for DEBUG_DEVRES_REFS"
+	depends on DEBUG_DEVRES_REFS && DEBUG_FS
+	help
+	  Registers a platform device whose driver exposes /dev/devm_refs_bad
+	  (state in devm memory, freed on unbind while open),
+	  /dev/devm_refs_good (refcounted state), /dev/devm_refs_cdev (a
+	  struct cdev embedded in devm memory) and a debugfs file bad_magic
+	  whose data is that devm memory and whose removal is a devres
+	  action. Unbinding with all four open must warn once for the bad
+	  node and once for the cdev, and report the debugfs file at info
+	  level after release. See drivers/base/test/devm_refs_test.c.
+
+	  If unsure say N.
diff --git a/drivers/base/test/Makefile b/drivers/base/test/Makefile
index 9ced7bbd56..556b9d2635 100644
--- a/drivers/base/test/Makefile
+++ b/drivers/base/test/Makefile
@@ -8,3 +8,5 @@ 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
+
+obj-$(CONFIG_DEVM_REFS_TEST)	+= devm_refs_test.o
diff --git a/drivers/base/test/devm_refs_test.c b/drivers/base/test/devm_refs_test.c
new file mode 100644
index 0000000000..40b4211d52
--- /dev/null
+++ b/drivers/base/test/devm_refs_test.c
@@ -0,0 +1,323 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Test vehicle for CONFIG_DEBUG_DEVRES_REFS.
+ *
+ * Registers one platform device bound to a driver that exposes two misc
+ * devices, one character device and one debugfs file (see
+ * devm_refs_dbg_show() below):
+ *
+ *   /dev/devm_refs_bad  - driver state allocated with devm_kzalloc(), so
+ *                         it is freed on unbind even while the node is open.
+ *                         file->private_data (set by the misc core to the
+ *                         embedded struct miscdevice) points into it.
+ *   /dev/devm_refs_good - driver state refcounted and freed on last put,
+ *                         so unbind with the node open is safe.
+ *   /dev/devm_refs_cdev - a struct cdev embedded in devm memory; an open
+ *                         file reaches it through inode->i_cdev.
+ *
+ * Unbinding the device with all nodes held open must produce exactly two
+ * DEBUG_DEVRES_REFS warnings, for the bad node and for the cdev, and an
+ * info line for the debugfs file after release. Closing the bad node and
+ * the cdev node after unbind are use-after-frees that KASAN reports
+ * independently; the cdev close reads the freed object in cdev_put() and
+ * then writes its kobject refcount in kobject_put(), so run this under
+ * KASAN only.
+ */
+#include <linux/cdev.h>
+#include <linux/debugfs.h>
+#include <linux/fs.h>
+#include <linux/kref.h>
+#include <linux/miscdevice.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <linux/seq_file.h>
+#include <linux/slab.h>
+#include <linux/uaccess.h>
+
+#define DEVM_REFS_MAGIC	0x5245465344455644ULL	/* "DVEDSFER" */
+
+struct devm_refs_bad {
+	struct miscdevice misc;
+	u64 magic;
+};
+
+struct devm_refs_good {
+	struct miscdevice misc;
+	struct kref kref;
+	u64 magic;
+};
+
+struct devm_refs_cdev {
+	struct cdev cdev;
+	u64 magic;
+};
+
+struct devm_refs_state {
+	struct devm_refs_bad *bad;
+	struct devm_refs_good *good;
+	struct devm_refs_cdev *cd;
+	struct class *class;
+	dev_t devt;
+	struct dentry *dbg;
+};
+
+static ssize_t devm_refs_cdev_read(struct file *file, char __user *buf,
+				   size_t len, loff_t *ppos)
+{
+	struct devm_refs_cdev *cd =
+		container_of(file_inode(file)->i_cdev, struct devm_refs_cdev, cdev);
+	char tmp[32];
+	int n;
+
+	n = scnprintf(tmp, sizeof(tmp), "%s\n",
+		      cd->magic == DEVM_REFS_MAGIC ? "ok" : "CORRUPT");
+	return simple_read_from_buffer(buf, len, ppos, tmp, n);
+}
+
+static const struct file_operations devm_refs_cdev_fops = {
+	.owner = THIS_MODULE,
+	.read = devm_refs_cdev_read,
+};
+
+/*
+ * A debugfs file whose i_private points into devm memory and whose
+ * removal is a devres action registered AFTER that allocation: debugfs
+ * revokes it before the memory goes, so the check must report it at info
+ * level after release, not WARN during the walk.
+ */
+static int devm_refs_dbg_show(struct seq_file *m, void *v)
+{
+	struct devm_refs_bad *bad = m->private;
+
+	seq_printf(m, "%s\n", bad->magic == DEVM_REFS_MAGIC ? "ok" : "CORRUPT");
+	return 0;
+}
+DEFINE_SHOW_ATTRIBUTE(devm_refs_dbg);
+
+static void devm_refs_dbg_remove(void *dir)
+{
+	debugfs_remove_recursive(dir);
+}
+
+static ssize_t devm_refs_bad_read(struct file *file, char __user *buf,
+				  size_t len, loff_t *ppos)
+{
+	struct devm_refs_bad *bad =
+		container_of(file->private_data, struct devm_refs_bad, misc);
+	char tmp[32];
+	int n;
+
+	n = scnprintf(tmp, sizeof(tmp), "%s\n",
+		      bad->magic == DEVM_REFS_MAGIC ? "ok" : "CORRUPT");
+	return simple_read_from_buffer(buf, len, ppos, tmp, n);
+}
+
+static int devm_refs_bad_release(struct inode *inode, struct file *file)
+{
+	struct devm_refs_bad *bad =
+		container_of(file->private_data, struct devm_refs_bad, misc);
+
+	/* After unbind this is a read of freed memory. */
+	pr_info("devm_refs_test: bad release, magic %s\n",
+		bad->magic == DEVM_REFS_MAGIC ? "ok" : "CORRUPT");
+	return 0;
+}
+
+static const struct file_operations devm_refs_bad_fops = {
+	.owner = THIS_MODULE,
+	.read = devm_refs_bad_read,
+	.release = devm_refs_bad_release,
+};
+
+static void devm_refs_good_free(struct kref *kref)
+{
+	struct devm_refs_good *good =
+		container_of(kref, struct devm_refs_good, kref);
+
+	pr_info("devm_refs_test: good freed on last put\n");
+	kfree(good);
+}
+
+static int devm_refs_good_open(struct inode *inode, struct file *file)
+{
+	struct devm_refs_good *good =
+		container_of(file->private_data, struct devm_refs_good, misc);
+
+	kref_get(&good->kref);
+	return 0;
+}
+
+static ssize_t devm_refs_good_read(struct file *file, char __user *buf,
+				   size_t len, loff_t *ppos)
+{
+	struct devm_refs_good *good =
+		container_of(file->private_data, struct devm_refs_good, misc);
+	char tmp[32];
+	int n;
+
+	n = scnprintf(tmp, sizeof(tmp), "%s\n",
+		      good->magic == DEVM_REFS_MAGIC ? "ok" : "CORRUPT");
+	return simple_read_from_buffer(buf, len, ppos, tmp, n);
+}
+
+static int devm_refs_good_release(struct inode *inode, struct file *file)
+{
+	struct devm_refs_good *good =
+		container_of(file->private_data, struct devm_refs_good, misc);
+
+	pr_info("devm_refs_test: good release, magic %s\n",
+		good->magic == DEVM_REFS_MAGIC ? "ok" : "CORRUPT");
+	kref_put(&good->kref, devm_refs_good_free);
+	return 0;
+}
+
+static const struct file_operations devm_refs_good_fops = {
+	.owner = THIS_MODULE,
+	.open = devm_refs_good_open,
+	.read = devm_refs_good_read,
+	.release = devm_refs_good_release,
+};
+
+static int devm_refs_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev, *cdev_dev;
+	struct devm_refs_state *st;
+	int ret;
+
+	st = devm_kzalloc(dev, sizeof(*st), GFP_KERNEL);
+	if (!st)
+		return -ENOMEM;
+
+	st->bad = devm_kzalloc(dev, sizeof(*st->bad), GFP_KERNEL);
+	if (!st->bad)
+		return -ENOMEM;
+	st->bad->magic = DEVM_REFS_MAGIC;
+	st->bad->misc.minor = MISC_DYNAMIC_MINOR;
+	st->bad->misc.name = "devm_refs_bad";
+	st->bad->misc.fops = &devm_refs_bad_fops;
+	st->bad->misc.parent = dev;
+
+	st->good = kzalloc_obj(*st->good);
+	if (!st->good)
+		return -ENOMEM;
+	kref_init(&st->good->kref);
+	st->good->magic = DEVM_REFS_MAGIC;
+	st->good->misc.minor = MISC_DYNAMIC_MINOR;
+	st->good->misc.name = "devm_refs_good";
+	st->good->misc.fops = &devm_refs_good_fops;
+	st->good->misc.parent = dev;
+
+	st->dbg = debugfs_create_dir("devm_refs_test", NULL);
+	debugfs_create_file("bad_magic", 0444, st->dbg, st->bad,
+			    &devm_refs_dbg_fops);
+	ret = devm_add_action_or_reset(dev, devm_refs_dbg_remove, st->dbg);
+	if (ret)
+		goto err_free_good;
+
+	ret = misc_register(&st->bad->misc);
+	if (ret)
+		goto err_free_good;
+	ret = misc_register(&st->good->misc);
+	if (ret)
+		goto err_dereg_bad;
+
+	/* A cdev embedded in devm memory, with its own /dev node. */
+	st->cd = devm_kzalloc(dev, sizeof(*st->cd), GFP_KERNEL);
+	if (!st->cd) {
+		ret = -ENOMEM;
+		goto err_dereg_good;
+	}
+	st->cd->magic = DEVM_REFS_MAGIC;
+	ret = alloc_chrdev_region(&st->devt, 0, 1, "devm_refs_cdev");
+	if (ret)
+		goto err_dereg_good;
+	cdev_init(&st->cd->cdev, &devm_refs_cdev_fops);
+	st->cd->cdev.owner = THIS_MODULE;
+	ret = cdev_add(&st->cd->cdev, st->devt, 1);
+	if (ret)
+		goto err_region;
+	st->class = class_create("devm_refs");
+	if (IS_ERR(st->class)) {
+		ret = PTR_ERR(st->class);
+		goto err_cdev;
+	}
+	cdev_dev = device_create(st->class, dev, st->devt, NULL,
+				 "devm_refs_cdev");
+	if (IS_ERR(cdev_dev)) {
+		ret = PTR_ERR(cdev_dev);
+		goto err_class;
+	}
+
+	platform_set_drvdata(pdev, st);
+	dev_info(dev, "probed: /dev/devm_refs_bad, /dev/devm_refs_good, /dev/devm_refs_cdev\n");
+	return 0;
+
+err_class:
+	class_destroy(st->class);
+err_cdev:
+	cdev_del(&st->cd->cdev);
+err_region:
+	unregister_chrdev_region(st->devt, 1);
+err_dereg_good:
+	misc_deregister(&st->good->misc);
+err_dereg_bad:
+	misc_deregister(&st->bad->misc);
+	/* Registered or not, drop the probe-time reference; the free runs on zero. */
+	kref_put(&st->good->kref, devm_refs_good_free);
+	return ret;
+err_free_good:
+	kfree(st->good);
+	return ret;
+}
+
+static void devm_refs_remove(struct platform_device *pdev)
+{
+	struct devm_refs_state *st = platform_get_drvdata(pdev);
+
+	device_destroy(st->class, st->devt);
+	class_destroy(st->class);
+	cdev_del(&st->cd->cdev);
+	unregister_chrdev_region(st->devt, 1);
+	misc_deregister(&st->good->misc);
+	misc_deregister(&st->bad->misc);
+	kref_put(&st->good->kref, devm_refs_good_free);
+	dev_info(&pdev->dev, "removed; devres release follows\n");
+}
+
+static struct platform_driver devm_refs_driver = {
+	.probe = devm_refs_probe,
+	.remove = devm_refs_remove,
+	.driver = {
+		.name = "devm_refs_test",
+	},
+};
+
+static struct platform_device *devm_refs_pdev;
+
+static int __init devm_refs_test_init(void)
+{
+	int ret;
+
+	ret = platform_driver_register(&devm_refs_driver);
+	if (ret)
+		return ret;
+
+	devm_refs_pdev = platform_device_register_simple("devm_refs_test", -1,
+							 NULL, 0);
+	if (IS_ERR(devm_refs_pdev)) {
+		platform_driver_unregister(&devm_refs_driver);
+		return PTR_ERR(devm_refs_pdev);
+	}
+	return 0;
+}
+
+static void __exit devm_refs_test_exit(void)
+{
+	platform_device_unregister(devm_refs_pdev);
+	platform_driver_unregister(&devm_refs_driver);
+}
+
+module_init(devm_refs_test_init);
+module_exit(devm_refs_test_exit);
+MODULE_DESCRIPTION("Test vehicle for DEBUG_DEVRES_REFS");
+MODULE_LICENSE("GPL");
-- 
2.43.0



  parent reply	other threads:[~2026-11-04 22:01 UTC|newest]

Thread overview: 8+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-04 21:55 [RFC PATCH 0/4] devres: warn about managed memory still held by open files at unbind Chad Aldreda
2026-11-04 21:55 ` [RFC PATCH 1/4] debugfs: add debugfs_file_revoked() for debugging aids Chad Aldreda
2026-11-04 22:00 ` [RFC PATCH 2/4] devres: add DEBUG_DEVRES_REFS to warn about managed memory still held by open files Chad Aldreda
2026-11-04 22:00 ` [RFC PATCH 3/4] media: v4l2-dev: name the video_device behind an open file for DEBUG_DEVRES_REFS Chad Aldreda
2026-11-04 22:00 ` Chad Aldreda [this message]
2026-11-04 22:07 ` [RFC PATCH 0/4] devres: warn about managed memory still held by open files at unbind Laurent Pinchart
2026-11-04 22:31   ` Chad Aldreda
2026-11-04 22:41     ` Laurent Pinchart

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20261104220041.3994559-3-ca@modelmiser.com \
    --to=ca@modelmiser.com \
    --cc=dakr@kernel.org \
    --cc=driver-core@lists.linux.dev \
    --cc=gregkh@linuxfoundation.org \
    --cc=hverkuil+cisco@kernel.org \
    --cc=johan@kernel.org \
    --cc=laurent.pinchart@ideasonboard.com \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-media@vger.kernel.org \
    --cc=mchehab@kernel.org \
    --cc=rafael@kernel.org \
    --cc=yux50000@hotmail.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.