From: Michael Bommarito <michael.bommarito@gmail.com>
To: Andrii Nakryiko <andrii@kernel.org>,
Eduard Zingerman <eddyz87@gmail.com>
Cc: Alexei Starovoitov <ast@kernel.org>,
Daniel Borkmann <daniel@iogearbox.net>,
Martin KaFai Lau <martin.lau@linux.dev>,
Kumar Kartikeya Dwivedi <memxor@gmail.com>,
Song Liu <song@kernel.org>,
Yonghong Song <yonghong.song@linux.dev>,
Jiri Olsa <jolsa@kernel.org>,
bpf@vger.kernel.org, linux-kernel@vger.kernel.org
Subject: [PATCH] libbpf: bound /proc/<pid>/maps scansets in parse_vma_segs()
Date: Thu, 21 May 2026 18:17:12 -0400 [thread overview]
Message-ID: <20260521221712.3077276-1-michael.bommarito@gmail.com> (raw)
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
reply other threads:[~2026-05-21 22:17 UTC|newest]
Thread overview: [no followups] expand[flat|nested] mbox.gz Atom feed
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260521221712.3077276-1-michael.bommarito@gmail.com \
--to=michael.bommarito@gmail.com \
--cc=andrii@kernel.org \
--cc=ast@kernel.org \
--cc=bpf@vger.kernel.org \
--cc=daniel@iogearbox.net \
--cc=eddyz87@gmail.com \
--cc=jolsa@kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=martin.lau@linux.dev \
--cc=memxor@gmail.com \
--cc=song@kernel.org \
--cc=yonghong.song@linux.dev \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®