mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH v2] cxl/port: Fix missing port lock in cxl_dport_remove()
@ 2026-06-08 22:35 Terry Bowman
  2026-06-09  0:35 ` Dave Jiang
  0 siblings, 1 reply; 6+ messages in thread
From: Terry Bowman @ 2026-06-08 22:35 UTC (permalink / raw)
  To: dave, jic23, dave.jiang, alison.schofield, vishal.l.verma,
	ira.weiny, djbw, ming.li, rrichter, Benjamin.Cheatham
  Cc: Smita.KoralahalliChannabasappa, stable, linux-cxl, linux-kernel,
	PradeepVineshReddy.Kodamati

xa_erase() in cxl_dport_remove() runs without the port device lock,
creating a race with any caller that does xa_load() on port->dports
and then dereferences the returned dport pointer. A concurrent
cxl_dport_remove() can erase and free the dport between the xa_load()
and the caller acquiring the port lock, causing a use-after-free.

For non-root ports the port lock is already held by the caller on two
paths:

1. Driver unbind: devres_release_all() is called from
   __device_release_driver() which holds port->dev.mutex.

2. Dynamic endpoint removal: cxl_detach_ep() takes the port lock
   before calling del_dports() -> del_dport() -> devres_release_group(),
   which synchronously runs cxl_dport_remove().

Use cond_cxl_root_lock/unlock(), which only acquires the port lock when
the port is a root port and the lock is therefore not already held.
This matches the pattern used in __devm_cxl_add_dport() for the same
reason.

The write-side fix to cxl_dport_remove() is necessary but not
sufficient. Callers that obtain a dport pointer via cxl_mem_find_port()
use a lockless xa_load() and must not dereference that pointer until a
lock that excludes free_dport()/kfree() is held.

For root ports, dport_to_host() returns uport_dev, so all three devres
actions (free_dport, cxl_dport_remove, cxl_dport_unlink) are registered
on uport_dev. __device_release_driver() holds uport_dev->mutex for the
full teardown sequence including kfree(dport). Holding uport_dev->mutex
on the read side therefore excludes concurrent dport freeing.

Fix rcd_pcie_cap_emit() by passing NULL to cxl_mem_find_port() to avoid
capturing a lockless dport pointer, then re-fetching dport inside the
uport_dev guard via cxl_find_dport_by_dev(). The previous guard on
root->dev was wrong: cxl_dport_remove() releases root->dev before
free_dport() runs, so root->dev does not protect against concurrent
kfree(dport).

Fix cxl_mem_probe() similarly: pass NULL to cxl_mem_find_port(), then
re-fetch dport inside scoped_guard(device, &parent_port->dev) for the
VH path, and re-fetch again inside scoped_guard(device, uport_dev) for
the RCH path. This closes both the TOCTOU window between the lockless
xa_load() and the guard acquisition, and the window between the two
sequential guards in the RCH path where a concurrent surprise removal
could free dport before devm_cxl_add_endpoint() dereferences it.

Reported-by: Sashiko
Fixes: 391785859e7e ("cxl/port: Move dport tracking to an xarray")
Link: https://lore.kernel.org/linux-cxl/20260505173029.2718246-1-terry.bowman@amd.com/
Signed-off-by: Terry Bowman <terry.bowman@amd.com>
Reviewed-by: Ben Cheatham <Benjamin.Cheatham@amd.com>
---
 drivers/cxl/core/port.c | 10 +++++++
 drivers/cxl/mem.c       | 65 +++++++++++++++++++++++++++++++----------
 drivers/cxl/pci.c       | 17 +++++++----
 3 files changed, 72 insertions(+), 20 deletions(-)

diff --git a/drivers/cxl/core/port.c b/drivers/cxl/core/port.c
index c5aacd7054f1..0b8f144596e8 100644
--- a/drivers/cxl/core/port.c
+++ b/drivers/cxl/core/port.c
@@ -1092,8 +1092,18 @@ static void cxl_dport_remove(void *data)
 	struct cxl_dport *dport = data;
 	struct cxl_port *port = dport->port;
 
+	/*
+	 * For non-root ports the port lock is already held by the caller
+	 * via devres_release_all() during driver unbind, which holds
+	 * port->dev.mutex throughout.  Acquiring it again unconditionally
+	 * would deadlock.  Use cond_cxl_root_lock() which only acquires
+	 * when the port is a root port and the lock is therefore not yet
+	 * held.
+	 */
+	cond_cxl_root_lock(port);
 	port->nr_dports--;
 	xa_erase(&port->dports, (unsigned long) dport->dport_dev);
+	cond_cxl_root_unlock(port);
 	put_device(dport->dport_dev);
 }
 
diff --git a/drivers/cxl/mem.c b/drivers/cxl/mem.c
index fcffe24dcb42..345b56f215ff 100644
--- a/drivers/cxl/mem.c
+++ b/drivers/cxl/mem.c
@@ -70,9 +70,9 @@ static int cxl_mem_probe(struct device *dev)
 	struct cxl_memdev *cxlmd = to_cxl_memdev(dev);
 	struct cxl_memdev_state *mds = to_cxl_memdev_state(cxlmd->cxlds);
 	struct cxl_dev_state *cxlds = cxlmd->cxlds;
-	struct device *endpoint_parent;
 	struct cxl_dport *dport;
 	struct dentry *dentry;
+	bool rch = false;
 	int rc;
 
 	if (!cxlds->media_ready)
@@ -107,8 +107,7 @@ static int cxl_mem_probe(struct device *dev)
 	if (rc)
 		return rc;
 
-	struct cxl_port *parent_port __free(put_cxl_port) =
-		cxl_mem_find_port(cxlmd, &dport);
+	struct cxl_port *parent_port __free(put_cxl_port) = cxl_mem_find_port(cxlmd, NULL);
 	if (!parent_port) {
 		dev_err(dev, "CXL port topology not found\n");
 		return -ENXIO;
@@ -123,21 +122,57 @@ static int cxl_mem_probe(struct device *dev)
 		}
 	}
 
-	if (dport->rch)
-		endpoint_parent = parent_port->uport_dev;
-	else
-		endpoint_parent = &parent_port->dev;
-
-	scoped_guard(device, endpoint_parent) {
-		if (!endpoint_parent->driver) {
-			dev_err(dev, "CXL port topology %s not enabled\n",
-				dev_name(endpoint_parent));
+	scoped_guard(device, &parent_port->dev) {
+		/*
+		 * Re-fetch dport under the port lock to close the TOCTOU
+		 * window between cxl_mem_find_port()'s lockless xa_load() and
+		 * this guard acquisition.  A concurrent surprise removal can
+		 * free the dport in that window.
+		 */
+		dport = cxl_find_dport_by_dev(parent_port, cxlmd->dev.parent->parent);
+		if (!dport) {
+			dev_err(dev, "CXL port topology %s not found\n",
+				dev_name(&parent_port->dev));
 			return -ENXIO;
 		}
+		rch = dport->rch;
+
+		if (!rch) {
+			if (!parent_port->dev.driver) {
+				dev_err(dev, "CXL port topology %s not enabled\n",
+					dev_name(&parent_port->dev));
+				return -ENXIO;
+			}
+			rc = devm_cxl_add_endpoint(&parent_port->dev, cxlmd, dport);
+			if (rc)
+				return rc;
+		}
+	}
 
-		rc = devm_cxl_add_endpoint(endpoint_parent, cxlmd, dport);
-		if (rc)
-			return rc;
+	if (rch) {
+		struct device *uport_dev = parent_port->uport_dev;
+
+		scoped_guard(device, uport_dev) {
+			if (!uport_dev->driver) {
+				dev_err(dev, "CXL port topology %s not enabled\n",
+					dev_name(uport_dev));
+				return -ENXIO;
+			}
+			/*
+			 * Re-fetch dport under uport_dev lock.  uport_dev->mutex
+			 * is held for the full devres teardown sequence including
+			 * free_dport()/kfree(), so this excludes concurrent
+			 * hotplug removal through the entire dereference.
+			 */
+			dport = cxl_find_dport_by_dev(parent_port, cxlmd->dev.parent->parent);
+			if (!dport) {
+				dev_err(dev, "CXL RCH dport not found\n");
+				return -ENXIO;
+			}
+			rc = devm_cxl_add_endpoint(uport_dev, cxlmd, dport);
+			if (rc)
+				return rc;
+		}
 	}
 
 	if (cxlmd->attach) {
diff --git a/drivers/cxl/pci.c b/drivers/cxl/pci.c
index bace662dc988..710a62a66429 100644
--- a/drivers/cxl/pci.c
+++ b/drivers/cxl/pci.c
@@ -708,10 +708,10 @@ static ssize_t rcd_pcie_cap_emit(struct device *dev, u16 offset, char *buf, size
 {
 	struct cxl_dev_state *cxlds = dev_get_drvdata(dev);
 	struct cxl_memdev *cxlmd = cxlds->cxlmd;
-	struct device *root_dev;
 	struct cxl_dport *dport;
+	struct device *root_dev;
 	struct cxl_port *root __free(put_cxl_port) =
-		cxl_mem_find_port(cxlmd, &dport);
+		cxl_mem_find_port(cxlmd, NULL);
 
 	if (!root)
 		return -ENXIO;
@@ -720,13 +720,20 @@ static ssize_t rcd_pcie_cap_emit(struct device *dev, u16 offset, char *buf, size
 	if (!root_dev)
 		return -ENXIO;
 
-	if (!dport->regs.rcd_pcie_cap)
-		return -ENXIO;
-
 	guard(device)(root_dev);
 	if (!root_dev->driver)
 		return -ENXIO;
 
+	/*
+	 * Fetch dport under uport_dev lock to protect against concurrent
+	 * hotplug removal. uport_dev->mutex is held for the entire devres
+	 * teardown sequence including free_dport(), so holding it here
+	 * excludes concurrent kfree(dport).
+	 */
+	dport = cxl_find_dport_by_dev(root, cxlmd->dev.parent->parent);
+	if (!dport || !dport->regs.rcd_pcie_cap)
+		return -ENXIO;
+
 	switch (width) {
 	case 2:
 		return sysfs_emit(buf, "%#x\n",
-- 
2.34.1


^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [PATCH v2] cxl/port: Fix missing port lock in cxl_dport_remove()
  2026-06-08 22:35 [PATCH v2] cxl/port: Fix missing port lock in cxl_dport_remove() Terry Bowman
@ 2026-06-09  0:35 ` Dave Jiang
  2026-06-09  7:40   ` Richard Cheng
  2026-06-09 14:22   ` Bowman, Terry
  0 siblings, 2 replies; 6+ messages in thread
