Skip to main content

nitro_cli/enclave_proc/
socket.rs

1// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// SPDX-License-Identifier: Apache-2.0
3#![deny(missing_docs)]
4#![deny(warnings)]
5
6use inotify::{EventMask, Inotify, WatchMask};
7use log::{debug, warn};
8use std::path::{Path, PathBuf};
9use std::sync::atomic::{AtomicBool, Ordering};
10use std::sync::Arc;
11use std::thread::{self, JoinHandle};
12
13use crate::common::get_socket_path;
14use crate::common::{ExitGracefully, NitroCliErrorEnum, NitroCliFailure, NitroCliResult};
15use crate::new_nitro_cli_failure;
16
17/// The structure which manages the Unix socket that an enclave process listens on for commands.
18#[derive(Default)]
19pub struct EnclaveProcSock {
20    /// The socket's file-system path.
21    socket_path: PathBuf,
22    /// The thread which listens for external events which delete the socket from the file-system.
23    remove_listener_thread: Option<JoinHandle<()>>,
24    /// A flag indicating if socket removal was requested.
25    requested_remove: Arc<AtomicBool>,
26}
27
28/// The enclave process socket must allow cloning, since that is needed by the socket-listening thread.
29impl Clone for EnclaveProcSock {
30    fn clone(&self) -> Self {
31        // Actually clone only what's relevant for the listening thread.
32        EnclaveProcSock {
33            socket_path: self.socket_path.clone(),
34            remove_listener_thread: None,
35            requested_remove: self.requested_remove.clone(),
36        }
37    }
38}
39
40impl Drop for EnclaveProcSock {
41    fn drop(&mut self) {
42        self.close_mut()
43            .ok_or_exit_with_errno(Some("Failed to drop socket"));
44    }
45}
46
47impl EnclaveProcSock {
48    /// Create a new `EnclaveProcSock` instance from a given enclave ID.
49    pub fn new(enclave_id: &str) -> NitroCliResult<Self> {
50        let socket_path = get_socket_path(enclave_id).map_err(|_| {
51            new_nitro_cli_failure!(
52                "Failed to create enclave process socket",
53                NitroCliErrorEnum::SocketPathNotFound
54            )
55        })?;
56
57        Ok(EnclaveProcSock {
58            socket_path,
59            remove_listener_thread: None,
60            requested_remove: Arc::new(AtomicBool::new(false)),
61        })
62    }
63
64    /// Get the path to the managed Unix socket.
65    pub fn get_path(&self) -> &Path {
66        self.socket_path.as_path()
67    }
68
69    /// Set the path of the managed Unix socket.
70    pub fn set_path(&mut self, socket_path: PathBuf) {
71        self.socket_path = socket_path;
72    }
73
74    /// Start monitoring the Unix socket's state using `inotify`.
75    pub fn start_monitoring(&mut self, exit_on_delete: bool) -> NitroCliResult<()> {
76        let path_clone = self.socket_path.clone();
77        let requested_remove_clone = self.requested_remove.clone();
78        let socket_inotify = Inotify::init().map_err(|e| {
79            new_nitro_cli_failure!(
80                &format!("Failed to initialize socket notifications: {e:?}"),
81                NitroCliErrorEnum::InotifyError
82            )
83        })?;
84
85        // Relevant events to listen for are:
86        // - IN_DELETE_SELF: triggered when the socket file inode gets removed.
87        // - IN_ATTRIB: triggered when the reference count of the file inode changes.
88        socket_inotify
89            .watches()
90            .add(
91                self.socket_path.as_path(),
92                WatchMask::ATTRIB | WatchMask::DELETE_SELF,
93            )
94            .map_err(|e| {
95                new_nitro_cli_failure!(
96                    &format!("Failed to add watch to inotify: {e:?}"),
97                    NitroCliErrorEnum::InotifyError
98                )
99            })?;
100        self.remove_listener_thread = Some(thread::spawn(move || {
101            socket_removal_listener(
102                path_clone,
103                requested_remove_clone,
104                socket_inotify,
105                exit_on_delete,
106            )
107        }));
108        Ok(())
109    }
110
111    /// Remove the managed Unix socket and clean up after it. This is called with a mutable self-reference.
112    fn close_mut(&mut self) -> NitroCliResult<()> {
113        // Delete the socket from the disk. Also mark that this operation is intended, so that the
114        // socket file monitoring thread doesn't exit forcefully when notifying the deletion.
115        self.requested_remove.store(true, Ordering::SeqCst);
116        if self.socket_path.exists() {
117            std::fs::remove_file(&self.socket_path).map_err(|e| {
118                new_nitro_cli_failure!(
119                    &format!(
120                        "Failed to remove socket file {:?} from disk: {:?}",
121                        self.socket_path, e
122                    ),
123                    NitroCliErrorEnum::FileOperationFailure
124                )
125                .add_info(vec![
126                    self.socket_path
127                        .to_str()
128                        .unwrap_or("Invalid unicode socket file name"),
129                    "Remove",
130                ])
131            })?;
132        }
133
134        // Since the socket file has been deleted, we also wait for the event listener thread to finish.
135        if self.remove_listener_thread.is_some() {
136            self.remove_listener_thread
137                .take()
138                .unwrap()
139                .join()
140                .map_err(|e| {
141                    new_nitro_cli_failure!(
142                        &format!("Failed to join socket notification thread: {e:?}"),
143                        NitroCliErrorEnum::ThreadJoinFailure
144                    )
145                })?;
146        }
147
148        Ok(())
149    }
150
151    /// Remove the managed Unix socket and clean up after it.
152    pub fn close(mut self) -> NitroCliResult<()> {
153        self.close_mut()
154            .map_err(|e| e.add_subaction("Close socket".to_string()))
155    }
156}
157
158/// Listen for an `inotify` event when the socket gets deleted from the disk.
159fn socket_removal_listener(
160    socket_path: PathBuf,
161    requested_remove: Arc<AtomicBool>,
162    mut socket_inotify: Inotify,
163    exit_on_delete: bool,
164) {
165    let mut buffer = [0u8; 4096];
166    let mut done = false;
167
168    debug!("Socket file event listener started for {:?}.", socket_path);
169
170    while !done {
171        // Read events.
172        let events = socket_inotify
173            .read_events_blocking(&mut buffer)
174            .map_err(|e| {
175                new_nitro_cli_failure!(
176                    &format!("Socket removal listener error: {e:?}"),
177                    NitroCliErrorEnum::InotifyError
178                )
179                .set_action("Run Enclave".to_string())
180            })
181            .ok_or_exit_with_errno(Some("Failed to read inotify events"));
182
183        for event in events {
184            // We monitor the DELETE_SELF event, which occurs when the inode is no longer referenced by anybody. We
185            // also monitor the IN_ATTRIB event, which gets triggered whenever the inode reference count changes. To
186            // make sure this is a deletion, we also verify if the socket file is still present in the file-system.
187            if (event.mask.contains(EventMask::ATTRIB)
188                || event.mask.contains(EventMask::DELETE_SELF))
189                && !socket_path.exists()
190            {
191                if requested_remove.load(Ordering::SeqCst) {
192                    // At this point, the socket is shutting itself down and has notified the
193                    // monitoring thread, so we just exit the loop gracefully.
194                    debug!("The enclave process socket has deleted itself.");
195                    done = true;
196                } else {
197                    // At this point, the socket has been deleted by an external action, so
198                    // we exit forcefully, since there is no longer any way for a CLI instance
199                    // to tell the current enclave process to terminate.
200                    warn!("The enclave process socket has been deleted!");
201                    if exit_on_delete {
202                        std::process::exit(1);
203                    }
204                    done = true;
205                }
206            }
207        }
208    }
209
210    debug!("Enclave process socket monitoring is done.");
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216
217    use std::os::unix::net::UnixListener;
218    use std::process::Command;
219
220    const DUMMY_ENCLAVE_ID: &str = "i-0000000000000000-enc0123456789012345";
221    const THREADS_STR: &str = "Threads:";
222    const WAIT_REMOVE_MILLIS: u64 = 10;
223
224    /// Inspects the content of /proc/<PID>/status in order to
225    /// retrieve the number of threads running in the context of
226    /// process <PID>.
227    fn get_num_threads_from_status_output(status_str: String) -> u32 {
228        let start_idx = status_str.find(THREADS_STR);
229        let mut iter = status_str.chars();
230        iter.by_ref().nth(start_idx.unwrap() + THREADS_STR.len()); // skip "Threads:\t"
231        let slice = iter.as_str();
232
233        let new_str = slice.to_string();
234        let end_idx = new_str.find('\n'); // skip after the first '\n'
235        let substr = &slice[..end_idx.unwrap()];
236
237        substr.parse().unwrap()
238    }
239
240    /// Tests that the initial values of the EnclaveProcSock attributes match the
241    /// expected ones.
242    #[test]
243    fn test_enclaveprocsock_init() {
244        let socket = EnclaveProcSock::new(DUMMY_ENCLAVE_ID);
245
246        assert!(socket.is_ok());
247
248        if let Ok(socket) = socket {
249            assert!(socket
250                .socket_path
251                .as_path()
252                .to_str()
253                .unwrap()
254                .contains("0123456789012345"));
255            assert!(socket.remove_listener_thread.is_none());
256            assert!(!socket.requested_remove.load(Ordering::SeqCst));
257        }
258    }
259
260    /// Tests that removing the socket file by means other than `close()` does not
261    /// trigger a `socket.requested_remove` change.
262    #[test]
263    fn test_start_monitoring() {
264        let socket = EnclaveProcSock::new(DUMMY_ENCLAVE_ID);
265
266        assert!(socket.is_ok());
267
268        if let Ok(mut socket) = socket {
269            UnixListener::bind(socket.get_path())
270                .map_err(|e| {
271                    new_nitro_cli_failure!(
272                        &format!("Failed to bind to socket: {e:?}"),
273                        NitroCliErrorEnum::SocketError
274                    )
275                })
276                .ok_or_exit_with_errno(Some("Error binding"));
277            let result = socket.start_monitoring(false);
278
279            assert!(result.is_ok());
280
281            // Remove socket file and expect `socket.requested_remove` to remain False
282            std::fs::remove_file(&socket.socket_path).unwrap();
283            std::thread::sleep(std::time::Duration::from_millis(WAIT_REMOVE_MILLIS));
284
285            assert!(!socket.requested_remove.load(Ordering::SeqCst));
286        }
287    }
288
289    /// Test that calling `close()` changes `socket.requested_remove` to True and
290    /// that the listener thread joins.
291    #[test]
292    fn test_close() {
293        let socket = EnclaveProcSock::new(DUMMY_ENCLAVE_ID);
294
295        assert!(socket.is_ok());
296
297        // Get number of running threads before spawning the socket removal listener thread
298        let out_cmd0 = Command::new("cat")
299            .arg(format!("/proc/{}/status", std::process::id()))
300            .output()
301            .expect("Failed to run cat");
302        let out0 = std::str::from_utf8(&out_cmd0.stdout).unwrap();
303        let crt_num_threads0 = get_num_threads_from_status_output(out0.to_string());
304
305        if let Ok(mut socket) = socket {
306            let _ = UnixListener::bind(socket.get_path())
307                .map_err(|e| {
308                    new_nitro_cli_failure!(
309                        &format!("Failed to bind to socket: {e:?}"),
310                        NitroCliErrorEnum::SocketError
311                    )
312                })
313                .ok_or_exit_with_errno(Some("Error binding"));
314            let result = socket.start_monitoring(true);
315
316            assert!(result.is_ok());
317
318            // Call `close_mut()` and expect `socket.requested_remove` to change to True
319            let result = socket.close_mut();
320
321            assert!(result.is_ok());
322            assert!(socket.requested_remove.load(Ordering::SeqCst));
323        }
324
325        // Get number of running threads after closing the socket removal listener thread
326        let out_cmd1 = Command::new("cat")
327            .arg(format!("/proc/{}/status", std::process::id()))
328            .output()
329            .expect("Failed to run cat");
330        let out1 = std::str::from_utf8(&out_cmd1.stdout).unwrap();
331        let crt_num_threads1 = get_num_threads_from_status_output(out1.to_string());
332
333        // Check that the number of threads remains the same before and after running the test
334        assert_eq!(crt_num_threads0, crt_num_threads1);
335    }
336}