Repository navigation
fix: F-2026-18818 | [Dual Defense] CallEVMWithData Ignores gasCap — Callback Gas Sandbox Broken - #46
Merged
Merged
Conversation
The parameter was accepted and ignored, so callers sandboxing EVM work got a 25M gas message regardless. Also runs the timeout callback on cachedCtx, as the ack path already does.
This was referenced Aug 26, 2026
Keeps #43's failure-path ConsumeGas alongside this branch's gasCap plumbing; updates the stale 'hardcoded DefaultGasCap' comment since a caller-supplied gasCap can now narrow it.
0xNilesh
added a commit
that referenced
this pull request
Aug 26, 2026
Keeps this branch's IsUint64 rejection and takes the updated DefaultGasCap comment from audit-fixes (#46 made the cap caller-narrowable).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Finding
CallEVMWithDataaccepts agasCap *big.Intand never reads it:Four call sites pass a real one, all silently ignored —
x/ibc/callbacks/keeper/keeper.go:192, 215, 321, 414. The intent there is unambiguous: they hand over the remaining Cosmos gas to sandbox the callback's EVM work.DerivedEVMCallWithDatahonours itsgasLimit; this helper did not.There is no backstop either. Those paths install
NewInfiniteGasMeterWithLimit(cbData.CommitGasLimit), butinfiniteGasMeterWithLimit.ConsumeGasonly panics on uint64 overflow — never on exceeding its own limit — soIsOutOfGas()is permanently false. EVM execution could therefore finish having used up toDefaultGasCap, and the laterctx.GasMeter().ConsumeGas(res.GasUsed)onto ibc-go's basicProcessCallbackmeter could then panicErrorOutOfGas.Net effect: up to ~25M gas of EVM work per callback while Cosmos charges at most
CommitGasLimit(capped at 1M) — CPU griefing against validators and relayers.Changes
CallEVMWithDatahonoursgasCap, clamped. Ported fromupstream/main, which already carries this fix, so the fork stays close to upstream:DefaultGasCapstays the ceiling — a caller can only ever narrow the limit, never widen it. A non-positive cap, or one wider than uint64, falls back to the default rather than truncating to something meaningless.Timeout callback now runs on
cachedCtx.keeper.go:414used the parentctxwhile reading its gas cap fromcachedCtxon the very next expression. The acknowledgement path usescachedCtxand says so in a comment; this was a copy-paste divergence that committed the callback's EVM state regardless of outcome and made the trailingwriteFn()a no-op. ItsConsumeGasdescriptor also saidonPacketAcknowledgementinside the timeout handler; corrected.Tests
tests/integration/x/vm/test_call_evm_gascap.go— new file, deliberately separate fromtest_call_evm.goso it does not collide with PR #43.The load-bearing assertion is that a cap below the call's cost stops the call. Asserting only that a generous cap succeeds would pass just as happily against the ignored parameter.
Mutation-verified —
GasLimitreverted to the hardcoded default, test re-run, fix restored:gasCapagainOne trap recorded in the test comments for whoever touches it next:
res.GasUsedis not a viable gas limit. It is the net, post-refund figure (~24k for this call), while the execution needs materially more than that up front — a cap of 2×GasUsedstill reverts, 10× succeeds. Do not "tighten" the headroom case tobaselineor a small multiple.Hacken's remediations
GasLimitfromgasCap, clamped — done.CommitGasLimitinsideInfiniteGasMeterWithLimit— declined. That meter is also installed byx/vm/keeper/grpc_query.go:899foreth_call/estimateGas; making it enforce would change query semantics well beyond this finding. Worth its own ticket. (Related latent bug spotted while reading it:GasConsumedToLimit()returns rawconsumed, contradicting its own doc comment, which says it caps at the limit.)ConsumeGasatmin(GasUsed, remaining)— largely subsumed. With rec 1 in placeres.GasUsedis bounded byCommitGasLimit, so the charge can only overrun whenExecutionGasLimit < CommitGasLimit— precisely the case where ibc-go'sAllowRetry()is true and re-panicking is the intended behaviour.CallEVMtocachedCtx— done.Scope note
The evm
x/ibc/callbackskeeper is not instantiated on Push.app.go:1007wiresibccallbacks.NewIBCMiddleware(transferStack, app.PacketForwardKeeper, wasmStack, maxCallbackGas), where the ContractKeeper iswasm.NewIBCHandler(...), and every node-sideCallEVM/CallEVMWithDatacall passesnil. So on Push this is currently inert: it removes a live footgun and aligns with upstream rather than closing a reachable hole.Pre-existing failures (verified against clean
audit-fixes, not caused here)x/vm/types—TestExtendedEipsx/vm/wrappers—TestMintAmountToAccountTestKeeperTestSuite/TestRefundGas/Case_invalid_GasPrice_in_messageEach reproduces identically with this branch's changes stashed.
gofmtreportsx/vm/keeper/call_evm.goas misaligned; that is pre-existing on base, inDerivedEVMCallWithData's message literal, and left alone to keep the diff focused.