From b0d9e864a046b71251a44ac8e7d4855dd4a64e18 Mon Sep 17 00:00:00 2001 From: Jay Bosamiya Date: Thu, 13 Aug 2026 19:11:31 -0700 Subject: [PATCH 1/7] Initialize base for overlay file system --- litebox/src/fs/mod.rs | 1 + litebox/src/fs/overlay.rs | 1174 +++++++++++++++++++++++++++++++++++++ 2 files changed, 1175 insertions(+) create mode 100644 litebox/src/fs/overlay.rs diff --git a/litebox/src/fs/mod.rs b/litebox/src/fs/mod.rs index 6fe847d0a..191936a21 100644 --- a/litebox/src/fs/mod.rs +++ b/litebox/src/fs/mod.rs @@ -20,6 +20,7 @@ pub mod in_mem; pub(crate) mod inode_allocator; pub mod layered; pub mod nine_p; +pub mod overlay; pub mod resolver; pub mod tar_ro; diff --git a/litebox/src/fs/overlay.rs b/litebox/src/fs/overlay.rs new file mode 100644 index 000000000..5e120d2ca --- /dev/null +++ b/litebox/src/fs/overlay.rs @@ -0,0 +1,1174 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! A writable upper backend layered over one or more immutable lower backends. +//! +//! All backends are logically exclusively owned by the overlay while it exists: lower backends must +//! not change, and the upper backend must only be mutated through the overlay. The entire +//! `.litebox-overlay-*` namespace is permanently reserved; an upper backend must be fresh or have +//! been initialized by this overlay format. +//! +//! The immutability described above (i.e., logical exclusivity) is a correctness requirement, not a +//! safety requirement. If a lower is changed (say, externally), operations may observe stale +//! entries or fail, but the overlay treats stale/mismatched objects as ordinary errors. Such +//! changes do not compromise memory safety or the structural integrity of its internal state. + +use alloc::boxed::Box; +use alloc::string::String; +use alloc::vec; +use alloc::vec::Vec; + +use hashbrown::{HashMap, HashSet}; + +use crate::LiteBox; +use crate::sync::{Mutex, MutexGuard, RawSyncPrimitivesProvider}; + +use super::backend::{ + Backend, BackendHandles, DirHandle, FileHandle, Handle, HandleRef, Permissioned, SeekBehavior, + WalkOutcome, WalkStopReason, WalkedComponent, WalkingDirHandle, +}; +use super::errors::{ + ChmodError, ChownError, FileStatusError, MkdirError, OpenError, PathError, ReadDirError, + ReadError, RmdirError, TruncateError, UnlinkError, WalkError, WriteError, +}; +use super::inode_allocator::InodeAllocator; +use super::{DirEntry, FileStatus, FileType, Mode, NodeInfo, OFlags}; + +/// The reserved namespace prefix; no overlay-visible name may start with it. +const MARKER_PREFIX: &str = ".litebox-overlay-"; +/// Prefix of a per-hidden-name whiteout marker; the suffix is the hidden name itself. +const WHITEOUT_PREFIX: &str = ".litebox-overlay-whiteout-"; +/// Name of the per-directory marker that hides all lower entries of that directory. +const OPAQUE_MARKER: &str = ".litebox-overlay-opaque"; + +/// A layered backend with a writable upper backend and one or more immutable lower backends. +pub struct Overlay { + upper: Box, + // Lower backends are ordered from highest to lowest precedence. + lowers: Vec>, + alloc: InodeAllocator, + /// Makes a resolve-then-mutate sequence atomic against other overlay mutations. It is + /// deliberately *not* taken by read paths. + namespace: Mutex, + state: Mutex, +} + +/// A held namespace lock, marking mutability of the upper backend. +type NamespaceGuard<'a, Platform> = MutexGuard<'a, Platform, Namespace>; +struct Namespace; + +struct State { + /// Overlay-visible identity assigned to each per-layer node. + ids: HashMap, + /// Files that have been copied up, by overlay identity, and their handle in the upper backend. + /// A handle opened against a lower backend stays valid, but every operation looks here first. + copied_up: HashMap, +} + +/// A node as identified by the layer that owns it; `Lower` carries the lower backend's index. +#[derive(Clone, PartialEq, Eq, Hash)] +enum LayerNode { + Upper(NodeInfo), + Lower(usize, NodeInfo), +} + +pub struct OverlayWalkingDir { + path: Vec, +} + +#[derive(Clone)] +pub struct OverlayDir { + path: Vec, +} + +/// An owned handle to a file opened through the overlay. +#[derive(Clone)] +pub struct OverlayFile { + /// The layer this file was _opened_ against; a later copy-up can move it, which is what + /// [`State::copied_up`] records. + layer: OverlayFileLayer, + // TODO(jayb): the parent path plus name is how object-addressed operations (`chmod`/`chown`) + // find the file again in order to copy it up. This must be revisited when rename lands, since a + // rename invalidates the recorded location. + parent: Vec, + name: String, +} + +/// The layer backing an open overlay file. +#[derive(Clone)] +enum OverlayFileLayer { + Upper(FileHandle), + Lower { + layer: usize, + handle: FileHandle, + /// The overlay identity of the file, under which a later copy-up records its upper handle. + node: NodeInfo, + }, +} + +/// A logical directory, resolved to the per-layer directories that make it up. +struct ResolvedDir { + upper: Option, + /// Per lower backend, in precedence order, its directory at this path if any. + lowers: Vec>, + entries: HashMap, +} + +/// An overlay-visible directory entry, plus which layers contribute to it. +struct ResolvedEntry { + /// The entry as reported by the layer that owns it. + entry: DirEntry, + upper: bool, + /// The highest-precedence lower backend with an entry of this name, if any. + lower: Option, + /// Per lower backend, whether it has a *directory* of this name that merges into this entry. + lower_directories: Vec, +} + +impl Overlay { + /// Construct an overlay over a single `lower`, using `allocator` for overlay-visible inodes. + pub fn new( + litebox: &LiteBox, + upper: impl Backend, + lower: impl Backend, + allocator: InodeAllocator, + ) -> Self { + Self::with_boxed_lowers(litebox, upper, vec![Box::new(lower)], allocator) + } + + /// Construct an overlay with lower backends ordered from highest to lowest precedence. + /// + /// # Panics + /// + /// Panics if `lowers` is empty. + pub fn with_boxed_lowers( + _litebox: &LiteBox, + upper: impl Backend, + lowers: Vec>, + allocator: InodeAllocator, + ) -> Self { + assert!( + !lowers.is_empty(), + "an overlay requires at least one lower backend" + ); + Self { + upper: Box::new(upper), + lowers, + alloc: allocator, + namespace: Mutex::new(Namespace), + state: Mutex::new(State { + ids: HashMap::new(), + copied_up: HashMap::new(), + }), + } + } + + fn resolve_root(&self) -> Result { + let upper = self.upper.owned_dir_at(self.upper.root(), OFlags::PATH)?; + let lowers = self + .lowers + .iter() + .map(|lower| lower.owned_dir_at(lower.root(), OFlags::PATH).map(Some)) + .collect::, _>>()?; + self.merge(Some(upper), lowers) + } + + /// Resolve the directory `dir_name` within the already-resolved `parent`, along with the + /// [`WalkedComponent`] reported by the layer that owns it. + fn resolve_child_dir( + &self, + parent: &ResolvedDir, + dir_name: &str, + ) -> Result<(ResolvedDir, WalkedComponent), OpenError> { + fn walk_into_dir( + backend: &dyn Backend, + parent: &DirHandle, + dir_name: &str, + ) -> Result<(DirHandle, WalkedComponent), OpenError> { + let walking = backend.walking_dir_at(parent).ok_or(OpenError::Io)?; + let outcome = backend + .walk_directories(walking, &[dir_name]) + .map_err(|error| match error { + WalkError::PathError(error) => OpenError::PathError(error), + WalkError::Io => OpenError::Io, + })?; + let [component] = &outcome.components[..] else { + return Err(PathError::ComponentNotADirectory.into()); + }; + if outcome.stop_reason != WalkStopReason::CompleteDirectory { + return Err(PathError::ComponentNotADirectory.into()); + } + let component = component.clone(); + let owned = backend.owned_dir_at(outcome.last, OFlags::PATH)?; + Ok((owned, component)) + } + + let entry = parent + .entries + .get(dir_name) + .ok_or(OpenError::PathError(PathError::MissingComponent))?; + if entry.entry.file_type != FileType::Directory { + return Err(OpenError::PathError(PathError::ComponentNotADirectory)); + } + + let mut owner_component = None; + let upper = match (&parent.upper, entry.upper) { + (Some(parent), true) => { + let (handle, component) = walk_into_dir(self.upper.as_ref(), parent, dir_name)?; + // The upper backend owns any name it has. + owner_component = Some(component); + Some(handle) + } + _ => None, + }; + + let mut lowers = Vec::with_capacity(self.lowers.len()); + for ((layer, lower), parent) in self.lowers.iter().enumerate().zip(&parent.lowers) { + let child = match parent { + Some(parent) if entry.lower_directories[layer] => { + let (handle, component) = walk_into_dir(lower.as_ref(), parent, dir_name)?; + // Otherwise the highest-precedence lower with this name owns it. + owner_component.get_or_insert(component); + Some(handle) + } + _ => None, + }; + lowers.push(child); + } + let component = + owner_component.expect("a merged directory is owned by upper or by a lower directory"); + + Ok((self.merge(upper, lowers)?, component)) + } + + /// Resolve a logical `path` (relative to the overlay root) to its per-layer directories. + fn resolve_dir(&self, path: &[String]) -> Result { + let mut current = self.resolve_root()?; + for name in path { + current = self.resolve_child_dir(¤t, name)?.0; + } + Ok(current) + } + + /// Copy the lower file `lower` up into the upper backend as `name` within `upper_dir`. + /// + /// The namespace lock is held for the whole copy, so a partially written (or failed and + /// unlinked) upper file is never observable. + // XXX(jayb): holding the namespace lock across a whole-file byte copy blocks every other + // namespace operation for as long as the copy takes. Ideally, we would avoid this by doing an + // atomic link/rename, would need to update `Backend` for that. + fn copy_up_file( + &self, + _guard: &NamespaceGuard<'_, Platform>, + upper_dir: &DirHandle, + name: &str, + lower: (usize, &FileHandle), + status: &FileStatus, + truncate: bool, + ) -> Result { + let (layer, lower) = lower; + let upper = self + .upper + .create_file_at(upper_dir.clone(), name, status.mode)?; + let copied = self + .upper + .chown( + HandleRef::File(&upper), + Some(status.owner.user), + Some(status.owner.group), + ) + .map_err(|error| match error { + ChownError::PathError(error) => OpenError::PathError(error), + ChownError::ReadOnlyFileSystem => OpenError::ReadOnlyFileSystem, + _ => OpenError::Io, + }); + + let copied = copied.and_then(|()| { + if truncate { + return Ok(()); + } + self.copy_bytes(layer, lower, &upper) + }); + + if let Err(error) = copied { + // Ancestor directories materialised for this copy-up deliberately stay behind. + let _rollback_result = self.upper.unlink_at(upper_dir.clone(), name); + return Err(error); + } + // The copied-up file keeps the identity it had in the lower backend, so existing + // lower-backed handles keep reporting the same inode. + if let Ok(upper_status) = self.upper.status(HandleRef::File(&upper)) { + self.bind_copy_up( + layer, + status.node_info.clone(), + upper_status.node_info, + Some(&upper), + ); + } + Ok(upper) + } + + fn copy_bytes( + &self, + layer: usize, + lower: &FileHandle, + upper: &FileHandle, + ) -> Result<(), OpenError> { + let mut offset = 0; + let mut buf = [0u8; 4096]; + loop { + let count = self.lowers[layer] + .read(lower, &mut buf, offset) + .map_err(|_| OpenError::Io)?; + if count == 0 { + return Ok(()); + } + let mut written = 0; + while written < count { + let progress = self + .upper + .write(upper, &buf[written..count], offset + written) + .map_err(|_| OpenError::Io)?; + if progress == 0 { + return Err(OpenError::Io); + } + written += progress; + } + offset += count; + } + } + + /// Materialise whatever `h` refers to in the upper backend, and return a handle to it. + fn ensure_upper( + &self, + locked: NamespaceGuard<'_, Platform>, + h: HandleRef<'_>, + ) -> Result { + let file = match h { + HandleRef::Dir(dir) => { + let path = &dir.get_typed::().path; + return Ok(Handle::Dir(self.ensure_upper_dir(&locked, path)?)); + } + HandleRef::File(file) => file.get_typed::(), + }; + let (layer, lower) = match &file.layer { + OverlayFileLayer::Upper(handle) => return Ok(Handle::File(handle.clone())), + OverlayFileLayer::Lower { layer, handle, .. } => (*layer, handle), + }; + if let Some(upper) = self.migrated(file) { + return Ok(Handle::File(upper)); + } + + let status = self.lowers[layer] + .status(HandleRef::File(lower)) + .map_err(file_status_to_open_error)?; + if status.file_type != FileType::RegularFile { + // Only regular files can be copied up. + return Err(OpenError::ReadOnlyFileSystem); + } + let upper_dir = self.ensure_upper_dir(&locked, &file.parent)?; + let upper = self.copy_up_file( + &locked, + &upper_dir, + &file.name, + (layer, lower), + &status, + false, + )?; + Ok(Handle::File(upper)) + } + + fn marker_present(&self, dir: &DirHandle, marker: &str) -> Result { + Ok(self + .upper + .list_dir_at(dir.clone())? + .iter() + .any(|entry| entry.name == marker)) + } + + /// Create `marker` in the upper directory `dir`, if not already there. + fn create_marker( + &self, + _locked: &NamespaceGuard<'_, Platform>, + dir: &DirHandle, + marker: &str, + ) -> Result<(), OpenError> { + if self + .marker_present(dir, marker) + .map_err(|_| OpenError::Io)? + { + return Ok(()); + } + self.upper + .create_file_at(dir.clone(), marker, Mode::empty())?; + Ok(()) + } + + fn remove_marker( + &self, + _locked: &NamespaceGuard<'_, Platform>, + dir: &DirHandle, + marker: &str, + ) -> Result<(), UnlinkError> { + if self + .marker_present(dir, marker) + .map_err(|_| UnlinkError::Io)? + { + self.upper.unlink_at(dir.clone(), marker)?; + } + Ok(()) + } + + /// Remove every overlay marker held directly by the upper directory `dir`, so that a + /// caller-visibly empty directory is also empty to the upper backend. + fn clear_markers( + &self, + _locked: &NamespaceGuard<'_, Platform>, + dir: &DirHandle, + ) -> Result, UnlinkError> { + let entries = self + .upper + .list_dir_at(dir.clone()) + .map_err(|_| UnlinkError::Io)?; + let mut removed = Vec::new(); + for entry in entries.iter().filter(|entry| !valid(&entry.name)) { + self.upper.unlink_at(dir.clone(), &entry.name)?; + removed.push(entry.name.clone()); + } + Ok(removed) + } + + /// Materialise `path` in the upper backend, creating any missing directory along the way. + /// + /// Only mutating operations call this: reads never write to the upper backend. + fn ensure_upper_dir( + &self, + locked: &NamespaceGuard<'_, Platform>, + path: &[String], + ) -> Result { + let mut upper = self.upper.owned_dir_at(self.upper.root(), OFlags::PATH)?; + for index in 0..path.len() { + // Re-resolve after each materialisation, since it changed the upper namespace. + let resolved = self.resolve_dir(&path[..=index])?; + upper = match resolved.upper { + Some(handle) => handle, + None => self.materialize_dir(locked, &upper, &resolved, &path[index])?, + }; + } + Ok(upper) + } + + /// Create the upper counterpart of the lower-only directory `resolved`, named `name` in + /// `parent`. + fn materialize_dir( + &self, + _locked: &NamespaceGuard<'_, Platform>, + parent: &DirHandle, + resolved: &ResolvedDir, + name: &str, + ) -> Result { + let (layer, backend, handle) = self.owning_dir(resolved).ok_or(OpenError::Io)?; + let status = backend + .status(HandleRef::Dir(handle)) + .map_err(file_status_to_open_error)?; + let child = self + .upper + .mkdir_at(parent.clone(), name, status.mode) + .map_err(|error| match error { + MkdirError::PathError(error) => OpenError::PathError(error), + MkdirError::AlreadyExists => OpenError::AlreadyExists, + MkdirError::ReadOnlyFileSystem => OpenError::ReadOnlyFileSystem, + MkdirError::NoWritePerms => OpenError::NoWritePerms, + _ => OpenError::Io, + })?; + // XXX(jayb): an atomic create-with-metadata `Backend` operation would avoid this + // best-effort rollback path. + match self.upper.chown( + HandleRef::Dir(&child), + Some(status.owner.user), + Some(status.owner.group), + ) { + Ok(()) => { + // A materialised directory stands in for the lower one, so it keeps its identity. + if let (Some(layer), Ok(upper)) = (layer, self.upper.status(HandleRef::Dir(&child))) + { + self.bind_copy_up(layer, status.node_info, upper.node_info, None); + } + Ok(child) + } + Err(error) => { + let _rollback_result = self.upper.rmdir_at(parent.clone(), name); + Err(match error { + ChownError::PathError(error) => OpenError::PathError(error), + ChownError::ReadOnlyFileSystem => OpenError::ReadOnlyFileSystem, + _ => OpenError::Io, + }) + } + } + } + + /// The layer that owns a resolved directory, and its handle within that layer: the upper + /// directory when there is one, the highest-precedence lower directory otherwise. + fn owning_dir<'a>( + &'a self, + dir: &'a ResolvedDir, + ) -> Option<(Option, &'a dyn Backend, &'a DirHandle)> { + match &dir.upper { + Some(handle) => Some((None, self.upper.as_ref(), handle)), + None => dir.lowers.iter().zip(&self.lowers).enumerate().find_map( + |(layer, (handle, lower))| { + handle + .as_ref() + .map(|handle| (Some(layer), lower.as_ref(), handle)) + }, + ), + } + } + + /// Run `f` against the layer that currently backs an open `file`: the one it was opened + /// against, or the upper backend if it has been copied up since. + fn with_file( + &self, + file: &OverlayFile, + f: impl FnOnce(Option, &dyn Backend, &FileHandle) -> R, + ) -> R { + if let Some(upper) = self.migrated(file) { + return f(None, self.upper.as_ref(), &upper); + } + match &file.layer { + OverlayFileLayer::Upper(handle) => f(None, self.upper.as_ref(), handle), + OverlayFileLayer::Lower { layer, handle, .. } => { + f(Some(*layer), self.lowers[*layer].as_ref(), handle) + } + } + } + + /// The overlay-visible identity of `node` as owned by `layer`, allocated on first sight. + fn map_node( + &self, + ids: &mut HashMap, + layer: Option, + node: NodeInfo, + ) -> NodeInfo { + let rdev = node.rdev; + ids.entry(layer_node(layer, node)) + .or_insert_with(|| NodeInfo { + rdev, + ..self.alloc.next() + }) + .clone() + } + + /// `status` as reported by `layer`, with its node identity replaced by the overlay's own. + fn map_status(&self, mut status: FileStatus, layer: Option) -> FileStatus { + status.node_info = self.map_node(&mut self.state.lock().ids, layer, status.node_info); + status + } + + /// Give the freshly created `upper` node the overlay identity of the `lower` node it copies, + /// which is what makes copy-up invisible: the object keeps its inode. + /// + /// `upper_file` is the new upper handle, which lets lower-backed handles follow the contents; + /// directories are addressed by path, so they have nothing to follow. + fn bind_copy_up( + &self, + layer: usize, + lower: NodeInfo, + upper: NodeInfo, + upper_file: Option<&FileHandle>, + ) { + let mut state = self.state.lock(); + let id = self.map_node(&mut state.ids, Some(layer), lower); + state.ids.insert(layer_node(None, upper), id.clone()); + if let Some(file) = upper_file { + state.copied_up.insert(id, file.clone()); + } + } + + /// The upper handle for `file`, if it has been copied up since it was opened. + fn migrated(&self, file: &OverlayFile) -> Option { + let OverlayFileLayer::Lower { node, .. } = &file.layer else { + return None; + }; + self.state.lock().copied_up.get(node).cloned() + } + + /// Merge the per-layer directories of one logical directory into its overlay-visible entries. + fn merge( + &self, + upper: Option, + lowers: Vec>, + ) -> Result { + let upper_entries = match &upper { + Some(handle) => self + .upper + .list_dir_at(handle.clone()) + .map_err(|_| OpenError::Io)?, + None => Vec::new(), + }; + // Markers held by this upper directory, which say what it hides from the lowers. + let markers: HashSet = upper_entries + .iter() + .filter(|entry| !valid(&entry.name)) + .map(|entry| entry.name.clone()) + .collect(); + let opaque = markers.contains(OPAQUE_MARKER); + + let mut entries = HashMap::new(); + // Names at which lower entries can no longer be merged in: an entry exists there that is + // not a directory in every layer that contributed to it. + let mut blocked = HashSet::new(); + + for mut entry in upper_entries.into_iter().filter(|entry| valid(&entry.name)) { + if entry.file_type != FileType::Directory { + blocked.insert(entry.name.clone()); + } + entry.ino_info = entry + .ino_info + .take() + .map(|node| self.map_node(&mut self.state.lock().ids, None, node)); + entries.insert( + entry.name.clone(), + ResolvedEntry { + entry, + upper: true, + lower: None, + lower_directories: vec![false; self.lowers.len()], + }, + ); + } + + if !opaque { + for (layer, handle) in lowers.iter().enumerate() { + let Some(handle) = handle else { + continue; + }; + let layer_entries = self.lowers[layer] + .list_dir_at(handle.clone()) + .map_err(|_| OpenError::Io)?; + for mut lower_entry in layer_entries { + let name = lower_entry.name.clone(); + if !valid(&name) || markers.contains(&whiteout(&name)) { + continue; + } + let directory = lower_entry.file_type == FileType::Directory; + let lower_node = lower_entry.ino_info.take(); + let entry = entries + .entry(name.clone()) + .or_insert_with(|| ResolvedEntry { + entry: lower_entry, + upper: false, + lower: Some(layer), + lower_directories: vec![false; self.lowers.len()], + }); + entry.lower.get_or_insert(layer); + if !entry.upper && entry.lower == Some(layer) { + // This layer owns the entry, so its node is the one callers see. + entry.entry.ino_info = lower_node.clone().map(|node| { + self.map_node(&mut self.state.lock().ids, Some(layer), node) + }); + } + if blocked.contains(&name) { + continue; + } + if directory { + entry.lower_directories[layer] = true; + // Several layers describe one logical directory; the one already resolved + // above owns the identity, and this layer's node adopts it. + if let (Some(node), Some(id)) = (lower_node, entry.entry.ino_info.clone()) { + self.state + .lock() + .ids + .entry(layer_node(Some(layer), node)) + .or_insert(id); + } + } else { + blocked.insert(name); + } + } + } + } + + Ok(ResolvedDir { + upper, + lowers, + entries, + }) + } +} + +/// The node `node` as owned by `layer`, which is `None` for the upper backend and `Some(index)` +/// for a lower one. +fn layer_node(layer: Option, node: NodeInfo) -> LayerNode { + match layer { + None => LayerNode::Upper(node), + Some(layer) => LayerNode::Lower(layer, node), + } +} + +/// Whether `name` may be visible through the overlay. +fn valid(name: &str) -> bool { + !name.starts_with(MARKER_PREFIX) +} + +/// The whiteout marker name that hides `name` in a directory. +fn whiteout(name: &str) -> String { + let mut out = String::from(WHITEOUT_PREFIX); + out.push_str(name); + out +} + +fn unlink_to_open_error(error: UnlinkError) -> OpenError { + match error { + UnlinkError::PathError(error) => OpenError::PathError(error), + UnlinkError::ReadOnlyFileSystem => OpenError::ReadOnlyFileSystem, + UnlinkError::NoWritePerms => OpenError::NoWritePerms, + _ => OpenError::Io, + } +} + +fn file_status_to_open_error(error: FileStatusError) -> OpenError { + match error { + FileStatusError::PathError(error) => OpenError::PathError(error), + _ => OpenError::Io, + } +} + +impl super::backend::private::Sealed for Overlay {} + +impl BackendHandles for Overlay { + type WalkingDirHandle<'a> = OverlayWalkingDir; + type FileHandle = OverlayFile; + type DirHandle = OverlayDir; +} + +impl Backend for Overlay { + fn root(&self) -> WalkingDirHandle<'_> { + WalkingDirHandle::from_typed::(OverlayWalkingDir { path: Vec::new() }) + } + + fn walk_directories<'a>( + &'a self, + from: WalkingDirHandle<'a>, + components: &[&str], + ) -> Result>, WalkError> { + fn open_to_walk_error(error: OpenError) -> WalkError { + match error { + OpenError::PathError(error) => WalkError::PathError(error), + _ => WalkError::Io, + } + } + let mut path = from.into_typed::().path; + let mut walked = Vec::with_capacity(components.len()); + let mut current = self.resolve_dir(&path).map_err(open_to_walk_error)?; + for name in components { + if !valid(name) { + return Err(PathError::InvalidPathname.into()); + } + let entry = current + .entries + .get(*name) + .ok_or(PathError::NoSuchFileOrDirectory)?; + if entry.entry.file_type != FileType::Directory { + return Ok(WalkOutcome { + components: walked, + last: WalkingDirHandle::from_typed::(OverlayWalkingDir { path }), + stop_reason: WalkStopReason::StoppedAtNonDirectory, + }); + } + let (child, component) = self + .resolve_child_dir(¤t, name) + .map_err(open_to_walk_error)?; + current = child; + path.push(String::from(*name)); + walked.push(component); + } + Ok(WalkOutcome { + components: walked, + last: WalkingDirHandle::from_typed::(OverlayWalkingDir { path }), + stop_reason: WalkStopReason::CompleteDirectory, + }) + } + + fn owned_dir_at( + &self, + dir: WalkingDirHandle<'_>, + flags: OFlags, + ) -> Result { + let path = dir.into_typed::().path; + let resolved = self.resolve_dir(&path)?; + let (_, backend, handle) = self.owning_dir(&resolved).ok_or(OpenError::Io)?; + let walking = backend.walking_dir_at(handle).ok_or(OpenError::Io)?; + backend.owned_dir_at(walking, flags)?; + Ok(DirHandle::from_typed::(OverlayDir { path })) + } + + fn walking_dir_at<'a>(&'a self, dir: &DirHandle) -> Option> { + Some(WalkingDirHandle::from_typed::(OverlayWalkingDir { + path: dir.get_typed::().path.clone(), + })) + } + + fn open_file_at( + &self, + dir: WalkingDirHandle<'_>, + name: &str, + flags: OFlags, + ) -> Result, OpenError> { + if !valid(name) { + return Err(PathError::InvalidPathname.into()); + } + if flags.contains(OFlags::DIRECTORY) { + return Err(PathError::ComponentNotADirectory.into()); + } + let path = dir.into_typed::().path; + let guard = self.namespace.lock(); + let resolved = self.resolve_dir(&path)?; + let entry = resolved + .entries + .get(name) + .ok_or(OpenError::PathError(PathError::NoSuchFileOrDirectory))?; + // The resolver only reaches `create_file_at` once a walk reported the name as missing, so + // an existing entry means an exclusive create must fail here. + if flags.contains(OFlags::CREAT | OFlags::EXCL) { + return Err(OpenError::AlreadyExists); + } + + let (layer, permissions) = if entry.upper { + let upper = resolved.upper.as_ref().ok_or(OpenError::Io)?; + let walking = self.upper.walking_dir_at(upper).ok_or(OpenError::Io)?; + let file = self.upper.open_file_at(walking, name, flags)?; + (OverlayFileLayer::Upper(file.item), file.permissions) + } else { + let layer = entry.lower.ok_or(OpenError::Io)?; + let lower_dir = resolved.lowers[layer].as_ref().ok_or(OpenError::Io)?; + let walking = self.lowers[layer] + .walking_dir_at(lower_dir) + .ok_or(OpenError::Io)?; + // An open that may modify the file has to copy it up first; the lower backends are + // immutable, so such an open is read-only down there. + // + // XXX(jayb): the resolver authorizes an open only after this returns, so a + // writable open can copy up before a later permission denial. A preflight + // authorization hook in `Backend` would make copy-up properly two-phase. + let writing = + flags.intersects(OFlags::WRONLY | OFlags::RDWR | OFlags::APPEND | OFlags::TRUNC); + let lower_flags = if writing { + OFlags::RDONLY + } else { + flags.difference(OFlags::CREAT) + }; + let file = self.lowers[layer].open_file_at(walking, name, lower_flags)?; + // The file's own identity, which is also the key a later copy-up records itself under. + let status = self.lowers[layer] + .status(HandleRef::File(&file.item)) + .map_err(file_status_to_open_error)?; + if writing { + if entry.entry.file_type != FileType::RegularFile { + // Only regular files can be copied up, and the lowers do not accept writes. + return Err(OpenError::ReadOnlyFileSystem); + } + let upper_dir = self.ensure_upper_dir(&guard, &path)?; + let upper = self.copy_up_file( + &guard, + &upper_dir, + name, + (layer, &file.item), + &status, + flags.contains(OFlags::TRUNC), + )?; + (OverlayFileLayer::Upper(upper), file.permissions) + } else { + let node = self.map_node(&mut self.state.lock().ids, Some(layer), status.node_info); + ( + OverlayFileLayer::Lower { + layer, + handle: file.item, + node, + }, + file.permissions, + ) + } + }; + + Ok(Permissioned { + item: FileHandle::from_typed::(OverlayFile { + layer, + parent: path, + name: String::from(name), + }), + permissions, + }) + } + + fn list_dir_at(&self, handle: DirHandle) -> Result, ReadDirError> { + let path = handle.into_typed::().path; + let resolved = self.resolve_dir(&path).map_err(|_| ReadDirError::Io)?; + let mut entries: Vec = resolved + .entries + .into_values() + .map(|entry| entry.entry) + .collect(); + entries.sort_by(|left, right| left.name.cmp(&right.name)); + Ok(entries) + } + + fn read(&self, h: &FileHandle, buf: &mut [u8], offset: usize) -> Result { + self.with_file(h.get_typed::(), |_, backend, handle| { + backend.read(handle, buf, offset) + }) + } + + fn get_static_backing_data(&self, h: &FileHandle) -> Option<&'static [u8]> { + self.with_file(h.get_typed::(), |_, backend, handle| { + backend.get_static_backing_data(handle) + }) + } + + fn write(&self, h: &FileHandle, buf: &[u8], offset: usize) -> Result { + let file = h.get_typed::(); + if let Some(upper) = self.migrated(file) { + return self.upper.write(&upper, buf, offset); + } + match &file.layer { + OverlayFileLayer::Upper(handle) => self.upper.write(handle, buf, offset), + // A writable open copies up first, so a lower-backed handle is read-only. + OverlayFileLayer::Lower { .. } => Err(WriteError::NotForWriting), + } + } + + fn truncate(&self, h: &FileHandle, length: usize) -> Result<(), TruncateError> { + let file = h.get_typed::(); + if let Some(upper) = self.migrated(file) { + return self.upper.truncate(&upper, length); + } + match &file.layer { + OverlayFileLayer::Upper(handle) => self.upper.truncate(handle, length), + OverlayFileLayer::Lower { .. } => Err(TruncateError::NotForWriting), + } + } + + fn seek_behavior(&self, h: &FileHandle) -> SeekBehavior { + self.with_file(h.get_typed::(), |_, backend, handle| { + backend.seek_behavior(handle) + }) + } + + fn status(&self, h: HandleRef<'_>) -> Result { + match h { + HandleRef::File(handle) => { + self.with_file(handle.get_typed::(), |layer, backend, handle| { + let status = backend.status(HandleRef::File(handle))?; + Ok(self.map_status(status, layer)) + }) + } + HandleRef::Dir(handle) => { + let path = &handle.get_typed::().path; + let resolved = self.resolve_dir(path).map_err(|_| FileStatusError::Io)?; + let (layer, backend, handle) = + self.owning_dir(&resolved).ok_or(FileStatusError::Io)?; + let status = backend.status(HandleRef::Dir(handle))?; + Ok(self.map_status(status, layer)) + } + } + } + + fn create_file_at( + &self, + dir: DirHandle, + name: &str, + mode: Mode, + ) -> Result { + if !valid(name) { + return Err(PathError::InvalidPathname.into()); + } + let path = dir.into_typed::().path; + let locked = self.namespace.lock(); + if self.resolve_dir(&path)?.entries.contains_key(name) { + return Err(OpenError::AlreadyExists); + } + let upper = self.ensure_upper_dir(&locked, &path)?; + let file = self.upper.create_file_at(upper.clone(), name, mode)?; + if let Err(error) = self.remove_marker(&locked, &upper, &whiteout(name)) { + let _rollback_result = self.upper.unlink_at(upper, name); + return Err(unlink_to_open_error(error)); + } + Ok(FileHandle::from_typed::(OverlayFile { + layer: OverlayFileLayer::Upper(file), + parent: path, + name: String::from(name), + })) + } + + fn mkdir_at(&self, dir: DirHandle, name: &str, mode: Mode) -> Result { + fn open_to_mkdir_error(error: OpenError) -> MkdirError { + match error { + OpenError::PathError(error) => MkdirError::PathError(error), + OpenError::AlreadyExists => MkdirError::AlreadyExists, + OpenError::ReadOnlyFileSystem => MkdirError::ReadOnlyFileSystem, + OpenError::NoWritePerms => MkdirError::NoWritePerms, + _ => MkdirError::Io, + } + } + if !valid(name) { + return Err(PathError::InvalidPathname.into()); + } + let mut path = dir.into_typed::().path; + let locked = self.namespace.lock(); + let resolved = self.resolve_dir(&path).map_err(open_to_mkdir_error)?; + if resolved.entries.contains_key(name) { + return Err(MkdirError::AlreadyExists); + } + let upper = self + .ensure_upper_dir(&locked, &path) + .map_err(open_to_mkdir_error)?; + let whiteout = whiteout(name); + let recreated = self + .marker_present(&upper, &whiteout) + .map_err(|_| MkdirError::Io)?; + let child = self.upper.mkdir_at(upper.clone(), name, mode)?; + + // A directory recreated over a whiteout must not re-merge with the lower directory it + // replaces, so it starts out opaque. + let cleared = if recreated { + self.create_marker(&locked, &child, OPAQUE_MARKER) + .and_then(|()| { + self.remove_marker(&locked, &upper, &whiteout) + .map_err(unlink_to_open_error) + }) + .map_err(open_to_mkdir_error) + } else { + Ok(()) + }; + if let Err(error) = cleared { + let _rollback_marker = self.clear_markers(&locked, &child); + let _rollback_dir = self.upper.rmdir_at(upper, name); + return Err(error); + } + + path.push(String::from(name)); + Ok(DirHandle::from_typed::(OverlayDir { path })) + } + + fn unlink_at(&self, dir: DirHandle, name: &str) -> Result<(), UnlinkError> { + fn open_to_unlink_error(error: OpenError) -> UnlinkError { + match error { + OpenError::PathError(error) => UnlinkError::PathError(error), + OpenError::ReadOnlyFileSystem => UnlinkError::ReadOnlyFileSystem, + OpenError::NoWritePerms => UnlinkError::NoWritePerms, + _ => UnlinkError::Io, + } + } + if !valid(name) { + return Err(PathError::InvalidPathname.into()); + } + let path = dir.into_typed::().path; + let locked = self.namespace.lock(); + let resolved = self.resolve_dir(&path).map_err(open_to_unlink_error)?; + let entry = resolved + .entries + .get(name) + .ok_or(PathError::NoSuchFileOrDirectory)?; + if entry.entry.file_type == FileType::Directory { + return Err(UnlinkError::IsADirectory); + } + // The whiteout goes in before the upper entry comes out, so a failure part-way through can + // never reveal the lower entry. + let upper = match entry.lower { + Some(_) => { + let upper = self + .ensure_upper_dir(&locked, &path) + .map_err(open_to_unlink_error)?; + self.create_marker(&locked, &upper, &whiteout(name)) + .map_err(open_to_unlink_error)?; + upper + } + None => resolved.upper.ok_or(UnlinkError::Io)?, + }; + if entry.upper { + self.upper.unlink_at(upper, name)?; + } + Ok(()) + } + + fn rmdir_at(&self, dir: DirHandle, name: &str) -> Result<(), RmdirError> { + fn open_to_rmdir_error(error: OpenError) -> RmdirError { + match error { + OpenError::PathError(error) => RmdirError::PathError(error), + OpenError::ReadOnlyFileSystem => RmdirError::ReadOnlyFileSystem, + OpenError::NoWritePerms => RmdirError::NoWritePerms, + _ => RmdirError::Io, + } + } + if !valid(name) { + return Err(PathError::InvalidPathname.into()); + } + let path = dir.into_typed::().path; + let locked = self.namespace.lock(); + let resolved = self.resolve_dir(&path).map_err(open_to_rmdir_error)?; + let entry = resolved + .entries + .get(name) + .ok_or(PathError::NoSuchFileOrDirectory)?; + if entry.entry.file_type != FileType::Directory { + return Err(RmdirError::NotADirectory); + } + let (child, _) = self + .resolve_child_dir(&resolved, name) + .map_err(open_to_rmdir_error)?; + if !child.entries.is_empty() { + return Err(RmdirError::NotEmpty); + } + + // As in `unlink_at`, hide the lower directory before removing the upper one. + let upper = match entry.lower { + Some(_) => { + let upper = self + .ensure_upper_dir(&locked, &path) + .map_err(open_to_rmdir_error)?; + self.create_marker(&locked, &upper, &whiteout(name)) + .map_err(open_to_rmdir_error)?; + upper + } + None => resolved.upper.ok_or(RmdirError::Io)?, + }; + if let Some(child) = &child.upper { + let cleared = self + .clear_markers(&locked, child) + .map_err(unlink_to_open_error) + .map_err(open_to_rmdir_error)?; + if let Err(error) = self.upper.rmdir_at(upper, name) { + for marker in cleared { + let _rollback_marker = self.create_marker(&locked, child, &marker); + } + return Err(error); + } + } + Ok(()) + } + + fn chmod(&self, h: HandleRef<'_>, mode: Mode) -> Result<(), ChmodError> { + let locked = self.namespace.lock(); + let handle = self.ensure_upper(locked, h).map_err(|error| match error { + OpenError::PathError(error) => ChmodError::PathError(error), + OpenError::ReadOnlyFileSystem => ChmodError::ReadOnlyFileSystem, + _ => ChmodError::Io, + })?; + self.upper.chmod(handle.as_ref(), mode) + } + + fn chown( + &self, + h: HandleRef<'_>, + user: Option, + group: Option, + ) -> Result<(), ChownError> { + let locked = self.namespace.lock(); + let handle = self.ensure_upper(locked, h).map_err(|error| match error { + OpenError::PathError(error) => ChownError::PathError(error), + OpenError::ReadOnlyFileSystem => ChownError::ReadOnlyFileSystem, + _ => ChownError::Io, + })?; + self.upper.chown(handle.as_ref(), user, group) + } +} From 3992c8d1fc28a8c948f27ca4b2682b7096bc34d2 Mon Sep 17 00:00:00 2001 From: Jay Bosamiya Date: Mon, 17 Aug 2026 17:49:39 -0700 Subject: [PATCH 2/7] Mechanically convert the layered tests over --- litebox/src/fs/tests.rs | 462 ++++++++++++++-------------------------- 1 file changed, 155 insertions(+), 307 deletions(-) diff --git a/litebox/src/fs/tests.rs b/litebox/src/fs/tests.rs index 138dd9d36..e3ae22538 100644 --- a/litebox/src/fs/tests.rs +++ b/litebox/src/fs/tests.rs @@ -26,6 +26,31 @@ fn in_mem_fs(litebox: &crate::LiteBox) -> I ) } +type OverlayFs = crate::fs::resolver::Resolver< + crate::platform::mock::MockPlatform, + crate::fs::overlay::Overlay, +>; + +/// An overlay of `upper` over a tar-backed lower layer. +fn overlay_fs( + litebox: &crate::LiteBox, + upper: crate::fs::in_mem::InMem, + tar_data: alloc::borrow::Cow<'static, [u8]>, +) -> OverlayFs { + crate::fs::resolver::Resolver::new( + litebox, + crate::fs::overlay::Overlay::new( + litebox, + upper, + crate::fs::tar_ro::TarRo::new( + tar_data, + crate::fs::inode_allocator::InodeAllocator::standalone(), + ), + crate::fs::inode_allocator::InodeAllocator::standalone(), + ), + ) +} + mod in_mem { use crate::LiteBox; use crate::fs::in_mem; @@ -1163,10 +1188,10 @@ mod tar_ro { } } -mod layered { +mod overlay { use crate::LiteBox; - use crate::fs::{FileSystem as _, FileType, Mode, OFlags}; - use crate::fs::{in_mem, layered}; + use crate::fs::in_mem::{InMem, InitialNode}; + use crate::fs::{FileSystem as _, FileType, Mode, OFlags, UserInfo}; use crate::platform::mock::MockPlatform; use alloc::vec; use alloc::vec::Vec; @@ -1174,15 +1199,41 @@ mod layered { const TEST_TAR_FILE: &[u8] = include_bytes!("./test.tar"); + /// The user these tests act as, and so the owner of anything they are set up as having created. + const ACTING_USER: UserInfo = UserInfo { + user: 1000, + group: 1000, + }; + const ALL_PERMS: Mode = Mode::RWXU.union(Mode::RWXG).union(Mode::RWXO); + + /// An upper backend whose root is writable by the acting user, holding `entries`. + /// + /// The overlay directs every mutation to the upper backend, so its root has to allow writes for + /// anything to be created; the old `layered` tests chmod-ed `/` as root for the same reason. + fn upper( + entries: impl IntoIterator, + ) -> InMem { + InMem::new_initialized( + [( + "/", + InitialNode::Directory { + mode: ALL_PERMS, + owner: UserInfo::ROOT, + }, + )] + .into_iter() + .chain(entries), + ) + } + + fn overlay_fs(litebox: &LiteBox, upper: InMem) -> super::OverlayFs { + super::overlay_fs(litebox, upper, TEST_TAR_FILE.into()) + } + #[test] fn file_read_from_lower() { let litebox = LiteBox::new(MockPlatform::new()); - let fs = layered::FileSystem::new( - &litebox, - super::in_mem_fs(&litebox), - super::tar_ro_fs(&litebox, TEST_TAR_FILE.into()), - layered::LayeringSemantics::LowerLayerReadOnly, - ); + let fs = overlay_fs(&litebox, upper([])); let fd = fs .open("foo", OFlags::RDONLY, Mode::RWXU) .expect("Failed to open file"); @@ -1217,12 +1268,7 @@ mod layered { #[test] fn dir_and_nonexist_checks() { let litebox = LiteBox::new(MockPlatform::new()); - let fs = layered::FileSystem::new( - &litebox, - super::in_mem_fs(&litebox), - super::tar_ro_fs(&litebox, TEST_TAR_FILE.into()), - layered::LayeringSemantics::LowerLayerReadOnly, - ); + let fs = overlay_fs(&litebox, upper([])); assert!(matches!( fs.open("bar/ba", OFlags::RDONLY, Mode::empty()), Err(crate::fs::errors::OpenError::PathError( @@ -1241,24 +1287,7 @@ mod layered { #[test] fn file_read_write_sync_up() { let litebox = LiteBox::new(MockPlatform::new()); - - let mut in_mem_fs = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut in_mem_fs, |fs| { - // Change the permissions for `/` to allow file creation - // - // TODO: We might need to force-allow file creation in cases where the lower level - // already has the file in the correct mode. This would likely require `stat` as well as - // some internal-only force-creation API. - fs.chmod("/", Mode::RWXU | Mode::RWXG | Mode::RWXO) - .expect("Failed to chmod /"); - }); - - let fs = layered::FileSystem::new( - &litebox, - in_mem_fs, - super::tar_ro_fs(&litebox, TEST_TAR_FILE.into()), - layered::LayeringSemantics::LowerLayerReadOnly, - ); + let fs = overlay_fs(&litebox, upper([])); let fd1 = fs .open("foo", OFlags::RDONLY, Mode::RWXU) .expect("Failed to open file"); @@ -1292,24 +1321,7 @@ mod layered { #[test] fn file_read_write_seek_sync() { let litebox = LiteBox::new(MockPlatform::new()); - - let mut in_mem_fs = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut in_mem_fs, |fs| { - // Change the permissions for `/` to allow file creation - // - // TODO: We might need to force-allow file creation in cases where the lower level - // already has the file in the correct mode. This would likely require `stat` as well as - // some internal-only force-creation API. - fs.chmod("/", Mode::RWXU | Mode::RWXG | Mode::RWXO) - .expect("Failed to chmod /"); - }); - - let fs = layered::FileSystem::new( - &litebox, - in_mem_fs, - super::tar_ro_fs(&litebox, TEST_TAR_FILE.into()), - layered::LayeringSemantics::LowerLayerReadOnly, - ); + let fs = overlay_fs(&litebox, upper([])); let fd1 = fs .open("foo", OFlags::RDONLY, Mode::RWXU) .expect("Failed to open file"); @@ -1339,13 +1351,7 @@ mod layered { #[test] fn file_deletion() { let litebox = LiteBox::new(MockPlatform::new()); - - let fs = layered::FileSystem::new( - &litebox, - super::in_mem_fs(&litebox), - super::tar_ro_fs(&litebox, TEST_TAR_FILE.into()), - layered::LayeringSemantics::LowerLayerReadOnly, - ); + let fs = overlay_fs(&litebox, upper([])); let fd = fs .open("foo", OFlags::RDONLY, Mode::RWXU) .expect("Failed to open file"); @@ -1380,28 +1386,25 @@ mod layered { #[test] fn o_directory_flag_tests() { let litebox = LiteBox::new(MockPlatform::new()); - let mut in_mem_fs = super::in_mem_fs(&litebox); - - in_mem::with_root_privileges(&mut in_mem_fs, |fs| { - fs.chmod("/", Mode::RWXU | Mode::RWXG | Mode::RWXO) - .expect("Failed to chmod /"); - }); - // Create a test directory in the upper layer - in_mem_fs - .mkdir("/upperdir", Mode::RWXU | Mode::RWXG | Mode::RWXO) - .expect("Failed to create directory"); - - // Create a test file in the upper layer - let fd = in_mem_fs - .open("/upperfile", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) - .expect("Failed to create file"); - in_mem_fs.close(&fd).expect("Failed to close file"); - - let fs = layered::FileSystem::new( + let fs = overlay_fs( &litebox, - in_mem_fs, - super::tar_ro_fs(&litebox, TEST_TAR_FILE.into()), - layered::LayeringSemantics::LowerLayerReadOnly, + upper([ + ( + "/upperdir", + InitialNode::Directory { + mode: ALL_PERMS, + owner: ACTING_USER, + }, + ), + ( + "/upperfile", + InitialNode::File { + mode: Mode::RWXU, + owner: ACTING_USER, + data: alloc::borrow::Cow::Borrowed(b""), + }, + ), + ]), ); // Test O_DIRECTORY on directory from lower layer (tar) @@ -1466,18 +1469,7 @@ mod layered { // shadowed by an attempt to create a file. fn file_create_exist_in_lower() { let litebox = LiteBox::new(MockPlatform::new()); - - let mut in_mem_fs = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut in_mem_fs, |fs| { - fs.chmod("/", Mode::RWXU | Mode::RWXG | Mode::RWXO) - .expect("Failed to chmod /"); - }); - let fs = layered::FileSystem::new( - &litebox, - in_mem_fs, - super::tar_ro_fs(&litebox, TEST_TAR_FILE.into()), - layered::LayeringSemantics::LowerLayerReadOnly, - ); + let fs = overlay_fs(&litebox, upper([])); let fd = fs .open("foo", OFlags::RDWR | OFlags::CREAT, Mode::RWXU) .expect("Failed to open file"); @@ -1493,12 +1485,7 @@ mod layered { #[test] fn read_dir_from_lower_layer() { let litebox = LiteBox::new(MockPlatform::new()); - let fs = layered::FileSystem::new( - &litebox, - super::in_mem_fs(&litebox), - super::tar_ro_fs(&litebox, TEST_TAR_FILE.into()), - layered::LayeringSemantics::LowerLayerReadOnly, - ); + let fs = overlay_fs(&litebox, upper([])); // Read bar subdirectory let fd = fs @@ -1520,27 +1507,25 @@ mod layered { #[test] fn read_dir_from_upper_layer() { let litebox = LiteBox::new(MockPlatform::new()); - - let mut in_mem_fs = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut in_mem_fs, |fs| { - // Set up root directory permissions to allow access - fs.chmod("/", Mode::RWXU | Mode::RWXG | Mode::RWXO) - .expect("Failed to chmod /"); - - // Create some files in the upper layer - fs.mkdir("/upperdir", Mode::RWXU | Mode::RWXG | Mode::RWXO) - .expect("Failed to create upperdir"); - let fd = fs - .open("/upperfile", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) - .expect("Failed to create upperfile"); - fs.close(&fd).expect("Failed to close upperfile"); - }); - - let fs = layered::FileSystem::new( + let fs = overlay_fs( &litebox, - in_mem_fs, - super::tar_ro_fs(&litebox, TEST_TAR_FILE.into()), - layered::LayeringSemantics::LowerLayerReadOnly, + upper([ + ( + "/upperdir", + InitialNode::Directory { + mode: ALL_PERMS, + owner: ACTING_USER, + }, + ), + ( + "/upperfile", + InitialNode::File { + mode: Mode::RWXU, + owner: ACTING_USER, + data: alloc::borrow::Cow::Borrowed(b""), + }, + ), + ]), ); // Read root directory (should contain entries from both layers) @@ -1593,19 +1578,7 @@ mod layered { #[test] fn o_excl_layered_tests() { let litebox = LiteBox::new(MockPlatform::new()); - - let mut in_mem_fs = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut in_mem_fs, |fs| { - fs.chmod("/", Mode::RWXU | Mode::RWXG | Mode::RWXO) - .expect("Failed to chmod /"); - }); - - let fs = layered::FileSystem::new( - &litebox, - in_mem_fs, - super::tar_ro_fs(&litebox, TEST_TAR_FILE.into()), - layered::LayeringSemantics::LowerLayerReadOnly, - ); + let fs = overlay_fs(&litebox, upper([])); // Test O_CREAT | O_EXCL on file that exists in lower layer (should fail) // "foo" exists in the tar file @@ -1707,20 +1680,7 @@ mod layered { #[test] fn dir_creation_inside_lower_existing_dir() { let litebox = LiteBox::new(MockPlatform::new()); - - let mut upper = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut upper, |fs| { - fs.chmod("/", Mode::RWXU | Mode::RWXG | Mode::RWXO) - .expect("Failed to chmod / in upper layer"); - }); - - let lower = super::tar_ro_fs(&litebox, TEST_TAR_FILE.into()); - let fs = layered::FileSystem::new( - &litebox, - upper, - lower, - layered::LayeringSemantics::LowerLayerReadOnly, - ); + let fs = overlay_fs(&litebox, upper([])); // Create the directory /bar/test (where /bar already exists inside the tar file) fs.mkdir("/bar/test", Mode::RWXU | Mode::RWXG | Mode::RWXO) @@ -1751,20 +1711,7 @@ mod layered { #[test] fn file_creation_with_ancestor_dir_migration() { let litebox = LiteBox::new(MockPlatform::new()); - - let mut upper = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut upper, |fs| { - fs.chmod("/", Mode::RWXU | Mode::RWXG | Mode::RWXO) - .expect("Failed to chmod / in upper layer"); - }); - - let lower = super::tar_ro_fs(&litebox, TEST_TAR_FILE.into()); - let fs = layered::FileSystem::new( - &litebox, - upper, - lower, - layered::LayeringSemantics::LowerLayerReadOnly, - ); + let fs = overlay_fs(&litebox, upper([])); // Open bar/test for writing (where bar exists in lower layer but test doesn't exist) // This should create ancestor directories and allow file creation @@ -1799,20 +1746,7 @@ mod layered { #[test] fn file_modification_with_ancestor_dir_migration() { let litebox = LiteBox::new(MockPlatform::new()); - - let mut upper = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut upper, |fs| { - fs.chmod("/", Mode::RWXU | Mode::RWXG | Mode::RWXO) - .expect("Failed to chmod / in upper layer"); - }); - - let lower = super::tar_ro_fs(&litebox, TEST_TAR_FILE.into()); - let fs = layered::FileSystem::new( - &litebox, - upper, - lower, - layered::LayeringSemantics::LowerLayerReadOnly, - ); + let fs = overlay_fs(&litebox, upper([])); // Open bar/baz for writing (both bar and baz exist in lower layer) // This should migrate ancestor directories and allow file modification @@ -1848,21 +1782,7 @@ mod layered { #[test] fn open_with_trunc() { let litebox = LiteBox::new(MockPlatform::new()); - - let lower = super::tar_ro_fs(&litebox, TEST_TAR_FILE.into()); - let mut upper = super::in_mem_fs(&litebox); - // Set up write permissions on the upper layer - in_mem::with_root_privileges(&mut upper, |fs| { - fs.chmod("/", Mode::RWXU | Mode::RWXG | Mode::RWXO) - .expect("Failed to chmod / in upper layer"); - }); - - let fs = layered::FileSystem::new( - &litebox, - upper, - lower, - layered::LayeringSemantics::LowerLayerReadOnly, - ); + let fs = overlay_fs(&litebox, upper([])); // Open with O_TRUNC should create a shadow file in upper layer let fd = fs @@ -1898,21 +1818,7 @@ mod layered { use crate::fs::errors::{PathError, RmdirError}; let litebox = LiteBox::new(MockPlatform::new()); - - // Prepare upper with permissive root - let mut upper = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut upper, |fs| { - fs.chmod("/", Mode::RWXU | Mode::RWXG | Mode::RWXO) - .expect("chmod / failed"); - }); - - let lower = super::tar_ro_fs(&litebox, TEST_TAR_FILE.into()); - let fs = layered::FileSystem::new( - &litebox, - upper, - lower, - layered::LayeringSemantics::LowerLayerReadOnly, - ); + let fs = overlay_fs(&litebox, upper([])); // Create an empty directory only in upper layer fs.mkdir("/upper_empty", Mode::RWXU | Mode::RWXG | Mode::RWXO) @@ -1942,18 +1848,7 @@ mod layered { use crate::fs::errors::{PathError, RmdirError}; let litebox = LiteBox::new(MockPlatform::new()); - - let mut upper = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut upper, |fs| { - fs.chmod("/", Mode::RWXU | Mode::RWXG | Mode::RWXO).unwrap(); - }); - let lower = super::tar_ro_fs(&litebox, TEST_TAR_FILE.into()); - let fs = layered::FileSystem::new( - &litebox, - upper, - lower, - layered::LayeringSemantics::LowerLayerReadOnly, - ); + let fs = overlay_fs(&litebox, upper([])); fs.mkdir("/upper_dir", Mode::RWXU | Mode::RWXG | Mode::RWXO) .expect("mkdir upper_dir failed"); @@ -1992,14 +1887,7 @@ mod layered { use crate::fs::errors::RmdirError; let litebox = LiteBox::new(MockPlatform::new()); - let upper = super::in_mem_fs(&litebox); // empty - let lower = super::tar_ro_fs(&litebox, TEST_TAR_FILE.into()); - let fs = layered::FileSystem::new( - &litebox, - upper, - lower, - layered::LayeringSemantics::LowerLayerReadOnly, - ); + let fs = overlay_fs(&litebox, upper([])); // "bar" exists in lower layer and contains "baz" (non-empty) assert!(matches!(fs.rmdir("bar"), Err(RmdirError::NotEmpty))); @@ -2010,18 +1898,7 @@ mod layered { use crate::fs::errors::RmdirError; let litebox = LiteBox::new(MockPlatform::new()); - - let mut upper = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut upper, |fs| { - fs.chmod("/", Mode::RWXU | Mode::RWXG | Mode::RWXO).unwrap(); - }); - let lower = super::tar_ro_fs(&litebox, TEST_TAR_FILE.into()); - let fs = layered::FileSystem::new( - &litebox, - upper, - lower, - layered::LayeringSemantics::LowerLayerReadOnly, - ); + let fs = overlay_fs(&litebox, upper([])); // Create a regular file (upper only) let fd = fs @@ -2047,24 +1924,11 @@ mod layered { use std::time::Duration; let litebox = LiteBox::new(MockPlatform::new()); - - let mut in_mem_fs = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut in_mem_fs, |fs| { - fs.chmod("/", Mode::RWXU | Mode::RWXG | Mode::RWXO) - .expect("Failed to chmod /"); - }); - - let fs = layered::FileSystem::new( - &litebox, - in_mem_fs, - super::tar_ro_fs(&litebox, TEST_TAR_FILE.into()), - layered::LayeringSemantics::LowerLayerReadOnly, - ); + let fs = overlay_fs(&litebox, upper([])); fs.file_status("foo").expect("Failed to stat foo"); - // Writing to the lower-layer file triggers copy-on-write migration via - // `migrate_file_up`. Run it on a worker thread. + // Writing to the lower-layer file triggers copy-up. Run it on a worker thread. let (tx, rx) = mpsc::channel(); thread::spawn(move || { let fd = fs @@ -2163,59 +2027,63 @@ mod stdio { } } -mod layered_stdio { +mod composed_stdio { use crate::LiteBox; + use crate::fs::composer::Composer; use crate::fs::devices::Devices; - use crate::fs::layered::LayeringSemantics; + use crate::fs::in_mem::{InMem, InitialNode}; use crate::fs::resolver::Resolver; - use crate::fs::{FileSystem as _, Mode, OFlags}; - use crate::fs::{in_mem, layered}; + use crate::fs::{FileSystem as _, Mode, OFlags, UserInfo}; use crate::platform::mock::MockPlatform; use alloc::vec; extern crate std; + type ComposedFs = Resolver; + + fn composed_fs(litebox: &LiteBox) -> ComposedFs { + Resolver::new( + litebox, + Composer::builder() + .mount("/", |_| { + InMem::::new_initialized([( + "/", + InitialNode::Directory { + mode: Mode::RWXU | Mode::RWXG | Mode::RWXO, + owner: UserInfo::ROOT, + }, + )]) + }) + .mount("/dev", |allocator| Devices::new(litebox, allocator)) + .build() + .unwrap(), + ) + } + #[test] - fn layered_stdio_open_read_write() { + fn stdio_open_read_write() { let platform = MockPlatform::new(); let litebox = LiteBox::new(platform); - let layered_fs = layered::FileSystem::new( - &litebox, - super::in_mem_fs(&litebox), - Resolver::new( - &litebox, - crate::fs::composer::Composer::builder() - .mount("/dev", |allocator| Devices::new(&litebox, allocator)) - .build() - .unwrap(), - ), - LayeringSemantics::LowerLayerWritableFiles, - ); + let fs = composed_fs(&litebox); // Test opening and writing to /dev/stdout - let fd_stdout = layered_fs + let fd_stdout = fs .open("/dev/stdout", OFlags::WRONLY, Mode::empty()) .expect("Failed to open /dev/stdout"); - let data = b"Hello, layered stdout!"; - layered_fs - .write(&fd_stdout, data, None) + let data = b"Hello, composed stdout!"; + fs.write(&fd_stdout, data, None) .expect("Failed to write to /dev/stdout"); - layered_fs - .close(&fd_stdout) - .expect("Failed to close /dev/stdout"); + fs.close(&fd_stdout).expect("Failed to close /dev/stdout"); assert_eq!(platform.stdout_queue.read().unwrap().len(), 1); assert_eq!(platform.stdout_queue.read().unwrap()[0], data); // Test opening and writing to /dev/stderr - let fd_stderr = layered_fs + let fd_stderr = fs .open("/dev/stderr", OFlags::WRONLY, Mode::empty()) .expect("Failed to open /dev/stderr"); - let data = b"Hello, layered stderr!"; - layered_fs - .write(&fd_stderr, data, None) + let data = b"Hello, composed stderr!"; + fs.write(&fd_stderr, data, None) .expect("Failed to write to /dev/stderr"); - layered_fs - .close(&fd_stderr) - .expect("Failed to close /dev/stderr"); + fs.close(&fd_stderr).expect("Failed to close /dev/stderr"); assert_eq!(platform.stderr_queue.read().unwrap().len(), 1); assert_eq!(platform.stderr_queue.read().unwrap()[0], data); @@ -2224,42 +2092,22 @@ mod layered_stdio { .stdin_queue .write() .unwrap() - .push_back(b"Hello, layered stdin!".to_vec()); - let fd_stdin = layered_fs + .push_back(b"Hello, composed stdin!".to_vec()); + let fd_stdin = fs .open("/dev/stdin", OFlags::RDONLY, Mode::empty()) .expect("Failed to open /dev/stdin"); let mut buffer = vec![0; 1024]; - let bytes_read = layered_fs + let bytes_read = fs .read(&fd_stdin, &mut buffer, None) .expect("Failed to read from /dev/stdin"); - assert_eq!(&buffer[..bytes_read], b"Hello, layered stdin!"); - layered_fs - .close(&fd_stdin) - .expect("Failed to close /dev/stdin"); + assert_eq!(&buffer[..bytes_read], b"Hello, composed stdin!"); + fs.close(&fd_stdin).expect("Failed to close /dev/stdin"); } #[test] - fn layered_write_to_non_dev() { + fn write_to_non_dev() { let litebox = LiteBox::new(MockPlatform::new()); - let in_mem = { - let mut in_mem = super::in_mem_fs(&litebox); - in_mem::with_root_privileges(&mut in_mem, |fs| { - fs.chmod("/", Mode::RWXU | Mode::RWXG | Mode::RWXO).unwrap(); - }); - in_mem - }; - let fs = layered::FileSystem::new( - &litebox, - in_mem, - Resolver::new( - &litebox, - crate::fs::composer::Composer::builder() - .mount("/dev", |allocator| Devices::new(&litebox, allocator)) - .build() - .unwrap(), - ), - LayeringSemantics::LowerLayerWritableFiles, - ); + let fs = composed_fs(&litebox); // Test file creation let path = "/testfile"; From f1e778787e641a611ab6557fbeadf83a77ba162d Mon Sep 17 00:00:00 2001 From: Jay Bosamiya Date: Mon, 17 Aug 2026 18:17:55 -0700 Subject: [PATCH 3/7] Rename things away from layered --- litebox/src/fs/tests.rs | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/litebox/src/fs/tests.rs b/litebox/src/fs/tests.rs index e3ae22538..9b2d12143 100644 --- a/litebox/src/fs/tests.rs +++ b/litebox/src/fs/tests.rs @@ -1209,7 +1209,7 @@ mod overlay { /// An upper backend whose root is writable by the acting user, holding `entries`. /// /// The overlay directs every mutation to the upper backend, so its root has to allow writes for - /// anything to be created; the old `layered` tests chmod-ed `/` as root for the same reason. + /// anything to be created. fn upper( entries: impl IntoIterator, ) -> InMem { @@ -1281,11 +1281,10 @@ mod overlay { fs.close(&fd).expect("Failed to close dir"); } - /// Check that for the same file, even though it started as a lower-level file, writing to it - /// successfully migrated it to an upper-level file, and converted the internal descriptors - /// over, such that the expected semantics of being able to see the updated file are held. + /// Check that for the same file, even though it started as a lower file, writing to it copies + /// it up and redirects handles already open on it, so every descriptor sees the update. #[test] - fn file_read_write_sync_up() { + fn file_read_write_copy_up() { let litebox = LiteBox::new(MockPlatform::new()); let fs = overlay_fs(&litebox, upper([])); let fd1 = fs @@ -1316,10 +1315,10 @@ mod overlay { fs.close(&fd2).expect("Failed to close file"); } - /// Similar to [`file_read_write_sync_up`] but also confirm that file positions have been + /// Similar to [`file_read_write_copy_up`] but also confirm that file positions have been /// maintained. #[test] - fn file_read_write_seek_sync() { + fn file_read_write_copy_up_keeps_position() { let litebox = LiteBox::new(MockPlatform::new()); let fs = overlay_fs(&litebox, upper([])); let fd1 = fs @@ -1576,7 +1575,7 @@ mod overlay { } #[test] - fn o_excl_layered_tests() { + fn o_excl_tests() { let litebox = LiteBox::new(MockPlatform::new()); let fs = overlay_fs(&litebox, upper([])); @@ -1600,7 +1599,7 @@ mod overlay { ) .expect("Failed to create new file with O_CREAT | O_EXCL"); - fs.write(&fd, b"layered test", None) + fs.write(&fd, b"overlay test", None) .expect("Failed to write to new file"); fs.close(&fd).expect("Failed to close new file"); @@ -1709,7 +1708,7 @@ mod overlay { } #[test] - fn file_creation_with_ancestor_dir_migration() { + fn file_creation_materializes_ancestor_dirs() { let litebox = LiteBox::new(MockPlatform::new()); let fs = overlay_fs(&litebox, upper([])); @@ -1744,12 +1743,12 @@ mod overlay { } #[test] - fn file_modification_with_ancestor_dir_migration() { + fn file_modification_materializes_ancestor_dirs() { let litebox = LiteBox::new(MockPlatform::new()); let fs = overlay_fs(&litebox, upper([])); // Open bar/baz for writing (both bar and baz exist in lower layer) - // This should migrate ancestor directories and allow file modification + // This copies up the ancestor directories and allows the file to be modified let fd = fs .open("bar/baz", OFlags::WRONLY, Mode::RWXU) .expect("Failed to open bar/baz for writing"); @@ -1784,7 +1783,7 @@ mod overlay { let litebox = LiteBox::new(MockPlatform::new()); let fs = overlay_fs(&litebox, upper([])); - // Open with O_TRUNC should create a shadow file in upper layer + // Open with O_TRUNC should copy the file up into the upper backend, empty let fd = fs .open("foo", OFlags::RDWR | OFlags::TRUNC, Mode::empty()) .expect("Failed to open file with O_TRUNC"); @@ -1918,7 +1917,7 @@ mod overlay { } #[test] - fn migrate_file_up_does_not_deadlock() { + fn copy_up_does_not_deadlock() { use std::sync::mpsc; use std::thread; use std::time::Duration; @@ -1940,7 +1939,7 @@ mod overlay { }); rx.recv_timeout(Duration::from_secs(2)) - .expect("migrate_file_up deadlocked"); + .expect("copy-up deadlocked"); } } From a44b7d27224b25b296cf49dfaa821527403ad665 Mon Sep 17 00:00:00 2001 From: Jay Bosamiya Date: Mon, 17 Aug 2026 18:34:53 -0700 Subject: [PATCH 4/7] Support nestable device mounts for inode allocation --- litebox/src/fs/composer.rs | 22 +++++++++++++++------- litebox/src/fs/inode_allocator.rs | 28 ++++++++++++++++++++++++---- 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/litebox/src/fs/composer.rs b/litebox/src/fs/composer.rs index 89a5f760a..5453c3f12 100644 --- a/litebox/src/fs/composer.rs +++ b/litebox/src/fs/composer.rs @@ -17,7 +17,7 @@ use super::errors::{ ChmodError, ChownError, FileStatusError, MkdirError, OpenError, PathError, ReadDirError, ReadError, RmdirError, TruncateError, UnlinkError, WalkError, WriteError, }; -use super::inode_allocator::InodeAllocator; +use super::inode_allocator::{InodeAllocator, InodeAllocators}; use super::{DirEntry, FileStatus, FileType, Mode, NodeInfo, OFlags, UserInfo}; use crate::path::Arg; use thiserror::Error; @@ -37,7 +37,7 @@ pub struct Composer { /// A [`Composer`] builder. pub struct ComposerBuilder { mounts: Vec<(Option, Box)>, - next_backend_device_id: u64, + allocators: InodeAllocators, } /// A mounted backend. @@ -70,7 +70,7 @@ impl Composer { pub fn builder() -> ComposerBuilder { ComposerBuilder { mounts: vec![], - next_backend_device_id: 1, + allocators: InodeAllocators::starting_at(1), } } } @@ -79,16 +79,24 @@ impl ComposerBuilder { /// Add a backend mounted at `path`. #[must_use] pub fn mount( - mut self, + self, path: impl Arg, backend: impl FnOnce(InodeAllocator) -> B, ) -> Self { - let backend_device_id = self.next_backend_device_id; + self.mount_nestable(path, |allocators| backend(allocators.next())) + } + + /// Add a backend mounted at `path`, which may draw an allocator per backend it is made of. + #[must_use] + pub fn mount_nestable( + mut self, + path: impl Arg, + backend: impl FnOnce(&InodeAllocators) -> B, + ) -> Self { // TODO(jayb): Decide whether we need a fallible version of closure-based mount. - let backend = backend(InodeAllocator::for_device(backend_device_id)); + let backend = backend(&self.allocators); self.mounts .push((path.as_rust_str().map(Into::into).ok(), Box::new(backend))); - self.next_backend_device_id = backend_device_id + 1; self } diff --git a/litebox/src/fs/inode_allocator.rs b/litebox/src/fs/inode_allocator.rs index 5f7df5eca..1d6424e4a 100644 --- a/litebox/src/fs/inode_allocator.rs +++ b/litebox/src/fs/inode_allocator.rs @@ -5,6 +5,27 @@ use core::sync::atomic::{AtomicU64, Ordering}; use super::NodeInfo; +/// Hands out [`InodeAllocator`]s, each with its own device id. +#[derive(Debug)] +pub struct InodeAllocators { + next_device_id: AtomicU64, +} + +impl InodeAllocators { + /// Start handing out allocators, beginning at `first_device_id`. + pub(super) fn starting_at(first_device_id: u64) -> Self { + Self { + next_device_id: AtomicU64::new(first_device_id), + } + } + + /// Hand out an allocator for one backend. + #[must_use] + pub fn next(&self) -> InodeAllocator { + InodeAllocator::for_device(self.next_device_id.fetch_add(1, Ordering::Relaxed)) + } +} + /// Allocator for `(device_id, inode)` pairs scoped to one backend instance. #[derive(Debug)] pub struct InodeAllocator { @@ -13,10 +34,9 @@ pub struct InodeAllocator { } impl InodeAllocator { - /// Construct an allocator for a specific `device_id`. The composer hands - /// out unique `device_id`s per mounted backend. + /// Construct an allocator for a specific `device_id`. #[must_use] - pub fn for_device(device_id: u64) -> Self { + pub(super) fn for_device(device_id: u64) -> Self { Self { device_id, counter: AtomicU64::new(1), @@ -27,7 +47,7 @@ impl InodeAllocator { /// /// This should (eventually) disappear once we have better device ID allocation setup. #[must_use] - pub fn standalone() -> Self { + pub(crate) fn standalone() -> Self { // `b"Stnd".hex()` const STANDALONE_DEVICE_ID: u64 = 0x53746e64; Self::for_device(STANDALONE_DEVICE_ID) From ec8b3d7f38d687f7e29c1c976aa6372cfcd11635 Mon Sep 17 00:00:00 2001 From: Jay Bosamiya Date: Mon, 17 Aug 2026 18:49:42 -0700 Subject: [PATCH 5/7] Migrate Linux shim to overlay --- .../src/lib.rs | 24 ++++----- .../tests/common/mod.rs | 20 +++----- litebox_runner_linux_userland/src/lib.rs | 6 +-- litebox_runner_linux_userland/tests/loader.rs | 22 ++++---- litebox_shim_linux/src/lib.rs | 51 ++++++------------- litebox_shim_linux/src/syscalls/tests.rs | 20 +++----- 6 files changed, 51 insertions(+), 92 deletions(-) diff --git a/litebox_runner_linux_on_windows_userland/src/lib.rs b/litebox_runner_linux_on_windows_userland/src/lib.rs index 3c1fc3073..61aed8619 100644 --- a/litebox_runner_linux_on_windows_userland/src/lib.rs +++ b/litebox_runner_linux_on_windows_userland/src/lib.rs @@ -70,27 +70,21 @@ pub fn run(cli_args: CliArgs) -> Result<()> { let platform = Platform::new(); let shim_builder = litebox_shim_linux::LinuxShimBuilder::new(platform); - let litebox = shim_builder.litebox(); // The program path is a Unix-style path inside the tar archive. let prog_path = &cli_args.program_and_arguments[0]; let initial_file_system = { - let in_mem = litebox::fs::resolver::Resolver::new( - litebox, - litebox::fs::in_mem::InMem::new_initialized([( - "/tmp", - litebox::fs::in_mem::InitialNode::Directory { - mode: litebox::fs::Mode::RWXU - | litebox::fs::Mode::RWXG - | litebox::fs::Mode::RWXO, - owner: litebox::fs::UserInfo { - user: 1000, - group: 1000, - }, + let in_mem = litebox::fs::in_mem::InMem::new_initialized([( + "/tmp", + litebox::fs::in_mem::InitialNode::Directory { + mode: litebox::fs::Mode::RWXU | litebox::fs::Mode::RWXG | litebox::fs::Mode::RWXO, + owner: litebox::fs::UserInfo { + user: 1000, + group: 1000, }, - )]), - ); + }, + )]); shim_builder.default_fs(in_mem, tar_data.into()) }; diff --git a/litebox_runner_linux_on_windows_userland/tests/common/mod.rs b/litebox_runner_linux_on_windows_userland/tests/common/mod.rs index 865ae0222..9be029823 100644 --- a/litebox_runner_linux_on_windows_userland/tests/common/mod.rs +++ b/litebox_runner_linux_on_windows_userland/tests/common/mod.rs @@ -22,24 +22,20 @@ impl TestLauncher { ) -> Self { let platform = Platform::new(); let shim_builder = litebox_shim_linux::LinuxShimBuilder::new(platform); - let litebox = shim_builder.litebox(); - let in_mem_fs = litebox::fs::resolver::Resolver::new( - litebox, - litebox::fs::in_mem::InMem::new_initialized([( - "/", - litebox::fs::in_mem::InitialNode::Directory { - mode: Mode::RWXU | Mode::RWXG | Mode::RWXO, - owner: litebox::fs::UserInfo::ROOT, - }, - )]), - ); + let in_mem = litebox::fs::in_mem::InMem::new_initialized([( + "/", + litebox::fs::in_mem::InitialNode::Directory { + mode: Mode::RWXU | Mode::RWXG | Mode::RWXO, + owner: litebox::fs::UserInfo::ROOT, + }, + )]); let tar_data = if tar_data.is_empty() { litebox::fs::tar_ro::EMPTY_TAR_FILE.into() } else { tar_data.into() }; - let fs = shim_builder.default_fs(in_mem_fs, tar_data); + let fs = shim_builder.default_fs(in_mem, tar_data); let mut this = Self { platform, shim_builder, diff --git a/litebox_runner_linux_userland/src/lib.rs b/litebox_runner_linux_userland/src/lib.rs index 746469b57..9581c9501 100644 --- a/litebox_runner_linux_userland/src/lib.rs +++ b/litebox_runner_linux_userland/src/lib.rs @@ -201,7 +201,6 @@ pub fn run(cli_args: CliArgs) -> Result<()> { } let shim_builder = litebox_shim_linux::LinuxShimBuilder::new(platform); - let litebox = shim_builder.litebox(); // SAFETY: `gettid` takes no pointer arguments and has no Rust-side aliasing requirements. let tid = unsafe { libc::syscall(libc::SYS_gettid) } .try_into() @@ -289,10 +288,7 @@ pub fn run(cli_args: CliArgs) -> Result<()> { )); } - let in_mem = litebox::fs::resolver::Resolver::new( - litebox, - litebox::fs::in_mem::InMem::new_initialized(entries), - ); + let in_mem = litebox::fs::in_mem::InMem::new_initialized(entries); shim_builder.default_fs(in_mem, tar_data.into()) }; diff --git a/litebox_runner_linux_userland/tests/loader.rs b/litebox_runner_linux_userland/tests/loader.rs index ebc2e5cd1..28f0d7f30 100644 --- a/litebox_runner_linux_userland/tests/loader.rs +++ b/litebox_runner_linux_userland/tests/loader.rs @@ -23,24 +23,20 @@ impl TestLauncher { ) -> Self { let platform = Platform::new(tun_device_name); let shim_builder = litebox_shim_linux::LinuxShimBuilder::new(platform); - let litebox = shim_builder.litebox(); - - let in_mem_fs = litebox::fs::resolver::Resolver::new( - litebox, - litebox::fs::in_mem::InMem::new_initialized([( - "/", - litebox::fs::in_mem::InitialNode::Directory { - mode: Mode::RWXU | Mode::RWXG | Mode::RWXO, - owner: litebox::fs::UserInfo::ROOT, - }, - )]), - ); + + let in_mem = litebox::fs::in_mem::InMem::new_initialized([( + "/", + litebox::fs::in_mem::InitialNode::Directory { + mode: Mode::RWXU | Mode::RWXG | Mode::RWXO, + owner: litebox::fs::UserInfo::ROOT, + }, + )]); let tar_data = if tar_data.is_empty() { litebox::fs::tar_ro::EMPTY_TAR_FILE.into() } else { tar_data.into() }; - let fs = shim_builder.default_fs(in_mem_fs, tar_data); + let fs = shim_builder.default_fs(in_mem, tar_data); let mut this = Self { platform, shim_builder, diff --git a/litebox_shim_linux/src/lib.rs b/litebox_shim_linux/src/lib.rs index 1a0efeb3d..c2808238a 100644 --- a/litebox_shim_linux/src/lib.rs +++ b/litebox_shim_linux/src/lib.rs @@ -56,15 +56,8 @@ use crate::syscalls::file::get_file_descriptor_flags; pub type DefaultFS = LinuxFS; -pub(crate) type LinuxFS = litebox::fs::layered::FileSystem< - Platform, - litebox::fs::resolver::Resolver>, - litebox::fs::layered::FileSystem< - Platform, - litebox::fs::resolver::Resolver, - litebox::fs::resolver::Resolver, - >, ->; +pub(crate) type LinuxFS = + litebox::fs::resolver::Resolver; pub(crate) type FileFd = litebox::fd::TypedFd; @@ -219,13 +212,13 @@ impl LinuxShimBuilder { &self.litebox } - /// Create a default layered file system with the given in-memory layer and tar data. + /// Create the default file system with the given in-memory layer and tar data. pub fn default_fs( &self, - in_mem_fs: litebox::fs::resolver::Resolver>, + in_mem: litebox::fs::in_mem::InMem, tar_data: Cow<'static, [u8]>, ) -> DefaultFS { - default_fs(&self.litebox, in_mem_fs, tar_data) + default_fs(&self.litebox, in_mem, tar_data) } /// Build the shim. @@ -376,40 +369,28 @@ impl LinuxShimProcess { } } -/// Create a default layered file system with the given in-memory layer and tar data. +/// Create the default file system with the given in-memory layer and tar data. fn default_fs( litebox: &LiteBox, - in_mem_fs: litebox::fs::resolver::Resolver>, + in_mem: litebox::fs::in_mem::InMem, tar_data: Cow<'static, [u8]>, ) -> LinuxFS { - let dev_stdio = litebox::fs::resolver::Resolver::new( + litebox::fs::resolver::Resolver::new( litebox, litebox::fs::composer::Composer::builder() + .mount_nestable("/", |allocators| { + litebox::fs::overlay::Overlay::new( + litebox, + in_mem, + litebox::fs::tar_ro::TarRo::new(tar_data, allocators.next()), + allocators.next(), + ) + }) .mount("/dev", |allocator| { litebox::fs::devices::Devices::new(litebox, allocator) }) .build() .unwrap(), - ); - let tar_ro = litebox::fs::resolver::Resolver::new( - litebox, - litebox::fs::composer::Composer::builder() - .mount("/", |allocator| { - litebox::fs::tar_ro::TarRo::new(tar_data, allocator) - }) - .build() - .unwrap(), - ); - litebox::fs::layered::FileSystem::new( - litebox, - in_mem_fs, - litebox::fs::layered::FileSystem::new( - litebox, - dev_stdio, - tar_ro, - litebox::fs::layered::LayeringSemantics::LowerLayerReadOnly, - ), - litebox::fs::layered::LayeringSemantics::LowerLayerWritableFiles, ) } diff --git a/litebox_shim_linux/src/syscalls/tests.rs b/litebox_shim_linux/src/syscalls/tests.rs index 8f418c79e..a371fb929 100644 --- a/litebox_shim_linux/src/syscalls/tests.rs +++ b/litebox_shim_linux/src/syscalls/tests.rs @@ -52,18 +52,14 @@ pub(crate) fn init_platform( let platform = test_platform(tun_device_name); let shim_builder = crate::LinuxShimBuilder::new(platform); - let litebox = shim_builder.litebox(); - let in_mem_fs = litebox::fs::resolver::Resolver::new( - litebox, - litebox::fs::in_mem::InMem::new_initialized([( - "/", - litebox::fs::in_mem::InitialNode::Directory { - mode: Mode::RWXU | Mode::RWXG | Mode::RWXO, - owner: litebox::fs::UserInfo::ROOT, - }, - )]), - ); - let fs = alloc::sync::Arc::new(shim_builder.default_fs(in_mem_fs, TEST_TAR_FILE.into())); + let in_mem = litebox::fs::in_mem::InMem::new_initialized([( + "/", + litebox::fs::in_mem::InitialNode::Directory { + mode: Mode::RWXU | Mode::RWXG | Mode::RWXO, + owner: litebox::fs::UserInfo::ROOT, + }, + )]); + let fs = alloc::sync::Arc::new(shim_builder.default_fs(in_mem, TEST_TAR_FILE.into())); let task = shim_builder.build().0.new_test_task(fs); if tun_device_name.is_some() { From a78235f116036ea078be1115eebd40e6c1764f5d Mon Sep 17 00:00:00 2001 From: Jay Bosamiya Date: Mon, 17 Aug 2026 18:53:16 -0700 Subject: [PATCH 6/7] Migrate snp to overlay --- litebox_runner_snp/src/main.rs | 55 +++++++++------------------------- 1 file changed, 14 insertions(+), 41 deletions(-) diff --git a/litebox_runner_snp/src/main.rs b/litebox_runner_snp/src/main.rs index edd12f270..c3d376cf9 100644 --- a/litebox_runner_snp/src/main.rs +++ b/litebox_runner_snp/src/main.rs @@ -34,15 +34,7 @@ impl log::Log for HostLogger { static HOST_LOGGER: HostLogger = HostLogger; type Platform = litebox_platform_linux_kernel::host::snp::snp_impl::SnpLinuxKernel; -type DefaultFS = litebox::fs::layered::FileSystem< - Platform, - litebox::fs::resolver::Resolver>, - litebox::fs::layered::FileSystem< - Platform, - litebox::fs::resolver::Resolver, - litebox::fs::resolver::Resolver, - >, ->; +type DefaultFS = litebox::fs::resolver::Resolver; type Shim = litebox_shim_linux::LinuxShim; @@ -207,16 +199,6 @@ pub extern "C" fn sandbox_process_init( #[allow(clippy::missing_panics_doc)] let shim = SHIM.get().expect("initialized"); let litebox = shim.litebox(); - let in_mem_fs = litebox::fs::resolver::Resolver::new( - litebox, - litebox::fs::in_mem::InMem::new_initialized([( - "/tmp", - litebox::fs::in_mem::InitialNode::Directory { - mode: litebox::fs::Mode::RWXU | litebox::fs::Mode::RWXG | litebox::fs::Mode::RWXO, - owner: litebox::fs::UserInfo::ROOT, - }, - )]), - ); let socket_addr = core::net::SocketAddr::V4(core::net::SocketAddrV4::new( core::net::Ipv4Addr::new(10, 0, 0, 1), @@ -229,7 +211,7 @@ pub extern "C" fn sandbox_process_init( globals::SM_TERM_GENERAL, ); }; - let nine_p_composer = litebox::fs::composer::Composer::builder() + let composer = litebox::fs::composer::Composer::builder() .mount("/", |allocator| { let Ok(backend) = litebox::fs::nine_p::NineP::::new( transport, 65536, "root", "/tmp", allocator, @@ -242,36 +224,27 @@ pub extern "C" fn sandbox_process_init( }; backend }) - .build() - .unwrap_or_else( - |(litebox::fs::composer::BuildError::NoMounts - | litebox::fs::composer::BuildError::InvalidMountPath - | litebox::fs::composer::BuildError::DuplicateMountPath)| unreachable!(), - ); - let nine_p = litebox::fs::resolver::Resolver::new(litebox, nine_p_composer); - let dev_stdio_composer = litebox::fs::composer::Composer::builder() .mount("/dev", |allocator| { litebox::fs::devices::Devices::new(litebox, allocator) }) + .mount("/tmp", |_allocator| { + litebox::fs::in_mem::InMem::::new_initialized([( + "/", + litebox::fs::in_mem::InitialNode::Directory { + mode: litebox::fs::Mode::RWXU + | litebox::fs::Mode::RWXG + | litebox::fs::Mode::RWXO, + owner: litebox::fs::UserInfo::ROOT, + }, + )]) + }) .build() .unwrap_or_else( |(litebox::fs::composer::BuildError::NoMounts | litebox::fs::composer::BuildError::InvalidMountPath | litebox::fs::composer::BuildError::DuplicateMountPath)| unreachable!(), ); - let dev_stdio = litebox::fs::resolver::Resolver::new(litebox, dev_stdio_composer); - let default_fs = litebox::fs::layered::FileSystem::new( - litebox, - in_mem_fs, - litebox::fs::layered::FileSystem::new( - litebox, - dev_stdio, - nine_p, - litebox::fs::layered::LayeringSemantics::LowerLayerReadOnly, - ), - litebox::fs::layered::LayeringSemantics::LowerLayerWritableFiles, - ); - let fs = alloc::sync::Arc::new(default_fs); + let fs = alloc::sync::Arc::new(litebox::fs::resolver::Resolver::new(litebox, composer)); // Loading a program may trigger page faults, so we need to set SHIM before this. let program = match shim.load_program(fs, platform.init_task(boot_params), &program, argv, envp) From 4e877478e9f5d7957634199054850945111f0ce8 Mon Sep 17 00:00:00 2001 From: Jay Bosamiya Date: Mon, 17 Aug 2026 18:56:05 -0700 Subject: [PATCH 7/7] Drop the old layered file system --- litebox/src/fs/layered.rs | 1447 ------------------------------------- litebox/src/fs/mod.rs | 1 - 2 files changed, 1448 deletions(-) delete mode 100644 litebox/src/fs/layered.rs diff --git a/litebox/src/fs/layered.rs b/litebox/src/fs/layered.rs deleted file mode 100644 index 226523f1f..000000000 --- a/litebox/src/fs/layered.rs +++ /dev/null @@ -1,1447 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -//! An layered file system, layering on [`FileSystem`](super::FileSystem) on top of another. - -use alloc::string::String; -use alloc::sync::Arc; -use alloc::vec::Vec; -use core::sync::atomic::{AtomicUsize, Ordering::SeqCst}; -use hashbrown::{HashMap, HashSet}; - -use crate::LiteBox; -use crate::fd::{InternalFd, TypedFd}; -use crate::path::Arg; -use crate::sync; - -use super::errors::{ - ChmodError, ChownError, CloseError, FileStatusError, MkdirError, OpenError, PathError, - ReadDirError, ReadError, RmdirError, SeekError, TruncateError, UnlinkError, WriteError, -}; -use super::{DirEntry, FileStatus, FileType, Mode, NodeInfo, OFlags, SeekWhence}; - -/// Just a random constant that is distinct from other file systems. In this case, it is -/// `b'Lyrs'.hex()`. -const DEVICE_ID: usize = 0x4c797273; - -/// Possible semantics for layering file systems together -#[non_exhaustive] -pub enum LayeringSemantics { - /// Lower layer is read-only. - /// - /// Any writes to the lower layer have copy-on-write semantics, copying it over to the upper - /// layer, before performing the write. - LowerLayerReadOnly, - /// Lower layer's files are writable. - /// - /// No new files can be made at the lower layer, but any existing files in the lower layer can - /// still be written to. If an upper level file exists with the same name as a lower layer file, - /// then it is shadowed, and only the upper layer file would be visible. - LowerLayerWritableFiles, -} - -/// A backing implementation of [`FileSystem`](super::FileSystem) that layers a file system on top -/// of another. -/// -/// This particular implementation itself doesn't carry or store any of the files, but delegates to -/// each of the layers. Specifically, this implementation will look for and work with files in -/// the upper layer, unless they don't exist, in which case the lower layer is looked at. -/// -/// The current design of layering supports treating the lower layer as read-only, or as a -/// transparent write-through. In read-only lower layer, if a file is opened in writable mode that -/// doesn't exist in the upper layer, but _does_ exist in the lower layer, this will have -/// copy-on-write semantics. -/// -/// Future versions of the layering might support other configurable options for the layering. -pub struct FileSystem< - Platform: sync::RawSyncPrimitivesProvider, - Upper: super::FileSystem + 'static, - Lower: super::FileSystem + 'static, -> { - litebox: LiteBox, - upper: Upper, - lower: Lower, - // TODO: Possibly support a single-threaded variant that doesn't have the cost of requiring a - // sync-primitives platform, as well as cost of mutexes and such? - root: sync::RwLock>, - layering_semantics: LayeringSemantics, - // cwd invariant: always ends with a `/` - current_working_dir: String, - node_info_lookup: sync::RwLock>, -} - -impl - FileSystem -{ - /// Construct a new `FileSystem` instance - #[must_use] - pub fn new( - litebox: &LiteBox, - upper: Upper, - lower: Lower, - layering_semantics: LayeringSemantics, - ) -> Self { - let root = sync::RwLock::new(RootDir::new()); - let node_info_lookup = sync::RwLock::new(HashMap::new()); - Self { - litebox: litebox.clone(), - upper, - lower, - root, - current_working_dir: "/".into(), - layering_semantics, - node_info_lookup, - } - } - - /// (private-only) check if the lower level has the path; if there is an I/O or path failure, - /// propagate the relevant error. - fn ensure_lower_contains(&self, path: &str) -> Result { - self.lower.file_status(path).map(|stat| stat.file_type) - } - - /// (private-only) Create all parent/ancestor directories for a `path`, making sure that each of - /// these exist in the lower layer. It does _not_ set up `path` itself on the upper layer - /// though; this is left to the callee to handle. - /// - /// NOTE: This is _not_ equivalent to running `mkdir -p {path}` or `mkdir {path}` or anything - /// like that. - fn mkdir_migrating_ancestor_dirs(&self, path: &str) -> Result<(), MkdirError> { - let path = self.absolute_path(path)?; - for dir in path.increasing_ancestors().map_err(PathError::from)? { - if dir == path { - return Ok(()); - } - match self.ensure_lower_contains(dir) { - Ok(FileType::Directory) => { - // The dir does in fact exist; we just need to confirm that the upper layer also - // has it. - match self - .upper - .mkdir(dir, self.lower.file_status(dir).unwrap().mode) - { - Ok(()) => { - // fallthrough to next increasing ancestor - } - Err(e) => match e { - MkdirError::AlreadyExists => { - // perfectly fine, just fallthrough to next place in the loop - } - MkdirError::ReadOnlyFileSystem - | MkdirError::Io - | MkdirError::NoWritePerms - | MkdirError::PathError( - PathError::ComponentNotADirectory - | PathError::InvalidPathname - | PathError::NoSearchPerms { .. }, - ) => { - return Err(e); - } - MkdirError::PathError( - PathError::NoSuchFileOrDirectory | PathError::MissingComponent, - ) => { - unreachable!() - } - }, - } - } - Ok(FileType::RegularFile | FileType::CharacterDevice) - | Err( - FileStatusError::PathError(PathError::MissingComponent) - | FileStatusError::ClosedFd, - ) => unreachable!(), - Err(FileStatusError::PathError(PathError::ComponentNotADirectory)) => { - unimplemented!() - } - Err(FileStatusError::PathError(PathError::InvalidPathname)) => { - unreachable!("we just confirmed valid path") - } - Err(FileStatusError::PathError(e @ PathError::NoSearchPerms { .. })) => { - Err(e)?; - } - Err(FileStatusError::PathError(PathError::NoSuchFileOrDirectory)) => { - assert_ne!(dir, path); - Err(PathError::MissingComponent)?; - } - Err(FileStatusError::Io) => return Err(MkdirError::Io), - } - } - // The loop above should return at one of its return points - unreachable!() - } - - /// (private-only) Migrate a file from lower to upper layer - /// - /// It performs a check to make sure that the lower level has the file, and if the lower-level - /// does not, then it will error out with the relevant `PathError` that can be propagated as - /// necessary. - /// - /// Note: this focuses only on files. - /// - /// If `copy_data` is `true`, it copies over the lower data to the upper one, otherwise, it - /// makes the upper file empty (similar to a truncate). Generally speaking, you want to use - /// `true` for `copy_data`. - fn migrate_file_up(&self, path: &str, copy_data: bool) -> Result<(), MigrationError> { - match self.layering_semantics { - LayeringSemantics::LowerLayerReadOnly => { - // fallthrough - } - LayeringSemantics::LowerLayerWritableFiles => { - // If this is ever hit, then that specific layered function calling this - // `migrate_file_up` function needs to be looked at to make sure that it is - // implemented correctly and update its semantics if necessary. The - // `migrate_file_up` functionality was implemented when there was only one set of - // semantics for layered file systems (namely `LowerLayerReadOnly`), thus the file - // system may not correctly account for other situations just yet (specifically, - // some situations might attempt to migrate files when they shouldn't). This - // particular panic is simply to catch such cases. - unreachable!() - } - } - - // We first open the file up at the lower level for reading - let lower_fd = match self.lower.open(path, OFlags::RDONLY, Mode::empty()) { - Ok(fd) => fd, - Err(e) => match e { - OpenError::AccessNotAllowed => return Err(MigrationError::NoReadPerms), - OpenError::Io => return Err(MigrationError::Io), - OpenError::NoWritePerms - | OpenError::ReadOnlyFileSystem - | OpenError::AlreadyExists - | OpenError::TruncateError(_) => unreachable!(), - OpenError::PathError(path_error) => return Err(path_error)?, - }, - }; - // We begin to read the lower file before opening the upper file, just in case the lower - // file is not really a file (in which case, we don't want to tell the upper layer anything, - // but error out sooner. - // - // Other than that, this is a simple loop that just copies over in chunks by a simple - // read-write loop. - let mut upper_fd = None; - let mut temp_buf = [0u8; 4096]; - loop { - match self.lower.read(&lower_fd, &mut temp_buf, None) { - Ok(size) => { - if upper_fd.is_none() { - // We are here the first time around, and did not error out, yay! We can - // actually open up the file. - // - // First, we make sure we've set up the ancestor directories. - match self.mkdir_migrating_ancestor_dirs(path) { - Ok(()) => {} - Err(e) => unimplemented!("{e} when setting up ancestor dirs"), - } - // Now we can actually open the file. - upper_fd = Some( - self.upper - .open( - path, - OFlags::CREAT | OFlags::WRONLY, - self.lower.fd_file_status(&lower_fd).unwrap().mode, - ) - .unwrap(), - ); - } - let upper_fd = upper_fd.as_ref().unwrap(); - if size > 0 && copy_data { - self.upper.write(upper_fd, &temp_buf[..size], None).expect( - "writing to upper layer must succeed, or layered file migration is in serious trouble", - ); - } else { - // EOF - break; - } - } - Err(e) => match e { - ReadError::NotAFile => { - // We can only have this happen the first time around - assert!(upper_fd.is_none()); - // In which case we quit early - return Err(MigrationError::NotAFile); - } - ReadError::ClosedFd | ReadError::NotForReading => unreachable!(), - ReadError::Io => return Err(MigrationError::Io), - }, - } - } - // After migrating the data, we also use these FDs to migrate the node-info over, so that - // any caller that tries to get the inode before/after the migration sees the same inode. - let found = self - .node_info_lookup - .read() - .get(&self.lower.fd_file_status(&lower_fd).unwrap().node_info) - .copied(); - if let Some(layered_id) = found { - let old = self.node_info_lookup.write().insert( - self.upper - .fd_file_status(upper_fd.as_ref().unwrap()) - .unwrap() - .node_info, - layered_id, - ); - assert!(old.is_none()); - } - // Now that we've migrated the data (and node-info) over, we can close out both of the file - // descriptors. - self.upper.close(&upper_fd.unwrap()).unwrap(); - self.lower.close(&lower_fd).unwrap(); - - // Now we need to migrate all the descriptor entries over. - // - // Perf: this does a full scan over all open descriptors: if a process has a HUGE number of - // open descriptors, this could be slow. - let RootDir { - entries: root_entries, - } = &mut *self.root.write(); - // First we figure out which entries need to be moved up. These entries are arc-cloned into - // a `Vec` so that we can release the lock the file descriptor table when setting things up - // within the upper layer. - let to_migrate: alloc::vec::Vec<(InternalFd, usize, OFlags, Entry)> = self - .litebox - .descriptor_table() - .iter::() - .filter_map(|(internal_fd, e)| { - if e.entry.path != path { - // Skip any that do not match the path - return None; - } - match &*e.entry.entry { - EntryX::Upper { fd: _ } => { - // Need to do nothing, jump to next - None - } - EntryX::Lower { fd: _ } => { - // We need to change this up to an upper-level entry. - Some(( - internal_fd, - e.entry.position.load(SeqCst), - e.entry.flags, - Arc::clone(&e.entry.entry), - )) - } - EntryX::Tombstone => unreachable!(), - } - }) - .collect(); - // Now we can actually perform the migration, since we've unlocked the lock on the - // file-descriptor table, which allows us to actually access things within the upper/lower - // levels without trouble. - for (internal_fd, position, flags, entry) in to_migrate { - // First, we set up the upper entry we'll be swapping/placing in. - let upper_fd = self.upper.open(path, flags, Mode::empty()).unwrap(); - if position > 0 { - self.upper - .seek( - &upper_fd, - isize::try_from(position).unwrap(), - SeekWhence::RelativeToBeginning, - ) - .unwrap(); - } - let upper_entry = Arc::new(EntryX::Upper { fd: upper_fd }); - // Then we check up on replacing entries - match Arc::strong_count(&entry) { - 0..=2 => { - // We are holding one, and also there must be an entry in `root` and the file - // descriptor table. - unreachable!() - } - 3 => { - // Perfect amount to trigger a `close` on the lower level, and remove - // the underlying root entry, since further syncing is no longer - // necessary. - let old_entry = self - .litebox - .descriptor_table() - .with_entry_mut_via_internal_fd::(internal_fd, |entry| { - core::mem::replace(&mut entry.entry.entry, upper_entry) - }) - .expect("nothing should have changed the existing entry"); - assert!(Arc::ptr_eq(&old_entry, &entry)); - drop(entry); - let root_entry = root_entries.remove(path).unwrap(); - assert!(Arc::ptr_eq(&old_entry, &root_entry)); - drop(root_entry); - let entry = Arc::into_inner(old_entry).unwrap(); - match entry { - EntryX::Upper { .. } | EntryX::Tombstone => unreachable!(), - EntryX::Lower { fd } => { - self.lower.close(&fd).unwrap(); - } - } - } - _ => { - // Other FDs are open with the same file too. We'll handle the open one - // here locally, and a future FD will take care of the relevant closing. - let old_entry = self - .litebox - .descriptor_table() - .with_entry_mut_via_internal_fd::(internal_fd, |entry| { - core::mem::replace(&mut entry.entry.entry, upper_entry) - }) - .expect("nothing should have changed the existing entry"); - assert!(Arc::ptr_eq(&old_entry, &entry)); - } - } - } - - Ok(()) - } - - // Gives the absolute path for `path`, resolving any `.` or `..`s, and making sure to account - // for any relative paths from current working directory. - // - // Note: does NOT account for symlinks. - fn absolute_path(&self, path: impl crate::path::Arg) -> Result { - assert!(self.current_working_dir.ends_with('/')); - let path = path.as_rust_str()?; - if path.starts_with('/') { - // Absolute path - Ok(path.normalized()?) - } else { - // Relative path - Ok((self.current_working_dir.clone() + path.as_rust_str()?).normalized()?) - } - } - - // Converts a `NodeInfo` from any of the layers into a layered `NodeInfo` - fn get_layered_nodeinfo(&self, node_info: NodeInfo) -> NodeInfo { - let mut node_info_lookup = self.node_info_lookup.write(); - let rdev = node_info.rdev; - // ino starts at 1 (zero represents deleted file) - let new_id = node_info_lookup.len() + 1; - let ino = *node_info_lookup.entry(node_info).or_insert(new_id); - NodeInfo { - dev: DEVICE_ID, - ino, - rdev, - } - } -} - -/// Possible errors when migrating a file up from lower to upper layer -#[derive(thiserror::Error, Debug)] -pub enum MigrationError { - #[error("does not point to a file")] - NotAFile, - #[error("no read access permissions")] - NoReadPerms, - #[error("I/O error")] - Io, - #[error(transparent)] - PathError(#[from] PathError), -} - -impl - super::private::Sealed for FileSystem -{ -} - -impl< - Platform: sync::RawSyncPrimitivesProvider, - Upper: super::FileSystem + 'static, - Lower: super::FileSystem + 'static, -> super::FileSystem for FileSystem -{ - fn open( - &self, - path: impl crate::path::Arg, - flags: OFlags, - mode: Mode, - ) -> Result, OpenError> { - let currently_supported_oflags: OFlags = OFlags::CREAT - | OFlags::RDONLY - | OFlags::WRONLY - | OFlags::RDWR - | OFlags::EXCL - | OFlags::TRUNC - | OFlags::NOCTTY - | OFlags::DIRECTORY - | OFlags::NONBLOCK - | OFlags::LARGEFILE - | OFlags::NOFOLLOW - | OFlags::APPEND; - if flags.intersects(currently_supported_oflags.complement()) { - unimplemented!("{flags:?}") - } - let path = self.absolute_path(path)?; - if flags.contains(OFlags::CREAT) { - if flags.contains(OFlags::EXCL) { - // O_EXCL with O_CREAT: fail if file already exists anywhere (upper or lower layer) - if self.file_status(path.as_str()).is_ok() { - return Err(OpenError::AlreadyExists); - } - } else { - // We must first attempt to open the file _without_ creating it, and only if that fails, - // do we fall-through and end up creating it (which will happen on the upper layer). - if let Ok(fd) = self.open(path.as_str(), flags - OFlags::CREAT, mode) { - return Ok(fd); - } - } - } - let mut tombstone_removal = false; - // If we already have an entry saying it is a tombstone, then we need to quit out early; - // otherwise, we'll check the levels. - if let Some(entry) = self.root.read().entries.get(&path) { - match entry.as_ref() { - EntryX::Tombstone => { - // The file has been cleared out; it used to exist on the lower level, but we - // explicitly have placed a tombstone in its place. - if flags.contains(OFlags::CREAT) { - // Fallthrough, since we will create it at the upper level now. We should - // remove the tombstone though. - tombstone_removal = true; - } else { - Err(PathError::NoSuchFileOrDirectory)?; - } - } - EntryX::Upper { .. } => unreachable!(), - EntryX::Lower { .. } => { - // As an optimization, since a lower-level file entry is always opened with the - // same flags, and since it indicates that there is no such file at the upper - // level, we can just return that directly (with the "real" flags being wrapped - // up in the layered descriptor). - return Ok(self.litebox.descriptor_table_mut().insert(Descriptor { - path, - flags, - entry: Arc::clone(entry), - position: 0.into(), - })); - } - } - } - if tombstone_removal { - if let Some(entry) = self.root.write().entries.remove(&path) { - let EntryX::Tombstone = *entry else { - unreachable!() - }; - } else { - // Another thread which also was attempting to create the same file (on top of a - // tombstoned file) won on the race to lock `self.root`, and thus it has already - // removed it for us. We don't need to remove it, and can proceed as normal. - } - } - // Otherwise, we first check the upper level, creating an entry if needed - match self.upper.open(&*path, flags, mode) { - Ok(fd) => { - let entry = Arc::new(EntryX::Upper { fd }); - return Ok(self.litebox.descriptor_table_mut().insert(Descriptor { - path, - flags, - entry, - position: 0.into(), - })); - } - Err(e) => match &e { - OpenError::AccessNotAllowed - | OpenError::Io - | OpenError::NoWritePerms - | OpenError::ReadOnlyFileSystem - | OpenError::AlreadyExists - | OpenError::TruncateError( - TruncateError::IsDirectory - | TruncateError::NotForWriting - | TruncateError::IsTerminalDevice - | TruncateError::ClosedFd - | TruncateError::Io, - ) - | OpenError::PathError( - PathError::ComponentNotADirectory - | PathError::InvalidPathname - | PathError::NoSearchPerms { .. }, - ) => { - // None of these can be handled by lower level, just quit out early - return Err(e); - } - OpenError::PathError(PathError::MissingComponent) - if flags.contains(OFlags::CREAT) => - { - // We must check if the lower layer contains all the directories; if it does, we - // can create the same directories and then re-trigger the open. - let dirname = path.rsplit_once('/').unwrap().0; - if let Ok(FileType::Directory) = self.ensure_lower_contains(dirname) { - // We must migrate the directories above, and then re-trigger the open - self.mkdir_migrating_ancestor_dirs(&path).unwrap(); - return self.open(path, flags, mode); - } - // Otherwise, handle-able by a lower level, fallthrough - } - OpenError::PathError( - PathError::NoSuchFileOrDirectory | PathError::MissingComponent, - ) => { - // Handle-able by a lower level, fallthrough - } - }, - } - // We must check the lower level, creating an entry if needed - let original_flags = flags; - let mut flags = flags; - // Prevent creation or truncation of files at lower level - flags.remove(OFlags::CREAT); - flags.remove(OFlags::TRUNC); - match self.layering_semantics { - LayeringSemantics::LowerLayerReadOnly => { - // Switch the lower level to read-only; the other calls will take care of - // copying into the upper level if/when necessary. - flags.remove(OFlags::RDWR); - flags.remove(OFlags::WRONLY); - flags.insert(OFlags::RDONLY); - } - LayeringSemantics::LowerLayerWritableFiles => { - // Do nothing more to the flags, because we might be writing things to lower level. - // We just make sure that there is no creation happening, that's all :) - assert!(!flags.contains(OFlags::CREAT)); - assert!(!flags.contains(OFlags::TRUNC)); - } - } - // Any errors from lower level now _must_ propagate up, so we can just invoke - // the lower level and set up the relevant descriptor upon success. - let entry = Arc::new(EntryX::Lower { - fd: self.lower.open(path.as_str(), flags, mode)?, - }); - let old = self - .root - .write() - .entries - .insert(path.clone(), Arc::clone(&entry)); - assert!(old.is_none()); - let fd = self.litebox.descriptor_table_mut().insert(Descriptor { - path, - flags: original_flags, - entry, - position: 0.into(), - }); - if original_flags.contains(OFlags::TRUNC) { - // The only scenario where we need to manually trigger truncation is when a file does - // not exist at the upper level but exists at the lower level; in that case, our - // `truncate` functionality (at the layered FS itself) should correctly migrate things - // over and handle them. - match self.truncate(&fd, 0, true) { - Ok(()) | Err(TruncateError::IsTerminalDevice) => { - // The terminal device is the one case we need to (due to Linux compatibility) - // explicitly ignore the truncation ability, and instead silently continue as if - // no error was thrown during truncation. - } - Err(e) => { - self.close(&fd).unwrap(); - return Err(e.into()); - } - } - } - Ok(fd) - } - - fn close(&self, fd: &FileFd) -> Result<(), CloseError> { - let Some(removed_entry) = self.litebox.descriptor_table_mut().remove(fd) else { - // Was duplicated, don't need to do anything. - return Ok(()); - }; - let Descriptor { - path, - entry, - flags: _, - position: _, - } = removed_entry.entry; - // We can first sanity check that we don't have a tombstone: none of the other operations - // should ever cause the entry _at_ an fd to become a tombstone, even if the entry at the - // path becomes a tombstone due to a file removal. - match entry.as_ref() { - EntryX::Upper { .. } | EntryX::Lower { .. } => {} - EntryX::Tombstone => unreachable!(), - } - // Crucially, we need to grab an exclusive lock to the root, so that the counts cannot - // change while we are reasoning about them. - let RootDir { - entries: root_entries, - } = &mut *self.root.write(); - // Our approach to this changes depending on whether this is an upper level FD or a - // lower FD. - match *entry { - EntryX::Tombstone => { - // A tombstone should never have even become an FD (if a file was opened, and then - // was subsequently deleted, then the FD itself would not yet be a tombstone, but - // would be pointing to the original value). - unreachable!() - } - EntryX::Upper { .. } => { - // Upper-level FDs do not have any entry in the root, nor do they share anything via - // `Arc`s. Thus, we can deal with them individually. - assert_eq!(Arc::strong_count(&entry), 1); - // Specifically, we can just immediately close them out, consuming the entry itself. - let EntryX::Upper { fd } = Arc::into_inner(entry).unwrap() else { - unreachable!() - }; - self.upper.close(&fd) - } - EntryX::Lower { .. } => { - // Lower level FDs almost always have a corresponding entry in the root. Thus, we - // might need to possibly clean things up from the root. - // - // First, we can attempt a fast-path clean-up by quickly check if there are other - // FDs referring to the same file - if Arc::strong_count(&entry) > 2 { - // There are _definitely_ other FDs pointing at this file, leave it alone - return Ok(()); - } - // Otherwise, either we have ourselves and the root pointing at it OR the root has - // been tombstoned out after the FDs have been opened at it. - match **root_entries.get(&path).unwrap() { - EntryX::Upper { .. } => unreachable!(), - EntryX::Lower { .. } => { - // We are going to have to deal with it at the entry too, fallthrough - } - EntryX::Tombstone => { - // A tombstone here means that the root doesn't contain the entry. There may - // possibly be other FDs opened for the same file before it was tombstoned - // out, so we'll close it out if we are the sole remaining holder; - // otherwise, it will be someone else's job to do so. - match Arc::into_inner(entry) { - Some(EntryX::Upper { .. } | EntryX::Tombstone) => unreachable!(), - Some(EntryX::Lower { fd }) => { - // We are the sole remaining holder of the FD. Let us clean things - // up at the lower level. - return self.lower.close(&fd); - } - None => { - // Someone else's job. We can quit successfully. - return Ok(()); - } - } - } - } - // Pull out the root entry, and perform a quick sanity check, and drop it out - // entirely, which should lead us to become the sole owner. - let root_entry = root_entries.remove(&path).unwrap(); - assert!(Arc::ptr_eq(&entry, &root_entry)); - assert!(matches!(*root_entry, EntryX::Lower { .. })); - drop(root_entry); - // We are now assured that we can close out the underlying file; we are the only - // holder of the entry, and thus can change it from an Arc to the underlying value - // itself, and then close it out. - let EntryX::Lower { fd, .. } = Arc::into_inner(entry).unwrap() else { - unreachable!() - }; - self.lower.close(&fd) - } - } - } - - fn read( - &self, - fd: &FileFd, - buf: &mut [u8], - offset: Option, - ) -> Result { - // Since a write to a lower-level file upgrades the underlying entry out completely to an - // upper-level file, we don't actually need to worry about a desync; a write to lower-level - // file will successfully be seen as just being an upper level file. Thus, it is sufficient - // just to delegate this operation based whether the entry points to upper or lower layers. - let entry = self - .litebox - .descriptor_table() - .with_entry(fd, |descriptor| { - let access_mode = descriptor.entry.flags & (OFlags::WRONLY | OFlags::RDWR); - if access_mode == OFlags::WRONLY { - Err(ReadError::NotForReading) - } else { - Ok(Arc::clone(&descriptor.entry.entry)) - } - }) - .ok_or(ReadError::ClosedFd) - .flatten()?; - // Perform the actual operation - let num_bytes = match entry.as_ref() { - EntryX::Upper { fd } => self.upper.read(fd, buf, offset)?, - EntryX::Lower { fd } => self.lower.read(fd, buf, offset)?, - EntryX::Tombstone => unreachable!(), - }; - self.litebox - .descriptor_table() - .get_entry(fd) - .ok_or(ReadError::ClosedFd)? - .entry - .position - .fetch_add(num_bytes, SeqCst); - Ok(num_bytes) - } - - fn write( - &self, - fd: &FileFd, - buf: &[u8], - offset: Option, - ) -> Result { - // Writing needs to be careful of how it is performing the write. Any upper-level file can - // instantly be written to; but a lower-level file must become a upper-level file, before - // actually being written to. - let (entry, path) = self - .litebox - .descriptor_table() - .with_entry(fd, |descriptor| { - if !descriptor.entry.flags.contains(OFlags::WRONLY) - && !descriptor.entry.flags.contains(OFlags::RDWR) - { - Err(WriteError::NotForWriting) - } else { - Ok(( - Arc::clone(&descriptor.entry.entry), - descriptor.entry.path.clone(), - )) - } - }) - .ok_or(WriteError::ClosedFd) - .flatten()?; - match entry.as_ref() { - EntryX::Upper { fd: upper_fd } => { - let num_bytes = self.upper.write(upper_fd, buf, offset)?; - self.litebox - .descriptor_table() - .get_entry(fd) - .unwrap() - .entry - .position - .fetch_add(num_bytes, SeqCst); - return Ok(num_bytes); - } - EntryX::Lower { fd: lower_fd } => { - match self.layering_semantics { - LayeringSemantics::LowerLayerReadOnly => { - // fallthrough - } - LayeringSemantics::LowerLayerWritableFiles => { - // Allow direct write to lower layer - let num_bytes = self.lower.write(lower_fd, buf, offset)?; - if let Some(e) = self.litebox.descriptor_table().get_entry(fd) { - e.entry.position.fetch_add(num_bytes, SeqCst); - } - return Ok(num_bytes); - } - } - } - EntryX::Tombstone => unreachable!(), - } - // Change it to an upper-level file, also altering the file descriptor. - drop(entry); - match self.migrate_file_up(&path, true) { - Ok(()) => {} - Err(MigrationError::NoReadPerms) => unimplemented!(), - Err(MigrationError::NotAFile) => return Err(WriteError::NotAFile), - Err(MigrationError::Io) => return Err(WriteError::Io), - Err(MigrationError::PathError(_e)) => unreachable!(), - } - // As a sanity check, in debug mode, confirm that it is now an upper file - debug_assert!(matches!( - *self - .litebox - .descriptor_table() - .get_entry(fd) - .unwrap() - .entry - .entry, - EntryX::Upper { .. } - )); - // Since it has been migrated, we can just re-trigger, causing it to apply to the - // upper layer - self.write(fd, buf, offset) - } - - fn seek( - &self, - fd: &FileFd, - offset: isize, - whence: SeekWhence, - ) -> Result { - let entry = self - .litebox - .descriptor_table() - .with_entry(fd, |descriptor| Arc::clone(&descriptor.entry.entry)) - .ok_or(SeekError::ClosedFd)?; - // Perform the seek, and update the position info - let position = match entry.as_ref() { - EntryX::Upper { fd } => self.upper.seek(fd, offset, whence)?, - EntryX::Lower { fd } => self.lower.seek(fd, offset, whence)?, - EntryX::Tombstone => unreachable!(), - }; - if let Some(e) = self.litebox.descriptor_table().get_entry(fd) { - e.entry.position.store(position, SeqCst); - } - Ok(position) - } - - fn truncate( - &self, - fd: &FileFd, - length: usize, - reset_offset: bool, - ) -> Result<(), TruncateError> { - let (flags, entry) = self - .litebox - .descriptor_table() - .with_entry(fd, |descriptor| { - (descriptor.entry.flags, Arc::clone(&descriptor.entry.entry)) - }) - .ok_or(TruncateError::ClosedFd)?; - let layered_fd = fd; - match entry.as_ref() { - EntryX::Upper { fd } => self.upper.truncate(fd, length, reset_offset), - EntryX::Lower { fd } => { - match self.layering_semantics { - LayeringSemantics::LowerLayerWritableFiles => { - self.lower.truncate(fd, length, reset_offset) - } - LayeringSemantics::LowerLayerReadOnly => { - if flags.contains(OFlags::WRONLY) || flags.contains(OFlags::RDWR) { - // We might need to migrate the file up - match self.lower.truncate(fd, length, reset_offset) { - Ok(()) | Err(TruncateError::ClosedFd) => unreachable!(), - Err(TruncateError::IsDirectory) => Err(TruncateError::IsDirectory), - Err(TruncateError::IsTerminalDevice) => { - Err(TruncateError::IsTerminalDevice) - } - Err(TruncateError::NotForWriting) => { - // We must actually migrate this file up, and keep it truncated. - // - // We must first drop the cloned entry to make sure that the ref - // counting works out correctly during migration. - drop(entry); - let path = self - .litebox - .descriptor_table() - .with_entry(layered_fd, |descriptor| { - descriptor.entry.path.clone() - }) - .ok_or(TruncateError::ClosedFd)?; - self.migrate_file_up(&path, false) - .expect("this migration should always succeed"); - - Ok(()) - } - Err(TruncateError::Io) => Err(TruncateError::Io), - } - } else { - // The lower level truncate will correctly identify dir/file and handle - // the difference in erroring. - self.lower.truncate(fd, length, reset_offset) - } - } - } - } - EntryX::Tombstone => unreachable!(), - } - } - - fn chmod(&self, path: impl crate::path::Arg, mode: Mode) -> Result<(), ChmodError> { - let path = self.absolute_path(path)?; - match self.upper.chmod(path.as_str(), mode) { - Ok(()) => return Ok(()), - Err(e) => match e { - ChmodError::NotTheOwner - | ChmodError::Io - | ChmodError::ReadOnlyFileSystem - | ChmodError::PathError( - PathError::ComponentNotADirectory - | PathError::InvalidPathname - | PathError::NoSearchPerms { .. }, - ) => { - return Err(e); - } - ChmodError::PathError( - PathError::NoSuchFileOrDirectory | PathError::MissingComponent, - ) => { - // fallthrough - } - }, - } - match self.ensure_lower_contains(&path) { - Ok(_) => {} - Err(FileStatusError::Io) => return Err(ChmodError::Io), - Err(FileStatusError::PathError(e)) => return Err(ChmodError::PathError(e)), - Err(FileStatusError::ClosedFd) => unreachable!(), - } - match self.migrate_file_up(&path, true) { - Ok(()) => {} - Err(MigrationError::NoReadPerms) => unimplemented!(), - Err(MigrationError::NotAFile) => unimplemented!(), - Err(MigrationError::Io) => return Err(ChmodError::Io), - Err(MigrationError::PathError(_e)) => unreachable!(), - } - // Since it has been migrated, we can just re-trigger, causing it to apply to the - // upper layer - self.chmod(path, mode) - } - - fn chown( - &self, - path: impl crate::path::Arg, - user: Option, - group: Option, - ) -> Result<(), ChownError> { - let path = self.absolute_path(path)?; - match self.upper.chown(path.as_str(), user, group) { - Ok(()) => return Ok(()), - Err(e) => match e { - ChownError::NotTheOwner - | ChownError::Io - | ChownError::ReadOnlyFileSystem - | ChownError::PathError( - PathError::ComponentNotADirectory - | PathError::InvalidPathname - | PathError::NoSearchPerms { .. }, - ) => { - return Err(e); - } - ChownError::PathError( - PathError::NoSuchFileOrDirectory | PathError::MissingComponent, - ) => { - // fallthrough - } - }, - } - match self.ensure_lower_contains(&path) { - Ok(_) => {} - Err(FileStatusError::Io) => return Err(ChownError::Io), - Err(FileStatusError::PathError(e)) => return Err(ChownError::PathError(e)), - Err(FileStatusError::ClosedFd) => unreachable!(), - } - match self.migrate_file_up(&path, true) { - Ok(()) => {} - Err(MigrationError::NoReadPerms) => unimplemented!(), - Err(MigrationError::NotAFile) => unimplemented!(), - Err(MigrationError::Io) => return Err(ChownError::Io), - Err(MigrationError::PathError(_e)) => unreachable!(), - } - // Since it has been migrated, we can just re-trigger, causing it to apply to the - // upper layer - self.chown(path, user, group) - } - - fn unlink(&self, path: impl crate::path::Arg) -> Result<(), UnlinkError> { - let path = self.absolute_path(path)?; - match self.upper.unlink(path.as_str()) { - Ok(()) => { - // If the lower level contains the file, then we need to place a tombstone in its - // path, to prevent the lower level from showing up above. - if self.ensure_lower_contains(&path).is_ok() { - // fallthrough to place the tombstone - } else { - // Lower level doesn't contain it, we are done (with success, since we actually - // removed the file). - return Ok(()); - } - } - Err(e) => match e { - UnlinkError::NoWritePerms - | UnlinkError::Io - | UnlinkError::IsADirectory - | UnlinkError::ReadOnlyFileSystem - | UnlinkError::PathError( - PathError::ComponentNotADirectory - | PathError::InvalidPathname - | PathError::NoSearchPerms { .. }, - ) => { - return Err(e); - } - UnlinkError::PathError( - PathError::NoSuchFileOrDirectory | PathError::MissingComponent, - ) => { - // We must now check if the lower level contains the file; if it does not, we - // must exit with failure. Otherwise, we fallthrough to place the tombstone. - match self.ensure_lower_contains(&path).map_err(|e| match e { - FileStatusError::Io => UnlinkError::Io, - FileStatusError::PathError(p) => UnlinkError::PathError(p), - FileStatusError::ClosedFd => unreachable!(), - })? { - FileType::RegularFile => { - // fallthrough - } - FileType::Directory => { - return Err(UnlinkError::IsADirectory); - } - FileType::CharacterDevice => unimplemented!(), - } - } - }, - } - // We can now place a tombstone over the lower level file, marking it as deleted, without - // actually changing the lower level. - self.root - .write() - .entries - .insert(path, Arc::new(EntryX::Tombstone)); - Ok(()) - } - - fn mkdir(&self, path: impl crate::path::Arg, mode: Mode) -> Result<(), MkdirError> { - let path = self.absolute_path(path)?; - match self.upper.mkdir(path.as_str(), mode) { - Ok(()) => { - // If we could successfully make the directory, we know that things are "sane" at - // the upper level, but we must also check the lower level to make sure that this - // directory didn't already exist. - if self.ensure_lower_contains(&path).is_ok() { - return Err(MkdirError::AlreadyExists); - } - return Ok(()); - } - Err(e) => match e { - MkdirError::NoWritePerms - | MkdirError::Io - | MkdirError::AlreadyExists - | MkdirError::ReadOnlyFileSystem - | MkdirError::PathError( - PathError::ComponentNotADirectory - | PathError::InvalidPathname - | PathError::NoSearchPerms { .. }, - ) => { - return Err(e); - } - MkdirError::PathError(PathError::NoSuchFileOrDirectory) => { - unreachable!() - } - MkdirError::PathError(PathError::MissingComponent) => { - // fallthrough - } - }, - } - // We know that at least one of the components is missing. We should check each of the - // components individually, making directories for any components that already exist at the - // lower layer, and erroring out if no lower layer component exists of that form. - self.mkdir_migrating_ancestor_dirs(&path)?; - // And then now we can make the upper directory. - self.upper.mkdir(path, mode) - } - - fn rmdir(&self, path: impl crate::path::Arg) -> Result<(), RmdirError> { - let path = self.absolute_path(path)?; - - // Prevent removing root explicitly (even if upper is empty). - if path == "/" { - return Err(RmdirError::Busy); - } - - let dir_fd = match self.open( - path.as_str(), - OFlags::RDONLY | OFlags::DIRECTORY, - Mode::empty(), - ) { - Ok(fd) => fd, - Err(e) => match e { - OpenError::PathError(PathError::ComponentNotADirectory) => { - return Err(RmdirError::NotADirectory); - } - OpenError::PathError(pe) => return Err(pe.into()), - OpenError::AccessNotAllowed => todo!(), - OpenError::Io => return Err(RmdirError::Io), - OpenError::ReadOnlyFileSystem => { - return Err(RmdirError::ReadOnlyFileSystem); - } - OpenError::NoWritePerms - | OpenError::AlreadyExists - | OpenError::TruncateError(_) => { - unreachable!() - } - }, - }; - let entries = match self.read_dir(&dir_fd) { - Ok(entries) => entries, - Err(ReadDirError::ClosedFd | ReadDirError::NotADirectory) => unreachable!(), - Err(ReadDirError::Io) => return Err(RmdirError::Io), - }; - self.close(&dir_fd).expect("close dir fd failed"); - // "." and ".." are always present; anything more => not empty. - if entries.len() > 2 { - return Err(RmdirError::NotEmpty); - } - - // blindly rmdir at upper layer, suppressing non-existence errors. - if let Err(e) = self.upper.rmdir(path.as_str()) { - match e { - RmdirError::PathError( - PathError::NoSuchFileOrDirectory | PathError::MissingComponent, - ) => { - // fallthrough - } - RmdirError::NotEmpty - | RmdirError::NotADirectory - | RmdirError::ReadOnlyFileSystem - | RmdirError::PathError( - PathError::ComponentNotADirectory | PathError::InvalidPathname, - ) => unreachable!(), - RmdirError::Busy - | RmdirError::NoWritePerms - | RmdirError::Io - | RmdirError::PathError(PathError::NoSearchPerms { .. }) => return Err(e), - } - } - - if let LayeringSemantics::LowerLayerReadOnly = self.layering_semantics { - self.root - .write() - .entries - .insert(path, Arc::new(EntryX::Tombstone)); - } else { - // If lower layer is writable, we can just rmdir there too, suppressing non-existence errors. - if let Err(e) = self.lower.rmdir(path.as_str()) { - match e { - RmdirError::PathError( - PathError::NoSuchFileOrDirectory | PathError::MissingComponent, - ) => { - // fallthrough - } - RmdirError::NotEmpty - | RmdirError::NotADirectory - | RmdirError::ReadOnlyFileSystem - | RmdirError::PathError( - PathError::ComponentNotADirectory | PathError::InvalidPathname, - ) => unreachable!(), - RmdirError::Busy - | RmdirError::NoWritePerms - | RmdirError::Io - | RmdirError::PathError(PathError::NoSearchPerms { .. }) => return Err(e), - } - } - } - Ok(()) - } - - fn read_dir(&self, fd: &FileFd) -> Result, ReadDirError> { - let (entry, path) = self - .litebox - .descriptor_table() - .with_entry(fd, |descriptor| { - ( - Arc::clone(&descriptor.entry.entry), - descriptor.entry.path.clone(), - ) - }) - .ok_or(ReadDirError::ClosedFd)?; - - let mut entries = match entry.as_ref() { - EntryX::Upper { fd } => { - // Get entries from upper layer - let mut upper_entries = self.upper.read_dir(fd)?; - - // Try to get entries from lower layer for the same path - if let Ok(lower_fd) = self - .lower - .open(path.as_str(), OFlags::RDONLY, Mode::empty()) - { - if let Ok(lower_entries) = self.lower.read_dir(&lower_fd) { - // Merge entries, avoiding duplicates (upper layer takes precedence) - let upper_names: HashSet = - upper_entries.iter().map(|e| e.name.clone()).collect(); - - for lower_entry in lower_entries { - if !upper_names.contains(&lower_entry.name) { - upper_entries.push(lower_entry); - } - } - } - let _ = self.lower.close(&lower_fd); - } - - upper_entries - } - EntryX::Lower { fd } => { - // This is the easy case, nothing to deal with upper entries. - self.lower.read_dir(fd)? - } - EntryX::Tombstone => unreachable!(), - }; - - for e in &mut entries { - if let Some(ni) = e.ino_info.take() { - e.ino_info = Some(self.get_layered_nodeinfo(ni)); - } - } - Ok(entries) - } - - fn file_status(&self, path: impl crate::path::Arg) -> Result { - // Note: we grab the info from the relevant level and then immediately spit back the same, - // essentially to ask the compiler to remind us we need to update this when we support - // inodes and such. - let path = self.absolute_path(path)?; - if let Some(entry) = self.root.read().entries.get(&path) { - let FileStatus { - file_type, - mode, - size, - owner, - node_info, - blksize, - } = match entry.as_ref() { - EntryX::Upper { fd } => self.upper.fd_file_status(fd)?, - EntryX::Lower { fd } => self.lower.fd_file_status(fd)?, - EntryX::Tombstone => { - return Err(PathError::NoSuchFileOrDirectory)?; - } - }; - return Ok(FileStatus { - file_type, - mode, - size, - owner, - node_info: self.get_layered_nodeinfo(node_info), - blksize, - }); - } - // The file is not open, we must look at the levels themselves. - match self.upper.file_status(&*path) { - Ok(FileStatus { - file_type, - mode, - size, - owner, - node_info, - blksize, - }) => { - return Ok(FileStatus { - file_type, - mode, - size, - owner, - node_info: self.get_layered_nodeinfo(node_info), - blksize, - }); - } - Err(e) => match e { - FileStatusError::PathError( - PathError::ComponentNotADirectory - | PathError::InvalidPathname - | PathError::NoSearchPerms { .. }, - ) => { - // None of these can be handled by lower level, just quit out early - return Err(e); - } - FileStatusError::Io => return Err(e), - FileStatusError::PathError( - PathError::NoSuchFileOrDirectory | PathError::MissingComponent, - ) => { - // Handle-able by a lower level, fallthrough - } - FileStatusError::ClosedFd => unreachable!(), - }, - } - let FileStatus { - file_type, - mode, - size, - owner, - node_info, - blksize, - } = self.lower.file_status(path)?; - Ok(FileStatus { - file_type, - mode, - size, - owner, - node_info: self.get_layered_nodeinfo(node_info), - blksize, - }) - } - - fn fd_file_status( - &self, - fd: &FileFd, - ) -> Result { - let entry = self - .litebox - .descriptor_table() - .with_entry(fd, |descriptor| Arc::clone(&descriptor.entry.entry)) - .ok_or(FileStatusError::ClosedFd)?; - let FileStatus { - file_type, - mode, - size, - owner, - node_info, - blksize, - } = match entry.as_ref() { - EntryX::Upper { fd } => self.upper.fd_file_status(fd)?, - EntryX::Lower { fd } => self.lower.fd_file_status(fd)?, - EntryX::Tombstone => unreachable!(), - }; - // Note: we grab the info and then immediately spit back the same, essentially to ask the - // compiler to remind us we need to update this when we support inodes and such. - Ok(FileStatus { - file_type, - mode, - size, - owner, - node_info: self.get_layered_nodeinfo(node_info), - blksize, - }) - } - - fn get_static_backing_data( - &self, - fd: &FileFd, - ) -> Option<&'static [u8]> { - let entry = self - .litebox - .descriptor_table() - .with_entry(fd, |descriptor| Arc::clone(&descriptor.entry.entry))?; - match entry.as_ref() { - EntryX::Upper { fd } => self.upper.get_static_backing_data(fd), - EntryX::Lower { fd } => self.lower.get_static_backing_data(fd), - EntryX::Tombstone => unreachable!(), - } - } -} - -struct Descriptor { - path: String, - flags: OFlags, - entry: Entry, - position: AtomicUsize, -} - -struct RootDir { - // keys are normalized paths; directories do not have the final `/` (thus the root would be at - // the empty-string key "") - // - // Invariant: this only stores lower+tombstone entries, no upper entries will show up here. - entries: HashMap>, -} - -impl RootDir { - fn new() -> Self { - Self { - entries: HashMap::new(), - } - } -} - -type Entry = Arc>; - -enum EntryX { - // This file should be considered a purely upper-level file, independent of whether lower level file exists or not. - Upper { fd: TypedFd }, - // This file is a lower-level file and does NOT exist in the upper level file. - Lower { fd: TypedFd }, - // This file exists in the lower level, but as far as the layered architecture is concerned, - // this is marked as deleted. RIP (x_x) - Tombstone, -} - -impl core::fmt::Debug - for EntryX -{ - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Self::Upper { fd: _ } => f.debug_struct("Upper").finish_non_exhaustive(), - Self::Lower { fd: _ } => f.debug_struct("Lower").finish_non_exhaustive(), - Self::Tombstone => write!(f, "Tombstone"), - } - } -} - -crate::fd::enable_fds_for_subsystem! { - @Platform: { sync::RawSyncPrimitivesProvider }, Upper: { super::FileSystem + 'static }, Lower: { super::FileSystem + 'static }; - FileSystem; - @Upper: { super::FileSystem + 'static }, Lower: { super::FileSystem + 'static }; - Descriptor; - -> FileFd; -} diff --git a/litebox/src/fs/mod.rs b/litebox/src/fs/mod.rs index 191936a21..fdeabced2 100644 --- a/litebox/src/fs/mod.rs +++ b/litebox/src/fs/mod.rs @@ -18,7 +18,6 @@ pub mod devices; pub mod errors; pub mod in_mem; pub(crate) mod inode_allocator; -pub mod layered; pub mod nine_p; pub mod overlay; pub mod resolver;