| // Copyright 2022 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "content/public/browser/browser_child_process_observer.h" |
| |
| #include "base/functional/bind.h" |
| #include "base/run_loop.h" |
| #include "build/build_config.h" |
| #include "content/browser/browser_child_process_host_impl.h" |
| #include "content/browser/child_process_host_impl.h" |
| #include "content/browser/service_host/utility_process_host.h" |
| #include "content/public/browser/browser_child_process_host.h" |
| #include "content/public/browser/browser_child_process_host_delegate.h" |
| #include "content/public/browser/child_process_data.h" |
| #include "content/public/common/content_switches.h" |
| #include "content/public/common/process_type.h" |
| #include "content/public/common/sandboxed_process_launcher_delegate.h" |
| #include "content/public/test/browser_test.h" |
| #include "content/public/test/content_browser_test.h" |
| #include "content/public/test/test_service.mojom.h" |
| #include "sandbox/policy/sandbox_type.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| // An enum that represent the different type of notitifcations that exist in |
| // BrowserChildProcessObserver. |
| enum class Notification { |
| kLaunchedAndConnected, |
| kDisconnected, |
| kCrashed, |
| kKilled, |
| kLaunchFailed, |
| kExitedNormally, |
| }; |
| |
| // Nicer test output. |
| std::ostream& operator<<(std::ostream& os, Notification notification) { |
| switch (notification) { |
| case Notification::kLaunchedAndConnected: |
| os << "LaunchedAndConnected"; |
| break; |
| case Notification::kDisconnected: |
| os << "Disconnected"; |
| break; |
| case Notification::kCrashed: |
| os << "Crashed"; |
| break; |
| case Notification::kKilled: |
| os << "Killed"; |
| break; |
| case Notification::kLaunchFailed: |
| os << "LaunchFailed"; |
| break; |
| case Notification::kExitedNormally: |
| os << "ExitedNormally"; |
| break; |
| } |
| return os; |
| } |
| |
| // Returns true if a child process whose ID is |child_id| is still alive. |
| bool IsHostAlive(int child_id) { |
| return BrowserChildProcessHost::FromID(child_id) != nullptr; |
| } |
| |
| } // namespace |
| |
| // A test BrowserChildProcessObserver that transforms every call to one of the |
| // observer's method to a call to the notification callback. |
| class BrowserChildProcessNotificationObserver |
| : public BrowserChildProcessObserver { |
| public: |
| using OnNotificationCallback = |
| base::RepeatingCallback<void(Notification notification)>; |
| |
| BrowserChildProcessNotificationObserver( |
| int child_id, |
| OnNotificationCallback on_notification_callback) |
| : child_id_(child_id), |
| on_notification_callback_(std::move(on_notification_callback)) { |
| BrowserChildProcessObserver::Add(this); |
| } |
| |
| ~BrowserChildProcessNotificationObserver() override { |
| BrowserChildProcessObserver::Remove(this); |
| } |
| |
| protected: |
| // BrowserChildProcessObserver: |
| void BrowserChildProcessLaunchedAndConnected( |
| const ChildProcessData& data) override { |
| OnNotification(data, Notification::kLaunchedAndConnected); |
| } |
| void BrowserChildProcessHostDisconnected( |
| const ChildProcessData& data) override { |
| OnNotification(data, Notification::kDisconnected); |
| } |
| void BrowserChildProcessCrashed( |
| const ChildProcessData& data, |
| const ChildProcessTerminationInfo& info) override { |
| OnNotification(data, Notification::kCrashed); |
| } |
| void BrowserChildProcessKilled( |
| const ChildProcessData& data, |
| const ChildProcessTerminationInfo& info) override { |
| OnNotification(data, Notification::kKilled); |
| } |
| void BrowserChildProcessLaunchFailed( |
| const ChildProcessData& data, |
| const ChildProcessTerminationInfo& info) override { |
| OnNotification(data, Notification::kLaunchFailed); |
| } |
| void BrowserChildProcessExitedNormally( |
| const ChildProcessData& data, |
| const ChildProcessTerminationInfo& info) override { |
| OnNotification(data, Notification::kExitedNormally); |
| } |
| |
| void OnNotification(const ChildProcessData& data, Notification notification) { |
| if (data.id == child_id_) |
| on_notification_callback_.Run(notification); |
| } |
| |
| private: |
| // Every notification coming for a child with a different ID will be ignored. |
| int child_id_; |
| |
| // The callback to invoke every time a method of the observer is called. |
| OnNotificationCallback on_notification_callback_; |
| }; |
| |
| // A helper class that allows the user to wait until a specific |notification| |
| // is sent for a child process whose ID matches |child_id|. |
| class WaitForNotificationObserver { |
| public: |
| WaitForNotificationObserver(int child_id, Notification notification) |
| : inner_observer_( |
| child_id, |
| base::BindRepeating(&WaitForNotificationObserver::OnNotification, |
| base::Unretained(this))), |
| notification_(notification) {} |
| |
| ~WaitForNotificationObserver() = default; |
| |
| // Waits until the notification is received. Returns immediately if it was |
| // already received. |
| void Wait() { |
| if (notification_received_) |
| return; |
| |
| DCHECK(!run_loop_.running()); |
| run_loop_.Run(); |
| } |
| |
| private: |
| void OnNotification(Notification notification) { |
| if (notification != notification_) |
| return; |
| |
| notification_received_ = true; |
| if (run_loop_.running()) |
| run_loop_.Quit(); |
| } |
| |
| BrowserChildProcessNotificationObserver inner_observer_; |
| Notification notification_; |
| base::RunLoop run_loop_; |
| bool notification_received_ = false; |
| }; |
| |
| class TestSandboxedProcessLauncherDelegate |
| : public SandboxedProcessLauncherDelegate { |
| public: |
| explicit TestSandboxedProcessLauncherDelegate( |
| sandbox::mojom::Sandbox sandbox_type) |
| : sandbox_type_(sandbox_type) {} |
| ~TestSandboxedProcessLauncherDelegate() override = default; |
| |
| // SandboxedProcessLauncherDelegate: |
| sandbox::mojom::Sandbox GetSandboxType() override { return sandbox_type_; } |
| |
| private: |
| sandbox::mojom::Sandbox sandbox_type_; |
| }; |
| |
| // A test-specific type of process host. Self-owned. |
| class TestProcessHost : public BrowserChildProcessHostDelegate { |
| public: |
| static base::WeakPtr<TestProcessHost> Create() { |
| auto* instance = new TestProcessHost(); |
| return instance->GetWeakPtr(); |
| } |
| |
| TestProcessHost() |
| : process_(BrowserChildProcessHost::Create( |
| PROCESS_TYPE_UTILITY, |
| this, |
| ChildProcessHost::IpcMode::kNormal)) {} |
| ~TestProcessHost() override = default; |
| |
| // Returns the ID of the child process. |
| int GetID() { return process_->GetData().id; } |
| |
| // Binds to the test service on the child process and returns the bound |
| // remote. |
| mojo::Remote<mojom::TestService> BindTestService() { |
| mojo::Remote<mojom::TestService> test_service; |
| |
| static_cast<ChildProcessHostImpl*>(process_->GetHost()) |
| ->child_process() |
| ->BindServiceInterface(test_service.BindNewPipeAndPassReceiver()); |
| |
| return test_service; |
| } |
| |
| // Returns the command line used to launch the child process. |
| std::unique_ptr<base::CommandLine> GetChildCommandLine() { |
| base::FilePath child_path = |
| ChildProcessHost::GetChildPath(ChildProcessHost::CHILD_NORMAL); |
| auto command_line = std::make_unique<base::CommandLine>(child_path); |
| |
| command_line->AppendSwitchASCII(switches::kProcessType, |
| switches::kUtilityProcess); |
| command_line->AppendSwitchASCII(switches::kUtilitySubType, |
| "Test Utility Process"); |
| sandbox::policy::SetCommandLineFlagsForSandboxType(command_line.get(), |
| sandbox_type_); |
| |
| return command_line; |
| } |
| |
| // Launches the child process using the default test launcher delegate. |
| void LaunchProcess() { |
| LaunchProcessWithDelegate( |
| std::make_unique<TestSandboxedProcessLauncherDelegate>(sandbox_type_)); |
| } |
| |
| // Launches the child process using a supplied sandbox delegate. |
| void LaunchProcessWithDelegate( |
| std::unique_ptr<SandboxedProcessLauncherDelegate> |
| sandboxed_process_launcher_delegate) { |
| process_->SetName(u"Test utility process"); |
| |
| auto command_line = GetChildCommandLine(); |
| bool terminate_on_shutdown = true; |
| |
| process_->Launch(std::move(sandboxed_process_launcher_delegate), |
| std::move(command_line), terminate_on_shutdown); |
| |
| test_service_ = BindTestService(); |
| } |
| |
| // Requests the child process to shutdown. |
| void ForceShutdown() { process_->GetHost()->ForceShutdown(); } |
| |
| // Disconnects the bound remote from the test service. |
| void Disconnect() { test_service_.reset(); } |
| |
| // Sets the sandbox type to use for the child process. |
| void SetSandboxType(sandbox::mojom::Sandbox sandbox_type) { |
| sandbox_type_ = sandbox_type; |
| } |
| |
| mojom::TestService* service() const { return test_service_.get(); } |
| |
| base::WeakPtr<TestProcessHost> GetWeakPtr() { |
| return weak_ptr_factory_.GetWeakPtr(); |
| } |
| |
| private: |
| sandbox::mojom::Sandbox sandbox_type_ = sandbox::mojom::Sandbox::kUtility; |
| |
| std::unique_ptr<BrowserChildProcessHost> process_; |
| |
| mojo::Remote<mojom::TestService> test_service_; |
| |
| base::WeakPtrFactory<TestProcessHost> weak_ptr_factory_{this}; |
| }; |
| |
| // A helper class that exposes which notifications were sent for a specific |
| // child process. |
| class TestBrowserChildProcessObserver { |
| public: |
| explicit TestBrowserChildProcessObserver(int child_id) |
| : inner_observer_(child_id, |
| base::BindRepeating( |
| &TestBrowserChildProcessObserver::OnNotification, |
| base::Unretained(this))) {} |
| |
| ~TestBrowserChildProcessObserver() = default; |
| |
| // Returns the notifications received for |child_id|. |
| const std::vector<Notification>& notifications() const { |
| return notifications_; |
| } |
| |
| private: |
| void OnNotification(Notification notification) { |
| notifications_.push_back(notification); |
| } |
| |
| BrowserChildProcessNotificationObserver inner_observer_; |
| |
| std::vector<Notification> notifications_; |
| }; |
| |
| class BrowserChildProcessObserverBrowserTest : public ContentBrowserTest {}; |
| |
| // Tests that launching and then using ForceShutdown() results in a normal |
| // termination. |
| #if defined(ADDRESS_SANITIZER) |
| // TODO(crbug.com/40238612): Fix ASAN failures on trybot. |
| #define MAYBE_LaunchAndForceShutdown DISABLED_LaunchAndForceShutdown |
| #else |
| #define MAYBE_LaunchAndForceShutdown LaunchAndForceShutdown |
| #endif |
| IN_PROC_BROWSER_TEST_F(BrowserChildProcessObserverBrowserTest, |
| MAYBE_LaunchAndForceShutdown) { |
| base::WeakPtr<TestProcessHost> host = TestProcessHost::Create(); |
| int child_id = host->GetID(); |
| |
| TestBrowserChildProcessObserver observer(child_id); |
| |
| { |
| WaitForNotificationObserver waiter(child_id, |
| Notification::kLaunchedAndConnected); |
| host->LaunchProcess(); |
| waiter.Wait(); |
| } |
| |
| { |
| WaitForNotificationObserver waiter(child_id, Notification::kDisconnected); |
| host->ForceShutdown(); |
| waiter.Wait(); |
| } |
| |
| Notification kExitNotification = |
| #if BUILDFLAG(IS_ANDROID) |
| // TODO(pmonette): On Android, this currently causes a killed |
| // notification. Consider fixing. |
| Notification::kKilled; |
| #else |
| Notification::kExitedNormally; |
| #endif // BUILDFLAG(IS_ANDROID) |
| |
| // The host should be deleted now. |
| EXPECT_FALSE(host); |
| EXPECT_FALSE(IsHostAlive(child_id)); |
| EXPECT_THAT(observer.notifications(), |
| testing::ElementsAreArray({Notification::kLaunchedAndConnected, |
| kExitNotification, |
| Notification::kDisconnected})); |
| } |
| |
| // Tests that launching and then deleting the host results in a normal |
| // termination. |
| IN_PROC_BROWSER_TEST_F(BrowserChildProcessObserverBrowserTest, |
| LaunchAndDelete) { |
| base::WeakPtr<TestProcessHost> host = TestProcessHost::Create(); |
| int child_id = host->GetID(); |
| |
| TestBrowserChildProcessObserver observer(child_id); |
| |
| { |
| WaitForNotificationObserver waiter(child_id, |
| Notification::kLaunchedAndConnected); |
| host->LaunchProcess(); |
| waiter.Wait(); |
| } |
| |
| { |
| WaitForNotificationObserver waiter(child_id, Notification::kDisconnected); |
| delete host.get(); |
| waiter.Wait(); |
| } |
| |
| // The host should be deleted now. |
| EXPECT_FALSE(host); |
| EXPECT_FALSE(IsHostAlive(child_id)); |
| EXPECT_THAT(observer.notifications(), |
| testing::ElementsAreArray({Notification::kLaunchedAndConnected, |
| Notification::kExitedNormally, |
| Notification::kDisconnected})); |
| } |
| |
| // Tests that launching and then disconnecting the service channel results in a |
| // normal termination. |
| // Note: This only works for services bound using BindServiceInterface(), not |
| // BindReceiver(). |
| #if defined(ADDRESS_SANITIZER) |
| // TODO(crbug.com/40238612): Fix ASAN failures on trybot. |
| #define MAYBE_LaunchAndDisconnect DISABLED_LaunchAndDisconnect |
| #else |
| #define MAYBE_LaunchAndDisconnect LaunchAndDisconnect |
| #endif |
| IN_PROC_BROWSER_TEST_F(BrowserChildProcessObserverBrowserTest, |
| MAYBE_LaunchAndDisconnect) { |
| base::WeakPtr<TestProcessHost> host = TestProcessHost::Create(); |
| int child_id = host->GetID(); |
| |
| TestBrowserChildProcessObserver observer(child_id); |
| |
| { |
| WaitForNotificationObserver waiter(child_id, |
| Notification::kLaunchedAndConnected); |
| host->LaunchProcess(); |
| waiter.Wait(); |
| } |
| |
| { |
| WaitForNotificationObserver waiter(child_id, Notification::kDisconnected); |
| host->Disconnect(); |
| waiter.Wait(); |
| } |
| |
| Notification kExitNotification = |
| #if BUILDFLAG(IS_ANDROID) |
| // On Android, kKilled is always sent in the case of a crash. |
| Notification::kKilled; |
| #else |
| Notification::kExitedNormally; |
| #endif // BUILDFLAG(IS_ANDROID) |
| |
| // The host should be deleted now. |
| EXPECT_FALSE(host); |
| EXPECT_FALSE(IsHostAlive(child_id)); |
| EXPECT_THAT(observer.notifications(), testing::ElementsAreArray({ |
| Notification::kLaunchedAndConnected, |
| kExitNotification, |
| Notification::kDisconnected, |
| })); |
| } |
| |
| // Tests that launching and then causing a crash the host results in a crashed |
| // notification. |
| // TODO(crbug.com/40868150): Times out on Android tests. |
| #if BUILDFLAG(IS_ANDROID) |
| #define MAYBE_LaunchAndCrash DISABLED_LaunchAndCrash |
| #else |
| #define MAYBE_LaunchAndCrash LaunchAndCrash |
| #endif |
| IN_PROC_BROWSER_TEST_F(BrowserChildProcessObserverBrowserTest, |
| MAYBE_LaunchAndCrash) { |
| base::WeakPtr<TestProcessHost> host = TestProcessHost::Create(); |
| int child_id = host->GetID(); |
| |
| TestBrowserChildProcessObserver observer(child_id); |
| |
| { |
| WaitForNotificationObserver waiter(child_id, |
| Notification::kLaunchedAndConnected); |
| host->LaunchProcess(); |
| waiter.Wait(); |
| } |
| |
| { |
| WaitForNotificationObserver waiter(child_id, Notification::kDisconnected); |
| host->service()->DoCrashImmediately(base::DoNothing()); |
| waiter.Wait(); |
| } |
| |
| Notification kCrashedNotification = |
| #if BUILDFLAG(IS_ANDROID) |
| // On Android, kKilled is always sent in the case of a crash. |
| Notification::kKilled; |
| #else |
| Notification::kCrashed; |
| #endif // BUILDFLAG(IS_ANDROID) |
| |
| // The host should be deleted now. |
| EXPECT_FALSE(host); |
| EXPECT_FALSE(IsHostAlive(child_id)); |
| EXPECT_THAT(observer.notifications(), |
| testing::ElementsAreArray({Notification::kLaunchedAndConnected, |
| kCrashedNotification, |
| Notification::kDisconnected})); |
| } |
| |
| // Tests that kLaunchFailed is correctly sent when the child process fails to |
| // launch. |
| // |
| // This test won't work as-is on POSIX platforms, where fork()+exec() is used to |
| // launch child processes, failure does not happen until exec(), therefore the |
| // test will see a valid child process followed by a |
| // TERMINATION_STATUS_ABNORMAL_TERMINATION of the forked process. However, |
| // posix_spawn() is used on macOS. |
| // See also ServiceProcessLauncherTest.FailToLaunchProcess. |
| #if !BUILDFLAG(IS_POSIX) || BUILDFLAG(IS_MAC) |
| IN_PROC_BROWSER_TEST_F(BrowserChildProcessObserverBrowserTest, LaunchFailed) { |
| base::WeakPtr<TestProcessHost> host = TestProcessHost::Create(); |
| int child_id = host->GetID(); |
| |
| #if BUILDFLAG(IS_WIN) |
| // The Windows sandbox does not like the child process being a different |
| // process, so launch unsandboxed for the purpose of this test. |
| host->SetSandboxType(sandbox::mojom::Sandbox::kNoSandbox); |
| #endif |
| |
| // Simulate a catastrophic launch failure for all child processes by |
| // making the path to the process non-existent. |
| base::CommandLine::ForCurrentProcess()->AppendSwitchPath( |
| switches::kBrowserSubprocessPath, |
| base::FilePath(FILE_PATH_LITERAL("non_existent_path"))); |
| |
| TestBrowserChildProcessObserver observer(child_id); |
| |
| { |
| WaitForNotificationObserver waiter(child_id, Notification::kLaunchFailed); |
| host->LaunchProcess(); |
| waiter.Wait(); |
| } |
| |
| // The host should be deleted now. |
| EXPECT_FALSE(host); |
| EXPECT_FALSE(IsHostAlive(child_id)); |
| EXPECT_THAT(observer.notifications(), |
| testing::ElementsAreArray({Notification::kLaunchFailed})); |
| } |
| #endif // !BUILDFLAG(IS_POSIX) || BUILDFLAG(IS_MAC) |
| |
| #if BUILDFLAG(IS_WIN) |
| class TestPreSpawnTargetFailureSandboxedProcessLauncherDelegate |
| : public TestSandboxedProcessLauncherDelegate { |
| public: |
| using TestSandboxedProcessLauncherDelegate:: |
| TestSandboxedProcessLauncherDelegate; |
| |
| // SandboxedProcessLauncherDelegate: |
| bool PreSpawnTarget(sandbox::TargetPolicy* policy) override { |
| // Force a failure in PreSpawnTarget(). |
| return false; |
| } |
| }; |
| |
| // Override the observer to verify the error occurred in PreSpawnTarget(). |
| class TestPreSpawnTargetFailureBrowserChildProcessNotificationObserver |
| : public BrowserChildProcessNotificationObserver { |
| public: |
| using BrowserChildProcessNotificationObserver:: |
| BrowserChildProcessNotificationObserver; |
| |
| // BrowserChildProcessObserver: |
| void BrowserChildProcessLaunchFailed( |
| const ChildProcessData& data, |
| const ChildProcessTerminationInfo& info) override { |
| EXPECT_EQ(info.exit_code, sandbox::SBOX_ERROR_DELEGATE_PRE_SPAWN); |
| BrowserChildProcessNotificationObserver::OnNotification( |
| data, Notification::kLaunchFailed); |
| } |
| }; |
| |
| // Tests that a pre spawn failure results in a failed launch. |
| IN_PROC_BROWSER_TEST_F(BrowserChildProcessObserverBrowserTest, |
| LaunchPreSpawnFailed) { |
| base::WeakPtr<TestProcessHost> host = TestProcessHost::Create(); |
| int child_id = host->GetID(); |
| |
| TestBrowserChildProcessObserver observer(child_id); |
| |
| { |
| WaitForNotificationObserver waiter(child_id, Notification::kLaunchFailed); |
| host->LaunchProcessWithDelegate( |
| std::make_unique< |
| TestPreSpawnTargetFailureSandboxedProcessLauncherDelegate>( |
| sandbox::mojom::Sandbox::kUtility)); |
| waiter.Wait(); |
| } |
| |
| // The host should be deleted now. |
| EXPECT_FALSE(host); |
| EXPECT_FALSE(IsHostAlive(child_id)); |
| EXPECT_THAT(observer.notifications(), |
| testing::ElementsAreArray({Notification::kLaunchFailed})); |
| } |
| #endif // BUILDFLAG(IS_WIN) |
| |
| } // namespace content |