blob: 2416ea823184baab7e4e5f70710d04610874861f [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 async_trait::async_trait;
use cm_config::SecurityPolicy;
use cm_util::TaskGroup;
use elf_runner::{crash_info::CrashRecords, process_launcher::NamespaceConnector};
use fidl::endpoints::{DiscoverableProtocolMarker, RequestStream, ServerEnd};
use fidl_fuchsia_component as fcomponent;
use fidl_fuchsia_component_runner as fcrunner;
use fidl_fuchsia_io as fio;
use fidl_fuchsia_memory_attribution as fattribution;
use fuchsia_async as fasync;
use fuchsia_zircon::{self as zx, Clock};
use futures::{future::BoxFuture, Future, FutureExt, TryStreamExt};
use namespace::{Namespace, NamespaceError};
use routing::policy::ScopedPolicyChecker;
use runner::component::{ChannelEpitaph, Controllable, Controller};
use sandbox::{Capability, CapabilityTrait, Dict, Open};
use std::sync::Arc;
use thiserror::Error;
use tracing::warn;
use vfs::{directory::entry::OpenRequest, execution_scope::ExecutionScope, service::endpoint};
use zx::{AsHandleRef, HandleBased, Task};
use crate::{
builtin::runner::BuiltinRunnerFactory,
model::component::WeakComponentInstance,
model::token::{InstanceRegistry, InstanceToken},
sandbox_util,
sandbox_util::LaunchTaskOnReceive,
};
const TYPE: &str = "type";
const SVC: &str = "svc";
/// The builtin runner runs components implemented inside component_manager.
///
/// Builtin components are still defined by a declaration. When a component uses
/// the builtin runner, the `type` field in the program block will identify which
/// builtin component to run (e.g `type: "elf_runner"`).
///
/// When bootstrapping the system, builtin components may be resolved by the builtin URL
/// scheme, e.g. fuchsia-builtin://#elf_runner.cm. However, it's entirely possible to resolve
/// a builtin component via other schemes. A component is a builtin component if and only
/// if it uses the builtin runner.
pub struct BuiltinRunner {
root_job: zx::Unowned<'static, zx::Job>,
task_group: TaskGroup,
elf_runner_resources: Arc<ElfRunnerResources>,
}
/// Pure data type holding some resources needed by the ELF runner.
// TODO(https://fxbug.dev/318697539): Most of this should be replaced by
// capabilities in the incoming namespace of the ELF runner component.
pub struct ElfRunnerResources {
/// Job policy requests in the program block of ELF components will be checked against
/// the provided security policy.
pub security_policy: Arc<SecurityPolicy>,
pub utc_clock: Option<Arc<Clock>>,
pub crash_records: CrashRecords,
pub instance_registry: Arc<InstanceRegistry>,
}
#[derive(Debug, Error)]
enum BuiltinRunnerError {
#[error("missing outgoing_dir in StartInfo")]
MissingOutgoingDir,
#[error("missing ns in StartInfo")]
MissingNamespace,
#[error("namespace error: {0}")]
NamespaceError(#[from] NamespaceError),
#[error("\"program.type\" must be specified")]
MissingProgramType,
#[error("cannot create job: {}", _0)]
JobCreation(fuchsia_zircon_status::Status),
#[error("unsupported \"program.type\": {}", _0)]
UnsupportedProgramType(String),
}
impl From<BuiltinRunnerError> for zx::Status {
fn from(value: BuiltinRunnerError) -> Self {
match value {
BuiltinRunnerError::MissingOutgoingDir
| BuiltinRunnerError::MissingNamespace
| BuiltinRunnerError::NamespaceError(_)
| BuiltinRunnerError::MissingProgramType
| BuiltinRunnerError::UnsupportedProgramType(_) => {
zx::Status::from_raw(fcomponent::Error::InvalidArguments.into_primitive() as i32)
}
BuiltinRunnerError::JobCreation(status) => status,
}
}
}
impl BuiltinRunner {
/// Creates a builtin runner with its required resources.
/// - `task_group`: The tasks associated with the builtin runner.
pub fn new(task_group: TaskGroup, elf_runner_resources: ElfRunnerResources) -> Self {
let root_job = fuchsia_runtime::job_default();
BuiltinRunner { root_job, task_group, elf_runner_resources: Arc::new(elf_runner_resources) }
}
/// Starts a builtin component.
fn start(
self: Arc<BuiltinRunner>,
mut start_info: fcrunner::ComponentStartInfo,
) -> Result<(impl Controllable, impl Future<Output = ChannelEpitaph> + Unpin), BuiltinRunnerError>
{
let outgoing_dir =
start_info.outgoing_dir.take().ok_or(BuiltinRunnerError::MissingOutgoingDir)?;
let namespace: Namespace =
start_info.ns.take().ok_or(BuiltinRunnerError::MissingNamespace)?.try_into()?;
let program_type = runner::get_program_string(&start_info, TYPE)
.ok_or(BuiltinRunnerError::MissingProgramType)?;
match program_type {
"elf_runner" => {
let job =
self.root_job.create_child_job().map_err(BuiltinRunnerError::JobCreation)?;
let program = ElfRunnerProgram::new(
job.duplicate_handle(zx::Rights::SAME_RIGHTS).unwrap(),
namespace,
self.elf_runner_resources.clone(),
);
program.serve_outgoing(outgoing_dir);
Ok((program, Box::pin(wait_for_job_termination(job))))
}
_ => Err(BuiltinRunnerError::UnsupportedProgramType(program_type.to_string())),
}
}
}
/// Waits for the job used by an ELF runner to run components to terminate, and translate
/// the return code to an epitaph.
///
/// Normally, the job will terminate when the builtin runner requests to stop the ELF runner.
/// We'll observe the asynchronous termination here and consider the ELF runner stopped.
async fn wait_for_job_termination(job: zx::Job) -> ChannelEpitaph {
fasync::OnSignals::new(&job.as_handle_ref(), zx::Signals::JOB_TERMINATED)
.await
.map(|_: fidl::Signals| ())
.unwrap_or_else(|error| warn!(%error, "error waiting for job termination"));
use fidl_fuchsia_component::Error;
let exit_status: ChannelEpitaph = match job.info() {
Ok(zx::JobInfo { return_code: zx::sys::ZX_TASK_RETCODE_SYSCALL_KILL, .. }) => {
// Stopping the ELF runner will destroy the job, so this is the only
// normal exit code path.
ChannelEpitaph::ok()
}
Ok(zx::JobInfo { return_code, .. }) => {
warn!(%return_code, "job terminated with abnormal return code");
Error::InstanceDied.into()
}
Err(error) => {
warn!(%error, "Unable to query job info");
Error::Internal.into()
}
};
exit_status
}
impl BuiltinRunnerFactory for BuiltinRunner {
fn get_scoped_runner(
self: Arc<Self>,
_checker: ScopedPolicyChecker,
open_request: OpenRequest<'_>,
) -> Result<(), zx::Status> {
open_request.open_service(endpoint(move |_scope, server_end| {
let runner = self.clone();
let mut stream = fcrunner::ComponentRunnerRequestStream::from_channel(server_end);
runner.clone().task_group.spawn(async move {
while let Ok(Some(request)) = stream.try_next().await {
let fcrunner::ComponentRunnerRequest::Start { start_info, controller, .. } =
request;
match runner.clone().start(start_info) {
Ok((program, on_exit)) => {
let controller =
Controller::new(program, controller.into_stream().unwrap());
runner.task_group.spawn(controller.serve(on_exit));
}
Err(err) => {
warn!("Builtin runner failed to run component: {err}");
let _ = controller.close_with_epitaph(err.into());
}
}
}
});
}))
}
}
/// The program of the ELF runner component.
struct ElfRunnerProgram {
task_group: TaskGroup,
execution_scope: ExecutionScope,
output: Dict,
job: zx::Job,
}
struct Inner {
resources: Arc<ElfRunnerResources>,
elf_runner: Arc<elf_runner::ElfRunner>,
}
impl ElfRunnerProgram {
/// Creates an ELF runner program.
/// - `job`: Each ELF component run by this runner will live inside a job that is a
/// child of the provided job.
fn new(job: zx::Job, namespace: Namespace, resources: Arc<ElfRunnerResources>) -> Self {
let namespace = Arc::new(namespace);
let connector = NamespaceConnector { namespace: namespace.clone() };
let elf_runner = elf_runner::ElfRunner::new(
job.duplicate_handle(zx::Rights::SAME_RIGHTS).unwrap(),
Box::new(connector),
resources.utc_clock.clone(),
resources.crash_records.clone(),
);
let inner = Arc::new(Inner { resources, elf_runner: Arc::new(elf_runner) });
let task_group = TaskGroup::new();
let inner_clone = inner.clone();
let elf_runner = Arc::new(LaunchTaskOnReceive::new(
task_group.as_weak(),
fcrunner::ComponentRunnerMarker::PROTOCOL_NAME,
None,
Arc::new(move |server_end, _| {
inner_clone
.clone()
.serve_component_runner(sandbox_util::take_handle_as_stream::<
fcrunner::ComponentRunnerMarker,
>(server_end))
.boxed()
}),
));
let inner_clone = inner.clone();
let snapshot_provider = Arc::new(LaunchTaskOnReceive::new(
task_group.as_weak(),
fattribution::ProviderMarker::PROTOCOL_NAME,
None,
Arc::new(move |server_end, _| {
inner_clone.clone().elf_runner.serve_memory_reporter(
sandbox_util::take_handle_as_stream::<fattribution::ProviderMarker>(server_end),
);
std::future::ready(Result::<(), anyhow::Error>::Ok(())).boxed()
}),
));
let mut output = Dict::new();
let svc = {
let mut svc = Dict::new();
svc.insert(
fcrunner::ComponentRunnerMarker::PROTOCOL_NAME.parse().unwrap(),
elf_runner.into_sender(WeakComponentInstance::invalid()).into(),
)
.ok();
svc.insert(
fattribution::ProviderMarker::PROTOCOL_NAME.parse().unwrap(),
snapshot_provider.into_sender(WeakComponentInstance::invalid()).into(),
)
.ok();
svc
};
output.insert(SVC.parse().unwrap(), Capability::Dictionary(svc)).ok();
let this = Self { task_group, execution_scope: ExecutionScope::new(), output, job };
this
}
/// Serves requests coming from `outgoing_dir` using `self.output`.
fn serve_outgoing(&self, outgoing_dir: ServerEnd<fio::DirectoryMarker>) {
let output = self.output.clone();
let open = Open::new(output.try_into_directory_entry().unwrap());
open.open(
self.execution_scope.clone(),
fio::OpenFlags::RIGHT_READABLE,
".".to_string(),
outgoing_dir.into_channel(),
);
}
}
/// In case `Controller` did not call `stop`, this will ensure that the job is destroyed.
impl Drop for ElfRunnerProgram {
fn drop(&mut self) {
_ = self.job.kill();
}
}
impl Inner {
async fn serve_component_runner(
self: Arc<Self>,
mut stream: fcrunner::ComponentRunnerRequestStream,
) -> Result<(), anyhow::Error> {
while let Ok(Some(request)) = stream.try_next().await {
let fcrunner::ComponentRunnerRequest::Start { mut start_info, controller, .. } =
request;
let Some(token) = start_info.component_instance.take() else {
warn!(
"When calling the ComponentRunner protocol of an ELF runner, \
one must provide the ComponentStartInfo.component_instance field."
);
_ = controller.close_with_epitaph(zx::Status::INVALID_ARGS);
continue;
};
let token = InstanceToken::from(token);
let Some(target_moniker) = self.resources.instance_registry.get(&token) else {
warn!(
"The provided ComponentStartInfo.component_instance token is invalid. \
The component has either already been destroyed, or the token is not minted by \
component_manager."
);
_ = controller.close_with_epitaph(zx::Status::NOT_SUPPORTED);
continue;
};
start_info.component_instance = Some(token.into());
let checker = ScopedPolicyChecker::new(
self.resources.security_policy.clone(),
target_moniker.clone(),
);
self.elf_runner.clone().get_scoped_runner(checker).start(start_info, controller).await;
}
Ok(())
}
}
#[async_trait]
impl Controllable for ElfRunnerProgram {
async fn kill(&mut self) {
warn!("Timed out stopping ElfRunner tasks");
self.stop().await
}
fn stop<'a>(&mut self) -> BoxFuture<'a, ()> {
_ = self.job.kill();
self.execution_scope.shutdown();
self.task_group.clone().join().boxed()
}
}
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use cm_types::NamespacePath;
use fcrunner::{ComponentNamespaceEntry, ComponentStartInfo};
use fidl::endpoints::ClientEnd;
use fidl_fuchsia_data::{Dictionary, DictionaryEntry, DictionaryValue};
use fidl_fuchsia_io::{self as fio, DirectoryProxy};
use fidl_fuchsia_process as fprocess;
use fuchsia_fs::directory::open_channel_in_namespace;
use fuchsia_runtime::{HandleInfo, HandleType};
use futures::channel::{self, oneshot};
use moniker::{Moniker, MonikerBase};
use sandbox::Directory;
use serve_processargs::NamespaceBuilder;
use vfs::{path::Path as VfsPath, ToObjectRequest};
use crate::{
bedrock::program::{Program, StartInfo},
model::escrow::EscrowedState,
runner::RemoteRunner,
};
use super::*;
fn make_security_policy() -> Arc<SecurityPolicy> {
Arc::new(Default::default())
}
fn make_scoped_policy_checker() -> ScopedPolicyChecker {
ScopedPolicyChecker::new(make_security_policy(), Moniker::new(vec![]))
}
fn make_builtin_runner() -> Arc<BuiltinRunner> {
let task_group = TaskGroup::new();
let security_policy = make_security_policy();
let crash_records = CrashRecords::new();
let instance_registry = InstanceRegistry::new();
let elf_runner_resources = ElfRunnerResources {
security_policy,
utc_clock: None,
crash_records,
instance_registry,
};
Arc::new(BuiltinRunner::new(task_group, elf_runner_resources))
}
fn make_start_info(
program_type: &str,
svc_dir: ClientEnd<fio::DirectoryMarker>,
) -> (ComponentStartInfo, DirectoryProxy) {
let (outgoing_dir, outgoing_server_end) = fidl::endpoints::create_proxy().unwrap();
let start_info = ComponentStartInfo {
resolved_url: Some("fuchsia-builtin://elf_runner.cm".to_string()),
program: Some(Dictionary {
entries: Some(vec![DictionaryEntry {
key: "type".to_string(),
value: Some(Box::new(DictionaryValue::Str(program_type.to_string()))),
}]),
..Default::default()
}),
ns: Some(vec![ComponentNamespaceEntry {
path: Some("/svc".to_string()),
directory: Some(svc_dir),
..Default::default()
}]),
outgoing_dir: Some(outgoing_server_end),
runtime_dir: None,
numbered_handles: None,
encoded_config: None,
break_on_start: None,
..Default::default()
};
(start_info, outgoing_dir)
}
/// Tests that:
/// - The builtin runner is able to start an ELF runner component.
/// - The ELF runner component started from it can start an ELF component.
/// - The ELF runner should be stopped in time, and doing so should also kill all
/// components run by it.
#[fuchsia::test]
async fn start_stop_elf_runner() {
let builtin_runner = make_builtin_runner();
let (client, server_end) =
fidl::endpoints::create_proxy::<fcrunner::ComponentRunnerMarker>().unwrap();
let scope = ExecutionScope::new();
let mut object_request = fio::OpenFlags::empty().to_object_request(server_end);
builtin_runner
.clone()
.get_scoped_runner(
make_scoped_policy_checker(),
OpenRequest::new(
scope.clone(),
fio::OpenFlags::empty(),
VfsPath::dot(),
&mut object_request,
),
)
.unwrap();
let (elf_runner_controller, server_end) = fidl::endpoints::create_proxy().unwrap();
// Start the ELF runner.
let (svc, svc_server_end) = fidl::endpoints::create_endpoints();
open_channel_in_namespace("/svc", fio::OpenFlags::RIGHT_READABLE, svc_server_end).unwrap();
let (start_info, outgoing_dir) = make_start_info("elf_runner", svc);
client.start(start_info, server_end).unwrap();
// Use the ComponentRunner FIDL in the outgoing directory of the ELF runner to run
// an ELF component.
let component_runner = fuchsia_component::client::connect_to_protocol_at_dir_svc::<
fcrunner::ComponentRunnerMarker,
>(&outgoing_dir)
.unwrap();
// Open the current package which contains a `signal-then-hang` component.
let (pkg, server_end) = fidl::endpoints::create_endpoints();
open_channel_in_namespace(
"/pkg",
fio::OpenFlags::RIGHT_READABLE | fio::OpenFlags::RIGHT_EXECUTABLE,
server_end,
)
.unwrap();
// Run the `signal-then-hang` component and add a numbered handle.
// This way we can monitor when that program is running.
let (ch1, ch2) = zx::Channel::create();
let (not_found, _) = channel::mpsc::unbounded();
let mut namespace = NamespaceBuilder::new(ExecutionScope::new(), not_found);
namespace
.add_entry(
Capability::Directory(Directory::new(pkg)),
&NamespacePath::new("/pkg").unwrap(),
)
.unwrap();
let moniker = Moniker::try_from(vec!["signal_then_hang"]).unwrap();
let token = builtin_runner.elf_runner_resources.instance_registry.add_for_tests(moniker);
let start_info = StartInfo {
resolved_url: "fuchsia://signal-then-hang.cm".to_string(),
program: Dictionary {
entries: Some(vec![
DictionaryEntry {
key: "runner".to_string(),
value: Some(Box::new(DictionaryValue::Str("elf".to_string()))),
},
DictionaryEntry {
key: "binary".to_string(),
value: Some(Box::new(DictionaryValue::Str(
"bin/signal_then_hang".to_string(),
))),
},
]),
..Default::default()
},
namespace,
numbered_handles: vec![fprocess::HandleInfo {
handle: ch1.into(),
id: HandleInfo::new(HandleType::User0, 0).as_raw(),
}],
encoded_config: None,
break_on_start: None,
component_instance: token,
};
let elf_runner = RemoteRunner::new(component_runner);
let (diagnostics_sender, _) = oneshot::channel();
let program = Program::start(
&elf_runner,
start_info,
EscrowedState::outgoing_dir_closed(),
diagnostics_sender,
ExecutionScope::new(),
)
.unwrap();
// Wait for the ELF component to signal on the channel.
let signals = fasync::OnSignals::new(&ch2, zx::Signals::USER_0).await.unwrap();
assert!(signals.contains(zx::Signals::USER_0));
// Stop the ELF runner component.
elf_runner_controller.stop().unwrap();
// The ELF runner controller channel should close normally.
let event = elf_runner_controller.take_event_stream().try_next().await;
assert_matches!(
event,
Err(fidl::Error::ClientChannelClosed { status, .. })
if status == zx::Status::OK
);
// The ELF component controller channel should close (abnormally, because its runner died).
let result = program.on_terminate().await;
let instance_died =
zx::Status::from_raw(fcomponent::Error::InstanceDied.into_primitive() as i32);
assert_eq!(result, instance_died);
}
/// Test that the builtin runner reports errors when starting unknown types.
#[fuchsia::test]
async fn start_error_unknown_type() {
let builtin_runner = make_builtin_runner();
let (client, server_end) =
fidl::endpoints::create_proxy::<fcrunner::ComponentRunnerMarker>().unwrap();
let scope = ExecutionScope::new();
let mut object_request = fio::OpenFlags::empty().to_object_request(server_end);
builtin_runner
.get_scoped_runner(
make_scoped_policy_checker(),
OpenRequest::new(
scope.clone(),
fio::OpenFlags::empty(),
VfsPath::dot(),
&mut object_request,
),
)
.unwrap();
let (controller, server_end) = fidl::endpoints::create_proxy().unwrap();
let (svc, svc_server_end) = fidl::endpoints::create_endpoints();
open_channel_in_namespace("/svc", fio::OpenFlags::RIGHT_READABLE, svc_server_end).unwrap();
let (start_info, _outgoing_dir) = make_start_info("foobar", svc);
client.start(start_info, server_end).unwrap();
let event = controller.take_event_stream().try_next().await;
let invalid_arguments =
zx::Status::from_raw(fcomponent::Error::InvalidArguments.into_primitive() as i32);
assert_matches!(
event,
Err(fidl::Error::ClientChannelClosed { status, .. })
if status == invalid_arguments
);
}
}