Skip to main content

luna_core/vm/
sandbox.rs

1//! Sandbox builder (B1, Phase 2 P2-A) — replaces the 5-line manual
2//! setter sequence with a fluent builder.
3//!
4//! Conservative defaults (per `.dev/rfcs/v1.1-rfc-b-ergo.md` §B1):
5//!
6//! - **No stdlib opened** — the embedder explicitly opts in to each
7//!   safe-by-default subset (`open_base`, `open_math`, `open_string`,
8//!   `open_table`, `open_coroutine`). `os` / `io` / `debug` / `package`
9//!   are intentionally not exposed on the builder; trusted embedders
10//!   call `vm.open_io()` etc. directly on the built Vm.
11//! - **Bytecode loading off** — `string.dump` / `load(bytecode)` are
12//!   rejected. Reverse with `allow_bytecode_loading()` for trusted hosts.
13//! - **No instruction or memory budget** — embedders set explicit caps
14//!   via `with_instr_budget` / `with_memory_cap`.
15//! - **JIT default follows the crate** — `luna-core` builds default to
16//!   `NullJitBackend` (interpreter only); `luna` builds default to
17//!   `CraneliftBackend` via `Vm::new_minimal_with_jit`. The builder
18//!   itself does not flip the JIT switch.
19//!
20//! ```
21//! use luna_core::vm::Vm;
22//! use luna_core::version::LuaVersion;
23//!
24//! let mut vm = Vm::sandbox(LuaVersion::Lua54)
25//!     .open_base()
26//!     .open_math()
27//!     .with_instr_budget(1_000_000)
28//!     .build();
29//!
30//! let r = vm.eval("return 1 + 2").unwrap();
31//! assert_eq!(r.len(), 1);
32//! ```
33
34use crate::version::LuaVersion;
35use crate::vm::exec::Vm;
36
37/// Fluent builder for a sandboxed `Vm`. Construct via [`Vm::sandbox`].
38///
39/// Methods take `self` (consume) and return `Self` (chain). Terminal
40/// method is [`SandboxBuilder::build`].
41#[derive(Debug, Clone, Copy)]
42pub struct SandboxBuilder {
43    version: LuaVersion,
44    base: bool,
45    math: bool,
46    string: bool,
47    table: bool,
48    coroutine: bool,
49    instr_budget: Option<i64>,
50    memory_cap: Option<usize>,
51    bytecode_loading: bool,
52}
53
54impl SandboxBuilder {
55    /// Create a builder with conservative defaults.
56    pub(crate) fn new(version: LuaVersion) -> Self {
57        SandboxBuilder {
58            version,
59            base: false,
60            math: false,
61            string: false,
62            table: false,
63            coroutine: false,
64            instr_budget: None,
65            memory_cap: None,
66            bytecode_loading: false,
67        }
68    }
69
70    /// Open the `base` library (`print`, `pcall`, `type`, etc.).
71    pub fn open_base(mut self) -> Self {
72        self.base = true;
73        self
74    }
75
76    /// Open the `math` library.
77    pub fn open_math(mut self) -> Self {
78        self.math = true;
79        self
80    }
81
82    /// Open the `string` library (no `string.dump` exposure unless
83    /// bytecode loading is also allowed).
84    pub fn open_string(mut self) -> Self {
85        self.string = true;
86        self
87    }
88
89    /// Open the `table` library.
90    pub fn open_table(mut self) -> Self {
91        self.table = true;
92        self
93    }
94
95    /// Open the `coroutine` library.
96    pub fn open_coroutine(mut self) -> Self {
97        self.coroutine = true;
98        self
99    }
100
101    /// Cap dispatched instructions to `n` per `call_value` invocation.
102    /// Beyond `n`, the Vm raises a catchable "instruction budget
103    /// exceeded" error so the embedder can yield control.
104    pub fn with_instr_budget(mut self, n: i64) -> Self {
105        self.instr_budget = Some(n);
106        self
107    }
108
109    /// Cap heap allocation to `n` bytes (approximate — see
110    /// crate-level docs on `heap.bytes()` accuracy).
111    pub fn with_memory_cap(mut self, n: usize) -> Self {
112        self.memory_cap = Some(n);
113        self
114    }
115
116    /// Allow `load(bytecode)` / `string.dump`. Untrusted scripts
117    /// should NOT have this; precompiled chunks bypass the parser's
118    /// safety gates.
119    pub fn allow_bytecode_loading(mut self) -> Self {
120        self.bytecode_loading = true;
121        self
122    }
123
124    /// Finalize and return the `Vm`.
125    pub fn build(self) -> Vm {
126        let mut vm = Vm::new_minimal(self.version);
127        if self.base {
128            vm.open_base();
129        }
130        if self.math {
131            vm.open_math();
132        }
133        if self.string {
134            vm.open_string();
135        }
136        if self.table {
137            vm.open_table();
138        }
139        if self.coroutine {
140            vm.open_coroutine();
141        }
142        vm.set_instr_budget(self.instr_budget);
143        if let Some(cap) = self.memory_cap {
144            vm.set_memory_cap(Some(cap));
145        }
146        vm.set_bytecode_loading(self.bytecode_loading);
147        vm
148    }
149}
150
151impl Vm {
152    /// Start a [`SandboxBuilder`] for a fresh, sandboxed Vm. See
153    /// [`SandboxBuilder`] for defaults.
154    pub fn sandbox(version: LuaVersion) -> SandboxBuilder {
155        SandboxBuilder::new(version)
156    }
157}