blob: 58a9bfbdf61b8123b6f539a36947ea62bebbb827 [file] [log] [blame]
// Copyright 2023 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.
use crate::capability::CapabilitySource;
use crate::model::component::WeakComponentInstance;
use ::routing::{error::RoutingError, policy::GlobalPolicyChecker};
use async_trait::async_trait;
use bedrock_error::{BedrockError, Explain};
use cm_types::Availability;
use cm_util::TaskGroup;
use fidl::{endpoints::ServerEnd, epitaph::ChannelEpitaphExt};
use fidl_fuchsia_component_sandbox as fsandbox;
use fidl_fuchsia_io as fio;
use fuchsia_zircon as zx;
use futures::future::BoxFuture;
use futures::FutureExt;
use sandbox::{AnyCapability, Capability, CapabilityTrait, Dict, Open};
use std::{fmt, sync::Arc};
use vfs::{
directory::entry::{self, DirectoryEntry, EntryInfo},
execution_scope::ExecutionScope,
path,
remote::RemoteLike,
};
use zx::HandleBased;
/// Types that implement [`Routable`] let the holder asynchronously request
/// capabilities from them.
#[async_trait]
pub trait Routable: Send + Sync {
async fn route(&self, request: Request) -> Result<Capability, BedrockError>;
}
/// [`Request`] contains metadata around how to obtain a capability.
#[derive(Debug, Clone)]
pub struct Request {
/// The minimal availability strength of the capability demanded by the requestor.
pub availability: Availability,
/// A reference to the requesting component.
pub target: WeakComponentInstance,
}
/// A [`Router`] is a capability that lets the holder obtain other capabilities
/// asynchronously. [`Router`] is the object capability representation of [`Routable`].
///
/// During routing, a request usually traverses through the component topology,
/// passing through several routers, ending up at some router that will fulfill
/// the request instead of forwarding it upstream.
#[derive(Clone)]
pub struct Router {
routable: Arc<dyn Routable>,
}
impl fmt::Debug for Router {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// TODO(https://fxbug.dev/329680070): Require `Debug` on `Routable` trait.
f.debug_struct("Router").field("routable", &"[some routable object]").finish()
}
}
// TODO(b/314343346): Complete or remove the Router implementation of sandbox::Capability
impl CapabilityTrait for Router {}
impl From<Router> for Capability {
fn from(router: Router) -> Self {
Capability::Router(Box::new(router))
}
}
/// Syntax sugar within the framework to express custom routing logic using a function
/// that takes a request and returns such future.
impl<F> Routable for F
where
F: Fn(Request) -> BoxFuture<'static, Result<Capability, BedrockError>> + Send + Sync + 'static,
{
// We use the desugared form of `async_trait` to avoid unnecessary boxing.
fn route<'a, 'b>(&'a self, request: Request) -> BoxFuture<'b, Result<Capability, BedrockError>>
where
'a: 'b,
Self: 'b,
{
self(request)
}
}
#[async_trait]
impl Routable for Router {
async fn route(&self, request: Request) -> Result<Capability, BedrockError> {
Router::route(self, request).await
}
}
impl Router {
/// Package a [`Routable`] object into a [`Router`].
pub fn new(routable: impl Routable + 'static) -> Self {
Router { routable: Arc::new(routable) }
}
/// Creates a router that will always resolve with the provided capability,
/// unless the capability is also a router, where it will recursively request
/// from the router.
pub fn new_ok(capability: impl Into<Capability>) -> Self {
let capability: Capability = capability.into();
Router::new(capability)
}
/// Creates a router that will always fail a request with the provided error.
pub fn new_error(error: impl Into<BedrockError>) -> Self {
let error: BedrockError = error.into();
Router::new(error)
}
pub fn from_any(any: AnyCapability) -> Router {
*any.into_any().downcast::<Router>().unwrap()
}
/// Obtain a capability from this router, following the description in `request`.
pub async fn route(&self, request: Request) -> Result<Capability, BedrockError> {
self.routable.route(request).await
}
/// Returns a router that ensures the capability request has an availability
/// strength that is at least the provided `availability`.
pub fn with_availability(self, availability: Availability) -> Router {
let route_fn = move |mut request: Request| {
let router = self.clone();
async move {
// The availability of the request must be compatible with the
// availability of this step of the route.
match ::routing::availability::advance(request.availability, availability) {
Ok(updated) => {
request.availability = updated;
// Everything checks out, forward the request.
let res = router.route(request).await;
res
}
Err(e) => Err(RoutingError::from(e).into()),
}
}
.boxed()
};
Router::new(route_fn)
}
/// Returns a router that ensures the capability request is allowed by the
/// policy in [`GlobalPolicyChecker`].
pub fn with_policy_check(
self,
capability_source: CapabilitySource,
policy_checker: GlobalPolicyChecker,
) -> Self {
Router::new(PolicyCheckRouter::new(capability_source, policy_checker, self))
}
/// Returns a [Dict] equivalent to `dict`, but with all [Router]s replaced with [Open].
///
/// This is an alternative to [Dict::try_into_open] when the [Dict] contains [Router]s, since
/// [Router] is not currently a type defined by the sandbox library.
pub fn dict_routers_to_open(
weak_component: &WeakComponentInstance,
dict: &Dict,
routing_task_group: TaskGroup,
) -> Dict {
let entries = dict.lock_entries();
let out = Dict::new();
let mut out_entries = out.lock_entries();
for (key, value) in &*entries {
let value = match value {
Capability::Dictionary(dict) => Capability::Dictionary(Self::dict_routers_to_open(
weak_component,
dict,
routing_task_group.clone(),
)),
Capability::Router(r) => {
let router = Router::from_any(r.clone());
let request = Request {
target: weak_component.clone(),
// Use the weakest availability, so that it gets immediately upgraded to
// the availability in `router`.
availability: cm_types::Availability::Transitional,
};
// TODO: Should we convert the Open to a Directory here if the Router wraps a
// Dict?
Capability::Open(Open::new(router.into_directory_entry(
request,
fio::DirentType::Service,
routing_task_group.clone(),
|_| {},
)))
}
other => other.clone(),
};
out_entries.insert(key.clone(), value);
}
drop(out_entries);
out
}
/// Converts the [Router] capability into DirectoryEntry such that open requests
/// will be fulfilled via the specified `request` on the router.
///
/// `entry_type` is the type of the entry when the DirectoryEntry is accessed through a `fuchsia.io`
/// connection.
///
/// Routing tasks are run on the `routing_task_group`.
///
/// When routing failed while exercising the returned DirectoryEntry, errors will be
/// sent to `errors_fn`.
pub fn into_directory_entry(
self,
request: Request,
entry_type: fio::DirentType,
routing_task_group: TaskGroup,
errors_fn: impl Fn(ErrorCapsule) + Send + Sync + 'static,
) -> Arc<dyn DirectoryEntry> {
struct RouterEntry<F> {
router: Router,
request: Request,
entry_type: fio::DirentType,
routing_task_group: TaskGroup,
errors_fn: F,
}
impl<F: Fn(ErrorCapsule) + Send + Sync + 'static> DirectoryEntry for RouterEntry<F> {
fn entry_info(&self) -> EntryInfo {
EntryInfo::new(fio::INO_UNKNOWN, self.entry_type)
}
fn open_entry(
self: Arc<Self>,
request: entry::OpenRequest<'_>,
) -> Result<(), zx::Status> {
request.open_remote(self)
}
}
impl<F: Fn(ErrorCapsule) + Send + Sync + 'static> RemoteLike for RouterEntry<F> {
fn open(
self: Arc<Self>,
scope: ExecutionScope,
flags: fio::OpenFlags,
relative_path: path::Path,
server_end: ServerEnd<fio::NodeMarker>,
) {
let this = self.clone();
self.routing_task_group.spawn(async move {
// Request a capability from the `router`.
let result = this.router.route(this.request.clone()).await;
match result {
Ok(capability) => {
// HACK: Dict needs special casing because [Dict::try_into_open]
// is unaware of [Router].
let capability = match capability {
Capability::Dictionary(d) => Router::dict_routers_to_open(
&this.request.target,
&d,
this.routing_task_group.clone(),
)
.into(),
cap => cap,
};
match super::capability_into_open(capability.clone()) {
Ok(open) => open.open(
scope,
flags,
relative_path,
server_end.into_channel(),
),
Err(error) => (this.errors_fn)(ErrorCapsule {
error: error.into(),
open_request: OpenRequest {
flags,
relative_path,
server_end: server_end.into_channel(),
},
}),
}
}
Err(error) => {
// Routing failed (e.g. broken route).
(this.errors_fn)(ErrorCapsule {
error,
open_request: OpenRequest {
flags,
relative_path,
server_end: server_end.into_channel(),
},
});
}
}
});
}
}
Arc::new(RouterEntry {
router: self.clone(),
request,
entry_type,
routing_task_group,
errors_fn,
})
}
}
/// [`ErrorCapsule `] holds an error from capability routing, and closes the
/// server endpoint in the open request with an appropriate epitaph on drop.
#[derive(Debug)]
pub struct ErrorCapsule {
error: BedrockError,
open_request: OpenRequest,
}
impl ErrorCapsule {
/// Destructures the [`ErrorCapsule`] into the error and the open request
/// sent by the client when they connect to the requested capability. It is
/// provided here such that the endpoint may be closed with an appropriate
/// epitaph, which is now the responsibility of the caller.
pub fn manually_handle(mut self) -> (BedrockError, OpenRequest) {
(self.error.clone(), self.open_request.take())
}
}
impl fmt::Display for ErrorCapsule {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.error)
}
}
impl Drop for ErrorCapsule {
fn drop(&mut self) {
self.open_request.close(self.error.as_zx_status());
}
}
#[derive(Debug)]
pub struct OpenRequest {
pub flags: fio::OpenFlags,
pub relative_path: vfs::path::Path,
pub server_end: zx::Channel,
}
impl OpenRequest {
fn take(&mut self) -> Self {
OpenRequest {
flags: self.flags.clone(),
relative_path: self.relative_path.clone(),
server_end: cm_util::channel::take_channel(&mut self.server_end),
}
}
fn close(&mut self, status: zx::Status) {
let server_end = cm_util::channel::take_channel(&mut self.server_end);
if !server_end.is_invalid_handle() {
let _ = server_end.close_with_epitaph(status);
}
}
}
impl From<Router> for fsandbox::Capability {
fn from(_router: Router) -> Self {
unimplemented!("TODO(b/314343346): Complete or remove the Router implementation of sandbox::Capability")
}
}
#[async_trait]
impl Routable for Capability {
async fn route(&self, request: Request) -> Result<Capability, BedrockError> {
match self.clone() {
Capability::Router(router) => Router::from_any(router).route(request).await,
capability => Ok(capability),
}
}
}
#[async_trait]
impl Routable for Dict {
async fn route(&self, request: Request) -> Result<Capability, BedrockError> {
Capability::Dictionary(self.clone()).route(request).await
}
}
#[async_trait]
impl Routable for BedrockError {
async fn route(&self, _: Request) -> Result<Capability, BedrockError> {
Err(self.clone())
}
}
pub struct PolicyCheckRouter {
capability_source: CapabilitySource,
policy_checker: GlobalPolicyChecker,
router: Router,
}
impl PolicyCheckRouter {
pub fn new(
capability_source: CapabilitySource,
policy_checker: GlobalPolicyChecker,
router: Router,
) -> Self {
PolicyCheckRouter { capability_source, policy_checker, router }
}
}
#[async_trait]
impl Routable for PolicyCheckRouter {
async fn route(&self, request: Request) -> Result<Capability, BedrockError> {
match self
.policy_checker
.can_route_capability(&self.capability_source, &request.target.moniker)
{
Ok(()) => self.router.route(request).await,
Err(policy_error) => Err(RoutingError::PolicyError(policy_error).into()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use assert_matches::assert_matches;
use bedrock_error::DowncastErrorForTest;
use sandbox::{Data, Message, Receiver};
#[fuchsia::test]
async fn availability_good() {
let source: Capability = Data::String("hello".to_string()).into();
let base = Router::new(source);
let proxy = base.with_availability(Availability::Optional);
let capability = proxy
.route(Request {
availability: Availability::Optional,
target: WeakComponentInstance::invalid(),
})
.await
.unwrap();
let capability = match capability {
Capability::Data(d) => d,
c => panic!("Bad enum {:#?}", c),
};
assert_eq!(capability, Data::String("hello".to_string()));
}
#[fuchsia::test]
async fn availability_bad() {
let source: Capability = Data::String("hello".to_string()).into();
let base = Router::new(source);
let proxy = base.with_availability(Availability::Optional);
let error = proxy
.route(Request {
availability: Availability::Required,
target: WeakComponentInstance::invalid(),
})
.await
.unwrap_err();
use ::routing::error::AvailabilityRoutingError;
assert_matches!(
error,
BedrockError::RoutingError(err)
if matches!(
err.downcast_for_test::<RoutingError>(),
RoutingError::AvailabilityRoutingError(
AvailabilityRoutingError::TargetHasStrongerAvailability
)
)
);
}
#[fuchsia::test]
async fn route_and_use_sender_with_dropped_receiver() {
// We want to test vending a sender with a router, dropping the associated receiver, and
// then using the sender. The objective is to observe an error, and not panic.
let (receiver, sender) = Receiver::new();
let router = Router::new_ok(sender.clone());
let capability = router
.route(Request {
availability: Availability::Required,
target: WeakComponentInstance::invalid(),
})
.await
.unwrap();
let sender = match capability {
Capability::Sender(c) => c,
c => panic!("Bad enum {:#?}", c),
};
drop(receiver);
let (ch1, _ch2) = zx::Channel::create();
assert!(sender.send(Message { channel: ch1 }).is_err());
}
}