diff --git a/Documentation/arch/x86/amd_hsmp.rst b/Documentation/arch/x86/amd_hsmp.rst index 8bb411f0d70d..fa1fc240e212 100644 --- a/Documentation/arch/x86/amd_hsmp.rst +++ b/Documentation/arch/x86/amd_hsmp.rst @@ -68,6 +68,13 @@ under per socket sysfs directory created at Note: lseek() is not supported as entire metrics table is read. +The sysfs metrics_bin path supports only HSMP protocol version 6 and, +because it is a file read, can return a torn snapshot if userspace +reads in pieces. The protocol version 7 metric table (~13 KB) also +exceeds PAGE_SIZE, so a read returns ``-EOPNOTSUPP`` there. For +atomic reads on any protocol version, use the +``HSMP_IOCTL_GET_TELEMETRY_DATA`` ioctl on /dev/hsmp (see below). + Metrics table definitions will be documented as part of Public PPR. The same is defined in the amd_hsmp.h header. @@ -167,7 +174,7 @@ Next thing, open the device file, as follows:: exit(1); } -The following IOCTL is defined: +The following IOCTLs are defined: ``ioctl(file, HSMP_IOCTL_CMD, struct hsmp_message *msg)`` The argument is a pointer to a:: @@ -180,6 +187,32 @@ The following IOCTL is defined: __u16 sock_ind; /* socket number */ }; +``ioctl(file, HSMP_IOCTL_GET_TELEMETRY_DATA, struct hsmp_telemetry_data *req)`` + Atomically fetch the firmware metric (telemetry) table for a socket. + The ioctl copies the table in one shot, so unlike the metrics_bin + sysfs path it cannot return a torn snapshot and is not bounded by + PAGE_SIZE. Required for HSMP protocol version 7+ (e.g. Family 1Ah + Model 50h-5Fh, whose table is ~13 KB). Argument:: + + struct hsmp_telemetry_data { + __u64 buf; /* User pointer to destination buffer */ + __u32 size; /* Size of @buf in bytes */ + __u16 sock_ind; /* Socket index */ + __u16 reserved; /* Reserved, must be zero */ + }; + + ``size`` must be non-zero and no larger than the table size firmware + reports for that socket; a larger value is rejected with ``-EINVAL`` + rather than short-written, and a smaller one returns the leading + ``size`` bytes of the snapshot. A non-zero ``reserved`` is also + rejected with ``-EINVAL``. + + The table layout depends on the protocol version, which userspace + reads from the ``protocol_version`` sysfs attribute. On version 6 + the table is ``struct hsmp_metric_table``, so callers pass + ``sizeof(struct hsmp_metric_table)``. Later version metrics table + layout is documented in the Public PPR. + The ioctl would return a non-zero on failure; you can read errno to see what happened. The transaction returns 0 on success. diff --git a/arch/x86/include/uapi/asm/amd_hsmp.h b/arch/x86/include/uapi/asm/amd_hsmp.h index d1d3bc60cffc..eda336bfd3e9 100644 --- a/arch/x86/include/uapi/asm/amd_hsmp.h +++ b/arch/x86/include/uapi/asm/amd_hsmp.h @@ -612,6 +612,39 @@ struct hsmp_metric_table { __u32 gfxclk_frequency[8]; }; +/** + * struct hsmp_telemetry_data - Request descriptor for HSMP telemetry IOCTL + * @buf: Input. Userspace pointer (encoded as __u64 to keep the layout + * stable between 32-bit and 64-bit callers) to the destination + * buffer that receives the metric table. + * @size: Input. Size in bytes of the buffer pointed to by @buf, and the + * number of bytes copied out on success. Must be non-zero and no + * larger than the metric table size firmware reports for this + * socket; a larger value is rejected with -EINVAL rather than + * short-written. A smaller value returns the leading @size bytes + * of the snapshot. The kernel does not write this field back. + * @sock_ind: Input. Socket index from which the metric table is read. + * @reserved: Reserved for future use. Callers should set this to zero; + * future kernels may begin interpreting the field, so passing + * a non-zero value today is not forwards compatible. + * + * Placing @buf first lets all fields fall on their natural alignment under + * the surrounding #pragma pack(4), so the struct is a tight 16 bytes with + * the same wire layout on 32-bit and 64-bit userspace. + * + * The metric table layout depends on the HSMP protocol version reported by + * firmware, which userspace can read from the protocol_version sysfs + * attribute. Protocol version 6 uses struct hsmp_metric_table, so callers on + * that version pass sizeof(struct hsmp_metric_table). Later version metrics + * table layout is documented in the Public PPR. + */ +struct hsmp_telemetry_data { + __u64 buf; + __u32 size; + __u16 sock_ind; + __u16 reserved; +}; + /* Reset to default packing */ #pragma pack() @@ -619,4 +652,16 @@ struct hsmp_metric_table { #define HSMP_BASE_IOCTL_NR 0xF8 #define HSMP_IOCTL_CMD _IOWR(HSMP_BASE_IOCTL_NR, 0, struct hsmp_message) +/* + * Fetch the firmware metric (telemetry) table for a given socket via the + * HSMP character device. This avoids the PAGE_SIZE limitation of the + * sysfs binary attribute path for tables larger than one page (such as the + * ~13 KB table used by HSMP protocol version 7). + * + * The direction is _IOW because the kernel only reads the request struct; + * the table itself is written to the buffer that @buf points at. + */ +#define HSMP_IOCTL_GET_TELEMETRY_DATA \ + _IOW(HSMP_BASE_IOCTL_NR, 1, struct hsmp_telemetry_data) + #endif /*_ASM_X86_AMD_HSMP_H_*/ diff --git a/drivers/platform/x86/amd/hsmp/hsmp.c b/drivers/platform/x86/amd/hsmp/hsmp.c index 3e9bdbcd9ea9..5e123a4ecea9 100644 --- a/drivers/platform/x86/amd/hsmp/hsmp.c +++ b/drivers/platform/x86/amd/hsmp/hsmp.c @@ -15,9 +15,12 @@ #include #include #include +#include #include #include +#include #include +#include #include "hsmp.h" @@ -347,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 }; @@ -416,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; @@ -445,13 +576,20 @@ ssize_t hsmp_metric_tbl_read(struct hsmp_socket *sock, char *buf, size_t size) */ guard(mutex)(&sock->metric_read_lock); - ret = hsmp_send_message(&msg); + 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)