fix(install): trust the local machine's built-in Administrator; name untrusted owners (#1705, #2023, #1686)

Windows install/daemon runtime directories created by an elevated process are
owned by the built-in Administrator ACCOUNT (RID 500 under the local machine
account-domain SID), even when that account is renamed or disabled. cbm's
directory-owner/ACL trust refused it, breaking install/activation on such
machines.

#1705: resolve THIS machine's built-in Administrator SID via LSA
(LsaOpenPolicy(POLICY_VIEW_LOCAL_INFORMATION) +
LsaQueryInformationPolicy(PolicyAccountDomainInformation) ->
CreateWellKnownSid(WinAccountAdministratorSid, accountDomainSid)), cached once
per process, and accept it as a trusted owner/grantee in the daemon
(win_sid_trusted) and the CLI activation transaction. Resolution is
deliberately NOT IsWellKnownSid(WinAccountAdministratorSid) and NOT a
trailing-RID-500 match: both accept a FOREIGN S-1-5-21-*-500 (a domain
administrator, or another machine's built-in Administrator), which would be a
cross-machine trust escalation. Any LSA or synthesis failure caches "none" and
grants no tolerance (fail closed). owner-not-current-user (the exact-owner
target rule) is unchanged.

#2023/#1686: an owner-trust refusal now names the offending owner SID (the path
was already carried), so a terse "status -3, os 0" becomes actionable.

Scope notes: on a domain controller -- which has no separate local SAM, so its
account domain IS the AD domain -- this trusts the domain Administrator, by
design (on a DC that account is the machine-equivalent top admin). An
un-sysprepped cloned Windows image shares the machine account-domain SID, so a
sibling clone's built-in Administrator would be non-foreign; this is inherent
to machine-SID identity.

Verified on a native Windows (arm64) VM: compiles under the sanitized build;
the daemon_ipc SID-trust test passes (local RID-500 trusted, a fabricated
foreign S-1-5-21-*-500 rejected, BUILTIN\Administrators trusted) at 35 passed /
0 failed with the fix, and 34 passed / 1 failed (ASSERT(local_ok)) with the
trust clause reverted -- the test binds precisely to the fix. #1856 (stock
ancestor ACEs): the Windows ancestor-ACL guards already pass; no defect
reproduced, no code change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel
2026-09-10 00:40:49 +02:00
co-authored by Claude Fable 5.1
parent cdff08f313
commit df579830f3
4 changed files with 333 additions and 3 deletions
+106 -3
View File
@@ -18,6 +18,7 @@
#define WIN32_LEAN_AND_MEAN
#endif
#include <aclapi.h>
#include <ntsecapi.h>
#include <sddl.h>
#include <windows.h>
#else
@@ -424,6 +425,101 @@ static void activation_windows_security_destroy(activation_windows_security_t *s
memset(security, 0, sizeof(*security));
}
/* #1705: THIS machine's built-in Administrator ACCOUNT (RID 500 under the local
* machine account-domain SID, S-1-5-21-<machine>-500) is a trusted owner/grantee,
* mirroring the daemon's win_sid_trusted (src/daemon/ipc.c). It is resolved via
* LSA (the local machine account-domain SID) plus CreateWellKnownSid and compared
* with EqualSid. It is deliberately NOT IsWellKnownSid(WinAccountAdministratorSid)
* and NOT a trailing-RID-500 test: both accept a FOREIGN S-1-5-21-*-500 (a domain
* admin, or another machine's built-in Administrator), opening a cross-machine
* bypass. Resolved once and cached for the process; any LSA or synthesis failure
* caches "none" and grants NO tolerance (fail closed). advapi32 is already loaded
* (this file calls GetSecurityInfo etc.), so the functions are resolved from its
* module handle with no new import. CLI activation is single-threaded by
* contract, so the cache needs no lock. */
typedef NTSTATUS(NTAPI *activation_lsa_open_policy_fn)(PLSA_UNICODE_STRING, PLSA_OBJECT_ATTRIBUTES,
ACCESS_MASK, PLSA_HANDLE);
typedef NTSTATUS(NTAPI *activation_lsa_query_information_policy_fn)(LSA_HANDLE,
POLICY_INFORMATION_CLASS,
PVOID *);
typedef NTSTATUS(NTAPI *activation_lsa_free_memory_fn)(PVOID);
typedef NTSTATUS(NTAPI *activation_lsa_close_fn)(LSA_HANDLE);
typedef BOOL(WINAPI *activation_create_well_known_sid_fn)(WELL_KNOWN_SID_TYPE, PSID, PSID, DWORD *);
static PSID activation_windows_local_admin_sid(void) {
static bool resolved = false;
static PSID cached = NULL;
if (resolved) {
return cached;
}
resolved = true;
HMODULE advapi = GetModuleHandleW(L"advapi32.dll");
if (!advapi) {
return NULL;
}
activation_lsa_open_policy_fn lsa_open =
(activation_lsa_open_policy_fn)(void (*)(void))GetProcAddress(advapi, "LsaOpenPolicy");
activation_lsa_query_information_policy_fn lsa_query =
(activation_lsa_query_information_policy_fn)(void (*)(void))GetProcAddress(
advapi, "LsaQueryInformationPolicy");
activation_lsa_free_memory_fn lsa_free =
(activation_lsa_free_memory_fn)(void (*)(void))GetProcAddress(advapi, "LsaFreeMemory");
activation_lsa_close_fn lsa_close =
(activation_lsa_close_fn)(void (*)(void))GetProcAddress(advapi, "LsaClose");
activation_create_well_known_sid_fn create_sid =
(activation_create_well_known_sid_fn)(void (*)(void))GetProcAddress(advapi,
"CreateWellKnownSid");
if (!lsa_open || !lsa_query || !lsa_free || !lsa_close || !create_sid) {
return NULL;
}
LSA_OBJECT_ATTRIBUTES attributes;
memset(&attributes, 0, sizeof(attributes));
LSA_HANDLE policy = NULL;
/* STATUS_SUCCESS is 0; any other status is treated as failure (fail closed). */
if (lsa_open(NULL, &attributes, POLICY_VIEW_LOCAL_INFORMATION, &policy) != 0 || !policy) {
return NULL;
}
POLICY_ACCOUNT_DOMAIN_INFO *domain = NULL;
if (lsa_query(policy, PolicyAccountDomainInformation, (PVOID *)&domain) == 0 && domain &&
domain->DomainSid && IsValidSid(domain->DomainSid)) {
DWORD needed = 0;
(void)create_sid(WinAccountAdministratorSid, domain->DomainSid, NULL, &needed);
if (needed > 0U) {
PSID admin = malloc(needed);
if (admin && create_sid(WinAccountAdministratorSid, domain->DomainSid, admin, &needed) &&
IsValidSid(admin)) {
cached = admin;
} else {
free(admin);
}
}
}
if (domain) {
(void)lsa_free(domain);
}
(void)lsa_close(policy);
return cached;
}
/* #2023/#1686: an owner refusal must name WHICH owner, not just "status -3, os 0".
* The path is already carried by g_activation_refusal_object; this appends the
* offending owner's SID string, mirroring the daemon's ACL/owner diagnostics, so
* the operator sees the exact identity to remove or the directory to move. */
static void activation_windows_note_untrusted_owner(const char *predicate, PSID owner,
DWORD os_error) {
char label[192];
LPSTR owner_text = NULL;
(void)snprintf(
label, sizeof(label), "%s; owner=%s", predicate,
(owner && IsValidSid(owner) && ConvertSidToStringSidA(owner, &owner_text) && owner_text)
? owner_text
: "unresolved-sid");
if (owner_text) {
(void)LocalFree(owner_text);
}
activation_note_refusal(label, os_error);
}
/* Trusted-owner acceptance for SOURCE-side objects: a downloaded release
* bundle is owned by whatever the machine's default-owner policy dictates
* (Administrators on GitHub-runner-class images). Its integrity is enforced
@@ -440,11 +536,13 @@ static bool activation_windows_owner_is_trusted(HANDLE handle) {
PSECURITY_DESCRIPTOR descriptor = NULL;
DWORD result = GetSecurityInfo(handle, SE_FILE_OBJECT, OWNER_SECURITY_INFORMATION, &owner, NULL,
NULL, NULL, &descriptor);
PSID local_admin = activation_windows_local_admin_sid();
bool trusted = result == ERROR_SUCCESS && owner && IsValidSid(owner) &&
(EqualSid(owner, user_sid) || IsWellKnownSid(owner, WinLocalSystemSid) ||
IsWellKnownSid(owner, WinBuiltinAdministratorsSid));
IsWellKnownSid(owner, WinBuiltinAdministratorsSid) ||
(local_admin && EqualSid(owner, local_admin)));
if (!trusted) {
activation_note_refusal("owner-not-trusted", result);
activation_windows_note_untrusted_owner("owner-not-trusted", owner, result);
}
if (descriptor) {
(void)LocalFree(descriptor);
@@ -522,7 +620,7 @@ static bool activation_windows_owner_is_current(HANDLE handle) {
}
}
if (!same) {
activation_note_refusal("owner-not-current-user", result);
activation_windows_note_untrusted_owner("owner-not-current-user", owner, result);
}
if (descriptor) {
(void)LocalFree(descriptor);
@@ -596,10 +694,15 @@ static bool activation_windows_acl_check(HANDLE handle, DWORD tolerated_untruste
PSID sid = (PSID)&ace->SidStart;
size_t sid_capacity = (size_t)header->AceSize - sid_offset;
DWORD sid_length = GetSidLengthRequired(((SID *)sid)->SubAuthorityCount);
PSID local_admin = activation_windows_local_admin_sid();
bool trusted = sid_length <= sid_capacity && IsValidSid(sid) &&
GetLengthSid(sid) == sid_length &&
(EqualSid(sid, user_sid) || IsWellKnownSid(sid, WinLocalSystemSid) ||
IsWellKnownSid(sid, WinBuiltinAdministratorsSid) ||
/* #1705: THIS machine's built-in Administrator (RID-500),
* resolved via LSA — never a foreign S-1-5-21-*-500; same
* tolerance as the daemon's win_sid_trusted. */
(local_admin && EqualSid(sid, local_admin)) ||
/* OWNER RIGHTS modulates whoever owns the object; the
* owner is separately validated in every chain that
* reaches here (same tolerance as the daemon IPC and
+105
View File
@@ -3377,6 +3377,7 @@ bool cbm_daemon_ipc_local_transition_release(cbm_daemon_ipc_local_transition_t *
#include <windows.h>
#include <aclapi.h>
#include <sddl.h>
#include <ntsecapi.h>
#include <fcntl.h>
#include <io.h>
#include <shlobj.h>
@@ -4034,17 +4035,121 @@ static bool win_sid_is_trusted_installer(const uint8_t *sid, size_t sid_length)
return true;
}
/* #1705: the built-in Administrator ACCOUNT of THIS machine — RID 500 under the
* local machine's own account-domain SID (S-1-5-21-<machine>-500) — is a
* legitimate owner/grantee of directories an elevated install created, even when
* that account has been renamed or is disabled. It is resolved by asking LSA for
* the local machine account-domain SID and synthesizing its RID-500 SID with
* CreateWellKnownSid, then compared with EqualSid.
*
* It is deliberately NOT tested with IsWellKnownSid(sid, WinAccountAdministratorSid)
* and NOT by matching a trailing RID of 500: BOTH of those accept ANY domain's
* -500 — a domain administrator, or another machine's built-in Administrator —
* which is exactly the cross-machine trust escalation this must never open. Only
* THIS machine's -500 is trusted.
*
* Resolved once per process and cached; any LSA or synthesis failure leaves the
* cache NULL and therefore grants NO tolerance at all (fail closed). advapi32 is
* reached through the already-loaded module handle in win_security_t and the
* function pointers are resolved dynamically, matching this file's SID-API style
* and adding no static import. */
typedef NTSTATUS(NTAPI *lsa_open_policy_fn)(PLSA_UNICODE_STRING, PLSA_OBJECT_ATTRIBUTES, ACCESS_MASK,
PLSA_HANDLE);
typedef NTSTATUS(NTAPI *lsa_query_information_policy_fn)(LSA_HANDLE, POLICY_INFORMATION_CLASS,
PVOID *);
typedef NTSTATUS(NTAPI *lsa_free_memory_fn)(PVOID);
typedef NTSTATUS(NTAPI *lsa_close_fn)(LSA_HANDLE);
typedef BOOL(WINAPI *create_well_known_sid_fn)(WELL_KNOWN_SID_TYPE, PSID, PSID, DWORD *);
static INIT_ONCE g_local_admin_sid_once = INIT_ONCE_STATIC_INIT;
static PSID g_local_admin_sid = NULL; /* process-lifetime cache; NULL => no tolerance */
static BOOL CALLBACK win_resolve_local_admin_sid(PINIT_ONCE once, PVOID parameter, PVOID *context) {
(void)once;
(void)context;
win_security_t *security = (win_security_t *)parameter;
if (!security || !security->advapi) {
return TRUE; /* ran once; cache stays NULL (fail closed) */
}
HMODULE advapi = security->advapi;
lsa_open_policy_fn lsa_open =
(lsa_open_policy_fn)(void (*)(void))GetProcAddress(advapi, "LsaOpenPolicy");
lsa_query_information_policy_fn lsa_query =
(lsa_query_information_policy_fn)(void (*)(void))GetProcAddress(advapi,
"LsaQueryInformationPolicy");
lsa_free_memory_fn lsa_free =
(lsa_free_memory_fn)(void (*)(void))GetProcAddress(advapi, "LsaFreeMemory");
lsa_close_fn lsa_close = (lsa_close_fn)(void (*)(void))GetProcAddress(advapi, "LsaClose");
create_well_known_sid_fn create_sid =
(create_well_known_sid_fn)(void (*)(void))GetProcAddress(advapi, "CreateWellKnownSid");
if (!lsa_open || !lsa_query || !lsa_free || !lsa_close || !create_sid) {
return TRUE;
}
LSA_OBJECT_ATTRIBUTES attributes;
memset(&attributes, 0, sizeof(attributes));
LSA_HANDLE policy = NULL;
/* STATUS_SUCCESS is 0; any other status (including informational positives) is
* treated as failure, keeping the outcome fail-closed. */
if (lsa_open(NULL, &attributes, POLICY_VIEW_LOCAL_INFORMATION, &policy) != 0 || !policy) {
return TRUE;
}
POLICY_ACCOUNT_DOMAIN_INFO *domain = NULL;
if (lsa_query(policy, PolicyAccountDomainInformation, (PVOID *)&domain) == 0 && domain &&
domain->DomainSid && security->is_valid_sid(domain->DomainSid)) {
DWORD needed = 0;
(void)create_sid(WinAccountAdministratorSid, domain->DomainSid, NULL, &needed);
if (needed > 0U) {
PSID resolved = malloc(needed);
if (resolved &&
create_sid(WinAccountAdministratorSid, domain->DomainSid, resolved, &needed) &&
security->is_valid_sid(resolved)) {
g_local_admin_sid = resolved;
} else {
free(resolved);
}
}
}
if (domain) {
(void)lsa_free(domain);
}
(void)lsa_close(policy);
return TRUE;
}
static PSID win_local_admin_sid(win_security_t *security) {
if (!security || !security->advapi) {
return NULL;
}
(void)InitOnceExecuteOnce(&g_local_admin_sid_once, win_resolve_local_admin_sid, (PVOID)security,
NULL);
return g_local_admin_sid;
}
static bool win_sid_trusted(win_security_t *security, PSID sid) {
if (!security || !sid || !security->is_valid_sid(sid)) {
return false;
}
DWORD sid_length = security->get_length_sid(sid);
PSID local_admin = win_local_admin_sid(security);
return (sid_length > 0U && security->equal_sid(sid, security->user_sid)) ||
security->is_well_known_sid(sid, WinLocalSystemSid) ||
security->is_well_known_sid(sid, WinBuiltinAdministratorsSid) ||
(local_admin && security->equal_sid(sid, local_admin)) ||
win_sid_is_trusted_installer((const uint8_t *)sid, (size_t)sid_length);
}
#ifdef CBM_ENABLE_TEST_SEAMS
bool cbm_daemon_ipc_win_sid_trusted_for_testing(void *sid) {
win_security_t security;
if (!win_security_init(&security)) {
return false;
}
bool trusted = win_sid_trusted(&security, (PSID)sid);
win_security_destroy(&security);
return trusted;
}
#endif
/* AppContainer identities: package SIDs (S-1-15-2-*) and capability SIDs
* (S-1-15-3-*), under the APP_PACKAGE identifier authority (15).
*
+7
View File
@@ -94,6 +94,13 @@ const char *cbm_daemon_ipc_validation_detail(void);
#ifdef CBM_ENABLE_TEST_SEAMS
/* #1537: seed the detail so a test can prove the CLI refusal surfaces it. */
void cbm_daemon_ipc_set_validation_detail_for_testing(const char *detail);
#ifdef _WIN32
/* #1705: run the daemon's directory-owner/ACE trust predicate against an
* arbitrary SID, so a test can assert THIS machine's built-in Administrator
* (RID-500) is trusted while a foreign S-1-5-21-*-500 is not. Returns false on
* any setup failure. Windows only. */
bool cbm_daemon_ipc_win_sid_trusted_for_testing(void *sid);
#endif
#endif
/* Create/validate an owner-only directory and securely open one regular
+115
View File
@@ -920,6 +920,120 @@ TEST(daemon_ipc_windows_private_directory_allows_add_subdirectory_only_ancestor)
PASS();
}
/* Derive THIS machine's account-domain SID (S-1-5-21-a-b-c) from the current
* process token (S-1-5-21-a-b-c-<rid>), independently of production's LSA-based
* resolution. Caller frees with FreeSid. */
static PSID ipc_test_local_account_domain_sid(void) {
HANDLE token = NULL;
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) {
return NULL;
}
DWORD needed = 0;
(void)GetTokenInformation(token, TokenUser, NULL, 0, &needed);
PSID domain = NULL;
if (needed > 0) {
void *buffer = calloc(1, needed);
if (buffer && GetTokenInformation(token, TokenUser, buffer, needed, &needed)) {
PSID user = ((TOKEN_USER *)buffer)->User.Sid;
if (user && IsValidSid(user)) {
UCHAR count = *GetSidSubAuthorityCount(user);
SID_IDENTIFIER_AUTHORITY *authority = GetSidIdentifierAuthority(user);
/* Machine/domain account SID: NT authority (Value[5]==5), first
* sub-authority 21, at least the three domain sub-authorities plus
* the account RID. Drop the RID to recover the domain SID. */
if (count >= 4 && authority->Value[5] == 5 && *GetSidSubAuthority(user, 0) == 21U) {
SID_IDENTIFIER_AUTHORITY nt = {SECURITY_NT_AUTHORITY};
(void)AllocateAndInitializeSid(
&nt, 4, *GetSidSubAuthority(user, 0), *GetSidSubAuthority(user, 1),
*GetSidSubAuthority(user, 2), *GetSidSubAuthority(user, 3), 0, 0, 0, 0,
&domain);
}
}
}
free(buffer);
}
(void)CloseHandle(token);
return domain;
}
/* Synthesize the RID-500 built-in Administrator SID for a given account-domain
* SID, exactly as production does. Caller frees with free(). */
static PSID ipc_test_admin_sid_for_domain(PSID domain_sid) {
if (!domain_sid) {
return NULL;
}
DWORD needed = 0;
(void)CreateWellKnownSid(WinAccountAdministratorSid, domain_sid, NULL, &needed);
if (needed == 0) {
return NULL;
}
PSID admin = malloc(needed);
if (admin && CreateWellKnownSid(WinAccountAdministratorSid, domain_sid, admin, &needed) &&
IsValidSid(admin)) {
return admin;
}
free(admin);
return NULL;
}
/* #1705: THIS machine's built-in Administrator ACCOUNT (RID-500 under the local
* account-domain SID) is a trusted directory owner/grantee, while a FOREIGN
* S-1-5-21-*-500 (a domain administrator, or another machine's built-in
* Administrator) must never be.
*
* RED without the fix: win_sid_trusted rejects the local RID-500 — on the VM this
* process runs as `test`, whose RID is not 500, so the -500 SID matches neither
* the current user nor SYSTEM, the Administrators GROUP, or TrustedInstaller — so
* local_ok is false. GREEN with the fix. foreign_ok is false either way, which is
* the exact cross-machine bypass this must never open. */
TEST(daemon_ipc_windows_sid_trust_accepts_local_admin_rejects_foreign_500) {
PSID local_domain = ipc_test_local_account_domain_sid();
PSID local_admin = ipc_test_admin_sid_for_domain(local_domain);
SID_IDENTIFIER_AUTHORITY nt = {SECURITY_NT_AUTHORITY};
PSID foreign_domain = NULL;
(void)AllocateAndInitializeSid(&nt, 4, 21U, 111U, 222U, 333U, 0, 0, 0, 0, &foreign_domain);
PSID foreign_admin = ipc_test_admin_sid_for_domain(foreign_domain);
PSID builtin_admins = NULL;
DWORD builtin_needed = 0;
(void)CreateWellKnownSid(WinBuiltinAdministratorsSid, NULL, NULL, &builtin_needed);
if (builtin_needed > 0) {
builtin_admins = malloc(builtin_needed);
if (builtin_admins &&
!CreateWellKnownSid(WinBuiltinAdministratorsSid, NULL, builtin_admins, &builtin_needed)) {
free(builtin_admins);
builtin_admins = NULL;
}
}
bool local_built = local_admin != NULL;
bool foreign_built = foreign_admin != NULL;
bool builtin_built = builtin_admins != NULL;
bool local_ok = local_built && cbm_daemon_ipc_win_sid_trusted_for_testing(local_admin);
bool foreign_ok = foreign_built && cbm_daemon_ipc_win_sid_trusted_for_testing(foreign_admin);
bool builtin_ok = builtin_built && cbm_daemon_ipc_win_sid_trusted_for_testing(builtin_admins);
free(local_admin);
free(foreign_admin);
free(builtin_admins);
if (local_domain) {
FreeSid(local_domain);
}
if (foreign_domain) {
FreeSid(foreign_domain);
}
ASSERT_TRUE(local_built);
ASSERT_TRUE(foreign_built);
ASSERT_TRUE(builtin_built);
ASSERT_TRUE(builtin_ok); /* the seam works: an already-trusted SID passes */
ASSERT_TRUE(local_ok); /* #1705 fix: THIS machine's RID-500 is trusted */
ASSERT_FALSE(foreign_ok); /* safety: a foreign -500 is NEVER trusted */
PASS();
}
TEST(daemon_ipc_windows_legacy_bridge_covers_handoff_and_lifetime) {
static const char key[] = "91a2b3c4d5e6f708";
char parent[TEST_PATH_CAP] = {0};
@@ -4964,6 +5078,7 @@ SUITE(daemon_ipc) {
RUN_TEST(daemon_ipc_windows_private_directory_rejects_untrusted_ancestor_acl);
RUN_TEST(daemon_ipc_windows_private_directory_ace_is_inheritable);
RUN_TEST(daemon_ipc_windows_private_directory_allows_add_subdirectory_only_ancestor);
RUN_TEST(daemon_ipc_windows_sid_trust_accepts_local_admin_rejects_foreign_500);
RUN_TEST(daemon_ipc_windows_legacy_bridge_covers_handoff_and_lifetime);
RUN_TEST(daemon_ipc_windows_local_transition_atomically_reserves_legacy_pipe);
RUN_TEST(daemon_ipc_windows_startup_retries_transient_rendezvous_reader);