Skip to content
Draft
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
254 changes: 211 additions & 43 deletions crates/cfsctl/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,20 @@ pub use composefs_http;
pub use composefs_oci;

use std::io::Read;
use std::path::Path;
use std::{ffi::OsString, path::PathBuf};

#[cfg(feature = "oci")]
use std::{fs::create_dir_all, io::IsTerminal, path::Path};
use std::{fs::create_dir_all, io::IsTerminal};

#[cfg(any(feature = "oci", feature = "http"))]
use std::sync::Arc;

use anyhow::Result;
use anyhow::{Context as _, Result};
use clap::{Parser, Subcommand, ValueEnum};
#[cfg(feature = "oci")]
use comfy_table::{presets::UTF8_FULL, Table};
use rustix::fs::{Mode, OFlags};

use rustix::fs::CWD;
use serde::Serialize;
Expand All @@ -48,9 +50,9 @@ use composefs::shared_internals::IO_BUF_CAPACITY;
use composefs::{
dumpfile::{dump_single_dir, dump_single_file},
erofs::reader::erofs_to_filesystem,
fsverity::{FsVerityHashValue, Sha256HashValue, Sha512HashValue},
fsverity::{Algorithm, FsVerityHashValue, Sha256HashValue, Sha512HashValue},
generic_tree::{FileSystem, Inode},
repository::Repository,
repository::{read_repo_algorithm, system_path, user_path, Repository, REPO_METADATA_FILENAME},
tree::RegularFile,
};

Expand Down Expand Up @@ -85,27 +87,31 @@ pub struct App {
#[clap(long, group = "repopath")]
system: bool,

/// What hash digest type to use for composefs repo
#[clap(long, value_enum, default_value_t = HashType::Sha512)]
pub hash: HashType,
/// What hash digest type to use for composefs repo.
/// If omitted, auto-detected from repository metadata (meta.json).
#[clap(long, value_enum)]
pub hash: Option<HashType>,

/// Sets the repository to insecure before running any operation and
/// prepend '?' to the composefs kernel command line when writing
/// boot entry.
#[clap(long)]
/// Deprecated: security mode is now auto-detected from meta.json.
/// Use `cfsctl init --insecure` to create a repo without verity.
/// Kept for backward compatibility.
#[clap(long, hide = true)]
insecure: bool,

/// Error if the repository does not have fs-verity enabled.
#[clap(long)]
require_verity: bool,

#[clap(subcommand)]
cmd: Command,
}

/// The Hash algorithm used for FsVerity computation
#[derive(Debug, Copy, Clone, PartialEq, Eq, ValueEnum, Default)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, ValueEnum)]
pub enum HashType {
/// Sha256
Sha256,
/// Sha512
#[default]
Sha512,
}

Expand Down Expand Up @@ -276,6 +282,30 @@ struct FsReadOptions {

#[derive(Debug, Subcommand)]
enum Command {
/// Initialize a new composefs repository with a metadata file.
///
/// Creates the repository directory (if it doesn't exist) and writes
/// a `meta.json` recording the digest algorithm. By default fs-verity
/// is enabled on `meta.json`, signaling that all objects require
/// verity. Use `--insecure` to skip (e.g. on tmpfs).
Init {
/// The fs-verity algorithm identifier.
/// Format: fsverity-<hash>-<lg_blocksize>, e.g. fsverity-sha512-12
#[clap(long, value_parser = clap::value_parser!(Algorithm), default_value = "fsverity-sha512-12")]
algorithm: Algorithm,
/// Path to the repository directory (created if it doesn't exist).
/// If omitted, uses --repo/--user/--system location.
path: Option<PathBuf>,
/// Do not enable fs-verity on meta.json (insecure repository).
#[clap(long)]
insecure: bool,
/// Migrate an old-format repository: remove streams/ and images/
/// (which encode the algorithm) but keep objects/, then write
/// fresh meta.json. Streams and images will need to be
/// re-imported after migration.
#[clap(long)]
reset_metadata: bool,
},
/// Take a transaction lock on the repository.
/// This prevents garbage collection from occurring.
Transaction,
Expand Down Expand Up @@ -375,10 +405,7 @@ where
std::iter::once(OsString::from("cfsctl")).chain(args.into_iter().map(Into::into)),
);

match args.hash {
HashType::Sha256 => run_cmd_with_repo(open_repo::<Sha256HashValue>(&args)?, args).await,
HashType::Sha512 => run_cmd_with_repo(open_repo::<Sha512HashValue>(&args)?, args).await,
}
run_app(args).await
}

#[cfg(feature = "oci")]
Expand All @@ -392,25 +419,170 @@ where
})
}

/// Open a repo
pub fn open_repo<ObjectID>(args: &App) -> Result<Repository<ObjectID>>
where
ObjectID: FsVerityHashValue,
{
let mut repo = (if let Some(path) = &args.repo {
Repository::open_path(CWD, path)
/// Resolve the repository path from CLI args without opening it.
///
/// Uses [`user_path`] and [`system_path`] to avoid duplicating
/// path constants.
fn resolve_repo_path(args: &App) -> Result<PathBuf> {
if let Some(path) = &args.repo {
Ok(path.clone())
} else if args.system {
Repository::open_system()
Ok(system_path())
} else if args.user {
Repository::open_user()
user_path()
} else if rustix::process::getuid().is_root() {
Repository::open_system()
Ok(system_path())
} else {
Repository::open_user()
user_path()
}
}

/// Determine the effective hash type for a repository.
///
/// Resolution order:
/// 1. If `meta.json` exists, use its algorithm. Error if `--hash` was
/// explicitly passed and conflicts.
/// 2. If no metadata, use `--hash` if given.
/// 3. Otherwise default to sha512.
///
/// Note: we read the metadata file directly here (rather than via
/// `Repository::metadata`) because this runs *before* we know which
/// generic `ObjectID` type to use — that's exactly what we're deciding.
fn resolve_hash_type(repo_path: &Path, cli_hash: Option<HashType>) -> Result<HashType> {
let repo_fd = rustix::fs::open(
repo_path,
OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC,
Mode::empty(),
)
.with_context(|| format!("opening repository {}", repo_path.display()))?;

let algorithm = read_repo_algorithm(&repo_fd)?.ok_or_else(|| {
anyhow::anyhow!(
"{REPO_METADATA_FILENAME} not found in {}; \
this repository must be initialized with `cfsctl init`",
repo_path.display(),
)
})?;

repo.set_insecure(args.insecure);
let detected = match algorithm {
Algorithm::Sha256 { .. } => HashType::Sha256,
Algorithm::Sha512 { .. } => HashType::Sha512,
};

// If the user explicitly passed --hash and it doesn't match, error
if let Some(explicit) = cli_hash {
if explicit != detected {
anyhow::bail!(
"repository is configured for {algorithm} (from {REPO_METADATA_FILENAME}) \
but --hash {} was specified",
match explicit {
HashType::Sha256 => "sha256",
HashType::Sha512 => "sha512",
},
);
}
}

Ok(detected)
}

/// Top-level dispatch: handle init specially, otherwise open repo and run.
pub async fn run_app(args: App) -> Result<()> {
// Init is handled before opening a repo since it creates one
if let Command::Init {
ref algorithm,
ref path,
insecure,
reset_metadata,
} = args.cmd
{
return run_init(
algorithm,
path.as_deref(),
insecure || args.insecure,
reset_metadata,
&args,
);
}

let repo_path = resolve_repo_path(&args)?;
let effective_hash = resolve_hash_type(&repo_path, args.hash)?;

match effective_hash {
HashType::Sha256 => run_cmd_with_repo(open_repo::<Sha256HashValue>(&args)?, args).await,
HashType::Sha512 => run_cmd_with_repo(open_repo::<Sha512HashValue>(&args)?, args).await,
}
}

/// Handle `cfsctl init`
fn run_init(
algorithm: &Algorithm,
path: Option<&Path>,
insecure: bool,
reset_metadata: bool,
args: &App,
) -> Result<()> {
let repo_path = if let Some(p) = path {
p.to_path_buf()
} else {
resolve_repo_path(args)?
};

if reset_metadata {
composefs::repository::reset_metadata(&repo_path)?;
}

// Ensure parent directories exist (init_path only creates the final dir).
if let Some(parent) = repo_path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("creating parent directories for {}", repo_path.display()))?;
}

// init_path handles idempotency: same algorithm is a no-op,
// different algorithm is an error.
let created = match algorithm {
Algorithm::Sha256 { .. } => {
Repository::<Sha256HashValue>::init_path(CWD, &repo_path, *algorithm, !insecure)?.1
}
Algorithm::Sha512 { .. } => {
Repository::<Sha512HashValue>::init_path(CWD, &repo_path, *algorithm, !insecure)?.1
}
};

if created {
println!(
"Initialized composefs repository at {}",
repo_path.display()
);
println!(" algorithm: {algorithm}");
if insecure {
println!(" verity: not required (insecure)");
} else {
println!(" verity: required");
}
} else {
println!("Repository already initialized at {}", repo_path.display());
}

Ok(())
}

/// Open a repo
pub fn open_repo<ObjectID>(args: &App) -> Result<Repository<ObjectID>>
where
ObjectID: FsVerityHashValue,
{
let path = resolve_repo_path(args)?;
let mut repo = Repository::open_path(CWD, path)?;
// Hidden --insecure flag for backward compatibility; the default
// now is to inherit the repo config, but if it's specified we
// disable requiring verity even if the repo says to use it.
if args.insecure {
repo.set_insecure();
}
if args.require_verity {
repo.require_verity()?;
}
Ok(repo)
}

Expand Down Expand Up @@ -509,6 +681,10 @@ where
ObjectID: FsVerityHashValue,
{
match args.cmd {
Command::Init { .. } => {
// Handled in run_app before we get here
unreachable!("init is handled before opening a repository");
}
Command::Transaction => {
// just wait for ^C
loop {
Expand Down Expand Up @@ -717,7 +893,7 @@ where
&repo,
entry,
&id,
args.insecure,
repo.is_insecure(),
bootdir,
None,
entry_id.as_deref(),
Expand Down Expand Up @@ -814,19 +990,11 @@ where
let mut img_buf = Vec::new();
std::fs::File::from(img_fd).read_to_end(&mut img_buf)?;

match args.hash {
HashType::Sha256 => dump_file_impl(
erofs_to_filesystem::<Sha256HashValue>(&img_buf)?,
&files,
backing_path_only,
)?,

HashType::Sha512 => dump_file_impl(
erofs_to_filesystem::<Sha512HashValue>(&img_buf)?,
&files,
backing_path_only,
)?,
};
dump_file_impl(
erofs_to_filesystem::<ObjectID>(&img_buf)?,
&files,
backing_path_only,
)?;
}
Command::Fsck { json } => {
let result = repo.fsck().await?;
Expand Down
9 changes: 2 additions & 7 deletions crates/cfsctl/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,15 @@
//! creating and mounting filesystem images, handling OCI containers, and performing
//! repository maintenance operations like garbage collection.

use cfsctl::{open_repo, run_cmd_with_repo, App, HashType};
use cfsctl::App;

use anyhow::Result;
use clap::Parser;
use composefs::fsverity::{Sha256HashValue, Sha512HashValue};

#[tokio::main]
async fn main() -> Result<()> {
env_logger::init();

let args = App::parse();

match args.hash {
HashType::Sha256 => run_cmd_with_repo(open_repo::<Sha256HashValue>(&args)?, args).await,
HashType::Sha512 => run_cmd_with_repo(open_repo::<Sha512HashValue>(&args)?, args).await,
}
cfsctl::run_app(args).await
}
9 changes: 8 additions & 1 deletion crates/composefs-oci/src/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,14 @@ mod test {
let (tar_data, diff_id) = build_baseimage();

let repo_dir = tempdir();
let repo = Arc::new(Repository::<Sha256HashValue>::open_path(CWD, &repo_dir)?);
let repo_path = repo_dir.path().join("repo");
let (repo, _) = Repository::<Sha256HashValue>::init_path(
CWD,
&repo_path,
composefs::fsverity::Algorithm::SHA256,
false,
)?;
let repo = Arc::new(repo);
let (verity, _stats) =
crate::import_layer(&repo, &diff_id, Some("layer"), &tar_data[..]).await?;

Expand Down
Loading
Loading