From fb445935338405110baca8f541a2df3b4cb8d712 Mon Sep 17 00:00:00 2001 From: Prasanna Kumar T S M Date: Thu, 19 Mar 2026 23:03:06 -0700 Subject: [PATCH 1/4] soc: xilinx: Fix race condition in event registration The zynqmp_power driver registers handlers for suspend and subsystem restart events using register_event(). However, the work structures (zynqmp_pm_init_suspend_work and zynqmp_pm_init_restart_work) used by these handlers were allocated and initialized after the registration call. This created a race window where, if the firmware triggered an event immediately after registration but before allocation, the callback (suspend_event_callback or subsystem_restart_event_callback) would dereference a NULL pointer in work_pending(), leading to a crash. Fix this by allocating and initializing the work structures before registering the events. Fixes: fcf544ac6439 ("soc: xilinx: Add cb event for subsystem restart") Signed-off-by: Prasanna Kumar T S M Signed-off-by: Michal Simek Link: https://lore.kernel.org/r/20260320060306.1540928-1-ptsm@linux.microsoft.com --- drivers/soc/xilinx/zynqmp_power.c | 43 ++++++++++++------------------- 1 file changed, 17 insertions(+), 26 deletions(-) diff --git a/drivers/soc/xilinx/zynqmp_power.c b/drivers/soc/xilinx/zynqmp_power.c index 9085db1b480a..9dd938bd01d8 100644 --- a/drivers/soc/xilinx/zynqmp_power.c +++ b/drivers/soc/xilinx/zynqmp_power.c @@ -303,18 +303,18 @@ static int zynqmp_pm_probe(struct platform_device *pdev) * is not available to use) or -ENODEV(Xilinx Event Manager not compiled), * then use ipi-mailbox or interrupt method. */ + zynqmp_pm_init_suspend_work = devm_kzalloc(&pdev->dev, + sizeof(struct zynqmp_pm_work_struct), + GFP_KERNEL); + if (!zynqmp_pm_init_suspend_work) + return -ENOMEM; + + INIT_WORK(&zynqmp_pm_init_suspend_work->callback_work, + zynqmp_pm_init_suspend_work_fn); + ret = register_event(&pdev->dev, PM_INIT_SUSPEND_CB, 0, 0, false, suspend_event_callback); if (!ret) { - zynqmp_pm_init_suspend_work = devm_kzalloc(&pdev->dev, - sizeof(struct zynqmp_pm_work_struct), - GFP_KERNEL); - if (!zynqmp_pm_init_suspend_work) - return -ENOMEM; - - INIT_WORK(&zynqmp_pm_init_suspend_work->callback_work, - zynqmp_pm_init_suspend_work_fn); - ret = zynqmp_pm_get_family_info(&pm_family_code); if (ret < 0) return ret; @@ -326,14 +326,6 @@ static int zynqmp_pm_probe(struct platform_device *pdev) else return -ENODEV; - ret = register_event(&pdev->dev, PM_NOTIFY_CB, node_id, EVENT_SUBSYSTEM_RESTART, - false, subsystem_restart_event_callback); - if (ret) { - dev_err(&pdev->dev, "Failed to Register with Xilinx Event manager %d\n", - ret); - return ret; - } - zynqmp_pm_init_restart_work = devm_kzalloc(&pdev->dev, sizeof(struct zynqmp_pm_work_struct), GFP_KERNEL); @@ -342,19 +334,18 @@ static int zynqmp_pm_probe(struct platform_device *pdev) INIT_WORK(&zynqmp_pm_init_restart_work->callback_work, zynqmp_pm_subsystem_restart_work_fn); + + ret = register_event(&pdev->dev, PM_NOTIFY_CB, node_id, EVENT_SUBSYSTEM_RESTART, + false, subsystem_restart_event_callback); + if (ret) { + dev_err(&pdev->dev, "Failed to Register with Xilinx Event manager %d\n", + ret); + return ret; + } } else if (ret != -EACCES && ret != -ENODEV) { dev_err(&pdev->dev, "Failed to Register with Xilinx Event manager %d\n", ret); return ret; } else if (of_property_present(pdev->dev.of_node, "mboxes")) { - zynqmp_pm_init_suspend_work = - devm_kzalloc(&pdev->dev, - sizeof(struct zynqmp_pm_work_struct), - GFP_KERNEL); - if (!zynqmp_pm_init_suspend_work) - return -ENOMEM; - - INIT_WORK(&zynqmp_pm_init_suspend_work->callback_work, - zynqmp_pm_init_suspend_work_fn); client = devm_kzalloc(&pdev->dev, sizeof(*client), GFP_KERNEL); if (!client) return -ENOMEM; From fdee7c66c0d7b6869c36b9f9a915abf29ab5b550 Mon Sep 17 00:00:00 2001 From: Prasanna Kumar T S M Date: Thu, 19 Mar 2026 23:04:45 -0700 Subject: [PATCH 2/4] soc: xilinx: Shutdown and free rx mailbox channel A mbox rx channel is requested using mbox_request_channel_byname() in probe. In remove callback, the rx mailbox channel is cleaned up when the rx_chan is NULL due to incorrect condition check. The mailbox channel is not shutdown and it can receive messages even after the device removal. This leads to use after free. Also the channel resources are not freed. Fix this by checking the rx_chan correctly. Fixes: ffdbae28d9d1a ("drivers: soc: xilinx: Use mailbox IPI callback") Signed-off-by: Prasanna Kumar T S M Signed-off-by: Michal Simek Link: https://lore.kernel.org/r/20260320060445.1541017-1-ptsm@linux.microsoft.com --- drivers/soc/xilinx/zynqmp_power.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/drivers/soc/xilinx/zynqmp_power.c b/drivers/soc/xilinx/zynqmp_power.c index 9dd938bd01d8..370e61ac47d8 100644 --- a/drivers/soc/xilinx/zynqmp_power.c +++ b/drivers/soc/xilinx/zynqmp_power.c @@ -389,8 +389,10 @@ static void zynqmp_pm_remove(struct platform_device *pdev) { sysfs_remove_file(&pdev->dev.kobj, &dev_attr_suspend_mode.attr); - if (!rx_chan) + if (rx_chan) { mbox_free_channel(rx_chan); + rx_chan = NULL; + } } static const struct of_device_id pm_of_match[] = { From 8ebebccf1579f6ce92bde3ddbb13df12c080f647 Mon Sep 17 00:00:00 2001 From: Ronak Jain Date: Wed, 20 May 2026 02:36:53 -0700 Subject: [PATCH 3/4] Documentation: ABI: add sysfs interface for ZynqMP CSU registers Document the new sysfs interface that exposes Configuration Security Unit (CSU) registers through the zynqmp-firmware driver. The interface is available under: /sys/devices/platform/firmware:zynqmp-firmware/csu_registers/ The CSU registers are discovered at boot time using the PM_QUERY_DATA firmware API. The following registers are currently supported: - multiboot (CSU_MULTI_BOOT) - idcode (CSU_IDCODE, read-only) - pcap-status (CSU_PCAP_STATUS, read-only) Read operations use the existing IOCTL_READ_REG firmware interface, while write operations use IOCTL_MASK_WRITE_REG. Access control is enforced by the firmware. Write attempts to read-only registers are rejected by firmware even though the sysfs file permissions allow writes. Document the ABI entry accordingly. Signed-off-by: Ronak Jain Signed-off-by: Michal Simek Link: https://lore.kernel.org/r/20260520093654.3303917-2-ronak.jain@amd.com --- .../ABI/stable/sysfs-driver-firmware-zynqmp | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/Documentation/ABI/stable/sysfs-driver-firmware-zynqmp b/Documentation/ABI/stable/sysfs-driver-firmware-zynqmp index c3fec3c835af..ac8c2314deee 100644 --- a/Documentation/ABI/stable/sysfs-driver-firmware-zynqmp +++ b/Documentation/ABI/stable/sysfs-driver-firmware-zynqmp @@ -254,3 +254,36 @@ Description: The expected result is 500. Users: Xilinx + +What: /sys/devices/platform/firmware\:zynqmp-firmware/csu_registers/* +Date: May 2026 +KernelVersion: 7.2 +Contact: "Ronak Jain" +Description: + Read/Write CSU (Configuration Security Unit) registers. + + This interface provides dynamic access to CSU registers that are + discovered from the firmware at boot time using PM_QUERY_DATA API. + + The supported registers are: + + - multiboot: CSU_MULTI_BOOT register + - idcode: CSU_IDCODE register (read-only) + - pcap-status: CSU_PCAP_STATUS register (read-only) + + Read operations use the existing IOCTL_READ_REG API. + Write operations use the existing IOCTL_MASK_WRITE_REG API. + + The firmware enforces access control - read-only registers will reject + write attempts even though the sysfs permissions show write access. + + Usage for reading:: + + # cat /sys/devices/platform/firmware\:zynqmp-firmware/csu_registers/multiboot + # cat /sys/devices/platform/firmware\:zynqmp-firmware/csu_registers/idcode + + Usage for writing (mask and value are in hexadecimal):: + + # echo 0xFFFFFFF 0x0 > /sys/devices/platform/firmware\:zynqmp-firmware/csu_registers/multiboot + +Users: Xilinx/AMD From 47d7bca76dd4f36ba0525d761f247c76ec9e4b17 Mon Sep 17 00:00:00 2001 From: Ronak Jain Date: Wed, 20 May 2026 02:36:54 -0700 Subject: [PATCH 4/4] firmware: zynqmp: Add dynamic CSU register discovery and sysfs interface Add support for dynamically discovering and exposing Configuration Security Unit (CSU) registers through sysfs. Leverage the existing PM_QUERY_DATA API to discover available registers at runtime, making the interface flexible and maintainable. Key features: - Dynamic register discovery using PM_QUERY_DATA API * PM_QID_GET_NODE_COUNT: Query number of available registers * PM_QID_GET_NODE_NAME: Query register names by index - Automatic sysfs attribute creation under csu_registers/ group - Read operations via existing IOCTL_READ_REG API - Write operations via existing IOCTL_MASK_WRITE_REG API The sysfs interface is created at: /sys/devices/platform/firmware:zynqmp-firmware/csu_registers/ Currently supported registers include: - multiboot (CSU_MULTI_BOOT) - idcode (CSU_IDCODE, read-only) - pcap-status (CSU_PCAP_STATUS, read-only) The dynamic discovery approach allows firmware to control which registers are exposed without requiring kernel changes, improving maintainability and security. The firmware does not currently expose per-register access mode information, so the kernel cannot distinguish read-only registers from read-write ones at discovery time. All discovered registers are therefore created with sysfs mode 0644, and the firmware is responsible for rejecting writes to registers it treats as read-only (for example idcode and pcap-status); that error is propagated back to userspace from the store callback. If a per-register access-mode query is added to the firmware in the future, sysfs permissions can be tightened to match. CSU register discovery is an optional feature: on firmware that lacks support for PM_QID_GET_NODE_COUNT or PM_QID_GET_NODE_NAME, the probe returns gracefully without exposing any sysfs entries. To keep the memory footprint minimal on that path, partial devm allocations made during discovery are explicitly released on failure so that no memory lingers until device unbind when the feature is unavailable. Signed-off-by: Ronak Jain Signed-off-by: Michal Simek Link: https://lore.kernel.org/r/20260520093654.3303917-3-ronak.jain@amd.com --- MAINTAINERS | 10 + drivers/firmware/xilinx/Makefile | 2 +- drivers/firmware/xilinx/zynqmp-csu-reg.c | 258 +++++++++++++++++++++++ drivers/firmware/xilinx/zynqmp-csu-reg.h | 18 ++ drivers/firmware/xilinx/zynqmp.c | 6 + include/linux/firmware/xlnx-zynqmp.h | 4 +- 6 files changed, 296 insertions(+), 2 deletions(-) create mode 100644 drivers/firmware/xilinx/zynqmp-csu-reg.c create mode 100644 drivers/firmware/xilinx/zynqmp-csu-reg.h diff --git a/MAINTAINERS b/MAINTAINERS index 2fb1c75afd16..d46789a093d9 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -29237,6 +29237,16 @@ F: drivers/dma/xilinx/xdma.c F: include/linux/dma/amd_xdma.h F: include/linux/platform_data/amd_xdma.h +XILINX ZYNQMP CSU REGISTER DRIVER +M: Senthil Nathan Thangaraj +R: Michal Simek +R: Ronak Jain +L: linux-arm-kernel@lists.infradead.org (moderated for non-subscribers) +S: Maintained +F: Documentation/ABI/stable/sysfs-driver-firmware-zynqmp +F: drivers/firmware/xilinx/zynqmp-csu-reg.c +F: drivers/firmware/xilinx/zynqmp-csu-reg.h + XILINX ZYNQMP DPDMA DRIVER M: Laurent Pinchart L: dmaengine@vger.kernel.org diff --git a/drivers/firmware/xilinx/Makefile b/drivers/firmware/xilinx/Makefile index 8db0e66b6b7e..6203f41daaa6 100644 --- a/drivers/firmware/xilinx/Makefile +++ b/drivers/firmware/xilinx/Makefile @@ -1,5 +1,5 @@ # SPDX-License-Identifier: GPL-2.0 # Makefile for Xilinx firmwares -obj-$(CONFIG_ZYNQMP_FIRMWARE) += zynqmp.o zynqmp-ufs.o zynqmp-crypto.o +obj-$(CONFIG_ZYNQMP_FIRMWARE) += zynqmp.o zynqmp-ufs.o zynqmp-crypto.o zynqmp-csu-reg.o obj-$(CONFIG_ZYNQMP_FIRMWARE_DEBUG) += zynqmp-debug.o diff --git a/drivers/firmware/xilinx/zynqmp-csu-reg.c b/drivers/firmware/xilinx/zynqmp-csu-reg.c new file mode 100644 index 000000000000..6e11a9b019f7 --- /dev/null +++ b/drivers/firmware/xilinx/zynqmp-csu-reg.c @@ -0,0 +1,258 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Xilinx Zynq MPSoC CSU Register Access + * + * Copyright (C) 2026 Advanced Micro Devices, Inc. + * + * Michal Simek + * Ronak Jain + */ + +#include +#include +#include +#include + +#include "zynqmp-csu-reg.h" + +/* Node ID for CSU module in firmware */ +#define CSU_NODE_ID 0 + +/* Maximum number of CSU registers supported */ +#define MAX_CSU_REGS 50 + +/* Size of register name returned by firmware (3 u32 words = 12 bytes) */ +#define CSU_REG_NAME_LEN 12 + +/** + * struct zynqmp_csu_reg - CSU register information + * @id: Register index from firmware + * @name: Register name + * @attr: Device attribute for sysfs + */ +struct zynqmp_csu_reg { + u32 id; + char name[CSU_REG_NAME_LEN]; + struct device_attribute attr; +}; + +/** + * struct zynqmp_csu_data - Per-device CSU data + * @csu_regs: Array of CSU registers + * @csu_attr_group: Attribute group for sysfs + */ +struct zynqmp_csu_data { + struct zynqmp_csu_reg *csu_regs; + struct attribute_group csu_attr_group; +}; + +/** + * zynqmp_pm_get_node_count() - Get number of supported nodes via QUERY_DATA + * + * Return: Number of nodes on success, or negative error code + */ +static int zynqmp_pm_get_node_count(void) +{ + struct zynqmp_pm_query_data qdata = {0}; + u32 ret_payload[PAYLOAD_ARG_CNT]; + int ret; + + qdata.qid = PM_QID_GET_NODE_COUNT; + + ret = zynqmp_pm_query_data(qdata, ret_payload); + if (ret) + return ret; + + return ret_payload[1]; +} + +/** + * zynqmp_pm_get_node_name() - Get node name via QUERY_DATA + * @index: Register index + * @name: Buffer to store register name + * + * Return: 0 on success, error code otherwise + */ +static int zynqmp_pm_get_node_name(u32 index, char *name) +{ + struct zynqmp_pm_query_data qdata = {0}; + u32 ret_payload[PAYLOAD_ARG_CNT]; + int ret; + + qdata.qid = PM_QID_GET_NODE_NAME; + qdata.arg1 = index; + + ret = zynqmp_pm_query_data(qdata, ret_payload); + if (ret) + return ret; + + memcpy(name, &ret_payload[1], CSU_REG_NAME_LEN); + name[CSU_REG_NAME_LEN - 1] = '\0'; + + return 0; +} + +/** + * zynqmp_csu_reg_show() - Generic show function for all registers + * @dev: Device pointer + * @attr: Device attribute + * @buf: Output buffer + * + * Return: Number of bytes written to buffer, or error code + */ +static ssize_t zynqmp_csu_reg_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct zynqmp_csu_reg *reg; + u32 value; + int ret; + + /* Use container_of to get register directly */ + reg = container_of(attr, struct zynqmp_csu_reg, attr); + + ret = zynqmp_pm_sec_read_reg(CSU_NODE_ID, reg->id, &value); + if (ret) + return ret; + + return sysfs_emit(buf, "0x%08x\n", value); +} + +/** + * zynqmp_csu_reg_store() - Generic store function for writable registers + * @dev: Device pointer + * @attr: Device attribute + * @buf: Input buffer + * @count: Buffer size + * + * Format: "mask value" - both mask and value required + * Example: echo "0xFFFFFFFF 0x12345678" > register + * + * Return: count on success, error code otherwise + */ +static ssize_t zynqmp_csu_reg_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct zynqmp_csu_reg *reg; + u32 mask, value; + int ret; + + reg = container_of(attr, struct zynqmp_csu_reg, attr); + + if (sscanf(buf, "%x %x", &mask, &value) != 2) + return -EINVAL; + + ret = zynqmp_pm_sec_mask_write_reg(CSU_NODE_ID, reg->id, mask, value); + if (ret) + return ret; + + return count; +} + +/** + * zynqmp_csu_discover_registers() - Discover CSU registers from firmware + * @pdev: Platform device pointer + * + * This function uses PM_QUERY_DATA to discover all available CSU registers + * and creates sysfs group under /sys/devices/platform/firmware:zynqmp-firmware/ + * + * Return: 0 on success, error code otherwise + */ +int zynqmp_csu_discover_registers(struct platform_device *pdev) +{ + struct zynqmp_csu_data *csu_data; + struct attribute **attrs; + int count, ret, i; + + ret = zynqmp_pm_is_function_supported(PM_QUERY_DATA, PM_QID_GET_NODE_COUNT); + if (ret) { + dev_dbg(&pdev->dev, "CSU register discovery not supported by current firmware\n"); + return 0; + } + + ret = zynqmp_pm_is_function_supported(PM_QUERY_DATA, PM_QID_GET_NODE_NAME); + if (ret) { + dev_dbg(&pdev->dev, "CSU register name query not supported by current firmware\n"); + return 0; + } + + count = zynqmp_pm_get_node_count(); + if (count < 0) + return count; + if (count == 0) { + dev_dbg(&pdev->dev, "No nodes available from firmware\n"); + return 0; + } + + /* Validate count to prevent excessive memory allocation */ + if (count > MAX_CSU_REGS) { + dev_err(&pdev->dev, "Register count %d exceeds maximum %d\n", + count, MAX_CSU_REGS); + return -EINVAL; + } + + dev_dbg(&pdev->dev, "Discovered %d nodes from firmware\n", count); + + csu_data = devm_kzalloc(&pdev->dev, sizeof(*csu_data), GFP_KERNEL); + if (!csu_data) + return -ENOMEM; + + csu_data->csu_regs = devm_kcalloc(&pdev->dev, count, sizeof(*csu_data->csu_regs), + GFP_KERNEL); + if (!csu_data->csu_regs) { + devm_kfree(&pdev->dev, csu_data); + return -ENOMEM; + } + + attrs = devm_kcalloc(&pdev->dev, count + 1, sizeof(*attrs), GFP_KERNEL); + if (!attrs) { + devm_kfree(&pdev->dev, csu_data->csu_regs); + devm_kfree(&pdev->dev, csu_data); + return -ENOMEM; + } + + for (i = 0; i < count; i++) { + struct zynqmp_csu_reg *reg = &csu_data->csu_regs[i]; + struct device_attribute *dev_attr = ®->attr; + + reg->id = i; + + ret = zynqmp_pm_get_node_name(i, reg->name); + if (ret) { + dev_warn(&pdev->dev, "Failed to get name for register %d\n", i); + snprintf(reg->name, sizeof(reg->name), "csu_reg_%d", i); + } + + /* + * The firmware does not expose per-register access mode via + * PM_QUERY_DATA today, so the kernel cannot tell read-only + * registers from read-write ones at discovery time. Expose + * every register as 0644 and rely on the firmware to reject + * IOCTL_MASK_WRITE_REG on read-only registers; the error is + * propagated back to userspace from the store callback. + */ + sysfs_attr_init(&dev_attr->attr); + dev_attr->attr.name = reg->name; + dev_attr->attr.mode = 0644; + dev_attr->show = zynqmp_csu_reg_show; + dev_attr->store = zynqmp_csu_reg_store; + + attrs[i] = &dev_attr->attr; + + dev_dbg(&pdev->dev, "Register %d: id=%d name=%s\n", i, reg->id, reg->name); + } + + csu_data->csu_attr_group.name = "csu_registers"; + csu_data->csu_attr_group.attrs = attrs; + + ret = devm_device_add_group(&pdev->dev, &csu_data->csu_attr_group); + if (ret) { + devm_kfree(&pdev->dev, attrs); + devm_kfree(&pdev->dev, csu_data->csu_regs); + devm_kfree(&pdev->dev, csu_data); + } + + return ret; +} +EXPORT_SYMBOL_GPL(zynqmp_csu_discover_registers); diff --git a/drivers/firmware/xilinx/zynqmp-csu-reg.h b/drivers/firmware/xilinx/zynqmp-csu-reg.h new file mode 100644 index 000000000000..b12415db3496 --- /dev/null +++ b/drivers/firmware/xilinx/zynqmp-csu-reg.h @@ -0,0 +1,18 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Xilinx Zynq MPSoC CSU Register Access + * + * Copyright (C) 2026 Advanced Micro Devices, Inc. + * + * Michal Simek + * Ronak Jain + */ + +#ifndef __ZYNQMP_CSU_REG_H__ +#define __ZYNQMP_CSU_REG_H__ + +#include + +int zynqmp_csu_discover_registers(struct platform_device *pdev); + +#endif /* __ZYNQMP_CSU_REG_H__ */ diff --git a/drivers/firmware/xilinx/zynqmp.c b/drivers/firmware/xilinx/zynqmp.c index fbe8510f4927..b549d07f7497 100644 --- a/drivers/firmware/xilinx/zynqmp.c +++ b/drivers/firmware/xilinx/zynqmp.c @@ -27,6 +27,7 @@ #include #include +#include "zynqmp-csu-reg.h" #include "zynqmp-debug.h" /* Max HashMap Order for PM API feature check (1<<7 = 128) */ @@ -2120,6 +2121,11 @@ static int zynqmp_firmware_probe(struct platform_device *pdev) dev_err_probe(&pdev->dev, PTR_ERR(em_dev), "EM register fail with error\n"); } + /* Discover CSU registers dynamically */ + ret = zynqmp_csu_discover_registers(pdev); + if (ret) + dev_warn(&pdev->dev, "CSU register discovery failed: %d\n", ret); + return of_platform_populate(dev->of_node, NULL, NULL, dev); } diff --git a/include/linux/firmware/xlnx-zynqmp.h b/include/linux/firmware/xlnx-zynqmp.h index d70dcd462b44..a4b293eb96ce 100644 --- a/include/linux/firmware/xlnx-zynqmp.h +++ b/include/linux/firmware/xlnx-zynqmp.h @@ -3,7 +3,7 @@ * Xilinx Zynq MPSoC Firmware layer * * Copyright (C) 2014-2021 Xilinx - * Copyright (C) 2022 - 2025 Advanced Micro Devices, Inc. + * Copyright (C) 2022 - 2026 Advanced Micro Devices, Inc. * * Michal Simek * Davorin Mista @@ -262,6 +262,8 @@ enum pm_query_id { PM_QID_CLOCK_GET_NUM_CLOCKS = 12, PM_QID_CLOCK_GET_MAX_DIVISOR = 13, PM_QID_PINCTRL_GET_ATTRIBUTES = 15, + PM_QID_GET_NODE_NAME = 16, + PM_QID_GET_NODE_COUNT = 17, }; enum rpu_oper_mode {