close
Skip to content
Merged
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
18 changes: 17 additions & 1 deletion crates/libcontainer/src/syscall/linux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -576,7 +576,7 @@ impl Syscall for LinuxSyscall {

if result.is_null() {
// There is no such user, or an error has occurred.
// errno gets set if theres an error.
// errno gets set if there's an error.
return None;
}

Expand Down Expand Up @@ -727,6 +727,22 @@ impl Syscall for LinuxSyscall {
umount2(target, flags)?;
Ok(())
}

fn get_uid(&self) -> Uid {
nix::unistd::getuid()
}

fn get_gid(&self) -> Gid {
nix::unistd::getgid()
}

fn get_euid(&self) -> Uid {
nix::unistd::geteuid()
}

fn get_egid(&self) -> Gid {
nix::unistd::getegid()
}
}

#[cfg(test)]
Expand Down
4 changes: 4 additions & 0 deletions crates/libcontainer/src/syscall/syscall.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ pub trait Syscall {
) -> Result<()>;
fn set_io_priority(&self, class: i64, priority: i64) -> Result<()>;
fn umount2(&self, target: &Path, flags: MntFlags) -> Result<()>;
fn get_uid(&self) -> Uid;
fn get_gid(&self) -> Gid;
fn get_euid(&self) -> Uid;
fn get_egid(&self) -> Gid;
}

#[derive(Clone, Copy)]
Expand Down
41 changes: 40 additions & 1 deletion crates/libcontainer/src/syscall/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,28 @@ impl MockCalls {

#[derive(Default)]
pub struct TestHelperSyscall {
mock_id: RefCell<MockId>,
mocks: MockCalls,
}

pub struct MockId {
uid: Uid,
gid: Gid,
euid: Uid,
egid: Gid,
}

impl Default for MockId {
fn default() -> Self {
Self {
uid: nix::unistd::getuid(),
gid: nix::unistd::getgid(),
euid: nix::unistd::geteuid(),
egid: nix::unistd::getegid(),
}
}
}

impl Syscall for TestHelperSyscall {
fn as_any(&self) -> &dyn Any {
self
Expand All @@ -158,7 +177,11 @@ impl Syscall for TestHelperSyscall {
}

fn set_id(&self, _uid: Uid, _gid: Gid) -> Result<()> {
unimplemented!()
self.mock_id.borrow_mut().uid = _uid;
self.mock_id.borrow_mut().gid = _gid;
self.mock_id.borrow_mut().euid = _uid;
self.mock_id.borrow_mut().egid = _gid;
Ok(())
}

fn unshare(&self, flags: CloneFlags) -> Result<()> {
Expand Down Expand Up @@ -276,6 +299,22 @@ impl Syscall for TestHelperSyscall {
}),
)
}

fn get_uid(&self) -> Uid {
self.mock_id.borrow().uid
}

fn get_gid(&self) -> Gid {
self.mock_id.borrow().gid
}

fn get_euid(&self) -> Uid {
self.mock_id.borrow().euid
}

fn get_egid(&self) -> Gid {
self.mock_id.borrow().egid
}
}

impl TestHelperSyscall {
Expand Down
33 changes: 22 additions & 11 deletions crates/libcontainer/src/user_ns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ use oci_spec::runtime::{Linux, LinuxIdMapping, LinuxNamespace, LinuxNamespaceTyp

use crate::error::MissingSpecError;
use crate::namespaces::{NamespaceError, Namespaces};
use crate::syscall::syscall::{create_syscall, Syscall};
use crate::utils;

// Wrap the uid/gid path function into a struct for dependency injection. This
// allows us to mock the id mapping logic in unit tests by using a different
// base path other than `/proc`.
Expand Down Expand Up @@ -149,6 +149,7 @@ pub struct UserNamespaceConfig {

impl UserNamespaceConfig {
pub fn new(spec: &Spec) -> Result<Option<Self>> {
let syscall = create_syscall();
let linux = spec.linux().as_ref().ok_or(MissingSpecError::Linux)?;
let namespaces = Namespaces::try_from(linux.namespaces().as_ref())
.map_err(ValidateSpecError::Namespaces)?;
Expand All @@ -159,7 +160,7 @@ impl UserNamespaceConfig {
if user_namespace.is_some() && user_namespace.unwrap().path().is_none() {
tracing::debug!("container with new user namespace should be created");

validate_spec_for_new_user_ns(spec).map_err(|err| {
validate_spec_for_new_user_ns(spec, &*syscall).map_err(|err| {
tracing::error!("failed to validate spec for new user namespace: {}", err);
err
})?;
Expand Down Expand Up @@ -216,13 +217,14 @@ impl TryFrom<&Linux> for UserNamespaceConfig {
let user_namespace = namespaces
.get(LinuxNamespaceType::User)
.map_err(ValidateSpecError::Namespaces)?;
let syscall = create_syscall();
Ok(Self {
newuidmap: None,
newgidmap: None,
uid_mappings: linux.uid_mappings().to_owned(),
gid_mappings: linux.gid_mappings().to_owned(),
user_namespace: user_namespace.cloned(),
privileged: !utils::rootless_required()?,
privileged: !utils::rootless_required(&*syscall)?,
id_mapper: UserNamespaceIDMapper::new(),
})
}
Expand Down Expand Up @@ -250,7 +252,10 @@ pub fn unprivileged_user_ns_enabled() -> Result<bool> {

/// Validates that the spec contains the required information for
/// creating a new user namespace
fn validate_spec_for_new_user_ns(spec: &Spec) -> std::result::Result<(), ValidateSpecError> {
fn validate_spec_for_new_user_ns(
spec: &Spec,
syscall: &dyn Syscall,
) -> std::result::Result<(), ValidateSpecError> {
tracing::debug!(
?spec,
"validating spec for container with new user namespace"
Expand Down Expand Up @@ -286,7 +291,7 @@ fn validate_spec_for_new_user_ns(spec: &Spec) -> std::result::Result<(), Validat
.as_ref()
.and_then(|process| process.user().additional_gids().as_ref())
{
let privileged = !utils::rootless_required()?;
let privileged = !utils::rootless_required(syscall)?;

match (privileged, additional_gids.is_empty()) {
(true, false) => {
Expand All @@ -299,7 +304,7 @@ fn validate_spec_for_new_user_ns(spec: &Spec) -> std::result::Result<(), Validat
}
(false, false) => {
tracing::error!(
user = ?nix::unistd::geteuid(),
user = ?syscall.get_euid(),
"user is unprivileged. Supplementary groups cannot be set in \
a rootless container for this user due to CVE-2014-8989",
);
Expand Down Expand Up @@ -463,6 +468,7 @@ mod tests {

#[test]
fn test_validate_ok() -> Result<()> {
let syscall = create_syscall();
let userns = LinuxNamespaceBuilder::default()
.typ(LinuxNamespaceType::User)
.build()?;
Expand All @@ -482,12 +488,13 @@ mod tests {
.gid_mappings(gid_mappings)
.build()?;
let spec = SpecBuilder::default().linux(linux).build()?;
assert!(validate_spec_for_new_user_ns(&spec).is_ok());
assert!(validate_spec_for_new_user_ns(&spec, &*syscall).is_ok());
Ok(())
}

#[test]
fn test_validate_err() -> Result<()> {
let syscall = create_syscall();
let userns = LinuxNamespaceBuilder::default()
.typ(LinuxNamespaceType::User)
.build()?;
Expand All @@ -511,7 +518,8 @@ mod tests {
&SpecBuilder::default()
.linux(linux_uid_empty)
.build()
.unwrap()
.unwrap(),
&*syscall
)
.is_err());

Expand All @@ -524,7 +532,8 @@ mod tests {
&SpecBuilder::default()
.linux(linux_gid_empty)
.build()
.unwrap()
.unwrap(),
&*syscall
)
.is_err());

Expand All @@ -536,7 +545,8 @@ mod tests {
&SpecBuilder::default()
.linux(linux_uid_none)
.build()
.unwrap()
.unwrap(),
&*syscall
)
.is_err());

Expand All @@ -548,7 +558,8 @@ mod tests {
&SpecBuilder::default()
.linux(linux_gid_none)
.build()
.unwrap()
.unwrap(),
&*syscall
)
.is_err());

Expand Down
11 changes: 7 additions & 4 deletions crates/libcontainer/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use nix::unistd::{Uid, User};
use oci_spec::runtime::Spec;

use crate::error::LibcontainerError;
use crate::syscall::syscall::{create_syscall, Syscall};
use crate::user_ns::UserNamespaceConfig;

#[derive(Debug, thiserror::Error)]
Expand Down Expand Up @@ -259,18 +260,19 @@ pub fn is_in_new_userns() -> Result<bool, std::io::Error> {
}

/// Checks if rootless mode needs to be used
pub fn rootless_required() -> Result<bool, std::io::Error> {
if !nix::unistd::geteuid().is_root() {
pub fn rootless_required(syscall: &dyn Syscall) -> Result<bool, std::io::Error> {
if !syscall.get_euid().is_root() {
return Ok(true);
}
is_in_new_userns()
}

/// checks if given spec is valid for current user namespace setup
pub fn validate_spec_for_new_user_ns(spec: &Spec) -> Result<(), LibcontainerError> {
let syscall = create_syscall();
let config = UserNamespaceConfig::new(spec)?;
let in_user_ns = is_in_new_userns().map_err(LibcontainerError::OtherIO)?;
let is_rootless_required = rootless_required().map_err(LibcontainerError::OtherIO)?;
let is_rootless_required = rootless_required(&*syscall).map_err(LibcontainerError::OtherIO)?;
// In case of rootless, there are 2 possible cases :
// we have a new user ns specified in the spec
// or the youki is launched in a new user ns (this is how podman does it)
Expand Down Expand Up @@ -372,7 +374,8 @@ mod tests {
{
let temdir = tempfile::tempdir()?;
let path = temdir.path().join("test");
let uid = nix::unistd::getuid().as_raw();
let syscall = create_syscall();
let uid = syscall.get_uid().as_raw();
let mode = Mode::S_IRWXU;
create_dir_all_with_mode(&path, uid, mode)?;
let metadata = path.metadata()?;
Expand Down
16 changes: 9 additions & 7 deletions crates/youki/src/commands/spec_json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ use libcontainer::oci_spec::runtime::{
LinuxBuilder, LinuxIdMappingBuilder, LinuxNamespace, LinuxNamespaceBuilder, LinuxNamespaceType,
Mount, Spec,
};
use nix;
use libcontainer::syscall::syscall::Syscall;
use serde_json::to_writer_pretty;

pub fn get_default() -> Result<Spec> {
Ok(Spec::default())
}

pub fn get_rootless() -> Result<Spec> {
pub fn get_rootless(syscall: &dyn Syscall) -> Result<Spec> {
// Remove network and user namespace from the default spec
let mut namespaces: Vec<LinuxNamespace> =
libcontainer::oci_spec::runtime::get_default_namespaces()
Expand All @@ -31,8 +31,8 @@ pub fn get_rootless() -> Result<Spec> {
.build()?,
);

let uid = nix::unistd::geteuid().as_raw();
let gid = nix::unistd::getegid().as_raw();
let uid = syscall.get_euid().as_raw();
let gid = syscall.get_egid().as_raw();

let linux = LinuxBuilder::default()
.namespaces(namespaces)
Expand Down Expand Up @@ -82,9 +82,9 @@ pub fn get_rootless() -> Result<Spec> {
}

/// spec Cli command
pub fn spec(args: liboci_cli::Spec) -> Result<()> {
pub fn spec(args: liboci_cli::Spec, syscall: &dyn Syscall) -> Result<()> {
let spec = if args.rootless {
get_rootless()?
get_rootless(syscall)?
} else {
get_default()?
};
Expand All @@ -100,14 +100,16 @@ pub fn spec(args: liboci_cli::Spec) -> Result<()> {
#[cfg(test)]
// Tests become unstable if not serial. The cause is not known.
mod tests {
use libcontainer::syscall::syscall::create_syscall;
use serial_test::serial;

use super::*;

#[test]
#[serial]
fn test_spec_json() -> Result<()> {
let spec = get_rootless()?;
let syscall = create_syscall();
let spec = get_rootless(&*syscall)?;
let tmpdir = tempfile::tempdir().expect("failed to create temp dir");
let path = tmpdir.path().join("config.json");
let file = File::create(path)?;
Expand Down
9 changes: 6 additions & 3 deletions crates/youki/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ mod workload;

use anyhow::{Context, Result};
use clap::{crate_version, CommandFactory, Parser};
use libcontainer::syscall::syscall::create_syscall;
use liboci_cli::{CommonCmd, GlobalOpts, StandardCmd};

use crate::commands::info;
Expand Down Expand Up @@ -88,6 +89,7 @@ fn main() -> Result<()> {

let opts = Opts::parse();
let mut app = Opts::command();
let syscall = create_syscall();

observability::init(&opts).map_err(|err| {
eprintln!("failed to initialize observability: {}", err);
Expand All @@ -96,10 +98,11 @@ fn main() -> Result<()> {

tracing::debug!(
"started by user {} with {:?}",
nix::unistd::geteuid(),
syscall.get_euid(),
std::env::args_os()
);
let root_path = rootpath::determine(opts.global.root)?;

let root_path = rootpath::determine(opts.global.root, &*syscall)?;
let systemd_cgroup = opts.global.systemd_cgroup;

let cmd_result = match opts.subcmd {
Expand Down Expand Up @@ -138,7 +141,7 @@ fn main() -> Result<()> {
std::process::exit(-1);
}
},
CommonCmd::Spec(spec) => commands::spec_json::spec(spec),
CommonCmd::Spec(spec) => commands::spec_json::spec(spec, &*syscall),
CommonCmd::Update(update) => commands::update::update(update, root_path),
},

Expand Down
Loading