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}