Filesystems · Data Integrity

Checksum Placement: Block Pointers, Metadata Trees, and Payloads

Where a filesystem can store a block checksum, why the pointer is usually the right home for it, and how each placement choice changes what a torn write can do.

A checksum has to live somewhere, and the choice of where determines which failures it can catch. Three placements are possible: inside the data block itself, in the block pointer that references the block, or in a separate side table indexed by block number. Each has been used by real filesystems, each has a distinct failure mode, and the reasoning that separates them is a case study in how atomicity requirements propagate downward into layout decisions.

The payload-adjacent layout and its atomicity problem

The simplest placement puts the checksum at the head of the block it protects: a four-byte field followed by the payload, with the checksum computed over the payload only. A reader fetches the block, splits off the checksum, recomputes over the remainder, and compares. There is no separate structure to keep consistent and no pointer format to extend.

The problem is that the checksum and the data it describes share a fate. A filesystem’s unit of update is typically larger than a device sector: a 4 KiB metadata node or a 128 KiB data block is written as a sequence of 512-byte or 4096-byte sector writes, and power loss between them leaves a mixture. If the checksum sits in the first sector and the payload in the rest, a torn write can install a new checksum over a payload that is partly old — and the reader then verifies the new checksum against a block whose contents are a blend that the writer never computed. Whether that verifies depends on where the tear fell. If the tear lands after the checksum sector but before the payload sectors, the checksum matches nothing and the block is correctly rejected. If the checksum sector is written last and the payload sectors first, a tear before the checksum leaves old checksum over new data, also rejected. The dangerous ordering is the one where the device or the filesystem writes the checksum sector together with the leading payload sectors and defers the trailing ones, because then the recomputation covers a prefix that is new and a suffix that is old.

That specific hazard is avoidable by ordering, but the general point stands: a checksum stored with its data can only attest to the state of the block at the moment the checksum was written, and any later partial update of the block invalidates the relationship in ways the reader cannot distinguish from media damage.

Payload-adjacent checksums survive in filesystems that treat the whole block as an atomic unit through some other mechanism, and in formats where the checksum is inside a larger structure that is itself protected. ZFS stores data checksums outside the data, in the block pointer, precisely to keep the two separable.

The pointer-side layout

In a tree-structured filesystem, every data block is named by a block pointer in its parent. That pointer already carries the block’s address, its birth transaction, its type, and its compression and encryption parameters. Adding a checksum field costs a fixed number of bytes in a structure that already exists and already must be written atomically as part of the parent’s update.

ZFS is the clearest example. Its block pointer (blkptr_t) is 128 bytes and holds a single 256-bit checksum alongside up to three DV (data virtual address) copies, along with the embedded birth time, logical and physical size, compression type, byte order, and object type. The checksum is recorded once even when a block is stored redundantly; every copy is verified against the same value, so a reader can tell whether a specific copy is bad rather than only that some copy is bad. The pointer itself is protected by the checksum of the block that contains it, so a corrupted pointer is caught when the parent is verified — the failure is detected one level up rather than silently redirecting a read.

The consequence for torn writes is what makes this placement attractive. A data block is written in full, then the pointer naming it is written in full as part of the parent block’s update. Until the pointer lands, the new data block is unreachable and its contents cannot be observed through the tree. After the pointer lands, the parent’s own checksum covers the pointer, so the pair is verified together. There is no window in which a reader can see a pointer to a half-written block, because the pointer only becomes visible when the parent block containing it becomes visible, and the parent block is written and verified as one unit.

btrfs uses the same arrangement with different constants. Its file extent items carry CRC32C values for the data they reference, and its tree blocks carry a checksum in the block header computed over the header and the node contents. The header checksum is stored inside the block it protects, which sounds like the payload-adjacent case, but a btrfs tree block is written as one unit through a copy-on-write path and is never updated in place, so the tear hazard is bounded by the same ordering discipline that makes the tree safe. The header checksum covers the header’s own address field as well as the node body, so a tree block moved to a different logical address fails verification even though its contents are intact.

The cost of pointer-side placement is structural: every pointer grows, and pointers are the most numerous structure in the filesystem. A 128-byte block pointer against a 16-byte extent record means eight times the metadata for the same data, which is why filesystems with pointer-side checksums also tend to have large blocks, aggressive compression of metadata, and a tree that stores many pointers per node to amortise the traversal.

Side tables and the indirection they introduce

A third option stores checksums in a dedicated structure keyed by physical block number, separate from both the data and the pointer. The advantage is that the checksum survives independently of the filesystem’s own metadata: if the tree is destroyed, the side table can still be used to audit the medium. The disadvantage is that the table must be updated whenever a block is written, which means an extra write per data write, and it must be kept consistent with the mapping from logical to physical addresses.

ext4’s metadata checksum feature illustrates a partial version of this. Metadata blocks — superblock, group descriptors, inode bitmaps, extent trees, directory blocks, inodes — carry a CRC32C in a field within the block, and the checksum is verified when the block is read. The seed is not the block’s address but its context: the filesystem UUID, plus the inode number and generation for anything that belongs to an inode, plus the group number for a group descriptor. Seeding with inode identity is a defence against relocation similar in effect to seeding with an address, because a metadata block copied into another inode’s slot fails verification even when its payload is byte-identical. Data blocks in ext4 are not checksummed at all, which is a deliberate scope decision rather than an oversight — the filesystem provides metadata integrity and leaves data integrity to layers above.

That seeding trick generalises. Any checksum that includes the block’s address in its input detects misdirected writes, because a block written to address A but verified as address B produces a different digest even when the payload is identical. ZFS does not need this because the pointer carries the checksum, but a scheme that verifies blocks without consulting a pointer — a scrub pass reading raw sectors, or a side table — gets the same protection from seeding.

What placement decides

The three placements differ in what a reader can conclude from a failed verification, and that difference matters more than the byte cost.

With a pointer-side checksum, a failure localises the fault to the block or to the pointer, and the pointer’s own protection tells you which. With a payload-side checksum, a failure says the block is internally inconsistent but not whether the inconsistency came from the write or the medium. With a side table, a failure is ambiguous between the data and the table, and resolving it requires a third source of truth.

They also differ in whether the checksum is available to a process that has only the block. A scrubber reading raw sectors cannot verify a pointer-side checksum without reconstructing the pointer, which means walking the tree; a payload-side checksum can be verified from the sector alone. This is why scrubbing is described as walking metadata and reading data, rather than as a linear sweep of the device — the traversal is the mechanism, and scrub passes and the discovery of latent damage covers how the two styles of pass trade thoroughness against cost.

One further constraint pushes most designs toward the pointer. Verification must happen before the data is returned to a caller, and the only component that knows which block a caller’s read corresponds to is the one holding the pointer. A checksum stored anywhere else requires an extra lookup on the read path, or a trust assumption that the block being verified is the block that was requested. The pointer is where the intent lives, so the pointer is where the expectation should be recorded.

References

  1. The Zettabyte File System — J. Bonwick, M. Ahrens, V. Henson, M. Maybee, M. Shellenbaum, FAST 2003
  2. btrfs documentation — On-disk format — btrfs project
  3. ext4 Data Structures and Algorithms — Checksums — Linux kernel documentation