Skip to main content

ferridriver_script/bindings/
process.rs

1//! A deliberately small, sandbox-safe `process` global.
2//!
3//! Node's `process` is mostly ambient authority; this exposes only the
4//! members that are either inert (platform/version/timing) or
5//! capability-gated (`env`). Everything that could escape the sandbox
6//! (`binding`, `dlopen`, `chdir`, `kill`, `setuid`, real `exit`) is
7//! absent or neutered. `process.env` is the operator's allow-list
8//! intersected with the real environment — empty by default.
9
10use std::time::Instant;
11
12use rquickjs::function::{Func, Rest};
13use rquickjs::{Ctx, Object, Value};
14
15use crate::engine::ScriptCaps;
16
17/// Install `globalThis.process`. Called once per session (the values
18/// are session-stable: env comes from resolved config, the
19/// monotonic clock anchors at session start).
20pub fn install(ctx: &Ctx<'_>, caps: &ScriptCaps, cwd: &str) -> rquickjs::Result<()> {
21  let g = ctx.globals();
22  let p = Object::new(ctx.clone())?;
23
24  // -- env: the only sensitive surface, default-deny ----------------
25  let env = Object::new(ctx.clone())?;
26  for (k, v) in &caps.env {
27    env.set(k.as_str(), v.as_str())?;
28  }
29  // Frozen so a script cannot stuff values in and mislead later code
30  // into thinking an env var is set.
31  freeze(ctx, &env)?;
32  p.set("env", env)?;
33
34  // -- inert platform identity --------------------------------------
35  p.set("platform", std::env::consts::OS)?; // "linux" | "macos" | ...
36  p.set("arch", std::env::consts::ARCH)?; // "x86_64" | "aarch64" | ...
37  let fv = env!("CARGO_PKG_VERSION");
38  p.set("version", format!("ferridriver-{fv}"))?;
39  let versions = Object::new(ctx.clone())?;
40  versions.set("ferridriver", fv)?;
41  versions.set("quickjs", "rquickjs-0.11")?;
42  freeze(ctx, &versions)?;
43  p.set("versions", versions)?;
44  let release = Object::new(ctx.clone())?;
45  release.set("name", "ferridriver")?;
46  freeze(ctx, &release)?;
47  p.set("release", release)?;
48
49  // argv: scripts get their inputs via the `args` global, not argv;
50  // expose a minimal, stable shape only for packages that read it.
51  let argv = rquickjs::Array::new(ctx.clone())?;
52  argv.set(0, "ferridriver")?;
53  argv.set(1, "script")?;
54  p.set("argv", argv)?;
55  p.set("argv0", "ferridriver")?;
56  p.set("pid", i64::from(std::process::id()))?;
57
58  // cwd(): the sandbox root, never the real process cwd (no path leak).
59  let root = cwd.to_string();
60  p.set("cwd", Func::from(move || root.clone()))?;
61
62  // nextTick -> microtask (QuickJS has queueMicrotask via webapi).
63  let next_tick = ctx.eval::<Value<'_>, _>(
64    "((cb, ...a) => { if (typeof cb !== 'function') throw new TypeError('callback required'); \
65       queueMicrotask(() => cb(...a)); })",
66  )?;
67  p.set("nextTick", next_tick)?;
68
69  // stdout/stderr: only `.write(chunk)` — routed into the same console
70  // capture the `console` global feeds (so output surfaces in
71  // `ScriptResult.console[]`), one trailing newline trimmed so a
72  // `write("x\n")` is one line, not a line + blank. Returns `true`
73  // (Node's "not backpressured"). No fd, not a TTY.
74  for (name, level) in [("stdout", "log"), ("stderr", "error")] {
75    let stream = Object::new(ctx.clone())?;
76    let f = rquickjs::Function::new(
77      ctx.clone(),
78      move |c: Ctx<'_>, chunk: Value<'_>| -> rquickjs::Result<bool> {
79        let s = chunk
80          .as_string()
81          .and_then(|v| v.to_string().ok())
82          .or_else(|| chunk.as_number().map(|n| n.to_string()))
83          .unwrap_or_default();
84        let s = s.strip_suffix('\n').unwrap_or(&s).to_string();
85        let console: Object<'_> = c.globals().get("console")?;
86        let sink: rquickjs::Function<'_> = console.get(level)?;
87        sink.call::<_, ()>((s,))?;
88        Ok(true)
89      },
90    )?;
91    stream.set("write", f)?;
92    stream.set("isTTY", false)?;
93    p.set(name, stream)?;
94  }
95
96  // hrtime([prev]) -> [seconds, nanos], monotonic from session start;
97  // hrtime.bigint() -> BigInt nanoseconds (Node parity).
98  let start = Instant::now();
99  let hrtime = rquickjs::Function::new(ctx.clone(), move |prev: Rest<Value<'_>>| -> Vec<i64> {
100    let now = start.elapsed();
101    let (mut s, mut n) = (
102      i64::try_from(now.as_secs()).unwrap_or(i64::MAX),
103      i64::from(now.subsec_nanos()),
104    );
105    if let Some(arr) = prev.0.first().and_then(|v| v.as_array()) {
106      let ps = arr.get::<i64>(0).unwrap_or(0);
107      let pn = arr.get::<i64>(1).unwrap_or(0);
108      s -= ps;
109      n -= pn;
110      if n < 0 {
111        s -= 1;
112        n += 1_000_000_000;
113      }
114    }
115    vec![s, n]
116  })?;
117  // Forward into a generic fn so the `Ctx` and the returned `Value`
118  // share one `'js` (an inline closure gives each its own lifetime).
119  let bigint = rquickjs::Function::new(ctx.clone(), move |c| hrtime_bigint(c, start))?;
120  hrtime.set("bigint", bigint)?;
121  p.set("hrtime", hrtime)?;
122
123  // exit(): never kill the server — surface intent as an error so a
124  // script that relies on it fails loudly instead of silently no-oping.
125  p.set(
126    "exit",
127    Func::from(|code: Rest<Value<'_>>| -> rquickjs::Result<()> {
128      let c = code.0.first().and_then(rquickjs::Value::as_int).unwrap_or(0);
129      Err(rquickjs::Error::new_from_js_message(
130        "process.exit",
131        "Error",
132        format!("process.exit({c}) is not allowed in the ferridriver sandbox"),
133      ))
134    }),
135  )?;
136
137  g.set("process", p)?;
138  Ok(())
139}
140
141/// `process.hrtime.bigint()` — nanoseconds since session start as a
142/// JS `BigInt`. Free fn so the closure's `Ctx`/return share `'js`.
143fn hrtime_bigint(ctx: Ctx<'_>, start: Instant) -> rquickjs::Result<Value<'_>> {
144  let nanos = u64::try_from(start.elapsed().as_nanos()).unwrap_or(u64::MAX);
145  Ok(rquickjs::BigInt::from_u64(ctx, nanos)?.into_value())
146}
147
148fn freeze<'js>(ctx: &Ctx<'js>, obj: &Object<'js>) -> rquickjs::Result<()> {
149  let freeze: rquickjs::Function<'js> = ctx.globals().get::<_, Object<'js>>("Object")?.get("freeze")?;
150  freeze.call::<_, Value<'js>>((obj.clone(),))?;
151  Ok(())
152}