Skip to content

Stop container system start from leaking into pall8t's stdout - #27

Merged
TakiTake merged 1 commit into
mainfrom
fix/system-start-stdout
Jul 11, 2026
Merged

Stop container system start from leaking into pall8t's stdout#27
TakiTake merged 1 commit into
mainfrom
fix/system-start-stdout

Conversation

@TakiTake

Copy link
Copy Markdown
Owner

Summary

Fixes the follow-up gap flagged in #26's description: container::system_start() ran container system start via Command::status() with no stdio configured, so the child inherited pall8t's real stdout. ensure_container_system() calls it whenever the container system service is stopped, and cmd_ls calls ensure_container_system() before printing its --json output — so a cold start (e.g. right after a reboot) could interleave the child's stdout ahead of the JSON, corrupting anything parsing it (the herdr integration in particular). This is intermittent by nature: it only triggers when the service happens to be stopped, so it "works" most of the time and fails unpredictably.

  • container::system_start now goes through the run_streaming plumbing added for pall8t buildのログが見たい #13's build-output fix (child stdout redirected to pall8t's own stderr, child stderr inherited).
  • util::run_streaming now takes an explicit stdin: Stdio parameter instead of hardcoding Stdio::null(). That default was right for container build (no business reading stdin) but wrong for container system start, which can prompt interactively on a fresh machine (e.g. apple/container's default-kernel-install confirmation) — closing its stdin unconditionally would turn that prompt into an unanswerable EOF. system_start now inherits stdin only when its own stdin is a TTY, and closes it otherwise, so a piped invocation (echo x | pall8t run) can't have its payload silently consumed by an unrelated prompt.
  • container::build_image (the other run_streaming caller) now explicitly passes Stdio::null(), preserving its original behavior.

Audit (as requested)

Grepped every subprocess spawn in the crate for the same shape (inherited stdout on a path that later prints machine-readable output): util.rs, home.rs (x2), herdr.rs, container.rs. Everything else uses Command::output(), which always captures rather than inherits, or is main.rs's exec_container, which uses .exec() to replace the pall8t process outright for TTY passthrough during run/exec and prints nothing machine-readable afterward in the same process (on success it never returns; on failure the error goes to stderr only). No other call sites needed changing.

Test plan

  • cargo fmt --check
  • cargo clippy --all-targets -- -D warnings
  • cargo test — 128 tests pass, including a new one proving the stdin parameter is actually threaded through to the child (via sh -c "read x"'s exit code as a proxy, since streamed output itself can't be captured in-process)
  • Ran /code-review (self) and two rounds of /skeptical-review (adversarial agent) — the second pass caught a real design gap in my first draft (unconditional Stdio::inherit() for system_start could leak piped stdin content into an unrelated prompt) and a self-contradictory doc comment; both fixed before this commit

Known minor edge case, not fixed

system_start (like build_image before it) now hard-fails if pall8t's own stderr (fd 2) is closed, since run_streaming needs to dup() it — e.g. pall8t ls --json 2>&- on a cold system now errors instead of starting the service. Pathological (deliberately closing fd 2), and already true for build_image since #26; this PR just extends the same characteristic to ls/run/stop's startup path. Flagging rather than fixing — didn't want to add scope for an edge case this narrow.

container::system_start() ran `container system start` via
Command::status() with no stdio configured, so the child inherited
pall8t's real stdout. ensure_container_system() calls it whenever the
service is stopped, and cmd_ls calls ensure_container_system() before
printing its --json output — so a cold start (e.g. right after a
reboot) could interleave the child's stdout ahead of the JSON,
corrupting anything parsing it (the herdr integration in particular).
Intermittent by nature: only triggers when the service happens to be
stopped, so it "works" until it doesn't.

Fixed by routing system_start through the run_streaming plumbing
added for #13's build-output fix (container's stdout redirected to
pall8t's own stderr, stderr inherited). util::run_streaming now takes
an explicit `stdin: Stdio` parameter instead of hardcoding
Stdio::null() — that default was right for `container build` (no
business reading stdin) but wrong for `container system start`, which
can prompt interactively on a fresh machine (e.g. a default-kernel-
install confirmation); closing its stdin unconditionally would have
turned that prompt into an unanswerable EOF. system_start now inherits
stdin only when its own stdin is a TTY, and closes it otherwise, so a
piped invocation (`echo x | pall8t run`) can't have its payload
consumed by an unrelated prompt.

Audited every other subprocess spawn in the crate for the same shape
(inherited stdout on a path that later prints machine-readable
output): everything else either captures via Command::output() or is
main.rs's exec_container, which replaces the pall8t process outright
for TTY passthrough and prints nothing afterward in-process. No other
call sites needed changing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@TakiTake
TakiTake merged commit b085d1d into main Jul 11, 2026
2 checks passed
@TakiTake
TakiTake deleted the fix/system-start-stdout branch July 11, 2026 14:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant