Skip to main content

secure_exec_sidecar_core/
signals.rs

1use secure_exec_bridge::ExecutionSignal;
2use secure_exec_sidecar_protocol::protocol::{SignalDispositionAction, SignalHandlerRegistration};
3use serde_json::Value;
4use std::collections::BTreeMap;
5
6pub fn execution_signal_to_kernel(signal: ExecutionSignal) -> i32 {
7    match signal {
8        ExecutionSignal::Terminate => 15,
9        ExecutionSignal::Interrupt => 2,
10        ExecutionSignal::Kill => 9,
11    }
12}
13
14pub fn execution_signal_from_number(signal: i32) -> Option<ExecutionSignal> {
15    match signal {
16        2 => Some(ExecutionSignal::Interrupt),
17        9 => Some(ExecutionSignal::Kill),
18        15 => Some(ExecutionSignal::Terminate),
19        _ => None,
20    }
21}
22
23pub fn default_signal_exit_code(signal: i32) -> Option<i32> {
24    (signal > 0).then_some(128 + signal)
25}
26
27pub fn is_valid_posix_signal_number(signal: u32) -> bool {
28    signal <= 31
29}
30
31pub fn parse_posix_signal(signal: &str) -> Option<i32> {
32    let trimmed = signal.trim();
33    if trimmed.is_empty() {
34        return None;
35    }
36
37    if let Ok(value) = trimmed.parse::<i32>() {
38        return (0..=31).contains(&value).then_some(value);
39    }
40
41    let upper = trimmed.to_ascii_uppercase();
42    let normalized = upper.strip_prefix("SIG").unwrap_or(&upper);
43    signal_number_from_name(normalized)
44}
45
46pub fn canonical_signal_name(signal: i32) -> Option<&'static str> {
47    match signal {
48        1 => Some("SIGHUP"),
49        2 => Some("SIGINT"),
50        3 => Some("SIGQUIT"),
51        4 => Some("SIGILL"),
52        5 => Some("SIGTRAP"),
53        6 => Some("SIGABRT"),
54        7 => Some("SIGBUS"),
55        8 => Some("SIGFPE"),
56        9 => Some("SIGKILL"),
57        10 => Some("SIGUSR1"),
58        11 => Some("SIGSEGV"),
59        12 => Some("SIGUSR2"),
60        13 => Some("SIGPIPE"),
61        14 => Some("SIGALRM"),
62        15 => Some("SIGTERM"),
63        16 => Some("SIGSTKFLT"),
64        17 => Some("SIGCHLD"),
65        18 => Some("SIGCONT"),
66        19 => Some("SIGSTOP"),
67        20 => Some("SIGTSTP"),
68        21 => Some("SIGTTIN"),
69        22 => Some("SIGTTOU"),
70        23 => Some("SIGURG"),
71        24 => Some("SIGXCPU"),
72        25 => Some("SIGXFSZ"),
73        26 => Some("SIGVTALRM"),
74        27 => Some("SIGPROF"),
75        28 => Some("SIGWINCH"),
76        29 => Some("SIGIO"),
77        30 => Some("SIGPWR"),
78        31 => Some("SIGSYS"),
79        _ => None,
80    }
81}
82
83pub fn signal_number_from_name(signal: &str) -> Option<i32> {
84    match signal {
85        "0" => Some(0),
86        "HUP" => Some(1),
87        "INT" => Some(2),
88        "QUIT" => Some(3),
89        "ILL" => Some(4),
90        "TRAP" => Some(5),
91        "ABRT" | "IOT" => Some(6),
92        "BUS" => Some(7),
93        "FPE" => Some(8),
94        "KILL" => Some(9),
95        "USR1" => Some(10),
96        "SEGV" => Some(11),
97        "USR2" => Some(12),
98        "PIPE" => Some(13),
99        "ALRM" => Some(14),
100        "TERM" => Some(15),
101        "STKFLT" => Some(16),
102        "CHLD" => Some(17),
103        "CONT" => Some(18),
104        "STOP" => Some(19),
105        "TSTP" => Some(20),
106        "TTIN" => Some(21),
107        "TTOU" => Some(22),
108        "URG" => Some(23),
109        "XCPU" => Some(24),
110        "XFSZ" => Some(25),
111        "VTALRM" => Some(26),
112        "PROF" => Some(27),
113        "WINCH" => Some(28),
114        "IO" | "POLL" => Some(29),
115        "PWR" => Some(30),
116        "SYS" => Some(31),
117        _ => None,
118    }
119}
120
121pub fn parse_process_signal_state_request(
122    args: &[Value],
123) -> Result<(u32, SignalHandlerRegistration), crate::SidecarCoreError> {
124    let signal = signal_state_u32_arg(args, 0, "process.signal_state signal")?;
125    validate_process_signal_number(signal, "process.signal_state signal")?;
126    let action = signal_state_str_arg(args, 1, "process.signal_state action")?;
127    let mask_json = signal_state_str_arg(args, 2, "process.signal_state mask")?;
128    let flags = signal_state_u32_arg(args, 3, "process.signal_state flags")?;
129    let mask: Vec<u32> = serde_json::from_str(mask_json).map_err(|error| {
130        crate::SidecarCoreError::new(format!(
131            "process.signal_state mask must be valid JSON: {error}"
132        ))
133    })?;
134    for signal in &mask {
135        validate_process_signal_number(*signal, "process.signal_state mask entries")?;
136    }
137    let action = match action.trim().to_ascii_lowercase().as_str() {
138        "default" => SignalDispositionAction::Default,
139        "ignore" => SignalDispositionAction::Ignore,
140        "user" => SignalDispositionAction::User,
141        other => {
142            return Err(crate::SidecarCoreError::new(format!(
143                "unsupported process.signal_state action {other}"
144            )));
145        }
146    };
147
148    Ok((
149        signal,
150        SignalHandlerRegistration {
151            action,
152            mask,
153            flags,
154        },
155    ))
156}
157
158pub fn apply_process_signal_state_update(
159    signal_states: &mut BTreeMap<String, BTreeMap<u32, SignalHandlerRegistration>>,
160    process_id: &str,
161    signal: u32,
162    registration: SignalHandlerRegistration,
163) {
164    if registration.action == SignalDispositionAction::Default
165        && registration.mask.is_empty()
166        && registration.flags == 0
167    {
168        let remove_process_entry = signal_states
169            .get_mut(process_id)
170            .map(|handlers| {
171                handlers.remove(&signal);
172                handlers.is_empty()
173            })
174            .unwrap_or(false);
175        if remove_process_entry {
176            signal_states.remove(process_id);
177        }
178        return;
179    }
180
181    signal_states
182        .entry(process_id.to_owned())
183        .or_default()
184        .insert(signal, registration);
185}
186
187fn validate_process_signal_number(signal: u32, label: &str) -> Result<(), crate::SidecarCoreError> {
188    if is_valid_posix_signal_number(signal) {
189        Ok(())
190    } else {
191        Err(crate::SidecarCoreError::new(format!(
192            "{label} must be a valid POSIX signal"
193        )))
194    }
195}
196
197fn signal_state_u32_arg(
198    args: &[Value],
199    index: usize,
200    label: &str,
201) -> Result<u32, crate::SidecarCoreError> {
202    let value = args
203        .get(index)
204        .ok_or_else(|| crate::SidecarCoreError::new(format!("{label} missing")))?;
205    if let Some(value) = value.as_u64() {
206        return u32::try_from(value)
207            .map_err(|_| crate::SidecarCoreError::new(format!("{label} must fit in u32")));
208    }
209    if let Some(value) = value.as_i64() {
210        return u32::try_from(value)
211            .map_err(|_| crate::SidecarCoreError::new(format!("{label} must fit in u32")));
212    }
213    if let Some(value) = value.as_str() {
214        return value
215            .parse::<u32>()
216            .map_err(|error| crate::SidecarCoreError::new(format!("{label}: {error}")));
217    }
218    Err(crate::SidecarCoreError::new(format!(
219        "{label} must be a u32"
220    )))
221}
222
223fn signal_state_str_arg<'a>(
224    args: &'a [Value],
225    index: usize,
226    label: &str,
227) -> Result<&'a str, crate::SidecarCoreError> {
228    args.get(index)
229        .and_then(Value::as_str)
230        .ok_or_else(|| crate::SidecarCoreError::new(format!("{label} must be a string")))
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236
237    #[test]
238    fn execution_signal_mapping_matches_posix_defaults() {
239        assert_eq!(execution_signal_to_kernel(ExecutionSignal::Interrupt), 2);
240        assert_eq!(execution_signal_to_kernel(ExecutionSignal::Kill), 9);
241        assert_eq!(execution_signal_to_kernel(ExecutionSignal::Terminate), 15);
242        assert_eq!(
243            execution_signal_from_number(2),
244            Some(ExecutionSignal::Interrupt)
245        );
246        assert_eq!(execution_signal_from_number(9), Some(ExecutionSignal::Kill));
247        assert_eq!(
248            execution_signal_from_number(15),
249            Some(ExecutionSignal::Terminate)
250        );
251        assert_eq!(execution_signal_from_number(10), None);
252    }
253
254    #[test]
255    fn default_signal_exit_code_is_128_plus_signal() {
256        assert_eq!(default_signal_exit_code(2), Some(130));
257        assert_eq!(default_signal_exit_code(9), Some(137));
258        assert_eq!(default_signal_exit_code(15), Some(143));
259        assert_eq!(default_signal_exit_code(0), None);
260    }
261
262    #[test]
263    fn validates_posix_signal_number_range() {
264        assert!(is_valid_posix_signal_number(0));
265        assert!(is_valid_posix_signal_number(31));
266        assert!(!is_valid_posix_signal_number(32));
267    }
268
269    #[test]
270    fn parses_signal_names_and_numbers() {
271        assert_eq!(parse_posix_signal("SIGTERM"), Some(15));
272        assert_eq!(parse_posix_signal("term"), Some(15));
273        assert_eq!(canonical_signal_name(16), Some("SIGSTKFLT"));
274        assert_eq!(parse_posix_signal("9"), Some(9));
275        assert_eq!(parse_posix_signal("0"), Some(0));
276        assert_eq!(parse_posix_signal("SIGBOGUS"), None);
277        assert_eq!(parse_posix_signal("32"), None);
278    }
279
280    #[test]
281    fn parses_and_applies_process_signal_state_updates() {
282        let args = vec![
283            Value::from(15),
284            Value::from("user"),
285            Value::from("[2]"),
286            Value::from(0),
287        ];
288        let (signal, registration) =
289            parse_process_signal_state_request(&args).expect("signal state");
290        assert_eq!(signal, 15);
291        assert_eq!(registration.action, SignalDispositionAction::User);
292        assert_eq!(registration.mask, vec![2]);
293
294        let mut states = BTreeMap::new();
295        apply_process_signal_state_update(&mut states, "proc-1", signal, registration);
296        assert!(states
297            .get("proc-1")
298            .is_some_and(|handlers| handlers.contains_key(&15)));
299
300        apply_process_signal_state_update(
301            &mut states,
302            "proc-1",
303            15,
304            SignalHandlerRegistration {
305                action: SignalDispositionAction::Default,
306                mask: Vec::new(),
307                flags: 0,
308            },
309        );
310        assert!(!states.contains_key("proc-1"));
311    }
312
313    #[test]
314    fn rejects_unknown_process_signal_state_values() {
315        let invalid_signal = parse_process_signal_state_request(&[
316            Value::from(32),
317            Value::from("user"),
318            Value::from("[]"),
319            Value::from(0),
320        ])
321        .expect_err("unknown signal must fail");
322        assert_eq!(
323            invalid_signal.to_string(),
324            "process.signal_state signal must be a valid POSIX signal"
325        );
326
327        let invalid_mask = parse_process_signal_state_request(&[
328            Value::from(15),
329            Value::from("user"),
330            Value::from("[32]"),
331            Value::from(0),
332        ])
333        .expect_err("unknown mask signal must fail");
334        assert_eq!(
335            invalid_mask.to_string(),
336            "process.signal_state mask entries must be a valid POSIX signal"
337        );
338    }
339}