a3s-box-runtime 3.2.3

MicroVM runtime engine — VM lifecycle, OCI images, attestation, networking
Documentation
use std::collections::BTreeMap;
use std::io::Read;
use std::path::Path;

use a3s_box_core::error::{BoxError, Result};
use mkext4::{Meta, SpecialKind};
use tar::Archive;

use super::tree::{normalize_archive_path, EntryMetadata, GuestPath, LogicalRootfs};

const DEFAULT_MAX_LAYER_BYTES: u64 = 16 * 1024 * 1024 * 1024;
const MAX_LAYER_ENTRIES: usize = 1_000_000;
const PAX_XATTR_PREFIX: &[u8] = b"SCHILY.xattr.";

pub(super) fn apply_layer(
    layer_path: &Path,
    digest: &str,
    compressed_size: u64,
    rootfs: &mut LogicalRootfs,
) -> Result<()> {
    rootfs.begin_layer()?;
    let decoder = crate::oci::layer_reader::open_verified(
        layer_path,
        digest,
        compressed_size,
        rootfs.spool.path(),
    )?;
    let limit = crate::oci::limited_reader::cap_from_env(
        "A3S_BOX_MAX_LAYER_BYTES",
        DEFAULT_MAX_LAYER_BYTES,
    );
    let decoder = crate::oci::limited_reader::LimitedReader::new(decoder, limit);
    let mut archive = Archive::new(decoder);
    let entries = archive.entries().map_err(|error| {
        oci_error(format!(
            "Failed to read OCI layer {}: {error}",
            layer_path.display()
        ))
    })?;

    for (index, entry) in entries.enumerate() {
        if index >= MAX_LAYER_ENTRIES {
            return Err(oci_error(format!(
                "OCI layer {} exceeds the {}-entry limit",
                layer_path.display(),
                MAX_LAYER_ENTRIES
            )));
        }
        let mut entry = entry.map_err(|error| {
            oci_error(format!(
                "Failed to read entry from {}: {error}",
                layer_path.display()
            ))
        })?;
        let raw_path = entry.path_bytes().into_owned();
        let path = normalize_archive_path(&raw_path)?;
        reject_reserved_path(&path)?;
        let Some(name) = path.last() else {
            if entry.header().entry_type().is_dir() {
                rootfs.directory(&path, metadata(&mut entry, &raw_path)?)?;
                continue;
            }
            return Err(oci_error("OCI root entry is not a directory"));
        };

        if name == b".wh..wh..opq" {
            rootfs.opaque(&path[..path.len() - 1].to_vec())?;
            continue;
        }
        if let Some(victim) = name.strip_prefix(b".wh.") {
            if victim.is_empty() {
                return Err(oci_error("OCI whiteout has an empty victim name"));
            }
            let mut victim_path = path[..path.len() - 1].to_vec();
            victim_path.push(victim.to_vec());
            reject_reserved_path(&victim_path)?;
            rootfs.whiteout(&victim_path)?;
            continue;
        }

        let entry_type = entry.header().entry_type();
        if entry_type.is_hard_link() {
            reject_overlay_xattrs(&mut entry, &raw_path)?;
            let target = entry
                .link_name_bytes()
                .ok_or_else(|| oci_error("OCI hardlink has no target"))?;
            let target = normalize_archive_path(&target)?;
            reject_reserved_path(&target)?;
            rootfs.hardlink(&path, &target)?;
            continue;
        }

        let metadata = metadata(&mut entry, &raw_path)?;
        if entry_type.is_dir() {
            rootfs.directory(&path, metadata)?;
        } else if entry_type.is_symlink() {
            let target = entry
                .link_name_bytes()
                .ok_or_else(|| oci_error("OCI symlink has no target"))?
                .into_owned();
            rootfs.symlink(&path, metadata, target)?;
        } else if entry_type.is_file() || entry_type.is_contiguous() || entry_type.is_gnu_sparse() {
            let size = entry.size();
            rootfs.regular(&path, metadata, &mut entry, size)?;
        } else if entry_type.is_character_special() {
            let (major, minor) = device_numbers(entry.header(), &raw_path)?;
            rootfs.special(&path, metadata, SpecialKind::Char { major, minor })?;
        } else if entry_type.is_block_special() {
            let (major, minor) = device_numbers(entry.header(), &raw_path)?;
            rootfs.special(&path, metadata, SpecialKind::Block { major, minor })?;
        } else if entry_type.is_fifo() {
            rootfs.special(&path, metadata, SpecialKind::Fifo)?;
        } else {
            return Err(oci_error(format!(
                "Unsupported OCI entry type {:?} at {}",
                entry_type,
                display_bytes(&raw_path)
            )));
        }
    }
    Ok(())
}

fn metadata<R: Read>(entry: &mut tar::Entry<'_, R>, path: &[u8]) -> Result<EntryMetadata> {
    let header = entry.header();
    let mode = u16::try_from(
        header.mode().map_err(|error| {
            oci_error(format!("Invalid mode at {}: {error}", display_bytes(path)))
        })? & 0o7777,
    )
    .map_err(|_| oci_error("OCI mode exceeds the ext4 metadata range"))?;
    let uid =
        u32::try_from(header.uid().map_err(|error| {
            oci_error(format!("Invalid uid at {}: {error}", display_bytes(path)))
        })?)
        .map_err(|_| oci_error(format!("OCI uid exceeds u32 at {}", display_bytes(path))))?;
    let gid =
        u32::try_from(header.gid().map_err(|error| {
            oci_error(format!("Invalid gid at {}: {error}", display_bytes(path)))
        })?)
        .map_err(|_| oci_error(format!("OCI gid exceeds u32 at {}", display_bytes(path))))?;
    let mtime = i64::try_from(header.mtime().map_err(|error| {
        oci_error(format!("Invalid mtime at {}: {error}", display_bytes(path)))
    })?)
    .map_err(|_| oci_error(format!("OCI mtime exceeds i64 at {}", display_bytes(path))))?;
    let xattrs = collect_xattrs(entry, path)?;
    Ok(EntryMetadata {
        meta: Meta::new(mode, uid, gid, (mtime, 0)),
        xattrs,
    })
}

fn collect_xattrs<R: Read>(
    entry: &mut tar::Entry<'_, R>,
    path: &[u8],
) -> Result<BTreeMap<String, Vec<u8>>> {
    let mut xattrs = BTreeMap::new();
    let Some(extensions) = entry.pax_extensions().map_err(|error| {
        oci_error(format!(
            "Failed to inspect PAX metadata at {}: {error}",
            display_bytes(path)
        ))
    })?
    else {
        return Ok(xattrs);
    };
    for extension in extensions {
        let extension = extension.map_err(|error| {
            oci_error(format!(
                "Invalid PAX metadata at {}: {error}",
                display_bytes(path)
            ))
        })?;
        let Some(name) = extension.key_bytes().strip_prefix(PAX_XATTR_PREFIX) else {
            continue;
        };
        if name.starts_with(b"trusted.overlay.") || name.starts_with(b"user.overlay.") {
            return Err(oci_error(format!(
                "OCI entry {} contains reserved overlayfs metadata",
                display_bytes(path)
            )));
        }
        let name = std::str::from_utf8(name).map_err(|_| {
            oci_error(format!(
                "OCI entry {} has a non-UTF-8 xattr name",
                display_bytes(path)
            ))
        })?;
        xattrs.insert(name.to_string(), extension.value_bytes().to_vec());
    }
    Ok(xattrs)
}

fn reject_overlay_xattrs<R: Read>(entry: &mut tar::Entry<'_, R>, path: &[u8]) -> Result<()> {
    collect_xattrs(entry, path).map(|_| ())
}

fn device_numbers(header: &tar::Header, path: &[u8]) -> Result<(u32, u32)> {
    let major = header
        .device_major()
        .map_err(|error| {
            oci_error(format!(
                "Invalid device major at {}: {error}",
                display_bytes(path)
            ))
        })?
        .ok_or_else(|| oci_error("OCI device entry has no major number"))?;
    let minor = header
        .device_minor()
        .map_err(|error| {
            oci_error(format!(
                "Invalid device minor at {}: {error}",
                display_bytes(path)
            ))
        })?
        .ok_or_else(|| oci_error("OCI device entry has no minor number"))?;
    Ok((major, minor))
}

fn reject_reserved_path(path: &GuestPath) -> Result<()> {
    let Some(first) = path.first() else {
        return Ok(());
    };
    let reserved = [
        b".a3s_image_metadata_v1.json".as_slice(),
        b".a3s_image_metadata_v1.json.tmp".as_slice(),
        b".a3s_rootfs_metadata_v1.json".as_slice(),
        b".a3s_rootfs_metadata_v1.json.tmp".as_slice(),
        b".a3s_rootfs_metadata_v1.previous.json".as_slice(),
    ];
    if reserved.contains(&first.as_slice()) {
        return Err(oci_error(format!(
            "OCI layer contains reserved internal path {}",
            display_bytes(first)
        )));
    }
    Ok(())
}

fn display_bytes(value: &[u8]) -> String {
    String::from_utf8_lossy(value).into_owned()
}

fn oci_error(message: impl Into<String>) -> BoxError {
    BoxError::OciImageError(message.into())
}