Skip to content

Fix #1099: tighten ValidMapKeyValue packet bound; inline bounds checks#1100

Merged
elazarg merged 5 commits intomainfrom
fix/issue-1099-validmapkeyvalue-packet-size
Apr 26, 2026
Merged

Fix #1099: tighten ValidMapKeyValue packet bound; inline bounds checks#1100
elazarg merged 5 commits intomainfrom
fix/issue-1099-validmapkeyvalue-packet-size

Conversation

@elazarg
Copy link
Copy Markdown
Collaborator

@elazarg elazarg commented Apr 26, 2026

Summary

Fixes #1099 and reshapes the bounds-check helpers around it into two primitives that each per-region case composes directly.

The bug

ValidMapKeyValue's T_PACKET branch checked the upper bound against the loose max_packet_size constant, while ValidAccess's T_PACKET dereference path used the runtime variable_registry.packet_size() for a tighter check. Helper map key/value pointers are real reads/writes through the pointer (the helper copies key_size / value_size bytes), so they need the same runtime ceiling as direct dereferences. Using max_packet_size was unsoundly loose: a program could pass verification while accessing past the actual runtime packet_size.

The fix at the call site now reads as one line:

require_upper_bound(ub, variable_registry.packet_size(), "Upper bound must be at most packet_size");

The refactor

Replaced the previous region_bounds / RegionBounds / require_region_bounds apparatus (and a brief detour through templated overloads and a static/dynamic taxonomy) with two trivial member primitives on EbpfChecker:

void require_lower_bound(const LinearExpression& access_lb,
                         const LinearExpression& floor,   const std::string& msg) const;
void require_upper_bound(const LinearExpression& access_ub,
                         const LinearExpression& ceiling, const std::string& msg) const;

Each per-type case (T_STACK, T_CTX, T_PACKET, T_SHARED, T_ALLOC_MEM) in EbpfChecker::operator()(const ValidAccess&) and operator()(const ValidMapKeyValue&) now spells out its floor and ceiling directly at the line where the access is checked, rather than routing through a per-region helper whose contract had to be carried in comments.

ValidAccess's outer loop also folds its inner is_region_access_type switch into the per-type switch — every bounds check is now in a case with a compile-time-constant region type. The runtime primary_kind_variable_for_type lookup is no longer needed at the call sites (each case names the offset variable directly).

The region_bounds free function and RegionBounds struct are removed; region_semantics.{hpp,cpp} shrinks to just is_region_access_type and primary_kind_variable_for_type (still used elsewhere).

Why this shape

The bug class behind #1099 was that the per-region helper had two T_PACKET ceilings (loose max_packet_size for pointer-comparison checks, runtime packet_size for dereferences) and the choice was hidden behind a std::optional<Variable> argument. "I forgot to pass it" looked identical to "I deliberately chose not to" — which is exactly how the asymmetry between ValidMapKeyValue and ValidAccess stayed invisible.

After the refactor the choice is the literal ceiling expression on the require_upper_bound line. The diff that fixes a #1099-class bug is the diff readers see at the call site.

Soundness

  • packet_size <= max_packet_size is an invariant of the abstract domain, so tightening the T_PACKET ceiling at this site can only reject programs that the previous version also could have caught at the analogous ValidAccess site — no previously-verified program becomes rejected for a reason that wasn't already there.
  • Pointer-comparison checks (width == 0) in ValidAccess::T_PACKET continue to use max_packet_size, preserving the legitimate end = ptr + N; if (end <= data_end) pattern.
  • All other per-region bounds (T_STACK, T_CTX, T_SHARED, T_ALLOC_MEM) preserve their floor/ceiling expressions exactly as in the previous code.

Test plan

  • cmake --build build --parallel — clean build
  • ./bin/tests — 1314 passed, 1 skipped, 236 failed-as-expected (unchanged from main)

References

ValidMapKeyValue checked T_PACKET upper bound against the generic
max_packet_size ceiling, while ValidAccess passed
variable_registry.packet_size() for a tighter, runtime-bound check.

