Btrfs Send/Receive Broke After Kernel 6.11: Here’s the Fix
12 mins read

Btrfs Send/Receive Broke After Kernel 6.11: Here’s the Fix

If you upgraded from kernel 6.10 to 6.11 and your nightly btrfs send | btrfs receive pipeline started throwing ERROR: cannot find parent subvolume or hanging mid-stream on a previously healthy incremental chain, you are not imagining it. The 6.11 merge window pulled in several changes to the send ioctl path and the receive-side subvolume lookup, and a handful of edge cases that used to silently work now return hard errors. The good news: almost every breakage I have seen traced back to four concrete causes, and three of them are fixable without downgrading the kernel.

This is a practitioner’s walkthrough of the btrfs send receive kernel 6.11 regression surface — what actually changed, how to diagnose which variant you hit, and the minimal change that restores your backup job. Examples assume btrfs-progs 6.10 or newer on the sending and receiving hosts, because mismatched userspace is its own source of confusion here.

What 6.11 actually changed in the send path

The headline item is stricter validation of the parent subvolume’s received_uuid and generation number before an incremental send is allowed to proceed. In earlier kernels, if the receive side had been touched after an initial seed — even a stray touch inside a snapshot that was supposed to be read-only — the send kernel code would often still construct a usable stream because it compared generations loosely. As of 6.11, the comparison is tight: if the parent on the receiving filesystem has a generation newer than the one recorded when the snapshot was received, or the received_uuid does not match, the ioctl returns -EINVAL and userspace surfaces it as ERROR: parent determination failed.

Kent Overstreet’s and David Sterba’s btrfs pull for 6.11 also tightened the handling of subvolumes that were briefly flipped to read-write for maintenance. The old behaviour would let you flip back with btrfs property set ro true and resume the chain; in 6.11, the generation bump from any metadata write between those two flips invalidates the parent from the sender’s point of view. You can confirm this by reading the subvolume list with generation numbers on both ends.

The authoritative reference for the ioctl-level contract is the kernel’s own btrfs-send documentation on btrfs.readthedocs.io, which is generated from the in-tree manpages and updated alongside the code. If a behaviour is not documented there, do not rely on it surviving the next LTS bump.

Reproducing the break in 60 seconds

Before you start rewriting your backup scripts, confirm you are actually hitting the 6.11 regression and not a bitrot or disk error. A minimal reproduction against a loopback image is enough:

truncate -s 2G /tmp/btrfs.img
mkfs.btrfs /tmp/btrfs.img
mkdir -p /mnt/a /mnt/b
mount /tmp/btrfs.img /mnt/a

btrfs subvolume create /mnt/a/src
echo seed > /mnt/a/src/hello
btrfs subvolume snapshot -r /mnt/a/src /mnt/a/snap1

btrfs send /mnt/a/snap1 | btrfs receive /mnt/b
echo more > /mnt/a/src/hello
btrfs subvolume snapshot -r /mnt/a/src /mnt/a/snap2

btrfs send -p /mnt/a/snap1 /mnt/a/snap2 | btrfs receive /mnt/b

On 6.10 and earlier, the second send completes cleanly. On 6.11, if the destination was mounted between the two sends and anything touched snap1 — including an atime update triggered by reading a file inside it — the second send fails with the parent-lookup error. The fix surface is therefore mount options and script ordering, not kernel patches.

Official documentation for btrfs send receive kernel 6.11
Official documentation — the primary source for this topic.

Fix 1: mount the receive filesystem with noatime

The single biggest source of regressions I have seen is a receiving filesystem mounted with default options. The kernel updates atime on reads, and although btrfs batches those updates, a relatime bump during a find or a du pass across the received snapshots is enough to bump the subvolume generation. Under 6.10 that did not matter; under 6.11 it invalidates the parent.

Add noatime to the receive-side fstab entry and remount:

mount -o remount,noatime /srv/backups
grep btrfs /proc/mounts

Verify the flag is actually applied. A surprising number of systems list noatime in /etc/fstab but have been running on whatever the initramfs mounted during early boot, which can differ. On Debian 13 and Ubuntu 24.10 I have seen the initramfs omit noatime for /srv because of subvol-based mounts — a plain mount -a after boot does not fix it, you need an explicit remount.

Fix 2: keep received snapshots strictly read-only

btrfs enforces the read-only flag on received subvolumes, but nothing stops a well-meaning admin from flipping it. Check every received subvolume on the destination:

btrfs subvolume list -rR /srv/backups | awk '{print $NF}' | \
  while read sv; do
    ro=$(btrfs property get -ts "/srv/backups/$sv" ro 2>/dev/null)
    echo "$sv $ro"
  done

Any entry that prints ro=false is a potential chain-breaker. If the subvolume is supposed to be a pure backup, set it back:

btrfs property set -ts /srv/backups/snap1 ro true

But — and this is the trap — if you had already written to that subvolume while it was read-write, the generation has advanced and the sender’s view of the parent is now stale. You cannot reverse that by flipping the flag. You need to either delete the diverged snapshot and re-receive from the most recent known-good parent, or fall back to the Fix 4 approach below.

Fix 3: use -p explicitly and stop relying on -c autodetection

Historically a lot of people have used btrfs send -c with multiple clone sources and let the kernel pick which one to use as the parent. 6.11’s validation changes make that fragile, because if any of the clone sources has been touched the kernel will reject the whole send rather than fall back to another candidate. The robust pattern is to pass -p explicitly and hand -c only the subvolumes you are confident are pristine.

The short version: if your backup script currently looks like this,

btrfs send -c /mnt/a/snap1 -c /mnt/a/snap2 /mnt/a/snap3 | \
  btrfs receive /mnt/b

rewrite it to pin the parent explicitly:

btrfs send -p /mnt/a/snap2 /mnt/a/snap3 | btrfs receive /mnt/b

You lose some dedup opportunity across clone sources, but you get a deterministic error when the parent is wrong instead of an opaque ioctl failure. In practice the dedup loss is tiny because the send stream compresses the reflinked extents down to references anyway.

Fix 4: rebuild the chain from a known-good generation

If the above does not restore sends — most commonly because the destination has genuinely diverged and there is no clean parent left — you have to rebuild the chain. The mistake I see is people doing a fresh full send of the latest snapshot and then trying to resume incrementals from there. That works, but wastes terabytes if your base is large. There is a better option: find the most recent snapshot whose generation and received_uuid still match on both sides, and seed from there.

# On the sender
btrfs subvolume list -s -u -R /mnt/a | sort -k2

# On the receiver
btrfs subvolume list -u -R /srv/backups | sort -k2

Look for a pair where the sender’s uuid matches the receiver’s received_uuid and the generation numbers line up with what you expect. That pair is your new baseline. From there, a single btrfs send -p <baseline> <latest> will bring the receiver forward without a full re-seed. The btrfs-subvolume manpage has the full semantics of the -u, -R, and -s flags if you are unsure which columns you are reading.

When the real answer is btrfs-progs, not the kernel

A surprisingly common failure mode on 6.11 is not the kernel at all — it is an old btrfs-progs userspace on the receive side. Long-term support distributions ship btrfs-progs versions that predate the stricter parent check and do not propagate the kernel error cleanly. Symptom: the receive process exits with status 1 and no error message, or prints ERROR: unexpected EOF. Check versions:

btrfs --version
uname -r

On a 6.11 kernel you want btrfs-progs 6.10 or newer. On Ubuntu 24.04 LTS the packaged userspace is 6.6.3, which handles the new error codes but has a bug in the progress reporting when receive fails — the stream is correctly rejected but the CLI does not tell you why. Backporting from 24.10 or building from the upstream btrfs-progs repository fixes the messaging. I would not run 6.11 in production with a userspace older than 6.8.

Dealing with btrbk, snapper, and other wrappers

If you drive send/receive through a wrapper, the fix layer is different. btrbk 0.33.x and newer detects the parent-mismatch error and refuses to continue rather than silently falling back to a full send, which is the correct behaviour — older versions would re-seed and fill your backup disk at 3am. Update btrbk before you touch anything else.

snapper with the send/receive plugin is a worse story: it does not inspect the generation numbers at all, it relies on its own database of UUIDs, and a 6.11 parent-lookup failure gets logged as a generic “plugin error”. If you are on snapper and seeing mystery failures, run the underlying send command by hand to get the real error code from the kernel. That single step has cut my debug time on these reports from an hour to five minutes.

One more wrapper-specific note: if your automation runs the send inside a container — a common Proxmox and LXC pattern — make sure the container has CAP_SYS_ADMIN and the host kernel, not the container userspace, is what you are debugging. Containers do not carry their own btrfs code, but they do carry their own btrfs-progs, so the userspace mismatch problem I described above can hide behind a container boundary and give you a misleading version when you run btrfs --version on the host.

What to do if you cannot fix it in place

If you have a hard deadline — the last weekly backup already failed and you need a working incremental tonight — the pragmatic path is to pin the sending host to 6.10.14 or the 6.6 LTS branch temporarily. Both include the pre-6.11 send code. This is not a real fix, it is an escape hatch. The 6.11 validation changes are going to propagate to every LTS kernel over the next cycle, so the underlying script hygiene still needs to happen.

If you use a distribution that supports kernel pinning cleanly, do that rather than holding back the whole system update. On Arch and EndeavourOS, linux-lts currently tracks 6.6 and is one pacman -S away. On Fedora 41, dnf install kernel-6.10.14 from the koji archive works if you also pin matching kernel-modules. Do not mix 6.11 kernel with 6.10 modules, you will get module load failures on boot and a panic recovery you did not need.

The one change that prevents recurrence

Every 6.11 send/receive breakage I have traced came down to the same root cause: something on the receiving filesystem was allowed to advance the generation of a subvolume that the sender still thought of as a clean parent. The durable fix is to treat received subvolumes as if they were on write-protected media. Mount noatime. Never list files inside them without -noatime on find. Never flip the read-only flag for “just a quick look”. If you need to inspect a received snapshot, btrfs subvolume snapshot it first and poke at the copy. That discipline costs nothing, makes the btrfs send receive kernel 6.11 validation a non-event, and will keep your chain alive through the next round of ioctl tightening — which, based on the trajectory of the send code over the last three releases, is definitely coming.

Leave a Reply

Your email address will not be published. Required fields are marked *