diff --git a/drivers/soc/qcom/Kconfig b/drivers/soc/qcom/Kconfig index ed23a7a5a166..e653be8432bd 100644 --- a/drivers/soc/qcom/Kconfig +++ b/drivers/soc/qcom/Kconfig @@ -645,6 +645,15 @@ config GH_TLMM_VM_MEM_ACCESS Say Y here to compile the driver as a part of kernel or M to compile as a module. +config QCOM_RAMDUMP + tristate "Qualcomm Technologies, Inc. Ramdump driver" + default n + help + This option enables the QTI ramdump driver. The ramdump driver + provides APIs to collect ramdumps which can be extracted from + userspace. Say 'Y' here to enable this driver. It's safe to say + 'N' here if you don't plan on collecting ramdumps. + config QCOM_ICC_BWMON tristate "QCOM Interconnect Bandwidth Monitor driver" depends on ARCH_QCOM || COMPILE_TEST diff --git a/drivers/soc/qcom/Makefile b/drivers/soc/qcom/Makefile index 80416014f44f..ff0b827052e3 100644 --- a/drivers/soc/qcom/Makefile +++ b/drivers/soc/qcom/Makefile @@ -57,4 +57,5 @@ obj-$(CONFIG_QCOM_VA_MINIDUMP) += qcom_va_minidump.o obj-$(CONFIG_QCOM_LOGBUF_VENDOR_HOOKS) += qcom_logbuf_vh.o obj-$(CONFIG_QCOM_HUNG_TASK_ENH) += hung_task_enh.o obj-$(CONFIG_GH_TLMM_VM_MEM_ACCESS) += gh_tlmm_vm_mem_access.o -obj-$(CONFIG_QCOM_ICC_BWMON) += icc-bwmon.o \ No newline at end of file +obj-$(CONFIG_QCOM_RAMDUMP) += qcom_ramdump.o +obj-$(CONFIG_QCOM_ICC_BWMON) += icc-bwmon.o diff --git a/drivers/soc/qcom/qcom_ramdump.c b/drivers/soc/qcom/qcom_ramdump.c new file mode 100644 index 000000000000..924d4f292d24 --- /dev/null +++ b/drivers/soc/qcom/qcom_ramdump.c @@ -0,0 +1,229 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (c) 2020-2021, The Linux Foundation. All rights reserved. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define RAMDUMP_TIMEOUT 120000 + +struct qcom_ramdump_desc { + void *data; + struct completion dump_done; +}; + +static int enable_dump_collection; +module_param(enable_dump_collection, int, 0644); + +bool dump_enabled(void) +{ + return enable_dump_collection; +} +EXPORT_SYMBOL(dump_enabled); + +static ssize_t qcom_devcd_readv(char *buffer, loff_t offset, size_t count, + void *data, size_t datalen) +{ + struct qcom_ramdump_desc *desc = data; + + return memory_read_from_buffer(buffer, count, &offset, desc->data, datalen); +} + +static void qcom_devcd_freev(void *data) +{ + struct qcom_ramdump_desc *desc = data; + + vfree(desc->data); + complete(&desc->dump_done); +} + +static int qcom_devcd_dump(struct device *dev, void *data, size_t datalen, gfp_t gfp) +{ + struct qcom_ramdump_desc desc; + int ret; + + desc.data = data; + init_completion(&desc.dump_done); + + dev_coredumpm(dev, NULL, &desc, datalen, gfp, qcom_devcd_readv, qcom_devcd_freev); + + ret = wait_for_completion_timeout(&desc.dump_done, msecs_to_jiffies(RAMDUMP_TIMEOUT)); + if (!ret) + dev_err(dev, "ramdump collection timed out\n"); + + return ret ? 0 : -ETIMEDOUT; +} + +int qcom_dump(struct list_head *segs, struct device *dev) +{ + struct qcom_dump_segment *segment; + void *data; + void __iomem *ptr; + size_t data_size = 0; + size_t offset = 0; + + if (!segs || list_empty(segs)) + return -EINVAL; + + list_for_each_entry(segment, segs, node) { + pr_info("Got segment size %d\n", segment->size); + data_size += segment->size; + } + + data = vmalloc(data_size); + if (!data) + return -ENOMEM; + + list_for_each_entry(segment, segs, node) { + if (segment->va) + memcpy(data + offset, segment->va, segment->size); + else { + ptr = devm_ioremap(dev, segment->da, segment->size); + if (!ptr) { + dev_err(dev, + "invalid coredump segment (%pad, %zu)\n", + &segment->da, segment->size); + memset(data + offset, 0xff, segment->size); + } else + memcpy_fromio(data + offset, ptr, + segment->size); + } + offset += segment->size; + } + + return qcom_devcd_dump(dev, data, data_size, GFP_KERNEL); +} +EXPORT_SYMBOL(qcom_dump); + +int qcom_elf_dump(struct list_head *segs, struct device *dev) +{ + struct qcom_dump_segment *segment; + struct elf32_phdr *phdr; + struct elf32_hdr *ehdr; + size_t data_size; + size_t offset; + int phnum = 0; + void *data; + void __iomem *ptr; + + + if (!segs || list_empty(segs)) + return -EINVAL; + + data_size = sizeof(*ehdr); + list_for_each_entry(segment, segs, node) { + data_size += sizeof(*phdr) + segment->size; + + phnum++; + } + + data = vmalloc(data_size); + if (!data) + return -ENOMEM; + + pr_debug("Creating elf with size %d\n", data_size); + ehdr = data; + + memset(ehdr, 0, sizeof(*ehdr)); + memcpy(ehdr->e_ident, ELFMAG, SELFMAG); + ehdr->e_ident[EI_CLASS] = ELFCLASS32; + ehdr->e_ident[EI_DATA] = ELFDATA2LSB; + ehdr->e_ident[EI_VERSION] = EV_CURRENT; + ehdr->e_ident[EI_OSABI] = ELFOSABI_NONE; + ehdr->e_type = ET_CORE; + ehdr->e_machine = EM_NONE; + ehdr->e_version = EV_CURRENT; + ehdr->e_entry = 0; + ehdr->e_phoff = sizeof(*ehdr); + ehdr->e_ehsize = sizeof(*ehdr); + ehdr->e_phentsize = sizeof(*phdr); + ehdr->e_phnum = phnum; + + phdr = data + ehdr->e_phoff; + offset = ehdr->e_phoff + sizeof(*phdr) * ehdr->e_phnum; + list_for_each_entry(segment, segs, node) { + memset(phdr, 0, sizeof(*phdr)); + phdr->p_type = PT_LOAD; + phdr->p_offset = offset; + phdr->p_vaddr = segment->da; + phdr->p_paddr = segment->da; + phdr->p_filesz = segment->size; + phdr->p_memsz = segment->size; + phdr->p_flags = PF_R | PF_W | PF_X; + phdr->p_align = 0; + + if (segment->va) + memcpy(data + offset, segment->va, segment->size); + else { + ptr = devm_ioremap(dev, segment->da, segment->size); + if (!ptr) { + dev_err(dev, + "invalid coredump segment (%pad, %zu)\n", + &segment->da, segment->size); + memset(data + offset, 0xff, segment->size); + } else + memcpy_fromio(data + offset, ptr, + segment->size); + } + + offset += phdr->p_filesz; + phdr++; + } + + return qcom_devcd_dump(dev, data, data_size, GFP_KERNEL); +} +EXPORT_SYMBOL(qcom_elf_dump); + +int qcom_fw_elf_dump(struct firmware *fw, struct device *dev) +{ + const struct elf32_phdr *phdrs, *phdr; + const struct elf32_hdr *ehdr; + struct qcom_dump_segment *segment; + struct list_head head; + int i; + + ehdr = (struct elf32_hdr *)fw->data; + phdrs = (struct elf32_phdr *)(ehdr + 1); + INIT_LIST_HEAD(&head); + + for (i = 0; i < ehdr->e_phnum; i++) { + phdr = &phdrs[i]; + + if (phdr->p_type != PT_LOAD) + continue; + + if ((phdr->p_flags & QCOM_MDT_TYPE_MASK) == QCOM_MDT_TYPE_HASH) + continue; + + if (!phdr->p_memsz) + continue; + + + segment = kzalloc(sizeof(*segment), GFP_KERNEL); + if (!segment) + return -ENOMEM; + + segment->da = phdr->p_paddr; + segment->size = phdr->p_memsz; + + list_add_tail(&segment->node, &head); + } + qcom_elf_dump(&head, dev); + return 0; +} +EXPORT_SYMBOL(qcom_fw_elf_dump); + +MODULE_DESCRIPTION("Qualcomm Technologies, Inc. Ramdump driver"); +MODULE_LICENSE("GPL"); + diff --git a/include/soc/qcom/qcom_ramdump.h b/include/soc/qcom/qcom_ramdump.h new file mode 100644 index 000000000000..f2f508512f63 --- /dev/null +++ b/include/soc/qcom/qcom_ramdump.h @@ -0,0 +1,44 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Copyright (c) 2021, The Linux Foundation. All rights reserved. + */ + +#ifndef _QCOM_RAMDUMP_HEADER +#define _QCOM_RAMDUMP_HEADER +#include +#include + +struct device; + +struct qcom_dump_segment { + struct list_head node; + dma_addr_t da; + void *va; + size_t size; +}; + +#if IS_ENABLED(CONFIG_QCOM_RAMDUMP) +extern int qcom_elf_dump(struct list_head *segs, struct device *dev); +extern int qcom_dump(struct list_head *head, struct device *dev); +extern int qcom_fw_elf_dump(struct firmware *fw, struct device *dev); +extern bool dump_enabled(void); +#else +static inline int qcom_elf_dump(struct list_head *segs, struct device *dev) +{ + return -ENODEV; +} +static inline int qcom_dump(struct list_head *head, struct device *dev) +{ + return -ENODEV; +} +static inline int qcom_fw_elf_dump(struct firmware *fw, struct device *dev) +{ + return -ENODEV; +} +static inline bool dump_enabled(void) +{ + return false; +} +#endif /* CONFIG_QCOM_RAMDUMP */ + +#endif