All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH 0/2] ubifs: tnc_delete: Fix two uaf bug when mem failure occurs
@ 2026-10-14  6:02 Zhihao Cheng
  2026-10-14  6:02 ` [PATCH 1/2] ubifs: tnc_delete: Don't free zero-branch znodes if memory allocation fails Zhihao Cheng
  2026-10-14  6:02 ` [PATCH 2/2] ubifs: tnc_delete: Don't update collapsed " Zhihao Cheng
  0 siblings, 2 replies; 5+ messages in thread
From: Zhihao Cheng @ 2026-10-14  6:02 UTC (permalink / raw)
  To: richard, ext-adrian.hunter
  Cc: linux-mtd, linux-kernel, chengzhihao1, yangerkun, yi.zhang

Fix two TNC traversing uaf problems after failure happens in
tnc_delete().

Zhihao Cheng (2):
  ubifs: tnc_delete: Don't free zero-branch znodes if memory allocation
    fails
  ubifs: tnc_delete: Don't update collapsed znodes if memory allocation
    fails

 fs/ubifs/tnc.c   | 44 +++++++++++++++++++++++++++++++++-----------
 fs/ubifs/ubifs.h |  2 ++
 2 files changed, 35 insertions(+), 11 deletions(-)

-- 
2.52.0


______________________________________________________
Linux MTD discussion mailing list
http://lists.infradead.org/mailman/listinfo/linux-mtd/

^ permalink raw reply	[flat|nested] 5+ messages in thread

* [PATCH 1/2] ubifs: tnc_delete: Don't free zero-branch znodes if memory allocation fails
  2026-10-14  6:02 [PATCH 0/2] ubifs: tnc_delete: Fix two uaf bug when mem failure occurs Zhihao Cheng
@ 2026-10-14  6:02 ` Zhihao Cheng
  2026-10-14  6:28   ` sashiko-bot
  2026-10-14  6:02 ` [PATCH 2/2] ubifs: tnc_delete: Don't update collapsed " Zhihao Cheng
  1 sibling, 1 reply; 5+ messages in thread
From: Zhihao Cheng @ 2026-10-14  6:02 UTC (permalink / raw)
  To: richard, ext-adrian.hunter
  Cc: linux-mtd, linux-kernel, chengzhihao1, yangerkun, yi.zhang

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@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


______________________________________________________
Linux MTD discussion mailing list
http://lists.infradead.org/mailman/listinfo/linux-mtd/

^ permalink raw reply related	[flat|nested] 5+ messages in thread

* [PATCH 2/2] ubifs: tnc_delete: Don't update collapsed znodes if memory allocation fails
  2026-10-14  6:02 [PATCH 0/2] ubifs: tnc_delete: Fix two uaf bug when mem failure occurs Zhihao Cheng
  2026-10-14  6:02 ` [PATCH 1/2] ubifs: tnc_delete: Don't free zero-branch znodes if memory allocation fails Zhihao Cheng
