Files
gstack/lib/cso/launcher-windows.c
Garry TanandOpenAI Codex 4a3c6a8a3c v1.87.0.0 feat: add verified CSO audits and replayable repair bundles (#2852)
* feat(cso): add verified audits and replayable repair bundles

* fix(cso): harden qualification and setup boundaries

* fix(cso): assemble security canaries at runtime

* fix(cso): bound release proof and maintenance work

Co-Authored-By: OpenAI Codex <noreply@openai.com>

* fix(cso): require complete evaluation reports

Co-Authored-By: OpenAI Codex <noreply@openai.com>

* fix(cso): replay expired snapshots from supplied source

Co-Authored-By: OpenAI Codex <noreply@openai.com>

* test(cso): synchronize DNS cancellation assertion

Co-Authored-By: OpenAI Codex <noreply@openai.com>

* chore(ship): exempt repository owner from liveness proof

Co-Authored-By: OpenAI Codex <noreply@openai.com>

* test(cso): make recheck retention overlap deterministic

Co-Authored-By: OpenAI Codex <noreply@openai.com>

* chore: bump version and changelog (v1.85.0.0)

Co-Authored-By: OpenAI Codex <noreply@openai.com>

* fix(cso): pass native release gates

Co-Authored-By: OpenAI Codex <noreply@openai.com>

* chore: move release to v1.86.0.0

Co-Authored-By: OpenAI Codex <noreply@openai.com>

* fix(cso): resolve rechecks by finding

Co-Authored-By: OpenAI Codex <noreply@openai.com>

* chore: move release to v1.87.0.0

Co-Authored-By: OpenAI Codex <noreply@openai.com>

* fix(cso): pass macOS and Windows release gates

Normalize BSD wc output, compare Windows paths by filesystem identity, preserve portable snapshot race coverage, and narrow POSIX-only Windows fixtures.

Co-Authored-By: OpenAI Codex <noreply@openai.com>

* fix(cso): harden native verification gates

* fix(cso): refine Windows native diagnostics

* test(cso): isolate Windows Git startup failure

* test(cso): stabilize Windows native diagnostics

* fix(cso): support hardened Git on Windows

* fix(cso): close final verification gaps

* test(cso): bound cold Docker fixture setup

* fix(cso): restore cross-platform free-suite gates

---------

Co-authored-by: OpenAI Codex <noreply@openai.com>
2026-09-14 15:14:58 -07:00

278 lines
14 KiB
C

/* Native Windows startup boundary. Compile with the static MSVC CRT (/MT):
* Bun must never run until the explicit environment block has been installed.
* Source: https://learn.microsoft.com/windows/win32/procthread/changing-environment-variables
*/
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef _WIN32_WINNT
#define _WIN32_WINNT 0x0601
#endif
#include <windows.h>
#include <bcrypt.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <wchar.h>
#pragma comment(lib, "bcrypt.lib")
#define CSO_PATH_CAP 32768
#define CSO_ENV_VALUE_CAP 8193
#ifndef GSTACK_CSO_CORE_SHA256
#error GSTACK_CSO_CORE_SHA256 must bind the launcher to its compiled core
#endif
#ifndef GSTACK_CSO_GIT_PATH
#error GSTACK_CSO_GIT_PATH must bind the launcher to setup's resolved git.exe
#endif
static int fail(const char *message) {
fprintf(stderr, "gstack-cso: %s\n", message);
return 69;
}
static int append(wchar_t *buffer, size_t *used, wchar_t value) {
if (*used >= CSO_PATH_CAP - 1) return 0;
buffer[(*used)++] = value;
buffer[*used] = L'\0';
return 1;
}
static int joined_path(wchar_t *output, size_t capacity,
const wchar_t *directory, const wchar_t *leaf) {
int written = swprintf(output, capacity, L"%ls\\%ls", directory, leaf);
return written >= 0 && (size_t)written < capacity;
}
/* Quote each argument using the Windows CRT's backslash/quote rules. No shell
* participates, including for arguments containing %, &, quotes or newlines. */
static int argument(wchar_t *buffer, size_t *used, const wchar_t *value) {
size_t slashes = 0;
if (!append(buffer, used, L'"')) return 0;
for (;; value++) {
if (*value == L'\\') { slashes++; continue; }
size_t count = (*value == L'"' || *value == L'\0') ? slashes * 2 : slashes;
if (*value == L'"') count++;
while (count--) if (!append(buffer, used, L'\\')) return 0;
slashes = 0;
if (*value == L'\0') break;
if (!append(buffer, used, *value)) return 0;
}
return append(buffer, used, L'"');
}
static int environment_entry(wchar_t *block, size_t *used,
const wchar_t *name, const wchar_t *value) {
size_t length = wcslen(name) + wcslen(value) + 2;
if (*used + length >= CSO_PATH_CAP) return 0;
memcpy(block + *used, name, wcslen(name) * sizeof(wchar_t));
*used += wcslen(name); block[(*used)++] = L'=';
memcpy(block + *used, value, (wcslen(value) + 1) * sizeof(wchar_t));
*used += wcslen(value) + 1; block[*used] = L'\0';
return 1;
}
static int inherited_entry(wchar_t *block, size_t *used, const wchar_t *name) {
wchar_t value[CSO_ENV_VALUE_CAP];
value[0] = L'\0';
DWORD length = GetEnvironmentVariableW(name, value, CSO_ENV_VALUE_CAP);
if (length >= (DWORD)CSO_ENV_VALUE_CAP) return 0;
return environment_entry(block, used, name, value);
}
/* Hash the already-open core. Its handle denies writes, deletes, and renames,
* so the bytes checked here are the bytes CreateProcessW will resolve below. */
static int sha256_handle(HANDLE file, char output[65]) {
BCRYPT_ALG_HANDLE algorithm = NULL;
BCRYPT_HASH_HANDLE hash = NULL;
PUCHAR object = NULL;
ULONG object_bytes = 0, hash_bytes = 0, returned = 0;
UCHAR digest[32], buffer[64 * 1024];
DWORD read_bytes = 0;
LARGE_INTEGER start;
int valid = 0;
start.QuadPart = 0;
if (!SetFilePointerEx(file, start, NULL, FILE_BEGIN)) goto cleanup;
if (BCryptOpenAlgorithmProvider(&algorithm, BCRYPT_SHA256_ALGORITHM,
MS_PRIMITIVE_PROVIDER, 0) < 0) goto cleanup;
if (BCryptGetProperty(algorithm, BCRYPT_OBJECT_LENGTH,
(PUCHAR)&object_bytes, (ULONG)sizeof(object_bytes), &returned, 0) < 0 ||
returned != (ULONG)sizeof(object_bytes) || object_bytes == 0 ||
object_bytes > 1024UL * 1024UL) goto cleanup;
if (BCryptGetProperty(algorithm, BCRYPT_HASH_LENGTH,
(PUCHAR)&hash_bytes, (ULONG)sizeof(hash_bytes), &returned, 0) < 0 ||
returned != (ULONG)sizeof(hash_bytes) || hash_bytes != (ULONG)sizeof(digest))
goto cleanup;
object = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, object_bytes);
if (!object || BCryptCreateHash(algorithm, &hash, object, object_bytes,
NULL, 0, 0) < 0) goto cleanup;
for (;;) {
if (!ReadFile(file, buffer, (DWORD)sizeof(buffer), &read_bytes, NULL))
goto cleanup;
if (read_bytes == 0) break;
if (BCryptHashData(hash, buffer, (ULONG)read_bytes, 0) < 0) goto cleanup;
}
if (BCryptFinishHash(hash, digest, (ULONG)sizeof(digest), 0) < 0) goto cleanup;
static const char hex[] = "0123456789abcdef";
for (ULONG index = 0; index < (ULONG)sizeof(digest); index++) {
output[index * 2] = hex[digest[index] >> 4];
output[index * 2 + 1] = hex[digest[index] & 15];
}
output[64] = '\0';
valid = 1;
cleanup:
if (hash) BCryptDestroyHash(hash);
if (object) {
SecureZeroMemory(object, object_bytes);
HeapFree(GetProcessHeap(), 0, object);
}
if (algorithm) BCryptCloseAlgorithmProvider(algorithm, 0);
if (!SetFilePointerEx(file, start, NULL, FILE_BEGIN)) valid = 0;
return valid;
}
static HANDLE inherited_stdio(DWORD id, DWORD access) {
HANDLE source = GetStdHandle(id), copy = INVALID_HANDLE_VALUE;
if (source && source != INVALID_HANDLE_VALUE) {
if (DuplicateHandle(GetCurrentProcess(), source, GetCurrentProcess(), &copy,
0, TRUE, DUPLICATE_SAME_ACCESS)) return copy;
return INVALID_HANDLE_VALUE;
}
SECURITY_ATTRIBUTES security = {(DWORD)sizeof(security), NULL, TRUE};
return CreateFileW(L"NUL", access, FILE_SHARE_READ | FILE_SHARE_WRITE,
&security, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
}
int wmain(int argc, wchar_t **argv) {
wchar_t module[CSO_PATH_CAP], core[CSO_PATH_CAP], windows[MAX_PATH + 1];
wchar_t caller_cwd[CSO_ENV_VALUE_CAP];
DWORD caller_length = GetCurrentDirectoryW(CSO_ENV_VALUE_CAP, caller_cwd);
if (!caller_length || caller_length >= (DWORD)CSO_ENV_VALUE_CAP)
return fail("caller working directory unavailable or too large");
DWORD length = GetModuleFileNameW(NULL, module, CSO_PATH_CAP);
if (!length || length >= (DWORD)CSO_PATH_CAP) return fail("launcher path unavailable");
HANDLE executable = CreateFileW(module, FILE_READ_ATTRIBUTES,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, NULL);
if (executable == INVALID_HANDLE_VALUE) return fail("launcher path unavailable");
length = GetFinalPathNameByHandleW(executable, module, CSO_PATH_CAP, FILE_NAME_NORMALIZED);
CloseHandle(executable);
if (!length || length >= (DWORD)CSO_PATH_CAP) return fail("launcher path unavailable");
wchar_t *slash = wcsrchr(module, L'\\');
if (!slash) return fail("invalid launcher path");
*slash = L'\0';
wchar_t gate_path[CSO_PATH_CAP];
if (!joined_path(gate_path, CSO_PATH_CAP, module, L".gstack-cso-generation.lock"))
return fail("generation lock path too long");
SECURITY_ATTRIBUTES gate_security = {(DWORD)sizeof(gate_security), NULL, TRUE};
HANDLE generation_gate = CreateFileW(gate_path, GENERIC_READ, FILE_SHARE_READ,
&gate_security, OPEN_EXISTING, FILE_ATTRIBUTE_HIDDEN | FILE_FLAG_OPEN_REPARSE_POINT, NULL);
BY_HANDLE_FILE_INFORMATION gate_info;
if (generation_gate == INVALID_HANDLE_VALUE || !GetFileInformationByHandle(generation_gate, &gate_info) ||
(gate_info.dwFileAttributes & (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT)) ||
gate_info.nNumberOfLinks != 1 || gate_info.nFileSizeHigh != 0 || gate_info.nFileSizeLow != 0)
return fail("installation generation lock is missing or invalid; run gstack setup/build");
if (!joined_path(core, CSO_PATH_CAP, module, L"gstack-cso-core.exe"))
return fail("core path too long");
HANDLE pinned_core = CreateFileW(core, GENERIC_READ, FILE_SHARE_READ, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OPEN_REPARSE_POINT, NULL);
BY_HANDLE_FILE_INFORMATION core_info;
if (pinned_core == INVALID_HANDLE_VALUE || !GetFileInformationByHandle(pinned_core, &core_info) ||
(core_info.dwFileAttributes & (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT)) ||
core_info.nNumberOfLinks != 1)
return fail("trusted compiled helper is missing or invalid; run gstack setup/build");
wchar_t generation_path[CSO_PATH_CAP];
if (!joined_path(generation_path, CSO_PATH_CAP, module, L".gstack-cso-generation"))
return fail("generation manifest path too long");
HANDLE generation = CreateFileW(generation_path, GENERIC_READ, FILE_SHARE_READ, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_HIDDEN | FILE_FLAG_OPEN_REPARSE_POINT, NULL);
BY_HANDLE_FILE_INFORMATION generation_info; char actual[66]; DWORD manifest_bytes = 0;
if (generation == INVALID_HANDLE_VALUE || !GetFileInformationByHandle(generation, &generation_info) ||
(generation_info.dwFileAttributes & (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT)) ||
generation_info.nNumberOfLinks != 1 || generation_info.nFileSizeHigh != 0 || generation_info.nFileSizeLow != 65 ||
!ReadFile(generation, actual, 65, &manifest_bytes, NULL) || manifest_bytes != 65)
return fail("generation manifest is missing or invalid; run gstack setup/build");
CloseHandle(generation); actual[65] = '\0';
if (actual[64] != '\n' || strncmp(actual, GSTACK_CSO_CORE_SHA256, 64) != 0)
return fail("launcher and compiled helper generations do not match; run gstack setup/build");
char core_sha256[65];
if (!sha256_handle(pinned_core, core_sha256) ||
strcmp(core_sha256, GSTACK_CSO_CORE_SHA256) != 0)
return fail("compiled helper digest does not match its launcher; run gstack setup/build");
wchar_t *command = calloc(CSO_PATH_CAP, sizeof(wchar_t));
wchar_t *environment = calloc(CSO_PATH_CAP, sizeof(wchar_t));
if (!command || !environment) return fail("startup allocation failed");
size_t used = 0;
if (!argument(command, &used, core)) return fail("arguments are too large");
for (int i = 1; i < argc; i++)
if (!append(command, &used, L' ') || !argument(command, &used, argv[i]))
return fail("arguments are too large");
/* Alphabetical, case-insensitive Unicode order; every other variable is
* absent, including BUN_OPTIONS, BUN_BE_BUN, NODE_OPTIONS and loader hooks. */
used = 0;
const wchar_t *names[] = {L"CLAUDE_PLUGIN_DATA", L"CLAUDE_PLUGIN_ROOT",
L"DOCKER_CONFIG", L"DOCKER_CONTEXT", L"DOCKER_HOST"};
for (size_t i = 0; i < sizeof(names) / sizeof(names[0]); i++)
if (!inherited_entry(environment, &used, names[i])) return fail("environment input is too large");
if (!environment_entry(environment, &used, L"GSTACK_CSO_CALLER_CWD", caller_cwd) ||
!environment_entry(environment, &used, L"GSTACK_CSO_GENERATION_GUARD", L"inherited-windows-generation-handle-v3") ||
!environment_entry(environment, &used, L"GSTACK_CSO_TRUSTED_GIT", GSTACK_CSO_GIT_PATH) ||
!inherited_entry(environment, &used, L"GSTACK_HOME") ||
!inherited_entry(environment, &used, L"HOME")) return fail("environment input is too large");
length = GetWindowsDirectoryW(windows, MAX_PATH + 1);
if (!length || length > (DWORD)MAX_PATH) return fail("Windows system directory unavailable");
wchar_t git_path[CSO_ENV_VALUE_CAP], git_directory[CSO_ENV_VALUE_CAP];
if (wcslen(GSTACK_CSO_GIT_PATH) >= CSO_ENV_VALUE_CAP) return fail("trusted Git path is too large");
wcscpy(git_path, GSTACK_CSO_GIT_PATH); wcscpy(git_directory, git_path);
wchar_t *git_slash = wcsrchr(git_directory, L'\\');
if (!git_slash || git_slash == git_directory) return fail("trusted Git path is invalid");
*git_slash = L'\0';
wchar_t trusted_path[CSO_ENV_VALUE_CAP];
int trusted_length = swprintf(trusted_path, CSO_ENV_VALUE_CAP,
L"%ls;%ls\\System32", git_directory, windows);
if (trusted_length < 0 || (size_t)trusted_length >= CSO_ENV_VALUE_CAP)
return fail("trusted system path is too large");
if (!environment_entry(environment, &used, L"LANG", L"C.UTF-8") ||
!environment_entry(environment, &used, L"LC_ALL", L"C.UTF-8") ||
!environment_entry(environment, &used, L"PATH", trusted_path) ||
!environment_entry(environment, &used, L"SystemRoot", windows) ||
!environment_entry(environment, &used, L"TZ", L"UTC") ||
!inherited_entry(environment, &used, L"USERPROFILE")) return fail("environment input is too large");
STARTUPINFOEXW startup = {0};
PROCESS_INFORMATION child = {0};
startup.StartupInfo.cb = (DWORD)sizeof(startup);
startup.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
HANDLE handles[4] = {inherited_stdio(STD_INPUT_HANDLE, GENERIC_READ),
inherited_stdio(STD_OUTPUT_HANDLE, GENERIC_WRITE), inherited_stdio(STD_ERROR_HANDLE, GENERIC_WRITE), generation_gate};
for (size_t i = 0; i < 4; i++) if (handles[i] == INVALID_HANDLE_VALUE) return fail("standard handles unavailable");
startup.StartupInfo.hStdInput = handles[0];
startup.StartupInfo.hStdOutput = handles[1];
startup.StartupInfo.hStdError = handles[2];
SIZE_T attribute_bytes = 0;
InitializeProcThreadAttributeList(NULL, 1, 0, &attribute_bytes);
startup.lpAttributeList = HeapAlloc(GetProcessHeap(), 0, attribute_bytes);
if (!startup.lpAttributeList ||
!InitializeProcThreadAttributeList(startup.lpAttributeList, 1, 0, &attribute_bytes) ||
!UpdateProcThreadAttribute(startup.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
handles, sizeof(handles), NULL, NULL)) return fail("standard handle isolation unavailable");
BOOL created = CreateProcessW(core, command, NULL, NULL, TRUE,
CREATE_UNICODE_ENVIRONMENT | EXTENDED_STARTUPINFO_PRESENT, environment,
module, &startup.StartupInfo, &child);
DeleteProcThreadAttributeList(startup.lpAttributeList);
HeapFree(GetProcessHeap(), 0, startup.lpAttributeList);
for (size_t i = 0; i < 3; i++) CloseHandle(handles[i]);
free(command); free(environment);
if (!created) { CloseHandle(pinned_core); return fail("trusted compiled helper could not start"); }
CloseHandle(child.hThread);
DWORD code = 69;
if (WaitForSingleObject(child.hProcess, INFINITE) == WAIT_OBJECT_0)
GetExitCodeProcess(child.hProcess, &code);
CloseHandle(child.hProcess); CloseHandle(pinned_core); CloseHandle(generation_gate);
return (int)code;
}