Skip to main content

shadow_rs/host/syscall/handler/
futex.rs

1use linux_api::errno::Errno;
2use linux_api::futex::{FUTEX_BITSET_MATCH_ANY, FutexFlags, FutexOp, FutexOpAndFlags};
3use shadow_shim_helper_rs::emulated_time::EmulatedTime;
4use shadow_shim_helper_rs::simulation_time::SimulationTime;
5use shadow_shim_helper_rs::syscall_types::ForeignPtr;
6
7use crate::core::worker::Worker;
8use crate::cshadow as c;
9use crate::host::futex_table::FutexRef;
10use crate::host::syscall::handler::{SyscallContext, SyscallHandler};
11use crate::host::syscall::type_formatting::SyscallNonDeterministicArg;
12use crate::host::syscall::types::SyscallError;
13
14impl SyscallHandler {
15    log_syscall!(
16        futex,
17        /* rv */ std::ffi::c_int,
18        /* uaddr */ *const u32,
19        /* op */ FutexOpAndFlags,
20        /* val */ u32,
21        /* utime */ *const std::ffi::c_void,
22        /* uaddr2 */ *const u32,
23        /* val3 */ SyscallNonDeterministicArg<u32>,
24    );
25    pub fn futex(
26        ctx: &mut SyscallContext,
27        uaddr: ForeignPtr<u32>,
28        op: std::ffi::c_int,
29        val: u32,
30        utime: ForeignPtr<linux_api::time::kernel_timespec>,
31        _uaddr2: ForeignPtr<u32>,
32        val3: u32,
33    ) -> Result<std::ffi::c_int, SyscallError> {
34        // TODO: currently only supports uaddr from the same virtual address space (i.e., process)
35        // Support across different address spaces requires us to compute a unique id from the
36        // hardware address (i.e., page table and offset). This is needed, e.g., when using
37        // futexes across process boundaries.
38
39        let op = FutexOpAndFlags::try_from(op).map_err(|_| Errno::EINVAL)?;
40        log::trace!("futex called with addr={uaddr:p} op={op:?}) and val={val}",);
41
42        let options = op.flags();
43        let operation = op.op();
44
45        for option in options.iter() {
46            match option {
47                FutexFlags::FUTEX_PRIVATE_FLAG => {
48                    // Tells the kernel that this futex isn't shared across processes,
49                    // allowing it to perform some optimizations.
50                    //
51                    // No action needed. Maybe we ought to warn if this flag *isn't*
52                    // included, since we don't implement correctly otherwise
53                    // (see TODO above re address spaces).
54                }
55                FutexFlags::FUTEX_CLOCK_REALTIME => {
56                    // Measure timeouts against CLOCK_REALTIME instead of CLOCK_MONOTONIC.
57                    // Currently they're the same clock for us.
58                }
59                unhandled_flag => {
60                    // We can get here either because we don't have a case for a named FutexFlag,
61                    // or because this value doesn't correspond to any named flags.
62                    log::warn!("Unhandled futex flag: {unhandled_flag:?}");
63                }
64            }
65        }
66
67        match operation {
68            FutexOp::FUTEX_WAIT => {
69                log::trace!("Handling FUTEX_WAIT operation {operation:?}");
70                return Self::futex_wait_helper(ctx, uaddr, val, utime, TimeoutType::Relative);
71            }
72            FutexOp::FUTEX_WAKE => {
73                log::trace!("Handling FUTEX_WAKE operation {operation:?}");
74                // TODO: Should we do better than a cast here? Maybe should add a test for this,
75                // and/or warn if it overflows?
76                return Ok(Self::futex_wake_helper(ctx, uaddr.cast::<()>(), val) as i32);
77            }
78            FutexOp::FUTEX_WAIT_BITSET => {
79                log::trace!("Handling FUTEX_WAIT_BITSET operation {operation:?} bitset {val3:b}");
80                if val3 == FUTEX_BITSET_MATCH_ANY {
81                    return Self::futex_wait_helper(ctx, uaddr, val, utime, TimeoutType::Absolute);
82                }
83                // Other bitsets not yet handled.
84            }
85            FutexOp::FUTEX_WAKE_BITSET => {
86                log::trace!("Handling FUTEX_WAKE_BITSET operation {operation:?} bitset {val3:b}");
87                if val3 == FUTEX_BITSET_MATCH_ANY {
88                    // TODO: Should we do better than a cast here? Maybe should add a test for this,
89                    // and/or warn if it overflows?
90                    return Ok(Self::futex_wake_helper(ctx, uaddr.cast::<()>(), val) as i32);
91                }
92                // Other bitsets not yet handled.
93            }
94            _ => {}
95        }
96
97        log::warn!("Unhandled futex operation {operation:?}");
98        Err(Errno::ENOSYS.into())
99    }
100
101    fn futex_wake_helper(ctx: &mut SyscallContext, ptr: ForeignPtr<()>, num_wakeups: u32) -> u32 {
102        // convert the virtual ptr to a physical ptr that can uniquely identify the futex
103        let ptr = ctx.objs.process.physical_address(ptr);
104
105        // lookup the futex in the futex table
106        let table = ctx.objs.host.futextable_borrow();
107        let futex = table.get(ptr);
108
109        let Some(futex) = futex else {
110            log::trace!("No futex found at futex addr {ptr:p}");
111            return 0;
112        };
113
114        log::trace!("Found futex {:p} at futex addr {ptr:p}", futex.ptr());
115
116        if num_wakeups == 0 {
117            return 0;
118        }
119
120        log::trace!("Futex trying to perform {num_wakeups} wakeups");
121        let num_woken = futex.wake(num_wakeups);
122        log::trace!("Futex was able to perform {num_woken}/{num_wakeups} wakeups");
123
124        num_woken
125    }
126
127    fn futex_wait_helper(
128        ctx: &mut SyscallContext,
129        ptr: ForeignPtr<u32>,
130        expected_val: u32,
131        timeout: ForeignPtr<linux_api::time::kernel_timespec>,
132        timeout_type: TimeoutType,
133    ) -> Result<i32, SyscallError> {
134        let mem = ctx.objs.process.memory_borrow();
135
136        // This is a new wait operation on the futex for this thread.
137        // Check if a timeout was given in the syscall args.
138        let timeout = if timeout.is_null() {
139            None
140        } else {
141            let tspec = mem.read(timeout)?;
142            let sim_time = SimulationTime::try_from(tspec).map_err(|_| Errno::EINVAL)?;
143            Some(sim_time)
144        };
145
146        // Normally, the load/compare is done atomically. Since Shadow does not run multiple
147        // threads from the same plugin at the same time, we do not use atomic ops.
148        // `man 2 futex`: blocking via a futex is an atomic compare-and-block operation
149        let futex_val = mem.read(ptr)?;
150
151        log::trace!("Futex value is {futex_val}, expected value is {expected_val}");
152        if !ctx.handler.is_blocked() && futex_val != expected_val {
153            log::trace!("Futex values don't match, try again later");
154            return Err(Errno::EAGAIN.into());
155        }
156
157        // convert the virtual ptr to a physical ptr that can uniquely identify the futex
158        let ptr = ctx.objs.process.physical_address(ptr.cast::<()>());
159
160        // check if we already have a futex
161        let mut table = ctx.objs.host.futextable_borrow_mut();
162        let futex = table.get(ptr);
163
164        if ctx.handler.is_blocked() {
165            let futex = futex.expect("syscall was blocked, but there wasn't an existing futex");
166
167            let result;
168
169            // we already blocked on wait, so this is either a timeout or wakeup
170            if timeout.is_some() && ctx.handler.did_listen_timeout_expire() {
171                // timeout while waiting for a wakeup
172                log::trace!("Futex {ptr:p} timeout out while waiting");
173                result = Err(Errno::ETIMEDOUT);
174            } else if ctx.objs.thread.unblocked_signal_pending(
175                ctx.objs.process,
176                &ctx.objs.host.shim_shmem_lock_borrow().unwrap(),
177            ) {
178                log::trace!("Futex {ptr:p} has been interrupted by a signal");
179                result = Err(Errno::EINTR);
180            } else {
181                // proper wakeup from another thread
182                log::trace!("Futex {ptr:p} has been woken up");
183                result = Ok(0);
184            }
185
186            // dynamically clean up the futex if needed
187            if futex.listener_count() == 0 {
188                log::trace!("Dynamically freed a futex object for futex addr {ptr:p}");
189                table.remove(ptr).expect("futex disappeared");
190            }
191
192            return result.map_err(Into::into);
193        }
194
195        // we'll need to block; dynamically create a futex if one does not yet exist
196        let futex = match futex {
197            Some(x) => x.clone(),
198            None => {
199                log::trace!("Dynamically created a new futex object for futex addr {ptr:p}");
200
201                let futex = unsafe { c::futex_new(ptr) };
202                assert!(!futex.is_null());
203                let futex = unsafe { FutexRef::new(futex) };
204
205                table
206                    .add(futex.clone())
207                    .expect("new futex is already in table");
208
209                futex
210            }
211        };
212
213        // now we need to block until another thread does a wake on the futex
214        log::trace!(
215            "Futex blocking for wakeup {} timeout",
216            if timeout.is_some() { "with" } else { "without" },
217        );
218        let mut rv = SyscallError::new_blocked_on_futex(futex, /* restartable= */ true);
219        if let Some(timeout) = timeout {
220            let now = Worker::current_time().unwrap();
221            let timeout = match timeout_type {
222                TimeoutType::Relative => now + timeout,
223                TimeoutType::Absolute => EmulatedTime::UNIX_EPOCH + timeout,
224            };
225
226            // handle if the timeout has already expired
227            let timeout = std::cmp::max(timeout, now);
228
229            rv.blocked_condition().unwrap().set_timeout(Some(timeout));
230        }
231
232        Err(rv)
233    }
234
235    log_syscall!(
236        get_robust_list,
237        /* rv */ std::ffi::c_int,
238        /* pid */ std::ffi::c_int,
239        /* head_ptr */ *const std::ffi::c_void,
240        /* len_ptr */ *const libc::size_t,
241    );
242    pub fn get_robust_list(
243        _ctx: &mut SyscallContext,
244        _pid: std::ffi::c_int,
245        _head_ptr: ForeignPtr<ForeignPtr<linux_api::futex::robust_list_head>>,
246        _len_ptr: ForeignPtr<libc::size_t>,
247    ) -> Result<(), Errno> {
248        warn_once_then_debug!("get_robust_list was called but we don't yet support it");
249        Err(Errno::ENOSYS)
250    }
251
252    log_syscall!(
253        set_robust_list,
254        /* rv */ std::ffi::c_int,
255        /* head */ *const std::ffi::c_void,
256        /* len */ libc::size_t,
257    );
258    pub fn set_robust_list(
259        _ctx: &mut SyscallContext,
260        _head: ForeignPtr<linux_api::futex::robust_list_head>,
261        _len: libc::size_t,
262    ) -> Result<(), Errno> {
263        warn_once_then_debug!("set_robust_list was called but we don't yet support it");
264        Err(Errno::ENOSYS)
265    }
266}
267
268/// The type of futex-wait timeout.
269enum TimeoutType {
270    /// Timeout is relative to current time.
271    Relative,
272    /// Timeout is absolute.
273    Absolute,
274}