@ 2026-10-14  6:02 ` Zhihao Cheng
  1 sibling, 0 replies; 5+ messages in thread
From: Zhihao Cheng @ 2026-10-14  6:02 UTC (permalink / raw)
  To: richard, ext-adrian.hunter
  Cc: linux-mtd, linux-kernel, chengzhihao1, yangerkun, yi.zhang

A collapsed znode is updated with 'znode->parent = NULL' before calling
insert_old_idx(), if a memory allocation failure happens, there will
exists non-root znode(without parent) in TNC, which leads a null-ptr-def
problem for TNC traversing, the details shown as:

 file: consists of many data nodes, TNC is:
                   znode_A (lv 2)
  	     /                         \
       znode_B (lvl 1)              znode (lv 1)
        /             \                 |
    znode_C(lv 0)...znode(lv 0)     znode (lv 0)
     |   |     |     |       |          |
  ino_1 ino_f dent dn_0 ... dn_i      dn_i+1

     P1
  truncate(file, 0)
   ubifs_setattr
    ubifs_jnl_truncate
     ubifs_tnc_remove_range
      tnc_delete // dn_* are removed
       do-while

  the TNC looks like:
          znode_A (lv 2, parent = NULL)
		|
	  znode_B (lvl 1, parent = znode_A)
		|
	  znode_C (lv 0, parent = znode_B)
	  |     |     |
	ino_1 ino_f dent

  tnc_delete will collapse the tree:
  // while (znode->child_cnt == 1 && znode->level != 0)
   // round 1
   zp = znode_A
   znode_B = get_znode(c, znode_A, 0)
   znode_B->parent = NULL
   znode_B->iip = 0
   insert_old_idx
   c->zroot = znode_B
   kfree(znode_A)
   // round 2
   zp = znode_B
   znode_C = get_znode(c, znode_B, 0)
   znode_C->parent = NULL
   znode_C->iip = 0
   ENOMEM = insert_old_idx
    return
   c->zroot = znode_C // skip

  the TNC looks like:
          znode_B (lv 1, parent = NULL)
		|
	  znode_C (lv 0, parent = NULL)
	  |     |     |
	ino_1 ino_f dent

      P2
  umount
   kill_ubifs_super
    ubifs_put_super
     ubifs_umount
      destroy_journal
       ubifs_tnc_close
        ubifs_destroy_tnc_tree
	 ubifs_destroy_tnc_subtree
	  znode_C = ubifs_tnc_postorder_first
	  if (zn == znode) // false, zn = znode_C, znode = znode_B
	  zn = ubifs_tnc_postorder_next(c, znode_C) // zn = NULL
	  for (n = 0; n < zn->child_cnt; n++) // null-ptr-def !

The root cause is that zroot & znode->parent updating is not atomic,
which could be interrupted by the insert_old_idx() failure, fix it by
making zroot & znode->parent updating together.

Reproducer is https://bugzilla.kernel.org/show_bug.cgi?id=222130.

Fixes: 1e51764a3c2a ("UBIFS: add new flash file system")
Signed-off-by: Zhihao Cheng <chengzhihao1@huawei.com>
---
 fs/ubifs/tnc.c | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/fs/ubifs/tnc.c b/fs/ubifs/tnc.c
index 679afddadf82..c9480d963e4c 100644
--- a/fs/ubifs/tnc.c
+++ b/fs/ubifs/tnc.c
@@ -2611,14 +2611,14 @@ static int tnc_delete(struct ubifs_info *c, struct ubifs_znode *znode, int n)
 			znode = dirty_cow_znode(c, zbr);
 			if (IS_ERR(znode))
 				return PTR_ERR(znode);
-			znode->parent = NULL;
-			znode->iip = 0;
 			if (c->zroot.len) {
 				err = insert_old_idx(c, c->zroot.lnum,
 						     c->zroot.offs);
 				if (err)
 					return err;
 			}
+			znode->parent = NULL;
+			znode->iip = 0;
 			c->zroot.lnum = zbr->lnum;
 			c->zroot.offs = zbr->offs;
 			c->zroot.len = zbr->len;
-- 
2.52.0


______________________________________________________
Linux MTD discussion mailing list
http://lists.infradead.org/mailman/listinfo/linux-mtd/

^ permalink raw reply related	[flat|nested] 5+ messages in thread

* Re: [PATCH 1/2] ubifs: tnc_delete: Don't free zero-branch znodes if memory allocation fails
  2026-10-14  6:02 ` [PATCH 1/2] ubifs: tnc_delete: Don't free zero-branch znodes if memory allocation fails Zhihao Cheng
@ 2026-10-14  6:28   ` sashiko-bot
  2026-10-14  6:48     ` Zhihao Cheng
  0 siblings, 1 reply; 5+ messages in thread
From: sashiko-bot @ 2026-10-14  6:28 UTC (permalink / raw)
  To: Zhihao Cheng, richard, ext-adrian.hunter; +Cc: linux-mtd, yangerkun, yi.zhang

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] ubifs: assertion failure on lookups due to zero-child znode after tnc_delete() error
--

commit 5c1854483c2d06b6fd3fc46791b422fcd958956f
Author: Zhihao Cheng <chengzhihao1@huawei.com>

ubifs: tnc_delete: Don't free zero-branch znodes if memory allocation fails

This commit delays the freeing of zero-branch znodes in tnc_delete()
until after all insert_old_idx_znode() calls succeed. It introduces a
local stack list to track znodes to be freed, aiming to prevent a
use-after-free race condition when traversing the TNC tree.

> diff --git a/fs/ubifs/tnc.c b/fs/ubifs/tnc.c
> --- a/fs/ubifs/tnc.c
> +++ b/fs/ubifs/tnc.c
[ ... ]
> @@ -2542,15 +2543,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;

[Severity: High]
If insert_old_idx_znode() returns an error like -ENOMEM, does this early 
return leave the zero-child znode linked in the TNC tree?

Because we return before removing the znode from its parent's zbranch array, 
the tree retains a node with child_cnt == 0. If a subsequent read operation 
(which is permitted even after the filesystem is forced to read-only mode) 
traverses this znode, it will call ubifs_search_zbranch():

