// SPDX-License-Identifier: AGPL-3.0-only
//! The limits a guest runs under (spec §6). Every one of them fails closed:
//! hitting it ends the run as `guest trapped (<which limit>)`.
use std::path::PathBuf;
use std::time::Duration;
/// Per-run limits. Each run gets a fresh store, so these may differ per job.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RunLimits {
/// wasmtime fuel (roughly one unit per wasm instruction).
pub fuel: u64,
/// Wall-clock budget, enforced by epoch interruption.
pub timeout: Duration,
/// Linear memory over all of the guest's memories together, in bytes.
pub memory_bytes: usize,
/// Table elements over all of the guest's tables together.
pub table_elements: usize,
/// Most core instances a guest may create. A component built by
/// wit-component with the emit-file import holds three (the guest's
/// module plus the canonical-ABI shim and fixup modules).
pub instances: usize,
/// Most tables a guest may create.
pub tables: usize,
/// Most linear memories a guest may create.
pub memories: usize,
/// Most files one run may emit.
pub max_files: usize,
/// Largest single emitted file, in bytes.
pub max_file_bytes: usize,
/// Largest sum of all emitted files, in bytes.
pub max_emit_bytes: usize,
/// Largest `note` or `reason` string the guest may return, in bytes.
pub max_text_bytes: usize,
}
impl Default for RunLimits {
fn default() -> Self {
RunLimits {
fuel: 2_000_000_000,
timeout: Duration::from_secs(10),
memory_bytes: 256 << 20,
table_elements: 100_000,
instances: 16,
tables: 16,
memories: 4,
max_files: 256,
max_file_bytes: 64 << 20,
max_emit_bytes: 128 << 20,
max_text_bytes: 64 << 10,
}
}
}
impl From<&heimdall_core::Limits> for RunLimits {
/// The job policy's limits, as the wasm host enforces them. `fuel: None`
/// in the policy means "no fuel metering": the run then has `u64::MAX`
/// fuel and only the wall clock stops it. The note/reason cap is not in
/// the policy and keeps its default.
fn from(l: &heimdall_core::Limits) -> Self {
let sz = |v: u64| usize::try_from(v).unwrap_or(usize::MAX);
RunLimits {
fuel: l.fuel.unwrap_or(u64::MAX),
timeout: l.wall,
memory_bytes: sz(l.memory_bytes),
table_elements: l.table_elements as usize,
instances: l.instances as usize,
tables: l.tables as usize,
memories: l.memories as usize,
max_files: l.max_output_files as usize,
max_file_bytes: sz(l.max_output_file_bytes),
max_emit_bytes: sz(l.max_output_total_bytes),
..RunLimits::default()
}
}
}
/// Host-wide settings: these shape the engine, so they hold for every guest
/// the host loads.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HostConfig {
/// DER root certificates a guest's `.wasm.p7s` must chain to. Never empty:
/// Heimdall has no unsigned mode.
pub trust_roots: Vec<PathBuf>,
/// Most native stack wasm may use, in bytes (deep recursion traps here).
pub stack_bytes: usize,
/// Largest module file the host reads at all, in bytes.
pub max_module_bytes: u64,
/// Epoch tick: the resolution of the wall-clock timeout.
pub epoch_tick: Duration,
}
impl HostConfig {
/// Defaults with the given trust roots.
pub fn new(trust_roots: impl IntoIterator<Item = PathBuf>) -> Self {
HostConfig {
trust_roots: trust_roots.into_iter().collect(),
stack_bytes: 512 << 10,
max_module_bytes: 64 << 20,
epoch_tick: Duration::from_millis(10),
}
}
/// Defaults with the given trust roots, and the stack limit of a job
/// policy (the stack limit is engine-wide, so it is set per host).
pub fn for_policy(
trust_roots: impl IntoIterator<Item = PathBuf>,
limits: &heimdall_core::Limits,
) -> Self {
HostConfig {
stack_bytes: usize::try_from(limits.stack_bytes).unwrap_or(usize::MAX),
..Self::new(trust_roots)
}
}
}