[Concept,v2,23/30] ext4l: Add write support

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

Commit Message

Simon Glass Jan. 2, 2026, 12:50 a.m. UTC
  From: Simon Glass <simon.glass@canonical.com>

Add the ability to write files to ext4 filesystems using the ext4l
driver. This enables the 'save' command to work with ext4l.

The implementation uses the jbd2 journal for crash safety:
- ext4l_write() creates files if needed and writes data
- Journal transactions commit synchronously for durability
- Buffer cache syncs dirty buffers after write operations

The write path consists of the following steps:
1. Lookup or create file via ext4_create()
2. Start journal transaction
3. For each block: get/allocate block, copy data, sync to disk
4. Update inode size and commit transaction
5. Sync all dirty buffers

Add an ext4l_op_ptr() macro to select between a write operation and a
fallback based on CONFIG_EXT4_WRITE, avoiding #ifdefs in fstypes[].

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

(no changes since v1)

 fs/ext4l/interface.c                | 271 ++++++++++++++++++++++++++++
 fs/fs_legacy.c                      |   2 +-
 include/ext4l.h                     |  23 +++
 test/fs/ext4l.c                     |  42 +++++
 test/py/tests/test_fs/test_ext4l.py |   5 +
 5 files changed, 342 insertions(+), 1 deletion(-)
  

Patch

diff --git a/fs/ext4l/interface.c b/fs/ext4l/interface.c
index c60ea7db684..94bfd71bfb5 100644
--- a/fs/ext4l/interface.c
+++ b/fs/ext4l/interface.c
@@ -874,6 +874,277 @@  int ext4l_read(const char *filename, void *buf, loff_t offset, loff_t len,
 	return 0;
 }
 
+static int ext4l_write_file(struct inode *dir, const char *filename, void *buf,
+			    loff_t offset, loff_t len, loff_t *actwrite)
+{
+	struct dentry *dir_dentry, *dentry, *result;
+	handle_t *handle = NULL;
+	struct buffer_head *bh;
+	struct inode *inode;
+	loff_t pos, end;
+	umode_t mode;
+	int ret;
+
+	/* Create dentry for the parent directory */
+	dir_dentry = kzalloc(sizeof(struct dentry), GFP_KERNEL);
+	if (!dir_dentry)
+		return -ENOMEM;
+	dir_dentry->d_inode = dir;
+	dir_dentry->d_sb = dir->i_sb;
+
+	/* Create dentry for the filename */
+	dentry = kzalloc(sizeof(struct dentry), GFP_KERNEL);
+	if (!dentry) {
+		kfree(dir_dentry);
+		return -ENOMEM;
+	}
+
+	/* Initialize dentry (kzalloc already zeros memory) */
+	dentry->d_name.name = filename;
+	dentry->d_name.len = strlen(filename);
+	dentry->d_sb = dir->i_sb;
+	dentry->d_parent = dir_dentry;
+
+	/* Lookup file */
+	result = ext4_lookup(dir, dentry, 0);
+
+	if (IS_ERR(result)) {
+		ret = PTR_ERR(result);
+		goto out_dentry;
+	}
+
+	if (result && result->d_inode) {
+		/* File exists - use the existing inode for overwrite */
+		inode = result->d_inode;
+		if (result != dentry) {
+			/* Use the result dentry instead */
+			kfree(dentry);
+			dentry = result;
+		}
+	} else {
+		/* ext4_lookup returned NULL or a dentry with NULL inode */
+		if (result && result != dentry) {
+			/* Free the result dentry since it doesn't have an inode */
+			kfree(result);
+		}
+		/* Keep using our original dentry */
+		dentry->d_inode = NULL;
+
+		/* File does not exist, create it */
+		/* Mode: 0644 (rw-r--r--) | S_IFREG */
+		mode = S_IFREG | 0644;
+		ret = ext4_create(&nop_mnt_idmap, dir, dentry, mode, true);
+		if (ret)
+			goto out_dentry;
+
+		inode = dentry->d_inode;
+	}
+	if (!inode) {
+		ret = -EIO;
+		goto out_dentry;
+	}
+
+	/*
+	 * Attach jinode for journaling if needed (like ext4_file_open does).
+	 * This is required for ordered data mode.
+	 */
+	ret = ext4_inode_attach_jinode(inode);
+	if (ret < 0)
+		goto out_dentry;
+
+	/*
+	 * Start a journal handle for the write operation.
+	 * U-Boot uses a synchronous single-transaction model where
+	 * ext4_journal_stop() commits immediately for crash safety.
+	 */
+	handle = ext4_journal_start(inode, EXT4_HT_WRITE_PAGE,
+				    EXT4_DATA_TRANS_BLOCKS(inode->i_sb));
+	if (IS_ERR(handle)) {
+		ret = PTR_ERR(handle);
+		handle = NULL;
+		goto out_dentry;
+	}
+
+	/* Write data to file */
+	pos = offset;
+	end = offset + len;
+	while (pos < end) {
+		ext4_lblk_t block = pos >> inode->i_blkbits;
+		uint block_offset = pos & (inode->i_sb->s_blocksize - 1);
+		uint bytes_to_write = inode->i_sb->s_blocksize - block_offset;
+		int needed_credits = EXT4_DATA_TRANS_BLOCKS(inode->i_sb);
+
+		if (pos + bytes_to_write > end)
+			bytes_to_write = end - pos;
+
+		/*
+		 * Ensure we have enough journal credits for this block.
+		 * Each block allocation can use up to EXT4_DATA_TRANS_BLOCKS
+		 * credits. Try to extend, or restart the transaction if needed.
+		 */
+		ret = ext4_journal_ensure_credits(handle, needed_credits, 0);
+		if (ret < 0)
+			goto out_handle;
+
+		bh = ext4_getblk(handle, inode, block, 0);
+
+		if (IS_ERR(bh)) {
+			ret = PTR_ERR(bh);
+			goto out_handle;
+		}
+		if (!bh) {
+			/* Block doesn't exist, allocate it */
+			bh = ext4_getblk(handle, inode, block,
+					 EXT4_GET_BLOCKS_CREATE);
+			if (IS_ERR(bh)) {
+				ret = PTR_ERR(bh);
+				goto out_handle;
+			}
+			if (!bh) {
+				ret = -EIO;
+				goto out_handle;
+			}
+		}
+
+		/* Get write access for journaling */
+		ret = ext4_journal_get_write_access(handle, inode->i_sb, bh,
+						    EXT4_JTR_NONE);
+		if (ret) {
+			brelse(bh);
+			goto out_handle;
+		}
+
+		/* Copy data to buffer */
+		memcpy(bh->b_data + block_offset, buf + (pos - offset),
+		       bytes_to_write);
+
+		/*
+		 * In data=journal mode, file data goes through the journal.
+		 * In data=ordered mode, write directly to disk.
+		 */
+		if (ext4_should_journal_data(inode)) {
+			/* data=journal: write through journal */
+			ret = ext4_handle_dirty_metadata(handle, inode, bh);
+			if (ret) {
+				brelse(bh);
+				goto out_handle;
+			}
+		} else {
+			/* data=ordered: write directly to disk */
+			mark_buffer_dirty(bh);
+			ret = sync_dirty_buffer(bh);
+			if (ret) {
+				brelse(bh);
+				goto out_handle;
+			}
+		}
+
+		brelse(bh);
+		pos += bytes_to_write;
+	}
+
+	/* Update inode size */
+	if (end > inode->i_size) {
+		i_size_write(inode, end);
+		/*
+		 * Also update i_disksize in ext4_inode_info - this is what gets
+		 * written to disk via ext4_fill_raw_inode -> ext4_isize_set
+		 */
+		EXT4_I(inode)->i_disksize = end;
+		/* Mark inode dirty to update on disk */
+		ext4_mark_inode_dirty(handle, inode);
+	}
+
+	*actwrite = len;
+	ret = 0;
+
+out_handle:
+	/* Stop handle - this commits the transaction synchronously in U-Boot */
+	if (handle) {
+		int stop_ret = ext4_journal_stop(handle);
+
+		if (stop_ret) {
+			if (!ret)
+				ret = stop_ret;
+		}
+	}
+
+out_dentry:
+	/*
+	 * Free our manually allocated dentries. In U-Boot's minimal dcache,
+	 * these won't be cached elsewhere.
+	 */
+	kfree(dir_dentry);
+	kfree(dentry);
+	return ret;
+}
+
+int ext4l_write(const char *filename, void *buf, loff_t offset, loff_t len,
+		loff_t *actwrite)
+{
+	struct inode *parent_inode = NULL;
+	char *parent_path = NULL;
+	const char *basename;
+	char *path_copy;
+	char *last_slash;
+	int ret;
+
+	if (!ext4l_sb)
+		return -ENODEV;
+
+	if (!filename || !buf || !actwrite)
+		return -EINVAL;
+
+	/* Check if filesystem is mounted read-write */
+	if (ext4l_sb->s_flags & SB_RDONLY)
+		return -EROFS;
+
+	/* Parse filename to get parent directory and basename */
+	path_copy = strdup(filename);
+	if (!path_copy)
+		return -ENOMEM;
+
+	last_slash = strrchr(path_copy, '/');
+
+	if (last_slash) {
+		*last_slash = '\0';
+		parent_path = path_copy;
+		basename = last_slash + 1;
+		if (*parent_path == '\0') /* Root directory */
+			parent_path = "/";
+	} else {
+		parent_path = "/";
+		basename = filename;
+	}
+
+	/* Resolve parent directory inode */
+	ret = ext4l_resolve_path(parent_path, &parent_inode);
+	if (ret) {
+		free(path_copy);
+		return ret;
+	}
+
+	if (!S_ISDIR(parent_inode->i_mode)) {
+		free(path_copy);
+		return -ENOTDIR;
+	}
+
+	/* Call write implementation */
+	ret = ext4l_write_file(parent_inode, basename, buf, offset, len,
+			       actwrite);
+
+	/* Sync all dirty buffers - U-Boot has no journal thread */
+	if (!ret) {
+		int sync_ret = bh_cache_sync();
+
+		if (sync_ret)
+			ret = sync_ret;
+	}
+
+	free(path_copy);
+	return ret;
+}
+
 void ext4l_close(void)
 {
 	ext4l_close_internal(false);
diff --git a/fs/fs_legacy.c b/fs/fs_legacy.c
index 155092519dd..66545834928 100644
--- a/fs/fs_legacy.c
+++ b/fs/fs_legacy.c
@@ -283,7 +283,7 @@  static struct fstype_info fstypes[] = {
 		.exists = ext4l_exists,
 		.size = ext4l_size,
 		.read = ext4l_read,
-		.write = fs_write_unsupported,
+		.write = ext4l_op_ptr(ext4l_write, fs_write_unsupported),
 		.uuid = ext4l_uuid,
 		.opendir = ext4l_opendir,
 		.readdir = ext4l_readdir,
diff --git a/include/ext4l.h b/include/ext4l.h
index 9cfe4867ffa..5dfb671690c 100644
--- a/include/ext4l.h
+++ b/include/ext4l.h
@@ -15,6 +15,13 @@  struct fs_dir_stream;
 struct fs_dirent;
 struct fs_statfs;
 
+/* Select op when EXT4_WRITE is enabled, fallback otherwise */
+#if CONFIG_IS_ENABLED(EXT4_WRITE)
+#define ext4l_op_ptr(op, fallback)	op
+#else
+#define ext4l_op_ptr(op, fallback)	fallback
+#endif
+
 /**
  * ext4l_probe() - Probe a block device for an ext4 filesystem
  *
@@ -69,6 +76,22 @@  int ext4l_size(const char *filename, loff_t *sizep);
 int ext4l_read(const char *filename, void *buf, loff_t offset, loff_t len,
 	       loff_t *actread);
 
+/**
+ * ext4l_write() - Write data to a file
+ *
+ * Creates the file if it doesn't exist. Overwrites existing content.
+ *
+ * @filename: Path to file
+ * @buf: Buffer containing data to write
+ * @offset: Byte offset to start writing at
+ * @len: Number of bytes to write
+ * @actwrite: Returns actual bytes written
+ * Return: 0 on success, -EROFS if read-only, -ENODEV if not mounted,
+ *	   -ENOTDIR if parent is not a directory, negative on other errors
+ */
+int ext4l_write(const char *filename, void *buf, loff_t offset, loff_t len,
+		loff_t *actwrite);
+
 /**
  * ext4l_get_uuid() - Get the filesystem UUID
  *
diff --git a/test/fs/ext4l.c b/test/fs/ext4l.c
index 43801f252f7..82587a87894 100644
--- a/test/fs/ext4l.c
+++ b/test/fs/ext4l.c
@@ -396,3 +396,45 @@  static int fs_test_ext4l_statfs_norun(struct unit_test_state *uts)
 }
 FS_TEST_ARGS(fs_test_ext4l_statfs_norun, UTF_SCAN_FDT | UTF_CONSOLE | UTF_MANUAL,
 	     { "fs_image", UT_ARG_STR });
+
+/**
+ * fs_test_ext4l_write_norun() - Test ext4l_write function
+ *
+ * Verifies that ext4l can write file contents to the filesystem.
+ *
+ * Arguments:
+ *   fs_image: Path to the ext4 filesystem image
+ */
+static int fs_test_ext4l_write_norun(struct unit_test_state *uts)
+{
+	const char *fs_image = ut_str(EXT4L_ARG_IMAGE);
+	const char *test_data = "test write data\n";
+	size_t test_len = strlen(test_data);
+	loff_t actwrite, actread;
+	char read_buf[32];
+	loff_t size;
+
+	ut_assertnonnull(fs_image);
+	ut_assertok(run_commandf("host bind 0 %s", fs_image));
+	ut_assertok(fs_set_blk_dev("host", "0", FS_TYPE_ANY));
+
+	/* Write a new file */
+	ut_assertok(ext4l_write("/newfile.txt", (void *)test_data, 0,
+				test_len, &actwrite));
+	ut_asserteq(test_len, actwrite);
+
+	/* Verify the file exists and has correct size */
+	ut_asserteq(1, ext4l_exists("/newfile.txt"));
+	ut_assertok(ext4l_size("/newfile.txt", &size));
+	ut_asserteq(test_len, size);
+
+	/* Read back and verify contents */
+	memset(read_buf, '\0', sizeof(read_buf));
+	ut_assertok(ext4l_read("/newfile.txt", read_buf, 0, 0, &actread));
+	ut_asserteq(test_len, actread);
+	ut_asserteq_str(test_data, read_buf);
+
+	return 0;
+}
+FS_TEST_ARGS(fs_test_ext4l_write_norun, UTF_SCAN_FDT | UTF_CONSOLE | UTF_MANUAL,
+	     { "fs_image", UT_ARG_STR });
diff --git a/test/py/tests/test_fs/test_ext4l.py b/test/py/tests/test_fs/test_ext4l.py
index 25d09dca889..e953be379eb 100644
--- a/test/py/tests/test_fs/test_ext4l.py
+++ b/test/py/tests/test_fs/test_ext4l.py
@@ -118,3 +118,8 @@  class TestExt4l:
         """Test that fsinfo command displays filesystem statistics."""
         with ubman.log.section('Test ext4l fsinfo'):
             ubman.run_ut('fs', 'fs_test_ext4l_fsinfo', fs_image=ext4_image)
+
+    def test_write(self, ubman, ext4_image):
+        """Test that ext4l can write file contents."""
+        with ubman.log.section('Test ext4l write'):
+            ubman.run_ut('fs', 'fs_test_ext4l_write', fs_image=ext4_image)