Skip to content

docs: specify command execution as two tiers - #565

Merged
David-Noble-at-work merged 1 commit into
developfrom
command-execution-design
Aug 19, 2026
Merged

David-Noble-at-work merged 1 commit into
developfrom
command-execution-design

Conversation

@David-Noble-at-work

Copy link
Copy Markdown
Contributor

A command is an argv vector. A script is a file that an argv vector runs. No third thing, and in particular
no command string assembled by concatenation and handed to an interpreter's -c.

docs/architecture/6.3-command-execution.md, with its status companion, indexed after 6.2.

Why now

Three places build command strings by concatenation, and each is exploitable, broken, or both:

  • pkg/platform — 70 concatenating call sites reaching sudo -n bash -c, with ParsePURL applying no charset
    validation and decoding %3B to ;
  • shell.exec / powershell.exec — both take an opaque string, with different escaping dialects, so one
    quoting helper cannot serve both
  • two test fixtures interpolated a native path into an sh -c string, where Windows backslashes were consumed
    as escapes

The two tiers

Tier 1 is exec.Command with an argv vector. All 70 platform commands fit, once two habits are unlearned:
a format string quoted only to hide it from sh is a plain argument, and a pipe whose right-hand side is a text
utility is a Go expression — | grep is strings.Contains, | wc -l is a line count.

Tier 2 takes a script string, writes it to a file, and runs it through tier 1. Pipes, loops and redirects
work because they're script content.

Tier 2's body stays static; values arrive as $1, $2. This is the half that makes it safe rather than
merely tidier — interpolating into the body would relocate the escaping bug, since script text is still shell
source. It also means no quoter is needed in either dialect.

Two details worth the review

  • The temp file goes in the scratch root, not os.CreateTemp — confinement, self-removing lifetime, and the
    restrictive mode a script carrying a secret needs on both platforms. The executable bit is never set; the
    interpreter is named explicitly.
  • Result.Command carries the argv vector for tier 1 and the script source for tier 2. Never the temp
    path, which is unauditable once scratch is reclaimed; never a joined string, which isn't what the kernel
    received.

Open

Five questions are recorded rather than assumed. The sharpest: does shell.exec(command=…) survive? It's the
only shape requiring escaping, so keeping it keeps the defect class alive, and retiring it breaks an authoring
surface.

#561's pkg/platform rewrite is tier 1 and can proceed as the first delivery without waiting for the
tiers to be built.

Refs #561.

A command is an argv vector. A script is a file that an argv vector runs. There is no third thing, and in
particular no command string assembled by concatenation and handed to an interpreter's -c.

The rule removes a defect class rather than defending against it. Today three places build command strings by
concatenation, and each is exploitable, broken, or both: pkg/platform reaches sudo -n bash -c through seventy
concatenating call sites while ParsePURL validates no charset and decodes %3B to a semicolon; shell.exec and
powershell.exec each take an opaque string; and two fixtures interpolated a native path into an sh -c string,
where windows backslashes were consumed as escapes.

Tier 1 is exec.Command with an argv vector. All seventy platform commands fit it once two habits are unlearned:
format strings quoted only to hide them from sh are plain arguments, and a pipe whose right side is a text
utility is a Go expression -- grep is strings.Contains, wc -l is a line count.

Tier 2 accepts a script as a string, writes it to a file, and runs it through tier 1. Pipes, loops and
redirects work because they are script content rather than argv. Its body stays static and values arrive as
$1, $2: interpolating into the body would relocate the escaping bug rather than remove it, since script text is
still shell source. Static bodies also need no quoter for either dialect, which matters because sh and
PowerShell disagree about how to escape a quote.

The file is created through the scratch root rather than os.CreateTemp, for confinement, a self-removing
lifetime, and the restrictive mode a script carrying a secret needs on both platforms. The executable bit is
never set; the interpreter is named explicitly.

Result.Command carries the argv vector for tier 1 and the script source for tier 2 -- never the temp path,
which is unauditable once scratch is reclaimed, and never a joined string, which is not what the kernel
received.

Five questions are left open rather than assumed, the sharpest being whether shell.exec(command=) survives at
all: it is the only shape that requires escaping, so keeping it keeps the defect class alive, and retiring it
breaks an authoring surface.

Refs #561.
@David-Noble-at-work David-Noble-at-work self-assigned this Aug 19, 2026
@David-Noble-at-work
David-Noble-at-work merged commit b6bc271 into develop Aug 19, 2026
7 of 8 checks passed
@David-Noble-at-work
David-Noble-at-work deleted the command-execution-design branch August 19, 2026 17:46
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