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
13 changes: 11 additions & 2 deletions .github/workflows/bootc-revdep.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,19 @@ permissions:

jobs:
bootc-test:
name: Build and test bootc with local composefs-rs
name: bootc with local composefs-rs (${{ matrix.name }})
if: never()
runs-on: ubuntu-24.04
timeout-minutes: 120
strategy:
fail-fast: false
matrix:
include:
- name: composefs backend
recipe: test
# ostree's composefs support running on our libcomposefs
- name: ostree backend
recipe: test-ostree

steps:
- name: Checkout repository
Expand All @@ -44,4 +53,4 @@ jobs:
echo "$HOME/.local/bin" >> $GITHUB_PATH

- name: Build and test bootc with local composefs-rs
run: just bootc/test
run: just bootc/${{ matrix.recipe }}
94 changes: 84 additions & 10 deletions bootc/Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,17 @@ export COMPOSEFS_BOOTC_REF := env("COMPOSEFS_BOOTC_REF", "main")
# Remote repository for bootc
export COMPOSEFS_BOOTC_REPO := env("COMPOSEFS_BOOTC_REPO", "https://github.com/bootc-dev/bootc")

# Buildroot image for `package-composefs`; its distribution must match
# bootc's base image (CentOS Stream 10 by default).
export COMPOSEFS_BOOTC_BUILDROOT := env("COMPOSEFS_BOOTC_BUILDROOT", "quay.io/centos/centos:stream10")

# Internal: absolute path to the composefs-rs checkout (parent of this Justfile)
export _COMPOSEFS_SRC := canonicalize(source_directory() + "/..")
# Internal: scratch clone and output directory for `package-composefs`
_composefs_rpm_src := COMPOSEFS_BOOTC_PATH + "/target/composefs-rpm-src"
_composefs_rpms := COMPOSEFS_BOOTC_PATH + "/target/composefs-packages"
# Internal: bootc's default output image name
_bootc_image := env("BOOTC_image", "localhost/bootc")

# Clone or update bootc repository
clone:
Expand Down Expand Up @@ -47,19 +56,10 @@ clone:
# the bind-mounted local source instead of fetching from git.
#
# Errors if the composefs-rs tree has uncommitted changes.
patch: clone
patch: clone _require-clean
#!/bin/bash
set -euo pipefail

# Require a clean composefs-rs working tree so we test a real commit.
# Only tracked files matter; untracked files are allowed.
# git diff HEAD already excludes untracked files.
if ! git -C "$_COMPOSEFS_SRC" diff --quiet HEAD 2>/dev/null; then
echo "error: composefs-rs has uncommitted changes — commit or stash first" >&2
git -C "$_COMPOSEFS_SRC" diff --stat HEAD >&2
exit 1
fi

cfs_path="$_COMPOSEFS_SRC/crates/composefs-ctl"

cd "$COMPOSEFS_BOOTC_PATH"
Expand Down Expand Up @@ -92,6 +92,79 @@ patch: clone
sed -i "s/^# Patched by composefs-rs.*/# Patched by composefs-rs at ${_rev}/" Cargo.toml
echo "bootc patched for composefs-rs at ${_rev}"

# Require a clean composefs-rs working tree so we test a real commit.
# Only tracked files matter; untracked files are allowed.
_require-clean:
#!/bin/bash
set -euo pipefail
# git diff HEAD already excludes untracked files.
if ! git -C "$_COMPOSEFS_SRC" diff --quiet HEAD 2>/dev/null; then
echo "error: composefs-rs has uncommitted changes — commit or stash first" >&2
git -C "$_COMPOSEFS_SRC" diff --stat HEAD >&2
exit 1
fi

# Build the composefs RPMs from the HEAD commit of this checkout, in a
# buildroot matching bootc's base image.
package-composefs: clone _require-clean
#!/bin/bash
set -euo pipefail
rm -rf "{{_composefs_rpm_src}}" "{{_composefs_rpms}}"
mkdir -p "{{_composefs_rpms}}"
# A scratch clone, so this also works from a git worktree and
# leaves the checkout's target/ alone.
git clone -q "$_COMPOSEFS_SRC" "{{_composefs_rpm_src}}"
podman run --rm --security-opt=label=disable \
-v "{{_composefs_rpm_src}}:/src" -v "{{_composefs_rpms}}:/out" \
"$COMPOSEFS_BOOTC_BUILDROOT" /src/bootc/build-composefs-rpms /src /out
ls -l "{{_composefs_rpms}}"

# Build a bootc image (ostree backend) whose composefs packages come from
# this checkout, replacing the distribution's C composefs. ostree then
# generates and mounts composefs images with the Rust libcomposefs.
build-ostree: patch package-composefs
#!/bin/bash
set -euo pipefail
cd "$COMPOSEFS_BOOTC_PATH"
export BOOTC_variant=ostree
# bootc's `package` recreates target/packages, so add ours after it.
# bootc installs every RPM there from a local repository that takes
# priority over the distribution's.
just package
cp -v "{{_composefs_rpms}}"/*.rpm target/packages/
BOOTC_SKIP_PACKAGE=1 just build
# Check that the libcomposefs ostree loads is ours, in the image and
# in the initramfs (where ostree-prepare-root mounts the composefs).
podman run --rm --security-opt=label=disable \
-v "{{_composefs_rpms}}:/run/composefs-packages:ro" "{{_bootc_image}}" bash -c '
set -euo pipefail
lib=$(ldd /usr/lib64/libostree-1.so.1 | awk "/libcomposefs\\.so/ { print \$3 }")
lib=$(readlink -f "$lib")
owner=$(rpm -qf "$lib")
expected=$(rpm -qp /run/composefs-packages/composefs-libs-[0-9]*.rpm)
echo "libostree uses $lib from $owner"
if [ "$owner" != "$expected" ]; then
echo "error: expected libcomposefs from $expected" >&2
exit 1
fi
initramfs=$(ls /usr/lib/modules/*/initramfs.img)
tmp=$(mktemp -d)
(cd "$tmp" && lsinitrd --unpack "$initramfs" "${lib#/}" 2>/dev/null)
if ! cmp "$lib" "$tmp/$lib"; then
echo "error: the initramfs has a different $lib" >&2
exit 1
fi
echo "initramfs has the same $lib"'

# Run bootc's ostree backend tests on the image from `build-ostree`: a
# bootc install and an upgrade with a reboot, so ostree writes composefs
# images and ostree-prepare-root mounts them through the Rust libcomposefs.
test-ostree *plans="readonly image-upgrade-reboot": build-ostree
#!/bin/bash
set -euo pipefail
cd "$COMPOSEFS_BOOTC_PATH"
BOOTC_variant=ostree BOOTC_SKIP_PACKAGE=1 just test-tmt {{plans}}

# Build sealed bootc image using local composefs-rs
# The path dependency is auto-detected and bind-mounted by bootc's Justfile
build: patch
Expand Down Expand Up @@ -165,6 +238,7 @@ config:
just bootc/test # Default: systemd + ext4 + uki + sealed
just bootc/test grub ext4 bls unsealed # grub + ext4 + BLS (unsealed)
just bootc/test systemd btrfs uki sealed # systemd-boot + btrfs + UKI (sealed)
just bootc/test-ostree # ostree backend with this libcomposefs

Example Usage:
just bootc/build # Clone main, patch, and build
Expand Down
39 changes: 39 additions & 0 deletions bootc/build-composefs-rpms
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/bin/bash
# Build the composefs RPMs (libcomposefs, mkcomposefs, mount.composefs, ...)
# the same way Packit does: pack.sh generates the source and vendor
# tarballs, then rpmbuild builds contrib/packaging/composefs.spec.
#
# Runs inside a buildroot container matching the bootc base image (see
# `package-composefs` in the Justfile) on a scratch clone of this
# repository, so the resulting packages can replace the distribution's
# C composefs in the bootc test image.
#
# Usage: build-composefs-rpms SRC OUTDIR
set -xeuo pipefail

