Skip to content

Linux: cbm_system_info / cbm_default_worker_count don't respect cgroup CPU/memory limits #363

Closed

Description

@yangsec888

Summary

When CBM runs inside a Linux container with cgroup CPU and/or memory
limits (the normal Kubernetes / Docker / Nomad / systemd-slice case),
cbm_system_info() and cbm_default_worker_count() report
node-level CPU count and RAM, not the cgroup's effective limits.
This produces three concrete operator-visible problems:

  1. cbm_default_worker_count(initial=true) returns
    sysconf(_SC_NPROCESSORS_ONLN), which on Linux is the number of
    online host CPUs. A 1-vCPU container scheduled onto a 16-core
    node spawns ~16 indexing workers, each with its own per-worker
    buffers (AST stacks, tree-sitter parsers, slab allocators).
    In a memory-constrained container this is the dominant OOMKill
    driver — far more so than g_budget (which is observability-only;
    cbm_mem_over_budget() and cbm_vmem_over_budget() are declared
    but never called).
  2. mem.init budget_mb=… log line is computed from the host's
    sysinfo(2).totalram, so a 2 GiB-limited pod on a 62 GB node
    logs budget_mb=31000, which is alarming and confuses incident
    response. (Cosmetic, but compounds 1.)
  3. The over-budget worker count also magnifies the blast radius of
    Dump-phase crash on large TS monorepo (v0.6.1, darwin-arm64) — pipeline completes through semantic_edges then terminates abnormally before gbuf.dump #317 (dump-phase OOM crash on large TS monorepo) and Silent index corruption after rapid kill/restart cycles #334 (silent
    index corruption after rapid kill/restart) — both of which we have
    reproduced in the wild on Kubernetes pods.

Where in the source

Verified against main at HEAD 22153563cd1072b4f79e0e27113f6e0dea3abc1a:

Suggested fix shape

Two reads in detect_system_linux(), both with safe fallbacks to
the existing host-scoped values:

  1. CPU count — read cgroup v2 cpu.max (preferred) or v1
    cpu.cfs_quota_us / cpu.cfs_period_us. If the result is
    max / unlimited / parse error, fall back to
    sysconf(_SC_NPROCESSORS_ONLN).
  2. Memory — read cgroup v2 memory.max (preferred) or v1
    memory.limit_in_bytes. If the result is max / unlimited or
    exceeds host total, fall back to sysinfo(2).totalram * mem_unit.

Both files live at well-known paths (/sys/fs/cgroup/... for v2,
/sys/fs/cgroup/<controller>/... for v1) and are simple text
reads. No new dependencies.

A reasonable cap for safety: min(cgroup_cpu, host_cpu) and
min(cgroup_mem, host_mem). This is robust against
mis-mounted-cgroups edge cases.

Compatible with existing env knobs

Once cgroup awareness lands, an explicit env-override remains useful
for ops who want to tune below cgroup limits (e.g. leave headroom
for sibling processes in the same container). A companion PR (filed
alongside this issue) adds CBM_WORKERS for that case. The two
together give: env > cgroup > host as the precedence chain,
matching the CBM_SQLITE_MMAP_SIZE precedent from commit 093707c.

Out of scope

  • macOS, BSD, Windows containerization stories (none of them have
    the same sysconf/sysinfo problem in practice).
  • mimalloc tuning. mimalloc respects its own arena settings; the
    request here is just to compute the inputs CBM passes to its
    worker scheduler and budget logger correctly.

Related

Activity

  1. cryptomaltese commented on Jun 2, 2026

    @cryptomaltese

    Reopen candidate — the cgroup-respect doesn't take effect under a systemd --user --scope memory limit on v0.7.0.

    Running v0.7.0 inside an 8 GB transient scope, mem.init still reports the host RAM, not the cgroup limit:

    $ systemd-run --user --scope -q -p MemoryMax=8G -p MemorySwapMax=0 -- \
        codebase-memory-mcp cli list_projects '{}'
    level=info msg=mem.init budget_mb=15913 total_ram_mb=31826
    

    total_ram_mb=31826 is the host's ~32 GB; budget_mb=15913 is ~50% of host, not of the 8 GB cgroup. Outside the scope it logs the same numbers, i.e. cbm_system_info is reading host RAM regardless of the enclosing cgroup v2 memory.max.

    Repro: the one-liner above (cgroup v2, systemd user manager). Expected: when the process's cgroup has a memory.max/memory.high below host RAM, the budget should derive from that (e.g. read /sys/fs/cgroup/<leaf>/memory.max for the current PID's cgroup, not the root).

    Context: we run cm-mcp inside an 8 GB scope specifically to bound the no-file-size-cap index OOM. The kernel hard-kill still protects us, but the intended self-sizing from this issue would be the softer, better guard — and it isn't kicking in here.

    Environment: v0.7.0, Linux cgroup v2, systemd --user.

  2. yangsec888 commented on Jun 3, 2026

    @yangsec888
    ContributorAuthor

    @cryptomaltese the repro is real but it's a version-skew issue, not a bug in the cgroup code. The cgroup-aware detection landed on main ~16 hours after v0.7.0 was tagged:

    Commit When What
    b133a20 2026-05-30 01:15 +0200 release: bump pkg/npm + pkg/pypi to v0.7.0
    d952238 2026-05-30 17:32 +0200 feat(platform): CBM_WORKERS env override for cbm_default_worker_count
    a5a3d1d 2026-05-30 17:47 +0200 feat(platform): cgroup-aware CPU/memory detection in detect_system_linux

    git tag --contains a5a3d1d returns empty — neither commit is in any tagged release yet, so a v0.7.0 binary will still read host RAM via sysinfo(2) exactly as you observed.

    Building from main at HEAD (currently 22fe481) and re-running your systemd-run --user --scope -q -p MemoryMax=8G ... repro should show mem.init deriving from /sys/fs/cgroup/<leaf>/memory.max instead.

    @DeusData — would you be open to cutting v0.7.1 as a patch release that pulls in d952238 + a5a3d1d (plus anything else queued)? It would unblock containerized deployments without having to ship from source. Happy to help test the release candidate against our 2 GiB k8s pod setup.

  3. DeusData commented on Jun 12, 2026

    @DeusData
    Owner

    Confirmed, and thanks @yangsec888 for pinning down the version skew — the cgroup-aware sizing (d952238) missed the v0.7.0 tag by hours and first shipped in v0.8.0. If you can confirm correct worker counts under your cgroup limits on v0.8.1, I'll close this out.

  4. yangsec888 commented on Jun 13, 2026

    @yangsec888
    ContributorAuthor

    Confirmed on v0.8.1 in our EKS pod (cgroup v2, limits: 2 CPU / 8 GiB; host: 8 CPU / ~31 GiB).

    Memory (no override): mem.init budget_mb=4096 total_ram_mb=8192 — derives from the 8 GiB cgroup, not host RAM.
    CPU, with CBM_WORKERS unset to isolate auto-detection (400-file repo):
    pipeline.mode mode=parallel workers=2 files=400
    parallel.extract.start files=400 workers=2
    parallel.mem.budget total_mb=4096 per_worker_mb=2048

    workers=2 matches the cgroup CPU quota rather than the 8 host CPUs. (In production we also pin CBM_WORKERS=2 explicitly, but the cgroup path lands on the same value on its own.)

    Good to close from our side — thanks for the version-skew confirmation.

  5. mayurpise commented on Jun 23, 2026

    @mayurpise

    Corroboration from the bare-metal (non-container) case — v0.8.1

    Confirming this from the angle the issue lists as out-of-scope-ish: a developer running CBM directly on a workstation, no container, no cgroup limit set. The cgroup-read fix proposed here is correct and necessary for pods, but it won't help this case (there's no cgroup to read), and it's a common one — so flagging it for the same fix's design.

    Repro (v0.8.1, linux-amd64, 121 GB host, bare metal):

    $ codebase-memory-mcp cli index_repository '{}'
    level=info msg=mem.init budget_mb=62305 total_ram_mb=124610   # exactly 50% of MemTotal
    
    • total_ram_mb is host MemTotal; budget_mb is 50% of it — matches g_budget = info.total_ram * 0.5.
    • Confirmed not cgroup-aware: running under systemd-run --user --scope -p MemoryMax=8G still logs total_ram_mb=124610 (host), as expected from the sysinfo(2) path you cite.
    • No env override exists today for the memory budget. Brute-forced the obvious candidates — CBM_MEM_BUDGET_MB, CBM_MEMORY_BUDGET_MB, CBM_MEM_BUDGET, CBM_BUDGET_MB, CBM_MAX_MEM_MB, CBM_RAM_PCT, CBM_MEM_PCT, GOMEMLIMIT — none move budget_mb. (CBM_WORKERS from the companion PR is the right lever; just noting there's no memory-axis knob yet.)

    Operator-visible impact on bare metal: on a 64 GB workstation, cbm_default_worker_count = full host nprocs and the 50%-of-RAM framing means a first index of a large repo (~50M LOC) climbed to ~30 GB peak and swap-thrashed the whole desktop into a hang — taking unrelated apps down with it. Consistent with your point that worker overspawn (not g_budget, which is observability-only) is the real OOM driver.

    Two asks for the bare-metal path, alongside the cgroup fix:

    1. Keep the precedence chain you describe (env > cgroup > host) on the memory axis too, not just CPU/workers — i.e. an explicit memory-budget override (or a configurable fraction) so workstation users can leave headroom for the rest of the machine when there's no cgroup to express it.
    2. Consider a less aggressive default worker count for very large repos when running un-cgrouped, since full-host-nprocs × per-worker buffers is the blast radius.

    Mitigation that works today for bare-metal users (no engine change needed): wrap the index call in a transient memory-bounded scope. The engine isn't cgroup-aware so it doesn't adopt the limit, but the kernel still enforces it — MemoryHigh throttles via reclaim (slower, but the host stays responsive) and MemoryMax is a hard ceiling:

    systemd-run --user --scope -q \
      -p MemoryHigh=25% -p MemoryMax=30% \
      -- codebase-memory-mcp cli index_repository '{"repo_path":"..."}'

    We've shipped this as the default in our downstream (Draft) — index_repository now always runs inside a 25%-RAM scope with CBM_WORKERS capped, which fully resolved the hang. Sharing in case it helps other bare-metal users before the cgroup/env work lands. Happy to test a build with a CBM_MEM_BUDGET_MB-style knob.

  6. added a commit that references this issue on Jun 26, 2026
  7. DeusData commented on Jun 28, 2026

    @DeusData
    Owner

    This issue now has a first-class, durable reproduction in the cumulative bug-reproduction suite landing in #667 — a test that pins the exact failing behaviour, so it's tracked and guarded against silent regression. Where #667 also lands the root-cause fix, that's called out in the PR; otherwise the reproduction makes the eventual fix straightforward and verifiable.

    The reproduction goes live once #667 merges. (Posted from the release branch's QA pass.)

  8. added
    priority/highNeeds near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker.
    on Jul 1, 2026
  9. added a commit that references this issue on Jul 8, 2026
  10. yangsec888 commented on Jul 8, 2026

    @yangsec888
    ContributorAuthor

    Both axes of #363 are now addressed on main:

    • CPU — cgroup-aware cbm_detect_cgroup_cpus() + the CBM_WORKERS override (tested in test_platform.c).
    • Memory — cgroup-aware cbm_detect_cgroup_mem() applying min(cgroup, host), plus the CBM_MEM_BUDGET_MB override. The override is being hardened in feat(mem): pure resolver + strict parse + clamp for CBM_MEM_BUDGET_MB #954: the parse + clamp logic is extracted into a pure, testable cbm_mem_resolve_budget() with strict parsing (rejects trailing garbage / ERANGE / non-positive), clamp-to-effective-RAM, and source= / mem.budget.clamped logging.

    The repro_issue363 guard is green and has been refreshed to match the shipped, clamped semantics.

    This looks closeable — flagging for maintainer confirmation.

  11. added this to the 0.9.1-rc milestone on Jul 8, 2026
  12. DeusData commented on Aug 20, 2026

    @DeusData
    Owner

    Thanks @yangsec888 for the detailed container report and follow-through, and thanks everyone who verified the behavior across Kubernetes, systemd scopes, and bare metal. Closing this as resolved: current main applies cgroup v1/v2 CPU and memory limits to system sizing, caps them against host capacity, supports CBM_WORKERS and CBM_MEM_BUDGET_MB overrides, and carries focused platform, budget, and repro_issue363 regression coverage. The original reporter also confirmed the automatic CPU and memory sizing under a constrained EKS pod.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingpriority/highNeeds near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker.stability/performanceServer crashes, OOM, hangs, high CPU/memory

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions