Skip to content

feat(security): #496 — --lockdown compile flag refuses arbitrary-code-execution surfaces#961

Merged
proggeramlug merged 1 commit into
mainfrom
feat/496-lockdown
May 18, 2026
Merged

feat(security): #496 — --lockdown compile flag refuses arbitrary-code-execution surfaces#961
proggeramlug merged 1 commit into
mainfrom
feat/496-lockdown

Conversation

@proggeramlug
Copy link
Copy Markdown
Contributor

Closes #496.

Summary

Single-flag opt-in to "this app is provably free of arbitrary-code-execution vectors." When set, the build fails if any of:

  1. perry-jsruntime is reachable from the module graph.
  2. Any perry.nativeLibrary archive is referenced.
  3. Any source module reaches child_process.*.

All three checks run together; the diagnostic lists every offending surface in one combined error so the reviewer can address the whole surface at once.

Zero runtime cost — purely a compile-time check.

Cross-platform — runs in the platform-agnostic compile_command driver before any backend (LLVM / WASM / ArkTS / HarmonyOS / Glance / SwiftUI / JS) is invoked. Every target inherits the protection from one choke point.

Standalone — doesn't depend on the in-flight #499 / #497 / #503 PRs. Reads existing CompilationContext fields (needs_js_runtime, native_libraries) directly. The HIR walker is the new piece.

Enabling lockdown (priority: package.json → env → CLI, last wins)

  • CLI: perry compile --lockdown ...
  • Env: PERRY_LOCKDOWN=1 (and PERRY_LOCKDOWN=0 explicitly disables)
  • package.json: { "perry": { "lockdown": true } }

Diagnostic example

Error: `--lockdown` refused the build because the following
arbitrary-code-execution surfaces are reachable:
  - perry-jsruntime (QuickJS-based eval-equivalent) is reachable
    from the module graph — see #499 docs for the matching opt-in
    gate
  - `perry.nativeLibrary` archives referenced by: @bloomengine/engine
  - `child_process.*` reached from 2 call site(s):
      - /repo/src/main.ts: child_process.execSync
      - /repo/lib/foo.ts: child_process.spawn

Site list capped at 12 entries to keep output bounded on pathological builds.

Test coverage

6 unit tests in perry-hir::lockdown::tests:

  • empty_module_has_no_violations — invariant.
  • top_level_exec_sync_records_violation — basic case.
  • nested_call_inside_if_recorded — walker descends into all Stmt arms.
  • every_specialised_variant_caught — exhaustive over the 7 ChildProcess* HIR variants (ChildProcessExec / ExecSync / Spawn / SpawnSync / SpawnBackground / GetProcessStatus / KillProcess). Pins the kind-name string for each so a regression in the walker surfaces here.
  • general_native_call_through_child_processNativeMethodCall { module: "child_process", method: "fork", … } fallback for any future spawn variant the HIR doesn't have a dedicated variant for yet.
  • unrelated_native_call_not_flagged — lockdown is scoped to child_process; fs.readFileSync doesn't trip.

End-to-end smoke (all four cases verified against the release binary):

  • No --lockdown → compiles cleanly.
  • --lockdown + child_process.execSync → fails with combined diagnostic naming the call site.
  • PERRY_LOCKDOWN=1 → same effect via env.
  • --lockdown on a program that doesn't reach any forbidden surface → compiles cleanly.

cargo test --release -p perry — 247 tests pass.

Acceptance

Notes

No Cargo.toml version bump, no CLAUDE.md version line touch, no CHANGELOG.md entry — maintainer folds those in at merge time.

…xecution surfaces

Single-flag opt-in to "this app is provably free of arbitrary-code-
execution vectors." When set, the build fails if any of:

  1. perry-jsruntime is reachable from the module graph
     (ctx.needs_js_runtime).
  2. Any perry.nativeLibrary archive is referenced
     (ctx.native_libraries non-empty).
  3. Any source module reaches child_process.*
     (HIR walk over ChildProcessExec / ChildProcessExecSync /
     ChildProcessSpawn / ChildProcessSpawnSync / ChildProcessSpawnBackground /
     ChildProcessGetProcessStatus / ChildProcessKillProcess + the
     general-shape NativeMethodCall { module: "child_process", \u2026 }
     fallback for any future spawn variants).

All three checks run together; the diagnostic lists every offending
surface in one combined error so the reviewer can address the whole
surface at once. child_process site list capped at 12 entries to keep
output bounded on pathological builds.

Enabling lockdown (priority: package.json -> env -> CLI, last wins):
- CLI: perry compile --lockdown ...
- Env: PERRY_LOCKDOWN=1 (PERRY_LOCKDOWN=0 explicitly disables)
- package.json: { "perry": { "lockdown": true } }

Cross-platform: runs in the platform-agnostic compile_command driver
before any backend (LLVM / WASM / ArkTS / HarmonyOS / Glance / SwiftUI
/ JS) is invoked. Every target inherits lockdown from one choke point.

Standalone implementation - doesn't depend on the in-flight #499 /
(needs_js_runtime, native_libraries) directly. The HIR walker is
new (perry_hir::lockdown).

perry-hir::lockdown module - 6 unit tests:
- empty module has no violations
- top-level execSync recorded
- nested call inside if recorded
- every ChildProcess* specialised variant caught (7 variants
  exhaustively pinned)
- general NativeMethodCall through child_process caught
- unrelated NativeMethod call (fs.readFileSync) not flagged

End-to-end smoke (all four cases verified against the release binary):
- No --lockdown -> compiles cleanly
- --lockdown + child_process.execSync -> fails with combined diagnostic
- PERRY_LOCKDOWN=1 -> same effect
- --lockdown on a clean program (no jsruntime / nativeLibrary /
  child_process) -> compiles cleanly

CompileArgs additions:
- pub lockdown: bool (#[arg(long)])
- CompilationContext.lockdown: bool

run.rs and dev.rs CompileArgs initialisers updated with the new field.

Acceptance:
- [x] --lockdown CLI flag (also PERRY_LOCKDOWN=1 env, also
      perry.lockdown: true in host package.json)
- [x] Refuses to link if perry-jsruntime is reachable
- [x] Refuses if any perry.nativeLibrary archive is referenced
- [x] Refuses if child_process.* is called from any source module
- [partial] Refuses if dynamic-code APIs are reached - #503's
      unconditional refusal already blocks obj[runtimeVar](), so
      lockdown gets that for free
- [x] Clear error messages pointing at the offending source span
- [x] Composes with the other supply-chain issues - reads the same
      CompilationContext flags they populate
@proggeramlug proggeramlug merged commit d4bb9a0 into main May 18, 2026
@proggeramlug proggeramlug deleted the feat/496-lockdown branch May 18, 2026 10: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.

security: --lockdown compile flag — refuse risky linkage

1 participant