Dynamic: writing a plain System.Object value recurses forever and crashes the process with a stack overflow

Closed
#644 0 comments 0 reactions 1 assignee View on GitHub

Maintainers usually reply within 1 day

@polyglotAI-bot is already working on this.

Since Oct 1, 2026.

  • #648 by @polyglotAI-bot — open

Assessment

This issue has not been assessed yet.

Description

bug
Describe the bug

A binary insert into a Dynamic column crashes the whole process when a value's runtime type is exactly System.Object (for example new object()). DynamicType.Write calls itself with the same value until the stack overflows. .NET cannot catch a StackOverflowException, so a try/catch around the insert does not help: the process aborts.

Other unsupported .NET types fail in a recoverable way. For example, a StringBuilder value throws ClickHouseBulkCopySerializationException with the inner exception ArgumentOutOfRangeException: Unknown type: System.Text.StringBuilder.

The same crash occurs when the System.Object value is inside a collection that Dynamic writes, or inside a JSON column:

Entry point Value Result
InsertBinaryAsync, column Dynamic new object() stack overflow, exit code 134
ClickHouseBulkCopy.WriteToServerAsync, column Dynamic new object() stack overflow
InsertBinaryAsync, column Dynamic new object[] { new object() } stack overflow
InsertBinaryAsync, column Dynamic new List<object> { new object() } stack overflow
InsertBinaryAsync, column Dynamic new Dictionary<string, object> { ["a"] = new object() } stack overflow
InsertBinaryAsync, column Array(Dynamic) new object[] { new object() } stack overflow
InsertBinaryAsync, column JSON, JsonWriteMode=Binary, registered POCO with an object[] property Items = new object[] { new object() } stack overflow
InsertBinaryAsync, column Dynamic new StringBuilder("x") (contrast) catchable ClickHouseBulkCopySerializationException

Values that only contain mapped types work: new object[] { 1, 2 } is stored as Array(Dynamic), and new Dictionary<string, object> { ["a"] = 1 } is stored as Map(String, Dynamic). Only an element whose runtime type is exactly System.Object causes the crash.

Steps to reproduce
  1. CREATE TABLE t (id UInt32, d Dynamic) ENGINE = Memory
  2. Insert one row whose d value is new object() with InsertBinaryAsync (see the code example).
  3. The process aborts with Stack overflow.
Expected behaviour

The insert throws a catchable exception, the same as for any other .NET type that has no ClickHouse mapping (ClickHouseBulkCopySerializationException with an inner ArgumentOutOfRangeException: Unknown type: System.Object). The process continues to run.

Code example
using var client = new ClickHouseClient("Host=localhost;Port=8123;Username=default;Password=");
await client.ExecuteNonQueryAsync("CREATE OR REPLACE TABLE t (id UInt32, d Dynamic) ENGINE = Memory");

try
{
    await client.InsertBinaryAsync("t", new[] { "id", "d" }, new[] { new object[] { 1u, new object() } });
}
catch (Exception ex)
{
    // Never reached: the process aborts before this.
    Console.WriteLine(ex);
}
Error log
Stack overflow.
   at ClickHouse.Driver.Types.BinaryTypeDescriptionWriter.WriteTypeHeader(ClickHouse.Driver.Formats.ExtendedBinaryWriter, ClickHouse.Driver.Types.ClickHouseType)
   at ClickHouse.Driver.Types.DynamicType.Write(ClickHouse.Driver.Formats.ExtendedBinaryWriter, System.Object)
   at ClickHouse.Driver.Types.DynamicType.Write(ClickHouse.Driver.Formats.ExtendedBinaryWriter, System.Object)
   ... (DynamicType.Write repeated 58129 times)
   at ClickHouse.Driver.Copy.Serializer.RowBinarySerializer.Serialize(System.Object[], ClickHouse.Driver.Types.ClickHouseType[], ClickHouse.Driver.Formats.ExtendedBinaryWriter)
   ...
   at ClickHouse.Driver.ClickHouseClient+<>c__DisplayClass62_0.<InsertBinaryAsync>b__0(ClickHouse.Driver.Copy.Batch, System.Threading.CancellationToken)

Process exit code: 134 (SIGABRT).

Root cause
  • TypeConverter.RegisterPlainType<DynamicType>() (ClickHouse.Driver/Types/TypeConverter.cs:131) adds DynamicType.FrameworkType to ReverseMapping (TypeConverter.cs:217-219). DynamicType.FrameworkType is typeof(object) (ClickHouse.Driver/Types/DynamicType.cs:15), so TypeConverter.ToClickHouseType(typeof(object)) returns DynamicType. It does not throw "Unknown type".
  • DynamicType.Write infers the type of a value with GetCachedInferredType(value.GetType()) (DynamicType.cs:47). For a System.Object value this is DynamicType itself. DynamicType.cs:49 then writes a Dynamic type header, and DynamicType.cs:50 calls inferredType.Write(writer, value), which is DynamicType.Write with the same value. This repeats with no end.
  • Containers reach the same code: object[], List<object> and Dictionary<string, object> infer as Array(Dynamic) / Map(String, Dynamic), so each element goes back into DynamicType.Write. JsonType.WriteUnhintedValue (ClickHouse.Driver/Types/JsonType.cs:466-467) also delegates to DynamicType.Write.

Open PR #640 changes DynamicType.Write to call a new InferType, but that method still uses the type-keyed GetCachedInferredType(value.GetType()), so typeof(object) still maps to DynamicType there.

The native TCP client (ClickHouse.Driver.Tcp) has a separate inference (DynamicTypeInference), and I did not test it.

Suggested fix

In DynamicType.Write, do not write a value as Dynamic inside a Dynamic. If the inferred type for a non-null value is DynamicType, throw the same "Unknown type" exception as for other unmapped types. This must stay true for elements of an inferred Array(Dynamic) / Map(String, Dynamic).

Do not remove typeof(object) → DynamicType from ReverseMapping. object[] → Array(Dynamic) and Dictionary<string, object> → Map(String, Dynamic) depend on it, and the server accepts those values (contrast cases above).

Configuration
Environment
  • Client version: main @ 3e2f799 (the same code is in release 1.5.0)
  • Language version: C# (net10.0 test program)
  • .NET version: 10.0.401
  • OS: Ubuntu 24.04.5 LTS (Docker)
ClickHouse server
  • ClickHouse Server version: 26.9.1.1629
  • ClickHouse Server non-default settings, if any: none
  • CREATE TABLE statements for tables involved: CREATE TABLE t (id UInt32, d Dynamic) ENGINE = Memory (also d Array(Dynamic) and d JSON, see table above)
  • Sample data for all these tables: one row, (1, new object())

Found by automated analysis of the client (Polyglot AI) while working on #638. Verified against a live server: each case in the table above was run in a separate process.

Dominant language
C#
Stars
100
Forks
26
Avg merge
20h 24m
Merged PRs (30d)
7

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from ClickHouse/clickhouse-cs

All issues in ClickHouse/clickhouse-cs

Similar issues

More C# issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.