blob: ddcc16e3b71c11db3f57ccb9c226e101c22bd25a [file] [log] [blame]
// Copyright 2022 The Chromium Authors. All rights reserved.
// 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/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);
}
private:
// 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);
}
// 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.
void LaunchProcess() {
process_->SetName(u"Test utility process");
auto sandboxed_process_launcher_delegate =
std::make_unique<TestSandboxedProcessLauncherDelegate>(sandbox_type_);
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(https://crbug.com/1363257): 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(https://crbug.com/1363257): 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(https://crbug.com/1368044): 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)
} // namespace content