// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "mojo/proxy/portal_proxy.h"
#include <cstddef>
#include <cstdint>
#include <utility>
#include <vector>
#include "base/check.h"
#include "base/memory/platform_shared_memory_region.h"
#include "base/memory/raw_ref.h"
#include "base/notreached.h"
#include "mojo/core/ipcz_driver/object.h"
#include "mojo/core/ipcz_driver/shared_buffer.h"
#include "mojo/core/ipcz_driver/wrapped_platform_handle.h"
#include "mojo/proxy/node_proxy.h"
#include "mojo/public/c/system/buffer.h"
#include "mojo/public/c/system/platform_handle.h"
#include "mojo/public/c/system/trap.h"
#include "mojo/public/c/system/types.h"
#include "mojo/public/cpp/platform/platform_handle.h"
#include "mojo/public/cpp/system/message_pipe.h"
#include "mojo/public/cpp/system/platform_handle.h"
#include "mojo/public/cpp/system/trap.h"
#include "third_party/ipcz/include/ipcz/ipcz.h"
namespace mojo_proxy {
using mojo::core::ScopedIpczHandle;
PortalProxy::PortalProxy(const raw_ref<const IpczAPI> ipcz,
NodeProxy& node_proxy,
ScopedIpczHandle portal,
mojo::ScopedMessagePipeHandle pipe)
: ipcz_(ipcz),
node_proxy_(node_proxy),
portal_(std::move(portal)),
pipe_(std::move(pipe)) {
CHECK_EQ(mojo::CreateTrap(&OnMojoPipeActivity, &pipe_trap_), MOJO_RESULT_OK);
const MojoResult add_trigger_result = MojoAddTrigger(
pipe_trap_->value(), pipe_->value(), MOJO_HANDLE_SIGNAL_READABLE,
MOJO_TRIGGER_CONDITION_SIGNALS_SATISFIED, trap_context(), nullptr);
CHECK_EQ(add_trigger_result, MOJO_RESULT_OK);
}
PortalProxy::~PortalProxy() = default;
void PortalProxy::Start() {
CHECK(!disconnected_);
CHECK(!watching_portal_);
CHECK(!watching_pipe_);
Flush();
}
void PortalProxy::Flush() {
CHECK(!in_flush_);
in_flush_ = true;
while (!disconnected_ && (!watching_portal_ || !watching_pipe_)) {
if (!disconnected_ && !watching_portal_) {
FlushAndWatchPortal();
}
if (!disconnected_ && !watching_pipe_) {
FlushAndWatchPipe();
}
}
in_flush_ = false;
if (disconnected_) {
// Deletes `this`.
Die();
}
}
void PortalProxy::FlushAndWatchPortal() {
for (;;) {
std::vector<uint8_t> data;
size_t num_bytes = 0;
std::vector<IpczHandle> handles;
size_t num_handles = 0;
IpczResult result =
ipcz_->Get(portal_.get(), IPCZ_NO_FLAGS, nullptr, nullptr, &num_bytes,
nullptr, &num_handles, nullptr);
if (result == IPCZ_RESULT_OK) {
mojo::WriteMessageRaw(pipe_.get(), nullptr, 0, nullptr, 0,
MOJO_WRITE_MESSAGE_FLAG_NONE);
continue;
}
if (result == IPCZ_RESULT_UNAVAILABLE) {
break;
}
if (result != IPCZ_RESULT_RESOURCE_EXHAUSTED) {
disconnected_ = true;
return;
}
data.resize(num_bytes);
handles.resize(num_handles);
result = ipcz_->Get(portal_.get(), IPCZ_NO_FLAGS, nullptr, data.data(),
&num_bytes, handles.data(), &num_handles, nullptr);
CHECK_EQ(result, IPCZ_RESULT_OK);
std::vector<MojoHandle> mojo_handles;
mojo_handles.reserve(handles.size());
for (IpczHandle handle : handles) {
mojo_handles.push_back(TranslateIpczToMojoHandle(ScopedIpczHandle(handle))
.release()
.value());
}
mojo::WriteMessageRaw(pipe_.get(), data.data(), data.size(),
mojo_handles.data(), mojo_handles.size(),
MOJO_WRITE_MESSAGE_FLAG_NONE);
}
IpczTrapConditionFlags flags;
const IpczTrapConditions trap_conditions{
.size = sizeof(trap_conditions),
.flags = IPCZ_TRAP_ABOVE_MIN_LOCAL_PARCELS | IPCZ_TRAP_DEAD,
.min_local_parcels = 0,
};
const IpczResult trap_result =
ipcz_->Trap(portal_.get(), &trap_conditions, &OnIpczPortalActivity,
trap_context(), IPCZ_NO_FLAGS, nullptr, &flags, nullptr);
if (trap_result == IPCZ_RESULT_OK) {
watching_portal_ = true;
return;
}
CHECK_EQ(trap_result, IPCZ_RESULT_FAILED_PRECONDITION);
if (flags & IPCZ_TRAP_DEAD) {
disconnected_ = true;
}
}
void PortalProxy::FlushAndWatchPipe() {
for (;;) {
std::vector<uint8_t> data;
std::vector<mojo::ScopedHandle> handles;
const MojoResult result = mojo::ReadMessageRaw(pipe_.get(), &data, &handles,
MOJO_READ_MESSAGE_FLAG_NONE);
if (result == MOJO_RESULT_SHOULD_WAIT) {
break;
}
if (result != MOJO_RESULT_OK) {
disconnected_ = true;
return;
}
std::vector<IpczHandle> ipcz_handles;
ipcz_handles.reserve(handles.size());
for (mojo::ScopedHandle& handle : handles) {
ipcz_handles.push_back(
TranslateMojoToIpczHandle(std::move(handle)).release());
}
const IpczResult put_result = ipcz_->Put(
portal_.get(), data.size() ? data.data() : nullptr, data.size(),
ipcz_handles.size() ? ipcz_handles.data() : nullptr,
ipcz_handles.size(), IPCZ_NO_FLAGS, nullptr);
if (put_result != IPCZ_RESULT_OK) {
disconnected_ = true;
return;
}
}
uint32_t num_events = 1;
MojoTrapEvent event{.struct_size = sizeof(event)};
const MojoResult result =
MojoArmTrap(pipe_trap_->value(), nullptr, &num_events, &event);
if (result == MOJO_RESULT_OK) {
watching_pipe_ = true;
return;
}
CHECK_EQ(result, MOJO_RESULT_FAILED_PRECONDITION);
CHECK_EQ(num_events, 1u);
if (event.result == MOJO_RESULT_FAILED_PRECONDITION) {
disconnected_ = true;
}
}
ScopedIpczHandle PortalProxy::TranslateMojoToIpczHandle(
mojo::ScopedHandle handle) {
// We don't know what kind of handle is in `handle`, but we can find out.
// First try to unwrap it as a generic platform handle.
MojoPlatformHandle platform_handle;
platform_handle.struct_size = sizeof(platform_handle);
const MojoResult unwrap_result =
MojoUnwrapPlatformHandle(handle->value(), nullptr, &platform_handle);
if (unwrap_result == MOJO_RESULT_OK) {
std::ignore = handle.release();
// Platform handles in ipcz are transmitted as boxed driver objects.
return ScopedIpczHandle(
mojo::core::ipcz_driver::WrappedPlatformHandle::MakeBoxed(
mojo::PlatformHandle::FromMojoPlatformHandle(&platform_handle)));
}
// We can non-destructively probe for a shared buffer handle by calling
// MojoGetBufferInfo().
MojoSharedBufferInfo info = {.struct_size = sizeof(info)};
const MojoResult info_result =
MojoGetBufferInfo(handle->value(), nullptr, &info);
if (info_result == MOJO_RESULT_OK) {
auto region =
mojo::UnwrapPlatformSharedMemoryRegion(mojo::ScopedSharedBufferHandle{
mojo::SharedBufferHandle{handle.release().value()}});
return ScopedIpczHandle(
mojo::core::ipcz_driver::SharedBuffer::MakeBoxed(std::move(region)));
}
// Since data pipe handles are never used on Chrome OS IPC boundaries outside
// the browser, we can assume that any other handles are message pipes.
IpczHandle portal_to_proxy, portal_to_host;
ipcz_->OpenPortals(mojo::core::GetIpczNode(), IPCZ_NO_FLAGS, nullptr,
&portal_to_proxy, &portal_to_host);
node_proxy_->AddPortalProxy(
ScopedIpczHandle{portal_to_proxy},
mojo::ScopedMessagePipeHandle{
mojo::MessagePipeHandle{handle.release().value()}});
return ScopedIpczHandle(portal_to_host);
}
mojo::ScopedHandle PortalProxy::TranslateIpczToMojoHandle(
ScopedIpczHandle handle) {
// Attempt a QueryPortalStatus() call. If this succeeds, we have a portal.
IpczPortalStatus status = {.size = sizeof(status)};
const IpczResult query_result =
ipcz_->QueryPortalStatus(handle.get(), IPCZ_NO_FLAGS, nullptr, &status);
if (query_result == IPCZ_RESULT_OK) {
// Create a new Mojo message pipe to proxy through. One end is bound to a
// new PortalProxy with the input `handle`; the other is returned to be
// forwarded to the legacy client.
mojo::MessagePipe pipe;
node_proxy_->AddPortalProxy(std::move(handle), std::move(pipe.handle0));
return mojo::ScopedHandle{mojo::Handle{pipe.handle1.release().value()}};
}
// Otherwise assume it's a boxed driver object. If it's not, something has
// gone horribly wrong, so just crash.
auto* object = mojo::core::ipcz_driver::ObjectBase::FromBox(handle.get());
CHECK(object);
switch (object->type()) {
case mojo::core::ipcz_driver::ObjectBase::Type::kWrappedPlatformHandle: {
auto wrapped_handle =
mojo::core::ipcz_driver::WrappedPlatformHandle::Unbox(
handle.release());
return mojo::WrapPlatformHandle(wrapped_handle->TakeHandle());
}
case mojo::core::ipcz_driver::ObjectBase::Type::kSharedBuffer: {
auto buffer =
mojo::core::ipcz_driver::SharedBuffer::Unbox(handle.release());
auto mojo_buffer =
mojo::WrapPlatformSharedMemoryRegion(std::move(buffer->region()));
return mojo::ScopedHandle{mojo::Handle{mojo_buffer.release().value()}};
}
default:
// No other types of driver objects are supported by the proxy.
NOTREACHED();
}
}
void PortalProxy::HandlePortalActivity(IpczTrapConditionFlags flags) {
if (flags & IPCZ_TRAP_REMOVED) {
// Proxy is being shut down. Do nothing.
return;
}
watching_portal_ = false;
if (flags & IPCZ_TRAP_DEAD) {
disconnected_ = true;
if (!in_flush_) {
// Deletes `this`.
Die();
return;
}
} else if (!in_flush_) {
Flush();
}
}
void PortalProxy::HandlePipeActivity(MojoResult result) {
if (result == MOJO_RESULT_CANCELLED) {
// Proxy is being shut down. Do nothing.
return;
}
watching_pipe_ = false;
if (result == MOJO_RESULT_FAILED_PRECONDITION) {
disconnected_ = true;
if (!in_flush_) {
// Deletes `this`.
Die();
return;
}
} else if (!in_flush_) {
Flush();
}
}
void PortalProxy::Die() {
CHECK(!in_flush_);
CHECK(disconnected_);
// Deletes `this`.
node_proxy_->RemovePortalProxy(this);
}
} // namespace mojo_proxy