Skip to content
Open
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
43 changes: 0 additions & 43 deletions litebox_packager/build.rs

This file was deleted.

180 changes: 100 additions & 80 deletions litebox_packager/src/lib.rs
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};

Expand Down Expand Up @@ -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).
Expand All @@ -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}");
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -194,7 +219,45 @@ 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_or(0o755, |m| m.mode())
};
let rewritten = rewrite_elf(&data, &inc.host_path, args.verbose)?;
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(())
}
Expand All @@ -208,7 +271,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
Expand Down Expand Up @@ -303,76 +371,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_or(0o644, |m| m.mode());
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,
});
}

// Include the rtld audit library so the rewriter backend can load it.
#[cfg(target_arch = "x86_64")]
{
const RTLD_AUDIT_TAR_PATH: &str = "lib/litebox_rtld_audit.so";
if !added_tar_paths.insert(RTLD_AUDIT_TAR_PATH.to_string()) {
bail!(
"tar already contains {RTLD_AUDIT_TAR_PATH} -- \
remove the conflicting entry or use --no-rewrite"
);
}
tar_entries.push(TarEntry {
tar_path: RTLD_AUDIT_TAR_PATH.to_string(),
data: include_bytes!(concat!(env!("OUT_DIR"), "/litebox_rtld_audit.so")).to_vec(),
mode: 0o755,
});
}

/// 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)?;
Expand All @@ -394,17 +403,20 @@ fn finalize_tar(
// 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)
Expand Down Expand Up @@ -496,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,
Expand Down Expand Up @@ -556,6 +569,7 @@ const ELF_MAGIC: [u8; 4] = [0x7f, b'E', b'L', b'F'];
/// through the rewriter. For actual ELF files, benign rewriter errors (already
/// hooked, no syscalls, unsupported object, missing `.text`) are treated as
/// warnings and the original bytes are returned.
#[allow(clippy::unnecessary_wraps)]
fn rewrite_elf(data: &[u8], path: &Path, verbose: bool) -> anyhow::Result<Vec<u8>> {
// Fast-path: skip the rewriter entirely for non-ELF files.
if data.len() < 4 || data[..4] != ELF_MAGIC {
Expand All @@ -572,7 +586,13 @@ fn rewrite_elf(data: &[u8], path: &Path, verbose: bool) -> anyhow::Result<Vec<u8
}
Ok(rewritten)
}
Err(e) => Err(e).with_context(|| format!("failed to rewrite {}", path.display())),
Err(e) => {
eprintln!(
" warning: failed to rewrite {}: {e}; including as-is",
path.display()
);
Ok(data.to_vec())
}
}
}

Expand All @@ -592,7 +612,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();
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
Expand Down
10 changes: 0 additions & 10 deletions litebox_packager/src/main.rs
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);
}
Loading
Loading