fs/ubifs/tnc_misc.c:ubifs_search_zbranch() {
        int beg = 0, end = znode->child_cnt, mid;
        int cmp;
        const struct ubifs_zbranch *zbr = &znode->zbranch[0];

        ubifs_assert(c, end > beg);
...
}

With end == 0 and beg == 0, this will trigger the assertion failure, which 
can panic the kernel if mounted with assert=panic. Does this error path need 
to remove the znode from its parent's zbranch, or otherwise mark it to 
prevent lookups from tripping the assertion?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261014060252.3531356-1-chengzhihao1@huawei.com?part=1

______________________________________________________
Linux MTD discussion mailing list
http://lists.infradead.org/mailman/listinfo/linux-mtd/

^ permalink raw reply	[flat|nested] 5+ messages in thread

* Re: [PATCH 1/2] ubifs: tnc_delete: Don't free zero-branch znodes if memory allocation fails
  2026-10-14  6:28   ` sashiko-bot
@ 2026-10-14  6:48     ` Zhihao Cheng
  0 siblings, 0 replies; 5+ messages in thread
From: Zhihao Cheng @ 2026-10-14  6:48 UTC (permalink / raw)
  To: sashiko-reviews, richard, ext-adrian.hunter
  Cc: linux-mtd, yangerkun, yi.zhang

在 2026/10/14 14:28, sashiko-bot@kernel.org 写道:
> Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
> - [High] ubifs: assertion failure on lookups due to zero-child znode after tnc_delete() error
> --
> 
> commit 5c1854483c2d06b6fd3fc46791b422fcd958956f
> Author: Zhihao Cheng <chengzhihao1@huawei.com>
> 
> ubifs: tnc_delete: Don't free zero-branch znodes if memory allocation fails
> 
> This commit delays the freeing of zero-branch znodes in tnc_delete()
> until after all insert_old_idx_znode() calls succeed. It introduces a
> local stack list to track znodes to be freed, aiming to prevent a
> use-after-free race condition when traversing the TNC tree.
> 
>> diff --git a/fs/ubifs/tnc.c b/fs/ubifs/tnc.c
>> --- a/fs/ubifs/tnc.c
>> +++ b/fs/ubifs/tnc.c
> [ ... ]
>> @@ -2542,15 +2543,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;
> 
> [Severity: High]
> If insert_old_idx_znode() returns an error like -ENOMEM, does this early
> return leave the zero-child znode linked in the TNC tree?
> 
> Because we return before removing the znode from its parent's zbranch array,
> the tree retains a node with child_cnt == 0. If a subsequent read operation
> (which is permitted even after the filesystem is forced to read-only mode)
> traverses this znode, it will call ubifs_search_zbranch():
> 
> fs/ubifs/tnc_misc.c:ubifs_search_zbranch() {
>          int beg = 0, end = znode->child_cnt, mid;
>          int cmp;
>          const struct ubifs_zbranch *zbr = &znode->zbranch[0];
> 
>          ubifs_assert(c, end > beg);
> ...
> }
> 
> With end == 0 and beg == 0, this will trigger the assertion failure, which
> can panic the kernel if mounted with assert=panic. Does this error path need
> to remove the znode from its parent's zbranch, or otherwise mark it to
> prevent lookups from tripping the assertion?
> 

Hi, the insert_old_idx_znode() makes tnc_delete return immediately even 
level0_znode->child_cnt becomes zero, which could trigger following 
assertion failure in ubifs_search_zbranch(), this is an already existed 
process.
This issue is not severe because after the insert_old_idx_znode error 
occurs, UBIFS has already switched to read-only mode and issued an 
alarm. Subsequent ubifs_assert is merely printing more alarms, unless 
the mount option uses assert=panic.

______________________________________________________
Linux MTD discussion mailing list
http://lists.infradead.org/mailman/listinfo/linux-mtd/

^ permalink raw reply	[flat|nested] 5+ messages in thread

end of thread, other threads:[~2026-10-14  6:49 UTC | newest]

Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-14  6:02 [PATCH 0/2] ubifs: tnc_delete: Fix two uaf bug when mem failure occurs Zhihao Cheng
2026-10-14  6:02 ` [PATCH 1/2] ubifs: tnc_delete: Don't free zero-branch znodes if memory allocation fails Zhihao Cheng
2026-10-14  6:28   ` sashiko-bot
2026-10-14  6:48     ` Zhihao Cheng
2026-10-14  6:02 ` [PATCH 2/2] ubifs: tnc_delete: Don't update collapsed " Zhihao Cheng

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.