Skip to main content

vfs/posix/
overlay_fs.rs

1use super::vfs::{
2    normalize_path, MemoryFileSystem, VfsError, VfsResult, VirtualDirEntry, VirtualFileSystem,
3    VirtualStat, VirtualUtimeSpec,
4};
5use base64::Engine;
6use std::collections::BTreeSet;
7
8const MAX_SNAPSHOT_DEPTH: usize = 1024;
9const OVERLAY_METADATA_ROOT: &str = "/.secure-exec-overlay";
10const OVERLAY_WHITEOUT_DIR: &str = "/.secure-exec-overlay/whiteouts";
11const OVERLAY_OPAQUE_DIR: &str = "/.secure-exec-overlay/opaque";
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum OverlayMode {
15    Ephemeral,
16    ReadOnly,
17}
18
19#[derive(Debug)]
20pub struct OverlayFileSystem {
21    lowers: Vec<MemoryFileSystem>,
22    upper: Option<MemoryFileSystem>,
23    writes_locked: bool,
24}
25
26#[derive(Debug, Clone, Copy)]
27enum OverlayMarkerKind {
28    Whiteout,
29    Opaque,
30}
31
32#[derive(Debug)]
33enum OverlaySnapshotKind {
34    Directory,
35    File(Vec<u8>),
36    Symlink(String),
37}
38
39#[derive(Debug)]
40struct OverlaySnapshotEntry {
41    path: String,
42    stat: VirtualStat,
43    kind: OverlaySnapshotKind,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
47struct OverlayCopyUpUsage {
48    total_bytes: u64,
49    inode_count: usize,
50}
51
52impl OverlayFileSystem {
53    pub fn new(lowers: Vec<MemoryFileSystem>, mode: OverlayMode) -> Self {
54        let mut effective_lowers = lowers;
55        if effective_lowers.is_empty() {
56            effective_lowers.push(MemoryFileSystem::new());
57        }
58
59        let mut upper = match mode {
60            OverlayMode::Ephemeral => Some(MemoryFileSystem::new()),
61            OverlayMode::ReadOnly => None,
62        };
63        if let Some(upper_filesystem) = upper.as_mut() {
64            sync_upper_root_metadata(upper_filesystem, &effective_lowers);
65        }
66
67        Self {
68            lowers: effective_lowers,
69            upper,
70            writes_locked: matches!(mode, OverlayMode::ReadOnly),
71        }
72    }
73
74    pub fn with_upper(lowers: Vec<MemoryFileSystem>, upper: MemoryFileSystem) -> Self {
75        let mut effective_lowers = lowers;
76        if effective_lowers.is_empty() {
77            effective_lowers.push(MemoryFileSystem::new());
78        }
79
80        Self {
81            lowers: effective_lowers,
82            upper: Some(upper),
83            writes_locked: false,
84        }
85    }
86
87    pub fn lock_writes(&mut self) {
88        self.writes_locked = true;
89    }
90
91    fn normalized(path: &str) -> String {
92        normalize_path(path)
93    }
94
95    fn parent_path(path: &str) -> String {
96        let normalized = Self::normalized(path);
97        if normalized == "/" {
98            return String::from("/");
99        }
100
101        match normalized.rsplit_once('/') {
102            Some(("", _)) | None => String::from("/"),
103            Some((parent, _)) => String::from(parent),
104        }
105    }
106
107    fn basename(path: &str) -> String {
108        let normalized = Self::normalized(path);
109        if normalized == "/" {
110            return String::from("/");
111        }
112        normalized
113            .rsplit('/')
114            .find(|component| !component.is_empty())
115            .unwrap_or("")
116            .to_owned()
117    }
118
119    fn validate_destination_parent(&mut self, path: &str) -> VfsResult<()> {
120        let parent = Self::parent_path(path);
121        let resolved_parent = self.resolve_merged_path(&parent, true, 0)?;
122        let stat = self.merged_lstat(&resolved_parent)?;
123        if !stat.is_directory {
124            return Err(Self::not_directory(&parent));
125        }
126        Ok(())
127    }
128
129    fn resolved_destination_path(&self, path: &str) -> VfsResult<String> {
130        let parent = Self::parent_path(path);
131        let resolved_parent = self.resolve_merged_path(&parent, true, 0)?;
132        Ok(Self::join_path(&resolved_parent, &Self::basename(path)))
133    }
134
135    fn resolve_merged_path(
136        &self,
137        path: &str,
138        follow_final_symlink: bool,
139        depth: usize,
140    ) -> VfsResult<String> {
141        if depth > MAX_SNAPSHOT_DEPTH {
142            return Err(VfsError::new(
143                "ELOOP",
144                format!("too many symbolic links while resolving '{path}'"),
145            ));
146        }
147
148        let normalized = Self::normalized(path);
149        if normalized == "/" {
150            return Ok(normalized);
151        }
152
153        let components: Vec<&str> = normalized
154            .split('/')
155            .filter(|component| !component.is_empty())
156            .collect();
157        let mut current = String::from("/");
158
159        for (index, component) in components.iter().enumerate() {
160            let candidate = Self::join_path(&current, component);
161            let is_final = index + 1 == components.len();
162            let should_follow = !is_final || follow_final_symlink;
163
164            if should_follow {
165                if let Ok(stat) = self.merged_lstat(&candidate) {
166                    if stat.is_symbolic_link {
167                        let target = self.read_link_inner(&candidate)?;
168                        let target_path = if target.starts_with('/') {
169                            Self::normalized(&target)
170                        } else {
171                            Self::normalized(&Self::join_path(
172                                &Self::parent_path(&candidate),
173                                &target,
174                            ))
175                        };
176                        let remainder = components[index + 1..].join("/");
177                        let next_path = if remainder.is_empty() {
178                            target_path
179                        } else {
180                            Self::normalized(&Self::join_path(&target_path, &remainder))
181                        };
182                        return self.resolve_merged_path(
183                            &next_path,
184                            follow_final_symlink,
185                            depth + 1,
186                        );
187                    }
188
189                    if !is_final && !stat.is_directory {
190                        return Err(Self::not_directory(&candidate));
191                    }
192                }
193            } else if let Ok(stat) = self.merged_lstat(&candidate) {
194                if !is_final && !stat.is_directory {
195                    return Err(Self::not_directory(&candidate));
196                }
197            }
198
199            current = candidate;
200        }
201
202        Ok(current)
203    }
204
205    fn destination_parent_copy_up_paths(&self, path: &str) -> VfsResult<Vec<String>> {
206        let parent = Self::parent_path(path);
207        let mut paths = Vec::new();
208        let mut seen = BTreeSet::new();
209        self.collect_destination_parent_copy_up_paths(&parent, &mut paths, &mut seen, 0)?;
210        Ok(paths)
211    }
212
213    fn collect_destination_parent_copy_up_paths(
214        &self,
215        parent: &str,
216        paths: &mut Vec<String>,
217        seen: &mut BTreeSet<String>,
218        depth: usize,
219    ) -> VfsResult<()> {
220        if depth > MAX_SNAPSHOT_DEPTH {
221            return Err(VfsError::new(
222                "ELOOP",
223                format!("too many symbolic links while resolving '{parent}'"),
224            ));
225        }
226
227        let normalized = Self::normalized(parent);
228        if normalized == "/" {
229            return Ok(());
230        }
231
232        let components: Vec<&str> = normalized
233            .split('/')
234            .filter(|component| !component.is_empty())
235            .collect();
236        let mut current = String::from("/");
237        for (index, component) in components.iter().enumerate() {
238            current = Self::join_path(&current, component);
239            let stat = self.merged_lstat(&current)?;
240
241            if stat.is_symbolic_link {
242                if !self.has_entry_in_upper(&current) && seen.insert(current.clone()) {
243                    paths.push(current.clone());
244                }
245
246                let target = self.read_link_inner(&current)?;
247                let target_path = if target.starts_with('/') {
248                    Self::normalized(&target)
249                } else {
250                    Self::normalized(&Self::join_path(&Self::parent_path(&current), &target))
251                };
252                let remainder = components[index + 1..].join("/");
253                let next_parent = if remainder.is_empty() {
254                    target_path
255                } else {
256                    Self::normalized(&Self::join_path(&target_path, &remainder))
257                };
258                return self.collect_destination_parent_copy_up_paths(
259                    &next_parent,
260                    paths,
261                    seen,
262                    depth + 1,
263                );
264            }
265
266            if self.find_lower_by_entry(&current).is_some()
267                && !self.has_entry_in_upper(&current)
268                && seen.insert(current.clone())
269            {
270                paths.push(current.clone());
271            }
272        }
273
274        Ok(())
275    }
276
277    fn encode_marker_path(path: &str) -> String {
278        base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(path)
279    }
280
281    fn marker_directory(kind: OverlayMarkerKind) -> &'static str {
282        match kind {
283            OverlayMarkerKind::Whiteout => OVERLAY_WHITEOUT_DIR,
284            OverlayMarkerKind::Opaque => OVERLAY_OPAQUE_DIR,
285        }
286    }
287
288    fn marker_path(kind: OverlayMarkerKind, path: &str) -> String {
289        format!(
290            "{}/{}",
291            Self::marker_directory(kind),
292            Self::encode_marker_path(&Self::normalized(path))
293        )
294    }
295
296    fn is_internal_metadata_path(path: &str) -> bool {
297        let normalized = Self::normalized(path);
298        normalized == OVERLAY_METADATA_ROOT
299            || normalized.starts_with(&(String::from(OVERLAY_METADATA_ROOT) + "/"))
300    }
301
302    /// Returns true if `path`, or the location it resolves to through symlinks,
303    /// lands in the reserved overlay metadata namespace.
304    ///
305    /// The lexical [`is_internal_metadata_path`] check alone is bypassable: the
306    /// underlying `MemoryFileSystem` follows symlinks, so a guest-created symlink
307    /// whose resolved target enters `/.secure-exec-overlay` (directly, or via a
308    /// symlink to an ancestor such as `/`) would slip past a purely lexical guard
309    /// and let the guest read or tamper with whiteout/opaque markers (e.g.
310    /// resurrecting a deleted lower-layer file). Resolving before the check
311    /// closes that hole while leaving ordinary symlinks unaffected.
312    fn touches_internal_metadata(&self, path: &str) -> bool {
313        if Self::is_internal_metadata_path(path) {
314            return true;
315        }
316        if let Ok(resolved) = self.resolve_merged_path(path, true, 0) {
317            if Self::is_internal_metadata_path(&resolved) {
318                return true;
319            }
320        }
321        if let Ok(resolved) = self.resolved_destination_path(path) {
322            if Self::is_internal_metadata_path(&resolved) {
323                return true;
324            }
325        }
326        false
327    }
328
329    fn entry_touches_internal_metadata(&self, path: &str) -> bool {
330        Self::is_internal_metadata_path(path)
331            || self
332                .resolved_destination_path(path)
333                .is_ok_and(|resolved| Self::is_internal_metadata_path(&resolved))
334    }
335
336    fn hidden_root_entry_name() -> &'static str {
337        ".secure-exec-overlay"
338    }
339
340    fn should_hide_directory_entry(path: &str, entry: &str) -> bool {
341        let normalized = Self::normalized(path);
342        normalized == "/" && entry == Self::hidden_root_entry_name()
343    }
344
345    fn should_ignore_raw_directory_entry(
346        upper: Option<&MemoryFileSystem>,
347        path: &str,
348        entry: &str,
349    ) -> bool {
350        if entry == "." || entry == ".." || Self::should_hide_directory_entry(path, entry) {
351            return true;
352        }
353
354        let entry_path = Self::join_path(path, entry);
355        Self::marker_exists_in_upper(upper, OverlayMarkerKind::Whiteout, &entry_path)
356    }
357
358    fn check_copy_up_usage_limits(
359        usage: &OverlayCopyUpUsage,
360        max_bytes: Option<u64>,
361        max_inodes: Option<usize>,
362    ) -> VfsResult<()> {
363        if let Some(limit) = max_bytes {
364            if usage.total_bytes > limit {
365                return Err(VfsError::new(
366                    "ENOSPC",
367                    format!(
368                        "overlay rename copy-up bytes {} exceed configured limit {}",
369                        usage.total_bytes, limit
370                    ),
371                ));
372            }
373        }
374
375        if let Some(limit) = max_inodes {
376            if usage.inode_count > limit {
377                return Err(VfsError::new(
378                    "ENOSPC",
379                    format!(
380                        "overlay rename copy-up inodes {} exceed configured limit {}",
381                        usage.inode_count, limit
382                    ),
383                ));
384            }
385        }
386
387        Ok(())
388    }
389
390    fn add_copy_up_usage(
391        usage: &mut OverlayCopyUpUsage,
392        bytes: u64,
393        inodes: usize,
394        max_bytes: Option<u64>,
395        max_inodes: Option<usize>,
396    ) -> VfsResult<()> {
397        usage.total_bytes = usage.total_bytes.saturating_add(bytes);
398        usage.inode_count = usage.inode_count.saturating_add(inodes);
399        Self::check_copy_up_usage_limits(usage, max_bytes, max_inodes)
400    }
401
402    fn remaining_inode_budget(
403        usage: &OverlayCopyUpUsage,
404        max_inodes: Option<usize>,
405    ) -> Option<usize> {
406        max_inodes.map(|limit| limit.saturating_sub(usage.inode_count))
407    }
408
409    fn copy_up_directory_entries_limited(
410        &mut self,
411        path: &str,
412        max_entries: Option<usize>,
413    ) -> VfsResult<Vec<String>> {
414        let Some(max_entries) = max_entries else {
415            return self.read_dir(path);
416        };
417
418        match self.read_dir_limited(path, max_entries) {
419            Ok(entries) => Ok(entries),
420            Err(error) if error.code() == "ENOMEM" => Err(VfsError::new(
421                "ENOSPC",
422                format!("overlay rename copy-up directory '{path}' exceeds configured inode limit"),
423            )),
424            Err(error) => Err(error),
425        }
426    }
427
428    fn directory_has_visible_entries_limited(&mut self, path: &str) -> VfsResult<bool> {
429        match self.read_dir_limited(path, 1) {
430            Ok(entries) => Ok(!entries.is_empty()),
431            Err(error) if error.code() == "ENOMEM" => Ok(true),
432            Err(error) => Err(error),
433        }
434    }
435
436    fn memory_subtree_usage_limited(
437        filesystem: &mut MemoryFileSystem,
438        path: &str,
439        max_bytes: Option<u64>,
440        max_inodes: Option<usize>,
441    ) -> VfsResult<OverlayCopyUpUsage> {
442        let mut usage = OverlayCopyUpUsage::default();
443        let mut visited = BTreeSet::new();
444        let mut pending = vec![Self::normalized(path)];
445        while let Some(current_path) = pending.pop() {
446            let stat = filesystem.lstat(&current_path)?;
447            if visited.insert((stat.dev, stat.ino)) {
448                let bytes = if stat.is_directory && !stat.is_symbolic_link {
449                    0
450                } else {
451                    stat.size
452                };
453                Self::add_copy_up_usage(&mut usage, bytes, 1, max_bytes, max_inodes)?;
454            }
455
456            if stat.is_directory && !stat.is_symbolic_link {
457                let remaining = Self::remaining_inode_budget(&usage, max_inodes);
458                let children = if let Some(max_entries) = remaining {
459                    filesystem.read_dir_limited(&current_path, max_entries)?
460                } else {
461                    filesystem.read_dir(&current_path)?
462                };
463                for entry in children.into_iter().rev() {
464                    if matches!(entry.as_str(), "." | "..") {
465                        continue;
466                    }
467                    if Self::should_hide_directory_entry(&current_path, &entry) {
468                        continue;
469                    }
470                    pending.push(Self::join_path(&current_path, &entry));
471                }
472            }
473        }
474
475        Ok(usage)
476    }
477
478    fn memory_subtree_released_usage(
479        filesystem: &mut MemoryFileSystem,
480        path: &str,
481    ) -> VfsResult<OverlayCopyUpUsage> {
482        let mut usage = OverlayCopyUpUsage::default();
483        let mut visited = BTreeSet::new();
484        let mut pending = vec![Self::normalized(path)];
485        while let Some(current_path) = pending.pop() {
486            let stat = filesystem.lstat(&current_path)?;
487            if visited.insert((stat.dev, stat.ino)) {
488                let subtree_links = filesystem.link_count_in_subtree(stat.ino, path) as u64;
489                if stat.is_directory || stat.nlink <= subtree_links {
490                    let bytes = if stat.is_directory && !stat.is_symbolic_link {
491                        0
492                    } else {
493                        stat.size
494                    };
495                    Self::add_copy_up_usage(&mut usage, bytes, 1, None, None)?;
496                }
497            }
498
499            if stat.is_directory && !stat.is_symbolic_link {
500                for entry in filesystem.read_dir(&current_path)?.into_iter().rev() {
501                    if matches!(entry.as_str(), "." | "..") {
502                        continue;
503                    }
504                    if Self::should_hide_directory_entry(&current_path, &entry) {
505                        continue;
506                    }
507                    pending.push(Self::join_path(&current_path, &entry));
508                }
509            }
510        }
511
512        Ok(usage)
513    }
514
515    fn upper_usage_limited(
516        &mut self,
517        max_bytes: Option<u64>,
518        max_inodes: Option<usize>,
519    ) -> VfsResult<OverlayCopyUpUsage> {
520        let Some(upper) = self.upper.as_mut() else {
521            return Ok(OverlayCopyUpUsage::default());
522        };
523
524        Self::memory_subtree_usage_limited(upper, "/", max_bytes, max_inodes)
525    }
526
527    fn upper_subtree_released_usage(&mut self, path: &str) -> VfsResult<OverlayCopyUpUsage> {
528        let Some(upper) = self.upper.as_mut() else {
529            return Ok(OverlayCopyUpUsage::default());
530        };
531
532        if !upper.exists(path) {
533            return Ok(OverlayCopyUpUsage::default());
534        }
535
536        Self::memory_subtree_released_usage(upper, path)
537    }
538
539    fn collect_copy_up_usage_limited(
540        &mut self,
541        path: &str,
542        usage: &mut OverlayCopyUpUsage,
543        max_bytes: Option<u64>,
544        max_inodes: Option<usize>,
545    ) -> VfsResult<()> {
546        let mut pending = vec![(Self::normalized(path), 0usize)];
547        while let Some((current_path, depth)) = pending.pop() {
548            if depth > MAX_SNAPSHOT_DEPTH {
549                return Err(VfsError::new(
550                    "EINVAL",
551                    format!("overlay snapshot depth limit exceeded at '{current_path}'"),
552                ));
553            }
554
555            let stat = self.merged_lstat(&current_path)?;
556            if !self.has_entry_in_upper(&current_path) {
557                let bytes = if stat.is_symbolic_link {
558                    self.read_link_inner(&current_path)?.len() as u64
559                } else if stat.is_directory {
560                    0
561                } else {
562                    stat.size
563                };
564                Self::add_copy_up_usage(usage, bytes, 1, max_bytes, max_inodes)?;
565            }
566
567            if stat.is_directory && !stat.is_symbolic_link {
568                let children = self.copy_up_directory_entries_limited(&current_path, max_inodes)?;
569                for entry in children.into_iter().rev() {
570                    pending.push((Self::join_path(&current_path, &entry), depth + 1));
571                }
572            }
573        }
574
575        Ok(())
576    }
577
578    fn collect_single_copy_up_usage_limited(
579        &mut self,
580        path: &str,
581        usage: &mut OverlayCopyUpUsage,
582        max_bytes: Option<u64>,
583        max_inodes: Option<usize>,
584    ) -> VfsResult<()> {
585        if self.has_entry_in_upper(path) {
586            return Ok(());
587        }
588
589        let stat = self.merged_lstat(path)?;
590        let bytes = if stat.is_symbolic_link {
591            self.read_link_inner(path)?.len() as u64
592        } else if stat.is_directory {
593            0
594        } else {
595            stat.size
596        };
597        Self::add_copy_up_usage(usage, bytes, 1, max_bytes, max_inodes)
598    }
599
600    pub fn check_rename_copy_up_limits(
601        &mut self,
602        old_path: &str,
603        new_path: &str,
604        max_bytes: Option<u64>,
605        max_inodes: Option<usize>,
606    ) -> VfsResult<()> {
607        let old_normalized = Self::normalized(old_path);
608        let new_normalized = Self::normalized(new_path);
609        if Self::is_internal_metadata_path(&old_normalized)
610            || Self::is_internal_metadata_path(&new_normalized)
611        {
612            return Err(VfsError::permission_denied("rename", old_path));
613        }
614
615        if old_normalized == "/" {
616            return Err(VfsError::permission_denied("rename", old_path));
617        }
618
619        if old_normalized == new_normalized {
620            return Ok(());
621        }
622
623        let source_stat = self.merged_lstat(old_path)?;
624        if self.writes_locked {
625            self.writable_upper(&old_normalized)?;
626        }
627        self.validate_destination_parent(&new_normalized)?;
628        let resolved_new_normalized = self.resolved_destination_path(&new_normalized)?;
629
630        if old_normalized == resolved_new_normalized {
631            return Ok(());
632        }
633
634        if source_stat.is_directory
635            && resolved_new_normalized.starts_with(&(old_normalized.clone() + "/"))
636        {
637            return Err(VfsError::new(
638                "EINVAL",
639                format!(
640                    "cannot move '{}' into its own descendant '{}'",
641                    old_path, new_path
642                ),
643            ));
644        }
645
646        let destination_parent_copy_up_paths =
647            self.destination_parent_copy_up_paths(&new_normalized)?;
648
649        if let Ok(destination_stat) = self.merged_lstat(&resolved_new_normalized) {
650            if destination_stat.is_directory
651                && !destination_stat.is_symbolic_link
652                && self.directory_has_visible_entries_limited(&resolved_new_normalized)?
653            {
654                return Err(Self::not_empty(&resolved_new_normalized));
655            }
656        }
657
658        let mut usage = self.upper_usage_limited(None, None)?;
659        if self.has_entry_in_upper(&resolved_new_normalized) {
660            let destination_usage = self.upper_subtree_released_usage(&resolved_new_normalized)?;
661            usage.total_bytes = usage
662                .total_bytes
663                .saturating_sub(destination_usage.total_bytes);
664            usage.inode_count = usage
665                .inode_count
666                .saturating_sub(destination_usage.inode_count);
667        }
668        Self::check_copy_up_usage_limits(&usage, max_bytes, max_inodes)?;
669        for path in destination_parent_copy_up_paths {
670            self.collect_single_copy_up_usage_limited(&path, &mut usage, max_bytes, max_inodes)?;
671        }
672        self.collect_copy_up_usage_limited(&old_normalized, &mut usage, max_bytes, max_inodes)?;
673
674        Self::check_copy_up_usage_limits(&usage, max_bytes, max_inodes)
675    }
676
677    fn marker_exists(&self, kind: OverlayMarkerKind, path: &str) -> bool {
678        Self::marker_exists_in_upper(self.upper.as_ref(), kind, path)
679    }
680
681    fn marker_exists_in_upper(
682        upper: Option<&MemoryFileSystem>,
683        kind: OverlayMarkerKind,
684        path: &str,
685    ) -> bool {
686        upper.is_some_and(|filesystem| filesystem.exists(&Self::marker_path(kind, path)))
687    }
688
689    fn is_whited_out(&self, path: &str) -> bool {
690        self.marker_exists(OverlayMarkerKind::Whiteout, path)
691    }
692
693    fn ensure_metadata_directories_in_upper(&mut self, path: &str) -> VfsResult<()> {
694        let upper = self.writable_upper(path)?;
695        upper.mkdir(OVERLAY_METADATA_ROOT, true)?;
696        upper.mkdir(OVERLAY_WHITEOUT_DIR, true)?;
697        upper.mkdir(OVERLAY_OPAQUE_DIR, true)?;
698        Ok(())
699    }
700
701    fn set_marker(&mut self, kind: OverlayMarkerKind, path: &str, present: bool) -> VfsResult<()> {
702        let marker_path = Self::marker_path(kind, path);
703        if present {
704            self.ensure_metadata_directories_in_upper(path)?;
705            self.writable_upper(path)?
706                .write_file(&marker_path, Self::normalized(path).into_bytes())?;
707            return Ok(());
708        }
709
710        if self
711            .upper
712            .as_ref()
713            .is_some_and(|upper| upper.exists(&marker_path))
714        {
715            self.writable_upper(path)?.remove_file(&marker_path)?;
716        }
717        Ok(())
718    }
719
720    fn add_whiteout(&mut self, path: &str) -> VfsResult<()> {
721        self.set_marker(OverlayMarkerKind::Whiteout, path, true)
722    }
723
724    fn remove_whiteout(&mut self, path: &str) -> VfsResult<()> {
725        self.set_marker(OverlayMarkerKind::Whiteout, path, false)
726    }
727
728    fn mark_opaque_directory(&mut self, path: &str) -> VfsResult<()> {
729        self.set_marker(OverlayMarkerKind::Opaque, path, true)
730    }
731
732    fn clear_opaque_directory(&mut self, path: &str) -> VfsResult<()> {
733        self.set_marker(OverlayMarkerKind::Opaque, path, false)
734    }
735
736    fn clear_path_metadata(&mut self, path: &str) -> VfsResult<()> {
737        self.remove_whiteout(path)?;
738        self.clear_opaque_directory(path)
739    }
740
741    fn join_path(base: &str, name: &str) -> String {
742        if base == "/" {
743            format!("/{name}")
744        } else {
745            format!("{base}/{name}")
746        }
747    }
748
749    fn rebase_path(path: &str, old_root: &str, new_root: &str) -> String {
750        if path == old_root {
751            return String::from(new_root);
752        }
753
754        format!("{new_root}{}", &path[old_root.len()..])
755    }
756
757    fn read_only_error(path: &str) -> VfsError {
758        VfsError::new("EROFS", format!("read-only filesystem: {path}"))
759    }
760
761    fn entry_not_found(path: &str) -> VfsError {
762        VfsError::new("ENOENT", format!("no such file: {path}"))
763    }
764
765    fn directory_not_found(path: &str) -> VfsError {
766        VfsError::new("ENOENT", format!("no such directory: {path}"))
767    }
768
769    fn already_exists(path: &str) -> VfsError {
770        VfsError::new("EEXIST", format!("file exists: {path}"))
771    }
772
773    fn not_directory(path: &str) -> VfsError {
774        VfsError::new("ENOTDIR", format!("not a directory: {path}"))
775    }
776
777    fn writable_upper(&mut self, path: &str) -> VfsResult<&mut MemoryFileSystem> {
778        if self.writes_locked {
779            return Err(Self::read_only_error(path));
780        }
781        self.upper
782            .as_mut()
783            .ok_or_else(|| Self::read_only_error(path))
784    }
785
786    fn path_exists_in_filesystem(filesystem: &MemoryFileSystem, path: &str) -> bool {
787        filesystem.exists(path)
788    }
789
790    fn has_entry_in_filesystem(filesystem: &MemoryFileSystem, path: &str) -> bool {
791        filesystem.lstat(path).is_ok()
792    }
793
794    fn exists_in_upper(&self, path: &str) -> bool {
795        self.upper
796            .as_ref()
797            .is_some_and(|upper| Self::path_exists_in_filesystem(upper, path))
798    }
799
800    fn has_entry_in_upper(&self, path: &str) -> bool {
801        self.upper
802            .as_ref()
803            .is_some_and(|upper| Self::has_entry_in_filesystem(upper, path))
804    }
805
806    fn find_lower_by_exists(&self, path: &str) -> Option<usize> {
807        self.lowers
808            .iter()
809            .position(|lower| Self::path_exists_in_filesystem(lower, path))
810    }
811
812    fn find_lower_by_entry(&self, path: &str) -> Option<(usize, VirtualStat)> {
813        self.lowers
814            .iter()
815            .enumerate()
816            .find_map(|(index, lower)| lower.lstat(path).ok().map(|stat| (index, stat)))
817    }
818
819    fn merged_lstat(&self, path: &str) -> VfsResult<VirtualStat> {
820        if Self::is_internal_metadata_path(path) {
821            return Err(Self::entry_not_found(path));
822        }
823        if self.is_whited_out(path) {
824            return Err(Self::entry_not_found(path));
825        }
826        if self.has_entry_in_upper(path) {
827            return self
828                .upper
829                .as_ref()
830                .expect("upper must exist when entry exists")
831                .lstat(path);
832        }
833        self.find_lower_by_entry(path)
834            .map(|(_, stat)| stat)
835            .ok_or_else(|| Self::entry_not_found(path))
836    }
837
838    /// `read_link` body without the resolving metadata guard, for use by the
839    /// internal symlink-resolution helpers (`resolve_merged_path` and friends).
840    /// The public `read_link` wraps this with `touches_internal_metadata`;
841    /// resolution must not call back into that wrapper or it would recurse on a
842    /// symlink that points at itself's resolution path.
843    fn read_link_inner(&self, path: &str) -> VfsResult<String> {
844        if Self::is_internal_metadata_path(path) {
845            return Err(Self::entry_not_found(path));
846        }
847        if self.is_whited_out(path) {
848            return Err(Self::entry_not_found(path));
849        }
850        if self.has_entry_in_upper(path) {
851            return self
852                .upper
853                .as_ref()
854                .expect("upper must exist when path exists")
855                .read_link(path);
856        }
857        let Some((index, _)) = self.find_lower_by_entry(path) else {
858            return Err(Self::entry_not_found(path));
859        };
860        self.lowers[index].read_link(path)
861    }
862
863    fn ensure_ancestor_directories_in_upper(&mut self, path: &str) -> VfsResult<()> {
864        if Self::is_internal_metadata_path(path) {
865            return Err(VfsError::permission_denied("mkdir", path));
866        }
867        let normalized = Self::normalized(path);
868        let parts = normalized
869            .split('/')
870            .filter(|part| !part.is_empty())
871            .collect::<Vec<_>>();
872
873        let mut current = String::new();
874        for part in parts.iter().take(parts.len().saturating_sub(1)) {
875            current.push('/');
876            current.push_str(part);
877
878            if self.exists_in_upper(&current) {
879                continue;
880            }
881
882            if let Some(index) = self.find_lower_by_exists(&current) {
883                let stat = self.lowers[index].stat(&current)?;
884                if !stat.is_directory {
885                    return Err(Self::not_directory(&current));
886                }
887
888                let upper = self.writable_upper(&current)?;
889                upper.mkdir(&current, false)?;
890                upper.chmod(&current, stat.mode)?;
891                upper.chown(&current, stat.uid, stat.gid)?;
892                continue;
893            }
894
895            let upper = self.writable_upper(&current)?;
896            upper.mkdir(&current, false)?;
897        }
898
899        Ok(())
900    }
901
902    fn copy_up_path(&mut self, path: &str) -> VfsResult<()> {
903        if self.has_entry_in_upper(path) {
904            return Ok(());
905        }
906
907        self.ensure_ancestor_directories_in_upper(path)?;
908
909        let (lower_index, stat) = self
910            .find_lower_by_entry(path)
911            .ok_or_else(|| Self::entry_not_found(path))?;
912        let xattrs = match self.lowers[lower_index].list_xattrs(path, false) {
913            Ok(names) => names
914                .into_iter()
915                .map(|name| {
916                    self.lowers[lower_index]
917                        .get_xattr(path, &name, false)
918                        .map(|value| (name, value))
919                })
920                .collect::<VfsResult<Vec<_>>>()?,
921            Err(error) if error.code() == "EOPNOTSUPP" => Vec::new(),
922            Err(error) => return Err(error),
923        };
924
925        if stat.is_symbolic_link {
926            let target = self.lowers[lower_index].read_link(path)?;
927            let upper = self.writable_upper(path)?;
928            upper.symlink(&target, path)?;
929            for (name, value) in xattrs {
930                upper.set_xattr(path, &name, value, 0, false)?;
931            }
932            return Ok(());
933        }
934
935        if stat.is_directory {
936            let upper = self.writable_upper(path)?;
937            upper.mkdir(path, false)?;
938            upper.chmod(path, stat.mode)?;
939            upper.chown(path, stat.uid, stat.gid)?;
940            for (name, value) in xattrs {
941                upper.set_xattr(path, &name, value, 0, false)?;
942            }
943            self.mark_opaque_directory(path)?;
944            return Ok(());
945        }
946
947        let data = self.lowers[lower_index].read_file(path)?;
948        let upper = self.writable_upper(path)?;
949        upper.write_file(path, data)?;
950        upper.chmod(path, stat.mode)?;
951        upper.chown(path, stat.uid, stat.gid)?;
952        for (name, value) in xattrs {
953            upper.set_xattr(path, &name, value, 0, false)?;
954        }
955        Ok(())
956    }
957
958    fn materialize_destination_parent_in_upper(&mut self, path: &str) -> VfsResult<()> {
959        if self.has_entry_in_upper(path) {
960            return Ok(());
961        }
962
963        if self
964            .merged_lstat(path)
965            .is_ok_and(|stat| stat.is_symbolic_link)
966        {
967            return self.copy_up_path(path);
968        }
969
970        self.ensure_ancestor_directories_in_upper(path)?;
971        let stat = self.merged_lstat(path)?;
972        if !stat.is_directory || stat.is_symbolic_link {
973            return Err(Self::not_directory(path));
974        }
975
976        let upper = self.writable_upper(path)?;
977        upper.create_dir(path)?;
978        upper.chmod(path, stat.mode)?;
979        upper.chown(path, stat.uid, stat.gid)?;
980        Ok(())
981    }
982
983    fn path_exists_in_merged_view(&self, path: &str) -> bool {
984        if self.is_whited_out(path) {
985            return false;
986        }
987        if self.has_entry_in_upper(path) {
988            return true;
989        }
990        self.find_lower_by_entry(path).is_some()
991    }
992
993    fn not_empty(path: &str) -> VfsError {
994        VfsError::new("ENOTEMPTY", format!("directory not empty, rmdir '{path}'"))
995    }
996
997    fn collect_snapshot_entries(
998        &mut self,
999        path: &str,
1000        entries: &mut Vec<OverlaySnapshotEntry>,
1001    ) -> VfsResult<()> {
1002        let mut pending = vec![(Self::normalized(path), 0usize)];
1003        while let Some((current_path, depth)) = pending.pop() {
1004            if depth > MAX_SNAPSHOT_DEPTH {
1005                return Err(VfsError::new(
1006                    "EINVAL",
1007                    format!("overlay snapshot depth limit exceeded at '{current_path}'"),
1008                ));
1009            }
1010
1011            let stat = self.merged_lstat(&current_path)?;
1012
1013            if stat.is_symbolic_link {
1014                entries.push(OverlaySnapshotEntry {
1015                    path: current_path.clone(),
1016                    stat,
1017                    kind: OverlaySnapshotKind::Symlink(self.read_link_inner(&current_path)?),
1018                });
1019                continue;
1020            }
1021
1022            if stat.is_directory {
1023                entries.push(OverlaySnapshotEntry {
1024                    path: current_path.clone(),
1025                    stat,
1026                    kind: OverlaySnapshotKind::Directory,
1027                });
1028
1029                let children = self.read_dir_with_types_inner(&current_path)?;
1030                for entry in children.into_iter().rev() {
1031                    pending.push((Self::join_path(&current_path, &entry.name), depth + 1));
1032                }
1033                continue;
1034            }
1035
1036            entries.push(OverlaySnapshotEntry {
1037                path: current_path.clone(),
1038                stat,
1039                kind: OverlaySnapshotKind::File(self.read_file(&current_path)?),
1040            });
1041        }
1042        Ok(())
1043    }
1044
1045    fn remove_snapshot_entries(&mut self, entries: &[OverlaySnapshotEntry]) -> VfsResult<()> {
1046        for entry in entries.iter().rev() {
1047            if self.has_entry_in_upper(&entry.path) {
1048                match entry.kind {
1049                    OverlaySnapshotKind::Directory => {
1050                        self.writable_upper(&entry.path)?.remove_dir(&entry.path)?;
1051                    }
1052                    OverlaySnapshotKind::File(_) | OverlaySnapshotKind::Symlink(_) => {
1053                        self.writable_upper(&entry.path)?.remove_file(&entry.path)?;
1054                    }
1055                }
1056            }
1057
1058            if self.find_lower_by_entry(&entry.path).is_some() {
1059                self.clear_opaque_directory(&entry.path)?;
1060                self.add_whiteout(&entry.path)?;
1061            } else {
1062                self.clear_path_metadata(&entry.path)?;
1063            }
1064        }
1065
1066        Ok(())
1067    }
1068
1069    fn directory_has_raw_children(&mut self, path: &str) -> VfsResult<bool> {
1070        let normalized = Self::normalized(path);
1071        let mut directory_exists = false;
1072
1073        if let Some(upper) = self.upper.as_mut() {
1074            if let Ok(entries) = upper.read_dir(&normalized) {
1075                directory_exists = true;
1076                if entries.into_iter().any(|entry| {
1077                    !Self::should_ignore_raw_directory_entry(Some(&*upper), &normalized, &entry)
1078                }) {
1079                    return Ok(true);
1080                }
1081            }
1082        }
1083
1084        let upper = self.upper.as_ref();
1085        for lower in self.lowers.iter_mut().rev() {
1086            if let Ok(entries) = lower.read_dir(&normalized) {
1087                directory_exists = true;
1088                if entries.into_iter().any(|entry| {
1089                    !Self::should_ignore_raw_directory_entry(upper, &normalized, &entry)
1090                }) {
1091                    return Ok(true);
1092                }
1093            }
1094        }
1095
1096        if !directory_exists {
1097            return Err(Self::directory_not_found(path));
1098        }
1099
1100        Ok(false)
1101    }
1102
1103    fn read_dir_with_types_inner(&mut self, path: &str) -> VfsResult<Vec<VirtualDirEntry>> {
1104        if self.is_whited_out(path) {
1105            return Err(Self::directory_not_found(path));
1106        }
1107
1108        let normalized = Self::normalized(path);
1109        let mut directory_exists = false;
1110        let mut entries = Vec::<VirtualDirEntry>::new();
1111        let mut seen = BTreeSet::<String>::new();
1112        let upper = self.upper.as_ref();
1113        let include_lowers = !Self::marker_exists_in_upper(upper, OverlayMarkerKind::Opaque, path);
1114
1115        if include_lowers {
1116            for lower in self.lowers.iter_mut().rev() {
1117                if let Ok(lower_entries) = lower.read_dir_with_types(path) {
1118                    directory_exists = true;
1119                    for entry in lower_entries {
1120                        if entry.name == "."
1121                            || entry.name == ".."
1122                            || Self::should_hide_directory_entry(path, &entry.name)
1123                        {
1124                            continue;
1125                        }
1126                        let child_path = if normalized == "/" {
1127                            format!("/{}", entry.name)
1128                        } else {
1129                            format!("{normalized}/{}", entry.name)
1130                        };
1131                        if Self::marker_exists_in_upper(
1132                            upper,
1133                            OverlayMarkerKind::Whiteout,
1134                            &child_path,
1135                        ) || seen.contains(&entry.name)
1136                        {
1137                            continue;
1138                        }
1139                        seen.insert(entry.name.clone());
1140                        entries.push(entry);
1141                    }
1142                }
1143            }
1144        }
1145
1146        if let Some(upper) = self.upper.as_mut() {
1147            if let Ok(upper_entries) = upper.read_dir_with_types(path) {
1148                directory_exists = true;
1149                for entry in upper_entries {
1150                    if entry.name == "."
1151                        || entry.name == ".."
1152                        || Self::should_hide_directory_entry(path, &entry.name)
1153                    {
1154                        continue;
1155                    }
1156                    if let Some(index) = entries
1157                        .iter()
1158                        .position(|existing| existing.name == entry.name)
1159                    {
1160                        entries[index] = entry;
1161                    } else {
1162                        seen.insert(entry.name.clone());
1163                        entries.push(entry);
1164                    }
1165                }
1166            }
1167        }
1168
1169        if !directory_exists {
1170            return Err(Self::directory_not_found(path));
1171        }
1172
1173        Ok(entries)
1174    }
1175
1176    fn marker_paths_in_upper(&mut self, kind: OverlayMarkerKind) -> VfsResult<Vec<String>> {
1177        let Some(upper) = self.upper.as_mut() else {
1178            return Ok(Vec::new());
1179        };
1180
1181        let marker_dir = Self::marker_directory(kind);
1182        let entries = match upper.read_dir(marker_dir) {
1183            Ok(entries) => entries,
1184            Err(error) if error.code() == "ENOENT" => return Ok(Vec::new()),
1185            Err(error) => return Err(error),
1186        };
1187
1188        let mut marker_paths = Vec::new();
1189        for entry in entries {
1190            if entry == "." || entry == ".." {
1191                continue;
1192            }
1193
1194            let marker_file = Self::join_path(marker_dir, &entry);
1195            let marker_path =
1196                String::from_utf8(upper.read_file(&marker_file).map_err(|_| {
1197                    VfsError::io(format!("invalid overlay marker '{marker_file}'"))
1198                })?)
1199                .map_err(|_| VfsError::io(format!("invalid overlay marker '{marker_file}'")))?;
1200            marker_paths.push(Self::normalized(&marker_path));
1201        }
1202
1203        Ok(marker_paths)
1204    }
1205
1206    fn path_in_subtree(path: &str, root: &str) -> bool {
1207        path == root || path.starts_with(&(String::from(root) + "/"))
1208    }
1209
1210    fn clear_subtree_metadata(&mut self, path: &str) -> VfsResult<()> {
1211        let normalized = Self::normalized(path);
1212        for kind in [OverlayMarkerKind::Whiteout, OverlayMarkerKind::Opaque] {
1213            for marker_path in self.marker_paths_in_upper(kind)? {
1214                if Self::path_in_subtree(&marker_path, &normalized) {
1215                    self.set_marker(kind, &marker_path, false)?;
1216                }
1217            }
1218        }
1219        Ok(())
1220    }
1221
1222    fn copy_subtree_metadata(&mut self, old_root: &str, new_root: &str) -> VfsResult<()> {
1223        let old_normalized = Self::normalized(old_root);
1224        let new_normalized = Self::normalized(new_root);
1225
1226        for kind in [OverlayMarkerKind::Whiteout, OverlayMarkerKind::Opaque] {
1227            for marker_path in self.marker_paths_in_upper(kind)? {
1228                if Self::path_in_subtree(&marker_path, &old_normalized) {
1229                    let destination =
1230                        Self::rebase_path(&marker_path, &old_normalized, &new_normalized);
1231                    self.set_marker(kind, &destination, true)?;
1232                }
1233            }
1234        }
1235
1236        Ok(())
1237    }
1238
1239    fn stage_snapshot_entries_in_upper(
1240        &mut self,
1241        entries: &[OverlaySnapshotEntry],
1242    ) -> VfsResult<()> {
1243        for entry in entries {
1244            match &entry.kind {
1245                OverlaySnapshotKind::Directory => {
1246                    if !self.has_entry_in_upper(&entry.path) {
1247                        self.ensure_ancestor_directories_in_upper(&entry.path)?;
1248                        self.writable_upper(&entry.path)?.create_dir(&entry.path)?;
1249                    }
1250                    self.writable_upper(&entry.path)?
1251                        .chmod(&entry.path, entry.stat.mode)?;
1252                    self.writable_upper(&entry.path)?.chown(
1253                        &entry.path,
1254                        entry.stat.uid,
1255                        entry.stat.gid,
1256                    )?;
1257                    self.mark_opaque_directory(&entry.path)?;
1258                }
1259                OverlaySnapshotKind::File(data) => {
1260                    if self.has_entry_in_upper(&entry.path) {
1261                        continue;
1262                    }
1263                    self.ensure_ancestor_directories_in_upper(&entry.path)?;
1264                    self.writable_upper(&entry.path)?
1265                        .write_file(&entry.path, data.clone())?;
1266                    self.writable_upper(&entry.path)?
1267                        .chmod(&entry.path, entry.stat.mode)?;
1268                    self.writable_upper(&entry.path)?.chown(
1269                        &entry.path,
1270                        entry.stat.uid,
1271                        entry.stat.gid,
1272                    )?;
1273                }
1274                OverlaySnapshotKind::Symlink(target) => {
1275                    if self.has_entry_in_upper(&entry.path) {
1276                        continue;
1277                    }
1278                    self.ensure_ancestor_directories_in_upper(&entry.path)?;
1279                    self.writable_upper(&entry.path)?
1280                        .symlink(target, &entry.path)?;
1281                }
1282            }
1283        }
1284
1285        Ok(())
1286    }
1287}
1288
1289fn sync_upper_root_metadata(upper: &mut MemoryFileSystem, lowers: &[MemoryFileSystem]) {
1290    let Some(root_stat) = lowers.iter().find_map(|lower| lower.lstat("/").ok()) else {
1291        return;
1292    };
1293
1294    upper
1295        .chmod("/", root_stat.mode)
1296        .expect("overlay upper root should exist");
1297    upper
1298        .chown("/", root_stat.uid, root_stat.gid)
1299        .expect("overlay upper root should exist");
1300}
1301
1302impl VirtualFileSystem for OverlayFileSystem {
1303    fn read_file(&mut self, path: &str) -> VfsResult<Vec<u8>> {
1304        if self.touches_internal_metadata(path) {
1305            return Err(Self::entry_not_found(path));
1306        }
1307        if self.is_whited_out(path) {
1308            return Err(Self::entry_not_found(path));
1309        }
1310        if self.exists_in_upper(path) {
1311            return self
1312                .upper
1313                .as_mut()
1314                .expect("upper must exist when path exists")
1315                .read_file(path);
1316        }
1317        let Some(index) = self.find_lower_by_exists(path) else {
1318            return Err(Self::entry_not_found(path));
1319        };
1320        self.lowers[index].read_file(path)
1321    }
1322
1323    fn read_dir(&mut self, path: &str) -> VfsResult<Vec<String>> {
1324        if self.touches_internal_metadata(path) {
1325            return Err(Self::directory_not_found(path));
1326        }
1327        if self.is_whited_out(path) {
1328            return Err(Self::directory_not_found(path));
1329        }
1330
1331        let normalized = Self::normalized(path);
1332        let mut directory_exists = false;
1333        let mut entries = BTreeSet::new();
1334        let upper = self.upper.as_ref();
1335        let include_lowers = !Self::marker_exists_in_upper(upper, OverlayMarkerKind::Opaque, path);
1336
1337        if include_lowers {
1338            for lower in self.lowers.iter_mut().rev() {
1339                if let Ok(lower_entries) = lower.read_dir(path) {
1340                    directory_exists = true;
1341                    for entry in lower_entries {
1342                        if entry == "."
1343                            || entry == ".."
1344                            || Self::should_hide_directory_entry(path, &entry)
1345                        {
1346                            continue;
1347                        }
1348                        let child_path = if normalized == "/" {
1349                            format!("/{entry}")
1350                        } else {
1351                            format!("{normalized}/{entry}")
1352                        };
1353                        if !Self::marker_exists_in_upper(
1354                            upper,
1355                            OverlayMarkerKind::Whiteout,
1356                            &child_path,
1357                        ) {
1358                            entries.insert(entry);
1359                        }
1360                    }
1361                }
1362            }
1363        }
1364
1365        if let Some(upper) = self.upper.as_mut() {
1366            if let Ok(upper_entries) = upper.read_dir(path) {
1367                directory_exists = true;
1368                for entry in upper_entries {
1369                    if entry == "."
1370                        || entry == ".."
1371                        || Self::should_hide_directory_entry(path, &entry)
1372                    {
1373                        continue;
1374                    }
1375                    entries.insert(entry);
1376                }
1377            }
1378        }
1379
1380        if !directory_exists {
1381            return Err(Self::directory_not_found(path));
1382        }
1383
1384        Ok(entries.into_iter().collect())
1385    }
1386
1387    fn read_dir_limited(&mut self, path: &str, max_entries: usize) -> VfsResult<Vec<String>> {
1388        if self.touches_internal_metadata(path) {
1389            return Err(Self::directory_not_found(path));
1390        }
1391        if self.is_whited_out(path) {
1392            return Err(Self::directory_not_found(path));
1393        }
1394
1395        let normalized = Self::normalized(path);
1396        let mut directory_exists = false;
1397        let mut entries = BTreeSet::new();
1398        let upper = self.upper.as_ref();
1399        let include_lowers = !Self::marker_exists_in_upper(upper, OverlayMarkerKind::Opaque, path);
1400
1401        if include_lowers {
1402            for lower in self.lowers.iter_mut().rev() {
1403                let lower_entries = match lower.read_dir_filtered_limited(
1404                    path,
1405                    max_entries.saturating_sub(entries.len()),
1406                    |entry| {
1407                        if entry == "."
1408                            || entry == ".."
1409                            || Self::should_hide_directory_entry(path, entry)
1410                        {
1411                            return false;
1412                        }
1413                        let child_path = if normalized == "/" {
1414                            format!("/{entry}")
1415                        } else {
1416                            format!("{normalized}/{entry}")
1417                        };
1418                        !Self::marker_exists_in_upper(
1419                            upper,
1420                            OverlayMarkerKind::Whiteout,
1421                            &child_path,
1422                        ) && !entries.contains(entry)
1423                    },
1424                ) {
1425                    Ok(entries) => entries,
1426                    Err(error) if error.code() == "ENOENT" || error.code() == "ENOTDIR" => {
1427                        continue;
1428                    }
1429                    Err(error) => return Err(error),
1430                };
1431                directory_exists = true;
1432                for entry in lower_entries {
1433                    entries.insert(entry);
1434                    if entries.len() > max_entries {
1435                        return Err(VfsError::new(
1436                            "ENOMEM",
1437                            format!(
1438                                "directory listing for '{path}' exceeds configured limit of {max_entries} entries"
1439                            ),
1440                        ));
1441                    }
1442                }
1443            }
1444        }
1445
1446        if let Some(upper) = self.upper.as_mut() {
1447            let upper_entries = match upper.read_dir_filtered_limited(
1448                path,
1449                max_entries.saturating_sub(entries.len()),
1450                |entry| {
1451                    entry != "."
1452                        && entry != ".."
1453                        && !Self::should_hide_directory_entry(path, entry)
1454                        && !entries.contains(entry)
1455                },
1456            ) {
1457                Ok(entries) => entries,
1458                Err(error) if error.code() == "ENOENT" => Vec::new(),
1459                Err(error) => return Err(error),
1460            };
1461            directory_exists = directory_exists || upper.exists(path);
1462            for entry in upper_entries {
1463                if entry == "." || entry == ".." || Self::should_hide_directory_entry(path, &entry)
1464                {
1465                    continue;
1466                }
1467                entries.insert(entry);
1468                if entries.len() > max_entries {
1469                    return Err(VfsError::new(
1470                        "ENOMEM",
1471                        format!(
1472                            "directory listing for '{path}' exceeds configured limit of {max_entries} entries"
1473                        ),
1474                    ));
1475                }
1476            }
1477        }
1478
1479        if !directory_exists {
1480            return Err(Self::directory_not_found(path));
1481        }
1482
1483        Ok(entries.into_iter().collect())
1484    }
1485
1486    fn read_dir_with_types(&mut self, path: &str) -> VfsResult<Vec<VirtualDirEntry>> {
1487        if self.touches_internal_metadata(path) {
1488            return Err(Self::directory_not_found(path));
1489        }
1490        self.read_dir_with_types_inner(path)
1491    }
1492
1493    fn write_file(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<()> {
1494        if self.touches_internal_metadata(path) {
1495            return Err(VfsError::permission_denied("open", path));
1496        }
1497        self.clear_path_metadata(path)?;
1498        if self.find_lower_by_entry(path).is_some() {
1499            self.copy_up_path(path)?;
1500        } else {
1501            self.ensure_ancestor_directories_in_upper(path)?;
1502        }
1503        self.writable_upper(path)?.write_file(path, content.into())
1504    }
1505
1506    fn create_file_exclusive(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<()> {
1507        if self.touches_internal_metadata(path) {
1508            return Err(VfsError::permission_denied("open", path));
1509        }
1510        self.clear_path_metadata(path)?;
1511        if self.path_exists_in_merged_view(path) {
1512            return Err(Self::already_exists(path));
1513        }
1514        self.ensure_ancestor_directories_in_upper(path)?;
1515        self.writable_upper(path)?
1516            .create_file_exclusive(path, content.into())
1517    }
1518
1519    fn append_file(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<u64> {
1520        if self.touches_internal_metadata(path) {
1521            return Err(VfsError::permission_denied("open", path));
1522        }
1523        self.clear_path_metadata(path)?;
1524        if self.find_lower_by_entry(path).is_some() {
1525            self.copy_up_path(path)?;
1526        } else {
1527            self.ensure_ancestor_directories_in_upper(path)?;
1528        }
1529        self.writable_upper(path)?.append_file(path, content.into())
1530    }
1531
1532    fn create_dir(&mut self, path: &str) -> VfsResult<()> {
1533        if self.touches_internal_metadata(path) {
1534            return Err(VfsError::permission_denied("mkdir", path));
1535        }
1536        self.clear_path_metadata(path)?;
1537        if self.path_exists_in_merged_view(path) {
1538            return Err(Self::already_exists(path));
1539        }
1540        self.ensure_ancestor_directories_in_upper(path)?;
1541        self.writable_upper(path)?.create_dir(path)
1542    }
1543
1544    fn mkdir(&mut self, path: &str, recursive: bool) -> VfsResult<()> {
1545        if self.touches_internal_metadata(path) {
1546            return Err(VfsError::permission_denied("mkdir", path));
1547        }
1548        self.clear_path_metadata(path)?;
1549        if self.path_exists_in_merged_view(path) {
1550            let stat = self.merged_lstat(path)?;
1551            if recursive && stat.is_directory && !stat.is_symbolic_link {
1552                return Ok(());
1553            }
1554            return Err(Self::already_exists(path));
1555        }
1556        self.ensure_ancestor_directories_in_upper(path)?;
1557        self.writable_upper(path)?.mkdir(path, recursive)
1558    }
1559
1560    fn mknod(&mut self, path: &str, mode: u32, rdev: u64) -> VfsResult<()> {
1561        if self.touches_internal_metadata(path) {
1562            return Err(VfsError::permission_denied("mknod", path));
1563        }
1564        self.clear_path_metadata(path)?;
1565        if self.path_exists_in_merged_view(path) {
1566            return Err(Self::already_exists(path));
1567        }
1568        self.ensure_ancestor_directories_in_upper(path)?;
1569        self.writable_upper(path)?.mknod(path, mode, rdev)
1570    }
1571
1572    fn exists(&self, path: &str) -> bool {
1573        if self.touches_internal_metadata(path) {
1574            return false;
1575        }
1576        self.path_exists_in_merged_view(path)
1577    }
1578
1579    fn stat(&mut self, path: &str) -> VfsResult<VirtualStat> {
1580        if self.touches_internal_metadata(path) {
1581            return Err(Self::entry_not_found(path));
1582        }
1583        if self.is_whited_out(path) {
1584            return Err(Self::entry_not_found(path));
1585        }
1586        if self.exists_in_upper(path) {
1587            return self
1588                .upper
1589                .as_mut()
1590                .expect("upper must exist when path exists")
1591                .stat(path);
1592        }
1593        let Some(index) = self.find_lower_by_exists(path) else {
1594            return Err(Self::entry_not_found(path));
1595        };
1596        self.lowers[index].stat(path)
1597    }
1598
1599    fn remove_file(&mut self, path: &str) -> VfsResult<()> {
1600        if self.entry_touches_internal_metadata(path) {
1601            return Err(VfsError::permission_denied("unlink", path));
1602        }
1603        if self.is_whited_out(path) {
1604            return Err(Self::entry_not_found(path));
1605        }
1606        // POSIX unlink(2) removes the directory entry itself and never follows
1607        // a symlink leaf, so existence must be checked with lstat semantics.
1608        // `exists()` resolves symlinks, which made dangling symlinks (for
1609        // example git's `.git/tXXXXXX` symlink-support probe) unremovable:
1610        // the entry stayed listed by readdir while unlink reported ENOENT.
1611        let lower_exists = self.find_lower_by_entry(path).is_some();
1612        let upper_exists = self.has_entry_in_upper(path);
1613        if !lower_exists && !upper_exists {
1614            return Err(Self::entry_not_found(path));
1615        }
1616        if upper_exists {
1617            self.writable_upper(path)?.remove_file(path)?;
1618        } else {
1619            self.writable_upper(path)?;
1620        }
1621        self.clear_opaque_directory(path)?;
1622        self.add_whiteout(path)?;
1623        Ok(())
1624    }
1625
1626    fn remove_dir(&mut self, path: &str) -> VfsResult<()> {
1627        let normalized = Self::normalized(path);
1628        if self.touches_internal_metadata(&normalized) {
1629            return Err(VfsError::permission_denied("rmdir", path));
1630        }
1631        if normalized == "/" {
1632            return Err(VfsError::permission_denied("rmdir", path));
1633        }
1634
1635        let stat = match self.merged_lstat(path) {
1636            Ok(stat) => stat,
1637            Err(error) if error.code() == "ENOENT" => return Err(Self::directory_not_found(path)),
1638            Err(error) => return Err(error),
1639        };
1640
1641        if !stat.is_directory || stat.is_symbolic_link {
1642            return Err(Self::not_directory(path));
1643        }
1644
1645        if self.directory_has_raw_children(path)? {
1646            return Err(Self::not_empty(path));
1647        }
1648
1649        let lower_exists = self.find_lower_by_entry(path).is_some();
1650        let upper_exists = self.has_entry_in_upper(path);
1651        if upper_exists {
1652            self.writable_upper(path)?.remove_dir(&normalized)?;
1653        } else {
1654            self.writable_upper(path)?;
1655        }
1656        if lower_exists {
1657            self.clear_opaque_directory(path)?;
1658            self.add_whiteout(path)?;
1659        } else {
1660            self.clear_path_metadata(path)?;
1661        }
1662        Ok(())
1663    }
1664
1665    fn rename(&mut self, old_path: &str, new_path: &str) -> VfsResult<()> {
1666        let old_normalized = Self::normalized(old_path);
1667        let new_normalized = Self::normalized(new_path);
1668        if self.touches_internal_metadata(&old_normalized)
1669            || self.touches_internal_metadata(&new_normalized)
1670        {
1671            return Err(VfsError::permission_denied("rename", old_path));
1672        }
1673
1674        if old_normalized == "/" {
1675            return Err(VfsError::permission_denied("rename", old_path));
1676        }
1677
1678        if old_normalized == new_normalized {
1679            return Ok(());
1680        }
1681
1682        let source_stat = self.merged_lstat(old_path)?;
1683        self.validate_destination_parent(&new_normalized)?;
1684        let resolved_new_normalized = self.resolved_destination_path(&new_normalized)?;
1685
1686        if old_normalized == resolved_new_normalized {
1687            return Ok(());
1688        }
1689
1690        if source_stat.is_directory
1691            && resolved_new_normalized.starts_with(&(old_normalized.clone() + "/"))
1692        {
1693            return Err(VfsError::new(
1694                "EINVAL",
1695                format!(
1696                    "cannot move '{}' into its own descendant '{}'",
1697                    old_path, new_path
1698                ),
1699            ));
1700        }
1701
1702        for path in self.destination_parent_copy_up_paths(&new_normalized)? {
1703            self.materialize_destination_parent_in_upper(&path)?;
1704        }
1705
1706        let mut snapshot_entries = Vec::new();
1707        self.collect_snapshot_entries(&old_normalized, &mut snapshot_entries)?;
1708
1709        self.clear_path_metadata(&resolved_new_normalized)?;
1710        self.clear_subtree_metadata(&resolved_new_normalized)?;
1711        if let Ok(destination_stat) = self.merged_lstat(&resolved_new_normalized) {
1712            if destination_stat.is_directory
1713                && !destination_stat.is_symbolic_link
1714                && self.directory_has_visible_entries_limited(&resolved_new_normalized)?
1715            {
1716                return Err(Self::not_empty(&resolved_new_normalized));
1717            }
1718
1719            if self.has_entry_in_upper(&resolved_new_normalized) {
1720                if destination_stat.is_directory && !destination_stat.is_symbolic_link {
1721                    self.writable_upper(&resolved_new_normalized)?
1722                        .remove_dir(&resolved_new_normalized)?;
1723                } else {
1724                    self.writable_upper(&resolved_new_normalized)?
1725                        .remove_file(&resolved_new_normalized)?;
1726                }
1727            }
1728        }
1729
1730        self.stage_snapshot_entries_in_upper(&snapshot_entries)?;
1731        self.copy_subtree_metadata(&old_normalized, &resolved_new_normalized)?;
1732        self.writable_upper(&old_normalized)?
1733            .rename(&old_normalized, &resolved_new_normalized)?;
1734        self.remove_snapshot_entries(&snapshot_entries)
1735    }
1736
1737    fn realpath(&self, path: &str) -> VfsResult<String> {
1738        if self.touches_internal_metadata(path) {
1739            return Err(Self::entry_not_found(path));
1740        }
1741        if self.is_whited_out(path) {
1742            return Err(Self::entry_not_found(path));
1743        }
1744        if self.exists_in_upper(path) {
1745            return self
1746                .upper
1747                .as_ref()
1748                .expect("upper must exist when path exists")
1749                .realpath(path);
1750        }
1751        let Some(index) = self.find_lower_by_exists(path) else {
1752            return Err(Self::entry_not_found(path));
1753        };
1754        self.lowers[index].realpath(path)
1755    }
1756
1757    fn symlink(&mut self, target: &str, link_path: &str) -> VfsResult<()> {
1758        if self.touches_internal_metadata(link_path) {
1759            return Err(VfsError::permission_denied("symlink", link_path));
1760        }
1761        self.clear_path_metadata(link_path)?;
1762        self.ensure_ancestor_directories_in_upper(link_path)?;
1763        self.writable_upper(link_path)?.symlink(target, link_path)
1764    }
1765
1766    fn read_link(&self, path: &str) -> VfsResult<String> {
1767        if self.touches_internal_metadata(path) {
1768            return Err(Self::entry_not_found(path));
1769        }
1770        if self.is_whited_out(path) {
1771            return Err(Self::entry_not_found(path));
1772        }
1773        if self.has_entry_in_upper(path) {
1774            return self
1775                .upper
1776                .as_ref()
1777                .expect("upper must exist when path exists")
1778                .read_link(path);
1779        }
1780        let Some((index, _)) = self.find_lower_by_entry(path) else {
1781            return Err(Self::entry_not_found(path));
1782        };
1783        self.lowers[index].read_link(path)
1784    }
1785
1786    fn lstat(&self, path: &str) -> VfsResult<VirtualStat> {
1787        if self.touches_internal_metadata(path) {
1788            return Err(Self::entry_not_found(path));
1789        }
1790        if self.is_whited_out(path) {
1791            return Err(Self::entry_not_found(path));
1792        }
1793        if self.has_entry_in_upper(path) {
1794            return self
1795                .upper
1796                .as_ref()
1797                .expect("upper must exist when path exists")
1798                .lstat(path);
1799        }
1800        self.find_lower_by_entry(path)
1801            .map(|(_, stat)| stat)
1802            .ok_or_else(|| Self::entry_not_found(path))
1803    }
1804
1805    fn link(&mut self, old_path: &str, new_path: &str) -> VfsResult<()> {
1806        if self.touches_internal_metadata(old_path) || self.touches_internal_metadata(new_path) {
1807            return Err(VfsError::permission_denied("link", new_path));
1808        }
1809        self.clear_path_metadata(new_path)?;
1810        self.copy_up_path(old_path)?;
1811        self.ensure_ancestor_directories_in_upper(new_path)?;
1812        self.writable_upper(new_path)?.link(old_path, new_path)
1813    }
1814
1815    fn chmod(&mut self, path: &str, mode: u32) -> VfsResult<()> {
1816        if self.touches_internal_metadata(path) {
1817            return Err(VfsError::permission_denied("chmod", path));
1818        }
1819        if self.is_whited_out(path) {
1820            return Err(Self::entry_not_found(path));
1821        }
1822        if !self.exists_in_upper(path) {
1823            self.copy_up_path(path)?;
1824        }
1825        self.writable_upper(path)?.chmod(path, mode)
1826    }
1827
1828    fn chown(&mut self, path: &str, uid: u32, gid: u32) -> VfsResult<()> {
1829        self.chown_spec(path, uid, gid, true)
1830    }
1831
1832    fn chown_spec(
1833        &mut self,
1834        path: &str,
1835        uid: u32,
1836        gid: u32,
1837        follow_symlinks: bool,
1838    ) -> VfsResult<()> {
1839        if self.touches_internal_metadata(path) {
1840            return Err(VfsError::permission_denied("chown", path));
1841        }
1842        if self.is_whited_out(path) {
1843            return Err(Self::entry_not_found(path));
1844        }
1845        if !self.exists_in_upper(path) {
1846            self.copy_up_path(path)?;
1847        }
1848        self.writable_upper(path)?
1849            .chown_spec(path, uid, gid, follow_symlinks)
1850    }
1851
1852    fn lchown(&mut self, path: &str, uid: u32, gid: u32) -> VfsResult<()> {
1853        if self.touches_internal_metadata(path) {
1854            return Err(VfsError::permission_denied("lchown", path));
1855        }
1856        if self.is_whited_out(path) {
1857            return Err(Self::entry_not_found(path));
1858        }
1859        if !self.has_entry_in_upper(path) {
1860            self.copy_up_path(path)?;
1861        }
1862        self.writable_upper(path)?.lchown(path, uid, gid)
1863    }
1864
1865    fn get_xattr(&mut self, path: &str, name: &str, follow_symlinks: bool) -> VfsResult<Vec<u8>> {
1866        if self.touches_internal_metadata(path) || self.is_whited_out(path) {
1867            return Err(Self::entry_not_found(path));
1868        }
1869        let upper_exists = if follow_symlinks {
1870            self.exists_in_upper(path)
1871        } else {
1872            self.has_entry_in_upper(path)
1873        };
1874        if upper_exists {
1875            return self
1876                .upper
1877                .as_mut()
1878                .expect("upper must exist when path exists")
1879                .get_xattr(path, name, follow_symlinks);
1880        }
1881        let lower_index = if follow_symlinks {
1882            self.find_lower_by_exists(path)
1883        } else {
1884            self.find_lower_by_entry(path).map(|(index, _)| index)
1885        }
1886        .ok_or_else(|| Self::entry_not_found(path))?;
1887        self.lowers[lower_index].get_xattr(path, name, follow_symlinks)
1888    }
1889
1890    fn list_xattrs(&mut self, path: &str, follow_symlinks: bool) -> VfsResult<Vec<String>> {
1891        if self.touches_internal_metadata(path) || self.is_whited_out(path) {
1892            return Err(Self::entry_not_found(path));
1893        }
1894        let upper_exists = if follow_symlinks {
1895            self.exists_in_upper(path)
1896        } else {
1897            self.has_entry_in_upper(path)
1898        };
1899        if upper_exists {
1900            return self
1901                .upper
1902                .as_mut()
1903                .expect("upper must exist when path exists")
1904                .list_xattrs(path, follow_symlinks);
1905        }
1906        let lower_index = if follow_symlinks {
1907            self.find_lower_by_exists(path)
1908        } else {
1909            self.find_lower_by_entry(path).map(|(index, _)| index)
1910        }
1911        .ok_or_else(|| Self::entry_not_found(path))?;
1912        self.lowers[lower_index].list_xattrs(path, follow_symlinks)
1913    }
1914
1915    fn set_xattr(
1916        &mut self,
1917        path: &str,
1918        name: &str,
1919        value: Vec<u8>,
1920        flags: u32,
1921        follow_symlinks: bool,
1922    ) -> VfsResult<()> {
1923        if self.touches_internal_metadata(path) {
1924            return Err(VfsError::permission_denied("setxattr", path));
1925        }
1926        if self.is_whited_out(path) {
1927            return Err(Self::entry_not_found(path));
1928        }
1929        let upper_exists = if follow_symlinks {
1930            self.exists_in_upper(path)
1931        } else {
1932            self.has_entry_in_upper(path)
1933        };
1934        if !upper_exists {
1935            self.copy_up_path(path)?;
1936        }
1937        self.writable_upper(path)?
1938            .set_xattr(path, name, value, flags, follow_symlinks)
1939    }
1940
1941    fn remove_xattr(&mut self, path: &str, name: &str, follow_symlinks: bool) -> VfsResult<()> {
1942        if self.touches_internal_metadata(path) {
1943            return Err(VfsError::permission_denied("removexattr", path));
1944        }
1945        if self.is_whited_out(path) {
1946            return Err(Self::entry_not_found(path));
1947        }
1948        let upper_exists = if follow_symlinks {
1949            self.exists_in_upper(path)
1950        } else {
1951            self.has_entry_in_upper(path)
1952        };
1953        if !upper_exists {
1954            self.copy_up_path(path)?;
1955        }
1956        self.writable_upper(path)?
1957            .remove_xattr(path, name, follow_symlinks)
1958    }
1959
1960    fn utimes(&mut self, path: &str, atime_ms: u64, mtime_ms: u64) -> VfsResult<()> {
1961        if self.touches_internal_metadata(path) {
1962            return Err(VfsError::permission_denied("utime", path));
1963        }
1964        if self.is_whited_out(path) {
1965            return Err(Self::entry_not_found(path));
1966        }
1967        if !self.exists_in_upper(path) {
1968            self.copy_up_path(path)?;
1969        }
1970        self.writable_upper(path)?.utimes(path, atime_ms, mtime_ms)
1971    }
1972
1973    fn utimes_spec(
1974        &mut self,
1975        path: &str,
1976        atime: VirtualUtimeSpec,
1977        mtime: VirtualUtimeSpec,
1978        follow_symlinks: bool,
1979    ) -> VfsResult<()> {
1980        if self.touches_internal_metadata(path) {
1981            return Err(VfsError::permission_denied("utime", path));
1982        }
1983        if self.is_whited_out(path) {
1984            return Err(Self::entry_not_found(path));
1985        }
1986        if !self.exists_in_upper(path) {
1987            self.copy_up_path(path)?;
1988        }
1989        self.writable_upper(path)?
1990            .utimes_spec(path, atime, mtime, follow_symlinks)
1991    }
1992
1993    fn truncate(&mut self, path: &str, length: u64) -> VfsResult<()> {
1994        if self.touches_internal_metadata(path) {
1995            return Err(VfsError::permission_denied("truncate", path));
1996        }
1997        if self.is_whited_out(path) {
1998            return Err(Self::entry_not_found(path));
1999        }
2000        if !self.exists_in_upper(path) {
2001            self.copy_up_path(path)?;
2002        }
2003        self.writable_upper(path)?.truncate(path, length)
2004    }
2005
2006    fn allocate(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
2007        if self.touches_internal_metadata(path) {
2008            return Err(VfsError::permission_denied("fallocate", path));
2009        }
2010        if self.is_whited_out(path) {
2011            return Err(Self::entry_not_found(path));
2012        }
2013        if !self.exists_in_upper(path) {
2014            self.copy_up_path(path)?;
2015        }
2016        self.writable_upper(path)?.allocate(path, offset, length)
2017    }
2018
2019    fn insert_range(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
2020        if self.touches_internal_metadata(path) {
2021            return Err(VfsError::permission_denied("fallocate", path));
2022        }
2023        if self.is_whited_out(path) {
2024            return Err(Self::entry_not_found(path));
2025        }
2026        if !self.exists_in_upper(path) {
2027            self.copy_up_path(path)?;
2028        }
2029        self.writable_upper(path)?
2030            .insert_range(path, offset, length)
2031    }
2032
2033    fn collapse_range(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
2034        if self.touches_internal_metadata(path) {
2035            return Err(VfsError::permission_denied("fallocate", path));
2036        }
2037        if self.is_whited_out(path) {
2038            return Err(Self::entry_not_found(path));
2039        }
2040        if !self.exists_in_upper(path) {
2041            self.copy_up_path(path)?;
2042        }
2043        self.writable_upper(path)?
2044            .collapse_range(path, offset, length)
2045    }
2046
2047    fn zero_range(
2048        &mut self,
2049        path: &str,
2050        offset: u64,
2051        length: u64,
2052        keep_size: bool,
2053    ) -> VfsResult<()> {
2054        if self.touches_internal_metadata(path) {
2055            return Err(VfsError::permission_denied("fallocate", path));
2056        }
2057        if self.is_whited_out(path) {
2058            return Err(Self::entry_not_found(path));
2059        }
2060        if !self.exists_in_upper(path) {
2061            self.copy_up_path(path)?;
2062        }
2063        self.writable_upper(path)?
2064            .zero_range(path, offset, length, keep_size)
2065    }
2066
2067    fn punch_hole(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
2068        if self.touches_internal_metadata(path) {
2069            return Err(VfsError::permission_denied("fallocate", path));
2070        }
2071        if self.is_whited_out(path) {
2072            return Err(Self::entry_not_found(path));
2073        }
2074        if !self.exists_in_upper(path) {
2075            self.copy_up_path(path)?;
2076        }
2077        self.writable_upper(path)?.punch_hole(path, offset, length)
2078    }
2079
2080    fn allocated_ranges(&mut self, path: &str) -> VfsResult<Vec<(u64, u64)>> {
2081        if self.touches_internal_metadata(path) || self.is_whited_out(path) {
2082            return Err(Self::entry_not_found(path));
2083        }
2084        if self.exists_in_upper(path) {
2085            self.writable_upper(path)?.allocated_ranges(path)
2086        } else {
2087            let Some(index) = self.find_lower_by_exists(path) else {
2088                return Err(Self::entry_not_found(path));
2089            };
2090            self.lowers[index].allocated_ranges(path)
2091        }
2092    }
2093
2094    fn unwritten_ranges(&mut self, path: &str) -> VfsResult<Vec<(u64, u64)>> {
2095        if self.touches_internal_metadata(path) || self.is_whited_out(path) {
2096            return Err(Self::entry_not_found(path));
2097        }
2098        if self.exists_in_upper(path) {
2099            self.writable_upper(path)?.unwritten_ranges(path)
2100        } else {
2101            let Some(index) = self.find_lower_by_exists(path) else {
2102                return Err(Self::entry_not_found(path));
2103            };
2104            self.lowers[index].unwritten_ranges(path)
2105        }
2106    }
2107
2108    fn pread(&mut self, path: &str, offset: u64, length: usize) -> VfsResult<Vec<u8>> {
2109        if self.touches_internal_metadata(path) {
2110            return Err(Self::entry_not_found(path));
2111        }
2112        if self.is_whited_out(path) {
2113            return Err(Self::entry_not_found(path));
2114        }
2115        if self.exists_in_upper(path) {
2116            return self
2117                .upper
2118                .as_mut()
2119                .expect("upper must exist when path exists")
2120                .pread(path, offset, length);
2121        }
2122        let Some(index) = self.find_lower_by_exists(path) else {
2123            return Err(Self::entry_not_found(path));
2124        };
2125        self.lowers[index].pread(path, offset, length)
2126    }
2127
2128    fn pwrite(&mut self, path: &str, content: impl Into<Vec<u8>>, offset: u64) -> VfsResult<()> {
2129        if self.touches_internal_metadata(path) {
2130            return Err(VfsError::permission_denied("pwrite", path));
2131        }
2132        if self.is_whited_out(path) {
2133            return Err(Self::entry_not_found(path));
2134        }
2135        if !self.exists_in_upper(path) {
2136            self.copy_up_path(path)?;
2137        }
2138        self.writable_upper(path)?
2139            .pwrite(path, content.into(), offset)
2140    }
2141}
2142
2143#[cfg(test)]
2144mod tests {
2145    use super::{OverlayFileSystem, OverlayMode};
2146    use crate::posix::vfs::{MemoryFileSystem, VfsResult, VirtualFileSystem};
2147
2148    #[test]
2149    fn symlink_into_metadata_namespace_cannot_read_or_resurrect_whiteouts() {
2150        let mut lower = MemoryFileSystem::new();
2151        lower.mkdir("/data", true).expect("create lower directory");
2152        lower
2153            .write_file("/data/secret.txt", b"secret".to_vec())
2154            .expect("seed lower file");
2155
2156        let mut overlay = OverlayFileSystem::with_upper(vec![lower], MemoryFileSystem::new());
2157
2158        // Delete a lower-layer file: a whiteout marker is written under the
2159        // reserved metadata root and the file disappears from the merged view.
2160        overlay
2161            .remove_file("/data/secret.txt")
2162            .expect("whiteout lower file");
2163        assert!(!overlay.exists("/data/secret.txt"));
2164
2165        // A guest symlink whose target is the metadata root must not become a
2166        // window into the reserved namespace.
2167        overlay
2168            .symlink("/.secure-exec-overlay/whiteouts", "/escape")
2169            .expect("creating the symlink itself is allowed");
2170
2171        // Listing through the symlink must be denied, not disclose markers.
2172        assert!(
2173            overlay.read_dir("/escape").is_err(),
2174            "listing the metadata namespace via a symlink must be denied"
2175        );
2176
2177        // Removing the whiteout marker through the symlink must be denied, so the
2178        // deleted lower-layer file cannot be resurrected.
2179        assert!(
2180            overlay.remove_file("/escape/anything").is_err(),
2181            "tampering with metadata via a symlink must be denied"
2182        );
2183        assert!(
2184            !overlay.exists("/data/secret.txt"),
2185            "deleted lower-layer file must stay deleted"
2186        );
2187
2188        // The same bypass via a symlink to an ancestor (e.g. `/`) is also closed.
2189        overlay
2190            .symlink("/", "/rootlink")
2191            .expect("symlink to root is allowed");
2192        assert!(
2193            overlay
2194                .read_dir("/rootlink/.secure-exec-overlay/whiteouts")
2195                .is_err(),
2196            "metadata must be unreachable via an ancestor symlink too"
2197        );
2198    }
2199
2200    #[test]
2201    fn whiteouts_persist_when_overlay_reopens_with_same_upper() {
2202        let mut lower = MemoryFileSystem::new();
2203        lower.mkdir("/data", true).expect("create lower directory");
2204        lower
2205            .write_file("/data/base.txt", b"base".to_vec())
2206            .expect("seed lower file");
2207        let lower_snapshot = lower.snapshot();
2208
2209        let mut overlay = OverlayFileSystem::with_upper(
2210            vec![MemoryFileSystem::from_snapshot(lower_snapshot.clone())],
2211            MemoryFileSystem::new(),
2212        );
2213        overlay
2214            .remove_file("/data/base.txt")
2215            .expect("whiteout lower file");
2216
2217        let upper = overlay.upper.take().expect("overlay upper");
2218        let restored_lower = MemoryFileSystem::from_snapshot(lower_snapshot);
2219        let mut restored = OverlayFileSystem::with_upper(vec![restored_lower], upper);
2220
2221        assert!(!restored.exists("/data/base.txt"));
2222        assert_eq!(
2223            restored.read_dir("/data").expect("read merged directory"),
2224            Vec::<String>::new()
2225        );
2226    }
2227
2228    #[test]
2229    fn remove_file_does_not_follow_a_self_referential_symlink() {
2230        let mut overlay = OverlayFileSystem::new(Vec::new(), OverlayMode::Ephemeral);
2231        overlay
2232            .symlink("self", "/self")
2233            .expect("create self-referential symlink");
2234
2235        overlay
2236            .remove_file("/self")
2237            .expect("unlink the symlink entry without following it");
2238
2239        assert_error_code(overlay.lstat("/self"), "ENOENT");
2240    }
2241
2242    #[test]
2243    fn copied_up_directories_become_opaque_and_hide_overlay_metadata() {
2244        let mut lower = MemoryFileSystem::new();
2245        lower.mkdir("/data", true).expect("create lower directory");
2246        lower
2247            .write_file("/data/base.txt", b"base".to_vec())
2248            .expect("seed lower file");
2249
2250        let mut overlay = OverlayFileSystem::new(vec![lower], OverlayMode::Ephemeral);
2251        overlay
2252            .chmod("/data", 0o700)
2253            .expect("copy up lower directory");
2254
2255        assert_eq!(
2256            overlay.read_dir("/data").expect("read opaque directory"),
2257            Vec::<String>::new()
2258        );
2259        let root_entries = overlay.read_dir("/").expect("read root");
2260        assert!(!root_entries
2261            .iter()
2262            .any(|entry| entry == ".secure-exec-overlay"));
2263    }
2264
2265    #[test]
2266    fn remove_dir_succeeds_when_only_lower_children_are_whited_out() {
2267        let mut lower = MemoryFileSystem::new();
2268        lower.mkdir("/a", true).expect("create lower directory");
2269        lower
2270            .write_file("/a/c", b"child".to_vec())
2271            .expect("seed lower child");
2272
2273        let mut overlay = OverlayFileSystem::new(vec![lower], OverlayMode::Ephemeral);
2274        overlay.remove_file("/a/c").expect("whiteout lower child");
2275        overlay
2276            .remove_dir("/a")
2277            .expect("remove merged-empty directory");
2278
2279        assert!(!overlay.exists("/a"));
2280        assert_error_code(overlay.read_dir("/a"), "ENOENT");
2281    }
2282
2283    #[test]
2284    fn rename_clears_destination_whiteout() {
2285        let lower = MemoryFileSystem::new();
2286        let mut overlay = OverlayFileSystem::new(vec![lower], OverlayMode::Ephemeral);
2287        overlay
2288            .write_file("/archive.zip", b"old".to_vec())
2289            .expect("create old archive");
2290        overlay
2291            .remove_file("/archive.zip")
2292            .expect("whiteout old archive");
2293        overlay
2294            .write_file("/workspace-temp", b"new".to_vec())
2295            .expect("create replacement");
2296
2297        overlay
2298            .rename("/workspace-temp", "/archive.zip")
2299            .expect("rename replacement over whiteout");
2300
2301        assert_eq!(
2302            overlay
2303                .read_file("/archive.zip")
2304                .expect("read renamed file"),
2305            b"new"
2306        );
2307        assert!(!overlay.exists("/workspace-temp"));
2308    }
2309
2310    #[test]
2311    fn remove_file_unlinks_dangling_symlink() {
2312        // git's symlink-support probe creates `.git/tXXXXXX -> testing` (a
2313        // dangling symlink), lstats it, and unlinks it. unlink(2) must remove
2314        // the link itself without resolving it.
2315        let lower = MemoryFileSystem::new();
2316        let mut overlay = OverlayFileSystem::new(vec![lower], OverlayMode::Ephemeral);
2317        overlay.mkdir("/repo", true).expect("create directory");
2318        overlay
2319            .symlink("testing", "/repo/probe")
2320            .expect("create dangling symlink");
2321        assert!(
2322            overlay
2323                .lstat("/repo/probe")
2324                .expect("lstat dangling symlink")
2325                .is_symbolic_link
2326        );
2327
2328        overlay
2329            .remove_file("/repo/probe")
2330            .expect("unlink dangling symlink");
2331
2332        assert!(overlay.lstat("/repo/probe").is_err());
2333        assert_eq!(
2334            overlay.read_dir("/repo").expect("read emptied directory"),
2335            Vec::<String>::new()
2336        );
2337        overlay
2338            .remove_dir("/repo")
2339            .expect("rmdir emptied directory");
2340    }
2341
2342    #[test]
2343    fn remove_file_unlinks_dangling_symlink_from_lower_layer() {
2344        let mut lower = MemoryFileSystem::new();
2345        lower.mkdir("/repo", true).expect("create lower directory");
2346        lower
2347            .symlink("missing-target", "/repo/probe")
2348            .expect("seed lower dangling symlink");
2349
2350        let mut overlay = OverlayFileSystem::new(vec![lower], OverlayMode::Ephemeral);
2351        overlay
2352            .remove_file("/repo/probe")
2353            .expect("whiteout lower dangling symlink");
2354
2355        assert!(overlay.lstat("/repo/probe").is_err());
2356        overlay
2357            .remove_dir("/repo")
2358            .expect("rmdir merged-empty directory");
2359    }
2360
2361    #[test]
2362    fn remove_dir_still_rejects_visible_children() {
2363        let mut lower = MemoryFileSystem::new();
2364        lower.mkdir("/a", true).expect("create lower directory");
2365        lower
2366            .write_file("/a/c", b"child".to_vec())
2367            .expect("seed lower child");
2368
2369        let mut overlay = OverlayFileSystem::new(vec![lower], OverlayMode::Ephemeral);
2370        assert_error_code(overlay.remove_dir("/a"), "ENOTEMPTY");
2371        assert!(overlay.exists("/a/c"));
2372    }
2373
2374    fn assert_error_code<T: std::fmt::Debug>(result: VfsResult<T>, expected: &str) {
2375        let error = result.expect_err("expected operation to fail");
2376        assert_eq!(error.code(), expected);
2377    }
2378}