Files
ycode/native/sandbox/git-control.h
T

130 lines
10 KiB
C++

#pragma once
#include <wincrypt.h>
#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<std::wstring> 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<int>(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<DWORD>(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<const char*>(bytes.data()), static_cast<int>(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<const char*>(bytes.data()), static_cast<int>(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<std::wstring> 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<std::wstring> 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<wchar_t> 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<SIZE_T>(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<DWORD>(-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<int>(exit);
}
}