From de1a68548ada22034bb16d9c3e446ccd64c9598d Mon Sep 17 00:00:00 2001 From: Jianlin Shi Date: Sat, 15 Aug 2026 16:19:16 +0800 Subject: [PATCH 1/5] bpf: roll back stream capacity when allocation fails bpf_stream_push_str() accounts the string length before allocating a stream element. If the allocation fails, the length remains charged even though no element is queued and therefore cannot be released by a reader. Repeated failures can exhaust the stream capacity permanently until the BPF program is freed. Refactor bpf_stream_release_capacity() to take a length so the consume and release sides are symmetric, and use it to roll back the charge when creating the stream element fails. Fixes: 5ab154f1463a ("bpf: Introduce BPF standard streams") Signed-off-by: Jianlin Shi --- kernel/bpf/stream.c | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/kernel/bpf/stream.c b/kernel/bpf/stream.c index be9ce98e94692..0b157ec4e38eb 100644 --- a/kernel/bpf/stream.c +++ b/kernel/bpf/stream.c @@ -68,10 +68,8 @@ static int bpf_stream_consume_capacity(struct bpf_stream *stream, int len) return 0; } -static void bpf_stream_release_capacity(struct bpf_stream *stream, struct bpf_stream_elem *elem) +static void bpf_stream_release_capacity(struct bpf_stream *stream, int len) { - int len = elem->total_len; - atomic_sub(len, &stream->capacity); } @@ -79,7 +77,14 @@ static int bpf_stream_push_str(struct bpf_stream *stream, const char *str, int l { int ret = bpf_stream_consume_capacity(stream, len); - return ret ?: __bpf_stream_push_str(&stream->log, str, len); + if (ret) + return ret; + + ret = __bpf_stream_push_str(&stream->log, str, len); + if (ret) + bpf_stream_release_capacity(stream, len); + + return ret; } static struct bpf_stream *bpf_stream_get(enum bpf_stream_id stream_id, struct bpf_prog_aux *aux) @@ -188,7 +193,7 @@ static int bpf_stream_read(struct bpf_stream *stream, void __user *buf, int len) if (cont) continue; bpf_stream_backlog_pop(stream); - bpf_stream_release_capacity(stream, elem); + bpf_stream_release_capacity(stream, elem->total_len); bpf_stream_free_elem(elem); } From c7d3740a94f669399c37d7072ae289f54cad775a Mon Sep 17 00:00:00 2001 From: Jianlin Shi Date: Sat, 15 Aug 2026 16:19:17 +0800 Subject: [PATCH 2/5] bpf: fix stream capacity leak in staging path bpf_stream_stage_printk() increments ss->len before pushing the formatted string to the staging log. If element allocation fails, ss->len remains inflated and bpf_stream_stage_commit() permanently charges the stream capacity for data that was never queued. Only account the string length after a successful push, and use vscnprintf() so the staged length is the truncated payload without the trailing NUL. Fixes: 5ab154f1463a ("bpf: Introduce BPF standard streams") Signed-off-by: Jianlin Shi --- kernel/bpf/stream.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/kernel/bpf/stream.c b/kernel/bpf/stream.c index 0b157ec4e38eb..ef5a7b06ffcb1 100644 --- a/kernel/bpf/stream.c +++ b/kernel/bpf/stream.c @@ -316,17 +316,19 @@ int bpf_stream_stage_printk(struct bpf_stream_stage *ss, const char *fmt, ...) { struct bpf_bprintf_buffers *buf; va_list args; - int ret; + int len, ret; if (bpf_try_get_buffers(&buf)) return -EBUSY; va_start(args, fmt); - ret = vsnprintf(buf->buf, ARRAY_SIZE(buf->buf), fmt, args); + /* Cap to the written length, excluding the trailing NUL. */ + len = vscnprintf(buf->buf, ARRAY_SIZE(buf->buf), fmt, args); va_end(args); - ss->len += ret; /* Exclude NULL byte during push. */ - ret = __bpf_stream_push_str(&ss->log, buf->buf, ret); + ret = __bpf_stream_push_str(&ss->log, buf->buf, len); + if (!ret) + ss->len += len; bpf_put_buffers(); return ret; } From 7171b2cecc71956a487d7d645c43b249df92221a Mon Sep 17 00:00:00 2001 From: Jianlin Shi Date: Sat, 15 Aug 2026 16:19:18 +0800 Subject: [PATCH 3/5] bpf: return partial progress from bpf_stream_read on fault bpf_stream_read() pops and frees stream elements after a successful copy_to_user(). If a later copy_to_user() fails, it currently restores only the current element's consumed_len and returns -EFAULT, hiding bytes already delivered to userspace and making the consumed data unrecoverable on retry. On a short copy, keep the successfully copied prefix of the current element and return the number of bytes copied. Return -EFAULT only when no bytes were copied for the call. Fixes: 5ab154f1463a ("bpf: Introduce BPF standard streams") Signed-off-by: Jianlin Shi --- kernel/bpf/stream.c | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/kernel/bpf/stream.c b/kernel/bpf/stream.c index ef5a7b06ffcb1..c1077160074cd 100644 --- a/kernel/bpf/stream.c +++ b/kernel/bpf/stream.c @@ -167,6 +167,7 @@ static int bpf_stream_read(struct bpf_stream *stream, void __user *buf, int len) while (rem_len) { int pos = len - rem_len; + int chunk, n; bool cont; node = bpf_stream_backlog_peek(stream); @@ -180,13 +181,14 @@ static int bpf_stream_read(struct bpf_stream *stream, void __user *buf, int len) cons_len = elem->consumed_len; cont = bpf_stream_consume_elem(elem, &rem_len) == false; - - ret = copy_to_user(buf + pos, elem->str + cons_len, - elem->consumed_len - cons_len); - /* Restore in case of error. */ - if (ret) { - ret = -EFAULT; - elem->consumed_len = cons_len; + chunk = elem->consumed_len - cons_len; + + n = copy_to_user(buf + pos, elem->str + cons_len, chunk); + if (n) { + /* Keep any successfully copied bytes; -EFAULT only if none. */ + elem->consumed_len -= n; + rem_len += n; + ret = (len == rem_len) ? -EFAULT : 0; break; } From 7872ec445a2cbd86c68af84a31c6f19574b5fa3e Mon Sep 17 00:00:00 2001 From: Jianlin Shi Date: Sat, 15 Aug 2026 16:19:19 +0800 Subject: [PATCH 4/5] bpf: reject oversized bpf_stream_vprintk output with -E2BIG bstr_printf() returns the would-be length excluding the trailing NUL. When that value is >= MAX_BPRINTF_BUF the message was truncated, but bpf_stream_push_str() still tried to allocate with the inflated length and failed with -ENOMEM. The boundary case of exactly MAX_BPRINTF_BUF could also copy the trailing NUL into the stream element. Reject such lengths with -E2BIG before charging stream capacity, and tighten bpf_stream_elem_alloc() to accept only payloads strictly shorter than the bprintf buffer. Fixes: 5ab154f1463a ("bpf: Introduce BPF standard streams") Signed-off-by: Jianlin Shi --- kernel/bpf/stream.c | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/kernel/bpf/stream.c b/kernel/bpf/stream.c index c1077160074cd..bd1e98fde4b06 100644 --- a/kernel/bpf/stream.c +++ b/kernel/bpf/stream.c @@ -22,11 +22,11 @@ static struct bpf_stream_elem *bpf_stream_elem_alloc(int len) size_t alloc_size; /* - * Length denotes the amount of data to be written as part of stream element, - * thus includes '\0' byte. We're capped by how much bpf_bprintf_buffers can - * accomodate, therefore deny allocations that won't fit into them. + * Length is the payload pushed into the stream, excluding the + * trailing NUL of the bprintf buffer. Reject anything that cannot + * fit without copying that NUL into the stream element. */ - if (len < 0 || len > max_len) + if (len < 0 || len >= max_len) return NULL; alloc_size = offsetof(struct bpf_stream_elem, str[len]); @@ -245,6 +245,11 @@ __bpf_kfunc int bpf_stream_vprintk(int stream_id, const char *fmt__str, const vo return ret; ret = bstr_printf(data.buf, MAX_BPRINTF_BUF, fmt__str, data.bin_args); + /* Truncation: reject before capacity charge (not -ENOMEM). */ + if (ret >= MAX_BPRINTF_BUF) { + bpf_bprintf_cleanup(&data); + return -E2BIG; + } /* Exclude NULL byte during push. */ ret = bpf_stream_push_str(stream, data.buf, ret); bpf_bprintf_cleanup(&data); From c2242a062c5ccc6162922750366525770e5e0d70 Mon Sep 17 00:00:00 2001 From: Jianlin Shi Date: Sat, 15 Aug 2026 16:19:20 +0800 Subject: [PATCH 5/5] selftests/bpf: cover stream capacity and partial read edge cases Add coverage for the stream fixes requested on the capacity rollback series: - oversized bpf_stream_printk() returns -E2BIG and does not leak capacity for a subsequent successful write; - bpf_prog_stream_read() returns the successfully copied prefix when the userspace buffer straddles an unmapped page. Signed-off-by: Jianlin Shi --- .../testing/selftests/bpf/prog_tests/stream.c | 70 +++++++++++++++++++ tools/testing/selftests/bpf/progs/stream.c | 12 ++++ 2 files changed, 82 insertions(+) diff --git a/tools/testing/selftests/bpf/prog_tests/stream.c b/tools/testing/selftests/bpf/prog_tests/stream.c index e4e9374309e26..fefe6431dc7c9 100644 --- a/tools/testing/selftests/bpf/prog_tests/stream.c +++ b/tools/testing/selftests/bpf/prog_tests/stream.c @@ -58,6 +58,76 @@ void test_stream_syscall(void) stream__destroy(skel); } +void test_stream_oversize(void) +{ + LIBBPF_OPTS(bpf_test_run_opts, opts); + struct stream *skel; + int ret, prog_fd; + char buf[8] = {}; + + skel = stream__open_and_load(); + if (!ASSERT_OK_PTR(skel, "stream__open_and_load")) + return; + + prog_fd = bpf_program__fd(skel->progs.stream_oversize); + ret = bpf_prog_test_run_opts(prog_fd, &opts); + ASSERT_OK(ret, "oversize run"); + ASSERT_EQ(opts.retval, -E2BIG, "oversize retval"); + + /* Oversized push must not permanently consume capacity. */ + prog_fd = bpf_program__fd(skel->progs.stream_syscall); + ret = bpf_prog_test_run_opts(prog_fd, &opts); + ASSERT_OK(ret, "syscall run"); + ASSERT_OK(opts.retval, "syscall retval"); + + ret = bpf_prog_stream_read(prog_fd, BPF_STREAM_STDOUT, buf, sizeof(buf), NULL); + ASSERT_EQ(ret, 3, "bytes after oversize"); + ASSERT_OK(memcmp(buf, "foo", 3), "payload after oversize"); + + stream__destroy(skel); +} + +void test_stream_partial_read(void) +{ + LIBBPF_OPTS(bpf_test_run_opts, opts); + struct stream *skel; + int ret, prog_fd; + long page_size; + char *page, *buf; + char rest[8] = {}; + + skel = stream__open_and_load(); + if (!ASSERT_OK_PTR(skel, "stream__open_and_load")) + return; + + prog_fd = bpf_program__fd(skel->progs.stream_syscall); + ret = bpf_prog_test_run_opts(prog_fd, &opts); + ASSERT_OK(ret, "ret"); + ASSERT_OK(opts.retval, "retval"); + + page_size = sysconf(_SC_PAGESIZE); + page = mmap(NULL, page_size * 2, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (!ASSERT_NEQ(page, MAP_FAILED, "mmap")) { + stream__destroy(skel); + return; + } + /* Leave only the first page mapped so a straddling copy faults. */ + ASSERT_OK(munmap(page + page_size, page_size), "munmap second page"); + + buf = page + page_size - 1; + ret = bpf_prog_stream_read(prog_fd, BPF_STREAM_STDOUT, buf, 3, NULL); + ASSERT_EQ(ret, 1, "partial bytes"); + ASSERT_EQ(buf[0], 'f', "first byte"); + + ret = bpf_prog_stream_read(prog_fd, BPF_STREAM_STDOUT, rest, sizeof(rest), NULL); + ASSERT_EQ(ret, 2, "remaining bytes"); + ASSERT_OK(memcmp(rest, "oo", 2), "remaining data"); + + munmap(page, page_size); + stream__destroy(skel); +} + static void test_address(struct bpf_program *prog, unsigned long *fault_addr_p) { LIBBPF_OPTS(bpf_test_run_opts, opts); diff --git a/tools/testing/selftests/bpf/progs/stream.c b/tools/testing/selftests/bpf/progs/stream.c index 8e8e1339dc743..385349aa3c693 100644 --- a/tools/testing/selftests/bpf/progs/stream.c +++ b/tools/testing/selftests/bpf/progs/stream.c @@ -36,7 +36,12 @@ struct { } array SEC(".maps"); #define ENOSPC 28 +#define E2BIG 7 #define _STR "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" +#define _X64 "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" +/* 1024 bytes: truncated by bstr_printf, must return -E2BIG. */ +#define _BIG_STR (_X64 _X64 _X64 _X64 _X64 _X64 _X64 _X64 \ + _X64 _X64 _X64 _X64 _X64 _X64 _X64 _X64) int size; u64 fault_addr; @@ -119,6 +124,13 @@ int stream_syscall(void *ctx) return 0; } +SEC("syscall") +__success __retval(-E2BIG) +int stream_oversize(void *ctx) +{ + return bpf_stream_printk(BPF_STDOUT, _BIG_STR); +} + SEC("syscall") __arch_x86_64 __arch_arm64