[Concept,11/26] ext4l: Clean up fully when unmounting

Message ID 20251231223008.3251711-12-sjg@u-boot.org
State New
Headers
Series ext4l: Add write support (part L) |

Commit Message

Simon Glass Dec. 31, 2025, 10:29 p.m. UTC
  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

Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com>
Signed-off-by: Simon Glass <simon.glass@canonical.com>
---

 fs/ext4l/ext4.h       |   6 ++
 fs/ext4l/ext4_uboot.h |   1 +
 fs/ext4l/interface.c  | 135 ++++++++++++++++++++++++++++++++++++++++++
 fs/ext4l/super.c      |  25 ++++++--
 4 files changed, 162 insertions(+), 5 deletions(-)
  

Patch

diff --git a/fs/ext4l/ext4.h b/fs/ext4l/ext4.h
index 669d5522f27..a1c80dd7cdf 100644
--- a/fs/ext4l/ext4.h
+++ b/fs/ext4l/ext4.h
@@ -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 */
diff --git a/fs/ext4l/ext4_uboot.h b/fs/ext4l/ext4_uboot.h
index 78d44faa0db..abe54e67aa4 100644
--- a/fs/ext4l/ext4_uboot.h
+++ b/fs/ext4l/ext4_uboot.h
@@ -2086,6 +2086,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 {
diff --git a/fs/ext4l/interface.c b/fs/ext4l/interface.c
index aebcc17fd3a..ceedabdb727 100644
--- a/fs/ext4l/interface.c
+++ b/fs/ext4l/interface.c
@@ -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();
diff --git a/fs/ext4l/super.c b/fs/ext4l/super.c
index 9ed6f907b7a..48c87eb0e97 100644
--- a/fs/ext4l/super.c
+++ b/fs/ext4l/super.c
@@ -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;
 
 /*