Skip to content

DAOS-19577 bio: Add a read-only meta and WAL header verifier - #19260

Open
knard38 wants to merge 6 commits into
masterfrom
ckochhof/dev/master/daos-19577/patch-001
Open

knard38 wants to merge 6 commits into
masterfrom
ckochhof/dev/master/daos-19577/patch-001

Conversation

@knard38

@knard38 knard38 commented Oct 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Offline tools such as ddb inspect the meta and WAL blobs of pools which may be damaged. The headers are checksum-protected, but a header with a valid checksum and an inconsistent content (torn write, format bug, flawed repair) is only caught by assertions of the engine's open path — a WAL blob ID left unset, or a RDB meta header referencing a data blob — which abort the process. A recovery tool has to report the error and carry on.

What this PR adds

  • bio_mc_hdr_verify(xs_ctxt, pool_id, flags) (bio.h, bio_context.c): opens the meta and WAL blob I/O contexts of a pool target, loads both headers with the existing loaders (magic, version, checksum) and verifies the content of the meta header. It never opens a session, replays the WAL or writes to the blobs (in particular, no v1→v2 header upgrade), and returns 0 when no meta device is configured (PMEM). flags only selects the SMD table (BIO_MC_FL_RDB).
  • Meta header checks (meta_hdr_verify() in bio_wal.c): a WAL blob ID is set, a RDB references no data blob (the two conditions bio_mc_open() asserts on), and the geometry is non-zero.
  • Each failed check is logged with D_ERROR and returned as -DER_DF_INVAL; loader errors (-DER_UNINIT, -DER_DF_INCOMPT, -DER_CSUM) and the SMD lookup error (-DER_NONEXIST) are passed through. The failure sites are also traced at DB_MGMT with the pool UUID.
  • Purely additive: bio_mc_open(), meta_open() and wal_open() are not modified, so the engine keeps its core dump at the point of detection, while recovery tools get their own tolerant, read-only path. The WAL header content checks follow in part 2.

Tests

bio_ut (utest suite VOS_md_on_ssd, AIO bdev):

  • The three header cases use per-test fixtures that create and destroy their pool (a RDB pool for the flag test), so a failed assertion cannot leak it.
  • Checksum-valid forged meta headers are staged on the real blob, one per invariant, next to the three loader errors, a healthy pool before and after use, an unknown pool and the RDB/pool flag mismatch. The forging cases end by restoring the pool's own header and checking that it is accepted and that the session reopens on it.

Validation

-Werror build and bio_ut (11/11) in MD-on-SSD mode; git-clang-format, codespell and git diff --check clean. No Features: pragma on purpose: no functional test exercises the verifier.

Steps for the author:

  • Commit message follows the guidelines.
  • Appropriate Features or Test-tag pragmas were used.
  • Appropriate Functional Test Stages were run.
  • At least two positive code reviews including at least one code owner from each category referenced in the PR.
  • Testing is complete. If necessary, forced-landing label added and a reason added in a comment.

After all prior steps are complete:

  • Gatekeeper requested (daos-gatekeeper added as a reviewer).

Offline tools such as ddb inspect the meta and WAL blobs of pools which
may be damaged. The meta and WAL headers are checksum-protected, so a
random corruption is rejected by bio_mc_open(), but a header with a
valid checksum and an inconsistent content (torn software write, format
bug, flawed repair) is only caught by the assertions of the engine's
open path: a WAL blob ID left unset in the meta header or a RDB meta
header referencing a data blob abort the process.

Add bio_mc_hdr_verify(), a read-only entry point which opens the meta
blob I/O context of a pool target, loads the meta header with the
existing loader (magic, version and checksum checks), verifies its
content, then opens the WAL blob it points to and loads the WAL header
the same way. The meta header checks are the invariants bio_mc_open()
relies on: a WAL blob ID is set, a RDB references no data blob, and the
geometry handed to vos_pool_open() is non-zero (block size, header
blocks, capacity). Each failed check is logged with the field and its
values and reported as -DER_DF_INVAL; the loader errors are passed
through (-DER_UNINIT, -DER_DF_INCOMPT, -DER_CSUM). The verifier never
opens a session, replays the WAL or writes to the blobs. The failure
sites are traced at the DB_MGMT level, like the rest of the open path.

bio_mc_open(), meta_open() and wal_open() are not modified. The checks
share the existing static load_meta_header() and load_wal_header()
through two internal wrappers, meta_hdr_check() and wal_hdr_check(),
declared in bio_wal.h. The content checks of the WAL header are added
by a follow-up patch.

Tests (bio_ut, VOS_md_on_ssd utest suite): stage checksum-valid forged
meta headers on the meta blob of an open pool, one per invariant
(missing WAL blob ID, zero block size, header blocks and capacity, data
blob of a RDB), and check bio_mc_hdr_verify() returns -DER_DF_INVAL;
also cover the bad magic, version and checksum loader errors through
the verifier, a healthy pool before and after use, an unknown pool and
the RDB/pool flag mismatch (-DER_NONEXIST from SMD). The forging tests
end by restoring the original header and checking that it is accepted
again and that the session reopens on it. The header tests get
per-test setup and teardown functions which create and destroy their
pool (a RDB pool for the flag test), so that a failed assertion does
not leak it; the test arguments carry the bio_mc_flags of the pool,
which the RDB fixture sets and the pool helpers read.

Signed-off-by: Cedric Koch-Hofer <cedric.koch-hofer@hpe.com>
@knard38 knard38 self-assigned this Oct 13, 2026
@knard38 knard38 added CR Catastrophic Recovery Feature meta-on-ssd Metadata on SSD Feature labels Oct 13, 2026
@github-actions

Copy link
Copy Markdown

Ticket title is 'ddb: implement wal_open and wal_close commands'
Status is 'In Progress'
Labels: 'catastrophic_recovery,ddb,md_on_ssd2'
https://daosio.atlassian.net/browse/DAOS-19577

@knard38
knard38 marked this pull request as ready for review October 13, 2026 14:23
@knard38
knard38 requested review from a team as code owners October 13, 2026 14:23
@daosbuild3

Copy link
Copy Markdown
Collaborator

@daosbuild3

Copy link
Copy Markdown
Collaborator

Test stage Functional Hardware Medium Provider MD on SSD completed with status FAILURE. https://jenkins-3.daos.hpc.amslabs.hpecorp.net//job/daos-stack/job/daos/view/change-requests/job/PR-19260/1/execution/node/2021/log

Comment thread src/bio/bio_wal.c
wal_hdr_check(struct bio_meta_context *mc)
{
return load_wal_header(mc);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The BIO MC open interface has already verified header, what are these functions introduced for? They'll be used by DDB? Them they should be part of DDB implementation.

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.

From my understanding, bio_mc_open() does verify the headers, but only their integrity (magic, version, checksum), and the new code doesn't duplicate that: meta_hdr_check() and wal_hdr_check() reuse the same load_meta_header() and load_wal_header(). What they add is what the open path doesn't do and what ddb needs on a pool that may be damaged:

  • Open aborts on a header with a valid checksum and an inconsistent content. Its content checks are D_ASSERTs: WAL blob ID set (bio_context.c:961), no data blob for a RDB (bio_context.c:973), checkpoint ID <= committed ID (bio_wal.c:2073), zero block count only with a zero ID (bio_wal.c:167). Fine for the engine, not for a recovery tool which has to report the error and carry on. I ran bio_mc_open() on checksum-valid forged headers (the unit tests of this PR and of the follow-up PR which checks the WAL header content): each of these four asserts ends with SIGABRT, where bio_mc_hdr_verify() returns -DER_DF_INVAL and logs the field.
  • Open checks only a small part of it. Here, a zero mh_blk_bytes, mh_hdr_blks or mh_tot_blks is accepted by bio_mc_open() and handed as is to VOS by bio_meta_get_attr(). The follow-up PRs (content of the WAL header, identity of the headers) widen the gap: counting their tests too, of the 34 inconsistent but checksum-valid headers forged by the unit tests, bio_mc_open() rejects 1, aborts on 5 and accepts 28, 8 of which abort at the next bio_wal_query() (a void function). A WAL block size of 0 makes bio_wal_replay() divide by zero.
  • Open is not read-only. meta_open() and wal_open() rewrite a v1 header in place (bio_wal.c:2158, bio_wal.c:2046), I checked that the blob goes from version 1 to 2. bio_mc_hdr_verify() leaves it untouched.

On the location: the loaders, the checksum helpers and the opening of the meta/WAL blob contexts (load_*_header(), meta_csum_*(), __bio_ioctxt_open()) are static in BIO, and the content checks mirror the engine's WAL ID arithmetic and asserts (wal_next_id(), wal_used_blks()), so I kept them next to what they mirror. In ddb they would duplicate all of it and not follow a change of the engine. It is not specific to the WAL headers either: DAOS-19782 tracks the same need for VOS (a damaged evtree checksum configuration aborts the fetch on a D_ASSERT), which I plan to solve the same way.

The only public function here is bio_mc_hdr_verify(). In this PR wal_hdr_check() is just the loader, the follow-up PR which verifies the content of the WAL header extends it. ddb's dw_open(), in the ddb follow-up, calls the verifier before bio_mc_open().

I left bio_mc_open(), meta_open() and wal_open() untouched on purpose: turning their asserts into errors would change the failure mode of the engine, and the same asserts are also reached at run time (wal_used_blks() from bio_wal_reserve()).

Comment thread src/vos/tests/wal_ut.c
{"header verify on inconsistent meta header", wal_ut_bad_meta_hdr, wal_ut_hdr_pool_setup,
wal_ut_hdr_pool_teardown},
{"header verify on rdb flag mismatch", wal_ut_rdb_flag_mismatch, wal_ut_hdr_rdb_pool_setup,
wal_ut_hdr_pool_teardown},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

WAL header is already verified on open, why do we need to introduce these unit tests?

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.

These tests cover the new bio_mc_hdr_verify(), they don't re-test the open path. Nothing on master forges or checks a header (wal_ut.c and vts_wal.c only use consistent ones), and the interesting cases can't be tested through bio_mc_open(): a header with a valid checksum and an inconsistent content makes it assert, which is why the verifier exists (see my answer on bio_wal.c).

What the tests check:

  • The integrity errors of the meta header loader (magic, version, checksum) are returned unchanged through the verifier.
  • One forged header per invariant, with a valid checksum, gives -DER_DF_INVAL: no WAL blob ID, zero block size, zero header blocks, zero capacity, and a data blob on a RDB (with a RDB pool).
  • Consistent headers are accepted (fresh pool with the session opened or closed, used pool) and an unknown pool gives -DER_NONEXIST. Each forging test ends by restoring the original header and checking that it is accepted again and that the session reopens on it, so the verifier is shown to accept what the engine opens.

About the WAL header: in this PR it is only loaded with the loader of the open path, so the tests only check that consistent WAL headers are accepted. The forged WAL header cases come with its content checks in the follow-up PR.

If the verifier were moved to ddb, its tests would move with it, so this is the same question as in the other thread.

Comment thread src/bio/bio_wal.c Outdated
}

/* Geometry handed to vos_pool_open() by bio_meta_get_attr(); the format never writes 0 */
if (hdr->mh_blk_bytes == 0 || hdr->mh_hdr_blks == 0 || hdr->mh_tot_blks == 0) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Only zero is invalid size? Are there some initial value? such as: meta_hdr->mh_blk_bytes = META_BLK_SZ. Then if possible, we can exactly check whether they are the expected values or not.

@knard38 knard38 Oct 22, 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.

  • Make more restrictive constants check

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 with commit d0d1176

@Nasf-Fan
Nasf-Fan self-requested a review October 20, 2026 07:38
@knard38
knard38 requested a review from NiuYawei October 22, 2026 06:19
knard38 and others added 4 commits October 22, 2026 12:01
Signed-off-by: Cedric Koch-Hofer <cedric.koch-hofer@hpe.com>
The geometry check of bio_mc_hdr_verify() only rejected a zero block
size, header size and capacity. A header with a valid checksum and any
other value is accepted by bio_mc_open() and handed as is to VOS by
bio_meta_get_attr(). VOS locates its meta data in the blob at
mh_hdr_blks * mh_blk_bytes, so a block size of 512 or 8192 bytes, or two
header blocks, moves it.

Check the block size and the header size against the constants the
format has always written (META_BLK_SZ and META_HDR_BLKS). The capacity
follows the size of the pool and has no expected value: check that it is
set and that it fits in the meta blob next to the header. It is compared
in blocks, since the product with the block size can wrap (a capacity of
1 << 60 blocks gives a capacity of 0 in bio_meta_get_attr()). Each failed
check is logged with the values and reported as -DER_DF_INVAL.

Tests (bio_ut, VOS_md_on_ssd utest suite): forge checksum-valid meta
headers with a block size of 512 and 8192 bytes, with two header blocks,
with a capacity which fills the blob exactly (accepted), with one block
more and with 1 << 60 blocks (rejected).

Signed-off-by: Cedric Koch-Hofer <cedric.koch-hofer@hpe.com>
The doc comment of bio_mc_hdr_verify() listed the checks the function
performs. The list repeats the code and the return codes documented
below it, and has to be edited each time a check changes. The checks
are described where they are implemented, in the comment of
meta_hdr_verify().

Keep what a caller needs to know: what the function verifies, that it
is read-only (unlike bio_mc_open(), which upgrades a version 1 meta or
WAL header when it opens the blobs) and what it is for.

Comment only, no functional change.

Signed-off-by: Cedric Koch-Hofer <cedric.koch-hofer@hpe.com>
@daosbuild3

Copy link
Copy Markdown
Collaborator

Test stage Functional Hardware Medium MD on SSD completed with status FAILURE. https://jenkins-3.daos.hpc.amslabs.hpecorp.net//job/daos-stack/job/daos/view/change-requests/job/PR-19260/2/execution/node/1836/log

@daosbuild3

Copy link
Copy Markdown
Collaborator

Test stage Functional Hardware Medium Provider MD on SSD completed with status FAILURE. https://jenkins-3.daos.hpc.amslabs.hpecorp.net//job/daos-stack/job/daos/view/change-requests/job/PR-19260/3/execution/node/1864/log

@daosbuild3

Copy link
Copy Markdown
Collaborator

Test stage Functional Hardware Medium MD on SSD completed with status FAILURE. https://jenkins-3.daos.hpc.amslabs.hpecorp.net//job/daos-stack/job/daos/view/change-requests/job/PR-19260/3/execution/node/1807/log

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

CR Catastrophic Recovery Feature meta-on-ssd Metadata on SSD Feature

Development

Successfully merging this pull request may close these issues.

5 participants