initial commit
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
// Native-Windows hostile worker fixture. It must pass the production bootstrap.
|
||||
#include <QtGlobal>
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <winsock2.h>
|
||||
#include <windows.h>
|
||||
#include <userenv.h>
|
||||
#include <io.h>
|
||||
#include "common/ipc.h"
|
||||
#include "common/worker_runtime.h"
|
||||
#include <QCommandLineParser>
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QTimer>
|
||||
#include <QtEndian>
|
||||
#include <cstdlib>
|
||||
|
||||
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>(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<HANDLE>(_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<HANDLE>(static_cast<quintptr>(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<u_short>(port));
|
||||
connected = ::connect(socket, reinterpret_cast<sockaddr *>(&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<char *>(&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<const BYTE *>(&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<quint32>(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();
|
||||
}
|
||||
Reference in New Issue
Block a user