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/ (or mirrored in //sdk/rust/zx-types), read this guide first.
Structs that are used only by syscalls annotated with @next (declared in zircon/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.
The syscall ABI is a contract between the kernel and every binary built against a supported API level, 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:
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 and the Fuchsia platform API evolution guidelines.
Any of these changes silently break an existing binary:
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.Renaming a field without changing its layout or meaning is ABI-compatible but breaks source compatibility. See Source compatibility.
The versioning scheme is guided by these principles:
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.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.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:
zx_info_vmo_v3_t) and keep supporting it in the kernel.zx_info_vmo_t) refer to the new layout.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 | 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 (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 | 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.
A few syscalls take or return structs but have no version channel:
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.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.
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 requires this.
Follow the existing convention in object.h:
zx_info_vmo_t, ZX_INFO_VMO) always refer to the newest version. Most code should use these._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, ...).zx_info_maps_v2_t contains zx_info_maps_mapping_v2_t). See Nested and shared structs.ZX_INFO_PROCESS is still __ZX_INFO_TOPIC(3u, 1u) even though ZX_INFO_PROCESS_V1 was removed.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:
So when you change a struct, the version bump propagates upward to every root that can reach it:
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:
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.
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:
zx_exception_report_t, returned by zx_object_get_info() with ZX_INFO_THREAD_EXCEPTION_REPORT.context argument of zx_thread_raise_exception().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:
_vN struct explicitly, so its layout is unchanged.If any affected root has no version channel, see Syscalls without a version channel.
New struct versions must follow the general rules for syscall structs:
uint32_t, uint64_t, zx_koid_t, etc.).uint8_t padding1[4];. Don't rely on implicit compiler padding.uint32_t or uint64_t over bool fields.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.
In the appropriate header under //zircon/system/public/zircon/syscalls/:
_vN name (for example, zx_info_kmem_stats_v1_t). The layout must be byte-for-byte identical to the struct that exists today._VN topic or constant for that old struct with the existing numeric value of the unversioned topic. Old binaries keep sending this value.zx_info_kmem_stats_t) to the new layout.__ZX_INFO_TOPIC(17u, 0) to __ZX_INFO_TOPIC(17u, 1)).For example, this is how ZX_INFO_KMEM_STATS was versioned:
-#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]
The kernel must accept every supported version.
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 and the vmo_info_to_version and MapsInfoVersion helpers in memory_stats.rs.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.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: 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 and //zircon/system/ulib/zx: Update high-level wrappers that expose the struct.//zircon/vdso/*.fidl: Update the syscall reference documentation in the doc comments, including documentation for the older versions.//zircon/kernel/tests/abi_type_validator.cc: Add size, alignment, and field offset checks for the new struct and keep the checks for the old versions.//tools/fidlcat, //src/lib/fidl_codec, and //src/lib/zxdump.//zircon/public/sysroot_sdk/sysroot.api: The build fails with instructions to update this file when public sysroot headers change.Add or update Zircon core tests (for example, under //zircon/system/utest/core/) that exercise every supported version, not just the newest one. For example, see how vmo.cc 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.
The scheme above preserves ABI compatibility for all existing binaries. Source compatibility depends on the kind of change:
_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:
_V1 and _V2, with the unversioned names temporarily aliased to _V1._V2._V2._V1.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:
_vN_t struct or _VN constant.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.
API-Review+1 from an API Council member.These RFCs used this scheme:
zx_info_process_v2_t)zx_info_task_runtime_v1_t)zx_info_vmo_t, zx_info_maps_t, zx_info_task_stats_t)zx_info_kmem_stats_v1_t)For the mechanics of adding a brand new syscall, see Adding a new syscall.