mirror of
https://github.com/torvalds/linux.git
synced 2026-11-06 21:58:01 +01:00
ext4: don't report delalloc data as a hole on indirect-mapped inodes
When a plain lookup finds no block, ext4_ind_map_blocks() sizes the hole
it reports by counting the empty subtrees under 'partial' in the on-disk
indirect tree. That count knows nothing about delayed allocation, so a
range holding delalloc data that has not been written back yet is
reported as a plain hole. The extent-mapped path does not have this
problem: it trims the hole it found at the first delayed extent before
returning it.
ext4_map_blocks() consults the extent status tree before it calls into
the mapping layer, so a query that starts exactly on the delayed block
still finds it. A query that starts earlier does not, because the hole
reported for the earlier block already spans the delayed one. On a
4k-block filesystem, lseek(SEEK_DATA) from offset 0 on a file whose only
data block is at logical block N and is still dirty returns:
N = 0..11 direct blocks N << 12
N = 12 first single-indirect block N << 12
N = 13..1035 inside that indirect block -1 ENXIO
N = 1036 first double-indirect block N << 12
Blocks 12 and 1036 survive because the query lands on the delayed block
itself. Blocks 13..1035 are swallowed by the 1024-block hole reported
for block 12. fiemap loses the same data for the same reason.
An ext3 filesystem mounted as ext4 hits this during ordinary use: ext3
inodes stay indirect mapped, while mount -t ext4 turns on delayed
allocation even though mounting ext3 with -o delalloc is explicitly
rejected. install(1) from coreutils uses SEEK_DATA to locate data in
its source file, so installing a sparse file that has not been written
back silently produces a destination of the right size holding none of
the data:
truncate -s 1G img && mkfs.ext3 -F img && mount -t ext4 img mnt
echo | dd of=mnt/src bs=1 count=1 seek=64K
install mnt/src mnt/dst
cmp mnt/src mnt/dst # differ: char 65537
Reconciling a hole with the extent status tree does not depend on how
the hole was found, so instead of open coding it a second time, split
the delalloc handling out of ext4_ext_determine_insert_hole() into
ext4_determine_insert_hole(), which takes the hole the caller located,
and call that from ext4_ind_map_blocks() as well. All the special cases
and the comments explaining them stay in one place.
Two things change for indirect-mapped inodes. They now cache the holes
they report, as the extent-mapped path already does: ext4_punch_hole()
already puts EXTENT_STATUS_HOLE on such inodes, ext4_map_blocks() and
ext4_da_map_blocks() already handle a cached hole, the latter by
replacing it with the delayed extent, and consecutive hole entries
merge, so the tree does not grow with the length of a scan. And when a
delayed extent covers the queried block itself - a race, since
ext4_map_blocks() found nothing in the tree just before - the hole is
now reported only up to the end of that extent instead of in full.
The indirect path passes hole_start == lblk, so the "delalloc extent in
front of the queried range" case cannot be reached there:
__es_tree_search() never returns an extent ending before the block it
was searched for.
With this the sweep above returns N << 12 for every N, while the
nodelalloc and extent-mapped controls are unchanged. generic/225,
generic/285, generic/448 and generic/490 pass on ext4 made both with and
without the extent feature. generic/286 and generic/436 pass on
extent-mapped ext4 and are skipped without it, since they need
fallocate.
Fixes: facab4d971 ("ext4: return hole from ext4_map_blocks()")
Reported-by: Alexander Monakov <amonakov@ispras.ru>
Closes: https://lore.kernel.org/linux-ext4/594c17d9-c00f-e485-96fb-cedf27ce3aa3@ispras.ru/
Suggested-by: Jan Kara <jack@suse.cz>
Cc: stable@vger.kernel.org
Signed-off-by: Daejun Park <daejun7.park@samsung.com>
Reviewed-by: Jan Kara <jack@suse.cz>
Reviewed-by: Zhang Yi <yi.zhang@huawei.com>
Link: https://patch.msgid.link/20260827084829epcms2p6ecc0c39a71d0c3eebf9cb1ade77314f6@epcms2p6
Signed-off-by: Theodore Ts'o <tytso@mit.edu>
This commit is contained in:
parent
418adcde99
commit
aed43f47e1
|
|
@ -3905,6 +3905,9 @@ extern void ext4_ext_tree_init(handle_t *handle, struct inode *inode);
|
|||
extern int ext4_ext_index_trans_blocks(struct inode *inode, int extents);
|
||||
extern int ext4_ext_map_blocks(handle_t *handle, struct inode *inode,
|
||||
struct ext4_map_blocks *map, int flags);
|
||||
ext4_lblk_t ext4_determine_insert_hole(struct inode *inode, ext4_lblk_t lblk,
|
||||
ext4_lblk_t hole_start,
|
||||
ext4_lblk_t hole_len);
|
||||
extern int ext4_ext_truncate(handle_t *, struct inode *);
|
||||
extern int ext4_ext_remove_space(struct inode *inode, ext4_lblk_t start,
|
||||
ext4_lblk_t end);
|
||||
|
|
|
|||
|
|
@ -4190,21 +4190,19 @@ static int get_implied_cluster_alloc(struct super_block *sb,
|
|||
}
|
||||
|
||||
/*
|
||||
* Determine hole length around the given logical block, first try to
|
||||
* locate and expand the hole from the given @path, and then adjust it
|
||||
* if it's partially or completely converted to delayed extents, insert
|
||||
* it into the extent cache tree if it's indeed a hole, finally return
|
||||
* the length of the determined extent.
|
||||
* Adjust the hole [@hole_start, @hole_start + @hole_len) the caller found
|
||||
* around the queried block @lblk if it's partially or completely converted
|
||||
* to delayed extents, insert it into the extent cache tree if it's indeed a
|
||||
* hole, finally return the length of the determined extent starting at
|
||||
* @lblk. @hole_start must not be behind @lblk.
|
||||
*/
|
||||
static ext4_lblk_t ext4_ext_determine_insert_hole(struct inode *inode,
|
||||
struct ext4_ext_path *path,
|
||||
ext4_lblk_t lblk)
|
||||
ext4_lblk_t ext4_determine_insert_hole(struct inode *inode, ext4_lblk_t lblk,
|
||||
ext4_lblk_t hole_start,
|
||||
ext4_lblk_t hole_len)
|
||||
{
|
||||
ext4_lblk_t hole_start, len;
|
||||
ext4_lblk_t len = hole_len;
|
||||
struct extent_status es;
|
||||
|
||||
hole_start = lblk;
|
||||
len = ext4_ext_find_hole(inode, path, &hole_start);
|
||||
again:
|
||||
ext4_es_find_extent_range(inode, &ext4_es_is_delayed, hole_start,
|
||||
hole_start + len - 1, &es);
|
||||
|
|
@ -4371,9 +4369,11 @@ int ext4_ext_map_blocks(handle_t *handle, struct inode *inode,
|
|||
* we couldn't try to create block if flags doesn't contain EXT4_GET_BLOCKS_CREATE
|
||||
*/
|
||||
if ((flags & EXT4_GET_BLOCKS_CREATE) == 0) {
|
||||
ext4_lblk_t len;
|
||||
ext4_lblk_t hole_start = map->m_lblk, len;
|
||||
|
||||
len = ext4_ext_determine_insert_hole(inode, path, map->m_lblk);
|
||||
len = ext4_ext_find_hole(inode, path, &hole_start);
|
||||
len = ext4_determine_insert_hole(inode, map->m_lblk,
|
||||
hole_start, len);
|
||||
|
||||
map->m_pblk = 0;
|
||||
map->m_len = min_t(unsigned int, map->m_len, len);
|
||||
|
|
|
|||
|
|
@ -586,6 +586,23 @@ int ext4_ind_map_blocks(handle_t *handle, struct inode *inode,
|
|||
for (i = partial - chain + 1; i < depth; i++)
|
||||
count = count * epb + (epb - offsets[i] - 1);
|
||||
count++;
|
||||
|
||||
/*
|
||||
* The count knows nothing about delayed allocation, so let
|
||||
* the common helper reconcile it with the extent status
|
||||
* tree. Only when the lookup really did find a hole:
|
||||
* ext4_get_branch() also returns a non-NULL partial when it
|
||||
* failed, and caching a hole for a range it could not read
|
||||
* would answer every later read from that cache instead of
|
||||
* retrying. Clamp the count first, a subtree can be bigger
|
||||
* than the logical block space when the block size is large.
|
||||
*/
|
||||
if (!err) {
|
||||
count = umin(count, EXT_MAX_BLOCKS - map->m_lblk);
|
||||
count = ext4_determine_insert_hole(inode, map->m_lblk,
|
||||
map->m_lblk, count);
|
||||
}
|
||||
|
||||
/* Fill in size of a hole we found */
|
||||
map->m_pblk = 0;
|
||||
map->m_len = umin(map->m_len, count);
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user