Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
🐹
🐹 Go
Not in CISA KEV
MEDIUM severity

GHSA-fw38-pc54-jvx9

MEDIUM

GHSA-fw38-pc54-jvx9 is a medium-severity (CVSS 5.9) Uncontrolled Resource Consumption vulnerability in github.com/klever-io/klever-go. O3 Security confirms whether GHSA-fw38-pc54-jvx9 is actually reachable in your code before you act, and blocks exploitation at runtime until you patch.

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

Also known asCVE-2026-49343GO-2026-5379
Published
Jun 5, 2026
Updated
Jun 25, 2026
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Aug 24, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

Exploitation Status

Proof-of-concept exploit code exists

  • CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.

Exploitation and automatability from CISA’s SSVC triage for GHSA-fw38-pc54-jvx9.

EPSS Exploitation Probability

via FIRST.org ↗
0.3%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs18th percentile — riskier than 18% of all scored CVEsHighest risk

EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.

How urgent is this, really

GHSA-fw38-pc54-jvx9 plotted by exploitation likelihood (EPSS) against impact (CVSS). The shaded corner — EPSS 50%+ and CVSS 7.0+ — is where this CVE doesn't sit, though severity or exploitability alone can still warrant action.

Where this sits among everything scored

Of 0 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Real counts from FIRST.org, not a sample — log-scaled since the landscape is heavily right-skewed.

Real-World Exposure

1 pkg affected
🐹github.com/klever-io/klever-go

Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects Go packages — download data is not available via public APIs for these ecosystems.

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():

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-data trie sync attempts stop making progress
  • the parent loop waits until context timeout
  • SyncAccounts() fails
  • epoch bootstrap fails

This is a core node availability issue. It affects fresh/restarting nodes and validators that need to bootstrap or resync state.

This is not a theoretical issue:

  • StartSyncing() performs network-dependent trie-node retrieval
  • it already has explicit timeout / failure paths
  • the leaked throttler slots are confirmed by runtime PoC

Recommended Fix

Release the slot with defer immediately after StartProcessing() and cancel the defer only if ownership is intentionally transferred, which is not the case here.

Example fix pattern:

  func (u *userAccountsSyncer) syncDataTrie(rootHash []byte, ssh data.SyncStatisticsHandler, ctx context.Context) error {
      u.throttler.StartProcessing()
      defer u.throttler.EndProcessing()

      u.syncerMutex.Lock()
      defer u.syncerMutex.Unlock()

      if _, ok := u.dataTries[string(rootHash)]; ok {
          return nil
      }

      dataTrie, err := trie.NewTrie(...)
      if err != nil {
          return err
      }

      trieSyncer, err := trie.NewTrieSyncer(arg)
      if err != nil {
          return err
      }

      u.trieSyncers[string(rootHash)] = trieSyncer
      return trieSyncer.StartSyncing(rootHash, ctx)
  }

The same pattern should be applied to:

  • data/syncer/userAccountsSyncer.go
  • data/syncer/kappAccountsSyncer.go

References

  • data/syncer/userAccountsSyncer.go
  • data/syncer/kappAccountsSyncer.go
  • data/trie/sync.go
  • core/throttler/numGoRoutinesThrottler.go
  • core/bootstrap/process.go
  • SECURITY.md

Affected Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
🐹Gogithub.com/klever-io/klever-goall versions1.7.18

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for github.com/klever-io/klever-go. O3's reachability analysis confirms whether the vulnerable code path is actually invoked in your application, so you act on real exposure instead of every transitive match.

  2. Fix

    Update github.com/klever-io/klever-go to 1.7.18 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-fw38-pc54-jvx9 is resolved across your whole dependency graph.

  3. Workarounds

    If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.

  4. How O3 protects you

    O3 pinpoints whether GHSA-fw38-pc54-jvx9 is reachable in your code and exactly where to fix it, then blocks exploitation in production at runtime until the patched version is deployed.

Tailored to GHSA-fw38-pc54-jvx9. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.

Frequently Asked Questions

## 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 affec
O3 Security · Impact-Aware SCA

Is GHSA-fw38-pc54-jvx9 in your dependencies?

O3 detects GHSA-fw38-pc54-jvx9 across Go dependencies and uses function-level reachability to confirm whether the vulnerable code path is actually reachable — not just present. No false positives.

GHSA-fw38-pc54-jvx9: klever-go (Medium 5.9) | O3 Security