/* Userspace Virtual Filesystem Copyright (C) 2015 Sebastian Herbord. All rights reserved. This file is part of usvfs. usvfs is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. usvfs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with usvfs. If not, see . */ // this file depends on so many stuff that the easiest way is to include // pch.h from shared #include "pch.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace spd = spdlog; namespace ush = usvfs::shared; // name of a file to be created in the virtual fs. Shouldn't exist on disc but the // directory must exist static LPCSTR VIRTUAL_FILEA = "C:/np.exe"; static LPCWSTR VIRTUAL_FILEW = L"C:/np.exe"; // a real file on disc that has to exist static LPCSTR REAL_FILEA = "C:/windows/notepad.exe"; static LPCWSTR REAL_FILEW = L"C:/windows/notepad.exe"; static LPCSTR REAL_DIRA = "C:/windows/Logs"; static LPCWSTR REAL_DIRW = L"C:/windows/Logs"; static std::shared_ptr logger() { std::shared_ptr result = spdlog::get("test"); if (result.get() == nullptr) { result = spdlog::stdout_logger_mt("test"); } return result; } auto defaultUsvfsParams(const char* instanceName = "usvfs_test") { std::unique_ptr parameters{ usvfsCreateParameters(), &usvfsFreeParameters}; usvfsSetInstanceName(parameters.get(), instanceName); usvfsSetDebugMode(parameters.get(), true); usvfsSetLogLevel(parameters.get(), LogLevel::Debug); usvfsSetCrashDumpType(parameters.get(), CrashDumpsType::None); usvfsSetCrashDumpPath(parameters.get(), ""); return std::move(parameters); } class USVFSTest : public testing::Test { public: void SetUp() { SHMLogger::create("usvfs"); // need to initialize logging in the context of the dll usvfsInitLogging(); } void TearDown() { std::array buffer; while (SHMLogger::instance().tryGet(buffer.data(), buffer.size())) { std::cout << buffer.data() << std::endl; } SHMLogger::free(); } private: }; class USVFSTestWithReroute : public testing::Test { public: void SetUp() { SHMLogger::create("usvfs"); // need to initialize logging in the context of the dll usvfsInitLogging(); auto params = defaultUsvfsParams(); m_Context.reset(usvfsCreateHookContext(*params, ::GetModuleHandle(nullptr))); usvfs::RedirectionTreeContainer& tree = m_Context->redirectionTable(); tree.addFile( ush::string_cast(VIRTUAL_FILEW, ush::CodePage::UTF8).c_str(), usvfs::RedirectionDataLocal(REAL_FILEA)); } void TearDown() { std::array buffer; while (SHMLogger::instance().tryGet(buffer.data(), buffer.size())) { std::cout << buffer.data() << std::endl; } m_Context.reset(); SHMLogger::free(); } private: std::unique_ptr m_Context; }; class USVFSTestAuto : public testing::Test { public: void SetUp() { auto params = defaultUsvfsParams(); usvfsConnectVFS(params.get()); SHMLogger::create("usvfs"); } void TearDown() { usvfsDisconnectVFS(); std::array buffer; while (SHMLogger::instance().tryGet(buffer.data(), buffer.size())) { std::cout << buffer.data() << std::endl; } SHMLogger::free(); } private: }; TEST_F(USVFSTest, CanResizeRedirectiontree) { using usvfs::shared::MissingThrow; ASSERT_NO_THROW({ usvfs::RedirectionTreeContainer container("treetest_shm", 1024); for (char i = 'a'; i <= 'z'; ++i) { for (char j = 'a'; j <= 'z'; ++j) { std::string name = std::string(R"(C:\temp\)") + i + j; container.addFile(name, usvfs::RedirectionDataLocal("gaga"), false); } } ASSERT_EQ("gaga", container->node("C:") ->node("temp") ->node("aa", MissingThrow) ->data() .linkTarget); ASSERT_EQ("gaga", container->node("C:") ->node("temp") ->node("az", MissingThrow) ->data() .linkTarget); }); } /* TEST_F(USVFSTest, CreateFileHookReportsCorrectErrorOnMissingFile) { ASSERT_NO_THROW({ USVFSParameters params; USVFSInitParameters(¶ms, "usvfs_test", true, LogLevel::Debug, CrashDumpsType::None, ""); std::unique_ptr ctx(CreateHookContext(params, ::GetModuleHandle(nullptr))); HANDLE res = usvfs::hook_CreateFileW(VIRTUAL_FILEW , GENERIC_READ , FILE_SHARE_READ | FILE_SHARE_WRITE , nullptr , OPEN_EXISTING , FILE_ATTRIBUTE_NORMAL , nullptr); ASSERT_EQ(INVALID_HANDLE_VALUE, res); ASSERT_EQ(ERROR_FILE_NOT_FOUND, ::GetLastError()); }); } */ /* TEST_F(USVFSTestWithReroute, CreateFileHookRedirectsFile) { ASSERT_NE(INVALID_HANDLE_VALUE , usvfs::hook_CreateFileW(VIRTUAL_FILEW , GENERIC_READ , FILE_SHARE_READ | FILE_SHARE_WRITE , nullptr , OPEN_EXISTING , FILE_ATTRIBUTE_NORMAL , nullptr)); } */ TEST_F(USVFSTest, GetFileAttributesHookReportsCorrectErrorOnMissingFile) { ASSERT_NO_THROW({ try { auto params = defaultUsvfsParams(); std::unique_ptr ctx( usvfsCreateHookContext(*params, ::GetModuleHandle(nullptr))); DWORD res = usvfs::hook_GetFileAttributesW(VIRTUAL_FILEW); ASSERT_EQ(INVALID_FILE_ATTRIBUTES, res); ASSERT_EQ(ERROR_FILE_NOT_FOUND, ::GetLastError()); } catch (const std::exception& e) { logger()->error("Exception: {}", e.what()); throw; } }); } TEST_F(USVFSTest, GetFileAttributesHookRedirectsFile) { auto params = defaultUsvfsParams(); std::unique_ptr ctx( usvfsCreateHookContext(*params, ::GetModuleHandle(nullptr))); usvfs::RedirectionTreeContainer& tree = ctx->redirectionTable(); tree.addFile( ush::string_cast(VIRTUAL_FILEW, ush::CodePage::UTF8).c_str(), usvfs::RedirectionDataLocal(REAL_FILEA)); ASSERT_EQ(::GetFileAttributesW(REAL_FILEW), usvfs::hook_GetFileAttributesW(VIRTUAL_FILEW)); } /* TEST_F(USVFSTest, GetFullPathNameOnRegularCurrentDirectory) { USVFSParameters params; USVFSInitParameters(¶ms, "usvfs_test", true, LogLevel::Debug, CrashDumpsType::None, ""); std::unique_ptr ctx(CreateHookContext(params, ::GetModuleHandle(nullptr))); std::wstring expected = winapi::wide::getCurrentDirectory() + L"\\filename.txt"; DWORD bufferLength = 32767; std::unique_ptr buffer(new wchar_t[bufferLength]); LPWSTR filePart = nullptr; DWORD res = usvfs::hook_GetFullPathNameW(L"filename.txt", bufferLength, buffer.get(), &filePart); ASSERT_NE(0UL, res); ASSERT_EQ(expected, std::wstring(buffer.get())); }*/ // small wrapper to call usvfs::hook_NtOpenFile with a path // // at some point in time, changes were made to USVFS such that calling a hooked // function from a handle obtained from a non-hooked function would not work anymore, // meaning that function such as CreateFileW that have no hook equivalent cannot // be used to test hook functions // // this function is useful to simulate a CreateFileW by internally using the hook // version of NtOpenFile // HANDLE hooked_NtOpenFile(LPCWSTR path, ACCESS_MASK accessMask, ULONG shareAccess, ULONG openOptions) { constexpr size_t BUFFER_SIZE = 2048; IO_STATUS_BLOCK statusBlock; OBJECT_ATTRIBUTES attributes; attributes.SecurityDescriptor = 0; attributes.SecurityQualityOfService = 0; attributes.RootDirectory = 0; attributes.Attributes = 0; attributes.Length = sizeof(OBJECT_ATTRIBUTES); WCHAR stringBuffer[BUFFER_SIZE]; UNICODE_STRING string; string.Buffer = stringBuffer; lstrcpyW(stringBuffer, L"\\??\\"); lstrcatW(stringBuffer, path); string.Length = static_cast(lstrlenW(stringBuffer) * 2); string.MaximumLength = BUFFER_SIZE; attributes.ObjectName = &string; HANDLE ret = INVALID_HANDLE_VALUE; if (usvfs::hook_NtOpenFile(&ret, accessMask, &attributes, &statusBlock, shareAccess, openOptions) != STATUS_SUCCESS) { return INVALID_HANDLE_VALUE; } return ret; } TEST_F(USVFSTest, NtQueryDirectoryFileRegularFile) { auto params = defaultUsvfsParams(); std::unique_ptr ctx( usvfsCreateHookContext(*params, ::GetModuleHandle(nullptr))); HANDLE hdl = hooked_NtOpenFile(L"C:\\", FILE_GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT); ASSERT_NE(INVALID_HANDLE_VALUE, hdl); IO_STATUS_BLOCK status; char buffer[1024]; usvfs::hook_NtQueryDirectoryFile(hdl, nullptr, nullptr, nullptr, &status, buffer, 1024, FileDirectoryInformation, TRUE, nullptr, TRUE); ASSERT_EQ(STATUS_SUCCESS, status.Status); usvfs::hook_NtClose(hdl); } TEST_F(USVFSTest, NtQueryDirectoryFileFindsVirtualFile) { auto params = defaultUsvfsParams(); std::unique_ptr ctx( usvfsCreateHookContext(*params, ::GetModuleHandle(nullptr))); usvfs::RedirectionTreeContainer& tree = ctx->redirectionTable(); tree.addFile(L"C:\\np.exe", usvfs::RedirectionDataLocal(REAL_FILEA)); HANDLE hdl = hooked_NtOpenFile(L"C:\\", FILE_GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT); ASSERT_NE(INVALID_HANDLE_VALUE, hdl); IO_STATUS_BLOCK status; char buffer[1024]; usvfs::UnicodeString fileName(L"np.exe"); usvfs::hook_NtQueryDirectoryFile(hdl, nullptr, nullptr, nullptr, &status, buffer, 1024, FileDirectoryInformation, TRUE, static_cast(fileName), TRUE); FILE_DIRECTORY_INFORMATION* info = reinterpret_cast(buffer); ASSERT_EQ(STATUS_SUCCESS, status.Status); ASSERT_EQ(0, wcscmp(info->FileName, L"np.exe")); usvfs::hook_NtClose(hdl); } TEST_F(USVFSTest, NtQueryDirectoryFileExVirtualFile) { auto params = defaultUsvfsParams(); std::unique_ptr ctx( usvfsCreateHookContext(*params, ::GetModuleHandle(nullptr))); usvfs::RedirectionTreeContainer& tree = ctx->redirectionTable(); tree.addFile(L"C:\\0123456789.txt", usvfs::RedirectionDataLocal(REAL_FILEA)); tree.addFile(L"C:\\123456", usvfs::RedirectionDataLocal(REAL_FILEA)); tree.addFile(L"C:\\abcdef", usvfs::RedirectionDataLocal(REAL_FILEA)); tree.addFile(L"C:\\abcdefghijklmnopqrstuvwxyz.txt", usvfs::RedirectionDataLocal(REAL_FILEA)); HANDLE hdl = hooked_NtOpenFile(L"C:\\", FILE_GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT); ASSERT_NE(INVALID_HANDLE_VALUE, hdl); IO_STATUS_BLOCK status; constexpr size_t BUFFER_SIZE = 2048; char buffer[BUFFER_SIZE]; std::vector foundFiles; while (usvfs::hook_NtQueryDirectoryFileEx( hdl, nullptr, nullptr, nullptr, &status, buffer, BUFFER_SIZE, FileFullDirectoryInformation, 0, nullptr) == STATUS_SUCCESS) { std::size_t offset = 0; while (offset < BUFFER_SIZE) { const auto* info = reinterpret_cast(buffer + offset); foundFiles.emplace_back(info->FileName, info->FileNameLength / sizeof(wchar_t)); if (info->NextEntryOffset == 0) { break; // no more entries } offset += info->NextEntryOffset; } } ASSERT_THAT(foundFiles, ::testing::IsSupersetOf({L"0123456789.txt", L"123456", L"abcdef", L"abcdefghijklmnopqrstuvwxyz.txt"})); usvfs::hook_NtClose(hdl); } TEST_F(USVFSTest, NtQueryObjectVirtualFile) { std::wstring c_drive_device; { // find the device path for C: HANDLE hdl = ::CreateFileW( L"C:\\", GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS, nullptr); ASSERT_NE(INVALID_HANDLE_VALUE, hdl); char buffer[2048]; ASSERT_EQ(STATUS_SUCCESS, ::NtQueryObject(hdl, ObjectNameInformation, buffer, sizeof(buffer), nullptr)); OBJECT_NAME_INFORMATION* information = reinterpret_cast(buffer); c_drive_device = std::wstring(information->Name.Buffer, information->Name.Length / 2); ::CloseHandle(hdl); } auto params = defaultUsvfsParams(); std::unique_ptr ctx( usvfsCreateHookContext(*params, ::GetModuleHandle(nullptr))); usvfs::RedirectionTreeContainer& tree = ctx->redirectionTable(); tree.addFile(L"C:\\np.exe", usvfs::RedirectionDataLocal(REAL_FILEA)); HANDLE hdl = hooked_NtOpenFile(L"C:\\np.exe", FILE_GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, FILE_NON_DIRECTORY_FILE | FILE_OPEN_FOR_BACKUP_INTENT); ASSERT_NE(INVALID_HANDLE_VALUE, hdl) << "last error=" << ::GetLastError(); { char buffer[1024]; IO_STATUS_BLOCK status; const auto res = usvfs::hook_NtQueryInformationFile( hdl, &status, buffer, sizeof(buffer), FileNameInformation); ASSERT_EQ(STATUS_SUCCESS, res); ASSERT_EQ(STATUS_SUCCESS, status.Status); FILE_NAME_INFORMATION* fileNameInfo = reinterpret_cast(buffer); ASSERT_EQ(L"\\np.exe", std::wstring(fileNameInfo->FileName, fileNameInfo->FileNameLength / 2)); } { char buffer[1024]; IO_STATUS_BLOCK status; const auto res = usvfs::hook_NtQueryInformationFile( hdl, &status, buffer, sizeof(buffer), FileNormalizedNameInformation); ASSERT_EQ(STATUS_SUCCESS, res); ASSERT_EQ(STATUS_SUCCESS, status.Status); ASSERT_EQ(sizeof(ULONG) + 7 * 2, status.Information); FILE_NAME_INFORMATION* fileNameInfo = reinterpret_cast(buffer); ASSERT_EQ(L"\\np.exe", std::wstring(fileNameInfo->FileName, fileNameInfo->FileNameLength / 2)); } // buffer of size should be too small for the original path (\Windows\notepad.exe) // but not for \np.exe { // the required size should be sizeof(ULONG) + 7 * 2 but apparently that is // not enough for the CI so using 16 * 2 which should be large enough for // the hooked version, but still too short for the non-hooked one char buffer[sizeof(ULONG) + 7 * 2]; IO_STATUS_BLOCK status; NTSTATUS res; res = ::NtQueryInformationFile(hdl, &status, buffer, sizeof(buffer), FileNameInformation); ASSERT_EQ(STATUS_BUFFER_OVERFLOW, res); ASSERT_EQ(STATUS_BUFFER_OVERFLOW, status.Status); res = usvfs::hook_NtQueryInformationFile(hdl, &status, buffer, sizeof(buffer), FileNameInformation); ASSERT_EQ(STATUS_SUCCESS, res); ASSERT_EQ(STATUS_SUCCESS, status.Status); ASSERT_EQ(sizeof(ULONG) + 7 * 2, status.Information); FILE_NAME_INFORMATION* fileNameInfo = reinterpret_cast(buffer); ASSERT_EQ(L"\\np.exe", std::wstring(fileNameInfo->FileName, fileNameInfo->FileNameLength / 2)); } { char buffer[2048]; const auto res = usvfs::hook_NtQueryObject(hdl, ObjectNameInformation, buffer, sizeof(buffer), nullptr); ASSERT_EQ(STATUS_SUCCESS, res); OBJECT_NAME_INFORMATION* information = reinterpret_cast(buffer); ASSERT_EQ(c_drive_device + L"np.exe", std::wstring(information->Name.Buffer, information->Name.Length / sizeof(wchar_t))); } { // expected length is sizeof struct + size of path (in bytes), including the // null-character const auto expectedLength = sizeof(OBJECT_NAME_INFORMATION) + c_drive_device.size() * 2 + 12 + 2; ULONG requiredLength; NTSTATUS res; char buffer[2048]; res = usvfs::hook_NtQueryObject(hdl, ObjectNameInformation, buffer, sizeof(OBJECT_NAME_INFORMATION) - 1, &requiredLength); ASSERT_EQ(STATUS_INFO_LENGTH_MISMATCH, res); ASSERT_EQ(expectedLength, requiredLength); res = usvfs::hook_NtQueryObject(hdl, ObjectNameInformation, buffer, sizeof(OBJECT_NAME_INFORMATION), &requiredLength); ASSERT_EQ(STATUS_BUFFER_OVERFLOW, res); ASSERT_EQ(expectedLength, requiredLength); } usvfs::hook_NtClose(hdl); } TEST_F(USVFSTestAuto, CannotCreateLinkToFileInNonexistantDirectory) { ASSERT_EQ(FALSE, usvfsVirtualLinkFile( REAL_FILEW, L"c:/this_directory_shouldnt_exist/np.exe", FALSE)); } TEST_F(USVFSTestAuto, CanCreateMultipleLinks) { static LPCWSTR outFile = LR"(C:\np.exe)"; static LPCWSTR outDir = LR"(C:\logs)"; static LPCWSTR outDirCanonizeTest = LR"(C:\.\not/../logs\.\a\.\b\.\c\..\.\..\.\..\)"; ASSERT_EQ(TRUE, usvfsVirtualLinkFile(REAL_FILEW, outFile, 0)); ASSERT_EQ(TRUE, usvfsVirtualLinkDirectoryStatic(REAL_DIRW, outDir, 0)); // both file and dir exist and have the correct type ASSERT_NE(INVALID_FILE_ATTRIBUTES, usvfs::hook_GetFileAttributesW(outFile)); ASSERT_NE(INVALID_FILE_ATTRIBUTES, usvfs::hook_GetFileAttributesW(outDir)); ASSERT_EQ(0UL, usvfs::hook_GetFileAttributesW(outFile) & FILE_ATTRIBUTE_DIRECTORY); ASSERT_NE(0UL, usvfs::hook_GetFileAttributesW(outDir) & FILE_ATTRIBUTE_DIRECTORY); ASSERT_NE(0UL, usvfs::hook_GetFileAttributesW(outDirCanonizeTest) & FILE_ATTRIBUTE_DIRECTORY); } int main(int argc, char** argv) { using namespace test; auto dllPath = path_of_usvfs_lib(platform_dependant_executable("usvfs", "dll")); ScopedLoadLibrary loadDll(dllPath.c_str()); if (!loadDll) { std::wcerr << L"failed to load usvfs dll: " << dllPath.c_str() << L", " << GetLastError() << std::endl; return 1; } // note: this makes the logger available only to functions statically linked to the // test binary, not those called in the dll auto logger = spdlog::stdout_logger_mt("usvfs"); logger->set_level(spdlog::level::warn); testing::InitGoogleTest(&argc, argv); int res = RUN_ALL_TESTS(); return res; }