Skip to content

Commit 2d23632

Browse files
committed
feat(config): make Windows cache-dir DACL hardening configurable
Add an opt-out for the Windows cache-directory DACL hardening (upstream #1624): CBM_SKIP_DACL_HARDENING=1 as an env kill switch effective from the first run, plus a persisted windows-dacl-hardening config key (default true) effective from the second run, with env taking precedence. When disabled: the creation-time stamp keeps the exact-owner stamp but drops the owner-only protected DACL; the runtime-directory walk skips the re-protection and restores the parent-inherited DACL (SE_DACL_PROTECTED cleared) on a directory hardened by a previous run, supplying a real ACL rather than a NULL DACL; the validators accept the OS-default inherited DACL. Owner validation stays active in every path, and the default behavior is unchanged. Docs updated (CONFIGURATION.md runtime settings + environment variables, README env table) and three Windows-only unit tests added in tests/test_daemon_ipc.c.
1 parent b9cde7e commit 2d23632

9 files changed

Lines changed: 436 additions & 9 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -682,6 +682,7 @@ codebase-memory-mcp config reset auto_index # reset to default
682682
| `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. |
683683
| `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`). |
684684
| `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. |
685+
| `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. |
685686

686687
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.
687688

‎docs/CONFIGURATION.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@ Current keys:
8686
|---|---|---|
8787
| `auto_index` | `false` | Automatically index new projects when an MCP session starts. |
8888
| `auto_index_limit` | `50000` | Maximum file count allowed for automatic indexing of a new project. |
89+
| `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. |
90+
91+
`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.
8992

9093
## 3. UI Settings
9194

@@ -121,6 +124,7 @@ These environment variables affect runtime behavior:
121124
| `CBM_DIAGNOSTICS` | `false` | Enable periodic `snapshot.json` and retained `trajectory.ndjson` below a fresh owner-private directory in the system temp directory. The daemon records the randomized paths in the `diagnostics.start` discovery record (a single JSON line) in `${CBM_CACHE_DIR}/logs/cbm-daemon.log`; that one record is emitted even when `CBM_LOG_LEVEL` suppresses ordinary logging, so the paths always remain discoverable. |
122125
| `CBM_DOWNLOAD_URL` | GitHub releases | Override the update download URL. |
123126
| `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`. |
127+
| `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. |
124128
| `CBM_WORKERS` | auto-detected | Override the indexing worker count. |
125129

126130
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 the existing process and cannot replace those values. To change them, close every daemon-backed session, 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 use their own current environment without starting the daemon.

‎src/cli/cli.c‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6592,6 +6592,8 @@ static const config_key_def_t CONFIG_KEYS[] = {
65926592
{CBM_CONFIG_UI_LANG, "auto", "Pin graph UI language: en, zh, or auto"},
65936593
{CBM_CONFIG_UI_ENABLED, "false", "Serve the graph UI on a loopback HTTP port"},
65946594
{CBM_CONFIG_UI_PORT, "9749", "Port for the graph UI listener when enabled"},
6595+
{CBM_CONFIG_WINDOWS_DACL_HARDENING, "true",
6596+
"Protect Windows cache-dir DACL (ownership kept when off)"},
65956597
};
65966598

65976599
/* #1558: ui_enabled and ui_port were reachable ONLY by hand-editing

‎src/cli/cli.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,7 @@ int cbm_config_delete(cbm_config_t *cfg, const char *key);
418418
* than the key-value store, but surfaced through `config` so it is findable. */
419419
#define CBM_CONFIG_UI_ENABLED "ui_enabled"
420420
#define CBM_CONFIG_UI_PORT "ui_port"
421+
#define CBM_CONFIG_WINDOWS_DACL_HARDENING "windows-dacl-hardening"
421422

422423
/* ── Binary activation safety ─────────────────────────────────── */
423424

‎src/daemon/ipc.c‎

Lines changed: 126 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3358,6 +3358,8 @@ typedef DWORD(WINAPI *get_security_info_fn)(HANDLE, SE_OBJECT_TYPE, SECURITY_INF
33583358
PSID *, PACL *, PACL *, PSECURITY_DESCRIPTOR *);
33593359
typedef DWORD(WINAPI *set_security_info_fn)(HANDLE, SE_OBJECT_TYPE, SECURITY_INFORMATION, PSID,
33603360
PSID, PACL, PACL);
3361+
typedef BOOL(WINAPI *get_security_descriptor_control_fn)(PSECURITY_DESCRIPTOR,
3362+
PSECURITY_DESCRIPTOR_CONTROL, LPDWORD);
33613363
typedef BOOL(WINAPI *impersonate_named_pipe_client_fn)(HANDLE);
33623364
typedef BOOL(WINAPI *revert_to_self_fn)(void);
33633365
typedef BOOL(WINAPI *get_named_pipe_client_process_id_fn)(HANDLE, PULONG);
@@ -3384,6 +3386,7 @@ typedef struct {
33843386
get_ace_fn get_ace;
33853387
get_security_info_fn get_security_info;
33863388
set_security_info_fn set_security_info;
3389+
get_security_descriptor_control_fn get_security_descriptor_control;
33873390
impersonate_named_pipe_client_fn impersonate_named_pipe_client;
33883391
revert_to_self_fn revert_to_self;
33893392
PSID user_sid;
@@ -3670,6 +3673,8 @@ static bool win_security_init(win_security_t *security) {
36703673
RESOLVE_ADVAPI_MEMBER(security, get_ace, get_ace_fn, "GetAce");
36713674
RESOLVE_ADVAPI_MEMBER(security, get_security_info, get_security_info_fn, "GetSecurityInfo");
36723675
RESOLVE_ADVAPI_MEMBER(security, set_security_info, set_security_info_fn, "SetSecurityInfo");
3676+
RESOLVE_ADVAPI_MEMBER(security, get_security_descriptor_control,
3677+
get_security_descriptor_control_fn, "GetSecurityDescriptorControl");
36733678
RESOLVE_ADVAPI_MEMBER(security, impersonate_named_pipe_client, impersonate_named_pipe_client_fn,
36743679
"ImpersonateNamedPipeClient");
36753680
RESOLVE_ADVAPI_MEMBER(security, revert_to_self, revert_to_self_fn, "RevertToSelf");
@@ -4088,6 +4093,16 @@ static bool win_file_acl_secure(win_security_t *security, HANDLE file, DWORD mut
40884093
bool secure =
40894094
status == ERROR_SUCCESS && descriptor && dacl && security->is_valid_acl(dacl) &&
40904095
security->get_acl_information(dacl, &information, sizeof(information), AclSizeInformation);
4096+
if (!cbm_windows_dacl_hardening_enabled()) {
4097+
/* Hardening disabled (D3): accept the OS-default inherited DACL — the
4098+
* owner-only one-ACE shape is no longer required, and the
4099+
* runtime-directory walk has already restored inheritance. The owner
4100+
* is still validated by win_file_owner_secure. */
4101+
if (descriptor) {
4102+
(void)LocalFree(descriptor);
4103+
}
4104+
return secure;
4105+
}
40914106
enum {
40924107
WIN_FILE_ACE_ALLOW = 0x00,
40934108
WIN_FILE_ACE_DENY = 0x01,
@@ -4158,6 +4173,77 @@ static bool win_file_security_secure(win_security_t *security, HANDLE file,
41584173
win_file_acl_secure(security, file, mutation, ancestor);
41594174
}
41604175

4176+
/* True when the object's DACL is protected (inheritance disabled) — the state
4177+
* hardening applies and the opt-out restores. */
4178+
static bool win_directory_dacl_protected(win_security_t *security, HANDLE directory) {
4179+
if (!security || !directory) {
4180+
return false;
4181+
}
4182+
PSECURITY_DESCRIPTOR descriptor = NULL;
4183+
DWORD status = security->get_security_info(directory, SE_FILE_OBJECT, DACL_SECURITY_INFORMATION,
4184+
NULL, NULL, NULL, NULL, &descriptor);
4185+
SECURITY_DESCRIPTOR_CONTROL control = 0;
4186+
DWORD revision = 0;
4187+
bool protected_dacl =
4188+
status == ERROR_SUCCESS && descriptor &&
4189+
security->get_security_descriptor_control(descriptor, &control, &revision) != 0 &&
4190+
(control & SE_DACL_PROTECTED) != 0;
4191+
if (descriptor) {
4192+
(void)LocalFree(descriptor);
4193+
}
4194+
return protected_dacl;
4195+
}
4196+
4197+
/* Copy the parent directory's DACL — the ACE set a freshly created child
4198+
* inherits — so the opt-out can restore the OS-default inherited shape. The
4199+
* returned DACL is owned by *descriptor_out (LocalFree) and must stay alive
4200+
* until the SetSecurityInfo call that consumes it. */
4201+
static bool win_directory_parent_dacl(win_security_t *security, const wchar_t *directory_path,
4202+
PACL *dacl_out, PSECURITY_DESCRIPTOR *descriptor_out) {
4203+
if (!security || !directory_path || !directory_path[0] || !dacl_out || !descriptor_out) {
4204+
return false;
4205+
}
4206+
size_t length = wcslen(directory_path);
4207+
if (length == 0) {
4208+
return false;
4209+
}
4210+
wchar_t *parent_path = wide_copy(directory_path);
4211+
if (!parent_path) {
4212+
return false;
4213+
}
4214+
size_t separator = length;
4215+
while (separator > 0 && parent_path[separator - 1] != L'\\' &&
4216+
parent_path[separator - 1] != L'/') {
4217+
separator--;
4218+
}
4219+
if (separator == 0) {
4220+
free(parent_path);
4221+
return false;
4222+
}
4223+
parent_path[separator - 1] = L'\0';
4224+
HANDLE parent = CreateFileW(
4225+
parent_path, READ_CONTROL, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL,
4226+
OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, NULL);
4227+
free(parent_path);
4228+
if (parent == INVALID_HANDLE_VALUE) {
4229+
return false;
4230+
}
4231+
PACL dacl = NULL;
4232+
PSECURITY_DESCRIPTOR descriptor = NULL;
4233+
DWORD status = security->get_security_info(parent, SE_FILE_OBJECT, DACL_SECURITY_INFORMATION,
4234+
NULL, NULL, &dacl, NULL, &descriptor);
4235+
(void)CloseHandle(parent);
4236+
if (status != ERROR_SUCCESS || !descriptor || !dacl || !security->is_valid_acl(dacl)) {
4237+
if (descriptor) {
4238+
(void)LocalFree(descriptor);
4239+
}
4240+
return false;
4241+
}
4242+
*dacl_out = dacl;
4243+
*descriptor_out = descriptor;
4244+
return true;
4245+
}
4246+
41614247
static bool win_runtime_directory_secure(const wchar_t *runtime_dir) {
41624248
win_security_t security;
41634249
if (!win_security_init(&security)) {
@@ -4206,11 +4292,46 @@ static bool win_runtime_directory_secure(const wchar_t *runtime_dir) {
42064292
win_file_owner_secure(&security, directory, false));
42074293
DWORD secure_result = ERROR_ACCESS_DENIED;
42084294
if (valid_handle && owner_ok) {
4209-
secure_result = security.set_security_info(
4210-
directory, SE_FILE_OBJECT,
4211-
(owner_exact ? 0U : (DWORD)OWNER_SECURITY_INFORMATION) | DACL_SECURITY_INFORMATION |
4212-
PROTECTED_DACL_SECURITY_INFORMATION,
4213-
owner_exact ? NULL : security.user_sid, NULL, security.directory_acl, NULL);
4295+
if (cbm_windows_dacl_hardening_enabled()) {
4296+
secure_result = security.set_security_info(
4297+
directory, SE_FILE_OBJECT,
4298+
(owner_exact ? 0U : (DWORD)OWNER_SECURITY_INFORMATION) | DACL_SECURITY_INFORMATION |
4299+
PROTECTED_DACL_SECURITY_INFORMATION,
4300+
owner_exact ? NULL : security.user_sid, NULL, security.directory_acl, NULL);
4301+
} else {
4302+
/* Hardening disabled (D3): keep the exact-owner repair (owner
4303+
* validators stay active) but never re-protect the DACL, and
4304+
* restore inheritance on a directory hardened by a previous run
4305+
* so rename-replace works again without manual icacls. */
4306+
secure_result = ERROR_SUCCESS;
4307+
if (!owner_exact && can_write_owner) {
4308+
secure_result = security.set_security_info(directory, SE_FILE_OBJECT,
4309+
OWNER_SECURITY_INFORMATION,
4310+
security.user_sid, NULL, NULL, NULL);
4311+
}
4312+
if (secure_result == ERROR_SUCCESS &&
4313+
win_directory_dacl_protected(&security, directory)) {
4314+
/* Restore the inherited DACL shape: copy the parent's DACL
4315+
* (the ACEs a freshly created directory inherits) and clear
4316+
* SE_DACL_PROTECTED. A NULL pDacl with
4317+
* DACL_SECURITY_INFORMATION would create a NULL DACL — full
4318+
* access to everyone — so a real ACL is always supplied. */
4319+
PACL inherited = NULL;
4320+
PSECURITY_DESCRIPTOR parent_descriptor = NULL;
4321+
if (win_directory_parent_dacl(&security, runtime_dir, &inherited,
4322+
&parent_descriptor)) {
4323+
secure_result = security.set_security_info(
4324+
directory, SE_FILE_OBJECT,
4325+
UNPROTECTED_DACL_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION, NULL,
4326+
NULL, inherited, NULL);
4327+
} else {
4328+
secure_result = ERROR_ACCESS_DENIED;
4329+
}
4330+
if (parent_descriptor) {
4331+
(void)LocalFree(parent_descriptor);
4332+
}
4333+
}
4334+
}
42144335
}
42154336
if (valid_handle && owner_ok && secure_result != ERROR_SUCCESS) {
42164337
ipc_validation_detail_set("owner/DACL repair failed (status %lu%s)",

‎src/foundation/compat_fs.c‎

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,41 @@
2727
#include <errno.h> /* errno for spawn-failure logging */
2828
#include <fcntl.h> /* _O_RDONLY */
2929
#include <io.h> /* _wunlink, _open_osfhandle, _close */
30+
#include <stdatomic.h>
3031
#include <stdint.h> /* intptr_t */
3132
#include "foundation/log.h"
33+
#include "foundation/platform.h" /* cbm_safe_getenv */
3234
#include "foundation/win_utf8.h"
3335

36+
/* DACL-hardening process flag: -1 undecided, 0 disabled, 1 enabled. The
37+
* accessor lazily resolves the CBM_SKIP_DACL_HARDENING kill switch ONLY — it
38+
* must stay dependency-free because it is reached from the stamp path, which
39+
* runs before the config store exists. The persisted windows-dacl-hardening
40+
* key is applied later through the explicit setter (env > config). */
41+
static atomic_int g_dacl_hardening = -1;
42+
43+
bool cbm_windows_dacl_hardening_enabled(void) {
44+
int state = atomic_load_explicit(&g_dacl_hardening, memory_order_acquire);
45+
if (state >= 0) {
46+
return state == 1;
47+
}
48+
char buf[CBM_SZ_32];
49+
bool skip =
50+
cbm_safe_getenv("CBM_SKIP_DACL_HARDENING", buf, sizeof(buf), NULL) != NULL && buf[0] == '1';
51+
atomic_store_explicit(&g_dacl_hardening, skip ? 0 : 1, memory_order_release);
52+
return !skip;
53+
}
54+
55+
void cbm_windows_dacl_hardening_set(bool enabled) {
56+
atomic_store_explicit(&g_dacl_hardening, enabled ? 1 : 0, memory_order_release);
57+
}
58+
59+
#ifdef CBM_ENABLE_TEST_SEAMS
60+
void cbm_windows_dacl_hardening_reset_for_testing(void) {
61+
atomic_store_explicit(&g_dacl_hardening, -1, memory_order_release);
62+
}
63+
#endif
64+
3465
struct cbm_dir {
3566
HANDLE find_handle;
3667
WIN32_FIND_DATAW find_data;
@@ -461,10 +492,17 @@ static void cbm_windows_stamp_dir_owner(const wchar_t *path) {
461492
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, NULL);
462493
if (directory != INVALID_HANDLE_VALUE) {
463494
if (SetEntriesInAclW(1U, &access, NULL, &acl) == ERROR_SUCCESS) {
464-
(void)SetSecurityInfo(directory, SE_FILE_OBJECT,
465-
OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION |
466-
PROTECTED_DACL_SECURITY_INFORMATION,
467-
user->User.Sid, NULL, acl, NULL);
495+
/* D3: when hardening is disabled the exact-owner stamp stays
496+
* (the owner validators remain active) but the owner-only
497+
* protected DACL is dropped, leaving the OS-default DACL. */
498+
DWORD stamp = OWNER_SECURITY_INFORMATION;
499+
PACL stamp_acl = NULL;
500+
if (cbm_windows_dacl_hardening_enabled()) {
501+
stamp |= DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION;
502+
stamp_acl = acl;
503+
}
504+
(void)SetSecurityInfo(directory, SE_FILE_OBJECT, stamp, user->User.Sid, NULL,
505+
stamp_acl, NULL);
468506
}
469507
(void)CloseHandle(directory);
470508
}
@@ -951,6 +989,20 @@ int cbm_exec_no_shell(const char *const *argv) {
951989
return CBM_NOT_FOUND; /* killed by signal */
952990
}
953991

992+
/* DACL hardening is Windows-only; the accessor keeps the safe default and the
993+
* setter is a no-op so shared callers (main_build_identity) compile unchanged. */
994+
bool cbm_windows_dacl_hardening_enabled(void) {
995+
return true;
996+
}
997+
998+
void cbm_windows_dacl_hardening_set(bool enabled) {
999+
(void)enabled;
1000+
}
1001+
1002+
#ifdef CBM_ENABLE_TEST_SEAMS
1003+
void cbm_windows_dacl_hardening_reset_for_testing(void) {}
1004+
#endif
1005+
9541006
#endif /* _WIN32 */
9551007

9561008
/* Canonicalize an EXISTING path (collapse `..`, resolve links/junctions):

‎src/foundation/compat_fs.h‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,20 @@ int cbm_pclose(FILE *f);
5959
/* Create directory (and parents). mode is ignored on Windows. Returns true on success. */
6060
bool cbm_mkdir_p(const char *path, int mode);
6161

62+
/* Windows cache-directory DACL hardening opt-out (#1624). The accessor
63+
* reflects CBM_SKIP_DACL_HARDENING (env-only, safe before the config store
64+
* exists); the setter applies the persisted windows-dacl-hardening key once
65+
* the store is readable (env wins over config). POSIX builds compile the
66+
* accessor to the default (hardening on) and the setter to a no-op — every
67+
* DACL site is Windows-only. */
68+
bool cbm_windows_dacl_hardening_enabled(void);
69+
void cbm_windows_dacl_hardening_set(bool enabled);
70+
#ifdef CBM_ENABLE_TEST_SEAMS
71+
/* Reset the lazily-cached flag so a test can re-resolve CBM_SKIP_DACL_HARDENING
72+
* regardless of earlier calls in the same process. */
73+
void cbm_windows_dacl_hardening_reset_for_testing(void);
74+
#endif
75+
6276
/* Delete a file. Returns 0 on success. */
6377
int cbm_unlink(const char *path);
6478
/* Remove <db_path>-wal/-shm/-journal. MUST be called by any path installing a fresh

0 commit comments

Comments
 (0)