blob: 0e2001721712ef2c9073a3e0111a17d58517ab5d [file] [view]
{% set rfcid = "RFC-0107" %}
{% include "docs/contribute/governance/rfcs/_common/_rfc_header.md" %}
# {{ rfc.name }}: {{ rfc.title }}
<!-- *** DO NOT EDIT ABOVE THIS LINE -->
## Summary
In Component Framework v2 (CFv2), a **parent component** can add **dynamic child
components** to the topology at runtime using [collections]. However, the set of
capabilities [offered][offer] to such a dynamic child component cannot be
configured at runtime. Offers are declared in the parent component's manifest,
which is immutable by the time the parent component has started running.
The parent component's manifest treats all the dynamic children in a given
collection uniformly; it has no way of even _talking about_ an individual
component within a collection. This implies certain limitations:
1. It is not possible to offer a capability to an individual dynamic child.
Offering a capability to a collection offers the capability to _all_ dynamic
children in that collection.
2. It is not possible to offer a capability from an individual dynamic child to
another component. Offering a capability from a collection creates an
aggregate FIDL service that allows connecting to _any_ dynamic child in that
collection.
This document proposes **dynamic offers**, which can be added to the component
topology at runtime.
## Stakeholders
* Users of CFv2 that build dynamic topologies at runtime. They will be
represented by the [**Driver Framework**][driver-framework] team.
* **Component Framework area owners**, who can verify that the design is
sensible and doesn't conflict with any future plans.
* **Security**, who can ensure this feature does not undermine the system's
security properties.
## Motivation
Most CFv2 use cases involve a static topology of components working together to
accomplish some task. The identities and relationships between all those
components are known up front and can be hard-coded into component manifests.
In some other use cases, the developer doesn't know the exact set of components
that will run, but _does_ know how these dynamic components will relate to each
other. The developer can group all the dynamic components into collections,
where each component in a given collection is offered the same set of
capabilities.
However, some users of CFv2 want to build complex topologies at runtime,
offering a unique set of capabilities to each dynamic child, and routing
capabilities between dynamic children.
### Use case: Drivers-as-Components
In Driver Framework's Drivers-as-Components project, dynamically-instantiated
driver components depend on capabilities provided by other
dynamically-instantiated driver components. We'll call capabilities that are
offered from one driver component to another **inter-driver capabilities**.
CFv2 does not natively support inter-driver capabilities. The Driver Framework
team has worked around this by implementing capability-routing-like
functionality in `driver_manager` itself. This workaround has some pretty rough
edges and exposes confusing differences between inter-driver capabilities and
"normal" capabilities:
* Components connect to "normal" capabilities by opening a path in their
incoming namespace, but connect to inter-driver capabilities via a
Driver-Framework-specific `exposed_dir` field.
* Once given an `exposed_dir`, the receiving component can access _all_ the
protocols implemented by its dependency. `driver_manager` doesn't attenuate
the receiver's access to only the protocols it needs. Driver Framework team
could fix this, but doing so would further duplicate Component Framework
mechanisms.
* Driver components will need to declare the "normal" capabilities they depend
on in their manifests' `use` section, but _should not_ list inter-driver
capabilities in the same way. If they did, Component Manager would not be
able to find an `offer` to satisfy the `use`.
* Currently, Component Manager mostly ignores these extraneous `use`s, but
that could cause confusion in the short term, and extraneous `use`s
might become errors in the longer term.
Driver authors should not have to see "normal" capabilities as different from
inter-driver capabilities, so the distinction should be removed as soon as
possible. The longer we wait, the more drivers will have to migrate when the
model changes.
## Design
This proposal extends the [`fuchsia.sys2.Realm/CreateChild`][CreateChild] API to
accept a list of [`OfferDecl`][OfferDecl]s describing additional capabilities
that will be offered to the newly created child, beyond those that would
normally be offered. We'll refer to `OfferDecl`s specified this way as **dynamic
offers**. `OfferDecl`s specified in the normal way (that is, listed in the
Component Manifest) will be called **static offers**.
Any kind of offer (i.e., protocol, directory, etc) can be used as a dynamic
offer.
Dynamic offers always target the newly created child component. As a result, the
set of dynamic offers targeting a component is known at component creation time
and is immutable thereafter. (Whether the `target` field of the `OfferDecl` must
actually be specified or not will be determined in API review.)
Any source that would be valid for a static offer is also valid for a dynamic
offer. Additionally, unlike static offers, dynamic offers can use a "sibling"
dynamic child component (i.e. a component with the same parent as the target) as
a source. If a dynamic offer has a dynamic child as a source, there's no
restriction that the target needs to be in the same collection as the source.
Once the child component is created, the dynamic offers should behave exactly
like static offers, including for considerations like component shutdown order.
### Destruction
A dynamic offer will be destroyed when either of its source or its target is
destroyed. After the offer has been destroyed, the system will behave exactly as
if it had never existed.
Where possible, users of dynamic offers should prefer destroying targets before
sources. The process of removing an offer from the topology is trivial if it's
the target that's being destroyed (if an offer falls in a forest and there's no
component around to hear it...). However, sometimes it can't be helped, and the
source will be destroyed before the target. Notably, when using a single-run
collection (introduced in [RFC-0101]), dynamic components are destroyed as soon
as they terminate, which can happen at any time.
Even if it's the source being destroyed, the process is straightforward:
1. Component manager shuts down the source component. This prevents the target
from opening any new connections to the source, and instructs the runner to
stop the source component (if it was running).
2. When the source component stops, any channels between the source and target
will close.
3. The source component and the dynamic offer will be removed from the topology
simultaneously.
From the target's perspective, the channel corresponding to the capability
closes and any attempts to re-acquire the capability fail permanently, as if the
capability hadn't been routed in the first place.
This RFC provides no mechanism for re-creating such an offer.
### Dependency cycle prevention
CFv2 requires that components and the strong offers between them form a directed
acyclic graph so that components can be gracefully shut down in dependency
order. For static offers, we can ensure there are no dependency cycles by
looking at the component manifests.
For dynamic offers, the following precepts are sufficient to ensure an acyclic
dependency graph:
1. Dynamic component creation order is a strict total order. In order words, no
two dynamic components are added to the topology at exactly the same time.
2. Dynamic offers are always created at the same time as their target component.
3. All offers (both static and dynamic) are immutable.
4. All offers (both static and dynamic) must always have a valid source and a
valid target. This property is checked by `CreateChild` on dynamic offer
creation, and whenever the source or the target is destroyed, the offer is
destroyed as well.
Put together, this means that a dynamic offer's source is always older than its
target. If the source was younger, that would mean it didn't exist when the
target was created, and therefore the offer would have had an invalid source at
creation time. Since "creation order" is a strict total order, no dependency
cycles are possible.
### Handling invalid routes
As of this writing, Component Manager does not proactively reject component
manifests with invalid routing configuration. That is, if a capability is
offered from a source that doesn't expose it or if a component doesn't receive
an offer for a capability it uses, no error is raised at component creation
time. An error only gets raised at the point when a component tries to open an
invalidly routed capability.
In the spirit of "dynamic offers should behave like static offers", we'll
implement the same behavior here. A component may create a dynamic offer for a
capability that the source doesn't expose. Likewise, a parent component may
create a dynamic child component without providing an offer for every capability
it uses.
In the future, if Component Manager changes to return errors earlier in these
situations, we will alter the contract of `CreateChild` to return errors as
well.
## Implementation
The user-facing surface of this change consists of:
1. A backwards-compatible change to the `CreateChild` API to accept a list of
dynamic offers as an argument.
2. A per-collection opt-in setting to allow dynamic offers to target the
components in that collection. (See the discussion in [Security
considerations](#security).)
3. An allowlist for parent components that may opt-in.
Implementation of (1) will be entirely local to Component Manager.
Implementation of (2) and (3) may involve changes to tools which parse CML
files. Either way, cross-team coordination should not be necessary.
## Performance
This change should have no impact on system performance outside of calls to
`CreateChild` and `DestroyChild`, which are rare and non-performance-critical
operations. Even for these operations, no adverse performance impact is
expected.
## Ergonomics
The offer destruction behavior is new, and could create ergonomic challenges for
authors of components whose dependencies suddenly vanish.
From the target component's perspective, determining what happened after the
offer is destroyed isn't completely straightforward: it simply sees the channel
close. The target component cannot immediately tell whether the source component
was destroyed, stopped, or closed the channel intentionally. To determine the
source was in fact destroyed, the target must attempt to re-connect and await a
response from Component Manager indicating a permanent failure to route.
## Security considerations {#security}
On its own, this design undermines an important security goal of CFv2: all
capability routes (or potential capability routes) should be declared, so they
can be audited. Any usage of dynamic offers outside of tests will need to ensure
that potential capability routes can be audited via some other mechanism besides
component manifests.
By using this feature, Driver Framework accepts responsibility for ensuring that
the space of potential capability routes can be audited. Currently, the rules
are encoded in a number of places: decentralized bind rules, source code for
board drivers, ACPI, and maybe others. This makes auditing a challenge. This gap
is not new and will need to be addressed by Driver Framework, but how to do so
is beyond the scope of this RFC.
An opt-in setting will be added to `CollectionDecl`, which when present will
allow the parent component to pass dynamic offers to components created in that
collection. This will make it clear to a CML reviewer that dynamic offers are in
play, and that sources outside the component manifest need to be consulted in
order to understand the scope of potential capability routes.
Furthermore, the ability to opt-in will be allowlisted by Component Framework
team, so they can ensure all usage is appropriate.
### Future work
If we find more use cases for dynamic offers, we may want to introduce **offer
upper-bounds** to the component manifest schema. Offer upper-bounds can be used
to constrain the kinds of dynamic offers that can be created at runtime.
The manifest would include statements like:
* "Protocol `fuchsia.example.Foo` can be dynamically offered from component `A`
to any component in collection `B`", or
* "Any directory capability can be dynamically offered between any two
components in collection `C`".
For the initial Driver Framework use case, we consider the coarser
collection-level opt-in to be sufficient. This stance MUST be re-evaluated
before allowlisting any other clients.
## Privacy considerations
This proposal does not expose any part of the system to data to which it didn't
already have access, so it should have no impact on privacy.
## Testing
Unit and integration test coverage will be added, similar to any other Component
Framework feature. Additional coverage will be provided indirectly by Driver
Framework's integration tests.
This feature should neither help nor harm the testability of the Component
Framework API. Using collections and the `Realm` protocol already requires
working with a real Component Manager instance in an integration testing
setting, and that will continue to be the case.
## Documentation
Since dynamic offers will be allowlisted and initially used only by a single
client, high-level Component Framework concept documentation should not be
updated to mention the existence of this feature. Documentation on the changes
to `CreateChild`, `DestroyChild`, and `CollectionDecl` should be sufficient.
This stance MUST be re-evaluated before allowlisting any other clients.
## Drawbacks, alternatives, and unknowns
The primary drawback of this design is that it makes auditing capability routes
more complex, since not all potential capability routes appear in component
manifests. However, this is intrinsic to Driver Framework: the topology of
driver components is inherently dynamic, as devices can be added and removed
from a machine on the fly. Static offers are clearly insufficient, so the story
*must* become more complicated. Dynamic offers attempts to keep this additional
complexity to a minimum by mirroring the behavior of static offers as much as
possible.
### Alternative: Capability tokens
**Capability tokens** have previously been proposed to solve a related problem.
That proposal can be thought of as a generalization of this RFC, where dynamic
offers can cross between realms. The parent of a source component "mints" a
capability token, which it passes to another parent component (potentially via a
chain of intermediate components). The receiving component can pass this token
into `CreateChild`, which acts as an authorization mechanism to demonstrate both
parents consent to the offer.
Once created, this dynamic offer should behave like a static offer, with the
major exception that these dynamic offers can cross _between realms_. Static
offers (and in the main proposal, dynamic offers as well) are always local to a
single parent component.
The implications of cross-realm offers aren't fully clear, and we do not believe
them to be necessary, so we have rejected this option.
### Alternative: Offering "live capability providers"
[fxrev.dev/483166](https://fxrev.dev/483166)
In the main proposal, dynamic offers are inert data. Offers represent
instructions for how Component Manager ought to route a request for a
capability, but don't provide access to the capability on their own.
We could instead pass around **capability providers**, channels that can be used
to obtain capabilities directly. `CreateChild` would then accept a vector of
named capability providers. When the dynamic child component tries to open a
dynamically-offered capability, that `Open` request will be forwarded to the
capability provider.
A parent component could obtain such a capability provider by:
* implementing the capability provider on its own,
* calling a `Realm` API to get a capability provider for one of its parent's
or children's exposed capabilities, or
* receiving a capability provider from another component over a channel.
This would unlock the same use cases as capability tokens, but has the added
benefit (or debatably, downside) that parents can offer capability providers
directly to their children: simply start up a server in memory and pass a
capability provider to `CreateChild`.
However, capability providers are incompatible with many valuable properties of
CFv2's routing framework: auditability of capability routes, persistence between
restarts, etc.
### Alternatives: Destruction
When a dynamic component that is the source of an offer is destroyed, there are
a number of reasonable behaviors other than destroying the offer. This section
discusses these alternatives.
#### Forbidding source destruction
An earlier draft of this RFC proposed simply forbidding destruction of a
component if it was the source of a dynamic offer. Components would be "pinned"
in this way, until the targets of those offers had been destroyed.
Even from the start, this seemed brittle and presented potential ergonomic
issues (i.e. a "component could not be destroyed" error may be very difficult to
handle). However, it became truly unworkable with the acceptance of [RFC-0101],
which introduced scenarios where components are destroyed automatically.
#### Destruction propagation
Another proposal was for destruction to propagate along offers. That is, if the
source of a dynamic offer was destroyed, the target would be destroyed as well,
and so on, recursively.
This option seemed scary and inconsistent with other CFv2 behavior. There are
many circumstances where capabilities are not routable or become unavailable,
and none of those circumstances cause compulsory termination or destruction of
components that depend on them, even when the dependency is strong. Components
can observe the fact that their dependencies are unavailable and quit if they so
choose.
[collections]: /docs/concepts/components/v2/realms.md#collections
[offer]: /docs/concepts/components/v2/capabilities/README.md#routing-terminology
[driver-framework]: /docs/development/drivers/concepts/fdf.md
[CreateChild]: https://cs.opensource.google/fuchsia/fuchsia/+/main:sdk/fidl/fuchsia.sys2/realm.fidl;l=58;drc=599c35934155b755453a2d9c228a434436e62db5
[OfferDecl]: https://cs.opensource.google/fuchsia/fuchsia/+/main:sdk/fidl/fuchsia.sys2/decls/offer_decl.fidl;l=14;drc=1969824ecf7b1e2096ca1b6c1587545699706da8
[RFC-0101]: /docs/contribute/governance/rfcs/0101_dynamic_components_with_numbered_handles.md