Skip to content

backfill-fold --json aborts with SIGABRT (exit 134) and a .NET stack trace on the trimmed release — WriteCommandError and the success-path JsonSerializer.Serialize(new { ... }) both throw uncaught #181

Description

@Widthdom

Summary

cdidx backfill-fold --json on the published trimmed-release binary (v1.10.0) crashes with an unhandled InvalidOperationException (Reflection-based serialization disabled), prints a .NET stack trace to stderr, and exits with signal 134 (SIGABRT)both on the success path and on every error path that goes through WriteCommandError. This is materially worse than #147's --json failure mode for status / search / files, which catches the exception and exits with the documented DbError = 3 code.

Repro

curl -fsSL https://raw.githubusercontent.com/Widthdom/CodeIndex/main/install.sh | bash
CDIDX=/root/.local/bin/cdidx

git clone https://github.com/Widthdom/CodeIndex /tmp/codeindex-src
"$CDIDX" /tmp/codeindex-src --db /tmp/test.db

Compare the two --json paths:

"$CDIDX" status --db /tmp/test.db --json; echo "status_ec=$?"
"$CDIDX" backfill-fold --db /tmp/test.db --json; echo "bf_ec=$?"

Output:

Error: database error: Reflection-based serialization has been disabled for this application. Either use the source generator APIs or explicitly configure the 'JsonSerializerOptions.TypeInfoResolver' property.
status_ec=3                    ← #147: caught error, DbError exit code (still wrong but graceful)

Unhandled exception. System.InvalidOperationException: Reflection-based serialization has been disabled for this application. ...
   at System.Text.Json.ThrowHelper.ThrowInvalidOperationException_JsonSerializerIsReflectionDisabled()
   at System.Text.Json.JsonSerializerOptions.ConfigureForJsonSerializer()
   at System.Text.Json.JsonSerializer.Serialize[TValue](TValue, JsonSerializerOptions )
   at CodeIndex.Cli.IndexCommandRunner.WriteCommandError(Boolean, JsonSerializerOptions, String, Int32, String )
   at CodeIndex.Cli.IndexCommandRunner.RunBackfillFold(String[], JsonSerializerOptions)
   at Program.<Main>$(String[])
/bin/bash: line 1: 16775 Aborted                 cdidx backfill-fold --db /tmp/test.db --json
bf_ec=134                      ← SIGABRT, .NET stack trace dumped to stderr

Same crash also reproduces when triggering the success path — running backfill-fold --json against a healthy DB whose folded columns are already up to date — because the success branch also serializes an anonymous object via JsonSerializer.Serialize(new { ... }, jsonOptions). The non---json form of the same command finishes successfully:

"$CDIDX" backfill-fold --db /tmp/test.db
# Backfilling folded-name columns ...
#   symbols:            0 row(s) rewritten
#   symbol_references:  0 row(s) rewritten
#   verified:           yes
#   stamp:              FoldReady bit set (user_version: 7 -> 7)

Even an unrecognized flag triggers the SIGABRT path:

"$CDIDX" backfill-fold --bogus-flag --json
# Warning: unknown option '--bogus-flag' (ignored) / 不明なオプション '--bogus-flag'(無視されます)
# Unhandled exception. System.InvalidOperationException: ...
# /bin/bash: line 1: ... Aborted

Suspected root cause (from reading the source)

Two places in src/CodeIndex/Cli/IndexCommandRunner.cs call JsonSerializer.Serialize(new { ... }, jsonOptions) on an anonymous type, which requires reflection-based serialization that is disabled in trimmed builds:

  1. Success path at IndexCommandRunner.cs:251:

    if (options.Json)
    {
        Console.WriteLine(JsonSerializer.Serialize(new
        {
            symbols,
            symbol_references = symbolReferences,
            rewrite_all = rewriteAll,
            verified,
            user_version_before = userVersionBefore,
            user_version_after = userVersionAfter,
            fold_ready = true,
        }, jsonOptions));
    }
  2. Error path at IndexCommandRunner.cs:686 (WriteCommandError), called from at least 5 sites in IndexCommandRunner (lines 200, 208, 238, 277, 446):

    private static int WriteCommandError(bool json, JsonSerializerOptions jsonOptions, string message, int exitCode, string? hint = null)
    {
        if (json)
            Console.WriteLine(JsonSerializer.Serialize(new { status = "error", message, hint }, jsonOptions));
        ...
    }

Both Serialize<T> calls hit JsonSerializerOptions.ConfigureForJsonSerializer() → throws InvalidOperationException because the trimmed runtime has no TypeInfoResolver. There is no surrounding try / catch in RunBackfillFold to catch this, and Program.<Main>$ doesn't appear to have a top-level catch for the backfill-fold path either, so the exception terminates the process with SIGABRT.

