summaryrefslogtreecommitdiff
path: root/drivers/platform/x86/amd/hsmp
diff options
context:
space:
mode:
Diffstat (limited to 'drivers/platform/x86/amd/hsmp')
-rw-r--r--drivers/platform/x86/amd/hsmp/acpi.c220
-rw-r--r--drivers/platform/x86/amd/hsmp/hsmp.c328
-rw-r--r--drivers/platform/x86/amd/hsmp/hsmp.h19
-rw-r--r--drivers/platform/x86/amd/hsmp/hwmon.c3
-rw-r--r--drivers/platform/x86/amd/hsmp/plat.c38
5 files changed, 549 insertions, 59 deletions
diff --git a/drivers/platform/x86/amd/hsmp/acpi.c b/drivers/platform/x86/amd/hsmp/acpi.c
index 97ed71593bdf..ddd7a04ee753 100644
--- a/drivers/platform/x86/amd/hsmp/acpi.c
+++ b/drivers/platform/x86/amd/hsmp/acpi.c
@@ -15,12 +15,18 @@
#include <linux/array_size.h>
#include <linux/bits.h>
#include <linux/bitfield.h>
+#include <linux/cleanup.h>
#include <linux/device.h>
#include <linux/dev_printk.h>
#include <linux/ioport.h>
+#include <linux/kref.h>
#include <linux/kstrtox.h>
+#include <linux/lockdep.h>
#include <linux/module.h>
#include <linux/platform_device.h>
+#include <linux/rwsem.h>
+#include <linux/slab.h>
+#include <linux/string.h>
#include <linux/sysfs.h>
#include <linux/topology.h>
#include <linux/uuid.h>
@@ -38,6 +44,17 @@
static struct hsmp_plat_device *hsmp_pdev;
+/*
+ * Tracks the ACPI socket platform devices that share the socket array and the
+ * /dev/hsmp misc device. The first probe initializes it, each further probe
+ * takes a reference and every remove (or probe failure) drops one; the last
+ * put frees the shared state via hsmp_acpi_sock_release(). All get/put run
+ * under hsmp_sock_rwsem held for write, so the counting is already serialized
+ * and the atomic in kref is not strictly needed; kref is used for the clearer
+ * get/put interface and its release callback.
+ */
+static struct kref hsmp_acpi_sock_kref;
+
struct hsmp_sys_attr {
struct device_attribute dattr;
u32 msg_id;
@@ -77,6 +94,8 @@ static inline int hsmp_get_uid(struct device *dev, u16 *sock_ind)
* bytes to integer.
*/
uid = acpi_device_uid(ACPI_COMPANION(dev));
+ if (!uid || strlen(uid) < 3)
+ return -EINVAL;
return kstrtou16(uid + 2, 10, sock_ind);
}
@@ -104,7 +123,7 @@ static acpi_status hsmp_resource(struct acpi_resource *res, void *data)
return AE_OK;
}
-static int hsmp_read_acpi_dsd(struct hsmp_socket *sock)
+static int hsmp_read_acpi_dsd(struct device *dev, struct hsmp_socket *sock)
{
struct acpi_buffer buf = { ACPI_ALLOCATE_BUFFER, NULL };
union acpi_object *guid, *mailbox_package;
@@ -113,10 +132,10 @@ static int hsmp_read_acpi_dsd(struct hsmp_socket *sock)
int ret = 0;
int j;
- status = acpi_evaluate_object_typed(ACPI_HANDLE(sock->dev), "_DSD", NULL,
+ status = acpi_evaluate_object_typed(ACPI_HANDLE(dev), "_DSD", NULL,
&buf, ACPI_TYPE_PACKAGE);
if (ACPI_FAILURE(status)) {
- dev_err(sock->dev, "Failed to read mailbox reg offsets from DSD table, err: %s\n",
+ dev_err(dev, "Failed to read mailbox reg offsets from DSD table, err: %s\n",
acpi_format_exception(status));
return -ENODEV;
}
@@ -139,7 +158,7 @@ static int hsmp_read_acpi_dsd(struct hsmp_socket *sock)
guid = &dsd->package.elements[0];
mailbox_package = &dsd->package.elements[1];
if (!is_acpi_hsmp_uuid(guid) || mailbox_package->type != ACPI_TYPE_PACKAGE) {
- dev_err(sock->dev, "Invalid hsmp _DSD table data\n");
+ dev_err(dev, "Invalid hsmp _DSD table data\n");
ret = -EINVAL;
goto free_buf;
}
@@ -148,12 +167,18 @@ static int hsmp_read_acpi_dsd(struct hsmp_socket *sock)
union acpi_object *msgobj, *msgstr, *msgint;
msgobj = &mailbox_package->package.elements[j];
- msgstr = &msgobj->package.elements[0];
- msgint = &msgobj->package.elements[1];
/* package should have 1 string and 1 integer object */
if (msgobj->type != ACPI_TYPE_PACKAGE ||
- msgstr->type != ACPI_TYPE_STRING ||
+ msgobj->package.count < 2) {
+ ret = -EINVAL;
+ goto free_buf;
+ }
+
+ msgstr = &msgobj->package.elements[0];
+ msgint = &msgobj->package.elements[1];
+
+ if (msgstr->type != ACPI_TYPE_STRING ||
msgint->type != ACPI_TYPE_INTEGER) {
ret = -EINVAL;
goto free_buf;
@@ -183,14 +208,14 @@ free_buf:
return ret;
}
-static int hsmp_read_acpi_crs(struct hsmp_socket *sock)
+static int hsmp_read_acpi_crs(struct device *dev, struct hsmp_socket *sock)
{
acpi_status status;
- status = acpi_walk_resources(ACPI_HANDLE(sock->dev), METHOD_NAME__CRS,
+ status = acpi_walk_resources(ACPI_HANDLE(dev), METHOD_NAME__CRS,
hsmp_resource, sock);
if (ACPI_FAILURE(status)) {
- dev_err(sock->dev, "Failed to look up MP1 base address from CRS method, err: %s\n",
+ dev_err(dev, "Failed to look up MP1 base address from CRS method, err: %s\n",
acpi_format_exception(status));
return -EINVAL;
}
@@ -198,10 +223,10 @@ static int hsmp_read_acpi_crs(struct hsmp_socket *sock)
return -EINVAL;
/* The mapped region should be un-cached */
- sock->virt_base_addr = devm_ioremap_uc(sock->dev, sock->mbinfo.base_addr,
+ sock->virt_base_addr = devm_ioremap_uc(dev, sock->mbinfo.base_addr,
sock->mbinfo.size);
if (!sock->virt_base_addr) {
- dev_err(sock->dev, "Failed to ioremap MP1 base address\n");
+ dev_err(dev, "Failed to ioremap MP1 base address\n");
return -ENOMEM;
}
@@ -215,7 +240,6 @@ static int hsmp_parse_acpi_table(struct device *dev, u16 sock_ind)
int ret;
sock->sock_ind = sock_ind;
- sock->dev = dev;
sock->amd_hsmp_rdwr = amd_hsmp_acpi_rdwr;
sema_init(&sock->hsmp_sem, 1);
@@ -223,12 +247,27 @@ static int hsmp_parse_acpi_table(struct device *dev, u16 sock_ind)
dev_set_drvdata(dev, sock);
/* Read MP1 base address from CRS method */
- ret = hsmp_read_acpi_crs(sock);
+ ret = hsmp_read_acpi_crs(dev, sock);
if (ret)
return ret;
/* Read mailbox offsets from DSD table */
- return hsmp_read_acpi_dsd(sock);
+ ret = hsmp_read_acpi_dsd(dev, sock);
+ if (ret)
+ return ret;
+
+ /*
+ * Publish sock->dev last. hsmp_send_message() uses it (via
+ * smp_load_acquire()) as the readiness gate for the lock-free data
+ * plane, so it must become visible only after virt_base_addr, the
+ * mailbox offsets and the semaphore are fully initialized. On a
+ * multi-socket system socket 0 exposes /dev/hsmp before later sockets
+ * finish probing, so without this an ioctl aimed at a socket still in
+ * bring-up could pass the gate and dereference a NULL virt_base_addr.
+ */
+ smp_store_release(&sock->dev, dev);
+
+ return 0;
}
static ssize_t hsmp_metric_tbl_acpi_read(struct file *filp, struct kobject *kobj,
@@ -238,13 +277,31 @@ static ssize_t hsmp_metric_tbl_acpi_read(struct file *filp, struct kobject *kobj
struct device *dev = container_of(kobj, struct device, kobj);
struct hsmp_socket *sock = dev_get_drvdata(dev);
+ /*
+ * metrics_bin is a sysfs binary attribute and is capped at PAGE_SIZE.
+ * It can therefore only carry the protocol version 6 metric table
+ * (struct hsmp_metric_table). The larger tables defined from protocol
+ * version 7 onwards do not fit; userspace on those systems must read
+ * the snapshot through HSMP_IOCTL_GET_TELEMETRY_DATA on /dev/hsmp.
+ * Surface the unsupported case here as -EOPNOTSUPP rather than
+ * silently truncating the snapshot.
+ */
+ if (hsmp_pdev->proto_ver != HSMP_PROTO_VER6)
+ return -EOPNOTSUPP;
+
return hsmp_metric_tbl_read(sock, buf, count);
}
static umode_t hsmp_is_sock_attr_visible(struct kobject *kobj,
const struct bin_attribute *battr, int id)
{
- if (hsmp_pdev->proto_ver == HSMP_PROTO_VER6)
+ /*
+ * Keep metrics_bin visible on protocol version 7 and later as well,
+ * so that userspace which expects the file to exist gets a clear
+ * -EOPNOTSUPP from the read handler instead of -ENOENT, and is
+ * pointed at HSMP_IOCTL_GET_TELEMETRY_DATA as the supported path.
+ */
+ if (hsmp_pdev->proto_ver >= HSMP_PROTO_VER6)
return battr->attr.mode;
return 0;
@@ -459,11 +516,20 @@ static ssize_t hsmp_freq_limit_source_show(struct device *dev, struct device_att
return len;
}
+/*
+ * Bring up one ACPI HSMP socket: parse its ACPI table, run the mailbox
+ * handshake and register its sysfs/hwmon interfaces.
+ *
+ * Called with hsmp_sock_rwsem held for write by hsmp_acpi_probe(), so the
+ * per-socket bring-up cannot race a concurrent probe or remove.
+ */
static int init_acpi(struct device *dev)
{
u16 sock_ind;
int ret;
+ lockdep_assert_held_write(&hsmp_sock_rwsem);
+
ret = hsmp_get_uid(dev, &sock_ind);
if (ret)
return ret;
@@ -491,7 +557,7 @@ static int init_acpi(struct device *dev)
return ret;
}
- if (hsmp_pdev->proto_ver == HSMP_PROTO_VER6) {
+ if (hsmp_pdev->proto_ver >= HSMP_PROTO_VER6) {
ret = hsmp_get_tbl_dram_base(sock_ind);
if (ret)
dev_info(dev, "Failed to init metric table\n");
@@ -576,6 +642,60 @@ static const struct acpi_device_id amd_hsmp_acpi_ids[] = {
};
MODULE_DEVICE_TABLE(acpi, amd_hsmp_acpi_ids);
+/*
+ * kref release: tear down the shared ACPI socket state once the last socket
+ * drops its reference. Deregister /dev/hsmp if it was registered, unmap any
+ * metric-table DRAM, destroy the per-socket mutexes and free the socket array.
+ *
+ * Runs from kref_put() with hsmp_sock_rwsem held for write, since the remove
+ * and probe-failure paths both drop their reference under that lock. The write
+ * lock has drained any in-flight hsmp_send_message(), so unmapping the mailbox
+ * and freeing the array cannot race the data plane.
+ */
+static void hsmp_acpi_sock_release(struct kref *kref)
+{
+ lockdep_assert_held_write(&hsmp_sock_rwsem);
+
+ 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 already took a reference on entry to hsmp_acpi_probe(), so clear
+ * its sock->dev and drop that reference; the shared state is released if it was
+ * the last one.
+ *
+ * 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.
+ *
+ * Called from hsmp_acpi_probe(), which already holds hsmp_sock_rwsem for write.
+ */
+static void hsmp_acpi_probe_failure_cleanup(struct device *dev)
+{
+ struct hsmp_socket *sock = dev_get_drvdata(dev);
+
+ lockdep_assert_held_write(&hsmp_sock_rwsem);
+
+ if (sock)
+ sock->dev = NULL;
+
+ kref_put(&hsmp_acpi_sock_kref, hsmp_acpi_sock_release);
+}
+
static int hsmp_acpi_probe(struct platform_device *pdev)
{
int ret;
@@ -584,34 +704,60 @@ static int hsmp_acpi_probe(struct platform_device *pdev)
if (!hsmp_pdev)
return -ENOMEM;
- if (!hsmp_pdev->is_probed) {
+ /*
+ * Multiple ACPI socket devices probe in parallel, but the one-time
+ * socket-array allocation and /dev/hsmp registration below must run
+ * exactly once. Hold the socket rwsem for write across the whole
+ * bring-up so it cannot race a concurrent probe or remove, and so the
+ * probe-failure teardown drains the data plane.
+ */
+ guard(rwsem_write)(&hsmp_sock_rwsem);
+
+ 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 = kzalloc_objs(*hsmp_pdev->sock,
+ hsmp_pdev->num_sockets);
if (!hsmp_pdev->sock)
return -ENOMEM;
+
+ hsmp_init_metric_read_locks(hsmp_pdev);
+ kref_init(&hsmp_acpi_sock_kref);
+ } else {
+ kref_get(&hsmp_acpi_sock_kref);
}
+ /*
+ * This socket now holds a reference (kref_init on the first socket,
+ * kref_get afterwards). Every failure path below drops it via
+ * hsmp_acpi_probe_failure_cleanup(), and a successful probe hands it to
+ * hsmp_acpi_remove().
+ */
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) {
- ret = hsmp_misc_register(&pdev->dev);
+ if (IS_ERR_OR_NULL(hsmp_pdev->mdev.this_device)) {
+ /*
+ * Register /dev/hsmp unparented. It is a singleton shared by all
+ * ACPI sockets and outlives all but the last of them, so
+ * parenting it to this socket's device would leave a dangling
+ * parent once that socket is unbound.
+ */
+ ret = hsmp_misc_register(NULL);
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");
}
return 0;
@@ -619,14 +765,26 @@ static int hsmp_acpi_probe(struct platform_device *pdev)
static void hsmp_acpi_remove(struct platform_device *pdev)
{
+ struct hsmp_socket *sock = dev_get_drvdata(&pdev->dev);
+
/*
- * We register only one misc_device even on multi-socket system.
- * So, deregister should happen only once.
+ * Serialize the kref_put() and any release it triggers against a
+ * concurrent probe, and drain the data plane for the whole
+ * teardown: this covers the per-socket unbind, whose mailbox devres
+ * unmaps once we return, and the last unbind that frees the socket
+ * array in hsmp_acpi_sock_release().
*/
- if (hsmp_pdev->is_probed) {
- hsmp_misc_deregister();
- hsmp_pdev->is_probed = false;
- }
+ guard(rwsem_write)(&hsmp_sock_rwsem);
+
+ /*
+ * 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.
+ */
+ sock->dev = NULL;
+
+ kref_put(&hsmp_acpi_sock_kref, 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 631ffc0978d1..5e123a4ecea9 100644
--- a/drivers/platform/x86/amd/hsmp/hsmp.c
+++ b/drivers/platform/x86/amd/hsmp/hsmp.c
@@ -10,10 +10,17 @@
#include <asm/amd/hsmp.h>
#include <linux/acpi.h>
+#include <linux/cleanup.h>
#include <linux/delay.h>
#include <linux/device.h>
+#include <linux/io.h>
+#include <linux/mutex.h>
+#include <linux/nospec.h>
+#include <linux/rwsem.h>
#include <linux/semaphore.h>
+#include <linux/slab.h>
#include <linux/sysfs.h>
+#include <linux/uaccess.h>
#include "hsmp.h"
@@ -41,6 +48,17 @@
static struct hsmp_plat_device hsmp_pdev;
/*
+ * Gates the AMD HSMP data plane against socket bring-up and teardown.
+ *
+ * hsmp_send_message() takes it for read, so open /dev/hsmp fds and hwmon reads
+ * run concurrently. Probe and remove take it for write: probe brings sockets
+ * up (running the mailbox handshake via hsmp_send_message_locked()) and remove
+ * tears them down, both excluding and draining the data plane.
+ */
+DECLARE_RWSEM(hsmp_sock_rwsem);
+EXPORT_SYMBOL_NS_GPL(hsmp_sock_rwsem, "AMD_HSMP");
+
+/*
* Send a message to the HSMP port via PCI-e config space registers
* or by writing to MMIO space.
*
@@ -182,28 +200,33 @@ static int validate_message(struct hsmp_message *msg)
return -EINVAL;
/*
- * Some older HSMP SET messages are updated to add GET in the same message.
- * In these messages, GET returns the current value and SET also returns
- * the successfully set value. To support this GET and SET in same message
- * while maintaining backward compatibility for the HSMP users,
- * hsmp_msg_desc_table[] indicates only maximum allowed response_sz.
+ * As the HSMP protocol evolves, newer platforms may define more
+ * response arguments for existing messages. Use an upper-bound
+ * check so that older userspace callers requesting fewer response
+ * words than what the current hsmp_msg_desc_table[] defines are
+ * still accepted, while rejecting requests that exceed the
+ * hardware capability.
*/
- if (hsmp_msg_desc_table[msg->msg_id].type == HSMP_SET_GET) {
- if (msg->response_sz > hsmp_msg_desc_table[msg->msg_id].response_sz)
- return -EINVAL;
- } else {
- /* only HSMP_SET or HSMP_GET messages go through this strict check */
- if (msg->response_sz != hsmp_msg_desc_table[msg->msg_id].response_sz)
- return -EINVAL;
- }
+ if (msg->response_sz > hsmp_msg_desc_table[msg->msg_id].response_sz)
+ return -EINVAL;
+
return 0;
}
-int hsmp_send_message(struct hsmp_message *msg)
+/*
+ * Core message send. The caller must hold hsmp_sock_rwsem: the data plane
+ * takes it for read so many messages run concurrently, while the probe-time
+ * senders run under the write lock taken by probe. Holding it here serializes
+ * every message against socket teardown, which also holds it for write.
+ */
+static int hsmp_send_message_locked(struct hsmp_message *msg)
{
struct hsmp_socket *sock;
+ unsigned int sock_ind;
int ret;
+ lockdep_assert_held(&hsmp_sock_rwsem);
+
if (!msg)
return -EINVAL;
ret = validate_message(msg);
@@ -212,7 +235,29 @@ int hsmp_send_message(struct hsmp_message *msg)
if (!hsmp_pdev.sock || msg->sock_ind >= hsmp_pdev.num_sockets)
return -ENODEV;
- sock = &hsmp_pdev.sock[msg->sock_ind];
+
+ /*
+ * Sanitize sock_ind after the bounds check. A mispredicted branch can
+ * still let the CPU speculatively use msg->sock_ind as an index into
+ * hsmp_pdev.sock[] (Spectre v1, CVE-2017-5753), including for callers
+ * other than hsmp_ioctl_msg() that pass a user-derived socket index.
+ */
+ sock_ind = array_index_nospec(msg->sock_ind, hsmp_pdev.num_sockets);
+ sock = &hsmp_pdev.sock[sock_ind];
+
+ /*
+ * A slot exists for every possible socket, but it is only usable once
+ * that socket has actually been probed. Reject messages aimed at a
+ * socket that was never brought up or is still in bring-up, so we never
+ * operate on a zero-initialized semaphore or an unmapped mailbox. A
+ * non-NULL dev also guarantees virt_base_addr, the mailbox offsets and
+ * the semaphore are visible.
+ *
+ * Held under hsmp_sock_rwsem; pairs with smp_store_release(&sock->dev)
+ * in hsmp_parse_acpi_table().
+ */
+ if (!smp_load_acquire(&sock->dev))
+ return -ENODEV;
ret = down_interruptible(&sock->hsmp_sem);
if (ret < 0)
@@ -224,6 +269,19 @@ int hsmp_send_message(struct hsmp_message *msg)
return ret;
}
+
+int hsmp_send_message(struct hsmp_message *msg)
+{
+ /*
+ * Data-plane entry point: open /dev/hsmp fds and hwmon sysfs reads issue
+ * messages from here. Take hsmp_sock_rwsem for read so messages run
+ * concurrently with each other but are drained and kept out while
+ * probe/remove hold it for write to tear a socket down.
+ */
+ guard(rwsem_read)(&hsmp_sock_rwsem);
+
+ return hsmp_send_message_locked(msg);
+}
EXPORT_SYMBOL_NS_GPL(hsmp_send_message, "AMD_HSMP");
int hsmp_msg_get_nargs(u16 sock_ind, u32 msg_id, u32 *data, u8 num_args)
@@ -264,7 +322,7 @@ int hsmp_test(u16 sock_ind, u32 value)
msg.args[0] = value;
msg.sock_ind = sock_ind;
- ret = hsmp_send_message(&msg);
+ ret = hsmp_send_message_locked(&msg);
if (ret)
return ret;
@@ -292,7 +350,7 @@ static bool is_get_msg(struct hsmp_message *msg)
return false;
}
-long hsmp_ioctl(struct file *fp, unsigned int cmd, unsigned long arg)
+static long hsmp_ioctl_msg(struct file *fp, unsigned long arg)
{
int __user *arguser = (int __user *)arg;
struct hsmp_message msg = { 0 };
@@ -308,6 +366,19 @@ long hsmp_ioctl(struct file *fp, unsigned int cmd, unsigned long arg)
if (msg.msg_id < HSMP_TEST || msg.msg_id >= HSMP_MSG_ID_MAX)
return -ENOMSG;
+ /*
+ * Sanitize the user-controlled msg_id against speculative
+ * execution. The bounds check above retires the out-of-range
+ * case with -ENOMSG, but a mispredicted branch can still let the
+ * CPU speculatively use msg_id as an index into
+ * hsmp_msg_desc_table[] (here and in validate_message() /
+ * is_get_msg() called downstream via hsmp_send_message()), and
+ * pull arbitrary kernel memory into the cache (Spectre v1,
+ * CVE-2017-5753). Clamp once into msg.msg_id so every downstream
+ * dereference sees the sanitized value.
+ */
+ msg.msg_id = array_index_nospec(msg.msg_id, HSMP_MSG_ID_MAX);
+
switch (fp->f_mode & (FMODE_WRITE | FMODE_READ)) {
case FMODE_WRITE:
/*
@@ -348,11 +419,139 @@ long hsmp_ioctl(struct file *fp, unsigned int cmd, unsigned long arg)
return 0;
}
-ssize_t hsmp_metric_tbl_read(struct hsmp_socket *sock, char *buf, size_t size)
+static ssize_t hsmp_metric_tbl_read_locked(struct hsmp_socket *sock, char *buf,
+ size_t size);
+
+/*
+ * Fetch the firmware metric (telemetry) table for the requested socket and
+ * copy it to the userspace buffer described by the request.
+ *
+ * The metric table size is variable across HSMP protocol versions and on
+ * Family 1Ah Model 50h-5Fh exceeds PAGE_SIZE. The request carries the buffer
+ * size, which may be anything up to the size firmware reported for this
+ * socket's table.
+ */
+static long hsmp_ioctl_get_telemetry(struct file *fp, unsigned long arg)
+{
+ void *kbuf __free(kvfree) = NULL;
+ void __user *arguser = (void __user *)arg;
+ struct hsmp_telemetry_data req;
+ struct hsmp_socket *sock;
+ void __user *user_buf;
+ size_t tbl_size;
+ unsigned int sock_ind;
+ int ret;
+
+ /* Telemetry data is read-only; require read access on the fd. */
+ if (!(fp->f_mode & FMODE_READ))
+ return -EPERM;
+
+ if (copy_from_user(&req, arguser, sizeof(req)))
+ return -EFAULT;
+
+ /*
+ * Reserved fields must be zero so future kernels can safely
+ * repurpose them without breaking already-deployed userspace.
+ */
+ if (req.reserved)
+ return -EINVAL;
+
+ user_buf = u64_to_user_ptr(req.buf);
+
+ /*
+ * /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 socket
+ * lookup, the checks on its metric-table state and the read itself:
+ * probe and remove take the same lock for write, so they cannot free the
+ * socket array, unmap the table or destroy the per-socket mutex while
+ * this runs.
+ *
+ * The lock is dropped before the copy_to_user() below. Faulting in the
+ * destination can block indefinitely on a userfaultfd-backed buffer,
+ * which would leave a socket unbind waiting for the write lock.
+ */
+ scoped_guard(rwsem_read, &hsmp_sock_rwsem) {
+ if (!hsmp_pdev.sock || req.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 sock_ind as an index into
+ * hsmp_pdev.sock[] and pull arbitrary kernel memory into the
+ * cache (Spectre v1, CVE-2017-5753). array_index_nospec() turns
+ * the bounds check into a data-flow clamp so the speculative
+ * load is in-range too.
+ */
+ sock_ind = array_index_nospec(req.sock_ind, hsmp_pdev.num_sockets);
+ sock = &hsmp_pdev.sock[sock_ind];
+ if (!sock->metric_tbl_addr)
+ return -ENODEV;
+
+ tbl_size = sock->metric_tbl_size;
+ if (!tbl_size)
+ return -ENODEV;
+
+ /*
+ * A request shorter than the firmware table is served with the
+ * leading @size bytes of the snapshot, so userspace built
+ * against an older table layout keeps working on firmware that
+ * grew the table. Asking for more than firmware provides is
+ * rejected rather than short-written, so a caller can never
+ * mistake a partial copy for a full one.
+ */
+ if (!req.size || req.size > tbl_size)
+ return -EINVAL;
+
+ /*
+ * The bounce buffer is overwritten in full by memcpy_fromio()
+ * inside hsmp_metric_tbl_read_locked(); use kvmalloc() to avoid
+ * the zeroing cost of kvzalloc() on the ~13 KB allocation done
+ * on every ioctl call.
+ */
+ kbuf = kvmalloc(tbl_size, GFP_KERNEL);
+ if (!kbuf)
+ return -ENOMEM;
+
+ ret = hsmp_metric_tbl_read_locked(sock, kbuf, tbl_size);
+ }
+
+ if (ret < 0)
+ return ret;
+
+ if (copy_to_user(user_buf, kbuf, req.size))
+ return -EFAULT;
+
+ return 0;
+}
+
+long hsmp_ioctl(struct file *fp, unsigned int cmd, unsigned long arg)
+{
+ switch (cmd) {
+ case HSMP_IOCTL_CMD:
+ return hsmp_ioctl_msg(fp, arg);
+ case HSMP_IOCTL_GET_TELEMETRY_DATA:
+ return hsmp_ioctl_get_telemetry(fp, arg);
+ default:
+ return -ENOTTY;
+ }
+}
+
+/*
+ * Caller must hold hsmp_sock_rwsem. It keeps @sock, its metric-table mapping
+ * and its metric_read_lock alive: probe and remove take the same lock for
+ * write while they bring sockets up and tear them down.
+ */
+static ssize_t hsmp_metric_tbl_read_locked(struct hsmp_socket *sock, char *buf,
+ size_t size)
{
struct hsmp_message msg = { 0 };
int ret;
+ lockdep_assert_held(&hsmp_sock_rwsem);
+
if (!sock || !buf)
return -EINVAL;
@@ -361,8 +560,7 @@ ssize_t hsmp_metric_tbl_read(struct hsmp_socket *sock, char *buf, size_t size)
return -ENOMEM;
}
- /* Do not support lseek(), also don't allow more than the size of metric table */
- if (size != sizeof(struct hsmp_metric_table)) {
+ if (size != sock->metric_tbl_size) {
dev_err(sock->dev, "Wrong buffer size\n");
return -EINVAL;
}
@@ -370,27 +568,77 @@ ssize_t hsmp_metric_tbl_read(struct hsmp_socket *sock, char *buf, size_t size)
msg.msg_id = HSMP_GET_METRIC_TABLE;
msg.sock_ind = sock->sock_ind;
- ret = hsmp_send_message(&msg);
+ /*
+ * HSMP_GET_METRIC_TABLE makes firmware refill this socket's shared
+ * metric DRAM region, which is then copied out below. Hold the
+ * per-socket lock across the fill-and-copy so concurrent readers of the
+ * same socket cannot return a torn snapshot.
+ */
+ guard(mutex)(&sock->metric_read_lock);
+
+ ret = hsmp_send_message_locked(&msg);
if (ret)
return ret;
memcpy_fromio(buf, sock->metric_tbl_addr, size);
return size;
}
+
+ssize_t hsmp_metric_tbl_read(struct hsmp_socket *sock, char *buf, size_t size)
+{
+ guard(rwsem_read)(&hsmp_sock_rwsem);
+
+ return hsmp_metric_tbl_read_locked(sock, buf, size);
+}
EXPORT_SYMBOL_NS_GPL(hsmp_metric_tbl_read, "AMD_HSMP");
+void hsmp_init_metric_read_locks(struct hsmp_plat_device *pdev)
+{
+ u16 i;
+
+ for (i = 0; i < pdev->num_sockets; i++)
+ mutex_init(&pdev->sock[i].metric_read_lock);
+}
+EXPORT_SYMBOL_NS_GPL(hsmp_init_metric_read_locks, "AMD_HSMP");
+
+void hsmp_destroy_metric_read_locks(struct hsmp_plat_device *pdev)
+{
+ u16 i;
+
+ for (i = 0; i < pdev->num_sockets; i++)
+ mutex_destroy(&pdev->sock[i].metric_read_lock);
+}
+EXPORT_SYMBOL_NS_GPL(hsmp_destroy_metric_read_locks, "AMD_HSMP");
+
+void hsmp_unmap_metric_tbls(struct hsmp_plat_device *pdev)
+{
+ struct hsmp_socket *sock;
+ u16 i;
+
+ for (i = 0; i < pdev->num_sockets; i++) {
+ sock = &pdev->sock[i];
+ if (sock->metric_tbl_addr) {
+ iounmap(sock->metric_tbl_addr);
+ sock->metric_tbl_addr = NULL;
+ }
+ sock->metric_tbl_size = 0;
+ }
+}
+EXPORT_SYMBOL_NS_GPL(hsmp_unmap_metric_tbls, "AMD_HSMP");
+
int hsmp_get_tbl_dram_base(u16 sock_ind)
{
struct hsmp_socket *sock = &hsmp_pdev.sock[sock_ind];
struct hsmp_message msg = { 0 };
phys_addr_t dram_addr;
+ size_t tbl_size;
int ret;
msg.sock_ind = sock_ind;
msg.response_sz = hsmp_msg_desc_table[HSMP_GET_METRIC_TABLE_DRAM_ADDR].response_sz;
msg.msg_id = HSMP_GET_METRIC_TABLE_DRAM_ADDR;
- ret = hsmp_send_message(&msg);
+ ret = hsmp_send_message_locked(&msg);
if (ret)
return ret;
@@ -403,12 +651,33 @@ int hsmp_get_tbl_dram_base(u16 sock_ind)
dev_err(sock->dev, "Invalid DRAM address for metric table\n");
return -ENOMEM;
}
- sock->metric_tbl_addr = devm_ioremap(sock->dev, dram_addr,
- sizeof(struct hsmp_metric_table));
+ /*
+ * 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_size = 0;
+
+ /* SMU returns table size from Family 1Ah Model 50h and forward */
+ if (msg.args[2])
+ tbl_size = msg.args[2];
+ else
+ tbl_size = sizeof(struct hsmp_metric_table);
+
+ sock->metric_tbl_addr = ioremap(dram_addr, tbl_size);
if (!sock->metric_tbl_addr) {
dev_err(sock->dev, "Failed to ioremap metric table addr\n");
return -ENOMEM;
}
+ sock->metric_tbl_size = tbl_size;
+
return 0;
}
EXPORT_SYMBOL_NS_GPL(hsmp_get_tbl_dram_base, "AMD_HSMP");
@@ -422,7 +691,7 @@ int hsmp_cache_proto_ver(u16 sock_ind)
msg.sock_ind = sock_ind;
msg.response_sz = hsmp_msg_desc_table[HSMP_GET_PROTO_VER].response_sz;
- ret = hsmp_send_message(&msg);
+ ret = hsmp_send_message_locked(&msg);
if (!ret)
hsmp_pdev.proto_ver = msg.args[0];
@@ -441,6 +710,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;
+ /*
+ * The caller chooses the parent. The platform driver has a single
+ * device whose lifetime matches /dev/hsmp and parents it there. The
+ * ACPI driver passes NULL: its /dev/hsmp is a singleton shared by
+ * per-socket devices that can be unbound individually and out of order,
+ * so parenting it to one would leave it attached to an already-removed
+ * device.
+ */
hsmp_pdev.mdev.parent = dev;
hsmp_pdev.mdev.nodename = HSMP_DEVNODE_NAME;
hsmp_pdev.mdev.mode = 0644;
@@ -452,6 +729,7 @@ EXPORT_SYMBOL_NS_GPL(hsmp_misc_register, "AMD_HSMP");
void hsmp_misc_deregister(void)
{
misc_deregister(&hsmp_pdev.mdev);
+ hsmp_pdev.mdev.this_device = NULL;
}
EXPORT_SYMBOL_NS_GPL(hsmp_misc_deregister, "AMD_HSMP");
diff --git a/drivers/platform/x86/amd/hsmp/hsmp.h b/drivers/platform/x86/amd/hsmp/hsmp.h
index 0509a442eaae..8dbff16a87b1 100644
--- a/drivers/platform/x86/amd/hsmp/hsmp.h
+++ b/drivers/platform/x86/amd/hsmp/hsmp.h
@@ -15,9 +15,12 @@
#include <linux/hwmon.h>
#include <linux/kconfig.h>
#include <linux/miscdevice.h>
+#include <linux/mutex.h>
#include <linux/pci.h>
+#include <linux/rwsem.h>
#include <linux/semaphore.h>
#include <linux/sysfs.h>
+#include <linux/types.h>
#define HSMP_METRICS_TABLE_NAME "metrics_bin"
@@ -27,7 +30,7 @@
#define HSMP_DEVNODE_NAME "hsmp"
#define ACPI_HSMP_DEVICE_HID "AMDI0097"
-#define DRIVER_VERSION "2.5"
+#define DRIVER_VERSION "2.6"
struct hsmp_mbaddr_info {
u32 base_addr;
@@ -41,8 +44,12 @@ struct hsmp_socket {
struct bin_attribute hsmp_attr;
struct hsmp_mbaddr_info mbinfo;
void __iomem *metric_tbl_addr;
+ /* Size of the region mapped at @metric_tbl_addr, as reported by SMU */
+ size_t metric_tbl_size;
void __iomem *virt_base_addr;
struct semaphore hsmp_sem;
+ /* Serializes HSMP_GET_METRIC_TABLE fill-and-copy for this socket */
+ struct mutex metric_read_lock;
char name[HSMP_ATTR_GRP_NAME_SIZE];
struct device *dev;
u16 sock_ind;
@@ -54,7 +61,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);
@@ -63,6 +69,9 @@ long hsmp_ioctl(struct file *fp, unsigned int cmd, unsigned long arg);
void hsmp_misc_deregister(void);
int hsmp_misc_register(struct device *dev);
int hsmp_get_tbl_dram_base(u16 sock_ind);
+void hsmp_unmap_metric_tbls(struct hsmp_plat_device *pdev);
+void hsmp_init_metric_read_locks(struct hsmp_plat_device *pdev);
+void hsmp_destroy_metric_read_locks(struct hsmp_plat_device *pdev);
ssize_t hsmp_metric_tbl_read(struct hsmp_socket *sock, char *buf, size_t size);
struct hsmp_plat_device *get_hsmp_pdev(void);
#if IS_ENABLED(CONFIG_HWMON)
@@ -71,4 +80,10 @@ int hsmp_create_sensor(struct device *dev, u16 sock_ind);
static inline int hsmp_create_sensor(struct device *dev, u16 sock_ind) { return 0; }
#endif
int hsmp_msg_get_nargs(u16 sock_ind, u32 msg_id, u32 *data, u8 num_args);
+
+/*
+ * Gates the HSMP data plane: hsmp_send_message() takes it for read; probe and
+ * remove take it for write to bring sockets up and tear them down.
+ */
+extern struct rw_semaphore hsmp_sock_rwsem;
#endif /* HSMP_H */
diff --git a/drivers/platform/x86/amd/hsmp/hwmon.c b/drivers/platform/x86/amd/hsmp/hwmon.c
index 0cc9a742497f..c8314eee06f4 100644
--- a/drivers/platform/x86/amd/hsmp/hwmon.c
+++ b/drivers/platform/x86/amd/hsmp/hwmon.c
@@ -31,6 +31,9 @@ static int hsmp_hwmon_write(struct device *dev, enum hwmon_sensor_types type,
if (attr != hwmon_power_cap)
return -EOPNOTSUPP;
+ if (val < 0)
+ return -EINVAL;
+
msg.num_args = 1;
msg.args[0] = val / MICROWATT_PER_MILLIWATT;
msg.msg_id = HSMP_SET_SOCKET_POWER_LIMIT;
diff --git a/drivers/platform/x86/amd/hsmp/plat.c b/drivers/platform/x86/amd/hsmp/plat.c
index e07f68575055..e9b2b809c0f5 100644
--- a/drivers/platform/x86/amd/hsmp/plat.c
+++ b/drivers/platform/x86/amd/hsmp/plat.c
@@ -13,12 +13,14 @@
#include <linux/acpi.h>
#include <linux/build_bug.h>
+#include <linux/cleanup.h>
#include <linux/device.h>
#include <linux/dev_printk.h>
#include <linux/kconfig.h>
#include <linux/module.h>
#include <linux/pci.h>
#include <linux/platform_device.h>
+#include <linux/rwsem.h>
#include <linux/sysfs.h>
#include <asm/amd/node.h>
@@ -201,6 +203,25 @@ static int init_platform_device(struct device *dev)
return 0;
}
+/*
+ * The socket array is devm-managed and freed by the driver core, but the
+ * metric-table DRAM regions are mapped with plain ioremap() during probe and
+ * the per-socket mutexes need an explicit mutex_destroy(), neither of which
+ * devres covers.
+ *
+ * Take the data-plane rwsem for write to drain any in-flight
+ * hsmp_send_message(), unmap the metric tables, destroy the mutexes and drop
+ * the global socket pointer, all before devres frees the array. Registered as
+ * a devres action so it runs on both remove and probe failure.
+ */
+static void hsmp_pltdrv_release(void *data)
+{
+ guard(rwsem_write)(&hsmp_sock_rwsem);
+ hsmp_unmap_metric_tbls(hsmp_pdev);
+ hsmp_destroy_metric_read_locks(hsmp_pdev);
+ hsmp_pdev->sock = NULL;
+}
+
static int hsmp_pltdrv_probe(struct platform_device *pdev)
{
int ret;
@@ -211,7 +232,22 @@ static int hsmp_pltdrv_probe(struct platform_device *pdev)
if (!hsmp_pdev->sock)
return -ENOMEM;
- ret = init_platform_device(&pdev->dev);
+ hsmp_init_metric_read_locks(hsmp_pdev);
+
+ ret = devm_add_action_or_reset(&pdev->dev, hsmp_pltdrv_release, NULL);
+ if (ret)
+ return ret;
+
+ /*
+ * init_platform_device() runs the mailbox handshake via the probe-only
+ * senders, which issue messages through hsmp_send_message_locked() and
+ * so require hsmp_sock_rwsem held. Hold it for write, matching probe's
+ * role as a socket bring-up path. The lock is not held across
+ * devm_add_action_or_reset() above so the release action, which also
+ * takes it for write, does not deadlock if that registration fails.
+ */
+ scoped_guard(rwsem_write, &hsmp_sock_rwsem)
+ ret = init_platform_device(&pdev->dev);
if (ret) {
dev_err(&pdev->dev, "Failed to init HSMP mailbox\n");
return ret;