Skip to content
Draft
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
102 changes: 102 additions & 0 deletions src/install.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
//! `--install` bootstrap: fetch the NixOS nix-installer and run it inside
//! our user namespace so it sees a writable `/nix` without real root.

use std::{
env, fs, io,
os::unix::fs::PermissionsExt,
path::{Path, PathBuf},
process,
};

const INSTALLER_BASE_URL: &str = "https://artifacts.nixos.org/nix-installer";

/// Determine the default nix store location when the user didn't pass one.
///
/// Follows XDG: `$XDG_DATA_HOME/nix`, falling back to `~/.local/share/nix`.
pub fn default_nixpath() -> PathBuf {
env::var_os("XDG_DATA_HOME")
.map(PathBuf::from)
.or_else(|| env::var_os("HOME").map(|h| PathBuf::from(h).join(".local/share")))
.expect("neither XDG_DATA_HOME nor HOME is set")
.join("nix")
}

fn installer_arch() -> &'static str {
match env::consts::ARCH {
"x86_64" => "x86_64-linux",
"aarch64" => "aarch64-linux",
other => {
eprintln!("--install: unsupported architecture {other}");
eprintln!("nix-installer ships binaries for x86_64 and aarch64 only.");
process::exit(1);
}
}
}

/// Fetch the nix-installer binary to a temp file and return its path.
///
/// Shells out to `curl` to avoid bundling an HTTP+TLS stack into a binary
/// whose normal operation never touches the network. Honours
/// `NIX_USER_CHROOT_INSTALLER` to skip the download (useful for offline
/// bootstrap and for the integration tests).
pub fn fetch_installer() -> io::Result<PathBuf> {
if let Some(path) = env::var_os("NIX_USER_CHROOT_INSTALLER") {
let path = PathBuf::from(path);
if !path.exists() {
return Err(io::Error::new(
io::ErrorKind::NotFound,
format!("NIX_USER_CHROOT_INSTALLER points at {path:?} which does not exist"),
));
}
return Ok(path);
}

let url = format!("{INSTALLER_BASE_URL}/nix-installer-{}", installer_arch());
let dest = env::temp_dir().join(format!("nix-installer.{}", process::id()));

eprintln!("Fetching {url}");
let status = process::Command::new("curl")
.args(["-sSfL", "--output"])
.arg(&dest)
.arg(&url)
.status()
.map_err(|e| io::Error::other(format!("failed to spawn curl (is it installed?): {e}")))?;

if !status.success() {
let _ = fs::remove_file(&dest);
return Err(io::Error::other(format!(
"curl exited with {status} while fetching {url}"
)));
}

fs::set_permissions(&dest, fs::Permissions::from_mode(0o755))?;
Ok(dest)
}

/// Command line to run inside the chroot.
///
/// `--rootless` keeps the installer entirely under `/nix`: no nixbld
/// users/group, no `/etc/nix/nix.conf`, no shell-profile edits, no init
/// integration. All of those would fail anyway since our chroot bind-mounts
/// the host `/etc` read-only and the user namespace only has a single uid/gid
/// mapped. The install receipt lands in `/nix/receipt.json` which surfaces on
/// the host as `<nixpath>/receipt.json` for later `uninstall`.
pub fn installer_command(installer: &Path) -> (String, Vec<String>) {
(
installer.to_string_lossy().into_owned(),
vec![
"install".into(),
"linux".into(),
"--rootless".into(),
"--no-confirm".into(),
],
)
}

pub fn print_next_steps(nixpath: &Path) {
eprintln!();
eprintln!("Installation complete. Enter the environment with:");
eprintln!();
eprintln!(" nix-user-chroot {} bash -l", nixpath.display());
eprintln!();
}
98 changes: 85 additions & 13 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use nix::{
};
use serde::Deserialize;

mod install;
mod mkdtemp;

const NONE: Option<&'static [u8]> = None;
Expand Down Expand Up @@ -397,7 +398,13 @@ impl<'a> RunChroot<'a> {
}
}

fn run_chroot(&self, cmd: &str, args: &[String], path_config: Option<PathConfig>) {
fn run_chroot(
&self,
cmd: &str,
args: &[String],
path_config: Option<PathConfig>,
map_root: bool,
) {
let cwd = env::current_dir().expect("cannot get current working directory");

let uid = unistd::getuid();
Expand Down Expand Up @@ -544,16 +551,23 @@ impl<'a> RunChroot<'a> {
let _ = file.write_all(b"deny");
}

// Normal operation maps our uid to itself so files created inside the
// chroot are owned by the real user on the host. --install instead
// maps us to root so nix-installer's EUID==0 checks pass; everything
// it writes still lands as our real uid on the host filesystem.
let inner_uid = if map_root { 0 } else { uid.as_raw() };
let inner_gid = if map_root { 0 } else { gid.as_raw() };

let mut uid_map =
fs::File::create("/proc/self/uid_map").expect("failed to open /proc/self/uid_map");
uid_map
.write_all(format!("{uid} {uid} 1").as_bytes())
.write_all(format!("{inner_uid} {uid} 1").as_bytes())
.expect("failed to write new uid mapping to /proc/self/uid_map");

let mut gid_map =
fs::File::create("/proc/self/gid_map").expect("failed to open /proc/self/gid_map");
gid_map
.write_all(format!("{gid} {gid} 1").as_bytes())
.write_all(format!("{inner_gid} {gid} 1").as_bytes())
.expect("failed to write new gid mapping to /proc/self/gid_map");

// restore cwd
Expand All @@ -570,7 +584,7 @@ impl<'a> RunChroot<'a> {
}
}

fn wait_for_child(rootdir: &Path, child_pid: unistd::Pid) -> ! {
fn wait_for_child(rootdir: &Path, child_pid: unistd::Pid) -> i32 {
let mut exit_status = 1;
loop {
match waitpid(child_pid, Some(WaitPidFlag::WUNTRACED)) {
Expand Down Expand Up @@ -601,7 +615,7 @@ fn wait_for_child(rootdir: &Path, child_pid: unistd::Pid) -> ! {
fs::remove_dir_all(rootdir)
.unwrap_or_else(|err| panic!("cannot remove tempdir {}: {}", rootdir.display(), err));

process::exit(exit_status);
exit_status
}

fn main() {
Expand All @@ -612,16 +626,67 @@ fn main() {
.init();

let args: Vec<String> = env::args().collect();
if args.len() < 3 {
eprintln!("Usage: {} <nixpath> <command>\n", args[0]);
process::exit(1);
}

let (nixpath_arg, cmd, cmd_args, map_root, installing): (
PathBuf,
String,
Vec<String>,
bool,
bool,
) = if args.get(1).map(String::as_str) == Some("--install") {
let nixpath = args
.get(2)
.map(PathBuf::from)
.unwrap_or_else(install::default_nixpath);

if nixpath.join("store").exists() {
eprintln!(
"{}: store already exists, refusing to reinstall.",
nixpath.display()
);
eprintln!(
"Enter it with: nix-user-chroot {} bash -l",
nixpath.display()
);
process::exit(1);
}

fs::create_dir_all(&nixpath).unwrap_or_else(|e| {
eprintln!("failed to create {}: {e}", nixpath.display());
process::exit(1);
});

let installer = install::fetch_installer().unwrap_or_else(|e| {
eprintln!("failed to fetch nix-installer: {e}");
process::exit(1);
});
let (cmd, cmd_args) = install::installer_command(&installer);
(nixpath, cmd, cmd_args, true, true)
} else {
if args.len() < 3 {
eprintln!("Usage: {} <nixpath> <command>", args[0]);
eprintln!(" {} --install [nixpath]", args[0]);
process::exit(1);
}
(
PathBuf::from(&args[1]),
args[2].clone(),
args[3..].to_vec(),
false,
false,
)
};

let rootdir = mkdtemp::mkdtemp("nix-chroot.XXXXXX")
.unwrap_or_else(|err| panic!("failed to create temporary directory: {err}"));

let nixdir = fs::canonicalize(&args[1])
.unwrap_or_else(|err| panic!("failed to resolve nix directory {}: {}", &args[1], err));
let nixdir = fs::canonicalize(&nixpath_arg).unwrap_or_else(|err| {
panic!(
"failed to resolve nix directory {}: {}",
nixpath_arg.display(),
err
)
});

let path_config_file_path = nixdir.join("etc/nix-user-chroot/path-config.toml");
let path_config: Option<PathConfig> = if path_config_file_path.exists() {
Expand Down Expand Up @@ -662,12 +727,19 @@ fn main() {
}

match unsafe { fork() } {
Ok(ForkResult::Parent { child, .. }) => wait_for_child(&rootdir, child),
Ok(ForkResult::Parent { child, .. }) => {
let status = wait_for_child(&rootdir, child);
if installing && status == 0 {
install::print_next_steps(&nixpath_arg);
}
process::exit(status);
}
Ok(ForkResult::Child) => {
RunChroot::new(&rootdir, &nixdir).run_chroot(&args[2], &args[3..], path_config)
RunChroot::new(&rootdir, &nixdir).run_chroot(&cmd, &cmd_args, path_config, map_root)
}
Err(e) => {
eprintln!("fork failed: {e}");
process::exit(1);
}
};
}
Expand Down