Add an extensible KALLSYMS_QUERY ioctl() interface to /proc/kallsyms to support fast point queries without scanning the entire text file. Exactly one query mode must be set in query_flags. When KALLSYMS_QUERY_BY_SYMBOL is set in query_flags, KALLSYMS_QUERY resolves a NUL-terminated symbol name in vmlinux or loaded modules and then populates sym_addr: - Searches vmlinux first via kallsyms_lookup_names() (binary search), which takes precedence over loaded modules, and falls back to module_kallsyms_on_each_symbol() under module_mutex for module symbols - Matches names exactly, like kallsyms_lookup_name(), disregarding the .llvm. matching, following precedent set by commit fb6a421fb615 ("kallsyms: Match symbols exactly with CONFIG_LTO_CLANG") - Resolves 0x0 symbol addresses in both vmlinux and modules - Rejects ambiguous symbols (multiple entries with the same name at different addresses within vmlinux, or across loaded modules) with -EADDRNOTAVAIL, while allowing same-address aliases Userspace symbolizers resolve kernel addresses to symbol names with /proc/kallsyms. This is typically done by reading the entire /proc/kallsyms text file, and keeping a compressed sorted copy of it in order to resolve addresses at runtime. When KALLSYMS_QUERY_BY_ADDR is set in query_flags, KALLSYMS_QUERY resolves one address to the symbol that contains it: - sym_addr holds the address to resolve on input. - sym_name_addr must point to a user buffer of sym_name_size bytes that receives the NUL-terminated symbol name. On output, sym_name_size holds the number of bytes written (including the terminating NUL). -ENAMETOOLONG is returned if the buffer is too small; a buffer of KSYM_NAME_LEN (512 at the time of writing) bytes is always large enough. - Symbols are resolved via kallsyms_lookup(), the same lookup as %pS, covering vmlinux, loaded modules, BPF JIT images, and freed module init functions still tracked by ftrace. On an 8 CPU Cuttlefish (x86-64 VM), using this patchseries to load a BPF program with 11 typeless ksyms has ~220x speedup (157ms -> 0.7ms). On an 8 CPU x86-64 VM with a 175k line kallsyms, there are broad performance and memory savings for the symbolization and callstack sampling tool, Perfetto: - Init time: ~350x speedup (155.7ms -> 0.44ms) - 50k symbolizations: ~12x speedup (25.2ms -> 2.2ms) - VmRSS: -68% (15.1MB -> 4.8MB) Suggested-by: Andrii Nakryiko Signed-off-by: Samuel Wu --- include/uapi/linux/fs.h | 41 +++++++++++++ kernel/kallsyms.c | 132 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 173 insertions(+) diff --git a/include/uapi/linux/fs.h b/include/uapi/linux/fs.h index 34c6f219462a..b43f227dad0e 100644 --- a/include/uapi/linux/fs.h +++ b/include/uapi/linux/fs.h @@ -674,6 +674,47 @@ struct procmap_query { __u64 build_id_addr; /* in */ }; +/* /proc/kallsyms ioctl */ +#define KALLSYMS_QUERY _IOWR(PROCFS_IOCTL_MAGIC, 18, struct kallsyms_query) + +/* Exactly one query mode must be set in query_flags. */ +enum kallsyms_query_flags { + /* + * Look up a symbol in vmlinux or loaded modules (vmlinux first; BPF + * and ftrace symbols are not searched) by exact NUL-terminated name + * at sym_name_addr and return sym_addr. sym_addr and sym_name_size + * must be 0 on input. + */ + KALLSYMS_QUERY_BY_SYMBOL = 0x01, + /* + * Look up the symbol covering sym_addr and write its NUL-terminated + * name to the sym_name_addr buffer of sym_name_size bytes. The same + * symbols as %pS are searched: vmlinux, loaded modules, BPF JIT + * images, and freed module init functions still tracked by ftrace. + * A buffer of KSYM_NAME_LEN (512 at the time of writing) bytes is + * always enough. On success, sym_name_size is set to the number of + * bytes written (including the terminating NUL). + */ + KALLSYMS_QUERY_BY_ADDR = 0x02, +}; + +/* + * Returns -ENOENT if no symbol matches, -EADDRNOTAVAIL if the name maps to + * more than one address, -ENAMETOOLONG if the name does not fit in + * KSYM_NAME_LEN (512 at the time of writing) or sym_name_size bytes, and + * -EPERM if kallsyms addresses are hidden from the opener of /proc/kallsyms + * (e.g. kptr_restrict). + */ +struct kallsyms_query { + __u64 size; /* in - struct size for compatibility */ + __u64 query_flags; /* in - query mode (enum kallsyms_query_flags) */ + __u64 sym_name_addr; /* in - address of symbol name buffer */ + __u64 sym_addr; /* out (BY_SYMBOL) / in (BY_ADDR) */ + __u64 sym_name_size; /* in/out - name buffer size / bytes written */ +}; + +#define KALLSYMS_QUERY_SIZE_VER0 40 /* sizeof first published struct */ + /* * Shutdown the filesystem. */ diff --git a/kernel/kallsyms.c b/kernel/kallsyms.c index aec2f06858af..7caaf3e421c7 100644 --- a/kernel/kallsyms.c +++ b/kernel/kallsyms.c @@ -31,6 +31,7 @@ #include #include #include +#include #include "kallsyms_internal.h" @@ -922,11 +923,142 @@ const char *kdb_walk_kallsyms(loff_t *pos) } #endif /* CONFIG_KGDB_KDB */ +struct kallsyms_mod_find_ctx { + const char *name; + unsigned long addr; + bool found; +}; + +static int kallsyms_mod_find_cb(void *data, const char *name, unsigned long addr) +{ + struct kallsyms_mod_find_ctx *ctx = data; + + if (strcmp(name, ctx->name) != 0) + return 0; + if (ctx->found && ctx->addr != addr) + return -EADDRNOTAVAIL; + ctx->found = true; + ctx->addr = addr; + return 0; +} + +static int do_kallsyms_query(struct kallsym_iter *iter, void __user *uarg) +{ + char namebuf[KSYM_NAME_LEN]; + struct kallsyms_query karg; + unsigned long addr; + unsigned int start; + unsigned int end; + unsigned int i; + __u64 usize; + long len; + int err; + + BUILD_BUG_ON(sizeof(struct kallsyms_query) != KALLSYMS_QUERY_SIZE_VER0); + + /* + * The text interface shows zero addresses to openers without permission. + * A zeroed point query result is useless (and an address-to-symbol query + * would leak KASLR), so fail instead. + */ + if (!iter->show_value) + return -EPERM; + if (copy_from_user(&usize, uarg, sizeof(usize))) + return -EFAULT; + if (usize > PAGE_SIZE) + return -E2BIG; + if (usize < KALLSYMS_QUERY_SIZE_VER0) + return -EINVAL; + + err = copy_struct_from_user(&karg, sizeof(karg), uarg, usize); + if (err) + return err; + + switch (karg.query_flags) { + case KALLSYMS_QUERY_BY_SYMBOL: + if (karg.sym_addr || karg.sym_name_size) + return -EINVAL; + + len = strncpy_from_user(namebuf, u64_to_user_ptr(karg.sym_name_addr), + sizeof(namebuf)); + if (len < 0) + return len; + if (len == 0) + return -EINVAL; + if (len == sizeof(namebuf)) + return -ENAMETOOLONG; + + if (kallsyms_lookup_names(namebuf, &start, &end)) { + struct kallsyms_mod_find_ctx ctx = { .name = namebuf }; + + err = module_kallsyms_on_each_symbol(NULL, kallsyms_mod_find_cb, &ctx); + /* Check first: maps -EOPNOTSUPP (CONFIG_MODULES=n) to -ENOENT. */ + if (!ctx.found) + return -ENOENT; + if (err) + return err; + addr = ctx.addr; + } else { + addr = kallsyms_sym_address(get_symbol_seq(start)); + for (i = start + 1; i <= end; i++) { + if (kallsyms_sym_address(get_symbol_seq(i)) != addr) + return -EADDRNOTAVAIL; + } + } + + karg.sym_addr = addr; + break; + case KALLSYMS_QUERY_BY_ADDR: + if (!karg.sym_name_size) + return -EINVAL; + addr = karg.sym_addr; + /* Reject addresses that do not fit in unsigned long (32-bit). */ + if (addr != karg.sym_addr) + return -ENOENT; + if (!kallsyms_lookup(addr, NULL, NULL, NULL, namebuf)) + return -ENOENT; + + len = strlen(namebuf) + 1; + if (karg.sym_name_size < len) + return -ENAMETOOLONG; + if (copy_to_user(u64_to_user_ptr(karg.sym_name_addr), namebuf, len)) + return -EFAULT; + + karg.sym_name_size = len; + break; + default: + return -EINVAL; + } + + if (copy_to_user(uarg, &karg, min_t(size_t, sizeof(karg), usize))) + return -EFAULT; + + return 0; +} + +static long kallsyms_ioctl(struct file *file, unsigned int cmd, unsigned long arg) +{ + struct seq_file *m = file->private_data; + + switch (_IOC_NR(cmd)) { + case _IOC_NR(KALLSYMS_QUERY): + if (!extensible_ioctl_valid(cmd, KALLSYMS_QUERY, KALLSYMS_QUERY_SIZE_VER0)) + return -ENOIOCTLCMD; + return do_kallsyms_query(m->private, (void __user *)arg); + default: + return -ENOIOCTLCMD; + } +} + static const struct proc_ops kallsyms_proc_ops = { .proc_open = kallsyms_open, .proc_read = seq_read, .proc_lseek = seq_lseek, .proc_release = seq_release_private, + .proc_ioctl = kallsyms_ioctl, +#ifdef CONFIG_COMPAT + .proc_compat_ioctl = compat_ptr_ioctl, +#endif }; static int __init kallsyms_init(void) -- 2.56.0.257.gf7497c7d27-goog Copy the KALLSYMS_QUERY ioctl definitions from include/uapi/linux/fs.h to tools/include/uapi/linux/fs.h for libbpf and the proc and bpf selftests. Only the new definitions are copied; other differences between the two headers are left alone. Signed-off-by: Samuel Wu --- tools/include/uapi/linux/fs.h | 41 +++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/tools/include/uapi/linux/fs.h b/tools/include/uapi/linux/fs.h index f0a26309b6d5..d224516e6d6e 100644 --- a/tools/include/uapi/linux/fs.h +++ b/tools/include/uapi/linux/fs.h @@ -567,4 +567,45 @@ struct procmap_query { __u64 build_id_addr; /* in */ }; +/* /proc/kallsyms ioctl */ +#define KALLSYMS_QUERY _IOWR(PROCFS_IOCTL_MAGIC, 18, struct kallsyms_query) + +/* Exactly one query mode must be set in query_flags. */ +enum kallsyms_query_flags { + /* + * Look up a symbol in vmlinux or loaded modules (vmlinux first; BPF + * and ftrace symbols are not searched) by exact NUL-terminated name + * at sym_name_addr and return sym_addr. sym_addr and sym_name_size + * must be 0 on input. + */ + KALLSYMS_QUERY_BY_SYMBOL = 0x01, + /* + * Look up the symbol covering sym_addr and write its NUL-terminated + * name to the sym_name_addr buffer of sym_name_size bytes. The same + * symbols as %pS are searched: vmlinux, loaded modules, BPF JIT + * images, and freed module init functions still tracked by ftrace. + * A buffer of KSYM_NAME_LEN (512 at the time of writing) bytes is + * always enough. On success, sym_name_size is set to the number of + * bytes written (including the terminating NUL). + */ + KALLSYMS_QUERY_BY_ADDR = 0x02, +}; + +/* + * Returns -ENOENT if no symbol matches, -EADDRNOTAVAIL if the name maps to + * more than one address, -ENAMETOOLONG if the name does not fit in + * KSYM_NAME_LEN (512 at the time of writing) or sym_name_size bytes, and + * -EPERM if kallsyms addresses are hidden from the opener of /proc/kallsyms + * (e.g. kptr_restrict). + */ +struct kallsyms_query { + __u64 size; /* in - struct size for compatibility */ + __u64 query_flags; /* in - query mode (enum kallsyms_query_flags) */ + __u64 sym_name_addr; /* in - address of symbol name buffer */ + __u64 sym_addr; /* out (BY_SYMBOL) / in (BY_ADDR) */ + __u64 sym_name_size; /* in/out - name buffer size / bytes written */ +}; + +#define KALLSYMS_QUERY_SIZE_VER0 40 /* sizeof first published struct */ + #endif /* _UAPI_LINUX_FS_H */ -- 2.56.0.257.gf7497c7d27-goog Document the existence of the KALLSYMS_QUERY ioctl() in the section describing the /proc/kallsyms file. Refer the user to the UAPI header for low-level details of this programmatic interface. Add a "kallsyms" row to the /proc file table. Signed-off-by: Samuel Wu --- Documentation/filesystems/proc.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Documentation/filesystems/proc.rst b/Documentation/filesystems/proc.rst index c102b62023cd..a0e2fe14a01a 100644 --- a/Documentation/filesystems/proc.rst +++ b/Documentation/filesystems/proc.rst @@ -761,6 +761,7 @@ files are there, and which are missing. ioports I/O port usage irq Masks for irq to cpu affinity (2.4)(smp?) isapnp ISA PnP (Plug&Play) Info (2.4) + kallsyms Kernel symbol table with dynamic queries (see text) kcore Kernel core image (can be ELF or A.OUT(deprecated in 2.4)) kmsg Kernel messages ksyms Kernel symbol table @@ -1368,6 +1369,20 @@ Provides counts of softirq handlers serviced since boot time, for each CPU. HRTIMER: 0 0 0 0 RCU: 1678 1769 2178 2250 + +kallsyms +~~~~~~~~ + +The /proc/kallsyms file lists all symbols in vmlinux and loaded modules. + +In addition to sequential text reading, /proc/kallsyms provides an ioctl()-based +API (`KALLSYMS_QUERY`) for querying specific symbols and addresses. This +interface is binary and is meant for efficient extraction of data without +parsing the entire text file. `struct kallsyms_query`, defined in the +linux/fs.h UAPI header, serves as the input/output argument to the +`KALLSYMS_QUERY` ioctl() command. See comments in the linux/fs.h UAPI header +for details on query semantics, supported flags, and return values. + 1.3 Networking info in /proc/net -------------------------------- -- 2.56.0.257.gf7497c7d27-goog When resolving typeless extern ksyms (!ext->ksym.type_id), libbpf currently reads and parses the entire /proc/kallsyms text file via libbpf_kallsyms_parse(). The kallsyms text file is commonly and quite easily several MB, which means with 4kB page sizes, parsing the file requires a few thousand read() syscalls. This is inefficient, especially in the common case when only a few symbols are needed. Add bpf_object__query_ksyms() to resolve typeless extern ksyms via the KALLSYMS_QUERY ioctl on /proc/kallsyms. Modules are still searched if a symbol is not found in vmlinux, and ambiguous symbols are still an error. The lookup follows kallsyms_lookup_name() semantics, so there are two differences from parsing the text file: - A name that exists in both vmlinux and a module resolves to the vmlinux symbol, instead of failing as ambiguous. - Names are matched exactly, so an extern does not match a .llvm. symbol on CONFIG_LTO_CLANG kernels. Fall back to reading and parsing the full text kallsyms only if the ioctl is not accessible: -ENOTTY on older kernels without the ioctl, or -EPERM if kallsyms addresses are hidden from the opener (e.g. kptr_restrict). A symbol that is not found is left unresolved, so a missing strong extern still fails with -ESRCH and a missing weak extern still defaults to zero, without a full text scan. Signed-off-by: Samuel Wu --- tools/lib/bpf/libbpf.c | 78 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 76 insertions(+), 2 deletions(-) diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c index 8fcb8cf39cc0..0e94d2335bd7 100644 --- a/tools/lib/bpf/libbpf.c +++ b/tools/lib/bpf/libbpf.c @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -59,6 +60,23 @@ #define BPF_FS_MAGIC 0xcafe4a11 #endif +#ifndef KALLSYMS_QUERY +/* 'f' corresponds to PROCFS_IOCTL_MAGIC (include/uapi/linux/fs.h) */ +#define KALLSYMS_QUERY _IOWR('f', 18, struct kallsyms_query) + +enum kallsyms_query_flags { + KALLSYMS_QUERY_BY_SYMBOL = 0x01, +}; + +struct kallsyms_query { + __u64 size; + __u64 query_flags; + __u64 sym_name_addr; + __u64 sym_addr; + __u64 sym_name_size; +}; +#endif + #define MAX_EVENT_NAME_LEN 64 #define BPF_FS_DEFAULT_PATH "/sys/fs/bpf" @@ -9644,8 +9662,64 @@ static int kallsyms_cb(unsigned long long sym_addr, char sym_type, return 0; } -static int bpf_object__read_kallsyms_file(struct bpf_object *obj) +static int bpf_object__query_ksyms(struct bpf_object *obj) +{ + struct kallsyms_query q; + struct extern_desc *ext; + int err = 0; + int fd; + int i; + + fd = open("/proc/kallsyms", O_RDONLY | O_CLOEXEC); + if (fd < 0) + return -errno; + + for (i = 0; i < obj->nr_extern; i++) { + ext = &obj->externs[i]; + if (ext->type != EXT_KSYM || ext->ksym.type_id) + continue; + + q = (struct kallsyms_query){ + .size = sizeof(q), + .query_flags = KALLSYMS_QUERY_BY_SYMBOL, + .sym_name_addr = ptr_to_u64(ext->name), + }; + + if (ioctl(fd, KALLSYMS_QUERY, &q) < 0) { + err = -errno; + if (err == -ENOENT) { + err = 0; + continue; + } + if (err == -EADDRNOTAVAIL) + pr_warn("extern (ksym) '%s': resolution is ambiguous\n", + ext->name); + break; + } + ext->is_set = true; + ext->ksym.addr = q.sym_addr; + pr_debug("extern (ksym) '%s': resolved in kernel to 0x%llx\n", + ext->name, ext->ksym.addr); + } + close(fd); + return err; +} + +static int bpf_object__resolve_typeless_ksyms(struct bpf_object *obj) { + int err; + + err = bpf_object__query_ksyms(obj); + /* + * Fall back to parsing /proc/kallsyms only if the ioctl is not + * accessible: not present (-ENOTTY on older kernels) or kallsyms + * addresses hidden from this opener (-EPERM, e.g. kptr_restrict). + */ + if (err != -ENOTTY && err != -EPERM) + return err; + + pr_debug("KALLSYMS_QUERY unavailable (%s), falling back to /proc/kallsyms\n", + errstr(err)); return libbpf_kallsyms_parse(kallsyms_cb, obj); } @@ -9922,7 +9996,7 @@ static int bpf_object__resolve_externs(struct bpf_object *obj, return -EINVAL; } if (need_kallsyms) { - err = bpf_object__read_kallsyms_file(obj); + err = bpf_object__resolve_typeless_ksyms(obj); if (err) return -EINVAL; } -- 2.56.0.257.gf7497c7d27-goog Typeless module ksyms are resolved through kallsyms, but the existing module ksym tests (ksyms_module, ksym_race) only use typed ksyms, which are resolved through BTF. With KALLSYMS_QUERY, libbpf resolves module symbols through the module lookup path of the ioctl. Add a weak typeless extern for bpf_testmod_ksym_percpu to the ksyms test and check that its address matches /proc/kallsyms when bpf_testmod is loaded. Signed-off-by: Samuel Wu --- tools/testing/selftests/bpf/prog_tests/ksyms.c | 7 +++++++ tools/testing/selftests/bpf/progs/test_ksyms.c | 3 +++ 2 files changed, 10 insertions(+) diff --git a/tools/testing/selftests/bpf/prog_tests/ksyms.c b/tools/testing/selftests/bpf/prog_tests/ksyms.c index dc7aab532fb1..62760284f746 100644 --- a/tools/testing/selftests/bpf/prog_tests/ksyms.c +++ b/tools/testing/selftests/bpf/prog_tests/ksyms.c @@ -11,6 +11,7 @@ void test_ksyms(void) struct test_ksyms *skel; struct test_ksyms__data *data; __u64 link_fops_addr, per_cpu_start_addr; + __u64 testmod_ksym_addr; struct stat st; __u64 btf_size; int err; @@ -48,6 +49,12 @@ void test_ksyms(void) ASSERT_EQ(data->out__btf_size, btf_size, "btf_size"); ASSERT_EQ(data->out__per_cpu_start, per_cpu_start_addr, "__per_cpu_start"); + if (env.has_testmod && + ASSERT_OK(kallsyms_find("bpf_testmod_ksym_percpu", &testmod_ksym_addr), + "bpf_testmod_ksym_percpu: kallsyms_find")) + ASSERT_EQ(data->out__bpf_testmod_ksym, testmod_ksym_addr, + "bpf_testmod_ksym_percpu"); + cleanup: test_ksyms__destroy(skel); } diff --git a/tools/testing/selftests/bpf/progs/test_ksyms.c b/tools/testing/selftests/bpf/progs/test_ksyms.c index 6c9cbb5a3bdf..69309f2a2d0a 100644 --- a/tools/testing/selftests/bpf/progs/test_ksyms.c +++ b/tools/testing/selftests/bpf/progs/test_ksyms.c @@ -9,6 +9,7 @@ __u64 out__bpf_link_fops = -1; __u64 out__bpf_link_fops1 = -1; __u64 out__btf_size = -1; __u64 out__per_cpu_start = -1; +__u64 out__bpf_testmod_ksym = -1; extern const void bpf_link_fops __ksym; extern const void __start_BTF __ksym; @@ -16,6 +17,7 @@ extern const void __stop_BTF __ksym; extern const void __per_cpu_start __ksym; /* non-existing symbol, weak, default to zero */ extern const void bpf_link_fops1 __ksym __weak; +extern const void bpf_testmod_ksym_percpu __ksym __weak; SEC("raw_tp/sys_enter") int handler(const void *ctx) @@ -25,6 +27,7 @@ int handler(const void *ctx) out__per_cpu_start = (__u64)&__per_cpu_start; out__bpf_link_fops1 = (__u64)&bpf_link_fops1; + out__bpf_testmod_ksym = (__u64)&bpf_testmod_ksym_percpu; return 0; } -- 2.56.0.257.gf7497c7d27-goog Add a selftest for the KALLSYMS_QUERY ioctl() on /proc/kallsyms. For symbol-to-address lookup (KALLSYMS_QUERY_BY_SYMBOL), cover valid lookups, missing symbols (-ENOENT), input validation (-EINVAL, -EFAULT, -ENAMETOOLONG, -E2BIG), and extensible struct size handling. For address-to-symbol lookup (KALLSYMS_QUERY_BY_ADDR), cover lookups at exact symbol start and interior offsets, buffer truncation (-ENAMETOOLONG), unmapped addresses (-ENOENT), and input validation (-EINVAL, -EFAULT). Signed-off-by: Samuel Wu --- tools/testing/selftests/proc/.gitignore | 1 + tools/testing/selftests/proc/Makefile | 1 + .../selftests/proc/proc-kallsyms-query.c | 213 ++++++++++++++++++ 3 files changed, 215 insertions(+) create mode 100644 tools/testing/selftests/proc/proc-kallsyms-query.c diff --git a/tools/testing/selftests/proc/.gitignore b/tools/testing/selftests/proc/.gitignore index 9c9735570abf..1762c28f524c 100644 --- a/tools/testing/selftests/proc/.gitignore +++ b/tools/testing/selftests/proc/.gitignore @@ -4,6 +4,7 @@ /fd-003-kthread /proc-2-is-kthread /proc-fsconfig-hidepid +/proc-kallsyms-query /proc-loadavg-001 /proc-maps-race /proc-multiple-procfs diff --git a/tools/testing/selftests/proc/Makefile b/tools/testing/selftests/proc/Makefile index a7de2bb6d8be..dc3c28e6b83b 100644 --- a/tools/testing/selftests/proc/Makefile +++ b/tools/testing/selftests/proc/Makefile @@ -29,6 +29,7 @@ TEST_GEN_PROGS += setns-sysvipc TEST_GEN_PROGS += thread-self TEST_GEN_PROGS += proc-multiple-procfs TEST_GEN_PROGS += proc-fsconfig-hidepid +TEST_GEN_PROGS += proc-kallsyms-query TEST_GEN_PROGS += proc-pidns include ../lib.mk diff --git a/tools/testing/selftests/proc/proc-kallsyms-query.c b/tools/testing/selftests/proc/proc-kallsyms-query.c new file mode 100644 index 000000000000..229cde432d6a --- /dev/null +++ b/tools/testing/selftests/proc/proc-kallsyms-query.c @@ -0,0 +1,213 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Tests for KALLSYMS_QUERY ioctl() on /proc/kallsyms. + */ +#undef NDEBUG +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "kselftest.h" + +#define KSYM_NAME_LEN 512 + +struct kallsyms_query_ext { + struct kallsyms_query v0; + __u64 extra; +}; + +static int do_query(int fd, void *q) +{ + return ioctl(fd, KALLSYMS_QUERY, q) < 0 ? -errno : 0; +} + +static struct kallsyms_query sym_query(const char *name) +{ + return (struct kallsyms_query){ + .size = sizeof(struct kallsyms_query), + .query_flags = KALLSYMS_QUERY_BY_SYMBOL, + .sym_name_addr = (__u64)(uintptr_t)name, + }; +} + +static int query_symbol(int fd, const char *name, struct kallsyms_query *q) +{ + *q = sym_query(name); + return do_query(fd, q); +} + +static int query_addr(int fd, unsigned long long addr, char *buf, + size_t buf_sz, struct kallsyms_query *q) +{ + *q = (struct kallsyms_query){ + .size = sizeof(*q), + .query_flags = KALLSYMS_QUERY_BY_ADDR, + .sym_name_addr = (__u64)(uintptr_t)buf, + .sym_addr = addr, + .sym_name_size = buf_sz, + }; + return do_query(fd, q); +} + +static bool find_vmlinux_symbol(const char *target, unsigned long long *addr_out) +{ + char name[KSYM_NAME_LEN]; + char line[1024]; + char type; + char mod; + FILE *f; + + f = fopen("/proc/kallsyms", "r"); + if (!f) + return false; + + while (fgets(line, sizeof(line), f)) { + if (sscanf(line, "%llx %c %511s %c", addr_out, &type, name, &mod) == 3 && + strcmp(name, target) == 0) { + fclose(f); + return true; + } + } + fclose(f); + return false; +} + +int main(void) +{ + const char *sym_name = "vfs_read"; + char resolved_name[KSYM_NAME_LEN]; + char long_name[KSYM_NAME_LEN + 1]; + size_t page_size = sysconf(_SC_PAGESIZE); + unsigned long long exp_addr; + struct kallsyms_query_ext q1; + struct kallsyms_query q; + char *page_buf; + int err; + int fd; + + fd = open("/proc/kallsyms", O_RDONLY | O_CLOEXEC); + if (fd < 0) { + if (errno == ENOENT) + return KSFT_SKIP; + ksft_exit_fail_msg("open(/proc/kallsyms) failed: %s\n", + strerror(errno)); + } + + if (!find_vmlinux_symbol(sym_name, &exp_addr)) { + close(fd); + return KSFT_SKIP; + } + + /* CASE 1: Valid vmlinux symbol query (or -EPERM when addresses are hidden) */ + err = query_symbol(fd, sym_name, &q); + if (err == -ENOTTY) { + close(fd); + return KSFT_SKIP; + } + if (!exp_addr) { + assert(err == -EPERM); + close(fd); + ksft_exit_skip("kallsyms addresses are hidden, only -EPERM checked\n"); + } + assert(err == 0); + assert(q.sym_addr == exp_addr); + + /* CASE 2: Missing symbol returns -ENOENT */ + assert(query_symbol(fd, "__nonexistent_kallsyms_symbol__", &q) == -ENOENT); + + /* CASE 3: Empty symbol name returns -EINVAL */ + assert(query_symbol(fd, "", &q) == -EINVAL); + + /* CASE 4: NULL sym_name_addr returns -EFAULT */ + assert(query_symbol(fd, NULL, &q) == -EFAULT); + + /* CASE 5: Symbol name >= KSYM_NAME_LEN returns -ENAMETOOLONG */ + memset(long_name, 'a', KSYM_NAME_LEN); + long_name[KSYM_NAME_LEN] = '\0'; + assert(query_symbol(fd, long_name, &q) == -ENAMETOOLONG); + + /* CASE 6: Symbol name == KSYM_NAME_LEN - 1 returns -ENOENT */ + long_name[KSYM_NAME_LEN - 1] = '\0'; + assert(query_symbol(fd, long_name, &q) == -ENOENT); + + /* CASE 7: Zero, multiple, or unknown query_flags return -EINVAL */ + q = sym_query(sym_name); + q.query_flags = 0; + assert(do_query(fd, &q) == -EINVAL); + q.query_flags = KALLSYMS_QUERY_BY_SYMBOL | KALLSYMS_QUERY_BY_ADDR; + assert(do_query(fd, &q) == -EINVAL); + q.query_flags = 1ULL << 63; + assert(do_query(fd, &q) == -EINVAL); + + /* CASE 8: Non-zero output fields on input in symbol mode return -EINVAL */ + q = sym_query(sym_name); + q.sym_addr = 1; + assert(do_query(fd, &q) == -EINVAL); + q = sym_query(sym_name); + q.sym_name_size = 1; + assert(do_query(fd, &q) == -EINVAL); + + /* CASE 9: size < minimum size returns -EINVAL */ + q = sym_query(sym_name); + q.size = KALLSYMS_QUERY_SIZE_VER0 - 8; + assert(do_query(fd, &q) == -EINVAL); + + /* CASE 10: size > PAGE_SIZE returns -E2BIG */ + q = sym_query(sym_name); + q.size = page_size + 8; + assert(do_query(fd, &q) == -E2BIG); + + /* CASE 11: Larger extensible struct with zeroed trailing bytes succeeds */ + q1 = (struct kallsyms_query_ext){ .v0 = sym_query(sym_name) }; + q1.v0.size = sizeof(q1); + assert(do_query(fd, &q1) == 0); + assert(q1.v0.sym_addr == exp_addr); + + /* + * CASE 12: Larger struct with non-zero trailing bytes returns -E2BIG. + * Set the last byte of a page-sized struct so that future fields do + * not change the result. + */ + page_buf = calloc(1, page_size); + assert(page_buf); + q = sym_query(sym_name); + q.size = page_size; + memcpy(page_buf, &q, sizeof(q)); + page_buf[page_size - 1] = 1; + assert(do_query(fd, page_buf) == -E2BIG); + free(page_buf); + + /* CASE 13: Valid address-to-symbol query (exact start + interior offset) */ + memset(resolved_name, 0, sizeof(resolved_name)); + assert(query_addr(fd, exp_addr, resolved_name, sizeof(resolved_name), &q) == 0); + assert(strcmp(resolved_name, sym_name) == 0); + assert(q.sym_addr == exp_addr); + assert(q.sym_name_size == strlen(sym_name) + 1); + + memset(resolved_name, 0, sizeof(resolved_name)); + assert(query_addr(fd, exp_addr + 1, resolved_name, sizeof(resolved_name), &q) == 0); + assert(strcmp(resolved_name, sym_name) == 0); + assert(q.sym_addr == exp_addr + 1); + assert(q.sym_name_size == strlen(sym_name) + 1); + + /* CASE 14: KALLSYMS_QUERY_BY_ADDR with buffer too small returns -ENAMETOOLONG */ + assert(query_addr(fd, exp_addr, resolved_name, strlen(sym_name), &q) == -ENAMETOOLONG); + + /* CASE 15: KALLSYMS_QUERY_BY_ADDR on unmapped address returns -ENOENT */ + assert(query_addr(fd, ~0ULL, resolved_name, sizeof(resolved_name), &q) == -ENOENT); + + /* CASE 16: KALLSYMS_QUERY_BY_ADDR invalid inputs return -EINVAL or -EFAULT */ + assert(query_addr(fd, exp_addr, resolved_name, 0, &q) == -EINVAL); + assert(query_addr(fd, exp_addr, NULL, sizeof(resolved_name), &q) == -EFAULT); + + close(fd); + return 0; +} -- 2.56.0.257.gf7497c7d27-goog