mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Hongyan Xu <getshell@seu.edu.cn>
To: "Rafael J . Wysocki" <rafael@kernel.org>
Cc: linux-acpi@vger.kernel.org, linux-kernel@vger.kernel.org,
	stable@vger.kernel.org, Hongyan Xu <getshell@seu.edu.cn>
Subject: [PATCH] PNP: fix unlocked access to the resource list
Date: Sun,  8 Nov 2026 18:56:29 +0800	[thread overview]
Message-ID: <20261108105629.1200-1-getshell@seu.edu.cn> (raw)

pnp_range_reserved() walks dev->resources without pnp_res_mutex and
dereferences each pnp_resource, while the "clear", "auto" and "set"
branches of resources_store() free those entries without holding it.

pnp_free_resource() is list_del() + kfree(): list_del() hides the entry
only from later walks, not from one in flight; no refcount, no RCU.

pnp_res_mutex already guards dev->resources (pnp_assign_resources() and
the .get() callbacks hold it), so the unlocked readers and the remaining
writers share it; resources_store() also takes a file-static leaf lock
around its whole body, closing the two-descriptor interleave and
serializing writes per PnP device.  The pnp_assign_resources() failure
path frees via pnp_clean_resource_table() under that lock again, which
also fixes the unlocked manager.c:295-298 free after mutex_unlock().

pnp_get_resource() and the protocol .set() callbacks still walk the list
unlocked; they cannot take this mutex, because their callers in
pnp_assign_resources() already hold it and it does not recurse.

Fixes: 1b8e69662e1a ("pnp: add PNP resource range checking function")
Cc: stable@vger.kernel.org # 6.6+
Signed-off-by: Hongyan Xu <getshell@seu.edu.cn>
---
 drivers/pnp/interface.c | 52 ++++++++++++++++++++++++++++++++++-
 drivers/pnp/manager.c   | 60 ++++++++++++++++++++++++++++++++++++++---
 drivers/pnp/resource.c  | 19 ++++++++++++-
 drivers/pnp/support.c   | 11 ++++++++
 4 files changed, 137 insertions(+), 5 deletions(-)

