Skip to content

Commit f462959

Browse files
committed
add FuzzFS and fix bugs found during fuzzing
1 parent 4d0daaf commit f462959

10 files changed

+272
-80
lines changed

Diff for: exported.go

+13-2
Original file line numberDiff line numberDiff line change
@@ -98,26 +98,37 @@ func (fp *File) Write(buf []byte) (int, error) {
9898

9999
// Close closes the file and syncs any unwritten data to the underlying device.
100100
func (fp *File) Close() error {
101-
fr := fp.f_close()
101+
fr := fp.obj.validate()
102+
if fr != frOK {
103+
return fr
104+
}
105+
106+
fr = fp.f_close()
102107
if fr != frOK {
103108
return fr
104109
}
105110
return nil
106111
}
107112

108-
// Sync syncs any unwritten data to the underlying device.
113+
// Sync commits the current contents of the file to the filesystem immediately.
109114
func (fp *File) Sync() error {
110115
fr := fp.obj.validate()
111116
if fr != frOK {
112117
return fr
113118
}
119+
114120
fr = fp.obj.fs.sync()
115121
if fr != frOK {
116122
return fr
117123
}
118124
return nil
119125
}
120126

127+
// Mode returns the lowest 2 bits of the file's permission (read, write or both).
128+
func (fp *File) Mode() Mode {
129+
return Mode(fp.flag & 3)
130+
}
131+
121132
// OpenDir opens the named directory for reading.
122133
func (fsys *FS) OpenDir(dp *Dir, path string) error {
123134
fr := fsys.f_opendir(&dp.dir, path)

Diff for: fat.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,7 @@ outerLoop:
424424
if csect+cc > uint32(fs.csize) {
425425
cc = uint32(fs.csize) - csect // clip at cluster boundary.
426426
}
427-
if fs.disk_write(wbuff, sect, int(cc)) != drOK {
427+
if fs.disk_write(wbuff[:cc*uint32(fs.ssize)], sect, int(cc)) != drOK {
428428
return bw, fp.abort(frDiskErr)
429429
}
430430
off := fp.sect - sect
@@ -506,8 +506,8 @@ func (fsys *FS) f_open(fp *File, name string, mode accessmode) fileResult {
506506
} else if fsys.perm == 0 {
507507
return frDenied
508508
}
509-
510509
var dj dir
510+
fp.obj.fs = fsys
511511
dj.obj.fs = fsys
512512
res := dj.follow_path(name)
513513
if res == frOK {

Diff for: fat_test.go

+11-76
Original file line numberDiff line numberDiff line change
@@ -168,69 +168,6 @@ func TestFileInfo(t *testing.T) {
168168
}
169169
}
170170

171-
func ExampleRead() {
172-
const (
173-
filename = "test.txt"
174-
data = "abc123"
175-
)
176-
var fs FS
177-
log := attachLogger(&fs)
178-
dev := DefaultFATByteBlocks(32000)
179-
fr := fs.mount_volume(dev, uint16(dev.blk.size()), faRead|faWrite)
180-
if fr != frOK {
181-
log.Error("mount failed:" + fr.Error())
182-
return
183-
}
184-
185-
var fp File
186-
187-
fr = fs.f_open(&fp, filename, faRead|faWrite|faCreateNew)
188-
if fr != frOK {
189-
log.Error("open for write failed:" + fr.Error())
190-
return
191-
}
192-
193-
n, fr := fp.f_write([]byte(data))
194-
if fr != frOK {
195-
log.Error("write failed:" + fr.Error())
196-
return
197-
}
198-
if n != len(data) {
199-
log.Error("write failed: short write")
200-
return
201-
}
202-
203-
fr = fp.f_close()
204-
if fr != frOK {
205-
log.Error("close failed:" + fr.Error())
206-
return
207-
}
208-
209-
// Read back data.
210-
fr = fs.f_open(&fp, filename, faRead)
211-
if fr != frOK {
212-
log.Error("open for read failed:" + fr.Error())
213-
return
214-
}
215-
buf := make([]byte, len(data))
216-
n, fr = fp.f_read(buf)
217-
if fr != frOK {
218-
log.Error("read failed:" + fr.Error())
219-
return
220-
}
221-
got := string(buf[:n])
222-
if got != data {
223-
log.Error("read failed", slog.String("got", got), slog.String("want", data))
224-
return
225-
}
226-
fr = fp.f_close()
227-
if fr != frOK {
228-
log.Error("close failed:" + fr.Error())
229-
return
230-
}
231-
fmt.Println("wrote and read back file OK!")
232-
}
233-
234171
func DefaultFATByteBlocks(numBlocks int) *BytesBlocks {
235172
const defaultBlockSize = 512
236173
blk, _ := makeBlockIndexer(defaultBlockSize)
@@ -327,26 +264,24 @@ func fatInitDiff(data []byte) (s string) {
327264
}
328265
return s
329266
}
330-
331267
func initTestFAT() (*FS, *BytesBlocks) {
332-
dev := DefaultFATByteBlocks(32000)
268+
return initTestFATWithLogger(32000, slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
269+
Level: slogLevelTrace,
270+
})))
271+
272+
}
273+
func initTestFATWithLogger(size int64, log *slog.Logger) (*FS, *BytesBlocks) {
274+
dev := DefaultFATByteBlocks(int(size))
333275
var fs FS
334-
attachLogger(&fs)
276+
fs.log = log
335277
ss := uint16(dev.blk.size())
336-
fr := fs.mount_volume(dev, ss, faRead|faWrite)
337-
if fr != frOK {
338-
panic(fr.Error())
278+
err := fs.Mount(dev, int(ss), ModeRW)
279+
if err != nil {
280+
panic(err)
339281
}
340282
return &fs, dev
341283
}
342284

343-
func attachLogger(fs *FS) *slog.Logger {
344-
fs.log = slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
345-
Level: slogLevelTrace,
346-
}))
347-
return fs.log
348-
}
349-
350285
// Start of clean slate FAT32 filesystem image with name `keylargo`, 8GB in size.
351286
// Contains a folder structure with a rootfile with some test, a rootdir directory
352287
// with a file in it.

