From 4c793c4ded00ff5baca4b0f1853cbbd519ce3642 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Thu, 13 Aug 2026 10:04:55 +0200 Subject: [PATCH] fix(windows): accept the owner Windows actually stamps on what we create MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An elevated install created its own staging file and then refused it: error: failed to stage install candidate: activation transaction I/O failed: owner-not-current-user We compared the object's owner against TokenUser. Windows stamps newly created objects with TokenOwner, and for a member of the Administrators group that defaults to BUILTIN\Administrators ("System objects: Default owner for objects created by members of the Administrators group" — default on Server, common on hardened clients). So on any elevated account the check rejected a file written seconds earlier by the same process. Reported by @spam0115 (#1580); @gotspatel hit the same wall from the other side (#1582) — a fresh install whose MCP server exits during initialize, from `C:\Users\Administrator`. The fix reads TokenOwner and accepts that SID too. This is stricter-or-equal, never looser: TokenOwner is the ONE SID this process stamps on its own creations, so the check still refuses anything a different account owns. It does NOT accept "whatever an administrator owns" — that would be the loose version, and the neighbouring activation_windows_owner_is_trusted already covers the pre-existing-directory case where a broader rule is appropriate. On a non-elevated account TokenOwner == TokenUser and nothing changes at all. Windows-only code, so it is unverifiable on the macOS host: this needs the Windows leg to compile and exercise, which is the next step before merge. Signed-off-by: Martin Vogel --- src/cli/activation_transaction.c | 57 ++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/src/cli/activation_transaction.c b/src/cli/activation_transaction.c index d9af2185b..101164e70 100644 --- a/src/cli/activation_transaction.c +++ b/src/cli/activation_transaction.c @@ -453,6 +453,52 @@ static bool activation_windows_owner_is_trusted(HANDLE handle) { return trusted; } +/* The SID Windows will stamp as OWNER on objects this process creates. + * + * That is TokenOwner, NOT TokenUser, and the two differ exactly when it matters: + * for a member of the Administrators group the default owner is + * BUILTIN\Administrators (the "System objects: Default owner for objects + * created by members of the Administrators group" policy, default on Server and + * common on hardened clients). So an elevated install created its staging file, + * then refused it as "owner-not-current-user" — we rejected a file we had just + * written ourselves (#1580), and the daemon path failed the same way, exiting + * before it could say anything (#1582). + * + * Reading TokenOwner is STRICTER-or-equal, never looser: it is the one SID this + * process stamps. On a non-elevated account TokenOwner == TokenUser and nothing + * changes. It does not accept "anything an administrator owns" — only the exact + * SID our own creations carry. */ +static bool activation_windows_token_owner(void **information_out, PSID *sid_out) { + *information_out = NULL; + *sid_out = NULL; + HANDLE token = NULL; + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) { + return false; + } + DWORD needed = 0; + (void)GetTokenInformation(token, TokenOwner, NULL, 0, &needed); + if (needed == 0 || GetLastError() != ERROR_INSUFFICIENT_BUFFER) { + (void)CloseHandle(token); + return false; + } + void *information = calloc(1, needed); + bool ok = + information && GetTokenInformation(token, TokenOwner, information, needed, &needed) != 0; + (void)CloseHandle(token); + if (!ok) { + free(information); + return false; + } + PSID sid = ((TOKEN_OWNER *)information)->Owner; + if (!sid || !IsValidSid(sid)) { + free(information); + return false; + } + *information_out = information; + *sid_out = sid; + return true; +} + static bool activation_windows_owner_is_current(HANDLE handle) { void *information = NULL; PSID user_sid = NULL; @@ -464,6 +510,17 @@ static bool activation_windows_owner_is_current(HANDLE handle) { DWORD result = GetSecurityInfo(handle, SE_FILE_OBJECT, OWNER_SECURITY_INFORMATION, &owner, NULL, NULL, NULL, &descriptor); bool same = result == ERROR_SUCCESS && owner && IsValidSid(owner) && EqualSid(owner, user_sid); + /* Not our user SID — but Windows may legitimately have stamped our token's + * OWNER instead (Administrators, for an elevated account). Accept that and + * only that; see activation_windows_token_owner. */ + if (!same && result == ERROR_SUCCESS && owner && IsValidSid(owner)) { + void *owner_information = NULL; + PSID token_owner = NULL; + if (activation_windows_token_owner(&owner_information, &token_owner)) { + same = EqualSid(owner, token_owner) != 0; + free(owner_information); + } + } if (!same) { activation_note_refusal("owner-not-current-user", result); }