diff --git a/store/gaskv/store.go b/store/gaskv/store.go index 75d379a03e2f..027710ed1a87 100644 --- a/store/gaskv/store.go +++ b/store/gaskv/store.go @@ -2,12 +2,28 @@ package gaskv import ( "io" + "math" "cosmossdk.io/store/types" ) var _ types.KVStore = &Store{} +// multiplyGasSafely performs multiplication of gas cost per byte with byte length +// and returns the result or panics with ErrorGasOverflow if overflow occurs. +func multiplyGasSafely(costPerByte types.Gas, byteLength int) types.Gas { + if costPerByte == 0 || byteLength == 0 { + return 0 + } + + // Check for overflow: if costPerByte > math.MaxUint64 / byteLength, then overflow + if costPerByte > math.MaxUint64/uint64(byteLength) { + panic(types.ErrorGasOverflow{Descriptor: "gas calculation overflow"}) + } + + return costPerByte * types.Gas(byteLength) +} + // Store applies gas tracking to an underlying KVStore. It implements the // KVStore interface. type Store struct { @@ -37,9 +53,9 @@ func (gs *Store) Get(key []byte) (value []byte) { gs.gasMeter.ConsumeGas(gs.gasConfig.ReadCostFlat, types.GasReadCostFlatDesc) value = gs.parent.Get(key) - // TODO overflow-safe math? - gs.gasMeter.ConsumeGas(gs.gasConfig.ReadCostPerByte*types.Gas(len(key)), types.GasReadPerByteDesc) - gs.gasMeter.ConsumeGas(gs.gasConfig.ReadCostPerByte*types.Gas(len(value)), types.GasReadPerByteDesc) + // Use overflow-safe math for gas calculation + gs.gasMeter.ConsumeGas(multiplyGasSafely(gs.gasConfig.ReadCostPerByte, len(key)), types.GasReadPerByteDesc) + gs.gasMeter.ConsumeGas(multiplyGasSafely(gs.gasConfig.ReadCostPerByte, len(value)), types.GasReadPerByteDesc) return value } @@ -49,9 +65,9 @@ func (gs *Store) Set(key, value []byte) { types.AssertValidKey(key) types.AssertValidValue(value) gs.gasMeter.ConsumeGas(gs.gasConfig.WriteCostFlat, types.GasWriteCostFlatDesc) - // TODO overflow-safe math? - gs.gasMeter.ConsumeGas(gs.gasConfig.WriteCostPerByte*types.Gas(len(key)), types.GasWritePerByteDesc) - gs.gasMeter.ConsumeGas(gs.gasConfig.WriteCostPerByte*types.Gas(len(value)), types.GasWritePerByteDesc) + // Use overflow-safe math for gas calculation + gs.gasMeter.ConsumeGas(multiplyGasSafely(gs.gasConfig.WriteCostPerByte, len(key)), types.GasWritePerByteDesc) + gs.gasMeter.ConsumeGas(multiplyGasSafely(gs.gasConfig.WriteCostPerByte, len(value)), types.GasWritePerByteDesc) gs.parent.Set(key, value) } @@ -170,8 +186,9 @@ func (gi *gasIterator) consumeSeekGas() { key := gi.Key() value := gi.Value() - gi.gasMeter.ConsumeGas(gi.gasConfig.ReadCostPerByte*types.Gas(len(key)), types.GasValuePerByteDesc) - gi.gasMeter.ConsumeGas(gi.gasConfig.ReadCostPerByte*types.Gas(len(value)), types.GasValuePerByteDesc) + // Use overflow-safe math for gas calculation + gi.gasMeter.ConsumeGas(multiplyGasSafely(gi.gasConfig.ReadCostPerByte, len(key)), types.GasValuePerByteDesc) + gi.gasMeter.ConsumeGas(multiplyGasSafely(gi.gasConfig.ReadCostPerByte, len(value)), types.GasValuePerByteDesc) } gi.gasMeter.ConsumeGas(gi.gasConfig.IterNextCostFlat, types.GasIterNextCostFlatDesc) } diff --git a/store/gaskv/store_test.go b/store/gaskv/store_test.go index 354832d17c40..b1d54b5a628e 100644 --- a/store/gaskv/store_test.go +++ b/store/gaskv/store_test.go @@ -2,6 +2,7 @@ package gaskv_test import ( "fmt" + "math" "testing" dbm "github.com/cosmos/cosmos-db" @@ -118,3 +119,78 @@ func TestGasKVStoreOutOfGasIterator(t *testing.T) { iterator.Next() require.Panics(t, func() { iterator.Value() }, "Expected out-of-gas") } + +func TestGasKVStoreOverflowSafeMath(t *testing.T) { + t.Parallel() + + // Test with very large values that would cause overflow + mem := dbadapter.Store{DB: dbm.NewMemDB()} + meter := types.NewInfiniteGasMeter() // Use infinite meter to avoid out-of-gas + + // Create a gas config with high per-byte costs to trigger overflow + // Use a value that when multiplied by MaxKeyLength would cause overflow + highCostPerByte := types.Gas(math.MaxUint64/uint64(types.MaxKeyLength) + 1) + config := types.GasConfig{ + HasCost: 1000, + DeleteCost: 1000, + ReadCostFlat: 1000, + ReadCostPerByte: highCostPerByte, + WriteCostFlat: 2000, + WriteCostPerByte: highCostPerByte, + IterNextCostFlat: 30, + } + + st := gaskv.NewStore(mem, meter, config) + + // Test Get with large key that would cause overflow + largeKey := make([]byte, types.MaxKeyLength) // Max key length * high cost > MaxUint64 + require.Panics(t, func() { st.Get(largeKey) }, "Expected gas overflow for large key in Get") + + // Test Set with large value that would cause overflow + smallKey := []byte("key") + largeValue := make([]byte, types.MaxKeyLength) // Max key length * high cost > MaxUint64 + require.Panics(t, func() { st.Set(smallKey, largeValue) }, "Expected gas overflow for large value in Set") + + // Test that normal operations still work with lower cost + normalConfig := types.GasConfig{ + HasCost: 1000, + DeleteCost: 1000, + ReadCostFlat: 1000, + ReadCostPerByte: 1, // Low cost per byte + WriteCostFlat: 2000, + WriteCostPerByte: 1, // Low cost per byte + IterNextCostFlat: 30, + } + normalSt := gaskv.NewStore(mem, meter, normalConfig) + normalKey := []byte("normal") + normalValue := []byte("value") + require.NotPanics(t, func() { normalSt.Set(normalKey, normalValue) }, "Normal operations should not panic") + require.NotPanics(t, func() { normalSt.Get(normalKey) }, "Normal operations should not panic") +} + +func TestGasKVStoreOverflowSafeMathWithZeroCost(t *testing.T) { + t.Parallel() + + // Test with zero cost per byte (should not cause overflow) + mem := dbadapter.Store{DB: dbm.NewMemDB()} + meter := types.NewInfiniteGasMeter() + + config := types.GasConfig{ + HasCost: 1000, + DeleteCost: 1000, + ReadCostFlat: 1000, + ReadCostPerByte: 0, // Zero cost per byte + WriteCostFlat: 2000, + WriteCostPerByte: 0, // Zero cost per byte + IterNextCostFlat: 30, + } + + st := gaskv.NewStore(mem, meter, config) + + // Test with reasonably large data within limits - should not panic with zero cost + largeKey := make([]byte, 1000) // Within MaxKeyLength limit + largeValue := make([]byte, 1000) // Within MaxValueLength limit + + require.NotPanics(t, func() { st.Set(largeKey, largeValue) }, "Zero cost should not cause overflow") + require.NotPanics(t, func() { st.Get(largeKey) }, "Zero cost should not cause overflow") +}