Diff for: fuzz_test.go

+168
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
package fat
2+
3+
import (
4+
"io"
5+
"log/slog"
6+
"os"
7+
"testing"
8+
)
9+
10+
// This function is a self contained fuzzing function whose working
11+
// principle is similiar to that of a virtual machine. It takes in
12+
// a series of 64-bit operations and performs them on a FS object.
13+
func FuzzFS(f *testing.F) {
14+
// 64-bit operation definition, starting with least significant bits:
15+
//
16+
// - OP: First 4 bits are the operation to perform.
17+
// - WHO: Next 4 bits is target of operation. A 0 value means random/nonexistent target.
18+
// - PERM: Next 2 bits are the permission, if applicable.
19+
// - RESERVED: Middle bits are reserved.
20+
// - DATASIZE: Last 16 bits is the size of the data to read/write, if applicable.
21+
const (
22+
opChangeDir uint64 = iota
23+
opCreateDir
24+
opCreateFile
25+
opOpenFile
26+
opReadFile
27+
opWriteFile
28+
opCloseFile
29+
30+
datasizeOff = 48
31+
whoOff = 4
32+
)
33+
type filinfo struct {
34+
file File
35+
ptr int64
36+
size int64
37+
name string
38+
closed bool
39+
}
40+
genName := func(fs *FS, dir string, who uint8) string {
41+
return dir + "/" + string('a'+who)
42+
}
43+
getWho := func(finfos []filinfo, who uint8) (filename *filinfo) {
44+
if len(finfos) == 0 {
45+
return nil
46+
}
47+
who %= uint8(len(finfos))
48+
return &finfos[who]
49+
}
50+
writeData := make([]byte, 1<<16)
51+
readData := make([]byte, 1<<16)
52+
for i := range writeData {
53+
writeData[i] = byte(i)
54+
}
55+
f.Add(opChangeDir, opCreateFile, opWriteFile|(1000<<datasizeOff),
56+
opCloseFile, opOpenFile, opReadFile|(1000<<datasizeOff),
57+
opChangeDir, opOpenFile|(1<<whoOff), opWriteFile|(1<<whoOff)|(1000<<datasizeOff),
58+
opCloseFile|(1<<whoOff), opOpenFile, opReadFile|(1<<whoOff)|(1001<<datasizeOff),
59+
)
60+
const totalFSSize = 2 * 32000
61+
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
62+
Level: slog.LevelError,
63+
}))
64+
f.Fuzz(func(t *testing.T, fsop0, fsop1, fsop2, fsop3, fsop4, fsop5, fsop6, fsop7, fsop8, fsop9, fsop10, fsop11 uint64) {
65+
fs, _ := initTestFATWithLogger(totalFSSize, logger)
66+
fsops := [...]uint64{fsop0, fsop1, fsop2, fsop3, fsop4, fsop5, fsop6, fsop7, fsop8, fsop9, fsop10, fsop11}
67+
fileinfos := make([]filinfo, 0, len(fsops))
68+
var dir string = "/"
69+
totalWritten := 0
70+
for _, fsop := range fsops {
71+
op := fsop & 0xf
72+
who := byte(fsop) >> 4
73+
perm := Mode(fsop>>8) & 3
74+
datasize := uint16(fsop >> 48)
75+
switch op {
76+
case opChangeDir:
77+
if dir == "/" {
78+
dir = "/rootdir"
79+
} else {
80+
dir = "/"
81+
}
82+
83+
case opCreateFile:
84+
fileinfos = append(fileinfos, filinfo{})
85+
info := &fileinfos[len(fileinfos)-1]
86+
filename := genName(fs, dir, who)
87+
err := fs.OpenFile(&info.file, filename, perm|ModeCreateAlways)
88+
if err != nil {
89+
fileinfos = fileinfos[:len(fileinfos)-1] // Uncommit file on error.
90+
}
91+
info.name = filename
92+
93+
case opOpenFile:
94+
info := getWho(fileinfos, who)
95+
if info == nil || !info.closed {
96+
// Don't open already open files for simplicity's sake.
97+
break
98+
}
99+
err := fs.OpenFile(&info.file, info.name, perm|ModeOpenExisting)
100+
if err == nil {
101+
info.closed = false
102+
info.ptr = 0
103+
}
104+
105+
case opCloseFile:
106+
info := getWho(fileinfos, who)
107+
if info == nil {
108+
break
109+
}
110+
err := info.file.Close()
111+
if err != nil && !info.closed {
112+
panic(err)
113+
}
114+
info.ptr = 0
115+
info.closed = true
116+
117+
case opWriteFile:
118+
if totalWritten >= totalFSSize*4/5 {
119+
break // Avoid growing the filesystem too much.
120+
}
121+
info := getWho(fileinfos, who)
122+
if info == nil || info.closed {
123+
break
124+
}
125+
n, err := info.file.Write(writeData[:datasize])
126+
if info.file.Mode()&ModeWrite == 0 {
127+
if n != 0 {
128+
panic("forbidden write")
129+
}
130+
break // Ignore if file not writable.
131+
}
132+
if err != nil {
133+
panic(err)
134+
} else if n != int(datasize) {
135+
panic("n != dsize")
136+
}
137+
info.ptr = min(info.ptr+int64(n), info.size)
138+
if info.ptr > info.size {
139+
info.size = info.ptr
140+
}
141+
totalWritten += n
142+
143+
case opReadFile:
144+
info := getWho(fileinfos, who)
145+
if info == nil || info.closed {
146+
break
147+
}
148+
n, err := info.file.Read(readData[:datasize])
149+
if info.file.Mode()&ModeRead == 0 {
150+
if n != 0 {
151+
panic("forbidden read")
152+
}
153+
break // Ignore if file not readable.
154+
}
155+
if err != nil && err != io.EOF {
156+
panic(err)
157+
}
158+
}
159+
}
160+
})
161+
}
162+
163+
func min(a, b int64) int64 {
164+
if a < b {
165+
return a
166+
}
167+
return b
168+
}

