#pragma once #include #include #include // Host-owned transaction channel. No workspace handles cross into an AppContainer. namespace workspace_write { static size_t fileLimit = 8 * 1024 * 1024; using CreateFileFn = NTSTATUS(NTAPI*)(PHANDLE, ACCESS_MASK, POBJECT_ATTRIBUTES, PIO_STATUS_BLOCK, PLARGE_INTEGER, ULONG, ULONG, ULONG, ULONG, PVOID, ULONG); using SetFileFn = NTSTATUS(NTAPI*)(HANDLE, PIO_STATUS_BLOCK, PVOID, ULONG, FILE_INFORMATION_CLASS); using ErrorFn = ULONG(NTAPI*)(NTSTATUS); template static Function nativeFunction(const char* name) { const auto address = GetProcAddress(GetModuleHandleW(L"ntdll.dll"), name); require(address != nullptr, "Resolve native transaction API"); Function result = nullptr; static_assert(sizeof(result) == sizeof(address)); memcpy(&result, &address, sizeof(result)); return result; } static bool nativeOk(NTSTATUS status) { if (status >= 0) return true; SetLastError(nativeFunction("RtlNtStatusToDosError")(status)); return false; } static HANDLE openRelative(HANDLE parent, const std::wstring& name, ACCESS_MASK access, bool directory, bool create = false) { UNICODE_STRING text = {}; text.Buffer = const_cast(name.data()); text.Length = static_cast(name.size() * sizeof(wchar_t)); text.MaximumLength = text.Length; OBJECT_ATTRIBUTES attributes = {}; attributes.Length = sizeof(attributes); attributes.RootDirectory = parent; attributes.ObjectName = &text; attributes.Attributes = 0x1040; // OBJ_CASE_INSENSITIVE | OBJ_DONT_REPARSE IO_STATUS_BLOCK status = {}; HANDLE handle = INVALID_HANDLE_VALUE; const ULONG options = 0x00200000 | 0x20 | (directory ? 1 : 0); // OPEN_REPARSE_POINT, synchronous, optional directory const auto result = nativeFunction("NtCreateFile")(&handle, access | SYNCHRONIZE, &attributes, &status, nullptr, FILE_ATTRIBUTE_NORMAL, directory ? FILE_SHARE_READ | FILE_SHARE_WRITE : FILE_SHARE_READ, create ? 2 : 1, options, nullptr, 0); return nativeOk(result) ? handle : INVALID_HANDLE_VALUE; } struct Root { std::vector> held; fs::path path; HANDLE handle = nullptr; std::wstring rootFinal; std::string rootIdentity; explicit Root(const std::wstring& input) { if (input.size() < 3 || input.size() > 1024 || !iswalpha(input[0]) || input[1] != L':' || input[2] != L'\\') throw std::runtime_error("Workspace requires a local drive path"); path = input.substr(0, 3); if (GetDriveTypeW(path.c_str()) != DRIVE_FIXED) throw std::runtime_error("Workspace requires a fixed local volume"); auto first = std::make_unique(CreateFileW(path.c_str(), FILE_LIST_DIRECTORY | FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, nullptr)); require(first->value != INVALID_HANDLE_VALUE, "Pin transaction volume"); wchar_t filesystem[32] = {}; require(GetVolumeInformationByHandleW(first->value, nullptr, 0, nullptr, nullptr, nullptr, filesystem, 32) != FALSE, "Inspect transaction volume"); if (_wcsicmp(filesystem, L"NTFS") != 0) throw std::runtime_error("Workspace transaction requires NTFS"); handle = first->value; held.push_back(std::move(first)); if (input.size() > 3) for (const auto& name : workspace_read::parts(input.substr(3))) { auto next = std::make_unique(openRelative(handle, name, FILE_LIST_DIRECTORY | FILE_READ_ATTRIBUTES, true)); require(next->value != INVALID_HANDLE_VALUE, "Pin transaction root ancestor"); workspace_read::information(next->value); handle = next->value; held.push_back(std::move(next)); path /= name; } rootFinal = workspace_read::finalPath(handle); rootIdentity = workspace_read::identity(handle); } std::wstring relative(HANDLE target) const { const auto actual = workspace_read::finalPath(target); if (actual.size() <= rootFinal.size() || _wcsnicmp(actual.c_str(), rootFinal.c_str(), rootFinal.size()) != 0 || actual[rootFinal.size()] != L'\\') throw std::runtime_error("Transaction handle escaped root"); return actual.substr(rootFinal.size() + 1); } }; static std::string hex(const BYTE* bytes, size_t size) { static const char* digits = "0123456789abcdef"; std::string result; result.reserve(size * 2); for (size_t index = 0; index < size; ++index) { result += digits[bytes[index] >> 4]; result += digits[bytes[index] & 15]; } return result; } static std::vector unhex(const std::string& text, size_t limit) { if (text.size() % 2 || text.size() > limit * 2) throw std::runtime_error("Invalid transaction field length"); std::vector result; result.reserve(text.size() / 2); auto digit = [](char ch) -> BYTE { if (ch >= '0' && ch <= '9') return static_cast(ch - '0'); if (ch >= 'a' && ch <= 'f') return static_cast(ch - 'a' + 10); throw std::runtime_error("Invalid transaction hex field"); }; for (size_t index = 0; index < text.size(); index += 2) result.push_back(static_cast((digit(text[index]) << 4) | digit(text[index + 1]))); return result; } static std::wstring pathField(const std::string& text) { const auto bytes = unhex(text, 4096); const int size = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, reinterpret_cast(bytes.data()), static_cast(bytes.size()), nullptr, 0); if (size <= 0 || size > 1024) throw std::runtime_error("Invalid transaction path encoding"); std::wstring result(size, 0); require(MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, reinterpret_cast(bytes.data()), static_cast(bytes.size()), result.data(), size) == size, "Decode transaction path"); return result; } static std::wstring key(std::wstring value) { std::transform(value.begin(), value.end(), value.begin(), [](wchar_t ch) { return static_cast(towlower(ch)); }); return value; } static std::string hash(const std::vector& bytes) { BCRYPT_ALG_HANDLE algorithm = nullptr; if (BCryptOpenAlgorithmProvider(&algorithm, BCRYPT_SHA256_ALGORITHM, nullptr, 0) < 0) throw std::runtime_error("Open transaction SHA256"); BYTE result[32] = {}; const auto status = BCryptHash(algorithm, nullptr, 0, const_cast(bytes.data()), static_cast(bytes.size()), result, sizeof(result)); BCryptCloseAlgorithmProvider(algorithm, 0); if (status < 0) throw std::runtime_error("Hash transaction contents"); return hex(result, sizeof(result)); } static std::vector contents(HANDLE handle) { const auto info = workspace_read::information(handle); const auto size = (static_cast(info.nFileSizeHigh) << 32) | info.nFileSizeLow; if ((info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) || size > fileLimit) throw std::runtime_error("Transaction file type or size denied"); std::vector bytes(static_cast(size)); DWORD count = 0; if (!bytes.empty()) require(ReadFile(handle, bytes.data(), static_cast(bytes.size()), &count, nullptr) && count == bytes.size(), "Read pinned transaction file"); return bytes; } static void reply(const std::string& text) { const auto line = text + '\n'; DWORD count = 0; require(WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), line.data(), static_cast(line.size()), &count, nullptr) && count == line.size(), "Write transaction response"); } struct Session { struct MissingParent : std::runtime_error { MissingParent() : std::runtime_error("Transaction parent missing") {} }; Root root; std::map> directories; std::map known; explicit Session(const std::wstring& path, const std::string& expected) : root(path) { if (expected != "-" && expected != root.rootIdentity) throw std::runtime_error("Workspace root identity changed"); } void validate(HANDLE handle, const std::wstring& relative, bool directory) { for (const auto& ancestor : root.held) workspace_read::information(ancestor->value); const auto info = workspace_read::information(handle); if (directory != ((info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0)) throw std::runtime_error("Transaction target type changed"); if (_wcsicmp(root.relative(handle).c_str(), relative.c_str()) != 0) throw std::runtime_error("Transaction final name changed"); } HANDLE parent(const std::vector& names) { HANDLE parentHandle = root.handle; std::wstring relative; workspace_read::information(parentHandle); for (size_t index = 0; index + 1 < names.size(); ++index) { if (!relative.empty()) relative += L'\\'; relative += names[index]; const auto name = key(relative); if (directories.count(name)) { parentHandle = directories[name]->value; workspace_read::information(parentHandle); continue; } if (directories.size() >= 10000) throw std::runtime_error("Transaction directory pin limit"); auto handle = std::make_unique(openRelative(parentHandle, names[index], FILE_LIST_DIRECTORY | FILE_READ_ATTRIBUTES, true)); if (handle->value == INVALID_HANDLE_VALUE && (GetLastError() == ERROR_FILE_NOT_FOUND || GetLastError() == ERROR_PATH_NOT_FOUND)) throw MissingParent(); require(handle->value != INVALID_HANDLE_VALUE, "Pin transaction ancestor"); validate(handle->value, relative, true); parentHandle = handle->value; directories.emplace(name, std::move(handle)); } return parentHandle; } void unpin(const std::wstring& relative) { const auto prefix = key(relative); for (auto entry = directories.begin(); entry != directories.end();) { if (entry->first == prefix || entry->first.compare(0, prefix.size() + 1, prefix + L'\\') == 0) entry = directories.erase(entry); else ++entry; } } std::string execute(const std::vector& fields) { if (fields.size() != 5) throw std::runtime_error("Invalid transaction frame"); const auto& operation = fields[0]; const auto relative = pathField(fields[1]); const auto names = workspace_read::parts(relative); const auto name = key(relative); HANDLE directory = nullptr; try { directory = parent(names); } catch (const MissingParent&) { if (operation == "cleanup") return "{\"ok\":true}"; if (operation == "read" || operation == "stat") return "{\"ok\":true,\"kind\":\"missing\"}"; throw; } const auto absolute = names.back(); const auto& nonce = fields[4]; if (operation == "write" || operation == "cleanup") { if (nonce.size() != 32 || nonce.find_first_not_of("0123456789abcdef") != std::string::npos) throw std::runtime_error("Invalid transaction mutation identity"); } const auto temporaryName = L".ycode-sandbox-" + std::wstring(nonce.begin(), nonce.end()); const auto temporary = temporaryName + L"-new.tmp"; const auto previous = temporaryName + L"-old.tmp"; if (operation == "cleanup") { const auto separator = fields[2].find(':'); if (separator == std::string::npos) throw std::runtime_error("Invalid transaction cleanup hashes"); const std::pair candidates[] = {{previous, fields[2].substr(0, separator)}, {temporary, fields[2].substr(separator + 1)}}; for (const auto& candidate : candidates) { Handle stale(openRelative(directory, candidate.first.wstring(), GENERIC_READ | DELETE, false)); if (stale.value == INVALID_HANDLE_VALUE && (GetLastError() == ERROR_FILE_NOT_FOUND || GetLastError() == ERROR_PATH_NOT_FOUND)) continue; require(stale.value != INVALID_HANDLE_VALUE, "Open transaction recovery scratch"); validate(stale.value, (fs::path(relative).parent_path() / candidate.first.filename()).wstring(), false); if (hash(contents(stale.value)) != candidate.second) throw std::runtime_error("Transaction scratch changed externally"); FILE_DISPOSITION_INFO disposition = {TRUE}; require(SetFileInformationByHandle(stale.value, FileDispositionInfo, &disposition, sizeof(disposition)) != FALSE, "Remove transaction recovery scratch"); } return "{\"ok\":true}"; } if (operation == "mkdir") { Handle created(openRelative(directory, absolute, FILE_LIST_DIRECTORY | FILE_READ_ATTRIBUTES, true, true)); require(created.value != INVALID_HANDLE_VALUE, "Create pinned transaction directory"); validate(created.value, relative, true); const auto identity = workspace_read::identity(created.value); directories[name] = std::make_unique(created.value); created.value = nullptr; known.erase(name); return "{\"ok\":true,\"identity\":\"" + identity + "\"}"; } if (operation == "rmdir") unpin(relative); const bool mutation = operation == "write" || operation == "unlink" || operation == "rmdir"; Handle target(openRelative(directory, absolute, GENERIC_READ | (mutation ? DELETE : 0), operation == "rmdir")); const DWORD error = target.value == INVALID_HANDLE_VALUE ? GetLastError() : ERROR_SUCCESS; const bool missing = error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND; if (target.value == INVALID_HANDLE_VALUE && !missing) { SetLastError(error); require(false, "Open pinned transaction target"); } if (operation == "read" || operation == "stat") { if (missing) { known.erase(name); return "{\"ok\":true,\"kind\":\"missing\"}"; } const bool isDirectory = (workspace_read::information(target.value).dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0; validate(target.value, relative, isDirectory); const auto identity = workspace_read::identity(target.value); known[name] = identity; if (isDirectory && !directories.count(name)) { if (directories.size() >= 10000) throw std::runtime_error("Transaction directory pin limit"); auto pinned = std::make_unique(openRelative(directory, absolute, FILE_LIST_DIRECTORY | FILE_READ_ATTRIBUTES, true)); require(pinned->value != INVALID_HANDLE_VALUE, "Pin inspected transaction directory"); validate(pinned->value, relative, true); if (workspace_read::identity(pinned->value) != identity) throw std::runtime_error("Inspected transaction directory identity changed"); directories[name] = std::move(pinned); } if (operation == "read" && isDirectory) throw std::runtime_error("Transaction target is a directory"); std::string result = "{\"ok\":true,\"kind\":\"" + std::string(isDirectory ? "directory" : "file") + "\",\"identity\":\"" + identity + "\""; if (operation == "read") result += ",\"data\":\"" + workspace_read::base64(contents(target.value)) + "\""; return result + '}'; } if (operation == "list") { if (missing) throw std::runtime_error("Transaction directory missing"); validate(target.value, relative, true); std::string result = "{\"ok\":true,\"entries\":["; bool first = true; size_t count = 0; std::vector buffer(65536); for (;;) { if (!GetFileInformationByHandleEx(target.value, FileIdBothDirectoryInfo, buffer.data(), static_cast(buffer.size()))) { if (GetLastError() == ERROR_NO_MORE_FILES) break; require(false, "Enumerate transaction directory"); } size_t offset = 0; for (;;) { const auto entry = reinterpret_cast(buffer.data() + offset); if (entry->FileNameLength > buffer.size() - offset - offsetof(FILE_ID_BOTH_DIR_INFO, FileName)) throw std::runtime_error("Invalid transaction directory record"); const std::wstring child(entry->FileName, entry->FileNameLength / sizeof(wchar_t)); if (child != L"." && child != L"..") { if (++count > 10000 || !workspace_read::component(child)) throw std::runtime_error("Transaction directory entry denied"); if (!first) result += ','; first = false; result += "\"" + jsonText(child) + "\""; } if (entry->NextEntryOffset == 0) break; if (entry->NextEntryOffset < sizeof(FILE_ID_BOTH_DIR_INFO) || entry->NextEntryOffset > buffer.size() - offset - sizeof(FILE_ID_BOTH_DIR_INFO)) throw std::runtime_error("Invalid transaction directory offset"); offset += entry->NextEntryOffset; } } return result + "]}"; } if (!mutation) throw std::runtime_error("Unknown transaction operation"); if (!missing) { validate(target.value, relative, operation == "rmdir"); if (operation != "rmdir" && (workspace_read::information(target.value).dwFileAttributes & FILE_ATTRIBUTE_READONLY)) throw std::runtime_error("Transaction read-only file denied"); const auto identity = workspace_read::identity(target.value); if (known.count(name) && known[name] != identity) throw std::runtime_error("Transaction target identity changed"); if (operation == "rmdir") { if (fields[2] != identity) throw std::runtime_error("Transaction directory identity changed"); } else if (fields[2] != hash(contents(target.value))) throw std::runtime_error("Transaction contents changed"); } else if (operation != "write" || fields[2] != "-") throw std::runtime_error("Transaction target disappeared"); if (operation == "unlink" || operation == "rmdir") { FILE_DISPOSITION_INFO disposition = {TRUE}; require(SetFileInformationByHandle(target.value, FileDispositionInfo, &disposition, sizeof(disposition)) != FALSE, "Delete pinned transaction target"); known.erase(name); return "{\"ok\":true}"; } const auto bytes = unhex(fields[3], fileLimit); Handle output(openRelative(directory, temporary, GENERIC_WRITE | DELETE | FILE_READ_ATTRIBUTES, false, true)); require(output.value != INVALID_HANDLE_VALUE, "Create transaction replacement"); bool published = false, moved = false; auto renameHandle = [directory](HANDLE handle, const fs::path& destination) { const auto name = destination.wstring(); std::vector storage(offsetof(FILE_RENAME_INFO, FileName) + name.size() * sizeof(wchar_t)); auto rename = reinterpret_cast(storage.data()); rename->ReplaceIfExists = FALSE; rename->RootDirectory = directory; rename->FileNameLength = static_cast(name.size() * sizeof(wchar_t)); memcpy(rename->FileName, name.data(), rename->FileNameLength); IO_STATUS_BLOCK status = {}; return nativeOk(nativeFunction("NtSetInformationFile")(handle, &status, rename, static_cast(storage.size()), static_cast(10))); }; try { DWORD count = 0; if (!bytes.empty()) require(WriteFile(output.value, bytes.data(), static_cast(bytes.size()), &count, nullptr) && count == bytes.size(), "Write transaction replacement"); require(FlushFileBuffers(output.value) != FALSE, "Flush transaction replacement"); validate(output.value, (fs::path(relative).parent_path() / fs::path(temporary).filename()).wstring(), false); const auto replacementIdentity = workspace_read::identity(output.value); // A no-overwrite handoff keeps the original handle exclusively pinned. // A competing basename creation fails publication instead of losing data. if (!missing) { require(renameHandle(target.value, previous), "Stage pinned transaction original"); moved = true; } require(renameHandle(output.value, absolute), "Publish pinned transaction replacement"); published = true; known[name] = replacementIdentity; if (moved) { FILE_DISPOSITION_INFO disposition = {TRUE}; require(SetFileInformationByHandle(target.value, FileDispositionInfo, &disposition, sizeof(disposition)) != FALSE, "Remove pinned transaction original"); } } catch (...) { FILE_DISPOSITION_INFO disposition = {TRUE}; if (!published) SetFileInformationByHandle(output.value, FileDispositionInfo, &disposition, sizeof(disposition)); if (moved && !published && !renameHandle(target.value, absolute)) throw std::runtime_error("Transaction handoff needs recovery; competing target preserved"); throw; } return "{\"ok\":true}"; } }; static int run(const std::wstring& root, const std::string& expected, bool gitMetadata = false) { if (gitMetadata) fileLimit = 128 * 1024 * 1024; if (inContainer(GetCurrentProcess())) throw std::runtime_error("Workspace transaction is host-only"); workspace_read::Lifetime lifetime(600000); Session session(root, expected); reply("{\"protocol\":1,\"rootIdentity\":\"" + session.root.rootIdentity + "\"}"); std::string line; size_t requests = 0; char buffer[16384]; DWORD count = 0; while (ReadFile(GetStdHandle(STD_INPUT_HANDLE), buffer, sizeof(buffer), &count, nullptr) && count) { for (DWORD index = 0; index < count; ++index) { if (buffer[index] != '\n') { if (line.size() >= fileLimit * 2 + 16384) throw std::runtime_error("Transaction frame limit"); line += buffer[index]; continue; } if (++requests > 100000) throw std::runtime_error("Transaction request limit"); std::vector fields; size_t start = 0; for (;;) { const auto end = line.find('\t', start); fields.push_back(line.substr(start, end == std::string::npos ? end : end - start)); if (end == std::string::npos) break; start = end + 1; } try { reply(session.execute(fields)); } catch (const std::exception& error) { std::string message; for (char ch : std::string(error.what())) { if (ch == '\\' || ch == '"') message += '\\'; message += ch; } reply("{\"ok\":false,\"error\":\"" + message + "\"}"); } line.clear(); } } if (!line.empty()) throw std::runtime_error("Truncated transaction frame"); return 0; } }