Skip to content

capi: Check expected_fsverity_digest and fix lcfs_mount_image() - #5

Closed
cgwalters-bot wants to merge 5 commits into
mainfrom
bot/capi-mount-fixes
Closed

cgwalters-bot wants to merge 5 commits into
mainfrom
bot/capi-mount-fixes

Conversation

@cgwalters-bot

@cgwalters-bot cgwalters-bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Collaborator

Security: lcfs_mount_fd() never read expected_fsverity_digest. The C library measures the image's fs-verity digest and refuses to mount on a mismatch (lcfs-mount.c lcfs_validate_verity_fd). ostree-prepare-root passes that digest for the deployment's composefs image, so on the Rust library its verification silently didn't happen. This PR checks the digest as C does: EWRONGVERITY (EILSEQ) on a mismatch, ENOVERITY (ENOTTY) when the image has no fs-verity, and EINVAL for an unparseable digest. The other options are validated up front, as in C.

It also fixes lcfs_mount_image()/lcfs_mount_fd() with object directories, which failed for every image:

  • The EROFS image was mounted twice, and the second mount got ENOTBLK.
  • The capi crate is built on its own, so it never enabled the pre-6.15 compat code, and 6.12 kernels rejected the mount with EBADF. It now enables it by default, like cfsctl and composefs-setup-root.

Also: upperdir/workdir are now passed through (ostree uses them for a transient root), and the mount is writable unless LCFS_MOUNT_FLAGS_READONLY is set, as in C.

New privileged integration tests run lcfs-mount-test, a small C program built into the test image against our libcomposefs. It mounts an image from a verity-enabled repository the way ostree-prepare-root does and reads back an inline and an external file, with and without the correct digest. The tests also cover wrong, unparseable and +-prefixed digests, an image without verity, and an upper layer that is writable, or read-only with LCFS_MOUNT_FLAGS_READONLY. An empty digest never matches, which is deliberately stricter than C (C skips the check).

Tested on a RHEL 10 devspace (6.12):

  • just test-integration-vm capi --test-threads=1 (CentOS Stream 10 VMs): 4/4 pass. The same tests as root on the host pass against the system C libcomposefs too, so the errors match C. On main, all 4 fail.
  • cargo test -p composefs-capi, just test-capi, clippy -D warnings and cargo fmt --check pass.

Not done here, follow-ups:

  • image_mountdir is still ignored. On pre-6.15 kernels, the temporary EROFS mount goes into a tempfile directory instead of the caller's path (ostree passes /run/ostree/.private/cfsroot-lower). Fixing that needs a composefs::mount::MountOptions field threaded into prepare_mount().
  • n_objdirs == 0 still mounts the bare EROFS image, where C returns EINVAL.
  • The .file redirect bug in convert.rs is separate. The ostree test in bootc: Test ostree against the Rust libcomposefs #4 still fails on it.

Related: composefs#323

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


Review draft in cgwalters-forge, not upstream yet. This section is removed when the PR is opened upstream.

  • Upstream: composefs/composefs-rs, base main
  • Board item: PVTI_lAHOAQ_SPs4Bj2Gizg8wsH8
  • Fork CI: off; the devspace testing described above is this PR's CI, and upstream CI runs once it is opened there

To review:

  • Approve, or comment /promote on a line of its own, to open it upstream, ready for review. Either covers only the commits pushed so far.
  • Add a /draft line (in the same comment or before) to open it upstream as a draft (/ready undoes that).
  • Close to drop it.
  • Edit the title and description freely: they become the upstream PR's. Review comments are squashed into the commits they concern, with a reply here.

libcomposefs is built on its own (`make install-capi`, which the RPM
spec's `make install` runs), so it never gets the kernel compat
features cfsctl and composefs-setup-root enable by default. Without
them, lcfs_mount_image() passes the detached EROFS mount to overlayfs
by fd, which kernels before 6.15 reject with EBADF. ostree-prepare-root
on CentOS Stream 10 (6.12) fails to mount the deployment's composefs
image that way.

The C library works on older kernels at runtime; give the Rust one the
same features and defaults as the other binaries.

Generated-by: AI
With object directories, lcfs_mount_fd() mounted the EROFS image and
then passed that mount to composefs_fsmount(), which expects the image
file and mounts it again. The second mount gets a directory as its
source and fails with ENOTBLK, so every composefs mount with a
basedir failed. ostree-prepare-root hits this on boot ("composefs:
failed to mount: Block device required") and drops to the emergency
shell.

Only mount the EROFS image ourselves when there's no overlay on top.

Generated-by: AI
lcfs_mount_fd() ignored expected_fsverity_digest and mounted whatever
image it was given. The C library measures the image's fs-verity digest
and refuses to mount it on a mismatch, and callers rely on that:
ostree-prepare-root passes the digest of the deployment's composefs
image, so with our library its verification silently didn't happen.

Check it the way C does: the kernel's sha256 fs-verity measurement of
the image must equal the expected digest, failing with EWRONGVERITY
(EILSEQ) if it doesn't and ENOVERITY (ENOTTY) if the image has no
fs-verity. Also validate the options up front as C does, so an
unparseable digest, unknown flags, an upperdir without a workdir (or
the reverse) and IDMAP without an fd fail with EINVAL before anything
is opened or mounted.

Generated-by: AI
lcfs_mount_fd() ignored upperdir and workdir, so a caller asking for a
writable composefs (ostree does, for a transient root) got a read-only
mount without its upper layer. Pass them to the overlayfs mount, and
like C make the mount writable unless LCFS_MOUNT_FLAGS_READONLY is set.

Generated-by: AI
Nothing tested mounting through the C API: the capi CI job runs the
C test suite, which doesn't mount anything. So lcfs_mount_image()
failed for every image with an object directory, and ignored
expected_fsverity_digest, without any test noticing.

The test image now also builds our libcomposefs and lcfs-mount-test,
a small C program that mounts an image the way ostree-prepare-root
does. The tests mount an image from a verity-enabled repository and
read back an inline and an external file, with and without the
correct digest; check the errors for a wrong digest, an unparseable
one and an image without fs-verity; and check that an upper layer
is writable, or read-only with LCFS_MOUNT_FLAGS_READONLY.

Generated-by: AI
@cgwalters-bot

Copy link
Copy Markdown
Collaborator Author

Opened upstream as composefs#401. Closing this review draft.

@cgwalters-bot

Copy link
Copy Markdown
Collaborator Author

Signed off 5 commit(s) with Signed-off-by: Colin Walters <walters@verbum.org> for composefs/composefs-rs's DCO check, on cgwalters's approval #5 (review) of f1752b514c16. Same trees; the head is now 468778ea6d5d.

@cgwalters-bot

Copy link
Copy Markdown
Collaborator Author

Signed off 1 commit(s) with Signed-off-by: Colin Walters <walters@verbum.org> for composefs/composefs-rs's DCO check, on cgwalters's approval composefs#401 (review) of c8345cba0048. Same trees; the head is now 5dbc8e127717.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants