* [RFC PATCH 0/1] btrfs: hybrid physical bad-block handling
@ 2026-11-12 13:47 Alexey Kuznetsov
2026-11-12 13:47 ` [RFC PATCH 1/1] btrfs: track physical bad extents, fast-fail, persist, and relocate Alexey Kuznetsov
0 siblings, 1 reply; 3+ messages in thread
From: Alexey Kuznetsov @ 2026-11-12 13:47 UTC (permalink / raw)
To: linux-btrfs; +Cc: linux-block, dm-devel, Alexey Kuznetsov
From: Alexey Kuznetsov <axet@me.com>
This is an RFC for physical bad-block handling in btrfs: a design
proposal plus a loop-device prototype. It is not a submission candidate.
Why the existing layers are not enough
--------------------------------------
Bad-block handling today lives either below the filesystem (dm-linear,
dm-quarantine, md, drive firmware) or in a logical structure such as
ext4's bad-block inode. Neither knows which physical copy of a mirrored
logical extent failed. As a result:
- a filesystem cannot use its own redundancy to distinguish "this copy
is bad" from "this logical extent is lost";
- it cannot relocate only the data that lives in the bad physical
range;
- it cannot stop the allocator from reusing that range, and
- it cannot report which files are affected instead of only which LBA
failed.
The prototype keeps the layer split:
block layer physical remap/quarantine, no filesystem knowledge
btrfs detection, mirror salvage, relocation, reporting
userspace scanning, persistence, policy, initramfs
Bad extents are tracked per device as physical byte ranges, not as a
filesystem-wide logical list and not as an ext4-style inode.
What the prototype does
-----------------------
The series is against 6.12. It adds fs/btrfs/bad-extents.c and hooks
scrub, the read path, the write path, the allocator, the device tree, and
the ioctl surface:
- scrub records the physical range of every initially failed sector.
An I/O error becomes "confirmed"; a checksum, header, or generation
failure becomes "suspect". Adjacent ranges coalesce and a second
observation upgrades suspect to confirmed.
- A read that overlaps a confirmed range fails before device I/O. The
failure goes through the normal read completion path, so btrfs retries
another mirror and returns -EIO only when no copy is left. Suspect
ranges are read normally so they can still be verified and repaired.
- Repair writes into a confirmed range are dropped, so repair cannot
recreate a copy in known-bad media.
- find_free_dev_extent() trims confirmed ranges out of candidate holes.
New chunks never land there. Suspect ranges remain allocatable.
Zoned devices are intentionally untouched.
- Ranges persist in the device tree as
(devid, BTRFS_DEV_BAD_EXTENT_KEY, physical_start) items holding
length, last logical offset, generation, state, and observation
count, under a new incompat bit. Mount loads them after device
stats; add/remove/clear write through; SYNC commits a scrub report.
Removing the last range clears the bit in the same transaction.
- On RAID1/RAID1C3/RAID1C4/DUP/RAID10, an ordinary write whose stripe
falls in a confirmed range is aliased to a healthy mirror of the same
logical range inside btrfs_map_block(). All copies are still
submitted, so the healthy copy is updated and the bad sector is never
written. If every copy is confirmed bad the write fails with -EIO.
Device replace is excluded because it manages its own stripe
duplication. With the experimental raid-stripe-tree feature, the
existing ordered-extent completion path records the aliased stripe;
without RST the alias is per-write and reads rely on the existing
mirror retry.
- Data-relocation reads are exempt from fast-fail, because relocation
is exactly the operation that has to read the old copy before moving
it.
- A userspace command, dmq btrfs-relocate, coalesces the reported
ranges and drives BTRFS_IOC_BALANCE_V2 with
BTRFS_BALANCE_ARGS_DEVID | BTRFS_BALANCE_ARGS_DRANGE. That selects
only chunks with a stripe on the affected device inside the bad
physical range. It works without btrfs-progs knowing the new
incompat bit. A companion userspace package also scans and persists
a sector map and feeds dm-linear/dm-quarantine for the non-btrfs and
boot paths.
Test results
------------
On 6.12.116 with loop devices, the prototype passes:
- ioctl add/coalesce/upgrade/split/remove/clear;
- persistence across unmount/remount and the incompat bit lifecycle;
- scrub detection of a deliberately corrupted data sector, exact
physical offset, SYNC, remount;
- dmq import into a checksummed map and mount through dm-linear with
EIO for the bad range;
- fast-fail on a confirmed range, normal reads for a suspect range;
- RAID1 fallback, skipped repair write, and EIO when both copies are
confirmed;
- allocator exclusion of a 176 MiB confirmed range (ENOSPC for a
100 MiB write) with a suspect-range control;
- a RAID1 in-place NODATACOW rewrite against a dm-quarantine poison
range: the target's write counter stays at zero, the healthy mirror
holds the new data, and a control write after clearing the extent
does reach the poisoned range;
- targeted relocation: BTRFS_IOC_BALANCE_V2 completes, the payload is
unchanged, and a new copy appears outside the confirmed range.
The userspace test suite (62 tests) covers the ioctl ABI, balance ABI
packing, map import, and CLI behaviour.
Known limitations / why this is an RFC
--------------------------------------
- The on-disk format and the ioctl ABI are placeholders.
- btrfs-progs 6.14 does not know the incompat bit (btrfs check reports
unsupported features 0x8000) and does not expose raid-stripe-tree
(mkfs.btrfs -O list-all omits it), so the RST alias path could not be
exercised on loop devices.
- Fast-fail is whole-bio/chunk granular; more data than the bad sector
may be failed and retried on another mirror.
- Relocation is manual and chunk-granular. It is not invoked from
scrub.
- Persisted items for a removed devid are not cleaned up. Devids are
not reused, so they are ignored.
- Write redirection only covers mirror profiles. Single-profile data
and metadata still fail; reallocating a single-profile write through
RST is a larger project.
- Boot-chain and LBA-0 handling are discussed in the userspace design,
not implemented here.
Questions for the list
----------------------
- Is a per-device physical extent list in the device tree the right
on-disk representation, or should this extend the raid-stripe tree or
use a dedicated tree?
- Should read fast-fail be per-sector instead of per-bio/chunk, and
should the I/O path record errors automatically or only scrub and
userspace imports?
- Is an incompat bit appropriate for a feature that changes read
semantics, or is a mount option / read-only toggle preferable?
- How should this interact with device replace and balance? The
prototype excludes dev-replace writes and uses balance as the
relocation engine.
- What is the right policy for upgrading suspect to confirmed, and how
should transient errors be excluded?
- Is RST the right persistence mechanism for the write alias, or should
there be a separate logical-to-physical indirection?
The userspace prototype and design notes, including the LBA-0 and
bootloader discussion, are at:
https://gitlab.com/axet/kernel-badblocks
The patch follows.
Alexey Kuznetsov (1):
btrfs: track physical bad extents, fast-fail, persist, and relocate
fs/btrfs/Makefile | 2 +-
fs/btrfs/accessors.h | 12 +
fs/btrfs/bad-extents.c | 825 ++++++++++++++++++++++++++++++++
fs/btrfs/bad-extents.h | 46 ++
fs/btrfs/bio.c | 38 +-
fs/btrfs/disk-io.c | 8 +
fs/btrfs/fs.h | 4 +
fs/btrfs/ioctl.c | 96 ++++
fs/btrfs/scrub.c | 32 ++
fs/btrfs/sysfs.c | 2 +
fs/btrfs/volumes.c | 29 ++
fs/btrfs/volumes.h | 5 +
include/uapi/linux/btrfs.h | 40 ++
include/uapi/linux/btrfs_tree.h | 22 +
14 files changed, 1159 insertions(+), 2 deletions(-)
create mode 100644 fs/btrfs/bad-extents.c
create mode 100644 fs/btrfs/bad-extents.h
--
2.47.3
^ permalink raw reply [flat|nested] 3+ messages in thread
* [RFC PATCH 1/1] btrfs: track physical bad extents, fast-fail, persist, and relocate
2026-11-12 13:47 [RFC PATCH 0/1] btrfs: hybrid physical bad-block handling Alexey Kuznetsov
@ 2026-11-12 13:47 ` Alexey Kuznetsov
2026-11-15 10:35 ` Zygo Blaxell
0 siblings, 1 reply; 3+ messages in thread
From: Alexey Kuznetsov @ 2026-11-12 13:47 UTC (permalink / raw)
To: linux-btrfs; +Cc: linux-block, dm-devel, Alexey Kuznetsov
From: Alexey Kuznetsov <axet@me.com>
From: Alexey Kuznetsov <axet@me.com>
RFC prototype of hybrid physical bad-block handling for btrfs.
The existing bad-block interfaces are either below the filesystem
(remappers, md, dm-linear) or logical (ext4's bad-block inode). Neither
knows which physical copy of a mirrored logical extent failed, so a
filesystem cannot use redundancy to salvage a bad sector, cannot relocate
only the affected data, and cannot avoid reusing the bad physical range.
This series teaches btrfs about per-device physical bad extents:
- scrub records the physical range of every initially failed sector;
an I/O error is confirmed, a checksum/header/generation mismatch is
suspect;
- reads that overlap a confirmed range fail before device I/O and go
through the normal completion path, so another mirror is retried and
a fully degraded extent returns -EIO instead of stale data;
- repair writes into a confirmed range are dropped;
- confirmed ranges are trimmed out of find_free_dev_extent(), so new
chunks never land in known-bad media; suspect ranges remain
allocatable for verification;
- ranges persist in the device tree under a new incompat feature and
are loaded at mount;
- writes on RAID1/RAID1C3/RAID1C4/DUP/RAID10 alias a confirmed-bad
stripe to a healthy mirror instead of writing the bad sector; RST
records the alias when that feature is enabled;
- userspace can drive targeted relocation through the existing balance
ioctl with a devid and physical-range filter.
A userspace side (dmq) scans, persists a sector map, feeds dm-linear or
dm-quarantine, and provides the relocation command. The kernel patch is
self-contained and does not depend on it.
This is an RFC, not a submission candidate. The on-disk format and the
ioctl ABI are placeholders. btrfs-progs 6.14 does not know the incompat
bit (btrfs check reports unsupported features 0x8000) or expose
raid-stripe-tree, so RST persistence could not be tested on loop
devices. Zoned devices are intentionally left alone. Fast-fail is
whole-bio/chunk granular. Relocation is manual. Persisted items for a
removed device are not cleaned up.
Feedback wanted on: on-disk representation (device-tree items vs a
dedicated tree vs extending RST), per-sector vs per-bio failure, whether
the I/O path should record errors or only scrub/userspace, how to
coordinate with device replace and balance, and whether an incompat bit
is the right compatibility mechanism for a feature that changes read
semantics.
Signed-off-by: Alexey Kuznetsov <axet@me.com>
---
fs/btrfs/Makefile | 2 +-
fs/btrfs/accessors.h | 12 +
fs/btrfs/bad-extents.c | 825 ++++++++++++++++++++++++++++++++
fs/btrfs/bad-extents.h | 46 ++
fs/btrfs/bio.c | 38 +-
fs/btrfs/disk-io.c | 8 +
fs/btrfs/fs.h | 4 +
fs/btrfs/ioctl.c | 96 ++++
fs/btrfs/scrub.c | 32 ++
fs/btrfs/sysfs.c | 2 +
fs/btrfs/volumes.c | 29 ++
fs/btrfs/volumes.h | 5 +
include/uapi/linux/btrfs.h | 40 ++
include/uapi/linux/btrfs_tree.h | 22 +
14 files changed, 1159 insertions(+), 2 deletions(-)
create mode 100644 fs/btrfs/bad-extents.c
create mode 100644 fs/btrfs/bad-extents.h
diff --git a/fs/btrfs/Makefile b/fs/btrfs/Makefile
index 87617f2..a736b82 100644
--- a/fs/btrfs/Makefile
+++ b/fs/btrfs/Makefile
@@ -33,7 +33,7 @@ btrfs-y += super.o ctree.o extent-tree.o print-tree.o root-tree.o dir-item.o \
uuid-tree.o props.o free-space-tree.o tree-checker.o space-info.o \
block-rsv.o delalloc-space.o block-group.o discard.o reflink.o \
subpage.o tree-mod-log.o extent-io-tree.o fs.o messages.o bio.o \
- lru_cache.o raid-stripe-tree.o fiemap.o direct-io.o
+ lru_cache.o raid-stripe-tree.o fiemap.o direct-io.o bad-extents.o
btrfs-$(CONFIG_BTRFS_FS_POSIX_ACL) += acl.o
btrfs-$(CONFIG_BTRFS_FS_REF_VERIFY) += ref-verify.o
diff --git a/fs/btrfs/accessors.h b/fs/btrfs/accessors.h
index 15ea634..2a18508 100644
--- a/fs/btrfs/accessors.h
+++ b/fs/btrfs/accessors.h
@@ -321,6 +321,18 @@ BTRFS_SETGET_FUNCS(raid_stride_physical, struct btrfs_raid_stride, physical, 64)
BTRFS_SETGET_STACK_FUNCS(stack_raid_stride_devid, struct btrfs_raid_stride, devid, 64);
BTRFS_SETGET_STACK_FUNCS(stack_raid_stride_physical, struct btrfs_raid_stride, physical, 64);
+/* struct btrfs_dev_bad_extent_item */
+BTRFS_SETGET_FUNCS(dev_bad_extent_length, struct btrfs_dev_bad_extent_item,
+ length, 64);
+BTRFS_SETGET_FUNCS(dev_bad_extent_logical, struct btrfs_dev_bad_extent_item,
+ logical, 64);
+BTRFS_SETGET_FUNCS(dev_bad_extent_generation, struct btrfs_dev_bad_extent_item,
+ generation, 64);
+BTRFS_SETGET_FUNCS(dev_bad_extent_state, struct btrfs_dev_bad_extent_item,
+ state, 32);
+BTRFS_SETGET_FUNCS(dev_bad_extent_errors, struct btrfs_dev_bad_extent_item,
+ errors, 32);
+
/* struct btrfs_dev_extent */
BTRFS_SETGET_FUNCS(dev_extent_chunk_tree, struct btrfs_dev_extent, chunk_tree, 64);
BTRFS_SETGET_FUNCS(dev_extent_chunk_objectid, struct btrfs_dev_extent,
diff --git a/fs/btrfs/bad-extents.c b/fs/btrfs/bad-extents.c
new file mode 100644
index 0000000..b86f074
--- /dev/null
+++ b/fs/btrfs/bad-extents.c
@@ -0,0 +1,825 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Per-device physical bad extent tracking (prototype).
+ *
+ * This is the btrfs side of the hybrid bad-block design: btrfs detects bad
+ * physical ranges (scrub already knows the logical -> physical mapping and
+ * which mirror failed) and reports them. Userspace can then hand the ranges
+ * to a block-layer remapper.
+ *
+ * The ranges are persisted in the device tree, keyed by devid and physical
+ * start offset, under a dedicated incompat feature. The in-memory list is
+ * authoritative while mounted; userspace mutations are written back by
+ * btrfs_bad_extents_persist() and loaded again by btrfs_bad_extents_load().
+ */
+
+#include <linux/kernel.h>
+#include <linux/list.h>
+#include <linux/mutex.h>
+#include <linux/overflow.h>
+#include <linux/sizes.h>
+#include <linux/slab.h>
+#include <linux/btrfs.h>
+
+#include "ctree.h"
+#include "fs.h"
+#include "accessors.h"
+#include "messages.h"
+#include "transaction.h"
+#include "volumes.h"
+#include "bad-extents.h"
+
+struct btrfs_bad_extent {
+ struct list_head list;
+ u64 start; /* physical byte offset on the device */
+ u64 length; /* bytes */
+ u64 logical; /* last logical byte offset, 0 if unknown */
+ u32 state; /* BTRFS_BAD_EXTENT_STATE_* */
+ u32 errors; /* number of observations */
+};
+
+static u32 bad_extent_sector_size(const struct btrfs_device *dev)
+{
+ return dev->sector_size ? dev->sector_size : SZ_4K;
+}
+
+void btrfs_bad_extents_init(struct btrfs_device *dev)
+{
+ INIT_LIST_HEAD(&dev->bad_extents);
+ mutex_init(&dev->bad_extents_lock);
+ atomic_set(&dev->bad_extents_count, 0);
+}
+
+void btrfs_bad_extents_free(struct btrfs_device *dev)
+{
+ struct btrfs_bad_extent *extent, *tmp;
+
+ mutex_lock(&dev->bad_extents_lock);
+ list_for_each_entry_safe(extent, tmp, &dev->bad_extents, list) {
+ list_del(&extent->list);
+ kfree(extent);
+ }
+ atomic_set(&dev->bad_extents_count, 0);
+ mutex_unlock(&dev->bad_extents_lock);
+}
+
+/*
+ * Record a physical range. Adjacent/overlapping ranges are coalesced, the
+ * state only ever moves forward (suspect -> confirmed), and the error counter
+ * accumulates observations.
+ */
+int btrfs_bad_extents_record(struct btrfs_device *dev, u64 physical,
+ u64 length, u64 logical, u32 state)
+{
+ struct btrfs_bad_extent *extent, *tmp, *new;
+ u64 start, end, new_end;
+ u32 sector_size;
+ int removed = 0;
+
+ if (!dev || !length)
+ return -EINVAL;
+ if (physical >= dev->total_bytes)
+ return -ERANGE;
+
+ sector_size = bad_extent_sector_size(dev);
+ length = min(length, dev->total_bytes - physical);
+ start = round_down(physical, sector_size);
+ end = min(round_up(physical + length, sector_size), dev->total_bytes);
+ if (end <= start)
+ return -ERANGE;
+
+ if (state != BTRFS_BAD_EXTENT_STATE_CONFIRMED)
+ state = BTRFS_BAD_EXTENT_STATE_SUSPECT;
+
+ new = kzalloc(sizeof(*new), GFP_NOFS);
+ if (!new)
+ return -ENOMEM;
+
+ new->start = start;
+ new->length = end - start;
+ new->logical = logical;
+ new->state = state;
+ new->errors = 1;
+ new_end = end;
+
+ mutex_lock(&dev->bad_extents_lock);
+ list_for_each_entry_safe(extent, tmp, &dev->bad_extents, list) {
+ u64 extent_end = extent->start + extent->length;
+
+ /* Merge touching and overlapping ranges as well. */
+ if (extent_end < new->start || extent->start > new_end)
+ continue;
+
+ new->start = min(new->start, extent->start);
+ new_end = max(new_end, extent_end);
+ new->state = max(new->state, extent->state);
+ if (U32_MAX - new->errors < extent->errors)
+ new->errors = U32_MAX;
+ else
+ new->errors += extent->errors;
+ if (!new->logical)
+ new->logical = extent->logical;
+ list_del(&extent->list);
+ kfree(extent);
+ removed++;
+ }
+ new->length = new_end - new->start;
+ atomic_inc(&dev->bad_extents_count);
+ if (removed)
+ atomic_sub(removed, &dev->bad_extents_count);
+
+ list_for_each_entry(extent, &dev->bad_extents, list) {
+ if (new->start < extent->start) {
+ list_add_tail(&new->list, &extent->list);
+ goto out;
+ }
+ }
+ list_add_tail(&new->list, &dev->bad_extents);
+out:
+ mutex_unlock(&dev->bad_extents_lock);
+ return 0;
+}
+
+/*
+ * Explicit import uses the same path as scrub, but a manual range defaults to
+ * confirmed rather than suspect.
+ */
+int btrfs_bad_extents_add(struct btrfs_device *dev, u64 start, u64 length,
+ u64 logical, u32 state)
+{
+ if (!state)
+ state = BTRFS_BAD_EXTENT_STATE_CONFIRMED;
+ return btrfs_bad_extents_record(dev, start, length, logical, state);
+}
+
+int btrfs_bad_extents_remove(struct btrfs_device *dev, u64 start, u64 length)
+{
+ struct btrfs_bad_extent *extent, *tmp, *right;
+ u64 end;
+ int removed = 0;
+ bool added = false;
+
+ if (!dev || !length)
+ return -EINVAL;
+ if (check_add_overflow(start, length, &end) || end <= start)
+ return -EINVAL;
+
+ /* A single removal can split at most one range. */
+ right = kzalloc(sizeof(*right), GFP_NOFS);
+ if (!right)
+ return -ENOMEM;
+
+ mutex_lock(&dev->bad_extents_lock);
+ list_for_each_entry_safe(extent, tmp, &dev->bad_extents, list) {
+ u64 extent_end = extent->start + extent->length;
+
+ if (end <= extent->start || start >= extent_end)
+ continue;
+
+ if (start <= extent->start && end >= extent_end) {
+ list_del(&extent->list);
+ kfree(extent);
+ removed++;
+ continue;
+ }
+
+ if (start <= extent->start) {
+ u64 delta = end - extent->start;
+
+ extent->start = end;
+ extent->length = extent_end - end;
+ if (extent->logical)
+ extent->logical += delta;
+ continue;
+ }
+
+ if (end >= extent_end) {
+ extent->length = start - extent->start;
+ continue;
+ }
+
+ /* The removed range is strictly inside this extent. */
+ *right = *extent;
+ INIT_LIST_HEAD(&right->list);
+ right->start = end;
+ right->length = extent_end - end;
+ if (right->logical)
+ right->logical += end - extent->start;
+ right->errors = 1;
+ extent->length = start - extent->start;
+ list_add(&right->list, &extent->list);
+ right = NULL;
+ added = true;
+ break;
+ }
+ if (removed)
+ atomic_sub(removed, &dev->bad_extents_count);
+ if (added)
+ atomic_inc(&dev->bad_extents_count);
+ mutex_unlock(&dev->bad_extents_lock);
+
+ kfree(right);
+ return 0;
+}
+
+void btrfs_bad_extents_clear(struct btrfs_device *dev)
+{
+ struct btrfs_bad_extent *extent, *tmp;
+
+ mutex_lock(&dev->bad_extents_lock);
+ list_for_each_entry_safe(extent, tmp, &dev->bad_extents, list) {
+ list_del(&extent->list);
+ kfree(extent);
+ }
+ atomic_set(&dev->bad_extents_count, 0);
+ mutex_unlock(&dev->bad_extents_lock);
+}
+
+/*
+ * True if [physical, physical + length) overlaps a confirmed bad extent.
+ *
+ * This is called from the read submission path, so the common case (no bad
+ * extents at all) is a single atomic read and no locking.
+ */
+bool btrfs_bad_extents_confirmed_overlap(struct btrfs_device *dev, u64 physical,
+ u64 length)
+{
+ struct btrfs_bad_extent *extent;
+ u64 end;
+ bool found = false;
+
+ if (!dev || !length || check_add_overflow(physical, length, &end))
+ return false;
+ if (atomic_read(&dev->bad_extents_count) == 0)
+ return false;
+
+ mutex_lock(&dev->bad_extents_lock);
+ list_for_each_entry(extent, &dev->bad_extents, list) {
+ if (extent->state != BTRFS_BAD_EXTENT_STATE_CONFIRMED)
+ continue;
+ if (physical < extent->start + extent->length &&
+ extent->start < end) {
+ found = true;
+ break;
+ }
+ }
+ mutex_unlock(&dev->bad_extents_lock);
+ return found;
+}
+
+/*
+ * Trim a candidate device-extent allocation hole so it never covers a
+ * confirmed bad physical range.
+ *
+ * On input [*start, *start + *size) is a free hole. On return the same
+ * fields describe the best sub-hole that does not overlap a confirmed
+ * extent. Returns true when that sub-hole is at least @num_bytes, which is
+ * the only case the caller may allocate. Suspect ranges are ignored: they
+ * are still candidates for verification and repair.
+ *
+ * Zoned devices are left untouched; zone-aware exclusion would also have to
+ * respect zone alignment and is outside this prototype.
+ */
+bool btrfs_bad_extents_trim_alloc_hole(struct btrfs_device *dev, u64 *start,
+ u64 *size, u64 num_bytes)
+{
+ struct btrfs_bad_extent *extent;
+ u64 hole_end, pos, best_start, best_size = 0;
+ bool fits = false;
+
+ if (!dev || !size || !*size || dev->zone_info)
+ return *size >= num_bytes;
+ if (atomic_read(&dev->bad_extents_count) == 0)
+ return *size >= num_bytes;
+
+ hole_end = *start + *size;
+ pos = *start;
+ best_start = pos;
+
+ mutex_lock(&dev->bad_extents_lock);
+ while (pos < hole_end) {
+ struct btrfs_bad_extent *bad = NULL;
+ u64 good_size;
+
+ list_for_each_entry(extent, &dev->bad_extents, list) {
+ if (extent->state != BTRFS_BAD_EXTENT_STATE_CONFIRMED)
+ continue;
+ if (extent->start + extent->length <= pos)
+ continue;
+ bad = extent;
+ break;
+ }
+
+ if (!bad || bad->start >= hole_end) {
+ good_size = hole_end - pos;
+ if (good_size > best_size) {
+ best_start = pos;
+ best_size = good_size;
+ }
+ if (good_size >= num_bytes)
+ fits = true;
+ break;
+ }
+
+ if (bad->start > pos) {
+ good_size = bad->start - pos;
+ if (good_size > best_size) {
+ best_start = pos;
+ best_size = good_size;
+ }
+ if (good_size >= num_bytes) {
+ fits = true;
+ break;
+ }
+ }
+ pos = bad->start + bad->length;
+ }
+ mutex_unlock(&dev->bad_extents_lock);
+
+ *start = best_start;
+ *size = best_size;
+ return fits;
+}
+
+/*
+ * Redirect writes that would touch a confirmed bad physical range to another
+ * mirror of the same logical range. This is the write-side counterpart of
+ * read fast-fail: a RAID1/DUP/RAID10 write can survive one dead copy by
+ * updating a live copy instead.
+ *
+ * The caller must only invoke this for mirror profiles, where @stripes holds
+ * all copies of the same logical range. When the raid-stripe-tree feature is
+ * enabled, btrfs_insert_raid_extent() later records the aliased stripe, so
+ * reads use the live mapping as well.
+ *
+ * Returns -EIO when every copy of the range is confirmed bad; the write must
+ * not touch a known-bad sector.
+ */
+int btrfs_bad_extents_redirect_write(struct btrfs_io_stripe *stripes,
+ int num_stripes, u64 length)
+{
+ int i;
+
+ if (num_stripes < 2 || length == 0)
+ return 0;
+
+ for (i = 0; i < num_stripes; i++) {
+ struct btrfs_io_stripe *bad = &stripes[i];
+ struct btrfs_io_stripe *good = NULL;
+ int j;
+
+ if (!bad->dev)
+ continue;
+ if (!btrfs_bad_extents_confirmed_overlap(bad->dev, bad->physical,
+ length))
+ continue;
+
+ for (j = 0; j < num_stripes; j++) {
+ if (j == i || !stripes[j].dev)
+ continue;
+ if (!btrfs_bad_extents_confirmed_overlap(stripes[j].dev,
+ stripes[j].physical,
+ length)) {
+ good = &stripes[j];
+ break;
+ }
+ }
+ if (!good)
+ return -EIO;
+
+ btrfs_warn_rl(bad->dev->fs_info,
+ "redirecting write from bad physical %llu on devid %llu to devid %llu physical %llu",
+ bad->physical, bad->dev->devid, good->dev->devid,
+ good->physical);
+ bad->dev = good->dev;
+ bad->physical = good->physical;
+ }
+ return 0;
+}
+
+int btrfs_bad_extents_list(struct btrfs_device *dev,
+ struct btrfs_ioctl_bad_extent *buf,
+ u64 capacity, u64 *total)
+{
+ struct btrfs_bad_extent *extent;
+ u64 nr = 0;
+
+ mutex_lock(&dev->bad_extents_lock);
+ list_for_each_entry(extent, &dev->bad_extents, list) {
+ if (buf && nr < capacity) {
+ buf[nr].start = extent->start;
+ buf[nr].length = extent->length;
+ buf[nr].logical = extent->logical;
+ buf[nr].devid = dev->devid;
+ buf[nr].state = extent->state;
+ buf[nr].errors = extent->errors;
+ buf[nr].reserved = 0;
+ }
+ nr++;
+ }
+ mutex_unlock(&dev->bad_extents_lock);
+
+ *total = nr;
+ return 0;
+}
+
+/*
+ * On-disk persistence.
+ *
+ * Bad extents live in the device tree as
+ * (devid, BTRFS_DEV_BAD_EXTENT_KEY, physical_start)
+ * items. A sync is a delete-all/insert-all of the extents for one devid.
+ */
+
+struct btrfs_bad_extent_snap {
+ u64 start;
+ u64 length;
+ u64 logical;
+ u32 state;
+ u32 errors;
+};
+
+/*
+ * Copy the in-memory list for a device into a plain array. The device list
+ * mutex is held only while looking up the device and taking the snapshot; the
+ * transaction commit below must not run under it because
+ * btrfs_run_dev_stats() takes the same mutex during commit.
+ */
+static int bad_extents_snapshot(struct btrfs_fs_info *fs_info, u64 devid,
+ struct btrfs_bad_extent_snap **snap_ret,
+ u64 *nr_ret)
+{
+ BTRFS_DEV_LOOKUP_ARGS(lookup);
+ struct btrfs_bad_extent_snap *snap = NULL;
+ struct btrfs_bad_extent *extent;
+ struct btrfs_device *dev;
+ u64 nr = 0;
+ u64 i = 0;
+ int ret = 0;
+
+ *snap_ret = NULL;
+ *nr_ret = 0;
+
+ lookup.devid = devid;
+ mutex_lock(&fs_info->fs_devices->device_list_mutex);
+ dev = btrfs_find_device(fs_info->fs_devices, &lookup);
+ if (!dev) {
+ ret = -ENODEV;
+ goto out_device;
+ }
+
+ mutex_lock(&dev->bad_extents_lock);
+ list_for_each_entry(extent, &dev->bad_extents, list)
+ nr++;
+
+ if (nr) {
+ snap = kcalloc(nr, sizeof(*snap), GFP_NOFS);
+ if (!snap) {
+ ret = -ENOMEM;
+ goto out_unlock;
+ }
+ list_for_each_entry(extent, &dev->bad_extents, list) {
+ snap[i].start = extent->start;
+ snap[i].length = extent->length;
+ snap[i].logical = extent->logical;
+ snap[i].state = extent->state;
+ snap[i].errors = extent->errors;
+ i++;
+ }
+ }
+ ASSERT(i == nr);
+
+out_unlock:
+ mutex_unlock(&dev->bad_extents_lock);
+out_device:
+ mutex_unlock(&fs_info->fs_devices->device_list_mutex);
+
+ if (ret)
+ kfree(snap);
+ else {
+ *snap_ret = snap;
+ *nr_ret = nr;
+ }
+ return ret;
+}
+
+static int bad_extents_count_persisted(struct btrfs_fs_info *fs_info,
+ u64 devid, u64 *nr)
+{
+ struct btrfs_key key;
+ struct btrfs_key found;
+ struct btrfs_path *path;
+ int ret;
+
+ *nr = 0;
+ path = btrfs_alloc_path();
+ if (!path)
+ return -ENOMEM;
+
+ key.objectid = devid;
+ key.type = BTRFS_DEV_BAD_EXTENT_KEY;
+ key.offset = 0;
+ ret = btrfs_search_slot(NULL, fs_info->dev_root, &key, path, 0, 0);
+ if (ret < 0)
+ goto out;
+
+ while (true) {
+ if (path->slots[0] >= btrfs_header_nritems(path->nodes[0])) {
+ ret = btrfs_next_leaf(fs_info->dev_root, path);
+ if (ret > 0) {
+ ret = 0;
+ break;
+ }
+ if (ret < 0)
+ goto out;
+ }
+ btrfs_item_key_to_cpu(path->nodes[0], &found, path->slots[0]);
+ if (found.objectid != devid ||
+ found.type != BTRFS_DEV_BAD_EXTENT_KEY)
+ break;
+ (*nr)++;
+ path->slots[0]++;
+ }
+ ret = 0;
+out:
+ btrfs_free_path(path);
+ return ret;
+}
+
+static int bad_extents_delete_all(struct btrfs_trans_handle *trans,
+ struct btrfs_fs_info *fs_info, u64 devid)
+{
+ struct btrfs_key key;
+ struct btrfs_key found;
+ struct btrfs_path *path;
+ int ret;
+
+ path = btrfs_alloc_path();
+ if (!path)
+ return -ENOMEM;
+
+ key.objectid = devid;
+ key.type = BTRFS_DEV_BAD_EXTENT_KEY;
+ key.offset = 0;
+ while (true) {
+ ret = btrfs_search_slot(trans, fs_info->dev_root, &key, path,
+ -1, 1);
+ if (ret < 0)
+ break;
+
+ if (path->slots[0] >= btrfs_header_nritems(path->nodes[0])) {
+ ret = btrfs_next_leaf(fs_info->dev_root, path);
+ if (ret > 0) {
+ ret = 0;
+ break;
+ }
+ if (ret < 0)
+ break;
+ }
+
+ btrfs_item_key_to_cpu(path->nodes[0], &found, path->slots[0]);
+ if (found.objectid != devid ||
+ found.type != BTRFS_DEV_BAD_EXTENT_KEY) {
+ ret = 0;
+ break;
+ }
+
+ ret = btrfs_del_item(trans, fs_info->dev_root, path);
+ if (ret)
+ break;
+ btrfs_release_path(path);
+ }
+ btrfs_free_path(path);
+ return ret;
+}
+
+static int bad_extents_insert(struct btrfs_trans_handle *trans,
+ struct btrfs_fs_info *fs_info, u64 devid,
+ const struct btrfs_bad_extent_snap *snap)
+{
+ struct btrfs_dev_bad_extent_item *item;
+ struct extent_buffer *leaf;
+ struct btrfs_path *path;
+ struct btrfs_key key;
+ int ret;
+
+ path = btrfs_alloc_path();
+ if (!path)
+ return -ENOMEM;
+
+ key.objectid = devid;
+ key.type = BTRFS_DEV_BAD_EXTENT_KEY;
+ key.offset = snap->start;
+ ret = btrfs_insert_empty_item(trans, fs_info->dev_root, path, &key,
+ sizeof(*item));
+ if (ret) {
+ btrfs_free_path(path);
+ return ret;
+ }
+
+ leaf = path->nodes[0];
+ item = btrfs_item_ptr(leaf, path->slots[0],
+ struct btrfs_dev_bad_extent_item);
+ btrfs_set_dev_bad_extent_length(leaf, item, snap->length);
+ btrfs_set_dev_bad_extent_logical(leaf, item, snap->logical);
+ btrfs_set_dev_bad_extent_generation(leaf, item, trans->transid);
+ btrfs_set_dev_bad_extent_state(leaf, item, snap->state);
+ btrfs_set_dev_bad_extent_errors(leaf, item, snap->errors);
+
+ btrfs_free_path(path);
+ return 0;
+}
+
+static int bad_extents_sync(struct btrfs_fs_info *fs_info, u64 devid,
+ const struct btrfs_bad_extent_snap *snap, u64 nr)
+{
+ struct btrfs_trans_handle *trans;
+ u64 old_nr;
+ u64 reserve;
+ u64 i;
+ int ret;
+
+ ret = bad_extents_count_persisted(fs_info, devid, &old_nr);
+ if (ret)
+ return ret;
+
+ /* Nothing to do: keep the incompat flag (if any) untouched. */
+ if (!nr && !old_nr)
+ return 0;
+
+ reserve = 2 + 2 * max(old_nr, nr);
+ if (reserve > 64)
+ reserve = 64;
+
+ trans = btrfs_start_transaction(fs_info->dev_root, reserve);
+ if (IS_ERR(trans))
+ return PTR_ERR(trans);
+
+ ret = bad_extents_delete_all(trans, fs_info, devid);
+ if (ret)
+ goto abort;
+
+ for (i = 0; i < nr; i++) {
+ ret = bad_extents_insert(trans, fs_info, devid, &snap[i]);
+ if (ret)
+ goto abort;
+ }
+
+ /*
+ * Set/clear the feature before committing. Once the last item is gone
+ * the feature has no remaining semantics, so it is safe to clear it and
+ * let older kernels mount the filesystem again.
+ */
+ if (nr)
+ btrfs_set_fs_incompat(fs_info, BAD_EXTENTS);
+ else
+ btrfs_clear_fs_incompat(fs_info, BAD_EXTENTS);
+
+ return btrfs_commit_transaction(trans);
+
+abort:
+ btrfs_abort_transaction(trans, ret);
+ return ret;
+}
+
+/*
+ * Persist the current in-memory list of one device. Must be called without
+ * device_list_mutex held.
+ */
+int btrfs_bad_extents_persist(struct btrfs_fs_info *fs_info, u64 devid)
+{
+ struct btrfs_bad_extent_snap *snap = NULL;
+ u64 nr = 0;
+ int ret;
+
+ if (sb_rdonly(fs_info->sb))
+ return -EROFS;
+
+ ret = bad_extents_snapshot(fs_info, devid, &snap, &nr);
+ if (ret)
+ return ret;
+
+ ret = bad_extents_sync(fs_info, devid, snap, nr);
+ kfree(snap);
+ return ret;
+}
+
+static int bad_extents_load_device(struct btrfs_device *dev)
+{
+ struct btrfs_fs_info *fs_info = dev->fs_info;
+ struct btrfs_dev_bad_extent_item *item;
+ struct btrfs_bad_extent *extent;
+ struct extent_buffer *leaf;
+ struct btrfs_key key;
+ struct btrfs_key found;
+ struct btrfs_path *path;
+ int ret;
+
+ path = btrfs_alloc_path();
+ if (!path)
+ return -ENOMEM;
+
+ key.objectid = dev->devid;
+ key.type = BTRFS_DEV_BAD_EXTENT_KEY;
+ key.offset = 0;
+ ret = btrfs_search_slot(NULL, fs_info->dev_root, &key, path, 0, 0);
+ if (ret < 0)
+ goto out;
+
+ while (true) {
+ u64 start, length, logical;
+ u32 state, errors;
+
+ if (path->slots[0] >= btrfs_header_nritems(path->nodes[0])) {
+ ret = btrfs_next_leaf(fs_info->dev_root, path);
+ if (ret > 0) {
+ ret = 0;
+ break;
+ }
+ if (ret < 0)
+ goto out;
+ }
+
+ leaf = path->nodes[0];
+ btrfs_item_key_to_cpu(leaf, &found, path->slots[0]);
+ if (found.objectid != dev->devid ||
+ found.type != BTRFS_DEV_BAD_EXTENT_KEY)
+ break;
+
+ if (btrfs_item_size(leaf, path->slots[0]) != sizeof(*item)) {
+ btrfs_warn(fs_info,
+ "invalid bad extent item size for devid %llu",
+ dev->devid);
+ path->slots[0]++;
+ continue;
+ }
+
+ item = btrfs_item_ptr(leaf, path->slots[0],
+ struct btrfs_dev_bad_extent_item);
+ start = found.offset;
+ length = btrfs_dev_bad_extent_length(leaf, item);
+ logical = btrfs_dev_bad_extent_logical(leaf, item);
+ state = btrfs_dev_bad_extent_state(leaf, item);
+ errors = btrfs_dev_bad_extent_errors(leaf, item);
+
+ if (!length || start >= dev->total_bytes ||
+ length > dev->total_bytes - start) {
+ btrfs_warn(fs_info,
+ "ignoring invalid bad extent on devid %llu start %llu length %llu",
+ dev->devid, start, length);
+ path->slots[0]++;
+ continue;
+ }
+ if (state != BTRFS_BAD_EXTENT_STATE_CONFIRMED)
+ state = BTRFS_BAD_EXTENT_STATE_SUSPECT;
+
+ extent = kzalloc(sizeof(*extent), GFP_NOFS);
+ if (!extent) {
+ ret = -ENOMEM;
+ goto out;
+ }
+ extent->start = start;
+ extent->length = length;
+ extent->logical = logical;
+ extent->state = state;
+ extent->errors = errors ? errors : 1;
+
+ mutex_lock(&dev->bad_extents_lock);
+ list_add_tail(&extent->list, &dev->bad_extents);
+ atomic_inc(&dev->bad_extents_count);
+ mutex_unlock(&dev->bad_extents_lock);
+
+ path->slots[0]++;
+ }
+ ret = 0;
+out:
+ btrfs_free_path(path);
+ return ret;
+}
+
+/*
+ * Load persisted bad extents at mount time. Only runs when the incompat
+ * feature is set, so filesystems without the feature pay nothing.
+ */
+int btrfs_bad_extents_load(struct btrfs_fs_info *fs_info)
+{
+ struct btrfs_device *dev;
+ int ret = 0;
+
+ if (!btrfs_fs_incompat(fs_info, BAD_EXTENTS))
+ return 0;
+ if (!fs_info->dev_root)
+ return 0;
+
+ mutex_lock(&fs_info->fs_devices->device_list_mutex);
+ list_for_each_entry(dev, &fs_info->fs_devices->devices, dev_list) {
+ ret = bad_extents_load_device(dev);
+ if (ret)
+ break;
+ }
+ mutex_unlock(&fs_info->fs_devices->device_list_mutex);
+
+ if (ret)
+ btrfs_err(fs_info, "failed to load bad extents: %d", ret);
+ return ret;
+}
diff --git a/fs/btrfs/bad-extents.h b/fs/btrfs/bad-extents.h
new file mode 100644
index 0000000..8ee847c
--- /dev/null
+++ b/fs/btrfs/bad-extents.h
@@ -0,0 +1,46 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Per-device physical bad extent tracking (prototype).
+ *
+ * The list is populated by scrub and by an ioctl. A list is meant to be
+ * consumed by userspace so it can feed the ranges to a block-layer remapper
+ * such as dm-quarantine, and it is persisted in the device tree so the
+ * information survives unmount/remount.
+ */
+
+#ifndef BTRFS_BAD_EXTENTS_H
+#define BTRFS_BAD_EXTENTS_H
+
+#include <linux/types.h>
+
+struct btrfs_device;
+struct btrfs_fs_info;
+struct btrfs_io_stripe;
+struct btrfs_ioctl_bad_extent;
+
+void btrfs_bad_extents_init(struct btrfs_device *dev);
+void btrfs_bad_extents_free(struct btrfs_device *dev);
+
+int btrfs_bad_extents_record(struct btrfs_device *dev, u64 physical,
+ u64 length, u64 logical, u32 state);
+int btrfs_bad_extents_add(struct btrfs_device *dev, u64 start, u64 length,
+ u64 logical, u32 state);
+int btrfs_bad_extents_remove(struct btrfs_device *dev, u64 start, u64 length);
+void btrfs_bad_extents_clear(struct btrfs_device *dev);
+int btrfs_bad_extents_list(struct btrfs_device *dev,
+ struct btrfs_ioctl_bad_extent *buf,
+ u64 capacity, u64 *total);
+
+bool btrfs_bad_extents_confirmed_overlap(struct btrfs_device *dev,
+ u64 physical, u64 length);
+
+bool btrfs_bad_extents_trim_alloc_hole(struct btrfs_device *dev, u64 *start,
+ u64 *size, u64 num_bytes);
+
+int btrfs_bad_extents_redirect_write(struct btrfs_io_stripe *stripes,
+ int num_stripes, u64 length);
+
+int btrfs_bad_extents_persist(struct btrfs_fs_info *fs_info, u64 devid);
+int btrfs_bad_extents_load(struct btrfs_fs_info *fs_info);
+
+#endif /* BTRFS_BAD_EXTENTS_H */
diff --git a/fs/btrfs/bio.c b/fs/btrfs/bio.c
index 6225f9d..f1f6cc2 100644
--- a/fs/btrfs/bio.c
+++ b/fs/btrfs/bio.c
@@ -8,6 +8,7 @@
#include "bio.h"
#include "ctree.h"
#include "volumes.h"
+#include "bad-extents.h"
#include "raid56.h"
#include "async-thread.h"
#include "dev-replace.h"
@@ -676,8 +677,10 @@ static bool btrfs_submit_chunk(struct btrfs_bio *bbio, int mirror_num)
struct btrfs_io_stripe smap;
blk_status_t ret;
int error;
+ bool reloc;
- if (!bbio->inode || btrfs_is_data_reloc_root(inode->root))
+ reloc = inode && btrfs_is_data_reloc_root(inode->root);
+ if (!bbio->inode || reloc)
smap.rst_search_commit_root = true;
else
smap.rst_search_commit_root = false;
@@ -760,6 +763,24 @@ static bool btrfs_submit_chunk(struct btrfs_bio *bbio, int mirror_num)
}
}
+ /*
+ * Do not touch a physical range that scrub confirmed bad. Route the
+ * failure through the normal read completion so btrfs can try another
+ * mirror; if there is no other copy the caller gets -EIO. Data
+ * relocation is the exception: it has to read the old copy to move
+ * the data away.
+ */
+ if (bio_op(bio) == REQ_OP_READ && !bioc && !reloc &&
+ btrfs_bad_extents_confirmed_overlap(smap.dev, smap.physical,
+ map_length)) {
+ bbio->mirror_num = mirror_num;
+ bio->bi_private = smap.dev;
+ bio->bi_status = BLK_STS_IOERR;
+ bio->bi_end_io = btrfs_simple_end_io;
+ btrfs_simple_end_io(bio);
+ return true;
+ }
+
btrfs_submit_bio(bio, bioc, &smap, mirror_num);
done:
return map_length == length;
@@ -834,6 +855,15 @@ int btrfs_repair_io_failure(struct btrfs_fs_info *fs_info, u64 ino, u64 start,
if (ret < 0)
goto out_counter_dec;
+ /* Never write repaired data back into a confirmed bad physical range. */
+ if (btrfs_bad_extents_confirmed_overlap(smap.dev, smap.physical, length)) {
+ btrfs_debug(fs_info,
+ "skip repair write into confirmed bad range on dev %s",
+ btrfs_dev_name(smap.dev));
+ ret = 0;
+ goto out_counter_dec;
+ }
+
if (!smap.dev->bdev ||
!test_bit(BTRFS_DEV_STATE_WRITEABLE, &smap.dev->dev_state)) {
ret = -EIO;
@@ -891,6 +921,12 @@ void btrfs_submit_repair_write(struct btrfs_bio *bbio, int mirror_num, bool dev_
ASSERT(smap.dev == fs_info->dev_replace.srcdev);
smap.dev = fs_info->dev_replace.tgtdev;
}
+ /* Do not write into a confirmed bad range (unless replacing to a new dev). */
+ if (btrfs_bad_extents_confirmed_overlap(smap.dev, smap.physical, length)) {
+ btrfs_bio_counter_dec(fs_info);
+ btrfs_bio_end_io(bbio, BLK_STS_OK);
+ return;
+ }
btrfs_submit_bio(&bbio->bio, NULL, &smap, mirror_num);
return;
diff --git a/fs/btrfs/disk-io.c b/fs/btrfs/disk-io.c
index d1f245e..96d54ae 100644
--- a/fs/btrfs/disk-io.c
+++ b/fs/btrfs/disk-io.c
@@ -49,6 +49,7 @@
#include "uuid-tree.h"
#include "relocation.h"
#include "scrub.h"
+#include "bad-extents.h"
#include "super.h"
#define BTRFS_SUPER_FLAG_SUPP (BTRFS_HEADER_FLAG_WRITTEN |\
@@ -2767,6 +2768,7 @@ void btrfs_init_fs_info(struct btrfs_fs_info *fs_info)
mutex_init(&fs_info->transaction_kthread_mutex);
mutex_init(&fs_info->cleaner_mutex);
mutex_init(&fs_info->ro_block_group_mutex);
+ mutex_init(&fs_info->bad_extents_mutex);
init_rwsem(&fs_info->commit_root_sem);
init_rwsem(&fs_info->cleanup_work_sem);
init_rwsem(&fs_info->subvol_sem);
@@ -3430,6 +3432,12 @@ int __cold open_ctree(struct super_block *sb, struct btrfs_fs_devices *fs_device
goto fail_block_groups;
}
+ ret = btrfs_bad_extents_load(fs_info);
+ if (ret) {
+ btrfs_err(fs_info, "failed to init bad extents: %d", ret);
+ goto fail_block_groups;
+ }
+
ret = btrfs_init_dev_replace(fs_info);
if (ret) {
btrfs_err(fs_info, "failed to init dev_replace: %d", ret);
diff --git a/fs/btrfs/fs.h b/fs/btrfs/fs.h
index de10a95..9092805 100644
--- a/fs/btrfs/fs.h
+++ b/fs/btrfs/fs.h
@@ -271,6 +271,7 @@ enum {
BTRFS_FEATURE_INCOMPAT_METADATA_UUID | \
BTRFS_FEATURE_INCOMPAT_RAID1C34 | \
BTRFS_FEATURE_INCOMPAT_ZONED | \
+ BTRFS_FEATURE_INCOMPAT_BAD_EXTENTS | \
BTRFS_FEATURE_INCOMPAT_SIMPLE_QUOTA)
#ifdef CONFIG_BTRFS_DEBUG
@@ -713,6 +714,9 @@ struct btrfs_fs_info {
refcount_t scrub_workers_refcnt;
struct workqueue_struct *scrub_workers;
+ /* Serialize bad extent mutation with device-tree persistence. */
+ struct mutex bad_extents_mutex;
+
struct btrfs_discard_ctl discard_ctl;
/* Is qgroup tracking in a consistent state? */
diff --git a/fs/btrfs/ioctl.c b/fs/btrfs/ioctl.c
index 3c3c767..4d329ee 100644
--- a/fs/btrfs/ioctl.c
+++ b/fs/btrfs/ioctl.c
@@ -35,6 +35,7 @@
#include "transaction.h"
#include "btrfs_inode.h"
#include "volumes.h"
+#include "bad-extents.h"
#include "locking.h"
#include "backref.h"
#include "send.h"
@@ -3330,6 +3331,99 @@ static long btrfs_ioctl_get_dev_stats(struct btrfs_fs_info *fs_info,
return ret;
}
+static long btrfs_ioctl_dev_bad_extents(struct btrfs_fs_info *fs_info,
+ void __user *arg)
+{
+ struct btrfs_ioctl_dev_bad_extents_args args;
+ struct btrfs_ioctl_bad_extent *buf = NULL;
+ BTRFS_DEV_LOOKUP_ARGS(lookup);
+ struct btrfs_device *dev;
+ bool mutating;
+ u64 total = 0;
+ int ret = 0;
+
+ if (copy_from_user(&args, arg, sizeof(args)))
+ return -EFAULT;
+
+ mutating = args.op != BTRFS_BAD_EXTENT_OP_LIST;
+
+ if (args.op != BTRFS_BAD_EXTENT_OP_LIST && !capable(CAP_SYS_ADMIN))
+ return -EPERM;
+ if (args.op != BTRFS_BAD_EXTENT_OP_LIST && sb_rdonly(fs_info->sb))
+ return -EROFS;
+
+ if (args.op == BTRFS_BAD_EXTENT_OP_LIST && args.nr_extents) {
+ if (args.nr_extents > (1U << 20))
+ return -E2BIG;
+ if (!args.extents)
+ return -EFAULT;
+ buf = kcalloc(args.nr_extents, sizeof(*buf), GFP_KERNEL);
+ if (!buf)
+ return -ENOMEM;
+ }
+
+ if (mutating)
+ mutex_lock(&fs_info->bad_extents_mutex);
+
+ lookup.devid = args.devid;
+ mutex_lock(&fs_info->fs_devices->device_list_mutex);
+ dev = btrfs_find_device(fs_info->fs_devices, &lookup);
+ if (!dev) {
+ ret = -ENODEV;
+ goto out_unlock;
+ }
+
+ switch (args.op) {
+ case BTRFS_BAD_EXTENT_OP_LIST:
+ ret = btrfs_bad_extents_list(dev, buf, args.nr_extents, &total);
+ break;
+ case BTRFS_BAD_EXTENT_OP_ADD:
+ ret = btrfs_bad_extents_add(dev, args.start, args.length,
+ args.logical, args.state);
+ break;
+ case BTRFS_BAD_EXTENT_OP_REMOVE:
+ ret = btrfs_bad_extents_remove(dev, args.start, args.length);
+ break;
+ case BTRFS_BAD_EXTENT_OP_CLEAR:
+ btrfs_bad_extents_clear(dev);
+ break;
+ case BTRFS_BAD_EXTENT_OP_SYNC:
+ /* Nothing to mutate; persistence happens below. */
+ break;
+ default:
+ ret = -EINVAL;
+ break;
+ }
+
+out_unlock:
+ mutex_unlock(&fs_info->fs_devices->device_list_mutex);
+
+ /*
+ * Persist outside device_list_mutex: committing the transaction can run
+ * btrfs_run_dev_stats(), which takes the same mutex.
+ */
+ if (!ret && mutating)
+ ret = btrfs_bad_extents_persist(fs_info, args.devid);
+ if (mutating)
+ mutex_unlock(&fs_info->bad_extents_mutex);
+
+ if (!ret && args.op == BTRFS_BAD_EXTENT_OP_LIST) {
+ u64 copied = min(total, args.nr_extents);
+
+ args.nr_extents = total;
+ if (copied &&
+ copy_to_user((void __user *)(unsigned long)args.extents,
+ buf, copied * sizeof(*buf)))
+ ret = -EFAULT;
+ }
+ kfree(buf);
+ if (ret)
+ return ret;
+ if (copy_to_user(arg, &args, sizeof(args)))
+ return -EFAULT;
+ return 0;
+}
+
static long btrfs_ioctl_dev_replace(struct btrfs_fs_info *fs_info,
void __user *arg)
{
@@ -4855,6 +4949,8 @@ long btrfs_ioctl(struct file *file, unsigned int
#endif
case BTRFS_IOC_GET_DEV_STATS:
return btrfs_ioctl_get_dev_stats(fs_info, argp);
+ case BTRFS_IOC_DEV_BAD_EXTENTS:
+ return btrfs_ioctl_dev_bad_extents(fs_info, argp);
case BTRFS_IOC_QUOTA_CTL:
return btrfs_ioctl_quota_ctl(file, argp);
case BTRFS_IOC_QGROUP_ASSIGN:
diff --git a/fs/btrfs/scrub.c b/fs/btrfs/scrub.c
index 9afe363..012727b 100644
--- a/fs/btrfs/scrub.c
+++ b/fs/btrfs/scrub.c
@@ -10,6 +10,7 @@
#include "ctree.h"
#include "discard.h"
#include "volumes.h"
+#include "bad-extents.h"
#include "disk-io.h"
#include "ordered-data.h"
#include "transaction.h"
@@ -1026,6 +1027,36 @@ static void scrub_write_sectors(struct scrub_ctx *sctx, struct scrub_stripe *str
* Writeback for dev-replace does not happen here, it needs extra
* synchronization for zoned devices.
*/
+
+/*
+ * Report the initially failed sectors of this stripe to the per-device bad
+ * extent list. This must run before repair reads rewrite the error bitmaps:
+ * at this point io_error_bitmap/csum_error_bitmap describe the first read.
+ */
+static void scrub_record_bad_extents(struct scrub_stripe *stripe)
+{
+ struct btrfs_fs_info *fs_info = stripe->bg->fs_info;
+ int sector_nr;
+
+ if (test_bit(SCRUB_STRIPE_FLAG_NO_REPORT, &stripe->state))
+ return;
+ if (!stripe->dev)
+ return;
+
+ for_each_set_bit(sector_nr, &stripe->init_error_bitmap,
+ stripe->nr_sectors) {
+ u64 offset = (u64)sector_nr << fs_info->sectorsize_bits;
+ u32 state = test_bit(sector_nr, &stripe->io_error_bitmap) ?
+ BTRFS_BAD_EXTENT_STATE_CONFIRMED :
+ BTRFS_BAD_EXTENT_STATE_SUSPECT;
+
+ btrfs_bad_extents_record(stripe->dev,
+ stripe->physical + offset,
+ fs_info->sectorsize,
+ stripe->logical + offset, state);
+ }
+}
+
static void scrub_stripe_read_repair_worker(struct work_struct *work)
{
struct scrub_stripe *stripe = container_of(work, struct scrub_stripe, work);
@@ -1051,6 +1082,7 @@ static void scrub_stripe_read_repair_worker(struct work_struct *work)
stripe->nr_sectors);
stripe->init_nr_meta_gen_errors = bitmap_weight(&stripe->meta_gen_error_bitmap,
stripe->nr_sectors);
+ scrub_record_bad_extents(stripe);
if (bitmap_empty(&stripe->init_error_bitmap, stripe->nr_sectors))
goto out;
diff --git a/fs/btrfs/sysfs.c b/fs/btrfs/sysfs.c
index 8f195e7..0298515 100644
--- a/fs/btrfs/sysfs.c
+++ b/fs/btrfs/sysfs.c
@@ -291,6 +291,7 @@ BTRFS_FEAT_ATTR_INCOMPAT(metadata_uuid, METADATA_UUID);
BTRFS_FEAT_ATTR_COMPAT_RO(free_space_tree, FREE_SPACE_TREE);
BTRFS_FEAT_ATTR_COMPAT_RO(block_group_tree, BLOCK_GROUP_TREE);
BTRFS_FEAT_ATTR_INCOMPAT(raid1c34, RAID1C34);
+BTRFS_FEAT_ATTR_INCOMPAT(bad_extents, BAD_EXTENTS);
BTRFS_FEAT_ATTR_INCOMPAT(simple_quota, SIMPLE_QUOTA);
#ifdef CONFIG_BLK_DEV_ZONED
BTRFS_FEAT_ATTR_INCOMPAT(zoned, ZONED);
@@ -325,6 +326,7 @@ static struct attribute *btrfs_supported_feature_attrs[] = {
BTRFS_FEAT_ATTR_PTR(free_space_tree),
BTRFS_FEAT_ATTR_PTR(raid1c34),
BTRFS_FEAT_ATTR_PTR(block_group_tree),
+ BTRFS_FEAT_ATTR_PTR(bad_extents),
BTRFS_FEAT_ATTR_PTR(simple_quota),
#ifdef CONFIG_BLK_DEV_ZONED
BTRFS_FEAT_ATTR_PTR(zoned),
diff --git a/fs/btrfs/volumes.c b/fs/btrfs/volumes.c
index 6d9df90..007407f 100644
--- a/fs/btrfs/volumes.c
+++ b/fs/btrfs/volumes.c
@@ -17,6 +17,7 @@
#include "disk-io.h"
#include "transaction.h"
#include "volumes.h"
+#include "bad-extents.h"
#include "raid56.h"
#include "rcu-string.h"
#include "dev-replace.h"
@@ -402,6 +403,7 @@ static struct btrfs_fs_devices *alloc_fs_devices(const u8 *fsid)
static void btrfs_free_device(struct btrfs_device *device)
{
WARN_ON(!list_empty(&device->post_commit_list));
+ btrfs_bad_extents_free(device);
rcu_string_free(device->name);
extent_io_tree_release(&device->alloc_state);
btrfs_destroy_dev_zone_info(device);
@@ -1854,6 +1856,9 @@ static int find_free_dev_extent(struct btrfs_device *device, u64 num_bytes,
hole_size = key.offset - search_start;
dev_extent_hole_check(device, &search_start, &hole_size,
num_bytes);
+ btrfs_bad_extents_trim_alloc_hole(device, &search_start,
+ &hole_size,
+ num_bytes);
if (hole_size > max_hole_size) {
max_hole_start = search_start;
@@ -1893,6 +1898,8 @@ next:
if (search_end > search_start) {
hole_size = search_end - search_start;
dev_extent_hole_check(device, &search_start, &hole_size, num_bytes);
+ btrfs_bad_extents_trim_alloc_hole(device, &search_start,
+ &hole_size, num_bytes);
if (hole_size > max_hole_size) {
max_hole_start = search_start;
@@ -6797,6 +6804,27 @@ int btrfs_map_block(struct btrfs_fs_info *fs_info, enum btrfs_map_op op,
goto out;
}
+ /*
+ * A write must not touch a physical range that scrub confirmed bad.
+ * For mirror profiles we can still complete it by updating a healthy
+ * copy instead; with the raid-stripe-tree feature enabled the aliased
+ * stripe is recorded at ordered extent completion, so reads use it too.
+ * Device replace has its own stripe duplication and is left alone.
+ */
+ if (op == BTRFS_MAP_WRITE && !dev_replace_is_ongoing &&
+ (map->type & (BTRFS_BLOCK_GROUP_RAID1_MASK |
+ BTRFS_BLOCK_GROUP_DUP |
+ BTRFS_BLOCK_GROUP_RAID10))) {
+ ret = btrfs_bad_extents_redirect_write(bioc->stripes,
+ io_geom.num_stripes,
+ *length);
+ if (ret) {
+ *bioc_ret = NULL;
+ btrfs_put_bioc(bioc);
+ goto out;
+ }
+ }
+
if (op != BTRFS_MAP_READ)
io_geom.max_errors = btrfs_chunk_max_errors(map);
@@ -6940,6 +6968,7 @@ struct btrfs_device *btrfs_alloc_device(struct btrfs_fs_info *fs_info,
INIT_LIST_HEAD(&dev->dev_list);
INIT_LIST_HEAD(&dev->dev_alloc_list);
INIT_LIST_HEAD(&dev->post_commit_list);
+ btrfs_bad_extents_init(dev);
atomic_set(&dev->dev_stats_ccnt, 0);
btrfs_device_data_ordered_init(dev);
diff --git a/fs/btrfs/volumes.h b/fs/btrfs/volumes.h
index a2863d2..d79904a 100644
--- a/fs/btrfs/volumes.h
+++ b/fs/btrfs/volumes.h
@@ -201,6 +201,11 @@ struct btrfs_device {
/* Bandwidth limit for scrub, in bytes */
u64 scrub_speed_max;
+
+ /* Physical bad extents reported by scrub (in-memory prototype). */
+ struct list_head bad_extents;
+ struct mutex bad_extents_lock;
+ atomic_t bad_extents_count;
};
/*
diff --git a/include/uapi/linux/btrfs.h b/include/uapi/linux/btrfs.h
index 8c5490c..aa75ca5 100644
--- a/include/uapi/linux/btrfs.h
+++ b/include/uapi/linux/btrfs.h
@@ -335,6 +335,7 @@ struct btrfs_ioctl_fs_info_args {
#define BTRFS_FEATURE_INCOMPAT_ZONED (1ULL << 12)
#define BTRFS_FEATURE_INCOMPAT_EXTENT_TREE_V2 (1ULL << 13)
#define BTRFS_FEATURE_INCOMPAT_RAID_STRIPE_TREE (1ULL << 14)
+#define BTRFS_FEATURE_INCOMPAT_BAD_EXTENTS (1ULL << 15)
#define BTRFS_FEATURE_INCOMPAT_SIMPLE_QUOTA (1ULL << 16)
struct btrfs_ioctl_feature_flags {
@@ -756,6 +757,43 @@ struct btrfs_ioctl_get_dev_stats {
__u64 unused[128 - 2 - BTRFS_DEV_STAT_VALUES_MAX];
};
+/*
+ * Per-device physical bad extents (prototype).
+ *
+ * Ranges are populated by scrub and by explicit import, and are persisted in
+ * the device tree under BTRFS_FEATURE_INCOMPAT_BAD_EXTENTS. Offsets are
+ * physical byte offsets on the device.
+ */
+#define BTRFS_BAD_EXTENT_STATE_SUSPECT 1
+#define BTRFS_BAD_EXTENT_STATE_CONFIRMED 2
+
+#define BTRFS_BAD_EXTENT_OP_LIST 0
+#define BTRFS_BAD_EXTENT_OP_ADD 1
+#define BTRFS_BAD_EXTENT_OP_REMOVE 2
+#define BTRFS_BAD_EXTENT_OP_CLEAR 3
+#define BTRFS_BAD_EXTENT_OP_SYNC 4 /* persist the current list */
+
+struct btrfs_ioctl_bad_extent {
+ __u64 start; /* physical byte offset on the device */
+ __u64 length; /* bytes */
+ __u64 logical; /* last logical byte offset, 0 if unknown */
+ __u64 devid;
+ __u32 state; /* BTRFS_BAD_EXTENT_STATE_* */
+ __u32 errors; /* number of observations */
+ __u64 reserved;
+};
+
+struct btrfs_ioctl_dev_bad_extents_args {
+ __u64 devid; /* in: device to operate on */
+ __u64 start; /* in: physical byte offset (ADD/REMOVE) */
+ __u64 length; /* in: bytes (ADD/REMOVE) */
+ __u64 logical; /* in: logical byte offset (ADD) */
+ __u64 nr_extents; /* in/out: array capacity; out: total extents */
+ __u64 extents; /* in: user pointer to btrfs_ioctl_bad_extent[] */
+ __u32 op; /* BTRFS_BAD_EXTENT_OP_* */
+ __u32 state; /* in: state for ADD */
+};
+
#define BTRFS_QUOTA_CTL_ENABLE 1
#define BTRFS_QUOTA_CTL_DISABLE 2
#define BTRFS_QUOTA_CTL_RESCAN__NOTUSED 3
@@ -1181,6 +1219,8 @@ enum btrfs_err_code {
struct btrfs_ioctl_encoded_io_args)
#define BTRFS_IOC_ENCODED_WRITE _IOW(BTRFS_IOCTL_MAGIC, 64, \
struct btrfs_ioctl_encoded_io_args)
+#define BTRFS_IOC_DEV_BAD_EXTENTS _IOWR(BTRFS_IOCTL_MAGIC, 65, \
+ struct btrfs_ioctl_dev_bad_extents_args)
#ifdef __cplusplus
}
diff --git a/include/uapi/linux/btrfs_tree.h b/include/uapi/linux/btrfs_tree.h
index fc29d27..0730c63 100644
--- a/include/uapi/linux/btrfs_tree.h
+++ b/include/uapi/linux/btrfs_tree.h
@@ -350,6 +350,14 @@
*/
#define BTRFS_DEV_REPLACE_KEY 250
+/*
+ * Persistently stores per-device physical bad extents in the device tree.
+ * One item exists per range and the key is built like this:
+ * (devid, BTRFS_DEV_BAD_EXTENT_KEY, physical_start)
+ * The range starts at physical_start and its length is stored in the item.
+ */
+#define BTRFS_DEV_BAD_EXTENT_KEY 254
+
/*
* Stores items that allow to quickly map UUIDs to something else.
* These items are part of the filesystem UUID tree.
@@ -1129,6 +1137,20 @@ struct btrfs_dev_stats_item {
__le64 values[BTRFS_DEV_STAT_VALUES_MAX];
} __attribute__ ((__packed__));
+/*
+ * Describes one physical bad extent of a device. The range starts at the
+ * key offset and covers length bytes. logical is the logical address the
+ * range was last seen at, or 0 if unknown. generation records the
+ * transaction that last wrote the item.
+ */
+struct btrfs_dev_bad_extent_item {
+ __le64 length;
+ __le64 logical;
+ __le64 generation;
+ __le32 state;
+ __le32 errors;
+} __attribute__ ((__packed__));
+
#define BTRFS_DEV_REPLACE_ITEM_CONT_READING_FROM_SRCDEV_MODE_ALWAYS 0
#define BTRFS_DEV_REPLACE_ITEM_CONT_READING_FROM_SRCDEV_MODE_AVOID 1
--
2.47.3
^ permalink raw reply related [flat|nested] 3+ messages in thread
* Re: [RFC PATCH 1/1] btrfs: track physical bad extents, fast-fail, persist, and relocate
2026-11-12 13:47 ` [RFC PATCH 1/1] btrfs: track physical bad extents, fast-fail, persist, and relocate Alexey Kuznetsov
@ 2026-11-15 10:35 ` Zygo Blaxell
0 siblings, 0 replies; 3+ messages in thread
From: Zygo Blaxell @ 2026-11-15 10:35 UTC (permalink / raw)
To: Alexey Kuznetsov; +Cc: linux-btrfs, linux-block, dm-devel
On Thu, Nov 12, 2026 at 08:47:03AM -0500, Alexey Kuznetsov wrote:
> Feedback wanted on: on-disk representation (device-tree items vs a
> dedicated tree vs extending RST), per-sector vs per-bio failure, whether
> the I/O path should record errors or only scrub/userspace, how to
> coordinate with device replace and balance, and whether an incompat bit
> is the right compatibility mechanism for a feature that changes read
> semantics.
Thanks for putting this together. I'm not sure why btrfs needs to be
involved at all, though.
The btrfs error-handling model already assumes a drive that remaps bad
sectors in firmware. A read hits a bad sector and gets EIO (or a csum
mismatch), btrfs reads another copy, and writes the good data back to
the failed location. That write is where the drive moves the sector to
a spare. From then on the LBA is good again, and the filesystem never
needs to know where the bad media is. The model breaks only when the
drive can't remap anymore, because its spares are used up or because
its firmware won't do it.
So the missing piece is exactly a device that behaves like that firmware
when the real firmware doesn't. Put stock btrfs on top of it and nothing
in btrfs has to change. It also works for every other filesystem, swap,
and raw users, which an in-btrfs list can't do.
The patch shows how hard it is to do this inside the filesystem. I had
a LLM read it against v6.12 (I haven't run it, because it has problems
too severe to be worth testing), and the mirror redirect, read fast-fail
and persistence all have problems that come from btrfs trying to remap
underneath itself. The LLM found:
- The write redirect in btrfs_map_block() tests the bad list against
*length, which at that point is the distance to the end of the
chunk. For RAID1/RAID1C3/RAID1C4/DUP, one confirmed sector diverts
every write below it in the chunk. The read check uses the real
bio length, so the diverted copy keeps old data that reads can
still reach. On nodatasum files that's stale data returned
without an error. The filesystem still believes those blocks are
mirrored.
- btrfs_map_repair_block() goes through the same redirect, so repair
writes land on the healthy copy, and the "skip repair write" checks
never see the bad device.
- Fast-fail returns true after a bio split, so the remainder of the
bbio is never submitted and the reader hangs. Every fast-fail also
increments read_io_errs without doing any I/O.
- Device replace reuses the devid, so the new disk inherits the old
disk's bad ranges at the next mount. The incompat bit is cleared
when one device's list empties, even if other devices still have
ranges.
- The existing test doesn't detect the bugs. It only writes at the
bad offset, and it compares the file right after sync, so it reads
the page cache rather than either disk copy.
- There are inaccuracies on free space tracking for metadata (with
remapped blocks removed) that can cause a filesystem abort.
- The LLM found more issues, but I'll stop here.
While these problems could be fixed, none of these problems exist if
the remapping happens below the filesystem and supports existing btrfs
write error handling.
Currently only CMR spinning drives support badblocks-style LBA-based
remap lists. Other storage types don't have stable LBA-to-physical
mappings, and a badblock-style remap list doesn't work on these devices.
Users with healthy CMR drives that have not exhausted their remap sectors
also gain nothing from this patch. By doing the sector remapping in
the dm layer, the runtime cost is paid only by those who use the layer.
What I'd want from a dm-quarantine kernel target:
The target detects failures on the lower device itself, and keeps the
remap table on the device. There are two kinds of failure to handle,
because a drive with no spares left often accepts the write and fails
on the read:
- A write fails on the lower device: allocate a reserve block, write
the data there, add the mapping, and complete the bio successfully.
- A read fails on the lower device: mark the block pending and return
EIO. The next write to a pending block goes straight to a new
reserve block. A later successful read of a pending block removes
it from the pending list, since the error was never confirmed.
That's the same thing drives do with their pending sector list, and with
btrfs on top, the next write is the repair write from the good copy.
Because a block is only remapped when it is written, the reserve copy
always holds data the filesystem wrote. That avoids the problem of
remapping an in-use sector to a reserve block full of garbage, and
nothing needs to be salvaged first.
The remap unit should be the lower device's logical block size, so
there's never a read-modify-write through a bad sector.
The table has to be crash-consistent with the data. A remapped write must
not be reported durable before its mapping is. Committing table changes
on FLUSH/FUA, the way dm-thin and dm-cache do with dm-persistent-data,
gives the right semantics. Keep two copies of the table so that a bad
sector under it isn't fatal.
Reserve allocations never move. New defects should take the next
free reserve block, and existing mappings stay put. The current dmq
build_table() doesn't do this. It assigns reserve offsets in sorted
order every time the table is rebuilt, and the map file doesn't record
them, so adding a bad range below an existing one silently points every
later remap at different reserve sectors. That needs fixing even in
the userspace-only dm-linear version.
The target's header goes at the start of the lower device, with the
reserved sectors at the end, making the upper device shorter than
the lower device by the size of the reserved area. The filesystem's
superblock locations are remapped like bad sectors, with the superblocks
copied to the upper device, and zeroed out on the lower device. Then udev
and btrfs device scan find the filesystem only on the dm device and never
through the raw device, so they won't bypass the remap. The current dmq
doesn't hide the lower device's superblocks. The same trick converts
an existing filesystem in place, as discussed in the GitHub issue.
Modern drives ship with tens of thousands of spare sectors. Anyone who
wants to keep using a drive whose own remap table is full has already
grown more defects than that, so a reasonable default reserve size is
about 256 MiB.
When the remap table is full, failing the write with EIO is the only
option. That's what drive firmware does too. It's not harmless on
btrfs: when a write error hits an ordered extent and no write to a
mirror or parity stripe succeeded, the ordered extent is silently
deleted, and data that write(2) already reported as written is lost.
The difference from firmware is that a kernel module can add more
reserve. Shrink the upper device and take more space from the top end
of the lower device, so the reserve area grows downward. The target
should raise an event when free reserve drops below a threshold, like
dm-thin's low_water_mark, so userspace can shrink the filesystem and
extend the reserve before the table fills.
The table should be a log that userspace can read and act on, not just
a private mapping. Every remap, pending entry and failure is recorded,
and the admin can intervene: inspect the entries, clear a pending
block, force a remap ahead of time from a badblocks scan, or retire a
remap. Counters for remapped, pending and failed blocks cover the
summary.
btrfs scrub does need a different approach to error reporting, but
not the one in the patch. Scrub can't write errors to the device
tree on a read-only filesystem, especially on a filesystem that goes
read-only because of its write errors. dmesg isn't a substitute either:
it's rate-limited, so a scratch on the disk yields only a fraction
of the affected files per scrub run. I outlined another method in
btrfs-progs#836 [1]: btrfs collects error events (physical address, error
type, repair result, block type) in ring buffers that userspace fetches
through a sysfs file, with blocking reads so a daemon can listen all
the time. Userspace resolves the addresses to files, and can feed the
same events into the dm target's bad block remapping table. Nothing is
written to the filesystem, so it works on a read-only mount, and it
catches errors outside of scrub too. If the buffer overruns, the reader
should be told how many events were lost, so it knows to scrub again.
dm-quarantine as posted is mostly dm-linear plus dm-error with runtime
messages, and the messages are a problem. dmq reloads it as suspend,
load an empty table, resume, then send the ranges, so for a moment a
mounted filesystem sees no remaps. Messages are fine as the
intervention interface, as long as they change the on-device log
rather than a table that only exists until the next reload.
So my suggestion is to drop the btrfs patch, fix at least the two
dmq issues above for the dm-linear version, and put the effort into a
remapping target with an on-device table.
[1] https://github.com/kdave/btrfs-progs/issues/836
^ permalink raw reply [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-11-15 10:35 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-12 13:47 [RFC PATCH 0/1] btrfs: hybrid physical bad-block handling Alexey Kuznetsov
2026-11-12 13:47 ` [RFC PATCH 1/1] btrfs: track physical bad extents, fast-fail, persist, and relocate Alexey Kuznetsov
2026-11-15 10:35 ` Zygo Blaxell
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox