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
6 changes: 5 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -188,16 +188,20 @@ jobs:
tool: nextest@${{ env.NEXTEST_VERSION }}
- uses: Swatinem/rust-cache@v2
- run: cargo clippy --locked --verbose --all-targets --all-features -p litebox_runner_linux_on_windows_userland
- run: cargo clippy --locked --verbose --all-targets --all-features -p litebox_packager
- run: cargo build --locked --verbose -p litebox_runner_linux_on_windows_userland
- run: cargo build --locked --verbose -p litebox_packager
- run: cargo nextest run --locked --profile ci -p litebox_runner_linux_on_windows_userland
- run: cargo nextest run --locked --profile ci -p litebox_packager
- run: cargo nextest run --locked --profile ci -p litebox_shim_linux --no-default-features --features platform_windows_userland
- run: |
cargo test --locked --verbose --doc -p litebox_runner_linux_on_windows_userland
cargo test --locked --verbose --doc -p litebox_packager
# We need to run `cargo test --doc` separately because doc tests
# aren't included in nextest at the moment. See relevant discussion at
# https://github.com/nextest-rs/nextest/issues/16
- name: Build documentation (fail on warnings)
run: cargo doc --locked --verbose --no-deps --all-features --document-private-items -p litebox_runner_linux_on_windows_userland
run: cargo doc --locked --verbose --no-deps --all-features --document-private-items -p litebox_runner_linux_on_windows_userland -p litebox_packager

build_and_test_snp:
name: Build and Test SNP
Expand Down
6 changes: 0 additions & 6 deletions litebox_packager/build.rs

This file was deleted.

162 changes: 94 additions & 68 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,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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lookup_mode in oci.rs uses 0o644 as a default value.

};
let rewritten = rewrite_elf(&data, &inc.host_path, args.verbose)?;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ignores --no-rewrite.

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 +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
Expand Down Expand Up @@ -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)?;
Expand All @@ -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)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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
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