Skip to content

fix(ebpf): verifier-friendly bound in tls_openssl::probe_ssl_read - #711

Merged
mlieberman85 merged 2 commits into
mainfrom
fix-ebpf-verifier-tls-openssl-len
Aug 19, 2026
Merged

fix(ebpf): verifier-friendly bound in tls_openssl::probe_ssl_read#711
mlieberman85 merged 2 commits into
mainfrom
fix-ebpf-verifier-tls-openssl-len

Conversation

@mlieberman85

Copy link
Copy Markdown
Contributor

Summary

Kernel bump on the `ubuntu-24.04` GHA runner tightened the verifier's scalar-range merge analysis, rejecting the current `if payload_size < 512` pattern in `tls_openssl.rs::probe_ssl_read` with:

R2 min value is negative, either use unsigned or 'var &= const'

Replace the if/else with an unconditional bitmask: `(payload_size & 0x1ff) as usize`. Verifier proves the 0..=511 bound in one pass without needing to widen through a branch merge.

Impact

  • Max capture drops 512 → 511 bytes. Fragment buffer stays 512; last byte stays zero.
  • No behavior change for payload_size < 512 (the common SSL fragment case).
  • Payloads ≥ 512 bytes lose 1 byte of visibility. Acceptable — `payload_truncated` flag already exists for oversized fragments.

Bracketing evidence

Test plan

  • Compiles clean with `cargo check --workspace --features ebpf-tracing`
  • CI ebpf-tracing lane passes — this is the actual verifier check

If CI still fails, the fix needs iteration in a Lima VM (macOS host can't run BPF).

🤖 Generated with Claude Code

mlieberman85 and others added 2 commits August 19, 2026 10:43
Kernel bump on the ubuntu-24.04 GHA runner between 2026-08-18 00:39
UTC (last-green m707) and 17:46 UTC (first-red m708) tightened the
verifier's scalar-range merge analysis. The `if payload_size < 512`
branch merges two scalar states with different upper bounds, which
the new verifier rejects with:

    R2 min value is negative, either use unsigned or 'var &= const'

when the merged scalar reaches `bpf_probe_read_user`.

## Fix

Replace the if/else with an unconditional bitmask:

    let capture_len = (payload_size & 0x1ff) as usize;

The bitmask gives an unconditional 0..=511 bound the verifier proves
in one pass without needing to widen through a branch merge.

## Behavioral impact

- Max capture drops from 512 → 511 bytes. The fragment buffer stays
  512 bytes; the last byte stays zero. Downstream consumers already
  handle short captures (payload_size < 512 is the common case).
- No behavior change for payload_size < 512.
- Payloads ≥ 512 bytes lose 1 byte of visibility (511 captured
  instead of 512). Acceptable trade-off — SSL fragments are usually
  well under 1 KiB and truncation was already flagged via
  `payload_truncated`.

## Bracketing evidence

- Zero `waybill-ebpf/` commits since 2026-08-17 (`git log
  --since="2026-08-17" -- waybill-ebpf/`)
- `bpf-linker` pinned at 0.11.0 (`.github/env/bpf-linker.env`)
- 3 consecutive main CI failures (post-#708, #709, #710) with
  identical verifier trace
- Last-green main CI was #707 (2026-08-18 00:39 UTC); first-red
  #708 (2026-08-18 17:46 UTC) — GHA runner bump falls in this
  window

## Follow-on

- No new tests — the CI-level verifier acceptance IS the test.
- The pattern of "verifier tightened after runner bump" is now
  documented for future incidents. Consider a periodic "canary"
  test against nightly runner images to detect these drifts
  proactively (out of scope for this fix).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
First attempt only masked ssl_read; ssl_write_entry had the same
if/else pattern and was still rejected. Also — the initial bitmask
alone wasn't enough because aya's `bpf_probe_read_user_buf` wrapper
casts `dst.len() as u32` internally, and the verifier can't tie
the slice length back to a proven bound (the shl 32; shr 32 zext
in the trace's pc=93-94 is that cast).

## New approach

Build the length through an explicit `u32` local with the bitmask,
then construct the sub-slice via `slice::from_raw_parts_mut(ptr,
capture_len_u32 as usize)` — bypassing the `&mut scratch_slice[..n]`
bounds-check wrapper that generates the offending scalar range.

The u32 local's provenance is clear to the verifier: it was
constructed from `input & 0x1ff`, so it's in 0..=511. Passing this
u32 → usize → slice.len() → u32 chain preserves the bound because
each step is a widening/narrowing cast where the u32-provenance
is visible.

Applied to both call sites: `ssl_read_return` + `ssl_write_entry`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@mlieberman85
mlieberman85 merged commit 4ae86be into main Aug 19, 2026
22 checks passed
mlieberman85 added a commit that referenced this pull request Aug 27, 2026
Cuts a stable v0.3.0. 43 commits since v0.2.0 with substantial
feature additions:

Feature highlights (grouped):

* Resolvers + graph completeness:
  - m663 (#708, #709, #710, #712): local cache-probe resolver — reads
    ~/.m2, $GOMODCACHE, ~/.cargo, RubyGems, npm, pip caches for
    high-confidence PURL extraction without network. New C152
    `waybill:resolver-tier` doc-scope annotation.
  - m235 (#688-#701): Gradle transitive dep-graph ladder — four-tier
    resolution (subprocess → cache → static → lockfile-only) with
    C146-C150 doc-scope + per-component annotations.
  - m236 (#703-#706): universalize `waybill:unresolved-reason` (C151)
    across all design-tier readers.

* Architecture:
  - m664 (#715): single-pass filesystem walker — consolidates 21+
    ecosystem-reader walk sites into one tree traversal per scan.
    5-6× warm-cache perf improvement on large fixtures.
  - m665 (#719): `--no-binary-scan=<MODE>` flag — operator opt-out
    of Go-binary content probing. C153 `waybill:binary-scan-
    suppressed` doc-scope annotation across CDX/SPDX 2.3/SPDX 3.

* eBPF:
  - m234 (#682, #686): un-pin bpf-linker to 0.11.0 via pre-built
    binary install + composite action + daily canary + verify script.

* Fixes:
  - #711: verifier-friendly bound in tls_openssl::probe_ssl_read
    for kernel bumps.
  - #680: Go workspace-mode detection in mod_why preflight.
  - Several release.yml + cosign compat fixes (#674, #675, #681).

Golden regeneration:

* CDX goldens: 11 files, 1-line churn each — `metadata.tools[].version`
  bumped 0.2.0 → 0.3.0.
* SPDX 2.3 goldens: 11 files. Version-string churn cascades into
  content-addressed SPDXRef-DocumentRoot-* IDs and documentNamespace
  URLs (both content-hashed).
* SPDX 3 goldens: 11 files. Same content-addressing cascade into
  doc-/pkg-/anno-/rel- IRIs.

Verified: normalized+sorted diff on all 33 goldens shows zero
semantic change beyond the version string per project memory
`feedback_verify_golden_churn_normalized`.

Also updates `waybill-cli/tests/fixtures/pkg_alias_binding/image-
baz.cdx.json` (same one-line version bump on an out-of-tree
regression fixture).

Skipping local pre-PR per project memory
`feedback_release_bump_prepr_slow` — version bump invalidates the
full compile cache so pre-PR takes 30+ min; CI will verify.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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