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