Files
codebase-memory-mcp/src/cli/activation_transaction.c
T
Glen KoandClaude 2fbf3fb977 fix(cli): fold per-user /home entry into the alias loop, add test seam
Address review on #2309:
- Fold the /home/<name> handling into the existing #2175 alias loop via a
  shared activation_alias_map() that builds the mapped path in a stack
  buffer and returns activation_string_copy(); raw allocator sites in
  activation_transaction.c drop from 80 to 78 (baseline lowered).
- Comment the deliberate difference: the per-user entry may resolve to a
  directory owned by the current account, the whole-alias case may not.
- Add cbm_activation_transaction_set_home_root_for_testing() (test seams
  only) and a Linux test that fails without the per-user branch.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Glen Ko <gleko@amazon.com>
2026-09-30 20:49:58 +00:00

2514 lines
104 KiB
C

/* Transactional binary activation. See activation_transaction.h. */
#include "cli/activation_transaction.h"
#include "foundation/log.h"
#include "foundation/macos_acl.h"
#include <errno.h>
#include <inttypes.h>
#include <stdatomic.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <wchar.h>
#include <sys/stat.h>
#ifdef _WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <aclapi.h>
#include <ntsecapi.h>
#include <sddl.h>
#include <windows.h>
#else
#include <fcntl.h>
#ifdef __APPLE__
#include <sys/acl.h>
#include <sys/stdio.h>
#elif defined(__linux__)
#include <sys/syscall.h>
#endif
#include <unistd.h>
#endif
typedef enum {
ACTIVATION_REPLACE = 0,
ACTIVATION_REMOVE = 1,
} activation_action_t;
typedef enum {
ACTIVATION_STAGED = 0,
ACTIVATION_COMMITTED = 1,
ACTIVATION_ROLLED_BACK = 2,
ACTIVATION_FINALIZED = 3,
ACTIVATION_FINALIZED_DEFERRED = 4,
ACTIVATION_RECOVERY_NEEDED = 5,
} activation_state_t;
typedef struct {
#ifdef _WIN32
DWORD volume_serial;
DWORD index_high;
DWORD index_low;
#else
dev_t device;
ino_t inode;
#endif
} activation_file_identity_t;
struct cbm_activation_transaction {
activation_action_t action;
activation_state_t state;
char *target_path;
char *directory_path;
char *target_name;
char *staged_path;
char *staged_name;
char *backup_path;
char *backup_name;
bool target_existed;
bool staged_exists;
bool backup_exists;
bool backup_contains_target;
bool deferred_cleanup;
activation_file_identity_t target_identity;
activation_file_identity_t directory_identity;
activation_file_identity_t staged_identity;
activation_file_identity_t backup_identity;
#ifndef _WIN32
int directory_fd;
#endif
};
/* The transaction's security predicates refuse by returning false without a
* usable OS last-error, so every caller used to report the same blind
* "status -3, os 0" for five distinct refusal classes (ancestor open,
* owner, DACL, target snapshot, staged create). The first predicate that
* refuses records what refused here; callers append the note to their error
* line. First-wins so the deepest (root) refusal survives the unwind.
* Single-threaded by contract: CLI activation runs on one thread. */
static char g_activation_refusal_note[512];
/* The validated object's path, set by each validator for the duration of its
* predicate calls so the note can say WHAT was refused, not just why. Points
* into the caller's storage; never dereferenced after the validator returns. */
static const char *g_activation_refusal_object = NULL;
static void activation_refusal_clear(void) {
g_activation_refusal_note[0] = '\0';
}
static void activation_note_refusal(const char *predicate, unsigned long os_error) {
if (g_activation_refusal_note[0] != '\0') {
return;
}
(void)snprintf(g_activation_refusal_note, sizeof(g_activation_refusal_note), "%s (os %lu)%s%s",
predicate, os_error, g_activation_refusal_object ? " at " : "",
g_activation_refusal_object ? g_activation_refusal_object : "");
}
#ifndef _WIN32
/* A permission refusal has no errno to report — the syscall succeeded and the
* POLICY said no. Reporters spent hours chasing "I/O failed" for what was a
* mode bit (#1535), so these refusals carry the mode and the path instead of a
* fabricated OS error code. POSIX-only: the Windows validators refuse on ACL
* predicates and report through activation_note_refusal with a real OS error. */
static void activation_note_refusal_detail(const char *predicate, const char *detail) {
if (g_activation_refusal_note[0] != '\0') {
return;
}
(void)snprintf(g_activation_refusal_note, sizeof(g_activation_refusal_note), "%s (%s)%s%s",
predicate, detail, g_activation_refusal_object ? " at " : "",
g_activation_refusal_object ? g_activation_refusal_object : "");
}
#endif
const char *cbm_activation_transaction_refusal_note(void) {
return g_activation_refusal_note;
}
#ifdef CBM_ENABLE_TEST_SEAMS
/* #1416 test seam: install a refusal note so the CLI attribution path is
* testable without constructing a real Windows ACL refusal. */
void cbm_activation_transaction_note_refusal_for_testing(const char *predicate,
unsigned long os_error) {
activation_refusal_clear();
if (predicate && predicate[0]) {
activation_note_refusal(predicate, os_error);
}
}
#endif
#ifdef _WIN32
typedef HANDLE activation_native_file_t;
#define ACTIVATION_INVALID_FILE INVALID_HANDLE_VALUE
#else
typedef int activation_native_file_t;
#define ACTIVATION_INVALID_FILE (-1)
#endif
static atomic_uint_fast64_t activation_unique_sequence = ATOMIC_VAR_INIT(0);
static cbm_activation_transaction_before_absent_publish_for_test_fn
activation_before_absent_publish_for_test;
static void *activation_before_absent_publish_context_for_test;
void cbm_activation_transaction_set_before_absent_publish_for_test(
cbm_activation_transaction_before_absent_publish_for_test_fn hook, void *context) {
activation_before_absent_publish_context_for_test = context;
activation_before_absent_publish_for_test = hook;
}
static char *activation_string_copy(const char *value) {
if (!value) {
return NULL;
}
size_t length = strlen(value);
char *copy = malloc(length + 1U);
if (copy) {
memcpy(copy, value, length + 1U);
}
return copy;
}
static char *activation_string_span(const char *value, size_t length) {
char *copy = malloc(length + 1U);
if (copy) {
memcpy(copy, value, length);
copy[length] = '\0';
}
return copy;
}
static bool activation_target_parts(const char *target_path, char **directory_out,
char **name_out) {
*directory_out = NULL;
*name_out = NULL;
if (!target_path || !target_path[0]) {
return false;
}
size_t length = strlen(target_path);
if (target_path[length - 1U] == '/' || target_path[length - 1U] == '\\') {
return false;
}
const char *separator = strrchr(target_path, '/');
#ifdef _WIN32
const char *backslash = strrchr(target_path, '\\');
if (backslash && (!separator || backslash > separator)) {
separator = backslash;
}
#endif
const char *base = separator ? separator + 1 : target_path;
if (!base[0] || strcmp(base, ".") == 0 || strcmp(base, "..") == 0) {
return false;
}
if (!separator) {
*directory_out = activation_string_copy(".");
} else if (separator == target_path) {
*directory_out = activation_string_span(target_path, 1U);
#ifdef _WIN32
} else if (separator == target_path + 2 && target_path[1] == ':') {
*directory_out = activation_string_span(target_path, 3U);
#endif
} else {
*directory_out = activation_string_span(target_path, (size_t)(separator - target_path));
}
if (*directory_out) {
*name_out = activation_string_copy(base);
}
if (!*directory_out || !*name_out) {
free(*directory_out);
free(*name_out);
*directory_out = NULL;
*name_out = NULL;
return false;
}
return true;
}
static char *activation_path_name_copy(const char *path) {
const char *slash = strrchr(path, '/');
#ifdef _WIN32
const char *backslash = strrchr(path, '\\');
if (backslash && (!slash || backslash > slash)) {
slash = backslash;
}
#endif
return activation_string_copy(slash ? slash + 1 : path);
}
static char *activation_unique_path(const char *directory, const char *tag) {
uint64_t sequence =
atomic_fetch_add_explicit(&activation_unique_sequence, 1U, memory_order_relaxed) + 1U;
#ifdef _WIN32
unsigned long process_id = (unsigned long)GetCurrentProcessId();
#else
unsigned long process_id = (unsigned long)getpid();
#endif
size_t directory_length = strlen(directory);
size_t needed = directory_length + strlen(tag) + 80U;
char *path = malloc(needed);
if (!path) {
return NULL;
}
/* Windows joins use the backslash: extended-length (\\\\?\\) directories
* reach this composer and that namespace performs no forward-slash
* translation. POSIX keeps the slash. */
#ifdef _WIN32
const char *separator = "\\";
#else
const char *separator = "/";
#endif
int written =
snprintf(path, needed, "%s%s.cbm-%s-%lu-%" PRIu64, directory,
directory[directory_length - 1U] == '/' || directory[directory_length - 1U] == '\\'
? ""
: separator,
tag, process_id, sequence);
if (written <= 0 || (size_t)written >= needed) {
free(path);
return NULL;
}
return path;
}
static bool activation_identity_equal(const activation_file_identity_t *left,
const activation_file_identity_t *right) {
#ifdef _WIN32
return left->volume_serial == right->volume_serial && left->index_high == right->index_high &&
left->index_low == right->index_low;
#else
return left->device == right->device && left->inode == right->inode;
#endif
}
#ifdef _WIN32
typedef struct {
void *token_information;
PSID user_sid;
PACL acl;
SECURITY_DESCRIPTOR descriptor;
SECURITY_ATTRIBUTES attributes;
} activation_windows_security_t;
static wchar_t *activation_utf8_to_wide_plain(const char *value) {
if (!value) {
return NULL;
}
int needed = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, value, -1, NULL, 0);
if (needed <= 0) {
return NULL;
}
wchar_t *wide = malloc((size_t)needed * sizeof(*wide));
if (!wide || MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, value, -1, wide, needed) <= 0) {
free(wide);
return NULL;
}
return wide;
}
/* File-API path conversion: an absolute drive path beyond the legacy limit is
* canonicalized and given the extended-length \\?\ prefix so every Win32
* file call accepts it. Managed-install generation stores and CI-runner paths
* routinely exceed 260 chars; the prefix also disables the \.\..\ and
* MAX_PATH parsing that the ancestry validator separately enforces. Non-file
* strings must not use this. */
static wchar_t *activation_utf8_to_wide(const char *value) {
wchar_t *wide = activation_utf8_to_wide_plain(value);
if (!wide) {
return NULL;
}
size_t length = wcslen(wide);
bool drive_absolute =
length >= 3 &&
((wide[0] >= L'A' && wide[0] <= L'Z') || (wide[0] >= L'a' && wide[0] <= L'z')) &&
wide[1] == L':' && (wide[2] == L'\\' || wide[2] == L'/');
if (!drive_absolute || length < 240U) {
return wide;
}
DWORD needed = GetFullPathNameW(wide, 0, NULL, NULL);
wchar_t *prefixed = needed > 0 ? malloc(((size_t)needed + 5U) * sizeof(*prefixed)) : NULL;
if (!prefixed) {
free(wide);
return NULL;
}
wmemcpy(prefixed, L"\\\\?\\", 4);
DWORD copied = GetFullPathNameW(wide, needed, prefixed + 4, NULL);
free(wide);
if (copied == 0 || copied >= needed) {
free(prefixed);
return NULL;
}
return prefixed;
}
static bool activation_windows_user(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, TokenUser, 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, TokenUser, information, needed, &needed) != 0;
(void)CloseHandle(token);
if (!ok) {
free(information);
return false;
}
PSID sid = ((TOKEN_USER *)information)->User.Sid;
if (!sid || !IsValidSid(sid)) {
free(information);
return false;
}
*information_out = information;
*sid_out = sid;
return true;
}
static bool activation_windows_security_init(activation_windows_security_t *security) {
memset(security, 0, sizeof(*security));
if (!activation_windows_user(&security->token_information, &security->user_sid)) {
return false;
}
EXPLICIT_ACCESSW access;
memset(&access, 0, sizeof(access));
access.grfAccessPermissions = GENERIC_ALL;
access.grfAccessMode = SET_ACCESS;
access.grfInheritance = NO_INHERITANCE;
access.Trustee.TrusteeForm = TRUSTEE_IS_SID;
access.Trustee.TrusteeType = TRUSTEE_IS_USER;
access.Trustee.ptstrName = (LPWSTR)security->user_sid;
bool ok =
SetEntriesInAclW(1, &access, NULL, &security->acl) == ERROR_SUCCESS &&
InitializeSecurityDescriptor(&security->descriptor, SECURITY_DESCRIPTOR_REVISION) &&
SetSecurityDescriptorDacl(&security->descriptor, TRUE, security->acl, FALSE) &&
/* Stamp the owner explicitly to the token user. Without this the file
* created with these attributes inherits the token's DEFAULT owner,
* which under an Administrators-default-owner policy (standard on
* Windows Server and GitHub's elevated runners) is BUILTIN\
* Administrators, not this user. Every staged file is then validated
* with a strict owner-is-current-user check (activation_windows_owner_
* is_current), so an unstamped file is rejected and private staging
* fails. The daemon stamps the owner identically for the same reason
* (see src/daemon/ipc.c). Setting the owner to our own token user
* needs no extra privilege. */
SetSecurityDescriptorOwner(&security->descriptor, security->user_sid, FALSE) &&
SetSecurityDescriptorControl(&security->descriptor, SE_DACL_PROTECTED, SE_DACL_PROTECTED);
if (!ok) {
if (security->acl) {
(void)LocalFree(security->acl);
}
free(security->token_information);
memset(security, 0, sizeof(*security));
return false;
}
security->attributes.nLength = sizeof(security->attributes);
security->attributes.lpSecurityDescriptor = &security->descriptor;
security->attributes.bInheritHandle = FALSE;
return true;
}
static void activation_windows_security_destroy(activation_windows_security_t *security) {
if (security->acl) {
(void)LocalFree(security->acl);
}
free(security->token_information);
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
* by identity snapshot/recheck plus the caller's build-fingerprint check,
* not by exact ownership. Staging targets created by this transaction keep
* the exact-current-owner rule. */
static bool activation_windows_owner_is_trusted(HANDLE handle) {
void *information = NULL;
PSID user_sid = NULL;
if (!activation_windows_user(&information, &user_sid)) {
return false;
}
PSID owner = NULL;
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) ||
(local_admin && EqualSid(owner, local_admin)));
if (!trusted) {
activation_windows_note_untrusted_owner("owner-not-trusted", owner, result);
}
if (descriptor) {
(void)LocalFree(descriptor);
}
free(information);
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;
if (!activation_windows_user(&information, &user_sid)) {
return false;
}
PSID owner = NULL;
PSECURITY_DESCRIPTOR descriptor = NULL;
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_windows_note_untrusted_owner("owner-not-current-user", owner, result);
}
if (descriptor) {
(void)LocalFree(descriptor);
}
free(information);
return same;
}
static bool activation_windows_acl_check(HANDLE handle, DWORD tolerated_untrusted_mask);
static bool activation_windows_acl_secure(HANDLE handle) {
return activation_windows_acl_check(handle, 0U);
}
static bool activation_windows_acl_check(HANDLE handle, DWORD tolerated_untrusted_mask) {
void *information = NULL;
PSID user_sid = NULL;
if (!activation_windows_user(&information, &user_sid)) {
return false;
}
PACL dacl = NULL;
PSECURITY_DESCRIPTOR descriptor = NULL;
DWORD result = GetSecurityInfo(handle, SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, NULL, NULL,
&dacl, NULL, &descriptor);
ACL_SIZE_INFORMATION acl_information;
memset(&acl_information, 0, sizeof(acl_information));
bool secure =
result == ERROR_SUCCESS && descriptor && dacl && IsValidAcl(dacl) != 0 &&
GetAclInformation(dacl, &acl_information, sizeof(acl_information), AclSizeInformation) != 0;
const DWORD mutation_rights = GENERIC_ALL | GENERIC_WRITE | FILE_WRITE_DATA | FILE_APPEND_DATA |
FILE_ADD_FILE | FILE_ADD_SUBDIRECTORY | FILE_DELETE_CHILD |
FILE_WRITE_EA | FILE_WRITE_ATTRIBUTES | DELETE | WRITE_DAC |
WRITE_OWNER;
enum {
ACTIVATION_WINDOWS_ACE_ALLOW = 0x00,
ACTIVATION_WINDOWS_ACE_DENY = 0x01,
ACTIVATION_WINDOWS_ACE_DENY_OBJECT = 0x06,
ACTIVATION_WINDOWS_ACE_DENY_CALLBACK = 0x0a,
ACTIVATION_WINDOWS_ACE_DENY_CALLBACK_OBJECT = 0x0c,
};
for (DWORD index = 0; secure && index < acl_information.AceCount; index++) {
void *opaque_ace = NULL;
if (!GetAce(dacl, index, &opaque_ace) || !opaque_ace) {
secure = false;
break;
}
ACE_HEADER *header = opaque_ace;
if (header->AceType == ACTIVATION_WINDOWS_ACE_DENY ||
header->AceType == ACTIVATION_WINDOWS_ACE_DENY_OBJECT ||
header->AceType == ACTIVATION_WINDOWS_ACE_DENY_CALLBACK ||
header->AceType == ACTIVATION_WINDOWS_ACE_DENY_CALLBACK_OBJECT) {
continue;
}
if (header->AceType != ACTIVATION_WINDOWS_ACE_ALLOW) {
/* Only the fixed-layout ACCESS_ALLOWED_ACE is parsed below. Every
* callback/object/compound allow form and every unknown future
* form is rejected so payloads cannot hide mutation grants. */
secure = false;
break;
}
const size_t sid_offset = offsetof(ACCESS_ALLOWED_ACE, SidStart);
const size_t sid_header_size = offsetof(SID, SubAuthority);
if ((size_t)header->AceSize < sid_offset + sid_header_size) {
secure = false;
break;
}
ACCESS_ALLOWED_ACE *ace = opaque_ace;
if ((ace->Mask & mutation_rights & ~tolerated_untrusted_mask) == 0) {
continue;
}
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
* launcher ancestry validators). */
IsWellKnownSid(sid, WinCreatorOwnerRightsSid));
if (!trusted) {
char label[128];
LPSTR sid_text = NULL;
/* #1856: say whether the grant is INHERITED. The remedy differs and
* the wrong one silently does nothing: `icacls <dir> /remove:g <sid>`
* cannot remove an inherited ACE -- that needs `/inheritance:r` --
* and the stock `C:\` ACE for Authenticated Users reaches every new
* child directory exactly this way. A reporter following the
* generic advice sees the command succeed and the refusal persist. */
bool inherited = (header->AceFlags & INHERITED_ACE) != 0;
(void)snprintf(label, sizeof(label), "acl-grants-cross-account-mutation to %s%s",
ConvertSidToStringSidA(sid, &sid_text) && sid_text ? sid_text
: "unparsable-sid",
inherited ? " (inherited from a parent directory)" : "");
if (sid_text) {
(void)LocalFree(sid_text);
}
activation_note_refusal(label, 0UL);
secure = false;
}
}
if (!secure) {
activation_note_refusal("acl-unreadable-or-malformed", result);
}
if (descriptor) {
(void)LocalFree(descriptor);
}
free(information);
return secure;
}
static bool activation_windows_identity(HANDLE handle, activation_file_identity_t *identity_out,
bool require_regular) {
BY_HANDLE_FILE_INFORMATION information;
if (handle == INVALID_HANDLE_VALUE || GetFileType(handle) != FILE_TYPE_DISK ||
!GetFileInformationByHandle(handle, &information)) {
return false;
}
if (require_regular && ((information.dwFileAttributes &
(FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT)) != 0 ||
information.nNumberOfLinks != 1)) {
return false;
}
identity_out->volume_serial = information.dwVolumeSerialNumber;
identity_out->index_high = information.nFileIndexHigh;
identity_out->index_low = information.nFileIndexLow;
return true;
}
static HANDLE activation_windows_directory_open_no_reparse(const char *directory) {
/* Canonicalize on the PLAIN wide form so GetFullPathNameW resolves any
* \.\..\ and slash forms; each ancestor is then opened through the
* extended-length prefix so a deep component past MAX_PATH still opens. */
wchar_t *input = activation_utf8_to_wide_plain(directory);
if (!input) {
return INVALID_HANDLE_VALUE;
}
/* Managed-install callers hand in extended-length paths already; drop
* their prefix for canonicalization and the drive-shape check below —
* the component walk re-adds it for every open. */
wchar_t *shape = wcsncmp(input, L"\\\\?\\", 4) == 0 ? input + 4 : input;
DWORD needed = GetFullPathNameW(shape, 0, NULL, NULL);
wchar_t *path = needed > 0 ? calloc((size_t)needed + 1U, sizeof(*path)) : NULL;
DWORD length = path ? GetFullPathNameW(shape, needed + 1U, path, NULL) : 0;
free(input);
if (!path || length < 3 || length > needed || path[1] != L':' ||
(path[2] != L'\\' && path[2] != L'/')) {
free(path);
return INVALID_HANDLE_VALUE;
}
for (DWORD index = 0; index < length; index++) {
if (path[index] == L'/') {
path[index] = L'\\';
}
}
/* prefixed = \\?\ + canonical path; each open truncates this buffer. */
wchar_t *prefixed = malloc(((size_t)length + 5U) * sizeof(*prefixed));
if (!prefixed) {
free(path);
return INVALID_HANDLE_VALUE;
}
wmemcpy(prefixed, L"\\\\?\\", 4);
wmemcpy(prefixed + 4, path, (size_t)length + 1U);
HANDLE final_handle = INVALID_HANDLE_VALUE;
for (DWORD boundary = 3; boundary <= length; boundary++) {
if (boundary < length && path[boundary] != L'\\') {
continue;
}
if (boundary < length && boundary > 0 && path[boundary - 1] == L'\\') {
free(prefixed);
free(path);
return INVALID_HANDLE_VALUE;
}
wchar_t saved = prefixed[boundary + 4U];
prefixed[boundary + 4U] = L'\0';
HANDLE handle =
CreateFileW(prefixed, FILE_READ_ATTRIBUTES | READ_CONTROL,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, NULL);
DWORD open_error = handle == INVALID_HANDLE_VALUE ? GetLastError() : ERROR_SUCCESS;
prefixed[boundary + 4U] = saved;
BY_HANDLE_FILE_INFORMATION information;
bool valid = handle != INVALID_HANDLE_VALUE && GetFileType(handle) == FILE_TYPE_DISK &&
GetFileInformationByHandle(handle, &information) &&
(information.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0 &&
(information.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0;
if (!valid) {
activation_note_refusal(handle == INVALID_HANDLE_VALUE
? "ancestor-component-open"
: "ancestor-component-not-plain-directory",
open_error);
if (handle != INVALID_HANDLE_VALUE) {
(void)CloseHandle(handle);
}
free(prefixed);
free(path);
return INVALID_HANDLE_VALUE;
}
if (boundary == length) {
final_handle = handle;
} else {
(void)CloseHandle(handle);
}
}
free(prefixed);
free(path);
return final_handle;
}
/* SOURCE-side directory rule: trusted owner, and cross-account grants are
* tolerated only for pure add rights (FILE_ADD_FILE/FILE_ADD_SUBDIRECTORY,
* standard on Downloads-class locations) — they cannot modify the existing
* source file, whose bytes are pinned by identity recheck plus the staged
* copy's build-fingerprint validation. */
static bool activation_source_directory_secure(const char *directory,
activation_file_identity_t *identity_out) {
g_activation_refusal_object = directory;
HANDLE handle = activation_windows_directory_open_no_reparse(directory);
BY_HANDLE_FILE_INFORMATION information;
bool open_ok = handle != INVALID_HANDLE_VALUE &&
GetFileInformationByHandle(handle, &information) &&
(information.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0 &&
(information.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0 &&
activation_windows_identity(handle, identity_out, false);
bool owner_ok = open_ok && activation_windows_owner_is_trusted(handle);
bool acl_ok = owner_ok && activation_windows_acl_check(
handle, FILE_ADD_FILE | (DWORD)FILE_ADD_SUBDIRECTORY);
if (handle != INVALID_HANDLE_VALUE) {
(void)CloseHandle(handle);
}
g_activation_refusal_object = NULL;
return acl_ok;
}
static bool activation_directory_secure(const char *directory, int *unused,
activation_file_identity_t *identity_out) {
(void)unused;
g_activation_refusal_object = directory;
HANDLE handle = activation_windows_directory_open_no_reparse(directory);
BY_HANDLE_FILE_INFORMATION information;
bool open_ok = handle != INVALID_HANDLE_VALUE &&
GetFileInformationByHandle(handle, &information) &&
(information.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0 &&
(information.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0 &&
activation_windows_identity(handle, identity_out, false);
bool owner_ok = open_ok && activation_windows_owner_is_current(handle);
bool acl_ok = owner_ok && activation_windows_acl_secure(handle);
if (handle != INVALID_HANDLE_VALUE) {
(void)CloseHandle(handle);
}
g_activation_refusal_object = NULL;
return acl_ok;
}
#else
static bool activation_posix_acl_empty(int descriptor) {
return cbm_macos_extended_acl_fd_is_empty(descriptor);
}
#ifdef __linux__
/* Root of the per-user home tree and the account trusted to own it. Root and
* "/home" in production; the test seam substitutes a fixture tree and the test
* account, because the real layout needs root-owned entries to reproduce. */
static const char *g_activation_home_root = "/home";
static uid_t g_activation_home_owner = 0;
#ifdef CBM_ENABLE_TEST_SEAMS
void cbm_activation_transaction_set_home_root_for_testing(const char *home_root,
unsigned long owner_uid) {
g_activation_home_root = home_root ? home_root : "/home";
g_activation_home_owner = home_root ? (uid_t)owner_uid : 0;
}
#endif
#endif
#if defined(__APPLE__) || defined(__linux__)
/* Map a trusted symlink `link` (already lstat'd) onto its resolved target and
* append `rest`. The link and the target must both belong to `owner`; with
* accept_own_target the target may instead belong to the current account.
* Returns NULL when either check fails or the mapped path does not fit. */
static char *activation_alias_map(const char *link, const struct stat *link_status, uid_t owner,
bool accept_own_target, const char *rest) {
char resolved[4096];
struct stat resolved_status;
if (link_status->st_uid != owner || !realpath(link, resolved) ||
lstat(resolved, &resolved_status) != 0 || !S_ISDIR(resolved_status.st_mode)) {
return NULL;
}
if (resolved_status.st_uid != owner &&
!(accept_own_target && resolved_status.st_uid == geteuid())) {
return NULL;
}
char mapped[4096];
int written = snprintf(mapped, sizeof(mapped), "%s%s", resolved, rest);
if (written <= 0 || (size_t)written >= sizeof(mapped)) {
return NULL;
}
return activation_string_copy(mapped);
}
#endif
static char *activation_posix_walk_path(const char *directory) {
#if defined(__APPLE__) || defined(__linux__)
/* macOS and immutable Linux layouts can expose writable trees through
* root-owned aliases (for example /tmp and /home on Atomic systems).
* Resolve only these trusted system aliases; arbitrary user symlinks must
* still fail the O_NOFOLLOW walk below. */
#ifdef __linux__
const char *const aliases[] = {"/tmp", "/var", g_activation_home_root};
#else
static const char *const aliases[] = {"/tmp", "/var"};
#endif
for (size_t index = 0; index < sizeof(aliases) / sizeof(aliases[0]); index++) {
const char *alias = aliases[index];
size_t alias_length = strlen(alias);
if (strncmp(directory, alias, alias_length) != 0 ||
(directory[alias_length] != '\0' && directory[alias_length] != '/')) {
continue;
}
uid_t owner = 0;
#ifdef __linux__
bool is_home = alias == g_activation_home_root;
if (is_home) {
owner = g_activation_home_owner;
}
#endif
struct stat alias_status;
if (lstat(alias, &alias_status) != 0) {
continue;
}
if (S_ISLNK(alias_status.st_mode)) {
return activation_alias_map(alias, &alias_status, owner, false,
directory + alias_length);
}
#ifdef __linux__
/* Managed Linux hosts often keep /home a real directory and point the
* per-user entry elsewhere (/home/alice -> /local/home/alice, #2306).
* Trust that entry only inside a root-owned /home nobody else can
* write, and only when root owns the entry itself. Unlike the whole-
* alias case above, the target may belong to the current account:
* that is the normal shape of a relocated home, and an account can
* already write its own home, so it gains nothing it did not have. */
const char *name = directory + alias_length;
size_t name_length = *name == '/' ? strcspn(name + 1, "/") : 0;
if (!is_home || !S_ISDIR(alias_status.st_mode) || alias_status.st_uid != owner ||
(alias_status.st_mode & 0022) != 0 || name_length == 0) {
continue;
}
char entry[4096];
size_t entry_length = alias_length + 1U + name_length;
if (entry_length >= sizeof(entry)) {
return NULL;
}
memcpy(entry, directory, entry_length);
entry[entry_length] = '\0';
struct stat entry_status;
if (lstat(entry, &entry_status) == 0 && S_ISLNK(entry_status.st_mode)) {
return activation_alias_map(entry, &entry_status, owner, true,
directory + entry_length);
}
#endif
}
#endif
return activation_string_copy(directory);
}
/* ANCESTOR policy (#1535). World-writable is still fatal: any local user could
* swap a path component mid-transaction. GROUP-writable is not — it is the
* default shape of ordinary home trees (WSL2 ships ~ and ~/.local at 0775, as
* do several distro skeletons and any site using a shared primary group), and
* refusing it made `install.sh` fail for a large fraction of Linux users with
* no actionable message. The group is a bounded, administratively-chosen set;
* the LEAF directory (below) stays strictly owner-private either way, so the
* binary itself is never left in a group-writable directory. Group-writable
* ancestors are warned about, out loud, rather than silently accepted. */
static bool activation_posix_intermediate_secure(const struct stat *status) {
bool trusted_owner = status->st_uid == 0 || status->st_uid == geteuid();
bool world_writable = (status->st_mode & 0002) != 0;
bool root_sticky = status->st_uid == 0 && (status->st_mode & S_ISVTX) != 0;
return S_ISDIR(status->st_mode) && trusted_owner && (!world_writable || root_sticky);
}
static bool activation_posix_intermediate_group_writable(const struct stat *status) {
bool root_sticky = status->st_uid == 0 && (status->st_mode & S_ISVTX) != 0;
return (status->st_mode & 0020) != 0 && !root_sticky;
}
static bool activation_directory_secure(const char *directory, int *directory_fd_out,
activation_file_identity_t *identity_out) {
int flags = O_RDONLY | O_DIRECTORY | O_CLOEXEC;
#ifdef O_NOFOLLOW
flags |= O_NOFOLLOW;
#endif
char *walk_path = activation_posix_walk_path(directory);
if (!walk_path) {
return false;
}
bool absolute = walk_path[0] == '/';
int descriptor = open(absolute ? "/" : ".", flags);
bool ok = descriptor >= 0;
char *cursor = walk_path;
while (*cursor == '/') {
cursor++;
}
if (ok && *cursor) {
struct stat initial_status;
ok = fstat(descriptor, &initial_status) == 0 &&
activation_posix_intermediate_secure(&initial_status);
}
while (ok && *cursor) {
char *component = cursor;
while (*cursor && *cursor != '/') {
cursor++;
}
char saved = *cursor;
*cursor = '\0';
if (strcmp(component, ".") == 0) {
/* Harmless explicit current-directory component. */
} else if (strcmp(component, "..") == 0 || !component[0]) {
ok = false;
} else {
int next = openat(descriptor, component, flags);
struct stat next_status;
bool next_ok = next >= 0 && fstat(next, &next_status) == 0;
char *remaining = cursor + (saved ? 1 : 0);
while (*remaining == '/') {
remaining++;
}
if (next_ok && *remaining) {
if (!activation_posix_intermediate_secure(&next_status)) {
char detail[64];
(void)snprintf(detail, sizeof(detail), "mode %04o, uid %lu",
(unsigned)(next_status.st_mode & 07777),
(unsigned long)next_status.st_uid);
g_activation_refusal_object = walk_path;
activation_note_refusal_detail("ancestor_directory_world_writable", detail);
g_activation_refusal_object = NULL;
next_ok = false;
} else if (activation_posix_intermediate_group_writable(&next_status)) {
char mode_text[16];
(void)snprintf(mode_text, sizeof(mode_text), "%04o",
(unsigned)(next_status.st_mode & 07777));
cbm_log_warn("activation.ancestor_group_writable", "path", walk_path, "mode",
mode_text);
}
}
if (next_ok) {
(void)close(descriptor);
descriptor = next;
} else {
if (next >= 0) {
(void)close(next);
}
ok = false;
}
}
*cursor = saved;
while (*cursor == '/') {
cursor++;
}
}
struct stat status;
if (ok && fstat(descriptor, &status) == 0) {
/* LEAF policy: strictly owner-private. This is the directory the binary
* is published into, so group/other write here would let another
* account replace the executable between validation and exec. Unlike
* the ancestors above, this one is refused — but it now says exactly
* which directory and which mode (#1535), instead of surfacing as a
* generic I/O failure that sent reporters hunting phantom disk errors. */
bool is_dir = S_ISDIR(status.st_mode);
bool owned = status.st_uid == geteuid();
bool private_permissions = (status.st_mode & 0022) == 0;
if (!is_dir || !owned || !private_permissions) {
char detail[64];
(void)snprintf(detail, sizeof(detail), "mode %04o, uid %lu",
(unsigned)(status.st_mode & 07777), (unsigned long)status.st_uid);
g_activation_refusal_object = directory;
activation_note_refusal_detail(!is_dir ? "install_dir_not_a_directory"
: !owned ? "install_dir_not_owned_by_you"
: "install_dir_group_or_world_writable",
detail);
g_activation_refusal_object = NULL;
ok = false;
} else if (!activation_posix_acl_empty(descriptor)) {
g_activation_refusal_object = directory;
activation_note_refusal_detail("install_dir_carries_extra_acl_entries", "posix acl");
g_activation_refusal_object = NULL;
ok = false;
}
} else {
ok = false;
}
free(walk_path);
if (!ok) {
if (descriptor >= 0) {
(void)close(descriptor);
}
return false;
}
identity_out->device = status.st_dev;
identity_out->inode = status.st_ino;
*directory_fd_out = descriptor;
return true;
}
#endif
static bool activation_directory_still_valid(const cbm_activation_transaction_t *transaction) {
#ifdef _WIN32
int ignored = 0;
activation_file_identity_t current;
return activation_directory_secure(transaction->directory_path, &ignored, &current) &&
activation_identity_equal(&current, &transaction->directory_identity);
#else
struct stat status;
if (transaction->directory_fd < 0 || fstat(transaction->directory_fd, &status) != 0 ||
!S_ISDIR(status.st_mode) || status.st_uid != geteuid() || (status.st_mode & 0022) != 0 ||
!activation_posix_acl_empty(transaction->directory_fd)) {
return false;
}
activation_file_identity_t current = {
.device = status.st_dev,
.inode = status.st_ino,
};
if (!activation_identity_equal(&current, &transaction->directory_identity)) {
return false;
}
int path_fd = -1;
activation_file_identity_t path_identity;
bool path_same =
activation_directory_secure(transaction->directory_path, &path_fd, &path_identity) &&
activation_identity_equal(&path_identity, &current);
if (path_fd >= 0) {
(void)close(path_fd);
}
return path_same;
#endif
}
static bool activation_native_close(activation_native_file_t file) {
#ifdef _WIN32
return file != INVALID_HANDLE_VALUE && CloseHandle(file) != 0;
#else
return file >= 0 && close(file) == 0;
#endif
}
static bool activation_native_sync(activation_native_file_t file) {
#ifdef _WIN32
return FlushFileBuffers(file) != 0;
#else
int result;
do {
result = fsync(file);
} while (result != 0 && errno == EINTR);
return result == 0 || errno == EINVAL || errno == ENOTSUP || errno == EROFS;
#endif
}
static bool activation_native_write_all(activation_native_file_t file, const void *data,
size_t length) {
const unsigned char *cursor = data;
while (length > 0) {
#ifdef _WIN32
DWORD chunk = length > (size_t)UINT32_MAX ? UINT32_MAX : (DWORD)length;
DWORD written = 0;
if (!WriteFile(file, cursor, chunk, &written, NULL) || written == 0) {
return false;
}
size_t count = (size_t)written;
#else
ssize_t result;
do {
result = write(file, cursor, length);
} while (result < 0 && errno == EINTR);
if (result <= 0) {
return false;
}
size_t count = (size_t)result;
#endif
cursor += count;
length -= count;
}
return true;
}
typedef enum {
ACTIVATION_CREATE_OK = 0,
ACTIVATION_CREATE_EXISTS = 1,
ACTIVATION_CREATE_ERROR = 2,
} activation_create_status_t;
static activation_create_status_t activation_private_file_create(
const cbm_activation_transaction_t *transaction, const char *path, const char *name,
activation_native_file_t *file_out, activation_file_identity_t *identity_out) {
*file_out = ACTIVATION_INVALID_FILE;
if (!activation_directory_still_valid(transaction)) {
return ACTIVATION_CREATE_ERROR;
}
#ifdef _WIN32
wchar_t *wide = activation_utf8_to_wide(path);
activation_windows_security_t security;
if (!wide || !activation_windows_security_init(&security)) {
free(wide);
return ACTIVATION_CREATE_ERROR;
}
g_activation_refusal_object = path;
SetLastError(ERROR_SUCCESS);
HANDLE file = CreateFileW(wide, GENERIC_READ | GENERIC_WRITE | READ_CONTROL | DELETE,
FILE_SHARE_READ, &security.attributes, CREATE_NEW,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OPEN_REPARSE_POINT, NULL);
DWORD error = file == INVALID_HANDLE_VALUE ? GetLastError() : ERROR_SUCCESS;
free(wide);
bool valid = file != INVALID_HANDLE_VALUE &&
SetHandleInformation(file, HANDLE_FLAG_INHERIT, 0) != 0 &&
activation_windows_identity(file, identity_out, true) &&
activation_windows_owner_is_current(file) && activation_windows_acl_secure(file);
activation_windows_security_destroy(&security);
if (!valid) {
if (file == INVALID_HANDLE_VALUE && error != ERROR_FILE_EXISTS &&
error != ERROR_ALREADY_EXISTS) {
activation_note_refusal("staged-file-create", error);
}
g_activation_refusal_object = NULL;
if (file != INVALID_HANDLE_VALUE) {
(void)CloseHandle(file);
}
return error == ERROR_FILE_EXISTS || error == ERROR_ALREADY_EXISTS
? ACTIVATION_CREATE_EXISTS
: ACTIVATION_CREATE_ERROR;
}
g_activation_refusal_object = NULL;
*file_out = file;
return ACTIVATION_CREATE_OK;
#else
int flags = O_RDWR | O_CREAT | O_EXCL | O_CLOEXEC;
#ifdef O_NOFOLLOW
flags |= O_NOFOLLOW;
#endif
int file = openat(transaction->directory_fd, name, flags, 0700);
int open_error = errno;
struct stat status;
bool valid = file >= 0 && fchmod(file, 0700) == 0 && fstat(file, &status) == 0 &&
S_ISREG(status.st_mode) && status.st_uid == geteuid() && status.st_nlink == 1 &&
(status.st_mode & 0777) == 0700 && activation_posix_acl_empty(file);
if (!valid) {
if (file >= 0) {
(void)close(file);
}
return file < 0 && open_error == EEXIST ? ACTIVATION_CREATE_EXISTS
: ACTIVATION_CREATE_ERROR;
}
identity_out->device = status.st_dev;
identity_out->inode = status.st_ino;
*file_out = file;
return ACTIVATION_CREATE_OK;
#endif
}
static cbm_activation_transaction_status_t activation_create_unique(
const cbm_activation_transaction_t *transaction, const char *tag, char **path_out,
char **name_out, activation_native_file_t *file_out, activation_file_identity_t *identity_out) {
*path_out = NULL;
*name_out = NULL;
*file_out = ACTIVATION_INVALID_FILE;
for (unsigned int attempt = 0; attempt < 1024U; attempt++) {
char *candidate = activation_unique_path(transaction->directory_path, tag);
if (!candidate) {
return CBM_ACTIVATION_TRANSACTION_NO_MEMORY;
}
char *name = activation_path_name_copy(candidate);
if (!name) {
free(candidate);
return CBM_ACTIVATION_TRANSACTION_NO_MEMORY;
}
activation_create_status_t created =
activation_private_file_create(transaction, candidate, name, file_out, identity_out);
if (created == ACTIVATION_CREATE_OK) {
*path_out = candidate;
*name_out = name;
return CBM_ACTIVATION_TRANSACTION_OK;
}
free(candidate);
free(name);
if (created != ACTIVATION_CREATE_EXISTS) {
return CBM_ACTIVATION_TRANSACTION_IO;
}
}
return CBM_ACTIVATION_TRANSACTION_IO;
}
#ifdef _WIN32
static bool activation_external_snapshot_with_owner(const char *path, bool require_current_owner,
bool *exists_out,
activation_file_identity_t *identity_out) {
*exists_out = false;
wchar_t *wide = activation_utf8_to_wide(path);
if (!wide) {
return false;
}
DWORD attributes = GetFileAttributesW(wide);
if (attributes == INVALID_FILE_ATTRIBUTES) {
DWORD error = GetLastError();
free(wide);
return error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND;
}
if ((attributes & (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT)) != 0) {
free(wide);
return false;
}
HANDLE handle = CreateFileW(wide, FILE_READ_ATTRIBUTES | READ_CONTROL,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL,
OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT, NULL);
free(wide);
bool valid = handle != INVALID_HANDLE_VALUE &&
activation_windows_identity(handle, identity_out, true) &&
(require_current_owner ? activation_windows_owner_is_current(handle)
: activation_windows_owner_is_trusted(handle)) &&
activation_windows_acl_secure(handle);
if (handle != INVALID_HANDLE_VALUE) {
(void)CloseHandle(handle);
}
if (!valid) {
return false;
}
*exists_out = true;
return true;
}
#endif
#ifndef _WIN32
static bool activation_posix_entry_snapshot_with_links(
const cbm_activation_transaction_t *transaction, const char *name, nlink_t required_links,
bool *exists_out, activation_file_identity_t *identity_out) {
*exists_out = false;
if (!activation_directory_still_valid(transaction)) {
return false;
}
struct stat before;
if (fstatat(transaction->directory_fd, name, &before, AT_SYMLINK_NOFOLLOW) != 0) {
return errno == ENOENT;
}
if (!S_ISREG(before.st_mode) || before.st_uid != geteuid() ||
before.st_nlink != required_links || (before.st_mode & 0022) != 0) {
return false;
}
int flags = O_RDONLY | O_CLOEXEC;
#ifdef O_NOFOLLOW
flags |= O_NOFOLLOW;
#endif
int file = openat(transaction->directory_fd, name, flags);
struct stat opened;
struct stat after;
bool valid = file >= 0 && fstat(file, &opened) == 0 && S_ISREG(opened.st_mode) &&
opened.st_uid == geteuid() && opened.st_nlink == required_links &&
(opened.st_mode & 0022) == 0 && opened.st_dev == before.st_dev &&
opened.st_ino == before.st_ino && activation_posix_acl_empty(file) &&
fstatat(transaction->directory_fd, name, &after, AT_SYMLINK_NOFOLLOW) == 0 &&
S_ISREG(after.st_mode) && after.st_uid == geteuid() &&
after.st_nlink == required_links && (after.st_mode & 0022) == 0 &&
after.st_dev == opened.st_dev && after.st_ino == opened.st_ino;
bool closed = file >= 0 && close(file) == 0;
if (!valid || !closed) {
return false;
}
identity_out->device = opened.st_dev;
identity_out->inode = opened.st_ino;
*exists_out = true;
return true;
}
#endif
static bool activation_entry_snapshot(const cbm_activation_transaction_t *transaction,
const char *path, const char *name, bool *exists_out,
activation_file_identity_t *identity_out) {
#ifdef _WIN32
return activation_directory_still_valid(transaction) &&
activation_external_snapshot_with_owner(path, true, exists_out, identity_out);
#else
(void)path;
return activation_posix_entry_snapshot_with_links(transaction, name, (nlink_t)1, exists_out,
identity_out);
#endif
}
static bool activation_path_matches(const cbm_activation_transaction_t *transaction,
const char *path, const char *name,
const activation_file_identity_t *expected, bool *exists_out) {
activation_file_identity_t actual;
bool exists = false;
if (!activation_entry_snapshot(transaction, path, name, &exists, &actual)) {
return false;
}
*exists_out = exists;
return !exists || activation_identity_equal(&actual, expected);
}
static bool activation_sync_directory(const cbm_activation_transaction_t *transaction) {
#ifdef _WIN32
/* MoveFileExW(MOVEFILE_WRITE_THROUGH) is the strongest portable
* directory-entry durability primitive available here. */
(void)transaction;
return true;
#else
int result;
do {
result = fsync(transaction->directory_fd);
} while (result != 0 && errno == EINTR);
return result == 0 || errno == EINVAL || errno == ENOTSUP || errno == EROFS;
#endif
}
#ifdef _WIN32
/* Renaming is how this transaction both publishes and retires -- and on Windows
* it is the ONLY mutation permitted on a running image, which is what lets a
* single binary replace or remove itself at all. It can still lose to a handle
* that is about to go away: an antivirus scan of a just-written executable, or
* a child process the OS has not finished reaping. Both surface as a sharing or
* access violation and both clear within moments.
*
* install.ps1 already retries this exact operation for this exact reason. The C
* path giving up on the first attempt is what let `uninstall` intermittently
* abandon a live installation with "cleanup may remain" -- observed on one
* runner while an identical job on the same image passed.
*
* Bounded on purpose: a file that is genuinely held must still fail, and fail
* closed, rather than spin. */
#define ACTIVATION_RENAME_ATTEMPTS 10U
#define ACTIVATION_RENAME_RETRY_MS 500U
static unsigned int activation_rename_failures_for_test;
static bool activation_rename_error_is_transient(DWORD error) {
return error == ERROR_SHARING_VIOLATION || error == ERROR_ACCESS_DENIED ||
error == ERROR_LOCK_VIOLATION;
}
#endif
void cbm_activation_transaction_rename_failures_set_for_test(unsigned int count) {
#ifdef _WIN32
activation_rename_failures_for_test = count;
#else
(void)count;
#endif
}
static bool activation_rename(const cbm_activation_transaction_t *transaction, const char *source,
const char *source_name, const char *destination,
const char *destination_name, bool replace_destination) {
if (!activation_directory_still_valid(transaction)) {
return false;
}
#ifdef _WIN32
wchar_t *wide_source = activation_utf8_to_wide(source);
wchar_t *wide_destination = activation_utf8_to_wide(destination);
DWORD flags = MOVEFILE_WRITE_THROUGH | (replace_destination ? MOVEFILE_REPLACE_EXISTING : 0);
bool moved = false;
for (unsigned int attempt = 0;
wide_source && wide_destination && !moved && attempt < ACTIVATION_RENAME_ATTEMPTS;
attempt++) {
if (activation_rename_failures_for_test > 0) {
activation_rename_failures_for_test--;
} else if (MoveFileExW(wide_source, wide_destination, flags) != 0) {
moved = true;
break;
} else if (!activation_rename_error_is_transient(GetLastError())) {
break;
}
if (attempt + 1U < ACTIVATION_RENAME_ATTEMPTS) {
Sleep(ACTIVATION_RENAME_RETRY_MS);
}
}
bool ok = moved && activation_directory_still_valid(transaction);
free(wide_source);
free(wide_destination);
return ok;
#else
(void)source;
(void)destination;
(void)replace_destination;
int result;
do {
result = renameat(transaction->directory_fd, source_name, transaction->directory_fd,
destination_name);
} while (result != 0 && errno == EINTR);
return result == 0;
#endif
}
typedef enum {
ACTIVATION_UNLINK_OK = 0,
ACTIVATION_UNLINK_DEFERRED = 1,
ACTIVATION_UNLINK_ERROR = 2,
} activation_unlink_status_t;
static activation_unlink_status_t activation_unlink_expected(
const cbm_activation_transaction_t *transaction, const char *path, const char *name,
const activation_file_identity_t *expected, bool allow_windows_deferred) {
bool exists = false;
if (!activation_path_matches(transaction, path, name, expected, &exists)) {
return ACTIVATION_UNLINK_ERROR;
}
if (!exists) {
return ACTIVATION_UNLINK_OK;
}
#ifdef _WIN32
wchar_t *wide = activation_utf8_to_wide(path);
if (!wide) {
return ACTIVATION_UNLINK_ERROR;
}
if (DeleteFileW(wide)) {
free(wide);
return ACTIVATION_UNLINK_OK;
}
DWORD error = GetLastError();
bool deferred = allow_windows_deferred &&
(error == ERROR_ACCESS_DENIED || error == ERROR_SHARING_VIOLATION ||
error == ERROR_LOCK_VIOLATION) &&
MoveFileExW(wide, NULL, MOVEFILE_DELAY_UNTIL_REBOOT) != 0;
free(wide);
return deferred ? ACTIVATION_UNLINK_DEFERRED : ACTIVATION_UNLINK_ERROR;
#else
(void)allow_windows_deferred;
int result;
do {
result = unlinkat(transaction->directory_fd, name, 0);
} while (result != 0 && errno == EINTR);
return result == 0 || errno == ENOENT ? ACTIVATION_UNLINK_OK : ACTIVATION_UNLINK_ERROR;
#endif
}
static void activation_transaction_destroy(cbm_activation_transaction_t *transaction) {
if (!transaction) {
return;
}
#ifndef _WIN32
if (transaction->directory_fd >= 0) {
(void)close(transaction->directory_fd);
}
#endif
free(transaction->target_path);
free(transaction->directory_path);
free(transaction->target_name);
free(transaction->staged_path);
free(transaction->staged_name);
free(transaction->backup_path);
free(transaction->backup_name);
free(transaction);
}
static cbm_activation_transaction_status_t activation_discard_staged_assets(
cbm_activation_transaction_t *transaction) {
bool ok = true;
if (transaction->staged_exists) {
activation_unlink_status_t removed = activation_unlink_expected(
transaction, transaction->staged_path, transaction->staged_name,
&transaction->staged_identity, false);
ok = removed == ACTIVATION_UNLINK_OK && ok;
if (removed == ACTIVATION_UNLINK_OK) {
transaction->staged_exists = false;
}
}
if (transaction->backup_exists && !transaction->backup_contains_target) {
activation_unlink_status_t removed = activation_unlink_expected(
transaction, transaction->backup_path, transaction->backup_name,
&transaction->backup_identity, false);
ok = removed == ACTIVATION_UNLINK_OK && ok;
if (removed == ACTIVATION_UNLINK_OK) {
transaction->backup_exists = false;
}
}
ok = activation_sync_directory(transaction) && ok;
return ok ? CBM_ACTIVATION_TRANSACTION_OK : CBM_ACTIVATION_TRANSACTION_IO;
}
static cbm_activation_transaction_status_t activation_transaction_prepare(
const char *target_path, activation_action_t action,
cbm_activation_transaction_t **transaction_out) {
*transaction_out = NULL;
activation_refusal_clear();
cbm_activation_transaction_t *transaction = calloc(1, sizeof(*transaction));
if (!transaction) {
return CBM_ACTIVATION_TRANSACTION_NO_MEMORY;
}
#ifndef _WIN32
transaction->directory_fd = -1;
#endif
transaction->action = action;
transaction->state = ACTIVATION_STAGED;
transaction->target_path = activation_string_copy(target_path);
if (!transaction->target_path) {
activation_transaction_destroy(transaction);
return CBM_ACTIVATION_TRANSACTION_NO_MEMORY;
}
if (!activation_target_parts(target_path, &transaction->directory_path,
&transaction->target_name)) {
activation_transaction_destroy(transaction);
return CBM_ACTIVATION_TRANSACTION_INVALID_ARGUMENT;
}
#ifdef _WIN32
int ignored = 0;
if (!activation_directory_secure(transaction->directory_path, &ignored,
&transaction->directory_identity)) {
#else
if (!activation_directory_secure(transaction->directory_path, &transaction->directory_fd,
&transaction->directory_identity)) {
#endif
activation_transaction_destroy(transaction);
return CBM_ACTIVATION_TRANSACTION_IO;
}
if (!activation_entry_snapshot(transaction, transaction->target_path, transaction->target_name,
&transaction->target_existed, &transaction->target_identity)) {
activation_note_refusal("target-entry-snapshot", 0UL);
activation_transaction_destroy(transaction);
return CBM_ACTIVATION_TRANSACTION_IO;
}
if (transaction->target_existed) {
activation_native_file_t reservation = ACTIVATION_INVALID_FILE;
cbm_activation_transaction_status_t status = activation_create_unique(
transaction, "backup", &transaction->backup_path, &transaction->backup_name,
&reservation, &transaction->backup_identity);
if (status != CBM_ACTIVATION_TRANSACTION_OK) {
activation_transaction_destroy(transaction);
return status;
}
transaction->backup_exists = true;
bool durable = activation_native_sync(reservation);
bool closed = activation_native_close(reservation);
if (!durable || !closed || !activation_sync_directory(transaction)) {
(void)activation_discard_staged_assets(transaction);
activation_transaction_destroy(transaction);
return CBM_ACTIVATION_TRANSACTION_IO;
}
}
*transaction_out = transaction;
return CBM_ACTIVATION_TRANSACTION_OK;
}
static void activation_failed_stage_cleanup(cbm_activation_transaction_t *transaction) {
if (transaction) {
(void)activation_discard_staged_assets(transaction);
activation_transaction_destroy(transaction);
}
}
cbm_activation_transaction_status_t cbm_activation_transaction_stage_bytes(
const char *target_path, const void *candidate, size_t candidate_size,
cbm_activation_transaction_t **transaction_out) {
if (transaction_out) {
*transaction_out = NULL;
}
if (!target_path || !candidate || candidate_size == 0 || !transaction_out) {
return CBM_ACTIVATION_TRANSACTION_INVALID_ARGUMENT;
}
cbm_activation_transaction_t *transaction = NULL;
cbm_activation_transaction_status_t status =
activation_transaction_prepare(target_path, ACTIVATION_REPLACE, &transaction);
if (status != CBM_ACTIVATION_TRANSACTION_OK) {
return status;
}
activation_native_file_t staged = ACTIVATION_INVALID_FILE;
status =
activation_create_unique(transaction, "stage", &transaction->staged_path,
&transaction->staged_name, &staged, &transaction->staged_identity);
if (status != CBM_ACTIVATION_TRANSACTION_OK) {
activation_failed_stage_cleanup(transaction);
return status;
}
transaction->staged_exists = true;
bool written = activation_native_write_all(staged, candidate, candidate_size);
bool durable = written && activation_native_sync(staged);
bool closed = activation_native_close(staged);
if (!written || !durable || !closed || !activation_sync_directory(transaction)) {
activation_failed_stage_cleanup(transaction);
return CBM_ACTIVATION_TRANSACTION_IO;
}
*transaction_out = transaction;
return CBM_ACTIVATION_TRANSACTION_OK;
}
static bool activation_source_open(const char *path, activation_native_file_t *file_out) {
*file_out = ACTIVATION_INVALID_FILE;
char *directory = NULL;
char *name = NULL;
if (!activation_target_parts(path, &directory, &name)) {
return false;
}
#ifdef _WIN32
activation_file_identity_t directory_identity;
activation_file_identity_t expected;
bool exists = false;
bool src_dir_ok = activation_source_directory_secure(directory, &directory_identity);
/* The source (a downloaded release bundle) follows the machine's
* default-owner policy; its bytes are pinned by identity recheck plus the
* staged copy's build-fingerprint validation, so a trusted owner
* (user/Administrators/SYSTEM) is sufficient here. Targets keep the
* exact-current-owner rule. */
bool snapshot_ok =
src_dir_ok && activation_external_snapshot_with_owner(path, false, &exists, &expected);
if (!src_dir_ok || !snapshot_ok || !exists) {
free(directory);
free(name);
return false;
}
wchar_t *wide = activation_utf8_to_wide(path);
if (!wide) {
free(directory);
free(name);
return false;
}
HANDLE file =
CreateFileW(wide, GENERIC_READ | FILE_READ_ATTRIBUTES | READ_CONTROL,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING,
FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_SEQUENTIAL_SCAN, NULL);
free(wide);
activation_file_identity_t actual;
activation_file_identity_t directory_now;
bool valid = file != INVALID_HANDLE_VALUE && activation_windows_identity(file, &actual, true) &&
activation_windows_owner_is_trusted(file) && activation_windows_acl_secure(file) &&
activation_identity_equal(&actual, &expected) &&
activation_source_directory_secure(directory, &directory_now) &&
activation_identity_equal(&directory_now, &directory_identity);
if (!valid) {
if (file != INVALID_HANDLE_VALUE) {
(void)CloseHandle(file);
}
free(directory);
free(name);
return false;
}
#else
int directory_fd = -1;
activation_file_identity_t directory_identity;
if (!activation_directory_secure(directory, &directory_fd, &directory_identity)) {
free(directory);
free(name);
return false;
}
struct stat before;
bool before_valid = fstatat(directory_fd, name, &before, AT_SYMLINK_NOFOLLOW) == 0 &&
S_ISREG(before.st_mode) && before.st_uid == geteuid() &&
before.st_nlink == 1 && (before.st_mode & 0022) == 0;
int flags = O_RDONLY | O_CLOEXEC;
#ifdef O_NOFOLLOW
flags |= O_NOFOLLOW;
#endif
int file = before_valid ? openat(directory_fd, name, flags) : -1;
struct stat information;
bool valid = file >= 0 && fstat(file, &information) == 0 && S_ISREG(information.st_mode) &&
information.st_uid == geteuid() && information.st_nlink == 1 &&
(information.st_mode & 0022) == 0 && activation_posix_acl_empty(file) &&
information.st_dev == before.st_dev && information.st_ino == before.st_ino;
(void)close(directory_fd);
if (!valid) {
if (file >= 0) {
(void)close(file);
}
free(directory);
free(name);
return false;
}
#endif
free(directory);
free(name);
*file_out = file;
return true;
}
static bool activation_native_read(activation_native_file_t file, void *buffer, size_t capacity,
size_t *read_out) {
#ifdef _WIN32
DWORD amount = 0;
DWORD request = capacity > (size_t)UINT32_MAX ? UINT32_MAX : (DWORD)capacity;
if (!ReadFile(file, buffer, request, &amount, NULL)) {
return false;
}
*read_out = (size_t)amount;
return true;
#else
ssize_t result;
do {
result = read(file, buffer, capacity);
} while (result < 0 && errno == EINTR);
if (result < 0) {
return false;
}
*read_out = (size_t)result;
return true;
#endif
}
cbm_activation_transaction_status_t cbm_activation_transaction_stage_file(
const char *target_path, const char *candidate_path,
cbm_activation_transaction_t **transaction_out) {
if (transaction_out) {
*transaction_out = NULL;
}
if (!target_path || !candidate_path || !candidate_path[0] || !transaction_out) {
return CBM_ACTIVATION_TRANSACTION_INVALID_ARGUMENT;
}
cbm_activation_transaction_t *transaction = NULL;
cbm_activation_transaction_status_t status =
activation_transaction_prepare(target_path, ACTIVATION_REPLACE, &transaction);
if (status != CBM_ACTIVATION_TRANSACTION_OK) {
return status;
}
activation_native_file_t source = ACTIVATION_INVALID_FILE;
if (!activation_source_open(candidate_path, &source)) {
activation_failed_stage_cleanup(transaction);
return CBM_ACTIVATION_TRANSACTION_IO;
}
activation_native_file_t staged = ACTIVATION_INVALID_FILE;
status =
activation_create_unique(transaction, "stage", &transaction->staged_path,
&transaction->staged_name, &staged, &transaction->staged_identity);
if (status != CBM_ACTIVATION_TRANSACTION_OK) {
(void)activation_native_close(source);
activation_failed_stage_cleanup(transaction);
return status;
}
transaction->staged_exists = true;
unsigned char buffer[64U * 1024U];
size_t total = 0;
bool copied = true;
for (;;) {
size_t amount = 0;
if (!activation_native_read(source, buffer, sizeof(buffer), &amount)) {
copied = false;
break;
}
if (amount == 0) {
break;
}
if (SIZE_MAX - total < amount || !activation_native_write_all(staged, buffer, amount)) {
copied = false;
break;
}
total += amount;
}
bool durable = copied && total > 0 && activation_native_sync(staged);
bool source_closed = activation_native_close(source);
bool staged_closed = activation_native_close(staged);
if (!copied || total == 0 || !durable || !source_closed || !staged_closed ||
!activation_sync_directory(transaction)) {
activation_failed_stage_cleanup(transaction);
return CBM_ACTIVATION_TRANSACTION_IO;
}
*transaction_out = transaction;
return CBM_ACTIVATION_TRANSACTION_OK;
}
cbm_activation_transaction_status_t cbm_activation_transaction_stage_removal(
const char *target_path, cbm_activation_transaction_t **transaction_out) {
if (transaction_out) {
*transaction_out = NULL;
}
if (!target_path || !transaction_out) {
return CBM_ACTIVATION_TRANSACTION_INVALID_ARGUMENT;
}
return activation_transaction_prepare(target_path, ACTIVATION_REMOVE, transaction_out);
}
#ifdef _WIN32
static bool activation_windows_copy_target_to_backup(cbm_activation_transaction_t *transaction) {
if (!activation_directory_still_valid(transaction)) {
return false;
}
wchar_t *target_path = activation_utf8_to_wide(transaction->target_path);
wchar_t *backup_path = activation_utf8_to_wide(transaction->backup_path);
if (!target_path || !backup_path) {
free(target_path);
free(backup_path);
return false;
}
HANDLE target =
CreateFileW(target_path, GENERIC_READ | FILE_READ_ATTRIBUTES | READ_CONTROL,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING,
FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_SEQUENTIAL_SCAN, NULL);
HANDLE backup =
CreateFileW(backup_path, GENERIC_READ | GENERIC_WRITE | FILE_READ_ATTRIBUTES | READ_CONTROL,
FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT, NULL);
free(target_path);
free(backup_path);
activation_file_identity_t target_identity;
activation_file_identity_t backup_identity;
LARGE_INTEGER beginning = {.QuadPart = 0};
bool valid = target != INVALID_HANDLE_VALUE && backup != INVALID_HANDLE_VALUE &&
activation_windows_identity(target, &target_identity, true) &&
activation_windows_identity(backup, &backup_identity, true) &&
activation_windows_owner_is_current(target) &&
activation_windows_owner_is_current(backup) &&
activation_windows_acl_secure(target) && activation_windows_acl_secure(backup) &&
activation_identity_equal(&target_identity, &transaction->target_identity) &&
activation_identity_equal(&backup_identity, &transaction->backup_identity) &&
SetFilePointerEx(backup, beginning, NULL, FILE_BEGIN) != 0 &&
SetEndOfFile(backup) != 0;
unsigned char buffer[64U * 1024U];
while (valid) {
DWORD amount = 0;
valid = ReadFile(target, buffer, (DWORD)sizeof(buffer), &amount, NULL) != 0;
if (!valid || amount == 0) {
break;
}
valid = activation_native_write_all(backup, buffer, (size_t)amount);
}
valid = valid && activation_native_sync(backup);
bool target_closed = target != INVALID_HANDLE_VALUE && CloseHandle(target) != 0;
bool backup_closed = backup != INVALID_HANDLE_VALUE && CloseHandle(backup) != 0;
return valid && target_closed && backup_closed && activation_directory_still_valid(transaction);
}
#endif
typedef enum {
ACTIVATION_PUBLISH_OK = 0,
ACTIVATION_PUBLISH_UNCHANGED_ERROR = 1,
ACTIVATION_PUBLISH_CHANGED_ERROR = 2,
} activation_publish_status_t;
#if defined(__APPLE__) || (defined(__linux__) && defined(SYS_renameat2))
static bool activation_noreplace_primitive_unavailable(int error) {
bool unavailable = error == ENOSYS || error == EINVAL;
#ifdef ENOTSUP
unavailable = unavailable || error == ENOTSUP;
#endif
#ifdef EOPNOTSUPP
unavailable = unavailable || error == EOPNOTSUPP;
#endif
return unavailable;
}
#endif
#ifndef _WIN32
/* Portable last resort for platforms/filesystems without a no-replace rename.
* linkat() atomically claims an absent destination. Until the staging link is
* removed both names deliberately retain the same verified inode, and the
* transaction's staged_exists flag records that partial publication so
* rollback can remove only the target link. */
static activation_publish_status_t activation_publish_absent_link_fallback(
cbm_activation_transaction_t *transaction) {
if (!activation_directory_still_valid(transaction)) {
return ACTIVATION_PUBLISH_UNCHANGED_ERROR;
}
int linked;
do {
linked = linkat(transaction->directory_fd, transaction->staged_name,
transaction->directory_fd, transaction->target_name, 0);
} while (linked != 0 && errno == EINTR);
if (linked != 0) {
return ACTIVATION_PUBLISH_UNCHANGED_ERROR;
}
activation_file_identity_t staged_identity;
activation_file_identity_t target_identity;
bool staged_exists = false;
bool target_exists = false;
bool linked_pair =
activation_posix_entry_snapshot_with_links(transaction, transaction->staged_name,
(nlink_t)2, &staged_exists, &staged_identity) &&
staged_exists &&
activation_identity_equal(&staged_identity, &transaction->staged_identity) &&
activation_posix_entry_snapshot_with_links(transaction, transaction->target_name,
(nlink_t)2, &target_exists, &target_identity) &&
target_exists && activation_identity_equal(&target_identity, &transaction->staged_identity);
if (!linked_pair) {
return ACTIVATION_PUBLISH_CHANGED_ERROR;
}
int removed;
do {
removed = unlinkat(transaction->directory_fd, transaction->staged_name, 0);
} while (removed != 0 && errno == EINTR);
if (removed != 0 && errno != ENOENT) {
return ACTIVATION_PUBLISH_CHANGED_ERROR;
}
transaction->staged_exists = false;
activation_file_identity_t published_identity;
bool published_exists = false;
if (!activation_posix_entry_snapshot_with_links(transaction, transaction->target_name,
(nlink_t)1, &published_exists,
&published_identity) ||
!published_exists ||
!activation_identity_equal(&published_identity, &transaction->staged_identity)) {
return ACTIVATION_PUBLISH_CHANGED_ERROR;
}
return ACTIVATION_PUBLISH_OK;
}
#endif
#if defined(_WIN32) || defined(__APPLE__) || (defined(__linux__) && defined(SYS_renameat2))
static activation_publish_status_t activation_finish_absent_publish(
cbm_activation_transaction_t *transaction) {
transaction->staged_exists = false;
bool target_exists = false;
return activation_path_matches(transaction, transaction->target_path, transaction->target_name,
&transaction->staged_identity, &target_exists) &&
target_exists
? ACTIVATION_PUBLISH_OK
: ACTIVATION_PUBLISH_CHANGED_ERROR;
}
#endif
static activation_publish_status_t activation_publish_absent_replacement(
cbm_activation_transaction_t *transaction) {
if (!activation_directory_still_valid(transaction)) {
return ACTIVATION_PUBLISH_UNCHANGED_ERROR;
}
bool staged_exists = false;
if (!activation_path_matches(transaction, transaction->staged_path, transaction->staged_name,
&transaction->staged_identity, &staged_exists) ||
!staged_exists) {
return ACTIVATION_PUBLISH_UNCHANGED_ERROR;
}
#ifdef _WIN32
wchar_t *source = activation_utf8_to_wide(transaction->staged_path);
wchar_t *destination = activation_utf8_to_wide(transaction->target_path);
bool published =
source && destination && MoveFileExW(source, destination, MOVEFILE_WRITE_THROUGH) != 0;
free(source);
free(destination);
if (!published) {
return ACTIVATION_PUBLISH_UNCHANGED_ERROR;
}
return activation_finish_absent_publish(transaction);
#elif defined(__APPLE__)
int renamed;
do {
renamed = renameatx_np(transaction->directory_fd, transaction->staged_name,
transaction->directory_fd, transaction->target_name, RENAME_EXCL);
} while (renamed != 0 && errno == EINTR);
if (renamed == 0) {
return activation_finish_absent_publish(transaction);
}
if (!activation_noreplace_primitive_unavailable(errno)) {
return ACTIVATION_PUBLISH_UNCHANGED_ERROR;
}
return activation_publish_absent_link_fallback(transaction);
#elif defined(__linux__) && defined(SYS_renameat2)
const unsigned int rename_noreplace = 1U;
long renamed;
do {
renamed = syscall(SYS_renameat2, transaction->directory_fd, transaction->staged_name,
transaction->directory_fd, transaction->target_name, rename_noreplace);
} while (renamed != 0 && errno == EINTR);
if (renamed == 0) {
return activation_finish_absent_publish(transaction);
}
if (!activation_noreplace_primitive_unavailable(errno)) {
return ACTIVATION_PUBLISH_UNCHANGED_ERROR;
}
return activation_publish_absent_link_fallback(transaction);
#else
return activation_publish_absent_link_fallback(transaction);
#endif
}
#ifndef _WIN32
static bool activation_linked_backup_pair_valid(const cbm_activation_transaction_t *transaction) {
activation_file_identity_t target_identity;
activation_file_identity_t backup_identity;
bool target_exists = false;
bool backup_exists = false;
return activation_posix_entry_snapshot_with_links(transaction, transaction->target_name,
(nlink_t)2, &target_exists,
&target_identity) &&
target_exists &&
activation_identity_equal(&target_identity, &transaction->target_identity) &&
activation_posix_entry_snapshot_with_links(transaction, transaction->backup_name,
(nlink_t)2, &backup_exists,
&backup_identity) &&
backup_exists &&
activation_identity_equal(&backup_identity, &transaction->target_identity);
}
static bool activation_remove_linked_backup(cbm_activation_transaction_t *transaction) {
if (!activation_linked_backup_pair_valid(transaction)) {
return false;
}
int result;
do {
result = unlinkat(transaction->directory_fd, transaction->backup_name, 0);
} while (result != 0 && errno == EINTR);
if (result == 0) {
transaction->backup_exists = false;
transaction->backup_contains_target = false;
}
return result == 0;
}
#endif
/* Publish over an existing target without a disappearance window. POSIX
* retains the old inode through a same-directory hard link before renameat.
* Windows copies the verified old bytes into the already-private backup, then
* MoveFileExW atomically replaces the target with the private staged file. */
static activation_publish_status_t activation_publish_existing_replacement(
cbm_activation_transaction_t *transaction) {
#ifdef _WIN32
transaction->backup_contains_target = false;
if (!activation_windows_copy_target_to_backup(transaction)) {
return ACTIVATION_PUBLISH_UNCHANGED_ERROR;
}
if (!activation_rename(transaction, transaction->staged_path, transaction->staged_name,
transaction->target_path, transaction->target_name, true)) {
return ACTIVATION_PUBLISH_UNCHANGED_ERROR;
}
transaction->staged_exists = false;
transaction->backup_contains_target = true;
bool target_exists = false;
if (!activation_path_matches(transaction, transaction->target_path, transaction->target_name,
&transaction->staged_identity, &target_exists) ||
!target_exists) {
return ACTIVATION_PUBLISH_CHANGED_ERROR;
}
#else
activation_unlink_status_t reservation_removed =
activation_unlink_expected(transaction, transaction->backup_path, transaction->backup_name,
&transaction->backup_identity, false);
if (reservation_removed != ACTIVATION_UNLINK_OK) {
return ACTIVATION_PUBLISH_UNCHANGED_ERROR;
}
transaction->backup_exists = false;
if (!activation_directory_still_valid(transaction) ||
linkat(transaction->directory_fd, transaction->target_name, transaction->directory_fd,
transaction->backup_name, 0) != 0) {
return ACTIVATION_PUBLISH_UNCHANGED_ERROR;
}
transaction->backup_exists = true;
transaction->backup_contains_target = true;
transaction->backup_identity = transaction->target_identity;
bool staged_exists = false;
if (!activation_linked_backup_pair_valid(transaction) ||
!activation_path_matches(transaction, transaction->staged_path, transaction->staged_name,
&transaction->staged_identity, &staged_exists) ||
!staged_exists) {
return activation_remove_linked_backup(transaction) ? ACTIVATION_PUBLISH_UNCHANGED_ERROR
: ACTIVATION_PUBLISH_CHANGED_ERROR;
}
if (!activation_sync_directory(transaction)) {
return activation_remove_linked_backup(transaction) ? ACTIVATION_PUBLISH_UNCHANGED_ERROR
: ACTIVATION_PUBLISH_CHANGED_ERROR;
}
if (!activation_rename(transaction, transaction->staged_path, transaction->staged_name,
transaction->target_path, transaction->target_name, true)) {
return activation_remove_linked_backup(transaction) ? ACTIVATION_PUBLISH_UNCHANGED_ERROR
: ACTIVATION_PUBLISH_CHANGED_ERROR;
}
transaction->staged_exists = false;
#endif
return ACTIVATION_PUBLISH_OK;
}
static bool activation_target_still_original(const cbm_activation_transaction_t *transaction) {
activation_file_identity_t current;
bool exists = false;
if (!activation_entry_snapshot(transaction, transaction->target_path, transaction->target_name,
&exists, &current) ||
exists != transaction->target_existed) {
return false;
}
return !exists || activation_identity_equal(&current, &transaction->target_identity);
}
static bool activation_absent_target_snapshot(const cbm_activation_transaction_t *transaction,
bool *exists_out, bool *linked_pair_out) {
*exists_out = false;
*linked_pair_out = false;
if (activation_path_matches(transaction, transaction->target_path, transaction->target_name,
&transaction->staged_identity, exists_out)) {
return true;
}
#ifndef _WIN32
if (!transaction->staged_exists) {
return false;
}
activation_file_identity_t staged_identity;
activation_file_identity_t target_identity;
bool staged_exists = false;
bool target_exists = false;
if (!activation_posix_entry_snapshot_with_links(transaction, transaction->target_name,
(nlink_t)2, &target_exists, &target_identity) ||
!target_exists ||
!activation_identity_equal(&target_identity, &transaction->staged_identity) ||
!activation_posix_entry_snapshot_with_links(transaction, transaction->staged_name,
(nlink_t)2, &staged_exists, &staged_identity) ||
!staged_exists ||
!activation_identity_equal(&staged_identity, &transaction->staged_identity)) {
return false;
}
*exists_out = true;
*linked_pair_out = true;
return true;
#else
return false;
#endif
}
static bool activation_remove_absent_published_target(cbm_activation_transaction_t *transaction,
bool linked_pair) {
if (!linked_pair) {
return activation_unlink_expected(transaction, transaction->target_path,
transaction->target_name, &transaction->staged_identity,
false) == ACTIVATION_UNLINK_OK;
}
#ifndef _WIN32
bool target_exists = false;
bool still_linked = false;
if (!activation_absent_target_snapshot(transaction, &target_exists, &still_linked) ||
!target_exists || !still_linked) {
return false;
}
int removed;
do {
removed = unlinkat(transaction->directory_fd, transaction->target_name, 0);
} while (removed != 0 && errno == EINTR);
if (removed != 0) {
return false;
}
activation_file_identity_t staged_identity;
bool staged_exists = false;
activation_file_identity_t absent_identity;
bool target_remains = false;
return activation_posix_entry_snapshot_with_links(transaction, transaction->staged_name,
(nlink_t)1, &staged_exists,
&staged_identity) &&
staged_exists &&
activation_identity_equal(&staged_identity, &transaction->staged_identity) &&
activation_posix_entry_snapshot_with_links(transaction, transaction->target_name,
(nlink_t)1, &target_remains,
&absent_identity) &&
!target_remains;
#else
return false;
#endif
}
static cbm_activation_transaction_status_t activation_rollback_internal(
cbm_activation_transaction_t *transaction) {
if (transaction->target_existed) {
bool backup_exists = false;
if (!transaction->backup_contains_target ||
!activation_path_matches(transaction, transaction->backup_path,
transaction->backup_name, &transaction->backup_identity,
&backup_exists) ||
!backup_exists) {
transaction->state = ACTIVATION_RECOVERY_NEEDED;
return CBM_ACTIVATION_TRANSACTION_ROLLBACK_FAILED;
}
bool target_exists = false;
activation_file_identity_t current_target;
if (!activation_entry_snapshot(transaction, transaction->target_path,
transaction->target_name, &target_exists, &current_target)) {
transaction->state = ACTIVATION_RECOVERY_NEEDED;
return CBM_ACTIVATION_TRANSACTION_ROLLBACK_FAILED;
}
if (target_exists &&
(transaction->action != ACTIVATION_REPLACE ||
!activation_identity_equal(&current_target, &transaction->staged_identity))) {
transaction->state = ACTIVATION_RECOVERY_NEEDED;
return CBM_ACTIVATION_TRANSACTION_ROLLBACK_FAILED;
}
if (!activation_rename(transaction, transaction->backup_path, transaction->backup_name,
transaction->target_path, transaction->target_name, target_exists)) {
transaction->state = ACTIVATION_RECOVERY_NEEDED;
return CBM_ACTIVATION_TRANSACTION_ROLLBACK_FAILED;
}
transaction->backup_exists = false;
transaction->backup_contains_target = false;
} else if (transaction->action == ACTIVATION_REPLACE) {
bool target_exists = false;
bool linked_pair = false;
if (!activation_absent_target_snapshot(transaction, &target_exists, &linked_pair)) {
transaction->state = ACTIVATION_RECOVERY_NEEDED;
return CBM_ACTIVATION_TRANSACTION_ROLLBACK_FAILED;
}
if (target_exists && !activation_remove_absent_published_target(transaction, linked_pair)) {
transaction->state = ACTIVATION_RECOVERY_NEEDED;
return CBM_ACTIVATION_TRANSACTION_ROLLBACK_FAILED;
}
}
transaction->state = ACTIVATION_ROLLED_BACK;
return activation_sync_directory(transaction) ? CBM_ACTIVATION_TRANSACTION_OK
: CBM_ACTIVATION_TRANSACTION_IO;
}
cbm_activation_transaction_status_t cbm_activation_transaction_commit(
cbm_activation_transaction_t *transaction, cbm_activation_transaction_validator_fn validator,
void *validator_context) {
if (!transaction) {
return CBM_ACTIVATION_TRANSACTION_INVALID_ARGUMENT;
}
if (transaction->state != ACTIVATION_STAGED) {
return CBM_ACTIVATION_TRANSACTION_INVALID_STATE;
}
if (!activation_target_still_original(transaction)) {
return CBM_ACTIVATION_TRANSACTION_IO;
}
if (transaction->action == ACTIVATION_REPLACE) {
bool staged_exists = false;
if (!transaction->staged_exists ||
!activation_path_matches(transaction, transaction->staged_path,
transaction->staged_name, &transaction->staged_identity,
&staged_exists) ||
!staged_exists) {
return CBM_ACTIVATION_TRANSACTION_IO;
}
}
if (transaction->target_existed) {
if (transaction->action == ACTIVATION_REPLACE) {
activation_publish_status_t published =
activation_publish_existing_replacement(transaction);
if (published == ACTIVATION_PUBLISH_UNCHANGED_ERROR) {
return CBM_ACTIVATION_TRANSACTION_IO;
}
if (published == ACTIVATION_PUBLISH_CHANGED_ERROR) {
transaction->state = ACTIVATION_RECOVERY_NEEDED;
cbm_activation_transaction_status_t restored =
activation_rollback_internal(transaction);
return restored == CBM_ACTIVATION_TRANSACTION_OK
? CBM_ACTIVATION_TRANSACTION_IO
: CBM_ACTIVATION_TRANSACTION_ROLLBACK_FAILED;
}
} else {
bool reservation_exists = false;
if (!transaction->backup_exists ||
!activation_path_matches(transaction, transaction->backup_path,
transaction->backup_name, &transaction->backup_identity,
&reservation_exists) ||
!reservation_exists ||
!activation_rename(transaction, transaction->target_path, transaction->target_name,
transaction->backup_path, transaction->backup_name, true)) {
return CBM_ACTIVATION_TRANSACTION_IO;
}
transaction->backup_identity = transaction->target_identity;
transaction->backup_contains_target = true;
}
}
if (transaction->action == ACTIVATION_REPLACE && !transaction->target_existed) {
if (activation_before_absent_publish_for_test) {
activation_before_absent_publish_for_test(
transaction->target_path, activation_before_absent_publish_context_for_test);
}
activation_publish_status_t published = activation_publish_absent_replacement(transaction);
if (published == ACTIVATION_PUBLISH_UNCHANGED_ERROR) {
return CBM_ACTIVATION_TRANSACTION_IO;
}
if (published == ACTIVATION_PUBLISH_CHANGED_ERROR) {
transaction->state = ACTIVATION_RECOVERY_NEEDED;
cbm_activation_transaction_status_t restored =
activation_rollback_internal(transaction);
return restored == CBM_ACTIVATION_TRANSACTION_OK
? CBM_ACTIVATION_TRANSACTION_IO
: CBM_ACTIVATION_TRANSACTION_ROLLBACK_FAILED;
}
}
transaction->state = ACTIVATION_COMMITTED;
if (!activation_directory_still_valid(transaction) || !activation_sync_directory(transaction)) {
cbm_activation_transaction_status_t restored = activation_rollback_internal(transaction);
return restored == CBM_ACTIVATION_TRANSACTION_OK
? CBM_ACTIVATION_TRANSACTION_IO
: CBM_ACTIVATION_TRANSACTION_ROLLBACK_FAILED;
}
if (validator && !validator(transaction->target_path, validator_context)) {
cbm_activation_transaction_status_t restored = activation_rollback_internal(transaction);
return restored == CBM_ACTIVATION_TRANSACTION_OK
? CBM_ACTIVATION_TRANSACTION_VALIDATION_FAILED
: CBM_ACTIVATION_TRANSACTION_ROLLBACK_FAILED;
}
return CBM_ACTIVATION_TRANSACTION_OK;
}
cbm_activation_transaction_status_t cbm_activation_transaction_rollback(
cbm_activation_transaction_t *transaction) {
if (!transaction) {
return CBM_ACTIVATION_TRANSACTION_INVALID_ARGUMENT;
}
if (transaction->state != ACTIVATION_COMMITTED &&
transaction->state != ACTIVATION_RECOVERY_NEEDED) {
return CBM_ACTIVATION_TRANSACTION_INVALID_STATE;
}
return activation_rollback_internal(transaction);
}
cbm_activation_transaction_status_t cbm_activation_transaction_finalize(
cbm_activation_transaction_t *transaction) {
if (!transaction) {
return CBM_ACTIVATION_TRANSACTION_INVALID_ARGUMENT;
}
if (transaction->state != ACTIVATION_COMMITTED) {
return CBM_ACTIVATION_TRANSACTION_INVALID_STATE;
}
if (!activation_directory_still_valid(transaction)) {
return CBM_ACTIVATION_TRANSACTION_IO;
}
if (transaction->backup_contains_target) {
activation_unlink_status_t removed = activation_unlink_expected(
transaction, transaction->backup_path, transaction->backup_name,
&transaction->backup_identity, true);
if (removed == ACTIVATION_UNLINK_ERROR) {
return CBM_ACTIVATION_TRANSACTION_IO;
}
if (removed == ACTIVATION_UNLINK_DEFERRED) {
transaction->deferred_cleanup = true;
transaction->state = ACTIVATION_FINALIZED_DEFERRED;
return CBM_ACTIVATION_TRANSACTION_DEFERRED;
}
transaction->backup_exists = false;
transaction->backup_contains_target = false;
}
if (!activation_sync_directory(transaction)) {
return CBM_ACTIVATION_TRANSACTION_IO;
}
transaction->state = ACTIVATION_FINALIZED;
return CBM_ACTIVATION_TRANSACTION_OK;
}
cbm_activation_transaction_status_t cbm_activation_transaction_close(
cbm_activation_transaction_t **transaction_io) {
if (!transaction_io || !*transaction_io) {
return CBM_ACTIVATION_TRANSACTION_INVALID_ARGUMENT;
}
cbm_activation_transaction_t *transaction = *transaction_io;
if (transaction->state == ACTIVATION_COMMITTED ||
transaction->state == ACTIVATION_RECOVERY_NEEDED) {
cbm_activation_transaction_status_t status = activation_rollback_internal(transaction);
if (status != CBM_ACTIVATION_TRANSACTION_OK) {
return status;
}
}
if (transaction->state == ACTIVATION_STAGED || transaction->state == ACTIVATION_ROLLED_BACK) {
cbm_activation_transaction_status_t status = activation_discard_staged_assets(transaction);
if (status != CBM_ACTIVATION_TRANSACTION_OK) {
return status;
}
}
activation_transaction_destroy(transaction);
*transaction_io = NULL;
return CBM_ACTIVATION_TRANSACTION_OK;
}
const char *cbm_activation_transaction_target_path(
const cbm_activation_transaction_t *transaction) {
return transaction ? transaction->target_path : NULL;
}
const char *cbm_activation_transaction_staged_path(
const cbm_activation_transaction_t *transaction) {
return transaction ? transaction->staged_path : NULL;
}
const char *cbm_activation_transaction_backup_path(
const cbm_activation_transaction_t *transaction) {
return transaction ? transaction->backup_path : NULL;
}
const char *cbm_activation_transaction_deferred_path(
const cbm_activation_transaction_t *transaction) {
return transaction && transaction->deferred_cleanup ? transaction->backup_path : NULL;
}
const char *cbm_activation_transaction_status_message(cbm_activation_transaction_status_t status) {
switch (status) {
case CBM_ACTIVATION_TRANSACTION_OK:
return "activation transaction completed";
case CBM_ACTIVATION_TRANSACTION_DEFERRED:
return "activation completed; old executable cleanup was deferred";
case CBM_ACTIVATION_TRANSACTION_INVALID_ARGUMENT:
return "invalid activation transaction argument";
case CBM_ACTIVATION_TRANSACTION_NO_MEMORY:
return "activation transaction allocation failed";
case CBM_ACTIVATION_TRANSACTION_IO:
return "activation transaction I/O failed";
case CBM_ACTIVATION_TRANSACTION_INVALID_STATE:
return "activation transaction is in the wrong state";
case CBM_ACTIVATION_TRANSACTION_VALIDATION_FAILED:
return "activated candidate failed validation and was rolled back";
case CBM_ACTIVATION_TRANSACTION_ROLLBACK_FAILED:
return "activation failed and the retained backup could not be restored";
default:
return "unknown activation transaction status";
}
}