tools/lib/bpf/usdt.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-)
parse_vma_segs() in tools/lib/bpf/usdt.c parses /proc/<pid>/maps
with two widthless scansets, "%s" into mode[16] and "%[^\n]"
into line[PATH_MAX]. Both assume the kernel caps maps records
to PATH_MAX, but it does not.
show_map_vma() emits the path via seq_path() against the seq
buffer, which doubles on overflow (m->size <<= 1 in
fs/seq_file.c), so a VMA whose backing path is a deeply nested
directory tree produces a single maps record longer than
PATH_MAX. scanf "%s" / "%[^\n]" without a width writes until
the field terminator regardless of destination size, so a
bpf_program__attach_usdt() consumer attaching against
an attacker-controlled PID overflows its own stack inside
parse_vma_segs().
Bound both scansets to the declared buffer sizes ("%15s" for
mode[16] and "%4095[^\n]" for line[PATH_MAX]) and drain any
residue past line[4094] with "%*[^\n]" before the trailing "\n",
matching the libbpf-local fscanf style. Without the drain the
residue of an over-long record would stay in the stream and
break the next "%zx-%zx" parse, so the loop would exit early and
any maps records after the over-long entry would be silently
skipped.
Fixes: 3e6fe5ce4d486 ("libbpf: Fix internal USDT address translation logic for shared libraries")
Cc: stable@vger.kernel.org
Assisted-by: Claude:claude-opus-4-7
Signed-off-by: Michael Bommarito <michael.bommarito@gmail.com>
---
Reproduced with Debian 12 on rootless podman: an unprivileged
container process mkdirs 50 nested 200-char directories and mmaps
a file at the bottom, producing a 10403-byte /proc/<host-pid>/maps
line. A harness on the host then calls the real parse_vma_segs()
against the container's PID; libbpf is built with
-fsanitize=address and the only local source change is dropping
the "static" keyword on parse_vma_segs so the symbol is linkable
from the harness.
Stock libbpf reports:
==ERROR: AddressSanitizer: stack-buffer-overflow
WRITE of size 10349 at <line> thread T0
#0 scanf_common -> #1 __isoc99_fscanf
#3 parse_vma_segs tools/lib/bpf/usdt.c:509
Address ... in frame parse_vma_segs at offset 8512, just past
line[PATH_MAX].
Patched libbpf parses the same maps cleanly. Follow-up calls
return 0 with seg_cnt > 0 for libc.so.6 and for
ld-linux-x86-64.so.2 (format drain), which appears in maps
after the over-long entry.
On normal hardened builds the stack canary aborts the consumer;
on builds without stack protector the bytes past line[] are
attacker-influenced path bytes.
Selftest gate
=============
tools/testing/selftests/bpf/test_progs -t usdt under QEMU x86_64
(KVM) on the patched kernel: all 6 subtests pass (usdt/basic,
basic_optimized, optimized_attach, multispec, urand_auto_attach,
urand_pid_attach) on both stock and patched libbpf, diff-clean.
The in-tree selftest does not itself exercise long maps records.
tools/lib/bpf/usdt.c | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/tools/lib/bpf/usdt.c b/tools/lib/bpf/usdt.c
index e3710933fd52a..e50622ef4c014 100644
--- a/tools/lib/bpf/usdt.c
+++ b/tools/lib/bpf/usdt.c
@@ -504,8 +504,11 @@ static int parse_vma_segs(int pid, const char *lib_path, struct elf_seg **segs,
* 7f5c6f5d1000-7f5c6f5d3000 rw-p 001c7000 08:04 21238613 /usr/lib64/libc-2.17.so
* 7f5c6f5d3000-7f5c6f5d8000 rw-p 00000000 00:00 0
* 7f5c6f5d8000-7f5c6f5d9000 r-xp 00000000 103:01 362990598 /data/users/andriin/linux/tools/bpf/usdt/libhello_usdt.so
+ *
+ * Bound the writes and drain residue: maps lines can exceed
+ * PATH_MAX when d_path() expands beyond it.
*/
- while (fscanf(f, "%zx-%zx %s %zx %*s %*d%[^\n]\n",
+ while (fscanf(f, "%zx-%zx %15s %zx %*s %*d%4095[^\n]%*[^\n]\n",
&seg_start, &seg_end, mode, &seg_off, line) == 5) {
void *tmp;
base-commit: 5200f5f493f79f14bbdc349e402a40dfb32f23c8
--
2.53.0
© 2016 - 2026 Red Hat, Inc.