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