Skip to content
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

wip: Install with fsverity #935

Draft
wants to merge 1 commit 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
10 changes: 9 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,17 @@ jobs:
uses: actions/checkout@v4
- name: Free up disk space on runner
run: sudo ./ci/clean-gha-runner.sh
- name: Enable fsverity for /
run: sudo tune2fs -O verity $(findmnt -vno SOURCE /)
- name: Install utils
run: sudo apt -y install fsverity
- name: Integration tests
run: |
set -xeu
# Build images to test; TODO investigate doing single container builds
# via GHA and pushing to a temporary registry to share among workflows?
sudo podman build -t localhost/bootc -f hack/Containerfile .
sudo podman build -t localhost/bootc-fsverity -f ci/Containerfile.install-fsverity
export CARGO_INCREMENTAL=0 # because we aren't caching the test runner bits
cargo build --release -p tests-integration
df -h /
Expand All @@ -84,8 +91,9 @@ jobs:
-v /run/dbus:/run/dbus -v /run/systemd:/run/systemd localhost/bootc /src/ostree-ext/ci/priv-integration.sh
# Nondestructive but privileged tests
sudo bootc-integration-tests host-privileged localhost/bootc
# Finally the install-alongside suite
# Install tests
sudo bootc-integration-tests install-alongside localhost/bootc
sudo bootc-integration-tests install-fsverity localhost/bootc-fsverity
docs:
if: ${{ contains(github.event.pull_request.labels.*.name, 'documentation') }}
runs-on: ubuntu-latest
Expand Down
74 changes: 66 additions & 8 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions ci/Containerfile.install-fsverity
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Enable fsverity at install time
FROM localhost/bootc
RUN <<EORUN
set -xeuo pipefail
mkdir -p /usr/lib/bootc/install
cat > /usr/lib/bootc/install/30-fsverity.toml <<EOF
[install]
fsverity = "enabled"
EOF
EORUN
2 changes: 2 additions & 0 deletions lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ ostree-ext = { path = "../ostree-ext" }
chrono = { workspace = true, features = ["serde"] }
clap = { workspace = true, features = ["derive","cargo"] }
clap_mangen = { workspace = true, optional = true }
#composefs = "0.2.0"
composefs = { git = "https://github.com/containers/composefs-rs", rev = "55ae2e9ba72f6afda4887d746e6b98f0a1875ac4" }
cap-std-ext = { workspace = true, features = ["fs_utf8"] }
hex = { workspace = true }
fn-error-context = { workspace = true }
Expand Down
26 changes: 26 additions & 0 deletions lib/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,16 @@ pub(crate) enum ImageOpts {
Cmd(ImageCmdOpts),
}

/// Options for consistency checking
#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
pub(crate) enum FsckOpts {
/// Check the state of fsverity on the ostree objects. Possible output:
/// "enabled" => All .file objects have fsverity
/// "disabled" => No .file objects have fsverity
/// "inconsistent" => Mixed state
OstreeVerity,
}

/// Hidden, internal only options
#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
pub(crate) enum InternalsOpts {
Expand All @@ -344,6 +354,8 @@ pub(crate) enum InternalsOpts {
FixupEtcFstab,
/// Should only be used by `make update-generated`
PrintJsonSchema,
/// Perform consistency checking.
Fsck,
/// Perform cleanup actions
Cleanup,
/// Proxy frontend for the `ostree-ext` CLI.
Expand Down Expand Up @@ -1051,6 +1063,20 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
)
.await
}
InternalsOpts::Fsck => {
let storage = get_storage().await?;
let r = crate::fsck::fsck(&storage).await?;
match r.errors.as_slice() {
[] => {}
errs => {
for err in errs {
eprintln!("error: {err}");
}
anyhow::bail!("fsck found errors");
}
}
Ok(())
}
InternalsOpts::FixupEtcFstab => crate::deploy::fixup_etc_fstab(&root),
InternalsOpts::PrintJsonSchema => {
let schema = schema_for!(crate::spec::Host);
Expand Down
128 changes: 128 additions & 0 deletions lib/src/fsck.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
//! # Write deployments merging image with configmap
//!
//! Create a merged filesystem tree with the image and mounted configmaps.

use std::os::fd::AsFd;
use std::str::FromStr as _;

use anyhow::Ok;
use anyhow::{Context, Result};
use camino::Utf8PathBuf;
use cap_std::fs::Dir;
use cap_std_ext::cap_std;
use fn_error_context::context;
use ostree_ext::keyfileext::KeyFileExt;
use ostree_ext::ostree;
use serde::{Deserialize, Serialize};

use crate::install::config::Tristate;
use crate::store::{self, Storage};

#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub(crate) enum VerityState {
Enabled,
Disabled,
Inconsistent,
}