From: Dave Jiang @ 2026-06-09  0:35 UTC (permalink / raw)
  To: Terry Bowman, dave, jic23, alison.schofield, vishal.l.verma,
	ira.weiny, djbw, ming.li, rrichter, Benjamin.Cheatham
  Cc: Smita.KoralahalliChannabasappa, stable, linux-cxl, linux-kernel,
	PradeepVineshReddy.Kodamati



On 6/8/26 3:35 PM, Terry Bowman wrote:
> xa_erase() in cxl_dport_remove() runs without the port device lock,
> creating a race with any caller that does xa_load() on port->dports
> and then dereferences the returned dport pointer. A concurrent
> cxl_dport_remove() can erase and free the dport between the xa_load()
> and the caller acquiring the port lock, causing a use-after-free.
> 
> For non-root ports the port lock is already held by the caller on two
> paths:
> 
> 1. Driver unbind: devres_release_all() is called from
>    __device_release_driver() which holds port->dev.mutex.
> 
> 2. Dynamic endpoint removal: cxl_detach_ep() takes the port lock
>    before calling del_dports() -> del_dport() -> devres_release_group(),
>    which synchronously runs cxl_dport_remove().
> 
> Use cond_cxl_root_lock/unlock(), which only acquires the port lock when
> the port is a root port and the lock is therefore not already held.
> This matches the pattern used in __devm_cxl_add_dport() for the same
> reason.
> 
> The write-side fix to cxl_dport_remove() is necessary but not
> sufficient. Callers that obtain a dport pointer via cxl_mem_find_port()
> use a lockless xa_load() and must not dereference that pointer until a
> lock that excludes free_dport()/kfree() is held.
> 
> For root ports, dport_to_host() returns uport_dev, so all three devres
> actions (free_dport, cxl_dport_remove, cxl_dport_unlink) are registered
> on uport_dev. __device_release_driver() holds uport_dev->mutex for the
> full teardown sequence including kfree(dport). Holding uport_dev->mutex
> on the read side therefore excludes concurrent dport freeing.
> 
> Fix rcd_pcie_cap_emit() by passing NULL to cxl_mem_find_port() to avoid
> capturing a lockless dport pointer, then re-fetching dport inside the
> uport_dev guard via cxl_find_dport_by_dev(). The previous guard on
> root->dev was wrong: cxl_dport_remove() releases root->dev before
> free_dport() runs, so root->dev does not protect against concurrent
> kfree(dport).
> 
> Fix cxl_mem_probe() similarly: pass NULL to cxl_mem_find_port(), then
> re-fetch dport inside scoped_guard(device, &parent_port->dev) for the
> VH path, and re-fetch again inside scoped_guard(device, uport_dev) for
> the RCH path. This closes both the TOCTOU window between the lockless
> xa_load() and the guard acquisition, and the window between the two
> sequential guards in the RCH path where a concurrent surprise removal
> could free dport before devm_cxl_add_endpoint() dereferences it.
> 
> Reported-by: Sashiko
> Fixes: 391785859e7e ("cxl/port: Move dport tracking to an xarray")
> Link: https://lore.kernel.org/linux-cxl/20260505173029.2718246-1-terry.bowman@amd.com/
> Signed-off-by: Terry Bowman <terry.bowman@amd.com>
> Reviewed-by: Ben Cheatham <Benjamin.Cheatham@amd.com>
> ---
>  drivers/cxl/core/port.c | 10 +++++++
>  drivers/cxl/mem.c       | 65 +++++++++++++++++++++++++++++++----------
>  drivers/cxl/pci.c       | 17 +++++++----
>  3 files changed, 72 insertions(+), 20 deletions(-)
> 
> diff --git a/drivers/cxl/core/port.c b/drivers/cxl/core/port.c
> index c5aacd7054f1..0b8f144596e8 100644
> --- a/drivers/cxl/core/port.c
> +++ b/drivers/cxl/core/port.c
> @@ -1092,8 +1092,18 @@ static void cxl_dport_remove(void *data)
>  	struct cxl_dport *dport = data;
>  	struct cxl_port *port = dport->port;
>  
> +	/*
> +	 * For non-root ports the port lock is already held by the caller
> +	 * via devres_release_all() during driver unbind, which holds
> +	 * port->dev.mutex throughout.  Acquiring it again unconditionally
> +	 * would deadlock.  Use cond_cxl_root_lock() which only acquires
> +	 * when the port is a root port and the lock is therefore not yet
> +	 * held.
> +	 */
> +	cond_cxl_root_lock(port);
>  	port->nr_dports--;
>  	xa_erase(&port->dports, (unsigned long) dport->dport_dev);
> +	cond_cxl_root_unlock(port);
>  	put_device(dport->dport_dev);
>  }
>  
> diff --git a/drivers/cxl/mem.c b/drivers/cxl/mem.c
> index fcffe24dcb42..345b56f215ff 100644
> --- a/drivers/cxl/mem.c
> +++ b/drivers/cxl/mem.c
> @@ -70,9 +70,9 @@ static int cxl_mem_probe(struct device *dev)
>  	struct cxl_memdev *cxlmd = to_cxl_memdev(dev);
>  	struct cxl_memdev_state *mds = to_cxl_memdev_state(cxlmd->cxlds);
>  	struct cxl_dev_state *cxlds = cxlmd->cxlds;
> -	struct device *endpoint_parent;
>  	struct cxl_dport *dport;
>  	struct dentry *dentry;
> +	bool rch = false;
>  	int rc;
>  
>  	if (!cxlds->media_ready)
> @@ -107,8 +107,7 @@ static int cxl_mem_probe(struct device *dev)
>  	if (rc)
>  		return rc;
>  
> -	struct cxl_port *parent_port __free(put_cxl_port) =
> -		cxl_mem_find_port(cxlmd, &dport);
> +	struct cxl_port *parent_port __free(put_cxl_port) = cxl_mem_find_port(cxlmd, NULL);
>  	if (!parent_port) {
>  		dev_err(dev, "CXL port topology not found\n");
>  		return -ENXIO;
> @@ -123,21 +122,57 @@ static int cxl_mem_probe(struct device *dev)
>  		}
>  	}
>  
> -	if (dport->rch)
> -		endpoint_parent = parent_port->uport_dev;
> -	else
> -		endpoint_parent = &parent_port->dev;
> -
> -	scoped_guard(device, endpoint_parent) {
> -		if (!endpoint_parent->driver) {
> -			dev_err(dev, "CXL port topology %s not enabled\n",
> -				dev_name(endpoint_parent));
> +	scoped_guard(device, &parent_port->dev) {
> +		/*
> +		 * Re-fetch dport under the port lock to close the TOCTOU
> +		 * window between cxl_mem_find_port()'s lockless xa_load() and
> +		 * this guard acquisition.  A concurrent surprise removal can
> +		 * free the dport in that window.
> +		 */
> +		dport = cxl_find_dport_by_dev(parent_port, cxlmd->dev.parent->parent);
> +		if (!dport) {
> +			dev_err(dev, "CXL port topology %s not found\n",
> +				dev_name(&parent_port->dev));
>  			return -ENXIO;
>  		}
> +		rch = dport->rch;
> +
> +		if (!rch) {
> +			if (!parent_port->dev.driver) {
> +				dev_err(dev, "CXL port topology %s not enabled\n",
> +					dev_name(&parent_port->dev));
> +				return -ENXIO;
> +			}
> +			rc = devm_cxl_add_endpoint(&parent_port->dev, cxlmd, dport);
> +			if (rc)
> +				return rc;
> +		}
> +	}
>  
> -		rc = devm_cxl_add_endpoint(endpoint_parent, cxlmd, dport);
> -		if (rc)
> -			return rc;
> +	if (rch) {
> +		struct device *uport_dev = parent_port->uport_dev;
> +
> +		scoped_guard(device, uport_dev) {
> +			if (!uport_dev->driver) {
> +				dev_err(dev, "CXL port topology %s not enabled\n",
> +					dev_name(uport_dev));
> +				return -ENXIO;
> +			}
> +			/*
> +			 * Re-fetch dport under uport_dev lock.  uport_dev->mutex
> +			 * is held for the full devres teardown sequence including
> +			 * free_dport()/kfree(), so this excludes concurrent
> +			 * hotplug removal through the entire dereference.
> +			 */
> +			dport = cxl_find_dport_by_dev(parent_port, cxlmd->dev.parent->parent);
> +			if (!dport) {
> +				dev_err(dev, "CXL RCH dport not found\n");
> +				return -ENXIO;
> +			}
> +			rc = devm_cxl_add_endpoint(uport_dev, cxlmd, dport);
> +			if (rc)
> +				return rc;
> +		}

Still reviewing the patch, but thoughts on moving the two new big blocks to a helper function?

DJ

>  	}
>  
>  	if (cxlmd->attach) {
> diff --git a/drivers/cxl/pci.c b/drivers/cxl/pci.c
> index bace662dc988..710a62a66429 100644
> --- a/drivers/cxl/pci.c
> +++ b/drivers/cxl/pci.c
> @@ -708,10 +708,10 @@ static ssize_t rcd_pcie_cap_emit(struct device *dev, u16 offset, char *buf, size
>  {
>  	struct cxl_dev_state *cxlds = dev_get_drvdata(dev);
>  	struct cxl_memdev *cxlmd = cxlds->cxlmd;
> -	struct device *root_dev;
>  	struct cxl_dport *dport;
> +	struct device *root_dev;
>  	struct cxl_port *root __free(put_cxl_port) =
> -		cxl_mem_find_port(cxlmd, &dport);
> +		cxl_mem_find_port(cxlmd, NULL);
>  
>  	if (!root)
>  		return -ENXIO;
> @@ -720,13 +720,20 @@ static ssize_t rcd_pcie_cap_emit(struct device *dev, u16 offset, char *buf, size
>  	if (!root_dev)
>  		return -ENXIO;
>  
> -	if (!dport->regs.rcd_pcie_cap)
> -		return -ENXIO;
> -
>  	guard(device)(root_dev);
>  	if (!root_dev->driver)
>  		return -ENXIO;
>  
> +	/*
> +	 * Fetch dport under uport_dev lock to protect against concurrent
> +	 * hotplug removal. uport_dev->mutex is held for the entire devres
> +	 * teardown sequence including free_dport(), so holding it here
> +	 * excludes concurrent kfree(dport).
> +	 */
> +	dport = cxl_find_dport_by_dev(root, cxlmd->dev.parent->parent);
> +	if (!dport || !dport->regs.rcd_pcie_cap)
> +		return -ENXIO;
> +
>  	switch (width) {
>  	case 2:
>  		return sysfs_emit(buf, "%#x\n",


^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [PATCH v2] cxl/port: Fix missing port lock in cxl_dport_remove()
  2026-06-09  0:35 ` Dave Jiang
@ 2026-06-09  7:40   ` Richard Cheng
  2026-06-09 15:12     ` Bowman, Terry
  2026-06-09 14:22   ` Bowman, Terry
  1 sibling, 1 reply; 6+ messages in thread
From: Richard Cheng @ 2026-06-09  7:40 UTC (permalink / raw)
  To: Dave Jiang
  Cc: Terry Bowman, dave, jic23, alison.schofield, vishal.l.verma,
	ira.weiny, djbw, ming.li, rrichter, Benjamin.Cheatham,
	Smita.KoralahalliChannabasappa, stable, linux-cxl, linux-kernel,
	PradeepVineshReddy.Kodamati

On Mon, Jun 08, 2026 at 05:35:23PM +0800, Dave Jiang wrote:
> 
> 
> On 6/8/26 3:35 PM, Terry Bowman wrote:
> > xa_erase() in cxl_dport_remove() runs without the port device lock,
> > creating a race with any caller that does xa_load() on port->dports
> > and then dereferences the returned dport pointer. A concurrent
> > cxl_dport_remove() can erase and free the dport between the xa_load()
> > and the caller acquiring the port lock, causing a use-after-free.
> > 
> > For non-root ports the port lock is already held by the caller on two
> > paths:
> > 
> > 1. Driver unbind: devres_release_all() is called from
> >    __device_release_driver() which holds port->dev.mutex.
> > 
> > 2. Dynamic endpoint removal: cxl_detach_ep() takes the port lock
> >    before calling del_dports() -> del_dport() -> devres_release_group(),
> >    which synchronously runs cxl_dport_remove().
> > 
> > Use cond_cxl_root_lock/unlock(), which only acquires the port lock when
> > the port is a root port and the lock is therefore not already held.
> > This matches the pattern used in __devm_cxl_add_dport() for the same
> > reason.
> > 
> > The write-side fix to cxl_dport_remove() is necessary but not
> > sufficient. Callers that obtain a dport pointer via cxl_mem_find_port()
> > use a lockless xa_load() and must not dereference that pointer until a
> > lock that excludes free_dport()/kfree() is held.
> >

Hi Terry,

I think the mechanism is right, cond_cxl_root_lock() in cxl_dport_remove()
is a no-op for non-root ports and a real qcquire only for root dports, so
no deadlock.

But this only cover the 2 cxl_mem_find_port() callers. The sibling
cxl_pci_find_port() has similar lockless xa_load() plus deref, and those
callers aren't fied.

Could you either extend the same fix in this series?
I'm happy to send a follow-up for other readers if that's easier for you.

> > For root ports, dport_to_host() returns uport_dev, so all three devres
> > actions (free_dport, cxl_dport_remove, cxl_dport_unlink) are registered
> > on uport_dev. __device_release_driver() holds uport_dev->mutex for the
> > full teardown sequence including kfree(dport). Holding uport_dev->mutex
> > on the read side therefore excludes concurrent dport freeing.
> > 
> > Fix rcd_pcie_cap_emit() by passing NULL to cxl_mem_find_port() to avoid
> > capturing a lockless dport pointer, then re-fetching dport inside the
> > uport_dev guard via cxl_find_dport_by_dev(). The previous guard on
> > root->dev was wrong: cxl_dport_remove() releases root->dev before
> > free_dport() runs, so root->dev does not protect against concurrent
> > kfree(dport).
> > 
> > Fix cxl_mem_probe() similarly: pass NULL to cxl_mem_find_port(), then
> > re-fetch dport inside scoped_guard(device, &parent_port->dev) for the
> > VH path, and re-fetch again inside scoped_guard(device, uport_dev) for
> > the RCH path. This closes both the TOCTOU window between the lockless
> > xa_load() and the guard acquisition, and the window between the two
> > sequential guards in the RCH path where a concurrent surprise removal
> > could free dport before devm_cxl_add_endpoint() dereferences it.
> > 
> > Reported-by: Sashiko
> > Fixes: 391785859e7e ("cxl/port: Move dport tracking to an xarray")
> > Link: https://lore.kernel.org/linux-cxl/20260505173029.2718246-1-terry.bowman@amd.com/
> > Signed-off-by: Terry Bowman <terry.bowman@amd.com>
> > Reviewed-by: Ben Cheatham <Benjamin.Cheatham@amd.com>
> > ---
> >  drivers/cxl/core/port.c | 10 +++++++
> >  drivers/cxl/mem.c       | 65 +++++++++++++++++++++++++++++++----------
> >  drivers/cxl/pci.c       | 17 +++++++----
> >  3 files changed, 72 insertions(+), 20 deletions(-)
> > 
> > diff --git a/drivers/cxl/core/port.c b/drivers/cxl/core/port.c
> > index c5aacd7054f1..0b8f144596e8 100644
> > --- a/drivers/cxl/core/port.c
> > +++ b/drivers/cxl/core/port.c
> > @@ -1092,8 +1092,18 @@ static void cxl_dport_remove(void *data)
> >  	struct cxl_dport *dport = data;
> >  	struct cxl_port *port = dport->port;
> >  
> > +	/*
> > +	 * For non-root ports the port lock is already held by the caller
> > +	 * via devres_release_all() during driver unbind, which holds
> > +	 * port->dev.mutex throughout.  Acquiring it again unconditionally
> > +	 * would deadlock.  Use cond_cxl_root_lock() which only acquires
> > +	 * when the port is a root port and the lock is therefore not yet
> > +	 * held.
> > +	 */
> > +	cond_cxl_root_lock(port);
> >  	port->nr_dports--;
> >  	xa_erase(&port->dports, (unsigned long) dport->dport_dev);
> > +	cond_cxl_root_unlock(port);

In the comment above, maybe worth adding some contents about this is
also what makes the RCH reads safe. It's no obvious for me.

Best regards,
Richard Cheng.

> >  	put_device(dport->dport_dev);
> >  }
> >  
> > diff --git a/drivers/cxl/mem.c b/drivers/cxl/mem.c
> > index fcffe24dcb42..345b56f215ff 100644
> > --- a/drivers/cxl/mem.c
> > +++ b/drivers/cxl/mem.c
> > @@ -70,9 +70,9 @@ static int cxl_mem_probe(struct device *dev)
> >  	struct cxl_memdev *cxlmd = to_cxl_memdev(dev);
> >  	struct cxl_memdev_state *mds = to_cxl_memdev_state(cxlmd->cxlds);
> >  	struct cxl_dev_state *cxlds = cxlmd->cxlds;
> > -	struct device *endpoint_parent;
> >  	struct cxl_dport *dport;
> >  	struct dentry *dentry;
> > +	bool rch = false;
> >  	int rc;
> >  
> >  	if (!cxlds->media_ready)
> > @@ -107,8 +107,7 @@ static int cxl_mem_probe(struct device *dev)
> >  	if (rc)
> >  		return rc;
> >  
> > -	struct cxl_port *parent_port __free(put_cxl_port) =
> > -		cxl_mem_find_port(cxlmd, &dport);
> > +	struct cxl_port *parent_port __free(put_cxl_port) = cxl_mem_find_port(cxlmd, NULL);
> >  	if (!parent_port) {
> >  		dev_err(dev, "CXL port topology not found\n");
> >  		return -ENXIO;
> > @@ -123,21 +122,57 @@ static int cxl_mem_probe(struct device *dev)
> >  		}
> >  	}
> >  
> > -	if (dport->rch)
> > -		endpoint_parent = parent_port->uport_dev;
> > -	else
> > -		endpoint_parent = &parent_port->dev;
> > -
> > -	scoped_guard(device, endpoint_parent) {
> > -		if (!endpoint_parent->driver) {
> > -			dev_err(dev, "CXL port topology %s not enabled\n",
> > -				dev_name(endpoint_parent));
> > +	scoped_guard(device, &parent_port->dev) {
> > +		/*
> > +		 * Re-fetch dport under the port lock to close the TOCTOU
> > +		 * window between cxl_mem_find_port()'s lockless xa_load() and
> > +		 * this guard acquisition.  A concurrent surprise removal can
> > +		 * free the dport in that window.
> > +		 */
> > +		dport = cxl_find_dport_by_dev(parent_port, cxlmd->dev.parent->parent);
> > +		if (!dport) {
> > +			dev_err(dev, "CXL port topology %s not found\n",
> > +				dev_name(&parent_port->dev));
> >  			return -ENXIO;
> >  		}
> > +		rch = dport->rch;
> > +
> > +		if (!rch) {
> > +			if (!parent_port->dev.driver) {
> > +				dev_err(dev, "CXL port topology %s not enabled\n",
> > +					dev_name(&parent_port->dev));
> > +				return -ENXIO;
> > +			}
> > +			rc = devm_cxl_add_endpoint(&parent_port->dev, cxlmd, dport);
> > +			if (rc)
> > +				return rc;
> > +		}
> > +	}
> >  
> > -		rc = devm_cxl_add_endpoint(endpoint_parent, cxlmd, dport);
> > -		if (rc)
> > -			return rc;
> > +	if (rch) {
> > +		struct device *uport_dev = parent_port->uport_dev;
> > +
> > +		scoped_guard(device, uport_dev) {
> > +			if (!uport_dev->driver) {
> > +				dev_err(dev, "CXL port topology %s not enabled\n",
> > +					dev_name(uport_dev));
> > +				return -ENXIO;
> > +			}
> > +			/*
> > +			 * Re-fetch dport under uport_dev lock.  uport_dev->mutex
> > +			 * is held for the full devres teardown sequence including
> > +			 * free_dport()/kfree(), so this excludes concurrent
> > +			 * hotplug removal through the entire dereference.
> > +			 */
> > +			dport = cxl_find_dport_by_dev(parent_port, cxlmd->dev.parent->parent);
> > +			if (!dport) {
> > +				dev_err(dev, "CXL RCH dport not found\n");
> > +				return -ENXIO;
> > +			}
> > +			rc = devm_cxl_add_endpoint(uport_dev, cxlmd, dport);
> > +			if (rc)
> > +				return rc;
> > +		}
> 
> Still reviewing the patch, but thoughts on moving the two new big blocks to a helper function?
> 
> DJ
> 
> >  	}
> >  
> >  	if (cxlmd->attach) {
> > diff --git a/drivers/cxl/pci.c b/drivers/cxl/pci.c
> > index bace662dc988..710a62a66429 100644
> > --- a/drivers/cxl/pci.c
> > +++ b/drivers/cxl/pci.c
> > @@ -708,10 +708,10 @@ static ssize_t rcd_pcie_cap_emit(struct device *dev, u16 offset, char *buf, size
> >  {
> >  	struct cxl_dev_state *cxlds = dev_get_drvdata(dev);
> >  	struct cxl_memdev *cxlmd = cxlds->cxlmd;
> > -	struct device *root_dev;
> >  	struct cxl_dport *dport;
> > +	struct device *root_dev;
> >  	struct cxl_port *root __free(put_cxl_port) =
> > -		cxl_mem_find_port(cxlmd, &dport);
> > +		cxl_mem_find_port(cxlmd, NULL);
> >  
> >  	if (!root)
> >  		return -ENXIO;
> > @@ -720,13 +720,20 @@ static ssize_t rcd_pcie_cap_emit(struct device *dev, u16 offset, char *buf, size
> >  	if (!root_dev)
> >  		return -ENXIO;
> >  
> > -	if (!dport->regs.rcd_pcie_cap)
> > -		return -ENXIO;
> > -
> >  	guard(device)(root_dev);
> >  	if (!root_dev->driver)
> >  		return -ENXIO;
> >  
> > +	/*
> > +	 * Fetch dport under uport_dev lock to protect against concurrent
> > +	 * hotplug removal. uport_dev->mutex is held for the entire devres
> > +	 * teardown sequence including free_dport(), so holding it here
> > +	 * excludes concurrent kfree(dport).
> > +	 */
> > +	dport = cxl_find_dport_by_dev(root, cxlmd->dev.parent->parent);
> > +	if (!dport || !dport->regs.rcd_pcie_cap)
> > +		return -ENXIO;
> > +
> >  	switch (width) {
> >  	case 2:
> >  		return sysfs_emit(buf, "%#x\n",
> 
> 

^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [PATCH v2] cxl/port: Fix missing port lock in cxl_dport_remove()
  2026-06-09  0:35 ` Dave Jiang
  2026-06-09  7:40   ` Richard Cheng
@ 2026-06-09 14:22   ` Bowman, Terry
  1 sibling, 0 replies; 6+ messages in thread
From: Bowman, Terry @ 2026-06-09 14:22 UTC (permalink / raw)
  To: Dave Jiang, dave, jic23, alison.schofield, vishal.l.verma,
	ira.weiny, djbw, ming.li, rrichter, Benjamin.Cheatham
  Cc: Smita.KoralahalliChannabasappa, stable, linux-cxl, linux-kernel,
	PradeepVineshReddy.Kodamati

On 6/8/2026 7:35 PM, Dave Jiang wrote:
> 
> 
> On 6/8/26 3:35 PM, Terry Bowman wrote:
>> xa_erase() in cxl_dport_remove() runs without the port device lock,
>> creating a race with any caller that does xa_load() on port->dports
>> and then dereferences the returned dport pointer. A concurrent
>> cxl_dport_remove() can erase and free the dport between the xa_load()
>> and the caller acquiring the port lock, causing a use-after-free.
>>
>> For non-root ports the port lock is already held by the caller on two
>> paths:
>>
>> 1. Driver unbind: devres_release_all() is called from
>>    __device_release_driver() which holds port->dev.mutex.
>>
>> 2. Dynamic endpoint removal: cxl_detach_ep() takes the port lock
>>    before calling del_dports() -> del_dport() -> devres_release_group(),
>>    which synchronously runs cxl_dport_remove().
>>
>> Use cond_cxl_root_lock/unlock(), which only acquires the port lock when
>> the port is a root port and the lock is therefore not already held.
>> This matches the pattern used in __devm_cxl_add_dport() for the same
>> reason.
>>
>> The write-side fix to cxl_dport_remove() is necessary but not
>> sufficient. Callers that obtain a dport pointer via cxl_mem_find_port()
>> use a lockless xa_load() and must not dereference that pointer until a
>> lock that excludes free_dport()/kfree() is held.
>>
>> For root ports, dport_to_host() returns uport_dev, so all three devres
>> actions (free_dport, cxl_dport_remove, cxl_dport_unlink) are registered
>> on uport_dev. __device_release_driver() holds uport_dev->mutex for the
>> full teardown sequence including kfree(dport). Holding uport_dev->mutex
>> on the read side therefore excludes concurrent dport freeing.
>>
>> Fix rcd_pcie_cap_emit() by passing NULL to cxl_mem_find_port() to avoid
>> capturing a lockless dport pointer, then re-fetching dport inside the
>> uport_dev guard via cxl_find_dport_by_dev(). The previous guard on
>> root->dev was wrong: cxl_dport_remove() releases root->dev before
>> free_dport() runs, so root->dev does not protect against concurrent
>> kfree(dport).
>>
>> Fix cxl_mem_probe() similarly: pass NULL to cxl_mem_find_port(), then
>> re-fetch dport inside scoped_guard(device, &parent_port->dev) for the
>> VH path, and re-fetch again inside scoped_guard(device, uport_dev) for
>> the RCH path. This closes both the TOCTOU window between the lockless
>> xa_load() and the guard acquisition, and the window between the two
>> sequential guards in the RCH path where a concurrent surprise removal
>> could free dport before devm_cxl_add_endpoint() dereferences it.
>>
>> Reported-by: Sashiko
>> Fixes: 391785859e7e ("cxl/port: Move dport tracking to an xarray")
>> Link: https://lore.kernel.org/linux-cxl/20260505173029.2718246-1-terry.bowman@amd.com/
>> Signed-off-by: Terry Bowman <terry.bowman@amd.com>
>> Reviewed-by: Ben Cheatham <Benjamin.Cheatham@amd.com>
>> ---
>>  drivers/cxl/core/port.c | 10 +++++++
>>  drivers/cxl/mem.c       | 65 +++++++++++++++++++++++++++++++----------
>>  drivers/cxl/pci.c       | 17 +++++++----
>>  3 files changed, 72 insertions(+), 20 deletions(-)
>>
>> diff --git a/drivers/cxl/core/port.c b/drivers/cxl/core/port.c
>> index c5aacd7054f1..0b8f144596e8 100644
>> --- a/drivers/cxl/core/port.c
>> +++ b/drivers/cxl/core/port.c
>> @@ -1092,8 +1092,18 @@ static void cxl_dport_remove(void *data)
>>  	struct cxl_dport *dport = data;
>>  	struct cxl_port *port = dport->port;
>>  
>> +	/*
>> +	 * For non-root ports the port lock is already held by the caller
>> +	 * via devres_release_all() during driver unbind, which holds
>> +	 * port->dev.mutex throughout.  Acquiring it again unconditionally
>> +	 * would deadlock.  Use cond_cxl_root_lock() which only acquires
>> +	 * when the port is a root port and the lock is therefore not yet
>> +	 * held.
>> +	 */
>> +	cond_cxl_root_lock(port);
>>  	port->nr_dports--;
>>  	xa_erase(&port->dports, (unsigned long) dport->dport_dev);
>> +	cond_cxl_root_unlock(port);
>>  	put_device(dport->dport_dev);
>>  }
>>  
>> diff --git a/drivers/cxl/mem.c b/drivers/cxl/mem.c
>> index fcffe24dcb42..345b56f215ff 100644
>> --- a/drivers/cxl/mem.c
>> +++ b/drivers/cxl/mem.c
>> @@ -70,9 +70,9 @@ static int cxl_mem_probe(struct device *dev)
>>  	struct cxl_memdev *cxlmd = to_cxl_memdev(dev);
>>  	struct cxl_memdev_state *mds = to_cxl_memdev_state(cxlmd->cxlds);
>>  	struct cxl_dev_state *cxlds = cxlmd->cxlds;
>> -	struct device *endpoint_parent;
>>  	struct cxl_dport *dport;
>>  	struct dentry *dentry;
>> +	bool rch = false;
>>  	int rc;
>>  
>>  	if (!cxlds->media_ready)
>> @@ -107,8 +107,7 @@ static int cxl_mem_probe(struct device *dev)
>>  	if (rc)
>>  		return rc;
>>  
>> -	struct cxl_port *parent_port __free(put_cxl_port) =
>> -		cxl_mem_find_port(cxlmd, &dport);
>> +	struct cxl_port *parent_port __free(put_cxl_port) = cxl_mem_find_port(cxlmd, NULL);
>>  	if (!parent_port) {
>>  		dev_err(dev, "CXL port topology not found\n");
>>  		return -ENXIO;
>> @@ -123,21 +122,57 @@ static int cxl_mem_probe(struct device *dev)
>>  		}
>>  	}
>>  
>> -	if (dport->rch)
>> -		endpoint_parent = parent_port->uport_dev;
>> -	else
>> -		endpoint_parent = &parent_port->dev;
>> -
>> -	scoped_guard(device, endpoint_parent) {
>> -		if (!endpoint_parent->driver) {
>> -			dev_err(dev, "CXL port topology %s not enabled\n",
>> -				dev_name(endpoint_parent));
>> +	scoped_guard(device, &parent_port->dev) {
>> +		/*
>> +		 * Re-fetch dport under the port lock to close the TOCTOU
>> +		 * window between cxl_mem_find_port()'s lockless xa_load() and
>> +		 * this guard acquisition.  A concurrent surprise removal can
>> +		 * free the dport in that window.
>> +		 */
>> +		dport = cxl_find_dport_by_dev(parent_port, cxlmd->dev.parent->parent);
>> +		if (!dport) {
>> +			dev_err(dev, "CXL port topology %s not found\n",
>> +				dev_name(&parent_port->dev));
>>  			return -ENXIO;
>>  		}
>> +		rch = dport->rch;
>> +
>> +		if (!rch) {
>> +			if (!parent_port->dev.driver) {
>> +				dev_err(dev, "CXL port topology %s not enabled\n",
>> +					dev_name(&parent_port->dev));
>> +				return -ENXIO;
>> +			}
>> +			rc = devm_cxl_add_endpoint(&parent_port->dev, cxlmd, dport);
>> +			if (rc)
>> +				return rc;
>> +		}
>> +	}
>>  
>> -		rc = devm_cxl_add_endpoint(endpoint_parent, cxlmd, dport);
>> -		if (rc)
>> -			return rc;
>> +	if (rch) {
>> +		struct device *uport_dev = parent_port->uport_dev;
>> +
>> +		scoped_guard(device, uport_dev) {
>> +			if (!uport_dev->driver) {
>> +				dev_err(dev, "CXL port topology %s not enabled\n",
>> +					dev_name(uport_dev));
>> +				return -ENXIO;
>> +			}
>> +			/*
>> +			 * Re-fetch dport under uport_dev lock.  uport_dev->mutex
>> +			 * is held for the full devres teardown sequence including
>> +			 * free_dport()/kfree(), so this excludes concurrent
>> +			 * hotplug removal through the entire dereference.
>> +			 */
>> +			dport = cxl_find_dport_by_dev(parent_port, cxlmd->dev.parent->parent);
>> +			if (!dport) {
>> +				dev_err(dev, "CXL RCH dport not found\n");
>> +				return -ENXIO;
>> +			}
>> +			rc = devm_cxl_add_endpoint(uport_dev, cxlmd, dport);
>> +			if (rc)
>> +				return rc;
>> +		}
> 
> Still reviewing the patch, but thoughts on moving the two new big blocks to a helper function?
> 
> DJ
> 

Hi Dave,

Yes, good point. That needs to be done.

- Terry

^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [PATCH v2] cxl/port: Fix missing port lock in cxl_dport_remove()
  2026-06-09  7:40   ` Richard Cheng
@ 2026-06-09 15:12     ` Bowman, Terry
  2026-06-09 16:03       ` Dave Jiang
  0 siblings, 1 reply; 6+ messages in thread
From: Bowman, Terry @ 2026-06-09 15:12 UTC (permalink / raw)
  To: Richard Cheng, Dave Jiang
  Cc: dave, jic23, alison.schofield, vishal.l.verma, ira.weiny, djbw,
	ming.li, rrichter, Benjamin.Cheatham,
	Smita.KoralahalliChannabasappa, stable, linux-cxl, linux-kernel,
	PradeepVineshReddy.Kodamati

On 6/9/2026 2:40 AM, Richard Cheng wrote:
> On Mon, Jun 08, 2026 at 05:35:23PM +0800, Dave Jiang wrote:
>>
>>
>> On 6/8/26 3:35 PM, Terry Bowman wrote:
>>> xa_erase() in cxl_dport_remove() runs without the port device lock,
>>> creating a race with any caller that does xa_load() on port->dports
>>> and then dereferences the returned dport pointer. A concurrent
>>> cxl_dport_remove() can erase and free the dport between the xa_load()
>>> and the caller acquiring the port lock, causing a use-after-free.
>>>
>>> For non-root ports the port lock is already held by the caller on two
>>> paths:
>>>
>>> 1. Driver unbind: devres_release_all() is called from
>>>    __device_release_driver() which holds port->dev.mutex.
>>>
>>> 2. Dynamic endpoint removal: cxl_detach_ep() takes the port lock
>>>    before calling del_dports() -> del_dport() -> devres_release_group(),
>>>    which synchronously runs cxl_dport_remove().
>>>
>>> Use cond_cxl_root_lock/unlock(), which only acquires the port lock when
>>> the port is a root port and the lock is therefore not already held.
>>> This matches the pattern used in __devm_cxl_add_dport() for the same
>>> reason.
>>>
>>> The write-side fix to cxl_dport_remove() is necessary but not
>>> sufficient. Callers that obtain a dport pointer via cxl_mem_find_port()
>>> use a lockless xa_load() and must not dereference that pointer until a
>>> lock that excludes free_dport()/kfree() is held.
>>>
> 
> Hi Terry,
> 
> I think the mechanism is right, cond_cxl_root_lock() in cxl_dport_remove()
> is a no-op for non-root ports and a real qcquire only for root dports, so
> no deadlock.
> 
> But this only cover the 2 cxl_mem_find_port() callers. The sibling
> cxl_pci_find_port() has similar lockless xa_load() plus deref, and those
> callers aren't fied.
> 
> Could you either extend the same fix in this series?
> I'm happy to send a follow-up for other readers if that's easier for you.
> 

Youre right there are other callsites requiring similar changes. You're follow-up 
would helpful. It will allow me to return to the error handling series.
Thanks.

>>> For root ports, dport_to_host() returns uport_dev, so all three devres
>>> actions (free_dport, cxl_dport_remove, cxl_dport_unlink) are registered
>>> on uport_dev. __device_release_driver() holds uport_dev->mutex for the
>>> full teardown sequence including kfree(dport). Holding uport_dev->mutex
>>> on the read side therefore excludes concurrent dport freeing.
>>>
>>> Fix rcd_pcie_cap_emit() by passing NULL to cxl_mem_find_port() to avoid
>>> capturing a lockless dport pointer, then re-fetching dport inside the
>>> uport_dev guard via cxl_find_dport_by_dev(). The previous guard on
>>> root->dev was wrong: cxl_dport_remove() releases root->dev before
>>> free_dport() runs, so root->dev does not protect against concurrent
>>> kfree(dport).
>>>
>>> Fix cxl_mem_probe() similarly: pass NULL to cxl_mem_find_port(), then
>>> re-fetch dport inside scoped_guard(device, &parent_port->dev) for the
>>> VH path, and re-fetch again inside scoped_guard(device, uport_dev) for
>>> the RCH path. This closes both the TOCTOU window between the lockless
>>> xa_load() and the guard acquisition, and the window between the two
>>> sequential guards in the RCH path where a concurrent surprise removal
>>> could free dport before devm_cxl_add_endpoint() dereferences it.
>>>
>>> Reported-by: Sashiko
>>> Fixes: 391785859e7e ("cxl/port: Move dport tracking to an xarray")
>>> Link: https://lore.kernel.org/linux-cxl/20260505173029.2718246-1-terry.bowman@amd.com/
>>> Signed-off-by: Terry Bowman <terry.bowman@amd.com>
>>> Reviewed-by: Ben Cheatham <Benjamin.Cheatham@amd.com>
>>> ---
>>>  drivers/cxl/core/port.c | 10 +++++++
>>>  drivers/cxl/mem.c       | 65 +++++++++++++++++++++++++++++++----------
>>>  drivers/cxl/pci.c       | 17 +++++++----
>>>  3 files changed, 72 insertions(+), 20 deletions(-)
>>>
>>> diff --git a/drivers/cxl/core/port.c b/drivers/cxl/core/port.c
>>> index c5aacd7054f1..0b8f144596e8 100644
>>> --- a/drivers/cxl/core/port.c
>>> +++ b/drivers/cxl/core/port.c
>>> @@ -1092,8 +1092,18 @@ static void cxl_dport_remove(void *data)
>>>  	struct cxl_dport *dport = data;
>>>  	struct cxl_port *port = dport->port;
>>>  
>>> +	/*
>>> +	 * For non-root ports the port lock is already held by the caller
>>> +	 * via devres_release_all() during driver unbind, which holds
>>> +	 * port->dev.mutex throughout.  Acquiring it again unconditionally
>>> +	 * would deadlock.  Use cond_cxl_root_lock() which only acquires
>>> +	 * when the port is a root port and the lock is therefore not yet
>>> +	 * held.
>>> +	 */
>>> +	cond_cxl_root_lock(port);
>>>  	port->nr_dports--;
>>>  	xa_erase(&port->dports, (unsigned long) dport->dport_dev);
>>> +	cond_cxl_root_unlock(port);
> 
> In the comment above, maybe worth adding some contents about this is
> also what makes the RCH reads safe. It's no obvious for me.
> 
> Best regards,
> Richard Cheng.
> 

For RCH/RCD case, the cond_cxl_root_lock() only synchronizes accesses to the 
xarray. The RP in the RCH/RCD context is a SW construct. We need to hold the 
port->uport_dev->mutex to prevent kfree().

- Terry


>>>  	put_device(dport->dport_dev);
>>>  }
>>>  
>>> diff --git a/drivers/cxl/mem.c b/drivers/cxl/mem.c
>>> index fcffe24dcb42..345b56f215ff 100644
>>> --- a/drivers/cxl/mem.c
>>> +++ b/drivers/cxl/mem.c
>>> @@ -70,9 +70,9 @@ static int cxl_mem_probe(struct device *dev)
>>>  	struct cxl_memdev *cxlmd = to_cxl_memdev(dev);
>>>  	struct cxl_memdev_state *mds = to_cxl_memdev_state(cxlmd->cxlds);
>>>  	struct cxl_dev_state *cxlds = cxlmd->cxlds;
>>> -	struct device *endpoint_parent;
>>>  	struct cxl_dport *dport;
>>>  	struct dentry *dentry;
>>> +	bool rch = false;
>>>  	int rc;
>>>  
>>>  	if (!cxlds->media_ready)
>>> @@ -107,8 +107,7 @@ static int cxl_mem_probe(struct device *dev)
>>>  	if (rc)
>>>  		return rc;
>>>  
>>> -	struct cxl_port *parent_port __free(put_cxl_port) =
>>> -		cxl_mem_find_port(cxlmd, &dport);
>>> +	struct cxl_port *parent_port __free(put_cxl_port) = cxl_mem_find_port(cxlmd, NULL);
>>>  	if (!parent_port) {
>>>  		dev_err(dev, "CXL port topology not found\n");
>>>  		return -ENXIO;
>>> @@ -123,21 +122,57 @@ static int cxl_mem_probe(struct device *dev)
>>>  		}
>>>  	}
>>>  
>>> -	if (dport->rch)
>>> -		endpoint_parent = parent_port->uport_dev;
>>> -	else
>>> -		endpoint_parent = &parent_port->dev;
>>> -
>>> -	scoped_guard(device, endpoint_parent) {
>>> -		if (!endpoint_parent->driver) {
>>> -			dev_err(dev, "CXL port topology %s not enabled\n",
>>> -				dev_name(endpoint_parent));
>>> +	scoped_guard(device, &parent_port->dev) {
>>> +		/*
>>> +		 * Re-fetch dport under the port lock to close the TOCTOU
>>> +		 * window between cxl_mem_find_port()'s lockless xa_load() and
>>> +		 * this guard acquisition.  A concurrent surprise removal can
>>> +		 * free the dport in that window.
>>> +		 */
>>> +		dport = cxl_find_dport_by_dev(parent_port, cxlmd->dev.parent->parent);
>>> +		if (!dport) {
>>> +			dev_err(dev, "CXL port topology %s not found\n",
>>> +				dev_name(&parent_port->dev));
>>>  			return -ENXIO;
>>>  		}
>>> +		rch = dport->rch;
>>> +
>>> +		if (!rch) {
>>> +			if (!parent_port->dev.driver) {
>>> +				dev_err(dev, "CXL port topology %s not enabled\n",
>>> +					dev_name(&parent_port->dev));
>>> +				return -ENXIO;
>>> +			}
>>> +			rc = devm_cxl_add_endpoint(&parent_port->dev, cxlmd, dport);
>>> +			if (rc)
>>> +				return rc;
>>> +		}
>>> +	}
>>>  
>>> -		rc = devm_cxl_add_endpoint(endpoint_parent, cxlmd, dport);
>>> -		if (rc)
>>> -			return rc;
>>> +	if (rch) {
>>> +		struct device *uport_dev = parent_port->uport_dev;
>>> +
>>> +		scoped_guard(device, uport_dev) {
>>> +			if (!uport_dev->driver) {
>>> +				dev_err(dev, "CXL port topology %s not enabled\n",
>>> +					dev_name(uport_dev));
>>> +				return -ENXIO;
>>> +			}
>>> +			/*
>>> +			 * Re-fetch dport under uport_dev lock.  uport_dev->mutex
>>> +			 * is held for the full devres teardown sequence including
>>> +			 * free_dport()/kfree(), so this excludes concurrent
>>> +			 * hotplug removal through the entire dereference.
>>> +			 */
>>> +			dport = cxl_find_dport_by_dev(parent_port, cxlmd->dev.parent->parent);
>>> +			if (!dport) {
>>> +				dev_err(dev, "CXL RCH dport not found\n");
>>> +				return -ENXIO;
>>> +			}
>>> +			rc = devm_cxl_add_endpoint(uport_dev, cxlmd, dport);
>>> +			if (rc)
>>> +				return rc;
>>> +		}
>>
>> Still reviewing the patch, but thoughts on moving the two new big blocks to a helper function?
>>
>> DJ
>>
>>>  	}
>>>  
>>>  	if (cxlmd->attach) {
>>> diff --git a/drivers/cxl/pci.c b/drivers/cxl/pci.c
>>> index bace662dc988..710a62a66429 100644
>>> --- a/drivers/cxl/pci.c
>>> +++ b/drivers/cxl/pci.c
>>> @@ -708,10 +708,10 @@ static ssize_t rcd_pcie_cap_emit(struct device *dev, u16 offset, char *buf, size
>>>  {
>>>  	struct cxl_dev_state *cxlds = dev_get_drvdata(dev);
>>>  	struct cxl_memdev *cxlmd = cxlds->cxlmd;
>>> -	struct device *root_dev;
>>>  	struct cxl_dport *dport;
>>> +	struct device *root_dev;
>>>  	struct cxl_port *root __free(put_cxl_port) =
>>> -		cxl_mem_find_port(cxlmd, &dport);
>>> +		cxl_mem_find_port(cxlmd, NULL);
>>>  
>>>  	if (!root)
>>>  		return -ENXIO;
>>> @@ -720,13 +720,20 @@ static ssize_t rcd_pcie_cap_emit(struct device *dev, u16 offset, char *buf, size
>>>  	if (!root_dev)
>>>  		return -ENXIO;
>>>  
>>> -	if (!dport->regs.rcd_pcie_cap)
>>> -		return -ENXIO;
>>> -
>>>  	guard(device)(root_dev);
>>>  	if (!root_dev->driver)
>>>  		return -ENXIO;
>>>  
>>> +	/*
>>> +	 * Fetch dport under uport_dev lock to protect against concurrent
>>> +	 * hotplug removal. uport_dev->mutex is held for the entire devres
>>> +	 * teardown sequence including free_dport(), so holding it here
>>> +	 * excludes concurrent kfree(dport).
>>> +	 */
>>> +	dport = cxl_find_dport_by_dev(root, cxlmd->dev.parent->parent);
>>> +	if (!dport || !dport->regs.rcd_pcie_cap)
>>> +		return -ENXIO;
>>> +
>>>  	switch (width) {
>>>  	case 2:
>>>  		return sysfs_emit(buf, "%#x\n",
>>
>>


^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [PATCH v2] cxl/port: Fix missing port lock in cxl_dport_remove()
  2026-06-09 15:12     ` Bowman, Terry
@ 2026-06-09 16:03       ` Dave Jiang
  0 siblings, 0 replies; 6+ messages in thread
From: Dave Jiang @ 2026-06-09 16:03 UTC (permalink / raw)
  To: Bowman, Terry, Richard Cheng
  Cc: dave, jic23, alison.schofield, vishal.l.verma, ira.weiny, djbw,
	ming.li, rrichter, Benjamin.Cheatham,
	Smita.KoralahalliChannabasappa, stable, linux-cxl, linux-kernel,
	PradeepVineshReddy.Kodamati



On 6/9/26 8:12 AM, Bowman, Terry wrote:
> On 6/9/2026 2:40 AM, Richard Cheng wrote:
>> On Mon, Jun 08, 2026 at 05:35:23PM +0800, Dave Jiang wrote:
>>>
>>>
>>> On 6/8/26 3:35 PM, Terry Bowman wrote:
>>>> xa_erase() in cxl_dport_remove() runs without the port device lock,
>>>> creating a race with any caller that does xa_load() on port->dports
>>>> and then dereferences the returned dport pointer. A concurrent
>>>> cxl_dport_remove() can erase and free the dport between the xa_load()
>>>> and the caller acquiring the port lock, causing a use-after-free.
>>>>
>>>> For non-root ports the port lock is already held by the caller on two
>>>> paths:
>>>>
>>>> 1. Driver unbind: devres_release_all() is called from
>>>>    __device_release_driver() which holds port->dev.mutex.
>>>>
>>>> 2. Dynamic endpoint removal: cxl_detach_ep() takes the port lock
>>>>    before calling del_dports() -> del_dport() -> devres_release_group(),
>>>>    which synchronously runs cxl_dport_remove().
>>>>
>>>> Use cond_cxl_root_lock/unlock(), which only acquires the port lock when
>>>> the port is a root port and the lock is therefore not already held.
>>>> This matches the pattern used in __devm_cxl_add_dport() for the same
>>>> reason.
>>>>
>>>> The write-side fix to cxl_dport_remove() is necessary but not
>>>> sufficient. Callers that obtain a dport pointer via cxl_mem_find_port()
>>>> use a lockless xa_load() and must not dereference that pointer until a
>>>> lock that excludes free_dport()/kfree() is held.
>>>>
>>
>> Hi Terry,
>>
>> I think the mechanism is right, cond_cxl_root_lock() in cxl_dport_remove()
>> is a no-op for non-root ports and a real qcquire only for root dports, so
>> no deadlock.
>>
>> But this only cover the 2 cxl_mem_find_port() callers. The sibling
>> cxl_pci_find_port() has similar lockless xa_load() plus deref, and those
>> callers aren't fied.
>>
>> Could you either extend the same fix in this series?
>> I'm happy to send a follow-up for other readers if that's easier for you.
>>
> 
> Youre right there are other callsites requiring similar changes. You're follow-up 
> would helpful. It will allow me to return to the error handling series.
> Thanks.

Richard,
Do you mind just bundle Terry's fix with your series and we can apply it all together? Thanks!

DJ

> 
>>>> For root ports, dport_to_host() returns uport_dev, so all three devres
>>>> actions (free_dport, cxl_dport_remove, cxl_dport_unlink) are registered
>>>> on uport_dev. __device_release_driver() holds uport_dev->mutex for the
>>>> full teardown sequence including kfree(dport). Holding uport_dev->mutex
>>>> on the read side therefore excludes concurrent dport freeing.
>>>>
>>>> Fix rcd_pcie_cap_emit() by passing NULL to cxl_mem_find_port() to avoid
>>>> capturing a lockless dport pointer, then re-fetching dport inside the
>>>> uport_dev guard via cxl_find_dport_by_dev(). The previous guard on
>>>> root->dev was wrong: cxl_dport_remove() releases root->dev before
>>>> free_dport() runs, so root->dev does not protect against concurrent
>>>> kfree(dport).
>>>>
>>>> Fix cxl_mem_probe() similarly: pass NULL to cxl_mem_find_port(), then
>>>> re-fetch dport inside scoped_guard(device, &parent_port->dev) for the
>>>> VH path, and re-fetch again inside scoped_guard(device, uport_dev) for
>>>> the RCH path. This closes both the TOCTOU window between the lockless
>>>> xa_load() and the guard acquisition, and the window between the two
>>>> sequential guards in the RCH path where a concurrent surprise removal
>>>> could free dport before devm_cxl_add_endpoint() dereferences it.
>>>>
>>>> Reported-by: Sashiko
>>>> Fixes: 391785859e7e ("cxl/port: Move dport tracking to an xarray")
>>>> Link: https://lore.kernel.org/linux-cxl/20260505173029.2718246-1-terry.bowman@amd.com/
>>>> Signed-off-by: Terry Bowman <terry.bowman@amd.com>
>>>> Reviewed-by: Ben Cheatham <Benjamin.Cheatham@amd.com>
>>>> ---
>>>>  drivers/cxl/core/port.c | 10 +++++++
>>>>  drivers/cxl/mem.c       | 65 +++++++++++++++++++++++++++++++----------
>>>>  drivers/cxl/pci.c       | 17 +++++++----
>>>>  3 files changed, 72 insertions(+), 20 deletions(-)
>>>>
>>>> diff --git a/drivers/cxl/core/port.c b/drivers/cxl/core/port.c
>>>> index c5aacd7054f1..0b8f144596e8 100644
>>>> --- a/drivers/cxl/core/port.c
>>>> +++ b/drivers/cxl/core/port.c
>>>> @@ -1092,8 +1092,18 @@ static void cxl_dport_remove(void *data)
>>>>  	struct cxl_dport *dport = data;
>>>>  	struct cxl_port *port = dport->port;
>>>>  
>>>> +	/*
>>>> +	 * For non-root ports the port lock is already held by the caller
>>>> +	 * via devres_release_all() during driver unbind, which holds
>>>> +	 * port->dev.mutex throughout.  Acquiring it again unconditionally
>>>> +	 * would deadlock.  Use cond_cxl_root_lock() which only acquires
>>>> +	 * when the port is a root port and the lock is therefore not yet
>>>> +	 * held.
>>>> +	 */
>>>> +	cond_cxl_root_lock(port);
>>>>  	port->nr_dports--;
>>>>  	xa_erase(&port->dports, (unsigned long) dport->dport_dev);
>>>> +	cond_cxl_root_unlock(port);
>>
>> In the comment above, maybe worth adding some contents about this is
>> also what makes the RCH reads safe. It's no obvious for me.
>>
>> Best regards,
>> Richard Cheng.
>>
> 
> For RCH/RCD case, the cond_cxl_root_lock() only synchronizes accesses to the 
> xarray. The RP in the RCH/RCD context is a SW construct. We need to hold the 
> port->uport_dev->mutex to prevent kfree().
> 
> - Terry
> 
> 
>>>>  	put_device(dport->dport_dev);
>>>>  }
>>>>  
>>>> diff --git a/drivers/cxl/mem.c b/drivers/cxl/mem.c
>>>> index fcffe24dcb42..345b56f215ff 100644
>>>> --- a/drivers/cxl/mem.c
>>>> +++ b/drivers/cxl/mem.c
>>>> @@ -70,9 +70,9 @@ static int cxl_mem_probe(struct device *dev)
>>>>  	struct cxl_memdev *cxlmd = to_cxl_memdev(dev);
>>>>  	struct cxl_memdev_state *mds = to_cxl_memdev_state(cxlmd->cxlds);
>>>>  	struct cxl_dev_state *cxlds = cxlmd->cxlds;
>>>> -	struct device *endpoint_parent;
>>>>  	struct cxl_dport *dport;
>>>>  	struct dentry *dentry;
>>>> +	bool rch = false;
>>>>  	int rc;
>>>>  
>>>>  	if (!cxlds->media_ready)
>>>> @@ -107,8 +107,7 @@ static int cxl_mem_probe(struct device *dev)
>>>>  	if (rc)
>>>>  		return rc;
>>>>  
>>>> -	struct cxl_port *parent_port __free(put_cxl_port) =
>>>> -		cxl_mem_find_port(cxlmd, &dport);
>>>> +	struct cxl_port *parent_port __free(put_cxl_port) = cxl_mem_find_port(cxlmd, NULL);
>>>>  	if (!parent_port) {
>>>>  		dev_err(dev, "CXL port topology not found\n");
>>>>  		return -ENXIO;
>>>> @@ -123,21 +122,57 @@ static int cxl_mem_probe(struct device *dev)
>>>>  		}
>>>>  	}
>>>>  
>>>> -	if (dport->rch)
>>>> -		endpoint_parent = parent_port->uport_dev;
>>>> -	else
>>>> -		endpoint_parent = &parent_port->dev;
>>>> -
>>>> -	scoped_guard(device, endpoint_parent) {
>>>> -		if (!endpoint_parent->driver) {
>>>> -			dev_err(dev, "CXL port topology %s not enabled\n",
>>>> -				dev_name(endpoint_parent));
>>>> +	scoped_guard(device, &parent_port->dev) {
>>>> +		/*
>>>> +		 * Re-fetch dport under the port lock to close the TOCTOU
>>>> +		 * window between cxl_mem_find_port()'s lockless xa_load() and
>>>> +		 * this guard acquisition.  A concurrent surprise removal can
>>>> +		 * free the dport in that window.
>>>> +		 */
>>>> +		dport = cxl_find_dport_by_dev(parent_port, cxlmd->dev.parent->parent);
>>>> +		if (!dport) {
>>>> +			dev_err(dev, "CXL port topology %s not found\n",
>>>> +				dev_name(&parent_port->dev));
>>>>  			return -ENXIO;
>>>>  		}
>>>> +		rch = dport->rch;
>>>> +
>>>> +		if (!rch) {
>>>> +			if (!parent_port->dev.driver) {
>>>> +				dev_err(dev, "CXL port topology %s not enabled\n",
>>>> +					dev_name(&parent_port->dev));
>>>> +				return -ENXIO;
>>>> +			}
>>>> +			rc = devm_cxl_add_endpoint(&parent_port->dev, cxlmd, dport);
>>>> +			if (rc)
>>>> +				return rc;
>>>> +		}
>>>> +	}
>>>>  
>>>> -		rc = devm_cxl_add_endpoint(endpoint_parent, cxlmd, dport);
>>>> -		if (rc)
>>>> -			return rc;
>>>> +	if (rch) {
>>>> +		struct device *uport_dev = parent_port->uport_dev;
>>>> +
>>>> +		scoped_guard(device, uport_dev) {
>>>> +			if (!uport_dev->driver) {
>>>> +				dev_err(dev, "CXL port topology %s not enabled\n",
>>>> +					dev_name(uport_dev));
>>>> +				return -ENXIO;
>>>> +			}
>>>> +			/*
>>>> +			 * Re-fetch dport under uport_dev lock.  uport_dev->mutex
>>>> +			 * is held for the full devres teardown sequence including
>>>> +			 * free_dport()/kfree(), so this excludes concurrent
>>>> +			 * hotplug removal through the entire dereference.
>>>> +			 */
>>>> +			dport = cxl_find_dport_by_dev(parent_port, cxlmd->dev.parent->parent);
>>>> +			if (!dport) {
>>>> +				dev_err(dev, "CXL RCH dport not found\n");
>>>> +				return -ENXIO;
>>>> +			}
>>>> +			rc = devm_cxl_add_endpoint(uport_dev, cxlmd, dport);
>>>> +			if (rc)
>>>> +				return rc;
>>>> +		}
>>>
>>> Still reviewing the patch, but thoughts on moving the two new big blocks to a helper function?
>>>
>>> DJ
>>>
>>>>  	}
>>>>  
>>>>  	if (cxlmd->attach) {
>>>> diff --git a/drivers/cxl/pci.c b/drivers/cxl/pci.c
>>>> index bace662dc988..710a62a66429 100644
>>>> --- a/drivers/cxl/pci.c
>>>> +++ b/drivers/cxl/pci.c
>>>> @@ -708,10 +708,10 @@ static ssize_t rcd_pcie_cap_emit(struct device *dev, u16 offset, char *buf, size
>>>>  {
>>>>  	struct cxl_dev_state *cxlds = dev_get_drvdata(dev);
>>>>  	struct cxl_memdev *cxlmd = cxlds->cxlmd;
>>>> -	struct device *root_dev;
>>>>  	struct cxl_dport *dport;
>>>> +	struct device *root_dev;
>>>>  	struct cxl_port *root __free(put_cxl_port) =
>>>> -		cxl_mem_find_port(cxlmd, &dport);
>>>> +		cxl_mem_find_port(cxlmd, NULL);
>>>>  
>>>>  	if (!root)
>>>>  		return -ENXIO;
>>>> @@ -720,13 +720,20 @@ static ssize_t rcd_pcie_cap_emit(struct device *dev, u16 offset, char *buf, size
>>>>  	if (!root_dev)
>>>>  		return -ENXIO;
>>>>  
>>>> -	if (!dport->regs.rcd_pcie_cap)
>>>> -		return -ENXIO;
>>>> -
>>>>  	guard(device)(root_dev);
>>>>  	if (!root_dev->driver)
>>>>  		return -ENXIO;
>>>>  
>>>> +	/*
>>>> +	 * Fetch dport under uport_dev lock to protect against concurrent
>>>> +	 * hotplug removal. uport_dev->mutex is held for the entire devres
>>>> +	 * teardown sequence including free_dport(), so holding it here
>>>> +	 * excludes concurrent kfree(dport).
>>>> +	 */
>>>> +	dport = cxl_find_dport_by_dev(root, cxlmd->dev.parent->parent);
>>>> +	if (!dport || !dport->regs.rcd_pcie_cap)
>>>> +		return -ENXIO;
>>>> +
>>>>  	switch (width) {
>>>>  	case 2:
>>>>  		return sysfs_emit(buf, "%#x\n",
>>>
>>>
> 


^ permalink raw reply	[flat|nested] 6+ messages in thread

end of thread, other threads:[~2026-06-09 16:03 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-06-08 22:35 [PATCH v2] cxl/port: Fix missing port lock in cxl_dport_remove() Terry Bowman
2026-06-09  0:35 ` Dave Jiang
2026-06-09  7:40   ` Richard Cheng
2026-06-09 15:12     ` Bowman, Terry
2026-06-09 16:03       ` Dave Jiang
2026-06-09 14:22   ` Bowman, Terry

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

Powered by JetHome