hl-engine 0.1.0

Safe Rust lifecycle API for the standalone HL Linux guest engine
use crate::{Access, Config, Error, Sandbox};
use std::ffi::{OsStr, OsString};
use std::path::Path;

fn checked_bytes(value: &OsStr) -> Result<&[u8], Error> {
    use std::os::unix::ffi::OsStrExt;
    if value.as_bytes().contains(&0) {
        Err(Error::InvalidConfig(
            "configuration strings must not contain NUL",
        ))
    } else {
        Ok(value.as_bytes())
    }
}

const MAGIC: u32 = 0x484c_4346;
const ABI: u32 = 5;
const HEADER_SIZE: usize = 144;
const HEADER_SIZE_U32: u32 = 144;
const MAGIC_OFFSET: usize = 0;
const POOL_SIZE_OFFSET: usize = 4;
const HEADER_SIZE_OFFSET: usize = 8;
const ABI_OFFSET: usize = 12;
const MEMORY_OFFSET: usize = 16;
const PID_OFFSET: usize = 24;
const CPU_OFFSET: usize = 28;
const UID_OFFSET: usize = 32;
const GID_OFFSET: usize = 36;
const ROOT_READ_ONLY_OFFSET: usize = 40;
const SANDBOX_OFFSET: usize = 44;
const NETWORK_ISOLATED_OFFSET: usize = 48;
const ROOTFS_OFFSET: usize = 56;
const HOSTNAME_OFFSET: usize = 64;
const VOLUMES_OFFSET: usize = 76;
const WORKDIR_OFFSET: usize = 84;
const ENVIRONMENT_OFFSET: usize = 88;
const CACHE_OFFSET: usize = 92;
const ARGUMENTS_OFFSET: usize = 108;
const RESULT_OFFSET: usize = 132;
const RESERVED_OFFSET: usize = 136;
const TAIL_PADDING_OFFSET: usize = 140;

const _: () = assert!(MEMORY_OFFSET % 8 == 0);
const _: () = assert!(RESULT_OFFSET == 132);
const _: () = assert!(TAIL_PADDING_OFFSET + 4 == HEADER_SIZE);

struct Pool(Vec<u8>);

impl Pool {
    fn new() -> Self {
        Self(vec![0])
    }

    fn string(&mut self, value: Option<&OsStr>) -> Result<u32, Error> {
        let Some(value) = value else { return Ok(0) };
        let value = checked_bytes(value)?;
        let offset = u32::try_from(self.0.len())
            .map_err(|_| Error::InvalidConfig("launch configuration is too large"))?;
        self.0.extend_from_slice(value);
        self.0.push(0);
        Ok(offset)
    }

    fn path(&mut self, value: Option<&Path>) -> Result<u32, Error> {
        self.string(value.map(Path::as_os_str))
    }

    fn arguments(&mut self, values: &[OsString]) -> Result<u32, Error> {
        let offset = u32::try_from(self.0.len())
            .map_err(|_| Error::InvalidConfig("launch configuration is too large"))?;
        for value in values {
            self.0.extend_from_slice(checked_bytes(value)?);
            self.0.push(0);
        }
        self.0.push(0);
        Ok(offset)
    }
}

struct Header([u8; HEADER_SIZE]);

