From 75383065e864a71ffd4d98035c156a2f68e5031b Mon Sep 17 00:00:00 2001 From: Ralf Jung Date: Sat, 15 Nov 2025 10:35:26 +0100 Subject: [PATCH 1/4] epoll: confirm that timerfd wakes up even on write of 0; add nicer libc utils --- tests/pass-dep/libc/libc-epoll-no-blocking.rs | 15 ++++-------- tests/utils/libc.rs | 23 +++++++++++++++++++ 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/tests/pass-dep/libc/libc-epoll-no-blocking.rs b/tests/pass-dep/libc/libc-epoll-no-blocking.rs index b689bb697a..2c55df853a 100644 --- a/tests/pass-dep/libc/libc-epoll-no-blocking.rs +++ b/tests/pass-dep/libc/libc-epoll-no-blocking.rs @@ -262,10 +262,8 @@ fn test_epoll_eventfd() { let flags = libc::EFD_NONBLOCK | libc::EFD_CLOEXEC; let fd = unsafe { libc::eventfd(0, flags) }; - // Write to the eventfd instance. - let sized_8_data: [u8; 8] = 1_u64.to_ne_bytes(); - let res = unsafe { libc_utils::write_all(fd, sized_8_data.as_ptr() as *const libc::c_void, 8) }; - assert_eq!(res, 8); + // Write 1 to the eventfd instance. + libc_utils::write_all_from_slice(fd, &1_u64.to_ne_bytes()).unwrap(); // Create an epoll instance. let epfd = unsafe { libc::epoll_create1(0) }; @@ -281,18 +279,15 @@ fn test_epoll_eventfd() { let expected_value = u64::try_from(fd).unwrap(); check_epoll_wait::<8>(epfd, &[(expected_event, expected_value)]); - // Write to the eventfd again. - let res = unsafe { libc_utils::write_all(fd, sized_8_data.as_ptr() as *const libc::c_void, 8) }; - assert_eq!(res, 8); + // Write 0 to the eventfd. + libc_utils::write_all_from_slice(fd, &0_u64.to_ne_bytes()).unwrap(); // This does not change the status, so we should get no event. // However, Linux performs a spurious wakeup. check_epoll_wait::<8>(epfd, &[(expected_event, expected_value)]); // Read from the eventfd. - let mut buf = [0u8; 8]; - let res = unsafe { libc_utils::read_all(fd, buf.as_mut_ptr().cast(), 8) }; - assert_eq!(res, 8); + libc_utils::read_all_into_array::<8>(fd).unwrap(); // This consumes the event, so the read status is gone. However, deactivation // does not trigger an event. diff --git a/tests/utils/libc.rs b/tests/utils/libc.rs index 4757a5a268..ceeb840f3b 100644 --- a/tests/utils/libc.rs +++ b/tests/utils/libc.rs @@ -22,6 +22,18 @@ pub unsafe fn read_all( return read_so_far as libc::ssize_t; } +#[track_caller] +pub fn read_all_into_array(fd: libc::c_int) -> Result<[u8; N], libc::ssize_t> { + let mut buf = [0; N]; + let res = unsafe { read_all(fd, buf.as_mut_ptr().cast(), buf.len()) }; + if res >= 0 { + assert_eq!(res as usize, buf.len()); + Ok(buf) + } else { + Err(res) + } +} + pub unsafe fn write_all( fd: libc::c_int, buf: *const libc::c_void, @@ -39,3 +51,14 @@ pub unsafe fn write_all( } return written_so_far as libc::ssize_t; } + +#[track_caller] +pub fn write_all_from_slice(fd: libc::c_int, buf: &[u8]) -> Result<(), libc::ssize_t> { + let res = unsafe { write_all(fd, buf.as_ptr().cast(), buf.len()) }; + if res >= 0 { + assert_eq!(res as usize, buf.len()); + Ok(()) + } else { + Err(res) + } +} From 9dbaf02f27a43fb6bbed679e56e8b1b025c82fe0 Mon Sep 17 00:00:00 2001 From: Ralf Jung Date: Sat, 15 Nov 2025 12:12:03 +0100 Subject: [PATCH 2/4] epoll: wake up multiple threads if that is needed to deliver all events --- src/shims/unix/linux_like/epoll.rs | 35 ++++++++------ tests/pass-dep/libc/libc-epoll-blocking.rs | 54 +++++++++++++++++++++- 2 files changed, 72 insertions(+), 17 deletions(-) diff --git a/src/shims/unix/linux_like/epoll.rs b/src/shims/unix/linux_like/epoll.rs index 16b7ddd8bd..19e74919ee 100644 --- a/src/shims/unix/linux_like/epoll.rs +++ b/src/shims/unix/linux_like/epoll.rs @@ -1,5 +1,5 @@ use std::cell::RefCell; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, VecDeque}; use std::io; use std::time::Duration; @@ -22,8 +22,8 @@ struct Epoll { /// "ready" list; instead, a boolean flag in this list tracks which subset is ready. This makes /// `epoll_wait` less efficient, but also requires less bookkeeping. interest_list: RefCell>, - /// A list of thread ids blocked on this epoll instance. - blocked: RefCell>, + /// The queue of threads blocked on this epoll instance, and how many events they'd like to get. + queue: RefCell>, } impl VisitProvenance for Epoll { @@ -459,7 +459,9 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { } }; // Record this thread as blocked. - epfd.blocked.borrow_mut().push(this.active_thread()); + epfd.queue + .borrow_mut() + .push_back((this.active_thread(), maxevents.try_into().unwrap())); // And block it. let dest = dest.clone(); // We keep a strong ref to the underlying `Epoll` to make sure it sticks around. @@ -483,8 +485,8 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { UnblockKind::TimedOut => { // Remove the current active thread_id from the blocked thread_id list. epfd - .blocked.borrow_mut() - .retain(|&id| id != this.active_thread()); + .queue.borrow_mut() + .retain(|&(id, _events)| id != this.active_thread()); this.write_int(0, &dest)?; interp_ok(()) }, @@ -550,7 +552,7 @@ fn update_readiness<'tcx>( &mut dyn FnMut(&mut EpollEventInterest) -> InterpResult<'tcx>, ) -> InterpResult<'tcx>, ) -> InterpResult<'tcx> { - let mut wakeup = false; + let mut num_ready = 0u32; // how many events we have ready to deliver for_each_interest(&mut |interest| { // Update the ready events tracked in this interest. let new_readiness = interest.relevant_events & active_events; @@ -565,17 +567,20 @@ fn update_readiness<'tcx>( ecx.release_clock(|clock| { interest.clock.join(clock); })?; - wakeup = true; + num_ready = num_ready.saturating_add(1); } interp_ok(()) })?; - if wakeup { - // Wake up threads that may have been waiting for events on this epoll. - // Do this only once for all the interests. - // Edge-triggered notification only notify one thread even if there are - // multiple threads blocked on the same epoll. - if let Some(thread_id) = epoll.blocked.borrow_mut().pop() { - ecx.unblock_thread(thread_id, BlockReason::Epoll)?; + // Edge-triggered notifications only wake up as many threads as are needed to deliver + // all the events. + while num_ready > 0 + && let Some((thread_id, events)) = epoll.queue.borrow_mut().pop_front() + { + ecx.unblock_thread(thread_id, BlockReason::Epoll)?; + // Keep track of how many events we have left to deliver (except if we saturated; + // in that case we just wake up everybody). + if num_ready != u32::MAX { + num_ready = num_ready.saturating_sub(events); } } diff --git a/tests/pass-dep/libc/libc-epoll-blocking.rs b/tests/pass-dep/libc/libc-epoll-blocking.rs index 81d4e501c4..e3966651e9 100644 --- a/tests/pass-dep/libc/libc-epoll-blocking.rs +++ b/tests/pass-dep/libc/libc-epoll-blocking.rs @@ -4,7 +4,6 @@ use std::convert::TryInto; use std::thread; -use std::thread::spawn; #[path = "../../utils/libc.rs"] mod libc_utils; @@ -17,6 +16,7 @@ fn main() { test_notification_after_timeout(); test_epoll_race(); wakeup_on_new_interest(); + multiple_events_wake_multiple_threads(); } // Using `as` cast since `EPOLLET` wraps around @@ -90,7 +90,7 @@ fn test_epoll_block_then_unblock() { // epoll_wait before triggering notification so it will block then get unblocked before timeout. let expected_event = u32::try_from(libc::EPOLLIN | libc::EPOLLOUT).unwrap(); let expected_value = fds[0] as u64; - let thread1 = spawn(move || { + let thread1 = thread::spawn(move || { thread::yield_now(); let data = "abcde".as_bytes().as_ptr(); let res = unsafe { libc_utils::write_all(fds[1], data as *const libc::c_void, 5) }; @@ -210,3 +210,53 @@ fn wakeup_on_new_interest() { // This should wake up the thread. t.join().unwrap(); } + +/// Ensure that if a single operation triggers multiple events, we wake up enough threads +/// to consume them all. +fn multiple_events_wake_multiple_threads() { + // Create an epoll instance. + let epfd = unsafe { libc::epoll_create1(0) }; + assert_ne!(epfd, -1); + + // Create an eventfd instance. + let flags = libc::EFD_NONBLOCK | libc::EFD_CLOEXEC; + let fd1 = unsafe { libc::eventfd(0, flags) }; + // Make a duplicate so that we have two file descriptors for the same file description. + let fd2 = unsafe { libc::dup(fd1) }; + + // Register both with epoll. + let mut ev = libc::epoll_event { events: EPOLL_IN_OUT_ET, u64: fd1 as u64 }; + let res = unsafe { libc::epoll_ctl(epfd, libc::EPOLL_CTL_ADD, fd1, &mut ev) }; + assert_eq!(res, 0); + let mut ev = libc::epoll_event { events: EPOLL_IN_OUT_ET, u64: fd2 as u64 }; + let res = unsafe { libc::epoll_ctl(epfd, libc::EPOLL_CTL_ADD, fd2, &mut ev) }; + assert_eq!(res, 0); + // Consume the initial event. + let expected = [(libc::EPOLLOUT as u32, fd1 as u64), (libc::EPOLLOUT as u32, fd2 as u64)]; + check_epoll_wait::<8>(epfd, &expected, -1); + + // Block two threads on the epoll, both wanting to get just one event. + let t1 = thread::spawn(move || { + let mut e = libc::epoll_event { events: 0, u64: 0 }; + let res = unsafe { libc::epoll_wait(epfd, &raw mut e, 1, -1) }; + assert!(res == 1); + (e.events, e.u64) + }); + let t2 = thread::spawn(move || { + let mut e = libc::epoll_event { events: 0, u64: 0 }; + let res = unsafe { libc::epoll_wait(epfd, &raw mut e, 1, -1) }; + assert!(res == 1); + (e.events, e.u64) + }); + // Yield so both threads are waiting now. + thread::yield_now(); + + // Trigger the eventfd. This triggers two events at once! + libc_utils::write_all_from_slice(fd1, &0_u64.to_ne_bytes()).unwrap(); + + // Both threads should have been woken up so that both events can be consumed. + let e1 = t1.join().unwrap(); + let e2 = t2.join().unwrap(); + // Ensure that across the two threads we got both events. + assert!(expected == [e1, e2] || expected == [e2, e1]); +} From fff7fb8d90ef155e85ac3f56ccaba746ee722e0e Mon Sep 17 00:00:00 2001 From: Ralf Jung Date: Sat, 15 Nov 2025 12:14:07 +0100 Subject: [PATCH 3/4] fix deadlock error showing 'no span available' nonsense backtraces --- src/concurrency/thread.rs | 7 ++++-- src/diagnostics.rs | 2 +- .../fail-dep/libc/eventfd_block_read_twice.rs | 2 -- .../libc/eventfd_block_read_twice.stderr | 13 +---------- .../libc/eventfd_block_write_twice.rs | 4 +--- .../libc/eventfd_block_write_twice.stderr | 13 +---------- .../libc/libc_epoll_block_two_thread.rs | 23 ++++++++----------- .../libc/libc_epoll_block_two_thread.stderr | 15 ++---------- .../libc/socketpair-close-while-blocked.rs | 1 - .../socketpair-close-while-blocked.stderr | 7 +----- .../libc/socketpair_block_read_twice.rs | 2 -- .../libc/socketpair_block_read_twice.stderr | 13 +---------- .../libc/socketpair_block_write_twice.rs | 2 -- .../libc/socketpair_block_write_twice.stderr | 13 +---------- 14 files changed, 24 insertions(+), 93 deletions(-) diff --git a/src/concurrency/thread.rs b/src/concurrency/thread.rs index 7838bd2143..c7ae335d04 100644 --- a/src/concurrency/thread.rs +++ b/src/concurrency/thread.rs @@ -515,10 +515,13 @@ impl<'tcx> ThreadManager<'tcx> { &mut self.threads[self.active_thread].stack } - pub fn all_stacks( + pub fn all_blocked_stacks( &self, ) -> impl Iterator>])> { - self.threads.iter_enumerated().map(|(id, t)| (id, &t.stack[..])) + self.threads + .iter_enumerated() + .filter(|(_id, t)| matches!(t.state, ThreadState::Blocked { .. })) + .map(|(id, t)| (id, &t.stack[..])) } /// Create a new thread and returns its id. diff --git a/src/diagnostics.rs b/src/diagnostics.rs index 47d60f912c..70543c653b 100644 --- a/src/diagnostics.rs +++ b/src/diagnostics.rs @@ -469,7 +469,7 @@ pub fn report_result<'tcx>( eprint!("{extra}"); // newlines are already in the string if show_all_threads { - for (thread, stack) in ecx.machine.threads.all_stacks() { + for (thread, stack) in ecx.machine.threads.all_blocked_stacks() { if thread != ecx.active_thread() { let stacktrace = Frame::generate_stacktrace_from_stack(stack); let (stacktrace, was_pruned) = prune_stacktrace(stacktrace, &ecx.machine); diff --git a/tests/fail-dep/libc/eventfd_block_read_twice.rs b/tests/fail-dep/libc/eventfd_block_read_twice.rs index 9dc554030c..98cc80b6b4 100644 --- a/tests/fail-dep/libc/eventfd_block_read_twice.rs +++ b/tests/fail-dep/libc/eventfd_block_read_twice.rs @@ -1,6 +1,4 @@ //@only-target: linux android illumos -//~^ERROR: deadlocked -//~^^ERROR: deadlocked //@compile-flags: -Zmiri-deterministic-concurrency //@error-in-other-file: deadlock diff --git a/tests/fail-dep/libc/eventfd_block_read_twice.stderr b/tests/fail-dep/libc/eventfd_block_read_twice.stderr index 4730045cfe..a7dfa0b6ea 100644 --- a/tests/fail-dep/libc/eventfd_block_read_twice.stderr +++ b/tests/fail-dep/libc/eventfd_block_read_twice.stderr @@ -14,24 +14,13 @@ note: inside `main` LL | thread2.join().unwrap(); | ^^^^^^^^^^^^^^ -error: the evaluated program deadlocked - | - = note: this thread got stuck here - = note: (no span available) - error: the evaluated program deadlocked --> tests/fail-dep/libc/eventfd_block_read_twice.rs:LL:CC | LL | let res: i64 = unsafe { libc::read(fd, buf.as_mut_ptr().cast(), 8).try_into().unwrap() }; | ^ this thread got stuck here -error: the evaluated program deadlocked - | - = note: this thread got stuck here - = note: (no span available) - = note: duplicate diagnostic emitted due to `-Z deduplicate-diagnostics=no` - note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace -error: aborting due to 4 previous errors +error: aborting due to 2 previous errors diff --git a/tests/fail-dep/libc/eventfd_block_write_twice.rs b/tests/fail-dep/libc/eventfd_block_write_twice.rs index 5297a32977..1a1d76eda2 100644 --- a/tests/fail-dep/libc/eventfd_block_write_twice.rs +++ b/tests/fail-dep/libc/eventfd_block_write_twice.rs @@ -1,6 +1,4 @@ //@only-target: linux android illumos -//~^ERROR: deadlocked -//~^^ERROR: deadlocked //@compile-flags: -Zmiri-deterministic-concurrency //@error-in-other-file: deadlock @@ -38,7 +36,7 @@ fn main() { let thread2 = thread::spawn(move || { let sized_8_data = (u64::MAX - 1).to_ne_bytes(); - // Write u64::MAX - 1, so the all subsequent write will block. + // Write u64::MAX - 1, so that all subsequent writes will block. let res: i64 = unsafe { // This `write` will initially blocked, then get unblocked by thread3, then get blocked again // because the `write` in thread1 executes first. diff --git a/tests/fail-dep/libc/eventfd_block_write_twice.stderr b/tests/fail-dep/libc/eventfd_block_write_twice.stderr index c0e38df98d..0e554598ec 100644 --- a/tests/fail-dep/libc/eventfd_block_write_twice.stderr +++ b/tests/fail-dep/libc/eventfd_block_write_twice.stderr @@ -14,24 +14,13 @@ note: inside `main` LL | thread2.join().unwrap(); | ^^^^^^^^^^^^^^ -error: the evaluated program deadlocked - | - = note: this thread got stuck here - = note: (no span available) - error: the evaluated program deadlocked --> tests/fail-dep/libc/eventfd_block_write_twice.rs:LL:CC | LL | libc::write(fd, sized_8_data.as_ptr() as *const libc::c_void, 8).try_into().unwrap() | ^ this thread got stuck here -error: the evaluated program deadlocked - | - = note: this thread got stuck here - = note: (no span available) - = note: duplicate diagnostic emitted due to `-Z deduplicate-diagnostics=no` - note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace -error: aborting due to 4 previous errors +error: aborting due to 2 previous errors diff --git a/tests/fail-dep/libc/libc_epoll_block_two_thread.rs b/tests/fail-dep/libc/libc_epoll_block_two_thread.rs index 054cb812d9..a48a7c08b9 100644 --- a/tests/fail-dep/libc/libc_epoll_block_two_thread.rs +++ b/tests/fail-dep/libc/libc_epoll_block_two_thread.rs @@ -1,11 +1,9 @@ //@compile-flags: -Zmiri-deterministic-concurrency -//~^ERROR: deadlocked -//~^^ERROR: deadlocked //@only-target: linux android illumos //@error-in-other-file: deadlock use std::convert::TryInto; -use std::thread::spawn; +use std::thread; // Using `as` cast since `EPOLLET` wraps around const EPOLL_IN_OUT_ET: u32 = (libc::EPOLLIN | libc::EPOLLOUT | libc::EPOLLET) as _; @@ -66,22 +64,21 @@ fn main() { let expected_event = u32::try_from(libc::EPOLLIN | libc::EPOLLOUT).unwrap(); let expected_value = fds[0] as u64; - let thread1 = spawn(move || { + let thread1 = thread::spawn(move || { check_epoll_wait::<1>(epfd, &[(expected_event, expected_value)], -1); - //~^ERROR: deadlocked }); - let thread2 = spawn(move || { + let thread2 = thread::spawn(move || { check_epoll_wait::<1>(epfd, &[(expected_event, expected_value)], -1); + //~^ERROR: deadlocked }); + // Yield so the threads are both blocked. + thread::yield_now(); - let thread3 = spawn(move || { - // Just a single write, so we only wake up one of them. - let data = "abcde".as_bytes().as_ptr(); - let res = unsafe { libc::write(fds[1], data as *const libc::c_void, 5) }; - assert!(res > 0 && res <= 5); - }); + // Just a single write, so we only wake up one of them. + let data = "abcde".as_bytes().as_ptr(); + let res = unsafe { libc::write(fds[1], data as *const libc::c_void, 5) }; + assert!(res > 0 && res <= 5); thread1.join().unwrap(); thread2.join().unwrap(); - thread3.join().unwrap(); } diff --git a/tests/fail-dep/libc/libc_epoll_block_two_thread.stderr b/tests/fail-dep/libc/libc_epoll_block_two_thread.stderr index c3cd04c0b5..63dc089809 100644 --- a/tests/fail-dep/libc/libc_epoll_block_two_thread.stderr +++ b/tests/fail-dep/libc/libc_epoll_block_two_thread.stderr @@ -1,8 +1,3 @@ -error: the evaluated program deadlocked - | - = note: this thread got stuck here - = note: (no span available) - error: the evaluated program deadlocked --> RUSTLIB/std/src/sys/thread/PLATFORM.rs:LL:CC | @@ -16,7 +11,7 @@ LL | let ret = unsafe { libc::pthread_join(id, ptr::null_mut()) }; note: inside `main` --> tests/fail-dep/libc/libc_epoll_block_two_thread.rs:LL:CC | -LL | thread1.join().unwrap(); +LL | thread2.join().unwrap(); | ^^^^^^^^^^^^^^ error: the evaluated program deadlocked @@ -25,13 +20,7 @@ error: the evaluated program deadlocked LL | check_epoll_wait::(epfd, &[(expected_event, expected_value)], -1); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this thread got stuck here -error: the evaluated program deadlocked - | - = note: this thread got stuck here - = note: (no span available) - = note: duplicate diagnostic emitted due to `-Z deduplicate-diagnostics=no` - note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace -error: aborting due to 4 previous errors +error: aborting due to 2 previous errors diff --git a/tests/fail-dep/libc/socketpair-close-while-blocked.rs b/tests/fail-dep/libc/socketpair-close-while-blocked.rs index 0699dec655..a975e404a9 100644 --- a/tests/fail-dep/libc/socketpair-close-while-blocked.rs +++ b/tests/fail-dep/libc/socketpair-close-while-blocked.rs @@ -1,6 +1,5 @@ //! This is a regression test for : we had some //! faulty logic around `release_clock` that led to this code not reporting a data race. -//~^^ERROR: deadlock //@ignore-target: windows # no libc socketpair on Windows //@compile-flags: -Zmiri-deterministic-concurrency //@error-in-other-file: deadlock diff --git a/tests/fail-dep/libc/socketpair-close-while-blocked.stderr b/tests/fail-dep/libc/socketpair-close-while-blocked.stderr index ac2346f606..1e802209fd 100644 --- a/tests/fail-dep/libc/socketpair-close-while-blocked.stderr +++ b/tests/fail-dep/libc/socketpair-close-while-blocked.stderr @@ -20,12 +20,7 @@ error: the evaluated program deadlocked LL | libc::read(fds[1], buf.as_mut_ptr().cast(), buf.len() as libc::size_t) | ^ this thread got stuck here -error: the evaluated program deadlocked - | - = note: this thread got stuck here - = note: (no span available) - note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace -error: aborting due to 3 previous errors +error: aborting due to 2 previous errors diff --git a/tests/fail-dep/libc/socketpair_block_read_twice.rs b/tests/fail-dep/libc/socketpair_block_read_twice.rs index 0fecfb8f66..37aa459064 100644 --- a/tests/fail-dep/libc/socketpair_block_read_twice.rs +++ b/tests/fail-dep/libc/socketpair_block_read_twice.rs @@ -1,6 +1,4 @@ //@ignore-target: windows # No libc socketpair on Windows -//~^ERROR: deadlocked -//~^^ERROR: deadlocked // test_race depends on a deterministic schedule. //@compile-flags: -Zmiri-deterministic-concurrency //@error-in-other-file: deadlock diff --git a/tests/fail-dep/libc/socketpair_block_read_twice.stderr b/tests/fail-dep/libc/socketpair_block_read_twice.stderr index c599432ff1..3f7bbc7796 100644 --- a/tests/fail-dep/libc/socketpair_block_read_twice.stderr +++ b/tests/fail-dep/libc/socketpair_block_read_twice.stderr @@ -14,24 +14,13 @@ note: inside `main` LL | thread2.join().unwrap(); | ^^^^^^^^^^^^^^ -error: the evaluated program deadlocked - | - = note: this thread got stuck here - = note: (no span available) - error: the evaluated program deadlocked --> tests/fail-dep/libc/socketpair_block_read_twice.rs:LL:CC | LL | libc::read(fds[1], buf.as_mut_ptr().cast(), buf.len() as libc::size_t) | ^ this thread got stuck here -error: the evaluated program deadlocked - | - = note: this thread got stuck here - = note: (no span available) - = note: duplicate diagnostic emitted due to `-Z deduplicate-diagnostics=no` - note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace -error: aborting due to 4 previous errors +error: aborting due to 2 previous errors diff --git a/tests/fail-dep/libc/socketpair_block_write_twice.rs b/tests/fail-dep/libc/socketpair_block_write_twice.rs index 048938c091..34eab8aaa0 100644 --- a/tests/fail-dep/libc/socketpair_block_write_twice.rs +++ b/tests/fail-dep/libc/socketpair_block_write_twice.rs @@ -1,6 +1,4 @@ //@ignore-target: windows # No libc socketpair on Windows -//~^ERROR: deadlocked -//~^^ERROR: deadlocked // test_race depends on a deterministic schedule. //@compile-flags: -Zmiri-deterministic-concurrency //@error-in-other-file: deadlock diff --git a/tests/fail-dep/libc/socketpair_block_write_twice.stderr b/tests/fail-dep/libc/socketpair_block_write_twice.stderr index 2230d99fc6..b8dbb513da 100644 --- a/tests/fail-dep/libc/socketpair_block_write_twice.stderr +++ b/tests/fail-dep/libc/socketpair_block_write_twice.stderr @@ -14,24 +14,13 @@ note: inside `main` LL | thread2.join().unwrap(); | ^^^^^^^^^^^^^^ -error: the evaluated program deadlocked - | - = note: this thread got stuck here - = note: (no span available) - error: the evaluated program deadlocked --> tests/fail-dep/libc/socketpair_block_write_twice.rs:LL:CC | LL | let res = unsafe { libc::write(fds[0], data.as_ptr() as *const libc::c_void, data.len()) }; | ^ this thread got stuck here -error: the evaluated program deadlocked - | - = note: this thread got stuck here - = note: (no span available) - = note: duplicate diagnostic emitted due to `-Z deduplicate-diagnostics=no` - note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace -error: aborting due to 4 previous errors +error: aborting due to 2 previous errors From 963eda6986d1cf8604f3802b77f139346c2ab5f3 Mon Sep 17 00:00:00 2001 From: Ralf Jung Date: Sat, 15 Nov 2025 12:28:53 +0100 Subject: [PATCH 4/4] epoll: ensure we count the events that each thread will take --- src/shims/unix/linux_like/epoll.rs | 14 +++++-- .../libc/libc_epoll_block_two_thread.rs | 40 ++++++++++--------- .../libc/libc_epoll_block_two_thread.stderr | 4 +- tests/pass-dep/libc/libc-epoll-blocking.rs | 3 +- 4 files changed, 36 insertions(+), 25 deletions(-) diff --git a/src/shims/unix/linux_like/epoll.rs b/src/shims/unix/linux_like/epoll.rs index 19e74919ee..04f4e33e1d 100644 --- a/src/shims/unix/linux_like/epoll.rs +++ b/src/shims/unix/linux_like/epoll.rs @@ -479,7 +479,8 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { |this, unblock: UnblockKind| { match unblock { UnblockKind::Ready => { - return_ready_list(&epfd, &dest, &event, this)?; + let events = return_ready_list(&epfd, &dest, &event, this)?; + assert!(events > 0, "we got woken up with no events to deliver"); interp_ok(()) }, UnblockKind::TimedOut => { @@ -583,6 +584,13 @@ fn update_readiness<'tcx>( num_ready = num_ready.saturating_sub(events); } } + // Sanity-check: if there are threads left to wake up, then there are no more ready events. + if !epoll.queue.borrow().is_empty() { + assert!( + epoll.interest_list.borrow().values().all(|i| !i.ready), + "there are unconsumed ready events and threads ready to take them" + ); + } interp_ok(()) } @@ -594,7 +602,7 @@ fn return_ready_list<'tcx>( dest: &MPlaceTy<'tcx>, events: &MPlaceTy<'tcx>, ecx: &mut MiriInterpCx<'tcx>, -) -> InterpResult<'tcx> { +) -> InterpResult<'tcx, i32> { let mut interest_list = epfd.interest_list.borrow_mut(); let mut num_of_events: i32 = 0; let mut array_iter = ecx.project_array_fields(events)?; @@ -634,5 +642,5 @@ fn return_ready_list<'tcx>( } } ecx.write_int(num_of_events, dest)?; - interp_ok(()) + interp_ok(num_of_events) } diff --git a/tests/fail-dep/libc/libc_epoll_block_two_thread.rs b/tests/fail-dep/libc/libc_epoll_block_two_thread.rs index a48a7c08b9..8c51416f5a 100644 --- a/tests/fail-dep/libc/libc_epoll_block_two_thread.rs +++ b/tests/fail-dep/libc/libc_epoll_block_two_thread.rs @@ -5,6 +5,9 @@ use std::convert::TryInto; use std::thread; +#[path = "../../utils/libc.rs"] +mod libc_utils; + // Using `as` cast since `EPOLLET` wraps around const EPOLL_IN_OUT_ET: u32 = (libc::EPOLLIN | libc::EPOLLOUT | libc::EPOLLET) as _; @@ -47,37 +50,36 @@ fn main() { let epfd = unsafe { libc::epoll_create1(0) }; assert_ne!(epfd, -1); - // Create a socketpair instance. - let mut fds = [-1, -1]; - let res = unsafe { libc::socketpair(libc::AF_UNIX, libc::SOCK_STREAM, 0, fds.as_mut_ptr()) }; - assert_eq!(res, 0); + // Create an eventfd instance. + let flags = libc::EFD_NONBLOCK | libc::EFD_CLOEXEC; + let fd1 = unsafe { libc::eventfd(0, flags) }; + // Make a duplicate so that we have two file descriptors for the same file description. + let fd2 = unsafe { libc::dup(fd1) }; - // Register one side of the socketpair with epoll. - let mut ev = libc::epoll_event { events: EPOLL_IN_OUT_ET, u64: fds[0] as u64 }; - let res = unsafe { libc::epoll_ctl(epfd, libc::EPOLL_CTL_ADD, fds[0], &mut ev) }; + // Register both with epoll. + let mut ev = libc::epoll_event { events: EPOLL_IN_OUT_ET, u64: fd1 as u64 }; + let res = unsafe { libc::epoll_ctl(epfd, libc::EPOLL_CTL_ADD, fd1, &mut ev) }; + assert_eq!(res, 0); + let mut ev = libc::epoll_event { events: EPOLL_IN_OUT_ET, u64: fd2 as u64 }; + let res = unsafe { libc::epoll_ctl(epfd, libc::EPOLL_CTL_ADD, fd2, &mut ev) }; assert_eq!(res, 0); - // epoll_wait to clear notification. - let expected_event = u32::try_from(libc::EPOLLOUT).unwrap(); - let expected_value = fds[0] as u64; - check_epoll_wait::<1>(epfd, &[(expected_event, expected_value)], 0); + // Consume the initial events. + let expected = [(libc::EPOLLOUT as u32, fd1 as u64), (libc::EPOLLOUT as u32, fd2 as u64)]; + check_epoll_wait::<8>(epfd, &expected, -1); - let expected_event = u32::try_from(libc::EPOLLIN | libc::EPOLLOUT).unwrap(); - let expected_value = fds[0] as u64; let thread1 = thread::spawn(move || { - check_epoll_wait::<1>(epfd, &[(expected_event, expected_value)], -1); + check_epoll_wait::<2>(epfd, &expected, -1); }); let thread2 = thread::spawn(move || { - check_epoll_wait::<1>(epfd, &[(expected_event, expected_value)], -1); + check_epoll_wait::<2>(epfd, &expected, -1); //~^ERROR: deadlocked }); // Yield so the threads are both blocked. thread::yield_now(); - // Just a single write, so we only wake up one of them. - let data = "abcde".as_bytes().as_ptr(); - let res = unsafe { libc::write(fds[1], data as *const libc::c_void, 5) }; - assert!(res > 0 && res <= 5); + // Create two events at once. + libc_utils::write_all_from_slice(fd1, &0_u64.to_ne_bytes()).unwrap(); thread1.join().unwrap(); thread2.join().unwrap(); diff --git a/tests/fail-dep/libc/libc_epoll_block_two_thread.stderr b/tests/fail-dep/libc/libc_epoll_block_two_thread.stderr index 63dc089809..6740faedb3 100644 --- a/tests/fail-dep/libc/libc_epoll_block_two_thread.stderr +++ b/tests/fail-dep/libc/libc_epoll_block_two_thread.stderr @@ -17,8 +17,8 @@ LL | thread2.join().unwrap(); error: the evaluated program deadlocked --> tests/fail-dep/libc/libc_epoll_block_two_thread.rs:LL:CC | -LL | check_epoll_wait::(epfd, &[(expected_event, expected_value)], -1); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this thread got stuck here +LL | check_epoll_wait::(epfd, &expected, -1); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this thread got stuck here note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace diff --git a/tests/pass-dep/libc/libc-epoll-blocking.rs b/tests/pass-dep/libc/libc-epoll-blocking.rs index e3966651e9..c67386b4f8 100644 --- a/tests/pass-dep/libc/libc-epoll-blocking.rs +++ b/tests/pass-dep/libc/libc-epoll-blocking.rs @@ -231,7 +231,8 @@ fn multiple_events_wake_multiple_threads() { let mut ev = libc::epoll_event { events: EPOLL_IN_OUT_ET, u64: fd2 as u64 }; let res = unsafe { libc::epoll_ctl(epfd, libc::EPOLL_CTL_ADD, fd2, &mut ev) }; assert_eq!(res, 0); - // Consume the initial event. + + // Consume the initial events. let expected = [(libc::EPOLLOUT as u32, fd1 as u64), (libc::EPOLLOUT as u32, fd2 as u64)]; check_epoll_wait::<8>(epfd, &expected, -1);