Helper map key/value pointer arguments are real dereferences
(not comparison-only checks), so they should use the same runtime
ceiling as direct packet dereferences. Using the looser ceiling
was sound but unnecessarily imprecise, and the asymmetry could
mask false negatives surfaced by tighter packet-bound reasoning.

Fixes #1099.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Elazar Gershuni <elazarg@gmail.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 26, 2026

📝 Walkthrough

Walkthrough

Replaces a single region_bounds API with three specialized region-bound helpers in src/crab/ebpf_checker.cpp, and removes the former region_bounds implementation and its declarations from src/crab/region_semantics.cpp / .hpp. Call sites in validity checks are updated to use the new helpers and simplified plumbing.

Changes

Cohort / File(s) Summary
eBPF checker — region-bound helpers
src/crab/ebpf_checker.cpp
Removed unified require_region_bounds; added three specialized helpers that split static vs dynamic region checks, handle T_STACK/T_CTX/T_PACKET ceilings (using either AnalysisContext, TypeEncoding::max_packet_size, or runtime packet_size), and constrain access_ub for T_SHARED/T_ALLOC_MEM. Updated ValidMapKeyValue and ValidAccess call sites to use new helpers and removed prior generic region-type / optional-packet-size plumbing.
Region semantics — implementation removed
src/crab/region_semantics.cpp
Deleted region_bounds implementation and supporting includes; left only primary_kind_variable_for_type(...). Error-handling and RegionBounds computation removed.
Region semantics — API/header cleanup
src/crab/region_semantics.hpp
Removed RegionBounds type and region_bounds(...) declaration, deleted related includes and docs referencing bounds API; retained is_region_access_type(...) and primary_kind_variable_for_type(...).

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Code changes implement the objective from #1099: refactoring region_bounds into specialized helpers and updating ValidMapKeyValue to use runtime packet_size instead of generic max_packet_size ceiling.
Out of Scope Changes check ✅ Passed All changes are scoped to the stated objective: removing unified region_bounds function and introducing specialized static/dynamic bounds helpers with explicit packet_size handling.
Title check ✅ Passed The title directly addresses the main change: fixing issue #1099 by tightening ValidMapKeyValue packet bounds and inlining bounds checks as described in the PR objectives.
Description check ✅ Passed The description thoroughly explains the bug, the refactor, and why the changes were made, all directly related to the changeset modifications across the three files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-1099-validmapkeyvalue-packet-size

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@elazarg elazarg changed the title Use runtime packet_size for ValidMapKeyValue T_PACKET bounds (fixes #1099) Use runtime packet_size for ValidMapKeyValue T_PACKET bounds Apr 26, 2026
elazarg and others added 3 commits April 26, 2026 14:52
region_bounds() ignores the packet_size argument for every region
type other than T_PACKET, so a non-empty packet_size paired with
e.g. T_STACK is silently dropped — a caller bug we currently cannot
detect. Add a debug-build assertion at the contract boundary so
mismatched arguments fail loudly during development.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Elazar Gershuni <elazarg@gmail.com>
Replace the runtime-dispatch `region_bounds(type, reg, ctx,
optional<Variable>)` with three constrained function templates:

  template <TypeEncoding T> requires CtxDerivedCeiling<T>
  RegionBounds region_bounds(const AnalysisContext&);

  template <TypeEncoding T> requires RegDerivedCeiling<T>
  RegionBounds region_bounds(const RegPack&);

  template <TypeEncoding T> requires (T == T_PACKET)
  RegionBounds region_bounds(Variable packet_size);

Each call site now states the region type explicitly and may only
pass the arguments that type actually consumes; mismatched
combinations are compile errors rather than silently-ignored
arguments. The packet runtime override is its own overload taking a
Variable, so the dereference path is structurally distinct from the
pointer-comparison path (which uses the AnalysisContext overload's
max_packet_size ceiling). This is the structural form of the
asymmetry that hid issue #1099 in `ValidMapKeyValue`.

`require_region_bounds` mirrors the same overload set and is now a
member-template on EbpfChecker, with a small `require_in_bounds`
helper for the shared assertion pair.

`ValidAccess`'s loop folds its inner `is_region_access_type` switch
into the outer per-type switch so every bounds call has a
compile-time-constant region type. The runtime
`primary_kind_variable_for_type` lookup is no longer needed at the
call sites (each case names the offset variable directly).

The previous `assert(!packet_size || type == T_PACKET)` is dropped:
the contract it guarded is now enforced at compile time.

Refs #1099.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Elazar Gershuni <elazarg@gmail.com>
Drop the constrained-template region_bounds API in favor of two
plainly-named member functions on EbpfChecker, distinguished by what
determines a region's ceiling rather than by C++ template machinery:

  check_access_to_static_region(TypeEncoding, lb, ub)
  check_access_to_static_region(Variable packet_size, lb, ub)
  check_access_to_dynamic_region(Variable region_size, lb, ub)

Static regions (T_STACK, T_CTX, T_PACKET) have ceilings that are a
property of the program invocation as a whole; dynamic regions
(T_SHARED, T_ALLOC_MEM) carry their ceiling as a per-allocation
kind variable on the RegPack. T_PACKET groups with the static
regions because there is exactly one packet per invocation -- its
bounds (packet_size set once at entry, or the loose max_packet_size
constant) do not vary with the allocation site of the pointer; the
two ceilings split further by access shape, which the (Variable)
overload of check_access_to_static_region encodes structurally.
The dynamic-region check takes the size variable directly, since
the TypeEncoding adds nothing the size does not already carry.

Verified: full test suite passes (1314 passed, 236 failed-as-expected,
unchanged from before the refactor).

Refs #1099.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Elazar Gershuni <elazarg@gmail.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/crab/ebpf_checker.cpp`:
- Around line 75-83: The T_PACKET branch of
check_access_to_static_region(TypeEncoding, ...) is only sound for comparison
checks but this precondition is only in the comment; enforce it by either
asserting that callers pass a comparison-shaped access when
TypeEncoding==T_PACKET (e.g. assert is_comparison_check in the caller or at the
top of check_access_to_static_region) or, preferably, split the "loose packet"
semantics into a dedicated helper (e.g. check_access_to_loose_packet_region or
similar) and keep check_access_to_static_region's TypeEncoding overload to
reject unexpected T_PACKET values (throw_fail/default case) so stray T_PACKET
arguments are caught structurally; update callers such as the ValidAccess
T_PACKET arm to call the new helper explicitly and add a brief comment
documenting the new helper's narrower contract.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4667111b-e5fd-499a-bf57-59e176aca057

📥 Commits

Reviewing files that changed from the base of the PR and between 9623d46 and 137f30b.

📒 Files selected for processing (3)
  • src/crab/ebpf_checker.cpp
  • src/crab/region_semantics.cpp
  • src/crab/region_semantics.hpp
💤 Files with no reviewable changes (1)
  • src/crab/region_semantics.cpp

Comment thread src/crab/ebpf_checker.cpp Outdated
…_bound

Drop the per-region check_access_to_* family and the RegionBounds struct in
favor of two primitives composed at each call site:

  require_lower_bound(access_lb, floor, msg);
  require_upper_bound(access_ub, ceiling, msg);

Each per-type case in EbpfChecker now spells out its floor and ceiling
directly where the access is checked, instead of routing through a
dispatcher whose contract had to be carried in comments. The bug class
that motivated #1099 (passing the wrong T_PACKET ceiling at one site)
is now a one-line read at every call site: it shows up as the literal
ceiling expression that gets passed to require_upper_bound.

The static/dynamic taxonomy and its overload set are gone -- they were
the API carrying weight that did not earn its keep; T_PACKET in
particular did not fit either bucket honestly (meta_offset floor, two
possible ceilings).

Refs #1099.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Elazar Gershuni <elazarg@gmail.com>
@elazarg elazarg changed the title Use runtime packet_size for ValidMapKeyValue T_PACKET bounds Fix #1099: tighten ValidMapKeyValue packet bound; inline bounds checks Apr 26, 2026
@elazarg elazarg merged commit e7046de into main Apr 26, 2026
16 checks passed
@elazarg elazarg deleted the fix/issue-1099-validmapkeyvalue-packet-size branch April 26, 2026 14:17
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.

ValidMapKeyValue uses generic max_packet_size for T_PACKET bounds; ValidAccess uses runtime packet_size()

1 participant