]> git.ipfire.org Git - thirdparty/linux.git/commitdiff
btrfs: avoid access-beyond-folio for bs > ps encoded writes
authorQu Wenruo <wqu@suse.com>
Sun, 7 Dec 2025 04:23:20 +0000 (14:53 +1030)
committerDavid Sterba <dsterba@suse.com>
Tue, 6 Jan 2026 00:22:59 +0000 (01:22 +0100)
[POTENTIAL BUG]
If the system page size is 4K and fs block size is 8K, and max_inline
mount option is set to 6K, we can inline a 6K sized data extent.

Then a encoded write submitted a compressed extent which is at file
offset 0, and the compressed length is 6K, which is allowed to be inlined.

Now a read beyond page boundary is triggered inside write_extent_buffer()
from insert_inline_extent().

[CAUSE]
Currently the function __cow_file_range_inline() can only accept a
single folio.

For regular compressed write path, we always allocate the compressed
folios using the minimal order matching the block size, thus the
@compressed_folio should always cover a full fs block thus it is fine.

But for encoded writes, they allocate page size folios, this means we
can hit a case where the compressed data is smaller than block size but
still larger than page size, in that case __cow_file_range_inline() will
be called with @compressed_size larger than a page.

In that case we will trigger a read beyond the folio inside
insert_inline_extent().

Thankfully this is not that common, as the default max_inline is only
2048 bytes, smaller than PAGE_SIZE, and bs > ps support is still
experimental.

[FIX]
We need to either allow insert_inline_extent() to accept a page array to
properly support such case, or reject such inline extent.

The latter is a much simpler solution, and considering bs > ps will stay
as a corner case and non-default max_inline will be even rarer, I don't
think we really need to fulfill such niche.

So just reject any inline extent that's larger than PAGE_SIZE, and add
an extra ASSERT() to insert_inline_extent() to catch such beyond-boundary
access.

Fixes: ec20799064c8 ("btrfs: enable encoded read/write/send for bs > ps cases")
Signed-off-by: Qu Wenruo <wqu@suse.com>
Reviewed-by: David Sterba <dsterba@suse.com>
Signed-off-by: David Sterba <dsterba@suse.com>
fs/btrfs/inode.c

index 03337fa7a61c05b3ec861b3faf1d3ce39d287399..bfb8e98d8c02a575e1091bc6d7e87c77489417e2 100644 (file)
@@ -480,13 +480,15 @@ static int insert_inline_extent(struct btrfs_trans_handle *trans,
        ASSERT(size <= sectorsize);
 
        /*
-        * The compressed size also needs to be no larger than a sector.
-        * That's also why we only need one page as the parameter.
+        * The compressed size also needs to be no larger than a page.
+        * That's also why we only need one folio as the parameter.
         */
-       if (compressed_folio)
+       if (compressed_folio) {
                ASSERT(compressed_size <= sectorsize);
-       else
+               ASSERT(compressed_size <= PAGE_SIZE);
+       } else {
                ASSERT(compressed_size == 0);
+       }
 
        if (compressed_size && compressed_folio)
                cur_size = compressed_size;
@@ -573,6 +575,18 @@ static bool can_cow_file_range_inline(struct btrfs_inode *inode,
        if (offset != 0)
                return false;
 
+       /*
+        * Even for bs > ps cases, cow_file_range_inline() can only accept a
+        * single folio.
+        *
+        * This can be problematic and cause access beyond page boundary if a
+        * page sized folio is passed into that function.
+        * And encoded write is doing exactly that.
+        * So here limits the inlined extent size to PAGE_SIZE.
+        */
+       if (size > PAGE_SIZE || compressed_size > PAGE_SIZE)
+               return false;
+
        /* Inline extents are limited to sectorsize. */
        if (size > fs_info->sectorsize)
                return false;