Fix excessive allocation in MsgPack schema inference for huge container headers#109019
Conversation
…er headers MsgPackSchemaReader::readObject() called msgpack::unpack() with the default unpack_limit(), which caps array/map/str/bin/ext at 0xffffffff. msgpack::unpack builds the object tree up front and eagerly allocates storage sized by the count/length declared in the header before reading the payload, so a tiny fuzzed header (e.g. an array32 declaring 0xffffffff elements) drove a single multi-gigabyte allocation that bypassed the query memory tracker: an allocation-size-too-big abort under sanitizers, and std::bad_alloc / OOM in release builds. Bound every container by the bytes currently buffered. A valid element occupies at least one encoded byte, so a fully buffered object never trips the bound, while an over-declaration throws msgpack::size_overflow, handled exactly like insufficient_bytes (grow the buffer and retry; reject as UNEXPECTED_END_OF_FILE once the whole input is buffered and it still does not fit). Covers all eager-allocation paths (array/map/str/bin/ext). Schema-inference only; the runtime MsgPackVisitor uses a streaming parser and is unaffected. Found by the CI stress fuzzer (AddressSanitizer, STID 3243-1df9). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pre-PR validation gate (click to expand)
Session id: cron:clickhouse-worker-slot-5:20260701-041300 |
|
cc @Avogar @al13n321 - could you review this? It bounds |
|
Workflow [PR], commit [65b84b8] Summary: ✅
AI ReviewSummaryThis PR hardens MsgPack schema inference by bounding Final Verdict
|
CI finish ledger — 4f77970Every failure below has an owner: a fixing PR (ours or external). Neither is caused by this PR (it touches only `src/Processors/Formats/Impl/MsgPackRowInputFormat.cpp` + a MsgPack schema-inference test; it cannot affect the query planner or IntersectOrExceptStep).
Session id: cron:our-pr-ci-monitor:20260701-130000 |
Thread format_settings.max_parser_depth into msgpack::unpack as the depth limit so a deeply nested header is rejected while the DOM tree is being built, before getDataType walks it and before the deep tree is fully allocated. Extends the allocation bounding already applied to array/map/str/bin/ext sizes. msgpack throws msgpack::depth_size_overflow, which derives from size_overflow, so it must be caught before the size_overflow grow-and-retry handler; otherwise deep nesting would be misrouted into UNEXPECTED_END_OF_FILE. It is rejected with the same TOO_DEEP_RECURSION message getDataType uses. max_parser_depth == 0 means unlimited (matching getDataType and the SQL parser); msgpack spells that 0xffffffff. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Good catch, done in ac873e2.
One subtlety: msgpack throws The runtime (data) path is unaffected: it uses the streaming |
msgpack allocates sizeof(object_kv) per declared map pair as soon as the pair count fits limit.map(), before the payload is checked for completeness. A valid map pair is two encoded objects (>= 2 bytes), so bounding the pair count by the raw byte count let a malformed map32 over-declare by almost 2x and still drive a large object_kv allocation before insufficient_bytes fired. Cap the map slot at available / 2: a fully buffered valid map has payload >= 2 * pairs so it never trips the bound, while an over-declaration is rejected before the allocation. Add a map32 regression case that keeps a full payload buffered past the header (exercising the map allocation path the raw available bound left open) plus a tight-boundary fixmap case guarding that valid maps are not over-rejected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
LLVM Coverage Report
Changed lines: Changed C/C++ lines covered: 32/32 (100.00%) · Uncovered code |
CI finish ledger — 65b84b8CI fully finished (Finish Workflow pass, Mergeable Check pass). No failed checks. All builds, functional/integration/stress/fuzzer/perf checks green after the map-underbound tightening (commit 65b84b8).
Session id: cron:our-pr-ci-monitor:20260707-190000 |
Changelog category (leave one):
Changelog entry (a user-readable short description of the changes that goes into CHANGELOG.md):
Fixed an excessive memory allocation during schema inference of the
MsgPackformat: a corrupted or fuzzed input whose array/map/string/binary header declares a huge element count no longer drives a single multi-gigabyte allocation (it is now rejected as malformed input). This fixes anallocation-size-too-bigabort under sanitizers and an out-of-memory condition in release builds.Description
No related open issue found. Found by the CI stress fuzzer (AddressSanitizer, STID 3243-1df9) on the
Stress test (arm_asan_ubsan)job.Report: https://s3.amazonaws.com/clickhouse-test-reports/json.html?PR=108724&sha=48e7880e5fbbdf289911c01ba3f856af22d85f64&name_0=PR&name_1=Stress%20test%20%28arm_asan_ubsan%29
Signature:
Root cause:
MsgPackSchemaReader::readObject()calledmsgpack::unpack()with the defaultmsgpack::unpack_limit(), which caps array/map/str/bin/ext at0xffffffff.msgpack::unpackbuilds the whole object tree up front, and for each container it eagerly allocates storage sized by the count/length declared in the header (create_object_visitor::start_map/start_array/visit_str/... callzone::allocate_align(count * sizeof(...))) before reading the payload. A tiny fuzzed header, e.g. anarray32declaring0xffffffffelements (5 bytes:dd ff ff ff ff), therefore drives a single ~128 GiB allocation that bypasses the query memory tracker: it aborts under a sanitizer (allocation-size-too-big) and throwsstd::bad_alloc/ exhausts memory in release. This is schema-inference-only; the runtimeMsgPackVisitoruses a streaming parser and is not affected.Fix: bound every container by the number of bytes currently buffered. A valid element occupies at least one encoded byte, so a fully buffered object never trips the bound, while an over-declaration throws
msgpack::size_overflow, handled exactly likeinsufficient_bytes(grow the buffer and retry; reject asUNEXPECTED_END_OF_FILEonce the whole input is buffered and it still does not fit). Covers all eager-allocation paths (array/map/str/bin/ext).Reproducer (rejected cleanly after the fix, no giant allocation):
Regression test:
tests/queries/0_stateless/04366_msgpack_schema_inference_huge_container.sh(over-declared array32/map32/str32/bin32 rejected; valid data, including a multi-megabyte string that spans several buffer reads, still inferred correctly).Version info
26.7.1.681(included in26.7and later)