nichlink_plugin_host/wasm.rs
1//! Fuel-metered, import-free Wasm plugin loading and invocation.
2//! 启用燃料计量、无导入的 Wasm 插件加载与调用。
3
4use std::sync::Mutex;
5
6use nichlink_run_method::{PluginAdapter, VerifiedPluginArtifact};
7use wasmi::{
8 Config, EnforcedLimits, Engine, Instance, Linker, Memory, Module, Store, StoreLimits,
9 StoreLimitsBuilder,
10};
11
12use crate::{HostError, PluginInstance};
13
14/// Resource budget applied to every Wasm call.
15/// 每次 Wasm 调用使用的资源预算。
16#[derive(Clone, Copy, Debug)]
17pub struct WasmLimits {
18 /// Linear-memory ceiling per instance, in bytes; growth beyond it traps.
19 /// 每个实例的线性内存上限(字节);超出即触发 trap。
20 pub memory_bytes: usize,
21 /// Fuel granted to each call; an exhausted call traps.
22 /// 每次调用授予的燃料;燃料耗尽时调用触发 trap。
23 ///
24 /// The unit belongs to the engine, not to this host: it is a bound on how
25 /// much work a call may do, not a promise that a given number buys a given
26 /// amount of work. A major engine upgrade can re-scale it, so a host that
27 /// tuned this value against an older engine should re-measure rather than
28 /// assume the same number still fits.
29 /// 该单位属于引擎而不属于本宿主:它是"一次调用最多做多少工作"的上限,而不是"某个数值
30 /// 能买到多少工作"的承诺。引擎大版本升级可能重新标定它,因此针对旧引擎调过这个值的宿主
31 /// 应当重新实测,而不是假设同一个数字仍然够用。
32 pub fuel_per_call: u64,
33 /// Largest request payload accepted, in bytes — but the host writes the input at
34 /// offset 0 of the plugin's *initial* memory before calling the handler, so the real
35 /// ceiling is the smaller of this and that memory. A one-page plugin therefore
36 /// refuses every input above 64 KiB however large this number is; a plugin that needs
37 /// more declares it in its initial memory (or grows it from `start`).
38 /// 接受的最大请求负载字节数——但宿主在调用处理函数之前把输入写在插件**初始**内存的偏移 0
39 /// 处,因此真正的上限是这个值与那块内存中较小的一个。于一页内存的插件无论这里多大都会拒绝
40 /// 超过 64 KiB 的输入;需要更多输入的插件应在初始内存里声明(或在 `start` 里增长)。
41 pub max_input_bytes: usize,
42 /// Largest response payload accepted, in bytes.
43 /// 接受的最大响应负载字节数。
44 pub max_output_bytes: usize,
45 /// Largest number of table elements a module may instantiate.
46 /// 模块可实例化的表元素上限。
47 ///
48 /// The memory ceiling does not bound a table: a table is a separate array of
49 /// function references, instantiated eagerly, so a module that declares
50 /// `(table 100000000 funcref)` costs hundreds of megabytes of host memory
51 /// without touching a single memory page. This is the ceiling for that array,
52 /// and a module over it fails to instantiate rather than being honoured.
53 /// 内存上限并不约束表:表是一块独立的函数引用数组,会即时实例化,因此声明
54 /// `(table 100000000 funcref)` 的模块会花掉宿主数百兆内存,而一页线性内存都没碰。
55 /// 这里是那块数组的上限,超过它的模块实例化失败,而不是被照办。
56 ///
57 /// Measured rather than estimated: a function reference costs the host 8 bytes
58 /// (`plugin-host/tests/wasm_table_cost.rs`), so the default ceiling of 4096 is
59 /// 32 KiB and the hundred-million-entry module above would be 762 MiB. The
60 /// limiter denies the allocation before the table exists, so refusing it was
61 /// measured at 5 KiB of peak allocation, not 762 MiB. This field is the only
62 /// bound on a single table's size: wasmi's `EnforcedLimits::strict()`, which
63 /// `WasmBackend::load` also applies, caps how many tables a module may declare
64 /// (`max_tables`) and how many element segments it may carry
65 /// (`max_element_segments`), but not how large one table may grow.
66 /// 实测而非估计:一个函数引用在宿主一侧占 8 字节(`plugin-host/tests/wasm_table_cost.rs`),
67 /// 因此默认上限 4096 是 32 KiB,而上面那个一亿条目的模块本来会是 762 MiB。限制器在表存在
68 /// 之前就拒绝这次分配,因此拒绝它的实测峰值是 5 KiB,而不是 762 MiB。本字段是单张表大小的
69 /// 唯一约束:wasmi 的 `EnforcedLimits::strict()`(`WasmBackend::load` 也会施加)限制的是一个
70 /// 模块可以声明多少张表(`max_tables`)与多少个元素段(`max_element_segments`),而不是
71 /// 单张表能长到多大。
72 pub table_elements: usize,
73 /// Largest total element-section payload a module may carry, in bytes.
74 /// 模块可携带的元素段负载总量上限,以字节计。
75 ///
76 /// A passive element segment is invisible to the two ceilings that look like
77 /// they cover it: `table_elements` bounds a table's *growth* and a passive
78 /// segment never grows one, while wasmi's `EnforcedLimits::strict()` caps how
79 /// many element *segments* a module may declare, not how many entries they
80 /// carry. wasmi materializes every entry at instantiation, measured at about
81 /// 32 bytes per entry against about one byte per entry in the compact
82 /// encoding — a 2 000 103-byte module carrying two million entries cost
83 /// 64 070 402 bytes of host memory, with `(table 1 funcref)` and default
84 /// limits. This budget is therefore also an allocation budget of roughly
85 /// thirty-two times it, which is why the default caps that shape at about
86 /// 8 MiB. The payload is measured from the binary's section headers before
87 /// compilation, the same hand-checked seam `max_module_bytes` uses, because no
88 /// engine limit applies before the engine runs.
89 /// 被动元素段对两道看起来覆盖它的上限都不可见:`table_elements` 约束的是表的**增长**,
90 /// 而被动段从不增长表;wasmi 的 `EnforcedLimits::strict()` 限制的是一个模块可以声明多少个
91 /// 元素**段**,而不是它们携带多少条目。wasmi 在实例化时为每个条目物化约 32 字节,而紧凑编码
92 /// 下每条约一字节——一个 2 000 103 字节、带两百万条目的模块,在 `(table 1 funcref)` 与默认
93 /// 上限下花掉 64 070 402 字节宿主内存。因此本预算同时也是约三十二倍的分配预算,这正是默认值
94 /// 把那种形状压在约 8 MiB 的原因。负载在编译前从二进制的段头量出,与 `max_module_bytes`
95 /// 用的是同一处手工检查接缝,因为在引擎运行之前没有任何引擎限制生效。
96 pub max_element_bytes: usize,
97 /// Largest artifact the backend will compile, in bytes.
98 /// 后端愿意编译的最大工件字节数。
99 ///
100 /// Compilation happens before any limit below can apply, so this is the one
101 /// bound that has to be checked by hand; it is what keeps a huge artifact from
102 /// spending the host's memory and time before the sandbox is even entered.
103 /// 编译发生在下面任何限制生效之前,因此这是唯一必须手工检查的上限;正是它阻止一个巨大
104 /// 工件在沙箱都没进入之前就花掉宿主的内存与时间。
105 pub max_module_bytes: usize,
106}
107
108impl Default for WasmLimits {
109 fn default() -> Self {
110 Self {
111 memory_bytes: 16 * 1024 * 1024,
112 fuel_per_call: 1_000_000,
113 max_input_bytes: 1024 * 1024,
114 max_output_bytes: 1024 * 1024,
115 table_elements: 4096,
116 max_element_bytes: 256 * 1024,
117 max_module_bytes: 16 * 1024 * 1024,
118 }
119 }
120}
121
122/// Loads Wasm without WASI or host imports.
123/// 在没有 WASI 和宿主导入的环境中加载 Wasm。
124#[derive(Clone, Copy, Debug, Default)]
125pub struct WasmBackend {
126 limits: WasmLimits,
127}
128
129impl WasmBackend {
130 /// Build a backend that applies `limits` to every loaded instance.
131 /// 构造一个对每个已加载实例施加 `limits` 的后端。
132 pub const fn new(limits: WasmLimits) -> Self {
133 Self { limits }
134 }
135
136 /// Compile and instantiate the artifact, checking the limits, the host ABI version
137 /// *when the plugin exports one* (a missing `nichlink_abi_version` is a legacy
138 /// plugin, as `README.md` says), and the `nichlink_health` export.
139 /// 编译并实例化工件,检查各项上限、宿主 ABI 版本(**当插件导出它时**;缺少
140 /// `nichlink_abi_version` 的是旧插件,见 `README.md`)以及 `nichlink_health` 导出。
141 ///
142 /// This performs **no slot policy**: channel, framework, mode and flow are checked by
143 /// [`WasmPluginTable::install`](crate::WasmPluginTable::install) and
144 /// `validate_artifact`. A host that calls the backend directly gets the wasm limits
145 /// and nothing else.
146 /// 本方法**不做槽位策略**:通道、框架、模式与 flow 由
147 /// [`WasmPluginTable::install`](crate::WasmPluginTable::install) 与 `validate_artifact`
148 /// 检查。直接调用后端的宿主只得到 wasm 上限,别的什么都没有。
149 pub fn load(&self, artifact: VerifiedPluginArtifact) -> Result<WasmInstance, HostError> {
150 let (_, bytes) = artifact.into_parts();
151 if bytes.len() > self.limits.max_module_bytes {
152 return Err(HostError::Limit(format!(
153 "artifact is {} bytes; limit is {}",
154 bytes.len(),
155 self.limits.max_module_bytes
156 )));
157 }
158 // A passive element segment never reaches `table_growing`, so the store's
159 // element ceiling does not see it: this is the only bound on what a
160 // module can make wasmi materialize at instantiation.
161 // 被动元素段永远到不了 `table_growing`,因此 store 的元素上限看不到它:这是对"模块能
162 // 让 wasmi 在实例化时物化多少"的唯一边界。
163 let element_bytes = element_section_bytes(&bytes);
164 if element_bytes > self.limits.max_element_bytes {
165 return Err(HostError::Limit(format!(
166 "module declares {element_bytes} bytes of element segments; limit is {}",
167 self.limits.max_element_bytes
168 )));
169 }
170 let mut config = Config::default();
171 config.consume_fuel(true);
172 // wasmi's own strict limits bound what a module may contain and refuse one
173 // whose functions could be compiled lazily; its defaults leave all of that
174 // unlimited, so a small artifact could otherwise buy unbounded compile-time
175 // work. `strict` is wasmi's number, not one invented here.
176 // wasmi 自带的 strict 限制约束模块可以包含什么,并拒绝那些函数可能被惰性编译的模块;
177 // 它的默认值把这一切都留成无限,因此一个很小的工件本来可以换来无界的编译期工作量。
178 // `strict` 是 wasmi 自己的数值,不是这里编的。
179 config.enforced_limits(EnforcedLimits::strict());
180 let engine = Engine::new(&config);
181 let module = Module::new(&engine, &bytes)
182 .map_err(|error| HostError::InvalidArtifact(error.to_string()))?;
183 let store_limits = StoreLimitsBuilder::new()
184 .memory_size(self.limits.memory_bytes)
185 .table_elements(self.limits.table_elements)
186 .instances(1)
187 .memories(1)
188 .tables(1)
189 .trap_on_grow_failure(true)
190 .build();
191 let mut store = Store::new(&engine, store_limits);
192 store.limiter(|limits| limits);
193 store
194 .set_fuel(self.limits.fuel_per_call)
195 .map_err(|error| HostError::Limit(error.to_string()))?;
196 let linker = Linker::new(&engine);
197 // `instantiate_and_start` is what `instantiate` plus `PreInstance::start`
198 // became in wasmi 1.x; it runs the module's `start` function, which is
199 // why the fuel above is set before it. A plugin cannot dodge its budget
200 // by doing work during instantiation.
201 // `instantiate_and_start` 就是 `instantiate` 加 `PreInstance::start` 在 wasmi 1.x
202 // 中的形态;它会运行模块的 `start` 函数,这也正是上面那笔燃料必须在此之前设定好的
203 // 原因。插件无法靠把工作放进实例化阶段来逃避预算。
204 let instance = linker
205 .instantiate_and_start(&mut store, &module)
206 .map_err(|error| HostError::InvalidArtifact(error.to_string()))?;
207 if let Ok(abi) = instance.get_typed_func::<(), i32>(&store, "nichlink_abi_version") {
208 let version = abi
209 .call(&mut store, ())
210 .map_err(|error| HostError::Abi(error.to_string()))?;
211 if version != nichlink_run_method::PLUGIN_ABI_VERSION as i32 {
212 return Err(HostError::Abi(format!(
213 "plugin ABI version {version} is incompatible with host ABI {}",
214 nichlink_run_method::PLUGIN_ABI_VERSION
215 )));
216 }
217 }
218 let memory = instance
219 .get_memory(&store, "memory")
220 .ok_or_else(|| HostError::Abi("missing exported memory `memory`".to_owned()))?;
221 instance
222 .get_typed_func::<(i32, i32), i64>(&store, "nichlink_health")
223 .map_err(|_| HostError::Abi("missing `nichlink_health(i32, i32) -> i64`".to_owned()))?;
224
225 Ok(WasmInstance {
226 state: Mutex::new(WasmState {
227 store,
228 instance,
229 memory,
230 }),
231 limits: self.limits,
232 })
233 }
234}
235
236/// Total element-section payload in a wasm binary, in bytes.
237/// wasm 二进制里元素段负载的总字节数。
238///
239/// Only the section headers are walked — `id`, LEB128 size, payload — which is
240/// enough to bound the entries, because every entry costs at least one byte of
241/// payload. A binary this walk cannot make sense of reports `0` and is left to the
242/// engine, whose diagnostic for a malformed module is better than a budget
243/// refusal would be.
244/// 只走段头——`id`、LEB128 长度、负载——这已足以约束条目数,因为每个条目至少占一字节负载。
245/// 本遍历读不懂的二进制报 `0` 并留给引擎:它对"模块损坏"的诊断比一条预算拒绝更有用。
246fn element_section_bytes(bytes: &[u8]) -> usize {
247 /// Magic (4) plus version (4).
248 /// 魔数(4)加版本(4)。
249 const HEADER: usize = 8;
250 /// Section id of the element section.
251 /// 元素段的段 id。
252 const ELEMENT_SECTION: u8 = 9;
253 if bytes.len() < HEADER || &bytes[..4] != b"\0asm" {
254 return 0;
255 }
256 let mut index = HEADER;
257 let mut total = 0usize;
258 while index < bytes.len() {
259 let id = bytes[index];
260 index += 1;
261 let Some((size, next)) = read_uleb(bytes, index) else {
262 return 0;
263 };
264 let size = size as usize;
265 if next + size > bytes.len() {
266 return 0;
267 }
268 if id == ELEMENT_SECTION {
269 total += size;
270 }
271 index = next + size;
272 }
273 total
274}
275
276/// One unsigned LEB128 value at `start`, with the index after it.
277/// `start` 处的一个无符号 LEB128 取值,以及它之后的索引。
278fn read_uleb(bytes: &[u8], start: usize) -> Option<(u32, usize)> {
279 let mut value = 0u32;
280 let mut shift = 0u32;
281 let mut index = start;
282 while index < bytes.len() {
283 let byte = bytes[index];
284 value |= u32::from(byte & 0x7f).checked_shl(shift)?;
285 index += 1;
286 if byte & 0x80 == 0 {
287 return Some((value, index));
288 }
289 shift += 7;
290 if shift >= 32 {
291 return None;
292 }
293 }
294 None
295}
296
297struct WasmState {
298 store: Store<StoreLimits>,
299 instance: Instance,
300 memory: Memory,
301}
302
303/// A loaded, fuel-metered Wasm plugin.
304/// 已加载并启用燃料计量的 Wasm 插件。
305pub struct WasmInstance {
306 state: Mutex<WasmState>,
307 limits: WasmLimits,
308}
309
310impl PluginInstance for WasmInstance {
311 fn adapter(&self) -> PluginAdapter {
312 PluginAdapter::Wasm
313 }
314
315 fn call(&self, operation: &str, input: &[u8]) -> Result<Vec<u8>, HostError> {
316 if input.len() > self.limits.max_input_bytes {
317 return Err(HostError::Limit(format!(
318 "input is {} bytes; limit is {}",
319 input.len(),
320 self.limits.max_input_bytes
321 )));
322 }
323 let export = operation_export(operation)?;
324 let mut state = self
325 .state
326 .lock()
327 .map_err(|_| HostError::Process("Wasm store lock was poisoned".to_owned()))?;
328 state
329 .store
330 .set_fuel(self.limits.fuel_per_call)
331 .map_err(|error| HostError::Limit(error.to_string()))?;
332 let memory = state.memory;
333 memory
334 .write(&mut state.store, 0, input)
335 .map_err(|error| HostError::Abi(error.to_string()))?;
336 let instance = state.instance;
337 let function = instance
338 .get_typed_func::<(i32, i32), i64>(&state.store, &export)
339 .map_err(|_| HostError::Abi(format!("missing `{export}(i32, i32) -> i64`")))?;
340 let packed = function
341 .call(&mut state.store, (0, input.len() as i32))
342 .map_err(|error| HostError::Process(error.to_string()))? as u64;
343 let offset = (packed >> 32) as usize;
344 let length = (packed & u64::from(u32::MAX)) as usize;
345 if length > self.limits.max_output_bytes {
346 return Err(HostError::Limit(format!(
347 "output is {length} bytes; limit is {}",
348 self.limits.max_output_bytes
349 )));
350 }
351 let end = offset
352 .checked_add(length)
353 .ok_or_else(|| HostError::Abi("output range overflowed".to_owned()))?;
354 if end > memory.data_size(&state.store) {
355 return Err(HostError::Abi(format!(
356 "output range {offset}..{end} is outside linear memory"
357 )));
358 }
359 let mut output = vec![0; length];
360 memory
361 .read(&state.store, offset, &mut output)
362 .map_err(|error| HostError::Abi(error.to_string()))?;
363 Ok(output)
364 }
365}
366
367fn operation_export(operation: &str) -> Result<String, HostError> {
368 if nichlink_run_method::validate_operation_name(operation).is_err() {
369 return Err(HostError::InvalidOperation(operation.to_owned()));
370 }
371 Ok(format!("nichlink_{operation}"))
372}