You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Severity: sev:3 — silent data corruption on a default code path. Any string value containing \, \t, or \n passed via Query::SetParam is encoded with an extra backslash; the server accepts the literal and stores a corrupted value with no error surfaced.
Description
clickhouse/base/wire_format.cpp:112 (WireFormat::WriteQuotedString) is used to encode named-parameter values for the native protocol. In the escape path it unconditionally writes a single \ (line 139) before the per-character branch, and the per-character branches for \\, \t, \n each already begin with \\ themselves. The result is an extra backslash for those three characters:
input \\ (one backslash) is emitted as '\\\\\\\\' (four backslashes between quotes); the server SQL-literal parser turns \\\\ into \\\\ → two backslashes
input \\t (tab) is emitted as '\\\\\\\\t' (backslash-backslash-backslash-t between quotes); the server parses to \\<tab>
input \\n (newline) — same pattern, parses to \\<newline>
The \\0, \\b, \\' branches use xNN hex escapes (x00, x08, x27) and combine with the stray \\ into a valid \\xNN, so those three values round-trip correctly — which is why the existing TEST_P(ClientCase, QueryParameters) (ut/client_ut.cpp:1856) doesn't catch it (and it never SELECTs the inserted string back to compare).
The bug is the C++ analogue of clickhouse-go #1576. In Go the same root cause produced a visible server parse error (Cannot parse quoted string: expected closing quote); in C++ the encoding happens to remain parseable, so the value is silently corrupted instead.
ClickHouse server version
Verified server-side parsing against 26.4.2.10 (local instance: SELECT length('\\\\\\\\'), hex('\\\\\\\\') → 2 / 5C5C, confirming a string of two backslashes is the parsed value of the 4-backslash literal the client emits for input \\). Client wire-output verified by code analysis of the actual WriteQuotedString source and a standalone replica of its logic; the C++ library itself was not built and the round-trip was not executed end-to-end in this run.
Reproduction
Minimal test case using the project's own Query::SetParam API and the QueryParameters test fixture style:
TEST_P(ClientCase, QueryParametersBackslash) {
constauto& server_info = client_->GetServerInfo();
if (versionNumber(server_info) < versionNumber(24, 7)) GTEST_SKIP();
const std::string table = "test_cpp_param_backslash";
client_->Execute("CREATE TEMPORARY TABLE IF NOT EXISTS " + table +
" (id UInt64, name String)");
Query ins("INSERT INTO " + table + " VALUES ({id:UInt64}, {name:String})");
ins.SetParam("id", "1").SetParam("name", "\\"); // a single backslash
client_->Execute(ins);
std::string seen;
Query sel("SELECT name FROM " + table + " WHERE id = 1");
sel.OnData([&](const Block& b) {
if (b.GetRowCount()) seen = std::string(b[0]->As<ColumnString>()->At(0));
});
client_->Select(sel);
// Expected: "\\" (length 1, a single backslash)// Observed: "\\\\" (length 2, two backslashes) — silent corruptionEXPECT_EQ(seen, std::string("\\"));
}
Replace the param value with "\\t" or "\\n" for the tab/newline variants — both will produce \\<tab> and \\<newline> respectively instead of the original byte. "a'b" and "a\\0b" round-trip correctly (those branches use \\xNN hex escapes).
Suggested fix
In WireFormat::WriteQuotedString (clickhouse/base/wire_format.cpp:112), the leading WriteAll(output, \"\\\\\", 1) on line 139 should be removed; each per-character case should be responsible for emitting its full escape sequence (e.g. \\\\, \\t, \\n, \\x27). The length accounting (size + 2 + 3 * quoted_count) should be revisited accordingly — the current 3 * quoted_count factor encodes the assumption that each special char expands to 4 wire bytes, which matches the current (buggy) output but not the correct 2-byte expansion for \\\\/\\t/\\n or the 4-byte expansion for \\xNN cases.
The existing QueryParameters test should also be extended to SELECT the inserted strings back and EXPECT_EQ them against the originals — without that, similar regressions will continue to slip through.
Dedup search
gh issue list --repo ClickHouse/clickhouse-cpp --search 'backslash parameter' --state all --limit 20 → 0 results
gh issue list --repo ClickHouse/clickhouse-cpp --search 'WriteQuotedString' --state all --limit 20 → 0 results
Target client repo: ClickHouse/clickhouse-cpp
Severity: sev:3 — silent data corruption on a default code path. Any string value containing
\,\t, or\npassed viaQuery::SetParamis encoded with an extra backslash; the server accepts the literal and stores a corrupted value with no error surfaced.Description
clickhouse/base/wire_format.cpp:112(WireFormat::WriteQuotedString) is used to encode named-parameter values for the native protocol. In the escape path it unconditionally writes a single\(line 139) before the per-character branch, and the per-character branches for\\,\t,\neach already begin with\\themselves. The result is an extra backslash for those three characters:\\(one backslash) is emitted as'\\\\\\\\'(four backslashes between quotes); the server SQL-literal parser turns\\\\into\\\\→ two backslashes\\t(tab) is emitted as'\\\\\\\\t'(backslash-backslash-backslash-t between quotes); the server parses to\\<tab>\\n(newline) — same pattern, parses to\\<newline>The
\\0,\\b,\\'branches usexNNhex escapes (x00,x08,x27) and combine with the stray\\into a valid\\xNN, so those three values round-trip correctly — which is why the existingTEST_P(ClientCase, QueryParameters)(ut/client_ut.cpp:1856) doesn't catch it (and it neverSELECTs the inserted string back to compare).The bug is the C++ analogue of clickhouse-go #1576. In Go the same root cause produced a visible server parse error (
Cannot parse quoted string: expected closing quote); in C++ the encoding happens to remain parseable, so the value is silently corrupted instead.ClickHouse server version
Verified server-side parsing against
26.4.2.10(local instance:SELECT length('\\\\\\\\'), hex('\\\\\\\\')→2 / 5C5C, confirming a string of two backslashes is the parsed value of the 4-backslash literal the client emits for input\\). Client wire-output verified by code analysis of the actualWriteQuotedStringsource and a standalone replica of its logic; the C++ library itself was not built and the round-trip was not executed end-to-end in this run.Reproduction
Minimal test case using the project's own
Query::SetParamAPI and theQueryParameterstest fixture style:Replace the param value with
"\\t"or"\\n"for the tab/newline variants — both will produce\\<tab>and\\<newline>respectively instead of the original byte."a'b"and"a\\0b"round-trip correctly (those branches use\\xNNhex escapes).Suggested fix
In
WireFormat::WriteQuotedString(clickhouse/base/wire_format.cpp:112), the leadingWriteAll(output, \"\\\\\", 1)on line 139 should be removed; each per-charactercaseshould be responsible for emitting its full escape sequence (e.g.\\\\,\\t,\\n,\\x27). The length accounting (size + 2 + 3 * quoted_count) should be revisited accordingly — the current3 * quoted_countfactor encodes the assumption that each special char expands to 4 wire bytes, which matches the current (buggy) output but not the correct 2-byte expansion for\\\\/\\t/\\nor the 4-byte expansion for\\xNNcases.The existing
QueryParameterstest should also be extended toSELECTthe inserted strings back andEXPECT_EQthem against the originals — without that, similar regressions will continue to slip through.Dedup search
gh issue list --repo ClickHouse/clickhouse-cpp --search 'backslash parameter' --state all --limit 20→ 0 resultsgh issue list --repo ClickHouse/clickhouse-cpp --search 'WriteQuotedString' --state all --limit 20→ 0 resultsgh issue list --repo ClickHouse/clickhouse-cpp --search 'escape parameter' --state all --limit 20→ 1 unrelated (issue Execute throws exception, but command line client accepts same query. #13, 2019, command-line vs Execute)gh pr list --repo ClickHouse/clickhouse-cpp --search 'backslash parameter' --state all --limit 20→ 0 resultsgh issue list --repo ClickHouse/integrations-ai-playground --label backfill --label target:clickhouse-cpp --search 'backslash' --state all --limit 20→ 0 resultsgh pr list --repo ClickHouse/integrations-ai-playground --search 'backslash cpp' --state all --limit 20→ 0 resultsSource bug
Filed in response to ClickHouse/clickhouse-go#1576 (Go client analogue — same root cause, different manifestation).