diff --git a/acpi.c b/acpi.c index 1390abc..ba08a0e 100644 --- a/acpi.c +++ b/acpi.c @@ -27,8 +27,13 @@ #include #include #include +#include +#include #include #include +#include +#include +#include #include #include @@ -54,6 +59,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; @@ -93,6 +109,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); } @@ -120,7 +138,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; @@ -129,10 +147,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; } @@ -155,7 +173,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; } @@ -164,12 +182,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; @@ -199,14 +223,14 @@ static int hsmp_read_acpi_dsd(struct hsmp_socket *sock) 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; } @@ -214,14 +238,14 @@ static int hsmp_read_acpi_crs(struct hsmp_socket *sock) return -EINVAL; #if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 5, 0) /* 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); #else - sock->virt_base_addr = devm_ioremap_nocache(sock->dev, sock->mbinfo.base_addr, + sock->virt_base_addr = devm_ioremap_nocache(dev, sock->mbinfo.base_addr, sock->mbinfo.size); #endif 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; } @@ -235,7 +259,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); @@ -243,12 +266,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, @@ -262,7 +300,19 @@ 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); - return hsmp_metric_tbl_read(sock, buf, count, off); + /* + * 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); } #if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 13, 0) @@ -273,6 +323,12 @@ static umode_t hsmp_is_sock_attr_visible(struct kobject *kobj, struct bin_attribute *battr, int id) #endif { + /* + * 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; @@ -392,10 +448,10 @@ static ssize_t hsmp_msg_fw_ver_show(struct device *dev, struct device_attribute FIELD_GET(FW_VER_MINOR_MASK, data), FIELD_GET(FW_VER_DEBUG_MASK, data)); #else - return sprintf(buf, "%lu.%lu.%lu\n", - FIELD_GET(FW_VER_MAJOR_MASK, data), - FIELD_GET(FW_VER_MINOR_MASK, data), - FIELD_GET(FW_VER_DEBUG_MASK, data)); + return sprintf(buf, "%lu.%lu.%lu\n", + FIELD_GET(FW_VER_MAJOR_MASK, data), + FIELD_GET(FW_VER_MINOR_MASK, data), + FIELD_GET(FW_VER_DEBUG_MASK, data)); #endif } @@ -528,18 +584,28 @@ static ssize_t hsmp_freq_limit_source_show(struct device *dev, struct device_att #if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 10, 0) len += sysfs_emit_at(buf, len, "%s\n", freqlimit_srcnames[index]); #else - len += scnprintf(buf, len, "%s\n", freqlimit_srcnames[index]); + len += scnprintf(buf + len, PAGE_SIZE - len, "%s\n", + freqlimit_srcnames[index]); #endif src_ind >>= 1; } 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; @@ -570,21 +636,22 @@ static int init_acpi(struct device *dev) if (hsmp_pdev->proto_ver >= HSMP_PROTO_VER6) { ret = hsmp_get_tbl_dram_base(sock_ind); if (ret) - dev_err(dev, "Failed to init metric table\n"); + dev_info(dev, "Failed to init metric table\n"); } ret = hsmp_create_sensor(dev, sock_ind); if (ret) - dev_err(dev, "Failed to register HSMP sensors with hwmon\n"); + dev_info(dev, "Failed to register HSMP sensors with hwmon\n"); dev_set_drvdata(dev, &hsmp_pdev->sock[sock_ind]); - return ret; + return 0; } static HSMP_CONST struct bin_attribute hsmp_metric_tbl_attr = { .attr = { .name = HSMP_METRICS_TABLE_NAME, .mode = 0444}, HSMP_BIN_READ = hsmp_metric_tbl_acpi_read, + .size = sizeof(struct hsmp_metric_table), }; static HSMP_CONST struct bin_attribute *hsmp_attr_list[] = { @@ -651,6 +718,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; @@ -659,37 +780,71 @@ 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. + */ + down_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; + ret = -ENODEV; + goto unlock; + } + + hsmp_pdev->sock = kcalloc(hsmp_pdev->num_sockets, + sizeof(*hsmp_pdev->sock), + GFP_KERNEL); + if (!hsmp_pdev->sock) { + ret = -ENOMEM; + goto unlock; } - hsmp_pdev->sock = devm_kcalloc(&pdev->dev, hsmp_pdev->num_sockets, - sizeof(*hsmp_pdev->sock), - GFP_KERNEL); - 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"); - return ret; + hsmp_acpi_probe_failure_cleanup(&pdev->dev); + goto unlock; } - 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"); - return ret; + hsmp_acpi_probe_failure_cleanup(&pdev->dev); + goto unlock; } - 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; + ret = 0; +unlock: + up_write(&hsmp_sock_rwsem); + + return ret; } #if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 11, 0) @@ -698,14 +853,28 @@ static void hsmp_acpi_remove(struct platform_device *pdev) static int hsmp_acpi_remove(struct platform_device *pdev) #endif { + 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; - } + down_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); + + up_write(&hsmp_sock_rwsem); #if LINUX_VERSION_CODE < KERNEL_VERSION(6, 11, 0) return 0; #endif diff --git a/amd_hsmp.h b/amd_hsmp.h index 1ddeccc..e534b96 100644 --- a/amd_hsmp.h +++ b/amd_hsmp.h @@ -53,21 +53,21 @@ enum hsmp_message_ids { HSMP_SET_XGMI_PSTATE_RANGE, /* 26h Set xGMI P-state range */ HSMP_CPU_RAIL_ISO_FREQ_POLICY, /* 27h Get/Set Cpu Iso frequency policy */ HSMP_DFC_ENABLE_CTRL, /* 28h Enable/Disable DF C-state */ - HSMP_PC6_ENABLE, /* 29h Get/Set PC6 Enable/Disable Status */ - HSMP_CC6_ENABLE, /* 2Ah Get/Set CC6 Enable/Disable Status */ + HSMP_PC6_ENABLE, /* 29h Get/Set PC6 enable/disable status */ + HSMP_CC6_ENABLE, /* 2Ah Get/Set CC6 enable/disable status */ HSMP_GET_RAPL_UNITS = 0x30, /* 30h Get scaling factor for energy */ HSMP_GET_RAPL_CORE_COUNTER, /* 31h Get core energy counter value */ HSMP_GET_RAPL_PACKAGE_COUNTER, /* 32h Get package energy counter value */ - HSMP_DIMM_SB_RD, /* 33h Get data from a specified device on the DIMM.*/ - HSMP_READ_CCD_POWER, /* 34h Get the average power consumed by CCD */ - HSMP_READ_TDELTA, /* 35h Get thermal solution behaviour */ - HSMP_GET_SVI3_VR_CTRL_TEMP, /* 36h Get temperature of SVI3 VR controlller rails */ - HSMP_GET_ENABLED_HSMP_CMDS, /* 37h Get/Set supported HSMP commands */ - HSMP_SET_GET_FLOOR_LIMIT, /* 38h Get/Set supported Floor Limit commands */ - HSMP_DIMM_SB_WR, /* 39h Set data to a specified device on the DIMM.*/ - HSMP_SDPS_LIMIT, /* 3Ah Get/Set SDPSLimit. */ + HSMP_DIMM_SB_RD, /* 33h Get DIMM sideband data */ + HSMP_READ_CCD_POWER, /* 34h Get average CCD power */ + HSMP_READ_TDELTA, /* 35h Get thermal behaviour */ + HSMP_GET_SVI3_VR_CTRL_TEMP, /* 36h Get SVI3 VR controller rail temp */ + HSMP_GET_ENABLED_HSMP_CMDS, /* 37h Get supported HSMP commands */ + HSMP_SET_GET_FLOOR_LIMIT, /* 38h Get/Set core floor frequency limit */ + HSMP_DIMM_SB_WR, /* 39h Set DIMM sideband data */ + HSMP_SDPS_LIMIT, /* 3Ah Get/Set SDPS limit */ HSMP_PQOS_TRAFFIC_PRIORITY, /* 3Bh Get/Set traffic priority */ - HSMP_PQOS_FLOATING_BW, /* 3Ch Get/Set floating bandwidth */ + HSMP_PQOS_FLOATING_BW, /* 3Ch Get/Set max floating bandwidth */ HSMP_MSG_ID_MAX, }; @@ -80,10 +80,10 @@ struct hsmp_message { }; enum hsmp_msg_type { - HSMP_RSVD = -1, - HSMP_SET = 0, - HSMP_GET = 1, - HSMP_SET_GET = 2, + HSMP_RSVD = -1, + HSMP_SET = 0, + HSMP_GET = 1, + HSMP_SET_GET = 2, }; enum hsmp_proto_versions { @@ -108,7 +108,8 @@ struct hsmp_msg_desc { * * Not supported messages would return -ENOMSG. */ -static const struct hsmp_msg_desc hsmp_msg_desc_table[] = { +static const struct hsmp_msg_desc hsmp_msg_desc_table[] + __attribute__((unused)) = { /* RESERVED */ {0, 0, HSMP_RSVD}, @@ -182,15 +183,24 @@ static const struct hsmp_msg_desc hsmp_msg_desc_table[] = { /* * HSMP_SET_XGMI_LINK_WIDTH, num_args = 1, response_sz = 0/1 - * input: args[0] = set/get XGMI Link width[31] + min link width[15:8] + max link width[7:0] - * output: args[0] = current min link width[15:8] + current max link width[7:0] + * input: args[0] = set/get XGMI Link width[31] (0 = set, 1 = get) + + * min link width[15:8] + max link width[7:0] + * Link width encoding: 0 = x4, 1 = x8, 2 = x16. + * On SET, max must be >= min. On GET, [15:0] are reserved. + * output: args[0] = reserved[31:16] + min link width[15:8] + + * max link width[7:0] */ {1, 1, HSMP_SET_GET}, /* - * HSMP_SET_DF_PSTATE, num_args = 1, response_sz = 0/1 - * input: args[0] = set/get df pstate[31] + df pstate[7:0] - * output: args[0] = APB Enabled/Disabled[8]+current df pstate[7:0] + * HSMP_SET_DF_PSTATE (APBDisable), num_args = 1, response_sz = 0/1 + * input: args[0] = set APB_DISABLE / get APB state[31] + * (0 = set & lock DF P-state, 1 = get) + + * reserved[30:8] + + * DF P-state[7:0] (0..2; reserved on GET) + * output: args[0] = reserved[31:9] + + * APB state[8] (1 = disabled, 0 = enabled) + + * locked DF P-state[7:0] if [8] = 1, else reserved */ {1, 1, HSMP_SET_GET}, @@ -260,7 +270,7 @@ static const struct hsmp_msg_desc hsmp_msg_desc_table[] = { /* * HSMP_GET_DIMM_THERMAL, num_args = 1, response_sz = 1 * input: args[0] = DIMM address[7:0] - * output: args[0] = temperature in degree celcius[31:21] + update rate in ms[16:8] + + * output: args[0] = temperature in degree celsius[31:21] + update rate in ms[16:8] + * DIMM address[7:0] */ {1, 1, HSMP_GET}, @@ -273,7 +283,7 @@ static const struct hsmp_msg_desc hsmp_msg_desc_table[] = { /* * HSMP_GET_CCLK_CORE_LIMIT, num_args = 1, response_sz = 1 - * input: args[0] = apic id of the core[31:0] + * input: args[0] = apic id [31:0] * output: args[0] = frequency in MHz[31:0] */ {1, 1, HSMP_GET}, @@ -318,16 +328,30 @@ static const struct hsmp_msg_desc hsmp_msg_desc_table[] = { {1, 1, HSMP_SET}, /* - * HSMP_SET_POWER_MODE, num_args = 1, response_sz = 0/1 - * input: args[0] = set/get power mode[31] + power efficiency mode[2:0] - * output: args[0] = current power efficiency mode[2:0] + * HSMP_SET_POWER_MODE (PwrEfficiencyModeSelection), + * num_args = 1, response_sz = 1 + * input: args[0] = set/get policy[31] (0 = set, 1 = get) + + * high util point[30:24] + + * low util point[23:17] + + * PPT limit[16:5] + + * reserved[4:3] + mode selection[2:0] + * [30:5] are valid only when [2:0] is a balanced core mode + * (4 or 5). [2:0] is reserved when getting (bit[31] = 1). + * output: args[0] same layout, [31] reserved, [2:0] = arbitrated + * current efficiency mode. */ {1, 1, HSMP_SET_GET}, /* - * HSMP_SET_PSTATE_MAX_MIN, num_args = 1, response_sz = 0/1 - * input: args[0] = set/get power mode[31] + min df pstate[15:8] + max df pstate[7:0] - * output: args[0] = min df pstate[15:8] + max df pstate[7:0] + * HSMP_SET_PSTATE_MAX_MIN (DfPstateRange), num_args = 1, response_sz = 0/1 + * input: args[0] = set/get DF P-state range[31] (0 = set, 1 = get) + + * reserved[30:16] + + * min DF P-state[15:8] + max DF P-state[7:0] + * DF P-state encoding: 0 = DFP0 (high performance), + * 1 = DFP1, 2 = DFP2 (low performance). + * [15:0] are reserved when getting (args[0] bit[31] = 1). + * output: args[0] = reserved[31:16] + min DF P-state[15:8] + + * max DF P-state[7:0] */ {1, 1, HSMP_SET_GET}, @@ -343,16 +367,18 @@ static const struct hsmp_msg_desc hsmp_msg_desc_table[] = { {0, 0, HSMP_GET}, /* - * HSMP_GET_METRIC_TABLE_DRAM_ADDR, num_args = 0, response_sz = 2 + * HSMP_GET_METRIC_TABLE_DRAM_ADDR, num_args = 0, response_sz = 3 * output: args[0] = lower 32 bits of the address * output: args[1] = upper 32 bits of the address + * output: args[2] = DRAM region size in bytes */ - {0, 2, HSMP_GET}, + {0, 3, HSMP_GET}, /* * HSMP_SET_XGMI_PSTATE_RANGE, num_args = 1, response_sz = 0/1 - * input: args[0] = set/get XGMI pstate range[31] + min xGMI p-state[15:8] + max xGMI state[7:0] - * output: args[0] = min xGMI p-state[15:8] + max xGMI state[7:0] + * input: args[0] = set/get xGMI p-state range[31] + + * min xGMI p-state[15:8] + max xGMI p-state[7:0] + * output: args[0] = min xGMI p-state[15:8] + max xGMI p-state[7:0] */ {1, 1, HSMP_SET_GET}, @@ -372,16 +398,26 @@ static const struct hsmp_msg_desc hsmp_msg_desc_table[] = { {1, 1, HSMP_SET_GET}, /* - * HSMP_PC6_REQUEST, num_args = 1, response_sz = 0/1 - * input: args[0] = set/get PC6 control[31] + disable/enable PC6[0] - * output: args[0] = current PC6 control status[0] + * HSMP_PC6_ENABLE (Pc6Enable), num_args = 1, response_sz = 0/1 + * input: args[0] = set/get PC6 control[31] (0 = set, 1 = get) + + * reserved[30:1] + + * enable PC6[0] (0 = disable, 1 = enable; + * reserved on GET) + * output: args[0] = reserved[31:1] + current PC6 control[0] + * (last value configured via HSMP or APML) */ {1, 1, HSMP_SET_GET}, /* - * HSMP_CC6_REQUEST, num_args = 1, response_sz = 0/1 - * input: args[0] = set/get CC6 control[31] + disable/enable CC6[0] - * output: args[0] = current CC6 control status[0] + * HSMP_CC6_ENABLE (CC6Enable), num_args = 1, response_sz = 0/1 + * Configures CC6 enable for all cores; changing the setting does + * not by itself transition cores in or out of CC6. + * input: args[0] = set/get CC6 control[31] (0 = set, 1 = get) + + * reserved[30:1] + + * enable CC6[0] (0 = disable, 1 = enable; + * reserved on GET) + * output: args[0] = reserved[31:1] + current CC6 control[0] + * (last value configured via HSMP or APML) */ {1, 1, HSMP_SET_GET}, @@ -400,7 +436,7 @@ static const struct hsmp_msg_desc hsmp_msg_desc_table[] = { /* * HSMP_GET_RAPL_CORE_COUNTER, num_args = 1, response_sz = 1 - * input: args[0] = Apic id[15:0] + * input: args[0] = apic id[15:0] * output: args[0] = lower 32 bits of energy * output: args[1] = upper 32 bits of energy */ @@ -415,115 +451,85 @@ static const struct hsmp_msg_desc hsmp_msg_desc_table[] = { /* * HSMP_DIMM_SB_RD, num_args = 1, response_sz = 1 - * input: args[0] = - * [07:00] DIMM address - * [11:08] LID of device - * [22:12] Register offset in given reg space - * [23] Register space - * output: args[0] = [03:00] Read data byte + * input: args[0] = reg space[23] + reg offset[22:12] + + * device LID[11:8] + DIMM address[7:0] + * output: args[0] = read data byte[3:0] */ {1, 1, HSMP_GET}, /* * HSMP_READ_CCD_POWER, num_args = 1, response_sz = 1 - * input: args[0] = [15:00] ApicId of core - * output: args[0] = [31:00] CCD power(mWatts) + * input: args[0] = apic id of core[15:0] + * output: args[0] = CCD power(mWatts)[31:0] */ {1, 1, HSMP_GET}, /* * HSMP_READ_TDELTA, num_args = 0, response_sz = 1 - * input: None - * output: args[0] = [31:00] Thermal Behaviour + * output: args[0] = thermal behaviour[31:0] */ {0, 1, HSMP_GET}, /* * HSMP_GET_SVI3_VR_CTRL_TEMP, num_args = 1, response_sz = 1 - * input: args[0] = - * [00] Read SVI3 temperature data - * [03:01] SVI3 rail index - * output: args[0] = - * [30:28] SVI3 rail index - * [27:00] SVI3 rail temperature(degree C) + * input: args[0] = SVI3 rail index[3:1] + read temperature[0] + * output: args[0] = SVI3 rail index[30:28] + + * rail temperature in degree C[27:0] */ {1, 1, HSMP_GET}, /* * HSMP_GET_ENABLED_HSMP_CMDS, num_args = 1, response_sz = 3 - * input: args[0] = [00] HSMP command mask - * output: args[0], args[1], args[2] = status of HSMP command + * input: args[0] = HSMP command mask[0] + * output: status of HSMP command = args[0], args[1], args[2] */ {1, 3, HSMP_GET}, /* * HSMP_SET_GET_FLOOR_LIMIT, num_args = 1, response_sz = 1 - * input: args[0] = - * [31:30]=Set or Get: - * 00=Set the Floor frequency per core. - * 01=Set the Floor frequency for all cores. - * 10=Get the Floor frequency of a core. - * 11=Get the Effective Floor frequency per core. - * [29:28]=Reserved. - * [27:16]=ApicId. - * Note: DataIn[27:16] are Reserved if DataIn[31:30]==01. - * - * If DataIn[31]=0 - * [15:0]=Floor frequency limit. - * Else - * [15:0]=Reserved. - * - * output: args[0] = - * If DataIn[31:30]=11 - * [15:0]=Effective Floor frequency limit(MHz). - * Else - * [15:0]=Floor frequency limit (MHz). - * The output will be None if DataIn[31]=0. + * input: args[0] = op[31:30] + reserved[29:28] + + * apic id[27:16] + floor frequency MHz[15:0] + * op encoding: 00 = set per-core floor, + * 01 = set all-cores floor (apic id reserved), + * 10 = get per-core floor, + * 11 = get per-core effective floor. + * Floor frequency field is reserved on GET (bit[31] = 1). + * output: args[0] = floor frequency MHz[15:0] + * (effective for op 11, configured for op 10; + * reserved on SET) */ {1, 1, HSMP_SET_GET}, /* * HSMP_DIMM_SB_WR, num_args = 1, response_sz = 0 - * input: args[0] = - * [07:00] DIMM address - * [11:08] LID of device - * [22:12] Register offset in given reg space - * [23] Register space - * [31:24] Write Data - * output: None + * input: args[0] = write data[31:24] + reg space[23] + + * reg offset[22:12] + device LID[11:8] + + * DIMM address[7:0] */ - {1, 0, HSMP_SET}, + {1, 0, HSMP_SET}, /* * HSMP_SDPS_LIMIT, num_args = 1, response_sz = 1 - * input: args[0] = - * [30:00] SDPS Limit - * [31] Set/Get - * output: args[0] = - * [30:00] SDPS Limit + * input: args[0] = set/get SDPS limit[31] (0 = set, 1 = get) + + * SDPS limit[30:0] + * output: args[0] = SDPS limit[30:0] */ - {1, 1, HSMP_SET_GET}, + {1, 1, HSMP_SET_GET}, - /* + /* * HSMP_PQOS_TRAFFIC_PRIORITY, num_args = 1, response_sz = 1 - * input: args[0] = - * [31:30] Operation - * [27:26] Priority selector - * [21:20] Priority value - * [19:0] Input - * output: args[0] = Supported priorities or Priority val[1:0] + * input: args[0] = op[31:30] + priority sel[27:26] + + * priority val[21:20] + input[19:0] + * output: args[0] = supported priorities or priority val[1:0] */ {1, 1, HSMP_SET_GET}, /* * HSMP_PQOS_FLOATING_BW, num_args = 1, response_sz = 2 - * input: args[0] = - * [31] Operation - * [30:29] Sub-operation - * [28:0] Parameter - * output: args[0] = Discovery bits or Floating/Global memory BW (Gbps) - * output: args[1] = Reserved or - * config (drop adjustment, sampling delay, hysteresis) + * input: args[0] = op[31] + sub-op[30:29] + params[28:0] + * output: args[0] = discovery bits or floating/global memory BW (Gbps) + * output: args[1] = reserved or config (drop adj, sampling delay, + * hysteresis) */ {1, 2, HSMP_SET_GET}, }; @@ -608,90 +614,37 @@ struct hsmp_metric_table { __u32 gfxclk_frequency[8]; }; -#define F1A_M50_M5F_MAX_CORES_PER_CCD_32 32 -#define F1A_M50_M5F_MAX_FREQ_TABLE_SIZE 4 -#define F1A_M50_M5F_MAX_XGMI 8 -#define F1A_M50_M5F_MAX_PCIE 8 -#define F1A_M50_M5F_MAX_CCD 8 - -/* Metrics table (supported only with proto version 7) */ -struct hsmp_metric_table_f1a_m50_5f_iod { - __u32 num_active_ccds; - __u32 accumulation_counter; - - /* TEMPERATURE */ - __u64 max_socket_temperature_acc; - - /* POWER */ - __u32 socket_power_limit; - __u32 max_socket_power_limit; - __u64 socket_power_acc; - __u64 core_power_acc; - __u64 uncore_power_acc; - - /* ENERGY */ - __u64 timestamp; - __u64 socket_energy_acc; - __u64 core_energy_acc; - __u64 uncore_energy_acc; - - /* FREQUENCY */ - __u64 fclk_frequency_acc; - __u64 uclk_frequency_acc; - __u64 ddr_rate_acc; - __u64 lclk_frequency_acc[F1A_M50_M5F_MAX_FREQ_TABLE_SIZE]; - - /* FREQUENCY RANGE */ - __u32 fclk_frequency_table[F1A_M50_M5F_MAX_FREQ_TABLE_SIZE]; - __u32 uclk_frequency_table[F1A_M50_M5F_MAX_FREQ_TABLE_SIZE]; - __u32 ddr_rate_table[F1A_M50_M5F_MAX_FREQ_TABLE_SIZE]; - __u32 max_df_pstate_range; - __u32 min_df_pstate_range; - __u32 lclk_frequency_table[F1A_M50_M5F_MAX_FREQ_TABLE_SIZE]; - __u32 max_lclk_dpm_range; - __u32 min_lclk_dpm_range; - - /* XGMI */ - __u64 xgmi_bit_rate[F1A_M50_M5F_MAX_XGMI]; - __u64 xgmi_read_bandwidth[F1A_M50_M5F_MAX_XGMI]; - __u64 xgmi_write_bandwidth[F1A_M50_M5F_MAX_XGMI]; - - /* ACTIVITY */ - __u64 socket_c0_residency_acc; - __u64 socket_df_cstate_residency_acc; - __u64 dram_read_bandwidth_acc; - __u64 dram_write_bandwidth_acc; - __u32 max_dram_bandwidth; - __u64 pcie_bandwidth_acc[F1A_M50_M5F_MAX_PCIE]; - - /* THROTTLERS */ - __u32 prochot_residency_acc; - __u32 ppt_residency_acc; - __u32 thm_residency_acc; - __u32 vrhot_residency_acc; - __u32 cpu_tdc_residency_acc; - __u32 soc_tdc_residency_acc; - __u32 io_mem_tdc_residency_acc; - __u32 fit_residency_acc; -}; - -struct hsmp_metric_table_f1a_m50_5f_ccd { - __u32 core_apicid_of_thread0[F1A_M50_M5F_MAX_CORES_PER_CCD_32]; - __u64 core_c0[F1A_M50_M5F_MAX_CORES_PER_CCD_32]; - __u64 core_cc1[F1A_M50_M5F_MAX_CORES_PER_CCD_32]; - __u64 core_cc6[F1A_M50_M5F_MAX_CORES_PER_CCD_32]; - __u64 core_frequency[F1A_M50_M5F_MAX_CORES_PER_CCD_32]; - __u64 core_frequency_effective[F1A_M50_M5F_MAX_CORES_PER_CCD_32]; - __u64 core_power[F1A_M50_M5F_MAX_CORES_PER_CCD_32]; -}; - -/* - * Future processors within the same family and model may support a - * variable number of CCDs and cores +/** + * 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_metric_table_f1a_m50_5f { - struct hsmp_metric_table_f1a_m50_5f_iod iod; - struct hsmp_metric_table_f1a_m50_5f_ccd ccd[F1A_M50_M5F_MAX_CCD]; +struct hsmp_telemetry_data { + __u64 buf; + __u32 size; + __u16 sock_ind; + __u16 reserved; }; /* Reset to default packing */ @@ -703,4 +656,16 @@ int hsmp_send_message(struct hsmp_message *msg); #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/amd_hsmp.rst b/amd_hsmp.rst index b77b888..fa1fc24 100644 --- a/amd_hsmp.rst +++ b/amd_hsmp.rst @@ -4,23 +4,38 @@ AMD HSMP interface ============================================ -Newer Fam19h EPYC server line of processors from AMD support system -management functionality via HSMP (Host System Management Port). +Newer Fam19h(model 0x00-0x1f, 0x30-0x3f, 0x90-0x9f, 0xa0-0xaf), +Fam1Ah(model 0x00-0x1f) EPYC server line of processors from AMD support +system management functionality via HSMP (Host System Management Port). The Host System Management Port (HSMP) is an interface to provide OS-level software with access to system management functions via a set of mailbox registers. More details on the interface can be found in chapter -"7 Host System Management Port (HSMP)" of the following PPR -https://www.amd.com/system/files/TechDocs/55898_B1_pub_0.50.zip +"7 Host System Management Port (HSMP)" of the family/model PPR +Eg: https://docs.amd.com/v/u/en-US/55898_B1_pub_0_50 + + +HSMP interface is supported on EPYC line of server CPUs and MI300A (APU). HSMP device ============================================ -amd_hsmp driver under the drivers/platforms/x86/ creates miscdevice -/dev/hsmp to let user space programs run hsmp mailbox commands. +amd_hsmp driver under drivers/platforms/x86/amd/hsmp/ has separate driver files +for ACPI object based probing, platform device based probing and for the common +code for these two drivers. + +Kconfig option CONFIG_AMD_HSMP_PLAT compiles plat.c and creates amd_hsmp.ko. +Kconfig option CONFIG_AMD_HSMP_ACPI compiles acpi.c and creates hsmp_acpi.ko. +Selecting any of these two configs automatically selects CONFIG_AMD_HSMP. This +compiles common code hsmp.c and creates hsmp_common.ko module. + +Both the ACPI and plat drivers create the miscdevice /dev/hsmp to let +user space programs run hsmp mailbox commands. + +The ACPI object format supported by the driver is defined below. $ ls -al /dev/hsmp crw-r--r-- 1 root root 10, 123 Jan 21 21:41 /dev/hsmp @@ -38,15 +53,106 @@ In-kernel integration: function hsmp_send_message(). * Locking across callers is taken care by the driver. -Features support by the interface include monitor and/or control of -a. boostlimit -b. current power, power limit, max power limit -c. c0 residency -d. prochot status -e. clocks (fclk, mclk and cclk) -f. ddr bandwidth, utilization -g. data fabric P-state +HSMP sysfs interface +==================== + +1. Metrics table binary sysfs + +AMD MI300A MCM provides GET_METRICS_TABLE message to retrieve +most of the system management information from SMU in one go. + +The metrics table is made available as hexadecimal sysfs binary file +under per socket sysfs directory created at +/sys/devices/platform/amd_hsmp/socket%d/metrics_bin + +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. + +2. HSMP telemetry sysfs files + +Following sysfs files are available at /sys/devices/platform/AMDI0097:0X/. + +* c0_residency_input: Percentage of cores in C0 state. +* prochot_status: Reports 1 if the processor is at thermal threshold value, + 0 otherwise. +* smu_fw_version: SMU firmware version. +* protocol_version: HSMP interface version. +* ddr_max_bw: Theoretical maximum DDR bandwidth in GB/s. +* ddr_utilised_bw_input: Current utilized DDR bandwidth in GB/s. +* ddr_utilised_bw_perc_input(%): Percentage of current utilized DDR bandwidth. +* mclk_input: Memory clock in MHz. +* fclk_input: Fabric clock in MHz. +* clk_fmax: Maximum frequency of socket in MHz. +* clk_fmin: Minimum frequency of socket in MHz. +* cclk_freq_limit_input: Core clock frequency limit per socket in MHz. +* pwr_current_active_freq_limit: Current active frequency limit of socket + in MHz. +* pwr_current_active_freq_limit_source: Source of current active frequency + limit. + +ACPI device object format +========================= +The ACPI object format expected from the amd_hsmp driver +for socket with ID00 is given below:: + + Device(HSMP) + { + Name(_HID, "AMDI0097") + Name(_UID, "ID00") + Name(HSE0, 0x00000001) + Name(RBF0, ResourceTemplate() + { + Memory32Fixed(ReadWrite, 0xxxxxxx, 0x00100000) + }) + Method(_CRS, 0, NotSerialized) + { + Return(RBF0) + } + Method(_STA, 0, NotSerialized) + { + If(LEqual(HSE0, One)) + { + Return(0x0F) + } + Else + { + Return(Zero) + } + } + Name(_DSD, Package(2) + { + Buffer(0x10) + { + 0x9D, 0x61, 0x4D, 0xB7, 0x07, 0x57, 0xBD, 0x48, + 0xA6, 0x9F, 0x4E, 0xA2, 0x87, 0x1F, 0xC2, 0xF6 + }, + Package(3) + { + Package(2) {"MsgIdOffset", 0x00010934}, + Package(2) {"MsgRspOffset", 0x00010980}, + Package(2) {"MsgArgOffset", 0x000109E0} + } + }) + } + +HSMP HWMON interface +==================== +HSMP power sensors are registered with the hwmon interface. A separate hwmon +directory is created for each socket and the following files are generated +within the hwmon directory. +- power1_input (read only) +- power1_cap_max (read only) +- power1_cap (read, write) An example ========== @@ -55,6 +161,7 @@ To access hsmp device from a C program. First, you need to include the headers:: #include + Which defines the supported messages/message IDs. Next thing, open the device file, as follows:: @@ -67,27 +174,52 @@ 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:: struct hsmp_message { - __u32 msg_id; /* Message ID */ - __u16 num_args; /* Number of arguments in message */ - __u16 response_sz; /* Number of expected response words */ - __u32 args[HSMP_MAX_MSG_LEN]; /* Argument(s) */ - __u32 response[HSMP_MAX_MSG_LEN]; /* Response word(s) */ - __u16 sock_ind; /* socket number */ + __u32 msg_id; /* Message ID */ + __u16 num_args; /* Number of input argument words in message */ + __u16 response_sz; /* Number of expected output/response words */ + __u32 args[HSMP_MAX_MSG_LEN]; /* argument/response buffer */ + __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. -More details on the interface can be found in chapter -"7 Host System Management Port (HSMP)" of the following PPR -https://www.amd.com/system/files/TechDocs/55898_B1_pub_0.50.zip +More details on the interface and message definitions can be found in chapter +"7 Host System Management Port (HSMP)" of the respective family/model PPR +eg: https://docs.amd.com/v/u/en-US/55898_B1_pub_0_50 User space C-APIs are made available by linking against the esmi library, -which is provided by the E-SMS project https://developer.amd.com/e-sms/. +which is provided by the E-SMS project https://www.amd.com/en/developer/e-sms.html. See: https://github.com/amd/esmi_ib_library diff --git a/hsmp.c b/hsmp.c index eae0edd..6667b68 100644 --- a/hsmp.c +++ b/hsmp.c @@ -18,9 +18,15 @@ #endif #include #include +#include +#include +#include +#include +#include #include -#include +#include #include +#include #include "hsmp.h" #include "amd_hsmp.h" /* this will come from linux kernel as UAPI header */ @@ -48,6 +54,21 @@ 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); +#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 13, 0) +EXPORT_SYMBOL_NS_GPL(hsmp_sock_rwsem, "AMD_HSMP"); +#else +EXPORT_SYMBOL_NS_GPL(hsmp_sock_rwsem, AMD_HSMP); +#endif + /* * Send a message to the HSMP port via PCI-e config space registers * or by writing to MMIO space. @@ -125,7 +146,7 @@ static int __hsmp_send_message(struct hsmp_socket *sock, struct hsmp_message *ms } if (unlikely(mbox_status == HSMP_STATUS_NOT_READY)) { - dev_err(sock->dev, "Message ID 0x%X failure : SMU tmeout (status = 0x%X)\n", + dev_err(sock->dev, "Message ID 0x%X failure : SMU timeout (status = 0x%X)\n", msg->msg_id, mbox_status); return -ETIMEDOUT; } else if (unlikely(mbox_status == HSMP_ERR_INVALID_MSG)) { @@ -190,28 +211,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); @@ -220,7 +246,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) @@ -232,6 +280,25 @@ int hsmp_send_message(struct hsmp_message *msg) return ret; } + +int hsmp_send_message(struct hsmp_message *msg) +{ + int ret; + + /* + * 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. + */ + down_read(&hsmp_sock_rwsem); + + ret = hsmp_send_message_locked(msg); + + up_read(&hsmp_sock_rwsem); + + return ret; +} #if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 13, 0) EXPORT_SYMBOL_NS_GPL(hsmp_send_message, "AMD_HSMP"); #else @@ -280,7 +347,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; @@ -312,7 +379,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 }; @@ -328,6 +395,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: /* @@ -368,48 +448,203 @@ long hsmp_ioctl(struct file *fp, unsigned int cmd, unsigned long arg) return 0; } -/** - * hsmp_metric_tbl_read - Read metric table - * - * This function maintains ABI compatibility for external consumers. - * It reads from offset 0, which works for all metrics table formats. - * External modules using this function will continue to work without - * modification. +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. * - * Return: number of bytes read or negative error code + * 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 __user *arguser = (void __user *)arg; + struct hsmp_telemetry_data req; + struct hsmp_socket *sock; + void __user *user_buf; + unsigned int sock_ind; + size_t tbl_size; + void *kbuf = NULL; + 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. + */ + down_read(&hsmp_sock_rwsem); + + if (!hsmp_pdev.sock || req.sock_ind >= hsmp_pdev.num_sockets) { + ret = -ENODEV; + goto unlock; + } + + /* + * 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) { + ret = -ENODEV; + goto unlock; + } + + tbl_size = sock->metric_tbl_size; + if (!tbl_size) { + ret = -ENODEV; + goto unlock; + } -ssize_t hsmp_metric_tbl_read(struct hsmp_socket *sock, char *buf, - size_t size, loff_t off) + /* + * 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) { + ret = -EINVAL; + goto unlock; + } + + /* + * 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) { + ret = -ENOMEM; + goto unlock; + } + + ret = hsmp_metric_tbl_read_locked(sock, kbuf, tbl_size); + +unlock: + up_read(&hsmp_sock_rwsem); + + if (ret < 0) + goto free_kbuf; + + ret = 0; + if (copy_to_user(user_buf, kbuf, req.size)) + ret = -EFAULT; + +free_kbuf: + kvfree(kbuf); + + return ret; +} + +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 }; - size_t var_size, remaining; int ret; + lockdep_assert_held(&hsmp_sock_rwsem); + if (!sock || !buf) return -EINVAL; - if (off < 0 || off > hsmp_pdev.hsmp_table_size) { - dev_err(sock->dev, "Invalid offset\n"); + if (!sock->metric_tbl_addr) { + dev_err(sock->dev, "Metrics table address not available\n"); + return -ENOMEM; + } + + if (size != sock->metric_tbl_size) { + dev_err(sock->dev, "Wrong buffer size\n"); return -EINVAL; } - /* Compute remaining bytes using explicit cast to avoid signed/unsigned mixing */ - remaining = hsmp_pdev.hsmp_table_size - (size_t)off; - var_size = min_t(size_t, size, remaining); - if (off == 0) { - msg.msg_id = HSMP_GET_METRIC_TABLE; - msg.sock_ind = sock->sock_ind; + msg.msg_id = HSMP_GET_METRIC_TABLE; + msg.sock_ind = sock->sock_ind; - ret = hsmp_send_message(&msg); - if (ret) { - dev_err(sock->dev, "Failed to send HSMP_GET_METRIC_TABLE, ret: %d\n", ret); + /* + * 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. + */ + mutex_lock(&sock->metric_read_lock); + + ret = hsmp_send_message_locked(&msg); + if (ret) { + mutex_unlock(&sock->metric_read_lock); return ret; - } } - memcpy_fromio(buf, (u8 __iomem *)sock->metric_tbl_addr + off, var_size); + memcpy_fromio(buf, sock->metric_tbl_addr, size); + + mutex_unlock(&sock->metric_read_lock); - return var_size; + return size; +} + +ssize_t hsmp_metric_tbl_read(struct hsmp_socket *sock, char *buf, size_t size) +{ + ssize_t ret; + + down_read(&hsmp_sock_rwsem); + + ret = hsmp_metric_tbl_read_locked(sock, buf, size); + + up_read(&hsmp_sock_rwsem); + + return ret; } #if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 13, 0) EXPORT_SYMBOL_NS_GPL(hsmp_metric_tbl_read, "AMD_HSMP"); @@ -417,20 +652,65 @@ EXPORT_SYMBOL_NS_GPL(hsmp_metric_tbl_read, "AMD_HSMP"); EXPORT_SYMBOL_NS_GPL(hsmp_metric_tbl_read, AMD_HSMP); #endif +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); +} +#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 13, 0) +EXPORT_SYMBOL_NS_GPL(hsmp_init_metric_read_locks, "AMD_HSMP"); +#else +EXPORT_SYMBOL_NS_GPL(hsmp_init_metric_read_locks, AMD_HSMP); +#endif + +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); +} +#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 13, 0) +EXPORT_SYMBOL_NS_GPL(hsmp_destroy_metric_read_locks, "AMD_HSMP"); +#else +EXPORT_SYMBOL_NS_GPL(hsmp_destroy_metric_read_locks, AMD_HSMP); +#endif + +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; + } +} +#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 13, 0) +EXPORT_SYMBOL_NS_GPL(hsmp_unmap_metric_tbls, "AMD_HSMP"); +#else +EXPORT_SYMBOL_NS_GPL(hsmp_unmap_metric_tbls, AMD_HSMP); +#endif + int hsmp_get_tbl_dram_base(u16 sock_ind) { struct hsmp_socket *sock = &hsmp_pdev.sock[sock_ind]; - struct hsmp_message msg_tbl_ver = { 0 }; struct hsmp_message msg = { 0 }; phys_addr_t dram_addr; - u32 table_ver; + 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; @@ -443,48 +723,33 @@ int hsmp_get_tbl_dram_base(u16 sock_ind) dev_err(sock->dev, "Invalid DRAM address for metric table\n"); return -ENOMEM; } - - /* Get metric table version */ - msg_tbl_ver.sock_ind = sock_ind; - msg_tbl_ver.response_sz = hsmp_msg_desc_table[HSMP_GET_METRIC_TABLE_VER].response_sz; - msg_tbl_ver.msg_id = HSMP_GET_METRIC_TABLE_VER; - - ret = hsmp_send_message(&msg_tbl_ver); - if (ret) - return ret; - - table_ver = msg_tbl_ver.args[0]; - - hsmp_pdev.hsmp_table_size = 0; - /* Determine metric table size based on CPU family/model and table version */ - switch (boot_cpu_data.x86) { - case 0x1A: - if (boot_cpu_data.x86_model >= 0x50 && - boot_cpu_data.x86_model <= 0x5F && - table_ver == 0x00700000) { - hsmp_pdev.hsmp_table_size = sizeof(struct hsmp_metric_table_f1a_m50_5f); - } - break; - case 0x19: - if (boot_cpu_data.x86_model >= 0x90 && - boot_cpu_data.x86_model <= 0x9F) { - hsmp_pdev.hsmp_table_size = sizeof(struct hsmp_metric_table); - } - break; + /* + * 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; - if (!hsmp_pdev.hsmp_table_size) { - dev_err(sock->dev, - "Metric table not supported for F%02Xh_M%02Xh (table version: 0x%08X)\n", - boot_cpu_data.x86, boot_cpu_data.x86_model, table_ver); - return -EOPNOTSUPP; - } + /* 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 = devm_ioremap(sock->dev, dram_addr, hsmp_pdev.hsmp_table_size); + 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; } #if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 13, 0) @@ -502,7 +767,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]; @@ -525,6 +790,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; @@ -540,6 +813,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; } #if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 13, 0) EXPORT_SYMBOL_NS_GPL(hsmp_misc_deregister, "AMD_HSMP"); diff --git a/hsmp.h b/hsmp.h index bec23e2..5930902 100644 --- a/hsmp.h +++ b/hsmp.h @@ -15,9 +15,12 @@ #include #include #include +#include #include +#include #include #include +#include /* * Helper macros to handle API changes across kernel versions: @@ -49,7 +52,7 @@ #define HSMP_DEVNODE_NAME "hsmp" #define ACPI_HSMP_DEVICE_HID "AMDI0097" -#define DRIVER_VERSION "3.0" +#define DRIVER_VERSION "3.1" struct hsmp_mbaddr_info { u32 base_addr; @@ -63,8 +66,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 pci_dev *root; struct device *dev; @@ -77,8 +84,6 @@ struct hsmp_plat_device { struct hsmp_socket *sock; u32 proto_ver; u16 num_sockets; - bool is_probed; - size_t hsmp_table_size; }; int hsmp_cache_proto_ver(u16 sock_ind); @@ -87,7 +92,10 @@ 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); -ssize_t hsmp_metric_tbl_read(struct hsmp_socket *sock, char *buf, size_t size, loff_t off); +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) int hsmp_create_sensor(struct device *dev, u16 sock_ind); @@ -95,4 +103,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/plat.c b/plat.c index 6f28289..39646e4 100644 --- a/plat.c +++ b/plat.c @@ -17,10 +17,12 @@ #endif #include +#include #include #include #include #include +#include #include #include "hsmp.h" @@ -84,7 +86,7 @@ static ssize_t hsmp_metric_tbl_plat_read(struct file *filp, struct kobject *kobj sock = &hsmp_pdev->sock[sock_ind]; - return hsmp_metric_tbl_read(sock, buf, count, off); + return hsmp_metric_tbl_read(sock, buf, count); } #if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 13, 0) @@ -118,7 +120,12 @@ static umode_t hsmp_is_sock_attr_visible(struct kobject *kobj, * Static array of 8 + 1(for NULL) elements is created below * to create sysfs groups for sockets. * is_bin_visible function is used to show / hide the necessary groups. + * + * Validate the maximum number against MAX_AMD_SOCKETS. If this changes, + * then the attributes and groups below must be adjusted. */ +static_assert(MAX_AMD_SOCKETS == 8); + #define HSMP_BIN_ATTR(index, _list) \ static HSMP_CONST struct bin_attribute attr##index = { \ .attr = { .name = HSMP_METRICS_TABLE_NAME, .mode = 0444}, \ @@ -140,12 +147,11 @@ HSMP_BIN_ATTR(5, *sock5_attr_list); HSMP_BIN_ATTR(6, *sock6_attr_list); HSMP_BIN_ATTR(7, *sock7_attr_list); - #define HSMP_BIN_ATTR_GRP(index, _list, _name) \ static HSMP_CONST struct attribute_group sock##index##_attr_grp = { \ HSMP_BIN_ATTRS_FIELD = _list, \ .is_bin_visible = hsmp_is_sock_attr_visible, \ - .name = #_name, \ + .name = #_name, \ } HSMP_BIN_ATTR_GRP(0, sock0_attr_list, socket0); @@ -225,18 +231,38 @@ static int init_platform_device(struct device *dev) if (hsmp_pdev->proto_ver == HSMP_PROTO_VER6) { ret = hsmp_get_tbl_dram_base(i); if (ret) - dev_err(dev, "Failed to init metric table\n"); + dev_info(dev, "Failed to init metric table\n"); } /* Register with hwmon interface for reporting power */ ret = hsmp_create_sensor(dev, i); if (ret) - dev_err(dev, "Failed to register HSMP sensors with hwmon\n"); + dev_info(dev, "Failed to register HSMP sensors with hwmon\n"); } 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) +{ + down_write(&hsmp_sock_rwsem); + hsmp_unmap_metric_tbls(hsmp_pdev); + hsmp_destroy_metric_read_locks(hsmp_pdev); + hsmp_pdev->sock = NULL; + up_write(&hsmp_sock_rwsem); +} + static int hsmp_pltdrv_probe(struct platform_device *pdev) { int ret; @@ -247,7 +273,23 @@ static int hsmp_pltdrv_probe(struct platform_device *pdev) if (!hsmp_pdev->sock) return -ENOMEM; + 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. + */ + down_write(&hsmp_sock_rwsem); ret = init_platform_device(&pdev->dev); + up_write(&hsmp_sock_rwsem); if (ret) { dev_err(&pdev->dev, "Failed to init HSMP mailbox\n"); return ret; @@ -277,7 +319,7 @@ static int hsmp_pltdrv_remove(struct platform_device *pdev) static struct platform_driver amd_hsmp_driver = { .probe = hsmp_pltdrv_probe, - .remove = hsmp_pltdrv_remove, + .remove = hsmp_pltdrv_remove, .driver = { .name = DRIVER_NAME, .dev_groups = hsmp_groups, @@ -369,8 +411,8 @@ static int __init hsmp_plt_init(void) #else hsmp_pdev->num_sockets = amd_num_nodes(); #endif - if (!hsmp_pdev->num_sockets) { - pr_err("No CPU sockets detected\n"); + if (!hsmp_pdev->num_sockets || hsmp_pdev->num_sockets > MAX_AMD_SOCKETS) { + pr_err("Wrong number of sockets\n"); return ret; }