Skip to main content

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}