Skip to content

[BUG] [v0.1.0] execute_command_streaming truncation panics on multi-byte UTF-8 due to byte-index slicing (exec/runner.rs) #168

@climax-dev-1

Description

@climax-dev-1

Bug Description

execute_command_streaming() in cortex-engine/src/exec/runner.rs truncates stdout, stderr, and aggregated output using byte-index slicing (&stdout[..MAX_OUTPUT_SIZE]). This panics if the output contains multi-byte UTF-8 characters and MAX_OUTPUT_SIZE (1MB) falls inside a multi-byte character.

Location

src/cortex-engine/src/exec/runner.rs, lines 381–405 (three separate truncation sites)

Root Cause

// Line 381-386: stdout truncation
let stdout_truncated = if stdout.len() > MAX_OUTPUT_SIZE {
    format!(
        "{}...\n[Output truncated, {} bytes total]",
        &stdout[..MAX_OUTPUT_SIZE],  // BUG: byte-index slice
        stdout.len()
    )
} else {
    stdout
};

// Line 391-396: stderr truncation (same bug)
let stderr_truncated = if stderr.len() > MAX_OUTPUT_SIZE {
    format!(
        "{}...\n[Output truncated, {} bytes total]",
        &stderr[..MAX_OUTPUT_SIZE],  // BUG: byte-index slice
        stderr.len()
    )
} else {
    stderr
};

// Line 402-407: aggregated truncation (same bug)
let aggregated_truncated = if aggregated.len() > MAX_OUTPUT_SIZE {
    format!(
        "{}...\n[Output truncated, {} bytes total]",
        &aggregated[..MAX_OUTPUT_SIZE],  // BUG: byte-index slice
        aggregated.len()
    )
} else {
    aggregated
};

stdout, stderr, and aggregated are String values built from line-by-line reading. .len() returns byte length, and &s[..MAX_OUTPUT_SIZE] slices at a byte position that may fall inside a multi-byte UTF-8 character, causing a panic.

Note: The non-streaming truncate_output() function (line 223) has the same bug pattern with &s[..MAX_OUTPUT_SIZE] on a Cow<str> from String::from_utf8_lossy.

Reproduction

// A command that outputs >1MB of multi-byte content
// e.g., a file with CJK characters where byte 1048576 falls mid-character
// The streaming executor will panic when truncating

Fix

Use floor_char_boundary or iterate chars:

let stdout_truncated = if stdout.len() > MAX_OUTPUT_SIZE {
    let end = stdout.floor_char_boundary(MAX_OUTPUT_SIZE);
    format!(
        "{}...\n[Output truncated, {} bytes total]",
        &stdout[..end],
        stdout.len()
    )
} else {
    stdout
};

Apply the same fix to all four truncation sites (3 in streaming + 1 in truncate_output).

Impact

When a shell command produces >1MB of output containing multi-byte UTF-8 characters (e.g., cat on a large internationalized file, build output with Unicode paths), the executor panics. This crashes the tool execution and breaks the agentic loop. Since MAX_OUTPUT_SIZE is 1MB, this is triggered whenever large multi-byte output is produced by any shell command.

Version

v0.1.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions