[PATCH 5/7] platform/x86/amd/hsmp: Add SMN read IOCTL support

From: Muralidhara M K

Date: Wed Jul 29 2026 - 12:52:25 EST


Bringing up a client platform means reading SMN registers that the HSMP
message set does not cover, and today that means either a debugfs
interface that is not present on production kernels or an out-of-tree
module. Add HSMP_IOCTL_SMN_CMD so a privileged tool can issue a 32-bit
SMN read for a given socket through /dev/hsmp, routed through the same
amd_smn_hsmp_rdwr() helper the driver already uses for its mailbox.

The ioctl is read-only. struct hsmp_smn_message carries a @write flag so
write support could be added later without changing the layout, but a
request that sets it is rejected with -EPERM regardless of the caller's
privilege or open mode: an SMN write can have side effects anywhere in
the SoC, and nothing in this driver can bound them. Reads still require
CAP_SYS_RAWIO, because even reading can touch registers with read side
effects.

Every field of the request falls on its natural alignment under the
surrounding #pragma pack(4), so the struct is a tight 12 bytes with the
same wire layout for 32-bit and 64-bit callers, and the reserved byte is
rejected when non-zero so a future kernel can repurpose it without
breaking deployed userspace.

The handler takes hsmp_sock_rwsem for read across the bounds check and
the access. /dev/hsmp is a singleton character device that outlives an
individual socket unbind, so an ioctl on an already-open fd can run
concurrently with teardown, and remove takes the same lock for write
while it tears the socket array down; without the lock the check against
num_sockets could be made against a count that is already being torn
down. The copy_to_user() is deliberately outside the lock, because
faulting in a userfaultfd-backed destination can block indefinitely and
would otherwise leave a socket unbind waiting for the write lock. This
mirrors HSMP_IOCTL_GET_TELEMETRY_DATA.

The user-controlled socket index is clamped with array_index_nospec()
before it is used to reach per-node data, mitigating Spectre v1
(CVE-2017-5753).

Signed-off-by: Muralidhara M K <muralidhara.mk@xxxxxxx>
---
arch/x86/include/uapi/asm/amd_hsmp.h | 34 ++++++++++++++
drivers/platform/x86/amd/hsmp/hsmp.c | 69 ++++++++++++++++++++++++++++
2 files changed, 103 insertions(+)

diff --git a/arch/x86/include/uapi/asm/amd_hsmp.h b/arch/x86/include/uapi/asm/amd_hsmp.h
index b4c3ddb9d1c1..9c4ad22e47ae 100644
--- a/arch/x86/include/uapi/asm/amd_hsmp.h
+++ b/arch/x86/include/uapi/asm/amd_hsmp.h
@@ -79,6 +79,31 @@ struct hsmp_message {
__u16 sock_ind; /* socket number */
};

