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}