blob: 0237389780eb9338e69f12b64b7d21bc34e6a0d9 [file] [log] [blame]
// 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.
use {
crate::{
model::moniker::{AbsoluteMoniker, MonikerError},
startup,
},
anyhow::{format_err, Context, Error},
cm_rust::FidlIntoNative,
cm_types::Url,
fidl_fuchsia_component_internal::{
self as component_internal, BuiltinPkgResolver, OutDirContents,
},
fidl_fuchsia_sys2 as fsys,
std::{convert::TryFrom, path::PathBuf, sync::Weak},
thiserror::Error,
};
/// Runtime configuration options.
/// This configuration intended to be "global", in that the same configuration
/// is applied throughout a given running instance of component_manager.
#[derive(Debug, PartialEq, Eq)]
pub struct RuntimeConfig {
/// How many children, maximum, are returned by a call to `ChildIterator.next()`.
pub list_children_batch_size: usize,
/// Security policy configuration.
pub security_policy: SecurityPolicy,
/// If true, component manager will be in debug mode. In this mode, component manager
/// provides the `BlockingEventSource` protocol and exposes this protocol. Component
/// manager will not start until it is resumed by a call to
/// `BlockingEventSource.StartComponentTree`.
///
/// This is done so that an external component (say an integration test) can subscribe
/// to events before the root component has started.
pub debug: bool,
/// If true, component_manager will serve an instance of fuchsia.process.Launcher and use this
/// launcher for the built-in ELF component runner. The root component can additionally
/// use and/or offer this service using '/builtin/fuchsia.process.Launcher' from realm.
// This flag exists because the built-in process launcher *only* works when
// component_manager runs under a job that has ZX_POL_NEW_PROCESS set to allow, like the root
// job. Otherwise, the component_manager process cannot directly create process through
// zx_process_create. When we run component_manager elsewhere, like in test environments, it
// has to use the fuchsia.process.Launcher service provided through its namespace instead.
pub use_builtin_process_launcher: bool,
/// If true, component_manager will maintain a UTC kernel clock and vend write handles through
/// an instance of `fuchsia.time.Maintenance`. This flag should only be used with the top-level
/// component_manager.
pub maintain_utc_clock: bool,
// The number of threads to use for running component_manager's executor.
// Value defaults to 1.
pub num_threads: usize,
/// The list of capabilities offered from component manager's namespace.
pub namespace_capabilities: Vec<cm_rust::CapabilityDecl>,
/// Which builtin resolver to use. If not supplied this defaults to the NONE option.
pub builtin_pkg_resolver: BuiltinPkgResolver,
/// Determine what content to expose through the component manager's
/// outgoing directory.
pub out_dir_contents: OutDirContents,
/// URL of the root component to launch. This field is used if no URL
/// is passed to component manager. If value is passed in both places, then
/// an error is raised.
pub root_component_url: Option<Url>,
}
/// Runtime security policy.
#[derive(Debug, Default, PartialEq, Eq)]
pub struct SecurityPolicy {
/// Allowlists for Zircon job policy.
pub job_policy: JobPolicyAllowlists,
}
/// Allowlists for Zircon job policy. Part of runtime security policy.
#[derive(Debug, Default, PartialEq, Eq)]
pub struct JobPolicyAllowlists {
/// Absolute monikers for components allowed to be given the ZX_POL_AMBIENT_MARK_VMO_EXEC job
/// policy.
///
/// Components must request this policy by including "job_policy_ambient_mark_vmo_exec: true" in
/// their manifest's program object and must be using the ELF runner.
/// This is equivalent to the v1 'deprecated-ambient-replace-as-executable' feature.
pub ambient_mark_vmo_exec: Vec<AbsoluteMoniker>,
/// Absolute monikers for components allowed to have their original process marked as critical
/// to component_manager's job.
///
/// Components must request this critical marking by including "main_process_critical: true" in
/// their manifest's program object and must be using the ELF runner.
pub main_process_critical: Vec<AbsoluteMoniker>,
}
impl Default for RuntimeConfig {
fn default() -> Self {
Self {
list_children_batch_size: 1000,
// security_policy must default to empty to ensure that it fails closed if no
// configuration is present or it fails to load.
security_policy: Default::default(),
debug: false,
use_builtin_process_launcher: false,
maintain_utc_clock: false,
num_threads: 1,
namespace_capabilities: vec![],
builtin_pkg_resolver: BuiltinPkgResolver::None,
out_dir_contents: OutDirContents::None,
root_component_url: Default::default(),
}
}
}
impl RuntimeConfig {
/// Load RuntimeConfig from the '--config' command line arg. Returns both the RuntimeConfig
/// and the path that the config was loaded from. Returns Err() if an an error occurs
/// loading it.
pub async fn load_from_file(args: &startup::Arguments) -> Result<(Self, PathBuf), Error> {
let config =
io_util::file::read_in_namespace_to_fidl::<component_internal::Config>(&args.config)
.await
.context(format!("Failed to read config file {}", &args.config))?;
Self::try_from(config)
.map(|s| (s, PathBuf::from(&args.config)))
.context(format!("Failed to apply config file {}", &args.config))
}
fn translate_namespace_capabilities(
capabilities: Option<Vec<fsys::CapabilityDecl>>,
) -> Result<Vec<cm_rust::CapabilityDecl>, Error> {
let capabilities = capabilities.unwrap_or(vec![]);
if let Some(c) = capabilities.iter().find(|c| {
!matches!(c, fsys::CapabilityDecl::Protocol(_) | fsys::CapabilityDecl::Directory(_))
}) {
return Err(format_err!("Type unsupported for namespace capability: {:?}", c));
}
cm_fidl_validator::validate_capabilities(&capabilities)?;
Ok(Some(capabilities).fidl_into_native())
}
}
fn parse_absolute_monikers_from_strings(
strs: &Option<Vec<String>>,
) -> Result<Vec<AbsoluteMoniker>, Error> {
let result: Result<Vec<AbsoluteMoniker>, MonikerError> = if let Some(strs) = strs {
strs.iter().map(|s| AbsoluteMoniker::parse_string_without_instances(s)).collect()
} else {
Ok(Vec::new())
};
result.context(format!("Moniker parsing error for {:?}", strs))
}
fn as_usize_or_default(value: Option<u32>, default: usize) -> usize {
match value {
Some(value) => value as usize,
None => default,
}
}
impl TryFrom<component_internal::Config> for RuntimeConfig {
type Error = Error;
fn try_from(config: component_internal::Config) -> Result<Self, Error> {
let default = RuntimeConfig::default();
let job_policy =
if let Some(component_internal::SecurityPolicy { job_policy: Some(job_policy) }) =
&config.security_policy
{
let ambient_mark_vmo_exec =
parse_absolute_monikers_from_strings(&job_policy.ambient_mark_vmo_exec)?;
let main_process_critical =
parse_absolute_monikers_from_strings(&job_policy.main_process_critical)?;
JobPolicyAllowlists { ambient_mark_vmo_exec, main_process_critical }
} else {
JobPolicyAllowlists::default()
};
let list_children_batch_size =
as_usize_or_default(config.list_children_batch_size, default.list_children_batch_size);
let num_threads = as_usize_or_default(config.num_threads, default.num_threads);
let root_component_url = match config.root_component_url {
Some(url) => Some(Url::new(url)?),
None => None,
};
Ok(RuntimeConfig {
list_children_batch_size,
security_policy: SecurityPolicy { job_policy },
namespace_capabilities: Self::translate_namespace_capabilities(
config.namespace_capabilities,
)?,
debug: config.debug.unwrap_or(default.debug),
use_builtin_process_launcher: config
.use_builtin_process_launcher
.unwrap_or(default.use_builtin_process_launcher),
maintain_utc_clock: config.maintain_utc_clock.unwrap_or(default.maintain_utc_clock),
num_threads,
builtin_pkg_resolver: config
.builtin_pkg_resolver
.unwrap_or(default.builtin_pkg_resolver),
out_dir_contents: config.out_dir_contents.unwrap_or(default.out_dir_contents),
root_component_url,
})
}
}
/// Errors returned by ScopedPolicyChecker.
#[derive(Debug, Clone, Error)]
pub enum PolicyError {
#[error("Security policy was unavailable to check")]
PolicyUnavailable,
#[error("security policy disallows \"{policy}\" job policy for \"{moniker}\"")]
JobPolicyDisallowed { policy: String, moniker: AbsoluteMoniker },
}
impl PolicyError {
fn job_policy_disallowed(policy: impl Into<String>, moniker: &AbsoluteMoniker) -> Self {
PolicyError::JobPolicyDisallowed { policy: policy.into(), moniker: moniker.clone() }
}
}
/// Evaluates security policy relative to a specific Realm (based on that Realm's AbsoluteMoniker).
pub struct ScopedPolicyChecker {
/// The runtime configuration containing the security policy to apply.
config: Weak<RuntimeConfig>,
/// The absolute moniker of the realm that policy will be evaluated for.
moniker: AbsoluteMoniker,
}
impl ScopedPolicyChecker {
pub fn new(config: Weak<RuntimeConfig>, moniker: AbsoluteMoniker) -> Self {
ScopedPolicyChecker { config, moniker }
}
// This interface is super simple for now since there's only two allowlists. In the future
// we'll probably want a different interface than an individual function per policy item.
pub fn ambient_mark_vmo_exec_allowed(&self) -> Result<(), PolicyError> {
let config = self.config.upgrade().ok_or(PolicyError::PolicyUnavailable)?;
if config.security_policy.job_policy.ambient_mark_vmo_exec.contains(&self.moniker) {
Ok(())
} else {
Err(PolicyError::job_policy_disallowed("ambient_mark_vmo_exec", &self.moniker))
}
}
pub fn main_process_critical_allowed(&self) -> Result<(), PolicyError> {
let config = self.config.upgrade().ok_or(PolicyError::PolicyUnavailable)?;
if config.security_policy.job_policy.main_process_critical.contains(&self.moniker) {
Ok(())
} else {
Err(PolicyError::job_policy_disallowed("main_process_critical", &self.moniker))
}
}
}
#[cfg(test)]
mod tests {
use {
super::*,
crate::model::moniker::ChildMoniker,
fidl::encoding::encode_persistent,
fidl::endpoints::ServerEnd,
fidl_fuchsia_io as fio, fidl_fuchsia_io2 as fio2, fuchsia_zircon as zx,
futures::future,
matches::assert_matches,
std::sync::Arc,
vfs::{
directory::entry::DirectoryEntry, execution_scope::ExecutionScope,
file::pcb::asynchronous::read_only, path, pseudo_directory,
},
};
const FOO_PKG_URL: &str = "fuchsia-pkg://fuchsia.com/foo#meta/foo.cmx";
macro_rules! test_config_ok {
(
$(
$test_name:ident => ($input:expr, $expected:expr),
)+
) => {
$(
#[test]
fn $test_name() {
assert_matches!(RuntimeConfig::try_from($input), Ok(v) if v == $expected);
}
)+
};
}
#[test]
fn invalid_moniker() {
let config = component_internal::Config {
debug: None,
list_children_batch_size: None,
maintain_utc_clock: None,
use_builtin_process_launcher: None,
builtin_pkg_resolver: None,
security_policy: Some(component_internal::SecurityPolicy {
job_policy: Some(component_internal::JobPolicyAllowlists {
main_process_critical: None,
ambient_mark_vmo_exec: Some(vec!["/".to_string(), "bad".to_string()]),
}),
}),
num_threads: None,
namespace_capabilities: None,
out_dir_contents: None,
root_component_url: None,
};
assert_matches!(RuntimeConfig::try_from(config), Err(_));
}
#[test]
fn invalid_root_component_url() {
let config = component_internal::Config {
debug: None,
list_children_batch_size: None,
maintain_utc_clock: None,
use_builtin_process_launcher: None,
builtin_pkg_resolver: None,
security_policy: None,
num_threads: None,
namespace_capabilities: None,
out_dir_contents: None,
root_component_url: Some("invalid url".to_string()),
};
assert_matches!(RuntimeConfig::try_from(config), Err(_));
}
test_config_ok! {
all_fields_none => (component_internal::Config {
debug: None,
list_children_batch_size: None,
security_policy: None,
maintain_utc_clock: None,
use_builtin_process_launcher: None,
num_threads: None,
namespace_capabilities: None,
builtin_pkg_resolver: None,
out_dir_contents: None,
root_component_url: None,
}, RuntimeConfig::default()),
all_leaf_nodes_none => (component_internal::Config {
debug: Some(false),
list_children_batch_size: Some(5),
maintain_utc_clock: Some(false),
builtin_pkg_resolver: None,
use_builtin_process_launcher: Some(true),
security_policy: Some(component_internal::SecurityPolicy {
job_policy: Some(component_internal::JobPolicyAllowlists {
main_process_critical: None,
ambient_mark_vmo_exec: None,
}),
}),
num_threads: Some(10),
namespace_capabilities: None,
out_dir_contents: None,
root_component_url: None,
}, RuntimeConfig {
debug:false, list_children_batch_size: 5,
maintain_utc_clock: false, use_builtin_process_launcher:true,
num_threads: 10,
builtin_pkg_resolver: BuiltinPkgResolver::None,
..Default::default() }),
all_fields_some => (
component_internal::Config {
debug: Some(true),
list_children_batch_size: Some(42),
maintain_utc_clock: Some(true),
use_builtin_process_launcher: Some(false),
builtin_pkg_resolver: Some(component_internal::BuiltinPkgResolver::None),
security_policy: Some(component_internal::SecurityPolicy {
job_policy: Some(component_internal::JobPolicyAllowlists {
main_process_critical: Some(vec!["/something/important".to_string()]),
ambient_mark_vmo_exec: Some(vec!["/".to_string(), "/foo/bar".to_string()]),
}),
}),
num_threads: Some(24),
namespace_capabilities: Some(vec![
fsys::CapabilityDecl::Protocol(fsys::ProtocolDecl {
name: Some("foo_svc".into()),
source_path: Some("/svc/foo".into()),
}),
fsys::CapabilityDecl::Directory(fsys::DirectoryDecl {
name: Some("bar_dir".into()),
source_path: Some("/bar".into()),
rights: Some(fio2::Operations::Connect),
}),
]),
out_dir_contents: Some(component_internal::OutDirContents::Svc),
root_component_url: Some(FOO_PKG_URL.to_string()),
},
RuntimeConfig {
debug: true,
list_children_batch_size: 42,
maintain_utc_clock: true,
use_builtin_process_launcher: false,
security_policy: SecurityPolicy {
job_policy: JobPolicyAllowlists {
ambient_mark_vmo_exec: vec![
AbsoluteMoniker::root(),
AbsoluteMoniker::from(vec!["foo:0", "bar:0"]),
],
main_process_critical: vec![
AbsoluteMoniker::from(vec!["something:0", "important:0"]),
],
}
},
num_threads: 24,
namespace_capabilities: vec![
cm_rust::CapabilityDecl::Protocol(cm_rust::ProtocolDecl {
name: "foo_svc".into(),
source_path: "/svc/foo".parse().unwrap(),
}),
cm_rust::CapabilityDecl::Directory(cm_rust::DirectoryDecl {
name: "bar_dir".into(),
source_path: "/bar".parse().unwrap(),
rights: fio2::Operations::Connect,
}),
],
builtin_pkg_resolver: BuiltinPkgResolver::None,
out_dir_contents: OutDirContents::Svc,
root_component_url: Some(Url::new(FOO_PKG_URL.to_string()).unwrap()),
}
),
}
#[fuchsia_async::run_singlethreaded(test)]
async fn config_from_file_no_arg() -> Result<(), Error> {
let args = startup::Arguments::default();
assert_matches!(RuntimeConfig::load_from_file(&args).await, Err(_));
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn config_from_file_missing() -> Result<(), Error> {
let args = startup::Arguments { config: "/foo/bar".to_string(), ..Default::default() };
assert_matches!(RuntimeConfig::load_from_file(&args).await, Err(_));
Ok(())
}
fn install_config_dir_in_namespace(
config_dir: &str,
config_file: &str,
content: Vec<u8>,
) -> Result<(), Error> {
let dir = pseudo_directory!(
config_file => read_only(move || future::ready(Ok(content.clone()))),
);
let (dir_server, dir_client) = zx::Channel::create().unwrap();
dir.open(
ExecutionScope::new(),
fio::OPEN_RIGHT_READABLE,
fio::MODE_TYPE_DIRECTORY,
path::Path::empty(),
ServerEnd::new(dir_server),
);
let ns = fdio::Namespace::installed().expect("Failed to get installed namespace");
ns.bind(config_dir, dir_client).expect("Failed to bind test directory");
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn config_from_file_valid() -> Result<(), Error> {
// Install a directory containing a test config file in the test process's namespace.
let config_dir = "/valid_config";
let config_file = "test_config";
let mut config = component_internal::Config {
debug: None,
list_children_batch_size: Some(42),
security_policy: None,
namespace_capabilities: None,
maintain_utc_clock: None,
use_builtin_process_launcher: None,
num_threads: None,
builtin_pkg_resolver: None,
out_dir_contents: None,
root_component_url: None,
};
install_config_dir_in_namespace(config_dir, config_file, encode_persistent(&mut config)?)?;
let config_path = [config_dir, "/", config_file].concat();
let args = startup::Arguments { config: config_path.to_string(), ..Default::default() };
let expected = (
RuntimeConfig { list_children_batch_size: 42, ..Default::default() },
PathBuf::from(config_path),
);
assert_matches!(RuntimeConfig::load_from_file(&args).await, Ok(v) if v == expected);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn config_from_file_invalid() -> Result<(), Error> {
// Install a directory containing a test config file in the test process's namespace.
let config_dir = "/invalid_config";
let config_file = "test_config";
// Add config file containing garbage data.
install_config_dir_in_namespace(config_dir, config_file, vec![0xfa, 0xde])?;
let config_path = [config_dir, "/", config_file].concat();
let args = startup::Arguments { config: config_path.to_string(), ..Default::default() };
assert_matches!(RuntimeConfig::load_from_file(&args).await, Err(_));
Ok(())
}
#[test]
fn policy_checker() {
macro_rules! assert_vmex_allowed_matches {
($config:expr, $moniker:expr, $expected:pat) => {
let result = ScopedPolicyChecker::new($config.clone(), $moniker.clone())
.ambient_mark_vmo_exec_allowed();
assert_matches!(result, $expected);
};
}
macro_rules! assert_vmex_disallowed {
($config:expr, $moniker:expr) => {
assert_vmex_allowed_matches!(
$config,
$moniker,
Err(PolicyError::JobPolicyDisallowed { .. })
);
};
}
macro_rules! assert_critical_allowed_matches {
($config:expr, $moniker:expr, $expected:pat) => {
let result = ScopedPolicyChecker::new($config.clone(), $moniker.clone())
.main_process_critical_allowed();
assert_matches!(result, $expected);
};
}
macro_rules! assert_critical_disallowed {
($config:expr, $moniker:expr) => {
assert_critical_allowed_matches!(
$config,
$moniker,
Err(PolicyError::JobPolicyDisallowed { .. })
);
};
}
let strong_config = Arc::new(RuntimeConfig::default());
let config = Arc::downgrade(&strong_config);
assert_vmex_disallowed!(config, AbsoluteMoniker::root());
assert_vmex_disallowed!(config, AbsoluteMoniker::from(vec!["foo:0"]));
assert_critical_disallowed!(config, AbsoluteMoniker::root());
assert_critical_disallowed!(config, AbsoluteMoniker::from(vec!["foo:0"]));
let allowed1 = AbsoluteMoniker::from(vec!["foo:0", "bar:0"]);
let allowed2 = AbsoluteMoniker::from(vec!["baz:0", "fiz:0"]);
let strong_config = Arc::new(RuntimeConfig {
security_policy: SecurityPolicy {
job_policy: JobPolicyAllowlists {
ambient_mark_vmo_exec: vec![allowed1.clone(), allowed2.clone()],
main_process_critical: vec![allowed1.clone(), allowed2.clone()],
},
},
..Default::default()
});
let config = Arc::downgrade(&strong_config);
assert_vmex_allowed_matches!(config, allowed1, Ok(()));
assert_vmex_allowed_matches!(config, allowed2, Ok(()));
assert_vmex_disallowed!(config, AbsoluteMoniker::root());
assert_vmex_disallowed!(config, allowed1.parent().unwrap());
assert_vmex_disallowed!(config, allowed1.child(ChildMoniker::from("baz:0")));
assert_critical_allowed_matches!(config, allowed1, Ok(()));
assert_critical_allowed_matches!(config, allowed2, Ok(()));
assert_critical_disallowed!(config, AbsoluteMoniker::root());
assert_critical_disallowed!(config, allowed1.parent().unwrap());
assert_critical_disallowed!(config, allowed1.child(ChildMoniker::from("baz:0")));
drop(strong_config);
assert_vmex_allowed_matches!(config, allowed1, Err(PolicyError::PolicyUnavailable));
assert_vmex_allowed_matches!(config, allowed2, Err(PolicyError::PolicyUnavailable));
assert_critical_allowed_matches!(config, allowed1, Err(PolicyError::PolicyUnavailable));
assert_critical_allowed_matches!(config, allowed2, Err(PolicyError::PolicyUnavailable));
}
}