triedb/pathdb: validate restart count and key offsets in trienode decoder

decodeRestartTrailer computed the minimum trailer size as int(8*nRestarts)+4.
nRestarts is a uint32, so the multiplication was evaluated in 32-bit and wrapped
for counts >= 1<<29, leaving the guard as len(keySection) < 4, which the caller
has already excluded. The subsequent offset read then indexed the section with a
negative value and panicked.

The decoded key offsets were also only checked for monotonicity, never against
the end of the key data, so an offset past the limit panicked when the section
was sliced.

Perform the size computation in 64-bit, bound each key offset by the key limit,
and defer the slice preallocation until after the size check.
This commit is contained in:
ozpool 2026-07-16 11:52:47 +05:30
parent 06b23b4293
commit 802eb91441
No known key found for this signature in database
2 changed files with 29 additions and 5 deletions

View file

@ -397,20 +397,25 @@ func decodeRestartTrailer(keySection []byte) ([]uint32, []uint32, int, error) {
}
nRestarts := binary.BigEndian.Uint32(keySection[len(keySection)-4:])
// Decode the trailer
// Decode the trailer. The multiplication is performed in 64-bit to prevent
// the result from wrapping around for large restart counts.
if uint64(len(keySection)) < uint64(nRestarts)*8+4 {
return nil, nil, 0, fmt.Errorf("key section too short, restarts: %d, size: %d", nRestarts, len(keySection))
}
var (
keyOffsets = make([]uint32, 0, int(nRestarts))
valOffsets = make([]uint32, 0, int(nRestarts))
keyLimit = len(keySection) - 4 - int(nRestarts)*8 // End of key data
)
if len(keySection) < int(8*nRestarts)+4 {
return nil, nil, 0, fmt.Errorf("key section too short, restarts: %d, size: %d", nRestarts, len(keySection))
}
for i := range int(nRestarts) {
o := len(keySection) - 4 - (int(nRestarts)-i)*8
keyOffset := binary.BigEndian.Uint32(keySection[o : o+4])
if i != 0 && keyOffset <= keyOffsets[i-1] {
return nil, nil, 0, fmt.Errorf("key offset is out of order, prev: %v, cur: %v", keyOffsets[i-1], keyOffset)
}
if int(keyOffset) > keyLimit {
return nil, nil, 0, fmt.Errorf("key offset is out of range, offset: %d, limit: %d", keyOffset, keyLimit)
}
keyOffsets = append(keyOffsets, keyOffset)
// Same value offset is allowed just in case all the trie nodes in the last
@ -421,7 +426,6 @@ func decodeRestartTrailer(keySection []byte) ([]uint32, []uint32, int, error) {
}
valOffsets = append(valOffsets, valOffset)
}
keyLimit := len(keySection) - 4 - int(nRestarts)*8 // End of key data
return keyOffsets, valOffsets, keyLimit, nil
}

View file

@ -656,6 +656,26 @@ func TestDecodeSingleCorruptedData(t *testing.T) {
if err == nil {
t.Fatal("Expected error for invalid restart count")
}
// Test with a restart count large enough to overflow the trailer size
// computation (8*nRestarts wraps around for nRestarts >= 1<<29).
corrupted = make([]byte, len(keySection))
copy(corrupted, keySection)
binary.BigEndian.PutUint32(corrupted[len(corrupted)-4:], 1<<29)
err = decodeSingle(corrupted, nil)
if err == nil {
t.Fatal("Expected error for overflowing restart count")
}
// Test with a key offset pointing beyond the end of the key data
corrupted = make([]byte, 12)
binary.BigEndian.PutUint32(corrupted[0:4], 0xffffffff) // key offset
binary.BigEndian.PutUint32(corrupted[4:8], 0) // value offset
binary.BigEndian.PutUint32(corrupted[8:12], 1) // restart count
err = decodeSingle(corrupted, nil)
if err == nil {
t.Fatal("Expected error for out-of-range key offset")
}
}
// Helper function to test encode/decode cycle