diff --git a/crates/execution/assets/runners/wasm-runner.mjs b/crates/execution/assets/runners/wasm-runner.mjs index 309bf7fcf..a324ce61b 100644 --- a/crates/execution/assets/runners/wasm-runner.mjs +++ b/crates/execution/assets/runners/wasm-runner.mjs @@ -1198,7 +1198,32 @@ function openGuestFileForPathOpen(fd, pathPtr, pathLen, oflags, rightsBase, fdfl return writeGuestUint32(openedFdPtr, openedFd); } -function retainPathOpenDelegateFd(openedFdPtr, guestPath, fdflags) { +function fsOpenNumericFlagsForManagedPath(rightsBase, fdflags) { + const wantsRead = hasReadRights(rightsBase); + const wantsWrite = hasWriteRights(rightsBase); + let flags = wantsWrite ? (wantsRead ? 0o2 : 0o1) : 0; + if ((Number(fdflags) & WASI_FDFLAGS_APPEND) !== 0) { + flags |= 0o2000; + } + return flags; +} + +function openManagedPathIoFd(guestPath, rightsBase, fdflags) { + if (typeof guestPath !== 'string' || guestPath === '/dev/null') { + return null; + } + try { + return fsModule.openSync( + guestPath, + fsOpenNumericFlagsForManagedPath(rightsBase, fdflags), + 0o666, + ); + } catch { + return null; + } +} + +function retainPathOpenDelegateFd(openedFdPtr, guestPath, fdflags, rightsBase) { if (!(instanceMemory instanceof WebAssembly.Memory)) { return WASI_ERRNO_SUCCESS; } @@ -1208,10 +1233,12 @@ function retainPathOpenDelegateFd(openedFdPtr, guestPath, fdflags) { const append = (Number(fdflags) & WASI_FDFLAGS_APPEND) !== 0; retainDelegateFd(openedFd); if (openedFd > 2 && !passthroughHandles.has(openedFd)) { + const ioFd = openManagedPathIoFd(guestPath, rightsBase, fdflags); closedPassthroughFds.delete(openedFd); passthroughHandles.set(openedFd, { kind: 'passthrough', targetFd: openedFd, + ioFd, displayFd: openedFd, refCount: 0, open: true, @@ -1219,6 +1246,9 @@ function retainPathOpenDelegateFd(openedFdPtr, guestPath, fdflags) { typeof guestPath === 'string' && resolveModuleGuestPathToHostMapping(guestPath)?.readOnly === true, append, + position: append && typeof ioFd === 'number' + ? Number(fsModule.fstatSync(ioFd).size ?? 0) + : 0, ...(typeof guestPath === 'string' ? { guestPath } : {}), }); } @@ -1467,6 +1497,12 @@ function releaseFdHandle(handle) { ) { delegateManagedFdClose(handle.targetFd); } + if (handle.refCount === 0 && handle.open && typeof handle.ioFd === 'number') { + try { + fsModule.closeSync(handle.ioFd); + } catch {} + handle.ioFd = null; + } return; } @@ -1688,11 +1724,11 @@ function collectGuestIovBytes(iovs, iovsLen) { throw new Error('WebAssembly memory is not available'); } - const view = new DataView(instanceMemory.buffer); const chunks = []; let totalLength = 0; for (let index = 0; index < (Number(iovsLen) >>> 0); index += 1) { + const view = new DataView(instanceMemory.buffer); const entryOffset = (Number(iovs) >>> 0) + index * 8; const ptr = view.getUint32(entryOffset, true); const len = view.getUint32(entryOffset + 4, true); @@ -1710,11 +1746,11 @@ function writeBytesToGuestIovs(iovs, iovsLen, bytes) { } const source = Buffer.from(bytes ?? []); - const view = new DataView(instanceMemory.buffer); - const memory = new Uint8Array(instanceMemory.buffer); let written = 0; for (let index = 0; index < (Number(iovsLen) >>> 0) && written < source.length; index += 1) { + const view = new DataView(instanceMemory.buffer); + const memory = new Uint8Array(instanceMemory.buffer); const entryOffset = (Number(iovs) >>> 0) + index * 8; const ptr = view.getUint32(entryOffset, true); const len = view.getUint32(entryOffset + 4, true); @@ -4188,6 +4224,54 @@ function hostFsModeFromStat(stat) { return Number.isInteger(mode) && mode > 0 ? mode >>> 0 : 0; } +const hostFsSizeByGuestPath = new Map(); +// Bound the per-path size cache so a guest truncating many distinct paths cannot +// grow it without limit. Entries are insertion-ordered, so evicting the oldest +// key is a cheap LRU-ish bound. +const HOST_FS_SIZE_CACHE_MAX_ENTRIES = 4096; +let hostFsSizeCacheEvictionWarned = false; + +function forgetHostFsSize(guestPath) { + if (typeof guestPath !== 'string') { + return; + } + hostFsSizeByGuestPath.delete(path.posix.normalize(guestPath)); +} + +function rememberHostFsSize(guestPath, size) { + if (typeof guestPath !== 'string') { + return; + } + const normalized = path.posix.normalize(guestPath); + if (!Number.isFinite(size) || size < 0) { + hostFsSizeByGuestPath.delete(normalized); + return; + } + if ( + !hostFsSizeByGuestPath.has(normalized) && + hostFsSizeByGuestPath.size >= HOST_FS_SIZE_CACHE_MAX_ENTRIES + ) { + const oldest = hostFsSizeByGuestPath.keys().next().value; + if (oldest !== undefined) { + hostFsSizeByGuestPath.delete(oldest); + } + if (!hostFsSizeCacheEvictionWarned) { + hostFsSizeCacheEvictionWarned = true; + traceHostProcess('host-fs-size-cache-evict', { + max: HOST_FS_SIZE_CACHE_MAX_ENTRIES, + }); + } + } + hostFsSizeByGuestPath.set(normalized, BigInt(Math.trunc(size))); +} + +function rememberedHostFsSize(guestPath) { + if (typeof guestPath !== 'string') { + return null; + } + return hostFsSizeByGuestPath.get(path.posix.normalize(guestPath)) ?? null; +} + function resolveHostFsPath(value, fromGuestDir = HOST_FS_GUEST_CWD) { return resolveHostFsMapping(value, fromGuestDir)?.hostPath ?? null; } @@ -4214,15 +4298,48 @@ const hostFsImport = { try { const targetFd = - typeof handle?.targetFd === 'number' ? Number(handle.targetFd) >>> 0 : descriptor; + typeof handle?.ioFd === 'number' + ? Number(handle.ioFd) >>> 0 + : typeof handle?.targetFd === 'number' + ? Number(handle.targetFd) >>> 0 + : descriptor; return hostFsModeFromStat(fsModule.fstatSync(targetFd)) || HOST_FS_MODE_REGULAR; } catch { return HOST_FS_MODE_REGULAR; } }, - path_mode(pathPtr, pathLen, followSymlinks) { + fd_size(fd) { + const descriptor = Number(fd) >>> 0; + try { + const handle = lookupFdHandle(descriptor); + const rememberedSize = rememberedHostFsSize(handle?.guestPath); + if (rememberedSize != null) { + return rememberedSize; + } + if (typeof handle?.ioFd === 'number') { + return BigInt(fsModule.fstatSync(Number(handle.ioFd) >>> 0).size ?? -1); + } + if (typeof handle?.guestPath === 'string') { + const hostPath = resolveHostFsPath(handle.guestPath); + if (typeof hostPath === 'string') { + return BigInt(fsModule.statSync(hostPath).size ?? -1); + } + return BigInt(fsModule.statSync(handle.guestPath).size ?? -1); + } + const targetFd = typeof handle?.targetFd === 'number' + ? Number(handle.targetFd) >>> 0 + : descriptor; + return BigInt(fsModule.fstatSync(targetFd).size ?? -1); + } catch { + return (1n << 64n) - 1n; + } + }, + path_mode(fd, pathPtr, pathLen, followSymlinks) { try { - const target = readGuestString(pathPtr, pathLen); + const target = resolvePathOpenGuestPath(fd, pathPtr, pathLen); + if (typeof target !== 'string') { + return 0; + } const hostPath = resolveHostFsPath(target); if (typeof hostPath !== 'string') { return 0; @@ -4244,9 +4361,40 @@ const hostFsImport = { return 0; } }, - chmod(pathPtr, pathLen, mode) { + path_size(fd, pathPtr, pathLen, followSymlinks) { + const target = resolvePathOpenGuestPath(fd, pathPtr, pathLen); + if (typeof target !== 'string') { + return (1n << 64n) - 1n; + } + const rememberedSize = rememberedHostFsSize(target); + if (rememberedSize != null) { + return rememberedSize; + } + try { - const target = readGuestString(pathPtr, pathLen); + const hostPath = resolveHostFsPath(target); + if (typeof hostPath === 'string') { + const stat = + Number(followSymlinks) === 0 + ? fsModule.lstatSync(hostPath) + : fsModule.statSync(hostPath); + return BigInt(stat?.size ?? -1); + } + const guestStat = + Number(followSymlinks) === 0 + ? fsModule.lstatSync(target) + : fsModule.statSync(target); + return BigInt(guestStat?.size ?? -1); + } catch { + return (1n << 64n) - 1n; + } + }, + chmod(fd, pathPtr, pathLen, mode) { + try { + const target = resolvePathOpenGuestPath(fd, pathPtr, pathLen); + if (typeof target !== 'string') { + return 1; + } const mapping = resolveHostFsMapping(target); if (!mapping || typeof mapping.hostPath !== 'string') { return 1; @@ -4266,6 +4414,67 @@ const hostFsImport = { return 1; } }, + fchmod(fd, mode) { + try { + const descriptor = Number(fd) >>> 0; + const handle = lookupFdHandle(descriptor); + if (handle?.readOnly === true) { + return 1; + } + if (typeof handle?.guestPath === 'string') { + const mapping = resolveHostFsMapping(handle.guestPath); + if (!mapping || typeof mapping.hostPath !== 'string' || mapping.readOnly) { + return 1; + } + fsModule.chmodSync(mapping.hostPath, Number(mode) >>> 0); + return 0; + } + const targetFd = + typeof handle?.targetFd === 'number' ? Number(handle.targetFd) >>> 0 : descriptor; + fsModule.fchmodSync(targetFd, Number(mode) >>> 0); + return 0; + } catch { + traceHostProcess('host-fs-fchmod-fault', {}); + return 1; + } + }, + ftruncate(fd, length) { + try { + const descriptor = Number(fd) >>> 0; + const nextSize = Number(length); + if (!Number.isFinite(nextSize) || nextSize < 0) { + return 1; + } + const handle = lookupFdHandle(descriptor); + if (handle?.readOnly === true) { + return 1; + } + if (typeof handle?.ioFd === 'number') { + fsModule.ftruncateSync(handle.ioFd, nextSize); + if ((handle.position ?? 0) > nextSize) { + handle.position = nextSize; + } + rememberHostFsSize(handle.guestPath, nextSize); + return 0; + } + if (typeof handle?.guestPath === 'string') { + const pathFd = fsModule.openSync(handle.guestPath, 0o1, 0o666); + try { + fsModule.ftruncateSync(pathFd, nextSize); + if ((handle.position ?? 0) > nextSize) { + handle.position = nextSize; + } + rememberHostFsSize(handle.guestPath, nextSize); + } finally { + fsModule.closeSync(pathFd); + } + return 0; + } + return 1; + } catch { + return 1; + } + }, }; wasiImport.clock_time_get = (clockId, precision, resultPtr) => { @@ -4444,7 +4653,7 @@ if (delegatePathOpen) { if (result === WASI_ERRNO_SUCCESS) { return __agentOSWasiMeasurePhase('path_open', 'fd_bookkeeping', () => - retainPathOpenDelegateFd(openedFdPtr, guestPath, fdflags) + retainPathOpenDelegateFd(openedFdPtr, guestPath, fdflags, rightsBase) ); } return result; @@ -4676,6 +4885,42 @@ wasiImport.fd_read = (fd, iovs, iovsLen, nreadPtr) => { } } + if (handle?.kind === 'passthrough' && typeof handle.ioFd === 'number') { + try { + const requestedLength = __agentOSWasiMeasurePhase('fd_read', 'iov_scan', () => { + if (!(instanceMemory instanceof WebAssembly.Memory)) { + return 0; + } + const view = new DataView(instanceMemory.buffer); + let total = 0; + for (let index = 0; index < (Number(iovsLen) >>> 0); index += 1) { + const entryOffset = (Number(iovs) >>> 0) + index * 8; + total += view.getUint32(entryOffset + 4, true); + } + return total >>> 0; + }); + const buffer = Buffer.alloc(requestedLength); + const bytesRead = __agentOSWasiMeasurePhase('fd_read', 'host_io', () => + fsModule.readSync( + handle.ioFd, + buffer, + 0, + requestedLength, + handle.position ?? 0, + ) + ); + handle.position = (handle.position ?? 0) + bytesRead; + const written = __agentOSWasiMeasurePhase('fd_read', 'guest_iov_write', () => + writeBytesToGuestIovs(iovs, iovsLen, buffer.subarray(0, bytesRead)) + ); + return __agentOSWasiMeasurePhase('fd_read', 'result_marshal', () => + writeGuestUint32(nreadPtr, written) + ); + } catch (error) { + return mapSyntheticFsError(error); + } + } + if ( numericFd === 0 && handle?.kind === 'passthrough' && @@ -4774,6 +5019,34 @@ wasiImport.fd_pread = (fd, iovs, iovsLen, offset, nreadPtr) => { } if (handle?.kind === 'passthrough') { + if (typeof handle.ioFd === 'number') { + try { + const requestedLength = (() => { + if (!(instanceMemory instanceof WebAssembly.Memory)) { + return 0; + } + const view = new DataView(instanceMemory.buffer); + let total = 0; + for (let index = 0; index < (Number(iovsLen) >>> 0); index += 1) { + const entryOffset = (Number(iovs) >>> 0) + index * 8; + total += view.getUint32(entryOffset + 4, true); + } + return total >>> 0; + })(); + const buffer = Buffer.alloc(requestedLength); + const bytesRead = fsModule.readSync( + handle.ioFd, + buffer, + 0, + requestedLength, + Number(offset), + ); + const written = writeBytesToGuestIovs(iovs, iovsLen, buffer.subarray(0, bytesRead)); + return writeGuestUint32(nreadPtr, written); + } catch (error) { + return mapSyntheticFsError(error); + } + } return delegateFdPread ? delegateFdPread(handle.targetFd, iovs, iovsLen, offset, nreadPtr) : WASI_ERRNO_BADF; @@ -4810,6 +5083,21 @@ wasiImport.fd_pwrite = (fd, iovs, iovsLen, offset, nwrittenPtr) => { if (handle.readOnly === true) { return WASI_ERRNO_ROFS; } + if (typeof handle.ioFd === 'number') { + try { + const bytes = collectGuestIovBytes(iovs, iovsLen); + const written = fsModule.writeSync( + handle.ioFd, + bytes, + 0, + bytes.length, + Number(offset), + ); + return writeGuestUint32(nwrittenPtr, written); + } catch (error) { + return mapSyntheticFsError(error); + } + } return delegateManagedFdPwrite ? delegateManagedFdPwrite(handle.targetFd, iovs, iovsLen, offset, nwrittenPtr) : WASI_ERRNO_BADF; @@ -4860,6 +5148,17 @@ wasiImport.fd_seek = (fd, offset, whence, newOffsetPtr) => { } if (handle?.kind === 'passthrough') { + if (typeof handle.ioFd === 'number') { + try { + const next = seekGuestFileHandle(handle, offset, whence); + if (next == null) { + return WASI_ERRNO_INVAL; + } + return writeGuestUint64(newOffsetPtr, next); + } catch { + return WASI_ERRNO_FAULT; + } + } return delegateManagedFdSeek ? delegateManagedFdSeek(handle.targetFd, offset, whence, newOffsetPtr) : WASI_ERRNO_BADF; @@ -4885,6 +5184,9 @@ wasiImport.fd_tell = (fd, offsetPtr) => { } if (handle?.kind === 'passthrough') { + if (typeof handle.ioFd === 'number') { + return writeGuestUint64(offsetPtr, BigInt(handle.position ?? 0)); + } return delegateManagedFdTell ? delegateManagedFdTell(handle.targetFd, offsetPtr) : WASI_ERRNO_BADF; @@ -5017,6 +5319,13 @@ wasiImport.fd_filestat_get = (fd, statPtr) => { } if (handle?.kind === 'passthrough') { + if (typeof handle.ioFd === 'number') { + try { + return writeGuestFilestat(statPtr, fsModule.fstatSync(handle.ioFd)); + } catch (error) { + return mapSyntheticFsError(error); + } + } return delegateManagedFdFilestatGet ? delegateManagedFdFilestatGet(handle.targetFd, statPtr) : WASI_ERRNO_BADF; @@ -5040,6 +5349,7 @@ wasiImport.fd_filestat_set_size = (fd, size) => { if ((handle.position ?? 0) > nextSize) { handle.position = nextSize; } + rememberHostFsSize(handle.guestPath, nextSize); return WASI_ERRNO_SUCCESS; } catch (error) { return mapSyntheticFsError(error); @@ -5050,6 +5360,37 @@ wasiImport.fd_filestat_set_size = (fd, size) => { if (handle.readOnly === true) { return WASI_ERRNO_ROFS; } + if (typeof handle.ioFd === 'number') { + try { + const nextSize = Number(size); + fsModule.ftruncateSync(handle.ioFd, nextSize); + if ((handle.position ?? 0) > nextSize) { + handle.position = nextSize; + } + rememberHostFsSize(handle.guestPath, nextSize); + return WASI_ERRNO_SUCCESS; + } catch (error) { + return mapSyntheticFsError(error); + } + } + if (typeof handle.guestPath === 'string') { + try { + const nextSize = Number(size); + const pathFd = fsModule.openSync(handle.guestPath, 0o1, 0o666); + try { + fsModule.ftruncateSync(pathFd, nextSize); + if ((handle.position ?? 0) > nextSize) { + handle.position = nextSize; + } + rememberHostFsSize(handle.guestPath, nextSize); + } finally { + fsModule.closeSync(pathFd); + } + return WASI_ERRNO_SUCCESS; + } catch (error) { + return mapSyntheticFsError(error); + } + } return delegateManagedFdFilestatSetSize ? delegateManagedFdFilestatSetSize(handle.targetFd, size) : WASI_ERRNO_BADF; @@ -5178,6 +5519,30 @@ wasiImport.fd_write = (fd, iovs, iovsLen, nwrittenPtr) => { if (handle.readOnly === true) { return WASI_ERRNO_ROFS; } + if (typeof handle.ioFd === 'number') { + try { + const bytes = __agentOSWasiMeasurePhase('fd_write', 'guest_iov_collect', () => + collectGuestIovBytes(iovs, iovsLen) + ); + const written = __agentOSWasiMeasurePhase('fd_write', 'host_io', () => + writeBytesToGuestFileHandle({ ...handle, targetFd: handle.ioFd }, bytes) + ); + if (handle.append) { + handle.position = Number(fsModule.fstatSync(handle.ioFd).size ?? 0); + } else { + handle.position = (handle.position ?? 0) + written; + } + // The write grew/changed the file; a size remembered from a prior + // truncate is now stale. Drop it so fd_size/path_size fall through to + // the authoritative fstat rather than reporting the old length. + forgetHostFsSize(handle.guestPath); + return __agentOSWasiMeasurePhase('fd_write', 'result_marshal', () => + writeGuestUint32(nwrittenPtr, written) + ); + } catch (error) { + return mapSyntheticFsError(error); + } + } return delegateManagedFdWrite ? __agentOSWasiMeasurePhase('fd_write', 'delegate_call', () => delegateManagedFdWrite(handle.targetFd, iovs, iovsLen, nwrittenPtr) diff --git a/crates/execution/src/node_import_cache.rs b/crates/execution/src/node_import_cache.rs index ee1c51701..b3052891a 100644 --- a/crates/execution/src/node_import_cache.rs +++ b/crates/execution/src/node_import_cache.rs @@ -3539,6 +3539,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), @@ -3616,6 +3623,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) => diff --git a/crates/sidecar/src/filesystem.rs b/crates/sidecar/src/filesystem.rs index 6a61e9847..a2f158e6a 100644 --- a/crates/sidecar/src/filesystem.rs +++ b/crates/sidecar/src/filesystem.rs @@ -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, @@ -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, ) @@ -1388,17 +1419,40 @@ 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 + 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))?; + } + process + .mapped_host_fd_mut(fd) + .expect("mapped guest fd present for ftruncate") .file .set_len(length) - .map(|()| Value::Null) .map_err(|error| { SidecarError::Io(format!( - "failed to truncate mapped guest fd {fd} -> {}: {error}", - mapped.path.display() + "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) @@ -1597,6 +1651,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( @@ -1605,19 +1668,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( @@ -3349,6 +3418,7 @@ fn materialize_mapped_host_path_from_kernel( fn open_mapped_host_fd( process: &mut ActiveProcess, host_path: PathBuf, + guest_path: Option, proc_path: PathBuf, flags: u32, ) -> Result { @@ -3386,6 +3456,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)) } diff --git a/crates/sidecar/src/state.rs b/crates/sidecar/src/state.rs index ccb5c4804..05347b27e 100644 --- a/crates/sidecar/src/state.rs +++ b/crates/sidecar/src/state.rs @@ -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, } pub(crate) struct ActiveCipherSession { diff --git a/registry/native/c/Makefile b/registry/native/c/Makefile index 1e7bcc254..21f1ccc1b 100644 --- a/registry/native/c/Makefile +++ b/registry/native/c/Makefile @@ -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) diff --git a/registry/native/c/programs/fs_probe.c b/registry/native/c/programs/fs_probe.c new file mode 100644 index 000000000..8194b6d96 --- /dev/null +++ b/registry/native/c/programs/fs_probe.c @@ -0,0 +1,443 @@ +/* fs_probe.c - deterministic native-vs-VM filesystem parity probe. + * + * argv[1] is a scratch directory. Run this once as a native Linux binary and + * once inside the VM, then diff stdout. The output intentionally avoids file + * descriptor numbers, absolute paths, uid/gid, and locale-dependent strings. + */ +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static const char *scratch_dir; +static char path_buffers[6][1024]; +static unsigned path_index; +static unsigned failures; + +static const char *path_for(const char *name) { + char *buffer = path_buffers[path_index++ % 6]; + snprintf(buffer, 1024, "%s/%s", scratch_dir, name); + return buffer; +} + +static void cleanup(void) { + unlink(path_for("mode.txt")); + unlink(path_for("append.txt")); + unlink(path_for("excl.txt")); + unlink(path_for("rw.txt")); + unlink(path_for("rename_src.txt")); + unlink(path_for("rename_dst.txt")); + unlink(path_for("link.txt")); + unlink(path_for("trunc.txt")); + unlink(path_for("large.bin")); + rmdir(path_for("dir")); +} + +static void report(const char *label, int ok) { + printf("%-32s %s\n", label, ok ? "ok" : "FAIL"); + if (!ok) { + failures++; + } +} + +static void expect_ok(const char *label, long ret) { + int err = errno; + int ok = ret >= 0; + printf("%-32s %s errno=%d\n", label, ok ? "ok" : "FAIL", ok ? 0 : err); + if (!ok) { + failures++; + } +} + +static const char *expected_errno_name(int err) { + if (err == EACCES) { + return "EACCES"; + } + if (err == EEXIST) { + return "EEXIST"; + } + return NULL; +} + +static void expect_errno(const char *label, long ret, int expected_errno) { + int err = errno; + int ok = ret < 0 && err == expected_errno; + const char *name = ret < 0 ? expected_errno_name(err) : NULL; + if (name) { + printf("%-32s %s errno=%s\n", label, ok ? "ok" : "FAIL", name); + } else { + printf("%-32s %s errno=%d\n", label, ok ? "ok" : "FAIL", ret < 0 ? err : 0); + } + if (!ok) { + failures++; + } +} + +static int write_all(int fd, const void *data, size_t len) { + const char *cursor = (const char *)data; + while (len > 0) { + ssize_t written = write(fd, cursor, len); + if (written < 0) { + return -1; + } + if (written == 0) { + errno = EIO; + return -1; + } + cursor += written; + len -= (size_t)written; + } + return 0; +} + +static int read_exact_file(const char *path, char *buffer, size_t len) { + int fd = open(path, O_RDONLY); + if (fd < 0) { + return -1; + } + size_t offset = 0; + while (offset < len) { + ssize_t n = read(fd, buffer + offset, len - offset); + if (n < 0) { + int err = errno; + close(fd); + errno = err; + return -1; + } + if (n == 0) { + close(fd); + errno = EIO; + return -1; + } + offset += (size_t)n; + } + close(fd); + return 0; +} + +static void write_text_file(const char *path, const char *text, mode_t mode) { + int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, mode); + if (fd < 0) { + return; + } + (void)write_all(fd, text, strlen(text)); + close(fd); +} + +static void mode_line(const char *label, const char *path, mode_t expected_mode, + off_t expected_size, int expected_type) { + struct stat st; + errno = 0; + if (stat(path, &st) != 0) { + printf("%-32s FAIL errno=%d\n", label, errno); + failures++; + return; + } + int ok = ((st.st_mode & 07777) == expected_mode) && + (st.st_size == expected_size) && + ((st.st_mode & S_IFMT) == expected_type); + printf("%-32s %s mode=%04o size=%lld type=%o\n", label, + ok ? "ok" : "FAIL", (unsigned)(st.st_mode & 07777), + (long long)st.st_size, (unsigned)(st.st_mode & S_IFMT)); + if (!ok) { + failures++; + } +} + +static void fmode_line(const char *label, int fd, mode_t expected_mode, + off_t expected_size, int expected_type) { + struct stat st; + errno = 0; + if (fstat(fd, &st) != 0) { + printf("%-32s FAIL errno=%d\n", label, errno); + failures++; + return; + } + int ok = ((st.st_mode & 07777) == expected_mode) && + (st.st_size == expected_size) && + ((st.st_mode & S_IFMT) == expected_type); + printf("%-32s %s mode=%04o size=%lld type=%o\n", label, + ok ? "ok" : "FAIL", (unsigned)(st.st_mode & 07777), + (long long)st.st_size, (unsigned)(st.st_mode & S_IFMT)); + if (!ok) { + failures++; + } +} + +static void stat_and_access_cases(void) { + const char *mode_path = path_for("mode.txt"); + int fd; + + errno = 0; + fd = open(mode_path, O_RDWR | O_CREAT | O_TRUNC, 0644); + expect_ok("open_create_0644", fd); + if (fd >= 0) { + errno = 0; + expect_ok("fchmod_initial_0644", fchmod(fd, 0644)); + errno = 0; + expect_ok("write_initial", write_all(fd, "hello", 5)); + fmode_line("fstat_initial_mode", fd, 0644, 5, S_IFREG); + close(fd); + } + mode_line("stat_initial_mode", mode_path, 0644, 5, S_IFREG); + + errno = 0; + expect_ok("access_F_OK", access(mode_path, F_OK)); + errno = 0; + expect_ok("access_R_OK", access(mode_path, R_OK)); + errno = 0; + expect_ok("access_W_OK", access(mode_path, W_OK)); + errno = 0; + expect_errno("access_X_OK_no_exec", access(mode_path, X_OK), EACCES); + + errno = 0; + expect_ok("chmod_0600", chmod(mode_path, 0600)); + mode_line("stat_after_chmod_0600", mode_path, 0600, 5, S_IFREG); + + errno = 0; + fd = open(mode_path, O_RDWR); + expect_ok("open_existing_RDWR", fd); + if (fd >= 0) { + errno = 0; + expect_ok("fchmod_0640", fchmod(fd, 0640)); + fmode_line("fstat_after_fchmod_0640", fd, 0640, 5, S_IFREG); + close(fd); + } + mode_line("stat_after_fchmod_0640", mode_path, 0640, 5, S_IFREG); + + errno = 0; + expect_ok("chmod_0755", chmod(mode_path, 0755)); + errno = 0; + expect_ok("access_X_OK_exec", access(mode_path, X_OK)); +} + +static void create_write_cases(void) { + const char *append_path = path_for("append.txt"); + const char *excl_path = path_for("excl.txt"); + const char *rw_path = path_for("rw.txt"); + char buffer[16] = {0}; + int fd; + + errno = 0; + fd = open(append_path, O_WRONLY | O_CREAT | O_TRUNC, 0644); + expect_ok("open_WRONLY_CREAT_TRUNC", fd); + if (fd >= 0) { + errno = 0; + expect_ok("write_append_first", write_all(fd, "abc", 3)); + errno = 0; + expect_ok("fsync_append_file", fsync(fd)); + close(fd); + } + + errno = 0; + fd = open(append_path, O_WRONLY | O_APPEND); + expect_ok("open_append", fd); + if (fd >= 0) { + errno = 0; + expect_ok("write_append_second", write_all(fd, "XYZ", 3)); + close(fd); + } + errno = 0; + report("readback_append_exact", + read_exact_file(append_path, buffer, 6) == 0 && + memcmp(buffer, "abcXYZ", 6) == 0); + + errno = 0; + fd = open(excl_path, O_WRONLY | O_CREAT | O_EXCL, 0644); + expect_ok("open_CREAT_EXCL_new", fd); + if (fd >= 0) { + close(fd); + } + errno = 0; + fd = open(excl_path, O_WRONLY | O_CREAT | O_EXCL, 0644); + expect_errno("open_CREAT_EXCL_existing", fd, EEXIST); + if (fd >= 0) { + close(fd); + } + + errno = 0; + fd = open(rw_path, O_RDWR | O_CREAT | O_TRUNC, 0644); + expect_ok("open_RDWR_create", fd); + if (fd >= 0) { + errno = 0; + expect_ok("write_read_file", write_all(fd, "native", 6)); + errno = 0; + expect_ok("seek_start", lseek(fd, 0, SEEK_SET)); + memset(buffer, 0, sizeof(buffer)); + errno = 0; + report("read_same_fd_exact", read(fd, buffer, 6) == 6 && + memcmp(buffer, "native", 6) == 0); + close(fd); + } +} + +static void directory_and_link_cases(void) { + struct stat st; + char link_buffer[256] = {0}; + ssize_t link_len; + + errno = 0; + expect_ok("mkdir_dir", mkdir(path_for("dir"), 0755)); + errno = 0; + expect_errno("mkdir_dir_EEXIST", mkdir(path_for("dir"), 0755), EEXIST); + errno = 0; + expect_ok("rmdir_dir", rmdir(path_for("dir"))); + + write_text_file(path_for("rename_src.txt"), "src", 0644); + write_text_file(path_for("rename_dst.txt"), "dst", 0644); + errno = 0; + expect_ok("rename_overwrite", rename(path_for("rename_src.txt"), + path_for("rename_dst.txt"))); + memset(link_buffer, 0, sizeof(link_buffer)); + report("rename_readback_exact", + read_exact_file(path_for("rename_dst.txt"), link_buffer, 3) == 0 && + memcmp(link_buffer, "src", 3) == 0); + + errno = 0; + expect_ok("symlink_relative", symlink("rename_dst.txt", path_for("link.txt"))); + errno = 0; + expect_ok("lstat_symlink", lstat(path_for("link.txt"), &st)); + report("lstat_symlink_type", (st.st_mode & S_IFMT) == S_IFLNK); + errno = 0; + link_len = readlink(path_for("link.txt"), link_buffer, sizeof(link_buffer) - 1); + if (link_len >= 0) { + link_buffer[link_len] = '\0'; + } + report("readlink_relative_exact", + link_len == (ssize_t)strlen("rename_dst.txt") && + strcmp(link_buffer, "rename_dst.txt") == 0); +} + +static void fd_and_metadata_cases(void) { + const char *trunc_path = path_for("trunc.txt"); + struct timespec ts[2] = { + {.tv_sec = 1700000000, .tv_nsec = 123000000}, + {.tv_sec = 1700000001, .tv_nsec = 456000000}, + }; + int fd; + int fd2; + + write_text_file(trunc_path, "truncate-me", 0644); + errno = 0; + expect_ok("truncate_4", truncate(trunc_path, 4)); + mode_line("stat_after_truncate_4", trunc_path, 0644, 4, S_IFREG); + + errno = 0; + fd = open(trunc_path, O_RDWR); + expect_ok("open_trunc_RDWR", fd); + if (fd >= 0) { + errno = 0; + expect_ok("ftruncate_2", ftruncate(fd, 2)); + fmode_line("fstat_after_ftruncate_2", fd, 0644, 2, S_IFREG); + + errno = 0; + fd2 = dup(fd); + expect_ok("dup_fd", fd2); + if (fd2 >= 0) { + close(fd2); + } + + errno = 0; + fd2 = dup2(fd, 42); + expect_ok("dup2_fd_42", fd2); + if (fd2 >= 0) { + close(fd2); + } + + errno = 0; + int flags = fcntl(fd, F_GETFL); + expect_ok("fcntl_GETFL", flags); + if (flags >= 0) { + errno = 0; + expect_ok("fcntl_SETFL_APPEND", fcntl(fd, F_SETFL, flags | O_APPEND)); + errno = 0; + flags = fcntl(fd, F_GETFL); + report("fcntl_APPEND_visible", flags >= 0 && (flags & O_APPEND) != 0); + } + close(fd); + } + + errno = 0; + expect_ok("utimensat_set", utimensat(AT_FDCWD, trunc_path, ts, 0)); +} + +static void memory_growth_write_case(void) { + const size_t chunk_size = 64 * 1024; + const size_t chunks = 100; + const size_t allocation_size = 24 * 1024 * 1024; + const char *large_path = path_for("large.bin"); + unsigned char *allocation = (unsigned char *)malloc(allocation_size); + int fd; + struct stat st; + + report("malloc_memory_growth_buffer", allocation != NULL); + if (allocation == NULL) { + return; + } + for (size_t i = 0; i < allocation_size; i++) { + allocation[i] = (unsigned char)(i * 31u + 7u); + } + unsigned char *chunk = allocation + allocation_size - chunk_size; + + errno = 0; + fd = open(large_path, O_WRONLY | O_CREAT | O_TRUNC, 0644); + expect_ok("large_open_create", fd); + if (fd >= 0) { + int ok = 1; + for (size_t i = 0; i < chunks; i++) { + if (write_all(fd, chunk, chunk_size) != 0) { + ok = 0; + break; + } + } + printf("%-32s %s errno=%d bytes=%llu\n", "large_write_after_growth", + ok ? "ok" : "FAIL", ok ? 0 : errno, + (unsigned long long)(ok ? chunk_size * chunks : 0)); + if (!ok) { + failures++; + } + close(fd); + } + + errno = 0; + if (stat(large_path, &st) == 0) { + report("large_stat_size", st.st_size == (off_t)(chunk_size * chunks)); + } else { + printf("%-32s FAIL errno=%d\n", "large_stat_size", errno); + failures++; + } + + char verify[64]; + errno = 0; + report("large_readback_prefix", + read_exact_file(large_path, verify, sizeof(verify)) == 0 && + memcmp(verify, chunk, sizeof(verify)) == 0); + free(allocation); +} + +int main(int argc, char **argv) { + scratch_dir = argc > 1 ? argv[1] : "/tmp/fs-probe"; +#if !defined(__wasi__) && !defined(__wasm32__) + umask(0); +#endif + mkdir(scratch_dir, 0777); + cleanup(); + + stat_and_access_cases(); + create_write_cases(); + directory_and_link_cases(); + fd_and_metadata_cases(); + memory_growth_write_case(); + + cleanup(); + printf("SUMMARY failures=%u\n", failures); + return failures == 0 ? 0 : 1; +} diff --git a/registry/native/patches/wasi-libc/0016-host-fs-mode-and-chmod.patch b/registry/native/patches/wasi-libc/0016-host-fs-mode-and-chmod.patch new file mode 100644 index 000000000..d21785e91 --- /dev/null +++ b/registry/native/patches/wasi-libc/0016-host-fs-mode-and-chmod.patch @@ -0,0 +1,263 @@ +Teach WASI libc about host filesystem mode bits and chmod. + +WASI preview1 filestat exposes file type but not POSIX permission bits. Agent OS +provides those bits through the shared host_fs import so native C tools can use +ordinary stat/access/chmod semantics without per-program patches. + +--- a/libc-bottom-half/cloudlibc/src/libc/sys/stat/stat_impl.h ++++ b/libc-bottom-half/cloudlibc/src/libc/sys/stat/stat_impl.h +@@ -8,6 +8,7 @@ + #include + #include + #include ++#include + + static_assert(S_ISBLK(S_IFBLK), "Value mismatch"); + static_assert(S_ISCHR(S_IFCHR), "Value mismatch"); +@@ -20,7 +21,44 @@ static_assert(S_ISREG(S_IFREG), "Value mismatch"); + static_assert(S_ISSOCK(S_IFSOCK), "Value mismatch"); + +-static inline void to_public_stat(const __wasi_filestat_t *in, +- struct stat *out) { ++uint32_t __agentos_host_fd_mode(uint32_t fd) __attribute__(( ++ __import_module__("host_fs"), ++ __import_name__("fd_mode") ++)); ++ ++uint32_t __agentos_host_path_mode(uint32_t fd, const char *path, size_t path_len, ++ uint32_t follow_symlinks) __attribute__(( ++ __import_module__("host_fs"), ++ __import_name__("path_mode") ++)); ++ ++uint64_t __agentos_host_fd_size(uint32_t fd) __attribute__(( ++ __import_module__("host_fs"), ++ __import_name__("fd_size") ++)); ++ ++uint64_t __agentos_host_path_size(uint32_t fd, const char *path, size_t path_len, ++ uint32_t follow_symlinks) __attribute__(( ++ __import_module__("host_fs"), ++ __import_name__("path_size") ++)); ++ ++static inline mode_t host_permissions(uint32_t mode) { ++ return mode & 07777; ++} ++ ++static inline void apply_host_stat_metadata(struct stat *out, ++ uint32_t host_mode, ++ uint64_t host_size) { ++ out->st_mode |= host_permissions(host_mode); ++ if (host_size != UINT64_MAX) { ++ out->st_size = (off_t)host_size; ++ } ++} ++ ++static inline void to_public_stat_with_mode(const __wasi_filestat_t *in, ++ struct stat *out, ++ uint32_t host_mode, ++ uint64_t host_size) { + // Ensure that we don't truncate any values. + static_assert(sizeof(in->dev) == sizeof(out->st_dev), "Size mismatch"); + static_assert(sizeof(in->ino) == sizeof(out->st_ino), "Size mismatch"); +@@ -43,6 +80,7 @@ static inline void to_public_stat_with_mode(const __wasi_filestat_t *in, + .st_ctim = timestamp_to_timespec(in->ctim), + }; + ++ apply_host_stat_metadata(out, host_mode, host_size); + // Convert file type to legacy types encoded in st_mode. + switch (in->filetype) { + case __WASI_FILETYPE_BLOCK_DEVICE: +@@ -67,6 +105,11 @@ static inline void to_public_stat_with_mode(const __wasi_filestat_t *in, + break; + } + } ++ ++static inline void to_public_stat(const __wasi_filestat_t *in, ++ struct stat *out) { ++ to_public_stat_with_mode(in, out, 0, UINT64_MAX); ++} + + static inline bool utimens_get_timestamps(const struct timespec *times, + __wasi_timestamp_t *st_atim, +--- a/libc-bottom-half/cloudlibc/src/libc/sys/stat/fstat.c ++++ b/libc-bottom-half/cloudlibc/src/libc/sys/stat/fstat.c +@@ -15,6 +15,7 @@ int fstat(int fildes, struct stat *buf) { + errno = error; + return -1; + } +- to_public_stat(&internal_stat, buf); ++ to_public_stat_with_mode(&internal_stat, buf, __agentos_host_fd_mode(fildes), ++ __agentos_host_fd_size(fildes)); + return 0; + } +--- a/libc-bottom-half/cloudlibc/src/libc/sys/stat/fstatat.c ++++ b/libc-bottom-half/cloudlibc/src/libc/sys/stat/fstatat.c +@@ -27,6 +27,13 @@ int __wasilibc_nocwd_fstatat(int fd, const char *restrict path, struct stat *res + errno = error; + return -1; + } +- to_public_stat(&internal_stat, buf); ++ uint32_t follow_symlinks = ++ (lookup_flags & __WASI_LOOKUPFLAGS_SYMLINK_FOLLOW) != 0; ++ size_t path_len = strlen(path); ++ to_public_stat_with_mode(&internal_stat, buf, ++ __agentos_host_path_mode(fd, path, path_len, ++ follow_symlinks), ++ __agentos_host_path_size(fd, path, path_len, ++ follow_symlinks)); + return 0; + } +--- a/libc-bottom-half/cloudlibc/src/libc/unistd/ftruncate.c ++++ b/libc-bottom-half/cloudlibc/src/libc/unistd/ftruncate.c +@@ -5,6 +5,12 @@ + #include ++#include + #include + ++uint32_t __agentos_host_ftruncate(uint32_t fd, uint64_t length) __attribute__(( ++ __import_module__("host_fs"), ++ __import_name__("ftruncate") ++)); ++ + int ftruncate(int fildes, off_t length) { + if (length < 0) { + errno = EINVAL; +@@ -12,8 +18,12 @@ int ftruncate(int fildes, off_t length) { + } + __wasi_filesize_t st_size = length; + __wasi_errno_t error = ++ __agentos_host_ftruncate((uint32_t)fildes, (uint64_t)st_size); ++ if (error != 0) { ++ error = + __wasi_fd_filestat_set_size(fildes, st_size); ++ } + if (error != 0) { + errno = error; + return -1; + } +--- a/libc-bottom-half/cloudlibc/src/libc/unistd/faccessat.c ++++ b/libc-bottom-half/cloudlibc/src/libc/unistd/faccessat.c +@@ -6,8 +6,16 @@ + #include + #include ++#include + #include ++#include + #include + ++uint32_t __agentos_host_path_mode(uint32_t fd, const char *path, size_t path_len, ++ uint32_t follow_symlinks) __attribute__(( ++ __import_module__("host_fs"), ++ __import_name__("path_mode") ++)); ++ + int __wasilibc_nocwd_faccessat(int fd, const char *path, int amode, int flag) { + // Validate function parameters. + if ((amode & ~(F_OK | R_OK | W_OK | X_OK)) != 0 || +@@ -26,6 +35,22 @@ int __wasilibc_nocwd_faccessat(int fd, const char *path, int amode, int flag) { + return -1; + } + ++ uint32_t mode = __agentos_host_path_mode(fd, path, strlen(path), 1); ++ if ((amode & R_OK) != 0 && (mode & 0444) == 0) { ++ errno = EACCES; ++ return -1; ++ } ++ if ((amode & W_OK) != 0 && (mode & 0222) == 0) { ++ errno = EACCES; ++ return -1; ++ } ++ // Match Linux access(2): execute for non-directories requires at least one ++ // execute bit. This is true even for root. ++ if ((amode & X_OK) != 0 && (mode & 0111) == 0) { ++ errno = EACCES; ++ return -1; ++ } ++ + // Test whether the requested access rights are present on the + // directory file descriptor. + if (amode != 0) { +--- a/libc-bottom-half/sources/posix.c ++++ b/libc-bottom-half/sources/posix.c +@@ -13,6 +13,25 @@ + #include + #include + ++#include ++#include ++ ++uint32_t __agentos_host_chmod(uint32_t fd, const char *path, size_t path_len, ++ uint32_t mode) __attribute__(( ++ __import_module__("host_fs"), ++ __import_name__("chmod") ++)); ++ ++uint32_t __agentos_host_fchmod(uint32_t fd, uint32_t mode) __attribute__(( ++ __import_module__("host_fs"), ++ __import_name__("fchmod") ++)); ++ ++static int chmod_errno_from_host_result(uint32_t result) { ++ errno = result == 0 ? 0 : EIO; ++ return result == 0 ? 0 : -1; ++} ++ + int __wasilibc_find_relpath_fallback( + const char *path, + const char **abs_prefix, +@@ -321,24 +340,38 @@ int chmod(const char *path, mode_t mode) { +- // TODO: We plan to support this eventually in WASI, but not yet. +- // Meanwhile, we provide a stub so that libc++'s `` +- // implementation will build unmodified. +- errno = ENOSYS; +- return -1; ++ char *relative_path; ++ int dirfd = find_relpath(path, &relative_path); ++ if (dirfd == -1) { ++ errno = ENOENT; ++ return -1; ++ } ++ ++ return chmod_errno_from_host_result(__agentos_host_chmod( ++ (uint32_t)dirfd, ++ relative_path, ++ strlen(relative_path), ++ (uint32_t)(mode & 07777))); + } + + int fchmod(int fd, mode_t mode) { +- // TODO: We plan to support this eventually in WASI, but not yet. +- // Meanwhile, we provide a stub so that libc++'s `` +- // implementation will build unmodified. +- errno = ENOSYS; +- return -1; ++ return chmod_errno_from_host_result(__agentos_host_fchmod( ++ (uint32_t)fd, ++ (uint32_t)(mode & 07777))); + } + + int fchmodat(int fd, const char *path, mode_t mode, int flag) { +- // TODO: We plan to support this eventually in WASI, but not yet. +- // Meanwhile, we provide a stub so that libc++'s `` +- // implementation will build unmodified. +- errno = ENOSYS; +- return -1; ++ if ((flag & ~AT_SYMLINK_NOFOLLOW) != 0) { ++ errno = EINVAL; ++ return -1; ++ } ++ if ((flag & AT_SYMLINK_NOFOLLOW) != 0) { ++ errno = EOPNOTSUPP; ++ return -1; ++ } ++ ++ return chmod_errno_from_host_result(__agentos_host_chmod( ++ (uint32_t)fd, ++ path, ++ strlen(path), ++ (uint32_t)(mode & 07777))); + } + + int statvfs(const char *__restrict path, struct statvfs *__restrict buf) {