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

            
10
use wasmtime::{Caller, Extern, Linker};
11

            
12
use crate::session::SessionData;
13

            
14
2558
pub fn register(linker: &mut Linker<SessionData>) -> wasmtime::Result<()> {
15
2558
    linker.func_wrap(
16
2558
        "env",
17
2558
        "log",
18
6
        |mut caller: Caller<'_, SessionData>, level: u32, ptr: u32, len: u32| {
19
6
            let Some(Extern::Memory(memory)) = caller.get_export("memory") else {
20
                return;
21
            };
22
6
            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
6
            let start = ptr as usize;
27
6
            let Some(end) = start.checked_add(len as usize) else {
28
                return;
29
            };
30
6
            let Some(bytes) = data.get(start..end) else {
31
                return;
32
            };
33
6
            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
6
            caller.data().push_output(msg);
40
6
            match level {
41
6
                0 => tracing::debug!("[script] {msg}"),
42
                1 => tracing::info!("[script] {msg}"),
43
                2 => tracing::warn!("[script] {msg}"),
44
                _ => tracing::error!("[script] {msg}"),
45
            }
46
6
        },
47
    )?;
48
2558
    Ok(())
49
2558
}
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.
57
101
pub fn register_silent(linker: &mut Linker<SessionData>) -> wasmtime::Result<()> {
58
101
    linker.func_wrap(
59
101
        "env",
60
101
        "log",
61
        |_caller: Caller<'_, SessionData>, _level: u32, _ptr: u32, _len: u32| {},
62
    )?;
63
101
    Ok(())
64
101
}