diff --git a/crates/test-programs/src/bin/p1_stat_extreme_host_mtime.rs b/crates/test-programs/src/bin/p1_stat_extreme_host_mtime.rs new file mode 100644 index 000000000000..733353f7b649 --- /dev/null +++ b/crates/test-programs/src/bin/p1_stat_extreme_host_mtime.rs @@ -0,0 +1,49 @@ +#![expect(unsafe_op_in_unsafe_fn, reason = "old code, not worth updating yet")] + +use std::{env, process}; +use test_programs::preview1::open_scratch_directory; + +const FILENAME: &str = "extreme.dat"; +const EXPECTED: &[u8] = b"hello"; + +unsafe fn test_stat_extreme_host_mtime(dir_fd: wasip1::Fd) { + let st = wasip1::path_filestat_get(dir_fd, 0, FILENAME).expect("path_filestat_get"); + assert_eq!(st.size, EXPECTED.len() as u64, "size"); + let _ = st.mtim; + let _ = st.atim; + + let fd = + wasip1::path_open(dir_fd, 0, FILENAME, 0, wasip1::RIGHTS_FD_READ, 0, 0).expect("path_open"); + let mut buf = [0u8; 16]; + let nread = wasip1::fd_read( + fd, + &[wasip1::Iovec { + buf: buf.as_mut_ptr(), + buf_len: buf.len(), + }], + ) + .expect("fd_read"); + assert_eq!(&buf[..nread], EXPECTED, "contents"); + wasip1::fd_close(fd).expect("fd_close"); +} + +fn main() { + let mut args = env::args(); + let prog = args.next().unwrap(); + let arg = if let Some(arg) = args.next() { + arg + } else { + eprintln!("usage: {prog} "); + process::exit(1); + }; + + let dir_fd = match open_scratch_directory(&arg) { + Ok(dir_fd) => dir_fd, + Err(err) => { + eprintln!("{err}"); + process::exit(1); + } + }; + + unsafe { test_stat_extreme_host_mtime(dir_fd) } +} diff --git a/crates/wasi/src/filesystem.rs b/crates/wasi/src/filesystem.rs index c6e89fef7895..74874ea549ee 100644 --- a/crates/wasi/src/filesystem.rs +++ b/crates/wasi/src/filesystem.rs @@ -310,18 +310,22 @@ impl DescriptorStat { /// Creates a `DescriptorStat` from a `Metadata` plus the hard link /// count. fn new(meta: &Metadata, link_count: u64) -> Self { - fn datetime_from(t: std::time::SystemTime) -> Datetime { - // FIXME make this infallible or handle errors properly - Datetime::try_from(t).unwrap() - } - Self { type_: meta.file_type().into(), link_count, size: meta.len(), - data_access_timestamp: meta.accessed().map(|t| datetime_from(t.into_std())).ok(), - data_modification_timestamp: meta.modified().map(|t| datetime_from(t.into_std())).ok(), - status_change_timestamp: meta.created().map(|t| datetime_from(t.into_std())).ok(), + data_access_timestamp: meta + .accessed() + .ok() + .and_then(|t| Datetime::try_from(t.into_std()).ok()), + data_modification_timestamp: meta + .modified() + .ok() + .and_then(|t| Datetime::try_from(t.into_std()).ok()), + status_change_timestamp: meta + .created() + .ok() + .and_then(|t| Datetime::try_from(t.into_std()).ok()), } } } diff --git a/crates/wasi/src/p2/host/filesystem.rs b/crates/wasi/src/p2/host/filesystem.rs index a81b6622e545..3a668bb33b7b 100644 --- a/crates/wasi/src/p2/host/filesystem.rs +++ b/crates/wasi/src/p2/host/filesystem.rs @@ -590,15 +590,17 @@ impl TryFrom for types::DescriptorStat { status_change_timestamp, }: crate::filesystem::DescriptorStat, ) -> Result { + // Internal timestamps use i64 seconds; wasi:clocks/wall-clock uses u64 + // (non-negative). Times outside that range become missing rather than + // failing the whole stat (e.g. host-clamped far-past mtimes on macOS). Ok(Self { type_: type_.into(), link_count, size, - data_access_timestamp: data_access_timestamp.map(|t| t.try_into()).transpose()?, + data_access_timestamp: data_access_timestamp.and_then(|t| t.try_into().ok()), data_modification_timestamp: data_modification_timestamp - .map(|t| t.try_into()) - .transpose()?, - status_change_timestamp: status_change_timestamp.map(|t| t.try_into()).transpose()?, + .and_then(|t| t.try_into().ok()), + status_change_timestamp: status_change_timestamp.and_then(|t| t.try_into().ok()), }) } } diff --git a/crates/wasi/tests/all/p1.rs b/crates/wasi/tests/all/p1.rs index cd7077afe8bf..cd1f6d4e335f 100644 --- a/crates/wasi/tests/all/p1.rs +++ b/crates/wasi/tests/all/p1.rs @@ -7,6 +7,14 @@ use wasmtime_wasi::p1::{WasiP1Ctx, add_to_linker_async}; use wasmtime_wasi::{WasiCtxBuilder, WasiView}; async fn run(path: &str, with_builder: impl FnOnce(&mut WasiCtxBuilder)) -> Result<()> { + run_with_workspace_setup(path, |_| Ok(()), with_builder).await +} + +async fn run_with_workspace_setup( + path: &str, + setup: impl FnOnce(&Path) -> Result<()>, + with_builder: impl FnOnce(&mut WasiCtxBuilder), +) -> Result<()> { let path = Path::new(path); let name = path.file_stem().unwrap().to_str().unwrap(); let engine = test_programs_artifacts::engine(|_config| {}); @@ -14,7 +22,7 @@ async fn run(path: &str, with_builder: impl FnOnce(&mut WasiCtxBuilder)) -> Resu add_to_linker_async(&mut linker, |t| &mut t.wasi)?; let module = Module::from_file(&engine, path)?; - let (mut store, _td) = Ctx::new(&engine, name, |builder| { + let (mut store, _td) = Ctx::new_with_workspace_setup(&engine, name, setup, |builder| { with_builder(builder); builder.build_p1() })?; @@ -69,6 +77,17 @@ async fn p1_fd_filestat_get() { async fn p1_fd_filestat_set() { run(P1_FD_FILESTAT_SET, |_| {}).await.unwrap() } + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn p1_stat_extreme_host_mtime() { + run_with_workspace_setup( + P1_STAT_EXTREME_HOST_MTIME, + crate::store::prepare_extreme_mtime_fixture, + |_| {}, + ) + .await + .unwrap() +} #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn p1_fd_flags_set() { run(P1_FD_FLAGS_SET, |_| {}).await.unwrap() diff --git a/crates/wasi/tests/all/p2/async_.rs b/crates/wasi/tests/all/p2/async_.rs index fe43baa0ccd6..5aaaf3679599 100644 --- a/crates/wasi/tests/all/p2/async_.rs +++ b/crates/wasi/tests/all/p2/async_.rs @@ -8,13 +8,21 @@ use wasmtime_wasi::p2::add_to_linker_async; use wasmtime_wasi::p2::bindings::Command; async fn run(path: &str, with_builder: impl FnOnce(&mut WasiCtxBuilder)) -> Result<()> { + run_with_workspace_setup(path, |_| Ok(()), with_builder).await +} + +async fn run_with_workspace_setup( + path: &str, + setup: impl FnOnce(&Path) -> Result<()>, + with_builder: impl FnOnce(&mut WasiCtxBuilder), +) -> Result<()> { let path = Path::new(path); let name = path.file_stem().unwrap().to_str().unwrap(); let engine = test_programs_artifacts::engine(|_config| {}); let mut linker = Linker::new(&engine); add_to_linker_async(&mut linker)?; - let (mut store, _td) = Ctx::new(&engine, name, |builder| { + let (mut store, _td) = Ctx::new_with_workspace_setup(&engine, name, setup, |builder| { with_builder(builder); MyWasiCtx::new(builder.build()) })?; @@ -73,6 +81,16 @@ async fn p1_fd_filestat_set() { run(P1_FD_FILESTAT_SET_COMPONENT, |_| {}).await.unwrap() } #[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn p1_stat_extreme_host_mtime() { + run_with_workspace_setup( + P1_STAT_EXTREME_HOST_MTIME_COMPONENT, + crate::store::prepare_extreme_mtime_fixture, + |_| {}, + ) + .await + .unwrap() +} +#[test_log::test(tokio::test(flavor = "multi_thread"))] async fn p1_fd_flags_set() { run(P1_FD_FLAGS_SET_COMPONENT, |_| {}).await.unwrap() } diff --git a/crates/wasi/tests/all/p2/sync.rs b/crates/wasi/tests/all/p2/sync.rs index 0c1aaf6fcd44..b95cf6dd3ffe 100644 --- a/crates/wasi/tests/all/p2/sync.rs +++ b/crates/wasi/tests/all/p2/sync.rs @@ -8,6 +8,14 @@ use wasmtime_wasi::p2::add_to_linker_sync; use wasmtime_wasi::p2::bindings::sync::Command; fn run(path: &str, with_builder: impl Fn(&mut WasiCtxBuilder)) -> Result<()> { + run_with_workspace_setup(path, |_| Ok(()), with_builder) +} + +fn run_with_workspace_setup( + path: &str, + setup: impl Fn(&Path) -> Result<()>, + with_builder: impl Fn(&mut WasiCtxBuilder), +) -> Result<()> { let path = Path::new(path); let name = path.file_stem().unwrap().to_str().unwrap(); let engine = test_programs_artifacts::engine(|_| {}); @@ -17,7 +25,7 @@ fn run(path: &str, with_builder: impl Fn(&mut WasiCtxBuilder)) -> Result<()> { let component = Component::from_file(&engine, path)?; for blocking in [false, true] { - let (mut store, _td) = Ctx::new(&engine, name, |builder| { + let (mut store, _td) = Ctx::new_with_workspace_setup(&engine, name, &setup, |builder| { with_builder(builder); builder.allow_blocking_current_thread(blocking); MyWasiCtx::new(builder.build()) @@ -77,6 +85,15 @@ fn p1_fd_filestat_set() { run(P1_FD_FILESTAT_SET_COMPONENT, |_| {}).unwrap() } #[test_log::test] +fn p1_stat_extreme_host_mtime() { + run_with_workspace_setup( + P1_STAT_EXTREME_HOST_MTIME_COMPONENT, + crate::store::prepare_extreme_mtime_fixture, + |_| {}, + ) + .unwrap() +} +#[test_log::test] fn p1_fd_flags_set() { run(P1_FD_FLAGS_SET_COMPONENT, |_| {}).unwrap() } diff --git a/crates/wasi/tests/all/store.rs b/crates/wasi/tests/all/store.rs index 50f9fbf9481c..1114633417cf 100644 --- a/crates/wasi/tests/all/store.rs +++ b/crates/wasi/tests/all/store.rs @@ -23,11 +23,23 @@ impl Ctx { engine: &Engine, name: &str, configure: impl FnOnce(&mut WasiCtxBuilder) -> T, + ) -> Result<(Store>, TempDir)> { + Self::new_with_workspace_setup(engine, name, |_| Ok(()), configure) + } + + /// Like [`Self::new`], but allows seeding the preopened scratch directory + /// before the guest runs (for host-prepared filesystem fixtures). + pub fn new_with_workspace_setup( + engine: &Engine, + name: &str, + setup: impl FnOnce(&std::path::Path) -> Result<()>, + configure: impl FnOnce(&mut WasiCtxBuilder) -> T, ) -> Result<(Store>, TempDir)> { const MAX_OUTPUT_SIZE: usize = 10 << 20; let stdout = MemoryOutputPipe::new(MAX_OUTPUT_SIZE); let stderr = MemoryOutputPipe::new(MAX_OUTPUT_SIZE); let workspace = prepare_workspace(name)?; + setup(workspace.path())?; // Create our wasi context. let mut builder = WasiCtxBuilder::new(); @@ -54,7 +66,7 @@ impl Ctx { stdout, }; - Ok((Store::new(&engine, ctx), workspace)) + Ok((Store::new(engine, ctx), workspace)) } } @@ -92,3 +104,45 @@ impl WasiView for Ctx { } } } + +/// Best-effort far-past host `SystemTime` for stress-testing filestat conversion. +/// +/// Prefer a value outside `Datetime`'s `i64` second range when the platform can +/// represent it (typical on Unix). Windows `SystemTime` is FILETIME-based and +/// cannot represent that far past, so fall back to the earliest practical +/// constructible time (around the Windows epoch, ~1601-01-01). The guest test +/// only requires that `path_filestat_get` does not panic the host. +fn extreme_host_mtime() -> std::time::SystemTime { + use std::time::{Duration, SystemTime}; + + // Outside i64 second range when representable (Unix). + if let Some(t) = + SystemTime::UNIX_EPOCH.checked_sub(Duration::from_secs((i64::MAX as u64).saturating_add(1))) + { + return t; + } + // Windows FILETIME lower bound ≈ 1601-01-01 UTC. + if let Some(t) = SystemTime::UNIX_EPOCH.checked_sub(Duration::from_secs(11_644_473_600)) { + return t; + } + SystemTime::UNIX_EPOCH + .checked_sub(Duration::from_secs(1)) + .unwrap_or(SystemTime::UNIX_EPOCH) +} + +/// Seed a preopened workspace with `extreme.dat` using a host mtime that may +/// fall outside WASI datetime ranges (or be clamped by the OS). +pub fn prepare_extreme_mtime_fixture(dir: &std::path::Path) -> Result<()> { + use std::fs::{File, FileTimes}; + use std::io::Write; + + let path = dir.join("extreme.dat"); + File::create(&path)?.write_all(b"hello")?; + let extreme = extreme_host_mtime(); + let f = File::options().write(true).open(&path)?; + let times = FileTimes::new().set_modified(extreme).set_accessed(extreme); + // Platforms may reject or clamp extreme times; the file still exists so the + // guest can open and stat without panicking the host. + let _ = f.set_times(times); + Ok(()) +}