Analyses / Incremental recovery
Keeping Btrfs incremental backups recoverable
18 October 2026 / Source review and controlled parser experiment
Keep an unchanged replication base, inventory every external clone source, and isolate the destination while receiving. A readable delta stream and a matching UUID are not a replacement for those conditions or for a restore test.
The backup includes its dependencies
A Btrfs send stream carries reconstruction commands. With -p, the receiver starts from a previously sent parent; -c makes additional snapshots available for cloning. Without an explicit parent, send may choose one from those clone sources. All snapshots participating in send must have their subvolume read-only property set; a read-only mount alone is insufficient. [1]
Retention follows references, not filenames. Our practical inference: a stored incremental stream is usable only with the state it actually references. Keeping every chronological predecessor is not the rule; preserving a reconstructible parent and the external clone sources used by the stream is. Audit dependencies before pruning either snapshots or archived streams.
| Input | What must survive | Acceptance check |
|---|---|---|
| Full send | The complete data-bearing stream | Receive and validate the intended subvolume |
| Incremental send | Stream, matching parent and referenced external clone sources | Resolve dependencies and test reconstruction |
--no-data | Metadata description only | Reject as a data-recovery copy |
The last mode deliberately omits file payload and cannot transfer file-data changes. Clone sources must represent identical state on both sides. [1] This dependency inventory complements the existing snapshot and backup comparison.
A received UUID identifies lineage, not content
The destination has its own subvolume UUID. received_uuid records the source identity used for replication. The stream format represents snapshot and clone references with UUID and transaction fields; it is abstracted from disk structures, and destination inode numbers can differ. Replication should not be described as a sector image. [3] [4]
There is a consequential implementation detail. In btrfs-progs v6.17, search_source_subvol first searches received UUIDs, then falls back to local UUIDs. It passes a transaction argument into subvol_uuid_search. [5] However, the latter function's body does not use its transid parameter: it looks up the UUID, reads root metadata and resolves a path. It performs no file-content comparison. [6, subvol_uuid_search]
Therefore, describing that lookup as a verified UUID-and-generation match overstates the code. This is a narrow source observation, not a claim that all receiver or kernel checks are absent, nor a demonstrated corruption case. Record identity metadata, but do not treat it as a content digest.
Restore into a working copy, not the replication base
Btrfs documents safeguards introduced in btrfs-progs 5.14.2 for switching a received snapshot to writable state. Older writable subvolumes carrying a received identity can still exist. Installing newer tools does not establish that those historical objects are valid bases. Nested subvolumes are another inventory boundary: their contents are excluded from a parent snapshot, and even the empty snapshot stubs are omitted by send. [3]
The pinned prop_read_only implementation refuses a read-only-to-writable switch with a received UUID unless forced. Its forced path changes the flag and then attempts to clear the identity. A reset failure emits a warning; that branch does not turn the function's final return into failure. [7] This is source inspection, not an observed ioctl failure.
Our operational conclusion is to keep the received base unchanged and create a separate writable snapshot for recovery work. Do not use force as routine error handling. When investigating a prior state change, retain warnings and inspect the resulting flags and identity, rather than inferring them from the requested action.
Stream validation stops before restoration
The v6.17 stream reader recomputes each command's CRC32C with its checksum field zeroed, rejects mismatches, and checks attribute boundaries. These are checks of command representation, not an end-to-end comparison with the source filesystem. [8]
btrfs receive --dump selects printing callbacks instead of do_receive. [5] It leaves the filesystem unchanged. [2] Our inference is that a successful dump cannot establish that a target possesses the necessary parent, has enough space, or reconstructs an application's usable state.
There is also a trust boundary. The receive manual warns against untrusted streams: crafted incremental instructions can create references to files elsewhere in the same filesystem. It recommends protecting trusted streams in transit. [2, BUGS] A command CRC is not sender authentication. A trusted transport and a restore test address different requirements.
A checksum pass does not prove a complete backup
We constructed six small protocol-v1 streams and passed each once to btrfs receive --dump, using the installed btrfs-progs v6.6.3 on Linux. This does not apply commands to a destination. The original Python harness builds the command bytes and records their SHA-256, stdout, stderr and exit status. This runtime is distinct from the v6.17 source review above.
| Controlled input | Bytes | Exit status |
|---|---|---|
| Six payload bytes in one WRITE command | 156 | 0 |
| Whole WRITE removed; all remaining command checksums unchanged | 113 | 0 |
| One payload bit flipped without updating its command checksum | 156 | 1 |
| END command removed | 146 | 1 |
| Last payload byte and END absent | 145 | 1 |
| Synthetic parent UUID; no destination or parent provided to dump | 112 | 0 |
The bit flip, incomplete command and missing END were rejected. Yet removing the entire WRITE command left a stream that passed: no surviving command's CRC needed changing. Its dump listed file creation but no write. This is a deliberately different command sequence, not evidence of spontaneous data loss. Per-command checksums cannot determine which operations the sender originally intended.
The synthetic snapshot reference also passed without supplying any destination or parent. This directly demonstrates the scope of dump mode, not a failure of the real receive path. A parent-availability or restored-content claim would need a separate filesystem experiment.
For archived streams, our recommendation is to retain a trusted whole-stream digest and byte length, check the producer's completion status, then test reconstruction and application invariants. A digest computed only after an incomplete transfer cannot prove what was omitted. Authentication protects the expected digest; none of these checks replaces the others.
Exact observations and environment / Original reproduction harness. Run python3 btrfs-dump-controls.py --verify btrfs-dump-controls.json; a differing runtime can produce different output and must be reported separately. No storage devices or elevated privileges are required.
Read-only at the end does not protect the whole receive
In finish_subvol, receive records source identity and then sets BTRFS_SUBVOL_RDONLY. [5] The manual explicitly warns that other writers can alter the destination during reception, leaving a final read-only subvolume that differs from the sender. Protect that location until reception completes. [2]
For an acceptance record, retain the tool and kernel versions, stream provenance, parent/clone inventory, completion status, warnings and final subvolume identity. Then validate the recovered files and application. These are our recommended checks, not a claim that any particular backup has passed them. See backup verification and application recovery for the separate acceptance criteria.
Application recovery needs its own check
Pillai et al.'s OSDI 2014 study separates filesystem persistence behavior from application correctness. BOB explores reordered block traces; ALICE tests modeled crash states against workload-specific checker scripts. Neither is a Btrfs send validator. [9, sections 2.2 and 3.1-3.6]
Our transfer of that method is modest: define the application invariant before accepting a restore, such as matching committed records and their referenced files. A parser pass, or even a matching byte copy, cannot establish that an originally inconsistent application state has become usable. We did not reproduce ALICE, and its historical results are not current Btrfs failure rates.
Evidence and limits
This synthesis reviews eight upstream files at btrfs-progs v6.17, commit c03e29f083678f47a7d7d810db4d21d905979b77. No Btrfs volume was mounted, modified or restored. The separate six-case experiment exercised only the installed v6.6.3 dump parser with synthetic byte streams; no crash, real receive or filesystem fault injection was executed. Distribution changes and later versions need separate review.
The source manifest and SHA-256 records with source locations identify the reviewed inputs. The capture script verifies those bytes when run with --verify beside the two JSON files. It does not validate filesystem behavior. Compare the distinct Btrfs mount-time replay boundary before interpreting any read-only recovery claim.
Primary sources
- btrfs-progs v6.17: send modes and clone-source preconditions
- btrfs-progs v6.17: receive validation, write window and trust warning
- btrfs-progs v6.17: received identity, 5.14.2 safeguards and nested subvolumes
- btrfs-progs v6.17: send stream format and reference fields
- btrfs-progs v6.17: source lookup, receive finalization and dump dispatch
- btrfs-progs v6.17: subvol_uuid_search implementation
- btrfs-progs v6.17: prop_read_only and UUID reset error handling
- btrfs-progs v6.17: per-command CRC and attribute decoding
- Pillai et al., All File Systems Are Not Created Equal, OSDI 2014, sections 2.2 and 3.1-3.6