1use std::cell::{Cell, RefCell};
4use std::collections::{HashMap, HashSet};
5use std::ffi::c_void;
6use std::mem::MaybeUninit;
7use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
8use std::sync::OnceLock;
9
10use serde::de;
11use v8::MapFnTo;
12use v8::ValueDeserializerHelper;
13use v8::ValueSerializerHelper;
14
15use crate::host_call::BridgeCallContext;
16
17static USE_CBOR_CODEC: AtomicBool = AtomicBool::new(false);
22static EMBEDDED_CBOR_USERS: AtomicUsize = AtomicUsize::new(0);
23const MAX_CBOR_BRIDGE_DEPTH: usize = 64;
24const MAX_CBOR_BRIDGE_CONTAINER_ITEMS: usize = 100_000;
25const MAX_VM_CONTEXTS: usize = 1024;
26const MAX_PENDING_PROMISES: usize = 1024;
27
28pub fn init_codec() {
31 USE_CBOR_CODEC.store(configured_cbor_codec_enabled(), Ordering::Relaxed);
32}
33
34pub fn enable_cbor_codec() {
35 USE_CBOR_CODEC.store(true, Ordering::Relaxed);
36}
37
38pub fn acquire_embedded_cbor_codec() {
39 EMBEDDED_CBOR_USERS.fetch_add(1, Ordering::AcqRel);
40 USE_CBOR_CODEC.store(true, Ordering::Relaxed);
41}
42
43pub fn release_embedded_cbor_codec() {
44 let previous = EMBEDDED_CBOR_USERS.fetch_sub(1, Ordering::AcqRel);
45 if previous <= 1 {
46 USE_CBOR_CODEC.store(configured_cbor_codec_enabled(), Ordering::Relaxed);
47 }
48}
49
50pub fn is_cbor_codec() -> bool {
52 USE_CBOR_CODEC.load(Ordering::Relaxed)
53}
54
55fn configured_cbor_codec_enabled() -> bool {
56 std::env::var("AGENTOS_V8_CODEC")
57 .map(|val| val == "cbor")
58 .unwrap_or(false)
59}
60
61pub fn external_refs() -> &'static v8::ExternalReferences {
65 static REFS: OnceLock<v8::ExternalReferences> = OnceLock::new();
66 REFS.get_or_init(|| {
67 v8::ExternalReferences::new(&[
68 v8::ExternalReference {
69 function: sync_bridge_callback.map_fn_to(),
70 },
71 v8::ExternalReference {
72 function: async_bridge_callback.map_fn_to(),
73 },
74 ])
75 })
76}
77
78struct DefaultSerializerDelegate;
80
81impl v8::ValueSerializerImpl for DefaultSerializerDelegate {
82 fn throw_data_clone_error<'s>(
83 &self,
84 scope: &mut v8::HandleScope<'s>,
85 message: v8::Local<'s, v8::String>,
86 ) {
87 let exc = v8::Exception::error(scope, message);
88 scope.throw_exception(exc);
89 }
90}
91
92struct DefaultDeserializerDelegate;
94
95impl v8::ValueDeserializerImpl for DefaultDeserializerDelegate {}
96
97pub fn serialize_v8_value(
102 scope: &mut v8::HandleScope,
103 value: v8::Local<v8::Value>,
104) -> Result<Vec<u8>, String> {
105 if is_cbor_codec() {
106 return serialize_cbor_value(scope, value);
107 }
108 serialize_v8_wire_value(scope, value)
109}
110
111pub fn serialize_v8_wire_value(
114 scope: &mut v8::HandleScope,
115 value: v8::Local<v8::Value>,
116) -> Result<Vec<u8>, String> {
117 let context = scope.get_current_context();
118 let serializer = v8::ValueSerializer::new(scope, Box::new(DefaultSerializerDelegate));
119 serializer.write_header();
120 serializer
121 .write_value(context, value)
122 .ok_or_else(|| "V8 ValueSerializer: failed to serialize value".to_string())?;
123 Ok(serializer.release())
124}
125
126pub fn deserialize_v8_value<'s>(
129 scope: &mut v8::HandleScope<'s>,
130 data: &[u8],
131) -> Result<v8::Local<'s, v8::Value>, String> {
132 if is_cbor_codec() {
133 return deserialize_cbor_value(scope, data);
134 }
135 deserialize_v8_wire_value(scope, data)
136}
137
138pub fn deserialize_v8_wire_value<'s>(
141 scope: &mut v8::HandleScope<'s>,
142 data: &[u8],
143) -> Result<v8::Local<'s, v8::Value>, String> {
144 let context = scope.get_current_context();
145 let deserializer =
146 v8::ValueDeserializer::new(scope, Box::new(DefaultDeserializerDelegate), data);
147 deserializer
148 .read_header(context)
149 .ok_or_else(|| "V8 ValueDeserializer: invalid header".to_string())?;
150 deserializer
151 .read_value(context)
152 .ok_or_else(|| "V8 ValueDeserializer: failed to deserialize value".to_string())
153}
154
155fn v8_to_cbor(
159 scope: &mut v8::HandleScope,
160 value: v8::Local<v8::Value>,
161) -> Result<ciborium::Value, String> {
162 let mut object_stack = Vec::new();
163 v8_to_cbor_inner(scope, value, 0, &mut object_stack)
164}
165
166fn v8_to_cbor_inner(
167 scope: &mut v8::HandleScope,
168 value: v8::Local<v8::Value>,
169 depth: usize,
170 object_stack: &mut Vec<v8::Global<v8::Object>>,
171) -> Result<ciborium::Value, String> {
172 if depth > MAX_CBOR_BRIDGE_DEPTH {
173 return Err(format!(
174 "CBOR encode depth exceeds limit of {MAX_CBOR_BRIDGE_DEPTH}"
175 ));
176 }
177
178 if value.is_null_or_undefined() {
179 return Ok(ciborium::Value::Null);
180 }
181 if value.is_boolean() {
182 return Ok(ciborium::Value::Bool(value.boolean_value(scope)));
183 }
184 if value.is_int32() {
185 return Ok(ciborium::Value::Integer(
186 value.int32_value(scope).unwrap_or(0).into(),
187 ));
188 }
189 if value.is_number() {
190 return Ok(ciborium::Value::Float(
191 value.number_value(scope).unwrap_or(0.0),
192 ));
193 }
194 if value.is_string() {
195 let s = value.to_rust_string_lossy(scope);
196 return Ok(ciborium::Value::Text(s));
197 }
198 if value.is_array_buffer_view() {
199 let view = v8::Local::<v8::ArrayBufferView>::try_from(value).unwrap();
200 let len = view.byte_length();
201 let mut buf = vec![0u8; len];
202 view.copy_contents(&mut buf);
203 return Ok(ciborium::Value::Bytes(buf));
204 }
205 if value.is_array() {
206 let obj = value
207 .to_object(scope)
208 .ok_or_else(|| "CBOR encode failed to convert array to object".to_string())?;
209 enter_cbor_object(scope, object_stack, obj)?;
210 let arr = v8::Local::<v8::Array>::try_from(value).unwrap();
211 let len = arr.length();
212 let item_count = cbor_container_item_count("array", len as usize)?;
213 let mut items = Vec::with_capacity(item_count);
214 let result = (|| {
215 for i in 0..len {
216 if let Some(elem) = arr.get_index(scope, i) {
217 items.push(v8_to_cbor_inner(scope, elem, depth + 1, object_stack)?);
218 } else {
219 items.push(ciborium::Value::Null);
220 }
221 }
222 Ok(ciborium::Value::Array(items))
223 })();
224 object_stack.pop();
225 return result;
226 }
227 if value.is_object() {
228 let obj = value.to_object(scope).unwrap();
229 enter_cbor_object(scope, object_stack, obj)?;
230 let names = obj
231 .get_own_property_names(scope, v8::GetPropertyNamesArgs::default())
232 .unwrap_or_else(|| v8::Array::new(scope, 0));
233 let len = names.length();
234 let item_count = cbor_container_item_count("object", len as usize)?;
235 let mut entries = Vec::with_capacity(item_count);
236 let result = (|| {
237 for i in 0..len {
238 let key = names.get_index(scope, i).unwrap();
239 let key_str = key.to_rust_string_lossy(scope);
240 let val = obj
241 .get(scope, key)
242 .unwrap_or_else(|| v8::undefined(scope).into());
243 entries.push((
244 ciborium::Value::Text(key_str),
245 v8_to_cbor_inner(scope, val, depth + 1, object_stack)?,
246 ));
247 }
248 Ok(ciborium::Value::Map(entries))
249 })();
250 object_stack.pop();
251 return result;
252 }
253 Ok(ciborium::Value::Null)
254}
255
256fn enter_cbor_object(
257 scope: &mut v8::HandleScope,
258 object_stack: &mut Vec<v8::Global<v8::Object>>,
259 object: v8::Local<v8::Object>,
260) -> Result<(), String> {
261 for previous in object_stack.iter() {
262 let previous = v8::Local::new(scope, previous);
263 if previous.strict_equals(object.into()) {
264 return Err("CBOR encode rejected circular object graph".to_string());
265 }
266 }
267 object_stack.push(v8::Global::new(scope, object));
268 Ok(())
269}
270
271fn cbor_container_item_count(kind: &str, item_count: usize) -> Result<usize, String> {
272 if item_count > MAX_CBOR_BRIDGE_CONTAINER_ITEMS {
273 return Err(format!(
274 "CBOR {kind} item count {item_count} exceeds limit of {MAX_CBOR_BRIDGE_CONTAINER_ITEMS}"
275 ));
276 }
277 Ok(item_count)
278}
279
280struct LimitedCborValue(ciborium::Value);
281
282impl<'de> de::Deserialize<'de> for LimitedCborValue {
283 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
284 where
285 D: de::Deserializer<'de>,
286 {
287 deserializer.deserialize_any(LimitedCborVisitor).map(Self)
288 }
289}
290
291struct LimitedCborSeed;
292
293impl<'de> de::DeserializeSeed<'de> for LimitedCborSeed {
294 type Value = ciborium::Value;
295
296 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
297 where
298 D: de::Deserializer<'de>,
299 {
300 deserializer.deserialize_any(LimitedCborVisitor)
301 }
302}
303
304struct LimitedCborVisitor;
305
306impl<'de> de::Visitor<'de> for LimitedCborVisitor {
307 type Value = ciborium::Value;
308
309 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
310 formatter.write_str("a bounded CBOR bridge value")
311 }
312
313 fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E> {
314 Ok(ciborium::Value::Bool(value))
315 }
316
317 fn visit_f32<E>(self, value: f32) -> Result<Self::Value, E> {
318 Ok(ciborium::Value::Float(value.into()))
319 }
320
321 fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E> {
322 Ok(ciborium::Value::Float(value))
323 }
324
325 fn visit_i8<E>(self, value: i8) -> Result<Self::Value, E> {
326 Ok(value.into())
327 }
328
329 fn visit_i16<E>(self, value: i16) -> Result<Self::Value, E> {
330 Ok(value.into())
331 }
332
333 fn visit_i32<E>(self, value: i32) -> Result<Self::Value, E> {
334 Ok(value.into())
335 }
336
337 fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E> {
338 Ok(value.into())
339 }
340
341 fn visit_i128<E>(self, value: i128) -> Result<Self::Value, E> {
342 Ok(value.into())
343 }
344
345 fn visit_u8<E>(self, value: u8) -> Result<Self::Value, E> {
346 Ok(value.into())
347 }
348
349 fn visit_u16<E>(self, value: u16) -> Result<Self::Value, E> {
350 Ok(value.into())
351 }
352
353 fn visit_u32<E>(self, value: u32) -> Result<Self::Value, E> {
354 Ok(value.into())
355 }
356
357 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E> {
358 Ok(value.into())
359 }
360
361 fn visit_u128<E>(self, value: u128) -> Result<Self::Value, E> {
362 Ok(value.into())
363 }
364
365 fn visit_char<E>(self, value: char) -> Result<Self::Value, E> {
366 Ok(value.into())
367 }
368
369 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
370 where
371 E: de::Error,
372 {
373 Ok(value.into())
374 }
375
376 fn visit_borrowed_str<E>(self, value: &'de str) -> Result<Self::Value, E>
377 where
378 E: de::Error,
379 {
380 Ok(value.into())
381 }
382
383 fn visit_string<E>(self, value: String) -> Result<Self::Value, E> {
384 Ok(value.into())
385 }
386
387 fn visit_bytes<E>(self, value: &[u8]) -> Result<Self::Value, E>
388 where
389 E: de::Error,
390 {
391 Ok(value.into())
392 }
393
394 fn visit_borrowed_bytes<E>(self, value: &'de [u8]) -> Result<Self::Value, E>
395 where
396 E: de::Error,
397 {
398 Ok(value.into())
399 }
400
401 fn visit_byte_buf<E>(self, value: Vec<u8>) -> Result<Self::Value, E> {
402 Ok(value.into())
403 }
404
405 fn visit_none<E>(self) -> Result<Self::Value, E> {
406 Ok(ciborium::Value::Null)
407 }
408
409 fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
410 where
411 D: de::Deserializer<'de>,
412 {
413 deserializer.deserialize_any(self)
414 }
415
416 fn visit_unit<E>(self) -> Result<Self::Value, E> {
417 Ok(ciborium::Value::Null)
418 }
419
420 fn visit_newtype_struct<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
421 where
422 D: de::Deserializer<'de>,
423 {
424 deserializer.deserialize_any(self)
425 }
426
427 fn visit_seq<A>(self, mut access: A) -> Result<Self::Value, A::Error>
428 where
429 A: de::SeqAccess<'de>,
430 {
431 if let Some(item_count) = access.size_hint() {
432 limited_cbor_item_count("array", item_count)?;
433 }
434
435 let mut items = Vec::new();
436 while let Some(item) = access.next_element_seed(LimitedCborSeed)? {
437 limited_cbor_item_count("array", items.len() + 1)?;
438 items.push(item);
439 }
440 Ok(ciborium::Value::Array(items))
441 }
442
443 fn visit_map<A>(self, mut access: A) -> Result<Self::Value, A::Error>
444 where
445 A: de::MapAccess<'de>,
446 {
447 if let Some(item_count) = access.size_hint() {
448 limited_cbor_item_count("map", item_count)?;
449 }
450
451 let mut entries = Vec::new();
452 while let Some(key) = access.next_key_seed(LimitedCborSeed)? {
453 limited_cbor_item_count("map", entries.len() + 1)?;
454 let value = access.next_value_seed(LimitedCborSeed)?;
455 entries.push((key, value));
456 }
457 Ok(ciborium::Value::Map(entries))
458 }
459
460 fn visit_enum<A>(self, access: A) -> Result<Self::Value, A::Error>
461 where
462 A: de::EnumAccess<'de>,
463 {
464 use serde::de::VariantAccess;
465
466 struct TaggedValueVisitor;
467
468 impl<'de> de::Visitor<'de> for TaggedValueVisitor {
469 type Value = ciborium::Value;
470
471 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
472 formatter.write_str("a tagged CBOR bridge value")
473 }
474
475 fn visit_seq<A>(self, mut access: A) -> Result<Self::Value, A::Error>
476 where
477 A: de::SeqAccess<'de>,
478 {
479 let tag = access
480 .next_element()?
481 .ok_or_else(|| de::Error::custom("expected tag"))?;
482 let value = access
483 .next_element_seed(LimitedCborSeed)?
484 .ok_or_else(|| de::Error::custom("expected tagged value"))?;
485 Ok(ciborium::Value::Tag(tag, Box::new(value)))
486 }
487 }
488
489 let (name, data): (String, _) = access.variant()?;
490 if name != "@@TAGGED@@" {
491 return Err(de::Error::custom("expected CBOR tag"));
492 }
493 data.tuple_variant(2, TaggedValueVisitor)
494 }
495}
496
497fn limited_cbor_item_count<E: de::Error>(kind: &str, item_count: usize) -> Result<usize, E> {
498 cbor_container_item_count(kind, item_count).map_err(de::Error::custom)
499}
500
501fn cbor_to_v8<'s>(
503 scope: &mut v8::HandleScope<'s>,
504 value: &ciborium::Value,
505) -> Result<v8::Local<'s, v8::Value>, String> {
506 cbor_to_v8_inner(scope, value, 0)
507}
508
509fn cbor_to_v8_inner<'s>(
510 scope: &mut v8::HandleScope<'s>,
511 value: &ciborium::Value,
512 depth: usize,
513) -> Result<v8::Local<'s, v8::Value>, String> {
514 if depth > MAX_CBOR_BRIDGE_DEPTH {
515 return Err(format!(
516 "CBOR decode depth exceeds limit of {MAX_CBOR_BRIDGE_DEPTH}"
517 ));
518 }
519
520 match value {
521 ciborium::Value::Null => Ok(v8::null(scope).into()),
522 ciborium::Value::Bool(b) => Ok(v8::Boolean::new(scope, *b).into()),
523 ciborium::Value::Integer(n) => {
524 let n: i128 = (*n).into();
525 if n >= i32::MIN as i128 && n <= i32::MAX as i128 {
526 Ok(v8::Integer::new(scope, n as i32).into())
527 } else {
528 Ok(v8::Number::new(scope, n as f64).into())
529 }
530 }
531 ciborium::Value::Float(f) => Ok(v8::Number::new(scope, *f).into()),
532 ciborium::Value::Text(s) => Ok(v8::String::new(scope, s)
533 .ok_or_else(|| "CBOR decode failed to allocate string".to_string())?
534 .into()),
535 ciborium::Value::Bytes(b) => {
536 let len = b.len();
537 let ab = v8::ArrayBuffer::new(scope, len);
538 if len > 0 {
539 let bs = ab.get_backing_store();
540 unsafe {
541 std::ptr::copy_nonoverlapping(
542 b.as_ptr(),
543 bs.data().unwrap().as_ptr() as *mut u8,
544 len,
545 );
546 }
547 }
548 Ok(v8::Uint8Array::new(scope, ab, 0, len)
549 .ok_or_else(|| "CBOR decode failed to allocate byte array".to_string())?
550 .into())
551 }
552 ciborium::Value::Array(items) => {
553 cbor_container_item_count("array", items.len())?;
554 let arr = v8::Array::new(scope, items.len() as i32);
555 for (i, item) in items.iter().enumerate() {
556 let val = cbor_to_v8_inner(scope, item, depth + 1)?;
557 arr.set_index(scope, i as u32, val);
558 }
559 Ok(arr.into())
560 }
561 ciborium::Value::Map(entries) => {
562 cbor_container_item_count("map", entries.len())?;
563 let obj = v8::Object::new(scope);
564 for (k, v) in entries {
565 let key = cbor_to_v8_inner(scope, k, depth + 1)?;
566 let val = cbor_to_v8_inner(scope, v, depth + 1)?;
567 obj.set(scope, key, val);
568 }
569 Ok(obj.into())
570 }
571 ciborium::Value::Tag(_, inner) => cbor_to_v8_inner(scope, inner, depth + 1),
572 _ => Ok(v8::undefined(scope).into()),
573 }
574}
575
576fn raw_bytes_to_uint8array<'s>(
577 scope: &mut v8::HandleScope<'s>,
578 bytes: &[u8],
579) -> Option<v8::Local<'s, v8::Value>> {
580 let len = bytes.len();
581 let ab = v8::ArrayBuffer::new(scope, len);
582 if len > 0 {
583 let bs = ab.get_backing_store();
584 unsafe {
585 std::ptr::copy_nonoverlapping(bytes.as_ptr(), bs.data()?.as_ptr() as *mut u8, len);
586 }
587 }
588 v8::Uint8Array::new(scope, ab, 0, len).map(Into::into)
589}
590
591fn bridge_response_payload_to_v8<'s>(
592 scope: &mut v8::HandleScope<'s>,
593 status: u8,
594 payload: &[u8],
595) -> Option<v8::Local<'s, v8::Value>> {
596 if status == 2 {
597 return raw_bytes_to_uint8array(scope, payload);
598 }
599
600 let v8_val = {
601 let tc = &mut v8::TryCatch::new(scope);
602 deserialize_v8_value(tc, payload).ok()
603 };
604 if v8_val.is_some() {
605 return v8_val;
606 }
607
608 raw_bytes_to_uint8array(scope, payload)
611}
612
613pub fn serialize_cbor_value(
615 scope: &mut v8::HandleScope,
616 value: v8::Local<v8::Value>,
617) -> Result<Vec<u8>, String> {
618 let cbor_val = v8_to_cbor(scope, value)?;
619 let mut buf = Vec::new();
620 ciborium::into_writer(&cbor_val, &mut buf).map_err(|e| format!("CBOR encode failed: {}", e))?;
621 Ok(buf)
622}
623
624pub fn deserialize_cbor_value<'s>(
626 scope: &mut v8::HandleScope<'s>,
627 data: &[u8],
628) -> Result<v8::Local<'s, v8::Value>, String> {
629 let LimitedCborValue(cbor_val) =
630 ciborium::de::from_reader_with_recursion_limit(data, MAX_CBOR_BRIDGE_DEPTH)
631 .map_err(|e| format!("CBOR decode failed: {}", e))?;
632 cbor_to_v8(scope, &cbor_val)
633}
634
635struct SyncBridgeFnData {
638 ctx: *const BridgeCallContext,
639 method: String,
640}
641
642pub struct BridgeFnStore {
645 #[allow(clippy::vec_box)]
647 _data: Vec<Box<SyncBridgeFnData>>,
648}
649
650struct AsyncBridgeFnData {
652 ctx: *const BridgeCallContext,
653 pending: *const PendingPromises,
654 method: String,
655}
656
657pub struct AsyncBridgeFnStore {
660 #[allow(clippy::vec_box)]
662 _data: Vec<Box<AsyncBridgeFnData>>,
663}
664
665pub struct PendingPromises {
668 map: RefCell<HashMap<u64, v8::Global<v8::PromiseResolver>>>,
669 reserved: Cell<usize>,
670}
671
672impl PendingPromises {
673 pub fn new() -> Self {
674 PendingPromises {
675 map: RefCell::new(HashMap::new()),
676 reserved: Cell::new(0),
677 }
678 }
679
680 fn capacity_error(&self) -> Option<String> {
681 let len = self.map.borrow().len().saturating_add(self.reserved.get());
682 if len >= MAX_PENDING_PROMISES {
683 return Some(format!(
684 "async bridge pending promise registry exceeded limit of {MAX_PENDING_PROMISES} promises"
685 ));
686 }
687 None
688 }
689
690 fn reserve(&self) -> Result<PendingPromiseReservation<'_>, String> {
691 if let Some(error) = self.capacity_error() {
692 return Err(error);
693 }
694 self.reserved.set(self.reserved.get().saturating_add(1));
695 Ok(PendingPromiseReservation {
696 pending: self,
697 active: true,
698 })
699 }
700
701 fn release_reservation(&self) {
702 self.reserved.set(self.reserved.get().saturating_sub(1));
703 }
704
705 fn insert_reserved(
706 &self,
707 call_id: u64,
708 resolver: v8::Global<v8::PromiseResolver>,
709 mut reservation: PendingPromiseReservation<'_>,
710 ) {
711 self.map.borrow_mut().insert(call_id, resolver);
712 reservation.active = false;
713 self.release_reservation();
714 }
715
716 pub fn remove(&self, call_id: u64) -> Option<v8::Global<v8::PromiseResolver>> {
718 self.map.borrow_mut().remove(&call_id)
719 }
720
721 pub fn len(&self) -> usize {
723 self.map.borrow().len()
724 }
725
726 pub fn is_empty(&self) -> bool {
728 self.map.borrow().is_empty()
729 }
730}
731
732impl Default for PendingPromises {
733 fn default() -> Self {
734 Self::new()
735 }
736}
737
738struct PendingPromiseReservation<'a> {
739 pending: &'a PendingPromises,
740 active: bool,
741}
742
743impl Drop for PendingPromiseReservation<'_> {
744 fn drop(&mut self) {
745 if self.active {
746 self.pending.release_reservation();
747 }
748 }
749}
750
751#[derive(Debug, Clone, Copy, PartialEq, Eq)]
752struct ThreadResourceUsageSnapshot {
753 user_cpu_us: u64,
754 system_cpu_us: u64,
755 max_rss_kib: i64,
756 shared_memory_size: i64,
757 unshared_data_size: i64,
758 unshared_stack_size: i64,
759 minor_page_faults: i64,
760 major_page_faults: i64,
761 swapped_out: i64,
762 fs_read: i64,
763 fs_write: i64,
764 ipc_sent: i64,
765 ipc_received: i64,
766 signals_count: i64,
767 voluntary_context_switches: i64,
768 involuntary_context_switches: i64,
769}
770
771fn non_negative_c_long(value: libc::c_long) -> i64 {
772 let normalized = i128::from(value).max(0);
773 normalized.min(i128::from(i64::MAX)) as i64
774}
775
776#[cfg(not(target_os = "macos"))]
779fn timeval_to_micros(value: libc::timeval) -> u64 {
780 let seconds = i128::from(value.tv_sec).max(0);
781 let micros = i128::from(value.tv_usec).max(0);
782 (seconds
783 .saturating_mul(1_000_000)
784 .saturating_add(micros)
785 .min(i128::from(u64::MAX))) as u64
786}
787
788#[cfg(not(target_os = "macos"))]
789fn current_thread_resource_usage() -> Result<ThreadResourceUsageSnapshot, String> {
790 let mut usage = MaybeUninit::<libc::rusage>::uninit();
791 let result = unsafe { libc::getrusage(libc::RUSAGE_THREAD, usage.as_mut_ptr()) };
792 if result != 0 {
793 return Err(format!(
794 "getrusage(RUSAGE_THREAD) failed: {}",
795 std::io::Error::last_os_error()
796 ));
797 }
798 let usage = unsafe { usage.assume_init() };
799 Ok(ThreadResourceUsageSnapshot {
800 user_cpu_us: timeval_to_micros(usage.ru_utime),
801 system_cpu_us: timeval_to_micros(usage.ru_stime),
802 max_rss_kib: non_negative_c_long(usage.ru_maxrss),
803 shared_memory_size: non_negative_c_long(usage.ru_ixrss),
804 unshared_data_size: non_negative_c_long(usage.ru_idrss),
805 unshared_stack_size: non_negative_c_long(usage.ru_isrss),
806 minor_page_faults: non_negative_c_long(usage.ru_minflt),
807 major_page_faults: non_negative_c_long(usage.ru_majflt),
808 swapped_out: non_negative_c_long(usage.ru_nswap),
809 fs_read: non_negative_c_long(usage.ru_inblock),
810 fs_write: non_negative_c_long(usage.ru_oublock),
811 ipc_sent: non_negative_c_long(usage.ru_msgsnd),
812 ipc_received: non_negative_c_long(usage.ru_msgrcv),
813 signals_count: non_negative_c_long(usage.ru_nsignals),
814 voluntary_context_switches: non_negative_c_long(usage.ru_nvcsw),
815 involuntary_context_switches: non_negative_c_long(usage.ru_nivcsw),
816 })
817}
818
819#[cfg(target_os = "macos")]
824fn macos_thread_cpu_micros() -> Result<(u64, u64), String> {
825 unsafe {
828 let port = libc::pthread_mach_thread_np(libc::pthread_self());
829 if port == 0 {
830 return Err("pthread_mach_thread_np returned MACH_PORT_NULL".to_string());
831 }
832 let mut info = MaybeUninit::<libc::thread_basic_info>::zeroed();
833 let mut count = (std::mem::size_of::<libc::thread_basic_info>()
834 / std::mem::size_of::<libc::integer_t>())
835 as libc::mach_msg_type_number_t;
836 let rc = libc::thread_info(
837 port,
838 libc::THREAD_BASIC_INFO as libc::thread_flavor_t,
839 info.as_mut_ptr() as libc::thread_info_t,
840 &mut count,
841 );
842 if rc != libc::KERN_SUCCESS {
843 return Err(format!("thread_info(THREAD_BASIC_INFO) failed: {rc}"));
844 }
845 let info = info.assume_init();
846 let to_micros = |t: libc::time_value_t| -> u64 {
847 let secs = i128::from(t.seconds).max(0);
848 let micros = i128::from(t.microseconds).max(0);
849 (secs
850 .saturating_mul(1_000_000)
851 .saturating_add(micros)
852 .min(i128::from(u64::MAX))) as u64
853 };
854 Ok((to_micros(info.user_time), to_micros(info.system_time)))
855 }
856}
857
858#[cfg(target_os = "macos")]
859fn current_thread_resource_usage() -> Result<ThreadResourceUsageSnapshot, String> {
860 let (user_cpu_us, system_cpu_us) = macos_thread_cpu_micros()?;
861
862 let mut usage = MaybeUninit::<libc::rusage>::uninit();
863 let result = unsafe { libc::getrusage(libc::RUSAGE_SELF, usage.as_mut_ptr()) };
864 if result != 0 {
865 return Err(format!(
866 "getrusage(RUSAGE_SELF) failed: {}",
867 std::io::Error::last_os_error()
868 ));
869 }
870 let usage = unsafe { usage.assume_init() };
871 Ok(ThreadResourceUsageSnapshot {
872 user_cpu_us,
874 system_cpu_us,
875 max_rss_kib: non_negative_c_long(usage.ru_maxrss) / 1024,
877 shared_memory_size: non_negative_c_long(usage.ru_ixrss),
879 unshared_data_size: non_negative_c_long(usage.ru_idrss),
880 unshared_stack_size: non_negative_c_long(usage.ru_isrss),
881 minor_page_faults: non_negative_c_long(usage.ru_minflt),
882 major_page_faults: non_negative_c_long(usage.ru_majflt),
883 swapped_out: non_negative_c_long(usage.ru_nswap),
884 fs_read: non_negative_c_long(usage.ru_inblock),
885 fs_write: non_negative_c_long(usage.ru_oublock),
886 ipc_sent: non_negative_c_long(usage.ru_msgsnd),
887 ipc_received: non_negative_c_long(usage.ru_msgrcv),
888 signals_count: non_negative_c_long(usage.ru_nsignals),
889 voluntary_context_switches: non_negative_c_long(usage.ru_nvcsw),
890 involuntary_context_switches: non_negative_c_long(usage.ru_nivcsw),
891 })
892}
893
894const EMULATED_OPENSSL_VERSION: &str = "3.6.2";
900
901fn set_object_string_property<'s>(
902 scope: &mut v8::HandleScope<'s>,
903 object: v8::Local<'s, v8::Object>,
904 key: &str,
905 value: &str,
906) {
907 let key = v8::String::new(scope, key).expect("V8 string key");
908 let value = v8::String::new(scope, value).expect("V8 string value");
909 let _ = object.set(scope, key.into(), value.into());
910}
911
912fn set_object_number_property<'s>(
913 scope: &mut v8::HandleScope<'s>,
914 object: v8::Local<'s, v8::Object>,
915 key: &str,
916 value: f64,
917) {
918 let key = v8::String::new(scope, key).expect("V8 string key");
919 let value = v8::Number::new(scope, value);
920 let _ = object.set(scope, key.into(), value.into());
921}
922
923fn number_property_or_zero<'s>(
924 scope: &mut v8::HandleScope<'s>,
925 object: v8::Local<'s, v8::Object>,
926 key: &str,
927) -> u64 {
928 let key = v8::String::new(scope, key).expect("V8 string key");
929 object
930 .get(scope, key.into())
931 .and_then(|value| value.integer_value(scope))
932 .and_then(|value| u64::try_from(value).ok())
933 .unwrap_or_default()
934}
935
936fn process_memory_usage_value<'s>(scope: &mut v8::HandleScope<'s>) -> v8::Local<'s, v8::Value> {
937 let mut stats = v8::HeapStatistics::default();
938 scope.get_heap_statistics(&mut stats);
939
940 let object = v8::Object::new(scope);
941 set_object_number_property(scope, object, "rss", stats.total_physical_size() as f64);
942 set_object_number_property(scope, object, "heapTotal", stats.total_heap_size() as f64);
943 set_object_number_property(scope, object, "heapUsed", stats.used_heap_size() as f64);
944 set_object_number_property(scope, object, "external", stats.external_memory() as f64);
945 set_object_number_property(
946 scope,
947 object,
948 "arrayBuffers",
949 stats.external_memory() as f64,
950 );
951 object.into()
952}
953
954fn process_cpu_usage_value<'s>(
955 scope: &mut v8::HandleScope<'s>,
956 args: &v8::FunctionCallbackArguments,
957) -> Result<v8::Local<'s, v8::Value>, String> {
958 let usage = current_thread_resource_usage()?;
959 let current_user = usage.user_cpu_us;
960 let current_system = usage.system_cpu_us;
961
962 let (user, system) = if args.length() > 0 {
963 let prev = args.get(0);
964 if prev.is_null_or_undefined() {
965 (current_user, current_system)
966 } else if let Some(prev) = prev.to_object(scope) {
967 let previous_user = number_property_or_zero(scope, prev, "user");
968 let previous_system = number_property_or_zero(scope, prev, "system");
969 (
970 current_user.saturating_sub(previous_user),
971 current_system.saturating_sub(previous_system),
972 )
973 } else {
974 (current_user, current_system)
975 }
976 } else {
977 (current_user, current_system)
978 };
979
980 let object = v8::Object::new(scope);
981 set_object_number_property(scope, object, "user", user as f64);
982 set_object_number_property(scope, object, "system", system as f64);
983 Ok(object.into())
984}
985
986fn process_resource_usage_value<'s>(
987 scope: &mut v8::HandleScope<'s>,
988) -> Result<v8::Local<'s, v8::Value>, String> {
989 let usage = current_thread_resource_usage()?;
990 let object = v8::Object::new(scope);
991 set_object_number_property(scope, object, "userCPUTime", usage.user_cpu_us as f64);
992 set_object_number_property(scope, object, "systemCPUTime", usage.system_cpu_us as f64);
993 set_object_number_property(scope, object, "maxRSS", usage.max_rss_kib as f64);
994 set_object_number_property(
995 scope,
996 object,
997 "sharedMemorySize",
998 usage.shared_memory_size as f64,
999 );
1000 set_object_number_property(
1001 scope,
1002 object,
1003 "unsharedDataSize",
1004 usage.unshared_data_size as f64,
1005 );
1006 set_object_number_property(
1007 scope,
1008 object,
1009 "unsharedStackSize",
1010 usage.unshared_stack_size as f64,
1011 );
1012 set_object_number_property(
1013 scope,
1014 object,
1015 "minorPageFault",
1016 usage.minor_page_faults as f64,
1017 );
1018 set_object_number_property(
1019 scope,
1020 object,
1021 "majorPageFault",
1022 usage.major_page_faults as f64,
1023 );
1024 set_object_number_property(scope, object, "swappedOut", usage.swapped_out as f64);
1025 set_object_number_property(scope, object, "fsRead", usage.fs_read as f64);
1026 set_object_number_property(scope, object, "fsWrite", usage.fs_write as f64);
1027 set_object_number_property(scope, object, "ipcSent", usage.ipc_sent as f64);
1028 set_object_number_property(scope, object, "ipcReceived", usage.ipc_received as f64);
1029 set_object_number_property(scope, object, "signalsCount", usage.signals_count as f64);
1030 set_object_number_property(
1031 scope,
1032 object,
1033 "voluntaryContextSwitches",
1034 usage.voluntary_context_switches as f64,
1035 );
1036 set_object_number_property(
1037 scope,
1038 object,
1039 "involuntaryContextSwitches",
1040 usage.involuntary_context_switches as f64,
1041 );
1042 Ok(object.into())
1043}
1044
1045fn process_versions_value<'s>(scope: &mut v8::HandleScope<'s>) -> v8::Local<'s, v8::Value> {
1046 let object = v8::Object::new(scope);
1047 set_object_string_property(scope, object, "v8", v8::V8::get_version());
1048 set_object_string_property(scope, object, "openssl", EMULATED_OPENSSL_VERSION);
1049 object.into()
1050}
1051
1052#[derive(Clone)]
1053struct VmContextState {
1054 context: v8::Global<v8::Context>,
1055 baseline_keys: HashSet<String>,
1056 mirrored_keys: HashSet<String>,
1057}
1058
1059#[derive(Clone, Debug)]
1060struct VmRunOptions {
1061 filename: String,
1062 line_offset: i32,
1063 column_offset: i32,
1064 timeout_ms: Option<u32>,
1065}
1066
1067impl Default for VmRunOptions {
1068 fn default() -> Self {
1069 Self {
1070 filename: String::from("evalmachine.<anonymous>"),
1071 line_offset: 0,
1072 column_offset: 0,
1073 timeout_ms: None,
1074 }
1075 }
1076}
1077
1078thread_local! {
1079 static VM_CONTEXTS: RefCell<HashMap<u32, VmContextState>> = RefCell::new(HashMap::new());
1080 static NEXT_VM_CONTEXT_ID: Cell<u32> = const { Cell::new(1) };
1081}
1082
1083fn vm_context_capacity_error(current_contexts: usize) -> Option<String> {
1084 if current_contexts >= MAX_VM_CONTEXTS {
1085 return Some(format!(
1086 "node:vm context registry exceeded limit of {MAX_VM_CONTEXTS} contexts"
1087 ));
1088 }
1089 None
1090}
1091
1092fn reserve_vm_context_slot<'s>(
1093 scope: &mut v8::HandleScope<'s>,
1094 context: v8::Local<'s, v8::Context>,
1095) -> Result<u32, String> {
1096 VM_CONTEXTS.with(|contexts| {
1097 let mut contexts = contexts.borrow_mut();
1098 if let Some(error) = vm_context_capacity_error(contexts.len()) {
1099 return Err(error);
1100 }
1101
1102 let context_id = next_vm_context_id();
1103 contexts.insert(
1104 context_id,
1105 VmContextState {
1106 context: v8::Global::new(scope, context),
1107 baseline_keys: HashSet::new(),
1108 mirrored_keys: HashSet::new(),
1109 },
1110 );
1111 Ok(context_id)
1112 })
1113}
1114
1115fn update_vm_context_slot(
1116 context_id: u32,
1117 baseline_keys: HashSet<String>,
1118 mirrored_keys: HashSet<String>,
1119) {
1120 VM_CONTEXTS.with(|contexts| {
1121 if let Some(state) = contexts.borrow_mut().get_mut(&context_id) {
1122 state.baseline_keys = baseline_keys;
1123 state.mirrored_keys = mirrored_keys;
1124 }
1125 });
1126}
1127
1128fn remove_vm_context_slot(context_id: u32) {
1129 VM_CONTEXTS.with(|contexts| {
1130 contexts.borrow_mut().remove(&context_id);
1131 });
1132}
1133
1134fn reset_vm_context_registry() {
1148 VM_CONTEXTS.with(|contexts| contexts.borrow_mut().clear());
1149 NEXT_VM_CONTEXT_ID.with(|next_id| next_id.set(1));
1150}
1151
1152#[must_use = "hold the guard for the session lifetime; dropping it evicts the vm context registry"]
1163pub struct VmContextRegistryGuard {
1164 _private: (),
1165}
1166
1167impl VmContextRegistryGuard {
1168 pub fn new() -> Self {
1172 reset_vm_context_registry();
1173 VmContextRegistryGuard { _private: () }
1174 }
1175}
1176
1177impl Default for VmContextRegistryGuard {
1178 fn default() -> Self {
1179 Self::new()
1180 }
1181}
1182
1183impl Drop for VmContextRegistryGuard {
1184 fn drop(&mut self) {
1185 reset_vm_context_registry();
1186 }
1187}
1188
1189#[cfg(test)]
1190fn clear_vm_context_registry_for_test() {
1191 reset_vm_context_registry();
1192}
1193
1194#[cfg(test)]
1195fn fill_vm_context_registry_for_test<'s>(
1196 scope: &mut v8::HandleScope<'s>,
1197 context: v8::Local<'s, v8::Context>,
1198 count: usize,
1199) {
1200 clear_vm_context_registry_for_test();
1201 for _ in 0..count {
1202 reserve_vm_context_slot(scope, context).expect("fill vm context test registry");
1203 }
1204}
1205
1206#[cfg(test)]
1207fn vm_context_registry_len_for_test() -> usize {
1208 VM_CONTEXTS.with(|contexts| contexts.borrow().len())
1209}
1210
1211fn next_vm_context_id() -> u32 {
1212 NEXT_VM_CONTEXT_ID.with(|next_id| {
1213 let id = next_id.get();
1214 let next = id.checked_add(1).unwrap_or(1);
1215 next_id.set(next.max(1));
1216 id
1217 })
1218}
1219
1220fn vm_collect_object_keys<'s>(
1221 scope: &mut v8::HandleScope<'s>,
1222 object: v8::Local<'s, v8::Object>,
1223) -> HashSet<String> {
1224 let names = object
1225 .get_own_property_names(scope, v8::GetPropertyNamesArgs::default())
1226 .unwrap_or_else(|| v8::Array::new(scope, 0));
1227 let mut keys = HashSet::new();
1228 for index in 0..names.length() {
1229 let Some(name) = names.get_index(scope, index) else {
1230 continue;
1231 };
1232 if name.is_string() {
1233 keys.insert(name.to_rust_string_lossy(scope));
1234 }
1235 }
1236 keys
1237}
1238
1239fn vm_set_property<'s>(
1240 scope: &mut v8::HandleScope<'s>,
1241 object: v8::Local<'s, v8::Object>,
1242 key: &str,
1243 value: v8::Local<'s, v8::Value>,
1244) {
1245 let Some(key_value) = v8::String::new(scope, key) else {
1246 return;
1247 };
1248 let _ = object.set(scope, key_value.into(), value);
1249}
1250
1251fn vm_delete_property<'s>(
1252 scope: &mut v8::HandleScope<'s>,
1253 object: v8::Local<'s, v8::Object>,
1254 key: &str,
1255) {
1256 let Some(key_value) = v8::String::new(scope, key) else {
1257 return;
1258 };
1259 let _ = object.delete(scope, key_value.into());
1260}
1261
1262fn vm_copy_sandbox_into_context<'s>(
1263 scope: &mut v8::HandleScope<'s>,
1264 sandbox: v8::Local<'s, v8::Object>,
1265 context_global: v8::Local<'s, v8::Object>,
1266 previous_mirrored_keys: &HashSet<String>,
1267) -> HashSet<String> {
1268 let current_keys = vm_collect_object_keys(scope, sandbox);
1269 for key in current_keys.iter() {
1270 let Some(key_value) = v8::String::new(scope, key) else {
1271 continue;
1272 };
1273 let value = sandbox
1274 .get(scope, key_value.into())
1275 .unwrap_or_else(|| v8::undefined(scope).into());
1276 vm_set_property(scope, context_global, key, value);
1277 }
1278 for key in previous_mirrored_keys {
1279 if !current_keys.contains(key) {
1280 vm_delete_property(scope, context_global, key);
1281 }
1282 }
1283 current_keys
1284}
1285
1286fn vm_copy_context_into_sandbox<'s>(
1287 scope: &mut v8::HandleScope<'s>,
1288 context_global: v8::Local<'s, v8::Object>,
1289 sandbox: v8::Local<'s, v8::Object>,
1290 baseline_keys: &HashSet<String>,
1291 previous_mirrored_keys: &HashSet<String>,
1292) -> HashSet<String> {
1293 let current_keys = vm_collect_object_keys(scope, context_global)
1294 .into_iter()
1295 .filter(|key| !baseline_keys.contains(key))
1296 .collect::<HashSet<_>>();
1297 for key in current_keys.iter() {
1298 let Some(key_value) = v8::String::new(scope, key) else {
1299 continue;
1300 };
1301 let value = context_global
1302 .get(scope, key_value.into())
1303 .unwrap_or_else(|| v8::undefined(scope).into());
1304 vm_set_property(scope, sandbox, key, value);
1305 }
1306 for key in previous_mirrored_keys {
1307 if !current_keys.contains(key) {
1308 vm_delete_property(scope, sandbox, key);
1309 }
1310 }
1311 current_keys
1312}
1313
1314fn vm_options_from_value<'s>(
1315 scope: &mut v8::HandleScope<'s>,
1316 value: v8::Local<'s, v8::Value>,
1317) -> VmRunOptions {
1318 if value.is_null_or_undefined() {
1319 return VmRunOptions::default();
1320 }
1321 if value.is_string() {
1322 return VmRunOptions {
1323 filename: value.to_rust_string_lossy(scope),
1324 ..VmRunOptions::default()
1325 };
1326 }
1327 let Some(options) = value.to_object(scope) else {
1328 return VmRunOptions::default();
1329 };
1330 let mut result = VmRunOptions::default();
1331 let read_string = |scope: &mut v8::HandleScope<'s>, key: &str| {
1332 let key_value = v8::String::new(scope, key).expect("V8 string key");
1333 options
1334 .get(scope, key_value.into())
1335 .filter(|value| value.is_string())
1336 .map(|value| value.to_rust_string_lossy(scope))
1337 };
1338 let read_i32 = |scope: &mut v8::HandleScope<'s>, key: &str| {
1339 let key_value = v8::String::new(scope, key).expect("V8 string key");
1340 options
1341 .get(scope, key_value.into())
1342 .and_then(|value| value.int32_value(scope))
1343 };
1344 let read_u32 = |scope: &mut v8::HandleScope<'s>, key: &str| {
1345 let key_value = v8::String::new(scope, key).expect("V8 string key");
1346 options
1347 .get(scope, key_value.into())
1348 .and_then(|value| value.integer_value(scope))
1349 .and_then(|value| u32::try_from(value).ok())
1350 };
1351
1352 if let Some(filename) = read_string(scope, "filename") {
1353 result.filename = filename;
1354 }
1355 if let Some(line_offset) = read_i32(scope, "lineOffset") {
1356 result.line_offset = line_offset;
1357 }
1358 if let Some(column_offset) = read_i32(scope, "columnOffset") {
1359 result.column_offset = column_offset;
1360 }
1361 result.timeout_ms = read_u32(scope, "timeout").filter(|timeout_ms| *timeout_ms > 0);
1362 result
1363}
1364
1365fn vm_throw_error<'s>(
1366 scope: &mut v8::HandleScope<'s>,
1367 message: &str,
1368 code: Option<&str>,
1369 type_error: bool,
1370) -> v8::Local<'s, v8::Value> {
1371 let message_value = v8::String::new(scope, message).expect("V8 error message");
1372 let exception = if type_error {
1373 v8::Exception::type_error(scope, message_value)
1374 } else {
1375 v8::Exception::error(scope, message_value)
1376 };
1377 if let Some(code) = code {
1378 if let Some(exception_object) = exception.to_object(scope) {
1379 let code_key = v8::String::new(scope, "code").expect("V8 code key");
1380 let code_value = v8::String::new(scope, code).expect("V8 code value");
1381 let _ = exception_object.set(scope, code_key.into(), code_value.into());
1382 }
1383 }
1384 scope.throw_exception(exception);
1385 exception
1386}
1387
1388fn vm_throw_execution_error<'s>(
1389 scope: &mut v8::HandleScope<'s>,
1390 error: &crate::ipc::ExecutionError,
1391) -> v8::Local<'s, v8::Value> {
1392 let message_value = v8::String::new(scope, &error.message).expect("V8 error message");
1393 let exception = match error.error_type.as_str() {
1394 "TypeError" => v8::Exception::type_error(scope, message_value),
1395 _ => v8::Exception::error(scope, message_value),
1396 };
1397 if let Some(exception_object) = exception.to_object(scope) {
1398 if let Some(code) = error.code.as_deref() {
1399 let code_key = v8::String::new(scope, "code").expect("V8 code key");
1400 let code_value = v8::String::new(scope, code).expect("V8 code value");
1401 let _ = exception_object.set(scope, code_key.into(), code_value.into());
1402 }
1403 if !error.stack.is_empty() {
1404 let stack_key = v8::String::new(scope, "stack").expect("V8 stack key");
1405 let stack_value = v8::String::new(scope, &error.stack).expect("V8 stack value");
1406 let _ = exception_object.set(scope, stack_key.into(), stack_value.into());
1407 }
1408 }
1409 scope.throw_exception(exception);
1410 exception
1411}
1412
1413fn vm_apply_script_origin_to_error(
1414 mut error: crate::ipc::ExecutionError,
1415 options: &VmRunOptions,
1416) -> crate::ipc::ExecutionError {
1417 let display_line = options.line_offset.saturating_add(1).max(1);
1418 let display_column = options.column_offset.saturating_add(1).max(1);
1419 let marker = format!("{}:{}", options.filename, display_line);
1420 if !error.stack.contains(&marker) {
1421 error.stack = format!(
1422 "{}: {}\n at {}:{}:{}",
1423 error.error_type, error.message, options.filename, display_line, display_column
1424 );
1425 }
1426 error
1427}
1428
1429fn vm_run_script_in_context<'s>(
1430 scope: &mut v8::HandleScope<'s>,
1431 isolate_handle: v8::IsolateHandle,
1432 context: v8::Local<'s, v8::Context>,
1433 code: &str,
1434 options: &VmRunOptions,
1435) -> Result<v8::Local<'s, v8::Value>, String> {
1436 let mut timeout_guard = match options.timeout_ms {
1437 Some(timeout_ms) => {
1438 let (abort_tx, _abort_rx) = crossbeam_channel::bounded::<()>(0);
1439 Some(crate::timeout::TimeoutGuard::new(
1440 timeout_ms,
1441 isolate_handle.clone(),
1442 abort_tx,
1443 )?)
1444 }
1445 None => None,
1446 };
1447
1448 let mut result = None;
1449 let mut exception = None;
1450 {
1451 let context_scope = &mut v8::ContextScope::new(scope, context);
1452 let tc = &mut v8::TryCatch::new(context_scope);
1453 let source = v8::String::new(tc, code)
1454 .ok_or_else(|| String::from("vm source string too large for V8"))?;
1455 let filename = v8::String::new(tc, &options.filename)
1456 .ok_or_else(|| String::from("vm filename too large for V8"))?;
1457 let origin = v8::ScriptOrigin::new(
1458 tc,
1459 filename.into(),
1460 options.line_offset.saturating_sub(1),
1461 options.column_offset,
1462 false,
1463 -1,
1464 None,
1465 false,
1466 false,
1467 false,
1468 None,
1469 );
1470 match v8::Script::compile(tc, source, Some(&origin)) {
1471 Some(script) => match script.run(tc) {
1472 Some(value) => {
1473 tc.perform_microtask_checkpoint();
1474 if let Some(thrown) = tc.exception() {
1475 exception = Some(vm_apply_script_origin_to_error(
1476 crate::execution::extract_error_info(tc, thrown),
1477 options,
1478 ));
1479 } else {
1480 result = Some(v8::Global::new(tc, value));
1481 }
1482 }
1483 None => {
1484 let failure_message = v8::String::new(tc, "vm script execution failed")
1485 .expect("vm failure message");
1486 let thrown = tc
1487 .exception()
1488 .unwrap_or_else(|| v8::Exception::error(tc, failure_message));
1489 exception = Some(vm_apply_script_origin_to_error(
1490 crate::execution::extract_error_info(tc, thrown),
1491 options,
1492 ));
1493 }
1494 },
1495 None => {
1496 let failure_message = v8::String::new(tc, "vm script compilation failed")
1497 .expect("vm failure message");
1498 let thrown = tc
1499 .exception()
1500 .unwrap_or_else(|| v8::Exception::error(tc, failure_message));
1501 exception = Some(vm_apply_script_origin_to_error(
1502 crate::execution::extract_error_info(tc, thrown),
1503 options,
1504 ));
1505 }
1506 }
1507 }
1508
1509 let timed_out = if let Some(ref mut guard) = timeout_guard {
1510 guard.cancel();
1511 guard.timed_out()
1512 } else {
1513 false
1514 };
1515
1516 if timed_out {
1517 isolate_handle.cancel_terminate_execution();
1518 return Ok(vm_throw_error(
1519 scope,
1520 &format!(
1521 "Script execution timed out after {}ms",
1522 options.timeout_ms.unwrap_or_default()
1523 ),
1524 Some("ERR_SCRIPT_EXECUTION_TIMEOUT"),
1525 false,
1526 ));
1527 }
1528
1529 if let Some(exception) = exception {
1530 return Ok(vm_throw_execution_error(scope, &exception));
1531 }
1532
1533 Ok(result
1534 .map(|result| v8::Local::new(scope, &result))
1535 .unwrap_or_else(|| v8::undefined(scope).into()))
1536}
1537
1538fn vm_create_context_value<'s>(
1539 scope: &mut v8::HandleScope<'s>,
1540 args: &mut v8::FunctionCallbackArguments<'s>,
1541) -> Result<v8::Local<'s, v8::Value>, String> {
1542 let sandbox_value = args.get(0);
1543 if !(sandbox_value.is_object() || sandbox_value.is_function()) {
1544 return Ok(vm_throw_error(
1545 scope,
1546 "The \"object\" argument must be of type object.",
1547 None,
1548 true,
1549 ));
1550 }
1551 let sandbox = sandbox_value
1552 .to_object(scope)
1553 .ok_or_else(|| String::from("vm.createContext expected an object sandbox"))?;
1554 let context = v8::Context::new(scope, Default::default());
1555 let context_id = match reserve_vm_context_slot(scope, context) {
1556 Ok(context_id) => context_id,
1557 Err(message) => {
1558 return Ok(vm_throw_error(
1559 scope,
1560 &message,
1561 Some("ERR_AGENTOS_VM_CONTEXT_LIMIT"),
1562 false,
1563 ));
1564 }
1565 };
1566 {
1567 let context_scope = &mut v8::ContextScope::new(scope, context);
1568 let global = context.global(context_scope);
1569 for key in [
1570 "Buffer",
1571 "require",
1572 "process",
1573 "module",
1574 "exports",
1575 "__dirname",
1576 "__filename",
1577 ] {
1578 vm_delete_property(context_scope, global, key);
1579 let undefined = v8::undefined(context_scope).into();
1580 vm_set_property(context_scope, global, key, undefined);
1581 }
1582 }
1583 let baseline_keys = {
1584 let context_scope = &mut v8::ContextScope::new(scope, context);
1585 let global = context.global(context_scope);
1586 vm_collect_object_keys(context_scope, global)
1587 };
1588 let mirrored_keys_result = {
1589 let tc = &mut v8::TryCatch::new(scope);
1590 let mirrored_keys = {
1591 let context_scope = &mut v8::ContextScope::new(tc, context);
1592 let global = context.global(context_scope);
1593 vm_copy_sandbox_into_context(context_scope, sandbox, global, &HashSet::new())
1594 };
1595 if tc.has_caught() {
1596 Err(tc
1597 .exception()
1598 .map(|exception| v8::Global::new(tc, exception)))
1599 } else {
1600 Ok(mirrored_keys)
1601 }
1602 };
1603 let mirrored_keys = match mirrored_keys_result {
1604 Ok(mirrored_keys) => mirrored_keys,
1605 Err(exception) => {
1606 remove_vm_context_slot(context_id);
1607 if let Some(exception) = exception {
1608 let exception = v8::Local::new(scope, &exception);
1609 scope.throw_exception(exception);
1610 return Ok(exception);
1611 }
1612 return Ok(vm_throw_error(
1613 scope,
1614 "vm.createContext failed while mirroring sandbox properties",
1615 None,
1616 false,
1617 ));
1618 }
1619 };
1620
1621 update_vm_context_slot(context_id, baseline_keys, mirrored_keys);
1622 Ok(v8::Integer::new_from_unsigned(scope, context_id).into())
1623}
1624
1625fn vm_run_in_context_value<'s>(
1626 scope: &mut v8::HandleScope<'s>,
1627 args: &mut v8::FunctionCallbackArguments<'s>,
1628) -> Result<v8::Local<'s, v8::Value>, String> {
1629 let context_id = args
1630 .get(0)
1631 .uint32_value(scope)
1632 .ok_or_else(|| String::from("vm.runInContext missing context id"))?;
1633 let code = args.get(1).to_rust_string_lossy(scope);
1634 let options_value = args.get(2);
1635 let options = vm_options_from_value(scope, options_value);
1636 let sandbox = args
1637 .get(3)
1638 .to_object(scope)
1639 .ok_or_else(|| String::from("vm.runInContext missing sandbox object"))?;
1640 let isolate_handle = unsafe { args.get_isolate() }.thread_safe_handle();
1641
1642 let Some((context_global, baseline_keys, mirrored_keys)) = VM_CONTEXTS.with(|contexts| {
1643 contexts.borrow().get(&context_id).map(|state| {
1644 (
1645 state.context.clone(),
1646 state.baseline_keys.clone(),
1647 state.mirrored_keys.clone(),
1648 )
1649 })
1650 }) else {
1651 return Ok(vm_throw_error(
1652 scope,
1653 "The \"contextifiedObject\" argument must be a vm context.",
1654 Some("ERR_INVALID_ARG_TYPE"),
1655 true,
1656 ));
1657 };
1658
1659 let context = v8::Local::new(scope, &context_global);
1660 {
1661 let context_scope = &mut v8::ContextScope::new(scope, context);
1662 let global = context.global(context_scope);
1663 vm_copy_sandbox_into_context(context_scope, sandbox, global, &mirrored_keys);
1664 }
1665 let result = vm_run_script_in_context(scope, isolate_handle, context, &code, &options)?;
1666 let updated_keys = {
1667 let context_scope = &mut v8::ContextScope::new(scope, context);
1668 let global = context.global(context_scope);
1669 vm_copy_context_into_sandbox(
1670 context_scope,
1671 global,
1672 sandbox,
1673 &baseline_keys,
1674 &mirrored_keys,
1675 )
1676 };
1677 VM_CONTEXTS.with(|contexts| {
1678 if let Some(state) = contexts.borrow_mut().get_mut(&context_id) {
1679 state.mirrored_keys = updated_keys;
1680 }
1681 });
1682 Ok(result)
1683}
1684
1685fn vm_run_in_this_context_value<'s>(
1686 scope: &mut v8::HandleScope<'s>,
1687 args: &mut v8::FunctionCallbackArguments<'s>,
1688) -> Result<v8::Local<'s, v8::Value>, String> {
1689 let code = args.get(0).to_rust_string_lossy(scope);
1690 let options_value = args.get(1);
1691 let options = vm_options_from_value(scope, options_value);
1692 let context = scope.get_current_context();
1693 let isolate_handle = unsafe { args.get_isolate() }.thread_safe_handle();
1694 vm_run_script_in_context(scope, isolate_handle, context, &code, &options)
1695}
1696
1697fn handle_local_bridge_call<'s>(
1698 scope: &mut v8::HandleScope<'s>,
1699 method: &str,
1700 args: &mut v8::FunctionCallbackArguments<'s>,
1701) -> Result<Option<v8::Local<'s, v8::Value>>, String> {
1702 match method {
1703 "process.memoryUsage" => Ok(Some(process_memory_usage_value(scope))),
1704 "process.cpuUsage" => process_cpu_usage_value(scope, args).map(Some),
1705 "process.resourceUsage" => process_resource_usage_value(scope).map(Some),
1706 "process.versions" => Ok(Some(process_versions_value(scope))),
1707 "_vmCreateContext" => vm_create_context_value(scope, args).map(Some),
1708 "_vmRunInContext" => vm_run_in_context_value(scope, args).map(Some),
1709 "_vmRunInThisContext" => vm_run_in_this_context_value(scope, args).map(Some),
1710 _ => Ok(None),
1711 }
1712}
1713
1714pub fn register_sync_bridge_fns(
1725 scope: &mut v8::HandleScope,
1726 ctx: *const BridgeCallContext,
1727 methods: &[&str],
1728) -> BridgeFnStore {
1729 let context = scope.get_current_context();
1730 let global = context.global(scope);
1731 let mut data = Vec::with_capacity(methods.len());
1732
1733 for &method_name in methods {
1734 let boxed = Box::new(SyncBridgeFnData {
1735 ctx,
1736 method: method_name.to_string(),
1737 });
1738 let ptr = &*boxed as *const SyncBridgeFnData as *mut c_void;
1740 data.push(boxed);
1741
1742 let external = v8::External::new(scope, ptr);
1743 let template = v8::FunctionTemplate::builder(sync_bridge_callback)
1744 .data(external.into())
1745 .build(scope);
1746 let func = template.get_function(scope).unwrap();
1747 attach_bridge_function_aliases(scope, func, &["applySync", "applySyncPromise"]);
1748
1749 let key = v8::String::new(scope, method_name).unwrap();
1750 global.set(scope, key.into(), func.into());
1751 }
1752
1753 BridgeFnStore { _data: data }
1754}
1755
1756fn sync_bridge_callback<'s>(
1758 scope: &mut v8::HandleScope<'s>,
1759 args: v8::FunctionCallbackArguments<'s>,
1760 mut rv: v8::ReturnValue,
1761) {
1762 let mut args = args;
1763 let external = match v8::Local::<v8::External>::try_from(args.data()) {
1765 Ok(ext) => ext,
1766 Err(_) => {
1767 let msg =
1768 v8::String::new(scope, "internal error: missing bridge function data").unwrap();
1769 let exc = v8::Exception::error(scope, msg);
1770 scope.throw_exception(exc);
1771 return;
1772 }
1773 };
1774 let data = unsafe { &*(external.value() as *const SyncBridgeFnData) };
1776 let ctx = unsafe { &*data.ctx };
1777
1778 {
1779 let tc = &mut v8::TryCatch::new(scope);
1780 match handle_local_bridge_call(tc, &data.method, &mut args) {
1781 Ok(Some(value)) => {
1782 if tc.has_caught() {
1783 let _ = tc.rethrow();
1784 return;
1785 }
1786 rv.set(value);
1787 return;
1788 }
1789 Ok(None) => {}
1790 Err(err) => {
1791 if tc.has_caught() {
1792 let _ = tc.rethrow();
1793 return;
1794 }
1795 let msg = v8::String::new(tc, &format!("bridge runtime error: {err}")).unwrap();
1796 let exc = v8::Exception::error(tc, msg);
1797 tc.throw_exception(exc);
1798 return;
1799 }
1800 }
1801 }
1802
1803 let encoded_args = match serialize_v8_args(scope, &args) {
1805 Ok(encoded_args) => encoded_args,
1806 Err(err) => {
1807 let msg =
1808 v8::String::new(scope, &format!("bridge serialization error: {}", err)).unwrap();
1809 let exc = v8::Exception::error(scope, msg);
1810 scope.throw_exception(exc);
1811 return;
1812 }
1813 };
1814
1815 match ctx.sync_call_response(&data.method, encoded_args) {
1817 Ok(Some(response)) => {
1818 let v8_val = bridge_response_payload_to_v8(scope, response.status, &response.payload);
1819 if let Some(val) = v8_val {
1820 rv.set(val);
1821 } else {
1822 let msg = v8::String::new(scope, "bridge response conversion failed").unwrap();
1823 let exc = v8::Exception::error(scope, msg);
1824 scope.throw_exception(exc);
1825 }
1826 }
1827 Ok(None) => {
1828 rv.set_undefined();
1829 }
1830 Err(err_msg) => {
1831 let msg = v8::String::new(scope, &err_msg).unwrap();
1832 let exc = v8::Exception::error(scope, msg);
1833 if let Some(code) = bridge_error_code(&err_msg) {
1834 let exc_object = exc.to_object(scope).unwrap();
1835 let code_key = v8::String::new(scope, "code").unwrap();
1836 let code_value = v8::String::new(scope, code).unwrap();
1837 let _ = exc_object.set(scope, code_key.into(), code_value.into());
1838 }
1839 scope.throw_exception(exc);
1840 }
1841 }
1842}
1843
1844pub fn register_async_bridge_fns(
1855 scope: &mut v8::HandleScope,
1856 ctx: *const BridgeCallContext,
1857 pending: *const PendingPromises,
1858 methods: &[&str],
1859) -> AsyncBridgeFnStore {
1860 let context = scope.get_current_context();
1861 let global = context.global(scope);
1862 let mut data = Vec::with_capacity(methods.len());
1863
1864 for &method_name in methods {
1865 let boxed = Box::new(AsyncBridgeFnData {
1866 ctx,
1867 pending,
1868 method: method_name.to_string(),
1869 });
1870 let ptr = &*boxed as *const AsyncBridgeFnData as *mut c_void;
1872 data.push(boxed);
1873
1874 let external = v8::External::new(scope, ptr);
1875 let template = v8::FunctionTemplate::builder(async_bridge_callback)
1876 .data(external.into())
1877 .build(scope);
1878 let func = template.get_function(scope).unwrap();
1879 attach_bridge_function_aliases(scope, func, &["apply"]);
1880
1881 let key = v8::String::new(scope, method_name).unwrap();
1882 global.set(scope, key.into(), func.into());
1883 }
1884
1885 AsyncBridgeFnStore { _data: data }
1886}
1887
1888fn attach_bridge_function_aliases<'s>(
1889 scope: &mut v8::HandleScope<'s>,
1890 func: v8::Local<'s, v8::Function>,
1891 aliases: &[&str],
1892) {
1893 let func_object = func.to_object(scope).unwrap();
1894 for alias in aliases {
1895 let key = v8::String::new(scope, alias).unwrap();
1896 let Some(wrapper) = build_bridge_apply_wrapper(scope, func) else {
1897 continue;
1898 };
1899 let _ = func_object.set(scope, key.into(), wrapper.into());
1900 }
1901}
1902
1903fn build_bridge_apply_wrapper<'s>(
1904 scope: &mut v8::HandleScope<'s>,
1905 func: v8::Local<'s, v8::Function>,
1906) -> Option<v8::Local<'s, v8::Function>> {
1907 let source = v8::String::new(
1908 scope,
1909 "(function (fn) { return function (_thisArg, args) { return fn(...(Array.isArray(args) ? args : [])); }; })",
1910 )?;
1911 let script = v8::Script::compile(scope, source, None)?;
1912 let factory = script.run(scope)?;
1913 let factory = v8::Local::<v8::Function>::try_from(factory).ok()?;
1914 let argv = [func.into()];
1915 let receiver = v8::undefined(scope).into();
1916 factory
1917 .call(scope, receiver, &argv)
1918 .and_then(|value| v8::Local::<v8::Function>::try_from(value).ok())
1919}
1920
1921fn reject_promise_with_error(
1922 scope: &mut v8::HandleScope,
1923 resolver: v8::Local<v8::PromiseResolver>,
1924 message: &str,
1925 code: Option<&str>,
1926) {
1927 let msg = v8::String::new(scope, message).unwrap();
1928 let exc = v8::Exception::error(scope, msg);
1929 if let Some(code) = code {
1930 let exc_object = exc.to_object(scope).unwrap();
1931 let code_key = v8::String::new(scope, "code").unwrap();
1932 let code_value = v8::String::new(scope, code).unwrap();
1933 let _ = exc_object.set(scope, code_key.into(), code_value.into());
1934 }
1935 resolver.reject(scope, exc);
1936}
1937
1938fn async_bridge_callback(
1940 scope: &mut v8::HandleScope,
1941 args: v8::FunctionCallbackArguments,
1942 mut rv: v8::ReturnValue,
1943) {
1944 let external = match v8::Local::<v8::External>::try_from(args.data()) {
1946 Ok(ext) => ext,
1947 Err(_) => {
1948 let msg = v8::String::new(scope, "internal error: missing async bridge function data")
1949 .unwrap();
1950 let exc = v8::Exception::error(scope, msg);
1951 scope.throw_exception(exc);
1952 return;
1953 }
1954 };
1955 let data = unsafe { &*(external.value() as *const AsyncBridgeFnData) };
1957 let ctx = unsafe { &*data.ctx };
1958 let pending = unsafe { &*data.pending };
1959
1960 let resolver = match v8::PromiseResolver::new(scope) {
1962 Some(r) => r,
1963 None => {
1964 let msg = v8::String::new(scope, "failed to create PromiseResolver").unwrap();
1965 let exc = v8::Exception::error(scope, msg);
1966 scope.throw_exception(exc);
1967 return;
1968 }
1969 };
1970
1971 let promise = resolver.get_promise(scope);
1973
1974 let reservation = match pending.reserve() {
1975 Ok(reservation) => reservation,
1976 Err(err_msg) => {
1977 reject_promise_with_error(
1978 scope,
1979 resolver,
1980 &err_msg,
1981 Some("ERR_AGENTOS_BRIDGE_PENDING_PROMISE_LIMIT"),
1982 );
1983 rv.set(promise.into());
1984 return;
1985 }
1986 };
1987
1988 let encoded_args = match serialize_v8_args(scope, &args) {
1990 Ok(encoded_args) => encoded_args,
1991 Err(err) => {
1992 let msg =
1993 v8::String::new(scope, &format!("bridge serialization error: {}", err)).unwrap();
1994 let exc = v8::Exception::error(scope, msg);
1995 scope.throw_exception(exc);
1996 return;
1997 }
1998 };
1999
2000 match ctx.async_send(&data.method, encoded_args) {
2002 Ok(call_id) => {
2003 let global_resolver = v8::Global::new(scope, resolver);
2005 pending.insert_reserved(call_id, global_resolver, reservation);
2006 }
2007 Err(err_msg) => {
2008 reject_promise_with_error(scope, resolver, &err_msg, None);
2010 }
2011 }
2012
2013 rv.set(promise.into());
2015}
2016
2017pub fn replace_bridge_fns(
2024 scope: &mut v8::HandleScope,
2025 ctx: *const BridgeCallContext,
2026 pending: *const PendingPromises,
2027 sync_fns: &[&str],
2028 async_fns: &[&str],
2029) -> (BridgeFnStore, AsyncBridgeFnStore) {
2030 reset_vm_context_registry();
2037 let sync_store = register_sync_bridge_fns(scope, ctx, sync_fns);
2038 let async_store = register_async_bridge_fns(scope, ctx, pending, async_fns);
2039 (sync_store, async_store)
2040}
2041
2042pub fn register_stub_bridge_fns(
2053 scope: &mut v8::HandleScope,
2054 sync_fns: &[&str],
2055 async_fns: &[&str],
2056) {
2057 let context = scope.get_current_context();
2058 let global = context.global(scope);
2059
2060 for &method_name in sync_fns {
2062 let template = v8::FunctionTemplate::builder(sync_bridge_callback).build(scope);
2063 let func = template.get_function(scope).unwrap();
2064 let key = v8::String::new(scope, method_name).unwrap();
2065 global.set(scope, key.into(), func.into());
2066 }
2067
2068 for &method_name in async_fns {
2070 let template = v8::FunctionTemplate::builder(async_bridge_callback).build(scope);
2071 let func = template.get_function(scope).unwrap();
2072 let key = v8::String::new(scope, method_name).unwrap();
2073 global.set(scope, key.into(), func.into());
2074 }
2075}
2076
2077fn serialize_v8_args(
2079 scope: &mut v8::HandleScope,
2080 args: &v8::FunctionCallbackArguments,
2081) -> Result<Vec<u8>, String> {
2082 let count = args.length();
2083 let array = v8::Array::new(scope, count);
2084 for i in 0..count {
2085 array.set_index(scope, i as u32, args.get(i));
2086 }
2087 serialize_v8_value(scope, array.into())
2088}
2089
2090pub fn resolve_pending_promise(
2095 scope: &mut v8::HandleScope,
2096 pending: &PendingPromises,
2097 call_id: u64,
2098 status: u8,
2099 result: Option<Vec<u8>>,
2100 error: Option<String>,
2101) -> Result<(), String> {
2102 let resolver_global = pending
2103 .remove(call_id)
2104 .ok_or_else(|| format!("no pending promise for call_id {}", call_id))?;
2105 let resolver = v8::Local::new(scope, &resolver_global);
2106
2107 if let Some(err_msg) = error {
2108 let msg = v8::String::new(scope, &err_msg).unwrap();
2109 let exc = v8::Exception::error(scope, msg);
2110 if let Some(code) = bridge_error_code(&err_msg) {
2111 let exc_object = exc.to_object(scope).unwrap();
2112 let code_key = v8::String::new(scope, "code").unwrap();
2113 let code_value = v8::String::new(scope, code).unwrap();
2114 let _ = exc_object.set(scope, code_key.into(), code_value.into());
2115 }
2116 resolver.reject(scope, exc);
2117 } else if let Some(result_bytes) = result {
2118 let v8_val = bridge_response_payload_to_v8(scope, status, &result_bytes);
2119 if let Some(val) = v8_val {
2120 resolver.resolve(scope, val);
2121 } else {
2122 let msg = v8::String::new(scope, "bridge response conversion failed").unwrap();
2123 let exc = v8::Exception::error(scope, msg);
2124 resolver.reject(scope, exc);
2125 }
2126 } else {
2127 let undef = v8::undefined(scope);
2128 resolver.resolve(scope, undef.into());
2129 }
2130
2131 scope.perform_microtask_checkpoint();
2133
2134 Ok(())
2135}
2136
2137fn bridge_error_code(message: &str) -> Option<&str> {
2138 const TRUSTED_PREFIXES: &[&str] = &[
2139 "ERR_AGENTOS_NODE_SYNC_RPC",
2140 "ERR_AGENTOS_PYTHON_VFS_RPC",
2141 "ERR_AGENTOS_BRIDGE",
2142 ];
2143
2144 let mut segments = message.split(':').map(str::trim);
2145 let first = segments.next()?;
2146 if is_errno_segment(first) {
2147 return Some(first);
2148 }
2149
2150 if TRUSTED_PREFIXES.contains(&first) {
2151 let second = segments.next()?;
2152 if is_errno_segment(second) {
2153 return Some(second);
2154 }
2155 }
2156
2157 None
2158}
2159
2160fn is_errno_segment(segment: &str) -> bool {
2161 segment.len() >= 2
2162 && segment.starts_with('E')
2163 && !segment.starts_with("ERR_")
2164 && segment[1..]
2165 .bytes()
2166 .all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit() || byte == b'_')
2167}
2168
2169#[cfg(test)]
2170mod tests {
2171 use super::{
2172 bridge_error_code, clear_vm_context_registry_for_test, deserialize_cbor_value,
2173 fill_vm_context_registry_for_test, register_async_bridge_fns, register_sync_bridge_fns,
2174 reserve_vm_context_slot, reset_vm_context_registry, serialize_cbor_value,
2175 vm_context_capacity_error, vm_context_registry_len_for_test, PendingPromises,
2176 VmContextRegistryGuard, MAX_CBOR_BRIDGE_CONTAINER_ITEMS, MAX_CBOR_BRIDGE_DEPTH,
2177 MAX_PENDING_PROMISES, MAX_VM_CONTEXTS,
2178 };
2179 use crate::host_call::BridgeCallContext;
2180 use crate::ipc_binary::{self, BinaryFrame};
2181 use crate::isolate;
2182 use std::io::{Cursor, Write};
2183 use std::process::Command;
2184 use std::sync::{Arc, Mutex};
2185
2186 struct SharedWriter(Arc<Mutex<Vec<u8>>>);
2187
2188 impl Write for SharedWriter {
2189 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
2190 self.0.lock().unwrap().write(buf)
2191 }
2192
2193 fn flush(&mut self) -> std::io::Result<()> {
2194 self.0.lock().unwrap().flush()
2195 }
2196 }
2197
2198 fn bridge_call_count(bytes: &[u8]) -> usize {
2199 let mut cursor = Cursor::new(bytes);
2200 let mut count = 0;
2201 while let Ok(frame) = ipc_binary::read_frame(&mut cursor) {
2202 if matches!(frame, BinaryFrame::BridgeCall { .. }) {
2203 count += 1;
2204 }
2205 }
2206 count
2207 }
2208
2209 #[test]
2210 fn bridge_error_code_rejects_guest_controlled_errno_segments() {
2211 assert_eq!(bridge_error_code("user said 'EACCES: denied'"), None);
2212 assert_eq!(
2213 bridge_error_code("prefix: user said 'EPERM': more text"),
2214 None
2215 );
2216 assert_eq!(bridge_error_code("ERR_AGENTOS_FAKE: EACCES: denied"), None);
2217 }
2218
2219 #[test]
2220 fn bridge_error_code_accepts_trusted_secure_exec_prefixes() {
2221 assert_eq!(
2222 bridge_error_code("ERR_AGENTOS_NODE_SYNC_RPC: EACCES: permission denied on /foo"),
2223 Some("EACCES")
2224 );
2225 assert_eq!(
2226 bridge_error_code("ERR_AGENTOS_PYTHON_VFS_RPC: ENOENT: missing file"),
2227 Some("ENOENT")
2228 );
2229 assert_eq!(bridge_error_code("EEXIST: already exists"), Some("EEXIST"));
2230 }
2231
2232 #[test]
2233 fn bridge_v8_hardening_rejects_cbor_abuse_and_vm_context_reentry_overflow() {
2234 const SUBPROCESS_ENV: &str = "AGENTOS_V8_BRIDGE_HARDENING_SUBPROCESS";
2235 if std::env::var_os(SUBPROCESS_ENV).is_none() {
2236 let output = Command::new(std::env::current_exe().expect("current test binary"))
2237 .arg("bridge::tests::bridge_v8_hardening_rejects_cbor_abuse_and_vm_context_reentry_overflow")
2238 .arg("--exact")
2239 .arg("--nocapture")
2240 .env(SUBPROCESS_ENV, "1")
2241 .output()
2242 .expect("spawn bridge hardening subprocess");
2243 assert!(
2244 output.status.success(),
2245 "bridge hardening subprocess failed with status {:?}\nstdout:\n{}\nstderr:\n{}",
2246 output.status.code(),
2247 String::from_utf8_lossy(&output.stdout),
2248 String::from_utf8_lossy(&output.stderr)
2249 );
2250 return;
2251 }
2252
2253 isolate::init_v8_platform();
2254
2255 let mut isolate = isolate::create_isolate(None);
2256 let context = isolate::create_context(&mut isolate);
2257 let scope = &mut v8::HandleScope::new(&mut isolate);
2258 let context = v8::Local::new(scope, &context);
2259 let scope = &mut v8::ContextScope::new(scope, context);
2260
2261 let object = v8::Object::new(scope);
2262 let self_key = v8::String::new(scope, "self").unwrap();
2263 assert!(object.set(scope, self_key.into(), object.into()).is_some());
2264
2265 let error = serialize_cbor_value(scope, object.into()).expect_err("cycle rejected");
2266 assert!(
2267 error.contains("circular object graph"),
2268 "unexpected error: {error}"
2269 );
2270
2271 let source = v8::String::new(
2272 scope,
2273 &format!(
2274 "const sparse = []; sparse.length = {}; sparse",
2275 MAX_CBOR_BRIDGE_CONTAINER_ITEMS + 1
2276 ),
2277 )
2278 .unwrap();
2279 let script = v8::Script::compile(scope, source, None).unwrap();
2280 let sparse = script.run(scope).unwrap();
2281 let error = serialize_cbor_value(scope, sparse).expect_err("sparse array rejected");
2282 assert!(
2283 error.contains(&format!(
2284 "item count {} exceeds limit",
2285 MAX_CBOR_BRIDGE_CONTAINER_ITEMS + 1
2286 )),
2287 "unexpected error: {error}"
2288 );
2289
2290 let mut value = ciborium::Value::Null;
2291 for _ in 0..=MAX_CBOR_BRIDGE_DEPTH {
2292 value = ciborium::Value::Array(vec![value]);
2293 }
2294 let mut encoded = Vec::new();
2295 ciborium::into_writer(&value, &mut encoded).unwrap();
2296 let error = deserialize_cbor_value(scope, &encoded).expect_err("depth rejected");
2297 assert!(
2298 error.contains("CBOR decode failed"),
2299 "unexpected error: {error}"
2300 );
2301
2302 let oversized_len = (MAX_CBOR_BRIDGE_CONTAINER_ITEMS + 1) as u32;
2303 let oversized_array_header = [
2304 0x9a,
2305 (oversized_len >> 24) as u8,
2306 (oversized_len >> 16) as u8,
2307 (oversized_len >> 8) as u8,
2308 oversized_len as u8,
2309 ];
2310 let error = deserialize_cbor_value(scope, &oversized_array_header)
2311 .expect_err("oversized array rejected before element allocation");
2312 assert!(
2313 error.contains(&format!(
2314 "item count {} exceeds limit",
2315 MAX_CBOR_BRIDGE_CONTAINER_ITEMS + 1
2316 )),
2317 "unexpected error: {error}"
2318 );
2319
2320 fill_vm_context_registry_for_test(scope, context, MAX_VM_CONTEXTS - 1);
2321 let bridge_ctx = BridgeCallContext::new(
2322 Box::new(Vec::new()),
2323 Box::new(Cursor::new(Vec::new())),
2324 String::from("test-session"),
2325 );
2326 let _bridge_fns = register_sync_bridge_fns(
2327 scope,
2328 &bridge_ctx as *const BridgeCallContext,
2329 &["_vmCreateContext"],
2330 );
2331
2332 let source = r#"
2333 let innerCode;
2334 const sandbox = {};
2335 Object.defineProperty(sandbox, "x", {
2336 get() {
2337 try {
2338 _vmCreateContext({});
2339 } catch (error) {
2340 innerCode = error && error.code;
2341 }
2342 return 1;
2343 },
2344 enumerable: true,
2345 });
2346
2347 const outerId = _vmCreateContext(sandbox);
2348 let limitCode;
2349 try {
2350 _vmCreateContext({});
2351 } catch (error) {
2352 limitCode = error && error.code;
2353 }
2354
2355 JSON.stringify({
2356 innerCode,
2357 limitCode,
2358 outerIsInteger: Number.isInteger(outerId),
2359 })
2360 "#;
2361 {
2362 let tc = &mut v8::TryCatch::new(scope);
2363 let source = v8::String::new(tc, source).unwrap();
2364 let script = v8::Script::compile(tc, source, None).unwrap();
2365 let result = script.run(tc);
2366 assert!(
2367 !tc.has_caught(),
2368 "unexpected exception while testing vm cap"
2369 );
2370 let details = result
2371 .expect("vm context cap script result")
2372 .to_rust_string_lossy(tc);
2373 assert_eq!(
2374 details,
2375 r#"{"innerCode":"ERR_AGENTOS_VM_CONTEXT_LIMIT","limitCode":"ERR_AGENTOS_VM_CONTEXT_LIMIT","outerIsInteger":true}"#,
2376 "vm context cap script should observe limit errors"
2377 );
2378 }
2379 assert_eq!(vm_context_registry_len_for_test(), MAX_VM_CONTEXTS);
2380 clear_vm_context_registry_for_test();
2381
2382 let source = r#"
2383 (() => {
2384 let thrownMessage;
2385 const sandbox = {};
2386 Object.defineProperty(sandbox, "x", {
2387 get() {
2388 throw new Error("sandbox getter failed");
2389 },
2390 enumerable: true,
2391 });
2392 try {
2393 _vmCreateContext(sandbox);
2394 } catch (error) {
2395 thrownMessage = error && error.message;
2396 }
2397
2398 const nextId = _vmCreateContext({});
2399 return JSON.stringify({
2400 thrownMessage,
2401 nextIsInteger: Number.isInteger(nextId),
2402 });
2403 })()
2404 "#;
2405 {
2406 let tc = &mut v8::TryCatch::new(scope);
2407 let source = v8::String::new(tc, source).unwrap();
2408 let script = v8::Script::compile(tc, source, None).unwrap();
2409 let result = script.run(tc);
2410 if tc.has_caught() {
2411 let exception = tc
2412 .exception()
2413 .map(|exception| exception.to_rust_string_lossy(tc))
2414 .unwrap_or_else(|| String::from("<missing exception>"));
2415 panic!("unexpected exception while testing vm rollback: {exception}");
2416 }
2417 let details = result
2418 .expect("vm context rollback script result")
2419 .to_rust_string_lossy(tc);
2420 assert_eq!(
2421 details, r#"{"thrownMessage":"sandbox getter failed","nextIsInteger":true}"#,
2422 "vm context rollback script should preserve the getter exception and keep registry usable"
2423 );
2424 }
2425 assert_eq!(vm_context_registry_len_for_test(), 1);
2426 clear_vm_context_registry_for_test();
2427
2428 let async_writer = Arc::new(Mutex::new(Vec::new()));
2429 let async_bridge_ctx = BridgeCallContext::new(
2430 Box::new(SharedWriter(Arc::clone(&async_writer))),
2431 Box::new(Cursor::new(Vec::new())),
2432 String::from("test-session"),
2433 );
2434 let async_pending = PendingPromises::new();
2435 let _async_bridge_fns = register_async_bridge_fns(
2436 scope,
2437 &async_bridge_ctx as *const BridgeCallContext,
2438 &async_pending as *const PendingPromises,
2439 &["_asyncFn"],
2440 );
2441 let source = format!(
2442 r#"
2443 for (let i = 0; i < {fill_count}; i++) {{
2444 _asyncFn(i);
2445 }}
2446 globalThis.__overflowPromise = _asyncFn("overflow");
2447 "#,
2448 fill_count = MAX_PENDING_PROMISES,
2449 );
2450 {
2451 let tc = &mut v8::TryCatch::new(scope);
2452 let source = v8::String::new(tc, &source).unwrap();
2453 let script = v8::Script::compile(tc, source, None).unwrap();
2454 assert!(script.run(tc).is_some());
2455 assert!(!tc.has_caught(), "async overflow should reject, not throw");
2456 }
2457 assert_eq!(async_pending.len(), MAX_PENDING_PROMISES);
2458 assert_eq!(
2459 bridge_call_count(&async_writer.lock().unwrap()),
2460 MAX_PENDING_PROMISES
2461 );
2462 {
2463 let key = v8::String::new(scope, "__overflowPromise").unwrap();
2464 let value = context.global(scope).get(scope, key.into()).unwrap();
2465 let promise = v8::Local::<v8::Promise>::try_from(value).unwrap();
2466 assert_eq!(promise.state(), v8::PromiseState::Rejected);
2467 let rejection = promise.result(scope);
2468 let rejection = v8::Local::<v8::Object>::try_from(rejection).unwrap();
2469 let code_key = v8::String::new(scope, "code").unwrap();
2470 let code = rejection.get(scope, code_key.into()).unwrap();
2471 assert_eq!(
2472 code.to_rust_string_lossy(scope),
2473 "ERR_AGENTOS_BRIDGE_PENDING_PROMISE_LIMIT"
2474 );
2475 }
2476
2477 let reentrant_writer = Arc::new(Mutex::new(Vec::new()));
2478 let reentrant_bridge_ctx = BridgeCallContext::new(
2479 Box::new(SharedWriter(Arc::clone(&reentrant_writer))),
2480 Box::new(Cursor::new(Vec::new())),
2481 String::from("test-session"),
2482 );
2483 let reentrant_pending = PendingPromises::new();
2484 let _reentrant_async_bridge_fns = register_async_bridge_fns(
2485 scope,
2486 &reentrant_bridge_ctx as *const BridgeCallContext,
2487 &reentrant_pending as *const PendingPromises,
2488 &["_asyncFn"],
2489 );
2490 let source = format!(
2491 r#"
2492 for (let i = 0; i < {fill_count}; i++) {{
2493 _asyncFn(i);
2494 }}
2495 let innerPromise;
2496 const reentrantArg = {{}};
2497 Object.defineProperty(reentrantArg, "x", {{
2498 get() {{
2499 innerPromise = _asyncFn("inner");
2500 return 1;
2501 }},
2502 enumerable: true,
2503 }});
2504 globalThis.__reentrantOuterPromise = _asyncFn(reentrantArg);
2505 globalThis.__reentrantInnerPromise = innerPromise;
2506 "#,
2507 fill_count = MAX_PENDING_PROMISES - 1,
2508 );
2509 {
2510 let tc = &mut v8::TryCatch::new(scope);
2511 let source = v8::String::new(tc, &source).unwrap();
2512 let script = v8::Script::compile(tc, source, None).unwrap();
2513 assert!(script.run(tc).is_some());
2514 assert!(!tc.has_caught(), "async reentry should reject, not throw");
2515 }
2516 assert_eq!(reentrant_pending.len(), MAX_PENDING_PROMISES);
2517 assert_eq!(
2518 bridge_call_count(&reentrant_writer.lock().unwrap()),
2519 MAX_PENDING_PROMISES
2520 );
2521 {
2522 let key = v8::String::new(scope, "__reentrantInnerPromise").unwrap();
2523 let value = context.global(scope).get(scope, key.into()).unwrap();
2524 let promise = v8::Local::<v8::Promise>::try_from(value).unwrap();
2525 assert_eq!(promise.state(), v8::PromiseState::Rejected);
2526 let rejection = promise.result(scope);
2527 let rejection = v8::Local::<v8::Object>::try_from(rejection).unwrap();
2528 let code_key = v8::String::new(scope, "code").unwrap();
2529 let code = rejection.get(scope, code_key.into()).unwrap();
2530 assert_eq!(
2531 code.to_rust_string_lossy(scope),
2532 "ERR_AGENTOS_BRIDGE_PENDING_PROMISE_LIMIT"
2533 );
2534 }
2535
2536 let buffer_reentry_writer = Arc::new(Mutex::new(Vec::new()));
2537 let buffer_reentry_bridge_ctx = BridgeCallContext::new(
2538 Box::new(SharedWriter(Arc::clone(&buffer_reentry_writer))),
2539 Box::new(Cursor::new(Vec::new())),
2540 String::from("test-session"),
2541 );
2542 let buffer_reentry_pending = PendingPromises::new();
2543 let _buffer_reentry_async_bridge_fns = register_async_bridge_fns(
2544 scope,
2545 &buffer_reentry_bridge_ctx as *const BridgeCallContext,
2546 &buffer_reentry_pending as *const PendingPromises,
2547 &["_asyncFn"],
2548 );
2549 let source = r#"
2550 let bufferInnerPromise;
2551 const bufferReentrantArg = {};
2552 Object.defineProperty(bufferReentrantArg, "x", {
2553 get() {
2554 bufferInnerPromise = _asyncFn("inner");
2555 return 1;
2556 },
2557 enumerable: true,
2558 });
2559 globalThis.__bufferOuterPromise = _asyncFn(bufferReentrantArg);
2560 globalThis.__bufferInnerPromise = bufferInnerPromise;
2561 "#;
2562 {
2563 let tc = &mut v8::TryCatch::new(scope);
2564 let source = v8::String::new(tc, source).unwrap();
2565 let script = v8::Script::compile(tc, source, None).unwrap();
2566 assert!(script.run(tc).is_some());
2567 assert!(
2568 !tc.has_caught(),
2569 "async serialization reentry should not panic or throw"
2570 );
2571 }
2572 assert_eq!(buffer_reentry_pending.len(), 2);
2573 assert_eq!(bridge_call_count(&buffer_reentry_writer.lock().unwrap()), 2);
2574 }
2575
2576 #[test]
2577 fn vm_context_capacity_error_trips_at_registry_limit() {
2578 assert!(vm_context_capacity_error(MAX_VM_CONTEXTS - 1).is_none());
2579
2580 let error = vm_context_capacity_error(MAX_VM_CONTEXTS).expect("limit error");
2581 assert!(
2582 error.contains(&format!("limit of {MAX_VM_CONTEXTS} contexts")),
2583 "unexpected error: {error}"
2584 );
2585 }
2586
2587 #[test]
2599 fn vm_context_registry_evicts_slots_on_finalize() {
2600 const SUBPROCESS_ENV: &str = "AGENTOS_V8_VM_CONTEXT_FINALIZE_SUBPROCESS";
2601 if std::env::var_os(SUBPROCESS_ENV).is_none() {
2602 let output = Command::new(std::env::current_exe().expect("current test binary"))
2603 .arg("bridge::tests::vm_context_registry_evicts_slots_on_finalize")
2604 .arg("--exact")
2605 .arg("--nocapture")
2606 .env(SUBPROCESS_ENV, "1")
2607 .output()
2608 .expect("spawn vm context finalize subprocess");
2609 assert!(
2610 output.status.success(),
2611 "vm context finalize subprocess failed with status {:?}\nstdout:\n{}\nstderr:\n{}",
2612 output.status.code(),
2613 String::from_utf8_lossy(&output.stdout),
2614 String::from_utf8_lossy(&output.stderr)
2615 );
2616 return;
2617 }
2618
2619 isolate::init_v8_platform();
2620 let mut isolate = isolate::create_isolate(None);
2621 let context = isolate::create_context(&mut isolate);
2622 let scope = &mut v8::HandleScope::new(&mut isolate);
2623 let context = v8::Local::new(scope, &context);
2624 let scope = &mut v8::ContextScope::new(scope, context);
2625
2626 reset_vm_context_registry();
2627 assert_eq!(
2628 vm_context_registry_len_for_test(),
2629 0,
2630 "registry starts empty"
2631 );
2632
2633 const CONTEXTS_PER_EXECUTION: usize = 3;
2639 let executions = MAX_VM_CONTEXTS * 4;
2640 for execution in 0..executions {
2641 {
2642 let _guard = VmContextRegistryGuard::new();
2643 for _ in 0..CONTEXTS_PER_EXECUTION {
2644 reserve_vm_context_slot(scope, context)
2645 .expect("reserve vm context slot below cap; a leak would hit the cap");
2646 }
2647 assert_eq!(
2648 vm_context_registry_len_for_test(),
2649 CONTEXTS_PER_EXECUTION,
2650 "slots reserved during execution {execution} must be live"
2651 );
2652 }
2653 assert_eq!(
2657 vm_context_registry_len_for_test(),
2658 0,
2659 "registry must be empty after execution {execution} finalizes"
2660 );
2661 }
2662
2663 let _guard = VmContextRegistryGuard::new();
2666 assert!(
2667 reserve_vm_context_slot(scope, context).is_ok(),
2668 "createContext must keep succeeding; leaked slots would have hit MAX_VM_CONTEXTS"
2669 );
2670 }
2671}