Skip to main content

libfuse_fs/util/
bind_mount.rs

1// Copyright (C) 2024 rk8s authors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3//! Bind mount utilities for container volume management
4
5use std::io::{Error, Result};
6use std::path::{Path, PathBuf};
7use std::sync::Arc;
8use tokio::sync::Mutex;
9use tracing::{debug, error, info};
10
11/// Represents a single bind mount
12#[derive(Debug, Clone)]
13pub struct BindMount {
14    /// Source path on host
15    pub source: PathBuf,
16    /// Target path relative to mount point
17    pub target: PathBuf,
18}
19
20impl BindMount {
21    /// Parse a bind mount specification like "proc:/proc" or "/host/path:/container/path"
22    pub fn parse(spec: &str) -> Result<Self> {
23        let parts: Vec<&str> = spec.split(':').collect();
24        if parts.len() != 2 {
25            return Err(Error::other(format!(
26                "Invalid bind mount spec: '{}'. Expected format: 'source:target'",
27                spec
28            )));
29        }
30
31        let source = PathBuf::from(parts[0]);
32        let target = PathBuf::from(parts[1]);
33
34        // Convert relative source paths to absolute from root
35        let source = if source.is_relative() {
36            PathBuf::from("/").join(source)
37        } else {
38            source
39        };
40
41        Ok(BindMount { source, target })
42    }
43}
44
45/// Manages multiple bind mounts with automatic cleanup
46pub struct BindMountManager {
47    mounts: Arc<Mutex<Vec<MountPoint>>>,
48    mountpoint: PathBuf,
49}
50
51#[derive(Debug)]
52struct MountPoint {
53    target: PathBuf,
54    mounted: bool,
55}
56
57impl BindMountManager {
58    /// Create a new bind mount manager
59    pub fn new<P: AsRef<Path>>(mountpoint: P) -> Self {
60        Self {
61            mounts: Arc::new(Mutex::new(Vec::new())),
62            mountpoint: mountpoint.as_ref().to_path_buf(),
63        }
64    }
65
66    /// Mount all bind mounts
67    pub async fn mount_all(&self, bind_specs: &[BindMount]) -> Result<()> {
68        let mut mounts = self.mounts.lock().await;
69
70        for bind in bind_specs {
71            let target_path = self
72                .mountpoint
73                .join(bind.target.strip_prefix("/").unwrap_or(&bind.target));
74
75            // Check if source is a file or directory
76            let source_metadata = std::fs::metadata(&bind.source)?;
77
78            if !target_path.exists() {
79                if source_metadata.is_file() {
80                    // For file bind mounts, create parent directory and an empty file
81                    if let Some(parent) = target_path.parent() {
82                        std::fs::create_dir_all(parent)?;
83                        debug!("Created parent directory: {:?}", parent);
84                    }
85                    std::fs::File::create(&target_path)?;
86                    debug!("Created target file: {:?}", target_path);
87                } else {
88                    // For directory bind mounts, create the directory
89                    std::fs::create_dir_all(&target_path)?;
90                    debug!("Created target directory: {:?}", target_path);
91                }
92            }
93
94            // Perform the bind mount
95            self.do_mount(&bind.source, &target_path)?;
96
97            mounts.push(MountPoint {
98                target: target_path.clone(),
99                mounted: true,
100            });
101
102            info!("Bind mounted {:?} -> {:?}", bind.source, target_path);
103        }
104
105        Ok(())
106    }
107
108    /// Perform the actual bind mount using mount(2) syscall
109    #[cfg(target_os = "linux")]
110    fn do_mount(&self, source: &Path, target: &Path) -> Result<()> {
111        use std::ffi::CString;
112
113        let source_cstr = CString::new(
114            source
115                .to_str()
116                .ok_or_else(|| Error::other(format!("Invalid source path: {:?}", source)))?,
117        )
118        .map_err(|e| Error::other(format!("CString error: {}", e)))?;
119
120        let target_cstr = CString::new(
121            target
122                .to_str()
123                .ok_or_else(|| Error::other(format!("Invalid target path: {:?}", target)))?,
124        )
125        .map_err(|e| Error::other(format!("CString error: {}", e)))?;
126
127        let fstype = CString::new("none").unwrap();
128
129        let ret = unsafe {
130            libc::mount(
131                source_cstr.as_ptr(),
132                target_cstr.as_ptr(),
133                fstype.as_ptr(),
134                libc::MS_BIND | libc::MS_REC,
135                std::ptr::null(),
136            )
137        };
138
139        if ret != 0 {
140            let err = Error::last_os_error();
141            error!("Failed to bind mount {:?} to {:?}: {}", source, target, err);
142            return Err(err);
143        }
144
145        // Prevent mount propagation issues by making the mount point a slave.
146        // This ensures that unmounting the target doesn't propagate back to the host/source
147        // if they are part of a shared subtree (which is common on modern Linux).
148        let ret = unsafe {
149            libc::mount(
150                std::ptr::null(),
151                target_cstr.as_ptr(),
152                std::ptr::null(),
153                libc::MS_SLAVE | libc::MS_REC,
154                std::ptr::null(),
155            )
156        };
157
158        if ret != 0 {
159            let err = Error::last_os_error();
160            error!("Failed to set mount propagation for {:?}: {}", target, err);
161            // Attempt cleanup
162            unsafe { libc::umount2(target_cstr.as_ptr(), libc::MNT_DETACH) };
163            return Err(err);
164        }
165
166        Ok(())
167    }
168
169    #[cfg(target_os = "macos")]
170    fn do_mount(&self, _source: &Path, _target: &Path) -> Result<()> {
171        // Bind mounts are not supported on non-Linux platforms yet
172        Err(Error::other("Bind mounts are not supported on macOS"))
173    }
174
175    /// Unmount all bind mounts
176    pub async fn unmount_all(&self) -> Result<()> {
177        let mut mounts = self.mounts.lock().await;
178        let mut errors = Vec::new();
179
180        // Unmount in reverse order
181        while let Some(mut mount) = mounts.pop() {
182            if mount.mounted {
183                if let Err(e) = self.do_unmount(&mount.target) {
184                    error!("Failed to unmount {:?}: {}", mount.target, e);
185                    errors.push(e);
186                } else {
187                    mount.mounted = false;
188                    info!("Unmounted {:?}", mount.target);
189                }
190            }
191        }
192
193        if !errors.is_empty() {
194            return Err(Error::other(format!(
195                "Failed to unmount {} bind mounts",
196                errors.len()
197            )));
198        }
199
200        Ok(())
201    }
202
203    /// Perform the actual unmount using umount(2) syscall
204    #[cfg(target_os = "linux")]
205    fn do_unmount(&self, target: &Path) -> Result<()> {
206        use std::ffi::CString;
207
208        let target_cstr = CString::new(
209            target
210                .to_str()
211                .ok_or_else(|| Error::other(format!("Invalid target path: {:?}", target)))?,
212        )
213        .map_err(|e| Error::other(format!("CString error: {}", e)))?;
214
215        let ret = unsafe { libc::umount2(target_cstr.as_ptr(), libc::MNT_DETACH) };
216
217        if ret != 0 {
218            let err = Error::last_os_error();
219            // EINVAL or ENOENT might mean it's already unmounted
220            if err.raw_os_error() != Some(libc::EINVAL) && err.raw_os_error() != Some(libc::ENOENT)
221            {
222                return Err(err);
223            }
224        }
225
226        Ok(())
227    }
228
229    #[cfg(target_os = "macos")]
230    fn do_unmount(&self, _target: &Path) -> Result<()> {
231        Ok(())
232    }
233}
234
235impl Drop for BindMountManager {
236    fn drop(&mut self) {
237        // Attempt to clean up on drop (synchronously)
238        let mounts = self.mounts.try_lock();
239        if let Ok(mut mounts) = mounts {
240            while let Some(mount) = mounts.pop() {
241                if mount.mounted {
242                    let _ = self.do_unmount(&mount.target);
243                }
244            }
245        }
246    }
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252
253    #[test]
254    fn test_parse_bind_mount() {
255        let bind = BindMount::parse("proc:/proc").unwrap();
256        assert_eq!(bind.source, PathBuf::from("/proc"));
257        assert_eq!(bind.target, PathBuf::from("/proc"));
258
259        let bind = BindMount::parse("/host/path:/container/path").unwrap();
260        assert_eq!(bind.source, PathBuf::from("/host/path"));
261        assert_eq!(bind.target, PathBuf::from("/container/path"));
262
263        let bind = BindMount::parse("sys:/sys").unwrap();
264        assert_eq!(bind.source, PathBuf::from("/sys"));
265        assert_eq!(bind.target, PathBuf::from("/sys"));
266    }
267
268    #[test]
269    fn test_invalid_bind_mount() {
270        assert!(BindMount::parse("invalid").is_err());
271        assert!(BindMount::parse("too:many:colons").is_err());
272    }
273
274    #[tokio::test]
275    #[cfg(target_os = "macos")]
276    async fn test_bind_mount_macos_fail() {
277        // Since mount_all calls do_mount, it should fail
278        // However, mount_all creates directories first. We should mock or use temp dirs.
279        // Or we can just call do_mount via internal method if it was public? It is private.
280        // We can call mount_all with dummy paths.
281        // But mount_all attempts to create dirs.
282        let temp = tempfile::tempdir().unwrap();
283        let source = temp.path().join("source");
284        std::fs::create_dir(&source).unwrap();
285        let target_dir = temp.path().join("target_dir");
286        let manager = BindMountManager::new(&target_dir);
287        let bind = BindMount {
288            source: source.clone(),
289            target: std::path::PathBuf::from("mnt"),
290        };
291
292        let result = manager.mount_all(&[bind]).await;
293        assert!(result.is_err());
294        assert_eq!(
295            result.unwrap_err().to_string(),
296            "Bind mounts are not supported on macOS"
297        );
298    }
299}