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.