diff --git a/Cargo.toml b/Cargo.toml
index 9615c74..aba3ef6 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -19,7 +19,6 @@ glob = "0.3.0"
log = "0.4.19"
nix = { version = "0.26.2", features = [
"sched",
- "signal",
"user",
], default-features = false }
oci-spec = { version = "0.6.1", features = ["image"], default-features = false }
diff --git a/README.md b/README.md
index d5a6e13..cd15ab7 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@ rpmoci builds OCI container images from RPM packages, using [DNF](https://github
rpmoci features:
- **deterministic** rpmoci locks RPM dependencies using the package file/lockfile paradigm of bundler/cargo etc and supports vendoring of RPMs for later rebuilds.
- - **unprivileged** rpmoci can build images in environments without docker access, and without root access (this relies on the user being able to create [user namespaces](https://www.man7.org/linux/man-pages/man7/user_namespaces.7.html))
+ - **unprivileged** rpmoci can build images in environments without access to a container runtime, and without root access (this relies on the user being able to create [user namespaces](https://www.man7.org/linux/man-pages/man7/user_namespaces.7.html))
- **small** rpmoci images are built solely from the RPMs you request, so don't contain unnecessary dependencies.
The design of rpmoci is influenced by [apko](https://github.com/chainguard-dev/apko) and [distroless](https://github.com/GoogleContainerTools/distroless) tooling.
@@ -19,6 +19,14 @@ rpmoci isn't published to a widely accessible location yet so you'll need build
rpmoci has a runtime dependency on dnf and python-rpm (a dependency of dnf, so shouldn't need to be specified directly).
+### Rootless setup
+When rpmoci runs as a non-root user it will automatically attempt to setup a user namespace in which to run.
+rpmoci maps the user's uid/gid to root in the user namespace.
+
+It also attempts to map the current user's subuid/subgid range into the user namespace, which is required for rpmoci to be able to create containers from RPMs that contain files owned by a non-root user.
+
+If your user doesn't have any subuids/subgids then you'll need to create them per [https://rootlesscontaine.rs/getting-started/common/subuid/](https://rootlesscontaine.rs/getting-started/common/subuid/).
+
## Getting started
You need to create an rpmoci.toml file. An example is:
diff --git a/src/main.rs b/src/main.rs
index 879ca7b..0989098 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -12,14 +12,14 @@
//!
//! You should have received a copy of the GNU General Public License
//! along with this program. If not, see .
-use std::{fs::File, io::Write, os::unix::process::CommandExt, path::PathBuf, process::Command};
+use std::{fs::read_to_string, os::unix::process::CommandExt, path::Path, process::Command};
use anyhow::{bail, Context, Result};
use clap::Parser;
use nix::{
sched::CloneFlags,
- sys::{signal, wait::wait},
- unistd::{close, getgid, getuid, pipe, read},
+ sys::wait::wait,
+ unistd::{close, getgid, getuid, pipe, read, Gid, Group, Pid, Uid, User},
};
use rpmoci::write;
@@ -65,27 +65,15 @@ fn run_in_userns() -> anyhow::Result<()> {
255
}),
stack,
- CloneFlags::CLONE_NEWUSER,
- Some(signal::SIGCHLD as i32),
+ CloneFlags::CLONE_NEWUSER | CloneFlags::CLONE_NEWNS,
+ None,
)
.context("Clone failed")?;
// this parent process sets up user namespace mappings, notifies the child to continue,
// then waits for the child to exit
close(reader)?;
- let child_proc = PathBuf::from("/proc").join(child.to_string());
- File::create(child_proc.join("setgroups"))
- .context("failed to create setgroups file")?
- .write_all(b"deny")
- .context("failed to write to setgroups file")?;
- // map the current uid/gid to root, and create mappings for uids/gids 1-999 as
- // RPMs could potentially contain files owned by any of these
- // (these additional mappings are the cause of us needing to spawn a child - otherwise
- // we could just unshare and configure mappings in the current process)
- File::create(child_proc.join("uid_map"))?
- .write_all(format!("0 {user_id} 1\n1 100000 999").as_bytes())?;
- File::create(child_proc.join("gid_map"))?
- .write_all(format!("0 {group_id} 1\n1 100000 999").as_bytes())?;
+ setup_id_maps(child, user_id, group_id).context("Failed to setup id mappings")?;
close(writer)?;
let status = wait()?;
if let nix::sys::wait::WaitStatus::Exited(_, code) = status {
@@ -95,6 +83,98 @@ fn run_in_userns() -> anyhow::Result<()> {
}
}
+// Represents a range of sub uid/gids
+#[derive(Debug)]
+struct SubIdRange {
+ start: usize,
+ count: usize,
+}
+
+fn get_sub_id_ranges(
+ subid_path: impl AsRef,
+ id: &str,
+ name: Option<&str>,
+) -> anyhow::Result> {
+ let subid_path = subid_path.as_ref();
+ Ok(read_to_string(subid_path)
+ .context(format!(
+ "Failed to read sub id file {}",
+ subid_path.display()
+ ))?
+ .lines() // split the string into an iterator of string slices
+ .filter_map(|line| {
+ let parts = line.splitn(2, ':').collect::>();
+ if parts.len() != 3 {
+ // Not a valid line
+ return None;
+ }
+ if Some(parts[0]) != name || parts[0] != id {
+ // Not a line for the desired user/group
+ return None;
+ }
+ if let (Ok(start), Ok(count)) = (parts[1].parse::(), parts[2].parse::()) {
+ Some(SubIdRange { start, count })
+ } else {
+ None
+ }
+ })
+ .collect())
+}
+
+/// Create new uid/gid mappings for the current user/group
+fn setup_id_maps(child: Pid, uid: Uid, gid: Gid) -> anyhow::Result<()> {
+ let username = User::from_uid(uid).ok().flatten().map(|user| user.name);
+ let uid_string = uid.to_string();
+ let subuid_ranges = get_sub_id_ranges("/etc/subuid", &uid_string, username.as_deref())?;
+
+ let groupname = Group::from_gid(gid)
+ .ok()
+ .flatten()
+ .map(|group: Group| group.name);
+ let gid_string = gid.to_string();
+ let subgid_ranges = get_sub_id_ranges("/etc/subuid", &gid.to_string(), groupname.as_deref())?;
+
+ let mut uid_args = vec![
+ child.to_string(),
+ "0".to_string(),
+ uid_string,
+ "1".to_string(),
+ ];
+ let mut next_uid = 1;
+ for range in subuid_ranges {
+ uid_args.push(next_uid.to_string());
+ uid_args.push(range.start.to_string());
+ uid_args.push(range.count.to_string());
+ next_uid += range.count;
+ }
+
+ let mut gid_args = vec![
+ child.to_string(),
+ "0".to_string(),
+ gid_string,
+ "1".to_string(),
+ ];
+ let mut next_gid = 1;
+ for range in subgid_ranges {
+ gid_args.push(next_gid.to_string());
+ gid_args.push(range.start.to_string());
+ gid_args.push(range.count.to_string());
+ next_gid += range.count;
+ }
+
+ let status = Command::new("newuidmap").args(uid_args).status()?;
+ if !status.success() {
+ bail!("Failed to create uid mappings");
+ }
+
+ let status = Command::new("newgidmap").args(gid_args).status()?;
+ if !status.success() {
+ bail!("Failed to create gid mappings");
+ }
+
+ Ok(())
+}
+
fn try_main() -> Result<()> {
let args = rpmoci::cli::Cli::parse();
env_logger::Builder::new()