CTLV compared with the Netlink YAML specs
The kernel already describes Netlink protocols in YAML (Documentation/netlink/specs/, schemas in Documentation/netlink/). CTLV specs look similar on purpose, and differ in ways that follow from CTLV being a character-device transport. This page lines them up. "Netlink" below means the modern genetlink schema, Documentation/netlink/genetlink.yaml; the older genetlink-c, genetlink-legacy and netlink-raw levels add keys on top of it and are mentioned only where they matter.
Common ground
If you know Netlink specs, these will feel familiar.
- A YAML file is the complete description of an interface. A generator produces the UAPI header and kernel-side tables from it, and the kernel validates incoming messages against what the spec declares.
- Messages are trees of typed attributes, with nesting, repetition, integers, strings and binary data.
- Attributes are declared in named collections and referenced by name from operations.
- Enums and flags are declared once and attached to integer attributes.
- Operations come in request/reply, multi-part (dump or query) and unsolicited (notification or event) flavours.
- Names are kebab-case and become upper-case C identifiers.
- Both are validated by a JSON Schema (draft 7) in the tree, with the same
%YAML 1.2 header and SPDX first line, and both use the licence expression ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause) in-tree.
Same interface, two specs
The Netlink handshake family (Documentation/netlink/specs/handshake.yaml) lets a TLS handler daemon accept a pending handshake request. Here is its accept command, abridged, and a CTLV spec for an equivalent invented interface.
Netlink
name: handshake
protocol: genetlink
doc: Netlink protocol to request a transport layer security handshake.
definitions:
-
type: enum
name: handler-class
value-start: 0
entries: [none, tlshd, max]
-
type: enum
name: auth
value-start: 0
entries: [unspec, unauth, psk, x509]
attribute-sets:
-
name: x509
attributes:
- { name: cert, type: s32 }
- { name: privkey, type: s32 }
-
name: accept
attributes:
- { name: sockfd, type: s32 }
- { name: handler-class, type: u32, enum: handler-class }
- { name: timeout, type: u32 }
- { name: auth-mode, type: u32, enum: auth }
- { name: peer-identity, type: u32, multi-attr: true }
- { name: certificate, type: nest, nested-attributes: x509,
multi-attr: true }
-
name: done
attributes:
- name: status
type: u32
checks: { max: max-errno }
operations:
list:
-
name: accept
doc: Handler retrieves next queued handshake request
attribute-set: accept
flags: [admin-perm]
do:
request:
attributes: [handler-class]
reply:
attributes: [sockfd, timeout, auth-mode,
peer-identity, certificate]
CTLV
family:
name: ctlv-handshake
inherits: ctlv
enums:
- name: handler-class
values: [ tlshd ]
- name: auth
values: [ unauth, psk, x509 ]
attr-groups:
- name: handshake
attrs:
- name: sockfd
type: sint
- name: handler-class
enum: handler-class
- name: timeout
type: uint
- name: auth-mode
enum: auth
- name: peer-identity
type: uint
max-occurrences: 8
- name: certificate
attrs: x509
max-occurrences: 4
- name: cert
type: sint
- name: privkey
type: sint
- name: status
type: uint
max: 4095
attr-sets:
- name: x509
attrs: [ handshake.cert, handshake.privkey ]
- name: accept-request
attrs: handshake.handler-class
- name: accept-reply
attrs: [ handshake.sockfd, handshake.timeout, handshake.auth-mode ]
optional-attrs: [ handshake.peer-identity, handshake.certificate ]
ops:
- name: accept
type: action
request:
attrs: accept-request
reply:
attrs: accept-reply
The CTLV spec passes the CTLV validator once registered. Points worth noticing:
- Netlink puts the attribute list for a message on the operation (
request.attributes); CTLV names an attr set and lets the set decide which attributes are required and which are optional.
- Netlink's
multi-attr: true says "may repeat" with no bound. CTLV's max-occurrences states the bound, and the bound feeds the generated message-length limit.
- The Netlink spec requires
doc strings; CTLV specs carry comments instead.
- Netlink's
admin-perm is a capability check. CTLV has no counterpart; access is decided by how the device node was opened (fd-access) and by the node's permissions.
- Netlink's enums begin at 0 unless told otherwise, and carry
unspec entries by convention. CTLV enums always begin at 1 with 0 reserved, so no placeholder is needed.
- The 8-bit, 16-bit and 32-bit choices disappear:
u32 and s32 both become 64-bit uint and sint, narrowed with min/max where needed.
At a glance
| Netlink YAML | CTLV YAML |
| Transport | Netlink sockets | Character device per instance, one ioctl, read() for events |
| Interface identity | Family name; numeric ID allocated at runtime | Family name plus a fixed numeric ID from an append-only registry |
| Reuse across interfaces | Attribute sets, subset-of; no inheritance between families | Single inheritance between families; stubs as extension points |
| Required vs optional attributes | Not expressed in the spec | Expressed per placement (attrs / optional-attrs) and enforced |
| Integer widths | 8, 16, 32, 64 bit, signed and unsigned, byte order selectable | Always 64 bit |
| Numbering | Sequential, overridable with value | Strictly by position; no override |
| Max message length | Not computed | Computed and bounded at generation time |
| Compatibility checking | No snapshot mechanism in the schemas | Checked-in ABI snapshots, checked by the tool |
| Schema variants | Four levels (genetlink, -c, -legacy, netlink-raw) | One |
| Unsolicited messages | Multicast groups | Per-file-description subscriptions and a bounded queue |
Identity and structure
| Topic | Netlink | CTLV |
| Top-level keys | name, doc, protocol, uapi-header, definitions, attribute-sets, operations, mcast-groups, kernel-family | family, enums, flags, attr-groups, attr-sets, ops |
| Required keys | name, doc, attribute-sets, operations | family only |
| Family ID | Allocated dynamically; the name identifies the family | Index in families.yaml; part of every attr and op ID |
| File name | Free | Must equal the family name |
| Inheritance | None. A spec is complete and independent of other specs | One parent. A child name is parent-suffix. The root family ctlv supplies discovery, introspection and event operations to every family |
| Partial reuse | subset-of declares a restricted view of another attribute set in the same spec | Selectors pick any mix of single attrs, whole groups and other sets |
| Extension by others | Not supported by the language | A parent leaves a stub set; one descendant fills it |
| Constants | definitions may declare const (and struct in the legacy level), and may point at an existing C header | Enums and flags only; no constants, no external headers |
| Naming control | name-prefix, enum-name, async-prefix, uapi-header | None; C names are derived from the YAML names, and a collision is an error |
| YAML dialect | Standard YAML validated by the schema | Strict subset: no anchors, aliases, merge keys, explicit tags or duplicate keys |
Attributes
Declaring and using
Both languages separate declaring an attribute from deciding where it appears, but at different granularity.
| Netlink | CTLV |
| Container | attribute-sets: a named space of attributes | attr-groups: a named namespace of attributes (declaration) |
| Message level | An operation names one attribute-set; its request and reply list attribute names from that set | An attr-set lists selectors; an operation names a set (or selectors) for its request and for its reply |
| Nesting | type: nest with nested-attributes naming another set | A nest attr with attrs (and optional-attrs) selectors |
| Attributes from different spaces in one message | Not within one operation, which names one set | Yes; a set can select from any groups |
| Recursion | No dedicated keyword | Explicit recursive: true, query replies only |
| Required attribute | No such concept in the spec | Listed under attrs; absence is a validation error naming the attribute |
| Repetition | multi-attr: true; unbounded | max-occurrences: N or unlimited (query replies only); repeated occurrences must be adjacent in a request |
| Arrays | indexed-array, sub-type | Repeat the attribute |
| Documentation | doc on nearly everything, required on the family and each operation | YAML comments |
Numbering
| Netlink | CTLV |
| Attribute number | Position within the set, overridable with value; the number is local to the attribute set | Position within the group; globally unique 64-bit ID combining family, group and attribute |
| Operation number | Position, overridable with value | Position; globally unique ID combining family and operation |
| Enum start | value-start, default 0 | Always 1 |
| Flag bits | From the starting bit given by value-start | From bit 0, at most 64 |
| Reserved slots | unused and pad attribute types | None |
CTLV's absence of value overrides means a CTLV spec file is its own numbering: moving or deleting a declaration changes IDs, which the ABI check reports.
Type mapping
| Netlink | CTLV | Notes |
u8, u16, u32, u64 | uint | CTLV is 64-bit on the wire; use max to state the real range. |
s8 … s64 | sint | Same. |
uint, sint | uint, sint | Netlink's variable-width forms; CTLV's are fixed 64-bit. |
flag (presence means true) | bool (one byte, 0 or 1) | A CTLV bool is always carried with a value. |
string | string | CTLV requires max-length. |
binary | binary | CTLV requires length or max-length. |
nest | attr with attrs | |
type: u32 + enum: name | enum: name | Enum is a primary form in CTLV; there is no separate integer width. |
enum-as-flags, or an enum of flags type | flags: name | Flags are their own definition kind in CTLV. |
byte-order | none | CTLV integers are not byte-order selectable. |
display-hint | none | CTLV has unit and scale for integers instead. |
indexed-array, nest-type-value | none | |
unused, pad | none | CTLV pads implicitly to 8 bytes. |
| none | blob | Describes a buffer in caller memory (direction, backing, maximum length, alignment). Netlink conveys such things as plain integers. |
| none | unit (seconds, hertz, bytes, celsius), scale | Machine-readable meaning of an integer. |
Validation constraints
Netlink checks | CTLV |
min, max (integer, constant name, or limit such as u8-max) | min, max on the attribute; integer literals only |
min-len, max-len, exact-len (integer or defined constant) | min-length, max-length, length on the attribute; integer literals only |
flags-mask (names a flags constant) | Implied by flags: name |
| Kernel policy generated from the spec | Kernel schema generated from the spec |
dont-validate: [strict, dump, dump-strict] | None; validation is not switchable per operation |
| Unknown attributes tolerated in non-strict mode | Rejected, unless the sender set CTLV_ATTR_F_IGNORE_IF_UNKNOWN on the attribute |
| No ordering rule between repeated attributes | Occurrences of one attribute must be contiguous |
| Message size left to the handler | Maximum request, reply and event sizes are computed by the generator and checked against 1 MiB limits |
| Errors reported through extended ACK | Errors reported in the reply buffer with a reason, message and offsets (error group of the root family) |
Operations
| Topic | Netlink | CTLV |
| Operation kinds | One command may offer do, dump, or both, and be tied to a notification | Each op has exactly one type: action, query or event |
| Request/reply shape | Attribute-name lists under do.request, do.reply, dump.request, dump.reply | request and reply placements (attrs, optional-attrs) |
| Multi-part results | Dump: a series of messages terminated by NLMSG_DONE | Query: one reply returned in fragments, resumable by cursor, with a generation check for staleness |
| Permissions | flags: [admin-perm, uns-admin-perm] (capability checks) | fd-access: read | read-write (how the descriptor was opened); default read-write |
| Hooks | pre and post function names per do / dump | A family-wide pre/post pair, plus a per-op pair with pre-post: true; implemented by name convention, not named in the spec |
| Conditional presence | config-cond (kernel config option) | optional: true (family opts in per device at init) and availability: dynamic (enable and disable at run time) |
| Introspection of what exists | Policy dump via the nlctrl family | The families-dump, ops-dump and op-schema-get operations of the root family, which report the effective per-device schema including disabled ops |
Events and notifications
| Netlink | CTLV |
| Declaration | A command with notify (reuse another command's reply type) or event (explicit attribute list), assigned an mcgrp | An op with type: event and its own reply |
| Delivery group | mcast-groups; a socket joins a group | No groups. Each open file description subscribes to individual events (subscriptions-add) and has its own queue |
| Overflow | Socket buffer overrun | A bounded queue (event-queue-set), with an event-overflow record reporting a sequence range and dropped count |
| Device removal | None in the language | device-removed event in the root family |
Evolution and tooling
| Netlink | CTLV |
| Generated outputs | UAPI header, kernel policy and operation tables, and user-space code, by ynl_gen_c.py from markers in the target files | UAPI header, kernel header and kernel source, by tools/ctlv from a manifest (generated-files.yaml) |
| User-space support | The YNL Python CLI and generated C user libraries read the same specs | The generator has UAPI and kernel modes only; user space discovers the schema at run time through the root family's introspection operations |
| Registry | None; family names are resolved by the kernel at run time | Append-only families.yaml; position is the ID; deprecated entries keep their slot |
| Compatibility enforcement | Regeneration keeps generated files in sync; no snapshot of the interface itself | Per-family ABI snapshot and a registry baseline; narrowing a bound, making a placement required, removing or renumbering anything fails the check |
| Schema levels | genetlink for new families; genetlink-c, genetlink-legacy, netlink-raw for older protocols with quirks (fixed headers, structs, sub-messages, raw protocols) | One schema; there are no pre-existing protocols to describe |
Key-by-key map
Closest equivalents. A dash means there is no counterpart.
| Netlink | CTLV |
name | family.name |
doc | — |
protocol | — |
uapi-header | generated-files.yaml outputs.uapi-header |
definitions type: enum | enums |
definitions type: flags | flags |
definitions type: const | — |
value-start, entries | values (fixed start) |
render-max, header, scope | — |
attribute-sets entry | attr-groups entry (declaration) plus attr-sets entry (use) |
subset-of | Selecting a subset of a group in an attr-set |
name-prefix, enum-name | — |
attribute type | attribute type, enum, flags, attrs or blob |
value (attribute or op) | — |
multi-attr | max-occurrences |
nested-attributes | attrs, optional-attrs |
checks.min, checks.max | min, max |
checks.min-len, max-len, exact-len | min-length, max-length, length |
checks.flags-mask | implied by flags |
byte-order, display-hint | — (unit, scale serve a different purpose) |
operations.list | ops |
do | type: action |
dump | type: query |
notify, event, mcgrp | type: event with reply |
attribute-set (on an operation) | —; request / reply select any sets |
request.attributes, reply.attributes | request.attrs, reply.attrs and the optional-attrs forms |
flags: [admin-perm] | fd-access (different mechanism) |
pre, post | pre-post |
config-cond | optional / availability (per device, not per build) |
dont-validate | — |
mcast-groups | — (subscriptions) |
kernel-family | family.pre-init, post-init, pre-fini (lifecycle callbacks) |
| — | family.inherits, stub, recursive, blob, unit, scale, suggested-reply-length, availability, optional |
Reading across
Most differences trace back to two facts.
- CTLV owns the whole stack for its devices. It therefore pins identity (registry), computes and bounds every message size, rejects what the schema does not place, and checks its own history. Netlink specs describe a protocol that mostly already exists in C and has to accommodate legacy, so they offer more escape hatches (
value, byte order, dont-validate, header and constant imports, four schema levels) and fewer guarantees.
- CTLV devices form families that extend each other. Inheritance, stubs and the root family's introspection exist for that. A Netlink family stands alone.
Porting an interface from a Netlink spec to CTLV therefore mostly means collapsing integer widths to uint/sint with ranges, turning each operation's attribute list into an attr set with explicit required and optional members, replacing multi-attr with a bound, replacing multicast groups with event operations, and deciding what each operation's fd-access should be.