Skip to content
Merged
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
21 changes: 18 additions & 3 deletions Containerfile
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,30 @@ RUN --mount=type=cache,target=/src/target \
--mount=type=cache,target=/root/.cargo/git \
cargo fetch

# Build cfsctl and integration test binary
# Two separate invocations: features are scoped to composefs-ctl and must not
# Build cfsctl, the integration test binary and libcomposefs.
# Separate invocations: features are scoped to composefs-ctl and must not
# be passed to composefs-integration-tests, which has no optional features.
# libcomposefs only gets rhel9 (pre-6.15 is its default).
RUN --network=none \
--mount=type=cache,target=/src/target \
--mount=type=cache,target=/root/.cargo/registry \
--mount=type=cache,target=/root/.cargo/git \
cargo build --release -p composefs-ctl --features="${cfsctl_features}" && \
cargo build --release -p composefs-integration-tests && \
capi_features=$(echo "${cfsctl_features}" | tr ', ' '\n\n' | grep -x rhel9 || true) && \
cargo build --release -p composefs-capi --features="${capi_features}" && \
cp /src/target/release/cfsctl /usr/bin/cfsctl && \
cp /src/target/release/cfsctl-integration-tests /usr/bin/cfsctl-integration-tests
cp /src/target/release/cfsctl-integration-tests /usr/bin/cfsctl-integration-tests && \
mkdir -p /usr/lib/composefs-rs-test && \
cp /src/target/release/libcomposefs_capi.so /usr/lib/composefs-rs-test/libcomposefs.so.1

# A C program calling our libcomposefs (not the distribution's), for the
# privileged libcomposefs tests
RUN --network=none \
ln -s libcomposefs.so.1 /usr/lib/composefs-rs-test/libcomposefs.so && \
gcc -o /usr/bin/lcfs-mount-test /src/crates/composefs-capi/tests/lcfs-mount-test.c \
-I/src/crates/composefs-capi/include -L/usr/lib/composefs-rs-test -lcomposefs \
-Wl,-rpath,/usr/lib/composefs-rs-test

# -- final bootable image --
FROM ${base_image}
Expand All @@ -56,3 +69,5 @@ RUN /src/contrib/packaging/install-test-deps.sh && rm -rf /src

COPY --from=build /usr/bin/cfsctl /usr/bin/cfsctl
COPY --from=build /usr/bin/cfsctl-integration-tests /usr/bin/cfsctl-integration-tests
COPY --from=build /usr/lib/composefs-rs-test /usr/lib/composefs-rs-test
COPY --from=build /usr/bin/lcfs-mount-test /usr/bin/lcfs-mount-test
8 changes: 8 additions & 0 deletions crates/composefs-capi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,16 @@ description = "C-compatible shared library (libcomposefs) backed by Rust compose
[lib]
crate-type = ["cdylib", "staticlib"]

[features]
# Like the other binaries, support older kernels by default; the C
# library handled them at runtime.
default = ['pre-6.15']
rhel9 = ['composefs/rhel9']
'pre-6.15' = ['composefs/pre-6.15']

[dependencies]
composefs = { workspace = true }
hex = { version = "0.4.0", default-features = false, features = ["std"] }
libc = "0.2"
anyhow = "1"
rustix = { version = "1", features = ["fs", "mm", "process", "mount"] }
Expand Down
189 changes: 152 additions & 37 deletions crates/composefs-capi/src/mount.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
use std::ffi::{CStr, CString, c_char, c_int};
use std::os::fd::{AsFd, FromRawFd, OwnedFd};
use std::os::fd::{AsFd, BorrowedFd, FromRawFd, OwnedFd};

use libc::size_t;
use rustix::fs::{CWD, Mode, OFlags, open};

use crate::errno::set_errno;