#[derive(Default, Serialize, Deserialize, Debug, PartialEq, Eq)]
pub(crate) struct FsckResult {
pub(crate) notices: Vec<String>,
pub(crate) errors: Vec<String>,
pub(crate) verity: Option<VerityState>,
}

/// Check the fsverity state of all regular files in this object directory.
#[context("Computing verity state")]
fn verity_state_of_objects(d: &Dir) -> Result<(u64, u64)> {
let mut enabled = 0;
let mut disabled = 0;
for ent in d.entries()? {
let ent = ent?;
if !ent.file_type()?.is_file() {
continue;
}
let name = ent.file_name();
let name = name
.into_string()
.map(Utf8PathBuf::from)
.map_err(|_| anyhow::anyhow!("Invalid UTF-8"))?;
let Some("file") = name.extension() else {
continue;
};
let f = d
.open(&name)
.with_context(|| format!("Failed to open {name}"))?;
let r: Option<composefs::fsverity::Sha256HashValue> =
composefs::fsverity::ioctl::fs_ioc_measure_verity(f.as_fd())?;
drop(f);
if r.is_some() {
enabled += 1;
} else {
disabled += 1;
}
}
Ok((enabled, disabled))
}

async fn verity_state_of_all_objects(repo: &ostree::Repo) -> Result<(u64, u64)> {
const MAX_CONCURRENT: usize = 3;

let repodir = Dir::reopen_dir(&repo.dfd_borrow())?;

let mut joinset = tokio::task::JoinSet::new();
let mut results = Vec::new();

for ent in repodir.read_dir("objects")? {
while joinset.len() >= MAX_CONCURRENT {
results.push(joinset.join_next().await.unwrap()??);
}
let ent = ent?;
if !ent.file_type()?.is_dir() {
continue;
}
let objdir = ent.open_dir()?;
joinset.spawn_blocking(move || verity_state_of_objects(&objdir));
}

while let Some(output) = joinset.join_next().await {
results.push(output??);
}
let r = results.into_iter().fold((0, 0), |mut acc, v| {
acc.0 += v.0;
acc.1 += v.1;
acc
});
Ok(r)
}

pub(crate) async fn fsck(storage: &Storage) -> Result<FsckResult> {
let mut r = FsckResult::default();

let repo_config = storage.repo().config();
let verity_state = {
let (k, v) = store::REPO_VERITY_CONFIG.split_once('.').unwrap();
repo_config
.optional_string(k, v)?
.map(|v| Tristate::from_str(&v))
.transpose()?
.unwrap_or_default()
};

r.verity = match verity_state_of_all_objects(&storage.repo()).await? {
(0, 0) => None,
(_, 0) => Some(VerityState::Enabled),
(0, _) => Some(VerityState::Disabled),
_ => Some(VerityState::Inconsistent),
};
if matches!(&r.verity, &Some(VerityState::Inconsistent)) {
let inconsistent = "Inconsistent fsverity state".to_string();
match verity_state {
Tristate::Disabled | Tristate::Maybe => r.notices.push(inconsistent),
Tristate::Enabled => r.errors.push(inconsistent),
}
}
serde_json::to_writer(std::io::stdout().lock(), &r)?;
Ok(r)
}
Loading
Loading