Split out of review feedback on #849 so that PR stays scoped to the LXC state-aware lifecycle slice.
Problem
For a normal (streaming) LXC exec, stdout carries guest-controlled output, but the SDK parses stdout as protocol data before consulting the executor-owned stderr channel.
execInSandboxAsync (sdk/node/src/state-aware.ts:191-193):
const errorEnvelope =
tryParseErrorEnvelope(stdout) ??
(backendKey === 'lxc' ? tryParseErrorEnvelopeFromLines(stderr) : null);
tryParseErrorEnvelope (sdk/node/src/state-aware-helper.ts:175-188) whole-string parses: JSON.parse(stdout.trim()), returning the envelope whenever the result has an error.code.
So a guest script whose complete stdout is a valid {"error":{"code":...}} document and which exits nonzero is thrown to the caller as an MxcError, even though it ran normally and that JSON is simply its output. The guest can forge any error code the SDK surfaces.
The last-non-empty-line narrowing that the surrounding comment cites as the safeguard lives in tryParseErrorEnvelopeFromLines, which is only applied to stderr. Nothing narrows the stdout path.
Why it is not fixed in #849
The stdout parse is not dead code -- a non-streaming (dry-run) exec legitimately uses stdout as its single client-facing channel, so it cannot simply be deleted. Fixing this means deciding per dispatch mode which channel is authoritative and making stderr the sole trusted channel for streaming LXC execs, including parse-time errors. That is a change to the exec dispatch contract shared with the other backends, not a local edit.
Suggested fix
Give LXC exec one trusted channel (stderr) for dispatch and parse-time errors, and stop interpreting guest stdout as protocol data on that path. Keep the stdout parse only where the executor owns stdout.
Found by Copilot review on #849 (comment 3780192923).
Split out of review feedback on #849 so that PR stays scoped to the LXC state-aware lifecycle slice.
Problem
For a normal (streaming) LXC exec, stdout carries guest-controlled output, but the SDK parses stdout as protocol data before consulting the executor-owned stderr channel.
execInSandboxAsync(sdk/node/src/state-aware.ts:191-193):tryParseErrorEnvelope(sdk/node/src/state-aware-helper.ts:175-188) whole-string parses:JSON.parse(stdout.trim()), returning the envelope whenever the result has anerror.code.So a guest script whose complete stdout is a valid
{"error":{"code":...}}document and which exits nonzero is thrown to the caller as anMxcError, even though it ran normally and that JSON is simply its output. The guest can forge any error code the SDK surfaces.The last-non-empty-line narrowing that the surrounding comment cites as the safeguard lives in
tryParseErrorEnvelopeFromLines, which is only applied to stderr. Nothing narrows the stdout path.Why it is not fixed in #849
The stdout parse is not dead code -- a non-streaming (dry-run) exec legitimately uses stdout as its single client-facing channel, so it cannot simply be deleted. Fixing this means deciding per dispatch mode which channel is authoritative and making stderr the sole trusted channel for streaming LXC execs, including parse-time errors. That is a change to the exec dispatch contract shared with the other backends, not a local edit.
Suggested fix
Give LXC exec one trusted channel (stderr) for dispatch and parse-time errors, and stop interpreting guest stdout as protocol data on that path. Keep the stdout parse only where the executor owns stdout.
Found by Copilot review on #849 (comment 3780192923).