#include "web/resource_policy.h" #include "web/windows_resource_handles.h" #include #include #include #include #include #include using namespace docview; namespace { std::wstring native(const QString &path) { return QDir::toNativeSeparators(path).toStdWString(); } bool put(const QString &path, const QByteArray &bytes) { QFile file(path); return file.open(QIODevice::WriteOnly) && file.write(bytes) == bytes.size(); } bool junction(const QString &path, const QString &destination) { if (!QDir().mkpath(path)) return false; const auto name = native(path), target = std::wstring(L"\\??\\") + native(destination); winresource::Handle handle(CreateFileW(name.c_str(), GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr)); if (!handle.valid()) return false; struct Header { DWORD tag; WORD bytes, reserved, substituteOffset, substituteBytes, printOffset, printBytes; }; const size_t stringBytes = (target.size() + 1) * sizeof(wchar_t); std::vector storage(sizeof(Header) + stringBytes, 0); auto *header = reinterpret_cast
(storage.data()); header->tag = IO_REPARSE_TAG_MOUNT_POINT; header->bytes = static_cast(8 + stringBytes); header->substituteBytes = static_cast(target.size() * sizeof(wchar_t)); header->printOffset = header->substituteBytes; header->printBytes = 0; std::memcpy(storage.data() + sizeof(Header), target.c_str(), stringBytes); DWORD returned = 0; return DeviceIoControl(handle.get(), FSCTL_SET_REPARSE_POINT, storage.data(), static_cast(storage.size()), nullptr, 0, &returned, nullptr); } } class WindowsResourceTests : public QObject { Q_OBJECT private slots: void pathsAndNativeComponents() { for (const auto &name : {L"..", L"CON.txt", L"COM1", L"LPT\u00b2.log", L"asset:stream", L"a/b", L"name.", L"name ", L"a\\b"}) QVERIFY(!winresource::safeComponent(name)); QVERIFY(winresource::safeComponent(L"chapter.xhtml")); for (const auto &root : {QString("C:relative"), QString("//server/share"), QString("//?/C:/tmp")}) { QVERIFY(!openResource(root, "a")); ResourceWriter writer(root, "a"); QVERIFY(!writer.isValid()); } } void readsOnlyPinnedRegularFiles() { QTemporaryDir root; QVERIFY(root.isValid()); QVERIFY(put(root.filePath("a.txt"), "hello")); auto file = openResource(root.path(), "a.txt"); QVERIFY(file); QCOMPARE(file->readAll(), QByteArray("hello")); const auto name = native(root.filePath("a.txt")); winresource::Handle mutate(CreateFileW(name.c_str(), GENERIC_WRITE | DELETE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, 0, nullptr)); QVERIFY(!mutate.valid()); QVERIFY(!openResource(root.path(), "a.txt:stream")); QVERIFY(!openResource(root.path(), "../a.txt")); } void atomicPublishAndCancellation() { QTemporaryDir root; QVERIFY(root.isValid()); { ResourceWriter writer(root.path(), "nested/book.css"); QVERIFY(writer.isValid()); QVERIFY(writer.write("first")); QVERIFY(!openResource(root.path(), "nested/book.css")); QVERIFY(writer.write(" second")); QVERIFY(writer.commit()); QVERIFY(!writer.write("late")); } auto file = openResource(root.path(), "nested/book.css"); QVERIFY(file); QCOMPARE(file->readAll(), QByteArray("first second")); file.reset(); { ResourceWriter writer(root.path(), "nested/cancel.css"); QVERIFY(writer.write("discard")); QVERIFY(!writer.write(QByteArray(65537, 'x'))); } QVERIFY(!openResource(root.path(), "nested/cancel.css")); QCOMPARE(QDir(root.filePath("nested")).entryList(QDir::Files | QDir::Hidden), QStringList{"book.css"}); { ResourceWriter existing(root.path(), "nested/book.css"); QVERIFY(existing.isValid()); QVERIFY(existing.write("replacement")); QVERIFY(!existing.commit()); } file = openResource(root.path(), "nested/book.css"); QVERIFY(file); QCOMPARE(file->readAll(), QByteArray("first second")); } void rejectsHardLinksAndJunctions() { QTemporaryDir root, outside; QVERIFY(root.isValid()); QVERIFY(outside.isValid()); QVERIFY(put(outside.filePath("secret"), "unchanged")); const auto secret = native(outside.filePath("secret")), link = native(root.filePath("hard")); QVERIFY2(CreateHardLinkW(link.c_str(), secret.c_str(), nullptr), "An NTFS test volume is required"); QVERIFY(!openResource(root.path(), "hard")); { ResourceWriter writer(root.path(), "hard"); QVERIFY(writer.write("bad")); QVERIFY(!writer.commit()); } QVERIFY2(junction(root.filePath("junction"), outside.path()), "Creating a junction on the NTFS test volume failed"); QVERIFY(!openResource(root.path(), "junction/secret")); QVERIFY(!openResource(root.filePath("junction"), "secret")); { ResourceWriter writer(root.path(), "junction/secret"); QVERIFY(!writer.isValid()); } QFile unchanged(outside.filePath("secret")); QVERIFY(unchanged.open(QIODevice::ReadOnly)); QCOMPARE(unchanged.readAll(), QByteArray("unchanged")); unchanged.close(); // Remove the junction itself, never recursively visit its destination. QVERIFY(RemoveDirectoryW(native(root.filePath("junction")).c_str())); } void writerPinsEveryAncestorAgainstSwaps() { QTemporaryDir root; QVERIFY(root.isValid()); QVERIFY(QDir().mkpath(root.filePath("a/b"))); ResourceWriter writer(root.path(), "a/b/result"); QVERIFY(writer.isValid()); QVERIFY(!MoveFileExW(native(root.filePath("a")).c_str(), native(root.filePath("moved")).c_str(), 0)); winresource::Handle mutate(CreateFileW(native(root.filePath("a/b")).c_str(), GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr)); QVERIFY(!mutate.valid()); QVERIFY(writer.write("pinned")); QVERIFY(writer.commit()); auto file = openResource(root.path(), "a/b/result"); QVERIFY(file); QCOMPARE(file->readAll(), QByteArray("pinned")); } }; QTEST_GUILESS_MAIN(WindowsResourceTests) #include "test_windows_resources.moc"