Skip to content

Commit 5abb3c9

Browse files
marcelfarresdeadprogram
authored andcommitted
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
1 parent dcf2013 commit 5abb3c9

File tree

5 files changed

+419
-88
lines changed

5 files changed

+419
-88
lines changed

examples/sd_mkdir_test/main.go

Lines changed: 395 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,395 @@
1+
// SD Card LittleFS Console — interactive serial console for LittleFS on SD card.
2+
//
3+
// This is a standalone example (does not use the shared console package) that
4+
// demonstrates LittleFS on an SD card over SPI. It provides basic filesystem
5+
// commands over the USB serial port at 115200 baud.
6+
//
7+
// Build for Grand Central M4:
8+
//
9+
// tinygo build -target=grandcentral-m4 -stack-size=16KB -o firmware.uf2 .
10+
//
11+
// Commands: HELP, FORMAT, MOUNT, UNMOUNT, LS, MKDIR, WRITE, CAT, RM,
12+
//
13+
// TEST (basic mkdir), TEST2 (nested dirs), TEST3 (files), TEST4 (stress)
14+
package main
15+
16+
import (
17+
"fmt"
18+
"machine"
19+
"os"
20+
"strings"
21+
"time"
22+
23+
"tinygo.org/x/drivers/sdcard"
24+
"tinygo.org/x/tinyfs/littlefs"
25+
)
26+
27+
var (
28+
sd *sdcard.Device
29+
fs *littlefs.LFS
30+
mounted bool
31+
)
32+
33+
func main() {
34+
time.Sleep(2 * time.Second)
35+
fmt.Println("=== SD Card LittleFS Console ===")
36+
fmt.Println("Type HELP for commands")
37+
38+
// Init SPI + SD card
39+
machine.SPI1.Configure(machine.SPIConfig{
40+
SCK: machine.SDCARD_SCK_PIN, SDO: machine.SDCARD_SDO_PIN,
41+
SDI: machine.SDCARD_SDI_PIN, Frequency: 1000000,
42+
})
43+
machine.SDCARD_CS_PIN.Configure(machine.PinConfig{Mode: machine.PinOutput})
44+
machine.SDCARD_CS_PIN.High()
45+
46+
dev := sdcard.New(machine.SPI1,
47+
machine.SDCARD_SCK_PIN, machine.SDCARD_SDO_PIN,
48+
machine.SDCARD_SDI_PIN, machine.SDCARD_CS_PIN)
49+
sd = &dev
50+
if err := sd.Configure(); err != nil {
51+
fmt.Println("SD init failed:", err)
52+
select {}
53+
}
54+
55+
fs = littlefs.New(sd)
56+
fs.Configure(&littlefs.Config{
57+
CacheSize: 512, LookaheadSize: 512, BlockCycles: 100,
58+
})
59+
if err := fs.Mount(); err != nil {
60+
fmt.Println("Mount failed (try FORMAT):", err)
61+
} else {
62+
mounted = true
63+
fmt.Println("Mounted OK")
64+
}
65+
66+
// Command loop
67+
var buf strings.Builder
68+
for {
69+
if machine.Serial.Buffered() > 0 {
70+
c, _ := machine.Serial.ReadByte()
71+
if c == '\n' || c == '\r' {
72+
if cmd := strings.TrimSpace(buf.String()); cmd != "" {
73+
run(cmd)
74+
}
75+
buf.Reset()
76+
} else {
77+
buf.WriteByte(c)
78+
}
79+
}
80+
time.Sleep(10 * time.Millisecond)
81+
}
82+
}
83+
84+
func run(cmd string) {
85+
parts := strings.SplitN(cmd, " ", 2)
86+
action := strings.ToUpper(parts[0])
87+
arg := ""
88+
if len(parts) > 1 {
89+
arg = strings.TrimSpace(parts[1])
90+
}
91+
fmt.Println(">>>", cmd)
92+
93+
switch action {
94+
case "HELP":
95+
fmt.Println(" FORMAT MOUNT UNMOUNT LS [path] MKDIR path")
96+
fmt.Println(" WRITE path data CAT path RM path")
97+
fmt.Println(" TEST — 10x mkdir+stat (basic bug trigger)")
98+
fmt.Println(" TEST2 — nested directories (parent/child/grandchild)")
99+
fmt.Println(" TEST3 — files inside directories")
100+
fmt.Println(" TEST4 — 50x high-frequency mkdir+stat stress")
101+
case "FORMAT":
102+
if mounted {
103+
fs.Unmount()
104+
mounted = false
105+
}
106+
if err := fs.Format(); err != nil {
107+
fmt.Println("Format FAILED:", err)
108+
return
109+
}
110+
if err := fs.Mount(); err != nil {
111+
fmt.Println("Mount FAILED:", err)
112+
return
113+
}
114+
mounted = true
115+
fmt.Println("OK")
116+
case "MOUNT":
117+
if mounted {
118+
fmt.Println("already mounted")
119+
return
120+
}
121+
if err := fs.Mount(); err != nil {
122+
fmt.Println("FAILED:", err)
123+
return
124+
}
125+
mounted = true
126+
fmt.Println("OK")
127+
case "UNMOUNT":
128+
if !mounted {
129+
fmt.Println("not mounted")
130+
return
131+
}
132+
if err := fs.Unmount(); err != nil {
133+
fmt.Println("FAILED:", err)
134+
return
135+
}
136+
mounted = false
137+
fmt.Println("OK")
138+
case "LS":
139+
if !requireMount() {
140+
return
141+
}
142+
if arg == "" {
143+
arg = "/"
144+
}
145+
dir, err := fs.Open(arg)
146+
if err != nil {
147+
fmt.Println("FAILED:", err)
148+
return
149+
}
150+
entries, _ := dir.Readdir(-1)
151+
dir.Close()
152+
for _, e := range entries {
153+
t := "f"
154+
if e.IsDir() {
155+
t = "d"
156+
}
157+
fmt.Printf(" %s %8d %s\n", t, e.Size(), e.Name())
158+
}
159+
if len(entries) == 0 {
160+
fmt.Println(" (empty)")
161+
}
162+
case "MKDIR":
163+
if !requireMount() || requireArg(arg) {
164+
return
165+
}
166+
if err := fs.Mkdir(arg, 0755); err != nil {
167+
fmt.Println("FAILED:", err)
168+
return
169+
}
170+
fmt.Println("OK")
171+
case "WRITE":
172+
if !requireMount() {
173+
return
174+
}
175+
wp := strings.SplitN(arg, " ", 2)
176+
if len(wp) < 2 {
177+
fmt.Println("Usage: WRITE path data")
178+
return
179+
}
180+
f, err := fs.OpenFile(wp[0], os.O_CREATE|os.O_WRONLY|os.O_TRUNC)
181+
if err != nil {
182+
fmt.Println("FAILED:", err)
183+
return
184+
}
185+
f.Write([]byte(wp[1]))
186+
f.Close()
187+
fmt.Println("OK")
188+
case "CAT":
189+
if !requireMount() || requireArg(arg) {
190+
return
191+
}
192+
f, err := fs.Open(arg)
193+
if err != nil {
194+
fmt.Println("FAILED:", err)
195+
return
196+
}
197+
buf := make([]byte, 256)
198+
n, _ := f.Read(buf)
199+
f.Close()
200+
fmt.Printf("%s\n", buf[:n])
201+
case "RM":
202+
if !requireMount() || requireArg(arg) {
203+
return
204+
}
205+
if err := fs.Remove(arg); err != nil {
206+
fmt.Println("FAILED:", err)
207+
return
208+
}
209+
fmt.Println("OK")
210+
case "TEST":
211+
testMkdir()
212+
case "TEST2":
213+
testNested()
214+
case "TEST3":
215+
testFiles()
216+
case "TEST4":
217+
testStress()
218+
default:
219+
fmt.Println("Unknown command. Type HELP.")
220+
}
221+
}
222+
223+
func requireMount() bool {
224+
if !mounted {
225+
fmt.Println("not mounted")
226+
}
227+
return mounted
228+
}
229+
230+
func requireArg(arg string) bool {
231+
if arg == "" {
232+
fmt.Println("missing argument")
233+
return true
234+
}
235+
return false
236+
}
237+
238+
// testMkdir exercises mkdir+stat in a loop — the pattern that triggers the
239+
// LLVM -O2 miscompilation bug in lfs_dir_fetchmatch (see littlefs/lfs.c).
240+
// At -O2 without the optnone fix, every stat fails with "no directory entry".
241+
func testMkdir() {
242+
if !requireMount() {
243+
return
244+
}
245+
const n = 10
246+
fail := 0
247+
for i := 0; i < n; i++ {
248+
p := fmt.Sprintf("/t_%d", i)
249+
if err := fs.Mkdir(p, 0755); err != nil {
250+
fmt.Printf(" mkdir %s: %v\n", p, err)
251+
fail++
252+
continue
253+
}
254+
if info, err := fs.Stat(p); err != nil {
255+
fmt.Printf(" FAIL %s: stat: %v\n", p, err)
256+
fail++
257+
} else if !info.IsDir() {
258+
fmt.Printf(" FAIL %s: not a dir\n", p)
259+
fail++
260+
} else {
261+
fmt.Printf(" OK %s\n", p)
262+
}
263+
}
264+
for i := 0; i < n; i++ {
265+
fs.Remove(fmt.Sprintf("/t_%d", i))
266+
}
267+
if fail > 0 {
268+
fmt.Printf("RESULT: %d/%d FAILED\n", fail, n)
269+
} else {
270+
fmt.Printf("RESULT: %d/%d passed\n", n, n)
271+
}
272+
}
273+
274+
// testNested creates parent → child → grandchild directories and verifies
275+
// each level with stat, then cleans up in reverse order.
276+
func testNested() {
277+
if !requireMount() {
278+
return
279+
}
280+
levels := []string{"/na", "/na/nb", "/na/nb/nc"}
281+
fail := 0
282+
for _, p := range levels {
283+
if err := fs.Mkdir(p, 0755); err != nil {
284+
fmt.Printf(" mkdir %s: %v\n", p, err)
285+
fail++
286+
continue
287+
}
288+
if info, err := fs.Stat(p); err != nil {
289+
fmt.Printf(" FAIL %s: stat: %v\n", p, err)
290+
fail++
291+
} else if !info.IsDir() {
292+
fmt.Printf(" FAIL %s: not a dir\n", p)
293+
fail++
294+
} else {
295+
fmt.Printf(" OK %s\n", p)
296+
}
297+
}
298+
// cleanup reverse
299+
for i := len(levels) - 1; i >= 0; i-- {
300+
fs.Remove(levels[i])
301+
}
302+
if fail > 0 {
303+
fmt.Printf("RESULT: %d/%d FAILED\n", fail, len(levels))
304+
} else {
305+
fmt.Printf("RESULT: %d/%d passed\n", len(levels), len(levels))
306+
}
307+
}
308+
309+
// testFiles creates directories and writes+reads files inside them.
310+
func testFiles() {
311+
if !requireMount() {
312+
return
313+
}
314+
fail := 0
315+
dirs := []string{"/fd1", "/fd1/fd2"}
316+
for _, d := range dirs {
317+
if err := fs.Mkdir(d, 0755); err != nil {
318+
fmt.Printf(" mkdir %s: %v\n", d, err)
319+
fail++
320+
}
321+
}
322+
// write files inside dirs
323+
files := map[string]string{
324+
"/fd1/a.txt": "hello",
325+
"/fd1/fd2/b.txt": "world",
326+
}
327+
for path, data := range files {
328+
f, err := fs.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC)
329+
if err != nil {
330+
fmt.Printf(" FAIL write %s: %v\n", path, err)
331+
fail++
332+
continue
333+
}
334+
f.Write([]byte(data))
335+
f.Close()
336+
// read back
337+
rf, err := fs.Open(path)
338+
if err != nil {
339+
fmt.Printf(" FAIL open %s: %v\n", path, err)
340+
fail++
341+
continue
342+
}
343+
buf := make([]byte, 64)
344+
n, _ := rf.Read(buf)
345+
rf.Close()
346+
if string(buf[:n]) != data {
347+
fmt.Printf(" FAIL %s: got %q want %q\n", path, string(buf[:n]), data)
348+
fail++
349+
} else {
350+
fmt.Printf(" OK %s\n", path)
351+
}
352+
}
353+
// cleanup
354+
for path := range files {
355+
fs.Remove(path)
356+
}
357+
for i := len(dirs) - 1; i >= 0; i-- {
358+
fs.Remove(dirs[i])
359+
}
360+
total := len(files)
361+
if fail > 0 {
362+
fmt.Printf("RESULT: %d/%d FAILED\n", fail, total)
363+
} else {
364+
fmt.Printf("RESULT: %d/%d passed\n", total, total)
365+
}
366+
}
367+
368+
// testStress does 50 rapid mkdir+stat cycles to stress-test the fix.
369+
func testStress() {
370+
if !requireMount() {
371+
return
372+
}
373+
const n = 50
374+
fail := 0
375+
for i := 0; i < n; i++ {
376+
p := fmt.Sprintf("/s_%d", i)
377+
if err := fs.Mkdir(p, 0755); err != nil {
378+
fmt.Printf(" mkdir %s: %v\n", p, err)
379+
fail++
380+
continue
381+
}
382+
if _, err := fs.Stat(p); err != nil {
383+
fmt.Printf(" FAIL %s: %v\n", p, err)
384+
fail++
385+
}
386+
}
387+
for i := 0; i < n; i++ {
388+
fs.Remove(fmt.Sprintf("/s_%d", i))
389+
}
390+
if fail > 0 {
391+
fmt.Printf("RESULT: %d/%d FAILED\n", fail, n)
392+
} else {
393+
fmt.Printf("RESULT: %d/%d passed\n", n, n)
394+
}
395+
}

0 commit comments

Comments
 (0)