// Compile/run only on native Windows with the production LPAC bootstrap enabled. #include #ifndef NOMINMAX #define NOMINMAX #endif #include #include "common/worker_process.h" #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace docview; namespace { struct Fixture { QTemporaryDir directory{QDir::tempPath() + "/docview-win-probe-XXXXXX"}; QString source; QByteArray sourceBytes; std::vector sentinels; ~Fixture() { for (auto handle : sentinels) CloseHandle(handle); } bool prepare(const QString &mode, quint16 port = 0) { if (!directory.isValid()) return false; source = directory.filePath("source.json"); if (!QDir().mkpath(directory.filePath("config")) || !QDir().mkpath(directory.filePath("cache"))) return false; const auto put = [](const QString &path, const QByteArray &bytes) { QFile file(path); return file.open(QIODevice::WriteOnly) && file.write(bytes) == bytes.size(); }; const auto sibling = directory.filePath("neighbor.txt"), config = directory.filePath("config/user-settings.json"); if (!put(sibling, "TEMPORARY NEIGHBOR FIXTURE") || !put(config, "{\"privateFixture\":true}")) return false; QJsonArray handles; for (int i = 0; i < 2; ++i) { const auto path = directory.filePath(QString("sentinel-%1.txt").arg(i)); if (!put(path, "TEMPORARY INHERITANCE SENTINEL")) return false; SECURITY_ATTRIBUTES attributes{sizeof(attributes), nullptr, TRUE}; const auto name = QDir::toNativeSeparators(path).toStdWString(); const auto handle = CreateFileW(name.c_str(), GENERIC_READ, FILE_SHARE_READ, &attributes, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); if (handle == INVALID_HANDLE_VALUE) return false; sentinels.push_back(handle); BY_HANDLE_FILE_INFORMATION info{}; DWORD flags = 0; if (!GetFileInformationByHandle(handle, &info) || !GetHandleInformation(handle, &flags) || !(flags & HANDLE_FLAG_INHERIT)) return false; handles.append(QJsonObject{{"value", QString::number(reinterpret_cast(handle))}, {"volume", qint64(info.dwVolumeSerialNumber)}, {"high", qint64(info.nFileIndexHigh)}, {"low", qint64(info.nFileIndexLow)}}); } const QJsonObject input{{"mode", mode}, {"fixtureRoot", directory.path()}, {"sourcePath", source}, {"siblingPath", sibling}, {"configPath", config}, {"outputPath", directory.filePath("cache/worker-output.bin")}, {"loopbackPort", int(port)}, {"sentinelHandles", handles}}; sourceBytes = QJsonDocument(input).toJson(QJsonDocument::Compact); return put(source, sourceBytes); } }; QString probeExecutable() { return QDir(QCoreApplication::applicationDirPath()).filePath("docview-windows-worker-probe.exe"); } bool launch(WorkerProcess &worker, const Fixture &fixture) { return worker.start(probeExecutable(), {"--source", fixture.source}); } } class WindowsWorkerTests : public QObject { Q_OBJECT private slots: void initTestCase() { QVERIFY2(QFileInfo::exists(probeExecutable()), "Build/deploy docview-windows-worker-probe with its generated LPAC-compatible manifest and Qt dependencies"); // A missing/unavailable sandbox is a failed native release gate. No // skip or unrestricted-worker fallback is accepted in this suite. } void protectedReadinessAndReplyOrder() { Fixture fixture; QVERIFY(fixture.prepare("echo")); WorkerProcess worker("native-session", 37); QSignalSpy ready(&worker, &WorkerProcess::ready), failures(&worker, &WorkerProcess::failed); QList replies; QVERIFY(launch(worker, fixture)); worker.request("render", {{"ordinal", 1}}, [&](const QCborMap &r) { replies << r; }); worker.request("metadata", {{"ordinal", 2}}, [&](const QCborMap &r) { replies << r; }); QTRY_COMPARE_WITH_TIMEOUT(replies.size(), 2, 20000); QCOMPARE(ready.size(), 1); QCOMPARE(failures.size(), 0); for (int i = 0; i < 2; ++i) { QCOMPARE(replies[i].value("requestId").toInteger(), i + 2); // first ping is bootstrap QCOMPARE(replies[i].value("sessionId").toString(), "native-session"); QCOMPARE(replies[i].value("generation").toInteger(), 37); const auto result = replies[i].value("result").toMap(); QCOMPARE(result.value("received").toInteger(), i + 2); QCOMPARE(result.value("echo").toMap().value("ordinal").toInteger(), i + 1); QVERIFY(result.value("sandboxed").toBool()); } QVERIFY(worker.idle()); worker.stop(); QCOMPARE(failures.size(), 0); } void rejectsHostileRepliesAfterReady_data() { QTest::addColumn("mode"); for (const auto *mode : {"wrong-session", "wrong-generation", "wrong-request", "wrong-operation", "wrong-version", "oversize", "illegal-more"}) QTest::newRow(mode) << QString::fromLatin1(mode); } void rejectsHostileRepliesAfterReady() { QFETCH(QString, mode); Fixture fixture; QVERIFY(fixture.prepare(mode)); WorkerProcess worker("protected-session", 7); QSignalSpy ready(&worker, &WorkerProcess::ready), failures(&worker, &WorkerProcess::failed); int callbacks = 0; QVERIFY(launch(worker, fixture)); QTRY_COMPARE_WITH_TIMEOUT(ready.size(), 1, 15000); worker.request("render", {}, [&](const QCborMap &) { ++callbacks; }); worker.request("metadata", {}, [&](const QCborMap &) { ++callbacks; }); QTRY_COMPARE_WITH_TIMEOUT(failures.size(), 1, 10000); QCOMPARE(failures.first()[0].toString(), "E_WORKER_CRASH"); QCOMPARE(callbacks, 0); QVERIFY(worker.idle()); QTest::qWait(100); QCOMPARE(failures.size(), 1); } void classifiesProtectedExit_data() { QTest::addColumn("mode"); QTest::addColumn("code"); QTest::newRow("ordinary") << QString("crash") << QString("E_WORKER_CRASH"); QTest::newRow("sandbox") << QString("sandbox-unavailable") << QString("E_SANDBOX_UNAVAILABLE"); } void classifiesProtectedExit() { QFETCH(QString, mode); QFETCH(QString, code); Fixture fixture; QVERIFY(fixture.prepare(mode)); WorkerProcess worker("exit-session", 1); QSignalSpy ready(&worker, &WorkerProcess::ready), failures(&worker, &WorkerProcess::failed); int callbacks = 0; QVERIFY(launch(worker, fixture)); QTRY_COMPARE_WITH_TIMEOUT(ready.size(), 1, 15000); worker.request("render", {}, [&](const QCborMap &) { ++callbacks; }); QTRY_COMPARE_WITH_TIMEOUT(failures.size(), 1, 10000); QCOMPARE(failures.first()[0].toString(), code); QCOMPARE(callbacks, 0); QTest::qWait(100); QCOMPARE(failures.size(), 1); } void normalErrorAndStreamKeepProtocolUsable() { Fixture fixture; QVERIFY(fixture.prepare("stream")); WorkerProcess worker("stream-session", 3); QSignalSpy failures(&worker, &WorkerProcess::failed); QByteArray bytes; QStringList order; bool final = false; QVERIFY(launch(worker, fixture)); worker.request("extract", {{"path", "fixture"}}, [&](const QCborMap &r) { const auto result = r.value("result").toMap(); if (result.value("more").toBool()) { QVERIFY(!worker.idle()); QVERIFY(!final); bytes += result.value("data").toByteArray(); order << "chunk"; } else { final = true; QCOMPARE(result.value("size").toInteger(), 9); order << "final"; } }); worker.request("metadata", {}, [&](const QCborMap &r) { QVERIFY(final); QVERIFY(!r.value("result").toMap().value("receivedDuringStream").toBool()); order << "next"; }); QTRY_COMPARE_WITH_TIMEOUT(order.size(), 4, 20000); QCOMPARE(bytes, QByteArray("alphabeta")); QCOMPARE(order, (QStringList{"chunk", "chunk", "final", "next"})); QCOMPARE(failures.size(), 0); worker.stop(); Fixture errors; QVERIFY(errors.prepare("error")); WorkerProcess errorWorker("errors", 1); QSignalSpy errorFailures(&errorWorker, &WorkerProcess::failed); QList replies; QVERIFY(launch(errorWorker, errors)); errorWorker.request("open", {}, [&](const QCborMap &r) { replies << r; }); QTRY_COMPARE_WITH_TIMEOUT(replies.size(), 1, 20000); QCOMPARE(replies.first().value("error").toMap().value("code").toString(), "E_FIXTURE"); QCOMPARE(errorFailures.size(), 0); } void actualLpacOsBoundaries() { QTcpServer listener; QVERIFY(listener.listen(QHostAddress::LocalHost, 0)); QSignalSpy connections(&listener, &QTcpServer::newConnection); Fixture fixture; QVERIFY(fixture.prepare("probe-os", listener.serverPort())); WorkerProcess worker("boundary-session", 9); QSignalSpy ready(&worker, &WorkerProcess::ready), failures(&worker, &WorkerProcess::failed); QList replies; QVERIFY(launch(worker, fixture)); worker.request("probe", {}, [&](const QCborMap &r) { replies << r; }); QTRY_COMPARE_WITH_TIMEOUT(replies.size(), 1, 25000); QCOMPARE(ready.size(), 1); QCOMPARE(failures.size(), 0); const auto result = replies.first().value("result").toMap(); for (const auto *key : {"fixture_valid", "source_handle_write_denied", "source_reopen_write_denied", "parent_read_denied", "sibling_read_denied", "config_read_denied", "arbitrary_output_denied", "extra_file_handles_absent", "loopback_denied", "child_creation_denied", "own_profile_write_denied", "own_profile_registry_write_denied"}) { QVERIFY2(result.value(QString::fromLatin1(key)).toBool(), qPrintable(QString::fromLatin1(key) + ": " + QString::fromUtf8(QJsonDocument::fromVariant(result.toVariantMap()).toJson(QJsonDocument::Compact)))); } QCOMPARE(connections.size(), 0); worker.stop(); QFile source(fixture.source); QVERIFY(source.open(QIODevice::ReadOnly)); QCOMPARE(source.readAll(), fixture.sourceBytes); QVERIFY(!QFileInfo::exists(fixture.directory.filePath("cache/worker-output.bin"))); } void gracefulCloseRequiresReply_data() { QTest::addColumn("acknowledged"); QTest::newRow("reply-before-exit") << true; QTest::newRow("exit-without-reply") << false; } void gracefulCloseRequiresReply() { QFETCH(bool, acknowledged); Fixture fixture; QVERIFY(fixture.prepare(acknowledged ? "close-reply-exit" : "close-without-reply")); WorkerProcess worker("close-session", 1); QSignalSpy ready(&worker, &WorkerProcess::ready), failures(&worker, &WorkerProcess::failed); int closed = 0, afterClose = 0; QVERIFY(launch(worker, fixture)); QTRY_COMPARE_WITH_TIMEOUT(ready.size(), 1, 15000); worker.request("close", {}, [&](const QCborMap &reply) { QVERIFY(reply.value("result").toMap().value("closed").toBool()); ++closed; }); worker.request("metadata", {}, [&](const QCborMap &) { ++afterClose; }); if (acknowledged) { QTRY_COMPARE_WITH_TIMEOUT(closed, 1, 10000); QTest::qWait(100); QCOMPARE(failures.size(), 0); } else { QTRY_COMPARE_WITH_TIMEOUT(failures.size(), 1, 10000); QCOMPARE(failures.first()[0].toString(), "E_WORKER_CRASH"); QCOMPARE(closed, 0); } QCOMPARE(afterClose, 0); QVERIFY(worker.idle()); QVERIFY(!worker.isConnected()); } }; QTEST_GUILESS_MAIN(WindowsWorkerTests) #include "test_windows_worker.moc"