mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Shuangpeng Bai <shuangpeng.kernel@gmail.com>
To: Bjorn Helgaas <bhelgaas@google.com>
Cc: linux-pci@vger.kernel.org, linux-kernel@vger.kernel.org,
	Shuangpeng Bai <shuangpeng.kernel@gmail.com>
Subject: [PATCH] PCI/VGA: vgaarb: Hold pci_dev references for per-file cards
Date: Tue, 30 Jun 2026 12:30:58 -0400	[thread overview]
Message-ID: <86061f17ea5f1fb76791ca5cc787ce84b6752f46.1782788237.git.shuangpeng.kernel@gmail.com> (raw)
In-Reply-To: <330FD8DD-ECBF-4531-900E-5B976FA9DF90@gmail.com>

The VGA arbiter stores pci_dev pointers in each /dev/vga_arbiter file's
private state to track the target device and per-card lock counts.  These
pointers may outlive PCI hot-remove, but the private state did not hold
pci_dev references for them.

After a userspace client opens /dev/vga_arbiter, selects and locks a PCI
VGA device, removes that device via sysfs, and closes the old fd,
vga_arb_release() can still dereference uc->pdev for dev_dbg() and
vga_put().  If the PCI device has already been freed, dynamic debug may
read the freed struct device in dev_driver_string(), triggering a KASAN
use-after-free.

Make each non-NULL priv->cards[] entry own one pci_dev reference.
priv->target is kept only as an alias of a tracked cards[] entry and does
not own an extra reference.  When selecting a target, consume the lookup
reference by either storing it in a new cards[] entry or dropping it if
the device is already tracked.  On release, use the still-referenced
pdevs to clean up lock accounting, then drop the pci_dev references after
leaving the spinlock.

Take vga_lock while converting vga_default_device() into a referenced
pci_dev and while looking up vgadev entries, so the reference acquisition
and vga_list traversal are serialized with VGA device removal.

Fixes: deb2d2ecd43d ("PCI/GPU: implement VGA arbitration on Linux")
Closes: https://lore.kernel.org/r/330FD8DD-ECBF-4531-900E-5B976FA9DF90@gmail.com/
Signed-off-by: Shuangpeng Bai <shuangpeng.kernel@gmail.com>
---
 drivers/pci/vgaarb.c | 87 +++++++++++++++++++++++++++++++++++---------
 1 file changed, 70 insertions(+), 17 deletions(-)

diff --git a/drivers/pci/vgaarb.c b/drivers/pci/vgaarb.c
index c360eee11dd9..244671d14e08 100644
--- a/drivers/pci/vgaarb.c
+++ b/drivers/pci/vgaarb.c
@@ -1044,6 +1044,58 @@ struct vga_arb_private {
 	spinlock_t lock;
 };
 
+/*
+ * Each non-NULL priv->cards[i].pdev owns one pci_dev reference.
+ * priv->target is only an alias of one of priv->cards[] and does not
+ * own an extra reference.
+ *
+ * On success, this consumes the caller's @pdev reference.  If the device
+ * is already tracked, the temporary reference is dropped; otherwise it is
+ * stored in a new cards[] entry.  On failure, the caller still owns @pdev.
+ */
+static int vga_arb_set_target(struct vga_arb_private *priv,
+			      struct pci_dev *pdev)
+{
+	unsigned long flags;
+	int i, ret = -ENOMEM;
+
+	spin_lock_irqsave(&priv->lock, flags);
+	for (i = 0; i < MAX_USER_CARDS; i++) {
+		if (priv->cards[i].pdev == pdev) {
+			priv->target = priv->cards[i].pdev;
+			ret = 0;
+			break;
+		}
+		if (!priv->cards[i].pdev) {
+			priv->cards[i].pdev = pdev;
+			priv->cards[i].io_cnt = 0;
+			priv->cards[i].mem_cnt = 0;
+			priv->target = priv->cards[i].pdev;
+			pdev = NULL;
+			ret = 0;
+			break;
+		}
+	}
+	spin_unlock_irqrestore(&priv->lock, flags);
+
+	if (!ret)
+		pci_dev_put(pdev);
+
+	return ret;
+}
+
+static struct pci_dev *vga_arb_get_default_pdev(void)
+{
+	struct pci_dev *pdev;
+	unsigned long flags;
+
+	spin_lock_irqsave(&vga_lock, flags);
+	pdev = pci_dev_get(vga_default_device());
+	spin_unlock_irqrestore(&vga_lock, flags);
+
+	return pdev;
+}
+
 static LIST_HEAD(vga_user_list);
 static DEFINE_SPINLOCK(vga_user_lock);
 
