Skip to content

l7: bound ClickHouse parser allocations by the captured payload size - #339

Closed
tmeaney-qumulo wants to merge 1 commit into
coroot:mainfrom
tmeaney-qumulo:fix-clickhouse-parser-unbounded-alloc
Closed

l7: bound ClickHouse parser allocations by the captured payload size#339
tmeaney-qumulo wants to merge 1 commit into
coroot:mainfrom
tmeaney-qumulo:fix-clickhouse-parser-unbounded-alloc

Conversation

@tmeaney-qumulo

Copy link
Copy Markdown

Problem

ParseClickhouse feeds a truncated, untrusted eBPF payload capture into ch-go's protocol decoder. ch-go's Reader.Str (via Buffer.Ensure) allocates make([]byte, n) from the decoded varint length prefix before validating n against the bytes actually available — it only checks n >= 0.

Against a well-behaved ClickHouse server that's harmless. Against a truncated capture, misaligned parses are routine — especially when the wire format is newer than the vendored ch-go v0.62.0 understands (we run ClickHouse 26.x) — and garbage bytes get read as a multi-gigabyte string length. The allocation fires even though the read then immediately fails; the recover() catches panics, not allocations.

Production impact

On our EKS cluster the agent OOM-crash-looped on every node hosting ClickHouse native-protocol clients, dying at any memory limit we tried (1Gi → 8Gi, up to 9 kills per pod, one 59-second life). Heap profile of a dying agent (Coroot's own Go heap profiling):

total                                                    (3.0 GB, 100%)
containers.(*Registry).handleEvents                      (2.2 GB,  72%)
  containers.(*Container).onL7Request
    ebpftracer/l7.ParseClickhouse
      ch-go/proto.(*Setting).Decode
        ch-go/proto.(*Reader).Str → StrAppend → StrRaw   ← make([]byte, n), n unvalidated

Nodes without CH clients ran flat at ~400Mi throughout. This is very likely the cause of #242 as well (undiagnosed OOM against a 9GB limit).

Fix

Replace the ch-go ClientInfo.DecodeAware / Setting.Decode calls with a field-identical bounded walk (same field order, same version feature gates, using ch-go's exported primitives and feature constants): every length-prefixed read is checked against the payload size before allocating. A claimed length beyond the capture is a misparse by construction.

  • Well-formed frames parse exactly as before — the existing TestParseClickHouse payloads pass unchanged
  • The new TestParseClickhouseGarbageLength demonstrates the bug: a 6-byte payload that previously allocated 4.3GB (measured via runtime.MemStats in the test) now allocates nothing

The deeper fix belongs in ch-go (StrRaw should validate the length against the source before Ensure), but bounding at this untrusted boundary is correct regardless of what ch-go does, and keeps the agent safe against any future protocol drift.

Happy to adjust the approach if you'd prefer a different shape (e.g. a max-string-length constant instead of the payload-size bound).

@tmeaney-qumulo tmeaney-qumulo mentioned this pull request Jul 30, 2026
@def def added the AI slop label Jul 30, 2026
ParseClickhouse feeds a truncated eBPF capture into ch-go's decoder, whose
Reader.Str allocates make([]byte, n) from the varint length prefix before
validating n against the bytes available. The capture is untrusted and
truncated by design, so a misaligned parse - routine when the wire format
is newer than ch-go v0.62.0 understands, e.g. ClickHouse 26.x - reads
garbage as a multi-gigabyte string length and the allocation fires even
though the read then fails. The recover() only catches panics, not
allocations.

In production this OOM-crash-looped the agent on every node hosting
ClickHouse native-protocol clients: 72% of agent heap (2.2GB) in
ParseClickhouse -> proto.(*Reader).StrRaw, agents dying at any memory
limit from 1Gi to 8Gi. Likely the cause of coroot#242 as well.

Replace the ch-go ClientInfo/Settings decoding with a field-identical
bounded walk: every length-prefixed read is checked against the payload
size before allocating - a claimed length beyond the capture is a misparse
by construction. Well-formed frames parse exactly as before (existing
tests unchanged and passing); the new test demonstrates a 6-byte garbage
payload that previously allocated 4.3GB now allocates nothing.
@tmeaney-qumulo
tmeaney-qumulo force-pushed the fix-clickhouse-parser-unbounded-alloc branch from 277fe62 to bdd1a8a Compare July 30, 2026 21:07
@tmeaney-qumulo

Copy link
Copy Markdown
Author

@def Hey would you mind me asking why you labeled this AI Slop?
This is something I have been digging into this evening to try and solve a problem with using Coroot in our production EKS cluster, if this fix isn't correct then no worries, but we cannot continue to use Coroot if we cannot have L7 tracing turned on and will need to look for other solutions.

@def

def commented Jul 30, 2026

Copy link
Copy Markdown
Member

@tmeaney-qumulo sure, there are a few reasons:

  • The amount of AI-generated code is quite large, so reviewing it would likely take longer than writing it from scratch.
  • The code is clearly AI-generated.
  • The issue you mentioned is most likely unrelated to this PR.

A much more helpful contribution, if you run into an issue with Coroot in your environment, is to open a detailed issue here. For example, in this case, attaching a memory profile would be far more valuable than submitting a large code change.

Thanks for understanding.

@tmeaney-qumulo

Copy link
Copy Markdown
Author

Ok but in this case it's 144 lines (including tests proving the issue) over 2 files and the profile is in the description, but no problem, if coroot cannot work for our enterprise requirements (L7 tracing is table stakes), we can find an alternative.

Thanks

@def

def commented Jul 30, 2026

Copy link
Copy Markdown
Member

@tmeaney-qumulo, the issue is real and already known, we discussed it in #319. I'm happy to fix it, but I'd rather do it properly. A 144-line AI-generated patch, even with tests, doesn't give me much confidence that it won't introduce panics or other regressions.

Let's close this PR and track the work in an issue instead. That way we can implement and review the fix properly.

@def

def commented Jul 30, 2026

Copy link
Copy Markdown
Member

@tmeaney-qumulo BTW which version of the node-agent are you using?

@tmeaney-qumulo

Copy link
Copy Markdown
Author

@def v1.35.0 for the node agent, and thank you for the links, those were useful.
One thing, they speak to misclassification of Clickhouse traffic whereas mine is actual Clickhouse traffic.

We are on Clickhouse v26.6, clickhouse-go v2.46.0 (ch-go v0.71.0) running a high throughout ingestion pipeline

@def

def commented Jul 30, 2026

Copy link
Copy Markdown
Member

@tmeaney-qumulo thanks for the details, it's really helpful. I'll try to reproduce the issue and get a fix out soon. If I end up with a similar implementation, I'll take a closer look at your changes. Let's keep this PR open for now.

@tmeaney-qumulo

Copy link
Copy Markdown
Author

@def Thank you! If you need anything more from me let me know

@def

def commented Jul 31, 2026

Copy link
Copy Markdown
Member

@tmeaney-qumulo I managed to reproduce the issue. It turned out to be caused by the per-query settings. As a fix, I implemented a simple ClickHouse protocol parser manually instead of using ch-go, which gives us better control over the behavior. Thanks again for the detailed report!

@def def closed this in #341 Jul 31, 2026
@tmeaney-qumulo

Copy link
Copy Markdown
Author

@def Amazing, thank you, I see you also released a new version of the agent, that's a quick turnaround time!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants