Skip to main content

Module resources

Module resources 

Source
Expand description

Host-process resource bounds for every subprocess this runtime spawns.

Sibling of crate::limits, and deliberately not a replacement for it: that module bounds a WASM guest inside its wasmtime store (linear memory, tables, epoch deadline), this one bounds the operating-system processes the runtime forks — shell::execute_shell and runtime::dispatch_native_tool. A capsule that cannot escape its containment can still wedge the host it runs on: fork bombs, unbounded open files, unbounded writes, unbounded per-process CPU and address space, and unbounded workdir growth are all denial of service, not boundary defeats — nothing outside the granted scope is read, written, or reached — but a wedged host is still a broken host.

Three mechanisms, in descending order of portability:

  • setrlimit(2) ceilings ([apply_hard_rlimits]), applied inside the forked child’s pre_exec window on every Unix platform. Per-process, POSIX-portable, no configuration required of the host.
  • a cgroup v2 scope ([crate::cgroup]) around the whole subprocess tree, Linux only. This is what rlimits structurally cannot do: RLIMIT_NPROC is a per-uid ceiling, checked against every thread the uid owns on the host, so it bounds neither this tree nor anything else in particular. pids.max on a cgroup counts only the scope’s own tasks, and a spawn that joins a scope gets no RLIMIT_NPROC from this module at all ([NprocBound]).
  • a periodic workdir-size check ([WorkdirGuard]), on every platform. A plain filesystem walk on a fixed cadence, so a breach is caught within one poll interval — not instantly.

§A silent manifest: no CPU limit, a default for everything else

HostResourceLimits::resolve reads the manifest’s capabilities.resources block, and treats the whole block’s absence as “every field omitted”. The two CPU fields carry over as declared: an omitted cpu_seconds sets no RLIMIT_CPU, so a subprocess keeps the limit this process inherited, and an omitted cgroup_cpu_percent writes no cpu.max, so the scope keeps the kernel’s max. Both change how fast and how long an agent’s work may run, not whether the host survives it. Every other field replaces an omission with its DEFAULT_* constant, as crate::limits::ExecutionLimits::resolve does: a capsule that declares none of them is bounded identically to one that declares every default by hand.

§Hard limits, not soft ones

setrlimit(2) takes both a soft (rlim_cur) and a hard (rlim_max) value, and an unprivileged process may raise its own soft limit up to the hard one at any time. Against a hostile agent a soft-only cap is therefore advisory: one setrlimit call from inside the capsule undoes it. Every limit this module sets writes rlim_cur == rlim_max, leaving no ceiling to raise into, and lowering rlim_max is irreversible for an unprivileged process.

A set RLIMIT_CPU is the one exception: its soft slot is cpu_seconds and its hard slot one second above ([cpu_rlimit_slots]). The kernel sends SIGXCPU, the signal [limit_from_signal] attributes, only while the soft slot sits below the hard one. The bound is the hard slot: a process that raises its soft slot to the hard one, or ignores SIGXCPU, gets SIGKILL at cpu_seconds + 1, so CPU time per process stays within cpu_seconds plus one second.

Structs§

HostResourceLimits
Fully-resolved host-process limits for one session: the manifest’s capabilities.resources block with every omitted field replaced by its DEFAULT_*, except the two CPU fields, which stay None when omitted.

Constants§

DEFAULT_CGROUP_IO_BYTES_PER_SEC
Default cgroup io.max read+write throughput on the workdir’s backing device — 100 MiB/s.
DEFAULT_CGROUP_MEMORY_BYTES
Default cgroup memory.max — 4 GiB aggregate across the whole subprocess tree. Above the per-process ceiling on purpose: the tree bound exists to catch what many processes do together, so setting it at or below the single-process bound would make it redundant.
DEFAULT_CGROUP_PIDS_MAX
Default cgroup pids.max — 256 tasks aggregate across the whole subprocess tree.
DEFAULT_MAX_FILE_SIZE_BYTES
Default RLIMIT_FSIZE hard ceiling — 4 GiB, the largest single file a subprocess may write.
DEFAULT_MAX_OPEN_FILES
Default RLIMIT_NOFILE hard ceiling — open descriptors per spawned subprocess.
DEFAULT_MAX_PROCESSES
Default RLIMIT_NPROC headroom for a spawn with no cgroup scope — how much past the uid’s count at that spawn a subprocess tree may add, in whichever unit this platform’s RLIMIT_NPROC is checked against. Unused for a spawn that joins a scope. See [NprocBound] for which spawns get it and [uid_task_count] for the per-platform unit.
DEFAULT_MEMORY_BYTES
Default RLIMIT_AS (Linux) / RLIMIT_DATA (macOS) hard ceiling — 2 GiB per subprocess.
DEFAULT_WORKDIR_MAX_BYTES
Default ceiling on total workdir size — 10 GiB, checked periodically.
V8_ADDRESS_SPACE_FLOOR_BYTES
The address-space limit below which W-RUN-010 warns for a V8 binary: 32 GiB. V8 reserves far more address space than it uses, and node failed with “Cannot allocate Wasm memory” at 16 GiB and ran at 32 GiB under ulimit -v.
V8_BINARY_NAMES
The capabilities.shell.allow file names W-RUN-010 treats as V8.

Functions§

v8_address_space_warning
W-RUN-010 as staging and mur doctor print it, or None when it does not fire.