src=$1
out=$2

dnf -y install cargo rust gcc git make openssl-devel pkgconf-pkg-config \
rpm-build zlib-devel zstd
git config --global --add safe.directory "$src"
cd "$src"

# Plain `cargo vendor` with the distribution's cargo, rather than
# installing cargo-vendor-filterer as the release workflow does.
PACK_VENDOR=cargo contrib/packaging/pack.sh

# Man pages need pandoc, and %autochangelog rpmautospec, neither of
# which CentOS Stream ships; the changelog doesn't matter here, and
# neither do debuginfo packages (the spec's release build has no
# debug info to split out).
topdir=$(mktemp -d)
# Offline, so the build must use the vendored crates, as in Koji.
CARGO_NET_OFFLINE=true rpmbuild -bb --without man \
--define "autochangelog %{nil}" \
--define "debug_package %{nil}" \
--define "_topdir ${topdir}" \
--define "_sourcedir ${src}/target" \
target/composefs.spec

cp "${topdir}"/RPMS/*/*.rpm "$out"/
29 changes: 25 additions & 4 deletions contrib/packaging/pack.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,33 @@ echo "Version: ${VERSION}"
# Source tarball from git
git archive --format=tar --prefix="${PREFIX}" -o "${TAR}" HEAD

# Vendor tarball via cargo-vendor-filterer
VENDOR_CONFIG=$(cargo vendor-filterer --prefix=vendor --format=tar.zstd "${VENDORTAR}")

# Fix the vendor config to use a relative "vendor" directory
TMPDIR=$(mktemp -d -p target)
trap 'rm -rf "${TMPDIR}"' EXIT

# Vendor tarball via cargo-vendor-filterer, or with PACK_VENDOR=cargo via
# plain `cargo vendor` (larger, as it keeps all platforms, but needs no
# extra tools).
case "${PACK_VENDOR:-filterer}" in
filterer)
VENDOR_CONFIG=$(cargo vendor-filterer --prefix=vendor --format=tar.zstd "${VENDORTAR}")
;;
cargo)
VENDOR_CONFIG=$(cargo vendor "${TMPDIR}/vendor")
tar -C "${TMPDIR}" --zstd -cf "${VENDORTAR}" vendor
rm -rf "${TMPDIR}/vendor"
;;
*)
echo "error: unknown PACK_VENDOR=${PACK_VENDOR}" >&2
exit 1
;;
esac

if test -z "${VENDOR_CONFIG}"; then
echo "error: vendoring printed no source replacement config" >&2
exit 1
fi

# Fix the vendor config to use a relative "vendor" directory
echo "${VENDOR_CONFIG}" | sed 's|^directory = ".*"|directory = "vendor"|' > "${TMPDIR}/vendor-config.toml"

# Embed .cargo/vendor-config.toml into the source tarball
Expand Down
40 changes: 28 additions & 12 deletions crates/composefs-boot/src/android_boot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
//! This module provides functionality to parse Android boot image format version 2 files
//! and extract embedded components like kernel, initrd, commandline and dtb.

use std::ffi::CStr;
use std::io::{Read, Seek, SeekFrom};
use thiserror::Error;
use zerocopy::{
Expand Down Expand Up @@ -115,12 +116,12 @@ impl AndroidBootImage {
}

// mkbootimg splits long command lines (with a null terminator for each)
let primary_len = nul_terminated_len(&header.cmdline);
let extra_len = nul_terminated_len(&header.extra_cmdline);
let primary = nul_terminated_bytes(&header.cmdline);
let extra = nul_terminated_bytes(&header.extra_cmdline);
let mut cmdline = [0; TOTAL_CMDLINE_SIZE];
cmdline[..primary_len].copy_from_slice(&header.cmdline[..primary_len]);
cmdline[primary_len..primary_len + extra_len]
.copy_from_slice(&header.extra_cmdline[..extra_len]);
for (dst, src) in cmdline.iter_mut().zip(primary.iter().chain(extra)) {
*dst = *src;
}

Ok(Self {
page_size,
Expand Down Expand Up @@ -179,16 +180,12 @@ impl AndroidBootImage {

/// Return the kernel command line stored in the image header.
pub fn cmdline(&self) -> Result<&str, AndroidBootError> {
let end = nul_terminated_len(&self.cmdline);
Ok(std::str::from_utf8(&self.cmdline[..end])?)
Ok(std::str::from_utf8(nul_terminated_bytes(&self.cmdline))?)
}
}

fn nul_terminated_len(bytes: &[u8]) -> usize {
bytes
.iter()
.position(|byte| *byte == 0)
.unwrap_or(bytes.len())
fn nul_terminated_bytes(bytes: &[u8]) -> &[u8] {
CStr::from_bytes_until_nul(bytes).map_or(bytes, CStr::to_bytes)
}

fn add_aligned(
Expand Down Expand Up @@ -305,6 +302,25 @@ pub(crate) mod tests {
Ok(())
}

#[test]
fn parses_cmdline_without_nul_terminators() -> Result<(), AndroidBootError> {
let mut bytes = image(b"kernel", b"ramdisk", b"");
let header = BootImageHeaderV2::mut_from_bytes(&mut bytes[..HEADER_SIZE])
.map_err(|_| AndroidBootError::InvalidHeader)?;
header.cmdline.fill(b'x');
header.extra_cmdline.fill(b'y');

let image = AndroidBootImage::parse(&mut Cursor::new(bytes))?;
let expected = format!(
"{}{}",
"x".repeat(CMDLINE_SIZE),
"y".repeat(EXTRA_CMDLINE_SIZE)
);
assert_eq!(image.cmdline()?, expected);
assert!(CStr::from_bytes_until_nul(&image.cmdline).is_err());
Ok(())
}

#[test]
fn rejects_non_utf8_cmdline() -> Result<(), AndroidBootError> {
let bytes = image_with_cmdline(b"kernel", b"ramdisk", b"", &[0xff]);
Expand Down
6 changes: 3 additions & 3 deletions crates/composefs-boot/src/selabel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -336,9 +336,9 @@ pub fn open_file<H: FsVerityHashValue>(
match dir.get_file_opt(filename.as_ref())? {
Some(file) => match file {
RegularFile::Inline(data) => Ok(Some(Box::new(Cursor::new(data.clone())))),
RegularFile::External(id, ..) | RegularFile::ExternalNoVerity(id, ..) => {
Ok(Some(Box::new(File::from(repo.open_object(id)?))))
}
RegularFile::External(..) | RegularFile::ExternalPath { .. } => Ok(Some(Box::new(
File::from(repo.open_object(&file.repo_object_id()?)?),
))),
RegularFile::Sparse(..) => Ok(None),
},
None => Ok(None),
Expand Down
1 change: 1 addition & 0 deletions crates/composefs-capi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ rhel9 = ['composefs/rhel9']
composefs = { workspace = true }
hex = { version = "0.4.0", default-features = false, features = ["std"] }
libc = "0.2"
log = { version = "0.4", default-features = false }
anyhow = "1"
rustix = { version = "1", features = ["fs", "mm", "process", "mount"] }
zerocopy = "0.8"
Expand Down
1 change: 1 addition & 0 deletions crates/composefs-capi/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ fn main() {

cc::Build::new()
.file("tests/test_lcfs.c")
.file("tests/ostree-image.c")
.include("include/libcomposefs")
.warnings(false)
.compile("test_lcfs_c");
Expand Down
Loading