Fix heap-buffer-overflow in syntax error formatting (parseQuery) by groeneai · Pull Request #105086 · ClickHouse/ClickHouse · GitHub
Skip to content

Fix heap-buffer-overflow in syntax error formatting (parseQuery)#105086

Merged
alexey-milovidov merged 8 commits into
ClickHouse:masterfrom
groeneai:groeneai/fix-101143-parser-error-message-oob
Jun 4, 2026
Merged

Fix heap-buffer-overflow in syntax error formatting (parseQuery)#105086
alexey-milovidov merged 8 commits into
ClickHouse:masterfrom
groeneai:groeneai/fix-101143-parser-error-message-oob

Conversation

@groeneai

@groeneai groeneai commented May 15, 2026

Copy link
Copy Markdown
Contributor

Fix a heap-buffer-overflow in syntax error message construction reported by select_parser_fuzzer and surfaced on #101143 (comment).

Root cause

In tryParseQuery, last_token = token_iterator.max() is the rightmost token the parser ever touched (across backtracks). this_query_end_pos walks forward from the post-parse iterator until it reaches a semicolon, end of stream, or error token. When backtracking lets the parser look past that boundary, last_token.begin > this_query_end_pos->end. The error formatter then computes total_bytes = end - last_token.begin, underflows size_t, and reads far past the buffer in UTF8::computeBytesBeforeWidth (the _mm_loadu_si128 in the SSE2 fast path of computeWidthImpl reads 16 bytes at offsets ahead of the actual buffer). ASan reports a heap-buffer-overflow; release builds silently splice bytes from neighboring heap memory into the error message.

ASan stack at the moment of the crash:

#0 computeWidthImpl                   src/Common/UTF8Helpers.cpp
#1 computeBytesBeforeWidth            src/Common/UTF8Helpers.cpp:221
#2 writeQueryAroundTheError           src/Parsers/parseQuery.cpp:125
#3 getSyntaxErrorMessage              src/Parsers/parseQuery.cpp:185
#4 tryParseQuery                      src/Parsers/parseQuery.cpp:361

Reproducer from select_parser_fuzzer (crash hash 54f737b6a7b9d6c3a2a7cd333df208dac4eb0361):

SELECT tU from kql($$Cust;
hJSON, Stri)2() IN (SELECT ers00)

The unterminated $$ heredoc forces the lexer to emit a DollarSign token followed by a $Cust BareWord, so the embedded ; is the first Semicolon reached by the forward walk while the parser had already explored much further during backtracking.

Fix

The other call sites of getSyntaxErrorMessage / getLexicalErrorMessage / getUnmatchedParenthesesErrorMessage in tryParseQuery already pass all_queries_end, which is always a safe upper bound. Only the generic-parse-error and excessive-input branches used the narrower this_query_end_pos->end. Replace those with std::max(this_query_end_pos->end, last_token.end) so the displayed range always covers last_token. last_token.end <= all_queries_end is a lexer invariant, so the new value stays within the buffer.

Test

Added a stateless regression test 04247_parser_error_message_oob_101143.sh that runs the fuzzer reproducer through clickhouse-local. Under ASan the unfixed binary aborts with a heap-buffer-overflow in computeWidthImpl; the fixed binary returns a clean SYNTAX_ERROR. Verified locally with the build-asan build on master @ 75fa606f459:

  • Without fix: ==ERROR: AddressSanitizer: heap-buffer-overflow ... READ of size 16 ... computeWidthImpl ... writeQueryAroundTheError ... tryParseQuery ...
  • With fix: Code: 62. DB::Exception: Syntax error: failed at position 39 ())... (SYNTAX_ERROR)

Crash artifact: https://s3.amazonaws.com/clickhouse-test-reports/PRs/101143/390795f8364f8ebbfccc9a10904dd2a5515767f9/libfuzzer_tests/select_parser_fuzzer/crash-54f737b6a7b9d6c3a2a7cd333df208dac4eb0361.trace

cc @alexey-milovidov

Changelog category (leave one):

  • Improvement

Changelog entry (a user-readable short description of the changes that goes into CHANGELOG.md):

Fix a heap-buffer-overflow read in syntax-error message construction (UTF8::computeWidthImpl via parseQuery.cpp) that occurred when the parser had backtracked past the first semicolon / end-of-stream token; release builds were splicing bytes from neighboring heap memory into the displayed error, ASan builds aborted.

Documentation entry for user-facing changes

  • Documentation is unchanged (internal error-formatting fix only).

Note

Medium Risk
Touches query parsing around kql(...) and changes failure behavior when a semicolon appears before the closing ), which could affect edge-case queries; however it is narrowly scoped and covered by a regression test.

Overview
Prevents the unquoted kql(...)/parenthesized KQL parser from scanning past a SQL-level ; by treating TokenType::Semicolon as a hard boundary and failing early, avoiding advancing the outer token high-water mark beyond the current statement and triggering an OOB read during syntax-error formatting.

Adds a stateless regression test that feeds a malformed kql($$...;...) query to clickhouse-local and asserts a clean syntax error is produced (no ASan heap-buffer-overflow).

Reviewed by Cursor Bugbot for commit c7e8374. Bugbot is set up for automated code reviews on this repo. Configure here.

Version info

  • Merged into: 26.6.1.369

`tryParseQuery` passes `this_query_end_pos->end` to `getSyntaxErrorMessage`
as the end of the displayed range, but uses `last_token = token_iterator.max()`
as the failure point. `this_query_end_pos` walks forward from the post-parse
iterator until it reaches a semicolon, end of stream, or error token, but the
parser may have looked past that boundary during backtracking. When that
happens, `last_token.begin > this_query_end_pos->end`, and
`writeQueryAroundTheError` computes `total_bytes = end - last_token.begin`,
underflows `size_t`, and reads far past the buffer in
`UTF8::computeBytesBeforeWidth` (the `_mm_loadu_si128` in the SSE2 fast path
of `computeWidthImpl` reads 16 bytes at offsets ahead of the actual buffer).
ASan reports a heap-buffer-overflow; release builds silently splice bytes
from neighboring heap memory into the error message.

Reproducer from `select_parser_fuzzer`
(crash hash `54f737b6a7b9d6c3a2a7cd333df208dac4eb0361`):

    SELECT tU from kql($$Cust;
    hJSON, Stri)2() IN (SELECT ers00)

The unterminated `$$` heredoc forces the lexer to emit a `DollarSign` token
followed by a `$Cust` `BareWord`, so the embedded `;` becomes the first
`Semicolon` reached by the forward walk while the parser had already explored
much further during backtracking.

The other call sites of `getSyntaxErrorMessage` / `getLexicalErrorMessage` /
`getUnmatchedParenthesesErrorMessage` in `tryParseQuery` already pass
`all_queries_end`, which is always a safe upper bound. Only the two sites
at the generic-parse-error and excessive-input branches used the narrower
`this_query_end_pos->end`. Replace those with
`std::max(this_query_end_pos->end, last_token.end)` so the displayed range
always covers `last_token`. `last_token.end <= all_queries_end` is a lexer
invariant, so the new value stays within the buffer.

Added a stateless regression test
`04247_parser_error_message_oob_101143.sh` that runs the fuzzer reproducer
through `clickhouse-local`. Under ASan the unfixed binary aborts with a
heap-buffer-overflow in `computeWidthImpl`; the fixed binary returns a
clean SYNTAX_ERROR.

Reported by @alexey-milovidov on
ClickHouse#101143 (comment)
@groeneai

Copy link
Copy Markdown
Contributor Author

@groeneai

groeneai commented May 15, 2026

Copy link
Copy Markdown
Contributor Author

cc @evillique @alexey-milovidov @yakov-olkhovskiy @PedroTadim — could you review this? Fixes the select_parser_fuzzer ASan heap-buffer-overflow flagged by Alexey on PR #101143. Single-file source change + one-shot stateless regression test.

@alexey-milovidov alexey-milovidov added the can be tested Allows running workflows for external contributors label May 15, 2026
@clickhouse-gh

clickhouse-gh Bot commented May 15, 2026

Copy link
Copy Markdown
Contributor

Workflow [PR], commit [d919504]

Summary:


AI Review

Summary

The final diff no longer changes tryParseQuery; it only rejects TokenType::Semicolon inside ParserKQLParenExpression and adds a stateless shell test for the original default-SQL select_parser_fuzzer input. I cannot approve this as the claimed heap-buffer-overflow fix because the changed parser path is Kusto-dialect-only, while the test and PR description still target the SQL parser path that reported the issue.

PR Metadata
  • Changelog category: if this remains a fix for an ASan heap-buffer-overflow read, Critical Bug Fix (crash, data loss, RBAC) is the matching category. If the final scope is only the current defensive Kusto parser guard, Improvement is acceptable.
  • Changelog entry: the current entry still claims a parseQuery.cpp / UTF8::computeWidthImpl heap-buffer-overflow fix, which no longer matches the final diff. For the current Kusto-only diff, replace it with something like:
Reject unquoted Kusto parenthesized expressions containing a SQL-level `;` before the matching `)`, so the Kusto parser does not scan past the current statement boundary. KQL queries that need semicolon-separated `let` statements should pass the query as a quoted string or `$$...$$` literal.
Findings
  • ⚠️ Majors
    • [src/Parsers/Kusto/ParserKQLStatement.cpp:175] Existing unresolved thread: ParserKQLParenExpression is only reached from the Kusto dialect, but 04305_parser_error_message_oob_101143.sh invokes the default SQL parser with the original SELECT ... FROM kql(...) reproducer. That means the added TokenType::Semicolon guard is not exercised by the regression test and does not prove or implement the claimed tryParseQuery heap-buffer-overflow fix. Either move the fix and regression back to the SQL path that handled the reported input, or retarget the PR as a Kusto parser improvement and run the regression under SET dialect = 'kusto'.
Tests
  • ⚠️ The regression evidence is still insufficient for the PR contract. The test should fail on the unfixed code and pass because of this PR's changed path; the author has already verified the current default-SQL reproducer returns a clean SYNTAX_ERROR on current master without this PR. Add the smallest Kusto-dialect test that actually reaches ParserKQLParenExpression, or restore a current SQL-dialect ASan reproducer if the PR is meant to fix tryParseQuery.
Final Verdict
  • Status: ⚠️ Request changes
  • Minimum required action: choose the PR scope, then make the implementation, regression test, title/body, and changelog entry all describe the same behavior.

@clickhouse-gh clickhouse-gh Bot added the pr-bugfix Pull request with bugfix, not backported by default label May 15, 2026

@alexey-milovidov alexey-milovidov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But no, this should not happen. The parser can't generate any tokens that are past the query end.

If it's a bug in the KQL parser (which, we know, is trash), fix it.

@alexey-milovidov alexey-milovidov self-assigned this May 15, 2026
@alexey-milovidov

Copy link
Copy Markdown
Member

while the parser had already explored much further during backtracking

Even if the parser consumed that non-terminated heredoc literal, the corresponding token's begin and end should be in between begin and end of the query.

@groeneai

Copy link
Copy Markdown
Contributor Author

@alexey-milovidov agreed — the right framing is "all parser-emitted tokens have [begin, end] within the query buffer", not the band-aid in tryParseQuery. I'll investigate which path produces the offending token for the reproducer

SELECT tU from kql($$Cust;\nhJSON, Stri)2() IN (SELECT ers00)

and fix it at the source — either the general lexer's $$ heredoc handling in src/Parsers/Lexer.cpp:485 or the KQL sub-parser in src/Parsers/Kusto/. I'll either revert src/Parsers/parseQuery.cpp entirely (keeping only the stateless regression test) or replace the change with a chassert(last_token.end <= this_query_end_pos->end) that documents the invariant.

I'll push a new commit and re-request review shortly.

Address @alexey-milovidov's review on PR ClickHouse#105086: instead of widening
the range passed to `writeQueryAroundTheError` in `tryParseQuery`,
stop the unquoted `kql(...)` paren-balancing walk at any `Semicolon`
token and restore `src/Parsers/parseQuery.cpp` to master.

`ParserKQLTableFunction::parseImpl` walks the outer SQL `Tokens` to
extract the argument substring between `(` and the matching `)`. The
walk previously crossed any embedded `;` token. For the fuzzer input
`SELECT tU from kql($$Cust;\nhJSON, Stri)2() IN (SELECT ers00)\n` the
unterminated `$$` heredoc is lexed as `DollarSign` + `BareWord`,
leaving the `;` after `$Cust` as a normal top-level `Semicolon`. The
walk advanced the outer `Tokens`'s high-water mark past it, so when
the parse later failed and `tryParseQuery` computed
`this_query_end_pos` by walking forward to the first `;`, the result
satisfied `last_token.begin > this_query_end_pos->end`. The error
formatter then computed `total_bytes = end - last_token.begin`,
underflowed `size_t`, and `UTF8::computeBytesBeforeWidth` (the SSE2
ASCII fast path in `computeWidthImpl`) read far past the buffer.

A `Semicolon` token in the outer lexer is always a statement boundary
from the surrounding SQL parser's point of view, so failing here is
the correct behavior: legitimate KQL programs that use `;` between
`let` statements must quote the argument with `'...'` or `\$\$...\$\$`
(issue ClickHouse#61742). The existing `02366_kql_*` tests all use quoted
arguments and are unaffected.

The regression test now covers the kql-parser fix path and points at
the embedded `;` in the error message.

Reviewer feedback: ClickHouse#105086 (review)
Crash hash: crash-54f737b6a7b9d6c3a2a7cd333df208dac4eb0361
@groeneai

Copy link
Copy Markdown
Contributor Author

@alexey-milovidov pushed 126fedd2e56 that moves the fix into ParserKQLTableFunction::parseImpl and restores src/Parsers/parseQuery.cpp to master.

Root cause

ParserKQLTableFunction::parseImpl extracts the argument substring between ( and the matching ) by walking the outer SQL Tokens while balancing parentheses (the comment in that file already calls the unquoted path "idiotic", issue #61742). For the fuzzer reproducer

SELECT tU from kql($$Cust;\nhJSON, Stri)2() IN (SELECT ers00)\n

the lexer (src/Parsers/Lexer.cpp:485) sees an unterminated $$ heredoc — find_first_symbols<'\$'> finds the matching \$ but no closing \$\$ pair anywhere else in the buffer, so the heredoc branch falls through to the standalone-dollar branch + BareWord and emits DollarSign(19,20) + BareWord(20,25) for \$\$Cust. The ; at position 25 is therefore a normal top-level Semicolon token.

The paren-balancing walk in parseImpl crossed that ; to keep looking for the matching ), and every pos->type access advanced the outer Tokens's max_pos. When the inner KQL parse later failed and the outer parse unwound, last_token = token_iterator.max() still pointed at a token past the embedded ;. tryParseQuery then walked this_query_end_pos forward from the rewound iterator to the first Semicolon, which is the embedded one at position 25-26 — leaving last_token.begin > this_query_end_pos->end.

writeQueryAroundTheError does total_bytes = end - last_token.begin, size_t underflows to ~2^64, and the SSE2 ASCII fast path in UTF8::computeWidthImpl reads past the 64-byte heap region holding the fuzzer input. So your invariant "the parser can't generate any tokens that are past the query end" is exactly what was violated — by the kql() paren-walk, not by the lexer or tryParseQuery.

Fix

Stop the unquoted paren-walk at any Semicolon token. A Semicolon is always a statement boundary from the outer SQL parser's view, so crossing it inside an argument list is structurally wrong. Legitimate KQL programs that need ; between let statements must use kql('...') or kql(\$\$...\$\$); all existing 02366_kql_* tests already do, so nothing in the test suite changes behavior.

Verified locally (ASan, clang-21)

  • The exact CI fuzzer input (the 61-byte file crash-54f737b6a7b9d6c3a2a7cd333df208dac4eb0361) now exits cleanly: Code: 62. DB::Exception: Syntax error: failed at position 26 (end of query): ;. Expected one of: string literal, list of aliases expressions, list of elements, identifier. — no ASan report.
  • Quoted forms still work: SELECT * FROM kql('T | project a') and SELECT * FROM kql(\$\$T | project a\$\$) both return rows.
  • Unquoted legitimate form still works: SELECT * FROM kql(T | project a) returns rows.
  • Other malformed inputs with embedded ; (e.g. kql(a;), kql(;), kql(()a;b)) now surface a clean syntax error pointing at the ;.

Pre-PR validation gate (re-checked for this iteration)

a) Deterministic repro — build-asan/programs/clickhouse local --query \"\$(cat tmp/crash.input)\", 61-byte file from S3. Without the fix: ASan heap-buffer-overflow at UTF8Helpers.cpp:143 via writeQueryAroundTheErrortryParseQuery. With the fix: SYNTAX_ERROR at position 26 pointing at the embedded ;.
b) Root cause explained — the paren-walking in ParserKQLTableFunction::parseImpl advances the outer Tokens's high-water mark across the embedded ;; on failure-unwind that mark leaks into tryParseQuery's last_token, violating the post-condition that last_token.begin <= this_query_end_pos->end.
c) Fix matches root cause — the change is in the KQL parser at the exact site that does the boundary crossing. src/Parsers/parseQuery.cpp is restored to master; no symptom-site widening anywhere.
d) Test intent preserved — no existing test pinned settings or assertions are changed. Quoted/heredoc/unquoted-without-; kql(...) calls all behave identically to master.
e) Both directions demonstrated — failure on master HEAD ASan binary, clean exit on the rebuilt ASan binary with 126fedd2e56.
f) Fix is general — every call to ParserKQLTableFunction::parseImpl's unquoted-arg path (KustoLayer::parse, ParserKQLOperators::genInOpExpr, ParserKQLOperators::genInOpExprCis, ParserKQLQuery.cpp:1067) shares the same loop, so the guard covers all of them. The sibling KQL loop in ParserKQLOperators::genInOpExprCis at line 220 already stops at Semicolon, so the fix aligns with existing precedent in the same module.

