RFC-0057: Default no handles

RFC-0057: Default no handles
StatusAccepted
Areas
  • FIDL
Description

We propose to disallow handles in FIDL type declarations by default, and add a new keyword resource to mark types that are allowed to contain handles or other resource types. Adding or removing a resource modifier may be a source-breaking change.

Authors
Date submitted (year-month-day)2020-01-16
Date reviewed (year-month-day)2020-01-23

"Look Ma, no hand(le)s!"

Summary

We propose to disallow handles in FIDL type declarations by default, and add a new keyword resource1 to mark types that are allowed to contain handles or other resource types. Adding or removing a resource modifier MAY be a source-breaking change.

Motivation

A distinctive feature of FIDL is its support for Zircon handles. Handles are 32-bit integers in memory, but they are treated specially: they must be moved rather than copied, and they must be closed to avoid leaking resources. This special treatment leads to problems when considering features that only make sense for plain data without handles. Although FIDL bindings can conditionally enable code based on the presence of handles, doing so is undesirable because it breaks evolvability guarantees. For example, adding a field to a table is normally safe, but adding a handle field would become source-breaking—not only for that table, but for all types transitively containing it. This pushes bindings to be conservative, always assuming that types might contain handles even if the library author never intends to add them.

The need to accommodate handles has led to compromise:

  • In Dart, an effort to implement FIDL to JSON encoding received pushback because it would only work on types without handles, harming evolvability. It was ultimately built using the MaxHandles attribute, but this was a temporary solution because the attribute only applies to the outermost type, not to all types transitively reachable from it.
  • In Rust, adding a handle to a type for the first time is source-breaking because the type will no longer derive the Clone trait. (Properly cloning a handle requires invoking the zx_handle_duplicate syscall, which can fail.)
  • The Rust bindings for protocols take FIDL objects by mutable reference and zero out handles, rather than explicitly taking ownership, so that objects without handles can be reused afterwards.

All these cases can be handled in a safer, more ergonomic way if we require library authors to indicate whether a type may contain handles, and if changing the indication is expected to be source-breaking.

Design

Terminology

A FIDL type is either a value type or a resource type. Resource types include:

  • handle and handle<H> where H is a handle subtype
  • P and request<P> where P is the name of a protocol
  • a struct, table, or union declared with the resource modifier
  • a type alias that refers to a resource type
  • a newtype RFC-0052 that wraps a resource type
  • T? where T is a non-nullable resource type
  • array<T> and vector<T> where T is a resource type

All other types are value types.

When the resource modifier is used correctly, value types never contain handles, whereas resource types may (now, or in the future) contain handles.

Language

A new modifier resource can be applied to struct, table, and union declarations.

Without resource, the declaration is not allowed to contain resource types. The FIDL compiler must verify this. It only needs to check direct fields: if A contains B, neither are marked as resources, and B contains a handle, then compilation will fail due to B and there is no need for a separate error message about A transitively containing a handle.

With resource, the declaration is allowed to contain resource types. The new type it declares is also considered a resource type, even if it does not contain resources.

In principle the language could allow resource on newtype declarations RFC-0052. However, there is no practical use for a resource newtype wrapping a value type, so instead newtypes implicitly inherit value/resource status from the type they wrap.

Grammar

This proposal modifies one rule in the FIDL grammar:

declaration-modifiers = "strict" | "resource" ;

JSON IR

This proposal adds a key "resource" with a boolean value to all objects in the "struct_declarations", "table_declarations", and "union_declarations" arrays.

