Skip to content

Make fs-verity optional #147

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

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
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
1 change: 1 addition & 0 deletions .github/workflows/examples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ jobs:
- { dir: 'uki', os: 'fedora' }
- { dir: 'unified', os: 'fedora' }
- { dir: 'unified-secureboot', os: 'fedora' }
- { dir: 'nofsverity', os: 'arch' }
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think you might have forgotten to also add the directory in this commit :)

fail-fast: false

steps:
Expand Down
9 changes: 7 additions & 2 deletions crates/cfsctl/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ pub struct App {
#[clap(long, group = "repopath")]
system: bool,

#[clap(long)]
Copy link
Collaborator

Choose a reason for hiding this comment

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

A small doc here might be nice.

insecure: bool,

#[clap(subcommand)]
cmd: Command,
}
Expand Down Expand Up @@ -160,7 +163,7 @@ async fn main() -> Result<()> {

let args = App::parse();

let repo: Repository<Sha256HashValue> = (if let Some(path) = &args.repo {
let mut repo: Repository<Sha256HashValue> = (if let Some(path) = &args.repo {
Repository::open_path(CWD, path)
} else if args.system {
Repository::open_system()
Expand All @@ -172,6 +175,8 @@ async fn main() -> Result<()> {
Repository::open_user()
})?;

repo.set_insecure(args.insecure);

match args.cmd {
Command::Transaction => {
// just wait for ^C
Expand Down Expand Up @@ -339,7 +344,7 @@ async fn main() -> Result<()> {
fs.print_dumpfile()?;
}
Command::Mount { name, mountpoint } => {
repo.mount(&name, &mountpoint)?;
repo.mount_at(&name, &mountpoint)?;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please merge this into the commit that made this change. Each commit ought to compile cleanly and pass tests.

}
Command::ImageObjects { name } => {
let objects = repo.objects_for_image(&name)?;
Expand Down
7 changes: 6 additions & 1 deletion crates/composefs-boot/src/write_boot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,14 @@ pub fn write_t2_simple<ObjectID: FsVerityHashValue>(
create_dir_all(&efi_linux)?;
let filename = efi_linux.join(t2.filename.as_ref());
let content = read_file(&t2.file, repo)?;
let Some(composefs) = get_cmdline_value(uki::get_cmdline(&content)?, "composefs=") else {
let Some(mut composefs) = get_cmdline_value(uki::get_cmdline(&content)?, "composefs=") else {
bail!("The UKI is missing a composefs= commandline parameter");
};

if let Some(stripped) = composefs.strip_prefix('?') {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, please add a new function to the end of cmdline.rs... You should probably also make this change at the same time as you change composefs-setup-root with the same change.

composefs = stripped
}

let expected = root_id.to_hex();
ensure!(
composefs == expected,
Expand Down
2 changes: 1 addition & 1 deletion crates/composefs-oci/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ pub fn mount<ObjectID: FsVerityHashValue>(
let Some(id) = config.get_config_annotation("containers.composefs.fsverity") else {
bail!("Can only mount sealed containers");
};
repo.mount(id, mountpoint)
repo.mount_at(id, mountpoint)
Copy link
Collaborator

Choose a reason for hiding this comment

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

ditto. Please merge that into the original commit.

}

#[cfg(test)]
Expand Down
35 changes: 21 additions & 14 deletions crates/composefs-setup-root/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use serde::Deserialize;

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

fn mount_composefs_image(sysroot: &OwnedFd, name: &str) -> Result<OwnedFd> {
let repo = Repository::<Sha256HashValue>::open_path(sysroot, "composefs")?;
let image = repo.open_image(name)?;
composefs_fsmount(image, name, repo.objects_dir()?).context("Failed to mount composefs image")
fn mount_composefs_image(sysroot: &OwnedFd, name: &str, insecure: bool) -> Result<OwnedFd> {
let mut repo = Repository::<Sha256HashValue>::open_path(sysroot, "composefs")?;
repo.set_insecure(insecure);
repo.mount(name).context("Failed to mount composefs image")
}

fn mount_subdir(
Expand Down Expand Up @@ -198,12 +198,19 @@ fn mount_subdir(
}

// Implementation
fn parse_composefs_cmdline<H: FsVerityHashValue>(cmdline: &str) -> Result<H> {
let Some(digest) = get_cmdline_value(cmdline, "composefs=") else {
fn parse_composefs_cmdline<H: FsVerityHashValue>(cmdline: &str) -> Result<(H, bool)> {
let Some(mut digest) = get_cmdline_value(cmdline, "composefs=") else {
bail!("Unable to find composefs= cmdline parameter");
};

H::from_hex(digest).context("Parsing composefs=")
let mut insecure = false;
if let Some(stripped) = digest.strip_prefix('?') {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we were going to move this code into cmdline.rs in the composefs-boot crate? We might also use ~ instead of ?.

digest = stripped;
insecure = true;
}

let hash = H::from_hex(digest).context("Parsing composefs=")?;
Ok((hash, insecure))
}

fn gpt_workaround() -> Result<()> {
Expand Down Expand Up @@ -232,11 +239,12 @@ fn setup_root(args: Args) -> Result<()> {
Some(cmdline) => cmdline,
None => &std::fs::read_to_string("/proc/cmdline")?,
};
let image = parse_composefs_cmdline::<Sha256HashValue>(cmdline)?.to_hex();
let (img, insecure) = parse_composefs_cmdline::<Sha256HashValue>(cmdline)?;
let image = img.to_hex();

let new_root = match args.root_fs {
Some(path) => open_root_fs(&path).context("Failed to clone specified root fs")?,
None => mount_composefs_image(&sysroot, &image)?,
None => mount_composefs_image(&sysroot, &image, insecure)?,
};

// we need to clone this before the next step to make sure we get the old one
Expand Down Expand Up @@ -289,9 +297,8 @@ mod test {
assert!(parse_composefs_cmdline::<Sha256HashValue>(case).is_err());
}
let digest = "8b7df143d91c716ecfa5fc1730022f6b421b05cedee8fd52b1fc65a96030ad52";
similar_asserts::assert_eq!(
parse_composefs_cmdline::<Sha256HashValue>(&format!("composefs={digest}")).unwrap(),
Sha256HashValue::from_hex(digest).unwrap()
);
let (digest_cmdline, _) =
parse_composefs_cmdline::<Sha256HashValue>(&format!("composefs={digest}")).unwrap();
similar_asserts::assert_eq!(digest_cmdline, Sha256HashValue::from_hex(digest).unwrap());
}
}
14 changes: 11 additions & 3 deletions crates/composefs/src/mount.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,21 @@ pub fn erofs_mount(image: OwnedFd) -> Result<OwnedFd> {
)?)
}

pub fn composefs_fsmount(image: OwnedFd, name: &str, basedir: impl AsFd) -> Result<OwnedFd> {
pub fn composefs_fsmount(
image: OwnedFd,
name: &str,
basedir: impl AsFd,
enable_verity: bool,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Almost feels like this should be a flags field.... in which case disable-verity would be the opt-in. Can we make this function pub(crate)? Ideally this will only ever be called via Repository now, which will get this logic correct...

) -> Result<OwnedFd> {
let erofs_mnt = prepare_mount(erofs_mount(image)?)?;

let overlayfs = FsHandle::open("overlay")?;
fsconfig_set_string(overlayfs.as_fd(), "source", format!("composefs:{name}"))?;
fsconfig_set_string(overlayfs.as_fd(), "metacopy", "on")?;
fsconfig_set_string(overlayfs.as_fd(), "redirect_dir", "on")?;
fsconfig_set_string(overlayfs.as_fd(), "verity", "require")?;
if enable_verity {
fsconfig_set_string(overlayfs.as_fd(), "verity", "require")?;
}
overlayfs_set_lower_and_data_fds(&overlayfs, &erofs_mnt, Some(&basedir))?;
fsconfig_create(overlayfs.as_fd())?;

Expand All @@ -101,7 +108,8 @@ pub fn mount_composefs_at(
name: &str,
basedir: impl AsFd,
mountpoint: impl AsRef<Path>,
enable_verity: bool,
) -> Result<()> {
let mnt = composefs_fsmount(image, name, basedir)?;
let mnt = composefs_fsmount(image, name, basedir, enable_verity)?;
Ok(mount_at(mnt, CWD, &canonicalize(mountpoint)?)?)
}
69 changes: 58 additions & 11 deletions crates/composefs/src/repository.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ use sha2::{Digest, Sha256};
use crate::{
fsverity::{
compute_verity, enable_verity, ensure_verity_equal, measure_verity, FsVerityHashValue,
MeasureVerityError,
},
mount::mount_composefs_at,
mount::{composefs_fsmount, mount_composefs_at},
splitstream::{DigestMap, SplitStreamReader, SplitStreamWriter},
util::{proc_self_fd, Sha256Digest},
};
Expand Down Expand Up @@ -58,6 +59,7 @@ pub struct Repository<ObjectID: FsVerityHashValue> {
repository: OwnedFd,
objects: OnceCell<OwnedFd>,
_data: std::marker::PhantomData<ObjectID>,
insecure: bool,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Probably move that above the PhantomData...

}

impl<ObjectID: FsVerityHashValue> Drop for Repository<ObjectID> {
Expand Down Expand Up @@ -86,6 +88,7 @@ impl<ObjectID: FsVerityHashValue> Repository<ObjectID> {
repository,
objects: OnceCell::new(),
_data: std::marker::PhantomData,
insecure: false,
Copy link
Collaborator

Choose a reason for hiding this comment

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

...ditto

})
}

Expand Down Expand Up @@ -127,7 +130,9 @@ impl<ObjectID: FsVerityHashValue> Repository<ObjectID> {
Ok(fd) => {
// measure the existing file to ensure that it's correct
// TODO: try to replace file if it's broken?
ensure_verity_equal(fd, &id)?;
if !self.insecure {
Copy link
Collaborator

Choose a reason for hiding this comment

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

As a general rule I think we always want to attempt to measure fs-verity even in insecure mode, but allow that it fails with "not supported" errors.

ensure_verity_equal(fd, &id)?;
}
return Ok(id);
}
Err(Errno::NOENT) => {
Expand All @@ -151,8 +156,23 @@ impl<ObjectID: FsVerityHashValue> Repository<ObjectID> {
)?;
drop(file);

enable_verity::<ObjectID>(&ro_fd).context("Enabling verity digest")?;
ensure_verity_equal(&ro_fd, &id).context("Double-checking verity digest")?;
if self.insecure {
match measure_verity::<ObjectID>(&ro_fd) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

The measure here doesn't make much sense. We just created this, so the answer will always be either that there's no fs-verity on the file, or that fs-verity is unsupported. But you could get that information by just trying to enable it and deal with the errors. I think you could probably also merge the code paths with the else (original) case... The only different you really need is what to do if the enabling fails with the not-supported error: in secure mode, that's fatal. In insecure mode, you ignore it.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Also please merge this into the first commit which adds Repository.insecure in the first place.

Ok(found) if found == FsVerityHashValue::from_hex(&path)? => {
// insecure but file sytem supports it, so enable it anyway
enable_verity::<ObjectID>(&ro_fd).context("Enabling verity digest")?;
ensure_verity_equal(&ro_fd, &id).context("Double-checking verity digest")?;
}
Ok(_) => bail!("fs-verity content mismatch"),
Err(MeasureVerityError::VerityMissing) => {
// file system doesn't support it, just continue without
}
Err(other) => Err(other)?,
}
} else {
enable_verity::<ObjectID>(&ro_fd).context("Enabling verity digest")?;
ensure_verity_equal(&ro_fd, &id).context("Double-checking verity digest")?;
}

match linkat(
CWD,
Expand All @@ -175,10 +195,17 @@ impl<ObjectID: FsVerityHashValue> Repository<ObjectID> {

fn open_with_verity(&self, filename: &str, expected_verity: &ObjectID) -> Result<OwnedFd> {
let fd = self.openat(filename, OFlags::RDONLY)?;
ensure_verity_equal(&fd, expected_verity)?;
if !self.insecure {
Copy link
Collaborator

Choose a reason for hiding this comment

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

ditto

ensure_verity_equal(&fd, expected_verity)?;
}
Ok(fd)
}

pub fn set_insecure(&mut self, insecure: bool) -> &mut Self {
self.insecure = insecure;
self
}

/// Creates a SplitStreamWriter for writing a split stream.
/// You should write the data to the returned object and then pass it to .store_stream() to
/// store the result.
Expand Down Expand Up @@ -220,6 +247,10 @@ impl<ObjectID: FsVerityHashValue> Repository<ObjectID> {

/// Basically the same as has_stream() except that it performs expensive verification
pub fn check_stream(&self, sha256: &Sha256Digest) -> Result<Option<ObjectID>> {
if self.insecure {
return self.has_stream(sha256);
Copy link
Collaborator

Choose a reason for hiding this comment

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

This isn't sufficient, I'm afraid. We want to iterate the full structure to check that it's consistent and matches the given sha256 body digest, even if we don't have fs-verity.

}

match self.openat(&format!("streams/{}", hex::encode(sha256)), OFlags::RDONLY) {
Ok(stream) => {
let measured_verity: ObjectID = measure_verity(&stream)?;
Expand Down Expand Up @@ -383,24 +414,40 @@ impl<ObjectID: FsVerityHashValue> Repository<ObjectID> {
self.write_image(Some(name), &data)
}

pub fn open_image(&self, name: &str) -> Result<OwnedFd> {
fn open_image(&self, name: &str) -> Result<(OwnedFd, bool)> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Depending on how many users this API has, it might make sense to inline it where it's used... I guess the GC thing is using it as well, though......

I'm glad it's no longer public, at least. A short doc about what the 'bool' is would certainly be appreciated though.

let image = self.openat(&format!("images/{name}"), OFlags::RDONLY)?;

if !name.contains("/") {
if !name.contains("/") && !self.insecure {
// A name with no slashes in it is taken to be a sha256 fs-verity digest
ensure_verity_equal(&image, &ObjectID::from_hex(name)?)?;
}

Ok(image)
match measure_verity::<ObjectID>(&image) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

You're effectively measuring and comparing verity twice here... You left the check above...

Ok(found) if found == FsVerityHashValue::from_hex(name)? => Ok((image, true)),
Copy link
Collaborator

Choose a reason for hiding this comment

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

name isn't always hex: it might be a ref with / in it. That's what the check above is about.

Copy link
Author

Choose a reason for hiding this comment

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

Right, if it does I guess we can just skip the entire thing and return (image, true)?

Ok(_) => bail!("fs-verity content mismatch"),
Err(MeasureVerityError::VerityMissing) if self.insecure => Ok((image, false)),
Err(other) => Err(other)?,
}
}

pub fn mount(&self, name: &str) -> Result<OwnedFd> {
let (image, enable_verity) = self.open_image(name)?;
Ok(composefs_fsmount(
image,
name,
self.objects_dir()?,
enable_verity,
)?)
}

pub fn mount(&self, name: &str, mountpoint: &str) -> Result<()> {
let image = self.open_image(name)?;
pub fn mount_at(&self, name: &str, mountpoint: &str) -> Result<()> {
let (image, enable_verity) = self.open_image(name)?;
Ok(mount_composefs_at(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we maybe get rid of mount_composefs_at() in mount.rs and use move_mount(self.mount(name), mountpoint) here instead (or whatever mount_at internal helper we have?)

image,
name,
self.objects_dir()?,
mountpoint,
enable_verity,
)?)
}

Expand Down Expand Up @@ -522,7 +569,7 @@ impl<ObjectID: FsVerityHashValue> Repository<ObjectID> {
}

pub fn objects_for_image(&self, name: &str) -> Result<HashSet<ObjectID>> {
let image = self.open_image(name)?;
let (image, _) = self.open_image(name)?;
let mut data = vec![];
std::fs::File::from(image).read_to_end(&mut data)?;
Ok(crate::erofs::reader::collect_objects(&data)?)
Expand Down
41 changes: 38 additions & 3 deletions examples/common/make-image
Original file line number Diff line number Diff line change
@@ -1,12 +1,47 @@
#!/bin/sh
#!/bin/bash

set -eux

output="$1"
FIX_VERITY=0
Copy link
Collaborator

Choose a reason for hiding this comment

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

The user should be able to specify FIX_VERITY=1 from the outside...

SETUP_REPART=0
FS_FORMAT=ext4
parsecmd() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This commandline interface thing seems a bit overkill to be honest...

It might be nicer to just be able to set a couple of extra envvars to modify the test environment, from outside:

  • an envvar to disable fs-verity complete, impacting both systemd-repart and the fix-verity script
  • an envvar to let choosing a different type for the rootfs (xfs, etc.)

Maybe we could avoid having a whole separate test scenario...

while [ $# -gt 0 ]; do
case "$1" in
#CMDSTART
-fv|--fix-verity) #
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please don't do two-character 'short' opts...

# run run-repart with FIX_VERITY=1
FIX_VERITY=1
;;
-sr|--setup-repart) #
# run run-repart with SETUP_REPART=1
SETUP_REPART=1
;;
-h|--help) #
# this help
echo "Usage: $0 [OPTION]..."
printf "\nCommand line arguments:\n"
sed -rn '/CMDSTART/,/CMDEND/{/\) \#|^ +# /{s/\)? #//g;s/^ //;p}}' "$0"
exit 0
;;
*)
echo "Unknown parameter $1"
exit 1
;;
esac
#CMDEND
shift
done
}

args=($@)
output="${args[-1]}"
parse_args=("${args[*]:0:${#args[@]}-1}")
parsecmd ${parse_args[@]}

# check that the image doesn't have errors
fsck.erofs tmp/sysroot/composefs/images/*

fakeroot "${0%/*}/run-repart" tmp/image.raw
SETUP_REPART=$SETUP_REPART FIX_VERITY=$FIX_VERITY fakeroot "${0%/*}/run-repart" tmp/image.raw
qemu-img convert -f raw tmp/image.raw -O qcow2 "${output}"
rm tmp/image.raw
2 changes: 1 addition & 1 deletion examples/common/run-repart
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ chcon system_u:object_r:etc_t:s0 tmp/sysroot/state/*/etc/*

definitions="${0%/*}/repart.d"

if [ "${FIX_VERITY:-}" = '1' ]; then
if [ "$SETUP_REPART" = '1' ]; then
Copy link
Collaborator

Choose a reason for hiding this comment

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

This variable is hilariously poorly named 🤣

Copy link
Collaborator

Choose a reason for hiding this comment

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

It might be interesting to make this setup a bit more 'neutral' by dynamically generating the repart.d directory in the tmp/ dir on each run using some sort of a heredoc-based approach... It's only two small files and it would be a bit cleaner than sed-hacking, I guess.

export SYSTEMD_REPART_MKFS_OPTIONS_EXT4='-O verity'
cp -r "${definitions}" tmp/repart.d
sed -i 's/:fsverity=copy//' tmp/repart.d/02-sysroot.conf
Expand Down
Loading