Skip to content
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ thiserror = "2.0.0"
tokio = "1.24.2"
toml = "0.8.0"
xxhash-rust = { version = "0.8.2", features = ["xxh32"] }
zerocopy = { version = "0.8.0", features = ["derive"] }
zerocopy = { version = "0.8.0", features = ["derive", "std"] }
zstd = "0.13.0"

[dev-dependencies]
Expand Down
30 changes: 19 additions & 11 deletions src/bin/cfsctl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ use clap::{Parser, Subcommand};

use rustix::fs::CWD;

use composefs::{oci, repository::Repository, util::parse_sha256};
use composefs::{
fsverity::{FsVerityHashValue, Sha256HashValue},
oci,
repository::Repository,
util::parse_sha256,
};

/// cfsctl
#[derive(Debug, Parser)]
Expand Down Expand Up @@ -106,7 +111,7 @@ fn main() -> Result<()> {

let args = App::parse();

let repo = (if let Some(path) = args.repo {
let repo: Repository<Sha256HashValue> = (if let Some(path) = args.repo {
Repository::open_path(CWD, path)
} else if args.system {
Repository::open_system()
Expand All @@ -130,7 +135,7 @@ fn main() -> Result<()> {
}
Command::ImportImage { reference } => {
let image_id = repo.import_image(&reference, &mut std::io::stdin())?;
println!("{}", hex::encode(image_id));
println!("{}", image_id.to_hex());
}
Command::Oci { cmd: oci_cmd } => match oci_cmd {
OciCommand::ImportLayer { name, sha256 } => {
Expand All @@ -140,7 +145,7 @@ fn main() -> Result<()> {
name.as_deref(),
&mut std::io::stdin(),
)?;
println!("{}", hex::encode(object_id));
println!("{}", object_id.to_hex());
}
OciCommand::LsLayer { name } => {
oci::ls_layer(&repo, &name)?;
Expand All @@ -150,7 +155,7 @@ fn main() -> Result<()> {
}
OciCommand::CreateImage { config, name } => {
let image_id = oci::image::create_image(&repo, &config, name.as_deref(), None)?;
println!("{}", hex::encode(image_id));
println!("{}", image_id.to_hex());
}
OciCommand::Pull { ref image, name } => {
let runtime = tokio::runtime::Builder::new_current_thread()
Expand All @@ -161,10 +166,13 @@ fn main() -> Result<()> {
runtime.block_on(async move { oci::pull(&repo, image, name.as_deref()).await })?;
}
OciCommand::Seal { verity, ref name } => {
let (sha256, verity) =
oci::seal(&repo, name, verity.map(parse_sha256).transpose()?.as_ref())?;
let (sha256, verity) = oci::seal(
&repo,
name,
verity.map(Sha256HashValue::from_hex).transpose()?.as_ref(),
)?;
println!("sha256 {}", hex::encode(sha256));
println!("verity {}", hex::encode(verity));
println!("verity {}", verity.to_hex());
}
OciCommand::Mount {
ref name,
Expand All @@ -182,18 +190,18 @@ fn main() -> Result<()> {
},
Command::CreateImage { ref path } => {
let image_id = composefs::fs::create_image(path, Some(&repo))?;
println!("{}", hex::encode(image_id));
println!("{}", image_id.to_hex());
}
Command::CreateDumpfile { ref path } => {
composefs::fs::create_dumpfile(path)?;
composefs::fs::create_dumpfile::<Sha256HashValue>(path)?;
}
Command::Mount { name, mountpoint } => {
repo.mount(&name, &mountpoint)?;
}
Command::ImageObjects { name } => {
let objects = repo.objects_for_image(&name)?;
for object in objects {
println!("{}", hex::encode(object));
println!("{}", object.to_hex());
}
}
Command::GC => {
Expand Down
41 changes: 21 additions & 20 deletions src/bin/composefs-setup-root.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use rustix::{
use serde::Deserialize;

use composefs::{
fsverity::Sha256HashValue,
fsverity::{FsVerityHashValue, Sha256HashValue},
mount::{composefs_fsmount, mount_at, FsHandle},
mountcompat::{overlayfs_set_fd, overlayfs_set_lower_and_data_fds, prepare_mount},
repository::Repository,
Expand Down Expand Up @@ -167,7 +167,7 @@ fn open_root_fs(path: &Path) -> Result<OwnedFd> {
}

fn mount_composefs_image(sysroot: &OwnedFd, name: &str) -> Result<OwnedFd> {
let repo = Repository::open_path(sysroot, "composefs")?;
let repo = Repository::<Sha256HashValue>::open_path(sysroot, "composefs")?;
let image = repo.open_image(name)?;
composefs_fsmount(image, name, repo.object_dir()?).context("Failed to mount composefs image")
}
Expand Down Expand Up @@ -200,13 +200,11 @@ fn mount_subdir(
}

// Implementation
fn parse_composefs_cmdline(cmdline: &[u8]) -> Result<Sha256HashValue> {
fn parse_composefs_cmdline<H: FsVerityHashValue>(cmdline: &[u8]) -> Result<H> {
// TODO?: officially we need to understand quoting with double-quotes...
for part in cmdline.split(|c| c.is_ascii_whitespace()) {
if let Some(digest) = part.strip_prefix(b"composefs=") {
let mut value = [0; 32];
hex::decode_to_slice(digest, &mut value).context("Parsing composefs=")?;
return Ok(value);
return H::from_hex(digest).context("Parsing composefs=");
}
}
bail!("Unable to find composefs= cmdline parameter");
Expand Down Expand Up @@ -238,7 +236,7 @@ fn setup_root(args: Args) -> Result<()> {
Some(cmdline) => cmdline.as_bytes(),
None => &std::fs::read("/proc/cmdline")?,
};
let image = hex::encode(parse_composefs_cmdline(cmdline)?);
let image = parse_composefs_cmdline::<Sha256HashValue>(cmdline)?.to_hex();

let new_root = match args.root_fs {
Some(path) => open_root_fs(&path).context("Failed to clone specified root fs")?,
Expand Down Expand Up @@ -284,18 +282,21 @@ fn main() -> Result<()> {
setup_root(args)
}

#[test]
fn test_parse() {
let failing = ["", "foo", "composefs", "composefs=foo"];
for case in failing {
assert!(parse_composefs_cmdline(case.as_bytes()).is_err());
#[cfg(test)]
mod test {
use super::*;

#[test]
fn test_parse() {
let failing = ["", "foo", "composefs", "composefs=foo"];
for case in failing {
assert!(parse_composefs_cmdline::<Sha256HashValue>(case.as_bytes()).is_err());
}
let digest = "8b7df143d91c716ecfa5fc1730022f6b421b05cedee8fd52b1fc65a96030ad52";
similar_asserts::assert_eq!(
parse_composefs_cmdline::<Sha256HashValue>(format!("composefs={digest}").as_bytes())
.unwrap(),
Sha256HashValue::from_hex(digest).unwrap()
);
}
let digest = "8b7df143d91c716ecfa5fc1730022f6b421b05cedee8fd52b1fc65a96030ad52";
let digest_bytes = hex::decode(digest).unwrap();
similar_asserts::assert_eq!(
parse_composefs_cmdline(format!("composefs={digest}").as_bytes())
.unwrap()
.as_slice(),
&digest_bytes
);
}
27 changes: 15 additions & 12 deletions src/dumpfile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use anyhow::Result;
use rustix::fs::FileType;

use crate::{
fsverity::Sha256HashValue,
fsverity::FsVerityHashValue,
image::{Directory, FileSystem, Inode, Leaf, LeafContent, RegularFile, Stat},
};

Expand Down Expand Up @@ -49,7 +49,7 @@ fn write_entry(
rdev: u64,
payload: impl AsRef<OsStr>,
content: &[u8],
digest: Option<&Sha256HashValue>,
digest: Option<&str>,
) -> fmt::Result {
let mode = stat.st_mode | ifmt.as_raw_mode();
let uid = stat.st_uid;
Expand All @@ -66,7 +66,7 @@ fn write_entry(
write_escaped(writer, content)?;
write!(writer, " ")?;
if let Some(id) = digest {
write!(writer, "{}", hex::encode(id))?;
write!(writer, "{}", id)?;
} else {
write_empty(writer)?;
}
Expand Down Expand Up @@ -105,7 +105,7 @@ pub fn write_leaf(
writer: &mut impl fmt::Write,
path: &Path,
stat: &Stat,
content: &LeafContent,
content: &LeafContent<impl FsVerityHashValue>,
nlink: usize,
) -> fmt::Result {
match content {
Expand All @@ -129,9 +129,9 @@ pub fn write_leaf(
*size,
nlink,
0,
format!("{:02x}/{}", id[0], hex::encode(&id[1..])),
id.to_object_pathname(),
&[],
Some(id),
Some(&id.to_hex()),
),
LeafContent::BlockDevice(rdev) => write_entry(
writer,
Expand Down Expand Up @@ -204,8 +204,8 @@ pub fn write_hardlink(writer: &mut impl fmt::Write, path: &Path, target: &OsStr)
Ok(())
}

struct DumpfileWriter<'a, W: Write> {
hardlinks: HashMap<*const Leaf, OsString>,
struct DumpfileWriter<'a, W: Write, ObjectID: FsVerityHashValue> {
hardlinks: HashMap<*const Leaf<ObjectID>, OsString>,
writer: &'a mut W,
}

Expand All @@ -215,15 +215,15 @@ fn writeln_fmt(writer: &mut impl Write, f: impl Fn(&mut String) -> fmt::Result)
Ok(writeln!(writer, "{}", tmp)?)
}

impl<'a, W: Write> DumpfileWriter<'a, W> {
impl<'a, W: Write, ObjectID: FsVerityHashValue> DumpfileWriter<'a, W, ObjectID> {
fn new(writer: &'a mut W) -> Self {
Self {
hardlinks: HashMap::new(),
writer,
}
}

fn write_dir(&mut self, path: &mut PathBuf, dir: &Directory) -> Result<()> {
fn write_dir(&mut self, path: &mut PathBuf, dir: &Directory<ObjectID>) -> Result<()> {
// nlink is 2 + number of subdirectories
// this is also true for the root dir since '..' is another self-ref
let nlink = dir.inodes().fold(2, |count, inode| {
Expand Down Expand Up @@ -256,7 +256,7 @@ impl<'a, W: Write> DumpfileWriter<'a, W> {
Ok(())
}

fn write_leaf(&mut self, path: &Path, leaf: &Rc<Leaf>) -> Result<()> {
fn write_leaf(&mut self, path: &Path, leaf: &Rc<Leaf<ObjectID>>) -> Result<()> {
let nlink = Rc::strong_count(leaf);

if nlink > 1 {
Expand All @@ -276,7 +276,10 @@ impl<'a, W: Write> DumpfileWriter<'a, W> {
}
}

pub fn write_dumpfile<W: Write>(writer: &mut W, fs: &FileSystem) -> Result<()> {
pub fn write_dumpfile(
writer: &mut impl Write,
fs: &FileSystem<impl FsVerityHashValue>,
) -> Result<()> {
// default pipe capacity on Linux is 16 pages (65536 bytes), but
// sometimes the BufWriter will write more than its capacity...
let mut buffer = BufWriter::with_capacity(32768, writer);
Expand Down
33 changes: 33 additions & 0 deletions src/erofs/composefs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};

use crate::fsverity::FsVerityHashValue;

/* From linux/fs/overlayfs/overlayfs.h struct ovl_metacopy */
#[derive(Debug, FromBytes, Immutable, KnownLayout, IntoBytes)]
#[repr(C)]
pub(super) struct OverlayMetacopy<H: FsVerityHashValue> {
version: u8,
len: u8,
flags: u8,
digest_algo: u8,
pub(super) digest: H,
}

impl<H: FsVerityHashValue> OverlayMetacopy<H> {
pub(super) fn new(digest: &H) -> Self {
Self {
version: 0,
len: size_of::<Self>() as u8,
flags: 0,
digest_algo: H::ALGORITHM,
digest: digest.clone(),
}
}

pub(super) fn valid(&self) -> bool {
self.version == 0
&& self.len == size_of::<Self>() as u8
&& self.flags == 0
&& self.digest_algo == H::ALGORITHM
}
}
1 change: 1 addition & 0 deletions src/erofs/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod composefs;
pub mod debug;
pub mod format;
pub mod reader;
Expand Down
30 changes: 16 additions & 14 deletions src/erofs/reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ use std::ops::Range;
use thiserror::Error;
use zerocopy::{little_endian::U32, FromBytes, Immutable, KnownLayout};

use super::format::{
CompactInodeHeader, ComposefsHeader, DataLayout, DirectoryEntryHeader, ExtendedInodeHeader,
InodeXAttrHeader, ModeField, Superblock, XAttrHeader,
use super::{
composefs::OverlayMetacopy,
format::{
CompactInodeHeader, ComposefsHeader, DataLayout, DirectoryEntryHeader, ExtendedInodeHeader,
InodeXAttrHeader, ModeField, Superblock, XAttrHeader,
},
};
use crate::fsverity::Sha256HashValue;
use crate::fsverity::FsVerityHashValue;

pub fn round_up(n: usize, to: usize) -> usize {
(n + to - 1) & !(to - 1)
Expand Down Expand Up @@ -483,26 +486,25 @@ pub enum ErofsReaderError {
type ReadResult<T> = Result<T, ErofsReaderError>;

#[derive(Debug)]
pub struct ObjectCollector {
pub struct ObjectCollector<ObjectID: FsVerityHashValue> {
visited_nids: HashSet<u64>,
nids_to_visit: BTreeSet<u64>,
objects: HashSet<Sha256HashValue>,
objects: HashSet<ObjectID>,
}

impl ObjectCollector {
impl<ObjectID: FsVerityHashValue> ObjectCollector<ObjectID> {
fn visit_xattr(&mut self, attr: &XAttr) {
// TODO: "4" is a bit magic, isn't it?
// This is the index of "trusted". See XATTR_PREFIXES in format.rs.
if attr.header.name_index != 4 {
return;
}
if attr.suffix() != b"overlay.metacopy" {
return;
}
let value = attr.value();
// TODO: oh look, more magic values...
if value.len() == 36 && value[..4] == [0, 36, 0, 1] {
// SAFETY: We already checked that the length is 4 + 32
self.objects.insert(value[4..].try_into().unwrap());
if let Ok(value) = OverlayMetacopy::read_from_bytes(attr.value()) {
if value.valid() {
self.objects.insert(value.digest);
}
}
}

Expand Down Expand Up @@ -550,7 +552,7 @@ impl ObjectCollector {
}
}

pub fn collect_objects(image: &[u8]) -> ReadResult<HashSet<Sha256HashValue>> {
pub fn collect_objects<ObjectID: FsVerityHashValue>(image: &[u8]) -> ReadResult<HashSet<ObjectID>> {
let img = Image::open(image);
let mut this = ObjectCollector {
visited_nids: HashSet::new(),
Expand Down
Loading