fix: validate physical record length > 0 and type (H1+H2)

DecodePhysicalRecord only checked length upper bound, not lower bound
(length > 0 per design §3.2 line 389). It also didn't validate the
fragment type field (RecInvalid=0 and types > RecLast are corruption
indicators per design §3.2 line 366-372).

Corrupt data with length=0 could pass CRC (payload is empty, CRC only
covers length+type bytes) and inject empty records into the fragment
collector. Invalid type values would only be caught at the collector
level, wrapped as TailCorruptionError, rather than rejected at the
parser level.

Changes:
- wal/record.go: DecodePhysicalRecord now rejects length=0 and
  type ∉ {RecFull..RecLast} before payload copy and CRC check.
  Checks are ordered to reject invalid records ASAP.
- wal/record_test.go: 5 tests:
  - TestDecodePhysicalRecord_RejectZeroLength (H1)
  - TestDecodePhysicalRecord_RejectInvalidType (H2, type=0)
  - TestDecodePhysicalRecord_RejectUnknownType (H2, type>RecLast)
  - TestDecodePhysicalRecord_ValidRecordsUnaffected (regression for
    all 4 valid types)
  - TestParseBlockWrapsInvalidRecordAsTailCorruption (integration:
    ParseBlock wraps H1/H2 errors as TailCorruptionError)

Verified: all existing tests pass. Full suite green including
go test -race ./... .

Audit context: docs/audit-3.2.md H1+H2.
This commit is contained in:
dailz
2026-06-18 13:59:04 +08:00
parent 408138b3c8
commit 57e7525ddf
3 changed files with 378 additions and 0 deletions
+10
View File
@@ -3,6 +3,7 @@ package wal
import (
"encoding/binary"
"errors"
"fmt"
"hash/crc32"
)
@@ -45,6 +46,15 @@ func DecodePhysicalRecord(data []byte) (rec *PhysicalRecord, consumed int, err e
length := binary.LittleEndian.Uint16(data[4:6])
recType := data[6]
// H1: length must be > 0 per design §3.2 line 389.
if length == 0 {
return nil, 0, errors.New("record: length must be > 0")
}
// H2: type must be valid per design §3.2 line 366-372.
if recType < RecFull || recType > RecLast {
return nil, 0, fmt.Errorf("record: invalid type %d", recType)
}
if int(length) > len(data)-PhysicalRecordHeaderSize {
return nil, 0, errors.New("record: data too short for payload")
}