From patchwork Fri Jan 2 00:50:53 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Simon Glass X-Patchwork-Id: 1187 Return-Path: X-Original-To: u-boot-concept@u-boot.org Delivered-To: u-boot-concept@u-boot.org DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1767315167; bh=eSqhWmmajrPtHmQQ1JNOrFRKwrN2P9DSEgxE6kE2e/8=; h=From:To:Date:In-Reply-To:References:CC:Subject:List-Id: List-Archive:List-Help:List-Owner:List-Post:List-Subscribe: List-Unsubscribe:From; b=hfWh0SQzHQQSlY6ljaDeq/dQN4ZQzuCAe+H/pnlLdU/Eu3qKtKMveO1sZEoCbJbcB oCWVMRuj7ryJjKNBnxWLzIhOvs6C7FmyCfcydazuH3Kzkyn1/rjJZprnlNN+SdfXPE l0DJ9Pbohzh5yrnFsrqtOAlubmilLo3Mgp2DUmEAUj5C2w+WpMAgqwSyWFC8jsGD2W AemlZtLEa+chO4jDc3X4qjrRKI0cEMwbnCzis4QIJ/1ZjTlysZkoTCGYdJdX5VQVXd clgW+o179IS6SQRAFSNCLtyXn5LXc2nPUQcq/fmLrxDuW44wfItyZpjZiU1uTK8kyF iHwx4MQsRcXSw== Received: from localhost (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id 7DB6D69004 for ; Thu, 1 Jan 2026 17:52:47 -0700 (MST) X-Virus-Scanned: Debian amavis at Received: from mail.u-boot.org ([127.0.0.1]) by localhost (mail.u-boot.org [127.0.0.1]) (amavis, port 10024) with ESMTP id p5PABwN6UtAR for ; Thu, 1 Jan 2026 17:52:47 -0700 (MST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1767315167; bh=eSqhWmmajrPtHmQQ1JNOrFRKwrN2P9DSEgxE6kE2e/8=; h=From:To:Date:In-Reply-To:References:CC:Subject:List-Id: List-Archive:List-Help:List-Owner:List-Post:List-Subscribe: List-Unsubscribe:From; b=hfWh0SQzHQQSlY6ljaDeq/dQN4ZQzuCAe+H/pnlLdU/Eu3qKtKMveO1sZEoCbJbcB oCWVMRuj7ryJjKNBnxWLzIhOvs6C7FmyCfcydazuH3Kzkyn1/rjJZprnlNN+SdfXPE l0DJ9Pbohzh5yrnFsrqtOAlubmilLo3Mgp2DUmEAUj5C2w+WpMAgqwSyWFC8jsGD2W AemlZtLEa+chO4jDc3X4qjrRKI0cEMwbnCzis4QIJ/1ZjTlysZkoTCGYdJdX5VQVXd clgW+o179IS6SQRAFSNCLtyXn5LXc2nPUQcq/fmLrxDuW44wfItyZpjZiU1uTK8kyF iHwx4MQsRcXSw== Received: from mail.u-boot.org (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id 6D51568313 for ; Thu, 1 Jan 2026 17:52:47 -0700 (MST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1767315165; bh=vdkBGgtGqCFuizgtM4akzqx9FA1lKxJdLujFQxI7gpg=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=aliSMxGmOCKNHfDLjPFMLus0e9xWGIf9Eorz39EsqltTW1UfE11fGmKyY+H3YsuHo Jq5DSTqi1awvNEKVjghJJOH4eBb/H5UYfNYsjiaVgrr9BsxMBWVksm8wvF7tIMKOcB jclUNjD8wLAbyhS3YDRMuVSOGoH7uOVDKRHVJ0WtdW8hiBIFBX/fUHlj9AWh3NpXrC FkEQoKgep3u6IUs43j939XK+rJ5ZaEvHYgcAqA3p0cl3gbpDp+eZtxO4MHhMH8saTu 7YlDOzeNR3KFJ0DAAqHC85E2hwszGI3DNgkVR5WRgaI3d129eygfJoSUx33t/0O9bg mfsOGDynB/G2g== Received: from localhost (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id 7915168313; Thu, 1 Jan 2026 17:52:45 -0700 (MST) X-Virus-Scanned: Debian amavis at Received: from mail.u-boot.org ([127.0.0.1]) by localhost (mail.u-boot.org [127.0.0.1]) (amavis, port 10026) with ESMTP id 0Jtaf69j3k_U; Thu, 1 Jan 2026 17:52:45 -0700 (MST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1767315161; bh=V38bDF2EvJRDwVtCS3fxSOBWCkpGmr5tD+1xylWx5uI=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=k0BfdkQpz6iQ+irDAgbaUjqhQYk3+dscWQMF/V6eM/YpJBLz8bJUmWRmFeWAITXz6 BNFQHyzgc28phP9OcJ+vugLwUpKsH0yjzqJEdoXUAFBdmNOgBSThYHTMoBg0VkC9mP 8kcD2F6giDHY8zJd9SiwTpY+yUCtDmjeN8Wb5hWHXprG5xBmqAU6pcqK77QFRWmfeb RadeEeUzWRQ77cPajWG/sUodkIP2zgpvrGat7BiSdx2VrOXPwfXMOr9kdzeCdsIed2 1V7cdhUU64F01epKYl1lOLRKQg9CkeKQvdJRYnSE/ztEGLpEgX/yi9W54r0KzqLkzl Syz5eTl2zY3Sg== Received: from u-boot.org (unknown [73.34.74.121]) by mail.u-boot.org (Postfix) with ESMTPSA id D313569007; Thu, 1 Jan 2026 17:52:40 -0700 (MST) From: Simon Glass To: U-Boot Concept Date: Thu, 1 Jan 2026 17:50:53 -0700 Message-ID: <20260102005112.552256-24-sjg@u-boot.org> X-Mailer: git-send-email 2.43.0 In-Reply-To: <20260102005112.552256-1-sjg@u-boot.org> References: <20260102005112.552256-1-sjg@u-boot.org> MIME-Version: 1.0 Message-ID-Hash: 53UM4LF252UWZ7PLIUMMBMUVCZVCHTHE X-Message-ID-Hash: 53UM4LF252UWZ7PLIUMMBMUVCZVCHTHE X-MailFrom: sjg@u-boot.org X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header CC: Heinrich Schuchardt , Simon Glass , Claude X-Mailman-Version: 3.3.10 Precedence: list Subject: [Concept] [PATCH v2 23/30] ext4l: Add write support List-Id: Discussion and patches related to U-Boot Concept Archived-At: List-Archive: List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: From: Simon Glass 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 Signed-off-by: Simon Glass --- (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(-) 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)