blob: 221c428450431d9e59a6a8536eca1b79978f4a2e [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/342213636): Remove this and spanify to fix the errors.
#pragma allow_unsafe_buffers
#endif
#include "content/browser/child_process_launcher_helper.h"
#import <BrowserEngineKit/BrowserEngineKit.h>
#include <list>
#include "base/apple/mach_port_rendezvous_ios.h"
#include "base/files/scoped_temp_dir.h"
#include "base/no_destructor.h"
#include "base/strings/sys_string_conversions.h"
#include "base/threading/platform_thread.h"
#include "content/browser/child_process_launcher.h"
#include "content/browser/child_process_launcher_helper_posix.h"
#include "content/public/browser/child_process_launcher_utils.h"
#include "content/public/common/content_switches.h"
#include "gpu/ipc/common/surface_handle.h"
#include "services/network/public/mojom/network_service.mojom.h"
#include "ui/accelerated_widget_mac/ca_layer_frame_sink_provider.h"
namespace content {
namespace internal {
static base::NoDestructor<base::Lock> g_process_table_lock_;
static base::NoDestructor<
std::map<pid_t, scoped_refptr<ChildProcessLauncherHelper>>>
g_process_table_;
void InvalidateProcess(NSObject* process) {
if ([process isKindOfClass:[BEWebContentProcess class]]) {
[(BEWebContentProcess*)process invalidate];
} else if ([process isKindOfClass:[BENetworkingProcess class]]) {
[(BENetworkingProcess*)process invalidate];
} else if ([process isKindOfClass:[BERenderingProcess class]]) {
[(BERenderingProcess*)process invalidate];
}
}
void OnChildProcessTerminatedOnAnyThread(pid_t process_id) {
base::AutoLock guard(*g_process_table_lock_);
auto it = g_process_table_->find(process_id);
if (it != g_process_table_->end()) {
it->second->ClearProcessStorage();
g_process_table_->erase(it);
}
}
bool WaitForExit(pid_t process_id, int* exit_code, base::TimeDelta timeout) {
base::TimeTicks wakeup_time = base::TimeTicks::Now() + timeout;
constexpr uint32_t kMaxSleepInMicroseconds = 1 << 18; // ~256 ms.
uint32_t max_sleep_time_usecs = 1 << 10; // ~1 ms.
int double_sleep_time = 0;
while (true) {
{
base::AutoLock guard(*g_process_table_lock_);
auto it = g_process_table_->find(process_id);
if (it != g_process_table_->end()) {
if (it->second->GetProcess() == nullptr) {
if (exit_code) {
*exit_code = it->second->GetExitCode().value_or(0);
}
return true;
}
} else {
return true;
}
}
base::TimeTicks now = base::TimeTicks::Now();
if (now > wakeup_time) {
return false;
}
const uint32_t sleep_time_usecs = static_cast<uint32_t>(
std::min(static_cast<uint64_t>((wakeup_time - now).InMicroseconds()),
uint64_t{max_sleep_time_usecs}));
base::PlatformThread::Sleep(base::Microseconds(sleep_time_usecs));
if ((max_sleep_time_usecs < kMaxSleepInMicroseconds) &&
(double_sleep_time++ % 4 == 0)) {
max_sleep_time_usecs *= 2;
}
}
}
bool TerminateNow(pid_t process_id, int exit_code, bool wait) {
NSObject* process = nullptr;
{
base::AutoLock guard(*g_process_table_lock_);
auto it = g_process_table_->find(process_id);
if (it != g_process_table_->end()) {
it->second->SetExitCode(exit_code);
process = it->second->GetProcess();
}
}
if (!process) {
return false;
}
InvalidateProcess(process);
if (wait) {
return WaitForExit(process_id, nullptr, base::Seconds(60));
}
return true;
}
// Object used to pass the result of the launch from the async
// dispatch_queue to the LauncherThread.
class LaunchResult {
public:
void Invalidate() { InvalidateProcess(process); }
id<BEProcessCapabilityGrant> GrantForeground(NSError** error) {
id<BEProcessCapabilityGrant> grant;
BEProcessCapability* cap = [BEProcessCapability foreground];
if ([process isKindOfClass:[BEWebContentProcess class]]) {
grant = [(BEWebContentProcess*)process grantCapability:cap error:error];
} else if ([process isKindOfClass:[BENetworkingProcess class]]) {
grant = [(BENetworkingProcess*)process grantCapability:cap error:error];
} else if ([process isKindOfClass:[BERenderingProcess class]]) {
grant = [(BERenderingProcess*)process grantCapability:cap error:error];
}
return grant;
}
xpc_connection_t CreateXPCConnection(NSError** error) {
if ([process isKindOfClass:[BEWebContentProcess class]]) {
return [(BEWebContentProcess*)process makeLibXPCConnectionError:error];
} else if ([process isKindOfClass:[BENetworkingProcess class]]) {
return [(BENetworkingProcess*)process makeLibXPCConnectionError:error];
} else if ([process isKindOfClass:[BERenderingProcess class]]) {
return [(BERenderingProcess*)process makeLibXPCConnectionError:error];
}
return {};
}
NSObject* process;
NSError* launch_error;
};
// Object to store the process handles.
class ProcessStorage : public ProcessStorageBase {
public:
ProcessStorage(NSObject* process,
xpc_connection_t connection,
id<BEProcessCapabilityGrant> grant)
: process_(process), ipc_channel_(connection), grant_(grant) {}
~ProcessStorage() override { [grant_ invalidate]; }
void ReleaseProcess() override { process_ = nullptr; }
NSObject* Process() { return process_; }
private:
NSObject* process_;
[[maybe_unused]] xpc_connection_t ipc_channel_;
id<BEProcessCapabilityGrant> grant_;
};
std::optional<mojo::NamedPlatformChannel>
ChildProcessLauncherHelper::CreateNamedPlatformChannelOnLauncherThread() {
DCHECK(CurrentlyOnProcessLauncherTaskRunner());
return std::nullopt;
}
void ChildProcessLauncherHelper::BeforeLaunchOnClientThread() {
DCHECK(client_task_runner_->RunsTasksInCurrentSequence());
}
std::unique_ptr<PosixFileDescriptorInfo>
ChildProcessLauncherHelper::GetFilesToMap() {
DCHECK(CurrentlyOnProcessLauncherTaskRunner());
return CreateDefaultPosixFilesToMap(
child_process_id(), mojo_channel_->remote_endpoint(),
/*files_to_preload=*/{}, GetProcessType(), command_line());
}
bool ChildProcessLauncherHelper::BeforeLaunchOnLauncherThread(
FileMappedForLaunch& files_to_register,
base::LaunchOptions* options) {
mojo::PlatformHandle endpoint =
mojo_channel_->TakeRemoteEndpoint().TakePlatformHandle();
DCHECK(endpoint.is_valid_mach_receive());
options->mach_ports_for_rendezvous.insert(std::make_pair(
'mojo', base::MachRendezvousPort(endpoint.TakeMachReceiveRight())));
return true;
}
ChildProcessLauncherHelper::Process
ChildProcessLauncherHelper::LaunchProcessOnLauncherThread(
const base::LaunchOptions* options,
std::unique_ptr<PosixFileDescriptorInfo> files_to_register,
bool* is_synchronous_launch,
int* launch_result) {
DCHECK(options);
*is_synchronous_launch = false;
rendezvous_server_ = std::make_unique<base::MachPortRendezvousServerIOS>(
options->mach_ports_for_rendezvous);
// We need to hand out unique "process ids" just use a static counter
// for now. There should only be one launcher thread so this is
// synchronous access it doesn't need to be an atomic.
static pid_t g_pid = 0;
pid_t process_id = ++g_pid;
static bool g_hooks_registered = false;
if (!g_hooks_registered) {
base::Process::SetTerminationHooks(&TerminateNow, &WaitForExit);
g_hooks_registered = true;
}
void (^process_terminated)() = ^void() {
OnChildProcessTerminatedOnAnyThread(process_id);
};
std::string process_type = GetProcessType();
std::string utility_sub_type =
command_line()->GetSwitchValueASCII(switches::kUtilitySubType);
if (process_type == switches::kUtilityProcess) {
void (^process_launch_complete)(BENetworkingProcess* process,
NSError* error) =
^void(BENetworkingProcess* process, NSError* error) {
auto result = std::make_unique<LaunchResult>(process, error);
GetProcessLauncherTaskRunner()->PostTask(
FROM_HERE,
base::BindOnce(&ChildProcessLauncherHelper::OnChildProcessStarted,
this, process_id, std::move(result)));
};
[BENetworkingProcess
networkProcessWithInterruptionHandler:process_terminated
completion:process_launch_complete];
} else if (process_type == switches::kGpuProcess) {
void (^process_launch_complete)(BERenderingProcess* process,
NSError* error) =
^void(BERenderingProcess* process, NSError* error) {
auto result = std::make_unique<LaunchResult>(process, error);
GetProcessLauncherTaskRunner()->PostTask(
FROM_HERE,
base::BindOnce(&ChildProcessLauncherHelper::OnChildProcessStarted,
this, process_id, std::move(result)));
};
[BERenderingProcess
renderingProcessWithInterruptionHandler:process_terminated
completion:process_launch_complete];
} else {
// This can be both kUtility and kRenderProcess.
void (^process_launch_complete)(BEWebContentProcess* process,
NSError* error) =
^void(BEWebContentProcess* process, NSError* error) {
auto result = std::make_unique<LaunchResult>(process, error);
GetProcessLauncherTaskRunner()->PostTask(
FROM_HERE,
base::BindOnce(&ChildProcessLauncherHelper::OnChildProcessStarted,
this, process_id, std::move(result)));
};
[BEWebContentProcess
webContentProcessWithInterruptionHandler:process_terminated
completion:process_launch_complete];
}
AddRef();
return Process();
}
void ChildProcessLauncherHelper::OnChildProcessStarted(
pid_t process_id,
std::unique_ptr<LaunchResult> launch_result) {
DCHECK(CurrentlyOnProcessLauncherTaskRunner());
scoped_refptr<ChildProcessLauncherHelper> ref(this);
Release(); // Balances with LaunchProcessOnLauncherThread.
int launch_result_code = LAUNCH_RESULT_FAILURE;
if (!launch_result->launch_error) {
NSError* error = nil;
// TODO(dtapuska): For now we grant everything foreground capability. We
// need to hook this grant up to the
// `RenderProcessHostImpl::UpdateProcessPriority()`.
id<BEProcessCapabilityGrant> grant = launch_result->GrantForeground(&error);
xpc_connection_t xpc_connection =
launch_result->CreateXPCConnection(&error);
if (xpc_connection) {
scoped_refptr<base::SequencedTaskRunner> client_task_runner =
client_task_runner_;
bool is_gpu_process = GetProcessType() == switches::kGpuProcess;
xpc_connection_set_event_handler(xpc_connection, ^(xpc_object_t event) {
if (event == XPC_ERROR_CONNECTION_INTERRUPTED ||
event == XPC_ERROR_CONNECTION_INVALID) {
OnChildProcessTerminatedOnAnyThread(process_id);
return;
}
const char* message_type = xpc_dictionary_get_string(event, "message");
if (message_type && strcmp(message_type, "layerHandle") == 0) {
// We only expect this message from the GPU process.
if (!is_gpu_process) {
xpc_connection_cancel(xpc_connection);
return;
}
xpc_object_t ca_layer_handle =
xpc_dictionary_get_value(event, "layer");
gpu::SurfaceHandle view_handle =
xpc_dictionary_get_uint64(event, "handle");
// Validate arguments.
if (!ca_layer_handle || !view_handle) {
xpc_connection_cancel(xpc_connection);
return;
}
client_task_runner->PostTask(
FROM_HERE,
base::BindOnce(
[](gpu::SurfaceHandle view_handle,
xpc_object_t ca_layer_handle) {
NSError* error = nullptr;
BELayerHierarchyHandle* be_handle = [BELayerHierarchyHandle
handleWithXPCRepresentation:ca_layer_handle
error:&error];
CALayerFrameSinkProvider* sink =
[CALayerFrameSinkProvider lookupByHandle:view_handle];
if (sink) {
sink.handle = be_handle;
}
},
view_handle, ca_layer_handle));
}
});
xpc_connection_resume(xpc_connection);
xpc_object_t message = xpc_dictionary_create(nil, nil, 0);
xpc_object_t args_array = xpc_array_create_empty();
for (const auto& arg : command_line()->argv()) {
xpc_object_t value = xpc_string_create(arg.c_str());
xpc_array_append_value(args_array, value);
}
xpc_dictionary_set_value(message, "args", args_array);
xpc_dictionary_set_fd(message, "stdout", STDOUT_FILENO);
xpc_dictionary_set_fd(message, "stderr", STDERR_FILENO);
// We create a scoped temporary directory for the child process.
// In order to share this unique directory with the child process
// we need to create bookmark data and then pass this over XPC
// to the child process. The child process will then deserialize
// it before then assigning the TMPDIR environment variable. We
// do this via XPC so that it is done early enough in the process
// creation so TMPDIR is set before any real Chromium code runs.
scoped_temp_dir_ = std::make_unique<base::ScopedTempDir>();
CHECK(scoped_temp_dir_->CreateUniqueTempDir());
NSURL* temp_dir_url = [[NSURL alloc]
initFileURLWithPath:base::SysUTF8ToNSString(
scoped_temp_dir_->GetPath().value())];
NSData* bookmark_temp_dir = [temp_dir_url
bookmarkDataWithOptions:NSURLBookmarkCreationMinimalBookmark
includingResourceValuesForKeys:nil
relativeToURL:nil
error:&error];
xpc_dictionary_set_data(message, "tmp_dir", bookmark_temp_dir.bytes,
bookmark_temp_dir.length);
xpc_dictionary_set_mach_send(
message, "port", rendezvous_server_->GetMachSendRight().get());
xpc_connection_send_message(xpc_connection, message);
launch_result_code = LAUNCH_RESULT_SUCCESS;
// Keep reference to process, xpc_connection and the grant for the process
// life.
process_storage_ = std::make_unique<ProcessStorage>(
launch_result->process, xpc_connection, grant);
// Add the process to the global table.
{
base::AutoLock guard(*g_process_table_lock_);
CHECK(!base::Contains(*g_process_table_, process_id));
g_process_table_->emplace(process_id, this);
}
} else {
[grant invalidate];
launch_result->Invalidate();
}
}
ChildProcessLauncherHelper::Process process;
process.process = base::Process(process_id);
#if TARGET_OS_SIMULATOR
process.process.SetIsContentProcess();
#endif
PostLaunchOnLauncherThread(std::move(process), launch_result_code);
}
bool ChildProcessLauncherHelper::IsUsingLaunchOptions() {
return true;
}
void ChildProcessLauncherHelper::AfterLaunchOnLauncherThread(
const ChildProcessLauncherHelper::Process& process,
const base::LaunchOptions* options) {}
ChildProcessTerminationInfo ChildProcessLauncherHelper::GetTerminationInfo(
const ChildProcessLauncherHelper::Process& process,
bool known_dead) {
ChildProcessTerminationInfo info;
if (!process_storage_) {
info.status = base::TERMINATION_STATUS_LAUNCH_FAILED;
} else if (static_cast<ProcessStorage*>(process_storage_.get())->Process() ==
nullptr) {
if (exit_code_.has_value()) {
if (exit_code_.value() == RESULT_CODE_NORMAL_EXIT) {
info.status = base::TERMINATION_STATUS_NORMAL_TERMINATION;
} else {
info.status = base::TERMINATION_STATUS_PROCESS_WAS_KILLED;
}
} else {
info.status = base::TERMINATION_STATUS_PROCESS_CRASHED;
}
} else {
info.status = base::TERMINATION_STATUS_STILL_RUNNING;
}
return info;
}
void ChildProcessLauncherHelper::ClearProcessStorage() {
if (process_storage_) {
process_storage_->ReleaseProcess();
}
}
void ChildProcessLauncherHelper::SetExitCode(int exit_code) {
exit_code_ = exit_code;
}
std::optional<int> ChildProcessLauncherHelper::GetExitCode() {
return exit_code_;
}
NSObject* ChildProcessLauncherHelper::GetProcess() {
if (process_storage_) {
return static_cast<ProcessStorage*>(process_storage_.get())->Process();
}
return nullptr;
}
// static
bool ChildProcessLauncherHelper::TerminateProcess(const base::Process& process,
int exit_code) {
// TODO(crbug.com/40565504): Determine whether we should also call
// EnsureProcessTerminated() to make sure of process-exit, and reap it.
return process.Terminate(exit_code, false);
}
// static
void ChildProcessLauncherHelper::ForceNormalProcessTerminationSync(
ChildProcessLauncherHelper::Process process) {
DCHECK(CurrentlyOnProcessLauncherTaskRunner());
// Client has gone away, so just kill the process. Using exit code 0 means
// that UMA won't treat this as a crash.
process.process.Terminate(RESULT_CODE_NORMAL_EXIT, false);
base::EnsureProcessTerminated(std::move(process.process));
}
void ChildProcessLauncherHelper::SetProcessPriorityOnLauncherThread(
base::Process process,
base::Process::Priority priority) {}
// static
base::File OpenFileToShare(const base::FilePath& path,
base::MemoryMappedFile::Region* region) {
// Not used yet (until required files are described in the service manifest on
// iOS).
NOTREACHED();
}
} // namespace internal
} // namespace content