Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -734,6 +734,7 @@ codebase-memory-mcp config reset auto_index # reset to default
| `CBM_WORKERS` | *(detected)* | Override the parallel-indexing worker count returned by `cbm_default_worker_count`. Useful inside containers where `sysconf(_SC_NPROCESSORS_ONLN)` reports host CPUs rather than the cgroup's effective quota. Range 1–256; invalid values are ignored with a warning. |
| `CBM_MEM_BUDGET_MB` | *(detected)* | Override the in-memory graph budget with an explicit cap in MiB, taking precedence over the `ram_fraction × total_RAM` default. Useful on bare-metal hosts without a cgroup limit, or to pin a budget *below* the cgroup limit so headroom is left for sibling processes. Must be a positive integer; it is clamped to detected total RAM (logged as `mem.budget.clamped`), and non-numeric or non-positive values are ignored with a warning (`mem.budget.env.invalid`). |
| `CBM_DUMP_VERIFY_MIN_RATIO` | `0.5` | After indexing, compare persisted SQLite node count to the in-memory dump count. When persisted nodes fall below this fraction of committed nodes (and committed > 50), `index_repository` returns `status:"degraded"` instead of silent `indexed`. Range 0–1; set `0` to disable. Invalid values are ignored with a warning. |
| `CBM_SKIP_DACL_HARDENING` | *(unset)* | Windows only: set to `1` to disable cache-directory DACL hardening from the very first run (the `windows-dacl-hardening` config key applies from run 2, since `_config.db` lives inside the cache directory). Overrides the config key; owner validation stays active. Use on hosts where the protected DACL breaks index publication. |

Environment used by daemon-owned components—such as diagnostics, daemon logging, and process-wide indexing resource limits—is captured from the first daemon-backed session that starts the daemon. Later sessions join that process and cannot replace those values. To change them, close all daemon-backed sessions, update the relevant agent configurations consistently, and restart a session. `CBM_ALLOWED_ROOT` remains session-specific, a conflicting `CBM_CACHE_DIR` is rejected, and one-shot CLI commands read their own environment without starting the daemon.

Expand Down
5 changes: 5 additions & 0 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ Current keys:
> `auto_index` still runs, and `index_repository` stays available for manual
> reindexing.

| `windows-dacl-hardening` | `true` | Windows only: keep the cache-directory DACL hardened (owner-only, inheritance disabled). Set `false` to opt out; owner validation stays active. |

`windows-dacl-hardening` is effective from the **second run**: the config store (`_config.db`) lives inside the cache directory, so it does not exist when the first run creates that directory. On the second run the directory is no longer re-protected, and a DACL hardened by the previous run has inheritance restored automatically. For an effect from the very first run, set `CBM_SKIP_DACL_HARDENING=1` instead (it overrides the config key). Disabling the hardening is intended for hosts where the protected DACL breaks index publication (`MoveFileExW` rename-replace fails with `ERROR_ACCESS_DENIED`, surfacing as the generic "Pipeline failed"); it is not recommended on multi-user or terminal-server hosts, where the owner-only DACL is the protection against other local accounts.

## 3. UI Settings

The optional built-in graph UI stores its settings in:
Expand Down Expand Up @@ -151,6 +155,7 @@ These environment variables affect runtime behavior:
| `CBM_DOWNLOAD_URL` | GitHub releases | Override the update download URL. |
| `CBM_LOG_LEVEL` | `info` | Set the log level to `debug`, `info`, `warn`, `error`, or `none` (or `0`-`4`). Thin-frontend messages use that session's stderr; detached daemon events use `${CBM_CACHE_DIR}/logs/cbm-daemon.log`. |
| `CBM_RUNTIME_DIR` | `%LOCALAPPDATA%` (Windows), `/private/tmp` (macOS), `/tmp` (other) | Parent directory for the daemon/CLI rendezvous directory, which CBM creates inside it as `cbm-daemon-<uid>` (`cbm-daemon-<key>` on Windows). Set it when the default ancestry cannot pass the private-directory check — see below. `CBM_CACHE_DIR` does **not** move the rendezvous. |
| `CBM_SKIP_DACL_HARDENING` | *(unset)* | Windows only: set to `1` to disable cache-directory DACL hardening from the **very first run**, before any config store exists (the `windows-dacl-hardening` key only applies from run 2). Overrides the config key; owner validation of the cache directory stays active. Use it on hosts where the protected DACL breaks index publication. |
| `CBM_WORKERS` | auto-detected | Override the indexing worker count. |

### Relocating the daemon rendezvous directory
Expand Down
2 changes: 2 additions & 0 deletions src/cli/cli.c
Original file line number Diff line number Diff line change
Expand Up @@ -6855,6 +6855,8 @@ static const config_key_def_t CONFIG_KEYS[] = {
{CBM_CONFIG_UI_LANG, "auto", "Pin graph UI language: en, zh, or auto"},
{CBM_CONFIG_UI_ENABLED, "false", "Serve the graph UI on a loopback HTTP port"},
{CBM_CONFIG_UI_PORT, "9749", "Port for the graph UI listener when enabled"},
{CBM_CONFIG_WINDOWS_DACL_HARDENING, "true",
"Protect Windows cache-dir DACL (ownership kept when off)"},
};

/* #1558: ui_enabled and ui_port were reachable ONLY by hand-editing
Expand Down
1 change: 1 addition & 0 deletions src/cli/cli.h
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,7 @@ int cbm_config_delete(cbm_config_t *cfg, const char *key);
* than the key-value store, but surfaced through `config` so it is findable. */
#define CBM_CONFIG_UI_ENABLED "ui_enabled"
#define CBM_CONFIG_UI_PORT "ui_port"
#define CBM_CONFIG_WINDOWS_DACL_HARDENING "windows-dacl-hardening"

/* Whether the background watcher subsystem should run at all (default true).
* When false, the daemon host skips building and starting the watcher entirely:
Expand Down
132 changes: 123 additions & 9 deletions src/daemon/ipc.c
Original file line number Diff line number Diff line change
Expand Up @@ -4119,6 +4119,16 @@ static bool win_file_acl_secure(win_security_t *security, HANDLE file, DWORD mut
bool secure =
status == ERROR_SUCCESS && descriptor && dacl && security->is_valid_acl(dacl) &&
security->get_acl_information(dacl, &information, sizeof(information), AclSizeInformation);
if (!cbm_windows_dacl_hardening_enabled()) {
/* Hardening disabled (D3): accept the OS-default inherited DACL — the
* owner-only one-ACE shape is no longer required, and the
* runtime-directory walk has already restored inheritance. The owner
* is still validated by win_file_owner_secure. */
if (descriptor) {
(void)LocalFree(descriptor);
}
return secure;
}
enum {
WIN_FILE_ACE_ALLOW = 0x00,
WIN_FILE_ACE_DENY = 0x01,
Expand Down Expand Up @@ -4370,6 +4380,77 @@ static void win_repair_runtime_children(win_security_t *security, const wchar_t
}
}

/* True when the object's DACL is protected (inheritance disabled) — the state
* hardening applies and the opt-out restores. */
static bool win_directory_dacl_protected(win_security_t *security, HANDLE directory) {
if (!security || !directory) {
return false;
}
PSECURITY_DESCRIPTOR descriptor = NULL;
DWORD status = security->get_security_info(directory, SE_FILE_OBJECT, DACL_SECURITY_INFORMATION,
NULL, NULL, NULL, NULL, &descriptor);
SECURITY_DESCRIPTOR_CONTROL control = 0;
DWORD revision = 0;
bool protected_dacl =
status == ERROR_SUCCESS && descriptor &&
security->get_security_descriptor_control(descriptor, &control, &revision) != 0 &&
(control & SE_DACL_PROTECTED) != 0;
if (descriptor) {
(void)LocalFree(descriptor);
}
return protected_dacl;
}

/* Copy the parent directory's DACL — the ACE set a freshly created child
* inherits — so the opt-out can restore the OS-default inherited shape. The
* returned DACL is owned by *descriptor_out (LocalFree) and must stay alive
* until the SetSecurityInfo call that consumes it. */
static bool win_directory_parent_dacl(win_security_t *security, const wchar_t *directory_path,
PACL *dacl_out, PSECURITY_DESCRIPTOR *descriptor_out) {
if (!security || !directory_path || !directory_path[0] || !dacl_out || !descriptor_out) {
return false;
}
size_t length = wcslen(directory_path);
if (length == 0) {
return false;
}
wchar_t *parent_path = wide_copy(directory_path);
if (!parent_path) {
return false;
}
size_t separator = length;
while (separator > 0 && parent_path[separator - 1] != L'\\' &&
parent_path[separator - 1] != L'/') {
separator--;
}
if (separator == 0) {
free(parent_path);
return false;
}
parent_path[separator - 1] = L'\0';
HANDLE parent = CreateFileW(
parent_path, READ_CONTROL, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL,
OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, NULL);
free(parent_path);
if (parent == INVALID_HANDLE_VALUE) {
return false;
}
PACL dacl = NULL;
PSECURITY_DESCRIPTOR descriptor = NULL;
DWORD status = security->get_security_info(parent, SE_FILE_OBJECT, DACL_SECURITY_INFORMATION,
NULL, NULL, &dacl, NULL, &descriptor);
(void)CloseHandle(parent);
if (status != ERROR_SUCCESS || !descriptor || !dacl || !security->is_valid_acl(dacl)) {
if (descriptor) {
(void)LocalFree(descriptor);
}
return false;
}
*dacl_out = dacl;
*descriptor_out = descriptor;
return true;
}

static bool win_runtime_directory_secure(const wchar_t *runtime_dir) {
win_security_t security;
if (!win_security_init(&security)) {
Expand Down Expand Up @@ -4435,16 +4516,49 @@ static bool win_runtime_directory_secure(const wchar_t *runtime_dir) {
* check, there is nothing to fix and the correct action is to leave it
* alone. When it IS wrong we still repair exactly as before. */
DWORD secure_result = ERROR_ACCESS_DENIED;
bool already_correct =
valid_handle && owner_exact && win_file_dacl_is_owner_only(&security, directory);
if (already_correct) {
secure_result = ERROR_SUCCESS;
if (cbm_windows_dacl_hardening_enabled()) {
bool already_correct =
valid_handle && owner_exact && win_file_dacl_is_owner_only(&security, directory);
if (already_correct) {
secure_result = ERROR_SUCCESS;
} else if (valid_handle && owner_ok) {
secure_result = security.set_security_info(
directory, SE_FILE_OBJECT,
(owner_exact ? 0U : (DWORD)OWNER_SECURITY_INFORMATION) | DACL_SECURITY_INFORMATION |
PROTECTED_DACL_SECURITY_INFORMATION,
owner_exact ? NULL : security.user_sid, NULL, security.directory_acl, NULL);
}
} else if (valid_handle && owner_ok) {
secure_result = security.set_security_info(
directory, SE_FILE_OBJECT,
(owner_exact ? 0U : (DWORD)OWNER_SECURITY_INFORMATION) | DACL_SECURITY_INFORMATION |
PROTECTED_DACL_SECURITY_INFORMATION,
owner_exact ? NULL : security.user_sid, NULL, security.directory_acl, NULL);
/* Hardening disabled (D3): keep the exact-owner repair (owner
* validators stay active) but never re-protect the DACL, and
* restore inheritance on a directory hardened by a previous run
* so rename-replace works again without manual icacls. */
secure_result = ERROR_SUCCESS;
if (!owner_exact && can_write_owner) {
secure_result =
security.set_security_info(directory, SE_FILE_OBJECT, OWNER_SECURITY_INFORMATION,
security.user_sid, NULL, NULL, NULL);
}
if (secure_result == ERROR_SUCCESS && win_directory_dacl_protected(&security, directory)) {
/* Restore the inherited DACL shape: copy the parent's DACL
* (the ACEs a freshly created directory inherits) and clear
* SE_DACL_PROTECTED. A NULL pDacl with
* DACL_SECURITY_INFORMATION would create a NULL DACL — full
* access to everyone — so a real ACL is always supplied. */
PACL inherited = NULL;
PSECURITY_DESCRIPTOR parent_descriptor = NULL;
if (win_directory_parent_dacl(&security, runtime_dir, &inherited, &parent_descriptor)) {
secure_result = security.set_security_info(directory, SE_FILE_OBJECT,
UNPROTECTED_DACL_SECURITY_INFORMATION |
DACL_SECURITY_INFORMATION,
NULL, NULL, inherited, NULL);
} else {
secure_result = ERROR_ACCESS_DENIED;
}
if (parent_descriptor) {
(void)LocalFree(parent_descriptor);
}
}
}
if (valid_handle && owner_ok && secure_result != ERROR_SUCCESS) {
ipc_validation_detail_set("owner/DACL repair failed (status %lu%s)",
Expand Down
60 changes: 56 additions & 4 deletions src/foundation/compat_fs.c
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,41 @@
#include <errno.h> /* errno for spawn-failure logging */
#include <fcntl.h> /* _O_RDONLY */
#include <io.h> /* _wunlink, _open_osfhandle, _close */
#include <stdatomic.h>
#include <stdint.h> /* intptr_t */
#include "foundation/log.h"
#include "foundation/platform.h" /* cbm_safe_getenv */
#include "foundation/win_utf8.h"

/* DACL-hardening process flag: -1 undecided, 0 disabled, 1 enabled. The
* accessor lazily resolves the CBM_SKIP_DACL_HARDENING kill switch ONLY — it
* must stay dependency-free because it is reached from the stamp path, which
* runs before the config store exists. The persisted windows-dacl-hardening
* key is applied later through the explicit setter (env > config). */
static atomic_int g_dacl_hardening = -1;

bool cbm_windows_dacl_hardening_enabled(void) {
int state = atomic_load_explicit(&g_dacl_hardening, memory_order_acquire);
if (state >= 0) {
return state == 1;
}
char buf[CBM_SZ_32];
bool skip =
cbm_safe_getenv("CBM_SKIP_DACL_HARDENING", buf, sizeof(buf), NULL) != NULL && buf[0] == '1';
atomic_store_explicit(&g_dacl_hardening, skip ? 0 : 1, memory_order_release);
return !skip;
}

void cbm_windows_dacl_hardening_set(bool enabled) {
atomic_store_explicit(&g_dacl_hardening, enabled ? 1 : 0, memory_order_release);
}

#ifdef CBM_ENABLE_TEST_SEAMS
void cbm_windows_dacl_hardening_reset_for_testing(void) {
atomic_store_explicit(&g_dacl_hardening, -1, memory_order_release);
}
#endif

struct cbm_dir {
HANDLE find_handle;
WIN32_FIND_DATAW find_data;
Expand Down Expand Up @@ -461,10 +492,17 @@ static void cbm_windows_stamp_dir_owner(const wchar_t *path) {
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, NULL);
if (directory != INVALID_HANDLE_VALUE) {
if (SetEntriesInAclW(1U, &access, NULL, &acl) == ERROR_SUCCESS) {
(void)SetSecurityInfo(directory, SE_FILE_OBJECT,
OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION |
PROTECTED_DACL_SECURITY_INFORMATION,
user->User.Sid, NULL, acl, NULL);
/* D3: when hardening is disabled the exact-owner stamp stays
* (the owner validators remain active) but the owner-only
* protected DACL is dropped, leaving the OS-default DACL. */
DWORD stamp = OWNER_SECURITY_INFORMATION;
PACL stamp_acl = NULL;
if (cbm_windows_dacl_hardening_enabled()) {
stamp |= DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION;
stamp_acl = acl;
}
(void)SetSecurityInfo(directory, SE_FILE_OBJECT, stamp, user->User.Sid, NULL,
stamp_acl, NULL);
}
(void)CloseHandle(directory);
}
Expand Down Expand Up @@ -951,6 +989,20 @@ int cbm_exec_no_shell(const char *const *argv) {
return CBM_NOT_FOUND; /* killed by signal */
}

/* DACL hardening is Windows-only; the accessor keeps the safe default and the
* setter is a no-op so shared callers (main_build_identity) compile unchanged. */
bool cbm_windows_dacl_hardening_enabled(void) {
return true;
}

void cbm_windows_dacl_hardening_set(bool enabled) {
(void)enabled;
}

#ifdef CBM_ENABLE_TEST_SEAMS
void cbm_windows_dacl_hardening_reset_for_testing(void) {}
#endif

#endif /* _WIN32 */

/* Canonicalize an EXISTING path (collapse `..`, resolve links/junctions):
Expand Down
14 changes: 14 additions & 0 deletions src/foundation/compat_fs.h
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,20 @@ int cbm_pclose(FILE *f);
/* Create directory (and parents). mode is ignored on Windows. Returns true on success. */
bool cbm_mkdir_p(const char *path, int mode);

/* Windows cache-directory DACL hardening opt-out (#1624). The accessor
* reflects CBM_SKIP_DACL_HARDENING (env-only, safe before the config store
* exists); the setter applies the persisted windows-dacl-hardening key once
* the store is readable (env wins over config). POSIX builds compile the
* accessor to the default (hardening on) and the setter to a no-op — every
* DACL site is Windows-only. */
bool cbm_windows_dacl_hardening_enabled(void);
void cbm_windows_dacl_hardening_set(bool enabled);
#ifdef CBM_ENABLE_TEST_SEAMS
/* Reset the lazily-cached flag so a test can re-resolve CBM_SKIP_DACL_HARDENING
* regardless of earlier calls in the same process. */
void cbm_windows_dacl_hardening_reset_for_testing(void);
#endif

/* Delete a file. Returns 0 on success. */
int cbm_unlink(const char *path);
/* Remove <db_path>-wal/-shm/-journal. MUST be called by any path installing a fresh
Expand Down
22 changes: 22 additions & 0 deletions src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -1238,6 +1238,28 @@ static main_build_identity_status_t main_build_identity(cbm_daemon_build_identit
return MAIN_BUILD_IDENTITY_CACHE_CANONICALIZE;
}
cbm_normalize_path_sep(canonical_cache);
#ifdef _WIN32
/* Resolve the DACL-hardening opt-out before the cache dir is secured.
* The config store lives inside the cache dir, so on the very first run
* it does not exist yet: the env kill switch is the only run-1 lever.
* When the env var is unset the persisted windows-dacl-hardening key
* applies from run 2 on (env > config, D2/D5). */
{
char skip_buf[CBM_SZ_16];
const char *skip_env =
cbm_safe_getenv("CBM_SKIP_DACL_HARDENING", skip_buf, sizeof(skip_buf), NULL);
if (skip_env == NULL || skip_buf[0] != '1') {
/* Kill switch inactive (unset or not exactly "1", matching the
* accessor): the persisted key applies. */
cbm_config_t *cfg = cbm_config_open(canonical_cache);
if (cfg) {
cbm_windows_dacl_hardening_set(
cbm_config_get_bool(cfg, CBM_CONFIG_WINDOWS_DACL_HARDENING, true));
cbm_config_close(cfg);
}
}
}
#endif
/* Admission is account-scoped, so its storage authority must be too.
* Harden the canonical object before hashing it. Replacement of this
* owner-only path by the same already-compromised OS account is outside
Expand Down
Loading
Loading