| // Copyright 2024 The ChromiumOS Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <fcntl.h> |
| #include <sys/sysmacros.h> |
| |
| #include <cstddef> |
| #include <cstdint> |
| #include <memory> |
| #include <optional> |
| |
| #include <brillo/files/file_util.h> |
| |
| #include "base/files/file.h" |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/files/scoped_temp_dir.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/test/task_environment.h" |
| #include "base/time/time.h" |
| #include "gmock/gmock.h" |
| #include "gtest/gtest.h" |
| #include "secagentd/bpf/bpf_types.h" |
| #include "secagentd/bpf_skeleton_wrappers.h" |
| #include "secagentd/common.h" |
| #include "secagentd/daemon.h" |
| #include "secagentd/plugins.h" |
| #include "secagentd/proto/security_xdr_events.pb.h" |
| #include "secagentd/test/mock_batch_sender.h" |
| #include "secagentd/test/mock_bpf_skeleton.h" |
| #include "secagentd/test/mock_device_user.h" |
| #include "secagentd/test/mock_image_cache.h" |
| #include "secagentd/test/mock_message_sender.h" |
| #include "secagentd/test/mock_platform.h" |
| #include "secagentd/test/mock_policies_features_broker.h" |
| #include "secagentd/test/mock_process_cache.h" |
| #include "secagentd/test/test_utils.h" |
| |
| namespace secagentd::testing { |
| namespace pb = cros_xdr::reporting; |
| |
| using ::testing::_; |
| using ::testing::AllOf; // For combining matchers |
| using ::testing::AnyNumber; |
| using ::testing::ByMove; |
| using ::testing::DoAll; |
| using ::testing::Eq; |
| using ::testing::Field; |
| using ::testing::Ref; |
| using ::testing::Return; |
| using ::testing::SaveArg; |
| using ::testing::StrictMock; |
| |
| class FilePluginTestFixture : public ::testing::Test { |
| protected: |
| using BatchSenderType = MockBatchSender<std::string, |
| pb::XdrFileEvent, |
| pb::FileEventAtomicVariant>; |
| |
| static constexpr uint32_t kBatchIntervalS = |
| Daemon::kDefaultPluginBatchIntervalS; |
| static constexpr uint32_t kAsyncTimeoutS = |
| std::max((kBatchIntervalS / 10), 1u); |
| |
| static void SetPluginBatchSenderForTesting( |
| PluginInterface* plugin, std::unique_ptr<BatchSenderType> batch_sender) { |
| // This downcast here is very unfortunate but it avoids a lot of templating |
| // in the plugin interface and the plugin factory. The factory generally |
| // requires future cleanup to cleanly accommodate plugin specific dependency |
| // injections. |
| google::protobuf::internal::DownCast<FilePlugin*>(plugin) |
| ->SetBatchSenderForTesting(std::move(batch_sender)); |
| } |
| |
| void CreateExpectedBinaries() { |
| std::vector<std::string> paths = {"usr/sbin/dlp", "usr/sbin/secagentd"}; |
| |
| for (const auto& path_string : paths) { |
| base::FilePath path = |
| fake_root_.GetPath().Append(base::FilePath(path_string)); |
| |
| base::FilePath parent_dir = |
| path.DirName(); // Extract the parent directory. |
| if (!base::CreateDirectory(parent_dir)) { |
| FAIL() << "Failed to create directory: " << parent_dir.value(); |
| } |
| |
| base::File file(path, |
| base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE); |
| if (!file.IsValid()) { |
| FAIL() << "Failed to create file at path: " << path.value(); |
| } |
| } |
| } |
| |
| void SetupDirectoryForValidation( |
| const std::string& path_string, |
| bool is_file, |
| bpf::device_monitoring_type device_monitoring_type, |
| bpf::file_monitoring_mode file_monitoring_mode, |
| cros_xdr::reporting::SensitiveFileType sensitive_file_type, |
| std::optional<std::string> hardlinkPath = std::nullopt, |
| bool should_be_monitored = true) { |
| base::FilePath path = |
| fake_root_.GetPath().Append(base::FilePath(path_string)); |
| if (!is_file) { |
| ASSERT_TRUE(base::CreateDirectory(path)); |
| } else { |
| base::FilePath parent_dir = |
| path.DirName(); // Extract the parent directory. |
| if (!base::CreateDirectory(parent_dir)) { |
| FAIL() << "Failed to create directory: " << parent_dir.value(); |
| } |
| |
| base::File file(path, |
| base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE); |
| if (!file.IsValid()) { |
| FAIL() << "Failed to create file at path: " << path.value(); |
| } |
| } |
| |
| // Handle optional hardlink creation. |
| if (hardlinkPath.has_value()) { |
| base::FilePath hardlinkFilePath = |
| fake_root_.GetPath().Append(base::FilePath(hardlinkPath.value())); |
| if (!is_file) { |
| ASSERT_TRUE(base::CreateDirectory(hardlinkFilePath)); |
| } else { |
| base::FilePath parent_dir = hardlinkFilePath.DirName(); |
| if (!base::CreateDirectory(parent_dir)) { |
| FAIL() << "Failed to create directory: " << parent_dir.value(); |
| } |
| |
| base::File file(hardlinkFilePath, base::File::FLAG_CREATE_ALWAYS | |
| base::File::FLAG_WRITE); |
| if (!file.IsValid()) { |
| FAIL() << "Failed to create file at path: " << path.value(); |
| } |
| } |
| } |
| |
| base::stat_wrapper_t fileStat; |
| if (base::File::Stat(path, &fileStat) != 0) { |
| FAIL() << "Failed to get file stat"; |
| } |
| |
| // Expected value from UserspaceToKernelDeviceId(). |
| uint32_t expected_kernel_dev = |
| ((major(fileStat.st_dev) << 20) | minor(fileStat.st_dev)); |
| bpf::inode_dev_map_key expected_bpfMapKey = {.inode_id = fileStat.st_ino, |
| .dev_id = expected_kernel_dev}; |
| |
| int times = should_be_monitored ? 1 : 0; |
| EXPECT_CALL( |
| *platform_, |
| BpfMapUpdateElementByFd( |
| 42, ::testing::Truly([expected_bpfMapKey](const void* arg) -> bool { |
| auto* key = static_cast<const bpf::inode_dev_map_key*>(arg); |
| if (!(key->inode_id == expected_bpfMapKey.inode_id && |
| key->dev_id == expected_bpfMapKey.dev_id)) { |
| LOG(ERROR) << "BpfMapUpdateElementByFd (42) - Key:" |
| << " inode_id=" << key->inode_id |
| << ", dev_id=" << key->dev_id |
| << " | Expected: inode_id=" |
| << expected_bpfMapKey.inode_id |
| << ", dev_id=" << expected_bpfMapKey.dev_id; |
| } |
| return key->inode_id == expected_bpfMapKey.inode_id && |
| key->dev_id == expected_bpfMapKey.dev_id; |
| }), |
| ::testing::Truly([file_monitoring_mode, |
| sensitive_file_type](const void* arg) -> bool { |
| auto* settings = |
| static_cast<const bpf::file_monitoring_settings*>(arg); |
| if (!(settings->file_monitoring_mode == file_monitoring_mode && |
| settings->sensitive_file_type == sensitive_file_type)) { |
| LOG(ERROR) << "BpfMapUpdateElementByFd (42) - Settings:" |
| << " file_monitoring_mode=" |
| << settings->file_monitoring_mode |
| << ", sensitive_file_type=" |
| << static_cast<uint8_t>( |
| settings->sensitive_file_type) |
| << " | Expected: file_monitoring_mode=" |
| << file_monitoring_mode |
| << ", sensitive_file_type=" << sensitive_file_type; |
| } |
| return settings->file_monitoring_mode == file_monitoring_mode && |
| settings->sensitive_file_type == sensitive_file_type; |
| }), |
| 0)) |
| .Times(times); |
| |
| EXPECT_CALL( |
| *platform_, |
| BpfMapUpdateElementByFd( |
| 43, ::testing::Truly([expected_bpfMapKey](const void* arg) -> bool { |
| const auto* dev_id_arg = static_cast<const uint32_t*>(arg); |
| if (!(*dev_id_arg == expected_bpfMapKey.dev_id)) { |
| LOG(ERROR) << "BpfMapUpdateElementByFd (43) - Dev ID:" |
| << " dev_id=" << *dev_id_arg |
| << " | Expected: dev_id=" |
| << expected_bpfMapKey.dev_id; |
| } |
| return *dev_id_arg == expected_bpfMapKey.dev_id; |
| }), |
| ::testing::Truly([device_monitoring_type, file_monitoring_mode, |
| sensitive_file_type](const void* arg) -> bool { |
| auto* settings = |
| static_cast<const bpf::device_file_monitoring_settings*>(arg); |
| if (!(settings->device_monitoring_type == |
| device_monitoring_type && |
| settings->file_monitoring_mode == file_monitoring_mode && |
| settings->sensitive_file_type == sensitive_file_type)) { |
| LOG(ERROR) << "BpfMapUpdateElementByFd (43) - Settings:" |
| << " device_monitoring_type=" |
| << settings->device_monitoring_type |
| << ", file_monitoring_mode=" |
| << settings->file_monitoring_mode |
| << ", sensitive_file_type=" |
| << static_cast<uint8_t>( |
| settings->sensitive_file_type) |
| << " | Expected: device_monitoring_type=" |
| << device_monitoring_type |
| << ", file_monitoring_mode=" << file_monitoring_mode |
| << ", sensitive_file_type=" << sensitive_file_type; |
| } |
| return settings->device_monitoring_type == |
| device_monitoring_type && |
| settings->file_monitoring_mode == file_monitoring_mode && |
| settings->sensitive_file_type == sensitive_file_type; |
| }), |
| 0)) |
| .Times(::testing::AnyNumber()); |
| } |
| |
| void SetSHADetails(pb::FileEventAtomicVariant& pb_event, |
| const ImageCacheInterface::HashValue& hash_value) { |
| auto variant_type = pb_event.variant_type_case(); |
| pb::FileImage* image; |
| switch (variant_type) { |
| case pb::FileEventAtomicVariant::kSensitiveModify: |
| image = pb_event.mutable_sensitive_modify() |
| ->mutable_file_modify() |
| ->mutable_image_after(); |
| break; |
| |
| case pb::FileEventAtomicVariant::kSensitiveRead: |
| image = pb_event.mutable_sensitive_read() |
| ->mutable_file_read() |
| ->mutable_image(); |
| break; |
| |
| case pb::FileEventAtomicVariant::VARIANT_TYPE_NOT_SET: |
| FAIL() << "In SetSHADetails the passed in protobuff did not have a " |
| "variant " |
| "type set."; |
| break; |
| } |
| image->set_partial_sha256(hash_value.sha256_is_partial); |
| image->set_sha256(hash_value.sha256); |
| } |
| |
| void FilePluginCollectEvent( |
| std::unique_ptr<FilePlugin::FileEventValue> event) { |
| google::protobuf::internal::DownCast<FilePlugin*>(plugin_.get()) |
| ->CollectEvent(std::move(event)); |
| } |
| |
| void TearDown() override { |
| task_environment_.RunUntilIdle(); |
| brillo::DeleteFile(fake_root_.GetPath()); |
| } |
| |
| void SetUp() override { |
| // For unit tests run everything on a single thread. |
| ASSERT_TRUE(fake_root_.CreateUniqueTempDir()); |
| bpf_skeleton = std::make_unique<MockBpfSkeleton>(); |
| bpf_skeleton_ = bpf_skeleton.get(); |
| skel_factory_ = base::MakeRefCounted<MockSkeletonFactory>(); |
| skel_factory_ref_ = skel_factory_.get(); |
| message_sender_ = base::MakeRefCounted<MockMessageSender>(); |
| process_cache_ = base::MakeRefCounted<MockProcessCache>(); |
| image_cache_ = base::MakeRefCounted<MockImageCache>(); |
| |
| auto batch_sender = std::make_unique<BatchSenderType>(); |
| batch_sender_ = batch_sender.get(); |
| plugin_factory_ = std::make_unique<PluginFactory>(skel_factory_); |
| device_user_ = base::MakeRefCounted<MockDeviceUser>(); |
| |
| SetPlatform(std::make_unique<StrictMock<MockPlatform>>()); |
| platform_ = static_cast<StrictMock<MockPlatform>*>(GetPlatform().get()); |
| |
| plugin_ = FilePlugin::CreateForTesting( |
| skel_factory_, message_sender_, process_cache_, image_cache_, |
| policies_features_broker_, device_user_, kBatchIntervalS, |
| kAsyncTimeoutS, fake_root_.GetPath()); |
| EXPECT_NE(nullptr, plugin_); |
| SetPluginBatchSenderForTesting(plugin_.get(), std::move(batch_sender)); |
| |
| CreateExpectedBinaries(); |
| |
| EXPECT_CALL(*skel_factory_, |
| Create(Types::BpfSkeleton::kFile, _, kBatchIntervalS)) |
| .WillOnce( |
| DoAll(SaveArg<1>(&cbs_), Return(ByMove(std::move(bpf_skeleton))))); |
| |
| EXPECT_CALL(*bpf_skeleton_, |
| FindBpfMapByName("blocklisted_binary_inode_map")) |
| .WillOnce(Return(41)); |
| EXPECT_CALL(*platform_, BpfMapUpdateElementByFd(41, _, _, 0)) |
| .Times(2) |
| .WillRepeatedly(Return(0)); |
| |
| EXPECT_CALL(*bpf_skeleton_, FindBpfMapByName("predefined_allowed_inodes")) |
| .WillRepeatedly(Return(42)); |
| |
| EXPECT_CALL(*bpf_skeleton_, FindBpfMapByName("device_monitoring_allowlist")) |
| .WillRepeatedly(Return(43)); |
| EXPECT_CALL(*platform_, BpfMapLookupElementByFd(43, _, _)) |
| .WillRepeatedly(Return(145)); |
| |
| EXPECT_CALL(*bpf_skeleton_, FindBpfMapByName("system_flags_shared")) |
| .WillOnce(Return(44)); |
| EXPECT_CALL(*platform_, BpfMapUpdateElementByFd(44, _, _, 0)) |
| .Times(4) |
| .WillRepeatedly(Return(0)); |
| |
| EXPECT_CALL(*bpf_skeleton_, FindBpfMapByName("allowlisted_hardlink_inodes")) |
| .WillRepeatedly(Return(45)); |
| EXPECT_CALL(*platform_, BpfMapUpdateElementByFd(45, _, _, 0)) |
| .WillRepeatedly(Return(0)); |
| |
| EXPECT_CALL(*platform_, FindPidByName("cryptohome-namespace-mounter")) |
| .WillRepeatedly(Return(12345)); |
| |
| SetupDirectoryForValidation( |
| "", false, bpf::MONITOR_ALL_FILES, bpf::READ_WRITE_ONLY, |
| cros_xdr::reporting::SensitiveFileType::ROOT_FS); |
| SetupDirectoryForValidation( |
| "var/lib/devicesettings", false, bpf::MONITOR_SPECIFIC_FILES, |
| bpf::READ_WRITE_ONLY, |
| cros_xdr::reporting::SensitiveFileType::DEVICE_POLICY); |
| SetupDirectoryForValidation( |
| "var/lib/devicesettings/owner.key", true, bpf::MONITOR_SPECIFIC_FILES, |
| bpf::READ_WRITE_ONLY, |
| cros_xdr::reporting::SensitiveFileType::DEVICE_POLICY_PUBLIC_KEY); |
| SetupDirectoryForValidation( |
| "home/.shadow/cryptohome.key", true, bpf::MONITOR_SPECIFIC_FILES, |
| bpf::READ_AND_READ_WRITE_BOTH, |
| cros_xdr::reporting::SensitiveFileType::SYSTEM_TPM_PUBLIC_KEY); |
| |
| file_plugin_ = static_cast<FilePlugin*>(plugin_.get()); |
| EXPECT_TRUE(plugin_->Activate().ok()); |
| } |
| |
| std::unique_ptr<FilePlugin::FileEventValue> CreateFileEventValue( |
| std::unique_ptr<pb::FileEventAtomicVariant> pb, |
| std::string name, |
| uint32_t tag, |
| bool is_no_exec) { |
| auto rv = std::make_unique<FilePlugin::FileEventValue>(); |
| rv->event = std::move(pb); |
| rv->meta_data.file_name = name; |
| rv->meta_data.mtime.tv_nsec = tag; |
| rv->meta_data.mtime.tv_sec = tag * 10; |
| rv->meta_data.ctime.tv_nsec = tag * 100; |
| rv->meta_data.ctime.tv_sec = tag * 1000; |
| rv->meta_data.pid_for_setns = tag * 10000; |
| rv->meta_data.is_noexec = is_no_exec; |
| return rv; |
| } |
| |
| void TriggerUserLogin(std::string userhash) { |
| file_plugin_->OnUserLogin("device_user", userhash); |
| } |
| |
| void TriggerUserLogout(std::string userhash) { |
| file_plugin_->OnUserLogout(userhash); |
| } |
| |
| ImageCacheInterface::ImageCacheKeyType DeriveImageCacheKeyType( |
| const FilePlugin::FileEventValue& fev) { |
| auto& meta = fev.meta_data; |
| secagentd::ImageCacheInterface::ImageCacheKeyType image_key; |
| image_key.mtime.tv_nsec = meta.mtime.tv_nsec; |
| image_key.mtime.tv_sec = meta.mtime.tv_sec; |
| |
| image_key.ctime.tv_nsec = meta.ctime.tv_nsec; |
| image_key.ctime.tv_sec = meta.ctime.tv_sec; |
| |
| auto& pb_event = *fev.event; |
| const pb::FileImage* image; |
| if (pb_event.variant_type_case() == |
| pb::FileEventAtomicVariant::kSensitiveRead) { |
| image = &pb_event.sensitive_read().file_read().image(); |
| } else if (pb_event.variant_type_case() == |
| pb::FileEventAtomicVariant::kSensitiveModify) { |
| image = &pb_event.sensitive_modify().file_modify().image_after(); |
| } else { |
| return image_key; |
| } |
| image_key.inode = image->inode(); |
| image_key.inode_device_id = image->inode_device_id(); |
| return image_key; |
| } // DeriveImageCacheKeyType |
| |
| base::ScopedTempDir fake_root_; |
| scoped_refptr<MockSkeletonFactory> skel_factory_; |
| MockSkeletonFactory* skel_factory_ref_; |
| scoped_refptr<MockMessageSender> message_sender_; |
| scoped_refptr<MockProcessCache> process_cache_; |
| scoped_refptr<MockImageCache> image_cache_; |
| scoped_refptr<MockDeviceUser> device_user_; |
| scoped_refptr<MockPoliciesFeaturesBroker> policies_features_broker_; |
| BatchSenderType* batch_sender_; |
| std::unique_ptr<PluginFactory> plugin_factory_; |
| std::unique_ptr<MockBpfSkeleton> bpf_skeleton; |
| MockBpfSkeleton* bpf_skeleton_; |
| std::unique_ptr<PluginInterface> plugin_; |
| FilePlugin* file_plugin_; |
| StrictMock<MockPlatform>* platform_; |
| BpfCallbacks cbs_; |
| // Needed because FilePlugin creates a new sequenced task. |
| base::test::TaskEnvironment task_environment_{ |
| base::test::TaskEnvironment::TimeSource::MOCK_TIME}; |
| }; |
| |
| TEST_F(FilePluginTestFixture, TestGetName) { |
| EXPECT_EQ("File", plugin_->GetName()); |
| } |
| |
| TEST_F(FilePluginTestFixture, TestActivationFailureBadSkeleton) { |
| auto plugin = plugin_factory_->Create( |
| Types::Plugin::kFile, message_sender_, process_cache_, |
| policies_features_broker_, device_user_, kBatchIntervalS); |
| EXPECT_TRUE(plugin); |
| SetPluginBatchSenderForTesting(plugin.get(), |
| std::make_unique<BatchSenderType>()); |
| |
| // Set up expectations. |
| EXPECT_CALL(*skel_factory_, |
| Create(Types::BpfSkeleton::kFile, _, kBatchIntervalS)) |
| .WillOnce(Return(ByMove(nullptr))); |
| EXPECT_FALSE(plugin->Activate().ok()); |
| } |
| |
| TEST_F(FilePluginTestFixture, TestBPFEventIsAvailable) { |
| const bpf::cros_event file_close_event = { |
| .data.file_event = |
| { |
| .type = bpf::cros_file_event_type::kFileCloseEvent, |
| .data.file_detailed_event = {}, |
| }, |
| .type = bpf::kFileEvent, |
| }; |
| EXPECT_CALL(*bpf_skeleton_, ConsumeEvent()).Times(1); |
| // Notify the plugin that an event is available. |
| cbs_.ring_buffer_read_ready_callback.Run(); |
| EXPECT_CALL(*message_sender_, SendMessage).Times(AnyNumber()); |
| cbs_.ring_buffer_event_callback.Run(file_close_event); |
| } |
| |
| TEST_F(FilePluginTestFixture, TestWrongBPFEvent) { |
| EXPECT_CALL(*bpf_skeleton_, ConsumeEvent()).Times(1); |
| // Notify the plugin that an event is available. |
| cbs_.ring_buffer_read_ready_callback.Run(); |
| EXPECT_CALL(*message_sender_, SendMessage).Times(0); |
| cbs_.ring_buffer_event_callback.Run( |
| bpf::cros_event{.type = bpf::kProcessEvent}); |
| task_environment_.AdvanceClock( |
| base::Seconds(kBatchIntervalS + kAsyncTimeoutS)); |
| } |
| |
| TEST_F(FilePluginTestFixture, TestFilePathResolutionInCryptoHome) { |
| SetupDirectoryForValidation( |
| "proc/12345/root/home/chronos/u-1cyhmjegwxintuwmetwgehnexlobgoincsttxegg/" |
| "MyFiles", |
| false, bpf::MONITOR_SPECIFIC_FILES, bpf::READ_AND_READ_WRITE_BOTH, |
| cros_xdr::reporting::SensitiveFileType::USER_FILE, |
| "proc/12345/root/home/user/1cyhmjegwxintuwmetwgehnexlobgoincsttxegg/" |
| "MyFiles"); |
| |
| TriggerUserLogin("1cyhmjegwxintuwmetwgehnexlobgoincsttxegg"); |
| |
| base::stat_wrapper_t fileStat; |
| base::FilePath path = fake_root_.GetPath().Append( |
| base::FilePath("proc/12345/root/home/chronos/" |
| "u-1cyhmjegwxintuwmetwgehnexlobgoincsttxegg/" |
| "MyFiles")); |
| if (base::File::Stat(path, &fileStat) != 0) { |
| FAIL() << "Failed to get file stat"; |
| } |
| |
| // Expected value from UserspaceToKernelDeviceId(). |
| uint32_t expected_kernel_dev = |
| ((major(fileStat.st_dev) << 20) | minor(fileStat.st_dev)); |
| bpf::inode_dev_map_key expected_bpfMapKey = {.inode_id = fileStat.st_ino, |
| .dev_id = expected_kernel_dev}; |
| |
| EXPECT_CALL( |
| *platform_, |
| BpfMapDeleteElementByFd( |
| 42, ::testing::Truly([expected_bpfMapKey](const void* arg) -> bool { |
| auto* key = static_cast<const bpf::inode_dev_map_key*>(arg); |
| if (!(key->inode_id == expected_bpfMapKey.inode_id && |
| key->dev_id == expected_bpfMapKey.dev_id)) { |
| LOG(ERROR) << "BpfMapDeleteElementByFd - Key:" << " inode_id=" |
| << key->inode_id << ", dev_id=" << key->dev_id |
| << " | Expected: inode_id=" |
| << expected_bpfMapKey.inode_id |
| << ", dev_id=" << expected_bpfMapKey.dev_id; |
| } |
| return key->inode_id == expected_bpfMapKey.inode_id && |
| key->dev_id == expected_bpfMapKey.dev_id; |
| }))) |
| .Times(1); |
| |
| TriggerUserLogout("1cyhmjegwxintuwmetwgehnexlobgoincsttxegg"); |
| } |
| |
| TEST_F(FilePluginTestFixture, TestReadWriteCoalescing) { |
| // events will be a write, modify, modify, read, read |
| // all from the same process and all affecting the same file. |
| std::string process_uuid{"process1"}; |
| |
| // create the expected coalesced modify. |
| pb::FileEventAtomicVariant expected_modify; |
| expected_modify.mutable_common()->set_create_timestamp_us(1726708200); |
| pb::FileModifyEvent* file_modify_event = |
| expected_modify.mutable_sensitive_modify(); |
| file_modify_event->mutable_process()->set_process_uuid(process_uuid); |
| pb::FileModify* file_modify = file_modify_event->mutable_file_modify(); |
| file_modify->set_modify_type( |
| pb::FileModify_ModifyType_WRITE_AND_MODIFY_ATTRIBUTE); |
| pb::FileImage* file_image = file_modify->mutable_image_after(); |
| file_image->set_inode(64); |
| file_image->set_inode_device_id(164); |
| file_image->set_pathname("filename"); |
| file_image->set_canonical_gid(45); |
| file_image->set_canonical_uid(76); |
| file_image->set_mode(123); |
| file_image->set_sha256("expected_modify"); |
| file_image->set_partial_sha256(true); |
| |
| file_image = file_modify->mutable_attributes_before(); |
| file_image->set_mode(321); |
| // Done setting up expected modify |
| |
| // expected coalesced read (based off the expected modify). |
| pb::FileEventAtomicVariant expected_read; |
| expected_read.mutable_common()->set_create_timestamp_us(1726708500); |
| pb::FileReadEvent* file_read_event = expected_read.mutable_sensitive_read(); |
| file_read_event->mutable_process()->CopyFrom(file_modify_event->process()); |
| auto* mutable_read_image = |
| file_read_event->mutable_file_read()->mutable_image(); |
| mutable_read_image->CopyFrom(file_modify->image_after()); |
| mutable_read_image->set_sha256("expected_read"); |
| mutable_read_image->set_partial_sha256(false); |
| |
| // a write event with differing attributes on the after image. |
| std::unique_ptr<pb::FileEventAtomicVariant> event = |
| std::make_unique<pb::FileEventAtomicVariant>(expected_modify); |
| event->mutable_common()->set_create_timestamp_us(1726708200); |
| file_modify = event->mutable_sensitive_modify()->mutable_file_modify(); |
| file_modify->set_modify_type(pb::FileModify_ModifyType_WRITE); |
| file_modify->clear_attributes_before(); |
| file_image = file_modify->mutable_image_after(); |
| file_image->set_mode(001); |
| file_image->set_canonical_uid(999); |
| file_image->set_canonical_uid(456); |
| FilePluginCollectEvent( |
| CreateFileEventValue(std::move(event), "modify0", 1, true)); |
| |
| // a change attribute event with differing before attributes and differing |
| // attributes on the after image. |
| event = std::make_unique<pb::FileEventAtomicVariant>(expected_modify); |
| event->mutable_common()->set_create_timestamp_us(1726708300); |
| event->mutable_sensitive_modify()->mutable_file_modify()->set_modify_type( |
| pb::FileModify_ModifyType_MODIFY_ATTRIBUTE); |
| file_image = event->mutable_sensitive_modify() |
| ->mutable_file_modify() |
| ->mutable_image_after(); |
| file_image->set_mode(002); |
| file_image->set_canonical_uid(888); |
| file_image->set_canonical_uid(789); |
| |
| FilePluginCollectEvent( |
| CreateFileEventValue(std::move(event), "modify2", 2, true)); |
| |
| // a change attribute event with matching before attributes and matching |
| // attributes on the after image. |
| event = std::make_unique<pb::FileEventAtomicVariant>(expected_modify); |
| event->mutable_common()->set_create_timestamp_us(1726708400); |
| event->mutable_sensitive_modify()->mutable_file_modify()->set_modify_type( |
| pb::FileModify_ModifyType_MODIFY_ATTRIBUTE); |
| auto fev = CreateFileEventValue(std::move(event), "modify3", 3, true); |
| auto image_key = DeriveImageCacheKeyType(*fev); |
| EXPECT_CALL(*image_cache_, |
| InclusiveGetImage(image_key, false, fev->meta_data.pid_for_setns, |
| base::FilePath(fev->meta_data.file_name))) |
| .WillOnce(Return(ImageCacheInterface::HashValue{ |
| .sha256 = "expected_modify", .sha256_is_partial = true})); |
| |
| FilePluginCollectEvent(std::move(fev)); |
| // read event with differing attributes on the image. |
| event = std::make_unique<pb::FileEventAtomicVariant>(expected_read); |
| event->mutable_common()->set_create_timestamp_us(1726708500); |
| file_image = |
| event->mutable_sensitive_read()->mutable_file_read()->mutable_image(); |
| file_image->set_mode(456); |
| file_image->set_canonical_gid(314); |
| file_image->set_canonical_uid(654); |
| FilePluginCollectEvent( |
| CreateFileEventValue(std::move(event), "read1", 1, true)); |
| |
| // read event with expected attributes. |
| event = std::make_unique<pb::FileEventAtomicVariant>(expected_read); |
| event->mutable_common()->set_create_timestamp_us(1726708600); |
| fev = CreateFileEventValue(std::move(event), "read2", 2, true); |
| image_key = DeriveImageCacheKeyType(*fev); |
| EXPECT_CALL(*image_cache_, |
| InclusiveGetImage(image_key, false, fev->meta_data.pid_for_setns, |
| base::FilePath(fev->meta_data.file_name))) |
| .WillOnce(Return(ImageCacheInterface::HashValue{ |
| .sha256 = "expected_read", .sha256_is_partial = false})); |
| FilePluginCollectEvent(std::move(fev)); |
| // read1 and read2 are expected to be coalesced. |
| |
| EXPECT_CALL(*batch_sender_, |
| Enqueue(::testing::Pointee(EqualsProto(expected_modify)))); |
| EXPECT_CALL(*batch_sender_, |
| Enqueue(::testing::Pointee(EqualsProto(expected_read)))); |
| task_environment_.FastForwardBy( |
| base::Seconds(kBatchIntervalS + kAsyncTimeoutS)); |
| } |
| |
| TEST_F(FilePluginTestFixture, TestNoCoalescing) { |
| // Make sure that coalescing does not happen for events that have differing |
| // process uuid, inode, inode device id or are different event types |
| // e.g read/write. |
| |
| // create a set of expected modifies which vary from the base modify by |
| // process uuid, inode or inode device id. |
| pb::FileEventAtomicVariant expected_modify1; |
| pb::FileModifyEvent* file_modify_event = |
| expected_modify1.mutable_sensitive_modify(); |
| file_modify_event->mutable_process()->set_process_uuid("process1"); |
| pb::FileImage* file_image = |
| file_modify_event->mutable_file_modify()->mutable_image_after(); |
| file_image->set_inode(64); |
| file_image->set_inode_device_id(164); |
| file_image->set_pathname("filename1"); |
| file_image->set_sha256("test"); |
| auto event = std::make_unique<pb::FileEventAtomicVariant>(expected_modify1); |
| |
| FilePluginCollectEvent( |
| CreateFileEventValue(std::move(event), "modify1", 1, true)); |
| EXPECT_CALL(*batch_sender_, |
| Enqueue(::testing::Pointee(EqualsProto(expected_modify1)))); |
| // Done setting up expected modify |
| |
| pb::FileEventAtomicVariant expected_modify2(expected_modify1); |
| expected_modify2.mutable_sensitive_modify() |
| ->mutable_process() |
| ->set_process_uuid("modified_process"); |
| event = std::make_unique<pb::FileEventAtomicVariant>(expected_modify2); |
| FilePluginCollectEvent( |
| CreateFileEventValue(std::move(event), "modify2", 2, true)); |
| EXPECT_CALL(*batch_sender_, |
| Enqueue(::testing::Pointee(EqualsProto(expected_modify2)))); |
| |
| pb::FileEventAtomicVariant expected_modify3(expected_modify1); |
| expected_modify3.mutable_sensitive_modify() |
| ->mutable_file_modify() |
| ->mutable_image_after() |
| ->set_inode(65); |
| event = std::make_unique<pb::FileEventAtomicVariant>(expected_modify3); |
| FilePluginCollectEvent( |
| CreateFileEventValue(std::move(event), "modify3", 3, true)); |
| EXPECT_CALL(*batch_sender_, |
| Enqueue(::testing::Pointee(EqualsProto(expected_modify3)))); |
| |
| pb::FileEventAtomicVariant expected_modify4(expected_modify1); |
| expected_modify4.mutable_sensitive_modify() |
| ->mutable_file_modify() |
| ->mutable_image_after() |
| ->set_inode_device_id(165); |
| event = std::make_unique<pb::FileEventAtomicVariant>(expected_modify4); |
| FilePluginCollectEvent( |
| CreateFileEventValue(std::move(event), "modify4", 4, true)); |
| EXPECT_CALL(*batch_sender_, |
| Enqueue(::testing::Pointee(EqualsProto(expected_modify4)))); |
| |
| // create a set of expected reads. Each expected varies from the base expected |
| // by process uuid, inode or inode device id. |
| pb::FileEventAtomicVariant expected_read1; |
| pb::FileReadEvent* file_read_event = expected_read1.mutable_sensitive_read(); |
| file_read_event->mutable_process()->CopyFrom( |
| expected_modify1.sensitive_modify().process()); |
| file_read_event->mutable_file_read()->mutable_image()->CopyFrom( |
| expected_modify1.sensitive_modify().file_modify().image_after()); |
| event = std::make_unique<pb::FileEventAtomicVariant>(expected_read1); |
| FilePluginCollectEvent( |
| CreateFileEventValue(std::move(event), "read1", 1, true)); |
| EXPECT_CALL(*batch_sender_, |
| Enqueue(::testing::Pointee(EqualsProto(expected_read1)))); |
| |
| pb::FileEventAtomicVariant expected_read2(expected_read1); |
| expected_read2.mutable_sensitive_read()->mutable_process()->set_process_uuid( |
| "modified_process"); |
| event = std::make_unique<pb::FileEventAtomicVariant>(expected_read2); |
| FilePluginCollectEvent( |
| CreateFileEventValue(std::move(event), "read2", 2, true)); |
| EXPECT_CALL(*batch_sender_, |
| Enqueue(::testing::Pointee(EqualsProto(expected_read2)))); |
| |
| pb::FileEventAtomicVariant expected_read3(expected_read1); |
| expected_read3.mutable_sensitive_read() |
| ->mutable_file_read() |
| ->mutable_image() |
| ->set_inode(65); |
| event = std::make_unique<pb::FileEventAtomicVariant>(expected_read3); |
| FilePluginCollectEvent( |
| CreateFileEventValue(std::move(event), "read3", 3, true)); |
| EXPECT_CALL(*batch_sender_, |
| Enqueue(::testing::Pointee(EqualsProto(expected_read3)))); |
| |
| pb::FileEventAtomicVariant expected_read4(expected_read1); |
| expected_read4.mutable_sensitive_read() |
| ->mutable_file_read() |
| ->mutable_image() |
| ->set_inode_device_id(165); |
| event = std::make_unique<pb::FileEventAtomicVariant>(expected_read4); |
| FilePluginCollectEvent( |
| CreateFileEventValue(std::move(event), "read4", 4, true)); |
| EXPECT_CALL(*batch_sender_, |
| Enqueue(::testing::Pointee(EqualsProto(expected_read4)))); |
| task_environment_.FastForwardBy( |
| base::Seconds(kBatchIntervalS + kAsyncTimeoutS)); |
| } |
| |
| TEST_F(FilePluginTestFixture, TestMultipleBatches) { |
| pb::FileEventAtomicVariant expected_modify1; |
| expected_modify1.mutable_common()->set_create_timestamp_us(1726708500); |
| pb::FileModifyEvent* file_modify_event = |
| expected_modify1.mutable_sensitive_modify(); |
| file_modify_event->mutable_process()->set_process_uuid("process1"); |
| pb::FileImage* file_image = |
| file_modify_event->mutable_file_modify()->mutable_image_after(); |
| file_image->set_inode(64); |
| file_image->set_inode_device_id(164); |
| file_image->set_pathname("filename1"); |
| file_image->set_sha256("test"); |
| |
| EXPECT_CALL(*batch_sender_, |
| Enqueue(::testing::Pointee(EqualsProto(expected_modify1)))) |
| .Times(1); |
| auto event = std::make_unique<pb::FileEventAtomicVariant>(expected_modify1); |
| FilePluginCollectEvent( |
| CreateFileEventValue(std::move(event), "modify1", 1, true)); |
| task_environment_.FastForwardBy( |
| base::Seconds(kBatchIntervalS + kAsyncTimeoutS)); |
| |
| pb::FileEventAtomicVariant expected_modify2(expected_modify1); |
| expected_modify2.mutable_common()->set_create_timestamp_us(1726709500); |
| EXPECT_CALL(*batch_sender_, |
| Enqueue(::testing::Pointee(EqualsProto(expected_modify2)))) |
| .Times(1); |
| |
| event = std::make_unique<pb::FileEventAtomicVariant>(expected_modify2); |
| FilePluginCollectEvent( |
| CreateFileEventValue(std::move(event), "modify1", 1, true)); |
| task_environment_.FastForwardBy( |
| base::Seconds(kBatchIntervalS + kAsyncTimeoutS)); |
| } |
| |
| // Test that SHAs will not be computed for events on a particular file |
| // if a write event for that file follows. |
| TEST_F(FilePluginTestFixture, TestWriteInvalidatesAllPreviousSHAs) { |
| // File will be written to by process A, then read by process B |
| // and then written to by process C. |
| // Show that only process C has SHA256 populated. |
| // Processes are intentionally different to prevent FilePlugin from |
| // coalescing events. |
| pb::FileImage file; |
| file.set_inode(64); |
| file.set_inode_device_id(640); |
| file.set_pathname("file1"); |
| |
| pb::FileEventAtomicVariant write1; |
| write1.mutable_sensitive_modify()->mutable_process()->set_process_uuid( |
| "process_a"); |
| write1.mutable_sensitive_modify() |
| ->mutable_file_modify() |
| ->mutable_image_after() |
| ->CopyFrom(file); |
| write1.mutable_sensitive_modify()->mutable_file_modify()->set_modify_type( |
| pb::FileModify_ModifyType_WRITE); |
| |
| pb::FileEventAtomicVariant read; |
| read.mutable_sensitive_read()->mutable_process()->set_process_uuid( |
| "process_b"); |
| read.mutable_sensitive_read()->mutable_file_read()->mutable_image()->CopyFrom( |
| file); |
| |
| pb::FileEventAtomicVariant write2; |
| write2.mutable_sensitive_modify()->mutable_process()->set_process_uuid( |
| "process_c"); |
| write2.mutable_sensitive_modify() |
| ->mutable_file_modify() |
| ->mutable_image_after() |
| ->CopyFrom(file); |
| write2.mutable_sensitive_modify()->mutable_file_modify()->set_modify_type( |
| pb::FileModify_ModifyType_WRITE); |
| |
| auto fev = CreateFileEventValue( |
| std::make_unique<pb::FileEventAtomicVariant>(write1), "write1", 1, true); |
| // SHA256 computation should not happened for first write since the SHA |
| // operation is aborted by write2. |
| EXPECT_CALL(*image_cache_, |
| InclusiveGetImage(DeriveImageCacheKeyType(*fev), false, |
| fev->meta_data.pid_for_setns, |
| base::FilePath(fev->meta_data.file_name))) |
| .Times(0); |
| EXPECT_CALL(*batch_sender_, Enqueue(::testing::Pointee(EqualsProto(write1)))); |
| FilePluginCollectEvent(std::move(fev)); |
| |
| fev = CreateFileEventValue(std::make_unique<pb::FileEventAtomicVariant>(read), |
| "read", 2, true); |
| // SHA256 computation should not happened for first write since the SHA |
| // operation is aborted by write2. |
| EXPECT_CALL(*image_cache_, |
| InclusiveGetImage(DeriveImageCacheKeyType(*fev), false, |
| fev->meta_data.pid_for_setns, |
| base::FilePath(fev->meta_data.file_name))) |
| .Times(0); |
| EXPECT_CALL(*batch_sender_, Enqueue(::testing::Pointee(EqualsProto(read)))); |
| FilePluginCollectEvent(std::move(fev)); |
| |
| fev = CreateFileEventValue( |
| std::make_unique<pb::FileEventAtomicVariant>(write2), "write2", 3, true); |
| // SHA256 computation should not happened for first write since the SHA |
| // operation is aborted by write2. |
| ImageCacheInterface::HashValue hash_val{.sha256 = "sha_write2", |
| .sha256_is_partial = false}; |
| EXPECT_CALL(*image_cache_, |
| InclusiveGetImage(DeriveImageCacheKeyType(*fev), false, |
| fev->meta_data.pid_for_setns, |
| base::FilePath(fev->meta_data.file_name))) |
| .WillOnce(Return(hash_val)); |
| |
| write2.mutable_sensitive_modify() |
| ->mutable_file_modify() |
| ->mutable_image_after() |
| ->set_partial_sha256(hash_val.sha256_is_partial); |
| write2.mutable_sensitive_modify() |
| ->mutable_file_modify() |
| ->mutable_image_after() |
| ->set_sha256(hash_val.sha256); |
| EXPECT_CALL(*batch_sender_, Enqueue(::testing::Pointee(EqualsProto(write2)))); |
| EXPECT_CALL(*batch_sender_, Flush()).Times(1); |
| FilePluginCollectEvent(std::move(fev)); |
| task_environment_.FastForwardBy( |
| base::Seconds(kBatchIntervalS + kAsyncTimeoutS)); |
| } |
| |
| // Test that reads do not invalidate SHAs. |
| TEST_F(FilePluginTestFixture, TestReadsDoNotInvalidateSHA) { |
| // File will be read twice by two different processes. |
| pb::FileImage file; |
| file.set_inode(64); |
| file.set_inode_device_id(640); |
| file.set_pathname("file1"); |
| |
| pb::FileEventAtomicVariant read1; |
| read1.mutable_sensitive_read()->mutable_process()->set_process_uuid( |
| "process_a"); |
| read1.mutable_sensitive_read() |
| ->mutable_file_read() |
| ->mutable_image() |
| ->CopyFrom(file); |
| |
| auto fev_read1 = CreateFileEventValue( |
| std::make_unique<pb::FileEventAtomicVariant>(read1), "read1", 1, true); |
| // SHA256 computation should not happened for first write since the SHA |
| // operation is aborted by write2. |
| ImageCacheInterface::HashValue read1_hash{.sha256 = "sha_read1", |
| .sha256_is_partial = false}; |
| EXPECT_CALL(*image_cache_, |
| InclusiveGetImage(DeriveImageCacheKeyType(*fev_read1), false, |
| fev_read1->meta_data.pid_for_setns, |
| base::FilePath(fev_read1->meta_data.file_name))) |
| .WillOnce(Return(read1_hash)); |
| SetSHADetails(read1, read1_hash); |
| EXPECT_CALL(*batch_sender_, Enqueue(::testing::Pointee(EqualsProto(read1)))); |
| FilePluginCollectEvent(std::move(fev_read1)); |
| |
| pb::FileEventAtomicVariant read2; |
| read2.mutable_sensitive_read()->mutable_process()->set_process_uuid( |
| "process_b"); |
| read2.mutable_sensitive_read() |
| ->mutable_file_read() |
| ->mutable_image() |
| ->CopyFrom(file); |
| auto fev_read2 = CreateFileEventValue( |
| std::make_unique<pb::FileEventAtomicVariant>(read2), "read2", 2, true); |
| // SHA256 computation should not happened for first write since the SHA |
| // operation is aborted by write2. |
| ImageCacheInterface::HashValue read2_hash{.sha256 = "sha_read2", |
| .sha256_is_partial = true}; |
| EXPECT_CALL(*image_cache_, |
| InclusiveGetImage(DeriveImageCacheKeyType(*fev_read2), false, |
| fev_read2->meta_data.pid_for_setns, |
| base::FilePath(fev_read2->meta_data.file_name))) |
| .WillOnce(Return(read2_hash)); |
| FilePluginCollectEvent(std::move(fev_read2)); |
| SetSHADetails(read2, read2_hash); |
| EXPECT_CALL(*batch_sender_, Enqueue(::testing::Pointee(EqualsProto(read2)))); |
| |
| EXPECT_CALL(*batch_sender_, Flush()).Times(1); |
| task_environment_.FastForwardBy( |
| base::Seconds(kBatchIntervalS + kAsyncTimeoutS)); |
| } |
| |
| // Test that modify attributes does not invalidate SHAs. |
| TEST_F(FilePluginTestFixture, TestChangeAttributesDoNotInvalidateSHA) { |
| // File will be read then have its attributes modified by two different |
| // processes. |
| pb::FileImage file; |
| file.set_inode(64); |
| file.set_inode_device_id(640); |
| file.set_pathname("file1"); |
| |
| pb::FileEventAtomicVariant read; |
| read.mutable_sensitive_read()->mutable_process()->set_process_uuid( |
| "process_a"); |
| read.mutable_sensitive_read()->mutable_file_read()->mutable_image()->CopyFrom( |
| file); |
| |
| auto fev_read = CreateFileEventValue( |
| std::make_unique<pb::FileEventAtomicVariant>(read), "read", 1, true); |
| // SHA256 computation should not happened for first write since the SHA |
| // operation is aborted by write2. |
| ImageCacheInterface::HashValue read_hash{.sha256 = "sha_read", |
| .sha256_is_partial = false}; |
| EXPECT_CALL(*image_cache_, |
| InclusiveGetImage(DeriveImageCacheKeyType(*fev_read), false, |
| fev_read->meta_data.pid_for_setns, |
| base::FilePath(fev_read->meta_data.file_name))) |
| .WillOnce(Return(read_hash)); |
| SetSHADetails(read, read_hash); |
| EXPECT_CALL(*batch_sender_, Enqueue(::testing::Pointee(EqualsProto(read)))); |
| FilePluginCollectEvent(std::move(fev_read)); |
| |
| pb::FileEventAtomicVariant modify_attribute; |
| modify_attribute.mutable_sensitive_modify() |
| ->mutable_process() |
| ->set_process_uuid("process_b"); |
| modify_attribute.mutable_sensitive_modify() |
| ->mutable_file_modify() |
| ->set_modify_type(pb::FileModify_ModifyType_MODIFY_ATTRIBUTE); |
| modify_attribute.mutable_sensitive_modify() |
| ->mutable_file_modify() |
| ->mutable_image_after() |
| ->CopyFrom(file); |
| auto fev_modify_attribute = CreateFileEventValue( |
| std::make_unique<pb::FileEventAtomicVariant>(modify_attribute), |
| "modify_attribute", 2, true); |
| // SHA256 computation should not happened for first write since the SHA |
| // operation is aborted by write2. |
| ImageCacheInterface::HashValue modify_attribute_hash{ |
| .sha256 = "sha_modify_attribute", .sha256_is_partial = true}; |
| EXPECT_CALL(*image_cache_, |
| InclusiveGetImage( |
| DeriveImageCacheKeyType(*fev_modify_attribute), false, |
| fev_modify_attribute->meta_data.pid_for_setns, |
| base::FilePath(fev_modify_attribute->meta_data.file_name))) |
| .WillOnce(Return(modify_attribute_hash)); |
| FilePluginCollectEvent(std::move(fev_modify_attribute)); |
| SetSHADetails(modify_attribute, modify_attribute_hash); |
| EXPECT_CALL(*batch_sender_, |
| Enqueue(::testing::Pointee(EqualsProto(modify_attribute)))); |
| |
| EXPECT_CALL(*batch_sender_, Flush()).Times(1); |
| task_environment_.FastForwardBy( |
| base::Seconds(kBatchIntervalS + kAsyncTimeoutS)); |
| } |
| |
| TEST_F(FilePluginTestFixture, TestOnDeviceMountUnknown) { |
| std::string path_string = "mnt/removable/usb1"; |
| base::FilePath path = |
| fake_root_.GetPath().Append(base::FilePath(path_string)); |
| base::CreateDirectory(path); |
| bpf::mount_data mount_data = {}; |
| std::strncpy(const_cast<char*>(mount_data.dest_device_path), |
| path_string.c_str(), sizeof(mount_data.dest_device_path) - 1); |
| mount_data.dest_device_path[sizeof(mount_data.dest_device_path) - 1] = |
| '\0'; // Ensure null termination |
| |
| SetupDirectoryForValidation( |
| path_string, false, bpf::MONITOR_SPECIFIC_FILES, bpf::READ_WRITE_ONLY, |
| cros_xdr::reporting::SensitiveFileType::USB_MASS_STORAGE, std::nullopt, |
| false); |
| |
| bpf::cros_event bpf_event; |
| bpf_event.type = bpf::kFileEvent; |
| bpf_event.data.file_event.type = bpf::cros_file_event_type::kFileMountEvent; |
| bpf_event.data.file_event.data.mount_event = mount_data; |
| bpf_event.data.file_event.mod_type = bpf::FMOD_MOUNT; |
| |
| cbs_.ring_buffer_event_callback.Run(bpf_event); |
| } |
| |
| bool WriteFileWithDynamicMountInfo(const base::FilePath& file_path, |
| uint32_t device_major, |
| uint32_t device_minor, |
| bool add_device_line) { |
| // Ensure the directory exists |
| base::FilePath parent_directory = file_path.DirName(); |
| if (!base::CreateDirectory(parent_directory)) { |
| LOG(ERROR) << "Failed to create directory: " << parent_directory.value(); |
| return false; |
| } |
| |
| // Prepare the content |
| std::string content = |
| "692 678 5:822 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw\n" |
| "693 678 3:232 / /sys rw,nosuid,nodev,noexec,relatime - sysfs sysfs rw\n"; |
| |
| // Add the dynamically generated mountinfo line |
| if (add_device_line) { |
| content += base::StringPrintf( |
| "694 692 %u:%u / /mnt/mock rw,relatime - ext4 ext4 rw,data=ordered\n", |
| device_major, device_minor); |
| } |
| |
| // Append the remaining lines of mountinfo |
| content += |
| "695 678 9:643 / /dev rw,nosuid,relatime master:2 - devtmpfs udev " |
| "rw,size=92777504k,nr_inodes=23194376,mode=755,inode64\n" |
| "700 695 2:224 / /dev/pts rw,nosuid,noexec,relatime master:3 - devpts " |
| "devpts rw,gid=5,mode=620,ptmxmode=000\n" |
| "703 695 3:273 / /dev/shm rw,nosuid,nodev master:4 - tmpfs tmpfs " |
| "rw,inode64\n" |
| "716 695 8:324 / /dev/hugepages rw,nosuid,nodev,relatime master:14 - " |
| "hugetlbfs hugetlbfs rw,pagesize=2M\n" |
| "720 695 7:231 / /dev/mqueue rw,nosuid,nodev,noexec,relatime master:16 - " |
| "mqueue mqueue rw\n"; |
| |
| // Write content to file using the 2-argument base::WriteFile |
| if (!base::WriteFile(file_path, content)) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| TEST_F(FilePluginTestFixture, TestOnDeviceMountMedia) { |
| SetupDirectoryForValidation( |
| "media/removable/usb1", false, bpf::MONITOR_SPECIFIC_FILES, |
| bpf::READ_WRITE_ONLY, |
| cros_xdr::reporting::SensitiveFileType::USB_MASS_STORAGE); |
| base::FilePath path = |
| fake_root_.GetPath().Append(base::FilePath("media/removable/usb1")); |
| bpf::mount_data mount_data = {}; |
| std::strncpy(const_cast<char*>(mount_data.dest_device_path), |
| path.value().c_str(), sizeof(mount_data.dest_device_path) - 1); |
| mount_data.dest_device_path[sizeof(mount_data.dest_device_path) - 1] = |
| '\0'; // Ensure null termination |
| |
| bpf::cros_event bpf_event; |
| bpf_event.type = bpf::kFileEvent; |
| bpf_event.data.file_event.type = bpf::cros_file_event_type::kFileMountEvent; |
| bpf_event.data.file_event.data.mount_event = mount_data; |
| bpf_event.data.file_event.mod_type = bpf::FMOD_MOUNT; |
| |
| cbs_.ring_buffer_event_callback.Run(bpf_event); |
| |
| SetupDirectoryForValidation( |
| "media/fuse/drivefs-gegergegehh", false, bpf::MONITOR_SPECIFIC_FILES, |
| bpf::READ_AND_READ_WRITE_BOTH, |
| cros_xdr::reporting::SensitiveFileType::USER_GOOGLE_DRIVE_FILE); |
| path = fake_root_.GetPath().Append( |
| base::FilePath("media/fuse/drivefs-gegergegehh")); |
| mount_data = {}; |
| std::strncpy(const_cast<char*>(mount_data.dest_device_path), |
| path.value().c_str(), sizeof(mount_data.dest_device_path) - 1); |
| mount_data.dest_device_path[sizeof(mount_data.dest_device_path) - 1] = |
| '\0'; // Ensure null termination |
| |
| bpf_event.type = bpf::kFileEvent; |
| bpf_event.data.file_event.type = bpf::cros_file_event_type::kFileMountEvent; |
| bpf_event.data.file_event.data.mount_event = mount_data; |
| bpf_event.data.file_event.mod_type = bpf::FMOD_MOUNT; |
| |
| cbs_.ring_buffer_event_callback.Run(bpf_event); |
| // Test umount with already mounted device_id for usb |
| path = fake_root_.GetPath().Append(base::FilePath("media/removable/usb1")); |
| bpf::umount_event umount_data = {}; |
| std::strncpy(const_cast<char*>(umount_data.dest_device_path), |
| path.value().c_str(), sizeof(umount_data.dest_device_path) - 1); |
| umount_data.dest_device_path[sizeof(umount_data.dest_device_path) - 1] = |
| '\0'; // Ensure null termination |
| |
| base::stat_wrapper_t fileStat; |
| if (base::File::Stat(path, &fileStat) != 0) { |
| FAIL() << "Failed to get file stat"; |
| } |
| |
| LOG(ERROR) << "Path INODE " << fileStat.st_ino; |
| |
| // Expected value from UserspaceToKernelDeviceId(). |
| uint32_t expected_kernel_dev = |
| ((major(fileStat.st_dev) << 20) | minor(fileStat.st_dev)); |
| umount_data.device_id = expected_kernel_dev; |
| |
| bpf_event.type = bpf::kFileEvent; |
| bpf_event.data.file_event.type = bpf::cros_file_event_type::kFileMountEvent; |
| bpf_event.data.file_event.data.umount_event = umount_data; |
| bpf_event.data.file_event.mod_type = bpf::FMOD_UMOUNT; |
| base::FilePath mountPathInRoot = |
| fake_root_.GetPath().Append("proc/self/mountinfo"); |
| base::FilePath mountPathInCryptoNamespace = |
| fake_root_.GetPath().Append("proc/12345/root/proc/self/mountinfo"); |
| EXPECT_TRUE(WriteFileWithDynamicMountInfo( |
| mountPathInRoot, major(fileStat.st_dev), minor(fileStat.st_dev), true)); |
| EXPECT_TRUE(WriteFileWithDynamicMountInfo(mountPathInCryptoNamespace, |
| major(fileStat.st_dev), |
| minor(fileStat.st_dev), true)); |
| |
| // Test umount with still mounted device_id for usb |
| EXPECT_CALL( |
| *platform_, |
| BpfMapDeleteElementByFd( |
| 43, ::testing::Truly([expected_kernel_dev](const void* arg) -> bool { |
| auto* dev_id = static_cast<const uint32_t*>(arg); |
| LOG(ERROR) << "BpfMapDeleteElementByFd (43) - Device ID: " |
| << *dev_id << " | Expected: " << expected_kernel_dev; |
| return *dev_id == expected_kernel_dev; |
| }))) |
| .Times(0); |
| |
| cbs_.ring_buffer_event_callback.Run(bpf_event); |
| |
| // Test umount with non_mounted device_id for usb |
| umount_data.device_id = 16663; |
| bpf_event.data.file_event.data.umount_event = umount_data; |
| EXPECT_CALL(*platform_, |
| BpfMapDeleteElementByFd( |
| 43, ::testing::Truly([umount_data](const void* arg) -> bool { |
| auto* dev_id = static_cast<const uint32_t*>(arg); |
| LOG(ERROR) |
| << "BpfMapDeleteElementByFd (43) - Device ID: (16663) " |
| << *dev_id << " | Expected: " << umount_data.device_id; |
| return *dev_id == 16663; |
| }))) |
| .Times(::testing::Exactly(1)); |
| |
| cbs_.ring_buffer_event_callback.Run(bpf_event); |
| // Test umount in cryptohome mountinfo |
| brillo::DeleteFile(mountPathInRoot); |
| EXPECT_TRUE(WriteFileWithDynamicMountInfo( |
| mountPathInRoot, major(fileStat.st_dev), minor(fileStat.st_dev), false)); |
| |
| path = fake_root_.GetPath().Append( |
| base::FilePath("media/fuse/drivefs-gegergegehh")); |
| |
| // Test umount with still mounted device_id for usb |
| EXPECT_CALL( |
| *platform_, |
| BpfMapDeleteElementByFd( |
| 43, ::testing::Truly([expected_kernel_dev](const void* arg) -> bool { |
| auto* dev_id = static_cast<const uint32_t*>(arg); |
| LOG(ERROR) << "BpfMapDeleteElementByFd (43) - Device ID: " |
| << *dev_id << " | Expected: " << expected_kernel_dev; |
| return *dev_id == expected_kernel_dev; |
| }))) |
| .Times(0); |
| umount_data.device_id = expected_kernel_dev; |
| bpf_event.data.file_event.data.umount_event = umount_data; |
| |
| cbs_.ring_buffer_event_callback.Run(bpf_event); |
| |
| // Test umount with non_mounted device_id for usb |
| umount_data.device_id = 16654; |
| bpf_event.data.file_event.data.umount_event = umount_data; |
| EXPECT_CALL(*platform_, |
| BpfMapDeleteElementByFd( |
| 43, ::testing::Truly([umount_data](const void* arg) -> bool { |
| auto* dev_id = static_cast<const uint32_t*>(arg); |
| LOG(ERROR) |
| << "BpfMapDeleteElementByFd - Device ID (16654): " |
| << *dev_id << " | Expected: " << umount_data.device_id; |
| return *dev_id == 16654; |
| }))) |
| .Times(::testing::Exactly(1)); |
| |
| cbs_.ring_buffer_event_callback.Run(bpf_event); |
| } |
| |
| } // namespace secagentd::testing |