secure_exec_sidecar_core/
signals.rs1use 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}