diff --git a/drivers/soc/qcom/minidump_log.c b/drivers/soc/qcom/minidump_log.c index 67948134835d..ebe1bc0f67aa 100644 --- a/drivers/soc/qcom/minidump_log.c +++ b/drivers/soc/qcom/minidump_log.c @@ -44,6 +44,8 @@ #include #include +#include +#include #endif #ifdef CONFIG_QCOM_DYN_MINIDUMP_STACK @@ -111,6 +113,11 @@ struct seq_buf *md_meminfo_seq_buf; struct seq_buf *md_slabinfo_seq_buf; +#ifdef CONFIG_PAGE_OWNER +size_t md_pageowner_dump_size = SZ_2M; +char *md_pageowner_dump_addr; +#endif + /* Modules information */ #ifdef CONFIG_MODULES #define NUM_MD_MODULES 200 @@ -977,6 +984,10 @@ dump_rq: if (md_slabinfo_seq_buf) md_dump_slabinfo(); +#ifdef CONFIG_PAGE_OWNER + if (md_pageowner_dump_addr) + md_dump_pageowner(); +#endif md_in_oops_handler = false; return NOTIFY_DONE; } @@ -1039,6 +1050,139 @@ err_seq_buf: return ret; } +static bool md_register_memory_dump(int size, char *name) +{ + void *buffer_start; + struct page *page; + int ret; + + page = cma_alloc(dev_get_cma_area(NULL), size >> PAGE_SHIFT, + 0, false); + + if (!page) { + pr_err("Failed to allocate %s minidump, increase cma size\n", + name); + return false; + } + + buffer_start = page_to_virt(page); + ret = md_register_minidump_entry(name, (uintptr_t)buffer_start, + virt_to_phys(buffer_start), size); + if (ret < 0) { + cma_release(dev_get_cma_area(NULL), page, size >> PAGE_SHIFT); + return false; + } + + /* Complete registration before adding enteries */ + smp_mb(); + +#ifdef CONFIG_PAGE_OWNER + if (!strcmp(name, "PAGEOWNER")) + WRITE_ONCE(md_pageowner_dump_addr, buffer_start); +#endif + return true; +} + +static bool md_unregister_memory_dump(char *name) +{ + struct page *page; + struct md_region *mdr; + struct md_region md_entry; + + mdr = md_get_region(name); + if (!mdr) { + pr_err("minidump entry for %s not found\n", name); + return false; + } + strlcpy(md_entry.name, mdr->name, sizeof(md_entry.name)); + md_entry.virt_addr = mdr->virt_addr; + md_entry.phys_addr = mdr->phys_addr; + md_entry.size = mdr->size; + page = virt_to_page(mdr->virt_addr); + + if (msm_minidump_remove_region(&md_entry) < 0) + return false; + + cma_release(dev_get_cma_area(NULL), page, + (md_entry.size) >> PAGE_SHIFT); + return true; +} + +static void update_dump_size(char *name, size_t size, + char **addr, size_t *dump_size) +{ + if ((*dump_size) == 0) { + if (md_register_memory_dump(size * SZ_1M, + name)) { + *dump_size = size * SZ_1M; + pr_info_ratelimited("%s Minidump set to %zd MB size\n", + name, size); + } + return; + } + if (md_unregister_memory_dump(name)) { + *addr = NULL; + if (size == 0) { + *dump_size = 0; + pr_info_ratelimited("%s Minidump : disabled\n", name); + return; + } + if (md_register_memory_dump(size * SZ_1M, + name)) { + *dump_size = size * SZ_1M; + pr_info_ratelimited("%s Minidump : set to %zd MB\n", + name, size); + } else if (md_register_memory_dump(*dump_size, + name)) { + pr_info_ratelimited("%s Minidump : Fallback to %zd MB\n", + name, (*dump_size) / SZ_1M); + } else { + pr_err_ratelimited("%s Minidump : disabled, Can't fallback to %zd MB,\n", + name, (*dump_size) / SZ_1M); + *dump_size = 0; + } + } else { + pr_err_ratelimited("Failed to unregister %s Minidump\n", name); + } +} + +#ifdef CONFIG_PAGE_OWNER +static DEFINE_MUTEX(page_owner_dump_size_lock); + +static ssize_t page_owner_dump_size_write(struct file *file, + const char __user *ubuf, + size_t count, loff_t *offset) +{ + unsigned long long size; + + if (kstrtoull_from_user(ubuf, count, 0, &size)) { + pr_err_ratelimited("Invalid format for size\n"); + return -EINVAL; + } + mutex_lock(&page_owner_dump_size_lock); + update_dump_size("PAGEOWNER", size, + &md_pageowner_dump_addr, &md_pageowner_dump_size); + mutex_unlock(&page_owner_dump_size_lock); + return count; +} + +static ssize_t page_owner_dump_size_read(struct file *file, char __user *ubuf, + size_t count, loff_t *offset) +{ + char buf[100]; + + snprintf(buf, sizeof(buf), "%llu MB\n", + md_pageowner_dump_size / SZ_1M); + return simple_read_from_buffer(ubuf, count, offset, buf, strlen(buf)); +} + +static const struct file_operations proc_page_owner_dump_size_ops = { + .open = simple_open, + .write = page_owner_dump_size_write, + .read = page_owner_dump_size_read, +}; +#endif + static void md_register_panic_data(void) { md_register_panic_entries(MD_RUNQUEUE_PAGES, "KRUNQUEUE", @@ -1051,6 +1195,11 @@ static void md_register_panic_data(void) &md_meminfo_seq_buf); md_register_panic_entries(MD_SLABINFO_PAGES, "SLABINFO", &md_slabinfo_seq_buf); + if (is_page_owner_enabled()) { + md_register_memory_dump(md_pageowner_dump_size, "PAGEOWNER"); + debugfs_create_file("page_owner_dump_size_mb", 0400, NULL, NULL, + &proc_page_owner_dump_size_ops); + } } #ifdef CONFIG_MODULES diff --git a/include/soc/qcom/minidump.h b/include/soc/qcom/minidump.h index 38676514047f..dfa16445680e 100644 --- a/include/soc/qcom/minidump.h +++ b/include/soc/qcom/minidump.h @@ -72,4 +72,14 @@ extern void minidump_add_trace_event(char *buf, size_t size); #else static inline void minidump_add_trace_event(char *buf, size_t size) {} #endif +#ifdef CONFIG_PAGE_OWNER +extern size_t md_pageowner_dump_size; +extern char *md_pageowner_dump_addr; + +extern bool is_page_owner_enabled(void); +extern void md_dump_pageowner(void); +#else +static inline void md_dump_pageowner(void) {} +static inline bool is_page_owner_enabled(void) { return false; } +#endif #endif diff --git a/mm/page_owner.c b/mm/page_owner.c index 35673f47d8af..56213834b670 100644 --- a/mm/page_owner.c +++ b/mm/page_owner.c @@ -12,7 +12,10 @@ #include #include #include - +#ifdef CONFIG_QCOM_MINIDUMP_PANIC_DUMP +#include +#include +#endif #include "internal.h" /* @@ -423,6 +426,145 @@ err: return -ENOMEM; } +#ifdef CONFIG_QCOM_MINIDUMP_PANIC_DUMP + +static unsigned long page_owner_filter = 0xF; +static unsigned long page_owner_handles_size = SZ_16K; +static int nr_handles; +static LIST_HEAD(accounted_call_site_list); +static DEFINE_MUTEX(accounted_call_site_lock); +struct accounted_call_site { + struct list_head list; + char name[50]; +}; + +bool is_page_owner_enabled(void) +{ + return page_owner_enabled; +} + +static bool found_stack(depot_stack_handle_t handle, + char *md_pageowner_dump_addr, char *cur) +{ + int *handles, i; + + handles = (int *) (md_pageowner_dump_addr + + md_pageowner_dump_size - page_owner_handles_size); + + for (i = 0; i < nr_handles; i++) + if (handle == handles[i]) + return true; + + if ((handles + nr_handles) + < (int *)(md_pageowner_dump_addr + + md_pageowner_dump_size)) { + handles[nr_handles] = handle; + nr_handles += 1; + } else { + pr_err_ratelimited("Can't stores handles increase page_owner_handles_size\n"); + } + return false; +} + +static bool check_unaccounted(char *buf, ssize_t count, + struct page *page, depot_stack_handle_t handle) +{ + int i, ret = 0; + unsigned long *entries; + unsigned int nr_entries; + struct accounted_call_site *call_site; + + if ((page->flags & + ((1UL << PG_lru) | (1UL << PG_slab) | (1UL << PG_swapbacked)))) + return false; + + nr_entries = stack_depot_fetch(handle, &entries); + for (i = 0; i < nr_entries; i++) { + ret = scnprintf(buf, count, "%pS\n", + (void *)entries[i]); + if (ret == count) + return false; + + mutex_lock(&accounted_call_site_lock); + list_for_each_entry(call_site, + &accounted_call_site_list, list) { + if (strnstr(buf, call_site->name, + strlen(buf))) { + mutex_unlock(&accounted_call_site_lock); + return false; + } + } + mutex_unlock(&accounted_call_site_lock); + } + return true; +} + +static ssize_t +dump_page_owner_md(char *buf, size_t count, unsigned long pfn, + struct page *page, struct page_owner *page_owner, + depot_stack_handle_t handle) +{ + int i, bit, ret = 0; + unsigned long *entries; + unsigned int nr_entries; + + if (page_owner_filter == 0xF) + goto dump; + + for (bit = 1; page_owner_filter >= bit; bit *= 2) { + if (page_owner_filter & bit) { + switch (bit) { + case 0x1: + if (check_unaccounted(buf, count, page, handle)) + goto dump; + break; + case 0x2: + if (page->flags & (1UL << PG_slab)) + goto dump; + break; + case 0x4: + if (page->flags & (1UL << PG_swapbacked)) + goto dump; + break; + case 0x8: + if ((page->flags & (1UL << PG_lru)) && + ~(page->flags & (1UL << PG_swapbacked))) + goto dump; + break; + default: + break; + } + } + if (bit >= 0x8) + return ret; + } + + if (bit > page_owner_filter) + return ret; +dump: + nr_entries = stack_depot_fetch(handle, &entries); + if ((buf > (md_pageowner_dump_addr + + md_pageowner_dump_size - page_owner_handles_size)) + || !found_stack(handle, md_pageowner_dump_addr, buf)) { + ret = scnprintf(buf, count, "%lu %u %u\n", + pfn, handle, nr_entries); + if (ret == count) + goto err; + + for (i = 0; i < nr_entries; i++) { + ret += scnprintf(buf + ret, count - ret, + "%p\n", (void *)entries[i]); + if (ret == count) + goto err; + } + } else { + ret = scnprintf(buf, count, "%lu %u %u\n", pfn, handle, 0); + } +err: + return ret; +} +#endif + void __dump_page_owner(struct page *page) { struct page_ext *page_ext = lookup_page_ext(page); @@ -486,6 +628,12 @@ read_page_owner(struct file *file, char __user *buf, size_t count, loff_t *ppos) struct page_owner *page_owner; depot_stack_handle_t handle; +#ifdef CONFIG_QCOM_MINIDUMP_PANIC_DUMP + char *addr; + ssize_t size; + + addr = md_pageowner_dump_addr; +#endif if (!static_branch_unlikely(&page_owner_inited)) return -EINVAL; @@ -496,7 +644,8 @@ read_page_owner(struct file *file, char __user *buf, size_t count, loff_t *ppos) while (!pfn_valid(pfn) && (pfn & (MAX_ORDER_NR_PAGES - 1)) != 0) pfn++; - drain_all_pages(NULL); + if (file) + drain_all_pages(NULL); /* Find an allocated page */ for (; pfn < max_pfn; pfn++) { @@ -560,13 +709,34 @@ read_page_owner(struct file *file, char __user *buf, size_t count, loff_t *ppos) /* Record the next PFN to read in the file offset */ *ppos = (pfn - min_low_pfn) + 1; - return print_page_owner(buf, count, pfn, page, + if (file) { + return print_page_owner(buf, count, pfn, page, page_owner, handle); + } else { +#ifdef CONFIG_QCOM_MINIDUMP_PANIC_DUMP + size = dump_page_owner_md(addr, count, pfn, page, + page_owner, handle); + if (size == count) { + pr_err("pageowner minidump region exhausted\n"); + return 0; + } + count -= size; + addr += size; +#endif + } } - return 0; } +#ifdef CONFIG_QCOM_MINIDUMP_PANIC_DUMP +void md_dump_pageowner(void) +{ + loff_t k = 0; + + read_page_owner(NULL, NULL, md_pageowner_dump_size, &k); +} +#endif + static void init_pages_in_zone(pg_data_t *pgdat, struct zone *zone) { unsigned long pfn = zone->zone_start_pfn; @@ -664,6 +834,158 @@ static const struct file_operations proc_page_owner_operations = { .read = read_page_owner, }; +#ifdef CONFIG_QCOM_MINIDUMP_PANIC_DUMP +static ssize_t page_owner_filter_write(struct file *file, + const char __user *ubuf, + size_t count, loff_t *offset) +{ + unsigned long filter; + + if (kstrtoul_from_user(ubuf, count, 0, &filter)) { + pr_err_ratelimited("Invalid format for filter\n"); + return -EINVAL; + } + + if (filter & (~0xF)) { + pr_err_ratelimited("Invalid filter : use following filters or any combinations of these\n" + "0x1 - unaccounted\n" + "0x2 - slab\n" + "0x4 - Anon\n" + "0x8 - File\n"); + return -EINVAL; + } + page_owner_filter = filter; + return count; +} + +static ssize_t page_owner_filter_read(struct file *file, char __user *ubuf, + size_t count, loff_t *offset) +{ + char buf[64]; + + snprintf(buf, sizeof(buf), "0x%lx\n", page_owner_filter); + return simple_read_from_buffer(ubuf, count, offset, buf, strlen(buf)); +} + +static const struct file_operations proc_page_owner_filter_ops = { + .open = simple_open, + .write = page_owner_filter_write, + .read = page_owner_filter_read, +}; + +static ssize_t page_owner_handle_write(struct file *file, + const char __user *ubuf, + size_t count, loff_t *offset) +{ + unsigned long size; + + if (kstrtoul_from_user(ubuf, count, 0, &size)) { + pr_err_ratelimited("Invalid format for handle size\n"); + return -EINVAL; + } + + if (size) { + if (size > (md_pageowner_dump_size / SZ_16K)) { + pr_err_ratelimited("size : %lu KB exceeds max size : %lu KB\n", + size, (md_pageowner_dump_size / SZ_16K)); + goto err; + } + page_owner_handles_size = size * SZ_1K; + } +err: + return count; +} + +static ssize_t page_owner_handle_read(struct file *file, char __user *ubuf, + size_t count, loff_t *offset) +{ + char buf[64]; + + snprintf(buf, sizeof(buf), "%lu KB\n", + (page_owner_handles_size / SZ_1K)); + return simple_read_from_buffer(ubuf, count, offset, buf, strlen(buf)); +} + +static const struct file_operations proc_page_owner_handle_ops = { + .open = simple_open, + .write = page_owner_handle_write, + .read = page_owner_handle_read, +}; + +static ssize_t page_owner_call_site_write(struct file *file, + const char __user *ubuf, + size_t count, loff_t *offset) +{ + struct accounted_call_site *call_site; + char buf[50]; + + if (count >= 50) { + pr_err_ratelimited("Input string size too large\n"); + return -EINVAL; + } + + memset(buf, 0, 50); + + if (copy_from_user(buf, ubuf, count)) { + pr_err_ratelimited("Couldn't copy from user\n"); + return -EFAULT; + } + + if (!isalpha(buf[0]) && buf[0] != '_') { + pr_err_ratelimited("Invalid call site name\n"); + return -EINVAL; + } + + call_site = kzalloc(sizeof(*call_site), GFP_KERNEL); + if (!call_site) + return -ENOMEM; + + strlcpy(call_site->name, buf, strlen(buf)); + mutex_lock(&accounted_call_site_lock); + list_add_tail(&call_site->list, &accounted_call_site_list); + mutex_unlock(&accounted_call_site_lock); + + return count; +} + +static ssize_t page_owner_call_site_read(struct file *file, char __user *ubuf, + size_t count, loff_t *offset) +{ + char *kbuf; + struct accounted_call_site *call_site; + int i = 1, ret = 0; + size_t size = PAGE_SIZE; + + kbuf = kmalloc(size, GFP_KERNEL); + if (!kbuf) + return -ENOMEM; + + ret = scnprintf(kbuf, count, "%s\n", "Accounted call sites:"); + mutex_lock(&accounted_call_site_lock); + list_for_each_entry(call_site, &accounted_call_site_list, list) { + ret += scnprintf(kbuf + ret, size - ret, + "%d. %s\n", i, call_site->name); + i += 1; + if (ret == size) { + ret = -ENOMEM; + mutex_unlock(&accounted_call_site_lock); + goto err; + } + } + mutex_unlock(&accounted_call_site_lock); + ret = simple_read_from_buffer(ubuf, count, offset, kbuf, strlen(kbuf)); +err: + kfree(kbuf); + return ret; +} + +static const struct file_operations proc_page_owner_call_site_ops = { + .open = simple_open, + .write = page_owner_call_site_write, + .read = page_owner_call_site_read, +}; +#endif + static int __init pageowner_init(void) { if (!static_branch_unlikely(&page_owner_inited)) { @@ -674,6 +996,14 @@ static int __init pageowner_init(void) debugfs_create_file("page_owner", 0400, NULL, NULL, &proc_page_owner_operations); +#ifdef CONFIG_QCOM_MINIDUMP_PANIC_DUMP + debugfs_create_file("page_owner_filter", 0400, NULL, NULL, + &proc_page_owner_filter_ops); + debugfs_create_file("page_owner_handles_size_kb", 0400, NULL, NULL, + &proc_page_owner_handle_ops); + debugfs_create_file("page_owner_call_sites", 0400, NULL, NULL, + &proc_page_owner_call_site_ops); +#endif return 0; } late_initcall(pageowner_init)