blob: 50d49efe353dabffd3651fd363f5cfcf45a9875f [file] [view] [edit]
# Versioning Zircon syscall structs
This guide explains how to change a struct that crosses the Zircon syscall
boundary without breaking existing binaries. This process has historically been
called *struct versioning* or *struct evolution*. The intended audience is
kernel developers and anyone (human or agent) modifying the Zircon system
interface.
If you are about to add, remove, reorder, resize, rename, or change the meaning
of a field in any struct declared under
[`//zircon/system/public/zircon/`][public-headers] (or mirrored in
[`//sdk/rust/zx-types`][zx-types]), read this guide first.
Structs that are used only by syscalls annotated with `@next` (declared in
[`zircon/syscalls-next.h`][syscalls-next-h]) are out of scope for this guide.
Those syscalls are not part of the stable ABI, so their structs follow different
rules.
## Why structs can't be changed in place
The syscall ABI is a contract between the kernel and every binary built against
a [supported API level][rfc-0239], including prebuilt binaries that are not
rebuilt alongside the kernel (for example, SDK consumers, prebuilt packages,
and CTF tests). Those binaries have the size, layout, and meaning of each
struct baked in.
The kernel only needs to support binaries built against a supported API level.
Like the rest of the platform, the kernel can drop support for an API level
once that level is retired. This has two consequences:
* When an API level is retired, the kernel can stop supporting struct versions
that only binaries targeting that level use. See
[Removing an old version](#removing-an-old-version).
* The current headers describe every supported API level, using the API level
macros in [`availability.h`][availability-h] (such as `ZX_AVAILABLE_SINCE`
and `FUCHSIA_API_LEVEL_AT_LEAST`) where needed. You don't need to dig through
the history of the headers or consider retired API levels to find out which
struct versions the kernel must support.
For more about API levels and the compatibility guarantees that apply to them,
see [RFC-0239: Platform versioning in practice][rfc-0239] and the
[Fuchsia platform API evolution guidelines][api-evolution].
Any of these changes silently break an existing binary:
* **Adding a field, even at the end.** For example, for `zx_object_get_info()`,
the kernel copies `sizeof(struct)` bytes and returns
`ZX_ERR_BUFFER_TOO_SMALL` when the caller's buffer is smaller. An old binary
that passes a buffer sized for the old struct starts failing.
* **Removing, reordering, or resizing a field.** Old binaries read or write
the wrong bytes.
* **Changing the meaning or units of an existing field.** The layout is
unchanged, but old binaries misinterpret the value.
Renaming a field without changing its layout or meaning is ABI-compatible but
breaks source compatibility. See [Source compatibility](#source-compatibility).
## Design rationale
The versioning scheme is guided by these principles:
* **Keep callers simple.** The kernel does the work of supporting multiple
versions. Callers don't need a library to encode, decode, or translate
structs (as they would with protobuf-style encodings). Every supported
language can use the plain C struct layouts through its native FFI.
* **Preserve performance.** If a single syscall gets or sets a group of
related values today, a new version must still allow that in a single
syscall.
* **Allow soft transitions.** An interface evolves in steps: add the new
version alongside the old, migrate callers, and then remove the old version.
### Alternatives considered
* **Only append new fields to the end of a struct.** This doesn't work when
the syscall reads or writes an array of structs (for example,
`ZX_INFO_PROCESS_MAPS` or `ZX_INFO_PROCESS_VMOS`), because the element
stride changes. It also forces new fields into a position that may waste
space on alignment padding. With versioned structs, each new version can
place fields where they logically belong.
* **Add a `version` field to each struct.** This is awkward for arrays of
structs and requires more kernel work for output structs. Because nearly
all syscalls that take or return structs already have a `topic` or `options`
argument, the version is encoded there instead. Don't add version fields to
syscall structs.
## The versioning scheme
Instead of modifying a struct in place, you introduce a new version of the
struct and keep serving the old version until no binaries depend on it:
1. Snapshot the existing struct under a versioned name (for example,
`zx_info_vmo_v3_t`) and keep supporting it in the kernel.
1. Make the unversioned name (`zx_info_vmo_t`) refer to the new layout.
1. Once all callers have been recompiled against the new version and no binary
built against a supported API level uses the old one, delete the old
version.
This scheme only works when the caller can tell the kernel which version it is
using. Each syscall that supports versioning has a *version channel* in its
arguments:
| Syscall(s) | Version channel | Encoding | Example |
| --- | --- | --- | --- |
| `zx_object_get_info()` | `topic` argument | Upper 4 bits of the topic, using `__ZX_INFO_TOPIC(topic, version)` in [`object.h`][object-h] | `ZX_INFO_VMO_V3` = `__ZX_INFO_TOPIC(23u, 2)`, `ZX_INFO_VMO` = `__ZX_INFO_TOPIC(23u, 3)` |
| `zx_clock_create()`, `zx_clock_get_details()`, `zx_clock_update()` | `options` argument | `ZX_CLOCK_ARGS_VERSION(n)` in [`clock.h`][clock-h] (6 bits at bit 58) | `zx_clock_update_args_v1_t` and `zx_clock_update_args_v2_t` |
| `zx_job_set_policy()` | `topic` argument | Distinct topic constants in [`policy.h`][policy-h] | `ZX_JOB_POL_BASIC_V1` and `ZX_JOB_POL_BASIC_V2` |
Note: Version numbers in the encoding are zero-based for `__ZX_INFO_TOPIC`,
while struct and constant names are one-based. For example,
`ZX_INFO_VMO_V1` is `__ZX_INFO_TOPIC(23u, 0)`.
The `__ZX_INFO_TOPIC` encoding has 4 bits of version, so a single
`zx_object_get_info()` topic can have at most 16 versions over its lifetime.
### Syscalls without a version channel
A few syscalls take or return structs but have no version channel:
* **Syscalls whose structs are considered final.** For example,
`zx_object_wait_many()` takes an array of `zx_wait_item_t`. The struct
already carries everything needed to wait on signals, so a change would
imply a different operation (such as waiting on something other than
signals). In that case, add a new syscall.
* **Syscalls whose struct format is determined by another syscall.** For
example, `zx_port_queue()` and `zx_port_wait()` have no version channel, but
they exchange `zx_port_packet_t`, a tagged union. New packet types of the
same size can be added to the union, and the `options` argument of
`zx_port_create()` could select a different packet format for a port.
Otherwise, if the syscall that consumes or produces the struct has no topic,
options bits, or other way for the caller to select a version, you cannot use
this scheme. Your options are to add a new syscall or to add a version channel
in a new syscall and migrate callers to it. Either option is a change to the
Zircon system interface; see [Process requirements](#process-requirements).
When you design a new syscall that takes or returns a struct, include a version
channel (such as reserved version bits in `options`) from the start so that the
struct can evolve later. The [system API rubric][system-rubric] requires this.
## Naming conventions
Follow the existing convention in [`object.h`][object-h]:
* The **unversioned** names (`zx_info_vmo_t`, `ZX_INFO_VMO`) always refer to the
**newest** version. Most code should use these.
* **Older** versions get a `_vN` suffix on the struct (`zx_info_vmo_v1_t`,
`zx_info_vmo_v2_t`, ...) and a `_VN` suffix on the topic or constant
(`ZX_INFO_VMO_V1`, ...).
* Nested structs that change are versioned together with their parent (for
example, `zx_info_maps_v2_t` contains `zx_info_maps_mapping_v2_t`). See
[Nested and shared structs](#nested-and-shared-structs).
* Version numbers are never reused. After an old version is removed, the
unversioned topic keeps its version bits. For example, `ZX_INFO_PROCESS`
is still `__ZX_INFO_TOPIC(3u, 1u)` even though `ZX_INFO_PROCESS_V1` was
removed.
## Nested and shared structs {#nested-and-shared-structs}
A version number identifies everything the kernel reads or writes for that
call, not just the outermost struct. Think of the struct selected by the
version channel as the *root*. The ABI covered by the root's version includes:
* Every struct, array, and union the root embeds by value, recursively.
* Any data the kernel reads or writes through pointers in the root.
So when you change a struct, the version bump propagates **upward** to every
root that can reach it:
* **Embedded by value:** If a nested struct changes size, the layout of every
struct that contains it changes, as does the stride of any array of those
structs. Even if the size doesn't change, old binaries misread a field whose
meaning changed.
* **Reached through a pointer:** The outer struct's layout doesn't change, but
old binaries still pass or expect the old format of the pointed-to data. The
kernel can only tell the formats apart by the root's version, so the root
needs a new version.
Because the kernel keeps serving the old root, every struct between the
changed struct and the root needs an old copy too. For example, when
`zx_arm64_exc_data_t` grew, the old layouts were preserved as a chain of
versioned structs served under `ZX_INFO_THREAD_EXCEPTION_REPORT_V1`:
```c
typedef struct zx_exception_report_v1 {
zx_exception_header_t header;
zx_exception_context_v1_t context; // contains zx_arm64_exc_data_v1_t
} zx_exception_report_v1_t;
```
Versioning propagates only upward, toward roots. Changing an outer struct
doesn't require you to version the structs it contains.
### Shared structs
Since the unversioned name always refers to the newest version, changing an
unversioned struct changes **every** struct that embeds it, including structs
used by syscalls you didn't intend to change. If any of those syscalls don't
get a version bump, the result is a silent ABI break.
For example, `zx_exception_context_t` reaches the kernel through several paths:
* Inside `zx_exception_report_t`, returned by `zx_object_get_info()` with
`ZX_INFO_THREAD_EXCEPTION_REPORT`.
* As the `context` argument of `zx_thread_raise_exception()`.
* Inside `zx_restricted_exception_t` (through `zx_exception_report_t`), which
restricted mode writes without any version channel. This struct is
currently exempt only because it is declared in the unstable
`zircon/syscalls-next.h`.
Before you change a struct, find every root it can be reached from: each
`zx_object_get_info()` topic, each argument versioned through `options`, and
each pointer the kernel follows. For each struct that contains the changed
struct, do one of the following:
* **Version it as well**, along with its root. This requires the root to have
a version channel.
* **Pin it** to the old version by changing it to embed the `_vN` struct
explicitly, so its layout is unchanged.
If any affected root has no version channel, see
[Syscalls without a version channel](#syscalls-without-a-version-channel).
## Struct layout rules
New struct versions must follow the general rules for syscall structs:
* Use fixed-width types (`uint32_t`, `uint64_t`, `zx_koid_t`, etc.).
* Make all padding explicit with named fields such as `uint8_t padding1[4];`.
Don't rely on implicit compiler padding.
* Prefer bit flags in a `uint32_t` or `uint64_t` over `bool` fields.
* Document every field, including whether it is valid only in some cases.
## Step-by-step: adding a new version
The following steps describe the usual single-CL approach used by recent
changes, such as the change that introduced the current `ZX_INFO_KMEM_STATS`
(RFC-0262) and the `zx_info_vmo_t`, `zx_info_maps_t`, and
`zx_info_task_stats_t` versions from RFC-0254.
### 1. Update the C headers
In the appropriate header under
[`//zircon/system/public/zircon/syscalls/`][syscalls-headers]:
1. Copy the current struct definition verbatim to a new struct with the next
`_vN` name (for example, `zx_info_kmem_stats_v1_t`). The layout must be
byte-for-byte identical to the struct that exists today.
1. Add a `_VN` topic or constant for that old struct with the **existing
numeric value** of the unversioned topic. Old binaries keep sending this
value.
1. Modify the unversioned struct (`zx_info_kmem_stats_t`) to the new layout.
1. Bump the version bits of the unversioned topic (for example, from
`__ZX_INFO_TOPIC(17u, 0)` to `__ZX_INFO_TOPIC(17u, 1)`).
1. If the struct you changed is embedded in, or pointed to by, other syscall
structs, version or pin each of them and their roots as described in
[Nested and shared structs](#nested-and-shared-structs).
For example, this is how `ZX_INFO_KMEM_STATS` was versioned:
```c
-#define ZX_INFO_KMEM_STATS ((zx_object_info_topic_t) 17u) // zx_info_kmem_stats_t[1]
+#define ZX_INFO_KMEM_STATS __ZX_INFO_TOPIC(17u, 1) // zx_info_kmem_stats_t[1]
+#define ZX_INFO_KMEM_STATS_V1 __ZX_INFO_TOPIC(17u, 0) // zx_info_kmem_stats_v1_t[1]
```
### 2. Update the kernel
The kernel must accept every supported version.
* **Output structs** (for example, `zx_object_get_info()` topics): Gather the
information once in the newest internal form, then convert it to the
requested version before you copy it out. See the handling of
`ZX_INFO_VMO_V1`, `ZX_INFO_VMO_V2`, and `ZX_INFO_VMO_V3` in
[`object_info.rs`][object-info-rs] and the `vmo_info_to_version` and
`MapsInfoVersion` helpers in [`memory_stats.rs`][memory-stats-rs].
* **Input structs** (for example, `zx_clock_update()` arguments or
`zx_job_set_policy()` policies): Decode the version from the version channel,
validate the buffer size for *that* version, and translate older versions to
the internal representation with well-defined defaults for new fields.
Reject unknown versions with `ZX_ERR_INVALID_ARGS`.
### 3. Update the bindings, documentation, and validators
Search the tree for the existing topic and struct names and update every place
that mirrors them. This usually includes:
* [`//sdk/rust/zx-types/src/lib.rs`][zx-types]: Add the `_VN` topic and the
`_vN_t` struct, update the unversioned struct, and keep the
`const_assert_eq!` size and alignment checks accurate for every version.
* [`//sdk/rust/zx`][zx-rust] and [`//zircon/system/ulib/zx`][zx-cpp]:
Update high-level wrappers that expose the struct.
* [`//zircon/vdso/*.fidl`][vdso]: Update the syscall reference documentation
in the doc comments, including documentation for the older versions.
* [`//zircon/kernel/tests/abi_type_validator.cc`][abi-validator]: Add size,
alignment, and field offset checks for the new struct and keep the checks
for the old versions.
* Tools that decode syscall structs, if they reference the topic, such as
[`//tools/fidlcat`][fidlcat], [`//src/lib/fidl_codec`][fidl-codec], and
[`//src/lib/zxdump`][zxdump].
* [`//zircon/public/sysroot_sdk/sysroot.api`][sysroot-api]: The build fails
with instructions to update this file when public sysroot headers change.
### 4. Add tests
Add or update Zircon core tests (for example, under
[`//zircon/system/utest/core/`][core-tests]) that exercise **every** supported
version, not just the newest one. For example, see how
[`vmo.cc`][vmo-test] queries `ZX_INFO_VMO_V1`, `ZX_INFO_VMO_V2`,
`ZX_INFO_VMO_V3`, and `ZX_INFO_VMO`, and checks that the shared fields agree.
Run the core tests with `fx core-tests` and build with `fx build`.
## Source compatibility
The scheme above preserves ABI compatibility for all existing binaries. Source
compatibility depends on the kind of change:
* **Only adding fields:** Code that uses the unversioned names picks up the new
version when recompiled, and usually continues to compile. Code that relies
on the exact size or on aggregate initialization of every field might need
updates.
* **Removing or renaming fields:** This breaks source compatibility for code
that uses those fields. Update all in-tree users in the same CL. Out-of-tree
users can pin to the old `_vN` names until they migrate.
If a source-breaking change affects a lot of out-of-tree code, use the
alias-based soft transition described in RFC-0074 instead:
1. Introduce both `_V1` and `_V2`, with the unversioned names temporarily
aliased to `_V1`.
1. Migrate all callers to `_V2`.
1. Repoint the unversioned aliases to `_V2`.
1. Migrate callers back to the unversioned names.
1. Remove `_V1`.
## Removing an old version
You can remove an old version only after no binary built against a supported
API level can still use it. Binaries that target a retired API level don't need
to be considered. Before you remove it, verify the following:
* No in-tree code references the `_vN_t` struct or `_VN` constant.
* No prebuilt binaries, SDK consumers at a supported API level, or CTF tests
depend on it. If in doubt, ask the Zircon kernel OWNERS.
The removal CL should delete the old struct and constant from the C headers,
the kernel handling, the Rust bindings, the ABI validator entries, the tools'
decoders, the vDSO documentation, and the tests for that version. For an
example, see the CL that removed `ZX_INFO_PROCESS_V1` (commit `934bcf504d3`).
Don't renumber the remaining versions, and don't reuse the removed version
number.
## Process requirements
* Changes to syscall structs are changes to the Zircon system interface and
require `API-Review+1` from an [API Council][api-council] member.
* Adding or removing Zircon system interfaces, or significantly changing their
semantics, might require an RFC. See
[RFC-0006: Addendum to the RFC process for Zircon][rfc-0006].
* Changing the meaning of an existing field in all versions is an exception
that requires explicit justification and API review, as done in RFC-0254.
## Prior art
These RFCs used this scheme:
* [RFC-0074: Expose process start time][rfc-0074] (`zx_info_process_v2_t`)
* [RFC-0084: More per-task runtime information][rfc-0084]
(`zx_info_task_runtime_v1_t`)
* [RFC-0254: Changing attribution for copy-on-write pages][rfc-0254]
(`zx_info_vmo_t`, `zx_info_maps_t`, `zx_info_task_stats_t`)
* [RFC-0262: Expand kmem stats][rfc-0262] (`zx_info_kmem_stats_v1_t`)
For the mechanics of adding a brand new syscall, see
[Adding a new syscall][adding-syscall].
[abi-validator]: /zircon/kernel/tests/abi_type_validator.cc
[adding-syscall]: /docs/development/kernel/adding_a_new_syscall.md
[api-council]: /docs/contribute/governance/api_council.md
[api-evolution]: /docs/development/api/evolution.md
[availability-h]: /zircon/system/public/zircon/availability.h
[clock-h]: /zircon/system/public/zircon/syscalls/clock.h
[core-tests]: /zircon/system/utest/core/
[fidl-codec]: /src/lib/fidl_codec/
[fidlcat]: /tools/fidlcat/
[memory-stats-rs]: /zircon/kernel/vm/memory_stats.rs
[object-h]: /zircon/system/public/zircon/syscalls/object.h
[object-info-rs]: /zircon/kernel/lib/syscalls/object_info.rs
[policy-h]: /zircon/system/public/zircon/syscalls/policy.h
[public-headers]: /zircon/system/public/zircon/
[rfc-0006]: /docs/contribute/governance/rfcs/0006_addendum_to_rfc_process_for_zircon.md
[rfc-0074]: /docs/contribute/governance/rfcs/0074_expose_process_start_time.md
[rfc-0084]: /docs/contribute/governance/rfcs/0084_zircon_more_per_task_runtime_information.md
[rfc-0239]: /docs/contribute/governance/rfcs/0239_platform_versioning_in_practice.md
[rfc-0254]: /docs/contribute/governance/rfcs/0254_changing_attribution_for_copy_on_write_pages.md
[rfc-0262]: /docs/contribute/governance/rfcs/0262_expand_kmem_stats.md
[syscalls-headers]: /zircon/system/public/zircon/syscalls/
[syscalls-next-h]: /zircon/system/public/zircon/syscalls-next.h
[system-rubric]: /docs/development/api/system.md#versioning-structs
[sysroot-api]: /zircon/public/sysroot_sdk/sysroot.api
[vdso]: /zircon/vdso/
[vmo-test]: /zircon/system/utest/core/vmo/vmo.cc
[zx-cpp]: /zircon/system/ulib/zx/
[zx-rust]: /sdk/rust/zx/
[zx-types]: /sdk/rust/zx-types/src/lib.rs
[zxdump]: /src/lib/zxdump/