* [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 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
* 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
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