[PATCH 00/26] ext4l: Add write support (part L)
From: Simon Glass <simon.glass@canonical.com> This series adds write support to the new ext4l filesystem driver, supporting file creation, modification, and deletion. The main additions are: - File write with journalling support - File deletion (unlink) - Directory creation (mkdir) - Symbolic link creation (ln) - File and directory rename Infrastructure improvements include proper cleanup of journal references when unmounting, safeguards to handle probe without explicit close, and prevention of use-after-free issues with buffer_heads that have active journal_heads. The series also enables ext4l for sandbox testing and allows the use of metadata_csum checksums which the existing ext4 driver does not support. Some other tweaks are included: - a fix for mcheck to avoid filling up pre-relocation malloc() - use the correct logo for readthedocs - add a flag to avoid using the video console in tests - a fallback option for finding persistent data-dir At this point ext4l is useable, but there is more work needed, including: - Add MAINTAINERS entry - Add more Kconfig options for various features (to reduce code size) - Enable for ARM and x86 targets - Documentation - Tests for failure cases (to check that the journal is doing its job) Simon Glass (26): sandbox: os: Check executable directory for persistent files sandbox: Add --quiet_vidconsole option to speed up output doc: Correct the U-Boot logo blkmap: Handle read-only slices in write path mcheck: Add Kconfig option for caller string length mcheck: Skip mcheck overhead for pre-relocation allocations Revert "lib: Add CONFIG_LIB_KMEM_CACHE for full kmem_cache support" ext4l: Move message buffer functions to support.c ext4l: Add inode-tracking lists ext4l: Fix a few problems with handling bh_cache ext4l: Clean up fully when unmounting ext4l: Remove duplicate atomic_add declarations ext4l: Use the real crc16 implementation ext4l: Add a few Kconfig dependencies ext4l: Add bh_cache_release_jbd() to clean up journal references ext4l: Add safeguard to close previous mount in probe ext4l: Prevent freeing buffer_heads with active journal_heads ext4l: Add support for read-only devices ext4l: Add write support ext4l: Add unlink support for file deletion ext4l: Add mkdir support for directory creation ext4l: Add symlink support ext4l: Update symlink to replace existing files ext4l: Add rename support test: fs_helper: Keep metadata_csum enabled for ext4l sandbox: Enable CONFIG_FS_EXT4L Kconfig | 11 + arch/sandbox/cpu/os.c | 27 +- arch/sandbox/cpu/start.c | 10 + arch/sandbox/include/asm/state.h | 1 + board/sandbox/sandbox.c | 16 + common/dlmalloc.c | 33 +- common/mcheck_core.inc.h | 5 +- configs/sandbox_defconfig | 2 + doc/arch/sandbox/sandbox.rst | 7 + doc/conf.py | 2 +- drivers/block/blkmap.c | 3 + fs/ext4l/Kconfig | 3 + fs/ext4l/ext4.h | 6 + fs/ext4l/ext4_uboot.h | 32 +- fs/ext4l/interface.c | 679 ++++++++++++++++++++++++++-- fs/ext4l/stub.c | 8 +- fs/ext4l/super.c | 25 +- fs/ext4l/support.c | 130 +++++- fs/fs_legacy.c | 10 +- include/ext4l.h | 67 +++ include/linux/fs.h | 1 + include/linux/slab.h | 27 +- lib/Kconfig | 8 - lib/Makefile | 1 - lib/kmem_cache.c | 20 - test/fs/ext4l.c | 251 ++++++++++ test/py/tests/fs_helper.py | 13 +- test/py/tests/test_event_dump.py | 1 + test/py/tests/test_fs/test_ext4l.py | 25 + tools/logos/u-boot-logo-text.svg | Bin 0 -> 8552 bytes 30 files changed, 1287 insertions(+), 137 deletions(-) delete mode 100644 lib/kmem_cache.c create mode 100644 tools/logos/u-boot-logo-text.svg -- 2.43.0 base-commit: 87b7abb0093eb536cd89fa6dd1b2f5f1049bf708 branch: extl
From: Simon Glass <simon.glass@canonical.com> When os_persistent_file() is called without a directory set in the environment variable and the file isn't found in the current directory, also check in the executable's directory. This allows tests like dm_test_host to work when run directly from the build directory rather than through the pytest framework, avoiding the need to set U_BOOT_PERSISTENT_DATA_DIR manually. Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- arch/sandbox/cpu/os.c | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/arch/sandbox/cpu/os.c b/arch/sandbox/cpu/os.c index 891dfd9c9b8..5278ce55766 100644 --- a/arch/sandbox/cpu/os.c +++ b/arch/sandbox/cpu/os.c @@ -5,6 +5,7 @@ #define _GNU_SOURCE +#include <assert.h> #include <dirent.h> #include <errno.h> #include <fcntl.h> @@ -338,10 +339,30 @@ int os_persistent_file(char *buf, int maxsize, const char *fname) } strcpy(ptr, fname); - if (access(buf, F_OK) == -1) - return -ENOENT; + if (access(buf, F_OK) == 0) + return 0; - return 0; + /* + * If no directory was specified and the file wasn't found, try the + * executable's directory with "persistent-data" appended. + */ + if (!dirname) { + struct sandbox_state *state = state_get_current(); + const char *prog; + char *slash; + + prog = state->prog_fname ? state->prog_fname : state->argv[0]; + assert(prog); + slash = strrchr(prog, '/'); + if (slash) { + snprintf(buf, maxsize, "%.*s/persistent-data/%s", + (int)(slash - prog), prog, fname); + if (access(buf, F_OK) == 0) + return 0; + } + } + + return -ENOENT; } int os_mktemp(char *fname, off_t size) -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> When running sandbox with lots of console output, the vidconsole slows things down significantly since it renders truetype fonts to the internal framebuffer. Add a -Q/--quiet_vidconsole command-line option that removes vidconsole from stdout and stderr, using only the serial console. This can provide a ~300x speedup for output-heavy operations. Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- arch/sandbox/cpu/start.c | 10 ++++++++++ arch/sandbox/include/asm/state.h | 1 + board/sandbox/sandbox.c | 16 ++++++++++++++++ doc/arch/sandbox/sandbox.rst | 7 +++++++ test/py/tests/test_event_dump.py | 1 + 5 files changed, 35 insertions(+) diff --git a/arch/sandbox/cpu/start.c b/arch/sandbox/cpu/start.c index 30d4f83b6ee..1a5ca97e15e 100644 --- a/arch/sandbox/cpu/start.c +++ b/arch/sandbox/cpu/start.c @@ -476,6 +476,16 @@ static int sandbox_cmdline_cb_no_term_present(struct sandbox_state *state, SANDBOX_CMDLINE_OPT_SHORT(no_term_present, 'A', 0, "Assume no terminal present (for pager testing)"); +static int sandbox_cmdline_cb_quiet_vidconsole(struct sandbox_state *state, + const char *arg) +{ + state->quiet_vidconsole = true; + + return 0; +} +SANDBOX_CMDLINE_OPT_SHORT(quiet_vidconsole, 'Q', 0, + "Don't use vidconsole for stdout/stderr"); + static int sandbox_cmdline_cb_upl(struct sandbox_state *state, const char *arg) { state->upl = true; diff --git a/arch/sandbox/include/asm/state.h b/arch/sandbox/include/asm/state.h index 6a89f0ca5ef..ff7493ec5d6 100644 --- a/arch/sandbox/include/asm/state.h +++ b/arch/sandbox/include/asm/state.h @@ -177,6 +177,7 @@ struct sandbox_state { bool soft_fail; /* Continue on failure */ bool pager_bypass; /* Enable pager-bypass mode */ bool no_term_present; /* Assume no terminal present */ + bool quiet_vidconsole; /* Don't use vidconsole for stdout */ int video_test; /* ms to wait before next assert */ const char *video_frames_dir; /* Directory to write video frames */ int video_frame_count; /* Number of frames written */ diff --git a/board/sandbox/sandbox.c b/board/sandbox/sandbox.c index d97945e58fc..78b2b7b1f47 100644 --- a/board/sandbox/sandbox.c +++ b/board/sandbox/sandbox.c @@ -10,7 +10,9 @@ #include <dm.h> #include <efi.h> #include <efi_loader.h> +#include <env.h> #include <env_internal.h> +#include <event.h> #include <extension_board.h> #include <init.h> #include <led.h> @@ -19,6 +21,7 @@ #include <os.h> #include <acpi/acpi_table.h> #include <asm/global_data.h> +#include <asm/state.h> #include <asm/test.h> #include <asm/u-boot-sandbox.h> #include <linux/kernel.h> @@ -185,3 +188,16 @@ void fwu_plat_get_bootidx(uint *boot_idx) *boot_idx = 0; } #endif + +static int sandbox_settings(void) +{ + struct sandbox_state *state = state_get_current(); + + if (state->quiet_vidconsole) { + env_set("stdout", "serial"); + env_set("stderr", "serial"); + } + + return 0; +} +EVENT_SPY_SIMPLE(EVT_SETTINGS_R, sandbox_settings); diff --git a/doc/arch/sandbox/sandbox.rst b/doc/arch/sandbox/sandbox.rst index b2f4d8913d2..90c3dfa837e 100644 --- a/doc/arch/sandbox/sandbox.rst +++ b/doc/arch/sandbox/sandbox.rst @@ -208,6 +208,13 @@ available options. Some of these are described below: -P, --pager_bypass Enable pager bypass mode for testing. +-Q, --quiet_vidconsole + Don't use vidconsole for stdout/stderr. By default, sandbox outputs to both + serial and vidconsole. This can be slow when there is a lot of output, due to + truetype font rendering to the internal framebuffer. Use this option to use + only serial output, which can provide a significant speedup for output-heavy + operations. + -r, --read Read driver state from a dtb file. In conjunction with `-w`, this allows sandbox to save and restore emulated hardware state (such as a TPM) across diff --git a/test/py/tests/test_event_dump.py b/test/py/tests/test_event_dump.py index 3ddd60198fe..ff0da82196b 100644 --- a/test/py/tests/test_event_dump.py +++ b/test/py/tests/test_event_dump.py @@ -22,5 +22,6 @@ EVT_LAST_STAGE_INIT efi_block_device_create .*lib/efi_driver/efi_block EVT_LAST_STAGE_INIT install_smbios_table .*lib/efi_loader/efi_smbios.c:.* EVT_LAST_STAGE_INIT last_stage_init .*arch/sandbox/cpu/start.c:.* EVT_MISC_INIT_F sandbox_early_getopt_check .*arch/sandbox/cpu/start.c:.* +EVT_SETTINGS_R sandbox_settings .*board/sandbox/sandbox.c:.* EVT_TEST h_adder_simple .*test/common/event.c:''' assert re.match(expect, out, re.MULTILINE) is not None -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> Update the ReadTheDocs documentation to use the correct U-Boot logo from the original 2023 website, stickers and coins. This includes the project name. Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- doc/conf.py | 2 +- tools/logos/u-boot-logo-text.svg | Bin 0 -> 8552 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 tools/logos/u-boot-logo-text.svg diff --git a/doc/conf.py b/doc/conf.py index 348f0132ffc..498893ad669 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -257,7 +257,7 @@ except ImportError: # The name of an image file (relative to this directory) to place at the top # of the sidebar. -html_logo = '../tools/logos/u-boot_logo.svg' +html_logo = '../tools/logos/u-boot-logo-text.svg' # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 diff --git a/tools/logos/u-boot-logo-text.svg b/tools/logos/u-boot-logo-text.svg new file mode 100644 index 0000000000000000000000000000000000000000..55b7f5c6e2a7e14067c28136238977d81a4a8810 GIT binary patch literal 8552 zcmd5?YjfMS68)ZEfhyBZJ24+1DN3XzwN5;p+nMxEr*5aWA1M-&SW^Tq2w75o{oMr# zKJ;*u*w=|Xj>X06?C!zt63FDo-8v%=Oq40lrvqx*1H$r{CuzQ%4*vY(XJb4drHt|< z%6QJEgPae3oV}WSV;JP_x7+_1@6(v&B{P0WST55=%0x)sy))kaZQR`$o@o=qn7yj{ zzvIPTq|22g-^V`?$EM>e@-LR=B8^w%E?#ZoRU{HgCcAwhcy2DU;{K=QdS@nVR!>tt zEME~qwrL_)fOhN;@Nva(*7o@!W!txWHyzl-CeR6gRmU`$4qysLJ#>_&Rcn4<#!<n- zPK!srry(`HH;(Oi)TTalhx82_#qNU5scrwDMwVc&0)E4U$9c2{Gi{7H=hDddk{gok zqzTLR7h$`C3u!FUjGZ20(7-W^{PeJ0Ql!&C5SVu0k46Xh>#6rA`hd6dGE8etr-PN0 zMQB;u?bg&nlZ&N=kjf&8S!rpxf?ti-`ZnXWz%2Pj#B2dmFf(V;y8YwU(v5~~CNgQK zX-4DfTUV{?*tTtHTh)s_024bn&-WC`;#5x>1c7Bcmg5-E!zlN;jCMv|zHXs}aomiI z1&d_*z+%2$^SrFg(d(SFor{ggRF5RKSjN_{2}+Ba)EdlynG>1Hj3Gq{LGGzX*B;FN zG~NO|$a|p2#8Oh#ea&Q)L^5heXo*YJgfJzj5dMDqGxo>NB#y(sxVWz$L4qBkIp4sB zsK=;&NgRS(){&g0>uAZ)$Zx>F6RT}#=9qv??u!~Z4zH#atnzPTtFHP<yiReDbtls- z`vn&n5KH3(q&O+sqpy7<ZC3}CvMQ`JvU31VIjbI9TJyb8MivAi>j;P%$3=^-vgFMY z9M~0hVV{as-(~XpI9z?Vl-J6zjcb(&;h%{G${nntdL98na{(9F?AK^d99qgN6*5(4 zC9iFxW<&;P*vN=FD!1nAkfPp7ms(CwTN$We0=!(d6Q3c*so7!2{mvk=C!1J8w(0u> zKtFNr+q9G-oo`OgE!clIDbV+Gf&ZIJVB`_xr%rn#&qIEyIhaTvR3Jlgl+O<R4w5yy z$<q6-RTLR(@>Rr{Se<}1v2;Ac#!12!wL>sC;*%59I43%3gqVpE%;=Tt3J5DP&$yTl zUN6)~MR7vrTqI2BCSQGeO&ro01SlvIYB8Vx!(u6U#zd6IkhG~?w=4w|7^mc$G-0P2 zbdC%WUbB#%uGd<wqJ(d!gQ^IDyr8R&ZJI-ap=+B_aIQyF?ohw!0`_YR<&0`I$`#+D zD$~It%1TyCn*~2`zJ?mf93D2rP&x(PkrYf91c%ZZHH;WJM-$bJFwpVOHLjywx=ueZ zKzU@Zcq0S^WFw3A3>>chXzw*@pq6O%vxZfaS+V$@i8+S~^Ta|4EAKUR5{f^XhpOJ} zcqxnKESnDABO-EyWhwZkSn~wPq`+!x!J0MrdRN4$h;?N^pupZ|Fa&DBEPQR-p6C15 z_+Zpj3h6b(Hh#~-9E$Mzd&QJc8?CA^ib$@;uC_HH%9U;n10K<b?IYL}o^+C);9wmf z8}r6I&taQzVSqOvplk#nZ5Qo9Uo7HyJpK&mV4q(JrT*2(CpVDit{g1gp?D{Oe>L=b z671&s<N^AVhy+)G>Ip9z__ck+Zyy@PaTB0=9(8>Ke<=LA#_+3B-bdxBeMPnM)K=Q} zM4-K=w12wtU<4g+AsVwRON+9p`Ff5`k{}okKf|%-><&xo*j14fyxKSmS?bXyE<uR_ z;YG(Ic!K63v^KnbWHUV-xI63vtzRXvphKK~3mbM5WRMp_W$5Tr8_;QyJPu62cGlbc z`Miv?dTx^-nwNA}r;PSJ-D$%IICkuJJdLjPTO0l#q56J{qRzP;iO#V>2OH+N*2c>B zFUG>57eKJmUc;#FwouUB<CYD`!<X6}3fJSvsB;$DV<G3!TmnfFKOf74I@<RP2exVK z2!;zf=#IincR#-r*u#C@hfC?Dct3CNTwqzX(;eED5qkTyNcHw<gEpA9gB<OCVd{A< z9<)wJD0W-+R&l&9!GtsNI(r{;j*EgOygx>Vr)M3nn+_378?11*2bljfixF&x)CiS2 zmqLBiyq8p)9zcaMLW9B3^ii-aEfiQx^)F{vqhrcTz;^Z<Sa$rfSew@{T_ttEe~mmi z3|`GEb+yZmdHaQ_@X2_;FdGi5=HXY*hfhZQ#TjuB&FDvM{0JxT?nj44IBgHroeI2) z!cim>5Bf-^Jptfw+M#N4c+-c&`s{jvl7~p7Q7(J3t-AE;iQtTY$#~V5!O=?S4o%ng zhQ;nWOLJya**Ubot!7_D>onVk@1iW7i}c#iul|NwyDV4?_j)lLCGx!KlrbrpFi;)1 zDGY6xdJ6||JwbJxQ617<{OahKf#=u=wL0-I5HZ}!g>bMdyRv_Hnul-}&4iXVhb2ui zg7EHnrst2lH7KS>A5ytzkJ=Ij-&4_7u4g-g*+hb3x%L})N8j%WI244V9DvlUQQz#~ z02thomynaI6LNC>0|qFkxp28RKJXbeouSrOm0e+DFny~87fJ|b27f`J`n>7u;2|BE zBkw??r?3#UGJv+rJcL_lq{1b<tVZSWiVGl6l4MM=Gpx^(FQzJ73y;Pw>hnBQh3j75 z8~K4gzOAS-GROAFr>(H1st1>XpGH-n<nbzJ*69)3Yq!b?s#2(?U6&qs_L!@U#el0h zT(OEWf(yu+uAj8$&MV^DW-x+th&Cuc3*a^wDEs3M1MAnfxDnhuG<r1KZelIre@fs* I>+IG40Fz{5@&Et; literal 0 HcmV?d00001 -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> Some blkmap slices (like blkmap_crypt) don't support writes and have their write function set to NULL. The blkmap_blk_write_slice() function calls the write function without checking if it's NULL, causing a crash when attempting to write to such slices. Add a NULL check before calling the write function. When the slice doesn't support writes, return 0 to indicate no blocks were written. Co-developed-by: Claude <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- drivers/block/blkmap.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/drivers/block/blkmap.c b/drivers/block/blkmap.c index 14aa1e4d088..bd55bdf24b7 100644 --- a/drivers/block/blkmap.c +++ b/drivers/block/blkmap.c @@ -281,6 +281,9 @@ static ulong blkmap_blk_write_slice(struct blkmap *bm, struct blkmap_slice *bms, { lbaint_t nr, cnt; + if (!bms->write) + return 0; + nr = blknr - bms->blknr; cnt = (blkcnt < bms->blkcnt) ? blkcnt : bms->blkcnt; return bms->write(bm, bms, nr, cnt, buffer); -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> The mcheck heap protection stores a caller string in each allocation header for debugging purposes. The length is hard-coded to 48 bytes in mcheck_core.inc.h Add a CONFIG_MCHECK_CALLER_LEN Kconfig option to make this configurable, allowing users to adjust the trade-off between the amount of debugging context and the memory overhead per allocation. Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- Kconfig | 11 +++++++++++ common/mcheck_core.inc.h | 5 ++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/Kconfig b/Kconfig index c28fddf5016..3b89f2c48dd 100644 --- a/Kconfig +++ b/Kconfig @@ -355,6 +355,17 @@ config MCHECK_HEAP_PROTECTION significantly increases memory overhead and should only be used for debugging. +config MCHECK_CALLER_LEN + int "Length of caller string in mcheck header" + depends on MCHECK_HEAP_PROTECTION + default 48 + help + Sets the maximum length of the caller string stored in each + allocation header. This string records the function/file/line + that allocated the memory, useful for debugging memory leaks. + Larger values provide more context but increase memory overhead + per allocation. + config SPL_SYS_MALLOC_F bool "Enable malloc() pool in SPL" depends on SPL_FRAMEWORK && SYS_MALLOC_F && SPL diff --git a/common/mcheck_core.inc.h b/common/mcheck_core.inc.h index 598a5d018ab..fa152790d45 100644 --- a/common/mcheck_core.inc.h +++ b/common/mcheck_core.inc.h @@ -73,7 +73,6 @@ // Full test suite can exceed 10000 concurrent allocations #define REGISTRY_SZ 12000 #define CANARY_DEPTH 2 -#define MCHECK_CALLER_LEN 48 // avoid problems with BSS at early stage: static char mcheck_pedantic_flag __section(".data") = 0; @@ -89,7 +88,7 @@ struct mcheck_hdr { size_t size; /* Exact size requested by user. */ size_t aln_skip; /* Ignored bytes, before the mcheck_hdr, to fulfill alignment */ mcheck_canary canary; /* Magic number to check header integrity. */ - char caller[MCHECK_CALLER_LEN]; /* caller info for debugging */ + char caller[CONFIG_MCHECK_CALLER_LEN]; /* caller info for debugging */ }; static void mcheck_default_abort(enum mcheck_status status, const void *p) @@ -212,7 +211,7 @@ static void *mcheck_allocated_helper(void *altoghether_ptr, size_t customer_sz, for (i = 0; i < CANARY_DEPTH; ++i) hdr->canary.elems[i] = MAGICWORD; if (caller) - strlcpy(hdr->caller, caller, MCHECK_CALLER_LEN); + strlcpy(hdr->caller, caller, CONFIG_MCHECK_CALLER_LEN); else hdr->caller[0] = '\0'; -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> When mcheck heap-protection is enabled, each allocation adds quite a bit of overhead for headers and canaries. While this is needed for the full allocator, it serves no purpose for pre-relocation allocations, since: 1. Simple malloc is a bump allocator that cannot free memory 3. Mcheck's corruption-detection provides no benefit for non-freeable memory Since the pre-relocation heap space is limited (typically <16KB), this overhead can exhaust the heap, causing boot failures. Fix this by bypassing mcheck hooks in dlmalloc(), dlfree(), dlmemalign() and dlcalloc() when called before relocation, directly calling the simple malloc functions instead. Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- common/dlmalloc.c | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/common/dlmalloc.c b/common/dlmalloc.c index 62e1e0a77da..5069a95d9b3 100644 --- a/common/dlmalloc.c +++ b/common/dlmalloc.c @@ -5984,6 +5984,15 @@ static const char *mcheck_caller(void) void *dlmalloc(size_t bytes) { + /* + * Skip mcheck for simple malloc (pre-relocation). Simple malloc is a + * bump allocator that can't free, so mcheck overhead is useless and + * wastes the limited pre-relocation heap space. + */ + if (CONFIG_IS_ENABLED(SYS_MALLOC_F) && + !(gd->flags & GD_FLG_FULL_MALLOC_INIT)) + return malloc_simple(bytes); + mcheck_pedantic_prehook(); size_t fullsz = mcheck_alloc_prehook(bytes); void *p = dlmalloc_impl(fullsz CALLER_NULL); @@ -5993,7 +6002,15 @@ void *dlmalloc(size_t bytes) return mcheck_alloc_posthook(p, bytes, mcheck_caller()); } -void dlfree(void *mem) { dlfree_impl(mcheck_free_prehook(mem)); } +void dlfree(void *mem) +{ + if (CONFIG_IS_ENABLED(SYS_MALLOC_F) && + !(gd->flags & GD_FLG_FULL_MALLOC_INIT)) { + dlfree_impl(mem); + return; + } + dlfree_impl(mcheck_free_prehook(mem)); +} void *dlrealloc(void *oldmem, size_t bytes) { @@ -6020,6 +6037,10 @@ void *dlrealloc(void *oldmem, size_t bytes) void *dlmemalign(size_t alignment, size_t bytes) { + if (CONFIG_IS_ENABLED(SYS_MALLOC_F) && + !(gd->flags & GD_FLG_FULL_MALLOC_INIT)) + return memalign_simple(alignment, bytes); + mcheck_pedantic_prehook(); size_t fullsz = mcheck_memalign_prehook(alignment, bytes); void *p = dlmemalign_impl(alignment, fullsz); @@ -6033,6 +6054,16 @@ void *dlmemalign(size_t alignment, size_t bytes) void *dlcalloc(size_t n, size_t elem_size) { + if (CONFIG_IS_ENABLED(SYS_MALLOC_F) && + !(gd->flags & GD_FLG_FULL_MALLOC_INIT)) { + size_t sz = n * elem_size; + void *p = malloc_simple(sz); + + if (p) + memset(p, '\0', sz); + return p; + } + mcheck_pedantic_prehook(); /* NB: no overflow check here */ size_t fullsz = mcheck_alloc_prehook(n * elem_size); -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> The memory leaks were in fact not coming from the kmem cache, so let's drop this unnecessary feature. This reverts commit e63fc511c3dadbc99b53611cea304bc2bb565888. Signed-off-by: Simon Glass <simon.glass@canonical.com> --- include/linux/slab.h | 27 +++++++++++---------------- lib/Kconfig | 8 -------- lib/Makefile | 1 - lib/kmem_cache.c | 20 -------------------- 4 files changed, 11 insertions(+), 45 deletions(-) delete mode 100644 lib/kmem_cache.c diff --git a/include/linux/slab.h b/include/linux/slab.h index caaaa3d9e16..2b374641534 100644 --- a/include/linux/slab.h +++ b/include/linux/slab.h @@ -84,19 +84,24 @@ static inline void *krealloc(const void *p, size_t new_size, gfp_t flags) void *kmemdup(const void *src, size_t len, gfp_t gfp); -/* kmem_cache implementation / stubs */ +/* kmem_cache stubs */ struct kmem_cache { int sz; }; struct kmem_cache *get_mem(int element_sz); -#define kmem_cache_create(a, sz, c, d, e) get_mem(sz) +#define kmem_cache_create(a, sz, c, d, e) ({ (void)(a); (void)(e); get_mem(sz); }) void *kmem_cache_alloc(struct kmem_cache *obj, gfp_t flag); -#if CONFIG_IS_ENABLED(LIB_KMEM_CACHE) -void kmem_cache_free(struct kmem_cache *cachep, void *obj); -void kmem_cache_destroy(struct kmem_cache *cachep); -#else +static inline void *kmem_cache_zalloc(struct kmem_cache *obj, gfp_t flags) +{ + void *ret = kmem_cache_alloc(obj, flags); + + if (ret) + memset(ret, 0, obj->sz); + return ret; +} + static inline void kmem_cache_free(struct kmem_cache *cachep, void *obj) { free(obj); @@ -106,15 +111,5 @@ static inline void kmem_cache_destroy(struct kmem_cache *cachep) { free(cachep); } -#endif - -static inline void *kmem_cache_zalloc(struct kmem_cache *obj, gfp_t flags) -{ - void *ret = kmem_cache_alloc(obj, flags); - - if (ret) - memset(ret, 0, obj->sz); - return ret; -} #endif /* _LINUX_SLAB_H */ diff --git a/lib/Kconfig b/lib/Kconfig index 5ddf8125766..9c7eb27c392 100644 --- a/lib/Kconfig +++ b/lib/Kconfig @@ -396,14 +396,6 @@ config TPL_TINY_MEMSET config RBTREE bool -config LIB_KMEM_CACHE - bool "Enable full kmem_cache implementation" - help - Provide a proper kmem_cache implementation in lib/linux_compat.c - that tracks allocated objects. This is needed by subsystems like - ext4 that require cache management. When disabled, simple inline - stubs are used instead. - config BITREVERSE bool "Bit reverse library from Linux" diff --git a/lib/Makefile b/lib/Makefile index 3890a827d1c..e770c273a8c 100644 --- a/lib/Makefile +++ b/lib/Makefile @@ -136,7 +136,6 @@ obj-$(CONFIG_$(PHASE_)OF_LIBFDT) += fdtdec.o fdtdec_common.o fdt_print.o obj-y += hang.o obj-y += linux_compat.o obj-y += linux_string.o -obj-$(CONFIG_$(PHASE_)LIB_KMEM_CACHE) += kmem_cache.o obj-$(CONFIG_$(PHASE_)LMB) += lmb.o obj-y += membuf.o obj-$(CONFIG_REGEX) += slre.o diff --git a/lib/kmem_cache.c b/lib/kmem_cache.c deleted file mode 100644 index bff9329aa53..00000000000 --- a/lib/kmem_cache.c +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0+ -/* - * kmem_cache implementation for U-Boot - * - * Copyright 2025 Canonical Ltd - * Written by Simon Glass <simon.glass@canonical.com> - */ - -#include <malloc.h> -#include <linux/slab.h> - -void kmem_cache_free(struct kmem_cache *cachep, void *obj) -{ - free(obj); -} - -void kmem_cache_destroy(struct kmem_cache *cachep) -{ - free(cachep); -} -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> Move the message buffer functions from interface.c to support.c since they are internal support code rather than filesystem-interface functions. This keeps interface.c focused on functions called from the U-Boot filesystem layer. Functions moved: - ext4l_msg_init() - ext4l_record_msg() - ext4l_get_msg_buf() - ext4l_print_msgs() Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- fs/ext4l/ext4_uboot.h | 6 +++-- fs/ext4l/interface.c | 51 ------------------------------------------- fs/ext4l/support.c | 51 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 53 deletions(-) diff --git a/fs/ext4l/ext4_uboot.h b/fs/ext4l/ext4_uboot.h index 087d8394ab6..37a4abb537f 100644 --- a/fs/ext4l/ext4_uboot.h +++ b/fs/ext4l/ext4_uboot.h @@ -2892,12 +2892,14 @@ void bh_cache_clear(void); int bh_cache_sync(void); int ext4l_read_block(sector_t block, size_t size, void *buffer); int ext4l_write_block(sector_t block, size_t size, void *buffer); +void ext4l_msg_init(void); +void ext4l_record_msg(const char *msg, int len); +struct membuf *ext4l_get_msg_buf(void); +void ext4l_print_msgs(void); /* ext4l interface functions (interface.c) */ struct blk_desc *ext4l_get_blk_dev(void); struct disk_partition *ext4l_get_partition(void); -void ext4l_record_msg(const char *msg, int len); -struct membuf *ext4l_get_msg_buf(void); #define sb_is_blkdev_sb(sb) ({ (void)(sb); 0; }) diff --git a/fs/ext4l/interface.c b/fs/ext4l/interface.c index 7c37c99488a..aebcc17fd3a 100644 --- a/fs/ext4l/interface.c +++ b/fs/ext4l/interface.c @@ -13,7 +13,6 @@ #include <env.h> #include <fs.h> #include <fs_legacy.h> -#include <membuf.h> #include <part.h> #include <malloc.h> #include <u-boot/uuid.h> @@ -24,9 +23,6 @@ #include "ext4_uboot.h" #include "ext4.h" -/* Message buffer size */ -#define EXT4L_MSG_BUF_SIZE 4096 - /* Global state */ static struct blk_desc *ext4l_dev_desc; static struct disk_partition ext4l_part; @@ -42,10 +38,6 @@ static int ext4l_open_dirs; /* Global super_block pointer for filesystem operations */ static struct super_block *ext4l_sb; -/* Message recording buffer */ -static struct membuf ext4l_msg_buf; -static char ext4l_msg_data[EXT4L_MSG_BUF_SIZE]; - /** * ext4l_get_blk_dev() - Get the current block device * @@ -150,49 +142,6 @@ void ext4l_clear_blk_dev(void) ext4l_mounted = 0; } -/** - * ext4l_msg_init() - Initialize the message buffer - */ -static void ext4l_msg_init(void) -{ - membuf_init(&ext4l_msg_buf, ext4l_msg_data, EXT4L_MSG_BUF_SIZE); -} - -/** - * ext4l_record_msg() - Record a message in the buffer - * - * @msg: Message string to record - * @len: Length of message - */ -void ext4l_record_msg(const char *msg, int len) -{ - membuf_put(&ext4l_msg_buf, msg, len); -} - -/** - * ext4l_get_msg_buf() - Get the message buffer - * - * Return: Pointer to the message buffer - */ -struct membuf *ext4l_get_msg_buf(void) -{ - return &ext4l_msg_buf; -} - -/** - * ext4l_print_msgs() - Print all recorded messages - * - * Prints the contents of the message buffer to the console. - */ -static void ext4l_print_msgs(void) -{ - char *data; - int len; - - while ((len = membuf_getraw(&ext4l_msg_buf, 80, true, &data)) > 0) - printf("%.*s", len, data); -} - int ext4l_probe(struct blk_desc *fs_dev_desc, struct disk_partition *fs_partition) { diff --git a/fs/ext4l/support.c b/fs/ext4l/support.c index 72bf819c3d6..1fc0bc0aa21 100644 --- a/fs/ext4l/support.c +++ b/fs/ext4l/support.c @@ -10,6 +10,7 @@ */ #include <blk.h> +#include <membuf.h> #include <part.h> #include <malloc.h> #include <u-boot/crc.h> @@ -19,6 +20,13 @@ #include "ext4_uboot.h" #include "ext4.h" +/* Message buffer size */ +#define EXT4L_MSG_BUF_SIZE 4096 + +/* Message recording buffer */ +static struct membuf ext4l_msg_buf; +static char ext4l_msg_data[EXT4L_MSG_BUF_SIZE]; + /* * Global task_struct for U-Boot. * This must be a single global instance shared across all translation units, @@ -46,6 +54,49 @@ u32 ext4l_crc32c(u32 crc, const void *address, unsigned int length) return crc32c_cal(crc, address, length, ext4l_crc32c_table); } +/** + * ext4l_msg_init() - Initialise the message buffer + */ +void ext4l_msg_init(void) +{ + membuf_init(&ext4l_msg_buf, ext4l_msg_data, EXT4L_MSG_BUF_SIZE); +} + +/** + * ext4l_record_msg() - Record a message in the buffer + * + * @msg: Message string to record + * @len: Length of message + */ +void ext4l_record_msg(const char *msg, int len) +{ + membuf_put(&ext4l_msg_buf, msg, len); +} + +/** + * ext4l_get_msg_buf() - Get the message buffer + * + * Return: Pointer to the message buffer + */ +struct membuf *ext4l_get_msg_buf(void) +{ + return &ext4l_msg_buf; +} + +/** + * ext4l_print_msgs() - Print all recorded messages + * + * Prints the contents of the message buffer to the console. + */ +void ext4l_print_msgs(void) +{ + char *data; + int len; + + while ((len = membuf_getraw(&ext4l_msg_buf, 80, true, &data)) > 0) + printf("%.*s", len, data); +} + /* * iget_locked - allocate a new inode * @sb: super block of filesystem -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> U-Boot does not track allocated inodes, causing memory leaks when remounting filesystems. Add s_inodes list to super_block and i_sb_list to inode structures to track all allocated inodes, allowing proper eviction on unmount. Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- fs/ext4l/ext4_uboot.h | 6 ++++++ fs/ext4l/support.c | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/fs/ext4l/ext4_uboot.h b/fs/ext4l/ext4_uboot.h index 37a4abb537f..78d44faa0db 100644 --- a/fs/ext4l/ext4_uboot.h +++ b/fs/ext4l/ext4_uboot.h @@ -682,6 +682,9 @@ struct super_block { const struct export_operations *s_export_op; const struct xattr_handler * const *s_xattr; struct dentry *d_sb; /* Parent dentry - stub */ + + /* U-Boot: list of all inodes, for freeing on unmount */ + struct list_head s_inodes; }; /* Block device read-only check - stub */ @@ -859,6 +862,9 @@ struct inode { struct rw_semaphore i_rwsem; /* inode lock */ const char *i_link; /* Symlink target for fast symlinks */ unsigned short i_write_hint; /* Write life time hint */ + + /* U-Boot: linkage into super_block s_inodes list */ + struct list_head i_sb_list; }; /* Inode time accessors */ diff --git a/fs/ext4l/support.c b/fs/ext4l/support.c index 1fc0bc0aa21..7f2bea4ca06 100644 --- a/fs/ext4l/support.c +++ b/fs/ext4l/support.c @@ -126,6 +126,10 @@ struct inode *iget_locked(struct super_block *sb, unsigned long ino) inode->i_mapping = &inode->i_data; inode->i_data.host = inode; INIT_LIST_HEAD(&ei->i_es_list); + INIT_LIST_HEAD(&inode->i_sb_list); + + /* Add to superblock's inode list for eviction on unmount */ + list_add(&inode->i_sb_list, &sb->s_inodes); return inode; } @@ -154,6 +158,10 @@ struct inode *new_inode(struct super_block *sb) inode->i_mapping = &inode->i_data; inode->i_data.host = inode; INIT_LIST_HEAD(&ei->i_es_list); + INIT_LIST_HEAD(&inode->i_sb_list); + + /* Add to superblock's inode list for eviction on unmount */ + list_add(&inode->i_sb_list, &sb->s_inodes); return inode; } -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> Several buffer cache issues cause problems when remounting: 1. bh_cache_insert() only checks block number, but the same block can be read with different sizes (e.g. superblock at 1K vs 4K). Check both block number and size when determining if already cached. 2. bh_cache_clear() leaves stale buffer references, causing memory leaks. Force the reference count to 1 before releasing since ext4 code won't access these buffers after unmount. 3. brelse() calls free_buffer_head() when the reference count reaches zero. However, buffer heads remain in the cache and are freed by bh_cache_clear() during unmount. This causes a double-free. Fix this by having brelse() only decrement the count, without freeing. Co-developed-by: Claude <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- fs/ext4l/support.c | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/fs/ext4l/support.c b/fs/ext4l/support.c index 7f2bea4ca06..3133a4ebead 100644 --- a/fs/ext4l/support.c +++ b/fs/ext4l/support.c @@ -258,9 +258,10 @@ static void bh_cache_insert(struct buffer_head *bh) unsigned int hash = bh_cache_hash(bh->b_blocknr); struct bh_cache_entry *entry; - /* Check if already in cache */ + /* Check if already in cache - must match block AND size */ for (entry = bh_cache[hash]; entry; entry = entry->next) { - if (entry->bh && entry->bh->b_blocknr == bh->b_blocknr) + if (entry->bh && entry->bh->b_blocknr == bh->b_blocknr && + entry->bh->b_size == bh->b_size) return; /* Already cached */ } @@ -316,7 +317,12 @@ void bh_cache_clear(void) struct buffer_head *bh = entry->bh; bh_clear_stale_jbd(bh); - /* Release the cache's reference */ + /* + * Force count to 1 so the buffer will be freed. + * On unmount, ext4 code won't access these + * buffers again, so extra references are stale. + */ + atomic_set(&bh->b_count, 1); if (atomic_dec_and_test(&bh->b_count)) free_buffer_head(bh); } @@ -629,14 +635,24 @@ struct buffer_head *sb_bread(struct super_block *sb, sector_t block) /** * brelse() - Release a buffer_head * @bh: Buffer head to release + * + * Decrements the reference count on the buffer. Buffer heads are cached + * and only freed by bh_cache_clear() on unmount, so this just decrements + * the count without freeing. */ void brelse(struct buffer_head *bh) { if (!bh) return; - if (atomic_dec_and_test(&bh->b_count)) - free_buffer_head(bh); + /* + * If buffer has JBD attached, don't let ref count go to zero. + * The journal owns a reference and will clean up properly. + */ + if (buffer_jbd(bh) && atomic_read(&bh->b_count) <= 1) + return; + + atomic_dec(&bh->b_count); } /** -- 2.43.0
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(-) 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; /* -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> The atomic_add() and atomic64_add() functions are now provided by asm-generic/atomic.h so remove the duplicate declarations from ext4_uboot.h and the implementation from stub.c Co-developed-by: Claude <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- fs/ext4l/ext4_uboot.h | 4 ---- fs/ext4l/stub.c | 8 +------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/fs/ext4l/ext4_uboot.h b/fs/ext4l/ext4_uboot.h index abe54e67aa4..df26c02b54d 100644 --- a/fs/ext4l/ext4_uboot.h +++ b/fs/ext4l/ext4_uboot.h @@ -2310,10 +2310,6 @@ void *kvzalloc(size_t size, gfp_t flags); #define kvmalloc(size, flags) kvzalloc(size, flags) unsigned long roundup_pow_of_two(unsigned long n); -/* Atomic operations - declarations for stub.c */ -void atomic_add(int val, atomic_t *v); -void atomic64_add(s64 val, atomic64_t *v); - /* Power of 2 check - declaration for stub.c */ int is_power_of_2(unsigned long n); diff --git a/fs/ext4l/stub.c b/fs/ext4l/stub.c index f072cb5713f..d328c6821a9 100644 --- a/fs/ext4l/stub.c +++ b/fs/ext4l/stub.c @@ -617,13 +617,7 @@ int percpu_init_rwsem(struct percpu_rw_semaphore *sem) return 0; } -/* Atomic operations */ -void atomic_add(int val, atomic_t *v) -{ - v->counter += val; -} - -/* atomic64_add is now a macro in ext4_uboot.h */ +/* atomic_add and atomic64_add are now in asm-generic/atomic.h */ /* Discard */ unsigned int bdev_max_discard_sectors(struct block_device *bdev) -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> The crc16() stub always returns 0, which causes group-descriptor checksum-verification to fail for filesystems using the old CRC16 checksum algorithm (gdt_csum feature without metadata_csum). Replace the stub with U-Boot's real CRC16 implementation to allow mounting these filesystems. Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- fs/ext4l/ext4_uboot.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fs/ext4l/ext4_uboot.h b/fs/ext4l/ext4_uboot.h index df26c02b54d..ba4b4266daa 100644 --- a/fs/ext4l/ext4_uboot.h +++ b/fs/ext4l/ext4_uboot.h @@ -2143,8 +2143,8 @@ struct fs_parse_result { /* ctype */ #include <linux/ctype.h> -/* crc16 */ -#define crc16(crc, buf, len) (0) +/* crc16 - use U-Boot's implementation */ +#include <linux/crc16.h> /* Timer and timing stubs */ #define HZ 1000 -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> The ext4l driver requires the filesystem framework (FS) and rbtree library (RBTREE) to build correctly. Add the first as an explicit dependency and 'select' the second, to prevent build failures when ext4l is enabled. Co-developed-by: Claude <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- fs/ext4l/Kconfig | 3 +++ 1 file changed, 3 insertions(+) diff --git a/fs/ext4l/Kconfig b/fs/ext4l/Kconfig index 998f0f45474..be989208ec5 100644 --- a/fs/ext4l/Kconfig +++ b/fs/ext4l/Kconfig @@ -5,6 +5,9 @@ config FS_EXT4L bool "Enable ext4 filesystem support (Linux port)" + depends on FS + select RBTREE + select CRC32C help This provides support for reading images from the ext4 filesystem, using code ported from Linux. This is mutually exclusive with -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> Add bh_cache_release_jbd() to forcibly release any journal_heads still attached to buffer_heads after journal destroy. This must be called after journal destroy but before bh_cache_clear() to ensure all journal_heads are properly released, even if journal destroy did not fully clean up (e.g., on abort). The function clears b_bh in each journal_head to prevent use-after-free when the buffer_head is later freed, and resets transaction pointers. Co-developed-by: Claude <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- fs/ext4l/ext4_uboot.h | 1 + fs/ext4l/support.c | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/fs/ext4l/ext4_uboot.h b/fs/ext4l/ext4_uboot.h index ba4b4266daa..00419fe834e 100644 --- a/fs/ext4l/ext4_uboot.h +++ b/fs/ext4l/ext4_uboot.h @@ -2891,6 +2891,7 @@ void free_buffer_head(struct buffer_head *bh); /* ext4l support functions (support.c) */ void ext4l_crc32c_init(void); +void bh_cache_release_jbd(void); void bh_cache_clear(void); int bh_cache_sync(void); int ext4l_read_block(sector_t block, size_t size, void *buffer); diff --git a/fs/ext4l/support.c b/fs/ext4l/support.c index 3133a4ebead..aaaf89092eb 100644 --- a/fs/ext4l/support.c +++ b/fs/ext4l/support.c @@ -332,6 +332,42 @@ void bh_cache_clear(void) } } +/** + * bh_cache_release_jbd() - Release all JBD references from buffer cache + * + * This must be called after journal destroy but before bh_cache_clear(). + * It ensures all journal_heads are properly released from buffer_heads + * even if the journal destroy didn't fully clean up (e.g., on abort). + */ +void bh_cache_release_jbd(void) +{ + int i; + struct bh_cache_entry *entry; + + for (i = 0; i < BH_CACHE_SIZE; i++) { + for (entry = bh_cache[i]; entry; entry = entry->next) { + if (entry->bh && buffer_jbd(entry->bh)) { + struct buffer_head *bh = entry->bh; + struct journal_head *jh = bh2jh(bh); + + /* + * Forcibly release the journal_head. + * Clear b_bh to prevent use-after-free when + * the buffer_head is later freed. + */ + if (jh) { + jh->b_bh = NULL; + jh->b_transaction = NULL; + jh->b_next_transaction = NULL; + jh->b_cp_transaction = NULL; + } + clear_buffer_jbd(bh); + bh->b_private = NULL; + } + } + } +} + /** * bh_cache_sync() - Sync all dirty buffers to disk * -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> When running multiple filesystem tests in sequence, probe may be called without an explicit close of the previous mount. The old device may have been rebound to a different file, making I/O to it invalid. Add a new ext4l_close_internal() function with a skip_io parameter to handle this case. When skip_io is true, it skips journal-destroy entirely since the device may be invalid. It will be recovered on next mount. Also call the ext4- and JBD2- cleanup functions to properly reset the global state for subsequent mounts: ext4_exit_system_zone(), ext4_exit_es(), ext4_exit_mballoc(), and jbd2_journal_exit_global() This ensures the caches are destroyed, thus freeing all orphaned journal_heads, even when skip_io is true. Co-developed-by: Claude <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- fs/ext4l/interface.c | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/fs/ext4l/interface.c b/fs/ext4l/interface.c index ceedabdb727..301e28af3b8 100644 --- a/fs/ext4l/interface.c +++ b/fs/ext4l/interface.c @@ -286,6 +286,17 @@ int ext4l_probe(struct blk_desc *fs_dev_desc, if (!fs_dev_desc) return -EINVAL; + /* + * Ensure any previous mount is properly closed before mounting again. + * This prevents resource leaks if probe is called without close. + * + * Since we're being called while a previous mount exists, we can't + * trust the old device state (it may have been rebound to a different + * file). Use skip_io=true to skip all I/O during close. + */ + if (ext4l_sb) + ext4l_close_internal(true); + /* Initialise message buffer for recording ext4 messages */ ext4l_msg_init(); @@ -855,12 +866,7 @@ int ext4l_read(const char *filename, void *buf, loff_t offset, loff_t len, void ext4l_close(void) { - if (ext4l_open_dirs > 0) - return; - - ext4l_dev_desc = NULL; - ext4l_sb = NULL; - ext4l_clear_blk_dev(); + ext4l_close_internal(false); } /** -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> When running filesystem tests back-to-back, buffer_heads could be freed while journal_heads still reference them. This causes use-after-free crashes when the journal code later accesses the stale b_bh pointer. Add protection in free_buffer_head() to skip buffers with JBD attached, since the journal owns a reference and will clean them up properly. Also add protection in brelse() to prevent the ref count from dropping to zero while JBD is still attached. Update comments in ext4l_close_internal() to clarify why cache cleanup is critical even during skip_io mode. Fixes crashes when test_fs13 runs after test_fs11 in the same session. Co-developed-by: Claude <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- fs/ext4l/support.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/fs/ext4l/support.c b/fs/ext4l/support.c index aaaf89092eb..3be40b6fca2 100644 --- a/fs/ext4l/support.c +++ b/fs/ext4l/support.c @@ -466,6 +466,15 @@ void free_buffer_head(struct buffer_head *bh) if (!bh) return; + /* + * Never free a buffer_head that has a journal_head attached. + * This would cause use-after-free when the journal tries to access it. + * The journal owns a reference and the buffer will be cleaned up when + * the journal_head is properly released. + */ + if (buffer_jbd(bh)) + return; + /* * Shadow buffers (b_private != NULL) share their folio with the * original buffer. Don't free the shared folio. -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> Some block devices, such as LUKS-encrypted volumes mounted via blkmap_crypt, only support read access. When ext4l attempts to write the superblock during mount, the write fails and causes mount to fail. Add a way to detect this read-only device detection: - Test writes during mount by writing back the superblock data; if the write fails, mark the device as read-only - Update bdev_read_only() to return the actual read_only status - Update sb_rdonly() to check the SB_RDONLY flag This allows ext4l to successfully mount read-only devices like LUKS volumes for read access. We could perhaps have a read-only flag in the block device, but that is left for another day. Co-developed-by: Claude <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- fs/ext4l/ext4_uboot.h | 8 ++++---- fs/ext4l/interface.c | 14 ++++++++++++-- include/linux/fs.h | 1 + 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/fs/ext4l/ext4_uboot.h b/fs/ext4l/ext4_uboot.h index 00419fe834e..da59956eee8 100644 --- a/fs/ext4l/ext4_uboot.h +++ b/fs/ext4l/ext4_uboot.h @@ -315,8 +315,8 @@ extern struct user_namespace init_user_ns; /* might_sleep - stub */ #define might_sleep() do { } while (0) -/* sb_rdonly - U-Boot mounts filesystems read-write */ -#define sb_rdonly(sb) 0 +/* sb_rdonly - check if filesystem is mounted read-only */ +#define sb_rdonly(sb) ((sb)->s_flags & SB_RDONLY) /* Trace stubs */ #define trace_ext4_journal_start_inode(...) do { } while (0) @@ -687,10 +687,10 @@ struct super_block { struct list_head s_inodes; }; -/* Block device read-only check - stub */ +/* Block device read-only check */ static inline int bdev_read_only(struct block_device *bdev) { - return 0; + return bdev ? bdev->read_only : 0; } /* kuid_t and kgid_t - from linux/cred.h */ diff --git a/fs/ext4l/interface.c b/fs/ext4l/interface.c index 301e28af3b8..c60ea7db684 100644 --- a/fs/ext4l/interface.c +++ b/fs/ext4l/interface.c @@ -401,8 +401,6 @@ int ext4l_probe(struct blk_desc *fs_dev_desc, goto err_free_buf; } - free(buf); - /* Save device info for later operations */ ext4l_dev_desc = fs_dev_desc; if (fs_partition) @@ -411,6 +409,18 @@ int ext4l_probe(struct blk_desc *fs_dev_desc, /* Set block device for buffer I/O */ ext4l_set_blk_dev(fs_dev_desc, fs_partition); + /* + * Test if device supports writes by writing back the same data. + * If write returns 0, the device is read-only (e.g. LUKS/blkmap_crypt) + */ + if (blk_dwrite(fs_dev_desc, + (part_offset + BLOCK_SIZE) / fs_dev_desc->blksz, + 2, buf) != 2) { + sb->s_bdev->read_only = true; + sb->s_flags |= SB_RDONLY; + } + free(buf); + /* Mount the filesystem */ ret = ext4_fill_super(sb, fc); if (ret) { diff --git a/include/linux/fs.h b/include/linux/fs.h index 0bf0d3b0379..54c0148ee72 100644 --- a/include/linux/fs.h +++ b/include/linux/fs.h @@ -76,6 +76,7 @@ struct block_device { void *bd_disk; struct super_block *bd_super; dev_t bd_dev; + bool read_only; }; /* errseq functions - stubs */ -- 2.43.0
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> --- 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) -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> Implement ext4l_unlink() to delete files from ext4 filesystems. This enables the 'rm' command to work with ext4l. The implementation: - Resolves the parent directory and target file - Verifies the target is not a directory (use rmdir for that) - Calls ext4_unlink() to remove the directory entry - Uses journal transactions for crash safety Add ext4l_op_ptr() macro to select between ext4l_unlink() and the fallback based on CONFIG_EXT4_WRITE Call ext4_commit_super() to ensure the changes are written to disk. Co-developed-by: Claude <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- fs/ext4l/interface.c | 251 +++++++++++++++++----------- fs/fs_legacy.c | 2 +- include/ext4l.h | 9 + test/fs/ext4l.c | 41 +++++ test/py/tests/test_fs/test_ext4l.py | 5 + 5 files changed, 212 insertions(+), 96 deletions(-) diff --git a/fs/ext4l/interface.c b/fs/ext4l/interface.c index 94bfd71bfb5..442617a0d3e 100644 --- a/fs/ext4l/interface.c +++ b/fs/ext4l/interface.c @@ -874,75 +874,131 @@ 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) +/** + * ext4l_resolve_file() - Resolve a file path for write operations + * @path: Path to process + * @dir_dentryp: Returns parent directory dentry (caller must kfree) + * @dentryp: Returns file dentry after lookup (caller must kfree) + * @path_copyp: Returns path copy (caller must free) + * + * Common setup for write operations. Validates inputs, checks read-write + * mount, parses path, resolves parent directory, creates dentries, and + * performs lookup. + * + * Return: 0 on success, negative on error + */ +static int ext4l_resolve_file(const char *path, struct dentry **dir_dentryp, + struct dentry **dentryp, char **path_copyp) { + char *path_copy, *dir_path, *last_slash; struct dentry *dir_dentry, *dentry, *result; - handle_t *handle = NULL; - struct buffer_head *bh; - struct inode *inode; - loff_t pos, end; - umode_t mode; + struct inode *dir_inode; + const char *basename; int ret; - /* Create dentry for the parent directory */ + if (!ext4l_sb) + return -ENODEV; + + if (!path) + return -EINVAL; + + /* Check if filesystem is mounted read-write */ + if (ext4l_sb->s_flags & SB_RDONLY) + return -EROFS; + + /* Parse path to get parent directory and basename */ + path_copy = strdup(path); + if (!path_copy) + return -ENOMEM; + + last_slash = strrchr(path_copy, '/'); + if (last_slash) { + *last_slash = '\0'; + dir_path = path_copy; + basename = last_slash + 1; + if (*dir_path == '\0') + dir_path = "/"; + } else { + dir_path = "/"; + basename = path; + } + + /* Resolve parent directory */ + ret = ext4l_resolve_path(dir_path, &dir_inode); + if (ret) { + free(path_copy); + return ret; + } + + if (!S_ISDIR(dir_inode->i_mode)) { + free(path_copy); + return -ENOTDIR; + } + + /* Create dentry for parent directory */ dir_dentry = kzalloc(sizeof(struct dentry), GFP_KERNEL); - if (!dir_dentry) + if (!dir_dentry) { + free(path_copy); return -ENOMEM; - dir_dentry->d_inode = dir; - dir_dentry->d_sb = dir->i_sb; + } + dir_dentry->d_inode = dir_inode; + dir_dentry->d_sb = dir_inode->i_sb; - /* Create dentry for the filename */ + /* Create dentry for the file */ dentry = kzalloc(sizeof(struct dentry), GFP_KERNEL); if (!dentry) { kfree(dir_dentry); + free(path_copy); 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_name.name = basename; + dentry->d_name.len = strlen(basename); + dentry->d_sb = dir_inode->i_sb; dentry->d_parent = dir_dentry; - /* Lookup file */ - result = ext4_lookup(dir, dentry, 0); - + /* Look up the file */ + result = ext4_lookup(dir_inode, dentry, 0); if (IS_ERR(result)) { - ret = PTR_ERR(result); - goto out_dentry; + kfree(dentry); + kfree(dir_dentry); + free(path_copy); + return PTR_ERR(result); } - if (result && result->d_inode) { + *dir_dentryp = dir_dentry; + *dentryp = dentry; + *path_copyp = path_copy; + + return 0; +} + +static int ext4l_write_file(struct dentry *dir_dentry, struct dentry *dentry, + void *buf, loff_t offset, loff_t len, + loff_t *actwrite) +{ + struct inode *dir = dir_dentry->d_inode; + handle_t *handle = NULL; + struct buffer_head *bh; + struct inode *inode; + loff_t pos, end; + umode_t mode; + int ret; + + if (dentry->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; - } + inode = dentry->d_inode; } 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; + return ret; inode = dentry->d_inode; } - if (!inode) { - ret = -EIO; - goto out_dentry; - } + if (!inode) + return -EIO; /* * Attach jinode for journaling if needed (like ext4_file_open does). @@ -950,7 +1006,7 @@ static int ext4l_write_file(struct inode *dir, const char *filename, void *buf, */ ret = ext4_inode_attach_jinode(inode); if (ret < 0) - goto out_dentry; + return ret; /* * Start a journal handle for the write operation. @@ -959,11 +1015,8 @@ static int ext4l_write_file(struct inode *dir, const char *filename, void *buf, */ 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; - } + if (IS_ERR(handle)) + return PTR_ERR(handle); /* Write data to file */ pos = offset; @@ -1063,84 +1116,92 @@ out_handle: if (handle) { int stop_ret = ext4_journal_stop(handle); - if (stop_ret) { - if (!ret) - ret = stop_ret; - } + if (stop_ret && !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; + struct dentry *dir_dentry, *dentry; char *path_copy; - char *last_slash; int ret; - if (!ext4l_sb) - return -ENODEV; - - if (!filename || !buf || !actwrite) + if (!buf || !actwrite) return -EINVAL; - /* Check if filesystem is mounted read-write */ - if (ext4l_sb->s_flags & SB_RDONLY) - return -EROFS; + ret = ext4l_resolve_file(filename, &dir_dentry, &dentry, &path_copy); + if (ret) + return ret; - /* Parse filename to get parent directory and basename */ - path_copy = strdup(filename); - if (!path_copy) - return -ENOMEM; + /* Call write implementation */ + ret = ext4l_write_file(dir_dentry, dentry, buf, offset, len, actwrite); - last_slash = strrchr(path_copy, '/'); + /* Sync all dirty buffers - U-Boot has no journal thread */ + if (!ret) { + int sync_ret = bh_cache_sync(); - 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; + if (sync_ret) + ret = sync_ret; } - /* Resolve parent directory inode */ - ret = ext4l_resolve_path(parent_path, &parent_inode); - if (ret) { - free(path_copy); + kfree(dentry); + kfree(dir_dentry); + free(path_copy); + return ret; +} + +int ext4l_unlink(const char *filename) +{ + struct dentry *dentry, *dir_dentry; + char *path_copy; + int ret; + + ret = ext4l_resolve_file(filename, &dir_dentry, &dentry, &path_copy); + if (ret) return ret; + + /* Check if file exists */ + if (!dentry->d_inode) { + ret = -ENOENT; + goto out; } - if (!S_ISDIR(parent_inode->i_mode)) { - free(path_copy); - return -ENOTDIR; + /* Cannot unlink directories with unlink - use rmdir */ + if (S_ISDIR(dentry->d_inode->i_mode)) { + ret = -EISDIR; + goto out; } - /* Call write implementation */ - ret = ext4l_write_file(parent_inode, basename, buf, offset, len, - actwrite); + /* Unlink the file */ + ret = __ext4_unlink(dir_dentry->d_inode, &dentry->d_name, + dentry->d_inode, dentry); - /* Sync all dirty buffers - U-Boot has no journal thread */ + /* + * Release inode - this triggers ext4_evict_inode for nlink=0 inodes, + * which frees the data blocks and inode. + */ + if (dentry->d_inode) { + iput(dentry->d_inode); + dentry->d_inode = NULL; + } + + /* Sync all dirty buffers after inode eviction */ if (!ret) { int sync_ret = bh_cache_sync(); if (sync_ret) ret = sync_ret; + /* Commit superblock with updated free counts */ + ext4_commit_super(ext4l_sb); } +out: + kfree(dentry); + kfree(dir_dentry); free(path_copy); return ret; } diff --git a/fs/fs_legacy.c b/fs/fs_legacy.c index 66545834928..1eb79b338ee 100644 --- a/fs/fs_legacy.c +++ b/fs/fs_legacy.c @@ -288,7 +288,7 @@ static struct fstype_info fstypes[] = { .opendir = ext4l_opendir, .readdir = ext4l_readdir, .closedir = ext4l_closedir, - .unlink = fs_unlink_unsupported, + .unlink = ext4l_op_ptr(ext4l_unlink, fs_unlink_unsupported), .mkdir = fs_mkdir_unsupported, .ln = fs_ln_unsupported, .rename = fs_rename_unsupported, diff --git a/include/ext4l.h b/include/ext4l.h index 5dfb671690c..abcf17f99ad 100644 --- a/include/ext4l.h +++ b/include/ext4l.h @@ -92,6 +92,15 @@ int ext4l_read(const char *filename, void *buf, loff_t offset, loff_t len, int ext4l_write(const char *filename, void *buf, loff_t offset, loff_t len, loff_t *actwrite); +/** + * ext4l_unlink() - Delete a file + * + * @filename: Path to file to delete + * Return: 0 on success, -ENOENT if file not found, -EISDIR if path is a + * directory, -EROFS if read-only, negative on other errors + */ +int ext4l_unlink(const char *filename); + /** * ext4l_get_uuid() - Get the filesystem UUID * diff --git a/test/fs/ext4l.c b/test/fs/ext4l.c index 82587a87894..dec6e3340bb 100644 --- a/test/fs/ext4l.c +++ b/test/fs/ext4l.c @@ -438,3 +438,44 @@ static int fs_test_ext4l_write_norun(struct unit_test_state *uts) } FS_TEST_ARGS(fs_test_ext4l_write_norun, UTF_SCAN_FDT | UTF_CONSOLE | UTF_MANUAL, { "fs_image", UT_ARG_STR }); + +/** + * fs_test_ext4l_unlink_norun() - Test ext4l_unlink function + * + * Verifies that ext4l can delete files from the filesystem. + * + * Arguments: + * fs_image: Path to the ext4 filesystem image + */ +static int fs_test_ext4l_unlink_norun(struct unit_test_state *uts) +{ + const char *fs_image = ut_str(EXT4L_ARG_IMAGE); + const char *test_data = "unlink test\n"; + size_t test_len = strlen(test_data); + loff_t actwrite; + + 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)); + + /* Create a new file to unlink */ + ut_assertok(ext4l_write("/unlinkme.txt", (void *)test_data, 0, + test_len, &actwrite)); + ut_asserteq(test_len, actwrite); + + /* Verify file exists (same mount) */ + ut_asserteq(1, ext4l_exists("/unlinkme.txt")); + + /* Unlink the file */ + ut_assertok(ext4l_unlink("/unlinkme.txt")); + + /* Verify file no longer exists */ + ut_asserteq(0, ext4l_exists("/unlinkme.txt")); + + /* Verify unlinking non-existent file returns -ENOENT */ + ut_asserteq(-ENOENT, ext4l_unlink("/nonexistent")); + + return 0; +} +FS_TEST_ARGS(fs_test_ext4l_unlink_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 e953be379eb..edea38f7faf 100644 --- a/test/py/tests/test_fs/test_ext4l.py +++ b/test/py/tests/test_fs/test_ext4l.py @@ -123,3 +123,8 @@ class TestExt4l: """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) + + def test_unlink(self, ubman, ext4_image): + """Test that ext4l can delete files.""" + with ubman.log.section('Test ext4l unlink'): + ubman.run_ut('fs', 'fs_test_ext4l_unlink', fs_image=ext4_image) -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> Implement ext4l_mkdir() to create directories on ext4 filesystems. The function parses the path to extract the parent directory and basename, resolves the parent inode, checks for existing entries, and calls the Linux ext4_mkdir() function to create the directory. Hook ext4l_mkdir into the filesystem layer via the .mkdir callback in fs_legacy.c, enabling the standard 'mkdir' command to work with ext4l filesystems. Add a unit test that verifies directory creation, duplicate detection (-EEXIST), nested directory creation, and error handling for non-existent parent directories (-ENOENT). Co-developed-by: Claude <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- fs/ext4l/interface.c | 43 ++++++++++++++++++++++++++++++++++++++++ fs/fs_legacy.c | 2 +- include/ext4l.h | 10 ++++++++++ test/fs/ext4l.c | 47 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 101 insertions(+), 1 deletion(-) diff --git a/fs/ext4l/interface.c b/fs/ext4l/interface.c index 442617a0d3e..010db3a3631 100644 --- a/fs/ext4l/interface.c +++ b/fs/ext4l/interface.c @@ -1206,6 +1206,49 @@ out: return ret; } +int ext4l_mkdir(const char *dirname) +{ + struct dentry *dentry, *dir_dentry, *result; + char *path_copy; + int ret; + + ret = ext4l_resolve_file(dirname, &dir_dentry, &dentry, &path_copy); + if (ret) + return ret; + + if (dentry->d_inode) { + /* Directory already exists */ + ret = -EEXIST; + goto out; + } + + /* Create the directory with mode 0755 (rwxr-xr-x) */ + result = ext4_mkdir(&nop_mnt_idmap, dir_dentry->d_inode, dentry, + S_IFDIR | 0755); + if (IS_ERR(result)) { + ret = PTR_ERR(result); + goto out; + } + + ret = 0; + + /* Sync all dirty buffers */ + { + int sync_ret = bh_cache_sync(); + + if (sync_ret) + ret = sync_ret; + /* Commit superblock with updated free counts */ + ext4_commit_super(ext4l_sb); + } + +out: + kfree(dentry); + kfree(dir_dentry); + 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 1eb79b338ee..b53d420f279 100644 --- a/fs/fs_legacy.c +++ b/fs/fs_legacy.c @@ -289,7 +289,7 @@ static struct fstype_info fstypes[] = { .readdir = ext4l_readdir, .closedir = ext4l_closedir, .unlink = ext4l_op_ptr(ext4l_unlink, fs_unlink_unsupported), - .mkdir = fs_mkdir_unsupported, + .mkdir = ext4l_op_ptr(ext4l_mkdir, fs_mkdir_unsupported), .ln = fs_ln_unsupported, .rename = fs_rename_unsupported, .statfs = ext4l_statfs, diff --git a/include/ext4l.h b/include/ext4l.h index abcf17f99ad..1a12ba1ac1c 100644 --- a/include/ext4l.h +++ b/include/ext4l.h @@ -101,6 +101,16 @@ int ext4l_write(const char *filename, void *buf, loff_t offset, loff_t len, */ int ext4l_unlink(const char *filename); +/** + * ext4l_mkdir() - Create a directory + * + * @dirname: Path of directory to create + * Return: 0 on success, -EEXIST if directory already exists, + * -ENOTDIR if parent is not a directory, -EROFS if read-only, + * negative on other errors + */ +int ext4l_mkdir(const char *dirname); + /** * ext4l_get_uuid() - Get the filesystem UUID * diff --git a/test/fs/ext4l.c b/test/fs/ext4l.c index dec6e3340bb..5930e4234ba 100644 --- a/test/fs/ext4l.c +++ b/test/fs/ext4l.c @@ -479,3 +479,50 @@ static int fs_test_ext4l_unlink_norun(struct unit_test_state *uts) } FS_TEST_ARGS(fs_test_ext4l_unlink_norun, UTF_SCAN_FDT | UTF_CONSOLE | UTF_MANUAL, { "fs_image", UT_ARG_STR }); + +/** + * fs_test_ext4l_mkdir_norun() - Test ext4l_mkdir function + * + * Verifies that ext4l can create directories on the filesystem. + * + * Arguments: + * fs_image: Path to the ext4 filesystem image + */ +static int fs_test_ext4l_mkdir_norun(struct unit_test_state *uts) +{ + const char *fs_image = ut_str(EXT4L_ARG_IMAGE); + static int test_counter; + char dir_name[32]; + char subdir_name[64]; + int ret; + + 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)); + + /* Use unique directory names to avoid issues with test re-runs */ + snprintf(dir_name, sizeof(dir_name), "/testdir%d", test_counter); + snprintf(subdir_name, sizeof(subdir_name), "%s/subdir", dir_name); + test_counter++; + + /* Create a new directory */ + ret = ext4l_mkdir(dir_name); + ut_assertok(ret); + + /* Verify directory exists */ + ut_asserteq(1, ext4l_exists(dir_name)); + + /* Verify creating duplicate returns -EEXIST */ + ut_asserteq(-EEXIST, ext4l_mkdir(dir_name)); + + /* Create nested directory */ + ut_assertok(ext4l_mkdir(subdir_name)); + ut_asserteq(1, ext4l_exists(subdir_name)); + + /* Verify creating directory in non-existent parent returns -ENOENT */ + ut_asserteq(-ENOENT, ext4l_mkdir("/nonexistent/dir")); + + return 0; +} +FS_TEST_ARGS(fs_test_ext4l_mkdir_norun, UTF_SCAN_FDT | UTF_CONSOLE | UTF_MANUAL, + { "fs_image", UT_ARG_STR }); -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> Add ext4l_ln() to create symbolic links. This uses the Linux ext4_symlink() function which supports both fast symlinks (stored in inode) and regular symlinks (stored in data blocks). Fix the fscrypt_prepare_symlink() stub to properly init the disk_link structure with the symlink target, which is required for symlink creation to work correctly. Add some notes about U-Boot's argument ordering with symlinks. Co-developed-by: Claude <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- fs/ext4l/ext4_uboot.h | 2 +- fs/ext4l/interface.c | 48 ++++++++++++++++++++++++++++++++++ fs/fs_legacy.c | 2 +- include/ext4l.h | 11 ++++++++ test/fs/ext4l.c | 60 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 121 insertions(+), 2 deletions(-) diff --git a/fs/ext4l/ext4_uboot.h b/fs/ext4l/ext4_uboot.h index da59956eee8..5ea573fcfb8 100644 --- a/fs/ext4l/ext4_uboot.h +++ b/fs/ext4l/ext4_uboot.h @@ -1579,7 +1579,7 @@ static inline char *d_path(const struct path *path, char *buf, int buflen) #define fscrypt_dio_supported(i) (1) #define fscrypt_has_permitted_context(p, c) ({ (void)(p); (void)(c); 1; }) #define fscrypt_is_nokey_name(d) ({ (void)(d); 0; }) -#define fscrypt_prepare_symlink(d, s, l, m, dl) ({ (void)(d); (void)(s); (void)(l); (void)(m); (void)(dl); 0; }) +#define fscrypt_prepare_symlink(d, s, l, m, dl) ({ (void)(d); (void)(m); (dl)->name = (unsigned char *)(s); (dl)->len = (l) + 1; 0; }) #define fscrypt_encrypt_symlink(i, s, l, d) ({ (void)(i); (void)(s); (void)(l); (void)(d); 0; }) #define fscrypt_prepare_link(o, d, n) ({ (void)(o); (void)(d); (void)(n); 0; }) #define fscrypt_prepare_rename(od, ode, nd, nde, f) ({ (void)(od); (void)(ode); (void)(nd); (void)(nde); (void)(f); 0; }) diff --git a/fs/ext4l/interface.c b/fs/ext4l/interface.c index 010db3a3631..c9dd25dd7b4 100644 --- a/fs/ext4l/interface.c +++ b/fs/ext4l/interface.c @@ -1249,6 +1249,54 @@ out: return ret; } +int ext4l_ln(const char *filename, const char *linkname) +{ + struct dentry *dentry, *dir_dentry; + char *path_copy; + int ret; + + /* + * Note: The parameter naming follows U-Boot's convention: + * - filename: the target file the link should point to + * - linkname: the path of the symlink to create + */ + if (!filename) + return -EINVAL; + + ret = ext4l_resolve_file(linkname, &dir_dentry, &dentry, &path_copy); + if (ret) + return ret; + + if (dentry->d_inode) { + /* File already exists */ + ret = -EEXIST; + goto out; + } + + /* Create the symlink - filename is what the link points to */ + ret = ext4_symlink(&nop_mnt_idmap, dir_dentry->d_inode, dentry, + filename); + if (ret) + goto out; + + /* Sync all dirty buffers */ + { + int sync_ret = bh_cache_sync(); + + if (sync_ret) + ret = sync_ret; + /* Commit superblock with updated free counts */ + ext4_commit_super(ext4l_sb); + } + +out: + kfree(dentry); + kfree(dir_dentry); + 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 b53d420f279..a325c2631d4 100644 --- a/fs/fs_legacy.c +++ b/fs/fs_legacy.c @@ -290,7 +290,7 @@ static struct fstype_info fstypes[] = { .closedir = ext4l_closedir, .unlink = ext4l_op_ptr(ext4l_unlink, fs_unlink_unsupported), .mkdir = ext4l_op_ptr(ext4l_mkdir, fs_mkdir_unsupported), - .ln = fs_ln_unsupported, + .ln = ext4l_op_ptr(ext4l_ln, fs_ln_unsupported), .rename = fs_rename_unsupported, .statfs = ext4l_statfs, }, diff --git a/include/ext4l.h b/include/ext4l.h index 1a12ba1ac1c..d0e420c8da2 100644 --- a/include/ext4l.h +++ b/include/ext4l.h @@ -111,6 +111,17 @@ int ext4l_unlink(const char *filename); */ int ext4l_mkdir(const char *dirname); +/** + * ext4l_ln() - Create a symbolic link + * + * @filename: Path of symlink to create + * @target: Target path the symlink points to + * Return: 0 on success, -EEXIST if file already exists, + * -ENOTDIR if parent is not a directory, -EROFS if read-only, + * negative on other errors + */ +int ext4l_ln(const char *filename, const char *target); + /** * ext4l_get_uuid() - Get the filesystem UUID * diff --git a/test/fs/ext4l.c b/test/fs/ext4l.c index 5930e4234ba..1d46a752f32 100644 --- a/test/fs/ext4l.c +++ b/test/fs/ext4l.c @@ -526,3 +526,63 @@ static int fs_test_ext4l_mkdir_norun(struct unit_test_state *uts) } FS_TEST_ARGS(fs_test_ext4l_mkdir_norun, UTF_SCAN_FDT | UTF_CONSOLE | UTF_MANUAL, { "fs_image", UT_ARG_STR }); + +/** + * fs_test_ext4l_ln_norun() - Test ext4l_ln function + * + * Verifies that ext4l can create symbolic links on the filesystem. + * + * Arguments: + * fs_image: Path to the ext4 filesystem image + */ +static int fs_test_ext4l_ln_norun(struct unit_test_state *uts) +{ + const char *fs_image = ut_str(EXT4L_ARG_IMAGE); + static int test_counter; + char link_name[32]; + const char *target = "/testfile.txt"; + loff_t size; + loff_t actread; + char buf[32]; + + 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)); + + /* Use unique symlink names to avoid issues with test re-runs */ + snprintf(link_name, sizeof(link_name), "/testlink%d", test_counter); + test_counter++; + + /* + * Create a symbolic link. ext4l_ln follows U-Boot's ln command + * convention: ext4l_ln(target, linkname) creates linkname pointing + * to target. + */ + ut_assertok(ext4l_ln(target, link_name)); + + /* Verify symlink exists */ + ut_asserteq(1, ext4l_exists(link_name)); + + /* + * Size through symlink should be target file's size (12 bytes), + * since ext4l_resolve_path follows symlinks (like stat, not lstat) + */ + ut_assertok(ext4l_size(link_name, &size)); + ut_asserteq(12, size); + + /* Verify we can read through the symlink */ + memset(buf, '\0', sizeof(buf)); + ut_assertok(ext4l_read(link_name, buf, 0, 0, &actread)); + ut_asserteq(12, actread); + ut_asserteq_str("hello world\n", buf); + + /* Verify creating duplicate returns -EEXIST */ + ut_asserteq(-EEXIST, ext4l_ln(target, link_name)); + + /* Verify creating symlink in non-existent parent returns -ENOENT */ + ut_asserteq(-ENOENT, ext4l_ln(target, "/nonexistent/link")); + + return 0; +} +FS_TEST_ARGS(fs_test_ext4l_ln_norun, UTF_SCAN_FDT | UTF_CONSOLE | UTF_MANUAL, + { "fs_image", UT_ARG_STR }); -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> The ext4l_ln() function returned -EEXIST when creating a symlink where a file already exists. This differs from the old ext4 implementation which deletes any existing file before creating the symlink (like ln -sf behaviour). Update ext4l_ln() to match this behaviour by calling __ext4_unlink() to remove any existing non-directory file before creating the symlink. Directories cannot be replaced with symlinks and return -EISDIR. This allows test_symlink3 to pass. Co-developed-by: Claude <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- fs/ext4l/interface.c | 18 +++++++++++++++--- include/ext4l.h | 5 ++++- test/fs/ext4l.c | 4 ++-- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/fs/ext4l/interface.c b/fs/ext4l/interface.c index c9dd25dd7b4..acd9ba5511e 100644 --- a/fs/ext4l/interface.c +++ b/fs/ext4l/interface.c @@ -1268,9 +1268,21 @@ int ext4l_ln(const char *filename, const char *linkname) return ret; if (dentry->d_inode) { - /* File already exists */ - ret = -EEXIST; - goto out; + /* File already exists - delete it first (like ln -sf) */ + if (S_ISDIR(dentry->d_inode->i_mode)) { + /* Cannot replace a directory with a symlink */ + ret = -EISDIR; + goto out; + } + + ret = __ext4_unlink(dir_dentry->d_inode, &dentry->d_name, + dentry->d_inode, dentry); + if (ret) + goto out; + + /* Release inode to free data blocks */ + iput(dentry->d_inode); + dentry->d_inode = NULL; } /* Create the symlink - filename is what the link points to */ diff --git a/include/ext4l.h b/include/ext4l.h index d0e420c8da2..882a27dad42 100644 --- a/include/ext4l.h +++ b/include/ext4l.h @@ -114,9 +114,12 @@ int ext4l_mkdir(const char *dirname); /** * ext4l_ln() - Create a symbolic link * + * Creates the symlink, replacing any existing file (like ln -sf). + * Refuses to replace a directory. + * * @filename: Path of symlink to create * @target: Target path the symlink points to - * Return: 0 on success, -EEXIST if file already exists, + * Return: 0 on success, -EISDIR if target is a directory, * -ENOTDIR if parent is not a directory, -EROFS if read-only, * negative on other errors */ diff --git a/test/fs/ext4l.c b/test/fs/ext4l.c index 1d46a752f32..c1d10dcc816 100644 --- a/test/fs/ext4l.c +++ b/test/fs/ext4l.c @@ -576,8 +576,8 @@ static int fs_test_ext4l_ln_norun(struct unit_test_state *uts) ut_asserteq(12, actread); ut_asserteq_str("hello world\n", buf); - /* Verify creating duplicate returns -EEXIST */ - ut_asserteq(-EEXIST, ext4l_ln(target, link_name)); + /* Verify creating duplicate succeeds (like ln -sf) */ + ut_assertok(ext4l_ln(target, link_name)); /* Verify creating symlink in non-existent parent returns -ENOENT */ ut_asserteq(-ENOENT, ext4l_ln(target, "/nonexistent/link")); -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> Add ext4l_rename() to rename files and directories, including moves across directories. This uses the Linux ext4_rename() function. Also fix the symlink test to verify reading through symlinks works correctly, since ext4l_resolve_path follows symlinks (stat behavior). Add Python test wrappers for mkdir, ln, and rename tests. Co-developed-by: Claude <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- fs/ext4l/interface.c | 54 +++++++++++++++++++++++++ fs/fs_legacy.c | 2 +- include/ext4l.h | 11 ++++++ test/fs/ext4l.c | 61 +++++++++++++++++++++++++++++ test/py/tests/test_fs/test_ext4l.py | 15 +++++++ 5 files changed, 142 insertions(+), 1 deletion(-) diff --git a/fs/ext4l/interface.c b/fs/ext4l/interface.c index acd9ba5511e..f581c32359e 100644 --- a/fs/ext4l/interface.c +++ b/fs/ext4l/interface.c @@ -1309,6 +1309,60 @@ out: return ret; } +int ext4l_rename(const char *old_path, const char *new_path) +{ + struct dentry *old_dentry, *new_dentry; + struct dentry *old_dir_dentry, *new_dir_dentry; + char *old_path_copy, *new_path_copy; + int ret; + + /* Check new_path before ext4l_resolve_file checks old_path */ + if (!new_path) + return -EINVAL; + + ret = ext4l_resolve_file(old_path, &old_dir_dentry, &old_dentry, + &old_path_copy); + if (ret) + return ret; + + if (!old_dentry->d_inode) { + /* Source file doesn't exist */ + ret = -ENOENT; + goto out_old; + } + + ret = ext4l_resolve_file(new_path, &new_dir_dentry, &new_dentry, + &new_path_copy); + if (ret) + goto out_old; + + /* Perform the rename */ + ret = ext4_rename(&nop_mnt_idmap, old_dir_dentry->d_inode, old_dentry, + new_dir_dentry->d_inode, new_dentry, 0); + if (ret) + goto out_new; + + /* Sync all dirty buffers */ + { + int sync_ret = bh_cache_sync(); + + if (sync_ret) + ret = sync_ret; + /* Commit superblock with updated free counts */ + ext4_commit_super(ext4l_sb); + } + +out_new: + kfree(new_dentry); + kfree(new_dir_dentry); + free(new_path_copy); +out_old: + kfree(old_dentry); + kfree(old_dir_dentry); + free(old_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 a325c2631d4..6e1bb449410 100644 --- a/fs/fs_legacy.c +++ b/fs/fs_legacy.c @@ -291,7 +291,7 @@ static struct fstype_info fstypes[] = { .unlink = ext4l_op_ptr(ext4l_unlink, fs_unlink_unsupported), .mkdir = ext4l_op_ptr(ext4l_mkdir, fs_mkdir_unsupported), .ln = ext4l_op_ptr(ext4l_ln, fs_ln_unsupported), - .rename = fs_rename_unsupported, + .rename = ext4l_op_ptr(ext4l_rename, fs_rename_unsupported), .statfs = ext4l_statfs, }, #endif diff --git a/include/ext4l.h b/include/ext4l.h index 882a27dad42..59990a3a6f4 100644 --- a/include/ext4l.h +++ b/include/ext4l.h @@ -125,6 +125,17 @@ int ext4l_mkdir(const char *dirname); */ int ext4l_ln(const char *filename, const char *target); +/** + * ext4l_rename() - Rename a file or directory + * + * @old_path: Current path of file or directory + * @new_path: New path for file or directory + * Return: 0 on success, -ENOENT if source not found, + * -ENOTDIR if parent is not a directory, -EROFS if read-only, + * negative on other errors + */ +int ext4l_rename(const char *old_path, const char *new_path); + /** * ext4l_get_uuid() - Get the filesystem UUID * diff --git a/test/fs/ext4l.c b/test/fs/ext4l.c index c1d10dcc816..0843ba1d5ba 100644 --- a/test/fs/ext4l.c +++ b/test/fs/ext4l.c @@ -586,3 +586,64 @@ static int fs_test_ext4l_ln_norun(struct unit_test_state *uts) } FS_TEST_ARGS(fs_test_ext4l_ln_norun, UTF_SCAN_FDT | UTF_CONSOLE | UTF_MANUAL, { "fs_image", UT_ARG_STR }); + +/** + * fs_test_ext4l_rename_norun() - Test ext4l_rename function + * + * Verifies that ext4l can rename files and directories on the filesystem. + * + * Arguments: + * fs_image: Path to the ext4 filesystem image + */ +static int fs_test_ext4l_rename_norun(struct unit_test_state *uts) +{ + const char *fs_image = ut_str(EXT4L_ARG_IMAGE); + const char *test_data = "rename test\n"; + size_t test_len = strlen(test_data); + static int test_counter; + char old_name[32], new_name[32], subdir_name[32], moved_name[64]; + loff_t actwrite, 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)); + + /* Use unique names to avoid issues with test re-runs */ + snprintf(old_name, sizeof(old_name), "/renameme%d.txt", test_counter); + snprintf(new_name, sizeof(new_name), "/renamed%d.txt", test_counter); + snprintf(subdir_name, sizeof(subdir_name), "/renamedir%d", test_counter); + snprintf(moved_name, sizeof(moved_name), "%s/moved.txt", subdir_name); + test_counter++; + + /* Create a file to rename */ + ut_assertok(ext4l_write(old_name, (void *)test_data, 0, + test_len, &actwrite)); + ut_asserteq(test_len, actwrite); + + /* Verify file exists */ + ut_asserteq(1, ext4l_exists(old_name)); + + /* Rename the file */ + ut_assertok(ext4l_rename(old_name, new_name)); + + /* Verify old name no longer exists, new name does */ + ut_asserteq(0, ext4l_exists(old_name)); + ut_asserteq(1, ext4l_exists(new_name)); + + /* Verify file size is preserved */ + ut_assertok(ext4l_size(new_name, &size)); + ut_asserteq(test_len, size); + + /* Verify renaming non-existent file returns -ENOENT */ + ut_asserteq(-ENOENT, ext4l_rename("/nonexistent", "/newname")); + + /* Test cross-directory rename */ + ut_assertok(ext4l_mkdir(subdir_name)); + ut_assertok(ext4l_rename(new_name, moved_name)); + ut_asserteq(0, ext4l_exists(new_name)); + ut_asserteq(1, ext4l_exists(moved_name)); + + return 0; +} +FS_TEST_ARGS(fs_test_ext4l_rename_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 edea38f7faf..0930ebd01ea 100644 --- a/test/py/tests/test_fs/test_ext4l.py +++ b/test/py/tests/test_fs/test_ext4l.py @@ -128,3 +128,18 @@ class TestExt4l: """Test that ext4l can delete files.""" with ubman.log.section('Test ext4l unlink'): ubman.run_ut('fs', 'fs_test_ext4l_unlink', fs_image=ext4_image) + + def test_mkdir(self, ubman, ext4_image): + """Test that ext4l can create directories.""" + with ubman.log.section('Test ext4l mkdir'): + ubman.run_ut('fs', 'fs_test_ext4l_mkdir', fs_image=ext4_image) + + def test_ln(self, ubman, ext4_image): + """Test that ext4l can create symbolic links.""" + with ubman.log.section('Test ext4l ln'): + ubman.run_ut('fs', 'fs_test_ext4l_ln', fs_image=ext4_image) + + def test_rename(self, ubman, ext4_image): + """Test that ext4l can rename files and directories.""" + with ubman.log.section('Test ext4l rename'): + ubman.run_ut('fs', 'fs_test_ext4l_rename', fs_image=ext4_image) -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> The ext4l driver supports metadata_csum checksums, unlike the old ext4 driver. Only disable metadata_csum when CONFIG_FS_EXT4L is not enabled, allowing ext4l to use modern ext4 filesystem features. Co-developed-by: Claude <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- test/py/tests/fs_helper.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/test/py/tests/fs_helper.py b/test/py/tests/fs_helper.py index 3c4c6e2df6f..ccde6683534 100644 --- a/test/py/tests/fs_helper.py +++ b/test/py/tests/fs_helper.py @@ -174,10 +174,15 @@ class FsHelper: stdout=DEVNULL if self.quiet else None) if self.fs_type.startswith('ext'): - sb_content = check_output(f'tune2fs -l {fs_img}', - shell=True).decode() - if 'metadata_csum' in sb_content: - check_call(f'tune2fs -O ^metadata_csum {fs_img}', shell=True) + # ext4l supports metadata_csum; the old ext4 driver does not. + # Only disable metadata_csum when using the old driver. + if self.config and self.config.buildconfig.get( + 'config_fs_ext4l', 'n') != 'y': + sb_content = check_output(f'tune2fs -l {fs_img}', + shell=True).decode() + if 'metadata_csum' in sb_content: + check_call(f'tune2fs -O ^metadata_csum {fs_img}', + shell=True) elif fs_lnxtype == 'exfat': check_call(f'fattools cp {self.srcdir}/* {fs_img}', shell=True) elif self.srcdir and os.listdir(self.srcdir): -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> Enable the new ext4l filesystem (Linux ext4 port) for sandbox testing. Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- configs/sandbox_defconfig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/configs/sandbox_defconfig b/configs/sandbox_defconfig index e9ac6894d1a..e55aef8e7f9 100644 --- a/configs/sandbox_defconfig +++ b/configs/sandbox_defconfig @@ -358,6 +358,8 @@ CONFIG_WDT_FTWDT010=y CONFIG_FS_CBFS=y CONFIG_FS_EXFAT=y CONFIG_FS_CRAMFS=y +CONFIG_FS_EXT4L=y +CONFIG_EXT4_WRITE=y CONFIG_ADDR_MAP=y CONFIG_PANIC_HANG=y CONFIG_CMD_DHRYSTONE=y -- 2.43.0
participants (1)
-
Simon Glass