From e7100c31c16da7b695f17df1788d9fc307ee00c6 Mon Sep 17 00:00:00 2001 From: eriknordmark Date: Fri, 29 May 2026 19:37:36 +0200 Subject: [PATCH 1/2] squashfs: honor partition start offset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit squashfs.Create and squashfs.Read work entirely in offsets relative to the start of the filesystem, but the backend they were handed referred to the whole disk. When the filesystem lives inside a partition (a non-zero start), Finalize wrote every section — including the superblock at offset 0 — to absolute backend offsets, clobbering the protective MBR/GPT and placing the filesystem in the wrong spot, and Read dispatched its metadata-table reads at unbiased squashfs-internal offsets and read garbage. Wrap the backend in backend.Sub(b, start, size) at Create and Read time when start is non-zero, so every internal ReadAt/WriteAt is biased by the partition offset while the existing offset arithmetic stays unchanged. Whole-disk callers (start == 0) are unaffected. Add a regression test that builds a squashfs inside a GPT partition and reads it back; the existing tests only ever exercised start == 0. Signed-off-by: eriknordmark Co-Authored-By: Claude Sonnet 4.6 --- filesystem/squashfs/partition_test.go | 135 ++++++++++++++++++++++++++ filesystem/squashfs/squashfs.go | 26 ++++- 2 files changed, 158 insertions(+), 3 deletions(-) create mode 100644 filesystem/squashfs/partition_test.go diff --git a/filesystem/squashfs/partition_test.go b/filesystem/squashfs/partition_test.go new file mode 100644 index 0000000..85e8ef6 --- /dev/null +++ b/filesystem/squashfs/partition_test.go @@ -0,0 +1,135 @@ +package squashfs_test + +import ( + "bytes" + "io" + "os" + "path/filepath" + "testing" + + diskfs "github.com/diskfs/go-diskfs" + "github.com/diskfs/go-diskfs/disk" + "github.com/diskfs/go-diskfs/filesystem" + "github.com/diskfs/go-diskfs/filesystem/squashfs" + "github.com/diskfs/go-diskfs/partition/gpt" +) + +// TestSquashfsInPartition exercises a squashfs that does not begin at +// offset 0 of its backend, i.e. one that lives inside a partition. +// +// Both Finalize and Read must honor the partition's byte offset: +// - Finalize must write the filesystem (starting with the superblock) +// at the partition's offset, not at backend offset 0 where it would +// clobber the protective MBR / GPT. +// - Read must read the superblock and all of its follow-on metadata +// tables (fragment, xattr, id) from that same offset. +// +// Before the offset fix, Finalize wrote the superblock to backend offset 0 +// and Read mis-biased its table reads, so a squashfs created inside a +// partition was both misplaced and unreadable. The existing whole-disk +// tests never caught this because they always pass start == 0. +func TestSquashfsInPartition(t *testing.T) { + const ( + sectorSize = 4096 // squashfs requires a blocksize >= 4096 + diskSize = 32 * 1024 * 1024 + partStart = 256 // sectors => 1 MiB into the disk + partSize = 8 * 1024 * 1024 + filename = "marker.txt" + fileContent = "squashfs partition-offset round-trip\n" + ) + + imgPath := filepath.Join(t.TempDir(), "disk.img") + + d, err := diskfs.Create(imgPath, diskSize, sectorSize) + if err != nil { + t.Fatalf("create disk: %v", err) + } + + // Lay down a GPT with a single partition that starts well inside the + // disk, so the filesystem's start offset is non-zero. + table := &gpt.Table{ + LogicalSectorSize: sectorSize, + PhysicalSectorSize: sectorSize, + ProtectiveMBR: true, + Partitions: []*gpt.Partition{ + {Index: 1, Start: partStart, Size: partSize, Type: gpt.LinuxFilesystem, Name: "rootfs"}, + }, + } + if err := d.Partition(table); err != nil { + t.Fatalf("write partition table: %v", err) + } + + // Reopen so the partition is re-read from disk and carries the disk's + // sector size; the in-memory partitions built above do not, so their + // GetStart() would otherwise use the 512-byte default. + d, err = diskfs.Open(imgPath, diskfs.WithSectorSize(sectorSize)) + if err != nil { + t.Fatalf("reopen disk: %v", err) + } + if _, err := d.GetPartitionTable(); err != nil { + t.Fatalf("re-read partition table: %v", err) + } + + // Build a squashfs in partition 1 with a known marker file. + fs, err := d.CreateFilesystem(disk.FilesystemSpec{Partition: 1, FSType: filesystem.TypeSquashfs}) + if err != nil { + t.Fatalf("CreateFilesystem(squashfs): %v", err) + } + rw, err := fs.OpenFile(filename, os.O_CREATE|os.O_RDWR) + if err != nil { + t.Fatalf("OpenFile for write: %v", err) + } + if _, err := rw.Write([]byte(fileContent)); err != nil { + t.Fatalf("write marker: %v", err) + } + sqs, ok := fs.(*squashfs.FileSystem) + if !ok { + t.Fatalf("filesystem is %T, want *squashfs.FileSystem", fs) + } + if err := sqs.Finalize(squashfs.FinalizeOptions{ + NoCompressInodes: true, + NoCompressData: true, + NoCompressFragments: true, + }); err != nil { + t.Fatalf("Finalize: %v", err) + } + + // Finalize must not have written over the GPT, and the squashfs + // superblock must sit at the partition's byte offset. + raw, err := os.ReadFile(imgPath) + if err != nil { + t.Fatalf("read disk image: %v", err) + } + if !bytes.HasPrefix(raw[sectorSize:sectorSize+8], []byte("EFI PART")) { + t.Fatal("GPT primary header was overwritten by Finalize") + } + partOffset := int64(partStart) * sectorSize + if !bytes.HasPrefix(raw[partOffset:partOffset+4], []byte("hsqs")) { + t.Fatalf("squashfs superblock did not land at partition offset %d", partOffset) + } + + // Reopen and read the filesystem back through the partition, which + // drives Read (and its metadata-table reads) at the non-zero start. + d2, err := diskfs.Open(imgPath, diskfs.WithSectorSize(sectorSize)) + if err != nil { + t.Fatalf("reopen disk: %v", err) + } + fs2, err := d2.GetFilesystem(1) + if err != nil { + t.Fatalf("GetFilesystem(1): %v", err) + } + if fs2.Type() != filesystem.TypeSquashfs { + t.Fatalf("filesystem read back as %v, want squashfs", fs2.Type()) + } + mf, err := fs2.OpenFile(filename, os.O_RDONLY) + if err != nil { + t.Fatalf("OpenFile for read: %v", err) + } + got, err := io.ReadAll(mf) + if err != nil { + t.Fatalf("read marker back: %v", err) + } + if string(got) != fileContent { + t.Errorf("marker content mismatch: got %q, want %q", string(got), fileContent) + } +} diff --git a/filesystem/squashfs/squashfs.go b/filesystem/squashfs/squashfs.go index ee18e6e..5b9bd55 100644 --- a/filesystem/squashfs/squashfs.go +++ b/filesystem/squashfs/squashfs.go @@ -90,11 +90,21 @@ func Create(b backend.Storage, size, start, blocksize int64) (*FileSystem, error return nil, fmt.Errorf("could not create working directory: %v", err) } + // Wrap the backend so all internal ReadAt/WriteAt calls in this + // package use offsets relative to the start of the filesystem. + // Finalize and Read are coded throughout in terms of squashfs- + // internal offsets; without this wrapping, every write/read on a + // non-zero start (i.e. inside a partition) would land at the wrong + // place on the underlying disk. + if start != 0 { + b = backend.Sub(b, start, size) + } + // create root directory // there is nothing in there return &FileSystem{ workspace: tmpdir, - start: start, + start: 0, size: size, backend: b, blocksize: blocksize, @@ -161,11 +171,21 @@ func Read(b backend.Storage, size, start, blocksize int64) (*FileSystem, error) return nil, err } + // Wrap the backend so all subsequent ReadAt calls use offsets + // relative to the start of the filesystem. The squashfs metadata + // (fragment table, inode table, etc.) carries squashfs-internal + // offsets, and helpers like readFragmentTable, readXattrsTable and + // readUidsGids ReadAt those offsets directly. Without wrapping, + // any non-zero start would land them at the wrong place on disk. + if start != 0 { + b = backend.Sub(b, start, size) + } + // load the information from the disk // read the superblock superblockBytes := make([]byte, superblockSize) - read, err = b.ReadAt(superblockBytes, start) + read, err = b.ReadAt(superblockBytes, 0) if err != nil { return nil, fmt.Errorf("unable to read bytes for superblock: %v", err) } @@ -211,7 +231,7 @@ func Read(b backend.Storage, size, start, blocksize int64) (*FileSystem, error) fs := &FileSystem{ workspace: "", // no workspace when we do nothing with it - start: start, + start: 0, // backend is already biased by start (if non-zero) via Sub above size: size, backend: b, superblock: s, From cdc917957c78876a11fd453622d122a24384ead0 Mon Sep 17 00:00:00 2001 From: eriknordmark Date: Fri, 29 May 2026 20:41:37 +0200 Subject: [PATCH 2/2] squashfs: drop unused FileSystem.start field The FileSystem.start field was only ever written (to 0 once the offset fix wraps the backend in backend.Sub) and never read anywhere in the package, so it carried no information. Remove it. The partition offset now lives entirely in the Sub-wrapped backend; the start parameter of Create and Read is unchanged. Signed-off-by: eriknordmark Co-Authored-By: Claude Sonnet 4.6 --- filesystem/squashfs/const_internal_test.go | 1 - filesystem/squashfs/squashfs.go | 3 --- 2 files changed, 4 deletions(-) diff --git a/filesystem/squashfs/const_internal_test.go b/filesystem/squashfs/const_internal_test.go index abbabb7..8a8530b 100644 --- a/filesystem/squashfs/const_internal_test.go +++ b/filesystem/squashfs/const_internal_test.go @@ -172,7 +172,6 @@ func testGetFilesystem(f fs.File) (*FileSystem, []byte, error) { workspace: "", compressor: &CompressorGzip{}, size: 5251072, - start: 0, backend: file.New(f, true), blocksize: blocksize, xattrs: nil, diff --git a/filesystem/squashfs/squashfs.go b/filesystem/squashfs/squashfs.go index 5b9bd55..ee80b1f 100644 --- a/filesystem/squashfs/squashfs.go +++ b/filesystem/squashfs/squashfs.go @@ -27,7 +27,6 @@ type FileSystem struct { workspace string superblock *superblock size int64 - start int64 backend backend.Storage blocksize int64 compressor Compressor @@ -104,7 +103,6 @@ func Create(b backend.Storage, size, start, blocksize int64) (*FileSystem, error // there is nothing in there return &FileSystem{ workspace: tmpdir, - start: 0, size: size, backend: b, blocksize: blocksize, @@ -231,7 +229,6 @@ func Read(b backend.Storage, size, start, blocksize int64) (*FileSystem, error) fs := &FileSystem{ workspace: "", // no workspace when we do nothing with it - start: 0, // backend is already biased by start (if non-zero) via Sub above size: size, backend: b, superblock: s,