diff --git a/ext4/internal/compactext4/compact.go b/ext4/internal/compactext4/compact.go index 7a8dceddf0..6ad82954dc 100644 --- a/ext4/internal/compactext4/compact.go +++ b/ext4/internal/compactext4/compact.go @@ -1276,6 +1276,13 @@ func (w *Writer) Close() error { dirCount++ } } + // Bits beyond inodesPerGroup don't correspond to real inodes; e2fsck + // expects this padding to be set so allocators never consider them + // free. Don't count them in usedInodeCount: free-inode accounting is + // based on inodesPerGroup real inodes. + for j := inodesPerGroup; j < BlockSize*8; j++ { + b[BlockSize+j/8] |= 1 << (j % 8) + } _, err := w.write(b[:]) if err != nil { return err diff --git a/ext4/internal/compactext4/compact_test.go b/ext4/internal/compactext4/compact_test.go index a1d7982c01..4525f2e1fd 100644 --- a/ext4/internal/compactext4/compact_test.go +++ b/ext4/internal/compactext4/compact_test.go @@ -221,6 +221,73 @@ func TestBasic(t *testing.T) { runTestsOnFiles(t, testFiles) } +// TestInodeBitmapPadding verifies that inode bitmap bits beyond +// inodesPerGroup are set in every group. e2fsck (1.47.x and later) reports +// "Padding at end of inode bitmap is not set" otherwise, and allocators +// could treat nonexistent inodes as free. +func TestInodeBitmapPadding(t *testing.T) { + image := "testfs.img" + imagef, err := os.Create(image) + if err != nil { + t.Fatal(err) + } + defer os.Remove(image) + defer imagef.Close() + + w := NewWriter(imagef) + testFiles := []testFile{ + {Path: "dir", File: &File{Mode: format.S_IFDIR | 0755}}, + {Path: "dir/file", File: &File{Mode: 0644}, Data: data[:40]}, + } + for _, tf := range testFiles { + createTestFile(t, w, tf) + } + if err := w.Close(); err != nil { + t.Fatal(err) + } + + var sb format.SuperBlock + if _, err := imagef.Seek(1024, io.SeekStart); err != nil { + t.Fatal(err) + } + if err := binary.Read(imagef, binary.LittleEndian, &sb); err != nil { + t.Fatal(err) + } + if sb.Magic != format.SuperBlockMagic { + t.Fatalf("bad superblock magic %#x", sb.Magic) + } + if sb.InodesPerGroup >= BlockSize*8 { + t.Fatalf("test image has no inode bitmap padding (inodesPerGroup=%d)", sb.InodesPerGroup) + } + groups := sb.InodesCount / sb.InodesPerGroup + + gds := make([]format.GroupDescriptor, groups) + if _, err := imagef.Seek(BlockSize, io.SeekStart); err != nil { + t.Fatal(err) + } + if err := binary.Read(imagef, binary.LittleEndian, gds); err != nil { + t.Fatal(err) + } + + var bitmap [BlockSize]byte + for g, gd := range gds { + if _, err := imagef.ReadAt(bitmap[:], int64(gd.InodeBitmapLow)*BlockSize); err != nil { + t.Fatal(err) + } + for j := sb.InodesPerGroup; j < BlockSize*8; j++ { + if bitmap[j/8]&(1<<(j%8)) == 0 { + t.Fatalf("group %d: inode bitmap padding bit %d is not set", g, j) + } + } + // Padding bits must not be counted as used inodes. + if uint32(gd.FreeInodesCountLow) > sb.InodesPerGroup { + t.Errorf("group %d: free inode count %d exceeds inodesPerGroup %d", g, gd.FreeInodesCountLow, sb.InodesPerGroup) + } + } + + fsck(t, image) +} + func TestLargeDirectory(t *testing.T) { testFiles := []testFile{ {Path: "bigdir", File: &File{Mode: format.S_IFDIR | 0755}},