GitHub Advisoryadvisory · Go

GHSA-fw38-pc54-jvx9 Klever-Go KVM: Throttler slot leak in trie account-data sync causes epoch bootstrap / state sync DoS

MEDIUM Published 2026-06-05 Modified 2026-06-05 Official page ↗
Aliases
CVE-2026-49343
Ecosystem
Go
Packages
github.com/klever-io/klever-go
Severity
Moderate
CVSS vector
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H
Weaknesses
CWE-400, CWE-772
Reviewed
2026-06-05

Affected packages

• Go github.com/klever-io/klever-go: < 1.7.18 → fixed in 1.7.18

Description

Summary The account-data trie syncers leak bounded throttler slots on error paths in `syncDataTrie()`. Each failed trie sync permanently consumes one slot from the `NumGoRoutinesThrottler`, and the slot is never returned unless the sync succeeds or the root hash was already present. I confirmed this on the current default branch `develop` at commit `9640d63` (observed on May 20, 2026). I also confirmed the bug with a runtime PoC using the real timeout path in `trieSyncer.StartSyncing()`: two timed-out sync attempts are enough to exhaust a throttler with capacity `2`. This affects the epoch bootstrap path because `syncUserAccountsState()` and `syncKappAccountsState()` create bounded throttlers and abort bootstrap immediately if the syncer returns an error. Once enough trie-root sync attempts fail, the syncer cannot make forward progress and bootstrap fails. ## Affected Components • `data/syncer/userAccountsSyncer.go` • `data/syncer/kappAccountsSyncer.go` • `data/trie/sync.go` • `core/throttler/numGoRoutinesThrottler.go` • `core/bootstrap/process.go` ## Affected Version Verified on: • `develop` HEAD `9640d63` Please check whether the same code is present in supported `1.7.x` releases. ## Suggested Severity High ## Vulnerability Details ### Root Cause Both account-data syncers call `StartProcessing()` before creating / starting the trie syncer, but they only call `EndProcessing()` on the success path and on the duplicate-root early return. `userAccountsSyncer.syncDataTrie()`: ```go func (u *userAccountsSyncer) syncDataTrie(rootHash []byte, ssh data.SyncStatisticsHandler, ctx context.Context) error { u.throttler.StartProcessing() u.syncerMutex.Lock() if _, ok := u.dataTries[string(rootHash)]; ok { u.syncerMutex.Unlock() u.throttler.EndProcessing() return nil } dataTrie, err := trie.NewTrie(...) if err != nil { u.syncerMutex.Unlock() return err } trieSyncer, err := trie.NewTrieSyncer(arg) if err != nil { u.syncerMutex.Unlock() return err } u.syncerMutex.Unlock() err = trieSyncer.StartSyncing(rootHash, ctx) if err != nil { return err } u.throttler.EndProcessing() return nil } The same bug exists in kappAccountsSyncer.syncDataTrie(). ``` ### Missing slot release paths After StartProcessing(), the following error paths return without EndProcessing(): 1. trie.NewTrie(...) returns an error 2. trie.NewTrieSyncer(...) returns an error 3. trieSyncer.StartSyncing(...) returns an error ### Why this matters NumGoRoutinesThrottler is a strict bounded counter: ``` func (ngrt *NumGoRoutinesThrottler) CanProcess() bool { valCounter := atomic.LoadInt32(&ngrt.counter) return valCounter < ngrt.max } func (ngrt *NumGoRoutinesThrottler) StartProcessing() { atomic.AddInt32(&ngrt.counter, 1) } func (ngrt *NumGoRoutinesThrottler) EndProcessing() { atomic.AddInt32(&ngrt.counter, -1) } Once leaked, a slot remains consumed for the lifetime of that throttler instance. The parent loops in both syncers wait for capacity before starting the next account-data trie sync: for !u.throttler.CanProcess() { select { case <-time.After(timeBetweenRetries): continue case <-ctx.Done(): return common.ErrTimeIsOut } } ``` So after enough failures, further roots stop progressing and the sync operation eventually returns time is out. ### Bootstrap impact Epoch bootstrap uses these syncers directly and aborts on any error: ``` err = e.syncUserAccountsState(e.epochStartMeta.Header.TrieRoot) if err != nil { return nil, nil, err } err = e.syncKappAccountsState(e.epochStartMeta.Header.KAppsTrieRoot) if err != nil { return nil, nil, err } ``` The throttlers for these paths are real bounded throttlers created from numConcurrentTrieSyncers. ## Proof of Concept I verified the bug with the real timeout path, not only with a canceled context. The PoC below uses: • a real NumGoRoutinesThrottler with capacity 2 • a real trieSyncer.StartSyncing() • an empty trie-node cache and a request handler that never supplies nodes • a short sync timeout (1s) so StartSyncing() returns trie.ErrTimeIsOut After the first failed sync, one slot remains leaked. After the second failed sync, the throttler is exhausted. ### PoC test ``` package syncer import ( "context" "testing" "time" commonmock "github.com/klever-io/klever-go/common/mock" corethrottler "github.com/klever-io/klever-go/core/throttler" "github.com/klever-io/klever-go/data" "github.com/klever-io/klever-go/data/trie" triestats "github.com/klever-io/klever-go/data/trie/statistics" "github.com/stretchr/testify/require" ) func newBaseSyncerForTimeoutPOC(t *testing.T) *baseAccountsSyncer { t.Helper() storageManager, err := trie.NewTrieStorageManagerWithoutPruning(commonmock.NewMemDbMock()) require.NoError(t, err) return &baseAccountsSyncer{ hasher: commonmock.HasherMock{}, marshalizer: &commonmock.MarshalizerMock{}, trieSyncers: make(map[string]data.TrieSyncer), dataTries: make(map[string]data.Trie), trieStorageManager: storageManager, requestHandler: &commonmock.RequestHandlerStub{}, timeout: time.Second, cacher: commonmock.NewCacherStub(), maxTrieLevelInMemory: 5, name: "timeout-poc", maxHardCapForMissingNodes: 1, } } func TestPOC_UserAccountsSyncer_LeaksThrottlerSlotOnTrieTimeout(t *testing.T) { thr, err := corethrottler.NewNumGoRoutinesThrottler(2) require.NoError(t, err) s := &userAccountsSyncer{ baseAccountsSyncer: newBaseSyncerForTimeoutPOC(t), throttler: thr, } err = s.syncDataTrie([]byte("missing-root-1"), triestats.NewTrieSyncStatistics(), context.Background()) require.ErrorIs(t, err, trie.ErrTimeIsOut) require.True(t, thr.CanProcess()) err = s.syncDataTrie([]byte("missing-root-2"), triestats.NewTrieSyncStatistics(), context.Background()) require.ErrorIs(t, err, trie.ErrTimeIsOut) require.False(t, thr.CanProcess()) } func TestPOC_KappAccountsSyncer_LeaksThrottlerSlotOnTrieTimeout(t *testing.T) { thr, err := corethrottler.NewNumGoRoutinesThrottler(2) require.NoError(t, err) s := &kappAccountsSyncer{ baseAccountsSyncer: newBaseSyncerForTimeoutPOC(t), throttler: thr, } err = s.syncDataTrie([]byte("missing-root-1"), triestats.NewTrieSyncStatistics(), context.Background()) require.ErrorIs(t, err, trie.ErrTimeIsOut) require.True(t, thr.CanProcess()) err = s.syncDataTrie([]byte("missing-root-2"), triestats.NewTrieSyncStatistics(), context.Background()) require.ErrorIs(t, err, trie.ErrTimeIsOut) require.False(t, thr.CanProcess()) } ``` ### Command used ``` go test ./data/syncer -run 'TestPOC_(User|Kapp)AccountsSyncer_LeaksThrottlerSlotOnTrieTimeout' -count=1 ``` ### Result ``` ok github.com/klever-io/klever-go/data/syncer 4.005s ``` This confirms the leak with the real timeout path from trieSyncer.StartSyncing(). ## Impact An attacker who can repeatedly cause trie-node sync failures or timeouts during bootstrap can consume the bounded sync throttler until no capacity remains. Once enough slots are leaked: • additional account-dat…

Related

Alias · CVE (1)

  • CVE-2026-49343 Klever-Go is the Go implementation of the Klever blockchain protocol. vulnerability

Weakness · CWE (2)

  • CWE-400 Uncontrolled Resource Consumption weakness
  • CWE-772 Missing Release of Resource after Effective Lifetime weakness

References