linux/drivers/hwtracing/coresight/ultrasoc-smb.c
Leo Yan 0289ada4a3 coresight: Fix memory leak in coresight_alloc_device_name()
The memory leak detector reports:

  echo clear > /sys/kernel/debug/kmemleak
  modprobe coresight_funnel
  rmmod coresight_funnel

  # Scan memory leak and report it
  echo scan > /sys/kernel/debug/kmemleak
  cat /sys/kernel/debug/kmemleak
  unreferenced object 0xffff0008020c7200 (size 64):
    comm "modprobe", pid 410, jiffies 4295333721
    hex dump (first 32 bytes):
      d8 da fe 7e 09 00 ff ff e8 2e ff 7e 09 00 ff ff  ...~.......~....
      b0 6c ff 7e 09 00 ff ff 30 83 00 7f 09 00 ff ff  .l.~....0.......
    backtrace (crc 4116a690):
      kmemleak_alloc+0xd8/0xf8
      __kmalloc_node_track_caller_noprof+0x2c8/0x6f0
      krealloc_node_align_noprof+0x13c/0x2c8
      coresight_alloc_device_name+0xe4/0x158 [coresight]
      0xffffd327ecef8394
      0xffffd327ecef85ec
      amba_probe+0x118/0x1c8
      really_probe+0xc8/0x3f0
      __driver_probe_device+0x88/0x190
      driver_probe_device+0x44/0x120
      __driver_attach+0x100/0x238
      bus_for_each_dev+0x84/0xf0
      driver_attach+0x2c/0x40
      bus_add_driver+0x128/0x258
      driver_register+0x64/0x138
      __amba_driver_register+0x2c/0x48

The memory leak is caused by not freeing the device list that maintains
device indices.

This device list preserves stable device indices across unbind and
rebind device operations, so it does not share the same lifetime as a
device instances and must only be freed when the module is unloaded.

Some modules do not implement a module exit callback because they are
registered using module_platform_driver().  As a result, the device
list cannot be released during module exit for those modules.

Fix this by moving the device list into the core layer.  As a general
solution, instead of maintaining a static list in each driver, drivers
now allocate device lists via coresight_allocate_device_list() and
device indices via coresight_allocate_device_idx().

The list is released only when the core module is unloaded by calling
coresight_release_device_list(), avoiding the leak.

Fixes: 0f5f9b6ba9 ("coresight: Use platform agnostic names")
Reviewed-by: James Clark <james.clark@linaro.org>
Signed-off-by: Leo Yan <leo.yan@arm.com>
Signed-off-by: Suzuki K Poulose <suzuki.poulose@arm.com>
Link: https://lore.kernel.org/r/20260209-arm_coresight_refactor_dev_register-v4-1-62d6042f76f7@arm.com
2026-02-25 11:14:44 +00:00

610 lines
15 KiB
C

// SPDX-License-Identifier: (GPL-2.0 OR MIT)
/*
* Siemens System Memory Buffer driver.
* Copyright(c) 2022, HiSilicon Limited.
*/
#include <linux/atomic.h>
#include <linux/acpi.h>
#include <linux/circ_buf.h>
#include <linux/err.h>
#include <linux/fs.h>
#include <linux/module.h>
#include <linux/mod_devicetable.h>
#include <linux/platform_device.h>
#include "coresight-etm-perf.h"
#include "coresight-priv.h"
#include "ultrasoc-smb.h"
#define ULTRASOC_SMB_DSM_UUID "82ae1283-7f6a-4cbe-aa06-53e8fb24db18"
static bool smb_buffer_not_empty(struct smb_drv_data *drvdata)
{
u32 buf_status = readl(drvdata->base + SMB_LB_INT_STS_REG);
return FIELD_GET(SMB_LB_INT_STS_NOT_EMPTY_MSK, buf_status);
}
static void smb_update_data_size(struct smb_drv_data *drvdata)
{
struct smb_data_buffer *sdb = &drvdata->sdb;
u32 buf_wrptr;
buf_wrptr = readl(drvdata->base + SMB_LB_WR_ADDR_REG) -
sdb->buf_hw_base;
/* Buffer is full */
if (buf_wrptr == sdb->buf_rdptr && smb_buffer_not_empty(drvdata)) {
sdb->data_size = sdb->buf_size;
return;
}
/* The buffer mode is circular buffer mode */
sdb->data_size = CIRC_CNT(buf_wrptr, sdb->buf_rdptr,
sdb->buf_size);
}
/*
* The read pointer adds @nbytes bytes (may round up to the beginning)
* after the data is read or discarded, while needing to update the
* available data size.
*/
static void smb_update_read_ptr(struct smb_drv_data *drvdata, u32 nbytes)
{
struct smb_data_buffer *sdb = &drvdata->sdb;
sdb->buf_rdptr += nbytes;
sdb->buf_rdptr %= sdb->buf_size;
writel(sdb->buf_hw_base + sdb->buf_rdptr,
drvdata->base + SMB_LB_RD_ADDR_REG);
sdb->data_size -= nbytes;
}
static void smb_reset_buffer(struct smb_drv_data *drvdata)
{
struct smb_data_buffer *sdb = &drvdata->sdb;
u32 write_ptr;
/*
* We must flush and discard any data left in hardware path
* to avoid corrupting the next session.
* Note: The write pointer will never exceed the read pointer.
*/
writel(SMB_LB_PURGE_PURGED, drvdata->base + SMB_LB_PURGE_REG);
/* Reset SMB logical buffer status flags */
writel(SMB_LB_INT_STS_RESET, drvdata->base + SMB_LB_INT_STS_REG);
write_ptr = readl(drvdata->base + SMB_LB_WR_ADDR_REG);
/* Do nothing, not data left in hardware path */
if (!write_ptr || write_ptr == sdb->buf_rdptr + sdb->buf_hw_base)
return;
/*
* The SMB_LB_WR_ADDR_REG register is read-only,
* Synchronize the read pointer to write pointer.
*/
writel(write_ptr, drvdata->base + SMB_LB_RD_ADDR_REG);
sdb->buf_rdptr = write_ptr - sdb->buf_hw_base;
}
static int smb_open(struct inode *inode, struct file *file)
{
struct smb_drv_data *drvdata = container_of(file->private_data,
struct smb_drv_data, miscdev);
guard(raw_spinlock)(&drvdata->spinlock);
if (drvdata->reading)
return -EBUSY;
if (drvdata->csdev->refcnt)
return -EBUSY;
smb_update_data_size(drvdata);
drvdata->reading = true;
return 0;
}
static ssize_t smb_read(struct file *file, char __user *data, size_t len,
loff_t *ppos)
{
struct smb_drv_data *drvdata = container_of(file->private_data,
struct smb_drv_data, miscdev);
struct smb_data_buffer *sdb = &drvdata->sdb;
struct device *dev = &drvdata->csdev->dev;
ssize_t to_copy = 0;
if (!len)
return 0;
if (!sdb->data_size)
return 0;
to_copy = min(sdb->data_size, len);
/* Copy parts of trace data when read pointer wrap around SMB buffer */
if (sdb->buf_rdptr + to_copy > sdb->buf_size)
to_copy = sdb->buf_size - sdb->buf_rdptr;
if (copy_to_user(data, sdb->buf_base + sdb->buf_rdptr, to_copy)) {
dev_dbg(dev, "Failed to copy data to user\n");
return -EFAULT;
}
*ppos += to_copy;
smb_update_read_ptr(drvdata, to_copy);
if (!sdb->data_size)
smb_reset_buffer(drvdata);
dev_dbg(dev, "%zu bytes copied\n", to_copy);
return to_copy;
}
static int smb_release(struct inode *inode, struct file *file)
{
struct smb_drv_data *drvdata = container_of(file->private_data,
struct smb_drv_data, miscdev);
guard(raw_spinlock)(&drvdata->spinlock);
drvdata->reading = false;
return 0;
}
static const struct file_operations smb_fops = {
.owner = THIS_MODULE,
.open = smb_open,
.read = smb_read,
.release = smb_release,
};
static ssize_t buf_size_show(struct device *dev, struct device_attribute *attr,
char *buf)
{
struct smb_drv_data *drvdata = dev_get_drvdata(dev->parent);
return sysfs_emit(buf, "0x%lx\n", drvdata->sdb.buf_size);
}
static DEVICE_ATTR_RO(buf_size);
static struct attribute *smb_sink_attrs[] = {
coresight_simple_reg32(read_pos, SMB_LB_RD_ADDR_REG),
coresight_simple_reg32(write_pos, SMB_LB_WR_ADDR_REG),
coresight_simple_reg32(buf_status, SMB_LB_INT_STS_REG),
&dev_attr_buf_size.attr,
NULL
};
static const struct attribute_group smb_sink_group = {
.attrs = smb_sink_attrs,
.name = "mgmt",
};
static const struct attribute_group *smb_sink_groups[] = {
&smb_sink_group,
NULL
};
static void smb_enable_hw(struct smb_drv_data *drvdata)
{
writel(SMB_GLB_EN_HW_ENABLE, drvdata->base + SMB_GLB_EN_REG);
}
static void smb_disable_hw(struct smb_drv_data *drvdata)
{
writel(0x0, drvdata->base + SMB_GLB_EN_REG);
}
static void smb_enable_sysfs(struct coresight_device *csdev)
{
struct smb_drv_data *drvdata = dev_get_drvdata(csdev->dev.parent);
if (coresight_get_mode(csdev) != CS_MODE_DISABLED)
return;
smb_enable_hw(drvdata);
coresight_set_mode(csdev, CS_MODE_SYSFS);
}
static int smb_enable_perf(struct coresight_device *csdev,
struct coresight_path *path)
{
struct smb_drv_data *drvdata = dev_get_drvdata(csdev->dev.parent);
struct perf_output_handle *handle = path->handle;
struct cs_buffers *buf = etm_perf_sink_config(handle);
pid_t pid;
if (!buf)
return -EINVAL;
/* Get a handle on the pid of the target process */
pid = buf->pid;
/* Device is already in used by other session */
if (drvdata->pid != -1 && drvdata->pid != pid)
return -EBUSY;
if (drvdata->pid == -1) {
smb_enable_hw(drvdata);
drvdata->pid = pid;
coresight_set_mode(csdev, CS_MODE_PERF);
}
return 0;
}
static int smb_enable(struct coresight_device *csdev, enum cs_mode mode,
struct coresight_path *path)
{
struct smb_drv_data *drvdata = dev_get_drvdata(csdev->dev.parent);
int ret = 0;
guard(raw_spinlock)(&drvdata->spinlock);
/* Do nothing, the trace data is reading by other interface now */
if (drvdata->reading)
return -EBUSY;
/* Do nothing, the SMB is already enabled as other mode */
if (coresight_get_mode(csdev) != CS_MODE_DISABLED &&
coresight_get_mode(csdev) != mode)
return -EBUSY;
switch (mode) {
case CS_MODE_SYSFS:
smb_enable_sysfs(csdev);
break;
case CS_MODE_PERF:
ret = smb_enable_perf(csdev, path);
break;
default:
ret = -EINVAL;
}
if (ret)
return ret;
csdev->refcnt++;
dev_dbg(&csdev->dev, "Ultrasoc SMB enabled\n");
return ret;
}
static int smb_disable(struct coresight_device *csdev)
{
struct smb_drv_data *drvdata = dev_get_drvdata(csdev->dev.parent);
guard(raw_spinlock)(&drvdata->spinlock);
if (drvdata->reading)
return -EBUSY;
csdev->refcnt--;
if (csdev->refcnt)
return -EBUSY;
/* Complain if we (somehow) got out of sync */
WARN_ON_ONCE(coresight_get_mode(csdev) == CS_MODE_DISABLED);
smb_disable_hw(drvdata);
/* Dissociate from the target process. */
drvdata->pid = -1;
coresight_set_mode(csdev, CS_MODE_DISABLED);
dev_dbg(&csdev->dev, "Ultrasoc SMB disabled\n");
return 0;
}
static void *smb_alloc_buffer(struct coresight_device *csdev,
struct perf_event *event, void **pages,
int nr_pages, bool overwrite)
{
struct cs_buffers *buf;
int node;
node = (event->cpu == -1) ? NUMA_NO_NODE : cpu_to_node(event->cpu);
buf = kzalloc_node(sizeof(struct cs_buffers), GFP_KERNEL, node);
if (!buf)
return NULL;
buf->snapshot = overwrite;
buf->nr_pages = nr_pages;
buf->data_pages = pages;
buf->pid = task_pid_nr(event->owner);
return buf;
}
static void smb_free_buffer(void *config)
{
struct cs_buffers *buf = config;
kfree(buf);
}
static void smb_sync_perf_buffer(struct smb_drv_data *drvdata,
struct cs_buffers *buf,
unsigned long head)
{
struct smb_data_buffer *sdb = &drvdata->sdb;
char **dst_pages = (char **)buf->data_pages;
unsigned long to_copy;
long pg_idx, pg_offset;
pg_idx = head >> PAGE_SHIFT;
pg_offset = head & (PAGE_SIZE - 1);
while (sdb->data_size) {
unsigned long pg_space = PAGE_SIZE - pg_offset;
to_copy = min(sdb->data_size, pg_space);
/* Copy parts of trace data when read pointer wrap around */
if (sdb->buf_rdptr + to_copy > sdb->buf_size)
to_copy = sdb->buf_size - sdb->buf_rdptr;
memcpy(dst_pages[pg_idx] + pg_offset,
sdb->buf_base + sdb->buf_rdptr, to_copy);
pg_offset += to_copy;
if (pg_offset >= PAGE_SIZE) {
pg_offset = 0;
pg_idx++;
pg_idx %= buf->nr_pages;
}
smb_update_read_ptr(drvdata, to_copy);
}
smb_reset_buffer(drvdata);
}
static unsigned long smb_update_buffer(struct coresight_device *csdev,
struct perf_output_handle *handle,
void *sink_config)
{
struct smb_drv_data *drvdata = dev_get_drvdata(csdev->dev.parent);
struct smb_data_buffer *sdb = &drvdata->sdb;
struct cs_buffers *buf = sink_config;
unsigned long data_size;
bool lost = false;
if (!buf)
return 0;
guard(raw_spinlock)(&drvdata->spinlock);
/* Don't do anything if another tracer is using this sink. */
if (csdev->refcnt != 1)
return 0;
smb_disable_hw(drvdata);
smb_update_data_size(drvdata);
/*
* The SMB buffer may be bigger than the space available in the
* perf ring buffer (handle->size). If so advance the offset so
* that we get the latest trace data.
*/
if (sdb->data_size > handle->size) {
smb_update_read_ptr(drvdata, sdb->data_size - handle->size);
lost = true;
}
data_size = sdb->data_size;
smb_sync_perf_buffer(drvdata, buf, handle->head);
if (!buf->snapshot && lost)
perf_aux_output_flag(handle, PERF_AUX_FLAG_TRUNCATED);
return data_size;
}
static const struct coresight_ops_sink smb_cs_ops = {
.enable = smb_enable,
.disable = smb_disable,
.alloc_buffer = smb_alloc_buffer,
.free_buffer = smb_free_buffer,
.update_buffer = smb_update_buffer,
};
static const struct coresight_ops cs_ops = {
.sink_ops = &smb_cs_ops,
};
static int smb_init_data_buffer(struct platform_device *pdev,
struct smb_data_buffer *sdb)
{
struct resource *res;
void *base;
res = platform_get_resource(pdev, IORESOURCE_MEM, SMB_BUF_ADDR_RES);
if (!res) {
dev_err(&pdev->dev, "SMB device failed to get resource\n");
return -EINVAL;
}
sdb->buf_rdptr = 0;
sdb->buf_hw_base = FIELD_GET(SMB_BUF_ADDR_LO_MSK, res->start);
sdb->buf_size = resource_size(res);
if (sdb->buf_size == 0)
return -EINVAL;
/*
* This is a chunk of memory, use classic mapping with better
* performance.
*/
base = devm_memremap(&pdev->dev, sdb->buf_hw_base, sdb->buf_size,
MEMREMAP_WB);
if (IS_ERR(base))
return PTR_ERR(base);
sdb->buf_base = base;
return 0;
}
static void smb_init_hw(struct smb_drv_data *drvdata)
{
smb_disable_hw(drvdata);
writel(SMB_LB_CFG_LO_DEFAULT, drvdata->base + SMB_LB_CFG_LO_REG);
writel(SMB_LB_CFG_HI_DEFAULT, drvdata->base + SMB_LB_CFG_HI_REG);
writel(SMB_GLB_CFG_DEFAULT, drvdata->base + SMB_GLB_CFG_REG);
writel(SMB_GLB_INT_CFG, drvdata->base + SMB_GLB_INT_REG);
writel(SMB_LB_INT_CTRL_CFG, drvdata->base + SMB_LB_INT_CTRL_REG);
}
static int smb_register_sink(struct platform_device *pdev,
struct smb_drv_data *drvdata)
{
struct coresight_platform_data *pdata = NULL;
struct coresight_desc desc = { 0 };
int ret;
pdata = coresight_get_platform_data(&pdev->dev);
if (IS_ERR(pdata))
return PTR_ERR(pdata);
desc.type = CORESIGHT_DEV_TYPE_SINK;
desc.subtype.sink_subtype = CORESIGHT_DEV_SUBTYPE_SINK_BUFFER;
desc.ops = &cs_ops;
desc.pdata = pdata;
desc.dev = &pdev->dev;
desc.groups = smb_sink_groups;
desc.name = coresight_alloc_device_name("ultra_smb", &pdev->dev);
if (!desc.name) {
dev_err(&pdev->dev, "Failed to alloc coresight device name");
return -ENOMEM;
}
desc.access = CSDEV_ACCESS_IOMEM(drvdata->base);
drvdata->csdev = coresight_register(&desc);
if (IS_ERR(drvdata->csdev))
return PTR_ERR(drvdata->csdev);
drvdata->miscdev.name = desc.name;
drvdata->miscdev.minor = MISC_DYNAMIC_MINOR;
drvdata->miscdev.fops = &smb_fops;
ret = misc_register(&drvdata->miscdev);
if (ret) {
coresight_unregister(drvdata->csdev);
dev_err(&pdev->dev, "Failed to register misc, ret=%d\n", ret);
}
return ret;
}
static void smb_unregister_sink(struct smb_drv_data *drvdata)
{
misc_deregister(&drvdata->miscdev);
coresight_unregister(drvdata->csdev);
}
static int smb_config_inport(struct device *dev, bool enable)
{
u64 func = enable ? 1 : 0;
union acpi_object *obj;
guid_t guid;
u64 rev = 0;
/*
* Using DSM calls to enable/disable ultrasoc hardwares on
* tracing path, to prevent ultrasoc packet format being exposed.
*/
if (guid_parse(ULTRASOC_SMB_DSM_UUID, &guid)) {
dev_err(dev, "Get GUID failed\n");
return -EINVAL;
}
obj = acpi_evaluate_dsm(ACPI_HANDLE(dev), &guid, rev, func, NULL);
if (!obj) {
dev_err(dev, "ACPI handle failed\n");
return -ENODEV;
}
ACPI_FREE(obj);
return 0;
}
static int smb_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct smb_drv_data *drvdata;
int ret;
drvdata = devm_kzalloc(dev, sizeof(*drvdata), GFP_KERNEL);
if (!drvdata)
return -ENOMEM;
drvdata->base = devm_platform_ioremap_resource(pdev, SMB_REG_ADDR_RES);
if (IS_ERR(drvdata->base)) {
dev_err(dev, "Failed to ioremap resource\n");
return PTR_ERR(drvdata->base);
}
smb_init_hw(drvdata);
ret = smb_init_data_buffer(pdev, &drvdata->sdb);
if (ret) {
dev_err(dev, "Failed to init buffer, ret = %d\n", ret);
return ret;
}
ret = smb_config_inport(dev, true);
if (ret)
return ret;
smb_reset_buffer(drvdata);
platform_set_drvdata(pdev, drvdata);
raw_spin_lock_init(&drvdata->spinlock);
drvdata->pid = -1;
ret = smb_register_sink(pdev, drvdata);
if (ret) {
smb_config_inport(&pdev->dev, false);
dev_err(dev, "Failed to register SMB sink\n");
return ret;
}
return 0;
}
static void smb_remove(struct platform_device *pdev)
{
struct smb_drv_data *drvdata = platform_get_drvdata(pdev);
smb_unregister_sink(drvdata);
smb_config_inport(&pdev->dev, false);
}
#ifdef CONFIG_ACPI
static const struct acpi_device_id ultrasoc_smb_acpi_match[] = {
{"HISI03A1", 0, 0, 0},
{}
};
MODULE_DEVICE_TABLE(acpi, ultrasoc_smb_acpi_match);
#endif
static struct platform_driver smb_driver = {
.driver = {
.name = "ultrasoc-smb",
.acpi_match_table = ACPI_PTR(ultrasoc_smb_acpi_match),
.suppress_bind_attrs = true,
},
.probe = smb_probe,
.remove = smb_remove,
};
module_platform_driver(smb_driver);
MODULE_DESCRIPTION("UltraSoc SMB CoreSight driver");
MODULE_LICENSE("Dual MIT/GPL");
MODULE_AUTHOR("Jonathan Zhou <jonathan.zhouwen@huawei.com>");
MODULE_AUTHOR("Qi Liu <liuqi115@huawei.com>");