Before you start#
Read ext4 and journaling. Linux v6.12 is the historical source baseline here. Btrfs and XFS are filesystems; OverlayFS is a union view built on other filesystems. Feature availability, mount options, backing filesystems and hardware all matter. Do not format or mount host devices for this lesson.
Learning goals#
Compare three distinct source paths, explain copy-on-write and reference sharing, distinguish XFS logging from OverlayFS copy-up, and reason about failures without claiming identical persistence guarantees.
Source walkthrough#
- Btrfs buffered write
- btrfs_do_write_iter → btrfs_buffered_write; ordered I/O and fsync logging occur later.
- XFS buffered write
- xfs_file_write_iter → iomap buffered write; writeback, ioend and fsync follow separately.
- OverlayFS copy-up
- Lower object may acquire an upper representation; whiteouts hide lower names.
Btrfs: Copy-on-write (COW) can preserve old versions by allocating new blocks and updating references instead of overwriting shared blocks in place. Trees store metadata and extent references track sharing; snapshots and reflinks can therefore share extents until a modification requires divergence. Metadata and, under the applicable configuration, file data have checksums. Checksums detect corruption under their coverage and can support repair when a valid redundant copy is available; they do not guarantee repair or authenticate data against an attacker. NOCOW and checksum-related options alter these assumptions. See fs/btrfs/ctree.c, fs/btrfs/inode.c and Btrfs documentation.
btrfs_cow_block is a metadata-tree block operation, not a complete file-write call chain. In its implementation, tree changes use a transaction handle and can require a new block; parent references must be updated according to the tree rules. btrfs_finish_ordered_io in inode.c handles completion of an ordered extent, connecting completed data I/O to metadata work. Failure, abort and transaction commit have separate consequences. A snapshot is not an independent backup against destruction of the entire underlying device.
XFS: XFS uses extents, allocation groups, metadata logging and iomap-based paths. Its log records metadata changes for recovery; it is not the same as Btrfs's general tree COW scheme. In fs/xfs/xfs_file.c, xfs_file_read_iter chooses between DAX, direct and buffered reads. A short excerpt (GPL-2.0; surrounding code omitted) is:
if (xfs_is_shutdown(mp))
return -EIO;
A forced shutdown prevents this read from proceeding normally. The next relevant dispatch is:
if (IS_DAX(inode))
ret = xfs_file_dax_read(iocb, to);
else if (iocb->ki_flags & IOCB_DIRECT)
ret = xfs_file_dio_read(iocb, to);
else
ret = xfs_file_buffered_read(iocb, to);
The inode's DAX state is checked first, then the direct-I/O request, otherwise the buffered method. The returned value is accounted when positive. fs/xfs/xfs_iomap.c supplies XFS mapping operations used by relevant iomap paths. These branches are XFS-specific dispatch decisions, not evidence that every filesystem calls XFS code.
OverlayFS: An overlay combines lower layers with an upper writable layer into a merged namespace. A lower-only object may need copy-up before modification. ovl_copy_up_one obtains lower attributes and checks that IDs can be mapped; failure aborts that operation. Its helper ovl_copy_up_data includes (GPL-2.0; surrounding lines omitted):
In ovl_copy_up_one, v6.12, SPDX GPL-2.0-only (surrounding code omitted):
if (!kuid_has_mapping(current_user_ns(), ctx.stat.uid) ||
!kgid_has_mapping(current_user_ns(), ctx.stat.gid))
return -EOVERFLOW;
/*
* With metacopy disabled, we fsync after final metadata copyup, for
* both regular files and directories to get atomic copyup semantics
* on filesystems that do not use strict metadata ordering (e.g. ubifs).
*
* With metacopy enabled we want to avoid fsync on all meta copyup
* that will hurt performance of workloads such as chown -R, so we
* only fsync on data copyup as legacy behavior.
*/
ctx.metadata_fsync = !OVL_FS(dentry->d_sb)->config.metacopy &&
(S_ISREG(ctx.stat.mode) || S_ISDIR(ctx.stat.mode));
Copy-up returns -EOVERFLOW when a lower owner or group has no mapping in the current user namespace, so the operation fails rather than skipping the copy. The comment ties metadata_fsync to whether metacopy is disabled, so the fsync after copy-up depends on configuration. The excerpt does not show the data copy or the install step.
if (!S_ISREG(c->stat.mode) || c->metacopy || !c->stat.size)
return 0;
This helper skips data copying for a non-regular file, a metadata-only copy-up, or a zero-length file. Otherwise it opens the temporary upper object, copies data and releases the file reference; a failed open or copy propagates an error. Metadata-only copy-up, when enabled, can defer copying data. A whiteout in the upper layer hides a lower name after deletion from the merged view; it does not erase the underlying lower file. The implementation is in fs/overlayfs/dir.c; options and limitations are in OverlayFS documentation.
Three distinct worked sequences#
Take a hypothetical file initially containing old. On Btrfs, a snapshot and the original may reference the same file extent. A later modification of the original can allocate a new data extent and update metadata so the snapshot still references the old contents. Reference counts and transaction state govern reclamation: deleting one reference cannot free an extent still referenced by the snapshot. Checksums and redundancy determine whether corruption can be detected and potentially repaired; neither follows solely from the word COW.
The mapping below is a conceptual model of the hypothetical snapshot example. It is not a captured Btrfs layout.
Read the text version
- Original file: references extent A before the write → Extent B (new): allocated by the modification (after the write, original points to new data)
- Snapshot: still references extent A → Extent A (old): shared until divergence (keeps the old reference)
On XFS, opening the file and requesting an ordinary cached read can reach xfs_file_read_iter and its buffered branch, subject to the file's configuration. A direct request selects the direct branch when DAX does not take precedence. A forced shutdown instead returns -EIO. XFS metadata logging and recovery are a separate question from which read branch was used: reading data successfully does not prove a previous multi-file update was durable.
On OverlayFS, suppose lower contains name=old and upper initially has no name. The merged view reads the lower object. A modifying operation may copy the object to upper, after which the merged view can use the upper version. If data copy or metadata installation fails, the modifying operation must not be considered successful merely because a temporary upper object existed; cleanup and the reported error matter. A subsequent deletion can leave a whiteout, making the merged name absent while the lower object still exists. The exact sequence changes with metacopy, index, redirect and backing filesystem options. These are conceptual examples, not captured executions and not identical implementations of one transaction model.
Recovery, sharing, and practical limits#
Btrfs transactions coordinate updates to tree roots and metadata. An ordered extent tracks data I/O and the metadata work needed around its completion; the ordered-extent mechanism is not a promise that all application writes form one atomic transaction. Reflinked files and snapshots can share physical extents, so editing one may require new allocation. Space exhaustion can prevent that allocation even though the old shared data is still readable. A checksum mismatch is evidence of a mismatch against stored checksum metadata, not a guarantee that a usable alternative copy exists. For meaningful resilience, consider the data and metadata profiles, device failure model, scrubbing, backups and verified restore procedure separately.
XFS organizes allocation into allocation groups to support concurrency and scalability; iomap describes file ranges to shared I/O machinery. Its metadata log supports recovery after an unclean shutdown. Depending on configuration and operation, XFS also supports reflinks and COW for shared data; saying that XFS has a log does not mean it can never use copy-on-write. The important contrast is the specific path and on-disk design being discussed, not a simplistic claim that only one filesystem can share extents. An XFS forced shutdown can cause subsequent operations to return errors; applications need to surface those errors and administrators need a recovery plan.
OverlayFS layers may themselves be backed by different filesystems with different durability and feature behavior. The writable upper and work directories have constraints, and a lower file may remain accessible outside the merged mount even after a whiteout hides it in the merged view. Copy-up can require space for data and metadata; if the upper filesystem fills, a write can fail even when the lower file is perfectly readable. A metadata-only copy-up changes the interpretation of where data resides, so inspecting one pathname or one apparent inode number is not enough to infer every backing object. OverlayFS is a namespace and copy-up mechanism, not a substitute for backup, encryption or an application transaction protocol.
Comparison under failure#
The three systems have different units of work. A Btrfs tree-block COW operation updates filesystem metadata structure under a transaction; a file-data ordered extent tracks data I/O and its completion. An XFS read dispatch chooses a data-access path, while the XFS log concerns metadata recovery. OverlayFS copy-up constructs an upper representation of an object from a lower one; the upper filesystem then has its own allocation and durability rules. These descriptions are intentionally different because the cited functions do not form three versions of one shared call chain.
Suppose a shared Btrfs extent has two references. A modification needs new space, but allocation fails. The operation must report failure; the presence of the original shared extent does not imply that the new data was stored. Conversely, reference sharing means that deleting one name or snapshot is not enough to reclaim data still referenced elsewhere. Which data can be recovered after a crash depends on transaction and sync boundaries and the actual error, not just the reference count.
For XFS, the worked excerpt's forced-shutdown branch yields -EIO before choosing a read path. A program that interprets this as an empty file confuses an error with EOF. A positive short read, a zero return at EOF and a negative error are distinct outcomes. For a write or filesystem sync, the relevant methods and logging path must be inspected separately; one cannot infer their success from this read function. An XFS deployment can use reflinks, so a comparison must not assert that reference sharing is unique to Btrfs.
For OverlayFS, suppose the lower file is readable but the upper filesystem is full. Reading the lower-only merged name may work, while an operation requiring copy-up can fail due to the upper layer. A temporary upper file can be created before all data and metadata operations complete, so both cleanup and the operation's return value matter. Whiteout semantics then apply to the merged namespace: hiding a lower entry does not sanitize or remove the lower data. An application requiring durable replacement must understand both overlay behavior and the upper filesystem's sync guarantees. None of these examples establishes a benchmark ranking; workloads and configurations must be measured.
The flow below restates copy-up as a teaching model. It omits locking and the install step.
- Lower object
the merged name reads the lower object until copy-up
- Attributes
ovl_copy_up_one reads lower attributes; -EOVERFLOW if IDs cannot map
- Metadata policy
the metacopy setting decides whether data or only metadata is copied
- Data skip
ovl_copy_up_data skips non-regular, metacopy-only and zero-length files
- Data copy
opens the temporary upper object and copies data; errors propagate
- Failure
the modifying operation fails; upper space or I/O errors are reported
Btrfs and XFS: buffered writes through synchronization#
For Btrfs, the regular file operations table selects btrfs_file_write_iter, which calls btrfs_do_write_iter. That function checks for filesystem error and selects encoded, direct, or buffered handling. Its buffered selection in fs/btrfs/file.c, GPL-2.0, is:
} else {
num_written = btrfs_buffered_write(iocb, from);
num_sync = num_written;
}
This branch is conditional: encoded and direct writes take different paths. btrfs_buffered_write handles a buffered write into page-cache folios with reservations and delalloc state. Delayed allocation (delalloc) records work before all physical extent placement is finalized; writeback subsequently allocates and submits data. An ordered extent tracks a range whose data I/O and associated metadata completion must be coordinated. btrfs_finish_ordered_io completes ordered-extent work; a successful copy to cache is not that completion. A positive num_sync is passed to generic_write_sync, and a sync error can replace the positive byte result. The exact extent and compression path depends on configuration, flags, and file state.
btrfs_sync_file starts ordered operations for the whole file range in this version, coordinates with concurrent writes and ordered extents, and starts a transaction. The following excerpt shows the logging attempt:
ret = btrfs_log_dentry_safe(trans, dentry, &ctx);
The result is not automatically a completed durable log: later code releases context and either synchronizes the log or forces a transaction commit when required. For example, a negative logging result is converted into BTRFS_LOG_FORCE_COMMIT; transaction-start failure instead exits with an error. The source comments explain why fsync broadens the range and coordinates ordered extents. Log replay and full transaction commit have different costs and conditions. A sync request can fail, so userspace must check it. btrfs_buffered_write takes the inode lock, performs generic and Btrfs-specific checks, allocates a page-pointer array, and reserves space; the allocation failure returns -ENOMEM via cleanup. It faults in the source iterator before locking pages to avoid a recursive lock scenario. These are concrete early failures before assuming that all requested bytes were copied. See fs/btrfs/file.c and fs/btrfs/tree-log.c.
For XFS, xfs_file_write_iter chooses DAX, direct, or buffered operation. In the buffered branch, xfs_file_buffered_write acquires the inode I/O lock, performs write checks, and calls the shared iomap buffered-write machinery with XFS operations. Its central call in fs/xfs/xfs_file.c, GPL-2.0, is:
ret = iomap_file_buffered_write(iocb, from,
&xfs_buffered_write_iomap_ops, NULL);
The I/O control block supplies file and position; the iterator supplies user data; XFS's iomap operations determine the mapping. The implementation in fs/xfs/xfs_iomap.c handles mapping and delayed-allocation decisions. Later writeback submits data; xfs_end_ioend handles completion work including relevant extent conversion and size updates. These are later writeback/completion stages, not a claim that iomap_file_buffered_write synchronously calls the completion handler before returning. In xfs_end_ioend, an I/O error on a shared COW mapping cancels the COW range and punches the corresponding data-fork delalloc range before finishing the ioends. On success the function performs COW completion or unwritten-extent conversion when applicable, and updates file size for an append when no earlier error remains. These branches prevent treating data completion and metadata state transition as one unconditional event. For -EDQUOT or -ENOSPC, the buffered-write function may attempt a bounded cleanup/retry before returning an error.
The sequence below shows the buffered branch and its later ioend completion. Writeback runs after write has already returned.
- write(2)xfs_file_write_iter
write(fd, buf, n)
- xfs_file_write_iterxfs_file_buffered_write
buffered branch when not DAX or direct
- xfs_file_buffered_writeiomap_file_buffered_write
iomap_file_buffered_write with XFS iomap ops
- iomap_file_buffered_writexfs_file_buffered_write
Return. copied into the page cache
- xfs_file_buffered_writexfs_file_write_iter
Return. positive count
- xfs_file_write_iterwrite(2)
Return. return to caller
- writebackxfs_end_ioend
writeback submits ioend later
- xfs_end_ioendwriteback
Return. ioend completion: COW cancel, conversion or size update
Time runs downward. Dashed arrows show returns.
Read the text version
- write(2) → xfs_file_write_iter: write(fd, buf, n)
- xfs_file_write_iter → xfs_file_buffered_write: buffered branch when not DAX or direct
- xfs_file_buffered_write → iomap_file_buffered_write: iomap_file_buffered_write with XFS iomap ops
- iomap_file_buffered_write → xfs_file_buffered_write (return): copied into the page cache
- xfs_file_buffered_write → xfs_file_write_iter (return): positive count
- xfs_file_write_iter → write(2) (return): return to caller
- writeback → xfs_end_ioend: writeback submits ioend later
- xfs_end_ioend → writeback (return): ioend completion: COW cancel, conversion or size update
xfs_file_fsync first calls file_write_and_wait_range and returns on error. Depending on data/log device topology and inode state it can flush a device and force the log; the implementation treats a concurrent later modification differently from I/O already completed before fsync. A successful buffered write alone does not establish any of those later steps. The special DAX and direct branches, reflink COW, shutdown state, mount options, and separate log devices alter the path. Do not infer that Btrfs's ordered extent and XFS's ioend are interchangeable data structures.
In xfs_file_fsync, v6.12, SPDX GPL-2.0 (surrounding code omitted):
error = file_write_and_wait_range(file, start, end);
if (error)
return error;
if (xfs_is_shutdown(mp))
return -EIO;
A writeback error returns before the shutdown check, and after successful writeback a forced shutdown returns -EIO. The excerpt does not show the log and device flush branches later in the function, so it does not establish durability.
For an illustrative 4096-byte overwrite, Btrfs and XFS can both first dirty cached bytes; later Btrfs coordinates an ordered extent and tree/log state, while XFS uses its iomap/writeback and ioend machinery. In either case an fsync error means the requested synchronization was not established. The order is a source-based explanation, not a recorded execution; actual allocation and logging vary with extent state.
Optional disposable-guest filesystem experiments#
These commands require an already-disposable Linux guest with root, loop-device support, mkfs.btrfs, mkfs.xfs, mount, umount, mountpoint, and GNU cp. They create sparse files inside the guest, never select a host device, and mount them temporarily. Do not run them on the host. A missing tool, unsupported filesystem, insufficient space or loop/mount permission is a valid stop; record the error. Exit the guest or restore its snapshot if cleanup fails. On a freshly provisioned guest, run as root:
set -eu
work=$(mktemp -d /tmp/fs-lab.XXXXXX)
mkdir "$work/mnt"
cleanup() { if mountpoint -q "$work/mnt"; then umount "$work/mnt" || { echo "Unmount failed; retain $work" >&2; return 1; }; fi; rm -rf -- "$work"; }
trap cleanup EXIT
for kind in btrfs xfs; do
truncate -s 512M "$work/$kind.img"
"mkfs.$kind" -f "$work/$kind.img" >/dev/null
mount -o loop "$work/$kind.img" "$work/mnt"
printf 'old\n' > "$work/mnt/original"
cp --reflink=always "$work/mnt/original" "$work/mnt/copy"
printf 'new\n' > "$work/mnt/original"
printf '%s: ' "$kind"; cat "$work/mnt/copy"
sync
umount "$work/mnt"
done
Expected each line is btrfs: old or xfs: old. --reflink=always fails instead of silently copying when reflinks are unsupported. The command demonstrates separate content after a successful reflink request; it does not measure physical sharing, recovery, or crash durability. XFS must have reflink support enabled by its actual mkfs.xfs defaults. The trap unmounts and removes only the created guest files; confirm mountpoint "$work/mnt" is false before manual recovery if interrupted.
For OverlayFS, use a separate guest temporary directory on an upper filesystem that supports the required features:
set -eu
work=$(mktemp -d /tmp/overlay-lab.XXXXXX)
mkdir "$work/lower" "$work/upper" "$work/work" "$work/merged"
cleanup() { if mountpoint -q "$work/merged"; then umount "$work/merged" || { echo "Unmount failed; retain $work" >&2; return 1; }; fi; rm -rf -- "$work"; }
trap cleanup EXIT
printf 'lower\n' > "$work/lower/file"
mount -t overlay overlay -o "lowerdir=$work/lower,upperdir=$work/upper,workdir=$work/work" "$work/merged"
printf 'upper\n' > "$work/merged/file"
printf 'merged: '; cat "$work/merged/file"
printf 'lower: '; cat "$work/lower/file"
rm "$work/merged/file"
test ! -e "$work/merged/file" && echo 'merged name hidden'
test -e "$work/lower/file" && echo 'lower name remains'
umount "$work/merged"
Expected merged: upper, lower: lower, then both test messages. The modification requires an upper representation; removal hides the lower name in the merged view. The exact whiteout representation depends on configuration. Stop on mount failure, unmount before deleting the directory, and restore the disposable guest if unmount fails. This is a semantic observation, not a crash test.
Practice#
Source exercise: inspect rg -n -A35 'xfs_file_read_iter\(' "$KERNEL_SRC/fs/xfs/xfs_file.c" and rg -n -A25 'ovl_copy_up_data\(' "$KERNEL_SRC/fs/overlayfs/copy_up.c". Locate the XFS shutdown and three branches, and the OverlayFS skip, open, copy, release and error paths. In fs/btrfs/ctree.c, inspect btrfs_cow_block and write down why it describes metadata-tree COW rather than claiming it is the entire userspace write.
In a disposable guest, observe a normal hard link as a counterexample to COW snapshot semantics:
python3 - <<'PY'
import os, tempfile
with tempfile.TemporaryDirectory() as d:
a, b = d + '/a', d + '/b'
with open(a, 'w') as f: f.write('old')
os.link(a, b)
with open(a, 'w') as f: f.write('new')
print(open(b).read())
print(os.stat(a).st_ino == os.stat(b).st_ino)
PY
Expected new and True: both names refer to one inode, so this is not a snapshot. If hard links are unsupported, record the error and filesystem rather than changing a host mount. The temporary directory is cleaned up. This does not test Btrfs reflinks, XFS logs or an OverlayFS mount. Deliver a three-column table: triggering operation, changed references or namespace, and one failure or limitation for each filesystem.
Knowledge check#
- Why does deleting one Btrfs snapshot reference not necessarily free a shared extent?
- Which XFS branch is selected if the inode is DAX, and what happens on forced shutdown?
- What does an OverlayFS whiteout hide, and what does it leave intact?
- Why is the hard-link experiment not evidence of a COW snapshot?
Answers#
- Other references can still point to the extent, so reference and transaction rules must permit reclamation first.
- The DAX branch is tested first; a forced-shutdown check precedes dispatch and returns
-EIO. - The whiteout hides the lower name in the merged view; the lower object itself remains in its lower filesystem.
- The two names share one inode and modification through one changes the object observed through the other; a COW snapshot preserves a separately referenced prior view under its filesystem rules.
Checkpoint#
Save the comparison table and hard-link output. Pass when each column uses its own source path and names a distinct failure or limitation; explain why neither copy-up nor a successful read proves durable multi-file transaction semantics. Re-read the respective source and correct the table if any mechanisms are conflated. This is not certification or complete coverage of these filesystems.