Labs first execution profile: decision and threat model
Historical record. The
rullst-labs-runnerimplementation described here was removed from the workspace for 13.0 (maintainer decision of 30 September 2026); its source remains in git history, and its CI jobs and helper scripts were removed with it.rullst-labsstill defines therust-function-wasm-v1profile, but Rullst no longer ships an executor: applications bring their own runner through the controller contract. Read the requirements below as a reference baseline for such a runner, not as shipped or currently tested software.
Status: implementation decision for an unpublished experimental candidate. The named Linux journey has hosted evidence below; independent security review and production readiness are not claimed. The September 23 freeze and September 24–26 validation window applied when this decision was recorded. Contracts and an offline simulation are intermediate work.
Recorded Linux acceptance
On September 21, run 35582398251
passed at 977e40a31393b6403c960919b9ea72c54813ef5a on disposable Ubuntu 24.04
with the pinned Rust 1.96 profile. Its minimized artifact records 26 checks:
actual isolation, compiler descriptor denial, shared kernel group capacity,
compilation/grading, ownership/idempotency, wrong answers, fuel/stack/memory
boundaries, compiler errors, filesystem/environment/import/output denial, fresh
guest state, compiler deadline/cleanup, cancellation, controller loss, recovery at full group capacity,
competing controllers and terminal source removal. Separate real Landlock and
seccomp regressions also passed. The same run repeated all 26 checks with
instrumentation and generated the scoped controller coverage report. Both
ordinary and instrumented artifacts preserve the named profile and outstanding
independent-review boundary. No live provider account was used.
The compiler-deadline case observes a real compiler before requiring the controller’s own five-second deadline to cancel the attempt and remove its entire group; neither an application cancellation nor a test-owned kill can satisfy it. Excess compiler output must produce the bounded resource-limit outcome, and retained diagnostics may not exceed the protocol byte limit.
PR #228 subsequently passed source
admission on the same head and merged into v13 at 278a115b:
- Native CI passed all 24 test shards across Linux, macOS and Windows, strict Clippy, formatting, database contracts and the evidence scorecard. All 43 branch-required checks passed; CodeQL reported no open alerts on the PR.
- The actual installed-archive job
audited 21 archives, exercised extracted Media/Labs contracts and the
independent application consumer, installed CLI
13.0.0-alpha.1and compiled all six blueprints. The application consumer has no executor dependency. - Combined coverage passed both exact 90% floors: 101,088/112,148 whole-repository lines (90.1380%) and 72,607/79,958 framework-library lines (90.8064%). The PR checkout tree equals the candidate’s tree.
Codecov’s separate, non-required patch check still reports 83.45936% against its 90% target, with 525 changed lines unmeasured. Compiler, worker and probe paths account for much of that gap: their sandbox counters deliberately are not exported. Passing isolated acceptance does not establish which missing lines ran. Preserve this quality limitation for follow-up without weakening isolation, excluding production paths or claiming that every check passed.
This evidence applies to that source revision and named host/profile. The final release campaign and independent isolation review remain separate requirements. It does not prove every adversarial case or every deployment environment.
Coverage must also measure the trusted controller’s lifecycle paths. The
instrumented fixture retains identical controller/worker executable hashes and
all namespace, environment, filesystem and resource checks. A CI-only LLVM hook
initializes profile output only for a trusted host with an explicit output path;
in the worker’s cleared environment it performs no profiling initialization.
Only trusted host processes write profiler data outside the sandbox; the worker
does not receive profiling variables or a host output directory. These counters
are merged with ordinary workspace coverage, without changing either 90% floor.
The scoped manual labs-isolation measurement diagnoses that path and cannot
replace the complete coverage or release campaign.
Supported journey
An authorized instructor creates an immutable exercise revision with a bounded exact grader. A currently authorized learner submits Rust source to a dedicated durable job. A separately deployed runner compiles and evaluates that submission under one named Linux profile. The application exposes authorized status, result and cancellation, with durable restart/lease recovery and bounded retention.
The first exercise is a pure function with the fixed ABI
solve(i64, i64) -> i64, evaluated over at most 64 instructor-defined cases.
Rust 1.96.0, the wasm32-unknown-unknown standard-library/toolchain files and
the runner artifact and minimal dynamic runtime tree must be pinned and checked. Submission compilation uses
fixed arguments and no Cargo build scripts, package downloads, external
dependencies, arbitrary shell commands or student-selected toolchain paths.
Rust source, compiler output and resulting Wasm are all untrusted bounded data.
Native Rullst applications, arbitrary-language workspaces and network exercises
remain later profiles rather than implicit capabilities of this one.
Wasmi 2.0.0 is selected for the first interpreter implementation: validation,
extra runtime checks, explicit structural/store/stack limits and deterministic
fuel are required; WAT parsing, WASI, host imports, shared memories, threads,
start functions and unsupported proposals are excluded. A fresh instance per
test case avoids hidden state between cases. Reference-type decoding is enabled
for the pinned Rust standard library’s call_indirect encoding; tables are
restricted to bounded funcref, and imports remain prohibited. The interpreter is one boundary
inside process isolation, not a substitute for it. Wasmtime/component/WASI
support remains a separate profile decision.
Ownership and process boundaries
| Component | Authority and data |
|---|---|
Application and rullst-labs | Current tenant/course/enrollment policy; exercise/grader revision; durable job submission/status/cancel/result. No execution engine, learner process or control socket. |
| Dedicated shared-local job store | Versioned, integrity-checked encrypted source/grader content; bounded jobs, leases, cancellation and minimized results. Separate key and database from application identity/session state. |
| Trusted runner controller | Dedicated job-store and receipt authority; pinned tool paths; leased work, compiler/executor launch, bounded output and teardown. No general application credentials. |
| Per-job compiler/interpreter process | Only the selected source, fixed toolchain, case inputs and disposable output/workspace. No expected answers, job database, runner signing key, host home, environment credentials or control sockets. |
Shared-local means one trusted local filesystem with explicit process separation; this profile does not supply a remote broker, network filesystem or distributed runner service. Application authorization never follows from a client-provided learner, course or provider identifier. File possession/configuration is not a student capability. Neither signed requests nor an AI evaluation can weaken the execution policy or manufacture a passing result.
Content and receipt keys require private regular files owned by the controller under protected canonical ancestors. The controller validates the opened descriptor, rejects symlinks and non-regular files without blocking on a FIFO, and requires exactly 32 bytes. File permissions alone do not protect a key whose parent directory lets another local user replace it.
Mandatory Linux execution boundary
The implementation must apply and test all of the following before accepting untrusted input into the worker:
- A dedicated delegated cgroups v2 subtree for each job, with hard memory,
swap, process and CPU limits plus controller-owned wall-clock deadlines.
Place the inert child in its bounded group before releasing input; cover
compiler initialization/translation as well as guest execution.
The delegated root must have a kernel-enforced
cgroup.max.descendantsbetween 1 and 32, including probe groups. Counting directories before creation cannot enforce capacity across concurrent controllers. Reconcile authenticated expired/cancelled attempts before preflight so a full subtree can recover; cleanup never releases source and cannot award a successful grade. - Unprivileged user, PID, mount, IPC and network namespaces; capability drop, no-new-privileges, no inherited terminal/session or additional user namespaces. The launcher is a reviewed, pinned configuration of Bubblewrap, not an arbitrary command string supplied by an application request.
- A minimal read-only toolchain/runtime tree, a fresh bounded workspace and explicit descriptors. No host home, D-Bus/container sockets, job-store mount, writable toolchain/cache, application files or metadata endpoint reachability. Tool files/directories and their canonical ancestors must be owned by root or the dedicated controller UID. Shared-writable ancestors are refused except sticky parents protecting the next owned component; tool trees themselves must never be shared-writable. Host administrators/controller ownership remain trusted, and digest checks do not replace trusted installation or custody.
- Before source is released, require fully enforced Landlock filesystem
restrictions (ABI v3 rights) for fixed tools, runtime files and the bounded
workspace. Spawn the trusted compiler helper before the interpreter applies
Landlock, then restrict each in a separate domain. The helper checks its own
actual OS boundary and denied access to its parent’s descriptors/memory before
acknowledging readiness. Only then may the interpreter receive source. Regular
/procand cgroup files are denied by the filesystem policy; the kernel does not mediate a process’s own anonymous pipes through/proc, so cross-process protection must use Landlock’s ptrace/domain hierarchy. Compiler stdin/stdout become null at exec; only capped stderr diagnostics return. Inherited descriptors are checked against the exact owned protocol handles. - Reviewed syscall restrictions in addition to namespaces; no extra privileges
or unbounded resource allowance on unsupported kernels/platforms.
The compiler selects the pinned linker by its absolute toolchain path. Rust’s
changed-
PATHspawn fallback uses a socket pair, so a bare linker name would fail under the unchanged no-sockets policy. A real seccomp regression verifies absolute-path spawning while socket-pair creation remains denied. Rust’s captured linker streams also requireioctl(FIONBIO)to toggle nonblocking reads on existing descriptors. The filter permits that exact request only; it does not grant device-control ioctls or socket creation. A real filter regression captures both child streams and verifies another ioctl request is still denied. The namespace exposes no device-control handles. - Structural Wasm validation, bounded compilation, memory/stack/table/fuel limits, no guest imports and bounded, normalized output. No deserialization of untrusted native/precompiled engine caches.
- Mandatory teardown of the entire job group and workspace. Uncertain cleanup, memory/process exhaustion, malformed output and worker loss cannot be graded as successful execution.
Runtime probes must check actual restrictions, including namespace separation,
denied filesystem/network/control-socket access, capability state and effective
cgroup limits. Property names, process exit zero or a launcher version alone are
not admission evidence. The development machine accepted a user-service
PrivateNetwork request while retaining the same network namespace; a subsequent
namespace launch failed. Those probes are evidence of an unsupported environment,
not permission to remove network restrictions or execute learner code there.
Automated acceptance must provision a supported disposable Linux host explicitly.
The Ubuntu 24.04 acceptance host needs an AppArmor namespace permission for its
trusted launcher. The hosted test provisions a root-owned private copy of the
distro Bubblewrap executable and a separately named, checksum-reviewed copy of
Ubuntu’s bwrap-userns-restrict profile, attached only to that launcher path.
Its child profile denies capabilities. This setup is confined to the disposable
host; it never disables AppArmor or changes a global namespace sysctl. The runner
still requires its own namespace, capability, seccomp, Landlock and cgroup probes.
Ordinary workstations are not reconfigured by the runner or local test suite.
The domain split addresses the kernel’s documented special-filesystem limitation. A failed denial probe blocks source release; removing that probe without a replacement boundary is not an acceptable compatibility fix.
Integrity, grading and recovery
An immutable request binds protocol/profile version, tenant, course, learner, exercise/grader revision, source digest, pinned tools, resource policy and local submission ID. Reusing an ID with different content fails. Database content is authenticated before use and bound to indexed identity/configuration. Source and expected answers do not appear in Debug, routine events, metric labels or errors.
Claiming a job creates a bounded durable lease and execution nonce before launch.
Worker output contains bounded case values/traps, never authoritative expected
answers or a caller-controlled passed=true. Trusted grading uses the exact
stored exercise revision. Result acceptance binds the current lease/request and
refuses stale, expired, cancelled, cross-tenant or incompatible receipts. A
replayed receipt cannot award credit twice. Compiler diagnostics are bounded,
treated as untrusted text and escaped for any HTML/terminal presentation.
Cancellation records denial before process termination and fences any late result. On restart, reconcile persisted work and owned job groups; do not assume that a missing connection or process means compilation/execution succeeded. Pure functions permit deliberate bounded re-execution after confirmed teardown; there is no exactly-once external side-effect claim. Retention covers encrypted source, workspaces, artifacts, diagnostics and result/idempotency tombstones. Restored state needs clock/configuration/lease reconciliation, not silent reuse of stale authority. Operators own encrypted backups and physical storage erasure. Authorized application maintenance expires queued source independently of runner availability, without touching leased work. A withdrawn grader can be removed only after all referencing job records have passed retention and been purged. Removed immutable revision identifiers must not be reused.
Adversarial acceptance required
The executable test plan includes malformed/oversized source and Wasm, incompatible
ABI/imports, infinite loops, recursion, memory/table/output/disk/process exhaustion,
compiler denial of service, filesystem traversal/symlinks, environment and file
descriptor leakage, network/metadata/control-socket denial, concurrency, worker
restart/loss, stale-result fencing, cancellation/expiry, idempotency collisions,
retention and actual application submission/status/result access. Include both
working Rust examples and failing submissions; a mocked receipt cannot pass the
execution/isolation gate.
Memory acceptance separately verifies denied growth, out-of-bounds access and
Rust allocation failure. An allocator abort is a generic guest trap because a
Wasm unreachable instruction does not reliably identify its original cause;
it must not be relabeled as a memory-specific trap merely to fit a test.
The platform matrix and named profile remain experimental until the required tests and independent isolation review exist. No claim of risk-free hostile-code execution, complete Rust/Rullst language support or generic secure CTF hosting follows from the contracts or this decision.
Primary implementation references
- Wasmi 2.0 API and feature boundaries
- Wasmi configuration and validation defaults
- Bubblewrap policy responsibilities and limitations
- Wasmtime security boundaries, including compilation denial of service
- Linux cgroups v2
- Landlock Rust bindings and enforcement status
- Linux seccomp filter
- Rust 1.96 captured process output
- Rust 1.96 Linux nonblocking descriptor operation
Reviewed September 20, 2026. Runtime/toolchain updates require fresh policy and compatibility review; benchmark claims from runtime projects are not Rullst performance or isolation evidence.