Skip to content

Commit 3b37b1d

Browse files
NathanFlurryclaude
andcommitted
feat: US-057 - Add snapshot security and integration tests
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0d74876 commit 3b37b1d

2 files changed

Lines changed: 473 additions & 0 deletions

File tree

crates/v8-runtime/src/snapshot.rs

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,5 +312,163 @@ mod tests {
312312
"all concurrent callers should get the same cached Arc"
313313
);
314314
}
315+
316+
// --- Part 10: WASM disabled after snapshot restore ---
317+
// Verifies that set_allow_wasm_code_generation_callback is not captured
318+
// in the snapshot — disable_wasm() must be re-applied after every restore.
319+
{
320+
let bridge_code = "(function() { globalThis.__wasm_test = true; })();";
321+
let blob = create_snapshot(bridge_code).expect("snapshot creation");
322+
let mut isolate = create_isolate_from_snapshot(blob, None);
323+
324+
// Apply WASM disable (same as session.rs does after restore)
325+
crate::execution::disable_wasm(&mut isolate);
326+
327+
let scope = &mut v8::HandleScope::new(&mut isolate);
328+
let context = v8::Context::new(scope, Default::default());
329+
let scope = &mut v8::ContextScope::new(scope, context);
330+
331+
// Attempt WebAssembly.compile — should throw
332+
let wasm_test_code = r#"
333+
(function() {
334+
try {
335+
var bytes = new Uint8Array([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]);
336+
new WebAssembly.Module(bytes);
337+
return "ALLOWED";
338+
} catch (e) {
339+
return "BLOCKED:" + e.message;
340+
}
341+
})()
342+
"#;
343+
let source = v8::String::new(scope, wasm_test_code).unwrap();
344+
let script = v8::Script::compile(scope, source, None).unwrap();
345+
let result = script.run(scope).unwrap();
346+
let result_str = result.to_rust_string_lossy(scope);
347+
348+
assert!(
349+
result_str.starts_with("BLOCKED:"),
350+
"WASM should be blocked after snapshot restore + disable_wasm(), got: {}",
351+
result_str
352+
);
353+
}
354+
355+
// --- Part 11: Session isolation — fresh contexts from same snapshot ---
356+
// Verifies that state set in one session's context does not leak
357+
// to another session's context (fresh context per session).
358+
{
359+
let bridge_code = "(function() { globalThis.__shared_bridge = 'ok'; })();";
360+
let blob = create_snapshot(bridge_code).expect("snapshot creation");
361+
let blob_bytes: Vec<u8> = blob.to_vec();
362+
363+
// "Session A": set a global variable
364+
{
365+
let mut isolate = create_isolate_from_snapshot(blob_bytes.clone(), None);
366+
let scope = &mut v8::HandleScope::new(&mut isolate);
367+
let context = v8::Context::new(scope, Default::default());
368+
let scope = &mut v8::ContextScope::new(scope, context);
369+
370+
let source = v8::String::new(scope, "globalThis.__session_secret = 'session-a-data';").unwrap();
371+
let script = v8::Script::compile(scope, source, None).unwrap();
372+
script.run(scope);
373+
374+
// Verify session A can see its own data
375+
let check = v8::String::new(scope, "globalThis.__session_secret").unwrap();
376+
let script = v8::Script::compile(scope, check, None).unwrap();
377+
let result = script.run(scope).unwrap();
378+
assert_eq!(result.to_rust_string_lossy(scope), "session-a-data");
379+
}
380+
381+
// "Session B": fresh context from same snapshot should NOT see session A's data
382+
{
383+
let mut isolate = create_isolate_from_snapshot(blob_bytes.clone(), None);
384+
let scope = &mut v8::HandleScope::new(&mut isolate);
385+
let context = v8::Context::new(scope, Default::default());
386+
let scope = &mut v8::ContextScope::new(scope, context);
387+
388+
let source = v8::String::new(scope, "typeof globalThis.__session_secret").unwrap();
389+
let script = v8::Script::compile(scope, source, None).unwrap();
390+
let result = script.run(scope).unwrap();
391+
assert_eq!(
392+
result.to_rust_string_lossy(scope),
393+
"undefined",
394+
"session B should not see session A's global state"
395+
);
396+
}
397+
}
398+
399+
// --- Part 12: External references survive snapshot restore ---
400+
// Verifies that FunctionTemplates registered on a restored isolate
401+
// correctly dispatch to Rust bridge callbacks via external_refs().
402+
{
403+
use std::cell::RefCell;
404+
use crate::bridge::{
405+
register_sync_bridge_fns, register_async_bridge_fns,
406+
SessionBuffers, PendingPromises,
407+
};
408+
use crate::host_call::BridgeCallContext;
409+
410+
let bridge_code = "(function() { globalThis.__ext_ref_test = true; })();";
411+
let blob = create_snapshot(bridge_code).expect("snapshot creation");
412+
let mut isolate = create_isolate_from_snapshot(blob, None);
413+
crate::execution::disable_wasm(&mut isolate);
414+
415+
// Create minimal BridgeCallContext (sync call will fail but we
416+
// test that the FunctionTemplate dispatches without crash)
417+
let (ipc_tx, _ipc_rx) = crossbeam_channel::unbounded::<Vec<u8>>();
418+
let (cmd_tx, cmd_rx) = crossbeam_channel::unbounded::<crate::session::SessionCommand>();
419+
let call_id_router: crate::host_call::CallIdRouter =
420+
Arc::new(Mutex::new(std::collections::HashMap::new()));
421+
422+
let receiver = crate::host_call::ReaderResponseReceiver::new(
423+
Box::new(std::io::Cursor::new(Vec::<u8>::new())),
424+
);
425+
let sender = crate::host_call::ChannelFrameSender::new(ipc_tx);
426+
let bridge_ctx = BridgeCallContext::with_receiver(
427+
Box::new(sender),
428+
Box::new(receiver),
429+
"test-session".to_string(),
430+
call_id_router,
431+
);
432+
let session_buffers = RefCell::new(SessionBuffers::new());
433+
let pending = PendingPromises::new();
434+
435+
let scope = &mut v8::HandleScope::new(&mut isolate);
436+
let context = v8::Context::new(scope, Default::default());
437+
let scope = &mut v8::ContextScope::new(scope, context);
438+
439+
// Register bridge functions on the restored isolate
440+
let _sync_store = register_sync_bridge_fns(
441+
scope,
442+
&bridge_ctx as *const BridgeCallContext,
443+
&session_buffers as *const RefCell<SessionBuffers>,
444+
&["_testSync"],
445+
);
446+
let _async_store = register_async_bridge_fns(
447+
scope,
448+
&bridge_ctx as *const BridgeCallContext,
449+
&pending as *const PendingPromises,
450+
&session_buffers as *const RefCell<SessionBuffers>,
451+
&["_testAsync"],
452+
);
453+
454+
// Verify the functions exist as globals
455+
let check = v8::String::new(scope, "typeof _testSync").unwrap();
456+
let script = v8::Script::compile(scope, check, None).unwrap();
457+
let result = script.run(scope).unwrap();
458+
assert_eq!(
459+
result.to_rust_string_lossy(scope),
460+
"function",
461+
"_testSync should be a function on restored isolate"
462+
);
463+
464+
let check = v8::String::new(scope, "typeof _testAsync").unwrap();
465+
let script = v8::Script::compile(scope, check, None).unwrap();
466+
let result = script.run(scope).unwrap();
467+
assert_eq!(
468+
result.to_rust_string_lossy(scope),
469+
"function",
470+
"_testAsync should be a function on restored isolate"
471+
);
472+
}
315473
}
316474
}

0 commit comments

Comments
 (0)