Skip to content

Commit 6c1f75b

Browse files
authored
Merge pull request #233 from CosmWasm/restrict-querier-redispatched-gas
Restrict querier redispatched gas
2 parents 789ef54 + d600958 commit 6c1f75b

File tree

4 files changed

+163
-67
lines changed

4 files changed

+163
-67
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module github.com/CosmWasm/wasmd
33
go 1.13
44

55
require (
6-
github.com/CosmWasm/go-cosmwasm v0.10.0-alpha2
6+
github.com/CosmWasm/go-cosmwasm v0.10.0-alpha4
77
github.com/cosmos/cosmos-sdk v0.39.1-0.20200727135228-9d00f712e334
88
github.com/golang/mock v1.4.3 // indirect
99
github.com/google/gofuzz v1.0.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ
99
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
1010
github.com/ChainSafe/go-schnorrkel v0.0.0-20200405005733-88cbf1b4c40d h1:nalkkPQcITbvhmL4+C4cKA87NW0tfm3Kl9VXRoPywFg=
1111
github.com/ChainSafe/go-schnorrkel v0.0.0-20200405005733-88cbf1b4c40d/go.mod h1:URdX5+vg25ts3aCh8H5IFZybJYKWhJHYMTnf+ULtoC4=
12-
github.com/CosmWasm/go-cosmwasm v0.10.0-alpha2 h1:k069wQF0f24w3A52mjR3AK6AfkuvQ5d0Mdu/rpB5nEk=
13-
github.com/CosmWasm/go-cosmwasm v0.10.0-alpha2/go.mod h1:gAFCwllx97ejI+m9SqJQrmd2SBW7HA0fOjvWWJjM2uc=
12+
github.com/CosmWasm/go-cosmwasm v0.10.0-alpha4 h1:1a3j/vdhnyYvUV+67Hg3GU87M9wn1jR6bReXDwy+TZQ=
13+
github.com/CosmWasm/go-cosmwasm v0.10.0-alpha4/go.mod h1:gAFCwllx97ejI+m9SqJQrmd2SBW7HA0fOjvWWJjM2uc=
1414
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
1515
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
1616
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=

x/wasm/internal/keeper/query_plugins.go

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package keeper
22

33
import (
44
"encoding/json"
5-
65
wasmTypes "github.com/CosmWasm/go-cosmwasm/types"
76
sdk "github.com/cosmos/cosmos-sdk/types"
87
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
@@ -17,18 +16,28 @@ type QueryHandler struct {
1716

1817
var _ wasmTypes.Querier = QueryHandler{}
1918

20-
func (q QueryHandler) Query(request wasmTypes.QueryRequest) ([]byte, error) {
19+
func (q QueryHandler) Query(request wasmTypes.QueryRequest, gasLimit uint64) ([]byte, error) {
20+
// set a limit for a subctx
21+
sdkGas := gasLimit / GasMultiplier
22+
subctx := q.Ctx.WithGasMeter(sdk.NewGasMeter(sdkGas))
23+
24+
// make sure we charge the higher level context even on panic
25+
defer func() {
26+
q.Ctx.GasMeter().ConsumeGas(subctx.GasMeter().GasConsumed(), "contract sub-query")
27+
}()
28+
29+
// do the query
2130
if request.Bank != nil {
22-
return q.Plugins.Bank(q.Ctx, request.Bank)
31+
return q.Plugins.Bank(subctx, request.Bank)
2332
}
2433
if request.Custom != nil {
25-
return q.Plugins.Custom(q.Ctx, request.Custom)
34+
return q.Plugins.Custom(subctx, request.Custom)
2635
}
2736
if request.Staking != nil {
28-
return q.Plugins.Staking(q.Ctx, request.Staking)
37+
return q.Plugins.Staking(subctx, request.Staking)
2938
}
3039
if request.Wasm != nil {
31-
return q.Plugins.Wasm(q.Ctx, request.Wasm)
40+
return q.Plugins.Wasm(subctx, request.Wasm)
3241
}
3342
return nil, wasmTypes.Unknown{}
3443
}

x/wasm/internal/keeper/recurse_test.go

Lines changed: 145 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,16 @@ package keeper
22

33
import (
44
"encoding/json"
5-
abci "github.com/tendermint/tendermint/abci/types"
65
"io/ioutil"
76
"os"
87
"testing"
98

9+
wasmTypes "github.com/CosmWasm/go-cosmwasm/types"
1010
"github.com/stretchr/testify/assert"
1111
"github.com/stretchr/testify/require"
1212

1313
sdk "github.com/cosmos/cosmos-sdk/types"
14+
abci "github.com/tendermint/tendermint/abci/types"
1415
)
1516

1617
type Recurse struct {
@@ -34,6 +35,50 @@ type recurseResponse struct {
3435
Hashed []byte `json:"hashed"`
3536
}
3637

38+
// number os wasm queries called from a contract
39+
var totalWasmQueryCounter int
40+
41+
func initRecurseContract(t *testing.T) (contract sdk.AccAddress, creator sdk.AccAddress, ctx sdk.Context, keeper Keeper, cleanup func()) {
42+
// we do one basic setup before all test cases (which are read-only and don't change state)
43+
tempDir, err := ioutil.TempDir("", "wasm")
44+
require.NoError(t, err)
45+
cleanup = func() { os.RemoveAll(tempDir) }
46+
47+
var realWasmQuerier func(ctx sdk.Context, request *wasmTypes.WasmQuery) ([]byte, error)
48+
countingQuerier := &QueryPlugins{
49+
Wasm: func(ctx sdk.Context, request *wasmTypes.WasmQuery) ([]byte, error) {
50+
totalWasmQueryCounter++
51+
return realWasmQuerier(ctx, request)
52+
},
53+
}
54+
ctx, keepers := CreateTestInput(t, false, tempDir, SupportedFeatures, nil, countingQuerier)
55+
accKeeper, keeper := keepers.AccountKeeper, keepers.WasmKeeper
56+
realWasmQuerier = WasmQuerier(&keeper)
57+
58+
deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000))
59+
creator = createFakeFundedAccount(ctx, accKeeper, deposit.Add(deposit...))
60+
61+
// store the code
62+
wasmCode, err := ioutil.ReadFile("./testdata/contract.wasm")
63+
require.NoError(t, err)
64+
codeID, err := keeper.Create(ctx, creator, wasmCode, "", "", nil)
65+
require.NoError(t, err)
66+
67+
// instantiate the contract
68+
_, _, bob := keyPubAddr()
69+
_, _, fred := keyPubAddr()
70+
initMsg := InitMsg{
71+
Verifier: fred,
72+
Beneficiary: bob,
73+
}
74+
initMsgBz, err := json.Marshal(initMsg)
75+
require.NoError(t, err)
76+
contractAddr, err := keeper.Instantiate(ctx, codeID, creator, nil, initMsgBz, "recursive contract", deposit)
77+
require.NoError(t, err)
78+
79+
return contractAddr, creator, ctx, keeper, cleanup
80+
}
81+
3782
func TestGasCostOnQuery(t *testing.T) {
3883
const (
3984
GasNoWork uint64 = InstanceCost + 2_756
@@ -87,33 +132,8 @@ func TestGasCostOnQuery(t *testing.T) {
87132
},
88133
}
89134

90-
// we do one basic setup before all test cases (which are read-only and don't change state)
91-
tempDir, err := ioutil.TempDir("", "wasm")
92-
require.NoError(t, err)
93-
defer os.RemoveAll(tempDir)
94-
95-
ctx, keepers := CreateTestInput(t, false, tempDir, SupportedFeatures, nil, nil)
96-
accKeeper, keeper := keepers.AccountKeeper, keepers.WasmKeeper
97-
deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000))
98-
creator := createFakeFundedAccount(ctx, accKeeper, deposit.Add(deposit...))
99-
100-
// store the code
101-
wasmCode, err := ioutil.ReadFile("./testdata/contract.wasm")
102-
require.NoError(t, err)
103-
codeID, err := keeper.Create(ctx, creator, wasmCode, "", "", nil)
104-
require.NoError(t, err)
105-
106-
// instantiate the contract
107-
_, _, bob := keyPubAddr()
108-
_, _, fred := keyPubAddr()
109-
initMsg := InitMsg{
110-
Verifier: fred,
111-
Beneficiary: bob,
112-
}
113-
initMsgBz, err := json.Marshal(initMsg)
114-
require.NoError(t, err)
115-
contractAddr, err := keeper.Instantiate(ctx, codeID, creator, nil, initMsgBz, "recursive contract", deposit)
116-
require.NoError(t, err)
135+
contractAddr, creator, ctx, keeper, cleanup := initRecurseContract(t)
136+
defer cleanup()
117137

118138
for name, tc := range cases {
119139
t.Run(name, func(t *testing.T) {
@@ -149,8 +169,7 @@ func TestGasCostOnQuery(t *testing.T) {
149169

150170
func TestGasOnExternalQuery(t *testing.T) {
151171
const (
152-
GasWork50 uint64 = InstanceCost + 8_464
153-
GasReturnHashed uint64 = 597
172+
GasWork50 uint64 = InstanceCost + 8_464
154173
)
155174

156175
cases := map[string]struct {
@@ -190,33 +209,8 @@ func TestGasOnExternalQuery(t *testing.T) {
190209
},
191210
}
192211

193-
// we do one basic setup before all test cases (which are read-only and don't change state)
194-
tempDir, err := ioutil.TempDir("", "wasm")
195-
require.NoError(t, err)
196-
defer os.RemoveAll(tempDir)
197-
198-
ctx, keepers := CreateTestInput(t, false, tempDir, SupportedFeatures, nil, nil)
199-
accKeeper, keeper := keepers.AccountKeeper, keepers.WasmKeeper
200-
deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000))
201-
creator := createFakeFundedAccount(ctx, accKeeper, deposit.Add(deposit...))
202-
203-
// store the code
204-
wasmCode, err := ioutil.ReadFile("./testdata/contract.wasm")
205-
require.NoError(t, err)
206-
codeID, err := keeper.Create(ctx, creator, wasmCode, "", "", nil)
207-
require.NoError(t, err)
208-
209-
// instantiate the contract
210-
_, _, bob := keyPubAddr()
211-
_, _, fred := keyPubAddr()
212-
initMsg := InitMsg{
213-
Verifier: fred,
214-
Beneficiary: bob,
215-
}
216-
initMsgBz, err := json.Marshal(initMsg)
217-
require.NoError(t, err)
218-
contractAddr, err := keeper.Instantiate(ctx, codeID, creator, nil, initMsgBz, "recursive contract", deposit)
219-
require.NoError(t, err)
212+
contractAddr, _, ctx, keeper, cleanup := initRecurseContract(t)
213+
defer cleanup()
220214

221215
for name, tc := range cases {
222216
t.Run(name, func(t *testing.T) {
@@ -233,7 +227,8 @@ func TestGasOnExternalQuery(t *testing.T) {
233227
if tc.expectPanic {
234228
require.Panics(t, func() {
235229
// this should run out of gas
236-
_, _ = NewQuerier(keeper)(ctx, path, req)
230+
_, err := NewQuerier(keeper)(ctx, path, req)
231+
t.Logf("%v", err)
237232
})
238233
} else {
239234
// otherwise, make sure we get a good success
@@ -243,3 +238,95 @@ func TestGasOnExternalQuery(t *testing.T) {
243238
})
244239
}
245240
}
241+
242+
func TestLimitRecursiveQueryGas(t *testing.T) {
243+
// The point of this test from https://github.com/CosmWasm/cosmwasm/issues/456
244+
// Basically, if I burn 90% of gas in CPU loop, then query out (to my self)
245+
// the sub-query will have all the original gas (minus the 40k instance charge)
246+
// and can burn 90% and call a sub-contract again...
247+
// This attack would allow us to use far more than the provided gas before
248+
// eventually hitting an OutOfGas panic.
249+
250+
const (
251+
// Note: about 100 SDK gas (10k wasmer gas) for each round of sha256
252+
GasWork2k uint64 = InstanceCost + 233_379 // we have 6x gas used in cpu than in the instance
253+
// This is overhead for calling into a sub-contract
254+
GasReturnHashed uint64 = 603
255+
)
256+
257+
cases := map[string]struct {
258+
gasLimit uint64
259+
msg Recurse
260+
expectQueriesFromContract int
261+
expectedGas uint64
262+
expectOutOfGas bool
263+
}{
264+
"no recursion, lots of work": {
265+
gasLimit: 4_000_000,
266+
msg: Recurse{
267+
Depth: 0,
268+
Work: 2000,
269+
},
270+
expectQueriesFromContract: 0,
271+
expectedGas: GasWork2k,
272+
},
273+
"recursion 5, lots of work": {
274+
gasLimit: 4_000_000,
275+
msg: Recurse{
276+
Depth: 5,
277+
Work: 2000,
278+
},
279+
expectQueriesFromContract: 5,
280+
expectedGas: GasWork2k + 5*(GasWork2k+GasReturnHashed),
281+
},
282+
// this is where we expect an error...
283+
// it has enough gas to run 4 times and die on the 5th (4th time dispatching to sub-contract)
284+
// however, if we don't charge the cpu gas before sub-dispatching, we can recurse over 20 times
285+
// TODO: figure out how to asset how deep it went
286+
"deep recursion, should die on 5th level": {
287+
gasLimit: 1_200_000,
288+
msg: Recurse{
289+
Depth: 50,
290+
Work: 2000,
291+
},
292+
expectQueriesFromContract: 4,
293+
expectOutOfGas: true,
294+
},
295+
}
296+
297+
contractAddr, _, ctx, keeper, cleanup := initRecurseContract(t)
298+
defer cleanup()
299+
300+
for name, tc := range cases {
301+
t.Run(name, func(t *testing.T) {
302+
// reset the counter before test
303+
totalWasmQueryCounter = 0
304+
305+
// make sure we set a limit before calling
306+
ctx = ctx.WithGasMeter(sdk.NewGasMeter(tc.gasLimit))
307+
require.Equal(t, uint64(0), ctx.GasMeter().GasConsumed())
308+
309+
// prepare the query
310+
recurse := tc.msg
311+
recurse.Contract = contractAddr
312+
msg := buildQuery(t, recurse)
313+
314+
// if we expect out of gas, make sure this panics
315+
if tc.expectOutOfGas {
316+
require.Panics(t, func() {
317+
_, err := keeper.QuerySmart(ctx, contractAddr, msg)
318+
t.Logf("Got error not panic: %#v", err)
319+
})
320+
assert.Equal(t, tc.expectQueriesFromContract, totalWasmQueryCounter)
321+
return
322+
}
323+
324+
// otherwise, we expect a successful call
325+
_, err := keeper.QuerySmart(ctx, contractAddr, msg)
326+
require.NoError(t, err)
327+
assert.Equal(t, tc.expectedGas, ctx.GasMeter().GasConsumed())
328+
329+
assert.Equal(t, tc.expectQueriesFromContract, totalWasmQueryCounter)
330+
})
331+
}
332+
}

0 commit comments

Comments
 (0)