chromium/mojo/proxy/portal_proxy.cc

// 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