-
Notifications
You must be signed in to change notification settings - Fork 114
Cross-platform packager: OCI symlink resolution, rewrite-include, rtld_audit removal #741
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: wdcui/stacked/pr1b-trampoline-format
Are you sure you want to change the base?
Changes from all commits
4aaa78a
dd357a8
5fbdeab
9ebf54f
ca194cb
74b27eb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,18 +1,15 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT license. | ||
|
|
||
| // Restrict this crate to only work on Linux, as it relies on `ldd` for | ||
| // dependency discovery and other Linux-specific functionality. | ||
| #![cfg(target_os = "linux")] | ||
|
|
||
| #[cfg(target_arch = "x86_64")] | ||
| pub mod oci; | ||
|
|
||
| use anyhow::{Context, bail}; | ||
| use clap::Parser; | ||
| use rayon::prelude::*; | ||
| use std::collections::{BTreeMap, BTreeSet}; | ||
| use std::os::unix::fs::MetadataExt as _; | ||
| #[cfg(target_os = "linux")] | ||
| use std::collections::BTreeMap; | ||
| use std::collections::BTreeSet; | ||
| use std::path::{Path, PathBuf}; | ||
| use tar::{Builder, Header}; | ||
|
|
||
|
|
@@ -48,10 +45,16 @@ pub struct CliArgs { | |
| #[arg(short = 'o', long = "output", default_value = "litebox_packager.tar")] | ||
| pub output: PathBuf, | ||
|
|
||
| /// Include extra files in the tar. | ||
| /// Include extra files in the tar (host mode only). | ||
| /// ELF files are automatically run through the syscall rewriter; non-ELF | ||
| /// files are included as-is. | ||
| /// Format: HOST_PATH:TAR_PATH (split on the first colon, so the tar path | ||
| /// may contain colons but the host path must not). | ||
| #[arg(long = "include", value_name = "HOST_PATH:TAR_PATH")] | ||
| #[arg( | ||
| long = "include", | ||
| value_name = "HOST_PATH:TAR_PATH", | ||
| conflicts_with = "oci_image" | ||
| )] | ||
| pub include: Vec<String>, | ||
|
|
||
| /// Skip rewriting specific files (by their absolute path on the host). | ||
|
|
@@ -64,11 +67,13 @@ pub struct CliArgs { | |
| } | ||
|
|
||
| /// Parsed `--include` entry. | ||
| #[cfg(target_os = "linux")] | ||
| struct IncludeEntry { | ||
| host_path: PathBuf, | ||
| tar_path: String, | ||
| } | ||
|
|
||
| #[cfg(target_os = "linux")] | ||
| fn parse_include(spec: &str) -> anyhow::Result<IncludeEntry> { | ||
| let Some(colon_idx) = spec.find(':') else { | ||
| bail!("invalid --include format: expected HOST_PATH:TAR_PATH, got: {spec}"); | ||
|
|
@@ -99,7 +104,24 @@ pub fn run(args: CliArgs) -> anyhow::Result<()> { | |
| } | ||
| } | ||
|
|
||
| // --- Phase 1: Validate inputs --- | ||
| // Host mode (local ELF files + ldd dependency discovery) is Linux-only. | ||
| #[cfg(target_os = "linux")] | ||
| { | ||
| run_host_mode(args) | ||
| } | ||
|
|
||
| #[cfg(not(target_os = "linux"))] | ||
| { | ||
| bail!( | ||
| "Host mode (local ELF files) is only supported on Linux. \ | ||
| Use --oci-image to pull a container image instead." | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| /// Host mode: package local ELF files with ldd-based dependency discovery. | ||
| #[cfg(target_os = "linux")] | ||
| fn run_host_mode(args: CliArgs) -> anyhow::Result<()> { | ||
| let input_files: Vec<PathBuf> = args | ||
| .input_files | ||
| .iter() | ||
|
|
@@ -151,12 +173,15 @@ pub fn run(args: CliArgs) -> anyhow::Result<()> { | |
|
|
||
| let par_results: Vec<anyhow::Result<Vec<TarEntry>>> = file_map_vec | ||
| .into_par_iter() | ||
| .map(|(real_path, tar_paths)| { | ||
| .map(|(real_path, tar_paths): (&PathBuf, &Vec<PathBuf>)| { | ||
| let data = std::fs::read(real_path) | ||
| .with_context(|| format!("failed to read {}", real_path.display()))?; | ||
| let mode = std::fs::metadata(real_path) | ||
| .with_context(|| format!("failed to stat {}", real_path.display()))? | ||
| .mode(); | ||
| let mode = { | ||
| use std::os::unix::fs::MetadataExt as _; | ||
| std::fs::metadata(real_path) | ||
| .with_context(|| format!("failed to stat {}", real_path.display()))? | ||
| .mode() | ||
| }; | ||
|
|
||
| let rewritten = if no_rewrite.contains(real_path) { | ||
| if verbose { | ||
|
|
@@ -194,7 +219,47 @@ pub fn run(args: CliArgs) -> anyhow::Result<()> { | |
| } | ||
| } | ||
|
|
||
| finalize_tar(tar_entries, added_tar_paths, &args)?; | ||
| // Append --include files (ELF files are automatically rewritten). | ||
| let includes: Vec<IncludeEntry> = args | ||
| .include | ||
| .iter() | ||
| .map(|s| parse_include(s)) | ||
| .collect::<anyhow::Result<Vec<_>>>()?; | ||
|
|
||
| for inc in &includes { | ||
| if !inc.host_path.exists() { | ||
| bail!("included file does not exist: {}", inc.host_path.display()); | ||
| } | ||
| if !added_tar_paths.insert(inc.tar_path.clone()) { | ||
| bail!( | ||
| "duplicate tar path from --include: '{}' (already present)", | ||
| inc.tar_path | ||
| ); | ||
| } | ||
| let data = std::fs::read(&inc.host_path) | ||
| .with_context(|| format!("failed to read included file {}", inc.host_path.display()))?; | ||
| let mode = { | ||
| use std::os::unix::fs::MetadataExt as _; | ||
| std::fs::metadata(&inc.host_path) | ||
| .map(|m| m.mode()) | ||
| .unwrap_or(0o755) | ||
| }; | ||
| let rewritten = rewrite_elf(&data, &inc.host_path, args.verbose)?; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This ignores |
||
| if args.verbose { | ||
| eprintln!( | ||
| " including {} as {}", | ||
| inc.host_path.display(), | ||
| inc.tar_path | ||
| ); | ||
| } | ||
| tar_entries.push(TarEntry { | ||
| tar_path: inc.tar_path.clone(), | ||
| data: rewritten, | ||
| mode, | ||
| }); | ||
| } | ||
|
|
||
| finalize_tar(tar_entries, &args)?; | ||
|
|
||
| Ok(()) | ||
| } | ||
|
|
@@ -208,7 +273,12 @@ fn run_oci(image_ref: &str, args: &CliArgs) -> anyhow::Result<()> { | |
|
|
||
| // --- Phase 2: Scan rootfs for files --- | ||
| eprintln!("Scanning rootfs..."); | ||
| let file_map = oci::scan_rootfs(&extracted.rootfs_path, args.verbose)?; | ||
| let file_map = oci::scan_rootfs( | ||
| &extracted.rootfs_path, | ||
| &extracted.symlink_map, | ||
| &extracted.permissions, | ||
| args.verbose, | ||
| )?; | ||
|
|
||
| let no_rewrite: BTreeSet<PathBuf> = args | ||
| .no_rewrite | ||
|
|
@@ -303,61 +373,17 @@ fn run_oci(image_ref: &str, args: &CliArgs) -> anyhow::Result<()> { | |
| } | ||
| } | ||
|
|
||
| finalize_tar(tar_entries, added_tar_paths, args)?; | ||
| finalize_tar(tar_entries, args)?; | ||
|
|
||
| Ok(()) | ||
| } | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Shared finalization: includes, rtld audit injection, tar build, size report | ||
| // Shared finalization: tar build, size report | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| /// Append `--include` files, inject the rtld audit library, build the output | ||
| /// tar, and print a size summary. | ||
| /// | ||
| /// Both host mode and OCI mode call this after producing their rewritten | ||
| /// `TarEntry` list. | ||
| fn finalize_tar( | ||
| mut tar_entries: Vec<TarEntry>, | ||
| mut added_tar_paths: BTreeSet<String>, | ||
| args: &CliArgs, | ||
| ) -> anyhow::Result<()> { | ||
| // Parse and append --include files. | ||
| let includes: Vec<IncludeEntry> = args | ||
| .include | ||
| .iter() | ||
| .map(|s| parse_include(s)) | ||
| .collect::<anyhow::Result<Vec<_>>>()?; | ||
|
|
||
| for inc in &includes { | ||
| if !inc.host_path.exists() { | ||
| bail!("included file does not exist: {}", inc.host_path.display()); | ||
| } | ||
| if !added_tar_paths.insert(inc.tar_path.clone()) { | ||
| bail!( | ||
| "duplicate tar path from --include: '{}' (already present)", | ||
| inc.tar_path | ||
| ); | ||
| } | ||
| let data = std::fs::read(&inc.host_path) | ||
| .with_context(|| format!("failed to read included file {}", inc.host_path.display()))?; | ||
| let mode = std::fs::metadata(&inc.host_path) | ||
| .map(|m| m.mode()) | ||
| .unwrap_or(0o644); | ||
| if args.verbose { | ||
| eprintln!( | ||
| " including {} as {}", | ||
| inc.host_path.display(), | ||
| inc.tar_path | ||
| ); | ||
| } | ||
| tar_entries.push(TarEntry { | ||
| tar_path: inc.tar_path.clone(), | ||
| data, | ||
| mode, | ||
| }); | ||
| } | ||
|
|
||
| /// Build the output tar and print a size summary. | ||
| fn finalize_tar(tar_entries: Vec<TarEntry>, args: &CliArgs) -> anyhow::Result<()> { | ||
| // Build tar. | ||
| eprintln!("Creating {}...", args.output.display()); | ||
| build_tar(&tar_entries, &args.output)?; | ||
|
|
@@ -377,21 +403,20 @@ fn finalize_tar( | |
| Ok(()) | ||
| } | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Dependency discovery (via ldd) | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| #[cfg(target_os = "linux")] | ||
| struct ResolvedDep { | ||
| ldd_path: PathBuf, | ||
| real_path: PathBuf, | ||
| } | ||
|
|
||
| #[cfg(target_os = "linux")] | ||
| struct DepDiscoveryResult { | ||
| resolved: Vec<ResolvedDep>, | ||
| missing: Vec<String>, | ||
| } | ||
|
|
||
| /// Run `ldd` on the given ELF and return resolved dependencies. | ||
| #[cfg(target_os = "linux")] | ||
| fn find_dependencies(elf_path: &Path, verbose: bool) -> anyhow::Result<DepDiscoveryResult> { | ||
| let output = std::process::Command::new("ldd") | ||
| .arg(elf_path) | ||
|
|
@@ -483,6 +508,7 @@ fn find_dependencies(elf_path: &Path, verbose: bool) -> anyhow::Result<DepDiscov | |
| /// appear in the tar. This includes the input files themselves and all their | ||
| /// transitive shared-library dependencies. Deduplicates by canonical path so each | ||
| /// file is only read and rewritten once. | ||
| #[cfg(target_os = "linux")] | ||
| fn discover_all_dependencies( | ||
| input_files: &[PathBuf], | ||
| verbose: bool, | ||
|
|
@@ -627,7 +653,7 @@ fn build_tar(entries: &[TarEntry], output: &Path) -> anyhow::Result<()> { | |
| let mut builder = Builder::new(file); | ||
|
|
||
| for entry in entries { | ||
| let mut header = Header::new_gnu(); | ||
| let mut header = Header::new_ustar(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need to use USTAR? It does support up to 255-byte path lengths by default. |
||
| header.set_size(entry.data.len() as u64); | ||
| // Mask to permission bits only (rwxrwxrwx). The full st_mode from | ||
| // MetadataExt::mode() includes file type bits (e.g., 0o100755) which | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,18 +1,8 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT license. | ||
|
|
||
| // Restrict this crate to only work on Linux, as it relies on `ldd` for | ||
| // dependency discovery and other Linux-specific functionality. | ||
|
|
||
| #[cfg(target_os = "linux")] | ||
| fn main() -> anyhow::Result<()> { | ||
| use clap::Parser as _; | ||
| use litebox_packager::CliArgs; | ||
| litebox_packager::run(CliArgs::parse()) | ||
| } | ||
|
|
||
| #[cfg(not(target_os = "linux"))] | ||
| fn main() { | ||
| eprintln!("This program is only supported on Linux"); | ||
| std::process::exit(1); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lookup_modeinoci.rsuses0o644as a default value.