oci-image 0.3.1

OCI container images on disk: image references, sha256 digests, one-pass layer unpack (whiteouts and opaque directories), gzip (all cores via lgz one-pass streaming decode; small layers on one core) and zstd layers, a content-addressed store of blobs and per-layer directories in overlayfs format (each layer unpacked once, shared between images, refcounted, collected), and overlay snapshot prep.
Documentation
//! Paths resolved beneath a directory held open: the layer writer's only way
//! to reach anything in a rootfs.
//!
//! The rootfs is opened once (`O_PATH`). Every parent directory is reached
//! from it with `openat2(RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS)` where the
//! kernel has it (5.6+), else by a walk of `O_NOFOLLOW` opens, one component
//! at a time, that follows a symlink only while it stays beneath the root.
//! A `..` above the root or an absolute symlink is `EXDEV`. Every write is
//! then an `*at` call on the parent's descriptor that does not follow the
//! last component, so a directory swapped for a symlink by another process
//! after it was checked is never written through: the next entry resolves
//! its parent again from the root.

use std::cell::Cell;
use std::ffi::{CStr, CString, OsStr, OsString};
use std::io;
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
use std::os::unix::ffi::{OsStrExt, OsStringExt};
use std::path::{Component, Path, PathBuf};

const RESOLVE_NO_MAGICLINKS: u64 = 0x02;
const RESOLVE_BENEATH: u64 = 0x08;

#[repr(C)]
struct OpenHow {
    flags: u64,
    mode: u64,
    resolve: u64,
}

pub(crate) fn cstr(s: &OsStr) -> io::Result<CString> {
    CString::new(s.as_bytes())
        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "a NUL in a name"))
}

pub(crate) fn cvt(r: libc::c_int) -> io::Result<libc::c_int> {
    if r < 0 {
        Err(io::Error::last_os_error())
    } else {
        Ok(r)
    }
}

/// `EXDEV`: the path leaves the root.
pub(crate) fn escape() -> io::Error {
    io::Error::from_raw_os_error(libc::EXDEV)
}

pub(crate) fn is_escape(e: &io::Error) -> bool {
    e.raw_os_error() == Some(libc::EXDEV)
}

pub(crate) fn openat(
    dir: RawFd,
    name: &CStr,
    flags: libc::c_int,
    mode: libc::mode_t,
) -> io::Result<OwnedFd> {
    let fd = cvt(unsafe {
        libc::openat(
            dir,
            name.as_ptr(),
            flags | libc::O_CLOEXEC,
            mode as libc::c_uint,
        )
    })?;
    Ok(unsafe { OwnedFd::from_raw_fd(fd) })
}

fn openat2(dir: RawFd, path: &CStr, flags: libc::c_int, resolve: u64) -> io::Result<OwnedFd> {
    let how = OpenHow {
        flags: (flags | libc::O_CLOEXEC) as u64,
        mode: 0,
        resolve,
    };
    let r = unsafe {
        libc::syscall(
            libc::SYS_openat2,
            dir,
            path.as_ptr(),
            &how as *const OpenHow,
            std::mem::size_of::<OpenHow>(),
        )
    };
    if r < 0 {
        return Err(io::Error::last_os_error());
    }
    Ok(unsafe { OwnedFd::from_raw_fd(r as RawFd) })
}

/// `lstat` of `name` in `dir`; `None` if there is nothing.
pub(crate) fn fstatat(dir: RawFd, name: &CStr) -> io::Result<Option<libc::stat>> {
    let mut st: libc::stat = unsafe { std::mem::zeroed() };
    match cvt(unsafe { libc::fstatat(dir, name.as_ptr(), &mut st, libc::AT_SYMLINK_NOFOLLOW) }) {
        Ok(_) => Ok(Some(st)),
        Err(e) if e.raw_os_error() == Some(libc::ENOENT) => Ok(None),
        Err(e) => Err(e),
    }
}

pub(crate) fn fstat(fd: RawFd) -> io::Result<libc::stat> {
    let mut st: libc::stat = unsafe { std::mem::zeroed() };
    cvt(unsafe { libc::fstat(fd, &mut st) })?;
    Ok(st)
}

pub(crate) fn is(st: &libc::stat, kind: libc::mode_t) -> bool {
    st.st_mode & libc::S_IFMT == kind
}

fn readlinkat(dir: RawFd, name: &CStr) -> io::Result<PathBuf> {
    let mut buf = vec![0u8; libc::PATH_MAX as usize];
    let n = unsafe { libc::readlinkat(dir, name.as_ptr(), buf.as_mut_ptr().cast(), buf.len()) };
    if n < 0 {
        return Err(io::Error::last_os_error());
    }
    buf.truncate(n as usize);
    Ok(PathBuf::from(OsString::from_vec(buf)))
}

/// The names in the directory `dir` (not `.` or `..`).
pub(crate) fn list(dir: RawFd) -> io::Result<Vec<CString>> {
    // fdopendir needs a readable descriptor of its own.
    let d = openat(dir, c".", libc::O_RDONLY | libc::O_DIRECTORY, 0)?;
    let raw = d.as_raw_fd();
    let dp = unsafe { libc::fdopendir(raw) };
    if dp.is_null() {
        return Err(io::Error::last_os_error());
    }
    // The DIR owns the descriptor now.
    std::mem::forget(d);
    let mut out = Vec::new();
    loop {
        let ent = unsafe { libc::readdir(dp) };
        if ent.is_null() {
            break;
        }
        let name = unsafe { CStr::from_ptr((*ent).d_name.as_ptr()) };
        if name.to_bytes() != b"." && name.to_bytes() != b".." {
            out.push(name.to_owned());
        }
    }
    unsafe { libc::closedir(dp) };
    Ok(out)
}

/// Remove `name` in `dir`: a directory with everything below it, never
/// following a symlink.
pub(crate) fn remove_at(dir: RawFd, name: &CStr) -> io::Result<()> {
    let Some(st) = fstatat(dir, name)? else {
        return Ok(());
    };
    if is(&st, libc::S_IFDIR) {
        let d = openat(
            dir,
            name,
            libc::O_RDONLY | libc::O_DIRECTORY | libc::O_NOFOLLOW,
            0,
        )?;
        for child in list(d.as_raw_fd())? {
            remove_at(d.as_raw_fd(), &child)?;
        }
        cvt(unsafe { libc::unlinkat(dir, name.as_ptr(), libc::AT_REMOVEDIR) })?;
    } else {
        cvt(unsafe { libc::unlinkat(dir, name.as_ptr(), 0) })?;
    }
    Ok(())
}

/// A root directory, held open; everything below it is reached from it.
pub(crate) struct Root {
    fd: OwnedFd,
    openat2: Cell<bool>,
}

impl Root {
    pub(crate) fn open(path: &Path) -> io::Result<Root> {
        let c = cstr(path.as_os_str())?;
        let fd = openat(libc::AT_FDCWD, &c, libc::O_PATH | libc::O_DIRECTORY, 0)?;
        Ok(Root {
            fd,
            openat2: Cell::new(true),
        })
    }

    /// Without `openat2`: the component walk only (tests of the fallback).
    #[cfg(test)]
    fn without_openat2(self) -> Self {
        self.openat2.set(false);
        self
    }

    fn dup(&self) -> io::Result<OwnedFd> {
        self.fd.try_clone()
    }

    /// The directory `rel` (clean, relative) beneath the root, as an `O_PATH`
    /// descriptor. With `create`, missing directories are made (0755).
    pub(crate) fn dir(&self, rel: &Path, create: bool) -> io::Result<OwnedFd> {
        if rel.as_os_str().is_empty() {
            return self.dup();
        }
        if self.openat2.get() {
            let c = cstr(rel.as_os_str())?;
            match openat2(
                self.fd.as_raw_fd(),
                &c,
                libc::O_PATH | libc::O_DIRECTORY,
                RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS,
            ) {
                Ok(fd) => return Ok(fd),
                Err(e)
                    if matches!(
                        e.raw_os_error(),
                        Some(libc::ENOSYS | libc::EPERM | libc::E2BIG)
                    ) =>
                {
                    self.openat2.set(false)
                }
                Err(e) if e.raw_os_error() == Some(libc::ENOENT) && create => {}
                Err(e) => return Err(e),
            }
        }
        self.walk(rel, create)
    }

    /// One `O_NOFOLLOW` open per component from the root; a relative symlink
    /// is followed by putting its target's components in front.
    fn walk(&self, rel: &Path, create: bool) -> io::Result<OwnedFd> {
        let mut stack: Vec<OwnedFd> = Vec::new();
        let mut todo: Vec<OsString> = Vec::new();
        push_components(&mut todo, rel)?;
        let mut hops = 0;
        while let Some(c) = todo.pop() {
            if c == ".." {
                if stack.pop().is_none() {
                    return Err(escape());
                }
                continue;
            }
            let cur = stack.last().map_or(self.fd.as_raw_fd(), |f| f.as_raw_fd());
            let name = cstr(&c)?;
            match openat(cur, &name, libc::O_PATH | libc::O_NOFOLLOW, 0) {
                Ok(fd) => {
                    let st = fstat(fd.as_raw_fd())?;
                    if is(&st, libc::S_IFDIR) {
                        stack.push(fd);
                    } else if is(&st, libc::S_IFLNK) {
                        hops += 1;
                        if hops > 40 {
                            return Err(io::Error::from_raw_os_error(libc::ELOOP));
                        }
                        let target = readlinkat(cur, &name)?;
                        if target.is_absolute() {
                            return Err(escape());
                        }
                        push_components(&mut todo, &target)?;
                    } else {
                        return Err(io::Error::from_raw_os_error(libc::ENOTDIR));
                    }
                }
                Err(e) if e.raw_os_error() == Some(libc::ENOENT) && create => {
                    match cvt(unsafe { libc::mkdirat(cur, name.as_ptr(), 0o755) }) {
                        Ok(_) => {}
                        Err(e) if e.raw_os_error() == Some(libc::EEXIST) => {}
                        Err(e) => return Err(e),
                    }
                    let fd = openat(
                        cur,
                        &name,
                        libc::O_PATH | libc::O_NOFOLLOW | libc::O_DIRECTORY,
                        0,
                    )?;
                    stack.push(fd);
                }
                Err(e) => return Err(e),
            }
        }
        match stack.pop() {
            Some(f) => Ok(f),
            None => self.dup(),
        }
    }
}

/// `p`'s components pushed so that the first is popped first; `..` kept,
/// `.` and `/` dropped.
fn push_components(todo: &mut Vec<OsString>, p: &Path) -> io::Result<()> {
    let mut v: Vec<OsString> = Vec::new();
    for c in p.components() {
        match c {
            Component::Normal(n) => v.push(n.to_os_string()),
            Component::ParentDir => v.push(OsString::from("..")),
            Component::CurDir | Component::RootDir => {}
            Component::Prefix(_) => return Err(escape()),
        }
    }
    todo.extend(v.into_iter().rev());
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn the_component_walk_refuses_what_leaves_the_root_like_openat2_does() {
        let base = tempfile::tempdir().unwrap();
        let root = base.path().join("r");
        std::fs::create_dir_all(root.join("usr/lib")).unwrap();
        std::os::unix::fs::symlink("usr/lib", root.join("lib")).unwrap();
        std::os::unix::fs::symlink("/usr/lib", root.join("abs")).unwrap();
        std::os::unix::fs::symlink("../..", root.join("up")).unwrap();
        for fast in [true, false] {
            let r = Root::open(&root).unwrap();
            let r = if fast { r } else { r.without_openat2() };
            assert!(r.dir(Path::new("lib"), false).is_ok(), "fast={fast}");
            assert!(r.dir(Path::new("usr/../lib"), false).is_ok(), "fast={fast}");
            for bad in ["abs", "up", "..", "usr/../.."] {
                let e = r.dir(Path::new(bad), true).unwrap_err();
                assert!(is_escape(&e), "{bad} fast={fast}: {e}");
            }
            r.dir(Path::new("lib/new/deeper"), true).unwrap();
            assert!(root.join("usr/lib/new/deeper").is_dir());
            std::fs::remove_dir_all(root.join("usr/lib/new")).unwrap();
        }
        assert!(!base.path().join("usr").exists());
    }
}