Skip to content

Windows x64: v0.9.1-rc.1 daemon leaves existing cache files unreadable (empty ACL) #1351

Description

@ertankucukoglu

Version

codebase-memory-mcp 0.9.1-rc.1

Platform

Windows (x64)

Install channel

GitHub release archive / install.sh / install.ps1

Binary variant

standard

What happened, and what did you expect?

When I tested v0.9.1-rc.1 against an existing local CBM installation and cache on Windows, the new daemon/ACL path left existing cache files with an empty ACL. The same Windows user could no longer read _config.db, log files, or sampled project database files.

After that, MCP requests such as search_graph and check_index_coverage did not complete within 15–20 seconds. Starting the RC daemon again reproduced the empty ACL on _config.db.

I expected the installer/daemon either to preserve usable file permissions or to fail safely before changing existing permissions. It must not leave the current user's cache unreadable.

Reproduction

Prerequisite: Windows x64 with an existing CBM installation/cache. The existing installation or one of its parent directories has inherited cross-account ACL entries.

  1. Back up the existing CBM executable, configuration, and cache.
  2. Verify the official v0.9.1-rc.1 Windows amd64 release archive and run install.ps1 with --skip-config against the existing installation.
  3. The installer rejects the existing installation directory with: acl-grants-cross-account-mutation
  4. Retry with an owner-private temporary directory. Activation then aborts with: active CBM sessions and operations could not be stopped safely; no activation was committed.
  5. Place the checksum-verified RC binary in a user-private directory without changing agent configuration, then start the daemon: codebase-memory-mcp.exe daemon start
  6. Observe: daemon.runtime_config_open_failed reason=config_db_unavailable
  7. Inspect the existing CBM cache: _config.db, logs, and sampled project .db files have an empty ACL and are unreadable by the current user.
  8. Start the RC daemon again; the empty ACL on _config.db is reproduced.
  9. On an already-indexed 31-node project, check_index_coverage did not complete within 15 seconds through the CLI. Small MCP search_graph and check_index_coverage calls did not complete within 15–20 seconds.

No repository source code is required for this reproduction; the failure occurs before source analysis.

Logs

activation transaction I/O failed: acl-grants-cross-account-mutation
active CBM sessions and operations could not be stopped safely; no activation was committed.
daemon.runtime_config_open_failed reason=config_db_unavailable

Diagnostics trajectory (memory / performance / leak issues)

CBM_DIAGNOSTICS=1 was enabled. The daemon created a local trajectory.ndjson file. I have not attached it because it may contain local environment details; I can provide a redacted extract if needed.

Project scale (if relevant)

31 nodes / small existing project / failure occurs before source analysis

Confirmations

  • I searched existing issues and this is not a duplicate.
  • My reproduction uses shareable code (a dummy snippet or a public OSS repository), not proprietary code.

Activity

  1. added
    editor/integrationEditor compatibility and CLI integration
    parsing/qualityGraph extraction bugs, false positives, missing edges
    windowsWindows-specific issues
    on Jul 30, 2026
  2. ertankucukoglu commented on Jul 30, 2026

    @ertankucukoglu
    ContributorAuthor

    Here is the issue log in zip format.
    cbm-v0.9.1-rc.1-windows-acl-report.zip

  3. hetasshi commented on Jul 30, 2026

    @hetasshi

    I can reproduce the same issue on Windows x64 with the v0.9.1-rc.1 UI build.

    Existing stable installation with 6 indexes in:
    C:\Users\user.cache\codebase-memory-mcp

    The archive downloaded, checksum verification passed, extraction passed, and the candidate binary was verified successfully. Installation then failed during activation with:

    activation transaction I/O failed: acl-grants-cross-account-mutation to S-1-5-21-398329113-3246588833-669947113-1583456143 (os 0)

    Temporary path:
    C:\Users\user\AppData\Local\Temp\cbm-install-132218333

    The installer reported that all 6 existing indexes were being kept. I did not attempt to bypass the ACL check or start the RC daemon afterward.

  4. added
    stability/performanceServer crashes, OOM, hangs, high CPU/memory
    ux/behaviorDisplay bugs, docs, adoption UX
    priority/highNeeds near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker.
    and removed
    parsing/qualityGraph extraction bugs, false positives, missing edges
    on Aug 3, 2026
  5. added this to the 0.9.1-rc milestone on Aug 3, 2026
  6. DeusData commented on Aug 3, 2026

    @DeusData
    Owner

    Thank you for the independent Windows x64 reproduction and the ACL evidence. Current main contains explicit private-DACL creation, validation, and repair paths for daemon/cache files, so an RC run that leaves existing cache files with an empty ACL is a direct security and availability regression in that lifecycle. I have routed this at high priority in 0.9.1-rc; this should block a stable promotion until the existing-cache upgrade case is deterministic and green on Windows.

  7. DeusData commented on Aug 4, 2026

    @DeusData
    Owner

    Update from a reproduction attempt on our Windows VM: the refusal half of your report reproduces exactly — a cache tree with cross-account mutation grants (your acl-grants-cross-account-mutation, our stock Authenticated Users:(M) drive-root inheritance) makes the daemon fail closed and activation abort, and #1449 now surfaces that with the flagged SID/path instead of the misleading sessions error.

    What we could not reproduce so far is the destructive half — _config.db ending up with an empty ACL. In every variant we ran (daemon start against a pre-existing tainted cache, failed install, failed uninstall), existing files kept their owner-usable ACLs; the fail-closed paths never touched them. To pin down your empty-ACL rewrite, could you share:

    1. icacls _config.db output from a pre-upgrade backup (what the 0.9.0-era baseline looked like — especially whether the entries were inherited or explicit);
    2. whether install.ps1 ran from an elevated PowerShell (admin token changes which repair branch runs);
    3. icacls of the cache directory itself in the broken state, and whether the empty-ACL files were only ever touched by the RC daemon or also by the aborted installer run.

    With that we can reproduce the exact repair branch that wrote the empty DACL and bind it with a test. The fail-before-mutate principle you stated — never change existing permissions before validation has passed — is exactly the contract we want to enforce once we can observe the violation.

  8. ertankucukoglu commented on Aug 4, 2026

    @ertankucukoglu
    ContributorAuthor

    Thanks for checking this. I retained the pre-RC backup, the session transcript, and both damaged cache trees.

    1. Pre-upgrade ACL baseline

    One caveat: I did not capture a literal icacls _config.db output before running the RC. The backup was created with:

    robocopy /E /COPY:DAT /DCOPY:DAT

    Therefore, its security descriptor was not copied; the backup file inherited permissions from the backup destination.

    The backup copy currently reports:

    C:\Users\Ertan.cache\codebase-memory-mcp-backups\pre-v0.9.1-rc.1-20260730-064940\cache_config.db
    NT AUTHORITY\SYSTEM:(I)(F)
    BUILTIN\Administrators:(I)(F)
    DESKTOP-PQ5VAR4\Ertan:(I)(F)

    Successfully processed 1 files; Failed processing 0 files

    A live Get-Acl capture taken before any RC installer or daemon run exists for the original cache logs directory:

    Path: C:\Users\Ertan.cache\codebase-memory-mcp\logs
    Owner: DESKTOP-PQ5VAR4\Ertan

    NT AUTHORITY\SYSTEM FullControl Allow IsInherited=True
    BUILTIN\Administrators FullControl Allow IsInherited=True
    DESKTOP-PQ5VAR4\Ertan FullControl Allow IsInherited=True

    So the pre-RC cache tree was using inherited, owner-usable ACLs. However, I cannot claim that the backup copy
    preserved the original _config.db DACL byte-for-byte.

    2. Was install.ps1 elevated?

    No. It was run from the normal, non-elevated Codex PowerShell session without RunAs:

    powershell.exe -NoProfile -ExecutionPolicy Bypass -File \install.ps1 --skip-config

    ExecutionPolicy Bypass was only used to run the downloaded script; it was not an elevation.

    3. Broken cache ACL

    The second damaged tree is still preserved. It was renamed in place on the same NTFS volume rather than copied.

    The cache directory itself reports:

    C:\Users\Ertan.cache\codebase-memory-mcp-rc-daemon-acl-damaged-20260730-070935
    DESKTOP-PQ5VAR4\Ertan:(F)

    Successfully processed 1 files; Failed processing 0 files

    The _config.db output contains no ACEs:

    C:\Users\Ertan.cache\codebase-memory-mcp-rc-daemon-acl-damaged-20260730-070935_config.db

    Successfully processed 1 files; Failed processing 0 files

    The logs directory also contains no ACEs:

    C:\Users\Ertan.cache\codebase-memory-mcp-rc-daemon-acl-damaged-20260730-070935\logs

    Successfully processed 1 files; Failed processing 0 files

    Get-Acl confirms the following:

    Cache root:
    Owner: DESKTOP-PQ5VAR4\Ertan
    AreAccessRulesProtected: True
    AccessRuleCount: 1
    SDDL: O:S-1-5-21-1998637452-353888612-3906549841-1001G:S-1-5-21-1998637452-353888612-3906549841-513D:PAI(A;;FA;;;S-
    1-5-21-1998637452-353888612-3906549841-1001)

    _config.db:
    Owner: DESKTOP-PQ5VAR4\Ertan
    AreAccessRulesProtected: False
    AccessRuleCount: 0
    SDDL: O:S-1-5-21-1998637452-353888612-3906549841-1001G:S-1-5-21-1998637452-353888612-3906549841-513D:AI

    logs:
    Owner: DESKTOP-PQ5VAR4\Ertan
    AreAccessRulesProtected: False

    4. Was the installer involved in the empty-ACL rewrite?

    The first damaged tree followed both the aborted installer attempts and RC startup, so that first occurrence alone
    cannot exclude the installer.

    734CA254D1F9E137A095B5C207C8D2DCA7EA8A9E09FFF82D1EB6FB38E07D5A79.
    
    • No install.ps1 invocation occurred after this restore.
    • 2026-07-30 07:08:20 +03:00: Ran codebase-memory-mcp.exe daemon start.
    • 2026-07-30 07:08:41 +03:00: _config.db again had an empty ACL and the daemon reported that it was not running.
    • The damaged tree was then preserved as codebase-memory-mcp-rc-daemon-acl-damaged-20260730-070935.

    At inspection time, these two RC processes were visible:

    52220 codebase-memory-mcp.exe daemon start
    59184 codebase-memory-mcp.exe

    The second process was likely the MCP client/frontend. Therefore, I can rule out the aborted installer as the cause of
    the second occurrence, although the saved evidence does not distinguish the daemon-internal path from the concurrently
    present RC client process.

  9. DeusData commented on Aug 4, 2026

    @DeusData
    Owner

    Follow-up on the policy question this report raised: the permission gate stays strict — CBM will continue to refuse trees carrying cross-account mutation grants rather than operate under them, which matches the fail-safe expectation you stated ("fail safely before changing existing permissions"). What improves in the next build is the diagnosis: the refusal now names the check, the SID and the path (#1449), instead of the misleading "active CBM sessions" error you hit at step 4.

    The empty-ACL half of your report is still open and still the part I most want to pin down — it did not reproduce on our Windows VM in any variant (daemon start against a pre-existing tainted cache, failed install, failed uninstall all left existing files owner-readable). The questions from my previous comment still stand whenever you have time: the pre-upgrade icacls _config.db baseline, whether install.ps1 ran elevated, and the icacls of the cache directory in the broken state. With those three we can reproduce the exact repair branch that wrote the empty DACL and bind it with a regression test.

    Thanks again for the precise step-by-step — it's what made the refusal half diagnosable.

  10. ertankucukoglu commented on Aug 5, 2026

    @ertankucukoglu
    ContributorAuthor

    Thanks for the follow-up. I repeated the Windows x64 v0.9.1-rc.1 test on 2026-08-05 with fresh, independent copies of the preserved pre-RC cache and timestamped ACL/process watchers. The empty-ACL issue reproduced again.

    Direct answers to the three questions

    1. Exact pre-RC _config.db baseline

    Immediately before each RC1 process, _config.db was readable and had three inherited full-control ACEs:

    <scenario-cache>\_config.db NT AUTHORITY\SYSTEM:(I)(F)
                                BUILTIN\Administrators:(I)(F)
                                <MACHINE>\<USER>:(I)(F)
    

    Recorded properties:

    Owner: <MACHINE>\<USER>
    Protected: False
    RuleCount: 3
    Readable: True
    SDDL: O:<CURRENT_USER_SID>G:<MACHINE_SID>-513D:AI(A;ID;FA;;;SY)(A;ID;FA;;;BA)(A;ID;FA;;;<CURRENT_USER_SID>)
    

    2. Was install.ps1 elevated?

    No. It ran from a normal, non-elevated PowerShell token. The run recorded WindowsPrincipal.IsInRole(Administrator) = False; there was no UAC elevation.

    3. Broken cache directory ACL

    After reproduction, the cache directory itself was still owner-readable but had become protected and contained only one non-inheritable ACE:

    <scenario-cache> <MACHINE>\<USER>:(F)
    

    Recorded properties:

    Protected: True
    RuleCount: 1
    Readable: True
    SDDL: O:<CURRENT_USER_SID>G:<MACHINE_SID>-513D:PAI(A;;FA;;;<CURRENT_USER_SID>)
    

    The important detail is that this ACE has no (OI) or (CI) inheritance flags. Existing child objects then had no usable ACEs. For _config.db after reproduction:

    Protected: False
    RuleCount: 0
    Readable: False
    SDDL: O:<CURRENT_USER_SID>G:<MACHINE_SID>-513D:AI
    

    icacls <scenario-cache>\_config.db printed the path with no ACE lines.

    Retest matrix

    Isolated scenario Result Failed sampled targets
    Standalone daemon start Reproduced 4
    Controlled MCP client Reproduced 4
    install.ps1, cross-account/tainted install target Not reproduced; rejected by the strict permission gate before cache mutation 0
    install.ps1, owner-private install target Reproduced before the inner installer aborted 4

    The four failed sampled targets in each reproducing scenario were _config.db, logs, a small existing project database, and the largest existing project database. All four became unreadable with RuleCount=0 and DACL SDDL ending in D:AI.

    Timing and process correlation

    • Standalone daemon: the first empty ACL was observed at 2026-08-05T07:44:46.4920873+03:00, while the top-level daemon start process (PID 89632) was running. This was before its internal permanent-daemon child was created.
    • MCP client: the first empty ACL was observed at 2026-08-05T07:46:01.6409509+03:00, while the top-level RC1 MCP process (PID 92604) was running and just before its internal daemon child appeared.
    • Owner-private installer: the first empty ACL was observed at 2026-08-05T07:50:22.1554540+03:00, while the inner RC1 install -y --force ... --skip-config process (PID 75068) was running.

    The tainted-target installer path behaved as intended and failed before mutation with:

    activation transaction I/O failed: acl-grants-cross-account-mutation to S-1-5-11
    

    The owner-private installer path reached the inner installer, changed the isolated cache ACLs, and then reported:

    active CBM sessions and operations could not be stopped safely; no activation was committed.
    

    One additional observation: in both installer scenarios, install.ps1 itself exited with code 0 even though the inner RC1 installer printed error: installation failed (exit code 1).

    Test controls and data safety

    • Each scenario used a separate 19.565 GiB / 600-file cache copy.
    • Stable v0.9.0 successfully read each copy immediately before RC1 was started.
    • The official RC1 executable SHA-256 was 7D82CA2FAEDCE72113B17F749A63A11860B9D78CBFC73691FAB9B31CB7E71C22.
    • The active v0.9.0 cache was never passed to RC1. Its 814-file / 27.797 GiB pre/post SHA-256 manifests had zero differences, and its sampled ACLs were unchanged.
    • Codex/Claude configuration hashes were unchanged.
    • No raw database or repository source content is included in the report.

    CBM_DIAGNOSTICS=1 and CBM_LOG_LEVEL=debug were set. RC1 did not emit a new native trajectory.ndjson in the isolated scenario TEMP/evidence directories during this retest, so I used an independent English NDJSON watcher instead. It recorded local/UTC timestamps, monotonic elapsed time, ACL/readability transitions every 100 ms, and CBM PID/parent/command-line state every 500 ms.

    I have a redacted evidence bundle containing the timeline, watcher NDJSON, exact pre/post icacls and SDDL captures, stdout/stderr, and binary hashes. I can attach it if it would help with the regression test; raw databases remain local.

  11. Kiborgik commented on Aug 11, 2026

    @Kiborgik
    Contributor

    Root cause: AddAccessAllowedAce cannot express inheritance flags

    I reproduced the empty-DACL half deterministically on Windows 11 (26200) with the 0.10.0 stable release, non-elevated standard token, and traced it to source.

    Reproduction

    I repaired the cache root to a healthy state, then ran a single MCP client spawn that failed to connect:

    BEFORE  protected=False  rules=5
    D:AI(A;OICI;FA;;;<user>)(A;OICIID;0x1200a9;;;<group>)(A;OICIID;FA;;;SY)(A;OICIID;FA;;;BA)(A;OICIID;FA;;;<user>)
    
    AFTER   protected=True   rules=1
    D:PAI(A;;FA;;;<user>)
    

    Four ACEs were deleted (SYSTEM, Administrators, a local group, and the inherited user ACE), leaving one non-inheritable ACE on a protected DACL. Children created after that point get an empty DACL, which is the RuleCount=0 state reported earlier in this thread for _config.db, logs, and project databases.

    The client exited with Connection closed and rewrote the tree anyway, so the mutation happens on a run that never activates. That explains why it reproduces from daemon start, from the MCP client, and from an aborted install.

    The defect

    src/daemon/ipc.c:3664 builds the owner-only ACL like this:

    !security->add_access_allowed_ace(security->acl, ACL_REVISION, GENERIC_ALL,
                                      security->user_sid) ||

    AddAccessAllowedAce has no AceFlags parameter, so it can only emit an ACE with flags set to zero. The resolver at ipc.c:3615 binds the non-Ex variant by name. This ACL cannot carry CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE at all.

    It is then applied to directories together with PROTECTED_DACL_SECURITY_INFORMATION:

    Site Object Correct
    ipc.c:4050 CreateDirectoryW(runtime_dir, &security.attributes) directory no
    ipc.c:4097 SetSecurityInfo(directory, …, security.acl) directory no
    ipc.c:4184 CreateDirectoryW(path, &security.attributes) directory no
    ipc.c:3819, 3823 mutex kernel object yes, leaf
    ipc.c:4236, 4260 private log file file yes, leaf

    win_runtime_directory_secure is also a repair path. Line 4051 continues on ERROR_ALREADY_EXISTS and line 4093 re-stamps the existing directory unconditionally, so a tree repaired by hand with icacls is destroyed again on the next start.

    The three other owner-only ACL builders get this right by using SetEntriesInAclW with an explicit grfInheritance:

    • src/foundation/compat.c:88, SUB_CONTAINERS_AND_OBJECTS_INHERIT
    • src/foundation/compat_fs.c:454, SUB_CONTAINERS_AND_OBJECTS_INHERIT
    • src/cli/activation_transaction.c:368, NO_INHERITANCE, which is correct because it is applied only to a CREATE_NEW file at line 929

    src/foundation/private_file_lock.c resolves AddAccessAllowedAce too (line 762) and applies SE_DACL_PROTECTED, but only ever to files, so it is unaffected.

    Why it did not reproduce on your VM

    The refusal half needs a tainted tree. The destructive half does not. It fires on any run that stamps a container, including a clean profile-layout install, so a VM whose trees were already private would only show it by inspecting AceFlags on the resulting directory rather than by any failure.

    Fix

    Build a second inheritable ACL for containers using AddAccessAllowedAceEx with CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE and use it at the three directory sites. Leaves keep the existing flagless ACL.

    One trap, which I hit before getting it right: the inheritable ACE must carry specific rights (FILE_ALL_ACCESS), not GENERIC_ALL. Windows splits an inheritable generic-rights ACE into an effective mapped ACE plus an INHERIT_ONLY one holding the generic bits. That makes two ACEs, and private_win_owner_only_dacl requires exactly one (private_file_lock.c:910), so every lock directory then fails validation and the process dies with:

    codebase-memory-mcp: secure CLI coordination could not be created (project-locks)
    

    With specific rights the ACL is a single inheritable ACE and both validators accept it. The check at private_file_lock.c:917 masks only INHERITED_ACE | INHERIT_ONLY_ACE.

    Verified locally

    Built from main with the patch (MinGW gcc, Windows 11 26200) and compared against the stock 0.10.0 release binary on the same cache path:

    Path stock 0.10.0 patched
    create, fresh directory PETRO\petro:(F) PETRO\petro:(OI)(CI)(F)
    repair, existing directory strips to (F) (OI)(CI)(F)
    cli list_projects works works

    The repair row is the one that matters here. That is the branch which rewrites an already-good tree on every daemon or client start.

    Test

    Added daemon_ipc_windows_private_directory_ace_is_inheritable. It asserts that the secured directory has exactly one ACE, that the ACE carries both inherit flags, that a child created inside it has a non-empty DACL, and that cbm_private_lock_directory_adopt_windows still accepts the directory. The child-DACL and inherit-flag assertions fail on current main. The ACE-count assertion catches the GENERIC_ALL variant described above.

    Caveat on my local run

    MinGW ships no libsanitizer, so I could not run scripts/test.sh in its ASan and UBSan configuration. My run was unsanitized. Several pre-existing -Werror diagnostics in tests/test_daemon_ipc.c and tests/test_daemon_application.c also had to be downgraded before the test runner would build under this gcc, none of them in code the patch touches. CI remains the authority on sanitizer and warning cleanliness.

  12. added 3 commits that reference this issue on Aug 11, 2026
    293b1f9
    5ac55ad
    3e540aa
  13. DeusData commented on Aug 12, 2026

    @DeusData
    Owner

    Fixed on main by @Kiborgik's #1531 (merge 7f23a66) — shipping in v0.10.3.

    Root cause: the daemon created its private directory with a flagless ACE under PROTECTED_DACL_SECURITY_INFORMATION. The protection severed inherited ACEs and the new ACE propagated nothing, so every file created inside was left with an empty DACL — unreadable, exactly as you reported. Directories now get a properly inheritable ACE (with specific rights rather than generic, so the owner-only validation still sees exactly one ACE).

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 workingeditor/integrationEditor compatibility and CLI integrationpriority/highNeeds near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker.stability/performanceServer crashes, OOM, hangs, high CPU/memoryux/behaviorDisplay bugs, docs, adoption UXwindowsWindows-specific issues

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions