blob: 43c8e4ddd7f9672cf691eaf012ffa49d8383c61f [file] [view]
<!-- mdformat off(templates not supported) -->
{% set rfcid = "RFC-0120" %}
{% include "docs/contribute/governance/rfcs/_common/_rfc_header.md" %}
# {{ rfc.name }}: {{ rfc.title }}
<!-- SET the `rfcid` VAR ABOVE. DO NOT EDIT ANYTHING ELSE ABOVE THIS LINE. -->
<!-- mdformat on -->
## Summary
This RFC formalizes the requirements to use (i.e. encode and decode) the FIDL
wire format absent of a transport. It also specifies a rubric on how bindings
should expose this functionality. We introduce the concept of **wire format
metadata** describing the revision and the features of the wire format, and
require its usage in encoding and decoding APIs, such that:
- Bindings officially support using the FIDL wire format without a transport.
- Users must transmit the wire format metadata along with the encoded message.
- Bindings may support a persistence convention where the message is prefixed by
the metadata.
## Motivation
A core principle of Fuchsia is to be updatable. We have invested heavily in ABI
compatibility when FIDL is used in an IPC context, such as two peers speaking a
FIDL protocol over a Zircon channel. The standalone use cases of the FIDL wire
format on the other hand, by virtue of being relatively rare, have seen less
attention to compatibility. For example, it was sometimes incorrectly assumed
that passing the encoded bytes of a FIDL message alone would result in an
evolvable ABI.
Both the [Driver metadata RFC][driver-metadata-rfc] and [RFC-0109: Fast datagram
sockets][fast-datagram-rfc] call for sending FIDL over byte-oriented interfaces.
Now is a good time to formalize the standalone uses of the FIDL wire format to
provide them with evolution and interoperability guarantees.
## Design
Bindings MUST support encoding and decoding the FIDL wire format without a
transport, an API whose requirements are detailed below. Note that many bindings
already have some form of public encoding/decoding API (e.g. `fidl::Encode` in
the high-level C++ bindings). They should be adjusted in accordance with this
RFC. This part of the RFC can thus be seen as a formalization of a core
functionality, clarifying the layering of FIDL.
### FIDL wire format
The focus of the FIDL wire format is binary compatibility: a set of guarantees
around schema evolution to support reading data written using a different
version of that schema. For example, a type with layout `struct{uint8;uint8;}`
may evolve into layout `struct{uint16;}`. While FIDL provides extensible data
structures, those do not support the evolution of the wire format itself, such
as switching FIDL tables to a more efficient representation. Two pieces of
information in the transactional header aid the binary compatibility of the FIDL
wire format when used over a protocol and a transport:
- Magic number: identifies the revision of the wire format. If a receiver does
not support this revision, it can deterministically refuse to decode, as
opposed to assigning erroneous interpretations to a mismatched wire format.
- Flags: indicates any soft-transitions that are enabled in this message. For
example, during the union-to-xunion migration, one of the bits in the flags
was used to indicate that unions were encoded using the extensible
representation.
When the FIDL wire format is used standalone, this information is missing from
the encoded results. We propose to incorporate a subset of that information into
encoding and decoding. Specifically:
- **Encoding** transforms binding/language-specific domain objects to the
**[FIDL encoded form][fidl-encoded-form]** and an opaque blob of **wire format
metadata** that describes the revision and features of the wire format being
used.
- **Decoding** consumes a FIDL message in encoded form and the corresponding
**wire format metadata**, producing binding/language-specific domain objects.
In pseudocode, they would have the following function signatures:
```
function Encode<T>(object: T) -> (EncodedMessage, WireFormatMetadata);
function Decode<T>(message: EncodedMessage, metadata: WireFormatMetadata) -> T;
```
`EncodedMessage` contains the encoded bytes as well as any handles in the
message. Most bindings do define a type with similar or equivalent purpose.
The wire format metadata itself will have an ABI compatible with a 64-bit
integer. Its layout is as follows, in pseudo-C-notation:
```c
struct fidl_wire_format_metadata_t {
uint8_t disambiguator;
uint8_t magic_number;
uint8_t at_rest_flags[2];
uint8_t reserved[4];
};
```
[RFC-0138: Handling unknown interactions][unknown-interactions-rfc] proposes
subdividing the flags in the transactional header into `dynamic_flags` - ones
that concern the request/response interaction model of a protocol, and
`at_rest_flags` - ones that concern the wire format. This RFC assumes that
design, but could be easily adapted accordingly without losing its key
properties.
The wire format metadata should have an alignment of 8 bytes, to facilitate
in-place decoding of messages. Bindings MUST represent the metadata externally
as an opaque structure that is 8 bytes long and has an alignment of 8 bytes
(e.g. a struct with a single `uint64` field). This allows the metadata itself to
evolve by preventing users from depending on specific fields inside the
metadata.
Bindings MUST check that the `reserved` bytes are zero. Bindings MUST NOT depend
on the `at_rest_flags` bytes to have any particular value. Bindings MUST
validate that the `magic_number` represents a wire format revision that is
[supported][supported-magic-number].
Bindings MUST check that the `disambiguator` byte is zero. Having a zero byte in
the front of the metadata prevents programs from mistaking a FIDL message as
text when the message is persisted as a file (see
[Convention for data persistence](#convention_for_data_persistence)).
Note that the information contained in a FIDL transactional header is a superset
of that in the wire format metadata. The semantics of the `at_rest_flags` field
and `magic_number` field is identical between the transactional header and the
wire format metadata.
Each message MUST only be used with its corresponding piece of metadata. In
other words, it is not allowed to share metadata (e.g. use metadata `A` to
decode both message `A` and message `B`) or swap metadata (e.g. use metadata `A`
to decode message `B` and use metadata `B` to decode message `A`). This allows
the message wire format revision to be altered at run-time, such as during wire
format soft migrations.
Bindings MUST support standalone usage with the following top-level types:
- Struct
- Table
- Union
Encoding and decoding functions MUST fail given any other data type. The failure
SHOULD occur at compile-time where possible.
The FIDL language does not mandate how the wire format metadata is transmitted
or associated with the encoded message. For example, the metadata could be
derived from the transactional message header when FIDL is used in a production
IPC context.
### Convention for data persistence
To better support the motivating use cases, we would like to specify a
convention for attaching the metadata to the encoded message, where the byte
content of the message is prefixed by the metadata. Bindings SHOULD support this
prefixed flavor of the standalone wire format usage, referred to as
**persistence**.
The following persistence use cases are in scope:
- Writing a single FIDL object to network, disk, or other byte/packet oriented
interfaces that do not support transferring Zircon handles, without opting
into a request/response paradigm. In other words, the data is "at rest".
- Support messages larger than 64 KiB. The 64 KiB message size limit is a
property of the Zircon channel transport. When persisting messages to a byte
vector, no such limitations apply. The existing Rust persistence API support
large messages and has been used to workaround the channel message size limit
pending built-in FIDL support for large messages, by manually persisting large
values into a VMO.
The following use cases are out of scope:
- Built-in support for encoding a _sequence_ of messages of the same type.
Applications may define custom streaming approaches that work better with
their specific use cases.
Using this prefixed API flavor improves ergonomics and safety in a number of
ways:
- The user does not have to manually keep track of the association between data
and metadata. The data simply follows the metadata, and can be sent as one
unit. By comparison, passing the metadata out-of-band increases risk of
mismatched versions. There is extra complexity when a receiver needs to handle
multiple wire format versions multiplexed into the same persistence medium:
- When the sender in a streaming API changes identity, the new sender may be
speaking a different wire format revision than the original sender.
- Consider a proxy that receives persistent messages from multiple components
using different wire format revisions, and stores them into a database. The
proxy would have to convert out-of-band flavors back into prefixed flavors
in order to preserve the different wire format revisions.
- Buffer management is simplified and performance may improve, which is
beneficial if the standalone wire format is used in a hot path. For example,
the bindings could allocate one buffer to hold both the metadata and the
payload, or describe a single vectorized write with the first element pointing
to the metadata.
- Users do not have to re-implement the same logic for passing the metadata in
multiple languages and client libraries, since FIDL already provides an
implementation.
The persistence API MUST support the following top-level types:
- Non-resource struct
- Non-resource table
- Non-resource union
Persistence MUST fail given any other data type. The failure SHOULD occur at
compile-time where possible.
In pseudocode, the persistence API would have the following function signatures:
```
function Persist<T>(object: T) -> vector<uint8>;
function Unpersist<T>(bytes: vector<uint8>) -> T;
```
Bindings MAY use alternate naming/method signatures that are the most
appropriate in the target language, as long as they follow the shape of the API
from a data flow perspective.
Bindings MAY support a vectorized `Persist` variant that supports vectorized
output, such as producing a `zx_channel_iovec_t` or `zx_iovec_t` that links to a
number of buffers, or integrating with the idiomatic writer interfaces of target
languages. Bindings SHOULD provide the vectorizing variant if they already use
that in IPC code paths.
Bindings MAY support a vectorized `Unpersist` variant the takes vectorized
input, such as consuming `zx_iovec_t` that links to a number of buffers, or
integrating with the idiomatic reader interfaces of target languages.
Note that persistence results in bytes, as opposed to standalone
encoding/decoding which may result in handles.
Bindings MUST support persisting large values that cause the encoded message
size to exceed 64 KiB.
The FIDL style guide and API rubric should be updated to include persistence
considerations:
- Clearly indicate if a binary blob uses the persistence convention or a
custom/out-of-band mechanism to pass the metadata.
### FIDL source language
This RFC does not change the FIDL source language.
## Implementation
Bindings should adjust their standalone encode/decode API to align with the
proposed design involving metadata. They do not have to exactly follow the
function signatures, as long as the functions are consistent with the proposal
from a data dependency perspective. For example, the behavior of the decoder
must be configurable via some means by the metadata.
The same standalone encode/decode API should be used to implement messaging,
such as transactional message dispatching over Zircon channels.
Binding support for the persistence API flavors can be added independently.
There is already an [implementation][rust-persistence-impl] of persistence APIs
in the Rust bindings, but the data format and API do not match the design in
this RFC. The Rust implementation will be adjusted to align with the accepted
design.
### Rust changes
Currently, the Rust bindings provide the following functions:
```rust
fn create_persistent_header() -> PersistentHeader;
fn encode_persistent_header(header: &mut PersistentHeader) -> Result<Vec<u8>>;
fn encode_persistent<T: Persistable>(body: &mut T) -> Result<Vec<u8>>;
fn encode_persistent_body<T: Persistable>(body: &mut T, header: &PersistentHeader) -> Result<Vec<u8>>;
fn decode_persistent<T: Persistable>(bytes: &[u8]) -> Result<T>;
fn decode_persistent_header(bytes: &[u8]) -> Result<PersistentHeader>;
fn decode_persistent_body<T: Persistable>(bytes: &[u8], header: &PersistentHeader) -> Result<T>;
```
These should be replaced with the following (exact signature might differ due to
borrowing and lifetime subtleties):
```rust
fn persist<T: Persistable, W: std::io::Write>(body: &mut T, writer: W) -> Result<()>;
fn unpersist<T: Persistable, R: std::io::Read>(reader: R) -> Result<T>;
fn standalone_encode<T: TopLevel, W: std::io::Write, H: core::iter::Extend<HandleDisposition>>(body: &mut T, writer: W, out_handles: &mut H) -> Result<WireMetadata>;
fn standalone_decode<T: TopLevel, R: std::io::Read>(reader: R, handles: &mut [HandleInfo], metadata: &WireMetadata) -> Result<T>;
struct WireMetadata { /* private fields */ }
```
The `TopLevel` trait is implemented for structs, unions, and tables.
In particular, the user can no longer create a persistent header out of nothing
and reuse the same header for encoding multiple messages.
Additionally, the bindings SHOULD provide a way to serialize/deserialize
`WireMetadata` to/from bytes, to support passing the metadata out-of-band.
## Performance
Standalone encoding and decoding is part of the transactional usages of FIDL,
while persistence APIs should share a majority of the code paths. Therefore, we
can reuse the same standards and performance benchmarks.
## Ergonomics
Binding ergonomics should be designed to encourage the **persistence**
convention. For example, a binding could use a shorter and more idiomatic
function name to represent the persistent flavor (e.g. `fidl::persist`), and use
a longer and more explicit function name to represent the public standalone
encoding/decoding API (e.g. `fidl::standalone::encode`).
## Backwards compatibility
This change itself is backwards compatible since it is purely additive, with the
exception of the Rust FIDL persistence implementation. To our knowledge, all
current readers, writers, and stored data of Rust FIDL persistence always evolve
in lockstep.
Adding wire format metadata improves future backwards compatibility, in
anticipation of upcoming FIDL wire format migrations.
The wire format metadata contains 5 reserved bytes. Those bytes may be
repurposed to take on additional meaning in the future. For example, we might
use one byte to describe persistence-specific concerns.
## Security considerations
The [validation requirements][validation] of the FIDL wire format apply here,
and hold the same security properties.
Of note, FIDL is not a self-describing format. Successfully deserializing a
persisted message using one message type does not guarantee the data was
originally serialized using that same message type:
- A program may be confused over whether a FIDL message contains a prefixed
metadata header, or if the metadata is passed out-of-band, leading to
incorrect input parsing. We believe such errors tend to be caught early at the
testing stage. Coupled with clear documentation, the security risk of this
confusion should be small.
- A malicious actor may trick a program into overwriting a persisted FIDL
message with type `Foo` with another message of a different type `Bar`, which
the attacker controls, by exploiting vulnerabilities in path processing. This
allows the malicious actor to indirectly influence the contents of the `Foo`
message.
The alternatives section presents a more involved format that mitigates this
risk, by extending the metadata header with information about the message type.
## Privacy considerations
[Padding bytes][padding] in the FIDL wire format are required to be zero, which
helps avoid leaking sensitive information.
Persistent data tends to carry larger privacy concerns compared to ephemeral
data in IPC that is swiftly consumed, but we also have IPC data being sent to a
component that will persist it or send it over a network. As a result, the
privacy concerns are similar between IPC and persistent APIs.
It is worth noting that developers can always manually persist FIDL data via
other means, such as JSON or XML, even if we do not provide an API. The usual
privacy reviews should apply when a future design mentions that it involves
persisting user or other sensitive data, regardless of if the methodology is via
FIDL persistence.
Privacy annotations on FIDL API elements will simplify privacy reviews and
enable better downstream tooling (e.g. automated redaction); they are out of
scope of this RFC which focuses on a particular method of transmitting FIDL
messages.
## Testing
We will extend GIDL, the FIDL conformance test suite, to test encoding and
decoding of the persistent format.
## Documentation
- Augment the [bindings spec][fidl-bindings-spec] to include the added
requirements from this RFC (e.g. [LLCPP][llcpp-bindings-reference]).
- Create a reference page about FIDL for standalone encoding/decoding and
persistence, and the relationship between the two APIs.
- [Rust][rust-encoding-decoding] and [LLCPP][llcpp-encoding-decoding] already
have related documentation. The existing documentation will be updated.
- Add to `//examples/fidl/` in all languages demonstrating standalone encoding
decoding and persistence, and add corresponding tutorials.
## Drawbacks, alternatives, and unknowns
### Alternative 1: Only support the persistence API
We could go one step further on the convention and prescribe it as the standard:
all metadata must precede the message payload. While sufficient for the use
cases we have observed today, this direction runs the risk of becoming too rigid
in the future. By providing both an un-opinionated standalone encoding/decoding
API and an opinionated persistence API on top, users will be able to pick one
most fitting to their design.
### Alternative 2: Allow sharing of the wire format metadata
We could allow the same metadata to be shared for all messages in a session.
This allows the metadata to be sent once at the beginning and then omitted for
the rest of the communication between two peers. This could be used to stream
multiple FIDL objects over one instance of a persistent medium. For example,
[Fast datagram sockets RFC][fast-datagram-rfc] could avoid adding 8 bytes to
each UDP datagram, by first sending the metadata over the socket, followed by
multiple objects in encoded form.
In doing so, we take on a constraint that the metadata must be independent of
specific messages being encoded, and only dependent on the version of the FIDL
runtime compiled into the producer of the message. This also means that a FIDL
encoder must not arbitrarily switch wire format representations at runtime,
should it support multiple wire format representations.
The 8 bytes additional tax imposed by the metadata does not seem prohibitively
expensive. Always including it with the message relieves a lot of extra metadata
tracking complexity on the users' end.
For use cases that really desire the raw performance of not including the
metadata, we can look towards adding streaming features natively in FIDL. For
example, one could imagine defining a transport over a socket that streams
values of a single type. Bindings could be implemented to skip sending the
metadata where possible and disallow changing the sender/receiver identity (for
example, the transport must be re-established every time a new peer is
introduced).
```fidl
@stream
protocol UdpSocketPayload over zx.socket {
Send(SendMsgPayload);
-> Recv(RecvMsgPayload);
};
```
Having the metadata always specific to a message also enables using flags on a
per-message basis. For example, we might use one flag to indicate that the body
is compressed. It's also conceivable that we could design a more packed wire
representation that is less rotated towards the constraints of efficient
in-memory IPC (e.g. no need to reserve space for pointers or alignment). The
alternative format could be indicated by another bit in the reserved region of
the metadata.
### Alternative 3: Use the transactional message header
The wire format metadata during persistence could be made compatible with the
[transactional message header][transactional-message-header-spec].
We would do so by framing persistence as a transport, with a single method that
writes the object:
```fidl
@persistence
type Metadata = table {
1: foo int32;
2: bar vector<int64>;
};
// desugars to
protocol MetadataSink over persistence {
// Ordinal is the hash of `mylib/MetadataSink.Metadata`.
Metadata(Metadata);
};
```
This approach has some advantages:
- Reuse existing FIDL features. Persistence is the same as messaging over
channels except that you write the bytes to some other kind of sink (vmo,
file, socket). We could also add streaming or multiple message support later
on, by adding control packets (control ordinals) that inform the message size
among other things.
- Reduce cross-talk and improve security by reusing the ordinal hash check: an
attacker cannot fake a message as another type by twiddling some bytes in the
data plane (the payload). This strategy is consistent with the security
property of FIDL methods over channels.
This seems an elegant case of transport generalization, but the result will be a
very strange protocol that is only one-way and one-shot: the client may only
send one kind of value exactly once. There is no opportunity for the receiver to
make a response. This wouldn't mesh well with evolution features we are adding
at a protocol level, such as open and closed interactions.
### Alternative 4: Extend the wire format metadata with message type information
As a less adventurous step than alternative 3, we could hash the fully-qualified
type name of the persisted message and add that to the wire format metadata to
identify the type of the message being persisted, without introducing the idea
of a full-fledged transport.
This reduces cross-talk but still has other subtle complexities:
- How to handle the renaming of a type: the name of a persisted type now becomes
part of the ABI since it affects the hash in the metadata.
- How to harmonize this with transactional IPC uses of FIDL: the main proposal
formulates the standalone encoding/decoding API as a lower-level core feature
of FIDL from which the transactional IPC functionalities could be built on
top. This alternative results in two separate functionalities. Specifically,
the wire format metadata cannot be derived from the transactional message
header, since the latter uses a method ordinal hash that is the same for both
the request and response type.
Overall, we believe the security risks prevented by this alternative is not
worth the extra complexities required.
### Alternative 5: Restrict standalone APIs to non-resource types
The main proposal suggests two kinds of public APIs dealing with the FIDL wire
format:
- Standalone encoding/decoding: may encode resource types and produce handles.
Shared by FIDL transactional messaging implementations (client and server
bindings).
- Persistence: does not allow resource types; results are pure data. Wire format
metadata always precedes the payload as one unit.
This stems from the observation that the persistence convention is sufficient
for all standalone use cases of FIDL today.
A future use case may be better suited to separately transmitting or storing the
wire format metadata and the payload. They could reach for the standalone
encoding/decoding API, but that has the drawback of allowing handles in the API,
which could be unwarranted.
An alternative is to provide three kinds of APIs:
- Binding-internal standalone encoding/decoding: may encode resource types. Used
by transactional messaging implementations.
- Public standalone encoding/decoding: does not allow resource types.
- Persistence: does not allow resource types. Wire format metadata always
precedes the payload as one unit.
This improves the non-resource guarantee when a use case desires separately
transmitting or storing the wire format metadata, but makes for a confusing API,
since we end up with two kinds of encoding/decoding API with the only difference
being support for resource types. This is compounded by target language
limitations where sometimes it can be difficult to hide an API from the public
surface.
The main proposal takes the simplification path and merges the first two kinds
of APIs in this alternative together.
## Prior art and references
- [I2I: Persistent FIDL messages][go-i2i-persistent-fidl]
- [Transactional Message Header V3][transactional-message-header-rfc]
- [FIDL Wire Format for Wires][go-fidl-wire-format-for-wires]
- [Phickle][go-phickle]
- [Cutting the FIDL Encoding Pickle][go-cutting-the-fidl-encoding-pickle]
- [Protocol buffers][protobuf]
[driver-metadata-rfc]: https://fuchsia-review.googlesource.com/c/fuchsia/+/543802
[fast-datagram-rfc]: /docs/contribute/governance/rfcs/0109_socket_datagram_socket.md
[fidl-bindings-spec]: /docs/reference/fidl/language/bindings-spec.md
[fidl-encoded-form]: /docs/reference/fidl/language/wire-format/README.md#dual-forms
[go-cutting-the-fidl-encoding-pickle]: http://go/cutting-the-fidl-encoding-pickle
[go-fidl-wire-format-for-wires]: http://go/fidl-wire-format-for-wires
[go-i2i-persistent-fidl]: http://go/i2i-persistent-fidl-messages
[go-phickle]: http://go/phickle
[llcpp-bindings-reference]: /docs/reference/fidl/bindings/llcpp-bindings.md#encoding-decoding
[llcpp-encoding-decoding]: /docs/reference/fidl/bindings/llcpp-bindings.md#encoding-decoding
[unknown-interactions-rfc]: /docs/contribute/governance/rfcs/0138_handling_unknown_interactions.md
[rust-encoding-decoding]: /docs/reference/fidl/bindings/rust-bindings.md#encoding-decoding
[rust-persistence-impl]: https://cs.opensource.google/fuchsia/fuchsia/+/b9b8d7aae9cff4398182a0e125055950556178e1:src/lib/fidl/rust/fidl/src/encoding.rs;l=3662
[supported-magic-number]: /docs/contribute/governance/rfcs/0037_transactional_message_header_v3.md#when_should_a_new_magic_number_be_assigned
[transactional-message-header-rfc]: /docs/contribute/governance/rfcs/0037_transactional_message_header_v3.md
[transactional-message-header-spec]: /docs/reference/fidl/language/wire-format/README.md#transactional-messages
[padding]: /docs/reference/fidl/language/wire-format/README.md#padding
[protobuf]: https://developers.google.com/protocol-buffers
[validation]: /docs/reference/fidl/language/wire-format/README.md#validation