Skip to content

eBPF: never inject on a stale tpinjector scratch buffer - #153

Open
paweljw wants to merge 1 commit into
mainfrom
obi-stale-msg-buffer-guard
Open

eBPF: never inject on a stale tpinjector scratch buffer#153
paweljw wants to merge 1 commit into
mainfrom
obi-stale-msg-buffer-guard

Conversation

@paweljw

@paweljw paweljw commented Aug 28, 2026

Copy link
Copy Markdown
Member

Problem

OBI's tpinjector sk_msg program can splice a 70-byte Traceparent: header into outbound TLS ciphertext, corrupting the record so the remote peer fails its MAC check and aborts with a bad_record_mac TLS alert (alert 20). Observed as a sustained, low-rate, probabilistic failure of outbound HTTPS from an nginx forward proxy on an instrumented host; retries of the identical request succeed.

Root cause in OBI v0.12.2 (bpf/tpinjector/tpinjector.c):

  1. fill_msg_buffers() is supposed to copy the outgoing bytes into the per-CPU scratch map msg_buffer_mem, but for known-SSL connections it early-returns without touching the buffer (:711-713).
  2. Both call sites ignore the return value (:1118, :1204).
  3. protocol_detector() never reads the actual message; it reads only the per-CPU scratch (:767-777). A stale plaintext POST /... HTTP/1.1 left by a previous message on the same CPU makes it classify a TLS message as an HTTP request.
  4. The HTTP/1 inject chain then bpf_msg_push_datas the traceparent after the first 0x0A byte found in the ciphertext.

The valid=0 shield entry written by the SSL uretprobe protects most sends, but is lost on a connection's first write (SSL binding happens after sk_msg), on SSL_write retries, on LRU eviction of the host-wide 10k-entry map, and on egress-key collisions (the key is ports-only). Whenever a shield gap coincides with a poisoned CPU buffer, the splice fires. The H2 path already guards exactly this false-positive class (:1075-1078); the HTTP/1 path never did.

Fix

Patch 009-never-inject-on-stale-msg-buffer.patch: honor fill_msg_buffers()' return at both call sites. No fresh buffer, no detection, no injection. In handle_existing_tp_pid a failed fill now clears the map entry and terminates handling for the message, mirroring the existing detector-failure path. H2 handling is untouched.

Verification

  • git apply --check clean on v0.12.2 + patch 008.
  • Full generate.sh + make compile of the patched tree in obi-generator:0.2.15 passes (exit 0).

Will also be proposed upstream separately.

fill_msg_buffers() deliberately bails for SSL connections (and on empty
messages or allocation failure) without refreshing the per-CPU scratch
buffer, but both call sites ignored its return value and ran
protocol_detector() anyway. The detector reads only that scratch buffer,
so a plaintext HTTP request left by a previous message on the same CPU
could make it approve injection into a message that is actually TLS
ciphertext. The resulting Traceparent splice corrupts the TLS record and
the peer aborts the connection with a bad_record_mac alert.

Patch 009 makes both call sites honor the fill result: no fresh buffer,
no detection, no injection. H2 handling is unaffected; it reads the
message directly and already has its own SSL guards.
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