Skip to main content

rpc/natives/
env_io.rs

1//! Host output channel for the eval-mode module.
2//!
3//! PRINT / DISPLAY / NEWLINE / DEBUG lower to a call into the `env.log`
4//! import (`(i32 level, i32 ptr, i32 len) -> ()`), reading the message from
5//! the guest's exported linear memory. Script mode wires this in
6//! `scripting::host`; the rpc Session path needs the same import or those
7//! natives can't compile. Output is routed to `tracing` (the rpc channel has
8//! no separate stdout) — same sink and level mapping as the script-mode host.
9
10use wasmtime::{Caller, Extern, Linker};
11
12use crate::session::SessionData;
13
14pub fn register(linker: &mut Linker<SessionData>) -> wasmtime::Result<()> {
15    linker.func_wrap(
16        "env",
17        "log",
18        |mut caller: Caller<'_, SessionData>, level: u32, ptr: u32, len: u32| {
19            let Some(Extern::Memory(memory)) = caller.get_export("memory") else {
20                return;
21            };
22            let data = memory.data(&caller);
23            // `ptr`/`len` are guest-controlled; compute the end with a checked
24            // add so a hostile/overflowing range is rejected (not wrapped in
25            // release / panicked in debug) before the slice.
26            let start = ptr as usize;
27            let Some(end) = start.checked_add(len as usize) else {
28                return;
29            };
30            let Some(bytes) = data.get(start..end) else {
31                return;
32            };
33            let Ok(msg) = std::str::from_utf8(bytes) else {
34                return;
35            };
36            // Capture into the per-request buffer (for the SLYNK mREPL's
37            // `:write-string`) AND tee to tracing (the sshd / text-REPL paths,
38            // which don't drain the buffer, still see it in logs).
39            caller.data().push_output(msg);
40            match level {
41                0 => tracing::debug!("[script] {msg}"),
42                1 => tracing::info!("[script] {msg}"),
43                2 => tracing::warn!("[script] {msg}"),
44                _ => tracing::error!("[script] {msg}"),
45            }
46        },
47    )?;
48    Ok(())
49}
50
51/// Links a NO-OP `env.log` for the render surface. Eval-mode modules always
52/// import `env.log` (PRINT/DISPLAY/DEBUG lower to it), so the import must
53/// resolve — but a template is untrusted source with no legitimate output
54/// channel, so render discards the message entirely: no per-request buffer
55/// append, no `tracing` tee. This closes the log-flood / unbounded-host-memory
56/// side channel a real `env.log` would hand an attacker-controlled template.
57pub fn register_silent(linker: &mut Linker<SessionData>) -> wasmtime::Result<()> {
58    linker.func_wrap(
59        "env",
60        "log",
61        |_caller: Caller<'_, SessionData>, _level: u32, _ptr: u32, _len: u32| {},
62    )?;
63    Ok(())
64}