diff --git a/drivers/pnp/interface.c b/drivers/pnp/interface.c
index 9303fef72df0..80a39cebf8e5 100644
--- a/drivers/pnp/interface.c
+++ b/drivers/pnp/interface.c
@@ -265,6 +265,30 @@ static ssize_t resources_show(struct device *dmdev,
 	buffer->buffer = buf;
 	buffer->curr = buffer->buffer;
 
+	/*
+	 * This is a sysfs attribute read, so the caller holds no PnP lock and
+	 * nothing serializes us against a concurrent write to the same
+	 * attribute from another file descriptor: kernfs only serializes
+	 * operations on the same open file.  Walk the list under the lock that
+	 * the writers of dev->resources take.
+	 *
+	 * That lock serializes the list itself, the dev->resources list head
+	 * and the struct pnp_resource entries linked into it, which is what
+	 * this walk needs.  It does not cover the scalar fields read below:
+	 * dev->active is written by pnp_activate_dev() and pnp_disable_dev()
+	 * (manager.c:438, :460) and by the protocol callbacks without a lock,
+	 * so the "state =" line can disagree with the entries below while a
+	 * store is in flight.  The worst case is a diagnostic line that is
+	 * briefly stale; dev->active is a plain int, so this is not a
+	 * use-after-free.
+	 *
+	 * resources_store_mutex is deliberately not taken here: that would
+	 * add this read to the store serialization described below, and a
+	 * diagnostic read should not queue up behind an in-progress
+	 * activation or deactivation of the device.
+	 */
+	mutex_lock(&pnp_res_mutex);
+
 	pnp_printf(buffer, "state = %s\n", dev->active ? "active" : "disabled");
 
 	list_for_each_entry(pnp_res, &dev->resources, list) {
@@ -295,6 +319,8 @@ static ssize_t resources_show(struct device *dmdev,
 		}
 	}
 
+	mutex_unlock(&pnp_res_mutex);
+
 	ret = (buffer->curr - buf);
 	kfree(buffer);
 	return ret;
@@ -333,6 +359,22 @@ static char *pnp_get_resource_value(char *buf,
 	return buf;
 }
 
+/*
+ * Serializes the writers of the "resources" attribute among each other.
+ *
+ * Taking pnp_res_mutex in each writer is not sufficient.  The "activate"
+ * branch runs the protocol .set() callback, which reads dev->resources
+ * through pnp_get_resource() without holding pnp_res_mutex, and dev->active,
+ * which gates the "clear", "auto", "set" and "fill" branches, is only set
+ * after that callback returned.  A second writer on another file descriptor
+ * therefore passes the gate and frees entries that the first one is still
+ * traversing.  The "get" branch has no dev->active gate at all, and the
+ * protocol .get() callback it calls frees the table too.
+ *
+ * Lock order: resources_store_mutex before pnp_res_mutex.
+ */
+static DEFINE_MUTEX(resources_store_mutex);
+
 static ssize_t resources_store(struct device *dmdev,
 			       struct device_attribute *attr, const char *ubuf,
 			       size_t count)
@@ -341,6 +383,8 @@ static ssize_t resources_store(struct device *dmdev,
 	char *buf = (void *)ubuf;
 	int retval = 0;
 
+	mutex_lock(&resources_store_mutex);
+
 	if (dev->status & PNP_ATTACHED) {
 		retval = -EBUSY;
 		dev_info(&dev->dev, "in use; can't configure\n");
@@ -365,14 +409,18 @@ static ssize_t resources_store(struct device *dmdev,
 	if (!strncasecmp(buf, "auto", 4)) {
 		if (dev->active)
 			goto done;
+		mutex_lock(&pnp_res_mutex);
 		pnp_init_resources(dev);
+		mutex_unlock(&pnp_res_mutex);
 		retval = pnp_auto_config_dev(dev);
 		goto done;
 	}
 	if (!strncasecmp(buf, "clear", 5)) {
 		if (dev->active)
 			goto done;
+		mutex_lock(&pnp_res_mutex);
 		pnp_init_resources(dev);
+		mutex_unlock(&pnp_res_mutex);
 		goto done;
 	}
 	if (!strncasecmp(buf, "get", 3)) {
@@ -390,8 +438,8 @@ static ssize_t resources_store(struct device *dmdev,
 		if (dev->active)
 			goto done;
 		buf += 3;
-		pnp_init_resources(dev);
 		mutex_lock(&pnp_res_mutex);
+		pnp_init_resources(dev);
 		while (1) {
 			buf = skip_spaces(buf);
 			if (!strncasecmp(buf, "io", 2)) {
@@ -432,6 +480,8 @@ static ssize_t resources_store(struct device *dmdev,
 	}
 
 done:
+	mutex_unlock(&resources_store_mutex);
+
 	if (retval < 0)
 		return retval;
 	return count;
diff --git a/drivers/pnp/manager.c b/drivers/pnp/manager.c
index 1765d6e60a8a..6360863d3dea 100644
--- a/drivers/pnp/manager.c
+++ b/drivers/pnp/manager.c
@@ -17,6 +17,51 @@
 #include <linux/mutex.h>
 #include "base.h"
 
+/*
+ * Serializes access to the resource list of every PnP device, i.e. to the
+ * dev->resources list head and to the struct pnp_resource entries linked
+ * into it: the additions in pnp_new_resource(), the removals in
+ * pnp_free_resource() and, at runtime, every direct walk of
+ * dev->resources in this directory, except the one in pnp_get_resource().
+ *
+ * "At runtime" is literal.  Three walks of dev->resources in this
+ * directory run without the lock on purpose, because they can only run
+ * while the device is being created, i.e. before it is linked into
+ * pnp_global and registered with the driver model, so no other CPU can
+ * reach it yet:
+ *
+ *   - quirks.c:305 (quirk_amd_mmconfig_area()) and quirks.c:388
+ *     (quirk_amd_ioapic()), called from pnp_fixup_device() at the top of
+ *     __pnp_add_device(), before the list_add_tail() into pnp_global and
+ *     before device_register() (core.c:164, :169, :174);
+ *   - core.c:104 (pnp_free_resources()), reached through
+ *     pnp_init_resources() from pnpacpi/core.c:273, isapnp/core.c:403 and
+ *     pnpbios/core.c:331, all three before their pnp_add_device() call.
+ *
+ * The runtime exception is a real gap.  pnp_get_resource() cannot take this
+ * mutex, because its callers in pnp_assign_resources() hold it already, so
+ * it walks the list lockless, and so does everything that reaches the list
+ * through it instead of walking dev->resources directly: the protocol
+ * .set() callbacks that program a device from pnp_start_dev(), and
+ * pnp_is_active() as used by the sound/isa drivers.  Those readers are not
+ * serialized against the writers above.
+ *
+ * The pnp_global device list is not covered either.  The naked
+ * pnp_for_each_dev() macro (include/linux/pnp.h) lets pnp_range_reserved()
+ * walk that list without pnp_lock, while pnp_delist_device() unlinks a
+ * device under pnp_lock and pnp_release_device() frees the device from the
+ * driver model release callback, so the walk can read a device that is
+ * already gone.  That is a defect of the device list, not of dev->resources,
+ * and it needs its own synchronization primitive.
+ *
+ * pnp_release_device() also calls pnp_free_resources() without the mutex,
+ * but by then the sysfs attributes are gone and the device is no longer
+ * reachable from resources_store(), so that path is out of scope here.
+ *
+ * This is an ordinary mutex, so it must not be taken from atomic context.
+ * It is not exported, so out-of-tree callers cannot acquire it; in-tree
+ * code still can, through the extern declaration in include/linux/pnp.h:330.
+ */
 DEFINE_MUTEX(pnp_res_mutex);
 
 static struct resource *pnp_find_resource(struct pnp_dev *dev,
@@ -292,12 +337,21 @@ static int pnp_assign_resources(struct pnp_dev *dev, int set)
 			break;
 	}
 
+	/*
+	 * Drop the partially assigned entries before dropping the lock, so
+	 * that no other reader of dev->resources can observe a table that is
+	 * about to be cleaned up, or briefly report it as reserved.
+	 */
+	if (ret < 0)
+		pnp_clean_resource_table(dev);
+
 	mutex_unlock(&pnp_res_mutex);
-	if (ret < 0) {
+
+	if (ret < 0)
 		pnp_dbg(&dev->dev, "pnp_assign_resources failed (%d)\n", ret);
-		pnp_clean_resource_table(dev);
-	} else
+	else
 		dbg_pnp_show_resources(dev, "pnp_assign_resources succeeded");
+
 	return ret;
 }
 
diff --git a/drivers/pnp/resource.c b/drivers/pnp/resource.c
index 6792bd023123..834aed24633f 100644
--- a/drivers/pnp/resource.c
+++ b/drivers/pnp/resource.c
@@ -20,6 +20,7 @@
 #include <linux/libata.h>
 #include <linux/ioport.h>
 #include <linux/init.h>
+#include <linux/mutex.h>
 
 #include <linux/pnp.h>
 #include "base.h"
@@ -690,14 +691,30 @@ int pnp_range_reserved(resource_size_t start, resource_size_t end)
 	struct pnp_resource *pnp_res;
 	resource_size_t *dev_start, *dev_end;
 
+	/*
+	 * pnp_res_mutex only serializes the walk over each device's resource
+	 * list.  The traversal of the pnp_global device list itself is not
+	 * covered by it and this function holds no other lock, so it can run
+	 * concurrently with the removal of a device; see the comment on
+	 * pnp_res_mutex for why that is a separate defect that needs its own
+	 * primitive.
+	 *
+	 * Taking the mutex makes this function sleepable, which is a change
+	 * for an exported symbol: atomic callers are no longer allowed.
+	 */
+	mutex_lock(&pnp_res_mutex);
 	pnp_for_each_dev(dev) {
 		list_for_each_entry(pnp_res, &dev->resources, list) {
 			dev_start = &pnp_res->res.start;
 			dev_end   = &pnp_res->res.end;
-			if (ranged_conflict(&start, &end, dev_start, dev_end))
+			if (ranged_conflict(&start, &end, dev_start, dev_end)) {
+				mutex_unlock(&pnp_res_mutex);
 				return 1;
+			}
 		}
 	}
+	mutex_unlock(&pnp_res_mutex);
+
 	return 0;
 }
 EXPORT_SYMBOL(pnp_range_reserved);
diff --git a/drivers/pnp/support.c b/drivers/pnp/support.c
index f6c866851769..559d66698742 100644
--- a/drivers/pnp/support.c
+++ b/drivers/pnp/support.c
@@ -10,6 +10,7 @@
 #include <linux/module.h>
 #include <linux/ctype.h>
 #include <linux/hex.h>
+#include <linux/mutex.h>
 #include <linux/pnp.h>
 #include "base.h"
 
@@ -80,6 +81,14 @@ void dbg_pnp_show_resources(struct pnp_dev *dev, char *desc)
 {
 	struct pnp_resource *pnp_res;
 
+	/*
+	 * Both callers, the tail of pnp_assign_resources() and
+	 * pnp_start_dev(), run without pnp_res_mutex and can execute
+	 * concurrently with a user space write to the sysfs "resources"
+	 * attribute, so the walk needs the lock.
+	 */
+	mutex_lock(&pnp_res_mutex);
+
 	if (list_empty(&dev->resources))
 		pnp_dbg(&dev->dev, "%s: no current resources\n", desc);
 	else {
@@ -87,6 +96,8 @@ void dbg_pnp_show_resources(struct pnp_dev *dev, char *desc)
 		list_for_each_entry(pnp_res, &dev->resources, list)
 			pnp_dbg(&dev->dev, "%pr\n", &pnp_res->res);
 	}
+
+	mutex_unlock(&pnp_res_mutex);
 }
 
 char *pnp_option_priority_name(struct pnp_option *option)
-- 
2.50.1.windows.1


                 reply	other threads:[~2026-11-08 10:56 UTC|newest]

Thread overview: [no followups] expand[flat|nested]  mbox.gz  Atom feed

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=20261108105629.1200-1-getshell@seu.edu.cn \
    --to=getshell@seu.edu.cn \
    --cc=linux-acpi@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=rafael@kernel.org \
    --cc=stable@vger.kernel.org \
    /path/to/YOUR_REPLY

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

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

all inboxes | Powered by JetHome®