Re: [PATCH] nilfs2: fix slab-out-of-bounds in nilfs_direct_propagate after truncation

Viacheslav Dubeyko <[email protected]> Tue, 28 Jul 2026 16:37:05 -0700
Newsgroups org.kernel.vger.linux-nilfs,org.kernel.vger.linux-kernel
Message-ID <[email protected]>
On Fri, 2026-07-17 at 13:39 +0900, Ryusuke Konishi wrote:
> Shuangpeng Bai reported that KASAN detected a slab-out-of-bounds
> error
> in nilfs_direct_propagate() during testing.
>=20
> Analysis revealed that after truncating a file, a node block
> immediately
> below the B-tree root was not deleted.=C2=A0 Instead, it remained in the
> B-tree
> node cache in a dirty state.=C2=A0 The log writer subsequently detected
> this
> block and incorrectly invoked nilfs_direct_propagate() on it, which
> is
> designed to handle only data blocks in direct mapping.
>=20
> B-tree nodes in the cache are managed by virtual block numbers, and
> their
> logical keys typically exceed the range expected by direct mapping.
> Consequently, processing such a node as a direct mapping entry
> triggers
> a slab-out-of-bounds access.
>=20
> The root cause is that when a B-tree mapping collapses into a direct
> mapping during truncation, an intermediate node block pointed to by
> the
> root node is left behind as garbage instead of being explicitly
> deleted.
>=20
> This resolves the issue by adding a nilfs_btree_discard() operation
> to delete the remaining intermediate node block during the
> conversion.
> A 'deform' flag is added to the bop_delete interface to explicitly
> signal
> that the deletion is part of a mapping transformation.=C2=A0 This allows
> the
> B-tree mapping implementation to perform the necessary cleanup and
> discarding of the residual node structure that would be otherwise be
> left
> orphaned after the transition.
>=20
> Reported-by: Shuangpeng Bai <[email protected]>
> Closes:
> https://lore.kernel.org/r/[email protected]
> Fixes: 36a580eb489f ("nilfs2: direct block mapping")
> Cc: [email protected]
> Signed-off-by: Ryusuke Konishi <[email protected]>
> ---
> Hi Viacheslav,
>=20
> Please apply this for the next cycle.
>=20
> This fixes a flaw in the original B-tree implementation related to
> truncation
> and resolves the reported out-of-bounds memory access issue.
>=20
> Thanks,
> Ryusuke Konishi
>=20
> =C2=A0fs/nilfs2/bmap.c=C2=A0=C2=A0 |=C2=A0 2 +-
> =C2=A0fs/nilfs2/bmap.h=C2=A0=C2=A0 |=C2=A0 2 +-
> =C2=A0fs/nilfs2/btree.c=C2=A0 | 39 ++++++++++++++++++++++++++++++++------=
-
> =C2=A0fs/nilfs2/direct.c |=C2=A0 4 ++--
> =C2=A04 files changed, 36 insertions(+), 11 deletions(-)
>=20
> diff --git a/fs/nilfs2/bmap.c b/fs/nilfs2/bmap.c
> index 5f0f1f283af0..83f6ea30cc8b 100644
> --- a/fs/nilfs2/bmap.c
> +++ b/fs/nilfs2/bmap.c
> @@ -175,7 +175,7 @@ static int nilfs_bmap_do_delete(struct nilfs_bmap
> *bmap, __u64 key)
> =C2=A0			return ret;
> =C2=A0	}
> =C2=A0
> -	return bmap->b_ops->bop_delete(bmap, key);
> +	return bmap->b_ops->bop_delete(bmap, key, false);
> =C2=A0}
> =C2=A0
> =C2=A0/**
> diff --git a/fs/nilfs2/bmap.h b/fs/nilfs2/bmap.h
> index 4656df392722..a72f3c308a5d 100644
> --- a/fs/nilfs2/bmap.h
> +++ b/fs/nilfs2/bmap.h
> @@ -63,7 +63,7 @@ struct nilfs_bmap_operations {
> =C2=A0	int (*bop_lookup_contig)(const struct nilfs_bmap *, __u64,
> __u64 *,
> =C2=A0				 unsigned int);
> =C2=A0	int (*bop_insert)(struct nilfs_bmap *, __u64, __u64);
> -	int (*bop_delete)(struct nilfs_bmap *, __u64);
> +	int (*bop_delete)(struct nilfs_bmap *bmap, __u64 key, bool
> deform);
> =C2=A0	void (*bop_clear)(struct nilfs_bmap *);
> =C2=A0
> =C2=A0	int (*bop_propagate)(struct nilfs_bmap *, struct buffer_head
> *);
> diff --git a/fs/nilfs2/btree.c b/fs/nilfs2/btree.c
> index 64d5f7c5ab44..64bac66af25b 100644
> --- a/fs/nilfs2/btree.c
> +++ b/fs/nilfs2/btree.c
> @@ -1425,6 +1425,28 @@ static void nilfs_btree_shrink(struct
> nilfs_bmap *btree,
> =C2=A0	path[level].bp_bh =3D NULL;
> =C2=A0}
> =C2=A0
> +/**
> + * nilfs_btree_discard - discard the last node for the mapping
> transformation
> + * @btree: bmap struct of btree
> + * @path: array of nilfs_btree_path struct
> + * @level: level of the B-tree node being operated on
> + * @keyp: argument for passing a key (unused)
> + * @ptrp: argument for passing a pointer (unused)
> + */
> +static void nilfs_btree_discard(struct nilfs_bmap *btree,
> +				struct nilfs_btree_path *path, int
> level,
> +				__u64 *keyp, __u64 *ptrp)
> +{
> +	struct nilfs_btree_node *root =3D nilfs_btree_get_root(btree);
> +
> +	nilfs_btree_node_delete(root, 0, NULL, NULL,
> +				NILFS_BTREE_ROOT_NCHILDREN_MAX);
> +	nilfs_btree_node_set_level(root, level);
> +
> +	nilfs_btnode_delete(path[level].bp_bh);
> +	path[level].bp_bh =3D NULL;
> +}
> +
> =C2=A0static void nilfs_btree_nop(struct nilfs_bmap *btree,
> =C2=A0			=C2=A0=C2=A0=C2=A0 struct nilfs_btree_path *path,
> =C2=A0			=C2=A0=C2=A0=C2=A0 int level, __u64 *keyp, __u64 *ptrp)
> @@ -1435,7 +1457,7 @@ static int nilfs_btree_prepare_delete(struct
> nilfs_bmap *btree,
> =C2=A0				=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 struct nilfs_btree_path *path,
> =C2=A0				=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 int *levelp,
> =C2=A0				=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 struct nilfs_bmap_stats
> *stats,
> -				=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 struct inode *dat)
> +				=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 struct inode *dat, bool
> deform)
> =C2=A0{
> =C2=A0	struct buffer_head *bh;
> =C2=A0	struct nilfs_btree_node *node, *parent, *sib;
> @@ -1522,15 +1544,17 @@ static int nilfs_btree_prepare_delete(struct
> nilfs_bmap *btree,
> =C2=A0			if (nilfs_btree_node_get_nchildren(node) - 1
> <=3D
> =C2=A0			=C2=A0=C2=A0=C2=A0 NILFS_BTREE_ROOT_NCHILDREN_MAX) {
> =C2=A0				path[level].bp_op =3D
> nilfs_btree_shrink;
> -				stats->bs_nblocks +=3D 2;
> -				level++;
> -				path[level].bp_op =3D nilfs_btree_nop;
> -				goto shrink_root_child;
> +			} else if (deform) {
> +				path[level].bp_op =3D
> nilfs_btree_discard;
> =C2=A0			} else {
> =C2=A0				path[level].bp_op =3D
> nilfs_btree_do_delete;
> =C2=A0				stats->bs_nblocks++;
> =C2=A0				goto out;
> =C2=A0			}
> +			stats->bs_nblocks +=3D 2;
> +			level++;
> +			path[level].bp_op =3D nilfs_btree_nop;
> +			goto shrink_root_child;
> =C2=A0		}
> =C2=A0	}
> =C2=A0
> @@ -1581,7 +1605,7 @@ static void nilfs_btree_commit_delete(struct
> nilfs_bmap *btree,
> =C2=A0		nilfs_bmap_set_dirty(btree);
> =C2=A0}
> =C2=A0
> -static int nilfs_btree_delete(struct nilfs_bmap *btree, __u64 key)
> +static int nilfs_btree_delete(struct nilfs_bmap *btree, __u64 key,
> bool deform)
> =C2=A0
> =C2=A0{
> =C2=A0	struct nilfs_btree_path *path;
> @@ -1601,7 +1625,8 @@ static int nilfs_btree_delete(struct nilfs_bmap
> *btree, __u64 key)
> =C2=A0
> =C2=A0	dat =3D NILFS_BMAP_USE_VBN(btree) ? nilfs_bmap_get_dat(btree)
> : NULL;
> =C2=A0
> -	ret =3D nilfs_btree_prepare_delete(btree, path, &level,
> &stats, dat);
> +	ret =3D nilfs_btree_prepare_delete(btree, path, &level,
> &stats, dat,
> +			deform);
> =C2=A0	if (ret < 0)
> =C2=A0		goto out;
> =C2=A0	nilfs_btree_commit_delete(btree, path, level, dat);
> diff --git a/fs/nilfs2/direct.c b/fs/nilfs2/direct.c
> index 8bd0b1374e25..b8643d3aa2f8 100644
> --- a/fs/nilfs2/direct.c
> +++ b/fs/nilfs2/direct.c
> @@ -144,7 +144,7 @@ static int nilfs_direct_insert(struct nilfs_bmap
> *bmap, __u64 key, __u64 ptr)
> =C2=A0	return ret;
> =C2=A0}
> =C2=A0
> -static int nilfs_direct_delete(struct nilfs_bmap *bmap, __u64 key)
> +static int nilfs_direct_delete(struct nilfs_bmap *bmap, __u64 key,
> bool deform)
> =C2=A0{
> =C2=A0	union nilfs_bmap_ptr_req req;
> =C2=A0	struct inode *dat;
> @@ -234,7 +234,7 @@ int nilfs_direct_delete_and_convert(struct
> nilfs_bmap *bmap,
> =C2=A0	/* no need to allocate any resource for conversion */
> =C2=A0
> =C2=A0	/* delete */
> -	ret =3D bmap->b_ops->bop_delete(bmap, key);
> +	ret =3D bmap->b_ops->bop_delete(bmap, key, true);
> =C2=A0	if (ret < 0)
> =C2=A0		return ret;
> =C2=A0

Applied.

Thanks,
Slava.