[go: nahoru, domu]

blob: 7a5765a74a7f64e584b0a080e659c88c019adcbb [file] [log] [blame]
// Copyright 2022 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/core/ipcz_driver/mojo_trap.h"
#include <cstdint>
#include <tuple>
#include <utility>
#include "base/check_op.h"
#include "base/memory/ref_counted.h"
#include "base/notreached.h"
#include "base/threading/platform_thread.h"
#include "base/threading/thread_restrictions.h"
#include "mojo/core/ipcz_api.h"
#include "mojo/core/ipcz_driver/data_pipe.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "third_party/ipcz/include/ipcz/ipcz.h"
namespace mojo::core::ipcz_driver {
namespace {
// Translates Mojo signal conditions to equivalent IpczTrapConditions for any
// portal used as a message pipe endpoint.
void GetConditionsForMessagePipeSignals(MojoHandleSignals signals,
IpczTrapConditions* conditions) {
conditions->flags |= IPCZ_TRAP_DEAD;
if (signals & MOJO_HANDLE_SIGNAL_READABLE) {
// Mojo's readable signal is equivalent to the condition of having more than
// zero parcels available to retrieve from a portal.
conditions->flags |= IPCZ_TRAP_ABOVE_MIN_LOCAL_PARCELS;
conditions->min_local_parcels = 0;
}
if (signals & MOJO_HANDLE_SIGNAL_PEER_CLOSED) {
conditions->flags |= IPCZ_TRAP_PEER_CLOSED;
}
}
// Translates Mojo signal conditions to equivalent IpczTrapConditions for any
// portal used as a data pipe endpoint. Watching data pipes for readability or
// writability is equivalent to watching their control portal for inbound
// parcels, since each transaction from the peer elicits such a parcel.
void GetConditionsForDataPipeSignals(MojoHandleSignals signals,
IpczTrapConditions* conditions) {
conditions->flags |= IPCZ_TRAP_DEAD;
if (signals & (MOJO_HANDLE_SIGNAL_WRITABLE | MOJO_HANDLE_SIGNAL_READABLE |
MOJO_HANDLE_SIGNAL_NEW_DATA_READABLE)) {
conditions->flags |= IPCZ_TRAP_ABOVE_MIN_LOCAL_PARCELS;
conditions->min_local_parcels = 0;
}
if (signals & MOJO_HANDLE_SIGNAL_PEER_CLOSED) {
conditions->flags |= IPCZ_TRAP_PEER_CLOSED;
}
}
// Computes the appropriate MojoResult value to convey in a MojoTrapEvent that
// is being generated for a trap covering `trapped_signals` regarding a handle
// with the given signals `state`. If the given state and signals don't require
// an event to be fired at all, this returns false and `result` is set to
// MOJO_RESULT_OK (a spurious event may still be fired in this case.) Otherwise
// this returns true and `result` is updated with the computed result value.
bool GetEventResultForSignalsState(const MojoHandleSignalsState& state,
MojoHandleSignals trapped_signals,
MojoResult& result) {
result = MOJO_RESULT_OK;
if (state.satisfied_signals & trapped_signals) {
return true;
}
if (!(state.satisfiable_signals & trapped_signals)) {
result = MOJO_RESULT_FAILED_PRECONDITION;
return true;
}
return false;
}
// Flushes DataPipe updates and populates a Mojo trap event appropriate for a
// trap watching the data pipe for `trigger_signals`. Returns true if and only
// if the pipe is actually in a state that would warrant a trap event, given the
// input signals
bool PopulateEventForDataPipe(DataPipe& pipe,
MojoHandleSignals trigger_signals,
MojoTrapEvent& event) {
if (!pipe.GetSignals(event.signals_state)) {
return false;
}
return GetEventResultForSignalsState(event.signals_state, trigger_signals,
event.result);
}
// Given an ipcz trap event resulting from an installed trigger for a message
// pipe portal, this translates the event into an equivalent Mojo trap event
// for a Mojo trap watching the message pipe for `trigger_signals`.
void PopulateEventForMessagePipe(MojoHandleSignals trigger_signals,
const IpczPortalStatus& current_status,
MojoTrapEvent& event) {
const MojoHandleSignals kRead = MOJO_HANDLE_SIGNAL_READABLE;
const MojoHandleSignals kWrite = MOJO_HANDLE_SIGNAL_WRITABLE;
const MojoHandleSignals kPeerClosed = MOJO_HANDLE_SIGNAL_PEER_CLOSED;
MojoHandleSignals& satisfied = event.signals_state.satisfied_signals;
MojoHandleSignals& satisfiable = event.signals_state.satisfiable_signals;
satisfied = 0;
satisfiable = kPeerClosed | MOJO_HANDLE_SIGNAL_QUOTA_EXCEEDED;
if (!(current_status.flags & IPCZ_PORTAL_STATUS_DEAD)) {
satisfiable |= kRead;
}
if (current_status.flags & IPCZ_PORTAL_STATUS_PEER_CLOSED) {
satisfied |= kPeerClosed;
} else {
satisfiable |= MOJO_HANDLE_SIGNAL_PEER_REMOTE | kWrite;
satisfied |= kWrite;
}
if (current_status.num_local_parcels > 0) {
satisfied |= kRead;
}
DCHECK((satisfied & satisfiable) == satisfied);
GetEventResultForSignalsState(event.signals_state, trigger_signals,
event.result);
}
// Indicates whether a Mojo trap can be armed to watch for `signals` on `pipe`.
// This returns true (and `event` is left in an unspecified state) if and only
// if one or more of the given signals are still satisfiable by the pipe but
// none are currently satisfied. Otherwise this returns false and `event` is
// populated with a signal state and result value that would be appropriate for
// a MojoTrapEvent to return as a blocking event from MojoArmTrap().
bool CanArmDataPipeTrigger(DataPipe& pipe,
MojoHandleSignals signals,
MojoTrapEvent& event) {
return !PopulateEventForDataPipe(pipe, signals, event);
}
} // namespace
// A Trigger is used as context for every trigger added to a Mojo trap. While a
// trap is armed, each of its Triggers has installed a unique ipcz trap to watch
// for its conditions.
struct MojoTrap::Trigger : public base::RefCountedThreadSafe<Trigger> {
// Constructs a new trigger for the given MojoTrap to observe `handle` for
// any of `signals` to be satisfied. `context` is the opaque context value
// given to the corresponding MojoAddTrigger() call. If `data_pipe` is
// non-null then it points to the DataPipe instance which owns the portal
// identified by `handle`; otherwise `handle` refers to a portal which is
// being used as a message pipe endpoint.
Trigger(scoped_refptr<MojoTrap> mojo_trap,
MojoHandle handle,
DataPipe* data_pipe,
MojoHandleSignals signals,
uintptr_t trigger_context)
: mojo_trap(std::move(mojo_trap)),
handle(handle),
data_pipe(base::WrapRefCounted(data_pipe)),
signals(signals),
trigger_context(trigger_context) {}
uintptr_t ipcz_context() const { return reinterpret_cast<uintptr_t>(this); }
static Trigger& FromEvent(const IpczTrapEvent& event) {
return *reinterpret_cast<Trigger*>(event.context);
}
const scoped_refptr<MojoTrap> mojo_trap;
const MojoHandle handle;
const scoped_refptr<DataPipe> data_pipe;
const MojoHandleSignals signals;
const uintptr_t trigger_context;
IpczTrapConditions conditions = {.size = sizeof(conditions), .flags = 0};
// Access to all fields below is effectively guarded by the owning MojoTrap's
// `lock_`.
bool armed = false;
bool removed = false;
private:
friend class base::RefCountedThreadSafe<Trigger>;
~Trigger() = default;
};
MojoTrap::MojoTrap(MojoTrapEventHandler handler) : handler_(handler) {}
MojoTrap::~MojoTrap() = default;
MojoResult MojoTrap::AddTrigger(MojoHandle handle,
MojoHandleSignals signals,
MojoTriggerCondition condition,
uintptr_t trigger_context) {
if (handle == MOJO_HANDLE_INVALID) {
return MOJO_RESULT_INVALID_ARGUMENT;
}
// If `handle` is a boxed DataPipe rather than a portal, we need to install a
// trap on the underlying portal.
auto* data_pipe = DataPipe::FromBox(handle);
scoped_refptr<DataPipe::PortalWrapper> control_portal;
if (data_pipe) {
control_portal = data_pipe->GetPortal();
if (!control_portal) {
return MOJO_RESULT_INVALID_ARGUMENT;
}
handle = control_portal->handle();
} else if (ObjectBase::FromBox(handle)) {
// Any other type of driver object cannot have traps installed.
return MOJO_RESULT_INVALID_ARGUMENT;
}
auto trigger = base::MakeRefCounted<Trigger>(this, handle, data_pipe, signals,
trigger_context);
if (condition == MOJO_TRIGGER_CONDITION_SIGNALS_UNSATISFIED) {
// There's only one user of MOJO_TRIGGER_CONDITION_SIGNALS_UNSATISFIED. It's
// used for peer remoteness tracking in Mojo bindings lazy serialization.
// That is effectively a dead feature, so we don't need to support watching
// for unsatisfied signals.
trigger->conditions.flags = IPCZ_NO_FLAGS;
} else if (data_pipe) {
GetConditionsForDataPipeSignals(signals, &trigger->conditions);
} else {
GetConditionsForMessagePipeSignals(signals, &trigger->conditions);
}
base::AutoLock lock(lock_);
auto [it, ok] = triggers_.try_emplace(trigger_context, trigger);
if (!ok) {
return MOJO_RESULT_ALREADY_EXISTS;
}
next_trigger_ = triggers_.begin();
// Install an ipcz trap to effectively monitor the lifetime of the watched
// object referenced by `handle`. Installation of the trap should always
// succeed, and its resulting trap event will always mark the end of this
// trigger's lifetime. This trap effectively owns a ref to the Trigger, as
// added here.
trigger->AddRef();
IpczTrapConditions removal_conditions = {
.size = sizeof(removal_conditions),
.flags = IPCZ_TRAP_REMOVED,
};
IpczResult result = GetIpczAPI().Trap(
handle, &removal_conditions, &TrapRemovalEventHandler,
trigger->ipcz_context(), IPCZ_NO_FLAGS, nullptr, nullptr, nullptr);
CHECK_EQ(result, IPCZ_RESULT_OK);
if (!armed_) {
return MOJO_RESULT_OK;
}
// The Mojo trap is already armed, so attempt to install an ipcz trap for
// the new trigger immediately.
MojoTrapEvent event;
result = ArmTrigger(*trigger, event);
if (result == IPCZ_RESULT_OK) {
return MOJO_RESULT_OK;
}
// The new trigger already needs to fire an event. OK.
armed_ = false;
DispatchOrQueueEvent(*trigger, event);
return MOJO_RESULT_OK;
}
MojoResult MojoTrap::RemoveTrigger(uintptr_t trigger_context) {
base::AutoLock lock(lock_);
auto it = triggers_.find(trigger_context);
if (it == triggers_.end()) {
return MOJO_RESULT_NOT_FOUND;
}
scoped_refptr<Trigger> trigger = std::move(it->second);
trigger->armed = false;
triggers_.erase(it);
next_trigger_ = triggers_.begin();
DispatchOrQueueTriggerRemoval(*trigger);
return MOJO_RESULT_OK;
}
MojoResult MojoTrap::Arm(MojoTrapEvent* blocking_events,
uint32_t* num_blocking_events) {
const uint32_t event_capacity =
num_blocking_events ? *num_blocking_events : 0;
if (event_capacity > 0 && !blocking_events) {
return MOJO_RESULT_INVALID_ARGUMENT;
}
if (event_capacity > 0 &&
blocking_events[0].struct_size < sizeof(blocking_events[0])) {
return MOJO_RESULT_INVALID_ARGUMENT;
}
base::AutoLock lock(lock_);
if (armed_) {
return MOJO_RESULT_OK;
}
if (triggers_.empty()) {
return MOJO_RESULT_NOT_FOUND;
}
uint32_t num_events_returned = 0;
auto increment_wrapped = [this](TriggerMap::iterator it) {
lock_.AssertAcquired();
if (++it != triggers_.end()) {
return it;
}
return triggers_.begin();
};
TriggerMap::iterator next_trigger = next_trigger_;
DCHECK(next_trigger != triggers_.end());
// We iterate over all triggers, starting just beyond wherever we started last
// time we were armed. This guards against any single trigger being starved.
const TriggerMap::iterator end_trigger = next_trigger;
do {
auto& [trigger_context, trigger] = *next_trigger;
next_trigger = increment_wrapped(next_trigger);
MojoTrapEvent event;
const IpczResult result = ArmTrigger(*trigger, event);
if (result == IPCZ_RESULT_OK) {
// Trap successfully installed, nothing else to do for this trigger.
continue;
}
if (result != IPCZ_RESULT_FAILED_PRECONDITION) {
NOTREACHED();
return result;
}
// The ipcz trap failed to install, so this trigger's conditions are already
// met. Accumulate would-be event details if there's output space.
if (event_capacity == 0) {
return MOJO_RESULT_FAILED_PRECONDITION;
}
blocking_events[num_events_returned++] = event;
} while (next_trigger != end_trigger &&
(num_events_returned == 0 || num_events_returned < event_capacity));
if (next_trigger != end_trigger) {
next_trigger_ = next_trigger;
} else {
next_trigger_ = increment_wrapped(next_trigger);
}
if (num_events_returned > 0) {
*num_blocking_events = num_events_returned;
return MOJO_RESULT_FAILED_PRECONDITION;
}
// The whole Mojo trap is collectively armed if and only if all of the
// triggers managed to install an ipcz trap.
armed_ = true;
return MOJO_RESULT_OK;
}
void MojoTrap::Close() {
// Effectively disable all triggers. A disabled trigger may have already
// installed an ipcz trap which hasn't yet fired an event. This ensures that
// if any such event does eventually fire, it will be ignored.
base::AutoLock lock(lock_);
TriggerMap triggers;
std::swap(triggers, triggers_);
next_trigger_ = triggers_.begin();
for (auto& [trigger_context, trigger] : triggers) {
trigger->armed = false;
DCHECK(!trigger->removed);
DispatchOrQueueTriggerRemoval(*trigger);
}
}
// static
void MojoTrap::TrapEventHandler(const IpczTrapEvent* event) {
Trigger::FromEvent(*event).mojo_trap->HandleEvent(*event);
}
// static
void MojoTrap::TrapRemovalEventHandler(const IpczTrapEvent* event) {
Trigger& trigger = Trigger::FromEvent(*event);
trigger.mojo_trap->HandleTrapRemoved(*event);
// Balanced by AddRef when installing the trigger's removal ipcz trap.
trigger.Release();
}
void MojoTrap::HandleEvent(const IpczTrapEvent& event) {
// Transfer the trap's implied Trigger reference to the local stack.
scoped_refptr<Trigger> trigger = WrapRefCounted(&Trigger::FromEvent(event));
trigger->Release();
base::AutoLock lock(lock_);
const bool trigger_active = armed_ && trigger->armed && !trigger->removed;
const bool is_removal = (event.condition_flags & IPCZ_TRAP_REMOVED) != 0;
trigger->armed = false;
if (!trigger_active || is_removal) {
// Removal events are handled separately by ipcz traps established at
// trigger creation, allowing handle closure to trigger an event even when
// the Mojo trap isn't armed.
return;
}
armed_ = false;
MojoTrapEvent mojo_event = {
.struct_size = sizeof(mojo_event),
.flags = (event.condition_flags & IPCZ_TRAP_WITHIN_API_CALL)
? MOJO_TRAP_EVENT_FLAG_WITHIN_API_CALL
: 0,
.trigger_context = trigger->trigger_context,
};
if (trigger->data_pipe) {
if (!PopulateEventForDataPipe(*trigger->data_pipe, trigger->signals,
mojo_event)) {
// This event may be spurious if the DataPipe itself is closing but its
// its control portal is not yet closed. In that case it's safe to drop
// without firing.
return;
}
} else {
PopulateEventForMessagePipe(trigger->signals, *event.status, mojo_event);
}
DispatchOrQueueEvent(*trigger, mojo_event);
}
void MojoTrap::HandleTrapRemoved(const IpczTrapEvent& event) {
base::AutoLock lock(lock_);
Trigger& trigger = Trigger::FromEvent(event);
if (trigger.removed) {
// The Mojo trap may have already been closed, in which case this trigger
// was already removed and its handler was already notified.
return;
}
triggers_.erase(trigger.trigger_context);
DispatchOrQueueTriggerRemoval(trigger);
next_trigger_ = triggers_.begin();
}
IpczResult MojoTrap::ArmTrigger(Trigger& trigger, MojoTrapEvent& event) {
lock_.AssertAcquired();
if (trigger.armed || trigger.removed) {
return IPCZ_RESULT_OK;
}
event.struct_size = sizeof(event);
event.flags = MOJO_TRAP_EVENT_FLAG_WITHIN_API_CALL;
event.trigger_context = trigger.trigger_context;
if (trigger.signals == 0) {
// Triggers which watch for no signals can never be armed by Mojo.
event.signals_state = {0, 0};
event.result = IPCZ_RESULT_FAILED_PRECONDITION;
return IPCZ_RESULT_FAILED_PRECONDITION;
}
DataPipe* const data_pipe = trigger.data_pipe.get();
if (data_pipe && !CanArmDataPipeTrigger(*data_pipe, trigger.signals, event)) {
return MOJO_RESULT_FAILED_PRECONDITION;
}
if (!data_pipe && (trigger.signals & MOJO_HANDLE_SIGNAL_WRITABLE)) {
// Message pipes are always writable, so a trap watching for writability can
// never be armed.
IpczPortalStatus status = {.size = sizeof(status)};
const IpczResult result = GetIpczAPI().QueryPortalStatus(
trigger.handle, IPCZ_NO_FLAGS, nullptr, &status);
if (result == IPCZ_RESULT_OK) {
PopulateEventForMessagePipe(trigger.signals, status, event);
}
return IPCZ_RESULT_FAILED_PRECONDITION;
}
// Bump the ref count on the Trigger. This ref is effectively owned by the
// trap if it's installed successfully.
trigger.AddRef();
IpczTrapConditionFlags satisfied_flags;
IpczPortalStatus status = {.size = sizeof(status)};
IpczResult result =
GetIpczAPI().Trap(trigger.handle, &trigger.conditions, &TrapEventHandler,
trigger.ipcz_context(), IPCZ_NO_FLAGS, nullptr,
&satisfied_flags, &status);
if (result == IPCZ_RESULT_OK) {
trigger.armed = true;
return MOJO_RESULT_OK;
}
// Balances the AddRef above since no trap was installed.
trigger.Release();
if (data_pipe) {
PopulateEventForDataPipe(*data_pipe, trigger.signals, event);
} else {
PopulateEventForMessagePipe(trigger.signals, status, event);
}
return result;
}
void MojoTrap::DispatchOrQueueTriggerRemoval(Trigger& trigger) {
lock_.AssertAcquired();
if (trigger.removed) {
return;
}
trigger.removed = true;
DispatchOrQueueEvent(
trigger,
MojoTrapEvent{
.struct_size = sizeof(MojoTrapEvent),
.flags = MOJO_TRAP_EVENT_FLAG_WITHIN_API_CALL,
.trigger_context = trigger.trigger_context,
.result = MOJO_RESULT_CANCELLED,
.signals_state = {.satisfied_signals = 0, .satisfiable_signals = 0},
});
}
void MojoTrap::DispatchOrQueueEvent(Trigger& trigger,
const MojoTrapEvent& event) {
lock_.AssertAcquired();
if (dispatching_thread_ == base::PlatformThread::CurrentRef()) {
// This thread is already dispatching an event, so queue this one. It will
// be dispatched before the thread fully unwinds from its current dispatch.
pending_mojo_events_.emplace_back(base::WrapRefCounted(&trigger), event);
return;
}
// Block as long as any other thread is dispatching.
while (dispatching_thread_.has_value()) {
base::ScopedAllowBaseSyncPrimitivesOutsideBlockingScope allow_wait;
waiters_++;
dispatching_condition_.Wait();
waiters_--;
}
dispatching_thread_ = base::PlatformThread::CurrentRef();
DispatchEvent(event);
// NOTE: This vector is only shrunk by the clear() below, but it may
// accumulate more events during each iteration. Hence we iterate by index.
for (size_t i = 0; i < pending_mojo_events_.size(); ++i) {
if (!pending_mojo_events_[i].trigger->removed ||
pending_mojo_events_[i].event.result == MOJO_RESULT_CANCELLED) {
DispatchEvent(pending_mojo_events_[i].event);
}
}
pending_mojo_events_.clear();
// We're done. Give other threads a chance.
dispatching_thread_.reset();
if (waiters_ > 0) {
dispatching_condition_.Signal();
}
}
void MojoTrap::DispatchEvent(const MojoTrapEvent& event) {
lock_.AssertAcquired();
DCHECK(dispatching_thread_ == base::PlatformThread::CurrentRef());
// Note that other threads may enter DispatchOrQueueEvent while this is
// unlocked; but they will be blocked from dispatching since we've set
// `dispatching_thread_` to our thread.
base::AutoUnlock unlock(lock_);
handler_(&event);
}
MojoTrap::PendingEvent::PendingEvent() = default;
MojoTrap::PendingEvent::PendingEvent(scoped_refptr<Trigger> trigger,
const MojoTrapEvent& event)
: trigger(std::move(trigger)), event(event) {}
MojoTrap::PendingEvent::PendingEvent(PendingEvent&&) = default;
MojoTrap::PendingEvent::~PendingEvent() = default;
} // namespace mojo::core::ipcz_driver