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