Skip to content

refactor(blaze)!: backend-agnostic checkpoint payload layout - #2677

Closed
WeissonHan wants to merge 2 commits into
alibaba:mainfrom
WeissonHan:codex/pr2476-agnostic-hibernate-20260819
Closed

refactor(blaze)!: backend-agnostic checkpoint payload layout#2677
WeissonHan wants to merge 2 commits into
alibaba:mainfrom
WeissonHan:codex/pr2476-agnostic-hibernate-20260819

Conversation

@WeissonHan

Copy link
Copy Markdown
Collaborator

Why

The checkpoint format freezes one backend's shape into the daemon: capture
hands every backend exactly two named VM files (vmstate.snap,
memory.snap), and both the store and the restore transaction validate
committed checkpoints against a fixed three-file whitelist. A backend whose
native checkpoint is a directory tree — runsc checkpoint writes an image
directory plus the OCI spec that restore revalidates byte-for-byte — cannot
express its payload in that contract at all. The same whitelist blocks
sandbox hibernation (#2464) from carrying anything but the VM pair, and the
capture orchestration's unconditional pause/resume rejects backends whose
capture primitive freezes the workload itself.

What changed

  • Checkpoints now carry two producer-owned payload subtrees: backend/
    belongs to the backend adapter and its internal layout is private to that
    backend; storage/ belongs to the storage provider and holds
    rootfs.snap. SnapshotRequest passes one payload directory instead of
    two file paths; RestoreRequest hands the same subtree back pinned through
    the retained directory descriptor.
  • Integrity moves from the whitelist to the manifest: publication walks both
    subtrees, rejects symlinks and non-regular files, hashes every regular file
    under its slash-separated relative path, and commits a canonically sorted
    inventory. Load and every revalidation boundary require the directory and
    the manifest to match exactly; artifact paths are validated as pure
    relative paths with bounded depth before the daemon resolves them.
  • The metadata format moves to version 2. Version-1 checkpoints map both
    producer subtrees onto the checkpoint root and stay restorable in place;
    new captures always publish version 2.
  • Capture orchestration quiesces backends through new
    BackendInstance::quiesce_for_capture / unquiesce_after_capture hooks
    (defaulting to pause/resume), so a self-freezing backend can override
    both as no-ops without touching the transaction.
  • The guest mock now produces and restores a directory-shaped payload
    (image/ plus bundle/config.json, mirroring a runsc checkpoint),
    proving the contract carries a container-backend layout end to end; the
    plain mock keeps the flat VM pair so both shapes stay covered.
  • English and Chinese user guides and changelogs document the new layout,
    the manifest inventory semantics, and the version-1 compatibility rule.

Related issue

Closes #2676

User / Agent impact

The checkpoint API response manifest now reports format_version: 2 and
artifact names as subtree-relative paths (for example
backend/vmstate.snap, storage/rootfs.snap). Checkpoints captured before
this change remain restorable; daemons older than this change cannot read
version-2 checkpoints.

Risk and compatibility

  • BREAKING (on-disk): new checkpoints use the subtree layout and format
    version 2. Version-1 images are read-only compatible.
  • The Firecracker adapter gains its snapshot()/restore() implementation
    in feat(blaze): capture Firecracker checkpoints #2473; this PR stays a draft until feat(blaze): capture Firecracker checkpoints #2473 lands, then absorbs the
    adapter's migration to the subtree contract as its final change before
    review completion.
  • Failpoint names on the capture/restore boundaries are unchanged, so the
    existing crash-injection coverage carries over unmodified.

Testing

  • cargo fmt --check, cargo clippy --workspace --all-targets --all-features -- -D warnings: clean.
  • blaze-core: 73/73 pass. blazed (default features, macOS): failure set
    is byte-identical to the main baseline (102 platform-specific failures
    caused by /var symlink validation on macOS; zero new, zero resolved).
  • New coverage: directory-shaped guest-mock capture→restore roundtrip;
    nested-payload publish/verify/pin; unregistered-file, empty-payload,
    traversal-path, and symlink negative cases; hand-written version-1
    checkpoint verification and root mapping; format-2 assertion on new
    captures.
  • Full Linux gate (default + all-features, including failpoint suites) is
    expected from CI; the failpoint tests only run meaningfully on Linux.

@github-actions github-actions Bot added component:blaze src/blaze scope:documentation ./docs/|./*.md|./NOTICE labels Aug 19, 2026
@WeissonHan

Copy link
Copy Markdown
Collaborator Author

@codex review this PR makes the checkpoint payload layout backend-agnostic: producer-owned subtrees (backend/, storage/) with manifest-driven inventory validation replacing the fixed three-file whitelist, format v2 with v1 read-only compatibility, plus overridable capture-quiesce hooks.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4b45877b6b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines 223 to +224
let paused = match crate::failpoint::backend("checkpoint-pause") {
Ok(()) => backend.pause().await,
Ok(()) => backend.quiesce_for_capture().await,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep no-op hooks quiesced through rootfs capture

When a self-freezing backend follows the new contract and overrides both capture hooks as no-ops, its snapshot primitive can return with the workload running, but storage synchronization and rootfs.snap capture happen only afterward. Writes made after the backend snapshot can therefore enter the rootfs image without appearing in the captured process or memory state, producing an internally inconsistent checkpoint; the workload must remain stopped until both producer subtrees are captured.

AGENTS.md reference: src/blaze/AGENTS.md:L30-L30

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 9fa4fab41. The hook contract now states explicitly that the quiesce must hold until unquiesce_after_capture, because storage synchronization and the rootfs capture run after snapshot() returns; a capture primitive that restarts the workload itself (leave-running style) must not pair with no-op hooks. Documented on quiesce_for_capture and in AGENTS.md. Both in-tree implementations keep the pause/resume defaults, so no behavior changes here.

Comment thread src/blaze/crates/blazed/src/checkpoint_store.rs Outdated
Comment thread src/blaze/crates/blazed/src/checkpoint_store.rs Outdated
Comment thread docs/user-guide/zh/runtime/blaze.md
@WeissonHan
WeissonHan force-pushed the codex/pr2476-agnostic-hibernate-20260819 branch from 4b45877 to 9fa4fab Compare August 19, 2026 11:49
@WeissonHan

Copy link
Copy Markdown
Collaborator Author

@codex review all three P1s and the P2 are addressed: payload paths handed to producers are now the child-resolvable configured pathnames with the integrity chain documented (retained-descriptor hashing + pre-HEAD identity revalidation), the walker validates every produced artifact path, the quiesce-hook contract states the workload must stay quiesced until unquiesce_after_capture, and the zh manifest example says format_version 2.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9fa4fab410

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/blaze/crates/blazed/src/checkpoint_store.rs
/// Version 2 hands each payload producer an owned subtree instead of a fixed
/// pair of named VM files, so directory-shaped payloads (for example a runsc
/// checkpoint image) commit without changing the format again.
pub const CHECKPOINT_FORMAT_VERSION: u32 = 2;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Update the checkpoint protocol design documentation

This changes the on-disk checkpoint protocol to version 2, but the untouched canonical design at src/blaze/docs/design/storage-artifact-synchronization.md:72-79 still says publication validates a fixed backend-state, memory, and writable-root artifact set. That contradicts the new manifest-driven backend subtree contract, and specs/documentation-standard.md:269 requires architecture or protocol changes to update the component design documentation.

AGENTS.md reference: AGENTS.md:L337-L343

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in fc748f7bf. docs/design/storage-artifact-synchronization.md (and the Chinese counterpart) now describe the producer-owned subtrees, the symlink/non-regular rejection, the mandatory writable-root capture, and the manifest-inventory validation instead of the fixed three-artifact set.

Comment thread src/blaze/CHANGELOG.md Outdated
@WeissonHan
WeissonHan force-pushed the codex/pr2476-agnostic-hibernate-20260819 branch from 9fa4fab to d3438e9 Compare August 19, 2026 13:05
@WeissonHan

Copy link
Copy Markdown
Collaborator Author

@codex review round-2 findings addressed: publication and v2 load now require the storage/rootfs.snap capture, the storage-artifact-synchronization design docs describe the subtree/manifest protocol, changelog entries are removed per the documentation standard, and removal traverses with its own generous depth bound so rejected payloads stay removable.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d3438e9364

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/blaze/crates/blazed/src/checkpoint_store.rs Outdated
@WeissonHan
WeissonHan force-pushed the codex/pr2476-agnostic-hibernate-20260819 branch from d3438e9 to a3111b9 Compare August 19, 2026 17:15
@WeissonHan

Copy link
Copy Markdown
Collaborator Author

@codex review round-3 finding addressed: removal classifies entries via statat(SYMLINK_NOFOLLOW) and unlinks every non-directory, so payloads rejected by publication (symlinks, FIFOs, sockets, devices) always stay removable; covered by a regression test.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a3111b9a4b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +427 to +432
let mut found = Vec::new();
let mut dirs = Vec::new();
for subtree in [PAYLOAD_BACKEND_DIR, PAYLOAD_STORAGE_DIR] {
let payload =
required_child_directory(directory, subtree, "open checkpoint payload subtree")?;
collect_payload_files(&payload, subtree, 1, &mut found, &mut dirs)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Include empty directories in payload integrity

When a backend-private payload uses a required empty directory, publication and revalidation collect that directory into dirs but compare only regular-file paths against the manifest. Deleting or injecting such a directory after publication therefore passes verify_restore_target and the later HEAD revalidation, even though the restore adapter consumes a different layout; a missing required directory can make restore fail only after the current backend has been stopped. Empty directories must either be represented in the integrity inventory or rejected during publication.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 591311aa2. collect_payload_files now returns a file count; a subtree entry that resolves to a directory with zero regular files fails publication. Regression test: publish_rejects_an_empty_payload_directory.

Comment on lines +48 to +52
/// Deepest nesting removal will traverse. Deliberately far above
/// [`MAX_PAYLOAD_DEPTH`]: a payload rejected for being too deep must still
/// be removable by the compensation path, so this bound only protects the
/// daemon from descriptor exhaustion on a pathological tree.
const MAX_REMOVAL_DEPTH: usize = 256;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep all rejected payload depths removable

When a faulty or hostile backend creates 256 or more nested directories below backend/, publication rejects the tree at the 16-level payload limit, but abort first renames the stage to a tombstone and then this separate 256-level removal limit rejects cleanup. Subsequent destroy or reconciliation attempts use the same helper, so the sandbox remains in RecoveryRequired until the tree is removed manually; use bounded iterative traversal or otherwise ensure every producer-created tree that publication rejects can still be deleted.

AGENTS.md reference: src/blaze/AGENTS.md:L32-L37

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Intentionally not changed. MAX_REMOVAL_DEPTH=256 is a descriptor-exhaustion guard for the daemon, not a security boundary: the backend adapter is an in-process trusted implementation (same threat model as the pre-split format, whose pinned fd also could not stop a same-UID content write). A payload rejected for exceeding MAX_PAYLOAD_DEPTH=16 is still fully removable because 256 >> 16. The constant and its rationale are documented inline.

The checkpoint format froze one backend's shape into the daemon: capture
handed every backend exactly two named VM files (vmstate.snap, memory.snap),
and both the store and the restore transaction validated committed
directories against a fixed three-file whitelist. A backend whose native
checkpoint is a directory tree - runsc writes an image directory plus the
OCI spec that restore must revalidate byte-for-byte - cannot express its
payload in that contract at all, so every non-VM backend would have had to
fake VM file names or fork the format.

Split the payload by producer instead. A checkpoint now carries two owned
subtrees: backend/ belongs to the backend adapter and its internal layout is
private to that backend; storage/ belongs to the storage provider and holds
the rootfs capture. SnapshotRequest passes one payload directory rather than
two file paths, RestoreRequest hands back the same subtree pinned through
the retained directory descriptor, and version-1 checkpoints map both
subtrees onto the checkpoint root so everything captured before the split
stays restorable in place.

With backend-private layouts a name whitelist can no longer exist, so
integrity moves to the manifest: publication walks both subtrees, rejects
symlinks and non-regular files, hashes every regular file under its
slash-separated relative path, and commits a canonically sorted inventory.
Load and every revalidation boundary then require the directory and the
manifest to match exactly - a missing, altered, or unregistered file fails
closed, which is the same guarantee the whitelist gave, now derived from
the record each checkpoint carries. Artifact paths are validated as pure
relative paths (no traversal, bounded depth and length) before the daemon
ever resolves them, and removal recurses with the same bound.

The metadata format moves to version 2; version-1 manifests keep their
frozen artifact-set validation and stay read-only. The guest mock now
produces a directory-shaped payload (image/ plus bundle/config.json,
mirroring a runsc checkpoint) and restores from it, proving the contract
carries a container-backend layout end to end, while the plain mock keeps
the flat VM pair so both shapes stay covered.

BREAKING: checkpoints written by this daemon use format version 2 and a
subtree layout that older daemons cannot read. Existing version-1
checkpoints remain restorable but are never re-captured in place.

Assisted-by: Qoder
Signed-off-by: Weisson <Weisson@linux.alibaba.com>
@WeissonHan
WeissonHan force-pushed the codex/pr2476-agnostic-hibernate-20260819 branch from a3111b9 to 2bb38e2 Compare August 20, 2026 01:56
@WeissonHan

Copy link
Copy Markdown
Collaborator Author

@codex review R5: empty-payload rejection, parent linkage verification, supervisor pattern for hibernate/resume, removal-depth trust model documented

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2bb38e2504

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +1925 to +1927
let child =
required_child_directory(&directory, &entry, "open checkpoint scratch directory")?;
remove_owned_directory_bounded(&directory, &entry, child, depth + 1)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Handle non-UTF-8 names during recursive removal

When a backend creates a non-UTF-8 filename anywhere in its owned payload subtree, publication rejects it through directory_names, but abort follows this recursive path and invokes the same UTF-8-only scanner before unlinking the entry. The stage therefore cannot be removed, leaving the sandbox in RecoveryRequired and causing every later cleanup attempt to fail on the same name; recursive removal should enumerate raw OsString names even though publication cannot encode them in the manifest.

AGENTS.md reference: src/blaze/AGENTS.md:L32-L37

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 2520bc02a (now part of the combined #2678). Both the checkpoint and hibernate removal paths now enumerate raw OsString names via directory_names_os / names_os (using OsStr::from_bytes on the rustix &CStr file names). Non-UTF-8 directory entries are classified by statat(SYMLINK_NOFOLLOW) and either recursed into or unlinked, so a backend that creates non-UTF-8 names no longer strands the sandbox in RecoveryRequired.

Checkpoint capture unconditionally paused the backend before invoking its
snapshot and resumed it afterwards. That sequence encodes a VM assumption:
Firecracker-style backends expose no consistent state until the VM is
paused from outside. Backends built on a self-freezing capture primitive
break under it - runsc checkpoint stops the container itself, and runsc
rejects state operations against a container that is already paused, so
the orchestration's own pause would poison the capture it prepares.

Route the sequence through a pair of orchestration hooks on
BackendInstance instead. quiesce_for_capture brings the workload to a
consistent stop before the snapshot and defaults to pause;
unquiesce_after_capture returns it to execution on both the publication
and the compensation path and defaults to resume. Existing backends keep
their exact behaviour through the defaults, while a self-freezing backend
overrides both hooks as no-ops and leaves pause/resume unimplemented.

The capture flow, its HEAD-update resume, and the compensation resume all
call the hooks now; the failpoint names covering these boundaries are
unchanged, so existing crash-injection coverage carries over as is.

BREAKING: backend implementations that overrode pause/resume purely to
participate in capture keep working through the defaults, but the
documented integration contract for new backends is now the hook pair.

Assisted-by: Qoder
Signed-off-by: Weisson <Weisson@linux.alibaba.com>
@WeissonHan
WeissonHan force-pushed the codex/pr2476-agnostic-hibernate-20260819 branch from 2bb38e2 to 2520bc0 Compare August 20, 2026 02:22
@WeissonHan

Copy link
Copy Markdown
Collaborator Author

Merged into #2678 as a stacked 3-commit series: payload subtrees → quiesce hook → hibernate rebuild.

@WeissonHan WeissonHan closed this Aug 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component:blaze src/blaze scope:documentation ./docs/|./*.md|./NOTICE

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[blaze] refactor: backend-agnostic checkpoint payload layout

1 participant