Skip to content

Commit 8379be7

Browse files
committed
wip: Install with fsverity
Signed-off-by: Colin Walters <walters@verbum.org>
1 parent c4f3e33 commit 8379be7

File tree

13 files changed

+328
-26
lines changed

13 files changed

+328
-26
lines changed

.github/workflows/ci.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,17 @@ jobs:
7171
run: sudo rm -f /bin/skopeo /usr/bin/skopeo
7272
- name: Free up disk space on runner
7373
run: sudo ./ci/clean-gha-runner.sh
74+
- name: Enable fsverity for /
75+
run: sudo tune2fs -O verity $(findmnt -vno SOURCE /)
76+
- name: Install utils
77+
run: sudo apt -y install fsverity
7478
- name: Integration tests
7579
run: |
7680
set -xeu
81+
# Build images to test; TODO investigate doing single container builds
82+
# via GHA and pushing to a temporary registry to share among workflows?
7783
sudo podman build -t localhost/bootc -f hack/Containerfile .
84+
sudo podman build -t localhost/bootc-fsverity -f ci/Containerfile.install-fsverity
7885
export CARGO_INCREMENTAL=0 # because we aren't caching the test runner bits
7986
cargo build --release -p tests-integration
8087
df -h /
@@ -83,8 +90,9 @@ jobs:
8390
df -h /
8491
# Nondestructive but privileged tests
8592
sudo bootc-integration-tests host-privileged localhost/bootc
86-
# Finally the install-alongside suite
93+
# Install tests
8794
sudo bootc-integration-tests install-alongside localhost/bootc
95+
sudo bootc-integration-tests install-fsverity localhost/bootc-fsverity
8896
docs:
8997
if: ${{ contains(github.event.pull_request.labels.*.name, 'documentation') }}
9098
runs-on: ubuntu-latest

Cargo.lock

Lines changed: 46 additions & 11 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
@@ -23,6 +23,8 @@ ostree-ext = { path = "../ostree-ext" }
2323
chrono = { workspace = true, features = ["serde"] }
2424
clap = { workspace = true, features = ["derive","cargo"] }
2525
clap_mangen = { workspace = true, optional = true }
26+
#composefs = "0.2.0"
27+
composefs = { path = "../../composefs-rs" }
2628
cap-std-ext = { workspace = true, features = ["fs_utf8"] }
2729
hex = { workspace = true }
2830
fn-error-context = { workspace = true }

lib/src/cli.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,16 @@ pub(crate) enum ImageOpts {
280280
Cmd(ImageCmdOpts),
281281
}
282282

283+
/// Options for consistency checking
284+
#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
285+
pub(crate) enum FsckOpts {
286+
/// Check the state of fsverity on the ostree objects. Possible output:
287+
/// "enabled" => All .file objects have fsverity
288+
/// "disabled" => No .file objects have fsverity
289+
/// "inconsistent" => Mixed state
290+
OstreeVerity,
291+
}
292+
283293
/// Hidden, internal only options
284294
#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
285295
pub(crate) enum InternalsOpts {
@@ -293,6 +303,8 @@ pub(crate) enum InternalsOpts {
293303
FixupEtcFstab,
294304
/// Should only be used by `make update-generated`
295305
PrintJsonSchema,
306+
/// Perform consistency checking.
307+
Fsck,
296308
/// Perform cleanup actions
297309
Cleanup,
298310
/// Proxy frontend for the `ostree-ext` CLI.
@@ -952,6 +964,20 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
952964
)
953965
.await
954966
}
967+
InternalsOpts::Fsck => {
968+
let storage = get_storage().await?;
969+
let r = crate::fsck::fsck(&storage).await?;
970+
match r.errors.as_slice() {
971+
[] => {}
972+
errs => {
973+
for err in errs {
974+
eprintln!("error: {err}");
975+
}
976+
anyhow::bail!("fsck found errors");
977+
}
978+
}
979+
Ok(())
980+
}
955981
InternalsOpts::FixupEtcFstab => crate::deploy::fixup_etc_fstab(&root),
956982
InternalsOpts::PrintJsonSchema => {
957983
let schema = schema_for!(crate::spec::Host);

lib/src/fsck.rs

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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 rustix::io::Errno;
17+
use serde::{Deserialize, Serialize};
18+
19+
use crate::install::config::Tristate;
20+
use crate::store::{self, Storage};
21+
22+
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
23+
#[serde(rename_all = "kebab-case")]
24+
pub(crate) enum VerityState {
25+
Enabled,
26+
Disabled,
27+
Inconsistent,
28+
}
29+
30+
#[derive(Default, Serialize, Deserialize, Debug, PartialEq, Eq)]
31+
pub(crate) struct FsckResult {
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+
match composefs::fsverity::ioctl::fs_ioc_measure_verity(f.as_fd()) {
59+
Ok(r) => Some(r),
60+
Err(e) if matches!(e.downcast_ref::<Errno>(), Some(&Errno::NOSYS)) => None,
61+
Err(e) => return Err(e),
62+
};
63+
drop(f);
64+
if r.is_some() {
65+
enabled += 1;
66+
} else {
67+
disabled += 1;
68+
}
69+
}
70+
Ok((enabled, disabled))
71+
}
72+
73+
async fn verity_state_of_all_objects(repo: &ostree::Repo) -> Result<(u64, u64)> {
74+
const MAX_CONCURRENT: usize = 3;
75+
76+
let repo_config = repo.config();
77+
let verity_state = {
78+
let (k, v) = store::REPO_VERITY_CONFIG.split_once('.').unwrap();
79+
repo_config
80+
.optional_string(k, v)?
81+
.map(|v| Tristate::from_str(&v))
82+
.transpose()?
83+
.unwrap_or_default()
84+
};
85+
86+
let repodir = Dir::reopen_dir(&repo.dfd_borrow())?;
87+
88+
let mut joinset = tokio::task::JoinSet::new();
89+
let mut results = Vec::new();
90+
91+
for ent in repodir.read_dir("objects")? {
92+
while joinset.len() >= MAX_CONCURRENT {
93+
results.push(joinset.join_next().await.unwrap()??);
94+
}
95+
let ent = ent?;
96+
if !ent.file_type()?.is_dir() {
97+
continue;
98+
}
99+
let objdir = ent.open_dir()?;
100+
joinset.spawn_blocking(move || verity_state_of_objects(&objdir));
101+
}
102+
103+
while let Some(output) = joinset.join_next().await {
104+
results.push(output??);
105+
}
106+
let r = results.into_iter().fold((0, 0), |mut acc, v| {
107+
acc.0 += v.0;
108+
acc.1 += v.1;
109+
acc
110+
});
111+
Ok(r)
112+
}
113+
114+
pub(crate) async fn fsck(storage: &Storage) -> Result<FsckResult> {
115+
let mut r = FsckResult::default();
116+
r.verity = match verity_state_of_all_objects(&storage.repo()).await? {
117+
(0, 0) => None,
118+
(_, 0) => Some(VerityState::Enabled),
119+
(0, _) => Some(VerityState::Disabled),
120+
_ => Some(VerityState::Inconsistent),
121+
};
122+
if matches!(&r.verity, &Some(VerityState::Inconsistent)) {
123+
r.errors.push("Inconsistent fsverity state".into());
124+
}
125+
serde_json::to_writer(std::io::stdout().lock(), &r)?;
126+
Ok(r)
127+
}

0 commit comments

Comments
 (0)