Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions lib/hyper/node/fire_vmm/jailer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -147,10 +147,15 @@ defmodule Hyper.Node.FireVMM.Jailer do
type
|> Instance.spec()
|> Instance.Spec.cgroup_v2()
|> Sys.Linux.Cgroup.V2.Config.as_linux()
|> Enum.flat_map(fn {file, value} -> ["--cgroup", "#{file}=#{value}"] end)
|> Enum.flat_map(&cgroup_flag/1)
end

@spec cgroup_flag({atom(), term()}) :: [String.t()]
defp cgroup_flag({:memory_max, bytes}), do: ["--memory-max", to_string(bytes)]

defp cgroup_flag({:cpu_max, %{quota_us: quota, period_us: period}}),
do: ["--cpu-quota-us", to_string(quota), "--cpu-period-us", to_string(period)]

@doc "Host path of the VM's per-VM jail dir (`<chroot_base>/<exec>/<id>`)."
@spec chroot_dir(Hyper.Vm.Id.t()) :: Path.t()
def chroot_dir(id) do
Expand Down
12 changes: 10 additions & 2 deletions native/suidhelper/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,12 @@ name = "tools_chroot_jail_grant_vsock"
path = "tests/tools/chroot_jail_grant_vsock.rs"

[[test]]
name = "util_chroot_jail"
path = "tests/util/chroot_jail.rs"
name = "firecracker_chroot_jail"
path = "tests/firecracker/chroot_jail.rs"

[[test]]
name = "firecracker_id"
path = "tests/firecracker/id.rs"

[[test]]
name = "e2e_config"
Expand Down Expand Up @@ -117,6 +121,10 @@ path = "tests/config_uid_gid_range.rs"
name = "config_network"
path = "tests/config_network.rs"

[[test]]
name = "config_paths"
path = "tests/config_paths.rs"

[[test]]
name = "tools_dmsetup_backing"
path = "tests/tools/dmsetup_backing.rs"
Expand Down
95 changes: 28 additions & 67 deletions native/suidhelper/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@
//! decide which UIDs it *accepts* (default `{900_000, 999_999}` when the key is
//! absent). Operators narrowing the range must set **both**.

use crate::firecracker::id::UidGidRange;
use crate::fs::file::{self, IsRegularFile, OpenFlags, OwnerOnlyWritable, RootOwner, SafeFile};
use crate::fs::path::{self, IsAbsolute, SafePath, StrictComponents};
use crate::fs::path::{
self, IsAbsolute, SafePath, SafePathBuf, StrictComponent, StrictComponentBuf, StrictComponents,
};
use crate::linux::cgroup;
use crate::process::binary::{self, RootSafeBin, RootSafeBinPathBuf};
use serde::Deserialize;
use smart_default::SmartDefault;
Expand All @@ -26,12 +30,8 @@ pub enum LoadingError {
File(#[from] file::ValidationError),
#[error("{0:?} could not be read")]
Unreadable(PathBuf),
#[error("{0:?} is not valid TOML")]
Malformed(PathBuf),
#[error("work_dir in {0:?} must be an absolute path")]
Relative(PathBuf),
#[error("uid_gid_range.min must be >= 1 and <= max (got min={min}, max={max})")]
BadUidGidRange { min: u32, max: u32 },
#[error("{0:?} is not valid TOML: {1}")]
Malformed(PathBuf, #[source] toml::de::Error),
}

/// Error returned by config accessors for tool binaries derived from config.
Expand All @@ -46,40 +46,6 @@ pub enum BinError {
const CONFIG_PATHSTR: &str = "/etc/hyper/config.toml";
const INSECURE_CONFIG_PATH_ENV: &str = "HYPER_SETUIDHELPER_CONFIG_PATH";

/// UID/GID allocation band, read from `[jails] uid_gid_range` in config.toml as
/// a two-element `[min, max]` array. Controls which UIDs the helper *accepts*
/// from the BEAM — see module docs.
#[derive(Debug, Clone, Copy, Deserialize)]
#[serde(from = "[u32; 2]")]
pub struct UidGidRange {
pub min: u32,
pub max: u32,
}

impl From<[u32; 2]> for UidGidRange {
fn from([min, max]: [u32; 2]) -> Self {
Self { min, max }
}
}

// Band defaults match Elixir's `compile_env` allocation defaults so that an
// unconfigured helper and an unconfigured node agree out of the box.
const DEFAULT_UID_GID: (u32, u32) = (900_000, 999_999);

/// Validate a uid_gid_range value. A present range where min==0 or min>max is
/// treated as a config trust violation — fatal at load, consistent with the
/// "present but untrusted" model. Exposed so tests can verify the contract
/// without touching the file system.
pub fn validate_uid_gid_range(r: &UidGidRange) -> Result<(), LoadingError> {
if r.min == 0 || r.min > r.max {
return Err(LoadingError::BadUidGidRange {
min: r.min,
max: r.max,
});
}
Ok(())
}

/// The config file path. In production this is the fixed `/etc/hyper/config.toml`.
/// Only in INSECURE TEST MODE (both gates open) may an env var redirect it — the
/// secure arm is always the hardcoded path, so a release build cannot be steered.
Expand All @@ -98,7 +64,7 @@ fn config_path() -> PathBuf {
#[derive(Debug, Clone, Deserialize, SmartDefault)]
pub struct Config {
#[default(default_work_dir())]
work_dir: PathBuf,
work_dir: SafePathBuf<IsAbsolute, StrictComponents>,
#[serde(default)]
tools: Tools,
#[serde(default)]
Expand All @@ -117,8 +83,8 @@ pub struct Config {
#[serde(default)]
pub struct Jails {
#[default(default_parent_cgroup())]
cgroup: String,
uid_gid_range: Option<UidGidRange>,
cgroup: StrictComponentBuf,
uid_gid_range: UidGidRange,
}

/// Paths to the external binaries the helper runs, the `[tools]` table.
Expand Down Expand Up @@ -153,13 +119,15 @@ pub struct Tools {
// The default data root. Must match the Elixir node's `@dev_work_dir`, which it
// uses when the same config file is absent, so both sides agree (see
// `Hyper.Node.check_helper_base`).
fn default_work_dir() -> PathBuf {
PathBuf::from("/srv/hyper")
fn default_work_dir() -> SafePathBuf<IsAbsolute, StrictComponents> {
SafePath::from_static::<"/srv/hyper">().to_owned()
}

fn default_parent_cgroup() -> String {
const JAILS: &StrictComponent = StrictComponent::from_static("jails");

fn default_parent_cgroup() -> StrictComponentBuf {
// Must match Elixir node's `@parent_cgroup`; operators need to keep them in sync.
"hyper".into()
StrictComponent::from_static("hyper").to_owned()
}

/// The `[network]` table: VM egress networking. Absent, or present without
Expand Down Expand Up @@ -204,13 +172,13 @@ impl Config {
}

/// Hyper's data root.
pub fn hyper_base(&self) -> &std::path::Path {
self.work_dir.as_path()
pub fn hyper_base(&self) -> &SafePath<IsAbsolute, StrictComponents> {
&self.work_dir
}

/// Hyper's jail root, `<work_dir>/jails`.
pub fn jail_base(&self) -> PathBuf {
self.work_dir.join("jails")
pub fn jail_base(&self) -> SafePathBuf<IsAbsolute, StrictComponents> {
self.work_dir.join(JAILS)
}

/// The validated `dmsetup` binary the helper will run.
Expand Down Expand Up @@ -285,19 +253,20 @@ impl Config {

/// The jailer `--parent-cgroup` value, from `[jails] cgroup`. Defaults to
/// `"hyper"`, matching the Elixir node's default.
pub fn parent_cgroup(&self) -> &str {
pub fn parent_cgroup(&self) -> &StrictComponent {
&self.jails.cgroup
}

pub fn parent_cgroup_dir(&self) -> SafePathBuf<IsAbsolute, StrictComponents> {
SafePath::from_static::<{ cgroup::ROOT }>().join(&self.jails.cgroup)
}

/// The UID/GID band the helper accepts from the BEAM. Defaults to
/// `(900_000, 999_999)` when the key is absent (matching Elixir's defaults).
/// A present range with min==0 or min>max is rejected at load time by
/// [`Config::safe_load`], so this accessor is always total.
pub fn uid_gid_range(&self) -> (u32, u32) {
self.jails
.uid_gid_range
.map(|r| (r.min, r.max))
.unwrap_or(DEFAULT_UID_GID)
pub fn uid_gid_range(&self) -> UidGidRange {
self.jails.uid_gid_range
}

/// Read, ownership-check, parse, and validate the config file. See the module
Expand All @@ -324,15 +293,7 @@ impl Config {
let body = std::io::read_to_string(std::fs::File::from(OwnedFd::from(file)))
.map_err(|_| LoadingError::Unreadable(path.clone()))?;
let config: Config =
toml::from_str(&body).map_err(|_| LoadingError::Malformed(path.clone()))?;

if !config.work_dir.is_absolute() {
return Err(LoadingError::Relative(path));
}

if let Some(r) = &config.jails.uid_gid_range {
validate_uid_gid_range(r)?;
}
toml::from_str(&body).map_err(|e| LoadingError::Malformed(path.clone(), e))?;

Ok(config)
}
Expand Down
Loading
Loading