feat(security): #496 — --lockdown compile flag refuses arbitrary-code-execution surfaces#961
Merged
Merged
Conversation
7 tasks
…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
a140cc1 to
d096b6b
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
perry-jsruntimeis reachable from the module graph.perry.nativeLibraryarchive is referenced.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_commanddriver 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
CompilationContextfields (needs_js_runtime,native_libraries) directly. The HIR walker is the new piece.Enabling lockdown (priority: package.json → env → CLI, last wins)
perry compile --lockdown ...PERRY_LOCKDOWN=1(andPERRY_LOCKDOWN=0explicitly disables)package.json:{ "perry": { "lockdown": true } }Diagnostic example
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 7ChildProcess*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_process—NativeMethodCall { 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 tochild_process;fs.readFileSyncdoesn't trip.End-to-end smoke (all four cases verified against the release binary):
--lockdown→ compiles cleanly.--lockdown+child_process.execSync→ fails with combined diagnostic naming the call site.PERRY_LOCKDOWN=1→ same effect via env.--lockdownon a program that doesn't reach any forbidden surface → compiles cleanly.cargo test --release -p perry— 247 tests pass.Acceptance
--lockdownCLI flag (alsoPERRY_LOCKDOWN=1env, alsoperry.lockdown: truein host package.json)perry-jsruntimeis reachableperry.nativeLibraryarchive is referencedchild_process.*is called from any source moduleobj[runtimeVar]()) #503's unconditional refusal already blocksobj[runtimeVar](), so lockdown gets that surface for free without an explicit hook hereCompilationContextflags that security: gateperry-jsruntimebehind explicit host opt-in #499 / security: host-app allowlist forperry.nativeLibraryandperry.compilePackages#497 populateNotes
No
Cargo.tomlversion bump, noCLAUDE.mdversion line touch, noCHANGELOG.mdentry — maintainer folds those in at merge time.