Rtwo/kernel/motorola/sm8550/drivers/soc/qcom/minidump_virtio.c
2025-09-30 19:22:48 -05:00

547 lines
13 KiB
C

// SPDX-License-Identifier: GPL-2.0-only
/*
* Copyright (c) 2024 Qualcomm Innovation Center, Inc. All rights reserved.
*/
#define pr_fmt(x) "virtio_minidump: " x
#include <linux/init.h>
#include <linux/export.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/platform_device.h>
#include <linux/err.h>
#include <linux/elf.h>
#include <linux/errno.h>
#include <linux/string.h>
#include <linux/slab.h>
#include <soc/qcom/minidump.h>
#include "debug_symbol.h"
#include "minidump_private.h"
#include "elf.h"
#include <linux/virtio.h>
#include <linux/virtio_types.h>
#include <linux/virtio_config.h>
#define MD_SS_UPDATE_REGION 0
#define MD_SS_ADD_REGION 1
#define MD_SS_REMOVE_REGION 2
#define VIRTIO_ID_MINIDUMP 0xC00D
#define MINIDUMP_MAX_NAME_LENGTH 14
#define MAX_ENTRY_NUM 200
struct virtio_minidump {
struct virtio_device *vdev;
struct virtqueue *vq;
struct completion rsp_avail;
struct mutex lock;
};
struct virtio_minidump_msg {
__virtio32 type;
u8 name[MINIDUMP_MAX_NAME_LENGTH];
__virtio64 phy_addr;
__virtio64 size;
__virtio32 result;
};
struct md_request {
struct md_region entry;
struct work_struct work;
enum minidump_entry_cmd minidump_cmd;
};
/* Protect elfheader and smem table from deferred calls contention */
static DEFINE_SPINLOCK(mdt_lock);
static DEFINE_RWLOCK(mdt_remove_lock);
static struct workqueue_struct *minidump_wq;
/* Set as globle variable */
static struct virtio_minidump *vmd;
static void md_virtio_add_work(struct md_region *entry)
{
struct virtio_minidump_msg *req, *rsp;
struct scatterlist sg[1];
unsigned int len;
int ret = 0;
if (!pfn_is_map_memory(virt_to_pfn(entry->virt_addr))) {
pr_err("%s: Invalid Phy address %llu\n", entry->name, entry->phys_addr);
return;
}
req = kzalloc(sizeof(struct virtio_minidump_msg), GFP_KERNEL);
if (!req) {
pr_err("%s: Alloc message memory fail\n", entry->name);
return;
}
strscpy(req->name, entry->name, sizeof(req->name));
req->phy_addr = cpu_to_virtio64(vmd->vdev, entry->phys_addr);
req->size = cpu_to_virtio64(vmd->vdev, entry->size);
req->type = cpu_to_virtio32(vmd->vdev, MD_SS_ADD_REGION);
sg_init_one(sg, req, sizeof(*req));
mutex_lock(&vmd->lock);
ret = virtqueue_add_outbuf(vmd->vq, sg, 1, req, GFP_KERNEL);
if (ret) {
pr_err("%s: fail to add output buffer\n", entry->name);
goto out;
}
virtqueue_kick(vmd->vq);
wait_for_completion(&vmd->rsp_avail);
rsp = virtqueue_get_buf(vmd->vq, &len);
if (!rsp) {
pr_err("%s: fail to get virtqueue buffer\n", entry->name);
goto out;
}
ret = virtio32_to_cpu(vmd->vdev, rsp->result);
out:
mutex_unlock(&vmd->lock);
kfree(req);
pr_debug("%s: %s return %d\n", __func__, entry->name, ret);
}
static void md_virtio_remove_work(struct md_region *entry)
{
struct virtio_minidump_msg *req, *rsp;
struct scatterlist sg[1];
unsigned int len;
int ret = 0;
req = kzalloc(sizeof(struct virtio_minidump_msg), GFP_KERNEL);
if (!req) {
pr_err("%s: Alloc message memory fail\n", entry->name);
return;
}
strscpy(req->name, entry->name, sizeof(req->name));
req->type = cpu_to_virtio32(vmd->vdev, MD_SS_REMOVE_REGION);
sg_init_one(sg, req, sizeof(*req));
mutex_lock(&vmd->lock);
ret = virtqueue_add_outbuf(vmd->vq, sg, 1, req, GFP_KERNEL);
if (ret) {
pr_err("%s: fail to add output buffer\n", entry->name);
goto out;
}
virtqueue_kick(vmd->vq);
wait_for_completion(&vmd->rsp_avail);
rsp = virtqueue_get_buf(vmd->vq, &len);
if (!rsp) {
pr_err("%s: fail to get virtqueue buffer\n", entry->name);
goto out;
}
ret = virtio32_to_cpu(vmd->vdev, rsp->result);
out:
mutex_unlock(&vmd->lock);
kfree(req);
pr_debug("%s: %s return %d\n", __func__, entry->name, ret);
}
static void md_virtio_update_work(struct md_region *entry)
{
struct virtio_minidump_msg *req, *rsp;
struct scatterlist sg[1];
unsigned int len;
int ret = 0;
req = kzalloc(sizeof(struct virtio_minidump_msg), GFP_KERNEL);
if (!req) {
pr_err("%s: Alloc message memory fail\n", entry->name);
return;
}
strscpy(req->name, entry->name, sizeof(req->name));
req->phy_addr = cpu_to_virtio64(vmd->vdev, entry->phys_addr);
req->size = cpu_to_virtio64(vmd->vdev, entry->size);
req->type = cpu_to_virtio32(vmd->vdev, MD_SS_UPDATE_REGION);
sg_init_one(sg, req, sizeof(*req));
mutex_lock(&vmd->lock);
ret = virtqueue_add_outbuf(vmd->vq, sg, 1, req, GFP_KERNEL);
if (ret) {
pr_err("%s: fail to add output buffer\n", entry->name);
goto out;
}
virtqueue_kick(vmd->vq);
wait_for_completion(&vmd->rsp_avail);
rsp = virtqueue_get_buf(vmd->vq, &len);
if (!rsp) {
pr_err("%s: fail to get virtqueue buffer\n", entry->name);
goto out;
}
ret = virtio32_to_cpu(vmd->vdev, rsp->result);
out:
mutex_unlock(&vmd->lock);
kfree(req);
pr_debug("%s: %s return %d\n", __func__, entry->name, ret);
}
static void minidump_work(struct work_struct *work)
{
struct md_request *vm_work =
container_of(work, struct md_request, work);
enum minidump_entry_cmd cmd;
cmd = vm_work->minidump_cmd;
switch (cmd) {
case MINIDUMP_UPDATE:
md_virtio_update_work(&vm_work->entry);
break;
case MINIDUMP_ADD:
md_virtio_add_work(&vm_work->entry);
break;
case MINIDUMP_REMOVE:
md_virtio_remove_work(&vm_work->entry);
break;
default:
pr_debug("No command for virtio minidump work\n");
break;
}
kfree(vm_work);
}
static int virtio_add_region(const struct md_region *entry)
{
struct md_request *vm_work;
/* alloc an entry for workqueue, need free in work */
vm_work = kzalloc(sizeof(*vm_work), GFP_ATOMIC);
if (!vm_work)
return -ENOMEM;
vm_work->entry = *entry;
vm_work->minidump_cmd = MINIDUMP_ADD;
INIT_WORK(&vm_work->work, minidump_work);
queue_work(minidump_wq, &vm_work->work);
return 0;
}
static int virtio_remove_region(const struct md_region *entry)
{
struct md_request *vm_work;
/* alloc an entry for workqueue, need free in work */
vm_work = kzalloc(sizeof(*vm_work), GFP_ATOMIC);
if (!vm_work)
return -ENOMEM;
vm_work->entry = *entry;
vm_work->minidump_cmd = MINIDUMP_REMOVE;
INIT_WORK(&vm_work->work, minidump_work);
queue_work(minidump_wq, &vm_work->work);
return 0;
}
static int virtio_update_region(const struct md_region *entry)
{
struct md_request *vm_work;
/* alloc an entry for workqueue, need free in work */
vm_work = kzalloc(sizeof(*vm_work), GFP_ATOMIC);
if (!vm_work)
return -ENOMEM;
vm_work->entry = *entry;
vm_work->minidump_cmd = MINIDUMP_UPDATE;
INIT_WORK(&vm_work->work, minidump_work);
queue_work(minidump_wq, &vm_work->work);
return 0;
}
static inline char *md_elf_lookup_string(struct elfhdr *hdr, int offset)
{
char *strtab = elf_str_table(hdr);
if ((strtab == NULL) || (minidump_elfheader.strtable_idx < offset))
return NULL;
return strtab + offset;
}
static int virtio_get_available_region(void)
{
return (MAX_ENTRY_NUM - md_num_regions);
}
/* Check available region count */
static int md_virtio_init_md_table(void)
{
int res;
res = virtio_get_available_region();
if (res < 0) {
pr_err("Get minidump info failed ret=%d\n", res);
return res;
}
pr_debug("Get available region count: %d\n", res);
minidump_wq = create_singlethread_workqueue("minidump_wq");
if (!minidump_wq) {
pr_err("Unable to initialize workqueue\n");
return -EINVAL;
}
return 0;
}
static int md_virtio_add_pending_entry(struct list_head *pending_list)
{
struct md_pending_region *pending_region, *tmp;
unsigned long flags;
/* Add pending entries to HLOS TOC */
list_for_each_entry_safe(pending_region, tmp, pending_list, list) {
virtio_add_region(&pending_region->entry);
spin_lock_irqsave(&mdt_lock, flags);
md_add_elf_header(&pending_region->entry);
list_del(&pending_region->list);
kfree(pending_region);
md_num_regions++;
spin_unlock_irqrestore(&mdt_lock, flags);
}
return 0;
}
static void md_virtio_reg_kelfhdr_entry(unsigned int elfh_size)
{
struct md_region *entry;
char *hdr_name = "KELF_HDR";
int ret;
entry = kzalloc(sizeof(struct md_region), GFP_KERNEL);
if (!entry)
return;
strscpy(entry->name, hdr_name, sizeof(entry->name));
entry->virt_addr = (u64)minidump_elfheader.ehdr;
entry->phys_addr = virt_to_phys(minidump_elfheader.ehdr);
entry->size = elfh_size;
ret = virtio_add_region(entry);
if (ret)
pr_err("Failed to register ELF header region\n");
kfree(entry);
}
static elf_addr_t md_virtio_get_md_table(void)
{
return (elf_addr_t)NULL;
}
static int md_virtio_remove_region(const struct md_region *entry)
{
int ret;
unsigned long flags;
ret = virtio_remove_region(entry);
if (ret)
return -EBUSY;
spin_lock_irqsave(&mdt_lock, flags);
write_lock(&mdt_remove_lock);
msm_minidump_clear_headers(entry);
md_num_regions--;
write_unlock(&mdt_remove_lock);
spin_unlock_irqrestore(&mdt_lock, flags);
return ret;
}
static int md_virtio_add_region(const struct md_region *entry, struct list_head *pending_list)
{
int ret;
unsigned long flags;
ret = virtio_add_region(entry);
if (ret)
return -EBUSY;
spin_lock_irqsave(&mdt_lock, flags);
md_add_elf_header(entry);
ret = md_num_regions;
md_num_regions++;
spin_unlock_irqrestore(&mdt_lock, flags);
return ret;
}
static void md_virtio_update_elf_header(int entryno, const struct md_region *entry)
{
struct elfhdr *hdr = minidump_elfheader.ehdr;
struct elf_shdr *shdr;
struct elf_phdr *phdr;
shdr = elf_section(hdr, entryno + 3);
phdr = elf_program(hdr, entryno + 1);
shdr->sh_addr = (elf_addr_t)entry->virt_addr;
phdr->p_vaddr = entry->virt_addr;
phdr->p_paddr = entry->phys_addr;
}
static int md_virtio_update_region(int regno, const struct md_region *entry)
{
int ret = 0;
unsigned long flags;
ret = virtio_update_region(entry);
if (ret)
return -EBUSY;
read_lock_irqsave(&mdt_remove_lock, flags);
md_virtio_update_elf_header(regno, entry);
read_unlock_irqrestore(&mdt_remove_lock, flags);
return ret;
}
static int md_virtio_get_available_region(void)
{
int res = -EBUSY;
unsigned long flags;
spin_lock_irqsave(&mdt_lock, flags);
res = virtio_get_available_region();
spin_unlock_irqrestore(&mdt_lock, flags);
return res;
}
static bool md_virtio_md_enable(void)
{
return true;
}
static struct md_region md_virtio_get_region(char *name)
{
struct md_region tmp = {0};
int i, j;
struct elfhdr *hdr = minidump_elfheader.ehdr;
struct elf_phdr *phdr;
struct elf_shdr *shdr;
char *hdr_name;
for (i = 0; i < hdr->e_shnum; i++) {
shdr = elf_section(hdr, i);
hdr_name = md_elf_lookup_string(hdr, shdr->sh_name);
if (hdr_name && !strcmp(hdr_name, name)) {
for (j = 0; j < hdr->e_phnum; j++) {
phdr = elf_program(hdr, j);
if (shdr->sh_addr == phdr->p_vaddr) {
strscpy(tmp.name, hdr_name,
sizeof(tmp.name));
tmp.phys_addr = phdr->p_vaddr;
tmp.virt_addr = phdr->p_paddr;
tmp.size = phdr->p_filesz;
goto out;
}
}
}
}
out:
return tmp;
}
static const struct md_ops md_virtio_ops = {
.init_md_table = md_virtio_init_md_table,
.add_pending_entry = md_virtio_add_pending_entry,
.reg_kelfhdr_entry = md_virtio_reg_kelfhdr_entry,
.get_md_table = md_virtio_get_md_table,
.remove_region = md_virtio_remove_region,
.add_region = md_virtio_add_region,
.update_region = md_virtio_update_region,
.get_available_region = md_virtio_get_available_region,
.md_enable = md_virtio_md_enable,
.get_region = md_virtio_get_region,
};
static struct md_init_data md_virtio_init_data = {
.ops = &md_virtio_ops,
};
/* virtqueue incoming data interrupt IRQ */
static void virtio_minidump_isr(struct virtqueue *vq)
{
struct virtio_minidump *vmd = vq->vdev->priv;
complete(&vmd->rsp_avail);
}
static int virtio_md_init_vqs(struct virtio_minidump *vmd)
{
struct virtqueue *vqs[1];
vq_callback_t *cbs[] = { virtio_minidump_isr };
static const char * const names[] = { "virtio_minidump_isr" };
int err;
err = virtio_find_vqs(vmd->vdev, 1, vqs, cbs, names, NULL);
if (err)
return err;
vmd->vq = vqs[0];
return 0;
}
static int minidump_virtio_driver_probe(struct virtio_device *vdev)
{
int ret = 0;
if (!virtio_has_feature(vdev, VIRTIO_F_VERSION_1))
return -ENODEV;
if (vmd)
return -EEXIST;
vmd = devm_kzalloc(&vdev->dev, sizeof(struct virtio_minidump),
GFP_KERNEL);
if (!vmd)
return -ENOMEM;
vmd->vdev = vdev;
vdev->priv = vmd;
mutex_init(&vmd->lock);
init_completion(&vmd->rsp_avail);
ret = virtio_md_init_vqs(vmd);
if (ret) {
dev_err(&vdev->dev, "fail to initialize virtqueue\n");
return ret;
}
virtio_device_ready(vdev);
ret = msm_minidump_driver_probe(&md_virtio_init_data);
if (ret) {
dev_err(&vdev->dev, "fail to probe driver\n");
return ret;
}
return ret;
}
static void minidump_virtio_driver_remove(struct virtio_device *vdev)
{
void *buf;
vdev->config->reset(vdev);
while ((buf = virtqueue_detach_unused_buf(vmd->vq)) != NULL)
kfree(buf);
vdev->config->del_vqs(vdev);
}
static const struct virtio_device_id id_table[] = {
{ VIRTIO_ID_MINIDUMP, VIRTIO_DEV_ANY_ID},
{ },
};
static unsigned int features[] = {
/* none */
};
static struct virtio_driver msm_minidump_virtio_driver = {
.feature_table = features,
.feature_table_size = ARRAY_SIZE(features),
.driver.name = KBUILD_MODNAME,
.driver.owner = THIS_MODULE,
.id_table = id_table,
.probe = minidump_virtio_driver_probe,
.remove = minidump_virtio_driver_remove,
};
module_virtio_driver(msm_minidump_virtio_driver);
MODULE_LICENSE("GPL");