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
405 changes: 393 additions & 12 deletions crates/execution/assets/runners/wasm-runner.mjs

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions crates/execution/src/node_import_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3521,6 +3521,13 @@ function createRpcBackedFsSync(fromGuestDir = '/') {
}
return createGuestFsStats(callSync('fs.fstatSync', [normalizedFd]));
},
ftruncateSync: (fd, len) => {
const normalizedFd = normalizeFsFd(fd);
if (isStdioFd(normalizedFd)) {
return hostFs.ftruncateSync(normalizedFd, len);
}
return callSync('fs.ftruncateSync', [normalizedFd, normalizeFsInteger(len ?? 0, 'length')]);
},
linkSync: (existingPath, newPath) =>
callSync('fs.linkSync', [
resolveGuestFsPath(existingPath, fromGuestDir),
Expand Down Expand Up @@ -3598,6 +3605,11 @@ function createRpcBackedFsSync(fromGuestDir = '/') {
resolveGuestFsPath(linkPath, fromGuestDir),
type,
]),
truncateSync: (target, len) =>
callSync('fs.truncateSync', [
resolveGuestFsPath(target, fromGuestDir),
normalizeFsInteger(len ?? 0, 'length'),
]),
unlinkSync: (target) =>
callSync('fs.unlinkSync', [resolveGuestFsPath(target, fromGuestDir)]),
utimesSync: (target, atime, mtime) =>
Expand Down
99 changes: 84 additions & 15 deletions crates/sidecar/src/filesystem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,40 @@ fn kernel_path_error(
other => other,
}
}

fn filesystem_access_denied(path: &str, mode: u32) -> SidecarError {
SidecarError::Execution(format!(
"EACCES: filesystem access denied for {path} with mode {mode:o}"
))
}

fn filesystem_access_mode_is_allowed(file_mode: u32, requested: u32) -> bool {
const ACCESS_MASK: u32 = libc::R_OK as u32 | libc::W_OK as u32 | libc::X_OK as u32;
if requested & ACCESS_MASK == 0 {
return true;
}
if requested & libc::R_OK as u32 != 0 && file_mode & 0o444 == 0 {
return false;
}
if requested & libc::W_OK as u32 != 0 && file_mode & 0o222 == 0 {
return false;
}
if requested & libc::X_OK as u32 != 0 && file_mode & 0o111 == 0 {
return false;
}
true
}
const PYTHON_PYODIDE_CACHE_GUEST_ROOT: &str = "/__agentos_pyodide_cache";
const UTIME_NOW_NSEC: i64 = libc::UTIME_NOW;
const UTIME_OMIT_NSEC: i64 = libc::UTIME_OMIT;

/// Backstop bound on a guest-controlled `ftruncate` length for a mapped host fd.
/// The kernel's configured truncate-size limit is the primary enforcement for
/// paths visible in the VFS; this caps the raw host `set_len` (and covers fds
/// with no kernel-visible guest path) so a hostile length cannot create an
/// enormous sparse host file or drive an unbounded sidecar-side mirror read.
const MAX_MAPPED_TRUNCATE_BYTES: u64 = 4 * 1024 * 1024 * 1024;

