From a4436aefe3f3d110cd6a13fca3894c82aac4c34b Mon Sep 17 00:00:00 2001 From: Lachlan Donald Date: Wed, 8 Jul 2026 13:59:50 +1000 Subject: [PATCH] fix: set padding bits at end of inode bitmap ext4 convention (enforced by e2fsck) is that inode bitmap bits beyond the last real inode in the block are set to 1 so allocators never treat nonexistent inodes as free. compactext4 left these tail bits clear in every group, so e2fsck 1.47.x reports "Padding at end of inode bitmap is not set" and exits 4 on tar2ext4 output. The block bitmap already handles its analogous case (absent blocks in the last group are marked allocated); this adds the inode-bitmap equivalent. Padding bits are not counted in usedInodeCount since free-inode accounting is based on inodesPerGroup real inodes. Found by running e2fsck -f -n against tar2ext4 output with e2fsck 1.47.3. Signed-off-by: Lachlan Donald --- ext4/internal/compactext4/compact.go | 7 +++ ext4/internal/compactext4/compact_test.go | 67 +++++++++++++++++++++++ 2 files changed, 74 insertions(+) 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}},