/* * Copyright (c) 2014-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. * */ #include #include #include #include "osquery/filesystem/fileops.h" #include "osquery/tests/test_util.h" namespace fs = boost::filesystem; namespace osquery { class FileOpsTests : public testing::Test { protected: void SetUp() override { createMockFileStructure(); } void TearDown() override { tearDownMockFileStructure(); } bool globResultsMatch(const std::vector& results, std::vector& expected) { if (results.size() == expected.size()) { size_t i = 0; for (auto const& path : results) { if (path != expected[i].make_preferred().string()) { return false; } i++; } return true; } return false; } }; class TempFile { public: TempFile() : path_((fs::temp_directory_path() / (std::string("osquery-") + std::to_string((rand() % 10000) + 20000))) .make_preferred() .string()) {} ~TempFile() { if (fs::exists(path_)) { fs::remove(path_); } } const std::string& path() const { return path_; } private: std::string path_; }; TEST_F(FileOpsTests, test_openFile) { TempFile tmp_file; std::string path = tmp_file.path(); { PlatformFile fd(path, PF_OPEN_EXISTING | PF_READ); EXPECT_FALSE(fd.isValid()); } { PlatformFile fd(path, PF_CREATE_NEW | PF_WRITE); EXPECT_TRUE(fd.isValid()); } { PlatformFile fd(path, PF_CREATE_NEW | PF_READ); EXPECT_FALSE(fd.isValid()); } fs::remove(path); { PlatformFile fd(path, PF_CREATE_ALWAYS | PF_READ); EXPECT_TRUE(fd.isValid()); } { PlatformFile fd(path, PF_CREATE_ALWAYS | PF_READ); EXPECT_TRUE(fd.isValid()); } { PlatformFile fd(path, PF_OPEN_EXISTING | PF_READ); EXPECT_TRUE(fd.isValid()); } } /* * This is a special function for testing file share operations on Windows. Our * PlatformFile as of now will only set FILE_SHARE_READ to play nicely with log * reading tools. However, we need to create one with FILE_SHARE_READ and * FILE_SHARE_WRITE for testing. */ std::unique_ptr openRWSharedFile(const std::string& path, int mode) { #ifdef WIN32 DWORD access_mask = -1; DWORD creation_disposition = -1; if (mode == (PF_OPEN_EXISTING | PF_READ)) { access_mask = PF_READ; creation_disposition = OPEN_EXISTING; } else if (mode == (PF_OPEN_ALWAYS | PF_WRITE)) { access_mask = PF_WRITE; creation_disposition = OPEN_ALWAYS; } HANDLE handle = ::CreateFileA(path.c_str(), access_mask, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, creation_disposition, 0, nullptr); return std::unique_ptr(new PlatformFile(handle)); #else return std::unique_ptr(new PlatformFile(path, mode)); #endif } TEST_F(FileOpsTests, test_shareRead) { TempFile tmp_file; std::string path = tmp_file.path(); const char* test1_data = "AAAABBBB"; const ssize_t test1_size = ::strlen(test1_data); { PlatformFile fd(path, PF_CREATE_NEW | PF_WRITE); EXPECT_TRUE(fd.isValid()); EXPECT_EQ(test1_size, fd.write(test1_data, test1_size)); } { auto reader_fd = openRWSharedFile(path, PF_OPEN_EXISTING | PF_READ); EXPECT_TRUE(reader_fd->isValid()); std::vector buf; buf.assign(test1_size, '\0'); EXPECT_EQ(test1_size, reader_fd->read(buf.data(), test1_size)); EXPECT_EQ(static_cast(test1_size), buf.size()); for (ssize_t i = 0; i < test1_size; i++) { EXPECT_EQ(test1_data[i], buf[i]); } PlatformFile fd(path, PF_OPEN_ALWAYS | PF_WRITE | PF_APPEND); EXPECT_TRUE(fd.isValid()); } } TEST_F(FileOpsTests, test_fileIo) { TempFile tmp_file; std::string path = tmp_file.path(); const char* expected_read = "AAAABBBBCCCCDDDD"; const ssize_t expected_read_len = ::strlen(expected_read); const ssize_t expected_write_len = ::strlen(expected_read); const size_t expected_buf_size = ::strlen(expected_read); { PlatformFile fd(path, PF_CREATE_NEW | PF_WRITE); EXPECT_TRUE(fd.isValid()); EXPECT_EQ(expected_write_len, fd.write(expected_read, expected_read_len)); } { std::vector buf(expected_read_len); PlatformFile fd(path, PF_OPEN_EXISTING | PF_READ); EXPECT_TRUE(fd.isValid()); EXPECT_FALSE(fd.isSpecialFile()); EXPECT_EQ(expected_read_len, fd.read(buf.data(), expected_read_len)); EXPECT_EQ(expected_buf_size, buf.size()); for (ssize_t i = 0; i < expected_read_len; i++) { EXPECT_EQ(expected_read[i], buf[i]); } } } TEST_F(FileOpsTests, test_append) { TempFile tmp_file; std::string path = tmp_file.path(); const char* test_data = "AAAABBBBCCCCDDDDD"; const ssize_t test_size = ::strlen(test_data); const ssize_t test1_size = 7; const ssize_t test2_size = test_size - test1_size; { PlatformFile fd(path, PF_OPEN_ALWAYS | PF_WRITE | PF_APPEND); EXPECT_TRUE(fd.isValid()); EXPECT_EQ(test1_size, fd.write(test_data, test1_size)); } { PlatformFile fd(path, PF_OPEN_ALWAYS | PF_WRITE | PF_APPEND); EXPECT_TRUE(fd.isValid()); EXPECT_EQ(test2_size, fd.write(&test_data[7], test2_size)); } { PlatformFile fd(path, PF_OPEN_EXISTING | PF_READ); EXPECT_TRUE(fd.isValid()); std::vector buf; buf.assign(test_size, '\0'); EXPECT_EQ(test_size, fd.read(buf.data(), test_size)); EXPECT_EQ(static_cast(test_size), buf.size()); for (ssize_t i = 0; i < test_size; i++) { EXPECT_EQ(test_data[i], buf[i]); } } } TEST_F(FileOpsTests, test_asyncIo) { TempFile tmp_file; std::string path = tmp_file.path(); const char* expected = "AAAABBBBCCCCDDDDEEEEFFFFGGGG"; const ssize_t expected_len = ::strlen(expected); { PlatformFile fd(path, PF_CREATE_NEW | PF_WRITE | PF_NONBLOCK); EXPECT_TRUE(fd.isValid()); EXPECT_EQ(expected_len, fd.write(expected, expected_len)); } { PlatformFile fd(path, PF_OPEN_EXISTING | PF_READ | PF_NONBLOCK); EXPECT_TRUE(fd.isValid()); EXPECT_FALSE(fd.isSpecialFile()); std::vector buf(expected_len); EXPECT_EQ(expected_len, fd.read(buf.data(), expected_len)); EXPECT_EQ(0, ::memcmp(expected, buf.data(), expected_len)); } { PlatformFile fd(path, PF_OPEN_EXISTING | PF_READ | PF_NONBLOCK); EXPECT_TRUE(fd.isValid()); EXPECT_FALSE(fd.isSpecialFile()); std::vector buf(expected_len); char* ptr = buf.data(); ssize_t part_bytes = 0; int iterations = 0; do { part_bytes = fd.read(ptr, 4); if (part_bytes > 0) { ptr += part_bytes; iterations++; } } while (part_bytes > 0); EXPECT_EQ(7, iterations); EXPECT_EQ(0, ::memcmp(expected, buf.data(), expected_len)); } } TEST_F(FileOpsTests, test_seekFile) { TempFile tmp_file; std::string path = tmp_file.path(); const char* expected = "AABBBBAACCCAAAAADDDDAAAAAAAA"; const ssize_t expected_len = ::strlen(expected); ssize_t expected_offs; { PlatformFile fd(path, PF_CREATE_ALWAYS | PF_WRITE); EXPECT_TRUE(fd.isValid()); EXPECT_EQ(expected_len, fd.write("AAAAAAAAAAAAAAAAAAAAAAAAAAAA", expected_len)); } // Cast to the proper type, off_t expected_offs = expected_len - 12; { PlatformFile fd(path, PF_OPEN_EXISTING | PF_WRITE); EXPECT_TRUE(fd.isValid()); EXPECT_EQ(expected_offs, fd.seek(-12, PF_SEEK_END)); EXPECT_EQ(4, fd.write("DDDD", 4)); EXPECT_EQ(2, fd.seek(2, PF_SEEK_BEGIN)); EXPECT_EQ(4, fd.write("BBBB", 4)); EXPECT_EQ(8, fd.seek(2, PF_SEEK_CURRENT)); EXPECT_EQ(3, fd.write("CCC", 3)); } { std::vector buffer(expected_len); PlatformFile fd(path, PF_OPEN_EXISTING | PF_READ); EXPECT_TRUE(fd.isValid()); EXPECT_EQ(expected_len, fd.read(buffer.data(), expected_len)); EXPECT_EQ(0, ::memcmp(buffer.data(), expected, expected_len)); } } TEST_F(FileOpsTests, test_large_read_write) { TempFile tmp_file; std::string path = tmp_file.path(); const std::string expected(200000000, 'A'); const ssize_t expected_len = expected.size(); { PlatformFile fd(path, PF_CREATE_ALWAYS | PF_WRITE); EXPECT_TRUE(fd.isValid()); auto write_len = fd.write(expected.c_str(), expected_len); EXPECT_EQ(expected_len, write_len); } { std::vector buffer(expected_len); PlatformFile fd(path, PF_OPEN_EXISTING | PF_READ); EXPECT_TRUE(fd.isValid()); auto read_len = fd.read(buffer.data(), expected_len); EXPECT_EQ(expected_len, read_len); EXPECT_EQ(expected, std::string(buffer.data())); } } TEST_F(FileOpsTests, test_chmod_no_exec) { TempFile tmp_file; std::string path = tmp_file.path(); { PlatformFile fd(path, PF_CREATE_ALWAYS | PF_WRITE); EXPECT_TRUE(fd.isValid()); EXPECT_EQ(4, fd.write("TEST", 4)); } EXPECT_TRUE(platformChmod(path, S_IRUSR | S_IWUSR | S_IROTH | S_IWOTH)); { PlatformFile fd(path, PF_OPEN_EXISTING | PF_READ); EXPECT_TRUE(fd.isValid()); auto status = fd.isExecutable(); EXPECT_TRUE(!status.ok()); EXPECT_EQ(1, status.getCode()); } } TEST_F(FileOpsTests, test_chmod_no_read) { TempFile tmp_file; std::string path = tmp_file.path(); { PlatformFile fd(path, PF_CREATE_ALWAYS | PF_WRITE); EXPECT_TRUE(fd.isValid()); EXPECT_EQ(4, fd.write("TEST", 4)); } EXPECT_TRUE(platformChmod(path, S_IWUSR | S_IWOTH)); { PlatformFile fd(path, PF_OPEN_EXISTING | PF_READ); EXPECT_FALSE(fd.isValid()); } { PlatformFile fd(path, PF_OPEN_EXISTING | PF_WRITE); EXPECT_TRUE(fd.isValid()); } } TEST_F(FileOpsTests, test_chmod_no_write) { TempFile tmp_file; std::string path = tmp_file.path(); { PlatformFile fd(path, PF_CREATE_ALWAYS | PF_WRITE); EXPECT_TRUE(fd.isValid()); EXPECT_EQ(4, fd.write("TEST", 4)); } EXPECT_TRUE(platformChmod(path, S_IRUSR | S_IROTH)); { PlatformFile fd(path, PF_OPEN_EXISTING | PF_READ); EXPECT_TRUE(fd.isValid()); } { PlatformFile fd(path, PF_OPEN_EXISTING | PF_WRITE); EXPECT_FALSE(fd.isValid()); } } TEST_F(FileOpsTests, test_safe_permissions) { const auto root_dir = (fs::temp_directory_path() / "safe-perms-test").string(); const auto temp_file = root_dir + "/test"; const int all_access = S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IWGRP | S_IXGRP | S_IROTH | S_IWOTH | S_IXOTH; fs::create_directories(root_dir); { PlatformFile fd(temp_file, PF_CREATE_ALWAYS | PF_WRITE); EXPECT_TRUE(fd.isValid()); EXPECT_TRUE( platformChmod(temp_file, S_IRUSR | S_IWGRP | S_IROTH | S_IWOTH)); EXPECT_TRUE(platformChmod(root_dir, S_IRUSR | S_IRGRP | S_IROTH)); auto status = fd.hasSafePermissions(); EXPECT_FALSE(status.ok()); EXPECT_EQ(1, status.getCode()); if (isPlatform(PlatformType::TYPE_POSIX)) { // On POSIX, chmod on a file requires +x on the parent directory EXPECT_TRUE(platformChmod(root_dir, all_access)); } EXPECT_TRUE(platformChmod(temp_file, S_IRUSR | S_IRGRP | S_IROTH)); EXPECT_TRUE(platformChmod(root_dir, S_IRUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH)); status = fd.hasSafePermissions(); if (isPlatform(PlatformType::TYPE_WINDOWS)) { EXPECT_FALSE(status.ok()); EXPECT_EQ(1, status.getCode()); } else { // On POSIX, we only check to see if temp_file has S_IWOTH EXPECT_TRUE(status.ok()); } if (isPlatform(PlatformType::TYPE_POSIX)) { // On POSIX, chmod on a file requires +x on the parent directory EXPECT_TRUE(platformChmod(root_dir, all_access)); } EXPECT_TRUE(platformChmod(temp_file, S_IRUSR | S_IRGRP | S_IROTH)); EXPECT_TRUE(platformChmod(root_dir, S_IRUSR | S_IRGRP | S_IWGRP | S_IROTH)); status = fd.hasSafePermissions(); if (isPlatform(PlatformType::TYPE_WINDOWS)) { EXPECT_FALSE(status.ok()); EXPECT_EQ(1, status.getCode()); } else { // On POSIX, we only check to see if temp_file has S_IWOTH EXPECT_TRUE(status.ok()); } if (isPlatform(PlatformType::TYPE_POSIX)) { // On POSIX, chmod on a file requires +x on the parent directory EXPECT_TRUE(platformChmod(root_dir, all_access)); } EXPECT_TRUE(platformChmod(temp_file, 0)); EXPECT_TRUE(platformChmod(root_dir, 0)); EXPECT_TRUE(fd.hasSafePermissions().ok()); if (isPlatform(PlatformType::TYPE_POSIX)) { // On POSIX, chmod on a file requires +x on the parent directory EXPECT_TRUE(platformChmod(root_dir, all_access)); } EXPECT_TRUE(platformChmod(temp_file, S_IRUSR | S_IRGRP | S_IROTH)); EXPECT_TRUE(platformChmod(root_dir, S_IRUSR | S_IRGRP | S_IROTH)); EXPECT_TRUE(fd.hasSafePermissions().ok()); } EXPECT_TRUE(platformChmod(root_dir, all_access)); EXPECT_TRUE(platformChmod(temp_file, all_access)); fs::remove_all(root_dir); } TEST_F(FileOpsTests, test_glob) { { std::vector expected{kFakeDirectory + "/door.txt", kFakeDirectory + "/root.txt", kFakeDirectory + "/root2.txt", kFakeDirectory + "/roto.txt"}; auto result = platformGlob(kFakeDirectory + "/*.txt"); EXPECT_TRUE(globResultsMatch(result, expected)); } { std::vector expected{kFakeDirectory + "/deep1/", kFakeDirectory + "/deep11/", kFakeDirectory + "/door.txt", kFakeDirectory + "/root.txt", kFakeDirectory + "/root2.txt", kFakeDirectory + "/roto.txt", kFakeDirectory + "/toplevel/"}; auto result = platformGlob(kFakeDirectory + "/*"); EXPECT_TRUE(globResultsMatch(result, expected)); } { std::vector expected{kFakeDirectory + "/deep1/deep2/", kFakeDirectory + "/deep1/level1.txt", kFakeDirectory + "/deep11/deep2/", kFakeDirectory + "/deep11/level1.txt", kFakeDirectory + "/deep11/not_bash", kFakeDirectory + "/toplevel/secondlevel1/", kFakeDirectory + "/toplevel/secondlevel2/", kFakeDirectory + "/toplevel/secondlevel3/"}; auto result = platformGlob(kFakeDirectory + "/*/*"); EXPECT_TRUE(globResultsMatch(result, expected)); } { std::vector expected{ kFakeDirectory + "/deep1/deep2/level2.txt", kFakeDirectory + "/deep11/deep2/deep3/", kFakeDirectory + "/deep11/deep2/level2.txt", kFakeDirectory + "/toplevel/secondlevel3/thirdlevel1/", }; auto result = platformGlob(kFakeDirectory + "/*/*/*"); EXPECT_TRUE(globResultsMatch(result, expected)); } { std::vector expected{kFakeDirectory + "/deep11/deep2/deep3/", kFakeDirectory + "/deep11/deep2/level2.txt"}; auto result = platformGlob(kFakeDirectory + "/*11/*/*"); EXPECT_TRUE(globResultsMatch(result, expected)); } { std::vector expected{kFakeDirectory + "/deep1/", kFakeDirectory + "/root.txt"}; auto result = platformGlob(kFakeDirectory + "/{deep,root}{1,.txt}"); EXPECT_TRUE(globResultsMatch(result, expected)); } { std::vector expected{kFakeDirectory + "/deep1/deep2/level2.txt", kFakeDirectory + "/deep11/deep2/deep3/", kFakeDirectory + "/deep11/deep2/level2.txt"}; auto result = platformGlob(kFakeDirectory + "/*/deep2/*"); EXPECT_TRUE(globResultsMatch(result, expected)); } { std::vector expected; if (isPlatform(PlatformType::TYPE_WINDOWS)) { expected = {kFakeDirectory + "/deep1/deep2/", kFakeDirectory + "/deep1/level1.txt", kFakeDirectory + "/deep11/deep2/", kFakeDirectory + "/deep11/level1.txt", kFakeDirectory + "/deep11/not_bash"}; } else { expected = {kFakeDirectory + "/deep1/deep2/", kFakeDirectory + "/deep11/deep2/", kFakeDirectory + "/deep1/level1.txt", kFakeDirectory + "/deep11/level1.txt", kFakeDirectory + "/deep11/not_bash"}; } auto result = platformGlob(kFakeDirectory + "/*/{deep2,level1,not_bash}{,.txt}"); EXPECT_TRUE(globResultsMatch(result, expected)); } } }