Public dry run dryrun-a43e907efd9d

Source commit: a43e907efd9d1561c23fe73499478e881f868355

Public tree identity: sha256:453dea30195485dd8939f575a69b39f4bb7acd84c4df23f8aa29b55d044f2673
This commit is contained in:
Clusterflux release dry run 2026-07-15 01:54:51 +02:00
commit 6f52bb46cd
210 changed files with 77158 additions and 0 deletions

View file

@ -0,0 +1,14 @@
[package]
name = "clusterflux-wasm-runtime"
version = "0.1.0"
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
clusterflux-core = { path = "../clusterflux-core" }
serde.workspace = true
serde_json.workspace = true
thiserror.workspace = true
tokio.workspace = true
wasmtime.workspace = true

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,427 @@
use super::*;
pub(super) fn task_host_linker(
engine: &Engine,
) -> Result<Linker<WasmtimeTaskHostState>, WasmTaskError> {
let mut linker = Linker::new(engine);
linker
.func_wrap(
"clusterflux",
"task_start_v1",
|mut caller: Caller<'_, WasmtimeTaskHostState>,
input_pointer: u32,
input_length: u32,
output_pointer: u32,
output_capacity: u32|
-> i32 {
task_host_call(
&mut caller,
input_pointer,
input_length,
output_pointer,
output_capacity,
TaskHostOperation::Start,
)
},
)
.map_err(wasmtime_error)?;
linker
.func_wrap(
"clusterflux",
"source_snapshot_v1",
|mut caller: Caller<'_, WasmtimeTaskHostState>,
input_pointer: u32,
input_length: u32,
output_pointer: u32,
output_capacity: u32|
-> i32 {
task_host_call(
&mut caller,
input_pointer,
input_length,
output_pointer,
output_capacity,
TaskHostOperation::SourceSnapshot,
)
},
)
.map_err(wasmtime_error)?;
linker
.func_wrap(
"clusterflux",
"vfs_operation_v1",
|mut caller: Caller<'_, WasmtimeTaskHostState>,
input_pointer: u32,
input_length: u32,
output_pointer: u32,
output_capacity: u32|
-> i32 {
task_host_call(
&mut caller,
input_pointer,
input_length,
output_pointer,
output_capacity,
TaskHostOperation::Vfs,
)
},
)
.map_err(wasmtime_error)?;
linker
.func_wrap(
"clusterflux",
"command_run_v1",
|mut caller: Caller<'_, WasmtimeTaskHostState>,
input_pointer: u32,
input_length: u32,
output_pointer: u32,
output_capacity: u32|
-> i32 {
task_host_call(
&mut caller,
input_pointer,
input_length,
output_pointer,
output_capacity,
TaskHostOperation::Command,
)
},
)
.map_err(wasmtime_error)?;
linker
.func_wrap(
"clusterflux",
"task_control_v1",
|mut caller: Caller<'_, WasmtimeTaskHostState>,
input_pointer: u32,
input_length: u32,
output_pointer: u32,
output_capacity: u32|
-> i32 {
task_host_call(
&mut caller,
input_pointer,
input_length,
output_pointer,
output_capacity,
TaskHostOperation::TaskControl,
)
},
)
.map_err(wasmtime_error)?;
linker
.func_wrap(
"clusterflux",
"debug_probe_v1",
|mut caller: Caller<'_, WasmtimeTaskHostState>,
input_pointer: u32,
input_length: u32,
output_pointer: u32,
output_capacity: u32|
-> i32 {
task_host_call(
&mut caller,
input_pointer,
input_length,
output_pointer,
output_capacity,
TaskHostOperation::DebugProbe,
)
},
)
.map_err(wasmtime_error)?;
linker
.func_wrap(
"clusterflux",
"task_join_v1",
|mut caller: Caller<'_, WasmtimeTaskHostState>,
input_pointer: u32,
input_length: u32,
output_pointer: u32,
output_capacity: u32|
-> i32 {
task_host_call(
&mut caller,
input_pointer,
input_length,
output_pointer,
output_capacity,
TaskHostOperation::Join,
)
},
)
.map_err(wasmtime_error)?;
Ok(linker)
}
pub(super) fn task_host_stub_linker<T: 'static>(
engine: &Engine,
) -> Result<Linker<T>, WasmTaskError> {
let mut linker = Linker::new(engine);
for import in [
"task_start_v1",
"task_join_v1",
"command_run_v1",
"task_control_v1",
"source_snapshot_v1",
"vfs_operation_v1",
] {
linker
.func_wrap(
"clusterflux",
import,
|_input_pointer: u32,
_input_length: u32,
_output_pointer: u32,
_output_capacity: u32|
-> i32 { -1 },
)
.map_err(wasmtime_error)?;
}
linker
.func_wrap(
"clusterflux",
"debug_probe_v1",
|mut caller: Caller<'_, T>,
input_pointer: u32,
input_length: u32,
output_pointer: u32,
output_capacity: u32|
-> i32 {
debug_probe_stub_call(
&mut caller,
input_pointer,
input_length,
output_pointer,
output_capacity,
)
},
)
.map_err(wasmtime_error)?;
Ok(linker)
}
fn debug_probe_stub_call<T>(
caller: &mut Caller<'_, T>,
input_pointer: u32,
input_length: u32,
output_pointer: u32,
output_capacity: u32,
) -> i32 {
let Some(memory) = caller
.get_export("memory")
.and_then(|export| export.into_memory())
else {
return -2;
};
let mut input = vec![0_u8; input_length as usize];
if memory
.read(&*caller, input_pointer as usize, &mut input)
.is_err()
{
return -3;
}
let response: Result<WasmHostDebugProbeResult, String> =
match serde_json::from_slice::<WasmHostDebugProbeRequest>(&input) {
Ok(request) => request.validate().map(|()| WasmHostDebugProbeResult {
abi_version: clusterflux_core::WASM_TASK_ABI_VERSION,
breakpoint_matched: false,
debug_epoch: None,
}),
Err(error) => Err(error.to_string()),
};
let Ok(encoded) = serde_json::to_vec(&response) else {
return -4;
};
if encoded.len() > output_capacity as usize {
return -(encoded.len() as i32);
}
if memory
.write(caller, output_pointer as usize, &encoded)
.is_err()
{
return -5;
}
encoded.len() as i32
}
enum TaskHostOperation {
Start,
Join,
Command,
TaskControl,
DebugProbe,
SourceSnapshot,
Vfs,
}
fn task_host_call(
caller: &mut Caller<'_, WasmtimeTaskHostState>,
input_pointer: u32,
input_length: u32,
output_pointer: u32,
output_capacity: u32,
operation: TaskHostOperation,
) -> i32 {
if input_length as usize > MAX_WASM_TASK_ENVELOPE_BYTES
|| output_capacity as usize > MAX_WASM_TASK_ENVELOPE_BYTES
{
return -1;
}
let Some(memory) = caller
.get_export("memory")
.and_then(|export| export.into_memory())
else {
return -2;
};
let mut input = vec![0_u8; input_length as usize];
if memory
.read(&*caller, input_pointer as usize, &mut input)
.is_err()
{
return -3;
}
if let Some(debug) = caller.data().host.debug_control() {
let stack_frames = WasmBacktrace::force_capture(&*caller)
.frames()
.iter()
.take(16)
.map(|frame| {
frame
.func_name()
.map(str::to_owned)
.unwrap_or_else(|| format!("wasm_function_{}", frame.func_index()))
})
.collect();
debug.record_stack_frames(stack_frames);
}
let encoded = match operation {
TaskHostOperation::Start => {
let debug = caller.data().host.debug_control();
let abort = caller.data().host.abort_signal();
if let Some(debug) = &debug {
debug.enter_quiescent_host_boundary(abort.as_deref());
}
let response: Result<WasmHostTaskHandle, String> =
match serde_json::from_slice::<WasmHostTaskStartRequest>(&input) {
Ok(request) => request
.validate()
.and_then(|()| caller.data_mut().host.start_task(request)),
Err(error) => Err(error.to_string()),
};
if let Some(debug) = &debug {
debug.leave_quiescent_host_boundary(abort.as_deref());
}
serde_json::to_vec(&response)
}
TaskHostOperation::Join => {
let debug = caller.data().host.debug_control();
let abort = caller.data().host.abort_signal();
if let Some(debug) = &debug {
debug.enter_quiescent_host_boundary(abort.as_deref());
}
let response: Result<WasmHostTaskJoinResult, String> = match serde_json::from_slice::<
WasmHostTaskJoinRequest,
>(&input)
{
Ok(request) if request.abi_version == clusterflux_core::WASM_TASK_ABI_VERSION => {
caller.data_mut().host.join_task(request)
}
Ok(request) => Err(format!(
"unsupported Wasm task ABI version {}",
request.abi_version
)),
Err(error) => Err(error.to_string()),
};
if let Some(debug) = &debug {
debug.leave_quiescent_host_boundary(abort.as_deref());
}
serde_json::to_vec(&response)
}
TaskHostOperation::Command => {
let response: Result<WasmHostCommandResult, String> =
match serde_json::from_slice::<WasmHostCommandRequest>(&input) {
Ok(request) => request
.validate()
.and_then(|()| caller.data_mut().host.run_command(request)),
Err(error) => Err(error.to_string()),
};
if let Err(error) = &response {
if error.contains("task execution cancelled:") {
caller.data_mut().fatal_host_error = Some(error.clone());
}
}
serde_json::to_vec(&response)
}
TaskHostOperation::TaskControl => {
let response: Result<WasmHostTaskControlResult, String> =
match serde_json::from_slice::<WasmHostTaskControlRequest>(&input) {
Ok(request) => request
.validate()
.and_then(|()| caller.data_mut().host.poll_task_control(request)),
Err(error) => Err(error.to_string()),
};
serde_json::to_vec(&response)
}
TaskHostOperation::DebugProbe => {
// A matched probe requests a Debug Epoch from inside this host call. Keep
// the guest at this quiescent boundary until every participant has frozen
// and the debugger resumes the epoch; otherwise a short-lived task can run
// past the probe (or trap) before its control watcher acknowledges all-stop.
let debug = caller.data().host.debug_control();
let abort = caller.data().host.abort_signal();
if let Some(debug) = &debug {
debug.enter_quiescent_host_boundary(abort.as_deref());
}
let response: Result<WasmHostDebugProbeResult, String> =
match serde_json::from_slice::<WasmHostDebugProbeRequest>(&input) {
Ok(request) => request
.validate()
.and_then(|()| caller.data_mut().host.debug_probe(request)),
Err(error) => Err(error.to_string()),
};
if let Some(debug) = &debug {
debug.leave_quiescent_host_boundary(abort.as_deref());
}
serde_json::to_vec(&response)
}
TaskHostOperation::Vfs => {
let response: Result<WasmHostVfsResult, String> =
match serde_json::from_slice::<WasmHostVfsRequest>(&input) {
Ok(request) => request
.validate()
.and_then(|()| caller.data_mut().host.vfs_operation(request)),
Err(error) => Err(error.to_string()),
};
serde_json::to_vec(&response)
}
TaskHostOperation::SourceSnapshot => {
let response: Result<WasmHostSourceSnapshotResult, String> =
match serde_json::from_slice::<WasmHostSourceSnapshotRequest>(&input) {
Ok(request) => request
.validate()
.and_then(|()| caller.data_mut().host.snapshot_source(request)),
Err(error) => Err(error.to_string()),
};
serde_json::to_vec(&response)
}
};
let Ok(encoded) = encoded else {
return -4;
};
let current_fuel = caller.get_fuel().unwrap_or(0);
let refilled_fuel = caller.data_mut().refill_fuel_after_host_call(current_fuel);
if caller.set_fuel(refilled_fuel).is_err() {
return -8;
}
if encoded.is_empty() || encoded.len() > output_capacity as usize {
return -5;
}
if memory
.write(&mut *caller, output_pointer as usize, &encoded)
.is_err()
{
return -6;
}
i32::try_from(encoded.len()).unwrap_or(-7)
}

