Skip to content

Fix two BPF verifier OOB rejections on strict/newer kernels#6

Open
jevansnyc wants to merge 1 commit into
masterfrom
fix/verifier-oob-strict-kernels
Open

Fix two BPF verifier OOB rejections on strict/newer kernels#6
jevansnyc wants to merge 1 commit into
masterfrom
fix/verifier-oob-strict-kernels

Conversation

@jevansnyc

Copy link
Copy Markdown

Summary

On stricter/newer kernels claudefeed fails to load with BPF_VERIFIER_REJECTED (reproduced on Linux 6.12 arm64). The handle_execve program has two out-of-bounds accesses, both from the same root cause: a bound the compiler proves, but the verifier doesn't carry. More lenient kernels happened to accept them; stricter ones don't.

Note: yeet truncates the verifier log, which makes it look like the failure is in name_matches. Loading the object directly — sudo bpftool prog loadall claudefeed.bpf.o /sys/fs/bpf/t — gives the real, untruncated verdict, and the first failure is actually in read_cmdline.

Bug 1 — read_cmdline, OOB write

592: call bpf_probe_read_user_str
invalid access to memory, mem_size=848 off=844 size=256
R1 max value is outside of the allowed memory range

The ARGSIZE write to &e->cmdline[sz] overflows the event record. The sz > ARGS_CEIL break keeps sz <= ARGS_CEIL, but the compiler folds the resulting clamp away as redundant, and the verifier — which doesn't carry that bound through the unrolled accumulation — sees the offset reach 512.

Fix: clamp the offset behind barrier_var() so the optimizer can't prove the clamp redundant, leaving the verifier a register it can bound.

Bug 2 — name_matches, OOB read

invalid access to map value, value_size=20 off=20 size=1
R0 min value is outside of the allowed memory range

needle_len lives in .data and is verifier-unbounded. The compiler trusts the nl <= NEEDLE_LEN guard and drops the & (NEEDLE_LEN-1) index mask, while the verifier lets j run past NEEDLE_LEN via the j >= nl exit — reading past the 16-byte needle.

Fix: constant trip count with no data-dependent break, so clang unrolls to NEEDLE_LEN iterations with compile-time-constant indices; gate each compare with j < nl to preserve early-mismatch semantics.

Testing

  • bpftool prog loadall -> all 5 programs (handle_execve, handle_exit, handle_openat, handle_tcp_connect, handle_inet_listen) load.
  • yeet run . -- --secs=8 on 6.12 arm64 -> streams exec/open/conn/listen events; no rejection.
  • Both changes are behavior-preserving.

claudefeed fails to load with BPF_VERIFIER_REJECTED on stricter kernels
(seen on Linux 6.12 arm64). Two out-of-bounds accesses in handle_execve,
both from a bound the compiler proves but the verifier does not carry:

- read_cmdline: the ARGSIZE write to &e->cmdline[sz] is flagged OOB
  (mem_size=848 off=844 size=256). The `sz > ARGS_CEIL` break keeps
  sz <= ARGS_CEIL, but that bound is lost across the unrolled
  accumulation. Clamp the offset behind barrier_var() so the optimizer
  can't fold the clamp away as redundant, leaving the verifier a
  bounded register.

- name_matches: needle[] is read out of bounds (value_size=20 off=20).
  needle_len lives in .data and is unbounded to the verifier; the
  compiler trusts the `nl <= NEEDLE_LEN` guard and drops the index mask,
  while the verifier runs j past NEEDLE_LEN via the `j >= nl` exit. Use
  a constant trip count with no data-dependent break so clang unrolls to
  NEEDLE_LEN iterations with compile-time-constant indices; gate each
  compare with `j < nl` to keep early-mismatch semantics.

Both changes are behavior-preserving. Verified with `bpftool prog
loadall` (all 5 programs load) and a live `yeet run` on 6.12 arm64.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jevansnyc jevansnyc marked this pull request as ready for review June 18, 2026 18:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant