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

Muralidhara M K <[email protected]> Wed, 29 Jul 2026 22:10:32 +0530
Newsgroups org.kernel.vger.platform-driver-x86,org.kernel.vger.linux-kernel
Message-ID <[email protected]>
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 <[email protected]>
---
 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