fsck: Add read-only diagnostic report for composefs state - #2497
cgwalters-bot wants to merge 1 commit into
Conversation
| collect_readonly_with_roots(&roots) | ||
| } | ||
|
|
||
| fn collect_readonly_with_roots(roots: &ReadonlyRoots<'_>) -> ReadonlyReport { |
There was a problem hiding this comment.
This is way too big of a function, needs to be split up
There was a problem hiding this comment.
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.
| detail: sanitize(detail), | ||
| } | ||
| } | ||
| fn read_composefs_cmdline( |
There was a problem hiding this comment.
Shouldn't reimplement this
There was a problem hiding this comment.
Now uses ComposefsCmdline::find_in_cmdline. The old parser only accepted 64 hex digits, so it redacted every real (sha512) composefs= value.
| truncated: &mut bool, | ||
| total: &mut u64, | ||
| ) -> RuntimeStaged { | ||
| let Ok(dir) = open_dir_beneath(sysroot, Path::new("run/composefs")) else { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| Err(e) => { | ||
| findings.push(finding( | ||
| "STAGED_MARKER_UNREADABLE", | ||
| "run/composefs/staged-deployment", |
There was a problem hiding this comment.
Now uses STATE_DIR_RELATIVE, COMPOSEFS, COMPOSEFS_TRANSIENT_STATE_DIR, COMPOSEFS_STAGED_DEPLOYMENT_FNAME and ORIGIN_KEY_IMAGE/ORIGIN_KEY_MANIFEST_DIGEST.
| Ok(dir) | ||
| } | ||
|
|
||
| fn open_from(dir: &Dir, relative: &Path) -> std::io::Result<File> { |
There was a problem hiding this comment.
Don't reimplement stuff like this use cap-std or have a strong rationale why it's not needed
There was a problem hiding this comment.
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.
| Ok(dir) => dir, | ||
| Err(e) => { | ||
| findings.push(finding( | ||
| "REPOSITORY_DIRECTORY_UNREADABLE", |
There was a problem hiding this comment.
The SCREAMING_SNAKE_CASE is ugly, let's use PascalCase basically each of these is like an error enum per above
There was a problem hiding this comment.
Codes are now a FindingCode enum that serializes as PascalCase (e.g. DeploymentOriginMissing). is_collection_error() replaces the string suffix matching.
| }) | ||
| .collect() | ||
| } | ||
| impl ImageReport { |
There was a problem hiding this comment.
Btw doesn't cargo fmt want a space here? We should ensure it does...
There was a problem hiding this comment.
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.
| } | ||
| edges | ||
| } | ||
| fn valid_sha256(v: &str) -> bool { |
There was a problem hiding this comment.
There's sstuff for this in oci-spec, avoid reimplmeenting your own validators
There was a problem hiding this comment.
Now parses with oci-spec's Digest::from_str, which accepts any valid algorithm.
411768d to
95399e3
Compare
Johan-Liebert1
left a comment
There was a problem hiding this comment.
Not a full review because Github is unable to load a ~3k diff...
| 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"; |
There was a problem hiding this comment.
We have this constant elsewhere
There was a problem hiding this comment.
Fixed: it now uses crate::install::EFI_LOADER_INFO.
| 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(), |
There was a problem hiding this comment.
For grub classic systems we have entries in /sysroot/boot
There was a problem hiding this comment.
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.
| 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(), |
There was a problem hiding this comment.
If the image has /efi with XBOOTLDR then ESP is mounted at /efi
There was a problem hiding this comment.
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.
| let mut findings = Vec::new(); | ||
| let mut truncated = false; | ||
| let mut read_bytes = 0; | ||
| let composefs_dir = open_dir_beneath(sysroot, Path::new("composefs")); |
There was a problem hiding this comment.
Don't we already have this in ReadonlyRoots.repo?
There was a problem hiding this comment.
Yes. ReadonlyRoots is gone and the repository is opened once in composefs_report.
| .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) |
There was a problem hiding this comment.
If it's a symlink (which we're not treating as an error) we won't read it.
There was a problem hiding this comment.
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.
| Ok(ini) => { | ||
| deployment.image = ini | ||
| .get::<String>("origin", "container") | ||
| .map(|v| redact_ref(&v)); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Agreed. The origin image ref is now reported as-is, digest-pinned @sha256: refs included. Only URL-style user:password@ credentials are redacted.
| truncated: &mut bool, | ||
| total: &mut u64, | ||
| ) -> RuntimeStaged { | ||
| let Ok(dir) = open_dir_beneath(sysroot, Path::new("run/composefs")) else { |
There was a problem hiding this comment.
this is incorrect. run/composefs is relative to / and not /sysroot
There was a problem hiding this comment.
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.
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
|
Rebased onto main; 1 commit, no content change. Generated-by: https://github.com/cgwalters/#llms |
f06e364 to
94f6532
Compare
|
Fixed the CI failures, squashed into the one commit (now 94f6532). The failing baseconfigs, test-upgrade and sealed UKI legs all failed in
Tested on a 16-core devspace: Generated-by: https://github.com/cgwalters/#llms |
a030d83 to
adda0ae
Compare
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
adda0ae to
f074a15
Compare
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 passedcargo clippy -p bootc-lib: no findings in fsck.rs under CI's lint setjust test-tmt-baseconfig var-volatile(composefs, readonly plan incl.015-test-fsck: 0 findings): passedplan-24-image-upgrade-reboot(composefs, grub/ext4/bls): passedRelated: #2422
Assisted-by: AI
Generated-by: https://github.com/cgwalters/#llms