From patchwork Fri Apr 11 05:14:00 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Qu Wenruo X-Patchwork-Id: 14047558 Received: from smtp-out1.suse.de (smtp-out1.suse.de [195.135.223.130]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id DEF0A6FB9 for ; Fri, 11 Apr 2025 05:14:28 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=195.135.223.130 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1744348471; cv=none; b=sBg2xO0DJpMwg9bG+vptwVr6yFBN3p/CYc1kFHuP34UQEyYp1e4gGVYfOHI2SDVKv0u54hgTZXigeCu1iTOmrQM5rZeB8MB2wpNP7ofVw1L4LHJl9pPh8juNFOF0Y0BsMMNGcSDFWzWYaa5SDhSsNyRQ18lo9hZzGG1XBpQmCw4= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1744348471; c=relaxed/simple; bh=4PS7UCyS7zXkHSmg9rLmrEcDGHT73/qlLZW5Td7jB7Q=; h=From:To:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=VX4Wx2F3dvk/0UpNURwxK6oJeZ5YdfU6VJEnVcOtfM9Dp1LEL9sPJ5cHbNSHwPg3eckgU74rnzqtLN1cO9GWwW4CXO28hL3TpyltD4k2mmqQqE3DfG1PfFIFIwTeU8BYA3ISKs+LcomT0GkLMPSQi5TI9d1rQImU4yS9bfbreiM= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=quarantine dis=none) header.from=suse.com; spf=pass smtp.mailfrom=suse.com; dkim=pass (1024-bit key) header.d=suse.com header.i=@suse.com header.b=sIqWymze; dkim=pass (1024-bit key) header.d=suse.com header.i=@suse.com header.b=sIqWymze; arc=none smtp.client-ip=195.135.223.130 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=quarantine dis=none) header.from=suse.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=suse.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=suse.com header.i=@suse.com header.b="sIqWymze"; dkim=pass (1024-bit key) header.d=suse.com header.i=@suse.com header.b="sIqWymze" Received: from imap1.dmz-prg2.suse.org (unknown [10.150.64.97]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (4096 bits) server-digest SHA256) (No client certificate requested) by smtp-out1.suse.de (Postfix) with ESMTPS id 267DF21167 for ; Fri, 11 Apr 2025 05:14:21 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=suse.com; s=susede1; t=1744348461; h=from:from:reply-to:date:date:message-id:message-id:to:to:cc: mime-version:mime-version: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=OZ04Z+jT6vZmIIzWikkYhC3ptA43ZW7MDKrbYyYv1YQ=; b=sIqWymzeFFXtLy3xogRyKdz7C8udHKqJBqGCrdkQD5WJOVX+pjttby8Z7Ma1oRq6iMjOy8 wjlKfFSXe8+HbCFOujuOqwfcvxMxutK+J24h5OQ9nnC7V0re8u1Yj1ou/96YJI9liiAHa3 d+X3xjxiUFLo9o384VNLuxe/XVEcups= Authentication-Results: smtp-out1.suse.de; none DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=suse.com; s=susede1; t=1744348461; h=from:from:reply-to:date:date:message-id:message-id:to:to:cc: mime-version:mime-version: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=OZ04Z+jT6vZmIIzWikkYhC3ptA43ZW7MDKrbYyYv1YQ=; b=sIqWymzeFFXtLy3xogRyKdz7C8udHKqJBqGCrdkQD5WJOVX+pjttby8Z7Ma1oRq6iMjOy8 wjlKfFSXe8+HbCFOujuOqwfcvxMxutK+J24h5OQ9nnC7V0re8u1Yj1ou/96YJI9liiAHa3 d+X3xjxiUFLo9o384VNLuxe/XVEcups= Received: from imap1.dmz-prg2.suse.org (localhost [127.0.0.1]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (4096 bits) server-digest SHA256) (No client certificate requested) by imap1.dmz-prg2.suse.org (Postfix) with ESMTPS id 6591213886 for ; Fri, 11 Apr 2025 05:14:20 +0000 (UTC) Received: from dovecot-director2.suse.de ([2a07:de40:b281:106:10:150:64:167]) by imap1.dmz-prg2.suse.org with ESMTPSA id QOucCiyl+Gf9XgAAD6G6ig (envelope-from ) for ; Fri, 11 Apr 2025 05:14:20 +0000 From: Qu Wenruo To: linux-btrfs@vger.kernel.org Subject: [PATCH 1/2] btrfs: make btrfs_truncate_block() to zero involved blocks in a folio Date: Fri, 11 Apr 2025 14:44:00 +0930 Message-ID: <158513ddd60a56c01e9404919a40b8739fc80d83.1744344865.git.wqu@suse.com> X-Mailer: git-send-email 2.49.0 In-Reply-To: References: Precedence: bulk X-Mailing-List: linux-btrfs@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 X-Spam-Level: X-Spamd-Result: default: False [-2.80 / 50.00]; BAYES_HAM(-3.00)[100.00%]; MID_CONTAINS_FROM(1.00)[]; NEURAL_HAM_LONG(-1.00)[-1.000]; R_MISSING_CHARSET(0.50)[]; NEURAL_HAM_SHORT(-0.20)[-1.000]; MIME_GOOD(-0.10)[text/plain]; FUZZY_BLOCKED(0.00)[rspamd.com]; RCVD_VIA_SMTP_AUTH(0.00)[]; RCPT_COUNT_ONE(0.00)[1]; ARC_NA(0.00)[]; DKIM_SIGNED(0.00)[suse.com:s=susede1]; DBL_BLOCKED_OPENRESOLVER(0.00)[suse.com:email,suse.com:mid,imap1.dmz-prg2.suse.org:helo]; FROM_EQ_ENVFROM(0.00)[]; FROM_HAS_DN(0.00)[]; MIME_TRACE(0.00)[0:+]; RCVD_COUNT_TWO(0.00)[2]; TO_MATCH_ENVRCPT_ALL(0.00)[]; TO_DN_NONE(0.00)[]; PREVIOUSLY_DELIVERED(0.00)[linux-btrfs@vger.kernel.org]; RCVD_TLS_ALL(0.00)[] X-Spam-Score: -2.80 X-Spam-Flag: NO [BUG] The following fsx sequence will fail on btrfs with 64K page size and 4K fs block size: #fsx -d -e 1 -N 4 $mnt/junk -S 36386 READ BAD DATA: offset = 0xe9ba, size = 0x6dd5, fname = /mnt/btrfs/junk OFFSET GOOD BAD RANGE 0xe9ba 0x0000 0x03ac 0x0 operation# (mod 256) for the bad data may be 3 ... LOG DUMP (4 total operations): 1( 1 mod 256): WRITE 0x6c62 thru 0x1147d (0xa81c bytes) HOLE ***WWWW 2( 2 mod 256): TRUNCATE DOWN from 0x1147e to 0x5448 ******WWWW 3( 3 mod 256): ZERO 0x1c7aa thru 0x28fe2 (0xc839 bytes) 4( 4 mod 256): MAPREAD 0xe9ba thru 0x1578e (0x6dd5 bytes) ***RRRR*** [CAUSE] Only 2 operations are really involved in this case: 3 pollute_eof 0x5448 thru 0xffff (0xabb8 bytes) 3 zero from 0x1c7aa to 0x28fe3, (0xc839 bytes) 4 mapread 0xe9ba thru 0x1578e (0x6dd5 bytes) At operation 3, fsx pollutes beyond EOF, that is done by mmap() and write into that mmap() range beyondd EOF. Such write will fill the range beyond EOF, but it will never reach disk as ranges beyond EOF will not be marked dirty nor uptodate. Then we zero_range for [0x1c7aa, 0x28fe3], and since the range is beyond our isize (which was 0x5448), we should zero out any range beyond EOF (0x5448). During btrfs_zero_range(), we call btrfs_truncate_block() to dirty the unaligned head block. But that function only really zero out the block at [0x5000, 0x5fff], it doesn't bother any range other that that block, since those range will not be marked dirty nor written back. So the range [0x6000, 0xffff] is still polluted, and later mapread() will return the poisoned value. Such behavior is only exposed when page size is larger than fs block btrfs, as for block size == page size case the block is exactly one page, and fsx only checks exactly one page at EOF. [FIX] Enhance btrfs_truncate_block() by: - Force callers to pass a @start/@end combination So that there will be no 0 length passed in. - Rename the @front parameter to an enum And make it matches the @start/@end parameter better by using TRUNCATE_HEAD_BLOCK and TRUNCATE_TAIL_BLOCK instead. - Pass the original unmodified range into btrfs_truncate_block() There are several call sites inside btrfs_zero_range() and btrfs_punch_hole() where we pass part of the original range for truncating. This hides the original range which can lead to under or over truncating. Thus we have to pass the original zero/punch range. - Make btrfs_truncate_block() to zero any involved blocks inside the folio Since we have the original range, we know exactly which range inside the folio that should be zeroed. It may cover other blocks other than the one with data space reserved, but that's fine, the zeroed range will not be written back anyway. Signed-off-by: Qu Wenruo --- fs/btrfs/btrfs_inode.h | 10 +++++-- fs/btrfs/file.c | 33 ++++++++++++++------- fs/btrfs/inode.c | 65 +++++++++++++++++++++++++++--------------- 3 files changed, 73 insertions(+), 35 deletions(-) diff --git a/fs/btrfs/btrfs_inode.h b/fs/btrfs/btrfs_inode.h index 4e2952cf5766..21b005ddf42c 100644 --- a/fs/btrfs/btrfs_inode.h +++ b/fs/btrfs/btrfs_inode.h @@ -547,8 +547,14 @@ int btrfs_add_link(struct btrfs_trans_handle *trans, struct btrfs_inode *parent_inode, struct btrfs_inode *inode, const struct fscrypt_str *name, int add_backref, u64 index); int btrfs_delete_subvolume(struct btrfs_inode *dir, struct dentry *dentry); -int btrfs_truncate_block(struct btrfs_inode *inode, loff_t from, loff_t len, - int front); + +enum btrfs_truncate_where { + BTRFS_TRUNCATE_HEAD_BLOCK, + BTRFS_TRUNCATE_TAIL_BLOCK, +}; +int btrfs_truncate_block(struct btrfs_inode *inode, loff_t from, loff_t end, + u64 orig_start, u64 orig_end, + enum btrfs_truncate_where where); int btrfs_start_delalloc_snapshot(struct btrfs_root *root, bool in_reclaim_context); int btrfs_start_delalloc_roots(struct btrfs_fs_info *fs_info, long nr, diff --git a/fs/btrfs/file.c b/fs/btrfs/file.c index e3fea1db4304..55fa91799fb6 100644 --- a/fs/btrfs/file.c +++ b/fs/btrfs/file.c @@ -2616,7 +2616,8 @@ static int btrfs_punch_hole(struct file *file, loff_t offset, loff_t len) u64 lockend; u64 tail_start; u64 tail_len; - u64 orig_start = offset; + const u64 orig_start = offset; + const u64 orig_end = offset + len - 1; int ret = 0; bool same_block; u64 ino_size; @@ -2659,7 +2660,8 @@ static int btrfs_punch_hole(struct file *file, loff_t offset, loff_t len) if (offset < ino_size) { truncated_block = true; ret = btrfs_truncate_block(BTRFS_I(inode), offset, len, - 0); + orig_start, orig_end, + BTRFS_TRUNCATE_HEAD_BLOCK); } else { ret = 0; } @@ -2669,7 +2671,9 @@ static int btrfs_punch_hole(struct file *file, loff_t offset, loff_t len) /* zero back part of the first block */ if (offset < ino_size) { truncated_block = true; - ret = btrfs_truncate_block(BTRFS_I(inode), offset, 0, 0); + ret = btrfs_truncate_block(BTRFS_I(inode), offset, -1, + orig_start, orig_end, + BTRFS_TRUNCATE_HEAD_BLOCK); if (ret) { btrfs_inode_unlock(BTRFS_I(inode), BTRFS_ILOCK_MMAP); return ret; @@ -2706,8 +2710,9 @@ static int btrfs_punch_hole(struct file *file, loff_t offset, loff_t len) if (tail_start + tail_len < ino_size) { truncated_block = true; ret = btrfs_truncate_block(BTRFS_I(inode), - tail_start + tail_len, - 0, 1); + tail_start, tail_start + tail_len - 1, + orig_start, orig_end, + BTRFS_TRUNCATE_TAIL_BLOCK); if (ret) goto out_only_mutex; } @@ -2875,6 +2880,8 @@ static int btrfs_zero_range(struct inode *inode, int ret; u64 alloc_hint = 0; const u64 sectorsize = fs_info->sectorsize; + const u64 orig_start = offset; + const u64 orig_end = offset + len - 1; u64 alloc_start = round_down(offset, sectorsize); u64 alloc_end = round_up(offset + len, sectorsize); u64 bytes_to_reserve = 0; @@ -2938,7 +2945,8 @@ static int btrfs_zero_range(struct inode *inode, if (len < sectorsize && em->disk_bytenr != EXTENT_MAP_HOLE) { free_extent_map(em); ret = btrfs_truncate_block(BTRFS_I(inode), offset, len, - 0); + orig_start, orig_end, + BTRFS_TRUNCATE_HEAD_BLOCK); if (!ret) ret = btrfs_fallocate_update_isize(inode, offset + len, @@ -2969,7 +2977,9 @@ static int btrfs_zero_range(struct inode *inode, alloc_start = round_down(offset, sectorsize); ret = 0; } else if (ret == RANGE_BOUNDARY_WRITTEN_EXTENT) { - ret = btrfs_truncate_block(BTRFS_I(inode), offset, 0, 0); + ret = btrfs_truncate_block(BTRFS_I(inode), offset, -1, + orig_start, orig_end, + BTRFS_TRUNCATE_HEAD_BLOCK); if (ret) goto out; } else { @@ -2986,8 +2996,9 @@ static int btrfs_zero_range(struct inode *inode, alloc_end = round_up(offset + len, sectorsize); ret = 0; } else if (ret == RANGE_BOUNDARY_WRITTEN_EXTENT) { - ret = btrfs_truncate_block(BTRFS_I(inode), offset + len, - 0, 1); + ret = btrfs_truncate_block(BTRFS_I(inode), offset, offset + len - 1, + orig_start, orig_end, + BTRFS_TRUNCATE_TAIL_BLOCK); if (ret) goto out; } else { @@ -3107,7 +3118,9 @@ static long btrfs_fallocate(struct file *file, int mode, * need to zero out the end of the block if i_size lands in the * middle of a block. */ - ret = btrfs_truncate_block(BTRFS_I(inode), inode->i_size, 0, 0); + ret = btrfs_truncate_block(BTRFS_I(inode), inode->i_size, -1, + inode->i_size, -1, + BTRFS_TRUNCATE_HEAD_BLOCK); if (ret) goto out; } diff --git a/fs/btrfs/inode.c b/fs/btrfs/inode.c index e283627c087d..0700a161b80e 100644 --- a/fs/btrfs/inode.c +++ b/fs/btrfs/inode.c @@ -4782,15 +4782,16 @@ static int btrfs_rmdir(struct inode *dir, struct dentry *dentry) * * @inode - inode that we're zeroing * @from - the offset to start zeroing - * @len - the length to zero, 0 to zero the entire range respective to the - * offset - * @front - zero up to the offset instead of from the offset on + * @end - the inclusive end to finish zeroing, can be -1 meaning truncating + * everything beyond @from. + * @where - Head or tail block to truncate. * * This will find the block for the "from" offset and cow the block and zero the * part we want to zero. This is used with truncate and hole punching. */ -int btrfs_truncate_block(struct btrfs_inode *inode, loff_t from, loff_t len, - int front) +int btrfs_truncate_block(struct btrfs_inode *inode, loff_t from, loff_t end, + u64 orig_start, u64 orig_end, + enum btrfs_truncate_where where) { struct btrfs_fs_info *fs_info = inode->root->fs_info; struct address_space *mapping = inode->vfs_inode.i_mapping; @@ -4800,20 +4801,30 @@ int btrfs_truncate_block(struct btrfs_inode *inode, loff_t from, loff_t len, struct extent_changeset *data_reserved = NULL; bool only_release_metadata = false; u32 blocksize = fs_info->sectorsize; - pgoff_t index = from >> PAGE_SHIFT; - unsigned offset = from & (blocksize - 1); + pgoff_t index = (where == BTRFS_TRUNCATE_HEAD_BLOCK) ? + (from >> PAGE_SHIFT) : (end >> PAGE_SHIFT); struct folio *folio; gfp_t mask = btrfs_alloc_write_mask(mapping); size_t write_bytes = blocksize; int ret = 0; u64 block_start; u64 block_end; + u64 clamp_start; + u64 clamp_end; - if (IS_ALIGNED(offset, blocksize) && - (!len || IS_ALIGNED(len, blocksize))) + ASSERT(where == BTRFS_TRUNCATE_HEAD_BLOCK || + where == BTRFS_TRUNCATE_TAIL_BLOCK); + + if (end == (loff_t)-1) + ASSERT(where == BTRFS_TRUNCATE_HEAD_BLOCK); + + if (IS_ALIGNED(from, blocksize) && IS_ALIGNED(end + 1, blocksize)) goto out; - block_start = round_down(from, blocksize); + if (where == BTRFS_TRUNCATE_HEAD_BLOCK) + block_start = round_down(from, blocksize); + else + block_start = round_down(end, blocksize); block_end = block_start + blocksize - 1; ret = btrfs_check_data_free_space(inode, &data_reserved, block_start, @@ -4893,17 +4904,22 @@ int btrfs_truncate_block(struct btrfs_inode *inode, loff_t from, loff_t len, goto out_unlock; } - if (offset != blocksize) { - if (!len) - len = blocksize - offset; - if (front) - folio_zero_range(folio, block_start - folio_pos(folio), - offset); - else - folio_zero_range(folio, - (block_start - folio_pos(folio)) + offset, - len); - } + /* + * Although we have only reserved space for the one block, we still should + * zero out all blocks in the original range. + * The remaining blocks normally are already holes thus no need to zero again, + * but it's possible for fs block size < page size cases to have memory mapped + * writes to pollute ranges beyond EOF. + * + * In that case although the polluted blocks beyond EOF will not reach disk, + * it still affects our page cache. + */ + clamp_start = max_t(u64, folio_pos(folio), orig_start); + clamp_end = min_t(u64, folio_pos(folio) + folio_size(folio) - 1, + orig_end); + folio_zero_range(folio, clamp_start - folio_pos(folio), + clamp_end - clamp_start + 1); + btrfs_folio_clear_checked(fs_info, folio, block_start, block_end + 1 - block_start); btrfs_folio_set_dirty(fs_info, folio, block_start, @@ -5005,7 +5021,8 @@ int btrfs_cont_expand(struct btrfs_inode *inode, loff_t oldsize, loff_t size) * rest of the block before we expand the i_size, otherwise we could * expose stale data. */ - ret = btrfs_truncate_block(inode, oldsize, 0, 0); + ret = btrfs_truncate_block(inode, oldsize, -1, oldsize, -1, + BTRFS_TRUNCATE_HEAD_BLOCK); if (ret) return ret; @@ -7649,7 +7666,9 @@ static int btrfs_truncate(struct btrfs_inode *inode, bool skip_writeback) btrfs_end_transaction(trans); btrfs_btree_balance_dirty(fs_info); - ret = btrfs_truncate_block(inode, inode->vfs_inode.i_size, 0, 0); + ret = btrfs_truncate_block(inode, inode->vfs_inode.i_size, -1, + inode->vfs_inode.i_size, -1, + BTRFS_TRUNCATE_HEAD_BLOCK); if (ret) goto out; trans = btrfs_start_transaction(root, 1); From patchwork Fri Apr 11 05:14:01 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Qu Wenruo X-Patchwork-Id: 14047557 Received: from smtp-out2.suse.de (smtp-out2.suse.de [195.135.223.131]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 4F1D76FB9 for ; Fri, 11 Apr 2025 05:14:24 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=195.135.223.131 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1744348466; cv=none; b=Gu8/rA9kaV2nhnCiXi+vXeZieTbQP72v1wsdsCXTJKLQQKVOlh0VZoeezlzZTYkwjhVqaHqkgUdPYg3hU86V4/G1sVy5DJVzmylXUKNrvFLukYo5l1ELt9xUq11+y0D2dr0hhzbFnihe1Pz19h8WCBm9IJramkmj1H4ETwHeoXQ= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1744348466; c=relaxed/simple; bh=sdF9RmFbgnGl/3bZGpclKxeTuIYWeS7v1VM0ooHVNfA=; h=From:To:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=St+powGtkABR2X0rTJ9NgmuSLV63/hjJWNRdlRlppXhIquvN/KPDe+NKiwiVxFGu/pOe+hwj8y8DwonVK5PEMUPSrRAw67jJPhPo6hC6J5zFrU4OyAfTgPYj6FcW4z51fa8d7/G729U7uBRCeuZndLAxG51dZqja/LTvSsPZG/4= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=quarantine dis=none) header.from=suse.com; spf=pass smtp.mailfrom=suse.com; dkim=pass (1024-bit key) header.d=suse.com header.i=@suse.com header.b=RLYU1dYN; dkim=pass (1024-bit key) header.d=suse.com header.i=@suse.com header.b=RLYU1dYN; arc=none smtp.client-ip=195.135.223.131 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=quarantine dis=none) header.from=suse.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=suse.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=suse.com header.i=@suse.com header.b="RLYU1dYN"; dkim=pass (1024-bit key) header.d=suse.com header.i=@suse.com header.b="RLYU1dYN" Received: from imap1.dmz-prg2.suse.org (imap1.dmz-prg2.suse.org [IPv6:2a07:de40:b281:104:10:150:64:97]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (4096 bits) server-digest SHA256) (No client certificate requested) by smtp-out2.suse.de (Postfix) with ESMTPS id 53A281F45B for ; Fri, 11 Apr 2025 05:14:22 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=suse.com; s=susede1; t=1744348462; h=from:from:reply-to:date:date:message-id:message-id:to:to:cc: mime-version:mime-version: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=2sZPn68YsL/HDO4If50phxsThbRPHIDd2xVtoFyHmm8=; b=RLYU1dYNwZUi6v+j5GScbZDFIE6l2Hw94QOCGqpiNo3WuIn+xfW4IgPmR8fKQzSwB5TKv3 Yt+we6r4DYz76o2L61pK/eMdFP9krtGpfiwSMnOW5bAzylGGb3XgyTPS4p6t8y+JrTabq4 46mrc6DNil4TPa4SWqb3DlrPj/XTkoc= Authentication-Results: smtp-out2.suse.de; dkim=pass header.d=suse.com header.s=susede1 header.b=RLYU1dYN DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=suse.com; s=susede1; t=1744348462; h=from:from:reply-to:date:date:message-id:message-id:to:to:cc: mime-version:mime-version: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=2sZPn68YsL/HDO4If50phxsThbRPHIDd2xVtoFyHmm8=; b=RLYU1dYNwZUi6v+j5GScbZDFIE6l2Hw94QOCGqpiNo3WuIn+xfW4IgPmR8fKQzSwB5TKv3 Yt+we6r4DYz76o2L61pK/eMdFP9krtGpfiwSMnOW5bAzylGGb3XgyTPS4p6t8y+JrTabq4 46mrc6DNil4TPa4SWqb3DlrPj/XTkoc= Received: from imap1.dmz-prg2.suse.org (localhost [127.0.0.1]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (4096 bits) server-digest SHA256) (No client certificate requested) by imap1.dmz-prg2.suse.org (Postfix) with ESMTPS id 94BA113886 for ; Fri, 11 Apr 2025 05:14:21 +0000 (UTC) Received: from dovecot-director2.suse.de ([2a07:de40:b281:106:10:150:64:167]) by imap1.dmz-prg2.suse.org with ESMTPSA id AM4qFi2l+Gf9XgAAD6G6ig (envelope-from ) for ; Fri, 11 Apr 2025 05:14:21 +0000 From: Qu Wenruo To: linux-btrfs@vger.kernel.org Subject: [PATCH 2/2] btrfs: make btrfs_truncate_block() zero folio range for certain subpage corner cases Date: Fri, 11 Apr 2025 14:44:01 +0930 Message-ID: X-Mailer: git-send-email 2.49.0 In-Reply-To: References: Precedence: bulk X-Mailing-List: linux-btrfs@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 X-Rspamd-Queue-Id: 53A281F45B X-Spam-Level: X-Spamd-Result: default: False [-3.01 / 50.00]; BAYES_HAM(-3.00)[100.00%]; NEURAL_HAM_LONG(-1.00)[-1.000]; MID_CONTAINS_FROM(1.00)[]; R_MISSING_CHARSET(0.50)[]; R_DKIM_ALLOW(-0.20)[suse.com:s=susede1]; NEURAL_HAM_SHORT(-0.20)[-1.000]; MIME_GOOD(-0.10)[text/plain]; MX_GOOD(-0.01)[]; TO_MATCH_ENVRCPT_ALL(0.00)[]; ARC_NA(0.00)[]; RCVD_VIA_SMTP_AUTH(0.00)[]; FROM_EQ_ENVFROM(0.00)[]; ASN(0.00)[asn:25478, ipnet:::/0, country:RU]; MIME_TRACE(0.00)[0:+]; FROM_HAS_DN(0.00)[]; PREVIOUSLY_DELIVERED(0.00)[linux-btrfs@vger.kernel.org]; RCVD_TLS_ALL(0.00)[]; DBL_BLOCKED_OPENRESOLVER(0.00)[suse.com:email,suse.com:dkim,suse.com:mid,imap1.dmz-prg2.suse.org:helo,imap1.dmz-prg2.suse.org:rdns]; DKIM_SIGNED(0.00)[suse.com:s=susede1]; FUZZY_BLOCKED(0.00)[rspamd.com]; TO_DN_NONE(0.00)[]; RCVD_COUNT_TWO(0.00)[2]; RCPT_COUNT_ONE(0.00)[1]; DKIM_TRACE(0.00)[suse.com:+] X-Rspamd-Server: rspamd2.dmz-prg2.suse.org X-Rspamd-Action: no action X-Spam-Score: -3.01 X-Spam-Flag: NO [BUG] For the following fsx -e 1 run, the btrfs still fails the run on 64K page size with 4K fs block size: READ BAD DATA: offset = 0x26b3a, size = 0xfafa, fname = /mnt/btrfs/junk OFFSET GOOD BAD RANGE 0x26b3a 0x0000 0x15b4 0x0 operation# (mod 256) for the bad data may be 21 [...] LOG DUMP (28 total operations): 1( 1 mod 256): SKIPPED (no operation) 2( 2 mod 256): SKIPPED (no operation) 3( 3 mod 256): SKIPPED (no operation) 4( 4 mod 256): SKIPPED (no operation) 5( 5 mod 256): WRITE 0x1ea90 thru 0x285e0 (0x9b51 bytes) HOLE 6( 6 mod 256): ZERO 0x1b1a8 thru 0x20bd4 (0x5a2d bytes) 7( 7 mod 256): FALLOC 0x22b1a thru 0x272fa (0x47e0 bytes) INTERIOR 8( 8 mod 256): WRITE 0x741d thru 0x13522 (0xc106 bytes) 9( 9 mod 256): MAPWRITE 0x73ee thru 0xdeeb (0x6afe bytes) 10( 10 mod 256): FALLOC 0xb719 thru 0xb994 (0x27b bytes) INTERIOR 11( 11 mod 256): COPY 0x15ed8 thru 0x18be1 (0x2d0a bytes) to 0x25f6e thru 0x28c77 12( 12 mod 256): ZERO 0x1615e thru 0x1770e (0x15b1 bytes) 13( 13 mod 256): SKIPPED (no operation) 14( 14 mod 256): DEDUPE 0x20000 thru 0x27fff (0x8000 bytes) to 0x1000 thru 0x8fff 15( 15 mod 256): SKIPPED (no operation) 16( 16 mod 256): CLONE 0xa000 thru 0xffff (0x6000 bytes) to 0x36000 thru 0x3bfff 17( 17 mod 256): ZERO 0x14adc thru 0x1b78a (0x6caf bytes) 18( 18 mod 256): TRUNCATE DOWN from 0x3c000 to 0x1e2e3 ******WWWW 19( 19 mod 256): CLONE 0x4000 thru 0x11fff (0xe000 bytes) to 0x16000 thru 0x23fff 20( 20 mod 256): FALLOC 0x311e1 thru 0x3681b (0x563a bytes) PAST_EOF 21( 21 mod 256): FALLOC 0x351c5 thru 0x40000 (0xae3b bytes) EXTENDING 22( 22 mod 256): WRITE 0x920 thru 0x7e51 (0x7532 bytes) 23( 23 mod 256): COPY 0x2b58 thru 0xc508 (0x99b1 bytes) to 0x117b1 thru 0x1b161 24( 24 mod 256): TRUNCATE DOWN from 0x40000 to 0x3c9a5 25( 25 mod 256): SKIPPED (no operation) 26( 26 mod 256): MAPWRITE 0x25020 thru 0x26b06 (0x1ae7 bytes) 27( 27 mod 256): SKIPPED (no operation) 28( 28 mod 256): READ 0x26b3a thru 0x36633 (0xfafa bytes) ***RRRR*** [CAUSE] The involved operations are: fallocating to largest ever: 0x40000 21 pollute_eof 0x24000 thru 0x2ffff (0xc000 bytes) 21 falloc from 0x351c5 to 0x40000 (0xae3b bytes) 28 read 0x26b3a thru 0x36633 (0xfafa bytes) At operation #21 a pollute_eof is done, by memory mappaed write into range [0x24000, 0x2ffff). At this stage, the inode size is 0x24000, which is block aligned. Then fallocate happens, and since it's expanding the inode, it will call btrfs_truncate_block() to truncate any unaligned range. But since the inode size is already block aligned, btrfs_truncate_block() does nothing and exit. However remember the folio at 0x20000 has some range polluted already, although they will not be written back to disk, it still affects the page cache, resulting the later operation #28 to read out the polluted value. [FIX] Instead of early exit from btrfs_truncate_block() if the range is already block aligned, do extra filio zeroing if the fs block size is smaller than the page size. This is to address exactly the above case where memory mapped write can still leave some garbage beyond EOF. Signed-off-by: Qu Wenruo --- fs/btrfs/inode.c | 83 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 82 insertions(+), 1 deletion(-) diff --git a/fs/btrfs/inode.c b/fs/btrfs/inode.c index 0700a161b80e..2dc9b565f1f1 100644 --- a/fs/btrfs/inode.c +++ b/fs/btrfs/inode.c @@ -4777,6 +4777,87 @@ static int btrfs_rmdir(struct inode *dir, struct dentry *dentry) return ret; } +/* + * A helper to zero out all blocks inside range [@orig_start, @orig_end) of + * the target folio. + * The target folio is the one containing the head or tail block of the range + * [@from, @end]. + * + * This is a special case for fs block size < page size, where even if the range + * [from, end] is already block aligned, we can still have blocks beyond EOF being + * polluted by memory mapped write. + */ +static int zero_range_folio(struct btrfs_inode *inode, loff_t from, loff_t end, + u64 orig_start, u64 orig_end, + enum btrfs_truncate_where where) +{ + const u32 blocksize = inode->root->fs_info->sectorsize; + struct address_space *mapping = inode->vfs_inode.i_mapping; + struct extent_io_tree *io_tree = &inode->io_tree; + struct extent_state *cached_state = NULL; + struct btrfs_ordered_extent *ordered; + pgoff_t index = (where == BTRFS_TRUNCATE_HEAD_BLOCK) ? + (from >> PAGE_SHIFT) : (end >> PAGE_SHIFT); + struct folio *folio; + u64 block_start; + u64 block_end; + u64 clamp_start; + u64 clamp_end; + int ret = 0; + + /* + * The target head/tail block is already block aligned. + * If block size >= PAGE_SIZE, meaning it's impossible to mmap a + * page containing anything other than the target block. + */ + if (blocksize >= PAGE_SIZE) + return 0; +again: + folio = filemap_lock_folio(mapping, index); + /* No folio present. */ + if (IS_ERR(folio)) + return 0; + + if (!folio_test_uptodate(folio)) { + ret = btrfs_read_folio(NULL, folio); + folio_lock(folio); + if (folio->mapping != mapping) { + folio_unlock(folio); + folio_put(folio); + goto again; + } + if (!folio_test_uptodate(folio)) { + ret = -EIO; + goto out_unlock; + } + } + folio_wait_writeback(folio); + + clamp_start = max_t(u64, folio_pos(folio), orig_start); + clamp_end = min_t(u64, folio_pos(folio) + folio_size(folio) - 1, + orig_end); + block_start = round_down(clamp_start, block_size); + block_end = round_up(clamp_end + 1, block_size) - 1; + lock_extent(io_tree, block_start, block_end, &cached_state); + ordered = btrfs_lookup_ordered_range(inode, block_start, block_end + 1 - block_end); + if (ordered) { + unlock_extent(io_tree, block_start, block_end, &cached_state); + folio_unlock(folio); + folio_put(folio); + btrfs_start_ordered_extent(ordered); + btrfs_put_ordered_extent(ordered); + goto again; + } + folio_zero_range(folio, clamp_start - folio_pos(folio), + clamp_end - clamp_start + 1); + unlock_extent(io_tree, block_start, block_end, &cached_state); + +out_unlock: + folio_unlock(folio); + folio_put(folio); + return ret; +} + /* * Read, zero a chunk and write a block. * @@ -4819,7 +4900,7 @@ int btrfs_truncate_block(struct btrfs_inode *inode, loff_t from, loff_t end, ASSERT(where == BTRFS_TRUNCATE_HEAD_BLOCK); if (IS_ALIGNED(from, blocksize) && IS_ALIGNED(end + 1, blocksize)) - goto out; + return zero_range_folio(inode, from, end, orig_start, orig_end, where); if (where == BTRFS_TRUNCATE_HEAD_BLOCK) block_start = round_down(from, blocksize);