1use std::collections::HashMap;
4use std::io::{Read, Write};
5use std::process::{Command, Stdio};
6use std::sync::{Arc, Condvar, Mutex};
7
8use sha2::{Digest, Sha256};
9
10use crate::bridge::{external_refs, register_stub_bridge_fns};
11use crate::isolate::init_v8_platform;
12use crate::session::{async_bridge_fns, sync_bridge_fns};
13
14const MAX_SNAPSHOT_BLOB_BYTES: usize = 50 * 1024 * 1024;
17const MAX_V8_BRIDGE_CODE_BYTES: usize = 16 * 1024 * 1024;
18const MAX_V8_USERLAND_CODE_BYTES: usize = 32 * 1024 * 1024;
22pub(crate) const V8_BRIDGE_CODE_LIMIT_ERROR_CODE: &str = "ERR_V8_BRIDGE_CODE_LIMIT";
23pub(crate) const V8_USERLAND_CODE_LIMIT_ERROR_CODE: &str = "ERR_V8_USERLAND_CODE_LIMIT";
24
25const SNAPSHOT_HELPER_ENV: &str = "AGENTOS_V8_SNAPSHOT_HELPER";
26const SNAPSHOT_HELPER_MAGIC: &[u8; 8] = b"SEV8SNP1";
27const SNAPSHOT_HELPER_OK: &[u8; 2] = b"OK";
28const SNAPSHOT_HELPER_ERR: &[u8; 3] = b"ERR";
29const SNAPSHOT_HELPER_NONE_LEN: u64 = u64::MAX;
30
31#[cfg(any(target_os = "linux", target_os = "android"))]
32#[used]
33#[link_section = ".init_array"]
34static SNAPSHOT_HELPER_CTOR: extern "C" fn() = snapshot_helper_ctor;
35
36#[cfg(target_os = "macos")]
40#[used]
41#[link_section = "__DATA,__mod_init_func"]
42static SNAPSHOT_HELPER_CTOR: extern "C" fn() = snapshot_helper_ctor;
43
44#[cfg(any(target_os = "linux", target_os = "android", target_os = "macos"))]
45extern "C" fn snapshot_helper_ctor() {
46 if std::env::var_os(SNAPSHOT_HELPER_ENV).is_some() {
47 let code = run_snapshot_helper_from_stdio();
48 std::process::exit(code);
49 }
50}
51
52pub(crate) fn validate_bridge_code_size(bridge_code: &str) -> Result<(), String> {
53 if bridge_code.len() > MAX_V8_BRIDGE_CODE_BYTES {
54 return Err(format!(
55 "{V8_BRIDGE_CODE_LIMIT_ERROR_CODE}: bridge code too large for V8 bridge setup: {} bytes (max {})",
56 bridge_code.len(),
57 MAX_V8_BRIDGE_CODE_BYTES
58 ));
59 }
60
61 Ok(())
62}
63
64pub(crate) fn validate_userland_code_size(userland_code: &str) -> Result<(), String> {
65 if userland_code.len() > MAX_V8_USERLAND_CODE_BYTES {
66 return Err(format!(
67 "{V8_USERLAND_CODE_LIMIT_ERROR_CODE}: userland snapshot code too large: {} bytes (max {})",
68 userland_code.len(),
69 MAX_V8_USERLAND_CODE_BYTES
70 ));
71 }
72
73 Ok(())
74}
75
76const SNAPSHOT_USERLAND_PREP: &str = r#"
82(function () {
83 // Agent-SDK bundles are esbuild IIFEs that expect a global CJS `require` for
84 // their node-builtin imports, but the bridge exposes require only via module
85 // wrappers. Bind one from the bridge's namespaced createRequire so the bundle's
86 // __require resolves builtins (via the in-context loadBuiltinModule) during
87 // snapshot eval; it also works post-restore (resolution flows through the real
88 // bridge fns swapped in after restore).
89 if (typeof globalThis.require === "undefined" &&
90 typeof globalThis.__secureExecGuestCreateRequire === "function") {
91 try {
92 globalThis.require = globalThis.__secureExecGuestCreateRequire("/root/index.js");
93 } catch (e) {}
94 }
95 // `process.versions` is a bridge-backed lazy getter: it derives `.node` from the
96 // per-session `config2.version` and merges a host bridge call. During snapshot
97 // creation the bridge fns are stubs AND the default `_processConfig` has no
98 // `version`, so the live getter THROWS — an agent SDK that reads it at
99 // module-init would fail. Rather than REPLACE the getter with a static value
100 // (which would permanently shadow the real per-session version for every
101 // restored session — they'd all read a frozen, fabricated identity), WRAP it:
102 // defer to the live getter so that post-restore — once the real bridge fns and
103 // per-session config are injected — sessions read accurate per-session versions,
104 // and fall back to the static, snapshot-safe identity only while the live getter
105 // throws (i.e. during snapshot creation, before any real config exists).
106 if (typeof process !== "undefined" && process) {
107 var staticVersions = { node: "20.0.0", v8: "12.0.0", uv: "1.0.0", modules: "115" };
108 try {
109 var __verDesc = Object.getOwnPropertyDescriptor(process, "versions");
110 var __liveVersions = __verDesc && __verDesc.get;
111 Object.defineProperty(process, "versions", {
112 configurable: true,
113 enumerable: true,
114 get: function () {
115 if (__liveVersions) {
116 try {
117 var v = __liveVersions.call(this);
118 // A real per-session result has a node version; during
119 // snapshot creation the live getter throws before here.
120 if (v && typeof v === "object" && v.node) return v;
121 } catch (e) {}
122 }
123 return staticVersions;
124 },
125 });
126 } catch (e) {}
127 }
128})();
129"#;
130
131fn run_snapshot_script(
135 scope: &mut v8::HandleScope,
136 code: &str,
137 label: &str,
138 eager_compile: bool,
139) -> Result<(), String> {
140 let try_catch = &mut v8::TryCatch::new(scope);
141 let source = match v8::String::new(try_catch, code) {
142 Some(source) => source,
143 None => return Err(format!("failed to create V8 string for {label}")),
144 };
145 let script = if eager_compile {
152 let resource_name = v8::String::new(try_catch, label).unwrap();
153 let origin = v8::ScriptOrigin::new(
154 try_catch,
155 resource_name.into(),
156 0,
157 0,
158 false,
159 0,
160 None,
161 false,
162 false,
163 false,
164 None,
165 );
166 let mut source = v8::script_compiler::Source::new(source, Some(&origin));
167 v8::script_compiler::compile(
168 try_catch,
169 &mut source,
170 v8::script_compiler::CompileOptions::EagerCompile,
171 v8::script_compiler::NoCacheReason::NoReason,
172 )
173 } else {
174 v8::Script::compile(try_catch, source, None)
175 };
176 let Some(script) = script else {
177 let message = try_catch
178 .exception()
179 .map(|exception| exception.to_rust_string_lossy(try_catch))
180 .unwrap_or_else(|| format!("{label} compilation failed during snapshot creation"));
181 return Err(format!(
182 "{label} compilation failed during snapshot creation: {message}"
183 ));
184 };
185 if script.run(try_catch).is_none() {
186 let message = try_catch
187 .exception()
188 .map(|exception| exception.to_rust_string_lossy(try_catch))
189 .unwrap_or_else(|| format!("{label} execution failed during snapshot creation"));
190 return Err(format!(
191 "{label} execution failed during snapshot creation: {message}"
192 ));
193 }
194 Ok(())
195}
196
197pub fn create_snapshot(bridge_code: &str) -> Result<Vec<u8>, String> {
209 create_snapshot_inner(bridge_code, None)
210}
211
212pub fn create_snapshot_with_userland(
225 bridge_code: &str,
226 userland_code: &str,
227) -> Result<Vec<u8>, String> {
228 create_snapshot_inner(bridge_code, Some(userland_code))
229}
230
231fn create_snapshot_inner(
232 bridge_code: &str,
233 userland_code: Option<&str>,
234) -> Result<Vec<u8>, String> {
235 validate_bridge_code_size(bridge_code)?;
236 if let Some(userland_code) = userland_code {
237 validate_userland_code_size(userland_code)?;
238 }
239
240 create_snapshot_in_subprocess(bridge_code, userland_code)
241}
242
243fn create_snapshot_inner_in_process(
244 bridge_code: &str,
245 userland_code: Option<&str>,
246) -> Result<Vec<u8>, String> {
247 validate_bridge_code_size(bridge_code)?;
248 if let Some(userland_code) = userland_code {
249 validate_userland_code_size(userland_code)?;
250 }
251
252 init_v8_platform();
253
254 crate::isolate::with_isolate_lifecycle_lock(|| {
255 let mut isolate = v8::Isolate::snapshot_creator(Some(external_refs()), None);
256 let bridge_result = {
257 let scope = &mut v8::HandleScope::new(&mut isolate);
258 let context = v8::Context::new(scope, Default::default());
259 let scope = &mut v8::ContextScope::new(scope, context);
260
261 let sync_bridge_fns = sync_bridge_fns();
263 let async_bridge_fns = async_bridge_fns();
264 register_stub_bridge_fns(scope, sync_bridge_fns, async_bridge_fns);
265
266 inject_snapshot_defaults(scope);
268
269 let result = (|| -> Result<(), String> {
273 run_snapshot_script(scope, bridge_code, "bridge code", false)?;
274 if let Some(userland_code) = userland_code {
275 run_snapshot_script(scope, SNAPSHOT_USERLAND_PREP, "userland prep", false)?;
281 run_snapshot_script(scope, userland_code, "userland code", true)?;
282 }
283 Ok(())
284 })();
285
286 scope.set_default_context(context);
287 result
288 };
289 let blob = isolate
290 .create_blob(v8::FunctionCodeHandling::Keep)
291 .ok_or_else(|| "V8 snapshot creation failed".to_string())?;
292 bridge_result?;
293
294 if blob.len() > MAX_SNAPSHOT_BLOB_BYTES {
296 return Err(format!(
297 "snapshot blob too large: {} bytes (max {})",
298 blob.len(),
299 MAX_SNAPSHOT_BLOB_BYTES
300 ));
301 }
302
303 Ok(blob.to_vec())
304 })
305}
306
307fn create_snapshot_in_subprocess(
308 bridge_code: &str,
309 userland_code: Option<&str>,
310) -> Result<Vec<u8>, String> {
311 let current_exe =
312 std::env::current_exe().map_err(|error| format!("snapshot helper path: {error}"))?;
313 let mut child = Command::new(current_exe)
314 .env(SNAPSHOT_HELPER_ENV, "1")
315 .stdin(Stdio::piped())
316 .stdout(Stdio::piped())
317 .stderr(Stdio::piped())
318 .spawn()
319 .map_err(|error| format!("spawn snapshot helper: {error}"))?;
320
321 {
322 let mut stdin = child
323 .stdin
324 .take()
325 .ok_or_else(|| String::from("snapshot helper stdin unavailable"))?;
326 write_snapshot_helper_request(&mut stdin, bridge_code, userland_code)?;
327 }
328
329 let output = child
330 .wait_with_output()
331 .map_err(|error| format!("wait for snapshot helper: {error}"))?;
332 if !output.status.success() {
333 let stderr = String::from_utf8_lossy(&output.stderr);
334 return Err(format!(
335 "snapshot helper exited with status {}: {}",
336 output.status, stderr
337 ));
338 }
339
340 parse_snapshot_helper_response(&output.stdout, &output.stderr)
341}
342
343fn write_snapshot_helper_request(
344 writer: &mut impl Write,
345 bridge_code: &str,
346 userland_code: Option<&str>,
347) -> Result<(), String> {
348 writer
349 .write_all(SNAPSHOT_HELPER_MAGIC)
350 .map_err(|error| format!("write snapshot helper magic: {error}"))?;
351 write_u64(writer, bridge_code.len() as u64)?;
352 write_u64(
353 writer,
354 userland_code
355 .map(|code| code.len() as u64)
356 .unwrap_or(SNAPSHOT_HELPER_NONE_LEN),
357 )?;
358 writer
359 .write_all(bridge_code.as_bytes())
360 .map_err(|error| format!("write snapshot helper bridge code: {error}"))?;
361 if let Some(userland_code) = userland_code {
362 writer
363 .write_all(userland_code.as_bytes())
364 .map_err(|error| format!("write snapshot helper userland code: {error}"))?;
365 }
366 Ok(())
367}
368
369fn parse_snapshot_helper_response(stdout: &[u8], stderr: &[u8]) -> Result<Vec<u8>, String> {
370 if stdout.starts_with(SNAPSHOT_HELPER_OK) {
371 let payload = &stdout[SNAPSHOT_HELPER_OK.len()..];
372 let (len, rest) = read_u64_from_slice(payload)?;
373 let len = usize::try_from(len)
374 .map_err(|_| String::from("snapshot helper OK payload length overflows usize"))?;
375 if rest.len() != len {
376 return Err(format!(
377 "snapshot helper OK payload length mismatch: declared {}, got {}",
378 len,
379 rest.len()
380 ));
381 }
382 return Ok(rest.to_vec());
383 }
384
385 if stdout.starts_with(SNAPSHOT_HELPER_ERR) {
386 let payload = &stdout[SNAPSHOT_HELPER_ERR.len()..];
387 let (len, rest) = read_u64_from_slice(payload)?;
388 let len = usize::try_from(len)
389 .map_err(|_| String::from("snapshot helper error length overflows usize"))?;
390 if rest.len() != len {
391 return Err(format!(
392 "snapshot helper error length mismatch: declared {}, got {}",
393 len,
394 rest.len()
395 ));
396 }
397 let message = String::from_utf8_lossy(rest);
398 return Err(message.into_owned());
399 }
400
401 Err(format!(
402 "snapshot helper returned invalid response (stdout {} bytes, stderr: {})",
403 stdout.len(),
404 String::from_utf8_lossy(stderr)
405 ))
406}
407
408fn run_snapshot_helper_from_stdio() -> i32 {
409 let mut input = Vec::new();
410 if let Err(error) = std::io::stdin().read_to_end(&mut input) {
411 let _ = write_snapshot_helper_error(format!("read snapshot helper request: {error}"));
412 return 2;
413 }
414
415 let result = (|| -> Result<Vec<u8>, String> {
416 let (bridge_code, userland_code) = parse_snapshot_helper_request(&input)?;
417 create_snapshot_inner_in_process(&bridge_code, userland_code.as_deref())
418 })();
419
420 match result {
421 Ok(blob) => match write_snapshot_helper_ok(&blob) {
422 Ok(()) => 0,
423 Err(error) => {
424 eprintln!("write snapshot helper response: {error}");
425 2
426 }
427 },
428 Err(error) => match write_snapshot_helper_error(error) {
429 Ok(()) => 0,
430 Err(error) => {
431 eprintln!("write snapshot helper error response: {error}");
432 2
433 }
434 },
435 }
436}
437
438fn parse_snapshot_helper_request(input: &[u8]) -> Result<(String, Option<String>), String> {
439 let Some(rest) = input.strip_prefix(SNAPSHOT_HELPER_MAGIC) else {
440 return Err(String::from("snapshot helper request missing magic"));
441 };
442 let (bridge_len, rest) = read_u64_from_slice(rest)?;
443 let (userland_len, rest) = read_u64_from_slice(rest)?;
444 let bridge_len = usize::try_from(bridge_len)
445 .map_err(|_| String::from("snapshot helper bridge length overflows usize"))?;
446 if rest.len() < bridge_len {
447 return Err(format!(
448 "snapshot helper bridge length mismatch: declared {}, got {}",
449 bridge_len,
450 rest.len()
451 ));
452 }
453 let (bridge_bytes, rest) = rest.split_at(bridge_len);
454 let userland_code = if userland_len == SNAPSHOT_HELPER_NONE_LEN {
455 if !rest.is_empty() {
456 return Err(format!(
457 "snapshot helper request has {} trailing bytes after bridge-only payload",
458 rest.len()
459 ));
460 }
461 None
462 } else {
463 let userland_len = usize::try_from(userland_len)
464 .map_err(|_| String::from("snapshot helper userland length overflows usize"))?;
465 if rest.len() != userland_len {
466 return Err(format!(
467 "snapshot helper userland length mismatch: declared {}, got {}",
468 userland_len,
469 rest.len()
470 ));
471 }
472 Some(
473 String::from_utf8(rest.to_vec())
474 .map_err(|error| format!("snapshot helper userland is not UTF-8: {error}"))?,
475 )
476 };
477 let bridge_code = String::from_utf8(bridge_bytes.to_vec())
478 .map_err(|error| format!("snapshot helper bridge is not UTF-8: {error}"))?;
479 Ok((bridge_code, userland_code))
480}
481
482fn write_snapshot_helper_ok(blob: &[u8]) -> std::io::Result<()> {
483 let mut stdout = std::io::stdout().lock();
484 stdout.write_all(SNAPSHOT_HELPER_OK)?;
485 write_u64_io(&mut stdout, blob.len() as u64)?;
486 stdout.write_all(blob)?;
487 stdout.flush()
488}
489
490fn write_snapshot_helper_error(message: String) -> std::io::Result<()> {
491 let mut stdout = std::io::stdout().lock();
492 stdout.write_all(SNAPSHOT_HELPER_ERR)?;
493 write_u64_io(&mut stdout, message.len() as u64)?;
494 stdout.write_all(message.as_bytes())?;
495 stdout.flush()
496}
497
498fn write_u64(writer: &mut impl Write, value: u64) -> Result<(), String> {
499 write_u64_io(writer, value).map_err(|error| format!("write snapshot helper length: {error}"))
500}
501
502fn write_u64_io(writer: &mut impl Write, value: u64) -> std::io::Result<()> {
503 writer.write_all(&value.to_le_bytes())
504}
505
506fn read_u64_from_slice(input: &[u8]) -> Result<(u64, &[u8]), String> {
507 let bytes = input
508 .get(..8)
509 .ok_or_else(|| String::from("snapshot helper payload ended before u64"))?;
510 let mut value = [0_u8; 8];
511 value.copy_from_slice(bytes);
512 Ok((u64::from_le_bytes(value), &input[8..]))
513}
514
515fn inject_snapshot_defaults(scope: &mut v8::HandleScope) {
525 let context = scope.get_current_context();
526 let global = context.global(scope);
527
528 let pc_code = r#"({
530 cwd: "/",
531 env: {},
532 timing_mitigation: "off",
533 frozen_time_ms: null,
534 high_resolution_time: false
535 })"#;
536 let pc_source = v8::String::new(scope, pc_code).unwrap();
537 let pc_script = v8::Script::compile(scope, pc_source, None).unwrap();
538 let pc_val = pc_script.run(scope).unwrap();
539 if let Some(pc_obj) = pc_val.to_object(scope) {
540 pc_obj.set_integrity_level(scope, v8::IntegrityLevel::Frozen);
541 }
542 let pc_key = v8::String::new(scope, "_processConfig").unwrap();
543 let attr = v8::PropertyAttribute::READ_ONLY;
546 global.define_own_property(scope, pc_key.into(), pc_val, attr);
547
548 let oc_code = r#"({
550 homedir: "/root",
551 tmpdir: "/tmp",
552 platform: "linux",
553 arch: "x64"
554 })"#;
555 let oc_source = v8::String::new(scope, oc_code).unwrap();
556 let oc_script = v8::Script::compile(scope, oc_source, None).unwrap();
557 let oc_val = oc_script.run(scope).unwrap();
558 if let Some(oc_obj) = oc_val.to_object(scope) {
559 oc_obj.set_integrity_level(scope, v8::IntegrityLevel::Frozen);
560 }
561 let oc_key = v8::String::new(scope, "_osConfig").unwrap();
562 let attr2 = v8::PropertyAttribute::READ_ONLY;
564 global.define_own_property(scope, oc_key.into(), oc_val, attr2);
565}
566
567pub fn create_isolate_from_snapshot<B>(blob: B, heap_limit_mb: Option<u32>) -> v8::OwnedIsolate
576where
577 B: std::ops::Deref<Target = [u8]> + std::borrow::Borrow<[u8]> + 'static,
578{
579 init_v8_platform();
580
581 let limit = heap_limit_mb.unwrap_or(crate::isolate::DEFAULT_HEAP_LIMIT_MB);
584 let limit_bytes = (limit as usize) * 1024 * 1024;
585 let params = v8::CreateParams::default()
586 .snapshot_blob(blob)
587 .external_references(&**external_refs())
588 .heap_limits(0, limit_bytes);
589 let mut isolate = crate::isolate::with_isolate_lifecycle_lock(|| v8::Isolate::new(params));
590 crate::isolate::configure_isolate(&mut isolate);
591 crate::isolate::install_heap_limit_guard(&mut isolate);
594 isolate
595}
596
597pub type SnapshotCacheKey = [u8; 32];
598
599pub struct SnapshotCache {
606 inner: Mutex<CacheInner>,
607 max_entries: usize,
608}
609
610struct CacheInner {
611 entries: Vec<CacheEntry>,
612 in_flight: HashMap<SnapshotCacheKey, Arc<InFlightEntry>>,
615}
616
617struct CacheEntry {
618 key: SnapshotCacheKey,
619 blob: Arc<Vec<u8>>,
623}
624
625struct InFlightEntry {
628 result: Mutex<Option<Result<Arc<Vec<u8>>, String>>>,
629 done: Condvar,
630}
631
632impl SnapshotCache {
633 pub fn new(max_entries: usize) -> Self {
634 SnapshotCache {
635 inner: Mutex::new(CacheInner {
636 entries: Vec::new(),
637 in_flight: HashMap::new(),
638 }),
639 max_entries,
640 }
641 }
642
643 pub fn get_or_create(&self, bridge_code: &str) -> Result<Arc<Vec<u8>>, String> {
649 self.get_or_create_with_userland(bridge_code, None)
650 }
651
652 pub fn try_get_with_userland(
655 &self,
656 bridge_code: &str,
657 userland_code: Option<&str>,
658 ) -> Option<Arc<Vec<u8>>> {
659 let key = snapshot_cache_key(bridge_code, userland_code);
660 let mut inner = self.inner.lock().unwrap();
661 if let Some(pos) = inner.entries.iter().position(|e| e.key == key) {
662 let entry = inner.entries.remove(pos);
663 let blob = Arc::clone(&entry.blob);
664 inner.entries.push(entry);
665 Some(blob)
666 } else {
667 None
668 }
669 }
670
671 pub fn get_or_create_with_userland(
676 &self,
677 bridge_code: &str,
678 userland_code: Option<&str>,
679 ) -> Result<Arc<Vec<u8>>, String> {
680 let key = snapshot_cache_key(bridge_code, userland_code);
681
682 let in_flight = {
684 let mut inner = self.inner.lock().unwrap();
685
686 if let Some(pos) = inner.entries.iter().position(|e| e.key == key) {
688 let entry = inner.entries.remove(pos);
689 let blob = Arc::clone(&entry.blob);
690 inner.entries.push(entry);
691 return Ok(blob);
692 }
693
694 if let Some(entry) = inner.in_flight.get(&key) {
696 Some(Arc::clone(entry))
697 } else {
698 let entry = Arc::new(InFlightEntry {
700 result: Mutex::new(None),
701 done: Condvar::new(),
702 });
703 inner.in_flight.insert(key, Arc::clone(&entry));
704 None
705 }
706 };
707
708 if let Some(entry) = in_flight {
710 let mut result = entry.result.lock().unwrap();
711 while result.is_none() {
712 result = entry.done.wait(result).unwrap();
713 }
714 return result.as_ref().unwrap().clone();
715 }
716
717 let creation_result = create_snapshot_inner(bridge_code, userland_code).map(Arc::new);
719
720 {
722 let mut inner = self.inner.lock().unwrap();
723
724 if let Ok(ref arc) = creation_result {
725 if inner.entries.len() >= self.max_entries {
727 inner.entries.remove(0);
728 }
729 inner.entries.push(CacheEntry {
730 key,
731 blob: Arc::clone(arc),
732 });
733 }
734
735 if let Some(entry) = inner.in_flight.remove(&key) {
737 let mut result = entry.result.lock().unwrap();
738 *result = Some(creation_result.clone());
739 entry.done.notify_all();
740 }
741 }
742
743 creation_result
744 }
745}
746
747pub fn snapshot_cache_key(bridge_code: &str, userland_code: Option<&str>) -> SnapshotCacheKey {
751 match userland_code {
752 None => {
753 let mut hasher = Sha256::new();
754 hasher.update(bridge_code.as_bytes());
755 hasher.finalize().into()
756 }
757 Some(userland_code) => {
758 let mut buf = Vec::with_capacity(bridge_code.len() + 1 + userland_code.len());
759 buf.extend_from_slice(bridge_code.as_bytes());
760 buf.push(0);
761 buf.extend_from_slice(userland_code.as_bytes());
762 let mut hasher = Sha256::new();
763 hasher.update(&buf);
764 hasher.finalize().into()
765 }
766 }
767}
768
769#[doc(hidden)]
770pub fn run_snapshot_consolidated_checks() {
771 fn eval(isolate: &mut v8::OwnedIsolate, code: &str) -> String {
772 let scope = &mut v8::HandleScope::new(isolate);
773 let context = v8::Context::new(scope, Default::default());
774 let scope = &mut v8::ContextScope::new(scope, context);
775 let source = v8::String::new(scope, code).unwrap();
776 let script = v8::Script::compile(scope, source, None).unwrap();
777 let result = script.run(scope).unwrap();
778 result.to_rust_string_lossy(scope)
779 }
780
781 init_v8_platform();
785 let _ = external_refs();
786
787 {
789 let bridge_code = "(function() { globalThis.__bridge_init = true; })();";
790 let blob = create_snapshot(bridge_code).expect("snapshot creation should succeed");
791 assert!(!blob.is_empty(), "snapshot blob should be non-empty");
792 }
793
794 {
796 let bridge_code = "(function() { globalThis.__testValue = 42; })();";
797 let blob = create_snapshot(bridge_code).expect("snapshot creation should succeed");
798 let mut isolate = create_isolate_from_snapshot(blob, None);
799 assert_eq!(eval(&mut isolate, "1 + 1"), "2");
802 }
803
804 {
806 let bridge_code = "/* empty bridge */";
807 let blob = create_snapshot(bridge_code).expect("snapshot creation should succeed");
808 let mut isolate = create_isolate_from_snapshot(blob, Some(8));
809 assert_eq!(eval(&mut isolate, "'heap ok'"), "heap ok");
810 }
811
812 {
814 let bridge_code = "(function() { globalThis.x = 1; })();";
815 let blob = create_snapshot(bridge_code).expect("snapshot creation should succeed");
816 assert!(
817 blob.len() < MAX_SNAPSHOT_BLOB_BYTES,
818 "normal bridge code should produce blob under 50MB limit"
819 );
820 }
821
822 {
824 let bridge_code = "(function() { globalThis.__counter = 0; })();";
825 let blob = create_snapshot(bridge_code).expect("snapshot creation should succeed");
826 let blob_bytes: Vec<u8> = blob.to_vec();
827
828 for i in 0..3 {
829 let mut isolate = create_isolate_from_snapshot(blob_bytes.clone(), None);
830 let result = eval(&mut isolate, &format!("{} + 1", i));
831 assert_eq!(result, format!("{}", i + 1));
832 }
833 }
834
835 {
837 let cache = SnapshotCache::new(4);
838 let bridge_code = "(function() { globalThis.__cached = 1; })();";
839
840 let arc1 = cache
841 .get_or_create(bridge_code)
842 .expect("first get_or_create");
843 let arc2 = cache
844 .get_or_create(bridge_code)
845 .expect("second get_or_create");
846
847 assert!(
849 Arc::ptr_eq(&arc1, &arc2),
850 "cache hit should return same Arc"
851 );
852 }
853
854 {
856 let cache = SnapshotCache::new(4);
857 let code_a = "(function() { globalThis.__a = 1; })();";
858 let code_b = "(function() { globalThis.__b = 2; })();";
859
860 let arc_a = cache.get_or_create(code_a).expect("create A");
861 let arc_b = cache.get_or_create(code_b).expect("create B");
862
863 assert!(
865 !Arc::ptr_eq(&arc_a, &arc_b),
866 "different code should produce different Arc"
867 );
868
869 let mut iso_a = create_isolate_from_snapshot((*arc_a).clone(), None);
871 assert_eq!(eval(&mut iso_a, "1 + 1"), "2");
872
873 let mut iso_b = create_isolate_from_snapshot((*arc_b).clone(), None);
874 assert_eq!(eval(&mut iso_b, "2 + 2"), "4");
875 }
876
877 {
879 let cache = SnapshotCache::new(2);
880 let code_1 = "(function() { globalThis.__v1 = 1; })();";
881 let code_2 = "(function() { globalThis.__v2 = 2; })();";
882 let code_3 = "(function() { globalThis.__v3 = 3; })();";
883
884 let arc_1 = cache.get_or_create(code_1).expect("create 1");
885 let _arc_2 = cache.get_or_create(code_2).expect("create 2");
886
887 let _arc_3 = cache.get_or_create(code_3).expect("create 3");
889
890 let arc_1_new = cache.get_or_create(code_1).expect("re-create 1");
892 assert!(
893 !Arc::ptr_eq(&arc_1, &arc_1_new),
894 "evicted entry should produce a new Arc on re-creation"
895 );
896
897 }
902
903 {
905 use std::sync::atomic::{AtomicUsize, Ordering};
906
907 let cache = Arc::new(SnapshotCache::new(4));
908 let bridge_code = "(function() { globalThis.__concurrent = 1; })();";
909
910 let first = cache.get_or_create(bridge_code).expect("pre-warm");
913
914 let num_threads = 4;
915 let barrier = Arc::new(std::sync::Barrier::new(num_threads));
916 let same_count = Arc::new(AtomicUsize::new(0));
917
918 let mut handles = vec![];
919 for _ in 0..num_threads {
920 let cache = Arc::clone(&cache);
921 let barrier = Arc::clone(&barrier);
922 let first = Arc::clone(&first);
923 let same_count = Arc::clone(&same_count);
924 let code = bridge_code.to_string();
925
926 handles.push(std::thread::spawn(move || {
927 barrier.wait();
928 let arc = cache.get_or_create(&code).expect("concurrent get");
929 if Arc::ptr_eq(&arc, &first) {
930 same_count.fetch_add(1, Ordering::Relaxed);
931 }
932 }));
933 }
934
935 for h in handles {
936 h.join().expect("thread join");
937 }
938
939 assert_eq!(
940 same_count.load(Ordering::Relaxed),
941 num_threads,
942 "all concurrent callers should get the same cached Arc"
943 );
944 }
945
946 {
948 let bridge_code = "(function() { globalThis.__wasm_test = true; })();";
949 let blob = create_snapshot(bridge_code).expect("snapshot creation");
950 let mut isolate = create_isolate_from_snapshot(blob, None);
951
952 let scope = &mut v8::HandleScope::new(&mut isolate);
953 let context = v8::Context::new(scope, Default::default());
954 let scope = &mut v8::ContextScope::new(scope, context);
955
956 let wasm_test_code = r#"
957 (function() {
958 var bytes = new Uint8Array([
959 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
960 0x01, 0x07, 0x01, 0x60, 0x02, 0x7f, 0x7f, 0x01, 0x7f,
961 0x03, 0x02, 0x01, 0x00,
962 0x07, 0x07, 0x01, 0x03, 0x61, 0x64, 0x64, 0x00, 0x00,
963 0x0a, 0x09, 0x01, 0x07, 0x00, 0x20, 0x00, 0x20, 0x01, 0x6a, 0x0b,
964 ]);
965 var module = new WebAssembly.Module(bytes);
966 var instance = new WebAssembly.Instance(module, {});
967 return String(instance.exports.add(2, 3));
968 })()
969 "#;
970 let source = v8::String::new(scope, wasm_test_code).unwrap();
971 let script = v8::Script::compile(scope, source, None).unwrap();
972 let result = script.run(scope).unwrap();
973 let result_str = result.to_rust_string_lossy(scope);
974
975 assert_eq!(
976 result_str, "5",
977 "WASM should remain enabled after snapshot restore"
978 );
979 }
980
981 {
985 let bridge_code = "(function() { globalThis.__shared_bridge = 'ok'; })();";
986 let blob = create_snapshot(bridge_code).expect("snapshot creation");
987 let blob_bytes: Vec<u8> = blob.to_vec();
988
989 {
991 let mut isolate = create_isolate_from_snapshot(blob_bytes.clone(), None);
992 let scope = &mut v8::HandleScope::new(&mut isolate);
993 let context = v8::Context::new(scope, Default::default());
994 let scope = &mut v8::ContextScope::new(scope, context);
995
996 let source =
997 v8::String::new(scope, "globalThis.__session_secret = 'session-a-data';").unwrap();
998 let script = v8::Script::compile(scope, source, None).unwrap();
999 script.run(scope);
1000
1001 let check = v8::String::new(scope, "globalThis.__session_secret").unwrap();
1003 let script = v8::Script::compile(scope, check, None).unwrap();
1004 let result = script.run(scope).unwrap();
1005 assert_eq!(result.to_rust_string_lossy(scope), "session-a-data");
1006 }
1007
1008 {
1010 let mut isolate = create_isolate_from_snapshot(blob_bytes.clone(), None);
1011 let scope = &mut v8::HandleScope::new(&mut isolate);
1012 let context = v8::Context::new(scope, Default::default());
1013 let scope = &mut v8::ContextScope::new(scope, context);
1014
1015 let source = v8::String::new(scope, "typeof globalThis.__session_secret").unwrap();
1016 let script = v8::Script::compile(scope, source, None).unwrap();
1017 let result = script.run(scope).unwrap();
1018 assert_eq!(
1019 result.to_rust_string_lossy(scope),
1020 "undefined",
1021 "session B should not see session A's global state"
1022 );
1023 }
1024 }
1025
1026 {
1030 use crate::bridge::{register_async_bridge_fns, register_sync_bridge_fns, PendingPromises};
1031 use crate::host_call::BridgeCallContext;
1032
1033 let bridge_code = "(function() { globalThis.__ext_ref_test = true; })();";
1034 let blob = create_snapshot(bridge_code).expect("snapshot creation");
1035 let mut isolate = create_isolate_from_snapshot(blob, None);
1036
1037 let (event_tx, _event_rx) =
1040 crossbeam_channel::unbounded::<crate::session::RuntimeEventEnvelope>();
1041 let (_cmd_tx, _cmd_rx) = crossbeam_channel::unbounded::<crate::session::SessionCommand>();
1042 let call_id_router: crate::host_call::CallIdRouter =
1043 Arc::new(Mutex::new(std::collections::HashMap::new()));
1044
1045 let receiver = crate::host_call::ReaderBridgeResponseReceiver::new(Box::new(
1046 std::io::Cursor::new(Vec::<u8>::new()),
1047 ));
1048 let sender = crate::host_call::ChannelRuntimeEventSender::new(event_tx, None);
1049 let bridge_ctx = BridgeCallContext::with_receiver(
1050 Box::new(sender),
1051 Box::new(receiver),
1052 "test-session".to_string(),
1053 call_id_router,
1054 Arc::new(std::sync::atomic::AtomicU64::new(1)),
1055 );
1056 let pending = PendingPromises::new();
1057
1058 let scope = &mut v8::HandleScope::new(&mut isolate);
1059 let context = v8::Context::new(scope, Default::default());
1060 let scope = &mut v8::ContextScope::new(scope, context);
1061
1062 let _sync_store = register_sync_bridge_fns(
1064 scope,
1065 &bridge_ctx as *const BridgeCallContext,
1066 &["_testSync"],
1067 );
1068 let _async_store = register_async_bridge_fns(
1069 scope,
1070 &bridge_ctx as *const BridgeCallContext,
1071 &pending as *const PendingPromises,
1072 &["_testAsync"],
1073 );
1074
1075 let check = v8::String::new(scope, "typeof _testSync").unwrap();
1077 let script = v8::Script::compile(scope, check, None).unwrap();
1078 let result = script.run(scope).unwrap();
1079 assert_eq!(
1080 result.to_rust_string_lossy(scope),
1081 "function",
1082 "_testSync should be a function on restored isolate"
1083 );
1084
1085 let check = v8::String::new(scope, "typeof _testAsync").unwrap();
1086 let script = v8::Script::compile(scope, check, None).unwrap();
1087 let result = script.run(scope).unwrap();
1088 assert_eq!(
1089 result.to_rust_string_lossy(scope),
1090 "function",
1091 "_testAsync should be a function on restored isolate"
1092 );
1093 }
1094
1095 {
1099 use crate::bridge::register_stub_bridge_fns;
1100
1101 let bridge_code = "/* stub test */";
1103 let blob = create_snapshot(bridge_code).expect("snapshot creation");
1104 let mut isolate = create_isolate_from_snapshot(blob, None);
1105
1106 let scope = &mut v8::HandleScope::new(&mut isolate);
1107 let context = v8::Context::new(scope, Default::default());
1108 let scope = &mut v8::ContextScope::new(scope, context);
1109
1110 register_stub_bridge_fns(
1111 scope,
1112 &["_log", "_error", "_fsReadFile", "_loadPolyfill"],
1113 &["_scheduleTimer", "_dynamicImport"],
1114 );
1115
1116 let check = v8::String::new(
1117 scope,
1118 r#"
1119 (function() {
1120 var names = ['_log', '_error', '_fsReadFile', '_loadPolyfill',
1121 '_scheduleTimer', '_dynamicImport'];
1122 for (var i = 0; i < names.length; i++) {
1123 if (typeof globalThis[names[i]] !== 'function') {
1124 return 'FAIL: ' + names[i] + ' is ' + typeof globalThis[names[i]];
1125 }
1126 }
1127 return 'OK';
1128 })()
1129 "#,
1130 )
1131 .unwrap();
1132 let script = v8::Script::compile(scope, check, None).unwrap();
1133 let result = script.run(scope).unwrap();
1134 assert_eq!(
1135 result.to_rust_string_lossy(scope),
1136 "OK",
1137 "all stub bridge functions should be registered as functions"
1138 );
1139 }
1140
1141 {
1146 use crate::bridge::register_stub_bridge_fns;
1147
1148 let mut snapshot_isolate = v8::Isolate::snapshot_creator(Some(external_refs()), None);
1149 {
1150 let scope = &mut v8::HandleScope::new(&mut snapshot_isolate);
1151 let context = v8::Context::new(scope, Default::default());
1152 let scope = &mut v8::ContextScope::new(scope, context);
1153
1154 let sync_bridge_fns = sync_bridge_fns();
1156 let async_bridge_fns = async_bridge_fns();
1157 register_stub_bridge_fns(scope, sync_bridge_fns, async_bridge_fns);
1158
1159 let iife_code = r#"
1162 (function() {
1163 // Verify bridge functions exist (like ivm-compat shim)
1164 var syncKeys = ['_log', '_error', '_resolveModule', '_loadFile', '_moduleFormat',
1165 '_cryptoRandomFill', '_fsReadFile', '_fsWriteFile',
1166 '_childProcessSpawnStart', '_childProcessPoll', '_childProcessSpawnSync'];
1167 var asyncKeys = ['_dynamicImport', '_scheduleTimer',
1168 '_networkHttpServerListenRaw'];
1169
1170 for (var i = 0; i < syncKeys.length; i++) {
1171 if (typeof globalThis[syncKeys[i]] !== 'function') {
1172 throw new Error('Missing sync: ' + syncKeys[i]);
1173 }
1174 }
1175 for (var i = 0; i < asyncKeys.length; i++) {
1176 if (typeof globalThis[asyncKeys[i]] !== 'function') {
1177 throw new Error('Missing async: ' + asyncKeys[i]);
1178 }
1179 }
1180
1181 // Simulate getter-based fs facade (setup only, no calls)
1182 var _fs = {};
1183 Object.defineProperties(_fs, {
1184 readFile: { get: function() { return globalThis._fsReadFile; }, enumerable: true },
1185 writeFile: { get: function() { return globalThis._fsWriteFile; }, enumerable: true },
1186 });
1187 globalThis._fs = _fs;
1188
1189 // Verify getter returns function reference without calling it
1190 if (typeof _fs.readFile !== 'function') {
1191 throw new Error('Getter should return function, got ' + typeof _fs.readFile);
1192 }
1193
1194 // Simulate closure wrapping (setup only, no calls)
1195 globalThis.__wrappedLog = function() {
1196 return globalThis._log.apply(null, arguments);
1197 };
1198
1199 globalThis.__bridge_setup_complete = true;
1200 })();
1201 "#;
1202 let source = v8::String::new(scope, iife_code).unwrap();
1203 let script = v8::Script::compile(scope, source, None).unwrap();
1204 let result = script.run(scope);
1205 assert!(
1206 result.is_some(),
1207 "bridge IIFE should execute without error against stub functions"
1208 );
1209
1210 let check =
1212 v8::String::new(scope, "String(globalThis.__bridge_setup_complete)").unwrap();
1213 let script = v8::Script::compile(scope, check, None).unwrap();
1214 let val = script.run(scope).unwrap();
1215 assert_eq!(
1216 val.to_rust_string_lossy(scope),
1217 "true",
1218 "bridge setup should complete with stub functions"
1219 );
1220
1221 scope.set_default_context(context);
1222 }
1223
1224 let blob = snapshot_isolate.create_blob(v8::FunctionCodeHandling::Keep);
1225 assert!(
1226 blob.is_some(),
1227 "snapshot creation should succeed with stub bridge functions"
1228 );
1229 assert!(
1230 !blob.unwrap().is_empty(),
1231 "snapshot blob should be non-empty"
1232 );
1233 }
1234
1235 {
1239 let iife_code = r#"
1241 (function() {
1242 // Verify all sync bridge functions are registered as stubs
1243 var syncFns = ['_log', '_error', '_resolveModule', '_loadFile',
1244 '_moduleFormat', '_loadPolyfill', '_cryptoRandomFill', '_cryptoRandomUUID',
1245 '_fsReadFile', '_fsWriteFile', '_fsReadFileBinary',
1246 '_fsWriteFileBinary', '_fsReadDir', '_fsMkdir', '_fsRmdir',
1247 '_fsExists', '_fsStat', '_fsUnlink', '_fsRename', '_fsChmod',
1248 '_fsChown', '_fsLink', '_fsSymlink', '_fsReadlink', '_fsLstat',
1249 '_fsTruncate', '_fsUtimes', '_childProcessSpawnStart',
1250 '_childProcessPoll', '_childProcessStdinWrite', '_childProcessStdinClose',
1251 '_childProcessKill', '_childProcessSpawnSync'];
1252 for (var i = 0; i < syncFns.length; i++) {
1253 if (typeof globalThis[syncFns[i]] !== 'function') {
1254 throw new Error('Missing sync stub: ' + syncFns[i] +
1255 ' (typeof=' + typeof globalThis[syncFns[i]] + ')');
1256 }
1257 }
1258
1259 // Verify all async bridge functions are registered as stubs
1260 var asyncFns = ['_dynamicImport', '_scheduleTimer',
1261 '_networkDnsLookupRaw',
1262 '_networkDnsResolveRaw',
1263 '_networkHttpServerListenRaw',
1264 '_networkHttpServerCloseRaw', '_networkHttpServerWaitRaw',
1265 '_networkHttp2ServerWaitRaw', '_networkHttp2SessionWaitRaw'];
1266 for (var i = 0; i < asyncFns.length; i++) {
1267 if (typeof globalThis[asyncFns[i]] !== 'function') {
1268 throw new Error('Missing async stub: ' + asyncFns[i] +
1269 ' (typeof=' + typeof globalThis[asyncFns[i]] + ')');
1270 }
1271 }
1272
1273 // Verify _processConfig default was injected
1274 if (typeof _processConfig !== 'object' || _processConfig === null) {
1275 throw new Error('_processConfig not injected: ' + typeof _processConfig);
1276 }
1277 if (_processConfig.cwd !== '/') {
1278 throw new Error('_processConfig.cwd should be "/", got: ' + _processConfig.cwd);
1279 }
1280
1281 // Verify _osConfig default was injected
1282 if (typeof _osConfig !== 'object' || _osConfig === null) {
1283 throw new Error('_osConfig not injected: ' + typeof _osConfig);
1284 }
1285 if (_osConfig.platform !== 'linux') {
1286 throw new Error('_osConfig.platform should be "linux", got: ' + _osConfig.platform);
1287 }
1288
1289 globalThis.__part15_ok = true;
1290 })();
1291 "#;
1292 let blob = create_snapshot(iife_code).expect(
1293 "create_snapshot should succeed with bridge code that checks stubs and defaults",
1294 );
1295 assert!(!blob.is_empty(), "snapshot blob should be non-empty");
1296
1297 let mut isolate = create_isolate_from_snapshot(blob, None);
1299 assert_eq!(eval(&mut isolate, "1 + 1"), "2");
1300 }
1301
1302 {
1307 let iife_code = r#"
1308 (function() {
1309 // Set up getter-based fs facade referencing bridge stubs
1310 var _fs = {};
1311 Object.defineProperties(_fs, {
1312 readFile: { get: function() { return globalThis._fsReadFile; }, enumerable: true },
1313 writeFile: { get: function() { return globalThis._fsWriteFile; }, enumerable: true },
1314 });
1315 globalThis._fs = _fs;
1316
1317 // Set up closure wrapping a bridge stub
1318 globalThis.myLog = function() {
1319 return globalThis._log.apply(null, arguments);
1320 };
1321
1322 // Set up a require-like function (doesn't call _loadPolyfill at setup)
1323 globalThis.require = function(name) {
1324 return globalThis._loadPolyfill(name);
1325 };
1326
1327 // Set up a console-like object
1328 globalThis.console = {
1329 log: function() { globalThis._log.apply(null, arguments); },
1330 error: function() { globalThis._error.apply(null, arguments); },
1331 };
1332
1333 // Read _processConfig at setup time (like process.cwd initialization)
1334 globalThis.__initialCwd = _processConfig.cwd;
1335
1336 globalThis.__part16_setup = true;
1337 })();
1338 "#;
1339 let blob = create_snapshot(iife_code)
1340 .expect("create_snapshot should succeed with full bridge IIFE pattern");
1341 assert!(!blob.is_empty());
1342
1343 let blob_bytes: Vec<u8> = blob.to_vec();
1345 let mut isolate = create_isolate_from_snapshot(blob_bytes, None);
1346 let scope = &mut v8::HandleScope::new(&mut isolate);
1347 let context = v8::Context::new(scope, Default::default());
1348 let scope = &mut v8::ContextScope::new(scope, context);
1349
1350 let check_code = r#"
1352 (function() {
1353 var results = [];
1354 results.push('_fs=' + (typeof _fs === 'object'));
1355 results.push('_fs.readFile=' + (typeof _fs.readFile === 'function'));
1356 results.push('myLog=' + (typeof myLog === 'function'));
1357 results.push('require=' + (typeof require === 'function'));
1358 results.push('console.log=' + (typeof console.log === 'function'));
1359 results.push('console.error=' + (typeof console.error === 'function'));
1360 results.push('__initialCwd=' + __initialCwd);
1361 results.push('__part16_setup=' + __part16_setup);
1362 return results.join(';');
1363 })()
1364 "#;
1365 let source = v8::String::new(scope, check_code).unwrap();
1366 let script = v8::Script::compile(scope, source, None).unwrap();
1367 let result = script.run(scope).unwrap();
1368 let result_str = result.to_rust_string_lossy(scope);
1369
1370 assert_eq!(
1371 result_str,
1372 "_fs=true;_fs.readFile=true;myLog=true;require=true;console.log=true;console.error=true;__initialCwd=/;__part16_setup=true",
1373 "restored context should have all bridge infrastructure from the IIFE"
1374 );
1375 }
1376
1377 {
1380 let cache = SnapshotCache::new(4);
1381 let code = r#"
1382 (function() {
1383 // Verify stubs are present (create_snapshot registers them)
1384 if (typeof _log !== 'function') throw new Error('no _log stub');
1385 if (typeof _processConfig !== 'object') throw new Error('no _processConfig');
1386 globalThis.__cached_context = true;
1387 })();
1388 "#;
1389
1390 let arc1 = cache.get_or_create(code).expect("first get_or_create");
1391 let arc2 = cache.get_or_create(code).expect("second get_or_create");
1392 assert!(
1393 Arc::ptr_eq(&arc1, &arc2),
1394 "cache hit should return same Arc"
1395 );
1396
1397 let mut isolate = create_isolate_from_snapshot((*arc1).clone(), None);
1399 assert_eq!(eval(&mut isolate, "1 + 1"), "2");
1400 }
1401
1402 {
1407 use crate::bridge::{replace_bridge_fns, PendingPromises};
1408 use crate::host_call::BridgeCallContext;
1409
1410 let bridge_code = r#"
1412 (function() {
1413 // Getter-based facade referencing globalThis._fsReadFile
1414 var _fs = {};
1415 Object.defineProperties(_fs, {
1416 readFile: { get: function() { return globalThis._fsReadFile; }, enumerable: true },
1417 });
1418 globalThis._fs = _fs;
1419 globalThis.__bridge_ready = true;
1420 })();
1421 "#;
1422 let blob = create_snapshot(bridge_code).expect("snapshot creation");
1423 let mut isolate = create_isolate_from_snapshot(blob, None);
1424
1425 let (event_tx, _event_rx) =
1427 crossbeam_channel::unbounded::<crate::session::RuntimeEventEnvelope>();
1428 let call_id_router: crate::host_call::CallIdRouter =
1429 Arc::new(Mutex::new(std::collections::HashMap::new()));
1430 let receiver = crate::host_call::ReaderBridgeResponseReceiver::new(Box::new(
1431 std::io::Cursor::new(Vec::<u8>::new()),
1432 ));
1433 let sender = crate::host_call::ChannelRuntimeEventSender::new(event_tx, None);
1434 let bridge_ctx = BridgeCallContext::with_receiver(
1435 Box::new(sender),
1436 Box::new(receiver),
1437 "test-session".to_string(),
1438 call_id_router,
1439 Arc::new(std::sync::atomic::AtomicU64::new(1)),
1440 );
1441 let pending = PendingPromises::new();
1442
1443 let scope = &mut v8::HandleScope::new(&mut isolate);
1445 let context = v8::Context::new(scope, Default::default());
1446 let scope = &mut v8::ContextScope::new(scope, context);
1447
1448 let (_sync_store, _async_store) = replace_bridge_fns(
1449 scope,
1450 &bridge_ctx as *const BridgeCallContext,
1451 &pending as *const PendingPromises,
1452 &["_log", "_fsReadFile"],
1453 &["_scheduleTimer"],
1454 );
1455
1456 let check = v8::String::new(
1458 scope,
1459 r#"
1460 (function() {
1461 var results = [];
1462 results.push('__bridge_ready=' + globalThis.__bridge_ready);
1463 results.push('_fs_exists=' + (typeof _fs === 'object'));
1464 // Getter should resolve to the REPLACED function (not stub)
1465 results.push('_fs.readFile_type=' + typeof _fs.readFile);
1466 // Direct global should also be the replaced function
1467 results.push('_log_type=' + typeof _log);
1468 results.push('_scheduleTimer_type=' + typeof _scheduleTimer);
1469 return results.join(';');
1470 })()
1471 "#,
1472 )
1473 .unwrap();
1474 let script = v8::Script::compile(scope, check, None).unwrap();
1475 let result = script.run(scope).unwrap();
1476 assert_eq!(
1477 result.to_rust_string_lossy(scope),
1478 "__bridge_ready=true;_fs_exists=true;_fs.readFile_type=function;_log_type=function;_scheduleTimer_type=function",
1479 "restored context should have bridge IIFE state + replaced functions"
1480 );
1481 }
1482
1483 {
1487 use crate::bridge::serialize_v8_value;
1488
1489 let bridge_code = r#"
1490 (function() {
1491 // Verify default _processConfig from snapshot
1492 globalThis.__snapshotCwd = _processConfig.cwd;
1493 })();
1494 "#;
1495 let blob = create_snapshot(bridge_code).expect("snapshot creation");
1496 let mut isolate = create_isolate_from_snapshot(blob, None);
1497
1498 let scope = &mut v8::HandleScope::new(&mut isolate);
1499 let context = v8::Context::new(scope, Default::default());
1500 let scope = &mut v8::ContextScope::new(scope, context);
1501
1502 let check = v8::String::new(scope, "__snapshotCwd").unwrap();
1504 let script = v8::Script::compile(scope, check, None).unwrap();
1505 let result = script.run(scope).unwrap();
1506 assert_eq!(result.to_rust_string_lossy(scope), "/");
1507
1508 let payload_code = r#"({
1510 processConfig: { cwd: "/app", env: { FOO: "bar" }, timing_mitigation: "off", frozen_time_ms: null },
1511 osConfig: { homedir: "/home/agentos", tmpdir: "/tmp", platform: "linux", arch: "arm64" }
1512 })"#;
1513 let payload_source = v8::String::new(scope, payload_code).unwrap();
1514 let payload_script = v8::Script::compile(scope, payload_source, None).unwrap();
1515 let payload_val = payload_script.run(scope).unwrap();
1516 let payload_bytes = serialize_v8_value(scope, payload_val).expect("serialize payload");
1517
1518 crate::execution::inject_globals_from_payload(scope, &payload_bytes)
1520 .expect("inject globals payload");
1521
1522 let check = v8::String::new(scope, "_processConfig.cwd").unwrap();
1524 let script = v8::Script::compile(scope, check, None).unwrap();
1525 let result = script.run(scope).unwrap();
1526 assert_eq!(
1527 result.to_rust_string_lossy(scope),
1528 "/app",
1529 "_processConfig.cwd should be overridden from '/' to '/app'"
1530 );
1531
1532 let check = v8::String::new(scope, "_osConfig.arch").unwrap();
1534 let script = v8::Script::compile(scope, check, None).unwrap();
1535 let result = script.run(scope).unwrap();
1536 assert_eq!(
1537 result.to_rust_string_lossy(scope),
1538 "arm64",
1539 "_osConfig.arch should be overridden to 'arm64'"
1540 );
1541 }
1542
1543 {
1545 let bridge_code = r#"
1546 (function() {
1547 globalThis.__snapshotFn = async function () { return "ok"; };
1548 })();
1549 "#;
1550 let blob = create_snapshot(bridge_code).expect("snapshot creation");
1551 let mut isolate = create_isolate_from_snapshot(blob, None);
1552
1553 let scope = &mut v8::HandleScope::new(&mut isolate);
1554 let context = v8::Context::new(scope, Default::default());
1555 let scope = &mut v8::ContextScope::new(scope, context);
1556
1557 let check = v8::String::new(
1558 scope,
1559 r#"(function() {
1560 return JSON.stringify({
1561 fnType: typeof globalThis.__snapshotFn,
1562 promiseType: typeof globalThis.__snapshotFn?.(),
1563 });
1564 })()"#,
1565 )
1566 .unwrap();
1567 let script = v8::Script::compile(scope, check, None).unwrap();
1568 let result = script.run(scope).unwrap();
1569 assert_eq!(
1570 result.to_rust_string_lossy(scope),
1571 r#"{"fnType":"function","promiseType":"object"}"#,
1572 "function-valued globals should survive snapshot restore"
1573 );
1574 }
1575
1576 {
1578 let bridge_code = concat!(
1579 include_str!(concat!(env!("OUT_DIR"), "/v8-bridge.js")),
1580 "\n",
1581 include_str!(concat!(env!("OUT_DIR"), "/v8-bridge-zlib.js"))
1582 );
1583 let blob = create_snapshot(bridge_code).expect("snapshot creation");
1584 let mut isolate = create_isolate_from_snapshot(blob, None);
1585
1586 let scope = &mut v8::HandleScope::new(&mut isolate);
1587 let context = v8::Context::new(scope, Default::default());
1588 let scope = &mut v8::ContextScope::new(scope, context);
1589
1590 let check = v8::String::new(
1591 scope,
1592 r#"(function() {
1593 return JSON.stringify({
1594 fetchType: typeof globalThis.fetch,
1595 headersType: typeof globalThis.Headers,
1596 requestType: typeof globalThis.Request,
1597 responseType: typeof globalThis.Response,
1598 });
1599 })()"#,
1600 )
1601 .unwrap();
1602 let script = v8::Script::compile(scope, check, None).unwrap();
1603 let result = script.run(scope).unwrap();
1604 assert_eq!(
1605 result.to_rust_string_lossy(scope),
1606 r#"{"fetchType":"function","headersType":"function","requestType":"function","responseType":"function"}"#,
1607 "bundled bridge should expose fetch globals in restored contexts"
1608 );
1609 }
1610
1611 {
1615 use std::sync::atomic::{AtomicBool, Ordering};
1616 use std::time::Instant;
1617
1618 let cache = Arc::new(SnapshotCache::new(4));
1619 let codes: Vec<String> = (0..3)
1620 .map(|i| {
1621 format!(
1622 "(function() {{ globalThis.__concurrent_{} = {}; }})();",
1623 i, i
1624 )
1625 })
1626 .collect();
1627
1628 let barrier = Arc::new(std::sync::Barrier::new(codes.len()));
1629 let all_ok = Arc::new(AtomicBool::new(true));
1630
1631 let mut handles = vec![];
1632 for code in &codes {
1633 let cache = Arc::clone(&cache);
1634 let barrier = Arc::clone(&barrier);
1635 let all_ok = Arc::clone(&all_ok);
1636 let code = code.clone();
1637
1638 handles.push(std::thread::spawn(move || {
1639 barrier.wait();
1640 let start = Instant::now();
1641 match cache.get_or_create(&code) {
1642 Ok(arc) => {
1643 assert!(!arc.is_empty());
1644 }
1645 Err(e) => {
1646 eprintln!("get_or_create failed: {}", e);
1647 all_ok.store(false, Ordering::Relaxed);
1648 }
1649 }
1650 start.elapsed()
1651 }));
1652 }
1653
1654 let mut durations = vec![];
1655 for h in handles {
1656 durations.push(h.join().expect("thread join"));
1657 }
1658
1659 assert!(
1660 all_ok.load(Ordering::Relaxed),
1661 "all concurrent get_or_create calls should succeed"
1662 );
1663
1664 for code in &codes {
1666 let arc1 = cache.get_or_create(code).unwrap();
1667 let arc2 = cache.get_or_create(code).unwrap();
1668 assert!(
1669 Arc::ptr_eq(&arc1, &arc2),
1670 "should be cache hit after creation"
1671 );
1672 }
1673 }
1674
1675 {
1678 let bridge_code = r#"
1679 (function() {
1680 globalThis.__bridge_ok = true;
1681 })();
1682 "#;
1683 let blob = create_snapshot(bridge_code).expect("snapshot creation");
1684 let blob_bytes: Vec<u8> = blob.to_vec();
1685
1686 {
1688 let mut isolate = create_isolate_from_snapshot(blob_bytes.clone(), None);
1689 let scope = &mut v8::HandleScope::new(&mut isolate);
1690 let context = v8::Context::new(scope, Default::default());
1691 let scope = &mut v8::ContextScope::new(scope, context);
1692
1693 let check = v8::String::new(scope, "String(__bridge_ok)").unwrap();
1695 let script = v8::Script::compile(scope, check, None).unwrap();
1696 let result = script.run(scope).unwrap();
1697 assert_eq!(result.to_rust_string_lossy(scope), "true");
1698
1699 let code = v8::String::new(scope, "globalThis.__user_data = 'session-a';").unwrap();
1701 let script = v8::Script::compile(scope, code, None).unwrap();
1702 script.run(scope);
1703 }
1704
1705 {
1707 let mut isolate = create_isolate_from_snapshot(blob_bytes.clone(), None);
1708 let scope = &mut v8::HandleScope::new(&mut isolate);
1709 let context = v8::Context::new(scope, Default::default());
1710 let scope = &mut v8::ContextScope::new(scope, context);
1711
1712 let check = v8::String::new(scope, "String(__bridge_ok)").unwrap();
1714 let script = v8::Script::compile(scope, check, None).unwrap();
1715 let result = script.run(scope).unwrap();
1716 assert_eq!(result.to_rust_string_lossy(scope), "true");
1717
1718 let check = v8::String::new(scope, "typeof __user_data").unwrap();
1720 let script = v8::Script::compile(scope, check, None).unwrap();
1721 let result = script.run(scope).unwrap();
1722 assert_eq!(
1723 result.to_rust_string_lossy(scope),
1724 "undefined",
1725 "session B should not see session A's user data"
1726 );
1727 }
1728 }
1729
1730 {
1733 fn run_in(scope: &mut v8::ContextScope<v8::HandleScope>, code: &str) -> String {
1737 let source = v8::String::new(scope, code).unwrap();
1738 let script = v8::Script::compile(scope, source, None).unwrap();
1739 let result = script.run(scope).unwrap();
1740 result.to_rust_string_lossy(scope)
1741 }
1742
1743 let bridge_code = "(function(){ globalThis.__bridge_ok = true; })();";
1748 let userland = r#"
1749 (function () {
1750 if (typeof globalThis._fsReadFile !== "function") {
1751 throw new Error("bridge fns missing during userland eval");
1752 }
1753 globalThis.__sideEffectCount = (globalThis.__sideEffectCount || 0) + 1;
1754 var secret = 42;
1755 globalThis.__x = { f: function () { return secret; } };
1756 })();
1757 "#;
1758
1759 let blob = create_snapshot_with_userland(bridge_code, userland)
1760 .expect("userland snapshot creation should succeed");
1761 let blob_bytes: Vec<u8> = blob.to_vec();
1762
1763 {
1766 let mut isolate = create_isolate_from_snapshot(blob_bytes.clone(), None);
1767 let scope = &mut v8::HandleScope::new(&mut isolate);
1768 let context = v8::Context::new(scope, Default::default());
1769 let scope = &mut v8::ContextScope::new(scope, context);
1770
1771 assert_eq!(
1772 run_in(scope, "String(globalThis.__x.f())"),
1773 "42",
1774 "userland export __x.f() should return 42 from the snapshot"
1775 );
1776 assert_eq!(
1777 run_in(scope, "String(globalThis.__sideEffectCount)"),
1778 "1",
1779 "userland top-level must run exactly once (zero re-eval on restore)"
1780 );
1781 assert_eq!(
1782 run_in(scope, "String(globalThis.__bridge_ok)"),
1783 "true",
1784 "bridge state should coexist with userland state in the snapshot"
1785 );
1786
1787 run_in(scope, "globalThis.__leak = 'session-a'; ''");
1789 }
1790
1791 {
1794 let mut isolate = create_isolate_from_snapshot(blob_bytes.clone(), None);
1795 let scope = &mut v8::HandleScope::new(&mut isolate);
1796 let context = v8::Context::new(scope, Default::default());
1797 let scope = &mut v8::ContextScope::new(scope, context);
1798
1799 assert_eq!(
1800 run_in(scope, "String(globalThis.__x.f())"),
1801 "42",
1802 "session B should see the captured userland export"
1803 );
1804 assert_eq!(
1805 run_in(scope, "String(globalThis.__sideEffectCount)"),
1806 "1",
1807 "session B counter must still be 1 (no re-eval, no cross-session bump)"
1808 );
1809 assert_eq!(
1810 run_in(scope, "typeof globalThis.__leak"),
1811 "undefined",
1812 "session B must NOT observe session A's mutation"
1813 );
1814 }
1815
1816 {
1818 let cache = SnapshotCache::new(4);
1819 let a = cache
1820 .get_or_create_with_userland(bridge_code, Some(userland))
1821 .expect("userland cache create");
1822 let b = cache
1823 .get_or_create_with_userland(bridge_code, Some(userland))
1824 .expect("userland cache hit");
1825 assert!(
1826 Arc::ptr_eq(&a, &b),
1827 "identical userland should hit the cache"
1828 );
1829
1830 let userland2 = "(function(){ globalThis.__x = { f: function(){ return 7; } }; })();";
1831 let c = cache
1832 .get_or_create_with_userland(bridge_code, Some(userland2))
1833 .expect("changed userland create");
1834 assert!(
1835 !Arc::ptr_eq(&a, &c),
1836 "changed userland (dep-graph change) should rebuild"
1837 );
1838 }
1839 }
1840
1841 if let Ok(bundle_path) = std::env::var("PI_SNAPSHOT_BUNDLE_PATH") {
1848 let userland = std::fs::read_to_string(&bundle_path)
1849 .unwrap_or_else(|e| panic!("read pi bundle at {bundle_path}: {e}"));
1850 let bridge_code = concat!(
1851 include_str!(concat!(env!("OUT_DIR"), "/v8-bridge.js")),
1852 "\n",
1853 include_str!(concat!(env!("OUT_DIR"), "/v8-bridge-zlib.js"))
1854 );
1855
1856 let blob = create_snapshot_with_userland(bridge_code, &userland)
1857 .expect("real pi SDK bundle should snapshot cleanly (pure-JS, no top-level I/O)");
1858 let mut isolate = create_isolate_from_snapshot(blob, None);
1859 let scope = &mut v8::HandleScope::new(&mut isolate);
1860 let context = v8::Context::new(scope, Default::default());
1861 let scope = &mut v8::ContextScope::new(scope, context);
1862
1863 let check = v8::String::new(
1864 scope,
1865 "(function(){ var r = globalThis.__PI_SDK_RUNTIME__; \
1866 return r && typeof r.createAgentSession === 'function' && \
1867 typeof r.createAllTools === 'function' ? 'ok' : 'missing'; })()",
1868 )
1869 .unwrap();
1870 let script = v8::Script::compile(scope, check, None).unwrap();
1871 let result = script.run(scope).unwrap();
1872 assert_eq!(
1873 result.to_rust_string_lossy(scope),
1874 "ok",
1875 "restored isolate must expose the pi SDK runtime global from the snapshot"
1876 );
1877 }
1878
1879 {
1884 let bridge_code = "(function(){ globalThis.__xt_bridge = true; })();";
1885 let userland = "(function(){ globalThis.__xt = { f: function(){ return 99; } }; })();";
1886 let blob_bytes: Vec<u8> = std::thread::spawn(move || {
1887 create_snapshot_with_userland(bridge_code, userland)
1888 .expect("cross-thread snapshot build should succeed")
1889 .to_vec()
1890 })
1891 .join()
1892 .expect("build thread join");
1893
1894 let mut isolate = create_isolate_from_snapshot(blob_bytes, None);
1895 let scope = &mut v8::HandleScope::new(&mut isolate);
1896 let context = v8::Context::new(scope, Default::default());
1897 let scope = &mut v8::ContextScope::new(scope, context);
1898 let check = v8::String::new(scope, "String(globalThis.__xt.f())").unwrap();
1899 let script = v8::Script::compile(scope, check, None).unwrap();
1900 let result = script.run(scope).unwrap();
1901 assert_eq!(
1902 result.to_rust_string_lossy(scope),
1903 "99",
1904 "a snapshot built on another thread must restore correctly on this thread"
1905 );
1906 }
1907
1908 {
1913 let bridge_code = "(function(){ globalThis.__iso_ok = true; })();";
1914 let userland = "(function(){ globalThis.__sdk = { v: 1 }; })();";
1915 let blob_bytes: Vec<u8> = create_snapshot_with_userland(bridge_code, userland)
1916 .expect("isolation snapshot")
1917 .to_vec();
1918
1919 {
1921 let mut isolate = create_isolate_from_snapshot(blob_bytes.clone(), None);
1922 let scope = &mut v8::HandleScope::new(&mut isolate);
1923 let context = v8::Context::new(scope, Default::default());
1924 let scope = &mut v8::ContextScope::new(scope, context);
1925 let src = v8::String::new(
1926 scope,
1927 "globalThis.__leakG = 'A'; globalThis.__sdk.v = 999; \
1928 Array.prototype.__leakP = 'A'; ''",
1929 )
1930 .unwrap();
1931 let script = v8::Script::compile(scope, src, None).unwrap();
1932 script.run(scope);
1933 }
1934
1935 {
1938 let mut isolate = create_isolate_from_snapshot(blob_bytes.clone(), None);
1939 let scope = &mut v8::HandleScope::new(&mut isolate);
1940 let context = v8::Context::new(scope, Default::default());
1941 let scope = &mut v8::ContextScope::new(scope, context);
1942 let check = v8::String::new(
1943 scope,
1944 "(function(){ return [ \
1945 String(globalThis.__iso_ok), \
1946 typeof globalThis.__leakG, \
1947 String(globalThis.__sdk.v), \
1948 typeof ([].__leakP) \
1949 ].join(','); })()",
1950 )
1951 .unwrap();
1952 let script = v8::Script::compile(scope, check, None).unwrap();
1953 let result = script.run(scope).unwrap();
1954 assert_eq!(
1955 result.to_rust_string_lossy(scope),
1956 "true,undefined,1,undefined",
1957 "session B must see snapshot state but NOT session A's global/SDK/prototype mutations"
1958 );
1959 }
1960 }
1961
1962 {
1974 use crate::bridge::serialize_v8_value;
1975
1976 let bridge_code = r#"
1982 (function () {
1983 globalThis.process = {
1984 get versions() {
1985 // Throws during creation (default _processConfig has no
1986 // version); returns the live per-session value post-restore.
1987 return { node: _processConfig.version.replace(/^v/, "") };
1988 },
1989 };
1990 })();
1991 "#;
1992 let userland = "(function(){ globalThis.__sdk_ready = true; })();";
1993 let blob = create_snapshot_with_userland(bridge_code, userland)
1994 .expect("userland snapshot with a lazy process.versions getter");
1995
1996 let mut isolate = create_isolate_from_snapshot(blob, None);
1997 let scope = &mut v8::HandleScope::new(&mut isolate);
1998 let context = v8::Context::new(scope, Default::default());
1999 let scope = &mut v8::ContextScope::new(scope, context);
2000
2001 let payload_code = r#"({
2004 processConfig: { cwd: "/", env: {}, version: "v99.1.2", timing_mitigation: "off", frozen_time_ms: null },
2005 osConfig: { homedir: "/root", tmpdir: "/tmp", platform: "linux", arch: "x64" }
2006 })"#;
2007 let payload_source = v8::String::new(scope, payload_code).unwrap();
2008 let payload_script = v8::Script::compile(scope, payload_source, None).unwrap();
2009 let payload_val = payload_script.run(scope).unwrap();
2010 let payload_bytes = serialize_v8_value(scope, payload_val).expect("serialize payload");
2011 crate::execution::inject_globals_from_payload(scope, &payload_bytes)
2012 .expect("inject per-session config");
2013
2014 let check = v8::String::new(scope, "String(process.versions.node)").unwrap();
2017 let script = v8::Script::compile(scope, check, None).unwrap();
2018 let result = script.run(scope).unwrap();
2019 assert_eq!(
2020 result.to_rust_string_lossy(scope),
2021 "99.1.2",
2022 "process.versions must defer to the live per-session getter post-restore, \
2023 not the frozen snapshot-build-time static identity (H-1 regression)"
2024 );
2025 }
2026}
2027
2028#[cfg(test)]
2029mod tests {
2030 use super::*;
2031
2032 #[test]
2033 fn bridge_cache_key_uses_full_sha256_digest() {
2034 assert_eq!(
2037 snapshot_cache_key("abc", None),
2038 [
2039 0xba, 0x78, 0x16, 0xbf, 0x8f, 0x01, 0xcf, 0xea, 0x41, 0x41, 0x40, 0xde, 0x5d, 0xae,
2040 0x22, 0x23, 0xb0, 0x03, 0x61, 0xa3, 0x96, 0x17, 0x7a, 0x9c, 0xb4, 0x10, 0xff, 0x61,
2041 0xf2, 0x00, 0x15, 0xad,
2042 ]
2043 );
2044 }
2045
2046 #[test]
2047 fn create_snapshot_rejects_oversized_bridge_code_before_v8_creation() {
2048 let bridge_code = " ".repeat(MAX_V8_BRIDGE_CODE_BYTES + 1);
2049 let error = match create_snapshot(&bridge_code) {
2050 Ok(_) => panic!("oversized bridge code should be rejected"),
2051 Err(error) => error,
2052 };
2053
2054 assert!(error.contains(V8_BRIDGE_CODE_LIMIT_ERROR_CODE));
2055 assert!(error.contains("bridge code too large for V8 bridge setup"));
2056 assert!(error.contains(&MAX_V8_BRIDGE_CODE_BYTES.to_string()));
2057 }
2058
2059 #[test]
2060 fn snapshot_cache_rejects_oversized_bridge_code_without_retaining_in_flight_state() {
2061 let cache = SnapshotCache::new(1);
2062 let bridge_code = " ".repeat(MAX_V8_BRIDGE_CODE_BYTES + 1);
2063
2064 for _ in 0..2 {
2065 let error = match cache.get_or_create(&bridge_code) {
2066 Ok(_) => panic!("oversized bridge code should be rejected"),
2067 Err(error) => error,
2068 };
2069
2070 assert!(error.contains(V8_BRIDGE_CODE_LIMIT_ERROR_CODE));
2071 }
2072 }
2073
2074 #[test]
2075 fn snapshot_cache_key_is_dep_keyed_over_bridge_and_userland() {
2076 let bridge = "bridge-a";
2077 assert_ne!(
2079 snapshot_cache_key(bridge, None),
2080 snapshot_cache_key(bridge, Some("user-1")),
2081 "adding userland must change the key"
2082 );
2083 assert_ne!(
2085 snapshot_cache_key(bridge, Some("user-1")),
2086 snapshot_cache_key(bridge, Some("user-2")),
2087 "different userland must produce a different key"
2088 );
2089 assert_eq!(
2091 snapshot_cache_key(bridge, Some("user-1")),
2092 snapshot_cache_key(bridge, Some("user-1")),
2093 );
2094 assert_ne!(
2096 snapshot_cache_key("ab", Some("c")),
2097 snapshot_cache_key("a", Some("bc")),
2098 "the bridge/userland split must be unambiguous"
2099 );
2100 }
2101
2102 #[test]
2103 fn create_snapshot_with_userland_rejects_oversized_userland_code() {
2104 let bridge_code = "(function(){})();";
2105 let userland = " ".repeat(MAX_V8_USERLAND_CODE_BYTES + 1);
2106 let error = match create_snapshot_with_userland(bridge_code, &userland) {
2107 Ok(_) => panic!("oversized userland code should be rejected"),
2108 Err(error) => error,
2109 };
2110
2111 assert!(error.contains(V8_USERLAND_CODE_LIMIT_ERROR_CODE));
2112 assert!(error.contains("userland snapshot code too large"));
2113 }
2114}