From 4c20469b78bde36e92b5c30ce8489b8e83aee939 Mon Sep 17 00:00:00 2001 From: Marcel Farres Date: Thu, 18 Dec 2025 10:12:10 -0500 Subject: [PATCH 1/2] fix: add Sync() calls to persist filesystem changes to storage - Add Sync() after Format, Unmount, Remove, Rename, Mkdir - Add Sync() after File.Close, File.Sync, File.Truncate - Sync on OpenFile with O_CREATE/O_TRUNC flags - Fix cache invalidation in lfs_bd_flush - Add TestDirectoryPersistence and NestedDirectories tests" --- littlefs/go_lfs.go | 88 ++++++++++++++++++++++++++++++++++++----- littlefs/go_lfs_test.go | 62 ++++++++++++++++++++++++++++- littlefs/lfs.c | 4 ++ 3 files changed, 144 insertions(+), 10 deletions(-) diff --git a/littlefs/go_lfs.go b/littlefs/go_lfs.go index 1e94f5d..fa02069 100644 --- a/littlefs/go_lfs.go +++ b/littlefs/go_lfs.go @@ -186,24 +186,48 @@ func (l *LFS) Mount() error { } func (l *LFS) Format() error { - return errval(C.lfs_format(l.lfs, l.cfg)) + if err := errval(C.lfs_format(l.lfs, l.cfg)); err != nil { + return err + } + if syncer, ok := l.dev.(tinyfs.Syncer); ok { + return syncer.Sync() + } + return nil } func (l *LFS) Unmount() error { - return errval(C.lfs_unmount(l.lfs)) + if err := errval(C.lfs_unmount(l.lfs)); err != nil { + return err + } + if syncer, ok := l.dev.(tinyfs.Syncer); ok { + return syncer.Sync() + } + return nil } func (l *LFS) Remove(path string) error { cs := cstring(path) defer C.free(unsafe.Pointer(cs)) - return errval(C.lfs_remove(l.lfs, cs)) + if err := errval(C.lfs_remove(l.lfs, cs)); err != nil { + return err + } + if syncer, ok := l.dev.(tinyfs.Syncer); ok { + return syncer.Sync() + } + return nil } func (l *LFS) Rename(oldPath string, newPath string) error { cs1, cs2 := cstring(oldPath), cstring(newPath) defer C.free(unsafe.Pointer(cs1)) defer C.free(unsafe.Pointer(cs2)) - return errval(C.lfs_rename(l.lfs, cs1, cs2)) + if err := errval(C.lfs_rename(l.lfs, cs1, cs2)); err != nil { + return err + } + if syncer, ok := l.dev.(tinyfs.Syncer); ok { + return syncer.Sync() + } + return nil } func (l *LFS) Stat(path string) (os.FileInfo, error) { @@ -223,7 +247,13 @@ func (l *LFS) Stat(path string) (os.FileInfo, error) { func (l *LFS) Mkdir(path string, _ os.FileMode) error { cs := (*C.char)(cstring(path)) defer C.free(unsafe.Pointer(cs)) - return errval(C.lfs_mkdir(l.lfs, cs)) + if err := errval(C.lfs_mkdir(l.lfs, cs)); err != nil { + return err + } + if syncer, ok := l.dev.(tinyfs.Syncer); ok { + return syncer.Sync() + } + return nil } func (l *LFS) Open(path string) (tinyfs.File, error) { @@ -261,6 +291,24 @@ func (l *LFS) OpenFile(path string, flags int) (tinyfs.File, error) { return nil, err } + if flags&(os.O_CREATE|os.O_TRUNC) != 0 { + if flags&os.O_TRUNC != 0 { + if err := errval(C.lfs_file_sync(l.lfs, file.fileptr())); err != nil { + file.Close() + return nil, err + } + } + if syncer, ok := l.dev.(tinyfs.Syncer); ok { + if err := syncer.Sync(); err != nil { + // Should we close and return error? + // Maybe just log? But we can't log. + // For now let's just return error and close. + file.Close() + return nil, err + } + } + } + return file, nil } @@ -305,14 +353,21 @@ func (f *File) Close() error { C.free(f.hndl) f.hndl = nil }() + var err error switch f.typ { case fileTypeReg: - return errval(C.lfs_file_close(f.lfs.lfs, f.fileptr())) + err = errval(C.lfs_file_close(f.lfs.lfs, f.fileptr())) case fileTypeDir: - return errval(C.lfs_dir_close(f.lfs.lfs, f.dirptr())) + err = errval(C.lfs_dir_close(f.lfs.lfs, f.dirptr())) default: panic("lfs: unknown typ for file handle") } + if err != nil { + return err + } + if syncer, ok := f.lfs.dev.(tinyfs.Syncer); ok { + return syncer.Sync() + } } return nil } @@ -373,12 +428,27 @@ func (f *File) Stat() (os.FileInfo, error) { // Sync synchronizes to storage so that any pending writes are written out. func (f *File) Sync() error { - return errval(C.lfs_file_sync(f.lfs.lfs, f.fileptr())) + if err := errval(C.lfs_file_sync(f.lfs.lfs, f.fileptr())); err != nil { + return err + } + if syncer, ok := f.lfs.dev.(tinyfs.Syncer); ok { + return syncer.Sync() + } + return nil } // Truncate the size of the file to the specified size func (f *File) Truncate(size uint32) error { - return errval(C.lfs_file_truncate(f.lfs.lfs, f.fileptr(), C.lfs_off_t(size))) + if err := errval(C.lfs_file_truncate(f.lfs.lfs, f.fileptr(), C.lfs_off_t(size))); err != nil { + return err + } + if err := errval(C.lfs_file_sync(f.lfs.lfs, f.fileptr())); err != nil { + return err + } + if syncer, ok := f.lfs.dev.(tinyfs.Syncer); ok { + return syncer.Sync() + } + return nil } func (f *File) Write(buf []byte) (n int, err error) { diff --git a/littlefs/go_lfs_test.go b/littlefs/go_lfs_test.go index 9d41735..41f68a0 100644 --- a/littlefs/go_lfs_test.go +++ b/littlefs/go_lfs_test.go @@ -211,7 +211,15 @@ func TestDirectories(t *testing.T) { }) t.Run("NestedDirectories", func(t *testing.T) { - + check(t, fs.Mkdir("parent", 0777)) + check(t, fs.Mkdir("parent/child", 0777)) + check(t, fs.Mkdir("parent/child/grandchild", 0777)) + // Verify they exist + info, err := fs.Stat("parent/child/grandchild") + check(t, err) + if !info.IsDir() { + t.Error("expected directory") + } }) t.Run("MultiBlockDirectory", func(t *testing.T) { @@ -337,3 +345,55 @@ func check(t *testing.T, err error) { t.Fatal(err) } } + +// TestDirectoryPersistence verifies that directories persist after unmount/remount. +// This is a regression test for the Sync() fix that ensures filesystem changes +// are flushed to the underlying block device. +func TestDirectoryPersistence(t *testing.T) { + bd := tinyfs.NewMemoryDevice(testPageSize, testBlockSize, testBlockCount) + fs := New(bd).Configure(defaultConfig) + + // Format and mount + if err := fs.Format(); err != nil { + t.Fatalf("Format failed: %v", err) + } + if err := fs.Mount(); err != nil { + t.Fatalf("Mount failed: %v", err) + } + + // Create directory + dirName := "persist_test" + if err := fs.Mkdir(dirName, 0755); err != nil { + t.Fatalf("Mkdir failed: %v", err) + } + + // Verify immediate existence + info, err := fs.Stat(dirName) + if err != nil { + t.Fatalf("Stat after mkdir failed: %v", err) + } + if !info.IsDir() { + t.Fatalf("Expected directory, got file") + } + + // Unmount + if err := fs.Unmount(); err != nil { + t.Fatalf("Unmount failed: %v", err) + } + + // Create new filesystem instance and remount (simulates reboot) + fs2 := New(bd).Configure(defaultConfig) + if err := fs2.Mount(); err != nil { + t.Fatalf("Remount failed: %v", err) + } + defer fs2.Unmount() + + // Verify directory persisted + info2, err := fs2.Stat(dirName) + if err != nil { + t.Fatalf("Directory '%s' lost after remount: %v", dirName, err) + } + if !info2.IsDir() { + t.Fatalf("'%s' exists but is not a directory", dirName) + } +} diff --git a/littlefs/lfs.c b/littlefs/lfs.c index 95c241e..8f6aff3 100644 --- a/littlefs/lfs.c +++ b/littlefs/lfs.c @@ -185,6 +185,10 @@ static int lfs_bd_flush(lfs_t *lfs, return err; } + if (pcache->block != LFS_BLOCK_NULL && pcache->block == rcache->block) { + lfs_cache_drop(lfs, rcache); + } + if (validate) { // check data on disk lfs_cache_drop(lfs, rcache); From af22fd97fb51ffd50f8d492a58e8a5dc5ef9f862 Mon Sep 17 00:00:00 2001 From: Marcel Farres Date: Tue, 10 Feb 2026 14:19:39 -0500 Subject: [PATCH 2/2] fix: work around LLVM -O2 miscompilation of lfs_dir_fetchmatch LLVM's ThinLTO at -O2 miscompiles lfs_dir_fetchmatch(), causing directory metadata scans to return incorrect results. mkdir succeeds but subsequent stat/open fails with 'no directory entry'. The bug does not appear at -Oz (TinyGo's default), which is why the earlier Sync() approach seemed to help. Add __attribute__((optnone)) to lfs_dir_fetchmatch to prevent the miscompilation. Targeted approaches (volatile, noinline, memory barriers) were tested but none work - the issue is in how -O2 transforms the function's complex control flow as a whole. - littlefs/lfs.c: optnone on lfs_dir_fetchmatch, revert cache fix - littlefs/go_lfs.go: revert Sync() calls (not the actual fix) - littlefs/go_lfs_callbacks.go: fix uint32 size parameter - littlefs/go_lfs_test.go: directory persistence tests - examples/sd_mkdir_test/: hardware test firmware for SD card --- examples/sd_mkdir_test/main.go | 395 +++++++++++++++++++++++++++++++++ littlefs/go_lfs.go | 88 +------- littlefs/go_lfs_callbacks.go | 12 +- littlefs/go_lfs_test.go | 2 - littlefs/lfs.c | 10 +- 5 files changed, 419 insertions(+), 88 deletions(-) create mode 100644 examples/sd_mkdir_test/main.go diff --git a/examples/sd_mkdir_test/main.go b/examples/sd_mkdir_test/main.go new file mode 100644 index 0000000..089db11 --- /dev/null +++ b/examples/sd_mkdir_test/main.go @@ -0,0 +1,395 @@ +// SD Card LittleFS Console — interactive serial console for LittleFS on SD card. +// +// This is a standalone example (does not use the shared console package) that +// demonstrates LittleFS on an SD card over SPI. It provides basic filesystem +// commands over the USB serial port at 115200 baud. +// +// Build for Grand Central M4: +// +// tinygo build -target=grandcentral-m4 -stack-size=16KB -o firmware.uf2 . +// +// Commands: HELP, FORMAT, MOUNT, UNMOUNT, LS, MKDIR, WRITE, CAT, RM, +// +// TEST (basic mkdir), TEST2 (nested dirs), TEST3 (files), TEST4 (stress) +package main + +import ( + "fmt" + "machine" + "os" + "strings" + "time" + + "tinygo.org/x/drivers/sdcard" + "tinygo.org/x/tinyfs/littlefs" +) + +var ( + sd *sdcard.Device + fs *littlefs.LFS + mounted bool +) + +func main() { + time.Sleep(2 * time.Second) + fmt.Println("=== SD Card LittleFS Console ===") + fmt.Println("Type HELP for commands") + + // Init SPI + SD card + machine.SPI1.Configure(machine.SPIConfig{ + SCK: machine.SDCARD_SCK_PIN, SDO: machine.SDCARD_SDO_PIN, + SDI: machine.SDCARD_SDI_PIN, Frequency: 1000000, + }) + machine.SDCARD_CS_PIN.Configure(machine.PinConfig{Mode: machine.PinOutput}) + machine.SDCARD_CS_PIN.High() + + dev := sdcard.New(machine.SPI1, + machine.SDCARD_SCK_PIN, machine.SDCARD_SDO_PIN, + machine.SDCARD_SDI_PIN, machine.SDCARD_CS_PIN) + sd = &dev + if err := sd.Configure(); err != nil { + fmt.Println("SD init failed:", err) + select {} + } + + fs = littlefs.New(sd) + fs.Configure(&littlefs.Config{ + CacheSize: 512, LookaheadSize: 512, BlockCycles: 100, + }) + if err := fs.Mount(); err != nil { + fmt.Println("Mount failed (try FORMAT):", err) + } else { + mounted = true + fmt.Println("Mounted OK") + } + + // Command loop + var buf strings.Builder + for { + if machine.Serial.Buffered() > 0 { + c, _ := machine.Serial.ReadByte() + if c == '\n' || c == '\r' { + if cmd := strings.TrimSpace(buf.String()); cmd != "" { + run(cmd) + } + buf.Reset() + } else { + buf.WriteByte(c) + } + } + time.Sleep(10 * time.Millisecond) + } +} + +func run(cmd string) { + parts := strings.SplitN(cmd, " ", 2) + action := strings.ToUpper(parts[0]) + arg := "" + if len(parts) > 1 { + arg = strings.TrimSpace(parts[1]) + } + fmt.Println(">>>", cmd) + + switch action { + case "HELP": + fmt.Println(" FORMAT MOUNT UNMOUNT LS [path] MKDIR path") + fmt.Println(" WRITE path data CAT path RM path") + fmt.Println(" TEST — 10x mkdir+stat (basic bug trigger)") + fmt.Println(" TEST2 — nested directories (parent/child/grandchild)") + fmt.Println(" TEST3 — files inside directories") + fmt.Println(" TEST4 — 50x high-frequency mkdir+stat stress") + case "FORMAT": + if mounted { + fs.Unmount() + mounted = false + } + if err := fs.Format(); err != nil { + fmt.Println("Format FAILED:", err) + return + } + if err := fs.Mount(); err != nil { + fmt.Println("Mount FAILED:", err) + return + } + mounted = true + fmt.Println("OK") + case "MOUNT": + if mounted { + fmt.Println("already mounted") + return + } + if err := fs.Mount(); err != nil { + fmt.Println("FAILED:", err) + return + } + mounted = true + fmt.Println("OK") + case "UNMOUNT": + if !mounted { + fmt.Println("not mounted") + return + } + if err := fs.Unmount(); err != nil { + fmt.Println("FAILED:", err) + return + } + mounted = false + fmt.Println("OK") + case "LS": + if !requireMount() { + return + } + if arg == "" { + arg = "/" + } + dir, err := fs.Open(arg) + if err != nil { + fmt.Println("FAILED:", err) + return + } + entries, _ := dir.Readdir(-1) + dir.Close() + for _, e := range entries { + t := "f" + if e.IsDir() { + t = "d" + } + fmt.Printf(" %s %8d %s\n", t, e.Size(), e.Name()) + } + if len(entries) == 0 { + fmt.Println(" (empty)") + } + case "MKDIR": + if !requireMount() || requireArg(arg) { + return + } + if err := fs.Mkdir(arg, 0755); err != nil { + fmt.Println("FAILED:", err) + return + } + fmt.Println("OK") + case "WRITE": + if !requireMount() { + return + } + wp := strings.SplitN(arg, " ", 2) + if len(wp) < 2 { + fmt.Println("Usage: WRITE path data") + return + } + f, err := fs.OpenFile(wp[0], os.O_CREATE|os.O_WRONLY|os.O_TRUNC) + if err != nil { + fmt.Println("FAILED:", err) + return + } + f.Write([]byte(wp[1])) + f.Close() + fmt.Println("OK") + case "CAT": + if !requireMount() || requireArg(arg) { + return + } + f, err := fs.Open(arg) + if err != nil { + fmt.Println("FAILED:", err) + return + } + buf := make([]byte, 256) + n, _ := f.Read(buf) + f.Close() + fmt.Printf("%s\n", buf[:n]) + case "RM": + if !requireMount() || requireArg(arg) { + return + } + if err := fs.Remove(arg); err != nil { + fmt.Println("FAILED:", err) + return + } + fmt.Println("OK") + case "TEST": + testMkdir() + case "TEST2": + testNested() + case "TEST3": + testFiles() + case "TEST4": + testStress() + default: + fmt.Println("Unknown command. Type HELP.") + } +} + +func requireMount() bool { + if !mounted { + fmt.Println("not mounted") + } + return mounted +} + +func requireArg(arg string) bool { + if arg == "" { + fmt.Println("missing argument") + return true + } + return false +} + +// testMkdir exercises mkdir+stat in a loop — the pattern that triggers the +// LLVM -O2 miscompilation bug in lfs_dir_fetchmatch (see littlefs/lfs.c). +// At -O2 without the optnone fix, every stat fails with "no directory entry". +func testMkdir() { + if !requireMount() { + return + } + const n = 10 + fail := 0 + for i := 0; i < n; i++ { + p := fmt.Sprintf("/t_%d", i) + if err := fs.Mkdir(p, 0755); err != nil { + fmt.Printf(" mkdir %s: %v\n", p, err) + fail++ + continue + } + if info, err := fs.Stat(p); err != nil { + fmt.Printf(" FAIL %s: stat: %v\n", p, err) + fail++ + } else if !info.IsDir() { + fmt.Printf(" FAIL %s: not a dir\n", p) + fail++ + } else { + fmt.Printf(" OK %s\n", p) + } + } + for i := 0; i < n; i++ { + fs.Remove(fmt.Sprintf("/t_%d", i)) + } + if fail > 0 { + fmt.Printf("RESULT: %d/%d FAILED\n", fail, n) + } else { + fmt.Printf("RESULT: %d/%d passed\n", n, n) + } +} + +// testNested creates parent → child → grandchild directories and verifies +// each level with stat, then cleans up in reverse order. +func testNested() { + if !requireMount() { + return + } + levels := []string{"/na", "/na/nb", "/na/nb/nc"} + fail := 0 + for _, p := range levels { + if err := fs.Mkdir(p, 0755); err != nil { + fmt.Printf(" mkdir %s: %v\n", p, err) + fail++ + continue + } + if info, err := fs.Stat(p); err != nil { + fmt.Printf(" FAIL %s: stat: %v\n", p, err) + fail++ + } else if !info.IsDir() { + fmt.Printf(" FAIL %s: not a dir\n", p) + fail++ + } else { + fmt.Printf(" OK %s\n", p) + } + } + // cleanup reverse + for i := len(levels) - 1; i >= 0; i-- { + fs.Remove(levels[i]) + } + if fail > 0 { + fmt.Printf("RESULT: %d/%d FAILED\n", fail, len(levels)) + } else { + fmt.Printf("RESULT: %d/%d passed\n", len(levels), len(levels)) + } +} + +// testFiles creates directories and writes+reads files inside them. +func testFiles() { + if !requireMount() { + return + } + fail := 0 + dirs := []string{"/fd1", "/fd1/fd2"} + for _, d := range dirs { + if err := fs.Mkdir(d, 0755); err != nil { + fmt.Printf(" mkdir %s: %v\n", d, err) + fail++ + } + } + // write files inside dirs + files := map[string]string{ + "/fd1/a.txt": "hello", + "/fd1/fd2/b.txt": "world", + } + for path, data := range files { + f, err := fs.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC) + if err != nil { + fmt.Printf(" FAIL write %s: %v\n", path, err) + fail++ + continue + } + f.Write([]byte(data)) + f.Close() + // read back + rf, err := fs.Open(path) + if err != nil { + fmt.Printf(" FAIL open %s: %v\n", path, err) + fail++ + continue + } + buf := make([]byte, 64) + n, _ := rf.Read(buf) + rf.Close() + if string(buf[:n]) != data { + fmt.Printf(" FAIL %s: got %q want %q\n", path, string(buf[:n]), data) + fail++ + } else { + fmt.Printf(" OK %s\n", path) + } + } + // cleanup + for path := range files { + fs.Remove(path) + } + for i := len(dirs) - 1; i >= 0; i-- { + fs.Remove(dirs[i]) + } + total := len(files) + if fail > 0 { + fmt.Printf("RESULT: %d/%d FAILED\n", fail, total) + } else { + fmt.Printf("RESULT: %d/%d passed\n", total, total) + } +} + +// testStress does 50 rapid mkdir+stat cycles to stress-test the fix. +func testStress() { + if !requireMount() { + return + } + const n = 50 + fail := 0 + for i := 0; i < n; i++ { + p := fmt.Sprintf("/s_%d", i) + if err := fs.Mkdir(p, 0755); err != nil { + fmt.Printf(" mkdir %s: %v\n", p, err) + fail++ + continue + } + if _, err := fs.Stat(p); err != nil { + fmt.Printf(" FAIL %s: %v\n", p, err) + fail++ + } + } + for i := 0; i < n; i++ { + fs.Remove(fmt.Sprintf("/s_%d", i)) + } + if fail > 0 { + fmt.Printf("RESULT: %d/%d FAILED\n", fail, n) + } else { + fmt.Printf("RESULT: %d/%d passed\n", n, n) + } +} diff --git a/littlefs/go_lfs.go b/littlefs/go_lfs.go index fa02069..1e94f5d 100644 --- a/littlefs/go_lfs.go +++ b/littlefs/go_lfs.go @@ -186,48 +186,24 @@ func (l *LFS) Mount() error { } func (l *LFS) Format() error { - if err := errval(C.lfs_format(l.lfs, l.cfg)); err != nil { - return err - } - if syncer, ok := l.dev.(tinyfs.Syncer); ok { - return syncer.Sync() - } - return nil + return errval(C.lfs_format(l.lfs, l.cfg)) } func (l *LFS) Unmount() error { - if err := errval(C.lfs_unmount(l.lfs)); err != nil { - return err - } - if syncer, ok := l.dev.(tinyfs.Syncer); ok { - return syncer.Sync() - } - return nil + return errval(C.lfs_unmount(l.lfs)) } func (l *LFS) Remove(path string) error { cs := cstring(path) defer C.free(unsafe.Pointer(cs)) - if err := errval(C.lfs_remove(l.lfs, cs)); err != nil { - return err - } - if syncer, ok := l.dev.(tinyfs.Syncer); ok { - return syncer.Sync() - } - return nil + return errval(C.lfs_remove(l.lfs, cs)) } func (l *LFS) Rename(oldPath string, newPath string) error { cs1, cs2 := cstring(oldPath), cstring(newPath) defer C.free(unsafe.Pointer(cs1)) defer C.free(unsafe.Pointer(cs2)) - if err := errval(C.lfs_rename(l.lfs, cs1, cs2)); err != nil { - return err - } - if syncer, ok := l.dev.(tinyfs.Syncer); ok { - return syncer.Sync() - } - return nil + return errval(C.lfs_rename(l.lfs, cs1, cs2)) } func (l *LFS) Stat(path string) (os.FileInfo, error) { @@ -247,13 +223,7 @@ func (l *LFS) Stat(path string) (os.FileInfo, error) { func (l *LFS) Mkdir(path string, _ os.FileMode) error { cs := (*C.char)(cstring(path)) defer C.free(unsafe.Pointer(cs)) - if err := errval(C.lfs_mkdir(l.lfs, cs)); err != nil { - return err - } - if syncer, ok := l.dev.(tinyfs.Syncer); ok { - return syncer.Sync() - } - return nil + return errval(C.lfs_mkdir(l.lfs, cs)) } func (l *LFS) Open(path string) (tinyfs.File, error) { @@ -291,24 +261,6 @@ func (l *LFS) OpenFile(path string, flags int) (tinyfs.File, error) { return nil, err } - if flags&(os.O_CREATE|os.O_TRUNC) != 0 { - if flags&os.O_TRUNC != 0 { - if err := errval(C.lfs_file_sync(l.lfs, file.fileptr())); err != nil { - file.Close() - return nil, err - } - } - if syncer, ok := l.dev.(tinyfs.Syncer); ok { - if err := syncer.Sync(); err != nil { - // Should we close and return error? - // Maybe just log? But we can't log. - // For now let's just return error and close. - file.Close() - return nil, err - } - } - } - return file, nil } @@ -353,21 +305,14 @@ func (f *File) Close() error { C.free(f.hndl) f.hndl = nil }() - var err error switch f.typ { case fileTypeReg: - err = errval(C.lfs_file_close(f.lfs.lfs, f.fileptr())) + return errval(C.lfs_file_close(f.lfs.lfs, f.fileptr())) case fileTypeDir: - err = errval(C.lfs_dir_close(f.lfs.lfs, f.dirptr())) + return errval(C.lfs_dir_close(f.lfs.lfs, f.dirptr())) default: panic("lfs: unknown typ for file handle") } - if err != nil { - return err - } - if syncer, ok := f.lfs.dev.(tinyfs.Syncer); ok { - return syncer.Sync() - } } return nil } @@ -428,27 +373,12 @@ func (f *File) Stat() (os.FileInfo, error) { // Sync synchronizes to storage so that any pending writes are written out. func (f *File) Sync() error { - if err := errval(C.lfs_file_sync(f.lfs.lfs, f.fileptr())); err != nil { - return err - } - if syncer, ok := f.lfs.dev.(tinyfs.Syncer); ok { - return syncer.Sync() - } - return nil + return errval(C.lfs_file_sync(f.lfs.lfs, f.fileptr())) } // Truncate the size of the file to the specified size func (f *File) Truncate(size uint32) error { - if err := errval(C.lfs_file_truncate(f.lfs.lfs, f.fileptr(), C.lfs_off_t(size))); err != nil { - return err - } - if err := errval(C.lfs_file_sync(f.lfs.lfs, f.fileptr())); err != nil { - return err - } - if syncer, ok := f.lfs.dev.(tinyfs.Syncer); ok { - return syncer.Sync() - } - return nil + return errval(C.lfs_file_truncate(f.lfs.lfs, f.fileptr(), C.lfs_off_t(size))) } func (f *File) Write(buf []byte) (n int, err error) { diff --git a/littlefs/go_lfs_callbacks.go b/littlefs/go_lfs_callbacks.go index e79d08f..3001b6b 100644 --- a/littlefs/go_lfs_callbacks.go +++ b/littlefs/go_lfs_callbacks.go @@ -14,8 +14,13 @@ const ( debug bool = false ) +// go_lfs_block_device_read is the CGo callback for LittleFS block reads. +// The size parameter must be uint32 (not int) to match C's lfs_size_t (uint32_t). +// On 64-bit hosts, Go int is 64 bits but CGo only writes the lower 32 bits, +// leaving garbage in the upper half — causing slice-bounds panics. +// //export go_lfs_block_device_read -func go_lfs_block_device_read(ctx unsafe.Pointer, block uint32, offset uint32, buf unsafe.Pointer, size int) int { +func go_lfs_block_device_read(ctx unsafe.Pointer, block uint32, offset uint32, buf unsafe.Pointer, size uint32) int { if debug { fmt.Printf("go_lfs_block_device_read: %v, %v, %v, %v, %v\n", ctx, block, offset, buf, size) } @@ -26,8 +31,11 @@ func go_lfs_block_device_read(ctx unsafe.Pointer, block uint32, offset uint32, b return go_lfs_block_errval("read", err) } +// go_lfs_block_device_prog is the CGo callback for LittleFS block writes. +// See go_lfs_block_device_read for why size must be uint32. +// //export go_lfs_block_device_prog -func go_lfs_block_device_prog(ctx unsafe.Pointer, block uint32, offset uint32, buf unsafe.Pointer, size int) int { +func go_lfs_block_device_prog(ctx unsafe.Pointer, block uint32, offset uint32, buf unsafe.Pointer, size uint32) int { if debug { fmt.Printf("go_lfs_block_device_prog: %v, %v, %v, %v, %v\n", ctx, block, offset, buf, size) } diff --git a/littlefs/go_lfs_test.go b/littlefs/go_lfs_test.go index 41f68a0..33fbc47 100644 --- a/littlefs/go_lfs_test.go +++ b/littlefs/go_lfs_test.go @@ -347,8 +347,6 @@ func check(t *testing.T, err error) { } // TestDirectoryPersistence verifies that directories persist after unmount/remount. -// This is a regression test for the Sync() fix that ensures filesystem changes -// are flushed to the underlying block device. func TestDirectoryPersistence(t *testing.T) { bd := tinyfs.NewMemoryDevice(testPageSize, testBlockSize, testBlockCount) fs := New(bd).Configure(defaultConfig) diff --git a/littlefs/lfs.c b/littlefs/lfs.c index 8f6aff3..f281f83 100644 --- a/littlefs/lfs.c +++ b/littlefs/lfs.c @@ -185,10 +185,6 @@ static int lfs_bd_flush(lfs_t *lfs, return err; } - if (pcache->block != LFS_BLOCK_NULL && pcache->block == rcache->block) { - lfs_cache_drop(lfs, rcache); - } - if (validate) { // check data on disk lfs_cache_drop(lfs, rcache); @@ -1073,7 +1069,11 @@ static int lfs_dir_traverse(lfs_t *lfs, } #endif -static lfs_stag_t lfs_dir_fetchmatch(lfs_t *lfs, +// Workaround for LLVM bug: this function gets miscompiled at -O2 and above, +// causing directory metadata scans to return incorrect results (mkdir succeeds +// but subsequent stat/open can't find the entry). Disabling optimization here +// prevents the miscompilation while keeping the rest of lfs.c fully optimized. +static __attribute__((optnone)) lfs_stag_t lfs_dir_fetchmatch(lfs_t *lfs, lfs_mdir_t *dir, const lfs_block_t pair[2], lfs_tag_t fmask, lfs_tag_t ftag, uint16_t *id, int (*cb)(void *data, lfs_tag_t tag, const void *buffer), void *data) {