Skip to content

Verify a source root lost+found on both sides (#959) - #961

Merged
sehkone merged 3 commits into
mainfrom
sehkone/issue-959
Aug 29, 2026
Merged

Verify a source root lost+found on both sides (#959)#961
sehkone merged 3 commits into
mainfrom
sehkone/issue-959

Conversation

@sehkone

@sehkone sehkone commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

What this changes

The migration metadata manifests dropped ./lost+found from both sides. That exemption exists because mke2fs seeds a lost+found directory in the root of every filesystem it makes, and that root is the destination of the relocation copy — without it, the destination carries one record the holding directory never had and no correct migration could ever verify.

Applied symmetrically, it also silenced the source side. A holding directory that legitimately holds a root lost+found — the whole source tree of the replacement procedure, whose source is itself a mounted reserve — had that directory's type, mode, numeric uid, numeric gid and link count compared against nothing at all, and a mismatch on it passed the gate that guards the closing rename.

The exemption now belongs to the destination side alone, and even there it is conditional. The rendered shell asks, at the moment it runs, whether the source root carries a lost+found directory:

if [ -d '<holding>/lost+found' ] && [ ! -L '<holding>/lost+found' ]; then find . -xdev -mindepth 1 -printf '%y %m %U %G %n %P\0' > "$SCRATCH"/meta.destination.raw; else find . -xdev -mindepth 1 ! \( -path './lost+found' -type d \) -printf '%y %m %U %G %n %P\0' > "$SCRATCH"/meta.destination.raw; fi

The source walk carries no filter at all — a record the copy did not put there cannot arise on the side the copy came from.

The predicate is -d and not -L rather than -d alone: test -d answers for a symbolic link's target, and nothing in this procedure follows one. find -type d does not either, so the two spellings agree on every entry.

Everything else stands: the walks are NUL-delimited, each stage is its own command over its own staged file, the three manifests are byte-compared with cmp, and no recursive delete is introduced. The branch is a plain if, not a pipeline, so a failed walk inside it still stops the sequence — set -e fires on the branch body, and there is a test for exactly that, driven down both branches.

Also here: a CodeQL rust/cleartext-logging alert. The ownership case labelled its assertion with the uid and gid it had just set, and the query reads any value bound to uid reaching a message as a leak — a red required check over two loop constants in a Linux-only test module. The message names the manifest field the case moves instead, which is what a failure needed to distinguish anyway.

Acceptance criteria

  • A newly formatted ext4 destination whose holding source lacks a root lost+found verifies successfully — the else branch, covered by the_reserves_own_lost_and_found_does_not_fail_the_verification and by the registrar-internal-init E2E scenario, which now also asserts the conditional is rendered and that the branch taken really was the exempting one.
  • A source-root lost+found mismatch fails the comparisona_mismatched_source_root_lost_and_found_fails_the_verification breaks type, mode and link count in turn over a copy that had already passed; a_reowned_source_root_lost_and_found_fails_the_verification does uid and gid where the run has the privilege to chown, and the rendered %y %m %U %G %n %P form is asserted unconditionally beside it.
  • Contents beneath a source root lost+found stay in every comparisoneverything_under_lost_and_found_is_still_compared now breaks the metadata, size and content passes separately, each over a copy that had already passed, rather than only the same-sized content case.
  • A non-directory named lost+found stays under the ordinary rulesa_regular_file_merely_bearing_the_name_is_not_exempt at the source root, and a_non_directory_bearing_the_name_at_the_destination_root_is_not_exempt at the destination root, which is where the exemption is actually spelled and so where -type d has to be the thing doing the work. a_symlink_bearing_the_name_at_the_source_root_is_refused_by_the_guard covers the third shape the name can take: a link to a real directory, which test -d alone would read as a directory, and which the type guard refuses before the question is ever asked.
  • The rendered commands stay NUL-delimited, staged, pipeline-free and cmp-comparedthe_rendered_migration_commands_are_pinned_exactly pins the whole sequence verbatim and re-checks the pipe, diff -r, rm -r and trailing-field bans.
  • Both manuals stay alignedboth_manuals_pin_the_asymmetric_lost_and_found_exemption asserts each page's relocation block carries the exclusion exactly once and carries the conditional, so a symmetric form cannot come back in either language.

Test plan

The rendered_sequence module is Linux-only (GNU find -printf, sort -z, sha256sum --zero), so it was run in a rust:latest container; everything else was run on macOS as well.

  • A source with no root lost+found verifies, and the destination's seeded directory is not treated as audit data
  • A source-root lost+found whose type, mode, numeric uid, numeric gid or link count differs fails the metadata comparison and stops before the closing rename
  • Descendants beneath a source root lost+found still participate in the metadata, regular-file-size and content comparisons — each broken separately
  • A regular file and a symlink bearing the name are not exempt, at the source root and at the destination root
  • cargo test --bin bootroot audit_store::migration — 27 passed, in the container both as root (so the uid/gid case ran) and as an unprivileged user (so the fail-closed-inside-the-conditional case ran, down both of its branches)
  • cargo test — green on macOS, and green in the Linux container (lib 1269, bin 1303, plus the other bins and integration suites). The 8 tests/bootroot_rotate.rs cases that fail in that container fail on a missing docker CLI and pass locally on macOS where Docker is present: 46 passed; 0 failed. The container's own umask matters: at the image default of 002 the registrar::audit::scan and registrar::openbao_audit suites refuse their own fixtures as group-writable, which is the safe-path check working rather than a failure of this branch
  • cargo clippy --all-targets -- -D warnings — clean on macOS and Linux
  • cargo fmt -- --config group_imports=StdExternalCrate --check — clean on macOS and Linux
  • ./scripts/check-docs.shmkdocs build --strict passes and the theme verification passes
  • markdownlint-cli2 over the repository — 0 issues
  • shellcheck -x scripts/impl/run-registrar-internal-init-e2e.sh — clean (only SC1091 info for sourced libs, resolved when run from the script's directory)
  • All CI checks green on the pushed branch, including Docker E2E (registrar-internal-init)

Local run notes

  • scripts/preflight/ci/e2e-matrix.sh did not run here. Its step 13 — run-registrar-internal-init-e2e.sh, the one arm that exercises this rendering end to end — requires passwordless sudo, and sudo -n true on this machine reports "a password is required"; the matrix's own usage text states --skip-hosts does not stand in for it. Port 8200 is also held by an unrelated live process from another session, which the earlier steps' port preflight refuses to start over, and tearing that down is not mine to do. CI's Docker E2E jobs gate that arm, and Docker E2E (registrar-internal-init) is green. Nothing was weakened, skipped, marked continue-on-error or deleted to get a local run to pass. What did run locally is the rendered_sequence suite, which executes the rendered shell for real against fixture trees on Linux, including every lost+found shape on both sides.
  • No CHANGELOG.md entry. The audit-store reserve, its relocation procedure and its verification are all announced under [Unreleased]; no user of the last release ever saw the symmetric exemption, so this is a rework of unreleased work rather than an observable change.
  • A directory's %n is two plus its subdirectory count, so no fixture can move that field alone without also changing the entries the manifest lists underneath. The link-count case therefore faults on both, which still demonstrates the field is compared rather than skipped; it is not an isolated one-field test and the code comment says so.

Closes #959

Part of #926

The metadata manifests dropped `./lost+found` symmetrically so that a
relocation onto a freshly made ext4 reserve could close: `mke2fs`
seeds that directory in the destination root and the holding directory
normally has no counterpart, so the comparison would fault every
correct migration. But the exemption also fired on the side the copy
came from. A holding directory that legitimately holds a root
`lost+found` — the whole source tree of the replacement procedure,
whose source is itself a mounted reserve — had that directory's type,
mode, numeric ownership and link count compared against nothing at
all, and a mismatch on it passed the gate that guards the closing
rename.

The exemption now belongs to the destination side alone, and even
there it is conditional: the rendered shell asks whether the source
root carries a `lost+found` directory and takes the unfiltered walk
when it does. The predicate is `-d` together with not `-L`, because
`test -d` answers for a symbolic link's target and this procedure
follows none — which is what `find -type d` also does, so the two
agree on every entry. The branch is a plain `if`, not a pipeline, so a
failed walk inside it still stops the sequence.

Closes #959
Comment thread src/commands/audit_store/migration.rs Fixed
The destination walk's exemption turns on what the source root holds,
so each shape that root can present it with is a separate branch of
the same rule and only some were exercised.

A source `lost+found` holding files had one same-sized difference
checked; the metadata and size passes reach underneath it too, and
each now breaks on its own over a copy that had already passed. A
non-directory bearing the name is planted at the destination root as
well as the source, which is where the exemption is actually spelled
and so where `-type d` has to be doing the work. The conditional's
fail-closed walk is driven down both of its branches rather than the
exempting one alone.

A symbolic link at the source root under that name went untested
altogether, and it is the reading `! -L` exists to rule out: `test -d`
answers for a link's target, so one pointing at a directory would
otherwise send the destination down the unfiltered walk. The type
guard refuses it before the question is asked, which is the property
worth pinning.

Part of #959
The ownership case labelled its assertion with the uid and gid it had
just set, and CodeQL's `rust/cleartext-logging` reads any value bound
to `uid` reaching a message as a leak — a red required check over two
loop constants in a Linux-only test module. The message says which
manifest field the case moves instead, which is what a failure needed
to distinguish anyway.

Part of #959
@sehkone sehkone changed the title Verify a source root lost+found on both sides Verify a source root lost+found on both sides (#959) Aug 29, 2026
@sehkone

sehkone commented Aug 29, 2026

Copy link
Copy Markdown
Contributor Author

[Reviewer Round 1]

Review: no findings. I would approve PR #961.

The destination-only conditional correctly preserves ext4’s seeded lost+found exception while making a source-root directory participate in the full metadata manifest. The rendered sequence remains staged, NUL-delimited, pipeline-free, and fail-closed.

The tests meaningfully cover the new branches: source/destination metadata mismatches, descendants, same-named non-directories, symlink refusal, and both conditional paths. English and Korean documentation match the rendered behavior. PR linkage and test-plan hygiene are also correct.

@sehkone

sehkone commented Aug 29, 2026

Copy link
Copy Markdown
Contributor Author

[Review Verdict Round 1: APPROVED]

@sehkone

sehkone commented Aug 29, 2026

Copy link
Copy Markdown
Contributor Author

Suggested squash commit

Title

Verify a source root lost+found on both sides

Body

The migration metadata manifests dropped `./lost+found` from both
sides. That exemption exists because `mke2fs` seeds a `lost+found`
directory in the root of every filesystem it makes, and that root is
the destination of the relocation copy — without it the destination
carries one record the holding directory never had, and no correct
migration could ever verify.

Applied symmetrically, it also silenced the source side. A holding
directory that legitimately holds a root `lost+found` — the whole
source tree of the replacement procedure, whose source is itself a
mounted reserve — had that directory's type, mode, numeric uid,
numeric gid and link count compared against nothing at all, and a
mismatch on it passed the gate that guards the closing rename.

The exemption now belongs to the destination side alone, and even
there it is conditional: the rendered shell asks, at the moment it
runs, whether the source root carries a `lost+found` directory, and
records its counterpart when it does. The source walk carries no
filter at all — a record the copy did not put there cannot arise on
the side the copy came from.

The predicate is `-d` and not `-L` rather than `-d` alone, because
`test -d` answers for a symbolic link's target and nothing in this
procedure follows one. `find -type d` does not either, so the two
spellings agree on every entry.

Everything else stands: the walks are NUL-delimited, each stage is
its own command over its own staged file, the three manifests are
byte-compared with `cmp`, and no recursive delete is introduced. The
branch is a plain `if`, not a pipeline, so a failed walk inside it
still stops the sequence.

Closes #959

Part of #926

@sehkone
sehkone merged commit be0e87c into main Aug 29, 2026
21 checks passed
@sehkone
sehkone deleted the sehkone/issue-959 branch August 29, 2026 07:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Verify root lost+found metadata during migration

2 participants