Note that this key is not redundant with "max_handles". Value types must have max_handles set to zero, but resource types can have any number of max_handles, as it reflects the actual contents of the declaration (as opposed to the library author's intent to allow handles).

Bindings

This proposal does not include specific changes to the bindings. However, it enables FIDL bindings authors (including the FIDL team) to address the issues discussed in Motivation. Here are some examples made possible by this FTP, but not required in accepting it:

  • Implement JSON serialization and serialization on value types (or more likely the FIDL Text format rather than JSON, as proposed in RFC-0058.
  • Use different type signatures for the C++ Clone() method on value/resource types, to emphasize that only resource cloning can fail.
  • Make Rust protocols take value-type arguments as &T and resource-type arguments as T, instead of using &mut T for both and only mutating the resource types.

API Rubric

The API rubric should provide guidance on when to use resource. Some simple cases:

  • A struct with no resource types SHOULD NOT be marked resource, since structs are not designed to be extended (adding a handle later would in most cases break ABI).
  • A strict table or union with no resource types SHOULD NOT be marked resource, since the strictness already signals that modifying its fields is a source-breaking change.

It should also address the case of flexible tables and unions that do not initially have handles. For instance, we might want to recommend erring on one side or the other depending on the purpose of the library, how widely it will be used, the anticipated cost of source-breaking changes in the languages being used, and other factors.

Implementation Strategy

The high-level implementation steps include:

  • Parse the resource keyword in fidlc.
  • Migrate existing FIDL libraries to use resource (more on this in Unknowns.
  • Verify value/resource type rules in fidlc, with tests.
  • Store the resource flag in the JSON IR and expose it in fidlgen.

Ergonomics

This proposal makes FIDL more complex since it introduces a new concept. Unlike other FIDL constructs such as "struct" and "protocol," new users are unlikely to guess what "resource" means, so they will need to learn from documentation.

It is arguable whether this proposal makes the FIDL language more or less ergonomic. It helps by drawing attention to declarations that contain handles, especially if the actual handle values are hidden in nested structures. Anyone skimming a library will immediately see that a structure carries handles, not just data. On the other hand, it might feel less ergonomic to worry about whether to use resource and to type the keyword. Changing one declaration from value to resource could have a painful cascading effect where many types must become resources (though this can be seen as a good thing, since otherwise it would show up as source breakage).

The increased complexity is justified by improvements in FIDL bindings. With the freedom to offer different APIs for value types and resource types, the bindings can be made safer and more ergonomic. For examples of these improvements, see Bindings.

Documentation and Examples

The following tasks need to be done:

  • Update all documentation involving handles to use resource as appropriate.
  • Update the FIDL Language Specification to explain the resource modifier.
  • Mention resource in the FIDL Tutorial. There should be a short note explaining all the modifiers (so, strict and resource).
  • Provide guidance on whether a new type without handles should be a resource.
  • Once bindings take advantage of the value/resource distinction, update their documentation to note the differences between APIs offered by value types and resource types, and provide instructions for transitioning between them (if possible).

Backwards Compatibility

This proposal has no impact on ABI compatibility.

Amendment (Jul 2021). During implementation, we discovered an edge case in this proposal's interaction with RFC-0033: Handling of unknown fields and strictness. Some bindings store unknown members when decoding tables and flexible unions; this is not possible for value types if the unknown member contains handles, so decoding must fail in this case. See the compatibility guide for more details.

Amendment (Oct 2021). After RFC-0137: Discard unknown data in FIDL, bindings no longer store unknown data, so there is no more edge case. The value/resource distinction thus has no impact on ABI compatibility.

Adding or removing a resource modifier is neither source-compatible nor transitionable,2 in the sense of RFC-0024. The bindings are explicitly allowed to generate incompatible APIs for two types that differ only in the presence of the modifier, and it may in fact be impossible to write code that compiles before and after adding/removing the modifier. Library authors wishing to transition to/from resource in a source-compatible manner must create new types and methods instead of changing existing ones.

Once bindings authors start taking advantage of the value/resource distinction, we will revisit this decision. It might be worthwhile to require a transitionable path (perhaps using an intermediate stage with the [Transitional] attribute). At the outset, this is unclear: it might be too restrictive, undermining potential API improvements this proposal is meant to enable.

Performance

This proposal has negligible impact on build performance: the FIDL compiler will do slightly more work to parse the new keyword and validate its use. It has no direct impact on IPC performance. It might enable a small improvement in some languages if bindings use the value/resource distinction to create APIs that discourage unnecessary copies. For example, there should be no need to clone a value-type object in order to send it multiple times.

Security

This proposal does not directly affect security, but it enables bindings to provide safer APIs. For example, C++ could force error handling on Clone() for resource types with [[nodiscard]], or Rust could take resource-type method arguments by move to prevent accidental use of the mutated object afterwards. These kinds of changes could prevent bugs, including security bugs.

Testing

This feature will be tested in the following ways:

  • Add tests in fidlc for the parsing and validation code paths. These tests should exercise a variety of cases where a declaration marked resource (or not) fails to conform to the definition of a resource type (or value type).
  • Add some resource type declarations to the goldens, in addition to fixing existing declarations that need resource.
  • Update the fidl-changes test suite to demonstrate the steps for a transition from value type to resource type and vice versa.

Drawbacks, Alternatives, and Unknowns

This proposal introduces a new keyword, which makes the language more complex. Having too many keywords could be a problem; "strict resource union" is a bit of a mouthful.

This proposal weakens FIDL evolvability guarantees in two ways:

  • Before, adding a handle to a type was not expected to be a source-breaking change. Now, this is explicitly allowed and expected (unless the type was marked resource in anticipation of needing to add a handle).
  • Before, a type could be declared with the future expectation of (1) adding handles to it, and (2) being able to include it as a field in any other type. Now, library authors must choose between (1) and (2) at the outset.

There are two main alternatives to this proposal:

  • Do nothing. Allow handles to be used anywhere, and live with the fact that bindings must preserve source compatibility when adding or removing handles.
  • Default allow handles. Like this proposal, but assume declarations are resource types by default, and require a value keyword to disallow resource types in a declaration.

The Motivation and Ergonomics sections argue against doing nothing. For the other alternative, experience has shown that most messages do not contain handles, and that passing handles in protocols requires care and upfront planning. In other words, value types are the common case, and the ability to add handles as an afterthought might not be as useful as it seems. This suggests that not allowing handles is the better default.

This proposal mainly benefits end-developers using FIDL bindings, whereas its drawbacks apply to library authors who design the APIs. This tradeoff is in keeping with the Fuchsia API Council Charter, which prioritizes end-developers over API designers and implementers.

One more alternative has been suggested: handles as references. Instead of banning handles from value types, it would resolve the value/resource issues by representing handles as references. Cloning a structure containing a handle would just make another reference to the same handle. This could be accomplished using shared_ptr in C++, and it could greatly simplify things without needing to add the resource keyword. However, it has its challenges:

  • All bindings would need a bookkeeping mechanism to ensure that a handle is only closed once its last reference is gone. This could be difficult in some languages.
  • After sending a handle to another process, all other references to it would become invalid, like dangling pointers. The convenience of treating handles more like ordinary values means we have less compile-time safety in these situations.
  • As this involves changing types for all handles, it would likely be a breaking change in all languages. A smooth transition would require a lot of work.

Several open questions remain for this proposal:

  • How should we migrate existing FIDL libraries? Marking all existing declarations with resource would be safe, but not reflect library authors' intentions. Marking only the bare minimum (i.e., types that contain handles) would work, but might be too aggressive in assuming that anything without handles was intended to never contain any.
  • How will this feature interact with generic data types, if they are adopted? For example, if we define a Pair<A, B> type, it should logically be a resource type if A or B is a resource type, rather than having to annotate Pair itself. Are there other cases where it is preferable to derive whether a type is a resource?

Prior Art and References

The goal of this proposal is to allow source-breaking changes to occur when changing a type's value/resource status. RFC-0024 is relevant to this goal, since it established the source-compatibility standard for FIDL. It also touched on the issue of handles making it difficult to use the Clone trait in Rust, which this proposal solves.

We are not aware of other IPC systems addressing this exact issue (distinguishing types that may contain handles, or system resources). However, the concept of annotating types in a way that "infects" all use-sites is common in programming languages. For example, async functions in JavaScript, Python, and Rust have this behaviour, as well as the IO monad in Haskell.


  1. An earlier version of this proposal instead called the keyword entity

  2. An earlier version of this proposal required the change to be transitionable.