diff --git a/crates/execution/assets/runners/wasm-runner.mjs b/crates/execution/assets/runners/wasm-runner.mjs index 309bf7fcf..49f63d345 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); @@ -2168,6 +2204,17 @@ function resolveSyntheticHostMapping(value, fromGuestDir = '/') { return resolveModuleGuestPathToHostMapping(guestPath); } +function chmodMappedGuestPath(guestPath, hostPath, mode) { + fsModule.chmodSync(hostPath, mode); + try { + if (typeof guestPath === 'string' && guestPath.length > 0) { + fsModule.chmodSync(guestPath, mode); + } + } catch { + // Best effort: host-mapped paths may not also exist as direct kernel paths. + } +} + function maybeCreateSyntheticCommandResult(command, args, cwd) { const basename = path.posix.basename(String(command || '')); @@ -2182,6 +2229,7 @@ function maybeCreateSyntheticCommandResult(command, args, cwd) { const mode = Number.parseInt(modeArg, 8) >>> 0; try { for (const targetArg of args.slice(1)) { + const guestPath = resolveSyntheticGuestPath(targetArg, cwd || '/'); const mapping = resolveSyntheticHostMapping(targetArg, cwd || '/'); if (!mapping || typeof mapping.hostPath !== 'string') { throw new Error(`No such file or directory: ${targetArg}`); @@ -2191,7 +2239,7 @@ function maybeCreateSyntheticCommandResult(command, args, cwd) { error.code = 'EROFS'; throw error; } - fsModule.chmodSync(mapping.hostPath, mode); + chmodMappedGuestPath(guestPath, mapping.hostPath, mode); } return { exitCode: 0, stdout: '', stderr: '' }; } catch (error) { @@ -4188,6 +4236,27 @@ function hostFsModeFromStat(stat) { return Number.isInteger(mode) && mode > 0 ? mode >>> 0 : 0; } +const hostFsSizeByGuestPath = new Map(); + +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; + } + 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 +4283,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 +4346,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 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 = readGuestString(pathPtr, pathLen); + const target = resolvePathOpenGuestPath(fd, pathPtr, pathLen); + if (typeof target !== 'string') { + return 1; + } const mapping = resolveHostFsMapping(target); if (!mapping || typeof mapping.hostPath !== 'string') { return 1; @@ -4259,13 +4392,74 @@ const hostFsImport = { hostPath: mapping.hostPath, mode: Number(mode) >>> 0, }); - fsModule.chmodSync(mapping.hostPath, Number(mode) >>> 0); + chmodMappedGuestPath(target, mapping.hostPath, Number(mode) >>> 0); return 0; } catch { traceHostProcess('host-fs-chmod-fault', {}); 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; + } + chmodMappedGuestPath(handle.guestPath, 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 +4638,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 +4870,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 +5004,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 +5068,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 +5133,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 +5169,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 +5304,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 +5334,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 +5345,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 +5504,26 @@ 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; + } + 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..fe9a5e505 100644 --- a/crates/sidecar/src/filesystem.rs +++ b/crates/sidecar/src/filesystem.rs @@ -81,6 +81,29 @@ 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; @@ -1177,6 +1200,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, ) @@ -1389,16 +1413,36 @@ pub(crate) fn service_javascript_fs_sync_rpc( )? .unwrap_or(0); if let Some(mapped) = process.mapped_host_fd_mut(fd) { - return mapped + let guest_path = mapped.guest_path.clone(); + let host_path = mapped.path.clone(); + let metadata = 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() )) - }); + }) + .and_then(|()| { + mapped.file.metadata().map_err(|error| { + SidecarError::Io(format!( + "failed to stat truncated mapped guest fd {fd} -> {}: {error}", + mapped.path.display() + )) + }) + })?; + if let Some(guest_path) = guest_path { + mirror_mapped_host_file_to_kernel( + kernel, + process, + kernel_pid, + &guest_path, + &host_path, + &metadata, + )?; + } + return Ok(Value::Null); } let fd_stat = kernel .fd_stat(EXECUTION_DRIVER_NAME, kernel_pid, fd) @@ -1597,6 +1641,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 +1658,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( @@ -2228,6 +2287,32 @@ fn mirror_kernel_path_to_process_shadow( write_process_shadow_file(&shadow_path, guest_path, &bytes) } +fn mirror_mapped_host_file_to_kernel( + kernel: &mut SidecarKernel, + process: &ActiveProcess, + kernel_pid: u32, + guest_path: &str, + host_path: &Path, + metadata: &fs::Metadata, +) -> Result<(), SidecarError> { + let bytes = fs::read(host_path).map_err(|error| { + SidecarError::Io(format!( + "failed to read mapped host file {}: {error}", + host_path.display() + )) + })?; + kernel + .write_file_for_process( + EXECUTION_DRIVER_NAME, + kernel_pid, + guest_path, + bytes, + Some(metadata.permissions().mode() & 0o7777), + ) + .map_err(|error| kernel_path_error("fs.ftruncate", guest_path, error))?; + mirror_kernel_path_to_process_shadow(kernel, process, kernel_pid, guest_path) +} + fn write_process_shadow_file( shadow_path: &Path, guest_path: &str, @@ -3349,6 +3434,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 +3472,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/crates/brush-interactive/0001-wasi-support.patch b/registry/native/patches/crates/brush-interactive/0001-wasi-support.patch index 3a0be1c35..bd64fe387 100644 --- a/registry/native/patches/crates/brush-interactive/0001-wasi-support.patch +++ b/registry/native/patches/crates/brush-interactive/0001-wasi-support.patch @@ -139,19 +139,3 @@ diff -ruN '--exclude=*.orig' a/src/reedline/validator.rs b/src/reedline/validato let shell = tokio::task::block_in_place(|| { tokio::runtime::Handle::current().block_on(self.shell.lock()) }); ---- a/Cargo.toml -+++ b/Cargo.toml -@@ -91,6 +91,13 @@ - "signal", - ] - -+[target."cfg(target_arch = \"wasm32\")".dependencies.tokio] -+version = "1.48.0" -+features = [ -+ "macros", -+ "sync", -+] -+ - [target."cfg(unix)".dependencies.nix] - version = "0.30.1" - features = ["term"] diff --git a/registry/native/patches/crates/uu_chmod/0001-wasi-compat.patch b/registry/native/patches/crates/uu_chmod/0001-wasi-compat.patch index c8297fe54..8e41964b6 100644 --- a/registry/native/patches/crates/uu_chmod/0001-wasi-compat.patch +++ b/registry/native/patches/crates/uu_chmod/0001-wasi-compat.patch @@ -1,6 +1,6 @@ --- a/src/chmod.rs +++ b/src/chmod.rs -@@ -8,16 +8,70 @@ +@@ -8,16 +8,87 @@ use clap::{Arg, ArgAction, Command}; use std::ffi::OsString; use std::fs; @@ -27,23 +27,42 @@ + mod host_fs { + #[link(wasm_import_module = "host_fs")] + unsafe extern "C" { -+ pub fn chmod(path_ptr: *const u8, path_len: u32, mode: u32) -> u32; ++ pub fn chmod(fd: u32, path_ptr: *const u8, path_len: u32, mode: u32) -> u32; ++ pub fn path_mode( ++ fd: u32, ++ path_ptr: *const u8, ++ path_len: u32, ++ follow_symlinks: u32, ++ ) -> u32; + } + } + -+ /// Extension trait to provide mode() on WASI Metadata. -+ pub trait MetadataMode { -+ fn mode(&self) -> u32; ++ fn fallback_mode(meta: &fs::Metadata) -> u32 { ++ let ft = meta.file_type(); ++ let base = if ft.is_dir() { 0o755 } else { 0o644 }; ++ if meta.permissions().readonly() { ++ base & !0o222 ++ } else { ++ base ++ } + } -+ impl MetadataMode for fs::Metadata { -+ fn mode(&self) -> u32 { -+ let ft = self.file_type(); -+ let base = if ft.is_dir() { 0o755 } else { 0o644 }; -+ if self.permissions().readonly() { -+ base & !0o222 -+ } else { -+ base -+ } ++ ++ pub fn mode_for_path(path: &Path, meta: &fs::Metadata, follow_symlinks: bool) -> u32 { ++ let Some(path_str) = path.to_str() else { ++ return fallback_mode(meta); ++ }; ++ let mode = unsafe { ++ host_fs::path_mode( ++ 3, ++ path_str.as_ptr(), ++ path_str.len() as u32, ++ if follow_symlinks { 1 } else { 0 }, ++ ) ++ }; ++ if mode == 0 { ++ fallback_mode(meta) ++ } else { ++ mode & 0o7777 + } + } + @@ -55,7 +74,7 @@ + .to_str() + .ok_or_else(|| Error::new(ErrorKind::InvalidInput, "path is not valid UTF-8"))? + .as_bytes(); -+ let status = unsafe { host_fs::chmod(bytes.as_ptr(), bytes.len() as u32, mode) }; ++ let status = unsafe { host_fs::chmod(3, bytes.as_ptr(), bytes.len() as u32, mode) }; + if status == 0 { + return Ok(()); + } @@ -65,13 +84,47 @@ + fs::set_permissions(path, perms) + } +} -+#[cfg(target_os = "wasi")] -+use wasi_compat::MetadataMode; + #[cfg(all(unix, not(target_os = "redox")))] use uucore::safe_traversal::{DirFd, SymlinkBehavior}; use uucore::{format_usage, show, show_error}; -@@ -683,7 +737,12 @@ +@@ -121,7 +192,16 @@ + let preserve_root = matches.get_flag(options::PRESERVE_ROOT); + let fmode = match matches.get_one::(options::REFERENCE) { + Some(fref) => match fs::metadata(fref) { +- Ok(meta) => Some(meta.mode() & 0o7777), ++ Ok(meta) => { ++ #[cfg(target_os = "wasi")] ++ { ++ Some(wasi_compat::mode_for_path(Path::new(fref), &meta, true)) ++ } ++ #[cfg(not(target_os = "wasi"))] ++ { ++ Some(meta.mode() & 0o7777) ++ } ++ } + Err(_) => { + return Err(ChmodError::CannotStat(fref.into()).into()); + } +@@ -623,7 +703,16 @@ + let metadata = get_metadata(file, dereference); + + let fperm = match metadata { +- Ok(meta) => meta.mode() & 0o7777, ++ Ok(meta) => { ++ #[cfg(target_os = "wasi")] ++ { ++ wasi_compat::mode_for_path(file, &meta, dereference) ++ } ++ #[cfg(not(target_os = "wasi"))] ++ { ++ meta.mode() & 0o7777 ++ } ++ } + Err(err) => { + // Handle dangling symlinks or other errors + return if file.is_symlink() && !dereference { +@@ -683,7 +772,12 @@ // Use the helper method for consistent reporting self.report_permission_change(file, fperm, mode); Ok(()) diff --git a/registry/native/patches/crates/uu_ls/0001-wasi-host-fs-mode-display.patch b/registry/native/patches/crates/uu_ls/0001-wasi-host-fs-mode-display.patch index 460cf4f41..ff68f349e 100644 --- a/registry/native/patches/crates/uu_ls/0001-wasi-host-fs-mode-display.patch +++ b/registry/native/patches/crates/uu_ls/0001-wasi-host-fs-mode-display.patch @@ -9,7 +9,7 @@ fsext::{MetadataTimeField, metadata_get_time}, line_ending::LineEnding, os_str_as_bytes_lossy, -@@ -77,6 +77,38 @@ +@@ -77,6 +77,60 @@ translate, version_cmp::version_cmp, }; @@ -18,37 +18,59 @@ + +#[cfg(target_os = "wasi")] +mod wasi_host_fs { ++ use std::env; ++ + use super::{Metadata, Path}; + + mod host_fs { + #[link(wasm_import_module = "host_fs")] + unsafe extern "C" { -+ pub fn path_mode(path_ptr: *const u8, path_len: u32, follow_symlinks: u32) -> u32; ++ pub fn path_mode( ++ fd: u32, ++ path_ptr: *const u8, ++ path_len: u32, ++ follow_symlinks: u32, ++ ) -> u32; + } + } + -+ pub fn mode_for_path(path: &Path, metadata: &Metadata, follow_symlinks: bool) -> u32 { ++ fn fallback_mode(metadata: &Metadata) -> u32 { ++ if metadata.is_dir() { 0o040755 } else { 0o100644 } ++ } ++ ++ fn raw_mode_for_path(path: &Path, follow_symlinks: bool) -> u32 { + let Some(path_str) = path.to_str() else { -+ return if metadata.is_dir() { 0o040755 } else { 0o100644 }; ++ return 0; + }; -+ let mode = unsafe { ++ unsafe { + host_fs::path_mode( ++ 3, + path_str.as_ptr(), + path_str.len() as u32, + if follow_symlinks { 1 } else { 0 }, + ) -+ }; -+ if mode == 0 { -+ if metadata.is_dir() { 0o040755 } else { 0o100644 } -+ } else { ++ } ++ } ++ ++ pub fn mode_for_path(path: &Path, metadata: &Metadata, follow_symlinks: bool) -> u32 { ++ let mode = raw_mode_for_path(path, follow_symlinks); ++ if mode != 0 { + mode ++ } else if path.is_relative() { ++ env::current_dir() ++ .ok() ++ .map(|cwd| raw_mode_for_path(&cwd.join(path), follow_symlinks)) ++ .filter(|mode| *mode != 0) ++ .unwrap_or_else(|| fallback_mode(metadata)) ++ } else { ++ fallback_mode(metadata) + } + } +} mod dired; use dired::{DiredOutput, is_dired_arg_present}; -@@ -2982,7 +3014,15 @@ +@@ -2982,7 +3036,15 @@ let is_acl_set = false; #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] let is_acl_set = has_acl(item.path()); diff --git a/registry/native/patches/crates/uu_stat/0001-wasi-metadata-compat.patch b/registry/native/patches/crates/uu_stat/0001-wasi-metadata-compat.patch index 4aa333abb..fc4a10e43 100644 --- a/registry/native/patches/crates/uu_stat/0001-wasi-metadata-compat.patch +++ b/registry/native/patches/crates/uu_stat/0001-wasi-metadata-compat.patch @@ -25,7 +25,7 @@ use uucore::{entries, format_usage, show_error, show_warning}; use clap::{Arg, ArgAction, ArgMatches, Command}; -@@ -23,10 +29,79 @@ +@@ -23,10 +29,85 @@ use std::ffi::{OsStr, OsString}; use std::fs::{FileType, Metadata}; use std::io::Write; @@ -79,7 +79,12 @@ + mod host_fs { + #[link(wasm_import_module = "host_fs")] + unsafe extern "C" { -+ pub fn path_mode(path_ptr: *const u8, path_len: u32, follow_symlinks: u32) -> u32; ++ pub fn path_mode( ++ fd: u32, ++ path_ptr: *const u8, ++ path_len: u32, ++ follow_symlinks: u32, ++ ) -> u32; + } + } + @@ -89,6 +94,7 @@ + }; + let mode = unsafe { + host_fs::path_mode( ++ 3, + path_str.as_ptr(), + path_str.len() as u32, + if follow_symlinks { 1 } else { 0 }, @@ -105,15 +111,16 @@ use thiserror::Error; use uucore::time::{FormatSystemTimeFallback, format_system_time, system_time_to_sec}; -@@ -1048,11 +1123,16 @@ - precision, - format, +@@ -1032,6 +1113,7 @@ + display_name: &str, + file: &OsString, + file_type: FileType, ++ effective_mode: u32, + from_user: bool, + #[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))] + follow_symbolic_links: bool, +@@ -1050,9 +1132,9 @@ } => { -+ #[cfg(target_os = "wasi")] -+ let effective_mode = -+ wasi_host_fs::mode_for_path(Path::new(file), meta, self.follow); -+ #[cfg(not(target_os = "wasi"))] -+ let effective_mode = meta.mode(); let output = match format { // access rights in octal - 'a' => OutputType::UnsignedOct(0o7777 & meta.mode()), @@ -124,7 +131,7 @@ // number of blocks allocated (see %B) 'b' => OutputType::Unsigned(meta.blocks()), -@@ -1096,9 +1176,9 @@ +@@ -1096,9 +1178,9 @@ // device number in hex 'D' => OutputType::UnsignedHex(meta.dev()), // raw mode in hex @@ -136,3 +143,23 @@ // group ID of owner 'g' => OutputType::Unsigned(meta.gid() as u64), // group name of owner +@@ -1234,6 +1316,11 @@ + match result { + Ok(meta) => { + let file_type = meta.file_type(); ++ #[cfg(target_os = "wasi")] ++ let effective_mode = ++ wasi_host_fs::mode_for_path(Path::new(&file), &meta, follow_symbolic_links); ++ #[cfg(not(target_os = "wasi"))] ++ let effective_mode = meta.mode(); + let tokens = if self.from_user + || !(file_type.is_char_device() || file_type.is_block_device()) + { +@@ -1249,6 +1336,7 @@ + &display_name, + &file, + file_type, ++ effective_mode, + self.from_user, + follow_symbolic_links, + ) { 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) { diff --git a/registry/native/scripts/patch-vendor.sh b/registry/native/scripts/patch-vendor.sh index 1c0f3c819..7a9b4f2aa 100755 --- a/registry/native/scripts/patch-vendor.sh +++ b/registry/native/scripts/patch-vendor.sh @@ -113,14 +113,15 @@ for CRATE_DIR in $CRATE_DIRS; do patch -p1 -d "$VENDOR_CRATE" < "$PATCH" > /dev/null 2>&1 echo "applied" elif patch --dry-run -R -p1 -d "$VENDOR_CRATE" < "$PATCH" > /dev/null 2>&1; then - patch -R -p1 -d "$VENDOR_CRATE" < "$PATCH" > /dev/null 2>&1 - patch -p1 -d "$VENDOR_CRATE" < "$PATCH" > /dev/null 2>&1 - echo "reapplied" + echo "already applied" else # Mixed state (e.g. an interrupted earlier run left some # hunks applied): apply the remaining hunks, tolerating # already-applied ones; fail only on genuine rejects. - OUT=$(patch -p1 -N -r /dev/null -d "$VENDOR_CRATE" < "$PATCH" 2>&1); RC=$? + set +e + OUT=$(patch -p1 -N -r /dev/null -d "$VENDOR_CRATE" < "$PATCH" 2>&1) + RC=$? + set -e if [ $RC -le 1 ] && ! echo "$OUT" | grep -q "FAILED"; then echo "converged (mixed state)" else diff --git a/registry/native/stubs/uucore/build.rs b/registry/native/stubs/uucore/build.rs index f4b36c0ac..2af31f6f2 100644 --- a/registry/native/stubs/uucore/build.rs +++ b/registry/native/stubs/uucore/build.rs @@ -234,9 +234,18 @@ fn embed_static_utility_locales( )?; let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap_or_default(); - let Some(registry_dir) = Path::new(&manifest_dir).parent() else { + let Some(native_dir) = Path::new(&manifest_dir) + .parent() + .and_then(Path::parent) + else { return Ok(()); // nothing to scan }; + let vendor_dir = native_dir.join("vendor"); + let registry_dir = if vendor_dir.exists() { + vendor_dir.as_path() + } else { + native_dir + }; // First, try to embed uucore locales - critical for common translations like "Usage:" embed_component_locales(embedded_file, locales_to_embed, "uucore", |locale| { @@ -252,16 +261,15 @@ fn embed_static_utility_locales( for entry in entries { let file_name = entry.file_name(); if let Some(dir_name) = file_name.to_str() { - // Match uu_- - if let Some((util_part, _)) = dir_name.split_once('-') { - if let Some(util_name) = util_part.strip_prefix("uu_") { - embed_component_locales( - embedded_file, - locales_to_embed, - util_name, - |locale| entry.path().join(format!("locales/{locale}.ftl")), - )?; - } + // Match cargo-vendor's uu_ directories and registry uu_- names. + let util_part = dir_name.split_once('-').map_or(dir_name, |(name, _)| name); + if let Some(util_name) = util_part.strip_prefix("uu_") { + embed_component_locales( + embedded_file, + locales_to_embed, + util_name, + |locale| entry.path().join(format!("locales/{locale}.ftl")), + )?; } } } diff --git a/registry/native/stubs/uucore/src/lib/mods/locale.rs b/registry/native/stubs/uucore/src/lib/mods/locale.rs index 7dfcdb25f..e50789f47 100644 --- a/registry/native/stubs/uucore/src/lib/mods/locale.rs +++ b/registry/native/stubs/uucore/src/lib/mods/locale.rs @@ -288,12 +288,38 @@ fn create_english_bundle_from_embedded( } fn get_message_internal(id: &str, args: Option) -> String { + ensure_localization_from_argv(); LOCALIZER.with(|lock| { lock.get() .map_or_else(|| id.to_string(), |loc| loc.format(id, args.as_ref())) // Return the key ID if localizer not initialized }) } +fn ensure_localization_from_argv() { + if LOCALIZER.with(|lock| lock.get().is_some()) { + return; + } + + thread_local! { + static LOCALIZATION_AUTO_INIT_ATTEMPTED: Cell = const { Cell::new(false) }; + } + if LOCALIZATION_AUTO_INIT_ATTEMPTED.with(|flag| flag.replace(true)) { + return; + } + + let Some(program) = std::env::args_os().next() else { + return; + }; + let Some(util_name) = Path::new(&program) + .file_name() + .and_then(|name| name.to_str()) + .map(crate::get_canonical_util_name) + else { + return; + }; + let _ = setup_localization(util_name); +} + /// Retrieves a localized message by its identifier. /// /// Looks up a message with the given ID in the current locale bundle and returns