Skip to main content

ferrijs/
runtime.rs

1//! One sandboxed realm: a `QuickJS` runtime, its context, the event loop
2//! that owns them, and the policy they run under.
3//!
4//! A [`Runtime`] is built once and run many times. `globalThis` state
5//! survives between runs REPL-style (a `globalThis.x =` persists; a
6//! script's own top-level declarations are scoped to that run), while the
7//! runtime's own globals (`console`, `args`) are refreshed every run so
8//! they always reflect the current call. A run that is force-halted
9//! (timeout) or hits an allocation fault leaves the heap untrustworthy;
10//! the runtime reports it as [`Run::poisoned`] and the host discards
11//! the realm and builds a fresh one.
12
13use std::future::Future;
14use std::pin::Pin;
15use std::sync::Arc;
16use std::sync::atomic::{AtomicBool, Ordering};
17use std::time::{Duration, Instant};
18
19use ferrijs_permissions::{Container, Permissions};
20use rquickjs::{AsyncContext, AsyncRuntime, CatchResultExt, Ctx, Module, Value};
21
22use crate::console::{ConsoleCapture, ConsoleSink};
23use crate::console_fmt::install_console;
24use crate::error::{ScriptError, ScriptErrorKind};
25use crate::extension::Extension;
26use crate::limits::{AppliedLimits, Deadline, Limits, NeverParked, PauseClock, RunOptions, TimeoutState, run_within};
27use crate::modules::{
28  BoxLoader, BoxResolver, FileLoader, FileResolver, LoaderChain, ModulePolicy, ModuleRegistry, RequireHook,
29  ResolverChain,
30};
31use crate::realm::RealmOptions;
32use crate::redact::Redactor;
33use crate::result::ConsoleEntry;
34use crate::source_map::{CompiledModule, SourceMapper};
35use crate::vm::{VmHandle, VmShutdown, spawn_vm_loop};
36use crate::vm_with;
37
38/// Default console-capture limits.
39pub const DEFAULT_MAX_CONSOLE_ENTRIES: usize = 1_000;
40pub const DEFAULT_MAX_CONSOLE_BYTES: usize = 1_048_576;
41pub const DEFAULT_MAX_CONSOLE_ENTRY_BYTES: usize = 8_192;
42
43/// How `console.*` output is kept.
44#[derive(Clone)]
45pub struct ConsoleOptions {
46  pub max_entries: usize,
47  pub max_bytes: usize,
48  pub max_entry_bytes: usize,
49  /// When set, `console.*` calls stream to this sink as they happen and
50  /// [`Run::console`] stays empty. `None` (the default) keeps the
51  /// buffered form every machine consumer reads.
52  ///
53  /// A streaming realm installs its console once and keeps it, so
54  /// `ConsoleEntry::ts_ms` counts from when the realm was built rather
55  /// than from the start of the run the entry belongs to: one clock for
56  /// the realm's whole life, which is what a session's event stream
57  /// wants anyway.
58  pub sink: Option<Arc<dyn ConsoleSink>>,
59}
60
61impl Default for ConsoleOptions {
62  fn default() -> Self {
63    Self {
64      max_entries: DEFAULT_MAX_CONSOLE_ENTRIES,
65      max_bytes: DEFAULT_MAX_CONSOLE_BYTES,
66      max_entry_bytes: DEFAULT_MAX_CONSOLE_ENTRY_BYTES,
67      sink: None,
68    }
69  }
70}
71
72impl std::fmt::Debug for ConsoleOptions {
73  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74    f.debug_struct("ConsoleOptions")
75      .field("max_entries", &self.max_entries)
76      .field("max_bytes", &self.max_bytes)
77      .field("max_entry_bytes", &self.max_entry_bytes)
78      .field("sink", &self.sink.is_some())
79      .finish()
80  }
81}
82
83/// What `process` reports about itself.
84#[derive(Debug, Clone, Default)]
85pub struct ProcessOptions {
86  /// What `process.cwd()` answers. Defaults to the module root.
87  pub cwd: Option<String>,
88  /// `process.argv[1..]`.
89  pub argv: Vec<String>,
90}
91
92/// Everything a [`Runtime`] is built from. Assembled by [`Builder`].
93pub struct Config {
94  pub limits: Limits,
95  pub console: ConsoleOptions,
96  pub realm: RealmOptions,
97  pub modules: ModulePolicy,
98  pub process: ProcessOptions,
99  pub identity: ferrijs_std::identity::Identity,
100  pub permissions: Arc<Container>,
101  pub redactor: Option<Arc<dyn Redactor>>,
102  pub pause_clock: Arc<dyn PauseClock>,
103  /// Install `globalThis.fs`, the way a scripting host does. Node has no
104  /// such global, so it is off unless asked for.
105  pub fs_global: bool,
106  /// Install the timer globals (`setTimeout` and the rest). On unless a
107  /// host installs timers of its own through an extension.
108  pub timers: bool,
109  /// What `fetch` sends through. `None` installs no `fetch` at all; the
110  /// default is the standalone [`crate::fetch::Client`].
111  #[cfg(feature = "fetch")]
112  pub fetch: Option<Arc<dyn crate::fetch::FetchBackend>>,
113  pub extensions: Vec<Arc<dyn Extension>>,
114}
115
116impl std::fmt::Debug for Config {
117  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118    f.debug_struct("Config")
119      .field("limits", &self.limits)
120      .field("console", &self.console)
121      .field("realm", &self.realm)
122      .field("modules", &self.modules)
123      .field("process", &self.process)
124      .field("identity", &self.identity)
125      .field("permissions", &self.permissions)
126      .field("fs_global", &self.fs_global)
127      .field("timers", &self.timers)
128      .field(
129        "extensions",
130        &self.extensions.iter().map(|e| e.name().to_string()).collect::<Vec<_>>(),
131      )
132      .finish_non_exhaustive()
133  }
134}
135
136/// Builds a [`Runtime`]. Every setting has a default; the defaults
137/// grant nothing.
138pub struct Builder {
139  config: Config,
140}
141
142impl Default for Builder {
143  fn default() -> Self {
144    Self {
145      config: Config {
146        limits: Limits::default(),
147        console: ConsoleOptions::default(),
148        realm: RealmOptions::default(),
149        modules: ModulePolicy::default(),
150        process: ProcessOptions::default(),
151        identity: ferrijs_std::identity::Identity::default(),
152        permissions: Arc::new(Container::new(Permissions::none())),
153        redactor: None,
154        pause_clock: Arc::new(NeverParked),
155        fs_global: false,
156        timers: true,
157        #[cfg(feature = "fetch")]
158        fetch: Some(Arc::new(crate::fetch::Client::new())),
159        extensions: Vec::new(),
160      },
161    }
162  }
163}
164
165impl Builder {
166  #[must_use]
167  pub fn limits(mut self, limits: Limits) -> Self {
168    self.config.limits = limits;
169    self
170  }
171
172  #[must_use]
173  pub fn console(mut self, console: ConsoleOptions) -> Self {
174    self.config.console = console;
175    self
176  }
177
178  #[must_use]
179  pub fn realm(mut self, realm: RealmOptions) -> Self {
180    self.config.realm = realm;
181    self
182  }
183
184  #[must_use]
185  pub fn modules(mut self, policy: ModulePolicy) -> Self {
186    self.config.modules = policy;
187    self
188  }
189
190  #[must_use]
191  pub fn process(mut self, process: ProcessOptions) -> Self {
192    self.config.process = process;
193    self
194  }
195
196  #[must_use]
197  pub fn identity(mut self, identity: ferrijs_std::identity::Identity) -> Self {
198    self.config.identity = identity;
199    self
200  }
201
202  /// The realm's policy. Replaces any container set before.
203  #[must_use]
204  pub fn permissions(mut self, permissions: Permissions) -> Self {
205    self.config.permissions = Arc::new(Container::new(permissions));
206    self
207  }
208
209  /// The realm's policy with a hook and/or audit attached.
210  #[must_use]
211  pub fn permission_container(mut self, container: Container) -> Self {
212    self.config.permissions = Arc::new(container);
213    self
214  }
215
216  #[must_use]
217  pub fn redactor(mut self, redactor: Arc<dyn Redactor>) -> Self {
218    self.config.redactor = Some(redactor);
219    self
220  }
221
222  #[must_use]
223  pub fn pause_clock(mut self, clock: Arc<dyn PauseClock>) -> Self {
224    self.config.pause_clock = clock;
225    self
226  }
227
228  #[must_use]
229  pub fn fs_global(mut self, on: bool) -> Self {
230    self.config.fs_global = on;
231    self
232  }
233
234  /// Whether the runtime installs the timer globals. A host that turns
235  /// this off installs its own through an extension.
236  #[must_use]
237  pub fn timers(mut self, on: bool) -> Self {
238    self.config.timers = on;
239    self
240  }
241
242  /// What `fetch` sends through, replacing the default client. A host
243  /// with its own HTTP stack installs it here; the realm's `net` grant
244  /// applies to it unchanged.
245  #[cfg(feature = "fetch")]
246  #[must_use]
247  pub fn fetch(mut self, backend: Arc<dyn crate::fetch::FetchBackend>) -> Self {
248    self.config.fetch = Some(backend);
249    self
250  }
251
252  /// No `fetch` global at all: for a realm that must have no network
253  /// entry point whatever its grants say.
254  #[cfg(feature = "fetch")]
255  #[must_use]
256  pub fn without_fetch(mut self) -> Self {
257    self.config.fetch = None;
258    self
259  }
260
261  #[must_use]
262  pub fn extension(mut self, extension: impl Extension + 'static) -> Self {
263    self.config.extensions.push(Arc::new(extension));
264    self
265  }
266
267  #[must_use]
268  pub fn extension_arc(mut self, extension: Arc<dyn Extension>) -> Self {
269    self.config.extensions.push(extension);
270    self
271  }
272
273  /// Build the realm: runtime, limits, loader, context, event loop, and
274  /// the one-time install of the standard library and every extension.
275  ///
276  /// # Errors
277  ///
278  /// When the engine cannot be created, an extension's modules clash,
279  /// or an install fails.
280  pub async fn build(self) -> Result<Runtime, ScriptError> {
281    Runtime::create(self.config).await
282  }
283}
284
285/// The realm's event-loop handle, stashed as context userdata so a
286/// binding that needs to dispatch back into the VM from another task
287/// can find it.
288struct VmHandleUd(VmHandle);
289
290// SAFETY: holds only an owned channel handle (`'static`; no borrowed
291// JS values), so re-stating the unused `'js` lifetime is sound.
292#[allow(unsafe_code)]
293unsafe impl rquickjs::JsLifetime<'_> for VmHandleUd {
294  type Changed<'to> = VmHandleUd;
295}
296
297/// The realm's event-loop handle, from inside a binding.
298#[must_use]
299pub fn vm_handle(ctx: &Ctx<'_>) -> Option<VmHandle> {
300  ctx.userdata::<VmHandleUd>().map(|ud| ud.0.clone())
301}
302
303/// The realm's module registry, from inside a binding.
304struct RegistryUd(Arc<ModuleRegistry>);
305
306// SAFETY: owned `Arc` only.
307#[allow(unsafe_code)]
308unsafe impl rquickjs::JsLifetime<'_> for RegistryUd {
309  type Changed<'to> = RegistryUd;
310}
311
312/// The realm's module registry, from inside a binding.
313#[must_use]
314pub fn registry(ctx: &Ctx<'_>) -> Option<Arc<ModuleRegistry>> {
315  ctx.userdata::<RegistryUd>().map(|ud| Arc::clone(&ud.0))
316}
317
318/// Outcome of one [`Runtime::run`]: the value or failure, what the run
319/// logged, how long it took, and whether the realm must be discarded.
320#[derive(Debug)]
321pub struct Run<T> {
322  pub result: Result<T, ScriptError>,
323  pub duration_ms: u64,
324  pub console: Vec<ConsoleEntry>,
325  /// The interpreter was force-halted mid-run (timeout) or hit an
326  /// allocation fault: the heap cannot be trusted, so the host must not
327  /// run anything else on this realm. A plain JS `throw` is NOT
328  /// poisoning.
329  pub poisoned: bool,
330}
331
332impl<T> Run<T> {
333  #[must_use]
334  pub fn is_ok(&self) -> bool {
335    self.result.is_ok()
336  }
337
338  /// The failure, if any.
339  #[must_use]
340  pub fn err(&self) -> Option<&ScriptError> {
341    self.result.as_ref().err()
342  }
343
344  pub fn map<U>(self, f: impl FnOnce(T) -> U) -> Run<U> {
345    Run {
346      result: self.result.map(f),
347      duration_ms: self.duration_ms,
348      console: self.console,
349      poisoned: self.poisoned,
350    }
351  }
352}
353
354impl Run<serde_json::Value> {
355  /// The run as the wire-shaped [`crate::result::ScriptResult`].
356  #[must_use]
357  pub fn into_result(self) -> crate::result::ScriptResult {
358    match self.result {
359      Ok(value) => crate::result::ScriptResult::ok(value, self.duration_ms, self.console),
360      Err(error) => crate::result::ScriptResult::err(error, self.duration_ms, self.console),
361    }
362  }
363}
364
365/// A body handed to [`Runtime::run`]: runs on the VM loop with the
366/// context in scope, and answers the run's value.
367///
368/// The future it returns is `!Send` (it holds JS values); the runtime
369/// drives it only on the VM loop, under the engine lock.
370pub type RunBody<T> =
371  Box<dyn for<'js> FnOnce(Ctx<'js>) -> Pin<Box<dyn Future<Output = Result<T, ScriptError>> + 'js>> + Send>;
372
373/// One sandboxed realm.
374pub struct Runtime {
375  engine: AsyncRuntime,
376  /// Submission handle to the realm's single VM event loop (see
377  /// [`crate::vm`]): one persistent `async_with` owns the runtime's
378  /// scheduler for the realm's whole life; every run and every
379  /// cross-task dispatch runs as a job `ctx.spawn`ed by that loop.
380  /// Nothing else may create an `async_with` against this runtime -- a
381  /// transient one steals the scheduler's single wake-queue slot and
382  /// dies with it, silently losing every later external wake.
383  vm: VmHandle,
384  /// Dropping this with the runtime ends the VM event loop, which
385  /// releases the engine on the loop's own task.
386  _vm_shutdown: VmShutdown,
387  config: Config,
388  registry: Arc<ModuleRegistry>,
389  applied: AppliedLimits,
390  timeout: Arc<TimeoutState>,
391  poisoned: AtomicBool,
392  /// The capture installed when the realm was built. Kept because a
393  /// streaming console needs no per-run one: with a sink, `push`
394  /// forwards and retains nothing, so a fresh capture per run would
395  /// re-install nineteen closures to arrive at the same behaviour. See
396  /// [`Self::run`].
397  base_console: Arc<ConsoleCapture>,
398}
399
400impl std::fmt::Debug for Runtime {
401  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
402    f.debug_struct("Runtime")
403      .field("config", &self.config)
404      .field("poisoned", &self.poisoned())
405      .finish_non_exhaustive()
406  }
407}
408
409impl Runtime {
410  #[must_use]
411  pub fn builder() -> Builder {
412    Builder::default()
413  }
414
415  async fn create(config: Config) -> Result<Self, ScriptError> {
416    let runtime = AsyncRuntime::new().map_err(|e| ScriptError::internal(format!("rquickjs runtime init: {e}")))?;
417
418    runtime.set_memory_limit(config.limits.memory).await;
419    runtime.set_max_stack_size(config.limits.stack).await;
420    runtime.set_gc_threshold(config.limits.gc_threshold).await;
421
422    // One interrupt handler for the realm's lifetime, reading the shared
423    // deadline cell. Installing per run and never disarming would let a
424    // stale deadline force-halt a callback entering the interpreter
425    // between runs.
426    let timeout = Arc::new(TimeoutState::new(Arc::clone(&config.pause_clock)));
427    {
428      let state = Arc::clone(&timeout);
429      runtime
430        .set_interrupt_handler(Some(Box::new(move || {
431          if state.expired() {
432            state.timed_out.store(true, Ordering::Relaxed);
433            true
434          } else {
435            false
436          }
437        })))
438        .await;
439    }
440
441    // The module table: the standard library plus every extension's
442    // modules, gathered BEFORE the loader is set, because `QuickJS`
443    // resolves a module graph eagerly at declare time.
444    // The module table: the standard library, minus whatever the policy
445    // withholds. A module that is not served is absent -- `require` and
446    // `import` fail to resolve it -- rather than present and refusing,
447    // which is the difference between a capability and a check.
448    let mut registry = ModuleRegistry::with_std();
449    registry.retain(|specifier| config.modules.serves_builtin(specifier));
450    let mut resolvers: Vec<BoxResolver> = Vec::new();
451    let mut loaders: Vec<BoxLoader> = Vec::new();
452    let mut require_hooks: Vec<Arc<dyn RequireHook>> = Vec::new();
453    for extension in &config.extensions {
454      extension
455        .modules(&mut registry)
456        .map_err(|e| ScriptError::internal(format!("extension `{}`: {e}", extension.name())))?;
457      for (resolver, loader) in extension.loaders() {
458        resolvers.push(resolver);
459        loaders.push(loader);
460      }
461      if let Some(hook) = extension.require_hook() {
462        require_hooks.push(hook);
463      }
464    }
465    let registry = Arc::new(registry);
466    let (native_resolver, native_loader) = registry.loader();
467    runtime
468      .set_loader(
469        (
470          native_resolver,
471          ResolverChain(resolvers),
472          FileResolver::new(config.modules.clone()),
473        ),
474        (native_loader, LoaderChain(loaders), FileLoader::new(&config.modules)),
475      )
476      .await;
477
478    let ctx = AsyncContext::full(&runtime)
479      .await
480      .map_err(|e| ScriptError::internal(format!("rquickjs context init: {e}")))?;
481
482    let (vm, vm_shutdown) = spawn_vm_loop(&ctx);
483
484    let base_console = Arc::new(Self::console_capture(&config));
485    let install_console_capture = Arc::clone(&base_console);
486    let kept_console = Arc::clone(&base_console);
487    let install_registry = Arc::clone(&registry);
488    let ud_vm = vm.clone();
489    let permissions = Arc::clone(&config.permissions);
490    let identity = config.identity.clone();
491    let process = ferrijs_std::node::process::ProcessOptions {
492      env: permissions.permissions().env_snapshot(),
493      cwd: config
494        .process
495        .cwd
496        .clone()
497        .unwrap_or_else(|| config.modules.root.to_string_lossy().into_owned()),
498      argv: config.process.argv.clone(),
499    };
500    let fs_global = config.fs_global;
501    let timers = config.timers;
502    #[cfg(feature = "fetch")]
503    let fetch_backend = config.fetch.clone();
504    let realm = config.realm.clone();
505    let extensions = config.extensions.clone();
506
507    let installed: Result<Result<(), ScriptError>, ScriptError> = vm_with!(vm => |ctx| {
508      let _ = ctx.store_userdata(VmHandleUd(ud_vm));
509      let _ = ctx.store_userdata(RegistryUd(Arc::clone(&install_registry)));
510      ferrijs_std::permissions::install(&ctx, permissions);
511      ferrijs_std::identity::set(&ctx, identity);
512
513      let fail = |what: &str, e: rquickjs::Error| ScriptError::internal(format!("failed to install {what}: {e}"));
514      ferrijs_std::init(&ctx).map_err(|e| fail("the standard library", e))?;
515      if timers {
516        crate::timers::install(&ctx).map_err(|e| fail("timers", e))?;
517      }
518      ferrijs_std::node::process::install(&ctx, &process).map_err(|e| fail("process", e))?;
519      if fs_global {
520        ferrijs_std::fs::init(&ctx).map_err(|e| fail("fs", e))?;
521      }
522      crate::modules::require::install(&ctx, install_registry, require_hooks).map_err(|e| fail("require", e))?;
523      #[cfg(feature = "fetch")]
524      if let Some(backend) = fetch_backend {
525        crate::fetch::install(&ctx, backend).map_err(|e| fail("fetch", e))?;
526      }
527      // A console from the start, so an extension's top-level
528      // `console.log` has somewhere to go; each run swaps in its own.
529      install_console(&ctx, base_console).map_err(|e| fail("console", e))?;
530
531      for extension in &extensions {
532        extension
533          .install_async(ctx.clone())
534          .await
535          .map_err(|e| ScriptError::internal(format!("extension `{}` failed to install: {e}", extension.name())))?;
536      }
537      // What an extension logged at its top level, when no sink is
538      // taking it live: forwarded to tracing rather than left in a
539      // buffer nothing drains.
540      for entry in install_console_capture.drain() {
541        tracing::info!(target: "ferrijs::extensions", "{}", entry.message);
542      }
543
544      crate::realm::lockdown(&ctx, &realm).map_err(|e| fail("the realm lockdown", e))?;
545      Ok(())
546    })
547    .await;
548    installed??;
549
550    let applied = AppliedLimits::new(&config.limits);
551    Ok(Self {
552      engine: runtime,
553      vm,
554      _vm_shutdown: vm_shutdown,
555      config,
556      registry,
557      applied,
558      timeout,
559      poisoned: AtomicBool::new(false),
560      base_console: kept_console,
561    })
562  }
563
564  fn console_capture(config: &Config) -> ConsoleCapture {
565    let capture = ConsoleCapture::new(
566      config.console.max_entries,
567      config.console.max_bytes,
568      config.console.max_entry_bytes,
569    );
570    let capture = match &config.redactor {
571      Some(redactor) => capture.with_redactor(Arc::clone(redactor)),
572      None => capture,
573    };
574    match &config.console.sink {
575      Some(sink) => capture.with_sink(Arc::clone(sink)),
576      None => capture,
577    }
578  }
579
580  /// The realm's event-loop handle. Cloneable; a host keeps one to
581  /// dispatch into the VM from its own tasks.
582  #[must_use]
583  pub fn handle(&self) -> VmHandle {
584    self.vm.clone()
585  }
586
587  #[must_use]
588  pub fn config(&self) -> &Config {
589    &self.config
590  }
591
592  #[must_use]
593  pub fn registry(&self) -> &Arc<ModuleRegistry> {
594    &self.registry
595  }
596
597  #[must_use]
598  pub fn permissions(&self) -> &Arc<Container> {
599    &self.config.permissions
600  }
601
602  /// Whether a run left the heap untrustworthy. Once set it stays set;
603  /// the host discards the realm.
604  #[must_use]
605  pub fn poisoned(&self) -> bool {
606    self.poisoned.load(Ordering::Relaxed)
607  }
608
609  /// A cloneable handle to the run deadline, for a host that re-arms it
610  /// from somewhere the runtime itself cannot be held.
611  #[must_use]
612  pub fn deadline(&self) -> Deadline {
613    Deadline(Arc::clone(&self.timeout))
614  }
615
616  /// Run `f` on the VM loop with the context in scope, outside any run
617  /// bracket: no deadline, no console capture, no poison detection.
618  /// For installs and lookups, not for user code.
619  ///
620  /// # Errors
621  ///
622  /// Only when the loop is gone (the realm was dropped).
623  pub async fn with<R, F>(&self, f: F) -> Result<R, ScriptError>
624  where
625    R: Send + 'static,
626    F: for<'js> FnOnce(Ctx<'js>) -> Pin<Box<dyn Future<Output = R> + Send + 'js>> + Send + 'static,
627  {
628    self.vm.with(f).await
629  }
630
631  /// Push resource limits to the engine, skipping any setter whose
632  /// value is unchanged since the last run.
633  async fn apply_limits(&self, memory: usize, stack: usize, gc: usize) {
634    if self.applied.memory.swap(memory, Ordering::Relaxed) != memory {
635      self.engine.set_memory_limit(memory).await;
636    }
637    if self.applied.stack.swap(stack, Ordering::Relaxed) != stack {
638      self.engine.set_max_stack_size(stack).await;
639    }
640    if self.applied.gc.swap(gc, Ordering::Relaxed) != gc {
641      self.engine.set_gc_threshold(gc).await;
642    }
643  }
644
645  async fn apply_run_options(&self, options: &RunOptions) -> Duration {
646    let limits = &self.config.limits;
647    self
648      .apply_limits(
649        options.memory.unwrap_or(limits.memory),
650        options.stack.unwrap_or(limits.stack),
651        options.gc_threshold.unwrap_or(limits.gc_threshold),
652      )
653      .await;
654    options.timeout.unwrap_or(limits.timeout)
655  }
656
657  /// Run `body` under the run bracket: limits applied, the deadline
658  /// armed, a fresh `console` capture installed, the backstop watching,
659  /// and the outcome classified (a force-halt or an allocation fault
660  /// poisons the realm; a throw does not).
661  ///
662  /// The body sees the context with `console` already refreshed. Its
663  /// value and failure are redacted before they are handed back.
664  pub async fn run<T: Send + 'static>(&self, options: RunOptions, body: RunBody<T>) -> Run<T> {
665    let started = Instant::now();
666    // A streaming console retains nothing and `drain` always answers
667    // empty, so a per-run capture would only be a second object with the
668    // same sink behind it -- and installing it costs nineteen closures
669    // on every call. The realm's own capture already forwards there.
670    let streaming = self.config.console.sink.is_some();
671    let console = if streaming {
672      Arc::clone(&self.base_console)
673    } else {
674      Arc::new(Self::console_capture(&self.config))
675    };
676    if self.poisoned() {
677      return Run {
678        result: Err(ScriptError::internal(
679          "this realm is poisoned by an earlier timeout or allocation fault; build a new one",
680        )),
681        duration_ms: 0,
682        console: Vec::new(),
683        poisoned: true,
684      };
685    }
686    let timeout = self.apply_run_options(&options).await;
687    let token = self.timeout.arm(started + timeout);
688    let run_console = Arc::clone(&console);
689
690    let fut = vm_with!(self.vm => |ctx| {
691      if !streaming
692        && let Err(e) = install_console(&ctx, run_console)
693      {
694        return Err(ScriptError::internal(format!("failed to install console: {e}")));
695      }
696      body(ctx).await
697    });
698
699    let backstop = timeout.saturating_add(self.config.limits.backstop_grace);
700    let outcome = match run_within(self.timeout.clock(), backstop, fut).await {
701      Ok(r) => r.and_then(|inner| inner),
702      Err(_) => return self.finish_backstop(token, started, &console, timeout),
703    };
704    self.finish(token, outcome, started, &console, timeout)
705  }
706
707  /// Build the `Run` from an outcome, applying the poison rule.
708  fn finish<T>(
709    &self,
710    token: crate::limits::ArmToken,
711    outcome: Result<T, ScriptError>,
712    started: Instant,
713    console: &ConsoleCapture,
714    timeout: Duration,
715  ) -> Run<T> {
716    self.timeout.disarm(token);
717    let duration_ms = elapsed_ms(started);
718    let drained = console.drain();
719    match outcome {
720      Ok(value) => Run {
721        result: Ok(value),
722        duration_ms,
723        console: drained,
724        poisoned: false,
725      },
726      Err(mut err) => {
727        let timed_out = self.timeout.timed_out.load(Ordering::Relaxed);
728        let oom = is_oom(&err);
729        let poisoned = timed_out || oom;
730        if timed_out {
731          err = ScriptError::timeout(duration_ms, timeout.as_millis().try_into().unwrap_or(u64::MAX));
732        } else if oom {
733          err.kind = ScriptErrorKind::MemoryLimit;
734        }
735        if let Some(redactor) = &self.config.redactor {
736          err.redact(redactor.as_ref());
737        }
738        if poisoned {
739          self.poisoned.store(true, Ordering::Relaxed);
740        }
741        Run {
742          result: Err(err),
743          duration_ms,
744          console: drained,
745          poisoned,
746        }
747      },
748    }
749  }
750
751  /// The `Run` for a backstop fire: the run was parked on a native
752  /// await past the deadline, so the interrupt handler never got a
753  /// chance to halt it. The future was dropped mid-flight, so by default
754  /// the realm is poisoned -- see [`crate::Limits::backstop_poisons`]
755  /// for the host that chooses otherwise.
756  fn finish_backstop<T>(
757    &self,
758    token: crate::limits::ArmToken,
759    started: Instant,
760    console: &ConsoleCapture,
761    timeout: Duration,
762  ) -> Run<T> {
763    self.timeout.disarm(token);
764    let poisoned = self.config.limits.backstop_poisons;
765    if poisoned {
766      self.poisoned.store(true, Ordering::Relaxed);
767    }
768    let duration_ms = elapsed_ms(started);
769    Run {
770      result: Err(ScriptError::timeout(
771        duration_ms,
772        timeout.as_millis().try_into().unwrap_or(u64::MAX),
773      )),
774      duration_ms,
775      console: console.drain(),
776      poisoned,
777    }
778  }
779
780  /// Evaluate a script with `args` bound as the `args` global, and
781  /// answer its top-level `return` value as JSON.
782  ///
783  /// The source is wrapped in an async IIFE, so `await` works at the top
784  /// level and `return <value>` surfaces as the result. `args` is never
785  /// interpolated into the source. For an ES module (`import` /
786  /// `export`, TypeScript) bundle it and use [`Self::eval_module`].
787  pub async fn eval_script(
788    &self,
789    source: &str,
790    args: &[serde_json::Value],
791    options: RunOptions,
792  ) -> Run<serde_json::Value> {
793    let source = source.to_string();
794    let args = args.to_vec();
795    let run = self
796      .run(
797        options,
798        Box::new(move |ctx| Box::pin(async move { script_body(&ctx, &source, &args).await })),
799      )
800      .await;
801    self.redact_run(run)
802  }
803
804  /// Evaluate a precompiled ES module with `args` bound as the `args`
805  /// global. A module cannot use top-level `return`, so the run's value
806  /// is the module's `default` export (`null` when it has none). Error
807  /// positions are remapped through the module's source map.
808  pub async fn eval_module(
809    &self,
810    module: &CompiledModule,
811    args: &[serde_json::Value],
812    options: RunOptions,
813  ) -> Run<serde_json::Value> {
814    let bytecode = Arc::clone(&module.bytecode);
815    let mapper = module.mapper();
816    let args = args.to_vec();
817    let run = self
818      .run(
819        options,
820        Box::new(move |ctx| Box::pin(async move { module_body(&ctx, &bytecode, mapper, &args).await })),
821      )
822      .await;
823    let run = run.map_err_pos(|e| {
824      if let Some(line) = e.line
825        && let Some((src, sl, sc)) = module.remap(line, e.column.unwrap_or(1))
826      {
827        e.message = format!("{} (at {src}:{sl}:{sc})", e.message);
828      }
829    });
830    self.redact_run(run)
831  }
832
833  /// Replace redacted values in a JSON document the way the runtime
834  /// does for its own results, for a host body that builds one.
835  pub fn redact_value(&self, value: &mut serde_json::Value) {
836    if let Some(redactor) = &self.config.redactor {
837      crate::redact::redact_json(redactor.as_ref(), value);
838    }
839  }
840
841  fn redact_run(&self, mut run: Run<serde_json::Value>) -> Run<serde_json::Value> {
842    // Console entries were redacted as they were pushed and the error in
843    // `finish`; the returned value has never been through a chokepoint
844    // until now.
845    if let Ok(value) = &mut run.result {
846      self.redact_value(value);
847    }
848    run
849  }
850
851  /// Declare and evaluate an ES module from source, under `name`, with
852  /// `args` bound. For a host without a bundler, or a test.
853  pub async fn eval_module_source(
854    &self,
855    name: &str,
856    source: &str,
857    args: &[serde_json::Value],
858    options: RunOptions,
859  ) -> Run<serde_json::Value> {
860    let name = name.to_string();
861    let source = source.to_string();
862    let args = args.to_vec();
863    let run = self
864      .run(
865        options,
866        Box::new(move |ctx| {
867          Box::pin(async move {
868            install_args(&ctx, &args)?;
869            let declared = match Module::declare(ctx.clone(), name.as_str(), source.as_bytes()).catch(&ctx) {
870              Ok(m) => m,
871              Err(e) => return Err(ScriptError::from_caught(&ctx, e, &source)),
872            };
873            let (evaluated, promise) = match declared.eval().catch(&ctx) {
874              Ok(v) => v,
875              Err(e) => return Err(ScriptError::from_caught(&ctx, e, &source)),
876            };
877            if let Err(e) = promise.into_future::<()>().await.catch(&ctx) {
878              return Err(ScriptError::from_caught(&ctx, e, &source));
879            }
880            let default = evaluated
881              .namespace()
882              .and_then(|ns| ns.get::<_, Value<'_>>("default"))
883              .unwrap_or_else(|_| Value::new_undefined(ctx.clone()));
884            Ok(crate::value::value_to_json(&ctx, default).unwrap_or(serde_json::Value::Null))
885          })
886        }),
887      )
888      .await;
889    self.redact_run(run)
890  }
891}
892
893/// The body of [`Runtime::eval_script`], for a host running it under
894/// its own [`Runtime::run`] bracket with globals of its own installed
895/// first: bind `args`, wrap `source` in an async IIFE, evaluate, and
896/// answer the `return` value as JSON.
897///
898/// # Errors
899///
900/// The script's own failure, positioned in the user's source.
901pub async fn script_body(
902  ctx: &Ctx<'_>,
903  source: &str,
904  args: &[serde_json::Value],
905) -> Result<serde_json::Value, ScriptError> {
906  install_args(ctx, args)?;
907  // One line of wrapper before the user's source, so a reported
908  // position is offset by one.
909  let wrapped = format!("(async () => {{\n{source}\n}})()");
910  let promise: rquickjs::Promise<'_> = ctx
911    .eval(wrapped.as_bytes())
912    .map_err(|e| ScriptError::from_caught_offset(ctx, rquickjs::CaughtError::from_error(ctx, e), source, 1))?;
913  let value: Value<'_> = promise
914    .into_future::<Value<'_>>()
915    .await
916    .map_err(|e| ScriptError::from_caught_offset(ctx, rquickjs::CaughtError::from_error(ctx, e), source, 1))?;
917  Ok(crate::value::value_to_json(ctx, value).unwrap_or(serde_json::Value::Null))
918}
919
920/// The body of [`Runtime::eval_module`]: register the module's source
921/// map, bind `args`, load and evaluate the bytecode, and answer the
922/// `default` export as JSON.
923///
924/// # Errors
925///
926/// A load, link or evaluation failure, labelled with the module name.
927pub async fn module_body(
928  ctx: &Ctx<'_>,
929  bytecode: &[u8],
930  mapper: SourceMapper,
931  args: &[serde_json::Value],
932) -> Result<serde_json::Value, ScriptError> {
933  let label = mapper.module_name.clone();
934  crate::source_map::register_bundle(ctx, mapper);
935  install_args(ctx, args)?;
936  let evaluated = eval_bytecode(ctx, bytecode, &label).await?;
937  let default = evaluated
938    .namespace()
939    .and_then(|ns| ns.get::<_, Value<'_>>("default"))
940    .unwrap_or_else(|_| Value::new_undefined(ctx.clone()));
941  Ok(crate::value::value_to_json(ctx, default).unwrap_or(serde_json::Value::Null))
942}
943
944/// Load `bytecode` as a module named `label`, evaluate it and await its
945/// top level. The evaluated module is handed back for a caller that
946/// reads its namespace.
947///
948/// # Errors
949///
950/// A load, link or evaluation failure, labelled with `label`.
951pub async fn eval_bytecode<'js>(
952  ctx: &Ctx<'js>,
953  bytecode: &[u8],
954  label: &str,
955) -> Result<Module<'js, rquickjs::module::Evaluated>, ScriptError> {
956  // SAFETY: `bytecode` was produced by `Module::write` by this exact
957  // rquickjs/QuickJS build with native endianness -- either in this
958  // process or restored from a bytecode cache whose ABI tag guarantees
959  // an ABI-identical toolchain wrote it. That contract is the bundle
960  // crate's to keep.
961  #[allow(unsafe_code)]
962  let declared = match (unsafe { Module::load(ctx.clone(), bytecode) }).catch(ctx) {
963    Ok(m) => m,
964    Err(e) => return Err(ScriptError::from_caught(ctx, e, label)),
965  };
966  let (evaluated, promise) = match declared.eval().catch(ctx) {
967    Ok(v) => v,
968    Err(e) => return Err(ScriptError::from_caught(ctx, e, label)),
969  };
970  if let Err(e) = promise.into_future::<()>().await.catch(ctx) {
971    return Err(ScriptError::from_caught(ctx, e, label));
972  }
973  Ok(evaluated)
974}
975
976impl<T> Run<T> {
977  fn map_err_pos(mut self, f: impl FnOnce(&mut ScriptError)) -> Self {
978    if let Err(e) = &mut self.result {
979      f(e);
980    }
981    self
982  }
983}
984
985/// Bind `args` as the `args` global: the JS array is built directly from
986/// the serde values -- no JSON string, no JS-side `JSON.parse`, and
987/// immune to a script reassigning `globalThis.JSON` in a persistent
988/// realm.
989///
990/// # Errors
991///
992/// Propagates the conversion.
993pub fn install_args(ctx: &Ctx<'_>, args: &[serde_json::Value]) -> Result<(), ScriptError> {
994  let array = rquickjs::Array::new(ctx.clone()).map_err(|e| ScriptError::internal(format!("args: {e}")))?;
995  for (i, a) in args.iter().enumerate() {
996    let v = crate::value::json_to_js(ctx, a).map_err(|e| ScriptError::internal(format!("args[{i}]: {e}")))?;
997    array
998      .set(i, v)
999      .map_err(|e| ScriptError::internal(format!("args[{i}]: {e}")))?;
1000  }
1001  ctx
1002    .globals()
1003    .set("args", array)
1004    .map_err(|e| ScriptError::internal(format!("args: {e}")))
1005}
1006
1007/// `QuickJS` raises an `out of memory` error when an allocation fails
1008/// after the runtime memory limit is hit. The allocation site is
1009/// arbitrary, so the heap cannot be trusted afterwards.
1010fn is_oom(err: &ScriptError) -> bool {
1011  err.kind == ScriptErrorKind::MemoryLimit || err.message.to_ascii_lowercase().contains("out of memory")
1012}
1013
1014fn elapsed_ms(started: Instant) -> u64 {
1015  u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX)
1016}