diff --git a/Cargo.lock b/Cargo.lock index afc8dcb..7a0c902 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -481,7 +481,7 @@ checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2" [[package]] name = "depot" -version = "1.0.1" +version = "1.0.2" dependencies = [ "anyhow", "ar", diff --git a/Cargo.toml b/Cargo.toml index b1b088f..e44ea01 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "depot" -version = "1.0.1" +version = "1.0.2" edition = "2024" [lints.rust] diff --git a/build.rs b/build.rs index ea7fa38..b1b82da 100644 --- a/build.rs +++ b/build.rs @@ -12,6 +12,7 @@ const BUILD_OPTION_KEYS: &[&str] = &[ fn main() { println!("cargo:rerun-if-env-changed=CC"); + println!("cargo:rerun-if-changed=src/fakeroot_preload.c"); for key in BUILD_OPTION_KEYS { println!("cargo:rerun-if-env-changed={key}"); if let Ok(value) = std::env::var(key) { @@ -19,6 +20,10 @@ fn main() { } } + if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("linux") { + build_fakeroot_preload(); + } + if std::env::var_os("CARGO_FEATURE_STATIC_EXCEPT_LIBC").is_some() && std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("linux") { @@ -32,6 +37,31 @@ fn main() { } } +fn build_fakeroot_preload() { + let compiler = std::env::var_os("CC").unwrap_or_else(|| "cc".into()); + let output_path = std::path::PathBuf::from( + std::env::var_os("OUT_DIR").expect("Cargo must provide OUT_DIR to build.rs"), + ) + .join("libdepot_fakeroot.so"); + let output = std::process::Command::new(compiler) + .args([ + "-std=c11", "-shared", "-fPIC", "-O2", "-Wall", "-Wextra", "-Werror", "-o", + ]) + .arg(&output_path) + .arg("src/fakeroot_preload.c") + .output() + .unwrap_or_else(|error| { + panic!("failed to launch C compiler for fakeroot preload: {error}") + }); + + if !output.status.success() { + panic!( + "failed to build fakeroot preload library:\n{}", + String::from_utf8_lossy(&output.stderr) + ); + } +} + fn gcc_runtime_archive(name: &str) -> Option { let compiler = std::env::var_os("CC").unwrap_or_else(|| "cc".into()); let output = std::process::Command::new(compiler) diff --git a/src/builder/autotools.rs b/src/builder/autotools.rs index bdf17e1..44690e2 100644 --- a/src/builder/autotools.rs +++ b/src/builder/autotools.rs @@ -339,7 +339,7 @@ pub fn build( Some("install"), ); for install_dir in install_dirs { - let mut install_cmd = fakeroot::wrap_install_command(make_exec, &install_destdir); + let mut install_cmd = fakeroot::wrap_install_command(make_exec, &install_destdir)?; install_cmd.current_dir(&install_dir); if make_exec_supports_make_assignments(make_exec) && !has_make_variable_override(&flags.make_install_vars, "DESTDIR") @@ -652,7 +652,7 @@ pub(crate) fn run_helper_install( ); for install_dir in install_dirs { - let mut install_cmd = fakeroot::wrap_install_command(make_exec, Path::new(&destdir)); + let mut install_cmd = fakeroot::wrap_install_command(make_exec, Path::new(&destdir))?; install_cmd.current_dir(&install_dir); if make_exec_supports_make_assignments(make_exec) && !has_make_variable_override(&flags.make_install_vars, "DESTDIR") diff --git a/src/builder/cmake.rs b/src/builder/cmake.rs index d103d5a..be45a89 100644 --- a/src/builder/cmake.rs +++ b/src/builder/cmake.rs @@ -227,7 +227,7 @@ pub fn build( })?; } - let mut install_cmd = fakeroot::wrap_install_command("cmake", &install_destdir); + let mut install_cmd = fakeroot::wrap_install_command("cmake", &install_destdir)?; if !install_targets.is_empty() { install_cmd.arg("--build").arg(&build_dir); install_cmd.arg("--target"); @@ -466,7 +466,7 @@ pub(crate) fn run_helper_install( let destdir = std::env::var("DESTDIR").context("DESTDIR must be set for cmake_install")?; let install_targets = phase_targets(&flags.make_install_target, &flags.make_install_targets); - let mut install_cmd = fakeroot::wrap_install_command("cmake", Path::new(&destdir)); + let mut install_cmd = fakeroot::wrap_install_command("cmake", Path::new(&destdir))?; if install_targets.is_empty() { install_cmd.arg("--install").arg(&build_dir); } else { diff --git a/src/builder/custom.rs b/src/builder/custom.rs index 788a251..f82a429 100755 --- a/src/builder/custom.rs +++ b/src/builder/custom.rs @@ -156,7 +156,7 @@ pub fn build( } ); // Use POSIX `sh` (doing something wrong if your system doesn't have it...) - let mut cmd = fakeroot::wrap_install_command("sh", &install_destdir); + let mut cmd = fakeroot::wrap_install_command("sh", &install_destdir)?; let wrapper = crate::shell_helpers::wrap_shell_command(". \"$1\""); // Run custom scripts through `sh -c` so helper commands like `haul` // work even when the helper scripts live on a `noexec` mount. @@ -257,7 +257,7 @@ fn build_function_mode_install_command( destdir: &Path, build_script: &Path, build_ran: bool, -) -> Command { +) -> Result { let mut wrapper = function_mode_shell_prelude(); wrapper.push_str(&format!( "depot_build_ran={}\n", @@ -311,9 +311,9 @@ fn build_function_mode_install_command( wrapper.push_str("fi\n"); } - let mut cmd = fakeroot::wrap_install_command("sh", destdir); + let mut cmd = fakeroot::wrap_install_command("sh", destdir)?; cmd.arg("-c").arg(wrapper).arg("sh").arg(build_script); - cmd + Ok(cmd) } fn run_function_mode_build_script( @@ -357,7 +357,7 @@ fn run_function_mode_build_script( .unwrap_or(false); let mut install_cmd = - build_function_mode_install_command(spec, install_destdir, build_script, build_ran); + build_function_mode_install_command(spec, install_destdir, build_script, build_ran)?; install_cmd.current_dir(build_dir); crate::builder::prepare_tool_command(&mut install_cmd, env_vars); let install_status = crate::interrupts::command_status(&mut install_cmd).map_err(|e| { @@ -613,18 +613,20 @@ depot_install() { } #[test] - fn test_build_function_mode_commands_split_fakeroot_boundary() { + fn test_build_function_mode_commands_split_fakeroot_boundary() -> Result<()> { + let tmp_dest = tempdir()?; let build_script = Path::new("/tmp/build.sh"); let state_file = Path::new("/tmp/build-state"); - let install_destdir = Path::new("/tmp/destdir"); + let install_destdir = tmp_dest.path(); let build_cmd = build_function_mode_build_command(build_script, state_file); assert_eq!(build_cmd.get_program(), std::ffi::OsStr::new("sh")); let spec = mk_spec("custom-function-fakeroot-split", "1.0"); let install_cmd = - build_function_mode_install_command(&spec, install_destdir, build_script, true); + build_function_mode_install_command(&spec, install_destdir, build_script, true)?; assert_eq!(install_cmd.get_program(), std::ffi::OsStr::new("sh")); + Ok(()) } #[test] diff --git a/src/builder/makefile.rs b/src/builder/makefile.rs index fc000f5..165cbdb 100644 --- a/src/builder/makefile.rs +++ b/src/builder/makefile.rs @@ -100,7 +100,7 @@ pub fn build( crate::log_info!(" Executing: {}", cmd_str); // We need to run each command under internal fakeroot - let mut cmd = crate::fakeroot::wrap_install_command("sh", &install_destdir); + let mut cmd = crate::fakeroot::wrap_install_command("sh", &install_destdir)?; cmd.arg("-c").arg(&cmd_str); cmd.current_dir(src_dir); diff --git a/src/builder/meson.rs b/src/builder/meson.rs index 29dacf0..37f1f6b 100644 --- a/src/builder/meson.rs +++ b/src/builder/meson.rs @@ -170,7 +170,7 @@ pub fn build( })?; } - let mut install_cmd = fakeroot::wrap_install_command("meson", &install_destdir); + let mut install_cmd = fakeroot::wrap_install_command("meson", &install_destdir)?; install_cmd.arg("install"); install_cmd.arg("-C").arg(&build_dir); @@ -336,7 +336,7 @@ pub(crate) fn run_helper_install( .unwrap_or_else(|| resolve_build_dir(&helper_source_dir(), &flags)); let destdir = std::env::var("DESTDIR").context("DESTDIR must be set for meson_install")?; - let mut install_cmd = fakeroot::wrap_install_command("meson", Path::new(&destdir)); + let mut install_cmd = fakeroot::wrap_install_command("meson", Path::new(&destdir))?; install_cmd.arg("install"); install_cmd.arg("-C").arg(&build_dir); for arg in extra_args { diff --git a/src/builder/perl.rs b/src/builder/perl.rs index 2b5ddd6..e094a8e 100644 --- a/src/builder/perl.rs +++ b/src/builder/perl.rs @@ -212,7 +212,7 @@ pub fn build( " (with internal fakeroot for build)" } ); - let mut install_cmd = fakeroot::wrap_install_command(make_exec, destdir); + let mut install_cmd = fakeroot::wrap_install_command(make_exec, destdir)?; install_cmd.current_dir(&install_dir); if autotools::make_exec_supports_make_assignments(make_exec) && !autotools::has_make_variable_override(&flags.make_install_vars, "DESTDIR") @@ -343,7 +343,7 @@ pub(crate) fn run_helper_install( ); for install_dir in install_dirs { - let mut install_cmd = fakeroot::wrap_install_command(make_exec, Path::new(&destdir)); + let mut install_cmd = fakeroot::wrap_install_command(make_exec, Path::new(&destdir))?; install_cmd.current_dir(&install_dir); if autotools::make_exec_supports_make_assignments(make_exec) && !autotools::has_make_variable_override(&flags.make_install_vars, "DESTDIR") diff --git a/src/fakeroot.rs b/src/fakeroot.rs index 1f01dea..dc89d38 100755 --- a/src/fakeroot.rs +++ b/src/fakeroot.rs @@ -1,14 +1,24 @@ //! Rootless install command support backed by Linux user namespaces. +use anyhow::{Context, Result}; +use std::ffi::{CString, OsString}; use std::fs; use std::io; +use std::os::unix::ffi::OsStrExt; +use std::os::unix::fs::PermissionsExt; use std::os::unix::process::CommandExt; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::process::Command; +use std::sync::OnceLock; const UID_MAP_PATH: &[u8] = b"/proc/self/uid_map\0"; const GID_MAP_PATH: &[u8] = b"/proc/self/gid_map\0"; const SETGROUPS_PATH: &[u8] = b"/proc/self/setgroups\0"; +const UID_XATTR: &[u8] = b"user.depot.fakeroot.uid\0"; +const GID_XATTR: &[u8] = b"user.depot.fakeroot.gid\0"; +const PRELOAD_LIBRARY_BYTES: &[u8] = + include_bytes!(concat!(env!("OUT_DIR"), "/libdepot_fakeroot.so")); +static PRELOAD_LIBRARY_DIR: OnceLock = OnceLock::new(); /// Check if the current process is running as root. pub fn is_root() -> bool { @@ -20,15 +30,133 @@ pub fn is_root() -> bool { /// The namespace maps UID/GID 0 to the invoking user, allowing install scripts /// to perform root-owned staged installs without changing host ownership or /// requiring an external fakeroot implementation. -pub fn wrap_install_command(program: &str, destdir: &Path) -> Command { +pub fn wrap_install_command(program: &str, destdir: &Path) -> Result { let mut command = build_command(program, shell_script_path(program)); command.env("DESTDIR", destdir); if !is_root() { + configure_ownership_preload(&mut command)?; configure_rootless_namespace(&mut command); } - command + Ok(command) +} + +fn configure_ownership_preload(command: &mut Command) -> Result<()> { + let library_path = materialize_preload_library()?; + let mut preload = OsString::from(&library_path); + if let Some(existing) = std::env::var_os("LD_PRELOAD").filter(|value| !value.is_empty()) { + preload.push(":"); + preload.push(existing); + } + command.env("LD_PRELOAD", preload); + Ok(()) +} + +fn materialize_preload_library() -> Result { + if let Some(dir) = PRELOAD_LIBRARY_DIR.get() { + return Ok(dir.path().join("libdepot_fakeroot.so")); + } + + let candidate = tempfile::Builder::new() + .prefix("depot-fakeroot-") + .tempdir() + .context("Failed to create private fakeroot library dir")?; + let candidate_path = candidate.path().join("libdepot_fakeroot.so"); + fs::write(&candidate_path, PRELOAD_LIBRARY_BYTES).with_context(|| { + format!( + "Failed to write fakeroot library {}", + candidate_path.display() + ) + })?; + fs::set_permissions(&candidate_path, fs::Permissions::from_mode(0o500)).with_context(|| { + format!( + "Failed to set permissions on fakeroot library {}", + candidate_path.display() + ) + })?; + let candidate_path = candidate_path.canonicalize().with_context(|| { + format!( + "Failed to resolve fakeroot library {}", + candidate_path.display() + ) + })?; + + if PRELOAD_LIBRARY_DIR.set(candidate).is_ok() { + Ok(candidate_path) + } else { + Ok(PRELOAD_LIBRARY_DIR + .get() + .expect("fakeroot library dir must be initialized") + .path() + .join("libdepot_fakeroot.so")) + } +} + +pub(crate) fn archive_ownership( + path: &Path, + metadata: &fs::Metadata, + symlink: bool, +) -> Result<(u64, u64)> { + use std::os::unix::fs::MetadataExt; + + let (default_uid, default_gid) = if is_root() { + (u64::from(metadata.uid()), u64::from(metadata.gid())) + } else { + (0, 0) + }; + let uid = read_id_xattr(path, UID_XATTR, symlink)?.unwrap_or(default_uid); + let gid = read_id_xattr(path, GID_XATTR, symlink)?.unwrap_or(default_gid); + Ok((uid, gid)) +} + +fn read_id_xattr(path: &Path, name: &[u8], symlink: bool) -> Result> { + let path = CString::new(path.as_os_str().as_bytes()) + .with_context(|| format!("Path contains an embedded NUL: {}", path.display()))?; + let mut value = [0_u8; 16]; + let length = unsafe { + if symlink { + nix::libc::lgetxattr( + path.as_ptr(), + name.as_ptr().cast(), + value.as_mut_ptr().cast(), + value.len(), + ) + } else { + nix::libc::getxattr( + path.as_ptr(), + name.as_ptr().cast(), + value.as_mut_ptr().cast(), + value.len(), + ) + } + }; + if length < 0 { + let error = io::Error::last_os_error(); + return match error.raw_os_error() { + Some(code) if code == nix::libc::ENODATA || code == nix::libc::ENOTSUP => Ok(None), + _ => Err(error).with_context(|| { + format!( + "Failed to read fakeroot ownership from {}", + path.to_string_lossy() + ) + }), + }; + } + + let value = std::str::from_utf8(&value[..length as usize]).with_context(|| { + format!( + "Invalid fakeroot ownership metadata on {}", + path.to_string_lossy() + ) + })?; + let id = value.parse::().with_context(|| { + format!( + "Invalid fakeroot ownership value {value:?} on {}", + path.to_string_lossy() + ) + })?; + Ok(Some(id)) } fn build_command(program: &str, script_path: Option<&Path>) -> Command { @@ -126,11 +254,14 @@ mod tests { #[test] fn rootless_command_runs_as_namespace_root() -> Result<()> { + if is_root() { + return Ok(()); + } let destdir = tempfile::tempdir()?; - let mut command = wrap_install_command("/bin/sh", destdir.path()); + let mut command = wrap_install_command("/bin/sh", destdir.path())?; command .arg("-c") - .arg("test \"$(/usr/bin/id -u)\" = 0; test \"$(/usr/bin/id -g)\" = 0; /usr/bin/touch \"$DESTDIR/root-owned\"; /usr/bin/chown 0:0 \"$DESTDIR/root-owned\"; /usr/bin/chmod 4755 \"$DESTDIR/root-owned\""); + .arg("test \"$(/usr/bin/id -u)\" = 0; test \"$(/usr/bin/id -g)\" = 0; /usr/bin/touch \"$DESTDIR/root-owned\"; if test -x /usr/bin/python3; then /usr/bin/python3 -c 'import os; os.chown(os.environ[\"DESTDIR\"] + \"/root-owned\", 0, 81)'; else /usr/bin/chown 0:81 \"$DESTDIR/root-owned\"; fi; /usr/bin/chmod 4755 \"$DESTDIR/root-owned\""); let status = command .status() @@ -141,16 +272,22 @@ mod tests { assert_eq!(metadata.uid(), nix::unistd::geteuid().as_raw()); assert_eq!(metadata.gid(), nix::unistd::getegid().as_raw()); assert_eq!(metadata.mode() & 0o7777, 0o4755); + assert_eq!( + archive_ownership(&destdir.path().join("root-owned"), &metadata, false)?, + (0, 81) + ); Ok(()) } #[test] - fn wrapped_command_preserves_program_and_sets_destdir() { - let command = wrap_install_command("make", Path::new("/tmp/package-root")); + fn wrapped_command_preserves_program_and_sets_destdir() -> Result<()> { + let destdir = tempfile::tempdir()?; + let command = wrap_install_command("make", destdir.path())?; assert_eq!(command.get_program(), "make"); assert!(command.get_envs().any(|(key, value)| { - key == "DESTDIR" && value.is_some_and(|value| value == "/tmp/package-root") + key == "DESTDIR" && value.is_some_and(|value| value == destdir.path()) })); + Ok(()) } } diff --git a/src/fakeroot_preload.c b/src/fakeroot_preload.c new file mode 100644 index 0000000..773126b --- /dev/null +++ b/src/fakeroot_preload.c @@ -0,0 +1,144 @@ +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include + +#define DEPOT_UID_XATTR "user.depot.fakeroot.uid" +#define DEPOT_GID_XATTR "user.depot.fakeroot.gid" + +static uid_t mapped_uid(uid_t uid) { + return uid == (uid_t)-1 ? uid : 0; +} + +static gid_t mapped_gid(gid_t gid) { + return gid == (gid_t)-1 ? gid : 0; +} + +static int missing_xattr_error(int error) { + return error == ENODATA || error == ENOTSUP; +} + +static int update_id_path(const char *path, const char *name, unsigned int id, + int nofollow) { + char value[16]; + int length = snprintf(value, sizeof(value), "%u", id); + if (length < 0 || (size_t)length >= sizeof(value)) { + errno = EOVERFLOW; + return -1; + } + + if (id == 0) { + int result = nofollow != 0 ? lremovexattr(path, name) + : removexattr(path, name); + if (result == 0 || missing_xattr_error(errno) || + (nofollow != 0 && errno == EPERM)) { + return 0; + } + return -1; + } + + if (nofollow != 0) { + return lsetxattr(path, name, value, (size_t)length, 0); + } + return setxattr(path, name, value, (size_t)length, 0); +} + +static int update_id_fd(int fd, const char *name, unsigned int id) { + char value[16]; + int length = snprintf(value, sizeof(value), "%u", id); + if (length < 0 || (size_t)length >= sizeof(value)) { + errno = EOVERFLOW; + return -1; + } + + if (id == 0) { + int result = fremovexattr(fd, name); + if (result == 0 || missing_xattr_error(errno)) { + return 0; + } + return -1; + } + return fsetxattr(fd, name, value, (size_t)length, 0); +} + +static int record_path(const char *path, uid_t uid, gid_t gid, int nofollow) { + if (uid != (uid_t)-1 && + update_id_path(path, DEPOT_UID_XATTR, (unsigned int)uid, nofollow) != + 0) { + return -1; + } + if (gid != (gid_t)-1 && + update_id_path(path, DEPOT_GID_XATTR, (unsigned int)gid, nofollow) != + 0) { + return -1; + } + return 0; +} + +static int record_fd(int fd, uid_t uid, gid_t gid) { + if (uid != (uid_t)-1 && + update_id_fd(fd, DEPOT_UID_XATTR, (unsigned int)uid) != 0) { + return -1; + } + if (gid != (gid_t)-1 && + update_id_fd(fd, DEPOT_GID_XATTR, (unsigned int)gid) != 0) { + return -1; + } + return 0; +} + +int chown(const char *path, uid_t uid, gid_t gid) { + if (syscall(SYS_chown, path, mapped_uid(uid), mapped_gid(gid)) != 0) { + return -1; + } + return record_path(path, uid, gid, 0); +} + +int lchown(const char *path, uid_t uid, gid_t gid) { + if (syscall(SYS_lchown, path, mapped_uid(uid), mapped_gid(gid)) != 0) { + return -1; + } + return record_path(path, uid, gid, 1); +} + +int fchown(int fd, uid_t uid, gid_t gid) { + if (syscall(SYS_fchown, fd, mapped_uid(uid), mapped_gid(gid)) != 0) { + return -1; + } + return record_fd(fd, uid, gid); +} + +int fchownat(int dirfd, const char *path, uid_t uid, gid_t gid, int flags) { + int fd; + int open_flags = O_RDONLY | O_CLOEXEC | O_NONBLOCK; + + if (syscall(SYS_fchownat, dirfd, path, mapped_uid(uid), mapped_gid(gid), + flags) != 0) { + return -1; + } + + if ((flags & AT_SYMLINK_NOFOLLOW) != 0) { + if (dirfd == AT_FDCWD || path[0] == '/') { + return record_path(path, uid, gid, 1); + } + errno = ENOTSUP; + return -1; + } + + fd = openat(dirfd, path, open_flags); + if (fd < 0) { + return -1; + } + if (record_fd(fd, uid, gid) != 0) { + int saved_errno = errno; + close(fd); + errno = saved_errno; + return -1; + } + return close(fd); +} diff --git a/src/package/packager.rs b/src/package/packager.rs index 3ff21b6..1d3cc54 100644 --- a/src/package/packager.rs +++ b/src/package/packager.rs @@ -8,7 +8,7 @@ use std::collections::HashMap; use std::fs; use std::os::unix::fs::MetadataExt; use std::path::{Path, PathBuf}; -use tar::{Builder, EntryType}; +use tar::{Builder, EntryType, Header, HeaderMode}; use zstd::stream::write::Encoder; type HardlinkKey = (u64, u64); @@ -42,6 +42,20 @@ fn license_value(licenses: &[String]) -> toml::Value { } } +fn package_header( + path: &Path, + metadata: &fs::Metadata, + symlink: bool, + mode: HeaderMode, +) -> Result
{ + let mut header = Header::new_gnu(); + header.set_metadata_in_mode(metadata, mode); + let (uid, gid) = crate::fakeroot::archive_ownership(path, metadata, symlink)?; + header.set_uid(uid); + header.set_gid(gid); + Ok(header) +} + pub struct Packager { pub spec: PackageSpec, pub destdir: PathBuf, @@ -109,15 +123,16 @@ impl Packager { let file_type = entry.file_type(); if file_type.is_dir() { - tar.append_dir(rel_path, path)?; + let metadata = fs::symlink_metadata(path)?; + let mut header = package_header(path, &metadata, false, HeaderMode::Complete)?; + header.set_size(0); + tar.append_data(&mut header, rel_path, std::io::empty())?; } else if file_type.is_symlink() { // For symlinks, we need to read the link and append to tar correctly let target = fs::read_link(path)?; - let mut header = tar::Header::new_gnu(); - header.set_metadata_in_mode( - &fs::symlink_metadata(path)?, - tar::HeaderMode::Deterministic, - ); + let metadata = fs::symlink_metadata(path)?; + let mut header = package_header(path, &metadata, true, HeaderMode::Deterministic)?; + header.set_size(0); tar.append_link(&mut header, rel_path, target)?; } else { // Files @@ -126,14 +141,15 @@ impl Packager { if let Some(first_rel_path) = hardlink_key.and_then(|key| archived_hardlinks.get(&key)) { - let mut header = tar::Header::new_gnu(); - header.set_metadata_in_mode(&metadata, tar::HeaderMode::Deterministic); + let mut header = + package_header(path, &metadata, false, HeaderMode::Deterministic)?; header.set_entry_type(EntryType::Link); header.set_size(0); tar.append_link(&mut header, rel_path, first_rel_path)?; } else { let mut file = fs::File::open(path)?; - tar.append_file(rel_path, &mut file)?; + let mut header = package_header(path, &metadata, false, HeaderMode::Complete)?; + tar.append_data(&mut header, rel_path, &mut file)?; if let Some(key) = hardlink_key { archived_hardlinks.insert(key, rel_path.to_path_buf()); } @@ -683,4 +699,41 @@ mod tests { assert_eq!(coreutils_meta.nlink(), 2); assert_eq!(ls_meta.nlink(), 2); } + + #[test] + fn test_create_package_preserves_rootless_fakeroot_ownership() -> Result<()> { + let tmp = tempfile::tempdir()?; + let dest = tmp.path().join("dest"); + let out = tmp.path().join("out"); + fs::create_dir_all(dest.join("usr/lib"))?; + fs::create_dir_all(&out)?; + + let mut command = crate::fakeroot::wrap_install_command("/bin/sh", &dest)?; + command.arg("-c").arg( + "/usr/bin/touch \"$DESTDIR/usr/lib/dbus-daemon-launch-helper\"; \ + /usr/bin/chown 0:81 \"$DESTDIR/usr/lib/dbus-daemon-launch-helper\"; \ + /usr/bin/chmod 4750 \"$DESTDIR/usr/lib/dbus-daemon-launch-helper\"", + ); + let status = command.status()?; + assert!(status.success()); + + let packager = mk_packager(dest); + let archive_path = packager.create_package(&out, "x86_64")?; + let archive_file = fs::File::open(archive_path)?; + let decoder = zstd::Decoder::new(archive_file)?; + let mut archive = tar::Archive::new(decoder); + let mut found = false; + for entry in archive.entries()? { + let entry = entry?; + if entry.path()?.as_ref() == Path::new("usr/lib/dbus-daemon-launch-helper") { + assert_eq!(entry.header().uid()?, 0); + assert_eq!(entry.header().gid()?, 81); + assert_eq!(entry.header().mode()? & 0o7777, 0o4750); + found = true; + break; + } + } + assert!(found); + Ok(()) + } }