From 12aeb0dc6405edf24056eae4d01202d696c78dca Mon Sep 17 00:00:00 2001 From: Guanghui Yang <3497809730@qq.com> Date: Sat, 8 Aug 2026 14:13:55 +0800 Subject: [PATCH 01/92] btrfs: free unlinked replace target on initialization failure btrfs_init_dev_replace_tgtdev() allocates the replacement target before looking up its dev_t and initializing its zoned device information. If either lookup_bdev() or btrfs_get_dev_zone_info() fails, the device has not been linked into fs_devices->devices yet, but the error path only drops the block device file reference. Free the allocated device on this error path to release its name, allocation state, zone info, and the device itself. The issue was found by a failure-path metadata residual analyzer and verified with targeted failure injection on v6.14. Assisted-by: Codex:gpt-5 Reviewed-by: Qu Wenruo Signed-off-by: Guanghui Yang <3497809730@qq.com> Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/dev-replace.c | 10 +++++++--- fs/btrfs/volumes.c | 2 +- fs/btrfs/volumes.h | 1 + 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/fs/btrfs/dev-replace.c b/fs/btrfs/dev-replace.c index 0284be0e4e82..cdfe093e5c5c 100644 --- a/fs/btrfs/dev-replace.c +++ b/fs/btrfs/dev-replace.c @@ -235,7 +235,8 @@ static int btrfs_init_dev_replace_tgtdev(struct btrfs_fs_info *fs_info, struct btrfs_device **device_out) { struct btrfs_fs_devices *fs_devices = fs_info->fs_devices; - struct btrfs_device *device; + struct btrfs_device *device = NULL; + struct btrfs_device *tmp_device; struct file *bdev_file; struct block_device *bdev; u64 devid = BTRFS_DEV_REPLACE_DEVID; @@ -264,8 +265,8 @@ static int btrfs_init_dev_replace_tgtdev(struct btrfs_fs_info *fs_info, sync_blockdev(bdev); - list_for_each_entry(device, &fs_devices->devices, dev_list) { - if (device->bdev == bdev) { + list_for_each_entry(tmp_device, &fs_devices->devices, dev_list) { + if (tmp_device->bdev == bdev) { btrfs_err(fs_info, "target device is in the filesystem!"); ret = -EEXIST; @@ -285,6 +286,7 @@ static int btrfs_init_dev_replace_tgtdev(struct btrfs_fs_info *fs_info, device = btrfs_alloc_device(NULL, &devid, NULL, device_path); if (IS_ERR(device)) { ret = PTR_ERR(device); + device = NULL; goto error; } @@ -328,6 +330,8 @@ static int btrfs_init_dev_replace_tgtdev(struct btrfs_fs_info *fs_info, error: /* Undo the open-time freeze deny. */ + if (device) + btrfs_free_device(device); btrfs_release_device_allow_freeze(bdev_file); return ret; } diff --git a/fs/btrfs/volumes.c b/fs/btrfs/volumes.c index 85ea9c5d4536..95cfb2664a78 100644 --- a/fs/btrfs/volumes.c +++ b/fs/btrfs/volumes.c @@ -403,7 +403,7 @@ static struct btrfs_fs_devices *alloc_fs_devices(const u8 *fsid) return fs_devs; } -static void btrfs_free_device(struct btrfs_device *device) +void btrfs_free_device(struct btrfs_device *device) { WARN_ON(!list_empty(&device->post_commit_list)); /* diff --git a/fs/btrfs/volumes.h b/fs/btrfs/volumes.h index 0415d74cad9b..337d7007d9e2 100644 --- a/fs/btrfs/volumes.h +++ b/fs/btrfs/volumes.h @@ -799,6 +799,7 @@ void btrfs_rm_dev_replace_remove_srcdev(struct btrfs_device *srcdev); void btrfs_rm_dev_replace_free_srcdev(struct btrfs_device *srcdev); void btrfs_destroy_dev_replace_tgtdev(struct btrfs_device *tgtdev, bool allow_freeze); +void btrfs_free_device(struct btrfs_device *device); unsigned long btrfs_full_stripe_len(struct btrfs_fs_info *fs_info, u64 logical); u64 btrfs_calc_stripe_length(const struct btrfs_chunk_map *map); From f5166f53b60b4d1ee9957c0baccd3b09735001d4 Mon Sep 17 00:00:00 2001 From: Guanghui Yang <3497809730@qq.com> Date: Tue, 11 Aug 2026 09:02:27 +0930 Subject: [PATCH 02/92] btrfs: roll back sprout setup after device add failure btrfs_init_new_device() calls btrfs_setup_sprout() before creating the first writable chunks for a seed filesystem. That moves the seed devices out of fs_info->fs_devices, clears the seeding state and installs a new fsid for the sprout filesystem. If a later step fails, the error path removes the new device but leaves fs_info->fs_devices in the partially initialized sprout state. The mounted filesystem can then be left with no open devices after the failed device add. Add the inverse of btrfs_setup_sprout() and use it from the error path so the mounted seed filesystem is restored before the temporary seed_devices copy is released. Fixes: 2b82032c34ec ("Btrfs: Seed device support") Assisted-by: Codex:gpt-5 Reviewed-by: Qu Wenruo Signed-off-by: Guanghui Yang <3497809730@qq.com> [ Fix a conflict with per-profile available space, revert sprout before updating per-profile available space estimation. ] Signed-off-by: Qu Wenruo Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/volumes.c | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/fs/btrfs/volumes.c b/fs/btrfs/volumes.c index 95cfb2664a78..1fad1aa716de 100644 --- a/fs/btrfs/volumes.c +++ b/fs/btrfs/volumes.c @@ -2814,6 +2814,41 @@ static void btrfs_setup_sprout(struct btrfs_fs_info *fs_info, btrfs_set_super_flags(disk_super, super_flags); } +static void btrfs_rollback_sprout(struct btrfs_fs_info *fs_info, + struct btrfs_fs_devices *seed_devices) +{ + struct btrfs_fs_devices *fs_devices = fs_info->fs_devices; + struct btrfs_super_block *disk_super = fs_info->super_copy; + struct btrfs_device *device; + u64 super_flags; + + lockdep_assert_held(&uuid_mutex); + lockdep_assert_held(&fs_devices->device_list_mutex); + + list_del_init(&seed_devices->seed_list); + list_splice_init_rcu(&seed_devices->devices, &fs_devices->devices, synchronize_rcu); + list_for_each_entry(device, &fs_devices->devices, dev_list) { + device->fs_devices = fs_devices; + } + + fs_devices->seeding = true; + fs_devices->num_devices = seed_devices->num_devices; + fs_devices->open_devices = seed_devices->open_devices; + fs_devices->missing_devices = seed_devices->missing_devices; + fs_devices->rotating = seed_devices->rotating; + fs_devices->latest_dev = seed_devices->latest_dev; + + memcpy(fs_devices->fsid, seed_devices->fsid, BTRFS_FSID_SIZE); + memcpy(fs_devices->metadata_uuid, seed_devices->metadata_uuid, BTRFS_FSID_SIZE); + memcpy(disk_super->fsid, seed_devices->fsid, BTRFS_FSID_SIZE); + + super_flags = (btrfs_super_flags(disk_super) | BTRFS_SUPER_FLAG_SEEDING); + btrfs_set_super_flags(disk_super, super_flags); + + seed_devices->opened = 0; + free_fs_devices(seed_devices); +} + /* * Store the expected generation for seed devices in device items. */ @@ -3165,6 +3200,8 @@ int btrfs_init_new_device(struct btrfs_fs_info *fs_info, const char *device_path orig_super_total_bytes); btrfs_set_super_num_devices(fs_info->super_copy, orig_super_num_devices); + if (seeding_dev) + btrfs_rollback_sprout(fs_info, seed_devices); btrfs_update_per_profile_avail(fs_info); mutex_unlock(&fs_info->chunk_mutex); mutex_unlock(&fs_info->fs_devices->device_list_mutex); From fdf56b9d03d475881d73b75afbdbe4058db04ffc Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Mon, 17 Aug 2026 17:00:43 +0930 Subject: [PATCH 03/92] btrfs: refactor read_key_bytes() to remove the dest_folio parameter The function read_key_bytes() have 3 call sites: - For BTRFS_VERITY_DESC_ITEM_KEY offset 0 inside btrfs_get_verity_descriptor() - For BTRFS_VERITY_DESC_ITEM_KEY offset 1 inside btrfs_get_verity_descriptor() Those are to read the description items, which are pretty small with fixed item size. Those call sites do not utilize the @dest_folio parameter. - For btrfs_read_merkle_tree_page() This is to read the BTRFS_VERITY_MERKLE_ITEM_KEY, which can be pretty large and split into multiple items. This is the only call site utilizing the @dest_folio parameter. Just for the only btrfs_read_merkle_tree_page() call site, we have a complex scheme for @dest and @dest_folio parameters. Since @dest can be NULL, it means if we pass @dest as NULL, then no matter if @dest_folio is provided, the merkle data will not be loaded into that @dest_folio. This can lead to a bug where a highmem folio is not mapped, then we pass folio_address(folio), which is NULL, into read_key_bytes(), causing no data to be written into @dest_folio. To address the complex scheme between @dest and @dest_folio, remove the @dest_folio parameter completely, and let the only caller to map the folio and pass the mapped kernel address into read_key_bytes() instead. This not only reduces the parameter list, but also make it much clear on the @dest parameter handling. The only downside is a longer duration of locally mapped page, but this should still be fine, as kmap_local_folio() can survive context switch. Reported-by: Hongling Zeng Link: https://lore.kernel.org/linux-btrfs/20260817022012.19658-1-zenghongling@kylinos.cn/ Fixes: 146054090b08 ("btrfs: initial fsverity support") Reviewed-by: David Sterba Signed-off-by: Qu Wenruo Signed-off-by: David Sterba --- fs/btrfs/verity.c | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/fs/btrfs/verity.c b/fs/btrfs/verity.c index 600337a84fbe..83dc7dee14cf 100644 --- a/fs/btrfs/verity.c +++ b/fs/btrfs/verity.c @@ -286,21 +286,17 @@ static int write_key_bytes(struct btrfs_inode *inode, u8 key_type, u64 offset, * @dest: Buffer to read into. This parameter has slightly tricky * semantics. If it is NULL, the function will not do any copying * and will just return the size of all the items up to len bytes. - * If dest_page is passed, then the function will kmap_local the - * page and ignore dest, but it must still be non-NULL to avoid the - * counting-only behavior. * @len: length in bytes to read - * @dest_folio: copy into this folio instead of the dest buffer * * Helper function to read items from the btree. This returns the number of * bytes read or < 0 for errors. We can return short reads if the items don't * exist on disk or aren't big enough to fill the desired length. Supports - * reading into a provided buffer (dest) or into the page cache + * reading into a provided buffer (dest). * * Returns number of bytes read or a negative error code on failure. */ static int read_key_bytes(struct btrfs_inode *inode, u8 key_type, u64 offset, - char *dest, u64 len, struct folio *dest_folio) + char *dest, u64 len) { BTRFS_PATH_AUTO_FREE(path); struct btrfs_root *root = inode->root; @@ -320,7 +316,11 @@ static int read_key_bytes(struct btrfs_inode *inode, u8 key_type, u64 offset, if (!path) return -ENOMEM; - if (dest_folio) + /* + * Merkle items can be large and split across multiple items, so enable + * readahead for such cases. + */ + if (key_type == BTRFS_VERITY_MERKLE_ITEM_KEY) path->reada = READA_FORWARD; key.objectid = btrfs_ino(inode); @@ -364,7 +364,7 @@ static int read_key_bytes(struct btrfs_inode *inode, u8 key_type, u64 offset, break; } - /* desc = NULL to just sum all the item lengths */ + /* dest == NULL to just sum all the item lengths */ if (!dest) copy_end = item_end; else @@ -377,16 +377,10 @@ static int read_key_bytes(struct btrfs_inode *inode, u8 key_type, u64 offset, copy_offset = offset - key.offset; if (dest) { - if (dest_folio) - kaddr = kmap_local_folio(dest_folio, 0); - data = btrfs_item_ptr(leaf, path->slots[0], void); read_extent_buffer(leaf, kaddr + dest_offset, (unsigned long)data + copy_offset, copy_bytes); - - if (dest_folio) - kunmap_local(kaddr); } offset += copy_bytes; @@ -677,7 +671,7 @@ int btrfs_get_verity_descriptor(struct inode *inode, void *buf, size_t buf_size) memset(&item, 0, sizeof(item)); ret = read_key_bytes(BTRFS_I(inode), BTRFS_VERITY_DESC_ITEM_KEY, 0, - (char *)&item, sizeof(item), NULL); + (char *)&item, sizeof(item)); if (ret < 0) return ret; @@ -694,7 +688,7 @@ int btrfs_get_verity_descriptor(struct inode *inode, void *buf, size_t buf_size) return -ERANGE; ret = read_key_bytes(BTRFS_I(inode), BTRFS_VERITY_DESC_ITEM_KEY, 1, - buf, buf_size, NULL); + buf, buf_size); if (ret < 0) return ret; if (ret != true_size) @@ -720,6 +714,7 @@ static struct page *btrfs_read_merkle_tree_page(struct inode *inode, struct folio *folio; u64 off = (u64)index << PAGE_SHIFT; loff_t merkle_pos = merkle_file_pos(inode); + void *kaddr; int ret; if (merkle_pos < 0) @@ -763,6 +758,7 @@ static struct page *btrfs_read_merkle_tree_page(struct inode *inode, } read_folio: + kaddr = kmap_local_folio(folio, 0); /* * Merkle item keys are indexed from byte 0 in the merkle tree. * They have the form: @@ -770,7 +766,8 @@ static struct page *btrfs_read_merkle_tree_page(struct inode *inode, * [ inode objectid, BTRFS_MERKLE_ITEM_KEY, offset in bytes ] */ ret = read_key_bytes(BTRFS_I(inode), BTRFS_VERITY_MERKLE_ITEM_KEY, off, - folio_address(folio), PAGE_SIZE, folio); + kaddr, PAGE_SIZE); + kunmap_local(kaddr); if (ret < 0) { folio_unlock(folio); folio_put(folio); From 15ed4a938b30fcb72e96187f36e2c4ff0e6e2227 Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Wed, 19 Aug 2026 10:36:16 +0930 Subject: [PATCH 04/92] btrfs: replace btrfs_repair_io_failure() to use bio for page iteration Currently btrfs_repair_io_failure() uses a @paddrs[] array to iterate pages. Such a parameter is required for bs > ps cases, as one fs block crosses several pages. However there is a much simpler and existing way to iterate pages: bio and bvec_iter. This changes btrfs_repair_io_failure() by: - Use a const @bvec_iter pointer to locate where the pages are - Extract file offset/logical from the @bbio - Require no @step parameter Above features allow us to shorten the parameter list. - Rename the function to btrfs_repair_bbio_failure() - Change the caller in btrfs_repair_eb_io_failure() to allocate a bbio Unlike the data read path, we do not have a handy bbio in that case. So we need to allocate one just for btrfs_repair_bbio_failure(). - Change the error reporting in btrfs_repair_bbio_failure() to include root id and use inode number directly Now for btree inode we will report a proper inode number (1). Reviewed-by: Boris Burkov Signed-off-by: Qu Wenruo Signed-off-by: David Sterba --- fs/btrfs/bio.c | 61 ++++++++++++++++++++++++++-------------------- fs/btrfs/bio.h | 5 ++-- fs/btrfs/disk-io.c | 25 +++++++++++++------ 3 files changed, 55 insertions(+), 36 deletions(-) diff --git a/fs/btrfs/bio.c b/fs/btrfs/bio.c index cc0bd03048ba..f8d4c2d55007 100644 --- a/fs/btrfs/bio.c +++ b/fs/btrfs/bio.c @@ -186,7 +186,6 @@ static void btrfs_end_repair_bio(struct btrfs_bio *repair_bbio, */ struct bvec_iter saved_iter = repair_bbio->saved_iter; const u32 step = min(fs_info->sectorsize, PAGE_SIZE); - const u64 logical = repair_bbio->saved_iter.bi_sector << SECTOR_SHIFT; const u32 nr_steps = repair_bbio->saved_iter.bi_size / step; int mirror = repair_bbio->mirror_num; phys_addr_t paddrs[BTRFS_MAX_BLOCKSIZE / PAGE_SIZE]; @@ -220,9 +219,8 @@ static void btrfs_end_repair_bio(struct btrfs_bio *repair_bbio, do { mirror = prev_repair_mirror(fbio, mirror); - btrfs_repair_io_failure(fs_info, btrfs_ino(inode), - repair_bbio->file_offset, fs_info->sectorsize, - logical, paddrs, step, mirror); + btrfs_repair_bbio_failure(repair_bbio, &repair_bbio->saved_iter, + fs_info->sectorsize, mirror); } while (mirror != fbio->bbio->mirror_num); done: @@ -925,21 +923,23 @@ void btrfs_submit_bbio(struct btrfs_bio *bbio, int mirror_num) * The I/O is issued synchronously to block the repair read completion from * freeing the bio. * - * @ino: Offending inode number - * @fileoff: File offset inside the inode + * @bbio: Original bbio where the repair is needed + * @orig_iter: Points to where the repair start is * @length: Length of the repair write - * @logical: Logical address of the range - * @paddrs: Physical address array of the content - * @step: Length of for each paddrs * @mirror_num: Mirror number to write to. Must not be zero */ -int btrfs_repair_io_failure(struct btrfs_fs_info *fs_info, u64 ino, u64 fileoff, - u32 length, u64 logical, const phys_addr_t paddrs[], - unsigned int step, int mirror_num) +int btrfs_repair_bbio_failure(struct btrfs_bio *bbio, const struct bvec_iter *orig_iter, + u32 length, int mirror_num) { - const u32 nr_steps = DIV_ROUND_UP_POW2(length, step); + struct btrfs_inode *inode = bbio->inode; + struct btrfs_fs_info *fs_info = inode->root->fs_info; struct btrfs_io_stripe smap = { 0 }; - struct bio *bio = NULL; + struct bvec_iter iter = *orig_iter; + struct bio *repair_bio = NULL; + const u64 logical = iter.bi_sector << SECTOR_SHIFT; + const u64 fileoff = bbio->file_offset + + ((iter.bi_sector - bbio->saved_iter.bi_sector) << SECTOR_SHIFT); + u32 cur = 0; int ret = 0; BUG_ON(!mirror_num); @@ -950,8 +950,9 @@ int btrfs_repair_io_failure(struct btrfs_fs_info *fs_info, u64 ino, u64 fileoff, ASSERT(IS_ALIGNED(fileoff, fs_info->sectorsize)); /* Either it's a single data or metadata block. */ ASSERT(length <= BTRFS_MAX_BLOCKSIZE); - ASSERT(step <= length); - ASSERT(is_power_of_2(step)); + + /* Our current iter should not be before the original bbio saved_iter. */ + ASSERT(iter.bi_sector >= bbio->saved_iter.bi_sector); /* * The fs either mounted RO or hit critical errors, no need @@ -979,15 +980,22 @@ int btrfs_repair_io_failure(struct btrfs_fs_info *fs_info, u64 ino, u64 fileoff, goto out_counter_dec; } - bio = bio_alloc(smap.dev->bdev, nr_steps, REQ_OP_WRITE | REQ_SYNC, GFP_NOFS); - bio->bi_iter.bi_sector = smap.physical >> SECTOR_SHIFT; - for (int i = 0; i < nr_steps; i++) { - ret = bio_add_page(bio, phys_to_page(paddrs[i]), step, offset_in_page(paddrs[i])); - /* We should have allocated enough slots to contain all the different pages. */ - ASSERT(ret == step); + repair_bio = bio_alloc(smap.dev->bdev, max(1, length >> PAGE_SHIFT), + REQ_OP_WRITE | REQ_SYNC, GFP_NOFS); + repair_bio->bi_iter.bi_sector = smap.physical >> SECTOR_SHIFT; + while (cur < length) { + struct page *page = bio_iter_page(&bbio->bio, iter); + const u32 pg_off = bio_iter_offset(&bbio->bio, iter); + const u32 cur_len = min(bio_iter_len(&bbio->bio, iter), length - cur); + + ret = bio_add_page(repair_bio, page, cur_len, pg_off); + ASSERT(ret == cur_len); + bio_advance_iter_single(&bbio->bio, &iter, cur_len); + cur += cur_len; } - ret = submit_bio_wait(bio); - bio_put(bio); + + ret = submit_bio_wait(repair_bio); + bio_put(repair_bio); if (ret) { /* try to remap that extent elsewhere? */ btrfs_dev_stat_inc_and_print(smap.dev, BTRFS_DEV_STAT_WRITE_ERRS); @@ -995,8 +1003,9 @@ int btrfs_repair_io_failure(struct btrfs_fs_info *fs_info, u64 ino, u64 fileoff, } btrfs_info_rl(fs_info, - "read error corrected: ino %llu off %llu (dev %s sector %llu)", - ino, fileoff, btrfs_dev_name(smap.dev), + "read error corrected: root %llu ino %llu off %llu (dev %s sector %llu)", + btrfs_root_id(inode->root), btrfs_ino(inode), fileoff, + btrfs_dev_name(smap.dev), smap.physical >> SECTOR_SHIFT); ret = 0; diff --git a/fs/btrfs/bio.h b/fs/btrfs/bio.h index 303ed6c7103d..b7bd377a0162 100644 --- a/fs/btrfs/bio.h +++ b/fs/btrfs/bio.h @@ -126,8 +126,7 @@ void btrfs_bio_end_io(struct btrfs_bio *bbio, blk_status_t status); void btrfs_submit_bbio(struct btrfs_bio *bbio, int mirror_num); void btrfs_submit_repair_write(struct btrfs_bio *bbio, int mirror_num, bool dev_replace); -int btrfs_repair_io_failure(struct btrfs_fs_info *fs_info, u64 ino, u64 fileoff, - u32 length, u64 logical, const phys_addr_t paddrs[], - unsigned int step, int mirror_num); +int btrfs_repair_bbio_failure(struct btrfs_bio *bbio, const struct bvec_iter *orig_iter, + u32 length, int mirror_num); #endif diff --git a/fs/btrfs/disk-io.c b/fs/btrfs/disk-io.c index dc7ad92876c0..0f3e8e74b768 100644 --- a/fs/btrfs/disk-io.c +++ b/fs/btrfs/disk-io.c @@ -176,19 +176,24 @@ static int btrfs_repair_eb_io_failure(const struct extent_buffer *eb, int mirror_num) { struct btrfs_fs_info *fs_info = eb->fs_info; - const u32 step = min(fs_info->nodesize, PAGE_SIZE); - const u32 nr_steps = eb->len / step; - phys_addr_t paddrs[BTRFS_MAX_BLOCKSIZE / PAGE_SIZE]; + struct btrfs_bio *bbio; + int ret; if (sb_rdonly(fs_info->sb)) return -EROFS; + /* + * This bbio is only to queue all pages for btrfs_repair_bbio_failure(). + * Thus it will never get its endio called. + */ + bbio = btrfs_bio_alloc(max(1, fs_info->nodesize >> PAGE_SHIFT), REQ_OP_READ, + BTRFS_I(fs_info->btree_inode), eb->start, NULL, NULL); + bbio->bio.bi_iter.bi_sector = eb->start >> SECTOR_SHIFT; for (int i = 0; i < num_extent_pages(eb); i++) { struct folio *folio = eb->folios[i]; /* No large folio support yet. */ ASSERT(folio_order(folio) == 0); - ASSERT(i < nr_steps); /* * For nodesize < page size, there is just one paddr, with some @@ -197,11 +202,17 @@ static int btrfs_repair_eb_io_failure(const struct extent_buffer *eb, * For nodesize >= page size, it's one or more paddrs, and eb->start * must be aligned to page boundary. */ - paddrs[i] = page_to_phys(&folio->page) + offset_in_page(eb->start); + ret = bio_add_page(&bbio->bio, &folio->page, min(PAGE_SIZE, fs_info->nodesize), + offset_in_page(eb->start)); + ASSERT(ret == min(PAGE_SIZE, fs_info->nodesize)); } + /* Since the bbio is never submitted, we have to save the iter manually. */ + bbio->saved_iter = bbio->bio.bi_iter; - return btrfs_repair_io_failure(fs_info, 0, eb->start, eb->len, - eb->start, paddrs, step, mirror_num); + ret = btrfs_repair_bbio_failure(bbio, &bbio->saved_iter, fs_info->nodesize, + mirror_num); + bio_put(&bbio->bio); + return ret; } /* From 3e4a083bf0c8bb727517ae781052b80167443077 Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Wed, 19 Aug 2026 10:36:17 +0930 Subject: [PATCH 05/92] btrfs: enhance btrfs_data_csum_ok() to use bio for page iteration Currently btrfs_data_csum_ok() requires a @paddr[] array to iterate all possible pages for bs > ps cases. However for all btrfs_data_csum_ok() call sites, we already have a btrfs_bio, and the bio infrastructure has many flexible ways to iterate multiple pages already. Change btrfs_data_csum_ok() to make full use of btrfs_bio by: - Change the parameter list to require a @bvec_iter pointer And remove @bio_offset, which can be calculated through @bvec_iter and bbio->saved_iter. Also remove paddrs[], we will iterate all the pages using bio interfaces. - Make the same parameter changes to repair_one_sector() - Use bio interfaces to iterate pages from a bio - Rename the function to btrfs_bio_data_csum_ok() - Remove on-stack paddrs[] array usage Reviewed-by: Boris Burkov Signed-off-by: Qu Wenruo Signed-off-by: David Sterba --- fs/btrfs/bio.c | 82 +++++++++++++++--------------------------- fs/btrfs/btrfs_inode.h | 4 +-- fs/btrfs/inode.c | 49 +++++++++++++++++++------ 3 files changed, 70 insertions(+), 65 deletions(-) diff --git a/fs/btrfs/bio.c b/fs/btrfs/bio.c index f8d4c2d55007..19b4855969f5 100644 --- a/fs/btrfs/bio.c +++ b/fs/btrfs/bio.c @@ -180,29 +180,13 @@ static void btrfs_end_repair_bio(struct btrfs_bio *repair_bbio, struct btrfs_failed_bio *fbio = repair_bbio->private; struct btrfs_inode *inode = repair_bbio->inode; struct btrfs_fs_info *fs_info = inode->root->fs_info; - /* - * We can not move forward the saved_iter, as it will be later - * utilized by repair_bbio again. - */ - struct bvec_iter saved_iter = repair_bbio->saved_iter; - const u32 step = min(fs_info->sectorsize, PAGE_SIZE); - const u32 nr_steps = repair_bbio->saved_iter.bi_size / step; int mirror = repair_bbio->mirror_num; - phys_addr_t paddrs[BTRFS_MAX_BLOCKSIZE / PAGE_SIZE]; - phys_addr_t paddr; - unsigned int slot = 0; - /* Repair bbio should be eaxctly one block sized. */ + /* Repair bbio should be exactly one block sized. */ ASSERT(repair_bbio->saved_iter.bi_size == fs_info->sectorsize); - btrfs_bio_for_each_block(paddr, &repair_bbio->bio, &saved_iter, step) { - ASSERT(slot < nr_steps); - paddrs[slot] = paddr; - slot++; - } - if (repair_bbio->bio.bi_status || - !btrfs_data_csum_ok(repair_bbio, dev, 0, paddrs)) { + !btrfs_bio_data_csum_ok(repair_bbio, &repair_bbio->saved_iter, dev)) { bio_reset(&repair_bbio->bio, NULL, REQ_OP_READ); repair_bbio->bio.bi_iter = repair_bbio->saved_iter; @@ -236,25 +220,21 @@ static void btrfs_end_repair_bio(struct btrfs_bio *repair_bbio, * read succeeded to restore the redundancy. */ static struct btrfs_failed_bio *repair_one_sector(struct btrfs_bio *failed_bbio, - u32 bio_offset, - phys_addr_t paddrs[], + const struct bvec_iter *orig_iter, struct btrfs_failed_bio *fbio) { struct btrfs_inode *inode = failed_bbio->inode; struct btrfs_fs_info *fs_info = inode->root->fs_info; - const u32 sectorsize = fs_info->sectorsize; - const u32 step = min(fs_info->sectorsize, PAGE_SIZE); - const u32 nr_steps = sectorsize / step; - /* - * For bs > ps cases, the saved_iter can be partially moved forward. - * In that case we should round it down to the block boundary. - */ - const u64 logical = round_down(failed_bbio->saved_iter.bi_sector << SECTOR_SHIFT, - sectorsize); struct btrfs_bio *repair_bbio; struct bio *repair_bio; + struct bvec_iter iter = *orig_iter; + const u32 sectorsize = fs_info->sectorsize; + const u32 bio_offset = ((iter.bi_sector - failed_bbio->saved_iter.bi_sector) << + SECTOR_SHIFT); + const u64 logical = (iter.bi_sector << SECTOR_SHIFT); int num_copies; int mirror; + u32 cur = 0; btrfs_debug(fs_info, "repair read error: read error at %llu", failed_bbio->file_offset + bio_offset); @@ -275,17 +255,21 @@ static struct btrfs_failed_bio *repair_one_sector(struct btrfs_bio *failed_bbio, atomic_inc(&fbio->repair_count); - repair_bio = bio_alloc_bioset(NULL, nr_steps, REQ_OP_READ, GFP_NOFS, - &btrfs_repair_bioset); + repair_bio = bio_alloc_bioset(NULL, max(1, sectorsize >> PAGE_SHIFT), + REQ_OP_READ, GFP_NOFS, &btrfs_repair_bioset); repair_bio->bi_iter.bi_sector = logical >> SECTOR_SHIFT; - for (int i = 0; i < nr_steps; i++) { + while (cur < sectorsize) { + struct page *page = bio_iter_page(&failed_bbio->bio, iter); + const u32 pg_off = bio_iter_offset(&failed_bbio->bio, iter); + const u32 cur_len = min(bio_iter_len(&failed_bbio->bio, iter), + sectorsize - cur); int ret; - ASSERT(offset_in_page(paddrs[i]) + step <= PAGE_SIZE); + ret = bio_add_page(repair_bio, page, cur_len, pg_off); + ASSERT(ret == cur_len); - ret = bio_add_page(repair_bio, phys_to_page(paddrs[i]), step, - offset_in_page(paddrs[i])); - ASSERT(ret == step); + bio_advance_iter_single(&failed_bbio->bio, &iter, cur_len); + cur += cur_len; } repair_bbio = btrfs_bio(repair_bio); @@ -303,18 +287,16 @@ static void btrfs_check_read_bio(struct btrfs_bio *bbio, struct btrfs_device *de struct btrfs_inode *inode = bbio->inode; struct btrfs_fs_info *fs_info = inode->root->fs_info; const u32 sectorsize = fs_info->sectorsize; - const u32 step = min(sectorsize, PAGE_SIZE); - const u32 nr_steps = sectorsize / step; - struct bvec_iter *iter = &bbio->saved_iter; + struct bvec_iter iter; blk_status_t status = bbio->bio.bi_status; struct btrfs_failed_bio *fbio = NULL; - phys_addr_t paddrs[BTRFS_MAX_BLOCKSIZE / PAGE_SIZE]; - phys_addr_t paddr; - u32 offset = 0; /* Read-repair requires the inode field to be set by the submitter. */ ASSERT(inode); + /* The original bbio should be sectorsize aligned. */ + ASSERT(IS_ALIGNED(bbio->saved_iter.bi_size, sectorsize)); + /* * Hand off repair bios to the repair code as there is no upper level * submitter for them. @@ -327,16 +309,10 @@ static void btrfs_check_read_bio(struct btrfs_bio *bbio, struct btrfs_device *de /* Clear the I/O error. A failed repair will reset it. */ bbio->bio.bi_status = BLK_STS_OK; - btrfs_bio_for_each_block(paddr, &bbio->bio, iter, step) { - paddrs[(offset / step) % nr_steps] = paddr; - offset += step; - - if (IS_ALIGNED(offset, sectorsize)) { - if (status || - !btrfs_data_csum_ok(bbio, dev, offset - sectorsize, paddrs)) - fbio = repair_one_sector(bbio, offset - sectorsize, - paddrs, fbio); - } + for (iter = bbio->saved_iter; iter.bi_size; + bio_advance_iter(&bbio->bio, &iter, sectorsize)) { + if (status || !btrfs_bio_data_csum_ok(bbio, &iter, dev)) + fbio = repair_one_sector(bbio, &iter, fbio); } if (bbio->csum != bbio->csum_inline) kvfree(bbio->csum); @@ -924,7 +900,7 @@ void btrfs_submit_bbio(struct btrfs_bio *bbio, int mirror_num) * freeing the bio. * * @bbio: Original bbio where the repair is needed - * @orig_iter: Points to where the repair start is + * @orig_iter: Points to where the repair starts * @length: Length of the repair write * @mirror_num: Mirror number to write to. Must not be zero */ diff --git a/fs/btrfs/btrfs_inode.h b/fs/btrfs/btrfs_inode.h index 1082fa92c145..171f96bdb8aa 100644 --- a/fs/btrfs/btrfs_inode.h +++ b/fs/btrfs/btrfs_inode.h @@ -513,8 +513,8 @@ void btrfs_calculate_block_csum_pages(struct btrfs_fs_info *fs_info, const phys_addr_t paddrs[], u8 *dest); int btrfs_check_block_csum(struct btrfs_fs_info *fs_info, phys_addr_t paddr, u8 *csum, const u8 * const csum_expected); -bool btrfs_data_csum_ok(struct btrfs_bio *bbio, struct btrfs_device *dev, - u32 bio_offset, const phys_addr_t paddrs[]); +bool btrfs_bio_data_csum_ok(struct btrfs_bio *bbio, const struct bvec_iter *orig_iter, + struct btrfs_device *dev); noinline int can_nocow_extent(struct btrfs_inode *inode, u64 offset, u64 *len, struct btrfs_file_extent *file_extent, bool nowait); diff --git a/fs/btrfs/inode.c b/fs/btrfs/inode.c index faf6ed7deb18..2a424d4c9fca 100644 --- a/fs/btrfs/inode.c +++ b/fs/btrfs/inode.c @@ -3551,27 +3551,31 @@ int btrfs_check_block_csum(struct btrfs_fs_info *fs_info, phys_addr_t paddr, u8 * different noncontiguous pages. * * @bbio: btrfs_io_bio which contains the csum - * @dev: device the sector is on - * @bio_offset: offset to the beginning of the bio (in bytes) - * @paddrs: physical addresses which back the fs block + * @orig_iter: bvec iter pointing to the start of the block + * @dev: device the sector is on (optional) * * Check if the checksum on a data block is valid. When a checksum mismatch is * detected, report the error and fill the corrupted range with zero. * * Return %true if the sector is ok or had no checksum to start with, else %false. */ -bool btrfs_data_csum_ok(struct btrfs_bio *bbio, struct btrfs_device *dev, - u32 bio_offset, const phys_addr_t paddrs[]) +bool btrfs_bio_data_csum_ok(struct btrfs_bio *bbio, + const struct bvec_iter *orig_iter, + struct btrfs_device *dev) { struct btrfs_inode *inode = bbio->inode; struct btrfs_fs_info *fs_info = inode->root->fs_info; + struct bvec_iter iter = *orig_iter; + struct btrfs_csum_ctx cctx; const u32 blocksize = fs_info->sectorsize; - const u32 step = min(blocksize, PAGE_SIZE); - const u32 nr_steps = blocksize / step; + const u32 bio_offset = (iter.bi_sector - bbio->saved_iter.bi_sector) << SECTOR_SHIFT; u64 file_offset = bbio->file_offset + bio_offset; u64 end = file_offset + blocksize - 1; u8 *csum_expected; u8 csum[BTRFS_CSUM_SIZE]; + u32 cur = 0; + + ASSERT(iter.bi_sector >= bbio->saved_iter.bi_sector); if (!bbio->csum) return true; @@ -3587,7 +3591,22 @@ bool btrfs_data_csum_ok(struct btrfs_bio *bbio, struct btrfs_device *dev, csum_expected = bbio->csum + (bio_offset >> fs_info->sectorsize_bits) * fs_info->csum_size; - btrfs_calculate_block_csum_pages(fs_info, paddrs, csum); + btrfs_csum_init(&cctx, fs_info->csum_type); + while (cur < blocksize) { + struct page *page = bio_iter_page(&bbio->bio, iter); + const u32 pg_off = bio_iter_offset(&bbio->bio, iter); + const u32 cur_len = min(bio_iter_len(&bbio->bio, iter), blocksize - cur); + void *kaddr; + + kaddr = kmap_local_page(page) + pg_off; + btrfs_csum_update(&cctx, kaddr, cur_len); + kunmap_local(kaddr); + + bio_advance_iter_single(&bbio->bio, &iter, cur_len); + cur += cur_len; + } + btrfs_csum_final(&cctx, csum); + if (unlikely(memcmp(csum, csum_expected, fs_info->csum_size) != 0)) goto zeroit; return true; @@ -3597,8 +3616,18 @@ bool btrfs_data_csum_ok(struct btrfs_bio *bbio, struct btrfs_device *dev, bbio->mirror_num); if (dev) btrfs_dev_stat_inc_and_print(dev, BTRFS_DEV_STAT_CORRUPTION_ERRS); - for (int i = 0; i < nr_steps; i++) - memzero_page(phys_to_page(paddrs[i]), offset_in_page(paddrs[i]), step); + cur = 0; + iter = *orig_iter; + while (cur < blocksize) { + struct page *page = bio_iter_page(&bbio->bio, iter); + const u32 pg_off = bio_iter_offset(&bbio->bio, iter); + const u32 cur_len = min(bio_iter_len(&bbio->bio, iter), blocksize - cur); + + memzero_page(page, pg_off, cur_len); + + bio_advance_iter_single(&bbio->bio, &iter, cur_len); + cur += cur_len; + } return false; } From 64978a4eced2b8f769cdfc27d68c6710e9c92627 Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Wed, 19 Aug 2026 10:36:18 +0930 Subject: [PATCH 06/92] btrfs: use a shared helper to calculate data checksum for a bio Since we are already calculating data checksum using bio interface, extract the generation part into btrfs_csum_one_bio_block(), and use that to replace the paddrs[] array based solution in csum_one_bio(). This will reduce 128 bytes on-stack memory usage for csum_one_bio(). Reviewed-by: Boris Burkov Signed-off-by: Qu Wenruo Signed-off-by: David Sterba --- fs/btrfs/btrfs_inode.h | 2 ++ fs/btrfs/file-item.c | 19 +++++------------ fs/btrfs/inode.c | 46 +++++++++++++++++++++++++----------------- 3 files changed, 34 insertions(+), 33 deletions(-) diff --git a/fs/btrfs/btrfs_inode.h b/fs/btrfs/btrfs_inode.h index 171f96bdb8aa..e137a99151bd 100644 --- a/fs/btrfs/btrfs_inode.h +++ b/fs/btrfs/btrfs_inode.h @@ -515,6 +515,8 @@ int btrfs_check_block_csum(struct btrfs_fs_info *fs_info, phys_addr_t paddr, u8 const u8 * const csum_expected); bool btrfs_bio_data_csum_ok(struct btrfs_bio *bbio, const struct bvec_iter *orig_iter, struct btrfs_device *dev); +void btrfs_csum_one_bio_block(struct btrfs_fs_info *fs_info, struct bio *bio, + const struct bvec_iter *orig_iter, u8 *csum); noinline int can_nocow_extent(struct btrfs_inode *inode, u64 offset, u64 *len, struct btrfs_file_extent *file_extent, bool nowait); diff --git a/fs/btrfs/file-item.c b/fs/btrfs/file-item.c index cf50fd623f41..581ca5653be9 100644 --- a/fs/btrfs/file-item.c +++ b/fs/btrfs/file-item.c @@ -801,25 +801,16 @@ static void csum_one_bio(struct btrfs_bio *bbio, struct bvec_iter *src) { struct btrfs_inode *inode = bbio->inode; struct btrfs_fs_info *fs_info = inode->root->fs_info; - struct bio *bio = &bbio->bio; struct btrfs_ordered_sum *sums = bbio->sums; - struct bvec_iter iter = *src; - phys_addr_t paddr; + struct bvec_iter iter; const u32 blocksize = fs_info->sectorsize; - const u32 step = min(blocksize, PAGE_SIZE); - const u32 nr_steps = blocksize / step; - phys_addr_t paddrs[BTRFS_MAX_BLOCKSIZE / PAGE_SIZE]; - u32 offset = 0; int index = 0; - btrfs_bio_for_each_block(paddr, bio, &iter, step) { - paddrs[(offset / step) % nr_steps] = paddr; - offset += step; + for (iter = *src; iter.bi_size; bio_advance_iter(&bbio->bio, &iter, blocksize)) { + btrfs_csum_one_bio_block(fs_info, &bbio->bio, &iter, + sums->sums + index); - if (IS_ALIGNED(offset, blocksize)) { - btrfs_calculate_block_csum_pages(fs_info, paddrs, sums->sums + index); - index += fs_info->csum_size; - } + index += fs_info->csum_size; } } diff --git a/fs/btrfs/inode.c b/fs/btrfs/inode.c index 2a424d4c9fca..e993ea4eb2e4 100644 --- a/fs/btrfs/inode.c +++ b/fs/btrfs/inode.c @@ -3546,6 +3546,32 @@ int btrfs_check_block_csum(struct btrfs_fs_info *fs_info, phys_addr_t paddr, u8 return 0; } +/* Generate data checksum for a single fs block, pointed to by @orig_iter. */ +void btrfs_csum_one_bio_block(struct btrfs_fs_info *fs_info, struct bio *bio, + const struct bvec_iter *orig_iter, u8 *csum) +{ + struct btrfs_csum_ctx cctx; + struct bvec_iter iter = *orig_iter; + const u32 blocksize = fs_info->sectorsize; + u32 cur = 0; + + btrfs_csum_init(&cctx, fs_info->csum_type); + while (cur < blocksize) { + struct page *page = bio_iter_page(bio, iter); + const u32 pg_off = bio_iter_offset(bio, iter); + const u32 cur_len = min(bio_iter_len(bio, iter), blocksize - cur); + void *kaddr; + + kaddr = kmap_local_page(page) + pg_off; + btrfs_csum_update(&cctx, kaddr, cur_len); + kunmap_local(kaddr); + + bio_advance_iter_single(bio, &iter, cur_len); + cur += cur_len; + } + btrfs_csum_final(&cctx, csum); +} + /* * Verify the checksum of a single data sector, which can be scattered at * different noncontiguous pages. @@ -3566,7 +3592,6 @@ bool btrfs_bio_data_csum_ok(struct btrfs_bio *bbio, struct btrfs_inode *inode = bbio->inode; struct btrfs_fs_info *fs_info = inode->root->fs_info; struct bvec_iter iter = *orig_iter; - struct btrfs_csum_ctx cctx; const u32 blocksize = fs_info->sectorsize; const u32 bio_offset = (iter.bi_sector - bbio->saved_iter.bi_sector) << SECTOR_SHIFT; u64 file_offset = bbio->file_offset + bio_offset; @@ -3591,22 +3616,7 @@ bool btrfs_bio_data_csum_ok(struct btrfs_bio *bbio, csum_expected = bbio->csum + (bio_offset >> fs_info->sectorsize_bits) * fs_info->csum_size; - btrfs_csum_init(&cctx, fs_info->csum_type); - while (cur < blocksize) { - struct page *page = bio_iter_page(&bbio->bio, iter); - const u32 pg_off = bio_iter_offset(&bbio->bio, iter); - const u32 cur_len = min(bio_iter_len(&bbio->bio, iter), blocksize - cur); - void *kaddr; - - kaddr = kmap_local_page(page) + pg_off; - btrfs_csum_update(&cctx, kaddr, cur_len); - kunmap_local(kaddr); - - bio_advance_iter_single(&bbio->bio, &iter, cur_len); - cur += cur_len; - } - btrfs_csum_final(&cctx, csum); - + btrfs_csum_one_bio_block(fs_info, &bbio->bio, orig_iter, csum); if (unlikely(memcmp(csum, csum_expected, fs_info->csum_size) != 0)) goto zeroit; return true; @@ -3616,8 +3626,6 @@ bool btrfs_bio_data_csum_ok(struct btrfs_bio *bbio, bbio->mirror_num); if (dev) btrfs_dev_stat_inc_and_print(dev, BTRFS_DEV_STAT_CORRUPTION_ERRS); - cur = 0; - iter = *orig_iter; while (cur < blocksize) { struct page *page = bio_iter_page(&bbio->bio, iter); const u32 pg_off = bio_iter_offset(&bbio->bio, iter); From f41f4dcddea1b9eb41fc3a5491b2a8dd1f7c21ff Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Wed, 19 Aug 2026 10:36:19 +0930 Subject: [PATCH 07/92] btrfs: remove on-stack paddrs[] array usage Since the bs > ps support, we have to handle cases where a data block is inside several discontiguous pages. Thus we need a local paddrs[] array to assemble a data block for bs > ps cases. However to handle all possible bs/ps combinations, we have to declare such array using the max block size vs page size, no matter the current block size and page size. This adds 128 bytes on-stack memory usage for several call sites, and also introduced several duplicated helpers to calculate checksum for a data block: - btrfs_calculate_block_csum_folio() - btrfs_calculate_block_csum_pages() - btrfs_check_block_csum() The differences are mostly in how the data is passed. The first one accepts a contiguous paddr range. The second one accepts an array of paddrs[]. The last one is just a simple wrapper of the first one. However the most common interface to iterate a data block is through bio, and we have already converted most callers to use the bio based interface, e.g. btrfs_bio_data_csum_ok() and btrfs_csum_one_bio_block(). Convert the remaining two call sites to address the remaining paddrs[] usage: - btrfs_calculate_block_csum_pages() inside verify_bio_data_sectors() This can be switched to btrfs_csum_one_bio_block(). This removes the 128 bytes on-stack memory usage. - btrfs_calculate_block_csum_pages() inside verify_one_sector() This call site doesn't use on-stack memory for paddrs[], but reuses the existing btrfs_raid_bio::bio_paddrs[] or btrfs_raid_bio::stripe_paddrs[]. So implement a local version called calculate_block_csum_paddrs(). Now there is no fixed on-stack paddrs[] usage anymore. Reviewed-by: Boris Burkov Signed-off-by: Qu Wenruo Signed-off-by: David Sterba --- fs/btrfs/btrfs_inode.h | 6 ---- fs/btrfs/inode.c | 75 ------------------------------------------ fs/btrfs/raid56.c | 46 +++++++++++++++----------- 3 files changed, 27 insertions(+), 100 deletions(-) diff --git a/fs/btrfs/btrfs_inode.h b/fs/btrfs/btrfs_inode.h index e137a99151bd..89e5e9c0c904 100644 --- a/fs/btrfs/btrfs_inode.h +++ b/fs/btrfs/btrfs_inode.h @@ -507,12 +507,6 @@ static inline void btrfs_set_inode_mapping_order(struct btrfs_inode *inode) inode->root->fs_info->block_max_order); } -void btrfs_calculate_block_csum_folio(struct btrfs_fs_info *fs_info, - const phys_addr_t paddr, u8 *dest); -void btrfs_calculate_block_csum_pages(struct btrfs_fs_info *fs_info, - const phys_addr_t paddrs[], u8 *dest); -int btrfs_check_block_csum(struct btrfs_fs_info *fs_info, phys_addr_t paddr, u8 *csum, - const u8 * const csum_expected); bool btrfs_bio_data_csum_ok(struct btrfs_bio *bbio, const struct bvec_iter *orig_iter, struct btrfs_device *dev); void btrfs_csum_one_bio_block(struct btrfs_fs_info *fs_info, struct bio *bio, diff --git a/fs/btrfs/inode.c b/fs/btrfs/inode.c index e993ea4eb2e4..54005045e123 100644 --- a/fs/btrfs/inode.c +++ b/fs/btrfs/inode.c @@ -3471,81 +3471,6 @@ int btrfs_finish_ordered_io(struct btrfs_ordered_extent *ordered) return btrfs_finish_one_ordered(ordered); } -/* - * Calculate the checksum of an fs block at physical memory address @paddr, - * and save the result to @dest. - * - * The folio containing @paddr must be large enough to contain a full fs block. - */ -void btrfs_calculate_block_csum_folio(struct btrfs_fs_info *fs_info, - const phys_addr_t paddr, u8 *dest) -{ - struct folio *folio = page_folio(phys_to_page(paddr)); - const u32 blocksize = fs_info->sectorsize; - const u32 step = min(blocksize, PAGE_SIZE); - const u32 nr_steps = blocksize / step; - phys_addr_t paddrs[BTRFS_MAX_BLOCKSIZE / PAGE_SIZE]; - - /* The full block must be inside the folio. */ - ASSERT(offset_in_folio(folio, paddr) + blocksize <= folio_size(folio)); - - for (int i = 0; i < nr_steps; i++) { - u32 pindex = offset_in_folio(folio, paddr + i * step) >> PAGE_SHIFT; - - /* - * For bs <= ps cases, we will only run the loop once, so the offset - * inside the page will only added to paddrs[0]. - * - * For bs > ps cases, the block must be page aligned, thus offset - * inside the page will always be 0. - */ - paddrs[i] = page_to_phys(folio_page(folio, pindex)) + offset_in_page(paddr); - } - return btrfs_calculate_block_csum_pages(fs_info, paddrs, dest); -} - -/* - * Calculate the checksum of a fs block backed by multiple noncontiguous pages - * at @paddrs[] and save the result to @dest. - * - * The folio containing @paddr must be large enough to contain a full fs block. - */ -void btrfs_calculate_block_csum_pages(struct btrfs_fs_info *fs_info, - const phys_addr_t paddrs[], u8 *dest) -{ - const u32 blocksize = fs_info->sectorsize; - const u32 step = min(blocksize, PAGE_SIZE); - const u32 nr_steps = blocksize / step; - struct btrfs_csum_ctx csum; - - btrfs_csum_init(&csum, fs_info->csum_type); - for (int i = 0; i < nr_steps; i++) { - const phys_addr_t paddr = paddrs[i]; - void *kaddr; - - ASSERT(offset_in_page(paddr) + step <= PAGE_SIZE); - kaddr = kmap_local_page(phys_to_page(paddr)) + offset_in_page(paddr); - btrfs_csum_update(&csum, kaddr, step); - kunmap_local(kaddr); - } - btrfs_csum_final(&csum, dest); -} - -/* - * Verify the checksum for a single sector without any extra action that depend - * on the type of I/O. - * - * @kaddr must be a properly kmapped address. - */ -int btrfs_check_block_csum(struct btrfs_fs_info *fs_info, phys_addr_t paddr, u8 *csum, - const u8 * const csum_expected) -{ - btrfs_calculate_block_csum_folio(fs_info, paddr, csum); - if (unlikely(memcmp(csum, csum_expected, fs_info->csum_size) != 0)) - return -EIO; - return 0; -} - /* Generate data checksum for a single fs block, pointed to by @orig_iter. */ void btrfs_csum_one_bio_block(struct btrfs_fs_info *fs_info, struct bio *bio, const struct bvec_iter *orig_iter, u8 *csum) diff --git a/fs/btrfs/raid56.c b/fs/btrfs/raid56.c index 1ee52a9dcee3..a5d0ef09d92a 100644 --- a/fs/btrfs/raid56.c +++ b/fs/btrfs/raid56.c @@ -1652,12 +1652,7 @@ static void verify_bio_data_sectors(struct btrfs_raid_bio *rbio, struct bio *bio) { struct btrfs_fs_info *fs_info = rbio->bioc->fs_info; - const u32 step = min(fs_info->sectorsize, PAGE_SIZE); - const u32 nr_steps = rbio->sector_nsteps; int total_sector_nr = get_bio_sector_nr(rbio, bio); - u32 offset = 0; - phys_addr_t paddrs[BTRFS_MAX_BLOCKSIZE / PAGE_SIZE]; - phys_addr_t paddr; /* No data csum for the whole stripe, no need to verify. */ if (!rbio->csum_bitmap || !rbio->csum_buf) @@ -1667,28 +1662,20 @@ static void verify_bio_data_sectors(struct btrfs_raid_bio *rbio, if (total_sector_nr >= rbio->nr_data * rbio->stripe_nsectors) return; - btrfs_bio_for_each_block_all(paddr, bio, step) { + for (struct bvec_iter iter = init_bvec_iter_for_bio(bio); + iter.bi_size; + bio_advance_iter(bio, &iter, fs_info->sectorsize), total_sector_nr++) { u8 csum_buf[BTRFS_CSUM_SIZE]; u8 *expected_csum; - paddrs[(offset / step) % nr_steps] = paddr; - offset += step; - - /* Not yet covering the full fs block, continue to the next step. */ - if (!IS_ALIGNED(offset, fs_info->sectorsize)) - continue; - /* No csum for this sector, skip to the next sector. */ - if (!test_bit(total_sector_nr, rbio->csum_bitmap)) { - total_sector_nr++; + if (!test_bit(total_sector_nr, rbio->csum_bitmap)) continue; - } expected_csum = rbio->csum_buf + total_sector_nr * fs_info->csum_size; - btrfs_calculate_block_csum_pages(fs_info, paddrs, csum_buf); + btrfs_csum_one_bio_block(fs_info, bio, &iter, csum_buf); if (unlikely(memcmp(csum_buf, expected_csum, fs_info->csum_size) != 0)) set_bit(total_sector_nr, rbio->error_bitmap); - total_sector_nr++; } } @@ -1879,6 +1866,27 @@ void raid56_parity_write(struct bio *bio, struct btrfs_io_context *bioc) start_async_work(rbio, rmw_rbio_work); } +static void calculate_block_csum_paddrs(struct btrfs_fs_info *fs_info, + const phys_addr_t paddrs[], u8 *dest) +{ + const u32 blocksize = fs_info->sectorsize; + const u32 step = min(blocksize, PAGE_SIZE); + const u32 nr_steps = blocksize / step; + struct btrfs_csum_ctx csum; + + btrfs_csum_init(&csum, fs_info->csum_type); + for (int i = 0; i < nr_steps; i++) { + const phys_addr_t paddr = paddrs[i]; + void *kaddr; + + ASSERT(offset_in_page(paddr) + step <= PAGE_SIZE); + kaddr = kmap_local_page(phys_to_page(paddr)) + offset_in_page(paddr); + btrfs_csum_update(&csum, kaddr, step); + kunmap_local(kaddr); + } + btrfs_csum_final(&csum, dest); +} + static int verify_one_sector(struct btrfs_raid_bio *rbio, int stripe_nr, int sector_nr) { @@ -1906,7 +1914,7 @@ static int verify_one_sector(struct btrfs_raid_bio *rbio, csum_expected = rbio->csum_buf + (stripe_nr * rbio->stripe_nsectors + sector_nr) * fs_info->csum_size; - btrfs_calculate_block_csum_pages(fs_info, paddrs, csum_buf); + calculate_block_csum_paddrs(fs_info, paddrs, csum_buf); if (unlikely(memcmp(csum_buf, csum_expected, fs_info->csum_size) != 0)) return -EIO; return 0; From 6d237d094798f422f02c88080b99b416a396a155 Mon Sep 17 00:00:00 2001 From: Breno Leitao Date: Mon, 24 Aug 2026 04:54:45 -0700 Subject: [PATCH 08/92] btrfs: skip extent tree lock in the shrinker for inodes without extent maps find_first_inode_to_shrink() takes inode->extent_tree.lock in write mode on every inode it walks, only to find out whether that inode has any extent maps. Most inodes have none, so the lock is taken and dropped again without any work being done. Check whether the tree is empty before taking the lock. tree->root is only modified with the tree lock held for write, so the unlocked read is a harmless race: a false empty just defers the inode to a later scan, which already happens whenever the write_trylock() below fails, and a false non-empty falls through to the existing check under the lock. Across the Meta production fleet the extent map shrinker is ~0.35% of non-idle kernel CPU. Attributing callees to their caller, find_first_inode_to_shrink() is ~65% of that, and the write_trylock() it does is ~30% of the whole shrinker. Micro benchmark: a 6 GiB btrfs on a loop device, 100000 empty files kept open, plus 200 1 MiB files created last so they get the highest inode numbers and every scan has to walk all the empty ones first. Each round drops the page cache, re-reads the data files to recreate the extent maps, then triggers the shrinker with "echo 2 > /proc/sys/vm/drop_caches". 15 rounds per run on ARM64 (Neoverse V2), 8 CPUs, no lock debugging. Cost of find_first_inode_to_shrink() from the ftrace function profiler, in ns per inode walked, median of runs: base patched delta idle 46.4 40.1 -13.6% 4 concurrent readers 47.8 38.4 -19.7% A separate build with CONFIG_LOCK_STAT, same test, for the extent map tree rwlock. The shrinker is not the only user of that lock, every extent map insert and lookup takes it too, which is why the acquisition count drops by two thirds rather than to nothing: base patched delta write acquisitions 628016 228000 -63.7% hold time total (us) 47512 22717 -52.2% acq cacheline bounces 1574 1288 -18.2% Signed-off-by: Breno Leitao Reviewed-by: Filipe Manana Signed-off-by: Filipe Manana Signed-off-by: David Sterba --- fs/btrfs/extent_map.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/fs/btrfs/extent_map.c b/fs/btrfs/extent_map.c index 6ad7b39ae358..86d9c6f5ff4b 100644 --- a/fs/btrfs/extent_map.c +++ b/fs/btrfs/extent_map.c @@ -1219,6 +1219,14 @@ static struct btrfs_inode *find_first_inode_to_shrink(struct btrfs_root *root, tree = &inode->extent_tree; + /* + * Most inodes have no extent maps, so check without the lock. + * The race is harmless: a false empty just defers the inode to + * a later scan, and a false non-empty is caught under the lock. + */ + if (data_race(RB_EMPTY_ROOT(&tree->root))) + goto next; + /* * We want to be fast so if the lock is busy we don't want to * spend time waiting for it (some task is about to do IO for From 13b5866be3edd278c8b0ede4ddfc0e6a454d9ca7 Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Tue, 25 Aug 2026 13:42:30 +0930 Subject: [PATCH 09/92] btrfs: remove unused variable flags from btrfs_read_qgroup_config() Since commit e562a8bdf652 ("btrfs: introduce BTRFS_QGROUP_RUNTIME_FLAG_CANCEL_RESCAN"), that @flags variable is no longer utilized. Just remove it. Reviewed-by: Johannes Thumshirn Signed-off-by: Qu Wenruo Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/qgroup.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/fs/btrfs/qgroup.c b/fs/btrfs/qgroup.c index f68b696b4bf7..f3685cbb8f2e 100644 --- a/fs/btrfs/qgroup.c +++ b/fs/btrfs/qgroup.c @@ -426,7 +426,6 @@ int btrfs_read_qgroup_config(struct btrfs_fs_info *fs_info) struct extent_buffer *l; int slot; int ret = 0; - u64 flags = 0; u64 rescan_progress = 0; if (!fs_info->quota_root) @@ -609,7 +608,6 @@ int btrfs_read_qgroup_config(struct btrfs_fs_info *fs_info) } out: btrfs_free_path(path); - fs_info->qgroup_flags |= flags; if (ret >= 0) { if (fs_info->qgroup_flags & BTRFS_QGROUP_STATUS_FLAG_ON) set_bit(BTRFS_FS_QUOTA_ENABLED, &fs_info->flags); From bce672e6603d7ae2b2760069af8b093d04542dfc Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Tue, 25 Aug 2026 13:42:31 +0930 Subject: [PATCH 10/92] btrfs: qgroup: use atomic operations for btrfs_fs_info::qgroup_flags Currently we define btrfs_fs_info::qgroup_flags as u64, to match the on-disk qgroup status item's flag. But for now we have only 4 bits utilized for that flag, and since it's u64 we have no way to properly use the existing atomic bit operations (requires an unsigned long pointer). This results in a lot of non-atomic operations inside qgroup code. Some maybe fine as other locks are involved, but still it's not a good practice. Remove those non-atomic operations by: - Re-define btrfs_fs_info::qgroup_flags as unsigned long - Define BTRFS_QGROUP_STATUS_BIT_* and BTRFS_QGROUP_RUNTIME_BIT_* Instead of the old value define the bit number. - Use set_bit()/clear_bit()/test_bit() to replace open-coded bit operations - Add one extra check at qgroup status item read time To make sure the on-disk flag is still inside ULONG_MAX. Otherwise reject the status item and disable qgroup. - Get rid of unnecessary spinlock when checking a single bit Reviewed-by: Johannes Thumshirn Signed-off-by: Qu Wenruo Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/fs.h | 2 +- fs/btrfs/ioctl.c | 2 +- fs/btrfs/qgroup.c | 97 ++++++++++++++++----------------- fs/btrfs/qgroup.h | 4 +- fs/btrfs/sysfs.c | 8 +-- include/uapi/linux/btrfs_tree.h | 21 ++++--- 6 files changed, 67 insertions(+), 67 deletions(-) diff --git a/fs/btrfs/fs.h b/fs/btrfs/fs.h index 10e15a319b93..3eba8438593c 100644 --- a/fs/btrfs/fs.h +++ b/fs/btrfs/fs.h @@ -811,7 +811,7 @@ struct btrfs_fs_info { struct btrfs_discard_ctl discard_ctl; /* Is qgroup tracking in a consistent state? */ - u64 qgroup_flags; + unsigned long qgroup_flags; /* Holds configuration and tracking. Protected by qgroup_lock. */ struct rb_root qgroup_tree; diff --git a/fs/btrfs/ioctl.c b/fs/btrfs/ioctl.c index 576cd76ca0bf..25d9feaa9003 100644 --- a/fs/btrfs/ioctl.c +++ b/fs/btrfs/ioctl.c @@ -3881,7 +3881,7 @@ static long btrfs_ioctl_quota_rescan_status(struct btrfs_fs_info *fs_info, if (!capable(CAP_SYS_ADMIN)) return -EPERM; - if (fs_info->qgroup_flags & BTRFS_QGROUP_STATUS_FLAG_RESCAN) { + if (test_bit(BTRFS_QGROUP_STATUS_BIT_RESCAN, &fs_info->qgroup_flags)) { qsa.flags = 1; qsa.progress = fs_info->qgroup_rescan_progress.objectid; } diff --git a/fs/btrfs/qgroup.c b/fs/btrfs/qgroup.c index f3685cbb8f2e..2c2ac0f16b1e 100644 --- a/fs/btrfs/qgroup.c +++ b/fs/btrfs/qgroup.c @@ -34,7 +34,7 @@ enum btrfs_qgroup_mode btrfs_qgroup_mode(const struct btrfs_fs_info *fs_info) { if (!test_bit(BTRFS_FS_QUOTA_ENABLED, &fs_info->flags)) return BTRFS_QGROUP_MODE_DISABLED; - if (fs_info->qgroup_flags & BTRFS_QGROUP_STATUS_FLAG_SIMPLE_MODE) + if (test_bit(BTRFS_QGROUP_STATUS_BIT_SIMPLE_MODE, &fs_info->qgroup_flags)) return BTRFS_QGROUP_MODE_SIMPLE; return BTRFS_QGROUP_MODE_FULL; } @@ -384,14 +384,14 @@ static bool squota_check_parent_usage(struct btrfs_fs_info *fs_info, struct btrf __printf(2, 3) static void qgroup_mark_inconsistent(struct btrfs_fs_info *fs_info, const char *fmt, ...) { - const u64 old_flags = fs_info->qgroup_flags; + const unsigned long old_flags = fs_info->qgroup_flags; if (btrfs_qgroup_mode(fs_info) == BTRFS_QGROUP_MODE_SIMPLE) return; - fs_info->qgroup_flags |= (BTRFS_QGROUP_STATUS_FLAG_INCONSISTENT | - BTRFS_QGROUP_RUNTIME_FLAG_CANCEL_RESCAN | - BTRFS_QGROUP_RUNTIME_FLAG_NO_ACCOUNTING); - if (!(old_flags & BTRFS_QGROUP_STATUS_FLAG_INCONSISTENT)) { + set_bit(BTRFS_QGROUP_STATUS_BIT_INCONSISTENT, &fs_info->qgroup_flags); + set_bit(BTRFS_QGROUP_RUNTIME_BIT_CANCEL_RESCAN, &fs_info->qgroup_flags); + set_bit(BTRFS_QGROUP_RUNTIME_BIT_NO_ACCOUNTING, &fs_info->qgroup_flags); + if (!test_bit(BTRFS_QGROUP_STATUS_BIT_INCONSISTENT, &old_flags)) { struct va_format vaf; va_list args; @@ -472,8 +472,12 @@ int btrfs_read_qgroup_config(struct btrfs_fs_info *fs_info) "old qgroup version, quota disabled"); goto out; } - fs_info->qgroup_flags = btrfs_qgroup_status_flags(l, ptr); - if (fs_info->qgroup_flags & BTRFS_QGROUP_STATUS_FLAG_SIMPLE_MODE) + if (btrfs_qgroup_status_flags(l, ptr) > ULONG_MAX) { + btrfs_err(fs_info, "invalid qgroup status flags, quota disabled"); + goto out; + } + fs_info->qgroup_flags = (unsigned long)btrfs_qgroup_status_flags(l, ptr); + if (test_bit(BTRFS_QGROUP_STATUS_BIT_SIMPLE_MODE, &fs_info->qgroup_flags)) qgroup_read_enable_gen(fs_info, l, slot, ptr); else if (btrfs_qgroup_status_generation(l, ptr) != fs_info->generation) qgroup_mark_inconsistent(fs_info, "qgroup generation mismatch"); @@ -609,12 +613,12 @@ int btrfs_read_qgroup_config(struct btrfs_fs_info *fs_info) out: btrfs_free_path(path); if (ret >= 0) { - if (fs_info->qgroup_flags & BTRFS_QGROUP_STATUS_FLAG_ON) + if (test_bit(BTRFS_QGROUP_STATUS_BIT_ON, &fs_info->qgroup_flags)) set_bit(BTRFS_FS_QUOTA_ENABLED, &fs_info->flags); - if (fs_info->qgroup_flags & BTRFS_QGROUP_STATUS_FLAG_RESCAN) + if (test_bit(BTRFS_QGROUP_STATUS_BIT_RESCAN, &fs_info->qgroup_flags)) ret = qgroup_rescan_init(fs_info, rescan_progress, 0); } else { - fs_info->qgroup_flags &= ~BTRFS_QGROUP_STATUS_FLAG_RESCAN; + clear_bit(BTRFS_QGROUP_STATUS_BIT_RESCAN, &fs_info->qgroup_flags); btrfs_sysfs_del_qgroups(fs_info); } @@ -1099,9 +1103,9 @@ int btrfs_quota_enable(struct btrfs_fs_info *fs_info, struct btrfs_qgroup_status_item); btrfs_set_qgroup_status_generation(leaf, ptr, trans->transid); btrfs_set_qgroup_status_version(leaf, ptr, BTRFS_QGROUP_STATUS_VERSION); - fs_info->qgroup_flags = BTRFS_QGROUP_STATUS_FLAG_ON; + fs_info->qgroup_flags = (1UL << BTRFS_QGROUP_STATUS_BIT_ON); if (simple) { - fs_info->qgroup_flags |= BTRFS_QGROUP_STATUS_FLAG_SIMPLE_MODE; + set_bit(BTRFS_QGROUP_STATUS_BIT_SIMPLE_MODE, &fs_info->qgroup_flags); btrfs_set_fs_incompat(fs_info, SIMPLE_QUOTA); /* * Set the enable generation to the next transaction, as we cannot @@ -1111,7 +1115,7 @@ int btrfs_quota_enable(struct btrfs_fs_info *fs_info, */ btrfs_set_qgroup_status_enable_gen(leaf, ptr, trans->transid + 1); } else { - fs_info->qgroup_flags |= BTRFS_QGROUP_STATUS_FLAG_INCONSISTENT; + set_bit(BTRFS_QGROUP_STATUS_BIT_INCONSISTENT, &fs_info->qgroup_flags); } btrfs_set_qgroup_status_flags(leaf, ptr, fs_info->qgroup_flags & BTRFS_QGROUP_STATUS_FLAGS_MASK); @@ -1401,8 +1405,8 @@ int btrfs_quota_disable(struct btrfs_fs_info *fs_info) spin_lock(&fs_info->qgroup_lock); quota_root = fs_info->quota_root; fs_info->quota_root = NULL; - fs_info->qgroup_flags &= ~BTRFS_QGROUP_STATUS_FLAG_ON; - fs_info->qgroup_flags &= ~BTRFS_QGROUP_STATUS_FLAG_SIMPLE_MODE; + clear_bit(BTRFS_QGROUP_STATUS_BIT_ON, &fs_info->qgroup_flags); + clear_bit(BTRFS_QGROUP_STATUS_BIT_SIMPLE_MODE, &fs_info->qgroup_flags); fs_info->qgroup_drop_subtree_thres = BTRFS_QGROUP_DROP_SUBTREE_THRES_DEFAULT; spin_unlock(&fs_info->qgroup_lock); @@ -1552,7 +1556,7 @@ static int quick_update_accounting(struct btrfs_fs_info *fs_info, } out: if (ret) - fs_info->qgroup_flags |= BTRFS_QGROUP_STATUS_FLAG_INCONSISTENT; + set_bit(BTRFS_QGROUP_STATUS_BIT_INCONSISTENT, &fs_info->qgroup_flags); return ret; } @@ -1873,7 +1877,7 @@ int btrfs_remove_qgroup(struct btrfs_trans_handle *trans, u64 qgroupid) * very frequently. */ if (btrfs_qgroup_mode(fs_info) == BTRFS_QGROUP_MODE_FULL && - !(fs_info->qgroup_flags & BTRFS_QGROUP_STATUS_FLAG_INCONSISTENT)) { + !test_bit(BTRFS_QGROUP_STATUS_BIT_INCONSISTENT, &fs_info->qgroup_flags)) { if (unlikely(qgroup->rfer || qgroup->excl || qgroup->rfer_cmpr || qgroup->excl_cmpr)) { DEBUG_WARN(); @@ -2118,7 +2122,7 @@ int btrfs_qgroup_trace_extent_post(struct btrfs_trans_handle *trans, */ ASSERT(trans != NULL); - if (fs_info->qgroup_flags & BTRFS_QGROUP_RUNTIME_FLAG_NO_ACCOUNTING) + if (test_bit(BTRFS_QGROUP_RUNTIME_BIT_NO_ACCOUNTING, &fs_info->qgroup_flags)) return 0; ret = btrfs_find_all_roots(&ctx, true); @@ -2959,7 +2963,7 @@ int btrfs_qgroup_account_extent(struct btrfs_trans_handle *trans, u64 bytenr, * we can't just exit here. */ if (!btrfs_qgroup_full_accounting(fs_info) || - fs_info->qgroup_flags & BTRFS_QGROUP_RUNTIME_FLAG_NO_ACCOUNTING) + test_bit(BTRFS_QGROUP_RUNTIME_BIT_NO_ACCOUNTING, &fs_info->qgroup_flags)) goto out_free; if (new_roots) { @@ -2981,7 +2985,7 @@ int btrfs_qgroup_account_extent(struct btrfs_trans_handle *trans, u64 bytenr, num_bytes, nr_old_roots, nr_new_roots); mutex_lock(&fs_info->qgroup_rescan_lock); - if (fs_info->qgroup_flags & BTRFS_QGROUP_STATUS_FLAG_RESCAN) { + if (test_bit(BTRFS_QGROUP_STATUS_BIT_RESCAN, &fs_info->qgroup_flags)) { if (fs_info->qgroup_rescan_progress.objectid <= bytenr) { mutex_unlock(&fs_info->qgroup_rescan_lock); ret = 0; @@ -3042,8 +3046,8 @@ int btrfs_qgroup_account_extents(struct btrfs_trans_handle *trans) num_dirty_extents++; trace_btrfs_qgroup_account_extents(fs_info, record, bytenr); - if (!ret && !(fs_info->qgroup_flags & - BTRFS_QGROUP_RUNTIME_FLAG_NO_ACCOUNTING)) { + if (!ret && !test_bit(BTRFS_QGROUP_RUNTIME_BIT_NO_ACCOUNTING, + &fs_info->qgroup_flags)) { struct btrfs_backref_walk_ctx ctx = { 0 }; ctx.bytenr = bytenr; @@ -3150,9 +3154,9 @@ int btrfs_run_qgroups(struct btrfs_trans_handle *trans) spin_lock(&fs_info->qgroup_lock); } if (btrfs_qgroup_enabled(fs_info)) - fs_info->qgroup_flags |= BTRFS_QGROUP_STATUS_FLAG_ON; + set_bit(BTRFS_QGROUP_STATUS_BIT_ON, &fs_info->qgroup_flags); else - fs_info->qgroup_flags &= ~BTRFS_QGROUP_STATUS_FLAG_ON; + clear_bit(BTRFS_QGROUP_STATUS_BIT_ON, &fs_info->qgroup_flags); spin_unlock(&fs_info->qgroup_lock); ret = update_qgroup_status_item(trans); @@ -3842,7 +3846,7 @@ static bool rescan_should_stop(struct btrfs_fs_info *fs_info) return true; if (!btrfs_qgroup_enabled(fs_info)) return true; - if (fs_info->qgroup_flags & BTRFS_QGROUP_RUNTIME_FLAG_CANCEL_RESCAN) + if (test_bit(BTRFS_QGROUP_RUNTIME_BIT_CANCEL_RESCAN, &fs_info->qgroup_flags)) return true; return false; } @@ -3892,12 +3896,10 @@ static void btrfs_qgroup_rescan_worker(struct btrfs_work *work) btrfs_free_path(path); mutex_lock(&fs_info->qgroup_rescan_lock); - if (ret > 0 && - fs_info->qgroup_flags & BTRFS_QGROUP_STATUS_FLAG_INCONSISTENT) { - fs_info->qgroup_flags &= ~BTRFS_QGROUP_STATUS_FLAG_INCONSISTENT; - } else if (ret < 0 || stopped) { - fs_info->qgroup_flags |= BTRFS_QGROUP_STATUS_FLAG_INCONSISTENT; - } + if (ret > 0) + clear_bit(BTRFS_QGROUP_STATUS_BIT_INCONSISTENT, &fs_info->qgroup_flags); + else if (ret < 0 || stopped) + set_bit(BTRFS_QGROUP_STATUS_BIT_INCONSISTENT, &fs_info->qgroup_flags); mutex_unlock(&fs_info->qgroup_rescan_lock); /* @@ -3921,9 +3923,9 @@ static void btrfs_qgroup_rescan_worker(struct btrfs_work *work) } mutex_lock(&fs_info->qgroup_rescan_lock); - if (!stopped || - fs_info->qgroup_flags & BTRFS_QGROUP_RUNTIME_FLAG_CANCEL_RESCAN) - fs_info->qgroup_flags &= ~BTRFS_QGROUP_STATUS_FLAG_RESCAN; + if (!stopped || test_bit(BTRFS_QGROUP_RUNTIME_BIT_CANCEL_RESCAN, + &fs_info->qgroup_flags)) + clear_bit(BTRFS_QGROUP_STATUS_BIT_RESCAN, &fs_info->qgroup_flags); if (trans) { int ret2 = update_qgroup_status_item(trans); @@ -3933,7 +3935,7 @@ static void btrfs_qgroup_rescan_worker(struct btrfs_work *work) } } fs_info->qgroup_rescan_running = false; - fs_info->qgroup_flags &= ~BTRFS_QGROUP_RUNTIME_FLAG_CANCEL_RESCAN; + clear_bit(BTRFS_QGROUP_RUNTIME_BIT_CANCEL_RESCAN, &fs_info->qgroup_flags); complete_all(&fs_info->qgroup_rescan_completion); mutex_unlock(&fs_info->qgroup_rescan_lock); @@ -3944,7 +3946,7 @@ static void btrfs_qgroup_rescan_worker(struct btrfs_work *work) if (stopped) { btrfs_info(fs_info, "qgroup scan paused"); - } else if (fs_info->qgroup_flags & BTRFS_QGROUP_RUNTIME_FLAG_CANCEL_RESCAN) { + } else if (test_bit(BTRFS_QGROUP_RUNTIME_BIT_CANCEL_RESCAN, &fs_info->qgroup_flags)) { btrfs_info(fs_info, "qgroup scan cancelled"); } else if (ret >= 0) { btrfs_info(fs_info, "qgroup scan completed%s", @@ -3971,13 +3973,11 @@ qgroup_rescan_init(struct btrfs_fs_info *fs_info, u64 progress_objectid, if (!init_flags) { /* we're resuming qgroup rescan at mount time */ - if (!(fs_info->qgroup_flags & - BTRFS_QGROUP_STATUS_FLAG_RESCAN)) { + if (!(test_bit(BTRFS_QGROUP_STATUS_BIT_RESCAN, &fs_info->qgroup_flags))) { btrfs_debug(fs_info, "qgroup rescan init failed, qgroup rescan is not queued"); ret = -EINVAL; - } else if (!(fs_info->qgroup_flags & - BTRFS_QGROUP_STATUS_FLAG_ON)) { + } else if (!(test_bit(BTRFS_QGROUP_STATUS_BIT_ON, &fs_info->qgroup_flags))) { btrfs_debug(fs_info, "qgroup rescan init failed, qgroup is not enabled"); ret = -ENOTCONN; @@ -3990,10 +3990,9 @@ qgroup_rescan_init(struct btrfs_fs_info *fs_info, u64 progress_objectid, mutex_lock(&fs_info->qgroup_rescan_lock); if (init_flags) { - if (fs_info->qgroup_flags & BTRFS_QGROUP_STATUS_FLAG_RESCAN) { + if (test_bit(BTRFS_QGROUP_STATUS_BIT_RESCAN, &fs_info->qgroup_flags)) { ret = -EINPROGRESS; - } else if (!(fs_info->qgroup_flags & - BTRFS_QGROUP_STATUS_FLAG_ON)) { + } else if (!test_bit(BTRFS_QGROUP_STATUS_BIT_ON, &fs_info->qgroup_flags)) { btrfs_debug(fs_info, "qgroup rescan init failed, qgroup is not enabled"); ret = -ENOTCONN; @@ -4006,13 +4005,13 @@ qgroup_rescan_init(struct btrfs_fs_info *fs_info, u64 progress_objectid, mutex_unlock(&fs_info->qgroup_rescan_lock); return ret; } - fs_info->qgroup_flags |= BTRFS_QGROUP_STATUS_FLAG_RESCAN; + set_bit(BTRFS_QGROUP_STATUS_BIT_RESCAN, &fs_info->qgroup_flags); } memset(&fs_info->qgroup_rescan_progress, 0, sizeof(fs_info->qgroup_rescan_progress)); - fs_info->qgroup_flags &= ~(BTRFS_QGROUP_RUNTIME_FLAG_CANCEL_RESCAN | - BTRFS_QGROUP_RUNTIME_FLAG_NO_ACCOUNTING); + clear_bit(BTRFS_QGROUP_RUNTIME_BIT_CANCEL_RESCAN, &fs_info->qgroup_flags); + clear_bit(BTRFS_QGROUP_RUNTIME_BIT_NO_ACCOUNTING, &fs_info->qgroup_flags); fs_info->qgroup_rescan_progress.objectid = progress_objectid; init_completion(&fs_info->qgroup_rescan_completion); mutex_unlock(&fs_info->qgroup_rescan_lock); @@ -4063,7 +4062,7 @@ btrfs_qgroup_rescan(struct btrfs_fs_info *fs_info) ret = btrfs_commit_current_transaction(fs_info->fs_root); if (ret) { - fs_info->qgroup_flags &= ~BTRFS_QGROUP_STATUS_FLAG_RESCAN; + clear_bit(BTRFS_QGROUP_STATUS_BIT_RESCAN, &fs_info->qgroup_flags); return ret; } @@ -4116,7 +4115,7 @@ int btrfs_qgroup_wait_for_completion(struct btrfs_fs_info *fs_info, void btrfs_qgroup_rescan_resume(struct btrfs_fs_info *fs_info) { - if (fs_info->qgroup_flags & BTRFS_QGROUP_STATUS_FLAG_RESCAN) { + if (test_bit(BTRFS_QGROUP_STATUS_BIT_RESCAN, &fs_info->qgroup_flags)) { mutex_lock(&fs_info->qgroup_rescan_lock); fs_info->qgroup_rescan_running = true; btrfs_queue_work(fs_info->qgroup_rescan_workers, diff --git a/fs/btrfs/qgroup.h b/fs/btrfs/qgroup.h index 80dd2dacd56d..b3aaad5e617d 100644 --- a/fs/btrfs/qgroup.h +++ b/fs/btrfs/qgroup.h @@ -121,8 +121,8 @@ struct btrfs_qgroup_swapped_blocks; * To minimize the chance of collision with new persisted status flags, these * count backwards from the MSB. */ -#define BTRFS_QGROUP_RUNTIME_FLAG_CANCEL_RESCAN (1ULL << 63) -#define BTRFS_QGROUP_RUNTIME_FLAG_NO_ACCOUNTING (1ULL << 62) +#define BTRFS_QGROUP_RUNTIME_BIT_CANCEL_RESCAN (BITS_PER_LONG - 1) +#define BTRFS_QGROUP_RUNTIME_BIT_NO_ACCOUNTING (BITS_PER_LONG - 2) #define BTRFS_QGROUP_DROP_SUBTREE_THRES_DEFAULT (3) diff --git a/fs/btrfs/sysfs.c b/fs/btrfs/sysfs.c index 39cb01ee441a..1df6340a7123 100644 --- a/fs/btrfs/sysfs.c +++ b/fs/btrfs/sysfs.c @@ -2359,9 +2359,7 @@ static ssize_t qgroup_enabled_show(struct kobject *qgroups_kobj, struct btrfs_fs_info *fs_info = to_fs_info(qgroups_kobj->parent); bool enabled; - spin_lock(&fs_info->qgroup_lock); - enabled = fs_info->qgroup_flags & BTRFS_QGROUP_STATUS_FLAG_ON; - spin_unlock(&fs_info->qgroup_lock); + enabled = test_bit(BTRFS_QGROUP_STATUS_BIT_ON, &fs_info->qgroup_flags); return sysfs_emit(buf, "%d\n", enabled); } @@ -2401,9 +2399,7 @@ static ssize_t qgroup_inconsistent_show(struct kobject *qgroups_kobj, struct btrfs_fs_info *fs_info = to_fs_info(qgroups_kobj->parent); bool inconsistent; - spin_lock(&fs_info->qgroup_lock); - inconsistent = (fs_info->qgroup_flags & BTRFS_QGROUP_STATUS_FLAG_INCONSISTENT); - spin_unlock(&fs_info->qgroup_lock); + inconsistent = test_bit(BTRFS_QGROUP_STATUS_BIT_INCONSISTENT, &fs_info->qgroup_flags); return sysfs_emit(buf, "%d\n", inconsistent); } diff --git a/include/uapi/linux/btrfs_tree.h b/include/uapi/linux/btrfs_tree.h index cc3b9f7dccaf..b6ccaf848e4b 100644 --- a/include/uapi/linux/btrfs_tree.h +++ b/include/uapi/linux/btrfs_tree.h @@ -1255,13 +1255,16 @@ static inline __u16 btrfs_qgroup_level(__u64 qgroupid) } /* - * is subvolume quota turned on? + * The following BTRFS_QGROUP_STATUS_BIT_* are for * btrfs_qgroup_status_item::flags. + * + * Is subvolume quota turned on? */ -#define BTRFS_QGROUP_STATUS_FLAG_ON (1ULL << 0) -/* - * RESCAN is set during the initialization phase - */ -#define BTRFS_QGROUP_STATUS_FLAG_RESCAN (1ULL << 1) +#define BTRFS_QGROUP_STATUS_BIT_ON (0) +#define BTRFS_QGROUP_STATUS_FLAG_ON (1UL << BTRFS_QGROUP_STATUS_BIT_ON) + +/* RESCAN is set during the initialization phase */ +#define BTRFS_QGROUP_STATUS_BIT_RESCAN (1) +#define BTRFS_QGROUP_STATUS_FLAG_RESCAN (1UL << BTRFS_QGROUP_STATUS_BIT_RESCAN) /* * Some qgroup entries are known to be out of date, * either because the configuration has changed in a way that @@ -1269,14 +1272,16 @@ static inline __u16 btrfs_qgroup_level(__u64 qgroupid) * with a non-qgroup-aware version. * Turning qouta off and on again makes it inconsistent, too. */ -#define BTRFS_QGROUP_STATUS_FLAG_INCONSISTENT (1ULL << 2) +#define BTRFS_QGROUP_STATUS_BIT_INCONSISTENT (2) +#define BTRFS_QGROUP_STATUS_FLAG_INCONSISTENT (1UL << BTRFS_QGROUP_STATUS_BIT_INCONSISTENT) /* * Whether or not this filesystem is using simple quotas. Not exactly the * incompat bit, because we support using simple quotas, disabling it, then * going back to full qgroup quotas. */ -#define BTRFS_QGROUP_STATUS_FLAG_SIMPLE_MODE (1ULL << 3) +#define BTRFS_QGROUP_STATUS_BIT_SIMPLE_MODE (3) +#define BTRFS_QGROUP_STATUS_FLAG_SIMPLE_MODE (1UL << BTRFS_QGROUP_STATUS_BIT_SIMPLE_MODE) #define BTRFS_QGROUP_STATUS_FLAGS_MASK (BTRFS_QGROUP_STATUS_FLAG_ON | \ BTRFS_QGROUP_STATUS_FLAG_RESCAN | \ From 27a895d6ec8d4d0e87cc08850226086b31107a06 Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Thu, 27 Aug 2026 16:25:29 +0930 Subject: [PATCH 11/92] btrfs: reject new qgroup rescan during subvolume dropping Commit 011b46c30476 ("btrfs: skip subtree scan if it's too high to avoid low stall in btrfs_commit_transaction()") introduced a threshold to skip huge subtree scan during subvolume dropping. But that's not covering all cases, e.g. rescan can still be started immediately after that huge subtree skipping. This will cause rescan to do the same accounting for that subtree anyway, still causing a long stall during transaction commit. Introduce a new runtime qgroup flag, BTRFS_QGROUP_RUNTIME_BIT_REJECT_RESCAN, so that during cleanup of a subvolume, no new qgroup rescan can be initiated. The rejection uses the same -EINPROGRESS, as if there is already a running qgroup rescan. And since we have the extra bit, we can no longer allow plain assignment in btrfs_quota_enable(), as the plain assignment will override the REJECT_RESCAN bit. To co-operate this new flag: - Make btrfs_quota_enable() to only set BTRFS_QGROUP_STATUS_BIT_ON So it won't override the existing BTRFS_QGROUP_RUNTIME_BIT_REJECT_RESCAN bit. - Make btrfs_quota_disable() to clear every non-rescan bit This includes: * BTRFS_QGROUP_STATUS_BIT_ON * BTRFS_QGROUP_STATUS_BIT_INCONSISTENT * BTRFS_QGROUP_RUNTIME_BIT_NO_ACCOUNTING For rescan related bits, they are either cleared by the rescan thread, or by the caller who rejects rescan. Reviewed-by: Boris Burkov Signed-off-by: Qu Wenruo Signed-off-by: David Sterba --- fs/btrfs/disk-io.c | 2 ++ fs/btrfs/qgroup.c | 13 +++++++++++-- fs/btrfs/qgroup.h | 11 +++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/fs/btrfs/disk-io.c b/fs/btrfs/disk-io.c index 0f3e8e74b768..94a7e9059a7b 100644 --- a/fs/btrfs/disk-io.c +++ b/fs/btrfs/disk-io.c @@ -1496,7 +1496,9 @@ static int cleaner_kthread(void *arg) btrfs_run_delayed_iputs(fs_info); + set_bit(BTRFS_QGROUP_RUNTIME_BIT_REJECT_RESCAN, &fs_info->qgroup_flags); again = btrfs_clean_one_deleted_snapshot(fs_info); + clear_bit(BTRFS_QGROUP_RUNTIME_BIT_REJECT_RESCAN, &fs_info->qgroup_flags); mutex_unlock(&fs_info->cleaner_mutex); /* diff --git a/fs/btrfs/qgroup.c b/fs/btrfs/qgroup.c index 2c2ac0f16b1e..e01b31aa0b1b 100644 --- a/fs/btrfs/qgroup.c +++ b/fs/btrfs/qgroup.c @@ -1103,7 +1103,7 @@ int btrfs_quota_enable(struct btrfs_fs_info *fs_info, struct btrfs_qgroup_status_item); btrfs_set_qgroup_status_generation(leaf, ptr, trans->transid); btrfs_set_qgroup_status_version(leaf, ptr, BTRFS_QGROUP_STATUS_VERSION); - fs_info->qgroup_flags = (1UL << BTRFS_QGROUP_STATUS_BIT_ON); + set_bit(BTRFS_QGROUP_STATUS_BIT_ON, &fs_info->qgroup_flags); if (simple) { set_bit(BTRFS_QGROUP_STATUS_BIT_SIMPLE_MODE, &fs_info->qgroup_flags); btrfs_set_fs_incompat(fs_info, SIMPLE_QUOTA); @@ -1405,8 +1405,14 @@ int btrfs_quota_disable(struct btrfs_fs_info *fs_info) spin_lock(&fs_info->qgroup_lock); quota_root = fs_info->quota_root; fs_info->quota_root = NULL; + /* + * Clear all on-disk and runtime bits, except RESCAN related ones, that + * are either handled by rescan thread, or the caller who rejects rescan. + */ clear_bit(BTRFS_QGROUP_STATUS_BIT_ON, &fs_info->qgroup_flags); clear_bit(BTRFS_QGROUP_STATUS_BIT_SIMPLE_MODE, &fs_info->qgroup_flags); + clear_bit(BTRFS_QGROUP_STATUS_BIT_INCONSISTENT, &fs_info->qgroup_flags); + clear_bit(BTRFS_QGROUP_RUNTIME_BIT_NO_ACCOUNTING, &fs_info->qgroup_flags); fs_info->qgroup_drop_subtree_thres = BTRFS_QGROUP_DROP_SUBTREE_THRES_DEFAULT; spin_unlock(&fs_info->qgroup_lock); @@ -3990,7 +3996,10 @@ qgroup_rescan_init(struct btrfs_fs_info *fs_info, u64 progress_objectid, mutex_lock(&fs_info->qgroup_rescan_lock); if (init_flags) { - if (test_bit(BTRFS_QGROUP_STATUS_BIT_RESCAN, &fs_info->qgroup_flags)) { + if (test_bit(BTRFS_QGROUP_STATUS_BIT_RESCAN, + &fs_info->qgroup_flags) || + test_bit(BTRFS_QGROUP_RUNTIME_BIT_REJECT_RESCAN, + &fs_info->qgroup_flags)) { ret = -EINPROGRESS; } else if (!test_bit(BTRFS_QGROUP_STATUS_BIT_ON, &fs_info->qgroup_flags)) { btrfs_debug(fs_info, diff --git a/fs/btrfs/qgroup.h b/fs/btrfs/qgroup.h index b3aaad5e617d..090ba5367872 100644 --- a/fs/btrfs/qgroup.h +++ b/fs/btrfs/qgroup.h @@ -124,6 +124,17 @@ struct btrfs_qgroup_swapped_blocks; #define BTRFS_QGROUP_RUNTIME_BIT_CANCEL_RESCAN (BITS_PER_LONG - 1) #define BTRFS_QGROUP_RUNTIME_BIT_NO_ACCOUNTING (BITS_PER_LONG - 2) +/* + * No new rescan allowed when set. + * + * During huge subtree dropping, qgroup will be marked inconsistent, and skip + * all future accounting to avoid long stall. But, an immediate rescan will + * re-enable qgroup and still stall the system. + * + * This bit is to avoid such rescan during the duration of a subvolume dropping. + */ +#define BTRFS_QGROUP_RUNTIME_BIT_REJECT_RESCAN (BITS_PER_LONG - 3) + #define BTRFS_QGROUP_DROP_SUBTREE_THRES_DEFAULT (3) /* From 310a6aa00cc80c749f5d71b7e2fa35dc6eacd065 Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Thu, 27 Aug 2026 16:25:30 +0930 Subject: [PATCH 12/92] btrfs: avoid long stall when dropping a non-shared large subvolume Commit 011b46c30476 ("btrfs: skip subtree scan if it's too high to avoid low stall in btrfs_commit_transaction()") introduced a mechanism to skip large subtree during snapshot dropping. But even for a subvolume without any shared tree blocks, we can still queue quite a lot of qgroup records into one transaction, and cause a long qgroup related stall. So also add a check against the subvolume root level, to determine if we need to mark qgroup inconsistent. Reviewed-by: Boris Burkov Signed-off-by: Qu Wenruo Signed-off-by: David Sterba --- fs/btrfs/extent-tree.c | 10 ++++++++++ fs/btrfs/qgroup.c | 18 ++++++++++++++++++ fs/btrfs/qgroup.h | 1 + 3 files changed, 29 insertions(+) diff --git a/fs/btrfs/extent-tree.c b/fs/btrfs/extent-tree.c index d6a4390ee34a..a0d5ab03aae2 100644 --- a/fs/btrfs/extent-tree.c +++ b/fs/btrfs/extent-tree.c @@ -6315,6 +6315,16 @@ int btrfs_drop_snapshot(struct btrfs_root *root, bool update_ref, bool for_reloc set_bit(BTRFS_ROOT_DELETING, &root->state); unfinished_drop = test_bit(BTRFS_ROOT_UNFINISHED_DROP, &root->state); + /* + * For subvolume dropping, check if the subvolume is large enough so + * that we need to mark qgroup inconsistent to avoid long qgroup stall. + * + * Even for a subvolume without any snapshot, there can still be + * a lot of qgroup records queued into one transaction. + */ + if (!for_reloc) + btrfs_qgroup_check_tree_drop(fs_info, rootid, + btrfs_header_level(root->node)); if (btrfs_disk_key_objectid(&root_item->drop_progress) == 0) { level = btrfs_header_level(root->node); path->nodes[level] = btrfs_lock_root_node(root); diff --git a/fs/btrfs/qgroup.c b/fs/btrfs/qgroup.c index e01b31aa0b1b..05e35eb126dc 100644 --- a/fs/btrfs/qgroup.c +++ b/fs/btrfs/qgroup.c @@ -2748,6 +2748,24 @@ int btrfs_qgroup_trace_subtree(struct btrfs_trans_handle *trans, return 0; } +void btrfs_qgroup_check_tree_drop(struct btrfs_fs_info *fs_info, u64 rootid, u8 level) +{ + u8 drop_subtree_thres; + + if (btrfs_qgroup_mode(fs_info) != BTRFS_QGROUP_MODE_FULL) + return; + + if (!btrfs_is_fstree(rootid)) + return; + + spin_lock(&fs_info->qgroup_lock); + drop_subtree_thres = fs_info->qgroup_drop_subtree_thres; + spin_unlock(&fs_info->qgroup_lock); + + if (level >= drop_subtree_thres) + qgroup_mark_inconsistent(fs_info, "subtree level reached threshold"); +} + static void qgroup_iterator_nested_add(struct list_head *head, struct btrfs_qgroup *qgroup) { if (!list_empty(&qgroup->nested_iterator)) diff --git a/fs/btrfs/qgroup.h b/fs/btrfs/qgroup.h index 090ba5367872..c64b26b09c22 100644 --- a/fs/btrfs/qgroup.h +++ b/fs/btrfs/qgroup.h @@ -376,6 +376,7 @@ int btrfs_qgroup_trace_leaf_items(struct btrfs_trans_handle *trans, int btrfs_qgroup_trace_subtree(struct btrfs_trans_handle *trans, struct extent_buffer *root_eb, u64 root_gen, int root_level); +void btrfs_qgroup_check_tree_drop(struct btrfs_fs_info *fs_info, u64 rootid, u8 level); int btrfs_qgroup_account_extent(struct btrfs_trans_handle *trans, u64 bytenr, u64 num_bytes, struct ulist *old_roots, struct ulist *new_roots); From 9a30f15f0f6e94312020969b0a32ac5be6a2ce0c Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Fri, 21 Aug 2026 19:42:10 +0930 Subject: [PATCH 13/92] btrfs: remove runtime tweakable feature sysfs interface There are 2 features that are marked runtime tweakable inside /sys/fs/btrfs/features/ - acl Which is a mount option, and it will not show up in /sys/fs/btrfs//features/ directory anyway. - extended_iref This feature can only be enabled, but not disabled at runtime. Furthermore it's already the default behavior since 3.12. So it means this feature is always enabled and cannot be disabled for modern btrfs. So there is no need to maintain the ability to modify btrfs' runtime features through sysfs. And furthermore, the existing btrfs_feature_attr_store() is race-prone, it relies on fs_info->transaction_kthread, but our sysfs interfaces are enabled before transaction_kthread. Meaning at mount time a sysfs write can trigger NULL pointer dereference if the transaction_kthread is not yet initialized. The opposite is also possible during unmount. Thankfully that race is not possible in the real world, as the only supported feature is already enabled. But it also means we do not really need to keep the race-prone infrastructure, so just remove it completely, and make the per-module and per-mount features files to be completely read-only. Even with the sysfs tweakable features removed, we can still enable extended_iref feature through ioctl. Reviewed-by: Boris Burkov Signed-off-by: Qu Wenruo Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/sysfs.c | 121 ++--------------------------------------------- 1 file changed, 4 insertions(+), 117 deletions(-) diff --git a/fs/btrfs/sysfs.c b/fs/btrfs/sysfs.c index 1df6340a7123..c5bb1c7eac6a 100644 --- a/fs/btrfs/sysfs.c +++ b/fs/btrfs/sysfs.c @@ -83,8 +83,7 @@ struct raid_kobject { #define BTRFS_FEAT_ATTR(_name, _feature_set, _feature_prefix, _feature_bit) \ static struct btrfs_feature_attr btrfs_attr_features_##_name = { \ .kobj_attr = __INIT_KOBJ_ATTR(_name, S_IRUGO, \ - btrfs_feature_attr_show, \ - btrfs_feature_attr_store), \ + btrfs_feature_attr_show, NULL), \ .feature_set = _feature_set, \ .feature_bit = _feature_prefix ##_## _feature_bit, \ } @@ -130,132 +129,22 @@ static u64 get_features(struct btrfs_fs_info *fs_info, return btrfs_super_incompat_flags(disk_super); } -static void set_features(struct btrfs_fs_info *fs_info, - enum btrfs_feature_set set, u64 features) -{ - struct btrfs_super_block *disk_super = fs_info->super_copy; - if (set == FEAT_COMPAT) - btrfs_set_super_compat_flags(disk_super, features); - else if (set == FEAT_COMPAT_RO) - btrfs_set_super_compat_ro_flags(disk_super, features); - else - btrfs_set_super_incompat_flags(disk_super, features); -} - -static int can_modify_feature(struct btrfs_feature_attr *fa) -{ - int val = 0; - u64 set, clear; - switch (fa->feature_set) { - case FEAT_COMPAT: - set = BTRFS_FEATURE_COMPAT_SAFE_SET; - clear = BTRFS_FEATURE_COMPAT_SAFE_CLEAR; - break; - case FEAT_COMPAT_RO: - set = BTRFS_FEATURE_COMPAT_RO_SAFE_SET; - clear = BTRFS_FEATURE_COMPAT_RO_SAFE_CLEAR; - break; - case FEAT_INCOMPAT: - set = BTRFS_FEATURE_INCOMPAT_SAFE_SET; - clear = BTRFS_FEATURE_INCOMPAT_SAFE_CLEAR; - break; - default: - btrfs_warn(NULL, "sysfs: unknown feature set %d", fa->feature_set); - return 0; - } - - if (set & fa->feature_bit) - val |= 1; - if (clear & fa->feature_bit) - val |= 2; - - return val; -} - static ssize_t btrfs_feature_attr_show(struct kobject *kobj, struct kobj_attribute *a, char *buf) { int val = 0; struct btrfs_fs_info *fs_info = to_fs_info(kobj); struct btrfs_feature_attr *fa = to_btrfs_feature_attr(a); + if (fs_info) { u64 features = get_features(fs_info, fa->feature_set); if (features & fa->feature_bit) val = 1; - } else - val = can_modify_feature(fa); + } return sysfs_emit(buf, "%d\n", val); } -static ssize_t btrfs_feature_attr_store(struct kobject *kobj, - struct kobj_attribute *a, - const char *buf, size_t count) -{ - struct btrfs_fs_info *fs_info; - struct btrfs_feature_attr *fa = to_btrfs_feature_attr(a); - u64 features, set, clear; - unsigned long val; - int ret; - - fs_info = to_fs_info(kobj); - if (!fs_info) - return -EPERM; - - if (sb_rdonly(fs_info->sb)) - return -EROFS; - - ret = kstrtoul(skip_spaces(buf), 0, &val); - if (ret) - return ret; - - if (fa->feature_set == FEAT_COMPAT) { - set = BTRFS_FEATURE_COMPAT_SAFE_SET; - clear = BTRFS_FEATURE_COMPAT_SAFE_CLEAR; - } else if (fa->feature_set == FEAT_COMPAT_RO) { - set = BTRFS_FEATURE_COMPAT_RO_SAFE_SET; - clear = BTRFS_FEATURE_COMPAT_RO_SAFE_CLEAR; - } else { - set = BTRFS_FEATURE_INCOMPAT_SAFE_SET; - clear = BTRFS_FEATURE_INCOMPAT_SAFE_CLEAR; - } - - features = get_features(fs_info, fa->feature_set); - - /* Nothing to do */ - if ((val && (features & fa->feature_bit)) || - (!val && !(features & fa->feature_bit))) - return count; - - if ((val && !(set & fa->feature_bit)) || - (!val && !(clear & fa->feature_bit))) { - btrfs_info(fs_info, - "%sabling feature %s on mounted fs is not supported.", - val ? "En" : "Dis", fa->kobj_attr.attr.name); - return -EPERM; - } - - btrfs_info(fs_info, "%s %s feature flag", - val ? "Setting" : "Clearing", fa->kobj_attr.attr.name); - - spin_lock(&fs_info->super_lock); - features = get_features(fs_info, fa->feature_set); - if (val) - features |= fa->feature_bit; - else - features &= ~fa->feature_bit; - set_features(fs_info, fa->feature_set, features); - spin_unlock(&fs_info->super_lock); - - /* - * We don't want to do full transaction commit from inside sysfs - */ - set_bit(BTRFS_FS_NEED_TRANS_COMMIT, &fs_info->flags); - wake_up_process(fs_info->transaction_kthread); - - return count; -} - static umode_t btrfs_feature_visible(struct kobject *kobj, struct attribute *attr, int unused) { @@ -269,9 +158,7 @@ static umode_t btrfs_feature_visible(struct kobject *kobj, fa = attr_to_btrfs_feature_attr(attr); features = get_features(fs_info, fa->feature_set); - if (can_modify_feature(fa)) - mode |= S_IWUSR; - else if (!(features & fa->feature_bit)) + if (!(features & fa->feature_bit)) mode = 0; } From 76c119fa458bd13c7f82855c29604e54d840b84f Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Tue, 1 Sep 2026 09:31:01 +0930 Subject: [PATCH 14/92] btrfs: use ordered extent to grab the logical address for submission In submit_one_sector() we call btrfs_get_extent() to grab the IO extent map so that we know where the logical location to submit the block. However there is no guarantee that there is an IO extent map for the block, and if there is no IO extent map nor ordered extent, btrfs_get_extent() can grab the file extent from on-disk metadata. That's why we have ASSERT()s to reject holes and compressed file extents. On the other hand, for the write range we should have both an IO extent map and an ordered extent, so there is no reason not to grab the ordered extent instead. There is some minor advantages: - No hole ordered extent So no need to rely on ASSERT()s to reject hole extents. And the ASSERT()s are depending on the kernel config, without CONFIG_BTRFS_ASSERT those ASSERT()s won't even trigger. - No IO errors Unlike btrfs_get_extent() which can return IO error when doing the metadata tree search, btrfs_lookup_ordered_extent() will either return an OE or not found. - Cached OE in bio_ctrl->bbio At bbio allocation we have already did an OE lookup, and we have a high chance that the current block also belongs to that OE. Use that cached OE can reduce the frequency to do an rb-tree search. - Smaller rb-tree Unlike extent-map-tree, which can contain cached extent maps, the life span of ordered extents are much shorter, they get removed from the ordered tree after the file extent item is inserted into the subvolume tree. So doing ordered extent tree search can be a tiny faster. And since we're here, also address some minor points: - Add error message for every EUCLEAN error - Remove a dead comment on btrfs_folio_clear_dirty() We no longer call folio_clear_dirty_for_io() since commit 095be159f3eb ("btrfs: unify folio dirty flag clearing"), so the folio flag is still dirty, and the folio dirty flag will be cleared by the last dirty block. Reviewed-by: Boris Burkov Reviewed-by: Johannes Thumshirn Reviewed-by: Daniel Vacek Signed-off-by: Qu Wenruo Signed-off-by: David Sterba --- fs/btrfs/extent_io.c | 64 +++++++++++++++++++++++++++----------------- 1 file changed, 40 insertions(+), 24 deletions(-) diff --git a/fs/btrfs/extent_io.c b/fs/btrfs/extent_io.c index d7600e5fa3d9..a221b63bdb20 100644 --- a/fs/btrfs/extent_io.c +++ b/fs/btrfs/extent_io.c @@ -1808,6 +1808,22 @@ static noinline_for_stack int writepage_delalloc(struct btrfs_inode *inode, return 0; } +static struct btrfs_ordered_extent *get_oe_from_bbio(const struct btrfs_bio *bbio, + u64 filepos) +{ + struct btrfs_ordered_extent *oe; + + if (!bbio || !bbio->ordered) + return NULL; + + oe = bbio->ordered; + if (!in_range(filepos, oe->file_offset, oe->num_bytes)) + return NULL; + + refcount_inc(&oe->refs); + return oe; +} + /* * Return 0 if we have submitted or queued the sector for submission. * Return <0 for critical errors, and the involved sector will be cleaned up. @@ -1820,11 +1836,10 @@ static int submit_one_sector(struct btrfs_inode *inode, loff_t i_size) { struct btrfs_fs_info *fs_info = inode->root->fs_info; - struct extent_map *em; + struct btrfs_ordered_extent *oe; u64 block_start; u64 disk_bytenr; u64 extent_offset; - u64 em_end; const u32 sectorsize = fs_info->sectorsize; unsigned int queued; @@ -1833,8 +1848,11 @@ static int submit_one_sector(struct btrfs_inode *inode, /* @filepos >= i_size case should be handled by the caller. */ ASSERT(filepos < i_size); - em = btrfs_get_extent(inode, NULL, filepos, sectorsize); - if (IS_ERR(em)) { + /* Try to reuse the existing OE from bbio first. */ + oe = get_oe_from_bbio(bio_ctrl->bbio, filepos); + if (!oe) + oe = btrfs_lookup_ordered_extent(inode, filepos); + if (unlikely(!oe)) { /* * bio_ctrl may contain a bio crossing several folios. * Submit it immediately so that the bio has a chance @@ -1857,31 +1875,25 @@ static int submit_one_sector(struct btrfs_inode *inode, */ btrfs_mark_ordered_io_finished(inode, filepos, fs_info->sectorsize, false); - return PTR_ERR(em); + btrfs_err_rl(fs_info, + "no ordered extent for root %lld ino %llu filepos %llu", + btrfs_root_id(inode->root), btrfs_ino(inode), + filepos); + return -EUCLEAN; } - extent_offset = filepos - em->start; - em_end = btrfs_extent_map_end(em); - ASSERT(filepos <= em_end); - ASSERT(IS_ALIGNED(em->start, sectorsize)); - ASSERT(IS_ALIGNED(em->len, sectorsize)); + extent_offset = filepos - oe->file_offset; + ASSERT(filepos < oe->file_offset + oe->num_bytes); + ASSERT(IS_ALIGNED(oe->file_offset, sectorsize)); + ASSERT(IS_ALIGNED(oe->num_bytes, sectorsize)); + ASSERT(oe->compress_type == BTRFS_COMPRESS_NONE); + ASSERT(!test_bit(BTRFS_ORDERED_COMPRESSED, &oe->flags)); - block_start = btrfs_extent_map_block_start(em); - disk_bytenr = btrfs_extent_map_block_start(em) + extent_offset; + block_start = oe->disk_bytenr + oe->offset; + disk_bytenr = block_start + extent_offset; - ASSERT(!btrfs_extent_map_is_compressed(em)); - ASSERT(block_start != EXTENT_MAP_HOLE); - ASSERT(block_start != EXTENT_MAP_INLINE); + btrfs_put_ordered_extent(oe); - btrfs_free_extent_map(em); - em = NULL; - - /* - * Although the PageDirty bit is cleared before entering this - * function, subpage dirty bit is not cleared. - * So clear subpage dirty bit here so next time we won't submit - * a folio for a range already written to disk. - */ btrfs_folio_clear_dirty(fs_info, folio, filepos, sectorsize); btrfs_folio_set_writeback(fs_info, folio, filepos, sectorsize); /* @@ -1898,6 +1910,10 @@ static int submit_one_sector(struct btrfs_inode *inode, btrfs_folio_clear_writeback(fs_info, folio, filepos, sectorsize); btrfs_mark_ordered_io_finished(inode, filepos, fs_info->sectorsize, false); + btrfs_err_rl(fs_info, + "failed to queue sector for root %lld ino %llu filepos %llu", + btrfs_root_id(inode->root), + btrfs_ino(inode), filepos); return -EUCLEAN; } return 0; From 45f86814783337092a4df6f656f7987c769b9579 Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Tue, 1 Sep 2026 10:01:33 +0930 Subject: [PATCH 15/92] btrfs: tree-checker: reject file extent items for special files File extent items are only utilized by regular files or symlinks, other files like directory/char/block/FIFO/sock files should not have any file extent item. Previously we were unable to reject such cases, as the inode item may not be in the same leaf. But we already have @prev_key in check_leaf_item(), this means we just need a new way to pass the mode of the previously hit inode item, then we can detect such problems. Introduce a new helper structure, saved_inode_info, to record the inode number and its mode hit in the same leaf, and keep it across the whole leaf. Then if we hit a file extent item, and the inode item is in the same leaf, we can refer to that to determine if we need to reject the file extent item. Now with the following corrupted fs tree, the kernel can safely reject the leaf: item 0 key (256 INODE_ITEM 0) itemoff 16123 itemsize 160 generation 3 transid 9 size 12 nbytes 16384 block group 0 mode 40755 links 1 uid 0 gid 0 rdev 0 sequence 1 flags 0x0(none) item 1 key (256 INODE_REF 256) itemoff 16111 itemsize 12 index 0 namelen 2 name: .. item 2 key (256 DIR_ITEM 496027801) itemoff 16075 itemsize 36 location key (257 INODE_ITEM 0) type FILE transid 9 data_len 0 name_len 6 name: foobar item 3 key (256 DIR_INDEX 2) itemoff 16039 itemsize 36 location key (257 INODE_ITEM 0) type FILE transid 9 data_len 0 name_len 6 name: foobar item 4 key (257 INODE_ITEM 0) itemoff 15879 itemsize 160 generation 9 transid 9 size 8192 nbytes 8192 block group 0 mode 60600 links 1 uid 0 gid 0 rdev 0 ^^ This is BLK type, not REG. sequence 2 flags 0x0(none) item 5 key (257 INODE_REF 256) itemoff 15863 itemsize 16 index 2 namelen 6 name: foobar item 6 key (257 EXTENT_DATA 0) itemoff 15810 itemsize 53 generation 9 type 1 (regular) extent data disk byte 13631488 nr 8192 extent data offset 0 nr 8192 ram 8192 extent compression 0 (none) extent encryption 0 With the patch, kernel will reject it with the following tree-checker errors: BTRFS critical (device loop0): corrupt leaf: root=5 block=30408704 slot=6 ino=257 file_offset=0, unexpected file extent item type 1 for inode mode 060600 BTRFS error (device loop0): read time tree block corruption detected on logical 30408704 mirror 1 Reported-by: ZhengYuan Huang Link: https://lore.kernel.org/linux-btrfs/20260817132051.267646-1-gality369@gmail.com/ Assisted-by: LLM (for generating the corrupted image) Reviewed-by: Johannes Thumshirn Signed-off-by: Qu Wenruo Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/tree-checker.c | 66 ++++++++++++++++++++++++++++++++++------- 1 file changed, 55 insertions(+), 11 deletions(-) diff --git a/fs/btrfs/tree-checker.c b/fs/btrfs/tree-checker.c index 4b1e47173c63..857ded43c3b9 100644 --- a/fs/btrfs/tree-checker.c +++ b/fs/btrfs/tree-checker.c @@ -163,6 +163,12 @@ static void dir_item_err(const struct extent_buffer *eb, int slot, va_end(args); } +/* Record info for the last hit inode. */ +struct saved_inode_info { + u64 ino; + u32 mode; +}; + /* * This functions checks prev_key->objectid, to ensure current key and prev_key * share the same objectid as inode number. @@ -204,15 +210,41 @@ static bool check_prev_ino(struct extent_buffer *leaf, prev_key->objectid, key->objectid); return false; } + +static bool can_have_extent_data(struct extent_buffer *leaf, + struct btrfs_key *key, int slot, u8 fi_type, + const struct saved_inode_info *inode_info) +{ + /* No inode item in this leaf. */ + if (inode_info->ino != key->objectid) + return true; + if (S_ISREG(inode_info->mode)) + return true; + if (S_ISLNK(inode_info->mode)) { + /* For a symlink, the file extent item should always be inlined. */ + if (unlikely(fi_type != BTRFS_FILE_EXTENT_INLINE)) + return false; + return true; + } + + /* + * The rest are special files, e.g. block/FIFO files, which cannnot + * have any file extent. + */ + return false; +} + static int check_extent_data_item(struct extent_buffer *leaf, struct btrfs_key *key, int slot, - struct btrfs_key *prev_key) + struct btrfs_key *prev_key, + const struct saved_inode_info *inode_info) { struct btrfs_fs_info *fs_info = leaf->fs_info; struct btrfs_file_extent_item *fi; u32 sectorsize = fs_info->sectorsize; u32 item_size = btrfs_item_size(leaf, slot); u64 extent_end; + u8 fi_type; if (unlikely(!IS_ALIGNED(key->offset, sectorsize))) { file_extent_err(leaf, slot, @@ -243,12 +275,18 @@ static int check_extent_data_item(struct extent_buffer *leaf, SZ_4K); return -EUCLEAN; } - if (unlikely(btrfs_file_extent_type(leaf, fi) >= - BTRFS_NR_FILE_EXTENT_TYPES)) { + fi_type = btrfs_file_extent_type(leaf, fi); + if (unlikely(fi_type >= BTRFS_NR_FILE_EXTENT_TYPES)) { file_extent_err(leaf, slot, "invalid type for file extent, have %u expect range [0, %u]", - btrfs_file_extent_type(leaf, fi), - BTRFS_NR_FILE_EXTENT_TYPES - 1); + fi_type, BTRFS_NR_FILE_EXTENT_TYPES - 1); + return -EUCLEAN; + } + + if (unlikely(!can_have_extent_data(leaf, key, slot, fi_type, inode_info))) { + file_extent_err(leaf, slot, + "unexpected file extent item type %u for inode mode 0%o", + fi_type, inode_info->mode); return -EUCLEAN; } @@ -270,7 +308,8 @@ static int check_extent_data_item(struct extent_buffer *leaf, btrfs_file_extent_encryption(leaf, fi)); return -EUCLEAN; } - if (btrfs_file_extent_type(leaf, fi) == BTRFS_FILE_EXTENT_INLINE) { + + if (fi_type == BTRFS_FILE_EXTENT_INLINE) { /* Inline extent must have 0 as key offset */ if (unlikely(key->offset)) { file_extent_err(leaf, slot, @@ -1206,7 +1245,8 @@ static int check_dev_item(struct extent_buffer *leaf, } static int check_inode_item(struct extent_buffer *leaf, - struct btrfs_key *key, int slot) + struct btrfs_key *key, int slot, + struct saved_inode_info *inode_info) { struct btrfs_fs_info *fs_info = leaf->fs_info; struct btrfs_inode_item *iitem; @@ -1291,6 +1331,8 @@ static int check_inode_item(struct extent_buffer *leaf, ro_flags); return -EUCLEAN; } + inode_info->ino = key->objectid; + inode_info->mode = mode; return 0; } @@ -2348,14 +2390,15 @@ static int check_free_space_bitmap(struct extent_buffer *leaf, static enum btrfs_tree_block_status check_leaf_item(struct extent_buffer *leaf, struct btrfs_key *key, int slot, - struct btrfs_key *prev_key) + struct btrfs_key *prev_key, + struct saved_inode_info *inode_info) { int ret = 0; struct btrfs_chunk *chunk; switch (key->type) { case BTRFS_EXTENT_DATA_KEY: - ret = check_extent_data_item(leaf, key, slot, prev_key); + ret = check_extent_data_item(leaf, key, slot, prev_key, inode_info); break; case BTRFS_EXTENT_CSUM_KEY: ret = check_csum_item(leaf, key, slot, prev_key); @@ -2385,7 +2428,7 @@ static enum btrfs_tree_block_status check_leaf_item(struct extent_buffer *leaf, ret = check_dev_extent_item(leaf, key, slot, prev_key); break; case BTRFS_INODE_ITEM_KEY: - ret = check_inode_item(leaf, key, slot); + ret = check_inode_item(leaf, key, slot, inode_info); break; case BTRFS_ROOT_ITEM_KEY: ret = check_root_item(leaf, key, slot); @@ -2433,6 +2476,7 @@ static enum btrfs_tree_block_status check_leaf_item(struct extent_buffer *leaf, enum btrfs_tree_block_status __btrfs_check_leaf(struct extent_buffer *leaf) { struct btrfs_fs_info *fs_info = leaf->fs_info; + struct saved_inode_info inode_info = { 0 }; /* No valid key type is 0, so all key should be larger than this key */ struct btrfs_key prev_key = {0, 0, 0}; struct btrfs_key key; @@ -2568,7 +2612,7 @@ enum btrfs_tree_block_status __btrfs_check_leaf(struct extent_buffer *leaf) } /* Check if the item size and content meet other criteria. */ - ret = check_leaf_item(leaf, &key, slot, &prev_key); + ret = check_leaf_item(leaf, &key, slot, &prev_key, &inode_info); if (unlikely(ret != BTRFS_TREE_BLOCK_CLEAN)) return ret; From 57c6777ebd249f79bb935366cd8cbe28d06f6101 Mon Sep 17 00:00:00 2001 From: Daniel Vacek Date: Wed, 2 Sep 2026 11:20:24 +0200 Subject: [PATCH 16/92] btrfs: consume given iter directly instead of copying in csum_one_bio() Avoid copying the iter twice in async case. We already have a copy csum_one_bio() can consume directly. No need to copy it again the second time. We can use this copy also in the sync case and get rid of the parameter. Reviewed-by: Qu Wenruo Signed-off-by: Daniel Vacek Signed-off-by: David Sterba --- fs/btrfs/file-item.c | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/fs/btrfs/file-item.c b/fs/btrfs/file-item.c index 581ca5653be9..2c6c14dfe393 100644 --- a/fs/btrfs/file-item.c +++ b/fs/btrfs/file-item.c @@ -797,18 +797,18 @@ int btrfs_lookup_csums_bitmap(struct btrfs_root *root, struct btrfs_path *path, return ret; } -static void csum_one_bio(struct btrfs_bio *bbio, struct bvec_iter *src) +static void csum_one_bio(struct btrfs_bio *bbio) { struct btrfs_inode *inode = bbio->inode; struct btrfs_fs_info *fs_info = inode->root->fs_info; struct btrfs_ordered_sum *sums = bbio->sums; - struct bvec_iter iter; const u32 blocksize = fs_info->sectorsize; int index = 0; - for (iter = *src; iter.bi_size; bio_advance_iter(&bbio->bio, &iter, blocksize)) { - btrfs_csum_one_bio_block(fs_info, &bbio->bio, &iter, - sums->sums + index); + for (struct bvec_iter *iter = &bbio->csum_saved_iter; + iter->bi_size; + bio_advance_iter(&bbio->bio, iter, blocksize)) { + btrfs_csum_one_bio_block(fs_info, &bbio->bio, iter, sums->sums + index); index += fs_info->csum_size; } @@ -820,7 +820,7 @@ static void csum_one_bio_work(struct work_struct *work) ASSERT(btrfs_op(&bbio->bio) == BTRFS_MAP_WRITE); ASSERT(bbio->async_csum == true); - csum_one_bio(bbio, &bbio->csum_saved_iter); + csum_one_bio(bbio); complete(&bbio->csum_done); } @@ -850,13 +850,13 @@ int btrfs_csum_one_bio(struct btrfs_bio *bbio, bool async) bbio->sums = sums; btrfs_add_ordered_sum(ordered, sums); + bbio->csum_saved_iter = bio->bi_iter; if (!async) { - csum_one_bio(bbio, &bbio->bio.bi_iter); + csum_one_bio(bbio); return 0; } init_completion(&bbio->csum_done); bbio->async_csum = true; - bbio->csum_saved_iter = bbio->bio.bi_iter; INIT_WORK(&bbio->csum_work, csum_one_bio_work); schedule_work(&bbio->csum_work); return 0; From e54f700d6ae3203221745c168dae20d88f2be4f1 Mon Sep 17 00:00:00 2001 From: Daniel Vacek Date: Wed, 2 Sep 2026 15:56:27 +0200 Subject: [PATCH 17/92] btrfs: use bio::remaining for async checksumming synchronization We can use bio::remaining counter to sync the offloaded checksumming. As a result we can slim down the btrfs_bio structure by 24 bytes and simplify the code a bit. Difference in pahole output: - /* size: 328, cachelines: 6, members: 15 */ + /* size: 304, cachelines: 5, members: 14 */ Moreover this will allow us enabling async checksumming with encryption where we need to checksum the bounce bio instead of our regular one embedded in btrfs_bio. And so we need to extend it's lifetime. This is the preferred way to do so. This also fixes a bug in experimental build where the async checksumming was using the system workqueue instead of fs_info::endio_workers. Fixes: dd57c78aec39 ("btrfs: introduce btrfs_bio::async_csum") Reviewed-by: Qu Wenruo Signed-off-by: Daniel Vacek Signed-off-by: David Sterba --- fs/btrfs/bio.c | 4 ---- fs/btrfs/bio.h | 4 ---- fs/btrfs/file-item.c | 8 +++----- 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/fs/btrfs/bio.c b/fs/btrfs/bio.c index 19b4855969f5..771b7d598aee 100644 --- a/fs/btrfs/bio.c +++ b/fs/btrfs/bio.c @@ -103,7 +103,6 @@ static struct btrfs_bio *btrfs_split_bio(struct btrfs_fs_info *fs_info, bbio->can_use_append = orig_bbio->can_use_append; bbio->is_scrub = orig_bbio->is_scrub; bbio->is_remap = orig_bbio->is_remap; - bbio->async_csum = orig_bbio->async_csum; atomic_inc(&orig_bbio->pending_ios); return bbio; @@ -114,9 +113,6 @@ void btrfs_bio_end_io(struct btrfs_bio *bbio, blk_status_t status) /* Make sure we're already in task context. */ ASSERT(in_task()); - if (bbio->async_csum) - wait_for_completion(&bbio->csum_done); - bbio->bio.bi_status = status; if (bbio->bio.bi_pool == &btrfs_clone_bioset) { struct btrfs_bio *orig_bbio = bbio->private; diff --git a/fs/btrfs/bio.h b/fs/btrfs/bio.h index b7bd377a0162..bbf362b8668b 100644 --- a/fs/btrfs/bio.h +++ b/fs/btrfs/bio.h @@ -58,7 +58,6 @@ struct btrfs_bio { struct btrfs_ordered_extent *ordered; struct btrfs_ordered_sum *sums; struct work_struct csum_work; - struct completion csum_done; struct bvec_iter csum_saved_iter; u64 orig_physical; u64 orig_logical; @@ -93,9 +92,6 @@ struct btrfs_bio { /* Whether the bio is coming from copy_remapped_data_io(). */ bool is_remap:1; - /* Whether the csum generation for data write is async. */ - bool async_csum:1; - /* Whether the bio is written using zone append. */ bool can_use_append:1; diff --git a/fs/btrfs/file-item.c b/fs/btrfs/file-item.c index 2c6c14dfe393..ae1fd4da38d3 100644 --- a/fs/btrfs/file-item.c +++ b/fs/btrfs/file-item.c @@ -819,9 +819,8 @@ static void csum_one_bio_work(struct work_struct *work) struct btrfs_bio *bbio = container_of(work, struct btrfs_bio, csum_work); ASSERT(btrfs_op(&bbio->bio) == BTRFS_MAP_WRITE); - ASSERT(bbio->async_csum == true); csum_one_bio(bbio); - complete(&bbio->csum_done); + bio_endio(&bbio->bio); } /* @@ -855,10 +854,9 @@ int btrfs_csum_one_bio(struct btrfs_bio *bbio, bool async) csum_one_bio(bbio); return 0; } - init_completion(&bbio->csum_done); - bbio->async_csum = true; + bio_inc_remaining(bio); INIT_WORK(&bbio->csum_work, csum_one_bio_work); - schedule_work(&bbio->csum_work); + queue_work(fs_info->endio_workers, &bbio->csum_work); return 0; } From 49c313970a5e7437e6b4ca1be33ec506ffb1843e Mon Sep 17 00:00:00 2001 From: Hemanth Selam Date: Mon, 7 Sep 2026 10:18:01 +0530 Subject: [PATCH 18/92] btrfs: fix typos and repeated words in comments Fix misspellings and repeated words in comments, found with scripts/checkpatch.pl and codespell. Only touches comments, no code changes. Assisted-by: Cursor:claude-opus-5 Signed-off-by: Hemanth Selam Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/block-group.h | 2 +- fs/btrfs/extent-io-tree.c | 2 +- fs/btrfs/fs.c | 2 +- fs/btrfs/raid56.c | 11 ++++------- fs/btrfs/send.c | 2 +- fs/btrfs/transaction.h | 2 +- fs/btrfs/tree-checker.c | 2 +- include/uapi/linux/btrfs_tree.h | 4 ++-- 8 files changed, 12 insertions(+), 15 deletions(-) diff --git a/fs/btrfs/block-group.h b/fs/btrfs/block-group.h index 69d56864d4ba..b349f94cf929 100644 --- a/fs/btrfs/block-group.h +++ b/fs/btrfs/block-group.h @@ -135,7 +135,7 @@ struct btrfs_block_group { u64 global_root_id; u64 remap_bytes; u32 identity_remap_count; - /* The last commited identity_remap_count value of this block group. */ + /* The last committed identity_remap_count value of this block group. */ u32 last_identity_remap_count; /* * The last committed used bytes of this block group, if the above @used diff --git a/fs/btrfs/extent-io-tree.c b/fs/btrfs/extent-io-tree.c index d6df11f6088c..992b8b42bdb4 100644 --- a/fs/btrfs/extent-io-tree.c +++ b/fs/btrfs/extent-io-tree.c @@ -751,7 +751,7 @@ int btrfs_clear_extent_bit_changeset(struct extent_io_tree *tree, u64 start, u64 btrfs_split_delalloc_extent(tree->inode, state, start); /* - * Temporarilly ajdust this state's range to match the + * Temporarily ajdust this state's range to match the * range for which we are clearing bits. */ state->start = start; diff --git a/fs/btrfs/fs.c b/fs/btrfs/fs.c index de160d29dde8..75a1217727a7 100644 --- a/fs/btrfs/fs.c +++ b/fs/btrfs/fs.c @@ -79,7 +79,7 @@ void btrfs_csum_init(struct btrfs_csum_ctx *ctx, u16 csum_type) blake2b_init(&ctx->blake2b, 32); break; default: - /* Checksume type is validated at mount time. */ + /* Checksum type is validated at mount time. */ BUG(); } } diff --git a/fs/btrfs/raid56.c b/fs/btrfs/raid56.c index a5d0ef09d92a..8ec24dbb180f 100644 --- a/fs/btrfs/raid56.c +++ b/fs/btrfs/raid56.c @@ -953,7 +953,7 @@ static void rbio_orig_end_io(struct btrfs_raid_bio *rbio, blk_status_t status) /* * Clear the data bitmap, as the rbio may be cached for later usage. - * do this before before unlock_stripe() so there will be no new bio + * do this before unlock_stripe() so there will be no new bio * for this bio. */ bitmap_clear(&rbio->dbitmap, 0, rbio->stripe_nsectors); @@ -988,7 +988,7 @@ static void rbio_orig_end_io(struct btrfs_raid_bio *rbio, blk_status_t status) * as possible, and only use stripe_sectors as fallback. * * Return NULL if bio_list_only is set but the specified sector has no - * coresponding bio. + * corresponding bio. */ static phys_addr_t *sector_paddrs_in_rbio(struct btrfs_raid_bio *rbio, int stripe_nr, int sector_nr, @@ -1450,10 +1450,7 @@ static int rmw_assemble_write_bios(struct btrfs_raid_bio *rbio, /* We should have at least one data sector. */ ASSERT(bitmap_weight(&rbio->dbitmap, rbio->stripe_nsectors)); - /* - * Reset errors, as we may have errors inherited from from degraded - * write. - */ + /* Reset errors, as we may have errors inherited from degraded write. */ bitmap_clear(rbio->error_bitmap, 0, rbio->nr_sectors); /* @@ -2632,7 +2629,7 @@ static int alloc_rbio_essential_pages(struct btrfs_raid_bio *rbio) return 0; } -/* Return true if the content of the step matches the caclulated one. */ +/* Return true if the content of the step matches the calculated one. */ static bool verify_one_parity_step(struct btrfs_raid_bio *rbio, void *pointers[], unsigned int sector_nr, unsigned int step_nr) diff --git a/fs/btrfs/send.c b/fs/btrfs/send.c index 5c59b9abedcd..c523bf950c89 100644 --- a/fs/btrfs/send.c +++ b/fs/btrfs/send.c @@ -7023,7 +7023,7 @@ static int changed_extent(struct send_ctx *sctx, * get modified or replaced with a new one). Note that deduplication * updates the inode item, but it only changes the iversion (sequence * field in the inode item) of the inode, so if a file is deduplicated - * the same amount of times in both the parent and send snapshots, its + * the same number of times in both the parent and send snapshots, its * iversion becomes the same in both snapshots, whence the inode item is * the same on both snapshots. */ diff --git a/fs/btrfs/transaction.h b/fs/btrfs/transaction.h index 3a57f227b5ed..89153cd22596 100644 --- a/fs/btrfs/transaction.h +++ b/fs/btrfs/transaction.h @@ -288,7 +288,7 @@ do { \ * Call btrfs_abort_transaction() as early as possible when an error condition * is detected, that way the exact stack trace is reported for some errors. * - * Error number must be negative as it encodes wheather it's the first abort. + * Error number must be negative as it encodes whether it's the first abort. */ #define btrfs_abort_transaction(trans, error) \ do { \ diff --git a/fs/btrfs/tree-checker.c b/fs/btrfs/tree-checker.c index 857ded43c3b9..594266c9c116 100644 --- a/fs/btrfs/tree-checker.c +++ b/fs/btrfs/tree-checker.c @@ -228,7 +228,7 @@ static bool can_have_extent_data(struct extent_buffer *leaf, } /* - * The rest are special files, e.g. block/FIFO files, which cannnot + * The rest are special files, e.g. block/FIFO files, which cannot * have any file extent. */ return false; diff --git a/include/uapi/linux/btrfs_tree.h b/include/uapi/linux/btrfs_tree.h index b6ccaf848e4b..47ee52859b45 100644 --- a/include/uapi/linux/btrfs_tree.h +++ b/include/uapi/linux/btrfs_tree.h @@ -230,7 +230,7 @@ * * Stored as an inline ref rather to avoid wasting space on a separate item on * top of the existing extent item. However, unlike the other inline refs, - * there is one one owner ref per extent rather than one per extent. + * there is one owner ref per extent rather than one per extent. * * Because of this, it goes at the front of the list of inline refs, and thus * must have a lower type value than any other inline ref type (to satisfy the @@ -243,7 +243,7 @@ #define BTRFS_EXTENT_DATA_REF_KEY 178 /* - * Obsolete key. Defintion removed in 6.6, value may be reused in the future. + * Obsolete key. Definition removed in 6.6, value may be reused in the future. * * #define BTRFS_EXTENT_REF_V0_KEY 180 */ From 24d966dea969f9ee327de2e85d50eff1e459fbce Mon Sep 17 00:00:00 2001 From: Jeff Layton Date: Tue, 25 Aug 2026 12:04:17 -0400 Subject: [PATCH 19/92] btrfs: split btrfs_insert_delayed_dir_index() into prealloc and commit phases Split btrfs_insert_delayed_dir_index() into three functions using a new btrfs_dir_index_prealloc struct to bundle the pre-allocated resources: - btrfs_prealloc_delayed_dir_index(): allocates the struct and performs the two GFP_NOFS allocations (delayed node + delayed item) that can fail with -ENOMEM. Returns the struct, or ERR_PTR on failure. - btrfs_insert_delayed_dir_index_prealloc(): populates the item data, inserts into the rb-tree, and reserves metadata space. Cannot fail with -ENOMEM since all allocations were done in the prealloc step. - btrfs_free_delayed_dir_index_prealloc(): frees pre-allocated resources when the caller's btree insertion fails. Tolerates NULL. The prealloc is returned as a pointer rather than filled into a caller-provided struct, so that a plain NULL means "no prealloc" and callers do not need a separate flag to track whether one exists. It is consumed (and freed) by either the commit or the free helper, so ownership is unambiguous. The original btrfs_insert_delayed_dir_index() is refactored into a thin wrapper that calls the prealloc and commit functions. This split allows callers to move the fallible memory allocations before the point of no return (the DIR_ITEM btree insertion), so that -ENOMEM can be returned cleanly without aborting the transaction. Assisted-by: LLM Reviewed-by: Qu Wenruo Signed-off-by: Jeff Layton Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/delayed-inode.c | 128 +++++++++++++++++++++++++++++++-------- fs/btrfs/delayed-inode.h | 17 ++++++ 2 files changed, 121 insertions(+), 24 deletions(-) diff --git a/fs/btrfs/delayed-inode.c b/fs/btrfs/delayed-inode.c index db2ffab0941a..21a2d123df14 100644 --- a/fs/btrfs/delayed-inode.c +++ b/fs/btrfs/delayed-inode.c @@ -6,6 +6,7 @@ #include #include +#include #include "ctree.h" #include "fs.h" #include "messages.h" @@ -1469,35 +1470,93 @@ static void btrfs_release_dir_index_item_space(struct btrfs_trans_handle *trans) trans->bytes_reserved -= bytes; } -/* Will return 0, -ENOMEM or -EEXIST (index number collision, unexpected). */ -int btrfs_insert_delayed_dir_index(struct btrfs_trans_handle *trans, - const char *name, int name_len, - struct btrfs_inode *dir, - const struct btrfs_disk_key *disk_key, u8 flags, - u64 index) +/* + * Pre-allocate a delayed node and delayed item for a dir index insertion and + * copy the name into the item. Call this before modifying the btree so that + * ENOMEM can be returned before any on-disk state has changed. + * + * The returned prealloc is consumed by either + * btrfs_insert_delayed_dir_index_prealloc() or + * btrfs_free_delayed_dir_index_prealloc(); it must not be used afterwards. + * + * Returns a prealloc on success, ERR_PTR on allocation failure. + */ +struct btrfs_dir_index_prealloc *btrfs_prealloc_delayed_dir_index(struct btrfs_inode *dir, + const char *name, + int name_len) { + struct btrfs_dir_index_prealloc *prealloc; + struct btrfs_delayed_node *node; + struct btrfs_delayed_item *item; + + prealloc = kzalloc_obj(*prealloc, GFP_NOFS); + if (!prealloc) + return ERR_PTR(-ENOMEM); + + node = btrfs_get_or_create_delayed_node(dir, &prealloc->tracker); + if (IS_ERR(node)) { + kfree(prealloc); + return ERR_CAST(node); + } + + item = btrfs_alloc_delayed_item(sizeof(struct btrfs_dir_item) + name_len, + node, BTRFS_DELAYED_INSERTION_ITEM); + if (!item) { + btrfs_release_delayed_node(node, &prealloc->tracker); + kfree(prealloc); + return ERR_PTR(-ENOMEM); + } + + memcpy(item->data + sizeof(struct btrfs_dir_item), name, name_len); + + prealloc->node = node; + prealloc->item = item; + return prealloc; +} +ALLOW_ERROR_INJECTION(btrfs_prealloc_delayed_dir_index, ERRNO); + +/* + * Free resources from btrfs_prealloc_delayed_dir_index() when the btree + * insertion failed and we will not commit the delayed dir index. Does nothing + * if @prealloc is NULL. + */ +void btrfs_free_delayed_dir_index_prealloc(struct btrfs_trans_handle *trans, + struct btrfs_dir_index_prealloc *prealloc) +{ + if (!prealloc) + return; + + btrfs_release_delayed_item(prealloc->item); + btrfs_release_dir_index_item_space(trans); + btrfs_release_delayed_node(prealloc->node, &prealloc->tracker); + kfree(prealloc); +} + +/* + * Commit a pre-allocated delayed dir index item. @prealloc must have been + * returned by btrfs_prealloc_delayed_dir_index(). This populates the item, + * adds it to the delayed node's rb-tree, and reserves metadata space. It + * cannot fail with ENOMEM. @prealloc is freed here in all cases. + * + * Return 0 or -EEXIST (index number collision, unexpected). + */ +int btrfs_insert_delayed_dir_index_prealloc(struct btrfs_trans_handle *trans, + struct btrfs_inode *dir, + struct btrfs_dir_index_prealloc *prealloc, + const struct btrfs_disk_key *disk_key, + u8 flags, u64 index) +{ + struct btrfs_delayed_node *delayed_node = prealloc->node; + struct btrfs_ref_tracker *tracker = &prealloc->tracker; + struct btrfs_delayed_item *delayed_item = prealloc->item; struct btrfs_fs_info *fs_info = trans->fs_info; const unsigned int leaf_data_size = BTRFS_LEAF_DATA_SIZE(fs_info); - struct btrfs_delayed_node *delayed_node; - struct btrfs_ref_tracker delayed_node_tracker; - struct btrfs_delayed_item *delayed_item; + const int name_len = delayed_item->data_len - sizeof(struct btrfs_dir_item); struct btrfs_dir_item *dir_item; bool reserve_leaf_space; u32 data_len; int ret; - delayed_node = btrfs_get_or_create_delayed_node(dir, &delayed_node_tracker); - if (IS_ERR(delayed_node)) - return PTR_ERR(delayed_node); - - delayed_item = btrfs_alloc_delayed_item(sizeof(*dir_item) + name_len, - delayed_node, - BTRFS_DELAYED_INSERTION_ITEM); - if (!delayed_item) { - ret = -ENOMEM; - goto release_node; - } - delayed_item->index = index; dir_item = (struct btrfs_dir_item *)delayed_item->data; @@ -1506,7 +1565,7 @@ int btrfs_insert_delayed_dir_index(struct btrfs_trans_handle *trans, btrfs_set_stack_dir_data_len(dir_item, 0); btrfs_set_stack_dir_name_len(dir_item, name_len); btrfs_set_stack_dir_flags(dir_item, flags); - memcpy((char *)(dir_item + 1), name, name_len); + /* Name was already copied by btrfs_prealloc_delayed_dir_index(). */ data_len = delayed_item->data_len + sizeof(struct btrfs_item); @@ -1524,7 +1583,9 @@ int btrfs_insert_delayed_dir_index(struct btrfs_trans_handle *trans, if (unlikely(ret)) { btrfs_err(trans->fs_info, "error adding delayed dir index item, name: %.*s, index: %llu, root: %llu, dir: %llu, dir->index_cnt: %llu, delayed_node->index_cnt: %llu, error: %pe", - name_len, name, index, btrfs_root_id(delayed_node->root), + name_len, + (const char *)(dir_item + 1), + index, btrfs_root_id(delayed_node->root), delayed_node->inode_id, dir->index_cnt, delayed_node->index_cnt, ERR_PTR(ret)); btrfs_release_delayed_item(delayed_item); @@ -1562,10 +1623,29 @@ int btrfs_insert_delayed_dir_index(struct btrfs_trans_handle *trans, mutex_unlock(&delayed_node->mutex); release_node: - btrfs_release_delayed_node(delayed_node, &delayed_node_tracker); + /* Must release the node before freeing @tracker's containing struct. */ + btrfs_release_delayed_node(delayed_node, tracker); + kfree(prealloc); return ret; } +/* Return 0, -ENOMEM or -EEXIST (index number collision, unexpected). */ +int btrfs_insert_delayed_dir_index(struct btrfs_trans_handle *trans, + const char *name, int name_len, + struct btrfs_inode *dir, + const struct btrfs_disk_key *disk_key, u8 flags, + u64 index) +{ + struct btrfs_dir_index_prealloc *prealloc; + + prealloc = btrfs_prealloc_delayed_dir_index(dir, name, name_len); + if (IS_ERR(prealloc)) + return PTR_ERR(prealloc); + + return btrfs_insert_delayed_dir_index_prealloc(trans, dir, prealloc, + disk_key, flags, index); +} + static bool btrfs_delete_delayed_insertion_item(struct btrfs_delayed_node *node, u64 index) { diff --git a/fs/btrfs/delayed-inode.h b/fs/btrfs/delayed-inode.h index fc752863f89b..6d12a145489f 100644 --- a/fs/btrfs/delayed-inode.h +++ b/fs/btrfs/delayed-inode.h @@ -121,6 +121,23 @@ int btrfs_insert_delayed_dir_index(struct btrfs_trans_handle *trans, const struct btrfs_disk_key *disk_key, u8 flags, u64 index); +struct btrfs_dir_index_prealloc { + struct btrfs_delayed_node *node; + struct btrfs_ref_tracker tracker; + struct btrfs_delayed_item *item; +}; + +struct btrfs_dir_index_prealloc *btrfs_prealloc_delayed_dir_index(struct btrfs_inode *dir, + const char *name, + int name_len); +void btrfs_free_delayed_dir_index_prealloc(struct btrfs_trans_handle *trans, + struct btrfs_dir_index_prealloc *prealloc); +int btrfs_insert_delayed_dir_index_prealloc(struct btrfs_trans_handle *trans, + struct btrfs_inode *dir, + struct btrfs_dir_index_prealloc *prealloc, + const struct btrfs_disk_key *disk_key, + u8 flags, u64 index); + int btrfs_delete_delayed_dir_index(struct btrfs_trans_handle *trans, struct btrfs_inode *dir, u64 index); From f95420e3068fe62ce659e150f81ddf894b56c8c9 Mon Sep 17 00:00:00 2001 From: Jeff Layton Date: Tue, 25 Aug 2026 12:04:18 -0400 Subject: [PATCH 20/92] btrfs: pre-allocate delayed dir index before btree modification Move the delayed dir index allocation in btrfs_insert_dir_item() before the insert_with_overflow() call that modifies the btree. Previously, the allocations happened after the DIR_ITEM was already inserted, meaning an ENOMEM failure left the btree in a partially-modified state that could only be resolved by aborting the transaction. Add an optional caller-provided btrfs_dir_index_prealloc parameter to btrfs_insert_dir_item(). When non-NULL, ownership of the prealloc transfers to btrfs_insert_dir_item(). When NULL, it allocates internally. All existing callers pass NULL to preserve the current behavior. Since ownership transfers, btrfs_insert_dir_item() must free the prealloc on every path that does not commit it. Route all such exits (including the early path allocation failure) through a common out_free_prealloc label, rather than keying cleanup on need_delayed_index. Remove the btrfs_insert_delayed_dir_index() wrapper, as there are no more callers. Assisted-by: LLM Suggested-by: Qu Wenruo Signed-off-by: Jeff Layton Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/delayed-inode.c | 21 ++------------------ fs/btrfs/delayed-inode.h | 5 ----- fs/btrfs/dir-item.c | 41 ++++++++++++++++++++++++++-------------- fs/btrfs/dir-item.h | 5 +++-- fs/btrfs/inode.c | 2 +- fs/btrfs/transaction.c | 3 +-- 6 files changed, 34 insertions(+), 43 deletions(-) diff --git a/fs/btrfs/delayed-inode.c b/fs/btrfs/delayed-inode.c index 21a2d123df14..1f20148c1fd4 100644 --- a/fs/btrfs/delayed-inode.c +++ b/fs/btrfs/delayed-inode.c @@ -687,7 +687,7 @@ static int btrfs_insert_delayed_item(struct btrfs_trans_handle *trans, /* * For delayed items to insert, we track reserved metadata bytes based * on the number of leaves that we will use. - * See btrfs_insert_delayed_dir_index() and + * See btrfs_insert_delayed_dir_index_prealloc() and * btrfs_delayed_item_reserve_metadata()). */ ASSERT(first_item->bytes_reserved == 0); @@ -1629,23 +1629,6 @@ int btrfs_insert_delayed_dir_index_prealloc(struct btrfs_trans_handle *trans, return ret; } -/* Return 0, -ENOMEM or -EEXIST (index number collision, unexpected). */ -int btrfs_insert_delayed_dir_index(struct btrfs_trans_handle *trans, - const char *name, int name_len, - struct btrfs_inode *dir, - const struct btrfs_disk_key *disk_key, u8 flags, - u64 index) -{ - struct btrfs_dir_index_prealloc *prealloc; - - prealloc = btrfs_prealloc_delayed_dir_index(dir, name, name_len); - if (IS_ERR(prealloc)) - return PTR_ERR(prealloc); - - return btrfs_insert_delayed_dir_index_prealloc(trans, dir, prealloc, - disk_key, flags, index); -} - static bool btrfs_delete_delayed_insertion_item(struct btrfs_delayed_node *node, u64 index) { @@ -1661,7 +1644,7 @@ static bool btrfs_delete_delayed_insertion_item(struct btrfs_delayed_node *node, /* * For delayed items to insert, we track reserved metadata bytes based * on the number of leaves that we will use. - * See btrfs_insert_delayed_dir_index() and + * See btrfs_insert_delayed_dir_index_prealloc() and * btrfs_delayed_item_reserve_metadata()). */ ASSERT(item->bytes_reserved == 0); diff --git a/fs/btrfs/delayed-inode.h b/fs/btrfs/delayed-inode.h index 6d12a145489f..57ba96cfaf9c 100644 --- a/fs/btrfs/delayed-inode.h +++ b/fs/btrfs/delayed-inode.h @@ -115,11 +115,6 @@ struct btrfs_delayed_item { }; void btrfs_init_delayed_root(struct btrfs_delayed_root *delayed_root); -int btrfs_insert_delayed_dir_index(struct btrfs_trans_handle *trans, - const char *name, int name_len, - struct btrfs_inode *dir, - const struct btrfs_disk_key *disk_key, u8 flags, - u64 index); struct btrfs_dir_index_prealloc { struct btrfs_delayed_node *node; diff --git a/fs/btrfs/dir-item.c b/fs/btrfs/dir-item.c index 84f1c64423d3..3a90736915af 100644 --- a/fs/btrfs/dir-item.c +++ b/fs/btrfs/dir-item.c @@ -106,8 +106,11 @@ int btrfs_insert_xattr_item(struct btrfs_trans_handle *trans, * Will return 0 or -ENOMEM */ int btrfs_insert_dir_item(struct btrfs_trans_handle *trans, - const struct fscrypt_str *name, struct btrfs_inode *dir, - const struct btrfs_key *location, u8 type, u64 index) + const struct fscrypt_str *name, + struct btrfs_inode *dir, + const struct btrfs_key *location, u8 type, + u64 index, + struct btrfs_dir_index_prealloc *prealloc) { int ret = 0; int ret2 = 0; @@ -119,17 +122,27 @@ int btrfs_insert_dir_item(struct btrfs_trans_handle *trans, struct btrfs_key key; struct btrfs_disk_key disk_key; u32 data_size; + const bool need_delayed_index = (root != root->fs_info->tree_root); key.objectid = btrfs_ino(dir); key.type = BTRFS_DIR_ITEM_KEY; key.offset = btrfs_name_hash(name->name, name->len); path = btrfs_alloc_path(); - if (!path) - return -ENOMEM; + if (!path) { + ret = -ENOMEM; + goto out_free_prealloc; + } btrfs_cpu_key_to_disk(&disk_key, location); + /* Pre-allocate the delayed dir index before modifying the btree. */ + if (need_delayed_index && !prealloc) { + prealloc = btrfs_prealloc_delayed_dir_index(dir, name->name, name->len); + if (IS_ERR(prealloc)) + return PTR_ERR(prealloc); + } + data_size = sizeof(*dir_item) + name->len; dir_item = insert_with_overflow(trans, root, path, &key, data_size, name->name, name->len); @@ -137,7 +150,7 @@ int btrfs_insert_dir_item(struct btrfs_trans_handle *trans, ret = PTR_ERR(dir_item); if (ret == -EEXIST) goto second_insert; - goto out_free; + goto out_free_prealloc; } if (IS_ENCRYPTED(&dir->vfs_inode)) @@ -154,21 +167,21 @@ int btrfs_insert_dir_item(struct btrfs_trans_handle *trans, write_extent_buffer(leaf, name->name, name_ptr, name->len); second_insert: - /* FIXME, use some real flag for selecting the extra index */ - if (root == root->fs_info->tree_root) { + if (!need_delayed_index) { ret = 0; - goto out_free; + goto out_free_prealloc; } btrfs_release_path(path); - ret2 = btrfs_insert_delayed_dir_index(trans, name->name, name->len, dir, - &disk_key, type, index); -out_free: + ret2 = btrfs_insert_delayed_dir_index_prealloc(trans, dir, prealloc, + &disk_key, type, index); if (ret) return ret; - if (ret2) - return ret2; - return 0; + return ret2; + +out_free_prealloc: + btrfs_free_delayed_dir_index_prealloc(trans, prealloc); + return ret; } static struct btrfs_dir_item *btrfs_lookup_match_dir( diff --git a/fs/btrfs/dir-item.h b/fs/btrfs/dir-item.h index e52174a8baf9..8d22976a0a77 100644 --- a/fs/btrfs/dir-item.h +++ b/fs/btrfs/dir-item.h @@ -13,12 +13,14 @@ struct btrfs_path; struct btrfs_inode; struct btrfs_root; struct btrfs_trans_handle; +struct btrfs_dir_index_prealloc; int btrfs_check_dir_item_collision(struct btrfs_root *root, u64 dir_ino, const struct fscrypt_str *name); int btrfs_insert_dir_item(struct btrfs_trans_handle *trans, const struct fscrypt_str *name, struct btrfs_inode *dir, - const struct btrfs_key *location, u8 type, u64 index); + const struct btrfs_key *location, u8 type, u64 index, + struct btrfs_dir_index_prealloc *prealloc); struct btrfs_dir_item *btrfs_lookup_dir_item(struct btrfs_trans_handle *trans, struct btrfs_root *root, struct btrfs_path *path, u64 dir, @@ -53,5 +55,4 @@ static inline u64 btrfs_name_hash(const char *name, int len) { return crc32c((u32)~1, name, len); } - #endif diff --git a/fs/btrfs/inode.c b/fs/btrfs/inode.c index 54005045e123..eb25ef83b61c 100644 --- a/fs/btrfs/inode.c +++ b/fs/btrfs/inode.c @@ -6905,7 +6905,7 @@ int btrfs_add_link(struct btrfs_trans_handle *trans, return ret; ret = btrfs_insert_dir_item(trans, name, parent_inode, &key, - btrfs_inode_type(inode), index); + btrfs_inode_type(inode), index, NULL); if (ret == -EEXIST || ret == -EOVERFLOW) goto fail_dir_item; else if (unlikely(ret)) { diff --git a/fs/btrfs/transaction.c b/fs/btrfs/transaction.c index 11b38133f737..c1555621ae4e 100644 --- a/fs/btrfs/transaction.c +++ b/fs/btrfs/transaction.c @@ -1891,8 +1891,7 @@ static noinline int create_pending_snapshot(struct btrfs_trans_handle *trans, goto fail; ret = btrfs_insert_dir_item(trans, &fname.disk_name, - parent_inode, &key, BTRFS_FT_DIR, - index); + parent_inode, &key, BTRFS_FT_DIR, index, NULL); if (unlikely(ret)) { btrfs_abort_transaction(trans, ret); goto fail; From 83fc65e09088b62a424ce13d8162f451a1415457 Mon Sep 17 00:00:00 2001 From: Jeff Layton Date: Tue, 25 Aug 2026 12:04:19 -0400 Subject: [PATCH 21/92] btrfs: handle ENOMEM from btrfs_insert_dir_item() without aborting Now that btrfs_insert_dir_item() returns -ENOMEM before modifying the btree (thanks to delayed dir index pre-allocation), callers can handle ENOMEM gracefully instead of aborting the transaction. - btrfs_add_link(): add -ENOMEM to the recoverable errors alongside -EEXIST and -EOVERFLOW. - btrfs_create_new_inode(): on -ENOMEM from btrfs_add_link(), orphan the newly-created inode instead of aborting. The inode item was already written with nlink 1, and discard_new_inode() marks it bad so eviction won't delete it. So clear_nlink() alone is not enough: persist nlink 0 via btrfs_update_inode(), otherwise orphan cleanup would see nlink > 0, drop the orphan item, and leak the inode. Fall back to aborting only if that update also fails. This turns a filesystem-killing abort into a graceful -ENOMEM return for create(), mkdir(), mknod(), symlink(), and link() under memory pressure. Assisted-by: LLM Signed-off-by: Jeff Layton Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/inode.c | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/fs/btrfs/inode.c b/fs/btrfs/inode.c index eb25ef83b61c..cf25615f9b56 100644 --- a/fs/btrfs/inode.c +++ b/fs/btrfs/inode.c @@ -6844,7 +6844,27 @@ int btrfs_create_new_inode(struct btrfs_trans_handle *trans, } else { ret = btrfs_add_link(trans, BTRFS_I(dir), BTRFS_I(inode), name, false, BTRFS_I(inode)->dir_index); - if (unlikely(ret)) { + if (ret == -ENOMEM) { + /* + * Orphan the new inode instead of aborting. The inode + * item was already written with nlink 1, and discard's + * eviction won't delete a bad inode, so nlink 0 must be + * persisted here or orphan cleanup would see nlink > 0, + * drop the orphan item, and leak the inode. + */ + clear_nlink(inode); + /* btrfs_orphan_add() aborts the transaction on failure. */ + ret = btrfs_orphan_add(trans, BTRFS_I(inode)); + if (ret) + goto discard; + ret = btrfs_update_inode(trans, BTRFS_I(inode)); + if (ret) { + btrfs_abort_transaction(trans, ret); + goto discard; + } + ret = -ENOMEM; + goto discard; + } else if (unlikely(ret)) { btrfs_abort_transaction(trans, ret); goto discard; } @@ -6906,7 +6926,7 @@ int btrfs_add_link(struct btrfs_trans_handle *trans, ret = btrfs_insert_dir_item(trans, name, parent_inode, &key, btrfs_inode_type(inode), index, NULL); - if (ret == -EEXIST || ret == -EOVERFLOW) + if (ret == -EEXIST || ret == -EOVERFLOW || ret == -ENOMEM) goto fail_dir_item; else if (unlikely(ret)) { btrfs_abort_transaction(trans, ret); From 1c9fdf06d7c2ab67ea3b9e88ddcc1c3150d5f10c Mon Sep 17 00:00:00 2001 From: Jeff Layton Date: Tue, 25 Aug 2026 12:04:20 -0400 Subject: [PATCH 22/92] btrfs: pre-allocate delayed dir index for non-overwrite rename For rename() without an overwrite target, pre-allocate the delayed dir index before any btree modifications so that ENOMEM can be returned before the source is unlinked from the old directory. Add a prealloc parameter to btrfs_add_link() that allows callers to pass pre-allocated delayed dir index resources. When provided, btrfs_add_link() takes ownership: it either passes the prealloc to btrfs_insert_dir_item() (which commits or frees it), or frees it on early error. All existing callers pass NULL to preserve the current behavior. In btrfs_rename(), when new_inode is NULL (no overwrite), call btrfs_prealloc_delayed_dir_index() before the first btree modification and pass the result through to btrfs_add_link(). If the prealloc fails, -ENOMEM is returned before any btree state has changed. The local prealloc pointer is cleared once ownership passes to btrfs_add_link(), so the out_fail path only frees one we still own. For overwrite rename (new_inode != NULL), the transaction still aborts on ENOMEM since earlier unlink operations have already made irreversible btree modifications. Assisted-by: LLM Signed-off-by: Jeff Layton Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/btrfs_inode.h | 4 +++- fs/btrfs/inode.c | 39 +++++++++++++++++++++++++++++++-------- fs/btrfs/tree-log.c | 4 ++-- 3 files changed, 36 insertions(+), 11 deletions(-) diff --git a/fs/btrfs/btrfs_inode.h b/fs/btrfs/btrfs_inode.h index 89e5e9c0c904..46c62f980c24 100644 --- a/fs/btrfs/btrfs_inode.h +++ b/fs/btrfs/btrfs_inode.h @@ -32,6 +32,7 @@ struct btrfs_trans_handle; struct btrfs_bio; struct btrfs_file_extent; struct btrfs_delayed_node; +struct btrfs_dir_index_prealloc; /* * Since we search a directory based on f_pos (struct dir_context::pos) we have @@ -523,7 +524,8 @@ int btrfs_unlink_inode(struct btrfs_trans_handle *trans, const struct fscrypt_str *name); int btrfs_add_link(struct btrfs_trans_handle *trans, struct btrfs_inode *parent_inode, struct btrfs_inode *inode, - const struct fscrypt_str *name, bool add_backref, u64 index); + const struct fscrypt_str *name, bool add_backref, u64 index, + struct btrfs_dir_index_prealloc *prealloc); int btrfs_delete_subvolume(struct btrfs_inode *dir, struct dentry *dentry); int btrfs_truncate_block(struct btrfs_inode *inode, u64 offset, u64 start, u64 end); diff --git a/fs/btrfs/inode.c b/fs/btrfs/inode.c index cf25615f9b56..c7bbcd11d01e 100644 --- a/fs/btrfs/inode.c +++ b/fs/btrfs/inode.c @@ -6843,7 +6843,7 @@ int btrfs_create_new_inode(struct btrfs_trans_handle *trans, } } else { ret = btrfs_add_link(trans, BTRFS_I(dir), BTRFS_I(inode), name, - false, BTRFS_I(inode)->dir_index); + false, BTRFS_I(inode)->dir_index, NULL); if (ret == -ENOMEM) { /* * Orphan the new inode instead of aborting. The inode @@ -6895,7 +6895,8 @@ int btrfs_create_new_inode(struct btrfs_trans_handle *trans, */ int btrfs_add_link(struct btrfs_trans_handle *trans, struct btrfs_inode *parent_inode, struct btrfs_inode *inode, - const struct fscrypt_str *name, bool add_backref, u64 index) + const struct fscrypt_str *name, bool add_backref, u64 index, + struct btrfs_dir_index_prealloc *prealloc) { int ret = 0; struct btrfs_key key; @@ -6921,11 +6922,13 @@ int btrfs_add_link(struct btrfs_trans_handle *trans, } /* Nothing to clean up yet */ - if (ret) + if (ret) { + btrfs_free_delayed_dir_index_prealloc(trans, prealloc); return ret; + } ret = btrfs_insert_dir_item(trans, name, parent_inode, &key, - btrfs_inode_type(inode), index, NULL); + btrfs_inode_type(inode), index, prealloc); if (ret == -EEXIST || ret == -EOVERFLOW || ret == -ENOMEM) goto fail_dir_item; else if (unlikely(ret)) { @@ -7079,7 +7082,7 @@ static int btrfs_link(struct dentry *old_dentry, struct inode *dir, inode_set_ctime_current(inode); ret = btrfs_add_link(trans, BTRFS_I(dir), BTRFS_I(inode), - &fname.disk_name, true, index); + &fname.disk_name, true, index, NULL); if (ret) goto fail; @@ -8493,14 +8496,14 @@ static int btrfs_rename_exchange(struct inode *old_dir, } ret = btrfs_add_link(trans, BTRFS_I(new_dir), BTRFS_I(old_inode), - new_name, false, old_idx); + new_name, false, old_idx, NULL); if (unlikely(ret)) { btrfs_abort_transaction(trans, ret); goto out_fail; } ret = btrfs_add_link(trans, BTRFS_I(old_dir), BTRFS_I(new_inode), - old_name, false, new_idx); + old_name, false, new_idx, NULL); if (unlikely(ret)) { btrfs_abort_transaction(trans, ret); goto out_fail; @@ -8573,6 +8576,7 @@ static int btrfs_rename(struct mnt_idmap *idmap, struct inode *new_inode = d_inode(new_dentry); struct inode *old_inode = d_inode(old_dentry); struct btrfs_rename_ctx rename_ctx; + struct btrfs_dir_index_prealloc *prealloc = NULL; u64 index = 0; int ret; int ret2; @@ -8696,6 +8700,23 @@ static int btrfs_rename(struct mnt_idmap *idmap, if (ret) goto out_fail; + /* + * When not overwriting an existing entry, pre-allocate the delayed dir + * index now so that ENOMEM is returned before any btree modifications. + * For the overwrite case, too many btree changes have already happened + * by the time btrfs_add_link() is called. + */ + if (!new_inode) { + prealloc = btrfs_prealloc_delayed_dir_index(BTRFS_I(new_dir), + new_fname.disk_name.name, + new_fname.disk_name.len); + if (IS_ERR(prealloc)) { + ret = PTR_ERR(prealloc); + prealloc = NULL; + goto out_fail; + } + } + BTRFS_I(old_inode)->dir_index = 0ULL; if (unlikely(old_ino == BTRFS_FIRST_FREE_OBJECTID)) { /* force full log commit if subvolume involved. */ @@ -8791,7 +8812,8 @@ static int btrfs_rename(struct mnt_idmap *idmap, } ret = btrfs_add_link(trans, BTRFS_I(new_dir), BTRFS_I(old_inode), - &new_fname.disk_name, false, index); + &new_fname.disk_name, false, index, prealloc); + prealloc = NULL; if (unlikely(ret)) { btrfs_abort_transaction(trans, ret); goto out_fail; @@ -8816,6 +8838,7 @@ static int btrfs_rename(struct mnt_idmap *idmap, } } out_fail: + btrfs_free_delayed_dir_index_prealloc(trans, prealloc); if (logs_pinned) { btrfs_end_log_trans(root); btrfs_end_log_trans(dest); diff --git a/fs/btrfs/tree-log.c b/fs/btrfs/tree-log.c index a00094604e54..cfb0b0e9e124 100644 --- a/fs/btrfs/tree-log.c +++ b/fs/btrfs/tree-log.c @@ -1683,7 +1683,7 @@ static noinline int add_inode_ref(struct walk_control *wc) } /* insert our name */ - ret = btrfs_add_link(trans, dir, inode, &name, false, ref_index); + ret = btrfs_add_link(trans, dir, inode, &name, false, ref_index, NULL); if (ret) { btrfs_abort_log_replay(wc, ret, "failed to add link for inode %llu in dir %llu ref_index %llu name %.*s root %llu", @@ -2031,7 +2031,7 @@ static noinline int insert_one_name(struct btrfs_trans_handle *trans, return PTR_ERR(dir); } - ret = btrfs_add_link(trans, dir, inode, name, true, index); + ret = btrfs_add_link(trans, dir, inode, name, true, index, NULL); /* FIXME, put inode into FIXUP list */ From 7d5319be57fea4b06fe1d157f4696ddc1e3927bd Mon Sep 17 00:00:00 2001 From: Usama Arif Date: Fri, 4 Sep 2026 09:41:48 -0700 Subject: [PATCH 23/92] btrfs: zstd: avoid a copy in zstd_decompress_bio() zstd_decompress_bio() gives zstd a sectorsize-sized scratch buffer, and btrfs_decompress_buf2page() then copies the part overlapping the read bio into the destination folios. Every delivered byte is written twice. Instead, choose the output buffer per streaming call. zstd_map_dest() kmaps the current page-bounded segment of the read bio, so zstd writes into the page cache directly. The scratch buffer is kept only for output with no destination: the prefix before a read starting inside a compressed extent, which zstd cannot skip, and gaps left by folios already in the page cache. Varying the output buffer across calls is safe: btrfs uses the default ZSTD_bm_buffered mode, where the sliding window lives in the dstream's internal buffer and the caller's dst is a pure sink. The read bio's iterator must still advance by exactly the bytes delivered, since btrfs_decompress_bio() zero-fills from it; that used to happen inside btrfs_decompress_buf2page() and is now an explicit bio_advance(), made only for output that reached a folio. bio_iter_iovec() exposes at most one base page, so direct output is page-bounded. Compared to the old sectorsize-sized chunks, this can increase stream calls when sectorsize exceeds PAGE_SIZE, but eliminates the extra btrfs copy for output delivered to the read bio; the 64 KiB sectorsize row below shows the copy still wins there. Benchmarked the change in 2-vCPU x86-64 KVM guests (4 KiB pages, RAM disk) using a 64 MiB zstd-compressed file. Results are medians of seven cold-cache reads in each of six interleaved A/B boot pairs; mincore confirmed zero resident pages before every run. Normal sequential reads with readahead produced: sectorsize base patched reduction 4 KiB 8.678 ms 8.004 ms 7.80% 16 KiB 8.216 ms 7.934 ms 3.64% 64 KiB 7.875 ms 7.344 ms 6.88% Random 4 KiB preads at 4 KiB sectorsize, means of six interleaved A/B boot pairs, patched better in all six: base patched gain 264.33 MB/s 272.67 MB/s 3.2% Signed-off-by: Usama Arif Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/zstd.c | 69 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 56 insertions(+), 13 deletions(-) diff --git a/fs/btrfs/zstd.c b/fs/btrfs/zstd.c index 58d9ff76fe07..280ac5273438 100644 --- a/fs/btrfs/zstd.c +++ b/fs/btrfs/zstd.c @@ -589,10 +589,48 @@ int zstd_compress_bio(struct list_head *ws, struct compressed_bio *cb) return ret; } +/* + * Map the destination for the next chunk of output. + * + * @decompressed is the offset of the next output byte inside the fully + * decompressed extent. If that offset has reached the current destination + * segment, its page-bounded bio_vec is kmapped so that zstd can write into the + * page cache directly, and the number of bytes writable there is returned. + * Otherwise @kaddr_ret is set to NULL and the number of bytes to skip before + * that segment is returned. This covers both the initial prefix and gaps in + * the destination bio. + */ +static u32 zstd_map_dest(struct compressed_bio *cb, u32 decompressed, + void **kaddr_ret) +{ + struct bio *orig_bio = &cb->orig_bbio->bio; + struct bio_vec bvec; + u32 bvec_offset; + u32 off; + + bvec = bio_iter_iovec(orig_bio, orig_bio->bi_iter); + /* + * cb->start may underflow, but subtracting that value can still give us + * the correct offset inside the full decompressed extent. + */ + bvec_offset = page_offset(bvec.bv_page) + bvec.bv_offset - cb->start; + + if (decompressed < bvec_offset) { + *kaddr_ret = NULL; + return bvec_offset - decompressed; + } + + off = decompressed - bvec_offset; + ASSERT(off < bvec.bv_len); + *kaddr_ret = bvec_kmap_local(&bvec) + off; + return bvec.bv_len - off; +} + int zstd_decompress_bio(struct list_head *ws, struct compressed_bio *cb) { struct btrfs_fs_info *fs_info = cb_to_fs_info(cb); struct workspace *workspace = list_entry(ws, struct workspace, list); + struct bio *orig_bio = &cb->orig_bbio->bio; struct folio_iter fi; size_t srclen = bio_get_size(&cb->bbio.bio); zstd_dstream *stream; @@ -600,7 +638,6 @@ int zstd_decompress_bio(struct list_head *ws, struct compressed_bio *cb) const unsigned int min_folio_size = btrfs_min_folio_size(fs_info); unsigned long folio_in_index = 0; unsigned long total_folios_in = DIV_ROUND_UP(srclen, min_folio_size); - unsigned long buf_start; unsigned long total_out = 0; bio_first_folio(&fi, &cb->bbio.bio, 0); @@ -624,15 +661,26 @@ int zstd_decompress_bio(struct list_head *ws, struct compressed_bio *cb) workspace->in_buf.pos = 0; workspace->in_buf.size = min_t(size_t, srclen, min_folio_size); - workspace->out_buf.dst = workspace->buf; - workspace->out_buf.pos = 0; - workspace->out_buf.size = fs_info->sectorsize; - - while (1) { + while (orig_bio->bi_iter.bi_size) { size_t ret2; + void *kaddr; + u32 dstlen; + + dstlen = zstd_map_dest(cb, total_out, &kaddr); + if (kaddr) { + workspace->out_buf.dst = kaddr; + workspace->out_buf.size = dstlen; + } else { + workspace->out_buf.dst = workspace->buf; + workspace->out_buf.size = min_t(u32, dstlen, + fs_info->sectorsize); + } + workspace->out_buf.pos = 0; ret2 = zstd_decompress_stream(stream, &workspace->out_buf, &workspace->in_buf); + if (kaddr) + kunmap_local(kaddr); if (unlikely(zstd_is_error(ret2))) { struct btrfs_inode *inode = cb->bbio.inode; @@ -643,14 +691,9 @@ int zstd_decompress_bio(struct list_head *ws, struct compressed_bio *cb) ret = -EIO; goto done; } - buf_start = total_out; total_out += workspace->out_buf.pos; - workspace->out_buf.pos = 0; - - ret = btrfs_decompress_buf2page(workspace->out_buf.dst, - total_out - buf_start, cb, buf_start); - if (ret == 0) - break; + if (kaddr) + bio_advance(orig_bio, workspace->out_buf.pos); if (workspace->in_buf.pos >= srclen) break; From 052434104f1e9e1770a22aa6b724f8dd3df1ff55 Mon Sep 17 00:00:00 2001 From: Zhen Ni Date: Mon, 22 Dec 2025 11:59:42 +0800 Subject: [PATCH 24/92] btrfs: replace is_data_bbio() with is_data_inode() for direct usage After commit 81cea6cd7041 ("btrfs: remove btrfs_bio::fs_info by extracting it from btrfs_bio::inode"), the btrfs_bio::inode field is mandatory for all btrfs_bio allocations. The NULL check is redundant and can be removed. As is_data_bbio() would be a trivial wrapper for is_data_bbio() replace all calls in in bio.c Link: https://lore.kernel.org/linux-btrfs/20251219084316.1164580-1-zhen.ni@easystack.cn Signed-off-by: Zhen Ni Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/bio.c | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/fs/btrfs/bio.c b/fs/btrfs/bio.c index 771b7d598aee..2c6234f6182f 100644 --- a/fs/btrfs/bio.c +++ b/fs/btrfs/bio.c @@ -27,15 +27,9 @@ struct btrfs_failed_bio { atomic_t repair_count; }; -/* Is this a data path I/O that needs storage layer checksum and repair? */ -static inline bool is_data_bbio(const struct btrfs_bio *bbio) -{ - return bbio->inode && is_data_inode(bbio->inode); -} - static bool bbio_has_ordered_extent(const struct btrfs_bio *bbio) { - return is_data_bbio(bbio) && btrfs_op(&bbio->bio) == BTRFS_MAP_WRITE; + return is_data_inode(bbio->inode) && btrfs_op(&bbio->bio) == BTRFS_MAP_WRITE; } /* @@ -356,7 +350,7 @@ static void simple_end_io_work(struct work_struct *work) if (bio_op(bio) == REQ_OP_READ) { /* Metadata reads are checked and repaired by the submitter. */ - if (is_data_bbio(bbio)) + if (is_data_inode(bbio->inode)) return btrfs_check_read_bio(bbio, bbio->bio.bi_private); return btrfs_bio_end_io(bbio, bbio->bio.bi_status); } @@ -390,7 +384,7 @@ static void btrfs_raid56_end_io(struct bio *bio) btrfs_bio_counter_dec(bioc->fs_info); bbio->mirror_num = bioc->mirror_num; - if (bio_op(bio) == REQ_OP_READ && is_data_bbio(bbio)) + if (bio_op(bio) == REQ_OP_READ && is_data_inode(bbio->inode)) btrfs_check_read_bio(bbio, NULL); else btrfs_bio_end_io(bbio, bbio->bio.bi_status); @@ -753,7 +747,7 @@ static bool btrfs_submit_chunk(struct btrfs_bio *bbio, int mirror_num) * our bio to the physical disk location, so we need to save the * original bytenr so we know what we're checksumming. */ - if (bio_op(bio) == REQ_OP_WRITE && is_data_bbio(bbio)) + if (bio_op(bio) == REQ_OP_WRITE && is_data_inode(bbio->inode)) bbio->orig_logical = logical; bbio->can_use_append = btrfs_use_zone_append(bbio); @@ -779,7 +773,7 @@ static bool btrfs_submit_chunk(struct btrfs_bio *bbio, int mirror_num) * Save the iter for the end_io handler and preload the checksums for * data reads. */ - if (bio_op(bio) == REQ_OP_READ && is_data_bbio(bbio)) { + if (bio_op(bio) == REQ_OP_READ && is_data_inode(bbio->inode)) { bbio->saved_iter = bio->bi_iter; ret = btrfs_lookup_bio_sums(bbio); status = errno_to_blk_status(ret); @@ -788,7 +782,7 @@ static bool btrfs_submit_chunk(struct btrfs_bio *bbio, int mirror_num) } if (btrfs_op(bio) == BTRFS_MAP_WRITE) { - if (is_data_bbio(bbio) && bioc && bioc->use_rst) { + if (is_data_inode(bbio->inode) && bioc && bioc->use_rst) { /* * No locking for the list update, as we only add to * the list in the I/O submission path, and list From b2ba99cf6f7ba3cd53fe6bb1b24a4c8cc5d74ba7 Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Tue, 8 Sep 2026 07:47:39 +0930 Subject: [PATCH 25/92] btrfs: use kvmalloc() for b-tree split_item() [BUG] There is a bug report that the kmalloc() call inside split_item() failed with the following call trace, and triggered a transaction abort: kworker/u69:8: page allocation failure: order:4, mode:0x40c40(GFP_NOFS|__GFP_COMP), nodemask=(null) CPU: 3 UID: 0 PID: 1154528 Comm: kworker/u69:8 Not tainted 7.0.2 #1 PREEMPTLAZY Workqueue: events_unbound btrfs_async_reclaim_metadata_space Call Trace: dump_stack_lvl+0x47/0x60 warn_alloc.cold+0x67/0xec __alloc_pages_slowpath.constprop.0+0x9bf/0xed0 __alloc_frozen_pages_noprof+0x1ac/0x1c0 ___kmalloc_large_node+0x9d/0xc0 __kmalloc_noprof+0x17b/0x1f0 split_item+0x9e/0x2e0 btrfs_del_csums+0x285/0x400 __btrfs_free_extent.isra.0+0x6de/0x12b0 __btrfs_run_delayed_refs+0x522/0x10c0 btrfs_run_delayed_refs+0x4d/0x1d0 flush_space+0x34d/0x4e0 do_async_reclaim_metadata_space+0x89/0x1d0 btrfs_async_reclaim_metadata_space+0x44/0x60 process_one_work+0x145/0x230 worker_thread+0x185/0x2e0 kthread+0xca/0x100 ret_from_fork+0x14e/0x200 ret_from_fork_asm+0x11/0x20 BTRFS error (device dm-3 state A): Transaction aborted (error -12) BTRFS: error (device dm-3 state A) in btrfs_del_csums:1053: errno=-12 Out of memory BTRFS info (device dm-3 state EA): forced readonly BTRFS: error (device dm-3 state EA) in do_free_extent_accounting:3168: errno=-12 Out of memory BTRFS error (device dm-3 state EA): failed to run delayed ref for logical 1202913873920 num_bytes 274432 type 184 action 2 ref_mod 1: -12 BTRFS: error (device dm-3 state EA) in btrfs_run_delayed_refs:2247: errno=-12 Out of memory [CAUSE] The kmalloc() call is to allocate a buffer to store the full item. However as shown in the above call trace, the order can be high (4), and since we're using GFP_NOFS, it's impossible to reclaim memory by writing back dirty pages. When there is no physically contiguous memory left, such high order allocation can easily fail, and if such kmalloc() happens in a critical path we can trigger a transaction abort. [FIX] Instead of kmalloc(), which requires physically contiguous pages, use kvmalloc(). There is no special requirement for physically contiguous pages here, we just want virtually contiguous memory as a buffer. Reported-by: xavierbachmeyer182 Link: https://lore.kernel.org/linux-btrfs/250decb0-d940-4fe6-9b54-d06e1b293a1b@suse.com/ Reviewed-by: Johannes Thumshirn Reviewed-by: Daniel Vacek Signed-off-by: Qu Wenruo Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/ctree.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fs/btrfs/ctree.c b/fs/btrfs/ctree.c index 8fe330d81b8f..fef0e49dd918 100644 --- a/fs/btrfs/ctree.c +++ b/fs/btrfs/ctree.c @@ -3943,7 +3943,7 @@ static noinline int split_item(struct btrfs_trans_handle *trans, orig_offset = btrfs_item_offset(leaf, path->slots[0]); item_size = btrfs_item_size(leaf, path->slots[0]); - buf = kmalloc(item_size, GFP_NOFS); + buf = kvmalloc(item_size, GFP_NOFS); if (!buf) return -ENOMEM; @@ -3981,7 +3981,7 @@ static noinline int split_item(struct btrfs_trans_handle *trans, btrfs_mark_buffer_dirty(trans, leaf); BUG_ON(btrfs_leaf_free_space(leaf) < 0); - kfree(buf); + kvfree(buf); return 0; } From 0d8c05fe050ab243489c0b5998e31d7d30003791 Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Tue, 8 Sep 2026 16:45:39 +0930 Subject: [PATCH 26/92] btrfs: tree-log: use kvmalloc() for overwrite_item() The @src_copy buffer utilized inside overwrite_item() can be as large as the nodesize. For an existing btrfs with 64KiB nodesize, it means there is a high chance to fail the kmalloc() call if there is not enough physically contiguous pages. Meanwhile there is really no need for such physically contiguous pages, as we only use that buffer to compare the content of the item. Use kvmalloc() to replace the kmalloc() call. For most cases that kvmalloc() call will be easily fulfilled by regular kmalloc(), but for really large items and large nodes, kvmalloc() will have a much higher chance to get memory allocated. Reviewed-by: Daniel Vacek Reviewed-by: Johannes Thumshirn Signed-off-by: Qu Wenruo Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/tree-log.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fs/btrfs/tree-log.c b/fs/btrfs/tree-log.c index cfb0b0e9e124..7fb476b19eb4 100644 --- a/fs/btrfs/tree-log.c +++ b/fs/btrfs/tree-log.c @@ -503,7 +503,7 @@ static int overwrite_item(struct walk_control *wc) btrfs_release_path(wc->subvol_path); return 0; } - src_copy = kmalloc(item_size, GFP_NOFS); + src_copy = kvmalloc(item_size, GFP_NOFS); if (!src_copy) { btrfs_abort_log_replay(wc, -ENOMEM, "failed to allocate memory for log leaf item"); @@ -514,7 +514,7 @@ static int overwrite_item(struct walk_control *wc) dst_ptr = btrfs_item_ptr_offset(dst_eb, dst_slot); ret = memcmp_extent_buffer(dst_eb, src_copy, dst_ptr, item_size); - kfree(src_copy); + kvfree(src_copy); /* * they have the same contents, just return, this saves * us from cowing blocks in the destination tree and doing From 883a1a260cf84c954bc6295dc052425d51dff923 Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Tue, 8 Sep 2026 16:45:40 +0930 Subject: [PATCH 27/92] btrfs: use kvmalloc() for uncompress_inline() Although btrfs doesn't support inlined extents larger than PAGE_SIZE for bs > ps cases, it's still possible for the experimental bs > ps support to mount a btrfs created on a system with a much larger page size, thus can still hit an inlined extent that is way larger than the current page size. E.g. a compressed inline extent which has 32K compressed size, is created on 64K page sized ARM64 with 64K sectorsize, then mounted on x86_64 with the experimental bs > ps support. In that case, when reading the compressed inline extent, we need to allocate a buffer that is the same size as the compressed inline extent (32K). That kmalloc() call will request physically contiguous memory for that 32K allocation, and if the system has a very fragmented memory space, such allocation can fail. But there is really no reason that we require such buffer to be physically contiguous, so change it to kvmalloc() to reduce the chance of allocation failure for bs > ps cases. And for all bs <= ps cases, the kvmalloc() call will just be fulfilled by kmalloc() so this will not bring any change to the most common cases. Only bs > ps will get the benefit of less memory allocation failure. Reviewed-by: Daniel Vacek Reviewed-by: Johannes Thumshirn Signed-off-by: Qu Wenruo Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/inode.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fs/btrfs/inode.c b/fs/btrfs/inode.c index c7bbcd11d01e..e7046e9a0e70 100644 --- a/fs/btrfs/inode.c +++ b/fs/btrfs/inode.c @@ -7149,7 +7149,7 @@ static noinline int uncompress_inline(struct btrfs_path *path, compress_type = btrfs_file_extent_compression(leaf, item); max_size = btrfs_file_extent_ram_bytes(leaf, item); inline_size = btrfs_file_extent_inline_item_len(leaf, path->slots[0]); - tmp = kmalloc(inline_size, GFP_NOFS); + tmp = kvmalloc(inline_size, GFP_NOFS); if (!tmp) return -ENOMEM; ptr = btrfs_file_extent_inline_start(item); @@ -7170,7 +7170,7 @@ static noinline int uncompress_inline(struct btrfs_path *path, if (max_size < blocksize) folio_zero_range(folio, max_size, blocksize - max_size); - kfree(tmp); + kvfree(tmp); return ret; } From 0f2cb9a9e95f3992e760de2d531c241e32c750eb Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Tue, 8 Sep 2026 16:45:41 +0930 Subject: [PATCH 28/92] btrfs: use kvmalloc() to allocate compression workspace buffer for zlib and zstd With the experimental bs > ps support, the workspace buffer for both zlib and zstd can be as large as 64K, and on 4K page sized systems such kmalloc() calls have a much higher chance to fail, as that requires physically contiguous memory to fulfill such allocation. The same also applies to S390's hardware accelerated path, which requires a buffer size of 4 pages. Meanwhile lzo is already using kvmalloc() for its buffer, and there is no special requirement for any physically contiguous memory anyway. So change the zlib and zstd workspace buffer allocation to use kvmalloc() to reduce the chance of memory allocation failure. Reviewed-by: Daniel Vacek Reviewed-by: Johannes Thumshirn Signed-off-by: Qu Wenruo Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/zlib.c | 10 +++++----- fs/btrfs/zstd.c | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/fs/btrfs/zlib.c b/fs/btrfs/zlib.c index 486b52db583e..e995d0b1452b 100644 --- a/fs/btrfs/zlib.c +++ b/fs/btrfs/zlib.c @@ -49,7 +49,7 @@ void zlib_free_workspace(struct list_head *ws) struct workspace *workspace = list_entry(ws, struct workspace, list); kvfree(workspace->strm.workspace); - kfree(workspace->buf); + kvfree(workspace->buf); kfree(workspace); } @@ -84,13 +84,13 @@ struct list_head *zlib_alloc_workspace(struct btrfs_fs_info *fs_info, unsigned i workspace->level = level; workspace->buf = NULL; if (need_special_buffer(fs_info)) { - workspace->buf = kmalloc(ZLIB_DFLTCC_BUF_SIZE, - __GFP_NOMEMALLOC | __GFP_NORETRY | - __GFP_NOWARN | GFP_NOIO); + workspace->buf = kvmalloc(ZLIB_DFLTCC_BUF_SIZE, + __GFP_NOMEMALLOC | __GFP_NORETRY | + __GFP_NOWARN | GFP_NOIO); workspace->buf_size = ZLIB_DFLTCC_BUF_SIZE; } if (!workspace->buf) { - workspace->buf = kmalloc(fs_info->sectorsize, GFP_KERNEL); + workspace->buf = kvmalloc(fs_info->sectorsize, GFP_KERNEL); workspace->buf_size = fs_info->sectorsize; } if (!workspace->strm.workspace || !workspace->buf) diff --git a/fs/btrfs/zstd.c b/fs/btrfs/zstd.c index 280ac5273438..cc92d0b1b948 100644 --- a/fs/btrfs/zstd.c +++ b/fs/btrfs/zstd.c @@ -373,7 +373,7 @@ void zstd_free_workspace(struct list_head *ws) struct workspace *workspace = list_entry(ws, struct workspace, list); kvfree(workspace->mem); - kfree(workspace->buf); + kvfree(workspace->buf); kfree(workspace); } @@ -391,7 +391,7 @@ struct list_head *zstd_alloc_workspace(struct btrfs_fs_info *fs_info, int level) workspace->req_level = level; workspace->last_used = jiffies; workspace->mem = kvmalloc(workspace->size, GFP_KERNEL | __GFP_NOWARN); - workspace->buf = kmalloc(fs_info->sectorsize, GFP_KERNEL); + workspace->buf = kvmalloc(fs_info->sectorsize, GFP_KERNEL); if (!workspace->mem || !workspace->buf) goto fail; From 9ef5239d53e1f18a038358481d4cc3448e98d652 Mon Sep 17 00:00:00 2001 From: Tal Zussman Date: Mon, 7 Sep 2026 16:19:55 -0400 Subject: [PATCH 29/92] btrfs: tests: rename process_page_range() to process_folio_range() It already operates on folios. No functional change. Reviewed-by: Qu Wenruo Signed-off-by: Tal Zussman Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/tests/extent-io-tests.c | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/fs/btrfs/tests/extent-io-tests.c b/fs/btrfs/tests/extent-io-tests.c index 23459cd4e503..6eb55bfb2bd4 100644 --- a/fs/btrfs/tests/extent-io-tests.c +++ b/fs/btrfs/tests/extent-io-tests.c @@ -18,8 +18,8 @@ #define PROCESS_RELEASE (1U << 1) #define PROCESS_TEST_LOCKED (1U << 2) -static noinline int process_page_range(struct inode *inode, u64 start, u64 end, - unsigned long flags) +static noinline int process_folio_range(struct inode *inode, u64 start, u64 end, + unsigned long flags) { int ret; struct folio_batch fbatch; @@ -221,8 +221,8 @@ static int test_find_delalloc(u32 sectorsize, u32 nodesize) test_start, max_bytes - 1, start, end); goto out_bits; } - if (process_page_range(inode, start, end, - PROCESS_TEST_LOCKED | PROCESS_UNLOCK)) { + if (process_folio_range(inode, start, end, + PROCESS_TEST_LOCKED | PROCESS_UNLOCK)) { test_err("there were unlocked pages in the range"); goto out_bits; } @@ -276,8 +276,8 @@ static int test_find_delalloc(u32 sectorsize, u32 nodesize) test_start, total_dirty - 1, start, end); goto out_bits; } - if (process_page_range(inode, start, end, - PROCESS_TEST_LOCKED | PROCESS_UNLOCK)) { + if (process_folio_range(inode, start, end, + PROCESS_TEST_LOCKED | PROCESS_UNLOCK)) { test_err("pages in range were not all locked"); goto out_bits; } @@ -317,8 +317,8 @@ static int test_find_delalloc(u32 sectorsize, u32 nodesize) test_start, test_start + PAGE_SIZE - 1, start, end); goto out_bits; } - if (process_page_range(inode, start, end, PROCESS_TEST_LOCKED | - PROCESS_UNLOCK)) { + if (process_folio_range(inode, start, end, PROCESS_TEST_LOCKED | + PROCESS_UNLOCK)) { test_err("pages in range were not all locked"); goto out_bits; } @@ -330,8 +330,8 @@ static int test_find_delalloc(u32 sectorsize, u32 nodesize) out: if (locked_page) put_page(locked_page); - process_page_range(inode, 0, total_dirty - 1, - PROCESS_UNLOCK | PROCESS_RELEASE); + process_folio_range(inode, 0, total_dirty - 1, + PROCESS_UNLOCK | PROCESS_RELEASE); iput(inode); out_root_info: btrfs_free_dummy_root(root); From a6f0c4b59ddb252d45e5027fec5c5a929e50f5a8 Mon Sep 17 00:00:00 2001 From: Tal Zussman Date: Mon, 7 Sep 2026 16:19:56 -0400 Subject: [PATCH 30/92] btrfs: tests: convert test_find_delalloc() to use folios This removes the last btrfs callers of find_or_create_page(), find_lock_page(), SetPageDirty(), ClearPageDirty(), and get_page(), and 15 calls to compound_head(). The folio lookups return an ERR_PTR instead of NULL, so adjust the error handling. Update the comments and test messages accordingly. The test still works in PAGE_SIZE units, which relies on the test inode never getting large folios, so assert that the folios are order-0 where that matters. Reviewed-by: Qu Wenruo Signed-off-by: Tal Zussman Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/tests/extent-io-tests.c | 97 ++++++++++++++++---------------- 1 file changed, 49 insertions(+), 48 deletions(-) diff --git a/fs/btrfs/tests/extent-io-tests.c b/fs/btrfs/tests/extent-io-tests.c index 6eb55bfb2bd4..ee8eabac47f1 100644 --- a/fs/btrfs/tests/extent-io-tests.c +++ b/fs/btrfs/tests/extent-io-tests.c @@ -112,8 +112,8 @@ static int test_find_delalloc(u32 sectorsize, u32 nodesize) struct btrfs_root *root = NULL; struct inode *inode = NULL; struct extent_io_tree *tmp; - struct page *page; - struct page *locked_page = NULL; + struct folio *folio; + struct folio *locked_folio = NULL; /* In this test we need at least 2 file extents at its maximum size */ u64 max_bytes = BTRFS_MAX_EXTENT_SIZE; u64 total_dirty = 2 * max_bytes; @@ -152,23 +152,27 @@ static int test_find_delalloc(u32 sectorsize, u32 nodesize) btrfs_extent_io_tree_init(NULL, tmp, IO_TREE_SELFTEST); /* - * First go through and create and mark all of our pages dirty, we pin - * everything to make sure our pages don't get evicted and screw up our + * First go through and create and mark all of our folios dirty, we pin + * everything to make sure our folios don't get evicted and screw up our * test. */ for (pgoff_t index = 0; index < (total_dirty >> PAGE_SHIFT); index++) { - page = find_or_create_page(inode->i_mapping, index, GFP_KERNEL); - if (!page) { - test_err("failed to allocate test page"); - ret = -ENOMEM; + folio = __filemap_get_folio(inode->i_mapping, index, + FGP_LOCK | FGP_ACCESSED | FGP_CREAT, + GFP_KERNEL); + if (IS_ERR(folio)) { + test_err("failed to allocate test folio"); + ret = PTR_ERR(folio); goto out; } - SetPageDirty(page); + /* The ranges below assume page sized folios. */ + ASSERT(folio_order(folio) == 0); + folio_set_dirty(folio); if (index) { - unlock_page(page); + folio_unlock(folio); } else { - get_page(page); - locked_page = page; + folio_get(folio); + locked_folio = folio; } } @@ -179,8 +183,7 @@ static int test_find_delalloc(u32 sectorsize, u32 nodesize) btrfs_set_extent_bit(tmp, 0, sectorsize - 1, EXTENT_DELALLOC, NULL); start = 0; end = start + PAGE_SIZE - 1; - found = find_lock_delalloc_range(inode, page_folio(locked_page), &start, - &end); + found = find_lock_delalloc_range(inode, locked_folio, &start, &end); if (!found) { test_err("should have found at least one delalloc"); goto out_bits; @@ -191,8 +194,8 @@ static int test_find_delalloc(u32 sectorsize, u32 nodesize) goto out_bits; } btrfs_unlock_extent(tmp, start, end, NULL); - unlock_page(locked_page); - put_page(locked_page); + folio_unlock(locked_folio); + folio_put(locked_folio); /* * Test this scenario @@ -201,17 +204,17 @@ static int test_find_delalloc(u32 sectorsize, u32 nodesize) * |--- search ---| */ test_start = SZ_64M; - locked_page = find_lock_page(inode->i_mapping, - test_start >> PAGE_SHIFT); - if (!locked_page) { - test_err("couldn't find the locked page"); + locked_folio = filemap_lock_folio(inode->i_mapping, test_start >> PAGE_SHIFT); + if (IS_ERR(locked_folio)) { + test_err("couldn't find the locked folio"); + locked_folio = NULL; goto out_bits; } + ASSERT(folio_order(locked_folio) == 0); btrfs_set_extent_bit(tmp, sectorsize, max_bytes - 1, EXTENT_DELALLOC, NULL); start = test_start; end = start + PAGE_SIZE - 1; - found = find_lock_delalloc_range(inode, page_folio(locked_page), &start, - &end); + found = find_lock_delalloc_range(inode, locked_folio, &start, &end); if (!found) { test_err("couldn't find delalloc in our range"); goto out_bits; @@ -223,12 +226,12 @@ static int test_find_delalloc(u32 sectorsize, u32 nodesize) } if (process_folio_range(inode, start, end, PROCESS_TEST_LOCKED | PROCESS_UNLOCK)) { - test_err("there were unlocked pages in the range"); + test_err("there were unlocked folios in the range"); goto out_bits; } btrfs_unlock_extent(tmp, start, end, NULL); - /* locked_page was unlocked above */ - put_page(locked_page); + /* locked_folio was unlocked above */ + folio_put(locked_folio); /* * Test this scenario @@ -236,16 +239,16 @@ static int test_find_delalloc(u32 sectorsize, u32 nodesize) * |--- search ---| */ test_start = max_bytes + sectorsize; - locked_page = find_lock_page(inode->i_mapping, test_start >> - PAGE_SHIFT); - if (!locked_page) { - test_err("couldn't find the locked page"); + locked_folio = filemap_lock_folio(inode->i_mapping, test_start >> PAGE_SHIFT); + if (IS_ERR(locked_folio)) { + test_err("couldn't find the locked folio"); + locked_folio = NULL; goto out_bits; } + ASSERT(folio_order(locked_folio) == 0); start = test_start; end = start + PAGE_SIZE - 1; - found = find_lock_delalloc_range(inode, page_folio(locked_page), &start, - &end); + found = find_lock_delalloc_range(inode, locked_folio, &start, &end); if (found) { test_err("found range when we shouldn't have"); goto out_bits; @@ -265,8 +268,7 @@ static int test_find_delalloc(u32 sectorsize, u32 nodesize) btrfs_set_extent_bit(tmp, max_bytes, total_dirty - 1, EXTENT_DELALLOC, NULL); start = test_start; end = start + PAGE_SIZE - 1; - found = find_lock_delalloc_range(inode, page_folio(locked_page), &start, - &end); + found = find_lock_delalloc_range(inode, locked_folio, &start, &end); if (!found) { test_err("didn't find our range"); goto out_bits; @@ -278,36 +280,35 @@ static int test_find_delalloc(u32 sectorsize, u32 nodesize) } if (process_folio_range(inode, start, end, PROCESS_TEST_LOCKED | PROCESS_UNLOCK)) { - test_err("pages in range were not all locked"); + test_err("folios in range were not all locked"); goto out_bits; } btrfs_unlock_extent(tmp, start, end, NULL); /* - * Now to test where we run into a page that is no longer dirty in the + * Now to test where we run into a folio that is no longer dirty in the * range we want to find. */ - page = find_get_page(inode->i_mapping, - (max_bytes + SZ_1M) >> PAGE_SHIFT); - if (!page) { - test_err("couldn't find our page"); + folio = filemap_get_folio(inode->i_mapping, (max_bytes + SZ_1M) >> PAGE_SHIFT); + if (IS_ERR(folio)) { + test_err("couldn't find our folio"); goto out_bits; } - ClearPageDirty(page); - put_page(page); + ASSERT(folio_order(folio) == 0); + folio_clear_dirty(folio); + folio_put(folio); /* We unlocked it in the previous test */ - lock_page(locked_page); + folio_lock(locked_folio); start = test_start; end = start + PAGE_SIZE - 1; /* - * Currently if we fail to find dirty pages in the delalloc range we + * Currently if we fail to find dirty folios in the delalloc range we * will adjust max_bytes down to PAGE_SIZE and then re-search. If * this changes at any point in the future we will need to fix this * tests expected behavior. */ - found = find_lock_delalloc_range(inode, page_folio(locked_page), &start, - &end); + found = find_lock_delalloc_range(inode, locked_folio, &start, &end); if (!found) { test_err("didn't find our range"); goto out_bits; @@ -319,7 +320,7 @@ static int test_find_delalloc(u32 sectorsize, u32 nodesize) } if (process_folio_range(inode, start, end, PROCESS_TEST_LOCKED | PROCESS_UNLOCK)) { - test_err("pages in range were not all locked"); + test_err("folios in range were not all locked"); goto out_bits; } ret = 0; @@ -328,8 +329,8 @@ static int test_find_delalloc(u32 sectorsize, u32 nodesize) dump_extent_io_tree(tmp); btrfs_clear_extent_bit(tmp, 0, total_dirty - 1, (unsigned)-1, NULL); out: - if (locked_page) - put_page(locked_page); + if (locked_folio) + folio_put(locked_folio); process_folio_range(inode, 0, total_dirty - 1, PROCESS_UNLOCK | PROCESS_RELEASE); iput(inode); From 768906912413ca1acbc0144a4863bbda4efc49c7 Mon Sep 17 00:00:00 2001 From: Tal Zussman Date: Mon, 7 Sep 2026 16:19:57 -0400 Subject: [PATCH 31/92] btrfs: tests: use eb folio helpers in extent buffer memory checks dump_eb_and_memory_contents() and verify_eb_and_memory() hardcode one page per folio instead of using get_eb_folio_index() and get_eb_offset_in_folio() like the rest of the extent buffer code. Use the helpers and folio_address(). This removes the last struct page usage in the file. No functional change. The tests only run with sectorsize == PAGE_SIZE, and the test extent buffers are backed by order-0 folios. Assisted-by: Claude:claude-fable-5-1 Reviewed-by: Qu Wenruo Signed-off-by: Tal Zussman Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/tests/extent-io-tests.c | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/fs/btrfs/tests/extent-io-tests.c b/fs/btrfs/tests/extent-io-tests.c index ee8eabac47f1..cd045778400d 100644 --- a/fs/btrfs/tests/extent-io-tests.c +++ b/fs/btrfs/tests/extent-io-tests.c @@ -672,8 +672,9 @@ static void dump_eb_and_memory_contents(struct extent_buffer *eb, void *memory, const char *test_name) { for (int i = 0; i < eb->len; i++) { - struct page *page = folio_page(eb->folios[i >> PAGE_SHIFT], 0); - void *addr = page_address(page) + offset_in_page(i); + const unsigned long idx = get_eb_folio_index(eb, i); + void *addr = folio_address(eb->folios[idx]) + + get_eb_offset_in_folio(eb, i); if (memcmp(addr, memory + i, 1) != 0) { test_err("%s failed", test_name); @@ -688,9 +689,12 @@ static int verify_eb_and_memory(struct extent_buffer *eb, void *memory, const char *test_name) { for (int i = 0; i < (eb->len >> PAGE_SHIFT); i++) { - void *eb_addr = folio_address(eb->folios[i]); + const unsigned long offset = i << PAGE_SHIFT; + const unsigned long idx = get_eb_folio_index(eb, offset); + void *eb_addr = folio_address(eb->folios[idx]) + + get_eb_offset_in_folio(eb, offset); - if (memcmp(memory + (i << PAGE_SHIFT), eb_addr, PAGE_SIZE) != 0) { + if (memcmp(memory + offset, eb_addr, PAGE_SIZE) != 0) { dump_eb_and_memory_contents(eb, memory, test_name); return -EUCLEAN; } From 3c5ff39a3899a03c2aa14217b233f40642e009ca Mon Sep 17 00:00:00 2001 From: Tal Zussman Date: Mon, 7 Sep 2026 16:19:58 -0400 Subject: [PATCH 32/92] btrfs: convert btrfs_compr_pool_scan() to use folios The compression pool holds order-0 folios, but btrfs_compr_pool_scan() walks it as struct page through page->lru. Walk it as folios, matching the other compression pool functions. This removes the last use of page->lru in btrfs and saves a call to compound_head() per freed folio. Reviewed-by: Qu Wenruo Signed-off-by: Tal Zussman Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/compression.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fs/btrfs/compression.c b/fs/btrfs/compression.c index c62b5148d5ac..979b2ffbd8fc 100644 --- a/fs/btrfs/compression.c +++ b/fs/btrfs/compression.c @@ -168,10 +168,10 @@ static unsigned long btrfs_compr_pool_scan(struct shrinker *sh, struct shrink_co spin_unlock(&compr_pool.lock); list_for_each_safe(tmp, next, &remove) { - struct page *page = list_entry(tmp, struct page, lru); + struct folio *folio = list_entry(tmp, struct folio, lru); - ASSERT(page_ref_count(page) == 1); - put_page(page); + ASSERT(folio_ref_count(folio) == 1); + folio_put(folio); } return freed; From d8fe0fda534371ff5838d038b8cc81412a9b92d3 Mon Sep 17 00:00:00 2001 From: Tal Zussman Date: Mon, 7 Sep 2026 16:19:59 -0400 Subject: [PATCH 33/92] btrfs: convert heuristic_collect_sample() to use folios Convert the sampling loop to folios. This removes the last caller of find_get_page() in btrfs and saves a call to compound_head() per sampled page. Document that the lookup is not supposed to fail with an ASSERT(). Reviewed-by: Qu Wenruo Signed-off-by: Tal Zussman Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/compression.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/fs/btrfs/compression.c b/fs/btrfs/compression.c index 979b2ffbd8fc..fa8b92592321 100644 --- a/fs/btrfs/compression.c +++ b/fs/btrfs/compression.c @@ -1488,7 +1488,7 @@ static bool sample_repeated_patterns(struct heuristic_ws *ws) static void heuristic_collect_sample(struct inode *inode, u64 start, u64 end, struct heuristic_ws *ws) { - struct page *page; + struct folio *folio; pgoff_t index, index_end; u32 i, curr_sample_pos; u8 *in_data; @@ -1514,8 +1514,10 @@ static void heuristic_collect_sample(struct inode *inode, u64 start, u64 end, curr_sample_pos = 0; while (index < index_end) { - page = find_get_page(inode->i_mapping, index); - in_data = kmap_local_page(page); + folio = filemap_get_folio(inode->i_mapping, index); + ASSERT(!IS_ERR(folio)); + in_data = kmap_local_folio(folio, + offset_in_folio(folio, (u64)index << PAGE_SHIFT)); /* Handle case where the start is not aligned to PAGE_SIZE */ i = start % PAGE_SIZE; while (i < PAGE_SIZE - SAMPLING_READ_SIZE) { @@ -1529,7 +1531,7 @@ static void heuristic_collect_sample(struct inode *inode, u64 start, u64 end, curr_sample_pos += SAMPLING_READ_SIZE; } kunmap_local(in_data); - put_page(page); + folio_put(folio); index++; } From dcfc00dc024347775d52101c2c3c4501bde486c7 Mon Sep 17 00:00:00 2001 From: Tal Zussman Date: Mon, 7 Sep 2026 16:20:00 -0400 Subject: [PATCH 34/92] btrfs: fix stale function references in compression comments add_ra_bio_pages() was renamed to add_ra_bio_folios(), and btrfs_compress_filemap_get_folio() wraps filemap_get_folio(), not find_get_page(). Update the comments accordingly. Reviewed-by: Qu Wenruo Signed-off-by: Tal Zussman Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/compression.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fs/btrfs/compression.c b/fs/btrfs/compression.c index fa8b92592321..20169d028961 100644 --- a/fs/btrfs/compression.c +++ b/fs/btrfs/compression.c @@ -431,7 +431,7 @@ static noinline int add_ra_bio_folios(struct inode *inode, u64 compressed_end, } /* - * Since add_ra_bio_pages() is always speculative, suppress + * Since add_ra_bio_folios() is always speculative, suppress * allocation warnings. */ masked_constraint_gfp = mapping_gfp_constraint(mapping, constraint_gfp); @@ -960,7 +960,7 @@ bool btrfs_compress_level_valid(unsigned int type, int level) return levels->min_level <= level && level <= levels->max_level; } -/* Wrapper around find_get_page(), with extra error message. */ +/* Wrapper around filemap_get_folio(), with extra error message. */ int btrfs_compress_filemap_get_folio(struct address_space *mapping, u64 start, struct folio **in_folio_ret) { From cf7ded644ed72747748b62dd7eda7e67c2d1b547 Mon Sep 17 00:00:00 2001 From: Tal Zussman Date: Mon, 7 Sep 2026 16:20:01 -0400 Subject: [PATCH 35/92] btrfs: use folios for reading super blocks from the block device btrfs_read_disk_super() and the zoned super block log comparison go through read_cache_page_gfp() and page_address(), and btrfs_release_disk_super() recovers the page with virt_to_page(). Use mapping_read_folio_gfp(), folio_address(), and virt_to_folio() instead. This removes the last callers of read_cache_page_gfp() and put_page() in btrfs. Compute the super block address with offset_in_folio() as write_dev_supers() does, rather than assuming it is at the start of the page. Assisted-by: Claude:claude-fable-5-1 Reviewed-by: Qu Wenruo Signed-off-by: Tal Zussman Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/volumes.c | 16 +++++++--------- fs/btrfs/zoned.c | 12 ++++++------ 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/fs/btrfs/volumes.c b/fs/btrfs/volumes.c index 1fad1aa716de..7c040f22dbc3 100644 --- a/fs/btrfs/volumes.c +++ b/fs/btrfs/volumes.c @@ -1358,16 +1358,14 @@ int btrfs_open_devices(struct btrfs_fs_devices *fs_devices, void btrfs_release_disk_super(struct btrfs_super_block *super) { - struct page *page = virt_to_page(super); - - put_page(page); + folio_put(virt_to_folio(super)); } struct btrfs_super_block *btrfs_read_disk_super(struct block_device *bdev, int copy_num, bool drop_cache) { struct btrfs_super_block *super; - struct page *page; + struct folio *folio; u64 bytenr, bytenr_orig; struct address_space *mapping = bdev->bd_mapping; int ret; @@ -1388,7 +1386,7 @@ struct btrfs_super_block *btrfs_read_disk_super(struct block_device *bdev, ASSERT(copy_num == 0); /* - * Drop the page of the primary superblock, so later read will + * Drop the folio of the primary superblock, so later read will * always read from the device. */ invalidate_inode_pages2_range(mapping, bytenr >> PAGE_SHIFT, @@ -1396,12 +1394,12 @@ struct btrfs_super_block *btrfs_read_disk_super(struct block_device *bdev, } filemap_invalidate_lock_shared(mapping); - page = read_cache_page_gfp(mapping, bytenr >> PAGE_SHIFT, GFP_NOFS); + folio = mapping_read_folio_gfp(mapping, bytenr >> PAGE_SHIFT, GFP_NOFS); filemap_invalidate_unlock_shared(mapping); - if (IS_ERR(page)) - return ERR_CAST(page); + if (IS_ERR(folio)) + return ERR_CAST(folio); - super = page_address(page); + super = folio_address(folio) + offset_in_folio(folio, bytenr); if (btrfs_super_magic(super) != BTRFS_MAGIC || btrfs_super_bytenr(super) != bytenr_orig) { btrfs_release_disk_super(super); diff --git a/fs/btrfs/zoned.c b/fs/btrfs/zoned.c index 08a15465a087..a1ef8caaacda 100644 --- a/fs/btrfs/zoned.c +++ b/fs/btrfs/zoned.c @@ -123,24 +123,24 @@ static int sb_write_pointer(struct block_device *bdev, struct blk_zone *zones, } else if (full[0] && full[1]) { /* Compare two super blocks */ struct address_space *mapping = bdev->bd_mapping; - struct page *page[BTRFS_NR_SB_LOG_ZONES]; struct btrfs_super_block *super[BTRFS_NR_SB_LOG_ZONES]; for (int i = 0; i < BTRFS_NR_SB_LOG_ZONES; i++) { u64 zone_end = (zones[i].start + zones[i].capacity) << SECTOR_SHIFT; u64 bytenr = ALIGN_DOWN(zone_end, BTRFS_SUPER_INFO_SIZE) - BTRFS_SUPER_INFO_SIZE; + struct folio *folio; filemap_invalidate_lock_shared(mapping); - page[i] = read_cache_page_gfp(mapping, - bytenr >> PAGE_SHIFT, GFP_NOFS); + folio = mapping_read_folio_gfp(mapping, bytenr >> PAGE_SHIFT, + GFP_NOFS); filemap_invalidate_unlock_shared(mapping); - if (IS_ERR(page[i])) { + if (IS_ERR(folio)) { if (i == 1) btrfs_release_disk_super(super[0]); - return PTR_ERR(page[i]); + return PTR_ERR(folio); } - super[i] = page_address(page[i]); + super[i] = folio_address(folio) + offset_in_folio(folio, bytenr); } if (btrfs_super_generation(super[0]) > From d8fb1db2dba2b0a23d8952cf79ba4f95a32000da Mon Sep 17 00:00:00 2001 From: Tal Zussman Date: Wed, 9 Sep 2026 13:10:48 -0400 Subject: [PATCH 36/92] btrfs: use u64 for the page indices in heuristic_collect_sample() index and index_end are derived from the u64 start and end offsets, and index is shifted back into a byte offset for offset_in_folio(), which needs a cast to u64 to be safe on 32-bit. Make them u64 instead so the cast goes away. They still fit pgoff_t where they are passed to filemap_get_folio(), as they came from a valid file offset. Signed-off-by: Tal Zussman Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/compression.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fs/btrfs/compression.c b/fs/btrfs/compression.c index 20169d028961..228d1cdd7c29 100644 --- a/fs/btrfs/compression.c +++ b/fs/btrfs/compression.c @@ -1489,7 +1489,7 @@ static void heuristic_collect_sample(struct inode *inode, u64 start, u64 end, struct heuristic_ws *ws) { struct folio *folio; - pgoff_t index, index_end; + u64 index, index_end; u32 i, curr_sample_pos; u8 *in_data; @@ -1517,7 +1517,7 @@ static void heuristic_collect_sample(struct inode *inode, u64 start, u64 end, folio = filemap_get_folio(inode->i_mapping, index); ASSERT(!IS_ERR(folio)); in_data = kmap_local_folio(folio, - offset_in_folio(folio, (u64)index << PAGE_SHIFT)); + offset_in_folio(folio, index << PAGE_SHIFT)); /* Handle case where the start is not aligned to PAGE_SIZE */ i = start % PAGE_SIZE; while (i < PAGE_SIZE - SAMPLING_READ_SIZE) { From 4ef2c403d24c33eb772d484e45ded5c0d915a7ec Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Wed, 9 Sep 2026 12:26:30 +0100 Subject: [PATCH 37/92] btrfs: increment extent count once when logging extents during fast fsync In btrfs_log_changed_extents() we increment the extent count twice, and we then fallback to a transaction commit if the count reaches a threshold of 32K. However we increment the count twice, which is confusing and pointless. So increment the count only once and reduce the threshold to half (16K). Reviewed-by: Qu Wenruo Signed-off-by: Filipe Manana Signed-off-by: David Sterba --- fs/btrfs/tree-log.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/fs/btrfs/tree-log.c b/fs/btrfs/tree-log.c index 7fb476b19eb4..1d9dfb63fc5a 100644 --- a/fs/btrfs/tree-log.c +++ b/fs/btrfs/tree-log.c @@ -5350,7 +5350,7 @@ static int btrfs_log_changed_extents(struct btrfs_trans_handle *trans, * have a bunch of extents we just want to commit since it will * be faster. */ - if (++num > 32768) { + if (++num > SZ_16K) { list_del_init(&tree->modified_extents); ret = -EFBIG; goto process; @@ -5368,7 +5368,6 @@ static int btrfs_log_changed_extents(struct btrfs_trans_handle *trans, refcount_inc(&em->refs); em->flags |= EXTENT_FLAG_LOGGING; list_add_tail(&em->list, &extents); - num++; } list_sort(NULL, &extents, extent_cmp); From 7a881b1139bb34c48cddea20dae9f53b2289de78 Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Fri, 11 Sep 2026 11:56:29 +0100 Subject: [PATCH 38/92] btrfs: tree-checker: cache accessor return value in CHECK_FE_ALIGNED() We are calling an accessor for a file extent item multiple times in the CHECK_FE_ALIGNED() macro, even when we don't find a corruption (once in the if statement's expression and then once again in the expression for the return value). This adds extra runtime overhead (which is critical since the tree checker runs against every extent buffer when it's read or before persisting it) and increases the module's size. So cache the accessor's return value in a variable and use it, reducing runtime, object size and making the source code shorter too. Also avoid repeating twice the IS_ALIGNED() computation. Before: $ size fs/btrfs/btrfs.ko text data bss dec hex filename 2073340 217928 15624 2306892 23334c fs/btrfs/btrfs.ko After: $ size fs/btrfs/btrfs.ko text data bss dec hex filename 2073076 217928 15624 2306628 233244 fs/btrfs/btrfs.ko Also running the following fsstress test and capturing the runtime of check_extent_data_item() (the only caller of CHECK_FE_ALIGNED()) in nanoseconds (using bpftrace), showed the following runtime improvements: Test: mkfs.btrfs -f /dev/nullb0 mount /dev/nullb0 /mnt fsstress -w -p 8 -n 5000 -s 12345 -d /mnt umount /mnt Before: Count: 2033671 Range: 0.000 - 1336060.000; Mean: 679.642; Median: 656.000; Stddev: 2031.323 Percentiles: 90th: 850.000; 95th: 909.000; 99th: 1272.000 0.000 - 6.647: 11 | 6.647 - 28.241: 36 | 28.241 - 110.807: 176 | 110.807 - 426.512: 38012 # 426.512 - 1633.663: 1983372 ##################################################### 1633.663 - 6249.399: 8315 | 6249.399 - 23898.422: 3243 | 23898.422 - 91382.340: 153 | 91382.340 - 349418.114: 36 | 349418.114 - 1336060.000: 11 | After: Count: 2092797 Range: 0.000 - 1677284.000; Mean: 627.650; Median: 617.000; Stddev: 1848.010 Percentiles: 90th: 809.000; 95th: 859.000; 99th: 1209.000 0.000 - 6.823: 18 | 6.823 - 29.602: 82 | 29.602 - 118.702: 416 | 118.702 - 467.232: 515774 ################# 467.232 - 1830.549: 1565599 ##################################################### 1830.549 - 7163.339: 5297 | 7163.339 - 28023.237: 4186 | 28023.237 - 109619.427: 166 | 109619.427 - 428793.471: 25 | 428793.471 - 1677284.000: 4 | Reviewed-by: Qu Wenruo Signed-off-by: Filipe Manana Signed-off-by: David Sterba --- fs/btrfs/tree-checker.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/fs/btrfs/tree-checker.c b/fs/btrfs/tree-checker.c index 594266c9c116..0f4f1b347c9e 100644 --- a/fs/btrfs/tree-checker.c +++ b/fs/btrfs/tree-checker.c @@ -108,13 +108,14 @@ static void file_extent_err(const struct extent_buffer *eb, int slot, */ #define CHECK_FE_ALIGNED(leaf, slot, fi, name, alignment) \ ({ \ - if (unlikely(!IS_ALIGNED(btrfs_file_extent_##name((leaf), (fi)), \ - (alignment)))) \ + const u64 val = btrfs_file_extent_##name((leaf), (fi)); \ + const bool not_aligned = !IS_ALIGNED(val, (alignment)); \ + \ + if (unlikely(not_aligned)) \ file_extent_err((leaf), (slot), \ "invalid %s for file extent, have %llu, should be aligned to %u", \ - (#name), btrfs_file_extent_##name((leaf), (fi)), \ - (alignment)); \ - (!IS_ALIGNED(btrfs_file_extent_##name((leaf), (fi)), (alignment))); \ + (#name), val, (alignment)); \ + not_aligned; \ }) static u64 file_extent_end(struct extent_buffer *leaf, From 5a593fe11e9f08bb743a13a283e71da971cbffbc Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Sun, 13 Sep 2026 20:59:15 +0930 Subject: [PATCH 39/92] btrfs: cleanup and rename submit_extent_folio() Cleanup submit_extent_folio() by: - Remove @size parameter Since commit b2e743927fdd ("btrfs: make btrfs_do_readpage() to do block-by-block read"), all callers are passing sectorsize as @size, so there is no need for such parameter. Furthermore since we only write one block at a time, there is no need for a while() loop, nor the advance of various local variables. - Update the comments on the parameter list * @disk_bytenr is shared for both read and write * rename @page to @folio - Update the return value to return 0 or error Since we won't queue multiple blocks anyway, there is no point in returning the queued bytes. It makes more sense to return an error code, although the only error code will be -EUCLEAN for writes. - Rename submit_extent_folio() to submit_one_block() - Rename submit_one_sector() to submit_write_sector() - Update the error message to utilize the new returned error code inside submit_write_sector() Reviewed-by: Boris Burkov Signed-off-by: Qu Wenruo Signed-off-by: David Sterba --- fs/btrfs/extent_io.c | 187 +++++++++++++++++++------------------------ 1 file changed, 81 insertions(+), 106 deletions(-) diff --git a/fs/btrfs/extent_io.c b/fs/btrfs/extent_io.c index a221b63bdb20..55e9144d4759 100644 --- a/fs/btrfs/extent_io.c +++ b/fs/btrfs/extent_io.c @@ -110,14 +110,14 @@ struct btrfs_bio_ctrl { * make the decision when submitting the bio. * * The pattern between do_readpage(), submit_one_bio() and - * submit_extent_folio() is quite subtle, so tracking this is tricky. + * submit_one_block() is quite subtle, so tracking this is tricky. * * As we process extent E, we might submit a bio with existing built up * extents before adding E to a new bio, or we might just add E to the * bio. As a result, E's generation could apply to the current bio or * to the next one, so we need to be careful to update the bio_ctrl's * generation with E's only when we are sure E is added to bio_ctrl->bbio - * in submit_extent_folio(). + * in submit_one_block(). * * See the comment in btrfs_lookup_bio_sums() for more detail on the * need for this optimization. @@ -797,118 +797,95 @@ static int alloc_new_bio(struct btrfs_inode *inode, } /* - * @disk_bytenr: logical bytenr where the write will be - * @page: page to add to the bio - * @size: portion of page that we want to write to - * @pg_offset: offset of the new bio or to check whether we are adding - * a contiguous page to the previous one + * @disk_bytenr: logical bytenr where the read/write will be + * @folio: the folio the block belongs to + * @pg_offset: the offset inside the folio * @read_em_generation: generation of the extent_map we are submitting * (only used for read) * - * The will either add the page into the existing @bio_ctrl->bbio, or allocate a + * This will either add the block into the existing @bio_ctrl->bbio, or allocate a * new one in @bio_ctrl->bbio. * The mirror number for this IO should already be initialized in * @bio_ctrl->mirror_num. * - * Return the number of bytes that are queued into a bio. - * If the returned bytes is smaller than @size, it means we hit a critical error - * for data write, where there is no ordered extent for the range. + * Return 0 if the block is queued or submitted. + * Return <0 for error. */ -static unsigned int submit_extent_folio(struct btrfs_bio_ctrl *bio_ctrl, - u64 disk_bytenr, struct folio *folio, - size_t size, unsigned long pg_offset, - u64 read_em_generation) +static int submit_one_block(struct btrfs_bio_ctrl *bio_ctrl, + u64 disk_bytenr, struct folio *folio, + unsigned long pg_offset, u64 read_em_generation) { struct btrfs_inode *inode = folio_to_inode(folio); + const struct btrfs_fs_info *fs_info = inode->root->fs_info; + const u32 blocksize = fs_info->sectorsize; loff_t file_offset = folio_pos(folio) + pg_offset; - unsigned int queued = 0; - ASSERT(pg_offset + size <= folio_size(folio)); + ASSERT(pg_offset + blocksize <= folio_size(folio)); ASSERT(bio_ctrl->end_io_func); if (bio_ctrl->bbio && !btrfs_bio_is_contig(bio_ctrl, disk_bytenr, file_offset)) submit_one_bio(bio_ctrl); - do { - u32 len = size; +again: + /* Allocate new bio if needed */ + if (!bio_ctrl->bbio) { + int ret; - /* Allocate new bio if needed */ - if (!bio_ctrl->bbio) { - int ret; + ret = alloc_new_bio(inode, bio_ctrl, disk_bytenr, file_offset); + if (ret < 0) + return ret; + } - ret = alloc_new_bio(inode, bio_ctrl, disk_bytenr, file_offset); - if (ret < 0) - break; - } + if (!bio_add_folio(&bio_ctrl->bbio->bio, folio, blocksize, pg_offset)) { + /* bio full: move on to a new one */ + submit_one_bio(bio_ctrl); + goto again; + } - /* Cap to the current ordered extent boundary if there is one. */ - if (len > bio_ctrl->len_to_oe_boundary) { - ASSERT(bio_ctrl->compress_type == BTRFS_COMPRESS_NONE); - ASSERT(is_data_inode(inode)); - len = bio_ctrl->len_to_oe_boundary; - } + /* + * Now that the folio is definitely added to the bio, include its + * generation in the max generation calculation. + */ + bio_ctrl->generation = max(bio_ctrl->generation, read_em_generation); + bio_ctrl->next_file_offset += blocksize; - if (!bio_add_folio(&bio_ctrl->bbio->bio, folio, len, pg_offset)) { - /* bio full: move on to a new one */ - submit_one_bio(bio_ctrl); - continue; - } - /* - * Now that the folio is definitely added to the bio, include its - * generation in the max generation calculation. - */ - bio_ctrl->generation = max(bio_ctrl->generation, read_em_generation); - bio_ctrl->next_file_offset += len; + if (bio_ctrl->wbc) + wbc_account_cgroup_owner(bio_ctrl->wbc, folio, blocksize); - if (bio_ctrl->wbc) - wbc_account_cgroup_owner(bio_ctrl->wbc, folio, len); + /* + * len_to_oe_boundary defaults to U32_MAX, which isn't folio or sector + * aligned. alloc_new_bio() then sets it to the end of our ordered + * extent for writes into zoned devices. + * + * When len_to_oe_boundary is tracking an ordered extent, the + * len_to_oe_boundary should follow that OE and never go beyond the max + * extent size (128MiB). + * + * When len_to_oe_boundary is U32_MAX, decreasing the length by + * blocksize will never make it reach 0, thus skipping the later + * submit_one_bio() call. So if len_to_oe_boundary() is not tracking + * an OE, do not decrease it. + * + * It's pretty hard to make a bio sized U32_MAX, but it can happen when + * the page cache is able to feed us contiguous folios for large + * extents. + */ + if (bio_ctrl->len_to_oe_boundary != U32_MAX) + bio_ctrl->len_to_oe_boundary -= blocksize; - size -= len; - pg_offset += len; - disk_bytenr += len; - file_offset += len; - queued += len; - - /* - * len_to_oe_boundary defaults to U32_MAX, which isn't folio or - * sector aligned. alloc_new_bio() then sets it to the end of - * our ordered extent for writes into zoned devices. - * - * When len_to_oe_boundary is tracking an ordered extent, we - * trust the ordered extent code to align things properly, and - * the check above to cap our write to the ordered extent - * boundary is correct. - * - * When len_to_oe_boundary is U32_MAX, the cap above would - * result in a 4095 byte IO for the last folio right before - * we hit the bio limit of UINT_MAX. bio_add_folio() has all - * the checks required to make sure we don't overflow the bio, - * and we should just ignore len_to_oe_boundary completely - * unless we're using it to track an ordered extent. - * - * It's pretty hard to make a bio sized U32_MAX, but it can - * happen when the page cache is able to feed us contiguous - * folios for large extents. - */ - if (bio_ctrl->len_to_oe_boundary != U32_MAX) - bio_ctrl->len_to_oe_boundary -= len; - - /* Ordered extent boundary: move on to a new bio. */ - if (bio_ctrl->len_to_oe_boundary == 0) - submit_one_bio(bio_ctrl); - /* - * If we have accumulated decent amount of IO, send it to the - * block layer so that IO can run while we are accumulating - * more folios to write. - */ - else if (bio_ctrl->wbc && - bio_ctrl->bbio->bio.bi_iter.bi_size >= - inode->root->fs_info->writeback_bio_size) - submit_one_bio(bio_ctrl); - - } while (size); - return queued; + /* Ordered extent boundary: move on to a new bio. */ + if (bio_ctrl->len_to_oe_boundary == 0) + submit_one_bio(bio_ctrl); + /* + * If we have accumulated decent amount of IO, send it to the block + * layer so that IO can run while we are accumulating more folios to + * write. + */ + else if (bio_ctrl->wbc && + bio_ctrl->bbio->bio.bi_iter.bi_size >= fs_info->writeback_bio_size) + submit_one_bio(bio_ctrl); + return 0; } static int attach_extent_buffer_folio(struct extent_buffer *eb, @@ -1092,7 +1069,6 @@ static int btrfs_do_readpage(struct folio *folio, struct extent_map **em_cached, u64 disk_bytenr; u64 block_start; u64 em_gen; - unsigned int queued; ASSERT(IS_ALIGNED(cur, fs_info->sectorsize)); if (cur >= last_byte) { @@ -1206,10 +1182,9 @@ static int btrfs_do_readpage(struct folio *folio, struct extent_map **em_cached, if (force_bio_submit) submit_one_bio(bio_ctrl); - queued = submit_extent_folio(bio_ctrl, disk_bytenr, folio, blocksize, - pg_offset, em_gen); + ret = submit_one_block(bio_ctrl, disk_bytenr, folio, pg_offset, em_gen); /* Read submission should not fail. */ - ASSERT(queued == blocksize); + ASSERT(ret == 0); } return 0; } @@ -1830,10 +1805,10 @@ static struct btrfs_ordered_extent *get_oe_from_bbio(const struct btrfs_bio *bbi * * Caller should make sure filepos < i_size and handle filepos >= i_size case. */ -static int submit_one_sector(struct btrfs_inode *inode, - struct folio *folio, - u64 filepos, struct btrfs_bio_ctrl *bio_ctrl, - loff_t i_size) +static int submit_write_sector(struct btrfs_inode *inode, + struct folio *folio, + u64 filepos, struct btrfs_bio_ctrl *bio_ctrl, + loff_t i_size) { struct btrfs_fs_info *fs_info = inode->root->fs_info; struct btrfs_ordered_extent *oe; @@ -1841,7 +1816,7 @@ static int submit_one_sector(struct btrfs_inode *inode, u64 disk_bytenr; u64 extent_offset; const u32 sectorsize = fs_info->sectorsize; - unsigned int queued; + int ret; ASSERT(IS_ALIGNED(filepos, sectorsize)); @@ -1904,17 +1879,17 @@ static int submit_one_sector(struct btrfs_inode *inode, */ ASSERT(folio_test_writeback(folio)); - queued = submit_extent_folio(bio_ctrl, disk_bytenr, folio, - sectorsize, filepos - folio_pos(folio), 0); - if (unlikely(queued < sectorsize)) { + ret = submit_one_block(bio_ctrl, disk_bytenr, folio, + offset_in_folio(folio, filepos), 0); + if (unlikely(ret < 0)) { btrfs_folio_clear_writeback(fs_info, folio, filepos, sectorsize); btrfs_mark_ordered_io_finished(inode, filepos, fs_info->sectorsize, false); btrfs_err_rl(fs_info, - "failed to queue sector for root %lld ino %llu filepos %llu", + "failed to queue sector for root %lld ino %llu filepos %llu: %pe", btrfs_root_id(inode->root), - btrfs_ino(inode), filepos); - return -EUCLEAN; + btrfs_ino(inode), filepos, ERR_PTR(ret)); + return ret; } return 0; } @@ -1999,7 +1974,7 @@ static noinline_for_stack int extent_writepage_io(struct btrfs_inode *inode, btrfs_folio_clear_dirty(fs_info, folio, cur, fs_info->sectorsize); continue; } - ret = submit_one_sector(inode, folio, cur, bio_ctrl, i_size); + ret = submit_write_sector(inode, folio, cur, bio_ctrl, i_size); if (unlikely(ret < 0)) { if (!found_error) found_error = ret; From 44edf5160b3479877fb8f76b4ebe72af676138ed Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Thu, 10 Sep 2026 11:35:52 +0930 Subject: [PATCH 40/92] btrfs: fix off-by-one end related to inode_need_compress() In most cases btrfs uses @end as the inclusive end bytenr for a range, and this applies to inode_need_compress(). However we have several sites not following the inclusive bytenr: - run_delalloc_inline() Which assigned @blocksize as @end for inode_need_compress() This makes inode_need_compress() always skip the disk_i_size check. - heuristic_collect_sample() Which assigned "start + BTRFS_MAX_UNCOMPRESSED" to @end, which is the exclusive bytenr. Neither is really causing any real problem, as heuristic_collect_sample() has proper checks to avoid reading anything beyond @end, and the sampling read size is 16 bytes, so it has enough headroom to handle that off-by-one problem. But still I do not like anything out of the common scheme, so fix the off-by-one @end for both call sites, and add extra ASSERT()s to catch such unaligned parameters. Reviewed-by: Boris Burkov Signed-off-by: Qu Wenruo Signed-off-by: David Sterba --- fs/btrfs/compression.c | 15 +++++++-------- fs/btrfs/inode.c | 5 ++++- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/fs/btrfs/compression.c b/fs/btrfs/compression.c index 228d1cdd7c29..7c1c018dbd6f 100644 --- a/fs/btrfs/compression.c +++ b/fs/btrfs/compression.c @@ -1488,11 +1488,14 @@ static bool sample_repeated_patterns(struct heuristic_ws *ws) static void heuristic_collect_sample(struct inode *inode, u64 start, u64 end, struct heuristic_ws *ws) { + const u32 blocksize = BTRFS_I(inode)->root->fs_info->sectorsize; struct folio *folio; u64 index, index_end; u32 i, curr_sample_pos; u8 *in_data; + ASSERT(IS_ALIGNED(start, blocksize) && IS_ALIGNED(end + 1, blocksize)); + /* * Compression handles the input data by chunks of 128KiB * (defined by BTRFS_MAX_UNCOMPRESSED) @@ -1502,18 +1505,14 @@ static void heuristic_collect_sample(struct inode *inode, u64 start, u64 end, * MAX_SAMPLE_SIZE - calculated under assumption that heuristic will * process no more than BTRFS_MAX_UNCOMPRESSED at a time. */ - if (end - start > BTRFS_MAX_UNCOMPRESSED) - end = start + BTRFS_MAX_UNCOMPRESSED; + if (end + 1 - start > BTRFS_MAX_UNCOMPRESSED) + end = start + BTRFS_MAX_UNCOMPRESSED - 1; index = start >> PAGE_SHIFT; index_end = end >> PAGE_SHIFT; - /* Don't miss unaligned end */ - if (!PAGE_ALIGNED(end)) - index_end++; - curr_sample_pos = 0; - while (index < index_end) { + while (index <= index_end) { folio = filemap_get_folio(inode->i_mapping, index); ASSERT(!IS_ERR(folio)); in_data = kmap_local_folio(folio, @@ -1522,7 +1521,7 @@ static void heuristic_collect_sample(struct inode *inode, u64 start, u64 end, i = start % PAGE_SIZE; while (i < PAGE_SIZE - SAMPLING_READ_SIZE) { /* Don't sample any garbage from the last page */ - if (start > end - SAMPLING_READ_SIZE) + if (start > end + 1 - SAMPLING_READ_SIZE) break; memcpy(&ws->sample[curr_sample_pos], &in_data[i], SAMPLING_READ_SIZE); diff --git a/fs/btrfs/inode.c b/fs/btrfs/inode.c index e7046e9a0e70..2a32072849cb 100644 --- a/fs/btrfs/inode.c +++ b/fs/btrfs/inode.c @@ -730,6 +730,9 @@ static inline int inode_need_compress(struct btrfs_inode *inode, u64 start, u64 end, bool check_inline) { struct btrfs_fs_info *fs_info = inode->root->fs_info; + const u32 blocksize = fs_info->sectorsize; + + ASSERT(IS_ALIGNED(start, blocksize) && IS_ALIGNED(end + 1, blocksize)); if (unlikely(!btrfs_inode_can_compress(inode))) { DEBUG_WARN("BTRFS: unexpected compression for ino %llu", btrfs_ino(inode)); @@ -2331,7 +2334,7 @@ static int run_delalloc_inline(struct btrfs_inode *inode, struct folio *locked_f btrfs_check_folio_write_protected(locked_folio); if (btrfs_inode_can_compress(inode) && - inode_need_compress(inode, 0, blocksize, true)) { + inode_need_compress(inode, 0, blocksize - 1, true)) { if (inode->defrag_compress > 0 && inode->defrag_compress < BTRFS_NR_COMPRESS_TYPES) { compress_type = inode->defrag_compress; From 70fc94d2e0ba38debe47ce6704ca033531a66419 Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Thu, 10 Sep 2026 11:35:53 +0930 Subject: [PATCH 41/92] btrfs: simplify heuristic_collect_sample() to handle large folios better Currently heuristic_collect_sample() is purely page size based, and it has a lot of extra handling just inside the page. However we already have large folio support, there is no need to look up the same folio repeatedly. Simplify the handling by: - Use @cur as the iterator instead of page index - Handle the sample copying on a per-folio basis Although kmap_local_folio() requires an offset to handle HIGHMEM page mapping, we have rejected large folios for HIGHMEM systems completely. So we can safely handle all sample copying inside the folio in one go. - Remove unnecessary unaligned range handling All the range passed in should be block aligned, thus there is no need to handle cases where sample crosses the block boundary. Reviewed-by: Boris Burkov Signed-off-by: Qu Wenruo Signed-off-by: David Sterba --- fs/btrfs/compression.c | 40 ++++++++++++++++------------------------ 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/fs/btrfs/compression.c b/fs/btrfs/compression.c index 7c1c018dbd6f..ad90e14032db 100644 --- a/fs/btrfs/compression.c +++ b/fs/btrfs/compression.c @@ -1489,10 +1489,8 @@ static void heuristic_collect_sample(struct inode *inode, u64 start, u64 end, struct heuristic_ws *ws) { const u32 blocksize = BTRFS_I(inode)->root->fs_info->sectorsize; - struct folio *folio; - u64 index, index_end; - u32 i, curr_sample_pos; - u8 *in_data; + u64 cur = start; + u32 curr_sample_pos = 0; ASSERT(IS_ALIGNED(start, blocksize) && IS_ALIGNED(end + 1, blocksize)); @@ -1508,33 +1506,27 @@ static void heuristic_collect_sample(struct inode *inode, u64 start, u64 end, if (end + 1 - start > BTRFS_MAX_UNCOMPRESSED) end = start + BTRFS_MAX_UNCOMPRESSED - 1; - index = start >> PAGE_SHIFT; - index_end = end >> PAGE_SHIFT; + while (cur < end) { + struct folio *folio; + void *in_data; + u64 next_pos; - curr_sample_pos = 0; - while (index <= index_end) { - folio = filemap_get_folio(inode->i_mapping, index); + folio = filemap_get_folio(inode->i_mapping, cur >> PAGE_SHIFT); + /* All folios inside the range should exist and be locked. */ ASSERT(!IS_ERR(folio)); - in_data = kmap_local_folio(folio, - offset_in_folio(folio, index << PAGE_SHIFT)); - /* Handle case where the start is not aligned to PAGE_SIZE */ - i = start % PAGE_SIZE; - while (i < PAGE_SIZE - SAMPLING_READ_SIZE) { - /* Don't sample any garbage from the last page */ - if (start > end + 1 - SAMPLING_READ_SIZE) - break; - memcpy(&ws->sample[curr_sample_pos], &in_data[i], - SAMPLING_READ_SIZE); - i += SAMPLING_INTERVAL; - start += SAMPLING_INTERVAL; + next_pos = min_t(u64, end + 1, folio_next_pos(folio)); + in_data = kmap_local_folio(folio, 0); + + for (; cur < next_pos; cur += SAMPLING_INTERVAL) { + memcpy(&ws->sample[curr_sample_pos], + in_data + offset_in_folio(folio, cur), + SAMPLING_READ_SIZE); curr_sample_pos += SAMPLING_READ_SIZE; } kunmap_local(in_data); folio_put(folio); - - index++; + cur = next_pos; } - ws->sample_size = curr_sample_pos; } From a6e5521ecc5e7129f1e4f4391ae4c24a75062d30 Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Wed, 16 Sep 2026 17:11:51 +0100 Subject: [PATCH 42/92] btrfs: add missing unlikely to a couple error checks during sys chunk array validation It's unexpected to find errors during sys chunk array validation and all checks use the unlikely tag except for two of them, so add it. Reviewed-by: Qu Wenruo Signed-off-by: Filipe Manana Signed-off-by: David Sterba --- fs/btrfs/disk-io.c | 2 +- fs/btrfs/tree-checker.c | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/fs/btrfs/disk-io.c b/fs/btrfs/disk-io.c index 94a7e9059a7b..21b72c90bd90 100644 --- a/fs/btrfs/disk-io.c +++ b/fs/btrfs/disk-io.c @@ -2387,7 +2387,7 @@ static int validate_sys_chunk_array(const struct btrfs_fs_info *fs_info, } ret = btrfs_check_chunk_valid(fs_info, NULL, chunk, key.offset, sectorsize); - if (ret < 0) + if (unlikely(ret < 0)) return ret; cur += btrfs_chunk_item_size(num_stripes); } diff --git a/fs/btrfs/tree-checker.c b/fs/btrfs/tree-checker.c index 0f4f1b347c9e..9cd79d97b9b5 100644 --- a/fs/btrfs/tree-checker.c +++ b/fs/btrfs/tree-checker.c @@ -1121,9 +1121,9 @@ int btrfs_check_chunk_valid(const struct btrfs_fs_info *fs_info, return -EUCLEAN; } - if (!remapped && - !valid_stripe_count(type & BTRFS_BLOCK_GROUP_PROFILE_MASK, - num_stripes, sub_stripes)) { + if (unlikely(!remapped && + !valid_stripe_count(type & BTRFS_BLOCK_GROUP_PROFILE_MASK, + num_stripes, sub_stripes))) { chunk_err(fs_info, leaf, chunk, logical, "invalid num_stripes:sub_stripes %u:%u for profile %llu", num_stripes, sub_stripes, From a85b9336d551d4204678baf9f850feae1440c817 Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Wed, 16 Sep 2026 17:20:52 +0100 Subject: [PATCH 43/92] btrfs: remove redundant eb generation check in btrfs_buffer_uptodate() It's pointless to check if the extent buffer's generation does not match the value of 'parent_transid' because if it does, then we have already entered the previous if statement and returned from the function. Reviewed-by: Qu Wenruo Signed-off-by: Filipe Manana Signed-off-by: David Sterba --- fs/btrfs/disk-io.c | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/fs/btrfs/disk-io.c b/fs/btrfs/disk-io.c index 21b72c90bd90..161a7b9bb27a 100644 --- a/fs/btrfs/disk-io.c +++ b/fs/btrfs/disk-io.c @@ -125,15 +125,13 @@ int btrfs_buffer_uptodate(struct extent_buffer *eb, u64 parent_transid, return 1; } - if (btrfs_header_generation(eb) != parent_transid) { - btrfs_err_rl(eb->fs_info, + btrfs_err_rl(eb->fs_info, "parent transid verify failed on logical %llu mirror %u wanted %llu found %llu", - eb->start, eb->read_mirror, - parent_transid, btrfs_header_generation(eb)); - clear_extent_buffer_uptodate(eb); - return 0; - } - return 1; + eb->start, eb->read_mirror, + parent_transid, btrfs_header_generation(eb)); + clear_extent_buffer_uptodate(eb); + + return 0; } static bool btrfs_supported_super_csum(u16 csum_type) From fa9194e3ebcbdc1ca16cc4faa99932569efb870b Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Wed, 16 Sep 2026 17:36:11 +0100 Subject: [PATCH 44/92] btrfs: remove duplicate error message when writing super blocks If the total error count is greater the maximum allowed number of errors, we print exactly the same error message twice. Remove one of the messages. Reviewed-by: Qu Wenruo Signed-off-by: Filipe Manana Signed-off-by: David Sterba --- fs/btrfs/disk-io.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/fs/btrfs/disk-io.c b/fs/btrfs/disk-io.c index 161a7b9bb27a..a8535e309b57 100644 --- a/fs/btrfs/disk-io.c +++ b/fs/btrfs/disk-io.c @@ -4228,8 +4228,6 @@ int write_all_supers(struct btrfs_trans_handle *trans) total_errors++; } if (unlikely(total_errors > max_errors)) { - btrfs_err(fs_info, "%d errors while writing supers", - total_errors); mutex_unlock(&fs_info->fs_devices->device_list_mutex); /* FUA is masked off if unsupported and can't be the reason */ From 3bb5ed05c1ead17a12456d77591c773e1032c16a Mon Sep 17 00:00:00 2001 From: Boris Burkov Date: Wed, 16 Sep 2026 15:17:24 -0700 Subject: [PATCH 45/92] btrfs: keep unused block groups queued when a pass fails Once any block_group sets ret!=0 in the main loop of btrfs_delete_unused_bgs(), the check if (ret || btrfs_mixed_space_info(space_info)) { btrfs_put_block_group(block_group); continue; } skips the rest of the unused bgs while unlinking them from fs_info->unused_bgs. There is no "level triggered" re-queueing of empty block groups onto fs_info->unused_bgs so it is possible to leak quite a bit of space this way and unless we happen to get a balance or re-use/re-empty one of these bgs, they are leaked for good, which can lead to a spurious enospc later. While I have observed such leaked blocked groups that are empty but not on the unused_bgs list on production systems, I have not observed that it is definitely due to this issue. I also reproduced this behavior by injecting an ENOSPC error from btrfs_start_trans_remove_block_group which can also fail with ENOMEM, so this feels like a legitimate injection point. To fix it, instead of checking ret in the loop, just break out of the loop when ret != 0. Also, link the bg to the retry list at the individual failure sites so that the failing bg is not leaked. Assisted-by: LLM (reproducer/error injection) Reviewed-by: Qu Wenruo Signed-off-by: Boris Burkov Signed-off-by: David Sterba --- fs/btrfs/block-group.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/fs/btrfs/block-group.c b/fs/btrfs/block-group.c index ee182369254c..2eb09c9901c9 100644 --- a/fs/btrfs/block-group.c +++ b/fs/btrfs/block-group.c @@ -1612,7 +1612,7 @@ void btrfs_delete_unused_bgs(struct btrfs_fs_info *fs_info) space_info = block_group->space_info; - if (ret || btrfs_mixed_space_info(space_info)) { + if (btrfs_mixed_space_info(space_info)) { btrfs_put_block_group(block_group); continue; } @@ -1727,6 +1727,7 @@ void btrfs_delete_unused_bgs(struct btrfs_fs_info *fs_info) ret = inc_block_group_ro(block_group, false); up_write(&space_info->groups_sem); if (ret < 0) { + btrfs_link_bg_list(block_group, &retry_list); ret = 0; goto next; } @@ -1749,6 +1750,7 @@ void btrfs_delete_unused_bgs(struct btrfs_fs_info *fs_info) block_group->start); if (IS_ERR(trans)) { btrfs_dec_block_group_ro(block_group); + btrfs_link_bg_list(block_group, &retry_list); ret = PTR_ERR(trans); goto next; } @@ -1759,6 +1761,7 @@ void btrfs_delete_unused_bgs(struct btrfs_fs_info *fs_info) */ if (!clean_pinned_extents(trans, block_group)) { btrfs_dec_block_group_ro(block_group); + btrfs_link_bg_list(block_group, &retry_list); goto end_trans; } @@ -1845,6 +1848,8 @@ void btrfs_delete_unused_bgs(struct btrfs_fs_info *fs_info) next: btrfs_put_block_group(block_group); spin_lock(&fs_info->unused_bgs_lock); + if (ret) + break; } list_splice_tail(&retry_list, &fs_info->unused_bgs); spin_unlock(&fs_info->unused_bgs_lock); From 220c44b1b90fbff746472cbb3dde778b02afbef9 Mon Sep 17 00:00:00 2001 From: Boris Burkov Date: Tue, 15 Sep 2026 10:58:31 -0700 Subject: [PATCH 46/92] btrfs: pre-flush reflink source before taking inode locks Consider the following sketch of a shell script: dd if=/dev/urandom of=/mnt/src bs=1M count=4096 cp --reflink=always /mnt/src /mnt/dst & sleep 0.1 # let the clone reach its flush time dd if=/mnt/src of=/dev/null bs=4K count=1 iflag=direct The current logic in reflink ensures the existence and stability of both the src and destination inode by locking them both and then flushing all dirty pages / ordered_extents under the inode lock. This blocks concurrent usage by even readers of the inode locks, like direct reads or seeks for the duration of writeback on the entirety of the two files. We observe this particular contention frequently in the Meta fleet. It is also a relatively common pattern to write the src file, then reflink it while it is still dirty, so that typical path naturally hits this contention. This workload motivates a relatively simple optimization: trigger the unavoidable src flushing outside the locked region. Therefore, do an optimistic btrfs_wait_ordered_range() without the locks while relying on the calls to btrfs_wait_ordered_range() under the locks in btrfs_remap_file_range_prep() to ensure correctness. In the worst case with concurrent writes while unlocked, we can end up doing the writeback twice, which I think is a reasonable price to pay to avoid victimizing innocent readers in a common case. With and without this patch, the above reproducer runs the reflink in the same ~0.5s on my system. Without the patch, the direct read blocks for basically the full duration of the reflink while with the patch it returns in a few milliseconds. Reviewed-by: Filipe Manana Signed-off-by: Boris Burkov Signed-off-by: David Sterba --- fs/btrfs/reflink.c | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/fs/btrfs/reflink.c b/fs/btrfs/reflink.c index d2a4101912bd..24c0b226bc31 100644 --- a/fs/btrfs/reflink.c +++ b/fs/btrfs/reflink.c @@ -833,6 +833,17 @@ static noinline int btrfs_clone_files(struct file *file, struct file *file_src, return 0; } +static u64 calc_remap_wb_len(struct btrfs_inode *src_inode, loff_t off, loff_t len, + unsigned int remap_flags) +{ + const u32 bs = src_inode->root->fs_info->sectorsize; + const loff_t isize = i_size_read(&src_inode->vfs_inode); + + if (len == 0 && !(remap_flags & REMAP_FILE_DEDUP)) + return ALIGN(isize, bs) - ALIGN_DOWN(off, bs); + return ALIGN(len, bs); +} + static int btrfs_remap_file_range_prep(struct file *file_in, loff_t pos_in, struct file *file_out, loff_t pos_out, loff_t *len, unsigned int remap_flags) @@ -876,10 +887,7 @@ static int btrfs_remap_file_range_prep(struct file *file_in, loff_t pos_in, * not for the ordered extents to complete. We need to wait for them * to complete so that new file extent items are in the fs tree. */ - if (*len == 0 && !(remap_flags & REMAP_FILE_DEDUP)) - wb_len = ALIGN(inode_in->vfs_inode.i_size, bs) - ALIGN_DOWN(pos_in, bs); - else - wb_len = ALIGN(*len, bs); + wb_len = calc_remap_wb_len(inode_in, pos_in, *len, remap_flags); /* * Workaround to make sure NOCOW buffered write reach disk as NOCOW. @@ -930,6 +938,7 @@ loff_t btrfs_remap_file_range(struct file *src_file, loff_t off, struct btrfs_inode *src_inode = BTRFS_I(file_inode(src_file)); struct btrfs_inode *dst_inode = BTRFS_I(file_inode(dst_file)); bool same_inode = dst_inode == src_inode; + u64 wb_start, wb_len; int ret; if (btrfs_is_shutdown(src_inode->root->fs_info)) @@ -938,6 +947,17 @@ loff_t btrfs_remap_file_range(struct file *src_file, loff_t off, if (remap_flags & ~(REMAP_FILE_DEDUP | REMAP_FILE_ADVISORY)) return -EINVAL; + /* + * Optimistically write out the src inode before taking locks. + * Consistency is properly ensured by the btrfs_wait_ordered_range() + * inside btrfs_remap_file_range_prep(). + */ + wb_start = ALIGN_DOWN(off, src_inode->root->fs_info->sectorsize); + wb_len = calc_remap_wb_len(src_inode, off, len, remap_flags); + ret = btrfs_wait_ordered_range(src_inode, wb_start, wb_len); + if (ret < 0) + return ret; + if (same_inode) { btrfs_inode_lock(src_inode, BTRFS_ILOCK_MMAP); } else { From e0c9d01770ef56792fbb2947128e5f43f7d49c61 Mon Sep 17 00:00:00 2001 From: Boris Burkov Date: Tue, 15 Sep 2026 10:58:32 -0700 Subject: [PATCH 47/92] btrfs: skip unlocked reflink source flush if the inode has writers The optimistic unlocked flushing is counter-productive if there is an active writer dirtying pages, as it results in doubly writing back any pages that get dirtied again before the locked flush. Therefore, pessimize it slightly and don't do the unlocked flushing if we have outstanding writable file descriptors or memory mappings. Since the inode isn't locked, this is only advisory, any new mapping or fd that sneaks in after the check but before the locking can still cause extra writeback. But it trivially helps quite a bit in simple cases with a long open file descriptor or mapping. I tested this with two similar workloads. One was keeping and fd open and doing continuous writes into the src file while cloning it and doing lseek on it and the other was slightly meaner, constantly opening and closing the fd to try to race this check. Workload 1 shows a huge regression in write amplification and writers and readers who experience userspace wall time stalls of 100ms+ with just the pre-flush, but recovers to normal with this check. Workload 2 is a smaller regression which is mostly helped but not fully resolved with this check. So we must weigh this regression against the benefit of the unlocked flush in the allegedly more common simpler case. Workload 1: fd kept open, 60s of clone/write/lseek looping. kernel clones clone mean s device MiB/clone writer stalls reader stalls for-next 564 0.095 119 10 10 pre-flush (p1) 179 0.338 405 94 93 skip (p1+p2) 540 0.100 116 9 9 Workload 2: same writer, but closing and reopening the fd around each 64 MiB write with some random sleeping to encourage racing. kernel clones clone mean s device MiB/clone writer stalls reader stalls for-next 740 0.070 83 9 9 pre-flush (p1) 255 0.224 246 27 26 skip (p1+p2) 518 0.107 121 11 11 Reviewed-by: Filipe Manana Signed-off-by: Boris Burkov Signed-off-by: David Sterba --- fs/btrfs/reflink.c | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/fs/btrfs/reflink.c b/fs/btrfs/reflink.c index 24c0b226bc31..8e37706b98c3 100644 --- a/fs/btrfs/reflink.c +++ b/fs/btrfs/reflink.c @@ -950,13 +950,19 @@ loff_t btrfs_remap_file_range(struct file *src_file, loff_t off, /* * Optimistically write out the src inode before taking locks. * Consistency is properly ensured by the btrfs_wait_ordered_range() - * inside btrfs_remap_file_range_prep(). + * inside btrfs_remap_file_range_prep(). This optimization causes + * redundant writeback if there is a concurrent writer to the src file + * so try to catch anyone holding the file open for writes and skip + * the optimization in that case. */ wb_start = ALIGN_DOWN(off, src_inode->root->fs_info->sectorsize); wb_len = calc_remap_wb_len(src_inode, off, len, remap_flags); - ret = btrfs_wait_ordered_range(src_inode, wb_start, wb_len); - if (ret < 0) - return ret; + if (!inode_is_open_for_write(&src_inode->vfs_inode) && + !mapping_writably_mapped(src_inode->vfs_inode.i_mapping)) { + ret = btrfs_wait_ordered_range(src_inode, wb_start, wb_len); + if (ret < 0) + return ret; + } if (same_inode) { btrfs_inode_lock(src_inode, BTRFS_ILOCK_MMAP); From cf02c310d018c7e5363b80dfd68ffdf159f3c914 Mon Sep 17 00:00:00 2001 From: Boris Burkov Date: Tue, 15 Sep 2026 10:58:33 -0700 Subject: [PATCH 48/92] btrfs: downgrade the reflink source inode lock for tree walking When doing a reflink, we are not writing to the src inode or modifying it in any way, so it is counter-intuitive to have to lock it exclusively. This has the cost of blocking other shared lock users of the src file for the duration of any new writeback since the optimistic pre-flush and all the actual reflinking. So we would like to downgrade the lock to shared as much as possible. There are two important gotchas to do with direct io that must be addressed to make this idea work. The first gotcha is that dio writes that come in under EOF try to take inode->i_rwsem shared, which would result in messed up views of the extents in the reflinked file. Therefore, add "being used as a reflink src" as another inode flag exception for the dio write shared tests that already exist to check for other dangerous conditions. In order to ensure that this flag is visible to the dio writer, reflink must first take i_rwsem exclusive, then set the flag, then downgrade to shared. The second is that dio reads increment inode->i_dio_count which blocks inode_dio_wait() in __generic_remap_file_range_prep(). Therefore, we must remain exclusive to dio reads through finishing the prep or risk starving the clone on a stream of dio readers. There is still O(extents) work in btrfs_clone() which can be done without holding the lock exclusive, so we should downgrade to shared and admit dio readers (and other shared lock users like lseek) after we are finished with inode_dio_wait(). By running concurrent clones and lseeks or dio reads on a src file with 32k extents on a vm, I can demonstrate 100ms+ stalls on the readers without this patch and they go away entirely with the patch. Reviewed-by: Filipe Manana Signed-off-by: Boris Burkov Signed-off-by: David Sterba --- fs/btrfs/btrfs_inode.h | 6 ++++++ fs/btrfs/direct-io.c | 7 +++++++ fs/btrfs/reflink.c | 17 +++++++++++++++-- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/fs/btrfs/btrfs_inode.h b/fs/btrfs/btrfs_inode.h index 46c62f980c24..03c00c99099d 100644 --- a/fs/btrfs/btrfs_inode.h +++ b/fs/btrfs/btrfs_inode.h @@ -99,6 +99,12 @@ enum { * range). */ BTRFS_INODE_COW_WRITE_ERROR, + /* + * Set by reflink while it holds the source's VFS inode lock shared. + * Direct IO writes also take that lock shared; one that finds this bit + * set after acquiring it must retry with the exclusive lock. + */ + BTRFS_INODE_REFLINK_SRC, /* * Indicate this is a directory that points to a subvolume for which * there is no root reference item. That's a case like the following: diff --git a/fs/btrfs/direct-io.c b/fs/btrfs/direct-io.c index 3075d7992713..ed3914257d39 100644 --- a/fs/btrfs/direct-io.c +++ b/fs/btrfs/direct-io.c @@ -913,6 +913,13 @@ ssize_t btrfs_direct_write(struct kiocb *iocb, struct iov_iter *from) goto relock; } + if ((ilock_flags & BTRFS_ILOCK_SHARED) && + test_bit(BTRFS_INODE_REFLINK_SRC, &BTRFS_I(inode)->runtime_flags)) { + btrfs_inode_unlock(BTRFS_I(inode), ilock_flags); + ilock_flags &= ~BTRFS_ILOCK_SHARED; + goto relock; + } + ret = generic_write_checks(iocb, from); if (ret <= 0) { btrfs_inode_unlock(BTRFS_I(inode), ilock_flags); diff --git a/fs/btrfs/reflink.c b/fs/btrfs/reflink.c index 8e37706b98c3..b5d2c094941d 100644 --- a/fs/btrfs/reflink.c +++ b/fs/btrfs/reflink.c @@ -939,6 +939,7 @@ loff_t btrfs_remap_file_range(struct file *src_file, loff_t off, struct btrfs_inode *dst_inode = BTRFS_I(file_inode(dst_file)); bool same_inode = dst_inode == src_inode; u64 wb_start, wb_len; + bool src_downgraded = false; int ret; if (btrfs_is_shutdown(src_inode->root->fs_info)) @@ -976,6 +977,12 @@ loff_t btrfs_remap_file_range(struct file *src_file, loff_t off, if (ret < 0 || len == 0) goto out_unlock; + if (!same_inode) { + set_bit(BTRFS_INODE_REFLINK_SRC, &src_inode->runtime_flags); + downgrade_write(&src_inode->vfs_inode.i_rwsem); + src_downgraded = true; + } + if (remap_flags & REMAP_FILE_DEDUP) ret = btrfs_extent_same(src_inode, off, len, dst_inode, destoff); else @@ -986,8 +993,14 @@ loff_t btrfs_remap_file_range(struct file *src_file, loff_t off, btrfs_inode_unlock(src_inode, BTRFS_ILOCK_MMAP); } else { btrfs_double_mmap_unlock(src_inode, dst_inode); - unlock_two_nondirectories(&src_inode->vfs_inode, - &dst_inode->vfs_inode); + if (src_downgraded) { + clear_bit(BTRFS_INODE_REFLINK_SRC, &src_inode->runtime_flags); + inode_unlock_shared(&src_inode->vfs_inode); + inode_unlock(&dst_inode->vfs_inode); + } else { + unlock_two_nondirectories(&src_inode->vfs_inode, + &dst_inode->vfs_inode); + } } /* From 7884a8f3279c28ff990e2eacd5c2479bb1c930d0 Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Thu, 17 Sep 2026 11:38:04 +0100 Subject: [PATCH 49/92] btrfs: fix off by one super block end offset calculation when writing super blocks We are skipping the write of a super block mirror if its end offset matches exactly the device's end offset. This happens because we are using a greater than or equals comparison instead of just a greater than comparison. Fixes: 935e5cc935bc ("Btrfs: fix wrong disk size when writing super blocks") Reviewed-by: Qu Wenruo Signed-off-by: Filipe Manana Signed-off-by: David Sterba --- fs/btrfs/disk-io.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/fs/btrfs/disk-io.c b/fs/btrfs/disk-io.c index a8535e309b57..90cf0647c47e 100644 --- a/fs/btrfs/disk-io.c +++ b/fs/btrfs/disk-io.c @@ -3909,8 +3909,7 @@ static int write_dev_supers(struct btrfs_device *device, atomic_inc(&device->sb_write_errors); continue; } - if (bytenr + BTRFS_SUPER_INFO_SIZE >= - device->commit_total_bytes) + if (bytenr + BTRFS_SUPER_INFO_SIZE > device->commit_total_bytes) break; btrfs_set_super_bytenr(sb, bytenr_orig); @@ -3988,8 +3987,7 @@ static int wait_dev_supers(struct btrfs_device *device, int max_mirrors) primary_failed = true; continue; } - if (bytenr + BTRFS_SUPER_INFO_SIZE >= - device->commit_total_bytes) + if (bytenr + BTRFS_SUPER_INFO_SIZE > device->commit_total_bytes) break; folio = filemap_get_folio(device->bdev->bd_mapping, From 1b934cad5c0e025e85f76aafec6edf55795b5252 Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Fri, 18 Sep 2026 12:24:37 +0100 Subject: [PATCH 50/92] btrfs: fix barrier usage in btrfs_record_root_in_trans() The barrier usage in btrfs_record_root_in_trans() is wrong, as the writer side, in record_root_in_trans(), sets BTRFS_ROOT_IN_TRANS_SETUP, does a write barrier and then sets the root's last transaction. This means the reader side must check the root's last transaction, issue a read barrier and then check for BTRFS_ROOT_IN_TRANS_SETUP. However, currently we issue a read barrier and then check the root's last transaction and the bit BTRFS_ROOT_IN_TRANS_SETUP, which can be problematic because the CPU is free to reorder the checks and the following can happen: 1) Before reading the root's last_trans, it checks that BTRFS_ROOT_IN_TRANS_SETUP is not set. 2) A writer sets BTRFS_ROOT_IN_TRANS_SETUP, does smp_wmb() and updates the root's last_trans. 3) The reader then sees the root's last_trans matches the current transaction and falsely concludes the root setup is completes and returns without waiting for the writer task to complete the setup (calling btrfs_init_reloc_root()). So fix the reading ordered as previously described: check the root's last_trans, issue read barrier and then check BTRFS_ROOT_IN_TRANS_SETUP (the reverse of what the writer side does). Assisted-by: LLM Reviewed-by: Boris Burkov Signed-off-by: Filipe Manana Signed-off-by: David Sterba --- fs/btrfs/transaction.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/fs/btrfs/transaction.c b/fs/btrfs/transaction.c index c1555621ae4e..13203ea9e116 100644 --- a/fs/btrfs/transaction.c +++ b/fs/btrfs/transaction.c @@ -511,10 +511,11 @@ int btrfs_record_root_in_trans(struct btrfs_trans_handle *trans, * see record_root_in_trans for comments about IN_TRANS_SETUP usage * and barriers */ - smp_rmb(); - if (btrfs_get_root_last_trans(root) == trans->transid && - !test_bit(BTRFS_ROOT_IN_TRANS_SETUP, &root->state)) - return 0; + if (btrfs_get_root_last_trans(root) == trans->transid) { + smp_rmb(); + if (!test_bit(BTRFS_ROOT_IN_TRANS_SETUP, &root->state)) + return 0; + } mutex_lock(&fs_info->reloc_mutex); ret = record_root_in_trans(trans, root, false); From 21721221e5cc9357366137e8dd1dbf349dc58ff0 Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Fri, 18 Sep 2026 12:46:22 +0100 Subject: [PATCH 51/92] btrfs: assert reloc mutex is held in record_root_in_trans() The fs_info->reloc_mutex is supposed to be held when record_root_in_trans() is called and we mention that in a comment inside the function. Add a lockdep assertion to check the lock is held. Reviewed-by: Boris Burkov Reviewed-by: Qu Wenruo Signed-off-by: Filipe Manana Signed-off-by: David Sterba --- fs/btrfs/transaction.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fs/btrfs/transaction.c b/fs/btrfs/transaction.c index 13203ea9e116..0a3913b61f16 100644 --- a/fs/btrfs/transaction.c +++ b/fs/btrfs/transaction.c @@ -413,6 +413,8 @@ static int record_root_in_trans(struct btrfs_trans_handle *trans, struct btrfs_fs_info *fs_info = root->fs_info; int ret = 0; + lockdep_assert_held(&fs_info->reloc_mutex); + if ((test_bit(BTRFS_ROOT_SHAREABLE, &root->state) && btrfs_get_root_last_trans(root) < trans->transid) || force) { WARN_ON(!force && root->commit_root != root->node); From 057e3c350a9185df598c88b4081168ed047c9247 Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Fri, 18 Sep 2026 17:41:28 +0100 Subject: [PATCH 52/92] btrfs: fix dangling nodes in tree-mod-log after error in btrfs_tree_mod_log_insert_root() If in btrfs_tree_mod_log_insert_root() there is an error in the call to tree_mod_log_insert() (the only possible error is -EEXIST, which means we have a bug or some memory corruption maybe) we free all the nodes in the 'tm_list' array but we don't remove them from the tree-mod-log rbtree, which can result in use-after-free bugs later. So make sure we remove the nodes from the rbtree before freeing them after an error. Fixes: 5de865eebb83 ("Btrfs: fix tree mod logging") Assisted-by: LLM (found the bug) Reviewed-by: Boris Burkov Signed-off-by: Filipe Manana Signed-off-by: David Sterba --- fs/btrfs/tree-mod-log.c | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/fs/btrfs/tree-mod-log.c b/fs/btrfs/tree-mod-log.c index a8094928f4c9..bfa348d6483f 100644 --- a/fs/btrfs/tree-mod-log.c +++ b/fs/btrfs/tree-mod-log.c @@ -483,10 +483,17 @@ int btrfs_tree_mod_log_insert_root(struct extent_buffer *old_root, goto out_unlock; } - if (tm_list) + if (tm_list) { ret = tree_mod_log_free_eb(fs_info, tm_list, nritems); - if (!ret) - ret = tree_mod_log_insert(fs_info, tm); + if (ret) + goto out_unlock; + } + + ret = tree_mod_log_insert(fs_info, tm); + if (ret && tm_list) { + for (i = 0; i < nritems; i++) + rb_erase(&tm_list[i]->node, &fs_info->tree_mod_log); + } out_unlock: write_unlock(&fs_info->tree_mod_log_lock); From 886f1aa6e6b30a02901aa7f140149d466e10a873 Mon Sep 17 00:00:00 2001 From: Wentao Liang Date: Wed, 16 Sep 2026 17:17:16 +0000 Subject: [PATCH 53/92] btrfs: zoned: fix block group reference leak in btrfs_repair_one_zone() btrfs_repair_one_zone() hands the block group reference taken by btrfs_lookup_block_group() over to relocating_repair_kthread(), which drops it. However the return value of kthread_run() is not checked, so when the kernel thread fails to spawn, nothing executes the put and the block group reference is leaked. Check kthread_run() and drop the reference if the thread failed to start. Fixes: f7ef5287a63d ("btrfs: zoned: relocate block group to repair IO failure in zoned filesystems") CC: stable@vger.kernel.org Reviewed-by: Boris Burkov Signed-off-by: Wentao Liang Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/volumes.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/fs/btrfs/volumes.c b/fs/btrfs/volumes.c index 7c040f22dbc3..116d882eecfe 100644 --- a/fs/btrfs/volumes.c +++ b/fs/btrfs/volumes.c @@ -9006,6 +9006,7 @@ static int relocating_repair_kthread(void *data) bool btrfs_repair_one_zone(struct btrfs_fs_info *fs_info, u64 logical) { struct btrfs_block_group *cache; + struct task_struct *task; if (!btrfs_is_zoned(fs_info)) return false; @@ -9023,8 +9024,9 @@ bool btrfs_repair_one_zone(struct btrfs_fs_info *fs_info, u64 logical) return true; } - kthread_run(relocating_repair_kthread, cache, - "btrfs-relocating-repair"); + task = kthread_run(relocating_repair_kthread, cache, "btrfs-relocating-repair"); + if (IS_ERR(task)) + btrfs_put_block_group(cache); return true; } From 1b214d3884f07da76b45475a86f6296092e42a7b Mon Sep 17 00:00:00 2001 From: Yang Xiuwei Date: Wed, 19 Aug 2026 10:54:36 +0800 Subject: [PATCH 54/92] btrfs: drop unused uring encoded IO REISSUE stash helpers After not keeping state across -EAGAIN, restoring bc->data on REISSUE is dead. Remove it, stop using the cmd PDU on the write path, and fold the read -EAGAIN check into the existing error path. Signed-off-by: Yang Xiuwei Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/ioctl.c | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/fs/btrfs/ioctl.c b/fs/btrfs/ioctl.c index 25d9feaa9003..52aab510aea0 100644 --- a/fs/btrfs/ioctl.c +++ b/fs/btrfs/ioctl.c @@ -4749,9 +4749,6 @@ static int btrfs_uring_encoded_read(struct io_uring_cmd *cmd, unsigned int issue struct io_btrfs_cmd *bc = io_uring_cmd_to_pdu(cmd, struct io_btrfs_cmd); struct btrfs_uring_encoded_data *data = NULL; - if (cmd->flags & IORING_URING_CMD_REISSUE) - data = bc->data; - if (!capable(CAP_SYS_ADMIN)) { ret = -EPERM; goto out_acct; @@ -4833,8 +4830,6 @@ static int btrfs_uring_encoded_read(struct io_uring_cmd *cmd, unsigned int issue ret = btrfs_encoded_read(&kiocb, &data->iter, &data->args, &cached_state, &disk_bytenr, &disk_io_size); - if (ret == -EAGAIN) - goto out_free; if (ret < 0 && ret != -EIOCBQUEUED) goto out_free; @@ -4891,12 +4886,8 @@ static int btrfs_uring_encoded_write(struct io_uring_cmd *cmd, unsigned int issu struct kiocb kiocb; ssize_t ret; void __user *sqe_addr; - struct io_btrfs_cmd *bc = io_uring_cmd_to_pdu(cmd, struct io_btrfs_cmd); struct btrfs_uring_encoded_data *data = NULL; - if (cmd->flags & IORING_URING_CMD_REISSUE) - data = bc->data; - if (!capable(CAP_SYS_ADMIN)) { ret = -EPERM; goto out_acct; @@ -4920,8 +4911,6 @@ static int btrfs_uring_encoded_write(struct io_uring_cmd *cmd, unsigned int issu goto out_acct; } - bc->data = data; - if (issue_flags & IO_URING_F_COMPAT) { #if defined(CONFIG_64BIT) && defined(CONFIG_COMPAT) struct btrfs_ioctl_encoded_io_args_32 args32; @@ -5007,7 +4996,6 @@ static int btrfs_uring_encoded_write(struct io_uring_cmd *cmd, unsigned int issu inc_syscw(current); kfree(data); - bc->data = NULL; return ret; } From 7d69a3c9b519ac9a3987919eb529d4b8c31c0274 Mon Sep 17 00:00:00 2001 From: Peng Fan Date: Sun, 20 Sep 2026 10:27:29 +0800 Subject: [PATCH 55/92] btrfs: use assign_bit() where applicable Convert open-coded if/else with set_bit/clear_bit and their non-atomic __set_bit/__clear_bit variants to the assign_bit/__assign_bit API. Done with Coccinelle semantic patch: // set_bit -> clear_bit => assign_bit @@ expression cond, bit, addr; @@ -if (cond) - set_bit(bit, addr); -else - clear_bit(bit, addr); +assign_bit(bit, addr, cond); // clear_bit -> set_bit => assign_bit @@ expression cond, bit, addr; @@ -if (cond) - clear_bit(bit, addr); -else - set_bit(bit, addr); +assign_bit(bit, addr, !cond); // __set_bit -> __clear_bit => __assign_bit @@ expression cond, bit, addr; @@ -if (cond) - __set_bit(bit, addr); -else - __clear_bit(bit, addr); +__assign_bit(bit, addr, cond); // __clear_bit -> __set_bit => __assign_bit @@ expression cond, bit, addr; @@ -if (cond) - __clear_bit(bit, addr); -else - __set_bit(bit, addr); +__assign_bit(bit, addr, !cond); Signed-off-by: Peng Fan Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/qgroup.c | 6 ++---- fs/btrfs/sysfs.c | 5 +---- fs/btrfs/volumes.c | 6 ++---- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/fs/btrfs/qgroup.c b/fs/btrfs/qgroup.c index 05e35eb126dc..bf06962d49f3 100644 --- a/fs/btrfs/qgroup.c +++ b/fs/btrfs/qgroup.c @@ -3177,10 +3177,8 @@ int btrfs_run_qgroups(struct btrfs_trans_handle *trans) "qgroup limit item update error %d", ret); spin_lock(&fs_info->qgroup_lock); } - if (btrfs_qgroup_enabled(fs_info)) - set_bit(BTRFS_QGROUP_STATUS_BIT_ON, &fs_info->qgroup_flags); - else - clear_bit(BTRFS_QGROUP_STATUS_BIT_ON, &fs_info->qgroup_flags); + assign_bit(BTRFS_QGROUP_STATUS_BIT_ON, &fs_info->qgroup_flags, + btrfs_qgroup_enabled(fs_info)); spin_unlock(&fs_info->qgroup_lock); ret = update_qgroup_status_item(trans); diff --git a/fs/btrfs/sysfs.c b/fs/btrfs/sysfs.c index c5bb1c7eac6a..3ac1fa608d3b 100644 --- a/fs/btrfs/sysfs.c +++ b/fs/btrfs/sysfs.c @@ -1117,10 +1117,7 @@ static ssize_t quota_override_store(struct kobject *kobj, if (knob > 1) return -EINVAL; - if (knob) - set_bit(BTRFS_FS_QUOTA_OVERRIDE, &fs_info->flags); - else - clear_bit(BTRFS_FS_QUOTA_OVERRIDE, &fs_info->flags); + assign_bit(BTRFS_FS_QUOTA_OVERRIDE, &fs_info->flags, knob); return len; } diff --git a/fs/btrfs/volumes.c b/fs/btrfs/volumes.c index 116d882eecfe..d3e6a9b429dd 100644 --- a/fs/btrfs/volumes.c +++ b/fs/btrfs/volumes.c @@ -698,10 +698,8 @@ static int btrfs_open_one_device(struct btrfs_fs_devices *fs_devices, clear_bit(BTRFS_DEV_STATE_WRITEABLE, &device->dev_state); fs_devices->seeding = true; } else { - if (bdev_read_only(file_bdev(bdev_file))) - clear_bit(BTRFS_DEV_STATE_WRITEABLE, &device->dev_state); - else - set_bit(BTRFS_DEV_STATE_WRITEABLE, &device->dev_state); + assign_bit(BTRFS_DEV_STATE_WRITEABLE, &device->dev_state, + !bdev_read_only(file_bdev(bdev_file))); } if (bdev_rot(file_bdev(bdev_file))) From 6fa6630f952237b47dadd5981af3a1fcd88b99ea Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Fri, 25 Sep 2026 16:45:34 +0100 Subject: [PATCH 56/92] btrfs: simplify dir item location setup in btrfs_insert_xattr_item() The location (a btrfs_disk_key) of a btrfs_dir_item used for a xattr is always zeroed and we have a complex setup in btrfs_insert_xattr_item() where we declare an on stack btrfs_key, zero it out, convert it into a btrfs_disk_key, also declared on stack, and then pass that btrfs_disk_key to a btrfs_set_dir_item_key() call. We can simplify this, and save stack space (one btrfs_disk_key and one btrfs_key), by simply using memzero_extent_buffer(). Reviewed-by: Qu Wenruo Signed-off-by: Filipe Manana Signed-off-by: David Sterba --- fs/btrfs/dir-item.c | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/fs/btrfs/dir-item.c b/fs/btrfs/dir-item.c index 3a90736915af..d39f85035b43 100644 --- a/fs/btrfs/dir-item.c +++ b/fs/btrfs/dir-item.c @@ -62,8 +62,7 @@ int btrfs_insert_xattr_item(struct btrfs_trans_handle *trans, int ret = 0; struct btrfs_dir_item *dir_item; unsigned long name_ptr, data_ptr; - struct btrfs_key key, location; - struct btrfs_disk_key disk_key; + struct btrfs_key key; struct extent_buffer *leaf; u32 data_size; @@ -79,11 +78,11 @@ int btrfs_insert_xattr_item(struct btrfs_trans_handle *trans, name, name_len); if (IS_ERR(dir_item)) return PTR_ERR(dir_item); - memset(&location, 0, sizeof(location)); leaf = path->nodes[0]; - btrfs_cpu_key_to_disk(&disk_key, &location); - btrfs_set_dir_item_key(leaf, dir_item, &disk_key); + memzero_extent_buffer(leaf, (unsigned long)dir_item + + offsetof(struct btrfs_dir_item, location), + sizeof(struct btrfs_disk_key)); btrfs_set_dir_flags(leaf, dir_item, BTRFS_FT_XATTR); btrfs_set_dir_name_len(leaf, dir_item, name_len); btrfs_set_dir_transid(leaf, dir_item, trans->transid); From 93aac90b2d5bce5697d43d7b664102abe3f649c1 Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Thu, 24 Sep 2026 15:28:14 +0930 Subject: [PATCH 57/92] btrfs: move __TRANS_* and TRANS_* flags out of transaction.h Among all those flags, only __TRANS_DUMMY is used outside of transaction.[ch], and there are only two places using it: - find_parent_nodes() Introduce a helper, btrfs_trans_is_dummy(), for this call site. - btrfs_init_dummy_trans() Move the function into transaction.[ch], and hide it behind CONFIG_BTRFS_FS_RUN_SANITY_TESTS. With the above changes, we can hide __TRANS_* and TRANS_* flags inside transaction.c. This allows us to modify those flags in the future without causing any changes to existing callers. After the flags relocation, now we can also hide __TRANS_DUMMY behind CONFIG_BTRFS_FS_RUN_SANITY_TESTS. Reviewed-by: Filipe Manana Signed-off-by: Qu Wenruo Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/backref.c | 2 +- fs/btrfs/tests/btrfs-tests.c | 9 --------- fs/btrfs/tests/btrfs-tests.h | 2 -- fs/btrfs/transaction.c | 36 ++++++++++++++++++++++++++++++++++++ fs/btrfs/transaction.h | 29 +++++++++++------------------ 5 files changed, 48 insertions(+), 30 deletions(-) diff --git a/fs/btrfs/backref.c b/fs/btrfs/backref.c index 1be632c742bd..9c93c0c0d6f5 100644 --- a/fs/btrfs/backref.c +++ b/fs/btrfs/backref.c @@ -1432,7 +1432,7 @@ static int find_parent_nodes(struct btrfs_backref_walk_ctx *ctx, goto out; } - if (ctx->trans && likely(ctx->trans->type != __TRANS_DUMMY) && + if (ctx->trans && likely(!btrfs_is_dummy_transaction(ctx->trans)) && ctx->time_seq != BTRFS_SEQ_LAST) { /* * We have a specific time_seq we care about and trans which diff --git a/fs/btrfs/tests/btrfs-tests.c b/fs/btrfs/tests/btrfs-tests.c index 6287d940323d..cebc9a17b94d 100644 --- a/fs/btrfs/tests/btrfs-tests.c +++ b/fs/btrfs/tests/btrfs-tests.c @@ -247,15 +247,6 @@ void btrfs_init_dummy_transaction(struct btrfs_transaction *trans, struct btrfs_ spin_lock_init(&trans->delayed_refs.lock); } -void btrfs_init_dummy_trans(struct btrfs_trans_handle *trans, - struct btrfs_fs_info *fs_info) -{ - memset(trans, 0, sizeof(*trans)); - trans->transid = 1; - trans->type = __TRANS_DUMMY; - trans->fs_info = fs_info; -} - int btrfs_run_sanity_tests(void) { int ret, i; diff --git a/fs/btrfs/tests/btrfs-tests.h b/fs/btrfs/tests/btrfs-tests.h index cea58fe84a6d..a16855138105 100644 --- a/fs/btrfs/tests/btrfs-tests.h +++ b/fs/btrfs/tests/btrfs-tests.h @@ -59,8 +59,6 @@ btrfs_alloc_dummy_block_group(struct btrfs_fs_info *fs_info, unsigned long lengt void btrfs_free_dummy_block_group(struct btrfs_block_group *cache); DEFINE_FREE(btrfs_free_dummy_block_group, struct btrfs_block_group *, btrfs_free_dummy_block_group(_T)); -void btrfs_init_dummy_trans(struct btrfs_trans_handle *trans, - struct btrfs_fs_info *fs_info); void btrfs_init_dummy_transaction(struct btrfs_transaction *trans, struct btrfs_fs_info *fs_info); struct btrfs_device *btrfs_alloc_dummy_device(struct btrfs_fs_info *fs_info); diff --git a/fs/btrfs/transaction.c b/fs/btrfs/transaction.c index 0a3913b61f16..dfab2f7f0776 100644 --- a/fs/btrfs/transaction.c +++ b/fs/btrfs/transaction.c @@ -38,6 +38,26 @@ static struct kmem_cache *btrfs_trans_handle_cachep; +enum { + ENUM_BIT(__TRANS_FREEZABLE), + ENUM_BIT(__TRANS_START), + ENUM_BIT(__TRANS_ATTACH), + ENUM_BIT(__TRANS_JOIN), + ENUM_BIT(__TRANS_JOIN_NOLOCK), +#ifdef CONFIG_BTRFS_FS_RUN_SANITY_TESTS + ENUM_BIT(__TRANS_DUMMY), +#endif + ENUM_BIT(__TRANS_JOIN_NOSTART), +}; + +#define TRANS_START (__TRANS_START | __TRANS_FREEZABLE) +#define TRANS_ATTACH (__TRANS_ATTACH) +#define TRANS_JOIN (__TRANS_JOIN | __TRANS_FREEZABLE) +#define TRANS_JOIN_NOLOCK (__TRANS_JOIN_NOLOCK) +#define TRANS_JOIN_NOSTART (__TRANS_JOIN_NOSTART) + +#define TRANS_EXTWRITERS (__TRANS_START | __TRANS_ATTACH) + /* * Transaction states and transitions * @@ -139,6 +159,22 @@ static const unsigned int btrfs_blocked_trans_types[TRANS_STATE_MAX] = { __TRANS_JOIN_NOSTART), }; +#ifdef CONFIG_BTRFS_FS_RUN_SANITY_TESTS +bool btrfs_is_dummy_transaction(const struct btrfs_trans_handle *trans) +{ + return trans->type == __TRANS_DUMMY; +} + +void btrfs_init_dummy_trans(struct btrfs_trans_handle *trans, + struct btrfs_fs_info *fs_info) +{ + memset(trans, 0, sizeof(*trans)); + trans->transid = 1; + trans->type = __TRANS_DUMMY; + trans->fs_info = fs_info; +} +#endif + void btrfs_put_transaction(struct btrfs_transaction *transaction) { if (refcount_dec_and_test(&transaction->use_count)) { diff --git a/fs/btrfs/transaction.h b/fs/btrfs/transaction.h index 89153cd22596..0b2e69293b3b 100644 --- a/fs/btrfs/transaction.h +++ b/fs/btrfs/transaction.h @@ -119,24 +119,6 @@ struct btrfs_transaction { wait_queue_head_t pending_wait; }; -enum { - ENUM_BIT(__TRANS_FREEZABLE), - ENUM_BIT(__TRANS_START), - ENUM_BIT(__TRANS_ATTACH), - ENUM_BIT(__TRANS_JOIN), - ENUM_BIT(__TRANS_JOIN_NOLOCK), - ENUM_BIT(__TRANS_DUMMY), - ENUM_BIT(__TRANS_JOIN_NOSTART), -}; - -#define TRANS_START (__TRANS_START | __TRANS_FREEZABLE) -#define TRANS_ATTACH (__TRANS_ATTACH) -#define TRANS_JOIN (__TRANS_JOIN | __TRANS_FREEZABLE) -#define TRANS_JOIN_NOLOCK (__TRANS_JOIN_NOLOCK) -#define TRANS_JOIN_NOSTART (__TRANS_JOIN_NOSTART) - -#define TRANS_EXTWRITERS (__TRANS_START | __TRANS_ATTACH) - /* * Number of extent buffers a transaction handle tracks for writeback * inhibition. The CLOCK reference bits pack into a u32 so this must not exceed @@ -305,6 +287,17 @@ do { \ __LINE__, __error); \ } while (0) +#ifdef CONFIG_BTRFS_FS_RUN_SANITY_TESTS +bool btrfs_is_dummy_transaction(const struct btrfs_trans_handle *trans); +void btrfs_init_dummy_trans(struct btrfs_trans_handle *trans, + struct btrfs_fs_info *fs_info); +#else +static inline bool btrfs_is_dummy_transaction(const struct btrfs_trans_handle *trans) +{ + return false; +} +#endif + int btrfs_end_transaction(struct btrfs_trans_handle *trans); struct btrfs_trans_handle *btrfs_start_transaction(struct btrfs_root *root, unsigned int num_items); From 4c13c58bcd447f4b92a7a190bf9ec18a45c1654c Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Thu, 24 Sep 2026 15:28:15 +0930 Subject: [PATCH 58/92] btrfs: remove __TRANS_FREEZABLE Inside transaction.c most TRANS_* flags are just a single bit, but there are 2 exceptions: - TRANS_START Which is (__TRANS_START | __TRANS_FREEZABLE) - TRANS_JOIN Which is (__TRANS_JOIN | __TRANS_FREEZABLE) The extra __TRANS_FREEZABLE flag indicates that those operations need to acquire sb intwrite lock to handle fs freezing. However since there are only two operations requiring sb intwrite lock, there is no need to introduce a dedicated flag for it, we can introduce a new TRANS_SB_INTWRITER_MASK to cover the only two cases, then use that new mask to determine whether the type requires sb intwrite lock. This makes all TRANS_* flags a single bit. Reviewed-by: Filipe Manana Signed-off-by: Qu Wenruo Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/transaction.c | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/fs/btrfs/transaction.c b/fs/btrfs/transaction.c index dfab2f7f0776..7e716f5bdf74 100644 --- a/fs/btrfs/transaction.c +++ b/fs/btrfs/transaction.c @@ -39,7 +39,6 @@ static struct kmem_cache *btrfs_trans_handle_cachep; enum { - ENUM_BIT(__TRANS_FREEZABLE), ENUM_BIT(__TRANS_START), ENUM_BIT(__TRANS_ATTACH), ENUM_BIT(__TRANS_JOIN), @@ -50,12 +49,15 @@ enum { ENUM_BIT(__TRANS_JOIN_NOSTART), }; -#define TRANS_START (__TRANS_START | __TRANS_FREEZABLE) +#define TRANS_START (__TRANS_START) #define TRANS_ATTACH (__TRANS_ATTACH) -#define TRANS_JOIN (__TRANS_JOIN | __TRANS_FREEZABLE) +#define TRANS_JOIN (__TRANS_JOIN) #define TRANS_JOIN_NOLOCK (__TRANS_JOIN_NOLOCK) #define TRANS_JOIN_NOSTART (__TRANS_JOIN_NOSTART) +/* Those types need to hold sb intwrite lock. */ +#define TRANS_SB_INTWRITER_MASK (__TRANS_START | __TRANS_JOIN) + #define TRANS_EXTWRITERS (__TRANS_START | __TRANS_ATTACH) /* @@ -750,6 +752,8 @@ start_transaction(struct btrfs_root *root, unsigned int num_items, } /* + * Only TRANS_START and TRANS_JOIN require sb intwrite lock. + * * If we are JOIN_NOLOCK we're already committing a transaction and * waiting on this guy, so we don't need to do the sb_start_intwrite * because we're already holding a ref. We need this because we could @@ -759,7 +763,7 @@ start_transaction(struct btrfs_root *root, unsigned int num_items, * If we are ATTACH, it means we just want to catch the current * transaction and commit it, so we needn't do sb_start_intwrite(). */ - if (type & __TRANS_FREEZABLE) + if (type & TRANS_SB_INTWRITER_MASK) sb_start_intwrite(fs_info->sb); if (may_wait_transaction(fs_info, type)) @@ -861,7 +865,7 @@ start_transaction(struct btrfs_root *root, unsigned int num_items, return h; join_fail: - if (type & __TRANS_FREEZABLE) + if (type & TRANS_SB_INTWRITER_MASK) sb_end_intwrite(fs_info->sb); kmem_cache_free(btrfs_trans_handle_cachep, h); alloc_fail: @@ -1142,7 +1146,7 @@ static int __btrfs_end_transaction(struct btrfs_trans_handle *trans, btrfs_trans_release_chunk_metadata(trans); - if (trans->type & __TRANS_FREEZABLE) + if (trans->type & TRANS_SB_INTWRITER_MASK) sb_end_intwrite(info->sb); /* @@ -2154,7 +2158,7 @@ static void cleanup_transaction(struct btrfs_trans_handle *trans, int err) fs_info->running_transaction = NULL; spin_unlock(&fs_info->trans_lock); - if (trans->type & __TRANS_FREEZABLE) + if (trans->type & TRANS_SB_INTWRITER_MASK) sb_end_intwrite(fs_info->sb); btrfs_put_transaction(cur_trans); btrfs_put_transaction(cur_trans); @@ -2686,7 +2690,7 @@ int btrfs_commit_transaction(struct btrfs_trans_handle *trans) btrfs_put_transaction(cur_trans); btrfs_put_transaction(cur_trans); - if (trans->type & __TRANS_FREEZABLE) + if (trans->type & TRANS_SB_INTWRITER_MASK) sb_end_intwrite(fs_info->sb); btrfs_scrub_continue(fs_info); From d658eb2bda4c67836cf49a88c082b3719258f814 Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Thu, 24 Sep 2026 15:28:16 +0930 Subject: [PATCH 59/92] btrfs: remove __TRANS_* flags After patch "btrfs: remove __TRANS_FREEZABLE", each TRANS_* flag is just the corresponding single-bit __TRANS_* flag. There is no need to split __TRANS_* and TRANS_* flags, just remove __TRANS_* flags and use TRANS_* flags instead. Since every TRANS_* flag is a single bit, do extra cleanups: - Add an ASSERT() in start_transaction() To make sure there is only a single bit set in @type - Rename TRANS_EXTWRITERS to TRANS_EXTWRITERS_MASK To follow the naming scheme that a multi-bit value has the _MASK suffix. Reviewed-by: Filipe Manana Signed-off-by: Qu Wenruo Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/transaction.c | 77 ++++++++++++++++++++---------------------- 1 file changed, 37 insertions(+), 40 deletions(-) diff --git a/fs/btrfs/transaction.c b/fs/btrfs/transaction.c index 7e716f5bdf74..367be91fc649 100644 --- a/fs/btrfs/transaction.c +++ b/fs/btrfs/transaction.c @@ -39,26 +39,20 @@ static struct kmem_cache *btrfs_trans_handle_cachep; enum { - ENUM_BIT(__TRANS_START), - ENUM_BIT(__TRANS_ATTACH), - ENUM_BIT(__TRANS_JOIN), - ENUM_BIT(__TRANS_JOIN_NOLOCK), + ENUM_BIT(TRANS_START), + ENUM_BIT(TRANS_ATTACH), + ENUM_BIT(TRANS_JOIN), + ENUM_BIT(TRANS_JOIN_NOLOCK), #ifdef CONFIG_BTRFS_FS_RUN_SANITY_TESTS - ENUM_BIT(__TRANS_DUMMY), + ENUM_BIT(TRANS_DUMMY), #endif - ENUM_BIT(__TRANS_JOIN_NOSTART), + ENUM_BIT(TRANS_JOIN_NOSTART), }; -#define TRANS_START (__TRANS_START) -#define TRANS_ATTACH (__TRANS_ATTACH) -#define TRANS_JOIN (__TRANS_JOIN) -#define TRANS_JOIN_NOLOCK (__TRANS_JOIN_NOLOCK) -#define TRANS_JOIN_NOSTART (__TRANS_JOIN_NOSTART) - /* Those types need to hold sb intwrite lock. */ -#define TRANS_SB_INTWRITER_MASK (__TRANS_START | __TRANS_JOIN) +#define TRANS_SB_INTWRITER_MASK (TRANS_START | TRANS_JOIN) -#define TRANS_EXTWRITERS (__TRANS_START | __TRANS_ATTACH) +#define TRANS_EXTWRITERS_MASK (TRANS_START | TRANS_ATTACH) /* * Transaction states and transitions @@ -139,32 +133,32 @@ enum { static const unsigned int btrfs_blocked_trans_types[TRANS_STATE_MAX] = { [TRANS_STATE_RUNNING] = 0U, [TRANS_STATE_COMMIT_PREP] = 0U, - [TRANS_STATE_COMMIT_START] = (__TRANS_START | __TRANS_ATTACH), - [TRANS_STATE_COMMIT_DOING] = (__TRANS_START | - __TRANS_ATTACH | - __TRANS_JOIN | - __TRANS_JOIN_NOSTART), - [TRANS_STATE_UNBLOCKED] = (__TRANS_START | - __TRANS_ATTACH | - __TRANS_JOIN | - __TRANS_JOIN_NOLOCK | - __TRANS_JOIN_NOSTART), - [TRANS_STATE_SUPER_COMMITTED] = (__TRANS_START | - __TRANS_ATTACH | - __TRANS_JOIN | - __TRANS_JOIN_NOLOCK | - __TRANS_JOIN_NOSTART), - [TRANS_STATE_COMPLETED] = (__TRANS_START | - __TRANS_ATTACH | - __TRANS_JOIN | - __TRANS_JOIN_NOLOCK | - __TRANS_JOIN_NOSTART), + [TRANS_STATE_COMMIT_START] = (TRANS_START | TRANS_ATTACH), + [TRANS_STATE_COMMIT_DOING] = (TRANS_START | + TRANS_ATTACH | + TRANS_JOIN | + TRANS_JOIN_NOSTART), + [TRANS_STATE_UNBLOCKED] = (TRANS_START | + TRANS_ATTACH | + TRANS_JOIN | + TRANS_JOIN_NOLOCK | + TRANS_JOIN_NOSTART), + [TRANS_STATE_SUPER_COMMITTED] = (TRANS_START | + TRANS_ATTACH | + TRANS_JOIN | + TRANS_JOIN_NOLOCK | + TRANS_JOIN_NOSTART), + [TRANS_STATE_COMPLETED] = (TRANS_START | + TRANS_ATTACH | + TRANS_JOIN | + TRANS_JOIN_NOLOCK | + TRANS_JOIN_NOSTART), }; #ifdef CONFIG_BTRFS_FS_RUN_SANITY_TESTS bool btrfs_is_dummy_transaction(const struct btrfs_trans_handle *trans) { - return trans->type == __TRANS_DUMMY; + return trans->type == TRANS_DUMMY; } void btrfs_init_dummy_trans(struct btrfs_trans_handle *trans, @@ -172,7 +166,7 @@ void btrfs_init_dummy_trans(struct btrfs_trans_handle *trans, { memset(trans, 0, sizeof(*trans)); trans->transid = 1; - trans->type = __TRANS_DUMMY; + trans->type = TRANS_DUMMY; trans->fs_info = fs_info; } #endif @@ -261,21 +255,21 @@ static noinline void switch_commit_roots(struct btrfs_trans_handle *trans) static inline void extwriter_counter_inc(struct btrfs_transaction *trans, unsigned int type) { - if (type & TRANS_EXTWRITERS) + if (type & TRANS_EXTWRITERS_MASK) atomic_inc(&trans->num_extwriters); } static inline void extwriter_counter_dec(struct btrfs_transaction *trans, unsigned int type) { - if (type & TRANS_EXTWRITERS) + if (type & TRANS_EXTWRITERS_MASK) atomic_dec(&trans->num_extwriters); } static inline void extwriter_counter_init(struct btrfs_transaction *trans, unsigned int type) { - atomic_set(&trans->num_extwriters, ((type & TRANS_EXTWRITERS) ? 1 : 0)); + atomic_set(&trans->num_extwriters, ((type & TRANS_EXTWRITERS_MASK) ? 1 : 0)); } static inline int extwriter_counter_read(struct btrfs_transaction *trans) @@ -666,11 +660,14 @@ start_transaction(struct btrfs_root *root, unsigned int num_items, bool do_chunk_alloc = false; int ret; + /* The type must be a single TRANS_* bit set. */ + ASSERT(has_single_bit_set(type)); + if (unlikely(BTRFS_FS_ERROR(fs_info))) return ERR_PTR(-EROFS); if (current->journal_info) { - WARN_ON(type & TRANS_EXTWRITERS); + WARN_ON(type & TRANS_EXTWRITERS_MASK); h = current->journal_info; refcount_inc(&h->use_count); WARN_ON(refcount_read(&h->use_count) > 2); From cd5e2d2f2ea2b2c644ac8a01b50a8f22bd23610f Mon Sep 17 00:00:00 2001 From: Boris Burkov Date: Mon, 28 Sep 2026 17:20:33 -0700 Subject: [PATCH 60/92] btrfs: allocate additional SYSTEM space earlier Currently, reserve_chunk_space() allocates a new system chunk when the space left is smaller than the reservation for a single chunk tree update. Therefore, a filesystem in single metadata mode on a very large block device can accumulate tens of thousands (TB) of chunks while never allocating more than the original 4MiB SYSTEM block group created by mkfs. If it then fills up (with large fallocates for example) and then that data is freed, en-masse, we end up trying to delete thousands of block groups in a single transaction in btrfs_delete_unused_bgs(). This process touches most of the nodes and leaves of the chunk tree, essentially trying to allocate roughly double the current usage of the chunk tree for cow. When this runs into needing a fresh system chunk, the fs is full of the empty bgs and we abort with ENOSPC in btrfs_remove_chunk() like: BTRFS error (device loop0 state A): Transaction 30733 aborted (-ENOSPC) space_info SYSTEM (sub-group id 0) has 0 free, is not full space_info total=4194304, used=3276800, pinned=0, reserved=917504, may_use=0, readonly=0 zone_unusable=0 BTRFS: error (device loop0 state A) in btrfs_remove_chunk:3643: errno=-28 No space left This was seen on a 30TiB production filesystem with 10TiB of unused block groups and reproduces on a 30TiB sparse loop device: mkfs with -m single, fallocate 1GiB files until ENOSPC, delete two of every three adjacent files, sync. To fix this, we should allocate the (relatively tiny) system chunks a little bit more eagerly. If we do it when it is half full, we ensure we can delete all of the block groups in one go. To fill up half of a 4MiB system bg requires thousands of block_groups so this should only affect very large fileystems. Assisted-by: LLM Reviewed-by: Filipe Manana Signed-off-by: Boris Burkov Signed-off-by: David Sterba --- fs/btrfs/block-group.c | 42 +++++++++++++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/fs/btrfs/block-group.c b/fs/btrfs/block-group.c index 2eb09c9901c9..0a41b16ed270 100644 --- a/fs/btrfs/block-group.c +++ b/fs/btrfs/block-group.c @@ -1574,6 +1574,19 @@ static bool btrfs_link_bg_list(struct btrfs_block_group *bg, struct list_head *l return added; } +/* + * Compute a rough bound on the bytes needed to modify every leaf in the chunk + * tree. Normally, we could just allocate a new system chunk then, but that can + * fail if there is no free space for a new dev extent. This can happen when + * we fill up a large filesystem then rapidly delete a large portion of it. + */ +static u64 system_space_target(struct btrfs_space_info *sinfo) +{ + lockdep_assert_held(&sinfo->lock); + + return btrfs_space_info_used(sinfo, true) + sinfo->bytes_used; +} + /* * Process the unused_bgs list and remove any that don't have any allocated * space inside of them. @@ -1661,6 +1674,9 @@ void btrfs_delete_unused_bgs(struct btrfs_fs_info *fs_info) if (btrfs_is_block_group_used(block_group) || (block_group->ro && !(block_group->flags & BTRFS_BLOCK_GROUP_REMAPPED)) || list_is_singular(&block_group->list) || + ((block_group->flags & BTRFS_BLOCK_GROUP_SYSTEM) && + space_info->total_bytes - block_group->length < + system_space_target(space_info)) || test_bit(BLOCK_GROUP_FLAG_FULLY_REMAPPED, &block_group->runtime_flags)) { /* * We want to bail if we made new allocations or have @@ -1674,6 +1690,10 @@ void btrfs_delete_unused_bgs(struct btrfs_fs_info *fs_info) * next block group of this type would be created with a * "single" profile (even if we're in a raid fs) because * fs_info->avail_*_alloc_bits would be 0. + * + * Also bail out if this is a system block group that + * system_space_target() relies on to ensure head room for + * mass deletion. */ trace_btrfs_skip_unused_block_group(block_group); spin_unlock(&block_group->lock); @@ -4509,6 +4529,7 @@ static void reserve_chunk_space(struct btrfs_trans_handle *trans, struct btrfs_fs_info *fs_info = trans->fs_info; struct btrfs_space_info *info; u64 left; + bool want_system_chunk, need_system_chunk; int ret = 0; /* @@ -4520,15 +4541,17 @@ static void reserve_chunk_space(struct btrfs_trans_handle *trans, info = btrfs_find_space_info(fs_info, BTRFS_BLOCK_GROUP_SYSTEM); spin_lock(&info->lock); left = info->total_bytes - btrfs_space_info_used(info, true); + want_system_chunk = info->total_bytes < system_space_target(info); + need_system_chunk = left < bytes; spin_unlock(&info->lock); - if (left < bytes && btrfs_test_opt(fs_info, ENOSPC_DEBUG)) { + if (need_system_chunk && btrfs_test_opt(fs_info, ENOSPC_DEBUG)) { btrfs_info(fs_info, "left=%llu, need=%llu, flags=%llu", left, bytes, type); btrfs_dump_space_info(info, 0, false); } - if (left < bytes) { + if (want_system_chunk || need_system_chunk) { u64 flags = btrfs_system_alloc_profile(fs_info); struct btrfs_block_group *bg; struct btrfs_space_info *space_info; @@ -4572,13 +4595,14 @@ static void reserve_chunk_space(struct btrfs_trans_handle *trans, } } - if (!ret) { - ret = btrfs_block_rsv_add(fs_info, - &fs_info->chunk_block_rsv, - bytes, BTRFS_RESERVE_NO_FLUSH); - if (!ret) - trans->chunk_bytes_reserved += bytes; - } + if (ret && need_system_chunk) + return; + + ret = btrfs_block_rsv_add(fs_info, + &fs_info->chunk_block_rsv, + bytes, BTRFS_RESERVE_NO_FLUSH); + if (!ret) + trans->chunk_bytes_reserved += bytes; } /* From 5612507486cdbd6da23b5bb972a8fcc192570d15 Mon Sep 17 00:00:00 2001 From: Boris Burkov Date: Mon, 28 Sep 2026 17:20:34 -0700 Subject: [PATCH 61/92] btrfs: commit after deleting an unused block group if system space is low If the device is fully allocated and the system space_info has no room for a second copy of the chunk tree, deleting many unused block groups in one transaction aborts it with -ENOSPC in btrfs_remove_chunk(). While we can get ahead of this problem in practice by allocating the system space more aggressively, that doesn't help a system that already got into this state anyway. In particular, this would be a problem on systems that got into this state on old code from before that improvement, but it is also a backstop in case we do mess up and still end up with too little system space. The way we end up needing a lot of system chunk space in practice is if we need to delete many bgs at once, resulting in touching many nodes/leaves in the chunk tree. Therefore, if we detect we are in a condition where we are low on system space, commit a single transaction deleting a single bg, which should not need much system chunk space. After this we have the freed bg and can proceed as usual. Any additional chunk allocations (for any reason) will also check the system space and are very likely to allocate more system space, so this should be a relatively robust mechanism. Assisted-by: LLM Reviewed-by: Filipe Manana Signed-off-by: Boris Burkov Signed-off-by: David Sterba --- fs/btrfs/block-group.c | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/fs/btrfs/block-group.c b/fs/btrfs/block-group.c index 0a41b16ed270..c148b9d6e597 100644 --- a/fs/btrfs/block-group.c +++ b/fs/btrfs/block-group.c @@ -1596,6 +1596,7 @@ void btrfs_delete_unused_bgs(struct btrfs_fs_info *fs_info) LIST_HEAD(retry_list); struct btrfs_block_group *block_group; struct btrfs_space_info *space_info; + struct btrfs_space_info *system_info; struct btrfs_trans_handle *trans; const bool async_trim_enabled = btrfs_test_opt(fs_info, DISCARD_ASYNC); int ret = 0; @@ -1613,10 +1614,13 @@ void btrfs_delete_unused_bgs(struct btrfs_fs_info *fs_info) if (!mutex_trylock(&fs_info->reclaim_bgs_lock)) return; + system_info = btrfs_find_space_info(fs_info, BTRFS_BLOCK_GROUP_SYSTEM); + spin_lock(&fs_info->unused_bgs_lock); while (!list_empty(&fs_info->unused_bgs)) { u64 used; int trimming; + bool low; block_group = list_first_entry(&fs_info->unused_bgs, struct btrfs_block_group, @@ -1864,7 +1868,18 @@ void btrfs_delete_unused_bgs(struct btrfs_fs_info *fs_info) btrfs_get_block_group(block_group); } end_trans: - btrfs_end_transaction(trans); + /* + * If we are low on system space, commit immediately to get back + * unallocated space to greatly improve the chances of + * successfully allocating a system chunk. + */ + spin_lock(&system_info->lock); + low = system_info->total_bytes < system_space_target(system_info); + spin_unlock(&system_info->lock); + if (unlikely(!ret && low)) + ret = btrfs_commit_transaction(trans); + else + btrfs_end_transaction(trans); next: btrfs_put_block_group(block_group); spin_lock(&fs_info->unused_bgs_lock); From 9bdef63e2b5d4a88810329a1cab3642d6378a895 Mon Sep 17 00:00:00 2001 From: Johannes Thumshirn Date: Tue, 29 Sep 2026 16:02:19 +0200 Subject: [PATCH 62/92] btrfs: zoned: fix double list add in btrfs_load_block_group_zone_info For DUP and RAID profiles with zones in a mismatching active state, i.e. due to one of the zones of the block-group being on a conventional zone, btrfs_zone_activate() has already put the block group on the list. This leads to a double list add and refcounting problems. Fixes: 265f7237dd25 ("btrfs: zoned: allow DUP on meta-data block groups") Fixes: 568220fa9657 ("btrfs: zoned: support RAID0/1/10 on top of raid stripe tree") Reviewed-by: Daniel Vacek Signed-off-by: Johannes Thumshirn Signed-off-by: David Sterba --- fs/btrfs/zoned.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/fs/btrfs/zoned.c b/fs/btrfs/zoned.c index a1ef8caaacda..9f562cc34e38 100644 --- a/fs/btrfs/zoned.c +++ b/fs/btrfs/zoned.c @@ -2005,10 +2005,12 @@ int btrfs_load_block_group_zone_info(struct btrfs_block_group *cache, bool new) if (!ret) { cache->meta_write_pointer = cache->alloc_offset + cache->start; if (test_bit(BLOCK_GROUP_FLAG_ZONE_IS_ACTIVE, &cache->runtime_flags)) { - btrfs_get_block_group(cache); spin_lock(&fs_info->zone_active_bgs_lock); - list_add_tail(&cache->active_bg_list, - &fs_info->zone_active_bgs); + if (list_empty(&cache->active_bg_list)) { + btrfs_get_block_group(cache); + list_add_tail(&cache->active_bg_list, + &fs_info->zone_active_bgs); + } spin_unlock(&fs_info->zone_active_bgs_lock); } } else { From d1d74665ab9a8971d42177ca865cfbe2897c1fc7 Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Tue, 29 Sep 2026 15:58:34 +0100 Subject: [PATCH 63/92] btrfs: fix incomplete iteration over fs state when logging messages In btrfs_state_to_string() we iterate over all the bits set in fs_info->fs_state using for_each_set_bit(), which requires its third argument to match the number of bits, but we pass it the number of bytes of the type used by fs_info->fs_state, an unsigned long, which is 8 on 64 bits platforms and 4 on 32 bits platforms. This means we are not iterating over bits in the ranges [8, 63] or [4, 31]. Fix this by passing BTRFS_FS_STATE_COUNT instead of sizeof(fs_state). Note that we could pass BITS_PER_TYPE(fs_state), but since BTRFS_FS_STATE_COUNT is 11 and we have no bits beyond that, it would cause pointless iterations over fs_state. This also makes pointless the checks for the bit being inside the limit of BTRFS_FS_STATE_COUNT inside the loop. Fixes: c067da878156 ("btrfs: add filesystems state details to error messages") Reviewed-by: Qu Wenruo Signed-off-by: Filipe Manana Signed-off-by: David Sterba --- fs/btrfs/messages.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/fs/btrfs/messages.c b/fs/btrfs/messages.c index 198d1747c80a..6debf0d4acad 100644 --- a/fs/btrfs/messages.c +++ b/fs/btrfs/messages.c @@ -42,9 +42,8 @@ static void btrfs_state_to_string(const struct btrfs_fs_info *info, char *buf) states_printed = true; } - for_each_set_bit(bit, &fs_state, sizeof(fs_state)) { - WARN_ON_ONCE(bit >= BTRFS_FS_STATE_COUNT); - if ((bit < BTRFS_FS_STATE_COUNT) && fs_state_chars[bit]) { + for_each_set_bit(bit, &fs_state, BTRFS_FS_STATE_COUNT) { + if (fs_state_chars[bit]) { *curr++ = fs_state_chars[bit]; states_printed = true; } From 3edda5249553b708c378a0d766018976315c63ad Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Wed, 30 Sep 2026 11:52:32 +0100 Subject: [PATCH 64/92] btrfs: fix duplicated code in fs state string in logged messages We are using the code 'E' when the fs is in an error state (transaction aborted) and when it's in the emergency shutdown state, which is confusing and not intentional. Fix this by using an unused code such as 'H' for the emergency shutdown state and add a comment over the fs_state_chars array to warn to not use 'E' as its use is not defined in the array but instead in the function btrfs_state_to_string(), since the fs state error is not defined as a bit but as an error code in fs_info->fs_error. Fixes: 9b2839451dfc ("btrfs: introduce a new shutdown state") Reviewed-by: Qu Wenruo Signed-off-by: Filipe Manana Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/messages.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fs/btrfs/messages.c b/fs/btrfs/messages.c index 6debf0d4acad..5fe5d34ab629 100644 --- a/fs/btrfs/messages.c +++ b/fs/btrfs/messages.c @@ -13,6 +13,8 @@ /* * Characters to print to indicate error conditions or uncommon filesystem state. * RO is not an error. + * Don't use 'E' because that's used to signal that there's an error stored in + * fs_info->fs_error (see btrfs_state_to_string() below). */ static const char fs_state_chars[] = { [BTRFS_FS_STATE_REMOUNTING] = 'M', @@ -24,7 +26,7 @@ static const char fs_state_chars[] = { [BTRFS_FS_STATE_NO_DATA_CSUMS] = 'C', [BTRFS_FS_STATE_SKIP_META_CSUMS] = 'S', [BTRFS_FS_STATE_LOG_CLEANUP_ERROR] = 'L', - [BTRFS_FS_STATE_EMERGENCY_SHUTDOWN] = 'E', + [BTRFS_FS_STATE_EMERGENCY_SHUTDOWN] = 'H', }; static void btrfs_state_to_string(const struct btrfs_fs_info *info, char *buf) From 38361baddd61cdf343609497b17d58507e8f9782 Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Wed, 30 Sep 2026 13:31:13 +0100 Subject: [PATCH 65/92] btrfs: add missing code for no delayed iput fs state in log messages Add a letter code for the BTRFS_FS_STATE_NO_DELAYED_IPUT fs state, which is currently missing and it's useful to have. Reviewed-by: Qu Wenruo Signed-off-by: Filipe Manana Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/messages.c | 1 + 1 file changed, 1 insertion(+) diff --git a/fs/btrfs/messages.c b/fs/btrfs/messages.c index 5fe5d34ab629..2610b447165e 100644 --- a/fs/btrfs/messages.c +++ b/fs/btrfs/messages.c @@ -26,6 +26,7 @@ static const char fs_state_chars[] = { [BTRFS_FS_STATE_NO_DATA_CSUMS] = 'C', [BTRFS_FS_STATE_SKIP_META_CSUMS] = 'S', [BTRFS_FS_STATE_LOG_CLEANUP_ERROR] = 'L', + [BTRFS_FS_STATE_NO_DELAYED_IPUT] = 'I', [BTRFS_FS_STATE_EMERGENCY_SHUTDOWN] = 'H', }; From 719aee83960b3e20099247c2d0bb47b969487b4c Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Wed, 30 Sep 2026 12:03:11 +0100 Subject: [PATCH 66/92] btrfs: remove unnecessary pointer increment in btrfs_state_to_string() There's no point in incrementing the local pointer after we set the NUL terminator, as we don't use the pointer anymore after that. Reviewed-by: Qu Wenruo Signed-off-by: Filipe Manana Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/messages.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/btrfs/messages.c b/fs/btrfs/messages.c index 2610b447165e..c42a12139377 100644 --- a/fs/btrfs/messages.c +++ b/fs/btrfs/messages.c @@ -56,7 +56,7 @@ static void btrfs_state_to_string(const struct btrfs_fs_info *info, char *buf) if (!states_printed) curr = buf; - *curr++ = 0; + *curr = 0; } #endif From 22bdddf40e046e1c9a9c690a2bc3b4790578ad24 Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Wed, 30 Sep 2026 12:09:43 +0100 Subject: [PATCH 67/92] btrfs: avoid overhead when logging messages if fs state is clean If the fs state does not have any errors and is not in some special state (like emergency shutdown or mounted with rescue options), then in btrfs_state_to_string() we end up doing a memcpy() into the buffer and then later just undo that work by setting the buffer's first element to the NUL terminator, since fs_info->fs_error is 0 and there is no bit set in fs_info->fs_state. Improve on this by returning early if there's no error in the fs and the fs state is clean, which is the expected scenario for a healthy fs that is not mounted with special options like the rescue options. Reviewed-by: Qu Wenruo Signed-off-by: Filipe Manana Signed-off-by: David Sterba --- fs/btrfs/messages.c | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/fs/btrfs/messages.c b/fs/btrfs/messages.c index c42a12139377..0704be7be871 100644 --- a/fs/btrfs/messages.c +++ b/fs/btrfs/messages.c @@ -33,27 +33,27 @@ static const char fs_state_chars[] = { static void btrfs_state_to_string(const struct btrfs_fs_info *info, char *buf) { unsigned int bit; - bool states_printed = false; unsigned long fs_state = READ_ONCE(info->fs_state); char *curr = buf; + if (likely(BTRFS_FS_ERROR(info) == 0 && fs_state == 0)) { + *curr = 0; + return; + } + memcpy(curr, STATE_STRING_PREFACE, sizeof(STATE_STRING_PREFACE)); curr += sizeof(STATE_STRING_PREFACE) - 1; - if (unlikely(BTRFS_FS_ERROR(info))) { + if (BTRFS_FS_ERROR(info)) *curr++ = 'E'; - states_printed = true; - } for_each_set_bit(bit, &fs_state, BTRFS_FS_STATE_COUNT) { - if (fs_state_chars[bit]) { + if (fs_state_chars[bit]) *curr++ = fs_state_chars[bit]; - states_printed = true; - } } - /* If no states were printed, reset the buffer */ - if (!states_printed) + /* If nothing printed, ensure we return an empty string. */ + if (curr == (buf + sizeof(STATE_STRING_PREFACE) - 1)) curr = buf; *curr = 0; From 08f750d9f8930e4ca053913b5ca3fd49fd9268c9 Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Wed, 30 Sep 2026 15:37:00 +0100 Subject: [PATCH 68/92] btrfs: fix off-by-one end offset check in report_setget_bounds() The addressable range with an extent buffer goes from byte 0 to byte eb->len - 1, so if a byte address matches eb->len, it's already beyond the addressable range and we should log that it's bad start address and not end address. Fixes: 234fdd2815ce ("btrfs: remove redundant check in up check_setget_bounds") Reviewed-by: Qu Wenruo Reviewed-by: Daniel Vacek Signed-off-by: Filipe Manana Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/accessors.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/btrfs/accessors.c b/fs/btrfs/accessors.c index 1248aa2535d3..d63ac6eb92de 100644 --- a/fs/btrfs/accessors.c +++ b/fs/btrfs/accessors.c @@ -16,7 +16,7 @@ static void __cold report_setget_bounds(const struct extent_buffer *eb, btrfs_warn(eb->fs_info, "bad eb member %s: ptr 0x%lx start %llu member offset %lu size %d", - (member_offset > eb->len ? "start" : "end"), + (member_offset >= eb->len ? "start" : "end"), (unsigned long)ptr, eb->start, member_offset, size); } From 2f19d4a02c82712891d6a6469f62695a9c6c2816 Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Wed, 30 Sep 2026 16:26:49 +0100 Subject: [PATCH 69/92] btrfs: remove fs_info argument from btrfs_block_rsv_add() The fs_info argument is not used, so remove it. Reviewed-by: Qu Wenruo Signed-off-by: Filipe Manana Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/block-group.c | 5 ++--- fs/btrfs/block-rsv.c | 3 +-- fs/btrfs/block-rsv.h | 3 +-- fs/btrfs/delayed-inode.c | 3 +-- fs/btrfs/props.c | 3 +-- fs/btrfs/relocation.c | 2 +- fs/btrfs/root-tree.c | 3 +-- fs/btrfs/transaction.c | 3 +-- 8 files changed, 9 insertions(+), 16 deletions(-) diff --git a/fs/btrfs/block-group.c b/fs/btrfs/block-group.c index c148b9d6e597..8ff21dde9a46 100644 --- a/fs/btrfs/block-group.c +++ b/fs/btrfs/block-group.c @@ -4613,9 +4613,8 @@ static void reserve_chunk_space(struct btrfs_trans_handle *trans, if (ret && need_system_chunk) return; - ret = btrfs_block_rsv_add(fs_info, - &fs_info->chunk_block_rsv, - bytes, BTRFS_RESERVE_NO_FLUSH); + ret = btrfs_block_rsv_add(&fs_info->chunk_block_rsv, bytes, + BTRFS_RESERVE_NO_FLUSH); if (!ret) trans->chunk_bytes_reserved += bytes; } diff --git a/fs/btrfs/block-rsv.c b/fs/btrfs/block-rsv.c index c68a8f4b7d19..ecbf66d6cc73 100644 --- a/fs/btrfs/block-rsv.c +++ b/fs/btrfs/block-rsv.c @@ -209,8 +209,7 @@ void btrfs_free_block_rsv(struct btrfs_fs_info *fs_info, kfree(rsv); } -int btrfs_block_rsv_add(struct btrfs_fs_info *fs_info, - struct btrfs_block_rsv *block_rsv, u64 num_bytes, +int btrfs_block_rsv_add(struct btrfs_block_rsv *block_rsv, u64 num_bytes, enum btrfs_reserve_flush_enum flush) { int ret; diff --git a/fs/btrfs/block-rsv.h b/fs/btrfs/block-rsv.h index 8359fb96bc3c..992272a01964 100644 --- a/fs/btrfs/block-rsv.h +++ b/fs/btrfs/block-rsv.h @@ -69,8 +69,7 @@ void btrfs_init_metadata_block_rsv(struct btrfs_fs_info *fs_info, enum btrfs_rsv_type type); void btrfs_free_block_rsv(struct btrfs_fs_info *fs_info, struct btrfs_block_rsv *rsv); -int btrfs_block_rsv_add(struct btrfs_fs_info *fs_info, - struct btrfs_block_rsv *block_rsv, u64 num_bytes, +int btrfs_block_rsv_add(struct btrfs_block_rsv *block_rsv, u64 num_bytes, enum btrfs_reserve_flush_enum flush); int btrfs_block_rsv_check(struct btrfs_block_rsv *block_rsv, int min_percent); int btrfs_block_rsv_refill(struct btrfs_fs_info *fs_info, diff --git a/fs/btrfs/delayed-inode.c b/fs/btrfs/delayed-inode.c index 1f20148c1fd4..333ce8b52489 100644 --- a/fs/btrfs/delayed-inode.c +++ b/fs/btrfs/delayed-inode.c @@ -600,8 +600,7 @@ static int btrfs_delayed_inode_reserve_metadata( ret = btrfs_qgroup_reserve_meta_prealloc(root, num_bytes, true, true); if (ret < 0) return ret; - ret = btrfs_block_rsv_add(fs_info, dst_rsv, num_bytes, - BTRFS_RESERVE_NO_FLUSH); + ret = btrfs_block_rsv_add(dst_rsv, num_bytes, BTRFS_RESERVE_NO_FLUSH); /* NO_FLUSH could only fail with -ENOSPC */ ASSERT(ret == 0 || ret == -ENOSPC); if (ret) diff --git a/fs/btrfs/props.c b/fs/btrfs/props.c index bb77d46376d4..7cc82366a1df 100644 --- a/fs/btrfs/props.c +++ b/fs/btrfs/props.c @@ -440,8 +440,7 @@ int btrfs_inode_inherit_props(struct btrfs_trans_handle *trans, */ if (need_reserve) { num_bytes = btrfs_calc_insert_metadata_size(fs_info, 1); - ret = btrfs_block_rsv_add(fs_info, trans->block_rsv, - num_bytes, + ret = btrfs_block_rsv_add(trans->block_rsv, num_bytes, BTRFS_RESERVE_NO_FLUSH); if (ret) return ret; diff --git a/fs/btrfs/relocation.c b/fs/btrfs/relocation.c index da54db75e7a9..b7de08c7e9e2 100644 --- a/fs/btrfs/relocation.c +++ b/fs/btrfs/relocation.c @@ -1795,7 +1795,7 @@ int prepare_to_merge(struct reloc_control *rc, int err) again: if (!err) { num_bytes = rc->merging_rsv_size; - ret = btrfs_block_rsv_add(fs_info, rc->block_rsv, num_bytes, + ret = btrfs_block_rsv_add(rc->block_rsv, num_bytes, BTRFS_RESERVE_FLUSH_ALL); if (ret) err = ret; diff --git a/fs/btrfs/root-tree.c b/fs/btrfs/root-tree.c index 2e4c3efbd02f..524210671ec4 100644 --- a/fs/btrfs/root-tree.c +++ b/fs/btrfs/root-tree.c @@ -510,8 +510,7 @@ int btrfs_subvolume_reserve_metadata(struct btrfs_root *root, num_bytes = btrfs_calc_insert_metadata_size(fs_info, items); rsv->space_info = btrfs_find_space_info(fs_info, BTRFS_BLOCK_GROUP_METADATA); - ret = btrfs_block_rsv_add(fs_info, rsv, num_bytes, - BTRFS_RESERVE_FLUSH_ALL); + ret = btrfs_block_rsv_add(rsv, num_bytes, BTRFS_RESERVE_FLUSH_ALL); if (ret == -ENOSPC && use_global_rsv) ret = btrfs_block_rsv_migrate(global_rsv, rsv, num_bytes, true); diff --git a/fs/btrfs/transaction.c b/fs/btrfs/transaction.c index 367be91fc649..286a9d455d26 100644 --- a/fs/btrfs/transaction.c +++ b/fs/btrfs/transaction.c @@ -1767,8 +1767,7 @@ static noinline int create_pending_snapshot(struct btrfs_trans_handle *trans, btrfs_reloc_pre_snapshot(pending, &to_reserve); if (to_reserve > 0) { - pending->error = btrfs_block_rsv_add(fs_info, - &pending->block_rsv, + pending->error = btrfs_block_rsv_add(&pending->block_rsv, to_reserve, BTRFS_RESERVE_NO_FLUSH); if (unlikely(pending->error)) From dec07422bc9293ba6938f8c1bf3fa05625530b46 Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Wed, 30 Sep 2026 16:31:01 +0100 Subject: [PATCH 70/92] btrfs: remove fs_info argument from btrfs_block_rsv_refill() The fs_info argument is not used, so remove it. Reviewed-by: Qu Wenruo Signed-off-by: Filipe Manana Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/block-rsv.c | 3 +-- fs/btrfs/block-rsv.h | 3 +-- fs/btrfs/inode.c | 4 ++-- fs/btrfs/relocation.c | 11 ++++------- 4 files changed, 8 insertions(+), 13 deletions(-) diff --git a/fs/btrfs/block-rsv.c b/fs/btrfs/block-rsv.c index ecbf66d6cc73..55e84ff0932e 100644 --- a/fs/btrfs/block-rsv.c +++ b/fs/btrfs/block-rsv.c @@ -238,8 +238,7 @@ int btrfs_block_rsv_check(struct btrfs_block_rsv *block_rsv, int min_percent) return ret; } -int btrfs_block_rsv_refill(struct btrfs_fs_info *fs_info, - struct btrfs_block_rsv *block_rsv, u64 num_bytes, +int btrfs_block_rsv_refill(struct btrfs_block_rsv *block_rsv, u64 num_bytes, enum btrfs_reserve_flush_enum flush) { int ret = -ENOSPC; diff --git a/fs/btrfs/block-rsv.h b/fs/btrfs/block-rsv.h index 992272a01964..39b8928e1559 100644 --- a/fs/btrfs/block-rsv.h +++ b/fs/btrfs/block-rsv.h @@ -72,8 +72,7 @@ void btrfs_free_block_rsv(struct btrfs_fs_info *fs_info, int btrfs_block_rsv_add(struct btrfs_block_rsv *block_rsv, u64 num_bytes, enum btrfs_reserve_flush_enum flush); int btrfs_block_rsv_check(struct btrfs_block_rsv *block_rsv, int min_percent); -int btrfs_block_rsv_refill(struct btrfs_fs_info *fs_info, - struct btrfs_block_rsv *block_rsv, u64 num_bytes, +int btrfs_block_rsv_refill(struct btrfs_block_rsv *block_rsv, u64 num_bytes, enum btrfs_reserve_flush_enum flush); int btrfs_block_rsv_migrate(struct btrfs_block_rsv *src_rsv, struct btrfs_block_rsv *dst_rsv, u64 num_bytes, diff --git a/fs/btrfs/inode.c b/fs/btrfs/inode.c index 2a32072849cb..343b6402bb35 100644 --- a/fs/btrfs/inode.c +++ b/fs/btrfs/inode.c @@ -5595,10 +5595,10 @@ static struct btrfs_trans_handle *evict_refill_and_join(struct btrfs_root *root, * if we fail to make this reservation we can re-try without the * delayed_refs_extra so we can make some forward progress. */ - ret = btrfs_block_rsv_refill(fs_info, rsv, rsv->size + delayed_refs_extra, + ret = btrfs_block_rsv_refill(rsv, rsv->size + delayed_refs_extra, BTRFS_RESERVE_FLUSH_EVICT); if (ret) { - ret = btrfs_block_rsv_refill(fs_info, rsv, rsv->size, + ret = btrfs_block_rsv_refill(rsv, rsv->size, BTRFS_RESERVE_FLUSH_EVICT); if (ret) { btrfs_warn(fs_info, diff --git a/fs/btrfs/relocation.c b/fs/btrfs/relocation.c index b7de08c7e9e2..65cb9dcf0aac 100644 --- a/fs/btrfs/relocation.c +++ b/fs/btrfs/relocation.c @@ -1675,8 +1675,7 @@ static noinline_for_stack int merge_reloc_root(struct reloc_control *rc, memset(&next_key, 0, sizeof(next_key)); while (1) { - ret = btrfs_block_rsv_refill(fs_info, rc->block_rsv, - min_reserved, + ret = btrfs_block_rsv_refill(rc->block_rsv, min_reserved, BTRFS_RESERVE_FLUSH_LIMIT); if (ret) goto out; @@ -2274,7 +2273,7 @@ static int refill_metadata_space(struct btrfs_trans_handle *trans, * If we get an enospc just kick back -EAGAIN so we know to drop the * transaction and try to refill when we can flush all the things. */ - ret = btrfs_block_rsv_refill(fs_info, rc->block_rsv, num_bytes, + ret = btrfs_block_rsv_refill(rc->block_rsv, num_bytes, BTRFS_RESERVE_FLUSH_LIMIT); if (ret) { u64 tmp = fs_info->nodesize * RELOCATION_RESERVED_NODES; @@ -3584,8 +3583,7 @@ int prepare_to_relocate(struct reloc_control *rc) rc->reserved_bytes = 0; rc->block_rsv->size = rc->extent_root->fs_info->nodesize * RELOCATION_RESERVED_NODES; - ret = btrfs_block_rsv_refill(rc->extent_root->fs_info, - rc->block_rsv, rc->block_rsv->size, + ret = btrfs_block_rsv_refill(rc->block_rsv, rc->block_rsv->size, BTRFS_RESERVE_FLUSH_ALL); if (ret) return ret; @@ -3637,8 +3635,7 @@ static noinline_for_stack int relocate_block_group(struct reloc_control *rc) while (1) { rc->reserved_bytes = 0; - ret = btrfs_block_rsv_refill(fs_info, rc->block_rsv, - rc->block_rsv->size, + ret = btrfs_block_rsv_refill(rc->block_rsv, rc->block_rsv->size, BTRFS_RESERVE_FLUSH_ALL); if (ret) { err = ret; From 1dd5d95b3fc8a337d11026c3d6e9a4236b276225 Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Wed, 30 Sep 2026 16:41:15 +0100 Subject: [PATCH 71/92] btrfs: remove pointless qgroup assignment in block_rsv_release_bytes() If num_bytes == (u64)-1, there's no point to set qgroup_to_release to block_rsv->qgroup_rsv_size as later on we will overwrite that value without ever reading the variable before that. So simplify things and remove that assignment. Reviewed-by: Qu Wenruo Signed-off-by: Filipe Manana Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/block-rsv.c | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/fs/btrfs/block-rsv.c b/fs/btrfs/block-rsv.c index 55e84ff0932e..fd604631b9ca 100644 --- a/fs/btrfs/block-rsv.c +++ b/fs/btrfs/block-rsv.c @@ -111,10 +111,8 @@ static u64 block_rsv_release_bytes(struct btrfs_fs_info *fs_info, u64 ret; spin_lock(&block_rsv->lock); - if (num_bytes == (u64)-1) { + if (num_bytes == (u64)-1) num_bytes = block_rsv->size; - qgroup_to_release = block_rsv->qgroup_rsv_size; - } block_rsv->size -= num_bytes; if (block_rsv->reserved >= block_rsv->size) { num_bytes = block_rsv->reserved - block_rsv->size; @@ -128,8 +126,6 @@ static u64 block_rsv_release_bytes(struct btrfs_fs_info *fs_info, qgroup_to_release = block_rsv->qgroup_rsv_reserved - block_rsv->qgroup_rsv_size; block_rsv->qgroup_rsv_reserved = block_rsv->qgroup_rsv_size; - } else { - qgroup_to_release = 0; } spin_unlock(&block_rsv->lock); From 95e3b8dc32179b9d2895fca5605d11a763e9d658 Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Wed, 30 Sep 2026 17:16:39 +0100 Subject: [PATCH 72/92] btrfs: remove unnecessary forward struct declarations in volumes.h There is no need to forward declare the structures as their definitions are brought in by the include of the linux/blkdev.h and fs.h files. Reviewed-by: Qu Wenruo Signed-off-by: Filipe Manana Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/volumes.h | 3 --- 1 file changed, 3 deletions(-) diff --git a/fs/btrfs/volumes.h b/fs/btrfs/volumes.h index 337d7007d9e2..6eddda239221 100644 --- a/fs/btrfs/volumes.h +++ b/fs/btrfs/volumes.h @@ -24,9 +24,6 @@ #include "extent-io-tree.h" #include "fs.h" -struct block_device; -struct bdev_handle; -struct btrfs_fs_info; struct btrfs_block_group; struct btrfs_trans_handle; struct btrfs_transaction; From dd906477cc2c33c82ec17e6301bd4be4ea588565 Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Thu, 1 Oct 2026 14:46:15 +0930 Subject: [PATCH 73/92] btrfs: qgroup: do not treat "ret > 0" as error when deleting a qgroup [BUG] There is a bug report that when removing a child qgroup, if removing the qgroup requires a rescan, it will delete the on-disk relation and qgroup items but not the in-memory qgroup rb entry. This makes all later qgroup rescan to fail. [CAUSE] The call chain looks like the following: btrfs_remove_qgroup() |- __del_qgroup_relation() | |- quick_update_accounting() | Which returns 1, showing this operation will require | a qgroup rescan |- if (ret) goto out; Which skips the del_qgroup_rb() call, leaving the in-memory qgroup rb entry still intact. Then later qgroup rescan/update will always fail and mark qgroup inconsistent: btrfs_run_qgroups() |- update_qgroup_info_item() | |- btrfs_search_slot() | Which returns -ENOENT, as the qgroup info item is already | deleted. |- qgroup_mark_inconsistent() |- return -ENOENT; [FIX] Do not treat "ret > 0" as an error. Reported-by: c.f.a@posteo.de Link: https://lore.kernel.org/linux-btrfs/14846642-85d3-4afb-ad99-1b74b22f5f90@posteo.de/ Fixes: 9c8b35b1ba21 ("btrfs: quota: Automatically update related qgroups or mark INCONSISTENT flags when assigning/deleting a qgroup relations.") Reviewed-by: Filipe Manana Signed-off-by: Qu Wenruo Signed-off-by: David Sterba --- fs/btrfs/qgroup.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/btrfs/qgroup.c b/fs/btrfs/qgroup.c index bf06962d49f3..dfe3827ec369 100644 --- a/fs/btrfs/qgroup.c +++ b/fs/btrfs/qgroup.c @@ -1853,7 +1853,7 @@ int btrfs_remove_qgroup(struct btrfs_trans_handle *trans, u64 qgroupid) struct btrfs_qgroup_list, next_group); ret = __del_qgroup_relation(trans, qgroupid, list->group->qgroupid); - if (ret) + if (ret < 0) goto out; } From 65583d4749af1bede388425b5ab70cb6cbb68125 Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Thu, 1 Oct 2026 14:46:16 +0930 Subject: [PATCH 74/92] btrfs: qgroup: use qgroup_mark_inconsistent() in quick_update_accounting() When quick_update_accounting() fails to handle a qgroup which is not fully exclusive (its exclusive number doesn't match the reference one), we have to mark the qgroup inconsistent. Unlike other locations we didn't use qgroup_mark_inconsistent(), thus when such problem happens no extra error message is shown. Use the helper in quick_update_accounting(), which does not only show the reason of marking qgroup inconsistent, but also cancels any running rescan and skips dirty accounting, avoiding wasting CPU time on already inconsistent qgroup numbers. Reviewed-by: Filipe Manana Signed-off-by: Qu Wenruo Signed-off-by: David Sterba --- fs/btrfs/qgroup.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/fs/btrfs/qgroup.c b/fs/btrfs/qgroup.c index dfe3827ec369..d3401a6df530 100644 --- a/fs/btrfs/qgroup.c +++ b/fs/btrfs/qgroup.c @@ -1562,7 +1562,10 @@ static int quick_update_accounting(struct btrfs_fs_info *fs_info, } out: if (ret) - set_bit(BTRFS_QGROUP_STATUS_BIT_INCONSISTENT, &fs_info->qgroup_flags); + qgroup_mark_inconsistent(fs_info, + "unable to do quick excl updating for qgroup %hu/%llu, ret=%d", + btrfs_qgroup_level(src), + btrfs_qgroup_subvolid(src), ret); return ret; } From 727bb633fea9a3b37061b837c85647218dc28485 Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Thu, 1 Oct 2026 14:46:17 +0930 Subject: [PATCH 75/92] btrfs: qgroup: do not leak -ENOENT from btrfs_remove_qgroup() If btrfs_remove_qgroup() is called on a qgroup that only exists in the rb-tree, then del_qgroup_item() will return -ENOENT, and we tolerate that -ENOENT. However if the to-be-deleted qgroup has no parent qgroups, the @ret will not be updated, and will leak to user space. It will make the ioctl() caller incorrectly assume the operation failed. Fix it by resetting @ret to 0 after a successful or tolerated del_qgroup_item() call, so the -ENOENT from del_qgroup_item() won't be leaked. Assisted-by: LLM (exposing this pre-existing problem) Reviewed-by: Filipe Manana Signed-off-by: Qu Wenruo Signed-off-by: David Sterba --- fs/btrfs/qgroup.c | 1 + 1 file changed, 1 insertion(+) diff --git a/fs/btrfs/qgroup.c b/fs/btrfs/qgroup.c index d3401a6df530..46de0a5e80d5 100644 --- a/fs/btrfs/qgroup.c +++ b/fs/btrfs/qgroup.c @@ -1850,6 +1850,7 @@ int btrfs_remove_qgroup(struct btrfs_trans_handle *trans, u64 qgroupid) ret = del_qgroup_item(trans, qgroupid); if (ret && ret != -ENOENT) goto out; + ret = 0; while (!list_empty(&qgroup->groups)) { list = list_first_entry(&qgroup->groups, From d00b85308abee300dfb7e1ac642f38bab9ec7492 Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Wed, 30 Sep 2026 18:29:58 +0930 Subject: [PATCH 76/92] btrfs: raid56: do not verify the content if there is no data checksum [BUG] There is a bug report that fio can fail to sync the data on a degraded RAID56 mount: # mkfs.btrfs -m raid1 -d raid5 -f $dev1 $dev2 $dev3 # mount $dev1 $mnt # btrfs balance start -d $mnt # umount $mnt # btrfs dev scan -u $dev2 # wipefs -fa $dev2 # mount -o degraded $dev1 $mnt # fio --name=degraded-write --directory=$mnt --rw=randwrite --fdatasync=4 \ --bs=4k --size=512M --randrepeat=1 The run will fail at fsync: fio: io_u error on file /mnt/btrfs/degraded-write.0.0: Input/output error: datasync offset=142254080, buflen=0 fio: pid=2064, err=5/file:io_u.c:2008, func=io_u error, error=Input/output error [CAUSE] With extra debug output, it shows the problem pretty well: rbio=2876243968 faila=0 sectornr=0 rbio=2876243968 error=0-15 csum=2 bio=15 The first line shows the rbio's full stripe logical, and stripe/sector number when verify_one_sector() failed. The second line shows more detail about the rbio, the sector range [0, 16) is corrupted, which indicates the first data stripe is corrupted, matching the missing device. The important part is that the csum bitmap shows the data csum is only for sector number 2, meanwhile the failed verification is at sector number 0. This means we did the checksum verification even if no corresponding checksum is populated. [FIX] There is already a similar check at verify_bio_data_sectors(), but unfortunately there is no such check at verify_one_sector(). Add the missing check to skip the csum verification if there is no corresponding data checksum. Reported-by: Bartosz Fenski Link: https://lore.kernel.org/linux-btrfs/a72409af-45ef-45df-9970-2e38bcb22ea6@fenski.pl/ Fixes: 7a3150723061 ("btrfs: raid56: do data csum verification during RMW cycle") CC: stable@vger.kernel.org Reviewed-by: Filipe Manana Signed-off-by: Qu Wenruo Signed-off-by: David Sterba --- fs/btrfs/raid56.c | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/fs/btrfs/raid56.c b/fs/btrfs/raid56.c index 8ec24dbb180f..5ec31299f1c3 100644 --- a/fs/btrfs/raid56.c +++ b/fs/btrfs/raid56.c @@ -1889,6 +1889,8 @@ static int verify_one_sector(struct btrfs_raid_bio *rbio, { struct btrfs_fs_info *fs_info = rbio->bioc->fs_info; phys_addr_t *paddrs; + const unsigned int total_sector_nr = + stripe_nr * rbio->stripe_nsectors + sector_nr; u8 csum_buf[BTRFS_CSUM_SIZE]; u8 *csum_expected; @@ -1898,6 +1900,11 @@ static int verify_one_sector(struct btrfs_raid_bio *rbio, /* No way to verify P/Q as they are not covered by data csum. */ if (stripe_nr >= rbio->nr_data) return 0; + + /* No data checksum for this sector. */ + if (!test_bit(total_sector_nr, rbio->csum_bitmap)) + return 0; + /* * If we're rebuilding a read, we have to use pages from the * bio list if possible. @@ -1908,9 +1915,7 @@ static int verify_one_sector(struct btrfs_raid_bio *rbio, paddrs = rbio_stripe_paddrs(rbio, stripe_nr, sector_nr); } - csum_expected = rbio->csum_buf + - (stripe_nr * rbio->stripe_nsectors + sector_nr) * - fs_info->csum_size; + csum_expected = rbio->csum_buf + total_sector_nr * fs_info->csum_size; calculate_block_csum_paddrs(fs_info, paddrs, csum_buf); if (unlikely(memcmp(csum_buf, csum_expected, fs_info->csum_size) != 0)) return -EIO; From c73eb6f727414a0a78ee993eccd6c2d2065e9571 Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Sun, 6 Sep 2026 16:33:37 +0930 Subject: [PATCH 77/92] btrfs: always use the second newest slot for rescue=usebackuproot [RESCUE FAILURE] There is a bug report that "rescue=usebackuproot" failed to properly mount the fs, but if using btrfs-mod-sb to use the second newest slot, then the fs can even pass the full "btrfs check" and be mounted RW. [CAUSE] During init_tree_roots(), "rescue=usebackuproot" only makes a difference if the tree and chunk roots failed to be load. The loading only requires the root node/leaf to be properly loaded: - tree root - extent tree - block group tree - device tree - data reloc tree - quota tree (optional) - uuid tree - raid stripe tree (for RST feature) And it doesn't really check any child tree blocks, thus for a lot of cases, "rescue=usebackuproot" won't make a difference. [ENHANCEMENT] Instead of trying every slot and settling down on the first passing backup slot, always use the second newest slot. The newest slot should always match the root info in the super block, and if it worked the end user should not even need to bother to use backup root. Older backup slots normally have a very low chance to work. Link: https://github.com/kdave/btrfs-progs/issues/1162#issuecomment-5475477108 Reviewed-by: Boris Burkov Signed-off-by: Qu Wenruo Signed-off-by: David Sterba --- fs/btrfs/disk-io.c | 102 +++++++++++++++++++-------------------------- 1 file changed, 42 insertions(+), 60 deletions(-) diff --git a/fs/btrfs/disk-io.c b/fs/btrfs/disk-io.c index 90cf0647c47e..d3d3d67ced3e 100644 --- a/fs/btrfs/disk-io.c +++ b/fs/btrfs/disk-io.c @@ -2711,73 +2711,55 @@ static int __cold init_tree_roots(struct btrfs_fs_info *fs_info) int backup_index = find_newest_super_backup(fs_info); struct btrfs_super_block *sb = fs_info->super_copy; struct btrfs_root *tree_root = fs_info->tree_root; - bool handle_error = false; + bool use_backup = btrfs_test_opt(fs_info, USEBACKUPROOT); int ret = 0; - int i; - for (i = 0; i < BTRFS_NUM_BACKUP_ROOTS; i++) { - if (handle_error) { - if (!IS_ERR(tree_root->node)) - free_extent_buffer(tree_root->node); - tree_root->node = NULL; - - if (!btrfs_test_opt(fs_info, USEBACKUPROOT)) - break; - - free_root_pointers(fs_info, 0); - - /* - * Don't use the log in recovery mode, it won't be - * valid - */ - btrfs_set_super_log_root(sb, 0); - - btrfs_warn(fs_info, "try to load backup roots slot %d", i); - ret = read_backup_root(fs_info, i); - backup_index = ret; - if (ret < 0) - return ret; + if (use_backup) { + ret = read_backup_root(fs_info, 1); + if (ret < 0) { + btrfs_err(fs_info, + "failed to load backup roots at slot %d", 1); + return ret; } - - ret = load_important_roots(fs_info); - if (ret) { - handle_error = true; - continue; - } - /* - * No need to hold btrfs_root::objectid_mutex since the fs - * hasn't been fully initialised and we are the only user + * Don't use the log in recovery mode, it won't be + * valid */ - ret = btrfs_init_root_free_objectid(tree_root); - if (ret < 0) { - handle_error = true; - continue; - } - - ASSERT(tree_root->free_objectid <= BTRFS_LAST_FREE_OBJECTID); - - ret = btrfs_read_roots(fs_info); - if (ret < 0) { - handle_error = true; - continue; - } - - /* All successful */ - fs_info->generation = btrfs_header_generation(tree_root->node); - btrfs_set_last_trans_committed(fs_info, fs_info->generation); - fs_info->last_reloc_trans = 0; - - /* Always begin writing backup roots after the one being used */ - if (backup_index < 0) { - fs_info->backup_root_index = 0; - } else { - fs_info->backup_root_index = backup_index + 1; - fs_info->backup_root_index %= BTRFS_NUM_BACKUP_ROOTS; - } - break; + btrfs_set_super_log_root(sb, 0); + backup_index = ret; + btrfs_warn(fs_info, "loaded backup roots at slot %d", 1); } + ret = load_important_roots(fs_info); + if (ret) + return ret; + + /* + * No need to hold btrfs_root::objectid_mutex since the fs + * hasn't been fully initialised and we are the only user + */ + ret = btrfs_init_root_free_objectid(tree_root); + if (ret < 0) + return ret; + + ASSERT(tree_root->free_objectid <= BTRFS_LAST_FREE_OBJECTID); + + ret = btrfs_read_roots(fs_info); + if (ret < 0) + return ret; + + /* All successful */ + fs_info->generation = btrfs_header_generation(tree_root->node); + btrfs_set_last_trans_committed(fs_info, fs_info->generation); + fs_info->last_reloc_trans = 0; + + /* Always begin writing backup roots after the one being used */ + if (backup_index < 0) { + fs_info->backup_root_index = 0; + } else { + fs_info->backup_root_index = backup_index + 1; + fs_info->backup_root_index %= BTRFS_NUM_BACKUP_ROOTS; + } return ret; } From 9421d1897ba2185cd2c3cf7bf517fd99d4f58516 Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Sun, 6 Sep 2026 16:33:38 +0930 Subject: [PATCH 78/92] btrfs: introduce more accurate usebackuproot options Previously we had no way to tell btrfs to use which backup slot, but always use the newest backup, and retry until all backup roots are exhausted. This doesn't give any control to the end user, even if the end user wants to use a specific backup slot. To address this problem, allow more fine-control on usebackuproot rescue option by introducing the following 4 rescue mount options: - rescue=usebackuproot_0 - rescue=usebackuproot_1 - rescue=usebackuproot_2 - rescue=usebackuproot_3 The number 0 means the newest slot, which should be the same as the one recorded in the super block and no need to load any backup root. 1/2/3 means the second/third/fourth(oldest) backup slot to use. The existing "rescue=usebackuproot" is just an alias of "rescue=usebackuproot_1". This will allow users to choose which backup root to use, thus can have a better control than blindly choose any slot that can pass the very basic root node checks. Reviewed-by: Boris Burkov Signed-off-by: Qu Wenruo Signed-off-by: David Sterba --- fs/btrfs/disk-io.c | 12 +++++++++--- fs/btrfs/fs.h | 1 + fs/btrfs/super.c | 28 ++++++++++++++++++++++++++-- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/fs/btrfs/disk-io.c b/fs/btrfs/disk-io.c index d3d3d67ced3e..66b054eb98c6 100644 --- a/fs/btrfs/disk-io.c +++ b/fs/btrfs/disk-io.c @@ -2715,10 +2715,14 @@ static int __cold init_tree_roots(struct btrfs_fs_info *fs_info) int ret = 0; if (use_backup) { - ret = read_backup_root(fs_info, 1); + ASSERT(fs_info->use_backup_slot >= 0 && + fs_info->use_backup_slot < BTRFS_NUM_BACKUP_ROOTS); + + ret = read_backup_root(fs_info, fs_info->use_backup_slot); if (ret < 0) { btrfs_err(fs_info, - "failed to load backup roots at slot %d", 1); + "failed to load backup roots at slot %d", + fs_info->use_backup_slot); return ret; } /* @@ -2727,7 +2731,8 @@ static int __cold init_tree_roots(struct btrfs_fs_info *fs_info) */ btrfs_set_super_log_root(sb, 0); backup_index = ret; - btrfs_warn(fs_info, "loaded backup roots at slot %d", 1); + btrfs_warn(fs_info, "loaded backup roots at slot %d", + fs_info->use_backup_slot); } ret = load_important_roots(fs_info); @@ -2898,6 +2903,7 @@ void btrfs_init_fs_info(struct btrfs_fs_info *fs_info) init_waitqueue_head(&fs_info->async_submit_wait); init_waitqueue_head(&fs_info->delayed_iputs_wait); + fs_info->use_backup_slot = -1; /* Usable values until the real ones are cached from the superblock */ fs_info->nodesize = 4096; fs_info->sectorsize = 4096; diff --git a/fs/btrfs/fs.h b/fs/btrfs/fs.h index 3eba8438593c..649623c603e8 100644 --- a/fs/btrfs/fs.h +++ b/fs/btrfs/fs.h @@ -857,6 +857,7 @@ struct btrfs_fs_info { /* Next backup root to be overwritten */ int backup_root_index; + int use_backup_slot; /* Device replace state */ struct btrfs_dev_replace dev_replace; diff --git a/fs/btrfs/super.c b/fs/btrfs/super.c index ddb620ac241b..8d01409ed382 100644 --- a/fs/btrfs/super.c +++ b/fs/btrfs/super.c @@ -87,6 +87,7 @@ struct btrfs_fs_context { unsigned long long mount_opt; unsigned long compress_type:4; int compress_level; + int use_backup_slot; refcount_t refs; }; @@ -175,6 +176,10 @@ static const struct constant_table btrfs_parameter_space_cache[] = { enum { Opt_rescue_usebackuproot, + Opt_rescue_usebackup_slot_0, + Opt_rescue_usebackup_slot_1, + Opt_rescue_usebackup_slot_2, + Opt_rescue_usebackup_slot_3, Opt_rescue_nologreplay, Opt_rescue_ignorebadroots, Opt_rescue_ignoredatacsums, @@ -185,6 +190,10 @@ enum { static const struct constant_table btrfs_parameter_rescue[] = { { "usebackuproot", Opt_rescue_usebackuproot }, + { "usebackuproot_0", Opt_rescue_usebackup_slot_0}, + { "usebackuproot_1", Opt_rescue_usebackup_slot_1}, + { "usebackuproot_2", Opt_rescue_usebackup_slot_2}, + { "usebackuproot_3", Opt_rescue_usebackup_slot_3}, { "nologreplay", Opt_rescue_nologreplay }, { "ignorebadroots", Opt_rescue_ignorebadroots }, { "ibadroots", Opt_rescue_ignorebadroots }, @@ -588,6 +597,14 @@ static int btrfs_parse_param(struct fs_context *fc, struct fs_parameter *param) switch (result.uint_32) { case Opt_rescue_usebackuproot: btrfs_set_opt(ctx->mount_opt, USEBACKUPROOT); + ctx->use_backup_slot = 1; + break; + case Opt_rescue_usebackup_slot_0: + case Opt_rescue_usebackup_slot_1: + case Opt_rescue_usebackup_slot_2: + case Opt_rescue_usebackup_slot_3: + btrfs_set_opt(ctx->mount_opt, USEBACKUPROOT); + ctx->use_backup_slot = result.uint_32 - Opt_rescue_usebackup_slot_0; break; case Opt_rescue_nologreplay: btrfs_set_opt(ctx->mount_opt, NOLOGREPLAY); @@ -611,6 +628,7 @@ static int btrfs_parse_param(struct fs_context *fc, struct fs_parameter *param) btrfs_set_opt(ctx->mount_opt, IGNOREBADROOTS); btrfs_set_opt(ctx->mount_opt, NOLOGREPLAY); btrfs_set_opt(ctx->mount_opt, USEBACKUPROOT); + ctx->use_backup_slot = 1; break; default: btrfs_info(NULL, "unrecognized rescue option '%s'", @@ -1089,8 +1107,11 @@ static int btrfs_show_options(struct seq_file *seq, struct dentry *dentry) seq_puts(seq, ",notreelog"); if (btrfs_test_opt(info, NOLOGREPLAY)) print_rescue_option(seq, "nologreplay", &printed); - if (btrfs_test_opt(info, USEBACKUPROOT)) - print_rescue_option(seq, "usebackuproot", &printed); + if (btrfs_test_opt(info, USEBACKUPROOT)) { + seq_printf(seq, "%susebackuproot_%d", printed ? ":" : ",rescue=", + info->use_backup_slot); + printed = true; + } if (btrfs_test_opt(info, IGNOREBADROOTS)) print_rescue_option(seq, "ignorebadroots", &printed); if (btrfs_test_opt(info, IGNOREDATACSUMS)) @@ -1406,6 +1427,7 @@ static void btrfs_ctx_to_info(struct btrfs_fs_info *fs_info, struct btrfs_fs_con fs_info->mount_opt = ctx->mount_opt; fs_info->compress_type = ctx->compress_type; fs_info->compress_level = ctx->compress_level; + fs_info->use_backup_slot = ctx->use_backup_slot; } static void btrfs_info_to_ctx(struct btrfs_fs_info *fs_info, struct btrfs_fs_context *ctx) @@ -1417,6 +1439,7 @@ static void btrfs_info_to_ctx(struct btrfs_fs_info *fs_info, struct btrfs_fs_con ctx->mount_opt = fs_info->mount_opt; ctx->compress_type = fs_info->compress_type; ctx->compress_level = fs_info->compress_level; + ctx->use_backup_slot = fs_info->use_backup_slot; } #define btrfs_info_if_set(fs_info, old_ctx, opt, fmt, args...) \ @@ -2209,6 +2232,7 @@ static int btrfs_init_fs_context(struct fs_context *fc) if (fc->purpose == FS_CONTEXT_FOR_RECONFIGURE) { btrfs_info_to_ctx(btrfs_sb(fc->root->d_sb), ctx); } else { + ctx->use_backup_slot = -1; ctx->thread_pool_size = min_t(unsigned long, num_online_cpus() + 2, 8); ctx->max_inline = BTRFS_DEFAULT_MAX_INLINE; From 0388b5bd3a6eb8bcb20208c3b6dbee96fd30bbe0 Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Thu, 1 Oct 2026 17:21:22 +0100 Subject: [PATCH 79/92] btrfs: qgroup: fix swapped blocks existence check when tracing after COW In btrfs_qgroup_trace_subtree_after_cow(), after we erase the swapped block, iterate over all possible tree levels to check if we sill have other swapped blocks. However the check is wrong, it considers that there are other swapped blocks if any rbtree for any level is empty, instead of not empty. As a consequence we set blocks->swapped to true basically every time since we always find an empty rbtree at least at level 7, since in practice it's nearly impossible to find such a huge btree. This makes every future call to btrfs_qgroup_trace_subtree_after_cow() do an unnecessary rbtree search instead of returning immediately. Fix this by updating the condition to set swapped to true if we find an rbtree that is not empty. Fixes: f616f5cd9da7 ("btrfs: qgroup: Use delayed subtree rescan for balance") Reviewed-by: Qu Wenruo Signed-off-by: Filipe Manana Signed-off-by: David Sterba --- fs/btrfs/ctree.h | 2 +- fs/btrfs/qgroup.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fs/btrfs/ctree.h b/fs/btrfs/ctree.h index 22ba2b4505b3..00d4c4184e76 100644 --- a/fs/btrfs/ctree.h +++ b/fs/btrfs/ctree.h @@ -160,7 +160,7 @@ enum { */ struct btrfs_qgroup_swapped_blocks { spinlock_t lock; - /* RM_EMPTY_ROOT() of above blocks[] */ + /* True if any of the rbtrees in the blocks array below is not empty. */ bool swapped; struct rb_root blocks[BTRFS_MAX_LEVEL]; }; diff --git a/fs/btrfs/qgroup.c b/fs/btrfs/qgroup.c index 46de0a5e80d5..798f21608de5 100644 --- a/fs/btrfs/qgroup.c +++ b/fs/btrfs/qgroup.c @@ -4913,7 +4913,7 @@ int btrfs_qgroup_trace_subtree_after_cow(struct btrfs_trans_handle *trans, /* Found one, remove it from @blocks first and update blocks->swapped */ rb_erase(&block->node, &blocks->blocks[level]); for (i = 0; i < BTRFS_MAX_LEVEL; i++) { - if (RB_EMPTY_ROOT(&blocks->blocks[i])) { + if (!RB_EMPTY_ROOT(&blocks->blocks[i])) { swapped = true; break; } From ba35b470084d97d729da87e6d2a1e55571a9aada Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Fri, 2 Oct 2026 16:45:47 +0100 Subject: [PATCH 80/92] btrfs: qgroup: abort transaction on failure to add qgroup relation When adding a qgroup relation, if we fail to add the relation item from "dst" to "src", we attempt to delete the relation item from "src" to "dst" that we added just before, however we ignore the deletion result and if we failed to delete we leave an inconsistency in the quota root. So check the result of the deletion and if it fails, abort the transaction to avoid persisting an inconsistent state. Reviewed-by: Qu Wenruo Signed-off-by: Filipe Manana Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/qgroup.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/fs/btrfs/qgroup.c b/fs/btrfs/qgroup.c index 798f21608de5..4b6c82a24bc5 100644 --- a/fs/btrfs/qgroup.c +++ b/fs/btrfs/qgroup.c @@ -1616,7 +1616,11 @@ int btrfs_add_qgroup_relation(struct btrfs_trans_handle *trans, u64 src, u64 dst ret = add_qgroup_relation_item(trans, dst, src); if (ret) { - del_qgroup_relation_item(trans, src, dst); + int ret2; + + ret2 = del_qgroup_relation_item(trans, src, dst); + if (ret2 < 0) + btrfs_abort_transaction(trans, ret); goto out; } From 1cf81f2196914c9a0ad192b68e1eb2ba7747a6d4 Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Fri, 2 Oct 2026 17:37:15 +0100 Subject: [PATCH 81/92] btrfs: qgroup: fix leak of qgroups in rb tree after failure to enable quotas When enabling quotas we add struct btrfs_qroup items to the rbtree fs_info->qgroup_tree, however if there's an error during the quota enable operation after adding those items, we exit without removing the items from the rbtree and freeing them. Fix this by calling btrfs_free_qgroup_config() on error, which also deletes all sysfs entries (it calls btrfs_sysfs_del_qgroups()). Reviewed-by: Qu Wenruo Signed-off-by: Filipe Manana Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/qgroup.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/fs/btrfs/qgroup.c b/fs/btrfs/qgroup.c index 4b6c82a24bc5..9f21b091c545 100644 --- a/fs/btrfs/qgroup.c +++ b/fs/btrfs/qgroup.c @@ -1293,8 +1293,13 @@ int btrfs_quota_enable(struct btrfs_fs_info *fs_info, if (ret) btrfs_put_root(quota_root); out: - if (ret) - btrfs_sysfs_del_qgroups(fs_info); + if (ret) { + /* + * Free all qgroups previously added with add_qgroup_rb() and + * sysfs entries. + */ + btrfs_free_qgroup_config(fs_info); + } mutex_unlock(&fs_info->qgroup_ioctl_lock); if (ret && trans) btrfs_end_transaction(trans); From aad3c3c84f065bbe8644233192ae143a583e4e23 Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Fri, 2 Oct 2026 17:52:02 +0100 Subject: [PATCH 82/92] btrfs: qgroup: merge error labels in btrfs_quota_enable() There is no need to have 3 different labels to which we goto on error. Simplify this and have a single one, named 'out', where we always free the path and put the root, as btrfs_free_path() and btrfs_put_root() accept NULL arguments and do nothing in that case. Reviewed-by: Qu Wenruo Signed-off-by: Filipe Manana Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/qgroup.c | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/fs/btrfs/qgroup.c b/fs/btrfs/qgroup.c index 9f21b091c545..c9fc3b6a5132 100644 --- a/fs/btrfs/qgroup.c +++ b/fs/btrfs/qgroup.c @@ -999,7 +999,7 @@ static int btrfs_clean_quota_tree(struct btrfs_trans_handle *trans, int btrfs_quota_enable(struct btrfs_fs_info *fs_info, struct btrfs_ioctl_quota_ctl_args *quota_ctl_args) { - struct btrfs_root *quota_root; + struct btrfs_root *quota_root = NULL; struct btrfs_root *tree_root = fs_info->tree_root; struct btrfs_path *path = NULL; struct btrfs_qgroup_status_item *ptr; @@ -1076,6 +1076,7 @@ int btrfs_quota_enable(struct btrfs_fs_info *fs_info, quota_root = btrfs_create_tree(trans, BTRFS_QUOTA_TREE_OBJECTID); if (IS_ERR(quota_root)) { ret = PTR_ERR(quota_root); + quota_root = NULL; btrfs_abort_transaction(trans, ret); goto out; } @@ -1084,7 +1085,7 @@ int btrfs_quota_enable(struct btrfs_fs_info *fs_info, if (unlikely(!path)) { ret = -ENOMEM; btrfs_abort_transaction(trans, ret); - goto out_free_root; + goto out; } key.objectid = 0; @@ -1095,7 +1096,7 @@ int btrfs_quota_enable(struct btrfs_fs_info *fs_info, sizeof(*ptr)); if (unlikely(ret)) { btrfs_abort_transaction(trans, ret); - goto out_free_path; + goto out; } leaf = path->nodes[0]; @@ -1131,7 +1132,7 @@ int btrfs_quota_enable(struct btrfs_fs_info *fs_info, goto out_add_root; if (unlikely(ret < 0)) { btrfs_abort_transaction(trans, ret); - goto out_free_path; + goto out; } while (1) { @@ -1150,14 +1151,14 @@ int btrfs_quota_enable(struct btrfs_fs_info *fs_info, if (unlikely(!prealloc)) { ret = -ENOMEM; btrfs_abort_transaction(trans, ret); - goto out_free_path; + goto out; } ret = add_qgroup_item(trans, quota_root, found_key.offset); if (unlikely(ret)) { btrfs_abort_transaction(trans, ret); - goto out_free_path; + goto out; } qgroup = add_qgroup_rb(fs_info, prealloc, found_key.offset); @@ -1165,13 +1166,13 @@ int btrfs_quota_enable(struct btrfs_fs_info *fs_info, ret = btrfs_sysfs_add_one_qgroup(fs_info, qgroup); if (unlikely(ret < 0)) { btrfs_abort_transaction(trans, ret); - goto out_free_path; + goto out; } ret = btrfs_search_slot_for_read(tree_root, &found_key, path, 1, 0); if (unlikely(ret < 0)) { btrfs_abort_transaction(trans, ret); - goto out_free_path; + goto out; } if (ret > 0) { /* @@ -1188,7 +1189,7 @@ int btrfs_quota_enable(struct btrfs_fs_info *fs_info, ret = btrfs_next_item(tree_root, path); if (unlikely(ret < 0)) { btrfs_abort_transaction(trans, ret); - goto out_free_path; + goto out; } if (ret) break; @@ -1199,21 +1200,21 @@ int btrfs_quota_enable(struct btrfs_fs_info *fs_info, ret = add_qgroup_item(trans, quota_root, BTRFS_FS_TREE_OBJECTID); if (unlikely(ret)) { btrfs_abort_transaction(trans, ret); - goto out_free_path; + goto out; } ASSERT(prealloc == NULL); prealloc = kzalloc_obj(*prealloc, GFP_NOFS); if (!prealloc) { ret = -ENOMEM; - goto out_free_path; + goto out; } qgroup = add_qgroup_rb(fs_info, prealloc, BTRFS_FS_TREE_OBJECTID); prealloc = NULL; ret = btrfs_sysfs_add_one_qgroup(fs_info, qgroup); if (unlikely(ret < 0)) { btrfs_abort_transaction(trans, ret); - goto out_free_path; + goto out; } /* @@ -1245,7 +1246,7 @@ int btrfs_quota_enable(struct btrfs_fs_info *fs_info, clear_bit(BTRFS_FS_SQUOTA_ENABLING, &fs_info->flags); fs_info->qgroup_enable_gen = 0; } - goto out_free_path; + goto out; } /* @@ -1262,7 +1263,7 @@ int btrfs_quota_enable(struct btrfs_fs_info *fs_info, /* Skip rescan for simple qgroups. */ if (btrfs_qgroup_mode(fs_info) == BTRFS_QGROUP_MODE_SIMPLE) - goto out_free_path; + goto out; ret = qgroup_rescan_init(fs_info, 0, 1); if (!ret) { @@ -1287,18 +1288,15 @@ int btrfs_quota_enable(struct btrfs_fs_info *fs_info, ret = 0; } -out_free_path: - btrfs_free_path(path); -out_free_root: - if (ret) - btrfs_put_root(quota_root); out: + btrfs_free_path(path); if (ret) { /* * Free all qgroups previously added with add_qgroup_rb() and * sysfs entries. */ btrfs_free_qgroup_config(fs_info); + btrfs_put_root(quota_root); } mutex_unlock(&fs_info->qgroup_ioctl_lock); if (ret && trans) From 599fdbf3e309e0e6227c67629a520c98059e2fff Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Fri, 2 Oct 2026 18:03:06 +0100 Subject: [PATCH 83/92] btrfs: abort transaction on qgroup failures in create_pending_snapshot() If we get an error when calling one of the qgroup functions, we jump to the 'fail' label without aborting the transaction. This is not a bug as up the call chain (transaction commit path), we will end up aborting the transaction. However having the explicit transaction abort in create_pending_snapshot() allows us to get a stack trace and log message that tells us exactly where we failed, useful for troubleshooting, and also adds consistency since in that function we abort the transaction in every other error path. Reviewed-by: Qu Wenruo Signed-off-by: Filipe Manana Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/transaction.c | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/fs/btrfs/transaction.c b/fs/btrfs/transaction.c index 286a9d455d26..8df7b47b09e0 100644 --- a/fs/btrfs/transaction.c +++ b/fs/btrfs/transaction.c @@ -1920,14 +1920,21 @@ static noinline int create_pending_snapshot(struct btrfs_trans_handle *trans, * To co-operate with that hack, we do hack again. * Or snapshot will be greatly slowed down by a subtree qgroup rescan */ - if (btrfs_qgroup_mode(fs_info) == BTRFS_QGROUP_MODE_FULL) + if (btrfs_qgroup_mode(fs_info) == BTRFS_QGROUP_MODE_FULL) { ret = qgroup_account_snapshot(trans, root, parent_root, pending->inherit, objectid); - else if (btrfs_qgroup_mode(fs_info) == BTRFS_QGROUP_MODE_SIMPLE) + if (unlikely(ret < 0)) { + btrfs_abort_transaction(trans, ret); + goto fail; + } + } else if (btrfs_qgroup_mode(fs_info) == BTRFS_QGROUP_MODE_SIMPLE) { ret = btrfs_qgroup_inherit(trans, btrfs_root_id(root), objectid, btrfs_root_id(parent_root), pending->inherit); - if (unlikely(ret < 0)) - goto fail; + if (unlikely(ret < 0)) { + btrfs_abort_transaction(trans, ret); + goto fail; + } + } ret = btrfs_insert_dir_item(trans, &fname.disk_name, parent_inode, &key, BTRFS_FT_DIR, index, NULL); From 9685445df748a7a33edb6b121350fae4af09c681 Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Fri, 2 Oct 2026 19:44:02 +0100 Subject: [PATCH 84/92] btrfs: qgroup: fix off-by-one max level check in qgroup_trace_new_subtree_blocks() We have maximum of 8 (BTRFS_MAX_LEVEL) levels in a btree, going from 0 to 7, so the upper bound checks should be: >= BTRFS_MAX_LEVEL and not: >= BTRFS_MAX_LEVEL - 1 In practice this will hardly ever be a problem since having a btree with 8 levels would imply a massively huge fs. Reviewed-by: Qu Wenruo Signed-off-by: Filipe Manana Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/qgroup.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fs/btrfs/qgroup.c b/fs/btrfs/qgroup.c index c9fc3b6a5132..54d82d6fd4bb 100644 --- a/fs/btrfs/qgroup.c +++ b/fs/btrfs/qgroup.c @@ -2491,8 +2491,8 @@ static int qgroup_trace_new_subtree_blocks(struct btrfs_trans_handle* trans, int i; /* Level sanity check */ - if (unlikely(cur_level < 0 || cur_level >= BTRFS_MAX_LEVEL - 1 || - root_level < 0 || root_level >= BTRFS_MAX_LEVEL - 1 || + if (unlikely(cur_level < 0 || cur_level >= BTRFS_MAX_LEVEL || + root_level < 0 || root_level >= BTRFS_MAX_LEVEL || root_level < cur_level)) { btrfs_err_rl(fs_info, "%s: bad levels, cur_level=%d root_level=%d", From 784cd5a83476c6c720521f21350fb823928555ff Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Mon, 5 Oct 2026 12:47:09 +0100 Subject: [PATCH 85/92] btrfs: fix lost error return value in btrfs_alloc_logged_file_extent() If we fail to allocate a reserved file extent we then call btrfs_record_squota_delta() and assign its return value to 'ret', discarding the error from the failed allocation attempt and allowing log replay to proceed as if no errors happened, resulting in some replayed file extent item to point to an unallocated extent. At the moment btrfs_record_squota_delta() always returns 0 (success). Fixes: cecbb533b5fc ("btrfs: record simple quota deltas in delayed refs") Reviewed-by: Qu Wenruo Signed-off-by: Filipe Manana Signed-off-by: David Sterba --- fs/btrfs/extent-tree.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fs/btrfs/extent-tree.c b/fs/btrfs/extent-tree.c index a0d5ab03aae2..15b4ed56ce71 100644 --- a/fs/btrfs/extent-tree.c +++ b/fs/btrfs/extent-tree.c @@ -5227,7 +5227,8 @@ int btrfs_alloc_logged_file_extent(struct btrfs_trans_handle *trans, offset, ins, 1, root_objectid); if (ret) btrfs_pin_extent(trans, ins->objectid, ins->offset); - ret = btrfs_record_squota_delta(fs_info, &delta); + else + ret = btrfs_record_squota_delta(fs_info, &delta); btrfs_put_block_group(block_group); return ret; } From c97b139c6ceeb69b264e0a87f2a47bab651a51a8 Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Mon, 5 Oct 2026 13:03:53 +0100 Subject: [PATCH 86/92] btrfs: qgroup: make btrfs_record_squota_delta() return void The function never returns any error, so simplify it and its callers by making it return void. Reviewed-by: Qu Wenruo Signed-off-by: Filipe Manana Signed-off-by: David Sterba --- fs/btrfs/extent-tree.c | 10 +++------- fs/btrfs/qgroup.c | 14 +++++--------- fs/btrfs/qgroup.h | 4 ++-- 3 files changed, 10 insertions(+), 18 deletions(-) diff --git a/fs/btrfs/extent-tree.c b/fs/btrfs/extent-tree.c index 15b4ed56ce71..9b0a5c6b96f3 100644 --- a/fs/btrfs/extent-tree.c +++ b/fs/btrfs/extent-tree.c @@ -1658,7 +1658,7 @@ static int run_delayed_data_ref(struct btrfs_trans_handle *trans, href->owning_root); free_head_ref_squota_rsv(trans->fs_info, href); if (!ret) - ret = btrfs_record_squota_delta(trans->fs_info, &delta); + btrfs_record_squota_delta(trans->fs_info, &delta); } else if (node->action == BTRFS_ADD_DELAYED_REF) { ret = __btrfs_inc_extent_ref(trans, node, extent_op); } else if (node->action == BTRFS_DROP_DELAYED_REF) { @@ -3180,11 +3180,7 @@ static int do_free_extent_accounting(struct btrfs_trans_handle *trans, } } - ret = btrfs_record_squota_delta(trans->fs_info, delta); - if (unlikely(ret)) { - btrfs_abort_transaction(trans, ret); - return ret; - } + btrfs_record_squota_delta(trans->fs_info, delta); /* If remapped, FST has already been taken care of in remove_range_from_remap_tree(). */ if (!remapped) { @@ -5228,7 +5224,7 @@ int btrfs_alloc_logged_file_extent(struct btrfs_trans_handle *trans, if (ret) btrfs_pin_extent(trans, ins->objectid, ins->offset); else - ret = btrfs_record_squota_delta(fs_info, &delta); + btrfs_record_squota_delta(fs_info, &delta); btrfs_put_block_group(block_group); return ret; } diff --git a/fs/btrfs/qgroup.c b/fs/btrfs/qgroup.c index 54d82d6fd4bb..cf8dfd5c692b 100644 --- a/fs/btrfs/qgroup.c +++ b/fs/btrfs/qgroup.c @@ -4966,10 +4966,9 @@ void btrfs_qgroup_destroy_extent_records(struct btrfs_transaction *trans) xa_destroy(&trans->delayed_refs.dirty_extents); } -int btrfs_record_squota_delta(struct btrfs_fs_info *fs_info, - const struct btrfs_squota_delta *delta) +void btrfs_record_squota_delta(struct btrfs_fs_info *fs_info, + const struct btrfs_squota_delta *delta) { - int ret; struct btrfs_qgroup *qgroup; struct btrfs_qgroup *qg; LIST_HEAD(qgroup_list); @@ -4979,24 +4978,22 @@ int btrfs_record_squota_delta(struct btrfs_fs_info *fs_info, if (btrfs_qgroup_mode(fs_info) != BTRFS_QGROUP_MODE_SIMPLE && !test_bit(BTRFS_FS_SQUOTA_ENABLING, &fs_info->flags)) - return 0; + return; if (!btrfs_is_fstree(root)) - return 0; + return; /* If the extent predates enabling quotas, don't count it. */ if (delta->generation < fs_info->qgroup_enable_gen) - return 0; + return; spin_lock(&fs_info->qgroup_lock); qgroup = find_qgroup_rb(fs_info, root); if (WARN_ON_ONCE(!qgroup)) { btrfs_warn(fs_info, "squota failed to find qgroup for root %llu", root); - ret = 0; goto out; } - ret = 0; qgroup_iterator_add(&qgroup_list, qgroup); list_for_each_entry(qg, &qgroup_list, iterator) { struct btrfs_qgroup_list *glist; @@ -5022,5 +5019,4 @@ int btrfs_record_squota_delta(struct btrfs_fs_info *fs_info, out: spin_unlock(&fs_info->qgroup_lock); - return ret; } diff --git a/fs/btrfs/qgroup.h b/fs/btrfs/qgroup.h index c64b26b09c22..a29b4e216f25 100644 --- a/fs/btrfs/qgroup.h +++ b/fs/btrfs/qgroup.h @@ -427,7 +427,7 @@ int btrfs_qgroup_trace_subtree_after_cow(struct btrfs_trans_handle *trans, struct btrfs_root *root, struct extent_buffer *eb); void btrfs_qgroup_destroy_extent_records(struct btrfs_transaction *trans); bool btrfs_check_quota_leak(const struct btrfs_fs_info *fs_info); -int btrfs_record_squota_delta(struct btrfs_fs_info *fs_info, - const struct btrfs_squota_delta *delta); +void btrfs_record_squota_delta(struct btrfs_fs_info *fs_info, + const struct btrfs_squota_delta *delta); #endif From bbb86c6dc1a8168c4f718b246dc0857321c7ba81 Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Mon, 5 Oct 2026 13:19:41 +0100 Subject: [PATCH 87/92] btrfs: use READ_ONCE() to read the chunk size from the data space_info The chunk size of a space_info is always updated with WRITE_ONCE() (see btrfs_update_space_info_chunk_size()), so when reading it in calc_effective_data_chunk_size() we should use READ_ONCE() to prevent load tearing. We are already using READ_ONCE() in every other function that accesses the chunk size of a space_info. Reviewed-by: Daniel Vacek Signed-off-by: Filipe Manana Signed-off-by: David Sterba --- fs/btrfs/space-info.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fs/btrfs/space-info.c b/fs/btrfs/space-info.c index 39a28e1bec8a..72fa01502461 100644 --- a/fs/btrfs/space-info.c +++ b/fs/btrfs/space-info.c @@ -443,8 +443,8 @@ static u64 calc_effective_data_chunk_size(const struct btrfs_fs_info *fs_info) */ data_sinfo = btrfs_find_space_info(fs_info, BTRFS_BLOCK_GROUP_DATA); if (btrfs_is_zoned(fs_info)) - return data_sinfo->chunk_size; - data_chunk_size = min(data_sinfo->chunk_size, + return READ_ONCE(data_sinfo->chunk_size); + data_chunk_size = min(READ_ONCE(data_sinfo->chunk_size), mult_perc(fs_info->fs_devices->total_rw_bytes, 10)); return min_t(u64, data_chunk_size, SZ_1G); } From 04136cd21c64cc7036d768e92a781347bb425d25 Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Wed, 7 Oct 2026 15:41:21 +0100 Subject: [PATCH 88/92] btrfs: defrag: fix key comparison with max defrag key in btrfs_defrag_leaves() The comparison can not be done in isolation for each key component (type and offset) after the objectid component. For example if the defrag_max key is (300 2 0) and the defrag_progress key is (301 1 0), the existing comparison makes us jump to the 'done' label with 'ret' set to -EAGAIN because 2 > 1, resulting in continuing to defrag despite having passed the objectid of 300. Fix this by doing the comparison correctly with btrfs_comp_cpu_keys(). Reviewed-by: Qu Wenruo Signed-off-by: Filipe Manana Signed-off-by: David Sterba --- fs/btrfs/defrag.c | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/fs/btrfs/defrag.c b/fs/btrfs/defrag.c index 6ec5dd760d42..75c87aee5de9 100644 --- a/fs/btrfs/defrag.c +++ b/fs/btrfs/defrag.c @@ -530,11 +530,7 @@ static int btrfs_defrag_leaves(struct btrfs_trans_handle *trans, out: btrfs_free_path(path); if (ret == -EAGAIN) { - if (root->defrag_max.objectid > root->defrag_progress.objectid) - goto done; - if (root->defrag_max.type > root->defrag_progress.type) - goto done; - if (root->defrag_max.offset > root->defrag_progress.offset) + if (btrfs_comp_cpu_keys(&root->defrag_max, &root->defrag_progress) > 0) goto done; ret = 0; } From eb55bc2bfe204e6bfccfe179226d7fb5024cb010 Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Wed, 7 Oct 2026 16:08:50 +0100 Subject: [PATCH 89/92] btrfs: defrag: fix check to see if next extent can be defragged too We want to check if the next extent is at maximum capacity but we are passing the current extent map to get_extent_max_capacity() and not the next extent map. For example if the current extent is compressed (its maximum capacity is 128K) and the next extent is uncompressed (its maximum capacity is 128M), then we can bail out and not defrag the next extent too - when for example the next extent as a size of 128K, is uncompressed, and the extent threshold is >= 256K. Fixes: 979b25c300db ("btrfs: defrag: don't defrag extents which are already at max capacity") Reviewed-by: Qu Wenruo Signed-off-by: Filipe Manana Signed-off-by: David Sterba --- fs/btrfs/defrag.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/btrfs/defrag.c b/fs/btrfs/defrag.c index 75c87aee5de9..7010d714cfe1 100644 --- a/fs/btrfs/defrag.c +++ b/fs/btrfs/defrag.c @@ -811,7 +811,7 @@ static bool defrag_check_next_extent(struct inode *inode, struct extent_map *em, * If the next extent is at its max capacity, defragging current extent * makes no sense, as the total number of extents won't change. */ - if (next->len >= get_extent_max_capacity(fs_info, em)) + if (next->len >= get_extent_max_capacity(fs_info, next)) goto out; /* Skip older extent */ if (next->generation < newer_than) From 9985f1643ac3f8595862e2f82108f0ccae4cf959 Mon Sep 17 00:00:00 2001 From: Hengyu Liang Date: Fri, 9 Oct 2026 13:33:59 -0400 Subject: [PATCH 90/92] btrfs: return -ENOTTY for unknown ioctls on the control device Commit 5ab2b180884c ("btrfs: factor out validation of btrfs_ioctl_vol_args::name") made btrfs_control_ioctl() store the result of btrfs_check_ioctl_vol_args_path() in ret before it looks at the command. However, ret is initialized to -ENOTTY and the switch has no default case, so this initial value was what an unknown command returned. As of now, every unknown ioctl on /dev/btrfs-control returns 0. A tool that tries a control ioctl which the running kernel does not have is told that the ioctl succeeded. The issue can be reproduced with a simple C program, run as root: #include #include #include int main(void) { static char buf[4096]; int fd = open("/dev/btrfs-control", O_RDWR); if (ioctl(fd, TCGETS, buf) < 0) perror("ioctl"); else printf("ioctl succeeded\n"); return 0; } Before commit 5ab2b180884c ("btrfs: factor out validation of btrfs_ioctl_vol_args::name"), the result is: ioctl: Inappropriate ioctl for device After that commit, the result is: ioctl succeeded This patch will add a default case that returns -ENOTTY. Fixes: 5ab2b180884c ("btrfs: factor out validation of btrfs_ioctl_vol_args::name") CC: stable@vger.kernel.org Reviewed-by: Qu Wenruo Signed-off-by: Hengyu Liang Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/super.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/fs/btrfs/super.c b/fs/btrfs/super.c index 8d01409ed382..7fabb775d4e3 100644 --- a/fs/btrfs/super.c +++ b/fs/btrfs/super.c @@ -2329,6 +2329,9 @@ static long btrfs_control_ioctl(struct file *file, unsigned int cmd, case BTRFS_IOC_GET_SUPPORTED_FEATURES: ret = btrfs_ioctl_get_supported_features((void __user*)arg); break; + default: + ret = -ENOTTY; + break; } out: From 940983a3000361ddff834a1c55bdb0e9e47ed88b Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Fri, 9 Oct 2026 08:43:39 +1030 Subject: [PATCH 91/92] btrfs: prevent use-after-free in btrfs_transaction::switch_commits If a transaction is aborted during committing, btrfs_transaction::switch_commits can be non-empty, but the btrfs_transaction structure can be freed at the last btrfs_put_transaction() inside cleanup_transaction(). The btrfs_root::dirty_list entry still linked into switch_commits will point to already released memory, causing use-after-free. Address this problem by manually deleting all entries inside btrfs_transaction::switch_commits. And since we're here, also release the dirty_log_pages and qgroup swapped blocks for that root, which are only released during a successful switch_commit_roots() call. Reviewed-by: Boris Burkov Signed-off-by: Qu Wenruo Signed-off-by: David Sterba --- fs/btrfs/disk-io.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/fs/btrfs/disk-io.c b/fs/btrfs/disk-io.c index 66b054eb98c6..02adda065097 100644 --- a/fs/btrfs/disk-io.c +++ b/fs/btrfs/disk-io.c @@ -4966,6 +4966,16 @@ void btrfs_cleanup_one_transaction(struct btrfs_transaction *cur_trans) list_del_init(&dev->post_commit_list); } + while (!list_empty(&cur_trans->switch_commits)) { + struct btrfs_root *root; + + root = list_first_entry(&cur_trans->switch_commits, + struct btrfs_root, dirty_list); + list_del_init(&root->dirty_list); + btrfs_extent_io_tree_release(&root->dirty_log_pages); + btrfs_qgroup_clean_swapped_blocks(root); + } + btrfs_destroy_delayed_refs(cur_trans); cur_trans->state = TRANS_STATE_COMMIT_START; From 58e844c3ba387ceb0f527a6ba82aa50c81c4d7b7 Mon Sep 17 00:00:00 2001 From: Yalagada Pavan Kumar Date: Fri, 9 Oct 2026 08:43:40 +1030 Subject: [PATCH 92/92] btrfs: fix use-after-free on quota enable allocation failure [BUG] There is a syzbot report that a use-after-free bug is triggered after a failed btrfs_quota_enable(): loop0: detected capacity change from 32768 to 0 ================================================================== BUG: KASAN: slab-use-after-free in __list_add_valid_or_report+0x4e/0x130 lib/list_debug.c:29 Read of size 8 at addr ffff888040e0a528 by task syz.0.0/5324 CPU: 0 UID: 0 PID: 5324 Comm: syz.0.0 Not tainted syzkaller #0 PREEMPT(full) Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.16.3-debian-1.16.3-2 04/01/2014 Call Trace: dump_stack_lvl+0xe8/0x150 lib/dump_stack.c:120 print_address_description+0x55/0x1e0 mm/kasan/report.c:378 print_report+0x58/0x70 mm/kasan/report.c:482 kasan_report+0x117/0x150 mm/kasan/report.c:595 __list_add_valid_or_report+0x4e/0x130 lib/list_debug.c:29 __list_add_valid include/linux/list.h:104 [inline] __list_add include/linux/list.h:169 [inline] list_add include/linux/list.h:192 [inline] list_move include/linux/list.h:345 [inline] add_root_to_dirty_list+0x38b/0x470 fs/btrfs/ctree.c:232 btrfs_force_cow_block+0xf09/0x1bf0 fs/btrfs/ctree.c:553 btrfs_cow_block+0x3f1/0xaf0 fs/btrfs/ctree.c:696 btrfs_search_slot+0xd70/0x2d20 fs/btrfs/ctree.c:-1 btrfs_search_prev_slot fs/btrfs/free-space-tree.c:134 [inline] remove_free_space_extent fs/btrfs/free-space-tree.c:732 [inline] __btrfs_remove_from_free_space_tree fs/btrfs/free-space-tree.c:837 [inline] btrfs_remove_from_free_space_tree+0x4d6/0xda0 fs/btrfs/free-space-tree.c:866 alloc_reserved_extent+0x4a/0x2b0 fs/btrfs/extent-tree.c:4976 alloc_reserved_tree_block fs/btrfs/extent-tree.c:5155 [inline] run_delayed_tree_ref fs/btrfs/extent-tree.c:1815 [inline] run_one_delayed_ref fs/btrfs/extent-tree.c:1851 [inline] btrfs_run_delayed_refs_for_head fs/btrfs/extent-tree.c:2058 [inline] __btrfs_run_delayed_refs+0x18b5/0x43b0 fs/btrfs/extent-tree.c:2134 btrfs_run_delayed_refs+0xdc/0x2a0 fs/btrfs/extent-tree.c:2246 btrfs_commit_transaction+0x28a/0x3170 fs/btrfs/transaction.c:2267 sync_filesystem+0x1d2/0x240 fs/sync.c:66 btrfs_reconfigure+0x2dc/0x2100 fs/btrfs/super.c:1511 reconfigure_super+0x232/0x8f0 fs/super.c:1128 do_remount fs/namespace.c:3410 [inline] path_mount+0xd4e/0x1050 fs/namespace.c:4160 do_mount fs/namespace.c:4181 [inline] __do_sys_mount fs/namespace.c:4397 [inline] __se_sys_mount+0x31d/0x420 fs/namespace.c:4374 do_syscall_x64 arch/x86/entry/syscall_64.c:61 [inline] do_syscall_64+0x166/0x520 arch/x86/entry/syscall_64.c:84 entry_SYSCALL_64_after_hwframe+0x77/0x7f RIP: 0033:0x7f9c8dd9f3ca Allocated by task 5323: kasan_save_stack mm/kasan/common.c:57 [inline] kasan_save_track+0x3e/0x80 mm/kasan/common.c:78 poison_kmalloc_redzone mm/kasan/common.c:409 [inline] __kasan_kmalloc+0x93/0xb0 mm/kasan/common.c:426 kasan_kmalloc include/linux/kasan.h:263 [inline] __kmalloc_cache_noprof+0x321/0x600 mm/slub.c:5563 _kmalloc_noprof include/linux/slab.h:991 [inline] _kzalloc_noprof include/linux/slab.h:1312 [inline] btrfs_alloc_root+0x75/0x840 fs/btrfs/disk-io.c:638 btrfs_create_tree+0xa8/0x5c0 fs/btrfs/disk-io.c:832 btrfs_quota_enable+0x3d6/0x1e20 fs/btrfs/qgroup.c:1074 btrfs_ioctl_quota_ctl+0x186/0x1f0 fs/btrfs/ioctl.c:3581 vfs_ioctl fs/ioctl.c:51 [inline] __do_sys_ioctl fs/ioctl.c:597 [inline] __se_sys_ioctl+0xfc/0x170 fs/ioctl.c:583 do_syscall_x64 arch/x86/entry/syscall_64.c:61 [inline] do_syscall_64+0x166/0x520 arch/x86/entry/syscall_64.c:84 entry_SYSCALL_64_after_hwframe+0x77/0x7f Freed by task 5323: kasan_save_stack mm/kasan/common.c:57 [inline] kasan_save_track+0x3e/0x80 mm/kasan/common.c:78 kasan_save_free_info+0x40/0x50 mm/kasan/generic.c:584 poison_slab_object mm/kasan/common.c:264 [inline] __kasan_slab_free+0x5c/0x80 mm/kasan/common.c:296 kasan_slab_free include/linux/kasan.h:235 [inline] slab_free_hook mm/slub.c:2748 [inline] slab_free mm/slub.c:6508 [inline] kfree+0x1c5/0x650 mm/slub.c:6801 btrfs_quota_enable+0x1208/0x1e20 fs/btrfs/qgroup.c:1292 btrfs_ioctl_quota_ctl+0x186/0x1f0 fs/btrfs/ioctl.c:3581 vfs_ioctl fs/ioctl.c:51 [inline] __do_sys_ioctl fs/ioctl.c:597 [inline] __se_sys_ioctl+0xfc/0x170 fs/ioctl.c:583 do_syscall_x64 arch/x86/entry/syscall_64.c:61 [inline] do_syscall_64+0x166/0x520 arch/x86/entry/syscall_64.c:84 entry_SYSCALL_64_after_hwframe+0x77/0x7f [CAUSE] During btrfs_quota_enable(), a temporary quota_root is created, then qgroup items are created for each subvolume and inserted into that root. If there are enough subvolumes, the level of quota_root will be bumped, and add_root_to_dirty_list() will be called, adding quota_root->dirty_list into fs_info->dirty_cowonly_roots. But if a problem is hit during btrfs_quota_enable() later, that quota_root will be released through btrfs_put_root(), causing the whole quota_root structure to be released, while leaving fs_info->dirty_cowonly_roots still pointing to the released quota_root->dirty_list. Later access through fs_info->dirty_cowonly_roots will trigger a use-after-free bug. [FIX] We can just call list_del_init(&root->dirty_list) when btrfs_quota_enable() failed. Also to follow the existing error handling behavior, abort the transaction when the memory preallocation failed. Reported-by: syzbot+947286c775f432b073a8@syzkaller.appspotmail.com Link: https://lore.kernel.org/linux-btrfs/6ac5a5e1.9ad129c8.3ce5b3.0010.GAE@google.com/ Closes: https://syzkaller.appspot.com/bug?extid=947286c775f432b073a8 Reviewed-by: Filipe Manana Signed-off-by: Yalagada Pavan Kumar Reviewed-by: Boris Burkov Reviewed-by: Qu Wenruo [ Change the commit message to include a proper analysis. ] Signed-off-by: Qu Wenruo Signed-off-by: David Sterba --- fs/btrfs/qgroup.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/fs/btrfs/qgroup.c b/fs/btrfs/qgroup.c index cf8dfd5c692b..a0651fbfe778 100644 --- a/fs/btrfs/qgroup.c +++ b/fs/btrfs/qgroup.c @@ -1207,6 +1207,7 @@ int btrfs_quota_enable(struct btrfs_fs_info *fs_info, prealloc = kzalloc_obj(*prealloc, GFP_NOFS); if (!prealloc) { ret = -ENOMEM; + btrfs_abort_transaction(trans, ret); goto out; } qgroup = add_qgroup_rb(fs_info, prealloc, BTRFS_FS_TREE_OBJECTID); @@ -1296,6 +1297,12 @@ int btrfs_quota_enable(struct btrfs_fs_info *fs_info, * sysfs entries. */ btrfs_free_qgroup_config(fs_info); + + if (quota_root) { + spin_lock(&fs_info->trans_lock); + list_del_init("a_root->dirty_list); + spin_unlock(&fs_info->trans_lock); + } btrfs_put_root(quota_root); } mutex_unlock(&fs_info->qgroup_ioctl_lock);