shadow_rs/utility/
shm_cleanup.rs

1use std::collections::HashSet;
2use std::fs;
3use std::path::Path;
4use std::path::PathBuf;
5use std::str::FromStr;
6
7use anyhow::Context;
8
9pub const SHM_DIR_PATH: &str = "/dev/shm/";
10const PROC_DIR_PATH: &str = "/proc/";
11const SHADOW_SHM_FILE_PREFIX: &str = "shadow_shmemfile";
12
13// Get the paths from the given directory path.
14fn get_dir_contents(dir: &Path) -> anyhow::Result<Vec<PathBuf>> {
15    fs::read_dir(dir)
16        .context(format!("Reading all directory entries from {:?}", dir))?
17        .map(|entry| {
18            Ok(entry
19                .context(format!("Reading a directory entry from {:?}", dir))?
20                .path())
21        })
22        .collect()
23}
24
25// Parse files in dir_path and return the paths to the shm files created by Shadow.
26fn get_shadow_shm_file_paths(dir_path: &Path) -> anyhow::Result<Vec<PathBuf>> {
27    let vec = get_dir_contents(dir_path)?
28        .into_iter()
29        .filter_map(|path| match path.file_name() {
30            Some(name) => name
31                .to_string_lossy()
32                .starts_with(SHADOW_SHM_FILE_PREFIX)
33                .then_some(Some(path)),
34            None => None, // ignore paths ending in '..'
35        })
36        .flatten()
37        .collect();
38    Ok(vec)
39}
40
41// Parse PIDs from entries in dir_path.
42fn get_running_pid_set(dir_path: &Path) -> anyhow::Result<HashSet<i32>> {
43    let set: HashSet<i32> = get_dir_contents(dir_path)?
44        .into_iter()
45        .filter_map(|path| match path.file_name() {
46            // ignore names that don't parse into PIDs
47            Some(name) => i32::from_str(&name.to_string_lossy()).ok(),
48            None => None, // ignore paths ending in '..'
49        })
50        .collect();
51    Ok(set)
52}
53
54// Parse the PID that is encoded in the Shadow shmem file name. The PID is the
55// part after the '-', e.g., 2738869 in the example file name:
56// `shadow_shmemfile_6379761.950298775-2738869`
57fn pid_from_shadow_shm_file_name(file_name: &str) -> anyhow::Result<i32> {
58    let pid_str = file_name.split('-').next_back().context(format!(
59        "Parsing PID separator '-' from shm file name {:?}",
60        file_name
61    ))?;
62    let pid = i32::from_str(pid_str).context(format!(
63        "Parsing PID '{}' from shm file name {:?}",
64        pid_str, file_name
65    ))?;
66    Ok(pid)
67}
68
69// Cleans up orphaned shared memory files that are no longer mapped by a shadow
70// process. This function should never fail or crash, but is not guaranteed to
71// reclaim all possible orphans. Returns the number of orphans removed.
72pub fn shm_cleanup(shm_dir: impl AsRef<Path>) -> anyhow::Result<u32> {
73    // Get the shm file paths before the PIDs to avoid a race condition (#1343).
74    let shm_paths = get_shadow_shm_file_paths(shm_dir.as_ref())?;
75    log::debug!(
76        "Found {} shadow shared memory files in {}",
77        shm_paths.len(),
78        shm_dir.as_ref().display()
79    );
80
81    let running_pids = get_running_pid_set(Path::new(PROC_DIR_PATH))?;
82    log::debug!(
83        "Found {} running PIDs in {}",
84        running_pids.len(),
85        PROC_DIR_PATH
86    );
87
88    // Count how many files we remove.
89    let mut num_removed = 0;
90
91    // Best effort: ignore failures on individual paths so we can try them all.
92    for path in shm_paths {
93        // Ignore paths ending in '..'
94        if let Some(file_name) = path.file_name() {
95            let creator_pid = match pid_from_shadow_shm_file_name(&file_name.to_string_lossy()) {
96                Ok(pid) => pid,
97                Err(e) => {
98                    log::warn!(
99                        "Unable to parse PID from shared memory file {:?}: {:?}",
100                        path,
101                        e
102                    );
103                    // Keep going to try the rest of the paths we found.
104                    continue;
105                }
106            };
107
108            // Do not remove the file if it's owner process is still running.
109            if !running_pids.contains(&creator_pid) {
110                log::trace!("Removing orphaned shared memory file {:?}", path);
111                if fs::remove_file(path).is_ok() {
112                    num_removed += 1;
113                }
114            }
115        }
116    }
117
118    log::debug!("Removed {} total shared memory files.", num_removed);
119    Ok(num_removed)
120}
121
122#[cfg(test)]
123mod tests {
124    use std::fs::OpenOptions;
125    use std::io;
126    use std::process;
127
128    use super::*;
129
130    fn touch(path: impl AsRef<Path>) -> io::Result<()> {
131        OpenOptions::new()
132            .create(true)
133            .truncate(false)
134            .write(true)
135            .open(path.as_ref())?;
136        Ok(())
137    }
138
139    #[test]
140    fn test_expired_shm_file_is_removed() {
141        let dir = tempfile::tempdir().unwrap();
142        let s = "shadow_shmemfile_6379761.950298775-999999999";
143        let expired: PathBuf = [dir.as_ref(), s.as_ref()].iter().collect();
144
145        touch(&expired).unwrap();
146        assert_eq!(shm_cleanup(&dir).unwrap(), 1);
147        assert!(!expired.exists(), "Exists: {}", expired.display());
148    }
149
150    #[test]
151    fn test_valid_shm_file_is_not_removed() {
152        let my_pid = process::id();
153        let dir = tempfile::tempdir().unwrap();
154        let s = format!("shadow_shmemfile_6379761.950298775-{my_pid}");
155        let valid: PathBuf = [dir.as_ref(), s.as_ref()].iter().collect();
156
157        touch(&valid).unwrap();
158        assert_eq!(shm_cleanup(&dir).unwrap(), 0);
159        assert!(valid.exists(), "Doesn't exist: {}", valid.display());
160    }
161
162    #[test]
163    fn test_nonshadow_shm_file_is_not_removed() {
164        let dir = tempfile::tempdir().unwrap();
165        let s = "shadow_unimportant_test_file";
166        let nonshadow: PathBuf = [dir.as_ref(), s.as_ref()].iter().collect();
167
168        touch(&nonshadow).unwrap();
169        assert_eq!(shm_cleanup(&dir).unwrap(), 0);
170        assert!(nonshadow.exists(), "Doesn't exist: {}", nonshadow.display());
171    }
172}