microsandbox_core/management/
rootfs.rs

1//! Root filesystem management for Microsandbox sandboxes.
2//!
3//! This module provides functionality for managing root filesystems used by Microsandbox sandboxes.
4//! It handles the creation, extraction, and merging of filesystem layers following OCI (Open
5//! Container Initiative) specifications.
6
7use std::{
8    collections::HashMap,
9    fs::Permissions,
10    os::unix::fs::PermissionsExt,
11    path::{Path, PathBuf},
12};
13
14use async_recursion::async_recursion;
15use tokio::fs;
16
17use crate::{config::PathPair, vm::VIRTIOFS_TAG_PREFIX, MicrosandboxResult};
18
19//--------------------------------------------------------------------------------------------------
20// Constants
21//--------------------------------------------------------------------------------------------------
22
23/// The opaque directory marker file name used in OCI layers.
24pub const OPAQUE_WHITEOUT_MARKER: &str = ".wh..wh..opq";
25
26/// The prefix for whiteout files in OCI layers.
27pub const WHITEOUT_PREFIX: &str = ".wh.";
28
29//--------------------------------------------------------------------------------------------------
30// Structs
31//--------------------------------------------------------------------------------------------------
32
33/// RAII guard that temporarily changes file permissions and restores them when dropped
34struct PermissionGuard {
35    path: PathBuf,
36    original_mode: u32,
37}
38
39impl PermissionGuard {
40    /// Creates a new guard that temporarily adds the given mode bits to the file permissions
41    fn new(path: impl AsRef<Path>, mode_to_add: u32) -> MicrosandboxResult<Self> {
42        let path = path.as_ref().to_path_buf();
43        let metadata = std::fs::metadata(&path)?;
44        let original_mode = metadata.permissions().mode();
45
46        // Update permissions
47        let mut perms = metadata.permissions();
48        perms.set_mode(original_mode | mode_to_add);
49        std::fs::set_permissions(&path, perms)?;
50
51        Ok(Self {
52            path,
53            original_mode,
54        })
55    }
56}
57
58impl Drop for PermissionGuard {
59    fn drop(&mut self) {
60        // Attempt to restore original permissions, ignore errors during drop
61        if let Ok(mut perms) = std::fs::metadata(&self.path).and_then(|m| Ok(m.permissions())) {
62            perms.set_mode(self.original_mode);
63            let _ = fs::set_permissions(&self.path, perms);
64        }
65    }
66}
67
68//--------------------------------------------------------------------------------------------------
69// Functions
70//--------------------------------------------------------------------------------------------------
71
72/// Updates a rootfs by adding sandbox script files to a `/.sandbox_scripts` directory.
73///
74/// This function:
75/// 1. Creates a `.sandbox_scripts` directory under the rootfs if it doesn't exist
76/// 2. For each script in the provided HashMap, creates a file with the given name
77/// 3. Adds a shebang line using the provided shell path
78/// 4. Makes the script files executable (rwxr-x---)
79/// 5. Creates a `shell` script containing just the shell path
80///
81/// ## Arguments
82///
83/// * `root_path` - Path to the root of the filesystem to patch
84/// * `scripts` - HashMap containing script names and their contents
85/// * `shell_path` - Path to the shell binary within the rootfs (e.g. "/bin/sh")
86pub async fn patch_with_sandbox_scripts(
87    scripts_dir: &Path,
88    scripts: &HashMap<String, String>,
89    shell_path: impl AsRef<Path>,
90) -> MicrosandboxResult<()> {
91    // Remove the scripts directory if it exists
92    if scripts_dir.exists() {
93        fs::remove_dir_all(&scripts_dir).await?;
94    }
95
96    // Create the directory if it doesn't exist
97    fs::create_dir_all(&scripts_dir).await?;
98
99    // Get shell path as string for shebang
100    let shell_path = shell_path.as_ref().to_string_lossy();
101    for (script_name, script_content) in scripts.iter() {
102        // Create script file path
103        let script_path = scripts_dir.join(script_name);
104
105        // Write shebang and content
106        let full_content = format!("#!{}\n{}\n", shell_path, script_content);
107        fs::write(&script_path, full_content).await?;
108
109        // Make executable for user and group (rwxr-x---)
110        fs::set_permissions(&script_path, Permissions::from_mode(0o750)).await?;
111    }
112
113    // Create shell script containing just the shell path
114    let shell_script_path = scripts_dir.join("shell");
115    fs::write(&shell_script_path, shell_path.to_string()).await?;
116    fs::set_permissions(&shell_script_path, Permissions::from_mode(0o750)).await?;
117
118    Ok(())
119}
120
121/// Updates the /etc/fstab file in the guest rootfs to mount the mapped directories.
122/// Creates the file if it doesn't exist.
123///
124/// This method:
125/// 1. Creates or updates the /etc/fstab file in the guest rootfs
126/// 2. Adds entries for each mapped directory using virtio-fs
127/// 3. Creates the mount points in the guest rootfs
128/// 4. Sets appropriate permissions on the fstab file
129///
130/// ## Format
131/// Each mapped directory is mounted using virtiofs with the following format:
132/// ```text
133/// virtiofs_N  /guest/path  virtiofs  defaults  0  0
134/// ```
135/// where N is the index of the mapped directory.
136///
137/// ## Arguments
138/// * `root_path` - Path to the guest rootfs
139/// * `mapped_dirs` - List of host:guest directory mappings to mount
140///
141/// ## Errors
142/// Returns an error if:
143/// - Cannot create directories in the rootfs
144/// - Cannot read or write the fstab file
145/// - Cannot set permissions on the fstab file
146pub async fn patch_with_virtiofs_mounts(
147    root_path: &Path,
148    mapped_dirs: &[PathPair],
149) -> MicrosandboxResult<()> {
150    let fstab_path = root_path.join("etc/fstab");
151
152    // Create parent directories if they don't exist
153    if let Some(parent) = fstab_path.parent() {
154        fs::create_dir_all(parent).await?;
155    }
156
157    // Read existing fstab content if it exists
158    let mut fstab_content = if fstab_path.exists() {
159        fs::read_to_string(&fstab_path).await?
160    } else {
161        String::new()
162    };
163
164    // Add header comment if file is empty
165    if fstab_content.is_empty() {
166        fstab_content.push_str(
167            "# /etc/fstab: static file system information.\n\
168                 # <file system>\t<mount point>\t<type>\t<options>\t<dump>\t<pass>\n",
169        );
170    }
171
172    // Add entries for mapped directories
173    for (idx, dir) in mapped_dirs.iter().enumerate() {
174        let tag = format!("{}_{}", VIRTIOFS_TAG_PREFIX, idx);
175        tracing::debug!("adding virtiofs mount for {}", tag);
176        let guest_path = dir.get_guest();
177
178        // Add entry for this mapped directory
179        fstab_content.push_str(&format!(
180            "{}\t{}\tvirtiofs\tdefaults\t0\t0\n",
181            tag, guest_path
182        ));
183
184        // Create the mount point directory in the guest rootfs
185        // Convert guest path to a relative path by removing leading slash
186        let guest_path_str = guest_path.as_str();
187        let relative_path = guest_path_str.strip_prefix('/').unwrap_or(guest_path_str);
188        let mount_point = root_path.join(relative_path);
189        fs::create_dir_all(mount_point).await?;
190    }
191
192    // Write updated fstab content
193    fs::write(&fstab_path, fstab_content).await?;
194
195    // Set proper permissions (644 - rw-r--r--)
196    let perms = fs::metadata(&fstab_path).await?.permissions();
197    let mut new_perms = perms;
198    new_perms.set_mode(0o644);
199    fs::set_permissions(&fstab_path, new_perms).await?;
200
201    Ok(())
202}
203
204/// Updates the /etc/hosts file in the guest rootfs to add hostname mappings.
205/// Creates the file if it doesn't exist.
206///
207/// This method:
208/// 1. Creates or updates the /etc/hosts file in the guest rootfs
209/// 2. Adds entries for each IP address and hostname pair
210/// 3. Sets appropriate permissions on the hosts file
211///
212/// ## Format
213/// Each hostname mapping follows the standard hosts file format:
214/// ```text
215/// 192.168.1.100  hostname1
216/// 192.168.1.101  hostname2
217/// ```
218///
219/// ## Arguments
220/// * `root_path` - Path to the guest rootfs
221/// * `hostname_mappings` - List of (IPv4 address, hostname) pairs to add
222///
223/// ## Errors
224/// Returns an error if:
225/// - Cannot create directories in the rootfs
226/// - Cannot read or write the hosts file
227/// - Cannot set permissions on the hosts file
228async fn _patch_with_hostnames(
229    root_path: &Path,
230    hostname_mappings: &[(std::net::Ipv4Addr, String)],
231) -> MicrosandboxResult<()> {
232    let hosts_path = root_path.join("etc/hosts");
233
234    // Create parent directories if they don't exist
235    if let Some(parent) = hosts_path.parent() {
236        fs::create_dir_all(parent).await?;
237    }
238
239    // Read existing hosts content if it exists
240    let mut hosts_content = if hosts_path.exists() {
241        fs::read_to_string(&hosts_path).await?
242    } else {
243        String::new()
244    };
245
246    // Add header comment if file is empty
247    if hosts_content.is_empty() {
248        hosts_content.push_str(
249            "# /etc/hosts: static table lookup for hostnames.\n\
250             # <ip-address>\t<hostname>\n\n\
251             127.0.0.1\tlocalhost\n\
252             ::1\tlocalhost ip6-localhost ip6-loopback\n",
253        );
254    }
255
256    // Add entries for hostname mappings
257    for (ip_addr, hostname) in hostname_mappings {
258        // Check if this mapping already exists
259        let entry = format!("{}\t{}", ip_addr, hostname);
260        if !hosts_content.contains(&entry) {
261            hosts_content.push_str(&format!("{}\n", entry));
262        }
263    }
264
265    // Write updated hosts content
266    fs::write(&hosts_path, hosts_content).await?;
267
268    // Set proper permissions (644 - rw-r--r--)
269    let perms = fs::metadata(&hosts_path).await?.permissions();
270    let mut new_perms = perms;
271    new_perms.set_mode(0o644);
272    fs::set_permissions(&hosts_path, new_perms).await?;
273
274    Ok(())
275}
276
277/// Updates the /etc/resolv.conf file in the guest rootfs to add default DNS servers if none exist.
278/// Creates the file if it doesn't exist.
279///
280/// This function:
281/// 1. Checks all root paths for existing /etc/resolv.conf files
282/// 2. If any nameserver entries exist in any layer, does nothing
283/// 3. If no nameservers exist in any layer, adds default ones (1.1.1.1 and 8.8.8.8) to the top layer
284/// 4. Sets appropriate permissions on the resolv.conf file
285///
286/// ## Format
287/// The resolv.conf file follows the standard format:
288/// ```text
289/// nameserver 1.1.1.1
290/// nameserver 8.8.8.8
291/// ```
292///
293/// ## Arguments
294/// * `root_paths` - List of root paths to check, ordered from bottom to top layer
295///                  For overlayfs, this should be [lower_layers..., patch_dir]
296///                  For native rootfs, this should be [root_path]
297///
298/// ## Errors
299/// Returns an error if:
300/// - Cannot create directories in the rootfs
301/// - Cannot read or write the resolv.conf file
302/// - Cannot set permissions on the resolv.conf file
303pub async fn patch_with_default_dns_settings(root_paths: &[PathBuf]) -> MicrosandboxResult<()> {
304    if root_paths.is_empty() {
305        return Ok(());
306    }
307
308    // Check all layers for existing nameserver entries
309    let mut has_nameserver = false;
310    for root_path in root_paths {
311        let resolv_path = root_path.join("etc/resolv.conf");
312        if resolv_path.exists() {
313            let content = fs::read_to_string(&resolv_path).await?;
314            if content
315                .lines()
316                .any(|line| line.trim_start().starts_with("nameserver "))
317            {
318                has_nameserver = true;
319                break;
320            }
321        }
322    }
323
324    // If no nameservers found in any layer, add defaults to the top layer
325    if !has_nameserver {
326        // Get the top layer (last in the list)
327        let top_layer = root_paths.last().unwrap();
328        let resolv_path = top_layer.join("etc/resolv.conf");
329
330        // Create parent directories if they don't exist
331        if let Some(parent) = resolv_path.parent() {
332            fs::create_dir_all(parent).await?;
333        }
334
335        // Create new resolv.conf with default nameservers
336        let mut resolv_content = String::from("# /etc/resolv.conf: DNS resolver configuration\n");
337        resolv_content.push_str("nameserver 1.1.1.1\n");
338        resolv_content.push_str("nameserver 8.8.8.8\n");
339
340        // Write the file
341        fs::write(&resolv_path, resolv_content).await?;
342
343        // Set proper permissions (644 - rw-r--r--)
344        let perms = fs::metadata(&resolv_path).await?.permissions();
345        let mut new_perms = perms;
346        new_perms.set_mode(0o644);
347        fs::set_permissions(&resolv_path, new_perms).await?;
348    }
349
350    Ok(())
351}
352
353/// Recursively copies a directory from source to destination, preserving file permissions.
354///
355/// This function:
356/// 1. Creates the destination directory if it doesn't exist
357/// 2. Recursively copies all files and subdirectories from source to destination
358/// 3. Preserves the original file permissions for all copied files and directories
359/// 4. Uses PermissionGuard to handle directories that may have restrictive permissions
360///
361/// ## Arguments
362/// * `src_dir` - Path to the source directory
363/// * `dst_dir` - Path to the destination directory
364///
365/// ## Errors
366/// Returns an error if:
367/// - Source directory doesn't exist or isn't readable
368/// - Cannot create the destination directory
369/// - Cannot copy files or subdirectories
370/// - Cannot set permissions on copied files or directories
371#[async_recursion(?Send)]
372pub async fn copy_dir_recursive(src_dir: &Path, dst_dir: &Path) -> MicrosandboxResult<()> {
373    // Ensure source directory exists
374    // Ensure source directory exists
375    if !src_dir.exists() {
376        return Err(crate::MicrosandboxError::PathNotFound(format!(
377            "source directory does not exist: {}",
378            src_dir.display()
379        )));
380    }
381
382    // Create destination directory if it doesn't exist
383    if !dst_dir.exists() {
384        fs::create_dir_all(dst_dir).await?;
385    }
386
387    // Copy the permissions from source to destination directory
388    let src_metadata = fs::metadata(src_dir).await?;
389    let src_perms = src_metadata.permissions();
390    fs::set_permissions(dst_dir, src_perms.clone()).await?;
391
392    // Create a permission guard to ensure we have read access to the source directory
393    // This temporarily adds read and execute permissions if needed
394    let _src_guard = PermissionGuard::new(src_dir, 0o500)?;
395
396    // Read directory entries
397    let mut entries = fs::read_dir(src_dir).await?;
398
399    while let Some(entry) = entries.next_entry().await? {
400        let src_path = entry.path();
401        let dst_path = dst_dir.join(entry.file_name());
402
403        let file_type = entry.file_type().await?;
404
405        if file_type.is_dir() {
406            // Recursively copy subdirectory
407            copy_dir_recursive(&src_path, &dst_path).await?;
408        } else if file_type.is_file() {
409            // Copy file with permissions
410            copy_file_with_permissions(&src_path, &dst_path).await?;
411        } else if file_type.is_symlink() {
412            // Copy symlink
413            let target = fs::read_link(&src_path).await?;
414            fs::symlink(target, &dst_path).await?;
415        }
416    }
417
418    Ok(())
419}
420
421/// Copies a single file from source to destination, preserving file permissions.
422///
423/// This function:
424/// 1. Creates the destination file
425/// 2. Copies the file contents
426/// 3. Preserves the original file permissions
427///
428/// ## Arguments
429/// * `src_file` - Path to the source file
430/// * `dst_file` - Path to the destination file
431///
432/// ## Errors
433/// Returns an error if:
434/// - Source file doesn't exist or isn't readable
435/// - Cannot create or write to the destination file
436/// - Cannot copy file contents
437/// - Cannot set permissions on the destination file
438async fn copy_file_with_permissions(
439    src_file: impl AsRef<Path>,
440    dst_file: impl AsRef<Path>,
441) -> MicrosandboxResult<()> {
442    let src_file = src_file.as_ref();
443    let dst_file = dst_file.as_ref();
444
445    // Create a permission guard to ensure we have read access to the source file
446    let _src_guard = PermissionGuard::new(src_file, 0o400)?;
447
448    // Copy the file contents
449    fs::copy(src_file, dst_file).await?;
450
451    // Copy the permissions from source to destination file
452    let src_metadata = fs::metadata(src_file).await?;
453    let src_perms = src_metadata.permissions();
454    fs::set_permissions(dst_file, src_perms).await?;
455
456    Ok(())
457}
458
459/// Sets the user.containers.override_stat xattr on the rootfs directory.
460///
461/// This function:
462/// 1. Sets the extended attribute user.containers.override_stat to "0:0:0555"
463/// 2. This overrides the UID:GID:MODE of the rootfs directory when accessed inside the container
464///
465/// ## Arguments
466/// * `root_path` - Path to the rootfs directory to modify
467///
468/// ## Errors
469/// Returns an error if:
470/// - Cannot set the extended attribute
471pub async fn patch_with_stat_override(root_path: &Path) -> MicrosandboxResult<()> {
472    // The xattr name to set
473    let xattr_name = "user.containers.override_stat";
474
475    // The value in the format "uid:gid:mode" (0:0:0555 means root:root with r-xr-xr-x permissions)
476    let xattr_value = "0:0:0555";
477
478    // Convert path to CString for xattr crate
479    let path_str = root_path.to_str().ok_or_else(|| {
480        crate::MicrosandboxError::InvalidArgument(format!(
481            "Could not convert path to string: {}",
482            root_path.display()
483        ))
484    })?;
485
486    // Set the xattr
487    match xattr::set(path_str, xattr_name, xattr_value.as_bytes()) {
488        Ok(_) => {
489            tracing::debug!(
490                "Set xattr {} = {} on {}",
491                xattr_name,
492                xattr_value,
493                root_path.display()
494            );
495            Ok(())
496        }
497        Err(err) => Err(crate::MicrosandboxError::Io(std::io::Error::new(
498            std::io::ErrorKind::Other,
499            format!("Failed to set xattr on {}: {}", root_path.display(), err),
500        ))),
501    }
502}
503
504//--------------------------------------------------------------------------------------------------
505// Tests
506//--------------------------------------------------------------------------------------------------
507
508#[cfg(test)]
509mod tests {
510    use std::path::PathBuf;
511
512    use tempfile::TempDir;
513
514    use crate::MicrosandboxError;
515
516    use super::*;
517
518    #[tokio::test]
519    async fn test_patch_rootfs_with_virtiofs_mounts() -> anyhow::Result<()> {
520        // Create a temporary directory to act as our rootfs
521        let root_dir = TempDir::new()?;
522        let root_path = root_dir.path();
523
524        // Create temporary directories for host paths
525        let host_dir = TempDir::new()?;
526        let host_data = host_dir.path().join("data");
527        let host_config = host_dir.path().join("config");
528        let host_app = host_dir.path().join("app");
529
530        // Create the host directories
531        fs::create_dir_all(&host_data).await?;
532        fs::create_dir_all(&host_config).await?;
533        fs::create_dir_all(&host_app).await?;
534
535        // Create test directory mappings using our temporary paths
536        let mapped_dirs = vec![
537            format!("{}:/container/data", host_data.display()).parse::<PathPair>()?,
538            format!("{}:/etc/app/config", host_config.display()).parse::<PathPair>()?,
539            format!("{}:/app", host_app.display()).parse::<PathPair>()?,
540        ];
541
542        // Update fstab
543        patch_with_virtiofs_mounts(root_path, &mapped_dirs).await?;
544
545        // Verify fstab file was created with correct content
546        let fstab_path = root_path.join("etc/fstab");
547        assert!(fstab_path.exists());
548
549        let fstab_content = fs::read_to_string(&fstab_path).await?;
550
551        // Check header
552        assert!(fstab_content.contains("# /etc/fstab: static file system information"));
553        assert!(fstab_content
554            .contains("<file system>\t<mount point>\t<type>\t<options>\t<dump>\t<pass>"));
555
556        // Check entries
557        assert!(fstab_content.contains("virtiofs_0\t/container/data\tvirtiofs\tdefaults\t0\t0"));
558        assert!(fstab_content.contains("virtiofs_1\t/etc/app/config\tvirtiofs\tdefaults\t0\t0"));
559        assert!(fstab_content.contains("virtiofs_2\t/app\tvirtiofs\tdefaults\t0\t0"));
560
561        // Verify mount points were created
562        assert!(root_path.join("container/data").exists());
563        assert!(root_path.join("etc/app/config").exists());
564        assert!(root_path.join("app").exists());
565
566        // Verify file permissions
567        let perms = fs::metadata(&fstab_path).await?.permissions();
568        assert_eq!(perms.mode() & 0o777, 0o644);
569
570        // Test updating existing fstab
571        let host_logs = host_dir.path().join("logs");
572        fs::create_dir_all(&host_logs).await?;
573
574        let new_mapped_dirs = vec![
575            format!("{}:/container/data", host_data.display()).parse::<PathPair>()?, // Keep one existing
576            format!("{}:/var/log", host_logs.display()).parse::<PathPair>()?,        // Add new one
577        ];
578
579        // Update fstab again
580        patch_with_virtiofs_mounts(root_path, &new_mapped_dirs).await?;
581
582        // Verify updated content
583        let updated_content = fs::read_to_string(&fstab_path).await?;
584        assert!(updated_content.contains("virtiofs_0\t/container/data\tvirtiofs\tdefaults\t0\t0"));
585        assert!(updated_content.contains("virtiofs_1\t/var/log\tvirtiofs\tdefaults\t0\t0"));
586
587        // Verify new mount point was created
588        assert!(root_path.join("var/log").exists());
589
590        Ok(())
591    }
592
593    #[tokio::test]
594    async fn test_patch_rootfs_with_virtiofs_mounts_permission_errors() -> anyhow::Result<()> {
595        // Skip this test in CI environments
596        if std::env::var("CI").is_ok() {
597            println!("Skipping permission test in CI environment");
598            return Ok(());
599        }
600
601        // Setup a rootfs where we can't write the fstab file
602        let readonly_dir = TempDir::new()?;
603        let readonly_path = readonly_dir.path();
604        let etc_path = readonly_path.join("etc");
605        fs::create_dir_all(&etc_path).await?;
606
607        // Make /etc directory read-only to simulate permission issues
608        let mut perms = fs::metadata(&etc_path).await?.permissions();
609        perms.set_mode(0o400); // read-only
610        fs::set_permissions(&etc_path, perms).await?;
611
612        // Verify permissions were actually set (helpful for debugging)
613        let actual_perms = fs::metadata(&etc_path).await?.permissions();
614        println!("Set /etc permissions to: {:o}", actual_perms.mode());
615
616        // Try to update fstab in a read-only /etc directory
617        let host_dir = TempDir::new()?;
618        let host_path = host_dir.path().join("test");
619        fs::create_dir_all(&host_path).await?;
620
621        let mapped_dirs =
622            vec![format!("{}:/container/data", host_path.display()).parse::<PathPair>()?];
623
624        // Function should detect it cannot write to /etc/fstab and return an error
625        let result = patch_with_virtiofs_mounts(readonly_path, &mapped_dirs).await;
626
627        // Detailed error reporting for debugging
628        if result.is_ok() {
629            println!("Warning: Write succeeded despite read-only permissions");
630            println!(
631                "Current /etc permissions: {:o}",
632                fs::metadata(&etc_path).await?.permissions().mode()
633            );
634            if etc_path.join("fstab").exists() {
635                println!(
636                    "fstab file was created with permissions: {:o}",
637                    fs::metadata(etc_path.join("fstab"))
638                        .await?
639                        .permissions()
640                        .mode()
641                );
642            }
643        }
644
645        assert!(
646            result.is_err(),
647            "Expected error when writing fstab to read-only /etc directory. \
648             Current /etc permissions: {:o}",
649            fs::metadata(&etc_path).await?.permissions().mode()
650        );
651        assert!(matches!(result.unwrap_err(), MicrosandboxError::Io(_)));
652
653        Ok(())
654    }
655
656    #[tokio::test]
657    async fn test_patch_with_hostnames() -> anyhow::Result<()> {
658        use std::net::Ipv4Addr;
659
660        // Create a temporary directory to act as our rootfs
661        let root_dir = TempDir::new()?;
662        let root_path = root_dir.path();
663
664        // Create test hostname mappings
665        let hostname_mappings = vec![
666            (Ipv4Addr::new(192, 168, 1, 100), "host1.local".to_string()),
667            (Ipv4Addr::new(192, 168, 1, 101), "host2.local".to_string()),
668        ];
669
670        // Update hosts file
671        _patch_with_hostnames(root_path, &hostname_mappings).await?;
672
673        // Verify hosts file was created with correct content
674        let hosts_path = root_path.join("etc/hosts");
675        assert!(hosts_path.exists());
676
677        let hosts_content = fs::read_to_string(&hosts_path).await?;
678
679        // Check header
680        assert!(hosts_content.contains("# /etc/hosts: static table lookup for hostnames"));
681        assert!(hosts_content.contains("127.0.0.1\tlocalhost"));
682        assert!(hosts_content.contains("::1\tlocalhost ip6-localhost ip6-loopback"));
683
684        // Check entries
685        assert!(hosts_content.contains("192.168.1.100\thost1.local"));
686        assert!(hosts_content.contains("192.168.1.101\thost2.local"));
687
688        // Verify file permissions
689        let perms = fs::metadata(&hosts_path).await?.permissions();
690        assert_eq!(perms.mode() & 0o777, 0o644);
691
692        // Test updating existing hosts file with new entries
693        let new_mappings = vec![
694            (Ipv4Addr::new(192, 168, 1, 100), "host1.local".to_string()), // Existing entry
695            (Ipv4Addr::new(192, 168, 1, 102), "host3.local".to_string()), // New entry
696        ];
697
698        // Update hosts file again
699        _patch_with_hostnames(root_path, &new_mappings).await?;
700
701        // Verify updated content
702        let updated_content = fs::read_to_string(&hosts_path).await?;
703
704        // Should still contain original entries
705        assert!(updated_content.contains("127.0.0.1\tlocalhost"));
706        assert!(updated_content.contains("::1\tlocalhost ip6-localhost ip6-loopback"));
707
708        // Should contain both old and new entries without duplicates
709        assert!(updated_content.contains("192.168.1.100\thost1.local"));
710        assert!(updated_content.contains("192.168.1.102\thost3.local"));
711
712        // Count occurrences of the first IP to ensure no duplicates
713        let count = updated_content
714            .lines()
715            .filter(|line| line.contains("192.168.1.100"))
716            .count();
717        assert_eq!(count, 1, "Should not have duplicate entries");
718
719        Ok(())
720    }
721
722    #[tokio::test]
723    async fn test_copy_dir_complex_permissions() -> anyhow::Result<()> {
724        // Skip this test in CI environments
725        if std::env::var("CI").is_ok() {
726            println!("Skipping permission test in CI environment");
727            return Ok(());
728        }
729
730        // Create temporary source and destination directories
731        let src_root = TempDir::new()?;
732        let dst_root = TempDir::new()?;
733
734        let src_path = src_root.path();
735        let dst_path = dst_root.path();
736
737        // Create a complex nested directory structure with restrictive permissions
738        // Structure:
739        // src/
740        //   ├── noaccess/     (0o000 - no permissions)
741        //   │   ├── hidden/   (0o700 - rwx------)
742        //   │   │   └── file  (0o600 - rw-------)
743        //   ├── readonly/     (0o400 - r--------)
744        //   │   ├── nested/   (0o500 - r-x------)
745        //   │   │   └── file  (0o400 - r--------)
746        //   └── normal/       (0o755 - rwxr-xr-x)
747        //       └── file      (0o644 - rw-r--r--)
748
749        // Create the directory structure first with normal permissions
750        let noaccess_dir = src_path.join("noaccess");
751        let hidden_dir = noaccess_dir.join("hidden");
752        let hidden_file = hidden_dir.join("file");
753
754        let readonly_dir = src_path.join("readonly");
755        let nested_dir = readonly_dir.join("nested");
756        let nested_file = nested_dir.join("file");
757
758        let normal_dir = src_path.join("normal");
759        let normal_file = normal_dir.join("file");
760
761        // Create directories
762        fs::create_dir_all(&hidden_dir).await?;
763        fs::create_dir_all(&nested_dir).await?;
764        fs::create_dir_all(&normal_dir).await?;
765
766        // Create files with content
767        fs::write(&hidden_file, "hidden content").await?;
768        fs::write(&nested_file, "nested content").await?;
769        fs::write(&normal_file, "normal content").await?;
770
771        // Now set the restrictive permissions
772        fs::set_permissions(&noaccess_dir, Permissions::from_mode(0o000)).await?; // ---------
773        fs::set_permissions(&hidden_dir, Permissions::from_mode(0o700)).await?; // rwx------
774        fs::set_permissions(&hidden_file, Permissions::from_mode(0o600)).await?; // rw-------
775
776        fs::set_permissions(&readonly_dir, Permissions::from_mode(0o400)).await?; // r--------
777        fs::set_permissions(&nested_dir, Permissions::from_mode(0o500)).await?; // r-x------
778        fs::set_permissions(&nested_file, Permissions::from_mode(0o400)).await?; // r--------
779
780        fs::set_permissions(&normal_dir, Permissions::from_mode(0o755)).await?; // rwxr-xr-x
781        fs::set_permissions(&normal_file, Permissions::from_mode(0o644)).await?; // rw-r--r--
782
783        // Verify permissions were set correctly
784        let noaccess_perms = fs::metadata(&noaccess_dir).await?.permissions().mode() & 0o777;
785        let readonly_perms = fs::metadata(&readonly_dir).await?.permissions().mode() & 0o777;
786
787        println!("No access dir permissions: {:o}", noaccess_perms);
788        println!("Read-only dir permissions: {:o}", readonly_perms);
789
790        // This copy should succeed despite the restrictive permissions
791        // The PermissionGuard will temporarily add necessary permissions
792        copy_dir_recursive(src_path, dst_path).await?;
793
794        // Verify the copy worked, even for directories with restrictive permissions
795        let dst_noaccess_dir = dst_path.join("noaccess");
796        let dst_hidden_dir = dst_noaccess_dir.join("hidden");
797        let dst_hidden_file = dst_hidden_dir.join("file");
798
799        let dst_readonly_dir = dst_path.join("readonly");
800        let dst_nested_dir = dst_readonly_dir.join("nested");
801        let dst_nested_file = dst_nested_dir.join("file");
802
803        let dst_normal_dir = dst_path.join("normal");
804        let dst_normal_file = dst_normal_dir.join("file");
805
806        // Check everything was copied
807        assert!(
808            dst_noaccess_dir.exists(),
809            "No-access directory was not copied"
810        );
811        assert!(dst_hidden_dir.exists(), "Hidden directory was not copied");
812        assert!(dst_hidden_file.exists(), "Hidden file was not copied");
813
814        assert!(
815            dst_readonly_dir.exists(),
816            "Read-only directory was not copied"
817        );
818        assert!(dst_nested_dir.exists(), "Nested directory was not copied");
819        assert!(dst_nested_file.exists(), "Nested file was not copied");
820
821        assert!(dst_normal_dir.exists(), "Normal directory was not copied");
822        assert!(dst_normal_file.exists(), "Normal file was not copied");
823
824        // Check file contents were preserved
825        assert_eq!(
826            fs::read_to_string(&dst_hidden_file).await?,
827            "hidden content"
828        );
829        assert_eq!(
830            fs::read_to_string(&dst_nested_file).await?,
831            "nested content"
832        );
833        assert_eq!(
834            fs::read_to_string(&dst_normal_file).await?,
835            "normal content"
836        );
837
838        // Check permissions were preserved
839        let dst_noaccess_perms =
840            fs::metadata(&dst_noaccess_dir).await?.permissions().mode() & 0o777;
841        let dst_hidden_perms = fs::metadata(&dst_hidden_dir).await?.permissions().mode() & 0o777;
842        let dst_hidden_file_perms =
843            fs::metadata(&dst_hidden_file).await?.permissions().mode() & 0o777;
844
845        let dst_readonly_perms =
846            fs::metadata(&dst_readonly_dir).await?.permissions().mode() & 0o777;
847        let dst_nested_perms = fs::metadata(&dst_nested_dir).await?.permissions().mode() & 0o777;
848        let dst_nested_file_perms =
849            fs::metadata(&dst_nested_file).await?.permissions().mode() & 0o777;
850
851        let dst_normal_perms = fs::metadata(&dst_normal_dir).await?.permissions().mode() & 0o777;
852        let dst_normal_file_perms =
853            fs::metadata(&dst_normal_file).await?.permissions().mode() & 0o777;
854
855        // Verify all permissions were preserved
856        assert_eq!(
857            dst_noaccess_perms, 0o000,
858            "No-access directory permissions not preserved"
859        );
860        assert_eq!(
861            dst_hidden_perms, 0o700,
862            "Hidden directory permissions not preserved"
863        );
864        assert_eq!(
865            dst_hidden_file_perms, 0o600,
866            "Hidden file permissions not preserved"
867        );
868
869        assert_eq!(
870            dst_readonly_perms, 0o400,
871            "Read-only directory permissions not preserved"
872        );
873        assert_eq!(
874            dst_nested_perms, 0o500,
875            "Nested directory permissions not preserved"
876        );
877        assert_eq!(
878            dst_nested_file_perms, 0o400,
879            "Nested file permissions not preserved"
880        );
881
882        assert_eq!(
883            dst_normal_perms, 0o755,
884            "Normal directory permissions not preserved"
885        );
886        assert_eq!(
887            dst_normal_file_perms, 0o644,
888            "Normal file permissions not preserved"
889        );
890
891        Ok(())
892    }
893
894    #[tokio::test]
895    async fn test_copy_dir_nonexistent_source() -> anyhow::Result<()> {
896        // Create a temporary destination directory
897        let dst_root = TempDir::new()?;
898        let dst_path = dst_root.path();
899
900        // Try to copy from a non-existent source
901        let src_path = PathBuf::from("/nonexistent/directory");
902
903        // This should fail with a PathNotFound error
904        let result = copy_dir_recursive(&src_path, dst_path).await;
905
906        assert!(
907            result.is_err(),
908            "Expected an error when source doesn't exist"
909        );
910        assert!(
911            matches!(result.unwrap_err(), MicrosandboxError::PathNotFound(_)),
912            "Expected a PathNotFound error"
913        );
914
915        Ok(())
916    }
917
918    #[tokio::test]
919    async fn test_patch_with_default_dns_settings() -> anyhow::Result<()> {
920        // Create a temporary directory to act as our rootfs
921        let root_dir = TempDir::new()?;
922        let root_path = root_dir.path();
923
924        // Test case 1: No existing resolv.conf
925        patch_with_default_dns_settings(&[root_path.to_path_buf()]).await?;
926
927        // Verify resolv.conf was created with correct content
928        let resolv_path = root_path.join("etc/resolv.conf");
929        assert!(resolv_path.exists());
930
931        let resolv_content = fs::read_to_string(&resolv_path).await?;
932
933        // Check content
934        assert!(resolv_content.contains("# /etc/resolv.conf: DNS resolver configuration"));
935        assert!(resolv_content.contains("nameserver 1.1.1.1"));
936        assert!(resolv_content.contains("nameserver 8.8.8.8"));
937
938        // Verify file permissions
939        let perms = fs::metadata(&resolv_path).await?.permissions();
940        assert_eq!(perms.mode() & 0o777, 0o644);
941
942        // Test case 2: Existing resolv.conf with no nameservers
943        let root_dir2 = TempDir::new()?;
944        let root_path2 = root_dir2.path();
945        let resolv_path2 = root_path2.join("etc/resolv.conf");
946        fs::create_dir_all(resolv_path2.parent().unwrap()).await?;
947        fs::write(&resolv_path2, "# Empty resolv.conf\n").await?;
948
949        patch_with_default_dns_settings(&[root_path2.to_path_buf()]).await?;
950
951        // Verify nameservers were added
952        let content2 = fs::read_to_string(&resolv_path2).await?;
953        assert!(content2.contains("nameserver 1.1.1.1"));
954        assert!(content2.contains("nameserver 8.8.8.8"));
955
956        // Test case 3: Existing resolv.conf with nameservers
957        let root_dir3 = TempDir::new()?;
958        let root_path3 = root_dir3.path();
959        let resolv_path3 = root_path3.join("etc/resolv.conf");
960        fs::create_dir_all(resolv_path3.parent().unwrap()).await?;
961        fs::write(
962            &resolv_path3,
963            "# Existing nameservers\nnameserver 192.168.1.1\n",
964        )
965        .await?;
966
967        patch_with_default_dns_settings(&[root_path3.to_path_buf()]).await?;
968
969        // Verify content was not changed
970        let content3 = fs::read_to_string(&resolv_path3).await?;
971        assert!(content3.contains("nameserver 192.168.1.1"));
972        assert!(!content3.contains("nameserver 1.1.1.1"));
973        assert!(!content3.contains("nameserver 8.8.8.8"));
974
975        // Test case 4: Multiple layers (overlayfs)
976        let root_dir4 = TempDir::new()?;
977        let lower_layer1 = root_dir4.path().join("lower1");
978        let lower_layer2 = root_dir4.path().join("lower2");
979        let patch_layer = root_dir4.path().join("patch");
980
981        // Create directories
982        fs::create_dir_all(&lower_layer1).await?;
983        fs::create_dir_all(&lower_layer2).await?;
984        fs::create_dir_all(&patch_layer).await?;
985
986        // Test 4a: No resolv.conf in any layer
987        patch_with_default_dns_settings(&[
988            lower_layer1.clone(),
989            lower_layer2.clone(),
990            patch_layer.clone(),
991        ])
992        .await?;
993
994        // Verify resolv.conf was created in patch layer only
995        assert!(!lower_layer1.join("etc/resolv.conf").exists());
996        assert!(!lower_layer2.join("etc/resolv.conf").exists());
997        let patch_resolv = patch_layer.join("etc/resolv.conf");
998        assert!(patch_resolv.exists());
999        let content = fs::read_to_string(&patch_resolv).await?;
1000        assert!(content.contains("nameserver 1.1.1.1"));
1001
1002        // Test 4b: resolv.conf exists in lower layer with nameserver
1003        let root_dir5 = TempDir::new()?;
1004        let lower_layer = root_dir5.path().join("lower");
1005        let patch_layer = root_dir5.path().join("patch");
1006        fs::create_dir_all(&lower_layer.join("etc")).await?;
1007        fs::create_dir_all(&patch_layer).await?;
1008
1009        // Create resolv.conf in lower layer with nameserver
1010        fs::write(
1011            lower_layer.join("etc/resolv.conf"),
1012            "nameserver 192.168.1.1\n",
1013        )
1014        .await?;
1015
1016        patch_with_default_dns_settings(&[lower_layer.clone(), patch_layer.clone()]).await?;
1017
1018        // Verify no resolv.conf was created in patch layer
1019        assert!(!patch_layer.join("etc/resolv.conf").exists());
1020        let lower_content = fs::read_to_string(lower_layer.join("etc/resolv.conf")).await?;
1021        assert!(lower_content.contains("nameserver 192.168.1.1"));
1022
1023        Ok(())
1024    }
1025
1026    #[tokio::test]
1027    async fn test_patch_with_stat_override() -> anyhow::Result<()> {
1028        // Skip this test if no xattr support
1029        if !xattr::SUPPORTED_PLATFORM {
1030            println!("Skipping xattr test on unsupported platform");
1031            return Ok(());
1032        }
1033
1034        // Create a temporary directory to act as our rootfs
1035        let root_dir = TempDir::new()?;
1036        let root_path = root_dir.path();
1037
1038        // Patch with stat override
1039        patch_with_stat_override(root_path).await?;
1040
1041        // Verify xattr was set correctly
1042        let xattr_value =
1043            xattr::get(root_path, "user.containers.override_stat").expect("Failed to get xattr");
1044
1045        // Check if xattr was set and has the correct value
1046        assert!(xattr_value.is_some(), "xattr was not set");
1047        assert_eq!(xattr_value.unwrap(), b"0:0:0555", "xattr value incorrect");
1048
1049        Ok(())
1050    }
1051}