+/**
+ * struct hsmp_smn_message - Request descriptor for the HSMP SMN read IOCTL
+ * @smn_address: Input. SMN address to read.
+ * @value: Output. Populated by the kernel with the value read.
+ * @sock_ind: Input. Socket index the read is issued on.
+ * @write: Must be zero. This IOCTL is read-only, so a request with
+ * @write set is rejected with -EPERM whatever the caller's
+ * privilege or open mode. The field is kept so that write
+ * support could be added later without changing the layout.
+ * @reserved: Reserved for future use. Callers must set this to zero; a
+ * non-zero value is rejected with -EINVAL so future kernels can
+ * repurpose the field without breaking deployed userspace.
+ *
+ * Every field falls on its natural alignment under the surrounding
+ * #pragma pack(4), so the struct is a tight 12 bytes with the same wire
+ * layout on 32-bit and 64-bit userspace.
+ */
+struct hsmp_smn_message {
+ __u32 smn_address;
+ __u32 value;
+ __u16 sock_ind;
+ __u8 write;
+ __u8 reserved;
+};
+
enum hsmp_msg_type {
HSMP_RSVD = -1,
HSMP_SET = 0,
@@ -664,6 +689,15 @@ struct hsmp_telemetry_data {
#define HSMP_IOCTL_GET_TELEMETRY_DATA \
_IOW(HSMP_BASE_IOCTL_NR, 1, struct hsmp_telemetry_data)

+/*
+ * Read a 32-bit SMN register on a given socket. This reaches registers
+ * outside the HSMP message ABI, so it requires CAP_SYS_RAWIO.
+ *
+ * The direction is _IOWR because the kernel reads the request struct and
+ * writes the value it read back into the same struct.
+ */
+#define HSMP_IOCTL_SMN_CMD _IOWR(HSMP_BASE_IOCTL_NR, 2, struct hsmp_smn_message)
+
/*
* Client HSMP messages supported on the Family 1Ah client platforms:
* Models 80h-87h (Medusa1), Models 88h-8Fh (Olympic Ridge) and
diff --git a/drivers/platform/x86/amd/hsmp/hsmp.c b/drivers/platform/x86/amd/hsmp/hsmp.c
index 2326506929a4..ec11c5840b87 100644
--- a/drivers/platform/x86/amd/hsmp/hsmp.c
+++ b/drivers/platform/x86/amd/hsmp/hsmp.c
@@ -8,8 +8,10 @@
*/

#include <asm/amd/hsmp.h>
+#include <asm/amd/node.h>

#include <linux/acpi.h>
+#include <linux/capability.h>
#include <linux/cleanup.h>
#include <linux/delay.h>
#include <linux/device.h>
@@ -430,6 +432,71 @@ static bool is_get_msg(struct hsmp_message *msg)
return false;
}

+/*
+ * Read a 32-bit SMN register on the requested socket.
+ *
+ * Raw SMN access reaches registers that are not part of the HSMP message ABI
+ * and whose side effects the driver cannot reason about, so it is gated on
+ * CAP_SYS_RAWIO. Writes are not offered at all: the request carries a @write
+ * flag so support could be added later, and a request that sets it today is
+ * refused whatever the caller's privilege or open mode.
+ */
+static long hsmp_ioctl_smn(struct file *fp, unsigned long arg)
+{
+ void __user *arguser = (void __user *)arg;
+ struct hsmp_smn_message smn;
+ unsigned int sock_ind;
+ int ret;
+
+ if (!capable(CAP_SYS_RAWIO))
+ return -EPERM;
+
+ /* The value read travels back in the request struct. */
+ if (!(fp->f_mode & FMODE_READ))
+ return -EPERM;
+
+ if (copy_from_user(&smn, arguser, sizeof(smn)))
+ return -EFAULT;
+
+ if (smn.write)
+ return -EPERM;
+
+ if (smn.reserved)
+ return -EINVAL;
+
+ /*
+ * /dev/hsmp is a singleton character device that outlives an individual
+ * socket unbind, so an ioctl on an already-open fd can run concurrently
+ * with socket teardown. Hold hsmp_sock_rwsem for read across the bounds
+ * check and the access, since remove takes the same lock for write
+ * while it tears the socket array down.
+ */
+ scoped_guard(rwsem_read, &hsmp_sock_rwsem) {
+ if (!hsmp_pdev.sock || smn.sock_ind >= hsmp_pdev.num_sockets)
+ return -ENODEV;
+
+ /*
+ * Sanitize the user-controlled socket index against speculative
+ * execution. The bounds check above retires the out-of-range
+ * case with -ENODEV, but a mispredicted branch can still let
+ * the CPU speculatively use the index to reach per-node data
+ * and pull arbitrary kernel memory into the cache (Spectre v1,
+ * CVE-2017-5753).
+ */
+ sock_ind = array_index_nospec(smn.sock_ind, hsmp_pdev.num_sockets);
+
+ ret = amd_smn_hsmp_rdwr(sock_ind, smn.smn_address, &smn.value, false);
+ }
+
+ if (ret)
+ return ret;
+
+ if (copy_to_user(arguser, &smn, sizeof(smn)))
+ return -EFAULT;
+
+ return 0;
+}
+
static long hsmp_ioctl_msg(struct file *fp, unsigned long arg)
{
int __user *arguser = (int __user *)arg;
@@ -620,6 +687,8 @@ long hsmp_ioctl(struct file *fp, unsigned int cmd, unsigned long arg)
return hsmp_ioctl_msg(fp, arg);
case HSMP_IOCTL_GET_TELEMETRY_DATA:
return hsmp_ioctl_get_telemetry(fp, arg);
+ case HSMP_IOCTL_SMN_CMD:
+ return hsmp_ioctl_smn(fp, arg);
default:
return -ENOTTY;
}
--
2.34.1