#pragma once #include #pragma comment(lib, "crypt32.lib") namespace git_control { static int ssh(int argc, wchar_t** argv) { if (inContainer(GetCurrentProcess())) throw std::runtime_error("Git SSH control is host-only"); BOOL inJob = FALSE; require(IsProcessInJob(GetCurrentProcess(), nullptr, &inJob) != FALSE && inJob, "Git SSH requires its control Job"); wchar_t module[32768] = {}; require(GetModuleFileNameW(nullptr, module, 32768) > 0, "Locate Git SSH control"); const auto tools = fs::path(module).parent_path(), root = tools.parent_path().parent_path(), executable = tools / L"ssh.exe"; for (const auto& path : {executable, root / L"ssh-config", root / L"ssh-key", root / L"known-hosts"}) nonReparse(path); std::vector args = {L"-F", (root / L"ssh-config").wstring(), L"-o", L"BatchMode=yes", L"-o", L"IdentitiesOnly=yes", L"-o", L"IdentityAgent=none", L"-o", L"ProxyCommand=none", L"-o", L"ProxyJump=none", L"-o", L"CanonicalizeHostname=no", L"-o", L"ClearAllForwardings=yes", L"-o", L"PermitLocalCommand=no", L"-o", L"StrictHostKeyChecking=yes", L"-o", L"UpdateHostKeys=no", L"-o", L"UserKnownHostsFile=" + (root / L"known-hosts").wstring(), L"-o", L"GlobalKnownHostsFile=" + (root / L"ssh-config").wstring(), L"-i", (root / L"ssh-key").wstring()}; for (int index = 1; index < argc; ++index) args.push_back(argv[index]); auto command = quote(executable.wstring()); for (const auto& arg : args) command += L" " + quote(arg); if (command.size() > 32000) throw std::runtime_error("Git SSH argv exceeds limit"); Handle input, output, error; require(DuplicateHandle(GetCurrentProcess(), GetStdHandle(STD_INPUT_HANDLE), GetCurrentProcess(), &input.value, 0, TRUE, DUPLICATE_SAME_ACCESS) != FALSE, "Git SSH stdin"); require(DuplicateHandle(GetCurrentProcess(), GetStdHandle(STD_OUTPUT_HANDLE), GetCurrentProcess(), &output.value, 0, TRUE, DUPLICATE_SAME_ACCESS) != FALSE, "Git SSH stdout"); require(DuplicateHandle(GetCurrentProcess(), GetStdHandle(STD_ERROR_HANDLE), GetCurrentProcess(), &error.value, 0, TRUE, DUPLICATE_SAME_ACCESS) != FALSE, "Git SSH stderr"); AttributeList attributes; HANDLE handles[] = {input.value, output.value, error.value}; require(UpdateProcThreadAttribute(attributes.list, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, handles, sizeof(handles), nullptr, nullptr) != FALSE, "Git SSH handle allowlist"); STARTUPINFOEXW startup = {}; startup.StartupInfo.cb = sizeof(startup); startup.StartupInfo.dwFlags = STARTF_USESTDHANDLES; startup.StartupInfo.hStdInput = input.value; startup.StartupInfo.hStdOutput = output.value; startup.StartupInfo.hStdError = error.value; startup.lpAttributeList = attributes.list; PROCESS_INFORMATION process = {}; require(CreateProcessW(executable.c_str(), command.data(), nullptr, nullptr, TRUE, EXTENDED_STARTUPINFO_PRESENT | CREATE_NO_WINDOW, nullptr, root.c_str(), &startup.StartupInfo, &process) != FALSE, "Create Git SSH process"); Handle child(process.hProcess), thread(process.hThread); WaitForSingleObject(child.value, INFINITE); DWORD exit = 125; require(GetExitCodeProcess(child.value, &exit) != FALSE, "Read Git SSH exit"); return static_cast(exit); } static int credential(bool protect) { if (inContainer(GetCurrentProcess())) throw std::runtime_error("Git credentials are host-only"); std::string input; char buffer[4096]; DWORD count = 0; while (ReadFile(GetStdHandle(STD_INPUT_HANDLE), buffer, sizeof(buffer), &count, nullptr) && count) { input.append(buffer, count); if (input.size() > 131072) throw std::runtime_error("Git credential exceeds limit"); } auto bytes = workspace_write::unhex(input, 65536); DATA_BLOB source = {static_cast(bytes.size()), bytes.data()}, output = {}; const BOOL result = protect ? CryptProtectData(&source, L"YCode Git credential", nullptr, nullptr, nullptr, CRYPTPROTECT_UI_FORBIDDEN, &output) : CryptUnprotectData(&source, nullptr, nullptr, nullptr, nullptr, CRYPTPROTECT_UI_FORBIDDEN, &output); require(result != FALSE, "Protect/unprotect Git credential with user DPAPI"); const auto encoded = workspace_write::hex(output.pbData, output.cbData); SecureZeroMemory(output.pbData, output.cbData); LocalFree(output.pbData); SecureZeroMemory(bytes.data(), bytes.size()); workspace_write::reply("{\"data\":\"" + encoded + "\"}"); return 0; } static std::wstring text(const std::string& encoded) { const auto bytes = workspace_write::unhex(encoded, 1024 * 1024); if (bytes.empty()) return L""; const int count = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, reinterpret_cast(bytes.data()), static_cast(bytes.size()), nullptr, 0); if (count <= 0) throw std::runtime_error("Invalid Git control UTF-8"); std::wstring value(count, 0); require(MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, reinterpret_cast(bytes.data()), static_cast(bytes.size()), value.data(), count) == count, "Decode Git control text"); if (value.find(L'\0') != std::wstring::npos) throw std::runtime_error("NUL in Git control text"); return value; } static std::vector fields(const fs::path& path) { nonReparse(path); Handle file(CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT, nullptr)); require(file.value != INVALID_HANDLE_VALUE, "Open Git control fields"); const auto bytes = workspace_write::contents(file.value); if (bytes.size() > 1024 * 1024) throw std::runtime_error("Git control fields exceed limit"); std::string input(bytes.begin(), bytes.end()); std::vector result; size_t start = 0; for (;;) { const auto end = input.find('\n', start); if (end == std::string::npos) break; if (result.size() >= 256) throw std::runtime_error("Git control field count exceeds limit"); result.push_back(text(input.substr(start, end - start))); start = end + 1; } if (start != input.size()) throw std::runtime_error("Incomplete Git control fields"); return result; } static int run(const std::wstring& input, DWORD timeout) { if (inContainer(GetCurrentProcess())) throw std::runtime_error("Git control is host-only"); if (timeout < 100 || timeout > 600000) throw std::runtime_error("Invalid Git control timeout"); const fs::path stage = fs::absolute(input); nonReparse(stage); const auto executable = stage / L"tools/git/git.exe"; nonReparse(executable); auto command = quote(executable.wstring()); for (const auto& arg : fields(stage / L"arguments")) command += L" " + quote(arg); if (command.size() > 32000) throw std::runtime_error("Git control argv exceeds Windows limit"); auto environmentFields = fields(stage / L"environment"); std::sort(environmentFields.begin(), environmentFields.end()); std::vector environment; for (const auto& entry : environmentFields) { const auto separator = entry.find(L'='); if (!separator || separator == std::wstring::npos || entry.substr(0, separator).find_first_not_of(L"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_") != std::wstring::npos) throw std::runtime_error("Invalid Git control environment"); environment.insert(environment.end(), entry.begin(), entry.end()); environment.push_back(0); } environment.push_back(0); Handle parent(parentProcess()), job(CreateJobObjectW(nullptr, nullptr)); writeJson(stage / L"native-lease.json", "{\"version\":1,\"pid\":" + std::to_string(GetCurrentProcessId()) + ",\"root\":\"" + jsonText(stage.wstring()) + "\"}\n"); require(job.value != nullptr, "Create Git control Job"); JOBOBJECT_EXTENDED_LIMIT_INFORMATION limits = {}; limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE | JOB_OBJECT_LIMIT_ACTIVE_PROCESS | JOB_OBJECT_LIMIT_JOB_MEMORY; limits.BasicLimitInformation.ActiveProcessLimit = 32; limits.JobMemoryLimit = static_cast(1024) * 1024 * 1024; require(SetInformationJobObject(job.value, JobObjectExtendedLimitInformation, &limits, sizeof(limits)) != FALSE, "Git control Job limits"); SECURITY_ATTRIBUTES inherited = {sizeof(inherited), nullptr, TRUE}; Handle stdinFile(CreateFileW((stage / L"input").c_str(), GENERIC_READ, FILE_SHARE_READ, &inherited, OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT, nullptr)); require(stdinFile.value != INVALID_HANDLE_VALUE, "Open Git control input"); Handle output, error; require(DuplicateHandle(GetCurrentProcess(), GetStdHandle(STD_OUTPUT_HANDLE), GetCurrentProcess(), &output.value, 0, TRUE, DUPLICATE_SAME_ACCESS) != FALSE, "Duplicate Git stdout"); require(DuplicateHandle(GetCurrentProcess(), GetStdHandle(STD_ERROR_HANDLE), GetCurrentProcess(), &error.value, 0, TRUE, DUPLICATE_SAME_ACCESS) != FALSE, "Duplicate Git stderr"); AttributeList attributes; HANDLE handles[] = {stdinFile.value, output.value, error.value}; require(UpdateProcThreadAttribute(attributes.list, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, handles, sizeof(handles), nullptr, nullptr) != FALSE, "Git control handle allowlist"); STARTUPINFOEXW startup = {}; startup.StartupInfo.cb = sizeof(startup); startup.StartupInfo.dwFlags = STARTF_USESTDHANDLES; startup.StartupInfo.hStdInput = stdinFile.value; startup.StartupInfo.hStdOutput = output.value; startup.StartupInfo.hStdError = error.value; startup.lpAttributeList = attributes.list; PROCESS_INFORMATION process = {}; require(CreateProcessW(executable.c_str(), command.data(), nullptr, nullptr, TRUE, EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT | CREATE_SUSPENDED | CREATE_NO_WINDOW, environment.data(), (stage / L"workspace").c_str(), &startup.StartupInfo, &process) != FALSE, "Create Git control process"); Handle child(process.hProcess), thread(process.hThread); if (!AssignProcessToJobObject(job.value, child.value)) { TerminateProcess(child.value, 125); throw std::runtime_error("Assign Git control Job"); } require(ResumeThread(thread.value) != static_cast(-1), "Resume Git control process"); HANDLE monitored[] = {child.value, parent.value}; const DWORD waited = WaitForMultipleObjects(2, monitored, FALSE, timeout); DWORD exit = waited == WAIT_TIMEOUT ? 124 : 130; if (waited == WAIT_OBJECT_0) require(GetExitCodeProcess(child.value, &exit) != FALSE, "Read Git control exit"); TerminateJobObject(job.value, exit); WaitForSingleObject(child.value, 5000); return static_cast(exit); } }