Skip to content
Open
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
23 changes: 20 additions & 3 deletions crates/native-sidecar/src/execution/child_process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6225,9 +6225,26 @@ where
}
if request.method == "process.fd_read" {
let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "fd_read fd")?;
let stat = kernel
.fd_stat(EXECUTION_DRIVER_NAME, child.kernel_pid, fd)
.map_err(kernel_error)?;
// Same class as the `fs.write` deferral pre-check: a guest-supplied
// fd was stat'ed against the kernel fd table with a bare `?`, but
// mapped host fds live in the per-process map instead. That raised
// EBADF, which escapes to the process-event pump and strands the
// in-flight sync RPC — the guest then blocks until the ~31s bridge
// deadline. The sibling `process.fd_write` arm above already
// converts its errors into a delivered RPC error; this one did not.
//
// A host-backed fd is never a kernel pipe, so it is never parked
// here; `Ok(false)` hands it to the normal RPC handler, which does
// its own mapped lookup and raises the correct errno if the fd is
// genuinely bad.
if child.mapped_host_fd(fd).is_some() {
return Ok(false);
}
let stat = match kernel.fd_stat(EXECUTION_DRIVER_NAME, child.kernel_pid, fd) {
Ok(stat) => stat,
Err(error) if error.code() == "EBADF" => return Ok(false),
Err(error) => return Err(kernel_error(error)),
};
if matches!(
stat.filetype,
agentos_kernel::fd_table::FILETYPE_REGULAR_FILE
Expand Down
79 changes: 79 additions & 0 deletions crates/native-sidecar/tests/crash_isolation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,85 @@ fn guest_failure_in_one_vm_does_not_break_peer_vm_execution() {

assert_eq!(exit_code, 0);
assert!(stderr.is_empty(), "unexpected follow-up stderr: {stderr}");

// Regression: a guest writing to a HOST-BACKED path (the VM cwd is a real
// host directory, so these go through mapped host fds, not the kernel VFS).
//
// This vector produced two successive production failures that the guest
// exception above does NOT cover, because a thrown JS error never reaches
// the sidecar's own error paths:
//
// 1. EBADF on a mapped fd escaped the process-event pump to `main`, which
// exit(1)'d — killing every co-located VM/actor (cross-tenant DoS).
// 2. After the pump stopped propagating it, the guest's sync-RPC response
// was never delivered and every guest file write hung ~31s.
//
// Both are asserted here: the DEADLINE inside collect_crash_process_output
// catches the hang, and the peer execution afterwards catches the crash.
// A test that only checks "did it eventually succeed" would pass while
// hanging, which is precisely how the second failure went unnoticed.
let write_entry = cwd.join("host_backed_write.cjs");
write_fixture(
&write_entry,
"const fs = require(\"node:fs\");\n\
const chunk = Buffer.alloc(64 * 1024, 65);\n\
const fd = fs.openSync(\"host_backed_write.bin\", \"w\");\n\
for (let i = 0; i < 16; i++) fs.writeSync(fd, chunk);\n\
fs.closeSync(fd);\n\
console.log(\"wrote\");\n",
);
execute_wire(
&mut sidecar,
8,
&connection_id,
&session_id,
&crash_vm_id,
"proc-host-write",
GuestRuntimeKind::JavaScript,
&write_entry,
Vec::new(),
);
let (write_stdout, write_stderr, write_exit) = collect_crash_process_output(
&mut sidecar,
&connection_id,
&session_id,
&crash_vm_id,
"proc-host-write",
);
assert_eq!(
write_exit, 0,
"host-backed write failed (stderr: {write_stderr})"
);
assert!(
write_stdout.contains("wrote"),
"host-backed write produced no output: {write_stdout}"
);

// The sidecar is shared: the peer VM must still execute afterwards. This is
// the co-tenancy invariant the cross-tenant DoS violated.
execute_wire(
&mut sidecar,
9,
&connection_id,
&session_id,
&healthy_vm_id,
"proc-healthy-3",
GuestRuntimeKind::JavaScript,
&healthy_entry,
Vec::new(),
);
let (_stdout, peer_stderr, peer_exit) = collect_crash_process_output(
&mut sidecar,
&connection_id,
&session_id,
&healthy_vm_id,
"proc-healthy-3",
);
assert_eq!(
peer_exit, 0,
"peer VM stopped working after a co-located host-backed write \
(stderr: {peer_stderr})"
);
}

fn collect_crash_process_output(
Expand Down