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 |
adda0ae to
f074a15
Compare
| /// A problem found while collecting the report. These are the "inner" | ||
| /// errors of fsck: collection continues past them, and each is reported. | ||
| #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)] | ||
| enum FindingCode { |
There was a problem hiding this comment.
I love the idea of structured errors but I think the real problem here is that ultimately we need to drive some of this down into the relevant code and stop using anyhow::Error which is gonna be painful. But let's track that as a TODO.
How about breaking this into at least separate enums for each subsystem? e.g. composefs vs ostree, /boot, grub-related things etc.
There was a problem hiding this comment.
Split FindingCode into per-subsystem enums (collection, host, composefs, deployment, /boot, GRUB, EFI) wrapped by an untagged outer enum, so the JSON codes are unchanged. Added a TODO on the enum about moving typed errors into the inspected code, tracked in cgwalters-forge/tracker#248. (No ostree group: the readonly report has no ostree findings yet.)
Generated-by: https://github.com/cgwalters/#llms
|
|
||
| impl FindingCode { | ||
| /// Whether this finding means some input could not be collected at all. | ||
| fn is_collection_error(self) -> bool { |
There was a problem hiding this comment.
How about may_mask_further_errors
There was a problem hiding this comment.
Renamed to may_mask_further_errors.
Generated-by: https://github.com/cgwalters/#llms
| serde_json::to_writer_pretty(&mut output, &result)?; | ||
| writeln!(output)?; |
There was a problem hiding this comment.
This is an inefficient pattern lock stdout write directly
There was a problem hiding this comment.
I read this as the locked stdout being line-buffered, so the pretty JSON went out a line per write; the output is now wrapped in a BufWriter and flushed once. Let me know if you meant something else.
Generated-by: https://github.com/cgwalters/#llms
f074a15 to
64e3608
Compare
|
@cgwalters Rebased onto main; 1 commit (now 7b10116), no content change, so nothing new to re-approve. It still awaits your review. Generated-by: https://github.com/cgwalters/#llms |
64e3608 to
7b10116
Compare
The checks returned at most one free-form error string, so the fsverity check pre-formatted its own truncated list of objects, and nothing could tell the kinds of problems apart. The readonly report being added in bootc-dev#2497 settled on a two-level shape instead: fatal errors in the outer anyhow::Result, every problem found collected as a typed finding with a subject. Use the same shape here, so the two can share a finding type once that lands. The output stays bounded: the runner prints at most five findings per check and counts the rest, as the fsverity check did itself before. The messages become one "code: subject: detail" line per finding; the tmt tests only look at the exit status. Generated-by: AI
The checks returned at most one free-form error string, so the fsverity check pre-formatted its own truncated list of objects, and nothing could tell the kinds of problems apart. The readonly report being added in bootc-dev#2497 settled on a two-level shape instead: fatal errors in the outer anyhow::Result, every problem found collected as a typed finding with a subject. Use the same shape here, so the two can share a finding type once that lands. The output stays bounded: the runner prints at most five findings per check and counts the rest, as the fsverity check did itself before. The messages become one "code: subject: detail" line per finding; the tmt tests only look at the exit status. Generated-by: AI Signed-off-by: Colin Walters <walters@verbum.org>
The checks returned at most one free-form error string, so the fsverity check pre-formatted its own truncated list of objects, and nothing could tell the kinds of problems apart. The readonly report being added in #2497 settled on a two-level shape instead: fatal errors in the outer anyhow::Result, every problem found collected as a typed finding with a subject. Use the same shape here, so the two can share a finding type once that lands. The output stays bounded: the runner prints at most five findings per check and counts the rest, as the fsverity check did itself before. The messages become one "code: subject: detail" line per finding; the tmt tests only look at the exit status. Generated-by: AI Signed-off-by: Colin Walters <walters@verbum.org>
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
7b10116 to
6c38116
Compare
|
@cgwalters Rebased onto main (now 6c38116, still 1 commit) after #2528 landed, and unified the two finding types as planned. Of your open review (changes requested at f074a15), the three comments (per-subsystem enums, What changed since 7b10116 (
Tested on a 16-core devspace (cgwalters-devspace-37068905608) at 6c38116: Generated-by: https://github.com/cgwalters/#llms |
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 devspace with
BOOTC_base=quay.io/fedora/fedora-bootc:44, at 64e3608:just validate: passedjust unit-tests: passed (bootc-lib: 299 passed)just test-tmt --composefs-backend --bootloader=grub --filesystem=ext4 --boot-type=bls readonly(incl.015-test-fsck: 0 findings): passedRelated: #2422
Assisted-by: AI
Generated-by: https://github.com/cgwalters/#llms