Skip to content

fsck: Add read-only diagnostic report for composefs state - #2497

Draft
cgwalters-bot wants to merge 1 commit into
bootc-dev:mainfrom
cgwalters-bot:issue-2422-fsck-report
Draft

cgwalters-bot wants to merge 1 commit into
bootc-dev:mainfrom
cgwalters-bot:issue-2422-fsck-report

Conversation

@cgwalters-bot

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

Copy link
Copy Markdown
Contributor

Why

Issue #2422 shows that missing composefs OCI metadata can make status, upgrade, and switch fail even after booting an older deployment. A read-only report makes it possible to collect boot, deployment, repository, and image evidence without mutating a broken system or stopping at the first missing input.

The collector bounds its reads. Fatal errors (no sysroot) fail the command; everything else is recorded as a typed finding and collection continues. This is a large patch; please review the report schema and overall approach before treating it as ready to merge.

The content of Colin's commit (fsck: Add read-only diagnostic report) changed in the review rework, which also took in Johan's comments, so his Signed-off-by was dropped pending his re-review.

Verification

On a 16-core RHEL 10 devspace, at adda0ae (same tree as f074a15):

  • cargo test -p bootc-lib: 293 passed
  • cargo clippy -p bootc-lib: no findings in fsck.rs under CI's lint set
  • just test-tmt-baseconfig var-volatile (composefs, readonly plan incl. 015-test-fsck: 0 findings): passed
  • plan-24-image-upgrade-reboot (composefs, grub/ext4/bls): passed

Related: #2422

Assisted-by: AI

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

@bootc-bot
bootc-bot Bot requested a review from cgwalters September 24, 2026 17:35
@github-actions github-actions Bot added the area/documentation Updates to the documentation label Sep 24, 2026
Comment thread crates/lib/src/fsck.rs Outdated
collect_readonly_with_roots(&roots)
}

fn collect_readonly_with_roots(roots: &ReadonlyRoots<'_>) -> ReadonlyReport {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is way too big of a function, needs to be split up

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Split up: collect_readonly_with_roots now just sequences composefs_report, collect_deployments/collect_deployment, resolve_manifest_stream and collect_host. The ReadonlyRoots struct is gone; four of its six dirs were never used. All of this is squashed into your commit, now a030d83 (fsck: Add read-only diagnostic report), with your sign-off kept.

Comment thread crates/lib/src/fsck.rs
detail: sanitize(detail),
}
}
fn read_composefs_cmdline(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't reimplement this

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now uses ComposefsCmdline::find_in_cmdline. The old parser only accepted 64 hex digits, so it redacted every real (sha512) composefs= value.

Comment thread crates/lib/src/fsck.rs Outdated
truncated: &mut bool,
total: &mut u64,
) -> RuntimeStaged {
let Ok(dir) = open_dir_beneath(sysroot, Path::new("run/composefs")) else {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While fsck needs to run to completion, we also should not just completely swallow errors.

This can also use cap-std-ext open_dir_optional.

I think our fsck infrastructure should be like the linting code - we have a Result<Result<>> or really Result<Vec<Result<>> or so, the outer is really truly fatal/unexpected runtime errors, the inner Vec<Result> is any errors we encoutered while running fsck.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opens now use open_dir_optional: a missing dir is None, and any other error becomes a finding (open_dir_or_record). That covers the staged marker, the deploy dir, the repository and the BLS/GRUB dirs. The report now has the two levels you described: collect_readonly_host returns anyhow::Result for fatal errors (e.g. no sysroot), and everything else goes into the findings list while collection continues. I left the existing FsckResult/FSCK_CHECKS alone; whether to convert those too is asked in cgwalters-forge/tracker#161.

Comment thread crates/lib/src/fsck.rs Outdated
Err(e) => {
findings.push(finding(
"STAGED_MARKER_UNREADABLE",
"run/composefs/staged-deployment",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reuse existing const

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now uses STATE_DIR_RELATIVE, COMPOSEFS, COMPOSEFS_TRANSIENT_STATE_DIR, COMPOSEFS_STAGED_DEPLOYMENT_FNAME and ORIGIN_KEY_IMAGE/ORIGIN_KEY_MANIFEST_DIGEST.

Comment thread crates/lib/src/fsck.rs Outdated
Ok(dir)
}

fn open_from(dir: &Dir, relative: &Path) -> std::io::Result<File> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't reimplement stuff like this use cap-std or have a strong rationale why it's not needed

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropped open_from/open_beneath/open_dir_from/metadata_beneath and the raw openat walks in favor of cap-std, which already confines every lookup to the dir. The only helper left is open_nofollow, which checks symlink_metadata first so that a symlinked final component is reported instead of being read.

Comment thread crates/lib/src/fsck.rs Outdated
Ok(dir) => dir,
Err(e) => {
findings.push(finding(
"REPOSITORY_DIRECTORY_UNREADABLE",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SCREAMING_SNAKE_CASE is ugly, let's use PascalCase basically each of these is like an error enum per above

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codes are now a FindingCode enum that serializes as PascalCase (e.g. DeploymentOriginMissing). is_collection_error() replaces the string suffix matching.

Comment thread crates/lib/src/fsck.rs
})
.collect()
}
impl ImageReport {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Btw doesn't cargo fmt want a space here? We should ensure it does...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rustfmt doesn't add blank lines between items. Its only knob for that, blank_lines_lower_bound, is nightly-only and applies inside blocks too, so we can't enforce it there. I added the blank lines across fsck.rs by hand.

Comment thread crates/lib/src/fsck.rs Outdated
}
edges
}
fn valid_sha256(v: &str) -> bool {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's sstuff for this in oci-spec, avoid reimplmeenting your own validators

@cgwalters-bot cgwalters-bot Sep 26, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now parses with oci-spec's Digest::from_str, which accepts any valid algorithm.

@cgwalters-bot cgwalters-bot moved this to In Review in Workstream Sep 24, 2026
@cgwalters
cgwalters force-pushed the issue-2422-fsck-report branch from 411768d to 95399e3 Compare September 24, 2026 21:55

@Johan-Liebert1 Johan-Liebert1 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a full review because Github is unable to load a ~3k diff...

Comment thread crates/lib/src/fsck.rs Outdated
const MAX_ARTIFACTS: usize = 256;
const MAX_LAYERS: usize = 256;
const MAX_REPORT_STRING: usize = 256;
const EFI_LOADER_INFO: &str = "LoaderInfo-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have this constant elsewhere

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: it now uses crate::install::EFI_LOADER_INFO.

Comment thread crates/lib/src/fsck.rs Outdated
repo: open_dir_from(&sysroot, Path::new("composefs")).ok(),
deploy: open_dir_from(&sysroot, Path::new("state/deploy")).ok(),
run: open_dir_from(&sysroot, Path::new("run")).ok(),
boot: open_dir_from(&sysroot, Path::new("boot")).ok(),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For grub classic systems we have entries in /sysroot/boot

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those are covered: the BLS roots are relative to the sysroot, so boot/loader/entries (and .staged) is /sysroot/boot/loader/entries, and GRUB user.cfg is read from boot/grub2 and boot/grub. The ReadonlyRoots.boot field this was on was never used and has been removed.

Comment thread crates/lib/src/fsck.rs Outdated
deploy: open_dir_from(&sysroot, Path::new("state/deploy")).ok(),
run: open_dir_from(&sysroot, Path::new("run")).ok(),
boot: open_dir_from(&sysroot, Path::new("boot")).ok(),
esp: open_dir_from(&sysroot, Path::new("boot/efi")).ok(),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the image has /efi with XBOOTLDR then ESP is mounted at /efi

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: efi/loader/entries (and .staged) are now BLS roots too, and boot artifacts are also looked up under efi/. The ESP mount check already accepted /sysroot/efi.

Comment thread crates/lib/src/fsck.rs Outdated
let mut findings = Vec::new();
let mut truncated = false;
let mut read_bytes = 0;
let composefs_dir = open_dir_beneath(sysroot, Path::new("composefs"));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we already have this in ReadonlyRoots.repo?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. ReadonlyRoots is gone and the repository is opened once in composefs_report.

Comment thread crates/lib/src/fsck.rs
.and_then(|dir| metadata_at(dir, "meta.json".as_ref()).ok());
let meta_contents = meta_fs
.as_ref()
.is_some_and(|m| m.file_type == EntryType::File)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's a symlink (which we're not treating as an error) we won't read it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. A symlinked composefs/meta.json now gets a RepositoryMetadataSymlink finding. A symlink there, or any error other than not-found, used to look the same as the file being absent.

Comment thread crates/lib/src/fsck.rs Outdated
Ok(ini) => {
deployment.image = ini
.get::<String>("origin", "container")
.map(|v| redact_ref(&v));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't redacting be very unhelpful here as we won't be able to determine if the origin points to the correct oci image or not?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. The origin image ref is now reported as-is, digest-pinned @sha256: refs included. Only URL-style user:password@ credentials are redacted.

Comment thread crates/lib/src/fsck.rs Outdated
truncated: &mut bool,
total: &mut u64,
) -> RuntimeStaged {
let Ok(dir) = open_dir_beneath(sysroot, Path::new("run/composefs")) else {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is incorrect. run/composefs is relative to / and not /sysroot

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, fixed: the staged marker is now read from the host root's run/composefs, and there's a unit test with separate host and sysroot dirs.

cgwalters-bot added a commit to cgwalters-bot/homegit that referenced this pull request Sep 25, 2026
When the head is his own commit, he acted on his review himself, as on
bootc-dev/bootc#2497, where he force-pushed after his line comments and
the PR was then listed as outstanding every sweep. Pushes by anyone
else, like GitHub's "Update branch" (web-flow), still answer nothing.

Generated-by: AI
@cgwalters-bot

Copy link
Copy Markdown
Contributor Author

Rebased onto main; 1 commit, no content change.

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

@cgwalters-bot
cgwalters-bot force-pushed the issue-2422-fsck-report branch 2 times, most recently from f06e364 to 94f6532 Compare September 26, 2026 06:05
@cgwalters-bot

Copy link
Copy Markdown
Contributor Author

Fixed the CI failures, squashed into the one commit (now 94f6532). The failing baseconfigs, test-upgrade and sealed UKI legs all failed in 015-test-fsck.nu on false findings, not on the transient/volatile layouts:

  • OCI_MANIFEST_DIGEST_INVALID: manifests and configs are stored as splitstreams, so the report now reads the JSON through SplitStreamReader instead of hashing the stream file.
  • BOOT_ARTIFACT_MISSING: GRUB BLS paths written as /boot/... (no separate /boot partition) are now also resolved against the physical root.
  • The test asserted describe starts with list, but nushell reports a non-empty list of records as table<...>.

Tested on a 16-core devspace: cargo test -p bootc-lib --lib fsck (19 passed), just test-tmt-baseconfig var-volatile and just test-upgrade (composefs, grub, ext4, centos-10), both passing plan-01-readonly. Your review comments aren't addressed yet.

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

@cgwalters-bot
cgwalters-bot force-pushed the issue-2422-fsck-report branch 2 times, most recently from a030d83 to adda0ae Compare September 26, 2026 22:31
Provide the diagnostic evidence requested by bootc-dev#2422 without changing
the booted system. Collection is capability-confined and non-mutating,
includes composefs and boot evidence, and reports bounded partial
failures rather than aborting on the first unavailable input.

Assisted-by: AI

Related: bootc-dev#2422

This branch has not been deployed

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

Labels

area/documentation Updates to the documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants