Repository navigation
fix(l7): resynchronize HTTP/2 parsing after any lost header block (#364 review) - #365
Conversation
…ates Eviction shifted the whole slice of entries. With a size update to 64 KiB a peer can fill the table with ~2,000 minimal entries, and a block of minimal insertions then evicts on every insertion: one 64 KB block took ~34 ms. The table is now a ring, so eviction is O(1); the same block decodes in ~0.3 ms. A size update above the 64 KiB the decoder will hold failed the block, losing everything after it. It is now clamped: the decoder only mirrors the encoder's table, and evicting entries the encoder still has makes them read as unknown, not wrong.
When no stack check is recognized in Write's prologue, the probe goes on the function entry, where a write made while the goroutine's stack grows is captured twice. That fallback was silent; it is now logged once per binary.
The tolerant decoder never decodes a wrong value as long as it sees every insertion. A header block lost without the parser knowing breaks that: later references then resolve to the wrong entries. Review found four ways that happened: - Ring-buffer drops. The parser never learned of them. The kernel now records a dropped event on its connection (connection.l7_lost, one bit per direction, in what was tail padding) and the next delivered event carries it (l7_event.lost_before, the former padding byte). The parser then drops that direction's partial frame and pending block and resets its table (stage events_lost). - Truncation that hides a following HEADERS frame. The table was reset only when the cut frame itself was HEADERS or CONTINUATION. It is now reset unless the missing bytes are known to be the tail of one other frame: also when frames after the cut were lost (one write often holds a response's DATA and then its trailers), when the cut frame's header was never captured, and when a frame ends inside a later read's own missing tail. - A HEADERS frame arriving while a header block waits for CONTINUATION frames replaced it silently; the pending block is now dropped with a reset. - A malformed HEADERS frame and an oversized partial frame were discarded without a reset. Also from review: - Eviction at the stream limit now prefers a request still waiting for response headers, oldest first. A request whose response was lost never gets them, while the oldest stream overall is often a live long-lived one (a watch) that has its headers and awaits its end. - A client block with no pseudo-headers on a stream the parser does not track (trailers of an evicted stream) no longer creates a request.
There was a problem hiding this comment.
Code Review
This pull request introduces robust handling for lost L7 events in the HTTP/2 parser and optimizes the HPACK decoder. Specifically, it tracks lost events via a new l7_lost field in the connection state, allowing the Go-side parser to reset its decoder state and avoid decoding incorrect paths when gaps occur. Additionally, the HPACK decoder is refactored to use a ring buffer for O(1) evictions, and the HTTP/2 stream eviction logic is improved to prioritize streams without response headers. A critical issue was identified in the eBPF code where modifications to conn->l7_lost are not persisted back to the active_connections map when TCP connection tracking misses, as conn still points to a temporary stack variable.
…TLS write On a TLS write to a connection the kernel was not tracking (an OpenSSL socket whose connect it missed), trace_enter_write inserted its stack copy into active_connections but went on using the copy. The protocol detected on that first write, and a loss recorded by send_event, were written to the copy and discarded, so the next write had to detect the protocol afresh, which a write starting mid-frame cannot. It now continues on the map entry, or on another CPU's if that one won the race. LLM capture, which skips the stack copy, now also sees that first write, binding a mark set from a ClientHello there rather than on the second write.
|
Thanks, this covers everything from the #364 review. The lost-event flag, the wider truncation resets, the pending-block drop, eviction order and the HPACK ring all look right. The server-side preface explanation makes sense. Three small things for a later pass, none blocking:
|
…cked connection (#367) * fix(ebpf): flag events lost to a failed copy, and mark TLS on the tracked connection Two gaps left after the lost-event flag (from review of #365): - An event dropped because bpf_probe_read of the user buffer failed was dropped before send_event, so l7_lost was not set and the parser could not see the gap. COPY_PAYLOAD_RINGBUF now records the loss on the connection, on the written or read side by where it runs. - On a TLS write to an untracked connection, mark_tls ran on the stack copy before the code switched to the map entry. If another CPU had inserted that entry first (the TCP connect path does, with tls 0), it never got the mark and its socket-level ciphertext was not skipped. The mark is now applied again after the switch. * refactor(ebpf): drop COPY_PAYLOAD_RINGBUF's unused event parameter The programs are unchanged (same instruction counts); ebpf.go differs only in the BTF line info that carries the edited source lines.
Summary
The review fixes for #364. #364 was merged before they were pushed, so they are here instead. Details are in the review reply on #364.
connection.l7_lost), and the next delivered event carries it (l7_event.lost_before). The parser resets that direction, counted as a newevents_loststage.active_connectionsbut kept using a stack copy. The protocol detected on that first write was discarded, so a following write that started mid-frame could never be detected. It now continues on the map entry.Engineering detail
/afor/b.l7_lostuses what was tail padding instruct connection, which stays 32 bytes.lost_beforetakes the event header's former padding byte.Local e2e: built the agent from this branch (the first three commits are identical to the head tested before #364 merged) and ran it in a local Docker VM (kernel 6.10). A Go net/http HTTP/2 client making ~1.5M requests per 60s had every request decoded on a fresh connection, with 0 decode errors. The short-lived-process suite captured 389 of 391. A test-only build with a 64 KB ring buffer forced 939 drops; the lost flag reached the parser and caused 2 resynchronizations, with 0 decode errors and every request outside the dropped events decoded. With the untracked-connection fix added: the same Go client decoded every request with 0 errors, and the scenario suite, including its OpenSSL clients, matched the earlier runs. Unit tests, vet and golangci-lint pass, and the eBPF programs load on 5.10 and 6.1.