impl Header {
    fn new() -> Self {
        Self([0; HEADER_SIZE])
    }
    fn u32(&mut self, offset: usize, value: u32) {
        self.0[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
    }
    fn i32(&mut self, offset: usize, value: i32) {
        self.u32(offset, u32::from_ne_bytes(value.to_ne_bytes()));
    }
    fn u64(&mut self, offset: usize, value: u64) {
        self.0[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
    }
}

fn environment(config: &Config) -> Result<Option<OsString>, Error> {
    use std::os::unix::ffi::OsStringExt;
    if config.environment.is_empty() {
        return Ok(None);
    }
    let mut output = Vec::new();
    for (index, (name, value)) in config.environment.iter().enumerate() {
        let name = checked_bytes(name)?;
        let value = checked_bytes(value)?;
        if name.is_empty()
            || name.contains(&b'=')
            || name.contains(&b'\n')
            || value.contains(&b'\n')
        {
            return Err(Error::InvalidConfig("invalid environment record"));
        }
        if index != 0 {
            output.push(b'\n');
        }
        output.extend_from_slice(name);
        output.push(b'=');
        output.extend_from_slice(value);
    }
    Ok(Some(OsString::from_vec(output)))
}

fn volumes(config: &Config) -> Result<Option<OsString>, Error> {
    use std::os::unix::ffi::OsStringExt;
    if config.mounts.is_empty() {
        return Ok(None);
    }
    let mut output = Vec::new();
    for (index, mount) in config.mounts.iter().enumerate() {
        let host = checked_bytes(mount.host.as_os_str())?;
        let guest = checked_bytes(mount.guest.as_os_str())?;
        if host.contains(&b',')
            || host.contains(&b':')
            || guest.contains(&b',')
            || guest.contains(&b':')
        {
            return Err(Error::InvalidConfig(
                "mount paths must not contain ':' or ','",
            ));
        }
        if index != 0 {
            output.push(b',');
        }
        output.extend_from_slice(if mount.access == Access::ReadOnly {
            b"ro:"
        } else {
            b"rw:"
        });
        output.extend_from_slice(guest);
        output.push(b':');
        output.extend_from_slice(host);
    }
    Ok(Some(OsString::from_vec(output)))
}

pub(crate) fn encode(
    config: &Config,
    arguments: &[OsString],
    result: Option<&Path>,
) -> Result<Vec<u8>, Error> {
    let mut pool = Pool::new();
    let rootfs = pool.path(config.rootfs.as_deref())?;
    let hostname = pool.string(config.hostname.as_deref())?;
    let workdir = pool.string(config.working_directory.as_deref())?;
    let environment = environment(config)?;
    let environment = pool.string(environment.as_deref())?;
    let cache = pool.path(config.translation_cache.as_deref())?;
    let volumes = volumes(config)?;
    let volumes = pool.string(volumes.as_deref())?;
    let result = pool.path(result)?;
    let arguments = pool.arguments(arguments)?;
    let pool_size = u32::try_from(pool.0.len())
        .map_err(|_| Error::InvalidConfig("launch configuration is too large"))?;

    let mut header = Header::new();
    header.u32(MAGIC_OFFSET, MAGIC);
    header.u32(POOL_SIZE_OFFSET, pool_size);
    header.u32(HEADER_SIZE_OFFSET, HEADER_SIZE_U32);
    header.u32(ABI_OFFSET, ABI);
    header.u64(MEMORY_OFFSET, config.memory_limit);
    header.u32(PID_OFFSET, config.pid_limit);
    header.u32(CPU_OFFSET, config.cpu_limit);
    header.i32(UID_OFFSET, config.uid.unwrap_or(-1));
    header.i32(GID_OFFSET, config.gid.unwrap_or(-1));
    header.u32(ROOT_READ_ONLY_OFFSET, u32::from(config.rootfs_read_only));
    header.u32(
        SANDBOX_OFFSET,
        match config.sandbox {
            Sandbox::Disabled => 0,
            Sandbox::Enabled => 1,
            Sandbox::SentryOnly => 2,
        },
    );
    header.u32(NETWORK_ISOLATED_OFFSET, u32::from(config.network_isolated));
    header.u32(ROOTFS_OFFSET, rootfs);
    header.u32(HOSTNAME_OFFSET, hostname);
    header.u32(VOLUMES_OFFSET, volumes);
    header.u32(WORKDIR_OFFSET, workdir);
    header.u32(ENVIRONMENT_OFFSET, environment);
    header.u32(CACHE_OFFSET, cache);
    header.u32(ARGUMENTS_OFFSET, arguments);
    header.u32(RESULT_OFFSET, result);
    header.u32(RESERVED_OFFSET, 0);
    header.u32(TAIL_PADDING_OFFSET, 0);

    let mut wire = Vec::with_capacity(HEADER_SIZE + pool.0.len());
    wire.extend_from_slice(&header.0);
    wire.extend_from_slice(&pool.0);
    debug_assert_eq!(wire.len(), HEADER_SIZE + pool_size as usize);
    Ok(wire)
}