Skip to main content

yash_env/system/concurrency/
signal.rs

1// This file is part of yash, an extended POSIX shell.
2// Copyright (C) 2026 WATANABE Yuki
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17//! Implementation of `Concurrent` related to signals
18
19use super::Concurrent;
20use crate::job::{RunBlocking, RunUnblocking};
21use crate::signal::Number;
22use crate::subshell::BlockSignals;
23use crate::system::signal::Sigset as _;
24use crate::system::{Disposition, Errno, Sigaction, Sigmask, SigmaskOp};
25use crate::trap::SignalSystem;
26use std::rc::Rc;
27
28/// Implementation of `SignalSystem` for `Concurrent`
29///
30/// `Concurrent` controls both the signal dispositions and the signal mask, so
31/// it can receive and handle signals without race conditions.
32impl<S> SignalSystem for Rc<Concurrent<S>>
33where
34    S: Sigmask + Sigaction,
35{
36    /// Returns the current disposition of the specified signal.
37    ///
38    /// This implementation simply forwards the call to the inner system's
39    /// [`GetSigaction::get_sigaction`](crate::system::GetSigaction::get_sigaction)
40    /// method.
41    fn get_disposition(&self, signal: Number) -> Result<Disposition, Errno> {
42        self.inner.get_sigaction(signal)
43    }
44
45    /// Sets the disposition of the specified signal to the given value and
46    /// returns the old disposition.
47    ///
48    /// This implementation both updates the signal disposition and the signal
49    /// mask to ensure that the [`select`](super::Select::select) method can
50    /// respond to received signals without race conditions. Specifically:
51    ///
52    /// - When setting the disposition to `Default` or `Ignore`, the signal is
53    ///   unblocked to allow it to be delivered as soon as possible.
54    /// - When setting the disposition to `Catch`, the signal is blocked so that
55    ///   it is only delivered inside the `select` method.
56    fn set_disposition(
57        &self,
58        signal: Number,
59        disposition: Disposition,
60    ) -> impl Future<Output = Result<Disposition, Errno>> + use<S> {
61        let this = Rc::clone(self);
62        async move {
63            if disposition == Disposition::Catch {
64                // Before setting the disposition to `Catch`, we need to block the signal
65                // to prevent it from being delivered before the disposition is updated.
66                this.update_sigmask_and_select_mask(SigmaskOp::Add, signal)
67                    .await?;
68            }
69
70            let old_action = this.inner.sigaction(signal, disposition)?;
71
72            if disposition != Disposition::Catch {
73                // After setting the disposition to `Default` or `Ignore`, we need to unblock
74                // the signal to allow it to be delivered if it was previously blocked.
75                this.update_sigmask_and_select_mask(SigmaskOp::Remove, signal)
76                    .await?;
77            }
78
79            Ok(old_action)
80        }
81    }
82}
83
84impl<S> Concurrent<S>
85where
86    S: Sigmask,
87{
88    /// Wrapper of the inner system's [`Sigmask::sigmask`] method that also
89    /// updates the `select_mask` field.
90    async fn update_sigmask_and_select_mask(
91        &self,
92        op: SigmaskOp,
93        signal: Number,
94    ) -> Result<(), Errno> {
95        let mut mask_of_signal = S::Sigset::new();
96        mask_of_signal.insert(signal)?;
97        let mut old_mask = S::Sigset::new();
98        self.inner
99            .sigmask(Some((op, &mask_of_signal)), Some(&mut old_mask))
100            .await?;
101
102        self.state
103            .borrow_mut()
104            .select_mask
105            .get_or_insert(old_mask)
106            .remove(signal)
107    }
108}
109
110impl<S> BlockSignals for Concurrent<S>
111where
112    S: Sigmask + BlockSignals,
113{
114    type SavedMask = S::SavedMask;
115
116    async fn block_sigint_sigquit(&self) -> Result<Self::SavedMask, Errno> {
117        self.inner.block_sigint_sigquit().await
118    }
119
120    async fn restore_sigmask(&self, mask: Self::SavedMask) -> Result<(), Errno> {
121        self.inner.restore_sigmask(mask).await
122    }
123}
124
125impl<S> BlockSignals for Rc<Concurrent<S>>
126where
127    S: Sigmask + BlockSignals,
128{
129    type SavedMask = S::SavedMask;
130
131    /// Blocks SIGINT and SIGQUIT, returning the previous signal mask.
132    ///
133    /// Between `block_sigint_sigquit` and
134    /// [`restore_sigmask`](Self::restore_sigmask), the disposition of any
135    /// signal must not be changed with the
136    /// [`set_disposition`](SignalSystem::set_disposition) method, or the
137    /// `restore_sigmask` method will leave the signal mask in an inconsistent
138    /// state. You must not call the [`select`](crate::system::Select::select)
139    /// method either, since it does not take the effect of
140    /// `block_sigint_sigquit` into account.
141    async fn block_sigint_sigquit(&self) -> Result<Self::SavedMask, Errno> {
142        (self as &Concurrent<S>).block_sigint_sigquit().await
143    }
144
145    async fn restore_sigmask(&self, mask: Self::SavedMask) -> Result<(), Errno> {
146        (self as &Concurrent<S>).restore_sigmask(mask).await
147    }
148}
149
150impl<S> RunBlocking for Concurrent<S>
151where
152    S: Sigmask + RunBlocking,
153{
154    async fn run_blocking<F, T>(&self, signal: Number, f: F) -> Result<T, Errno>
155    where
156        F: AsyncFnOnce() -> Result<T, Errno>,
157    {
158        self.inner.run_blocking(signal, f).await
159    }
160}
161
162impl<S> RunBlocking for Rc<Concurrent<S>>
163where
164    S: Sigmask + RunBlocking,
165{
166    /// Runs the given function with the specified signal blocked.
167    ///
168    /// The disposition of any signal must not be changed with inside the
169    /// function, or the signal mask will be left in an inconsistent state after
170    /// the function returns. The function must not call the
171    /// [`select`](crate::system::Select::select) method either, since it does
172    /// not take the effect of `run_blocking` into account.
173    async fn run_blocking<F, T>(&self, signal: Number, f: F) -> Result<T, Errno>
174    where
175        F: AsyncFnOnce() -> Result<T, Errno>,
176    {
177        (self as &Concurrent<S>).run_blocking(signal, f).await
178    }
179}
180
181impl<S> RunUnblocking for Concurrent<S>
182where
183    S: Sigmask + RunUnblocking,
184{
185    async fn run_unblocking<F, T>(&self, signal: Number, f: F) -> Result<T, Errno>
186    where
187        F: AsyncFnOnce() -> Result<T, Errno>,
188    {
189        self.inner.run_unblocking(signal, f).await
190    }
191}
192
193impl<S> RunUnblocking for Rc<Concurrent<S>>
194where
195    S: Sigmask + RunUnblocking,
196{
197    /// Runs the given function with the specified signal unblocked.
198    ///
199    /// The disposition of any signal must not be changed with inside the
200    /// function, or the signal mask will be left in an inconsistent state after
201    /// the function returns. The function must not call the
202    /// [`select`](crate::system::Select::select) method either, since it does
203    /// not take the effect of `run_unblocking` into account.
204    async fn run_unblocking<F, T>(&self, signal: Number, f: F) -> Result<T, Errno>
205    where
206        F: AsyncFnOnce() -> Result<T, Errno>,
207    {
208        (self as &Concurrent<S>).run_unblocking(signal, f).await
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215    use crate::job::{ProcessResult, ProcessState};
216    use crate::system::SendSignal as _;
217    use crate::system::r#virtual::sigset::Sigset;
218    use crate::system::r#virtual::{SIGQUIT, SIGTERM, SIGUSR1, VirtualSystem};
219    use futures_util::FutureExt as _;
220    use std::num::NonZero;
221
222    #[test]
223    fn setting_disposition_from_default_to_catch() {
224        let inner = VirtualSystem::new();
225        let system = Rc::new(Concurrent::new(inner.clone()));
226        let result = system
227            .set_disposition(SIGTERM, Disposition::Catch)
228            .now_or_never()
229            .unwrap();
230        assert_eq!(result, Ok(Disposition::Default));
231        assert_eq!(system.get_disposition(SIGTERM), Ok(Disposition::Catch));
232
233        // When the disposition is set to `Catch`, the signal is blocked.
234        inner.raise(SIGTERM).now_or_never().unwrap().unwrap();
235        // So the process should still be running.
236        assert_eq!(inner.current_process().state(), ProcessState::Running);
237    }
238
239    #[test]
240    fn setting_disposition_from_default_to_ignore() {
241        let inner = VirtualSystem::new();
242        let system = Rc::new(Concurrent::new(inner.clone()));
243        let result = system
244            .set_disposition(SIGTERM, Disposition::Ignore)
245            .now_or_never()
246            .unwrap();
247        assert_eq!(result, Ok(Disposition::Default));
248        assert_eq!(system.get_disposition(SIGTERM), Ok(Disposition::Ignore));
249
250        // Since the signal is ignored, sending it should have no effect.
251        inner.raise(SIGTERM).now_or_never().unwrap().unwrap();
252        assert_eq!(inner.current_process().state(), ProcessState::Running);
253    }
254
255    #[test]
256    fn setting_disposition_from_ignore_to_catch() {
257        let system = Rc::new(Concurrent::new(VirtualSystem::new()));
258        system
259            .set_disposition(SIGQUIT, Disposition::Ignore)
260            .now_or_never()
261            .unwrap()
262            .unwrap();
263
264        let result = system
265            .set_disposition(SIGQUIT, Disposition::Catch)
266            .now_or_never()
267            .unwrap();
268        assert_eq!(result, Ok(Disposition::Ignore));
269        assert_eq!(system.get_disposition(SIGQUIT), Ok(Disposition::Catch));
270    }
271
272    #[test]
273    fn setting_disposition_from_catch_to_default() {
274        let inner = VirtualSystem::new();
275        let system = Rc::new(Concurrent::new(inner.clone()));
276        system
277            .set_disposition(SIGQUIT, Disposition::Catch)
278            .now_or_never()
279            .unwrap()
280            .unwrap();
281        // When the disposition is set to `Catch`, the signal is blocked.
282        system.raise(SIGQUIT).now_or_never().unwrap().unwrap();
283
284        // Resetting the disposition to `Default` should unblock the signal,
285        // which should cause the process to be terminated.
286        let result = system
287            .set_disposition(SIGQUIT, Disposition::Default)
288            .now_or_never();
289        assert_eq!(result, None);
290        assert_eq!(
291            inner.current_process().state(),
292            ProcessState::Halted(ProcessResult::Signaled {
293                signal: SIGQUIT,
294                core_dump: true
295            })
296        );
297    }
298
299    #[test]
300    fn first_update_sigmask_and_select_mask_updates_blocking_mask() {
301        let inner = VirtualSystem::new();
302        _ = inner
303            .current_process_mut()
304            .block_signals(SigmaskOp::Set, [SIGQUIT, SIGTERM, SIGUSR1]);
305        let system = Rc::new(Concurrent::new(inner.clone()));
306
307        let result = system
308            .update_sigmask_and_select_mask(SigmaskOp::Add, SIGTERM)
309            .now_or_never()
310            .unwrap();
311        assert_eq!(result, Ok(()));
312        let blocked_signals = inner
313            .current_process()
314            .blocked_signals()
315            .iter()
316            .copied()
317            .collect::<Vec<_>>();
318        assert_eq!(blocked_signals, [SIGQUIT, SIGTERM, SIGUSR1]);
319    }
320
321    #[test]
322    fn first_update_sigmask_and_select_mask_sets_select_mask() {
323        let inner = VirtualSystem::new();
324        _ = inner
325            .current_process_mut()
326            .block_signals(SigmaskOp::Set, [SIGQUIT, SIGTERM, SIGUSR1]);
327        let system = Rc::new(Concurrent::new(inner.clone()));
328
329        system
330            .update_sigmask_and_select_mask(SigmaskOp::Add, SIGTERM)
331            .now_or_never()
332            .unwrap()
333            .unwrap();
334        assert_eq!(
335            system.state.borrow().select_mask,
336            Some(Sigset::from_iter([SIGQUIT, SIGUSR1]))
337        );
338    }
339
340    #[ignore = "current VirtualSystem::sigmask silently ignores invalid signals"]
341    #[test]
342    fn first_update_sigmask_and_select_mask_leaves_select_mask_unchanged_on_error() {
343        let system = Rc::new(Concurrent::new(VirtualSystem::new()));
344        let invalid_signal = Number::from_raw_unchecked(NonZero::new(-1).unwrap());
345        let result = system
346            .update_sigmask_and_select_mask(SigmaskOp::Add, invalid_signal)
347            .now_or_never()
348            .unwrap();
349        assert_eq!(result, Err(Errno::EINVAL));
350        assert_eq!(system.state.borrow().select_mask, None);
351    }
352
353    #[test]
354    fn second_update_sigmask_and_select_mask_updates_select_mask() {
355        let inner = VirtualSystem::new();
356        _ = inner
357            .current_process_mut()
358            .block_signals(SigmaskOp::Set, [SIGQUIT, SIGTERM, SIGUSR1]);
359        let system = Rc::new(Concurrent::new(inner.clone()));
360
361        system
362            .update_sigmask_and_select_mask(SigmaskOp::Add, SIGTERM)
363            .now_or_never()
364            .unwrap()
365            .unwrap();
366        system
367            .update_sigmask_and_select_mask(SigmaskOp::Remove, SIGQUIT)
368            .now_or_never()
369            .unwrap()
370            .unwrap();
371        assert_eq!(
372            system.state.borrow().select_mask,
373            Some(Sigset::from(SIGUSR1))
374        );
375    }
376}