From: Simon Glass <simon.glass@canonical.com>
Resources are not properly released on unmount, causing memory leaks
in long-running U-Boot sessions that remount filesystems.
Add ext4l_free_sb() to release all resources on unmount:
- Destroy journal and commit superblock
- Release superblock buffer and unregister lazy init
- Free mballoc data and release system zone
- Destroy xattr caches
- Free group descriptors and flex groups
- Evict all tracked inodes
- Free root dentry, sbi, and superblock structures
Also:
- Init the s_inodes list when allocating superblock
- Free mount context (ctx, fc) after successful mount
- Call destroy_inodecache() during global cleanup
- Clear folio cache on buddy cache inode before iput()
Co-developed-by: Claude <noreply@anthropic.com>
Signed-off-by: Simon Glass <simon.glass@canonical.com>
---
(no changes since v1)
fs/ext4l/ext4.h | 6 ++
fs/ext4l/ext4_uboot.h | 2 +
fs/ext4l/interface.c | 135 ++++++++++++++++++++++++++++++++++++++++++
fs/ext4l/mballoc.c | 4 ++
fs/ext4l/super.c | 25 ++++++--
fs/ext4l/support.c | 24 ++++++++
6 files changed, 191 insertions(+), 5 deletions(-)
@@ -3947,4 +3947,10 @@ extern int ext4_block_write_begin(handle_t *handle, struct folio *folio,
#define EFSBADCRC EBADMSG /* Bad CRC detected */
#define EFSCORRUPTED EUCLEAN /* Filesystem is corrupted */
+/* Cleanup functions exported from super.c */
+void ext4_group_desc_free(struct ext4_sb_info *sbi);
+void ext4_flex_groups_free(struct ext4_sb_info *sbi);
+void ext4_destroy_lazy_init(void);
+void destroy_inodecache(void);
+
#endif /* _EXT4_H */
@@ -1395,6 +1395,7 @@ struct folio *__filemap_get_folio(struct address_space *mapping,
gfp_t gfp);
void folio_put(struct folio *folio);
void folio_get(struct folio *folio);
+void mapping_clear_folio_cache(struct address_space *mapping);
/* projid_t - project ID type */
typedef unsigned int projid_t;
@@ -2095,6 +2096,7 @@ struct fs_context {
/* ext4 superblock initialisation and commit */
int ext4_fill_super(struct super_block *sb, struct fs_context *fc);
int ext4_commit_super(struct super_block *sb);
+void ext4_unregister_li_request(struct super_block *sb);
/* fs_parameter stubs */
struct fs_parameter {
@@ -22,6 +22,8 @@
#include "ext4_uboot.h"
#include "ext4.h"
+#include "ext4_jbd2.h"
+#include "xattr.h"
/* Global state */
static struct blk_desc *ext4l_dev_desc;
@@ -142,6 +144,134 @@ void ext4l_clear_blk_dev(void)
ext4l_mounted = 0;
}
+/**
+ * ext4l_free_sb() - Free superblock and associated resources
+ * @sb: Superblock to free
+ * @skip_io: If true, skip all I/O operations (for forced close)
+ *
+ * Releases all resources associated with the superblock including the journal,
+ * caches, inodes, and the superblock structure itself.
+ */
+static void ext4l_free_sb(struct super_block *sb, bool skip_io)
+{
+ struct ext4_sb_info *sbi = EXT4_SB(sb);
+
+ /*
+ * Destroy journal first to properly clean up all buffers.
+ * If skip_io is set, the device may be invalid so skip
+ * journal destroy entirely - it will be recovered on next mount.
+ */
+ if (sbi->s_journal && !skip_io)
+ ext4_journal_destroy(sbi, sbi->s_journal);
+
+ /* Commit superblock if device is valid and I/O is allowed */
+ if (!skip_io)
+ ext4_commit_super(sb);
+
+ /* Release superblock buffer */
+ brelse(sbi->s_sbh);
+
+ /* Unregister lazy init and free if no longer needed */
+ ext4_unregister_li_request(sb);
+ ext4_destroy_lazy_init();
+
+ /* Free mballoc data */
+ ext4_mb_release(sb);
+
+ /* Release system zone */
+ ext4_release_system_zone(sb);
+
+ /* Destroy xattr caches */
+ ext4_xattr_destroy_cache(sbi->s_ea_inode_cache);
+ sbi->s_ea_inode_cache = NULL;
+ ext4_xattr_destroy_cache(sbi->s_ea_block_cache);
+ sbi->s_ea_block_cache = NULL;
+
+ /* Free group descriptors and flex groups */
+ ext4_group_desc_free(sbi);
+ ext4_flex_groups_free(sbi);
+
+ /* Evict all inodes before destroying caches */
+ while (!list_empty(&sb->s_inodes)) {
+ struct inode *inode;
+ struct ext4_inode_info *ei;
+
+ inode = list_first_entry(&sb->s_inodes,
+ struct inode, i_sb_list);
+ list_del_init(&inode->i_sb_list);
+ /* Clear extent status and free the inode */
+ ext4_es_remove_extent(inode, 0, EXT_MAX_BLOCKS);
+ ei = EXT4_I(inode);
+ kfree(ei);
+ }
+
+ /* Free root dentry */
+ if (sb->s_root) {
+ kfree(sb->s_root);
+ sb->s_root = NULL;
+ }
+
+ /* Free sbi */
+ kfree(sbi->s_blockgroup_lock);
+ kfree(sbi);
+
+ /* Free structures allocated in ext4l_probe() */
+ kfree(sb->s_bdev->bd_mapping);
+ kfree(sb->s_bdev);
+ kfree(sb);
+}
+
+/**
+ * ext4l_close_internal() - Internal close function
+ * @skip_io: If true, skip all I/O operations (for forced close)
+ *
+ * When called from the safeguard in ext4l_probe(), the device may be
+ * invalid (rebound to a different file), so skip_io should be true to
+ * avoid crashes when trying to write to the device.
+ */
+static void ext4l_close_internal(bool skip_io)
+{
+ struct super_block *sb = ext4l_sb;
+
+ if (ext4l_open_dirs > 0)
+ return;
+
+ if (sb)
+ ext4l_free_sb(sb, skip_io);
+
+ ext4l_dev_desc = NULL;
+ ext4l_sb = NULL;
+
+ /*
+ * Force cleanup of any remaining journal_heads before clearing
+ * the buffer cache. This ensures no stale journal_head references
+ * survive to the next mount. This is critical even when skip_io
+ * is true - we MUST disconnect journal_heads before freeing
+ * buffer_heads to avoid dangling pointers.
+ */
+ bh_cache_release_jbd();
+
+ ext4l_clear_blk_dev();
+
+ /*
+ * Clean up ext4 and JBD2 global state so it can be properly
+ * reinitialised on the next mount. This is important in U-Boot
+ * where we may mount/unmount filesystems multiple times in a
+ * single session.
+ *
+ * Even when skip_io is true (journal wasn't properly destroyed),
+ * we must destroy the caches to free all orphaned journal_heads.
+ * The next mount will reinitialise fresh caches.
+ */
+ ext4_exit_system_zone();
+ ext4_exit_es();
+ if (IS_ENABLED(CONFIG_EXT4_WRITE))
+ ext4_exit_mballoc();
+ if (IS_ENABLED(CONFIG_EXT4_JOURNAL))
+ jbd2_journal_exit_global();
+ destroy_inodecache();
+}
+
int ext4l_probe(struct blk_desc *fs_dev_desc,
struct disk_partition *fs_partition)
{
@@ -192,6 +322,7 @@ int ext4l_probe(struct blk_desc *fs_dev_desc,
ret = -ENOMEM;
goto err_exit_es;
}
+ INIT_LIST_HEAD(&sb->s_inodes);
/* Allocate block_device */
sb->s_bdev = kzalloc(sizeof(struct block_device), GFP_KERNEL);
@@ -279,6 +410,10 @@ int ext4l_probe(struct blk_desc *fs_dev_desc,
/* Store super_block for later operations */
ext4l_sb = sb;
+ /* Free mount context - no longer needed after successful mount */
+ kfree(ctx);
+ kfree(fc);
+
/* Print messages if ext4l_msgs environment variable is set */
if (env_get_yesno("ext4l_msgs") == 1)
ext4l_print_msgs();
@@ -3890,6 +3890,10 @@ void ext4_mb_release(struct super_block *sb)
ext4_mb_largest_free_orders_destroy(sbi);
kfree(sbi->s_mb_offsets);
kfree(sbi->s_mb_maxs);
+#ifdef __UBOOT__
+ if (sbi->s_buddy_cache)
+ mapping_clear_folio_cache(sbi->s_buddy_cache->i_mapping);
+#endif
iput(sbi->s_buddy_cache);
if (sbi->s_mb_stats) {
ext4_msg(sb, KERN_INFO,
@@ -48,7 +48,6 @@ static int ext4_unfreeze(struct super_block *sb);
static int ext4_freeze(struct super_block *sb);
static inline int ext2_feature_set_ok(struct super_block *sb);
static inline int ext3_feature_set_ok(struct super_block *sb);
-static void ext4_unregister_li_request(struct super_block *sb);
static void ext4_clear_request_list(void);
static struct inode *ext4_get_journal_inode(struct super_block *sb,
unsigned int journal_inum);
@@ -1228,7 +1227,7 @@ static void ext4_percpu_param_destroy(struct ext4_sb_info *sbi)
percpu_free_rwsem(&sbi->s_writepages_rwsem);
}
-static void ext4_group_desc_free(struct ext4_sb_info *sbi)
+void ext4_group_desc_free(struct ext4_sb_info *sbi)
{
struct buffer_head **group_desc;
int i;
@@ -1241,7 +1240,7 @@ static void ext4_group_desc_free(struct ext4_sb_info *sbi)
rcu_read_unlock();
}
-static void ext4_flex_groups_free(struct ext4_sb_info *sbi)
+void ext4_flex_groups_free(struct ext4_sb_info *sbi)
{
struct flex_groups **flex_groups;
int i;
@@ -1484,7 +1483,7 @@ static int __init init_inodecache(void)
return 0;
}
-static void destroy_inodecache(void)
+void destroy_inodecache(void)
{
/*
* Make sure all delayed rcu free inodes are flushed before we
@@ -3708,7 +3707,7 @@ static void ext4_remove_li_request(struct ext4_li_request *elr)
kfree(elr);
}
-static void ext4_unregister_li_request(struct super_block *sb)
+void ext4_unregister_li_request(struct super_block *sb)
{
mutex_lock(&ext4_li_mtx);
if (!ext4_li_info) {
@@ -3722,6 +3721,22 @@ static void ext4_unregister_li_request(struct super_block *sb)
mutex_unlock(&ext4_li_mtx);
}
+/*
+ * ext4_destroy_lazy_init() - Free lazy init info if no longer needed
+ *
+ * In U-Boot, there is no lazy init thread, so this must be called after
+ * ext4_unregister_li_request() to free ext4_li_info when the list is empty.
+ */
+void ext4_destroy_lazy_init(void)
+{
+ mutex_lock(&ext4_li_mtx);
+ if (ext4_li_info && list_empty(&ext4_li_info->li_request_list)) {
+ kfree(ext4_li_info);
+ ext4_li_info = NULL;
+ }
+ mutex_unlock(&ext4_li_mtx);
+}
+
static struct task_struct *ext4_lazyinit_task;
/*
@@ -897,3 +897,27 @@ void folio_get(struct folio *folio)
if (folio)
folio->_refcount++;
}
+
+/**
+ * mapping_clear_folio_cache() - Release all folios in an address_space cache
+ * @mapping: The address_space to clear
+ *
+ * Releases the cache's reference to each folio. If no other references exist,
+ * the folio will be freed.
+ */
+void mapping_clear_folio_cache(struct address_space *mapping)
+{
+ int i;
+
+ if (!mapping)
+ return;
+
+ for (i = 0; i < mapping->folio_cache_count; i++) {
+ struct folio *folio = mapping->folio_cache[i];
+
+ if (folio)
+ folio_put(folio);
+ mapping->folio_cache[i] = NULL;
+ }
+ mapping->folio_cache_count = 0;
+}