This document describes the design principles that have guided Component Framework to date.
If a principles is included in this document, either Component Framework already supports it, or it part of the original vision even if not fully realized in today‘s system. This document aims to promote a common understanding of what the principles are, not what they should be. The merit of these principles, or suggestions for new principles that the framework wasn’t originally designed to provide, are out of scope for this document. Once the migration to Components v2 is complete, we expect to modify the principles to best match what has been learned since Component Framework was first envisioned. Consider this document a recording of historical context as a starting point for a process to evolve them.
We define a principle as a fundamental statement about the properties of a system that generally holds true. Principles are derived from the system's design goals. Although generally true, under certain circumstances a principle may not be perfectly satisfied, for example:
There are a few strategies to improve the degree to which the system sustains a principle, which are not mutually exclusive:
Some of the principles in this doc are specific to Component Framework. Others are general Fuchsia principles that Component Framework also commits to.
Components should receive the minimum capabilities they need to perform their role in the system and nothing more, in accordance with the Principle of Least Privilege.
Note: Component framework by itself cannot guarantee that components have least privilege. The responsibility for this principle extends to platform and product maintainers that use the component framework to assemble topologies.
Example: sysmgr
offers an example of an anti-pattern in Components v1. The v1 APIs supported the ability to instantiate enclosed sub-realms provisioned with their own set of capabilities. However, sysmgr
defined a realm which contained almost every system capability, making it very easy for a v1 component to violate least privilege.
The component framework provides capability routing as the main form of access control. It requires every capability* to be explicitly declared in a route thereby granting the capability to children. By employing capability routing, a parent component defines the sandboxes for its children.
There was originally an idea to complement capability routing with role-based access control. This idea never got past early brainstorming however.
There was an intent for component manager to support flexible isolation policies, which are another mechanism to control privilege. More in Isolation below.
A system exhibits ambient authority when a program can perform an operation on an object without being granted access to the object explicitly, e.g. by referring to it by name or number. It may be difficult to ensure that the program has a legitimate reason to access the object because it can generate the name by itself.
In contrast, with capability-based security, a program can only perform an operation on an object if it has a capability to access that object. Capabilities can be transferred but cannot be forged, enabling delegation of access to occur safely without ambient authorities.
Fuchsia, and likewise component framework, should operate without ambient authorities.
Since capabilities are only* provided by the parent, there is no global namespace from which to acquire capabilities or operate on objects. Example: what looks to a component like its POSIX filesystem is derived from the parent‘s declaration of the component’s sandbox.
Component framework, except in the case of a few privileged APIs,[*][peer-exception] does not reveal to components the identity of their peers. This prevents components from misusing peer identity to build ambient authorities.
Framework capabilities are a form of limited ambient authority because any component can access them. However, this ambient authority is safe because the scope of these capabilities is limited to the component's own realm.*
Component framework environments, which allow a parent to provision capabilities that are inherited by default, are a compromise between security and ergonomics that create a sort of ambient authority. While components have the ability to override the environment passed to a child, most of the time they don‘t and unlike regular capability routing, the component doesn’t have insight into the capabilities that constitute the environment. In particular, environments are used to provision runners and resolvers. This points to a possible gap in the framework for a way to route capabilities both securely and ergonomically.
Components are the foundational building blocks of Fuchsia software. Universality is the principle that all userspace software should run as components.
There is a strong and weak form of this principle. The weak form is that all software should run within components. The strong form is that programs that are part of a larger subsystem ought to be components and not processes. Furthermore, individual programs may decide to break submodules into components.
As of April 2022, a large chunk of the system is running as v2 components.
Most user experience components (running under Modular) are still v1 as of April 2022, but there is an active project to migrate them to v2.
Drivers and filesystems are processes, but there are plans to turn them into components.
It is not clear if the strong form of the principle will be applied everywhere. For example:
We never developed formal guidance for when it is appropriate to represent a program as a component vs. a process.
There may be multiple instances of the same (or different) components running simultaneously on a Fuchsia device. Component instances are independent along the following dimensions:
It's possible to create multiple instances of a component. Component instances carry their own state, children and lifecycle.
Today's system has a few concepts which map to “component identity”, used in different contexts:
archivist
and test_manager
to identify clients.Grants (persisted capabilities) and mailboxes (basically a message bus between components that don't require the sender and receiver to be running) were not implemented.
The question of what “component identity” means is not fully resolved. As mentioned in the section above, component framework has various notions of identity which are not consistent between each other. For example, under what circumstances is a component considered the “same” when it is updated/moved/recreated?
Components may possess sensitive information and privileges and the component framework is responsible for protecting their confidentiality, integrity, and availability. To do so, the component framework uses various mechanisms to isolate components and prevent them from interfering with one another. In particular:
Because components interact through capabilities, they do not know who is on the other end.
The component framework, in collaboration with runners, supports various mechanisms for isolating state between components, which it either defines itself or uses from Zircon:
Component framework does not reveal the identity of a component's peers.*
There was originally an idea that the component framework would define a type of container, called a “compartment”, to act as a runtime isolation boundary. This would count as one of the basic component relationships. Configuring the compartment boundaries would let product owners make tradeoffs between safety and performance.
We‘ve achieved the key performance benefits of colocating components in processes through custom solutions for specific use cases (devhost, fshost), but nothing that is generally usable at the component framework level. At one point, it was believed that Flutter would present an important use case because Flutter programs can benefit from collocation (so that each Flutter program doesn’t have to bring along its own runtime). However, this turned out to be difficult to achieve due the lack of a stable ABI in Flutter and Dart, and there wasn't a use case that strongly called for it.
There is a long-standing idea that component framework could give components an obfuscated token they could use to locally identify their peers. This was called “obfuscated monikers”: component manager could take the relative moniker between components, hash it (perhaps with a nonce), and present the client with the hash. Since this is derived from the relative moniker, it would inherently be instance-specific.
System resources are finite. There's only so much memory, disk, or CPU time available on a computing device. The component framework should keep track of how resources are used by components to ensure they are being used efficiently and that they can be reclaimed when no longer required or when they are more urgently needed for other purposes if the system is oversubscribed.
Resources must be used for a reason. As a general rule, every resource in the system must be accounted for in some way so the system can ensure they are being used effectively.
Every component exists for a reason. Parent component instances are responsible for determining the existence of their children by destroying children that are no longer of use. Parents also play a role in setting resource constraints for their children.
Every component runs for a reason. The component framework starts component instances when they have work to do, such as in response to incoming service requests from other components, and stops them when the demand is gone (or has lesser priority than other demands that contend for the same resources).
The component framework's current support for accountability is basic.
The component framework requires that every component belongs to a parent component.
Component manager starts component instances in response to a request (with the exception of single-run components).Component manager never proactively stops components except when they are destroyed or during system shutdown.
Component manager reclaims resources of destroyed dynamic components, erasing their storage in the background after the component is deleted.
Component framework supports an eager
option that causes a component instance to be started automatically. There is substantial uncertainty about how this feature fits into the vision for accountability. There is no clear ‘reason’ associated with the startup of eager components, and component manager never restarts eager components. On the other hand, from a UX perspective it‘s a simple, convenient way to run ‘daemon’-type components, and we haven’t come up with a better solution yet.
There was originally a much larger vision for accountability.
Every running process must belong to at least one component instance whose capabilities are currently in use, were recently of use, or will soon be of use; any outliers are considered to be running for no reason and are promptly stopped.
“Resources must be used for a reason” is currently true for isolated storage, but that‘s about it. There was a vision for a resource management system where each resource in use on the system would be attributed to a particular component instance. This attribution could be used to expose metrics for diagnostics, enforce resource limits, and balance load. Examples of resources we might track include storage, memory, CPU, GPU, power, or bandwidth. It’s likely we'll implement this, or a subset of it, in the future.
“Every component runs for a reason” is only partially achieved today. Most components are started in response to a request to access one of their exposed capabilities. However, component manager makes no effort to proactively terminate components that are not in use or are consuming too many resources. In particular, measuring when a component is “not in use” is known to be a hard problem because component manager only brokers the introduction phase of service discovery -- once the connection between client and provider has been established, it gets out of the way.
There were originally plans to build a “deferred communication” framework. This would grant the ability for a component to dispatch a message or work item which is delivered to the receiving component at a later time, relaxing constraints on when components need to run and giving the component runtime more leeway to start and stop them. In particular, the following systems were proposed:
The component framework should offer mechanisms to preserve the illusion of continuity: the user should generally not be concerned about restarting their software because it will automatically resume right where they left off, even when they reboot or replace their devices.
The fidelity of the illusion depends on how well the following properties are preserved across restarts:
In practice, the illusion is imperfect. The system cannot guarantee faithful reproduction in the presence of software upgrades, non-determinism, bugs, faults, and external dependencies on network services.
While it might seem simpler to keep components running forever, eventually the system will run out of resources so it needs a way to balance its working set size by stopping less essential components at a moment's notice (see Accountability).
In general, components continue to exist even when they stop running. Compare this with how processes work.
The capabilities routed to, from, or through a component remain consistent if the component is restarted. However, the connections to those capabilities are not preserved across restarts. Depending on the capability, a new instance might not act the same way as the old one, or it might not be possible to get multiple instances.
Component framework supports persistent storage for static component instances. The component's storage can be preserved even if its topological position changes.
There are many components in the system not tolerant to restarts. Many of these can be found by searching for components that use the reboot_on_terminate
feature.
There are no standard design patterns for how to build a component that is able to recover its state when it restarts, or for what to do when one of its dependencies becomes unavailable.
There are open questions about how restart policy for components should be configured. Relatedly, there are questions about when and how to reestablish connections between components when the server is restarted.
Components have no way to persist capabilities (also known as “grants”). If they are restarted, they must re-acquire them. Components also have no way to persist messages they have received or defer dispatch of messages to a later time. However, there was an idea for a message queue architecture called “mailboxes” which would have supported this.
If a component subscribed to events and died while some unprocessed events were in its queue, it will lose those events.
Components do not support suspend/hibernate.
This principle could have also been called “prefer static over dynamic”.
The component framework instills a general preference for APIs that are static, declarative, and assembly-time over those that are dynamic, imperative, and runtime-based.
This is not to say that all component framework APIs are declarative -- a completely static system wouldn‘t be very useful! However, the general rule is that if an aspect of a component’s definition or behavior could be described statically, it should be.
Being declarative offers the following advantages:
The vision for declarative APIs is implemented through CML. CML statically describes a component's inputs and outputs (capability routes), composition (children), and execution information.
Combined together, the component manifests in a Fuchsia build form a “component instance tree” that can be explored with host-side tooling (scrutiny
) or even just by inspecting the source files. There is also a verify routes
plugin for scrutiny
run automatically on CQ, that verifies all routes in the static topology are intact.*
[Security policy allowlists][src-security-policy] are another part of the declarative API.
Some parts of the component framework API are imperative, but only when there is a good reason for them to be. Examples include: collections, dynamic offers, RealmBuilder, and service aggregation from collections.
The dynamic parts of the component framework API are not as thoroughly developed. Historically, a lot of these questions were delegated to the session framework, but session framework has since been sunset. Overall, current products don't demand much in the way of dynamic component configuration. However, this may change if and when Fuchsia embraces third party or more open products like workstations.
Component framework APIs tend to be either mostly static or mostly dynamic; there is not much in between (service aggregation is an exception). In some cases it could be useful to have APIs that are principally static but delegate some aspects to runtime, or that are principally dynamic but are constrained by a statically described “upper bound”.
Component instances have no awareness of where the services in their sandbox actually come from. They perceive a subjective reality defined by their sandbox. It follows that component instances should not be able to distinguish whether they are running within a “test” sandbox or a “real” sandbox unless they are provided some means of external attestation, notwithstanding the possibility of covert side-channels.
Parents have a significant degree of authority over their children:
A corollary of this principle is that component manager should have no global singletons.
A large part of the vision of sandboxing was implemented. When you define a subtree it behaves a lot like a full topology:
framework
source are scoped. For example, if you request the hub from framework
, you'll get a hub rooted at that component.Some parts of the component framework API use absolute monikers:
The system shutdown API, although it could conceivably be scoped to a subtree, always shuts down the entire tree.
There was an idea that you could run ffx component relative
to a subtree, although this hasn't been implemented.
A related idea, which was never implemented, is that it should be possible to run a nested instance of component manager in the tree, and it should be possible to “compose” the subtree under the nested component manager with the parent tree.
In its most powerful form, recursive symmetry could support “lifting” a subtree of a topology to run on a different device, with all persisted state intact.
Encapsulation, as in OOP, refers to the concealment of a component's internal structure or data from a containing component. In particular, this means component instances should only have direct awareness of their child components (components they instantiated themselves) but not about their children’s descendants and not about their own ancestors.
Components are, faithful, faithful to encapsulation. Parents have access to the identities and exposed capabilities of their children, but not to their grandchildren, barring some privileged APIs.*
As discussed in Sandboxing, there are no safeguards against malicious parents that would offer their children a compromised capability. This is by design; in capability based systems, it's normal for parents to dominate their children.
Normally, a child‘s internal state is isolated from its parent, just like any other two component instances. However, there are clever ways a parent can circumvent this. For example, a parent could inject a pseudo ELF runner that behaves mostly like an ELF runner, but it injects a thread into the component’s process that exfiltrates the component's private memory.
Loose coupling makes it easier to evolve components over time. The component architecture abstracts away most component implementation details (such as the programming language used to implement components) behind common IPC protocols and data formats.
When a component uses a capability, it should explicitly declare the constraints it needs that capability to satisfy. As long as a capability provider satisfies these constraints, it should be possible to substitute one implementation for another. This property is called substitutability. Examples of such constraints are:
When components request capabilities by name from their namespace, the choice of which implementation to use resides in the component’s ancestors since they set up the environment for the component. This “call by meaning” approach to capability discovery makes the system more dynamic and configurable than if components explicitly requested to be bound to specific implementations of these capabilities (although sometimes they may).
Capabilities are the inputs and outputs of a component. To a large extent, the interactions between components can be described in terms of the capabilities routed between them, and in this sense components depend on each others' interfaces, not their implementations.
However, when a component instantiates a child, it chooses the child component by specifying a URL. In this case the component might expect a particular implementation of the child. This is a form of tighter coupling, although there is a degree of freedom because the URL is resolved relative to a resolver, which makes the ultimate decision about what component to resolve the URL to.
Component framework hides the identity of peers, which supports substitutability.
Components depend on capabilities only by name, without any version information. This can make components tightly coupled by version. Platform ABI version will solve a part of this problem.
It's not clear to what extent dependencies by component URLs agree with this principle.
Components can be updated independently of other components.
Component binaries and assets can be fetched just in time, cached, and removed when no longer of proximate use, freeing up storage for other components.
Software packages are signed to verify their authenticity and integrity, making it safe to retrieve them again from any available source, including from other Fuchsia devices.
In eng builds, when a component is relaunched, its runtime information, i.e. package, binary, and namespace, is updated to the latest version from the package server. (However, otherwise its manifest is not updated.)
A component's runtime assets (binary and package) are discarded when the component terminates.
There is an RFC approved for eager updates that will make it possible to update packages outside of an OTA. However, some work remains to integrate this update flow with the component framework.
Component manager never evicts the cached copy of a manifest. In eng builds, this can lead to inconsistency between the component's runtime state and its children or capability routes.
The essentials of the component architecture should be easy for developers to learn and apply.
The component architecture offers a relatively small number of general-purpose primitives which effectively cover the needs of software composition for all of Fuchsia’s architectural layers, from device drivers to third-party end-user apps. All components use the same primitives though they may receive different capabilities due to their respective roles.
The component architecture also eschews making assumptions about product-specific requirements, such as whether the product has a user interface or how it works. This way, we don’t have to reinvent the wheel for each new use-case that comes along.
The component framework has a responsibility to make it easy for users to get the most out of the component framework. Here are some ways of doing this:
We have reference documentation under //docs/concepts/components and //docs/development/components. There is a Components getting started guide.
We have some basic examples under //examples.
The general sentiment is that many component framework APIs aren't as elegant or user friendly as they could be.
The developer experience of iterating on a component and validating correctness outside of tests leaves much to be desired. We have several ideas for runtime-based component exploration tools that we've not yet pursued.
We have much more documentation to write, especially howto-style docs. In addition, some of the existing documentation could benefit from some love. The examples we have are fairly basic and limited to C++ and rust. We could probably benefit from more sophisticated or realistic examples.
The component framework provides abstractions over parts of Zircon but these abstractions are different than Zircon and they do not capture all the features that Zircon provides. For example:
The component framework does little to promote compatibility with software written with a traditional program model. This was probably intentional, but it‘s likely we’ll decide to incorporate some of these features in the future.
The framework (through component manager) gives access to some capabilities not specifically granted by the parent, for example:
fuchsia.component.Realm
, which allows a component to control the lifecycle of its children./pkg
, which every component gets even without having to request ithub
, which allows its client to traverse part of component topology rooted at a particular realm, meaning that it can observe and access the services of all components within that realm.Nevertheless, thanks to the invariant that framework capabilities never provide access to capabilities from the containing environment, these capabilities do not violate least privilege, no ambient authority, or encapsulation.
There are some privileged APIs (hub, realm-scoped event streams) that expose internal information about a component, such as its relative moniker, URL, or outgoing directory. However, these APIs are locked down and only usable by non-production or specially privileged components like archivist
or debug_data
.
There is a limit to how much of the topology can be statically validated. When the exploration reaches a collection, in general it has to stop because the contents of a collection are runtime determined.