about summary refs log tree commit diff
diff options
context:
space:
mode:
authorbors <bors@rust-lang.org>2022-09-13 21:20:13 +0000
committerbors <bors@rust-lang.org>2022-09-13 21:20:13 +0000
commit2f1fa12659ac6b852a9dec46b79a0b02736f4e44 (patch)
treed4aff5aa053c6916728623863cd834b54d329d58
parent7e66a9ff1690a7b4e0b46110350814ff8de251c9 (diff)
parentc8346376264ea99cafd6550b5b5f460cf82b326f (diff)
downloadrust-2f1fa12659ac6b852a9dec46b79a0b02736f4e44.tar.gz
rust-2f1fa12659ac6b852a9dec46b79a0b02736f4e44.zip
Auto merge of #2506 - pvdrz:a-really-bad-clock, r=saethlin
Make `sleep` work with isolation enabled

Implement a virtual monotone clock that can be used to track time while isolation is enabled. This virtual clock keeps an internal nanoseconds counter that will be increased by a fixed amount at the end of every basic block.

When a process sleeps, this clock will return immediately and increase the counter by the interval the process was supposed to sleep. Making miri execution faster than native code :trollface:.

cc `@RalfJung` `@saethlin` `@JakobDegen`
-rw-r--r--src/clock.rs115
-rw-r--r--src/concurrency/thread.rs50
-rw-r--r--src/eval.rs5
-rw-r--r--src/lib.rs4
-rw-r--r--src/machine.rs11
-rw-r--r--src/shims/time.rs29
-rw-r--r--src/shims/unix/linux/sync.rs6
-rw-r--r--src/shims/unix/sync.rs2
-rw-r--r--tests/pass/shims/time-with-isolation.rs35
9 files changed, 204 insertions, 53 deletions
diff --git a/src/clock.rs b/src/clock.rs
new file mode 100644
index 00000000000..3f33273e1e5
--- /dev/null
+++ b/src/clock.rs
@@ -0,0 +1,115 @@
+use std::sync::atomic::{AtomicU64, Ordering};
+use std::time::{Duration, Instant as StdInstant};
+
+/// When using a virtual clock, this defines how many nanoseconds we pretend are passing for each
+/// basic block.
+const NANOSECONDS_PER_BASIC_BLOCK: u64 = 10;
+
+#[derive(Debug)]
+pub struct Instant {
+    kind: InstantKind,
+}
+
+#[derive(Debug)]
+enum InstantKind {
+    Host(StdInstant),
+    Virtual { nanoseconds: u64 },
+}
+
+impl Instant {
+    pub fn checked_add(&self, duration: Duration) -> Option<Instant> {
+        match self.kind {
+            InstantKind::Host(instant) =>
+                instant.checked_add(duration).map(|i| Instant { kind: InstantKind::Host(i) }),
+            InstantKind::Virtual { nanoseconds } =>
+                u128::from(nanoseconds)
+                    .checked_add(duration.as_nanos())
+                    .and_then(|n| u64::try_from(n).ok())
+                    .map(|nanoseconds| Instant { kind: InstantKind::Virtual { nanoseconds } }),
+        }
+    }
+
+    pub fn duration_since(&self, earlier: Instant) -> Duration {
+        match (&self.kind, earlier.kind) {
+            (InstantKind::Host(instant), InstantKind::Host(earlier)) =>
+                instant.duration_since(earlier),
+            (
+                InstantKind::Virtual { nanoseconds },
+                InstantKind::Virtual { nanoseconds: earlier },
+            ) => Duration::from_nanos(nanoseconds.saturating_sub(earlier)),
+            _ => panic!("all `Instant` must be of the same kind"),
+        }
+    }
+}
+
+/// A monotone clock used for `Instant` simulation.
+#[derive(Debug)]
+pub struct Clock {
+    kind: ClockKind,
+}
+
+#[derive(Debug)]
+enum ClockKind {
+    Host {
+        /// The "time anchor" for this machine's monotone clock.
+        time_anchor: StdInstant,
+    },
+    Virtual {
+        /// The "current virtual time".
+        nanoseconds: AtomicU64,
+    },
+}
+
+impl Clock {
+    /// Create a new clock based on the availability of communication with the host.
+    pub fn new(communicate: bool) -> Self {
+        let kind = if communicate {
+            ClockKind::Host { time_anchor: StdInstant::now() }
+        } else {
+            ClockKind::Virtual { nanoseconds: 0.into() }
+        };
+
+        Self { kind }
+    }
+
+    /// Let the time pass for a small interval.
+    pub fn tick(&self) {
+        match &self.kind {
+            ClockKind::Host { .. } => {
+                // Time will pass without us doing anything.
+            }
+            ClockKind::Virtual { nanoseconds } => {
+                nanoseconds.fetch_add(NANOSECONDS_PER_BASIC_BLOCK, Ordering::SeqCst);
+            }
+        }
+    }
+
+    /// Sleep for the desired duration.
+    pub fn sleep(&self, duration: Duration) {
+        match &self.kind {
+            ClockKind::Host { .. } => std::thread::sleep(duration),
+            ClockKind::Virtual { nanoseconds } => {
+                // Just pretend that we have slept for some time.
+                nanoseconds.fetch_add(duration.as_nanos().try_into().unwrap(), Ordering::SeqCst);
+            }
+        }
+    }
+
+    /// Return the `anchor` instant, to convert between monotone instants and durations relative to the anchor.
+    pub fn anchor(&self) -> Instant {
+        match &self.kind {
+            ClockKind::Host { time_anchor } => Instant { kind: InstantKind::Host(*time_anchor) },
+            ClockKind::Virtual { .. } => Instant { kind: InstantKind::Virtual { nanoseconds: 0 } },
+        }
+    }
+
+    pub fn now(&self) -> Instant {
+        match &self.kind {
+            ClockKind::Host { .. } => Instant { kind: InstantKind::Host(StdInstant::now()) },
+            ClockKind::Virtual { nanoseconds } =>
+                Instant {
+                    kind: InstantKind::Virtual { nanoseconds: nanoseconds.load(Ordering::SeqCst) },
+                },
+        }
+    }
+}
diff --git a/src/concurrency/thread.rs b/src/concurrency/thread.rs
index 19da0fc678a..78a357dd6af 100644
--- a/src/concurrency/thread.rs
+++ b/src/concurrency/thread.rs
@@ -3,7 +3,7 @@
 use std::cell::RefCell;
 use std::collections::hash_map::Entry;
 use std::num::TryFromIntError;
-use std::time::{Duration, Instant, SystemTime};
+use std::time::{Duration, SystemTime};
 
 use log::trace;
 
@@ -189,9 +189,9 @@ pub enum Time {
 
 impl Time {
     /// How long do we have to wait from now until the specified time?
-    fn get_wait_time(&self) -> Duration {
+    fn get_wait_time(&self, clock: &Clock) -> Duration {
         match self {
-            Time::Monotonic(instant) => instant.saturating_duration_since(Instant::now()),
+            Time::Monotonic(instant) => instant.duration_since(clock.now()),
             Time::RealTime(time) =>
                 time.duration_since(SystemTime::now()).unwrap_or(Duration::new(0, 0)),
         }
@@ -490,13 +490,16 @@ impl<'mir, 'tcx: 'mir> ThreadManager<'mir, 'tcx> {
     }
 
     /// Get a callback that is ready to be called.
-    fn get_ready_callback(&mut self) -> Option<(ThreadId, TimeoutCallback<'mir, 'tcx>)> {
+    fn get_ready_callback(
+        &mut self,
+        clock: &Clock,
+    ) -> Option<(ThreadId, TimeoutCallback<'mir, 'tcx>)> {
         // We iterate over all threads in the order of their indices because
         // this allows us to have a deterministic scheduler.
         for thread in self.threads.indices() {
             match self.timeout_callbacks.entry(thread) {
                 Entry::Occupied(entry) =>
-                    if entry.get().call_time.get_wait_time() == Duration::new(0, 0) {
+                    if entry.get().call_time.get_wait_time(clock) == Duration::new(0, 0) {
                         return Some((thread, entry.remove().callback));
                     },
                 Entry::Vacant(_) => {}
@@ -553,7 +556,7 @@ impl<'mir, 'tcx: 'mir> ThreadManager<'mir, 'tcx> {
     /// used in stateless model checkers such as Loom: run the active thread as
     /// long as we can and switch only when we have to (the active thread was
     /// blocked, terminated, or has explicitly asked to be preempted).
-    fn schedule(&mut self) -> InterpResult<'tcx, SchedulingAction> {
+    fn schedule(&mut self, clock: &Clock) -> InterpResult<'tcx, SchedulingAction> {
         // Check whether the thread has **just** terminated (`check_terminated`
         // checks whether the thread has popped all its stack and if yes, sets
         // the thread state to terminated).
@@ -580,7 +583,7 @@ impl<'mir, 'tcx: 'mir> ThreadManager<'mir, 'tcx> {
         // at the time of the call".
         // <https://pubs.opengroup.org/onlinepubs/9699919799/functions/pthread_cond_timedwait.html>
         let potential_sleep_time =
-            self.timeout_callbacks.values().map(|info| info.call_time.get_wait_time()).min();
+            self.timeout_callbacks.values().map(|info| info.call_time.get_wait_time(clock)).min();
         if potential_sleep_time == Some(Duration::new(0, 0)) {
             return Ok(SchedulingAction::ExecuteTimeoutCallback);
         }
@@ -615,7 +618,8 @@ impl<'mir, 'tcx: 'mir> ThreadManager<'mir, 'tcx> {
             // All threads are currently blocked, but we have unexecuted
             // timeout_callbacks, which may unblock some of the threads. Hence,
             // sleep until the first callback.
-            std::thread::sleep(sleep_time);
+
+            clock.sleep(sleep_time);
             Ok(SchedulingAction::ExecuteTimeoutCallback)
         } else {
             throw_machine_stop!(TerminationInfo::Deadlock);
@@ -865,6 +869,9 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
         callback: TimeoutCallback<'mir, 'tcx>,
     ) {
         let this = self.eval_context_mut();
+        if !this.machine.communicate() && matches!(call_time, Time::RealTime(..)) {
+            panic!("cannot have `RealTime` callback with isolation enabled!")
+        }
         this.machine.threads.register_timeout_callback(thread, call_time, callback);
     }
 
@@ -878,18 +885,19 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
     #[inline]
     fn run_timeout_callback(&mut self) -> InterpResult<'tcx> {
         let this = self.eval_context_mut();
-        let (thread, callback) =
-            if let Some((thread, callback)) = this.machine.threads.get_ready_callback() {
-                (thread, callback)
-            } else {
-                // get_ready_callback can return None if the computer's clock
-                // was shifted after calling the scheduler and before the call
-                // to get_ready_callback (see issue
-                // https://github.com/rust-lang/miri/issues/1763). In this case,
-                // just do nothing, which effectively just returns to the
-                // scheduler.
-                return Ok(());
-            };
+        let (thread, callback) = if let Some((thread, callback)) =
+            this.machine.threads.get_ready_callback(&this.machine.clock)
+        {
+            (thread, callback)
+        } else {
+            // get_ready_callback can return None if the computer's clock
+            // was shifted after calling the scheduler and before the call
+            // to get_ready_callback (see issue
+            // https://github.com/rust-lang/miri/issues/1763). In this case,
+            // just do nothing, which effectively just returns to the
+            // scheduler.
+            return Ok(());
+        };
         // This back-and-forth with `set_active_thread` is here because of two
         // design decisions:
         // 1. Make the caller and not the callback responsible for changing
@@ -906,7 +914,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
     #[inline]
     fn schedule(&mut self) -> InterpResult<'tcx, SchedulingAction> {
         let this = self.eval_context_mut();
-        this.machine.threads.schedule()
+        this.machine.threads.schedule(&this.machine.clock)
     }
 
     /// Handles thread termination of the active thread: wakes up threads joining on this one,
diff --git a/src/eval.rs b/src/eval.rs
index 8cdb2876f1a..e1ef7fa9817 100644
--- a/src/eval.rs
+++ b/src/eval.rs
@@ -359,11 +359,6 @@ pub fn eval_entry<'tcx>(
                     assert!(ecx.step()?, "a terminated thread was scheduled for execution");
                 }
                 SchedulingAction::ExecuteTimeoutCallback => {
-                    assert!(
-                        ecx.machine.communicate(),
-                        "scheduler callbacks require disabled isolation, but the code \
-                        that created the callback did not check it"
-                    );
                     ecx.run_timeout_callback()?;
                 }
                 SchedulingAction::ExecuteDtors => {
diff --git a/src/lib.rs b/src/lib.rs
index 4fb6704165b..016ed01f4da 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -51,6 +51,7 @@ extern crate rustc_session;
 extern crate rustc_span;
 extern crate rustc_target;
 
+mod clock;
 mod concurrency;
 mod diagnostics;
 mod eval;
@@ -81,6 +82,7 @@ pub use crate::shims::time::EvalContextExt as _;
 pub use crate::shims::tls::{EvalContextExt as _, TlsData};
 pub use crate::shims::EvalContextExt as _;
 
+pub use crate::clock::{Clock, Instant};
 pub use crate::concurrency::{
     data_race::{
         AtomicFenceOrd, AtomicReadOrd, AtomicRwOrd, AtomicWriteOrd,
@@ -89,7 +91,7 @@ pub use crate::concurrency::{
     sync::{CondvarId, EvalContextExt as SyncEvalContextExt, MutexId, RwLockId},
     thread::{
         EvalContextExt as ThreadsEvalContextExt, SchedulingAction, ThreadId, ThreadManager,
-        ThreadState,
+        ThreadState, Time,
     },
 };
 pub use crate::diagnostics::{
diff --git a/src/machine.rs b/src/machine.rs
index 60fe2a91adf..bd2c4300465 100644
--- a/src/machine.rs
+++ b/src/machine.rs
@@ -4,7 +4,6 @@
 use std::borrow::Cow;
 use std::cell::RefCell;
 use std::fmt;
-use std::time::Instant;
 
 use rand::rngs::StdRng;
 use rand::SeedableRng;
@@ -327,8 +326,8 @@ pub struct Evaluator<'mir, 'tcx> {
     /// The table of directory descriptors.
     pub(crate) dir_handler: shims::unix::DirHandler,
 
-    /// The "time anchor" for this machine's monotone clock (for `Instant` simulation).
-    pub(crate) time_anchor: Instant,
+    /// This machine's monotone clock.
+    pub(crate) clock: Clock,
 
     /// The set of threads.
     pub(crate) threads: ThreadManager<'mir, 'tcx>,
@@ -434,7 +433,6 @@ impl<'mir, 'tcx> Evaluator<'mir, 'tcx> {
             enforce_abi: config.check_abi,
             file_handler: FileHandler::new(config.mute_stdout_stderr),
             dir_handler: Default::default(),
-            time_anchor: Instant::now(),
             layouts,
             threads: ThreadManager::default(),
             static_roots: Vec::new(),
@@ -454,6 +452,7 @@ impl<'mir, 'tcx> Evaluator<'mir, 'tcx> {
             preemption_rate: config.preemption_rate,
             report_progress: config.report_progress,
             basic_block_count: 0,
+            clock: Clock::new(config.isolated_op == IsolatedOp::Allow),
             external_so_lib: config.external_so_file.as_ref().map(|lib_file_path| {
                 // Check if host target == the session target.
                 if env!("TARGET") != target_triple {
@@ -1036,6 +1035,10 @@ impl<'mir, 'tcx> Machine<'mir, 'tcx> for Evaluator<'mir, 'tcx> {
 
         // These are our preemption points.
         ecx.maybe_preempt_active_thread();
+
+        // Make sure some time passes.
+        ecx.machine.clock.tick();
+
         Ok(())
     }
 
diff --git a/src/shims/time.rs b/src/shims/time.rs
index a574a0612c4..f083ab49900 100644
--- a/src/shims/time.rs
+++ b/src/shims/time.rs
@@ -1,6 +1,5 @@
-use std::time::{Duration, Instant, SystemTime};
+use std::time::{Duration, SystemTime};
 
-use crate::concurrency::thread::Time;
 use crate::*;
 
 /// Returns the time elapsed between the provided time and the unix epoch as a `Duration`.
@@ -23,7 +22,6 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
         let this = self.eval_context_mut();
 
         this.assert_target_os("linux", "clock_gettime");
-        this.check_no_isolation("`clock_gettime`")?;
 
         let clk_id = this.read_scalar(clk_id_op)?.to_i32()?;
 
@@ -40,9 +38,10 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
             [this.eval_libc_i32("CLOCK_MONOTONIC")?, this.eval_libc_i32("CLOCK_MONOTONIC_COARSE")?];
 
         let duration = if absolute_clocks.contains(&clk_id) {
+            this.check_no_isolation("`clock_gettime` with `REALTIME` clocks")?;
             system_time_to_duration(&SystemTime::now())?
         } else if relative_clocks.contains(&clk_id) {
-            Instant::now().duration_since(this.machine.time_anchor)
+            this.machine.clock.now().duration_since(this.machine.clock.anchor())
         } else {
             let einval = this.eval_libc("EINVAL")?;
             this.set_last_error(einval)?;
@@ -123,11 +122,10 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
         let this = self.eval_context_mut();
 
         this.assert_target_os("windows", "QueryPerformanceCounter");
-        this.check_no_isolation("`QueryPerformanceCounter`")?;
 
         // QueryPerformanceCounter uses a hardware counter as its basis.
         // Miri will emulate a counter with a resolution of 1 nanosecond.
-        let duration = Instant::now().duration_since(this.machine.time_anchor);
+        let duration = this.machine.clock.now().duration_since(this.machine.clock.anchor());
         let qpc = i64::try_from(duration.as_nanos()).map_err(|_| {
             err_unsup_format!("programs running longer than 2^63 nanoseconds are not supported")
         })?;
@@ -146,7 +144,6 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
         let this = self.eval_context_mut();
 
         this.assert_target_os("windows", "QueryPerformanceFrequency");
-        this.check_no_isolation("`QueryPerformanceFrequency`")?;
 
         // Retrieves the frequency of the hardware performance counter.
         // The frequency of the performance counter is fixed at system boot and
@@ -164,11 +161,10 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
         let this = self.eval_context_ref();
 
         this.assert_target_os("macos", "mach_absolute_time");
-        this.check_no_isolation("`mach_absolute_time`")?;
 
         // This returns a u64, with time units determined dynamically by `mach_timebase_info`.
         // We return plain nanoseconds.
-        let duration = Instant::now().duration_since(this.machine.time_anchor);
+        let duration = this.machine.clock.now().duration_since(this.machine.clock.anchor());
         let res = u64::try_from(duration.as_nanos()).map_err(|_| {
             err_unsup_format!("programs running longer than 2^64 nanoseconds are not supported")
         })?;
@@ -182,7 +178,6 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
         let this = self.eval_context_mut();
 
         this.assert_target_os("macos", "mach_timebase_info");
-        this.check_no_isolation("`mach_timebase_info`")?;
 
         let info = this.deref_operand(info_op)?;
 
@@ -202,7 +197,6 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
         let this = self.eval_context_mut();
 
         this.assert_target_os_is_unix("nanosleep");
-        this.check_no_isolation("`nanosleep`")?;
 
         let duration = match this.read_timespec(&this.deref_operand(req_op)?)? {
             Some(duration) => duration,
@@ -213,17 +207,17 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
             }
         };
         // If adding the duration overflows, let's just sleep for an hour. Waking up early is always acceptable.
-        let timeout_time = Instant::now()
+        let now = this.machine.clock.now();
+        let timeout_time = now
             .checked_add(duration)
-            .unwrap_or_else(|| Instant::now().checked_add(Duration::from_secs(3600)).unwrap());
-        let timeout_time = Time::Monotonic(timeout_time);
+            .unwrap_or_else(|| now.checked_add(Duration::from_secs(3600)).unwrap());
 
         let active_thread = this.get_active_thread();
         this.block_thread(active_thread);
 
         this.register_timeout_callback(
             active_thread,
-            timeout_time,
+            Time::Monotonic(timeout_time),
             Box::new(move |ecx| {
                 ecx.unblock_thread(active_thread);
                 Ok(())
@@ -238,19 +232,18 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
         let this = self.eval_context_mut();
 
         this.assert_target_os("windows", "Sleep");
-        this.check_no_isolation("`Sleep`")?;
 
         let timeout_ms = this.read_scalar(timeout)?.to_u32()?;
 
         let duration = Duration::from_millis(timeout_ms.into());
-        let timeout_time = Time::Monotonic(Instant::now().checked_add(duration).unwrap());
+        let timeout_time = this.machine.clock.now().checked_add(duration).unwrap();
 
         let active_thread = this.get_active_thread();
         this.block_thread(active_thread);
 
         this.register_timeout_callback(
             active_thread,
-            timeout_time,
+            Time::Monotonic(timeout_time),
             Box::new(move |ecx| {
                 ecx.unblock_thread(active_thread);
                 Ok(())
diff --git a/src/shims/unix/linux/sync.rs b/src/shims/unix/linux/sync.rs
index a3f3a28dbc2..cf5a945c5fa 100644
--- a/src/shims/unix/linux/sync.rs
+++ b/src/shims/unix/linux/sync.rs
@@ -1,7 +1,7 @@
 use crate::concurrency::thread::Time;
 use crate::*;
 use rustc_target::abi::{Align, Size};
-use std::time::{Instant, SystemTime};
+use std::time::SystemTime;
 
 /// Implementation of the SYS_futex syscall.
 /// `args` is the arguments *after* the syscall number.
@@ -106,14 +106,14 @@ pub fn futex<'tcx>(
                     if op & futex_realtime != 0 {
                         Time::RealTime(SystemTime::UNIX_EPOCH.checked_add(duration).unwrap())
                     } else {
-                        Time::Monotonic(this.machine.time_anchor.checked_add(duration).unwrap())
+                        Time::Monotonic(this.machine.clock.anchor().checked_add(duration).unwrap())
                     }
                 } else {
                     // FUTEX_WAIT uses a relative timestamp.
                     if op & futex_realtime != 0 {
                         Time::RealTime(SystemTime::now().checked_add(duration).unwrap())
                     } else {
-                        Time::Monotonic(Instant::now().checked_add(duration).unwrap())
+                        Time::Monotonic(this.machine.clock.now().checked_add(duration).unwrap())
                     }
                 })
             };
diff --git a/src/shims/unix/sync.rs b/src/shims/unix/sync.rs
index 0a4904f4bac..496985fd083 100644
--- a/src/shims/unix/sync.rs
+++ b/src/shims/unix/sync.rs
@@ -840,7 +840,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
         let timeout_time = if clock_id == this.eval_libc_i32("CLOCK_REALTIME")? {
             Time::RealTime(SystemTime::UNIX_EPOCH.checked_add(duration).unwrap())
         } else if clock_id == this.eval_libc_i32("CLOCK_MONOTONIC")? {
-            Time::Monotonic(this.machine.time_anchor.checked_add(duration).unwrap())
+            Time::Monotonic(this.machine.clock.anchor().checked_add(duration).unwrap())
         } else {
             throw_unsup_format!("unsupported clock id: {}", clock_id);
         };
diff --git a/tests/pass/shims/time-with-isolation.rs b/tests/pass/shims/time-with-isolation.rs
new file mode 100644
index 00000000000..b6444319b59
--- /dev/null
+++ b/tests/pass/shims/time-with-isolation.rs
@@ -0,0 +1,35 @@
+use std::time::{Duration, Instant};
+
+fn test_sleep() {
+    // We sleep a *long* time here -- but the clock is virtual so the test should still pass quickly.
+    let before = Instant::now();
+    std::thread::sleep(Duration::from_secs(3600));
+    let after = Instant::now();
+    assert!((after - before).as_secs() >= 3600);
+}
+
+/// Ensure that time passes even if we don't sleep (but just work).
+fn test_time_passes() {
+    // Check `Instant`.
+    let now1 = Instant::now();
+    // Do some work to make time pass.
+    for _ in 0..10 {
+        drop(vec![42]);
+    }
+    let now2 = Instant::now();
+    assert!(now2 > now1);
+    // Sanity-check the difference we got.
+    let diff = now2.duration_since(now1);
+    assert_eq!(now1 + diff, now2);
+    assert_eq!(now2 - diff, now1);
+    // The virtual clock is deterministic and I got 29us on a 64-bit Linux machine. However, this
+    // changes according to the platform so we use an interval to be safe. This should be updated
+    // if `NANOSECONDS_PER_BASIC_BLOCK` changes.
+    assert!(diff.as_micros() > 10);
+    assert!(diff.as_micros() < 40);
+}
+
+fn main() {
+    test_time_passes();
+    test_sleep();
+}