Skip to content
Closed
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
7 changes: 7 additions & 0 deletions crates/composefs-capi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ 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 }
libc = "0.2"
Expand Down
158 changes: 142 additions & 16 deletions crates/composefs-capi/src/mount.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
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};
Expand All @@ -21,8 +21,98 @@ 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;

/// Parses a hex digest; None if it isn't 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.
fn parse_hex_digest(hex: &[u8]) -> Option<Vec<u8>> {
if !hex.len().is_multiple_of(2) || hex.len() / 2 > MAX_DIGEST_SIZE {
return None;
}
// Digit by digit: u8::from_str_radix() would also accept e.g. "+a".
let digit = |c: u8| char::from(c).to_digit(16);
hex.chunks(2)
.map(|pair| Some((digit(pair[0])? << 4 | digit(pair[1])?) as u8))
.collect()
}

/// Checks the options like the C library does before mounting, returning
/// the parsed expected image digest, if any, or an errno.
///
/// # Safety
///
/// `options` must be null or point to valid mount options.
unsafe fn validate_options(options: *const LcfsMountOptions) -> Result<Option<Vec<u8>>, c_int> {
let Some(opts) = (unsafe { options.as_ref() }) 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);
}
let hex = unsafe { CStr::from_ptr(opts.expected_fsverity_digest) };
parse_hex_digest(hex.to_bytes())
.map(Some)
.ok_or(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 @@ -39,6 +129,12 @@ pub unsafe extern "C" fn lcfs_mount_image(
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 Down Expand Up @@ -68,6 +164,14 @@ pub unsafe extern "C" fn lcfs_mount_fd(
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,13 +181,14 @@ 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() {
Expand Down Expand Up @@ -114,14 +219,10 @@ pub unsafe extern "C" fn lcfs_mount_fd(
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 @@ -132,6 +233,23 @@ pub unsafe extern "C" fn lcfs_mount_fd(

if !options.is_null() {
let opts = &*options;
// validate_options() checked that both or neither are set.
if !opts.upperdir.is_null() {
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 +259,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 +279,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
68 changes: 68 additions & 0 deletions crates/composefs-capi/tests/lcfs-mount-test.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/* Mount a composefs image with lcfs_mount_image(), the way C consumers
* such as ostree-prepare-root do. Used by the privileged integration
* tests to exercise the Rust libcomposefs.
*
* Usage: lcfs-mount-test [-d DIGEST] [-u UPPERDIR -w WORKDIR [-r]] IMAGE MOUNTPOINT OBJDIR
*
* The mount is read-only unless there's an upper directory; -r makes it
* read-only then too.
*
* On failure, prints the error and exits with errno as the status, so
* callers can check which error the library reported.
*
* SPDX-License-Identifier: MIT OR Apache-2.0
*/
#include <errno.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <libcomposefs/lcfs-mount.h>

int main(int argc, char **argv)
{
struct lcfs_mount_options_s options = { 0 };
const char *objdirs[1];
bool readonly = false;
int opt;

options.idmap_fd = -1;
options.flags = LCFS_MOUNT_FLAGS_READONLY;
while ((opt = getopt(argc, argv, "d:u:w:r")) != -1) {
switch (opt) {
case 'd':
options.expected_fsverity_digest = optarg;
break;
case 'u':
options.upperdir = optarg;
options.flags &= ~LCFS_MOUNT_FLAGS_READONLY;
break;
case 'w':
options.workdir = optarg;
break;
case 'r':
readonly = true;
break;
default:
fprintf(stderr, "usage: %s [-d DIGEST] [-u UPPERDIR -w WORKDIR [-r]] IMAGE MOUNTPOINT OBJDIR\n",
argv[0]);
return 2;
}
}
if (readonly)
options.flags |= LCFS_MOUNT_FLAGS_READONLY;
if (argc - optind != 3) {
fprintf(stderr, "expected IMAGE MOUNTPOINT OBJDIR\n");
return 2;
}

objdirs[0] = argv[optind + 2];
options.objdirs = objdirs;
options.n_objdirs = 1;
if (lcfs_mount_image(argv[optind], argv[optind + 1], &options) < 0) {
int errsv = errno;
fprintf(stderr, "lcfs_mount_image: %s\n", strerror(errsv));
return errsv;
}
return 0;
}
Loading