sic swallows a bare --, breaking every nested command that needs one #1

Closed
opened 2026-07-22 21:05:10 +00:00 by williams · 1 comment

Summary

sic consumes a bare -- as its own argument separator instead of passing it through to the remote argv. Any nested tool that itself requires -- (incus exec, docker exec, kubectl exec, env, git) is therefore unusable through sic in its natural form.

This matters more than it sounds, because the fleet's normal shape is jump-host → container runtime → container, and that middle hop is exactly where -- lives.

Reproductions (both hit during one session, 2026-07-22)

1. Silently runs the wrong command on the wrong host

sic boltzmann incus exec memory -- docker exec stash-stash-1 /stash remember -n /ideas 'text'
sicd: exec: "docker": executable file not found in $PATH

sic ate the --, so everything after it became the command to run on boltzmann rather than being handed to incus exec. Note the failure mode: it does not error on the malformed invocation, it cheerfully runs something else.

2. Same class, different victim — command runs outside the container

sic dcw2 sudo incus exec pica -- systemctl reload caddy
Failed to reload caddy.service: Interactive authentication required.

This looks like a permissions problem and is not: systemctl ran on dcw2, not inside pica, because the -- vanished. Diagnosing this cost real time precisely because the error message points somewhere else entirely.

Why the workaround is not acceptable

The fix in both cases was:

sic <host> sh -c "incus exec <c> -- docker exec <c2> /stash remember -n /ideas 'text'"

which reintroduces full shell quoting for every hop after the first — the exact problem sic exists to eliminate. A 4-hop call (noether -> sic boltzmann -> incus exec memory -> docker exec stash -> /stash remember 'TEXT') ends up with one protected hop and three unprotected ones, and the payload quoted through all of them.

Suggested fix

Either of:

  1. Pass a literal -- through to the remote argv. If sic needs its own separator, require an explicit escape (sic host --sic-- cmd ...) rather than claiming the common one.
  2. Jump-host-to-container addressing — let the target be an address, e.g. sic boltzmann/memory <argv> or sic boltzmann/memory/stash-stash-1 <argv>, with sic constructing each runtime invocation itself and re-encoding argv as netstrings at every hop. This makes the bug moot by construction, since nothing ever hands a shell a string to parse. Needs a per-host runtime resolver (incus on boltzmann/hertz/dcw2/pve1, docker nested in memory, pct on data) or an explicit scheme like host/pct:107.

(2) is the more useful shape and is tracked as an idea in stash /ideas; (1) is the minimal correctness fix.

Related, separate bug

sic does not forward stdin, and does not say so. sic hertz tee /path <<EOF ... EOF produced a zero-byte file with no error and no non-zero exit — content silently lost. Every heredoc/pipe use since has had to be replaced by scp-then-move. Worth its own issue; at minimum it should fail loudly rather than write an empty file.

## Summary `sic` consumes a bare `--` as its own argument separator instead of passing it through to the remote argv. Any nested tool that itself requires `--` (`incus exec`, `docker exec`, `kubectl exec`, `env`, `git`) is therefore unusable through `sic` in its natural form. This matters more than it sounds, because the fleet's normal shape is **jump-host → container runtime → container**, and that middle hop is exactly where `--` lives. ## Reproductions (both hit during one session, 2026-07-22) **1. Silently runs the wrong command on the wrong host** ``` sic boltzmann incus exec memory -- docker exec stash-stash-1 /stash remember -n /ideas 'text' ``` ``` sicd: exec: "docker": executable file not found in $PATH ``` `sic` ate the `--`, so everything after it became the command to run **on boltzmann** rather than being handed to `incus exec`. Note the failure mode: it does not error on the malformed invocation, it cheerfully runs something else. **2. Same class, different victim — command runs outside the container** ``` sic dcw2 sudo incus exec pica -- systemctl reload caddy ``` ``` Failed to reload caddy.service: Interactive authentication required. ``` This looks like a permissions problem and is not: `systemctl` ran on **dcw2**, not inside `pica`, because the `--` vanished. Diagnosing this cost real time precisely because the error message points somewhere else entirely. ## Why the workaround is not acceptable The fix in both cases was: ``` sic <host> sh -c "incus exec <c> -- docker exec <c2> /stash remember -n /ideas 'text'" ``` which **reintroduces full shell quoting for every hop after the first** — the exact problem `sic` exists to eliminate. A 4-hop call (`noether -> sic boltzmann -> incus exec memory -> docker exec stash -> /stash remember 'TEXT'`) ends up with one protected hop and three unprotected ones, and the payload quoted through all of them. ## Suggested fix Either of: 1. **Pass a literal `--` through to the remote argv.** If `sic` needs its own separator, require an explicit escape (`sic host --sic-- cmd ...`) rather than claiming the common one. 2. **Jump-host-to-container addressing** — let the target be an address, e.g. `sic boltzmann/memory <argv>` or `sic boltzmann/memory/stash-stash-1 <argv>`, with `sic` constructing each runtime invocation itself and re-encoding argv as netstrings at **every** hop. This makes the bug moot by construction, since nothing ever hands a shell a string to parse. Needs a per-host runtime resolver (incus on boltzmann/hertz/dcw2/pve1, docker nested in `memory`, `pct` on data) or an explicit scheme like `host/pct:107`. (2) is the more useful shape and is tracked as an idea in stash `/ideas`; (1) is the minimal correctness fix. ## Related, separate bug `sic` **does not forward stdin, and does not say so**. `sic hertz tee /path <<EOF ... EOF` produced a **zero-byte file** with no error and no non-zero exit — content silently lost. Every heredoc/pipe use since has had to be replaced by scp-then-move. Worth its own issue; at minimum it should fail loudly rather than write an empty file.
Owner

Fixed in v0.2.0 (v2 wire protocol). Argv is carried end-to-end as length-framed netstrings — no layer re-parses or interprets it — so a bare -- survives into the innermost argv untouched, and nested incus exec … -- / docker exec / pct exec … chains work in their natural form. Covered by TestBareDoubleDashPayloadIsOpaque plus the argv-boundary tests, and smoke-verified live through a real 2-hop container chain. Released and rolled out fleet-wide.

Fixed in **v0.2.0** (v2 wire protocol). Argv is carried end-to-end as length-framed netstrings — no layer re-parses or interprets it — so a bare `--` survives into the innermost argv untouched, and nested `incus exec … --` / `docker exec` / `pct exec …` chains work in their natural form. Covered by `TestBareDoubleDashPayloadIsOpaque` plus the argv-boundary tests, and smoke-verified live through a real 2-hop container chain. Released and rolled out fleet-wide.
Sign in to join this conversation.
No Label
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marfrit/sic#1