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 {file_name:?}",
60    ))?;
61    let pid = i32::from_str(pid_str).context(format!(
62        "Parsing PID '{pid_str}' from shm file name {file_name:?}",
63    ))?;
64    Ok(pid)
65}
66
67// Cleans up orphaned shared memory files that are no longer mapped by a shadow
68// process. This function should never fail or crash, but is not guaranteed to
69// reclaim all possible orphans. Returns the number of orphans removed.
70pub fn shm_cleanup(shm_dir: impl AsRef<Path>) -> anyhow::Result<u32> {
71    // Get the shm file paths before the PIDs to avoid a race condition (#1343).
72    let shm_paths = get_shadow_shm_file_paths(shm_dir.as_ref())?;
73    log::debug!(
74        "Found {} shadow shared memory files in {}",
75        shm_paths.len(),
76        shm_dir.as_ref().display()
77    );
78
79    let running_pids = get_running_pid_set(Path::new(PROC_DIR_PATH))?;
80    log::debug!(
81        "Found {} running PIDs in {}",
82        running_pids.len(),
83        PROC_DIR_PATH
84    );
85
86    // Count how many files we remove.
87    let mut num_removed = 0;
88
89    // Best effort: ignore failures on individual paths so we can try them all.
90    for path in shm_paths {
91        // Ignore paths ending in '..'
92        if let Some(file_name) = path.file_name() {
93            let creator_pid = match pid_from_shadow_shm_file_name(&file_name.to_string_lossy()) {
94                Ok(pid) => pid,
95                Err(e) => {
96                    log::warn!("Unable to parse PID from shared memory file {path:?}: {e:?}");
97                    // Keep going to try the rest of the paths we found.
98                    continue;
99                }
100            };
101
102            // Do not remove the file if it's owner process is still running.
103            if !running_pids.contains(&creator_pid) {
104                log::trace!("Removing orphaned shared memory file {path:?}");
105                if fs::remove_file(path).is_ok() {
106                    num_removed += 1;
107                }
108            }
109        }
110    }
111
112    log::debug!("Removed {num_removed} total shared memory files.");
113    Ok(num_removed)
114}
115
116#[cfg(test)]
117mod tests {
118    use std::fs::OpenOptions;
119    use std::io;
120    use std::process;
121
122    use super::*;
123
124    fn touch(path: impl AsRef<Path>) -> io::Result<()> {
125        OpenOptions::new()
126            .create(true)
127            .truncate(false)
128            .write(true)
129            .open(path.as_ref())?;
130        Ok(())
131    }
132
133    #[test]
134    fn test_expired_shm_file_is_removed() {
135        let dir = tempfile::tempdir().unwrap();
136        let s = "shadow_shmemfile_6379761.950298775-999999999";
137        let expired: PathBuf = [dir.as_ref(), s.as_ref()].iter().collect();
138
139        touch(&expired).unwrap();
140        assert_eq!(shm_cleanup(&dir).unwrap(), 1);
141        assert!(!expired.exists(), "Exists: {}", expired.display());
142    }
143
144    #[test]
145    fn test_valid_shm_file_is_not_removed() {
146        let my_pid = process::id();
147        let dir = tempfile::tempdir().unwrap();
148        let s = format!("shadow_shmemfile_6379761.950298775-{my_pid}");
149        let valid: PathBuf = [dir.as_ref(), s.as_ref()].iter().collect();
150
151        touch(&valid).unwrap();
152        assert_eq!(shm_cleanup(&dir).unwrap(), 0);
153        assert!(valid.exists(), "Doesn't exist: {}", valid.display());
154    }
155
156    #[test]
157    fn test_nonshadow_shm_file_is_not_removed() {
158        let dir = tempfile::tempdir().unwrap();
159        let s = "shadow_unimportant_test_file";
160        let nonshadow: PathBuf = [dir.as_ref(), s.as_ref()].iter().collect();
161
162        touch(&nonshadow).unwrap();
163        assert_eq!(shm_cleanup(&dir).unwrap(), 0);
164        assert!(nonshadow.exists(), "Doesn't exist: {}", nonshadow.display());
165    }
166}