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::signal::Number;
21use crate::system::{Disposition, Errno, Sigaction, Sigmask, SigmaskOp};
22use crate::trap::SignalSystem;
23use std::rc::Rc;
24
25/// Implementation of `SignalSystem` for `Concurrent`
26///
27/// `Concurrent` controls both the signal dispositions and the signal mask, so
28/// it can receive and handle signals without race conditions.
29impl<S> SignalSystem for Rc<Concurrent<S>>
30where
31    S: Sigmask + Sigaction,
32{
33    /// Returns the current disposition of the specified signal.
34    ///
35    /// This implementation simply forwards the call to the inner system's
36    /// [`GetSigaction::get_sigaction`](crate::system::GetSigaction::get_sigaction)
37    /// method.
38    fn get_disposition(&self, signal: Number) -> Result<Disposition, Errno> {
39        self.inner.get_sigaction(signal)
40    }
41
42    /// Sets the disposition of the specified signal to the given value and
43    /// returns the old disposition.
44    ///
45    /// This implementation both updates the signal disposition and the signal
46    /// mask to ensure that the [`select`](Concurrent::select) method can
47    /// respond to received signals without race conditions. Specifically:
48    ///
49    /// - When setting the disposition to `Default` or `Ignore`, the signal is
50    ///   unblocked to allow it to be delivered as soon as possible.
51    /// - When setting the disposition to `Catch`, the signal is blocked so that
52    ///   it is only delivered inside the `select` method.
53    fn set_disposition(
54        &self,
55        signal: Number,
56        disposition: Disposition,
57    ) -> impl Future<Output = Result<Disposition, Errno>> + use<S> {
58        let this = Rc::clone(self);
59        async move {
60            if disposition == Disposition::Catch {
61                // Before setting the disposition to `Catch`, we need to block the signal
62                // to prevent it from being delivered before the disposition is updated.
63                this.update_sigmask_and_select_mask(SigmaskOp::Add, signal)
64                    .await?;
65            }
66
67            let old_action = this.inner.sigaction(signal, disposition)?;
68
69            if disposition != Disposition::Catch {
70                // After setting the disposition to `Default` or `Ignore`, we need to unblock
71                // the signal to allow it to be delivered if it was previously blocked.
72                this.update_sigmask_and_select_mask(SigmaskOp::Remove, signal)
73                    .await?;
74            }
75
76            Ok(old_action)
77        }
78    }
79}
80
81impl<S> Concurrent<S>
82where
83    S: Sigmask,
84{
85    /// Wrapper of the inner system's [`Sigmask::sigmask`] method that also
86    /// updates the `select_mask` field.
87    async fn update_sigmask_and_select_mask(
88        &self,
89        op: SigmaskOp,
90        signal: Number,
91    ) -> Result<(), Errno> {
92        let mut old_mask = Vec::new();
93        self.inner
94            .sigmask(Some((op, &[signal])), Some(&mut old_mask))
95            .await?;
96
97        self.state
98            .borrow_mut()
99            .select_mask
100            .get_or_insert(old_mask)
101            .retain(|&s| s != signal);
102        Ok(())
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use crate::job::{ProcessResult, ProcessState};
110    use crate::system::SendSignal as _;
111    use crate::system::r#virtual::{SIGQUIT, SIGTERM, SIGUSR1, VirtualSystem};
112    use futures_util::FutureExt as _;
113    use std::num::NonZero;
114
115    #[test]
116    fn setting_disposition_from_default_to_catch() {
117        let inner = VirtualSystem::new();
118        let system = Rc::new(Concurrent::new(inner.clone()));
119        let result = system
120            .set_disposition(SIGTERM, Disposition::Catch)
121            .now_or_never()
122            .unwrap();
123        assert_eq!(result, Ok(Disposition::Default));
124        assert_eq!(system.get_disposition(SIGTERM), Ok(Disposition::Catch));
125
126        // When the disposition is set to `Catch`, the signal is blocked.
127        inner.raise(SIGTERM).now_or_never().unwrap().unwrap();
128        // So the process should still be running.
129        assert_eq!(inner.current_process().state(), ProcessState::Running);
130    }
131
132    #[test]
133    fn setting_disposition_from_default_to_ignore() {
134        let inner = VirtualSystem::new();
135        let system = Rc::new(Concurrent::new(inner.clone()));
136        let result = system
137            .set_disposition(SIGTERM, Disposition::Ignore)
138            .now_or_never()
139            .unwrap();
140        assert_eq!(result, Ok(Disposition::Default));
141        assert_eq!(system.get_disposition(SIGTERM), Ok(Disposition::Ignore));
142
143        // Since the signal is ignored, sending it should have no effect.
144        inner.raise(SIGTERM).now_or_never().unwrap().unwrap();
145        assert_eq!(inner.current_process().state(), ProcessState::Running);
146    }
147
148    #[test]
149    fn setting_disposition_from_ignore_to_catch() {
150        let system = Rc::new(Concurrent::new(VirtualSystem::new()));
151        system
152            .set_disposition(SIGQUIT, Disposition::Ignore)
153            .now_or_never()
154            .unwrap()
155            .unwrap();
156
157        let result = system
158            .set_disposition(SIGQUIT, Disposition::Catch)
159            .now_or_never()
160            .unwrap();
161        assert_eq!(result, Ok(Disposition::Ignore));
162        assert_eq!(system.get_disposition(SIGQUIT), Ok(Disposition::Catch));
163    }
164
165    #[test]
166    fn setting_disposition_from_catch_to_default() {
167        let inner = VirtualSystem::new();
168        let system = Rc::new(Concurrent::new(inner.clone()));
169        system
170            .set_disposition(SIGQUIT, Disposition::Catch)
171            .now_or_never()
172            .unwrap()
173            .unwrap();
174        // When the disposition is set to `Catch`, the signal is blocked.
175        system.raise(SIGQUIT).now_or_never().unwrap().unwrap();
176
177        // Resetting the disposition to `Default` should unblock the signal,
178        // which should cause the process to be terminated.
179        let result = system
180            .set_disposition(SIGQUIT, Disposition::Default)
181            .now_or_never();
182        assert_eq!(result, None);
183        assert_eq!(
184            inner.current_process().state(),
185            ProcessState::Halted(ProcessResult::Signaled {
186                signal: SIGQUIT,
187                core_dump: true
188            })
189        );
190    }
191
192    #[test]
193    fn first_update_sigmask_and_select_mask_updates_blocking_mask() {
194        let inner = VirtualSystem::new();
195        _ = inner
196            .current_process_mut()
197            .block_signals(SigmaskOp::Set, &[SIGQUIT, SIGTERM, SIGUSR1]);
198        let system = Rc::new(Concurrent::new(inner.clone()));
199
200        let result = system
201            .update_sigmask_and_select_mask(SigmaskOp::Add, SIGTERM)
202            .now_or_never()
203            .unwrap();
204        assert_eq!(result, Ok(()));
205        let blocked_signals = inner
206            .current_process()
207            .blocked_signals()
208            .iter()
209            .copied()
210            .collect::<Vec<_>>();
211        assert_eq!(blocked_signals, [SIGQUIT, SIGTERM, SIGUSR1]);
212    }
213
214    #[test]
215    fn first_update_sigmask_and_select_mask_sets_select_mask() {
216        let inner = VirtualSystem::new();
217        _ = inner
218            .current_process_mut()
219            .block_signals(SigmaskOp::Set, &[SIGQUIT, SIGTERM, SIGUSR1]);
220        let system = Rc::new(Concurrent::new(inner.clone()));
221
222        system
223            .update_sigmask_and_select_mask(SigmaskOp::Add, SIGTERM)
224            .now_or_never()
225            .unwrap()
226            .unwrap();
227        assert_eq!(
228            system.state.borrow().select_mask.as_deref(),
229            Some([SIGQUIT, SIGUSR1].as_slice())
230        );
231    }
232
233    #[ignore = "current VirtualSystem::sigmask silently ignores invalid signals"]
234    #[test]
235    fn first_update_sigmask_and_select_mask_leaves_select_mask_unchanged_on_error() {
236        let system = Rc::new(Concurrent::new(VirtualSystem::new()));
237        let invalid_signal = Number::from_raw_unchecked(NonZero::new(-1).unwrap());
238        let result = system
239            .update_sigmask_and_select_mask(SigmaskOp::Add, invalid_signal)
240            .now_or_never()
241            .unwrap();
242        assert_eq!(result, Err(Errno::EINVAL));
243        assert_eq!(system.state.borrow().select_mask.as_deref(), None);
244    }
245
246    #[test]
247    fn second_update_sigmask_and_select_mask_updates_select_mask() {
248        let inner = VirtualSystem::new();
249        _ = inner
250            .current_process_mut()
251            .block_signals(SigmaskOp::Set, &[SIGQUIT, SIGTERM, SIGUSR1]);
252        let system = Rc::new(Concurrent::new(inner.clone()));
253
254        system
255            .update_sigmask_and_select_mask(SigmaskOp::Add, SIGTERM)
256            .now_or_never()
257            .unwrap()
258            .unwrap();
259        system
260            .update_sigmask_and_select_mask(SigmaskOp::Remove, SIGQUIT)
261            .now_or_never()
262            .unwrap()
263            .unwrap();
264        assert_eq!(
265            system.state.borrow().select_mask.as_deref(),
266            Some([SIGUSR1].as_slice())
267        );
268    }
269}