Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/edda

crates/guest-host/src/module.rs

//! **A loaded guest**: verify → digest → compile → tick, then bounded stores and
//! the call shapes, with a trap turned into the bound that caused it.
//!
//! Shape borrowed from holger's plugin host (which borrowed it from znippy's
//! loader): a fresh `Store` per call, `alloc`/`result_len` for the buffer
//! protocol, host functions on a `Linker` the *host* fills in. What is
//! deliberately kept from holger's version is the error handling: every
//! failure is a named `Err` all the way out — a missing export, a trap, a
//! refused allocation, a failed host import — because a host that answered
//! "nothing here" to a trapped module would be a silent empty answer over real
//! data.
//!
//! # The load order, and why it is this order
//!
//! 1. Validate the limits (a `0` is a refusal, not "unlimited").
//! 2. Read the bytes and **verify the signature over them** — BEFORE
//!    compilation, because compiling an unverified module is already running
//!    cranelift over attacker-supplied input.
//! 3. Digest the same bytes: the census name.
//! 4. Compile with both CPU bounds on at the engine level (`from_binary`, not
//!    `from_file`, so a text module is never accepted as a binary one).
//! 5. Check the export set, naming the missing symbol.
//! 6. Start the epoch ticker.
//!
//! Nothing is instantiated at load. What a module may *import* is the host's
//! decision at instantiation, on the `Linker` it hands [`LoadedModule::instantiate`].

use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;

use wasmtime::{Caller, Engine, Extern, Instance, Linker, Memory, Module, Store, Trap};

use crate::convention::{exports, pack, unpack};
use crate::limits::{Bound, BoundedLimiter, EpochTicker, LimitZero, PluginLimits};
use crate::verify::{ModuleTrust, TrustError};

/// What the host lends a guest for the length of one call, as this crate needs
/// to see it. A host's own state struct implements this over two fields.
pub trait GuestState: Send + 'static {
    /// The memory/table ceiling for this call, and the record of whether it
    /// was hit. Lives in the store data because that is the only thing a
    /// `Store::limiter` closure can reach.
    fn limiter(&mut self) -> &mut BoundedLimiter;
    /// Every failure a host import hit, in order. An import can only signal
    /// failure to wasm as a `0`, which a module is free to ignore — so the real
    /// reason is kept here and surfaced after the call returns.
    fn faults(&mut self) -> &mut Vec<String>;
}

/// Why a module did not load. `Display` names the module and what was wrong.
#[derive(Debug)]
pub enum LoadError {
    /// A limit is zero.
    Limits(LimitZero),
    /// The file could not be read.
    Unreadable { path: PathBuf, detail: String },
    /// The trust policy refused it.
    Trust(TrustError),
    /// The bounded engine could not be built.
    Engine { path: PathBuf, detail: String },
    /// Not a loadable wasm binary.
    NotWasm { path: PathBuf, detail: String },
    /// One or more required exports are absent.
    MissingExports { path: PathBuf, missing: Vec<String>, required: Vec<String> },
    /// No exported linear `memory`, so the host cannot pass arguments.
    NoMemory { path: PathBuf },
    /// Imports from a namespace the host does not provide.
    ForeignImports { path: PathBuf, allowed: String, imports: Vec<String> },
}

impl std::fmt::Display for LoadError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            LoadError::Limits(e) => write!(f, "{e}"),
            LoadError::Unreadable { path, detail } => write!(f, "{}: could not be read: {detail}", path.display()),
            LoadError::Trust(e) => write!(f, "{e}"),
            LoadError::Engine { path, detail } => write!(f, "{}: bounded wasm engine: {detail}", path.display()),
            LoadError::NotWasm { path, detail } => write!(f, "{}: not a loadable wasm module: {detail}", path.display()),
            LoadError::MissingExports { path, missing, required } => write!(
                f,
                "{}: not a guest of this kind — missing export(s): {}.\nA module must export all of: {}",
                path.display(),
                missing.join(", "),
                required.join(", ")
            ),
            LoadError::NoMemory { path } => {
                write!(f, "{}: exports no linear `memory`; the host cannot pass arguments to it", path.display())
            }
            LoadError::ForeignImports { path, allowed, imports } => write!(
                f,
                "{}: imports from outside `{allowed}`: {}. A wasm32-unknown-unknown module has no \
                 syscalls to name; anything here is a runtime this host does not provide.",
                path.display(),
                imports.join(", ")
            ),
        }
    }
}

impl std::error::Error for LoadError {}

impl From<TrustError> for LoadError {
    fn from(e: TrustError) -> Self {
        LoadError::Trust(e)
    }
}

impl From<LimitZero> for LoadError {
    fn from(e: LimitZero) -> Self {
        LoadError::Limits(e)
    }
}

/// Why one call into a guest did not answer.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CallError {
    /// A resource bound fired. `refusal` is [`Bound::refusal`], already worded.
    Bound { bound: Bound, refusal: String },
    /// The module trapped for a reason that is not a bound.
    Trap { path: PathBuf, export: String, detail: String },
    /// An export has the wrong signature, or a pointer/length the module
    /// returned points outside its memory, or instantiation failed.
    Shape { path: PathBuf, detail: String },
    /// The call returned, but a host import failed during it. A module that
    /// swallowed the `0` and answered "absent" would otherwise turn a broken
    /// host into an empty answer.
    Faults { path: PathBuf, export: String, faults: Vec<String> },
}

impl std::fmt::Display for CallError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            CallError::Bound { refusal, .. } => write!(f, "{refusal}"),
            CallError::Trap { path, export, detail } => write!(f, "{}: {export} trapped: {detail}", path.display()),
            CallError::Shape { path, detail } => write!(f, "{}: {detail}", path.display()),
            CallError::Faults { path, export, faults } => {
                write!(f, "{}: {export}: a host call failed: {}", path.display(), faults.join("; "))
            }
        }
    }
}

impl std::error::Error for CallError {}

impl CallError {
    /// The bound, when this is one.
    pub fn bound(&self) -> Option<Bound> {
        match self {
            CallError::Bound { bound, .. } => Some(*bound),
            _ => None,
        }
    }
}

/// A store, an instance and its memory: what one call runs in.
pub struct Instantiated<T: 'static> {
    pub store: Store<T>,
    pub instance: Instance,
    pub memory: Memory,
}

/// A `.wasm` verified, digested, compiled and ticking — not yet instantiated.
///
/// `Engine` and `Module` are `Send + Sync`; a `Store` is not, so one is made
/// per call through [`LoadedModule::instantiate`]. A call therefore cannot
/// observe another call's linear memory, which is what makes a `&self` verb
/// sound on a shared host.
pub struct LoadedModule {
    engine: Engine,
    module: Module,
    path: PathBuf,
    limits: PluginLimits,
    /// Keeps the engine's epoch advancing for as long as this module is
    /// loaded. `Arc`, because [`LoadedModule::clone_shared`] mounts one
    /// compiled module under N names and must not start N ticker threads.
    ticker: Arc<EpochTicker>,
    signer: Option<String>,
    digest_hex: String,
    /// Fuel burned by the most recent completed call, and the most any call
    /// has burned since load. wasmtime knows exactly what a call consumed;
    /// this is what lets a ceiling be a measurement rather than a guess.
    fuel_last: AtomicU64,
    fuel_peak: AtomicU64,
}

impl LoadedModule {
    /// Load `path` under `limits` and `trust`, requiring `required_exports`.
    pub fn load(
        path: impl AsRef<Path>,
        limits: &PluginLimits,
        trust: &ModuleTrust,
        required_exports: &[&str],
    ) -> Result<Self, LoadError> {
        let path = path.as_ref().to_path_buf();
        limits.validate()?;
        let bytes = std::fs::read(&path).map_err(|e| LoadError::Unreadable { path: path.clone(), detail: e.to_string() })?;
        Self::from_bytes(path, &bytes, limits, trust, required_exports)
    }

    /// [`LoadedModule::load`] over bytes already in hand. `path` is the label
    /// every refusal names, and where the `.p7s` is looked for.
    pub fn from_bytes(
        path: PathBuf,
        bytes: &[u8],
        limits: &PluginLimits,
        trust: &ModuleTrust,
        required_exports: &[&str],
    ) -> Result<Self, LoadError> {
        limits.validate()?;
        // Provenance BEFORE compilation.
        let signer = trust.verify(&path, bytes)?;
        let digest_hex = crate::census::digest_hex(bytes);

        let engine = Engine::new(&limits.engine_config())
            .map_err(|e| LoadError::Engine { path: path.clone(), detail: e.to_string() })?;
        let module = Module::from_binary(&engine, bytes)
            .map_err(|e| LoadError::NotWasm { path: path.clone(), detail: e.to_string() })?;

        let present: Vec<String> = module.exports().map(|e| e.name().to_string()).collect();
        let missing: Vec<String> = required_exports
            .iter()
            .filter(|want| !present.iter().any(|p| p == *want))
            .map(|s| s.to_string())
            .collect();
        if !missing.is_empty() {
            return Err(LoadError::MissingExports {
                path,
                missing,
                required: required_exports.iter().map(|s| s.to_string()).collect(),
            });
        }
        if !present.iter().any(|p| p == exports::MEMORY) {
            return Err(LoadError::NoMemory { path });
        }

        let (_, tick_period) = limits.epoch_schedule();
        let ticker = Arc::new(EpochTicker::start(&engine, tick_period));
        Ok(Self {
            engine,
            module,
            path,
            limits: limits.clone(),
            ticker,
            signer,
            digest_hex,
            fuel_last: AtomicU64::new(0),
            fuel_peak: AtomicU64::new(0),
        })
    }

    /// Refuse a module that imports from any namespace but `allowed`.
    pub fn refuse_foreign_imports(&self, allowed: &str) -> Result<(), LoadError> {
        let foreign: Vec<String> = self
            .module
            .imports()
            .filter(|i| i.module() != allowed)
            .map(|i| format!("{}::{}", i.module(), i.name()))
            .collect();
        if foreign.is_empty() {
            Ok(())
        } else {
            Err(LoadError::ForeignImports { path: self.path.clone(), allowed: allowed.to_string(), imports: foreign })
        }
    }

    /// The names this module imports from `namespace`, in module order.
    pub fn imports_from(&self, namespace: &str) -> Vec<String> {
        self.module.imports().filter(|i| i.module() == namespace).map(|i| i.name().to_string()).collect()
    }

    /// Every export name.
    pub fn export_names(&self) -> Vec<String> {
        self.module.exports().map(|e| e.name().to_string()).collect()
    }

    /// The same compiled module, sharing engine and ticker. Compiling is the
    /// expensive part; this is free, and it starts no second ticker thread.
    pub fn clone_shared(&self) -> Self {
        Self {
            engine: self.engine.clone(),
            module: self.module.clone(),
            path: self.path.clone(),
            limits: self.limits.clone(),
            ticker: Arc::clone(&self.ticker),
            signer: self.signer.clone(),
            digest_hex: self.digest_hex.clone(),
            fuel_last: AtomicU64::new(self.fuel_last.load(Ordering::Relaxed)),
            fuel_peak: AtomicU64::new(self.fuel_peak.load(Ordering::Relaxed)),
        }
    }

    pub fn path(&self) -> &Path {
        &self.path
    }
    pub fn limits(&self) -> &PluginLimits {
        &self.limits
    }
    pub fn engine(&self) -> &Engine {
        &self.engine
    }
    pub fn module(&self) -> &Module {
        &self.module
    }
    /// The verified signer's common name, or `None` when no provenance was
    /// required. Never `None` because verification failed — that is a
    /// [`LoadError::Trust`], and the module is not here.
    pub fn signer(&self) -> Option<&str> {
        self.signer.as_deref()
    }
    /// Hex blake3-256 of the module bytes, as the signature covers them.
    pub fn digest_hex(&self) -> &str {
        &self.digest_hex
    }
    /// Fuel burned by the most recent completed call.
    pub fn last_call_fuel(&self) -> u64 {
        self.fuel_last.load(Ordering::Relaxed)
    }
    /// The most fuel any one call has burned since load.
    pub fn peak_call_fuel(&self) -> u64 {
        self.fuel_peak.load(Ordering::Relaxed)
    }
    /// The fuel ceiling in force. What [`LoadedModule::peak_call_fuel`] is read against.
    pub fn fuel_ceiling(&self) -> u64 {
        self.limits.fuel
    }

    fn shape(&self, detail: String) -> CallError {
        CallError::Shape { path: self.path.clone(), detail }
    }

    /// Instantiate with `state` and the imports on `linker`: a store with the
    /// limiter, the fuel tank and the epoch deadline installed. There is no
    /// constructor that skips them.
    pub fn instantiate<T: GuestState>(&self, state: T, linker: &Linker<T>) -> Result<Instantiated<T>, CallError> {
        let mut store = Store::new(&self.engine, state);
        store.limiter(|s| s.limiter());
        store.set_fuel(self.limits.fuel).map_err(|e| self.shape(format!("setting CPU fuel: {e}")))?;
        let (ticks, _) = self.limits.epoch_schedule();
        store.set_epoch_deadline(ticks);
        let instance = linker
            .instantiate(&mut store, &self.module)
            .map_err(|e| self.shape(format!("instantiate failed: {e}")))?;
        let memory = instance
            .get_memory(&mut store, exports::MEMORY)
            .ok_or_else(|| self.shape("exports no linear `memory`".to_string()))?;
        Ok(Instantiated { store, instance, memory })
    }

    fn bound_hit<T: GuestState>(&self, store: &mut Store<T>, err: Option<&wasmtime::Error>) -> Option<Bound> {
        if let Some(b) = store.data_mut().limiter().refused() {
            return Some(b);
        }
        let err = err?;
        match err.downcast_ref::<Trap>() {
            Some(Trap::OutOfFuel) => Some(Bound::Fuel),
            Some(Trap::Interrupt) => Some(Bound::Deadline),
            _ => None,
        }
    }

    fn bound_error(&self, bound: Bound, export: &str) -> CallError {
        CallError::Bound { bound, refusal: bound.refusal(&self.path.display().to_string(), export, &self.limits) }
    }

    /// The error for a trapped call: the named bound if one fired, else the trap.
    fn trap_error<T: GuestState>(&self, store: &mut Store<T>, export: &str, err: wasmtime::Error) -> CallError {
        match self.bound_hit(store, Some(&err)) {
            Some(bound) => self.bound_error(bound, export),
            None => CallError::Trap { path: self.path.clone(), export: export.to_string(), detail: err.to_string() },
        }
    }