@@ -1294,13 +1346,14 @@ static ssize_t vga_arb_write(struct file *file, const char __user *buf,
 	} else if (strncmp(curr_pos, "target ", 7) == 0) {
 		unsigned int domain, bus, devfn;
 		struct vga_device *vgadev;
+		unsigned long flags;
 
 		curr_pos += 7;
 		remaining -= 7;
 		pr_debug("client 0x%p called 'target'\n", priv);
 		/* If target is default */
 		if (!strncmp(curr_pos, "default", 7))
-			pdev = pci_dev_get(vga_default_device());
+			pdev = vga_arb_get_default_pdev();
 		else {
 			if (!vga_pci_str_to_vars(curr_pos, remaining,
 						 &domain, &bus, &devfn)) {
@@ -1321,7 +1374,9 @@ static ssize_t vga_arb_write(struct file *file, const char __user *buf,
 				pdev);
 		}
 
+		spin_lock_irqsave(&vga_lock, flags);
 		vgadev = vgadev_find(pdev);
+		spin_unlock_irqrestore(&vga_lock, flags);
 		pr_debug("vgadev %p\n", vgadev);
 		if (vgadev == NULL) {
 			if (pdev) {
@@ -1333,18 +1388,8 @@ static ssize_t vga_arb_write(struct file *file, const char __user *buf,
 			goto done;
 		}
 
-		priv->target = pdev;
-		for (i = 0; i < MAX_USER_CARDS; i++) {
-			if (priv->cards[i].pdev == pdev)
-				break;
-			if (priv->cards[i].pdev == NULL) {
-				priv->cards[i].pdev = pdev;
-				priv->cards[i].io_cnt = 0;
-				priv->cards[i].mem_cnt = 0;
-				break;
-			}
-		}
-		if (i == MAX_USER_CARDS) {
+		ret_val = vga_arb_set_target(priv, pdev);
+		if (ret_val) {
 			vgaarb_dbg(&pdev->dev, "maximum user cards (%d) number reached, ignoring this one!\n",
 				MAX_USER_CARDS);
 			pci_dev_put(pdev);
@@ -1354,7 +1399,6 @@ static ssize_t vga_arb_write(struct file *file, const char __user *buf,
 		}
 
 		ret_val = count;
-		pci_dev_put(pdev);
 		goto done;
 
 
@@ -1394,6 +1438,7 @@ static __poll_t vga_arb_fpoll(struct file *file, poll_table *wait)
 
 static int vga_arb_open(struct inode *inode, struct file *file)
 {
+	struct pci_dev *pdev;
 	struct vga_arb_private *priv;
 	unsigned long flags;
 
@@ -1410,8 +1455,9 @@ static int vga_arb_open(struct inode *inode, struct file *file)
 	spin_unlock_irqrestore(&vga_user_lock, flags);
 
 	/* Set the client's lists of locks */
-	priv->target = vga_default_device(); /* Maybe this is still null! */
-	priv->cards[0].pdev = priv->target;
+	pdev = vga_arb_get_default_pdev(); /* Maybe this is still null! */
+	priv->cards[0].pdev = pdev;
+	priv->target = pdev;
 	priv->cards[0].io_cnt = 0;
 	priv->cards[0].mem_cnt = 0;
 
@@ -1422,8 +1468,9 @@ static int vga_arb_release(struct inode *inode, struct file *file)
 {
 	struct vga_arb_private *priv = file->private_data;
 	struct vga_arb_user_card *uc;
+	struct pci_dev *pdevs[MAX_USER_CARDS];
 	unsigned long flags;
-	int i;
+	int i, nr_pdevs = 0;
 
 	pr_debug("%s\n", __func__);
 
@@ -1439,9 +1486,15 @@ static int vga_arb_release(struct inode *inode, struct file *file)
 			vga_put(uc->pdev, VGA_RSRC_LEGACY_IO);
 		while (uc->mem_cnt--)
 			vga_put(uc->pdev, VGA_RSRC_LEGACY_MEM);
+		pdevs[nr_pdevs++] = uc->pdev;
+		uc->pdev = NULL;
 	}
+	priv->target = NULL;
 	spin_unlock_irqrestore(&vga_user_lock, flags);
 
+	for (i = 0; i < nr_pdevs; i++)
+		pci_dev_put(pdevs[i]);
+
 	kfree(priv);
 
 	return 0;
-- 
2.43.0


      reply	other threads:[~2026-06-30 16:31 UTC|newest]

Thread overview: 2+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-06-22  4:01 [BUG] PCI/VGA: KASAN slab-use-after-free in dev_driver_string from vga_arb_release Shuangpeng
2026-06-30 16:30 ` Shuangpeng Bai [this message]

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=86061f17ea5f1fb76791ca5cc787ce84b6752f46.1782788237.git.shuangpeng.kernel@gmail.com \
    --to=shuangpeng.kernel@gmail.com \
    --cc=bhelgaas@google.com \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-pci@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®