Skip to content

Commit 02c97ca

Browse files
committed
feat: US-020 - Sandbox-side SecureExec.bindings injection
1 parent d633908 commit 02c97ca

3 files changed

Lines changed: 76 additions & 2 deletions

File tree

packages/secure-exec-nodejs/src/execution-driver.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ import type {
7373
ProcessConfig,
7474
} from "@secure-exec/core/internal/shared/api-types";
7575
import type { BudgetState } from "./isolate-bootstrap.js";
76-
import { type FlattenedBinding, flattenBindingTree } from "./bindings.js";
76+
import { type FlattenedBinding, flattenBindingTree, BINDING_PREFIX } from "./bindings.js";
7777

7878
export { NodeExecutionDriverOptions };
7979

@@ -558,6 +558,9 @@ export class NodeExecutionDriver implements RuntimeDriver {
558558
const bridgeCode = buildFullBridgeCode();
559559

560560
// Build post-restore script with per-execution config
561+
const bindingKeys = this.flattenedBindings
562+
? this.flattenedBindings.map((b) => b.key.slice(BINDING_PREFIX.length))
563+
: [];
561564
const postRestoreScript = buildPostRestoreScript(
562565
execProcessConfig,
563566
s.osConfig,
@@ -573,6 +576,7 @@ export class NodeExecutionDriver implements RuntimeDriver {
573576
frozenTimeMs,
574577
options.mode,
575578
options.filePath,
579+
bindingKeys,
576580
);
577581

578582
// Execute in V8 session
@@ -726,6 +730,7 @@ function buildPostRestoreScript(
726730
frozenTimeMs: number,
727731
mode: "run" | "exec",
728732
filePath?: string,
733+
bindingKeys?: string[],
729734
): string {
730735
const parts: string[] = [];
731736

@@ -803,6 +808,9 @@ function buildPostRestoreScript(
803808
})};`);
804809
parts.push(getIsolateRuntimeSource("applyCustomGlobalPolicy"));
805810

811+
// Inflate SecureExec.bindings from flattened __bind.* globals
812+
parts.push(buildBindingsInflationSnippet(bindingKeys ?? []));
813+
806814
return parts.join("\n");
807815
}
808816

@@ -819,3 +827,24 @@ function getHardenedGlobals(): string[] { return HARDENED_NODE_CUSTOM_GLOBALS; }
819827
function getMutableGlobals(): string[] { return MUTABLE_NODE_CUSTOM_GLOBALS; }
820828
function getProcessConfigGlobalKey(): string { return HOST_BRIDGE_GLOBAL_KEYS.processConfig; }
821829
function getOsConfigGlobalKey(): string { return HOST_BRIDGE_GLOBAL_KEYS.osConfig; }
830+
831+
/** Build the JS snippet that inflates __bind.* globals into a frozen SecureExec.bindings tree. */
832+
function buildBindingsInflationSnippet(bindingKeys: string[]): string {
833+
return `(function(){
834+
var __bindingKeys__=${JSON.stringify(bindingKeys)};
835+
var tree={};
836+
for(var i=0;i<__bindingKeys__.length;i++){
837+
var parts=__bindingKeys__[i].split(".");
838+
var node=tree;
839+
for(var j=0;j<parts.length-1;j++){node[parts[j]]=node[parts[j]]||{};node=node[parts[j]];}
840+
node[parts[parts.length-1]]=globalThis["__bind."+__bindingKeys__[i]];
841+
}
842+
function deepFreeze(obj){
843+
var vals=Object.values(obj);
844+
for(var k=0;k<vals.length;k++){if(typeof vals[k]==="object"&&vals[k]!==null)deepFreeze(vals[k]);}
845+
return Object.freeze(obj);
846+
}
847+
Object.defineProperty(globalThis,"SecureExec",{value:Object.freeze({bindings:deepFreeze(tree)}),writable:false,enumerable:true,configurable:false});
848+
for(var i=0;i<__bindingKeys__.length;i++){delete globalThis["__bind."+__bindingKeys__[i]];}
849+
})();`;
850+
}

scripts/ralph/prd.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,7 @@
320320
"Typecheck passes"
321321
],
322322
"priority": 20,
323-
"passes": false,
323+
"passes": true,
324324
"notes": "Custom bindings Phase 2. ~15-20 LOC for the inflation snippet. Depends on US-019."
325325
},
326326
{

scripts/ralph/progress.txt

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
- Docs compatibility link slug is nodejs-compatibility (not node-compatability)
3535
- WasmVM native code (Rust, C, patches) canonical location is native/wasmvm/ (alongside native/v8-runtime/)
3636
- Test files in packages/secure-exec-wasmvm/test/ use ../../../native/wasmvm/target/ for WASM binary paths (3 levels up from test/ to repo root)
37+
- Custom bindings types/validation/flattening live in secure-exec-nodejs/src/bindings.ts — flattened __bind.* keys merge into bridgeHandlers Record
38+
- BridgeHandler = (...args: unknown[]) => unknown | Promise<unknown> — both sync and async handlers work through the same V8 IPC bridge
3739

3840
# Ralph Progress Log
3941
Started: Sat Mar 21 02:49:43 AM PDT 2026
@@ -525,3 +527,46 @@ Started: Sat Mar 21 02:49:43 AM PDT 2026
525527
- CI workflow had a stale `cd wasmvm` that was missed in US-016's path update sweep — always verify CI with `grep -r` after path moves
526528
- packages/secure-exec-node/ (old name) still has a stale dist/ directory — harmless but cruft
527529
---
530+
531+
## 2026-03-21 06:25 - US-019
532+
- Implemented custom bindings core plumbing for host-to-sandbox function bridge
533+
- Created bindings.ts with BindingTree/BindingFunction types, validation, and flattenBindingTree()
534+
- Added bindings?: BindingTree to NodeRuntimeOptions (kernel-runtime.ts)
535+
- Added bindings?: BindingTree to NodeExecutionDriverOptions (isolate-bootstrap.ts)
536+
- Threaded bindings through NodeRuntimeDriver → NodeExecutionDriver constructor
537+
- Flattened bindings merged into bridgeHandlers with __bind. prefix in executeInternal()
538+
- Validation rejects: invalid JS identifiers, keys starting with _, nesting > 4, leaf count > 64
539+
- Sync/async detection via AsyncFunction instanceof check
540+
- Exported BindingTree, BindingFunction, BINDING_PREFIX from @secure-exec/nodejs and secure-exec barrel
541+
- Files changed:
542+
- packages/secure-exec-nodejs/src/bindings.ts (new — types, validation, flattening)
543+
- packages/secure-exec-nodejs/src/kernel-runtime.ts (bindings option + threading)
544+
- packages/secure-exec-nodejs/src/isolate-bootstrap.ts (NodeExecutionDriverOptions.bindings)
545+
- packages/secure-exec-nodejs/src/execution-driver.ts (flattenedBindings field, merge into bridgeHandlers)
546+
- packages/secure-exec-nodejs/src/index.ts (re-exports)
547+
- packages/secure-exec/src/index.ts (barrel re-exports)
548+
- **Learnings for future iterations:**
549+
- bridgeHandlers is a simple Record<string, BridgeHandler> — any key added to this map becomes callable from sandbox via V8 IPC bridge (no Rust changes needed)
550+
- Internal bridge names all start with single _ (e.g., _fsReadFile, _log) — custom bindings use __bind. prefix to avoid collision
551+
- NodeExecutionDriverOptions extends RuntimeDriverOptions (from core), but bindings are Node-specific so extend at the node level only
552+
- AsyncFunction detection: `Object.getPrototypeOf(async function () {}).constructor` — instanceof check works for all async functions
553+
- Validation runs once at construction time, flattened result cached — merge into bridgeHandlers is per-execution
554+
---
555+
556+
## 2026-03-21 06:36 - US-020
557+
- Implemented sandbox-side SecureExec.bindings injection in execution-driver.ts
558+
- Added buildBindingsInflationSnippet() function that generates the inflation JS snippet
559+
- Inflation snippet: builds nested object tree from __bind.* globals, deep-freezes it, sets as globalThis.SecureExec
560+
- SecureExec is non-writable, non-configurable via Object.defineProperty
561+
- Raw __bind.* globals deleted from globalThis after inflation
562+
- SecureExec.bindings is always present (empty frozen object when no bindings registered)
563+
- Binding keys extracted from flattenedBindings by stripping BINDING_PREFIX, passed as JSON literal to snippet
564+
- Files changed:
565+
- packages/secure-exec-nodejs/src/execution-driver.ts (30 LOC added — buildBindingsInflationSnippet function, binding keys extraction, parameter threading)
566+
- **Learnings for future iterations:**
567+
- buildPostRestoreScript() is the right injection point for per-execution sandbox setup code — it runs after bridge code snapshot phase so bridge calls work
568+
- Inflation snippet must use var (not const/let) for broader V8 compatibility in the injected context
569+
- BINDING_PREFIX ("__bind.") is the separator — binding keys stored without prefix in the inflation snippet, prefixed when looking up globals
570+
- Object.defineProperty with writable:false, configurable:false ensures sandbox code cannot delete or overwrite SecureExec
571+
- deepFreeze recursion only freezes objects, not functions — leaf binding functions remain callable but their container objects are frozen
572+
---

0 commit comments

Comments
 (0)