* 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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.