Re-requesting review.

@groeneai

Copy link
Copy Markdown
Contributor Author

cc @alexey-milovidov @evillique @PedroTadim @yakov-olkhovskiy — could you re-review? The new commit moves the fix into ParserKQLTableFunction::parseImpl per @alexey-milovidov's review, and src/Parsers/parseQuery.cpp is back to master.

@alexey-milovidov alexey-milovidov added pr-improvement Pull request with some product improvements and removed pr-bugfix Pull request with bugfix, not backported by default labels May 16, 2026
@clickhouse-gh clickhouse-gh Bot added the manual approve Manual approve required to run CI label May 23, 2026
The prior commit deleted/modified workflow files in this branch as a
workaround for GitHub's workflow-scope check. Restoring them to match
`master` so the PR diff no longer contains unrelated changes.
@alexey-milovidov alexey-milovidov removed the manual approve Manual approve required to run CI label May 24, 2026
/// the SQL-lexer level: callers that need KQL `let` statements must quote
/// the argument with `'...'` or `$$...$$` (issue #61742). Fail here so
/// the outer parser surfaces a clean syntax error pointing at the `;`.
if (pos->type == TokenType::Semicolon)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not cover the reproducer as written. ParserKQLParenExpression is only reached from the Kusto dialect parser, selected when dialect = 'kusto'; the new regression test invokes $CLICKHOUSE_LOCAL --query ... with the default ClickHouse SQL dialect, and the original select_parser_fuzzer report is a SQL SELECT ... FROM kql(...) parse. That means this guard is not reached for the test/reproducer, so the tryParseQuery path can still leave last_token past this_query_end_pos and hit the same out-of-bounds read in error formatting.

Please either run the regression under the Kusto dialect if that is the intended fixed path, or move the guard/fix to the SQL parser path that actually handles the reported input.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed - ParserKQLParenExpression is only reached from the Kusto-dialect path (ParserKQLSubquery in ParserKQLQuery.cpp:1067, ParserKQLOperators::genInOpExpr / genInOpExprCis in ParserKQLOperators.cpp:191 and :244). The regression test runs in the default SQL dialect through ParserQueryWithOutput -> ParserSelectWithUnionQuery -> ParserExpression, and kql is not a registered SQL table function (SELECT * FROM kql(...) returns UNKNOWN_FUNCTION), so the guard is not engaged for the documented reproducer.

Re-verified all four heap-buffer-overflow crash inputs from select_parser_fuzzer on the current master ASan binary (no fix applied):

All four return a clean SYNTAX_ERROR with exit code 0 and no ASan report. CIDB shows zero heap-buffer-overflow occurrences in select_parser_fuzzer since 2026-05-17 (13 days clean across all PRs and master).

