Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ It is designed for systems that execute code you do not fully trust:
- **Dual-Hypervisor Core**: Uses KVM-backed Firecracker on Linux, and native `Virtualization.framework` on macOS.
- **Host-Reliant Disk Mounts**: The guest microVM has no shell, utilities, or libraries. Service code and language runtimes (Bun, Node, Deno, QuickJS) are compiled on the host and attached as read-only virtual block devices (`/dev/vdb` and `/dev/vdc`).
- **VSOCK Multiplexing**: Low-latency communication handshakes stream stdout/stderr and exit codes directly back to the host via virtual sockets, bypassing network interfaces.
- **Resource Enforcement**: `memoryMb` and `cpuLimit` are applied to Firecracker machine config, while `timeoutMs` is enforced by a host-side watchdog that force-terminates timed-out VMs.
- **Preflight & Metric Timelines**: Sub-millisecond logging of all VM lifecycle transitions (disk format, boot connect, execution, cleanup).

## Quick Start
Expand Down
1 change: 1 addition & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ ignite-cli/ignite-http
-> Host transmits JSON payload (environment, execution script)
-> Guest Agent executes runtime command inside microVM sandbox
-> Guest Agent streams stdout/stderr multiplexed frames over VSOCK
-> Host watchdog enforces `timeoutMs`; on timeout it force-terminates the Firecracker process
-> Guest Agent captures exit code and triggers reboot(POWER_OFF)
-> Host tears down hypervisor and deletes temporary UDS socket files
```
Expand Down
2 changes: 1 addition & 1 deletion docs/threat-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Ignite aims to provide defense-in-depth for executing untrusted JS/TS code insid
| Mitigate network exfiltration | VMs are started without virtual network interfaces. |
| Mitigate host file tampering | App files (`/app`) and engine code (`/runtime`) are attached as read-only virtual block devices. |
| Limit privilege escalation | No shell (`/bin/sh`), compiler, or system utilities exist in the guest rootfs. |
| Bound runaway processes | Memory, vCPUs, and time execution limits are enforced directly by the hypervisor process. |
| Bound runaway processes | Memory/vCPU limits are applied to Firecracker machine config, and a host watchdog force-terminates the VM when `timeoutMs` is exceeded. |
| VSOCK Only handshake | Handshake and stdout/stderr pipes occur over a dedicated virtual socket (VSOCK) connection. |

## Trust Boundaries
Expand Down
113 changes: 102 additions & 11 deletions ignite-core/src/platform/firecracker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const VSOCK_PORT_SUFFIX: &str = "_1052";
const API_READY_TIMEOUT_MS: u64 = 500;
const API_READY_POLL_MS: u64 = 5;
const GUEST_CID: u32 = 3;
const IO_POLL_TIMEOUT_MS: u64 = 50;

impl Default for FirecrackerOrchestrator {
fn default() -> Self {
Expand Down Expand Up @@ -202,7 +203,7 @@ impl MicroVmOrchestrator for FirecrackerOrchestrator {
on_stdout: Option<Box<dyn Fn(&str) + Send>>,
on_stderr: Option<Box<dyn Fn(&str) + Send>>,
) -> Result<ExecutionMetrics> {
let config = self.config.as_ref().ok_or_else(|| IgniteError::Config {
let config = self.config.clone().ok_or_else(|| IgniteError::Config {
message: "VM not configured".to_string(),
source: None,
})?;
Expand All @@ -222,6 +223,7 @@ impl MicroVmOrchestrator for FirecrackerOrchestrator {
match listener.accept() {
Ok((stream, _)) => {
stream.set_nonblocking(false)?;
stream.set_read_timeout(Some(Duration::from_millis(IO_POLL_TIMEOUT_MS)))?;
break stream;
}
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
Expand All @@ -244,10 +246,10 @@ impl MicroVmOrchestrator for FirecrackerOrchestrator {

// 2. Transmit execution payload (length-prefixed)
let payload = serde_json::json!({
"env": config.env,
"runtime_args": config.runtime_args,
"entrypoint": config.entrypoint,
"input": config.input,
"env": &config.env,
"runtime_args": &config.runtime_args,
"entrypoint": &config.entrypoint,
"input": &config.input,
});

let payload_bytes = serde_json::to_vec(&payload)?;
Expand All @@ -262,19 +264,73 @@ impl MicroVmOrchestrator for FirecrackerOrchestrator {

loop {
let mut type_byte = [0u8; 1];
if socket.read_exact(&mut type_byte).is_err() {
break; // VM socket disconnected
if let Err(e) = Self::read_exact_with_timeout(
&mut socket,
&mut type_byte,
start_time,
config.timeout_ms,
) {
if e.kind() == std::io::ErrorKind::UnexpectedEof {
break; // VM socket disconnected
}
if e.kind() == std::io::ErrorKind::TimedOut {
self.cleanup_vm_processes();
return Err(IgniteError::Execution {
message: format!(
"Execution exceeded timeoutMs ({}ms); VM force-terminated by host watchdog",
config.timeout_ms
),
source: None,
});
}
self.cleanup_vm_processes();
return Err(e.into());
}

let mut len_bytes = [0u8; 4];
if socket.read_exact(&mut len_bytes).is_err() {
break;
if let Err(e) = Self::read_exact_with_timeout(
&mut socket,
&mut len_bytes,
start_time,
config.timeout_ms,
) {
if e.kind() == std::io::ErrorKind::UnexpectedEof {
break;
}
if e.kind() == std::io::ErrorKind::TimedOut {
self.cleanup_vm_processes();
return Err(IgniteError::Execution {
message: format!(
"Execution exceeded timeoutMs ({}ms); VM force-terminated by host watchdog",
config.timeout_ms
),
source: None,
});
}
self.cleanup_vm_processes();
return Err(e.into());
}
let length = u32::from_be_bytes(len_bytes) as usize;

let mut data = vec![0u8; length];
if socket.read_exact(&mut data).is_err() {
break;
if let Err(e) =
Self::read_exact_with_timeout(&mut socket, &mut data, start_time, config.timeout_ms)
{
if e.kind() == std::io::ErrorKind::UnexpectedEof {
break;
}
if e.kind() == std::io::ErrorKind::TimedOut {
self.cleanup_vm_processes();
return Err(IgniteError::Execution {
message: format!(
"Execution exceeded timeoutMs ({}ms); VM force-terminated by host watchdog",
config.timeout_ms
),
source: None,
});
}
self.cleanup_vm_processes();
return Err(e.into());
}

let chunk = String::from_utf8_lossy(&data);
Expand Down Expand Up @@ -338,4 +394,39 @@ impl FirecrackerOrchestrator {
let _ = fs::remove_file(&vsock_listener_path);
}
}

fn read_exact_with_timeout(
stream: &mut UnixStream,
buffer: &mut [u8],
start_time: Instant,
timeout_ms: u32,
) -> std::io::Result<()> {
let mut read_total = 0usize;
while read_total < buffer.len() {
match stream.read(&mut buffer[read_total..]) {
Ok(0) => {
return Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
"VSOCK stream closed",
));
}
Ok(n) => {
read_total += n;
}
Err(e)
if e.kind() == std::io::ErrorKind::TimedOut
|| e.kind() == std::io::ErrorKind::WouldBlock =>
{
if start_time.elapsed().as_millis() > (timeout_ms as u128) {
return Err(std::io::Error::new(
std::io::ErrorKind::TimedOut,
"Execution timed out",
));
}
}
Err(e) => return Err(e),
}
}
Ok(())
}
}
Loading