#pragma once #include #include #include #include // This host-only reader never grants a workspace handle to the sandbox. namespace workspace_read { struct Lifetime { Handle parent{parentProcess()}, done{CreateEventW(nullptr, TRUE, FALSE, nullptr)}; std::thread monitor; explicit Lifetime(DWORD timeout = 4000) { require(done.value != nullptr, "Create reader lifecycle event"); monitor = std::thread([this, timeout]() { HANDLE events[] = {parent.value, done.value}; const DWORD state = WaitForMultipleObjects(2, events, FALSE, timeout); if (state != WAIT_OBJECT_0 + 1) TerminateProcess(GetCurrentProcess(), 125); }); } ~Lifetime() { SetEvent(done.value); monitor.join(); } }; static bool component(const std::wstring& name) { if (name.empty() || name == L"." || name == L".." || name.back() == L'.' || name.back() == L' ') return false; for (wchar_t ch : name) if (ch < 32 || std::wstring(L"<>:\"/\\|?*").find(ch) != std::wstring::npos) return false; auto stem = name.substr(0, name.find(L'.')); std::transform(stem.begin(), stem.end(), stem.begin(), [](wchar_t ch) { return static_cast(towupper(ch)); }); if (stem == L"CON" || stem == L"PRN" || stem == L"AUX" || stem == L"NUL") return false; return !(stem.size() == 4 && (stem.substr(0, 3) == L"COM" || stem.substr(0, 3) == L"LPT") && stem[3] >= L'1' && stem[3] <= L'9'); } static std::vector parts(std::wstring value) { std::replace(value.begin(), value.end(), L'/', L'\\'); std::vector result; size_t start = 0; for (;;) { const auto end = value.find(L'\\', start); const auto name = value.substr(start, end == std::wstring::npos ? end : end - start); if (!component(name)) throw std::runtime_error("Invalid workspace path component"); result.push_back(name); if (end == std::wstring::npos) return result; start = end + 1; } } static std::wstring finalPath(HANDLE handle) { std::vector buffer(32768); const DWORD count = GetFinalPathNameByHandleW(handle, buffer.data(), static_cast(buffer.size()), FILE_NAME_NORMALIZED | VOLUME_NAME_GUID); require(count > 0 && count < buffer.size(), "Resolve workspace handle"); std::wstring path(buffer.data(), count); while (path.size() > 1 && path.back() == L'\\') path.pop_back(); return path; } static BY_HANDLE_FILE_INFORMATION information(HANDLE handle) { BY_HANDLE_FILE_INFORMATION result = {}; require(GetFileInformationByHandle(handle, &result) != FALSE, "Inspect workspace handle"); if (result.dwFileAttributes & (FILE_ATTRIBUTE_REPARSE_POINT | FILE_ATTRIBUTE_DEVICE)) throw std::runtime_error("Workspace reparse point or device denied"); if (!(result.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) && result.nNumberOfLinks != 1) throw std::runtime_error("Workspace hard link denied"); return result; } static std::string identity(HANDLE handle) { FILE_ID_INFO id = {}; require(GetFileInformationByHandleEx(handle, FileIdInfo, &id, sizeof(id)) != FALSE, "Identify workspace handle"); std::ostringstream text; text << std::hex << std::setfill('0') << std::setw(16) << id.VolumeSerialNumber << ':'; for (BYTE byte : id.FileId.Identifier) text << std::setw(2) << static_cast(byte); return text.str(); } struct Target { std::vector> held; fs::path path; std::wstring rootFinal; std::string rootIdentity; HANDLE open(bool directory) { // Read sharing only pins names against rename/delete and excludes writable handles, // including handles used to retarget a reparse point. All ancestors stay open. auto handle = std::make_unique(CreateFileW(path.c_str(), directory ? FILE_LIST_DIRECTORY | FILE_READ_ATTRIBUTES : GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, nullptr)); require(handle->value != INVALID_HANDLE_VALUE, "Open pinned workspace target"); const auto info = information(handle->value); if (directory && !(info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) throw std::runtime_error("Workspace ancestor is not a directory"); const auto result = handle->value; held.push_back(std::move(handle)); return result; } explicit Target(const std::wstring& root) { if (root.size() < 3 || root.size() > 1024 || !iswalpha(root[0]) || root[1] != L':' || root[2] != L'\\') throw std::runtime_error("Workspace requires a local drive path"); path = root.substr(0, 3); if (GetDriveTypeW(path.c_str()) != DRIVE_FIXED) throw std::runtime_error("Workspace requires a fixed local volume"); HANDLE last = open(true); wchar_t filesystem[32] = {}; require(GetVolumeInformationByHandleW(last, nullptr, 0, nullptr, nullptr, nullptr, filesystem, 32) != FALSE, "Inspect workspace volume"); if (_wcsicmp(filesystem, L"NTFS") != 0) throw std::runtime_error("Workspace reader currently requires NTFS"); if (root.size() > 3) for (const auto& name : parts(root.substr(3))) { path /= name; last = open(true); } rootFinal = finalPath(last); rootIdentity = identity(last); } HANDLE resolve(const std::wstring& relative) { if (relative == L".") return held.back()->value; const auto names = parts(relative); HANDLE last = nullptr; for (size_t index = 0; index < names.size(); ++index) { path /= names[index]; last = open(index + 1 < names.size()); } return last; } std::wstring relative(HANDLE handle) const { const auto actual = finalPath(handle); if (_wcsicmp(actual.c_str(), rootFinal.c_str()) == 0) return L"."; if (actual.size() <= rootFinal.size() || _wcsnicmp(actual.c_str(), rootFinal.c_str(), rootFinal.size()) != 0 || actual[rootFinal.size()] != L'\\') throw std::runtime_error("Workspace handle escaped root"); return actual.substr(rootFinal.size() + 1); } }; static std::string base64(const std::vector& bytes) { static const char* alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; std::string result; result.reserve((bytes.size() + 2) / 3 * 4); for (size_t index = 0; index < bytes.size(); index += 3) { const unsigned int value = (static_cast(bytes[index]) << 16) | (index + 1 < bytes.size() ? static_cast(bytes[index + 1]) << 8 : 0) | (index + 2 < bytes.size() ? bytes[index + 2] : 0); result += alphabet[value >> 18]; result += alphabet[(value >> 12) & 63]; result += index + 1 < bytes.size() ? alphabet[(value >> 6) & 63] : '='; result += index + 2 < bytes.size() ? alphabet[value & 63] : '='; } return result; } static int run(const std::wstring& operation, const std::wstring& root, const std::wstring& relative, const std::string& expected, DWORD limit) { if (inContainer(GetCurrentProcess())) throw std::runtime_error("Workspace reader is host-only"); Lifetime lifetime; if (operation != L"identity" && operation != L"read" && operation != L"stat" && operation != L"list") throw std::runtime_error("Invalid workspace reader operation"); if (limit == 0 || limit > 8 * 1024 * 1024) throw std::runtime_error("Invalid workspace reader limit"); Target target(root); if (operation != L"identity" && target.rootIdentity != expected) throw std::runtime_error("Workspace root identity changed"); const HANDLE handle = target.resolve(relative); const auto before = information(handle); const bool directory = (before.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0; const auto size = (static_cast(before.nFileSizeHigh) << 32) | before.nFileSizeLow; std::string result = "{\"protocol\":1,\"rootIdentity\":\"" + target.rootIdentity + "\",\"identity\":\"" + identity(handle) + "\",\"path\":\"" + jsonText(target.relative(handle)) + "\",\"kind\":\"" + (directory ? "directory" : "file") + "\",\"size\":" + std::to_string(size); if (operation == L"read") { if (directory) throw std::runtime_error("Workspace target is a directory"); if (size > limit) throw std::runtime_error("Workspace file exceeds read limit"); std::vector bytes(static_cast(size)); DWORD read = 0; if (!bytes.empty()) require(ReadFile(handle, bytes.data(), static_cast(bytes.size()), &read, nullptr) && read == bytes.size(), "Read pinned workspace file"); const auto after = information(handle); if (after.nNumberOfLinks != 1 || after.nFileSizeLow != before.nFileSizeLow || after.nFileSizeHigh != before.nFileSizeHigh || CompareFileTime(&before.ftLastWriteTime, &after.ftLastWriteTime) != 0) throw std::runtime_error("Workspace file changed while reading"); result += ",\"data\":\"" + base64(bytes) + "\""; } else if (operation == L"list") { if (!directory) throw std::runtime_error("Workspace target is not a directory"); result += ",\"entries\":["; std::vector buffer(65536); bool first = true; DWORD count = 0; for (;;) { if (!GetFileInformationByHandleEx(handle, FileIdBothDirectoryInfo, buffer.data(), static_cast(buffer.size()))) { if (GetLastError() == ERROR_NO_MORE_FILES) break; require(false, "Enumerate pinned workspace 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 directory record"); const std::wstring name(entry->FileName, entry->FileNameLength / sizeof(wchar_t)); if (name != L"." && name != L"..") { if (++count > limit) throw std::runtime_error("Workspace directory exceeds entry limit"); if (!first) result += ','; first = false; result += "{\"name\":\"" + jsonText(name) + "\",\"kind\":\"" + ((entry->FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) ? "link" : (entry->FileAttributes & FILE_ATTRIBUTE_DIRECTORY) ? "directory" : "file") + "\"}"; } 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 next directory record"); offset += entry->NextEntryOffset; } } result += ']'; } result += "}\n"; DWORD written = 0; require(WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), result.data(), static_cast(result.size()), &written, nullptr) && written == result.size(), "Write workspace reader result"); return 0; } }