[PATCH v4 5/6] platform/x86/amd/hsmp: ACPI HSMP refcounted sockets and coordinated release

From: Muralidhara M K

Date: Wed Jul 08 2026 - 00:25:50 EST


The ACPI driver binds one platform device per socket but shares a single
socket array and a single /dev/hsmp misc device across them. Replace the
is_probed flag with state that tracks this shared ownership:

- miscdevice.this_device tells whether /dev/hsmp is registered, so the
misc device is registered on the first socket and torn down last. A
preceding change clears mdev.this_device on deregister so this gate
stays reliable across a re-probe.

- hsmp_acpi_sock_refs counts the sockets that have probed successfully.
It is guarded by hsmp_acpi_probe_mutex, so a plain counter is enough and
no atomic refcount is needed. The shared socket array is allocated with
kcalloc() on the first probe and freed by hsmp_acpi_sock_release() when
the count drops back to zero.

hsmp_acpi_sock_release() is the single teardown helper: it deregisters
/dev/hsmp if registered, unmaps any metric-table DRAM, destroys the
per-socket mutexes and frees the array. Both the remove path and the
probe-failure path call it once they are the last owner, so the teardown
lives in one place.

Both paths also clear this socket's dev, so a message issued after a
non-final unbind (or to a socket that failed to probe on a multi-socket
system, whose array stays alive and whose remove() is never called) cannot
reach the mailbox that devres is about to unmap.

Two lifetime fixes fall out of the array persisting across a non-final
unbind:
- hsmp_get_tbl_dram_base() iounmap()s any stale metric_tbl_addr before
remapping, so a rebind does not leak one mapping per cycle. It runs
during (re)probe before the metric sysfs attribute is exposed, so no
reader can be using the old mapping.

- /dev/hsmp is no longer parented to a per-socket device. Sockets can be
unbound individually and out of order, and the misc device outlives all
but the last of them, so parenting it to one socket's device would leave
a dangling parent. Register it unparented instead.

An upcoming change wires both teardown paths into a data-plane rwsem so
they are drained against the lock-free data plane, nested inside
hsmp_acpi_probe_mutex.

Signed-off-by: Muralidhara M K <muralidhara.mk@xxxxxxx>
---
drivers/platform/x86/amd/hsmp/acpi.c | 102 ++++++++++++++++++++++-----
drivers/platform/x86/amd/hsmp/hsmp.c | 21 +++++-
drivers/platform/x86/amd/hsmp/hsmp.h | 1 -
3 files changed, 106 insertions(+), 18 deletions(-)

diff --git a/drivers/platform/x86/amd/hsmp/acpi.c b/drivers/platform/x86/amd/hsmp/acpi.c
index 095289ddb7e7..d35465079eea 100644
--- a/drivers/platform/x86/amd/hsmp/acpi.c
+++ b/drivers/platform/x86/amd/hsmp/acpi.c
@@ -43,11 +43,20 @@
static struct hsmp_plat_device *hsmp_pdev;

/*
- * Serializes ACPI probe, remove and init_acpi() so concurrent socket probes
- * cannot race the is_probed handshake or the one-time socket-array allocation.
+ * Serializes ACPI probe, remove and init_acpi() and guards the shared socket
+ * state they manage: the one-time socket-array allocation, /dev/hsmp
+ * registration and hsmp_acpi_sock_refs. A plain counter is therefore enough
+ * for the refcount; no atomic is needed.
*/
static DEFINE_MUTEX(hsmp_acpi_probe_mutex);

