From b60e3322102c7c73dc84336169bbff261ec5f7b9 Mon Sep 17 00:00:00 2001 From: nileshpatil6 Date: Sun, 23 Aug 2026 23:34:23 +0530 Subject: [PATCH] ext4/dmverity: use io.ReadFull when reading the super block and root hash ReadDMVerityInfoReader takes an io.Reader and calls r.Read(block) once per block, treating a return of fewer than blockSize bytes as a failure: if s, err := r.Read(block); err != nil || s != blockSize { io.Reader explicitly permits a Read to return fewer bytes than requested without that being an error, so a hash device that is entirely valid fails to parse whenever it arrives through a reader that returns short reads. Only an *os.File happens to satisfy the current assumption. Use io.ReadFull for both blocks. A genuinely truncated device still reports the same errors: io.ReadFull returns io.EOF when nothing could be read and io.ErrUnexpectedEOF on a partial block, so the existing "unexpected bytes read" message and the ErrSuperBlockReadFailure and ErrRootHashReadFailure wrapping are preserved. Adds a test that feeds the same bytes through a reader returning one byte per call and checks the root digest matches the whole-block read. Signed-off-by: nileshpatil6 --- ext4/dmverity/dmverity.go | 19 +++++++++------- ext4/dmverity/dmverity_test.go | 41 ++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 8 deletions(-) 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)