* Responsible Disclosure: Heap Out-of-Bounds Write via Integer Underflow in libbpf parse_cpu_mask_str() (libbpf.c:14446)
@ 2026-07-03 12:22 Vuln seeker Cyber Security Team
2026-07-07 14:47 ` John Fastabend
0 siblings, 1 reply; 3+ messages in thread
From: Vuln seeker Cyber Security Team @ 2026-07-03 12:22 UTC (permalink / raw)
To: bpf; +Cc: security
[-- Attachment #1.1: Type: text/plain, Size: 6528 bytes --]
Dear libbpf Security Team,
I am responsibly disclosing a heap out-of-bounds write vulnerability in
libbpf's CPU mask parser. An integer underflow in parse_cpu_mask_str()
causes a memset to write far past a shrunk heap allocation, reachable
end-to-end through the public libbpf_num_possible_cpus() API.
*VULNERABILITY SUMMARY*
parse_cpu_mask_str() in src/libbpf.c validates each parsed CPU range with:
if (start < 0 || start > end)
return -EINVAL;
but never checks start against the running *mask_sz. When an out-of-order
or overlapping range is encountered (e.g. "2-3,0-1"), the second iteration
causes realloc(*mask, end+1) to shrink the buffer, after which:
memset(tmp + *mask_sz, 0, start - *mask_sz) // libbpf.c:14446
computes start - *mask_sz as a negative int (e.g. 0 - 4 = -4), which
converts to a huge size_t. The memset then writes far past the shrunk
allocation, producing a heap out-of-bounds write.
Two inputs have been confirmed:
- "2-3,0-1": start - *mask_sz = 0 - 4 = -4 -> negative-size-param
(size=-4)
- "5,0": start - *mask_sz = 0 - 6 = -6 -> negative-size-param
(size=-6)
*AFFECTED COMPONENT*
Repository : https://github.com/libbpf/libbpf
Commit : caa25e0b72746fef7ea01497ece7db57187f132c (HEAD validated)
File : src/libbpf.c
Function : parse_cpu_mask_str() (line 14446)
*CWE CLASSIFICATION*
Primary : CWE-191 (Integer Underflow)
Resultant : CWE-122 (Heap-based Buffer Overflow)
Resultant : CWE-787 (Out-of-bounds Write)
*CALL CHAIN (end-to-end via public API)*
libbpf_num_possible_cpus() (libbpf.c:14500 -- LIBBPF_API, exported in
libbpf.map)
parse_cpu_mask_file("/sys/devices/system/cpu/possible")
(libbpf.c:14486)
parse_cpu_mask_str(<file bytes>)
(libbpf.c:14412)
memset(tmp + *mask_sz, 0, start - *mask_sz) OOB
(libbpf.c:14446)
The vulnerability is reached through a documented, exported public symbol
-- not an internal helper. Any libbpf consumer that calls
libbpf_num_possible_cpus() is exposed whenever the bytes at
/sys/devices/system/cpu/possible are malformed or out of order.
*REACHABILITY*
On stock Linux the kernel always writes ascending CPU ranges to
/sys/devices/system/cpu/possible, so the default host sysfs path is safe.
The realistic exposure is:
- A host or container where that sysfs path is attacker-influenced
(custom or overlay sysfs, crafted mount namespace, or a non-Linux/emulated
CPU-mask source)
- Any downstream consumer that feeds untrusted CPU-mask strings to the
internal parse_cpu_mask_str helper directly
This is why the CVSS score is conservative (Medium). The crash is genuine
on the shipping public API, not just an internal code path.
*REPRODUCTION*
Prerequisites:
- clang with AddressSanitizer, libelf-dev, zlib1g-dev, unshare
(util-linux)
Steps:
1. Clone and build libbpf with ASan:
git clone https://github.com/libbpf/libbpf.git repo
cd repo && git checkout caa25e0b72746fef7ea01497ece7db57187f132c
# Build all .c files with -fsanitize=address and link libbpf.so
2. Build the e2e harness (calls ONLY the public
libbpf_num_possible_cpus()):
clang -O1 -g -fsanitize=address -I<incl> e2e.c -Wl,-rpath,.
libbpf.so -o e2e
3. Bind-mount the crafted possible-cpus file and invoke the public API:
unshare -m -r sh -c \
"mount --bind poc-possible-cpus.txt
/sys/devices/system/cpu/possible; \
ASAN_OPTIONS=abort_on_error=0:exitcode=99 ./e2e"
*Expected output (UNPATCHED):*
ERROR: AddressSanitizer: negative-size-param: (size=-4)
#0 in __asan_memset
#1 in parse_cpu_mask_str src/libbpf.c:14446
#2 in parse_cpu_mask_file src/libbpf.c:14486
#3 in libbpf_num_possible_cpus src/libbpf.c:14500
#4 in main
0x... is located 2 bytes after 2-byte region [...)
allocated by thread T0 in parse_cpu_mask_str src/libbpf.c:14440 (realloc)
Valid input "0-31" returns libbpf_num_possible_cpus() = 32 cleanly with no
ASan output.
Full build and run instructions, including the bind-mount technique and
patch verification steps, are provided in the attached REPRODUCE.md.
*SEVERITY ASSESSMENT*
CVSS 4.0 Vector : AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:L
CVSS 4.0 Score : 4.0 (Medium)
CWE : CWE-122/787 via CWE-191
Scored Medium due to the high attack complexity (attacker must influence
the sysfs path content) and the local attack vector. The integrity and
availability impact is low to moderate: the OOB write corrupts adjacent
heap memory, which can lead to process crash or heap metadata corruption in
sanitized and production builds respectively.
Note: this finding is distinct from CVE-2025-29481 (bpf_object__init_prog);
no existing GHSA or fix commit addresses parse_cpu_mask_str.
*SUGGESTED FIX*
Add a check in parse_cpu_mask_str() to reject a new sub-range whose start
falls below the current *mask_sz, preventing the shrink-then-underflow
sequence:
if (start < *mask_sz)
return -EINVAL; // "CPU range out of order or overlapping"
A verified patch (fix.diff, approximately 5 lines) is attached. It has been
tested against three scenarios:
- "2-3,0-1": returns -22, log message "CPU range out of order or
overlapping", no crash
- "5,0": returns -22, no crash
- "0-31": libbpf_num_possible_cpus() = 32, no regression
*ATTACHED ARTIFACTS*
report.md Detailed technical assessment
fix.diff Unified diff patch (verified at HEAD caa25e0b)
asan-trace-e2e.txt ASan trace from the public API e2e harness
asan-trace.txt ASan trace from the direct parse_cpu_mask_str
harness
poc-possible-cpus.txt Crafted /sys/devices/system/cpu/possible input
("2-3,0-1")
poc-cpumask.txt Direct function-level input (same content)
REPRODUCE.md Step-by-step reproduction walkthrough
We discovered this issue via static analysis and validated it end-to-end on
2026-06-25 against HEAD commit caa25e0b, with a QA re-validation on a clean
box confirming both the crash and the patch on the same day. We are sharing
this report in accordance with responsible disclosure practices and are
happy to coordinate on a remediation timeline with the libbpf maintainers.
Please do not hesitate to reach out if you require additional information,
a different proof-of-concept format, or clarification on any part of this
report.
Reporter: Yevgen Goncharuk <yevgen@vulnseeker.org>.
Signed-off-by: Yevgen Goncharuk <yevgen@vulnseeker.org>
Author: Yevgen Goncharuk
Best Regards,
Yevgen Goncharuk
[-- Attachment #1.2: Type: text/html, Size: 7826 bytes --]
[-- Attachment #2: asan-trace-e2e.txt --]
[-- Type: text/plain, Size: 2159 bytes --]
# E2E trace — crash reached through the PUBLIC libbpf API libbpf_num_possible_cpus()
# (not the internal parse_cpu_mask_str). The malformed CPU-mask data enters via the
# real file-read path parse_cpu_mask_file() reading /sys/devices/system/cpu/possible.
# A crafted possible-cpus file ("2-3,0-1") was bind-mounted over that sysfs path in an
# unshared mount namespace; the e2e harness calls ONLY libbpf_num_possible_cpus().
=================================================================
==PID==ERROR: AddressSanitizer: negative-size-param: (size=-4)
#0 <addr> in __asan_memset (<scratch>/e2e+0xca09c)
#1 <addr> in parse_cpu_mask_str <scratch>/repo/src/libbpf.c:14446:3
#2 <addr> in parse_cpu_mask_file <scratch>/repo/src/libbpf.c:14486:9
#3 <addr> in libbpf_num_possible_cpus <scratch>/repo/src/libbpf.c:14500:8
#4 <addr> in main <scratch>/e2e.c:4:13
#5 <addr> in __libc_start_main libc.so.6
#6 <addr> in _start (<scratch>/e2e+0x2c360)
0x502000000034 is located 2 bytes after 2-byte region [0x502000000030,0x502000000032)
allocated by thread T0 here:
#0 <addr> in realloc (<scratch>/e2e+0xcc5b0)
#1 <addr> in parse_cpu_mask_str <scratch>/repo/src/libbpf.c:14440:9
#2 <addr> in parse_cpu_mask_file <scratch>/repo/src/libbpf.c:14486:9
#3 <addr> in libbpf_num_possible_cpus <scratch>/repo/src/libbpf.c:14500:8
#4 <addr> in main <scratch>/e2e.c:4:13
SUMMARY: AddressSanitizer: negative-size-param (<scratch>/e2e) in __asan_memset
==PID==ABORTING
# Captured at libbpf HEAD caa25e0b72746fef7ea01497ece7db57187f132c.
# OOB memset sink: src/libbpf.c:14446 memset(tmp + *mask_sz, 0, start - *mask_sz)
# shrunk realloc region: src/libbpf.c:14440 realloc(*mask, end + 1)
# Public entry: libbpf_num_possible_cpus() (LIBBPF_API, exported in libbpf.map).
# Second e2e input "5,0" -> negative-size-param (size=-6) at the same sink, same stack.
#
# Patch (fix.diff) re-verified on the SAME e2e harness:
# crafted "2-3,0-1" -> libbpf: "CPU range out of order or overlapping" -> returns -22, no ASan crash
# valid "0-31" -> libbpf_num_possible_cpus() = 32 (clean)
[-- Attachment #3: asan-trace.txt --]
[-- Type: text/plain, Size: 1452 bytes --]
=================================================================
==PID==ERROR: AddressSanitizer: negative-size-param: (size=-4)
#0 <addr> in __asan_memset (<scratch>/poc+0xf043c)
#1 <addr> in parse_cpu_mask_str <scratch>/repo/src/libbpf.c:14446:3
#2 <addr> in main <scratch>/harness.c:12:15
#3 <addr> in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
#4 <addr> in __libc_start_main csu/../csu/libc-start.c:360:3
#5 <addr> in _start (<scratch>/poc+0x52700)
0x502000000034 is located 2 bytes after 2-byte region [0x502000000030,0x502000000032)
allocated by thread T0 here:
#0 <addr> in realloc (<scratch>/poc+0xf2950)
#1 <addr> in parse_cpu_mask_str <scratch>/repo/src/libbpf.c:14440:9
#2 <addr> in main <scratch>/harness.c:12:15
#3 <addr> in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
SUMMARY: AddressSanitizer: negative-size-param (<scratch>/poc) in __asan_memset
==PID==ABORTING
# Trace captured at libbpf HEAD caa25e0b72746fef7ea01497ece7db57187f132c.
# memset OOB write is at src/libbpf.c:14446 (memset(tmp + *mask_sz, 0, start - *mask_sz));
# the shrunk realloc region is allocated at src/libbpf.c:14440 (realloc(*mask, end + 1)).
# Reproduced by linking the real parse_cpu_mask_str from a full ASan build of libbpf
# (clang -O1 -fsanitize=address) against a 3-line harness; input "2-3,0-1".
# Second PoC input "5,0" yields size=-6 at the same sink.
[-- Attachment #4: fix.diff --]
[-- Type: application/octet-stream, Size: 443 bytes --]
diff --git a/src/libbpf.c b/src/libbpf.c
index ab2071f..7ab8932 100644
--- a/src/libbpf.c
+++ b/src/libbpf.c
@@ -14437,6 +14437,11 @@ int parse_cpu_mask_str(const char *s, bool **mask, int *mask_sz)
err = -EINVAL;
goto cleanup;
}
+ if (start < *mask_sz) {
+ pr_warn("CPU range out of order or overlapping in %s\n", s);
+ err = -EINVAL;
+ goto cleanup;
+ }
tmp = realloc(*mask, end + 1);
if (!tmp) {
err = -ENOMEM;
[-- Attachment #5: poc-cpumask.txt --]
[-- Type: text/plain, Size: 8 bytes --]
2-3,0-1
[-- Attachment #6: poc-possible-cpus.txt --]
[-- Type: text/plain, Size: 8 bytes --]
2-3,0-1
[-- Attachment #7: REPORTING.md --]
[-- Type: text/markdown, Size: 792 bytes --]
# Reporting channel — libbpf
- **Channel class:** Kernel security team email (private) — kernel CNA
- **PVR:** OFF — no GitHub Security Advisory route.
- **Primary contact:** `security@kernel.org` — canonical, because libbpf's authoritative source is `tools/lib/bpf` in the kernel bpf-next tree and CVEs are issued by the kernel CNA.
- Maintainer: Andrii Nakryiko `andrii.nakryiko@gmail.com`
- Technical list `bpf@vger.kernel.org` is public — NOT for embargoed reports.
- **Alternative (for the standalone/userspace parsing surface OSS-Fuzz exercises):** file via the OSS-Fuzz issue tracker (90-day embargo).
- **Google Patch-Rewards eligible:** ✅ yes (OSS-Fuzz-integrated).
Reporter: Yevgen Goncharuk <yevgen@vulnseeker.org>. Nothing transmitted by tooling — user sends.
[-- Attachment #8: report.md --]
[-- Type: text/markdown, Size: 3551 bytes --]
# libbpf @ caa25e0b72746fef7ea01497ece7db57187f132c (src/libbpf.c:14446, parse_cpu_mask_str)
| Field | Value |
|---|---|
| Verdict | confirmed-poc-e2e (real public API entry point) |
| CVSS | 4.0 Medium AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:L |
| Patch | build-verified + e2e-verified (rebuilt at HEAD: blocks both crash inputs through the public API, valid inputs preserved) |
| Dedup | novel |
| Channel | GitHub Private Vulnerability Reporting on libbpf/libbpf; maintainer andrii.nakryiko@gmail.com. OSS-Fuzz-integrated -> landed patch Google Patch-Rewards eligible. |
## PoC
```
E2E via PUBLIC API: AddressSanitizer: negative-size-param (size=-4) in __asan_memset
<- parse_cpu_mask_str src/libbpf.c:14446
<- parse_cpu_mask_file src/libbpf.c:14486
<- libbpf_num_possible_cpus src/libbpf.c:14500 (LIBBPF_API, exported)
<- main
Write 2 bytes after 2-byte realloc(end+1) region. The malformed CPU mask enters
through the real file path: libbpf_num_possible_cpus() reads
/sys/devices/system/cpu/possible. PoC bind-mounts a crafted possible-cpus file
("2-3,0-1") over that sysfs path in an unshared mount namespace and calls only the
public libbpf_num_possible_cpus() linked against the REAL libbpf.so (clang -O1
-fsanitize=address, all 21 .c files). Second input "5,0" -> size=-6, same stack.
Valid "0-31" -> returns 32; "0-3"/"0-1,2-3" parse cleanly. Reproduced at HEAD
caa25e0b. (A function-level harness calling parse_cpu_mask_str directly also
reproduces — see poc-cpumask.txt — but the e2e PoC above hits the shipping public
entry point, not the internal helper.)
```
## Notes
Heap OOB write in parse_cpu_mask_str: only checks start>end, never start vs running *mask_sz. Out-of-order/overlapping range shrinks buffer via realloc(end+1) then memset(tmp+*mask_sz, 0, start-*mask_sz) underflows int (0-4) -> size_t huge count past shrunk alloc. CWE-122/787 via CWE-191. VERDICT confirmed-poc-e2e: the crash now reproduces through the SHIPPING PUBLIC API libbpf_num_possible_cpus() (LIBBPF_API, exported in libbpf.map) reading /sys/devices/system/cpu/possible via parse_cpu_mask_file -> parse_cpu_mask_str. The e2e harness links the real libbpf.so and references ONLY the public symbol; a crafted possible-cpus file is bind-mounted over the sysfs path in an unshared mount namespace. ASan negative-size-param (size=-4 for "2-3,0-1", size=-6 for "5,0") at src/libbpf.c:14446. Reachability nuance (keeps it Medium): on stock Linux the kernel always writes ascending ranges to that sysfs file, so the default host producer is trusted; realistic exposure is a host/container where that file is attacker-influenced, or a downstream consumer feeding untrusted CPU-mask strings to the helpers. Patch (fix.diff, 5 lines: reject start<*mask_sz) git-apply-checks OK, and re-verified on the e2e harness: blocks both crash inputs (returns -22, "CPU range out of order or overlapping") and preserves valid input (0-31 -> 32). Packet contains: report.md, fix.diff, REPRODUCE.md, READINESS.md, asan-trace.txt, asan-trace-e2e.txt, poc-cpumask.txt, poc-possible-cpus.txt. Box scratch cleaned, no internals leaked. CodeQL note was cpp/missing-check-scanf (benign here) -- right function, wrong reason; real bug is the start-*mask_sz underflow.
## Dedup
Distinct from CVE-2025-29481 (bpf_object__init_prog). No GHSA/commit for parse_cpu_mask_str; sink unchanged in current master line 14446.
Reporter: Yevgen Goncharuk <yevgen@vulnseeker.org>. Validated 2026-06-25; QA re-validated on a clean box at HEAD 2026-06-25 (crash + patch both re-confirmed).
[-- Attachment #9: REPRODUCE.md --]
[-- Type: text/markdown, Size: 6282 bytes --]
# REPRODUCE — libbpf parse_cpu_mask_str heap OOB write
Target: libbpf @ caa25e0b72746fef7ea01497ece7db57187f132c
Sink: `src/libbpf.c:14446` — `memset(tmp + *mask_sz, 0, start - *mask_sz)`
Bug class: CWE-787 / CWE-122 heap out-of-bounds write via CWE-191 int underflow.
## Mechanism
`parse_cpu_mask_str` validates only `start < 0 || start > end`. It never checks
`start` against the running `*mask_sz`. An out-of-order / overlapping CPU range
(e.g. a later sub-range with a smaller end than an earlier one) causes
`realloc(*mask, end + 1)` to SHRINK the buffer, after which
`memset(tmp + *mask_sz, 0, start - *mask_sz)` computes a negative `int`
(`start - *mask_sz`) that converts to a huge `size_t`, writing far past the
shrunk allocation.
- Input `"2-3,0-1"`: iter1 start=2,end=3 -> realloc 4 bytes, mask_sz=4;
iter2 start=0,end=1 -> realloc 2 bytes (shrink), `start-*mask_sz = 0-4 = -4`
-> ASan `negative-size-param (size=-4)`.
- Input `"5,0"`: `0-6 = -6` -> `negative-size-param (size=-6)`.
## Confirmed end-to-end: the crash is driven through the PUBLIC libbpf API
The OOB is reached without touching any internal symbol. `libbpf_num_possible_cpus()`
is a `LIBBPF_API` public function (exported in `src/libbpf.map`). It calls
`parse_cpu_mask_file("/sys/devices/system/cpu/possible", ...)`, which `read(2)`s the
file and hands the bytes to `parse_cpu_mask_str`. A malformed possible-cpus file
content reaches the vulnerable `memset`:
```
main
-> libbpf_num_possible_cpus() [PUBLIC, libbpf.c:14489]
-> parse_cpu_mask_file("/sys/.../cpu/possible") [libbpf.c:14500/14486]
-> parse_cpu_mask_str(<file bytes>) [libbpf.c:14412]
-> memset(tmp + *mask_sz, 0, start - *mask_sz) OOB [libbpf.c:14446]
```
### Build + run (copy-paste, e2e)
Requires clang with ASan, libelf-dev, zlib1g-dev, git, and `unshare` (util-linux).
```sh
SCRATCH=$(mktemp -d)
cd "$SCRATCH"
git clone https://github.com/libbpf/libbpf.git repo
cd repo
git checkout caa25e0b72746fef7ea01497ece7db57187f132c # or current HEAD
# Build every libbpf .c with ASan (-fPIC for the shared library)
cd src
for f in *.c; do
clang -O1 -g -fsanitize=address -fno-omit-frame-pointer -fPIC -D_GNU_SOURCE \
-I. -I../include -I../include/uapi -c "$f" -o "$SCRATCH/obj_${f%.c}.o"
done
# Link the REAL libbpf shared object (exports libbpf_num_possible_cpus)
clang -shared -fsanitize=address -o "$SCRATCH/libbpf.so" "$SCRATCH"/obj_*.o -lelf -lz
nm -D "$SCRATCH/libbpf.so" | grep libbpf_num_possible_cpus # exported public symbol
# Public-header include tree for <bpf/libbpf.h>
mkdir -p "$SCRATCH/incl/bpf"; cp "$SCRATCH/repo/src/"*.h "$SCRATCH/incl/bpf/"
# E2E harness: calls ONLY the public API — no internal symbol references
cat > "$SCRATCH/e2e.c" <<'EOF'
#include <stdio.h>
#include <bpf/libbpf.h>
int main(void) {
int n = libbpf_num_possible_cpus();
printf("libbpf_num_possible_cpus() = %d\n", n);
return 0;
}
EOF
clang -O1 -g -fsanitize=address -I"$SCRATCH/incl" "$SCRATCH/e2e.c" \
-Wl,-rpath,"$SCRATCH" "$SCRATCH/libbpf.so" -o "$SCRATCH/e2e"
# Crafted malformed possible-cpus files (real product input)
printf "2-3,0-1\n" > "$SCRATCH/fake_possible" # out-of-order range -> size=-4
printf "5,0\n" > "$SCRATCH/fake_possible2" # -> size=-6
printf "0-31\n" > "$SCRATCH/valid_possible" # valid -> 32 cpus
# Drive the REAL path: bind-mount the crafted file over sysfs in a private mount ns,
# then call the public API. (unshare -r maps to root inside the user+mount ns.)
run() { unshare -m -r sh -c "mount --bind \"$1\" /sys/devices/system/cpu/possible; \
ASAN_OPTIONS=abort_on_error=0:exitcode=99 \"$SCRATCH/e2e\""; echo "exit=$?"; }
run "$SCRATCH/fake_possible" # AddressSanitizer: negative-size-param (size=-4), exit=99
run "$SCRATCH/fake_possible2" # negative-size-param (size=-6), exit=99
run "$SCRATCH/valid_possible" # libbpf_num_possible_cpus() = 32, exit=0
```
Observed ASan stack (see `asan-trace-e2e.txt`):
`__asan_memset <- parse_cpu_mask_str:14446 <- parse_cpu_mask_file:14486 <- libbpf_num_possible_cpus:14500 <- main`.
The PoC input file `poc-possible-cpus.txt` contains `2-3,0-1` (place it at
`/sys/devices/system/cpu/possible`, e.g. via the bind-mount above). The original
direct-call input file `poc-cpumask.txt` (also `2-3,0-1`) remains for the
function-level harness.
## Patch verification (re-run against the e2e harness)
```sh
cd "$SCRATCH/repo"
git apply --check /path/to/fix.diff # applies clean at HEAD
git apply /path/to/fix.diff
# rebuild libbpf.c, relink libbpf.so, re-run the SAME e2e harness:
cd src
clang -O1 -g -fsanitize=address -fno-omit-frame-pointer -fPIC -D_GNU_SOURCE \
-I. -I../include -I../include/uapi -c libbpf.c -o "$SCRATCH/obj_libbpf.o"
clang -shared -fsanitize=address -o "$SCRATCH/libbpf.so" "$SCRATCH"/obj_*.o -lelf -lz
# run fake_possible ("2-3,0-1") -> libbpf: "CPU range out of order or overlapping" -> returns -22, no crash, exit=0
# run fake_possible2 ("5,0") -> returns -22, no crash, exit=0
# run valid_possible ("0-31") -> libbpf_num_possible_cpus() = 32, exit=0
```
Confirmed: patch blocks both e2e crash inputs through the public API and preserves
valid input parsing.
## Reachability note (updated — e2e via public API)
`libbpf_num_possible_cpus()` is a PUBLIC, exported API (`LIBBPF_API`, present in
`src/libbpf.map`). It reads `/sys/devices/system/cpu/possible` and parses its
contents through `parse_cpu_mask_file` -> `parse_cpu_mask_str`. The OOB write is
therefore reachable end-to-end from a normal libbpf consumer whenever the bytes at
that sysfs path are malformed/out-of-order. On stock Linux the kernel always emits
ascending ranges there, so the default host path is safe; the realistic exposure is
(a) a host/container where that file is attacker-influenced (custom/overlay sysfs,
crafted mount, or a non-Linux/emulated cpu-mask source), or (b) any downstream
consumer that feeds untrusted CPU-mask strings to the internal helpers. The crash
is genuine on the SHIPPING public API, not just the internal helper. Scored Medium
(CVSS 4.0, AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:L) because the default sysfs producer
is trusted; the e2e PoC demonstrates the sink is reachable through the public entry
point given malformed input at that path.
^ permalink raw reply related [flat|nested] 3+ messages in thread
* Re: Responsible Disclosure: Heap Out-of-Bounds Write via Integer Underflow in libbpf parse_cpu_mask_str() (libbpf.c:14446)
2026-07-03 12:22 Responsible Disclosure: Heap Out-of-Bounds Write via Integer Underflow in libbpf parse_cpu_mask_str() (libbpf.c:14446) Vuln seeker Cyber Security Team
@ 2026-07-07 14:47 ` John Fastabend
2026-07-08 7:33 ` Vuln seeker Cyber Security Team
0 siblings, 1 reply; 3+ messages in thread
From: John Fastabend @ 2026-07-07 14:47 UTC (permalink / raw)
To: Vuln seeker Cyber Security Team; +Cc: bpf
On Fri, Jul 03, 2026 at 05:22:30PM +0500, Vuln seeker Cyber Security Team wrote:
>Dear libbpf Security Team,
[...]
>libbpf's CPU mask parser. An integer underflow in parse_cpu_mask_str()
>causes a memset to write far past a shrunk heap allocation, reachable
>end-to-end through the public libbpf_num_possible_cpus() API.
Send the fix seems worth checking to me if we are already checking
start/end. If some app is exposing user input to this then I would
push a report to them.
[...]
> - Any downstream consumer that feeds untrusted CPU-mask strings to
> the
>internal parse_cpu_mask_str helper directly
Not sure why untrusted input is getting here but sure if someone
is piping user input to libbpf let them know. I think if you have
untrusted user input hitting libbpf you have bigger problems.
[...]
>
> if (start < *mask_sz)
> return -EINVAL; // "CPU range out of order or overlapping"
>
Send a fix. Thanks.
^ permalink raw reply [flat|nested] 3+ messages in thread
* Re: Responsible Disclosure: Heap Out-of-Bounds Write via Integer Underflow in libbpf parse_cpu_mask_str() (libbpf.c:14446)
2026-07-07 14:47 ` John Fastabend
@ 2026-07-08 7:33 ` Vuln seeker Cyber Security Team
0 siblings, 0 replies; 3+ messages in thread
From: Vuln seeker Cyber Security Team @ 2026-07-08 7:33 UTC (permalink / raw)
To: John Fastabend; +Cc: bpf
[-- Attachment #1.1: Type: text/plain, Size: 1278 bytes --]
Hello John,
As requested, I have reattached the fix for the identified vulnerability.
Please let me know if you are able to access the file now.
Best regards,
On Tue, Jul 7, 2026 at 7:47 PM John Fastabend <john.fastabend@gmail.com>
wrote:
> On Fri, Jul 03, 2026 at 05:22:30PM +0500, Vuln seeker Cyber Security Team
> wrote:
> >Dear libbpf Security Team,
>
> [...]
>
> >libbpf's CPU mask parser. An integer underflow in parse_cpu_mask_str()
> >causes a memset to write far past a shrunk heap allocation, reachable
> >end-to-end through the public libbpf_num_possible_cpus() API.
>
> Send the fix seems worth checking to me if we are already checking
> start/end. If some app is exposing user input to this then I would
> push a report to them.
>
> [...]
>
> > - Any downstream consumer that feeds untrusted CPU-mask strings to
> > the
> >internal parse_cpu_mask_str helper directly
>
>
> Not sure why untrusted input is getting here but sure if someone
> is piping user input to libbpf let them know. I think if you have
> untrusted user input hitting libbpf you have bigger problems.
>
> [...]
>
> >
> > if (start < *mask_sz)
> > return -EINVAL; // "CPU range out of order or overlapping"
> >
>
> Send a fix. Thanks.
>
[-- Attachment #1.2: Type: text/html, Size: 1759 bytes --]
[-- Attachment #2: fix.diff --]
[-- Type: application/octet-stream, Size: 443 bytes --]
diff --git a/src/libbpf.c b/src/libbpf.c
index ab2071f..7ab8932 100644
--- a/src/libbpf.c
+++ b/src/libbpf.c
@@ -14437,6 +14437,11 @@ int parse_cpu_mask_str(const char *s, bool **mask, int *mask_sz)
err = -EINVAL;
goto cleanup;
}
+ if (start < *mask_sz) {
+ pr_warn("CPU range out of order or overlapping in %s\n", s);
+ err = -EINVAL;
+ goto cleanup;
+ }
tmp = realloc(*mask, end + 1);
if (!tmp) {
err = -ENOMEM;
^ permalink raw reply related [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-07-08 7:33 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-03 12:22 Responsible Disclosure: Heap Out-of-Bounds Write via Integer Underflow in libbpf parse_cpu_mask_str() (libbpf.c:14446) Vuln seeker Cyber Security Team
2026-07-07 14:47 ` John Fastabend
2026-07-08 7:33 ` Vuln seeker Cyber Security Team
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox