diff --git a/Cargo.lock b/Cargo.lock index b294e8a..6a37665 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -382,7 +382,7 @@ checksum = "807800ff3288b621186fe0a8f3392c4652068257302709c24efd918c3dffcdc2" [[package]] name = "depot" -version = "0.11.0" +version = "0.11.1" dependencies = [ "anyhow", "ar", diff --git a/Cargo.toml b/Cargo.toml index 6eabbd5..fc64f1c 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "depot" -version = "0.11.0" +version = "0.11.1" edition = "2024" [lints.rust] diff --git a/src/commands.rs b/src/commands.rs index 4cffa4c..548da69 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -199,6 +199,7 @@ fn load_package_archive_into_staging( let zstd_decoder = zstd::stream::read::Decoder::new(file) .with_context(|| format!("Failed to read zstd stream {}", archive_path.display()))?; let mut archive = tar::Archive::new(zstd_decoder); + archive.set_preserve_permissions(true); archive.unpack(&extract_dir).with_context(|| { format!( "Failed to extract package archive {} into {}", @@ -289,6 +290,7 @@ fn extract_package_archive_to_staging( let zstd_decoder = zstd::stream::read::Decoder::new(file) .with_context(|| format!("Failed to read zstd stream {}", archive_path.display()))?; let mut archive = tar::Archive::new(zstd_decoder); + archive.set_preserve_permissions(true); archive.unpack(&extract_dir).with_context(|| { format!( "Failed to extract package archive {} into {}", @@ -2891,6 +2893,74 @@ mod tests { Ok(()) } + #[test] + #[cfg(unix)] + fn binary_archive_install_preserves_setuid_permissions() -> Result<()> { + use std::os::unix::fs::PermissionsExt; + + let rootfs = tempfile::tempdir().context("Failed to create temp rootfs")?; + let pkg_dir = tempfile::tempdir().context("Failed to create temp package dir")?; + let archive_path = pkg_dir.path().join("sudo-1.0-1-x86_64.depot.pkg.tar.zst"); + + let file = fs::File::create(&archive_path) + .with_context(|| format!("Failed to create {}", archive_path.display()))?; + let encoder = + zstd::stream::write::Encoder::new(file, 3).context("Failed to create zstd encoder")?; + let mut tar = tar::Builder::new(encoder); + let payload = b"sudo"; + let mut header = tar::Header::new_gnu(); + header.set_path("bin/sudo").unwrap(); + header.set_size(payload.len() as u64); + header.set_mode(0o4755); + header.set_cksum(); + tar.append(&header, &payload[..]).unwrap(); + let encoder = tar.into_inner().unwrap(); + encoder.finish().unwrap(); + + let mut cfg = config::Config::for_rootfs(rootfs.path()); + cfg.build_dir = rootfs.path().join("var/cache/depot/build"); + cfg.db_dir = rootfs.path().join("var/lib/depot"); + + let staged = extract_package_archive_to_staging(&cfg, &archive_path)?; + let staged_mode = fs::metadata(staged.path().join("bin/sudo"))? + .permissions() + .mode() + & 0o7777; + assert_eq!(staged_mode, 0o4755); + + let record = db::repo::BinaryRepoPackageRecord { + repo_name: "core".into(), + name: "sudo".into(), + version: "1.0".into(), + revision: 1, + filename: archive_path + .file_name() + .and_then(|f| f.to_str()) + .unwrap_or_default() + .to_string(), + size: payload.len() as u64, + sha256: String::new(), + sha512: String::new(), + description: Some("sudo".into()), + homepage: Some("https://example.test".into()), + license: Some("ISC".into()), + provides: Vec::new(), + runtime_dependencies: Vec::new(), + optional_dependencies: Vec::new(), + }; + let spec = package_spec_from_repo_record(&record); + let installed = + install_package_outputs_to_rootfs(&spec, staged.path(), rootfs.path(), &cfg)?; + + assert_eq!(installed.len(), 1); + let root_mode = fs::metadata(rootfs.path().join("bin/sudo"))? + .permissions() + .mode() + & 0o7777; + assert_eq!(root_mode, 0o4755); + Ok(()) + } + #[test] fn merge_missing_dependencies_preserves_order_and_uniqueness() { let merged = merge_missing_dependencies( diff --git a/src/staging/mod.rs b/src/staging/mod.rs index adfd17f..977b0b1 100755 --- a/src/staging/mod.rs +++ b/src/staging/mod.rs @@ -214,6 +214,21 @@ fn append_os_suffix(path: &Path, suffix: &str) -> PathBuf { PathBuf::from(s) } +#[cfg(unix)] +fn apply_unix_mode(dst: &Path, metadata: &fs::Metadata) -> Result<()> { + use std::os::unix::fs::PermissionsExt; + + let mode = metadata.permissions().mode() & 0o7777; + fs::set_permissions(dst, fs::Permissions::from_mode(mode)) + .with_context(|| format!("Failed to set permissions on {}", dst.display()))?; + Ok(()) +} + +#[cfg(not(unix))] +fn apply_unix_mode(_dst: &Path, _metadata: &fs::Metadata) -> Result<()> { + Ok(()) +} + fn is_elf_file(path: &Path) -> Result { let mut file = fs::File::open(path).with_context(|| format!("Failed to open {}", path.display()))?; @@ -647,6 +662,7 @@ pub fn install_atomic( let dest_path = rootfs.join(rel_path); if !dest_path.exists() { fs::create_dir_all(&dest_path)?; + apply_unix_mode(&dest_path, &src_path.symlink_metadata()?)?; } } @@ -730,6 +746,7 @@ pub fn install_atomic( } else { fs::copy(src_path, &dest_path) .with_context(|| format!("Failed to install: {}", install_rel_path))?; + apply_unix_mode(&dest_path, &metadata)?; } }