Skip to content

Commit 11e5c46

Browse files
committed
wip: Install with fsverity
Signed-off-by: Colin Walters <[email protected]>
1 parent 33444ce commit 11e5c46

File tree

13 files changed

+348
-23
lines changed

13 files changed

+348
-23
lines changed

.github/workflows/ci.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,17 @@ jobs:
6969
uses: actions/checkout@v4
7070
- name: Free up disk space on runner
7171
run: sudo ./ci/clean-gha-runner.sh
72+
- name: Enable fsverity for /
73+
run: sudo tune2fs -O verity $(findmnt -vno SOURCE /)
74+
- name: Install utils
75+
run: sudo apt -y install fsverity
7276
- name: Integration tests
7377
run: |
7478
set -xeu
79+
# Build images to test; TODO investigate doing single container builds
80+
# via GHA and pushing to a temporary registry to share among workflows?
7581
sudo podman build -t localhost/bootc -f hack/Containerfile .
82+
sudo podman build -t localhost/bootc-fsverity -f ci/Containerfile.install-fsverity
7683
export CARGO_INCREMENTAL=0 # because we aren't caching the test runner bits
7784
cargo build --release -p tests-integration
7885
df -h /
@@ -84,8 +91,9 @@ jobs:
8491
-v /run/dbus:/run/dbus -v /run/systemd:/run/systemd localhost/bootc /src/ostree-ext/ci/priv-integration.sh
8592
# Nondestructive but privileged tests
8693
sudo bootc-integration-tests host-privileged localhost/bootc
87-
# Finally the install-alongside suite
94+
# Install tests
8895
sudo bootc-integration-tests install-alongside localhost/bootc
96+
sudo bootc-integration-tests install-fsverity localhost/bootc-fsverity
8997
docs:
9098
if: ${{ contains(github.event.pull_request.labels.*.name, 'documentation') }}
9199
runs-on: ubuntu-latest

Cargo.lock

Lines changed: 66 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ci/Containerfile.install-fsverity

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Enable fsverity at install time
2+
FROM localhost/bootc
3+
RUN <<EORUN
4+
set -xeuo pipefail
5+
mkdir -p /usr/lib/bootc/install
6+
cat > /usr/lib/bootc/install/30-fsverity.toml <<EOF
7+
[install]
8+
fsverity = "enabled"
9+
EOF
10+
EORUN

lib/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ ostree-ext = { path = "../ostree-ext" }
2222
chrono = { workspace = true, features = ["serde"] }
2323
clap = { workspace = true, features = ["derive","cargo"] }
2424
clap_mangen = { workspace = true, optional = true }
25+
#composefs = "0.2.0"
26+
composefs = { git = "https://github.com/containers/composefs-rs", rev = "55ae2e9ba72f6afda4887d746e6b98f0a1875ac4" }
2527
cap-std-ext = { workspace = true, features = ["fs_utf8"] }
2628
hex = { workspace = true }
2729
fn-error-context = { workspace = true }

lib/src/cli.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,16 @@ pub(crate) enum ImageOpts {
359359
Cmd(ImageCmdOpts),
360360
}
361361

362+
/// Options for consistency checking
363+
#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
364+
pub(crate) enum FsckOpts {
365+
/// Check the state of fsverity on the ostree objects. Possible output:
366+
/// "enabled" => All .file objects have fsverity
367+
/// "disabled" => No .file objects have fsverity
368+
/// "inconsistent" => Mixed state
369+
OstreeVerity,
370+
}
371+
362372
/// Hidden, internal only options
363373
#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
364374
pub(crate) enum InternalsOpts {
@@ -372,6 +382,8 @@ pub(crate) enum InternalsOpts {
372382
FixupEtcFstab,
373383
/// Should only be used by `make update-generated`
374384
PrintJsonSchema,
385+
/// Perform consistency checking.
386+
Fsck,
375387
/// Perform cleanup actions
376388
Cleanup,
377389
/// Proxy frontend for the `ostree-ext` CLI.
@@ -1089,6 +1101,20 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
10891101
)
10901102
.await
10911103
}
1104+
InternalsOpts::Fsck => {
1105+
let storage = get_storage().await?;
1106+
let r = crate::fsck::fsck(&storage).await?;
1107+
match r.errors.as_slice() {
1108+
[] => {}
1109+
errs => {
1110+
for err in errs {
1111+
eprintln!("error: {err}");
1112+
}
1113+
anyhow::bail!("fsck found errors");
1114+
}
1115+
}
1116+
Ok(())
1117+
}
10921118
InternalsOpts::FixupEtcFstab => crate::deploy::fixup_etc_fstab(&root),
10931119
InternalsOpts::PrintJsonSchema => {
10941120
let schema = schema_for!(crate::spec::Host);

lib/src/fsck.rs

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
//! # Write deployments merging image with configmap
2+
//!
3+
//! Create a merged filesystem tree with the image and mounted configmaps.
4+
5+
use std::os::fd::AsFd;
6+
use std::str::FromStr as _;
7+
8+
use anyhow::Ok;
9+
use anyhow::{Context, Result};
10+
use camino::Utf8PathBuf;
11+
use cap_std::fs::Dir;
12+
use cap_std_ext::cap_std;
13+
use fn_error_context::context;
14+
use ostree_ext::keyfileext::KeyFileExt;
15+
use ostree_ext::ostree;
16+
use serde::{Deserialize, Serialize};
17+
18+
use crate::install::config::Tristate;
19+
use crate::store::{self, Storage};
20+
21+
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
22+
#[serde(rename_all = "kebab-case")]
23+
pub(crate) enum VerityState {
24+
Enabled,
25+
Disabled,
26+
Inconsistent,
27+
}
28+
29+
#[derive(Default, Serialize, Deserialize, Debug, PartialEq, Eq)]
30+
pub(crate) struct FsckResult {
31+
pub(crate) notices: Vec<String>,
32+
pub(crate) errors: Vec<String>,
33+
pub(crate) verity: Option<VerityState>,
34+
}
35+
36+
/// Check the fsverity state of all regular files in this object directory.
37+
#[context("Computing verity state")]
38+
fn verity_state_of_objects(d: &Dir) -> Result<(u64, u64)> {
39+
let mut enabled = 0;
40+
let mut disabled = 0;
41+
for ent in d.entries()? {
42+
let ent = ent?;
43+
if !ent.file_type()?.is_file() {
44+
continue;
45+
}
46+
let name = ent.file_name();
47+
let name = name
48+
.into_string()
49+
.map(Utf8PathBuf::from)
50+
.map_err(|_| anyhow::anyhow!("Invalid UTF-8"))?;
51+
let Some("file") = name.extension() else {
52+
continue;
53+
};
54+
let f = d
55+
.open(&name)
56+
.with_context(|| format!("Failed to open {name}"))?;
57+
let r: Option<composefs::fsverity::Sha256HashValue> =
58+
composefs::fsverity::ioctl::fs_ioc_measure_verity(f.as_fd())?;
59+
drop(f);
60+
if r.is_some() {
61+
enabled += 1;
62+
} else {
63+
disabled += 1;
64+
}
65+
}
66+
Ok((enabled, disabled))
67+
}
68+
69+
async fn verity_state_of_all_objects(repo: &ostree::Repo) -> Result<(u64, u64)> {
70+
const MAX_CONCURRENT: usize = 3;
71+
72+
let repodir = Dir::reopen_dir(&repo.dfd_borrow())?;
73+
74+
let mut joinset = tokio::task::JoinSet::new();
75+
let mut results = Vec::new();
76+
77+
for ent in repodir.read_dir("objects")? {
78+
while joinset.len() >= MAX_CONCURRENT {
79+
results.push(joinset.join_next().await.unwrap()??);
80+
}
81+
let ent = ent?;
82+
if !ent.file_type()?.is_dir() {
83+
continue;
84+
}
85+
let objdir = ent.open_dir()?;
86+
joinset.spawn_blocking(move || verity_state_of_objects(&objdir));
87+
}
88+
89+
while let Some(output) = joinset.join_next().await {
90+
results.push(output??);
91+
}
92+
let r = results.into_iter().fold((0, 0), |mut acc, v| {
93+
acc.0 += v.0;
94+
acc.1 += v.1;
95+
acc
96+
});
97+
Ok(r)
98+
}
99+
100+
pub(crate) async fn fsck(storage: &Storage) -> Result<FsckResult> {
101+
let mut r = FsckResult::default();
102+
103+
let repo_config = storage.repo().config();
104+
let verity_state = {
105+
let (k, v) = store::REPO_VERITY_CONFIG.split_once('.').unwrap();
106+
repo_config
107+
.optional_string(k, v)?
108+
.map(|v| Tristate::from_str(&v))
109+
.transpose()?
110+
.unwrap_or_default()
111+
};
112+
113+
r.verity = match verity_state_of_all_objects(&storage.repo()).await? {
114+
(0, 0) => None,
115+
(_, 0) => Some(VerityState::Enabled),
116+
(0, _) => Some(VerityState::Disabled),
117+
_ => Some(VerityState::Inconsistent),
118+
};
119+
if matches!(&r.verity, &Some(VerityState::Inconsistent)) {
120+
let inconsistent = "Inconsistent fsverity state".to_string();
121+
match verity_state {
122+
Tristate::Disabled | Tristate::Maybe => r.notices.push(inconsistent),
123+
Tristate::Enabled => r.errors.push(inconsistent),
124+
}
125+
}
126+
serde_json::to_writer(std::io::stdout().lock(), &r)?;
127+
Ok(r)
128+
}

0 commit comments

Comments
 (0)