Skip to content

Commit 19914e4

Browse files
NathanFlurryclaude
andcommitted
feat: US-065 - Wire post-restore init script through IPC
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent db9cccf commit 19914e4

11 files changed

Lines changed: 149 additions & 16 deletions

File tree

crates/v8-runtime/src/execution.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,54 @@ fn run_bridge_cached(
237237
(0, None)
238238
}
239239

240+
/// Run a short init script (e.g. post-restore config). Compiles and executes
241+
/// via v8::Script, returning (exit_code, error) on failure. No code caching.
242+
pub fn run_init_script(
243+
scope: &mut v8::HandleScope,
244+
code: &str,
245+
) -> (i32, Option<ExecutionError>) {
246+
if code.is_empty() {
247+
return (0, None);
248+
}
249+
let tc = &mut v8::TryCatch::new(scope);
250+
let source = match v8::String::new(tc, code) {
251+
Some(s) => s,
252+
None => {
253+
return (
254+
1,
255+
Some(ExecutionError {
256+
error_type: "Error".into(),
257+
message: "init script string too large for V8".into(),
258+
stack: String::new(),
259+
code: None,
260+
}),
261+
);
262+
}
263+
};
264+
let script = match v8::Script::compile(tc, source, None) {
265+
Some(s) => s,
266+
None => {
267+
return match tc.exception() {
268+
Some(e) => {
269+
let (c, err) = exception_to_result(tc, e);
270+
(c, Some(err))
271+
}
272+
None => (1, None),
273+
};
274+
}
275+
};
276+
if script.run(tc).is_none() {
277+
return match tc.exception() {
278+
Some(e) => {
279+
let (c, err) = exception_to_result(tc, e);
280+
(c, Some(err))
281+
}
282+
None => (1, None),
283+
};
284+
}
285+
(0, None)
286+
}
287+
240288
/// Execute user code as a CJS script (mode='exec').
241289
///
242290
/// Runs bridge_code as IIFE first (if non-empty), then compiles and runs user_code

crates/v8-runtime/src/ipc_binary.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ pub enum BinaryFrame {
6060
mode: u8, // 0 = exec, 1 = run
6161
file_path: String,
6262
bridge_code: String,
63+
post_restore_script: String,
6364
user_code: String,
6465
},
6566
BridgeResponse {
@@ -224,6 +225,7 @@ fn encode_body(buf: &mut Vec<u8>, frame: &BinaryFrame) -> io::Result<()> {
224225
mode,
225226
file_path,
226227
bridge_code,
228+
post_restore_script,
227229
user_code,
228230
} => {
229231
buf.push(MSG_EXECUTE);
@@ -235,6 +237,10 @@ fn encode_body(buf: &mut Vec<u8>, frame: &BinaryFrame) -> io::Result<()> {
235237
let bc_bytes = bridge_code.as_bytes();
236238
buf.extend_from_slice(&(bc_bytes.len() as u32).to_be_bytes());
237239
buf.extend_from_slice(bc_bytes);
240+
// post_restore_script length (u32 BE)
241+
let prs_bytes = post_restore_script.as_bytes();
242+
buf.extend_from_slice(&(prs_bytes.len() as u32).to_be_bytes());
243+
buf.extend_from_slice(prs_bytes);
238244
// user_code (rest of frame)
239245
buf.extend_from_slice(user_code.as_bytes());
240246
}
@@ -377,13 +383,16 @@ fn decode_body(buf: &[u8]) -> io::Result<BinaryFrame> {
377383
let file_path = read_utf8(buf, &mut pos, fp_len)?;
378384
let bc_len = read_u32(buf, &mut pos)? as usize;
379385
let bridge_code = read_utf8(buf, &mut pos, bc_len)?;
386+
let prs_len = read_u32(buf, &mut pos)? as usize;
387+
let post_restore_script = read_utf8(buf, &mut pos, prs_len)?;
380388
let remaining = buf.len() - pos;
381389
let user_code = read_utf8(buf, &mut pos, remaining)?;
382390
Ok(BinaryFrame::Execute {
383391
session_id,
384392
mode,
385393
file_path,
386394
bridge_code,
395+
post_restore_script,
387396
user_code,
388397
})
389398
}
@@ -640,6 +649,7 @@ mod tests {
640649
mode: 0,
641650
file_path: "".into(),
642651
bridge_code: "(function(){ /* bridge */ })()".into(),
652+
post_restore_script: "".into(),
643653
user_code: "console.log('hello')".into(),
644654
});
645655
}
@@ -651,6 +661,7 @@ mod tests {
651661
mode: 1,
652662
file_path: "/app/index.mjs".into(),
653663
bridge_code: "(function(){ /* bridge */ })()".into(),
664+
post_restore_script: "__runtimeApplyConfig({})".into(),
654665
user_code: "export default 42".into(),
655666
});
656667
}
@@ -900,6 +911,7 @@ mod tests {
900911
mode: 0,
901912
file_path: "".into(),
902913
bridge_code: "bridge()".into(),
914+
post_restore_script: "".into(),
903915
user_code: "1+1".into(),
904916
},
905917
BinaryFrame::DestroySession {
@@ -989,6 +1001,7 @@ mod tests {
9891001
mode: 0,
9901002
file_path: "".into(),
9911003
bridge_code: "".into(),
1004+
post_restore_script: "".into(),
9921005
user_code: "".into(),
9931006
},
9941007
BinaryFrame::BridgeResponse {
@@ -1085,6 +1098,7 @@ mod tests {
10851098
mode: 0,
10861099
file_path: "".into(),
10871100
bridge_code: "".into(),
1101+
post_restore_script: "".into(),
10881102
user_code: "".into(),
10891103
},
10901104
0x05,
@@ -1354,6 +1368,7 @@ mod tests {
13541368
mode: 0,
13551369
file_path: long_path,
13561370
bridge_code: "".into(),
1371+
post_restore_script: "".into(),
13571372
user_code: "".into(),
13581373
};
13591374
let result = frame_to_bytes(&frame);

crates/v8-runtime/src/session.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,7 @@ fn session_thread(
309309
BinaryFrame::Execute {
310310
session_id,
311311
bridge_code,
312+
post_restore_script,
312313
user_code,
313314
mode,
314315
file_path,
@@ -401,6 +402,30 @@ fn session_thread(
401402
);
402403
}
403404

405+
// Run post-restore init script (config, mutable state reset)
406+
// after bridge fn replacement but before user code
407+
if !post_restore_script.is_empty() {
408+
let scope = &mut v8::HandleScope::new(iso);
409+
let ctx = v8::Local::new(scope, &exec_context);
410+
let scope = &mut v8::ContextScope::new(scope, ctx);
411+
let (prs_code, prs_err) = execution::run_init_script(scope, &post_restore_script);
412+
if prs_code != 0 {
413+
let result_frame = BinaryFrame::ExecutionResult {
414+
session_id,
415+
exit_code: prs_code,
416+
exports: None,
417+
error: prs_err.map(|e| ExecutionErrorBin {
418+
error_type: e.error_type,
419+
message: e.message,
420+
stack: e.stack,
421+
code: e.code.unwrap_or_default(),
422+
}),
423+
};
424+
send_message(&ipc_tx, &result_frame, &mut msg_frame_buf);
425+
continue;
426+
}
427+
}
428+
404429
// Start timeout guard before execution
405430
let mut timeout_guard = match (cpu_time_limit_ms, maybe_abort_tx) {
406431
(Some(ms), Some(abort_tx)) => {

packages/secure-exec-core/isolate-runtime/src/common/runtime-globals.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ declare global {
114114
payloadLimitErrorCode?: string;
115115
}) => void)
116116
| undefined;
117+
var __runtimeResetProcessState: (() => void) | undefined;
117118
var __runtimeProcessCwdOverride: unknown;
118119
var __runtimeProcessEnvOverride: unknown;
119120
var __runtimeStdinData: unknown;

packages/secure-exec-core/src/bridge/process.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,8 @@ function getNowMs(): number {
113113
: Date.now();
114114
}
115115

116-
// Start time for uptime calculation
117-
const _processStartTime = getNowMs();
116+
// Start time for uptime calculation (mutable for snapshot restore)
117+
let _processStartTime = getNowMs();
118118

119119
const BUFFER_MAX_LENGTH =
120120
typeof (BufferPolyfill as unknown as { kMaxLength?: unknown }).kMaxLength ===
@@ -156,6 +156,19 @@ if (
156156
let _exitCode = 0;
157157
let _exited = false;
158158

159+
// Expose reset function for snapshot restore — resets mutable state
160+
// captured in this closure so each restored context starts fresh.
161+
(globalThis as Record<string, unknown>).__runtimeResetProcessState =
162+
function () {
163+
_processStartTime =
164+
typeof performance !== "undefined" && performance.now
165+
? performance.now()
166+
: Date.now();
167+
_exitCode = 0;
168+
_exited = false;
169+
delete (globalThis as Record<string, unknown>).__runtimeResetProcessState;
170+
};
171+
159172
/**
160173
* Thrown by `process.exit()` to unwind the sandbox call stack. The host
161174
* catches this to extract the exit code without killing the isolate.

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

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -121,21 +121,20 @@ export function composePostRestoreScript(config: {
121121
payloadLimitErrorCode: config.payloadLimitErrorCode,
122122
})});`);
123123

124+
// Reset mutable state from snapshot (no-op on fresh context, resets stale
125+
// values on snapshot-restored context)
126+
parts.push(`if (typeof globalThis.__runtimeResetProcessState === "function") globalThis.__runtimeResetProcessState();`);
127+
124128
return parts.join("\n");
125129
}
126130

127131
/**
128-
* Compose the default bridge code for snapshot warm-up.
129-
* Uses timingMitigation='none' and default budget values so the snapshot
130-
* is ready for the most common session configuration.
132+
* Compose the bridge code for snapshot warm-up.
133+
* Returns only the static IIFE — the post-restore script is sent
134+
* separately per-execution so the snapshot is config-independent.
131135
*/
132136
export function composeBridgeCodeForWarmup(): string {
133-
return composeStaticBridgeCode() + "\n" + composePostRestoreScript({
134-
timingMitigation: "off",
135-
frozenTimeMs: 0,
136-
payloadLimitBytes: DEFAULT_ISOLATE_JSON_PAYLOAD_BYTES,
137-
payloadLimitErrorCode: PAYLOAD_LIMIT_ERROR_CODE,
138-
});
137+
return composeStaticBridgeCode();
139138
}
140139

141140
const MAX_ERROR_MESSAGE_CHARS = 8192;
@@ -274,12 +273,17 @@ export class NodeExecutionDriver implements RuntimeDriver {
274273
});
275274
}
276275

277-
/** Compose the full bridge code: static IIFE + per-execution post-restore script. */
278-
private composeBridgeCode(
276+
/** Compose the static bridge IIFE (no per-session config). */
277+
private composeBridgeCode(): string {
278+
return composeStaticBridgeCode();
279+
}
280+
281+
/** Compose the per-execution post-restore script. */
282+
private composePostRestore(
279283
timingMitigation: TimingMitigation,
280284
frozenTimeMs: number,
281285
): string {
282-
return composeStaticBridgeCode() + "\n" + composePostRestoreScript({
286+
return composePostRestoreScript({
283287
timingMitigation,
284288
frozenTimeMs,
285289
maxTimers: this.deps.maxTimers,
@@ -325,8 +329,9 @@ export class NodeExecutionDriver implements RuntimeDriver {
325329
},
326330
});
327331

328-
// Compose bridge code
329-
const bridgeCode = this.composeBridgeCode(timingMitigation, frozenTimeMs);
332+
// Compose bridge code and post-restore script (sent separately over IPC)
333+
const bridgeCode = this.composeBridgeCode();
334+
const postRestoreScript = this.composePostRestore(timingMitigation, frozenTimeMs);
330335

331336
// Transform user code (dynamic import → __dynamicImport)
332337
const userCode = transformDynamicImport(options.code);
@@ -370,6 +375,7 @@ export class NodeExecutionDriver implements RuntimeDriver {
370375
// Execute via V8 session
371376
const result: V8ExecutionResult = await session.execute({
372377
bridgeCode,
378+
postRestoreScript,
373379
userCode: fullUserCode,
374380
mode: options.mode,
375381
filePath: options.filePath,

packages/secure-exec-v8/src/ipc-binary.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export type BinaryFrame =
6363
mode: number;
6464
filePath: string;
6565
bridgeCode: string;
66+
postRestoreScript: string;
6667
userCode: string;
6768
}
6869
| {
@@ -166,13 +167,18 @@ export function decodeFrame(buf: Buffer): BinaryFrame {
166167
pos += 4;
167168
const bridgeCode = buf.toString("utf8", pos, pos + bcLen);
168169
pos += bcLen;
170+
const prsLen = buf.readUInt32BE(pos);
171+
pos += 4;
172+
const postRestoreScript = buf.toString("utf8", pos, pos + prsLen);
173+
pos += prsLen;
169174
const userCode = buf.toString("utf8", pos);
170175
return {
171176
type: "Execute",
172177
sessionId,
173178
mode,
174179
filePath,
175180
bridgeCode,
181+
postRestoreScript,
176182
userCode,
177183
};
178184
}
@@ -325,6 +331,12 @@ function encodeBody(frame: BinaryFrame): Buffer {
325331
bcLen.writeUInt32BE(bcBuf.length, 0);
326332
parts.push(bcLen);
327333
parts.push(bcBuf);
334+
// post_restore_script (u32 BE length prefix)
335+
const prsBuf = Buffer.from(frame.postRestoreScript, "utf8");
336+
const prsLen = Buffer.alloc(4);
337+
prsLen.writeUInt32BE(prsBuf.length, 0);
338+
parts.push(prsLen);
339+
parts.push(prsBuf);
328340
// user_code (rest of frame)
329341
parts.push(Buffer.from(frame.userCode, "utf8"));
330342
break;

packages/secure-exec-v8/src/runtime.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,7 @@ export async function createV8Runtime(
389389
type: "Execute",
390390
sessionId,
391391
bridgeCode: sendBridgeCode ? execOptions.bridgeCode : "",
392+
postRestoreScript: execOptions.postRestoreScript ?? "",
392393
userCode: execOptions.userCode,
393394
mode: execOptions.mode === "exec" ? 0 : 1,
394395
filePath: execOptions.filePath ?? "",

packages/secure-exec-v8/src/session.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ export interface V8ExecutionResult {
2323
export interface V8ExecutionOptions {
2424
/** Bridge bundle IIFE to execute before user code. */
2525
bridgeCode: string;
26+
/** Post-restore config script — runs after bridge replacement, before user code. */
27+
postRestoreScript?: string;
2628
/** User code to execute. */
2729
userCode: string;
2830
/** Execution mode: 'exec' for CJS script, 'run' for ES module. */

packages/secure-exec-v8/test/ipc-binary.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ describe("Host → Rust messages", () => {
7373
mode: 0,
7474
filePath: "",
7575
bridgeCode: "(function(){ /* bridge */ })()",
76+
postRestoreScript: "",
7677
userCode: "console.log('hello')",
7778
});
7879
});
@@ -84,6 +85,7 @@ describe("Host → Rust messages", () => {
8485
mode: 1,
8586
filePath: "/app/index.mjs",
8687
bridgeCode: "(function(){ /* bridge */ })()",
88+
postRestoreScript: "",
8789
userCode: "export default 42",
8890
});
8991
});
@@ -315,6 +317,7 @@ describe("framing validation", () => {
315317
mode: 0,
316318
filePath: "",
317319
bridgeCode: "bridge()",
320+
postRestoreScript: "",
318321
userCode: "1+1",
319322
},
320323
{ type: "DestroySession", sessionId: "a" },
@@ -379,6 +382,7 @@ describe("session_id extraction", () => {
379382
mode: 0,
380383
filePath: "",
381384
bridgeCode: "",
385+
postRestoreScript: "",
382386
userCode: "",
383387
},
384388
"sess-exec",
@@ -470,6 +474,7 @@ describe("wire format interop", () => {
470474
mode: 0,
471475
filePath: "",
472476
bridgeCode: "",
477+
postRestoreScript: "",
473478
userCode: "",
474479
},
475480
0x05,

0 commit comments

Comments
 (0)