    /// Record what this call burned. `get_fuel` answers what is LEFT, so the
    /// consumption is the difference; an engine with fuel off answers `Err`,
    /// and then nothing is recorded — an unknown consumption is not zero.
    fn record_fuel<T: 'static>(&self, store: &Store<T>) {
        let Ok(left) = store.get_fuel() else { return };
        let used = self.limits.fuel.saturating_sub(left);
        self.fuel_last.store(used, Ordering::Relaxed);
        self.fuel_peak.fetch_max(used, Ordering::Relaxed);
    }

    /// After a call RETURNED: a bound that fired anyway (a module may swallow
    /// the `-1` from a refused `memory.grow`), then any host-import fault.
    fn settle<T: GuestState>(&self, store: &mut Store<T>, export: &str) -> Result<(), CallError> {
        self.record_fuel(store);
        if let Some(b) = self.bound_hit(store, None) {
            return Err(self.bound_error(b, export));
        }
        let faults = std::mem::take(store.data_mut().faults());
        if !faults.is_empty() {
            return Err(CallError::Faults { path: self.path.clone(), export: export.to_string(), faults });
        }
        Ok(())
    }

    /// Copy `arg` into the guest through its own `alloc`; `0` for an empty arg.
    fn push_arg<T: GuestState>(&self, inst: &mut Instantiated<T>, arg: &[u8]) -> Result<u32, CallError> {
        if arg.is_empty() {
            return Ok(0);
        }
        let alloc = inst
            .instance
            .get_typed_func::<u32, u32>(&mut inst.store, exports::ALLOC)
            .map_err(|e| self.shape(format!("{} is not `(u32) -> u32`: {e}", exports::ALLOC)))?;
        let ptr = alloc.call(&mut inst.store, arg.len() as u32).map_err(|e| self.trap_error(&mut inst.store, exports::ALLOC, e))?;
        inst.memory.write(&mut inst.store, ptr as usize, arg).map_err(|e| {
            self.shape(format!("{} returned ptr {ptr} that cannot hold {} bytes: {e}", exports::ALLOC, arg.len()))
        })?;
        Ok(ptr)
    }

    fn read_bytes<T: 'static>(&self, inst: &mut Instantiated<T>, ptr: u32, len: u32, export: &str) -> Result<Vec<u8>, CallError> {
        // Slice first, copy second: a lying guest's len must never size a host allocation.
        let data = inst.memory.data(&inst.store);
        (ptr as usize)
            .checked_add(len as usize)
            .and_then(|end| data.get(ptr as usize..end))
            .map(<[u8]>::to_vec)
            .ok_or_else(|| {
                self.shape(format!("{export} returned ptr {ptr}/len {len}, outside linear memory of {} bytes", data.len()))
            })
    }

    /// `export() -> u64`. The `plugin_abi` shape.
    pub fn call_u64<T: GuestState>(&self, inst: &mut Instantiated<T>, export: &str) -> Result<u64, CallError> {
        let f = inst
            .instance
            .get_typed_func::<(), u64>(&mut inst.store, export)
            .map_err(|e| self.shape(format!("{export} is not `() -> u64`: {e}")))?;
        let out = f.call(&mut inst.store, ()).map_err(|e| self.trap_error(&mut inst.store, export, e))?;
        self.settle(&mut inst.store, export)?;
        Ok(out)
    }

    /// `export() -> ptr`, length from `result_len()`. The `plugin_manifest` shape.
    pub fn call_ptr<T: GuestState>(&self, inst: &mut Instantiated<T>, export: &str) -> Result<Vec<u8>, CallError> {
        let f = inst
            .instance
            .get_typed_func::<(), u32>(&mut inst.store, export)
            .map_err(|e| self.shape(format!("{export} is not `() -> u32`: {e}")))?;
        let ptr = f.call(&mut inst.store, ()).map_err(|e| self.trap_error(&mut inst.store, export, e))?;
        let len = self.result_len(inst)?;
        let out = self.read_bytes(inst, ptr, len, export)?;
        self.settle(&mut inst.store, export)?;
        Ok(out)
    }

    /// `export(ptr, len) -> ptr`, length from `result_len()`. The shape of a
    /// per-verb export (holger's ABI v1).
    pub fn call_with_ptr<T: GuestState>(&self, inst: &mut Instantiated<T>, export: &str, arg: &[u8]) -> Result<Vec<u8>, CallError> {
        let arg_ptr = self.push_arg(inst, arg)?;
        let f = inst
            .instance
            .get_typed_func::<(u32, u32), u32>(&mut inst.store, export)
            .map_err(|e| self.shape(format!("{export} is not `(u32,u32) -> u32`: {e}")))?;
        let ptr = f.call(&mut inst.store, (arg_ptr, arg.len() as u32)).map_err(|e| self.trap_error(&mut inst.store, export, e))?;
        let len = self.result_len(inst)?;
        let out = self.read_bytes(inst, ptr, len, export)?;
        self.settle(&mut inst.store, export)?;
        Ok(out)
    }

    /// `plugin_call(op, ptr, len) -> ptr << 32 | len`. THE verb shape.
    pub fn call_op<T: GuestState>(&self, inst: &mut Instantiated<T>, op: u32, arg: &[u8]) -> Result<Vec<u8>, CallError> {
        let export = exports::PLUGIN_CALL;
        let arg_ptr = self.push_arg(inst, arg)?;
        let f = inst
            .instance
            .get_typed_func::<(u32, u32, u32), u64>(&mut inst.store, export)
            .map_err(|e| self.shape(format!("{export} is not `(u32,u32,u32) -> u64`: {e}")))?;
        let packed = f
            .call(&mut inst.store, (op, arg_ptr, arg.len() as u32))
            .map_err(|e| self.trap_error(&mut inst.store, export, e))?;
        let (ptr, len) = unpack(packed);
        let out = self.read_bytes(inst, ptr, len, &format!("op {op}"))?;
        self.settle(&mut inst.store, export)?;
        Ok(out)
    }

    fn result_len<T: GuestState>(&self, inst: &mut Instantiated<T>) -> Result<u32, CallError> {
        let f = inst
            .instance
            .get_typed_func::<(), u32>(&mut inst.store, exports::RESULT_LEN)
            .map_err(|e| self.shape(format!("{} is not `() -> u32`: {e}", exports::RESULT_LEN)))?;
        f.call(&mut inst.store, ()).map_err(|e| self.trap_error(&mut inst.store, exports::RESULT_LEN, e))
    }
}

/// Prints WHERE the module came from, WHO signed it and WHAT it may spend —
/// never the engine or the module.
impl std::fmt::Debug for LoadedModule {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("LoadedModule")
            .field("path", &self.path)
            .field("digest", &self.digest_hex)
            .field("signer", &self.signer)
            .field("limits", &self.limits)
            .finish()
    }
}

// ─── Host-import helpers: what an import closure needs to touch guest memory ──

/// Read a `(ptr, len)` pair out of the caller's memory.
pub fn read_mem<T>(caller: &mut Caller<'_, T>, ptr: u32, len: u32) -> Option<Vec<u8>> {
    let memory = match caller.get_export(exports::MEMORY) {
        Some(Extern::Memory(m)) => m,
        _ => return None,
    };
    let end = (ptr as usize).checked_add(len as usize)?;
    memory.data(&*caller).get(ptr as usize..end).map(<[u8]>::to_vec)
}

/// Read a UTF-8 argument, recording a fault (named `what`) if it is unreadable.
pub fn read_str<T: GuestState>(caller: &mut Caller<'_, T>, ptr: u32, len: u32, what: &str) -> Option<String> {
    let Some(raw) = read_mem(caller, ptr, len) else {
        caller.data_mut().faults().push(format!("{what}: argument outside linear memory"));
        return None;
    };
    match String::from_utf8(raw) {
        Ok(s) => Some(s),
        Err(_) => {
            caller.data_mut().faults().push(format!("{what}: argument is not UTF-8"));
            None
        }
    }
}

/// Read a byte argument, recording a fault (named `what`) if it is unreadable.
pub fn read_arg<T: GuestState>(caller: &mut Caller<'_, T>, ptr: u32, len: u32, what: &str) -> Option<Vec<u8>> {
    let out = read_mem(caller, ptr, len);
    if out.is_none() {
        caller.data_mut().faults().push(format!("{what}: argument outside linear memory"));
    }
    out
}

/// Copy `bytes` into the guest through its own `alloc` and return
/// `ptr << 32 | len`. **`0` means the host call failed**, and the reason is
/// in the faults — never "absent".
pub fn write_to_wasm<T: GuestState>(caller: &mut Caller<'_, T>, bytes: &[u8]) -> u64 {
    let alloc = match caller.get_export(exports::ALLOC) {
        Some(Extern::Func(f)) => match f.typed::<u32, u32>(&*caller) {
            Ok(t) => t,
            Err(_) => {
                caller.data_mut().faults().push(format!("{} is not `(u32) -> u32`", exports::ALLOC));
                return 0;
            }
        },
        _ => {
            caller.data_mut().faults().push(format!("module exports no `{}`", exports::ALLOC));
            return 0;
        }
    };
    let ptr = match alloc.call(&mut *caller, bytes.len() as u32) {
        Ok(p) => p,
        Err(e) => {
            caller.data_mut().faults().push(format!("{} trapped: {e}", exports::ALLOC));
            return 0;
        }
    };
    let memory = match caller.get_export(exports::MEMORY) {
        Some(Extern::Memory(m)) => m,
        _ => {
            caller.data_mut().faults().push(format!("module exports no `{}`", exports::MEMORY));
            return 0;
        }
    };
    if let Err(e) = memory.write(&mut *caller, ptr as usize, bytes) {
        caller.data_mut().faults().push(format!("writing host answer into linear memory: {e}"));
        return 0;
    }
    pack(ptr, bytes.len() as u32)
}

/// Record a host-import failure and answer the failure word.
pub fn fault<T: GuestState>(caller: &mut Caller<'_, T>, what: impl Into<String>) -> u64 {
    caller.data_mut().faults().push(what.into());
    0
}