Reject a metadata database truncated to the age header and nonce (closes #152)
check / check (pull_request) Successful in 2m33s

An object holding exactly the age header plus its 16-byte nonce decrypts
without error: age.Decrypt succeeds, and on the first read the age stream's
io.ErrUnexpectedEOF is mapped by the zstd decoder to a clean io.EOF at frame
start. blobgen.Reader.Read then reported zero bytes and no error, so a
truncated stream was indistinguishable from a valid empty one. Blobs are
caught by the Close-time hash check, but the metadata database export was not:
restore built a fresh schema on the empty file and exited reporting success.

blobgen.Reader.Read now, on EOF, reads once more from the age reader and
surfaces io.ErrUnexpectedEOF unless that read is (0, io.EOF) — the state a
genuine end leaves behind, so an empty payload still round-trips to empty with
no error. downloadSnapshotDB additionally rejects a zero-length decrypted
database before OpenReadOnly applies a schema.

On next the decrypted-DB identity check already rejects an empty database, so
the restore-level test guards the required end-to-end property; the blobgen
tests are the ones that fail without the reader change.

Model: opus-4-8
This commit is contained in:
2026-09-22 15:12:48 +00:00
parent 1244c9e48d
commit 03f126edc1
4 changed files with 177 additions and 0 deletions
+17
View File
@@ -2,6 +2,7 @@ package blobgen
import (
"crypto/sha256"
"errors"
"fmt"
"hash"
"io"
@@ -56,6 +57,22 @@ func (r *Reader) Read(p []byte) (int, error) {
n, err := r.teeReader.Read(p)
r.bytesRead += int64(n)
// When the ciphertext is cut right after the age header plus its
// 16-byte nonce, the age reader's first read fails with
// io.ErrUnexpectedEOF, and the zstd decoder maps that to a clean
// io.EOF at frame start. That makes a truncated stream look like a
// valid empty one. Distinguish the two: on EOF, read once more from
// the age reader. A genuine end leaves it at (0, io.EOF); a truncated
// stream leaves its stored io.ErrUnexpectedEOF, which we surface.
if errors.Is(err, io.EOF) {
var probe [1]byte
m, ageErr := r.decryptor.Read(probe[:])
if m != 0 || !errors.Is(ageErr, io.EOF) {
return n, io.ErrUnexpectedEOF
}
}
return n, err
}