diff --git a/ext4/dmverity/dmverity.go b/ext4/dmverity/dmverity.go index 6e91c4bbc5..5a1ea82f8c 100644 --- a/ext4/dmverity/dmverity.go +++ b/ext4/dmverity/dmverity.go @@ -183,11 +183,14 @@ func ReadDMVerityInfo(vhdPath string, offsetInBytes int64) (*VerityInfo, error) func ReadDMVerityInfoReader(r io.Reader) (*VerityInfo, error) { block := make([]byte, blockSize) - if s, err := r.Read(block); err != nil || s != blockSize { - if err != nil { - return nil, fmt.Errorf("%w: %w", ErrSuperBlockReadFailure, err) + // io.Reader is allowed to return fewer bytes than requested without that + // being an error, so read the whole block rather than relying on a single + // Read filling it. + if s, err := io.ReadFull(r, block); err != nil { + if errors.Is(err, io.ErrUnexpectedEOF) { + return nil, fmt.Errorf("unexpected bytes read expected=%d actual=%d: %w", blockSize, s, ErrSuperBlockReadFailure) } - return nil, fmt.Errorf("unexpected bytes read expected=%d actual=%d: %w", blockSize, s, ErrSuperBlockReadFailure) + return nil, fmt.Errorf("%w: %w", ErrSuperBlockReadFailure, err) } dmvSB := &dmveritySuperblock{} @@ -200,11 +203,11 @@ func ReadDMVerityInfoReader(r io.Reader) (*VerityInfo, error) { return nil, ErrNotVeritySuperBlock } - if s, err := r.Read(block); err != nil || s != blockSize { - if err != nil { - return nil, fmt.Errorf("%w: %w", ErrRootHashReadFailure, err) + if s, err := io.ReadFull(r, block); err != nil { + if errors.Is(err, io.ErrUnexpectedEOF) { + return nil, fmt.Errorf("unexpected bytes read expected=%d, actual=%d: %w", blockSize, s, ErrRootHashReadFailure) } - return nil, fmt.Errorf("unexpected bytes read expected=%d, actual=%d: %w", blockSize, s, ErrRootHashReadFailure) + return nil, fmt.Errorf("%w: %w", ErrRootHashReadFailure, err) } rootHash := hash2(dmvSB.Salt[:dmvSB.SaltSize], block) diff --git a/ext4/dmverity/dmverity_test.go b/ext4/dmverity/dmverity_test.go index f425500924..83b066093c 100644 --- a/ext4/dmverity/dmverity_test.go +++ b/ext4/dmverity/dmverity_test.go @@ -72,6 +72,47 @@ func TestInvalidReadNotEnoughBytes(t *testing.T) { } } +// shortReader returns at most n bytes per Read, which io.Reader explicitly +// permits. A reader like this still delivers the whole hash device. +type shortReader struct { + r io.Reader + n int +} + +func (s *shortReader) Read(p []byte) (int, error) { + if len(p) > s.n { + p = p[:s.n] + } + return s.r.Read(p) +} + +func TestReadDMVerityInfoReaderShortReads(t *testing.T) { + tmpFile := tempFileWithContentLength(t, blockSize) + targetFile, err := writeDMVeritySuperBlock(tmpFile.Name()) + if err != nil { + t.Fatalf("failed to write dm-verity super-block: %s", err) + } + content, err := os.ReadFile(targetFile.Name()) + if err != nil { + t.Fatalf("failed to read temp file: %s", err) + } + // super block plus one block of root hash data + content = append(content[blockSize:], bytes.Repeat([]byte{1}, blockSize)...) + + want, err := ReadDMVerityInfoReader(bytes.NewReader(content)) + if err != nil { + t.Fatalf("failed to read verity info from a whole-block reader: %s", err) + } + + got, err := ReadDMVerityInfoReader(&shortReader{r: bytes.NewReader(content), n: 1}) + if err != nil { + t.Fatalf("failed to read verity info from a short reader: %s", err) + } + if got.RootDigest != want.RootDigest { + t.Fatalf("root digest mismatch: short reader got %q, want %q", got.RootDigest, want.RootDigest) + } +} + func TestNotVeritySuperBlock(t *testing.T) { tmpFile := tempFileWithContentLength(t, 2*blockSize) _, err := ReadDMVerityInfo(tmpFile.Name(), blockSize)