The other --json paths (status / search / files / etc.) probably have a wider try / catch somewhere upstream that converts the same exception into the DbError exit code, which is why #147 sees a graceful "Error: database error: ..." instead of a SIGABRT.

There's also a secondary subtlety: even after the fold serialization is fixed, the error path will fail too because WriteCommandError itself uses an anonymous object. Any --json error reporting needs a non-anonymous, source-gen-registered DTO.

Why it matters

  • Any CI / automation script that calls cdidx backfill-fold --json on the published binary aborts with a SIGABRT instead of receiving a structured error. The .NET stack trace is leaked to stderr.
  • Even on a healthy DB the success path crashes, so the documented JSON output of backfill-fold is unobtainable on the trimmed release.
  • The crash happens at the wrong layer for AI consumers. Per Published --json CLI crashes with misleading "Error: database error:" prefix and exit code 3 on trimmed build #147, AI agents that branch on exit codes treat exit 3 as "DB needs rebuild." Exit 134 (SIGABRT) is not in CommandExitCodes at all, so they have no documented meaning to attach — most will treat it as "unknown failure" and either retry endlessly or give up.
  • The .NET stack trace leaks an internal WriteCommandError symbol with the trimming-disabled message text — confusing surface for a user who has no context for "what is JsonTypeInfo."
  • WriteCommandError is shared by 5 call sites in IndexCommandRunner. All 5 will exhibit the same SIGABRT under --json if their respective error conditions are triggered.

Suggested direction

Two layers; both are useful.

Layer 1 — stop the SIGABRT immediately

Wrap every --json call site in a try / catch (InvalidOperationException ex when ex.Message.StartsWith("Reflection-based serialization has been disabled")) that falls back to the human-readable error path. This converts the crash into the same observable shape as status --json on the trimmed build (graceful "Error: ..." + a non-zero exit code).

In WriteCommandError:

private static int WriteCommandError(bool json, JsonSerializerOptions jsonOptions, string message, int exitCode, string? hint = null)
{
    if (json)
    {
        try
        {
            Console.WriteLine(JsonSerializer.Serialize(new { status = "error", message, hint }, jsonOptions));
            return exitCode;
        }
        catch (InvalidOperationException ex) when (ex.Message.Contains("Reflection-based serialization has been disabled"))
        {
            // Fall through to human-readable on trimmed builds
        }
    }
    Console.Error.WriteLine($"Error: {message}");
    if (hint != null) Console.Error.WriteLine($"Hint: {hint}");
    return exitCode;
}

Same shape around the success-path serialize at line 251.

This still leaves --json non-functional, but at least it's no longer SIGABRT — it matches #147's failure mode.

Layer 2 — fix the underlying serialization (mirrors #147 / #178)

Wire a source-generated JsonSerializerContext for every DTO emitted by --json paths, and replace anonymous objects with named DTOs (the source generator can't see anonymous types). Specifically:

  • A BackfillFoldSuccessResult DTO for the success payload.
  • A CommandErrorResult DTO for WriteCommandError.

Both registered with [JsonSerializable(...)] on a shared [JsonSourceGenerationOptions] partial class. Once that's in place, both --json success and error paths emit real JSON on the trimmed build.

Or alternatively, disable PublishTrimmed for the release binary (#147 suggestion 1). That fixes the family without touching the call sites.

Scope

  • src/CodeIndex/Cli/IndexCommandRunner.cs:251 — wrap or replace anonymous-object serialize in success path.
  • src/CodeIndex/Cli/IndexCommandRunner.cs:683-694 (WriteCommandError) — wrap or replace anonymous-object serialize in error path.
  • Audit the other 4 WriteCommandError callers (lines 200, 208, 238, 277, 446) for any --json arg-handling regression once the wrap lands.
  • src/CodeIndex/CodeIndex.csproj — Layer 2 alternative: source-gen context or disable trimming.
  • tests/CodeIndex.Tests/IndexCommandRunnerTests.cs — regression: invoke backfill-fold --json and assert it doesn't Environment.FailFast / SIGABRT, and assert exit code is one of the documented CommandExitCodes.

Related

Environment

  • cdidx: v1.10.0 (installed via install.shPublishTrimmed=true).
  • Target DB: any DB created by cdidx index ... — repro doesn't depend on DB contents, the crash is in the JSON output path.
  • Platform: linux-x64 container.
  • Filed from a cloud Claude Code session per CLOUD_BOOTSTRAP_PROMPT.md.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions