blob: aec2f7f9eb9a59560076e4389c4c49189f0a7441 [file] [log] [blame]
// Copyright 2015 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 "components/arc/arc_bridge_bootstrap.h"
#include <fcntl.h>
#include <grp.h>
#include <poll.h>
#include <unistd.h>
#include <utility>
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/location.h"
#include "base/macros.h"
#include "base/memory/ptr_util.h"
#include "base/posix/eintr_wrapper.h"
#include "base/sys_info.h"
#include "base/task_runner_util.h"
#include "base/threading/thread_checker.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/threading/worker_pool.h"
#include "chromeos/cryptohome/cryptohome_parameters.h"
#include "chromeos/dbus/dbus_method_call_status.h"
#include "chromeos/dbus/dbus_thread_manager.h"
#include "chromeos/dbus/session_manager_client.h"
#include "components/user_manager/user_manager.h"
#include "ipc/unix_domain_socket_util.h"
#include "mojo/edk/embedder/embedder.h"
#include "mojo/edk/embedder/platform_channel_pair.h"
#include "mojo/edk/embedder/platform_channel_utils_posix.h"
#include "mojo/edk/embedder/platform_handle_vector.h"
#include "mojo/edk/embedder/scoped_platform_handle.h"
#include "mojo/public/cpp/bindings/binding.h"
namespace arc {
namespace {
const base::FilePath::CharType kArcBridgeSocketPath[] =
FILE_PATH_LITERAL("/var/run/chrome/arc_bridge.sock");
const char kArcBridgeSocketGroup[] = "arc-bridge";
const base::FilePath::CharType kDiskCheckPath[] = "/home";
const int64_t kCriticalDiskFreeBytes = 64 << 20; // 64MB
// This is called when StopArcInstance D-Bus method completes. Since we have the
// ArcInstanceStopped() callback and are notified if StartArcInstance fails, we
// don't need to do anything when StopArcInstance completes.
void DoNothingInstanceStopped(bool) {}
chromeos::SessionManagerClient* GetSessionManagerClient() {
// If the DBusThreadManager or the SessionManagerClient aren't available,
// there isn't much we can do. This should only happen when running tests.
if (!chromeos::DBusThreadManager::IsInitialized() ||
!chromeos::DBusThreadManager::Get() ||
!chromeos::DBusThreadManager::Get()->GetSessionManagerClient())
return nullptr;
return chromeos::DBusThreadManager::Get()->GetSessionManagerClient();
}
// Creates a pipe. Returns true on success, otherwise false.
// On success, |read_fd| will be set to the fd of the read side, and
// |write_fd| will be set to the one of write side.
bool CreatePipe(base::ScopedFD* read_fd, base::ScopedFD* write_fd) {
int fds[2];
if (pipe2(fds, O_NONBLOCK | O_CLOEXEC) < 0) {
PLOG(ERROR) << "pipe2()";
return false;
}
read_fd->reset(fds[0]);
write_fd->reset(fds[1]);
return true;
}
// Waits until |raw_socket_fd| is readable.
// The operation may be cancelled originally triggered by user interaction to
// disable ARC, or ARC instance is unexpectedly stopped (e.g. crash).
// To notify such a situation, |raw_cancel_fd| is also passed to here, and the
// write side will be closed in such a case.
bool WaitForSocketReadable(int raw_socket_fd, int raw_cancel_fd) {
struct pollfd fds[2] = {
{raw_socket_fd, POLLIN, 0}, {raw_cancel_fd, POLLIN, 0},
};
if (HANDLE_EINTR(poll(fds, arraysize(fds), -1)) <= 0) {
PLOG(ERROR) << "poll()";
return false;
}
if (fds[1].revents) {
// Notified that Stop() is invoked. Cancel the Mojo connecting.
VLOG(1) << "Stop() was called during ConnectMojo()";
return false;
}
DCHECK(fds[0].revents);
return true;
}
// TODO(hidehiko): Refactor more to make this class unittest-able, for at least
// state-machine part.
class ArcBridgeBootstrapImpl : public ArcBridgeBootstrap,
public chromeos::SessionManagerClient::Observer {
public:
// The possible states of the bootstrap connection. In the normal flow,
// the state changes in the following sequence:
//
// NOT_STARTED
// Start() ->
// CHECKING_DISK_SPACE
// OnDiskSpaceChecked() ->
// CREATING_SOCKET
// CreateSocket() -> OnSocketCreated() ->
// STARTING_INSTANCE
// -> OnInstanceStarted() ->
// CONNECTING_MOJO
// ConnectMojo() -> OnMojoConnected() ->
// RUNNING
//
// At any state, Stop() can be called. It does not immediately stop the
// instance, but will eventually stop it.
// The actual stop will be notified via OnStopped() of the |delegate_|.
//
// When Stop() is called, it makes various behavior based on the current
// phase.
//
// NOT_STARTED:
// Do nothing. Immediately transition to the STOPPED state.
// CHECKING_DISK_SPACE, CREATING_SOCKET:
// The main task of those phases runs on WorkerPool thread. So, Stop()
// just sets the flag and return. On the main task completion, a callback
// will run on the main (practically UI) thread, and the flag is checked
// at the beginning of them. This should work under the assumption that
// the main tasks do not block indefinitely.
// STARTING_INSTANCE:
// The ARC instance is starting via SessionManager. So, similar to
// CHECKING_DISK_SPACE/CREATING_SOCKET cases, Stop() just sets the flag
// and return. In its callback, it checks if ARC instance is successfully
// started or not. In case of success, a request to stop the ARC instance
// is sent to SessionManager. Its completion will be notified via
// ArcInstanceStopped. Otherwise, it just turns into STOPPED state.
// CONNECTING_MOJO:
// The main task runs on WorkerPool thread, but it is blocking call.
// So, Stop() sends a request to cancel the blocking by closing the pipe
// whose read side is also polled. Then, in its callback, similar to
// STARTING_INSTANCE, a request to stop the ARC instance is sent to
// SessionManager, and ArcInstanceStopped handles remaining procedure.
// RUNNING:
// There is no more callback which runs on normal flow, so Stop() requests
// to stop the ARC instance via SessionManager.
//
// Another trigger to change the state coming from outside of this class
// is an event ArcInstanceStopped() sent from SessionManager, when ARC
// instace unexpectedly terminates. ArcInstanceStopped() turns the state into
// STOPPED immediately.
// This happens only when STARTING_INSTANCE, CONNECTING_MOJO or RUNNING
// state.
//
// STARTING_INSTANCE:
// In OnInstanceStarted(), |state_| is checked at the beginning. If it is
// STOPPED, then ArcInstanceStopped() is called. Do nothing in that case.
// CONNECTING_MOJO:
// Similar to Stop() case above, ArcInstanceStopped() also notifies to
// WorkerPool() thread to cancel it to unblock the thread. In
// OnMojoConnected(), similar to OnInstanceStarted(), check if |state_| is
// STOPPED, then do nothing.
// RUNNING:
// It is not necessary to do anything special here.
//
// In NOT_STARTED or STOPPED state, the instance can be safely destructed.
// Specifically, in STOPPED state, there may be inflight operations or
// pending callbacks. Though, what they do is just do-nothing conceptually
// and they can be safely ignored.
//
// Note: Order of constants below matters. Please make sure to sort them
// in chronological order.
enum class State {
// ARC is not yet started.
NOT_STARTED,
// Checking the disk space.
CHECKING_DISK_SPACE,
// An UNIX socket is being created.
CREATING_SOCKET,
// The request to start the instance has been sent.
STARTING_INSTANCE,
// The instance has started. Waiting for it to connect to the IPC bridge.
CONNECTING_MOJO,
// The instance is fully set up.
RUNNING,
// ARC is terminated.
STOPPED,
};
ArcBridgeBootstrapImpl();
~ArcBridgeBootstrapImpl() override;
// ArcBridgeBootstrap:
void Start() override;
void Stop() override;
private:
// Called after getting the device free disk space.
void OnFreeDiskSpaceObtained(int64_t disk_free_bytes);
// Creates the UNIX socket on the bootstrap thread and then processes its
// file descriptor.
static base::ScopedFD CreateSocket();
void OnSocketCreated(base::ScopedFD fd);
// DBus callback for StartArcInstance().
void OnInstanceStarted(base::ScopedFD socket_fd, bool success);
// Synchronously accepts a connection on |socket_fd| and then processes the
// connected socket's file descriptor.
static base::ScopedFD ConnectMojo(base::ScopedFD socket_fd,
base::ScopedFD cancel_fd);
void OnMojoConnected(base::ScopedFD fd);
// Request to stop ARC instance via DBus.
void StopArcInstance();
// chromeos::SessionManagerClient::Observer:
void ArcInstanceStopped(bool clean) override;
// Completes the termination procedure.
void OnStopped(ArcBridgeService::StopReason reason);
// The state of the bootstrap connection.
State state_ = State::NOT_STARTED;
// When Stop() is called, this flag is set.
bool stop_requested_ = false;
// In CONNECTING_MOJO state, this is set to the write side of the pipe
// to notify cancelling of the procedure.
base::ScopedFD accept_cancel_pipe_;
base::ThreadChecker thread_checker_;
// WeakPtrFactory to use callbacks.
base::WeakPtrFactory<ArcBridgeBootstrapImpl> weak_factory_;
private:
DISALLOW_COPY_AND_ASSIGN(ArcBridgeBootstrapImpl);
};
ArcBridgeBootstrapImpl::ArcBridgeBootstrapImpl()
: weak_factory_(this) {
chromeos::SessionManagerClient* client = GetSessionManagerClient();
if (client == nullptr)
return;
client->AddObserver(this);
}
ArcBridgeBootstrapImpl::~ArcBridgeBootstrapImpl() {
DCHECK(thread_checker_.CalledOnValidThread());
// TODO(hidehiko): CHECK if |state_| is in NOT_STARTED or STOPPED.
// Currently, specifically on shutdown, the state_ can be any value.
chromeos::SessionManagerClient* client = GetSessionManagerClient();
if (client == nullptr)
return;
client->RemoveObserver(this);
}
void ArcBridgeBootstrapImpl::Start() {
DCHECK(thread_checker_.CalledOnValidThread());
DCHECK(delegate_);
DCHECK_EQ(state_, State::NOT_STARTED);
VLOG(2) << "Starting ARC session.";
VLOG(2) << "Checking disk space...";
state_ = State::CHECKING_DISK_SPACE;
// TODO(crbug.com/628124): Move disk space checking logic to session_manager.
base::PostTaskAndReplyWithResult(
base::WorkerPool::GetTaskRunner(true).get(), FROM_HERE,
base::Bind(&base::SysInfo::AmountOfFreeDiskSpace,
base::FilePath(kDiskCheckPath)),
base::Bind(&ArcBridgeBootstrapImpl::OnFreeDiskSpaceObtained,
weak_factory_.GetWeakPtr()));
}
void ArcBridgeBootstrapImpl::OnFreeDiskSpaceObtained(int64_t disk_free_bytes) {
DCHECK(thread_checker_.CalledOnValidThread());
DCHECK_EQ(state_, State::CHECKING_DISK_SPACE);
if (stop_requested_) {
VLOG(1) << "Stop() called while checking disk space";
OnStopped(ArcBridgeService::StopReason::SHUTDOWN);
return;
}
if (disk_free_bytes < 0) {
LOG(ERROR) << "ARC: Failed to get free disk space";
OnStopped(ArcBridgeService::StopReason::GENERIC_BOOT_FAILURE);
return;
}
if (disk_free_bytes < kCriticalDiskFreeBytes) {
LOG(ERROR) << "ARC: The device is too low on disk space to start ARC";
OnStopped(ArcBridgeService::StopReason::LOW_DISK_SPACE);
return;
}
VLOG(2) << "Disk space check is done. Creating socket...";
state_ = State::CREATING_SOCKET;
base::PostTaskAndReplyWithResult(
base::WorkerPool::GetTaskRunner(true).get(), FROM_HERE,
base::Bind(&ArcBridgeBootstrapImpl::CreateSocket),
base::Bind(&ArcBridgeBootstrapImpl::OnSocketCreated,
weak_factory_.GetWeakPtr()));
}
// static
base::ScopedFD ArcBridgeBootstrapImpl::CreateSocket() {
base::FilePath socket_path(kArcBridgeSocketPath);
int raw_fd = -1;
if (!IPC::CreateServerUnixDomainSocket(socket_path, &raw_fd))
return base::ScopedFD();
base::ScopedFD socket_fd(raw_fd);
// Change permissions on the socket.
struct group arc_bridge_group;
struct group* arc_bridge_group_res = nullptr;
char buf[10000];
if (HANDLE_EINTR(getgrnam_r(kArcBridgeSocketGroup, &arc_bridge_group, buf,
sizeof(buf), &arc_bridge_group_res)) < 0) {
PLOG(ERROR) << "getgrnam_r";
return base::ScopedFD();
}
if (!arc_bridge_group_res) {
LOG(ERROR) << "Group '" << kArcBridgeSocketGroup << "' not found";
return base::ScopedFD();
}
if (HANDLE_EINTR(chown(kArcBridgeSocketPath, -1, arc_bridge_group.gr_gid)) <
0) {
PLOG(ERROR) << "chown";
return base::ScopedFD();
}
if (!base::SetPosixFilePermissions(socket_path, 0660)) {
PLOG(ERROR) << "Could not set permissions: " << socket_path.value();
return base::ScopedFD();
}
return socket_fd;
}
void ArcBridgeBootstrapImpl::OnSocketCreated(base::ScopedFD socket_fd) {
DCHECK(thread_checker_.CalledOnValidThread());
DCHECK_EQ(state_, State::CREATING_SOCKET);
if (stop_requested_) {
VLOG(1) << "Stop() called while connecting";
OnStopped(ArcBridgeService::StopReason::SHUTDOWN);
return;
}
if (!socket_fd.is_valid()) {
LOG(ERROR) << "ARC: Error creating socket";
OnStopped(ArcBridgeService::StopReason::GENERIC_BOOT_FAILURE);
return;
}
VLOG(2) << "Socket is created. Starting ARC instance...";
state_ = State::STARTING_INSTANCE;
user_manager::UserManager* user_manager = user_manager::UserManager::Get();
DCHECK(user_manager->GetPrimaryUser());
const cryptohome::Identification cryptohome_id(
user_manager->GetPrimaryUser()->GetAccountId());
chromeos::SessionManagerClient* session_manager_client =
chromeos::DBusThreadManager::Get()->GetSessionManagerClient();
session_manager_client->StartArcInstance(
cryptohome_id,
base::Bind(&ArcBridgeBootstrapImpl::OnInstanceStarted,
weak_factory_.GetWeakPtr(), base::Passed(&socket_fd)));
}
void ArcBridgeBootstrapImpl::OnInstanceStarted(base::ScopedFD socket_fd,
bool success) {
DCHECK(thread_checker_.CalledOnValidThread());
if (state_ == State::STOPPED) {
// This is the case that error is notified via DBus before the
// OnInstanceStarted() callback is invoked. The stopping procedure has
// been run, so do nothing.
return;
}
DCHECK_EQ(state_, State::STARTING_INSTANCE);
if (stop_requested_) {
if (success) {
// The ARC instance has started to run. Request to stop.
StopArcInstance();
return;
}
OnStopped(ArcBridgeService::StopReason::SHUTDOWN);
return;
}
if (!success) {
LOG(ERROR) << "Failed to start ARC instance";
OnStopped(ArcBridgeService::StopReason::GENERIC_BOOT_FAILURE);
return;
}
VLOG(2) << "ARC instance is successfully started. Connecting Mojo...";
state_ = State::CONNECTING_MOJO;
// Prepare a pipe so that AcceptInstanceConnection can be interrupted on
// Stop().
base::ScopedFD cancel_fd;
if (!CreatePipe(&cancel_fd, &accept_cancel_pipe_)) {
OnStopped(ArcBridgeService::StopReason::GENERIC_BOOT_FAILURE);
return;
}
base::PostTaskAndReplyWithResult(
base::WorkerPool::GetTaskRunner(true).get(), FROM_HERE,
base::Bind(&ArcBridgeBootstrapImpl::ConnectMojo, base::Passed(&socket_fd),
base::Passed(&cancel_fd)),
base::Bind(&ArcBridgeBootstrapImpl::OnMojoConnected,
weak_factory_.GetWeakPtr()));
}
// static
base::ScopedFD ArcBridgeBootstrapImpl::ConnectMojo(base::ScopedFD socket_fd,
base::ScopedFD cancel_fd) {
if (!WaitForSocketReadable(socket_fd.get(), cancel_fd.get())) {
VLOG(1) << "Mojo connection was cancelled.";
return base::ScopedFD();
}
int raw_fd = -1;
if (!IPC::ServerOnConnect(socket_fd.get(), &raw_fd)) {
return base::ScopedFD();
}
base::ScopedFD scoped_fd(raw_fd);
// Hardcode pid 0 since it is unused in mojo.
const base::ProcessHandle kUnusedChildProcessHandle = 0;
mojo::edk::PlatformChannelPair channel_pair;
mojo::edk::ChildProcessLaunched(kUnusedChildProcessHandle,
channel_pair.PassServerHandle(),
mojo::edk::GenerateRandomToken());
mojo::edk::ScopedPlatformHandleVectorPtr handles(
new mojo::edk::PlatformHandleVector{
channel_pair.PassClientHandle().release()});
struct iovec iov = {const_cast<char*>(""), 1};
ssize_t result = mojo::edk::PlatformChannelSendmsgWithHandles(
mojo::edk::PlatformHandle(scoped_fd.get()), &iov, 1, handles->data(),
handles->size());
if (result == -1) {
PLOG(ERROR) << "sendmsg";
return base::ScopedFD();
}
return scoped_fd;
}
void ArcBridgeBootstrapImpl::OnMojoConnected(base::ScopedFD fd) {
DCHECK(thread_checker_.CalledOnValidThread());
if (state_ == State::STOPPED) {
// This is the case that error is notified via DBus before the
// OnMojoConnected() callback is invoked. The stopping procedure has
// been run, so do nothing.
return;
}
DCHECK_EQ(state_, State::CONNECTING_MOJO);
accept_cancel_pipe_.reset();
if (stop_requested_) {
StopArcInstance();
return;
}
if (!fd.is_valid()) {
LOG(ERROR) << "Invalid handle";
StopArcInstance();
return;
}
mojo::ScopedMessagePipeHandle server_pipe = mojo::edk::CreateMessagePipe(
mojo::edk::ScopedPlatformHandle(mojo::edk::PlatformHandle(fd.release())));
if (!server_pipe.is_valid()) {
LOG(ERROR) << "Invalid pipe";
StopArcInstance();
return;
}
mojom::ArcBridgeInstancePtr instance;
instance.Bind(mojo::InterfacePtrInfo<mojom::ArcBridgeInstance>(
std::move(server_pipe), 0u));
// TODO(hidehiko): Move to creating ArcBridgeHost here to fix the twisted
// state change.
VLOG(2) << "Mojo is connected. ARC is running.";
state_ = State::RUNNING;
delegate_->OnConnectionEstablished(std::move(instance));
}
void ArcBridgeBootstrapImpl::Stop() {
DCHECK(thread_checker_.CalledOnValidThread());
DCHECK(delegate_);
VLOG(2) << "Stopping ARC session is requested.";
// For second time or later, just do nothing.
// It is already in the stopping phase.
if (stop_requested_)
return;
stop_requested_ = true;
switch (state_) {
case State::NOT_STARTED:
OnStopped(ArcBridgeService::StopReason::SHUTDOWN);
return;
case State::CHECKING_DISK_SPACE:
case State::CREATING_SOCKET:
case State::STARTING_INSTANCE:
// Before starting the ARC instance, we do nothing here.
// At some point, a callback will be invoked on UI thread,
// and stopping procedure will be run there.
// On Chrome shutdown, it is not the case because the message loop is
// already stopped here. Practically, it is not a problem because;
// - On disk space checking or on socket creating, it is ok to simply
// ignore such cases, because we no-longer continue the bootstrap
// procedure.
// - On starting instance, the container instance can be leaked.
// Practically it is not problematic because the session manager will
// clean it up.
return;
case State::CONNECTING_MOJO:
// Mojo connection is being waited on a WorkerPool thread.
// Request to cancel it. Following stopping procedure will run
// in its callback.
DCHECK(accept_cancel_pipe_.get());
accept_cancel_pipe_.reset();
return;
case State::RUNNING:
// Now ARC instance is running. Request to stop it.
StopArcInstance();
return;
case State::STOPPED:
// The instance is already stopped. Do nothing.
return;
}
}
void ArcBridgeBootstrapImpl::StopArcInstance() {
DCHECK(thread_checker_.CalledOnValidThread());
DCHECK(state_ == State::STARTING_INSTANCE ||
state_ == State::CONNECTING_MOJO || state_ == State::RUNNING);
// Notification will arrive through ArcInstanceStopped().
VLOG(2) << "Requesting to stop ARC instance";
chromeos::SessionManagerClient* session_manager_client =
chromeos::DBusThreadManager::Get()->GetSessionManagerClient();
session_manager_client->StopArcInstance(
base::Bind(&DoNothingInstanceStopped));
}
void ArcBridgeBootstrapImpl::ArcInstanceStopped(bool clean) {
DCHECK(thread_checker_.CalledOnValidThread());
VLOG(1) << "Notified that ARC instance is stopped "
<< (clean ? "cleanly" : "uncleanly");
// In case that crash happens during before the Mojo channel is connected,
// unlock the WorkerPool thread.
accept_cancel_pipe_.reset();
ArcBridgeService::StopReason reason;
if (stop_requested_) {
// If the ARC instance is stopped after its explicit request,
// return SHUTDOWN.
reason = ArcBridgeService::StopReason::SHUTDOWN;
} else if (clean) {
// If the ARC instance is stopped, but it is not explicitly requested,
// then this is triggered by some failure during the starting procedure.
// Return GENERIC_BOOT_FAILURE for the case.
reason = ArcBridgeService::StopReason::GENERIC_BOOT_FAILURE;
} else {
// Otherwise, this is caused by CRASH occured inside of the ARC instance.
reason = ArcBridgeService::StopReason::CRASH;
}
OnStopped(reason);
}
void ArcBridgeBootstrapImpl::OnStopped(ArcBridgeService::StopReason reason) {
DCHECK(thread_checker_.CalledOnValidThread());
// OnStopped() should be called once per instance.
DCHECK_NE(state_, State::STOPPED);
VLOG(2) << "ARC session is stopped.";
state_ = State::STOPPED;
delegate_->OnStopped(reason);
}
} // namespace
// static
std::unique_ptr<ArcBridgeBootstrap> ArcBridgeBootstrap::Create() {
return base::MakeUnique<ArcBridgeBootstrapImpl>();
}
} // namespace arc