The originally-reported SQL-dialect crash no longer reproduces locally or in CI, so I cannot demonstrate that this PR's change fixes it. The KQL-side guard is a defensive invariant maintenance only and is not the right place for the SQL-side reproducer. I am stepping back per the no-speculative-fixes guidance for unreproducible bugs and will defer to maintainer preference on whether to close this PR or keep it scoped purely as a KQL-parser improvement (in which case the regression test should be removed or re-targeted via SET dialect = 'kusto').

alexey-milovidov and others added 2 commits June 3, 2026 06:57
…_attach_table_storage_clauses_without_engine_error on master

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@alexey-milovidov

Copy link
Copy Markdown
Member

Merged master (the branch was red and 743 commits behind; this picks up the px != 0 / QueryPlanResourceHolder terminate fix #106096 that accounts for the chronic Stress-test red). Renumbered the regression test 04247_…04305_… because 04247 now collides with 04247_attach_table_storage_clauses_without_engine_error on master.

The three pre-existing CI failures are all in parser-unrelated queries (Server died; Bad cast from type A to B STID 3336-2f87; px != 0 STID 3224-4198 on a SELECT DISTINCT … toFixedString … UNION ALL … max_memory_usage='4Mi') — chronic master flakes, not caused by this KQL-parser change.

On the open coverage thread: I reproduced @groeneai's finding on a current-master binary — the fuzzer input returns a clean SYNTAX_ERROR (no ASan abort) in both the default SQL dialect (what the test runs) and SET dialect='kusto', so the crash is no longer reproducible and the test passes irrespective of the guard. The disposition (keep as a defensive KQL improvement, re-target the test to dialect='kusto' for line coverage, or close as superseded) is a maintainer call left to @alexey-milovidov — note the 2026-05-16 approval predates the 2026-05-30 coverage discussion.

@clickhouse-gh

clickhouse-gh Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

LLVM Coverage Report

Metric Baseline Current Δ
Lines 84.40% 84.40% +0.00%
Functions 92.40% 92.40% +0.00%
Branches 77.00% 77.00% +0.00%

Changed lines: Changed C/C++ lines covered by tests: 6/7 (85.71%) | Lost baseline coverage: none · Uncovered code

Full report · Diff report

@groeneai

groeneai commented Jun 3, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for merging master, renumbering the test, and independently confirming the unreproducibility.

To summarize where this stands so the disposition is clear:

  • The select_parser_fuzzer heap-buffer-overflow (crash hash 54f737b6...) no longer reproduces on current master in either the default SQL dialect or SET dialect='kusto' (your check matches mine from 2026-05-30 across all four corpus inputs). CIDB shows the last occurrence on 2026-05-17; zero in the 17 days since.
  • The Semicolon guard in ParserKQLParenExpression::parseImpl is only on the Kusto-dialect paren-walk. The reproducer and the regression test run under the default SQL dialect, which never reaches that path, so the test passes with or without the guard. It is not a true regression test for the reported crash.

Given the crash is unreproducible in both dialects and I cannot back the guard with a failing repro, my recommendation is option (c): close as superseded. The out-of-bounds read is no longer reachable, and I would rather not ship a defensive guard that no test can demonstrate is load-bearing.

If you would prefer to retain the small KQL invariant guard for robustness (it mirrors the existing Semicolon stop already present in ParserKQLOperators::genInOpExprCis), then option (b) is the way to make the change at least cover the touched line: re-target the test to SET dialect='kusto'. I should be upfront that even under kusto the input now returns a clean SYNTAX_ERROR, so that test would be line-coverage of the changed function rather than a behavioral regression guard. I am happy to push that test-only change on your word.

Final keep/close call is yours.

@alexey-milovidov alexey-milovidov merged commit 6e11c6e into ClickHouse:master Jun 4, 2026
165 of 167 checks passed
@clickgapai

clickgapai commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

@robot-ch-test-poll3 robot-ch-test-poll3 added the pr-synced-to-cloud The PR is synced to the cloud repo label Jun 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

can be tested Allows running workflows for external contributors pr-improvement Pull request with some product improvements pr-synced-to-cloud The PR is synced to the cloud repo

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants