445 lines
28 KiB
C++
445 lines
28 KiB
C++
#define WIN32_LEAN_AND_MEAN
|
|
#define NOMINMAX
|
|
#include <windows.h>
|
|
#include <userenv.h>
|
|
#include <sddl.h>
|
|
#include <aclapi.h>
|
|
#include <tlhelp32.h>
|
|
#include <algorithm>
|
|
#include <cstdio>
|
|
#include <filesystem>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <vector>
|
|
|
|
namespace fs = std::filesystem;
|
|
struct Handle {
|
|
HANDLE value = nullptr;
|
|
Handle() = default;
|
|
explicit Handle(HANDLE handle) : value(handle) {}
|
|
~Handle() { if (value && value != INVALID_HANDLE_VALUE) CloseHandle(value); }
|
|
Handle(const Handle&) = delete;
|
|
Handle& operator=(const Handle&) = delete;
|
|
};
|
|
static void require(bool ok, const char* operation) {
|
|
if (!ok) throw std::runtime_error(std::string(operation) + ": win32=" + std::to_string(GetLastError()));
|
|
}
|
|
static void security(DWORD error, const char* operation) {
|
|
if (error != ERROR_SUCCESS) throw std::runtime_error(std::string(operation) + ": win32=" + std::to_string(error));
|
|
}
|
|
static bool inContainer(HANDLE process) {
|
|
Handle token;
|
|
require(OpenProcessToken(process, TOKEN_QUERY, &token.value) != FALSE, "OpenProcessToken");
|
|
DWORD value = 0, bytes = 0;
|
|
require(GetTokenInformation(token.value, TokenIsAppContainer, &value, sizeof(value), &bytes) != FALSE, "TokenIsAppContainer");
|
|
return value != 0;
|
|
}
|
|
static bool matchesContainer(HANDLE process, PSID expected) {
|
|
Handle token;
|
|
require(OpenProcessToken(process, TOKEN_QUERY, &token.value) != FALSE, "Open child token");
|
|
DWORD bytes = 0; GetTokenInformation(token.value, TokenAppContainerSid, nullptr, 0, &bytes);
|
|
std::vector<BYTE> buffer(bytes);
|
|
require(GetTokenInformation(token.value, TokenAppContainerSid, buffer.data(), bytes, &bytes) != FALSE, "Child AppContainer SID");
|
|
return EqualSid(reinterpret_cast<TOKEN_APPCONTAINER_INFORMATION*>(buffer.data())->TokenAppContainer, expected) != FALSE;
|
|
}
|
|
static std::wstring quote(const std::wstring& value) {
|
|
std::wstring result = L"\"";
|
|
size_t slashes = 0;
|
|
for (wchar_t ch : value) {
|
|
if (ch == L'\\') { ++slashes; continue; }
|
|
if (ch == L'\"') result.append(slashes * 2 + 1, L'\\');
|
|
else result.append(slashes, L'\\');
|
|
slashes = 0;
|
|
result += ch;
|
|
}
|
|
result.append(slashes * 2, L'\\');
|
|
return result + L"\"";
|
|
}
|
|
static std::string jsonText(const std::wstring& value) {
|
|
const int count = WideCharToMultiByte(CP_UTF8, 0, value.c_str(), static_cast<int>(value.size()), nullptr, 0, nullptr, nullptr);
|
|
std::string bytes(count, 0), escaped;
|
|
WideCharToMultiByte(CP_UTF8, 0, value.c_str(), static_cast<int>(value.size()), bytes.data(), count, nullptr, nullptr);
|
|
for (char ch : bytes) { if (ch == '\\' || ch == '"') escaped += '\\'; escaped += ch; }
|
|
return escaped;
|
|
}
|
|
static void writeJson(const fs::path& target, const std::string& json) {
|
|
Handle output(CreateFileW(target.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, nullptr));
|
|
require(output.value != INVALID_HANDLE_VALUE, "Create control report");
|
|
DWORD written = 0;
|
|
require(WriteFile(output.value, json.data(), static_cast<DWORD>(json.size()), &written, nullptr) && written == json.size(), "Write control report");
|
|
require(FlushFileBuffers(output.value) != FALSE, "Flush control report");
|
|
}
|
|
static std::wstring windowsDirectory() {
|
|
wchar_t buffer[MAX_PATH] = {};
|
|
require(GetWindowsDirectoryW(buffer, MAX_PATH) != 0, "GetWindowsDirectory");
|
|
return buffer;
|
|
}
|
|
static void nonReparse(const fs::path& target) {
|
|
auto current = target;
|
|
for (;;) {
|
|
const DWORD attributes = GetFileAttributesW(current.c_str());
|
|
require(attributes != INVALID_FILE_ATTRIBUTES, "GetFileAttributes");
|
|
if (attributes & FILE_ATTRIBUTE_REPARSE_POINT) throw std::runtime_error("Reparse point in sandbox control path");
|
|
const auto parent = current.parent_path();
|
|
if (parent == current || parent.empty()) break;
|
|
current = parent;
|
|
}
|
|
}
|
|
static void privateDirectory(const fs::path& path) {
|
|
Handle token;
|
|
require(OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token.value) != FALSE, "OpenProcessToken");
|
|
DWORD bytes = 0;
|
|
GetTokenInformation(token.value, TokenUser, nullptr, 0, &bytes);
|
|
std::vector<BYTE> user(bytes);
|
|
require(GetTokenInformation(token.value, TokenUser, user.data(), bytes, &bytes) != FALSE, "TokenUser");
|
|
LPWSTR sid = nullptr;
|
|
require(ConvertSidToStringSidW(reinterpret_cast<TOKEN_USER*>(user.data())->User.Sid, &sid) != FALSE, "ConvertSidToStringSid");
|
|
const std::wstring sddl = L"D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;" + std::wstring(sid) + L")";
|
|
LocalFree(sid);
|
|
PSECURITY_DESCRIPTOR descriptor = nullptr;
|
|
require(ConvertStringSecurityDescriptorToSecurityDescriptorW(sddl.c_str(), SDDL_REVISION_1, &descriptor, nullptr) != FALSE, "Private DACL");
|
|
PACL acl = nullptr; BOOL present = FALSE, defaulted = FALSE;
|
|
require(GetSecurityDescriptorDacl(descriptor, &present, &acl, &defaulted) != FALSE, "GetSecurityDescriptorDacl");
|
|
const DWORD error = SetNamedSecurityInfoW(const_cast<LPWSTR>(path.c_str()), SE_FILE_OBJECT,
|
|
DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION, nullptr, nullptr, acl, nullptr);
|
|
LocalFree(descriptor);
|
|
security(error, "Set private control DACL");
|
|
}
|
|
static void grantStage(const fs::path& target, PSID sid, bool writable = true) {
|
|
if (!writable) privateDirectory(target);
|
|
PSECURITY_DESCRIPTOR old = nullptr;
|
|
PACL acl = nullptr, next = nullptr;
|
|
security(GetNamedSecurityInfoW(target.c_str(), SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, nullptr, nullptr, &acl, nullptr, &old), "Read stage DACL");
|
|
EXPLICIT_ACCESSW access = {};
|
|
access.grfAccessPermissions = FILE_GENERIC_READ | FILE_GENERIC_EXECUTE;
|
|
if (writable) access.grfAccessPermissions |= FILE_GENERIC_WRITE | DELETE;
|
|
access.grfAccessMode = GRANT_ACCESS;
|
|
access.grfInheritance = SUB_CONTAINERS_AND_OBJECTS_INHERIT;
|
|
access.Trustee.TrusteeForm = TRUSTEE_IS_SID;
|
|
access.Trustee.TrusteeType = TRUSTEE_IS_UNKNOWN;
|
|
access.Trustee.ptstrName = static_cast<LPWSTR>(sid);
|
|
const DWORD combined = SetEntriesInAclW(1, &access, acl, &next);
|
|
if (combined != ERROR_SUCCESS) { LocalFree(old); security(combined, "Build stage DACL"); }
|
|
const DWORD applied = SetNamedSecurityInfoW(const_cast<LPWSTR>(target.c_str()), SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, nullptr, nullptr, next, nullptr);
|
|
LocalFree(next); LocalFree(old);
|
|
security(applied, "Grant stage DACL");
|
|
PSECURITY_DESCRIPTOR label = nullptr;
|
|
require(ConvertStringSecurityDescriptorToSecurityDescriptorW(L"S:(ML;OICI;NW;;;LW)", SDDL_REVISION_1, &label, nullptr) != FALSE, "Stage integrity label");
|
|
PACL sacl = nullptr; BOOL present = FALSE, defaulted = FALSE;
|
|
require(GetSecurityDescriptorSacl(label, &present, &sacl, &defaulted) != FALSE, "GetSecurityDescriptorSacl");
|
|
const DWORD labeled = SetNamedSecurityInfoW(const_cast<LPWSTR>(target.c_str()), SE_FILE_OBJECT, LABEL_SECURITY_INFORMATION, nullptr, nullptr, nullptr, sacl);
|
|
LocalFree(label);
|
|
security(labeled, "Set stage integrity label");
|
|
}
|
|
struct Profile {
|
|
std::wstring name;
|
|
PSID sid = nullptr;
|
|
explicit Profile(std::wstring value) : name(std::move(value)) {
|
|
if (name.size() != 42 || name.substr(0, 10) != L"YCode.Run." ||
|
|
name.substr(10).find_first_not_of(L"0123456789abcdef") != std::wstring::npos) throw std::runtime_error("Invalid profile identity");
|
|
const HRESULT hr = CreateAppContainerProfile(name.c_str(), L"YCode isolated command", L"YCode per-command workspace snapshot", nullptr, 0, &sid);
|
|
if (FAILED(hr)) throw std::runtime_error("CreateAppContainerProfile: hresult=" + std::to_string(static_cast<unsigned long>(hr)));
|
|
}
|
|
bool remove() {
|
|
if (!sid) return true;
|
|
FreeSid(sid); sid = nullptr;
|
|
return SUCCEEDED(DeleteAppContainerProfile(name.c_str()));
|
|
}
|
|
~Profile() { remove(); }
|
|
};
|
|
static bool validProfile(const std::wstring& name) {
|
|
return name.size() == 42 && name.substr(0, 10) == L"YCode.Run." && name.substr(10).find_first_not_of(L"0123456789abcdef") == std::wstring::npos;
|
|
}
|
|
static HANDLE parentProcess() {
|
|
Handle snapshot(CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0));
|
|
require(snapshot.value != INVALID_HANDLE_VALUE, "CreateToolhelp32Snapshot");
|
|
PROCESSENTRY32W entry = {}; entry.dwSize = sizeof(entry);
|
|
require(Process32FirstW(snapshot.value, &entry) != FALSE, "Process32First");
|
|
do {
|
|
if (entry.th32ProcessID == GetCurrentProcessId()) {
|
|
HANDLE parent = OpenProcess(SYNCHRONIZE, FALSE, entry.th32ParentProcessID);
|
|
require(parent != nullptr, "Open parent monitor");
|
|
return parent;
|
|
}
|
|
} while (Process32NextW(snapshot.value, &entry));
|
|
throw std::runtime_error("Parent process identity unavailable");
|
|
}
|
|
struct Drive {
|
|
std::wstring name, target;
|
|
explicit Drive(const fs::path& stage) : target(L"\\??\\" + stage.wstring()) {
|
|
Handle mutex(CreateMutexW(nullptr, FALSE, L"Local\\YCode.Sandbox.DriveAllocator.v1"));
|
|
require(mutex.value != nullptr, "Create drive allocator mutex");
|
|
const DWORD lock = WaitForSingleObject(mutex.value, 5000);
|
|
if (lock != WAIT_OBJECT_0 && lock != WAIT_ABANDONED) throw std::runtime_error("Drive allocator busy");
|
|
for (wchar_t letter = L'Z'; letter >= L'D'; --letter) {
|
|
const std::wstring candidate = std::wstring(1, letter) + L":";
|
|
wchar_t existing[32768] = {};
|
|
if (QueryDosDeviceW(candidate.c_str(), existing, 32768)) continue;
|
|
if (DefineDosDeviceW(DDD_RAW_TARGET_PATH | DDD_NO_BROADCAST_SYSTEM, candidate.c_str(), target.c_str())) { name = candidate; break; }
|
|
}
|
|
ReleaseMutex(mutex.value);
|
|
if (name.empty()) throw std::runtime_error("No free drive for sandbox workspace");
|
|
}
|
|
bool release() {
|
|
if (name.empty()) return true;
|
|
const bool removed = DefineDosDeviceW(DDD_REMOVE_DEFINITION | DDD_EXACT_MATCH_ON_REMOVE | DDD_RAW_TARGET_PATH | DDD_NO_BROADCAST_SYSTEM, name.c_str(), target.c_str()) != FALSE;
|
|
if (removed) name.clear();
|
|
return removed;
|
|
}
|
|
~Drive() { release(); }
|
|
fs::path root() const { return fs::path(name + L"\\"); }
|
|
};
|
|
static std::vector<wchar_t> childEnvironment(const fs::path& stage) {
|
|
const auto win = windowsDirectory();
|
|
std::vector<std::wstring> entries = {
|
|
L"=" + stage.root_name().wstring() + L"=" + (stage / L"workspace").wstring(),
|
|
L"APPDATA=" + (stage / L"home/AppData/Roaming").wstring(), L"COMSPEC=" + win + L"\\System32\\cmd.exe",
|
|
L"HOME=" + (stage / L"home").wstring(), L"LOCALAPPDATA=" + (stage / L"home/AppData/Local").wstring(),
|
|
L"PATH=" + (stage / L"tools/node").wstring() + L";" + (stage / L"tools/python").wstring() + L";" + (stage / L"tools/git").wstring() + L";" + (stage / L"tools").wstring() + L";" + win + L"\\System32;" + win + L"\\System32\\WindowsPowerShell\\v1.0",
|
|
L"NODE_REPL_HISTORY=" + (stage / L"home/node_repl_history").wstring(),
|
|
L"NPM_CONFIG_CACHE=" + (stage / L"home/npm-cache").wstring(), L"NPM_CONFIG_OFFLINE=true",
|
|
L"NPM_CONFIG_USERCONFIG=" + (stage / L"home/npmrc").wstring(),
|
|
L"PYTHONHOME=" + (stage / L"tools/python").wstring(), L"PYTHONNOUSERSITE=1", L"PYTHONDONTWRITEBYTECODE=1",
|
|
L"PIP_CONFIG_FILE=NUL", L"PIP_NO_INPUT=1", L"PIP_NO_INDEX=1", L"PIP_DISABLE_PIP_VERSION_CHECK=1",
|
|
L"PIP_CACHE_DIR=" + (stage / L"home/pip-cache").wstring(),
|
|
L"PYTHONPATH=" + (stage / L"tools/python-adapter").wstring() + L";" + (stage / L"home/python-site").wstring(),
|
|
L"GIT_CONFIG_NOSYSTEM=1", L"GIT_CONFIG_GLOBAL=NUL", L"GIT_CONFIG_SYSTEM=NUL", L"GIT_TERMINAL_PROMPT=0",
|
|
L"GIT_OPTIONAL_LOCKS=0", L"GIT_PAGER=", L"GIT_EDITOR=" + (stage / L"tools/ycode-git-editor.exe").generic_wstring(), L"GIT_SEQUENCE_EDITOR=" + (stage / L"tools/ycode-git-editor.exe").generic_wstring(), L"GIT_ALLOW_PROTOCOL=",
|
|
L"GIT_EXEC_PATH=" + (stage / L"tools/git/empty-hooks").wstring(), L"GIT_TEMPLATE_DIR=" + (stage / L"tools/git/empty-template").wstring(),
|
|
L"GIT_CONFIG_COUNT=7", L"GIT_CONFIG_KEY_0=core.hooksPath", L"GIT_CONFIG_VALUE_0=" + (stage / L"tools/git/empty-hooks").wstring(),
|
|
L"GIT_CONFIG_KEY_1=core.fsmonitor", L"GIT_CONFIG_VALUE_1=false", L"GIT_CONFIG_KEY_2=credential.helper", L"GIT_CONFIG_VALUE_2=",
|
|
L"GIT_CONFIG_KEY_3=commit.gpgSign", L"GIT_CONFIG_VALUE_3=false", L"GIT_CONFIG_KEY_4=tag.gpgSign", L"GIT_CONFIG_VALUE_4=false",
|
|
L"GIT_CONFIG_KEY_5=maintenance.auto", L"GIT_CONFIG_VALUE_5=false",
|
|
L"GIT_CONFIG_KEY_6=diff.autoRefreshIndex", L"GIT_CONFIG_VALUE_6=false",
|
|
L"PATHEXT=.COM;.EXE;.BAT;.CMD", L"SystemRoot=" + win, L"TEMP=" + (stage / L"tmp").wstring(),
|
|
L"TMP=" + (stage / L"tmp").wstring(), L"USERPROFILE=" + (stage / L"home").wstring(),
|
|
L"WINDIR=" + win, L"YCODE_SANDBOX=appcontainer", L"YCODE_SANDBOX_BRIDGE=" + (stage / L"tools/ycode-bridge.cjs").wstring(),
|
|
L"YCODE_SANDBOX_PROCESS=" + (stage / L"tools/ycode-process.cjs").wstring(), L"YCODE_SANDBOX_ROOT=" + stage.wstring()
|
|
};
|
|
if (fs::is_directory(stage / L"repository")) {
|
|
entries.push_back(L"GIT_DIR=" + (stage / L"repository").wstring());
|
|
entries.push_back(L"GIT_WORK_TREE=" + (stage / L"workspace").wstring());
|
|
}
|
|
std::sort(entries.begin(), entries.end(), [](const auto& a, const auto& b) { return _wcsicmp(a.c_str(), b.c_str()) < 0; });
|
|
std::vector<wchar_t> block;
|
|
for (const auto& entry : entries) { block.insert(block.end(), entry.begin(), entry.end()); block.push_back(0); }
|
|
block.push_back(0);
|
|
return block;
|
|
}
|
|
static void forward(HANDLE input, HANDLE output) {
|
|
char buffer[16384]; DWORD read = 0;
|
|
while (ReadFile(input, buffer, sizeof(buffer), &read, nullptr) && read) {
|
|
DWORD offset = 0;
|
|
while (offset < read) {
|
|
DWORD written = 0;
|
|
if (!WriteFile(output, buffer + offset, read - offset, &written, nullptr) || !written) return;
|
|
offset += written;
|
|
}
|
|
}
|
|
}
|
|
static bool brokerInput(const std::string& line, std::string& decoded) {
|
|
if (line.size() > 65536 || line.rfind("input:", 0) != 0) return false;
|
|
const std::string alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
const auto encoded = line.substr(6);
|
|
if (encoded.empty() || encoded.size() % 4) return false;
|
|
unsigned int accumulator = 0; int bits = 0; bool padding = false; size_t paddingCount = 0;
|
|
for (size_t index = 0; index < encoded.size(); ++index) {
|
|
const char ch = encoded[index];
|
|
if (ch == '=') { padding = true; if (++paddingCount > 2 || index < encoded.size() - 2) return false; continue; }
|
|
const auto value = alphabet.find(ch);
|
|
if (padding || value == std::string::npos) return false;
|
|
accumulator = (accumulator << 6) | static_cast<unsigned int>(value); bits += 6;
|
|
if (bits >= 8) { bits -= 8; decoded += static_cast<char>((accumulator >> bits) & 255); }
|
|
}
|
|
return decoded.size() <= 32768 && bits == static_cast<int>(paddingCount) * 2 && (bits == 0 || (accumulator & ((1U << bits) - 1)) == 0);
|
|
}
|
|
struct AttributeList {
|
|
std::vector<BYTE> bytes;
|
|
LPPROC_THREAD_ATTRIBUTE_LIST list = nullptr;
|
|
AttributeList() {
|
|
SIZE_T size = 0; InitializeProcThreadAttributeList(nullptr, 2, 0, &size);
|
|
bytes.resize(size); list = reinterpret_cast<LPPROC_THREAD_ATTRIBUTE_LIST>(bytes.data());
|
|
require(InitializeProcThreadAttributeList(list, 2, 0, &size) != FALSE, "InitializeProcThreadAttributeList");
|
|
}
|
|
~AttributeList() { if (list) DeleteProcThreadAttributeList(list); }
|
|
};
|
|
namespace git_control { static std::vector<std::wstring> fields(const fs::path& path); }
|
|
static int run(const std::wstring& name, const fs::path& stage, const fs::path& report, DWORD timeout, bool broker = false) {
|
|
if (inContainer(GetCurrentProcess())) throw std::runtime_error("Runner cannot broker from inside an AppContainer");
|
|
if (!stage.is_absolute() || report.parent_path() != stage.parent_path() || report.filename() != L"result.json" ||
|
|
stage.filename() != L"stage" || fs::exists(report)) throw std::runtime_error("Invalid sandbox control layout");
|
|
nonReparse(stage);
|
|
privateDirectory(stage.parent_path());
|
|
Profile profile(name);
|
|
// Windows derives the AppContainer TEMP path from LOCALAPPDATA even when TEMP
|
|
// was supplied explicitly. Provision that private path before applying ACLs.
|
|
fs::create_directories(stage / L"home/AppData/Local/Packages" / name / L"AC/Temp");
|
|
grantStage(stage, profile.sid);
|
|
for (const auto& entry : fs::recursive_directory_iterator(stage)) {
|
|
if (GetFileAttributesW(entry.path().c_str()) & FILE_ATTRIBUTE_REPARSE_POINT) throw std::runtime_error("Reparse point in stage");
|
|
const auto relative = entry.path().lexically_relative(stage).generic_wstring();
|
|
const bool tool = relative == L"tools" || relative.rfind(L"tools/", 0) == 0 ||
|
|
relative == L"workspace/node_modules" || relative.rfind(L"workspace/node_modules/", 0) == 0 ||
|
|
relative == L"home/python-site" || relative.rfind(L"home/python-site/", 0) == 0;
|
|
grantStage(entry.path(), profile.sid, !tool);
|
|
}
|
|
Drive drive(stage);
|
|
writeJson(stage.parent_path() / L"lease.json", "{\"version\":1,\"pid\":" + std::to_string(GetCurrentProcessId()) +
|
|
",\"profile\":\"" + jsonText(name) + "\",\"drive\":\"" + jsonText(drive.name) + "\",\"stage\":\"" + jsonText(stage.wstring()) + "\"}\n");
|
|
Handle parent(parentProcess());
|
|
Handle job(CreateJobObjectW(nullptr, nullptr)); require(job.value != nullptr, "CreateJobObject");
|
|
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, "Job limits");
|
|
JOBOBJECT_BASIC_UI_RESTRICTIONS ui = {}; ui.UIRestrictionsClass = JOB_OBJECT_UILIMIT_ALL;
|
|
require(SetInformationJobObject(job.value, JobObjectBasicUIRestrictions, &ui, sizeof(ui)) != FALSE, "Job UI limits");
|
|
SECURITY_ATTRIBUTES inherited = {sizeof(inherited), nullptr, TRUE};
|
|
Handle outRead, outWrite, errRead, errWrite;
|
|
require(CreatePipe(&outRead.value, &outWrite.value, &inherited, 0) != FALSE, "stdout pipe");
|
|
require(CreatePipe(&errRead.value, &errWrite.value, &inherited, 0) != FALSE, "stderr pipe");
|
|
require(SetHandleInformation(outRead.value, HANDLE_FLAG_INHERIT, 0) != FALSE, "stdout inheritance");
|
|
require(SetHandleInformation(errRead.value, HANDLE_FLAG_INHERIT, 0) != FALSE, "stderr inheritance");
|
|
Handle input, inputWrite;
|
|
if (broker) {
|
|
require(CreatePipe(&input.value, &inputWrite.value, &inherited, 65536) != FALSE, "broker child input");
|
|
require(SetHandleInformation(inputWrite.value, HANDLE_FLAG_INHERIT, 0) != FALSE, "broker input writer inheritance");
|
|
} else input.value = CreateFileW(L"NUL", GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, &inherited, OPEN_EXISTING, 0, nullptr);
|
|
require(input.value != INVALID_HANDLE_VALUE, "NUL input");
|
|
AttributeList attributes;
|
|
SECURITY_CAPABILITIES caps = {}; caps.AppContainerSid = profile.sid;
|
|
require(UpdateProcThreadAttribute(attributes.list, 0, PROC_THREAD_ATTRIBUTE_SECURITY_CAPABILITIES, &caps, sizeof(caps), nullptr, nullptr) != FALSE, "AppContainer capability attribute");
|
|
HANDLE handles[] = {input.value, outWrite.value, errWrite.value};
|
|
require(UpdateProcThreadAttribute(attributes.list, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, handles, sizeof(handles), nullptr, nullptr) != FALSE, "Handle allowlist attribute");
|
|
STARTUPINFOEXW startup = {}; startup.StartupInfo.cb = sizeof(startup);
|
|
startup.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
|
|
startup.StartupInfo.hStdInput = input.value; startup.StartupInfo.hStdOutput = outWrite.value; startup.StartupInfo.hStdError = errWrite.value;
|
|
startup.lpAttributeList = attributes.list;
|
|
auto executable = windowsDirectory() + L"\\System32\\cmd.exe";
|
|
std::wstring command = quote(executable) + L" /d /s /c \"\"" + (drive.root() / L"command.cmd").wstring() + L"\"\"";
|
|
if (fs::exists(stage / L"git-arguments")) {
|
|
executable = (drive.root() / L"tools/git/git.exe").wstring(); command = quote(executable);
|
|
for (const auto& arg : git_control::fields(stage / L"git-arguments")) command += L" " + quote(arg);
|
|
if (command.size() > 32000) throw std::runtime_error("Isolated Git argv exceeds Windows limit");
|
|
}
|
|
auto environment = childEnvironment(drive.root());
|
|
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(), (drive.root() / L"workspace").c_str(), &startup.StartupInfo, &process) != FALSE, "Create AppContainer process");
|
|
Handle child(process.hProcess), thread(process.hThread);
|
|
if (!AssignProcessToJobObject(job.value, child.value) || !inContainer(child.value) || !matchesContainer(child.value, profile.sid)) {
|
|
TerminateProcess(child.value, 125); throw std::runtime_error("Child isolation verification failed");
|
|
}
|
|
CloseHandle(outWrite.value); outWrite.value = nullptr;
|
|
CloseHandle(errWrite.value); errWrite.value = nullptr;
|
|
const ULONGLONG started = GetTickCount64();
|
|
require(ResumeThread(thread.value) != static_cast<DWORD>(-1), "Resume isolated process");
|
|
std::thread stdoutThread(forward, outRead.value, GetStdHandle(STD_OUTPUT_HANDLE));
|
|
std::thread stderrThread(forward, errRead.value, GetStdHandle(STD_ERROR_HANDLE));
|
|
Handle cancelled(CreateEventW(nullptr, TRUE, FALSE, nullptr));
|
|
std::thread controlThread([&cancelled, &inputWrite, broker]() {
|
|
char buffer[4096] = {}; DWORD read = 0; std::string pending;
|
|
while (ReadFile(GetStdHandle(STD_INPUT_HANDLE), buffer, sizeof(buffer), &read, nullptr) && read) {
|
|
pending.append(buffer, read);
|
|
if (pending.size() > 65536) { SetEvent(cancelled.value); return; }
|
|
size_t newline = 0;
|
|
while ((newline = pending.find('\n')) != std::string::npos) {
|
|
const auto line = pending.substr(0, newline); pending.erase(0, newline + 1);
|
|
if (line == "cancel") { SetEvent(cancelled.value); return; }
|
|
std::string decoded;
|
|
if (!broker || !brokerInput(line, decoded)) { SetEvent(cancelled.value); return; }
|
|
DWORD written = 0;
|
|
if (!WriteFile(inputWrite.value, decoded.data(), static_cast<DWORD>(decoded.size()), &written, nullptr) || written != decoded.size()) { SetEvent(cancelled.value); return; }
|
|
}
|
|
}
|
|
});
|
|
HANDLE monitored[] = {child.value, parent.value, cancelled.value};
|
|
const DWORD waited = WaitForMultipleObjects(3, monitored, FALSE, timeout);
|
|
DWORD exit = 125;
|
|
const char* status = "error";
|
|
if (waited == WAIT_OBJECT_0) { GetExitCodeProcess(child.value, &exit); status = "completed"; }
|
|
else if (waited == WAIT_TIMEOUT) { exit = 124; status = "timeout"; }
|
|
else if (waited == WAIT_OBJECT_0 + 1) { exit = 130; status = "parent-exited"; }
|
|
else if (waited == WAIT_OBJECT_0 + 2) { exit = 130; status = "cancelled"; }
|
|
// No breakaway flags: close/terminate the job also stops still-running descendants.
|
|
const bool terminated = TerminateJobObject(job.value, exit) != FALSE;
|
|
WaitForSingleObject(child.value, 5000);
|
|
bool quiescent = false;
|
|
for (int attempt = 0; terminated && attempt < 500; ++attempt) {
|
|
JOBOBJECT_BASIC_ACCOUNTING_INFORMATION accounting = {};
|
|
if (!QueryInformationJobObject(job.value, JobObjectBasicAccountingInformation, &accounting, sizeof(accounting), nullptr)) break;
|
|
if (accounting.ActiveProcesses == 0) { quiescent = true; break; }
|
|
Sleep(10);
|
|
}
|
|
if (!quiescent) { status = "error"; exit = 125; }
|
|
CancelSynchronousIo(controlThread.native_handle());
|
|
controlThread.join();
|
|
stdoutThread.join(); stderrThread.join();
|
|
const bool cleaned = profile.remove();
|
|
const bool unmapped = drive.release();
|
|
const auto elapsed = GetTickCount64() - started;
|
|
const std::string json = "{\"protocol\":1,\"backend\":\"windows-appcontainer\",\"isolated\":true,\"network\":\"deny\",\"status\":\"" +
|
|
std::string(status) + "\",\"exitCode\":" + std::to_string(exit) + ",\"durationMs\":" + std::to_string(elapsed) +
|
|
",\"profileRemoved\":" + (cleaned ? "true" : "false") + ",\"driveRemoved\":" + (unmapped ? "true" : "false") + ",\"broker\":" + (broker ? "\"bound-stdio-v1\"" : "null") + "}\n";
|
|
writeJson(report, json);
|
|
return 0;
|
|
}
|
|
#include "filesystem-read.h"
|
|
#include "filesystem-write.h"
|
|
#include "workspace-state.h"
|
|
#include "git-control.h"
|
|
|
|
int wmain(int argc, wchar_t** argv) {
|
|
try {
|
|
if (_wcsicmp(fs::path(argv[0]).filename().c_str(), L"ycode-git-editor.exe") == 0) return 0;
|
|
if (_wcsicmp(fs::path(argv[0]).filename().c_str(), L"git-ssh.exe") == 0) return git_control::ssh(argc, argv);
|
|
if (argc == 2 && (std::wstring(argv[1]) == L"--git-protect" || std::wstring(argv[1]) == L"--git-unprotect")) return git_control::credential(std::wstring(argv[1]) == L"--git-protect");
|
|
if (argc == 4 && std::wstring(argv[1]) == L"--git-control") {
|
|
size_t end = 0; const auto timeout = std::stoul(argv[3], &end);
|
|
if (end != wcslen(argv[3]) || timeout > 600000) throw std::runtime_error("Invalid Git control timeout");
|
|
return git_control::run(argv[2], static_cast<DWORD>(timeout));
|
|
}
|
|
if (argc == 3 && std::wstring(argv[1]) == L"--seal-workspace") return workspace_state::run(argv[2]);
|
|
if (argc == 4 && (std::wstring(argv[1]) == L"--workspace-write" || std::wstring(argv[1]) == L"--git-metadata-write")) {
|
|
const std::wstring expected(argv[3]);
|
|
if (expected.find_first_not_of(L"0123456789abcdef:-") != std::wstring::npos) throw std::runtime_error("Invalid workspace identity");
|
|
std::string expectedBytes;
|
|
for (wchar_t ch : expected) expectedBytes += static_cast<char>(ch);
|
|
return workspace_write::run(argv[2], expectedBytes, std::wstring(argv[1]) == L"--git-metadata-write");
|
|
}
|
|
if (argc == 7 && std::wstring(argv[1]) == L"--workspace-read") {
|
|
const std::wstring expected(argv[5]);
|
|
if (expected.find_first_not_of(L"0123456789abcdef:-") != std::wstring::npos) throw std::runtime_error("Invalid workspace identity");
|
|
size_t end = 0; const auto limit = std::stoul(argv[6], &end);
|
|
if (end != wcslen(argv[6]) || limit > 8 * 1024 * 1024) throw std::runtime_error("Invalid workspace limit");
|
|
std::string expectedBytes;
|
|
for (wchar_t ch : expected) expectedBytes += static_cast<char>(ch);
|
|
return workspace_read::run(argv[2], argv[3], argv[4], expectedBytes, static_cast<DWORD>(limit));
|
|
}
|
|
if (argc == 2 && std::wstring(argv[1]) == L"--version") {
|
|
std::puts("{\"protocol\":1,\"backend\":\"windows-appcontainer\",\"version\":\"0.7.0\",\"network\":\"deny\"}"); return 0;
|
|
}
|
|
if (argc == 3 && std::wstring(argv[1]) == L"--cleanup") {
|
|
if (inContainer(GetCurrentProcess()) || !validProfile(argv[2])) throw std::runtime_error("Invalid profile cleanup request");
|
|
const HRESULT cleaned = DeleteAppContainerProfile(argv[2]);
|
|
return SUCCEEDED(cleaned) || cleaned == HRESULT_FROM_WIN32(ERROR_NOT_FOUND) || cleaned == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) || cleaned == HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND) ? 0 : 125;
|
|
}
|
|
if (argc == 4 && std::wstring(argv[1]) == L"--release-drive") {
|
|
const std::wstring drive = argv[2];
|
|
const fs::path stage(argv[3]);
|
|
if (inContainer(GetCurrentProcess()) || drive.size() != 2 || drive[0] < L'D' || drive[0] > L'Z' || drive[1] != L':' || !stage.is_absolute() || stage.filename() != L"stage") throw std::runtime_error("Invalid drive cleanup request");
|
|
const auto target = L"\\??\\" + stage.wstring();
|
|
const BOOL removed = DefineDosDeviceW(DDD_REMOVE_DEFINITION | DDD_EXACT_MATCH_ON_REMOVE | DDD_RAW_TARGET_PATH | DDD_NO_BROADCAST_SYSTEM, drive.c_str(), target.c_str());
|
|
return removed || GetLastError() == ERROR_FILE_NOT_FOUND ? 0 : 125;
|
|
}
|
|
const std::wstring mode = argc > 1 ? argv[1] : L"";
|
|
if (argc != 6 || (mode != L"--run" && mode != L"--run-stdio")) throw std::runtime_error("Usage: --run or --run-stdio profile stage result.json timeout-ms");
|
|
size_t end = 0; const auto timeout = std::stoul(argv[5], &end);
|
|
if (end != wcslen(argv[5]) || timeout < 100 || timeout > 600000) throw std::runtime_error("Invalid timeout");
|
|
return run(argv[2], fs::path(argv[3]), fs::path(argv[4]), static_cast<DWORD>(timeout), mode == L"--run-stdio");
|
|
} catch (const std::exception& error) { std::fprintf(stderr, "YCODE_SANDBOX_ERROR %s\n", error.what()); return 125; }
|
|
}
|