diff --git a/filesystem/ext4/ext4.go b/filesystem/ext4/ext4.go index 40ddc65..3a54291 100644 --- a/filesystem/ext4/ext4.go +++ b/filesystem/ext4/ext4.go @@ -328,8 +328,16 @@ func Create(b backend.Storage, size, start, sectorsize int64, p *Params) (*FileS // recalculate if it was not user provided if !userProvidedBlocksize { - sectorsPerBlockR, blocksizeR, numblocksR := recalculateBlocksize(numblocks, size) + sectorsPerBlockR, blocksizeR, numblocksR := recalculateBlocksize(size) _, blocksize, numblocks = uint8(sectorsPerBlockR), blocksizeR, numblocksR + // resize_inode / reserved-GDT growth isn't yet supported for + // non-1 KiB block sizes (see TODO further down). When *we* + // picked the non-1 KiB default, drop the feature so Create + // doesn't try to lay out blocks it can't read back. + const oneKiB = uint32(SectorSize512) * 2 + if blocksize != oneKiB { + fflags.reservedGDTBlocksForExpansion = false + } } // how many blocks in each block group (and therefore how many block groups) @@ -1865,36 +1873,21 @@ func (fs *FileSystem) readBlock(blockNumber uint64) ([]byte, error) { return blockBytes, nil } -// recalculate blocksize based on the existing number of blocks -// - 0 <= blocks < 3MM : floppy - blocksize = 1024 -// - 3MM <= blocks < 512MM : small - blocksize = 1024 -// - 512MM <= blocks < 4*1024*1024MM : default - blocksize = -// - 4*1024*1024MM <= blocks < 16*1024*1024MM : big - blocksize = -// - 16*1024*1024MM <= blocks : huge - blocksize = -// -// the original code from e2fsprogs https://git.kernel.org/pub/scm/fs/ext2/e2fsprogs.git/tree/misc/mke2fs.c -func recalculateBlocksize(numblocks, size int64) (sectorsPerBlock int, blocksize uint32, numBlocksAdjusted int64) { - var ( - million64 = int64(million) - sectorSize512 = uint32(SectorSize512) - ) - switch { - case 0 <= numblocks && numblocks < 3*million64: - sectorsPerBlock = 2 - blocksize = 2 * sectorSize512 - case 3*million64 <= numblocks && numblocks < 512*million64: - sectorsPerBlock = 2 - blocksize = 2 * sectorSize512 - case 512*million64 <= numblocks && numblocks < 4*1024*1024*million64: - sectorsPerBlock = 2 - blocksize = 2 * sectorSize512 - case 4*1024*1024*million64 <= numblocks && numblocks < 16*1024*1024*million64: - sectorsPerBlock = 2 - blocksize = 2 * sectorSize512 - case numblocks > 16*1024*1024*million64: - sectorsPerBlock = 2 - blocksize = 2 * sectorSize512 +// recalculateBlocksize picks a default ext4 block size when the caller +// did not specify one. We follow mke2fs's "small" vs "default" split: +// 1 KiB blocks below 512 MiB, 4 KiB blocks at or above. The previous +// implementation hard-coded 1 KiB blocks for all sizes, which made the +// journal allocator (capped at 128 MiB) need >65535 1-KiB blocks at a +// few GiB — past both the per-extent cap and the inode's 4-extent root +// limit. 4 KiB blocks keep a typical journal in a single extent. +func recalculateBlocksize(size int64) (sectorsPerBlock int, blocksize uint32, numBlocksAdjusted int64) { + const smallFilesystemThreshold = 512 * 1024 * 1024 // 512 MiB + if size < smallFilesystemThreshold { + sectorsPerBlock = 2 // 1 KiB blocks + } else { + sectorsPerBlock = 8 // 4 KiB blocks } + blocksize = uint32(sectorsPerBlock) * uint32(SectorSize512) return sectorsPerBlock, blocksize, size / int64(blocksize) } diff --git a/filesystem/ext4/multigb_test.go b/filesystem/ext4/multigb_test.go new file mode 100644 index 0000000..c527d68 --- /dev/null +++ b/filesystem/ext4/multigb_test.go @@ -0,0 +1,63 @@ +package ext4 + +import ( + "bytes" + "fmt" + "os" + "os/exec" + "path/filepath" + "testing" + + "github.com/diskfs/go-diskfs/backend/file" +) + +// TestCreateMultiGB exercises Create on filesystems past the 512 MiB +// threshold at which recalculateBlocksize switches to 4 KiB blocks. +// 2 GiB is the size that exposed diskfs/go-diskfs#402 under the +// previous 1 KiB-blocks default (a 64 MiB journal then required 65536 +// blocks, exceeding both the 32768-blocks-per-extent cap and the +// inode-root extent tree's 4-extent limit). +func TestCreateMultiGB(t *testing.T) { + for _, sizeGiB := range []int64{1, 2} { + sizeGiB := sizeGiB + t.Run(fmt.Sprintf("%dGiB", sizeGiB), func(t *testing.T) { + tmp := t.TempDir() + imgPath := filepath.Join(tmp, "fs.img") + size := sizeGiB * 1024 * 1024 * 1024 + f, err := os.Create(imgPath) + if err != nil { + t.Fatal(err) + } + if err := f.Truncate(size); err != nil { + _ = f.Close() + t.Fatal(err) + } + if err := f.Close(); err != nil { + t.Fatal(err) + } + f, err = os.OpenFile(imgPath, os.O_RDWR, 0) + if err != nil { + t.Fatal(err) + } + defer func() { _ = f.Close() }() + fs, err := Create(file.New(f, false), size, 0, 512, &Params{}) + if err != nil { + t.Fatalf("Create failed: %v", err) + } + if fs == nil { + t.Fatalf("Create returned nil filesystem") + } + if err := f.Sync(); err != nil { + t.Fatalf("Sync: %v", err) + } + cmd := exec.Command("e2fsck", "-f", "-n", imgPath) + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + if err := cmd.Run(); err != nil { + t.Fatalf("e2fsck failed: %v\nstdout:\n%s\nstderr:\n%s", + err, stdout.String(), stderr.String()) + } + }) + } +}