#[derive(Debug, Clone)]
struct MappedRuntimeHostPath {
guest_path: String,
Expand Down Expand Up @@ -1177,6 +1207,7 @@ pub(crate) fn service_javascript_fs_sync_rpc(
return open_mapped_host_fd(
process,
host_path,
Some(path.to_string()),
opened.handle.proc_path(),
flags,
)
Expand Down Expand Up @@ -1388,17 +1419,38 @@ pub(crate) fn service_javascript_fs_sync_rpc(
"filesystem ftruncate length",
)?
.unwrap_or(0);
if let Some(mapped) = process.mapped_host_fd_mut(fd) {
return mapped
.file
.set_len(length)
.map(|()| Value::Null)
.map_err(|error| {
SidecarError::Io(format!(
"failed to truncate mapped guest fd {fd} -> {}: {error}",
mapped.path.display()
))
});
if let Some(mapped_guest_path) = process
.mapped_host_fd_mut(fd)
.map(|mapped| mapped.guest_path.clone())
{
// `length` is guest-controlled. Bound it before resizing the host
// file so a hostile value cannot create an enormous sparse host
// file. For a VFS-visible guest path the kernel truncate below is
// the primary (configured) size enforcement and mirrors the new
// length without reading the whole host file into sidecar memory.
if length > MAX_MAPPED_TRUNCATE_BYTES {
return Err(SidecarError::Io(format!(
"ftruncate length {length} exceeds maximum \
{MAX_MAPPED_TRUNCATE_BYTES} for mapped guest fd {fd}"
)));
}
if let Some(guest_path) = mapped_guest_path.as_deref() {
kernel
.truncate(guest_path, length)
.map_err(|error| kernel_path_error("fs.ftruncate", guest_path, error))?;
}
let mapped = process.mapped_host_fd_mut(fd).ok_or_else(|| {
SidecarError::Io(format!(
"mapped guest fd {fd} disappeared during ftruncate"
))
})?;
mapped.file.set_len(length).map_err(|error| {
SidecarError::Io(format!("failed to truncate mapped guest fd {fd}: {error}"))
})?;
if let Some(guest_path) = mapped_guest_path.as_deref() {
mirror_kernel_path_to_process_shadow(kernel, process, kernel_pid, guest_path)?;
}
return Ok(Value::Null);
}
let fd_stat = kernel
.fd_stat(EXECUTION_DRIVER_NAME, kernel_pid, fd)
Expand Down Expand Up @@ -1597,6 +1649,15 @@ pub(crate) fn service_javascript_fs_sync_rpc(
let path =
javascript_sync_rpc_path_arg(process, &request.args, 0, "filesystem access path")?;
let path = path.as_str();
let mode =
javascript_sync_rpc_arg_u32_optional(&request.args, 1, "filesystem access mode")?
.unwrap_or(0);
let valid_mask = libc::R_OK as u32 | libc::W_OK as u32 | libc::X_OK as u32;
if mode & !valid_mask != 0 {
return Err(SidecarError::Execution(format!(
"EINVAL: invalid filesystem access mode {mode:o}"
)));
}
if let Some(mapped_host) = mapped_runtime_host_path_for_read(process, path) {
materialize_mapped_host_path_from_kernel(kernel, kernel_pid, path, &mapped_host)?;
let opened = open_mapped_runtime_beneath(
Expand All @@ -1605,19 +1666,25 @@ pub(crate) fn service_javascript_fs_sync_rpc(
O_PATH_ANCHOR,
Mode::empty(),
)?;
fs::metadata(opened.handle.proc_path()).map_err(|error| {
let metadata = fs::metadata(opened.handle.proc_path()).map_err(|error| {
SidecarError::Io(format!(
"failed to access mapped guest path {} -> {}: {error}",
path,
opened.host_path.display()
))
})?;
if !filesystem_access_mode_is_allowed(metadata.permissions().mode(), mode) {
return Err(filesystem_access_denied(path, mode));
}
return Ok(Value::Null);
}
kernel
let stat = kernel
.stat_for_process(EXECUTION_DRIVER_NAME, kernel_pid, path)
.map(|_| Value::Null)
.map_err(kernel_error)
.map_err(kernel_error)?;
if !filesystem_access_mode_is_allowed(stat.mode, mode) {
return Err(filesystem_access_denied(path, mode));
}
Ok(Value::Null)
}
"fs.copyFileSync" | "fs.promises.copyFile" => {
let source = javascript_sync_rpc_path_arg(
Expand Down Expand Up @@ -3349,6 +3416,7 @@ fn materialize_mapped_host_path_from_kernel(
fn open_mapped_host_fd(
process: &mut ActiveProcess,
host_path: PathBuf,
guest_path: Option<String>,
proc_path: PathBuf,
flags: u32,
) -> Result<Value, SidecarError> {
Expand Down Expand Up @@ -3386,6 +3454,7 @@ fn open_mapped_host_fd(
let fd = process.allocate_mapped_host_fd(crate::state::ActiveMappedHostFd {
file,
path: host_path,
guest_path,
});
Ok(json!(fd))
}
Expand Down
1 change: 1 addition & 0 deletions crates/sidecar/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,7 @@ pub(crate) struct ActiveProcess {
pub(crate) struct ActiveMappedHostFd {
pub(crate) file: File,
pub(crate) path: PathBuf,
pub(crate) guest_path: Option<String>,
}

pub(crate) struct ActiveCipherSession {
Expand Down
67 changes: 66 additions & 1 deletion packages/browser/src/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3247,7 +3247,10 @@ export const POLYFILL_CODE_MAP: Record<string, string> = {
}
return 0o100644;
},
path_mode(pathPtr, pathLen, followSymlinks) {
// Signature must match the node runner's host_fs.path_mode
// (fd, pathPtr, pathLen, followSymlinks). The guest passes the
// directory fd first (3 = cwd preopen); the path is at args 2/3.
path_mode(_fd, pathPtr, pathLen, followSymlinks) {
try {
const guestPath = resolveGuestPath(readString(pathPtr, pathLen));
const stat = Number(followSymlinks) === 0
Expand All @@ -3258,6 +3261,68 @@ export const POLYFILL_CODE_MAP: Record<string, string> = {
return 0;
}
},
// Matches node runner host_fs.chmod(fd, pathPtr, pathLen, mode):
// 0 on success, 1 on failure.
chmod(_fd, pathPtr, pathLen, mode) {
try {
const guestPath = resolveGuestPath(readString(pathPtr, pathLen));
fs().chmodSync(guestPath, Number(mode) >>> 0);
return 0;
} catch {
return 1;
}
},
// The node runner exports 7 host_fs symbols; mirror the full
// contract here so any guest module that imports the rest still
// instantiates in-browser (a missing import is a hard LinkError).
// Sentinels match the node runner: (1<<64)-1 for size, 1 for
// mutations.
fd_size(fd) {
try {
const descriptor = fd >>> 0;
const handle = lookupSyntheticFd(descriptor);
if (
handle &&
handle.kind === "guest-file" &&
typeof handle.targetFd === "number"
) {
return BigInt(fs().fstatSync(handle.targetFd).size ?? -1);
}
const parentEntry =
parentWasi && parentWasi.fdTable && parentWasi.fdTable.get(descriptor);
if (
parentEntry &&
parentEntry.kind === "file" &&
typeof parentEntry.realFd === "number"
) {
return BigInt(fs().fstatSync(parentEntry.realFd).size ?? -1);
}
return (1n << 64n) - 1n;
} catch {
return (1n << 64n) - 1n;
}
},
path_size(_fd, pathPtr, pathLen, followSymlinks) {
try {
const guestPath = resolveGuestPath(readString(pathPtr, pathLen));
const stat = Number(followSymlinks) === 0
? fs().lstatSync(guestPath)
: fs().statSync(guestPath);
return BigInt(stat.size ?? -1);
} catch {
return (1n << 64n) - 1n;
}
},
// Browser fs() has no fd-based fchmod/ftruncate; provide the
// symbols (best-effort failure) so imports resolve. Rust guest
// binaries bypass wasi-libc stat/chmod/truncate, so these paths
// are unreached in practice.
fchmod(_fd, _mode) {
return 1;
},
ftruncate(_fd, _length) {
return 1;
},
},
host_process: {
proc_spawn(argvPtr, argvLen, envpPtr, envpLen, stdinFd, stdoutFd, stderrFd, cwdPtr, cwdLen, retPid) {
Expand Down
2 changes: 1 addition & 1 deletion registry/native/c/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ COMMANDS_DIR ?= ../target/wasm32-wasip1/release/commands
COMMANDS := zip unzip envsubst sqlite3 curl wget duckdb http_get vim

# Programs requiring patched sysroot (Tier 2+ custom host imports)
PATCHED_PROGRAMS := isatty_test getpid_test getppid_test getppid_verify userinfo pipe_test dup_test spawn_child spawn_exit_code pipeline kill_child waitpid_return waitpid_edge syscall_coverage getpwuid_test signal_tests sigaction_self sigaction_behavior delayed_tcp_echo delayed_kill pipe_edge tcp_accept_spawn tcp_echo tcp_server http_server udp_echo unix_socket signal_handler http_get dns_lookup sqlite3_cli curl wget
PATCHED_PROGRAMS := isatty_test getpid_test getppid_test getppid_verify userinfo pipe_test dup_test spawn_child spawn_exit_code pipeline kill_child waitpid_return waitpid_edge syscall_coverage getpwuid_test signal_tests sigaction_self sigaction_behavior delayed_tcp_echo delayed_kill pipe_edge tcp_accept_spawn tcp_echo tcp_server http_server udp_echo unix_socket signal_handler http_get dns_lookup sqlite3_cli curl wget fs_probe

# Discover all .c source files in programs/
ALL_SOURCES := $(wildcard programs/*.c)
Expand Down
Loading
Loading