| // Copyright 2022 The ChromiumOS Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <stddef.h> |
| #include <stdint.h> |
| |
| #include <algorithm> |
| #include <memory> |
| #include <string> |
| #include <tuple> |
| #include <utility> |
| #include <vector> |
| |
| #include <base/check.h> |
| #include <base/check_op.h> |
| #include <base/command_line.h> |
| #include <base/logging.h> |
| #include <base/memory/scoped_refptr.h> |
| #include <base/task/sequenced_task_runner.h> |
| #include <base/task/single_thread_task_runner.h> |
| #include <base/test/scoped_chromeos_version_info.h> |
| #include <base/test/task_environment.h> |
| #include <base/test/test_future.h> |
| #include <base/test/test_timeouts.h> |
| #include <base/time/time.h> |
| #include <brillo/dbus/dbus_object.h> |
| #include <brillo/dbus/dbus_object_test_helpers.h> |
| #include <brillo/fake_cryptohome.h> |
| #include <brillo/fuzzed_proto_generator.h> |
| #include <brillo/secure_blob.h> |
| #include <cryptohome/proto_bindings/UserDataAuth.pb.h> |
| #include <dbus/bus.h> |
| #include <dbus/cryptohome/dbus-constants.h> |
| #include <dbus/dbus.h> |
| #include <dbus/message.h> |
| #include <dbus/mock_bus.h> |
| #include <dbus/mock_object_proxy.h> |
| #include <dbus/object_proxy.h> |
| #include <featured/feature_library.h> |
| #include <fuzzer/FuzzedDataProvider.h> |
| #include <gmock/gmock.h> |
| #include <libhwsec/factory/fuzzed_factory.h> |
| #include <libstorage/platform/fuzzers/fuzzed_platform.h> |
| #include <libstorage/platform/keyring/fake_keyring.h> |
| #include <libstorage/platform/platform.h> |
| #include <libstorage/storage_container/backing_device_factory.h> |
| #include <libstorage/storage_container/storage_container_factory.h> |
| |
| #include "cryptohome/auth_factor/manager.h" |
| #include "cryptohome/cleanup/user_oldest_activity_timestamp_manager.h" |
| #include "cryptohome/crypto.h" |
| #include "cryptohome/cryptohome_keys_manager.h" |
| #include "cryptohome/device_management_client_proxy.h" |
| #include "cryptohome/filesystem_layout.h" |
| #include "cryptohome/keyset_management.h" |
| #include "cryptohome/recoverable_key_store/mock_backend_cert_provider.h" |
| #include "cryptohome/service_userdataauth.h" |
| #include "cryptohome/storage/cryptohome_vault_factory.h" |
| #include "cryptohome/storage/homedirs.h" |
| #include "cryptohome/storage/mock_mount_factory.h" |
| #include "cryptohome/storage/mount_factory.h" |
| #include "cryptohome/user_secret_stash/manager.h" |
| #include "cryptohome/user_secret_stash/storage.h" |
| #include "cryptohome/userdataauth.h" |
| |
| namespace cryptohome { |
| namespace { |
| |
| using ::brillo::Blob; |
| using ::brillo::BlobFromString; |
| using ::brillo::FuzzedProtoGenerator; |
| using ::testing::_; |
| using ::testing::NiceMock; |
| |
| // Run at most this number of "commands" when processing a single fuzzer input. |
| // This is chosen semi-arbitrarily, just to avoid spurious timeout reports. |
| constexpr int kMaxCommandCount = 10000; |
| |
| constexpr char kStubSystemSalt[] = "stub-system-salt"; |
| // A few typical values to choose from when simulating the system info in the |
| // fuzzer. We don't use completely random strings as only few aspects are |
| // relevant for code-under-test, and this way fuzzer can discover them quickly. |
| constexpr const char* kLsbReleaseVariants[] = { |
| // A sample value for code running in production image. |
| "CHROMEOS_RELEASE_TRACK=stable-channel\n" |
| "CHROMEOS_RELEASE_VERSION=15160.0.0\n" |
| "DEVICETYPE=CHROMEBOOK\n", |
| // A sample value for code running in test image, and with a different |
| // device type. |
| "CHROMEOS_RELEASE_TRACK=testimage-channel\n" |
| "CHROMEOS_RELEASE_VERSION=11012.0.2018_08_28_1422\n" |
| "DEVICETYPE=CHROMEBOX\n", |
| // An empty value to simulate failure. |
| "", |
| }; |
| |
| // Performs initialization and holds state that's shared across all invocations |
| // of the fuzzer. |
| class Environment { |
| public: |
| Environment() { |
| base::CommandLine::Init(0, nullptr); |
| TestTimeouts::Initialize(); |
| // Suppress log spam from the code-under-test. |
| logging::SetMinLogLevel(logging::LOGGING_FATAL); |
| } |
| |
| Environment(const Environment&) = delete; |
| Environment& operator=(const Environment&) = delete; |
| |
| base::test::TaskEnvironment& task_environment() { return task_environment_; } |
| |
| private: |
| base::test::TaskEnvironment task_environment_{ |
| base::test::TaskEnvironment::TimeSource::MOCK_TIME, |
| base::test::TaskEnvironment::ThreadingMode::MAIN_THREAD_ONLY}; |
| // Initialize the system salt singleton with a stub value. Ideally we'd only |
| // override the salt path and let the fuzzer explore the salt generation |
| // flows, but for this to work we'd need to inject `Platform` into Libbrillo. |
| brillo::cryptohome::home::FakeSystemSaltLoader system_salt_loader_{ |
| kStubSystemSalt}; |
| }; |
| |
| // Performs any global state setup and teardown that need to happen for input |
| // the fuzzer is tested with. |
| class PerInputEnvironment { |
| public: |
| PerInputEnvironment() = default; |
| |
| PerInputEnvironment(const PerInputEnvironment&) = delete; |
| PerInputEnvironment& operator=(const PerInputEnvironment&) = delete; |
| |
| ~PerInputEnvironment() { |
| // Tear down the singleton platform features. This will get created by the |
| // UDA instance with the dbus buses it uses and we don't want those to keep |
| // being used in every run. Instead, shut it down after each run at the the |
| // global get re-created with new buses. |
| feature::PlatformFeatures::ShutdownForTesting(); |
| } |
| }; |
| |
| std::unique_ptr<CryptohomeVaultFactory> CreateVaultFactory( |
| libstorage::Platform& platform, FuzzedDataProvider& provider) { |
| // Only stub out `Keyring`, because unlike other classes its real |
| // implementation does platform operations that don't go through `Platform`. |
| auto container_factory = |
| std::make_unique<libstorage::StorageContainerFactory>( |
| &platform, /* metrics */ nullptr, |
| std::make_unique<libstorage::FakeKeyring>(), |
| std::make_unique<libstorage::BackingDeviceFactory>(&platform)); |
| container_factory->set_allow_fscrypt_v2(provider.ConsumeBool()); |
| auto vault_factory = std::make_unique<CryptohomeVaultFactory>( |
| &platform, std::move(container_factory)); |
| vault_factory->set_enable_application_containers(provider.ConsumeBool()); |
| return vault_factory; |
| } |
| |
| std::unique_ptr<MountFactory> CreateMountFactory() { |
| auto mount_factory = std::make_unique<MockMountFactory>(); |
| auto* mount_factory_ptr = mount_factory.get(); |
| // Configure the usage of in-process mount helper, as out-of-process |
| // mounting is not fuzzing-compatible. |
| EXPECT_CALL(*mount_factory, New(_, _, _, _)) |
| .WillRepeatedly([=](libstorage::Platform* platform, HomeDirs* homedirs, |
| bool legacy_mount, bool bind_mount_downloads) { |
| return mount_factory_ptr->NewConcrete(platform, homedirs, legacy_mount, |
| bind_mount_downloads); |
| }); |
| return mount_factory; |
| } |
| |
| // Set up a mock dbus object that will also create mock object proxy objects as |
| // needed if GetObjectProxy is called on it. |
| struct MockDbusWithProxies { |
| // Aliases to make the mocked types a little easier to read. |
| using BusType = NiceMock<dbus::MockBus>; |
| using KeyType = std::pair<std::string, std::string>; |
| using ProxyType = NiceMock<dbus::MockObjectProxy>; |
| |
| MockDbusWithProxies() |
| : refptr(base::MakeRefCounted<BusType>(dbus::Bus::Options())) { |
| EXPECT_CALL(*refptr, GetObjectProxy(_, _)) |
| .WillRepeatedly( |
| [this](const std::string& service_name, |
| const dbus::ObjectPath& object_path) -> dbus::ObjectProxy* { |
| KeyType key(service_name, object_path.value()); |
| auto iter = proxies.find(key); |
| if (iter == proxies.end()) { |
| auto proxy = base::MakeRefCounted<ProxyType>( |
| refptr.get(), service_name, object_path); |
| iter = proxies.emplace(key, std::move(proxy)).first; |
| } |
| return iter->second.get(); |
| }); |
| } |
| |
| // The mock dbus object. |
| scoped_refptr<BusType> refptr; |
| // A map of any mock object proxies, by service name + object path. |
| std::map<KeyType, scoped_refptr<ProxyType>> proxies; |
| }; |
| |
| std::string GenerateFuzzedDBusMethodName( |
| const brillo::dbus_utils::DBusObject& dbus_object, |
| const std::string& dbus_interface_name, |
| FuzzedDataProvider& provider) { |
| // The value to return if the code below fails to generate a valid one. It |
| // must satisfy D-Bus restrictions on method names (e.g., be nonempty). |
| static constexpr char kFallbackName[] = "foo"; |
| CHECK(dbus_validate_member(kFallbackName, /*error=*/nullptr)); |
| |
| const brillo::dbus_utils::DBusInterface* const dbus_interface = |
| dbus_object.FindInterface(dbus_interface_name); |
| CHECK(dbus_interface); |
| |
| // Generate the method name either by picking one of exported methods or by |
| // creating a "random" string. |
| const std::vector<std::string> exported_method_names = |
| dbus_interface->GetMethodNames(); |
| // The max value in the range here is used to trigger the random generation. |
| const size_t selected_method_index = |
| provider.ConsumeIntegralInRange<size_t>(0, exported_method_names.size()); |
| if (selected_method_index < exported_method_names.size()) { |
| return exported_method_names[selected_method_index]; |
| } |
| |
| std::string fuzzed_name = provider.ConsumeRandomLengthString(); |
| if (!dbus_validate_member(fuzzed_name.c_str(), /*error=*/nullptr)) { |
| return kFallbackName; |
| } |
| return fuzzed_name; |
| } |
| |
| std::unique_ptr<dbus::MethodCall> GenerateFuzzedDBusCallMessage( |
| const brillo::dbus_utils::DBusObject& dbus_object, |
| const std::string& dbus_interface_name, |
| const std::string& dbus_method_name, |
| const std::vector<Blob>& breadcrumbs, |
| FuzzedDataProvider& provider) { |
| auto dbus_call = |
| std::make_unique<dbus::MethodCall>(dbus_interface_name, dbus_method_name); |
| // The serial number can be hardcoded, since we never perform concurrent D-Bus |
| // requests in the fuzzer. |
| dbus_call->SetSerial(1); |
| |
| // Construct "random" arguments for the D-Bus call. |
| dbus::MessageWriter dbus_writer(dbus_call.get()); |
| if (provider.ConsumeBool()) { |
| FuzzedProtoGenerator generator(breadcrumbs, provider); |
| Blob argument = generator.Generate(); |
| dbus_writer.AppendArrayOfBytes(argument); |
| } |
| |
| return dbus_call; |
| } |
| |
| std::unique_ptr<dbus::Response> RunBlockingDBusCall( |
| std::unique_ptr<dbus::MethodCall> method_call_message, |
| brillo::dbus_utils::DBusObject& dbus_object) { |
| // Obtain the interface object for the name specified in the call. |
| brillo::dbus_utils::DBusInterface* const dbus_interface = |
| dbus_object.FindInterface(method_call_message->GetInterface()); |
| CHECK(dbus_interface); |
| // Start the call. |
| base::test::TestFuture<std::unique_ptr<dbus::Response>> dbus_response_future; |
| brillo::dbus_utils::DBusInterfaceTestHelper::HandleMethodCall( |
| dbus_interface, method_call_message.get(), |
| dbus_response_future.GetCallback()); |
| // Wait for the reply and return it. |
| return dbus_response_future.Take(); |
| } |
| |
| // Add new interesting blobs to `breadcrumbs` from `dbus_response`, if there's |
| // any (i.e., a reply field which we should try using in subsequent requests). |
| void UpdateBreadcrumbs(const std::string& dbus_method_name, |
| std::unique_ptr<dbus::Response> dbus_response, |
| std::vector<Blob>& breadcrumbs) { |
| CHECK(dbus_response); |
| dbus::MessageReader reader(dbus_response.get()); |
| if (dbus_method_name == user_data_auth::kStartAuthSession) { |
| user_data_auth::StartAuthSessionReply start_auth_session_reply; |
| if (reader.PopArrayOfBytesAsProto(&start_auth_session_reply) && |
| !start_auth_session_reply.auth_session_id().empty()) { |
| // Keep as a breadcrumb the AuthSessionId which the code-under-test |
| // returned, so that the fuzzer can realistically test multiple D-Bus |
| // calls against the same AuthSession (the IDs are random tokens, which |
| // Libfuzzer can't "guess" itself). |
| breadcrumbs.push_back( |
| BlobFromString(start_auth_session_reply.auth_session_id())); |
| } |
| } |
| } |
| |
| // Triggers a random D-Bus method call on the UserDataAuth interface. |
| void SimulateIncomingDBusCall(FuzzedDataProvider& provider, |
| brillo::dbus_utils::DBusObject& dbus_object, |
| std::vector<Blob>& breadcrumbs) { |
| const std::string dbus_method_name = GenerateFuzzedDBusMethodName( |
| dbus_object, user_data_auth::kUserDataAuthInterface, provider); |
| std::unique_ptr<dbus::Response> dbus_response = RunBlockingDBusCall( |
| GenerateFuzzedDBusCallMessage(dbus_object, |
| user_data_auth::kUserDataAuthInterface, |
| dbus_method_name, breadcrumbs, provider), |
| dbus_object); |
| if (dbus_response) { |
| UpdateBreadcrumbs(dbus_method_name, std::move(dbus_response), breadcrumbs); |
| } |
| } |
| |
| // Simulates clocks moving forward. |
| void SimulateSleep(const base::TimeTicks& start_time, |
| Environment& env, |
| FuzzedDataProvider& provider) { |
| // The constants are chosen semi-arbitrarily. The overall sum of sleeps should |
| // be neither too short, as we want good test coverage, nor overly long, to |
| // avoid timeouts due to periodic tasks scheduled by code-under-test. |
| static constexpr base::TimeDelta kMaxSingleSleep = base::Minutes(1); |
| static constexpr base::TimeDelta kMaxOverallSleep = base::Days(1); |
| |
| // Choose sleep duration. It's at most `kMaxSingleSleep`, but also the sum of |
| // all sleeps is at most `kMaxOverallSleep`. |
| base::TimeTicks max_time = start_time + kMaxOverallSleep; |
| base::TimeDelta remaining_time = max_time - base::TimeTicks::Now(); |
| CHECK_GE(remaining_time, base::TimeDelta()); |
| base::TimeDelta current_sleep_max = std::min(kMaxSingleSleep, remaining_time); |
| int64_t current_sleep = provider.ConsumeIntegralInRange<int64_t>( |
| 0, current_sleep_max.InMicroseconds()); |
| |
| // Move clocks and execute scheduled tasks whose target times were reached. |
| env.task_environment().FastForwardBy(base::Microseconds(current_sleep)); |
| } |
| |
| // Simulates "locking" the device. This corresponds to the real world's one-time |
| // operation of finalizing InstallAttributes during enterprise enrollment or |
| // first consumer user login. Until it's done, some mount-related operations |
| // fail and fuzzer can't test code in them. |
| void SimulateDeviceLocking( |
| DeviceManagementClientProxy& device_management_client, |
| FuzzedDataProvider& provider) { |
| // We set specific attributes instead of completely "random" contents, as |
| // empirically it's hard for the Libfuzzer to figure out the right strings. |
| std::ignore = device_management_client.InstallAttributesSet( |
| "enterprise.owned", |
| BlobFromString(provider.ConsumeBool() ? "true" : "false")); |
| std::ignore = device_management_client.InstallAttributesFinalize(); |
| } |
| |
| } // namespace |
| |
| extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { |
| static Environment env; |
| PerInputEnvironment per_input_env; |
| FuzzedDataProvider provider(data, size); |
| |
| // Prepare global singletons with per-test-case parameters. |
| base::test::ScopedChromeOSVersionInfo scoped_version( |
| provider.PickValueInArray(kLsbReleaseVariants), |
| /*lsb_release_time=*/base::Time::UnixEpoch()); |
| |
| // Prepare `UserDataAuth`'s dependencies. |
| libstorage::FuzzedPlatform platform{provider}; |
| hwsec::FuzzedFactory hwsec_factory{provider}; |
| std::unique_ptr<const hwsec::CryptohomeFrontend> hwsec{ |
| hwsec_factory.GetCryptohomeFrontend()}; |
| std::unique_ptr<const hwsec::PinWeaverManagerFrontend> hwsec_pw_manager{ |
| hwsec_factory.GetPinWeaverManagerFrontend()}; |
| std::unique_ptr<const hwsec::RecoveryCryptoFrontend> recovery_crypto{ |
| hwsec_factory.GetRecoveryCryptoFrontend()}; |
| CryptohomeKeysManager cryptohome_keys_manager{hwsec.get(), &platform}; |
| Crypto crypto{hwsec.get(), hwsec_pw_manager.get(), &cryptohome_keys_manager, |
| recovery_crypto.get()}; |
| DeviceManagementClientProxy device_management_client; |
| UserOldestActivityTimestampManager user_activity_timestamp_manager{&platform}; |
| KeysetManagement keyset_management{&platform, &crypto, |
| std::make_unique<VaultKeysetFactory>()}; |
| UssStorage uss_storage{&platform}; |
| UssManager uss_manager{uss_storage}; |
| AuthFactorManager auth_factor_manager{&platform, &keyset_management, |
| &uss_manager}; |
| UserDataAuth::BackingApis backing_apis = { |
| .platform = &platform, |
| .hwsec = hwsec.get(), |
| .hwsec_pw_manager = hwsec_pw_manager.get(), |
| .recovery_crypto = recovery_crypto.get(), |
| .cryptohome_keys_manager = &cryptohome_keys_manager, |
| .crypto = &crypto, |
| .device_management_client = &device_management_client, |
| .user_activity_timestamp_manager = &user_activity_timestamp_manager, |
| .keyset_management = &keyset_management, |
| .uss_storage = &uss_storage, |
| .uss_manager = &uss_manager, |
| .auth_factor_manager = &auth_factor_manager, |
| }; |
| std::unique_ptr<CryptohomeVaultFactory> vault_factory = |
| CreateVaultFactory(platform, provider); |
| std::unique_ptr<MountFactory> mount_factory = CreateMountFactory(); |
| MockDbusWithProxies bus, mount_thread_bus; |
| NiceMock<MockRecoverableKeyStoreBackendCertProvider> key_store_cert_provider; |
| |
| // Prepare `UserDataAuth`. Set up a single-thread mode (which is not how the |
| // daemon works in production, but allows faster and reproducible fuzzing). |
| auto userdataauth = std::make_unique<UserDataAuth>(backing_apis); |
| userdataauth->set_mount_task_runner( |
| base::SingleThreadTaskRunner::GetCurrentDefault()); |
| userdataauth->set_vault_factory_for_testing(vault_factory.get()); |
| userdataauth->set_mount_factory_for_testing(mount_factory.get()); |
| userdataauth->set_key_store_cert_provider(&key_store_cert_provider); |
| if (!userdataauth->Initialize(mount_thread_bus.refptr)) { |
| // This should be a rare case (e.g., the mocked system salt writing failed). |
| return 0; |
| } |
| |
| // Prepare DeviceManagementClientProxy instance. |
| auto device_management_client_proxy = |
| std::make_unique<DeviceManagementClientProxy>(mount_thread_bus.refptr); |
| |
| // Prepare `UserDataAuthAdaptor`. D-Bus handlers of the code-under-test become |
| // registered on the given stub D-Bus object. |
| brillo::dbus_utils::DBusObject dbus_object( |
| /*object_manager=*/nullptr, /*bus=*/nullptr, /*object_path=*/{}); |
| UserDataAuthAdaptor userdataauth_adaptor(bus.refptr, &dbus_object, |
| userdataauth.get()); |
| userdataauth_adaptor.RegisterAsync(); |
| |
| // This is the main part of the fuzzer, where we exercise code-under-test |
| // using various "random" commands. |
| enum class Command { |
| kIncomingDBusCall, // Simulate an incoming D-Bus call. |
| kSleep, // Simulate clocks moving forward. |
| kLockDevice, // Simulates "locking" the device. |
| // Must be the last item. |
| kMaxValue = kLockDevice |
| }; |
| const base::TimeTicks start_time = base::TimeTicks::Now(); |
| // `breadcrumbs` contain blobs which are useful to reuse across multiple calls |
| // but which Libfuzzer cannot realistically generate itself. |
| std::vector<Blob> breadcrumbs; |
| for (int command_number = 0; |
| command_number < kMaxCommandCount && provider.remaining_bytes() > 0; |
| ++command_number) { |
| switch (provider.ConsumeEnum<Command>()) { |
| case Command::kIncomingDBusCall: { |
| SimulateIncomingDBusCall(provider, dbus_object, breadcrumbs); |
| break; |
| } |
| case Command::kSleep: { |
| SimulateSleep(start_time, env, provider); |
| break; |
| } |
| case Command::kLockDevice: { |
| SimulateDeviceLocking(*device_management_client_proxy, provider); |
| break; |
| } |
| } |
| } |
| |
| // TODO(b/258547478): Remove this after `UserDataAuth` and |
| // `UserDataAuthAdaptor` lifetime issues are resolved (they post tasks with |
| // unretained pointers). |
| env.task_environment().RunUntilIdle(); |
| |
| return 0; |
| } |
| |
| } // namespace cryptohome |