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