Skip to content

[Bug]: Multiple Unbounded Module-Level Caches Cause Memory Growth #6034

@coygeek

Description

@coygeek

CVSS Assessment

Metric Value
Score 5.3 / 10.0
Severity Medium
Vector CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:N/I:N/A:H

CVSS v3.1 Calculator

Summary

Several modules maintain unbounded in-memory caches without TTL or size limits. While keyed by legitimate IDs, these caches grow monotonically over the gateway's lifetime, eventually causing memory exhaustion.

Affected Code

File 1: extensions/mattermost/src/mattermost/send.ts:31-32

const botUserCache = new Map<string, MattermostUser>();
const userByNameCache = new Map<string, MattermostUser>();
// No TTL, no size limit - caches user objects indefinitely

File 2: extensions/matrix/src/matrix/send/targets.ts:21

const directRoomCache = new Map<string, string>();
// No TTL, no size limit - caches room mappings indefinitely

File 3: src/media-understanding/runner.ts:88-89

const binaryCache = new Map<string, Promise<string | null>>();
const geminiProbeCache = new Map<string, Promise<boolean>>();
// Caches Promise results indefinitely for binary path lookups and gemini CLI probes

File 4: src/discord/monitor/presence-cache.ts:7

const presenceCache = new Map<string, Map<string, GatewayPresenceUpdate>>();
// Nested Map caching Discord presence updates with no TTL or size limit
// Particularly concerning for bots in large servers with many users

Additional instances (non-exhaustive):

  • extensions/tlon/src/monitor/history.ts:12 - messageCache
  • extensions/zalo/src/proxy.ts:6 - proxyCache
  • ui/src/ui/markdown.ts:46 - markdownCache
  • src/imessage/probe.ts:25 - rpcSupportCache
  • src/plugins/loader.ts:42 - registryCache
  • extensions/googlechat/src/auth.ts:12 - authCache
  • src/telegram/sent-message-cache.ts:13 - sentMessages

Contrast with good patterns:

BlueBubbles has proper cache with TTL:

// extensions/bluebubbles/src/probe.ts:20-21
const serverInfoCache = new Map<string, { info: BlueBubblesServerInfo; expires: number }>();
const CACHE_TTL_MS = 10 * 60 * 1000; // 10 minutes

Mattermost monitor.ts uses proper expiry:

// extensions/mattermost/src/mattermost/monitor.ts:277-278
const channelCache = new Map<string, { value: MattermostChannel | null; expiresAt: number }>();
const userCache = new Map<string, { value: MattermostUser | null; expiresAt: number }>();

Attack Surface

How is this reached?

  • Network (HTTP/WebSocket endpoint, API call)

Authentication required?

  • Low (any authenticated user)

Entry point: Normal usage patterns - sending messages, looking up users, processing media, Discord presence updates. Not directly attackable but causes gradual degradation.

Exploit Conditions

Complexity:

  • High (requires race condition, specific config, or timing)

User interaction:

  • None (automatic, no victim action needed)

Prerequisites: Long-running gateway process with active usage across Mattermost, Matrix, Discord, or media processing features.

Impact Assessment

Scope:

  • Unchanged (impact limited to vulnerable component)

What can an attacker do?

Impact Type Level Description
Confidentiality None No data exposure
Integrity None No data modification
Availability High Gradual memory exhaustion over weeks/months

Steps to Reproduce

  1. Deploy gateway with Mattermost/Matrix/Discord integrations active
  2. Use normally for weeks/months
  3. Monitor memory usage:
    # Log memory every hour
    while true; do
      echo "$(date): $(ps aux | grep openclaw | awk '{print $6}')" >> memory.log
      sleep 3600
    done
  4. Observe: Memory grows monotonically, never decreases
  5. Plot memory.log: see linear growth over time
  6. Eventually: OOM or swap thrashing degrades performance

Recommended Fix

Apply the existing createDedupeCache pattern or add explicit cleanup:

import { createDedupeCache } from "../infra/dedupe.js";

// Option 1: Use existing bounded cache utility (for deduplication scenarios)
const recentMessages = createDedupeCache({
  ttlMs: 3600000,  // 1 hour
  maxSize: 1000,
});

// Option 2: Add TTL-based expiry (following BlueBubbles pattern)
const CACHE_TTL_MS = 10 * 60 * 1000; // 10 minutes
const botUserCache = new Map<string, { user: MattermostUser; expires: number }>();

function getCachedBotUser(key: string): MattermostUser | null {
  const cached = botUserCache.get(key);
  if (cached && cached.expires > Date.now()) {
    return cached.user;
  }
  return null;
}

// Option 3: Add periodic cleanup
const CACHE_MAX_AGE_MS = 3600000;
const CACHE_CLEANUP_INTERVAL_MS = 300000;

setInterval(() => {
  const now = Date.now();
  for (const [key, entry] of cache) {
    if (now - entry.cachedAt > CACHE_MAX_AGE_MS) {
      cache.delete(key);
    }
  }
}, CACHE_CLEANUP_INTERVAL_MS);

References

  • CWE: CWE-770 - Allocation of Resources Without Limits or Throttling
  • Related: BlueBubbles extension demonstrates the correct cache pattern at extensions/bluebubbles/src/probe.ts:20-21
  • Related: createDedupeCache utility at src/infra/dedupe.ts provides bounded caching with TTL and maxSize

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions