From patchwork Wed Dec 31 22:29:52 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Simon Glass X-Patchwork-Id: 1158 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=1767220312; bh=xKBfBeZBaZ4JgjtrO63EK7c/HsZU+x0FSs5yg8suDfE=; 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=W5sN587E4XG4DpbwK62UCFBI62ODIPUNIIXhACcKS4NzXpqmNZriEc1b7P+xfIgJ6 t5AHSCslb8L9fGc+/8dE5QudPzS2Ti0rrj7grUarTh9VtQ/7aaiGxsE2+Urgcf/QPu VqIvkqXWopLgIpcoIxBkRwuEtxMMBVvRtPhZS5agpCFfb0Nc1dG63zRgkDoWgrIyae g09nV5yiMC8n+h+mijwx76QgtlsssV1CbgCM2tP3rtJ53Trphna8IjzX8Kmh1YcorG in7oiCKrJv5LU02OwIXngNpOvquOf/0qttk0798Ubniit/AT9qzvv/UL3eW8m296h0 /HcbPsv3caBHg== Received: from localhost (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id E042868F51 for ; Wed, 31 Dec 2025 15:31:52 -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 4hJAlfIV9W4S for ; Wed, 31 Dec 2025 15:31:52 -0700 (MST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1767220310; bh=xKBfBeZBaZ4JgjtrO63EK7c/HsZU+x0FSs5yg8suDfE=; 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=WOtye7ztVjkDAwXWjVMkSNnkjP2gIkPveopHymHq4LQxRBJ27A0TbNUk70HwIi13f q3TLUoYignNADJeHkcBf+cOUQd4hhINZSy/NHzOkEBiUtIDeWfU2gQgjs7BZw1HG97 SGEwnytkKM0mnrJQMk3YLitIMweGdMADiJV0YUdwvDe3oRjR+OOJs1yW50WpDcLRgQ ebHl1UPhSiG8YAra2+kvNyujdJmcc3qif6bHNIkrZGiLwlYHvglW4UJv7ZC0f89l+D S+hJ01Ew6duSUglHDhIWnPqvEJh7KK1wjxBygIffZguWI9lyOvgX/KuWPjjugifjPP qHW1r6DPS9OMw== Received: from mail.u-boot.org (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id DD3DA68FCB for ; Wed, 31 Dec 2025 15:31:50 -0700 (MST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1767220307; bh=dQyzmmg/amSVcNt0D0Q8GuHVjo5rFV2fO+By+H47ePA=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=Jtp46EQPdsPZQw4YqoQiLLZKZvpDJU2qalKwBt4/p/0IjmrUytqHymZpbCdCUMdhw MvaoSq9OBcs11jd5uZug619Sq4En/l5pPFftjieqLenM7Kq07g8eAqr1QkKZLAgyNa nxvWbi6ouHIODaIrh9jJvprQVpMwz7FJHVEGOIhSh+kT3pNB0+TXlS8CRXC3HmngDz sc+17P6ADGBkIwYimqPSjLQXu9PgYtOkewC4UvsoRiqYWHiN0XAOxXJe2CZSyIH6/L EwmSAGb2/+pl0L56WZc+AW6Ix3BHsTpqZeaFsMm8WX0+KbzqvBiTIBTqQzLJiYxENK CQVdN+iYiSs6w== Received: from localhost (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id 3995768FCB; Wed, 31 Dec 2025 15:31: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 10026) with ESMTP id Cfu-mxIvFoS3; Wed, 31 Dec 2025 15:31:47 -0700 (MST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1767220301; bh=xtAIB4mBJQcAMsSg2BvcoHc0yJlVcSPNOm3xNb7Q2Y0=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=ZEzP9xuJtRnAITpDdp4v/Vyd7YEYKl0OFbhO2gUnXlab39N43n6Z3Ka7JQDrwlJPF Z4XmQIxMd6/eeT9kSFYv+dmq1/eQhh8c75F8M2JXundkp7iTD7WSMR3eSe8Fhm4rC2 WKaMtMvlAMECnjzJHExiSKhQ/KZW8M5d0IET9wGlMqhpHlXdAnm/PxuyGnLMTMH/5m WkLxeUYx1kA7GEBoduFwS4Skjm47etgjIxSNk01DmnuOFRi4GYj4qQhyusXDn8o4zD kx7ZV8kIFSR10Ec9j/mpoDldZztDWDxZTakG0EX5BK0xqQiRjNnOiDO2ycZrt8rS7Y A5QlnenFls2kw== Received: from u-boot.org (unknown [73.34.74.121]) by mail.u-boot.org (Postfix) with ESMTPSA id 5111F68F65; Wed, 31 Dec 2025 15:31:41 -0700 (MST) From: Simon Glass To: U-Boot Concept Date: Wed, 31 Dec 2025 15:29:52 -0700 Message-ID: <20251231223008.3251711-20-sjg@u-boot.org> X-Mailer: git-send-email 2.43.0 In-Reply-To: <20251231223008.3251711-1-sjg@u-boot.org> References: <20251231223008.3251711-1-sjg@u-boot.org> MIME-Version: 1.0 Message-ID-Hash: VJCOUW4F4RSIHRTSR4FHNESA4NVDF7JM X-Message-ID-Hash: VJCOUW4F4RSIHRTSR4FHNESA4NVDF7JM 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 19/26] 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 --- 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)