/// `struct lcfs_mount_options_s` from lcfs-mount.h.
///
/// The entry points take it as `Option<&LcfsMountOptions>`: Rust guarantees
/// that has the ABI of a nullable C pointer, with null as `None`.
#[repr(C)]
pub struct LcfsMountOptions {
pub objdirs: *const *const c_char,
Expand All @@ -21,8 +25,88 @@ pub struct LcfsMountOptions {
}

const LCFS_MOUNT_FLAGS_REQUIRE_VERITY: u32 = 1 << 0;
const LCFS_MOUNT_FLAGS_READONLY: u32 = 1 << 1;
const LCFS_MOUNT_FLAGS_IDMAP: u32 = 1 << 3;
const LCFS_MOUNT_FLAGS_TRY_VERITY: u32 = 1 << 4;
const LCFS_MOUNT_FLAGS_MASK: u32 = (1 << 5) - 1;

/// `EWRONGVERITY` from lcfs-mount.h: the image's fs-verity digest doesn't
/// match `expected_fsverity_digest`.
const EWRONGVERITY: c_int = libc::EILSEQ;
/// `ENOVERITY` from lcfs-mount.h: the image has no fs-verity digest.
const ENOVERITY: c_int = libc::ENOTTY;
/// Longest digest accepted in `expected_fsverity_digest`, as in C.
const MAX_DIGEST_SIZE: usize = 64;

/// Checks the options like the C library does before mounting, returning
/// the parsed expected image digest, if any, or an errno.
///
/// The digest must be an even number of hex digits, of at most
/// [`MAX_DIGEST_SIZE`] bytes. An empty string parses to an empty digest,
/// which then never matches. That's deliberately stricter than C, which
/// treats an empty digest as no digest and mounts without checking.
///
/// # Safety
///
/// `expected_fsverity_digest` must be null or a valid C string.
unsafe fn validate_options(options: Option<&LcfsMountOptions>) -> Result<Option<Vec<u8>>, c_int> {
let Some(opts) = options else {
return Ok(None);
};
if opts.flags & !LCFS_MOUNT_FLAGS_MASK != 0
|| opts.upperdir.is_null() != opts.workdir.is_null()
|| (opts.flags & LCFS_MOUNT_FLAGS_IDMAP != 0 && opts.idmap_fd < 0)
{
return Err(libc::EINVAL);
}
if opts.expected_fsverity_digest.is_null() {
return Ok(None);
}
// SAFETY: non-null, and the caller guarantees it's a C string.
let digest_hex = unsafe { CStr::from_ptr(opts.expected_fsverity_digest) };
match hex::decode(digest_hex.to_bytes()) {
Ok(digest) if digest.len() <= MAX_DIGEST_SIZE => Ok(Some(digest)),
_ => Err(libc::EINVAL),
}
}

/// Checks the image's fs-verity digest (as measured by the kernel, like
/// the C library) against the expected one.
fn verify_image_digest(image: BorrowedFd<'_>, expected: &[u8]) -> Result<(), c_int> {
use composefs::fsverity::{MeasureVerityError, Sha256HashValue, measure_verity};
use zerocopy::IntoBytes;

let found = measure_verity::<Sha256HashValue>(image).map_err(|e| match e {
MeasureVerityError::VerityMissing | MeasureVerityError::FilesystemNotSupported => ENOVERITY,
MeasureVerityError::InvalidDigestAlgorithm { .. }
| MeasureVerityError::InvalidDigestSize { .. } => EWRONGVERITY,
MeasureVerityError::Io(e) => match e.raw_os_error() {
Some(libc::ENODATA | libc::EOPNOTSUPP | libc::ENOTTY) => ENOVERITY,
Some(errno) => errno,
None => libc::EIO,
},
})?;
if found.as_bytes() == expected {
Ok(())
} else {
Err(EWRONGVERITY)
}
}

/// Opens a directory given in the mount options.
///
/// # Safety
///
/// `path` must be a valid C string.
unsafe fn open_dir(path: *const c_char, flags: OFlags) -> Result<OwnedFd, c_int> {
let path = unsafe { CStr::from_ptr(path) };
open(
path,
flags | OFlags::DIRECTORY | OFlags::CLOEXEC,
Mode::empty(),
)
.map_err(|e| e.raw_os_error())
}

fn io_error_to_errno(e: &std::io::Error) -> c_int {
e.raw_os_error().unwrap_or(libc::EINVAL)
Expand All @@ -32,13 +116,19 @@ fn io_error_to_errno(e: &std::io::Error) -> c_int {
pub unsafe extern "C" fn lcfs_mount_image(
path: *const c_char,
mountpoint: *const c_char,
options: *mut LcfsMountOptions,
options: Option<&LcfsMountOptions>,
) -> c_int {
if path.is_null() || mountpoint.is_null() {
set_errno(libc::EINVAL);
return -1;
}

// Like C, reject bad options before touching the image.
if let Err(errno) = unsafe { validate_options(options) } {
set_errno(errno);
return -1;
}

unsafe {
let path_cstr = CStr::from_ptr(path);

Expand All @@ -61,13 +151,21 @@ pub unsafe extern "C" fn lcfs_mount_image(
pub unsafe extern "C" fn lcfs_mount_fd(
fd: c_int,
mountpoint: *const c_char,
options: *mut LcfsMountOptions,
options: Option<&LcfsMountOptions>,
) -> c_int {
if fd < 0 || mountpoint.is_null() {
set_errno(libc::EINVAL);
return -1;
}

let expected_digest = match unsafe { validate_options(options) } {
Ok(digest) => digest,
Err(errno) => {
set_errno(errno);
return -1;
}
};

unsafe {
let mountpoint_cstr = CStr::from_ptr(mountpoint);

Expand All @@ -77,51 +175,44 @@ pub unsafe extern "C" fn lcfs_mount_fd(
}
let image_fd = OwnedFd::from_raw_fd(dup_fd);

let erofs_fd = match composefs::mount::erofs_mount(image_fd) {
Ok(fd) => fd,
Err(e) => {
set_errno(io_error_to_errno(&e));
return -1;
}
};
// Callers such as ostree-prepare-root rely on this check to only
// mount the image they expect.
if let Some(expected) = expected_digest
&& let Err(errno) = verify_image_digest(image_fd.as_fd(), &expected)
{
set_errno(errno);
return -1;
}

let mut basedirs: Vec<CString> = Vec::new();
if !options.is_null() {
let opts = &*options;
if !opts.objdirs.is_null() && opts.n_objdirs > 0 {
for i in 0..opts.n_objdirs {
let dir_ptr = *opts.objdirs.add(i);
if !dir_ptr.is_null() {
basedirs.push(CStr::from_ptr(dir_ptr).to_owned());
}
if let Some(opts) = options
&& !opts.objdirs.is_null()
&& opts.n_objdirs > 0
{
for i in 0..opts.n_objdirs {
let dir_ptr = *opts.objdirs.add(i);
if !dir_ptr.is_null() {
basedirs.push(CStr::from_ptr(dir_ptr).to_owned());
}
}
}

let verity = if !options.is_null() {
let opts = &*options;
if (opts.flags & LCFS_MOUNT_FLAGS_REQUIRE_VERITY) != 0 {
composefs::mount::VerityRequirement::Required
} else if (opts.flags & LCFS_MOUNT_FLAGS_TRY_VERITY) != 0 {
composefs::mount::VerityRequirement::Try
} else {
composefs::mount::VerityRequirement::Disabled
}
let flags = options.map_or(0, |opts| opts.flags);
let verity = if (flags & LCFS_MOUNT_FLAGS_REQUIRE_VERITY) != 0 {
composefs::mount::VerityRequirement::Required
} else if (flags & LCFS_MOUNT_FLAGS_TRY_VERITY) != 0 {
composefs::mount::VerityRequirement::Try
} else {
composefs::mount::VerityRequirement::Disabled
};

if !basedirs.is_empty() {
let mut basedir_fds: Vec<OwnedFd> = Vec::new();
for dir in &basedirs {
match open(
dir.as_c_str(),
OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC,
Mode::empty(),
) {
match open_dir(dir.as_ptr(), OFlags::RDONLY) {
Ok(fd) => basedir_fds.push(fd),
Err(e) => {
set_errno(e.raw_os_error());
Err(errno) => {
set_errno(errno);
return -1;
}
}
Expand All @@ -130,8 +221,24 @@ pub unsafe extern "C" fn lcfs_mount_fd(
let borrowed: Vec<_> = basedir_fds.iter().map(|fd| fd.as_fd()).collect();
let mut mount_options = composefs::mount::MountOptions::default();

if !options.is_null() {
let opts = &*options;
if let Some(opts) = options {
// validate_options() checked that both or neither are set.
if !opts.upperdir.is_null() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done, as a separate prep commit "capi: Use Option for the lcfs_mount_fd() options" (ee7198d): lcfs_mount_fd() converts the pointer once with NonNull::new(options).map(|p| p.as_ref()) and the rest uses if let Some(opts); validate_options() now takes Option<&LcfsMountOptions>. The later commits changed only to fit it: 23fdafd -> 3338f44, 1d177f4 -> 0cd328a, 468778e -> a692d1e (the first two, 70efbf3 and 637bb78, are unchanged). Those lost your Signed-off-by for now; it will be restored with the new sign-off tool.

Generated-by: https://github.com/cgwalters/#llms

let dirs = open_dir(opts.upperdir, OFlags::PATH)
.and_then(|upper| Ok((upper, open_dir(opts.workdir, OFlags::PATH)?)));
match dirs {
Ok((upper, work)) => {
mount_options.set_overlay(upper, work);
}
Err(errno) => {
set_errno(errno);
return -1;
}
}
// As in C, a mount with an upper layer is writable
// unless asked otherwise.
mount_options.set_read_write(opts.flags & LCFS_MOUNT_FLAGS_READONLY == 0);
}
if (opts.flags & LCFS_MOUNT_FLAGS_IDMAP) != 0 && opts.idmap_fd >= 0 {
let dup_idmap = libc::dup(opts.idmap_fd);
if dup_idmap < 0 {
Expand All @@ -141,8 +248,9 @@ pub unsafe extern "C" fn lcfs_mount_fd(
}
}

// composefs_fsmount() mounts the EROFS image itself.
match composefs::mount::composefs_fsmount(
erofs_fd,
image_fd,
"composefs",
&borrowed,
verity,
Expand All @@ -160,6 +268,13 @@ pub unsafe extern "C" fn lcfs_mount_fd(
}
}
} else {
let erofs_fd = match composefs::mount::erofs_mount(image_fd) {
Ok(fd) => fd,
Err(e) => {
set_errno(io_error_to_errno(&e));
return -1;
}
};
if let Err(e) = composefs::mount::mount_at(&erofs_fd, CWD, mountpoint_cstr) {
set_errno(e.raw_os_error());
return -1;
Expand Down
Loading
Loading