+/*
+ * Number of ACPI socket platform devices that have probed successfully. The
+ * shared socket array is allocated on the first probe and freed once this
+ * drops back to zero.
+ */
+static unsigned int hsmp_acpi_sock_refs;
+
struct hsmp_sys_attr {
struct device_attribute dattr;
u32 msg_id;
@@ -614,6 +623,56 @@ static const struct acpi_device_id amd_hsmp_acpi_ids[] = {
};
MODULE_DEVICE_TABLE(acpi, amd_hsmp_acpi_ids);

+/*
+ * Tear down the shared ACPI socket state once the last socket is gone:
+ * deregister /dev/hsmp if it was registered, unmap any metric-table DRAM,
+ * destroy the per-socket mutexes and free the socket array.
+ *
+ * Called with hsmp_acpi_probe_mutex held, serializing it against a concurrent
+ * probe.
+ */
+static void hsmp_acpi_sock_release(void)
+{
+ lockdep_assert_held(&hsmp_acpi_probe_mutex);
+
+ if (!IS_ERR_OR_NULL(hsmp_pdev->mdev.this_device))
+ hsmp_misc_deregister();
+ hsmp_unmap_metric_tbls(hsmp_pdev);
+ hsmp_destroy_metric_read_locks(hsmp_pdev);
+ kfree(hsmp_pdev->sock);
+ hsmp_pdev->sock = NULL;
+ hsmp_pdev->num_sockets = 0;
+ hsmp_pdev->proto_ver = 0;
+}
+
+/**
+ * hsmp_acpi_probe_failure_cleanup() - Undo a failed ACPI socket probe.
+ * @dev: ACPI companion device whose probe failed.
+ *
+ * This device never incremented hsmp_acpi_sock_refs, so clear its sock->dev
+ * and, if it was the only socket in play, release the shared state.
+ *
+ * Clearing sock->dev matters on multi-socket systems: when a non-first socket
+ * fails, the array stays alive (owned by an already-probed socket) and
+ * remove() is never called for this device, yet devres unmaps its mailbox once
+ * probe() returns. Without clearing dev, a later message to this index would
+ * pass every gate in hsmp_send_message() and reach the unmapped mailbox.
+ *
+ * sock is NULL if probe failed before hsmp_parse_acpi_table() set the drvdata.
+ */
+static void hsmp_acpi_probe_failure_cleanup(struct device *dev)
+{
+ struct hsmp_socket *sock = dev_get_drvdata(dev);
+
+ lockdep_assert_held(&hsmp_acpi_probe_mutex);
+
+ if (sock)
+ sock->dev = NULL;
+
+ if (!hsmp_acpi_sock_refs)
+ hsmp_acpi_sock_release();
+}
+
static int hsmp_acpi_probe(struct platform_device *pdev)
{
int ret;
@@ -624,16 +683,16 @@ static int hsmp_acpi_probe(struct platform_device *pdev)

guard(mutex)(&hsmp_acpi_probe_mutex);

- if (!hsmp_pdev->is_probed) {
+ if (!hsmp_pdev->sock) {
hsmp_pdev->num_sockets = topology_max_packages();
if (!hsmp_pdev->num_sockets) {
dev_err(&pdev->dev, "No CPU sockets detected\n");
return -ENODEV;
}

- hsmp_pdev->sock = devm_kcalloc(&pdev->dev, hsmp_pdev->num_sockets,
- sizeof(*hsmp_pdev->sock),
- GFP_KERNEL);
+ hsmp_pdev->sock = kcalloc(hsmp_pdev->num_sockets,
+ sizeof(*hsmp_pdev->sock),
+ GFP_KERNEL);
if (!hsmp_pdev->sock)
return -ENOMEM;

@@ -643,35 +702,46 @@ static int hsmp_acpi_probe(struct platform_device *pdev)
ret = init_acpi(&pdev->dev);
if (ret) {
dev_err(&pdev->dev, "Failed to initialize HSMP interface.\n");
+ hsmp_acpi_probe_failure_cleanup(&pdev->dev);
return ret;
}

- if (!hsmp_pdev->is_probed) {
+ if (IS_ERR_OR_NULL(hsmp_pdev->mdev.this_device)) {
ret = hsmp_misc_register(&pdev->dev);
if (ret) {
dev_err(&pdev->dev, "Failed to register misc device\n");
+ hsmp_acpi_probe_failure_cleanup(&pdev->dev);
return ret;
}
- hsmp_pdev->is_probed = true;
- dev_dbg(&pdev->dev, "AMD HSMP ACPI is probed successfully\n");
+ dev_dbg(&pdev->dev, "AMD HSMP ACPI misc device registered\n");
}

+ hsmp_acpi_sock_refs++;
+
return 0;
}

static void hsmp_acpi_remove(struct platform_device *pdev)
{
+ struct hsmp_socket *sock = dev_get_drvdata(&pdev->dev);
+
+ /*
+ * Serialize the decrement and any release it triggers against a
+ * concurrent probe so the count cannot be revived from zero.
+ */
guard(mutex)(&hsmp_acpi_probe_mutex);

/*
- * We register only one misc_device even on multi-socket system.
- * So, deregister should happen only once.
+ * Clear this socket's dev so hsmp_send_message() rejects it before
+ * devres unmaps the mailbox. On a non-final unbind the socket array
+ * stays alive, so without this a later message to this index would
+ * reach an unmapped iomem region.
*/
- if (hsmp_pdev->is_probed) {
- hsmp_misc_deregister();
- hsmp_destroy_metric_read_locks(hsmp_pdev);
- hsmp_pdev->is_probed = false;
- }
+ sock->dev = NULL;
+
+ hsmp_acpi_sock_refs--;
+ if (!hsmp_acpi_sock_refs)
+ hsmp_acpi_sock_release();
}

static struct platform_driver amd_hsmp_driver = {
diff --git a/drivers/platform/x86/amd/hsmp/hsmp.c b/drivers/platform/x86/amd/hsmp/hsmp.c
index ccd19c7bf115..e47e86116f16 100644
--- a/drivers/platform/x86/amd/hsmp/hsmp.c
+++ b/drivers/platform/x86/amd/hsmp/hsmp.c
@@ -483,6 +483,18 @@ int hsmp_get_tbl_dram_base(u16 sock_ind)
dev_err(sock->dev, "Invalid DRAM address for metric table\n");
return -ENOMEM;
}
+ /*
+ * The ACPI socket array is shared across sockets and outlives a
+ * per-socket unbind, so metric_tbl_addr may hold a mapping from an
+ * earlier bind of this socket. Unmap it before remapping so an
+ * unbind/rebind cycle does not leak a metric-table mapping. This runs
+ * during probe before the metric sysfs attribute is exposed, so no
+ * reader can be using it.
+ */
+ if (sock->metric_tbl_addr) {
+ iounmap(sock->metric_tbl_addr);
+ sock->metric_tbl_addr = NULL;
+ }
sock->metric_tbl_addr = ioremap(dram_addr, sizeof(struct hsmp_metric_table));
if (!sock->metric_tbl_addr) {
dev_err(sock->dev, "Failed to ioremap metric table addr\n");
n
@@ -520,7 +532,14 @@ int hsmp_misc_register(struct device *dev)
hsmp_pdev.mdev.name = HSMP_CDEV_NAME;
hsmp_pdev.mdev.minor = MISC_DYNAMIC_MINOR;
hsmp_pdev.mdev.fops = &hsmp_fops;
- hsmp_pdev.mdev.parent = dev;
+ /*
+ * /dev/hsmp is a singleton shared by all sockets and is torn down on
+ * the last socket's unbind. Do not parent it to a per-socket device:
+ * those can be unbound individually and out of order, which would leave
+ * the misc device parented to an already-removed device. Leave it
+ * unparented so its lifetime is independent.
+ */
+ hsmp_pdev.mdev.parent = NULL;
hsmp_pdev.mdev.nodename = HSMP_DEVNODE_NAME;
hsmp_pdev.mdev.mode = 0644;

diff --git a/drivers/platform/x86/amd/hsmp/hsmp.h b/drivers/platform/x86/amd/hsmp/hsmp.h
index a43a8043a6cb..bc2050dcfdb5 100644
--- a/drivers/platform/x86/amd/hsmp/hsmp.h
+++ b/drivers/platform/x86/amd/hsmp/hsmp.h
@@ -57,7 +57,6 @@ struct hsmp_plat_device {
struct hsmp_socket *sock;
u32 proto_ver;
u16 num_sockets;
- bool is_probed;
};

int hsmp_cache_proto_ver(u16 sock_ind);
--
2.34.1