-
-
Notifications
You must be signed in to change notification settings - Fork 69k
[Bug]: Multiple Unbounded Module-Level Caches Cause Memory Growth #6034
Description
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 |
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 indefinitelyFile 2: extensions/matrix/src/matrix/send/targets.ts:21
const directRoomCache = new Map<string, string>();
// No TTL, no size limit - caches room mappings indefinitelyFile 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 probesFile 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 usersAdditional instances (non-exhaustive):
extensions/tlon/src/monitor/history.ts:12-messageCacheextensions/zalo/src/proxy.ts:6-proxyCacheui/src/ui/markdown.ts:46-markdownCachesrc/imessage/probe.ts:25-rpcSupportCachesrc/plugins/loader.ts:42-registryCacheextensions/googlechat/src/auth.ts:12-authCachesrc/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 minutesMattermost 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
- Deploy gateway with Mattermost/Matrix/Discord integrations active
- Use normally for weeks/months
- Monitor memory usage:
# Log memory every hour while true; do echo "$(date): $(ps aux | grep openclaw | awk '{print $6}')" >> memory.log sleep 3600 done
- Observe: Memory grows monotonically, never decreases
- Plot memory.log: see linear growth over time
- 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:
createDedupeCacheutility atsrc/infra/dedupe.tsprovides bounded caching with TTL and maxSize