Re: [PATCH bpf-next v2 1/6] kallsyms: Add KALLSYMS_QUERY ioctl to /proc/kallsyms

From: Samuel Wu

Date: Thu Oct 29 2026 - 17:59:06 EST


On Tue, Oct 27, 2026 at 3:25 PM Andrii Nakryiko
<andrii.nakryiko@xxxxxxxxx> wrote:
>
> On Thu, Oct 22, 2026 at 5:17 PM Samuel Wu <wusamuel@xxxxxxxxxx> wrote:
> >
> > 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.<hash> 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 <andrii@xxxxxxxxxx>
> > Signed-off-by: Samuel Wu <wusamuel@xxxxxxxxxx>
> > ---
> > 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 */
> > +};
>
> it might be smart to have QUERY_BY_ADDR and QUERY_BY_NAME to be two
> separate ioctl commands with its own query in/out struct. This would
> remove addr vs name ambiguity in sym_name_addr field, and will allow
> us to evolve APIs independently, if necessary. They should still
> probably reuse the same type to represent symbol information, and have
> kallsyms_query struct provide a pointer to that struct (and its
> maximum size), which the kernel will fill out and set the size it
> actually knows about.
>
> struct kallsyms_query_by_addr {
> __u64 size;
> __u64 query_flags;
> __u64 sym_addr; /* in */
> struct kallsyms_sym *sym; /* out */
> __u64 sym_len;
> };
>
> struct kallsyms_sym {
> __u64 sym_addr;
> __u64 sym_name;
> __u64 sym_name_len;
> __u64 sym_module;
> __u64 sym_module_len; /* if zero, no module requested/available */
> };
>
> e.g. for symbol you'd want to return its name, address, but also
> module name. and we should leave a way to extend it further with more
> optional information (that's the kallsyms extensibility bit I was
> talking about).

Agree, two ioctls with a shared symbol type is cleaner. v3 plan is to
have QUERY_BY_NAME and QUERY_BY_ADDR both fill a shared struct
kallsyms_sym (address, size, name, module), passed by pointer so it
can grow later.

> aslo, we should think about adding ways to paginate (or at least leave
> a possibility of that in the future) if there are multiple matches
> (e.g., for static functions with the same name resolving to multiple
> addresses); not sure how it should look like API wise, but it would be
> good to still return "any" or "first" match, but let user know there
> are multiple, so user can decide how to proceed...

Makes sense, but I'm thinking v3 doesn't need pagination or batching.
I didn't see any performance wins when profiling libbpf, and it can be
added later without breaking UAPI.

> on input I'd also add module name as optional input argument, with
> "vmlinux" limiting to just vmlinux, set module name to just that name,
> and NULL/zero meaning "we don't care"

Makes sense, so here is a possible plan for QUERY_BY_NAME:
- Optional module filter: NULL = any, "vmlinux", or a module name
- Returns the first match by default (vmlinux first, then modules). An
ALL_MATCHES flag searches everything and reports the number of matches

And an open question I'm unsure about: Should libbpf keep the light
skeleton semantics (take the first match, no ambiguity check), or set
ALL_MATCHES to keep today's check that fails on names with multiple
addresses?

> for QUERY_BY_ADDR we should have a flag saying whether an exact match
> by address is requested or we should find a symbol that covers the
> address (that's most useful for symbolization). maybe even "at or
> after" address mode, so you can paginate (though probably explicitly
> designed batch API would be even more useful later)

For v3 I think we can keep BY_ADDR to the covering lookup only. The
result can fill in the covering symbol's (start address, size, name,
module), and callers can check for an exact match themselves by
comparing the start address with the queried address. A batch API can
be added later when there is a user for it.

> tl;dr, this is the right direction, but I think we should polish API a
> bit more to be more universally useful and extensible
> > +
> > +#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 <linux/kernel.h>
> > #include <linux/bsearch.h>
> > #include <linux/btf_ids.h>
> > +#include <linux/uaccess.h>
> >
> > #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)
>
> PROCMAP_QUERY uses offsetofend() for the last defined field, any
> reason to have this #define and BUILD_BUG_ON here?

David pointed out that the size field in struct kallsyms_query is
redundant. In v3, I can drop the size field and use _IOC_SIZE(cmd),
keep the *_SIZE_VER0 define (since we need minimum size). Perfectly
fine to switch to the PROCMAP_QUERY style though if preferred.

Thanks Andrii for all the thoughtful feedback!

> > + return -EINVAL;
> > +
> > + err = copy_struct_from_user(&karg, sizeof(karg), uarg, usize);
> > + if (err)
> > + return err;
> > +
>
> [...]