From 905c8d7fdd16cd0c99bc91458e56c452c3930db7 Mon Sep 17 00:00:00 2001 From: eriknordmark Date: Fri, 29 May 2026 19:27:13 +0200 Subject: [PATCH] sync: allow target partition larger than source verifyBlockCopy required both partitions' full ReadContents byte count to equal expectedSize, so copying a smaller source into a larger target failed even when the leading bytes matched. That larger-target shape is the normal one when growing a partition before a later swap-and-delete renames it into place. Require each side to hold at least expectedSize bytes, hash exactly expectedSize bytes per side via NewLimitWriter, and drop the equality check on the target's full size. A smaller-than-expected partition is still treated as an error. Closes #403. Signed-off-by: eriknordmark Co-Authored-By: Claude Sonnet 4.6 --- sync/verify.go | 35 +++++++++----- sync/verify_blockcopy_test.go | 88 +++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 12 deletions(-) create mode 100644 sync/verify_blockcopy_test.go diff --git a/sync/verify.go b/sync/verify.go index cdd5ff01..0c8e1a9b 100644 --- a/sync/verify.go +++ b/sync/verify.go @@ -21,26 +21,37 @@ func verifyBlockCopy(d *disk.Disk, from, to int, expectedSize int64) error { if err != nil { return err } - // create a sha256sum of both partitions and compare - // but limit it to expectedSize + + // Source partition must hold at least expectedSize bytes — the + // copy reported writing that many, so reading less than that back + // indicates a real inconsistency. + if got := origPart.GetSize(); got < expectedSize { + return fmt.Errorf("original partition size %d is smaller than expected size %d", got, expectedSize) + } + // Target partition must hold at least expectedSize bytes; it may + // be larger (the common case when growing a partition before a + // later swap+delete renames it into place). The verification only + // needs to confirm the leading expectedSize bytes match. + if got := targetPart.GetSize(); got < expectedSize { + return fmt.Errorf("target partition size %d is smaller than expected size %d", got, expectedSize) + } + + // Hash the leading expectedSize bytes of both partitions and + // compare. NewLimitWriter caps the hasher's input, and + // origPart.ReadContents would otherwise feed all of the source + // partition's bytes into the hasher — for our purposes, we want + // only expectedSize bytes hashed on each side regardless of how + // large either partition is. origHasher := sha256.New() - size, err := origPart.ReadContents(d.Backend, origHasher) - if err != nil { + if _, err := origPart.ReadContents(d.Backend, NewLimitWriter(origHasher, expectedSize)); err != nil { return err } - if size != expectedSize { - return fmt.Errorf("original partition size %d is different than expected size %d", size, expectedSize) - } origResult := origHasher.Sum(nil) targetHasher := sha256.New() - size, err = targetPart.ReadContents(d.Backend, NewLimitWriter(targetHasher, expectedSize)) - if err != nil { + if _, err := targetPart.ReadContents(d.Backend, NewLimitWriter(targetHasher, expectedSize)); err != nil { return err } - if size != expectedSize { - return fmt.Errorf("target partition size %d is different than expected size %d", size, expectedSize) - } targetResult := targetHasher.Sum(nil) if !bytes.Equal(origResult, targetResult) { diff --git a/sync/verify_blockcopy_test.go b/sync/verify_blockcopy_test.go new file mode 100644 index 00000000..b2dc0d91 --- /dev/null +++ b/sync/verify_blockcopy_test.go @@ -0,0 +1,88 @@ +package sync + +import ( + "os" + "path/filepath" + "testing" + + diskfs "github.com/diskfs/go-diskfs" + "github.com/diskfs/go-diskfs/disk" + "github.com/diskfs/go-diskfs/partition/gpt" +) + +// TestVerifyBlockCopyTargetLargerThanSource covers the grow case: a smaller +// source partition copied into a larger target. verifyBlockCopy must compare +// only the leading expectedSize bytes and accept a target that is larger than +// the source, while still catching a real mismatch within that region. +func TestVerifyBlockCopyTargetLargerThanSource(t *testing.T) { + const ( + sectorSize = 512 + diskSize = 64 * 1024 * 1024 + srcStart = 2048 // sectors => 1 MiB in + tgtStart = srcStart + 32768 // 16 MiB after source start + srcSize = 8 * 1024 * 1024 // source partition + tgtSize = 24 * 1024 * 1024 // target is 3x larger + expected = int64(srcSize) + ) + + imgPath := filepath.Join(t.TempDir(), "disk.img") + d, err := diskfs.Create(imgPath, diskSize, sectorSize) + if err != nil { + t.Fatalf("create disk: %v", err) + } + table := &gpt.Table{ + LogicalSectorSize: sectorSize, + PhysicalSectorSize: sectorSize, + ProtectiveMBR: true, + Partitions: []*gpt.Partition{ + {Index: 1, Start: srcStart, Size: srcSize, Type: gpt.LinuxFilesystem, Name: "source"}, + {Index: 2, Start: tgtStart, Size: tgtSize, Type: gpt.LinuxFilesystem, Name: "target"}, + }, + } + if err := d.Partition(table); err != nil { + t.Fatalf("write partition table: %v", err) + } + + // Write identical leading expectedSize bytes into both partitions; the + // target's trailing bytes stay zero and must not affect the result. + content := make([]byte, expected) + for i := range content { + content[i] = byte(i*7 + 1) + } + writeAt := func(off int64, b []byte) { + t.Helper() + img, err := os.OpenFile(imgPath, os.O_RDWR, 0) + if err != nil { + t.Fatalf("open image for write: %v", err) + } + defer img.Close() + if _, err := img.WriteAt(b, off); err != nil { + t.Fatalf("write image at %d: %v", off, err) + } + } + writeAt(srcStart*sectorSize, content) + writeAt(tgtStart*sectorSize, content) + + openDisk := func() *disk.Disk { + t.Helper() + dd, err := diskfs.Open(imgPath, diskfs.WithSectorSize(sectorSize)) + if err != nil { + t.Fatalf("open disk: %v", err) + } + if _, err := dd.GetPartitionTable(); err != nil { + t.Fatalf("read partition table: %v", err) + } + return dd + } + + // Grow case: larger target, identical leading bytes — must verify clean. + if err := verifyBlockCopy(openDisk(), 1, 2, expected); err != nil { + t.Errorf("grow with identical leading bytes: unexpected error: %v", err) + } + + // Corrupt a byte inside the compared region of the target; must be caught. + writeAt(tgtStart*sectorSize, []byte{^content[0]}) + if err := verifyBlockCopy(openDisk(), 1, 2, expected); err == nil { + t.Error("corrupted target within compared region: expected mismatch error, got nil") + } +}