Skip to content

Commit 89439d4

Browse files
authored
fix: support fat32 on 4k native disks (#316)
This fixes the issue that `GetFilesystem()` fails on vfat created on disks with 4k sector size. `mkfs.vfat` uses 4k size when disk is native 4k. This can be reproduced by creating a loopback device with 4k sectors and running mkfs.vfat on it. ```bash truncate -s 10M /tmp/vfat_4k.img sudo losetup -f --show -b 4096 /tmp/vfat_4k.img sudo mkfs.vfat -F 32 -n TEST4K /dev/loop0 sudo losetup -d /dev/loop0 ``` Then can be tested as: ```go package main import ( "github.com/diskfs/go-diskfs" "github.com/diskfs/go-diskfs/backend/file" "github.com/stretchr/testify/require" ) func main() { // Open the image file created with 4K sectors bk, err := file.OpenFromPath("/tmp/vfat_4k.img", false) require.NoError(err, "failed to open image file") defer bk.Close() // Try to open the disk diskInfo, err := diskfs.OpenBackend(bk, diskfs.WithOpenMode(diskfs.ReadWriteExclusive)) require.NoError(err, "failed to open backend") defer diskInfo.Close() // This will fail with unpatched go-diskfs: // "unknown filesystem on partition 0" // Because go-diskfs rejects bytesPerSector != 512 fs, err := diskInfo.GetFilesystem(0) require.NoError(err, "failed to get filesystem from partition 0") defer fs.Close() } ``` When mkfs.vfat formats a device with 4K logical sectors, it writes bytesPerSector=4096 in the BPB, which go-diskfs rejects. Signed-off-by: Noel Georgi <[email protected]>
1 parent 58541aa commit 89439d4

File tree

5 files changed

+43
-18
lines changed

5 files changed

+43
-18
lines changed

filesystem/fat32/common_test.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515

1616
const (
1717
Fat32File = "./testdata/dist/fat32.img"
18+
Fat32File4kB = "./testdata/dist/fat32-4k.img"
1819
fsckFile = "./testdata/dist/fsck.txt"
1920
rootdirFile = "./testdata/dist/root_dir.txt"
2021
rootdirFileFLS = "./testdata/dist/root_dir_fls.txt"
@@ -199,9 +200,7 @@ func testGetValidDirectoryEntriesFromFile(dirFilePath, dirEntryPattern string, f
199200
text := scanner.Text()
200201
dirEntryMatch := testDirectoryEntryRE.FindStringSubmatch(text)
201202
fileEntryMatch := testFileEntryRE.FindStringSubmatch(text)
202-
var (
203-
de *directoryEntry
204-
)
203+
var de *directoryEntry
205204
switch {
206205
case len(dirEntryMatch) == 4:
207206
filenameShort := dirEntryMatch[1]

filesystem/fat32/dos20bpb.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,13 @@ func dos20BPBFromBytes(b []byte) (*dos20BPB, error) {
2525
}
2626
bpb := dos20BPB{}
2727
// make sure we have a valid sector size
28+
// Modern mkfs.vfat creates FAT32 with larger sector sizes on 4K native disks
29+
// Accept any power-of-2 sector size >= 512
2830
sectorSize := binary.LittleEndian.Uint16(b[0:2])
29-
if sectorSize != uint16(SectorSize512) {
30-
return nil, fmt.Errorf("invalid sector size %d provided in DOS 2.0 BPB. Must be %d", sectorSize, SectorSize512)
31+
if sectorSize < uint16(SectorSize512) || (sectorSize&(sectorSize-1)) != 0 {
32+
return nil, fmt.Errorf("invalid sector size %d provided in DOS 2.0 BPB. Must be power of 2 and >= %d", sectorSize, SectorSize512)
3133
}
32-
bpb.bytesPerSector = SectorSize512
34+
bpb.bytesPerSector = SectorSize(sectorSize)
3335
bpb.sectorsPerCluster = b[2]
3436
bpb.reservedSectors = binary.LittleEndian.Uint16(b[3:5])
3537
bpb.fatCount = b[5]

filesystem/fat32/fat32.go

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -341,30 +341,36 @@ func Read(b backend.Storage, size, start, blocksize int64) (*FileSystem, error)
341341
return nil, fmt.Errorf("requested size is smaller than minimum allowed FAT32 size %d", blocksize*4)
342342
}
343343
// load the information from the disk
344-
// read first 512 bytes from the file
345-
bsb := make([]byte, SectorSize512)
344+
// We need to read at least the first logical sector to get the BPB
345+
// Try reading up to 4096 bytes to handle 4K sector devices
346+
maxSectorSize := 4096
347+
bsb := make([]byte, maxSectorSize)
346348
n, err := b.ReadAt(bsb, start)
347349
if err != nil {
348350
return nil, fmt.Errorf("could not read bytes from file: %w", err)
349351
}
350-
if uint16(n) < uint16(SectorSize512) {
351-
return nil, fmt.Errorf("only could read %d bytes from file", n)
352+
if n < int(SectorSize512) {
353+
return nil, fmt.Errorf("only could read %d bytes from file, need at least %d", n, SectorSize512)
352354
}
353-
bs, err := msDosBootSectorFromBytes(bsb)
354355

356+
// Parse boot sector from the first 512 bytes (boot sector structure is always 512 bytes)
357+
bs, err := msDosBootSectorFromBytes(bsb[:SectorSize512])
355358
if err != nil {
356359
return nil, fmt.Errorf("error reading MS-DOS Boot Sector: %w", err)
357360
}
358361

362+
// Get the actual sector size from the BPB
363+
bytesPerSector := bs.biosParameterBlock.dos331BPB.dos20BPB.bytesPerSector
359364
sectorsPerFat := bs.biosParameterBlock.sectorsPerFat
360-
fatSize := sectorsPerFat * uint32(SectorSize512)
365+
fatSize := sectorsPerFat * uint32(bytesPerSector)
361366
reservedSectors := bs.biosParameterBlock.dos331BPB.dos20BPB.reservedSectors
362367
sectorsPerCluster := bs.biosParameterBlock.dos331BPB.dos20BPB.sectorsPerCluster
363-
fatPrimaryStart := uint64(reservedSectors) * uint64(SectorSize512)
368+
fatPrimaryStart := uint64(reservedSectors) * uint64(bytesPerSector)
364369
fatSecondaryStart := fatPrimaryStart + uint64(fatSize)
365370

371+
// FSI sector is always 512 bytes
366372
fsisBytes := make([]byte, 512)
367-
read, err := b.ReadAt(fsisBytes, int64(bs.biosParameterBlock.fsInformationSector)*blocksize+start)
373+
read, err := b.ReadAt(fsisBytes, int64(bs.biosParameterBlock.fsInformationSector)*int64(bytesPerSector)+start)
368374
if err != nil {
369375
return nil, fmt.Errorf("unable to read bytes for FSInformationSector: %w", err)
370376
}
@@ -392,7 +398,7 @@ func Read(b backend.Storage, size, start, blocksize int64) (*FileSystem, error)
392398
fsis: *fsis,
393399
table: *fat,
394400
dataStart: dataStart,
395-
bytesPerCluster: int(sectorsPerCluster) * int(SectorSize512),
401+
bytesPerCluster: int(sectorsPerCluster) * int(bytesPerSector),
396402
start: start,
397403
size: size,
398404
backend: b,
@@ -994,7 +1000,6 @@ func (fs *FileSystem) mkLabel(parent *Directory, name string) (*directoryEntry,
9941000
// if it does not exist, it may or may not make it
9951001
func (fs *FileSystem) readDirWithMkdir(p string, doMake bool) (*Directory, []*directoryEntry, error) {
9961002
paths, err := splitPath(p)
997-
9981003
if err != nil {
9991004
return nil, nil, err
10001005
}

filesystem/fat32/fat32_test.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,25 @@ func TestFat32Type(t *testing.T) {
111111
}
112112
}
113113

114+
func TestFat32With4kSectors(t *testing.T) {
115+
bk, err := file.OpenFromPath(fat32.Fat32File4kB, true)
116+
if err != nil {
117+
t.Fatalf("Failed to open file with 4k sectors: %v", err)
118+
}
119+
120+
d, err := diskfs.OpenBackend(bk)
121+
if err != nil {
122+
t.Fatalf("Failed to open disk with 4k sectors: %v", err)
123+
}
124+
125+
defer d.Close()
126+
127+
_, err = d.GetFilesystem(0)
128+
if err != nil {
129+
t.Fatalf("Failed to get filesystem with 4k sectors: %v", err)
130+
}
131+
}
132+
114133
func TestFat32Mkdir(t *testing.T) {
115134
// only do this test if os.Getenv("TEST_IMAGE") contains a real image
116135
if intImage == "" {
@@ -1249,7 +1268,6 @@ func Test_Rename(t *testing.T) {
12491268
// do not create orig file
12501269
},
12511270
post: func(_ *testing.T, _ *fat32.FileSystem) {
1252-
12531271
},
12541272
},
12551273
{
@@ -1406,7 +1424,6 @@ func Test_Remove(t *testing.T) {
14061424
// do not create any file
14071425
},
14081426
post: func(_ *testing.T, _ *fat32.FileSystem) {
1409-
14101427
},
14111428
},
14121429
{

filesystem/fat32/testdata/mkfat32.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ set -x
1616
apk --update add dosfstools mtools sleuthkit
1717
dd if=/dev/zero of=/data/fat32.img bs=1M count=10
1818
mkfs.vfat -v -F 32 /data/fat32.img
19+
dd if=/dev/zero of=/data/fat32-4k.img bs=1M count=10
20+
mkfs.vfat -v -F 32 -S 4096 /data/fat32-4k.img
1921
echo "mtools_skip_check=1" >> /etc/mtools.conf
2022
mmd -i /data/fat32.img ::/foo
2123
mmd -i /data/fat32.img ::/foo/bar

0 commit comments

Comments
 (0)