View file

@ -0,0 +1,248 @@
use super::*;
#[test]
fn fuel_token_bucket_refills_after_idle_and_never_exceeds_burst() {
let limits = WasmtimeRuntimeLimits {
fuel_units_per_second: 100,
fuel_burst_seconds: 2,
memory_bytes: 1024 * 1024,
};
let mut bucket = FuelTokenBucket::new(&limits);
bucket.last_refill = Instant::now() - Duration::from_millis(1_500);
let refilled = bucket.refill(0);
assert!((150..=151).contains(&refilled));
bucket.last_refill = Instant::now() - Duration::from_secs(10);
assert_eq!(bucket.refill(refilled), 200);
for _ in 0..10_000 {
assert!(bucket.refill(200) <= 200);
}
}
struct AbortSignalHost {
abort: Arc<AtomicBool>,
debug: Option<Arc<WasmDebugControl>>,
}
impl WasmTaskHost for AbortSignalHost {
fn abort_signal(&self) -> Option<Arc<AtomicBool>> {
Some(Arc::clone(&self.abort))
}
fn debug_control(&self) -> Option<Arc<WasmDebugControl>> {
self.debug.clone()
}
fn start_task(
&mut self,
_request: WasmHostTaskStartRequest,
) -> Result<WasmHostTaskHandle, String> {
Err("not used".to_owned())
}
fn join_task(
&mut self,
_request: WasmHostTaskJoinRequest,
) -> Result<WasmHostTaskJoinResult, String> {
Err("not used".to_owned())
}
fn run_command(
&mut self,
_request: WasmHostCommandRequest,
) -> Result<WasmHostCommandResult, String> {
Err("not used".to_owned())
}
fn poll_task_control(
&mut self,
request: WasmHostTaskControlRequest,
) -> Result<WasmHostTaskControlResult, String> {
request.validate()?;
Ok(WasmHostTaskControlResult {
abi_version: clusterflux_core::WASM_TASK_ABI_VERSION,
cancellation_requested: false,
})
}
fn vfs_operation(&mut self, _request: WasmHostVfsRequest) -> Result<WasmHostVfsResult, String> {
Err("not used".to_owned())
}
fn snapshot_source(
&mut self,
_request: WasmHostSourceSnapshotRequest,
) -> Result<WasmHostSourceSnapshotResult, String> {
Err("not used".to_owned())
}
}
#[test]
fn epoch_interruption_aborts_cpu_bound_wasm_without_a_host_call() {
let abort = Arc::new(AtomicBool::new(false));
let trigger = Arc::clone(&abort);
let trigger_thread = thread::spawn(move || {
thread::sleep(Duration::from_millis(50));
trigger.store(true, Ordering::Release);
});
let wasm = r#"
(module
(func (export "spin") (param i32) (result i32)
(loop $forever
br $forever)
i32.const 0))
"#;
let started = std::time::Instant::now();
let error = WasmtimeTaskRuntime::new()
.unwrap()
.run_i32_export_verified_with_task_host(
wasm,
&Digest::sha256(wasm),
"spin",
1,
Box::new(AbortSignalHost { abort, debug: None }),
)
.unwrap_err();
trigger_thread.join().unwrap();
assert!(matches!(error, WasmTaskError::HostControl(_)));
assert!(error.to_string().contains("process abort"));
assert!(started.elapsed() < Duration::from_secs(2));
}
#[test]
fn aborting_one_store_does_not_poison_the_shared_engine() {
let runtime = WasmtimeTaskRuntime::new().unwrap();
let abort = Arc::new(AtomicBool::new(false));
let trigger = Arc::clone(&abort);
let trigger_thread = thread::spawn(move || {
thread::sleep(Duration::from_millis(50));
trigger.store(true, Ordering::Release);
});
let spinning_wasm = r#"
(module
(func (export "spin") (param i32) (result i32)
(loop $forever
br $forever)
i32.const 0))
"#;
let error = runtime
.run_i32_export_verified_with_task_host(
spinning_wasm,
&Digest::sha256(spinning_wasm),
"spin",
0,
Box::new(AbortSignalHost { abort, debug: None }),
)
.unwrap_err();
trigger_thread.join().unwrap();
assert!(matches!(error, WasmTaskError::HostControl(_)));
let succeeding_wasm = r#"
(module
(func (export "increment") (param i32) (result i32)
local.get 0
i32.const 1
i32.add))
"#;
assert_eq!(
runtime
.run_i32_export_verified(
succeeding_wasm,
&Digest::sha256(succeeding_wasm),
"increment",
41,
)
.unwrap(),
42
);
}
#[test]
fn debug_control_freezes_and_resumes_executing_wasm_before_abort() {
let runtime = WasmtimeTaskRuntime::new().unwrap();
let abort = Arc::new(AtomicBool::new(false));
let debug = Arc::new(WasmDebugControl::default());
let execution_abort = Arc::clone(&abort);
let execution_debug = Arc::clone(&debug);
let (finished_tx, finished_rx) = std::sync::mpsc::channel();
let execution = thread::spawn(move || {
let wasm = r#"
(module
(func (export "spin") (param i32) (result i32)
(loop $forever
br $forever)
i32.const 0))
"#;
let result = runtime.run_i32_export_verified_with_task_host(
wasm,
&Digest::sha256(wasm),
"spin",
0,
Box::new(AbortSignalHost {
abort: execution_abort,
debug: Some(execution_debug),
}),
);
let _ = finished_tx.send(());
result
});
debug.request_freeze(7);
assert!(debug.wait_until_frozen(7, Duration::from_secs(2)));
assert!(finished_rx.try_recv().is_err());
debug.request_resume(7);
assert!(debug.wait_until_running(7, Duration::from_secs(2)));
abort.store(true, Ordering::Release);
let error = execution.join().unwrap().unwrap_err();
assert!(matches!(error, WasmTaskError::HostControl(_)));
assert!(error.to_string().contains("process abort"));
}
#[test]
fn pending_freeze_blocks_a_quiescent_host_call_before_it_starts() {
let debug = Arc::new(WasmDebugControl::default());
debug.request_freeze(9);
let boundary_debug = Arc::clone(&debug);
let (entered_tx, entered_rx) = std::sync::mpsc::channel();
let boundary = thread::spawn(move || {
boundary_debug.enter_quiescent_host_boundary(None);
entered_tx.send(()).unwrap();
boundary_debug.leave_quiescent_host_boundary(None);
});
assert!(debug.wait_until_frozen(9, Duration::from_secs(1)));
assert!(entered_rx.try_recv().is_err());
debug.request_resume(9);
assert!(debug.wait_until_running(9, Duration::from_secs(1)));
entered_rx.recv_timeout(Duration::from_secs(1)).unwrap();
boundary.join().unwrap();
}
#[test]
fn wasm_linear_memory_growth_is_bounded_per_store() {
let wasm = r#"
(module
(memory 1)
(func (export "grow") (param i32) (result i32)
i32.const 5000
memory.grow
drop
local.get 0))
"#;
let runtime = WasmtimeTaskRuntime::new().unwrap();
let error = runtime.run_i32_export(wasm, "grow", 7).unwrap_err();
assert!(error.to_string().contains("memory") || error.to_string().contains("grow"));
assert_eq!(
runtime
.run_i32_export(
"(module (func (export \"healthy\") (param i32) (result i32) local.get 0))",
"healthy",
11,
)
.unwrap(),
11
);
}