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..82fe383f468e 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()), } } } @@ -1167,3 +1171,54 @@ impl WasiFilesystemCtxView<'_> { Ok(results) } } + +#[cfg(test)] +mod test { + use super::*; + use crate::clocks::Datetime; + use std::io::{Read, Write}; + use std::time::{Duration, SystemTime}; + + #[test] + fn datetime_try_from_extreme_system_time() { + let extreme = SystemTime::UNIX_EPOCH + .checked_sub(Duration::from_secs((i64::MAX as u64) + 1)) + .unwrap(); + assert!(Datetime::try_from(extreme).ok().is_none()); + } + + #[test] + fn host_prepared_extreme_mtime_stats_and_reads() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("extreme.dat"); + std::fs::File::create(&path) + .unwrap() + .write_all(b"hello") + .unwrap(); + + let extreme = SystemTime::UNIX_EPOCH + .checked_sub(Duration::from_secs((i64::MAX as u64) + 1)) + .unwrap(); + let f = std::fs::OpenOptions::new().write(true).open(&path).unwrap(); + let times = std::fs::FileTimes::new() + .set_modified(extreme) + .set_accessed(extreme); + // Platforms may reject or clamp extreme times. + let _ = f.set_times(times); + drop(f); + + let f = std::fs::File::open(&path).unwrap(); + let stat = sys::stat(&f).expect("stat"); + let mut buf = Vec::new(); + std::fs::File::open(&path) + .unwrap() + .read_to_end(&mut buf) + .unwrap(); + assert_eq!(buf, b"hello"); + let _ = ( + stat.data_access_timestamp, + stat.data_modification_timestamp, + stat.status_change_timestamp, + ); + } +} 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..182d7a8b1d64 100644 --- a/crates/wasi/tests/all/p1.rs +++ b/crates/wasi/tests/all/p1.rs @@ -25,6 +25,29 @@ async fn run(path: &str, with_builder: impl FnOnce(&mut WasiCtxBuilder)) -> Resu Ok(()) } +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, |t| &mut t.wasi)?; + + let module = Module::from_file(&engine, path)?; + let (mut store, _td) = Ctx::new_with_workspace_setup(&engine, name, setup, |builder| { + with_builder(builder); + builder.build_p1() + })?; + store.data_mut().wasi.ctx().table.set_max_capacity(1000); + let instance = linker.instantiate_async(&mut store, &module).await?; + let start = instance.get_typed_func::<(), ()>(&mut store, "_start")?; + start.call_async(&mut store, ()).await?; + Ok(()) +} + foreach_p1!(assert_test_exists); // Below here is mechanical: there should be one test for every binary in @@ -69,6 +92,32 @@ 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() { + use std::fs::{File, FileTimes}; + use std::io::Write; + use std::time::{Duration, SystemTime}; + + run_with_workspace_setup( + P1_STAT_EXTREME_HOST_MTIME, + |dir| { + let path = dir.join("extreme.dat"); + File::create(&path)?.write_all(b"hello")?; + let extreme = SystemTime::UNIX_EPOCH + .checked_sub(Duration::from_secs((i64::MAX as u64) + 1)) + .expect("construct extreme SystemTime"); + 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. + let _ = f.set_times(times); + Ok(()) + }, + |_| {}, + ) + .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..58e91cfd1e24 100644 --- a/crates/wasi/tests/all/p2/async_.rs +++ b/crates/wasi/tests/all/p2/async_.rs @@ -27,6 +27,30 @@ async fn run(path: &str, with_builder: impl FnOnce(&mut WasiCtxBuilder)) -> Resu .map_err(|()| wasmtime::format_err!("run returned a failure")) } +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_with_workspace_setup(&engine, name, setup, |builder| { + with_builder(builder); + MyWasiCtx::new(builder.build()) + })?; + let component = Component::from_file(&engine, path)?; + let command = Command::instantiate_async(&mut store, &component, &linker).await?; + command + .wasi_cli_run() + .call_run(&mut store) + .await? + .map_err(|()| wasmtime::format_err!("run returned a failure")) +} + foreach_p1!(assert_test_exists); foreach_p2!(assert_test_exists); @@ -73,6 +97,30 @@ 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() { + use std::fs::{File, FileTimes}; + use std::io::Write; + use std::time::{Duration, SystemTime}; + + run_with_workspace_setup( + P1_STAT_EXTREME_HOST_MTIME_COMPONENT, + |dir| { + let path = dir.join("extreme.dat"); + File::create(&path)?.write_all(b"hello")?; + let extreme = SystemTime::UNIX_EPOCH + .checked_sub(Duration::from_secs((i64::MAX as u64) + 1)) + .expect("construct extreme SystemTime"); + let f = File::options().write(true).open(&path)?; + let times = FileTimes::new().set_modified(extreme).set_accessed(extreme); + let _ = f.set_times(times); + Ok(()) + }, + |_| {}, + ) + .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..89871a98b0bf 100644 --- a/crates/wasi/tests/all/p2/sync.rs +++ b/crates/wasi/tests/all/p2/sync.rs @@ -31,6 +31,34 @@ fn run(path: &str, with_builder: impl Fn(&mut WasiCtxBuilder)) -> Result<()> { Ok(()) } +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(|_| {}); + let mut linker = Linker::new(&engine); + add_to_linker_sync(&mut linker)?; + + let component = Component::from_file(&engine, path)?; + + for blocking in [false, true] { + 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()) + })?; + let command = Command::instantiate(&mut store, &component, &linker)?; + command + .wasi_cli_run() + .call_run(&mut store)? + .map_err(|()| wasmtime::format_err!("run returned a failure"))?; + } + Ok(()) +} + foreach_p1!(assert_test_exists); foreach_p2!(assert_test_exists); @@ -77,6 +105,29 @@ fn p1_fd_filestat_set() { run(P1_FD_FILESTAT_SET_COMPONENT, |_| {}).unwrap() } #[test_log::test] +fn p1_stat_extreme_host_mtime() { + use std::fs::{File, FileTimes}; + use std::io::Write; + use std::time::{Duration, SystemTime}; + + run_with_workspace_setup( + P1_STAT_EXTREME_HOST_MTIME_COMPONENT, + |dir| { + let path = dir.join("extreme.dat"); + File::create(&path)?.write_all(b"hello")?; + let extreme = SystemTime::UNIX_EPOCH + .checked_sub(Duration::from_secs((i64::MAX as u64) + 1)) + .expect("construct extreme SystemTime"); + let f = File::options().write(true).open(&path)?; + let times = FileTimes::new().set_modified(extreme).set_accessed(extreme); + let _ = f.set_times(times); + Ok(()) + }, + |_| {}, + ) + .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..12c5f44de319 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)) } }