Diff for: testdata/fuzz/FuzzFS/091491d32c1aa6cc

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
go test fuzz v1
2+
uint64(291)
3+
uint64(2)
4+
uint64(82)
5+
uint64(52)
6+
uint64(3)
7+
uint64(2)
8+
uint64(162)
9+
uint64(3)
10+
uint64(3)
11+
uint64(2)
12+
uint64(100)
13+
uint64(3)

Diff for: testdata/fuzz/FuzzFS/0b42f34074fab2cc

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
go test fuzz v1
2+
uint64(0)
3+
uint64(2)
4+
uint64(3)
5+
uint64(5)
6+
uint64(50)
7+
uint64(2)
8+
uint64(3)
9+
uint64(3)
10+
uint64(58)
11+
uint64(3)
12+
uint64(3)
13+
uint64(50)

Diff for: testdata/fuzz/FuzzFS/10f7e88b2528bd67

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
go test fuzz v1
2+
uint64(86)
3+
uint64(48)
4+
uint64(36)
5+
uint64(338)
6+
uint64(6)
7+
uint64(4)
8+
uint64(86)
9+
uint64(48)
10+
uint64(36)
11+
uint64(263)
12+
uint64(32)
13+
uint64(4)

Diff for: testdata/fuzz/FuzzFS/11ab75d6ca0e9f3d

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
go test fuzz v1
2+
uint64(0)
3+
uint64(2)
4+
uint64(25)
5+
uint64(51)
6+
uint64(0)
7+
uint64(100)
8+
uint64(0)
9+
uint64(3)
10+
uint64(1)
11+
uint64(6)
12+
uint64(3)
13+
uint64(4)

0 commit comments

Comments
 (0)