feat(config): make Windows cache-dir DACL hardening configurable (env var + CLI config key) - #1649
feat(config): make Windows cache-dir DACL hardening configurable (env var + CLI config key)#1649roosteer wants to merge 3 commits into
Conversation
|
Thank you for this, and especially for the two details that show you understood the constraint rather than working around it: the env kill switch working from the very first run (the config store lives inside the directory being hardened, so a persisted key alone cannot help someone who is locked out on run one), and keeping owner validation active in every path when hardening is off. Those are exactly the two things that would have made a naive version of this unsafe. Your #1620 report is also the best-evidenced thing in the tracker this week — the background I need to be straight with you about how this will be handled, because it is a policy question rather than a code review. Making security hardening opt-out is a maintainer decision, not something I will merge on technical merit alone. The protected-DACL behaviour was a deliberate choice, and an escape hatch changes what a default install guarantees — including for people who never read the flag. That belongs to the project owner, and I will put it in front of them with your evidence attached rather than quietly deciding it. There is also a live alternative worth weighing against yours: the re-stamp currently fires on every process start, whether or not anything is wrong. That is what produces the rewrite window your Two things I can do now: your work is queued behind a v0.10.5 that fixes a batch of install and startup blockers (CI is badly backed up tonight), and #1628 — which makes an atomic-publish failure report the actual Win32 error instead of I will come back with a decision rather than leaving this open indefinitely. |
|
Thanks for opening this — it has been seen, and it is queued. This note is automated, but it is not a brush-off: it exists so you know where your PR stands instead of having to guess from silence. Current review status: working through a backlog. What that means for this PR, concretely:
Things that will genuinely speed it up whenever review does happen:
If this fixes a bug, a reproduction we can run is worth more than a description of the symptom. Thanks for contributing, and sorry in advance for the wait. |
|
Third-party data point, not a position on the policy question. In #1620 the maintainer wrote that it would "genuinely help to know whether [ Short answer: on this host, yes — and the failure it replaces is now legible. A directory under the user profile passes cleanly: A world-writable ancestor is refused, and the refusal names the exact ACE: Worth saying plainly since #1620 was closed on the diagnosis half: that message is a large improvement over what sent that reporter chasing "check repo_path exists". It names the entry index, the rights mask and the identity, so the operator can fix it or relocate without guessing. What I would not conclude from this: that #1649 is unnecessary. My host is a personal machine where I control the profile ACLs. A domain-managed machine whose profile tree carries an inherited grant to some administrative or backup group would fail the same check with no operator-reachable alternative, and that is precisely the case the flag exists for. One passing host does not establish that every host has a passing location — it only kills the strongest version of the objection, which was that maybe none do. A use case for Today, with |
|
I owe you the policy decision I promised, and I am going to hold it back deliberately — because two findings block this regardless of how it goes, and a third may make the question moot. Let me take them in order of consequence. First, what is right here. When hardening is off you restore inheritance by copying a real parent ACL rather than passing a Blocking finding 1: the opt-out is much wider than the PR describes. The description says the opt-out relaxes the owner-only protected DACL to "the OS-default inherited DACL". The code does more than that. The early return you added to if (!cbm_windows_dacl_hardening_enabled()) {
...
return secure;
}and at that point Concretely: the The PR repeats that "owner validation stays active", and that is true. It is also not a substitute: ownership constrains who owns the object, not what a third principal holding a write ACE may do to it. A world-writable cache directory owned by you is still world-writable. If the intent really is "accept the inherited DACL but still refuse untrusted mutation grants", then the early return needs to sit after the ACE scan and relax only the one-ACE shape requirement. That is a much narrower and much more defensible change, and I would look at it very differently. Blocking finding 2: this breaks lock adoption.
This failure mode is already documented in the tree, at
That is the same conflation I described hitting from the other direction when my own attempt broke Windows daemon startup: "no untrusted grants" and "protected against inheritance" are different properties, and the lock layer needs the second one. Your CI could not catch it — the Windows legs are cancelled, and the three new tests are Third — and this is why I am not answering the policy question yet: the motivation may have shrunk. The alternative I floated on 14 August has since landed. The unconditional per-start rewrite that this PR exists to escape should therefore already be much rarer on current @junk151516 — thank you for measuring on your own machine and, more importantly, for arguing against over-reading your own result. Pointing out that one passing host only kills the strongest objection, and that a domain-managed profile tree with an inherited administrative grant would still fail with no operator-reachable alternative, is exactly the right way to offer evidence. I have not treated it as settled, but it is on the record and it is the scenario I will test against. Two housekeeping items whichever way this goes: DCO is red because the single commit carries no |
|
Two small corrections on the CI evidence, both of which sharpen rather than weaken your point. "The Windows legs are cancelled" isn't quite it — three Windows jobs ran and passed on this head (
Only Second: that run is from 14 Aug and On the substance I have nothing to add. I checked both blocking findings against the tree and they reproduce:
The comment already in the tree at |
2d23632 to
be6c7ea
Compare
Add an opt-out for the Windows cache-directory DACL hardening (upstream 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. Signed-off-by: rv <raffaele.verde2@gmail.com>
be6c7ea to
ff87146
Compare
|
Pointer, since the maintainer asked here for a re-measurement on current |
|
Thank you for connecting the requested re-measurement back to this PR. The new evidence materially changes the diagnosis: the v0.10.8 failure persists with no daemon and a fresh cache, while the same protected owner-only DACL permits the native rename on an unmanaged control host. Current This PR still changes a local security boundary and the earlier lock-adoption concerns remain unresolved, so we need more time to review the policy and implementation. I am treating the fresh #1620 evidence as a reason to reconsider that closed issue separately, not as proof that this opt-out is safe as written. Thank you both for the controlled comparison. |
…opt-out branch The rebase that fused main's already_correct fast-path with the opt-out gate kept both copies of the get_security_descriptor_control machinery: the typedef, the win_security_t member, and the RESOLVE_ADVAPI_MEMBER call. C rejects duplicate struct members, so the msys2 clang -Werror build failed with "duplicate member 'get_security_descriptor_control'" and pr-smoke (windows-latest) never produced a binary. Remove the PR's second copies, keeping main's originals, so git diff main...HEAD shows zero net change for these symbols. Also reformat the clang-format-20 violations in the opt-out branch of win_runtime_directory_secure: break the owner-repair set_security_info call after the '=', join the win_directory_dacl_protected if condition, and wrap the inheritance-restore call's flag argument. No behavioral change: opt-out semantics are exactly as before. Signed-off-by: rv <raffaele.verde2@gmail.com>
Summary
Closes #1624 (fix option #2 from #1620).
Makes the Windows cache-directory DACL hardening configurable via two opt-out surfaces:
CBM_SKIP_DACL_HARDENING=1— effective from the very first run, before any config store exists.windows-dacl-hardening(defaulttrue) — settable viaconfig set/reset, visible viaconfig list. Effective from the second run (the store_config.dblives inside the cache directory, so it does not exist when the first run creates that directory).Precedence: env > config. When hardening is disabled, owner validation of the cache directory stays active in every path; the default behavior is unchanged.
What changes when disabled
cbm_windows_stamp_dir_owner(creation stamp)DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATIONdropped → OS-default inherited DACLwin_runtime_directory_secure(per-start re-application)icaclswin_file_acl_secure(validator)Decisions and tradeoffs
D1 — Flag threading: process-global accessor, not a signature change. The stamp site lives in
foundation/(reached fromcbm_mkdir_p, ~25 call sites) while the harden site lives indaemon/; threading aboolthrough both would touch two definitions plus ~9 callers and still could not reach the stamp without going throughcbm_mkdir_p. Instead astatic atomic_int g_dacl_hardening = -1+ accessor, mirroring the existingcbm_mem_profile_enabledidiom.D2 — Config flows via an explicit setter, not the accessor. The accessor resolves the env var only, so it stays dependency-free inside the stamp path (which runs before any config store exists). A lazy config read inside the accessor would recurse:
cbm_config_open→mkdirp→ stamp → accessor.main_build_identityreads the config once (env unset) and stores it via the setter.D3 — Skip + unprotect (self-healing), not skip-only. The issue's literal proposal (skip the protection) leaves an already-hardened directory broken — the user would still need a manual
icacls /inheritance:e, contradicting "config set false→ index →indexed". The opt-out therefore also restores inheritance. Important implementation detail:SetSecurityInfowithDACL_SECURITY_INFORMATIONand aNULLpDacl creates a NULL DACL (full access to everyone) and would also make the relaxed validator fail closed. The unprotect therefore copies the parent directory's DACL (the ACE set a freshly created directory inherits) and clearsSE_DACL_PROTECTED— the same end state asicacls /inheritance:e.D4 — Global stamp skip, runtime-dir-only unprotect. The stamp skip is global (all dirs created via
cbm_mkdir_p); other directories do not use rename-replace, so skipping their DACL protection has no functional effect. The unprotect is naturally runtime-dir-only.D5 — Two-phase activation timing (documented, not worked around). Env from run 1; config from run 2. A single-run config opt-out is impossible by construction (the store lives inside the boundary being protected) and is documented in
docs/CONFIGURATION.md.D6 — Verification strategy. Wine (the only local Windows runtime) does not implement real ACL semantics:
SE_DACL_PROTECTEDis not honored, so the unprotect unit test asserts meaningfully only on real Windows (fork CI msys2 legs). Local gates: clang-format-20 + cppcheck, linux/amd64 test+build, mingw cross-compile, Wine test-runner and version check.D7 — Unit tests. Three Windows-only tests in
tests/test_daemon_ipc.c: env accessor contract, stamp-skip (inherited DACL + exact owner), and relaxed-validator + unprotect (previously hardened directory accepted,SE_DACL_PROTECTEDcleared, resulting DACL non-NULL and valid).Security tradeoff. The protected DACL defends the cache/IPC boundary against other local accounts. Disabling it is intended for single-user hosts where the protected DACL breaks
MoveFileExWrename-replace (EDR/minifilter conflicts); it is not recommended on multi-user/terminal-server hosts. Default remainstrue; owner validation is never disabled.Verification
./test-infrastructure/run.sh lint— pass (clang-format-20 + cppcheck 2.20.0)./test-infrastructure/run.sh amd64— build pass; 5 test failures are pre-existing and base-identical (container runs as root vs/srcowned by uid 1000 → activation-transaction ancestor check; vendored-integrity drift) — verified by running the same leg on the base commitdocker compose -f test-infrastructure/docker-compose.yml run --rm test-windows— mingw cross-compile pass; Winedaemon_ipcsuite shows no regressions vs base (2/3 new tests pass under Wine; the third requires real ACL semantics, per D6)./test-infrastructure/run.sh windows— production mingw build + Wine version check passTest plan for reviewers
config set windows-dacl-hardening false→ index any repo →status:"indexed"(on an affected host; regression-check on a normal host that default behavior is unchanged)CBM_SKIP_DACL_HARDENING=1overridestruein config