Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Send and Receive

btrfs send and btrfs receive transfer filesystem state between two btrfs filesystems as a byte stream. This page explains how the mechanism works and how to use the btrfs-stream and btrfs-uapi crates to implement receive in your own application.

How send works

btrfs send asks the kernel to generate a stream representing the contents of a read-only subvolume. The kernel traverses the subvolume’s B-trees and emits a sequence of commands describing every file, directory, symlink, and extent. For an incremental send (with -p <parent>), only the differences from the parent subvolume are emitted.

The kernel is invoked via BTRFS_IOC_SEND, which writes the stream to a file descriptor (typically the write end of a pipe). A reader thread on the other end consumes the stream and writes it to a file or stdout.

The stream format

The stream is a binary format consisting of a header followed by a sequence of commands.

The stream header identifies the format version (v1, v2, or v3) and contains a magic number (btrfs-stream\0). After the header, commands follow back-to-back until an END command signals completion.

Each command has the following structure:

u32  total_length    (length of the entire command, including this header)
u16  command_type    (BTRFS_SEND_C_* constant)
u32  crc32c          (checksum of the command, with the crc field zeroed)
     attributes...   (variable-length TLV list)

Attributes are TLV-encoded:

u16  attribute_type  (BTRFS_SEND_A_* constant)
u16  length
     data...

The CRC32C used by btrfs is the raw variant (initial seed 0, no final XOR), not the standard ISO 3309 variant (initial seed 0xFFFFFFFF). When computing or verifying a checksum, use:

#![allow(unused)]
fn main() {
let crc = !crc32c::crc32c_append(!0u32, data);
}

Parsing a stream with btrfs-stream

The btrfs-stream crate provides StreamReader, which parses commands one at a time from any Read source:

#![allow(unused)]
fn main() {
use btrfs_stream::{StreamReader, StreamCommand};

let mut reader = StreamReader::new(input)?; // reads and validates the header
while let Some(command) = reader.read_command()? {
    match command {
        StreamCommand::Subvol { path, uuid, ctransid } => { /* create subvolume */ }
        StreamCommand::MkFile { path } => { /* create file */ }
        StreamCommand::Write { path, offset, data } => { /* write data */ }
        StreamCommand::Rename { path, path_to } => { /* rename */ }
        StreamCommand::End => break,
        // ... all 22+ command types
    }
}
}

StreamReader::new reads the stream header and returns an error if the magic is wrong or the version is unsupported. read_command returns None at EOF.

Applying a stream with btrfs-uapi

To implement receive, you need to apply each command to a mounted btrfs filesystem. The relevant operations are:

Subvolume and snapshot creation (BTRFS_IOC_SUBVOL_CREATE, BTRFS_IOC_SNAP_CREATE_V2): for Subvol commands, create a new empty subvolume. For Snapshot commands, look up the source subvolume by UUID using subvolume_search_by_received_uuid or subvolume_search_by_uuid, then create a writable snapshot.

File operations: standard POSIX calls — open/create, unlink, mkdir, rmdir, symlink, link, rename. btrfs does not require any special ioctls for these.

Write (BTRFS_IOC_ENCODED_WRITE or pwrite): v2 streams may send pre-compressed data via ENCODED_WRITE. If the kernel supports it, this can be passed directly; otherwise decompress and fall back to pwrite.

Clone (BTRFS_IOC_CLONE_RANGE): shares an extent between two files without copying data. The source file is found by resolving its UUID via the UUID tree.

Subvolume finalization: once all commands for a subvolume have been processed, call BTRFS_IOC_SET_RECEIVED_SUBVOL to record the UUID and ctransid, then set the subvolume read-only with BTRFS_IOC_SUBVOL_SETFLAGS.

Using ReceiveContext

If you want a complete, ready-to-use receive implementation rather than building your own, the receive feature of btrfs-stream provides ReceiveContext:

btrfs-stream = { version = "0.5", features = ["receive"] }
#![allow(unused)]
fn main() {
use btrfs_stream::ReceiveContext;

let mut ctx = ReceiveContext::new(destination_dir)?;
ctx.receive(input_stream)?;
}

ReceiveContext handles all command types including v2 encoded writes (with decompression fallback for zlib, zstd, and lzo) and v3 fs-verity. It uses an fd cache to avoid reopening the same file for sequential writes, which is important for performance when receiving large files.