Skip to contentSkip to conversation

ext4: convert pa_count from atomic_t to refcount_t

PATCH In review rafad900 4 participants 5 messages Jul 20 – Jul 28 ext4 file system

lore
Share & tools
lore

rafad900

author
#0 lore raw

PATCH 1/1 ext4: convert pa_count from atomic_t to refcount_t

The pa_count field in struct ext4_prealloc_space is reference counter for preallocation descriptors. Converting it from atomic_t to refcount_t provides underflow and overflow protection.

This was found using Coccinelle script
atomic_as_refcounter.cocci

Testing was done on a QEMU x86 VM revealing a pre-existing race condition leading to a use-after-free.

---
 fs/ext4/mballoc.c | 22 +++++++++++-----------
 fs/ext4/mballoc.h |  2 +-
 2 files changed, 12 insertions(+), 12 deletions(-)
fs/ext4/mballoc.c +11 -11
index ed1bd00e11cd..59b3d4abaab0 100644
@@ -4823,7 +4823,7 @@ ext4_mb_check_group_pa(ext4_fsblk_t goal_block,
 	ext4_fsblk_t cur_distance, new_distance;
 
 	if (cpa == NULL) {
-		atomic_inc(&pa->pa_count);
+		refcount_inc(&pa->pa_count);
 		return pa;
 	}
 	cur_distance = abs(goal_block - cpa->pa_pstart);
@@ -4833,8 +4833,8 @@ ext4_mb_check_group_pa(ext4_fsblk_t goal_block,
 		return cpa;
 
 	/* drop the previous reference */
-	atomic_dec(&cpa->pa_count);
-	atomic_inc(&pa->pa_count);
+	refcount_dec(&cpa->pa_count);
+	refcount_inc(&pa->pa_count);
 	return pa;
 }
 
@@ -4993,7 +4993,7 @@ ext4_mb_use_preallocated(struct ext4_allocation_context *ac)
 	}
 
 	if (tmp_pa->pa_free && likely(ext4_mb_pa_goal_check(ac, tmp_pa))) {
-		atomic_inc(&tmp_pa->pa_count);
+		refcount_inc(&tmp_pa->pa_count);
 		ext4_mb_use_inode_pa(ac, tmp_pa);
 		spin_unlock(&tmp_pa->pa_lock);
 		read_unlock(&ei->i_prealloc_lock);
@@ -5139,7 +5139,7 @@ static void ext4_mb_mark_pa_deleted(struct super_block *sb,
 static inline void ext4_mb_pa_free(struct ext4_prealloc_space *pa)
 {
 	BUG_ON(!pa);
-	BUG_ON(atomic_read(&pa->pa_count));
+	BUG_ON(refcount_read(&pa->pa_count));
 	BUG_ON(pa->pa_deleted == 0);
 	kmem_cache_free(ext4_pspace_cachep, pa);
 }
@@ -5165,7 +5165,7 @@ static void ext4_mb_put_pa(struct ext4_allocation_context *ac,
 
 	/* in this short window concurrent discard can set pa_deleted */
 	spin_lock(&pa->pa_lock);
-	if (!atomic_dec_and_test(&pa->pa_count) || pa->pa_free != 0) {
+	if (!refcount_dec_and_test(&pa->pa_count) || pa->pa_free != 0) {
 		spin_unlock(&pa->pa_lock);
 		return;
 	}
@@ -5535,7 +5535,7 @@ ext4_mb_discard_group_preallocations(struct super_block *sb,
 	list_for_each_entry_safe(pa, tmp,
 				&grp->bb_prealloc_list, pa_group_list) {
 		spin_lock(&pa->pa_lock);
-		if (atomic_read(&pa->pa_count)) {
+		if (refcount_read(&pa->pa_count)) {
 			spin_unlock(&pa->pa_lock);
 			*busy = 1;
 			continue;
@@ -5637,7 +5637,7 @@ void ext4_discard_preallocations(struct inode *inode)
 		BUG_ON(pa->pa_node_lock.inode_lock != &ei->i_prealloc_lock);
 
 		spin_lock(&pa->pa_lock);
-		if (atomic_read(&pa->pa_count)) {
+		if (refcount_read(&pa->pa_count)) {
 			/* this shouldn't happen often - nobody should
 			 * use preallocation while we're discarding it */
 			spin_unlock(&pa->pa_lock);
@@ -5720,7 +5720,7 @@ static int ext4_mb_pa_alloc(struct ext4_allocation_context *ac)
 	pa = kmem_cache_zalloc(ext4_pspace_cachep, GFP_NOFS);
 	if (!pa)
 		return -ENOMEM;
-	atomic_set(&pa->pa_count, 1);
+	refcount_set(&pa->pa_count, 1);
 	ac->ac_pa = pa;
 	return 0;
 }
@@ -5731,7 +5731,7 @@ static void ext4_mb_pa_put_free(struct ext4_allocation_context *ac)
 
 	BUG_ON(!pa);
 	ac->ac_pa = NULL;
-	WARN_ON(!atomic_dec_and_test(&pa->pa_count));
+	WARN_ON(!refcount_dec_and_test(&pa->pa_count));
 	/*
 	 * current function is only called due to an error or due to
 	 * len of found blocks < len of requested blocks hence the PA has not
@@ -5948,7 +5948,7 @@ ext4_mb_discard_lg_preallocations(struct super_block *sb,
 				pa_node.lg_list,
 				lockdep_is_held(&lg->lg_prealloc_lock)) {
 		spin_lock(&pa->pa_lock);
-		if (atomic_read(&pa->pa_count)) {
+		if (refcount_read(&pa->pa_count)) {
 			/*
 			 * This is the pa that we just used
 			 * for block allocation. So don't
fs/ext4/mballoc.h +1 -1
index 39333ce72cbd..f708d303228e 100644
@@ -126,7 +126,7 @@ struct ext4_prealloc_space {
 		struct rcu_head	pa_rcu;
 	} u;
 	spinlock_t		pa_lock;
-	atomic_t		pa_count;
+	refcount_t		pa_count;
 	unsigned		pa_deleted;
 	ext4_fsblk_t		pa_pstart;	/* phys. block */
 	ext4_lblk_t		pa_lstart;	/* log. block */
Signature · 1 line
2.43.0
syzbot Tested-by [email protected] on v1 1/1
#1 lore raw
syzbot ci has tested the following series

syzbot ci has tested the following series

[v1] ext4: convert pa_count from atomic_t to refcount_t
https://lore.kernel.org/all/[email protected]
* [PATCH] ext4: convert pa_count from atomic_t to refcount_t

and found the following issue:
WARNING: refcount bug in ext4_mb_use_preallocated

Full report is available here:
https://ci.syzbot.org/series/34fda797-e979-409c-bc2d-7af3e163df14

***

WARNING: refcount bug in ext4_mb_use_preallocated

tree:      torvalds
URL:       https://kernel.googlesource.com/pub/scm/linux/kernel/git/torvalds/linux
base:      1590cf0329716306e948a8fc29f1d3ee87d3989f
arch:      amd64
compiler:  Debian clang version 22.1.6 (++20260514074242+fc4aad7b5db3-1~exp1~20260514074407.73), Debian LLD 22.1.6
config:    https://ci.syzbot.org/builds/05fa6c1d-a594-4691-ace3-ded3fc2ca4dc/config
------------[ cut here ]------------
refcount_t: addition on 0; use-after-free.
WARNING: lib/refcount.c:25 at refcount_warn_saturate+0x9f/0x110, CPU#0: kworker/u9:2/51
Modules linked in:
CPU: 0 UID: 0 PID: 51 Comm: kworker/u9:2 Not tainted syzkaller #0 PREEMPT(full) 
Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.16.2-debian-1.16.2-1 04/01/2014
Workqueue: writeback wb_workfn (flush-8:0)
RIP: 0010:refcount_warn_saturate+0x9f/0x110
Code: eb 66 85 db 74 3e 83 fb 01 75 4c e8 8b 73 0c fd 48 8d 3d 34 58 84 0b 67 48 0f b9 3a eb 4a e8 78 73 0c fd 48 8d 3d 31 58 84 0b <67> 48 0f b9 3a eb 37 e8 65 73 0c fd 48 8d 3d 2e 58 84 0b 67 48 0f
RSP: 0018:ffffc90000bb6590 EFLAGS: 00010293
RAX: ffffffff84b9ee08 RBX: 0000000000000002 RCX: ffff8881072a8000
RDX: 0000000000000000 RSI: ffffffff8f126800 RDI: ffffffff903e4640
RBP: ffff888112257dd0 R08: ffff8881072a8000 R09: 0000000000000005
R10: 0000000000000004 R11: 0000000000000000 R12: dffffc0000000000
R13: ffff888112257d98 R14: ffff888112257e10 R15: 0000000000000008
FS:  0000000000000000(0000) GS:ffff88818dc15000(0000) knlGS:0000000000000000
CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: 00007fc831b2c080 CR3: 0000000112724000 CR4: 00000000000006f0
Call Trace:
 <TASK>
 ext4_mb_use_preallocated+0xaa0/0x14e0
 ext4_mb_new_blocks+0x5a0/0x4560
 ext4_ext_map_blocks+0x1534/0x5850
 ext4_map_create_blocks+0x116/0x530
 ext4_map_blocks+0x7e4/0x1240
 ext4_do_writepages+0x23d3/0x47a0
 ext4_writepages+0x241/0x3b0
 do_writepages+0x338/0x560
 __writeback_single_inode+0x12e/0xf90
 writeback_sb_inodes+0x9de/0x1b00
 __writeback_inodes_wb+0x114/0x240
 wb_writeback+0x42f/0xad0
 wb_workfn+0x980/0x10f0
 process_scheduled_works+0xa8e/0x14e0
 worker_thread+0xa47/0xfb0
 kthread+0x388/0x470
 ret_from_fork+0x514/0xb70
 ret_from_fork_asm+0x1a/0x30
 </TASK>

***

If these findings have caused you to resend the series or submit a separate fix, please add the following tag to your commit message:

---
This report is generated by a bot. It may contain errors. syzbot ci engineers can be reached at [email protected].

To test a patch for this bug, please reply with #syz test (should be on a separate line).

The patch should be attached to the email.
Note: arguments like custom git repos and branches are not supported.

reviewer
#2 lore raw
In reply to rafad900
Quoted 5 lines · On Sun 19-07-26 20:44:41, rafad900 wrote:

The pa_count field in struct ext4_prealloc_space is reference counter for preallocation descriptors. Converting it from atomic_t to refcount_t provides underflow and overflow protection.

Quoted 8 lines

This was found using Coccinelle script
atomic_as_refcounter.cocci

Testing was done on a QEMU x86 VM revealing a pre-existing race condition leading to a use-after-free.

This is a bit problematic as for pa_count 0->1 transition is fine (as long as pa_deleted isn't set) so this conversion will trigger false warnings as the 0-day report shows.

Honza

Quoted 117 lines
---
 fs/ext4/mballoc.c | 22 +++++++++++-----------
 fs/ext4/mballoc.h |  2 +-
 2 files changed, 12 insertions(+), 12 deletions(-)
fs/ext4/mballoc.c +11 -11
index ed1bd00e11cd..59b3d4abaab0 100644
@@ -4823,7 +4823,7 @@ ext4_mb_check_group_pa(ext4_fsblk_t goal_block,
 	ext4_fsblk_t cur_distance, new_distance;
 
 	if (cpa == NULL) {
-		atomic_inc(&pa->pa_count);
+		refcount_inc(&pa->pa_count);
 		return pa;
 	}
 	cur_distance = abs(goal_block - cpa->pa_pstart);
@@ -4833,8 +4833,8 @@ ext4_mb_check_group_pa(ext4_fsblk_t goal_block,
 		return cpa;
 
 	/* drop the previous reference */
-	atomic_dec(&cpa->pa_count);
-	atomic_inc(&pa->pa_count);
+	refcount_dec(&cpa->pa_count);
+	refcount_inc(&pa->pa_count);
 	return pa;
 }
 
@@ -4993,7 +4993,7 @@ ext4_mb_use_preallocated(struct ext4_allocation_context *ac)
 	}
 
 	if (tmp_pa->pa_free && likely(ext4_mb_pa_goal_check(ac, tmp_pa))) {
-		atomic_inc(&tmp_pa->pa_count);
+		refcount_inc(&tmp_pa->pa_count);
 		ext4_mb_use_inode_pa(ac, tmp_pa);
 		spin_unlock(&tmp_pa->pa_lock);
 		read_unlock(&ei->i_prealloc_lock);
@@ -5139,7 +5139,7 @@ static void ext4_mb_mark_pa_deleted(struct super_block *sb,
 static inline void ext4_mb_pa_free(struct ext4_prealloc_space *pa)
 {
 	BUG_ON(!pa);
-	BUG_ON(atomic_read(&pa->pa_count));
+	BUG_ON(refcount_read(&pa->pa_count));
 	BUG_ON(pa->pa_deleted == 0);
 	kmem_cache_free(ext4_pspace_cachep, pa);
 }
@@ -5165,7 +5165,7 @@ static void ext4_mb_put_pa(struct ext4_allocation_context *ac,
 
 	/* in this short window concurrent discard can set pa_deleted */
 	spin_lock(&pa->pa_lock);
-	if (!atomic_dec_and_test(&pa->pa_count) || pa->pa_free != 0) {
+	if (!refcount_dec_and_test(&pa->pa_count) || pa->pa_free != 0) {
 		spin_unlock(&pa->pa_lock);
 		return;
 	}
@@ -5535,7 +5535,7 @@ ext4_mb_discard_group_preallocations(struct super_block *sb,
 	list_for_each_entry_safe(pa, tmp,
 				&grp->bb_prealloc_list, pa_group_list) {
 		spin_lock(&pa->pa_lock);
-		if (atomic_read(&pa->pa_count)) {
+		if (refcount_read(&pa->pa_count)) {
 			spin_unlock(&pa->pa_lock);
 			*busy = 1;
 			continue;
@@ -5637,7 +5637,7 @@ void ext4_discard_preallocations(struct inode *inode)
 		BUG_ON(pa->pa_node_lock.inode_lock != &ei->i_prealloc_lock);
 
 		spin_lock(&pa->pa_lock);
-		if (atomic_read(&pa->pa_count)) {
+		if (refcount_read(&pa->pa_count)) {
 			/* this shouldn't happen often - nobody should
 			 * use preallocation while we're discarding it */
 			spin_unlock(&pa->pa_lock);
@@ -5720,7 +5720,7 @@ static int ext4_mb_pa_alloc(struct ext4_allocation_context *ac)
 	pa = kmem_cache_zalloc(ext4_pspace_cachep, GFP_NOFS);
 	if (!pa)
 		return -ENOMEM;
-	atomic_set(&pa->pa_count, 1);
+	refcount_set(&pa->pa_count, 1);
 	ac->ac_pa = pa;
 	return 0;
 }
@@ -5731,7 +5731,7 @@ static void ext4_mb_pa_put_free(struct ext4_allocation_context *ac)
 
 	BUG_ON(!pa);
 	ac->ac_pa = NULL;
-	WARN_ON(!atomic_dec_and_test(&pa->pa_count));
+	WARN_ON(!refcount_dec_and_test(&pa->pa_count));
 	/*
 	 * current function is only called due to an error or due to
 	 * len of found blocks < len of requested blocks hence the PA has not
@@ -5948,7 +5948,7 @@ ext4_mb_discard_lg_preallocations(struct super_block *sb,
 				pa_node.lg_list,
 				lockdep_is_held(&lg->lg_prealloc_lock)) {
 		spin_lock(&pa->pa_lock);
-		if (atomic_read(&pa->pa_count)) {
+		if (refcount_read(&pa->pa_count)) {
 			/*
 			 * This is the pa that we just used
 			 * for block allocation. So don't
fs/ext4/mballoc.h +1 -1
index 39333ce72cbd..f708d303228e 100644
@@ -126,7 +126,7 @@ struct ext4_prealloc_space {
 		struct rcu_head	pa_rcu;
 	} u;
 	spinlock_t		pa_lock;
-	atomic_t		pa_count;
+	refcount_t		pa_count;
 	unsigned		pa_deleted;
 	ext4_fsblk_t		pa_pstart;	/* phys. block */
 	ext4_lblk_t		pa_lstart;	/* log. block */
Signature · 1 line
2.43.0
Signature · 2 lines
Jan Kara <[email protected]>
SUSE Labs, CR
maintainer
#3 lore raw
Quoted 4 lines · Jan Kara

This is a bit problematic as for pa_count 0->1 transition is fine (as long as pa_deleted isn't set) so this conversion will trigger false warnings as the 0-day report shows.

For better or for worse, the lifetime rules of the ext4_prealloc_space structure don't match those assumed by refcount.h. Which is to say, it's not a reference count the same way that say, struct inode's i_links_count.

The whole *point* of the preallocation space is that we are holding some blocks for use by an inode or by block group, because it's too expensive to figure out that this set of blocks is available for allocation. So even though pa_count is zero, meaning that there isn't an *active* allocation in process, under normal circumstances we want to keep ext4_prealloc_space around and linked into its relevant linked list. This is why the pa_count 0->1 transition is OK, and it is _not_ an existing "use after free".

There's actually quite a lot of documentation in fs/ext4/mballoc.c, but the comments don't really make this point clearly enough. I think that's because the assumption is that this high-level description of what the goals of ext4_prealloc_space is assumed to be known by everyone trying to modify that part of the block allocator.

But given that we are having more people trying to submit changes to ext4 (in some cases, assisted by LLM's), it might be a good idea to improve the documentation, both for the sake of the humans and the LLM's that are trying to understand how things work.

Just for yucks, I asked an LLM to explain what a human might want to need to know about how the ext4_prealloc_space sutrcture was used, and it came up with this. (See attached.)

I don't propose adding this directly to the beginning of fs/ext4/mballoc.c, but we might want to see if there is some parts of this that we do want to add. (And to seeif what we currently have as comments at the beginning of mballoc.c is accurate, since I don't think anyone has reviewed it in a while.)

Cheers,

- Ted

P.S. Hi Rafa/Rafad, it appears that you are a relatively new Kernel contributor; I see that the only other patch contribution from your gmail account is was to attempt to address a jfs syzkaller issue. If you intend to contribute more ext4 changes, welcome! It would be great if you introduced yourself so we know a bit more about who you are, and what name you would prefer to be known by.

# struct ext4_prealloc_space allocation/deallocation and pa_count semantics

## struct ext4_prealloc_space — overview

Defined in fs/ext4/mballoc.h:118:

```c
struct ext4_prealloc_space {
	union {
		struct rb_node   inode_node;   /* for inode PA rbtree */
		struct list_head lg_list;      /* for lg PAs */
	} pa_node;
	struct list_head pa_group_list;
	union {
		struct list_head pa_tmp_list;
		struct rcu_head  pa_rcu;
	} u;
	spinlock_t   pa_lock;
	atomic_t     pa_count;
	unsigned     pa_deleted;
	ext4_fsblk_t pa_pstart;
	ext4_lblk_t  pa_lstart;
	ext4_grpblk_t pa_len;
	ext4_grpblk_t pa_free;
	unsigned short pa_type;   /* MB_INODE_PA or MB_GROUP_PA */
	union {
		rwlock_t   *inode_lock;
		spinlock_t *lg_lock;
	} pa_node_lock;
	struct inode *pa_inode;
};
```

A PA represents a chunk of physical blocks reserved ahead of an actual write, so future allocations for the same file (inode PA) or the same locality group of small files (group PA) can be satisfied without a full search. Each live PA sits on three structures: grp->bb_prealloc_list (per-group list, protected by the group lock), and either the inode's i_prealloc_node rbtree (protected by i_prealloc_lock, for MB_INODE_PA) or a locality group's hashed lg_prealloc_list[] (RCU list, for MB_GROUP_PA).

## Allocation

- ext4_mb_pa_alloc() (mballoc.c:5716) — kmem_cache_zalloc()s the object from ext4_pspace_cachep and does atomic_set(&pa->pa_count, 1). This single reference represents the caller's (the allocation context ac's) own hold on the PA — it isn't in any list yet.
- If the regular allocator succeeds and finds *more* space than requested, ext4_mb_new_preallocation() calls ext4_mb_new_inode_pa() or ext4_mb_new_group_pa(), which fill in pa_pstart/pa_lstart/pa_len/pa_free, mark pa_type, then link the PA into grp->bb_prealloc_list and into the inode rbtree (or leave it to be added to the lg hash later via ext4_mb_add_n_trim()). These linking steps do **not** touch pa_count.
- If no extra space was found, or an error occurs, ext4_mb_pa_put_free() (mballoc.c:5729) is used instead: it asserts (WARN_ON) that dropping the sole reference (atomic_dec_and_test) actually hits zero, then marks pa_deleted = 1 and frees it via ext4_mb_pa_free() — since it was never linked anywhere, there's nothing to unlink.

## pa_count semantics

pa_count is a **reference count on "who is currently using this PA for an in-flight allocation,"** not a count of pointers/links to the struct. It answers: *is anyone actively consuming blocks from this PA right now?*

- It starts at 1 when created (owned by the allocating ac).
- Every additional reader that wants to reuse the PA for a new allocation request bumps it first, under pa->pa_lock (or the rbtree/list lock protecting lookup):
- ext4_mb_use_preallocated() → atomic_inc(&tmp_pa->pa_count) before calling ext4_mb_use_inode_pa() (inode PA lookup via rbtree, mballoc.c:4997).
- ext4_mb_check_group_pa() (mballoc.c:4819) walks the lg hash list picking the closest group PA to the goal block, incrementing the candidate's count and decrementing the previous candidate's count as it walks (atomic_inc/atomic_dec pair) — pure "trial reference" bookkeeping while searching.
- ext4_mb_put_pa() (mballoc.c:5160) is the release path:
atomic_dec_and_test(&pa->pa_count). Only when this hits zero **and** pa_free == 0 (all blocks in the PA have been consumed) does it actually delete/free the PA — unlink from bb_prealloc_list, unlink from the inode rbtree or lg list, mark pa_deleted, and free (immediately via ext4_mb_pa_free() for inode PAs since they're rbtree-protected by a rwlock that serializes with lookups; via call_rcu() for group PAs since lookups walk the lg list under RCU).
- If the decrement reaches zero but pa_free != 0 (there are still unused blocks in the PA available for future requests), the PA is simply left alone/relinked — it's not consumed yet, just not currently in use by any allocator.

This is why pa_count is checked before discarding a PA for space
reclaim: ext4_mb_discard_group_preallocations(), ext4_discard_preallocations(), and ext4_mb_discard_lg_preallocations() all do if (atomic_read(&pa->pa_count)) { skip it } — a nonzero count means some other thread is mid-allocation against that PA (holds pa_lock briefly, but the increment happens under the rbtree/list lock before the discarding thread can grab pa_group_list/inode rbtree/lg-list locks), so it's unsafe to rip blocks out from under it. This is a real (if narrow) race window explicitly called out in the "possible race" comments (e.g. mballoc.c:5195-5205 and the block-history comment at 5003-5030): a PA's pa_free can only reach 0 while i_data_sem is held for that inode, and once it hits 0 the same call chain (ext4_mb_new_blocks → ext4_mb_release_context → ext4_mb_put_pa) immediately removes it, so no other thread ever observes a zero-pa_free, still-linked PA in the tree.

The invariant enforced by BUG_ON(atomic_read(&pa->pa_count)) in ext4_mb_pa_free() (mballoc.c:5143) is absolute: nothing may free a ext4_prealloc_space while any reference is outstanding.

## Deallocation paths

Two flavors of actual teardown, both requiring pa_deleted == 1 first (set once, guarded so double-delete warns rather than corrupting state — ext4_mb_mark_pa_deleted()):

1. **Consumed exactly** (ext4_mb_put_pa): the last user of a fully-drained PA (pa_free == 0) tears it down inline — no blocks need to be freed back to the bitmap because they were all legitimately allocated.
2. **Discarded with leftover blocks** (ext4_mb_release_inode_pa / ext4_mb_release_group_pa, called from ext4_mb_discard_group_preallocations(), ext4_discard_preallocations() (per-inode, e.g. on truncate/evict), and ext4_mb_discard_lg_preallocations() (trimming an overlong lg hash bucket)): these walk the still-reserved-but-unused bit range and call mb_free_blocks() to actually release it back to the buddy/bitmap, accounting into sbi->s_mb_discarded, before the PA struct itself is freed.

In both flavors, the struct's memory is only released via ext4_mb_pa_free() (or the RCU-deferred ext4_mb_pa_callback() for group PAs, since they're looked up under rcu_read_lock()), and only after confirming pa_count == 0 and pa_deleted == 1.

## Inode PA vs. group PA: how the two types differ

pa_type (MB_INODE_PA vs MB_GROUP_PA) isn't just a label — the two kinds have distinct owners, distinct container structures, distinct sizing/shrink behavior, and distinct discard triggers. The choice between them is made once per allocation request, in ext4_mb_group_or_file() (mballoc.c:5831):

- If the request isn't a data allocation, or EXT4_MB_HINT_GOAL_ONLY is set, no preallocation of either kind happens.
- Otherwise the function computes inode_pa_eligible (false only for a closed, non-open-for-write file whose size already matches — no point prealloc'ing past EOF for a file nobody will extend) and group_pa_eligible (false if s_mb_group_prealloc is disabled, or if the request is "large" — bigger than s_mb_stream_request — since dumping a large file's blocks into a shared per-CPU locality-group pool would fragment it for everyone else).
- If group allocation isn't eligible, the request falls back to inode preallocation (EXT4_MB_STREAM_ALLOC) or no preallocation at all.
- If group allocation *is* eligible, the context grabs a per-CPU struct ext4_locality_group (raw_cpu_ptr(sbi->s_locality_groups)) and locks lg->lg_mutex for the duration of the allocation, serializing all allocation/discard activity against that CPU's locality group.

### Inode preallocation (MB_INODE_PA)

**Purpose:** speeds up sequential writes to a single file by reserving trailing blocks past what was actually requested, so the next ext4_map_blocks() call for the same inode finds physically contiguous space without a fresh search.

**Ownership/container:** exactly one inode owns it. It's threaded onto ei->i_prealloc_node, an rbtree keyed by pa_lstart (logical block), protected by ei->i_prealloc_lock (a rwlock — readers use read_lock() in ext4_mb_use_preallocated(), writers use write_lock() to insert/erase). It's also linked into grp->bb_prealloc_list for the physical group it lives in, and ei->i_prealloc_active is incremented on creation / decremented in ext4_mb_mark_pa_deleted().

**Creation (ext4_mb_new_inode_pa(), mballoc.c:5251):** called only when the block allocator found *more* contiguous space (ac_b_ex) than the caller actually asked for (ac_o_ex) — BUG_ON(ac_o_ex.fe_len >= ac_b_ex.fe_len). The extra logic (the adjust_bex block) decides where within the goal window to anchor pa_lstart so the original request's logical range is still covered, since the found extent may be smaller than the full normalized goal. pa_free is initialized to the whole pa_len and then immediately reduced by ext4_mb_use_inode_pa(), which consumes just the sub-range needed for *this* request (so a freshly created inode PA typically already has pa_free < pa_len by the time it's inserted into the rbtree).

**Subsequent reuse:** later allocation requests for the same inode (typically the next sequential write) hit ext4_mb_use_preallocated(), which does a rbtree walk (Steps 1-4 in that function) to find the PA whose logical range is adjacent to/overlapping the new request's logical start, skipping any PA already marked pa_deleted by a racing discard. On a hit it does atomic_inc(&pa_count) then ext4_mb_use_inode_pa(), which trims pa_free by however much of the PA this request consumes. Because inode PAs are per-file and non-shared across processes, in practice contention is only against a truncate/evict of the same file or an ENOSPC-driven discard.

**Freeing:**
- Normal exhaustion: once pa_free reaches 0, the next ext4_mb_put_pa() on it deletes and frees it inline — no leftover blocks, nothing to return to the bitmap.
- Explicit discard on ext4_discard_preallocations() (called from truncate, ext4_evict_inode(), etc.): walks the whole rbtree under write_lock(&ei->i_prealloc_lock), and for every PA with pa_count == 0 marks it deleted and moves it to a local list (a PA still in use — pa_count != 0 — triggers a WARN_ON and a retry loop, since nothing should be actively allocating from a file while it's being truncated/evicted). The collected PAs are then unlinked from bb_prealloc_list, have their still-unused blocks returned to the bitmap/buddy via ext4_mb_release_inode_pa(), and freed with ext4_mb_pa_free().
- ENOSPC-driven discard (ext4_mb_discard_group_preallocations(), per-group): treats inode and group PAs uniformly at the bb_prealloc_list level, but unlinks inode PAs from the rbtree (rb_erase) rather than an RCU list, and frees them immediately (not RCU-deferred), matching the rwlock-based (not RCU-based) protection of the rbtree.

### Group preallocation (MB_GROUP_PA)

**Purpose:** batches small, unrelated files' allocations from the same CPU into shared, moderately-sized chunks, so many small files opened concurrently on one CPU don't each trigger a full buddy search — instead they carve slices out of one shared reservation.

**Ownership/container:** not tied to one inode (pa_inode = NULL); owned by a per-CPU struct ext4_locality_group. It's linked into lg->lg_prealloc_list[order] — a hash table of PREALLOC_TB_SIZE (10) RCU list buckets, indexed by fls(pa_free) - 1 (i.e. buckets group PAs by approximate remaining free size) — plus the same grp->bb_prealloc_list per-group list that inode PAs use. The lg_list is RCU-protected (list_for_each_entry_rcu, list_add_tail_rcu, list_del_rcu) since lookups (ext4_mb_use_preallocated()'s try_group_pa path, ext4_mb_check_group_pa()) happen under rcu_read_lock() rather than the rwlock inode PAs use.

**Creation (ext4_mb_new_group_pa(), mballoc.c:5352):** same precondition as inode PA (found more than requested), but pa_lstart is set equal to pa_pstart (group PAs have no meaningful "logical" position of their own — they're just a floating pool of physical blocks) and it's linked only into grp->bb_prealloc_list at creation time; insertion into the lg hash table is deferred to ext4_mb_release_context() after the current request's consumption is known.

**Subsequent reuse — the key difference from inode PA:** rather than trimming a sub-range out of the middle of the PA the way ext4_mb_use_inode_pa() does, ext4_mb_use_group_pa() (mballoc.c:4791) always takes blocks off the *front*. It deliberately does **not** correct pa_pstart/pa_len/pa_free at use time (comment: avoids a race with a concurrently-loading buddy bitmap); that correction happens afterward, in ext4_mb_release_context():

```c
pa->pa_pstart += EXT4_C2B(sbi, ac->ac_b_ex.fe_len);
pa->pa_lstart += EXT4_C2B(sbi, ac->ac_b_ex.fe_len);
pa->pa_free   -= ac->ac_b_ex.fe_len;
pa->pa_len    -= ac->ac_b_ex.fe_len;
```

So a group PA's pa_pstart literally slides forward every time a chunk is carved off the front — it behaves like a bump allocator over a fixed physical range, unlike an inode PA whose pa_pstart/pa_lstart are fixed for its whole life and whose *interior* free sub-ranges get consumed. After shrinking, if pa_free is still nonzero the PA is
re-hashed: removed from its current lg_prealloc_list[order] bucket and re-added via ext4_mb_add_n_trim(), since its shrunken pa_free may now belong in a different fls() order bucket.

**List trimming (ext4_mb_add_n_trim(), mballoc.c:6019):** when re-inserting a group PA into its bucket, the function also counts how many entries are in that bucket; if it exceeds 8, ext4_mb_discard_lg_preallocations() is invoked to prune it back down to 5, discarding the least-recently-useful (by walk order) non-busy (pa_count == 0), non-deleted entries. This is a size-bounded eviction unique to group PAs — inode PAs have no analogous cap because there's naturally at most a handful of live PAs per inode.

**Freeing:**
- Normal exhaustion: same as inode PA — pa_free hits 0 and the last ext4_mb_put_pa() deletes it inline. Since pa_type == MB_GROUP_PA, unlinking uses list_del_rcu(&pa->pa_node.lg_list) and the struct is freed via call_rcu(&pa->u.pa_rcu, ext4_mb_pa_callback) rather than immediately — readers may still be mid-traversal of the RCU list.
- List-size trimming (ext4_mb_discard_lg_preallocations()): triggered proactively (not by ENOSPC) whenever a bucket grows past 8 entries, as described above. Leftover blocks are returned to the bitmap/buddy via ext4_mb_release_group_pa() (a simple single-range mb_free_blocks(), unlike the inode path's mb_find_next_zero_bit() scan, because a group PA's unused region is always one contiguous trailing range, never a punctured interior).
- ENOSPC-driven discard (ext4_mb_discard_group_preallocations()):
identical group-list logic as for inode PAs, except unlinking is list_del_rcu() from the lg hash bucket instead of rb_erase(), and freeing is RCU-deferred.

### Summary of the asymmetry

| | Inode PA | Group PA |
|---|---|---|
| Owner | one inode | per-CPU locality group |
| Container | `i_prealloc_node` rbtree (rwlock) | `lg_prealloc_list[order]` hash of RCU lists |
| `pa_lstart` meaning | real logical file offset | mirrors `pa_pstart`, no real meaning |
| Consumption pattern | interior sub-range per use | always trims from the front (bump-allocator) |
| Correction timing | immediate, in `ext4_mb_use_inode_pa()` | deferred to `ext4_mb_release_context()` |
| Size-bounded eviction | none (bounded by file count) | yes — `ext4_mb_add_n_trim()` caps buckets at 8 |
| Free on empty | immediate `ext4_mb_pa_free()` | RCU-deferred `call_rcu()` |
| Discard trigger(s) | truncate/evict (`ext4_discard_preallocations`), ENOSPC | bucket overflow (`ext4_mb_discard_lg_preallocations`), ENOSPC |

Both types, however, share the same pa_count discipline described
above: nothing may unlink or free either kind while pa_count > 0, and pa_free == 0 is what distinguishes "quietly retire on last put" from "actively discard and return blocks."

## ext4_mb_new_preallocation() — the dispatch point

```c
static void ext4_mb_new_preallocation(struct ext4_allocation_context *ac)
{
	if (ac->ac_flags & EXT4_MB_HINT_GROUP_ALLOC)
		ext4_mb_new_group_pa(ac);
	else
		ext4_mb_new_inode_pa(ac);
}
```

This is the single call site (invoked from ext4_mb_use_best_found(), mballoc.c:2282, right after the regular allocator settles on ac_b_ex) that turns "we found a good extent" into "we created a PA object for future reuse." It doesn't decide *whether* to preallocate — that decision was already made earlier by ext4_mb_group_or_file(), which set EXT4_MB_HINT_GROUP_ALLOC in ac_flags (or left it unset, or set EXT4_MB_HINT_NOPREALLOC which routes around this call entirely — see ext4_mb_use_best_found()). ext4_mb_new_preallocation() is purely a two-way switch on that already-decided flag, dispatching to whichever of ext4_mb_new_inode_pa() / ext4_mb_new_group_pa() matches. Both callees share the same precondition (ac_o_ex.fe_len < ac_b_ex.fe_len — the allocator over-shot the request) and both expect ac->ac_pa to already point at a freshly ext4_mb_pa_alloc()'d object (allocated earlier in ext4_mb_new_blocks() before the search even ran, at mballoc.c:6306, so it's ready the moment the allocator finds a hit).

## ext4_mb_discard_preallocations_should_retry() — deciding whether an ENOSPC-looking failure is real

When ext4_mb_regular_allocator() fails to find a suitable extent (ac->ac_status != AC_STATUS_FOUND), ext4_mb_new_blocks()'s retry loop doesn't immediately give up with -ENOSPC. Preallocations can be holding blocks reserved-but-unused, which the bitmap/buddy structures see as "in use" even though nobody will ever consume them (e.g. an inode PA nobody will finish writing to, or a group PA sitting idle in an lg bucket) — so a real "disk full" and a "full of dead-weight preallocation" look identical to the allocator. This function (mballoc.c:6133), called at the repeat: label (mballoc.c:6338-6340), is what tells the caller whether it's worth clearing that dead weight out and trying again, up to 3 times (retries < 3):

```c
static bool ext4_mb_discard_preallocations_should_retry(struct super_block *sb,
			struct ext4_allocation_context *ac, u64 *seq)
{
	int freed;
	u64 seq_retry = 0;
	bool ret = false;

	freed = ext4_mb_discard_preallocations(sb, ac->ac_o_ex.fe_len);
	if (freed) {
		ret = true;
		goto out_dbg;
	}
	seq_retry = ext4_get_discard_pa_seq_sum();
	if (!(ac->ac_flags & EXT4_MB_STRICT_CHECK) || seq_retry != *seq) {
		ac->ac_flags |= EXT4_MB_STRICT_CHECK;
		*seq = seq_retry;
		ret = true;
	}

out_dbg:
	return ret;
}
```

It works in two tiers:

1. **Try to actually free something.** It calls ext4_mb_discard_preallocations(sb, ac->ac_o_ex.fe_len) (mballoc.c:6106), which loops ext4_mb_discard_group_preallocations() over every group in the filesystem (or until needed clusters have been freed) tearing down any PA with pa_count == 0 and returning its leftover pa_free blocks to the bitmap. If this frees any blocks at all (freed != 0), that's a strong signal the retry is worth it — return true immediately without even checking the sequence counter.

2. **If nothing was freed, use the discard_pa_seq counter to detect a race rather than genuine exhaustion.** The comment block at mballoc.c:435-452 explains the scheme: every CPU keeps a percpu discard_pa_seq counter that's bumped whenever blocks are marked used/freed (mb_mark_used()/mb_free_blocks()) or whenever a PA is discarded. ext4_mb_new_blocks() samples *its own* CPU's counter once up front (seq = this_cpu_read(discard_pa_seq) at mballoc.c:6301) as a cheap fast-path check. If the retry-should-happen function finds freed == 0 on this call, it falls back to ext4_get_discard_pa_seq_sum(), which sums the counter across *all* CPUs — a more expensive, globally-accurate view. If that global sum has moved since the sample was taken (seq_retry != *seq), it means some other CPU concurrently allocated, freed, or discarded blocks/PAs while this allocation was in flight — i.e., the group's apparent fullness may already be stale, so it's worth retrying even though this thread's own discard pass found nothing. The EXT4_MB_STRICT_CHECK flag is a one-shot escalation:
the first time this branch is taken it's set on ac_flags and the retry is granted unconditionally; on a *second* consecutive miss with the flag already set, the retry is only granted if the global sequence value actually changed since the last check (seq_retry != *seq) — otherwise the loop concludes the fullness is real and returns false, letting -ENOSPC propagate. EXT4_MB_STRICT_CHECK also has a side effect back in group scanning:
ext4_mb_good_group_nolock() (mballoc.c:2777) takes the group's spinlock instead of reading grp->bb_free unlocked when the flag is set, trading the fast-path's tolerance for stale/racy free-count reads for a consistent view — appropriate once the allocator is in this "am I really out of space" retry regime rather than the common case.

In short: ext4_mb_new_blocks() treats an allocation failure as potentially caused by live-but-idle preallocations rather than true exhaustion, and this function is the policy that decides, cheaply at first (did discarding free anything?) and then via a global race-detection heuristic (did *anything* change filesystem-wide?), whether another pass over the groups is worth attempting before surfacing -ENOSPC to the caller.

#4 lore raw

Hello everyone,

Thanks for the helpful responses! My name is Rafael A. Diaz Cruz and I'm hoping to join the Linux community. I have a background in CS with a Masters from UC Davis and am currently working as a Software Dev at AMEX.

I read the kernel in my spare time and joined the LFX mentorship starting in April of this year but due to personal reasons I wasn't able to dedicate much time until recently. I do hope to contribute more to ext4 and the kernel as a whole, however, given that the kernel is over 40 million lines of code I think it
might take me some time to catch up :) I do greatly appreciate your tolerance for
my lack of expertise!

I did continue looking at what the overall ext4_prealloc_space struct was used for and realized that the pa_count was being set to 0 on purpose. I did not understand it at first given that other subsystems in the kernel treat 0 references
as the object being completely dead. But I think I see now how it's being used.

I did try to build my mental model of how ext4 was treating the preallocation blocks
from the comments alone, yet, it's taking me some time due to the sheer size of mballoc.c and its complexity. I was wondering if there are some diagrams that show the flow of the overall system? I get lost in particular when thinking of multiple
threads reaching the same function.

Also, I have the unit tests for both the kernel and the ext4 fs but I would like to know
if there are other methods by which I should check my work besides QEMU.

https://www.linkedin.com/in/rafaelalejandrodiazcruz/

Quoted 53 lines · Theodore Tso
Quoted 4 lines · Jan Kara

This is a bit problematic as for pa_count 0->1 transition is fine (as long as pa_deleted isn't set) so this conversion will trigger false warnings as the 0-day report shows.

For better or for worse, the lifetime rules of the ext4_prealloc_space structure don't match those assumed by refcount.h. Which is to say, it's not a reference count the same way that say, struct inode's i_links_count.

The whole *point* of the preallocation space is that we are holding some blocks for use by an inode or by block group, because it's too expensive to figure out that this set of blocks is available for allocation. So even though pa_count is zero, meaning that there isn't an *active* allocation in process, under normal circumstances we want to keep ext4_prealloc_space around and linked into its relevant linked list. This is why the pa_count 0->1 transition is OK, and it is _not_ an existing "use after free".

There's actually quite a lot of documentation in fs/ext4/mballoc.c, but the comments don't really make this point clearly enough. I think that's because the assumption is that this high-level description of what the goals of ext4_prealloc_space is assumed to be known by everyone trying to modify that part of the block allocator.

But given that we are having more people trying to submit changes to ext4 (in some cases, assisted by LLM's), it might be a good idea to improve the documentation, both for the sake of the humans and the LLM's that are trying to understand how things work.

Just for yucks, I asked an LLM to explain what a human might want to need to know about how the ext4_prealloc_space sutrcture was used, and it came up with this. (See attached.)

I don't propose adding this directly to the beginning of fs/ext4/mballoc.c, but we might want to see if there is some parts of this that we do want to add. (And to seeif what we currently have as comments at the beginning of mballoc.c is accurate, since I don't think anyone has reviewed it in a while.)

Cheers,

- Ted

P.S. Hi Rafa/Rafad, it appears that you are a relatively new Kernel contributor; I see that the only other patch contribution from your gmail account is was to attempt to address a jfs syzkaller issue. If you intend to contribute more ext4 changes, welcome! It would be great if you introduced yourself so we know a bit more about who you are, and what name you would prefer to be known by.