// Native-Windows hostile worker fixture. It must pass the production bootstrap. #include #ifndef NOMINMAX #define NOMINMAX #endif #include #include #include #include #include "common/ipc.h" #include "common/worker_runtime.h" #include #include #include #include #include #include #include #include #include #include using namespace docview; namespace { std::wstring native(const QString &path) { return QDir::toNativeSeparators(path).toStdWString(); } QCborMap response(const QCborMap &request, const QCborMap &result) { auto reply = request; reply.remove(QStringLiteral("payload")); reply.insert(QStringLiteral("result"), result); return reply; } bool raw(WorkerRuntime &runtime, const QCborMap &reply) { const auto bytes = QCborValue(reply).toCbor(); char header[4]; qToBigEndian(quint32(bytes.size()), header); return runtime.transport->write(header, 4) == 4 && runtime.transport->write(bytes) == bytes.size() && runtime.flushWrites(); } bool fixturePaths(const QJsonObject &fixture) { const QString root = QDir::fromNativeSeparators(fixture.value("fixtureRoot").toString()); if (!QDir::isAbsolutePath(root) || !QFileInfo(root).fileName().startsWith("docview-win-probe-") || QDir::cleanPath(root) != root || root.startsWith("//") || root.contains(QChar::Null)) return false; for (const auto &key : {"sourcePath", "siblingPath", "configPath", "outputPath"}) { const auto path = QDir::fromNativeSeparators(fixture.value(key).toString()); if (path.contains(QChar::Null) || QDir::cleanPath(path) != path || !path.startsWith(root + '/', Qt::CaseInsensitive)) return false; } return true; } bool deniedOpen(const QString &path, DWORD access, DWORD disposition, DWORD flags, DWORD *failure) { const auto wide = native(path); HANDLE handle = CreateFileW(wide.c_str(), access, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, disposition, flags | FILE_FLAG_OPEN_REPARSE_POINT, nullptr); if (handle != INVALID_HANDLE_VALUE) { CloseHandle(handle); *failure = 0; return false; } *failure = GetLastError(); return *failure == ERROR_ACCESS_DENIED || *failure == ERROR_PRIVILEGE_NOT_HELD; } QCborMap probeOs(WorkerRuntime &runtime, const QJsonObject &fixture, const QString &packageSid) { QCborMap result; if (!fixturePaths(fixture)) return {{"fixture_valid", false}}; result.insert(QStringLiteral("fixture_valid"), true); const auto checkOpen = [&](const char *label, const QString &path, DWORD access, DWORD disposition, DWORD flags = 0) { DWORD error = 0; const bool denied = deniedOpen(path, access, disposition, flags, &error); result.insert(QString::fromLatin1(label), denied); result.insert(QString::fromLatin1(label) + "_error", qint64(error)); }; DWORD written = 0; const bool nativeWrite = WriteFile(reinterpret_cast(_get_osfhandle(runtime.source.handle())), "X", 1, &written, nullptr); const DWORD writeError = nativeWrite ? 0 : GetLastError(); result.insert(QStringLiteral("source_handle_write_denied"), !nativeWrite && writeError == ERROR_ACCESS_DENIED); result.insert(QStringLiteral("source_handle_write_error"), qint64(writeError)); checkOpen("source_reopen_write_denied", fixture.value("sourcePath").toString(), GENERIC_WRITE, OPEN_EXISTING); if (result.value("source_reopen_write_denied_error").toInteger() == ERROR_SHARING_VIOLATION) result.insert(QStringLiteral("source_reopen_write_denied"), true); // broker's pinned read-only sharing also enforces this boundary checkOpen("parent_read_denied", fixture.value("fixtureRoot").toString(), FILE_LIST_DIRECTORY, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS); checkOpen("sibling_read_denied", fixture.value("siblingPath").toString(), GENERIC_READ, OPEN_EXISTING); checkOpen("config_read_denied", fixture.value("configPath").toString(), GENERIC_READ, OPEN_EXISTING); checkOpen("arbitrary_output_denied", fixture.value("outputPath").toString(), GENERIC_WRITE, CREATE_NEW); bool extraAbsent = true; const auto sentinels = fixture.value("sentinelHandles").toArray(); if (sentinels.size() != 2) extraAbsent = false; for (const auto &value : sentinels) { const auto item = value.toObject(); bool ok = false; const auto number = item.value("value").toString().toULongLong(&ok); BY_HANDLE_FILE_INFORMATION info{}; if (!ok || !number) { extraAbsent = false; continue; } const HANDLE handle = reinterpret_cast(static_cast(number)); // Handle numbers can be reused inside the child. Compare file identities, // not merely numeric validity, and never read the sentinel's content. if (GetFileType(handle) == FILE_TYPE_DISK && GetFileInformationByHandle(handle, &info) && info.dwVolumeSerialNumber == quint64(item.value("volume").toInteger()) && info.nFileIndexHigh == quint64(item.value("high").toInteger()) && info.nFileIndexLow == quint64(item.value("low").toInteger())) extraAbsent = false; } result.insert(QStringLiteral("extra_file_handles_absent"), extraAbsent); int networkError = 0; bool connected = false; WSADATA wsa{}; if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) networkError = WSAGetLastError(); else { SOCKET socket = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (socket == INVALID_SOCKET) networkError = WSAGetLastError(); else { u_long nonblocking = 1; ioctlsocket(socket, FIONBIO, &nonblocking); sockaddr_in address{}; address.sin_family = AF_INET; address.sin_addr.s_addr = htonl(INADDR_LOOPBACK); const int port = fixture.value("loopbackPort").toInt(); if (port < 1 || port > 65535) { closesocket(socket); WSACleanup(); return {{"fixture_valid", false}}; } address.sin_port = htons(static_cast(port)); connected = ::connect(socket, reinterpret_cast(&address), sizeof(address)) == 0; networkError = connected ? 0 : WSAGetLastError(); if (networkError == WSAEWOULDBLOCK) { fd_set writes, errors; FD_ZERO(&writes); FD_ZERO(&errors); FD_SET(socket, &writes); FD_SET(socket, &errors); timeval timeout{2, 0}; if (select(0, nullptr, &writes, &errors, &timeout) > 0) { int size = sizeof(networkError); if (getsockopt(socket, SOL_SOCKET, SO_ERROR, reinterpret_cast(&networkError), &size) == 0) connected = networkError == 0; } } closesocket(socket); } WSACleanup(); } result.insert(QStringLiteral("loopback_denied"), !connected && networkError == WSAEACCES); result.insert(QStringLiteral("loopback_error"), networkError); auto image = native(QCoreApplication::applicationFilePath()); auto command = std::wstring(L"\"") + image + L"\" --child-marker"; STARTUPINFOW startup{}; startup.cb = sizeof(startup); PROCESS_INFORMATION child{}; const bool spawned = CreateProcessW(image.c_str(), command.data(), nullptr, nullptr, FALSE, CREATE_NO_WINDOW, nullptr, nullptr, &startup, &child); const DWORD childError = spawned ? 0 : GetLastError(); if (spawned) { TerminateProcess(child.hProcess, 93); WaitForSingleObject(child.hProcess, 5000); CloseHandle(child.hThread); CloseHandle(child.hProcess); } result.insert(QStringLiteral("child_creation_denied"), !spawned && (childError == ERROR_ACCESS_DENIED || childError == ERROR_CHILD_PROCESS_BLOCKED || childError == ERROR_NOT_ENOUGH_QUOTA)); result.insert(QStringLiteral("child_creation_error"), qint64(childError)); PWSTR folder = nullptr; const auto sid = packageSid.toStdWString(); if (SUCCEEDED(GetAppContainerFolderPath(sid.c_str(), &folder)) && folder) { // This is the unique profile created for this protected test worker. checkOpen("own_profile_write_denied", QString::fromWCharArray(folder) + "/docview-probe-write.tmp", GENERIC_WRITE, CREATE_NEW); CoTaskMemFree(folder); } else result.insert(QStringLiteral("own_profile_write_denied"), false); HKEY registry = nullptr; const auto registryStatus = GetAppContainerRegistryLocation(KEY_SET_VALUE | KEY_CREATE_SUB_KEY, ®istry); bool registryDenied = registryStatus == HRESULT_FROM_WIN32(ERROR_ACCESS_DENIED); if (registryStatus == S_OK) { const DWORD marker = 1; const auto status = RegSetValueExW(registry, L"DocViewProbe", 0, REG_DWORD, reinterpret_cast(&marker), sizeof(marker)); registryDenied = status == ERROR_ACCESS_DENIED; if (status == ERROR_SUCCESS) RegDeleteValueW(registry, L"DocViewProbe"); RegCloseKey(registry); } result.insert(QStringLiteral("own_profile_registry_write_denied"), registryDenied); return result; } } int main(int argc, char **argv) { QCoreApplication app(argc, argv); if (app.arguments().contains("--child-marker")) return 92; QCommandLineParser parser; addWorkerOptions(parser); parser.process(app); QString error; auto runtime = startWorkerRuntime(parser, {}, &error); if (!runtime) return 5; if (runtime->source.size() < 2 || runtime->source.size() > 65536) return 120; const auto fixture = QJsonDocument::fromJson(runtime->source.readAll()).object(); const auto behavior = fixture.value("mode").toString(); if (behavior.isEmpty() || behavior.size() > 64) return 120; IpcChannel channel(runtime->transport.get()); QObject::connect(&channel, &IpcChannel::protocolError, &app, [] { std::_Exit(123); }); int received = 0; bool streaming = false; QObject::connect(&channel, &IpcChannel::messageReceived, &app, [&](const QCborMap &request) { ++received; // Broker readiness always verifies the production protected bootstrap, // even when a later request deliberately crashes or corrupts protocol. if (received == 1) { if (request.value("operation").toString() != "ping" || !channel.send(response(request, {{"ready", true}})) || !runtime->flushWrites()) std::_Exit(124); return; } if (behavior == "crash") std::_Exit(73); if (behavior == "sandbox-unavailable") std::_Exit(5); if (behavior == "close-without-reply") std::_Exit(0); if (behavior == "close-reply-exit" && request.value("operation").toString() == "close") { if (!raw(*runtime, response(request, {{"closed", true}}))) std::_Exit(125); std::_Exit(0); } if (behavior == "oversize") { char header[4]; qToBigEndian(MaxControlFrame + 1, header); if (runtime->transport->write(header, 4) != 4 || !runtime->flushWrites()) std::_Exit(124); return; } QCborMap result{{"received", received}, {"sandboxed", true}, {"echo", request.value("payload")}, {"receivedDuringStream", streaming}}; auto reply = response(request, result); if (behavior == "probe-os") reply = response(request, probeOs(*runtime, fixture, parser.value("sandbox-sid"))); else if (behavior == "wrong-session") reply.insert(QStringLiteral("sessionId"), "foreign"); else if (behavior == "wrong-generation") reply.insert(QStringLiteral("generation"), request.value("generation").toInteger() + 1); else if (behavior == "wrong-request") reply.insert(QStringLiteral("requestId"), request.value("requestId").toInteger() + 1); else if (behavior == "wrong-operation") reply.insert(QStringLiteral("operation"), "metadata"); else if (behavior == "wrong-version") reply.insert(QStringLiteral("protocolVersion"), 2); else if (behavior == "illegal-more") reply.insert(QStringLiteral("result"), QCborMap{{"more", true}}); else if (behavior == "error") { reply.remove(QStringLiteral("result")); reply.insert(QStringLiteral("error"), QCborMap{{"code", "E_FIXTURE"}, {"message", "fixture error"}}); } else if (behavior == "stream" && request.value("operation").toString() == "extract") { streaming = true; if (!raw(*runtime, response(request, {{"more", true}, {"data", QByteArray("alpha")}}))) std::_Exit(125); QTimer::singleShot(30, &app, [&, request] { if (!raw(*runtime, response(request, {{"more", true}, {"data", QByteArray("beta")}}))) std::_Exit(125); }); QTimer::singleShot(60, &app, [&, request] { streaming = false; if (!raw(*runtime, response(request, {{"more", false}, {"size", 9}}))) std::_Exit(125); }); return; } if (!raw(*runtime, reply)) std::_Exit(125); }); return app.exec(); }