| // Copyright 2020 The Fuchsia 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 "device_interface.h" |
| |
| #include <lib/async/cpp/task.h> |
| #include <lib/fit/defer.h> |
| #include <zircon/device/network.h> |
| |
| #include <fbl/alloc_checker.h> |
| |
| #include "log.h" |
| #include "rx_queue.h" |
| #include "session.h" |
| #include "tx_queue.h" |
| |
| // Static sanity assertions from far-away defined buffer_descriptor_t. |
| // A buffer descriptor is always described in 64 bit words. |
| static_assert(sizeof(buffer_descriptor_t) % 8 == 0); |
| // Verify no unseen padding is being added by the compiler and all padding reservation fields are |
| // working as expected, check the offset of every 64 bit word in the struct. |
| static_assert(offsetof(buffer_descriptor_t, frame_type) == 0); |
| static_assert(offsetof(buffer_descriptor_t, port_id) == 8); |
| static_assert(offsetof(buffer_descriptor_t, offset) == 16); |
| static_assert(offsetof(buffer_descriptor_t, head_length) == 24); |
| static_assert(offsetof(buffer_descriptor_t, inbound_flags) == 32); |
| // Descriptor length is reported as uint8 words in session info, make sure that fits. |
| static_assert(sizeof(buffer_descriptor_t) / sizeof(uint64_t) < std::numeric_limits<uint8_t>::max()); |
| |
| namespace { |
| const char* DeviceStatusToString(network::internal::DeviceStatus status) { |
| switch (status) { |
| case network::internal::DeviceStatus::STARTING: |
| return "STARTING"; |
| case network::internal::DeviceStatus::STARTED: |
| return "STARTED"; |
| case network::internal::DeviceStatus::STOPPING: |
| return "STOPPING"; |
| case network::internal::DeviceStatus::STOPPED: |
| return "STOPPED"; |
| } |
| } |
| } // namespace |
| |
| namespace network { |
| |
| zx::status<std::unique_ptr<NetworkDeviceInterface>> NetworkDeviceInterface::Create( |
| async_dispatcher_t* dispatcher, ddk::NetworkDeviceImplProtocolClient parent, |
| const char* parent_name) { |
| return internal::DeviceInterface::Create(dispatcher, parent, parent_name); |
| } |
| |
| namespace internal { |
| |
| uint16_t TransformFifoDepth(uint16_t device_depth) { |
| // We're going to say the depth is twice the depth of the device to account for in-flight |
| // buffers, as long as it doesn't go over the maximum fifo depth. |
| |
| // Check for overflow. |
| if (device_depth > (std::numeric_limits<uint16_t>::max() >> 1)) { |
| return kMaxFifoDepth; |
| } |
| |
| return std::min(kMaxFifoDepth, static_cast<uint16_t>(device_depth << 1)); |
| } |
| |
| zx::status<std::unique_ptr<DeviceInterface>> DeviceInterface::Create( |
| async_dispatcher_t* dispatcher, ddk::NetworkDeviceImplProtocolClient parent, |
| const char* parent_name) { |
| fbl::AllocChecker ac; |
| std::unique_ptr<DeviceInterface> device(new (&ac) DeviceInterface(dispatcher, parent)); |
| if (!ac.check()) { |
| return zx::error(ZX_ERR_NO_MEMORY); |
| } |
| zx_status_t status = device->Init(parent_name); |
| if (status != ZX_OK) { |
| return zx::error(status); |
| } |
| return zx::ok(std::move(device)); |
| } |
| |
| DeviceInterface::~DeviceInterface() { |
| ZX_ASSERT_MSG(primary_session_ == nullptr, |
| "Can't destroy DeviceInterface with active primary session. (%s)", |
| primary_session_->name()); |
| ZX_ASSERT_MSG(sessions_.is_empty(), "Can't destroy DeviceInterface with %ld pending session(s).", |
| sessions_.size()); |
| ZX_ASSERT_MSG(dead_sessions_.is_empty(), |
| "Can't destroy DeviceInterface with %ld pending dead session(s).", |
| dead_sessions_.size()); |
| ZX_ASSERT_MSG(bindings_.is_empty(), "Can't destroy device interface with %ld attached bindings.", |
| bindings_.size()); |
| size_t active_ports = std::count_if( |
| ports_.begin(), ports_.end(), |
| [](const std::unique_ptr<DevicePort>& port) { return static_cast<bool>(port); }); |
| ZX_ASSERT_MSG(!active_ports, "Can't destroy device interface with %ld ports", active_ports); |
| } |
| |
| zx_status_t DeviceInterface::Init(const char* parent_name) { |
| LOGF_TRACE("network-device: %s('%s')", __FUNCTION__, parent_name); |
| if (!device_.is_valid()) { |
| LOG_ERROR("network-device: init: no protocol"); |
| return ZX_ERR_INTERNAL; |
| } |
| |
| network_device_impl_protocol_t proto; |
| device_.GetProto(&proto); |
| if (proto.ops == nullptr) { |
| LOG_ERROR("network-device: init: null protocol ops"); |
| return ZX_ERR_INTERNAL; |
| } |
| network_device_impl_protocol_ops_t& ops = *proto.ops; |
| if (ops.init == nullptr || ops.get_info == nullptr || ops.stop == nullptr || |
| ops.start == nullptr || ops.queue_tx == nullptr || ops.queue_rx_space == nullptr || |
| ops.prepare_vmo == nullptr || ops.release_vmo == nullptr || ops.set_snoop == nullptr) { |
| LOGF_ERROR("network-device: init: device '%s': incomplete protocol", parent_name); |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| |
| device_.GetInfo(&device_info_); |
| if (device_info_.buffer_alignment == 0) { |
| LOGF_ERROR("network-device: init: device '%s' reports invalid zero buffer alignment", |
| parent_name); |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| if (device_info_.rx_threshold > device_info_.rx_depth) { |
| LOGF_ERROR("network-device: init: device'%s' reports rx_threshold = %d larger than rx_depth %d", |
| parent_name, device_info_.rx_threshold, device_info_.rx_depth); |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| |
| if (device_info_.rx_accel_count > netdev::wire::kMaxAccelFlags || |
| device_info_.tx_accel_count > netdev::wire::kMaxAccelFlags) { |
| LOGF_ERROR("network-device: init: device '%s' reports too many acceleration flags", |
| parent_name); |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| // Copy the vectors of supported acceleration flags. |
| { |
| fbl::Span span(device_info_.rx_accel_list, device_info_.rx_accel_count); |
| std::transform(span.begin(), span.end(), accel_rx_.begin(), |
| [](uint8_t v) { return static_cast<netdev::wire::RxAcceleration>(v); }); |
| } |
| { |
| fbl::Span span(device_info_.tx_accel_list, device_info_.tx_accel_count); |
| std::transform(span.begin(), span.end(), accel_tx_.begin(), |
| [](uint8_t v) { return static_cast<netdev::wire::TxAcceleration>(v); }); |
| } |
| // Clear list accessors -- they point to device-owned memory. We can access the lists through the |
| // |accel_rx_| and |accel_tx_| member fields. |
| device_info_.rx_accel_list = nullptr; |
| device_info_.tx_accel_list = nullptr; |
| |
| if (device_info_.rx_depth > kMaxFifoDepth || device_info_.tx_depth > kMaxFifoDepth) { |
| LOGF_ERROR("network-device: init: device '%s' reports too large FIFO depths: %d/%d (max=%d)", |
| parent_name, device_info_.rx_depth, device_info_.tx_depth, kMaxFifoDepth); |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| |
| zx::status tx_queue = TxQueue::Create(this); |
| if (tx_queue.is_error()) { |
| LOGF_ERROR("network-device: init: device failed to start Tx Queue: %s", |
| tx_queue.status_string()); |
| return tx_queue.status_value(); |
| } |
| tx_queue_ = std::move(tx_queue.value()); |
| |
| zx::status rx_queue = RxQueue::Create(this); |
| if (rx_queue.is_error()) { |
| LOGF_ERROR("network-device: init: device failed to start Rx Queue: %s", |
| rx_queue.status_string()); |
| return rx_queue.status_value(); |
| } |
| rx_queue_ = std::move(rx_queue.value()); |
| |
| zx_status_t status; |
| { |
| fbl::AutoLock lock(&control_lock_); |
| if ((status = vmo_store_.Reserve(MAX_VMOS)) != ZX_OK) { |
| LOGF_ERROR("network-device: init: failed to init session identifiers %s", |
| zx_status_get_string(status)); |
| return status; |
| } |
| } |
| |
| // Init session with parent. |
| if ((status = device_.Init(this, &network_device_ifc_protocol_ops_)) != ZX_OK) { |
| LOGF_ERROR("network-device: init: NetworkDevice Init failed: %s", zx_status_get_string(status)); |
| return status; |
| } |
| |
| return ZX_OK; |
| } |
| |
| void DeviceInterface::Teardown(fit::callback<void()> teardown_callback) { |
| // stop all rx queue operation immediately. |
| rx_queue_->JoinThread(); |
| LOGF_TRACE("network-device: %s", __FUNCTION__); |
| |
| control_lock_.Acquire(); |
| // Can't call teardown again until the teardown process has ended. |
| ZX_ASSERT(teardown_callback_ == nullptr); |
| teardown_callback_ = std::move(teardown_callback); |
| |
| ContinueTeardown(TeardownState::RUNNING); |
| } |
| |
| zx_status_t DeviceInterface::Bind(fidl::ServerEnd<netdev::Device> req) { |
| fbl::AutoLock lock(&control_lock_); |
| // Don't attach new bindings if we're tearing down. |
| if (teardown_state_ != TeardownState::RUNNING) { |
| return ZX_ERR_BAD_STATE; |
| } |
| return Binding::Bind(this, std::move(req)); |
| } |
| |
| void DeviceInterface::NetworkDeviceIfcPortStatusChanged(uint8_t port_id, |
| const port_status_t* new_status) { |
| SharedAutoLock lock(&control_lock_); |
| // Skip port status changes if tearing down. During teardown ports may disappear and device |
| // implementation may not be aware of it yet. |
| if (teardown_state_ != TeardownState::RUNNING) { |
| return; |
| } |
| WithPort(port_id, [&new_status, port_id](const std::unique_ptr<DevicePort>& port) { |
| if (!port) { |
| LOGF_ERROR("network-device: StatusChanged on unknown port=%d %d %d", port_id, |
| new_status->flags, new_status->mtu); |
| return; |
| } |
| |
| LOGF_TRACE("network-device: StatusChanged(port=%d) %d %d", port_id, new_status->flags, |
| new_status->mtu); |
| port->StatusChanged(*new_status); |
| }); |
| } |
| |
| void DeviceInterface::NetworkDeviceIfcAddPort(uint8_t port_id, |
| const network_port_protocol_t* port_proto) { |
| LOGF_TRACE("network-device: %s(%d)", __FUNCTION__, port_id); |
| auto port_client = ddk::NetworkPortProtocolClient(port_proto); |
| auto release_port = fit::defer([&port_client]() { |
| if (port_client.is_valid()) { |
| port_client.Removed(); |
| } |
| }); |
| fbl::AutoLock lock(&control_lock_); |
| // Don't allow new ports if tearing down. |
| if (teardown_state_ != TeardownState::RUNNING) { |
| LOGF_WARN("network-device: port %d not added, teardown in progress", port_id); |
| return; |
| } |
| if (port_id >= ports_.size()) { |
| LOGF_ERROR("network-device: port id %d out of allowed range: [0, %ld)", port_id, ports_.size()); |
| return; |
| } |
| std::unique_ptr<DevicePort>& port_slot = ports_[port_id]; |
| if (port_slot) { |
| LOGF_ERROR("network-device: port %d already exists", port_id); |
| return; |
| } |
| |
| std::unique_ptr<MacAddrDeviceInterface> mac; |
| mac_addr_protocol_t mac_proto; |
| port_client.GetMac(&mac_proto); |
| ddk::MacAddrProtocolClient mac_client(&mac_proto); |
| if (mac_client.is_valid()) { |
| zx::status status = MacAddrDeviceInterface::Create(mac_client); |
| if (status.is_error()) { |
| LOGF_ERROR("network-device: failed to instantiate MAC information for port %d: %s", port_id, |
| status.status_string()); |
| return; |
| } |
| mac = std::move(status.value()); |
| } |
| |
| fbl::AllocChecker checker; |
| std::unique_ptr<DevicePort> port( |
| new (&checker) DevicePort(dispatcher_, port_id, port_client, std::move(mac), |
| fit::bind_member(this, &DeviceInterface::OnPortTeardownComplete))); |
| if (!checker.check()) { |
| LOGF_ERROR("network-device: failed to allocate port memory"); |
| return; |
| } |
| |
| // Clear port_client to prevent deferred call from notifying removal. |
| port_client.clear(); |
| port_slot = std::move(port); |
| |
| for (auto& watcher : port_watchers_) { |
| watcher.PortAdded(port_id); |
| } |
| } |
| |
| void DeviceInterface::NetworkDeviceIfcRemovePort(uint8_t port_id) { |
| LOGF_TRACE("network-device: %s(%d)", __FUNCTION__, port_id); |
| SharedAutoLock lock(&control_lock_); |
| // Ignore if we're tearing down, all ports will be removed as part of teardown. |
| if (teardown_state_ != TeardownState::RUNNING) { |
| return; |
| } |
| WithPort(port_id, |
| [this](const std::unique_ptr<DevicePort>& port) __TA_REQUIRES_SHARED(control_lock_) { |
| if (port) { |
| for (auto& watcher : port_watchers_) { |
| watcher.PortRemoved(port->id()); |
| } |
| port->Teardown(); |
| } |
| }); |
| } |
| |
| void DeviceInterface::NetworkDeviceIfcCompleteRx(const rx_buffer_t* rx_list, size_t rx_count) { |
| LOGF_TRACE("network-device: %s(_, %ld)", __FUNCTION__, rx_count); |
| rx_queue_->CompleteRxList(rx_list, rx_count); |
| } |
| |
| void DeviceInterface::NetworkDeviceIfcCompleteTx(const tx_result_t* tx_list, size_t tx_count) { |
| LOGF_TRACE("network-device: %s(_, %ld)", __FUNCTION__, tx_count); |
| tx_queue_->CompleteTxList(tx_list, tx_count); |
| } |
| |
| void DeviceInterface::NetworkDeviceIfcSnoop(const rx_buffer_t* rx_list, size_t rx_count) { |
| // TODO(fxbug.dev/43028): Implement real version. Current implementation acts as if no LISTEN is |
| // ever in place. |
| } |
| |
| void DeviceInterface::GetInfo(GetInfoRequestView request, GetInfoCompleter::Sync& completer) { |
| LOGF_TRACE("network-device: %s", __FUNCTION__); |
| netdev::wire::DeviceInfo::Frame_ frame; |
| netdev::wire::DeviceInfo device_info( |
| fidl::ObjectView<netdev::wire::DeviceInfo::Frame_>::FromExternal(&frame)); |
| |
| uint8_t min_descriptor_length = sizeof(buffer_descriptor_t) / sizeof(uint64_t); |
| uint8_t descriptor_version = NETWORK_DEVICE_DESCRIPTOR_VERSION; |
| uint16_t rx_depth = rx_fifo_depth(); |
| uint16_t tx_depth = tx_fifo_depth(); |
| auto tx_accel = fidl::VectorView<netdev::wire::TxAcceleration>::FromExternal( |
| accel_tx_.data(), device_info_.tx_accel_count); |
| auto rx_accel = fidl::VectorView<netdev::wire::RxAcceleration>::FromExternal( |
| accel_rx_.data(), device_info_.rx_accel_count); |
| device_info.set_min_descriptor_length(min_descriptor_length) |
| .set_descriptor_version(descriptor_version) |
| .set_rx_depth(rx_depth) |
| .set_tx_depth(tx_depth) |
| .set_buffer_alignment(device_info_.buffer_alignment) |
| .set_max_buffer_length(device_info_.max_buffer_length) |
| .set_max_buffer_parts(device_info_.max_buffer_parts) |
| .set_min_rx_buffer_length(device_info_.min_rx_buffer_length) |
| .set_min_tx_buffer_length(device_info_.min_tx_buffer_length) |
| .set_min_tx_buffer_head(device_info_.tx_head_length) |
| .set_min_tx_buffer_tail(device_info_.tx_tail_length) |
| .set_tx_accel(fidl::ObjectView<decltype(tx_accel)>::FromExternal(&tx_accel)) |
| .set_rx_accel(fidl::ObjectView<decltype(rx_accel)>::FromExternal(&rx_accel)); |
| |
| completer.Reply(std::move(device_info)); |
| } |
| |
| void DeviceInterface::OpenSession(OpenSessionRequestView request, |
| OpenSessionCompleter::Sync& completer) { |
| zx::status response = OpenSession(request->session_name, std::move(request->session_info)); |
| if (response.is_error()) { |
| completer.ReplyError(response.error_value()); |
| } else { |
| auto& [session, fifos] = response.value(); |
| completer.ReplySuccess(std::move(session), std::move(fifos)); |
| } |
| } |
| |
| zx::status<netdev::wire::DeviceOpenSessionResponse> DeviceInterface::OpenSession( |
| fidl::StringView name, netdev::wire::SessionInfo session_info) { |
| fbl::AutoLock lock(&control_lock_); |
| // We're currently tearing down and can't open any new sessions. |
| if (teardown_state_ != TeardownState::RUNNING) { |
| return zx::error(ZX_ERR_UNAVAILABLE); |
| } |
| |
| zx::status endpoints = fidl::CreateEndpoints<netdev::Session>(); |
| if (endpoints.is_error()) { |
| return endpoints.take_error(); |
| } |
| |
| zx::status session_creation = |
| Session::Create(dispatcher_, session_info, name, this, std::move(endpoints->server)); |
| if (session_creation.is_error()) { |
| return session_creation.take_error(); |
| } |
| auto& [session, fifos] = session_creation.value(); |
| |
| if (!session_info.has_data()) { |
| return zx::error(ZX_ERR_INVALID_ARGS); |
| } |
| // NB: It's safe to register the VMO after session creation (and thread start) because sessions |
| // always start in a paused state, so the tx path can't be running while we hold the control lock. |
| zx::status vmo_registration = RegisterDataVmo(std::move(session_info.data())); |
| if (vmo_registration.is_error()) { |
| return vmo_registration.take_error(); |
| } |
| auto& [vmo_id, vmo] = vmo_registration.value(); |
| session->SetDataVmo(vmo_id, vmo); |
| |
| if (session->ShouldTakeOverPrimary(primary_session_.get())) { |
| // Set this new session as the primary session. |
| std::swap(primary_session_, session); |
| rx_queue_->TriggerSessionChanged(); |
| } |
| if (session) { |
| // Add the new session (or the primary session if it the new session just took over) to the list |
| // of sessions. |
| sessions_.push_back(std::move(session)); |
| } |
| |
| return zx::ok(netdev::wire::DeviceOpenSessionResponse{ |
| .session = std::move(endpoints->client), |
| .fifos = std::move(fifos), |
| }); |
| } |
| |
| void DeviceInterface::GetPort(GetPortRequestView request, GetPortCompleter::Sync& _completer) { |
| SharedAutoLock lock(&control_lock_); |
| WithPort(request->id, |
| [req = std::move(request->port)](const std::unique_ptr<DevicePort>& port) mutable { |
| if (port) { |
| port->Bind(std::move(req)); |
| } else { |
| req.Close(ZX_ERR_NOT_FOUND); |
| } |
| }); |
| } |
| |
| void DeviceInterface::GetPortWatcher(GetPortWatcherRequestView request, |
| GetPortWatcherCompleter::Sync& _completer) { |
| fbl::AutoLock lock(&control_lock_); |
| if (teardown_state_ != TeardownState::RUNNING) { |
| // Don't install new watchers after teardown has started. |
| return; |
| } |
| |
| fbl::AllocChecker ac; |
| auto watcher = fbl::make_unique_checked<PortWatcher>(&ac); |
| if (!ac.check()) { |
| request->watcher.Close(ZX_ERR_NO_MEMORY); |
| return; |
| } |
| |
| std::array<uint8_t, MAX_PORTS> port_ids; |
| size_t port_id_count = 0; |
| |
| for (const std::unique_ptr<DevicePort>& port : ports_) { |
| if (port) { |
| port_ids[port_id_count++] = port->id(); |
| } |
| } |
| |
| zx_status_t status = watcher->Bind(dispatcher_, fbl::Span(port_ids.begin(), port_id_count), |
| std::move(request->watcher), [this](PortWatcher& watcher) { |
| fbl::AutoLock lock(&control_lock_); |
| port_watchers_.erase(watcher); |
| ContinueTeardown(TeardownState::PORT_WATCHERS); |
| }); |
| |
| if (status != ZX_OK) { |
| LOGF_ERROR("network-device: Failed to bind port watcher: %s", zx_status_get_string(status)); |
| return; |
| } |
| port_watchers_.push_back(std::move(watcher)); |
| } |
| |
| uint16_t DeviceInterface::rx_fifo_depth() const { |
| return TransformFifoDepth(device_info_.rx_depth); |
| } |
| |
| uint16_t DeviceInterface::tx_fifo_depth() const { |
| return TransformFifoDepth(device_info_.tx_depth); |
| } |
| |
| void DeviceInterface::SessionStarted(Session& session) { |
| bool should_start = false; |
| { |
| fbl::AutoLock lock(&control_lock_); |
| if (session.IsListen()) { |
| has_listen_sessions_.store(true, std::memory_order_relaxed); |
| } |
| if (session.IsPrimary()) { |
| active_primary_sessions_++; |
| if (session.ShouldTakeOverPrimary(primary_session_.get())) { |
| // Push primary session to sessions list. |
| sessions_.push_back(std::move(primary_session_)); |
| // Find the session in the list and promote it to primary. |
| primary_session_ = sessions_.erase(session); |
| ZX_ASSERT(primary_session_); |
| // Notify rx queue of primary session change. |
| rx_queue_->TriggerSessionChanged(); |
| } |
| should_start = active_primary_sessions_ != 0; |
| } |
| } |
| |
| if (should_start) { |
| // Start the device if we haven't done so already. |
| StartDevice(); |
| } |
| |
| if (evt_session_started) { |
| evt_session_started(session.name()); |
| } |
| } |
| |
| bool DeviceInterface::SessionStoppedInner(Session& session) { |
| if (session.IsListen()) { |
| bool any = primary_session_ && primary_session_->IsListen() && !primary_session_->IsPaused(); |
| for (auto& s : sessions_) { |
| any |= s.IsListen() && !s.IsPaused(); |
| } |
| has_listen_sessions_.store(any, std::memory_order_relaxed); |
| } |
| |
| if (!session.IsPrimary()) { |
| return false; |
| } |
| |
| ZX_ASSERT(active_primary_sessions_ > 0); |
| if (&session == primary_session_.get()) { |
| // If this was the primary session, offer all other sessions to take over: |
| Session* primary_candidate = &session; |
| for (auto& i : sessions_) { |
| primary_candidate->AssertParentControlLockShared(*this); |
| if (primary_candidate->IsDying() || i.ShouldTakeOverPrimary(primary_candidate)) { |
| primary_candidate = &i; |
| } |
| } |
| // If we found a candidate to take over primary... |
| if (primary_candidate != primary_session_.get()) { |
| // ...promote it. |
| sessions_.push_back(std::move(primary_session_)); |
| primary_session_ = sessions_.erase(*primary_candidate); |
| ZX_ASSERT(primary_session_); |
| } |
| if (teardown_state_ == TeardownState::RUNNING) { |
| rx_queue_->TriggerSessionChanged(); |
| } |
| } |
| |
| active_primary_sessions_--; |
| return active_primary_sessions_ == 0; |
| } |
| |
| void DeviceInterface::SessionStopped(Session& session) { |
| control_lock_.Acquire(); |
| if (SessionStoppedInner(session)) { |
| // Stop the device, no more sessions are running. |
| StopDevice(); |
| } else { |
| control_lock_.Release(); |
| } |
| } |
| |
| void DeviceInterface::StartDevice() { |
| LOGF_TRACE("network-device: %s", __FUNCTION__); |
| |
| bool start = false; |
| { |
| fbl::AutoLock lock(&control_lock_); |
| // Start the device if we haven't done so already. |
| switch (device_status_) { |
| case DeviceStatus::STARTED: |
| case DeviceStatus::STARTING: |
| // Remove any pending operations we may have. |
| pending_device_op_ = PendingDeviceOperation::NONE; |
| break; |
| case DeviceStatus::STOPPING: |
| // Device is currently stopping, let's record that we want to start it. |
| pending_device_op_ = PendingDeviceOperation::START; |
| break; |
| case DeviceStatus::STOPPED: |
| // Device is in STOPPED state, start it. |
| device_status_ = DeviceStatus::STARTING; |
| start = true; |
| break; |
| } |
| } |
| |
| if (start) { |
| StartDeviceInner(); |
| } |
| } |
| |
| void DeviceInterface::StartDeviceInner() { |
| LOGF_TRACE("network-device: %s", __FUNCTION__); |
| device_.Start( |
| [](void* cookie, zx_status_t status) { |
| auto device = reinterpret_cast<DeviceInterface*>(cookie); |
| { |
| fbl::AutoLock lock(&device->control_lock_); |
| ZX_ASSERT_MSG(device->device_status_ == DeviceStatus::STARTING, |
| "device not in starting status: %s", |
| DeviceStatusToString(device->device_status_)); |
| if (status != ZX_OK) { |
| LOGF_ERROR("network-device: failed to start implementation: %s", |
| zx_status_get_string(status)); |
| switch (device->SetDeviceStatus(DeviceStatus::STOPPED)) { |
| case PendingDeviceOperation::STOP: |
| case PendingDeviceOperation::NONE: |
| break; |
| case PendingDeviceOperation::START: |
| ZX_PANIC("unexpected start pending while starting already"); |
| break; |
| } |
| if (device->primary_session_) { |
| LOGF_ERROR("killing session '%s' because device failed to start", |
| device->primary_session_->name()); |
| device->primary_session_->Kill(); |
| } |
| for (auto& s : device->sessions_) { |
| LOGF_ERROR("killing session '%s' because device failed to start", s.name()); |
| s.Kill(); |
| } |
| // We have effectively shut down the device, so finish tearing it down. |
| device->ContinueTeardown(TeardownState::SESSIONS); |
| return; |
| } |
| } |
| device->DeviceStarted(); |
| }, |
| this); |
| } |
| |
| void DeviceInterface::StopDevice(std::optional<TeardownState> continue_teardown) { |
| LOGF_TRACE("network-device: %s", __FUNCTION__); |
| bool stop = false; |
| switch (device_status_) { |
| case DeviceStatus::STOPPED: |
| case DeviceStatus::STOPPING: |
| // Remove any pending operations we may have. |
| pending_device_op_ = PendingDeviceOperation::NONE; |
| break; |
| case DeviceStatus::STARTING: |
| // Device is currently starting, let's record that we want to stop it. |
| pending_device_op_ = PendingDeviceOperation::STOP; |
| break; |
| case DeviceStatus::STARTED: |
| // Device is in STARTED state, stop it. |
| device_status_ = DeviceStatus::STOPPING; |
| stop = true; |
| } |
| if (continue_teardown.has_value()) { |
| bool did_teardown = ContinueTeardown(continue_teardown.value()); |
| stop = stop && !did_teardown; |
| } else { |
| control_lock_.Release(); |
| } |
| if (stop) { |
| StopDeviceInner(); |
| } |
| } |
| |
| void DeviceInterface::StopDeviceInner() { |
| LOGF_TRACE("network-device: %s", __FUNCTION__); |
| device_.Stop([](void* cookie) { reinterpret_cast<DeviceInterface*>(cookie)->DeviceStopped(); }, |
| this); |
| } |
| |
| PendingDeviceOperation DeviceInterface::SetDeviceStatus(DeviceStatus status) { |
| PendingDeviceOperation pending_op = pending_device_op_; |
| device_status_ = status; |
| pending_device_op_ = PendingDeviceOperation::NONE; |
| return pending_op; |
| } |
| |
| void DeviceInterface::DeviceStarted() { |
| LOGF_TRACE("network-device: %s", __FUNCTION__); |
| control_lock_.Acquire(); |
| switch (SetDeviceStatus(DeviceStatus::STARTED)) { |
| case PendingDeviceOperation::STOP: |
| StopDevice(); |
| return; |
| case PendingDeviceOperation::NONE: |
| case PendingDeviceOperation::START: |
| break; |
| } |
| NotifyTxQueueAvailable(); |
| control_lock_.Release(); |
| // Notify Rx queue that the device has started. |
| rx_queue_->TriggerRxWatch(); |
| } |
| |
| void DeviceInterface::DeviceStopped() { |
| LOGF_TRACE("network-device: %s", __FUNCTION__); |
| control_lock_.Acquire(); |
| PendingDeviceOperation pending_op = SetDeviceStatus(DeviceStatus::STOPPED); |
| if (ContinueTeardown(TeardownState::SESSIONS)) { |
| return; |
| } |
| switch (pending_op) { |
| case PendingDeviceOperation::START: |
| StartDevice(); |
| return; |
| case PendingDeviceOperation::NONE: |
| case PendingDeviceOperation::STOP: |
| break; |
| } |
| } |
| |
| bool DeviceInterface::ContinueTeardown(network::internal::DeviceInterface::TeardownState state) { |
| // The teardown process goes through different phases, encoded by the TeardownState enumeration. |
| // - RUNNING: no teardown is in process. We move out of the RUNNING state by calling Unbind on all |
| // the DeviceInterface's bindings. |
| // - BINDINGS: Waiting for all bindings to close. Only moves to next state once all bindings are |
| // closed, then calls unbind on all watchers and moves to the WATCHERS state. |
| // - PORTS: Waiting for all ports to teardown. Only moves to the next state once all ports are |
| // destroyed, then proceeds to stop and destroy all sessions. |
| // - SESSIONS: Waiting for all sessions to be closed and destroyed (dead or alive). This is the |
| // final stage, once all the sessions are properly destroyed the teardown_callback_ will be |
| // triggered, marking the end of the teardown process. |
| // |
| // To protect the linearity of the teardown process, once it has started (the state is no longer |
| // RUNNING) no more bindings, watchers, or sessions can be created. |
| |
| fit::callback<void()> teardown_callback = |
| [this, state]() __TA_REQUIRES(control_lock_) -> fit::callback<void()> { |
| if (state != teardown_state_) { |
| return nullptr; |
| } |
| switch (teardown_state_) { |
| case TeardownState::RUNNING: { |
| teardown_state_ = TeardownState::BINDINGS; |
| LOGF_TRACE("network-device: Teardown state is BINDINGS (%ld bindings to destroy)", |
| bindings_.size()); |
| if (!bindings_.is_empty()) { |
| for (auto& b : bindings_) { |
| b.Unbind(); |
| } |
| } |
| __FALLTHROUGH; |
| } |
| case TeardownState::BINDINGS: { |
| // Pre-condition to enter port watchers state: bindings must be empty. |
| if (!bindings_.is_empty()) { |
| return nullptr; |
| } |
| teardown_state_ = TeardownState::PORT_WATCHERS; |
| LOGF_TRACE("network-device: Teardown state is PORT_WATCHERS (%ld watchers to destroy)", |
| port_watchers_.size()); |
| if (!port_watchers_.is_empty()) { |
| for (auto& w : port_watchers_) { |
| w.Unbind(); |
| } |
| } |
| __FALLTHROUGH; |
| } |
| case TeardownState::PORT_WATCHERS: { |
| // Pre-condition to enter ports state: port watchers must be empty. |
| if (!port_watchers_.is_empty()) { |
| return nullptr; |
| } |
| teardown_state_ = TeardownState::PORTS; |
| size_t port_count = 0; |
| for (auto& p : ports_) { |
| if (p) { |
| p->Teardown(); |
| port_count++; |
| } |
| } |
| LOGF_TRACE("network-device: Teardown state is PORTS (%ld ports to destroy)", port_count); |
| __FALLTHROUGH; |
| } |
| case TeardownState::PORTS: { |
| // Pre-condition to enter sessions state: ports must all be destroyed. |
| if (std::any_of(ports_.begin(), ports_.end(), [](const std::unique_ptr<DevicePort>& port) { |
| return static_cast<bool>(port); |
| })) { |
| return nullptr; |
| } |
| teardown_state_ = TeardownState::SESSIONS; |
| LOGF_TRACE("network-device: Teardown state is SESSIONS (primary=%s) (alive=%ld) (dead=%ld)", |
| primary_session_ ? "true" : "false", sessions_.size(), dead_sessions_.size()); |
| if (primary_session_ || !sessions_.is_empty()) { |
| // If we have any sessions, signal all of them to stop their threads callback. Each |
| // session that finishes operating will go through the `NotifyDeadSession` machinery. The |
| // teardown is only complete when all sessions are destroyed. |
| LOG_TRACE("network-device: Teardown: sessions are running, scheduling teardown"); |
| if (primary_session_) { |
| primary_session_->Kill(); |
| } |
| for (auto& s : sessions_) { |
| s.Kill(); |
| } |
| // We won't check for dead sessions here, since all the sessions we just called `Kill` on |
| // will go into the dead state asynchronously. Any sessions that are already in the dead |
| // state will also get checked in `PruneDeadSessions` at a later time. |
| return nullptr; |
| } |
| // No sessions are alive. Now check if we have any dead sessions that are waiting to reclaim |
| // buffers. |
| if (!dead_sessions_.is_empty()) { |
| LOG_TRACE("network-device: Teardown: dead sessions pending, waiting for teardown"); |
| // We need to wait for the device to safely give us all the buffers back before completing |
| // the teardown. |
| return nullptr; |
| } |
| // We can teardown immediately, let it fall through |
| __FALLTHROUGH; |
| } |
| case TeardownState::SESSIONS: { |
| // Condition to finish teardown: no more sessions exists (dead or alive) and the device |
| // state is STOPPED. |
| if (sessions_.is_empty() && !primary_session_ && dead_sessions_.is_empty() && |
| device_status_ == DeviceStatus::STOPPED) { |
| teardown_state_ = TeardownState::FINISHED; |
| LOG_TRACE("network-device: Teardown finished"); |
| return std::move(teardown_callback_); |
| } |
| LOG_TRACE("network-device: Teardown: Still pending sessions teardown"); |
| return nullptr; |
| } |
| case TeardownState::FINISHED: |
| ZX_PANIC("Nothing to do if the teardown state is finished."); |
| } |
| }(); |
| control_lock_.Release(); |
| if (teardown_callback) { |
| teardown_callback(); |
| return true; |
| } |
| return false; |
| } |
| |
| zx::status<AttachedPort> DeviceInterface::AcquirePort( |
| uint8_t port_id, fbl::Span<const netdev::wire::FrameType> rx_frame_types) { |
| return WithPort( |
| port_id, |
| [this, &rx_frame_types](const std::unique_ptr<DevicePort>& port) -> zx::status<AttachedPort> { |
| if (!port) { |
| return zx::error(ZX_ERR_NOT_FOUND); |
| } |
| if (std::any_of(rx_frame_types.begin(), rx_frame_types.end(), |
| [&port](netdev::wire::FrameType frame_type) { |
| return !port->IsValidRxFrameType(frame_type); |
| })) { |
| return zx::error(ZX_ERR_INVALID_ARGS); |
| } |
| return zx::ok(AttachedPort(this, port.get(), rx_frame_types)); |
| }); |
| } |
| |
| void DeviceInterface::OnPortTeardownComplete(DevicePort& port) { |
| LOGF_TRACE("network-device: %s(%d)", __FUNCTION__, port.id()); |
| |
| control_lock_.Acquire(); |
| bool stop_device = false; |
| // Go over the non-primary sessions first, so we don't mess with the primary session. |
| for (auto& session : sessions_) { |
| session.AssertParentControlLock(*this); |
| if (session.OnPortDestroyed(port.id())) { |
| stop_device |= SessionStoppedInner(session); |
| } |
| } |
| if (primary_session_) { |
| primary_session_->AssertParentControlLock(*this); |
| if (primary_session_->OnPortDestroyed(port.id())) { |
| stop_device |= SessionStoppedInner(*primary_session_); |
| } |
| } |
| ports_[port.id()] = nullptr; |
| if (stop_device) { |
| StopDevice(TeardownState::PORTS); |
| } else { |
| ContinueTeardown(TeardownState::PORTS); |
| } |
| } |
| |
| void DeviceInterface::ReleaseVmo(Session& session) { |
| uint8_t vmo; |
| vmo = session.ClearDataVmo(); |
| zx::status result = vmo_store_.Unregister(vmo); |
| if (result.is_error()) { |
| // Avoid notifying the device implementation if unregistration fails. |
| // A non-ok return here means we're either attempting to double-release a VMO or the sessions |
| // didn't have a registered VMO. |
| LOGF_WARN("network-device(%s): Failed to unregister VMO %d: %s", session.name(), vmo, |
| result.status_string()); |
| return; |
| } |
| |
| // NB: We're calling into the device layer with the control lock held here. |
| device_.ReleaseVmo(vmo); |
| } |
| |
| fbl::RefPtr<RefCountedFifo> DeviceInterface::primary_rx_fifo() { |
| SharedAutoLock lock(&control_lock_); |
| if (primary_session_) { |
| return primary_session_->rx_fifo(); |
| } |
| return nullptr; |
| } |
| |
| void DeviceInterface::NotifyTxQueueAvailable() { |
| if (primary_session_) { |
| primary_session_->ResumeTx(); |
| } |
| for (auto& session : sessions_) { |
| session.ResumeTx(); |
| } |
| } |
| |
| void DeviceInterface::NotifyTxReturned(bool was_full) { |
| SharedAutoLock lock(&control_lock_); |
| if (was_full) { |
| NotifyTxQueueAvailable(); |
| } |
| PruneDeadSessions(); |
| } |
| |
| void DeviceInterface::QueueRxSpace(const rx_space_buffer_t* rx, size_t count) { |
| LOGF_TRACE("network-device: %s(_, %ld)", __FUNCTION__, count); |
| device_.QueueRxSpace(rx, count); |
| } |
| |
| void DeviceInterface::QueueTx(const tx_buffer_t* tx, size_t count) { |
| LOGF_TRACE("network-device: %s(_, %ld)", __FUNCTION__, count); |
| device_.QueueTx(tx, count); |
| } |
| |
| void DeviceInterface::NotifyDeadSession(Session& dead_session) { |
| LOGF_TRACE("network-device: %s('%s')", __FUNCTION__, dead_session.name()); |
| // First of all, stop all data-plane operations with stopped session. |
| if (!dead_session.IsPaused()) { |
| // Stop the session. |
| SessionStopped(dead_session); |
| } |
| if (dead_session.IsPrimary()) { |
| // Tell rx queue this session can't be used anymore. |
| rx_queue_->PurgeSession(dead_session); |
| } |
| |
| // Now find it in sessions and remove it. |
| std::unique_ptr<Session> session_ptr; |
| control_lock_.Acquire(); |
| if (&dead_session == primary_session_.get()) { |
| // Nullify primary session. |
| session_ptr = std::move(primary_session_); |
| rx_queue_->TriggerSessionChanged(); |
| } else { |
| session_ptr = sessions_.erase(dead_session); |
| } |
| |
| // we can destroy the session immediately. |
| if (session_ptr->ShouldDestroy()) { |
| LOGF_TRACE("network-device: %s('%s') destroying session", __FUNCTION__, dead_session.name()); |
| ReleaseVmo(*session_ptr); |
| session_ptr = nullptr; |
| ContinueTeardown(TeardownState::SESSIONS); |
| return; |
| } |
| |
| // otherwise, add it to the list of dead sessions so we can wait for buffers to be returned before |
| // destroying it. |
| LOGF_TRACE( |
| "network-device: %s('%s') session is dead, waiting for buffers to be " |
| "reclaimed", |
| __FUNCTION__, session_ptr->name()); |
| dead_sessions_.push_back(std::move(session_ptr)); |
| control_lock_.Release(); |
| } |
| |
| void DeviceInterface::PruneDeadSessions() __TA_REQUIRES_SHARED(control_lock_) { |
| auto it = dead_sessions_.begin(); |
| while (it != dead_sessions_.end()) { |
| Session& session = *it; |
| // increment iterator before erasing, because of DoublyLinkedList |
| ++it; |
| if (session.ShouldDestroy()) { |
| // Schedule for destruction. |
| // |
| // Destruction must happen later because we currently hold shared access to the control lock |
| // and we need an exclusive lock to erase items from the dead sessions list. |
| // |
| // ShouldDestroy should only return true once in the lifetime of a session, which guarantees |
| // that postponing the destruction on the dispatcher is always safe. |
| async::PostTask(dispatcher_, [&session, this]() { |
| control_lock_.Acquire(); |
| LOGF_TRACE("network-device: destroying %s", session.name()); |
| ReleaseVmo(session); |
| dead_sessions_.erase(session); |
| ContinueTeardown(TeardownState::SESSIONS); |
| }); |
| } else { |
| LOGF_TRACE("network-device: %s: %s still pending", __FUNCTION__, session.name()); |
| } |
| } |
| } |
| |
| zx::status<std::pair<uint8_t, DataVmoStore::StoredVmo*>> DeviceInterface::RegisterDataVmo( |
| zx::vmo vmo) __TA_REQUIRES(control_lock_) { |
| if (vmo_store_.is_full()) { |
| return zx::error(ZX_ERR_NO_RESOURCES); |
| } |
| // Duplicate the VMO to share with device implementation. |
| zx::vmo device_vmo; |
| if (zx_status_t status = vmo.duplicate(ZX_RIGHT_SAME_RIGHTS, &device_vmo); status != ZX_OK) { |
| return zx::error(status); |
| } |
| |
| zx::status registration = vmo_store_.Register(std::move(vmo)); |
| if (registration.is_error()) { |
| return registration.take_error(); |
| } |
| uint8_t id = registration.value(); |
| DataVmoStore::StoredVmo* stored_vmo = vmo_store_.GetVmo(id); |
| |
| // NB: We're calling into the device implementation here while holding the control lock |
| // exclusively which we generally try to avoid in case the device wants to call back into us. |
| // Furthermore, `PrepareVmo` should have a response so that we can wait for the device to do its |
| // registration before we start sending it buffers with that VMO id. |
| // Irrelevant right now because this is a synchronous call. |
| // TODO(https://fxbug.dev/75456): We should wait until PrepareVmo returns (possibly |
| // asynchronously) before allowing the session to run. |
| device_.PrepareVmo(id, std::move(device_vmo)); |
| |
| return zx::ok(std::make_pair(id, stored_vmo)); |
| } |
| |
| void DeviceInterface::CommitAllSessions() { |
| if (primary_session_) { |
| primary_session_->AssertParentRxLock(*this); |
| primary_session_->CommitRx(); |
| } |
| for (auto& session : sessions_) { |
| session.AssertParentRxLock(*this); |
| session.CommitRx(); |
| } |
| PruneDeadSessions(); |
| } |
| |
| void DeviceInterface::CopySessionData(const Session& owner, const RxFrameInfo& frame_info) { |
| if (primary_session_ && primary_session_.get() != &owner) { |
| primary_session_->AssertParentRxLock(*this); |
| primary_session_->AssertParentControlLockShared(*this); |
| primary_session_->CompleteRxWith(owner, frame_info); |
| } |
| |
| for (auto& session : sessions_) { |
| if (&session != &owner) { |
| session.AssertParentRxLock(*this); |
| session.AssertParentControlLockShared(*this); |
| session.CompleteRxWith(owner, frame_info); |
| } |
| } |
| } |
| |
| void DeviceInterface::ListenSessionData(const Session& owner, |
| fbl::Span<const uint16_t> descriptors) { |
| if ((device_info_.device_features & FEATURE_NO_AUTO_SNOOP) || |
| !has_listen_sessions_.load(std::memory_order_relaxed)) { |
| // Avoid walking through sessions and acquiring Rx lock if we know no listen sessions are |
| // attached. |
| return; |
| } |
| fbl::AutoLock rx_lock(&rx_lock_); |
| SharedAutoLock control(&control_lock_); |
| bool copied = false; |
| for (const uint16_t& descriptor : descriptors) { |
| if (primary_session_ && primary_session_.get() != &owner && primary_session_->IsListen()) { |
| primary_session_->AssertParentRxLock(*this); |
| primary_session_->AssertParentControlLockShared(*this); |
| copied |= primary_session_->ListenFromTx(owner, descriptor); |
| } |
| for (auto& s : sessions_) { |
| if (&s != &owner && s.IsListen()) { |
| s.AssertParentRxLock(*this); |
| s.AssertParentControlLockShared(*this); |
| copied |= s.ListenFromTx(owner, descriptor); |
| } |
| } |
| } |
| if (copied) { |
| CommitAllSessions(); |
| } |
| } |
| |
| zx_status_t DeviceInterface::LoadRxDescriptors(RxSessionTransaction& transact) { |
| if (!primary_session_) { |
| return ZX_ERR_BAD_STATE; |
| } |
| return primary_session_->LoadRxDescriptors(transact); |
| } |
| |
| bool DeviceInterface::IsDataPlaneOpen() { return device_status_ == DeviceStatus::STARTED; } |
| |
| DeviceInterface::DeviceInterface(async_dispatcher_t* dispatcher, |
| ddk::NetworkDeviceImplProtocolClient parent) |
| : dispatcher_(dispatcher), |
| device_(parent), |
| vmo_store_(vmo_store::Options{ |
| vmo_store::MapOptions{ZX_VM_PERM_READ | ZX_VM_PERM_WRITE | ZX_VM_REQUIRE_NON_RESIZABLE, |
| nullptr}, |
| std::nullopt}) {} |
| |
| zx_status_t DeviceInterface::Binding::Bind(DeviceInterface* interface, |
| fidl::ServerEnd<netdev::Device> channel) { |
| fbl::AllocChecker ac; |
| std::unique_ptr<Binding> binding(new (&ac) Binding); |
| if (!ac.check()) { |
| return ZX_ERR_NO_MEMORY; |
| } |
| auto* binding_ptr = binding.get(); |
| binding->binding_ = |
| fidl::BindServer(interface->dispatcher_, std::move(channel), interface, |
| [binding_ptr](DeviceInterface* interface, fidl::UnbindInfo /*unused*/, |
| fidl::ServerEnd<fuchsia_hardware_network::Device> /*unused*/) { |
| bool bindings_empty; |
| interface->control_lock_.Acquire(); |
| interface->bindings_.erase(*binding_ptr); |
| bindings_empty = interface->bindings_.is_empty(); |
| if (bindings_empty) { |
| interface->ContinueTeardown(TeardownState::BINDINGS); |
| } else { |
| interface->control_lock_.Release(); |
| } |
| }); |
| interface->bindings_.push_front(std::move(binding)); |
| return ZX_OK; |
| } |
| |
| void DeviceInterface::Binding::Unbind() { |
| auto binding = std::move(binding_); |
| if (binding.has_value()) { |
| binding->Unbind(); |
| } |
| } |
| |
| } // namespace internal |
| } // namespace network |