[PATCH 1/2] ubifs: tnc_delete: Don't free zero-branch znodes if memory allocation fails
Zhihao Cheng
chengzhihao1 at huawei.com
Tue Oct 13 23:02:51 PDT 2026
The freed zero-branch znodes won't be removed from TNC if a failure
(eg. mem alloc in insert_old_idx_znode) happens in tnc_delete, then
TNC traversing could access the freed znodes, the details shown as:
file: consists of many data nodes, TNC is:
znode_A (lv 2)
/ \
znode (lv 1) znode_B (lv 1)
/ \ |
znode(lv 0)...znode(lv 0) znode_C (lv 0, child_cnt = 1)
| | | | | |
ino_1 ino_f dent dn0...dn_i-1 dn_i
P1
truncate(file, dn_i-1)
ubifs_setattr
ubifs_jnl_truncate
ubifs_tnc_remove_range
tnc_delete // dn_i is removed, znode_C->child_cnt becomes 0
do-while
kfree(znode_C)
ENOMEM = insert_old_idx_znode(c, znode_B)
return
znode->zbranch[i] = znode->zbranch[i + 1] // skip,
// znode_C is still in TNC
the TNC looks like:
znode_A (lv 2)
/ \
znode (lv 1) znode_B (lv 1)
/ \ |
znode(lv 0)...znode(lv 0) znode_C (lv 0, child_cnt = 0, freed)
| | | | |
ino_1 ino_f dent dn0...dn_i-1
P2
drop_caches_sysctl_handler
shrink_slab
ubifs_shrink_scan
shrink_tnc_trees
shrink_tnc
ubifs_tnc_levelorder_next
access znode_C // UAF
The root cause is that zero-branch znodes freeing & znode->zbranch is not
atomic, which could be interrupted by the insert_old_idx_znode() failure,
fix it by freeing znodes after all insert_old_idx_znode() callers success.
The znodes collapsing won't meet the problem if insert_old_idx() fails,
because znode is freed after updating c->zroot, the TNC traversing won't
access a freed znode.
Notice: we add new field 'fnext' in struct ubifs_znode to trace to be
freed znodes. Because tnc_delete and do_commit could race, the members
parent/cnext/cparent can not be reused. Importing a stack array to record
znodes is also not recommended, becase the UBIFS_MAX_LEVELS*sizeof(ptr)
becomes 4096, which may cause the compiler to complain.
Reproducer is https://bugzilla.kernel.org/show_bug.cgi?id=222129.
Fixes: 1e51764a3c2a ("UBIFS: add new flash file system")
Signed-off-by: Zhihao Cheng <chengzhihao1 at huawei.com>
---
fs/ubifs/tnc.c | 40 +++++++++++++++++++++++++++++++---------
fs/ubifs/ubifs.h | 2 ++
2 files changed, 33 insertions(+), 9 deletions(-)
diff --git a/fs/ubifs/tnc.c b/fs/ubifs/tnc.c
index 776e6bc6a00d..679afddadf82 100644
--- a/fs/ubifs/tnc.c
+++ b/fs/ubifs/tnc.c
@@ -2514,8 +2514,9 @@ int ubifs_tnc_add_nm(struct ubifs_info *c, const union ubifs_key *key,
static int tnc_delete(struct ubifs_info *c, struct ubifs_znode *znode, int n)
{
struct ubifs_zbranch *zbr;
- struct ubifs_znode *zp;
+ struct ubifs_znode *zp, *zn;
int i, err;
+ struct ubifs_znode free_head = { .fnext = NULL };
/* Delete without merge for now */
ubifs_assert(c, znode->level == 0);
@@ -2543,7 +2544,7 @@ static int tnc_delete(struct ubifs_info *c, struct ubifs_znode *znode, int n)
* This was the last zbranch, we have to delete this znode from the
* parent.
*/
-
+ zn = &free_head;
do {
ubifs_assert(c, !ubifs_zn_obsolete(znode));
ubifs_assert(c, ubifs_zn_dirty(znode));
@@ -2554,15 +2555,20 @@ static int tnc_delete(struct ubifs_info *c, struct ubifs_znode *znode, int n)
atomic_long_dec(&c->dirty_zn_cnt);
err = insert_old_idx_znode(c, znode);
- if (err)
+ if (err) {
+ zn = free_head.fnext;
+ while (zn) {
+ struct ubifs_znode *next = zn->fnext;
+
+ zn->fnext = NULL;
+ zn = next;
+ }
return err;
+ }
+
+ zn->fnext = znode;
+ zn = znode;
- if (znode->cnext) {
- __set_bit(OBSOLETE_ZNODE, &znode->flags);
- atomic_long_inc(&c->clean_zn_cnt);
- atomic_long_inc(&ubifs_clean_zn_cnt);
- } else
- kfree(znode);
znode = zp;
} while (znode->child_cnt == 1); /* while removing last child */
@@ -2575,6 +2581,22 @@ static int tnc_delete(struct ubifs_info *c, struct ubifs_znode *znode, int n)
znode->zbranch[i].znode->iip = i;
}
+ /* Free zero-branchs znodes. */
+ zn = free_head.fnext;
+ while (zn) {
+ struct ubifs_znode *next = zn->fnext;
+
+ zn->fnext = NULL;
+ if (zn->cnext) {
+ __set_bit(OBSOLETE_ZNODE, &zn->flags);
+ atomic_long_inc(&c->clean_zn_cnt);
+ atomic_long_inc(&ubifs_clean_zn_cnt);
+ } else {
+ kfree(zn);
+ }
+ zn = next;
+ }
+
/*
* If this is the root and it has only 1 child then
* collapse the tree.
diff --git a/fs/ubifs/ubifs.h b/fs/ubifs/ubifs.h
index 00db0d19a85e..c3ae7c2625a4 100644
--- a/fs/ubifs/ubifs.h
+++ b/fs/ubifs/ubifs.h
@@ -761,6 +761,7 @@ struct ubifs_zbranch {
* @parent: parent znode or NULL if it is the root
* @cnext: next znode to commit
* @cparent: parent node for this commit
+ * @fnext: next znode to free
* @ciip: index in cparent's zbranch array
* @flags: znode flags (%DIRTY_ZNODE, %COW_ZNODE or %OBSOLETE_ZNODE)
* @time: last access time (seconds)
@@ -780,6 +781,7 @@ struct ubifs_znode {
struct ubifs_znode *parent;
struct ubifs_znode *cnext;
struct ubifs_znode *cparent;
+ struct ubifs_znode *fnext;
int ciip;
unsigned long flags;
time64_t time;
--
2.52.0
More information about the linux-mtd
mailing list