* [PATCH RFC v2 00/15] ctlv: Add a schema-driven TLV uAPI for character devices
@ 2026-11-13 15:54 Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 01/15] ctlv: Add the schema language and generator Jiri Pirko
` (14 more replies)
0 siblings, 15 replies; 24+ messages in thread
From: Jiri Pirko @ 2026-11-13 15:54 UTC (permalink / raw)
To: linux-coco
Cc: linux-kernel, linux-api, x86, kvm, linux-doc, jgg, david.quigley,
lukas, ksinyuk, rodrigo.vivi, cedric.xing, sameo, drdeeglaze,
James.Bottomley, aik, jpiotrowski, qinkun, pgonda, jic23,
alistair.francis, ankita, suzuki.poulose, kas, rick.p.edgecombe,
elena.reshetova, dave.hansen, sathyanarayanan.kuppuswamy,
peter.fang, jarkko, zohar, yeoreum.yun, aneesh.kumar, dave.jiang,
nik.borisov, sergii.dmytruk
From: Jiri Pirko <jiri@nvidia.com>
RFC v1 [1] posted CTLV together with its first user, the attestation
subsystem. v2 posts them as two series: this one, and the attestation
series, which applies on top of it and is posted alongside as
[PATCH RFC v2 0/9] attestation: Add a common subsystem and TDX support
CTLV is a schema-driven TLV uAPI on a character device, meant for uAPIs
too unconstrained for fixed structures: lists of records, nested and
variable-sized data, optional fields. Every request is checked against
the family's YAML schema before the driver sees it, and the UAPI
header, the bindings and the docs are generated from the same schema.
A family grows without versions, by new ops and optional fields. One
ioctl over a global number space lets strace or a seccomp supervisor
decode any family, and a query whose answer is larger than the
caller's buffer is answered in parts, with a cursor and a generation
that keep them consistent.
An ioctl with structures of its own fixes their layout, needs pointers
to more structures for variable data, and has to be validated and
decoded on its own. sysfs and configfs would assemble one request
through several files, where CTLV checks and executes it at once.
Netlink is built around sockets and network namespaces, while these
devices want an fd and the DAC model of a device node, and it was
turned down for the TSM's SPDM uAPI [2]. For a value or two, sysfs or a
plain ioctl stays simpler, and CTLV is not meant for that.
The uAPI is one ioctl on the device's fd:
struct ctlv_op_exec {
__u32 size;
__u32 flags;
__aligned_u64 op_id;
__aligned_u64 request;
__aligned_u64 reply;
__u32 request_len;
__u32 reply_len;
__aligned_u64 generation;
__aligned_u64 cursor;
};
ioctl(fd, CTLV_IOC_OP_EXEC, &exec);
An op is an action, executed once, or a query, which the generation
and the cursor carry across replies. Each op needs read or read-write
access, checked against the mode the device was opened with. A failed
op returns an errno, and in the reply buffer why the request was
refused and the offset of the offending TLV.
A family is specified in YAML. The root family, which every device
answers, lets a program not compiled against a family discover it. Its
first op:
ops:
# The families this device answers, this one first and the device family
# last. The only names any reply of this family carries.
family-get:
type: query
fd-access: read
reply:
# One entry per family of the chain.
families[]:
# The family ID, which bits 63..48 of the family's types carry.
id: uint
# The registry name, also what a uevent carries. Bounded here, this
# being the only place a message carries a family name.
name: string(..63)
ctlvgen --signatures prints a family's ops as calls, ? marking what
may be absent and [] what may repeat. The root family's three, with
the purpose of each:
# The device's family chain, root first.
family-get() -> { families[]: { id, name } }
# Every op the device publishes, with its type, access and lengths.
op-get() ->
{ ops[]: { id, type, availability?, disabled?, request-max-len?,
action-reply-max-len?, query-reply-min-len?,
query-reply-suggested-len?, fd-access?, error-info-max-len? } }
# The tree of one op's request and reply.
op-schema-get(id) -> { request?: schema-node, reply?: schema-node }
libctlv is a small C library for the mechanical parts: opening a
device by its family, building requests, parsing replies, reassembling
queries and building blob descriptors, with typed bindings generated
per family. ctlvctl needs no generated code, only the family's
specification, or the .json the build writes from it, for the names.
Both build with the kernel's CROSS_COMPILE, LLVM and O=. Here they
run in a VM, on a device of the simulation provider from the branch
[3], with CTLV_SPEC_DIR pointing at Documentation/ctlv/specs. The
device's MR 1 can be extended, and its evidence object 2 is an event
log:
# Every CTLV device and its family chain.
$ ctlvctl list
attestation/attestation0 ctlv/ctlv-attestation/ctlv-attestation-sim
# The ops the device publishes.
$ ctlvctl ops attestation/attestation0
# ctlv/ctlv-attestation/ctlv-attestation-sim, generation 0
family-get query read fixed request 16 reply 112..65536
op-get query read fixed request 16 reply 56..65536
op-schema-get query read fixed request 40 reply 168..65536
...
mr-read query read fixed request 80 reply 112..4096
mr-extend action read-write fixed request 7232 reply 2824
...
# mr-read's request and reply.
$ ctlvctl schema attestation/attestation0 mr-read
mr-read (query)
request:
root record
mr-read id required uint [0, 4294967295]
device-generation optional uint [0, 4294967295]
mr-read options optional record
reply:
root record
device-generation required uint [0, 4294967295]
mr-generation required uint [0, 4294967295]
mr-values required record x23
mr-value.hash-algo required enum [0, 22] {HASH_ALGO_MD4, ...}
mr-value.data required binary len [16, 64]
# MR 1's banks.
$ ctlvctl exec attestation/attestation0 mr-read id=1
# mr-read, 1 fragment, generation 0
device-generation 0
mr-generation 2
mr-values
mr-value.hash-algo HASH_ALGO_SHA256
mr-value.data hex:0000000000000000000000000000000000000000000000000000000000000000
mr-values
mr-value.hash-algo HASH_ALGO_SHA384
mr-value.data hex:000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
# Extend MR 1 by a digest for each of its banks.
$ ctlvctl exec attestation/attestation0 mr-extend --json '{"id":1,
"mr-values":[
{"hash-algo":"HASH_ALGO_SHA256","data":"hex:b1a3c4fc2b10743a5f79ce443178df6e99b4426fcae2ef3e1f76a6a26d8f0860"},
{"hash-algo":"HASH_ALGO_SHA384","data":"hex:c8a6d5e64f03a5f3cad5125670fccff0c065a9db91f00a81eb6630836629a85969802308430aa28ea0d6fdd8b1eb0cc8"}]}'
# mr-extend, 1 fragment, generation 0
device-generation 0
mr-generation 3
mr-values
mr-value.hash-algo HASH_ALGO_SHA256
mr-value.data hex:b1a3c4fc2b10743a5f79ce443178df6e99b4426fcae2ef3e1f76a6a26d8f0860
mr-values
mr-value.hash-algo HASH_ALGO_SHA384
mr-value.data hex:c8a6d5e64f03a5f3cad5125670fccff0c065a9db91f00a81eb6630836629a85969802308430aa28ea0d6fdd8b1eb0cc8
# MR 1 again, with its new value and generation.
$ ctlvctl exec attestation/attestation0 mr-read id=1
# mr-read, 1 fragment, generation 1
device-generation 0
mr-generation 3
mr-values
mr-value.hash-algo HASH_ALGO_SHA256
mr-value.data hex:a4fb91fd25115ec617cc4f17a40590e685cc58de5a65bb119e821e09d780a158
mr-values
mr-value.hash-algo HASH_ALGO_SHA384
mr-value.data hex:1dcbf559ca9d6b48d782d268933c07a9e33c96bb21e1b868c2db7be929a852d3c5dbc5cf44427400c268a2f557c38c75
# The event log, an output blob, written to a file.
$ ctlvctl exec attestation/attestation0 evidence-get --json \
'{"objects":[{"id":2,"data":{"file":"log.bin"}}]}'
# evidence-get, 1 fragment, generation 0
device-generation 0
evidence-get evidence-generation 1
evidence-get results
evidence-get results.format ccel
evidence-get results.length 65
There are no versions. struct ctlv_op_exec carries its size and grows
only at its end. A family grows by new ops and new optional fields,
and anything incompatible is a new op. Each family has a checked-in
ABI snapshot, and regen refuses a change that breaks it.
Events, a per-fd queue of what changed on a device, are left out until
their first user is posted, and so are memfd and dma-buf blobs. Both
are on the branch [3], with the generator's tests, KUnit, test
families and selftests.
The series adds 23787 lines:
generator, Python 7864
core, C 5643
ctlvctl 3519
libctlv 3242
docs 1510
generated files 1161
meta-schemas, registry 368
root family specification 211
ABI snapshots 118
build 151
Changes since RFC v1, each patch listing its own:
- Specs are written as Jason suggested: enums, flags, records and ops,
each op the tree of its request and reply, every type derived from
where its field sits. Attribute groups and sets are gone. The
comment above an item is its doc, in the UAPI headers and the
rendered docs, and ctlvgen --signatures prints the ops as above.
- TLV, type, record and field replace attr and nest.
- The events are left out, with read() and poll().
- A subsystem allocates its device and embeds CTLV, as fwctl does, and
may route the ioctl from its own file operations. The engine is in
lib/ctlv/, and a subsystem's class gets its nodes from CTLV, and
there is no ctlv class.
- struct ctlv_op_exec is sized by its size field and read with
copy_struct_from_user(). A reply buffer may be any length, and an
output blob too short fails with EMSGSIZE and the length it needs.
- Enum extension points: an enum whose values a descendant family
gives.
- An op that reads is <object>-get: families-dump and ops-dump are
family-get and op-get.
- Inline messages are at most 64 KiB, and records nest at most 8 deep.
- The UAPI headers are in include/uapi/ctlv/.
- regen writes the ABI snapshots too, and refuses an ABI break unless
ABI_BREAK=1.
- The userspace ABI opens with the goals, a worked example and a
glossary, and collects the compatibility rules. The family guide is
built around a worked example family.
- libctlv and ctlvctl are posted. ctlvctl runs every op with exec,
takes a blob in --json as a file, as Aneesh's patched copy did, and
sizes an output blob by the length the op fails with.
The patches:
1-3 the spec language and generator, the message format and the
family API, and the root family
4 the userspace ABI
5-11 messages, blobs, validation and introspection, then dispatch
12 character device nodes
13 how to write a family
14-15 libctlv and ctlvctl
Note to reviewers: the uAPI, in 1, 2 and 4, and the family API, in 2
and 13, matter most at this stage.
[1] https://lore.kernel.org/all/20261015145053.3664509-1-jiri@resnulli.us/
[2] https://lore.kernel.org/all/20260921131911.532d81f4@kernel.org/
[3] https://github.com/jpirko/linux_mlxsw/commits/wip_attestation_all_2
Jiri Pirko (15):
ctlv: Add the schema language and generator
ctlv: Add the UAPI message format and family API
ctlv: Add the root family specification and generated files
Documentation: ctlv: Add userspace ABI
ctlv: Prepare message input handling
ctlv: Prepare blob descriptor validation
ctlv: Prepare schema validation
ctlv: Prepare message output handling
ctlv: Prepare VA blob handling
ctlv: Prepare runtime introspection
ctlv: Add operation dispatch
ctlv: Add character device nodes
Documentation: ctlv: Describe how to write a family
tools/ctlv: Add the userspace library
tools/ctlv: Add ctlvctl
Documentation/ctlv/abi/ctlv.yaml | 111 +
Documentation/ctlv/abi/families.registry.yaml | 7 +
Documentation/ctlv/ctlv-registry-schema.yaml | 38 +
Documentation/ctlv/ctlv-schema.yaml | 227 ++
Documentation/ctlv/families.yaml | 17 +
.../ctlv/generated-files-schema.yaml | 68 +
Documentation/ctlv/generated-files.yaml | 18 +
Documentation/ctlv/specs/ctlv.yaml | 211 ++
Documentation/driver-api/ctlv.rst | 723 +++++
Documentation/driver-api/index.rst | 1 +
Documentation/userspace-api/ctlv.rst | 783 +++++
Documentation/userspace-api/index.rst | 1 +
.../userspace-api/ioctl/ioctl-number.rst | 2 +
MAINTAINERS | 12 +
include/linux/ctlv.h | 149 +
include/linux/ctlv_device.h | 57 +
include/linux/ctlv_family.h | 652 ++++
include/linux/ctlv_gen.h | 154 +
include/uapi/ctlv/ctlv.h | 180 ++
include/uapi/ctlv/ctlv_families_gen.h | 17 +
include/uapi/ctlv/ctlv_gen.h | 278 ++
lib/Kconfig | 2 +
lib/Makefile | 2 +
lib/ctlv/Kconfig | 13 +
lib/ctlv/Makefile | 5 +
lib/ctlv/blob.c | 421 +++
lib/ctlv/blob.h | 15 +
lib/ctlv/blob_desc.c | 101 +
lib/ctlv/blob_desc.h | 29 +
lib/ctlv/builder.c | 455 +++
lib/ctlv/builder.h | 14 +
lib/ctlv/core.c | 1169 +++++++
lib/ctlv/ctlv_gen.c | 161 +
lib/ctlv/device.c | 179 ++
lib/ctlv/internal.h | 87 +
lib/ctlv/introspection.c | 505 +++
lib/ctlv/message.c | 393 +++
lib/ctlv/message.h | 91 +
lib/ctlv/schema.c | 1092 +++++++
lib/ctlv/schema.h | 54 +
tools/Makefile | 9 +-
tools/ctlv/Makefile | 62 +
tools/ctlv/ctlv-regen.sh | 64 +
tools/ctlv/ctlvctl/.gitignore | 4 +
tools/ctlv/ctlvctl/Makefile | 64 +
tools/ctlv/ctlvctl/ctlvctl.c | 2787 +++++++++++++++++
tools/ctlv/ctlvctl/spec.c | 672 ++++
tools/ctlv/ctlvctl/spec.h | 60 +
tools/ctlv/ctlvgen/__init__.py | 1 +
tools/ctlv/ctlvgen/cli.py | 178 ++
tools/ctlv/ctlvgen/lib/__init__.py | 1 +
tools/ctlv/ctlvgen/lib/cenum.py | 178 ++
tools/ctlv/ctlvgen/lib/cgen.py | 763 +++++
tools/ctlv/ctlvgen/lib/compat.py | 877 ++++++
tools/ctlv/ctlvgen/lib/dump.py | 161 +
tools/ctlv/ctlvgen/lib/errors.py | 16 +
tools/ctlv/ctlvgen/lib/gen_doc.py | 684 ++++
tools/ctlv/ctlvgen/lib/gen_family.py | 285 ++
tools/ctlv/ctlvgen/lib/gen_kernel.py | 576 ++++
tools/ctlv/ctlvgen/lib/gen_runtime.py | 66 +
tools/ctlv/ctlvgen/lib/gen_uapi.py | 182 ++
tools/ctlv/ctlvgen/lib/gen_user.py | 808 +++++
tools/ctlv/ctlvgen/lib/outputs.py | 363 +++
tools/ctlv/ctlvgen/lib/semantics.py | 1219 +++++++
tools/ctlv/ctlvgen/lib/sizes.py | 239 ++
tools/ctlv/ctlvgen/lib/spec.py | 813 +++++
tools/ctlv/ctlvgen/lib/tree.py | 158 +
tools/ctlv/ctlvgen/lib/yamlload.py | 138 +
tools/ctlv/libctlv/Makefile | 41 +
tools/ctlv/libctlv/ctlv.h | 413 +++
tools/ctlv/libctlv/ctlv_user_gen.c | 551 ++++
tools/ctlv/libctlv/device.c | 412 +++
tools/ctlv/libctlv/internal.h | 161 +
tools/ctlv/libctlv/introspection.c | 238 ++
tools/ctlv/libctlv/message.c | 518 +++
tools/ctlv/libctlv/schema.c | 742 +++++
tools/ctlv/libctlv/spec.c | 186 ++
tools/ctlv/libctlv/transport.c | 572 ++++
tools/ctlv/pyproject.toml | 30 +
tools/ctlv/requirements.txt | 2 +
80 files changed, 23787 insertions(+), 1 deletion(-)
create mode 100644 Documentation/ctlv/abi/ctlv.yaml
create mode 100644 Documentation/ctlv/abi/families.registry.yaml
create mode 100644 Documentation/ctlv/ctlv-registry-schema.yaml
create mode 100644 Documentation/ctlv/ctlv-schema.yaml
create mode 100644 Documentation/ctlv/families.yaml
create mode 100644 Documentation/ctlv/generated-files-schema.yaml
create mode 100644 Documentation/ctlv/generated-files.yaml
create mode 100644 Documentation/ctlv/specs/ctlv.yaml
create mode 100644 Documentation/driver-api/ctlv.rst
create mode 100644 Documentation/userspace-api/ctlv.rst
create mode 100644 include/linux/ctlv.h
create mode 100644 include/linux/ctlv_device.h
create mode 100644 include/linux/ctlv_family.h
create mode 100644 include/linux/ctlv_gen.h
create mode 100644 include/uapi/ctlv/ctlv.h
create mode 100644 include/uapi/ctlv/ctlv_families_gen.h
create mode 100644 include/uapi/ctlv/ctlv_gen.h
create mode 100644 lib/ctlv/Kconfig
create mode 100644 lib/ctlv/Makefile
create mode 100644 lib/ctlv/blob.c
create mode 100644 lib/ctlv/blob.h
create mode 100644 lib/ctlv/blob_desc.c
create mode 100644 lib/ctlv/blob_desc.h
create mode 100644 lib/ctlv/builder.c
create mode 100644 lib/ctlv/builder.h
create mode 100644 lib/ctlv/core.c
create mode 100644 lib/ctlv/ctlv_gen.c
create mode 100644 lib/ctlv/device.c
create mode 100644 lib/ctlv/internal.h
create mode 100644 lib/ctlv/introspection.c
create mode 100644 lib/ctlv/message.c
create mode 100644 lib/ctlv/message.h
create mode 100644 lib/ctlv/schema.c
create mode 100644 lib/ctlv/schema.h
create mode 100644 tools/ctlv/Makefile
create mode 100755 tools/ctlv/ctlv-regen.sh
create mode 100644 tools/ctlv/ctlvctl/.gitignore
create mode 100644 tools/ctlv/ctlvctl/Makefile
create mode 100644 tools/ctlv/ctlvctl/ctlvctl.c
create mode 100644 tools/ctlv/ctlvctl/spec.c
create mode 100644 tools/ctlv/ctlvctl/spec.h
create mode 100644 tools/ctlv/ctlvgen/__init__.py
create mode 100755 tools/ctlv/ctlvgen/cli.py
create mode 100644 tools/ctlv/ctlvgen/lib/__init__.py
create mode 100644 tools/ctlv/ctlvgen/lib/cenum.py
create mode 100644 tools/ctlv/ctlvgen/lib/cgen.py
create mode 100644 tools/ctlv/ctlvgen/lib/compat.py
create mode 100644 tools/ctlv/ctlvgen/lib/dump.py
create mode 100644 tools/ctlv/ctlvgen/lib/errors.py
create mode 100644 tools/ctlv/ctlvgen/lib/gen_doc.py
create mode 100644 tools/ctlv/ctlvgen/lib/gen_family.py
create mode 100644 tools/ctlv/ctlvgen/lib/gen_kernel.py
create mode 100644 tools/ctlv/ctlvgen/lib/gen_runtime.py
create mode 100644 tools/ctlv/ctlvgen/lib/gen_uapi.py
create mode 100644 tools/ctlv/ctlvgen/lib/gen_user.py
create mode 100644 tools/ctlv/ctlvgen/lib/outputs.py
create mode 100644 tools/ctlv/ctlvgen/lib/semantics.py
create mode 100644 tools/ctlv/ctlvgen/lib/sizes.py
create mode 100644 tools/ctlv/ctlvgen/lib/spec.py
create mode 100644 tools/ctlv/ctlvgen/lib/tree.py
create mode 100644 tools/ctlv/ctlvgen/lib/yamlload.py
create mode 100644 tools/ctlv/libctlv/Makefile
create mode 100644 tools/ctlv/libctlv/ctlv.h
create mode 100644 tools/ctlv/libctlv/ctlv_user_gen.c
create mode 100644 tools/ctlv/libctlv/device.c
create mode 100644 tools/ctlv/libctlv/internal.h
create mode 100644 tools/ctlv/libctlv/introspection.c
create mode 100644 tools/ctlv/libctlv/message.c
create mode 100644 tools/ctlv/libctlv/schema.c
create mode 100644 tools/ctlv/libctlv/spec.c
create mode 100644 tools/ctlv/libctlv/transport.c
create mode 100644 tools/ctlv/pyproject.toml
create mode 100644 tools/ctlv/requirements.txt
--
2.56.0
^ permalink raw reply [flat|nested] 24+ messages in thread
* [PATCH RFC v2 01/15] ctlv: Add the schema language and generator
2026-11-13 15:54 [PATCH RFC v2 00/15] ctlv: Add a schema-driven TLV uAPI for character devices Jiri Pirko
@ 2026-11-13 15:55 ` Jiri Pirko
2026-11-13 16:23 ` sashiko-bot
2026-11-13 15:55 ` [PATCH RFC v2 02/15] ctlv: Add the UAPI message format and family API Jiri Pirko
` (13 subsequent siblings)
14 siblings, 1 reply; 24+ messages in thread
From: Jiri Pirko @ 2026-11-13 15:55 UTC (permalink / raw)
To: linux-coco
Cc: linux-kernel, linux-api, x86, kvm, linux-doc, jgg, david.quigley,
lukas, ksinyuk, rodrigo.vivi, cedric.xing, sameo, drdeeglaze,
James.Bottomley, aik, jpiotrowski, qinkun, pgonda, jic23,
alistair.francis, ankita, suzuki.poulose, kas, rick.p.edgecombe,
elena.reshetova, dave.hansen, sathyanarayanan.kuppuswamy,
peter.fang, jarkko, zohar, yeoreum.yun, aneesh.kumar, dave.jiang,
nik.borisov, sergii.dmytruk
From: Jiri Pirko <jiri@nvidia.com>
Define the YAML schema and append-only family registry. Add ctlvgen to
validate specifications and emit kernel and UAPI outputs.
Assisted-by: Cursor: GPT-5.6 Sol
Assisted-by: Cursor: Claude Opus 5.5
Signed-off-by: Jiri Pirko <jiri@nvidia.com>
---
RFC->RFCv2:
- dropped the family priv and its lifecycle hooks, handing callbacks
the CTLV instance
- fixed the ABI check to accept max-occurrences widened to unlimited
- made a --mode and file kind pair with no generator a usage error
- stated the attr form rules in the meta-schema
- computed each op's error information length
- generated no reply lengths into user headers
- checked the limits against <linux/ctlv_family.h>
- lowered the inline message limit to 64 KiB
- lowered the record depth limit to 8
- stopped reserving the callback typedef names
- dropped the event op type
- renamed stubs to extension points
- let an enum take its values from a C header
- made abi-check report a changed attr kind, not fail on its bounds
- left an optional blob below the root out of a reply
- wrote each op's request and reply as a tree of fields, naming a
record only where it is reused
- derived every type from where its field sits, so a spec carries no
numbers and the ABI snapshot only checks them
- named the spec in every generated file, and gave each field and op
one constant
- said TLV, type, record and field instead of attr
- said record instead of nest, the root being one
- said node, field or placed record instead of placement
- numbered every enum member and flag bit in the generated headers
- generated the UAPI headers into include/uapi/ctlv/
- type-checked ctlvgen with mypy --strict in make lint
- required a comment on every spec item, each enum member and flag
- printed each enum member's comment in the UAPI headers
- put each field's doc above its generated helpers
- folded abi-update into regen, refusing a break of the committed ABI
unless ABI_BREAK=1, and validate, abi-check and check-regen into all
- added enum extension points, whose values a descendant family gives
- said message instead of wire
- let a count name an enum, one entry per value
- let an integer in bytes take K, M and G
- kept a hyphenated name whole when wrapping a generated comment
---
Documentation/ctlv/ctlv-registry-schema.yaml | 38 +
Documentation/ctlv/ctlv-schema.yaml | 227 +++
.../ctlv/generated-files-schema.yaml | 64 +
MAINTAINERS | 7 +
tools/ctlv/Makefile | 39 +
tools/ctlv/ctlv-regen.sh | 64 +
tools/ctlv/ctlvgen/__init__.py | 1 +
tools/ctlv/ctlvgen/cli.py | 137 ++
tools/ctlv/ctlvgen/lib/__init__.py | 1 +
tools/ctlv/ctlvgen/lib/cenum.py | 178 +++
tools/ctlv/ctlvgen/lib/cgen.py | 763 +++++++++++
tools/ctlv/ctlvgen/lib/compat.py | 877 ++++++++++++
tools/ctlv/ctlvgen/lib/errors.py | 16 +
tools/ctlv/ctlvgen/lib/gen_family.py | 285 ++++
tools/ctlv/ctlvgen/lib/gen_kernel.py | 576 ++++++++
tools/ctlv/ctlvgen/lib/gen_uapi.py | 182 +++
tools/ctlv/ctlvgen/lib/outputs.py | 355 +++++
tools/ctlv/ctlvgen/lib/semantics.py | 1219 +++++++++++++++++
tools/ctlv/ctlvgen/lib/sizes.py | 239 ++++
tools/ctlv/ctlvgen/lib/spec.py | 813 +++++++++++
tools/ctlv/ctlvgen/lib/tree.py | 158 +++
tools/ctlv/ctlvgen/lib/yamlload.py | 138 ++
tools/ctlv/pyproject.toml | 30 +
tools/ctlv/requirements.txt | 2 +
24 files changed, 6409 insertions(+)
create mode 100644 Documentation/ctlv/ctlv-registry-schema.yaml
create mode 100644 Documentation/ctlv/ctlv-schema.yaml
create mode 100644 Documentation/ctlv/generated-files-schema.yaml
create mode 100644 tools/ctlv/Makefile
create mode 100755 tools/ctlv/ctlv-regen.sh
create mode 100644 tools/ctlv/ctlvgen/__init__.py
create mode 100755 tools/ctlv/ctlvgen/cli.py
create mode 100644 tools/ctlv/ctlvgen/lib/__init__.py
create mode 100644 tools/ctlv/ctlvgen/lib/cenum.py
create mode 100644 tools/ctlv/ctlvgen/lib/cgen.py
create mode 100644 tools/ctlv/ctlvgen/lib/compat.py
create mode 100644 tools/ctlv/ctlvgen/lib/errors.py
create mode 100644 tools/ctlv/ctlvgen/lib/gen_family.py
create mode 100644 tools/ctlv/ctlvgen/lib/gen_kernel.py
create mode 100644 tools/ctlv/ctlvgen/lib/gen_uapi.py
create mode 100644 tools/ctlv/ctlvgen/lib/outputs.py
create mode 100644 tools/ctlv/ctlvgen/lib/semantics.py
create mode 100644 tools/ctlv/ctlvgen/lib/sizes.py
create mode 100644 tools/ctlv/ctlvgen/lib/spec.py
create mode 100644 tools/ctlv/ctlvgen/lib/tree.py
create mode 100644 tools/ctlv/ctlvgen/lib/yamlload.py
create mode 100644 tools/ctlv/pyproject.toml
create mode 100644 tools/ctlv/requirements.txt
diff --git a/Documentation/ctlv/ctlv-registry-schema.yaml b/Documentation/ctlv/ctlv-registry-schema.yaml
new file mode 100644
index 000000000000..d9a2840fc230
--- /dev/null
+++ b/Documentation/ctlv/ctlv-registry-schema.yaml
@@ -0,0 +1,38 @@
+# SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause)
+%YAML 1.2
+---
+$id: http://kernel.org/schemas/ctlv/ctlv-registry-schema.yaml#
+$schema: https://json-schema.org/draft-07/schema
+
+$defs:
+ name:
+ type: string
+ maxLength: 63
+ pattern: ^[a-z0-9]+(-[a-z0-9]+)*$
+
+title: CTLV family registry
+description: Append-only list of CTLV family names, indexed by family ID
+type: object
+required: [ families ]
+additionalProperties: false
+properties:
+ families:
+ description: |
+ Family names in family ID order: the entry at index N has family ID N.
+ The root family owns ID 0 and so is the first entry, and cannot be
+ deprecated. Every other entry comes after the entry of the family it
+ inherits. A deprecated entry keeps its ID reserved.
+ type: array
+ minItems: 1
+ maxItems: 65536
+ items:
+ oneOf:
+ - $ref: '#/$defs/name'
+ - type: object
+ required: [ name, deprecated ]
+ additionalProperties: false
+ properties:
+ name:
+ $ref: '#/$defs/name'
+ deprecated:
+ const: true
diff --git a/Documentation/ctlv/ctlv-schema.yaml b/Documentation/ctlv/ctlv-schema.yaml
new file mode 100644
index 000000000000..c61d8bffc0c7
--- /dev/null
+++ b/Documentation/ctlv/ctlv-schema.yaml
@@ -0,0 +1,227 @@
+# SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause)
+%YAML 1.2
+---
+$id: http://kernel.org/schemas/ctlv/ctlv-schema.yaml#
+$schema: https://json-schema.org/draft-07/schema
+
+# The syntax gate for a CTLV family specification: permitted keys and their
+# shapes. A field's schema kind and its bounds are parsed, and anything needing
+# more than one declaration at a time is checked, by tools/ctlv/ctlvgen/lib/.
+
+$defs:
+ name:
+ description: Kebab-case identifier.
+ type: string
+ pattern: ^[a-z0-9]+(-[a-z0-9]+)*$
+
+ c-name:
+ description: C identifier.
+ type: string
+ pattern: ^[A-Za-z_][A-Za-z0-9_]*$
+
+ family-name:
+ description: Name carried by family discovery.
+ allOf:
+ - $ref: '#/$defs/name'
+ - maxLength: 63
+
+ field-key:
+ description:
+ A field's name, then [N] for 1 to N occurrences, [enum] for one per
+ value of an enum, or [] for any number, which only a query reply can
+ hold, then ? when it may be absent.
+ type: string
+ pattern: ^[a-z0-9]+(-[a-z0-9]+)*(\[([0-9]*|[a-z0-9]+(-[a-z0-9]+)*)\])?\??$
+
+ include:
+ description:
+ A named record whose fields are spliced in with their IDs, all of them
+ optional when the name ends in ?.
+ type: string
+ pattern: ^[a-z0-9]+(-[a-z0-9]+)*\??$
+
+ field:
+ description:
+ A schema kind with its bounds, an enum or flags by name, "record X" for
+ a field holding named record X, "extension X" for a record a descendant
+ family fills, bare "record" for a field of records:, or an indented
+ map for an unnamed record.
+ oneOf:
+ - type: string
+ minLength: 1
+ - $ref: '#/$defs/record'
+
+ record:
+ description: Fields in the order they are numbered.
+ type: object
+ minProperties: 1
+ propertyNames:
+ anyOf:
+ - $ref: '#/$defs/field-key'
+ - const: $include
+ properties:
+ $include:
+ oneOf:
+ - $ref: '#/$defs/include'
+ - type: array
+ minItems: 1
+ items:
+ $ref: '#/$defs/include'
+ additionalProperties:
+ $ref: '#/$defs/field'
+
+ message:
+ description:
+ Omitted, empty or {} for no fields, "record X" to use named record X,
+ or the record written in place.
+ oneOf:
+ - type: 'null'
+ - type: object
+ maxProperties: 0
+ - type: string
+ pattern: ^record [a-z0-9]+(-[a-z0-9]+)*$
+ - $ref: '#/$defs/record'
+
+ value-list:
+ type: array
+ minItems: 1
+ items:
+ $ref: '#/$defs/name'
+
+ enum:
+ description:
+ Value names in declaration order, numbered from 1 because 0 is
+ reserved, and append-only. A $header enum is enum $enum of
+ include/uapi/<$header>, whose constants are its values, under their
+ own names and numbers; with $end, only those before that constant.
+ "extension" is an enum extension point, whose values one descendant
+ family gives.
+ oneOf:
+ - $ref: '#/$defs/value-list'
+ - type: object
+ required: [ $header, $enum ]
+ additionalProperties: false
+ properties:
+ $header:
+ type: string
+ pattern: ^[a-z0-9_-]+(/[a-z0-9_-]+)*\.h$
+ $enum:
+ $ref: '#/$defs/c-name'
+ $end:
+ $ref: '#/$defs/c-name'
+ - const: extension
+
+ flags:
+ description: Bit names, numbered from bit 0 and append-only.
+ allOf:
+ - $ref: '#/$defs/value-list'
+ - maxItems: 64
+
+ op:
+ type: object
+ required: [ type ]
+ additionalProperties: false
+ properties:
+ type:
+ description:
+ action is invoked once and never fragments, and query may fragment
+ its reply.
+ enum: [ action, query ]
+ fd-access:
+ description:
+ Authority needed to execute an action or query. read permits an
+ observation fd; read-write, the default, requires a full-control fd.
+ enum: [ read, read-write ]
+ availability:
+ description: >
+ Whether a family may enable and disable this op per device. fixed,
+ the default, is always enabled. dynamic is a permission rather than
+ a state: every device starts with the op enabled.
+ enum: [ fixed, dynamic ]
+ optional:
+ description:
+ Whether family initialization must opt in before publication for
+ this op to exist on the device. False by default. An op not opted
+ into is absent from introspection.
+ type: boolean
+ pre-post:
+ description: >
+ Whether this op gets a generated pre/post hook pair of its own,
+ which its declaring family implements. It runs inside the pair of
+ the device family, or the core family for a core op.
+ False by default. Kernel implementation metadata: it reaches
+ neither UAPI nor op-schema introspection.
+ type: boolean
+ request:
+ description: An op's request.
+ $ref: '#/$defs/message'
+ reply:
+ description:
+ Required on a query. Omitted on an action means ACK-only.
+ $ref: '#/$defs/message'
+ suggested-reply-length:
+ description:
+ Query only, in bytes. An omitted value is 64K raised to the
+ computed minimum. A declared value must be between that minimum
+ and the kernel's inline message limit.
+ oneOf:
+ - type: integer
+ minimum: 1
+ - type: string
+ pattern: ^[0-9]+[KMG]$
+
+title: CTLV family
+description: Specification of one CTLV family schema
+type: object
+required: [ family ]
+additionalProperties: false
+properties:
+ family:
+ description:
+ Registered family name. It carries no numeric ID; the ID comes from
+ the position in Documentation/ctlv/families.yaml.
+ $ref: '#/$defs/family-name'
+ inherits:
+ description:
+ Name of the single direct parent family, which every family but the
+ root one names. The complete parent schema is inherited and its
+ definitions keep the parent's IDs. The name above must be this name,
+ one dash, and a local suffix.
+ $ref: '#/$defs/family-name'
+ enums:
+ description:
+ A family that inherits may also fill an inherited enum extension point
+ here, by declaring an enum of the same name with its values.
+ type: object
+ minProperties: 1
+ propertyNames:
+ $ref: '#/$defs/name'
+ additionalProperties:
+ $ref: '#/$defs/enum'
+ flags:
+ type: object
+ minProperties: 1
+ propertyNames:
+ $ref: '#/$defs/name'
+ additionalProperties:
+ $ref: '#/$defs/flags'
+ records:
+ description:
+ Named records, which an op's message, "record X" or $include uses, and
+ fields of records:, written with their schema kind, which bare "record"
+ places. A family that inherits may also fill an inherited extension
+ point here, by declaring a record of the same name.
+ type: object
+ minProperties: 1
+ propertyNames:
+ $ref: '#/$defs/field-key'
+ additionalProperties:
+ $ref: '#/$defs/field'
+ ops:
+ description: Numbered from 1 in the order listed, and append-only.
+ type: object
+ minProperties: 1
+ propertyNames:
+ $ref: '#/$defs/name'
+ additionalProperties:
+ $ref: '#/$defs/op'
diff --git a/Documentation/ctlv/generated-files-schema.yaml b/Documentation/ctlv/generated-files-schema.yaml
new file mode 100644
index 000000000000..d3529ce6e8e7
--- /dev/null
+++ b/Documentation/ctlv/generated-files-schema.yaml
@@ -0,0 +1,64 @@
+# SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause)
+%YAML 1.2
+---
+$schema: https://json-schema.org/draft-07/schema
+
+$defs:
+ path:
+ type: string
+ minLength: 1
+ pattern: ^[^/].*$
+
+ outputs:
+ type: object
+ minProperties: 1
+ additionalProperties: false
+ properties:
+ uapi-header:
+ $ref: '#/$defs/path'
+ kernel-header:
+ $ref: '#/$defs/path'
+ kernel-source:
+ $ref: '#/$defs/path'
+
+ registry:
+ type: object
+ required: [ source, outputs ]
+ additionalProperties: false
+ properties:
+ source:
+ $ref: '#/$defs/path'
+ outputs:
+ type: object
+ required: [ uapi-header ]
+ additionalProperties: false
+ properties:
+ uapi-header:
+ $ref: '#/$defs/path'
+
+ family:
+ type: object
+ required: [ name, source, outputs ]
+ additionalProperties: false
+ properties:
+ name:
+ type: string
+ source:
+ $ref: '#/$defs/path'
+ outputs:
+ $ref: '#/$defs/outputs'
+
+title: CTLV generated-file manifest
+type: object
+required: [ version, registry, families ]
+additionalProperties: false
+properties:
+ version:
+ const: 1
+ registry:
+ $ref: '#/$defs/registry'
+ families:
+ type: array
+ minItems: 1
+ items:
+ $ref: '#/$defs/family'
diff --git a/MAINTAINERS b/MAINTAINERS
index 93abbb315065..5c8e7ad9b599 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -7183,6 +7183,13 @@ M: Jaya Kumar <jayakumar.alsa@gmail.com>
S: Maintained
F: sound/pci/cs5535audio/
+CTLV SUBSYSTEM
+M: Jiri Pirko <jiri@resnulli.us>
+L: linux-kernel@vger.kernel.org
+S: Maintained
+F: Documentation/ctlv/
+F: tools/ctlv/
+
CTU CAN FD DRIVER
M: Pavel Pisa <pisa@cmp.felk.cvut.cz>
M: Ondrej Ille <ondrej.ille@gmail.com>
diff --git a/tools/ctlv/Makefile b/tools/ctlv/Makefile
new file mode 100644
index 000000000000..bdec011595aa
--- /dev/null
+++ b/tools/ctlv/Makefile
@@ -0,0 +1,39 @@
+# SPDX-License-Identifier: GPL-2.0
+
+KDIR = ../..
+DOCDIR = $(KDIR)/Documentation/ctlv
+
+# Writes nothing. Fails on an invalid specification, and on any generated file
+# or ABI snapshot that regen would change.
+all:
+ @./ctlv-regen.sh -c
+
+# Rewrites every generated file and ABI snapshot, and names each one it writes.
+# A specification change that breaks the committed ABI is refused unless
+# ABI_BREAK=1 says it is meant.
+regen:
+ @./ctlv-regen.sh $(if $(filter 1,$(ABI_BREAK)),-b)
+
+# Style and type checks, needing yamllint, pycodestyle, pyflakes, and mypy
+# with types-PyYAML and types-jsonschema, which the tool itself does not use.
+#
+# A flow sequence is written "[ one, two ]" here, as in the tree's other
+# meta-schemas, which stock yamllint rejects; the same relaxation is in
+# Documentation/devicetree/bindings/.yamllint.
+YAMLLINT_CONF = {extends: default, rules: {brackets: {max-spaces-inside: 1}}}
+
+PYSRC = ctlvgen
+
+lint:
+ yamllint -d "$(YAMLLINT_CONF)" $(DOCDIR)
+ python3 -m pycodestyle $(PYSRC)
+ python3 -m pyflakes $(PYSRC)
+ python3 -m mypy $(PYSRC)
+
+clean:
+ rm -rf ctlvgen/__pycache__
+ rm -rf ctlvgen/lib/__pycache__
+ rm -rf ctlvgen.egg-info
+ rm -rf build
+
+.PHONY: all regen lint clean
diff --git a/tools/ctlv/ctlv-regen.sh b/tools/ctlv/ctlv-regen.sh
new file mode 100755
index 000000000000..1fdf5f49a8db
--- /dev/null
+++ b/tools/ctlv/ctlv-regen.sh
@@ -0,0 +1,64 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+#
+# Reconcile the checked-in files against the independent output manifests,
+# and the ABI snapshots against the specifications.
+#
+# -b write even a change that breaks the committed ABI
+# -f accepted for compatibility; reconciliation always renders everything
+# -c do not write anything, fail if any output differs from what the
+# generator produces now
+# -p search the given directory instead of the kernel tree
+
+TOOL=$(dirname "$(realpath "$0")")/ctlvgen/cli.py
+abi_break=
+check=
+search=
+
+while getopts ':bfcp:' option; do
+ case "$option" in
+ b)
+ abi_break=yes
+ ;;
+ f)
+ ;;
+ c)
+ check=yes
+ ;;
+ p)
+ if [ -z "$OPTARG" ]; then
+ echo "Option -p needs a nonempty directory" >&2
+ exit 1
+ fi
+ search=$OPTARG
+ ;;
+ :)
+ echo "Option -$OPTARG needs an argument" >&2
+ exit 1
+ ;;
+ ?)
+ echo "Unrecognized option '-$OPTARG'" >&2
+ exit 1
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+if [ "$#" -ne 0 ]; then
+ echo "Unexpected argument '$1'" >&2
+ exit 1
+fi
+
+KDIR=$(realpath "$(dirname "$0")/../..")
+args=(--spec "$KDIR/Documentation/ctlv/specs/ctlv.yaml")
+if [ -n "$search" ]; then
+ args+=(--output-root "$search")
+fi
+if [ -n "$check" ]; then
+ args+=(--outputs-check)
+else
+ args+=(--outputs-update)
+fi
+if [ -n "$abi_break" ]; then
+ args+=(--abi-break)
+fi
+exec "$TOOL" "${args[@]}"
diff --git a/tools/ctlv/ctlvgen/__init__.py b/tools/ctlv/ctlvgen/__init__.py
new file mode 100644
index 000000000000..f84069cb2c3d
--- /dev/null
+++ b/tools/ctlv/ctlvgen/__init__.py
@@ -0,0 +1 @@
+# SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
diff --git a/tools/ctlv/ctlvgen/cli.py b/tools/ctlv/ctlvgen/cli.py
new file mode 100755
index 000000000000..78e09cfd620e
--- /dev/null
+++ b/tools/ctlv/ctlvgen/cli.py
@@ -0,0 +1,137 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+"""Command line front end of the CTLV schema tool."""
+
+from __future__ import annotations
+
+import argparse
+import pathlib
+import sys
+from collections.abc import Callable
+
+# So that this file works both as a module of the ctlvgen package and as the
+# script the Makefile and ctlv-regen.sh run. The imports below have to follow.
+sys.path.append(pathlib.Path(__file__).resolve().parents[1].as_posix())
+
+from ctlvgen.lib import cgen # noqa: E402
+from ctlvgen.lib import gen_kernel # noqa: E402
+from ctlvgen.lib import gen_uapi # noqa: E402
+from ctlvgen.lib import semantics # noqa: E402
+from ctlvgen.lib import spec # noqa: E402
+from ctlvgen.lib import tree # noqa: E402
+from ctlvgen.lib.errors import CtlvError # noqa: E402
+
+
+# What each --mode and file generates.
+GENERATORS: dict[tuple[str, str], Callable[..., str]] = {
+ ('uapi', 'header'): gen_uapi.render,
+ ('kernel', 'header'): gen_kernel.render_header,
+ ('kernel', 'source'): gen_kernel.render_source,
+}
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description='CTLV schema tool')
+ parser.add_argument('--spec', type=str, required=True,
+ help='path to a CTLV family specification')
+ parser.add_argument('--schema', type=str,
+ help='path to the meta-schema, derived from --spec '
+ 'when not given')
+ parser.add_argument('--registry', type=str,
+ help='path to the family registry, derived from '
+ '--spec when not given')
+ action = parser.add_mutually_exclusive_group(required=True)
+ action.add_argument('--validate', action='store_true',
+ help='validate the specification and exit')
+ action.add_argument('--outputs-check', action='store_true',
+ help='check every file in the output manifests and '
+ 'every ABI snapshot')
+ action.add_argument('--outputs-update', action='store_true',
+ help='regenerate every file in the output manifests '
+ 'and every ABI snapshot')
+ action.add_argument('--mode',
+ choices=('uapi', 'kernel'),
+ help='generate the files of this mode')
+ parser.add_argument('--schema-only', action='store_true',
+ help='omit the family API from kernel output')
+ files = parser.add_mutually_exclusive_group()
+ files.add_argument('--header', action='store_true',
+ help='generate the header of --mode')
+ files.add_argument('--source', action='store_true',
+ help='generate the source of --mode')
+ parser.add_argument('-o', '--output', type=str,
+ help='write to this file instead of standard output')
+ parser.add_argument('--cmp-out', action='store_true',
+ help='leave the output file alone when its contents '
+ 'would not change')
+ parser.add_argument('--output-root', type=str,
+ help='alternate destination root for generated files')
+ parser.add_argument('--abi-break', action='store_true',
+ help='let --outputs-update break the committed ABI')
+ args = parser.parse_args()
+
+ file = next((name for name in ('header', 'source')
+ if getattr(args, name)), None)
+ if args.mode and not file:
+ parser.error('--mode needs --header or --source')
+ if file and not args.mode:
+ parser.error('--header and --source need --mode')
+ if args.mode and (args.mode, file) not in GENERATORS:
+ parser.error(f'--mode {args.mode} cannot generate --{file}')
+ if args.schema_only and args.mode != 'kernel':
+ parser.error('--schema-only needs --mode kernel')
+ output_action = args.outputs_check or args.outputs_update
+ if args.output is not None and not args.mode:
+ parser.error('-o/--output needs --mode')
+ if args.cmp_out and (not args.mode or args.output is None):
+ parser.error('--cmp-out needs --mode and --output')
+ if args.output_root is not None:
+ if not output_action:
+ parser.error('--output-root needs an outputs action')
+ if not args.output_root:
+ parser.error('--output-root cannot be empty')
+ if args.abi_break and not args.outputs_update:
+ parser.error('--abi-break needs --outputs-update')
+
+ paths = spec.Paths(args.spec, schema=args.schema, registry=args.registry)
+ try:
+ if args.outputs_check:
+ tree.check(paths, args.output_root)
+ return 0
+ elif args.outputs_update:
+ for item in tree.update(paths, args.output_root, args.abi_break):
+ print(f'ctlvgen: {item}', file=sys.stderr)
+ return 0
+ family = semantics.load(paths)
+ if args.validate:
+ cgen.symbols(family)
+ else:
+ assert file is not None
+ _generate(args, file, family)
+ except CtlvError as exc:
+ print(f'ctlvgen: {exc}', file=sys.stderr)
+ return 1
+ return 0
+
+
+def _generate(args: argparse.Namespace, file: str,
+ family: semantics.Family) -> None:
+ render = GENERATORS[(args.mode, file)]
+ if args.mode == 'kernel':
+ text = render(family, family_api=not args.schema_only)
+ else:
+ text = render(family)
+ if args.output is None:
+ sys.stdout.write(text)
+ return
+ path = pathlib.Path(args.output)
+ # Leaving an unchanged file untouched keeps its timestamp, which is what
+ # ctlv-regen.sh compares against the specification.
+ if args.cmp_out and path.exists() and \
+ path.read_text(encoding='utf-8') == text:
+ return
+ path.write_text(text, encoding='utf-8')
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/tools/ctlv/ctlvgen/lib/__init__.py b/tools/ctlv/ctlvgen/lib/__init__.py
new file mode 100644
index 000000000000..f84069cb2c3d
--- /dev/null
+++ b/tools/ctlv/ctlvgen/lib/__init__.py
@@ -0,0 +1 @@
+# SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
diff --git a/tools/ctlv/ctlvgen/lib/cenum.py b/tools/ctlv/ctlvgen/lib/cenum.py
new file mode 100644
index 000000000000..0b3b67d335f4
--- /dev/null
+++ b/tools/ctlv/ctlvgen/lib/cenum.py
@@ -0,0 +1,178 @@
+# SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+"""The constants of an enum in a uapi header.
+
+Only what a uapi enum needs is understood: integer literals, the enum's
+earlier constants, parentheses and integer operators. Anything else, a macro
+included, is refused rather than guessed at.
+"""
+
+from __future__ import annotations
+
+import pathlib
+import re
+from typing import NoReturn
+
+from .errors import CtlvError
+
+_COMMENT = re.compile(r'/\*.*?\*/|//[^\n]*', re.S)
+_TOKEN = re.compile(r'\s*(?:(?P<number>0[xX][0-9a-fA-F]+|[0-9]+)[uUlL]*|'
+ r'(?P<name>[A-Za-z_]\w*)|(?P<op><<|>>|[-+*/%&|^~()]))')
+_IDENT = re.compile(r'[A-Za-z_]\w*')
+
+# The binary operators by C precedence, loosest first.
+_BINARY: tuple[tuple[str, ...], ...] = (
+ ('|',), ('^',), ('&',), ('<<', '>>'), ('+', '-'), ('*', '/', '%'))
+
+
+def read(where: str, include: pathlib.Path, header: str, tag: str,
+ end: str | None) -> tuple[tuple[str, int], ...]:
+ """Return enum tag of include/header, as constants before end."""
+ try:
+ text = (include / header).read_text(encoding='utf-8')
+ except OSError:
+ raise CtlvError(where, f'<{header}> is not a uapi header') from None
+ return parse(where, text, f'<{header}>', tag, end)
+
+
+def parse(where: str, text: str, header: str, tag: str,
+ end: str | None) -> tuple[tuple[str, int], ...]:
+ """Return enum tag of header text, as constants before end."""
+ found = re.search(rf'\benum\s+{re.escape(tag)}\s*{{([^}}]*)}}',
+ _COMMENT.sub(' ', text))
+ if found is None:
+ raise CtlvError(where, f'{header} has no enum {tag}')
+ constants: list[tuple[str, int]] = []
+ numbers: dict[str, int] = {}
+ number = -1
+ for item in _items(found.group(1)):
+ name, equals, value = (part.strip() for part in item.partition('='))
+ if not _IDENT.fullmatch(name):
+ raise CtlvError(where, f'enum {tag} has {item!r}, which is not a '
+ 'constant')
+ if equals:
+ number = _Expression(where, tag, name, value, numbers).value()
+ else:
+ number += 1
+ if number < 0:
+ raise CtlvError(where, f'{name} of enum {tag} is {number}, a '
+ 'negative value')
+ constants.append((name, number))
+ numbers[name] = number
+ if end is None:
+ return tuple(constants)
+ if end not in numbers:
+ raise CtlvError(where, f'enum {tag} has no {end}')
+ kept = tuple(constants[:[name for name, _ in constants].index(end)])
+ if not kept:
+ raise CtlvError(where, f'enum {tag} has no constant before {end}')
+ for name, number in kept:
+ if number >= numbers[end]:
+ raise CtlvError(where, f'{name} of enum {tag} is {number}, not '
+ f'below {end}')
+ return kept
+
+
+def _items(body: str) -> list[str]:
+ """The enumerators of an enum body, split at the commas outside ()."""
+ items: list[str] = []
+ depth = start = 0
+ for index, char in enumerate(body):
+ depth += {'(': 1, ')': -1}.get(char, 0)
+ if char == ',' and depth == 0:
+ items.append(body[start:index])
+ start = index + 1
+ items.append(body[start:])
+ return [item.strip() for item in items if item.strip()]
+
+
+class _Expression:
+ """One constant's initializer, evaluated as C evaluates it."""
+
+ def __init__(self, where: str, tag: str, name: str, text: str,
+ numbers: dict[str, int]) -> None:
+ self.what = f'{name} of enum {tag}'
+ self.where = where
+ self.tag = tag
+ self.numbers = numbers
+ self.tokens: list[tuple[str, str]] = []
+ position = 0
+ text = text.rstrip()
+ while position < len(text):
+ found = _TOKEN.match(text, position)
+ if found is None or found.lastgroup is None:
+ self._fail(f'cannot read {text!r}')
+ self.tokens.append((found.lastgroup,
+ found.group(found.lastgroup)))
+ position = found.end()
+ self.position = 0
+
+ def _fail(self, problem: str) -> NoReturn:
+ raise CtlvError(self.where, f'{self.what}: {problem}')
+
+ def value(self) -> int:
+ result = self._binary(0)
+ if self.position != len(self.tokens):
+ self._fail(f'cannot read {self.tokens[self.position][1]!r}')
+ return result
+
+ def _peek(self) -> str | None:
+ if self.position == len(self.tokens):
+ return None
+ return self.tokens[self.position][1]
+
+ def _next(self) -> tuple[str, str]:
+ if self.position == len(self.tokens):
+ self._fail('the value ends too soon')
+ self.position += 1
+ return self.tokens[self.position - 1]
+
+ def _binary(self, level: int) -> int:
+ if level == len(_BINARY):
+ return self._unary()
+ left = self._binary(level + 1)
+ while self._peek() in _BINARY[level]:
+ operator = self._next()[1]
+ left = self._apply(operator, left, self._binary(level + 1))
+ return left
+
+ def _unary(self) -> int:
+ kind, token = self._next()
+ if token in ('-', '+', '~'):
+ operand = self._unary()
+ return {'-': -operand, '+': operand, '~': ~operand}[token]
+ if token == '(':
+ inner = self._binary(0)
+ if self._next()[1] != ')':
+ self._fail('a ( is not closed')
+ return inner
+ if kind == 'number':
+ if len(token) > 1 and token[0] == '0' and token[1] not in 'xX':
+ return int(token, 8)
+ return int(token, 0)
+ if kind == 'name' and token in self.numbers:
+ return self.numbers[token]
+ if kind == 'name':
+ self._fail(f'{token} is not an earlier constant of enum '
+ f'{self.tag}')
+ self._fail(f'cannot read {token!r}')
+
+ def _apply(self, operator: str, left: int, right: int) -> int:
+ if operator in ('/', '%'):
+ if right == 0:
+ self._fail('divides by zero')
+ quotient = abs(left) // abs(right)
+ if (left < 0) != (right < 0):
+ quotient = -quotient
+ return quotient if operator == '/' else left - right * quotient
+ if operator in ('<<', '>>') and right < 0:
+ self._fail('shifts by a negative count')
+ return {
+ '|': lambda: left | right,
+ '^': lambda: left ^ right,
+ '&': lambda: left & right,
+ '<<': lambda: left << right,
+ '>>': lambda: left >> right,
+ '+': lambda: left + right,
+ '-': lambda: left - right,
+ '*': lambda: left * right,
+ }[operator]()
diff --git a/tools/ctlv/ctlvgen/lib/cgen.py b/tools/ctlv/ctlvgen/lib/cgen.py
new file mode 100644
index 000000000000..4fa7bd4c3735
--- /dev/null
+++ b/tools/ctlv/ctlvgen/lib/cgen.py
@@ -0,0 +1,763 @@
+# SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+"""C output scaffolding shared by the CTLV generators.
+
+The preamble ctlv-regen.sh recovers a file's specification and mode from, which
+is why the specification stays directly above the marker.
+"""
+
+from __future__ import annotations
+
+import os
+import pathlib
+import textwrap
+from collections.abc import Callable
+
+from . import semantics
+from . import spec
+from .errors import CtlvError
+
+REGEN: str = 'tools/ctlv/ctlv-regen.sh'
+
+LINE_LIMIT: int = 100
+HARD_LINE_LIMIT: int = 100
+TAB_WIDTH: int = 8
+
+# " * " plus the text has to stay inside the line limit.
+COMMENT_WIDTH: int = LINE_LIMIT - 3
+
+# C spelling of each schema kind. A test checks these against
+# enum ctlv_schema_kind.
+KINDS: dict[str, str] = {
+ 'uint': 'CTLV_SCHEMA_UINT',
+ 'sint': 'CTLV_SCHEMA_SINT',
+ 'bool': 'CTLV_SCHEMA_BOOL',
+ 'string': 'CTLV_SCHEMA_STRING',
+ 'binary': 'CTLV_SCHEMA_BINARY',
+ 'enum': 'CTLV_SCHEMA_ENUM',
+ 'flags': 'CTLV_SCHEMA_FLAGS',
+ 'record': 'CTLV_SCHEMA_RECORD',
+ 'blob': 'CTLV_SCHEMA_BLOB',
+}
+
+LIFECYCLE_API_SYMBOLS: tuple[str, ...] = (
+ 'ctlv_init',
+ 'ctlv_publish',
+ 'ctlv_unpublish',
+ 'ctlv_fini',
+ 'ctlv_uevent',
+ 'ctlv_family_of',
+ 'ctlv_op_provide',
+ 'ctlv_op_state_set',
+ 'ctlv_device_class_register',
+ 'ctlv_device_class_unregister',
+ 'ctlv_device_init',
+ 'ctlv_device_fini',
+ 'ctlv_device_add',
+ 'ctlv_device_del',
+)
+
+
+def width(line: str) -> int:
+ """Return the display width of one line, a tab being eight columns."""
+ return len(line.expandtabs(TAB_WIDTH))
+
+
+def ident(*names: str) -> str:
+ """Return the lower case C spelling of one or more schema names."""
+ return '_'.join(name.replace('-', '_') for name in names)
+
+
+def macro(*names: str) -> str:
+ """Return the upper case C spelling of one or more schema names."""
+ return ident(*names).upper()
+
+
+def enum_tag(*names: str) -> str:
+ return f'_{ident(*names)}'
+
+
+def header_bounds(definition: semantics.Definition) -> tuple[str, str]:
+ """A header enum's lowest constant and $end - 1, or its highest.
+
+ A number in a gap between constants is in range, for the family to refuse.
+ """
+ assert definition.header is not None
+ low = min(definition.values, key=lambda value: value[1])[0]
+ if definition.header.end:
+ return low, f'{definition.header.end} - 1'
+ return low, max(definition.values, key=lambda value: value[1])[0]
+
+
+def family_suffix(family_name: str) -> str:
+ suffix = family_name
+ if family_name.startswith('ctlv-'):
+ suffix = family_name.removeprefix('ctlv-')
+ return macro(suffix)
+
+
+def family_const(family_name: str) -> str:
+ if family_name == 'ctlv':
+ return 'CTLV_FAMILY_ROOT'
+ return f'CTLV_FAMILY_{family_suffix(family_name)}'
+
+
+def uapi_header(family_name: str, in_kernel: bool = False) -> str:
+ """Return the include of the generated UAPI header of family_name."""
+ prefix = 'uapi/' if in_kernel else ''
+ return f'<{prefix}ctlv/{ident(family_name)}_gen.h>'
+
+
+def families_uapi_header(in_kernel: bool = False) -> str:
+ prefix = 'uapi/' if in_kernel else ''
+ return f'<{prefix}ctlv/ctlv_families_gen.h>'
+
+
+def kernel_header(family_name: str) -> str:
+ """Return the include of the generated kernel header of family_name.
+
+ The root family's interface is installed beside <linux/ctlv.h>; any other
+ family's is private to the family implementation compiling it.
+ """
+ name = f'{ident(family_name)}_gen.h'
+ if family_name == spec.CORE_FAMILY_NAME:
+ return f'<linux/{name}>'
+ return f'"{name}"'
+
+
+def spec_path(path: pathlib.Path | str) -> str:
+ """Return path relative to the kernel tree that holds it.
+
+ ctlv-regen.sh resolves it from the tree root, so it must not depend on how
+ the generator was invoked. Outside a kernel tree it is recorded as given.
+ """
+ full = os.path.abspath(path)
+ relative = ''
+ while True:
+ relative = os.path.join(os.path.basename(full), relative)
+ parent = os.path.dirname(full)
+ if os.path.exists(os.path.join(parent, 'MAINTAINERS')):
+ return relative[:-1]
+ if parent == full:
+ return str(path)
+ full = parent
+
+
+def wrapped(opening: str, items: tuple[str, ...], closing: str,
+ trailer: str = '', adaptive: bool = False) -> list[str]:
+ """Return the lines of one parenthesized list, wrapped at the limit.
+
+ trailer is what a continued line ends with beyond the item: a backslash
+ inside a macro definition, nothing elsewhere.
+ """
+ parts = [f'{item}, ' for item in items[:-1]] + [items[-1] + closing]
+ column = width(opening)
+ part_width = max(width(part.rstrip() + trailer) for part in parts)
+ indent_column = column
+ if adaptive:
+ indent_column = min(column, max(0, LINE_LIMIT - part_width))
+ indent = '\t' * (indent_column // TAB_WIDTH) + \
+ ' ' * (indent_column % TAB_WIDTH)
+ lines = []
+ line = opening
+ for part in parts:
+ if width(line + part.rstrip() + trailer) > LINE_LIMIT:
+ if line != opening:
+ lines.append(line.rstrip())
+ elif adaptive and opening.rstrip():
+ lines.append(opening.rstrip())
+ else:
+ line += part
+ continue
+ line = indent
+ line += part
+ lines.append(line.rstrip())
+ return lines
+
+
+class Writer:
+ """The lines of one generated file."""
+
+ def __init__(self) -> None:
+ self._lines: list[str] = []
+
+ def line(self, text: str = '') -> None:
+ self._lines.append(text)
+
+ def comment(self, *lines: str) -> None:
+ """One comment block, one input line per output line."""
+ if len(lines) == 1:
+ self.line(f'/* {lines[0]} */')
+ return
+ self.line('/*')
+ for text in lines:
+ self.line(f' * {text}'.rstrip())
+ self.line(' */')
+
+ def doc(self, doc: str | None, indent: str = '') -> None:
+ """One declaration's doc, as one comment."""
+ if not doc:
+ return
+ text = ' '.join(doc.split())
+ if width(f'{indent}/* {text} */') <= LINE_LIMIT:
+ self.line(f'{indent}/* {text} */')
+ return
+ self.line(f'{indent}/*')
+ for line in textwrap.wrap(text, COMMENT_WIDTH - width(indent),
+ break_on_hyphens=False):
+ self.line(f'{indent} * {line}')
+ self.line(f'{indent} */')
+
+ def define_block(self, name: str, *lines: str) -> None:
+ """One #define whose value takes more than the one line."""
+ header = f'#define {name} \\'
+ if width(header) > LINE_LIMIT and '(' in name:
+ macro, params = name.removesuffix(')').split('(', 1)
+ for text in wrapped(f'#define {macro}(',
+ tuple(params.split(', ')), ')', ' \\',
+ adaptive=True):
+ self.line(f'{text} \\')
+ else:
+ self.line(header)
+ for text in lines[:-1]:
+ self.line(f'{text} \\')
+ self.line(lines[-1])
+
+ def define(self, name: str, value: str) -> None:
+ """One #define, continued when it would run past the line limit."""
+ text = f'#define {name} {value}'
+ if width(text) <= LINE_LIMIT:
+ self.line(text)
+ return
+ self.define_block(name, f'\t{value}')
+
+ def define_call(self, name: str, function: str, *args: str) -> None:
+ """One #define whose value is a call, wrapped where it has to be."""
+ call = f'{function}({", ".join(args)})'
+ if width(f'#define {name} {call}') <= LINE_LIMIT:
+ self.line(f'#define {name} {call}')
+ return
+ if width(f'\t{call}') <= LINE_LIMIT:
+ self.define_block(name, f'\t{call}')
+ return
+ self.define_block(
+ name, *wrapped(f'\t{function}(', args, ')', ' \\',
+ adaptive=True))
+
+ def declaration(self, prefix: str, declarator: str) -> None:
+ """One declaration, continued when it would run past the limit."""
+ text = f'{prefix} {declarator}'
+ if width(text) <= LINE_LIMIT:
+ self.line(text)
+ return
+ indent = prefix[:len(prefix) - len(prefix.lstrip('\t'))]
+ self.line(prefix)
+ continuation = f'{indent}\t'
+ if width(f'{continuation}{declarator}') > LINE_LIMIT:
+ continuation = '\t'
+ self.line(f'{continuation}{declarator}')
+
+ def parenthesized(self, opening: str, items: tuple[str, ...],
+ closing: str) -> None:
+ """One parenthesized list, wrapped at the line limit."""
+ for text in wrapped(opening, items, closing, adaptive=True):
+ self.line(text)
+
+ def assertion(self, left: str, operator: str, right: int | str) -> None:
+ """One static_assert, broken after its operator when it must be."""
+ opening = 'static_assert('
+ text = f'{opening}{left} {operator} {right});'
+ if width(text) <= LINE_LIMIT:
+ self.line(text)
+ return
+ column = width(opening)
+ indent = '\t' * (column // TAB_WIDTH) + ' ' * (column % TAB_WIDTH)
+ self.line(f'{opening}{left} {operator}')
+ self.line(f'{indent}{right});')
+
+ def signature(self, prefix: str, name: str, params: tuple[str, ...],
+ closing: str = ');') -> None:
+ """One function signature, wrapped at the line limit."""
+ lines = wrapped(f'{prefix} {name}(', params, closing)
+ one_line = f'{prefix} {name}({", ".join(params)}{closing}'
+ if lines[0].endswith('(') and width(one_line) <= HARD_LINE_LIMIT:
+ self.line(one_line)
+ return
+ if max(width(line) for line in lines) <= LINE_LIMIT:
+ for text in lines:
+ self.line(text)
+ return
+ if name.startswith('*'):
+ self.line(f'{prefix} *')
+ name = name.removeprefix('*')
+ else:
+ self.line(prefix)
+ declarator = f'{name}({", ".join(params)}{closing}'
+ if width(declarator) <= HARD_LINE_LIMIT:
+ self.line(declarator)
+ return
+ opening = f'{name}('
+ lines = wrapped(opening, params, closing)
+ if max(width(line) for line in lines) > LINE_LIMIT:
+ self.line(opening)
+ self.parenthesized('\t', params, closing)
+ else:
+ for text in lines:
+ self.line(text)
+
+ def open_guard(self, guard: str) -> None:
+ self.line(f'#ifndef {guard}')
+ self.line(f'#define {guard}')
+ self.line()
+
+ def close_guard(self, guard: str) -> None:
+ self.line(f'#endif /* {guard} */')
+
+ def includes(self, *headers: str) -> None:
+ """One include block, an empty entry separating two groups."""
+ for header in headers:
+ self.line(f'#include {header}' if header else '')
+ self.line()
+
+ def text(self) -> str:
+ while self._lines and not self._lines[-1]:
+ self._lines.pop()
+ return '\n'.join(self._lines) + '\n'
+
+
+def preamble(origin: semantics.Family | spec.Registry, mode: str, file: str,
+ source: str | None = None) -> Writer:
+ """Return a Writer holding the preamble of one generated file."""
+ writer = Writer()
+ if file == 'source':
+ writer.line(f'// SPDX-License-Identifier: {origin.license}')
+ else:
+ writer.line(f'/* SPDX-License-Identifier: {origin.license} */')
+ writer.line('/*')
+ writer.line(f' * Generated from {spec_path(source or origin.path)} by')
+ writer.line(f' * {REGEN}. Do not edit.')
+ writer.line(' */')
+ writer.line(f'/* CTLV-GEN {mode} {file} */')
+ writer.line()
+ return writer
+
+
+class Names:
+ """The C names of one effective family's declarations.
+
+ A declaration is named after the family that owns it, so an inherited
+ field keeps the name its own family's header gives it.
+ """
+
+ def __init__(self, family: semantics.Family | spec.FamilySpec) -> None:
+ self.family = family
+ self.prefix = macro(family.name)
+ self.ident = ident(family.name)
+
+ @property
+ def effective(self) -> semantics.Family:
+ """The resolved family, which every name but its files' needs."""
+ assert isinstance(self.family, semantics.Family)
+ return self.family
+
+ def owner_prefix(self, family_id: int) -> str:
+ return macro(self.effective.owners[family_id])
+
+ def owner_ident(self, family_id: int) -> str:
+ return ident(self.effective.owners[family_id])
+
+ # Names that a family's UAPI header defines.
+
+ def family_const(self) -> str:
+ return family_const(self.family.name)
+
+ def field(self, field: semantics.Field) -> str:
+ """The type of one field, after the record holding it."""
+ return f'{self.owner_prefix(field.family_id)}_T_{macro(field.c_name)}'
+
+ def field_unit(self, field: semantics.Field) -> str:
+ return f'{self.field(field)}_UNIT'
+
+ def field_scale(self, field: semantics.Field) -> str:
+ return f'{self.field(field)}_SCALE'
+
+ def op_id(self, op: semantics.Op) -> str:
+ return f'{self.owner_prefix(op.family_id)}_OP_{macro(op.name)}'
+
+ def definition(self, definition: semantics.Definition) -> str:
+ """The C type name of an enum definition."""
+ return enum_tag(self.effective.owners[definition.family_id],
+ definition.name)
+
+ def value(self, definition: semantics.Definition, value_name: str) -> str:
+ """One enum value or flag bit."""
+ if definition.header:
+ return value_name
+ return f'{self.owner_prefix(definition.family_id)}_' \
+ f'{macro(definition.name, value_name)}'
+
+ def value_bit(self, definition: semantics.Definition,
+ value_name: str) -> str:
+ return f'{self.value(definition, value_name)}_BIT'
+
+ def bit_enum(self, definition: semantics.Definition) -> str:
+ return enum_tag(self.effective.owners[definition.family_id],
+ definition.name, 'bit')
+
+ def value_mask(self, definition: semantics.Definition) -> str:
+ """The mask of every bit a flags definition declares."""
+ return f'{self.owner_prefix(definition.family_id)}_' \
+ f'{macro(definition.name)}_MASK'
+
+ # Names that a family's kernel metadata defines: its own ops, its
+ # extension points and the ones it fills.
+
+ def extension(self, extension: semantics.Extension) -> str:
+ """The ID one extension point has among its family's."""
+ return f'{self.owner_prefix(extension.family_id)}_EXT_' \
+ f'{macro(extension.name)}'
+
+ def enum_extension(self, extension: semantics.EnumExtension) -> str:
+ """The ID one enum extension point has among its family's."""
+ return f'{self.owner_prefix(extension.family_id)}_ENUM_EXT_' \
+ f'{macro(extension.name)}'
+
+ def enum_fill_schemas(self) -> str:
+ return f'{self.ident}_enum_fill_schemas'
+
+ def op_schemas(self) -> str:
+ return f'{self.ident}_op_schemas'
+
+ def schema(self) -> str:
+ return f'{self.ident}_schema'
+
+ def ext_fill_schemas(self) -> str:
+ return f'{self.ident}_ext_fill_schemas'
+
+ def node_schemas(self, op: semantics.Op, which: str) -> str:
+ # File scope, so no family prefix: an op name is unique within its
+ # effective family.
+ return f'{ident(op.name)}_{which}_node_schemas'
+
+ def ext_schemas(self, op: semantics.Op, which: str) -> str:
+ return f'{ident(op.name)}_{which}_ext_schemas'
+
+ def fill_node_schemas(self, fill: semantics.Fill) -> str:
+ return f'{ident(fill.name)}_fill_node_schemas'
+
+ def message_node_schemas(self, message: semantics.Message) -> str:
+ return f'{ident(message.name)}_node_schemas'
+
+ def message_schema(self, message: semantics.Message) -> str:
+ """The tree the core builds one of its own messages against."""
+ return f'{self.ident}_{ident(message.name)}_schema'
+
+ def message_max_len(self, message: semantics.Message) -> str:
+ return f'{self.prefix}_{macro(message.name)}_MAX_LEN'
+
+ # Names that a family's device descriptor defines. Named after the family
+ # that declared what they answer: an inherited op is answered by that
+ # family implementation, on whatever device it arrived at.
+
+ def descriptor(self) -> str:
+ return f'{self.ident}_family'
+
+ def ops(self) -> str:
+ return f'{self.ident}_ops'
+
+ def ext_ops(self) -> str:
+ return f'{self.ident}_ext_ops'
+
+ def ext_ident(self, fill: semantics.Fill) -> str:
+ return f'{self.ident}_{ident(fill.name)}'
+
+ def ext_callback(self, fill: semantics.Fill, which: str) -> str:
+ """One half of what this family does with an extension point."""
+ return f'{self.ext_ident(fill)}_{which}'
+
+ def op_ident(self, op: semantics.Op) -> str:
+ return f'{self.owner_ident(op.family_id)}_{ident(op.name)}'
+
+ def callback(self, op: semantics.Op) -> str:
+ return f'{self.op_ident(op)}_{op.type}'
+
+ def op_helper(self, op: semantics.Op, what: str) -> str:
+ """What one device decides about one op."""
+ return f'{self.op_ident(op)}_{what}'
+
+ def field_ident(self, field: semantics.Field) -> str:
+ """The stem of every helper of one field, after its constant."""
+ return f'{self.owner_ident(field.family_id)}_{ident(field.c_name)}'
+
+ def field_helper(self, field: semantics.Field, what: str) -> str:
+ """One helper of one field: reading it, or appending it."""
+ return f'{self.field_ident(field)}_{what}'
+
+ def op_hook(self, op: semantics.Op, which: str) -> str:
+ """One half of the pair that wraps this op alone."""
+ return f'{self.op_ident(op)}_{which}'
+
+ # Where a generated file lives: the installed constants, the schema
+ # metadata a family compiles, and the descriptor it registers.
+
+ def uapi_guard(self) -> str:
+ return f'_UAPI_CTLV_{self.prefix}_GEN_H'
+
+ def uapi_header(self, in_kernel: bool = False) -> str:
+ return uapi_header(self.family.name, in_kernel)
+
+ def kernel_guard(self) -> str:
+ return f'_LINUX_{self.prefix}_GEN_H'
+
+ def kernel_header(self) -> str:
+ return kernel_header(self.family.name)
+
+ def user_guard(self) -> str:
+ return f'_CTLV_{self.prefix}_USER_GEN_H'
+
+ def user_header(self) -> str:
+ return f'"{self.ident}_user_gen.h"'
+
+ # Names that a family's userspace binding defines, after the family that
+ # declared the op, as a per-op helper is in the kernel.
+
+ def user_op(self, op: semantics.Op) -> str:
+ return f'{self.owner_ident(op.family_id)}_{ident(op.name)}'
+
+ def user_op_helper(self, op: semantics.Op, what: str) -> str:
+ return f'{self.user_op(op)}_{what}'
+
+ def user_op_len(self, op: semantics.Op, what: str) -> str:
+ """One length of one op, as its specification computed it."""
+ return f'{self.op_id(op)}_{macro(what)}'
+
+ def user_node_schema(self, op: semantics.Op, which: str) -> str:
+ # File scope, so no family prefix, as on the kernel side.
+ return f'{ident(op.name)}_{which}_node_schema'
+
+ def user_spec(self) -> str:
+ """What this family's specification says, to compare a device to."""
+ return f'{self.ident}_spec'
+
+ def user_op_specs(self) -> str:
+ return f'{self.ident}_op_specs'
+
+
+def getter_value(field: semantics.Field) -> bool:
+ """Whether a field has a getter beside the reader that locates it."""
+ return field.decl.kind != 'blob' and \
+ not (field.decl.kind == 'record' and field.decl.max_occurrences != 1)
+
+
+def input_readers(names: Names, field: semantics.Field) -> tuple[str, ...]:
+ """The names of what reads one field of a request."""
+ readers = [names.field_helper(field, 'ref')]
+ readers.append(names.field_helper(field, 'for_each')
+ if field.decl.max_occurrences != 1
+ else names.field_helper(field, 'present'))
+ if field.decl.kind == 'blob':
+ readers.append(names.field_helper(field, 'resolve'))
+ elif getter_value(field):
+ readers.append(names.field_helper(field, 'get'))
+ return tuple(readers)
+
+
+def output_writers(names: Names, field: semantics.Field) -> tuple[str, ...]:
+ """The names of what appends one field to a reply.
+
+ A blob has none: a request carries one and a reply never does.
+ """
+ if field.decl.kind == 'blob':
+ return ()
+ what = 'start' if field.decl.kind == 'record' else 'put'
+ return (names.field_helper(field, what),)
+
+
+# What a blob field may be backed by, and the libctlv call describing one. A
+# putter exists per backing the field permits, so what a caller may
+# describe is decided by the specification.
+BLOB_PUTTERS: dict[str, tuple[str, tuple[str, ...]]] = {
+ 'va': ('va', ('const void *address', '__u64 length')),
+}
+
+# The lengths a user-mode header states for one op, by sizes.OpSizes member.
+# What a device answers with, its descendants' fills included, op-get says.
+USER_LENGTHS: dict[str, str] = {
+ 'request_max_len': 'request-max-len',
+}
+
+
+def user_readers(names: Names, field: semantics.Field) -> tuple[str, ...]:
+ """The names of what reads one field in the userspace binding."""
+ if field.decl.kind == 'blob':
+ return ()
+ readers = [names.field_helper(field, 'for_each')
+ if field.decl.max_occurrences != 1
+ else names.field_helper(field, 'present')]
+ if getter_value(field):
+ readers.append(names.field_helper(field, 'get'))
+ return tuple(readers)
+
+
+def user_writers(names: Names, field: semantics.Field) -> tuple[str, ...]:
+ """The names of what appends one field in the userspace binding.
+
+ A blob has one putter per backing the field allows, where the kernel
+ side has none.
+ """
+ if field.decl.kind == 'blob':
+ assert field.decl.blob is not None
+ return tuple(names.field_helper(field,
+ f'put_{BLOB_PUTTERS[backing][0]}')
+ for backing in field.decl.blob.backings)
+ what = 'start' if field.decl.kind == 'record' else 'put'
+ return (names.field_helper(field, what),)
+
+
+class _Table:
+ """Generated C names, each mapped to what it was generated for."""
+
+ def __init__(self, family: semantics.Family) -> None:
+ self.family = family
+ self.names: dict[str, str] = {}
+
+ def add(self, name: str, origin: str) -> None:
+ previous = self.names.get(name)
+ if previous is not None:
+ raise CtlvError(f'{self.family.path}: {origin}',
+ f'generated name {name} is already used by '
+ f'{previous}')
+ self.names[name] = origin
+
+
+def user_symbols(family: semantics.Family) -> dict[str, str]:
+ """Return every C name of family's binding, mapped to its origin.
+
+ The binding covers this family's own fields and ops, reaching the rest
+ through the parent's header.
+ """
+ names = Names(family)
+ table = _Table(family)
+
+ for field in family.local_fields:
+ what = f'field {field.full_name}'
+ for helper in user_readers(names, field) + \
+ user_writers(names, field):
+ table.add(helper, what)
+ for op in family.local_ops:
+ what = f'op {op.name}'
+ table.add(names.user_op(op), what)
+ if op.request is not None:
+ table.add(names.user_op_helper(op, 'request_new'), what)
+ for member, what_len in USER_LENGTHS.items():
+ if getattr(op.sizes, member):
+ table.add(names.user_op_len(op, what_len), what)
+ return table.names
+
+
+def _extensions_anywhere(node: semantics.Node) -> bool:
+ return bool(node.extensions) or any(_extensions_anywhere(child)
+ for child in node.children())
+
+
+def symbols(family: semantics.Family) -> dict[str, str]:
+ """Return every generated C name of family, mapped to its declaration.
+
+ Dashes become underscores, so two YAML names can spell one C name. That
+ has to be an error: one of the two constants would silently not exist.
+ """
+ names = Names(family)
+ table = _Table(family)
+ add = table.add
+
+ for name in LIFECYCLE_API_SYMBOLS:
+ add(name, 'the CTLV lifecycle API')
+ add(names.family_const(), f'family {family.name}')
+ # A child header may include every ancestor's family API, and built-in
+ # families also share one linker namespace with the root.
+ for family_id, owner in family.owners.items():
+ owner_ident = ident(owner)
+ if any(op.family_id == family_id for op in family.ops):
+ add(f'{owner_ident}_op_schemas', f'family {owner}')
+ add(f'{owner_ident}_schema', f'family {owner}')
+ add(f'{owner_ident}_family', f'family {owner}')
+ for fill in family.fills:
+ for which in ('fill', 'read'):
+ add(names.ext_callback(fill, which), f'record {fill.name}')
+ for definition in family.definitions.values():
+ if definition.header or definition.is_extension_point:
+ continue
+ what = f'{definition.kind} {definition.name}'
+ if definition.kind == 'enum':
+ add(f'enum {names.definition(definition)}', what)
+ else:
+ add(f'enum {names.bit_enum(definition)}', what)
+ add(names.value_mask(definition), what)
+ for value_name, _ in definition.values:
+ if definition.kind == 'flags':
+ add(names.value_bit(definition, value_name), what)
+ add(names.value(definition, value_name), what)
+ for field in family.fields.values():
+ what = f'field {field.full_name}'
+ add(names.field(field), what)
+ if field.decl.unit is not None:
+ add(names.field_unit(field), what)
+ add(names.field_scale(field), what)
+ for extension in family.extensions.values():
+ add(names.extension(extension), f'extension {extension.name}')
+ for enum_extension in family.enum_extensions.values():
+ add(names.enum_extension(enum_extension),
+ f'enum {enum_extension.name}')
+ _helper_symbols(family, add)
+ for op in family.ops:
+ what = f'op {op.name}'
+ add(names.op_id(op), what)
+ add(names.callback(op), what)
+ if op.optional:
+ add(names.op_helper(op, 'provide'), what)
+ if op.availability == 'dynamic':
+ add(names.op_helper(op, 'enable'), what)
+ add(names.op_helper(op, 'disable'), what)
+ if op.pre_post:
+ add(names.op_hook(op, 'pre'), what)
+ add(names.op_hook(op, 'post'), what)
+ for message in family.messages:
+ what = f'record {message.name}'
+ add(names.message_node_schemas(message), what)
+ add(names.message_schema(message), what)
+ add(names.message_max_len(message), what)
+ if family.fills:
+ add(names.ext_fill_schemas(), f'family {family.name}')
+ add(names.ext_ops(), f'family {family.name}')
+ if family.enum_fills:
+ add(names.enum_fill_schemas(), f'family {family.name}')
+ if family.local_ops:
+ add(names.ops(), f'family {family.name}')
+ for op in family.local_ops:
+ what = f'op {op.name}'
+ for which in ('request', 'reply'):
+ root = getattr(op, which)
+ if root is None:
+ continue
+ add(names.node_schemas(op, which), what)
+ if _extensions_anywhere(root):
+ add(names.ext_schemas(op, which), what)
+ for fill in family.fills:
+ add(names.fill_node_schemas(fill), f'record {fill.name}')
+ return table.names
+
+
+def _helper_symbols(family: semantics.Family,
+ add: Callable[[str, str], None]) -> None:
+ """The readers and writers of the whole chain, each where it is emitted.
+
+ Every family of the chain emits the helpers its own uses add, so the
+ chain's helpers are those of each prefix of it.
+ """
+ names = Names(family)
+ inputs, outputs = semantics.field_uses(family)
+ for field in inputs.values():
+ for helper in input_readers(names, field):
+ add(helper, f'field {field.full_name}')
+ for field in outputs.values():
+ for helper in output_writers(names, field):
+ add(helper, f'field {field.full_name}')
diff --git a/tools/ctlv/ctlvgen/lib/compat.py b/tools/ctlv/ctlvgen/lib/compat.py
new file mode 100644
index 000000000000..76bd14f60750
--- /dev/null
+++ b/tools/ctlv/ctlvgen/lib/compat.py
@@ -0,0 +1,877 @@
+# SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+"""ABI snapshots and the compatibility checker.
+
+A change is compatible only if the new schema still contains everything the
+checked-in snapshot describes, with the same IDs, no narrowed bound, no
+member that became required and no member of a reply that became optional.
+A new member has to be optional. Regenerating
+rewrites a snapshot, refusing an incompatible one unless the break is meant,
+so the snapshot's diff is how the change gets reviewed.
+
+The snapshot is keyed by record path. A record's ID comes from where it is in
+the spec and a field's type from its record and number, so the checker only
+compares them; it assigns nothing.
+"""
+
+from __future__ import annotations
+
+import pathlib
+import re
+from typing import Any
+
+import yaml
+
+from . import cgen
+from . import semantics
+from . import spec
+from . import yamlload
+from .errors import CtlvError
+
+HEADER: str = '''\
+# SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause)
+%YAML 1.2
+---
+# Checked-in ABI snapshot of the {name} family, from
+# {spec}, written by
+# "make -C tools/ctlv regen".
+#
+# Updating the file is how a UAPI change gets reviewed; an update that
+# removes or renumbers anything, narrows a bound, or makes a member required
+# is an ABI break, whatever the file says afterwards.
+'''
+
+REGISTRY_HEADER: str = '''\
+# SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause)
+%YAML 1.2
+---
+# Checked-in ABI baseline of the ordered CTLV family registry.
+# Written by "make -C tools/ctlv regen".
+'''
+
+SNAPSHOT_INDENT: int = 2
+
+_SIZE: re.Pattern[str] = re.compile(r'(?P<value>[0-9]+)(?P<suffix>[KMG])')
+
+# The bounds that are byte counts, which a snapshot writes as a spec does.
+_LENGTHS: tuple[str, ...] = ('min-length', 'max-length', 'length', 'alignment')
+
+
+class _Dumper(yaml.SafeDumper):
+ """Dumper that never emits anchors, which the strict loader rejects."""
+
+ def ignore_aliases(self, data: object) -> bool:
+ return True
+
+ def increase_indent(self, flow: bool = False,
+ indentless: bool = False) -> None:
+ # Indent a sequence under its key, not flush left as pyyaml does.
+ return super().increase_indent(flow, False)
+
+
+class _FlowMap(dict[str, Any]):
+ pass
+
+
+class _FlowSeq(list[str]):
+ pass
+
+
+class _Hex(int):
+ pass
+
+
+_Dumper.add_representer(
+ _FlowMap,
+ lambda dumper, value: dumper.represent_mapping(
+ 'tag:yaml.org,2002:map', value, flow_style=True))
+_Dumper.add_representer(
+ _FlowSeq,
+ lambda dumper, value: dumper.represent_sequence(
+ 'tag:yaml.org,2002:seq', value, flow_style=True))
+_Dumper.add_representer(
+ _Hex,
+ lambda dumper, value: dumper.represent_scalar(
+ 'tag:yaml.org,2002:int', f'{value:#010x}'))
+
+
+def build(family: semantics.Family) -> dict[str, Any]:
+ """Return one family's ABI in the comparison form."""
+ return {
+ 'family': family.name,
+ 'family-id': family.family_id,
+ 'parent': family.parent,
+ 'spec': cgen.spec_path(family.path),
+ 'definitions': _definitions(family.local_definitions),
+ 'records': _records(family),
+ 'extensions': {extension.name: extension.ext_id
+ for extension in family.local_extensions},
+ 'enum-extensions': {extension.name: extension.ext_id
+ for extension in family.local_enum_extensions},
+ 'fills': {fill.name: fill.record.path for fill in family.fills},
+ 'ops': _ops(family.local_ops),
+ }
+
+
+def render(family: semantics.Family) -> str:
+ """Return one family's compact snapshot as YAML text."""
+ return render_data(build(family))
+
+
+def render_data(data: dict[str, Any]) -> str:
+ """Render comparison-form data as a compact snapshot."""
+ body = yaml.dump(_compact(data), Dumper=_Dumper, sort_keys=False,
+ default_flow_style=False, width=1000000,
+ indent=SNAPSHOT_INDENT)
+ body = '\n'.join(line for source in body.splitlines()
+ for line in _wrap_flow(source)) + '\n'
+ return HEADER.format(name=data['family'], spec=data['spec']) + body
+
+
+def _wrap_flow(line: str, width: int = 80) -> tuple[str, ...]:
+ """Wrap one flow collection with continuations aligned after its opener.
+
+ Where an item does not fit there, the collection starts a line of its own
+ below its key.
+ """
+ if len(line) <= width:
+ return (line,)
+ starts = [index for index in (line.find('{'), line.find('['))
+ if index >= 0]
+ if not starts:
+ return (line,)
+ start = min(starts)
+ end, items = _flow_items(line, start)
+ if end is None:
+ return (line,)
+
+ wrapped = _wrap_items(line[:start + 1], line[end:], start + 1, items,
+ width)
+ key = line[:start].rstrip()
+ if max(map(len, wrapped)) <= width or not key.endswith(':'):
+ return wrapped
+ indent = len(line) - len(line.lstrip()) + SNAPSHOT_INDENT
+ return (key,) + _wrap_items(' ' * indent + line[start], line[end:],
+ indent + 1, items, width)
+
+
+def _wrap_items(prefix: str, suffix: str, indent_width: int,
+ items: tuple[str, ...], width: int) -> tuple[str, ...]:
+ indent = ' ' * indent_width
+ wrapped = []
+ current = prefix
+ for index, item in enumerate(items):
+ last = index + 1 == len(items)
+ token = item + ('' if last else ',')
+ separator = '' if current.endswith(('{', '[')) else ' '
+ closing = len(suffix) if last else 0
+ if len(current) + len(separator) + len(token) + closing > width:
+ wrapped.append(current)
+ current = indent + token
+ else:
+ current += separator + token
+ current += suffix
+ wrapped.append(current)
+ return tuple(wrapped)
+
+
+def _flow_items(line: str,
+ start: int) -> tuple[int | None, tuple[str, ...]]:
+ pairs = {'{': '}', '[': ']'}
+ stack = [line[start]]
+ quote = None
+ escape = False
+ item_start = start + 1
+ items = []
+ for index in range(start + 1, len(line)):
+ char = line[index]
+ if quote is not None:
+ if escape:
+ escape = False
+ elif char == '\\':
+ escape = True
+ elif char == quote:
+ quote = None
+ continue
+ if char in ('"', "'"):
+ quote = char
+ elif char in pairs:
+ stack.append(char)
+ elif char in pairs.values():
+ if pairs[stack[-1]] != char:
+ return None, ()
+ stack.pop()
+ if not stack:
+ items.append(line[item_start:index].strip())
+ return index, tuple(item for item in items if item)
+ elif char == ',' and len(stack) == 1:
+ items.append(line[item_start:index].strip())
+ item_start = index + 1
+ return None, ()
+
+
+def breaks(family: semantics.Family, path: pathlib.Path | str,
+ text: str | None = None) -> list[str]:
+ """Every way family breaks the snapshot in path, or the one in text."""
+ return _compare(_expand(yamlload.load(path, text)), build(family),
+ _reply_records(family))
+
+
+def check(family: semantics.Family, path: pathlib.Path) -> None:
+ """Compare a family against its snapshot, or raise with every problem."""
+ problems = breaks(family, path)
+ if problems:
+ raise CtlvError(f'{family.name} against {path}',
+ 'the schema is not compatible with its snapshot\n' +
+ '\n'.join(f' {problem}' for problem in problems))
+
+
+# The comparison form, from a resolved family.
+
+def _definitions(
+ items: tuple[semantics.Definition, ...]) -> dict[str, dict[str, Any]]:
+ return {definition.name: _definition(definition)
+ for definition in items if not definition.is_extension_point}
+
+
+def _definition(definition: semantics.Definition) -> dict[str, Any]:
+ entry: dict[str, Any] = {'kind': definition.kind,
+ 'values': dict(definition.values)}
+ if definition.header:
+ entry['header'] = _header_keys(definition.header)
+ return entry
+
+
+def _header_keys(header: spec.HeaderEnum) -> dict[str, str]:
+ keys = {'$header': header.path, '$enum': header.tag}
+ if header.end:
+ keys['$end'] = header.end
+ return keys
+
+
+def _records(family: semantics.Family) -> dict[str, dict[str, Any]]:
+ records = {}
+ for record in family.local_records:
+ own = {field.type for field in record.fields}
+ records[record.path] = {
+ 'record-id': record.record_id,
+ 'fields': {member.field.name: _field(member.field,
+ member.required)
+ for member in record.members
+ if member.field.type in own},
+ 'members': _foreign(family, record, own),
+ }
+ return records
+
+
+def _foreign(family: semantics.Family, record: semantics.Record,
+ own: set[int]) -> dict[str, bool]:
+ """What a record has that it does not declare: placed records and includes.
+
+ An include is one member, optional when it made a field optional that the
+ record it splices requires.
+ """
+ members = {}
+ for member in record.members:
+ field = member.field
+ if field.type in own:
+ continue
+ if field.shared:
+ members[f'record {field.name}'] = member.required
+ continue
+ key = f'$include {field.record}'
+ source = family.records[field.record]
+ declared = any(item.required for item in source.members
+ if item.field.type == field.type)
+ members.setdefault(key, True)
+ if declared and not member.required:
+ members[key] = False
+ return members
+
+
+def _reply_records(family: semantics.Family) -> set[str]:
+ """The paths of the records a reply of the chain carries, fills too."""
+ owners = {field.type: record.path for record in family.records.values()
+ for field in record.fields}
+ found: set[str] = set()
+
+ def walk(node: semantics.Node) -> None:
+ for extension in node.extensions:
+ fill = family.fill_of(extension)
+ if fill is not None:
+ found.add(fill.record.path)
+ for child, _optional in semantics.composed_members(family, node):
+ found.add(owners[child.child_field.type])
+ if child.child_field.holds is not None:
+ found.add(child.child_field.holds)
+ walk(child)
+
+ for op in family.ops:
+ if op.reply is not None:
+ assert op.reply_record is not None
+ found.add(op.reply_record.path)
+ walk(op.reply)
+ return found
+
+
+def _field(field: semantics.Field, required: bool) -> dict[str, Any]:
+ decl = field.decl
+ entry = {'number': field.number, 'kind': decl.kind,
+ 'required': required,
+ 'max-occurrences': decl.count_enum or decl.max_occurrences}
+ if decl.kind in ('enum', 'flags'):
+ entry['definition'] = decl.ref
+ if decl.kind == 'record':
+ if decl.extension is not None:
+ entry['extension'] = decl.extension
+ else:
+ entry['record'] = field.holds
+ for name, value in (('min', decl.min), ('max', decl.max),
+ ('min-length', decl.min_length),
+ ('max-length', decl.max_length),
+ ('unit', decl.unit), ('scale', decl.scale)):
+ if value is not None:
+ entry[name] = value
+ if decl.blob is not None:
+ entry['blob'] = {
+ 'direction': decl.blob.direction,
+ 'backings': list(decl.blob.backings),
+ 'max-length': decl.blob.max_length,
+ 'alignment': decl.blob.alignment,
+ }
+ return entry
+
+
+def _ops(items: tuple[semantics.Op, ...]) -> dict[str, dict[str, Any]]:
+ ops = {}
+ for op in items:
+ ops[op.name] = {
+ 'op-id': op.local_id,
+ 'type': op.type,
+ 'availability': op.availability,
+ 'optional': op.optional,
+ 'pre-post': op.pre_post,
+ 'request': op.request_record.path if op.request_record else None,
+ 'reply': op.reply_record.path if op.reply_record else None,
+ 'fd-access': op.fd_access,
+ }
+ return ops
+
+
+# The compact form a snapshot is written in.
+
+def _compact(data: dict[str, Any]) -> dict[str, Any]:
+ family = _FlowMap({
+ 'name': data['family'],
+ 'id': data['family-id'],
+ })
+ if data['parent'] is not None:
+ family['inherits'] = data['parent']
+
+ compact: dict[str, Any] = {'family': family}
+ for kind, section in (('enum', 'enums'), ('flags', 'flags')):
+ definitions = {
+ name: _FlowMap(entry.get('header', entry['values']))
+ for name, entry in data['definitions'].items()
+ if entry['kind'] == kind
+ }
+ if definitions:
+ compact[section] = definitions
+ if data['records']:
+ compact['records'] = {path: _compact_record(record)
+ for path, record in data['records'].items()}
+ if data['extensions']:
+ compact['extensions'] = _FlowMap(data['extensions'])
+ if data['enum-extensions']:
+ compact['enum-extensions'] = _FlowMap(data['enum-extensions'])
+ if data['fills']:
+ compact['fills'] = _FlowMap(data['fills'])
+ if data['ops']:
+ compact['ops'] = {name: _compact_op(entry)
+ for name, entry in data['ops'].items()}
+ return compact
+
+
+def _compact_record(record: dict[str, Any]) -> dict[str, Any]:
+ compact: dict[str, Any] = {'id': _Hex(record['record-id'])}
+ if record['fields']:
+ compact['fields'] = {name: _compact_field(field)
+ for name, field in record['fields'].items()}
+ if record['members']:
+ compact['members'] = _FlowSeq(
+ key + ('' if required else '?')
+ for key, required in record['members'].items())
+ return compact
+
+
+def _compact_field(field: dict[str, Any]) -> _FlowMap:
+ kind = field['kind']
+ compact = _FlowMap({'number': field['number']})
+ if kind in ('enum', 'flags'):
+ compact[kind] = field['definition']
+ elif kind == 'blob':
+ blob = field['blob']
+ compact['blob'] = blob['direction']
+ compact['backings'] = _FlowSeq(blob['backings'])
+ compact['max-length'] = _size(blob['max-length'])
+ if blob['alignment'] != 1:
+ compact['alignment'] = _size(blob['alignment'])
+ elif kind == 'record':
+ if 'extension' in field:
+ compact['extension'] = field['extension']
+ else:
+ compact['record'] = field['record']
+ else:
+ compact['type'] = kind
+
+ if kind in ('uint', 'sint'):
+ low, high = _integer_extremes(kind)
+ bound = _size if field.get('unit') == 'bytes' else _integer
+ if field.get('min', low) != low:
+ compact['min'] = bound(field['min'])
+ if field.get('max', high) != high:
+ compact['max'] = bound(field['max'])
+ if 'unit' in field:
+ compact['unit'] = field['unit']
+ if field['scale']:
+ compact['scale'] = field['scale']
+ elif kind in ('string', 'binary'):
+ minimum = field.get('min-length', 0)
+ maximum = field['max-length']
+ if kind == 'binary' and minimum == maximum:
+ compact['length'] = _size(maximum)
+ else:
+ if minimum:
+ compact['min-length'] = _size(minimum)
+ compact['max-length'] = _size(maximum)
+
+ maximum = field['max-occurrences']
+ if maximum != 1:
+ compact['max-occurrences'] = (
+ 'unlimited' if maximum == spec.UNLIMITED else maximum)
+ if not field['required']:
+ compact['optional'] = True
+ return compact
+
+
+def _integer(value: int) -> int | str:
+ """An integer bound as a spec writes it, a plain one as a number."""
+ text = spec.number_text(value)
+ return value if text == str(value) else text
+
+
+def _size(value: int) -> int | str:
+ """A byte count as a spec writes it, a plain one as a number."""
+ text = spec.size_text(value)
+ return value if text == str(value) else text
+
+
+def _integer_extremes(kind: str) -> tuple[int, int]:
+ return (0, spec.U64_MAX) if kind == 'uint' else (spec.S64_MIN,
+ spec.S64_MAX)
+
+
+def _compact_op(op: dict[str, Any]) -> _FlowMap:
+ abi = _FlowMap({
+ 'id': op['op-id'],
+ 'type': op['type'],
+ })
+ if op.get('fd-access') != spec.DEFAULT_FD_ACCESS:
+ abi['fd-access'] = op.get('fd-access')
+ availability = op.get('availability', spec.DEFAULT_AVAILABILITY)
+ if availability != spec.DEFAULT_AVAILABILITY:
+ abi['availability'] = availability
+ if op.get('optional', False):
+ abi['optional'] = True
+ if op.get('pre-post', False):
+ abi['pre-post'] = True
+ if op.get('request') is not None:
+ abi['request'] = op['request']
+ if op.get('reply') is not None:
+ abi['reply'] = op['reply']
+ return abi
+
+
+def _expand(compact: dict[str, Any]) -> dict[str, Any]:
+ family = compact['family']
+ definitions = {}
+ for kind, section in (('enum', 'enums'), ('flags', 'flags')):
+ for name, values in compact.get(section, {}).items():
+ definitions[name] = {
+ 'kind': kind,
+ 'values': {} if '$header' in values else values,
+ }
+ if '$header' in values:
+ definitions[name]['header'] = dict(values)
+ return {
+ 'family': family['name'],
+ 'family-id': family['id'],
+ 'parent': family.get('inherits'),
+ 'definitions': definitions,
+ 'records': {path: _expand_record(path, record)
+ for path, record in compact.get('records', {}).items()},
+ 'extensions': dict(compact.get('extensions', {})),
+ 'enum-extensions': dict(compact.get('enum-extensions', {})),
+ 'fills': dict(compact.get('fills', {})),
+ 'ops': _expand_ops(compact.get('ops', {})),
+ }
+
+
+def _expand_record(path: str, compact: dict[str, Any]) -> dict[str, Any]:
+ members = {}
+ for item in compact.get('members', []):
+ members[item.removesuffix('?')] = not item.endswith('?')
+ return {
+ 'record-id': compact['id'],
+ 'fields': {name: _expand_field(f'{path}.{name}', field)
+ for name, field in compact.get('fields', {}).items()},
+ 'members': members,
+ }
+
+
+def _expand_field(where: str, compact: dict[str, Any]) -> dict[str, Any]:
+ primary = [name for name in ('type', 'enum', 'flags', 'blob', 'record',
+ 'extension')
+ if name in compact]
+ if len(primary) != 1:
+ raise CtlvError(f'ABI snapshot field {where}',
+ 'expected one of type, enum, flags, blob, record or '
+ 'extension')
+ kind = primary[0]
+ if kind == 'type':
+ kind = compact['type']
+ elif kind in ('record', 'extension'):
+ kind = 'record'
+ field = {
+ 'number': compact['number'],
+ 'kind': kind,
+ 'required': not compact.get('optional', False),
+ 'max-occurrences': compact.get('max-occurrences', 1),
+ }
+ if field['max-occurrences'] == 'unlimited':
+ field['max-occurrences'] = spec.UNLIMITED
+ if kind in ('enum', 'flags'):
+ field['definition'] = compact[kind]
+ elif kind == 'record':
+ for key in ('record', 'extension'):
+ if key in compact:
+ field[key] = compact[key]
+ elif kind in ('uint', 'sint'):
+ low, high = _integer_extremes(kind)
+ field['min'] = _number(where, compact.get('min', low))
+ field['max'] = _number(where, compact.get('max', high))
+ if 'unit' in compact:
+ field['unit'] = compact['unit']
+ field['scale'] = compact.get('scale', 0)
+ elif kind in ('string', 'binary'):
+ if 'length' in compact:
+ field['min-length'] = _number(where, compact['length'])
+ field['max-length'] = field['min-length']
+ else:
+ field['min-length'] = _number(where,
+ compact.get('min-length', 0))
+ field['max-length'] = _number(where, compact['max-length'])
+ elif kind == 'blob':
+ field['blob'] = {
+ 'direction': compact['blob'],
+ 'backings': list(compact['backings']),
+ 'max-length': _number(where, compact['max-length']),
+ 'alignment': _number(where, compact.get('alignment', 1)),
+ }
+ return field
+
+
+def _number(where: str, value: int | str) -> int:
+ """A number as a snapshot writes it: plain, a limit's name or a size."""
+ if isinstance(value, int):
+ return value
+ if value in spec.NAMED_NUMBERS:
+ return spec.NAMED_NUMBERS[value]
+ found = _SIZE.fullmatch(str(value))
+ if found is None:
+ raise CtlvError(f'ABI snapshot field {where}',
+ f'"{value}" is not a number')
+ scale = dict(spec.SIZE_SUFFIXES)[found['suffix']]
+ return int(found['value']) * scale
+
+
+def _expand_ops(ops: dict[str, Any]) -> dict[str, dict[str, Any]]:
+ expanded = {}
+ for name, abi in ops.items():
+ expanded[name] = {
+ 'op-id': abi['id'],
+ 'type': abi['type'],
+ 'availability': abi.get(
+ 'availability', spec.DEFAULT_AVAILABILITY),
+ 'optional': abi.get('optional', False),
+ 'pre-post': abi.get('pre-post', False),
+ 'request': abi.get('request'),
+ 'reply': abi.get('reply'),
+ 'fd-access': abi.get('fd-access', spec.DEFAULT_FD_ACCESS),
+ }
+ return expanded
+
+
+def registry_build(
+ registry: spec.Registry) -> dict[str, list[str | dict[str, Any]]]:
+ """Return the registry in the same normalized form as its source."""
+ families: list[str | dict[str, Any]] = []
+ for entry in registry.entries:
+ if entry.deprecated:
+ families.append({'name': entry.name, 'deprecated': True})
+ else:
+ families.append(entry.name)
+ return {'families': families}
+
+
+def registry_render(registry: spec.Registry) -> str:
+ body = yaml.dump(registry_build(registry), Dumper=_Dumper,
+ sort_keys=False, default_flow_style=False, width=76)
+ return REGISTRY_HEADER + body
+
+
+def registry_problems(old: spec.Registry,
+ current: spec.Registry) -> list[str]:
+ problems = []
+ for family_id, was in enumerate(old.entries):
+ if family_id >= len(current.entries):
+ problems.append(f'family ID {family_id} ({was.name}) was removed')
+ continue
+ now = current.entries[family_id]
+ if now.name != was.name:
+ problems.append(f'family ID {family_id} was {was.name}, now '
+ f'{now.name}')
+ elif was.deprecated and not now.deprecated:
+ problems.append(f'family ID {family_id} ({was.name}) was reused '
+ 'after deprecation')
+ return problems
+
+
+# The comparison.
+
+def _compare(old: dict[str, Any], new: dict[str, Any],
+ replies: set[str]) -> list[str]:
+ problems = []
+ if old['family'] != new['family']:
+ problems.append(f'family was renamed from {old["family"]} to '
+ f'{new["family"]}')
+ if old['family-id'] != new['family-id']:
+ problems.append(f'family-id was {old["family-id"]}, now '
+ f'{new["family-id"]}')
+ # A name carries its ancestry, so reparenting renames every symbol.
+ if old['parent'] != new['parent']:
+ problems.append(f'the inherited family was {old["parent"]}, now '
+ f'{new["parent"]}')
+ _compare_definitions(problems, old['definitions'], new['definitions'])
+ _compare_records(problems, old['records'], new['records'], replies)
+ _compare_extensions(problems, 'extension point', old['extensions'],
+ new['extensions'])
+ _compare_extensions(problems, 'enum extension point',
+ old['enum-extensions'], new['enum-extensions'])
+ _compare_fills(problems, old['fills'], new['fills'])
+ _compare_ops(problems, old['ops'], new['ops'])
+ return problems
+
+
+def _compare_definitions(problems: list[str], old: dict[str, dict[str, Any]],
+ new: dict[str, dict[str, Any]]) -> None:
+ for name, was in old.items():
+ now = new.get(name)
+ if now is None:
+ problems.append(f'{was["kind"]} {name} was removed')
+ continue
+ where = f'{was["kind"]} {name}'
+ if now['kind'] != was['kind']:
+ problems.append(f'{where} is now a {now["kind"]} definition')
+ if 'header' in was or 'header' in now:
+ _compare_header(problems, where, was, now)
+ continue
+ for value, number in was['values'].items():
+ if value not in now['values']:
+ problems.append(f'{where}: value {value} was removed')
+ elif now['values'][value] != number:
+ problems.append(f'{where}: value {value} was {number}, now '
+ f'{now["values"][value]}')
+
+
+def _compare_header(problems: list[str], where: str, was: dict[str, Any],
+ now: dict[str, Any]) -> None:
+ """A header numbers its enum, so only its $end may change, to later."""
+ old, new = was.get('header'), now.get('header')
+ if old is None or new is None:
+ source = new or old
+ assert source is not None
+ problems.append(f'{where} {"now" if old is None else "no longer"} '
+ f'takes its values from <{source["$header"]}>')
+ return
+ if (old['$header'], old['$enum']) != (new['$header'], new['$enum']):
+ problems.append(f'{where} was enum {old["$enum"]} of '
+ f'<{old["$header"]}>, now enum {new["$enum"]} of '
+ f'<{new["$header"]}>')
+ return
+ end = old.get('$end')
+ if end == new.get('$end'):
+ return
+ if end is None:
+ problems.append(f'{where}: $end {new["$end"]} leaves values out')
+ elif end not in now['values']:
+ problems.append(f'{where}: $end was {end}, and may only move later')
+
+
+def _compare_records(problems: list[str], old: dict[str, dict[str, Any]],
+ new: dict[str, dict[str, Any]],
+ replies: set[str]) -> None:
+ for path, was in old.items():
+ now = new.get(path)
+ where = f'record {path}'
+ if now is None:
+ problems.append(f'{where} was removed')
+ continue
+ if was['record-id'] != now['record-id']:
+ problems.append(f'{where}: ID was {was["record-id"]:#010x}, now '
+ f'{now["record-id"]:#010x}')
+ reply = path in replies
+ for name, field_was in was['fields'].items():
+ field_now = now['fields'].get(name)
+ if field_now is None:
+ problems.append(f'field {path}.{name} was removed')
+ continue
+ _compare_field(problems, f'field {path}.{name}', field_was,
+ field_now, reply)
+ for name, field_now in now['fields'].items():
+ if name not in was['fields'] and field_now['required']:
+ problems.append(f'{where}: new field {name} must be '
+ 'optional')
+ _compare_members(problems, where, was['members'], now['members'],
+ reply)
+
+
+def _compare_field(problems: list[str], where: str, was: dict[str, Any],
+ now: dict[str, Any], reply: bool) -> None:
+ for key in ('number', 'kind', 'definition', 'record', 'extension',
+ 'unit', 'scale'):
+ if was.get(key) != now.get(key):
+ problems.append(f'{where}: {key} was {was.get(key)}, now '
+ f'{now.get(key)}')
+ if now['required'] and not was['required']:
+ problems.append(f'{where} became required')
+ if reply and was['required'] and not now['required']:
+ problems.append(f'{where} became optional')
+ # The bounds of one schema kind say nothing about another's.
+ if was['kind'] != now['kind']:
+ return
+ for key in ('min', 'min-length'):
+ if key in was and now.get(key, 0) > was[key]:
+ problems.append(f'{where}: {key} was narrowed from '
+ f'{_text(key, was[key])} to '
+ f'{_text(key, now[key])}')
+ for key in ('max', 'max-length'):
+ if key in was and now.get(key, 0) < was[key]:
+ problems.append(f'{where}: {key} was narrowed from '
+ f'{_text(key, was[key])} to '
+ f'{_text(key, now[key])}')
+ _compare_occurrences(problems, where, was, now)
+ if 'blob' in was:
+ _compare_blob(problems, where, was['blob'], now.get('blob', {}))
+
+
+def _text(key: str, value: int) -> str:
+ return spec.size_text(value) if key in _LENGTHS else \
+ spec.number_text(value)
+
+
+def _compare_occurrences(problems: list[str], where: str, was: dict[str, Any],
+ now: dict[str, Any]) -> None:
+ old_max = was['max-occurrences']
+ new_max = now['max-occurrences']
+ if old_max == new_max or new_max == spec.UNLIMITED:
+ return
+ # One enum's count only grows; another's, or a number, is not comparable.
+ if isinstance(old_max, str) or isinstance(new_max, str):
+ problems.append(f'{where}: max-occurrences was '
+ f'{_occurrences(old_max)}, now '
+ f'{_occurrences(new_max)}')
+ elif old_max == spec.UNLIMITED or new_max < old_max:
+ problems.append(f'{where}: max-occurrences was narrowed from '
+ f'{_occurrences(old_max)} to {_occurrences(new_max)}')
+
+
+def _compare_blob(problems: list[str], where: str, was: dict[str, Any],
+ now: dict[str, Any]) -> None:
+ for key in ('direction', 'alignment'):
+ if was.get(key) != now.get(key):
+ problems.append(f'{where}: blob {key} was {was.get(key)}, now '
+ f'{now.get(key)}')
+ for backing in was.get('backings', []):
+ if backing not in now.get('backings', []):
+ problems.append(f'{where}: blob backing {backing} was removed')
+ if now.get('max-length', 0) < was.get('max-length', 0):
+ problems.append(f'{where}: blob max-length was narrowed from '
+ f'{spec.size_text(was["max-length"])} to '
+ f'{spec.size_text(now["max-length"])}')
+
+
+def _compare_members(problems: list[str], where: str, old: dict[str, bool],
+ new: dict[str, bool], reply: bool) -> None:
+ for key, required in old.items():
+ if key not in new:
+ problems.append(f'{where}: {key} was removed')
+ elif new[key] and not required:
+ problems.append(f'{where}: {key} became required')
+ elif reply and required and not new[key]:
+ problems.append(f'{where}: {key} became optional')
+ for key, required in new.items():
+ if key not in old and required:
+ problems.append(f'{where}: new {key} must be optional')
+
+
+def _compare_extensions(problems: list[str], what: str, old: dict[str, int],
+ new: dict[str, int]) -> None:
+ for name, ext_id in old.items():
+ if name not in new:
+ problems.append(f'{what} {name} was removed')
+ elif new[name] != ext_id:
+ problems.append(f'{what} {name}: ID was {ext_id}, now '
+ f'{new[name]}')
+
+
+def _compare_fills(problems: list[str], old: dict[str, str],
+ new: dict[str, str]) -> None:
+ for name, path in old.items():
+ if name not in new:
+ problems.append(f'extension fill {name} was removed')
+ elif new[name] != path:
+ problems.append(f'extension fill {name} was record {path}, now '
+ f'{new[name]}')
+
+
+def _compare_ops(problems: list[str], old: dict[str, dict[str, Any]],
+ new: dict[str, dict[str, Any]]) -> None:
+ for name, was in old.items():
+ now = new.get(name)
+ if now is None:
+ problems.append(f'op {name} was removed')
+ continue
+ where = f'op {name}'
+ # Whether an op may be disabled or withheld is what userspace compiles
+ # against; whether it is either right now is per device.
+ for key in ('op-id', 'type', 'fd-access',
+ 'availability', 'optional', 'pre-post'):
+ if was.get(key) != now.get(key):
+ problems.append(f'{where}: {key} was {was.get(key)}, now '
+ f'{now.get(key)}')
+ for what in ('request', 'reply'):
+ if (what == 'reply' and was['type'] == 'action' and
+ was[what] is None and now[what] is not None):
+ problems.append(f'{where}: an ACK-only action gained a '
+ 'reply schema')
+ elif was[what] is not None and now[what] is None:
+ problems.append(f'{where}: the {what} schema was removed')
+ elif was[what] is not None and was[what] != now[what]:
+ problems.append(f'{where}: the {what} was record '
+ f'{was[what]}, now {now[what]}')
+
+
+def _occurrences(max_occurrences: int | str) -> str:
+ if max_occurrences == spec.UNLIMITED:
+ return 'unlimited'
+ return str(max_occurrences)
diff --git a/tools/ctlv/ctlvgen/lib/errors.py b/tools/ctlv/ctlvgen/lib/errors.py
new file mode 100644
index 000000000000..f6d557d90f38
--- /dev/null
+++ b/tools/ctlv/ctlvgen/lib/errors.py
@@ -0,0 +1,16 @@
+# SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+"""The single error type reported by the CTLV schema tool."""
+
+from __future__ import annotations
+
+
+class CtlvError(Exception):
+ """A specification, registry or ABI snapshot is not acceptable.
+
+ The test suite matches diagnostics, so a message's wording is contract.
+ """
+
+ def __init__(self, where: str | None, what: str) -> None:
+ self.where = where
+ self.what = what
+ super().__init__(f'{where}: {what}' if where else what)
diff --git a/tools/ctlv/ctlvgen/lib/gen_family.py b/tools/ctlv/ctlvgen/lib/gen_family.py
new file mode 100644
index 000000000000..7ce96331803a
--- /dev/null
+++ b/tools/ctlv/ctlvgen/lib/gen_family.py
@@ -0,0 +1,285 @@
+# SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+"""The descriptor of one family: who answers each of its ops.
+
+One struct ctlv_family and one struct ctlv_op per action or query
+this family declares, plus the helpers a device uses to decide them. A family
+answers what it declared: an inherited op is answered through the descriptor
+of the family that declared it, by that family implementation.
+"""
+
+from __future__ import annotations
+
+from collections.abc import Iterable
+
+from . import cgen
+from . import semantics
+from . import spec
+
+# The instance, the request/reply, and a context for core helpers. A device
+# embeds the instance, so a callback reaches its state with container_of().
+CALLBACK_PARAMS: tuple[str, ...] = (
+ 'struct ctlv *ctlv', 'struct ctlv_op_ctx *ctx',
+ 'const struct ctlv_cursor *request', 'struct ctlv_cursor *reply')
+
+# What a hook takes, and what each half returns: a pre may refuse the op.
+HOOK_PARAMS: tuple[str, ...] = ('struct ctlv *ctlv',
+ 'struct ctlv_op_ctx *ctx')
+HOOK_RETURNS: tuple[tuple[str, str], ...] = (('pre', 'int'), ('post', 'void'))
+
+# The reply-side callback for an extension point.
+EXT_HOOKS: tuple[tuple[str, str, tuple[str, ...]], ...] = (
+ ('fill', 'int', ('struct ctlv *ctlv', 'struct ctlv_op_ctx *ctx',
+ 'const struct ctlv_cursor *request',
+ 'struct ctlv_cursor *record')),
+)
+
+# The state helpers of a dynamically available op.
+STATES: tuple[str, ...] = ('enable', 'disable')
+
+INPUT_MACROS: dict[str, str] = {
+ 'uint': 'U64',
+ 'sint': 'S64',
+ 'bool': 'BOOL',
+ 'string': 'STRING',
+ 'binary': 'BINARY',
+ 'enum': 'U64',
+ 'flags': 'U64',
+ 'record': 'RECORD',
+ 'blob': 'BLOB',
+}
+
+PUT_MACROS: dict[str, str] = {
+ 'uint': 'U64',
+ 'sint': 'S64',
+ 'bool': 'BOOL',
+ 'string': 'STRING',
+ 'binary': 'BINARY',
+ 'enum': 'U64',
+ 'flags': 'U64',
+}
+
+
+def bindings(family: semantics.Family) -> tuple[semantics.Op, ...]:
+ """The ops family binds, in the order it binds them.
+
+ Its own ops, in declaration order. An op it inherited is answered by the
+ family that declared it.
+ """
+ return tuple(family.local_ops)
+
+
+def _implementer(family: semantics.Family) -> str:
+ """Who writes the callbacks of a family."""
+ if family.name == spec.CORE_FAMILY_NAME:
+ return 'the CTLV core'
+ return 'this family'
+
+
+def _macro(writer: cgen.Writer, name: str, args: tuple[str, ...]) -> None:
+ writer.parenthesized(f'{name}(', args, ')')
+
+
+def emit_header(writer: cgen.Writer, names: cgen.Names,
+ family: semantics.Family) -> None:
+ """Append the family API to a generated kernel header."""
+ inputs, outputs = semantics.own_uses(family)
+ bound = bindings(family)
+ if bound:
+ writer.comment(f'Implemented by {_implementer(family)}, one per '
+ 'action and query it declares.')
+ for op in bound:
+ writer.signature('int', names.callback(op), CALLBACK_PARAMS)
+ writer.line()
+ _hooks(writer, names, family)
+ _ext_hooks(writer, names, family)
+ writer.declaration('extern const struct ctlv_family',
+ f'{names.descriptor()};')
+ writer.line()
+ writer.line('#ifndef _CTLV_GENERATED_METADATA_ONLY')
+ _helpers(writer, names, family)
+ _readers(writer, names, inputs.values())
+ _writers(writer, names, outputs.values())
+ writer.line('#endif /* _CTLV_GENERATED_METADATA_ONLY */')
+ writer.line()
+
+
+def _hooks(writer: cgen.Writer, names: cgen.Names,
+ family: semantics.Family) -> None:
+ """The pair around one op, for each op that asked for one."""
+ marked = [op for op in bindings(family) if op.pre_post]
+ if not marked:
+ return
+ writer.comment('Per-op hooks inside the device pair.')
+ for first, op in enumerate(marked):
+ if first:
+ writer.line()
+ for which, returns in HOOK_RETURNS:
+ writer.signature(returns, names.op_hook(op, which), HOOK_PARAMS)
+ writer.line()
+
+
+def _ext_hooks(writer: cgen.Writer, names: cgen.Names,
+ family: semantics.Family) -> None:
+ """The fill and read callbacks of each extension point the family fills."""
+ for fill in family.fills:
+ owner = family.owners[fill.family_id]
+ writer.comment(f'{fill.name} of {owner}: fill callback.')
+ for which, returns, params in EXT_HOOKS:
+ writer.signature(returns, names.ext_callback(fill, which),
+ params)
+ writer.line()
+
+
+def _helpers(writer: cgen.Writer, names: cgen.Names,
+ family: semantics.Family) -> None:
+ """What one device may decide about each op this family declares."""
+ ops = [op for op in family.local_ops
+ if op.optional or op.availability == 'dynamic']
+ if not ops:
+ return
+ writer.line()
+ writer.comment('Optional publication and dynamic availability helpers.')
+ for op in ops:
+ if op.optional:
+ _macro(writer, 'CTLV_OP_PROVIDE',
+ (names.op_ident(op), names.op_id(op)))
+ if op.availability == 'dynamic':
+ for state in STATES:
+ _macro(writer, f'CTLV_OP_{state.upper()}',
+ (names.op_ident(op), names.op_id(op)))
+
+
+def _readers(writer: cgen.Writer, names: cgen.Names,
+ fields: Iterable[semantics.Field]) -> None:
+ """What reads one field of a validated request, wherever it is placed.
+
+ A reader finds its field by type among the members of the record it is
+ handed.
+ """
+ fields = tuple(fields)
+ if not fields:
+ return
+ writer.line()
+ writer.comment('Reading a validated message. A blob has a resolver '
+ 'instead of a getter.')
+ for field in fields:
+ repeated = field.decl.max_occurrences != 1
+ name = names.field_ident(field)
+ writer.doc(field.decl.doc)
+ _macro(writer, 'CTLV_IN_REF',
+ (name, names.field(field), cgen.KINDS[field.decl.kind]))
+ if repeated:
+ _iterator(writer, names, field)
+ else:
+ _macro(writer, 'CTLV_IN_HAS', (name,))
+ if field.decl.kind == 'blob':
+ macro = 'CTLV_IN_' + ('EACH_' if repeated else '') + 'BLOB'
+ _macro(writer, macro, (name,))
+ elif cgen.getter_value(field):
+ macro = 'CTLV_IN_' + ('EACH_' if repeated else '') + \
+ INPUT_MACROS[field.decl.kind]
+ _macro(writer, macro, (name,))
+
+
+def _writers(writer: cgen.Writer, names: cgen.Names,
+ fields: Iterable[semantics.Field]) -> None:
+ """What a callback appends to the reply it was handed.
+
+ One helper per field, returning whether its TLV made it into the message.
+ """
+ fields = tuple(field for field in fields
+ if cgen.output_writers(names, field))
+ if not fields:
+ return
+ writer.line()
+ writer.comment('Writing a reply. An append is atomic; a record ends with '
+ 'ctlv_record_end().')
+ for field in fields:
+ if field.decl.kind == 'record':
+ macro = 'CTLV_RECORD_START'
+ else:
+ macro = f'CTLV_PUT_{PUT_MACROS[field.decl.kind]}'
+ writer.doc(field.decl.doc)
+ _macro(writer, macro,
+ (names.field_ident(field), names.field(field),
+ cgen.KINDS[field.decl.kind]))
+
+
+def _iterator(writer: cgen.Writer, names: cgen.Names,
+ field: semantics.Field) -> None:
+ """Every occurrence of a repeated field, in message order."""
+ writer.define_call(
+ f'{names.field_helper(field, "for_each")}(_cursor, _record)',
+ 'CTLV_IN_FOR_EACH', names.field_ident(field), '_cursor', '_record')
+
+
+def emit_source(writer: cgen.Writer, names: cgen.Names,
+ family: semantics.Family) -> None:
+ """Append the family bindings to a generated kernel source."""
+ _bindings(writer, names, family)
+ _ext_ops(writer, names, family)
+ _descriptor(writer, names, family)
+
+
+def _bindings(writer: cgen.Writer, names: cgen.Names,
+ family: semantics.Family) -> None:
+ bound = bindings(family)
+ if not bound:
+ return
+ writer.comment('Local action and query bindings in declaration order.')
+ writer.declaration('static const struct ctlv_op',
+ f'{names.ops()}[] = {{')
+ for op in bound:
+ suffix = '_PRE_POST' if op.pre_post else ''
+ writer.line(f'\tCTLV_OP_{op.type.upper()}{suffix}('
+ f'{names.op_ident(op)}),')
+ writer.line('};')
+ writer.line()
+
+
+def _ext_ops(writer: cgen.Writer, names: cgen.Names,
+ family: semantics.Family) -> None:
+ """One entry per extension point this family fills, in the same order."""
+ if not family.fills:
+ return
+ writer.comment('Extension-point callbacks.')
+ writer.declaration('static const struct ctlv_ext_ops',
+ f'{names.ext_ops()}[] = {{')
+ for fill in family.fills:
+ args = (cgen.family_const(family.owners[fill.family_id]),
+ names.extension(family.extensions[fill.name]),
+ names.ext_ident(fill))
+ writer.parenthesized('\tCTLV_EXT_OPS(', args, '),')
+ writer.line('};')
+ writer.line()
+
+
+def _descriptor(writer: cgen.Writer, names: cgen.Names,
+ family: semantics.Family) -> None:
+ if family.name == spec.CORE_FAMILY_NAME:
+ writer.comment('Root-family descriptor used by the CTLV core.')
+ else:
+ writer.comment('Family descriptor used by ctlv_init().')
+ writer.declaration('const struct ctlv_family',
+ f'{names.descriptor()} = {{')
+ writer.line(f'\t.schema = &{names.schema()},')
+ parent = _parent_descriptor(family)
+ if parent:
+ writer.line(f'\t.parent = &{parent},')
+ if bindings(family):
+ writer.line(f'\t.ops = {names.ops()},')
+ writer.line(f'\t.ops_count = ARRAY_SIZE({names.ops()}),')
+ if family.fills:
+ ext_ops = names.ext_ops()
+ writer.line(f'\t.ext_ops = {ext_ops},')
+ writer.line(f'\t.ext_ops_count = ARRAY_SIZE({ext_ops}),')
+ writer.line('};')
+ # The one symbol an inheriting family's module needs of this one.
+ writer.line(f'EXPORT_SYMBOL_NS_GPL({names.descriptor()}, "CTLV");')
+
+
+def _parent_descriptor(family: semantics.Family) -> str | None:
+ """The direct parent descriptor, none only for the root family."""
+ if family.parent is None:
+ return None
+ return cgen.Names(family.chain[-2]).descriptor()
diff --git a/tools/ctlv/ctlvgen/lib/gen_kernel.py b/tools/ctlv/ctlvgen/lib/gen_kernel.py
new file mode 100644
index 000000000000..f4721013693c
--- /dev/null
+++ b/tools/ctlv/ctlvgen/lib/gen_kernel.py
@@ -0,0 +1,576 @@
+# SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+"""The kernel metadata of one family.
+
+Each family describes only what it declares. Each op request and reply becomes
+one flat, breadth-first array of struct ctlv_node_schema the core walks by
+index.
+"""
+
+from __future__ import annotations
+
+import dataclasses
+import re
+
+from . import cgen
+from . import gen_family
+from . import semantics
+from . import spec
+
+
+def root_macro(*names: str) -> str:
+ """Return a constant of the root family's generated UAPI header."""
+ return cgen.macro(spec.CORE_FAMILY_NAME, *names)
+
+
+FD_ACCESSES: dict[str, str] = {access: root_macro('op-fd-access', access)
+ for access in spec.FD_ACCESSES}
+
+AVAILABILITIES: dict[str, str] = {
+ availability: root_macro('op-availability', availability)
+ for availability in spec.AVAILABILITIES
+}
+
+# Lengths that have to fit the inline message limit.
+INLINE_FIELDS: tuple[str, ...] = (
+ 'request_max_len', 'action_reply_max_len', 'query_reply_min_len',
+ 'query_reply_suggested_len')
+
+# The limits a spec writes by name, as <linux/limits.h> spells them.
+LIMITS: dict[int, str] = {value: cgen.macro(name)
+ for name, value in spec.NAMED_NUMBERS.items()}
+
+_USES_LIMITS: re.Pattern[str] = re.compile(
+ r'\b[SU](?:8|16|32|64)_(?:MIN|MAX)\b')
+_USES_SIZES: re.Pattern[str] = re.compile(r'\bSZ_[0-9]+[KMG]\b')
+
+
+@dataclasses.dataclass(frozen=True)
+class FlatNode:
+ """One node of a flattened tree, with its children as an index range."""
+
+ node: semantics.Node
+ first_child: int
+ children_count: int
+ required_children_count: int
+
+
+def flatten(root: semantics.Node) -> tuple[FlatNode, ...]:
+ """Return the nodes of one tree breadth first, the root first.
+
+ A re-entering node points back at nodes already laid out, which is what
+ keeps the array finite.
+ """
+ flat = []
+ levels: dict[str, tuple[int, int, int]] = {}
+ queue = [(root, levels)]
+ next_index = 1
+ while queue:
+ node, levels = queue.pop(0)
+ if node.reenters is not None:
+ first_child, children_count, required_count = levels[node.reenters]
+ flat.append(FlatNode(node=node, first_child=first_child,
+ children_count=children_count,
+ required_children_count=required_count))
+ continue
+ children = node.children()
+ level = (next_index if children else 0, len(children),
+ len(node.required_children))
+ flat.append(FlatNode(node=node, first_child=level[0],
+ children_count=level[1],
+ required_children_count=level[2]))
+ next_index += len(children)
+ if node.expands:
+ levels = levels | {node.expands: level}
+ queue += [(child, levels) for child in children]
+ return tuple(flat)
+
+
+def ext_schemas(
+ root: semantics.Node) -> tuple[tuple[int, semantics.Extension], ...]:
+ """Return each extension point schema with the node index it joins."""
+ schemas = []
+ for index, flat in enumerate(flatten(root)):
+ for extension in flat.node.extensions:
+ schemas.append((index, extension))
+ return tuple(schemas)
+
+
+def render_header(family: semantics.Family, *, family_api: bool) -> str:
+ """Return one family's generated kernel header."""
+ names = cgen.Names(family)
+ cgen.symbols(family)
+ guard = names.kernel_guard()
+ local = family.local_ops
+
+ writer = cgen.preamble(family, 'kernel', 'header')
+ what = 'Kernel metadata and family API' if family_api else \
+ 'Schema metadata'
+ writer.comment(f'{what} of the {family.name} CTLV family.')
+ writer.line()
+ writer.open_guard(guard)
+ writer.includes(*_header_includes(family, names, family_api))
+
+ _extensions(writer, names, family)
+ if local:
+ writer.comment('Ops this family declares, in declaration order.')
+ writer.declaration('extern const struct ctlv_op_schema',
+ f'{names.op_schemas()}[];')
+ writer.line()
+ writer.declaration('extern const struct ctlv_family_schema',
+ f'{names.schema()};')
+ writer.line()
+ _message_decls(writer, names, family)
+ if family_api:
+ gen_family.emit_header(writer, names, family)
+ writer.close_guard(guard)
+ return writer.text()
+
+
+def _extensions(writer: cgen.Writer, names: cgen.Names,
+ family: semantics.Family) -> None:
+ """The ID of each extension point this family declares."""
+ own = family.local_extensions
+ if own:
+ writer.comment('Extension points, as a descendant\'s fill names '
+ 'one.')
+ for extension in own:
+ writer.define(names.extension(extension), str(extension.ext_id))
+ writer.line()
+ enums = family.local_enum_extensions
+ if enums:
+ writer.comment('Enum extension points, numbered apart from the '
+ 'others.')
+ for enum_extension in enums:
+ writer.define(names.enum_extension(enum_extension),
+ str(enum_extension.ext_id))
+ writer.line()
+
+
+def _message_decls(writer: cgen.Writer, names: cgen.Names,
+ family: semantics.Family) -> None:
+ """The messages the core builds itself, and the room each one needs."""
+ for message in family.messages:
+ writer.comment(f'The {message.name} message, built by the core.')
+ writer.define(names.message_max_len(message), str(message.max_len))
+ writer.line()
+ writer.declaration('extern const struct ctlv_schema_tree',
+ f'{names.message_schema(message)};')
+ writer.line()
+
+
+def _header_includes(family: semantics.Family, names: cgen.Names,
+ family_api: bool) -> tuple[str, ...]:
+ """The generated types, constants and direct parent API."""
+ headers = ['<linux/ctlv_family.h>']
+ parent = []
+ if family.parent not in (None, spec.CORE_FAMILY_NAME):
+ # Its extension IDs, which this family's fills name.
+ parent = ['', cgen.Names(family.chain[-2]).kernel_header()]
+ if not family_api:
+ return tuple(headers + parent)
+ headers += ['<linux/err.h>', '<linux/errno.h>', '',
+ names.uapi_header(in_kernel=True)]
+ return tuple(headers + parent)
+
+
+def render_source(family: semantics.Family, *, family_api: bool) -> str:
+ """Return one family's generated kernel source."""
+ names = cgen.Names(family)
+ cgen.symbols(family)
+ trees = _trees(family)
+
+ body = cgen.Writer()
+ for op, which, root in trees:
+ _node_schemas(body, family, names, names.node_schemas(op, which),
+ root)
+ _ext_schemas(body, names, op, which, root)
+ _ext_fill_schemas(body, family, names)
+ _enum_fill_schemas(body, family, names)
+ _messages(body, family, names)
+ _table(body, names, family)
+ _descriptor(body, names, family)
+ _uevent(body, family)
+ _limits(body, family, trees)
+ if family_api:
+ if family.local_ops:
+ body.line()
+ gen_family.emit_source(body, names, family)
+ text = body.text()
+
+ writer = cgen.preamble(family, 'kernel', 'source')
+ if family_api:
+ writer.line('#define _CTLV_GENERATED_METADATA_ONLY')
+ writer.includes(*_includes(names, family, family_api, text))
+ return f'{writer.text()}\n{text}'
+
+
+_Tree = tuple[semantics.Op, str, semantics.Node]
+
+
+def _trees(family: semantics.Family) -> list[_Tree]:
+ """Every tree of every local op, in the order they are emitted."""
+ trees = []
+ for op in family.local_ops:
+ if op.request is not None:
+ trees.append((op, 'request', op.request))
+ if op.reply is not None:
+ trees.append((op, 'reply', op.reply))
+ return trees
+
+
+def _roots(family: semantics.Family,
+ trees: list[_Tree]) -> list[semantics.Node]:
+ """Every emitted tree root: an op's, a fill's and a core message's."""
+ return [root for _, _, root in trees] + \
+ [fill.root for fill in family.fills] + \
+ [message.root for message in family.messages]
+
+
+def _includes(names: cgen.Names, family: semantics.Family, family_api: bool,
+ body: str) -> list[str]:
+ headers = ['<linux/array_size.h>', '<linux/build_bug.h>',
+ '<linux/ctlv_family.h>']
+ if family_api:
+ headers.append('<linux/export.h>')
+ # The root family's enums name op types, availabilities and blob backings.
+ uapi = [cgen.uapi_header(spec.CORE_FAMILY_NAME, in_kernel=True)]
+ if family.name != spec.CORE_FAMILY_NAME:
+ uapi.append(names.uapi_header(in_kernel=family_api))
+ if 'BIT(' in body:
+ # The mask of permitted blob backings.
+ headers.append('<linux/bits.h>')
+ if _USES_LIMITS.search(body):
+ headers.append('<linux/limits.h>')
+ if _USES_SIZES.search(body):
+ headers.append('<linux/sizes.h>')
+ groups = [sorted(headers), uapi, [names.kernel_header()]]
+ lines: list[str] = []
+ for group in groups:
+ if not group:
+ continue
+ if lines:
+ lines.append('')
+ lines += group
+ return lines
+
+
+def _node_schemas(writer: cgen.Writer, family: semantics.Family,
+ names: cgen.Names, symbol: str,
+ root: semantics.Node) -> None:
+ writer.declaration('static const struct ctlv_node_schema',
+ f'{symbol}[] = {{')
+ for flat in flatten(root):
+ _node(writer, family, names, flat)
+ writer.line('};')
+ writer.line()
+
+
+def _node(writer: cgen.Writer, family: semantics.Family, names: cgen.Names,
+ flat: FlatNode) -> None:
+ macro, args = _node_initializer(family, names, flat)
+ if macro == 'CTLV_NODE_SCHEMA_BLOB':
+ _blob_node(writer, macro, args)
+ else:
+ writer.parenthesized(f'\t{macro}(', args, '),')
+
+
+def _blob_node(writer: cgen.Writer, macro: str, args: tuple[str, ...]) -> None:
+ opening = f'\t{macro}('
+
+ for line in cgen.wrapped(opening, args[:4], ','):
+ writer.line(line)
+ column = cgen.width(opening)
+ indent = '\t' * (column // cgen.TAB_WIDTH) + \
+ ' ' * (column % cgen.TAB_WIDTH)
+ backings = args[4].split(' | ')
+ for backing in backings[:-1]:
+ writer.line(f'{indent}{backing} |')
+ writer.line(f'{indent}{backings[-1]},')
+ writer.line(f'{indent}{args[5]}),')
+
+
+def _node_initializer(family: semantics.Family, names: cgen.Names,
+ flat: FlatNode) -> tuple[str, tuple[str, ...]]:
+ node = flat.node
+
+ if node.is_root:
+ return 'CTLV_NODE_SCHEMA_ROOT', (
+ str(flat.first_child),
+ str(flat.children_count),
+ str(flat.required_children_count),
+ )
+
+ decl = node.child_field.decl
+ kind = cgen.KINDS[decl.kind].removeprefix('CTLV_SCHEMA_')
+ macro = f'CTLV_NODE_SCHEMA_{kind}'
+ occurrences = str(node.max_occurrences) if node.max_occurrences else \
+ 'CTLV_NODE_SCHEMA_UNLIMITED'
+ args = [names.field(node.child_field), occurrences]
+ if decl.kind == 'record':
+ if node.reenters is not None:
+ macro += '_REENTERS'
+ args += [str(flat.first_child), str(flat.children_count),
+ str(flat.required_children_count)]
+ else:
+ if decl.kind in ('uint', 'sint') and decl.unit is not None:
+ macro += '_UNIT'
+ if decl.kind == 'enum':
+ assert decl.ref is not None
+ definition = family.definitions[decl.ref]
+ if definition.header:
+ macro += '_RANGE'
+ elif definition.extension is not None:
+ macro += '_EXT'
+ args += _constraint_args(family, names, decl)
+ return macro, tuple(args)
+
+
+def _ext_schemas(writer: cgen.Writer, names: cgen.Names, op: semantics.Op,
+ which: str, root: semantics.Node) -> None:
+ """The extension points of one tree, by the node they widen."""
+ schemas = ext_schemas(root)
+ if not schemas:
+ return
+ writer.comment('Extension points selected by this tree.')
+ writer.declaration('static const struct ctlv_ext_schema',
+ f'{names.ext_schemas(op, which)}[] = {{')
+ for index, extension in schemas:
+ macro = 'CTLV_EXT_SCHEMA_OPTIONAL' if extension.optional else \
+ 'CTLV_EXT_SCHEMA'
+ args = (cgen.family_const(names.effective.owners[extension.family_id]),
+ names.extension(extension), str(index))
+ writer.parenthesized(f'\t{macro}(', args, '),')
+ writer.line('};')
+ writer.line()
+
+
+def _ext_fill_schemas(writer: cgen.Writer, family: semantics.Family,
+ names: cgen.Names) -> None:
+ """The members this family puts into the extension points it inherited."""
+ if not family.fills:
+ return
+ for fill in family.fills:
+ _node_schemas(writer, family, names,
+ names.fill_node_schemas(fill), fill.root)
+ writer.comment('The extension points this family fills.')
+ writer.declaration('static const struct ctlv_ext_fill_schema',
+ f'{names.ext_fill_schemas()}[] = {{')
+ for fill in family.fills:
+ extension = family.extensions[fill.name]
+ args = (cgen.family_const(family.owners[fill.family_id]),
+ names.extension(extension), cgen.ident(fill.name))
+ writer.parenthesized('\tCTLV_EXT_FILL_SCHEMA(', args, '),')
+ writer.line('};')
+ writer.line()
+
+
+def _enum_fill_schemas(writer: cgen.Writer, family: semantics.Family,
+ names: cgen.Names) -> None:
+ """How many values this family gives each enum extension point."""
+ fills = family.enum_fills
+ if not fills:
+ return
+ writer.comment('The enum extension points this family fills.')
+ writer.declaration('static const struct ctlv_enum_fill_schema',
+ f'{names.enum_fill_schemas()}[] = {{')
+ for fill in fills:
+ assert fill.extension is not None
+ args = (cgen.family_const(family.owners[fill.extension.family_id]),
+ names.enum_extension(fill.extension), str(fill.max_value))
+ writer.parenthesized('\tCTLV_ENUM_FILL_SCHEMA(', args, '),')
+ writer.line('};')
+ writer.line()
+
+
+def _messages(writer: cgen.Writer, family: semantics.Family,
+ names: cgen.Names) -> None:
+ """The messages the core builds where no op declared one."""
+ for message in family.messages:
+ _node_schemas(writer, family, names,
+ names.message_node_schemas(message), message.root)
+ writer.declaration('const struct ctlv_schema_tree',
+ f'{names.message_schema(message)} = {{')
+ node_schemas = names.message_node_schemas(message)
+ writer.line(f'\t.node_schemas = {node_schemas},')
+ writer.line('\t.node_schemas_count =')
+ writer.line(f'\t\tARRAY_SIZE({node_schemas}),')
+ writer.line('};')
+ writer.line()
+ writer.assertion(names.message_max_len(message), '<=', message.limit)
+ writer.line()
+
+
+def _constraint_args(family: semantics.Family, names: cgen.Names,
+ decl: spec.FieldDecl) -> list[str]:
+ """The schema kind's arguments of one node-schema macro."""
+ if decl.kind in ('uint', 'sint'):
+ assert decl.min is not None and decl.max is not None
+ args = [_bound(decl.kind, decl.min), _bound(decl.kind, decl.max)]
+ if decl.unit is not None:
+ args += [f'CTLV_UNIT_{cgen.macro(decl.unit)}', str(decl.scale)]
+ return args
+ if decl.kind == 'enum':
+ assert decl.ref is not None
+ definition = family.definitions[decl.ref]
+ if definition.header:
+ return list(cgen.header_bounds(definition))
+ if definition.extension is not None:
+ extension = definition.extension
+ owner = names.effective.owners[extension.family_id]
+ return [cgen.family_const(owner),
+ names.enum_extension(extension)]
+ return [str(max(definition.numbers()))]
+ if decl.kind == 'flags':
+ assert decl.ref is not None
+ return [names.value_mask(family.definitions[decl.ref])]
+ if decl.kind in ('string', 'binary'):
+ assert decl.min_length is not None and decl.max_length is not None
+ return [_length(decl.min_length), _length(decl.max_length)]
+ if decl.kind == 'blob':
+ assert decl.blob is not None
+ return _blob_args(decl.blob)
+ return []
+
+
+def _bound(kind: str, value: int) -> str:
+ """One integer bound, a limit by the name the spec gives it."""
+ if value in LIMITS:
+ return LIMITS[value]
+ if kind == 'uint' and value > spec.S64_MAX:
+ return f'{value}ULL'
+ return str(value)
+
+
+def _length(value: int) -> str:
+ """One byte count, as <linux/sizes.h> spells a power of two."""
+ # Its largest G name is SZ_512G.
+ if 1 << 10 <= value <= 1 << 39 and not value & (value - 1):
+ return f'SZ_{spec.size_text(value)}'
+ if value in (spec.U32_MAX, spec.U64_MAX):
+ return LIMITS[value]
+ if value > spec.S64_MAX:
+ return f'{value}ULL'
+ return str(value)
+
+
+def _blob_args(blob: spec.BlobDecl) -> list[str]:
+ direction = root_macro('blob-direction', blob.direction)
+ return [_length(blob.max_length), _length(blob.alignment),
+ _backings(blob.backings), direction]
+
+
+def _backings(backings: tuple[str, ...]) -> str:
+ terms = [f'BIT({root_macro("blob-type", backing)})'
+ for backing in backings]
+ return ' | '.join(terms)
+
+
+def _table(writer: cgen.Writer, names: cgen.Names,
+ family: semantics.Family) -> None:
+ local = family.local_ops
+ if not local:
+ return
+ writer.comment('The ops this family declares, in declaration order.')
+ writer.declaration('const struct ctlv_op_schema',
+ f'{names.op_schemas()}[] = {{')
+ for op in local:
+ macro, args = _op_schema_initializer(names, op)
+ writer.parenthesized(f'\t{macro}(', args, '),')
+ writer.line('};')
+ writer.line()
+
+
+def _op_schema_initializer(names: cgen.Names,
+ op: semantics.Op) -> tuple[str, tuple[str, ...]]:
+ sizes = op.sizes
+ if op.type == 'action':
+ macro = 'CTLV_OP_SCHEMA_ACTION'
+ args = [names.op_id(op), FD_ACCESSES[op.fd_access],
+ AVAILABILITIES[op.availability],
+ str(sizes.request_max_len),
+ str(sizes.action_reply_max_len),
+ _op_tree(op, 'request'), _op_tree(op, 'reply')]
+ else:
+ macro = 'CTLV_OP_SCHEMA_QUERY'
+ args = [names.op_id(op), FD_ACCESSES[op.fd_access],
+ AVAILABILITIES[op.availability],
+ str(sizes.request_max_len),
+ str(sizes.query_reply_min_len),
+ str(sizes.query_reply_suggested_len),
+ _op_tree(op, 'request'), _op_tree(op, 'reply')]
+ if op.optional:
+ args.append('.optional = true')
+ if op.pre_post:
+ args.append('.pre_post = true')
+ return macro, tuple(args)
+
+
+def _op_tree(op: semantics.Op, which: str) -> str:
+ root = getattr(op, which)
+ if root is None:
+ return 'CTLV_SCHEMA_TREE_NONE()'
+ macro = 'CTLV_SCHEMA_TREE_EXTS' if ext_schemas(root) else \
+ 'CTLV_SCHEMA_TREE'
+ return f'{macro}({cgen.ident(op.name, which)})'
+
+
+def _descriptor(writer: cgen.Writer, names: cgen.Names,
+ family: semantics.Family) -> None:
+ """This family's node of the schema graph."""
+ writer.comment('The family ID is its position in the central registry.')
+ writer.declaration('const struct ctlv_family_schema',
+ f'{names.schema()} = {{')
+ writer.line(f'\t.name = "{family.name}",')
+ writer.line(f'\t.family_id = {family.family_id},')
+ if family.local_ops:
+ writer.line(f'\t.ops = {names.op_schemas()},')
+ writer.line(f'\t.ops_count = ARRAY_SIZE({names.op_schemas()}),')
+ if family.fills:
+ ext_fill_schemas = names.ext_fill_schemas()
+ writer.line(f'\t.ext_fill_schemas = {ext_fill_schemas},')
+ writer.line(f'\t.ext_fill_schemas_count = '
+ f'ARRAY_SIZE({ext_fill_schemas}),')
+ if family.enum_fills:
+ enum_fill_schemas = names.enum_fill_schemas()
+ writer.line(f'\t.enum_fill_schemas = {enum_fill_schemas},')
+ writer.line(f'\t.enum_fill_schemas_count = '
+ f'ARRAY_SIZE({enum_fill_schemas}),')
+ writer.line('};')
+ writer.line()
+
+
+def _uevent(writer: cgen.Writer, family: semantics.Family) -> None:
+ """What this chain's names cost a device's uevent environment."""
+ names = [item.name for item in family.chain]
+ variables = [f'CTLV_FAMILY={names[-1]}']
+ variables += [f'CTLV_COMPATIBLE_{index}={name}'
+ for index, name in enumerate(names)]
+ variables.append(f'CTLV_COMPATIBLE_N={len(names)}')
+ # "KEY=value" plus the NUL, as add_uevent_var() charges it.
+ writer.comment('Maximum uevent cost for this family chain.')
+ writer.line(f'static_assert({sum(len(item) + 1 for item in variables)} <= '
+ 'CTLV_UEVENT_MAX_LEN);')
+ writer.line(f'static_assert({len(variables)} <= CTLV_UEVENT_MAX_VARS);')
+ writer.line()
+
+
+def _limits(writer: cgen.Writer, family: semantics.Family,
+ trees: list[tuple[semantics.Op, str, semantics.Node]]) -> None:
+ local = family.local_ops
+ if not local:
+ return
+ writer.comment('Guards for generated maxima.')
+ inline = max(getattr(op.sizes, field)
+ for op in local for field in INLINE_FIELDS)
+ writer.line(f'static_assert({inline} <= CTLV_MAX_INLINE_MESSAGE_LEN);')
+ depth = max(_record_depth(root) for root in _roots(family, trees))
+ if depth:
+ writer.line(f'static_assert({depth} <= '
+ 'CTLV_MAX_DECLARED_RECORD_DEPTH);')
+
+
+def _record_depth(node: semantics.Node) -> int:
+ """Deepest record below node, which is itself at depth zero."""
+ depths = [1 + _record_depth(child) for child in node.children()
+ if child.child_field.decl.kind == 'record']
+ return max(depths, default=0)
diff --git a/tools/ctlv/ctlvgen/lib/gen_uapi.py b/tools/ctlv/ctlvgen/lib/gen_uapi.py
new file mode 100644
index 000000000000..25f9a21d0feb
--- /dev/null
+++ b/tools/ctlv/ctlvgen/lib/gen_uapi.py
@@ -0,0 +1,182 @@
+# SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+"""The UAPI header of one family.
+
+Names for numbers: the type of every field and the ID of every op, complete,
+enum values, flag bits and units. The message format is the transport
+header's. An inherited declaration is not repeated, already having a name in
+the header of the family that owns it.
+"""
+
+from __future__ import annotations
+
+import textwrap
+from collections.abc import Iterable
+
+from . import cgen
+from . import semantics
+from . import spec
+
+
+def render(family: semantics.Family) -> str:
+ """Return the UAPI header of family as text."""
+ names = cgen.Names(family)
+ cgen.symbols(family)
+ guard = names.uapi_guard()
+
+ writer = cgen.preamble(family, 'uapi', 'header')
+ _file_comment(writer, family)
+ writer.line()
+ writer.open_guard(guard)
+ writer.includes(*_includes(family))
+
+ _definitions(writer, family, names)
+ _records(writer, family, names)
+ _ops(writer, family, names)
+
+ writer.close_guard(guard)
+ return writer.text()
+
+
+def render_families(registry: spec.Registry) -> str:
+ """Return the family-number header of one registry."""
+ guard = '_UAPI_CTLV_CTLV_FAMILIES_GEN_H'
+ values = [(cgen.family_const(entry.name), family_id, None)
+ for family_id, entry in enumerate(registry.entries)]
+ writer = cgen.preamble(registry, 'uapi', 'header', registry.path)
+
+ writer.comment('CTLV family numbers.')
+ writer.line()
+ writer.open_guard(guard)
+ _numbered_enum(writer, '_ctlv_family', values)
+ writer.line()
+ writer.close_guard(guard)
+ return writer.text()
+
+
+def _includes(family: semantics.Family) -> list[str]:
+ headers = ['<ctlv/ctlv.h>', cgen.families_uapi_header()]
+ if family.parent:
+ # Inherited constants live in the parent's header, so one include is
+ # enough for the whole effective schema.
+ headers.append(cgen.uapi_header(family.parent))
+ own = family.local_definitions
+ if any(definition.kind == 'flags' for definition in own):
+ # _BITULL(), which is the form of BIT_ULL() that userspace has.
+ headers.append('<linux/const.h>')
+ headers += [f'<{definition.header.path}>' for definition in own
+ if definition.header]
+ return sorted(set(headers))
+
+
+def _file_comment(writer: cgen.Writer, family: semantics.Family) -> None:
+ paragraphs = [
+ f'Constants of the {family.name} CTLV family. Types and op IDs are '
+ 'complete; the message format carrying them is in <ctlv/ctlv.h>.',
+ ]
+ if family.parent:
+ paragraphs.append(
+ f'The inherited {family.parent} header is included here, so one '
+ 'include covers the whole effective schema.')
+ lines: list[str] = []
+ for paragraph in paragraphs:
+ if lines:
+ lines.append('')
+ lines += textwrap.wrap(paragraph, cgen.COMMENT_WIDTH,
+ break_on_hyphens=False)
+ writer.comment(*lines)
+
+
+def _definitions(writer: cgen.Writer, family: semantics.Family,
+ names: cgen.Names) -> None:
+ for definition in family.local_definitions:
+ if definition.header or definition.is_extension_point:
+ continue
+ if definition.kind == 'enum':
+ _enum(writer, names, definition)
+ else:
+ _flags(writer, names, definition)
+ writer.line()
+
+
+def _enum(writer: cgen.Writer, names: cgen.Names,
+ definition: semantics.Definition) -> None:
+ if definition.extension is not None:
+ owner = names.effective.owners[definition.extension.family_id]
+ writer.comment(f'enum {definition.name}, an extension point of '
+ f'{owner}')
+ else:
+ writer.comment(f'enum {definition.name}')
+ values = [(names.value(definition, value_name), number, doc)
+ for (value_name, number), doc in zip(definition.values,
+ definition.value_docs)]
+ _numbered_enum(writer, names.definition(definition), values)
+
+
+def _flags(writer: cgen.Writer, names: cgen.Names,
+ definition: semantics.Definition) -> None:
+ writer.comment(f'flags {definition.name}')
+ bits = [(names.value_bit(definition, value_name), bit, None)
+ for value_name, bit in definition.values]
+ _numbered_enum(writer, names.bit_enum(definition), bits)
+ writer.line()
+ values = []
+ for (value_name, _bit), doc in zip(definition.values,
+ definition.value_docs):
+ value = names.value(definition, value_name)
+ writer.doc(doc)
+ writer.define(value,
+ f'_BITULL({names.value_bit(definition, value_name)})')
+ values.append(value)
+ _flags_mask(writer, names.value_mask(definition), values)
+
+
+def _records(writer: cgen.Writer, family: semantics.Family,
+ names: cgen.Names) -> None:
+ """One #define per field, under the record that holds it."""
+ for record in family.local_records:
+ if not record.fields:
+ continue
+ writer.comment(record.path)
+ for field in record.fields:
+ writer.doc(field.decl.doc)
+ writer.define_block(
+ names.field(field),
+ f'\tCTLV_TYPE({names.family_const()}, '
+ f'{record.record_id:#010x}, {field.number})')
+ if field.decl.unit is not None:
+ writer.define(names.field_unit(field),
+ f'CTLV_UNIT_{cgen.macro(field.decl.unit)}')
+ writer.define(names.field_scale(field), str(field.decl.scale))
+ writer.line()
+
+
+def _numbered_enum(writer: cgen.Writer, name: str,
+ values: Iterable[tuple[str, int, str | None]]) -> None:
+ # Every member says its value, so that none moves with its neighbours.
+ writer.line(f'enum {name} {{')
+ for value, number, doc in values:
+ writer.doc(doc, '\t')
+ writer.line(f'\t{value} = {number},')
+ writer.line('};')
+
+
+def _flags_mask(writer: cgen.Writer, name: str, values: list[str]) -> None:
+ if len(values) == 1:
+ writer.define(name, values[0])
+ return
+ lines = [f'\t({values[0]} |']
+ lines += [f'\t {value} |' for value in values[1:-1]]
+ lines.append(f'\t {values[-1]})')
+ writer.define_block(name, *lines)
+
+
+def _ops(writer: cgen.Writer, family: semantics.Family,
+ names: cgen.Names) -> None:
+ own = family.local_ops
+ if not own:
+ return
+ writer.comment('ops')
+ for op in own:
+ writer.define_call(names.op_id(op), 'CTLV_OP', names.family_const(),
+ str(op.local_id))
+ writer.line()
diff --git a/tools/ctlv/ctlvgen/lib/outputs.py b/tools/ctlv/ctlvgen/lib/outputs.py
new file mode 100644
index 000000000000..a295055c46b2
--- /dev/null
+++ b/tools/ctlv/ctlvgen/lib/outputs.py
@@ -0,0 +1,355 @@
+# SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+"""The complete set of generated files and its reconciliation."""
+
+from __future__ import annotations
+
+import dataclasses
+import os
+import pathlib
+import subprocess
+import tempfile
+from collections.abc import Callable, Mapping
+from typing import Optional
+
+from . import cgen
+from . import gen_kernel
+from . import gen_uapi
+from . import semantics
+from . import spec
+from . import yamlload
+from .errors import CtlvError
+
+
+FLAVOURS: dict[str, Callable[..., str]] = {
+ 'uapi-header': gen_uapi.render,
+ 'kernel-header': gen_kernel.render_header,
+ 'kernel-source': gen_kernel.render_source,
+}
+
+IDENTITIES: dict[str, tuple[str, str, str]] = {
+ 'uapi-header': ('uapi', 'header', '.h'),
+ 'kernel-header': ('kernel', 'header', '.h'),
+ 'kernel-source': ('kernel', 'source', '.c'),
+}
+
+ROOT_FLAVOURS = frozenset(FLAVOURS)
+FAMILY_FLAVOURS: frozenset[str] = frozenset(FLAVOURS)
+FIXTURE_FLAVOURS: frozenset[str] = frozenset({
+ 'uapi-header', 'kernel-header', 'kernel-source',
+})
+
+
+@dataclasses.dataclass(frozen=True)
+class Output:
+ family: str | None
+ source: pathlib.PurePosixPath
+ registry: pathlib.PurePosixPath
+ flavour: str
+ path: pathlib.PurePosixPath
+
+
+def _spec_paths(paths: spec.Paths, source: pathlib.PurePosixPath,
+ registry: pathlib.PurePosixPath) -> spec.Paths:
+ return spec.Paths(paths.kernel_dir / source, schema=paths.schema,
+ registry=paths.kernel_dir / registry)
+
+
+def _relative(where: str, value: str) -> pathlib.PurePosixPath:
+ path = pathlib.PurePosixPath(value)
+ if path.is_absolute() or '..' in path.parts or \
+ any(part.startswith('.') for part in path.parts) or \
+ str(path) != value:
+ raise CtlvError(where, f'"{value}" is not a normalized relative path')
+ return path
+
+
+def _repository_path(paths: spec.Paths,
+ path: pathlib.Path) -> pathlib.PurePosixPath:
+ return pathlib.PurePosixPath(
+ pathlib.Path(path).relative_to(paths.kernel_dir).as_posix())
+
+
+_Context = tuple[pathlib.Path, pathlib.PurePosixPath, pathlib.PurePosixPath,
+ Optional[list[str]], bool]
+
+
+def _manifest_contexts(paths: spec.Paths) -> tuple[_Context, ...]:
+ fixtures_dir = paths.kernel_dir / 'tools/ctlv/tests/specs'
+ contexts: list[_Context] = [
+ (paths.generated, _repository_path(paths, paths.registry),
+ _repository_path(paths, paths.spec_dir), None, False),
+ ]
+ manifest = fixtures_dir.parent / 'generated-files.yaml'
+ if manifest.exists():
+ fixtures = sorted(path.stem for path in fixtures_dir.glob('*.yaml'))
+ contexts.append((
+ manifest,
+ _repository_path(paths, fixtures_dir.parent / 'families.yaml'),
+ _repository_path(paths, fixtures_dir), fixtures, True))
+ return tuple(contexts)
+
+
+def load(paths: spec.Paths) -> tuple[Output, ...]:
+ """Load and validate every generated-file manifest."""
+ outputs = []
+ families = set()
+ destinations = set()
+ for manifest, expected_registry, source_dir, expected_names, fixture in \
+ _manifest_contexts(paths):
+ data = yamlload.load(manifest)
+ spec._validate_syntax(manifest, data, paths.generated_schema)
+ registry_record = data['registry']
+ where = f'{manifest}: registry'
+ registry_source = _relative(where, registry_record['source'])
+ if registry_source != expected_registry:
+ raise CtlvError(where, f'source is {registry_source}, expected '
+ f'{expected_registry}')
+ registry = spec.load_registry(
+ paths, paths.kernel_dir / registry_source)
+ if expected_names is None:
+ expected_names = [entry.name for entry in registry.entries]
+ records = data['families']
+ if [record['name'] for record in records] != expected_names:
+ raise CtlvError(str(manifest),
+ 'manifest families do not match their sources')
+
+ for flavour, value in registry_record['outputs'].items():
+ destination = _relative(f'{where} {flavour}', value)
+ suffix = IDENTITIES[flavour][2]
+ if destination.suffix != suffix:
+ raise CtlvError(f'{where} {flavour}',
+ f'output must end in {suffix}')
+ if destination in destinations:
+ raise CtlvError(f'{where} {flavour}',
+ f'duplicate output path {destination}')
+ destinations.add(destination)
+ outputs.append(Output(None, registry_source, registry_source,
+ flavour, destination))
+
+ for record in records:
+ name = record['name']
+ where = f'{manifest}: family {name}'
+ if name in families:
+ raise CtlvError(where, 'duplicate family')
+ families.add(name)
+
+ expected_source = source_dir / f'{name}.yaml'
+ source = _relative(where, record['source'])
+ if source != expected_source:
+ raise CtlvError(where, f'source is {source}, expected '
+ f'{expected_source}')
+
+ required = FIXTURE_FLAVOURS if fixture else FAMILY_FLAVOURS
+ if not fixture and name == spec.CORE_FAMILY_NAME:
+ required = ROOT_FLAVOURS
+ flavours = frozenset(record['outputs'])
+ if flavours != required:
+ missing = sorted(required - flavours)
+ extra = sorted(flavours - required)
+ detail = []
+ if missing:
+ detail.append(f'missing {", ".join(missing)}')
+ if extra:
+ detail.append(f'unexpected {", ".join(extra)}')
+ raise CtlvError(where, '; '.join(detail))
+
+ family = semantics.load(
+ _spec_paths(paths, source, registry_source))
+ if family.name != name:
+ raise CtlvError(where, f'source declares family '
+ f'{family.name}')
+ for flavour, value in record['outputs'].items():
+ destination = _relative(f'{where} {flavour}', value)
+ suffix = IDENTITIES[flavour][2]
+ if destination.suffix != suffix:
+ raise CtlvError(f'{where} {flavour}',
+ f'output must end in {suffix}')
+ if destination in destinations:
+ raise CtlvError(f'{where} {flavour}',
+ f'duplicate output path {destination}')
+ destinations.add(destination)
+ outputs.append(Output(name, source, registry_source, flavour,
+ destination))
+ return tuple(outputs)
+
+
+def render(paths: spec.Paths,
+ plan: tuple[Output, ...]) -> dict[pathlib.PurePosixPath, str]:
+ """Render every planned output before any destination is changed."""
+ families: dict[tuple[pathlib.PurePosixPath, pathlib.PurePosixPath],
+ semantics.Family] = {}
+ rendered = {}
+ registered_registry = _repository_path(paths, paths.registry)
+ for output in plan:
+ source = _output_source(output)
+ if output.family is None:
+ registry = spec.load_registry(
+ paths, paths.kernel_dir / output.registry)
+ text = gen_uapi.render_families(registry)
+ else:
+ key = (output.source, output.registry)
+ family = families.get(key)
+ if family is None:
+ family = semantics.load(
+ _spec_paths(paths, output.source, output.registry))
+ families[key] = family
+ renderer = FLAVOURS[output.flavour]
+ if output.flavour in ('kernel-header', 'kernel-source'):
+ text = renderer(
+ family, family_api=output.registry == registered_registry)
+ else:
+ text = renderer(family)
+ mode, file, _ = IDENTITIES[output.flavour]
+ if _identity(text) != (source, mode, file):
+ raise CtlvError(str(output.path),
+ 'generator emitted the wrong output marker')
+ rendered[output.path] = text
+ return rendered
+
+
+def _output_source(output: Output) -> str:
+ return output.source.as_posix()
+
+
+def _marked_files(root: pathlib.Path) -> set[str]:
+ if (root / '.git').exists():
+ result = subprocess.run(
+ ['git', 'grep', '--untracked', '--files-with-matches',
+ 'CTLV-GEN', '--', '*.c', '*.h', '*.rst'],
+ cwd=root, capture_output=True, text=True, check=False)
+ if result.returncode not in (0, 1):
+ raise CtlvError(str(root), result.stderr.strip())
+ candidates = (root / line for line in result.stdout.splitlines())
+ else:
+ candidates = (path for suffix in ('*.c', '*.h', '*.rst')
+ for path in root.rglob(suffix))
+
+ marked = set()
+ for path in candidates:
+ if path.is_symlink() or not path.is_file():
+ continue
+ text = path.read_text(encoding='utf-8', errors='replace')
+ if _identity(text) is not None:
+ marked.add(path.relative_to(root).as_posix())
+ return marked
+
+
+def _destination(root: pathlib.Path,
+ relative: pathlib.PurePosixPath) -> pathlib.Path:
+ destination = root.joinpath(*relative.parts)
+ if destination.is_symlink():
+ raise CtlvError(str(destination),
+ 'generated output cannot be a symlink')
+ parent = destination.parent
+ while parent != root and not parent.exists():
+ parent = parent.parent
+ if parent.is_symlink() or root.resolve() not in (
+ parent.resolve(), *parent.resolve().parents):
+ raise CtlvError(str(destination),
+ 'generated output escapes its output root')
+ return destination
+
+
+def _replace(destination: pathlib.Path, text: str) -> None:
+ destination.parent.mkdir(parents=True, exist_ok=True)
+ fd, temporary = tempfile.mkstemp(dir=destination.parent,
+ prefix=f'.{destination.name}.')
+ try:
+ with os.fdopen(fd, 'w', encoding='utf-8') as stream:
+ stream.write(text)
+ os.chmod(temporary, 0o644)
+ os.replace(temporary, destination)
+ finally:
+ if os.path.exists(temporary):
+ os.unlink(temporary)
+
+
+def _source(line: str, prefix: str) -> str | None:
+ """The spec a 'Generated from <spec> by' line names, or None."""
+ if not line.startswith(prefix) or not line.endswith(' by'):
+ return None
+ return line[len(prefix):-len(' by')]
+
+
+def _identity(text: str) -> tuple[str | None, str, str] | None:
+ """The source, mode and file a generated file's preamble names."""
+ lines = text.splitlines()
+ if len(lines) < 6:
+ return None
+ c_spdx = (lines[0].startswith('// SPDX-License-Identifier: ') or
+ (lines[0].startswith('/* SPDX-License-Identifier: ') and
+ lines[0].endswith(' */')))
+ if c_spdx and lines[1] == '/*' and \
+ _source(lines[2], ' * Generated from ') and \
+ lines[3] == f' * {cgen.REGEN}. Do not edit.' and \
+ lines[4] == ' */' and \
+ lines[5].startswith('/* CTLV-GEN ') and lines[5].endswith(' */'):
+ source = _source(lines[2], ' * Generated from ')
+ fields = lines[5][3:-3].split()
+ elif lines[0].startswith('.. SPDX-License-Identifier: ') and \
+ _source(lines[1], '.. Generated from ') and \
+ lines[2] == f' {cgen.REGEN}. Do not edit.' and \
+ lines[3].startswith('.. CTLV-GEN '):
+ source = _source(lines[1], '.. Generated from ')
+ fields = lines[3][3:].split()
+ else:
+ return None
+ if len(fields) == 3 and fields[0] == 'CTLV-GEN':
+ return source, fields[1], fields[2]
+ return None
+
+
+def reconcile(paths: spec.Paths, output_root: pathlib.Path | str | None = None,
+ check: bool = False,
+ extra: Mapping[pathlib.PurePosixPath, str] | None = None
+ ) -> tuple[str, ...]:
+ """Check or atomically update every generated output.
+
+ extra are files without a marker, checked and written with the rest.
+ """
+ plan = load(paths)
+ rendered = render(paths, plan)
+ rendered.update(extra or {})
+ root = pathlib.Path(paths.kernel_dir if output_root is None
+ else output_root)
+ if not root.is_dir():
+ raise CtlvError(str(root), 'output root is not a directory')
+ expected = {output.path.as_posix() for output in plan}
+ unexpected = sorted(_marked_files(root) - expected)
+ if unexpected:
+ raise CtlvError('generated outputs',
+ 'unexpected files\n' +
+ '\n'.join(f' {path}' for path in unexpected))
+
+ states = {}
+ problems = []
+ unsafe = []
+ by_path = {output.path: output for output in plan}
+ for relative, text in rendered.items():
+ destination = _destination(root, relative)
+ if not destination.exists():
+ problems.append(f'missing {relative}')
+ states[relative] = (destination, text)
+ continue
+ if not destination.is_file():
+ raise CtlvError(str(destination),
+ 'generated output is not a regular file')
+ current = destination.read_text(encoding='utf-8')
+ if current == text:
+ continue
+ problems.append(f'stale {relative}')
+ output = by_path.get(relative)
+ if output is not None and not check:
+ mode, file, _ = IDENTITIES[output.flavour]
+ if _identity(current) != (_output_source(output), mode, file):
+ unsafe.append(f'{relative} is not the planned generated file')
+ states[relative] = (destination, text)
+ if unsafe:
+ raise CtlvError('generated outputs',
+ 'refusing to overwrite files\n' +
+ '\n'.join(f' {problem}' for problem in unsafe))
+ if check and problems:
+ raise CtlvError('generated outputs', '\n'.join(problems))
+ for destination, text in states.values():
+ _replace(destination, text)
+ return tuple(problems)
diff --git a/tools/ctlv/ctlvgen/lib/semantics.py b/tools/ctlv/ctlvgen/lib/semantics.py
new file mode 100644
index 000000000000..86725e7c1d69
--- /dev/null
+++ b/tools/ctlv/ctlvgen/lib/semantics.py
@@ -0,0 +1,1219 @@
+# SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+"""The effective form of a CTLV family.
+
+The specifications of one inheritance chain resolved into one schema: a type
+for every field from where it is declared, every record composed of its own
+fields and what it places and includes, and one tree per op request and reply.
+A rule enforced here is not re-enforced downstream.
+"""
+
+from __future__ import annotations
+
+import dataclasses
+from collections.abc import Iterable
+from typing import TypeVar
+
+from . import sizes
+from . import spec
+from .errors import CtlvError
+
+MAX_FLAG_BITS: int = 64
+
+# These root enums are stored more narrowly than a generic enum range.
+ROOT_ENUM_VALUES_MAX: dict[str, int] = {
+ 'op-type': spec.U8_MAX,
+ 'op-availability': spec.U8_MAX,
+ 'op-fd-access': spec.U8_MAX,
+ 'blob-direction': spec.U8_MAX,
+ # BIT(value) is stored in struct ctlv_node_schema.blob.backings.
+ 'blob-type': 15,
+}
+
+# Type kinds, bits 47..46 of a type, as enum ctlv_type_kind has them.
+TYPE_KIND_NAMED: int = 0
+TYPE_KIND_REQUEST: int = 2
+TYPE_KIND_REPLY: int = 3
+TYPE_KINDS: dict[str, int] = {
+ 'named': TYPE_KIND_NAMED, 'field': TYPE_KIND_NAMED,
+ 'request': TYPE_KIND_REQUEST, 'reply': TYPE_KIND_REPLY,
+}
+
+# What the 64 bits of a type hold room for.
+MAX_RECORD_INDEX: int = (1 << 14) - 1
+MAX_UNNAMED_DEPTH: int = 4
+MAX_UNNAMED_PEERS: int = 15
+MAX_FIELD_NUMBER: int = spec.U16_MAX
+
+
+def _check_count(where: str, what: str, count: int, maximum: int) -> None:
+ if count > maximum:
+ raise CtlvError(where, f'{count} {what} exceed the {maximum} maximum')
+
+
+def record_id(role: str, index: int) -> int:
+ """The record of a type: its type kind, then its index."""
+ return (TYPE_KINDS[role] << 30) | (index << 16)
+
+
+def type_of(family_id: int, record: int, number: int) -> int:
+ return (family_id << 48) | (record << 16) | number
+
+
+@dataclasses.dataclass(frozen=True)
+class EnumExtension:
+ """An enum extension point, whose values one descendant family gives."""
+
+ name: str
+ family_id: int
+ ext_id: int
+
+
+@dataclasses.dataclass(frozen=True)
+class Definition:
+ """An effective enum or flags definition, value_docs in values' order.
+
+ extension is set on an enum extension point, which has no values, and on
+ the enum filling it, which belongs to the family that fills it.
+ """
+
+ name: str
+ kind: str
+ family_id: int
+ values: tuple[tuple[str, int], ...]
+ value_docs: tuple[str | None, ...]
+ header: spec.HeaderEnum | None = None
+ doc: str | None = None
+ extension: EnumExtension | None = None
+
+ def numbers(self) -> tuple[int, ...]:
+ return tuple(number for _, number in self.values)
+
+ @property
+ def is_extension_point(self) -> bool:
+ return (self.extension is not None and
+ self.extension.family_id == self.family_id)
+
+ @property
+ def is_fill(self) -> bool:
+ return (self.extension is not None and
+ self.extension.family_id != self.family_id)
+
+ @property
+ def max_value(self) -> int:
+ """The highest value, 0 for an extension point nothing filled."""
+ return max(self.numbers(), default=0)
+
+
+@dataclasses.dataclass(frozen=True)
+class Field:
+ """A field, with its complete TLV type.
+
+ record is the name of the record declaring it, which its constant and
+ helpers take; shared marks a field of records:, which takes its own.
+ full_name says where it is, and holds is the path of the record it holds.
+ """
+
+ record: str
+ name: str
+ family_id: int
+ record_id: int
+ number: int
+ type: int
+ decl: spec.FieldDecl
+ full_name: str
+ shared: bool = False
+ holds: str | None = None
+
+ @property
+ def c_name(self) -> str:
+ return self.name if self.shared else f'{self.record}-{self.name}'
+
+
+@dataclasses.dataclass(frozen=True)
+class Member:
+ """A field as one record has it: its own, placed or included."""
+
+ field: Field
+ required: bool
+ doc: str | None = None
+
+
+@dataclasses.dataclass(frozen=True)
+class Record:
+ """A named record, a field of records:, an op's request or reply root,
+ or an unnamed record inside one of them.
+
+ role says which: named, field, request, reply or unnamed. path is
+ unique, name is what constants and helpers take. fields are the ones
+ declared here, members the whole record.
+ """
+
+ name: str
+ family_id: int
+ record_id: int
+ role: str
+ path: str
+ fields: tuple[Field, ...]
+ members: tuple[Member, ...]
+ doc: str | None = None
+
+
+@dataclasses.dataclass(frozen=True)
+class Extension:
+ """An extension point, which one descendant family's record fills."""
+
+ name: str
+ family_id: int
+ ext_id: int
+ optional: bool = False
+
+
+@dataclasses.dataclass(frozen=True)
+class Fill:
+ """The record one family puts into an extension point it inherited.
+
+ root stands for the extension point, so its children are what splices in.
+ """
+
+ name: str
+ owner_family_id: int
+ family_id: int
+ ext_id: int
+ root: Node
+ record: Record
+
+
+@dataclasses.dataclass(frozen=True)
+class Node:
+ """One node of an expanded op tree. The root has no field.
+
+ extensions are the extension points this record holds. A recursive record
+ becomes a node naming its record in expands and childless nodes below it
+ naming it in reenters, so walking children terminates.
+ """
+
+ field: Field | None
+ required: bool
+ max_occurrences: int
+ required_children: tuple[Node, ...]
+ optional_children: tuple[Node, ...]
+ extensions: tuple[Extension, ...] = ()
+ expands: str | None = None
+ reenters: str | None = None
+
+ @property
+ def is_root(self) -> bool:
+ return self.field is None
+
+ @property
+ def child_field(self) -> Field:
+ """The field of a node below the root, which every one has."""
+ assert self.field is not None
+ return self.field
+
+ def children(self) -> tuple[Node, ...]:
+ return self.required_children + self.optional_children
+
+
+@dataclasses.dataclass(frozen=True)
+class Message:
+ """One message root that no op declares, built by the core unasked.
+
+ Only the root family declares one.
+ """
+
+ name: str
+ family_id: int
+ record_id: int
+ root: Node
+ max_len: int
+ limit: str
+
+
+@dataclasses.dataclass(frozen=True)
+class Op:
+ """An effective op.
+
+ availability is the permission to enable and disable the op per device,
+ not a state. optional means a device has the op only once its family opts
+ in. pre_post asks for a pair of this op's own, inside the one the device
+ puts around every op.
+ """
+
+ name: str
+ type: str
+ fd_access: str
+ availability: str
+ optional: bool
+ pre_post: bool
+ family_id: int
+ local_id: int
+ op_id: int
+ request: Node | None
+ reply: Node | None
+ sizes: sizes.OpSizes
+ doc: str | None = None
+ request_record: Record | None = None
+ reply_record: Record | None = None
+
+
+_Owned = TypeVar('_Owned', Definition, Record, Field, Extension,
+ EnumExtension, Op)
+
+
+@dataclasses.dataclass(frozen=True)
+class Family:
+ """One effective family: itself plus everything it inherits.
+
+ owners maps a family ID to its name for every family in the chain. fills
+ is what a generator emits, chain_fills what a device answers with.
+ """
+
+ name: str
+ family_id: int
+ path: str
+ chain: tuple[spec.FamilySpec, ...]
+ owners: dict[int, str]
+ definitions: dict[str, Definition]
+ records: dict[str, Record]
+ fields: dict[str, Field]
+ extensions: dict[str, Extension]
+ ops: tuple[Op, ...]
+ fills: tuple[Fill, ...]
+ chain_fills: tuple[Fill, ...]
+ messages: tuple[Message, ...]
+ enum_extensions: dict[str, EnumExtension] = dataclasses.field(
+ default_factory=dict)
+
+ @property
+ def parent(self) -> str | None:
+ return self.chain[-1].inherits
+
+ @property
+ def license(self) -> str:
+ return self.chain[-1].license
+
+ @property
+ def doc(self) -> str | None:
+ return self.chain[-1].doc
+
+ def _local(self, items: Iterable[_Owned]) -> tuple[_Owned, ...]:
+ return tuple(item for item in items
+ if item.family_id == self.family_id)
+
+ @property
+ def local_definitions(self) -> tuple[Definition, ...]:
+ return self._local(self.definitions.values())
+
+ @property
+ def local_records(self) -> tuple[Record, ...]:
+ return self._local(self.records.values())
+
+ @property
+ def local_fields(self) -> tuple[Field, ...]:
+ return self._local(self.fields.values())
+
+ @property
+ def local_extensions(self) -> tuple[Extension, ...]:
+ return self._local(self.extensions.values())
+
+ @property
+ def local_enum_extensions(self) -> tuple[EnumExtension, ...]:
+ return self._local(self.enum_extensions.values())
+
+ @property
+ def enum_fills(self) -> tuple[Definition, ...]:
+ """The enum extension points this family fills, as it fills them."""
+ return tuple(definition for definition in self.local_definitions
+ if definition.is_fill)
+
+ @property
+ def local_ops(self) -> tuple[Op, ...]:
+ return self._local(self.ops)
+
+ def fill_of(self, extension: Extension) -> Fill | None:
+ """What this chain put into one extension point, if anything did."""
+ for fill in self.chain_fills:
+ if (fill.family_id, fill.ext_id) == (extension.family_id,
+ extension.ext_id):
+ return fill
+ return None
+
+
+def field_uses(family: Family, family_ids: set[int] | None = None
+ ) -> tuple[dict[int, Field], dict[int, Field]]:
+ """The fields read in a request and written in a reply.
+
+ Two dicts by type, in the order the fields are met, over the ops and
+ fills of the families in family_ids, the whole chain by default.
+ """
+ ids = set(family.owners) if family_ids is None else set(family_ids)
+ inputs: dict[int, Field] = {}
+ outputs: dict[int, Field] = {}
+
+ def walk(node: Node, found: dict[int, Field]) -> None:
+ for child in node.children():
+ found.setdefault(child.child_field.type, child.child_field)
+ walk(child, found)
+ for extension in node.extensions:
+ fill = family.fill_of(extension)
+ if fill is not None and fill.owner_family_id in ids:
+ walk(fill.root, found)
+
+ for op in family.ops:
+ if op.family_id not in ids:
+ continue
+ if op.request is not None:
+ walk(op.request, inputs)
+ if op.reply is not None:
+ walk(op.reply, outputs)
+ for message in family.messages:
+ if message.family_id in ids:
+ walk(message.root, outputs)
+ return inputs, outputs
+
+
+def own_uses(family: Family) -> tuple[dict[int, Field], dict[int, Field]]:
+ """The uses this family adds to its parent's, which it has helpers for.
+
+ A helper is named after the family declaring the field, and emitted by
+ the first family of the chain whose ops or fills read or write it.
+ """
+ inputs, outputs = field_uses(family)
+ parent_inputs, parent_outputs = field_uses(
+ family, set(family.owners) - {family.family_id})
+ return ({key: field for key, field in inputs.items()
+ if key not in parent_inputs},
+ {key: field for key, field in outputs.items()
+ if key not in parent_outputs})
+
+
+def load(paths: spec.Paths) -> Family:
+ """Load the family at paths.spec, resolved against its whole chain."""
+ registry = spec.load_registry(paths)
+ return _Resolver(paths, registry).resolve()
+
+
+class _Resolver:
+ def __init__(self, paths: spec.Paths, registry: spec.Registry) -> None:
+ self.paths = paths
+ self.registry = registry
+ self.definitions: dict[str, Definition] = {}
+ self.records: dict[str, Record] = {}
+ # Named records and fields of records:, by name, and where each is.
+ self.named: dict[str, Record] = {}
+ self.declared: dict[str, tuple[int, str, int, spec.RecordSpec]] = {}
+ self.fields: dict[str, Field] = {}
+ self.c_names: dict[tuple[int, str], Field] = {}
+ self.extensions: dict[str, Extension] = {}
+ self.enum_extensions: dict[str, EnumExtension] = {}
+ self.fill_records: dict[str, Record] = {}
+ self.fillers: dict[str, str] = {}
+ # Where each extension point sits: message, context, record level.
+ self.sites: dict[str, list[tuple[str, str, int]]] = {}
+ self.ops: list[Op] = []
+ self.used: set[str] = set()
+ self.building: tuple[str, ...] = ()
+
+ def resolve(self) -> Family:
+ chain = self._load_chain()
+ self._check_names(chain)
+ self._check_order(chain)
+ target = chain[-1]
+ for family in chain:
+ family_id = self._family_id(family)
+ self._add_definitions(family, family_id, family.enums, 'enum')
+ self._add_definitions(family, family_id, family.flags, 'flags')
+ self._add_records(family, family_id)
+ self._add_ops(family, family_id)
+ self._check_used(family, family_id)
+ fills = self._fills(target)
+ chain_fills = self._fills(None)
+ self._check_fills(chain_fills)
+ _check_count(f'{target.path}: family {target.name}',
+ 'extension fills', len(fills), spec.U32_MAX)
+ records = sorted(self.records.values(), key=_record_order)
+ resolved = Family(
+ name=target.name,
+ family_id=self._family_id(target),
+ path=target.path,
+ chain=chain,
+ owners={self._family_id(item): item.name for item in chain},
+ definitions=self.definitions,
+ records={record.path: record for record in records},
+ fields=self.fields,
+ extensions=self.extensions,
+ ops=tuple(self.ops),
+ fills=fills,
+ chain_fills=chain_fills,
+ messages=self._messages(target),
+ enum_extensions=self.enum_extensions,
+ )
+ _check_composed(resolved)
+ _check_docs(target)
+ return resolved
+
+ def _messages(self, family: spec.FamilySpec) -> tuple[Message, ...]:
+ """The message roots this family declares that no op uses.
+
+ The root family alone has them, and has to have them.
+ """
+ messages = []
+ for name in sizes.CORE_MESSAGES:
+ record = self.named.get(name)
+ if record is None or record.role != 'named':
+ if family.name != spec.CORE_FAMILY_NAME:
+ continue
+ raise CtlvError(f'{family.path}: family {family.name}',
+ f'the root family has to declare the "{name}" '
+ 'record, which the core builds a message of')
+ if record.family_id != self._family_id(family):
+ continue
+ where = self.declared[name][1]
+ root = self._root(record.members, where, ())
+ if _extensions_anywhere(root):
+ raise CtlvError(where, 'a message the core builds cannot '
+ 'hold an extension point')
+ self._check_subtree(where, root, 'message')
+ _check_tree_size(where, root)
+ messages.append(Message(
+ name=name,
+ family_id=record.family_id,
+ record_id=record.record_id,
+ root=root,
+ max_len=sizes.message_max_len(where, name, root),
+ limit=sizes.CORE_MESSAGES[name][0],
+ ))
+ return tuple(messages)
+
+ def _error_info_max_len(self) -> int:
+ """The longest error information the core builds, 0 without one."""
+ record = self.named.get(sizes.ERROR_INFO)
+ if record is None:
+ return 0
+ where = self.declared[record.name][1]
+ root = self._root(record.members, where, ())
+ self._check_subtree(where, root, 'message')
+ return sizes.root_max_len(root)
+
+ def _fills(self, family: spec.FamilySpec | None) -> tuple[Fill, ...]:
+ """The extension points one family fills, with what it puts in each.
+
+ Passing None asks for the fills of the whole chain.
+ """
+ fills = []
+ for name, filler in self.fillers.items():
+ if family is not None and filler != family.name:
+ continue
+ extension = self.extensions[name]
+ record = self.fill_records[name]
+ where = self.declared[name][1]
+ root = self._root(record.members, where, ())
+ _check_tree_size(where, root)
+ if _reenters_anywhere(root):
+ raise CtlvError(where, 'a fill cannot re-enter a record')
+ owner_family_id = self.registry.family_id(filler)
+ assert owner_family_id is not None
+ fills.append(Fill(
+ name=name,
+ owner_family_id=owner_family_id,
+ family_id=extension.family_id,
+ ext_id=extension.ext_id,
+ root=root,
+ record=record,
+ ))
+ return tuple(fills)
+
+ # Loading and identity
+
+ def _load_chain(self) -> tuple[spec.FamilySpec, ...]:
+ family = spec.load(self.paths.spec, self.paths.schema)
+ if self.paths.spec.stem != family.name:
+ raise CtlvError(f'{family.path}: family {family.name}',
+ 'a specification must be named after its family, '
+ f'so this file must be {family.name}.yaml')
+ chain = [family]
+ seen = [family.name]
+ self._check_root(family)
+ while chain[0].inherits:
+ parent_name = chain[0].inherits
+ child = chain[0]
+ if parent_name in seen:
+ # seen runs from the selected family to its ancestors.
+ raise CtlvError(f'{child.path}: family {child.name}',
+ 'inheritance cycle: ' +
+ ' -> '.join(seen + [parent_name]))
+ parent_path = self.paths.sibling_spec(parent_name)
+ parent = spec.load(parent_path, self.paths.schema)
+ if parent.name != parent_name:
+ raise CtlvError(f'{parent_path}: family {parent.name}',
+ f'expected family {parent_name}')
+ self._check_root(parent)
+ seen.append(parent_name)
+ chain.insert(0, parent)
+ return tuple(chain)
+
+ def _check_root(self, family: spec.FamilySpec) -> None:
+ """Require every family but the root one to name what it inherits."""
+ if family.name == spec.CORE_FAMILY_NAME:
+ if family.inherits:
+ raise CtlvError(f'{family.path}: family {family.name}',
+ 'the root family heads every chain, so it '
+ 'inherits nothing')
+ elif not family.inherits:
+ raise CtlvError(f'{family.path}: family {family.name}',
+ 'must name the family it inherits, which is the '
+ f'root {spec.CORE_FAMILY_NAME} for a family '
+ 'directly below it')
+
+ def _check_names(self, chain: tuple[spec.FamilySpec, ...]) -> None:
+ """Require every child family name to carry its whole ancestry."""
+ for parent, child in zip(chain, chain[1:]):
+ suffix = child.name.removeprefix(f'{parent.name}-')
+ if not suffix or suffix == child.name:
+ raise CtlvError(f'{child.path}: family {child.name}',
+ f'a family that inherits {parent.name} must '
+ f'be named {parent.name}-<suffix>')
+
+ def _check_order(self, chain: tuple[spec.FamilySpec, ...]) -> None:
+ """Require a parent to be registered before what inherits it.
+
+ Ascending positions let the core walk a chain once and index it.
+ """
+ for parent, child in zip(chain, chain[1:]):
+ if self._family_id(parent) >= self._family_id(child):
+ raise CtlvError(f'{child.path}: family {child.name}',
+ f'is registered before {parent.name}, the '
+ 'family it inherits')
+
+ def _family_id(self, family: spec.FamilySpec) -> int:
+ family_id = self.registry.family_id(family.name)
+ if family_id is None:
+ raise CtlvError(f'{family.path}: family {family.name}',
+ f'not registered in {self.registry.path}')
+ return family_id
+
+ # Definitions
+
+ def _add_definitions(self, family: spec.FamilySpec, family_id: int,
+ definitions: tuple[spec.ValueList, ...],
+ kind: str) -> None:
+ for definition in definitions:
+ where = f'{family.path}: {kind} {definition.name}'
+ existing = self.definitions.get(definition.name)
+ if (existing is not None and existing.is_extension_point and
+ kind == 'enum'):
+ self._fill_enum(definition, family_id, existing, where)
+ continue
+ if existing is not None:
+ owner = self._owner_name(existing.family_id)
+ if existing.is_fill:
+ raise CtlvError(where, 'enum extension point '
+ f'{definition.name} was already '
+ f'filled by family {owner}')
+ raise CtlvError(where, f'{definition.name} is already '
+ f'declared by {owner}')
+ if definition.extension:
+ self._enum_extension(definition, family_id, where)
+ continue
+ if kind == 'flags' and len(definition.values) > MAX_FLAG_BITS:
+ raise CtlvError(where, f'more than {MAX_FLAG_BITS} flag bits')
+ if kind == 'enum':
+ _check_count(where, 'enum values', len(definition.values),
+ spec.U32_MAX)
+ if family_id == spec.CORE_FAMILY_ID:
+ maximum = ROOT_ENUM_VALUES_MAX.get(definition.name)
+ if maximum is not None:
+ _check_count(where, 'enum values',
+ len(definition.values), maximum)
+ # Enum values from one, zero being reserved, unless a header
+ # numbers them; flag bits from zero.
+ first = 1 if kind == 'enum' else 0
+ values = tuple((value.name, first + index
+ if value.number is None else value.number)
+ for index, value in enumerate(definition.values))
+ self.definitions[definition.name] = Definition(
+ definition.name, kind, family_id, values,
+ tuple(value.doc for value in definition.values),
+ definition.header, definition.doc)
+
+ def _enum_extension(self, definition: spec.ValueList, family_id: int,
+ where: str) -> None:
+ if family_id == spec.CORE_FAMILY_ID:
+ raise CtlvError(where, 'the root family has no extension points')
+ ext_id = 1 + sum(point.family_id == family_id
+ for point in self.enum_extensions.values())
+ _check_count(where, 'enum extension points', ext_id, spec.U32_MAX)
+ extension = EnumExtension(definition.name, family_id, ext_id)
+ self.enum_extensions[definition.name] = extension
+ self.definitions[definition.name] = Definition(
+ definition.name, 'enum', family_id, (), (), None, definition.doc,
+ extension)
+
+ def _fill_enum(self, definition: spec.ValueList, family_id: int,
+ point: Definition, where: str) -> None:
+ """Give an inherited enum extension point its values."""
+ if definition.extension:
+ owner = self._owner_name(point.family_id)
+ raise CtlvError(where, f'enum extension point {definition.name} '
+ f'is declared by {owner}; a further one '
+ 'needs a new name')
+ if definition.header:
+ raise CtlvError(where, 'a fill numbers its own values, which a '
+ 'header cannot')
+ _check_count(where, 'enum values', len(definition.values),
+ spec.U32_MAX)
+ self.definitions[definition.name] = Definition(
+ definition.name, 'enum', family_id,
+ tuple((value.name, 1 + index)
+ for index, value in enumerate(definition.values)),
+ tuple(value.doc for value in definition.values), None,
+ definition.doc, point.extension)
+
+ def _definition(self, decl: spec.FieldDecl) -> spec.FieldDecl:
+ """Resolve a field naming an enum or flags into which one it is, and
+ a count naming an enum into one occurrence per value."""
+ if decl.count_enum is not None:
+ decl = dataclasses.replace(decl,
+ max_occurrences=self._count(decl))
+ if decl.kind != 'definition':
+ return decl
+ assert decl.ref is not None
+ definition = self.definitions.get(decl.ref)
+ if definition is None:
+ raise CtlvError(decl.where, f'"{decl.ref}" is not a schema kind, '
+ 'an enum or flags')
+ return dataclasses.replace(decl, kind=definition.kind)
+
+ def _count(self, decl: spec.FieldDecl) -> int:
+ name = decl.count_enum
+ assert name is not None
+ definition = self.definitions.get(name)
+ if definition is None or definition.kind != 'enum':
+ raise CtlvError(decl.where, f'count {name} is not an enum')
+ if definition.is_extension_point:
+ raise CtlvError(decl.where, f'count {name} is an enum extension '
+ 'point, whose values a descendant '
+ 'family gives')
+ return len(definition.values)
+
+ # Records
+
+ def _owner_name(self, family_id: int) -> str:
+ return self.registry.entries[family_id].name
+
+ def _add_records(self, family: spec.FamilySpec, family_id: int) -> None:
+ where = f'{family.path}: family {family.name}'
+ _check_count(where, 'named records', len(family.records),
+ MAX_RECORD_INDEX)
+ own = []
+ for index, record in enumerate(family.records, start=1):
+ self._check_new_record(record)
+ self.declared[record.name] = (family_id, record.where, index,
+ record)
+ own.append(record.name)
+ self.used = set()
+ for name in own:
+ self._named(name, None)
+ for name in own:
+ if name in self.extensions and name not in self.fillers:
+ self._fill(family, name)
+
+ def _check_new_record(self, record: spec.RecordSpec) -> None:
+ name = record.name
+ extension = self.extensions.get(name)
+ if extension is not None:
+ if name in self.fillers:
+ raise CtlvError(record.where, 'extension point was already '
+ 'filled by family '
+ f'{self.fillers[name]}')
+ if record.body is None:
+ raise CtlvError(record.where, 'a fill is a record, not a '
+ 'field of records:')
+ return
+ if name in self.declared:
+ owner = self._owner_name(self.declared[name][0])
+ raise CtlvError(record.where, f'already declared by {owner}')
+
+ def _fill(self, family: spec.FamilySpec, name: str) -> None:
+ self.fillers[name] = family.name
+ self.fill_records[name] = self.named[name]
+
+ def _check_used(self, family: spec.FamilySpec, family_id: int) -> None:
+ """A family that inherits declares a record to fill or to use."""
+ if family_id == spec.CORE_FAMILY_ID:
+ return
+ for record in family.records:
+ if record.name in self.used or record.name in self.fill_records:
+ continue
+ raise CtlvError(record.where,
+ f'no inherited extension point is called '
+ f'{record.name}, and nothing in {family.name} '
+ 'uses it')
+
+ def _named(self, name: str, where: str | None) -> Record:
+ """Return the named record or field of records: called name."""
+ if name in self.named:
+ return self.named[name]
+ if name not in self.declared:
+ if name in self.extensions:
+ raise CtlvError(where, f'{name} is an extension point, which '
+ 'extension names')
+ raise CtlvError(where, f'no record is called {name}')
+ if name in self.building:
+ raise CtlvError(where, 'record cycle: ' +
+ ' -> '.join(self.building + (name,)))
+ family_id, declared_at, index, record_spec = self.declared[name]
+ self.building += (name,)
+ try:
+ if record_spec.body is not None:
+ record = self._record(record_spec.body, name, family_id,
+ record_id('named', index), 'named',
+ name, name, 0, declared_at,
+ record_spec.doc)
+ else:
+ record = self._field_record(record_spec, family_id, index)
+ finally:
+ self.building = self.building[:-1]
+ self.named[name] = record
+ return record
+
+ def _field_record(self, record_spec: spec.RecordSpec, family_id: int,
+ index: int) -> Record:
+ """A field of records:: a record of one field, under its name."""
+ name = record_spec.name
+ identifier = record_id('field', index)
+ assert record_spec.field is not None
+ decl = self._definition(record_spec.field)
+ holds = None
+ if decl.kind == 'record' and decl.ref is not None:
+ holds = self._held(decl.ref, decl.where)
+ field = self._new_field(Field(
+ record=name, name=name, family_id=family_id,
+ record_id=identifier, number=1,
+ type=type_of(family_id, identifier, 1), decl=decl,
+ full_name=name, shared=True, holds=holds))
+ record = Record(name=name, family_id=family_id,
+ record_id=identifier, role='field', path=name,
+ fields=(field,), members=(Member(field, True),),
+ doc=record_spec.doc)
+ self.records[name] = record
+ return record
+
+ def _held(self, name: str, where: str) -> str:
+ """Return the path of the named record a field holds."""
+ self.used.add(name)
+ if name in self.building:
+ # A record holding itself, which the tree re-enters.
+ is_field = self.declared[name][3].body is None
+ else:
+ is_field = self._named(name, where).role == 'field'
+ if is_field:
+ raise CtlvError(where, f'record {name} holds a named record, and '
+ f'{name} is a field of records:, which '
+ 'bare record places')
+ return name
+
+ def _record(self, body: spec.Body, name: str, family_id: int,
+ identifier: int, role: str, path: str, owner: str, depth: int,
+ where: str, doc: str | None = None) -> Record:
+ """Number the fields of one record and compose its members."""
+ own, members, unnamed, number = [], [], 0, 0
+ for item in body.items:
+ if isinstance(item, spec.Include):
+ members += self._include(item)
+ continue
+ if isinstance(item, spec.Place):
+ target = self._named(item.name, item.where)
+ self.used.add(item.name)
+ if target.role != 'field':
+ raise CtlvError(item.where, f'bare record places a field '
+ f'of records:, and '
+ f'{item.name} is a record, '
+ f'which record {item.name} '
+ 'holds or $include splices')
+ members.append(Member(target.fields[0], not item.optional,
+ item.doc))
+ continue
+ number += 1
+ _check_count(item.where, 'fields', number, MAX_FIELD_NUMBER)
+ decl = self._definition(item)
+ if role in ('request', 'reply'):
+ full_name = f'{owner} {decl.name}'
+ else:
+ full_name = f'{path}.{decl.name}'
+ holds = None
+ if decl.kind == 'record' and decl.body is not None:
+ unnamed += 1
+ if unnamed > MAX_UNNAMED_PEERS:
+ raise CtlvError(item.where, f'more than '
+ f'{MAX_UNNAMED_PEERS} '
+ 'unnamed records side by '
+ f'side in {path}; name one '
+ 'in records:')
+ if depth >= MAX_UNNAMED_DEPTH:
+ raise CtlvError(item.where, f'unnamed records go more '
+ f'than {MAX_UNNAMED_DEPTH} '
+ f'deep below {owner}; name '
+ 'one in records:')
+ holds = self._record(
+ decl.body, f'{owner}-{decl.name}', family_id,
+ identifier | (unnamed << (12 - 4 * depth)), 'unnamed',
+ full_name, owner, depth + 1, item.where).path
+ elif decl.kind == 'record' and decl.ref is not None:
+ holds = self._held(decl.ref, decl.where)
+ elif decl.extension is not None:
+ self._extension(decl.extension, family_id, decl.where)
+ field = self._new_field(Field(
+ record=name, name=decl.name, family_id=family_id,
+ record_id=identifier, number=number,
+ type=type_of(family_id, identifier, number), decl=decl,
+ full_name=full_name, holds=holds))
+ own.append(field)
+ members.append(Member(field, not decl.optional, decl.doc))
+ self._check_members(where, path, members)
+ record = Record(name=name, family_id=family_id, record_id=identifier,
+ role=role, path=path, fields=tuple(own),
+ members=tuple(members), doc=doc)
+ self.records[path] = record
+ return record
+
+ def _include(self, item: spec.Include) -> list[Member]:
+ target = self._named(item.name, item.where)
+ self.used.add(item.name)
+ if target.role == 'field':
+ raise CtlvError(item.where, f'$include splices a named record, '
+ f'and {item.name} is a field of '
+ 'records:, which bare record places')
+ return [dataclasses.replace(member,
+ required=member.required and
+ not item.optional)
+ for member in target.members]
+
+ def _check_members(self, where: str, path: str,
+ members: list[Member]) -> None:
+ names: dict[str, str] = {}
+ types: set[int] = set()
+ for member in members:
+ field = member.field
+ if field.type in types:
+ raise CtlvError(where, f'{field.full_name} is in {path} '
+ 'twice')
+ if field.name in names:
+ raise CtlvError(where, f'two fields of {path} are called '
+ f'{field.name}: {names[field.name]} '
+ f'and {field.full_name}')
+ names[field.name] = field.full_name
+ types.add(field.type)
+
+ def _new_field(self, field: Field) -> Field:
+ clash = self.c_names.get((field.family_id, field.c_name))
+ if clash is not None:
+ hint = ''
+ kinds = {clash.record_id >> 30, field.record_id >> 30}
+ if (clash.record_id >> 16 & MAX_RECORD_INDEX ==
+ field.record_id >> 16 & MAX_RECORD_INDEX and
+ kinds == {TYPE_KIND_REQUEST, TYPE_KIND_REPLY}):
+ hint = ('; a field an op has in both its request and its '
+ 'reply is a field of records:')
+ raise CtlvError(field.decl.where, f'{clash.full_name} and '
+ f'{field.full_name} would both '
+ f'be named {field.c_name}'
+ f'{hint}')
+ self.c_names[(field.family_id, field.c_name)] = field
+ self.fields[field.full_name] = field
+ return field
+
+ def _extension(self, name: str, family_id: int, where: str) -> Extension:
+ extension = self.extensions.get(name)
+ if extension is not None:
+ if extension.family_id != family_id:
+ owner = self._owner_name(extension.family_id)
+ raise CtlvError(where, f'extension point {name} is '
+ f'declared by {owner}; a further one '
+ 'needs a new name')
+ return extension
+ if name in self.declared:
+ raise CtlvError(where, f'{name} is a record, which record '
+ f'{name} holds')
+ if family_id == spec.CORE_FAMILY_ID:
+ raise CtlvError(where, 'the root family has no extension points')
+ ext_id = 1 + sum(point.family_id == family_id
+ for point in self.extensions.values())
+ _check_count(where, 'extension points', ext_id, spec.U32_MAX)
+ extension = Extension(name, family_id, ext_id)
+ self.extensions[name] = extension
+ return extension
+
+ # Ops
+
+ def _add_ops(self, family: spec.FamilySpec, family_id: int) -> None:
+ _check_count(f'{family.path}: family {family.name}', 'ops',
+ len(family.ops), MAX_RECORD_INDEX)
+ error_info_max_len = self._error_info_max_len()
+ for local_id, op in enumerate(family.ops, start=1):
+ where = f'{family.path}: op {op.name}'
+ if any(existing.name == op.name for existing in self.ops):
+ raise CtlvError(where, f'op "{op.name}" is already declared')
+ request_record = reply_record = None
+ request = reply = None
+ if op.request is not None:
+ request_record = self._message(op, op.request, family_id,
+ local_id, 'request')
+ request = self._tree(request_record, where + ' request')
+ if op.reply is not None:
+ reply_record = self._message(op, op.reply, family_id,
+ local_id, 'reply')
+ reply = self._tree(reply_record, where + ' reply')
+ self._check_context(where, op, request, reply)
+ self.ops.append(Op(
+ name=op.name,
+ type=op.type,
+ fd_access=op.fd_access,
+ availability=op.availability,
+ optional=op.optional,
+ pre_post=op.pre_post,
+ family_id=family_id,
+ local_id=local_id,
+ op_id=(family_id << 48) | local_id,
+ request=request,
+ reply=reply,
+ sizes=sizes.compute(where, op, request, reply,
+ error_info_max_len),
+ doc=op.doc,
+ request_record=request_record,
+ reply_record=reply_record,
+ ))
+
+ def _message(self, op: spec.OpSpec, message: spec.MessageSpec,
+ family_id: int, local_id: int, which: str) -> Record:
+ if message.ref is not None:
+ return self.named[self._held(message.ref, message.where)]
+ return self._record(message.body, op.name, family_id,
+ record_id(which, local_id), which,
+ f'{op.name} {which}', op.name, 0, message.where,
+ message.doc)
+
+ def _tree(self, record: Record, where: str) -> Node:
+ root = self._root(record.members, where, ())
+ _check_tree_size(where, root)
+ return root
+
+ def _root(self, members: tuple[Member, ...], where: str,
+ opened: tuple[str, ...]) -> Node:
+ children = self._children(members, where, opened, 0)
+ return Node(None, True, 1,
+ tuple(node for node in children if node.required),
+ tuple(node for node in children if not node.required))
+
+ def _children(self, members: tuple[Member, ...], where: str,
+ opened: tuple[str, ...], depth: int) -> list[Node]:
+ return [self._node(member.field, member.required, where, opened,
+ depth)
+ for member in members]
+
+ def _node(self, field: Field, required: bool, where: str,
+ opened: tuple[str, ...], depth: int) -> Node:
+ decl = field.decl
+ if decl.kind != 'record':
+ return Node(field, required, decl.max_occurrences, (), ())
+ if decl.extension is not None:
+ return Node(field, required, decl.max_occurrences, (), (),
+ extensions=(self.extensions[decl.extension],))
+ assert field.holds is not None
+ record = self.records[field.holds]
+ named = record.role == 'named'
+ if named and record.name in opened:
+ # Open above, so one level further down rather than a new one.
+ return Node(field, required, decl.max_occurrences, (), (),
+ reenters=record.name)
+ if depth + 1 > sizes.MAX_DECLARED_RECORD_DEPTH:
+ raise CtlvError(where, f'{field.full_name} goes deeper than the '
+ f'{sizes.MAX_DECLARED_RECORD_DEPTH} level '
+ 'limit')
+ children = self._children(record.members, where,
+ opened + ((record.name,) if named else ()),
+ depth + 1)
+ return Node(field, required, decl.max_occurrences,
+ tuple(child for child in children if child.required),
+ tuple(child for child in children if not child.required),
+ expands=record.name if named else None)
+
+ def _check_context(self, where: str, op: spec.OpSpec, request: Node | None,
+ reply: Node | None) -> None:
+ if request is not None:
+ self._check_subtree(where, request, 'request')
+ if reply is None:
+ return
+ if op.type == 'query':
+ self._check_subtree(where, reply, 'query reply')
+ self._check_reentry(where, reply)
+ else:
+ self._check_subtree(where, reply, 'action reply')
+
+ def _check_reentry(self, where: str, root: Node) -> None:
+ """A repeating level cannot also be widened by a descendant."""
+ if not _reenters_anywhere(root) or not _extensions_anywhere(root):
+ return
+ raise CtlvError(f'{where} query reply',
+ 're-enters a record and holds an extension point, '
+ 'and a descendant cannot widen a level that repeats')
+
+ def _check_fills(self, fills: tuple[Fill, ...]) -> None:
+ """Hold each fill to every place its extension point sits.
+
+ A fill nested in another is met after it, so its places are known.
+ """
+ for fill in fills:
+ where = self.declared[fill.name][1]
+ for site, context, level in self.sites.get(fill.name, []):
+ self._check_subtree(f'{where} in {site}', fill.root, context,
+ level)
+
+ def _check_subtree(self, where: str, node: Node, context: str,
+ level: int = 0) -> None:
+ """Hold a tree to its context and note where its extensions sit.
+
+ level is how many records deep node is, the root being 0.
+ """
+ if node.field is not None:
+ name = node.field.full_name
+ if (node.max_occurrences == spec.UNLIMITED and
+ context != 'query reply'):
+ raise CtlvError(f'{where} {context}',
+ f'{name} has no count limit, which only a '
+ 'query reply may contain')
+ if node.reenters is not None and context != 'query reply':
+ raise CtlvError(f'{where} {context}',
+ f'{name} re-enters {node.reenters}, so it '
+ 'has no maximum length, which only a query '
+ 'reply may contain')
+ if node.field.decl.kind == 'blob' and context != 'request':
+ raise CtlvError(f'{where} {context}',
+ f'{name} is a blob, which only a request '
+ 'may contain')
+ if (node.field.decl.kind == 'record' and
+ level > sizes.MAX_DECLARED_RECORD_DEPTH):
+ raise CtlvError(f'{where} {context}',
+ f'{name} goes deeper than the '
+ f'{sizes.MAX_DECLARED_RECORD_DEPTH} level '
+ 'limit')
+ for extension in node.extensions:
+ self.sites.setdefault(extension.name, []).append(
+ (where, context, level))
+ for child in node.children():
+ self._check_subtree(where, child, context, level + 1)
+
+
+def _record_order(record: Record) -> tuple[int, bool, int, int, int]:
+ """Named records first, each before the records it holds, then ops."""
+ kind = record.record_id >> 30
+ index = record.record_id >> 16 & MAX_RECORD_INDEX
+ return (record.family_id, kind != TYPE_KIND_NAMED, index, kind,
+ record.record_id & 0xffff)
+
+
+def _reenters_anywhere(node: Node) -> bool:
+ return any(child.reenters is not None or _reenters_anywhere(child)
+ for child in node.children())
+
+
+def _extensions_anywhere(node: Node) -> bool:
+ return bool(node.extensions) or any(_extensions_anywhere(child)
+ for child in node.children())
+
+
+def composed_members(family: Family, node: Node) -> list[tuple[Node, bool]]:
+ """The children of node with the fills of its extension points."""
+ members = [(child, False) for child in node.required_children]
+ members += [(child, True) for child in node.optional_children]
+ for extension in node.extensions:
+ fill = family.fill_of(extension)
+ if fill is not None:
+ members += composed_members(family, fill.root)
+ return members
+
+
+def _check_composed(family: Family) -> None:
+ def walk_composed(where: str, node: Node) -> int:
+ members = composed_members(family, node)
+ required = sum(not optional for _child, optional in members)
+ optional = len(members) - required
+
+ _check_count(where, 'composed required fields', required,
+ spec.U32_MAX)
+ _check_count(where, 'composed optional fields', optional,
+ spec.U32_MAX)
+ count = 1
+ for child, _optional in members:
+ count += walk_composed(where, child)
+ return count
+
+ for op in family.ops:
+ total = 0
+ for which, root in (('request', op.request), ('reply', op.reply)):
+ if root is None:
+ continue
+ where = f'{family.path}: op {op.name} {which}'
+ count = walk_composed(where, root)
+ _check_count(where, 'composed schema nodes', count,
+ spec.U32_MAX)
+ total += count
+ _check_count(f'{family.path}: op {op.name}',
+ 'composed request and reply nodes', total,
+ spec.U32_MAX)
+
+
+def _check_docs(family: spec.FamilySpec) -> None:
+ """Require the comment that documents what a family declares.
+
+ The values of a set a header numbers are the header's to describe.
+ """
+ _check_doc(f'{family.path}: family {family.name}', family.doc)
+ for values in family.enums + family.flags:
+ _check_doc(values.where, values.doc)
+ if values.header:
+ continue
+ for value in values.values:
+ if not value.doc:
+ raise CtlvError(value.where, 'needs a comment of its own '
+ 'right above it, so the values '
+ 'go one to a line')
+ for record in family.records:
+ _check_doc(record.where, record.doc)
+ if record.body is not None:
+ _check_body_docs(record.body)
+ for op in family.ops:
+ _check_doc(op.where, op.doc)
+ for message in (op.request, op.reply):
+ if message is not None:
+ _check_body_docs(message.body)
+
+
+def _check_body_docs(body: spec.Body) -> None:
+ for item in body.items:
+ if isinstance(item, spec.FieldDecl):
+ _check_doc(item.where, item.doc)
+ if item.body is not None:
+ _check_body_docs(item.body)
+
+
+def _check_doc(where: str, doc: str | None) -> None:
+ if not doc:
+ raise CtlvError(where, 'needs a comment right above it, which is its '
+ 'documentation')
+
+
+def _node_count(node: Node) -> int:
+ return 1 + sum(_node_count(child) for child in node.children())
+
+
+def _extension_count(node: Node) -> int:
+ return len(node.extensions) + sum(_extension_count(child)
+ for child in node.children())
+
+
+def _check_tree_size(where: str, root: Node) -> None:
+ _check_count(where, 'schema nodes', _node_count(root), spec.U32_MAX)
+ _check_count(where, 'schema extension points', _extension_count(root),
+ spec.U32_MAX)
diff --git a/tools/ctlv/ctlvgen/lib/sizes.py b/tools/ctlv/ctlvgen/lib/sizes.py
new file mode 100644
index 000000000000..3024eece9b5b
--- /dev/null
+++ b/tools/ctlv/ctlvgen/lib/sizes.py
@@ -0,0 +1,239 @@
+# SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+"""Length arithmetic over an expanded op tree.
+
+These numbers decide whether an op can run at all, so they are kept apart from
+the rest of resolution and tested against independently calculated lengths.
+The limits mirror include/linux/ctlv_family.h, and a test checks that.
+"""
+
+from __future__ import annotations
+
+import dataclasses
+from typing import TYPE_CHECKING
+
+from . import spec
+from .errors import CtlvError
+
+# semantics imports this module.
+if TYPE_CHECKING:
+ from . import semantics
+
+ALIGNTO: int = 8
+HDR_LEN: int = 16
+
+MAX_INLINE_MESSAGE_LEN: int = 0x10000
+MAX_ERROR_LEN: int = 0x4000
+MAX_RECORD_DEPTH: int = 8
+
+# The suggested reply length of a query whose spec suggests none.
+DEFAULT_QUERY_REPLY_LEN: int = 0x10000
+
+# One level short of what the transport carries: op-schema-get describes a
+# message, and a description goes one level deeper than what it describes.
+MAX_DECLARED_RECORD_DEPTH: int = MAX_RECORD_DEPTH - 1
+
+# Every integer, enum and flags payload is 64-bit.
+SCALAR_PAYLOAD_LEN: int = 8
+
+# A bool payload is one byte; only the TLV holding it is padded to eight.
+BOOL_PAYLOAD_LEN: int = 1
+
+# How many scalar descriptor fields each blob backing carries. A test checks
+# these against the root family's blob record.
+BLOB_DESCRIPTOR_FIELDS: dict[str, int] = {
+ 'va': 3,
+}
+
+ERROR_INFO: str = 'error-info'
+
+# Records of the root family that are message roots in their own right, each
+# with the core's limit and the name the kernel gives it.
+CORE_MESSAGES: dict[str, tuple[str, int]] = {
+ ERROR_INFO: ('CTLV_MAX_ERROR_LEN', MAX_ERROR_LEN),
+}
+
+
+@dataclasses.dataclass(frozen=True)
+class OpSizes:
+ """Generated lengths of one op. Zero means "does not apply"."""
+
+ request_max_len: int
+ action_reply_max_len: int
+ query_reply_min_len: int
+ query_reply_suggested_len: int
+ error_info_max_len: int
+
+
+def align(length: int) -> int:
+ return (length + ALIGNTO - 1) & ~(ALIGNTO - 1)
+
+
+def tlv_max_len(node: semantics.Node) -> int:
+ """Maximum length of one occurrence of node, excluding its own padding."""
+ return HDR_LEN + _payload_max_len(node)
+
+
+def root_max_len(root: semantics.Node) -> int:
+ """Maximum length of a complete message root."""
+ return HDR_LEN + _payload_max_len(root)
+
+
+def min_reply_len(root: semantics.Node) -> int:
+ """Buffer length needed for the largest indivisible leaf and its path.
+
+ A leaf below a re-entered level sits as deep as the transport allows.
+ """
+ if _reenters(root):
+ return (MAX_RECORD_DEPTH + 1) * HDR_LEN + _largest_leaf(root)
+ return _min_walk(root, 0)
+
+
+def compute(where: str, op: spec.OpSpec, request: semantics.Node | None,
+ reply: semantics.Node | None, error_info_max_len: int) -> OpSizes:
+ """Return the generated lengths of one op."""
+ request_max_len = 0
+ if request is not None:
+ request_max_len = root_max_len(request)
+ _check_inline(where, 'request', request_max_len)
+
+ action_reply_max_len = 0
+ query_reply_min_len = 0
+ query_reply_suggested_len = 0
+
+ if op.type == 'action' and reply is not None:
+ action_reply_max_len = root_max_len(reply)
+ _check_inline(where, 'action reply', action_reply_max_len)
+ elif op.type == 'query':
+ assert reply is not None
+ query_reply_min_len = min_reply_len(reply)
+ _check_inline(where, 'largest query reply leaf and its path',
+ query_reply_min_len)
+ query_reply_suggested_len = _suggested_len(
+ where, op.suggested_reply_length, query_reply_min_len)
+
+ return OpSizes(request_max_len=request_max_len,
+ action_reply_max_len=action_reply_max_len,
+ query_reply_min_len=query_reply_min_len,
+ query_reply_suggested_len=query_reply_suggested_len,
+ error_info_max_len=error_info_max_len)
+
+
+def composed(where: str, op: semantics.Op, request: semantics.Node | None,
+ reply: semantics.Node | None) -> OpSizes:
+ """Return the lengths one op needs for one family.
+
+ compute()'s numbers over the composed tree, fills spliced in. A suggestion
+ is raised here rather than refused.
+ """
+ sizes = OpSizes(request_max_len=root_max_len(request) if request else 0,
+ action_reply_max_len=0, query_reply_min_len=0,
+ query_reply_suggested_len=0,
+ error_info_max_len=op.sizes.error_info_max_len)
+ _check_inline(where, 'request', sizes.request_max_len)
+
+ if op.type == 'action' and reply is not None:
+ sizes = dataclasses.replace(sizes,
+ action_reply_max_len=root_max_len(reply))
+ _check_inline(where, 'action reply', sizes.action_reply_max_len)
+ elif op.type == 'query':
+ assert reply is not None
+ minimum = min_reply_len(reply)
+ _check_inline(where, 'largest query reply leaf and its path', minimum)
+ sizes = dataclasses.replace(
+ sizes, query_reply_min_len=minimum,
+ query_reply_suggested_len=max(op.sizes.query_reply_suggested_len,
+ minimum))
+ return sizes
+
+
+def message_max_len(where: str, name: str, root: semantics.Node) -> int:
+ """Return the maximum length of one core message, against its limit."""
+ limit_name, limit = CORE_MESSAGES[name]
+ length = root_max_len(root)
+ if length > limit:
+ raise CtlvError(where, f'needs {length} bytes, more than the {limit} '
+ f'of {limit_name}')
+ return length
+
+
+def _payload_max_len(node: semantics.Node) -> int:
+ if node.is_root:
+ return _children_max_len(node)
+
+ decl = node.child_field.decl
+ if decl.kind in ('uint', 'sint', 'enum', 'flags'):
+ return SCALAR_PAYLOAD_LEN
+ if decl.kind == 'bool':
+ return BOOL_PAYLOAD_LEN
+ if decl.kind == 'string':
+ assert decl.max_length is not None
+ # One terminating NUL, which max-length excludes.
+ return decl.max_length + 1
+ if decl.kind == 'binary':
+ assert decl.max_length is not None
+ return decl.max_length
+ if decl.kind == 'blob':
+ assert decl.blob is not None
+ widest = max(BLOB_DESCRIPTOR_FIELDS[backing]
+ for backing in decl.blob.backings)
+ return widest * align(HDR_LEN + SCALAR_PAYLOAD_LEN)
+ return _children_max_len(node)
+
+
+def _children_max_len(node: semantics.Node) -> int:
+ total = 0
+ for child in node.children():
+ if child.max_occurrences == spec.UNLIMITED:
+ raise AssertionError(
+ f'{child.child_field.full_name} has unlimited occurrences, '
+ 'so it has no maximum length')
+ if child.reenters is not None:
+ raise AssertionError(
+ f'{child.child_field.full_name} re-enters {child.reenters}, '
+ 'so it has no maximum length')
+ total += child.max_occurrences * align(tlv_max_len(child))
+ return total
+
+
+def _min_walk(node: semantics.Node, ancestors_len: int) -> int:
+ children = node.children()
+ if children:
+ return max(_min_walk(child, ancestors_len + HDR_LEN)
+ for child in children)
+ if node.is_root:
+ return ancestors_len + HDR_LEN
+ return ancestors_len + align(tlv_max_len(node))
+
+
+def _reenters(node: semantics.Node) -> bool:
+ return any(child.reenters is not None or _reenters(child)
+ for child in node.children())
+
+
+def _largest_leaf(node: semantics.Node) -> int:
+ """The longest node of the tree that a fragment cannot split."""
+ lengths = [align(tlv_max_len(child))
+ for child in node.children()
+ if child.child_field.decl.kind != 'record']
+ lengths += [_largest_leaf(child) for child in node.children()]
+ return max(lengths, default=0)
+
+
+def _check_inline(where: str, what: str, length: int) -> None:
+ if length > MAX_INLINE_MESSAGE_LEN:
+ raise CtlvError(where, f'{what} needs {length} bytes, more than the '
+ f'{MAX_INLINE_MESSAGE_LEN} byte inline '
+ 'message limit')
+
+
+def _suggested_len(where: str, declared: int | None, minimum: int) -> int:
+ if declared is None:
+ return max(DEFAULT_QUERY_REPLY_LEN, minimum)
+ if declared < minimum:
+ raise CtlvError(where, f'suggested-reply-length {declared} is below '
+ f'the computed minimum of {minimum}')
+ if declared > MAX_INLINE_MESSAGE_LEN:
+ raise CtlvError(where, f'suggested-reply-length {declared} exceeds '
+ f'the {MAX_INLINE_MESSAGE_LEN} byte inline '
+ 'message limit')
+ return declared
diff --git a/tools/ctlv/ctlvgen/lib/spec.py b/tools/ctlv/ctlvgen/lib/spec.py
new file mode 100644
index 000000000000..7d1f2e14f70f
--- /dev/null
+++ b/tools/ctlv/ctlvgen/lib/spec.py
@@ -0,0 +1,813 @@
+# SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+"""The as-written form of a CTLV family specification.
+
+The strict YAML subset, the meta-schema, and every rule that one declaration
+decides on its own. Comparing declarations and walking a chain are
+semantics.py's. Omitted values are normalized here, once.
+"""
+
+from __future__ import annotations
+
+import dataclasses
+import pathlib
+import re
+from collections.abc import Callable
+from typing import Any
+
+import jsonschema
+
+from . import cenum
+from . import yamlload
+from .errors import CtlvError
+
+# A generated file carries the license of its specification's first line.
+LICENSE: re.Pattern[str] = re.compile(
+ r'#\s*SPDX-License-Identifier:\s*(\S.*?)\s*$')
+
+# The registry is indexed by family ID, so the root family is its first entry.
+CORE_FAMILY_NAME: str = 'ctlv'
+CORE_FAMILY_ID: int = 0
+
+# Where a $header is, under the kernel tree: an enum is taken from uapi only.
+UAPI_DIR: str = 'include/uapi'
+
+MAX_FAMILY_NAME_LEN: int = 63
+U8_MAX: int = (1 << 8) - 1
+U16_MAX: int = (1 << 16) - 1
+U32_MAX: int = (1 << 32) - 1
+MAX_FAMILY_ID: int = U16_MAX
+
+# Zero means unlimited, as introspection reports it.
+UNLIMITED: int = 0
+
+U64_MAX: int = (1 << 64) - 1
+S64_MIN: int = -(1 << 63)
+S64_MAX: int = (1 << 63) - 1
+S8_MIN: int = -(1 << 7)
+S8_MAX: int = (1 << 7) - 1
+
+# The limits a spec, a snapshot and the docs write by name.
+NAMED_NUMBERS: dict[str, int] = {}
+for _bits in (8, 16, 32, 64):
+ NAMED_NUMBERS[f'u{_bits}-max'] = (1 << _bits) - 1
+ NAMED_NUMBERS[f's{_bits}-min'] = -(1 << (_bits - 1))
+ NAMED_NUMBERS[f's{_bits}-max'] = (1 << (_bits - 1)) - 1
+SIZE_SUFFIXES: tuple[tuple[str, int], ...] = (
+ ('G', 1 << 30), ('M', 1 << 20), ('K', 1 << 10))
+
+INTEGER_KINDS: tuple[str, ...] = ('uint', 'sint')
+SCALAR_KINDS: tuple[str, ...] = ('uint', 'sint', 'bool', 'string', 'binary',
+ 'enum', 'flags')
+SCHEMA_KINDS: tuple[str, ...] = ('uint', 'sint', 'bool', 'string', 'binary',
+ 'blob')
+UNITS: tuple[str, ...] = ('seconds', 'hertz', 'bytes', 'celsius')
+BACKINGS: tuple[str, ...] = ('va',)
+DIRECTIONS: dict[str, str] = {'in': 'input', 'out': 'output', 'inout': 'inout'}
+DEFAULT_BACKINGS: tuple[str, ...] = ('va',)
+
+FD_ACCESSES: tuple[str, ...] = ('read', 'read-write')
+DEFAULT_FD_ACCESS: str = 'read-write'
+
+# Every op has one of these, and only an action or a query may be dynamic.
+AVAILABILITIES: tuple[str, ...] = ('fixed', 'dynamic')
+DEFAULT_AVAILABILITY: str = 'fixed'
+
+_NAME: str = r'[a-z0-9]+(?:-[a-z0-9]+)*'
+_FIELD_KEY: re.Pattern[str] = re.compile(
+ rf'(?P<name>{_NAME})(?:\[(?P<count>[0-9]*|{_NAME})\])?(?P<optional>\?)?')
+_KIND: re.Pattern[str] = re.compile(
+ rf'(?P<kind>{_NAME})(?:\((?P<args>[^()]*)\))?')
+_REFERENCE: re.Pattern[str] = re.compile(
+ rf'(?P<what>record|extension)(?: +(?P<name>{_NAME}))?')
+_INCLUDE: re.Pattern[str] = re.compile(rf'(?P<name>{_NAME})(?P<optional>\?)?')
+_NUMBER: re.Pattern[str] = re.compile(r'(?P<value>-?[0-9]+)(?P<suffix>[KMG]?)')
+_SCALE: re.Pattern[str] = re.compile(r'scale +(?P<value>-?[0-9]+)')
+_ALIGN: re.Pattern[str] = re.compile(r'align +(?P<value>\S+)')
+
+
+def number_text(value: int) -> str:
+ """Return an integer as a spec writes it, a limit by its name."""
+ for name, number in NAMED_NUMBERS.items():
+ if number == value:
+ return name
+ return str(value)
+
+
+def size_text(value: int) -> str:
+ """Return a byte count as a spec writes it."""
+ for suffix, scale in SIZE_SUFFIXES:
+ if value >= scale and value % scale == 0:
+ return f'{value // scale}{suffix}'
+ if value >= 1 << 10:
+ return number_text(value)
+ return str(value)
+
+
+@dataclasses.dataclass(frozen=True)
+class Value:
+ """One value of an enum or a flags definition, numbered if a header's."""
+
+ name: str
+ where: str
+ doc: str | None = None
+ number: int | None = None
+
+
+@dataclasses.dataclass(frozen=True)
+class HeaderEnum:
+ """Enum tag of include/uapi/<path>, its constants before end if given."""
+
+ path: str
+ tag: str
+ end: str | None = None
+
+
+@dataclasses.dataclass(frozen=True)
+class ValueList:
+ """An enum or a flags definition, or an enum extension point."""
+
+ name: str
+ where: str
+ values: tuple[Value, ...]
+ header: HeaderEnum | None = None
+ doc: str | None = None
+ extension: bool = False
+
+
+@dataclasses.dataclass(frozen=True)
+class BlobDecl:
+ """The blob properties of one blob field declaration."""
+
+ direction: str
+ backings: tuple[str, ...]
+ max_length: int
+ alignment: int
+
+
+@dataclasses.dataclass(frozen=True)
+class FieldDecl:
+ """One field declaration.
+
+ kind is the schema kind, enum or flags, or record for a record, which holds
+ the fields written in place in body, the named record ref, or the
+ extension point extension. A field that does not apply stays None.
+ count_enum is the enum a count names, whose values the chain counts.
+ """
+
+ name: str
+ kind: str
+ where: str
+ optional: bool = False
+ max_occurrences: int = 1
+ count_enum: str | None = None
+ ref: str | None = None
+ body: Body | None = None
+ extension: str | None = None
+ blob: BlobDecl | None = None
+ min: int | None = None
+ max: int | None = None
+ min_length: int | None = None
+ max_length: int | None = None
+ unit: str | None = None
+ scale: int | None = None
+ doc: str | None = None
+
+
+@dataclasses.dataclass(frozen=True)
+class Place:
+ """A field of records: placed by bare record, under its own name."""
+
+ name: str
+ optional: bool
+ where: str
+ doc: str | None = None
+
+
+@dataclasses.dataclass(frozen=True)
+class Include:
+ """A named record whose fields are spliced in with their IDs."""
+
+ name: str
+ optional: bool
+ where: str
+
+
+@dataclasses.dataclass(frozen=True)
+class Body:
+ """A record as written: fields, placed records and includes, in order."""
+
+ items: tuple[FieldDecl | Place | Include, ...] = ()
+
+
+@dataclasses.dataclass(frozen=True)
+class RecordSpec:
+ """One entry of records:, a named record or a field of records:."""
+
+ name: str
+ where: str
+ body: Body | None = None
+ field: FieldDecl | None = None
+ doc: str | None = None
+
+
+@dataclasses.dataclass(frozen=True)
+class MessageSpec:
+ """A request or a reply: a record in place, or a named one."""
+
+ where: str
+ body: Body = Body()
+ ref: str | None = None
+ doc: str | None = None
+
+
+@dataclasses.dataclass(frozen=True)
+class OpSpec:
+ """One op."""
+
+ name: str
+ type: str
+ where: str
+ request: MessageSpec | None
+ reply: MessageSpec | None = None
+ fd_access: str = DEFAULT_FD_ACCESS
+ availability: str = DEFAULT_AVAILABILITY
+ optional: bool = False
+ pre_post: bool = False
+ suggested_reply_length: int | None = None
+ doc: str | None = None
+
+
+@dataclasses.dataclass(frozen=True)
+class FamilySpec:
+ """One specification file, syntax-checked and normalized."""
+
+ path: str
+ name: str
+ license: str
+ inherits: str | None
+ enums: tuple[ValueList, ...]
+ flags: tuple[ValueList, ...]
+ records: tuple[RecordSpec, ...]
+ ops: tuple[OpSpec, ...]
+ doc: str | None = None
+
+
+@dataclasses.dataclass(frozen=True)
+class RegistryEntry:
+ name: str
+ deprecated: bool
+
+
+@dataclasses.dataclass(frozen=True)
+class Registry:
+ """The append-only family registry, indexed by family ID."""
+
+ path: str
+ entries: tuple[RegistryEntry, ...]
+
+ @property
+ def license(self) -> str:
+ return _license(self.path)
+
+ def family_id(self, name: str) -> int | None:
+ """Return the registered family ID of name, or None if unregistered."""
+ for family_id, entry in enumerate(self.entries):
+ if entry.name == name:
+ return family_id
+ return None
+
+
+class Paths:
+ """Where the specs, the registry and the meta-schemas are."""
+
+ def __init__(self, spec: pathlib.Path | str,
+ schema: pathlib.Path | str | None = None,
+ registry: pathlib.Path | str | None = None,
+ abi: pathlib.Path | str | None = None) -> None:
+ self.spec = pathlib.Path(spec)
+ self.spec_dir = self.spec.parent
+ self.doc_dir = self.spec_dir.parent
+ self.schema = pathlib.Path(schema or self.doc_dir / 'ctlv-schema.yaml')
+ self.registry = pathlib.Path(registry or
+ self.doc_dir / 'families.yaml')
+ # The two meta-schemas ship together, so overriding one locates both.
+ self.registry_schema = (self.schema.parent /
+ 'ctlv-registry-schema.yaml')
+ self.abi_dir = pathlib.Path(abi or self.doc_dir / 'abi')
+ self.registry_abi = self.abi_dir / 'families.registry.yaml'
+ self.generated = self.doc_dir / 'generated-files.yaml'
+ self.generated_schema = self.doc_dir / 'generated-files-schema.yaml'
+ self.kernel_dir = self.schema.parent.parent.parent
+
+ def sibling_spec(self, name: str) -> pathlib.Path:
+ """Return the path of another family's specification.
+
+ The one root family lives where the meta-schema does, so a corpus of
+ fixtures inherits it rather than a copy.
+ """
+ if name == CORE_FAMILY_NAME:
+ return self.schema.parent / 'specs' / f'{name}.yaml'
+ return self.spec_dir / f'{name}.yaml'
+
+ def abi(self, name: str) -> pathlib.Path:
+ """Return the path of one family's checked-in ABI snapshot."""
+ return self.abi_dir / f'{name}.yaml'
+
+
+def load_registry(paths: Paths, path: pathlib.Path | str | None = None,
+ text: str | None = None) -> Registry:
+ """Load and check the family registry, or the one in text."""
+ path = pathlib.Path(path or paths.registry)
+ data = yamlload.load(path, text)
+ _validate_syntax(path, data, paths.registry_schema)
+
+ entries = []
+ seen = set()
+ for item in data['families']:
+ if isinstance(item, dict):
+ entry = RegistryEntry(item['name'], item['deprecated'])
+ else:
+ entry = RegistryEntry(item, False)
+ _family_name(f'{path}: family {entry.name}', entry.name)
+ if entry.name in seen:
+ raise CtlvError(f'{path}: {entry.name}', 'duplicate family')
+ seen.add(entry.name)
+ entries.append(entry)
+
+ core = entries[CORE_FAMILY_ID]
+ if core.name != CORE_FAMILY_NAME:
+ raise CtlvError(f'{path}: {core.name}',
+ f'family ID {CORE_FAMILY_ID} belongs to '
+ f'{CORE_FAMILY_NAME}, so it must be the first entry')
+ if core.deprecated:
+ raise CtlvError(f'{path}: {core.name}',
+ 'the core family cannot be deprecated')
+ if len(entries) - 1 > MAX_FAMILY_ID:
+ raise CtlvError(str(path),
+ f'{len(entries)} entries need a family ID past the '
+ f'{MAX_FAMILY_ID} maximum')
+ return Registry(str(path), tuple(entries))
+
+
+def load(path: pathlib.Path | str, schema: pathlib.Path) -> FamilySpec:
+ """Load, syntax-check and normalize one family specification."""
+ path = pathlib.Path(path)
+ data, lines = yamlload.load_with_lines(path)
+ _validate_syntax(path, data, schema)
+
+ reader = _Reader(path, lines,
+ pathlib.Path(schema).parent.parent.parent / UAPI_DIR)
+ name = _family_name(f'{path}: family {data["family"]}', data['family'])
+ inherits = data.get('inherits')
+ if inherits is not None:
+ inherits = _family_name(f'{path}: family {name} parent', inherits)
+ return FamilySpec(
+ path=str(path),
+ name=name,
+ license=_license(path),
+ inherits=inherits,
+ enums=reader.value_lists(data.get('enums', {}), 'enum'),
+ flags=reader.value_lists(data.get('flags', {}), 'flags'),
+ records=reader.records(data.get('records', {})),
+ ops=reader.ops(data.get('ops', {})),
+ doc=reader.doc(data, 'family'),
+ )
+
+
+def _license(path: pathlib.Path | str) -> str:
+ """Return the SPDX identifier the specification's first line carries."""
+ with open(path, 'r', encoding='utf-8') as stream:
+ found = LICENSE.match(stream.readline())
+ if found is None:
+ raise CtlvError(str(path),
+ 'the first line must be an SPDX-License-Identifier '
+ 'comment, which every generated file inherits')
+ return found.group(1)
+
+
+def _validate_syntax(path: pathlib.Path, data: dict[str, Any],
+ schema_path: pathlib.Path) -> None:
+ schema = yamlload.load(schema_path)
+ validator = jsonschema.Draft7Validator(schema)
+ error = jsonschema.exceptions.best_match(validator.iter_errors(data))
+ if error is None:
+ return
+ where = f'{path}: {error.json_path}' if error.absolute_path else str(path)
+ raise CtlvError(where, error.message)
+
+
+def _family_name(where: str, name: str) -> str:
+ if len(name) > MAX_FAMILY_NAME_LEN:
+ raise CtlvError(where, f'name is {len(name)} characters, more than '
+ f'the {MAX_FAMILY_NAME_LEN} maximum')
+ return name
+
+
+def _arguments(text: str) -> list[str]:
+ return [part.strip() for part in text.split(',')]
+
+
+class _Reader:
+ """Parses the parts of one specification, knowing its lines for docs."""
+
+ def __init__(self, path: pathlib.Path, lines: list[str],
+ uapi: pathlib.Path | None = None) -> None:
+ self.path = path
+ self.lines = lines
+ self.uapi = uapi
+
+ def doc(self, mapping: dict[str, Any], key: str) -> str | None:
+ """Return the comment lines right above key, without their #."""
+ return self._comment(getattr(mapping, 'lines', {}).get(key))
+
+ def _comment(self, line: int | None) -> str | None:
+ if line is None:
+ return None
+ texts = []
+ for index in range(line - 1, -1, -1):
+ text = self.lines[index].strip()
+ if not text.startswith('#') or LICENSE.match(text):
+ break
+ texts.append(text[1:].strip())
+ return '\n'.join(reversed(texts)).strip() or None
+
+ def where(self, mapping: dict[str, Any], key: str, what: str) -> str:
+ return self._at(getattr(mapping, 'lines', {}).get(key), what)
+
+ def _at(self, line: int | None, what: str) -> str:
+ if line is None:
+ return f'{self.path}: {what}'
+ return f'{self.path}:{line + 1}: {what}'
+
+ def _values(self, raw: list[str], after: int | None,
+ what: str) -> tuple[Value, ...]:
+ """Each value of a set, with the comment right above its line.
+
+ after is the line of the key the values follow. A value on that line,
+ or on another value's, has no comment of its own.
+ """
+ lines = getattr(raw, 'lines', [None] * len(raw))
+ values = []
+ for name, line in zip(raw, lines):
+ doc = None if line == after else self._comment(line)
+ values.append(Value(name, self._at(line, f'{what} value {name}'),
+ doc))
+ after = line
+ return tuple(values)
+
+ def value_lists(self, raw_lists: dict[str, Any],
+ what: str) -> tuple[ValueList, ...]:
+ lists = []
+ for name, raw in raw_lists.items():
+ where = self.where(raw_lists, name, f'{what} {name}')
+ if name in SCHEMA_KINDS or name in ('record', 'extension'):
+ raise CtlvError(where, f'"{name}" is a keyword of the spec '
+ f'language, so no {what} is called '
+ 'that')
+ if raw == 'extension':
+ lists.append(ValueList(name, where, (),
+ doc=self.doc(raw_lists, name),
+ extension=True))
+ continue
+ if isinstance(raw, dict):
+ lists.append(self._header_enum(name, where, raw,
+ self.doc(raw_lists, name)))
+ continue
+ after = getattr(raw_lists, 'lines', {}).get(name)
+ seen = set()
+ for value in raw:
+ if value in seen:
+ raise CtlvError(where, f'duplicate value "{value}"')
+ seen.add(value)
+ lists.append(ValueList(name, where,
+ self._values(raw, after, f'{what} {name}'),
+ doc=self.doc(raw_lists, name)))
+ return tuple(lists)
+
+ def _header_enum(self, name: str, where: str, raw: dict[str, str],
+ doc: str | None) -> ValueList:
+ header = HeaderEnum(raw['$header'], raw['$enum'], raw.get('$end'))
+ if self.uapi is None:
+ raise CtlvError(where, 'no kernel tree to read headers from')
+ constants = cenum.read(where, self.uapi, header.path, header.tag,
+ header.end)
+ values = tuple(Value(constant, f'{where} value {constant}',
+ number=number)
+ for constant, number in constants)
+ return ValueList(name, where, values, header, doc)
+
+ def records(self, raw_records: dict[str, Any]) -> tuple[RecordSpec, ...]:
+ records = []
+ for key, raw in raw_records.items():
+ found = _FIELD_KEY.fullmatch(key)
+ assert found is not None
+ name = found['name']
+ where = self.where(raw_records, key, f'record {name}')
+ doc = self.doc(raw_records, key)
+ if found['optional']:
+ raise CtlvError(where, 'a definition cannot be optional; '
+ 'whatever places it says so')
+ if isinstance(raw, dict):
+ if found['count'] is not None:
+ raise CtlvError(where, 'a count belongs to a field, and '
+ 'this is a record')
+ records.append(RecordSpec(name, body=self.body(raw, where),
+ doc=doc, where=where))
+ continue
+ decl = self.field(key, raw, where, doc)
+ if isinstance(decl, Place):
+ raise CtlvError(where, 'bare record places a field of '
+ 'records:, and this is where one is '
+ 'declared')
+ if decl.kind == 'record' and decl.ref is None:
+ raise CtlvError(where, 'a field of records: is shared by '
+ 'name, and an extension point is '
+ 'not shared')
+ records.append(RecordSpec(name, field=decl, doc=doc,
+ where=where))
+ return tuple(records)
+
+ def body(self, raw: dict[str, Any], where: str) -> Body:
+ items: list[FieldDecl | Place | Include] = []
+ names = set()
+ for key, value in raw.items():
+ if key == '$include':
+ for text in [value] if isinstance(value, str) else value:
+ found = _INCLUDE.fullmatch(text)
+ assert found is not None
+ items.append(Include(found['name'],
+ bool(found['optional']),
+ self.where(raw, key,
+ f'$include {text}')))
+ continue
+ item = self.field(key, value, where, self.doc(raw, key), raw)
+ if item.name in names:
+ raise CtlvError(item.where, f'two fields are called '
+ f'{item.name}')
+ names.add(item.name)
+ items.append(item)
+ return Body(tuple(items))
+
+ def field(self, key: str, raw: str | dict[str, Any], owner: str,
+ doc: str | None,
+ mapping: dict[str, Any] | None = None) -> FieldDecl | Place:
+ """Return the FieldDecl or the Place one key and its value say."""
+ found = _FIELD_KEY.fullmatch(key)
+ assert found is not None
+ name = found['name']
+ optional = bool(found['optional'])
+ count = found['count']
+ where = owner
+ if mapping is not None:
+ where = self.where(mapping, key, f'field {name}')
+ occurrences, count_enum = self._occurrences(where, count)
+ common: dict[str, Any] = {'name': name, 'optional': optional,
+ 'max_occurrences': occurrences,
+ 'count_enum': count_enum, 'doc': doc,
+ 'where': where}
+ if isinstance(raw, dict):
+ return FieldDecl(kind='record', body=self.body(raw, where),
+ **common)
+ text = raw.strip()
+ reference = _REFERENCE.fullmatch(text)
+ if reference is not None:
+ return self._reference(where, reference, count, common)
+ found = _KIND.fullmatch(text)
+ if found is None:
+ raise CtlvError(where, f'"{text}" is not a schema kind')
+ kind, args = found['kind'], found['args']
+ if kind in INTEGER_KINDS:
+ return FieldDecl(kind=kind, **self._integer(where, kind, args),
+ **common)
+ if kind == 'bool':
+ if args is not None:
+ raise CtlvError(where, 'a bool takes no bounds')
+ return FieldDecl(kind=kind, **common)
+ if kind in ('string', 'binary'):
+ return FieldDecl(kind=kind, **self._lengths(where, kind, args),
+ **common)
+ if kind == 'blob':
+ return FieldDecl(kind=kind, blob=self._blob(where, args),
+ **common)
+ if args is not None:
+ raise CtlvError(where, f'{kind} names an enum or flags, which '
+ 'take no bounds')
+ # Which of the two it names, the chain says.
+ return FieldDecl(kind='definition', ref=kind, **common)
+
+ def _reference(self, where: str, reference: re.Match[str],
+ count: str | None,
+ common: dict[str, Any]) -> FieldDecl | Place:
+ target = reference['name']
+ if reference['what'] == 'extension':
+ if target is None:
+ raise CtlvError(where, 'extension needs the name of its '
+ 'extension point')
+ return FieldDecl(kind='record', extension=target, **common)
+ if target is not None:
+ return FieldDecl(kind='record', ref=target, **common)
+ if count is not None:
+ raise CtlvError(where, 'a field of records: takes its count '
+ 'from its declaration, not where it is '
+ 'placed')
+ return Place(common['name'], common['optional'], where,
+ common['doc'])
+
+ def _occurrences(self, where: str,
+ count: str | None) -> tuple[int, str | None]:
+ """A count, or the enum it names, which semantics.py counts."""
+ if count is None:
+ return 1, None
+ if count == '':
+ return UNLIMITED, None
+ if not count.isdigit():
+ return 1, count
+ value = int(count)
+ if value < 1 or value > U32_MAX:
+ raise CtlvError(where, f'count {value} is outside 1 to u32-max')
+ return value, None
+
+ def _integer_value(self, where: str, text: str, kind: str,
+ unit: str | None) -> int:
+ if text in NAMED_NUMBERS:
+ return NAMED_NUMBERS[text]
+ found = _NUMBER.fullmatch(text)
+ if found is None:
+ raise CtlvError(where, f'"{text}" is not a number')
+ if found['suffix'] and unit != 'bytes':
+ raise CtlvError(where, f'"{text}": K, M and G are for byte '
+ f'counts; give the {kind} the bytes unit')
+ factor = dict(SIZE_SUFFIXES).get(found['suffix'], 1)
+ return int(found['value']) * factor
+
+ def _size(self, where: str, text: str, what: str,
+ limit: int = U32_MAX) -> int:
+ if text in NAMED_NUMBERS:
+ value = NAMED_NUMBERS[text]
+ else:
+ found = _NUMBER.fullmatch(text)
+ if found is None:
+ raise CtlvError(where, f'{what} "{text}" is not a number')
+ value = int(found['value'])
+ for suffix, scale in SIZE_SUFFIXES:
+ if found['suffix'] == suffix:
+ value *= scale
+ if value < 0 or value > limit:
+ raise CtlvError(where, f'{what} {text} is outside 0 to '
+ f'{number_text(limit)}')
+ return value
+
+ def _range(self, where: str, text: str,
+ parse: Callable[[str], int]) -> tuple[int | None, int | None]:
+ low, _, high = text.partition('..')
+ return (parse(low.strip()) if low.strip() else None,
+ parse(high.strip()) if high.strip() else None)
+
+ def _integer(self, where: str, kind: str,
+ args: str | None) -> dict[str, Any]:
+ low, high = (0, U64_MAX) if kind == 'uint' else (S64_MIN, S64_MAX)
+ minimum, maximum, unit, scale = low, high, None, None
+ bounds = None
+ for arg in _arguments(args) if args is not None else ():
+ scaled = _SCALE.fullmatch(arg)
+ if '..' in arg and bounds is None:
+ bounds = arg
+ elif arg in UNITS and unit is None:
+ unit = arg
+ elif scaled is not None and scale is None:
+ scale = int(scaled['value'])
+ else:
+ raise CtlvError(where, f'"{arg}" is not a range, a unit or '
+ 'a scale, or is given twice')
+ if bounds is not None:
+ first, last = self._range(
+ where, bounds,
+ lambda text: self._integer_value(where, text, kind, unit))
+ minimum = low if first is None else first
+ maximum = high if last is None else last
+ if minimum < low or minimum > high:
+ raise CtlvError(where, f'minimum {minimum} is outside the '
+ f'{kind} range')
+ if maximum < low or maximum > high:
+ raise CtlvError(where, f'maximum {maximum} is outside the '
+ f'{kind} range')
+ if minimum > maximum:
+ raise CtlvError(where, f'minimum {minimum} is greater than '
+ f'maximum {maximum}')
+ if scale is not None and unit is None:
+ raise CtlvError(where, 'scale requires a unit')
+ if unit is not None and scale is None:
+ scale = 0
+ if scale is not None and (scale < S8_MIN or scale > S8_MAX):
+ raise CtlvError(where, f'scale {scale} is outside the s8 range')
+ return {'min': minimum, 'max': maximum, 'unit': unit, 'scale': scale}
+
+ def _lengths(self, where: str, kind: str,
+ args: str | None) -> dict[str, Any]:
+ if args is None or '..' not in args and not args.strip():
+ raise CtlvError(where, f'a {kind} needs a maximum length, '
+ f'as {kind}(..N)')
+
+ def size(text: str) -> int:
+ return self._size(where, text, 'length')
+
+ if '..' in args:
+ minimum, maximum = self._range(where, args, size)
+ else:
+ minimum = maximum = size(args.strip())
+ if maximum is None:
+ raise CtlvError(where, f'a {kind} needs a maximum length, '
+ f'as {kind}(..N)')
+ minimum = minimum or 0
+ if minimum > maximum:
+ raise CtlvError(where, f'minimum length {minimum} is greater '
+ f'than maximum length {maximum}')
+ return {'min_length': minimum, 'max_length': maximum}
+
+ def _blob(self, where: str, args: str) -> BlobDecl:
+ parts = _arguments(args) if args is not None else ['']
+ direction = DIRECTIONS.get(parts[0])
+ if direction is None:
+ raise CtlvError(where, 'a blob starts with its direction, in, '
+ 'out or inout')
+ maximum, alignment, backings = None, None, None
+ for arg in parts[1:]:
+ aligned = _ALIGN.fullmatch(arg)
+ if arg.startswith('..') and maximum is None:
+ maximum = self._size(where, arg[2:].strip(), 'blob length',
+ U64_MAX)
+ elif aligned is not None and alignment is None:
+ alignment = self._size(where, aligned['value'],
+ 'blob alignment')
+ elif backings is None and all(word in BACKINGS
+ for word in arg.split()):
+ backings = tuple(arg.split())
+ if len(set(backings)) != len(backings):
+ raise CtlvError(where, f'"{arg}" names a backing twice')
+ else:
+ raise CtlvError(where, f'"{arg}" is not a maximum length, an '
+ 'alignment or a list of backings, or '
+ 'is given twice')
+ maximum = U32_MAX if maximum is None else maximum
+ alignment = 1 if alignment is None else alignment
+ if maximum < 1:
+ raise CtlvError(where, 'a blob holds at least one byte')
+ if alignment < 1 or alignment & (alignment - 1):
+ raise CtlvError(where, f'blob alignment {alignment} is not a '
+ 'power of two')
+ if maximum % alignment:
+ raise CtlvError(where, f'blob length {size_text(maximum)} is not '
+ 'a multiple of alignment '
+ f'{size_text(alignment)}')
+ return BlobDecl(direction=direction,
+ backings=backings or DEFAULT_BACKINGS,
+ max_length=maximum, alignment=alignment)
+
+ def message(self, raw_op: dict[str, Any], key: str,
+ where: str) -> MessageSpec:
+ raw = raw_op.get(key)
+ doc = self.doc(raw_op, key)
+ if raw is None or raw == {}:
+ return MessageSpec(where=where, doc=doc)
+ if isinstance(raw, str):
+ found = _REFERENCE.fullmatch(raw)
+ assert found is not None
+ return MessageSpec(ref=found['name'], where=where, doc=doc)
+ return MessageSpec(body=self.body(raw, where), where=where, doc=doc)
+
+ def ops(self, raw_ops: dict[str, Any]) -> tuple[OpSpec, ...]:
+ ops = []
+ for name, raw in raw_ops.items():
+ where = self.where(raw_ops, name, f'op {name}')
+ op_type = raw['type']
+ suggested = raw.get('suggested-reply-length')
+ if suggested is not None:
+ suggested = self._size(where, str(suggested),
+ 'suggested-reply-length')
+ self._check_op_keys(where, raw, op_type)
+ request = self.message(raw, 'request', f'{where} request')
+ reply = None
+ if 'reply' in raw:
+ reply = self.message(raw, 'reply', f'{where} reply')
+ ops.append(OpSpec(
+ name=name,
+ type=op_type,
+ where=where,
+ request=request,
+ reply=reply,
+ fd_access=raw.get('fd-access', DEFAULT_FD_ACCESS),
+ availability=raw.get('availability', DEFAULT_AVAILABILITY),
+ optional=raw.get('optional', False),
+ pre_post=raw.get('pre-post', False),
+ suggested_reply_length=suggested,
+ doc=self.doc(raw_ops, name),
+ ))
+ return tuple(ops)
+
+ def _check_op_keys(self, where: str, raw: dict[str, Any],
+ op_type: str) -> None:
+ if op_type == 'query' and 'reply' not in raw:
+ raise CtlvError(where, 'a query op must declare a reply')
+ if 'suggested-reply-length' in raw and op_type != 'query':
+ raise CtlvError(where, 'suggested-reply-length is only valid on '
+ 'a query op')
diff --git a/tools/ctlv/ctlvgen/lib/tree.py b/tools/ctlv/ctlvgen/lib/tree.py
new file mode 100644
index 000000000000..724c6569e9e3
--- /dev/null
+++ b/tools/ctlv/ctlvgen/lib/tree.py
@@ -0,0 +1,158 @@
+# SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+"""Whole-tree registry, specification and ABI checks, and regeneration."""
+
+from __future__ import annotations
+
+import pathlib
+import subprocess
+from collections.abc import Collection
+
+from . import compat
+from . import outputs
+from . import semantics
+from . import spec
+from .errors import CtlvError
+
+
+def _stems(directory: pathlib.Path, exclude: tuple[str, ...] = ()) -> set[str]:
+ return {path.stem for path in directory.glob('*.yaml')
+ if path.name not in exclude}
+
+
+def coverage_problems(
+ paths: spec.Paths, registry: spec.Registry, snapshots: bool = True,
+ allowed_missing_snapshots: Collection[str] = ()) -> list[str]:
+ expected = {entry.name for entry in registry.entries}
+ problems = []
+
+ actual_specs = _stems(paths.spec_dir)
+ for name in sorted(expected - actual_specs):
+ problems.append(f'missing specification {name}.yaml')
+ for name in sorted(actual_specs - expected):
+ problems.append(f'unexpected specification {name}.yaml')
+
+ if snapshots:
+ actual_abi = _stems(paths.abi_dir,
+ exclude=(paths.registry_abi.name,))
+ missing = expected - actual_abi - set(allowed_missing_snapshots)
+ for name in sorted(missing):
+ problems.append(f'missing ABI snapshot {name}.yaml')
+ for name in sorted(actual_abi - expected):
+ problems.append(f'unexpected ABI snapshot {name}.yaml')
+ return problems
+
+
+def _relative(paths: spec.Paths,
+ path: pathlib.Path) -> pathlib.PurePosixPath:
+ return pathlib.PurePosixPath(path.relative_to(paths.kernel_dir).as_posix())
+
+
+def _families(paths: spec.Paths,
+ registry: spec.Registry) -> tuple[semantics.Family, ...]:
+ return tuple(semantics.load(spec.Paths(
+ paths.spec_dir / f'{entry.name}.yaml', schema=paths.schema,
+ registry=paths.registry)) for entry in registry.entries)
+
+
+def _snapshots(paths: spec.Paths, registry: spec.Registry,
+ families: tuple[semantics.Family, ...]
+ ) -> dict[pathlib.PurePosixPath, str]:
+ texts = {_relative(paths, paths.registry_abi):
+ compat.registry_render(registry)}
+ for family in families:
+ texts[_relative(paths, paths.abi(family.name))] = compat.render(family)
+ return texts
+
+
+def _git(root: pathlib.Path, *args: str) -> subprocess.CompletedProcess[str]:
+ return subprocess.run(['git', *args], cwd=root, capture_output=True,
+ text=True, check=False)
+
+
+def _baseline(root: pathlib.Path, relative: pathlib.PurePosixPath,
+ committed: bool) -> tuple[str, str | None]:
+ """Where the ABI to keep is read from, and its text, or None for none.
+
+ Regeneration keeps what HEAD has, so that what it wrote but nobody
+ committed can still change. A check, or a tree without git, keeps the file.
+ """
+ if committed and (root / '.git').exists():
+ where = f'HEAD:./{relative}'
+ found = _git(root, 'rev-parse', '--verify', '--quiet', where)
+ if found.returncode == 1:
+ return where, None
+ if found.returncode == 0:
+ found = _git(root, 'cat-file', 'blob', found.stdout.strip())
+ if found.returncode:
+ raise CtlvError(str(root), found.stderr.strip())
+ return where, found.stdout
+ path = root.joinpath(*relative.parts)
+ if not path.is_file():
+ return str(path), None
+ return str(path), path.read_text(encoding='utf-8')
+
+
+def _breaks(paths: spec.Paths, root: pathlib.Path, registry: spec.Registry,
+ families: tuple[semantics.Family, ...],
+ committed: bool) -> list[str]:
+ problems = []
+ where, text = _baseline(root, _relative(paths, paths.registry_abi),
+ committed)
+ if text is not None:
+ baseline = spec.load_registry(paths, where, text)
+ problems += [f'registry: {problem}' for problem in
+ compat.registry_problems(baseline, registry)]
+ for family in families:
+ where, text = _baseline(root, _relative(paths, paths.abi(family.name)),
+ committed)
+ if text is not None:
+ problems += [f'{family.name}: {problem}' for problem in
+ compat.breaks(family, where, text)]
+ return problems
+
+
+def _root(paths: spec.Paths,
+ output_root: pathlib.Path | str | None) -> pathlib.Path:
+ return pathlib.Path(paths.kernel_dir if output_root is None
+ else output_root)
+
+
+def check(paths: spec.Paths,
+ output_root: pathlib.Path | str | None = None) -> None:
+ """Fail on anything regeneration would change or refuse."""
+ registry = spec.load_registry(paths)
+ problems = coverage_problems(paths, registry)
+ if problems:
+ raise CtlvError('CTLV tree', '\n'.join(problems))
+ families = _families(paths, registry)
+ problems = _breaks(paths, _root(paths, output_root), registry, families,
+ committed=False)
+ if problems:
+ raise CtlvError('the ABI snapshots', 'the specifications break them' +
+ ''.join(f'\n {problem}' for problem in problems))
+ outputs.reconcile(paths, output_root, check=True,
+ extra=_snapshots(paths, registry, families))
+
+
+def update(paths: spec.Paths, output_root: pathlib.Path | str | None = None,
+ abi_break: bool = False) -> tuple[str, ...]:
+ """Regenerate every output and ABI snapshot, and say what changed.
+
+ A change that breaks the committed ABI is refused unless abi_break.
+ """
+ registry = spec.load_registry(paths)
+ problems = coverage_problems(
+ paths, registry,
+ allowed_missing_snapshots=[entry.name for entry in registry.entries])
+ if problems:
+ raise CtlvError('CTLV tree', '\n'.join(problems))
+ families = _families(paths, registry)
+ problems = _breaks(paths, _root(paths, output_root), registry, families,
+ committed=True)
+ if problems and not abi_break:
+ raise CtlvError('the committed ABI', 'the specifications break it' +
+ ''.join(f'\n {problem}' for problem in problems) +
+ '\nregen with ABI_BREAK=1 writes it anyway')
+ changed = outputs.reconcile(paths, output_root,
+ extra=_snapshots(paths, registry, families))
+ return tuple(f'ABI break, {problem}' for problem in problems) + changed
diff --git a/tools/ctlv/ctlvgen/lib/yamlload.py b/tools/ctlv/ctlvgen/lib/yamlload.py
new file mode 100644
index 000000000000..0a470fa58d09
--- /dev/null
+++ b/tools/ctlv/ctlvgen/lib/yamlload.py
@@ -0,0 +1,138 @@
+# SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+"""Strict YAML subset loader.
+
+Anchors, aliases, merge keys, explicit tags, duplicate mapping keys and
+multiple documents per file are all rejected: they let two readers of a file
+that defines an ABI disagree about what it says.
+"""
+
+from __future__ import annotations
+
+import pathlib
+from collections.abc import Hashable, Iterator
+from typing import Any, NoReturn
+
+import yaml
+
+from .errors import CtlvError
+
+
+class Mapping(dict[Any, Any]):
+ """A mapping that knows the line each of its keys is on, from zero."""
+
+ def __init__(self) -> None:
+ super().__init__()
+ self.lines: dict[Any, int] = {}
+
+
+class Sequence(list[Any]):
+ """A list that knows the line each of its items starts on, from zero."""
+
+ def __init__(self) -> None:
+ super().__init__()
+ self.lines: list[int] = []
+
+
+def _mark(path: pathlib.Path | str, mark: yaml.error.Mark | None) -> str:
+ if mark is None:
+ return str(path)
+ return f'{path}:{mark.line + 1}'
+
+
+class _StrictLoader(yaml.SafeLoader):
+ """SafeLoader without the features that make a document ambiguous."""
+
+ def __init__(self, stream: str, path: pathlib.Path | str) -> None:
+ super().__init__(stream)
+ self.ctlv_path = path
+
+ def _reject(self, mark: yaml.error.Mark, what: str) -> NoReturn:
+ raise CtlvError(_mark(self.ctlv_path, mark), what)
+
+ def compose_node(self, parent: yaml.nodes.Node | None,
+ index: int) -> yaml.nodes.Node | None:
+ event = self.peek_event()
+ mark = event.start_mark
+ if isinstance(event, yaml.events.AliasEvent):
+ self._reject(mark, 'YAML aliases are not allowed')
+ if getattr(event, 'anchor', None) is not None:
+ self._reject(mark, 'YAML anchors are not allowed')
+ if _has_explicit_tag(event):
+ self._reject(mark,
+ f'explicit YAML tag "{event.tag}" is not allowed')
+ return super().compose_node(parent, index)
+
+ def construct_mapping(self, node: yaml.nodes.MappingNode,
+ deep: bool = False) -> dict[Hashable, Any]:
+ seen = set()
+ for key_node, _ in node.value:
+ if key_node.tag == 'tag:yaml.org,2002:merge':
+ self._reject(key_node.start_mark,
+ 'YAML merge keys are not allowed')
+ key = self.construct_object(key_node, deep=deep)
+ if key in seen:
+ self._reject(key_node.start_mark, f'duplicate key "{key}"')
+ seen.add(key)
+ return super().construct_mapping(node, deep=deep)
+
+
+def _construct_map(loader: _StrictLoader,
+ node: yaml.nodes.MappingNode) -> Iterator[Mapping]:
+ data = Mapping()
+ yield data
+ data.update(loader.construct_mapping(node))
+ for key_node, _value in node.value:
+ key = loader.construct_object(key_node)
+ data.lines[key] = key_node.start_mark.line
+
+
+def _construct_seq(loader: _StrictLoader,
+ node: yaml.nodes.SequenceNode) -> Iterator[Sequence]:
+ data = Sequence()
+ yield data
+ data.extend(loader.construct_sequence(node))
+ data.lines = [item.start_mark.line for item in node.value]
+
+
+_StrictLoader.add_constructor('tag:yaml.org,2002:map', _construct_map)
+_StrictLoader.add_constructor('tag:yaml.org,2002:seq', _construct_seq)
+
+
+def _has_explicit_tag(event: yaml.events.Event) -> bool:
+ if getattr(event, 'tag', None) is None:
+ return False
+ implicit = getattr(event, 'implicit', False)
+ if isinstance(implicit, tuple):
+ return not (implicit[0] or implicit[1])
+ return not implicit
+
+
+def load(path: pathlib.Path | str, text: str | None = None) -> Mapping:
+ """Return the single YAML document in path, or in text read from it."""
+ return load_with_lines(path, text)[0]
+
+
+def load_with_lines(path: pathlib.Path | str,
+ text: str | None = None) -> tuple[Mapping, list[str]]:
+ """Return the document in path, or in text, and the lines of its text."""
+ try:
+ if text is None:
+ with open(path, 'r', encoding='utf-8') as stream:
+ text = stream.read()
+ loader = _StrictLoader(text, path)
+ try:
+ data = loader.get_single_data()
+ finally:
+ loader.dispose()
+ except OSError as exc:
+ raise CtlvError(str(path), f'cannot read: {exc.strerror}') from exc
+ except yaml.YAMLError as exc:
+ mark = getattr(exc, 'problem_mark', None)
+ described = [getattr(exc, 'context', None),
+ getattr(exc, 'problem', None)]
+ problem = ', '.join(part for part in described if part) or str(exc)
+ raise CtlvError(_mark(path, mark), problem) from exc
+
+ if not isinstance(data, Mapping):
+ raise CtlvError(str(path), 'expected a YAML mapping at the top level')
+ return data, text.splitlines()
diff --git a/tools/ctlv/pyproject.toml b/tools/ctlv/pyproject.toml
new file mode 100644
index 000000000000..fe1d04f589b2
--- /dev/null
+++ b/tools/ctlv/pyproject.toml
@@ -0,0 +1,30 @@
+[build-system]
+requires = ["setuptools>=61.0"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "ctlvgen"
+authors = [
+ {name = "Jiri Pirko", email = "jiri@resnulli.us"},
+]
+description = "CTLV schema tool"
+version = "0.0.1"
+requires-python = ">=3.9"
+dependencies = [
+ "pyyaml==6.*",
+ "jsonschema==4.*"
+]
+
+[tool.setuptools.packages.find]
+include = ["ctlvgen", "ctlvgen.lib"]
+
+[project.scripts]
+ctlvgen = "ctlvgen.cli:main"
+
+[tool.mypy]
+strict = true
+
+# PyYAML's stubs leave parser methods the loader calls untyped.
+[[tool.mypy.overrides]]
+module = "ctlvgen.lib.yamlload"
+disallow_untyped_calls = false
diff --git a/tools/ctlv/requirements.txt b/tools/ctlv/requirements.txt
new file mode 100644
index 000000000000..2a73e762f9b9
--- /dev/null
+++ b/tools/ctlv/requirements.txt
@@ -0,0 +1,2 @@
+pyyaml==6.*
+jsonschema==4.*
--
2.56.0
^ permalink raw reply related [flat|nested] 24+ messages in thread
* [PATCH RFC v2 02/15] ctlv: Add the UAPI message format and family API
2026-11-13 15:54 [PATCH RFC v2 00/15] ctlv: Add a schema-driven TLV uAPI for character devices Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 01/15] ctlv: Add the schema language and generator Jiri Pirko
@ 2026-11-13 15:55 ` Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 03/15] ctlv: Add the root family specification and generated files Jiri Pirko
` (12 subsequent siblings)
14 siblings, 0 replies; 24+ messages in thread
From: Jiri Pirko @ 2026-11-13 15:55 UTC (permalink / raw)
To: linux-coco
Cc: linux-kernel, linux-api, x86, kvm, linux-doc, jgg, david.quigley,
lukas, ksinyuk, rodrigo.vivi, cedric.xing, sameo, drdeeglaze,
James.Bottomley, aik, jpiotrowski, qinkun, pgonda, jic23,
alistair.francis, ankita, suzuki.poulose, kas, rick.p.edgecombe,
elena.reshetova, dave.hansen, sathyanarayanan.kuppuswamy,
peter.fang, jarkko, zohar, yeoreum.yun, aneesh.kumar, dave.jiang,
nik.borisov, sergii.dmytruk
From: Jiri Pirko <jiri@nvidia.com>
Define CTLV's common TLV transport, ioctl and identifiers. Add the
in-kernel API used by family implementations and generated code,
including the implementation limits: userspace learns the lengths of
each op from the device, so they stay out of the UAPI.
Assisted-by: Cursor: GPT-5.6 Sol
Assisted-by: Cursor: Claude Opus 5.5
Signed-off-by: Jiri Pirko <jiri@nvidia.com>
---
RFC->RFCv2:
- handed family callbacks the CTLV instance, and dropped the family
priv and its lifecycle hooks
- sized struct ctlv_op_exec by a field, not the ioctl number
- moved the implementation limits to <linux/ctlv_family.h>
- lowered the inline message limit to 64 KiB
- lowered the record depth limit to 8
- aligned attrs with __ALIGN_KERNEL()
- defined the attr flags with _BITUL()
- spelled out the callback signatures in the ops structures
- wrote the schema macros as compound literals
- dropped the event record header and CTLV_OP_SCHEMA_EVENT()
- renamed the stub schemas and ops to extension schemas and ops
- let an enum's range start at any value
- said how long a cursor lives and what bounds the record depth
- renamed struct ctlv_attr to struct ctlv_hdr and its attr_id to type
- built a type from the family, a record and a field, in place of an
attr group and number
- said TLV, type, record and field instead of attr
- said record instead of nest, the root being one
- dropped "physical" from the cursor kernel-doc
- numbered every enum member
- moved the UAPI header to include/uapi/ctlv/, as <ctlv/ctlv.h>
- added the schemas of enum extension points and their fills
- said message instead of wire
- retitled from "Add the UAPI wire format and family API"
- moved the ioctl number registration to the dispatch patch
---
MAINTAINERS | 2 +
include/linux/ctlv.h | 57 +++++
include/linux/ctlv_family.h | 431 ++++++++++++++++++++++++++++++++++++
include/uapi/ctlv/ctlv.h | 180 +++++++++++++++
4 files changed, 670 insertions(+)
create mode 100644 include/linux/ctlv.h
create mode 100644 include/linux/ctlv_family.h
create mode 100644 include/uapi/ctlv/ctlv.h
diff --git a/MAINTAINERS b/MAINTAINERS
index 5c8e7ad9b599..bf6d5bab0da9 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -7188,6 +7188,8 @@ M: Jiri Pirko <jiri@resnulli.us>
L: linux-kernel@vger.kernel.org
S: Maintained
F: Documentation/ctlv/
+F: include/linux/ctlv*.h
+F: include/uapi/ctlv/
F: tools/ctlv/
CTU CAN FD DRIVER
diff --git a/include/linux/ctlv.h b/include/linux/ctlv.h
new file mode 100644
index 000000000000..e151e32a0ffd
--- /dev/null
+++ b/include/linux/ctlv.h
@@ -0,0 +1,57 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES
+ *
+ * CTLV family API.
+ */
+#ifndef _LINUX_CTLV_H
+#define _LINUX_CTLV_H
+
+#include <linux/types.h>
+
+#include <uapi/ctlv/ctlv.h>
+
+struct ctlv_family;
+struct ctlv;
+struct ctlv_msg;
+struct ctlv_op_ctx;
+struct ctlv_schema_tree;
+
+/**
+ * struct ctlv_cursor - One TLV position in one message.
+ * @msg: the message this position is in, core-private bookkeeping
+ * @hdr: the TLV itself, or NULL when a lookup found nothing
+ * @parent: reading, the record containing @hdr, NULL at the root
+ * @up: writing, cursor of the record containing @hdr, NULL at the root. The
+ * chain of them is the open-record stack, so a cursor must not outlive the
+ * cursor of its record
+ * @start: writing, where @hdr begins in the message. An append reaches what
+ * it lengthens through the buffer rather than through @hdr, that buffer
+ * being the core's own
+ * @tree: schema tree the node belongs to, which is a fill's own tree for a
+ * TLV that arrived through one
+ * @node: index of the node within @tree
+ * @depth: record levels above this position, the root being zero
+ * @index_record_scope: schema scope used to read the members of @hdr, a record
+ * @index_scope: core-private schema scope containing this TLV
+ *
+ * Fixed-size, so family code can put a child cursor on the stack.
+ * ctlv_record_start() opens at most CTLV_MAX_RECORD_DEPTH levels, which bounds
+ * that stack. The members belong to the core: a family reaches a message
+ * through what was generated for the op it answers. One direction at a time,
+ * a helper of the other direction refusing the cursor.
+ */
+struct ctlv_cursor {
+ struct ctlv_msg *msg;
+ const struct ctlv_hdr *hdr;
+ const struct ctlv_hdr *parent;
+ struct ctlv_cursor *up;
+ size_t start;
+ const struct ctlv_schema_tree *tree;
+ u32 node;
+ u32 depth;
+ u32 index_record_scope;
+ u32 index_scope;
+};
+
+#endif /* _LINUX_CTLV_H */
diff --git a/include/linux/ctlv_family.h b/include/linux/ctlv_family.h
new file mode 100644
index 000000000000..360eb6e30974
--- /dev/null
+++ b/include/linux/ctlv_family.h
@@ -0,0 +1,431 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES
+ *
+ * CTLV family metadata and generated-code support.
+ */
+#ifndef _LINUX_CTLV_FAMILY_H
+#define _LINUX_CTLV_FAMILY_H
+
+#include <linux/ctlv.h>
+#include <linux/kobject.h>
+#include <linux/types.h>
+
+#include <uapi/ctlv/ctlv.h>
+
+/*
+ * Implementation limits, not maxima of message fields: userspace learns the
+ * lengths of each op from op-get, so raising one is compatible.
+ */
+
+/* Request roots, action replies and query reply fragments. */
+#define CTLV_MAX_INLINE_MESSAGE_LEN 0x10000 /* 64 KiB */
+/* Includes the error information root. */
+#define CTLV_MAX_ERROR_LEN 0x4000 /* 16 KiB */
+/* The root is at depth zero. Validation recurses once per level. */
+#define CTLV_MAX_RECORD_DEPTH 8
+
+/* op-schema-get needs one level to describe the declared schema. */
+#define CTLV_MAX_DECLARED_RECORD_DEPTH (CTLV_MAX_RECORD_DEPTH - 1)
+
+struct ctlv_node_schema {
+ u64 type;
+ u8 kind;
+ u8 unit;
+ s8 scale;
+ bool reenters;
+ /* An enum extension point, which enum_ext names in place of a range. */
+ bool is_enum_ext;
+ u32 max_occurrences;
+ u32 first_child;
+ u32 children_count;
+ u32 required_children_count;
+ union {
+ struct {
+ u64 min;
+ u64 max;
+ } range;
+ u64 flags_mask;
+ struct {
+ u32 min_len;
+ u32 max_len;
+ } length;
+ struct {
+ u64 max_len;
+ u32 alignment;
+ u16 backings;
+ u8 direction;
+ } blob;
+ struct {
+ u16 family_id;
+ u32 ext_id;
+ } enum_ext;
+ };
+};
+
+#define CTLV_NODE_SCHEMA_UNLIMITED 0
+
+/* Node 0 of every tree: the message root, holding the top-level fields. */
+#define CTLV_NODE_SCHEMA_ROOT(_first, _count, _required) \
+ ((struct ctlv_node_schema){ \
+ .type = CTLV_TYPE_ROOT, \
+ .kind = CTLV_SCHEMA_RECORD, \
+ .max_occurrences = 1, \
+ .first_child = (_first), \
+ .children_count = (_count), \
+ .required_children_count = (_required), \
+ })
+
+/* A record whose members are the _count nodes from _first, required first. */
+#define CTLV_NODE_SCHEMA_RECORD(_type, _occurrences, _first, _count, _required) \
+ ((struct ctlv_node_schema){ \
+ .type = (_type), \
+ .kind = CTLV_SCHEMA_RECORD, \
+ .max_occurrences = (_occurrences), \
+ .first_child = (_first), \
+ .children_count = (_count), \
+ .required_children_count = (_required), \
+ })
+
+/* A recursive record, sharing the members of the one above holding its set. */
+#define CTLV_NODE_SCHEMA_RECORD_REENTERS(_type, _occurrences, _first, _count, _required) \
+ ((struct ctlv_node_schema){ \
+ .type = (_type), \
+ .kind = CTLV_SCHEMA_RECORD, \
+ .reenters = true, \
+ .max_occurrences = (_occurrences), \
+ .first_child = (_first), \
+ .children_count = (_count), \
+ .required_children_count = (_required), \
+ })
+
+/* A u64 between _min and _max. */
+#define CTLV_NODE_SCHEMA_UINT(_type, _occurrences, _min, _max) \
+ ((struct ctlv_node_schema){ \
+ .type = (_type), \
+ .kind = CTLV_SCHEMA_UINT, \
+ .max_occurrences = (_occurrences), \
+ .range = { .min = (_min), .max = (_max) }, \
+ })
+
+/* A u64 between _min and _max that carries a unit and a scale. */
+#define CTLV_NODE_SCHEMA_UINT_UNIT(_type, _occurrences, _min, _max, _unit, _scale) \
+ ((struct ctlv_node_schema){ \
+ .type = (_type), \
+ .kind = CTLV_SCHEMA_UINT, \
+ .max_occurrences = (_occurrences), \
+ .range = { .min = (_min), .max = (_max) }, \
+ .unit = (_unit), \
+ .scale = (_scale), \
+ })
+
+/* An s64 between _min and _max, both stored as u64. */
+#define CTLV_NODE_SCHEMA_SINT(_type, _occurrences, _min, _max) \
+ ((struct ctlv_node_schema){ \
+ .type = (_type), \
+ .kind = CTLV_SCHEMA_SINT, \
+ .max_occurrences = (_occurrences), \
+ .range = { .min = (_min), .max = (_max) }, \
+ })
+
+/* An s64 between _min and _max that carries a unit and a scale. */
+#define CTLV_NODE_SCHEMA_SINT_UNIT(_type, _occurrences, _min, _max, _unit, _scale) \
+ ((struct ctlv_node_schema){ \
+ .type = (_type), \
+ .kind = CTLV_SCHEMA_SINT, \
+ .max_occurrences = (_occurrences), \
+ .range = { .min = (_min), .max = (_max) }, \
+ .unit = (_unit), \
+ .scale = (_scale), \
+ })
+
+/* An enum taking _min to _max, gaps between a header enum's values included. */
+#define CTLV_NODE_SCHEMA_ENUM_RANGE(_type, _occurrences, _min, _max) \
+ ((struct ctlv_node_schema){ \
+ .type = (_type), \
+ .kind = CTLV_SCHEMA_ENUM, \
+ .max_occurrences = (_occurrences), \
+ .range = { .min = (_min), .max = (_max) }, \
+ })
+
+/* A family's own enum, whose values are numbered from 1 to _max. */
+#define CTLV_NODE_SCHEMA_ENUM(_type, _occurrences, _max) \
+ CTLV_NODE_SCHEMA_ENUM_RANGE(_type, _occurrences, 1, _max)
+
+/* Enum extension point _ext of _family, numbered as the device's family fills it. */
+#define CTLV_NODE_SCHEMA_ENUM_EXT(_type, _occurrences, _family, _ext) \
+ ((struct ctlv_node_schema){ \
+ .type = (_type), \
+ .kind = CTLV_SCHEMA_ENUM, \
+ .is_enum_ext = true, \
+ .max_occurrences = (_occurrences), \
+ .enum_ext = { .family_id = (_family), .ext_id = (_ext) }, \
+ })
+
+/* A u64 of flags, none of them outside _mask. */
+#define CTLV_NODE_SCHEMA_FLAGS(_type, _occurrences, _mask) \
+ ((struct ctlv_node_schema){ \
+ .type = (_type), \
+ .kind = CTLV_SCHEMA_FLAGS, \
+ .max_occurrences = (_occurrences), \
+ .flags_mask = (_mask), \
+ })
+
+/* A u8 that is 0 or 1. */
+#define CTLV_NODE_SCHEMA_BOOL(_type, _occurrences) \
+ ((struct ctlv_node_schema){ \
+ .type = (_type), \
+ .kind = CTLV_SCHEMA_BOOL, \
+ .max_occurrences = (_occurrences), \
+ })
+
+/* A NUL-terminated string of _min to _max bytes, the NUL excluded. */
+#define CTLV_NODE_SCHEMA_STRING(_type, _occurrences, _min, _max) \
+ ((struct ctlv_node_schema){ \
+ .type = (_type), \
+ .kind = CTLV_SCHEMA_STRING, \
+ .max_occurrences = (_occurrences), \
+ .length = { .min_len = (_min), .max_len = (_max) }, \
+ })
+
+/* A payload of _min to _max bytes. */
+#define CTLV_NODE_SCHEMA_BINARY(_type, _occurrences, _min, _max) \
+ ((struct ctlv_node_schema){ \
+ .type = (_type), \
+ .kind = CTLV_SCHEMA_BINARY, \
+ .max_occurrences = (_occurrences), \
+ .length = { .min_len = (_min), .max_len = (_max) }, \
+ })
+
+/* A blob descriptor, for data passed by reference instead of inline. */
+#define CTLV_NODE_SCHEMA_BLOB(_type, _occurrences, _max, _alignment, _backings, _dir) \
+ ((struct ctlv_node_schema){ \
+ .type = (_type), \
+ .kind = CTLV_SCHEMA_BLOB, \
+ .max_occurrences = (_occurrences), \
+ .blob = { \
+ .max_len = (_max), \
+ .alignment = (_alignment), \
+ .backings = (_backings), \
+ .direction = (_dir), \
+ }, \
+ })
+
+struct ctlv_ext_schema {
+ u16 family_id;
+ u32 ext_id;
+ u32 node;
+ bool optional;
+};
+
+/* An extension point at _node, which a descendant family fills. */
+#define CTLV_EXT_SCHEMA(_family, _ext, _node) \
+ ((struct ctlv_ext_schema){ \
+ .family_id = (_family), \
+ .ext_id = (_ext), \
+ .node = (_node), \
+ })
+
+/* An optional extension point: what fills it is optional too. */
+#define CTLV_EXT_SCHEMA_OPTIONAL(_family, _ext, _node) \
+ ((struct ctlv_ext_schema){ \
+ .family_id = (_family), \
+ .ext_id = (_ext), \
+ .node = (_node), \
+ .optional = true, \
+ })
+
+struct ctlv_schema_tree {
+ const struct ctlv_node_schema *node_schemas;
+ u32 node_schemas_count;
+ const struct ctlv_ext_schema *ext_schemas;
+ u32 ext_schemas_count;
+};
+
+/* The tree of one message, from _name##_node_schemas. */
+#define CTLV_SCHEMA_TREE(_name) \
+ ((struct ctlv_schema_tree){ \
+ .node_schemas = _name##_node_schemas, \
+ .node_schemas_count = ARRAY_SIZE(_name##_node_schemas), \
+ })
+
+/* The same, for a message with extension points in _name##_ext_schemas. */
+#define CTLV_SCHEMA_TREE_EXTS(_name) \
+ ((struct ctlv_schema_tree){ \
+ .node_schemas = _name##_node_schemas, \
+ .node_schemas_count = ARRAY_SIZE(_name##_node_schemas), \
+ .ext_schemas = _name##_ext_schemas, \
+ .ext_schemas_count = ARRAY_SIZE(_name##_ext_schemas), \
+ })
+
+/* The tree of a message an op does not have. */
+#define CTLV_SCHEMA_TREE_NONE() ((struct ctlv_schema_tree){})
+
+struct ctlv_ext_fill_schema {
+ u16 family_id;
+ u32 ext_id;
+ struct ctlv_schema_tree members;
+};
+
+/* What a family puts into an ancestor's extension point. */
+#define CTLV_EXT_FILL_SCHEMA(_family, _ext, _name) \
+ ((struct ctlv_ext_fill_schema){ \
+ .family_id = (_family), \
+ .ext_id = (_ext), \
+ .members = CTLV_SCHEMA_TREE(_name##_fill), \
+ })
+
+struct ctlv_enum_fill_schema {
+ u16 family_id;
+ u32 ext_id;
+ u32 max;
+};
+
+/* The values 1 to _max a family gives an ancestor's enum extension point. */
+#define CTLV_ENUM_FILL_SCHEMA(_family, _ext, _max) \
+ ((struct ctlv_enum_fill_schema){ \
+ .family_id = (_family), \
+ .ext_id = (_ext), \
+ .max = (_max), \
+ })
+
+struct ctlv_op_sizes {
+ u32 request_max_len;
+ u32 action_reply_max_len;
+ u32 query_reply_min_len;
+ u32 query_reply_suggested_len;
+};
+
+struct ctlv_op_schema {
+ u64 op_id;
+ u8 type;
+ u8 fd_access;
+ u8 availability;
+ bool optional;
+ bool pre_post;
+ struct ctlv_schema_tree request;
+ struct ctlv_schema_tree reply;
+ struct ctlv_op_sizes sizes;
+};
+
+/* An op run once per call, its reply at most _reply_len bytes. */
+#define CTLV_OP_SCHEMA_ACTION(_op, _access, _availability, _request_len, _reply_len, \
+ _request, _reply, ...) \
+ ((struct ctlv_op_schema){ \
+ .op_id = (_op), \
+ .type = CTLV_OP_TYPE_ACTION, \
+ .fd_access = (_access), \
+ .availability = (_availability), \
+ .request = _request, \
+ .reply = _reply, \
+ .sizes = { \
+ .request_max_len = (_request_len), \
+ .action_reply_max_len = (_reply_len), \
+ }, \
+ __VA_ARGS__ \
+ })
+
+/* An op answered in fragments, each needing _reply_min bytes of room. */
+#define CTLV_OP_SCHEMA_QUERY(_op, _access, _availability, _request_len, _reply_min, \
+ _reply_suggested, _request, _reply, ...) \
+ ((struct ctlv_op_schema){ \
+ .op_id = (_op), \
+ .type = CTLV_OP_TYPE_QUERY, \
+ .fd_access = (_access), \
+ .availability = (_availability), \
+ .request = _request, \
+ .reply = _reply, \
+ .sizes = { \
+ .request_max_len = (_request_len), \
+ .query_reply_min_len = (_reply_min), \
+ .query_reply_suggested_len = (_reply_suggested), \
+ }, \
+ __VA_ARGS__ \
+ })
+
+struct ctlv_family_schema {
+ const char *name;
+ u16 family_id;
+ const struct ctlv_op_schema *ops;
+ u32 ops_count;
+ const struct ctlv_ext_fill_schema *ext_fill_schemas;
+ u32 ext_fill_schemas_count;
+ const struct ctlv_enum_fill_schema *enum_fill_schemas;
+ u32 enum_fill_schemas_count;
+};
+
+/* Leave half of a uevent environment to the device model. */
+#define CTLV_UEVENT_MAX_LEN (UEVENT_BUFFER_SIZE / 2)
+#define CTLV_UEVENT_MAX_VARS (UEVENT_NUM_ENVP / 2)
+
+/* A field a reader expects among the children of the record it reads. */
+struct ctlv_input_ref {
+ u64 type;
+ u8 kind;
+};
+
+struct ctlv_op {
+ union {
+ int (*action)(struct ctlv *ctlv, struct ctlv_op_ctx *ctx,
+ const struct ctlv_cursor *request,
+ struct ctlv_cursor *reply);
+ int (*query)(struct ctlv *ctlv, struct ctlv_op_ctx *ctx,
+ const struct ctlv_cursor *request,
+ struct ctlv_cursor *reply);
+ };
+ int (*pre)(struct ctlv *ctlv, struct ctlv_op_ctx *ctx);
+ void (*post)(struct ctlv *ctlv, struct ctlv_op_ctx *ctx);
+};
+
+/* An action implemented by _name##_action. */
+#define CTLV_OP_ACTION(_name) ((struct ctlv_op){ .action = _name##_action })
+
+/* A query implemented by _name##_query. */
+#define CTLV_OP_QUERY(_name) ((struct ctlv_op){ .query = _name##_query })
+
+/* An action with its own hooks, _name##_pre and _name##_post. */
+#define CTLV_OP_ACTION_PRE_POST(_name) \
+ ((struct ctlv_op){ \
+ .action = _name##_action, \
+ .pre = _name##_pre, \
+ .post = _name##_post, \
+ })
+
+/* A query with its own hooks, _name##_pre and _name##_post. */
+#define CTLV_OP_QUERY_PRE_POST(_name) \
+ ((struct ctlv_op){ \
+ .query = _name##_query, \
+ .pre = _name##_pre, \
+ .post = _name##_post, \
+ })
+
+struct ctlv_ext_ops {
+ u16 family_id;
+ u32 ext_id;
+ int (*fill)(struct ctlv *ctlv, struct ctlv_op_ctx *ctx,
+ const struct ctlv_cursor *request,
+ struct ctlv_cursor *record);
+};
+
+/* How a family fills an ancestor's extension point. */
+#define CTLV_EXT_OPS(_family, _ext, _name) \
+ ((struct ctlv_ext_ops){ \
+ .family_id = (_family), \
+ .ext_id = (_ext), \
+ .fill = _name##_fill, \
+ })
+
+struct ctlv_family {
+ const struct ctlv_family_schema *schema;
+ const struct ctlv_family *parent;
+ const struct ctlv_op *ops;
+ u32 ops_count;
+ const struct ctlv_ext_ops *ext_ops;
+ u32 ext_ops_count;
+};
+
+extern const struct ctlv_family ctlv_family;
+
+u32 ctlv_enum_ext_max(const struct ctlv_family *family, u16 family_id, u32 ext_id);
+
+#endif /* _LINUX_CTLV_FAMILY_H */
diff --git a/include/uapi/ctlv/ctlv.h b/include/uapi/ctlv/ctlv.h
new file mode 100644
index 000000000000..654e78bab414
--- /dev/null
+++ b/include/uapi/ctlv/ctlv.h
@@ -0,0 +1,180 @@
+/* SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause) */
+/*
+ * Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES
+ *
+ * CTLV transport UAPI: the TLV message format and the op-execution ioctl.
+ * Schema-free; a family's constants are generated into <ctlv/<family>_gen.h>,
+ * the root family's into <ctlv/ctlv_gen.h>.
+ */
+#ifndef _UAPI_CTLV_CTLV_H
+#define _UAPI_CTLV_CTLV_H
+
+#include <linux/const.h>
+#include <linux/ioctl.h>
+#include <linux/types.h>
+
+/**
+ * struct ctlv_hdr - TLV header.
+ * @len: this header plus its payload, excluding this TLV's trailing padding
+ * @flags: CTLV_HDR_F_* bits
+ * @type: family, record and field, or CTLV_TYPE_ROOT for a message root
+ *
+ * TLVs are aligned to CTLV_ALIGNTO, which is why @type is __aligned_u64: a
+ * 32-bit ABI would otherwise align this structure to four bytes. A record's
+ * payload is the concatenation of CTLV_ALIGN(child.len) bytes per child, so
+ * child padding counts inside the containing record's @len while a TLV's own
+ * padding does not. Scalar payloads are native-endian. Within each request
+ * root or record, the TLVs of one @type are contiguous; different types
+ * otherwise have no required order.
+ */
+struct ctlv_hdr {
+ __u32 len;
+ __u32 flags;
+ __aligned_u64 type;
+};
+
+#define CTLV_ALIGNTO 8
+#define CTLV_ALIGN(_len) __ALIGN_KERNEL((_len), CTLV_ALIGNTO)
+
+/* Set exactly on the root and on records, clear on scalar TLVs. */
+#define CTLV_HDR_F_RECORD _BITUL(0)
+/* Query fragments: set on the root and each record still open at the end. */
+#define CTLV_HDR_F_FRAG_MORE _BITUL(1)
+/*
+ * A request TLV, never the root, that a device whose schema does not declare
+ * its type skips whole instead of refusing the message. A type that schema
+ * does declare is held to it wherever it appears, so this asks to be forgiven
+ * what this kernel has never heard of and nothing else.
+ */
+#define CTLV_HDR_F_IGNORE_IF_UNKNOWN _BITUL(2)
+
+/* The root: carries CTLV_HDR_F_RECORD, valid only at a message start. */
+#define CTLV_TYPE_ROOT 0ULL
+
+/**
+ * enum ctlv_type_kind - What the record of a type starts from.
+ * @CTLV_TYPE_KIND_NAMED: a named record
+ * @CTLV_TYPE_KIND_RESERVED: reserved, so no type has it
+ * @CTLV_TYPE_KIND_REQUEST: an op's request
+ * @CTLV_TYPE_KIND_REPLY: an op's reply
+ *
+ * Bits 47..46 of a type. The index in 45..32 is the named record's place in
+ * the family's records, or the op's number.
+ */
+enum ctlv_type_kind {
+ CTLV_TYPE_KIND_NAMED = 0,
+ CTLV_TYPE_KIND_RESERVED = 1,
+ CTLV_TYPE_KIND_REQUEST = 2,
+ CTLV_TYPE_KIND_REPLY = 3,
+};
+
+/*
+ * Family in bits 63..48, record in 47..16, field in 15..0. The record is
+ * the type kind, the index, then the path of unnamed records below them, four
+ * bits a level (31..16).
+ */
+#define CTLV_TYPE(_family, _record, _field) \
+ (((__u64)(_family) << 48) | \
+ ((__u64)(_record) << 16) | \
+ (__u64)(_field))
+
+#define CTLV_TYPE_FAMILY(_type) ((__u16)((_type) >> 48))
+#define CTLV_TYPE_KIND(_type) ((__u8)(((_type) >> 46) & 3))
+#define CTLV_TYPE_RECORD(_type) ((__u32)((_type) >> 16))
+#define CTLV_TYPE_FIELD(_type) ((__u16)(_type))
+
+/* Complete op: family in bits 63..48, zero in 47..32, op in 31..0. */
+#define CTLV_OP(_family, _op) \
+ (((__u64)(_family) << 48) | (__u64)(_op))
+
+#define CTLV_OP_FAMILY(_op) ((__u16)((_op) >> 48))
+#define CTLV_OP_NUMBER(_op) ((__u32)(_op))
+
+/**
+ * enum ctlv_unit - CTLV-defined unit of an integer field.
+ * @CTLV_UNIT_NONE: dimensionless
+ * @CTLV_UNIT_SECONDS: seconds
+ * @CTLV_UNIT_HERTZ: hertz
+ * @CTLV_UNIT_BYTES: bytes
+ * @CTLV_UNIT_CELSIUS: degrees Celsius
+ *
+ * A payload is value * 10^scale of this unit, from the decimal scale the field
+ * declares alongside. Changing either is an ABI break.
+ */
+enum ctlv_unit {
+ CTLV_UNIT_NONE = 0,
+ CTLV_UNIT_SECONDS = 1,
+ CTLV_UNIT_HERTZ = 2,
+ CTLV_UNIT_BYTES = 3,
+ CTLV_UNIT_CELSIUS = 4,
+};
+
+/**
+ * enum ctlv_schema_kind - CTLV-defined schema kind of a field.
+ * @CTLV_SCHEMA_UINT: unsigned integer payload bounded by a range
+ * @CTLV_SCHEMA_SINT: signed integer payload bounded by a range
+ * @CTLV_SCHEMA_BOOL: one payload byte holding zero or one
+ * @CTLV_SCHEMA_STRING: NUL-terminated string payload bounded by length
+ * @CTLV_SCHEMA_BINARY: opaque payload bounded by length
+ * @CTLV_SCHEMA_ENUM: unsigned integer payload restricted to a range
+ * @CTLV_SCHEMA_FLAGS: unsigned integer payload restricted to a mask
+ * @CTLV_SCHEMA_RECORD: a record, its fields described by further nodes
+ * @CTLV_SCHEMA_BLOB: blob descriptor record
+ *
+ * Exactly one schema kind applies to a field. These are the values
+ * op-schema-get reports, and the set is append-only.
+ */
+enum ctlv_schema_kind {
+ CTLV_SCHEMA_UINT = 0,
+ CTLV_SCHEMA_SINT = 1,
+ CTLV_SCHEMA_BOOL = 2,
+ CTLV_SCHEMA_STRING = 3,
+ CTLV_SCHEMA_BINARY = 4,
+ CTLV_SCHEMA_ENUM = 5,
+ CTLV_SCHEMA_FLAGS = 6,
+ CTLV_SCHEMA_RECORD = 7,
+ CTLV_SCHEMA_BLOB = 8,
+};
+
+/**
+ * struct ctlv_op_exec - Op-execution ioctl argument.
+ * @size: sizeof(struct ctlv_op_exec) as the caller was built with it
+ * @flags: must be zero
+ * @op_id: complete op ID; zero is invalid
+ * @request: struct ctlv_hdr __user *, the mandatory complete request root
+ * @reply: struct ctlv_hdr __user *, the success reply or the error
+ * information
+ * @request_len: readable bytes beginning at @request, at least one root header;
+ * the transport copy boundary, which may exceed the root's own length
+ * @reply_len: writable bytes beginning at @reply; input only, never
+ * overwritten by the kernel
+ * @generation: query only; zero on a first fragment, the previously returned
+ * value on continuation, and the query generation on output. Actions must
+ * set zero.
+ * @cursor: query only; zero starts a query and a nonzero value resumes it. On
+ * output it is the next cursor when the root carries
+ * CTLV_HDR_F_FRAG_MORE and zero when no further fragment exists. Actions
+ * must set zero.
+ *
+ * The structure only grows at its end. The kernel reads @size bytes, at least
+ * the 56 of this first version, refuses a tail it does not know unless it is
+ * zero, and writes back only within @size. @request and @reply are fixed-width
+ * representations of userspace pointers so that the compat layout is identical.
+ */
+struct ctlv_op_exec {
+ __u32 size;
+ __u32 flags;
+ __aligned_u64 op_id;
+ __aligned_u64 request;
+ __aligned_u64 reply;
+ __u32 request_len;
+ __u32 reply_len;
+ __aligned_u64 generation;
+ __aligned_u64 cursor;
+};
+
+#define CTLV_IOC_MAGIC 0xBB
+
+#define CTLV_IOC_OP_EXEC _IO(CTLV_IOC_MAGIC, 0x00)
+
+#endif /* _UAPI_CTLV_CTLV_H */
--
2.56.0
^ permalink raw reply related [flat|nested] 24+ messages in thread
* [PATCH RFC v2 03/15] ctlv: Add the root family specification and generated files
2026-11-13 15:54 [PATCH RFC v2 00/15] ctlv: Add a schema-driven TLV uAPI for character devices Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 01/15] ctlv: Add the schema language and generator Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 02/15] ctlv: Add the UAPI message format and family API Jiri Pirko
@ 2026-11-13 15:55 ` Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 04/15] Documentation: ctlv: Add userspace ABI Jiri Pirko
` (11 subsequent siblings)
14 siblings, 0 replies; 24+ messages in thread
From: Jiri Pirko @ 2026-11-13 15:55 UTC (permalink / raw)
To: linux-coco
Cc: linux-kernel, linux-api, x86, kvm, linux-doc, jgg, david.quigley,
lukas, ksinyuk, rodrigo.vivi, cedric.xing, sameo, drdeeglaze,
James.Bottomley, aik, jpiotrowski, qinkun, pgonda, jic23,
alistair.francis, ankita, suzuki.poulose, kas, rick.p.edgecombe,
elena.reshetova, dave.hansen, sathyanarayanan.kuppuswamy,
peter.fang, jarkko, zohar, yeoreum.yun, aneesh.kumar, dave.jiang,
nik.borisov, sergii.dmytruk
From: Jiri Pirko <jiri@nvidia.com>
Define the root family used by every CTLV device. Add its ABI snapshot,
registry and manifest entries, and generated outputs.
The generated kernel descriptor is preparatory and is linked after its
handlers arrive with operation dispatch.
Assisted-by: Cursor: GPT-5.6 Sol
Assisted-by: Cursor: Claude Opus 5.5
Signed-off-by: Jiri Pirko <jiri@nvidia.com>
---
RFC->RFCv2:
- reported the error information length of actions and queries
- dropped the root family's priv and its hooks
- dropped the event ops, the events and the sets, groups and attrs only
they use
- warned against adding a family before agreeing its position
- wrote the spec as op trees and records
- added required-length to the error information
- said node instead of placement
- said record instead of nest, the root being one
- numbered every enum member
- moved the generated UAPI headers to include/uapi/ctlv/
- documented every item of the spec
- put each field's doc above its generated helpers
- renamed families-dump and ops-dump to family-get and op-get
- put ctlv_gen.c in lib/ctlv/
- said message instead of wire
---
Documentation/ctlv/abi/ctlv.yaml | 111 +++++++
Documentation/ctlv/abi/families.registry.yaml | 7 +
Documentation/ctlv/families.yaml | 17 ++
Documentation/ctlv/generated-files.yaml | 17 ++
Documentation/ctlv/specs/ctlv.yaml | 211 +++++++++++++
include/linux/ctlv_gen.h | 154 ++++++++++
include/uapi/ctlv/ctlv_families_gen.h | 17 ++
include/uapi/ctlv/ctlv_gen.h | 278 ++++++++++++++++++
lib/ctlv/ctlv_gen.c | 161 ++++++++++
9 files changed, 973 insertions(+)
create mode 100644 Documentation/ctlv/abi/ctlv.yaml
create mode 100644 Documentation/ctlv/abi/families.registry.yaml
create mode 100644 Documentation/ctlv/families.yaml
create mode 100644 Documentation/ctlv/generated-files.yaml
create mode 100644 Documentation/ctlv/specs/ctlv.yaml
create mode 100644 include/linux/ctlv_gen.h
create mode 100644 include/uapi/ctlv/ctlv_families_gen.h
create mode 100644 include/uapi/ctlv/ctlv_gen.h
create mode 100644 lib/ctlv/ctlv_gen.c
diff --git a/Documentation/ctlv/abi/ctlv.yaml b/Documentation/ctlv/abi/ctlv.yaml
new file mode 100644
index 000000000000..dc26ba575a68
--- /dev/null
+++ b/Documentation/ctlv/abi/ctlv.yaml
@@ -0,0 +1,111 @@
+# SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause)
+%YAML 1.2
+---
+# Checked-in ABI snapshot of the ctlv family, from
+# Documentation/ctlv/specs/ctlv.yaml, written by
+# "make -C tools/ctlv regen".
+#
+# Updating the file is how a UAPI change gets reviewed; an update that
+# removes or renumbers anything, narrows a bound, or makes a member required
+# is an ABI break, whatever the file says afterwards.
+family: {name: ctlv, id: 0}
+enums:
+ op-type: {action: 1, query: 2}
+ op-availability: {fixed: 1, dynamic: 2}
+ blob-type: {va: 1}
+ blob-direction: {input: 1, output: 2, inout: 3}
+ validation-reason: {malformed-message: 1, unknown-type: 2, unknown-flag: 3,
+ record-too-deep: 4, missing-field: 5,
+ too-many-occurrences: 6, value-out-of-range: 7,
+ bad-length: 8, bad-string: 9, bad-blob-descriptor: 10,
+ stale-generation: 11, noncontiguous-type: 12}
+ op-fd-access: {read: 1, read-write: 2}
+records:
+ blob:
+ id: 0x00010000
+ fields:
+ type: {number: 1, enum: blob-type}
+ address: {number: 2, type: uint, optional: true}
+ fd: {number: 3, type: uint, max: s32-max, optional: true}
+ offset: {number: 4, type: uint, optional: true}
+ length: {number: 5, type: uint, min: 1}
+ error-info:
+ id: 0x00020000
+ fields:
+ reason: {number: 1, enum: validation-reason, optional: true}
+ message: {number: 2, type: string, max-length: 255, optional: true}
+ bad-tlv-offset: {number: 3, type: uint, optional: true}
+ record-offset: {number: 4, type: uint, optional: true}
+ missing-type: {number: 5, type: uint, optional: true}
+ expected-generation: {number: 6, type: uint, optional: true}
+ current-generation: {number: 7, type: uint, optional: true}
+ required-length: {number: 8, type: uint, optional: true}
+ schema-node:
+ id: 0x00030000
+ fields:
+ type: {number: 1, type: uint}
+ kind: {number: 2, type: uint}
+ max-occurrences: {number: 3, type: uint}
+ min: {number: 4, type: uint, optional: true}
+ max: {number: 5, type: uint, optional: true}
+ min-length: {number: 6, type: uint, optional: true}
+ max-length: {number: 7, type: uint, optional: true}
+ flags-mask: {number: 8, type: uint, optional: true}
+ unit: {number: 9, type: uint, optional: true}
+ scale: {number: 10, type: sint, optional: true}
+ blob-direction: {number: 11, enum: blob-direction, optional: true}
+ blob-backings: {number: 12, type: uint, optional: true}
+ blob-max-length: {number: 13, type: uint, optional: true}
+ blob-alignment: {number: 14, type: uint, optional: true}
+ reenters: {number: 15, type: uint, min: 1, optional: true}
+ fields: {number: 16, record: schema-node, max-occurrences: unlimited,
+ optional: true}
+ optional-fields: {number: 17, record: schema-node,
+ max-occurrences: unlimited, optional: true}
+ family-get request:
+ id: 0x80010000
+ family-get reply:
+ id: 0xc0010000
+ fields:
+ families: {number: 1, record: family-get families,
+ max-occurrences: unlimited}
+ family-get families:
+ id: 0xc0011000
+ fields:
+ id: {number: 1, type: uint}
+ name: {number: 2, type: string, max-length: 63}
+ op-get request:
+ id: 0x80020000
+ op-get reply:
+ id: 0xc0020000
+ fields:
+ ops: {number: 1, record: op-get ops, max-occurrences: unlimited}
+ op-get ops:
+ id: 0xc0021000
+ fields:
+ id: {number: 1, type: uint, min: 1}
+ type: {number: 2, enum: op-type}
+ availability: {number: 3, enum: op-availability, optional: true}
+ disabled: {number: 4, type: bool, optional: true}
+ request-max-len: {number: 5, type: uint, optional: true}
+ action-reply-max-len: {number: 6, type: uint, optional: true}
+ query-reply-min-len: {number: 7, type: uint, optional: true}
+ query-reply-suggested-len: {number: 8, type: uint, optional: true}
+ fd-access: {number: 9, enum: op-fd-access, optional: true}
+ error-info-max-len: {number: 10, type: uint, optional: true}
+ op-schema-get request:
+ id: 0x80030000
+ fields:
+ id: {number: 1, type: uint, min: 1}
+ op-schema-get reply:
+ id: 0xc0030000
+ fields:
+ request: {number: 1, record: schema-node, optional: true}
+ reply: {number: 2, record: schema-node, optional: true}
+ops:
+ family-get: {id: 1, type: query, fd-access: read, request: family-get request,
+ reply: family-get reply}
+ op-get: {id: 2, type: query, fd-access: read, request: op-get request,
+ reply: op-get reply}
+ op-schema-get: {id: 3, type: query, fd-access: read,
+ request: op-schema-get request, reply: op-schema-get reply}
diff --git a/Documentation/ctlv/abi/families.registry.yaml b/Documentation/ctlv/abi/families.registry.yaml
new file mode 100644
index 000000000000..5845e7457771
--- /dev/null
+++ b/Documentation/ctlv/abi/families.registry.yaml
@@ -0,0 +1,7 @@
+# SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause)
+%YAML 1.2
+---
+# Checked-in ABI baseline of the ordered CTLV family registry.
+# Written by "make -C tools/ctlv regen".
+families:
+ - ctlv
diff --git a/Documentation/ctlv/families.yaml b/Documentation/ctlv/families.yaml
new file mode 100644
index 000000000000..4d219d7af74a
--- /dev/null
+++ b/Documentation/ctlv/families.yaml
@@ -0,0 +1,17 @@
+# SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause)
+%YAML 1.2
+---
+# Do not add a family without agreeing its position with the CTLV
+# maintainers (CTLV SUBSYSTEM in MAINTAINERS) first. The position is the
+# family ID, so families added in parallel trees end up with the same ID.
+#
+# Append-only registry of CTLV family names, indexed by family ID: the entry at
+# index N has family ID N, up to 65535. The root family owns ID 0 and heads the
+# list, and every other entry comes after the entry of the family it inherits.
+# An entry may be marked deprecated, but never reordered, removed, renamed or
+# reused, its position being its family ID. Every entry must have a
+# specification in specs/<name>.yaml, and is added by the change that adds the
+# implementation. Fixtures under tools/ctlv/tests/ are not registered.
+
+families:
+ - ctlv
diff --git a/Documentation/ctlv/generated-files.yaml b/Documentation/ctlv/generated-files.yaml
new file mode 100644
index 000000000000..5488ddf40e17
--- /dev/null
+++ b/Documentation/ctlv/generated-files.yaml
@@ -0,0 +1,17 @@
+# SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause)
+%YAML 1.2
+---
+# Generated files for registered families.
+version: 1
+registry:
+ source: Documentation/ctlv/families.yaml
+ outputs:
+ uapi-header: include/uapi/ctlv/ctlv_families_gen.h
+
+families:
+ - name: ctlv
+ source: Documentation/ctlv/specs/ctlv.yaml
+ outputs:
+ uapi-header: include/uapi/ctlv/ctlv_gen.h
+ kernel-header: include/linux/ctlv_gen.h
+ kernel-source: lib/ctlv/ctlv_gen.c
diff --git a/Documentation/ctlv/specs/ctlv.yaml b/Documentation/ctlv/specs/ctlv.yaml
new file mode 100644
index 000000000000..a7108f59c267
--- /dev/null
+++ b/Documentation/ctlv/specs/ctlv.yaml
@@ -0,0 +1,211 @@
+# SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause)
+%YAML 1.2
+---
+# The root CTLV family, family ID 0 and the first entry in families.yaml. It
+# inherits nothing, and every other family name begins with "ctlv-". Every
+# CTLV device answers these ops in addition to its own family's. Every blob
+# field of every family carries the blob record, and every failure may write
+# error-info. Nothing here may be reordered or removed.
+family: ctlv
+
+enums:
+ # What an op is, which decides the messages it has.
+ op-type:
+ # Executed and acknowledged.
+ - action
+ # Executed and answered, in as many fragments as the answer needs.
+ - query
+ # A permission, not a state: what a device currently allows is disabled.
+ op-availability:
+ # A device that publishes the op always answers it.
+ - fixed
+ # A device may disable and enable the op at any time.
+ - dynamic
+ # What holds the bytes of a blob.
+ blob-type:
+ # Anonymous memory of the calling process, at address.
+ - va
+ # Which way the bytes of a blob go.
+ blob-direction:
+ # From the caller to the device, which only reads the blob.
+ - input
+ # From the device to the caller.
+ - output
+ # Both ways: the device reads the blob and writes it.
+ - inout
+ # The first rule a refused message broke.
+ validation-reason:
+ # The TLVs do not frame: the root is not one, a length runs past its
+ # record, or a TLV is a record where its schema has a scalar or the
+ # reverse.
+ - malformed-message
+ # A TLV of a type its record does not place there, not marked
+ # IGNORE_IF_UNKNOWN or of a type the schema places elsewhere.
+ - unknown-type
+ # A header flag the message may not carry, or a flags payload with a bit
+ # its field does not declare.
+ - unknown-flag
+ # Records nested deeper than the core follows.
+ - record-too-deep
+ # A required field is absent, and missing-type says which.
+ - missing-field
+ # A field occurs more often than its schema allows.
+ - too-many-occurrences
+ # An integer, an enum value or a bool outside what its field takes.
+ - value-out-of-range
+ # A payload of the wrong length for its kind, or outside its field's
+ # length bounds.
+ - bad-length
+ # A string payload that is not exactly one NUL-terminated string.
+ - bad-string
+ # A blob descriptor its field does not take, or one whose memory could
+ # not be acquired or is too short for what the op writes there.
+ - bad-blob-descriptor
+ # A query continuation of a generation the query no longer has.
+ - stale-generation
+ # The TLVs of one type are not contiguous in their record.
+ - noncontiguous-type
+ # What an op asks of the file description that executes it.
+ op-fd-access:
+ # Read access, which an observation fd has as well as a full-control one.
+ - read
+ # Read-write access, which only a full-control fd has.
+ - read-write
+
+records:
+ # va takes address; fd and offset are reserved for later fd-backed
+ # types.
+ blob:
+ # What holds the bytes, which says which of the fields below are present.
+ type: blob-type
+ # Where a va blob starts in the calling process.
+ address?: uint
+ # Reserved for a later fd-backed type.
+ fd?: uint(..s32-max)
+ # Reserved for a later fd-backed type.
+ offset?: uint
+ # The length of the blob in bytes, at most what its field takes.
+ length: uint(1..)
+
+ # Only what describes the first failure is present.
+ error-info:
+ # The rule the message broke.
+ reason?: validation-reason
+ # What went wrong, for a person to read.
+ message?: string(..255)
+ # The offset of the offending TLV from the start of the message, or of
+ # its record when what is wrong is something the record lacks.
+ bad-tlv-offset?: uint
+ # The offset of the record holding it, from the start of the message.
+ record-offset?: uint
+ # The type of the required field that is absent.
+ missing-type?: uint
+ # The generation the query continuation carried.
+ expected-generation?: uint
+ # The generation the query has now.
+ current-generation?: uint
+ # How long the output blob at bad-tlv-offset has to be for the op.
+ required-length?: uint
+
+ # One node of an op's request or reply tree. Only the members the node's
+ # schema kind has are present. Nothing here is a name: a caller matches the
+ # family chain from family-get to find the generated UAPI.
+ schema-node:
+ # Zero in a message root node, as in the root TLV.
+ type: uint
+ # enum ctlv_schema_kind.
+ kind: uint
+ # Zero where the node may repeat as often as the message has room.
+ max-occurrences: uint
+ # The inclusive lower bound of an integer or enum payload, a raw 64-bit
+ # value read according to kind. An enum's values are numbered without
+ # gaps.
+ min?: uint
+ # The inclusive upper bound, read the same way.
+ max?: uint
+ # The shortest payload of a string or binary field, a string's terminator
+ # excluded.
+ min-length?: uint
+ # The longest payload, likewise.
+ max-length?: uint
+ # The bits a flags payload may carry.
+ flags-mask?: uint
+ # enum ctlv_unit, absent when the payload is dimensionless.
+ unit?: uint
+ # The decimal exponent of the unit, a payload being value * 10^scale of
+ # it, present only with a unit.
+ scale?: sint
+ # Which way a blob field's bytes go.
+ blob-direction?: blob-direction
+ # Mask of BIT(enum ctlv_blob_type): the backings this node takes.
+ blob-backings?: uint
+ # Maximum external payload length, unrelated to the inline message limit.
+ blob-max-length?: uint
+ # The power of two a blob's address or offset is a multiple of.
+ blob-alignment?: uint
+ # Levels to climb to the node whose members are this one's as well, one
+ # meaning the node this one is a member of. A node that carries it lists
+ # no members itself.
+ reenters?: uint(1..)
+ # The required nodes one level in.
+ fields[]?: record schema-node
+ # The optional nodes one level in.
+ optional-fields[]?: record schema-node
+
+ops:
+ # The families this device answers, this one first and the device family
+ # last. The only names any reply of this family carries.
+ family-get:
+ type: query
+ fd-access: read
+ reply:
+ # One entry per family of the chain.
+ families[]:
+ # The family ID, which bits 63..48 of the family's types carry.
+ id: uint
+ # The registry name, also what a uevent carries. Bounded here, this
+ # being the only place a message carries a family name.
+ name: string(..63)
+
+ # One entry per effective op. Length fields that do not apply to an
+ # entry's op type are absent.
+ op-get:
+ type: query
+ fd-access: read
+ reply:
+ # One entry per op the device publishes.
+ ops[]:
+ # The op ID, which CTLV_IOC_OP_EXEC carries.
+ id: uint(1..)
+ # Action or query.
+ type: op-type
+ # Whether the device may disable and enable the op.
+ availability?: op-availability
+ # Whether the device has the op disabled right now.
+ disabled?: bool
+ # The longest request composed from the schema.
+ request-max-len?: uint
+ # The reply buffer an action needs, zero for one that answers nothing.
+ action-reply-max-len?: uint
+ # The shortest reply buffer a query takes.
+ query-reply-min-len?: uint
+ # The reply buffer length the family suggests for a query.
+ query-reply-suggested-len?: uint
+ # The access a file description needs to execute the op.
+ fd-access?: op-fd-access
+ # The longest error information the op can return.
+ error-info-max-len?: uint
+
+ # The request and reply ABI of one op, whether or not it can be called right
+ # now. A removed op answers as one that was never declared.
+ op-schema-get:
+ type: query
+ fd-access: read
+ request:
+ # The ID of the op to describe.
+ id: uint(1..)
+ reply:
+ # The root of the op's request tree, absent when it has no request.
+ request?: record schema-node
+ # The root of its reply tree, absent when it has no reply.
+ reply?: record schema-node
diff --git a/include/linux/ctlv_gen.h b/include/linux/ctlv_gen.h
new file mode 100644
index 000000000000..117e5714c3fe
--- /dev/null
+++ b/include/linux/ctlv_gen.h
@@ -0,0 +1,154 @@
+/* SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause) */
+/*
+ * Generated from Documentation/ctlv/specs/ctlv.yaml by
+ * tools/ctlv/ctlv-regen.sh. Do not edit.
+ */
+/* CTLV-GEN kernel header */
+
+/* Kernel metadata and family API of the ctlv CTLV family. */
+
+#ifndef _LINUX_CTLV_GEN_H
+#define _LINUX_CTLV_GEN_H
+
+#include <linux/ctlv_family.h>
+#include <linux/err.h>
+#include <linux/errno.h>
+
+#include <uapi/ctlv/ctlv_gen.h>
+
+/* Ops this family declares, in declaration order. */
+extern const struct ctlv_op_schema ctlv_op_schemas[];
+
+extern const struct ctlv_family_schema ctlv_schema;
+
+/* The error-info message, built by the core. */
+#define CTLV_ERROR_INFO_MAX_LEN 456
+
+extern const struct ctlv_schema_tree ctlv_error_info_schema;
+
+/* Implemented by the CTLV core, one per action and query it declares. */
+int ctlv_family_get_query(struct ctlv *ctlv, struct ctlv_op_ctx *ctx,
+ const struct ctlv_cursor *request, struct ctlv_cursor *reply);
+int ctlv_op_get_query(struct ctlv *ctlv, struct ctlv_op_ctx *ctx, const struct ctlv_cursor *request,
+ struct ctlv_cursor *reply);
+int ctlv_op_schema_get_query(struct ctlv *ctlv, struct ctlv_op_ctx *ctx,
+ const struct ctlv_cursor *request, struct ctlv_cursor *reply);
+
+extern const struct ctlv_family ctlv_family;
+
+#ifndef _CTLV_GENERATED_METADATA_ONLY
+
+/* Reading a validated message. A blob has a resolver instead of a getter. */
+/* The ID of the op to describe. */
+CTLV_IN_REF(ctlv_op_schema_get_id, CTLV_T_OP_SCHEMA_GET_ID, CTLV_SCHEMA_UINT)
+CTLV_IN_HAS(ctlv_op_schema_get_id)
+CTLV_IN_U64(ctlv_op_schema_get_id)
+
+/* Writing a reply. An append is atomic; a record ends with ctlv_record_end(). */
+/* One entry per family of the chain. */
+CTLV_RECORD_START(ctlv_family_get_families, CTLV_T_FAMILY_GET_FAMILIES, CTLV_SCHEMA_RECORD)
+/* The family ID, which bits 63..48 of the family's types carry. */
+CTLV_PUT_U64(ctlv_family_get_families_id, CTLV_T_FAMILY_GET_FAMILIES_ID, CTLV_SCHEMA_UINT)
+/*
+ * The registry name, also what a uevent carries. Bounded here, this being the only place a message
+ * carries a family name.
+ */
+CTLV_PUT_STRING(ctlv_family_get_families_name, CTLV_T_FAMILY_GET_FAMILIES_NAME, CTLV_SCHEMA_STRING)
+/* One entry per op the device publishes. */
+CTLV_RECORD_START(ctlv_op_get_ops, CTLV_T_OP_GET_OPS, CTLV_SCHEMA_RECORD)
+/* The op ID, which CTLV_IOC_OP_EXEC carries. */
+CTLV_PUT_U64(ctlv_op_get_ops_id, CTLV_T_OP_GET_OPS_ID, CTLV_SCHEMA_UINT)
+/* Action or query. */
+CTLV_PUT_U64(ctlv_op_get_ops_type, CTLV_T_OP_GET_OPS_TYPE, CTLV_SCHEMA_ENUM)
+/* Whether the device may disable and enable the op. */
+CTLV_PUT_U64(ctlv_op_get_ops_availability, CTLV_T_OP_GET_OPS_AVAILABILITY, CTLV_SCHEMA_ENUM)
+/* Whether the device has the op disabled right now. */
+CTLV_PUT_BOOL(ctlv_op_get_ops_disabled, CTLV_T_OP_GET_OPS_DISABLED, CTLV_SCHEMA_BOOL)
+/* The longest request composed from the schema. */
+CTLV_PUT_U64(ctlv_op_get_ops_request_max_len, CTLV_T_OP_GET_OPS_REQUEST_MAX_LEN, CTLV_SCHEMA_UINT)
+/* The reply buffer an action needs, zero for one that answers nothing. */
+CTLV_PUT_U64(ctlv_op_get_ops_action_reply_max_len, CTLV_T_OP_GET_OPS_ACTION_REPLY_MAX_LEN,
+ CTLV_SCHEMA_UINT)
+/* The shortest reply buffer a query takes. */
+CTLV_PUT_U64(ctlv_op_get_ops_query_reply_min_len, CTLV_T_OP_GET_OPS_QUERY_REPLY_MIN_LEN,
+ CTLV_SCHEMA_UINT)
+/* The reply buffer length the family suggests for a query. */
+CTLV_PUT_U64(ctlv_op_get_ops_query_reply_suggested_len, CTLV_T_OP_GET_OPS_QUERY_REPLY_SUGGESTED_LEN,
+ CTLV_SCHEMA_UINT)
+/* The access a file description needs to execute the op. */
+CTLV_PUT_U64(ctlv_op_get_ops_fd_access, CTLV_T_OP_GET_OPS_FD_ACCESS, CTLV_SCHEMA_ENUM)
+/* The longest error information the op can return. */
+CTLV_PUT_U64(ctlv_op_get_ops_error_info_max_len, CTLV_T_OP_GET_OPS_ERROR_INFO_MAX_LEN,
+ CTLV_SCHEMA_UINT)
+/* The root of the op's request tree, absent when it has no request. */
+CTLV_RECORD_START(ctlv_op_schema_get_request, CTLV_T_OP_SCHEMA_GET_REQUEST, CTLV_SCHEMA_RECORD)
+/* Zero in a message root node, as in the root TLV. */
+CTLV_PUT_U64(ctlv_schema_node_type, CTLV_T_SCHEMA_NODE_TYPE, CTLV_SCHEMA_UINT)
+/* enum ctlv_schema_kind. */
+CTLV_PUT_U64(ctlv_schema_node_kind, CTLV_T_SCHEMA_NODE_KIND, CTLV_SCHEMA_UINT)
+/* Zero where the node may repeat as often as the message has room. */
+CTLV_PUT_U64(ctlv_schema_node_max_occurrences, CTLV_T_SCHEMA_NODE_MAX_OCCURRENCES, CTLV_SCHEMA_UINT)
+/*
+ * The inclusive lower bound of an integer or enum payload, a raw 64-bit value read according to
+ * kind. An enum's values are numbered without gaps.
+ */
+CTLV_PUT_U64(ctlv_schema_node_min, CTLV_T_SCHEMA_NODE_MIN, CTLV_SCHEMA_UINT)
+/* The inclusive upper bound, read the same way. */
+CTLV_PUT_U64(ctlv_schema_node_max, CTLV_T_SCHEMA_NODE_MAX, CTLV_SCHEMA_UINT)
+/* The shortest payload of a string or binary field, a string's terminator excluded. */
+CTLV_PUT_U64(ctlv_schema_node_min_length, CTLV_T_SCHEMA_NODE_MIN_LENGTH, CTLV_SCHEMA_UINT)
+/* The longest payload, likewise. */
+CTLV_PUT_U64(ctlv_schema_node_max_length, CTLV_T_SCHEMA_NODE_MAX_LENGTH, CTLV_SCHEMA_UINT)
+/* The bits a flags payload may carry. */
+CTLV_PUT_U64(ctlv_schema_node_flags_mask, CTLV_T_SCHEMA_NODE_FLAGS_MASK, CTLV_SCHEMA_UINT)
+/* enum ctlv_unit, absent when the payload is dimensionless. */
+CTLV_PUT_U64(ctlv_schema_node_unit, CTLV_T_SCHEMA_NODE_UNIT, CTLV_SCHEMA_UINT)
+/*
+ * The decimal exponent of the unit, a payload being value * 10^scale of it, present only with a
+ * unit.
+ */
+CTLV_PUT_S64(ctlv_schema_node_scale, CTLV_T_SCHEMA_NODE_SCALE, CTLV_SCHEMA_SINT)
+/* Which way a blob field's bytes go. */
+CTLV_PUT_U64(ctlv_schema_node_blob_direction, CTLV_T_SCHEMA_NODE_BLOB_DIRECTION, CTLV_SCHEMA_ENUM)
+/* Mask of BIT(enum ctlv_blob_type): the backings this node takes. */
+CTLV_PUT_U64(ctlv_schema_node_blob_backings, CTLV_T_SCHEMA_NODE_BLOB_BACKINGS, CTLV_SCHEMA_UINT)
+/* Maximum external payload length, unrelated to the inline message limit. */
+CTLV_PUT_U64(ctlv_schema_node_blob_max_length, CTLV_T_SCHEMA_NODE_BLOB_MAX_LENGTH, CTLV_SCHEMA_UINT)
+/* The power of two a blob's address or offset is a multiple of. */
+CTLV_PUT_U64(ctlv_schema_node_blob_alignment, CTLV_T_SCHEMA_NODE_BLOB_ALIGNMENT, CTLV_SCHEMA_UINT)
+/*
+ * Levels to climb to the node whose members are this one's as well, one meaning the node this one
+ * is a member of. A node that carries it lists no members itself.
+ */
+CTLV_PUT_U64(ctlv_schema_node_reenters, CTLV_T_SCHEMA_NODE_REENTERS, CTLV_SCHEMA_UINT)
+/* The required nodes one level in. */
+CTLV_RECORD_START(ctlv_schema_node_fields, CTLV_T_SCHEMA_NODE_FIELDS, CTLV_SCHEMA_RECORD)
+/* The optional nodes one level in. */
+CTLV_RECORD_START(ctlv_schema_node_optional_fields, CTLV_T_SCHEMA_NODE_OPTIONAL_FIELDS,
+ CTLV_SCHEMA_RECORD)
+/* The root of its reply tree, absent when it has no reply. */
+CTLV_RECORD_START(ctlv_op_schema_get_reply, CTLV_T_OP_SCHEMA_GET_REPLY, CTLV_SCHEMA_RECORD)
+/* The rule the message broke. */
+CTLV_PUT_U64(ctlv_error_info_reason, CTLV_T_ERROR_INFO_REASON, CTLV_SCHEMA_ENUM)
+/* What went wrong, for a person to read. */
+CTLV_PUT_STRING(ctlv_error_info_message, CTLV_T_ERROR_INFO_MESSAGE, CTLV_SCHEMA_STRING)
+/*
+ * The offset of the offending TLV from the start of the message, or of its record when what is
+ * wrong is something the record lacks.
+ */
+CTLV_PUT_U64(ctlv_error_info_bad_tlv_offset, CTLV_T_ERROR_INFO_BAD_TLV_OFFSET, CTLV_SCHEMA_UINT)
+/* The offset of the record holding it, from the start of the message. */
+CTLV_PUT_U64(ctlv_error_info_record_offset, CTLV_T_ERROR_INFO_RECORD_OFFSET, CTLV_SCHEMA_UINT)
+/* The type of the required field that is absent. */
+CTLV_PUT_U64(ctlv_error_info_missing_type, CTLV_T_ERROR_INFO_MISSING_TYPE, CTLV_SCHEMA_UINT)
+/* The generation the query continuation carried. */
+CTLV_PUT_U64(ctlv_error_info_expected_generation, CTLV_T_ERROR_INFO_EXPECTED_GENERATION,
+ CTLV_SCHEMA_UINT)
+/* The generation the query has now. */
+CTLV_PUT_U64(ctlv_error_info_current_generation, CTLV_T_ERROR_INFO_CURRENT_GENERATION,
+ CTLV_SCHEMA_UINT)
+/* How long the output blob at bad-tlv-offset has to be for the op. */
+CTLV_PUT_U64(ctlv_error_info_required_length, CTLV_T_ERROR_INFO_REQUIRED_LENGTH, CTLV_SCHEMA_UINT)
+#endif /* _CTLV_GENERATED_METADATA_ONLY */
+
+#endif /* _LINUX_CTLV_GEN_H */
diff --git a/include/uapi/ctlv/ctlv_families_gen.h b/include/uapi/ctlv/ctlv_families_gen.h
new file mode 100644
index 000000000000..83f1719c89aa
--- /dev/null
+++ b/include/uapi/ctlv/ctlv_families_gen.h
@@ -0,0 +1,17 @@
+/* SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause) */
+/*
+ * Generated from Documentation/ctlv/families.yaml by
+ * tools/ctlv/ctlv-regen.sh. Do not edit.
+ */
+/* CTLV-GEN uapi header */
+
+/* CTLV family numbers. */
+
+#ifndef _UAPI_CTLV_CTLV_FAMILIES_GEN_H
+#define _UAPI_CTLV_CTLV_FAMILIES_GEN_H
+
+enum _ctlv_family {
+ CTLV_FAMILY_ROOT = 0,
+};
+
+#endif /* _UAPI_CTLV_CTLV_FAMILIES_GEN_H */
diff --git a/include/uapi/ctlv/ctlv_gen.h b/include/uapi/ctlv/ctlv_gen.h
new file mode 100644
index 000000000000..1eff51c0dd58
--- /dev/null
+++ b/include/uapi/ctlv/ctlv_gen.h
@@ -0,0 +1,278 @@
+/* SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause) */
+/*
+ * Generated from Documentation/ctlv/specs/ctlv.yaml by
+ * tools/ctlv/ctlv-regen.sh. Do not edit.
+ */
+/* CTLV-GEN uapi header */
+
+/*
+ * Constants of the ctlv CTLV family. Types and op IDs are complete; the message format carrying
+ * them is in <ctlv/ctlv.h>.
+ */
+
+#ifndef _UAPI_CTLV_CTLV_GEN_H
+#define _UAPI_CTLV_CTLV_GEN_H
+
+#include <ctlv/ctlv.h>
+#include <ctlv/ctlv_families_gen.h>
+
+/* enum op-type */
+enum _ctlv_op_type {
+ /* Executed and acknowledged. */
+ CTLV_OP_TYPE_ACTION = 1,
+ /* Executed and answered, in as many fragments as the answer needs. */
+ CTLV_OP_TYPE_QUERY = 2,
+};
+
+/* enum op-availability */
+enum _ctlv_op_availability {
+ /* A device that publishes the op always answers it. */
+ CTLV_OP_AVAILABILITY_FIXED = 1,
+ /* A device may disable and enable the op at any time. */
+ CTLV_OP_AVAILABILITY_DYNAMIC = 2,
+};
+
+/* enum blob-type */
+enum _ctlv_blob_type {
+ /* Anonymous memory of the calling process, at address. */
+ CTLV_BLOB_TYPE_VA = 1,
+};
+
+/* enum blob-direction */
+enum _ctlv_blob_direction {
+ /* From the caller to the device, which only reads the blob. */
+ CTLV_BLOB_DIRECTION_INPUT = 1,
+ /* From the device to the caller. */
+ CTLV_BLOB_DIRECTION_OUTPUT = 2,
+ /* Both ways: the device reads the blob and writes it. */
+ CTLV_BLOB_DIRECTION_INOUT = 3,
+};
+
+/* enum validation-reason */
+enum _ctlv_validation_reason {
+ /*
+ * The TLVs do not frame: the root is not one, a length runs past its record, or a TLV is a
+ * record where its schema has a scalar or the reverse.
+ */
+ CTLV_VALIDATION_REASON_MALFORMED_MESSAGE = 1,
+ /*
+ * A TLV of a type its record does not place there, not marked IGNORE_IF_UNKNOWN or of a
+ * type the schema places elsewhere.
+ */
+ CTLV_VALIDATION_REASON_UNKNOWN_TYPE = 2,
+ /*
+ * A header flag the message may not carry, or a flags payload with a bit its field does not
+ * declare.
+ */
+ CTLV_VALIDATION_REASON_UNKNOWN_FLAG = 3,
+ /* Records nested deeper than the core follows. */
+ CTLV_VALIDATION_REASON_RECORD_TOO_DEEP = 4,
+ /* A required field is absent, and missing-type says which. */
+ CTLV_VALIDATION_REASON_MISSING_FIELD = 5,
+ /* A field occurs more often than its schema allows. */
+ CTLV_VALIDATION_REASON_TOO_MANY_OCCURRENCES = 6,
+ /* An integer, an enum value or a bool outside what its field takes. */
+ CTLV_VALIDATION_REASON_VALUE_OUT_OF_RANGE = 7,
+ /* A payload of the wrong length for its kind, or outside its field's length bounds. */
+ CTLV_VALIDATION_REASON_BAD_LENGTH = 8,
+ /* A string payload that is not exactly one NUL-terminated string. */
+ CTLV_VALIDATION_REASON_BAD_STRING = 9,
+ /*
+ * A blob descriptor its field does not take, or one whose memory could not be acquired or
+ * is too short for what the op writes there.
+ */
+ CTLV_VALIDATION_REASON_BAD_BLOB_DESCRIPTOR = 10,
+ /* A query continuation of a generation the query no longer has. */
+ CTLV_VALIDATION_REASON_STALE_GENERATION = 11,
+ /* The TLVs of one type are not contiguous in their record. */
+ CTLV_VALIDATION_REASON_NONCONTIGUOUS_TYPE = 12,
+};
+
+/* enum op-fd-access */
+enum _ctlv_op_fd_access {
+ /* Read access, which an observation fd has as well as a full-control one. */
+ CTLV_OP_FD_ACCESS_READ = 1,
+ /* Read-write access, which only a full-control fd has. */
+ CTLV_OP_FD_ACCESS_READ_WRITE = 2,
+};
+
+/* blob */
+/* What holds the bytes, which says which of the fields below are present. */
+#define CTLV_T_BLOB_TYPE \
+ CTLV_TYPE(CTLV_FAMILY_ROOT, 0x00010000, 1)
+/* Where a va blob starts in the calling process. */
+#define CTLV_T_BLOB_ADDRESS \
+ CTLV_TYPE(CTLV_FAMILY_ROOT, 0x00010000, 2)
+/* Reserved for a later fd-backed type. */
+#define CTLV_T_BLOB_FD \
+ CTLV_TYPE(CTLV_FAMILY_ROOT, 0x00010000, 3)
+/* Reserved for a later fd-backed type. */
+#define CTLV_T_BLOB_OFFSET \
+ CTLV_TYPE(CTLV_FAMILY_ROOT, 0x00010000, 4)
+/* The length of the blob in bytes, at most what its field takes. */
+#define CTLV_T_BLOB_LENGTH \
+ CTLV_TYPE(CTLV_FAMILY_ROOT, 0x00010000, 5)
+
+/* error-info */
+/* The rule the message broke. */
+#define CTLV_T_ERROR_INFO_REASON \
+ CTLV_TYPE(CTLV_FAMILY_ROOT, 0x00020000, 1)
+/* What went wrong, for a person to read. */
+#define CTLV_T_ERROR_INFO_MESSAGE \
+ CTLV_TYPE(CTLV_FAMILY_ROOT, 0x00020000, 2)
+/*
+ * The offset of the offending TLV from the start of the message, or of its record when what is
+ * wrong is something the record lacks.
+ */
+#define CTLV_T_ERROR_INFO_BAD_TLV_OFFSET \
+ CTLV_TYPE(CTLV_FAMILY_ROOT, 0x00020000, 3)
+/* The offset of the record holding it, from the start of the message. */
+#define CTLV_T_ERROR_INFO_RECORD_OFFSET \
+ CTLV_TYPE(CTLV_FAMILY_ROOT, 0x00020000, 4)
+/* The type of the required field that is absent. */
+#define CTLV_T_ERROR_INFO_MISSING_TYPE \
+ CTLV_TYPE(CTLV_FAMILY_ROOT, 0x00020000, 5)
+/* The generation the query continuation carried. */
+#define CTLV_T_ERROR_INFO_EXPECTED_GENERATION \
+ CTLV_TYPE(CTLV_FAMILY_ROOT, 0x00020000, 6)
+/* The generation the query has now. */
+#define CTLV_T_ERROR_INFO_CURRENT_GENERATION \
+ CTLV_TYPE(CTLV_FAMILY_ROOT, 0x00020000, 7)
+/* How long the output blob at bad-tlv-offset has to be for the op. */
+#define CTLV_T_ERROR_INFO_REQUIRED_LENGTH \
+ CTLV_TYPE(CTLV_FAMILY_ROOT, 0x00020000, 8)
+
+/* schema-node */
+/* Zero in a message root node, as in the root TLV. */
+#define CTLV_T_SCHEMA_NODE_TYPE \
+ CTLV_TYPE(CTLV_FAMILY_ROOT, 0x00030000, 1)
+/* enum ctlv_schema_kind. */
+#define CTLV_T_SCHEMA_NODE_KIND \
+ CTLV_TYPE(CTLV_FAMILY_ROOT, 0x00030000, 2)
+/* Zero where the node may repeat as often as the message has room. */
+#define CTLV_T_SCHEMA_NODE_MAX_OCCURRENCES \
+ CTLV_TYPE(CTLV_FAMILY_ROOT, 0x00030000, 3)
+/*
+ * The inclusive lower bound of an integer or enum payload, a raw 64-bit value read according to
+ * kind. An enum's values are numbered without gaps.
+ */
+#define CTLV_T_SCHEMA_NODE_MIN \
+ CTLV_TYPE(CTLV_FAMILY_ROOT, 0x00030000, 4)
+/* The inclusive upper bound, read the same way. */
+#define CTLV_T_SCHEMA_NODE_MAX \
+ CTLV_TYPE(CTLV_FAMILY_ROOT, 0x00030000, 5)
+/* The shortest payload of a string or binary field, a string's terminator excluded. */
+#define CTLV_T_SCHEMA_NODE_MIN_LENGTH \
+ CTLV_TYPE(CTLV_FAMILY_ROOT, 0x00030000, 6)
+/* The longest payload, likewise. */
+#define CTLV_T_SCHEMA_NODE_MAX_LENGTH \
+ CTLV_TYPE(CTLV_FAMILY_ROOT, 0x00030000, 7)
+/* The bits a flags payload may carry. */
+#define CTLV_T_SCHEMA_NODE_FLAGS_MASK \
+ CTLV_TYPE(CTLV_FAMILY_ROOT, 0x00030000, 8)
+/* enum ctlv_unit, absent when the payload is dimensionless. */
+#define CTLV_T_SCHEMA_NODE_UNIT \
+ CTLV_TYPE(CTLV_FAMILY_ROOT, 0x00030000, 9)
+/*
+ * The decimal exponent of the unit, a payload being value * 10^scale of it, present only with a
+ * unit.
+ */
+#define CTLV_T_SCHEMA_NODE_SCALE \
+ CTLV_TYPE(CTLV_FAMILY_ROOT, 0x00030000, 10)
+/* Which way a blob field's bytes go. */
+#define CTLV_T_SCHEMA_NODE_BLOB_DIRECTION \
+ CTLV_TYPE(CTLV_FAMILY_ROOT, 0x00030000, 11)
+/* Mask of BIT(enum ctlv_blob_type): the backings this node takes. */
+#define CTLV_T_SCHEMA_NODE_BLOB_BACKINGS \
+ CTLV_TYPE(CTLV_FAMILY_ROOT, 0x00030000, 12)
+/* Maximum external payload length, unrelated to the inline message limit. */
+#define CTLV_T_SCHEMA_NODE_BLOB_MAX_LENGTH \
+ CTLV_TYPE(CTLV_FAMILY_ROOT, 0x00030000, 13)
+/* The power of two a blob's address or offset is a multiple of. */
+#define CTLV_T_SCHEMA_NODE_BLOB_ALIGNMENT \
+ CTLV_TYPE(CTLV_FAMILY_ROOT, 0x00030000, 14)
+/*
+ * Levels to climb to the node whose members are this one's as well, one meaning the node this one
+ * is a member of. A node that carries it lists no members itself.
+ */
+#define CTLV_T_SCHEMA_NODE_REENTERS \
+ CTLV_TYPE(CTLV_FAMILY_ROOT, 0x00030000, 15)
+/* The required nodes one level in. */
+#define CTLV_T_SCHEMA_NODE_FIELDS \
+ CTLV_TYPE(CTLV_FAMILY_ROOT, 0x00030000, 16)
+/* The optional nodes one level in. */
+#define CTLV_T_SCHEMA_NODE_OPTIONAL_FIELDS \
+ CTLV_TYPE(CTLV_FAMILY_ROOT, 0x00030000, 17)
+
+/* family-get reply */
+/* One entry per family of the chain. */
+#define CTLV_T_FAMILY_GET_FAMILIES \
+ CTLV_TYPE(CTLV_FAMILY_ROOT, 0xc0010000, 1)
+
+/* family-get families */
+/* The family ID, which bits 63..48 of the family's types carry. */
+#define CTLV_T_FAMILY_GET_FAMILIES_ID \
+ CTLV_TYPE(CTLV_FAMILY_ROOT, 0xc0011000, 1)
+/*
+ * The registry name, also what a uevent carries. Bounded here, this being the only place a message
+ * carries a family name.
+ */
+#define CTLV_T_FAMILY_GET_FAMILIES_NAME \
+ CTLV_TYPE(CTLV_FAMILY_ROOT, 0xc0011000, 2)
+
+/* op-get reply */
+/* One entry per op the device publishes. */
+#define CTLV_T_OP_GET_OPS \
+ CTLV_TYPE(CTLV_FAMILY_ROOT, 0xc0020000, 1)
+
+/* op-get ops */
+/* The op ID, which CTLV_IOC_OP_EXEC carries. */
+#define CTLV_T_OP_GET_OPS_ID \
+ CTLV_TYPE(CTLV_FAMILY_ROOT, 0xc0021000, 1)
+/* Action or query. */
+#define CTLV_T_OP_GET_OPS_TYPE \
+ CTLV_TYPE(CTLV_FAMILY_ROOT, 0xc0021000, 2)
+/* Whether the device may disable and enable the op. */
+#define CTLV_T_OP_GET_OPS_AVAILABILITY \
+ CTLV_TYPE(CTLV_FAMILY_ROOT, 0xc0021000, 3)
+/* Whether the device has the op disabled right now. */
+#define CTLV_T_OP_GET_OPS_DISABLED \
+ CTLV_TYPE(CTLV_FAMILY_ROOT, 0xc0021000, 4)
+/* The longest request composed from the schema. */
+#define CTLV_T_OP_GET_OPS_REQUEST_MAX_LEN \
+ CTLV_TYPE(CTLV_FAMILY_ROOT, 0xc0021000, 5)
+/* The reply buffer an action needs, zero for one that answers nothing. */
+#define CTLV_T_OP_GET_OPS_ACTION_REPLY_MAX_LEN \
+ CTLV_TYPE(CTLV_FAMILY_ROOT, 0xc0021000, 6)
+/* The shortest reply buffer a query takes. */
+#define CTLV_T_OP_GET_OPS_QUERY_REPLY_MIN_LEN \
+ CTLV_TYPE(CTLV_FAMILY_ROOT, 0xc0021000, 7)
+/* The reply buffer length the family suggests for a query. */
+#define CTLV_T_OP_GET_OPS_QUERY_REPLY_SUGGESTED_LEN \
+ CTLV_TYPE(CTLV_FAMILY_ROOT, 0xc0021000, 8)
+/* The access a file description needs to execute the op. */
+#define CTLV_T_OP_GET_OPS_FD_ACCESS \
+ CTLV_TYPE(CTLV_FAMILY_ROOT, 0xc0021000, 9)
+/* The longest error information the op can return. */
+#define CTLV_T_OP_GET_OPS_ERROR_INFO_MAX_LEN \
+ CTLV_TYPE(CTLV_FAMILY_ROOT, 0xc0021000, 10)
+
+/* op-schema-get request */
+/* The ID of the op to describe. */
+#define CTLV_T_OP_SCHEMA_GET_ID \
+ CTLV_TYPE(CTLV_FAMILY_ROOT, 0x80030000, 1)
+
+/* op-schema-get reply */
+/* The root of the op's request tree, absent when it has no request. */
+#define CTLV_T_OP_SCHEMA_GET_REQUEST \
+ CTLV_TYPE(CTLV_FAMILY_ROOT, 0xc0030000, 1)
+/* The root of its reply tree, absent when it has no reply. */
+#define CTLV_T_OP_SCHEMA_GET_REPLY \
+ CTLV_TYPE(CTLV_FAMILY_ROOT, 0xc0030000, 2)
+
+/* ops */
+#define CTLV_OP_FAMILY_GET CTLV_OP(CTLV_FAMILY_ROOT, 1)
+#define CTLV_OP_OP_GET CTLV_OP(CTLV_FAMILY_ROOT, 2)
+#define CTLV_OP_OP_SCHEMA_GET CTLV_OP(CTLV_FAMILY_ROOT, 3)
+
+#endif /* _UAPI_CTLV_CTLV_GEN_H */
diff --git a/lib/ctlv/ctlv_gen.c b/lib/ctlv/ctlv_gen.c
new file mode 100644
index 000000000000..78b322507711
--- /dev/null
+++ b/lib/ctlv/ctlv_gen.c
@@ -0,0 +1,161 @@
+// SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause)
+/*
+ * Generated from Documentation/ctlv/specs/ctlv.yaml by
+ * tools/ctlv/ctlv-regen.sh. Do not edit.
+ */
+/* CTLV-GEN kernel source */
+
+#define _CTLV_GENERATED_METADATA_ONLY
+#include <linux/array_size.h>
+#include <linux/build_bug.h>
+#include <linux/ctlv_family.h>
+#include <linux/export.h>
+#include <linux/limits.h>
+
+#include <uapi/ctlv/ctlv_gen.h>
+
+#include <linux/ctlv_gen.h>
+
+static const struct ctlv_node_schema family_get_request_node_schemas[] = {
+ CTLV_NODE_SCHEMA_ROOT(0, 0, 0),
+};
+
+static const struct ctlv_node_schema family_get_reply_node_schemas[] = {
+ CTLV_NODE_SCHEMA_ROOT(1, 1, 1),
+ CTLV_NODE_SCHEMA_RECORD(CTLV_T_FAMILY_GET_FAMILIES, CTLV_NODE_SCHEMA_UNLIMITED, 2, 2, 2),
+ CTLV_NODE_SCHEMA_UINT(CTLV_T_FAMILY_GET_FAMILIES_ID, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_STRING(CTLV_T_FAMILY_GET_FAMILIES_NAME, 1, 0, 63),
+};
+
+static const struct ctlv_node_schema op_get_request_node_schemas[] = {
+ CTLV_NODE_SCHEMA_ROOT(0, 0, 0),
+};
+
+static const struct ctlv_node_schema op_get_reply_node_schemas[] = {
+ CTLV_NODE_SCHEMA_ROOT(1, 1, 1),
+ CTLV_NODE_SCHEMA_RECORD(CTLV_T_OP_GET_OPS, CTLV_NODE_SCHEMA_UNLIMITED, 2, 10, 2),
+ CTLV_NODE_SCHEMA_UINT(CTLV_T_OP_GET_OPS_ID, 1, 1, U64_MAX),
+ CTLV_NODE_SCHEMA_ENUM(CTLV_T_OP_GET_OPS_TYPE, 1, 2),
+ CTLV_NODE_SCHEMA_ENUM(CTLV_T_OP_GET_OPS_AVAILABILITY, 1, 2),
+ CTLV_NODE_SCHEMA_BOOL(CTLV_T_OP_GET_OPS_DISABLED, 1),
+ CTLV_NODE_SCHEMA_UINT(CTLV_T_OP_GET_OPS_REQUEST_MAX_LEN, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_T_OP_GET_OPS_ACTION_REPLY_MAX_LEN, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_T_OP_GET_OPS_QUERY_REPLY_MIN_LEN, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_T_OP_GET_OPS_QUERY_REPLY_SUGGESTED_LEN, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_ENUM(CTLV_T_OP_GET_OPS_FD_ACCESS, 1, 2),
+ CTLV_NODE_SCHEMA_UINT(CTLV_T_OP_GET_OPS_ERROR_INFO_MAX_LEN, 1, 0, U64_MAX),
+};
+
+static const struct ctlv_node_schema op_schema_get_request_node_schemas[] = {
+ CTLV_NODE_SCHEMA_ROOT(1, 1, 1),
+ CTLV_NODE_SCHEMA_UINT(CTLV_T_OP_SCHEMA_GET_ID, 1, 1, U64_MAX),
+};
+
+static const struct ctlv_node_schema op_schema_get_reply_node_schemas[] = {
+ CTLV_NODE_SCHEMA_ROOT(1, 2, 0),
+ CTLV_NODE_SCHEMA_RECORD(CTLV_T_OP_SCHEMA_GET_REQUEST, 1, 3, 17, 3),
+ CTLV_NODE_SCHEMA_RECORD(CTLV_T_OP_SCHEMA_GET_REPLY, 1, 20, 17, 3),
+ CTLV_NODE_SCHEMA_UINT(CTLV_T_SCHEMA_NODE_TYPE, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_T_SCHEMA_NODE_KIND, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_T_SCHEMA_NODE_MAX_OCCURRENCES, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_T_SCHEMA_NODE_MIN, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_T_SCHEMA_NODE_MAX, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_T_SCHEMA_NODE_MIN_LENGTH, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_T_SCHEMA_NODE_MAX_LENGTH, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_T_SCHEMA_NODE_FLAGS_MASK, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_T_SCHEMA_NODE_UNIT, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_SINT(CTLV_T_SCHEMA_NODE_SCALE, 1, S64_MIN, S64_MAX),
+ CTLV_NODE_SCHEMA_ENUM(CTLV_T_SCHEMA_NODE_BLOB_DIRECTION, 1, 3),
+ CTLV_NODE_SCHEMA_UINT(CTLV_T_SCHEMA_NODE_BLOB_BACKINGS, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_T_SCHEMA_NODE_BLOB_MAX_LENGTH, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_T_SCHEMA_NODE_BLOB_ALIGNMENT, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_T_SCHEMA_NODE_REENTERS, 1, 1, U64_MAX),
+ CTLV_NODE_SCHEMA_RECORD_REENTERS(CTLV_T_SCHEMA_NODE_FIELDS, CTLV_NODE_SCHEMA_UNLIMITED, 3,
+ 17, 3),
+ CTLV_NODE_SCHEMA_RECORD_REENTERS(CTLV_T_SCHEMA_NODE_OPTIONAL_FIELDS,
+ CTLV_NODE_SCHEMA_UNLIMITED, 3, 17, 3),
+ CTLV_NODE_SCHEMA_UINT(CTLV_T_SCHEMA_NODE_TYPE, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_T_SCHEMA_NODE_KIND, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_T_SCHEMA_NODE_MAX_OCCURRENCES, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_T_SCHEMA_NODE_MIN, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_T_SCHEMA_NODE_MAX, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_T_SCHEMA_NODE_MIN_LENGTH, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_T_SCHEMA_NODE_MAX_LENGTH, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_T_SCHEMA_NODE_FLAGS_MASK, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_T_SCHEMA_NODE_UNIT, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_SINT(CTLV_T_SCHEMA_NODE_SCALE, 1, S64_MIN, S64_MAX),
+ CTLV_NODE_SCHEMA_ENUM(CTLV_T_SCHEMA_NODE_BLOB_DIRECTION, 1, 3),
+ CTLV_NODE_SCHEMA_UINT(CTLV_T_SCHEMA_NODE_BLOB_BACKINGS, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_T_SCHEMA_NODE_BLOB_MAX_LENGTH, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_T_SCHEMA_NODE_BLOB_ALIGNMENT, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_T_SCHEMA_NODE_REENTERS, 1, 1, U64_MAX),
+ CTLV_NODE_SCHEMA_RECORD_REENTERS(CTLV_T_SCHEMA_NODE_FIELDS, CTLV_NODE_SCHEMA_UNLIMITED, 20,
+ 17, 3),
+ CTLV_NODE_SCHEMA_RECORD_REENTERS(CTLV_T_SCHEMA_NODE_OPTIONAL_FIELDS,
+ CTLV_NODE_SCHEMA_UNLIMITED, 20, 17, 3),
+};
+
+static const struct ctlv_node_schema error_info_node_schemas[] = {
+ CTLV_NODE_SCHEMA_ROOT(1, 8, 0),
+ CTLV_NODE_SCHEMA_ENUM(CTLV_T_ERROR_INFO_REASON, 1, 12),
+ CTLV_NODE_SCHEMA_STRING(CTLV_T_ERROR_INFO_MESSAGE, 1, 0, 255),
+ CTLV_NODE_SCHEMA_UINT(CTLV_T_ERROR_INFO_BAD_TLV_OFFSET, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_T_ERROR_INFO_RECORD_OFFSET, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_T_ERROR_INFO_MISSING_TYPE, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_T_ERROR_INFO_EXPECTED_GENERATION, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_T_ERROR_INFO_CURRENT_GENERATION, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_T_ERROR_INFO_REQUIRED_LENGTH, 1, 0, U64_MAX),
+};
+
+const struct ctlv_schema_tree ctlv_error_info_schema = {
+ .node_schemas = error_info_node_schemas,
+ .node_schemas_count =
+ ARRAY_SIZE(error_info_node_schemas),
+};
+
+static_assert(CTLV_ERROR_INFO_MAX_LEN <= CTLV_MAX_ERROR_LEN);
+
+/* The ops this family declares, in declaration order. */
+const struct ctlv_op_schema ctlv_op_schemas[] = {
+ CTLV_OP_SCHEMA_QUERY(CTLV_OP_FAMILY_GET, CTLV_OP_FD_ACCESS_READ, CTLV_OP_AVAILABILITY_FIXED,
+ 16, 112, 65536, CTLV_SCHEMA_TREE(family_get_request),
+ CTLV_SCHEMA_TREE(family_get_reply)),
+ CTLV_OP_SCHEMA_QUERY(CTLV_OP_OP_GET, CTLV_OP_FD_ACCESS_READ, CTLV_OP_AVAILABILITY_FIXED, 16,
+ 56, 65536, CTLV_SCHEMA_TREE(op_get_request),
+ CTLV_SCHEMA_TREE(op_get_reply)),
+ CTLV_OP_SCHEMA_QUERY(CTLV_OP_OP_SCHEMA_GET, CTLV_OP_FD_ACCESS_READ,
+ CTLV_OP_AVAILABILITY_FIXED, 40, 168, 65536,
+ CTLV_SCHEMA_TREE(op_schema_get_request),
+ CTLV_SCHEMA_TREE(op_schema_get_reply)),
+};
+
+/* The family ID is its position in the central registry. */
+const struct ctlv_family_schema ctlv_schema = {
+ .name = "ctlv",
+ .family_id = 0,
+ .ops = ctlv_op_schemas,
+ .ops_count = ARRAY_SIZE(ctlv_op_schemas),
+};
+
+/* Maximum uevent cost for this family chain. */
+static_assert(60 <= CTLV_UEVENT_MAX_LEN);
+static_assert(3 <= CTLV_UEVENT_MAX_VARS);
+
+/* Guards for generated maxima. */
+static_assert(65536 <= CTLV_MAX_INLINE_MESSAGE_LEN);
+static_assert(2 <= CTLV_MAX_DECLARED_RECORD_DEPTH);
+
+/* Local action and query bindings in declaration order. */
+static const struct ctlv_op ctlv_ops[] = {
+ CTLV_OP_QUERY(ctlv_family_get),
+ CTLV_OP_QUERY(ctlv_op_get),
+ CTLV_OP_QUERY(ctlv_op_schema_get),
+};
+
+/* Root-family descriptor used by the CTLV core. */
+const struct ctlv_family ctlv_family = {
+ .schema = &ctlv_schema,
+ .ops = ctlv_ops,
+ .ops_count = ARRAY_SIZE(ctlv_ops),
+};
+EXPORT_SYMBOL_NS_GPL(ctlv_family, "CTLV");
--
2.56.0
^ permalink raw reply related [flat|nested] 24+ messages in thread
* [PATCH RFC v2 04/15] Documentation: ctlv: Add userspace ABI
2026-11-13 15:54 [PATCH RFC v2 00/15] ctlv: Add a schema-driven TLV uAPI for character devices Jiri Pirko
` (2 preceding siblings ...)
2026-11-13 15:55 ` [PATCH RFC v2 03/15] ctlv: Add the root family specification and generated files Jiri Pirko
@ 2026-11-13 15:55 ` Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 05/15] ctlv: Prepare message input handling Jiri Pirko
` (10 subsequent siblings)
14 siblings, 0 replies; 24+ messages in thread
From: Jiri Pirko @ 2026-11-13 15:55 UTC (permalink / raw)
To: linux-coco
Cc: linux-kernel, linux-api, x86, kvm, linux-doc, jgg, david.quigley,
lukas, ksinyuk, rodrigo.vivi, cedric.xing, sameo, drdeeglaze,
James.Bottomley, aik, jpiotrowski, qinkun, pgonda, jic23,
alistair.francis, ankita, suzuki.poulose, kas, rick.p.edgecombe,
elena.reshetova, dave.hansen, sathyanarayanan.kuppuswamy,
peter.fang, jarkko, zohar, yeoreum.yun, aneesh.kumar, dave.jiang,
nik.borisov, sergii.dmytruk
From: Jiri Pirko <jiri@nvidia.com>
Describe what CTLV is for, its terms, transport, discovery, message
format and userspace compatibility rules.
Existing interfaces each miss some of the needs listed under Goals. An
ioctl with a structure of its own fixes its layout, so variable data
needs pointers to more structures, each subsystem validates its own,
and strace needs a decoder for every ioctl. sysfs and configfs
assemble one request through several files, with no point where the
whole request is checked and executed at once. Netlink is built around
sockets and network namespaces, a poor fit for devices that have
nothing to do with either and want the DAC model of a device node.
Assisted-by: Cursor: GPT-5.6 Sol
Assisted-by: Cursor: Claude Opus 5.5
Signed-off-by: Jiri Pirko <jiri@nvidia.com>
---
RFC->RFCv2:
- documented the size and flags of struct ctlv_op_exec
- documented EOVERFLOW for a blob range that wraps
- documented that a reply buffer may be any length
- replaced the limits section with the lengths a device reports
- dropped the events
- reworked the document after review, opening with a worked example
- said how a program finds a device's family
- said what a blob descriptor is made of and how a bad one fails
- said TLV, type, record and field instead of attr
- said record instead of nest, the root being one
- said how a type is made of a family, a record and a field
- imported path_id in the udev rule that matches ID_PATH
- opened with the goals
- added a glossary, and dropped "physical"
- collected the compatibility rules in one section
- kept open()'s errors apart from the ioctls'
- documented required-length, the length an output blob needs
- took the UAPI headers from include/uapi/ctlv/
- documented enum extension points
- said message instead of wire
- added a worked type example
- led the devices section with a subsystem's own nodes
- said where stable device names come from, with a udev rule
- gave the glossary an example of an extension point
---
Documentation/userspace-api/ctlv.rst | 694 ++++++++++++++++++++++++++
Documentation/userspace-api/index.rst | 1 +
MAINTAINERS | 1 +
3 files changed, 696 insertions(+)
create mode 100644 Documentation/userspace-api/ctlv.rst
diff --git a/Documentation/userspace-api/ctlv.rst b/Documentation/userspace-api/ctlv.rst
new file mode 100644
index 000000000000..f1f775071a78
--- /dev/null
+++ b/Documentation/userspace-api/ctlv.rst
@@ -0,0 +1,694 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+==================
+CTLV userspace ABI
+==================
+
+:Author: Jiri Pirko
+
+Char-Device Tag-Length-Value (CTLV) is a flexible way to describe system calls
+delivered over ioctl for a kernel subsystem. It provides a 64-bit operation
+number with variable-length input and output arguments described in a YAML
+schema. The arguments are serialized using a TLV scheme. Strong support for
+forward and backward compatibility is baked into the schema design.
+
+CTLV excels for uAPIs that are too unconstrained to fit in simple structs:
+things that have arrays of arrays, variable-size structs, variable-sized data,
+strings and similar, which are challenging to represent otherwise.
+
+As a transport, CTLV uses a global number space. Inspection tools like strace,
+or a seccomp user-notification supervisor, can always decode a CTLV ioctl
+without having to know what subsystem is using it. The global number space is
+organized into families, each roughly corresponding to a subsystem.
+
+A family describes what its devices accept in YAML, a generator turns that into
+kernel metadata, UAPI headers and userspace bindings, and CTLV validates every
+message before a family callback sees it. Userspace executes every operation
+through one ioctl.
+
+This document is the ABI. What each family accepts is specified under
+``Documentation/ctlv/specs``; writing a family is :doc:`/driver-api/ctlv`.
+
+Goals
+=====
+
+A uAPI too unconstrained for fixed structures needs four things: every request
+checked before a driver sees it, against one description that its headers and
+documentation come from too; room to grow in both directions without versions;
+one encoding that tools decode without knowing the subsystem; and answers
+larger than the caller's buffer, read in parts that stay consistent.
+
+CTLV is not worth it for a value or two, where a sysfs attribute or a plain
+ioctl is simpler.
+
+Each part of the design serves one of the needs:
+
+- The schema. CTLV checks a message's bounds, lengths, occurrence counts and
+ record depth (a record is a TLV of TLVs) against it before a family callback
+ runs, and the generator makes the kernel metadata, the UAPI header, the
+ userspace bindings and the documentation from it.
+- The tree of TLVs, for records and variable data. Fields are optional or
+ required by declaration, so a message can grow without breaking a caller
+ that does not know the new field.
+- One ioctl over a global number space. A new operation is a new
+ specification entry rather than a new ioctl number, and strace or a seccomp
+ supervisor decodes any family.
+- Queries apart from actions. Only a query can be answered in parts: the
+ kernel returns what fits with a cursor to continue from, and a generation,
+ so that a caller whose answer changed between the parts gets ``ESTALE``
+ rather than a mix of two states.
+- Introspection. Every device answers ``family-get``, ``op-get`` and
+ ``op-schema-get``, so a program built without a family still learns what
+ the device accepts.
+
+`Compatibility`_ collects the rules for growing a family.
+
+An example
+==========
+
+Take ``ctlv-example``, a small invented family whose devices hold numbered
+items, each with an optional label; :doc:`/driver-api/ctlv` writes it. Its
+system calls are::
+
+ item_set_action(u64 index, string label?, blob data?) -> status;
+ item_get_query() -> { u64 index; string label?; }[];
+
+``?`` marks an optional argument. The family's specification declares
+``item-set`` with the bounds the kernel checks:
+
+.. code-block:: yaml
+
+ ops:
+ # Sets an item's label and data.
+ item-set:
+ type: action
+ request:
+ # Which item it is.
+ index: uint(..4095)
+ # What the item is called.
+ label?: string(..63)
+ # The item's contents, which the device reads.
+ data?: blob(in, ..1M, align 8, va)
+
+To set the label of item 3 to "ab", a program builds a request of two TLVs,
+the index and the label, under the root every message starts with::
+
+ offset TLV len flags contents
+ ------ ---------------- --- ------ ----------------
+ 0 root 64 RECORD type = 0
+ 16 .. index 24 - 3
+ 40 .. label 19 - "ab" and its NUL
+ 64 end of message
+
+and passes it to the device:
+
+.. code-block:: c
+
+ struct ctlv_op_exec exec = {
+ .size = sizeof(exec),
+ .op_id = CTLV_EXAMPLE_OP_ITEM_SET,
+ .request = (__u64)(uintptr_t)msg,
+ .request_len = 64,
+ };
+
+ if (ioctl(fd, CTLV_IOC_OP_EXEC, &exec))
+ err(1, "item-set");
+
+The op ID and the types come from the family's generated UAPI header.
+Before the family sees the request, the kernel checks it against the
+specification: ``index`` must be at most 4095 and ``label`` at most 63 bytes.
+
+The sections below define each piece: the device, the message format, the
+ioctl, queries, blobs and introspection, and then the rules of compatibility.
+
+Terms
+=====
+
+family
+ The specification of what a device accepts, its operations and records,
+ under one number of the global number space. A family may inherit another,
+ its parent, taking its operations and filling the extension points it
+ leaves. A device answers as one family and is compatible with each of its
+ ancestors.
+
+operation, op
+ What a family declares a device does: an action, executed once and
+ acknowledged, or a query.
+
+TLV
+ Type, length, value: a ``struct ctlv_hdr`` and its payload.
+
+record
+ A TLV whose payload is TLVs, its fields, flagged ``CTLV_HDR_F_RECORD``:
+ a named record of the family, an operation's request or reply, or a
+ record declared inside one of those.
+
+root
+ The record a message starts with, of type zero: the operation's request
+ or reply.
+
+message
+ A request or a reply: one root record.
+
+field
+ One member of a record, a value or a record, required or optional, with its
+ bounds and how often it may repeat.
+
+type
+ The 64-bit number in a TLV's header naming its family, its record and its
+ field.
+
+extension point
+ A field a family leaves for a descendant family to fill with a record of
+ its own, as ``ctlv-accelerator`` leaves ``device-info`` for
+ ``ctlv-accelerator-foo``.
+
+enum extension point
+ An enum a family leaves for a descendant family to give values, so that a
+ device takes the values its own family chain gives.
+
+query
+ An operation answered in as many fragments as its answer needs, which a
+ generation keeps consistent.
+
+fragment
+ One reply of a query: as much of the answer as fits, with a cursor to
+ continue from.
+
+Devices
+========
+
+A device appears in one of two places, and the same ioctls run its family's
+operations on both:
+
+- a character device of a subsystem, in its class and ``/dev`` directory, as
+ an accelerator subsystem would have it::
+
+ /dev/accelerator/accelerator0
+ /sys/class/accelerator/accelerator0
+
+- a file whose other operations are a subsystem's own.
+
+Sysfs shows class membership, the ``dev`` number and the device-model link to
+the parent device. There is no family sysfs attribute; ``family-get`` on an
+open descriptor is the authoritative answer to what a device is.
+
+The number in a name says nothing about the device and, like ``sdX`` or
+``eventN``, may differ between boots. Stable names come from udev rules
+matching the uevent properties described under Discovery. A container is
+given such a name, ``/dev/ctlv-example`` below, rather than whichever number
+the device got.
+
+``O_RDONLY`` opens an observation fd, ``O_RDWR`` opens a full-control fd, and
+``O_WRONLY`` is rejected with ``-EACCES``. Every action and query declares the
+authority it needs: ``read`` permits either accepted fd, while ``read-write``
+requires full control. ``dup()`` and ``SCM_RIGHTS`` preserve that authority;
+attenuation requires a separate observation open.
+
+Discovery
+---------
+
+Both uevents, addition and removal, carry the family chain as numbered
+properties::
+
+ CTLV_FAMILY=ctlv-accelerator-foo
+ CTLV_COMPATIBLE_0=ctlv
+ CTLV_COMPATIBLE_1=ctlv-accelerator
+ CTLV_COMPATIBLE_2=ctlv-accelerator-foo
+ CTLV_COMPATIBLE_N=3
+
+``CTLV_FAMILY`` is the family the device answers as, the numbered properties
+are its ancestry from the root to that family, and ``CTLV_COMPATIBLE_N`` is
+how many there are. An index never moves: changing a family's parent is an
+ABI break, adding a descendant only appends.
+
+A device's uevents carry them whichever subsystem it is in, so a program finds
+every device by ``CTLV_FAMILY`` rather than by class. A program that wants
+every device, including ones appearing while it starts, must listen before it
+looks, which closes the window between enumerating and subscribing::
+
+ 1. create a udev monitor and enable it
+ 2. enumerate the existing character devices whose uevent has CTLV_FAMILY
+ 3. drain the events already queued on the monitor, keeping those with it
+ 4. deduplicate by sysfs path
+ 5. open the device node
+ 6. ask family-get on the open descriptor for the authoritative chain
+
+Steps 1 to 4 are pre-open discovery, all a udev rule can match on; step 6 does
+not need a udev database. A rule matches a family through the compatibility
+properties::
+
+ # /etc/udev/rules.d/70-ctlv.rules
+ #
+ # Anything of one family, whatever family implementation registered it.
+ ENV{CTLV_FAMILY}=="ctlv-accelerator-foo", GROUP="accel", MODE="0660"
+
+ # Anything descended from a family.
+ ENV{CTLV_COMPATIBLE_1}=="ctlv-accelerator", TAG+="uaccess"
+
+ # A stable name for a family with one device per system.
+ ENV{CTLV_FAMILY}=="ctlv-example", SYMLINK+="ctlv-example"
+
+ # A stable name per parent device, for a family with several devices.
+ ENV{CTLV_FAMILY}=="ctlv-accelerator-foo", IMPORT{builtin}="path_id"
+ ENV{CTLV_FAMILY}=="ctlv-accelerator-foo", ENV{ID_PATH}=="?*", \
+ SYMLINK+="accel/by-path/$env{ID_PATH}"
+
+A device whose uevent leaves the family out has a file a program opens by the
+device's own name, and the open descriptor answers ``family-get`` all the
+same.
+
+CTLV does not standardise such aliases and does not promise that a generic
+identifier survives a reboot or a reprobe; a family with a meaningful identity
+exposes it through an operation of its own.
+
+Message format
+==============
+
+A message is a tree of TLVs. Every TLV begins with the same header, at an
+eight-byte boundary::
+
+ struct ctlv_hdr {
+ __u32 len; /* header + payload, without padding */
+ __u32 flags; /* CTLV_HDR_F_* */
+ __aligned_u64 type; /* family, record, field; 0 for a root */
+ };
+
+One unsigned integer TLV with an eight-byte payload, in a message::
+
+ 0 1 2 3
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ +---------------------------------------------------------------+
+ | len = 24 |
+ +---------------------------------------------------------------+
+ | flags = 0 |
+ +---------------------------------------------------------------+
+ | |
+ + type +
+ | |
+ +---------------------------------------------------------------+
+ | |
+ + payload +
+ | |
+ +---------------------------------------------------------------+
+
+The rules that make a tree out of that:
+
+- ``CTLV_ALIGNTO`` is 8 and ``CTLV_ALIGN()`` rounds up to it. Every TLV
+ starts at a multiple of eight.
+- ``len`` counts this header plus this TLV's payload, not its own trailing
+ padding.
+- A record's payload is the concatenation of ``CTLV_ALIGN(child.len)`` bytes
+ per child, so a child's padding is counted inside its parent's ``len`` and
+ a leaf's own padding by nobody.
+- ``CTLV_HDR_F_RECORD`` is set exactly on a record, the root included.
+- A message begins with one root, whose ``type`` is ``CTLV_TYPE_ROOT``
+ (zero) and whose payload is the message. A root never appears inside a
+ record, and a record never carries the root type.
+- Scalar payloads are native-endian and their lengths exact: eight bytes for
+ an integer, one byte holding zero or one for a boolean, the string including
+ its NUL terminator, the declared length for binary data.
+- A message goes no deeper than its operation's schema, the root being level
+ zero.
+
+A whole message with every offset, for a request of one integer and one record
+carrying a string and a further record::
+
+ offset TLV len flags contents
+ ------ ------------------------------ --- ------ ----------------
+ 0 root 120 RECORD type = 0
+ 16 .. chunks 24 - 8-byte integer
+ 40 .. chunk 80 RECORD
+ 56 .. label 19 - "ab" and its NUL
+ 80 .. item 40 RECORD
+ 96 .. value 24 - 8-byte integer
+ 120 end of message
+
+ +----------------------------------------------------------------------+ 0
+ | root: len = 120, flags = RECORD, type = 0 |
+ | +----------------------------------------------------------------+ | 16
+ | | chunks: len = 24 | payload: 8 bytes | |
+ | +----------------------------------------------------------------+ | 40
+ | | chunk: len = 80, flags = RECORD | |
+ | | +----------------------------------------------------------+ | | 56
+ | | | label: len = 19 | payload: 'a' 'b' 0 | 5 bytes padding | | |
+ | | +----------------------------------------------------------+ | | 80
+ | | | item: len = 40, flags = RECORD | | |
+ | | | +----------------------------------------------------+ | | | 96
+ | | | | value: len = 24 | payload: 8 bytes | | | |
+ | | | +----------------------------------------------------+ | | | 120
+ | | +----------------------------------------------------------+ | |
+ | +----------------------------------------------------------------+ |
+ +----------------------------------------------------------------------+
+
+``label.len`` is 19: its header and three payload bytes, not the five bytes of
+padding after it. ``chunk`` counts those five, its ``len`` of 80 being its
+header plus ``CTLV_ALIGN(19)`` plus ``CTLV_ALIGN(40)``. A walk over a record's
+children advances by ``CTLV_ALIGN(child.len)``, a walk of one payload by
+``child.len``.
+
+A type names the family that declared it, a record within that family and a
+field within that record::
+
+ 63 48 47 46 45 32 31 28 27 24 23 20 19 16 15 0
+ +----------+-----+---------+-----+-----+-----+-----+-----------+
+ | family |kind | index | L1 | L2 | L3 | L4 | field |
+ +----------+-----+---------+-----+-----+-----+-----+-----------+
+ |<--------------- record -------------->|
+
+ CTLV_TYPE(family, record, field)
+ CTLV_TYPE_FAMILY(type) / CTLV_TYPE_KIND(type) / CTLV_TYPE_RECORD(type) /
+ CTLV_TYPE_FIELD(type)
+
+A record's type kind, ``enum ctlv_type_kind``, says what it starts from: a
+named record of the family (``CTLV_TYPE_KIND_NAMED``), an op's request
+(``_REQUEST``), or an op's reply (``_REPLY``). The index
+is the named record's place among the family's records, or the op's number. A
+record declared inside one of those adds a path, one hex digit a level for up
+to four levels, saying which it is among the records declared beside it,
+from 1; zero ends the path. The field is its place in its record, from 1. A
+named record has the same types in every message that carries it.
+
+If ``ctlv-example`` were family 7, ``label`` in an item of the reply of
+``item-get``, its op 2, would be ``0x0007c00210000002``. Read in blocks of
+four hex digits, ``0007`` is the family, ``c002`` op 2's reply, ``1000`` the
+path to ``items``, the reply's first record, and ``0002`` the field, the
+second of an item.
+
+An operation ID is the family and the op's number, with the record bits
+zero::
+
+ CTLV_OP(family, op)
+ CTLV_OP_FAMILY(op) / CTLV_OP_NUMBER(op)
+
+Use the generated types and op IDs. A family's header defines every type it
+declares and includes its parent's.
+
+TLV order
+---------
+
+TLVs may come in any order, with one rule: within the root and within each
+record of a request, all TLVs of one type, such as the elements of an array,
+must be next to each other. ``A A B`` and ``B A A`` are valid; ``A B A`` is
+not. An unknown TLV the kernel skips (see the next section) separates two of
+them too, so ``A X A`` is invalid even when ``X`` is skipped.
+
+TLVs an older kernel may skip
+-----------------------------
+
+A program built against a newer version of a family may want to send a field
+that an older kernel does not know. Normally an unknown type fails the
+request. Setting ``CTLV_HDR_F_IGNORE_IF_UNKNOWN`` in a TLV's header asks the
+kernel to skip it instead, together with everything in it.
+
+The flag only matters for a type that none of the device's families
+declares. A type the device knows is validated as usual, flag or not:
+it fails the request where the schema does not place it, and is checked
+against its bounds where it does. A skipped TLV still has to be well
+formed: its length must fit inside its record, and its header may carry only
+defined flags. The root cannot carry the flag, and replies never do.
+
+So the kernel acts on what it knows and ignores the rest. A caller that needs
+to know which of its fields will be acted on asks ``op-schema-get`` first.
+
+Executing an operation
+======================
+
+One ioctl executes every operation::
+
+ struct ctlv_op_exec {
+ __u32 size; /* sizeof(struct ctlv_op_exec) */
+ __u32 flags; /* must be zero */
+ __aligned_u64 op_id;
+ __aligned_u64 request; /* struct ctlv_hdr __user * */
+ __aligned_u64 reply; /* struct ctlv_hdr __user * */
+ __u32 request_len;
+ __u32 reply_len;
+ __aligned_u64 generation; /* queries only */
+ __aligned_u64 cursor; /* queries only */
+ };
+
+ #define CTLV_IOC_OP_EXEC _IO(0xBB, 0x00)
+
+``size`` is the size of the structure the caller was built with, at least the
+56 bytes of this first version. The structure only grows at its end, and the
+kernel reads ``size`` bytes: a field past the end of an older caller's
+structure reads as zero, and bytes a newer caller sends past the fields this
+kernel knows must be zero. The kernel writes back only within ``size``.
+
+``request_len`` is how many bytes the kernel may read at ``request`` and
+``reply_len`` how many it may write at ``reply``. They are copy boundaries, not
+message lengths: a root's ``len`` may be shorter than ``request_len``, the
+bytes after it being ignored, and may not be longer. ``reply_len`` is never
+overwritten; the length written is the reply root's ``len``. It may be any
+length: a query's fragment stops at a limit of the kernel's own, leaving the
+rest of a longer buffer unused. The pointers are fixed-width so a 32-bit and a
+64-bit process pass the same structure.
+
+Every action and query carries a request root, even where the request is
+empty, so ``request_len`` is at least one header. The
+``request-max-len`` reported by ``op-get`` is the maximum request composed
+from the known schema; a newer caller may exceed it with well-formed unknown
+TLVs marked ``IGNORE_IF_UNKNOWN``. The kernel refuses a request longer than a
+limit of its own, which no request composed from a schema exceeds.
+
+The two op types differ in what they answer:
+
+action
+ Executed once and acknowledged. ``reply_len`` is at least the
+ ``action-reply-max-len`` in ``op-get``, zero for an action that answers
+ nothing.
+
+query
+ Executed once and answered in as many fragments as it needs, using
+ ``generation`` and ``cursor``. ``reply_len`` is at least the
+ ``query-reply-min-len`` in ``op-get``, which also suggests a length as
+ ``query-reply-suggested-len``.
+
+Errors
+------
+
+A failed operation returns a negative errno. Where the failure is a rule the
+message broke and the caller offered a reply buffer, CTLV also writes
+error information there in TLV form: a ``validation-reason`` value, a
+human-readable message, the offset of the offending TLV, the offset of the
+record it was in, the type of a missing field, the length an output blob
+needs, and for a stale query the expected and the current generation. A
+buffer of the operation's ``error-info-max-len`` in ``op-get`` has room for
+it, and one too small leaves it unwritten; the errno is authoritative either
+way.
+
+What the ioctls answer with, as against a family's own callback:
+
+``ENOTTY``
+ Any ioctl but ``CTLV_IOC_OP_EXEC``, on a file only CTLV answers. Where the
+ file is also another subsystem's, that subsystem answers any other ioctl.
+
+``EINVAL``
+ ``size`` was below 56, ``flags`` was not zero, ``op_id`` was zero,
+ ``generation`` or ``cursor`` was set on something that is not a query, or
+ the message broke a rule of the format or of the schema.
+
+``E2BIG``
+ ``size`` was larger than the structure this kernel knows, and the bytes
+ past it were not zero.
+
+``EFAULT``
+ The structure, the request or the reply was not accessible.
+
+``EOVERFLOW``
+ A blob descriptor's address or offset plus its length does not fit in 64
+ bits.
+
+``EOPNOTSUPP``
+ The device does not publish the operation, or publishes it but has it
+ disabled.
+
+``EACCES``
+ The fd lacks the operation's declared authority.
+
+``EMSGSIZE``
+ ``reply_len`` is shorter than the operation's ``action-reply-max-len`` or
+ ``query-reply-min-len``, and nothing was executed; or an ``output`` blob
+ is too short, which the error information reports with
+ ``required-length``; see `Blobs`_.
+
+``ESTALE``
+ A continuation whose ``generation`` no longer matches. Start the query
+ again.
+
+``ENODEV``
+ The device was unregistered. Descriptors stay open and every operation on
+ them fails this way.
+
+``EBADF``, ``EIO``
+ A blob descriptor named something that is not a file, or the file refused
+ the transfer.
+
+``ENOMEM``
+ The kernel could not allocate what the operation needed.
+
+``open()`` answers ``EACCES`` for ``O_WRONLY``, which no operation accepts,
+``ENODEV`` once the device has been unregistered, and ``ENOMEM`` when the
+kernel could not allocate the open file.
+
+Queries and fragments
+=====================
+
+A query's answer may be larger than the buffer the caller wants to commit, so
+the kernel fills the buffer with as much of it as fits where the tree can be
+cut. The first call passes ``generation`` and ``cursor`` zero. On return:
+
+- the kernel puts the query's generation in ``generation``;
+- ``cursor`` is nonzero when there is more to come, zero when the answer is
+ complete;
+- the reply root carries ``CTLV_HDR_F_FRAG_MORE`` when the answer continues,
+ and so does every record still logically open at the cut.
+
+To continue, the caller repeats the call with the same ``op_id`` and request,
+the returned ``cursor`` and the returned ``generation``, and reassembles by
+concatenating the children of each still-open record.
+
+One answer of three items, cut by a buffer that holds two::
+
+ call 1: cursor in = 0 call 2: cursor in = c
+ cursor out = c cursor out = 0
+
+ +----------------------------+ +----------------------------+
+ | root: FRAG_MORE | | root: (no FRAG_MORE) |
+ | +-----------------------+ | | +-----------------------+ |
+ | | chunk: FRAG_MORE | | | | chunk: | |
+ | | +------------------+ | | | | +------------------+ | |
+ | | | item: value = 1 | | | | | | item: value = 3 | | |
+ | | +------------------+ | | | | +------------------+ | |
+ | | | item: value = 2 | | | | +-----------------------+ |
+ | | +------------------+ | | +----------------------------+
+ | +-----------------------+ |
+ +----------------------------+
+
+ reassembled: root -> chunk -> item(1), item(2), item(3)
+
+``FRAG_MORE`` on the root says the answer continues; on ``chunk`` it says the
+next fragment resumes inside that same chunk. A complete record does not carry
+it, so a reader knows whether to append or to open a new record beside it.
+
+Every continuing fragment adds at least one complete logical TLV beyond its
+repeated path, and a cursor value cannot recur within one logical reply.
+A reader must reject either violation.
+
+A continuation whose generation no longer matches fails with ``-ESTALE``, and
+the error information carries the expected and the current generation.
+
+Blobs
+=====
+
+A ``binary`` field carries its bytes in the message itself, within the
+length its family declares. An operation that moves more data than a message
+should carry declares a blob field instead, and the caller passes a
+descriptor rather than the data: the root family's ``blob`` record, declared
+in ``Documentation/ctlv/specs/ctlv.yaml`` and numbered in
+``include/uapi/ctlv/ctlv_gen.h``. ``blob.type`` names the backing and
+``blob.length`` the length in bytes; the backing says which of
+``blob.address``, ``blob.fd`` and ``blob.offset`` name the memory. One
+backing is defined:
+
+``va``
+ Anonymous userspace memory of the calling process, named by an address in
+ ``blob.address``.
+
+The backings a field accepts, the direction data may flow, the maximum
+length and the required alignment are declared by the family and reported by
+``op-schema-get``. Memory is acquired for the operation and released before it
+returns; a descriptor the operation may not use is refused before the callback.
+While the operation is in flight, userspace must not modify an ``input`` blob
+or access an ``output`` or ``inout`` blob. Whether two descriptors overlap is
+also the caller's responsibility. Keep every named VA mapping valid until the
+operation returns.
+
+A descriptor is checked with the rest of the request, before any memory is
+acquired. A malformed field fails like any other. A backing the
+blob field does not accept, fields that do not fit the backing, a zero length
+or one above the maximum, or a misaligned address or offset fails with
+``EINVAL``, and a range that does not fit in 64 bits with ``EOVERFLOW``; both
+report ``bad-blob-descriptor`` at the descriptor. The memory is acquired when
+the operation first uses it, and a failure there, such as ``ENOMEM``, reports
+the same reason at the same place. An operation that
+finds an ``output`` blob too short for what it has to write there fails with
+``EMSGSIZE``, and reports the same with ``required-length``, how long the blob
+has to be, so the caller can retry with one that long.
+
+Breaking the in-flight rules harms only the caller. The kernel copies blob
+data, so what such an operation reads or writes is unspecified and the
+operation may fail, but no kernel memory is at risk.
+
+Introspection
+=============
+
+Three queries describe a device to a program that was not compiled against it.
+The CTLV core answers all three on every device.
+
+``family-get``
+ The device's family chain, root first, as a numeric ID and a name per
+ entry. The authoritative answer to what the device is.
+
+``op-get``
+ One entry per published operation: its ID, op type, required fd access,
+ whether its availability is fixed or dynamic, whether it is currently
+ disabled, and those of its lengths on this device that apply to its op type:
+ ``request-max-len``, ``action-reply-max-len``, ``query-reply-min-len``,
+ ``query-reply-suggested-len`` and ``error-info-max-len``. An unpublished
+ operation has no entry and is indistinguishable from one no family ever
+ declared.
+
+``op-schema-get``
+ The whole tree of one operation: every field of the request and the
+ reply, with its type, its schema kind, its bounds, its occurrence limit,
+ and its unit and scale where it has them.
+
+A schema that describes a schema does not end, so it is reported through
+re-entry: a node whose fields are those of a level already described carries
+``schema-node.reenters``, saying how many levels up that level is. A consumer
+that follows ``reenters`` handles any depth.
+
+The lengths in ``op-get`` include whatever a descendant family added to an
+inherited operation, so ask rather than compute: a family's generated maximum
+is right for its own devices and a lower bound on a descendant's. Likewise, an
+enum extension point's bounds are the values the device's family chain gives
+it, from 1 to 0 when it gives none, and their names are in the uapi header of
+the family that gives them.
+
+A program compiled against a family needs only ``family-get``, once, when
+it opens the device. The chain runs from ``ctlv`` to the device's family, and
+a HW driver usually has a family of its own, such as
+``ctlv-accelerator-foo``, so the last entry also says whose fields the replies
+carry. The answer comes from the open descriptor, so it works where udev never
+ran, such as inside a container; the uevent variables under Discovery are for
+udev rules. A field a descendant family adds to an inherited operation also
+carries that family's ID, in bits 63..48 of its type.
+
+Compatibility
+=============
+
+CTLV has no versions. A program built against one kernel keeps working on a
+later one, and a later program finds out what an earlier kernel acts on, by
+these rules:
+
+- ``struct ctlv_op_exec`` carries its size and grows only at its end; see
+ `Executing an operation`_.
+- A family grows by new operations and new optional fields. A field's type
+ includes its place in its record, so a new field goes last, and the others
+ keep their types.
+- A reply may carry TLVs an older program does not know, and it skips them. A
+ request TLV the kernel does not know fails the request unless the program
+ marks it ``CTLV_HDR_F_IGNORE_IF_UNKNOWN``, and ``op-schema-get`` tells a
+ newer program which of its fields the kernel acts on; see
+ `TLVs an older kernel may skip`_.
+- Anything incompatible is a new operation.
+
+Each family has a checked-in ABI snapshot under ``Documentation/ctlv/abi``, so
+a change to its uAPI shows as a change to that file. ``make -C tools/ctlv
+regen`` rewrites the snapshot from the family's specification, and refuses
+removing or renumbering anything, changing a field's kind or an operation's
+type, access or availability, narrowing a bound or an occurrence limit,
+making a field that the kernel writes optional, and, in any message,
+making a field required or adding a required one.
diff --git a/Documentation/userspace-api/index.rst b/Documentation/userspace-api/index.rst
index a68b1bea57a8..d1b53c4e16eb 100644
--- a/Documentation/userspace-api/index.rst
+++ b/Documentation/userspace-api/index.rst
@@ -45,6 +45,7 @@ Devices and I/O
:maxdepth: 1
accelerators/ocxl
+ ctlv
dma-buf-heaps
dma-buf-alloc-exchange
fwctl/index
diff --git a/MAINTAINERS b/MAINTAINERS
index bf6d5bab0da9..e6748a4ad282 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -7188,6 +7188,7 @@ M: Jiri Pirko <jiri@resnulli.us>
L: linux-kernel@vger.kernel.org
S: Maintained
F: Documentation/ctlv/
+F: Documentation/userspace-api/ctlv.rst
F: include/linux/ctlv*.h
F: include/uapi/ctlv/
F: tools/ctlv/
--
2.56.0
^ permalink raw reply related [flat|nested] 24+ messages in thread
* [PATCH RFC v2 05/15] ctlv: Prepare message input handling
2026-11-13 15:54 [PATCH RFC v2 00/15] ctlv: Add a schema-driven TLV uAPI for character devices Jiri Pirko
` (3 preceding siblings ...)
2026-11-13 15:55 ` [PATCH RFC v2 04/15] Documentation: ctlv: Add userspace ABI Jiri Pirko
@ 2026-11-13 15:55 ` Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 06/15] ctlv: Prepare blob descriptor validation Jiri Pirko
` (9 subsequent siblings)
14 siblings, 0 replies; 24+ messages in thread
From: Jiri Pirko @ 2026-11-13 15:55 UTC (permalink / raw)
To: linux-coco
Cc: linux-kernel, linux-api, x86, kvm, linux-doc, jgg, david.quigley,
lukas, ksinyuk, rodrigo.vivi, cedric.xing, sameo, drdeeglaze,
James.Bottomley, aik, jpiotrowski, qinkun, pgonda, jic23,
alistair.francis, ankita, suzuki.poulose, kas, rick.p.edgecombe,
elena.reshetova, dave.hansen, sathyanarayanan.kuppuswamy,
peter.fang, jarkko, zohar, yeoreum.yun, aneesh.kumar, dave.jiang,
nik.borisov, sergii.dmytruk
From: Jiri Pirko <jiri@nvidia.com>
Add TLV walking, request indexing and validated input accessors for
generated family readers, and CTLV_ASSERT() for the core's invariants
that no error path depends on, checked only with CONFIG_CTLV_DEBUG.
Keep CONFIG_CTLV unwired until dispatch completes the module.
Assisted-by: Cursor: GPT-5.6 Sol
Assisted-by: Cursor: Claude Opus 5.5
Signed-off-by: Jiri Pirko <jiri@nvidia.com>
---
RFC->RFCv2:
- made ctlv_attr_next() end the walk on an attr overrunning its scope
- made CTLV a symbol its users select, with no prompt or help
- used void pointer arithmetic for attr addresses
- scoped the rebind loop variable to its loop
- cleared the binary attr length once
- dropped the comment ending message.c
- added CTLV_ASSERT(), checked only with CONFIG_CTLV_DEBUG
- said TLV, type, record and field instead of attr
- said record instead of nest, the root being one
- kept the length an output blob needs with the error
- separated the steps of the longer functions
- numbered every enum member
- took the UAPI headers from include/uapi/ctlv/
- moved to lib/ctlv/
---
MAINTAINERS | 1 +
include/linux/ctlv_family.h | 135 +++++++++++++
lib/ctlv/Kconfig | 13 ++
lib/ctlv/Makefile | 4 +
lib/ctlv/internal.h | 74 +++++++
lib/ctlv/message.c | 393 ++++++++++++++++++++++++++++++++++++
lib/ctlv/message.h | 91 +++++++++
7 files changed, 711 insertions(+)
create mode 100644 lib/ctlv/Kconfig
create mode 100644 lib/ctlv/Makefile
create mode 100644 lib/ctlv/internal.h
create mode 100644 lib/ctlv/message.c
create mode 100644 lib/ctlv/message.h
diff --git a/MAINTAINERS b/MAINTAINERS
index e6748a4ad282..29bdb4de915d 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -7191,6 +7191,7 @@ F: Documentation/ctlv/
F: Documentation/userspace-api/ctlv.rst
F: include/linux/ctlv*.h
F: include/uapi/ctlv/
+F: lib/ctlv/
F: tools/ctlv/
CTU CAN FD DRIVER
diff --git a/include/linux/ctlv_family.h b/include/linux/ctlv_family.h
index 360eb6e30974..ae8ed69df5ea 100644
--- a/include/linux/ctlv_family.h
+++ b/include/linux/ctlv_family.h
@@ -428,4 +428,139 @@ extern const struct ctlv_family ctlv_family;
u32 ctlv_enum_ext_max(const struct ctlv_family *family, u16 family_id, u32 ext_id);
+struct ctlv_cursor *
+ctlv_input_field(struct ctlv_cursor *cursor, const struct ctlv_cursor *record,
+ struct ctlv_input_ref ref);
+bool ctlv_input_cursor(const struct ctlv_cursor *cursor,
+ struct ctlv_input_ref ref);
+void ctlv_cursor_next(struct ctlv_cursor *cursor);
+bool ctlv_cursor_ok(const struct ctlv_cursor *cursor);
+u64 ctlv_cursor_u64(const struct ctlv_cursor *cursor);
+s64 ctlv_cursor_s64(const struct ctlv_cursor *cursor);
+bool ctlv_cursor_bool(const struct ctlv_cursor *cursor);
+const char *ctlv_cursor_string(const struct ctlv_cursor *cursor);
+const void *ctlv_cursor_binary(const struct ctlv_cursor *cursor, u32 *len);
+
+#define CTLV_IN_REF(_name, _type, _kind) \
+static inline struct ctlv_input_ref _name##_ref(void) \
+{ \
+ return (struct ctlv_input_ref){ \
+ .type = (_type), \
+ .kind = (_kind), \
+ }; \
+}
+
+#define CTLV_IN_FOR_EACH(_name, _cursor, _record) \
+ for (struct ctlv_cursor *__ctlv_cursor = \
+ ctlv_input_field(_cursor, _record, _name##_ref()); \
+ ctlv_cursor_ok(__ctlv_cursor); \
+ ctlv_cursor_next(__ctlv_cursor))
+
+#define CTLV_IN_HAS(_name) \
+static inline bool _name##_present(const struct ctlv_cursor *record) \
+{ \
+ struct ctlv_cursor cursor; \
+ \
+ ctlv_input_field(&cursor, record, _name##_ref()); \
+ return ctlv_cursor_ok(&cursor); \
+}
+
+#define CTLV_IN_U64(_name) \
+static inline u64 _name##_get(const struct ctlv_cursor *record) \
+{ \
+ struct ctlv_cursor cursor; \
+ \
+ ctlv_input_field(&cursor, record, _name##_ref()); \
+ return ctlv_cursor_u64(&cursor); \
+}
+
+#define CTLV_IN_S64(_name) \
+static inline s64 _name##_get(const struct ctlv_cursor *record) \
+{ \
+ struct ctlv_cursor cursor; \
+ \
+ ctlv_input_field(&cursor, record, _name##_ref()); \
+ return ctlv_cursor_s64(&cursor); \
+}
+
+#define CTLV_IN_BOOL(_name) \
+static inline bool _name##_get(const struct ctlv_cursor *record) \
+{ \
+ struct ctlv_cursor cursor; \
+ \
+ ctlv_input_field(&cursor, record, _name##_ref()); \
+ return ctlv_cursor_bool(&cursor); \
+}
+
+#define CTLV_IN_STRING(_name) \
+static inline const char * \
+_name##_get(const struct ctlv_cursor *record) \
+{ \
+ struct ctlv_cursor cursor; \
+ \
+ ctlv_input_field(&cursor, record, _name##_ref()); \
+ return ctlv_cursor_string(&cursor); \
+}
+
+#define CTLV_IN_BINARY(_name) \
+static inline const void * \
+_name##_get(const struct ctlv_cursor *record, u32 *len) \
+{ \
+ struct ctlv_cursor cursor; \
+ \
+ ctlv_input_field(&cursor, record, _name##_ref()); \
+ return ctlv_cursor_binary(&cursor, len); \
+}
+
+#define CTLV_IN_RECORD(_name) \
+static inline void _name##_get(struct ctlv_cursor *inner, \
+ const struct ctlv_cursor *record) \
+{ \
+ ctlv_input_field(inner, record, _name##_ref()); \
+}
+
+#define CTLV_IN_EACH_U64(_name) \
+static inline u64 _name##_get(const struct ctlv_cursor *cursor) \
+{ \
+ if (!ctlv_input_cursor(cursor, _name##_ref())) \
+ return 0; \
+ return ctlv_cursor_u64(cursor); \
+}
+
+#define CTLV_IN_EACH_S64(_name) \
+static inline s64 _name##_get(const struct ctlv_cursor *cursor) \
+{ \
+ if (!ctlv_input_cursor(cursor, _name##_ref())) \
+ return 0; \
+ return ctlv_cursor_s64(cursor); \
+}
+
+#define CTLV_IN_EACH_BOOL(_name) \
+static inline bool _name##_get(const struct ctlv_cursor *cursor) \
+{ \
+ if (!ctlv_input_cursor(cursor, _name##_ref())) \
+ return false; \
+ return ctlv_cursor_bool(cursor); \
+}
+
+#define CTLV_IN_EACH_STRING(_name) \
+static inline const char * \
+_name##_get(const struct ctlv_cursor *cursor) \
+{ \
+ if (!ctlv_input_cursor(cursor, _name##_ref())) \
+ return NULL; \
+ return ctlv_cursor_string(cursor); \
+}
+
+#define CTLV_IN_EACH_BINARY(_name) \
+static inline const void * \
+_name##_get(const struct ctlv_cursor *cursor, u32 *len) \
+{ \
+ if (!ctlv_input_cursor(cursor, _name##_ref())) { \
+ *len = 0; \
+ return NULL; \
+ } \
+ return ctlv_cursor_binary(cursor, len); \
+}
+
#endif /* _LINUX_CTLV_FAMILY_H */
diff --git a/lib/ctlv/Kconfig b/lib/ctlv/Kconfig
new file mode 100644
index 000000000000..465663bb7efa
--- /dev/null
+++ b/lib/ctlv/Kconfig
@@ -0,0 +1,13 @@
+# SPDX-License-Identifier: GPL-2.0-only
+config CTLV
+ tristate
+
+config CTLV_DEBUG
+ bool "CTLV core self-checks"
+ depends on CTLV
+ help
+ Checks the CTLV core's own bookkeeping where no error path depends
+ on the result, and warns once when it is wrong. The checks that do
+ guard an error path are made either way.
+
+ If you are unsure, say N.
diff --git a/lib/ctlv/Makefile b/lib/ctlv/Makefile
new file mode 100644
index 000000000000..4f0fe41bc9e1
--- /dev/null
+++ b/lib/ctlv/Makefile
@@ -0,0 +1,4 @@
+# SPDX-License-Identifier: GPL-2.0
+obj-$(CONFIG_CTLV) += ctlv.o
+
+ctlv-y += message.o
diff --git a/lib/ctlv/internal.h b/lib/ctlv/internal.h
new file mode 100644
index 000000000000..90d07d2638d3
--- /dev/null
+++ b/lib/ctlv/internal.h
@@ -0,0 +1,74 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES
+ *
+ * CTLV core internals.
+ */
+#ifndef _CTLV_INTERNAL_H
+#define _CTLV_INTERNAL_H
+
+#include <linux/bug.h>
+#include <linux/ctlv_family.h>
+#include <linux/types.h>
+
+#include <uapi/ctlv/ctlv.h>
+
+#include "message.h"
+
+/* Without CONFIG_CTLV_DEBUG @cond is not evaluated, so nothing may rely on it. */
+#ifdef CONFIG_CTLV_DEBUG
+#define CTLV_ASSERT(cond) ((void)WARN_ON_ONCE(!(cond)))
+#else
+#define CTLV_ASSERT(cond) BUILD_BUG_ON_INVALID(cond)
+#endif
+
+#define CTLV_ERROR_MESSAGE_LEN 256
+
+/* An op that is always enabled has no per-device availability state. */
+#define CTLV_OP_STATE_NONE U32_MAX
+
+struct ctlv_op_entry {
+ const struct ctlv_op_schema *schema;
+ const struct ctlv_op *binding;
+ struct ctlv_op_sizes sizes;
+ u32 state_index;
+ bool absent;
+};
+
+struct ctlv_error {
+ u32 reason;
+ char message[CTLV_ERROR_MESSAGE_LEN];
+ u32 bad_tlv_offset;
+ u32 record_offset;
+ u64 missing_type;
+ u64 expected_generation;
+ u64 current_generation;
+ u64 required_length;
+};
+
+struct ctlv_query_fragment {
+ u64 generation;
+ u64 cursor;
+ u64 reported;
+ u64 next_cursor;
+ bool progress;
+ bool answered;
+ int result;
+};
+
+struct ctlv_op_ctx {
+ const struct ctlv_family *family;
+ const struct ctlv_op_entry *op;
+
+ struct ctlv_msg request;
+ struct ctlv_cursor request_root;
+
+ struct ctlv_msg output;
+ struct ctlv_cursor output_root;
+ struct ctlv_cursor *deepest;
+
+ struct ctlv_query_fragment frag;
+ struct ctlv_error error;
+};
+
+#endif /* _CTLV_INTERNAL_H */
diff --git a/lib/ctlv/message.c b/lib/ctlv/message.c
new file mode 100644
index 000000000000..8d672e42cbe3
--- /dev/null
+++ b/lib/ctlv/message.c
@@ -0,0 +1,393 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES
+ *
+ * Message walking, indexing, and generated-accessor primitives.
+ */
+#include <linux/align.h>
+#include <linux/bits.h>
+#include <linux/bug.h>
+#include <linux/ctlv.h>
+#include <linux/err.h>
+#include <linux/errno.h>
+#include <linux/export.h>
+#include <linux/string.h>
+#include <linux/types.h>
+
+#include <uapi/ctlv/ctlv.h>
+#include <uapi/ctlv/ctlv_gen.h>
+
+#include "internal.h"
+#include "message.h"
+
+enum ctlv_message_step ctlv_message_next(const struct ctlv_hdr *record,
+ const struct ctlv_hdr **child)
+{
+ const u8 *end = (const u8 *)record + record->len;
+ const u8 *next;
+ size_t room;
+
+ if (record->len < sizeof(*record))
+ return CTLV_MESSAGE_STEP_BAD;
+ if (*child)
+ next = (const u8 *)*child + ALIGN((size_t)(*child)->len, CTLV_ALIGNTO);
+ else
+ next = (const u8 *)(record + 1);
+ if (next >= end)
+ return next == end ? CTLV_MESSAGE_STEP_END : CTLV_MESSAGE_STEP_BAD;
+
+ room = end - next;
+ if (room < sizeof(**child))
+ return CTLV_MESSAGE_STEP_BAD;
+ *child = (const struct ctlv_hdr *)next;
+ /*
+ * Length before alignment, so that aligning a length this record could
+ * never hold is not what overflows.
+ */
+ if ((*child)->len < sizeof(**child) || (*child)->len > room)
+ return CTLV_MESSAGE_STEP_BAD;
+ if (ALIGN((size_t)(*child)->len, CTLV_ALIGNTO) > room)
+ return CTLV_MESSAGE_STEP_BAD;
+ return CTLV_MESSAGE_STEP_TLV;
+}
+
+u32 ctlv_message_offset(const struct ctlv_msg *msg, const struct ctlv_hdr *hdr)
+{
+ return (const u8 *)hdr - (const u8 *)msg->root;
+}
+
+void ctlv_message_root(struct ctlv_cursor *cursor, struct ctlv_msg *msg)
+{
+ *cursor = (struct ctlv_cursor){
+ .msg = msg,
+ .hdr = msg->root,
+ .tree = msg->tree,
+ .index_record_scope = CTLV_MESSAGE_INPUT_INDEX_NONE,
+ .index_scope = CTLV_MESSAGE_INPUT_INDEX_NONE,
+ };
+ if (msg->request && msg->index)
+ cursor->index_record_scope = msg->index->records[0].first_scope;
+}
+
+static bool ctlv_message_record_index_ok(const struct ctlv_cursor *record)
+{
+ const struct ctlv_message_input_index *index = record->msg->index;
+ const struct ctlv_message_input_record *indexed;
+ const struct ctlv_message_input_scope *scope;
+ const struct ctlv_hdr *hdr;
+
+ if (WARN_ON_ONCE(!record->msg->request || !index ||
+ record->index_record_scope >= index->scopes_count))
+ return false;
+ scope = &index->scopes[record->index_record_scope];
+ if (WARN_ON_ONCE(scope->record >= index->records_count))
+ return false;
+ indexed = &index->records[scope->record];
+ hdr = (const void *)record->msg->root + indexed->offset;
+ if (WARN_ON_ONCE(hdr != record->hdr))
+ return false;
+ return !WARN_ON_ONCE(scope->tree != record->tree || scope->node != record->node);
+}
+
+static bool ctlv_message_input_scope_ok(const struct ctlv_message_input_index *index,
+ const struct ctlv_message_input_scope *scope)
+{
+ return !WARN_ON_ONCE(scope->first_slot > index->slots_count ||
+ scope->slots_count > index->slots_count - scope->first_slot);
+}
+
+/* A reader names a field its record lacks only by a family's mistake. */
+static bool ctlv_message_input_slot(const struct ctlv_cursor *record, struct ctlv_input_ref ref,
+ u32 *slot)
+{
+ const struct ctlv_schema_tree *tree = record->tree;
+ const struct ctlv_node_schema *parent;
+ const struct ctlv_node_schema *node;
+
+ if (WARN_ON_ONCE(!tree || !tree->node_schemas || record->node >= tree->node_schemas_count))
+ return false;
+ parent = &tree->node_schemas[record->node];
+ if (WARN_ON_ONCE(parent->first_child > tree->node_schemas_count ||
+ parent->children_count > tree->node_schemas_count - parent->first_child))
+ return false;
+ for (*slot = 0; *slot < parent->children_count; (*slot)++) {
+ node = &tree->node_schemas[parent->first_child + *slot];
+ if (node->type == ref.type)
+ return !WARN_ON_ONCE(node->kind != ref.kind);
+ }
+ WARN_ON_ONCE(1);
+ return false;
+}
+
+static const struct ctlv_message_input_slot *
+ctlv_message_cursor_index(const struct ctlv_cursor *cursor)
+{
+ const struct ctlv_message_input_index *index;
+ const struct ctlv_message_input_scope *scope;
+ const struct ctlv_node_schema *parent;
+ const struct ctlv_node_schema *node;
+ const struct ctlv_message_input_slot *slot;
+ const struct ctlv_message_input_record *record;
+ u32 local_slot;
+ u32 offset;
+
+ if (WARN_ON_ONCE(!cursor->msg || !cursor->tree || !cursor->msg->request ||
+ !cursor->msg->index))
+ return NULL;
+
+ index = cursor->msg->index;
+ if (WARN_ON_ONCE(cursor->index_scope >= index->scopes_count))
+ return NULL;
+ scope = &index->scopes[cursor->index_scope];
+ if (!ctlv_message_input_scope_ok(index, scope))
+ return NULL;
+
+ parent = &scope->tree->node_schemas[scope->node];
+ if (WARN_ON_ONCE(scope->tree != cursor->tree || cursor->node < parent->first_child ||
+ cursor->node >= parent->first_child + scope->slots_count))
+ return NULL;
+
+ local_slot = cursor->node - parent->first_child;
+ node = &cursor->tree->node_schemas[cursor->node];
+ slot = &index->slots[scope->first_slot + local_slot];
+ if (!cursor->hdr)
+ return slot;
+
+ if (WARN_ON_ONCE(cursor->hdr->type != node->type))
+ return NULL;
+ if (WARN_ON_ONCE(scope->record >= index->records_count))
+ return NULL;
+ record = &index->records[scope->record];
+ if (WARN_ON_ONCE(cursor->parent != (const void *)cursor->msg->root + record->offset))
+ return NULL;
+ offset = ctlv_message_offset(cursor->msg, cursor->hdr);
+ if (WARN_ON_ONCE(offset < slot->first_offset || offset >= slot->end_offset ||
+ offset + ALIGN((size_t)cursor->hdr->len, CTLV_ALIGNTO) >
+ slot->end_offset))
+ return NULL;
+
+ if (node->kind == CTLV_SCHEMA_RECORD) {
+ const struct ctlv_message_input_scope *record_scope;
+
+ if (WARN_ON_ONCE(cursor->index_record_scope >= index->scopes_count))
+ return NULL;
+ record_scope = &index->scopes[cursor->index_record_scope];
+ if (WARN_ON_ONCE(record_scope->record >= index->records_count ||
+ index->records[record_scope->record].offset != offset ||
+ record_scope->tree != cursor->tree ||
+ record_scope->node != cursor->node))
+ return NULL;
+ } else if (WARN_ON_ONCE(cursor->index_record_scope != CTLV_MESSAGE_INPUT_INDEX_NONE)) {
+ return NULL;
+ }
+ return slot;
+}
+
+bool ctlv_message_input_rebind(struct ctlv_cursor *record, const struct ctlv_schema_tree *tree,
+ u32 node)
+{
+ const struct ctlv_message_input_scope *bound_scope;
+ const struct ctlv_message_input_record *indexed;
+
+ if (WARN_ON_ONCE(!record->hdr || !record->msg || !record->msg->index))
+ return false;
+ if (!ctlv_message_record_index_ok(record))
+ return false;
+ bound_scope = &record->msg->index->scopes[record->index_record_scope];
+ indexed = &record->msg->index->records[bound_scope->record];
+ for (u32 i = 0; i < indexed->scopes_count; i++) {
+ u32 scope_index = indexed->first_scope + i;
+ const struct ctlv_message_input_scope *scope =
+ &record->msg->index->scopes[scope_index];
+
+ if (scope->tree != tree || scope->node != node)
+ continue;
+ record->tree = tree;
+ record->node = node;
+ record->index_record_scope = scope_index;
+ return true;
+ }
+ return false;
+}
+
+struct ctlv_cursor *ctlv_input_field(struct ctlv_cursor *cursor, const struct ctlv_cursor *record,
+ struct ctlv_input_ref ref)
+{
+ const struct ctlv_message_input_index *index;
+ const struct ctlv_message_input_scope *scope;
+ const struct ctlv_message_input_slot *indexed;
+ u32 slot;
+
+ *cursor = (struct ctlv_cursor){
+ .index_record_scope = CTLV_MESSAGE_INPUT_INDEX_NONE,
+ .index_scope = CTLV_MESSAGE_INPUT_INDEX_NONE,
+ };
+
+ if (WARN_ON_ONCE(!record->hdr || !record->msg))
+ return cursor;
+ if (!ctlv_message_record_index_ok(record))
+ return cursor;
+
+ index = record->msg->index;
+ if (WARN_ON_ONCE(record->index_record_scope >= index->scopes_count))
+ return cursor;
+ scope = &index->scopes[record->index_record_scope];
+ if (!ctlv_message_input_scope_ok(index, scope))
+ return cursor;
+ if (!ctlv_message_input_slot(record, ref, &slot) ||
+ WARN_ON_ONCE(slot >= scope->slots_count))
+ return cursor;
+
+ indexed = &index->slots[scope->first_slot + slot];
+ *cursor = (struct ctlv_cursor){
+ .msg = record->msg,
+ .parent = record->hdr,
+ .hdr = indexed->count ?
+ (const void *)record->msg->root + indexed->first_offset : NULL,
+ .tree = record->tree,
+ .node = record->tree->node_schemas[record->node].first_child + slot,
+ .depth = record->depth + 1,
+ .index_record_scope = CTLV_MESSAGE_INPUT_INDEX_NONE,
+ .index_scope = record->index_record_scope,
+ };
+
+ if (indexed->count && ref.kind == CTLV_SCHEMA_RECORD) {
+ if (WARN_ON_ONCE(indexed->first_record >= index->records_count))
+ return cursor;
+ cursor->index_record_scope = index->records[indexed->first_record].first_scope;
+ }
+ return cursor;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_input_field, "CTLV");
+
+bool ctlv_input_cursor(const struct ctlv_cursor *cursor, struct ctlv_input_ref ref)
+{
+ const struct ctlv_node_schema *node;
+
+ if (!ctlv_message_cursor_index(cursor))
+ return false;
+ node = &cursor->tree->node_schemas[cursor->node];
+ return !WARN_ON_ONCE(node->type != ref.type || node->kind != ref.kind);
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_input_cursor, "CTLV");
+
+void ctlv_cursor_next(struct ctlv_cursor *cursor)
+{
+ const struct ctlv_message_input_slot *slot;
+ const struct ctlv_message_input_scope *scope;
+ const struct ctlv_node_schema *node;
+ const struct ctlv_hdr *next;
+ u32 next_record;
+ u32 next_offset;
+
+ slot = ctlv_message_cursor_index(cursor);
+ if (!slot || WARN_ON_ONCE(!cursor->hdr))
+ return;
+
+ next = (const void *)cursor->hdr +
+ ALIGN((size_t)cursor->hdr->len, CTLV_ALIGNTO);
+ next_offset = ctlv_message_offset(cursor->msg, next);
+ if (next_offset >= slot->end_offset) {
+ CTLV_ASSERT(next_offset == slot->end_offset);
+ cursor->hdr = NULL;
+ cursor->index_record_scope = CTLV_MESSAGE_INPUT_INDEX_NONE;
+ return;
+ }
+
+ cursor->hdr = next;
+ node = &cursor->tree->node_schemas[cursor->node];
+ if (node->kind == CTLV_SCHEMA_RECORD) {
+ if (WARN_ON_ONCE(cursor->index_record_scope >= cursor->msg->index->scopes_count))
+ return;
+ scope = &cursor->msg->index->scopes[cursor->index_record_scope];
+ if (WARN_ON_ONCE(scope->record >= cursor->msg->index->records_count))
+ return;
+ next_record = cursor->msg->index->records[scope->record].next;
+ if (WARN_ON_ONCE(next_record == CTLV_MESSAGE_INPUT_INDEX_NONE ||
+ next_record >= cursor->msg->index->records_count))
+ return;
+ cursor->index_record_scope = cursor->msg->index->records[next_record].first_scope;
+ }
+ CTLV_ASSERT(ctlv_message_cursor_index(cursor));
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_cursor_next, "CTLV");
+
+bool ctlv_cursor_ok(const struct ctlv_cursor *cursor)
+{
+ return cursor && cursor->hdr && ctlv_message_cursor_index(cursor);
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_cursor_ok, "CTLV");
+
+/* The payload, checked against the schema kinds a reader of this C type accepts. */
+static const void *ctlv_message_cursor_payload(const struct ctlv_cursor *cursor, u32 kinds,
+ u32 *len)
+{
+ const struct ctlv_node_schema *node;
+
+ if (!ctlv_message_cursor_index(cursor) || WARN_ON_ONCE(!cursor->hdr))
+ return NULL;
+ node = &cursor->tree->node_schemas[cursor->node];
+ if (WARN_ON_ONCE(!(kinds & BIT(node->kind))))
+ return NULL;
+ *len = ctlv_message_hdr_payload_len(cursor->hdr);
+ return ctlv_message_hdr_payload(cursor->hdr);
+}
+
+/* A length disagreeing with a validated schema kind is this core's mistake. */
+static const void *ctlv_message_cursor_fixed(const struct ctlv_cursor *cursor, u32 kinds,
+ u32 expected)
+{
+ const void *payload;
+ u32 len;
+
+ payload = ctlv_message_cursor_payload(cursor, kinds, &len);
+ if (!payload || WARN_ON_ONCE(len != expected))
+ return NULL;
+ return payload;
+}
+
+u64 ctlv_cursor_u64(const struct ctlv_cursor *cursor)
+{
+ const u64 *value = ctlv_message_cursor_fixed(cursor,
+ BIT(CTLV_SCHEMA_UINT) | BIT(CTLV_SCHEMA_ENUM) |
+ BIT(CTLV_SCHEMA_FLAGS),
+ sizeof(u64));
+
+ return value ? *value : 0;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_cursor_u64, "CTLV");
+
+s64 ctlv_cursor_s64(const struct ctlv_cursor *cursor)
+{
+ const s64 *value = ctlv_message_cursor_fixed(cursor, BIT(CTLV_SCHEMA_SINT), sizeof(s64));
+
+ return value ? *value : 0;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_cursor_s64, "CTLV");
+
+bool ctlv_cursor_bool(const struct ctlv_cursor *cursor)
+{
+ const u8 *value = ctlv_message_cursor_fixed(cursor, BIT(CTLV_SCHEMA_BOOL), sizeof(u8));
+
+ return value ? *value : false;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_cursor_bool, "CTLV");
+
+const char *ctlv_cursor_string(const struct ctlv_cursor *cursor)
+{
+ const char *value;
+ u32 len;
+
+ value = ctlv_message_cursor_payload(cursor, BIT(CTLV_SCHEMA_STRING), &len);
+ if (!value || WARN_ON_ONCE(!len || value[len - 1]))
+ return NULL;
+ return value;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_cursor_string, "CTLV");
+
+const void *ctlv_cursor_binary(const struct ctlv_cursor *cursor, u32 *len)
+{
+ *len = 0;
+ return ctlv_message_cursor_payload(cursor, BIT(CTLV_SCHEMA_BINARY), len);
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_cursor_binary, "CTLV");
diff --git a/lib/ctlv/message.h b/lib/ctlv/message.h
new file mode 100644
index 000000000000..c177021be88a
--- /dev/null
+++ b/lib/ctlv/message.h
@@ -0,0 +1,91 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES
+ *
+ * Private message representation and record walking.
+ */
+#ifndef _CTLV_MESSAGE_H
+#define _CTLV_MESSAGE_H
+
+#include <linux/ctlv_family.h>
+#include <linux/types.h>
+
+#include <uapi/ctlv/ctlv.h>
+
+#define CTLV_MESSAGE_INPUT_INDEX_NONE U32_MAX
+
+/* A field's type remains in the generated schema, not in this request index. */
+struct ctlv_message_input_slot {
+ u32 first_offset;
+ u32 end_offset;
+ u32 count;
+ u32 excess_offset;
+ u32 first_record;
+ u32 last_record;
+};
+
+struct ctlv_message_input_scope {
+ const struct ctlv_schema_tree *tree;
+ u32 record;
+ u32 node;
+ u32 first_slot;
+ u32 slots_count;
+ bool optional;
+};
+
+struct ctlv_message_input_record {
+ u32 offset;
+ u32 first_scope;
+ u32 scopes_count;
+ u32 next;
+};
+
+struct ctlv_message_input_index {
+ struct ctlv_message_input_record *records;
+ struct ctlv_message_input_scope *scopes;
+ struct ctlv_message_input_slot *slots;
+ u32 records_count;
+ u32 records_capacity;
+ u32 scopes_count;
+ u32 scopes_capacity;
+ u32 slots_count;
+ u32 slots_capacity;
+};
+
+struct ctlv_msg {
+ struct ctlv_hdr *root;
+ size_t len;
+ const struct ctlv_schema_tree *tree;
+ const struct ctlv_family *family;
+ struct ctlv_op_ctx *ctx;
+ struct ctlv_message_input_index *index;
+ bool request;
+ int poison;
+};
+
+enum ctlv_message_step {
+ CTLV_MESSAGE_STEP_TLV = 0,
+ CTLV_MESSAGE_STEP_END = 1,
+ CTLV_MESSAGE_STEP_BAD = 2,
+};
+
+enum ctlv_message_step ctlv_message_next(const struct ctlv_hdr *record,
+ const struct ctlv_hdr **child);
+u32 ctlv_message_offset(const struct ctlv_msg *msg,
+ const struct ctlv_hdr *hdr);
+void ctlv_message_root(struct ctlv_cursor *cursor, struct ctlv_msg *msg);
+bool ctlv_message_input_rebind(struct ctlv_cursor *record,
+ const struct ctlv_schema_tree *tree, u32 node);
+
+static inline u32 ctlv_message_hdr_payload_len(const struct ctlv_hdr *hdr)
+{
+ return hdr->len - sizeof(*hdr);
+}
+
+static inline const void *
+ctlv_message_hdr_payload(const struct ctlv_hdr *hdr)
+{
+ return hdr + 1;
+}
+
+#endif /* _CTLV_MESSAGE_H */
--
2.56.0
^ permalink raw reply related [flat|nested] 24+ messages in thread
* [PATCH RFC v2 06/15] ctlv: Prepare blob descriptor validation
2026-11-13 15:54 [PATCH RFC v2 00/15] ctlv: Add a schema-driven TLV uAPI for character devices Jiri Pirko
` (4 preceding siblings ...)
2026-11-13 15:55 ` [PATCH RFC v2 05/15] ctlv: Prepare message input handling Jiri Pirko
@ 2026-11-13 15:55 ` Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 07/15] ctlv: Prepare schema validation Jiri Pirko
` (8 subsequent siblings)
14 siblings, 0 replies; 24+ messages in thread
From: Jiri Pirko @ 2026-11-13 15:55 UTC (permalink / raw)
To: linux-coco
Cc: linux-kernel, linux-api, x86, kvm, linux-doc, jgg, david.quigley,
lukas, ksinyuk, rodrigo.vivi, cedric.xing, sameo, drdeeglaze,
James.Bottomley, aik, jpiotrowski, qinkun, pgonda, jic23,
alistair.francis, ankita, suzuki.poulose, kas, rick.p.edgecombe,
elena.reshetova, dave.hansen, sathyanarayanan.kuppuswamy,
peter.fang, jarkko, zohar, yeoreum.yun, aneesh.kumar, dave.jiang,
nik.borisov, sergii.dmytruk
From: Jiri Pirko <jiri@nvidia.com>
Add parsing and VA checks for blob descriptors shared by schema
validation and runtime acquisition.
Keep CONFIG_CTLV unwired until dispatch completes the module.
Assisted-by: Cursor: GPT-5.6 Sol
Assisted-by: Cursor: Claude Opus 5.5
Signed-off-by: Jiri Pirko <jiri@nvidia.com>
---
RFC->RFCv2:
- returned EOVERFLOW for a blob range that wraps
- said TLV, type, record and field instead of attr
- said record instead of nest, the root being one
- numbered every enum member
- took the UAPI headers from include/uapi/ctlv/
- moved to lib/ctlv/
---
lib/ctlv/Makefile | 2 +-
lib/ctlv/blob_desc.c | 101 +++++++++++++++++++++++++++++++++++++++++++
lib/ctlv/blob_desc.h | 29 +++++++++++++
3 files changed, 131 insertions(+), 1 deletion(-)
create mode 100644 lib/ctlv/blob_desc.c
create mode 100644 lib/ctlv/blob_desc.h
diff --git a/lib/ctlv/Makefile b/lib/ctlv/Makefile
index 4f0fe41bc9e1..5edc0b4b973f 100644
--- a/lib/ctlv/Makefile
+++ b/lib/ctlv/Makefile
@@ -1,4 +1,4 @@
# SPDX-License-Identifier: GPL-2.0
obj-$(CONFIG_CTLV) += ctlv.o
-ctlv-y += message.o
+ctlv-y += blob_desc.o message.o
diff --git a/lib/ctlv/blob_desc.c b/lib/ctlv/blob_desc.c
new file mode 100644
index 000000000000..7c68ef5702fd
--- /dev/null
+++ b/lib/ctlv/blob_desc.c
@@ -0,0 +1,101 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES
+ *
+ * Blob descriptor parsing and validation.
+ */
+#include <linux/align.h>
+#include <linux/bits.h>
+#include <linux/errno.h>
+#include <linux/overflow.h>
+#include <linux/types.h>
+
+#include <uapi/ctlv/ctlv.h>
+#include <uapi/ctlv/ctlv_gen.h>
+
+#include "blob_desc.h"
+#include "message.h"
+
+static int ctlv_blob_member(u64 type)
+{
+ switch (type) {
+ case CTLV_T_BLOB_TYPE:
+ return CTLV_BLOB_MEMBER_TYPE;
+ case CTLV_T_BLOB_ADDRESS:
+ return CTLV_BLOB_MEMBER_ADDRESS;
+ case CTLV_T_BLOB_FD:
+ return CTLV_BLOB_MEMBER_FD;
+ case CTLV_T_BLOB_OFFSET:
+ return CTLV_BLOB_MEMBER_OFFSET;
+ case CTLV_T_BLOB_LENGTH:
+ return CTLV_BLOB_MEMBER_LENGTH;
+ default:
+ return -1;
+ }
+}
+
+u32 ctlv_blob_desc_read(const struct ctlv_hdr *blob, struct ctlv_blob_desc *desc,
+ const struct ctlv_hdr **bad)
+{
+ const struct ctlv_hdr *child = NULL;
+ enum ctlv_message_step step;
+
+ *desc = (struct ctlv_blob_desc){};
+ while ((step = ctlv_message_next(blob, &child)) == CTLV_MESSAGE_STEP_TLV) {
+ int member = ctlv_blob_member(child->type);
+
+ *bad = child;
+ if (child->flags & ~CTLV_HDR_F_RECORD)
+ return CTLV_VALIDATION_REASON_UNKNOWN_FLAG;
+ if (child->flags & CTLV_HDR_F_RECORD)
+ return CTLV_VALIDATION_REASON_MALFORMED_MESSAGE;
+ if (member < 0)
+ return CTLV_VALIDATION_REASON_UNKNOWN_TYPE;
+ if (ctlv_message_hdr_payload_len(child) != sizeof(u64))
+ return CTLV_VALIDATION_REASON_BAD_LENGTH;
+ if (desc->present & BIT(member))
+ return CTLV_VALIDATION_REASON_BAD_BLOB_DESCRIPTOR;
+ desc->present |= BIT(member);
+ desc->member[member] = *(const u64 *)ctlv_message_hdr_payload(child);
+ }
+ *bad = NULL;
+ if (step == CTLV_MESSAGE_STEP_BAD)
+ return CTLV_VALIDATION_REASON_MALFORMED_MESSAGE;
+ return 0;
+}
+
+static bool ctlv_blob_variant_ok(const struct ctlv_blob_desc *desc)
+{
+ u32 required = BIT(CTLV_BLOB_MEMBER_TYPE) | BIT(CTLV_BLOB_MEMBER_LENGTH);
+
+ switch (desc->member[CTLV_BLOB_MEMBER_TYPE]) {
+ case CTLV_BLOB_TYPE_VA:
+ required |= BIT(CTLV_BLOB_MEMBER_ADDRESS);
+ break;
+ default:
+ return false;
+ }
+ return desc->present == required;
+}
+
+/* Every failure is a bad descriptor; only a range that wraps is -EOVERFLOW. */
+int ctlv_blob_desc_check(const struct ctlv_blob_desc *desc, const struct ctlv_node_schema *node)
+{
+ u64 start, length, end;
+
+ if (!ctlv_blob_variant_ok(desc) ||
+ !(node->blob.backings & BIT(desc->member[CTLV_BLOB_MEMBER_TYPE])))
+ return -EINVAL;
+
+ length = desc->member[CTLV_BLOB_MEMBER_LENGTH];
+ if (!length || length > node->blob.max_len)
+ return -EINVAL;
+ start = desc->member[desc->present & BIT(CTLV_BLOB_MEMBER_ADDRESS) ?
+ CTLV_BLOB_MEMBER_ADDRESS :
+ CTLV_BLOB_MEMBER_OFFSET];
+ if (!IS_ALIGNED(start, node->blob.alignment))
+ return -EINVAL;
+ if (check_add_overflow(start, length, &end))
+ return -EOVERFLOW;
+ return 0;
+}
diff --git a/lib/ctlv/blob_desc.h b/lib/ctlv/blob_desc.h
new file mode 100644
index 000000000000..e88693cbee4b
--- /dev/null
+++ b/lib/ctlv/blob_desc.h
@@ -0,0 +1,29 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef _CTLV_BLOB_DESC_H
+#define _CTLV_BLOB_DESC_H
+
+#include <linux/ctlv_family.h>
+#include <linux/types.h>
+
+#include <uapi/ctlv/ctlv.h>
+
+/* The members of a descriptor, in the order the root family declares them. */
+enum {
+ CTLV_BLOB_MEMBER_TYPE = 0,
+ CTLV_BLOB_MEMBER_ADDRESS = 1,
+ CTLV_BLOB_MEMBER_FD = 2,
+ CTLV_BLOB_MEMBER_OFFSET = 3,
+ CTLV_BLOB_MEMBER_LENGTH = 4,
+ CTLV_BLOB_MEMBERS = 5,
+};
+
+struct ctlv_blob_desc {
+ u64 member[CTLV_BLOB_MEMBERS];
+ u32 present;
+};
+
+u32 ctlv_blob_desc_read(const struct ctlv_hdr *blob, struct ctlv_blob_desc *desc,
+ const struct ctlv_hdr **bad);
+int ctlv_blob_desc_check(const struct ctlv_blob_desc *desc, const struct ctlv_node_schema *node);
+
+#endif /* _CTLV_BLOB_DESC_H */
--
2.56.0
^ permalink raw reply related [flat|nested] 24+ messages in thread
* [PATCH RFC v2 07/15] ctlv: Prepare schema validation
2026-11-13 15:54 [PATCH RFC v2 00/15] ctlv: Add a schema-driven TLV uAPI for character devices Jiri Pirko
` (5 preceding siblings ...)
2026-11-13 15:55 ` [PATCH RFC v2 06/15] ctlv: Prepare blob descriptor validation Jiri Pirko
@ 2026-11-13 15:55 ` Jiri Pirko
2026-11-13 16:22 ` sashiko-bot
2026-11-13 15:55 ` [PATCH RFC v2 08/15] ctlv: Prepare message output handling Jiri Pirko
` (7 subsequent siblings)
14 siblings, 1 reply; 24+ messages in thread
From: Jiri Pirko @ 2026-11-13 15:55 UTC (permalink / raw)
To: linux-coco
Cc: linux-kernel, linux-api, x86, kvm, linux-doc, jgg, david.quigley,
lukas, ksinyuk, rodrigo.vivi, cedric.xing, sameo, drdeeglaze,
James.Bottomley, aik, jpiotrowski, qinkun, pgonda, jic23,
alistair.francis, ankita, suzuki.poulose, kas, rick.p.edgecombe,
elena.reshetova, dave.hansen, sathyanarayanan.kuppuswamy,
peter.fang, jarkko, zohar, yeoreum.yun, aneesh.kumar, dave.jiang,
nik.borisov, sergii.dmytruk
From: Jiri Pirko <jiri@nvidia.com>
Add generated-schema walking, message validation and request indexing.
Use the descriptor layer to check blob fields.
Keep CONFIG_CTLV unwired until dispatch completes the module.
Assisted-by: Cursor: GPT-5.6 Sol
Assisted-by: Cursor: Claude Opus 5.5
Signed-off-by: Jiri Pirko <jiri@nvidia.com>
---
RFC->RFCv2:
- refused extension points in a re-entering query reply
- returned EOVERFLOW for a blob range that wraps
- simplified growing the request index
- declared the request index free in schema.h
- dropped the event record length
- renamed stubs to extension points
- made a check no error path depends on a CTLV_ASSERT()
- said TLV, type, record and field instead of attr
- said record instead of nest, the root being one
- said node or field instead of placement
- separated the steps of the longer functions
- took the UAPI headers from include/uapi/ctlv/
- took the values the device's family chain gives an enum extension
point
- moved to lib/ctlv/
- said message instead of wire
---
lib/ctlv/Makefile | 2 +-
lib/ctlv/schema.c | 1092 +++++++++++++++++++++++++++++++++++++++++++++
lib/ctlv/schema.h | 54 +++
3 files changed, 1147 insertions(+), 1 deletion(-)
create mode 100644 lib/ctlv/schema.c
create mode 100644 lib/ctlv/schema.h
diff --git a/lib/ctlv/Makefile b/lib/ctlv/Makefile
index 5edc0b4b973f..3fd656835b8a 100644
--- a/lib/ctlv/Makefile
+++ b/lib/ctlv/Makefile
@@ -1,4 +1,4 @@
# SPDX-License-Identifier: GPL-2.0
obj-$(CONFIG_CTLV) += ctlv.o
-ctlv-y += blob_desc.o message.o
+ctlv-y += blob_desc.o message.o schema.o
diff --git a/lib/ctlv/schema.c b/lib/ctlv/schema.c
new file mode 100644
index 000000000000..8ff3c7c5b5fe
--- /dev/null
+++ b/lib/ctlv/schema.c
@@ -0,0 +1,1092 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES
+ *
+ * Generated metadata remains owner-local; effective schemas walk the chain.
+ */
+#include <linux/align.h>
+#include <linux/bits.h>
+#include <linux/ctlv.h>
+#include <linux/err.h>
+#include <linux/errno.h>
+#include <linux/limits.h>
+#include <linux/minmax.h>
+#include <linux/overflow.h>
+#include <linux/slab.h>
+#include <linux/string.h>
+#include <linux/types.h>
+
+#include <uapi/ctlv/ctlv.h>
+#include <uapi/ctlv/ctlv_gen.h>
+
+#include "blob_desc.h"
+#include "internal.h"
+#include "message.h"
+#include "schema.h"
+
+/* Every 64-bit payload, and every blob descriptor member, is this long. */
+#define CTLV_SCHEMA_SCALAR_TLV_LEN (sizeof(struct ctlv_hdr) + sizeof(u64))
+
+/*
+ * Where composing a tree's maximum length gives up, which keeps the check on a
+ * composed length from being passed by a total that wrapped.
+ */
+#define CTLV_SCHEMA_MAX_COMPOSED_LEN CTLV_MAX_INLINE_MESSAGE_LEN
+
+const struct ctlv_family *ctlv_family_ancestor(const struct ctlv_family *family, u32 up)
+{
+ while (up-- && family)
+ family = family->parent;
+ return family;
+}
+
+u32 ctlv_family_levels(const struct ctlv_family *family)
+{
+ const struct ctlv_family *ancestor;
+ u32 levels = 0;
+
+ for (ancestor = family; ancestor; ancestor = ancestor->parent)
+ levels++;
+ return levels;
+}
+
+void ctlv_schema_cursor_init(struct ctlv_schema_cursor *cursor, const struct ctlv_family *family)
+{
+ cursor->target = family;
+ cursor->level = ctlv_family_levels(family);
+ cursor->family = NULL;
+ cursor->index = 0;
+}
+
+const struct ctlv_op_schema *ctlv_schema_next_op(struct ctlv_schema_cursor *cursor,
+ const struct ctlv_family **family)
+{
+ while (cursor->level) {
+ const struct ctlv_family_schema *schema;
+
+ if (!cursor->family)
+ cursor->family = ctlv_family_ancestor(cursor->target, cursor->level - 1);
+ schema = cursor->family->schema;
+ if (cursor->index < schema->ops_count) {
+ *family = cursor->family;
+ return &schema->ops[cursor->index++];
+ }
+ cursor->level--;
+ cursor->family = NULL;
+ cursor->index = 0;
+ }
+ return NULL;
+}
+
+bool ctlv_schema_member(const struct ctlv_family *family, const struct ctlv_schema_tree *tree,
+ u32 node, u64 type, struct ctlv_schema_pos *member)
+{
+ const struct ctlv_node_schema *record;
+ u32 index;
+
+ if (!tree->node_schemas || node >= tree->node_schemas_count)
+ return false;
+ record = &tree->node_schemas[node];
+
+ for (index = 0; index < record->children_count; index++) {
+ u32 child = record->first_child + index;
+
+ if (tree->node_schemas[child].type == type) {
+ *member = (struct ctlv_schema_pos){
+ .tree = tree,
+ .node = child,
+ };
+ return true;
+ }
+ }
+ for (index = 0; index < tree->ext_schemas_count; index++) {
+ const struct ctlv_ext_schema *ext_schema = &tree->ext_schemas[index];
+ const struct ctlv_ext_fill_schema *fill_schema;
+
+ if (ext_schema->node != node)
+ continue;
+ fill_schema = ctlv_schema_fill(family, ext_schema->family_id, ext_schema->ext_id);
+ /* The members are children of the fill schema's root. */
+ if (fill_schema &&
+ ctlv_schema_member(family, &fill_schema->members, 0, type, member))
+ return true;
+ }
+ return false;
+}
+
+const struct ctlv_ext_fill_schema *ctlv_schema_fill(const struct ctlv_family *family,
+ u16 family_id, u32 ext_id)
+{
+ const struct ctlv_family *owner;
+ u32 index;
+
+ for (owner = family; owner; owner = owner->parent) {
+ const struct ctlv_family_schema *schema = owner->schema;
+
+ for (index = 0; index < schema->ext_fill_schemas_count; index++)
+ if (schema->ext_fill_schemas[index].family_id == family_id &&
+ schema->ext_fill_schemas[index].ext_id == ext_id)
+ return &schema->ext_fill_schemas[index];
+ }
+ return NULL;
+}
+
+/**
+ * ctlv_enum_ext_max - The highest value a family gives an enum extension point.
+ * @family: the family of a device
+ * @family_id: the family declaring the point
+ * @ext_id: the point's <FAMILY>_ENUM_EXT_<NAME>
+ *
+ * Return: the highest value the chain of @family gives the point, the lowest
+ * being 1, or 0 where it gives none.
+ */
+u32 ctlv_enum_ext_max(const struct ctlv_family *family, u16 family_id, u32 ext_id)
+{
+ const struct ctlv_family *owner;
+ u32 index;
+
+ for (owner = family; owner; owner = owner->parent) {
+ const struct ctlv_family_schema *schema = owner->schema;
+
+ for (index = 0; index < schema->enum_fill_schemas_count; index++)
+ if (schema->enum_fill_schemas[index].family_id == family_id &&
+ schema->enum_fill_schemas[index].ext_id == ext_id)
+ return schema->enum_fill_schemas[index].max;
+ }
+ return 0;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_enum_ext_max, "CTLV");
+
+/* The highest value an enum node takes on a device of family. */
+u64 ctlv_schema_enum_max(const struct ctlv_family *family, const struct ctlv_node_schema *node)
+{
+ if (!node->is_enum_ext)
+ return node->range.max;
+ return ctlv_enum_ext_max(family, node->enum_ext.family_id, node->enum_ext.ext_id);
+}
+
+/*
+ * The same arithmetic the generator did, over the tree it emitted plus this
+ * family's fill schemas, since a fill makes an extension point longer.
+ */
+
+struct ctlv_schema_length_walk {
+ const struct ctlv_family *family;
+ bool bad;
+};
+
+static u64 ctlv_schema_children_max_len(struct ctlv_schema_length_walk *walk,
+ const struct ctlv_schema_tree *tree, u32 index, u32 depth);
+
+static u64 ctlv_schema_payload_max_len(struct ctlv_schema_length_walk *walk,
+ const struct ctlv_schema_tree *tree, u32 index, u32 depth)
+{
+ const struct ctlv_node_schema *node = &tree->node_schemas[index];
+
+ switch (node->kind) {
+ case CTLV_SCHEMA_UINT:
+ case CTLV_SCHEMA_SINT:
+ case CTLV_SCHEMA_ENUM:
+ case CTLV_SCHEMA_FLAGS:
+ return sizeof(u64);
+ case CTLV_SCHEMA_BOOL:
+ return 1;
+ case CTLV_SCHEMA_STRING:
+ /*
+ * One terminating NUL, which the declared maximum excludes.
+ * Widened first so that a maximum at U32_MAX cannot wrap.
+ */
+ return (u64)node->length.max_len + 1;
+ case CTLV_SCHEMA_BINARY:
+ return node->length.max_len;
+ case CTLV_SCHEMA_BLOB:
+ /*
+ * Type, length and an address; anything but a bare VA is one
+ * member wider, an fd with an optional offset.
+ */
+ return (node->blob.backings == BIT(CTLV_BLOB_TYPE_VA) ? 3 : 4) *
+ ALIGN(CTLV_SCHEMA_SCALAR_TLV_LEN, CTLV_ALIGNTO);
+ default:
+ return ctlv_schema_children_max_len(walk, tree, index, depth);
+ }
+}
+
+static u64 ctlv_schema_tlv_max_len(struct ctlv_schema_length_walk *walk,
+ const struct ctlv_schema_tree *tree, u32 index, u32 depth)
+{
+ return sizeof(struct ctlv_hdr) + ctlv_schema_payload_max_len(walk, tree, index, depth);
+}
+
+static u64 ctlv_schema_children_max_len(struct ctlv_schema_length_walk *walk,
+ const struct ctlv_schema_tree *tree, u32 index, u32 depth)
+{
+ const struct ctlv_node_schema *node = &tree->node_schemas[index];
+ u64 total = 0;
+ u32 i;
+
+ if (depth > CTLV_MAX_DECLARED_RECORD_DEPTH) {
+ walk->bad = true;
+ return 0;
+ }
+
+ for (i = 0; i < node->children_count; i++) {
+ u32 child = node->first_child + i;
+ u64 each, all;
+
+ /*
+ * Neither an unlimited node nor one that re-enters has a
+ * maximum, and only a query reply may hold either.
+ */
+ if (tree->node_schemas[child].reenters ||
+ !tree->node_schemas[child].max_occurrences) {
+ walk->bad = true;
+ return 0;
+ }
+
+ each = ALIGN(ctlv_schema_tlv_max_len(walk, tree, child, depth + 1), CTLV_ALIGNTO);
+ if (check_mul_overflow((u64)tree->node_schemas[child].max_occurrences, each,
+ &all) ||
+ check_add_overflow(total, all, &total) ||
+ total > CTLV_SCHEMA_MAX_COMPOSED_LEN) {
+ walk->bad = true;
+ return 0;
+ }
+ }
+
+ for (i = 0; i < tree->ext_schemas_count; i++) {
+ const struct ctlv_ext_schema *ext_schema = &tree->ext_schemas[i];
+ const struct ctlv_ext_fill_schema *fill_schema;
+
+ if (ext_schema->node != index)
+ continue;
+ fill_schema =
+ ctlv_schema_fill(walk->family, ext_schema->family_id, ext_schema->ext_id);
+ if (!fill_schema)
+ continue;
+ if (check_add_overflow(total,
+ ctlv_schema_children_max_len(walk, &fill_schema->members, 0,
+ depth),
+ &total) ||
+ total > CTLV_SCHEMA_MAX_COMPOSED_LEN) {
+ walk->bad = true;
+ return 0;
+ }
+ }
+ return total;
+}
+
+/* Buffer length that the largest indivisible leaf and its records need. */
+static u64 ctlv_schema_min_len(struct ctlv_schema_length_walk *walk,
+ const struct ctlv_schema_tree *tree, u32 index, u32 above, u32 depth)
+{
+ const struct ctlv_node_schema *node = &tree->node_schemas[index];
+ bool has_children = false;
+ u64 deepest = 0;
+ u32 i;
+
+ if (depth > CTLV_MAX_DECLARED_RECORD_DEPTH) {
+ walk->bad = true;
+ return 0;
+ }
+
+ for (i = 0; i < node->children_count; i++) {
+ u32 child = node->first_child + i;
+
+ has_children = true;
+ deepest = max(deepest,
+ ctlv_schema_min_len(walk, tree, child,
+ above + sizeof(struct ctlv_hdr), depth + 1));
+ }
+
+ for (i = 0; i < tree->ext_schemas_count; i++) {
+ const struct ctlv_ext_schema *ext_schema = &tree->ext_schemas[i];
+ const struct ctlv_ext_fill_schema *fill_schema;
+ const struct ctlv_node_schema *root;
+ u32 member;
+
+ if (ext_schema->node != index)
+ continue;
+ fill_schema =
+ ctlv_schema_fill(walk->family, ext_schema->family_id, ext_schema->ext_id);
+ if (!fill_schema)
+ continue;
+ root = &fill_schema->members.node_schemas[0];
+ for (member = 0; member < root->children_count; member++) {
+ has_children = true;
+ deepest = max(deepest, ctlv_schema_min_len(walk, &fill_schema->members,
+ root->first_child + member,
+ above + sizeof(struct ctlv_hdr),
+ depth + 1));
+ }
+ }
+
+ if (has_children)
+ return deepest;
+ /* An empty root is a message of its own header alone. */
+ if (!index)
+ return above + sizeof(struct ctlv_hdr);
+ return above + ALIGN(ctlv_schema_tlv_max_len(walk, tree, index, depth), CTLV_ALIGNTO);
+}
+
+static u64 ctlv_schema_root_max_len(struct ctlv_schema_length_walk *walk,
+ const struct ctlv_schema_tree *tree)
+{
+ return sizeof(struct ctlv_hdr) + ctlv_schema_children_max_len(walk, tree, 0, 0);
+}
+
+/* Size one deepest legal leaf and every record above it. */
+static u64 ctlv_schema_reentering_min_len(struct ctlv_schema_length_walk *walk,
+ const struct ctlv_schema_tree *tree)
+{
+ u64 leaf = 0;
+ u32 index;
+
+ for (index = 1; index < tree->node_schemas_count; index++) {
+ if (tree->node_schemas[index].kind == CTLV_SCHEMA_RECORD)
+ continue;
+ leaf = max(leaf,
+ ALIGN(ctlv_schema_tlv_max_len(walk, tree, index, 0), CTLV_ALIGNTO));
+ }
+ return (CTLV_MAX_RECORD_DEPTH + 1) * sizeof(struct ctlv_hdr) + leaf;
+}
+
+static bool ctlv_schema_tree_reenters(const struct ctlv_schema_tree *tree)
+{
+ u32 index;
+
+ for (index = 0; index < tree->node_schemas_count; index++)
+ if (tree->node_schemas[index].reenters)
+ return true;
+ return false;
+}
+
+int ctlv_schema_op_sizes(const struct ctlv_family *family, const struct ctlv_op_schema *op,
+ struct ctlv_op_sizes *sizes)
+{
+ struct ctlv_schema_length_walk walk = { .family = family };
+ u64 request = 0, reply = 0;
+
+ if (op->request.node_schemas)
+ request = ctlv_schema_root_max_len(&walk, &op->request);
+
+ switch (op->type) {
+ case CTLV_OP_TYPE_ACTION:
+ if (op->reply.node_schemas)
+ reply = ctlv_schema_root_max_len(&walk, &op->reply);
+ break;
+ case CTLV_OP_TYPE_QUERY:
+ if (ctlv_schema_tree_reenters(&op->reply)) {
+ /* This size ignores extension points, which ctlvgen rejects here. */
+ if (WARN_ON_ONCE(op->reply.ext_schemas_count))
+ return -EINVAL;
+ reply = ctlv_schema_reentering_min_len(&walk, &op->reply);
+ } else {
+ reply = ctlv_schema_min_len(&walk, &op->reply, 0, 0, 0);
+ }
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ if (walk.bad || request > CTLV_MAX_INLINE_MESSAGE_LEN ||
+ reply > CTLV_MAX_INLINE_MESSAGE_LEN)
+ return -EINVAL;
+
+ *sizes = (struct ctlv_op_sizes){ .request_max_len = request };
+ switch (op->type) {
+ case CTLV_OP_TYPE_ACTION:
+ sizes->action_reply_max_len = reply;
+ break;
+ case CTLV_OP_TYPE_QUERY:
+ sizes->query_reply_min_len = reply;
+ /*
+ * What the specification asked for, unless a filled extension
+ * point made one fragment longer than that.
+ */
+ sizes->query_reply_suggested_len =
+ max_t(u32, op->sizes.query_reply_suggested_len, reply);
+ break;
+ }
+ return 0;
+}
+
+/*
+ * Only a completely valid request reaches family code; nothing is repaired or
+ * normalized. The first failure is the one recorded, and the walk stops there.
+ */
+
+struct ctlv_schema_validation {
+ struct ctlv_msg *msg;
+ struct ctlv_error *error;
+};
+
+void ctlv_message_index_free(struct ctlv_msg *msg)
+{
+ struct ctlv_message_input_index *index;
+
+ if (!msg || !msg->index)
+ return;
+ index = msg->index;
+ kvfree(index->slots);
+ kvfree(index->scopes);
+ kvfree(index->records);
+ kfree(index);
+ msg->index = NULL;
+}
+
+/* kvrealloc() does not zero, so callers initialize every entry they add. */
+static void *ctlv_schema_index_grow(void *items, u32 *capacity, u32 needed, size_t item_size)
+{
+ u32 grown = max3(needed, *capacity * 2, 16U);
+ void *new;
+
+ if (needed <= *capacity)
+ return items;
+ new = kvrealloc(items, array_size(grown, item_size), GFP_KERNEL_ACCOUNT);
+ if (!new)
+ return ERR_PTR(-ENOMEM);
+ *capacity = grown;
+ return new;
+}
+
+static int ctlv_schema_index_slots_add(struct ctlv_message_input_index *index, u32 count,
+ u32 *first)
+{
+ struct ctlv_message_input_slot *slots;
+ u32 needed;
+ u32 i;
+
+ if (check_add_overflow(index->slots_count, count, &needed))
+ return -EOVERFLOW;
+ slots = ctlv_schema_index_grow(index->slots, &index->slots_capacity, needed,
+ sizeof(*slots));
+ if (IS_ERR(slots))
+ return PTR_ERR(slots);
+ index->slots = slots;
+ *first = index->slots_count;
+ for (i = index->slots_count; i < needed; i++)
+ index->slots[i] = (struct ctlv_message_input_slot){
+ .excess_offset = CTLV_MESSAGE_INPUT_INDEX_NONE,
+ .first_record = CTLV_MESSAGE_INPUT_INDEX_NONE,
+ .last_record = CTLV_MESSAGE_INPUT_INDEX_NONE,
+ };
+ index->slots_count = needed;
+ return 0;
+}
+
+static int ctlv_schema_index_scope_add(struct ctlv_schema_validation *v, u32 record_index,
+ const struct ctlv_schema_tree *tree, u32 node, bool optional)
+{
+ struct ctlv_message_input_index *index = v->msg->index;
+ const struct ctlv_node_schema *parent = &tree->node_schemas[node];
+ struct ctlv_message_input_scope *scopes;
+ u32 needed;
+ u32 first_slot;
+ u32 scope_index;
+ u32 i;
+ int ret;
+
+ if (check_add_overflow(index->scopes_count, 1U, &needed))
+ return -EOVERFLOW;
+ scopes = ctlv_schema_index_grow(index->scopes, &index->scopes_capacity, needed,
+ sizeof(*scopes));
+ if (IS_ERR(scopes))
+ return PTR_ERR(scopes);
+ index->scopes = scopes;
+
+ ret = ctlv_schema_index_slots_add(index, parent->children_count, &first_slot);
+ if (ret)
+ return ret;
+
+ scope_index = index->scopes_count++;
+ index->scopes[scope_index] = (struct ctlv_message_input_scope){
+ .tree = tree,
+ .record = record_index,
+ .node = node,
+ .first_slot = first_slot,
+ .slots_count = parent->children_count,
+ .optional = optional,
+ };
+ index->records[record_index].scopes_count++;
+
+ for (i = 0; i < tree->ext_schemas_count; i++) {
+ const struct ctlv_ext_schema *ext_schema = &tree->ext_schemas[i];
+ const struct ctlv_ext_fill_schema *fill_schema;
+
+ if (ext_schema->node != node)
+ continue;
+ fill_schema = ctlv_schema_fill(v->msg->family, ext_schema->family_id,
+ ext_schema->ext_id);
+ if (!fill_schema)
+ continue;
+ ret = ctlv_schema_index_scope_add(v, record_index, &fill_schema->members, 0,
+ optional || ext_schema->optional);
+ if (ret)
+ return ret;
+ }
+ return 0;
+}
+
+static int ctlv_schema_index_record_add(struct ctlv_schema_validation *v,
+ const struct ctlv_schema_tree *tree, u32 node,
+ const struct ctlv_hdr *record_hdr, u32 *record_index)
+{
+ struct ctlv_message_input_index *index = v->msg->index;
+ struct ctlv_message_input_record *records;
+ u32 needed;
+ u32 added;
+ int ret;
+
+ if (check_add_overflow(index->records_count, 1U, &needed))
+ return -EOVERFLOW;
+ records = ctlv_schema_index_grow(index->records, &index->records_capacity, needed,
+ sizeof(*records));
+ if (IS_ERR(records))
+ return PTR_ERR(records);
+ index->records = records;
+ added = index->records_count++;
+ index->records[added] = (struct ctlv_message_input_record){
+ .offset = ctlv_message_offset(v->msg, record_hdr),
+ .first_scope = index->scopes_count,
+ .next = CTLV_MESSAGE_INPUT_INDEX_NONE,
+ };
+ ret = ctlv_schema_index_scope_add(v, added, tree, node, false);
+ if (ret)
+ return ret;
+ *record_index = added;
+ return 0;
+}
+
+/* A request is never a fragment: only a reply of a query is ever incomplete. */
+#define CTLV_SCHEMA_HDR_F_REQUEST (CTLV_HDR_F_RECORD | CTLV_HDR_F_IGNORE_IF_UNKNOWN)
+#define CTLV_SCHEMA_HDR_F_REPLY CTLV_HDR_F_RECORD
+
+/*
+ * Record the first failure and refuse the message. @hdr is NULL when what is
+ * wrong with the record is something it does not carry.
+ */
+static int ctlv_schema_bad(struct ctlv_schema_validation *v, u32 reason,
+ const struct ctlv_hdr *record, const struct ctlv_hdr *hdr)
+{
+ *v->error = (struct ctlv_error){
+ .reason = reason,
+ .bad_tlv_offset = ctlv_message_offset(v->msg, hdr ?: record),
+ .record_offset = ctlv_message_offset(v->msg, record),
+ };
+ return -EINVAL;
+}
+
+/* The one failure that names a field the message does not contain. */
+static int ctlv_schema_bad_missing(struct ctlv_schema_validation *v, const struct ctlv_hdr *record,
+ u64 type)
+{
+ ctlv_schema_bad(v, CTLV_VALIDATION_REASON_MISSING_FIELD, record, NULL);
+ v->error->missing_type = type;
+ return -EINVAL;
+}
+
+static struct ctlv_message_input_slot *ctlv_schema_index_slot(struct ctlv_schema_validation *v,
+ u32 record_index,
+ const struct ctlv_schema_pos *pos)
+{
+ struct ctlv_message_input_index *index = v->msg->index;
+ const struct ctlv_message_input_record *record;
+ u32 i;
+
+ if (WARN_ON_ONCE(record_index >= index->records_count))
+ return NULL;
+ record = &index->records[record_index];
+ for (i = 0; i < record->scopes_count; i++) {
+ const struct ctlv_message_input_scope *scope =
+ &index->scopes[record->first_scope + i];
+ const struct ctlv_node_schema *parent;
+ u32 slot;
+
+ if (scope->tree != pos->tree)
+ continue;
+ parent = &scope->tree->node_schemas[scope->node];
+ if (pos->node < parent->first_child ||
+ pos->node >= parent->first_child + scope->slots_count)
+ continue;
+ slot = pos->node - parent->first_child;
+ return &index->slots[scope->first_slot + slot];
+ }
+ CTLV_ASSERT(0);
+ return NULL;
+}
+
+static int ctlv_schema_index_tlv_add(struct ctlv_schema_validation *v, u32 record_index,
+ const struct ctlv_schema_pos *pos, const struct ctlv_hdr *hdr,
+ bool *seen)
+{
+ struct ctlv_message_input_slot *slot = ctlv_schema_index_slot(v, record_index, pos);
+ const struct ctlv_node_schema *schema = &pos->tree->node_schemas[pos->node];
+ u32 offset = ctlv_message_offset(v->msg, hdr);
+
+ if (!slot)
+ return -EINVAL;
+ *seen = slot->count;
+ if (!slot->count)
+ slot->first_offset = offset;
+ slot->count++;
+ if (schema->max_occurrences && slot->count > schema->max_occurrences &&
+ slot->excess_offset == CTLV_MESSAGE_INPUT_INDEX_NONE)
+ slot->excess_offset = offset;
+ slot->end_offset = offset + ALIGN((size_t)hdr->len, CTLV_ALIGNTO);
+ return 0;
+}
+
+static void ctlv_schema_index_record_link(struct ctlv_message_input_index *index,
+ struct ctlv_message_input_slot *slot, u32 record_index)
+{
+ if (slot->first_record == CTLV_MESSAGE_INPUT_INDEX_NONE)
+ slot->first_record = record_index;
+ else
+ index->records[slot->last_record].next = record_index;
+ slot->last_record = record_index;
+}
+
+static int ctlv_schema_index_required(struct ctlv_schema_validation *v, u32 record_index,
+ const struct ctlv_hdr *record_hdr)
+{
+ const struct ctlv_message_input_index *index = v->msg->index;
+ const struct ctlv_message_input_record *record = &index->records[record_index];
+ u32 i;
+
+ for (i = 0; i < record->scopes_count; i++) {
+ const struct ctlv_message_input_scope *scope =
+ &index->scopes[record->first_scope + i];
+ const struct ctlv_node_schema *parent = &scope->tree->node_schemas[scope->node];
+ u32 slot;
+
+ for (slot = 0; slot < scope->slots_count; slot++) {
+ const struct ctlv_message_input_slot *indexed;
+ const struct ctlv_node_schema *child =
+ &scope->tree->node_schemas[parent->first_child + slot];
+ const struct ctlv_hdr *excess;
+
+ indexed = &index->slots[scope->first_slot + slot];
+ if (!indexed->count && slot < parent->required_children_count &&
+ !scope->optional)
+ return ctlv_schema_bad_missing(v, record_hdr, child->type);
+ if (!child->max_occurrences || indexed->count <= child->max_occurrences)
+ continue;
+ if (WARN_ON_ONCE(indexed->excess_offset == CTLV_MESSAGE_INPUT_INDEX_NONE))
+ return -EINVAL;
+ excess = (const void *)((const u8 *)v->msg->root + indexed->excess_offset);
+ return ctlv_schema_bad(v, CTLV_VALIDATION_REASON_TOO_MANY_OCCURRENCES,
+ record_hdr, excess);
+ }
+ }
+ return 0;
+}
+
+/* The bits a TLV of this message may carry, whichever way it travelled. */
+static u32 ctlv_schema_flags_allowed(const struct ctlv_schema_validation *v)
+{
+ return v->msg->request ? CTLV_SCHEMA_HDR_F_REQUEST : CTLV_SCHEMA_HDR_F_REPLY;
+}
+
+static u64 ctlv_schema_scalar_value(const struct ctlv_hdr *hdr)
+{
+ /* Aligned by the message format: the root began an allocation. */
+ return *(const u64 *)ctlv_message_hdr_payload(hdr);
+}
+
+bool ctlv_schema_value_ok(const struct ctlv_family *family,
+ const struct ctlv_node_schema *node, u64 value)
+{
+ switch (node->kind) {
+ case CTLV_SCHEMA_SINT:
+ return (s64)value >= (s64)node->range.min && (s64)value <= (s64)node->range.max;
+ case CTLV_SCHEMA_FLAGS:
+ /* A bit no family declared is one nothing can be told about. */
+ return !(value & ~node->flags_mask);
+ case CTLV_SCHEMA_BOOL:
+ return value <= 1;
+ case CTLV_SCHEMA_ENUM:
+ /*
+ * An enum's values are numbered in declaration order and never
+ * skipped, so its range bounds it like an integer.
+ */
+ if (node->is_enum_ext)
+ return value >= 1 && value <= ctlv_schema_enum_max(family, node);
+ return value >= node->range.min && value <= node->range.max;
+ default:
+ return value >= node->range.min && value <= node->range.max;
+ }
+}
+
+bool ctlv_schema_length_ok(const struct ctlv_node_schema *node, u32 len)
+{
+ return len >= node->length.min_len && len <= node->length.max_len;
+}
+
+static int ctlv_schema_check_integer(struct ctlv_schema_validation *v,
+ const struct ctlv_node_schema *node,
+ const struct ctlv_hdr *record, const struct ctlv_hdr *hdr)
+{
+ u64 value;
+
+ if (ctlv_message_hdr_payload_len(hdr) != sizeof(value))
+ return ctlv_schema_bad(v, CTLV_VALIDATION_REASON_BAD_LENGTH, record, hdr);
+ value = ctlv_schema_scalar_value(hdr);
+ if (ctlv_schema_value_ok(v->msg->family, node, value))
+ return 0;
+ if (node->kind == CTLV_SCHEMA_FLAGS)
+ return ctlv_schema_bad(v, CTLV_VALIDATION_REASON_UNKNOWN_FLAG, record, hdr);
+ return ctlv_schema_bad(v, CTLV_VALIDATION_REASON_VALUE_OUT_OF_RANGE, record, hdr);
+}
+
+static int ctlv_schema_check_bool(struct ctlv_schema_validation *v,
+ const struct ctlv_node_schema *node,
+ const struct ctlv_hdr *record,
+ const struct ctlv_hdr *hdr)
+{
+ const u8 *value = ctlv_message_hdr_payload(hdr);
+
+ if (ctlv_message_hdr_payload_len(hdr) != sizeof(*value))
+ return ctlv_schema_bad(v, CTLV_VALIDATION_REASON_BAD_LENGTH, record, hdr);
+ if (!ctlv_schema_value_ok(v->msg->family, node, *value))
+ return ctlv_schema_bad(v, CTLV_VALIDATION_REASON_VALUE_OUT_OF_RANGE, record, hdr);
+ return 0;
+}
+
+static int ctlv_schema_check_string(struct ctlv_schema_validation *v,
+ const struct ctlv_node_schema *node,
+ const struct ctlv_hdr *record, const struct ctlv_hdr *hdr)
+{
+ u32 len = ctlv_message_hdr_payload_len(hdr);
+ const char *value = ctlv_message_hdr_payload(hdr);
+
+ /*
+ * One terminating NUL and no other, so a family may hand the payload
+ * to anything that takes a C string.
+ */
+ if (!len || value[len - 1] || strnlen(value, len) != len - 1)
+ return ctlv_schema_bad(v, CTLV_VALIDATION_REASON_BAD_STRING, record, hdr);
+ if (!ctlv_schema_length_ok(node, len - 1))
+ return ctlv_schema_bad(v, CTLV_VALIDATION_REASON_BAD_LENGTH, record, hdr);
+ return 0;
+}
+
+static int ctlv_schema_check_binary(struct ctlv_schema_validation *v,
+ const struct ctlv_node_schema *node,
+ const struct ctlv_hdr *record, const struct ctlv_hdr *hdr)
+{
+ if (!ctlv_schema_length_ok(node, ctlv_message_hdr_payload_len(hdr)))
+ return ctlv_schema_bad(v, CTLV_VALIDATION_REASON_BAD_LENGTH, record, hdr);
+ return 0;
+}
+
+/*
+ * The one shape the tree cannot describe: a record of core-owned fields
+ * whose members depend on its type. Nothing here acquires anything.
+ */
+static int ctlv_schema_check_blob(struct ctlv_schema_validation *v,
+ const struct ctlv_node_schema *node, const struct ctlv_hdr *blob)
+{
+ const struct ctlv_hdr *bad = NULL;
+ struct ctlv_blob_desc desc;
+ u32 reason;
+ int err;
+
+ reason = ctlv_blob_desc_read(blob, &desc, &bad);
+ if (reason)
+ return ctlv_schema_bad(v, reason, blob, bad);
+ err = ctlv_blob_desc_check(&desc, node);
+ if (err)
+ ctlv_schema_bad(v, CTLV_VALIDATION_REASON_BAD_BLOB_DESCRIPTOR, blob, NULL);
+ return err;
+}
+
+static int ctlv_schema_check_record(struct ctlv_schema_validation *v,
+ const struct ctlv_schema_tree *tree, u32 node,
+ const struct ctlv_hdr *record, u32 depth);
+static int ctlv_schema_check_indexed_record(struct ctlv_schema_validation *v,
+ const struct ctlv_schema_tree *tree, u32 node,
+ const struct ctlv_hdr *record, u32 depth,
+ u32 *record_index);
+
+static int ctlv_schema_check_tlv(struct ctlv_schema_validation *v,
+ const struct ctlv_schema_pos *pos, const struct ctlv_hdr *record,
+ const struct ctlv_hdr *hdr, u32 depth, u32 *record_index)
+{
+ const struct ctlv_node_schema *node = &pos->tree->node_schemas[pos->node];
+ bool is_record = node->kind == CTLV_SCHEMA_RECORD || node->kind == CTLV_SCHEMA_BLOB;
+
+ if (hdr->flags & ~ctlv_schema_flags_allowed(v))
+ return ctlv_schema_bad(v, CTLV_VALIDATION_REASON_UNKNOWN_FLAG, record, hdr);
+ /* The schema says whether a TLV is a record, not the sender. */
+ if (!(hdr->flags & CTLV_HDR_F_RECORD) != !is_record)
+ return ctlv_schema_bad(v, CTLV_VALIDATION_REASON_MALFORMED_MESSAGE, record, hdr);
+
+ switch (node->kind) {
+ case CTLV_SCHEMA_BOOL:
+ return ctlv_schema_check_bool(v, node, record, hdr);
+ case CTLV_SCHEMA_STRING:
+ return ctlv_schema_check_string(v, node, record, hdr);
+ case CTLV_SCHEMA_BINARY:
+ return ctlv_schema_check_binary(v, node, record, hdr);
+ case CTLV_SCHEMA_BLOB:
+ return ctlv_schema_check_blob(v, node, hdr);
+ case CTLV_SCHEMA_RECORD:
+ if (v->msg->request)
+ return ctlv_schema_check_indexed_record(v, pos->tree, pos->node, hdr,
+ depth + 1, record_index);
+ return ctlv_schema_check_record(v, pos->tree, pos->node, hdr, depth + 1);
+ default:
+ return ctlv_schema_check_integer(v, node, record, hdr);
+ }
+}
+
+/* How many times one member occurs, and where the one too many is. */
+static u32 ctlv_schema_occurrences(const struct ctlv_hdr *record, u64 type, u32 max,
+ const struct ctlv_hdr **excess)
+{
+ const struct ctlv_hdr *child = NULL;
+ u32 count = 0;
+
+ while (ctlv_message_next(record, &child) == CTLV_MESSAGE_STEP_TLV) {
+ if (child->type != type)
+ continue;
+ count++;
+ if (max && count == max + 1)
+ *excess = child;
+ }
+ return count;
+}
+
+/*
+ * An optional extension point makes everything it was filled with optional
+ * too: what a descendant added cannot be something an ancestor's caller had
+ * to send.
+ */
+static int ctlv_schema_check_members(struct ctlv_schema_validation *v,
+ const struct ctlv_schema_tree *tree, u32 node,
+ const struct ctlv_hdr *record, bool optional)
+{
+ const struct ctlv_node_schema *parent = &tree->node_schemas[node];
+ u32 index;
+
+ for (index = 0; index < parent->children_count; index++) {
+ const struct ctlv_hdr *excess = NULL;
+ const struct ctlv_node_schema *child;
+ u32 count;
+
+ child = &tree->node_schemas[parent->first_child + index];
+ count = ctlv_schema_occurrences(record, child->type, child->max_occurrences,
+ &excess);
+ if (!count) {
+ if (index < parent->required_children_count && !optional)
+ return ctlv_schema_bad_missing(v, record, child->type);
+ continue;
+ }
+ /* An unlimited node, which only a reply may hold. */
+ if (child->max_occurrences && count > child->max_occurrences)
+ return ctlv_schema_bad(v, CTLV_VALIDATION_REASON_TOO_MANY_OCCURRENCES,
+ record, excess);
+ }
+
+ for (index = 0; index < tree->ext_schemas_count; index++) {
+ const struct ctlv_ext_schema *ext_schema = &tree->ext_schemas[index];
+ const struct ctlv_ext_fill_schema *fill_schema;
+ int ret;
+
+ if (ext_schema->node != node)
+ continue;
+ fill_schema = ctlv_schema_fill(v->msg->family, ext_schema->family_id,
+ ext_schema->ext_id);
+ if (!fill_schema)
+ continue;
+ ret = ctlv_schema_check_members(v, &fill_schema->members, 0, record,
+ optional || ext_schema->optional);
+ if (ret)
+ return ret;
+ }
+ return 0;
+}
+
+/* Whether one tree places one type anywhere, fills included. */
+static bool ctlv_schema_tree_declares(const struct ctlv_family *family,
+ const struct ctlv_schema_tree *tree, u64 type)
+{
+ u32 index;
+
+ for (index = 0; index < tree->node_schemas_count; index++)
+ if (tree->node_schemas[index].type == type)
+ return true;
+ for (index = 0; index < tree->ext_schemas_count; index++) {
+ const struct ctlv_ext_schema *ext_schema = &tree->ext_schemas[index];
+ const struct ctlv_ext_fill_schema *fill_schema;
+
+ fill_schema = ctlv_schema_fill(family, ext_schema->family_id, ext_schema->ext_id);
+ if (fill_schema &&
+ ctlv_schema_tree_declares(family, &fill_schema->members, type))
+ return true;
+ }
+ return false;
+}
+
+/* Search every type known anywhere in the effective schema. */
+static bool ctlv_schema_declares(const struct ctlv_family *family, u64 type)
+{
+ const struct ctlv_family *owner;
+ struct ctlv_schema_cursor cursor;
+ const struct ctlv_op_schema *op;
+
+ ctlv_schema_cursor_init(&cursor, family);
+ while ((op = ctlv_schema_next_op(&cursor, &owner)))
+ if (ctlv_schema_tree_declares(family, &op->request, type) ||
+ ctlv_schema_tree_declares(family, &op->reply, type))
+ return true;
+ return false;
+}
+
+/*
+ * Unknown IDs remain reserved unless explicitly ignorable; ignored unknown
+ * records are opaque.
+ */
+static int ctlv_schema_check_unknown(struct ctlv_schema_validation *v,
+ const struct ctlv_hdr *record,
+ const struct ctlv_hdr *hdr)
+{
+ /* Which refuses the bit itself on a reply, where it means nothing. */
+ if (hdr->flags & ~ctlv_schema_flags_allowed(v))
+ return ctlv_schema_bad(v, CTLV_VALIDATION_REASON_UNKNOWN_FLAG, record, hdr);
+ if (!(hdr->flags & CTLV_HDR_F_IGNORE_IF_UNKNOWN) ||
+ ctlv_schema_declares(v->msg->family, hdr->type))
+ return ctlv_schema_bad(v, CTLV_VALIDATION_REASON_UNKNOWN_TYPE, record, hdr);
+ return 0;
+}
+
+static int ctlv_schema_check_indexed_record(struct ctlv_schema_validation *v,
+ const struct ctlv_schema_tree *tree, u32 node,
+ const struct ctlv_hdr *record, u32 depth,
+ u32 *record_index)
+{
+ struct ctlv_message_input_index *index = v->msg->index;
+ const struct ctlv_hdr *child = NULL;
+ const struct ctlv_hdr *split = NULL;
+ u64 previous = 0;
+ bool previous_valid = false;
+ u32 added;
+ enum ctlv_message_step step;
+ int ret;
+
+ if (depth > CTLV_MAX_RECORD_DEPTH)
+ return ctlv_schema_bad(v, CTLV_VALIDATION_REASON_RECORD_TOO_DEEP, record, NULL);
+ ret = ctlv_schema_index_record_add(v, tree, node, record, &added);
+ if (ret)
+ return ret;
+
+ while ((step = ctlv_message_next(record, &child)) == CTLV_MESSAGE_STEP_TLV) {
+ struct ctlv_schema_pos pos;
+ bool seen;
+
+ if (!ctlv_schema_member(v->msg->family, tree, node, child->type, &pos)) {
+ previous_valid = false;
+ continue;
+ }
+ ret = ctlv_schema_index_tlv_add(v, added, &pos, child, &seen);
+ if (ret)
+ return ret;
+ if (seen && (!previous_valid || previous != child->type) && !split)
+ split = child;
+ previous = child->type;
+ previous_valid = true;
+ }
+
+ ret = ctlv_schema_index_required(v, added, record);
+ if (ret)
+ return ret;
+ if (step == CTLV_MESSAGE_STEP_BAD)
+ return ctlv_schema_bad(v, CTLV_VALIDATION_REASON_MALFORMED_MESSAGE, record, NULL);
+
+ child = NULL;
+ while (ctlv_message_next(record, &child) == CTLV_MESSAGE_STEP_TLV) {
+ struct ctlv_schema_pos pos;
+ struct ctlv_message_input_slot *slot;
+ u32 child_record = CTLV_MESSAGE_INPUT_INDEX_NONE;
+
+ if (!ctlv_schema_member(v->msg->family, tree, node, child->type, &pos)) {
+ ret = ctlv_schema_check_unknown(v, record, child);
+ if (ret)
+ return ret;
+ continue;
+ }
+ if (child == split)
+ return ctlv_schema_bad(v, CTLV_VALIDATION_REASON_NONCONTIGUOUS_TYPE, record,
+ child);
+ ret = ctlv_schema_check_tlv(v, &pos, record, child, depth, &child_record);
+ if (ret)
+ return ret;
+ if (child_record != CTLV_MESSAGE_INPUT_INDEX_NONE) {
+ slot = ctlv_schema_index_slot(v, added, &pos);
+ if (!slot)
+ return -EINVAL;
+ ctlv_schema_index_record_link(index, slot, child_record);
+ }
+ }
+ *record_index = added;
+ return 0;
+}
+
+/*
+ * Required members first, then every TLV in the order the record carries them,
+ * so that what is reported is the first thing a caller has to fix.
+ */
+static int ctlv_schema_check_record(struct ctlv_schema_validation *v,
+ const struct ctlv_schema_tree *tree, u32 node,
+ const struct ctlv_hdr *record, u32 depth)
+{
+ const struct ctlv_hdr *child = NULL;
+ enum ctlv_message_step step;
+ int ret;
+
+ if (depth > CTLV_MAX_RECORD_DEPTH)
+ return ctlv_schema_bad(v, CTLV_VALIDATION_REASON_RECORD_TOO_DEEP, record, NULL);
+
+ ret = ctlv_schema_check_members(v, tree, node, record, false);
+ if (ret)
+ return ret;
+
+ while ((step = ctlv_message_next(record, &child)) == CTLV_MESSAGE_STEP_TLV) {
+ struct ctlv_schema_pos pos;
+
+ if (!ctlv_schema_member(v->msg->family, tree, node, child->type, &pos)) {
+ ret = ctlv_schema_check_unknown(v, record, child);
+ if (ret)
+ return ret;
+ /* Skipped whole: the step past it checked its length. */
+ continue;
+ }
+ ret = ctlv_schema_check_tlv(v, &pos, record, child, depth, NULL);
+ if (ret)
+ return ret;
+ }
+ if (step == CTLV_MESSAGE_STEP_BAD)
+ return ctlv_schema_bad(v, CTLV_VALIDATION_REASON_MALFORMED_MESSAGE, record, NULL);
+ return 0;
+}
+
+int ctlv_schema_validate(struct ctlv_msg *msg, struct ctlv_error *error)
+{
+ struct ctlv_schema_validation validate = { .msg = msg, .error = error };
+ u32 root_index;
+ int ret;
+
+ if (!msg->request)
+ return ctlv_schema_check_record(&validate, msg->tree, 0, msg->root, 0);
+ if (WARN_ON_ONCE(msg->index))
+ return -EINVAL;
+ msg->index = kzalloc_obj(*msg->index, GFP_KERNEL_ACCOUNT);
+ if (!msg->index)
+ return -ENOMEM;
+ ret = ctlv_schema_check_indexed_record(&validate, msg->tree, 0, msg->root, 0, &root_index);
+ if (ret || WARN_ON_ONCE(root_index)) {
+ ctlv_message_index_free(msg);
+ return ret ?: -EINVAL;
+ }
+ return 0;
+}
diff --git a/lib/ctlv/schema.h b/lib/ctlv/schema.h
new file mode 100644
index 000000000000..60eb5cd927ae
--- /dev/null
+++ b/lib/ctlv/schema.h
@@ -0,0 +1,54 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES
+ *
+ * Private generated-schema walkers.
+ */
+#ifndef _CTLV_SCHEMA_H
+#define _CTLV_SCHEMA_H
+
+#include <linux/ctlv_family.h>
+#include <linux/types.h>
+
+struct ctlv_schema_cursor {
+ const struct ctlv_family *target;
+ const struct ctlv_family *family;
+ u32 level;
+ u32 index;
+};
+
+struct ctlv_schema_pos {
+ const struct ctlv_schema_tree *tree;
+ u32 node;
+};
+
+const struct ctlv_family *
+ctlv_family_ancestor(const struct ctlv_family *family, u32 up);
+u32 ctlv_family_levels(const struct ctlv_family *family);
+void ctlv_schema_cursor_init(struct ctlv_schema_cursor *cursor,
+ const struct ctlv_family *family);
+const struct ctlv_op_schema *
+ctlv_schema_next_op(struct ctlv_schema_cursor *cursor,
+ const struct ctlv_family **family);
+const struct ctlv_ext_fill_schema *
+ctlv_schema_fill(const struct ctlv_family *family, u16 family_id,
+ u32 ext_id);
+u64 ctlv_schema_enum_max(const struct ctlv_family *family,
+ const struct ctlv_node_schema *node);
+int ctlv_schema_op_sizes(const struct ctlv_family *family,
+ const struct ctlv_op_schema *op,
+ struct ctlv_op_sizes *sizes);
+bool ctlv_schema_member(const struct ctlv_family *family,
+ const struct ctlv_schema_tree *tree, u32 node,
+ u64 type, struct ctlv_schema_pos *member);
+bool ctlv_schema_value_ok(const struct ctlv_family *family,
+ const struct ctlv_node_schema *node, u64 value);
+bool ctlv_schema_length_ok(const struct ctlv_node_schema *node, u32 len);
+
+struct ctlv_error;
+struct ctlv_msg;
+
+int ctlv_schema_validate(struct ctlv_msg *msg, struct ctlv_error *error);
+void ctlv_message_index_free(struct ctlv_msg *msg);
+
+#endif /* _CTLV_SCHEMA_H */
--
2.56.0
^ permalink raw reply related [flat|nested] 24+ messages in thread
* [PATCH RFC v2 08/15] ctlv: Prepare message output handling
2026-11-13 15:54 [PATCH RFC v2 00/15] ctlv: Add a schema-driven TLV uAPI for character devices Jiri Pirko
` (6 preceding siblings ...)
2026-11-13 15:55 ` [PATCH RFC v2 07/15] ctlv: Prepare schema validation Jiri Pirko
@ 2026-11-13 15:55 ` Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 09/15] ctlv: Prepare VA blob handling Jiri Pirko
` (6 subsequent siblings)
14 siblings, 0 replies; 24+ messages in thread
From: Jiri Pirko @ 2026-11-13 15:55 UTC (permalink / raw)
To: linux-coco
Cc: linux-kernel, linux-api, x86, kvm, linux-doc, jgg, david.quigley,
lukas, ksinyuk, rodrigo.vivi, cedric.xing, sameo, drdeeglaze,
James.Bottomley, aik, jpiotrowski, qinkun, pgonda, jic23,
alistair.francis, ankita, suzuki.poulose, kas, rick.p.edgecombe,
elena.reshetova, dave.hansen, sathyanarayanan.kuppuswamy,
peter.fang, jarkko, zohar, yeoreum.yun, aneesh.kumar, dave.jiang,
nik.borisov, sergii.dmytruk
From: Jiri Pirko <jiri@nvidia.com>
Add schema-aware reply construction, generated output accessors,
operation errors and query fragment completion.
Keep CONFIG_CTLV unwired until dispatch completes the module.
Assisted-by: Cursor: GPT-5.6 Sol
Assisted-by: Cursor: Claude Opus 5.5
Signed-off-by: Jiri Pirko <jiri@nvidia.com>
---
RFC->RFCv2:
- added ctlv_record_cancel()
- moved the function kernel-doc to the definitions
- made the warning for a reply past its declared length a CTLV_ASSERT()
- said TLV, type, record and field instead of attr
- said record instead of nest, the root being one
- took the UAPI headers from include/uapi/ctlv/
- moved to lib/ctlv/
---
include/linux/ctlv.h | 24 ++
include/linux/ctlv_family.h | 41 ++++
lib/ctlv/Makefile | 2 +-
lib/ctlv/builder.c | 455 ++++++++++++++++++++++++++++++++++++
lib/ctlv/builder.h | 14 ++
5 files changed, 535 insertions(+), 1 deletion(-)
create mode 100644 lib/ctlv/builder.c
create mode 100644 lib/ctlv/builder.h
diff --git a/include/linux/ctlv.h b/include/linux/ctlv.h
index e151e32a0ffd..5b75550de678 100644
--- a/include/linux/ctlv.h
+++ b/include/linux/ctlv.h
@@ -34,6 +34,8 @@ struct ctlv_schema_tree;
* @depth: record levels above this position, the root being zero
* @index_record_scope: schema scope used to read the members of @hdr, a record
* @index_scope: core-private schema scope containing this TLV
+ * @progress: writing, whether the fragment had progressed before this record
+ * opened
*
* Fixed-size, so family code can put a child cursor on the stack.
* ctlv_record_start() opens at most CTLV_MAX_RECORD_DEPTH levels, which bounds
@@ -52,6 +54,28 @@ struct ctlv_cursor {
u32 depth;
u32 index_record_scope;
u32 index_scope;
+ bool progress;
};
+void ctlv_record_end(struct ctlv_cursor *child);
+void ctlv_record_cancel(struct ctlv_cursor *child);
+
+int ctlv_op_error(struct ctlv_op_ctx *ctx, int error, const char *message);
+__printf(3, 4)
+int ctlv_op_errorf(struct ctlv_op_ctx *ctx, int error, const char *fmt, ...);
+
+/*
+ * The core retains no query state. A continuation repeats the request and
+ * passes back the family's generation and nonzero cursor. It fails with
+ * -ESTALE if the generation changed. Each fragment must complete a logical
+ * leaf and return a cursor different from its input.
+ *
+ * ctlv_op_query_more() marks still-open records, whose cursors must stay live.
+ */
+u64 ctlv_op_query_generation(const struct ctlv_op_ctx *ctx);
+u64 ctlv_op_query_cursor(const struct ctlv_op_ctx *ctx);
+int ctlv_op_query_more(struct ctlv_op_ctx *ctx, u64 generation,
+ u64 next_cursor);
+int ctlv_op_query_done(struct ctlv_op_ctx *ctx, u64 generation);
+
#endif /* _LINUX_CTLV_H */
diff --git a/include/linux/ctlv_family.h b/include/linux/ctlv_family.h
index ae8ed69df5ea..4d7d4fb65fec 100644
--- a/include/linux/ctlv_family.h
+++ b/include/linux/ctlv_family.h
@@ -441,6 +441,47 @@ bool ctlv_cursor_bool(const struct ctlv_cursor *cursor);
const char *ctlv_cursor_string(const struct ctlv_cursor *cursor);
const void *ctlv_cursor_binary(const struct ctlv_cursor *cursor, u32 *len);
+bool ctlv_put_u64(struct ctlv_cursor *record, u64 type, u8 kind, u64 value);
+bool ctlv_put_s64(struct ctlv_cursor *record, u64 type, u8 kind, s64 value);
+bool ctlv_put_bool(struct ctlv_cursor *record, u64 type, u8 kind, bool value);
+bool ctlv_put_string(struct ctlv_cursor *record, u64 type, u8 kind,
+ const char *value);
+bool ctlv_put_binary(struct ctlv_cursor *record, u64 type, u8 kind,
+ const void *value, u32 len);
+bool ctlv_record_start(struct ctlv_cursor *record, struct ctlv_cursor *child,
+ u64 type, u8 kind);
+
+#define __CTLV_DEFINE_PUT(_name, _ctype, _put_fn, _type, _kind) \
+static inline bool _name##_put(struct ctlv_cursor *record, \
+ _ctype value) \
+{ \
+ return _put_fn(record, (_type), (_kind), value); \
+}
+
+#define CTLV_PUT_U64(_name, _type, _kind) \
+ __CTLV_DEFINE_PUT(_name, u64, ctlv_put_u64, _type, _kind)
+#define CTLV_PUT_S64(_name, _type, _kind) \
+ __CTLV_DEFINE_PUT(_name, s64, ctlv_put_s64, _type, _kind)
+#define CTLV_PUT_BOOL(_name, _type, _kind) \
+ __CTLV_DEFINE_PUT(_name, bool, ctlv_put_bool, _type, _kind)
+#define CTLV_PUT_STRING(_name, _type, _kind) \
+ __CTLV_DEFINE_PUT(_name, const char *, ctlv_put_string, \
+ _type, _kind)
+
+#define CTLV_PUT_BINARY(_name, _type, _kind) \
+static inline bool _name##_put(struct ctlv_cursor *record, \
+ const void *value, u32 len) \
+{ \
+ return ctlv_put_binary(record, (_type), (_kind), value, len); \
+}
+
+#define CTLV_RECORD_START(_name, _type, _kind) \
+static inline bool _name##_start(struct ctlv_cursor *record, \
+ struct ctlv_cursor *inner) \
+{ \
+ return ctlv_record_start(record, inner, (_type), (_kind)); \
+}
+
#define CTLV_IN_REF(_name, _type, _kind) \
static inline struct ctlv_input_ref _name##_ref(void) \
{ \
diff --git a/lib/ctlv/Makefile b/lib/ctlv/Makefile
index 3fd656835b8a..e1a22a199ef1 100644
--- a/lib/ctlv/Makefile
+++ b/lib/ctlv/Makefile
@@ -1,4 +1,4 @@
# SPDX-License-Identifier: GPL-2.0
obj-$(CONFIG_CTLV) += ctlv.o
-ctlv-y += blob_desc.o message.o schema.o
+ctlv-y += blob_desc.o builder.o message.o schema.o
diff --git a/lib/ctlv/builder.c b/lib/ctlv/builder.c
new file mode 100644
index 000000000000..976e2f8d1aaa
--- /dev/null
+++ b/lib/ctlv/builder.c
@@ -0,0 +1,455 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES
+ *
+ * Schema-aware reply construction and operation completion.
+ */
+#include <linux/align.h>
+#include <linux/bug.h>
+#include <linux/ctlv.h>
+#include <linux/errno.h>
+#include <linux/export.h>
+#include <linux/stdarg.h>
+#include <linux/string.h>
+#include <linux/types.h>
+
+#include <uapi/ctlv/ctlv.h>
+#include <uapi/ctlv/ctlv_gen.h>
+
+#include "builder.h"
+#include "internal.h"
+#include "message.h"
+#include "schema.h"
+
+static void ctlv_message_output_frag_mark(struct ctlv_op_ctx *ctx);
+
+void ctlv_message_output_init(struct ctlv_msg *msg, struct ctlv_op_ctx *ctx, void *buf, size_t len,
+ const struct ctlv_schema_tree *tree)
+{
+ *msg = (struct ctlv_msg){
+ .root = buf,
+ .len = len,
+ .tree = tree,
+ .family = ctx->family,
+ .ctx = ctx,
+ };
+ *msg->root = (struct ctlv_hdr){
+ .len = sizeof(*msg->root),
+ .flags = CTLV_HDR_F_RECORD,
+ .type = CTLV_TYPE_ROOT,
+ };
+}
+
+/* The first thing that went wrong is what the op ends up failing with. */
+static bool ctlv_message_output_poison(struct ctlv_cursor *record, int error)
+{
+ if (!record->msg->poison)
+ record->msg->poison = error;
+ return false;
+}
+
+/* A poisoned message is refused silently: whatever poisoned it has warned. */
+static bool ctlv_message_output_ready(struct ctlv_cursor *record)
+{
+ if (WARN_ON_ONCE(!record->msg || !record->hdr))
+ return false;
+ /* Also what refuses a cursor that reads a request. */
+ if (WARN_ON_ONCE(!record->msg->ctx))
+ return false;
+ if (record->msg->poison)
+ return false;
+ if (WARN_ON_ONCE(record->msg->ctx->deepest != record))
+ return ctlv_message_output_poison(record, -EINVAL);
+ return true;
+}
+
+/* What the schema places in this record under one type, as one schema kind. */
+static const struct ctlv_node_schema *ctlv_message_output_member(struct ctlv_cursor *record,
+ u64 type, u8 kind,
+ struct ctlv_schema_pos *pos)
+{
+ const struct ctlv_node_schema *node;
+
+ if (!ctlv_message_output_ready(record))
+ return NULL;
+ if (WARN_ON_ONCE(!ctlv_schema_member(record->msg->family, record->tree,
+ record->node, type, pos))) {
+ ctlv_message_output_poison(record, -EINVAL);
+ return NULL;
+ }
+ node = &pos->tree->node_schemas[pos->node];
+ if (WARN_ON_ONCE(node->kind != kind)) {
+ ctlv_message_output_poison(record, -EINVAL);
+ return NULL;
+ }
+ return node;
+}
+
+/*
+ * A query is offered whatever room its caller had, so running out ends a
+ * fragment. Anything else wrote a message its own schema does not describe.
+ */
+static bool ctlv_message_output_full(struct ctlv_cursor *record)
+{
+ const struct ctlv_op_schema *op = record->msg->ctx->op->schema;
+
+ if (op->type == CTLV_OP_TYPE_QUERY)
+ return false;
+ CTLV_ASSERT(0);
+ return ctlv_message_output_poison(record, -EMSGSIZE);
+}
+
+/* What one cursor stands on, as the buffer this direction may change. */
+static struct ctlv_hdr *ctlv_message_output_hdr(const struct ctlv_cursor *record)
+{
+ return (struct ctlv_hdr *)((u8 *)record->msg->root + record->start);
+}
+
+/* The pad is written rather than left as it was: it belongs to the record. */
+static struct ctlv_hdr *ctlv_message_output_write(struct ctlv_cursor *record, u64 type, u32 flags,
+ const void *payload, u32 payload_len)
+{
+ u32 len = sizeof(struct ctlv_hdr) + payload_len;
+ size_t step = ALIGN((size_t)len, CTLV_ALIGNTO);
+ struct ctlv_msg *msg = record->msg;
+ struct ctlv_cursor *open;
+ struct ctlv_hdr *hdr;
+ size_t offset;
+
+ offset = record->start + ctlv_message_output_hdr(record)->len;
+ if (offset + step > msg->len) {
+ ctlv_message_output_full(record);
+ return NULL;
+ }
+
+ hdr = (struct ctlv_hdr *)((u8 *)msg->root + offset);
+ *hdr = (struct ctlv_hdr){
+ .len = len,
+ .flags = flags,
+ .type = type,
+ };
+ if (payload_len)
+ memcpy(hdr + 1, payload, payload_len);
+ memset((u8 *)hdr + len, 0, step - len);
+ if (!(flags & CTLV_HDR_F_RECORD))
+ msg->ctx->frag.progress = true;
+
+ /* Every record the TLV landed in is that much longer, the root too. */
+ for (open = record; open; open = open->up)
+ ctlv_message_output_hdr(open)->len += step;
+ return hdr;
+}
+
+/* The 64-bit payload that every integer, enum and flags field carries. */
+static bool ctlv_message_put_scalar(struct ctlv_cursor *record, u64 type, u8 kind, u64 value)
+{
+ const struct ctlv_node_schema *node;
+ struct ctlv_schema_pos pos;
+
+ node = ctlv_message_output_member(record, type, kind, &pos);
+ if (!node)
+ return false;
+ /* A value outside what the field declares is one nothing may be told. */
+ if (WARN_ON_ONCE(!ctlv_schema_value_ok(record->msg->family, node, value)))
+ return ctlv_message_output_poison(record, -EINVAL);
+ return ctlv_message_output_write(record, type, 0, &value, sizeof(value));
+}
+
+bool ctlv_put_u64(struct ctlv_cursor *record, u64 type, u8 kind, u64 value)
+{
+ return ctlv_message_put_scalar(record, type, kind, value);
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_put_u64, "CTLV");
+
+bool ctlv_put_s64(struct ctlv_cursor *record, u64 type, u8 kind, s64 value)
+{
+ return ctlv_message_put_scalar(record, type, kind, value);
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_put_s64, "CTLV");
+
+bool ctlv_put_bool(struct ctlv_cursor *record, u64 type, u8 kind, bool value)
+{
+ u8 payload = value;
+ struct ctlv_schema_pos pos;
+
+ if (!ctlv_message_output_member(record, type, kind, &pos))
+ return false;
+ return ctlv_message_output_write(record, type, 0, &payload, sizeof(payload));
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_put_bool, "CTLV");
+
+bool ctlv_put_string(struct ctlv_cursor *record, u64 type, u8 kind, const char *value)
+{
+ const struct ctlv_node_schema *node;
+ struct ctlv_schema_pos pos;
+ size_t len;
+
+ node = ctlv_message_output_member(record, type, kind, &pos);
+ if (!node)
+ return false;
+ len = strnlen(value, (size_t)node->length.max_len + 1);
+ if (WARN_ON_ONCE(len > node->length.max_len || !ctlv_schema_length_ok(node, len)))
+ return ctlv_message_output_poison(record, -EINVAL);
+ return ctlv_message_output_write(record, type, 0, value, len + 1);
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_put_string, "CTLV");
+
+bool ctlv_put_binary(struct ctlv_cursor *record, u64 type, u8 kind, const void *value, u32 len)
+{
+ const struct ctlv_node_schema *node;
+ struct ctlv_schema_pos pos;
+
+ node = ctlv_message_output_member(record, type, kind, &pos);
+ if (!node)
+ return false;
+ if (WARN_ON_ONCE(!ctlv_schema_length_ok(node, len)))
+ return ctlv_message_output_poison(record, -EINVAL);
+ return ctlv_message_output_write(record, type, 0, value, len);
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_put_binary, "CTLV");
+
+bool ctlv_record_start(struct ctlv_cursor *record, struct ctlv_cursor *child, u64 type, u8 kind)
+{
+ struct ctlv_schema_pos pos;
+ struct ctlv_hdr *hdr;
+
+ *child = (struct ctlv_cursor){};
+ if (!ctlv_message_output_member(record, type, kind, &pos))
+ return false;
+ /* Unreachable through any schema the generator would have accepted. */
+ if (WARN_ON_ONCE(record->depth + 1 > CTLV_MAX_RECORD_DEPTH))
+ return ctlv_message_output_poison(record, -EINVAL);
+
+ hdr = ctlv_message_output_write(record, type, CTLV_HDR_F_RECORD, NULL, 0);
+ if (!hdr)
+ return false;
+
+ *child = (struct ctlv_cursor){
+ .msg = record->msg,
+ .hdr = hdr,
+ .up = record,
+ .start = ctlv_message_offset(record->msg, hdr),
+ .tree = pos.tree,
+ .node = pos.node,
+ .depth = record->depth + 1,
+ .progress = record->msg->ctx->frag.progress,
+ };
+ record->msg->ctx->deepest = child;
+ return true;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_record_start, "CTLV");
+
+/* Only the innermost open record of an unpoisoned message can be closed. */
+static struct ctlv_op_ctx *ctlv_message_output_closing(struct ctlv_cursor *child)
+{
+ struct ctlv_op_ctx *ctx;
+
+ if (WARN_ON_ONCE(!child->msg || !child->msg->ctx || !child->up))
+ return NULL;
+ ctx = child->msg->ctx;
+ if (child->msg->poison)
+ return NULL;
+ if (WARN_ON_ONCE(ctx->deepest != child)) {
+ ctlv_message_output_poison(child, -EINVAL);
+ return NULL;
+ }
+ return ctx;
+}
+
+/**
+ * ctlv_record_end - Finish an output record.
+ * @child: record cursor returned by a generated start helper
+ */
+void ctlv_record_end(struct ctlv_cursor *child)
+{
+ struct ctlv_op_ctx *ctx = ctlv_message_output_closing(child);
+
+ if (!ctx)
+ return;
+ if (ctlv_message_output_hdr(child)->len == sizeof(struct ctlv_hdr))
+ ctx->frag.progress = true;
+ ctx->deepest = child->up;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_record_end, "CTLV");
+
+/**
+ * ctlv_record_cancel - Drop an output record and everything written into it.
+ * @child: innermost open record cursor returned by a generated start helper
+ *
+ * For a query that ran out of room partway through a record: the fragment then
+ * ends before it, and the next fragment writes it again.
+ */
+void ctlv_record_cancel(struct ctlv_cursor *child)
+{
+ struct ctlv_op_ctx *ctx = ctlv_message_output_closing(child);
+ struct ctlv_cursor *open;
+ u32 len;
+
+ if (!ctx)
+ return;
+ /* The record lengthened each one holding it by exactly its length. */
+ len = ctlv_message_output_hdr(child)->len;
+ for (open = child->up; open; open = open->up)
+ ctlv_message_output_hdr(open)->len -= len;
+ ctx->frag.progress = child->progress;
+ ctx->deepest = child->up;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_record_cancel, "CTLV");
+
+static bool ctlv_op_error_valid(int error)
+{
+ return !WARN_ON_ONCE(error >= 0 || error < -MAX_ERRNO);
+}
+
+/**
+ * ctlv_op_error - Record an operation error message.
+ * @ctx: operation context
+ * @error: negative error to return
+ * @message: message copied into the operation result
+ *
+ * Return: @error, or -EINVAL if @error is invalid.
+ */
+int ctlv_op_error(struct ctlv_op_ctx *ctx, int error, const char *message)
+{
+ if (!ctlv_op_error_valid(error))
+ return error < 0 && error >= -MAX_ERRNO ? error : -EINVAL;
+ if (!ctx->error.message[0])
+ strscpy(ctx->error.message, message, sizeof(ctx->error.message));
+ return error;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_op_error, "CTLV");
+
+/**
+ * ctlv_op_errorf - Format an operation error message.
+ * @ctx: operation context
+ * @error: negative error to return
+ * @fmt: printf format and arguments
+ *
+ * Return: @error, or -EINVAL if @error is invalid.
+ */
+int ctlv_op_errorf(struct ctlv_op_ctx *ctx, int error, const char *fmt, ...)
+{
+ va_list args;
+
+ if (!ctlv_op_error_valid(error))
+ return error < 0 && error >= -MAX_ERRNO ? error : -EINVAL;
+ if (ctx->error.message[0])
+ return error;
+ va_start(args, fmt);
+ vscnprintf(ctx->error.message, sizeof(ctx->error.message), fmt, args);
+ va_end(args);
+ return error;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_op_errorf, "CTLV");
+
+/* Continuation is a query's, so asking about one anywhere else is a bug. */
+static bool ctlv_op_is_query(const struct ctlv_op_ctx *ctx)
+{
+ return !WARN_ON_ONCE(ctx->op->schema->type != CTLV_OP_TYPE_QUERY);
+}
+
+/**
+ * ctlv_op_query_generation - Get a query continuation generation.
+ * @ctx: query operation context
+ *
+ * Return: generation supplied by the previous fragment, or zero initially.
+ */
+u64 ctlv_op_query_generation(const struct ctlv_op_ctx *ctx)
+{
+ if (!ctlv_op_is_query(ctx))
+ return 0;
+ return ctx->frag.generation;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_op_query_generation, "CTLV");
+
+/**
+ * ctlv_op_query_cursor - Get a query continuation cursor.
+ * @ctx: query operation context
+ *
+ * Return: cursor supplied by the previous fragment, or zero initially.
+ */
+u64 ctlv_op_query_cursor(const struct ctlv_op_ctx *ctx)
+{
+ if (!ctlv_op_is_query(ctx))
+ return 0;
+ return ctx->frag.cursor;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_op_query_cursor, "CTLV");
+
+/*
+ * What both endings check: a query, one ending, and a generation that has not
+ * moved under a continuation, which no fragment can be delivered across.
+ */
+static int ctlv_query_fragment_end(struct ctlv_op_ctx *ctx, u64 generation)
+{
+ if (!ctlv_op_is_query(ctx))
+ return -EINVAL;
+ if (WARN_ON_ONCE(ctx->frag.answered))
+ return -EINVAL;
+ ctx->frag.answered = true;
+ /* Whatever the builder gave up on has warned about it already. */
+ if (ctx->output.poison)
+ return ctx->output.poison;
+ if (ctx->frag.cursor && generation != ctx->frag.generation) {
+ ctx->error.reason = CTLV_VALIDATION_REASON_STALE_GENERATION;
+ ctx->error.expected_generation = ctx->frag.generation;
+ ctx->error.current_generation = generation;
+ return -ESTALE;
+ }
+ ctx->frag.reported = generation;
+ return 0;
+}
+
+/**
+ * ctlv_op_query_more - Complete a query fragment with more data pending.
+ * @ctx: query operation context
+ * @generation: current data generation
+ * @next_cursor: nonzero cursor for the next fragment
+ *
+ * Return: 0, or a negative error.
+ */
+int ctlv_op_query_more(struct ctlv_op_ctx *ctx, u64 generation, u64 next_cursor)
+{
+ int ret = ctlv_query_fragment_end(ctx, generation);
+
+ if (!ret &&
+ WARN_ON_ONCE(!ctx->frag.progress || !next_cursor || next_cursor == ctx->frag.cursor))
+ ret = -EINVAL;
+ if (!ret) {
+ ctlv_message_output_frag_mark(ctx);
+ ctx->frag.next_cursor = next_cursor;
+ }
+ ctx->frag.result = ret;
+ return ret;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_op_query_more, "CTLV");
+
+/**
+ * ctlv_op_query_done - Complete the last query fragment.
+ * @ctx: query operation context
+ * @generation: current data generation
+ *
+ * Return: 0, or a negative error.
+ */
+int ctlv_op_query_done(struct ctlv_op_ctx *ctx, u64 generation)
+{
+ int ret = ctlv_query_fragment_end(ctx, generation);
+
+ if (!ret && WARN_ON_ONCE(ctx->deepest != &ctx->output_root)) {
+ /* A cursor of a frame that has returned is not followed. */
+ ctx->deepest = &ctx->output_root;
+ ret = -EINVAL;
+ }
+ ctx->frag.result = ret;
+ return ret;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_op_query_done, "CTLV");
+
+static void ctlv_message_output_frag_mark(struct ctlv_op_ctx *ctx)
+{
+ struct ctlv_cursor *open;
+
+ for (open = ctx->deepest; open; open = open->up)
+ ctlv_message_output_hdr(open)->flags |= CTLV_HDR_F_FRAG_MORE;
+ ctx->deepest = &ctx->output_root;
+}
diff --git a/lib/ctlv/builder.h b/lib/ctlv/builder.h
new file mode 100644
index 000000000000..744595ebd88e
--- /dev/null
+++ b/lib/ctlv/builder.h
@@ -0,0 +1,14 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef _CTLV_BUILDER_H
+#define _CTLV_BUILDER_H
+
+#include <linux/types.h>
+
+struct ctlv_msg;
+struct ctlv_op_ctx;
+struct ctlv_schema_tree;
+
+void ctlv_message_output_init(struct ctlv_msg *msg, struct ctlv_op_ctx *ctx, void *buf, size_t len,
+ const struct ctlv_schema_tree *tree);
+
+#endif /* _CTLV_BUILDER_H */
--
2.56.0
^ permalink raw reply related [flat|nested] 24+ messages in thread
* [PATCH RFC v2 09/15] ctlv: Prepare VA blob handling
2026-11-13 15:54 [PATCH RFC v2 00/15] ctlv: Add a schema-driven TLV uAPI for character devices Jiri Pirko
` (7 preceding siblings ...)
2026-11-13 15:55 ` [PATCH RFC v2 08/15] ctlv: Prepare message output handling Jiri Pirko
@ 2026-11-13 15:55 ` Jiri Pirko
2026-11-13 16:13 ` sashiko-bot
2026-11-13 15:55 ` [PATCH RFC v2 10/15] ctlv: Prepare runtime introspection Jiri Pirko
` (5 subsequent siblings)
14 siblings, 1 reply; 24+ messages in thread
From: Jiri Pirko @ 2026-11-13 15:55 UTC (permalink / raw)
To: linux-coco
Cc: linux-kernel, linux-api, x86, kvm, linux-doc, jgg, david.quigley,
lukas, ksinyuk, rodrigo.vivi, cedric.xing, sameo, drdeeglaze,
James.Bottomley, aik, jpiotrowski, qinkun, pgonda, jic23,
alistair.francis, ankita, suzuki.poulose, kas, rick.p.edgecombe,
elena.reshetova, dave.hansen, sathyanarayanan.kuppuswamy,
peter.fang, jarkko, zohar, yeoreum.yun, aneesh.kumar, dave.jiang,
nik.borisov, sergii.dmytruk
From: Jiri Pirko <jiri@nvidia.com>
Add task-bound VA blob resolution and CPU read/write access for CTLV
families.
The blob object can be compile-checked individually. CONFIG_CTLV remains
unwired until operation dispatch completes the module.
Assisted-by: Cursor: GPT-5.6 Sol
Assisted-by: Cursor: Claude Opus 5.5
Signed-off-by: Jiri Pirko <jiri@nvidia.com>
---
RFC->RFCv2:
- released a context's blobs from any task
- returned EOVERFLOW for a blob range that wraps
- moved the function kernel-doc to the definitions
- said TLV, type, record and field instead of attr
- said record instead of nest, the root being one
- added ctlv_blob_too_short() for an output blob too short
- took the UAPI headers from include/uapi/ctlv/
- moved to lib/ctlv/
---
include/linux/ctlv.h | 9 +
include/linux/ctlv_family.h | 23 ++
lib/ctlv/Makefile | 2 +-
lib/ctlv/blob.c | 421 ++++++++++++++++++++++++++++++++++++
lib/ctlv/blob.h | 15 ++
lib/ctlv/internal.h | 8 +
6 files changed, 477 insertions(+), 1 deletion(-)
create mode 100644 lib/ctlv/blob.c
create mode 100644 lib/ctlv/blob.h
diff --git a/include/linux/ctlv.h b/include/linux/ctlv.h
index 5b75550de678..afb71cc643d3 100644
--- a/include/linux/ctlv.h
+++ b/include/linux/ctlv.h
@@ -78,4 +78,13 @@ int ctlv_op_query_more(struct ctlv_op_ctx *ctx, u64 generation,
u64 next_cursor);
int ctlv_op_query_done(struct ctlv_op_ctx *ctx, u64 generation);
+struct ctlv_blob;
+
+int ctlv_blob_put(struct ctlv_blob *blob);
+u64 ctlv_blob_len(const struct ctlv_blob *blob);
+int ctlv_blob_read(struct ctlv_blob *blob, u64 offset, void *dst, size_t len);
+int ctlv_blob_write(struct ctlv_blob *blob, u64 offset, const void *src,
+ size_t len);
+int ctlv_blob_too_short(struct ctlv_blob *blob, u64 len);
+
#endif /* _LINUX_CTLV_H */
diff --git a/include/linux/ctlv_family.h b/include/linux/ctlv_family.h
index 4d7d4fb65fec..61ae5b7cec83 100644
--- a/include/linux/ctlv_family.h
+++ b/include/linux/ctlv_family.h
@@ -451,6 +451,8 @@ bool ctlv_put_binary(struct ctlv_cursor *record, u64 type, u8 kind,
bool ctlv_record_start(struct ctlv_cursor *record, struct ctlv_cursor *child,
u64 type, u8 kind);
+struct ctlv_blob *ctlv_blob_resolve(struct ctlv_op_ctx *ctx,
+ const struct ctlv_cursor *blob);
#define __CTLV_DEFINE_PUT(_name, _ctype, _put_fn, _type, _kind) \
static inline bool _name##_put(struct ctlv_cursor *record, \
_ctype value) \
@@ -560,6 +562,17 @@ static inline void _name##_get(struct ctlv_cursor *inner, \
ctlv_input_field(inner, record, _name##_ref()); \
}
+#define CTLV_IN_BLOB(_name) \
+static inline struct ctlv_blob * \
+_name##_resolve(struct ctlv_op_ctx *ctx, \
+ const struct ctlv_cursor *record) \
+{ \
+ struct ctlv_cursor cursor; \
+ \
+ ctlv_input_field(&cursor, record, _name##_ref()); \
+ return ctlv_blob_resolve(ctx, &cursor); \
+}
+
#define CTLV_IN_EACH_U64(_name) \
static inline u64 _name##_get(const struct ctlv_cursor *cursor) \
{ \
@@ -604,4 +617,14 @@ _name##_get(const struct ctlv_cursor *cursor, u32 *len) \
return ctlv_cursor_binary(cursor, len); \
}
+#define CTLV_IN_EACH_BLOB(_name) \
+static inline struct ctlv_blob * \
+_name##_resolve(struct ctlv_op_ctx *ctx, \
+ const struct ctlv_cursor *cursor) \
+{ \
+ if (!ctlv_input_cursor(cursor, _name##_ref())) \
+ return ERR_PTR(-EINVAL); \
+ return ctlv_blob_resolve(ctx, cursor); \
+}
+
#endif /* _LINUX_CTLV_FAMILY_H */
diff --git a/lib/ctlv/Makefile b/lib/ctlv/Makefile
index e1a22a199ef1..4455062f83d7 100644
--- a/lib/ctlv/Makefile
+++ b/lib/ctlv/Makefile
@@ -1,4 +1,4 @@
# SPDX-License-Identifier: GPL-2.0
obj-$(CONFIG_CTLV) += ctlv.o
-ctlv-y += blob_desc.o builder.o message.o schema.o
+ctlv-y += blob.o blob_desc.o builder.o message.o schema.o
diff --git a/lib/ctlv/blob.c b/lib/ctlv/blob.c
new file mode 100644
index 000000000000..14cbbc4bcab1
--- /dev/null
+++ b/lib/ctlv/blob.c
@@ -0,0 +1,421 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES
+ *
+ * Blob backing is acquired lazily in the submitting task and held until put.
+ */
+#include <linux/align.h>
+#include <linux/atomic.h>
+#include <linux/bits.h>
+#include <linux/capability.h>
+#include <linux/ctlv.h>
+#include <linux/err.h>
+#include <linux/errno.h>
+#include <linux/highmem.h>
+#include <linux/limits.h>
+#include <linux/minmax.h>
+#include <linux/mm.h>
+#include <linux/mm_types.h>
+#include <linux/overflow.h>
+#include <linux/sched.h>
+#include <linux/sched/mm.h>
+#include <linux/sched/signal.h>
+#include <linux/slab.h>
+#include <linux/types.h>
+
+#include <uapi/ctlv/ctlv.h>
+#include <uapi/ctlv/ctlv_gen.h>
+
+#include "blob.h"
+#include "blob_desc.h"
+#include "internal.h"
+#include "message.h"
+
+struct ctlv_blob {
+ u64 len;
+ u8 direction;
+ u8 type;
+ /* Where the descriptor sat in the request, for error information. */
+ u32 tlv_offset;
+ u32 record_offset;
+ struct ctlv_op_ctx *ctx;
+ struct list_head entry;
+ union {
+ struct {
+ struct mm_struct *mm;
+ struct page **pages;
+ unsigned long pages_count;
+ u32 start;
+ } va;
+ };
+};
+
+static bool ctlv_blob_readable(const struct ctlv_blob *blob)
+{
+ return blob->direction != CTLV_BLOB_DIRECTION_OUTPUT;
+}
+
+static bool ctlv_blob_writable(const struct ctlv_blob *blob)
+{
+ return blob->direction != CTLV_BLOB_DIRECTION_INPUT;
+}
+
+static bool ctlv_blob_same_task(const struct ctlv_blob *blob)
+{
+ return !blob->ctx || !blob->ctx->submitter || blob->ctx->submitter == current;
+}
+
+static bool ctlv_blob_access_live(const struct ctlv_blob *blob)
+{
+ return ctlv_blob_same_task(blob) && (!blob->ctx || blob->ctx->blobs_live);
+}
+
+/*
+ * Charged against RLIMIT_MEMLOCK of the address space the pages came from. A
+ * pin lasts one op, which is why it is not FOLL_LONGTERM.
+ */
+static int ctlv_blob_pin_charge(struct mm_struct *mm, unsigned long pages_count)
+{
+ unsigned long limit = rlimit(RLIMIT_MEMLOCK) >> PAGE_SHIFT;
+
+ if (atomic64_add_return(pages_count, &mm->pinned_vm) > limit && !capable(CAP_IPC_LOCK)) {
+ atomic64_sub(pages_count, &mm->pinned_vm);
+ return -ENOMEM;
+ }
+ return 0;
+}
+
+static int ctlv_blob_va_range(u64 address, u64 len, unsigned long *first_page,
+ unsigned long *pages_count)
+{
+ unsigned long first, last, span, count;
+ size_t bytes;
+ u64 end;
+
+ if (!len)
+ return -EINVAL;
+ if (check_add_overflow(address, len, &end))
+ return -EOVERFLOW;
+ if (end > TASK_SIZE)
+ return -EFAULT;
+
+ /* Bounded by an unsigned long above, so nothing below loses a bit. */
+ first = (unsigned long)address & PAGE_MASK;
+ last = ((unsigned long)end - 1) & PAGE_MASK;
+ if (check_sub_overflow(last, first, &span) ||
+ check_add_overflow(span >> PAGE_SHIFT, 1UL, &count) ||
+ check_mul_overflow(count, sizeof(struct page *), &bytes))
+ return -EOVERFLOW;
+
+ *first_page = first;
+ *pages_count = count;
+ return 0;
+}
+
+static int ctlv_blob_va_acquire(struct ctlv_blob *blob, u64 address)
+{
+ unsigned long pinned = 0, first, pages_count;
+ unsigned int gup_flags = 0;
+ struct page **pages;
+ int ret;
+
+ /* Before charging or allocating, both of which this sizes. */
+ ret = ctlv_blob_va_range(address, blob->len, &first, &pages_count);
+ if (ret)
+ return ret;
+
+ pages = kvmalloc_array(pages_count, sizeof(*pages), GFP_KERNEL_ACCOUNT);
+ if (!pages)
+ return -ENOMEM;
+
+ if (ctlv_blob_writable(blob))
+ gup_flags |= FOLL_WRITE;
+ ret = ctlv_blob_pin_charge(current->mm, pages_count);
+ if (ret)
+ goto err_free_pages;
+
+ while (pinned < pages_count) {
+ long got = pin_user_pages_fast(first + (pinned << PAGE_SHIFT),
+ min_t(unsigned long, pages_count - pinned, INT_MAX),
+ gup_flags, pages + pinned);
+
+ /* A short pin is progress; nothing pinned is the caller's. */
+ if (got <= 0) {
+ ret = got ? (int)got : -EFAULT;
+ goto err_unpin;
+ }
+ pinned += got;
+ }
+
+ /* Referenced, so the unpin is safe however far the submitter got. */
+ mmgrab(current->mm);
+ blob->va.start = offset_in_page(address);
+ blob->va.mm = current->mm;
+ blob->va.pages = pages;
+ blob->va.pages_count = pages_count;
+ return 0;
+
+ /* The pins taken, and the whole charge, which covered every page. */
+err_unpin:
+ unpin_user_pages(pages, pinned);
+ atomic64_sub(pages_count, ¤t->mm->pinned_vm);
+err_free_pages:
+ kvfree(pages);
+ return ret;
+}
+
+static void ctlv_blob_va_release(struct ctlv_blob *blob)
+{
+ /* Dirtied, so that what the kernel wrote survives the unpin. */
+ unpin_user_pages_dirty_lock(blob->va.pages, blob->va.pages_count, ctlv_blob_writable(blob));
+ atomic64_sub(blob->va.pages_count, &blob->va.mm->pinned_vm);
+ kvfree(blob->va.pages);
+ mmdrop(blob->va.mm);
+}
+
+static void ctlv_blob_va_copy(struct ctlv_blob *blob, u64 offset, void *buf, size_t len, bool write)
+{
+ u64 at = blob->va.start + offset;
+ unsigned long index = at >> PAGE_SHIFT;
+ u32 in_page = offset_in_page(at);
+ u8 *bytes = buf;
+
+ while (len) {
+ struct page *page = blob->va.pages[index++];
+ size_t step = min_t(size_t, len, PAGE_SIZE - in_page);
+
+ if (write)
+ memcpy_to_page(page, in_page, bytes, step);
+ else
+ memcpy_from_page(bytes, page, in_page, step);
+ bytes += step;
+ len -= step;
+ in_page = 0;
+ }
+}
+
+/**
+ * ctlv_blob_len - Get a resolved blob's length.
+ * @blob: resolved blob
+ *
+ * Return: blob length in bytes.
+ */
+u64 ctlv_blob_len(const struct ctlv_blob *blob)
+{
+ return blob->len;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_blob_len, "CTLV");
+
+/*
+ * The length, the direction and addressing from zero are all the family's own
+ * to know, so getting one wrong is a kernel bug rather than a caller's.
+ */
+static bool ctlv_blob_range_ok(struct ctlv_blob *blob, u64 offset, size_t len, bool write)
+{
+ u64 end;
+
+ if (WARN_ON_ONCE(write ? !ctlv_blob_writable(blob) : !ctlv_blob_readable(blob)))
+ return false;
+ if (WARN_ON_ONCE(!ctlv_blob_access_live(blob)))
+ return false;
+ if (WARN_ON_ONCE(check_add_overflow(offset, (u64)len, &end) || end > blob->len))
+ return false;
+ return true;
+}
+
+static int ctlv_blob_copy(struct ctlv_blob *blob, u64 offset, void *buf, size_t len, bool write)
+{
+ if (!ctlv_blob_range_ok(blob, offset, len, write))
+ return -EINVAL;
+ if (!len)
+ return 0;
+
+ switch (blob->type) {
+ case CTLV_BLOB_TYPE_VA:
+ ctlv_blob_va_copy(blob, offset, buf, len, write);
+ return 0;
+ default:
+ return -EINVAL;
+ }
+}
+
+/**
+ * ctlv_blob_read - Copy bytes from a resolved input blob.
+ * @blob: resolved blob
+ * @offset: byte offset
+ * @dst: destination buffer
+ * @len: number of bytes
+ *
+ * Return: 0, or a negative error.
+ */
+int ctlv_blob_read(struct ctlv_blob *blob, u64 offset, void *dst, size_t len)
+{
+ return ctlv_blob_copy(blob, offset, dst, len, false);
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_blob_read, "CTLV");
+
+/**
+ * ctlv_blob_write - Copy bytes to a resolved output blob.
+ * @blob: resolved blob
+ * @offset: byte offset
+ * @src: source buffer
+ * @len: number of bytes
+ *
+ * Return: 0, or a negative error.
+ */
+int ctlv_blob_write(struct ctlv_blob *blob, u64 offset, const void *src, size_t len)
+{
+ return ctlv_blob_copy(blob, offset, (void *)src, len, true);
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_blob_write, "CTLV");
+
+/* Any task can release: a VA blob unpins through the mm it pinned from. */
+static int ctlv_blob_release(struct ctlv_blob *blob)
+{
+ switch (blob->type) {
+ case CTLV_BLOB_TYPE_VA:
+ ctlv_blob_va_release(blob);
+ break;
+ }
+ if (blob->ctx) {
+ list_del(&blob->entry);
+ blob->ctx = NULL;
+ }
+ kfree(blob);
+ return 0;
+}
+
+/**
+ * ctlv_blob_put - Release a resolved blob.
+ * @blob: blob returned by a generated resolve helper
+ *
+ * Return: 0, or a negative error.
+ */
+int ctlv_blob_put(struct ctlv_blob *blob)
+{
+ if (!blob)
+ return 0;
+ if (WARN_ON_ONCE(!ctlv_blob_same_task(blob)))
+ return -EINVAL;
+ return ctlv_blob_release(blob);
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_blob_put, "CTLV");
+
+void ctlv_blob_context_init(struct ctlv_op_ctx *ctx)
+{
+ INIT_LIST_HEAD(&ctx->blobs);
+ ctx->blobs_live = true;
+ ctx->blob_error = 0;
+}
+
+void ctlv_blob_context_track(struct ctlv_blob *blob, struct ctlv_op_ctx *ctx)
+{
+ blob->ctx = ctx;
+ list_add_tail(&blob->entry, &ctx->blobs);
+}
+
+int ctlv_blob_context_finish(struct ctlv_op_ctx *ctx)
+{
+ ctx->blobs_live = false;
+ if (WARN_ON_ONCE(!list_empty(&ctx->blobs)) && !ctx->blob_error)
+ ctx->blob_error = -EINVAL;
+ while (!list_empty(&ctx->blobs)) {
+ struct ctlv_blob *blob = list_first_entry(&ctx->blobs, struct ctlv_blob, entry);
+
+ ctlv_blob_release(blob);
+ }
+ return ctx->blob_error;
+}
+
+/*
+ * Which of an op's blobs was the unusable one, in the shape validation would
+ * have used. The errno the family returns says what went wrong.
+ */
+static void ctlv_blob_context_fail(struct ctlv_op_ctx *ctx, u32 tlv_offset, u32 record_offset,
+ u64 required_length)
+{
+ if (ctx->error.reason)
+ return;
+ ctx->error.reason = CTLV_VALIDATION_REASON_BAD_BLOB_DESCRIPTOR;
+ ctx->error.bad_tlv_offset = tlv_offset;
+ ctx->error.record_offset = record_offset;
+ ctx->error.required_length = required_length;
+}
+
+/**
+ * ctlv_blob_too_short - Fail an op on an output blob too short for its data.
+ * @blob: resolved output blob
+ * @len: length the op needs @blob to have, more than it has
+ *
+ * Reports @blob and @len in the op's error information, unless an earlier
+ * failure is reported there already, so that the caller can retry with a
+ * blob that long.
+ *
+ * Return: -EMSGSIZE, for the family to return.
+ */
+int ctlv_blob_too_short(struct ctlv_blob *blob, u64 len)
+{
+ if (WARN_ON_ONCE(!ctlv_blob_writable(blob) || !blob->ctx || len <= blob->len))
+ return -EINVAL;
+ ctlv_blob_context_fail(blob->ctx, blob->tlv_offset, blob->record_offset, len);
+ return -EMSGSIZE;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_blob_too_short, "CTLV");
+
+static struct ctlv_blob *ctlv_blob_acquire(const struct ctlv_blob_desc *desc, u8 direction)
+{
+ u64 address = desc->member[CTLV_BLOB_MEMBER_ADDRESS];
+ u64 len = desc->member[CTLV_BLOB_MEMBER_LENGTH];
+ struct ctlv_blob *blob;
+ int ret;
+
+ blob = kzalloc_obj(*blob, GFP_KERNEL_ACCOUNT);
+ if (!blob)
+ return ERR_PTR(-ENOMEM);
+ INIT_LIST_HEAD(&blob->entry);
+ blob->len = len;
+ blob->direction = direction;
+ blob->type = desc->member[CTLV_BLOB_MEMBER_TYPE];
+
+ ret = ctlv_blob_va_acquire(blob, address);
+ if (ret) {
+ kfree(blob);
+ return ERR_PTR(ret);
+ }
+ return blob;
+}
+
+struct ctlv_blob *ctlv_blob_resolve(struct ctlv_op_ctx *ctx, const struct ctlv_cursor *blob)
+{
+ const struct ctlv_hdr *bad = NULL;
+ const struct ctlv_node_schema *node;
+ u32 tlv_offset, record_offset;
+ struct ctlv_blob_desc desc;
+ struct ctlv_blob *acquired;
+
+ /* An absent blob, or a cursor from some other execution entirely. */
+ if (WARN_ON_ONCE(blob->msg != &ctx->request || !ctlv_cursor_ok(blob)))
+ return ERR_PTR(-EINVAL);
+ node = &blob->tree->node_schemas[blob->node];
+ if (WARN_ON_ONCE(node->kind != CTLV_SCHEMA_BLOB))
+ return ERR_PTR(-EINVAL);
+ if (WARN_ON_ONCE(!ctx->blobs_live || !ctx->submitter || ctx->submitter != current))
+ return ERR_PTR(-EINVAL);
+ /* Validated before the callback, so anything wrong with it is a bug. */
+ if (WARN_ON_ONCE(ctlv_blob_desc_read(blob->hdr, &desc, &bad) ||
+ ctlv_blob_desc_check(&desc, node)))
+ return ERR_PTR(-EINVAL);
+
+ tlv_offset = ctlv_message_offset(blob->msg, blob->hdr);
+ record_offset = ctlv_message_offset(blob->msg, blob->parent);
+ acquired = ctlv_blob_acquire(&desc, node->blob.direction);
+ if (IS_ERR(acquired)) {
+ ctlv_blob_context_fail(ctx, tlv_offset, record_offset, 0);
+ return acquired;
+ }
+ acquired->tlv_offset = tlv_offset;
+ acquired->record_offset = record_offset;
+ ctlv_blob_context_track(acquired, ctx);
+ return acquired;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_blob_resolve, "CTLV");
diff --git a/lib/ctlv/blob.h b/lib/ctlv/blob.h
new file mode 100644
index 000000000000..bb7b933a1ced
--- /dev/null
+++ b/lib/ctlv/blob.h
@@ -0,0 +1,15 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef _CTLV_BLOB_H
+#define _CTLV_BLOB_H
+
+#include <linux/types.h>
+
+struct ctlv_blob;
+struct ctlv_op_ctx;
+
+void ctlv_blob_context_init(struct ctlv_op_ctx *ctx);
+void ctlv_blob_context_track(struct ctlv_blob *blob,
+ struct ctlv_op_ctx *ctx);
+int ctlv_blob_context_finish(struct ctlv_op_ctx *ctx);
+
+#endif /* _CTLV_BLOB_H */
diff --git a/lib/ctlv/internal.h b/lib/ctlv/internal.h
index 90d07d2638d3..1dbab9709261 100644
--- a/lib/ctlv/internal.h
+++ b/lib/ctlv/internal.h
@@ -9,6 +9,7 @@
#include <linux/bug.h>
#include <linux/ctlv_family.h>
+#include <linux/list.h>
#include <linux/types.h>
#include <uapi/ctlv/ctlv.h>
@@ -22,6 +23,8 @@
#define CTLV_ASSERT(cond) BUILD_BUG_ON_INVALID(cond)
#endif
+struct task_struct;
+
#define CTLV_ERROR_MESSAGE_LEN 256
/* An op that is always enabled has no per-device availability state. */
@@ -58,6 +61,8 @@ struct ctlv_query_fragment {
struct ctlv_op_ctx {
const struct ctlv_family *family;
+ /* Compared against current and never followed: identity, not a hold. */
+ const struct task_struct *submitter;
const struct ctlv_op_entry *op;
struct ctlv_msg request;
@@ -68,6 +73,9 @@ struct ctlv_op_ctx {
struct ctlv_cursor *deepest;
struct ctlv_query_fragment frag;
+ struct list_head blobs;
+ bool blobs_live;
+ int blob_error;
struct ctlv_error error;
};
--
2.56.0
^ permalink raw reply related [flat|nested] 24+ messages in thread
* [PATCH RFC v2 10/15] ctlv: Prepare runtime introspection
2026-11-13 15:54 [PATCH RFC v2 00/15] ctlv: Add a schema-driven TLV uAPI for character devices Jiri Pirko
` (8 preceding siblings ...)
2026-11-13 15:55 ` [PATCH RFC v2 09/15] ctlv: Prepare VA blob handling Jiri Pirko
@ 2026-11-13 15:55 ` Jiri Pirko
2026-11-13 16:24 ` sashiko-bot
2026-11-13 15:55 ` [PATCH RFC v2 11/15] ctlv: Add operation dispatch Jiri Pirko
` (4 subsequent siblings)
14 siblings, 1 reply; 24+ messages in thread
From: Jiri Pirko @ 2026-11-13 15:55 UTC (permalink / raw)
To: linux-coco
Cc: linux-kernel, linux-api, x86, kvm, linux-doc, jgg, david.quigley,
lukas, ksinyuk, rodrigo.vivi, cedric.xing, sameo, drdeeglaze,
James.Bottomley, aik, jpiotrowski, qinkun, pgonda, jic23,
alistair.francis, ankita, suzuki.poulose, kas, rick.p.edgecombe,
elena.reshetova, dave.hansen, sathyanarayanan.kuppuswamy,
peter.fang, jarkko, zohar, yeoreum.yun, aneesh.kumar, dave.jiang,
nik.borisov, sergii.dmytruk
From: Jiri Pirko <jiri@nvidia.com>
Add handlers that expose registered families, effective operations and
message schemas through the CTLV root family. Start struct ctlv with the
device, family, operations and op state they read.
The introspection object can be compile-checked here. CONFIG_CTLV
remains unwired until operation dispatch completes the module.
Assisted-by: Cursor: GPT-5.6 Sol
Assisted-by: Cursor: Claude Opus 5.5
Signed-off-by: Jiri Pirko <jiri@nvidia.com>
---
RFC->RFCv2:
- handed the introspection ops the CTLV instance
- reported each op's error information length
- emitted introspection fields without staging them
- kept each guard() on one line
- gave each dump cursor field its own mask and said what it holds
- took struct ctlv from the event patch, without the event state
- dropped the event record length from op-get
- renamed stubs to extension points
- made a check no error path depends on a CTLV_ASSERT()
- said TLV, type, record and field instead of attr
- said record instead of nest, the root being one
- said node instead of placement
- numbered every enum member
- reported the values the device's family chain gives an enum
extension point
- moved to lib/ctlv/
---
include/linux/ctlv.h | 26 ++
lib/ctlv/Makefile | 3 +-
lib/ctlv/internal.h | 3 +
lib/ctlv/introspection.c | 505 +++++++++++++++++++++++++++++++++++++++
4 files changed, 536 insertions(+), 1 deletion(-)
create mode 100644 lib/ctlv/introspection.c
diff --git a/include/linux/ctlv.h b/include/linux/ctlv.h
index afb71cc643d3..3847f8411edf 100644
--- a/include/linux/ctlv.h
+++ b/include/linux/ctlv.h
@@ -7,6 +7,8 @@
#ifndef _LINUX_CTLV_H
#define _LINUX_CTLV_H
+#include <linux/device.h>
+#include <linux/spinlock_types.h>
#include <linux/types.h>
#include <uapi/ctlv/ctlv.h>
@@ -15,6 +17,7 @@ struct ctlv_family;
struct ctlv;
struct ctlv_msg;
struct ctlv_op_ctx;
+struct ctlv_op_entry;
struct ctlv_schema_tree;
/**
@@ -57,6 +60,29 @@ struct ctlv_cursor {
bool progress;
};
+/**
+ * struct ctlv - The CTLV state of one device, embedded by its subsystem.
+ * @dev: device whose release frees the structure embedding this instance;
+ * every open file pins it
+ *
+ * A family callback is handed the instance and reaches its own state with
+ * container_of().
+ */
+struct ctlv {
+ struct device *dev;
+ /* private: */
+ const struct ctlv_family *family;
+
+ struct ctlv_op_entry *ops;
+ u32 ops_count;
+
+ /* Op publication and state writers, never the dispatch path. */
+ spinlock_t transition_lock;
+ u64 state_generation;
+ u32 dynamic_ops_count;
+ unsigned long *disabled;
+};
+
void ctlv_record_end(struct ctlv_cursor *child);
void ctlv_record_cancel(struct ctlv_cursor *child);
diff --git a/lib/ctlv/Makefile b/lib/ctlv/Makefile
index 4455062f83d7..76010d52dd8a 100644
--- a/lib/ctlv/Makefile
+++ b/lib/ctlv/Makefile
@@ -1,4 +1,5 @@
# SPDX-License-Identifier: GPL-2.0
obj-$(CONFIG_CTLV) += ctlv.o
-ctlv-y += blob.o blob_desc.o builder.o message.o schema.o
+ctlv-y += blob.o blob_desc.o builder.o introspection.o message.o \
+ schema.o
diff --git a/lib/ctlv/internal.h b/lib/ctlv/internal.h
index 1dbab9709261..2e1e36799a17 100644
--- a/lib/ctlv/internal.h
+++ b/lib/ctlv/internal.h
@@ -60,6 +60,7 @@ struct ctlv_query_fragment {
};
struct ctlv_op_ctx {
+ struct ctlv *ctlv;
const struct ctlv_family *family;
/* Compared against current and never followed: identity, not a hold. */
const struct task_struct *submitter;
@@ -79,4 +80,6 @@ struct ctlv_op_ctx {
struct ctlv_error error;
};
+struct ctlv_op_entry *ctlv_op_find(struct ctlv *ctlv, u64 op_id);
+
#endif /* _CTLV_INTERNAL_H */
diff --git a/lib/ctlv/introspection.c b/lib/ctlv/introspection.c
new file mode 100644
index 000000000000..d030debe69e3
--- /dev/null
+++ b/lib/ctlv/introspection.c
@@ -0,0 +1,505 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES
+ *
+ * Root-family discovery queries and their fragmentation.
+ */
+
+#include <linux/bitfield.h>
+#include <linux/bitmap.h>
+#include <linux/bits.h>
+#include <linux/cleanup.h>
+#include <linux/ctlv.h>
+#include <linux/ctlv_gen.h>
+#include <linux/errno.h>
+#include <linux/slab.h>
+#include <linux/spinlock.h>
+#include <linux/types.h>
+
+#include "internal.h"
+#include "schema.h"
+
+/* A cursor packs the entry and member where fragmentation resumes. */
+#define CTLV_INTROSPECTION_MEMBER_MASK GENMASK_U64(63, 48)
+#define CTLV_INTROSPECTION_ENTRY_MASK GENMASK_U64(47, 0)
+
+/*
+ * family-get: the entry is a family's position in the chain from the root,
+ * the member its next field. op-get: the entry indexes the published ops,
+ * the member is the next CTLV_INTROSPECTION_OP_FIELD_*.
+ */
+struct ctlv_introspection_dump_position {
+ u64 entry;
+ u32 member;
+};
+
+static void ctlv_introspection_dump_resume(struct ctlv_introspection_dump_position *pos, u64 cursor)
+{
+ pos->entry = FIELD_GET(CTLV_INTROSPECTION_ENTRY_MASK, cursor);
+ pos->member = FIELD_GET(CTLV_INTROSPECTION_MEMBER_MASK, cursor);
+}
+
+static u64 ctlv_introspection_dump_cursor(const struct ctlv_introspection_dump_position *pos)
+{
+ return FIELD_PREP(CTLV_INTROSPECTION_ENTRY_MASK, pos->entry) |
+ FIELD_PREP(CTLV_INTROSPECTION_MEMBER_MASK, pos->member);
+}
+
+static bool ctlv_introspection_family_entry(struct ctlv_cursor *reply, struct ctlv_cursor *entry,
+ const struct ctlv_family *family,
+ struct ctlv_introspection_dump_position *pos)
+{
+ const struct ctlv_family_schema *schema = family->schema;
+
+ if (!ctlv_family_get_families_start(reply, entry))
+ return false;
+ if (pos->member == 0) {
+ if (!ctlv_family_get_families_id_put(entry, schema->family_id))
+ return false;
+ pos->member = 1;
+ }
+ if (pos->member == 1) {
+ if (!ctlv_family_get_families_name_put(entry, schema->name))
+ return false;
+ pos->member = 2;
+ }
+ ctlv_record_end(entry);
+ pos->member = 0;
+ return true;
+}
+
+int ctlv_family_get_query(struct ctlv *ctlv, struct ctlv_op_ctx *ctx,
+ const struct ctlv_cursor *request, struct ctlv_cursor *reply)
+{
+ const struct ctlv_family *family = ctx->family;
+ u32 levels = ctlv_family_levels(family);
+ struct ctlv_cursor entry;
+ struct ctlv_introspection_dump_position pos;
+
+ ctlv_introspection_dump_resume(&pos, ctlv_op_query_cursor(ctx));
+ /* The immutable root-first chain always has generation zero. */
+ for (; pos.entry < levels; pos.entry++) {
+ const struct ctlv_family *ancestor =
+ ctlv_family_ancestor(family, levels - 1 - pos.entry);
+
+ if (!ctlv_introspection_family_entry(reply, &entry, ancestor, &pos))
+ return ctlv_op_query_more(ctx, 0, ctlv_introspection_dump_cursor(&pos));
+ }
+ return ctlv_op_query_done(ctx, 0);
+}
+
+/* Snapshot published-op state and generation once per fragment. */
+struct ctlv_op_snapshot {
+ unsigned long *disabled;
+ u64 generation;
+};
+
+static int ctlv_introspection_snapshot_take(struct ctlv *ctlv,
+ struct ctlv_op_snapshot *snapshot)
+{
+ if (ctlv->dynamic_ops_count) {
+ snapshot->disabled = bitmap_zalloc(ctlv->dynamic_ops_count, GFP_KERNEL);
+ if (!snapshot->disabled)
+ return -ENOMEM;
+ }
+ guard(spinlock)(&ctlv->transition_lock);
+ snapshot->generation = ctlv->state_generation;
+ if (snapshot->disabled)
+ bitmap_copy(snapshot->disabled, ctlv->disabled, ctlv->dynamic_ops_count);
+ return 0;
+}
+
+static bool ctlv_introspection_op_disabled(const struct ctlv_op_snapshot *snapshot,
+ const struct ctlv_op_entry *op)
+{
+ if (op->state_index == CTLV_OP_STATE_NONE)
+ return false;
+ return test_bit(op->state_index, snapshot->disabled);
+}
+
+/* Report effective lengths, including descendant fills. */
+enum ctlv_introspection_op_field {
+ CTLV_INTROSPECTION_OP_FIELD_ID = 0,
+ CTLV_INTROSPECTION_OP_FIELD_TYPE = 1,
+ CTLV_INTROSPECTION_OP_FIELD_AVAILABILITY = 2,
+ CTLV_INTROSPECTION_OP_FIELD_DISABLED = 3,
+ CTLV_INTROSPECTION_OP_FIELD_REQUEST_MAX_LEN = 4,
+ CTLV_INTROSPECTION_OP_FIELD_ACTION_REPLY_MAX_LEN = 5,
+ CTLV_INTROSPECTION_OP_FIELD_QUERY_REPLY_MIN_LEN = 6,
+ CTLV_INTROSPECTION_OP_FIELD_QUERY_REPLY_SUGGESTED_LEN = 7,
+ CTLV_INTROSPECTION_OP_FIELD_FD_ACCESS = 8,
+ CTLV_INTROSPECTION_OP_FIELD_ERROR_INFO_MAX_LEN = 9,
+ CTLV_INTROSPECTION_OP_FIELD_END = 10,
+};
+
+static bool ctlv_introspection_op_field_put(struct ctlv_cursor *entry,
+ const struct ctlv_op_snapshot *snapshot,
+ const struct ctlv_op_entry *op, u32 member)
+{
+ const struct ctlv_op_sizes *sizes = &op->sizes;
+
+ switch (member) {
+ case CTLV_INTROSPECTION_OP_FIELD_ID:
+ return ctlv_op_get_ops_id_put(entry, op->schema->op_id);
+ case CTLV_INTROSPECTION_OP_FIELD_TYPE:
+ return ctlv_op_get_ops_type_put(entry, op->schema->type);
+ case CTLV_INTROSPECTION_OP_FIELD_AVAILABILITY:
+ return ctlv_op_get_ops_availability_put(entry, op->schema->availability);
+ case CTLV_INTROSPECTION_OP_FIELD_DISABLED:
+ return ctlv_op_get_ops_disabled_put(entry,
+ ctlv_introspection_op_disabled(snapshot, op));
+ case CTLV_INTROSPECTION_OP_FIELD_REQUEST_MAX_LEN:
+ return ctlv_op_get_ops_request_max_len_put(entry, sizes->request_max_len);
+ case CTLV_INTROSPECTION_OP_FIELD_ACTION_REPLY_MAX_LEN:
+ /* Zero is a reply that acknowledges and no more. */
+ return ctlv_op_get_ops_action_reply_max_len_put(entry,
+ sizes->action_reply_max_len);
+ case CTLV_INTROSPECTION_OP_FIELD_QUERY_REPLY_MIN_LEN:
+ return ctlv_op_get_ops_query_reply_min_len_put(entry, sizes->query_reply_min_len);
+ case CTLV_INTROSPECTION_OP_FIELD_QUERY_REPLY_SUGGESTED_LEN:
+ return ctlv_op_get_ops_query_reply_suggested_len_put(
+ entry, sizes->query_reply_suggested_len);
+ case CTLV_INTROSPECTION_OP_FIELD_FD_ACCESS:
+ return ctlv_op_get_ops_fd_access_put(entry, op->schema->fd_access);
+ default:
+ return ctlv_op_get_ops_error_info_max_len_put(entry, CTLV_ERROR_INFO_MAX_LEN);
+ }
+}
+
+static bool ctlv_introspection_op_field_applies(const struct ctlv_op_entry *op, u32 member)
+{
+ switch (member) {
+ case CTLV_INTROSPECTION_OP_FIELD_ID:
+ case CTLV_INTROSPECTION_OP_FIELD_TYPE:
+ case CTLV_INTROSPECTION_OP_FIELD_AVAILABILITY:
+ case CTLV_INTROSPECTION_OP_FIELD_DISABLED:
+ case CTLV_INTROSPECTION_OP_FIELD_REQUEST_MAX_LEN:
+ case CTLV_INTROSPECTION_OP_FIELD_FD_ACCESS:
+ case CTLV_INTROSPECTION_OP_FIELD_ERROR_INFO_MAX_LEN:
+ return true;
+ case CTLV_INTROSPECTION_OP_FIELD_ACTION_REPLY_MAX_LEN:
+ return op->schema->type == CTLV_OP_TYPE_ACTION;
+ default:
+ return op->schema->type == CTLV_OP_TYPE_QUERY;
+ }
+}
+
+static bool ctlv_introspection_op_entry(struct ctlv_cursor *reply, struct ctlv_cursor *entry,
+ const struct ctlv_op_snapshot *snapshot,
+ const struct ctlv_op_entry *op,
+ struct ctlv_introspection_dump_position *pos)
+{
+ if (!ctlv_op_get_ops_start(reply, entry))
+ return false;
+ for (; pos->member < CTLV_INTROSPECTION_OP_FIELD_END; pos->member++) {
+ if (!ctlv_introspection_op_field_applies(op, pos->member))
+ continue;
+ if (!ctlv_introspection_op_field_put(entry, snapshot, op, pos->member))
+ return false;
+ }
+ ctlv_record_end(entry);
+ pos->member = 0;
+ return true;
+}
+
+int ctlv_op_get_query(struct ctlv *ctlv, struct ctlv_op_ctx *ctx,
+ const struct ctlv_cursor *request, struct ctlv_cursor *reply)
+{
+ struct ctlv_op_snapshot snapshot = {};
+ struct ctlv_cursor entry;
+ struct ctlv_introspection_dump_position pos;
+ int ret;
+
+ ret = ctlv_introspection_snapshot_take(ctlv, &snapshot);
+ if (ret)
+ return ret;
+ unsigned long *disabled __free(bitmap) = snapshot.disabled;
+
+ /* The published index is immutable, so a cursor into it holds. */
+ ctlv_introspection_dump_resume(&pos, ctlv_op_query_cursor(ctx));
+ for (; pos.entry < ctlv->ops_count; pos.entry++)
+ if (!ctlv_introspection_op_entry(reply, &entry, &snapshot,
+ &ctlv->ops[pos.entry], &pos))
+ return ctlv_op_query_more(ctx, snapshot.generation,
+ ctlv_introspection_dump_cursor(&pos));
+ return ctlv_op_query_done(ctx, snapshot.generation);
+}
+
+/* A leaf-count cursor permits a fragment boundary at any tree depth. */
+
+/* One record of the description, standing for one node of the schema. */
+struct ctlv_introspection_frame {
+ struct ctlv_cursor record;
+ u64 type;
+ bool open;
+ /* Which node this stands for, so that a re-entry can be measured. */
+ const struct ctlv_schema_tree *tree;
+ u32 node;
+};
+
+/* The frames outlive the walk, as a fragment's open records must stay alive. */
+struct ctlv_introspection_walk {
+ const struct ctlv_family *family;
+ struct ctlv_cursor *root;
+ struct ctlv_introspection_frame *frames;
+ u64 ordinal;
+ u64 resume;
+ int error;
+};
+
+/* A record is opened by the first thing written inside it. */
+static bool ctlv_introspection_open(struct ctlv_introspection_walk *walk, u32 depth)
+{
+ struct ctlv_introspection_frame *frame = &walk->frames[depth];
+ struct ctlv_cursor *up = walk->root;
+
+ if (frame->open)
+ return true;
+ if (depth) {
+ if (!ctlv_introspection_open(walk, depth - 1))
+ return false;
+ up = &walk->frames[depth - 1].record;
+ }
+ if (!ctlv_record_start(up, &frame->record, frame->type, CTLV_SCHEMA_RECORD))
+ return false;
+ frame->open = true;
+ return true;
+}
+
+/* Skipped leaves advance the ordinal; a failed write remains the resume point. */
+static bool ctlv_introspection_put(struct ctlv_introspection_walk *walk, u32 depth, u64 type,
+ u8 kind, u64 value)
+{
+ struct ctlv_cursor *record = &walk->frames[depth].record;
+ bool ok;
+
+ if (walk->ordinal < walk->resume) {
+ walk->ordinal++;
+ return true;
+ }
+ if (!ctlv_introspection_open(walk, depth))
+ return false;
+ if (kind == CTLV_SCHEMA_SINT)
+ ok = ctlv_put_s64(record, type, kind, value);
+ else
+ ok = ctlv_put_u64(record, type, kind, value);
+ if (!ok)
+ return false;
+ walk->ordinal++;
+ return true;
+}
+
+static bool ctlv_introspection_uint(struct ctlv_introspection_walk *walk, u32 depth, u64 type,
+ u64 value)
+{
+ return ctlv_introspection_put(walk, depth, type, CTLV_SCHEMA_UINT, value);
+}
+
+/* Emit only constraints for this node's schema kind. */
+static bool ctlv_introspection_node_fields(struct ctlv_introspection_walk *walk, u32 depth,
+ const struct ctlv_node_schema *node, u32 reenters)
+{
+ if (!ctlv_introspection_uint(walk, depth, CTLV_T_SCHEMA_NODE_TYPE, node->type) ||
+ !ctlv_introspection_uint(walk, depth, CTLV_T_SCHEMA_NODE_KIND, node->kind) ||
+ !ctlv_introspection_uint(walk, depth, CTLV_T_SCHEMA_NODE_MAX_OCCURRENCES,
+ node->max_occurrences))
+ return false;
+
+ switch (node->kind) {
+ case CTLV_SCHEMA_ENUM:
+ /* An extension point's maximum is 0 when nothing filled it. */
+ if (!ctlv_introspection_uint(walk, depth, CTLV_T_SCHEMA_NODE_MIN,
+ node->is_enum_ext ? 1 : node->range.min) ||
+ !ctlv_introspection_uint(walk, depth, CTLV_T_SCHEMA_NODE_MAX,
+ ctlv_schema_enum_max(walk->family, node)))
+ return false;
+ break;
+ case CTLV_SCHEMA_UINT:
+ case CTLV_SCHEMA_SINT:
+ if (!ctlv_introspection_uint(walk, depth, CTLV_T_SCHEMA_NODE_MIN,
+ node->range.min) ||
+ !ctlv_introspection_uint(walk, depth, CTLV_T_SCHEMA_NODE_MAX,
+ node->range.max))
+ return false;
+ break;
+ case CTLV_SCHEMA_FLAGS:
+ if (!ctlv_introspection_uint(walk, depth, CTLV_T_SCHEMA_NODE_FLAGS_MASK,
+ node->flags_mask))
+ return false;
+ break;
+ case CTLV_SCHEMA_STRING:
+ case CTLV_SCHEMA_BINARY:
+ if (!ctlv_introspection_uint(walk, depth, CTLV_T_SCHEMA_NODE_MIN_LENGTH,
+ node->length.min_len) ||
+ !ctlv_introspection_uint(walk, depth, CTLV_T_SCHEMA_NODE_MAX_LENGTH,
+ node->length.max_len))
+ return false;
+ break;
+ case CTLV_SCHEMA_BLOB:
+ if (!ctlv_introspection_put(walk, depth, CTLV_T_SCHEMA_NODE_BLOB_DIRECTION,
+ CTLV_SCHEMA_ENUM, node->blob.direction) ||
+ !ctlv_introspection_uint(walk, depth, CTLV_T_SCHEMA_NODE_BLOB_BACKINGS,
+ node->blob.backings) ||
+ !ctlv_introspection_uint(walk, depth, CTLV_T_SCHEMA_NODE_BLOB_MAX_LENGTH,
+ node->blob.max_len) ||
+ !ctlv_introspection_uint(walk, depth, CTLV_T_SCHEMA_NODE_BLOB_ALIGNMENT,
+ node->blob.alignment))
+ return false;
+ break;
+ default:
+ break;
+ }
+
+ if (node->unit != CTLV_UNIT_NONE &&
+ (!ctlv_introspection_uint(walk, depth, CTLV_T_SCHEMA_NODE_UNIT, node->unit) ||
+ !ctlv_introspection_put(walk, depth, CTLV_T_SCHEMA_NODE_SCALE, CTLV_SCHEMA_SINT,
+ node->scale)))
+ return false;
+ return !reenters ||
+ ctlv_introspection_uint(walk, depth, CTLV_T_SCHEMA_NODE_REENTERS, reenters);
+}
+
+/* Re-entry targets an ancestor with the same child range. */
+static u32 ctlv_introspection_reenters(struct ctlv_introspection_walk *walk, u32 depth,
+ const struct ctlv_schema_tree *tree,
+ const struct ctlv_node_schema *node)
+{
+ u32 levels;
+
+ for (levels = 1; levels <= depth; levels++) {
+ const struct ctlv_introspection_frame *up = &walk->frames[depth - levels];
+
+ if (up->tree == tree &&
+ tree->node_schemas[up->node].first_child == node->first_child)
+ return levels;
+ }
+ /* A back edge to nothing is metadata this core cannot describe. */
+ CTLV_ASSERT(0);
+ walk->error = -EINVAL;
+ return 0;
+}
+
+static bool ctlv_introspection_node(struct ctlv_introspection_walk *walk, u32 depth,
+ const struct ctlv_schema_tree *tree, u32 index);
+
+/* The members of a fill inherit the optionality of its extension point. */
+static bool ctlv_introspection_children(struct ctlv_introspection_walk *walk, u32 depth,
+ const struct ctlv_schema_tree *tree, u32 index,
+ bool force_optional)
+{
+ const struct ctlv_node_schema *node = &tree->node_schemas[index];
+ u32 i;
+
+ if (WARN_ON_ONCE(depth + 1 >= CTLV_MAX_RECORD_DEPTH)) {
+ /* Deeper than a specification may declare, so unreachable. */
+ walk->error = -EINVAL;
+ return false;
+ }
+ for (i = 0; i < node->children_count; i++) {
+ u32 child = node->first_child + i;
+ bool required = !force_optional && i < node->required_children_count;
+
+ walk->frames[depth + 1] = (struct ctlv_introspection_frame){
+ .type = required ? CTLV_T_SCHEMA_NODE_FIELDS :
+ CTLV_T_SCHEMA_NODE_OPTIONAL_FIELDS,
+ .tree = tree,
+ .node = child,
+ };
+ if (!ctlv_introspection_node(walk, depth + 1, tree, child))
+ return false;
+ }
+ for (i = 0; i < tree->ext_schemas_count; i++) {
+ const struct ctlv_ext_schema *ext_schema = &tree->ext_schemas[i];
+ const struct ctlv_ext_fill_schema *fill_schema;
+
+ if (ext_schema->node != index)
+ continue;
+ fill_schema =
+ ctlv_schema_fill(walk->family, ext_schema->family_id, ext_schema->ext_id);
+ /* Nothing of this chain filled it, so it contributes none. */
+ if (!fill_schema)
+ continue;
+ /* Its root is the extension point, its children the members. */
+ if (!ctlv_introspection_children(walk, depth, &fill_schema->members, 0,
+ force_optional || ext_schema->optional))
+ return false;
+ }
+ return true;
+}
+
+/* One node, into the record that stands for it. */
+static bool ctlv_introspection_node(struct ctlv_introspection_walk *walk, u32 depth,
+ const struct ctlv_schema_tree *tree, u32 index)
+{
+ const struct ctlv_node_schema *node = &tree->node_schemas[index];
+ u32 reenters = 0;
+
+ if (node->reenters) {
+ reenters = ctlv_introspection_reenters(walk, depth, tree, node);
+ if (!reenters)
+ return false;
+ }
+ if (!ctlv_introspection_node_fields(walk, depth, node, reenters))
+ return false;
+ /*
+ * A node that re-enters a level says so instead of listing that level's
+ * members again, which is what makes unbounded depth describable.
+ */
+ if (!node->reenters && !ctlv_introspection_children(walk, depth, tree, index, false))
+ return false;
+ if (walk->frames[depth].open)
+ ctlv_record_end(&walk->frames[depth].record);
+ return true;
+}
+
+/* One of the two messages of an op, the root of it being a node like any. */
+static bool ctlv_introspection_message(struct ctlv_introspection_walk *walk, u64 type,
+ const struct ctlv_schema_tree *tree)
+{
+ /* An op without such a message has nothing to describe. */
+ if (!tree->node_schemas)
+ return true;
+ walk->frames[0] = (struct ctlv_introspection_frame){
+ .type = type,
+ .tree = tree,
+ };
+ return ctlv_introspection_node(walk, 0, tree, 0);
+}
+
+int ctlv_op_schema_get_query(struct ctlv *ctlv, struct ctlv_op_ctx *ctx,
+ const struct ctlv_cursor *request, struct ctlv_cursor *reply)
+{
+ struct ctlv_introspection_walk walk = {
+ .family = ctx->family,
+ .root = reply,
+ .resume = ctlv_op_query_cursor(ctx),
+ };
+ const struct ctlv_op_entry *op;
+
+ /* Describe published ops regardless of current dynamic state. */
+ op = ctlv_op_find(ctx->ctlv, ctlv_op_schema_get_id_get(request));
+ if (!op)
+ return -EOPNOTSUPP;
+
+ /*
+ * One frame per level the reply may reach, too many for the stack: a
+ * description is one level deeper than the message it describes.
+ */
+ struct ctlv_introspection_frame *frames __free(kfree) =
+ kcalloc(CTLV_MAX_RECORD_DEPTH, sizeof(*frames), GFP_KERNEL);
+
+ if (!frames)
+ return -ENOMEM;
+ walk.frames = frames;
+
+ if (!ctlv_introspection_message(&walk, CTLV_T_OP_SCHEMA_GET_REQUEST,
+ &op->schema->request) ||
+ !ctlv_introspection_message(&walk, CTLV_T_OP_SCHEMA_GET_REPLY,
+ &op->schema->reply)) {
+ if (walk.error)
+ return walk.error;
+ /* A schema is immutable, so there is nothing to be stale. */
+ return ctlv_op_query_more(ctx, 0, walk.ordinal);
+ }
+ return ctlv_op_query_done(ctx, 0);
+}
--
2.56.0
^ permalink raw reply related [flat|nested] 24+ messages in thread
* [PATCH RFC v2 11/15] ctlv: Add operation dispatch
2026-11-13 15:54 [PATCH RFC v2 00/15] ctlv: Add a schema-driven TLV uAPI for character devices Jiri Pirko
` (9 preceding siblings ...)
2026-11-13 15:55 ` [PATCH RFC v2 10/15] ctlv: Prepare runtime introspection Jiri Pirko
@ 2026-11-13 15:55 ` Jiri Pirko
2026-11-13 16:29 ` sashiko-bot
2026-11-13 15:55 ` [PATCH RFC v2 12/15] ctlv: Add character device nodes Jiri Pirko
` (3 subsequent siblings)
14 siblings, 1 reply; 24+ messages in thread
From: Jiri Pirko @ 2026-11-13 15:55 UTC (permalink / raw)
To: linux-coco
Cc: linux-kernel, linux-api, x86, kvm, linux-doc, jgg, david.quigley,
lukas, ksinyuk, rodrigo.vivi, cedric.xing, sameo, drdeeglaze,
James.Bottomley, aik, jpiotrowski, qinkun, pgonda, jic23,
alistair.francis, ankita, suzuki.poulose, kas, rick.p.edgecombe,
elena.reshetova, dave.hansen, sathyanarayanan.kuppuswamy,
peter.fang, jarkko, zohar, yeoreum.yun, aneesh.kumar, dave.jiang,
nik.borisov, sergii.dmytruk
From: Jiri Pirko <jiri@nvidia.com>
Let a subsystem embed a CTLV instance in a device of its own, resolve
family chains and dispatch operations through validated messages. Wire
CONFIG_CTLV and the generated root family into lib/ after all module
objects are present.
Assisted-by: Cursor: GPT-5.6 Sol
Assisted-by: Cursor: Claude Opus 5.5
Signed-off-by: Jiri Pirko <jiri@nvidia.com>
---
RFC->RFCv2:
- let the subsystem allocate the device and embed CTLV, as fwctl does
- let a subsystem route the CTLV ioctl from its own file operations
- moved to lib/ctlv/, and the device nodes to the next patch
- dropped the family lifecycle hooks and the per-family privs
- used only gotos in ctlv_op_exec()
- read struct ctlv_op_exec with copy_struct_from_user()
- took a reply buffer of any length
- made plain what the breaks in ctlv_file_open() leave
- kept each guard() on one line
- moved the function kernel-doc to the definitions
- dropped read(), poll() and the op state and removal events
- renamed ctlv_stub_fill() to ctlv_ext_fill()
- said TLV, type, record and field instead of attr
- said record instead of nest, the root being one
- reported the length an output blob needs as required-length
- separated the steps of the longer functions
- took the UAPI headers from include/uapi/ctlv/
- said message instead of wire
- registered the ioctl number, which this patch handles first
---
.../userspace-api/ioctl/ioctl-number.rst | 2 +
include/linux/ctlv.h | 33 +
include/linux/ctlv_family.h | 22 +
lib/Kconfig | 2 +
lib/Makefile | 2 +
lib/ctlv/Makefile | 4 +-
lib/ctlv/core.c | 1152 +++++++++++++++++
7 files changed, 1215 insertions(+), 2 deletions(-)
create mode 100644 lib/ctlv/core.c
diff --git a/Documentation/userspace-api/ioctl/ioctl-number.rst b/Documentation/userspace-api/ioctl/ioctl-number.rst
index dbe462040999..f257b1a22ec2 100644
--- a/Documentation/userspace-api/ioctl/ioctl-number.rst
+++ b/Documentation/userspace-api/ioctl/ioctl-number.rst
@@ -387,6 +387,8 @@ Code Seq# Include File Comments
<mailto:linux-hyperv@vger.kernel.org>
0xBA 00-0F uapi/linux/liveupdate.h Pasha Tatashin
<mailto:pasha.tatashin@soleen.com>
+0xBB 00-0F uapi/ctlv/ctlv.h CTLV
+ <mailto:jiri@resnulli.us>
0xC0 00-0F linux/usb/iowarrior.h
0xCA 00-0F uapi/misc/cxl.h Dead since 6.15
0xCA 10-2F uapi/misc/ocxl.h
diff --git a/include/linux/ctlv.h b/include/linux/ctlv.h
index 3847f8411edf..f075d0a99b80 100644
--- a/include/linux/ctlv.h
+++ b/include/linux/ctlv.h
@@ -8,17 +8,20 @@
#define _LINUX_CTLV_H
#include <linux/device.h>
+#include <linux/rwsem.h>
#include <linux/spinlock_types.h>
#include <linux/types.h>
#include <uapi/ctlv/ctlv.h>
struct ctlv_family;
+struct ctlv_file;
struct ctlv;
struct ctlv_msg;
struct ctlv_op_ctx;
struct ctlv_op_entry;
struct ctlv_schema_tree;
+struct file;
/**
* struct ctlv_cursor - One TLV position in one message.
@@ -60,6 +63,16 @@ struct ctlv_cursor {
bool progress;
};
+/**
+ * struct ctlv_hooks - What a device runs around the ops of its instance.
+ * @pre: entered before every op the core does not answer, and may refuse it
+ * @post: entered after every op whose @pre succeeded
+ */
+struct ctlv_hooks {
+ int (*pre)(struct ctlv *ctlv, struct ctlv_op_ctx *ctx);
+ void (*post)(struct ctlv *ctlv, struct ctlv_op_ctx *ctx);
+};
+
/**
* struct ctlv - The CTLV state of one device, embedded by its subsystem.
* @dev: device whose release frees the structure embedding this instance;
@@ -71,6 +84,9 @@ struct ctlv_cursor {
struct ctlv {
struct device *dev;
/* private: */
+ const struct ctlv_hooks *hooks;
+ struct rw_semaphore registration_lock;
+ bool registered;
const struct ctlv_family *family;
struct ctlv_op_entry *ops;
@@ -78,14 +94,31 @@ struct ctlv {
/* Op publication and state writers, never the dispatch path. */
spinlock_t transition_lock;
+ bool published;
+ bool destroying;
u64 state_generation;
u32 dynamic_ops_count;
unsigned long *disabled;
};
+int ctlv_init(struct ctlv *ctlv, struct device *dev,
+ const struct ctlv_family *family, const struct ctlv_hooks *hooks);
+void ctlv_publish(struct ctlv *ctlv);
+void ctlv_unpublish(struct ctlv *ctlv);
+void ctlv_fini(struct ctlv *ctlv);
+bool ctlv_family_inherits(const struct ctlv_family *family,
+ const struct ctlv_family *ancestor);
+
+struct ctlv_file *ctlv_file_open(struct ctlv *ctlv, struct file *filp);
+long ctlv_file_ioctl(struct ctlv_file *cfile, unsigned int cmd,
+ unsigned long arg);
+void ctlv_file_close(struct ctlv_file *cfile);
+
void ctlv_record_end(struct ctlv_cursor *child);
void ctlv_record_cancel(struct ctlv_cursor *child);
+int ctlv_ext_fill(struct ctlv_op_ctx *ctx, const struct ctlv_cursor *request,
+ struct ctlv_cursor *record);
int ctlv_op_error(struct ctlv_op_ctx *ctx, int error, const char *message);
__printf(3, 4)
int ctlv_op_errorf(struct ctlv_op_ctx *ctx, int error, const char *fmt, ...);
diff --git a/include/linux/ctlv_family.h b/include/linux/ctlv_family.h
index 61ae5b7cec83..d8abdd4964c9 100644
--- a/include/linux/ctlv_family.h
+++ b/include/linux/ctlv_family.h
@@ -428,6 +428,9 @@ extern const struct ctlv_family ctlv_family;
u32 ctlv_enum_ext_max(const struct ctlv_family *family, u16 family_id, u32 ext_id);
+void ctlv_op_provide(struct ctlv *ctlv, u64 op_id);
+void ctlv_op_state_set(struct ctlv *ctlv, u64 op_id, bool disabled);
+
struct ctlv_cursor *
ctlv_input_field(struct ctlv_cursor *cursor, const struct ctlv_cursor *record,
struct ctlv_input_ref ref);
@@ -453,6 +456,25 @@ bool ctlv_record_start(struct ctlv_cursor *record, struct ctlv_cursor *child,
struct ctlv_blob *ctlv_blob_resolve(struct ctlv_op_ctx *ctx,
const struct ctlv_cursor *blob);
+
+#define CTLV_OP_PROVIDE(_name, _op_id) \
+static inline void _name##_provide(struct ctlv *ctlv) \
+{ \
+ ctlv_op_provide(ctlv, (_op_id)); \
+}
+
+#define CTLV_OP_ENABLE(_name, _op_id) \
+static inline void _name##_enable(struct ctlv *ctlv) \
+{ \
+ ctlv_op_state_set(ctlv, (_op_id), false); \
+}
+
+#define CTLV_OP_DISABLE(_name, _op_id) \
+static inline void _name##_disable(struct ctlv *ctlv) \
+{ \
+ ctlv_op_state_set(ctlv, (_op_id), true); \
+}
+
#define __CTLV_DEFINE_PUT(_name, _ctype, _put_fn, _type, _kind) \
static inline bool _name##_put(struct ctlv_cursor *record, \
_ctype value) \
diff --git a/lib/Kconfig b/lib/Kconfig
index 1e42f5167c68..7d81e1d502d1 100644
--- a/lib/Kconfig
+++ b/lib/Kconfig
@@ -496,6 +496,8 @@ source "lib/vdso/Kconfig"
source "lib/fonts/Kconfig"
+source "lib/ctlv/Kconfig"
+
config SG_SPLIT
def_bool n
help
diff --git a/lib/Makefile b/lib/Makefile
index a2afe954805c..db7e4fcc2c6f 100644
--- a/lib/Makefile
+++ b/lib/Makefile
@@ -342,6 +342,8 @@ obj-$(CONFIG_OBJAGG) += objagg.o
# pldmfw library
obj-$(CONFIG_PLDMFW) += pldmfw/
+obj-$(CONFIG_CTLV) += ctlv/
+
obj-$(CONFIG_GENERIC_LIB_DEVMEM_IS_ALLOWED) += devmem_is_allowed.o
obj-$(CONFIG_FIRMWARE_TABLE) += fw_table.o
diff --git a/lib/ctlv/Makefile b/lib/ctlv/Makefile
index 76010d52dd8a..73acdbdf387d 100644
--- a/lib/ctlv/Makefile
+++ b/lib/ctlv/Makefile
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: GPL-2.0
obj-$(CONFIG_CTLV) += ctlv.o
-ctlv-y += blob.o blob_desc.o builder.o introspection.o message.o \
- schema.o
+ctlv-y += blob.o blob_desc.o builder.o core.o introspection.o \
+ message.o schema.o ctlv_gen.o
diff --git a/lib/ctlv/core.c b/lib/ctlv/core.c
new file mode 100644
index 000000000000..605f191de77b
--- /dev/null
+++ b/lib/ctlv/core.c
@@ -0,0 +1,1152 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES
+ */
+#define pr_fmt(fmt) "ctlv: " fmt
+#include <linux/bitmap.h>
+#include <linux/bitops.h>
+#include <linux/build_bug.h>
+#include <linux/cleanup.h>
+#include <linux/container_of.h>
+#include <linux/ctlv.h>
+#include <linux/ctlv_gen.h>
+#include <linux/device.h>
+#include <linux/err.h>
+#include <linux/fs.h>
+#include <linux/kobject.h>
+#include <linux/list.h>
+#include <linux/lockdep.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/overflow.h>
+#include <linux/slab.h>
+#include <linux/stddef.h>
+#include <linux/string.h>
+#include <linux/uaccess.h>
+#include <linux/vmalloc.h>
+
+#include <uapi/ctlv/ctlv.h>
+
+#include "blob.h"
+#include "builder.h"
+#include "internal.h"
+#include "schema.h"
+
+/*
+ * One layout on every ABI, including compat. Alignment is asserted too because
+ * a 32-bit ABI aligns a plain __u64 to four bytes.
+ */
+static_assert(sizeof(struct ctlv_hdr) == 16);
+static_assert(__alignof__(struct ctlv_hdr) == CTLV_ALIGNTO);
+
+static_assert(sizeof(struct ctlv_op_exec) == 56);
+static_assert(__alignof__(struct ctlv_op_exec) == CTLV_ALIGNTO);
+
+/* Claims persist across unload so one ID cannot later name another family. */
+struct ctlv_family_claim {
+ struct list_head entry;
+ char *name;
+ u16 family_id;
+};
+
+static LIST_HEAD(ctlv_family_claims);
+static DEFINE_MUTEX(ctlv_family_claims_lock);
+
+static int ctlv_family_claim_add(const struct ctlv_family_schema *schema)
+{
+ struct ctlv_family_claim *claim;
+
+ guard(mutex)(&ctlv_family_claims_lock);
+ list_for_each_entry(claim, &ctlv_family_claims, entry) {
+ bool same_id = claim->family_id == schema->family_id;
+ bool same_name = !strcmp(claim->name, schema->name);
+
+ if (same_id && same_name)
+ return 0;
+ if (same_id || same_name) {
+ pr_err("family %s wants ID %u, which belongs to %s\n", schema->name,
+ schema->family_id, claim->name);
+ return -EEXIST;
+ }
+ }
+
+ claim = kzalloc_obj(*claim, GFP_KERNEL);
+ if (!claim)
+ return -ENOMEM;
+ claim->name = kstrdup(schema->name, GFP_KERNEL);
+ if (!claim->name) {
+ kfree(claim);
+ return -ENOMEM;
+ }
+ claim->family_id = schema->family_id;
+ list_add_tail(&claim->entry, &ctlv_family_claims);
+ return 0;
+}
+
+static void ctlv_family_claims_free(void)
+{
+ struct ctlv_family_claim *claim, *next;
+
+ list_for_each_entry_safe(claim, next, &ctlv_family_claims, entry) {
+ list_del(&claim->entry);
+ kfree(claim->name);
+ kfree(claim);
+ }
+}
+
+/*
+ * Everything resolved below is generated, so a failure here is a bug, checked
+ * anyway because an out-of-tree family comes from another generator.
+ */
+
+static const char *ctlv_family_name(const struct ctlv_family *family)
+{
+ if (!family->schema || !family->schema->name)
+ return "(unnamed)";
+ return family->schema->name;
+}
+
+static int ctlv_family_bad(const struct ctlv_family *family, const char *what)
+{
+ pr_err("family %s: %s\n", ctlv_family_name(family), what);
+ return -EINVAL;
+}
+
+static int ctlv_op_bad(const struct ctlv_family *family, u64 op_id, const char *what)
+{
+ pr_err("family %s: op %#018llx %s\n", ctlv_family_name(family), op_id, what);
+ return -EINVAL;
+}
+
+/*
+ * A family is named its parent's whole name plus a suffix, so one name states
+ * the chain it belongs to and every family of this core is ctlv-something.
+ */
+static int ctlv_family_chain_check(const struct ctlv_family *family)
+{
+ const struct ctlv_family *child;
+
+ for (child = family; child != &ctlv_family; child = child->parent) {
+ const struct ctlv_family *parent;
+ size_t len;
+
+ if (!child)
+ return ctlv_family_bad(family, "heads no chain of the core's");
+ parent = child->parent;
+ if (!child->schema || !parent || !parent->schema)
+ return ctlv_family_bad(child, "does not name a complete parent descriptor");
+ if (!child->schema->name || !parent->schema->name)
+ return ctlv_family_bad(family, "inherits a family with no name");
+ if (parent->schema->family_id >= child->schema->family_id)
+ return ctlv_family_bad(child, "does not follow its parent in the registry");
+ len = strlen(parent->schema->name);
+ if (!len || strncmp(child->schema->name, parent->schema->name, len) ||
+ child->schema->name[len] != '-')
+ return ctlv_family_bad(family, "is not named after the family it inherits");
+ }
+ return 0;
+}
+
+/* One descriptor of a chain: what it answers with, and what it fills. */
+static int ctlv_family_descriptor_check(const struct ctlv_family *family)
+{
+ const struct ctlv_family_schema *schema = family->schema;
+ u32 index;
+
+ /*
+ * A family that declares no op of its own binds nothing: what it
+ * inherited is answered where it was declared.
+ */
+ if (!schema || !schema->name || !*schema->name || (family->ops_count && !family->ops))
+ return ctlv_family_bad(family, "descriptor is incomplete");
+ /* Every device answers the root family already, as the core. */
+ if (schema->family_id == CTLV_FAMILY_ROOT && family != &ctlv_family)
+ return ctlv_family_bad(family, "claims the root family ID");
+ /* An extension point is answered by whichever family filled it. */
+ if (family->ext_ops_count != schema->ext_fill_schemas_count)
+ return ctlv_family_bad(family, "answers other extension points than it fills");
+ for (index = 0; index < family->ext_ops_count; index++) {
+ const struct ctlv_ext_fill_schema *fill_schema = &schema->ext_fill_schemas[index];
+ const struct ctlv_ext_ops *ext_ops = &family->ext_ops[index];
+
+ if (ext_ops->family_id == fill_schema->family_id &&
+ ext_ops->ext_id == fill_schema->ext_id)
+ continue;
+ return ctlv_family_bad(family, "answers an extension point it does not fill");
+ }
+ return 0;
+}
+
+/* Every descriptor binds its local schema. */
+static int ctlv_family_descriptors_check(const struct ctlv_family *family)
+{
+ const struct ctlv_family *descriptor;
+
+ for (descriptor = family; descriptor; descriptor = descriptor->parent) {
+ int ret;
+
+ ret = ctlv_family_descriptor_check(descriptor);
+ if (ret)
+ return ret;
+ }
+ return 0;
+}
+
+/* The graph a device answers over, and the descriptors standing on it. */
+static int ctlv_family_check(const struct ctlv_family *family)
+{
+ int ret;
+
+ if (!family->schema)
+ return ctlv_family_bad(family, "descriptor is incomplete");
+ ret = ctlv_family_chain_check(family);
+ if (ret)
+ return ret;
+ return ctlv_family_descriptors_check(family);
+}
+
+/* What one walk of the chain has accounted for so far. */
+struct ctlv_op_resolution {
+ const struct ctlv_family *target;
+ const struct ctlv_family *family;
+ u32 bound;
+ u32 dynamic;
+ u64 previous;
+};
+
+/* Every binding of one family answers an op, so a spare one answers nothing. */
+static int ctlv_op_resolution_family_bound(const struct ctlv_op_resolution *state)
+{
+ const struct ctlv_family *family = state->family;
+
+ if (!family || state->bound == family->ops_count)
+ return 0;
+ /* The core's own table is this core's bug, not a family's. */
+ if (WARN_ON_ONCE(family == &ctlv_family))
+ return -EINVAL;
+ return ctlv_family_bad(family, "binds more than its schema asks for");
+}
+
+/*
+ * The ops of one family are contiguous in the walk, so the family before this
+ * one is complete as soon as another family's op arrives.
+ */
+static int ctlv_op_resolution_family_enter(struct ctlv_op_resolution *state,
+ const struct ctlv_family *family)
+{
+ int ret;
+
+ if (state->family == family)
+ return 0;
+ ret = ctlv_op_resolution_family_bound(state);
+ if (ret)
+ return ret;
+ state->family = family;
+ state->bound = 0;
+ return 0;
+}
+
+static int ctlv_op_resolution_add(struct ctlv_op_resolution *state, const struct ctlv_family *owner,
+ const struct ctlv_op_schema *op, struct ctlv_op_entry *entry)
+{
+ const struct ctlv_family *target = state->target;
+ const struct ctlv_family *family = state->family;
+ int ret;
+
+ entry->schema = op;
+ entry->state_index = CTLV_OP_STATE_NONE;
+
+ /*
+ * An op ID is its family's ID and a number within it, so a walk from
+ * the oldest ancestor down ascends, which covers uniqueness too.
+ */
+ if (!CTLV_OP_NUMBER(op->op_id) || CTLV_OP_FAMILY(op->op_id) != owner->schema->family_id)
+ return ctlv_op_bad(target, op->op_id, "belongs to another family");
+ if (op->op_id <= state->previous)
+ return ctlv_op_bad(target, op->op_id, "is out of order");
+ state->previous = op->op_id;
+
+ switch (op->type) {
+ case CTLV_OP_TYPE_ACTION:
+ case CTLV_OP_TYPE_QUERY:
+ if (op->fd_access != CTLV_OP_FD_ACCESS_READ &&
+ op->fd_access != CTLV_OP_FD_ACCESS_READ_WRITE)
+ return ctlv_op_bad(family, op->op_id, "has an unknown fd access");
+ /* The family that declared the op is the one that answers it. */
+ if (state->bound >= family->ops_count)
+ return ctlv_op_bad(family, op->op_id, "has nothing bound to it");
+ entry->binding = &family->ops[state->bound++];
+ /* Required even for an op this device disables or withholds. */
+ if (!entry->binding->action)
+ return ctlv_op_bad(family, op->op_id, "has no callback");
+ /* An optional op arrives absent, for the opt-in to clear. */
+ entry->absent = op->optional;
+ /* An op has a whole pair of hooks of its own, or none. */
+ if (!entry->binding->pre != !entry->binding->post)
+ return ctlv_op_bad(family, op->op_id, "has half a hook pair");
+ /* No message carries a pair, so this is the only check of it. */
+ if (!entry->binding->pre != !op->pre_post)
+ return ctlv_op_bad(family, op->op_id, "disagrees about its hook pair");
+ break;
+ default:
+ return ctlv_op_bad(target, op->op_id, "has an unknown type");
+ }
+
+ switch (op->availability) {
+ case CTLV_OP_AVAILABILITY_FIXED:
+ break;
+ case CTLV_OP_AVAILABILITY_DYNAMIC:
+ /*
+ * Numbered from zero as the walk meets them, per device: two
+ * families of one chain need not have the same ops.
+ */
+ entry->state_index = state->dynamic++;
+ break;
+ default:
+ return ctlv_op_bad(target, op->op_id, "has an unknown availability");
+ }
+
+ /*
+ * Once per device: what an inherited op may carry depends on the
+ * extension points this family filled and on nothing that changes later.
+ */
+ ret = ctlv_schema_op_sizes(target, op, &entry->sizes);
+ if (ret)
+ return ctlv_op_bad(target, op->op_id, "needs messages this core cannot carry");
+ return 0;
+}
+
+static int ctlv_op_resolution_init(struct ctlv *ctlv, const struct ctlv_family *family)
+{
+ struct ctlv_op_resolution state = {
+ .target = family,
+ };
+ const struct ctlv_family *owner;
+ const struct ctlv_op_schema *op;
+ struct ctlv_schema_cursor cursor;
+ u32 ops_count = 0;
+ int ret;
+
+ ret = ctlv_family_check(family);
+ if (ret)
+ return ret;
+
+ ctlv_schema_cursor_init(&cursor, family);
+ while (ctlv_schema_next_op(&cursor, &owner))
+ ops_count++;
+ if (!ops_count)
+ return ctlv_family_bad(family, "has an effective schema with no op");
+
+ ctlv->ops = kcalloc(ops_count, sizeof(*ctlv->ops), GFP_KERNEL);
+ if (!ctlv->ops)
+ return -ENOMEM;
+ ctlv->ops_count = ops_count;
+
+ ops_count = 0;
+ ctlv_schema_cursor_init(&cursor, family);
+ while ((op = ctlv_schema_next_op(&cursor, &owner))) {
+ ret = ctlv_op_resolution_family_enter(&state, owner);
+ if (ret)
+ return ret;
+ ret = ctlv_op_resolution_add(&state, owner, op, &ctlv->ops[ops_count++]);
+ if (ret)
+ return ret;
+ }
+ /* The device family, no op of another having ended its table. */
+ ret = ctlv_op_resolution_family_bound(&state);
+ if (ret)
+ return ret;
+ ctlv->dynamic_ops_count = state.dynamic;
+ return 0;
+}
+
+/**
+ * ctlv_family_inherits - Check a family's ancestry.
+ * @family: family
+ * @ancestor: family that may be @family or one of its ancestors
+ *
+ * Return: whether @family is @ancestor or inherits from it.
+ */
+bool ctlv_family_inherits(const struct ctlv_family *family,
+ const struct ctlv_family *ancestor)
+{
+ for (; family; family = family->parent)
+ if (family == ancestor)
+ return true;
+ return false;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_family_inherits, "CTLV");
+
+/*
+ * Dispatch reads the availability bit under no lock, so an op already admitted
+ * is neither cancelled nor waited for by a disable.
+ */
+
+struct ctlv_op_entry *ctlv_op_find(struct ctlv *ctlv, u64 op_id)
+{
+ u32 index;
+
+ /* The index ascends by op ID, so a miss stops early. */
+ for (index = 0; index < ctlv->ops_count; index++) {
+ if (ctlv->ops[index].schema->op_id == op_id)
+ return &ctlv->ops[index];
+ if (ctlv->ops[index].schema->op_id > op_id)
+ break;
+ }
+ return NULL;
+}
+
+static bool ctlv_op_disabled(struct ctlv *ctlv, const struct ctlv_op_entry *op)
+{
+ if (op->state_index == CTLV_OP_STATE_NONE)
+ return false;
+ /* Acquire, so what the family prepared before enabling is visible. */
+ return test_bit_acquire(op->state_index, ctlv->disabled);
+}
+
+static void ctlv_op_publish_all(struct ctlv *ctlv)
+{
+ u32 published = 0;
+ u32 index;
+
+ /* Against a family still opting in as this is called. */
+ guard(spinlock)(&ctlv->transition_lock);
+ for (index = 0; index < ctlv->ops_count; index++) {
+ if (ctlv->ops[index].absent)
+ continue;
+ ctlv->ops[published++] = ctlv->ops[index];
+ }
+ ctlv->ops_count = published;
+ ctlv->published = true;
+}
+
+void ctlv_op_provide(struct ctlv *ctlv, u64 op_id)
+{
+ struct ctlv_op_entry *op;
+
+ guard(spinlock)(&ctlv->transition_lock);
+ if (WARN_ON_ONCE(ctlv->published || ctlv->destroying))
+ return;
+ op = ctlv_op_find(ctlv, op_id);
+ if (WARN_ON_ONCE(!op))
+ return;
+ op->absent = false;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_op_provide, "CTLV");
+
+void ctlv_op_state_set(struct ctlv *ctlv, u64 op_id, bool disabled)
+{
+ const struct ctlv_op_entry *op;
+
+ /*
+ * Also serializes lookup against publication, which compacts the
+ * array, and against teardown stopping state changes.
+ */
+ guard(spinlock)(&ctlv->transition_lock);
+ if (ctlv->destroying)
+ return;
+
+ op = ctlv_op_find(ctlv, op_id);
+ if (WARN_ON_ONCE(!op) || WARN_ON_ONCE(op->state_index == CTLV_OP_STATE_NONE))
+ return;
+ /* An op this device never took on has no state to change. */
+ if (WARN_ON_ONCE(op->absent))
+ return;
+
+ /*
+ * Fully ordered, releasing what the family prepared to the acquire in
+ * ctlv_op_disabled(). The old state makes a repeat change nothing.
+ */
+ if (disabled ? test_and_set_bit(op->state_index, ctlv->disabled) :
+ !test_and_clear_bit(op->state_index, ctlv->disabled))
+ return;
+
+ if (!ctlv->published)
+ return;
+ ctlv->state_generation++;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_op_state_set, "CTLV");
+
+static int ctlv_op_check_mode(const struct file *filp, const struct ctlv_op_entry *op)
+{
+ switch (op->schema->fd_access) {
+ case CTLV_OP_FD_ACCESS_READ:
+ return filp->f_mode & FMODE_READ ? 0 : -EACCES;
+ case CTLV_OP_FD_ACCESS_READ_WRITE:
+ return (filp->f_mode & (FMODE_READ | FMODE_WRITE)) == (FMODE_READ | FMODE_WRITE) ?
+ 0 :
+ -EACCES;
+ default:
+ /* Registration rejects this before the device is visible. */
+ return -EACCES;
+ }
+}
+
+static u32 ctlv_reply_needed(const struct ctlv_op_entry *op)
+{
+ if (op->schema->type == CTLV_OP_TYPE_QUERY)
+ return op->sizes.query_reply_min_len;
+ return op->sizes.action_reply_max_len;
+}
+
+/*
+ * request_len is the copy boundary; root length frames the message. Ignored
+ * unknown TLVs may exceed the schema maximum up to the transport limit.
+ */
+static int ctlv_request_frame(struct ctlv_op_ctx *ctx, const struct ctlv_op_exec *exec,
+ const struct ctlv_hdr *root)
+{
+ if (root->len < sizeof(*root) || root->len > exec->request_len ||
+ root->type != CTLV_TYPE_ROOT || root->flags != CTLV_HDR_F_RECORD) {
+ ctx->error.reason = CTLV_VALIDATION_REASON_MALFORMED_MESSAGE;
+ return -EINVAL;
+ }
+ return 0;
+}
+
+static int ctlv_request_validate(struct ctlv_op_ctx *ctx, const struct ctlv_op_exec *exec,
+ struct ctlv_hdr *root)
+{
+ int ret;
+
+ ret = ctlv_request_frame(ctx, exec, root);
+ if (ret)
+ return ret;
+ if (!ctx->op->schema->request.node_schemas)
+ return 0;
+
+ ctx->request = (struct ctlv_msg){
+ .root = root,
+ .len = exec->request_len,
+ .tree = &ctx->op->schema->request,
+ .family = ctx->family,
+ .request = true,
+ };
+ return ctlv_schema_validate(&ctx->request, &ctx->error);
+}
+
+/*
+ * An action is given exactly the maximum its schema allows, so running out of
+ * room is this kernel's mistake. A query is given whatever the caller offered,
+ * up to the longest fragment this core builds.
+ */
+static u32 ctlv_reply_capacity(const struct ctlv_op_entry *op, const struct ctlv_op_exec *exec)
+{
+ if (!op->schema->reply.node_schemas)
+ return 0;
+ if (op->schema->type == CTLV_OP_TYPE_QUERY)
+ return min(exec->reply_len, CTLV_MAX_INLINE_MESSAGE_LEN);
+ return op->sizes.action_reply_max_len;
+}
+
+/* Actions and final query fragments must be complete schema-valid replies. */
+static int ctlv_reply_finish(struct ctlv_op_ctx *ctx)
+{
+ struct ctlv_error refused = {};
+
+ if (!ctx->output.root)
+ return 0;
+ /* Whatever poisoned it has warned about it already. */
+ if (ctx->output.poison)
+ return ctx->output.poison;
+ /* A query was held to its open records by the ending it came to. */
+ if (ctx->op->schema->type == CTLV_OP_TYPE_QUERY) {
+ if (ctx->frag.cursor || ctx->frag.next_cursor)
+ return 0;
+ } else if (WARN_ON_ONCE(ctx->deepest != &ctx->output_root)) {
+ /* A cursor of a returned frame is reset, never followed. */
+ ctx->deepest = &ctx->output_root;
+ return -EINVAL;
+ }
+ if (WARN_ON_ONCE(ctlv_schema_validate(&ctx->output, &refused)))
+ return -EINVAL;
+ return 0;
+}
+
+/* A successful query callback must end exactly one fragment. */
+static int ctlv_query_fragment_result(struct ctlv_op_ctx *ctx)
+{
+ if (WARN_ON_ONCE(!ctx->frag.answered))
+ return -EINVAL;
+ return ctx->frag.result;
+}
+
+static int ctlv_op_call(struct ctlv_op_ctx *ctx)
+{
+ const struct ctlv_op_entry *op = ctx->op;
+ struct ctlv_cursor *request = NULL;
+ struct ctlv_cursor *reply = NULL;
+ int blob_ret;
+ int ret;
+
+ if (ctx->request.root) {
+ ctlv_message_root(&ctx->request_root, &ctx->request);
+ request = &ctx->request_root;
+ }
+ if (ctx->output.root)
+ reply = &ctx->output_root;
+
+ ctlv_blob_context_init(ctx);
+ if (op->schema->type == CTLV_OP_TYPE_ACTION) {
+ ret = op->binding->action(ctx->ctlv, ctx, request, reply);
+ } else {
+ ret = op->binding->query(ctx->ctlv, ctx, request, reply);
+ if (!ret)
+ ret = ctlv_query_fragment_result(ctx);
+ }
+ blob_ret = ctlv_blob_context_finish(ctx);
+ if (!ret)
+ ret = blob_ret;
+ if (ret)
+ return ret;
+ return ctlv_reply_finish(ctx);
+}
+
+/*
+ * The device's hooks around the declaring op's, except around a core op. The
+ * availability check is innermost.
+ */
+static int ctlv_op_dispatch(struct ctlv_op_ctx *ctx)
+{
+ const struct ctlv_hooks *hooks = ctx->ctlv->hooks;
+ const struct ctlv_op *op = ctx->op->binding;
+ struct ctlv *ctlv = ctx->ctlv;
+ int ret;
+
+ if (CTLV_OP_FAMILY(ctx->op->schema->op_id) == CTLV_FAMILY_ROOT)
+ hooks = NULL;
+ if (hooks) {
+ ret = hooks->pre(ctlv, ctx);
+ if (ret)
+ return ret;
+ }
+
+ if (op->pre) {
+ ret = op->pre(ctlv, ctx);
+ if (ret)
+ goto out;
+ }
+
+ /* The only authoritative check: introspection is advisory. */
+ if (ctlv_op_disabled(ctlv, ctx->op))
+ ret = -EOPNOTSUPP;
+ else
+ ret = ctlv_op_call(ctx);
+
+ if (op->post)
+ op->post(ctlv, ctx);
+out:
+ if (hooks)
+ hooks->post(ctlv, ctx);
+ return ret;
+}
+
+/* Error information replaces the reply atomically; errno remains authoritative. */
+static void ctlv_error_info(struct ctlv_op_ctx *ctx, u32 room, void **reply, u32 *reply_len)
+{
+ const struct ctlv_error *error = &ctx->error;
+ struct ctlv_cursor *root = &ctx->output_root;
+ void *buf;
+
+ if ((!error->reason && !error->message[0]) || !room)
+ return;
+ buf = kvmalloc(CTLV_ERROR_INFO_MAX_LEN, GFP_KERNEL_ACCOUNT);
+ if (!buf)
+ return;
+
+ /* The same builder over another schema, and a buffer of its own. */
+ ctlv_message_output_init(&ctx->output, ctx, buf, CTLV_ERROR_INFO_MAX_LEN,
+ &ctlv_error_info_schema);
+ ctlv_message_root(root, &ctx->output);
+ ctx->deepest = root;
+
+ if (error->reason)
+ ctlv_put_u64(root, CTLV_T_ERROR_INFO_REASON, CTLV_SCHEMA_ENUM, error->reason);
+ if (error->message[0])
+ ctlv_error_info_message_put(root, error->message);
+ if (error->reason == CTLV_VALIDATION_REASON_STALE_GENERATION) {
+ /* The one failure that is about no part of any message. */
+ ctlv_put_u64(root, CTLV_T_ERROR_INFO_EXPECTED_GENERATION, CTLV_SCHEMA_UINT,
+ error->expected_generation);
+ ctlv_put_u64(root, CTLV_T_ERROR_INFO_CURRENT_GENERATION, CTLV_SCHEMA_UINT,
+ error->current_generation);
+ } else if (error->reason) {
+ ctlv_put_u64(root, CTLV_T_ERROR_INFO_BAD_TLV_OFFSET, CTLV_SCHEMA_UINT,
+ error->bad_tlv_offset);
+ ctlv_put_u64(root, CTLV_T_ERROR_INFO_RECORD_OFFSET, CTLV_SCHEMA_UINT,
+ error->record_offset);
+ /* No field's type is zero, so a zero names nothing. */
+ if (error->missing_type)
+ ctlv_put_u64(root, CTLV_T_ERROR_INFO_MISSING_TYPE, CTLV_SCHEMA_UINT,
+ error->missing_type);
+ if (error->required_length)
+ ctlv_put_u64(root, CTLV_T_ERROR_INFO_REQUIRED_LENGTH, CTLV_SCHEMA_UINT,
+ error->required_length);
+ }
+
+ if (ctx->output.poison || ctx->output.root->len > room) {
+ kvfree(buf);
+ return;
+ }
+ *reply = buf;
+ *reply_len = ctx->output.root->len;
+}
+
+struct ctlv_file {
+ struct ctlv *ctlv;
+ /* The device file, whose release frees this. */
+ struct file *filp;
+};
+
+/*
+ * Everything that needs the device to still have a family. @reply is built
+ * here and becomes the caller's to free, to copy out once the family is gone.
+ */
+static int ctlv_op_exec(struct ctlv_file *cfile, struct ctlv_op_exec *exec,
+ void *snapshot, void **reply, u32 *reply_len)
+{
+ struct ctlv *ctlv = cfile->ctlv;
+ const struct ctlv_op_entry *op;
+ struct ctlv_op_ctx ctx = {};
+ void *out = NULL;
+ u32 capacity;
+ int ret;
+
+ /* No family numbers an op zero, so a zero here is an unset field. */
+ if (!exec->op_id)
+ return -EINVAL;
+ op = ctlv_op_find(ctlv, exec->op_id);
+ if (!op)
+ return -EOPNOTSUPP;
+ ret = ctlv_op_check_mode(cfile->filp, op);
+ if (ret)
+ return ret;
+
+ /*
+ * Only a query resumes. A cursor says which fragment this is, so a
+ * generation without one continues what was never started.
+ */
+ if (op->schema->type == CTLV_OP_TYPE_QUERY) {
+ if (!exec->cursor && exec->generation)
+ return -EINVAL;
+ ctx.frag.generation = exec->generation;
+ ctx.frag.cursor = exec->cursor;
+ } else if (exec->generation || exec->cursor) {
+ return -EINVAL;
+ }
+
+ /* Before the callback, so a buffer too small mutates nothing. */
+ if (exec->reply_len < ctlv_reply_needed(op))
+ return -EMSGSIZE;
+
+ ctx.ctlv = ctlv;
+ ctx.family = ctlv->family;
+ /* Whose memory a blob descriptor of this request describes. */
+ ctx.submitter = current;
+ ctx.op = op;
+
+ /* Before any hook, so that no family code sees an unchecked TLV. */
+ ret = ctlv_request_validate(&ctx, exec, snapshot);
+ if (ret)
+ goto out_error_info;
+
+ capacity = ctlv_reply_capacity(op, exec);
+ if (capacity) {
+ /*
+ * Not zeroed: a byte is copied out only once the root's
+ * length covers it, padding included.
+ */
+ out = kvmalloc(capacity, GFP_KERNEL_ACCOUNT);
+ if (!out) {
+ ret = -ENOMEM;
+ goto out_request;
+ }
+ ctlv_message_output_init(&ctx.output, &ctx, out, capacity, &op->schema->reply);
+ ctlv_message_root(&ctx.output_root, &ctx.output);
+ ctx.deepest = &ctx.output_root;
+ }
+
+ ret = ctlv_op_dispatch(&ctx);
+ if (ret)
+ goto out_error_info;
+
+ /* Where the caller carries on from, zero for anything but a query. */
+ exec->generation = ctx.frag.reported;
+ exec->cursor = ctx.frag.next_cursor;
+ *reply_len = ctx.output.root ? ctx.output.root->len : 0;
+ *reply = out;
+ out = NULL;
+ ret = 0;
+ goto out_request;
+
+out_error_info:
+ ctlv_error_info(&ctx, exec->reply_len, reply, reply_len);
+out_request:
+ ctlv_message_index_free(&ctx.request);
+ kvfree(out);
+ return ret;
+}
+
+/* What one family of this device's chain does with one extension point. */
+static const struct ctlv_ext_ops *ctlv_ext_ops_find(const struct ctlv *ctlv,
+ const struct ctlv_ext_schema *ext_schema)
+{
+ const struct ctlv_family *family;
+ u32 index;
+
+ /* From the device family towards the core, where the fill was found. */
+ for (family = ctlv->family; family; family = family->parent)
+ for (index = 0; index < family->ext_ops_count; index++)
+ if (family->ext_ops[index].family_id == ext_schema->family_id &&
+ family->ext_ops[index].ext_id == ext_schema->ext_id)
+ return &family->ext_ops[index];
+ return NULL;
+}
+
+/**
+ * ctlv_ext_fill - Delegate an extension-point output record.
+ * @ctx: operation context
+ * @request: validated request root
+ * @record: output record selected by the generated helper
+ *
+ * Return: filling callback result, or 0 for an unfilled point.
+ */
+int ctlv_ext_fill(struct ctlv_op_ctx *ctx, const struct ctlv_cursor *request,
+ struct ctlv_cursor *record)
+{
+ const struct ctlv_schema_tree *tree;
+ u32 index;
+
+ if (WARN_ON_ONCE(!record->tree))
+ return -EINVAL;
+ tree = record->tree;
+
+ for (index = 0; index < tree->ext_schemas_count; index++) {
+ const struct ctlv_ext_schema *ext_schema = &tree->ext_schemas[index];
+ const struct ctlv_cursor *filled_request = request;
+ const struct ctlv_ext_fill_schema *fill_schema;
+ const struct ctlv_ext_ops *ext_ops;
+ struct ctlv_cursor filled;
+ int ret;
+
+ if (ext_schema->node != record->node)
+ continue;
+ ext_ops = ctlv_ext_ops_find(ctx->ctlv, ext_schema);
+ if (!ext_ops || !ext_ops->fill)
+ continue;
+
+ if (request) {
+ fill_schema = ctlv_schema_fill(ctx->family, ext_schema->family_id,
+ ext_schema->ext_id);
+ if (WARN_ON_ONCE(!fill_schema))
+ return -EINVAL;
+ filled = *request;
+ if (ctlv_message_input_rebind(&filled, &fill_schema->members, 0))
+ filled_request = &filled;
+ else
+ filled_request = NULL;
+ }
+
+ ret = ext_ops->fill(ctx->ctlv, ctx, filled_request, record);
+ if (ret)
+ return ret;
+ }
+ return 0;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_ext_fill, "CTLV");
+
+static long ctlv_op_exec_ioctl(struct ctlv_file *cfile, unsigned long arg)
+{
+ struct ctlv_op_exec __user *uexec = (struct ctlv_op_exec __user *)arg;
+ struct ctlv *ctlv = cfile->ctlv;
+ struct ctlv_op_exec exec;
+ u32 reply_len = 0;
+ u32 size;
+ int ret;
+
+ if (get_user(size, &uexec->size))
+ return -EFAULT;
+ /* The first version ends at cursor, so every size covers the write-back. */
+ if (size < offsetofend(struct ctlv_op_exec, cursor))
+ return -EINVAL;
+ ret = copy_struct_from_user(&exec, sizeof(exec), uexec, size);
+ if (ret)
+ return ret;
+ if (exec.flags)
+ return -EINVAL;
+ if (exec.request_len < sizeof(struct ctlv_hdr) ||
+ exec.request_len > CTLV_MAX_INLINE_MESSAGE_LEN)
+ return -EINVAL;
+
+ /*
+ * Before the lock, which destruction must not wait on for a faulting
+ * copy. Everything after this reads the snapshot, never userspace.
+ */
+ void *snapshot __free(kvfree) = kvmalloc(exec.request_len, GFP_KERNEL_ACCOUNT);
+ if (!snapshot)
+ return -ENOMEM;
+ if (copy_from_user(snapshot, u64_to_user_ptr(exec.request), exec.request_len))
+ return -EFAULT;
+
+ void *reply __free(kvfree) = NULL;
+
+ scoped_guard(rwsem_read, &ctlv->registration_lock) {
+ if (!ctlv->registered || !ctlv->family)
+ return -ENODEV;
+ ret = ctlv_op_exec(cfile, &exec, snapshot, &reply, &reply_len);
+ }
+ /* Do not make destruction wait for a faulting userspace copy. */
+ if (reply_len && copy_to_user(u64_to_user_ptr(exec.reply), reply, reply_len))
+ return ret ?: -EFAULT;
+ /*
+ * The only fields the kernel writes back, a field at a time so that
+ * what the caller stated about its own buffers stays the caller's.
+ */
+ if (!ret && (put_user(exec.generation, &uexec->generation) ||
+ put_user(exec.cursor, &uexec->cursor)))
+ return -EFAULT;
+ return ret;
+}
+
+/**
+ * ctlv_file_ioctl - Run a CTLV ioctl on a device file.
+ * @cfile: state returned by ctlv_file_open()
+ * @cmd: ioctl command
+ * @arg: ioctl argument
+ *
+ * Return: ioctl result, or -ENOIOCTLCMD for a command that is not CTLV's.
+ */
+long ctlv_file_ioctl(struct ctlv_file *cfile, unsigned int cmd, unsigned long arg)
+{
+ switch (cmd) {
+ case CTLV_IOC_OP_EXEC:
+ return ctlv_op_exec_ioctl(cfile, arg);
+ default:
+ return -ENOIOCTLCMD;
+ }
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_file_ioctl, "CTLV");
+
+static bool ctlv_file_open_allowed(struct ctlv *ctlv)
+{
+ lockdep_assert_held_read(&ctlv->registration_lock);
+ return ctlv->registered && ctlv->family;
+}
+
+/**
+ * ctlv_file_open - Attach CTLV to one open file of its device.
+ * @ctlv: published instance
+ * @filp: device file, open for reading
+ *
+ * Return: per-open state, or an error pointer.
+ */
+struct ctlv_file *ctlv_file_open(struct ctlv *ctlv, struct file *filp)
+{
+ struct ctlv_file *cfile;
+ int ret;
+
+ /*
+ * A failed registration can withdraw the node while an open is waiting
+ * for the registration lock, so admission itself first pins the device.
+ */
+ get_device(ctlv->dev);
+
+ do {
+ guard(rwsem_read)(&ctlv->registration_lock);
+ if (!ctlv_file_open_allowed(ctlv)) {
+ ret = -ENODEV;
+ break;
+ }
+ if (!(filp->f_mode & FMODE_READ)) {
+ ret = -EACCES;
+ break;
+ }
+
+ cfile = kzalloc_obj(*cfile, GFP_KERNEL_ACCOUNT);
+ if (!cfile) {
+ ret = -ENOMEM;
+ break;
+ }
+ cfile->ctlv = ctlv;
+ cfile->filp = filp;
+ /* This description keeps the reference taken on entry. */
+ return cfile;
+ } while (0);
+
+ put_device(ctlv->dev);
+ return ERR_PTR(ret);
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_file_open, "CTLV");
+
+/**
+ * ctlv_file_close - Detach CTLV from a device file.
+ * @cfile: state returned by ctlv_file_open()
+ *
+ * For the release of the device file.
+ */
+void ctlv_file_close(struct ctlv_file *cfile)
+{
+ struct device *dev = cfile->ctlv->dev;
+
+ kfree(cfile);
+ put_device(dev);
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_file_close, "CTLV");
+
+/* Uevent identity is available to rules before the device can be opened. */
+static int ctlv_uevent_families(struct kobj_uevent_env *env,
+ const struct ctlv_family *family)
+{
+ u32 levels = ctlv_family_levels(family);
+ u32 level;
+ int ret;
+
+ ret = add_uevent_var(env, "CTLV_FAMILY=%s", family->schema->name);
+ if (ret)
+ return ret;
+ for (level = 0; level < levels; level++) {
+ const struct ctlv_family *ancestor =
+ ctlv_family_ancestor(family, levels - 1 - level);
+
+ ret = add_uevent_var(env, "CTLV_COMPATIBLE_%u=%s", level, ancestor->schema->name);
+ if (ret)
+ return ret;
+ }
+ return add_uevent_var(env, "CTLV_COMPATIBLE_N=%u", levels);
+}
+
+static int ctlv_uevent_check(const struct ctlv_family *family)
+{
+ struct kobj_uevent_env *env;
+ int ret;
+
+ env = kzalloc_obj(*env, GFP_KERNEL);
+ if (!env)
+ return -ENOMEM;
+ ret = ctlv_uevent_families(env, family);
+ if (!ret && (env->buflen > CTLV_UEVENT_MAX_LEN || env->envp_idx > CTLV_UEVENT_MAX_VARS)) {
+ pr_err("family %s: its chain of names needs %d bytes in %d properties of a uevent, more than the %d in %d it may take\n",
+ family->schema->name, env->buflen, env->envp_idx, CTLV_UEVENT_MAX_LEN,
+ CTLV_UEVENT_MAX_VARS);
+ ret = -E2BIG;
+ }
+ kfree(env);
+ return ret;
+}
+
+/**
+ * ctlv_init - Prepare an embedded instance for one family.
+ * @ctlv: zeroed instance
+ * @dev: device
+ * @family: device family
+ * @hooks: pair around every op the core does not answer, or NULL
+ *
+ * Until ctlv_publish(), the family decides its optional and dynamic
+ * ops through its generated helpers.
+ *
+ * Return: 0, or a negative error. ctlv_fini() applies either way.
+ */
+int ctlv_init(struct ctlv *ctlv, struct device *dev,
+ const struct ctlv_family *family, const struct ctlv_hooks *hooks)
+{
+ int ret;
+
+ /* Nothing in common means no pair, not half of one. */
+ if (!dev || !family || (hooks && (!hooks->pre || !hooks->post)))
+ return -EINVAL;
+
+ ctlv->dev = dev;
+ ctlv->hooks = hooks;
+ init_rwsem(&ctlv->registration_lock);
+ spin_lock_init(&ctlv->transition_lock);
+
+ /* Before anything reads the schema through this instance. */
+ ret = ctlv_op_resolution_init(ctlv, family);
+ if (ret)
+ return ret;
+
+ /* Before a subsystem announces a device that could not be. */
+ ret = ctlv_uevent_check(family);
+ if (ret)
+ return ret;
+
+ if (ctlv->dynamic_ops_count) {
+ ctlv->disabled = bitmap_zalloc(ctlv->dynamic_ops_count, GFP_KERNEL);
+ if (!ctlv->disabled)
+ return -ENOMEM;
+ }
+
+ ret = ctlv_family_claim_add(family->schema);
+ if (ret)
+ return ret;
+ ctlv->family = family;
+ return 0;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_init, "CTLV");
+
+/**
+ * ctlv_publish - Fix the op table and accept operations.
+ * @ctlv: initialized instance
+ */
+void ctlv_publish(struct ctlv *ctlv)
+{
+ ctlv_op_publish_all(ctlv);
+ scoped_guard(rwsem_write, &ctlv->registration_lock)
+ ctlv->registered = true;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_publish, "CTLV");
+
+/**
+ * ctlv_unpublish - Stop operations.
+ * @ctlv: published instance
+ *
+ * Waits for the operations in flight. No family callback runs afterwards.
+ */
+void ctlv_unpublish(struct ctlv *ctlv)
+{
+ u32 index;
+
+ lockdep_assert_not_held(&ctlv->registration_lock);
+ lockdep_assert_not_held(&ctlv->transition_lock);
+
+ /*
+ * The write side waits for admitted callbacks. Once held, registration
+ * state keeps every later ioctl out.
+ */
+ scoped_guard(rwsem_write, &ctlv->registration_lock)
+ ctlv->registered = false;
+
+ scoped_guard(spinlock, &ctlv->transition_lock)
+ ctlv->destroying = true;
+
+ /* No callback can reach what the family owns after this. */
+ scoped_guard(rwsem_write, &ctlv->registration_lock)
+ WRITE_ONCE(ctlv->family, NULL);
+ for (index = 0; index < ctlv->ops_count; index++)
+ ctlv->ops[index].binding = NULL;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_unpublish, "CTLV");
+
+/**
+ * ctlv_fini - Free what an instance allocated.
+ * @ctlv: instance never published, or unpublished
+ *
+ * For the release of the device.
+ */
+void ctlv_fini(struct ctlv *ctlv)
+{
+ bitmap_free(ctlv->disabled);
+ ctlv->disabled = NULL;
+ kfree(ctlv->ops);
+ ctlv->ops = NULL;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_fini, "CTLV");
+
+static void __exit ctlv_module_exit(void)
+{
+ ctlv_family_claims_free();
+}
+
+module_exit(ctlv_module_exit);
+MODULE_DESCRIPTION("CTLV character device TLV UAPI core");
+MODULE_LICENSE("GPL");
--
2.56.0
^ permalink raw reply related [flat|nested] 24+ messages in thread
* [PATCH RFC v2 12/15] ctlv: Add character device nodes
2026-11-13 15:54 [PATCH RFC v2 00/15] ctlv: Add a schema-driven TLV uAPI for character devices Jiri Pirko
` (10 preceding siblings ...)
2026-11-13 15:55 ` [PATCH RFC v2 11/15] ctlv: Add operation dispatch Jiri Pirko
@ 2026-11-13 15:55 ` Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 13/15] Documentation: ctlv: Describe how to write a family Jiri Pirko
` (2 subsequent siblings)
14 siblings, 0 replies; 24+ messages in thread
From: Jiri Pirko @ 2026-11-13 15:55 UTC (permalink / raw)
To: linux-coco
Cc: linux-kernel, linux-api, x86, kvm, linux-doc, jgg, david.quigley,
lukas, ksinyuk, rodrigo.vivi, cedric.xing, sameo, drdeeglaze,
James.Bottomley, aik, jpiotrowski, qinkun, pgonda, jic23,
alistair.francis, ankita, suzuki.poulose, kas, rick.p.edgecombe,
elena.reshetova, dave.hansen, sathyanarayanan.kuppuswamy,
peter.fang, jarkko, zohar, yeoreum.yun, aneesh.kumar, dave.jiang,
nik.borisov, sergii.dmytruk
From: Jiri Pirko <jiri@nvidia.com>
Give a subsystem's device a node of the subsystem's own class, whose
file operations are the CTLV ones. The subsystem registers the class
with CTLV, which takes its device numbers, and embeds a struct
ctlv_device beside the instance.
Assisted-by: Cursor: GPT-5.6 Sol
Assisted-by: Cursor: Claude Opus 5.5
Signed-off-by: Jiri Pirko <jiri@nvidia.com>
---
RFC->RFCv2:
- split out of the device and operation dispatch patch
- made a library each subsystem's class builds its nodes with, and
dropped the ctlv class
---
include/linux/ctlv_device.h | 57 ++++++++++++
lib/ctlv/Makefile | 2 +-
lib/ctlv/core.c | 17 ++++
lib/ctlv/device.c | 179 ++++++++++++++++++++++++++++++++++++
lib/ctlv/internal.h | 2 +
5 files changed, 256 insertions(+), 1 deletion(-)
create mode 100644 include/linux/ctlv_device.h
create mode 100644 lib/ctlv/device.c
diff --git a/include/linux/ctlv_device.h b/include/linux/ctlv_device.h
new file mode 100644
index 000000000000..9ff10ce7368f
--- /dev/null
+++ b/include/linux/ctlv_device.h
@@ -0,0 +1,57 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES
+ *
+ * Character device nodes of CTLV instances, in a subsystem's own class.
+ */
+#ifndef _LINUX_CTLV_DEVICE_H
+#define _LINUX_CTLV_DEVICE_H
+
+#include <linux/cdev.h>
+#include <linux/device.h>
+#include <linux/idr.h>
+#include <linux/types.h>
+
+struct ctlv;
+struct module;
+
+/**
+ * struct ctlv_device_class - The class of a subsystem's nodes.
+ * @class: the class, whose dev_release frees a node's structure; CTLV sets
+ * its dev_uevent
+ * @max_devices: how many nodes the class may have at once
+ */
+struct ctlv_device_class {
+ struct class class;
+ unsigned int max_devices;
+ /* private: */
+ struct module *owner;
+ dev_t devt;
+ struct ida ids;
+};
+
+/**
+ * struct ctlv_device - The node of one instance.
+ * @dev: the node's device, named for its class and a number of its own
+ *
+ * Embedded beside the instance in the subsystem's device structure.
+ */
+struct ctlv_device {
+ struct device dev;
+ /* private: */
+ struct cdev cdev;
+ struct ctlv *ctlv;
+ struct ctlv_device_class *cls;
+};
+
+int __ctlv_device_class_register(struct ctlv_device_class *cls, struct module *owner);
+#define ctlv_device_class_register(cls) __ctlv_device_class_register(cls, THIS_MODULE)
+void ctlv_device_class_unregister(struct ctlv_device_class *cls);
+
+int ctlv_device_init(struct ctlv_device *ctlvdev, struct ctlv_device_class *cls,
+ struct device *parent, struct ctlv *ctlv);
+void ctlv_device_fini(struct ctlv_device *ctlvdev);
+int ctlv_device_add(struct ctlv_device *ctlvdev);
+void ctlv_device_del(struct ctlv_device *ctlvdev);
+
+#endif /* _LINUX_CTLV_DEVICE_H */
diff --git a/lib/ctlv/Makefile b/lib/ctlv/Makefile
index 73acdbdf387d..e783ad450c12 100644
--- a/lib/ctlv/Makefile
+++ b/lib/ctlv/Makefile
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: GPL-2.0
obj-$(CONFIG_CTLV) += ctlv.o
-ctlv-y += blob.o blob_desc.o builder.o core.o introspection.o \
+ctlv-y += blob.o blob_desc.o builder.o core.o device.o introspection.o \
message.o schema.o ctlv_gen.o
diff --git a/lib/ctlv/core.c b/lib/ctlv/core.c
index 605f191de77b..eb493104da62 100644
--- a/lib/ctlv/core.c
+++ b/lib/ctlv/core.c
@@ -1034,6 +1034,23 @@ static int ctlv_uevent_check(const struct ctlv_family *family)
return ret;
}
+/**
+ * ctlv_uevent - Add the family variables to a device uevent.
+ * @ctlv: instance
+ * @env: uevent environment
+ *
+ * Return: 0, or a negative error.
+ */
+int ctlv_uevent(const struct ctlv *ctlv, struct kobj_uevent_env *env)
+{
+ /* Read unlocked: a uevent may be asked for under the registration lock. */
+ const struct ctlv_family *family = READ_ONCE(ctlv->family);
+
+ if (!family)
+ return 0;
+ return ctlv_uevent_families(env, family);
+}
+
/**
* ctlv_init - Prepare an embedded instance for one family.
* @ctlv: zeroed instance
diff --git a/lib/ctlv/device.c b/lib/ctlv/device.c
new file mode 100644
index 000000000000..b761739e5442
--- /dev/null
+++ b/lib/ctlv/device.c
@@ -0,0 +1,179 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES
+ *
+ * Character device nodes of CTLV instances.
+ */
+#include <linux/bug.h>
+#include <linux/cdev.h>
+#include <linux/cleanup.h>
+#include <linux/container_of.h>
+#include <linux/ctlv.h>
+#include <linux/ctlv_device.h>
+#include <linux/device.h>
+#include <linux/err.h>
+#include <linux/fs.h>
+#include <linux/idr.h>
+#include <linux/kdev_t.h>
+#include <linux/module.h>
+#include <linux/rwsem.h>
+
+#include "internal.h"
+
+static int ctlv_device_open(struct inode *inode, struct file *filp)
+{
+ struct ctlv_device *ctlvdev = container_of(inode->i_cdev, struct ctlv_device, cdev);
+ struct ctlv_file *cfile;
+
+ cfile = ctlv_file_open(ctlvdev->ctlv, filp);
+ if (IS_ERR(cfile))
+ return PTR_ERR(cfile);
+ filp->private_data = cfile;
+ return 0;
+}
+
+static int ctlv_device_release(struct inode *inode, struct file *filp)
+{
+ ctlv_file_close(filp->private_data);
+ return 0;
+}
+
+static long ctlv_device_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
+{
+ return ctlv_file_ioctl(filp->private_data, cmd, arg);
+}
+
+static const struct file_operations ctlv_device_operations = {
+ .owner = THIS_MODULE,
+ .open = ctlv_device_open,
+ .release = ctlv_device_release,
+ .unlocked_ioctl = ctlv_device_ioctl,
+ /* Every ioctl field is fixed width, so one entry point serves both. */
+ .compat_ioctl = compat_ptr_ioctl,
+};
+
+static int ctlv_device_uevent(const struct device *dev, struct kobj_uevent_env *env)
+{
+ const struct ctlv_device *ctlvdev = container_of_const(dev, struct ctlv_device, dev);
+
+ return ctlv_uevent(ctlvdev->ctlv, env);
+}
+
+/**
+ * __ctlv_device_class_register - Register a class of nodes.
+ * @cls: class, its name, dev_release and max_devices set
+ * @owner: module of the dev_release, held while a node is open
+ *
+ * Called as ctlv_device_class_register(), which passes the calling module.
+ *
+ * Return: 0, or a negative error.
+ */
+int __ctlv_device_class_register(struct ctlv_device_class *cls, struct module *owner)
+{
+ int ret;
+
+ if (WARN_ON(!cls->max_devices))
+ return -EINVAL;
+ ret = alloc_chrdev_region(&cls->devt, 0, cls->max_devices, cls->class.name);
+ if (ret)
+ return ret;
+ cls->owner = owner;
+ cls->class.dev_uevent = ctlv_device_uevent;
+ ida_init(&cls->ids);
+ ret = class_register(&cls->class);
+ if (ret)
+ unregister_chrdev_region(cls->devt, cls->max_devices);
+ return ret;
+}
+EXPORT_SYMBOL_NS_GPL(__ctlv_device_class_register, "CTLV");
+
+/**
+ * ctlv_device_class_unregister - Unregister a class of nodes.
+ * @cls: class whose nodes are all released
+ */
+void ctlv_device_class_unregister(struct ctlv_device_class *cls)
+{
+ class_unregister(&cls->class);
+ unregister_chrdev_region(cls->devt, cls->max_devices);
+ ida_destroy(&cls->ids);
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_device_class_unregister, "CTLV");
+
+/**
+ * ctlv_device_init - Prepare the node of an instance.
+ * @ctlvdev: zeroed node, in the structure the class's dev_release frees
+ * @cls: registered class
+ * @parent: parent device, or NULL
+ * @ctlv: instance, for ctlv_init() with the node's device
+ *
+ * From here on, put_device() unwinds, whether or not this succeeds.
+ *
+ * Return: 0, or a negative error.
+ */
+int ctlv_device_init(struct ctlv_device *ctlvdev, struct ctlv_device_class *cls,
+ struct device *parent, struct ctlv *ctlv)
+{
+ int devnum;
+
+ device_initialize(&ctlvdev->dev);
+ ctlvdev->dev.class = &cls->class;
+ ctlvdev->dev.parent = parent;
+ ctlvdev->ctlv = ctlv;
+ cdev_init(&ctlvdev->cdev, &ctlv_device_operations);
+ ctlvdev->cdev.owner = cls->owner;
+
+ devnum = ida_alloc_max(&cls->ids, cls->max_devices - 1, GFP_KERNEL);
+ if (devnum < 0)
+ return devnum;
+ ctlvdev->cls = cls;
+ ctlvdev->dev.devt = MKDEV(MAJOR(cls->devt), devnum);
+ return dev_set_name(&ctlvdev->dev, "%s%d", cls->class.name, devnum);
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_device_init, "CTLV");
+
+/**
+ * ctlv_device_fini - Free the node's number.
+ * @ctlvdev: node
+ *
+ * For the class's dev_release, before it frees the node.
+ */
+void ctlv_device_fini(struct ctlv_device *ctlvdev)
+{
+ if (ctlvdev->cls)
+ ida_free(&ctlvdev->cls->ids, MINOR(ctlvdev->dev.devt));
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_device_fini, "CTLV");
+
+/**
+ * ctlv_device_add - Add the node.
+ * @ctlvdev: node, whose instance was initialized with its device and published
+ *
+ * The node may be opened as soon as it appears, so its instance is published
+ * before and unpublished after ctlv_device_del().
+ *
+ * Return: 0, or a negative error.
+ */
+int ctlv_device_add(struct ctlv_device *ctlvdev)
+{
+ struct ctlv *ctlv = ctlvdev->ctlv;
+ bool published;
+
+ scoped_guard(rwsem_read, &ctlv->registration_lock)
+ published = ctlv->registered;
+ if (WARN_ON(ctlv->dev != &ctlvdev->dev || !published))
+ return -EINVAL;
+ return cdev_device_add(&ctlvdev->cdev, &ctlvdev->dev);
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_device_add, "CTLV");
+
+/**
+ * ctlv_device_del - Remove the node.
+ * @ctlvdev: added node
+ *
+ * A file still open keeps the node's device until it is closed.
+ */
+void ctlv_device_del(struct ctlv_device *ctlvdev)
+{
+ cdev_device_del(&ctlvdev->cdev, &ctlvdev->dev);
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_device_del, "CTLV");
diff --git a/lib/ctlv/internal.h b/lib/ctlv/internal.h
index 2e1e36799a17..8fb5f0abdc47 100644
--- a/lib/ctlv/internal.h
+++ b/lib/ctlv/internal.h
@@ -23,6 +23,7 @@
#define CTLV_ASSERT(cond) BUILD_BUG_ON_INVALID(cond)
#endif
+struct kobj_uevent_env;
struct task_struct;
#define CTLV_ERROR_MESSAGE_LEN 256
@@ -81,5 +82,6 @@ struct ctlv_op_ctx {
};
struct ctlv_op_entry *ctlv_op_find(struct ctlv *ctlv, u64 op_id);
+int ctlv_uevent(const struct ctlv *ctlv, struct kobj_uevent_env *env);
#endif /* _CTLV_INTERNAL_H */
--
2.56.0
^ permalink raw reply related [flat|nested] 24+ messages in thread
* [PATCH RFC v2 13/15] Documentation: ctlv: Describe how to write a family
2026-11-13 15:54 [PATCH RFC v2 00/15] ctlv: Add a schema-driven TLV uAPI for character devices Jiri Pirko
` (11 preceding siblings ...)
2026-11-13 15:55 ` [PATCH RFC v2 12/15] ctlv: Add character device nodes Jiri Pirko
@ 2026-11-13 15:55 ` Jiri Pirko
2026-11-13 16:13 ` sashiko-bot
2026-11-13 15:55 ` [PATCH RFC v2 14/15] tools/ctlv: Add the userspace library Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 15/15] tools/ctlv: Add ctlvctl Jiri Pirko
14 siblings, 1 reply; 24+ messages in thread
From: Jiri Pirko @ 2026-11-13 15:55 UTC (permalink / raw)
To: linux-coco
Cc: linux-kernel, linux-api, x86, kvm, linux-doc, jgg, david.quigley,
lukas, ksinyuk, rodrigo.vivi, cedric.xing, sameo, drdeeglaze,
James.Bottomley, aik, jpiotrowski, qinkun, pgonda, jic23,
alistair.francis, ankita, suzuki.poulose, kas, rick.p.edgecombe,
elena.reshetova, dave.hansen, sathyanarayanan.kuppuswamy,
peter.fang, jarkko, zohar, yeoreum.yun, aneesh.kumar, dave.jiang,
nik.borisov, sergii.dmytruk
From: Jiri Pirko <jiri@nvidia.com>
Describe the YAML language, generated family descriptors, lifecycle,
operations, blobs, inheritance and compatible extension rules.
Assisted-by: Cursor: GPT-5.6 Sol
Assisted-by: Cursor: Claude Opus 5.5
Signed-off-by: Jiri Pirko <jiri@nvidia.com>
---
RFC->RFCv2:
- fixed the family descriptor name in the ctlv_create() example
- documented ctlv_record_cancel()
- stated how the generator keeps every message within the limit
- dropped the events
- reworked the guide after review, around a worked example family
- warned that a family's registry position is its ID
- renamed stubs to extension points
- added the kernel-doc of the family API
- described specs as op trees and records
- said to return ctlv_blob_too_short() for an output blob too short
- said what validation accepted instead of placements
- said record instead of nest, the root being one
- took the UAPI headers from include/uapi/ctlv/
- documented every item of the example family's spec
- generated and recorded a family's ABI with one regen, and listed
the files it should change
- described enum extension points
- said how to name an op that reads
- described a subsystem's nodes in place of the ctlv class
- said message instead of wire
---
Documentation/driver-api/ctlv.rst | 716 +++++++++++++++++++++++++++++
Documentation/driver-api/index.rst | 1 +
MAINTAINERS | 1 +
3 files changed, 718 insertions(+)
create mode 100644 Documentation/driver-api/ctlv.rst
diff --git a/Documentation/driver-api/ctlv.rst b/Documentation/driver-api/ctlv.rst
new file mode 100644
index 000000000000..34d20c14a9bf
--- /dev/null
+++ b/Documentation/driver-api/ctlv.rst
@@ -0,0 +1,716 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+=====================
+Writing a CTLV family
+=====================
+
+:Author: Jiri Pirko
+
+A CTLV family is a set of system calls for one sort of device: actions and
+queries a program executes. The family is written as a
+YAML specification, a generator turns that into headers, and the family
+implementation supplies the callbacks the generated header names. CTLV checks
+every request against the specification before a callback sees it. The
+userspace ABI is :doc:`/userspace-api/ctlv`; the in-tree specifications are
+under ``Documentation/ctlv/specs``.
+
+The example
+===========
+
+This guide writes a small invented family, ``ctlv-example``, with two system
+calls::
+
+ item_set_action(u64 index, string label?, blob data?) -> status;
+ item_get_query() -> { u64 index; string label?; }[];
+
+``index`` is 0..4095, ``label`` is at most 63 bytes excluding NUL, and
+``data`` is at most 1 MiB with 8-byte alignment. ``?`` marks an optional
+argument. An action runs once and returns a status. A query returns an answer
+that may be larger than the caller's buffer, which CTLV delivers in as many
+fragments as the buffer needs; a list, as here, is the usual case.
+
+Write the specification
+=======================
+
+The specification starts with the family. ``ctlv-example`` inherits ``ctlv``,
+the root family every device answers:
+
+.. code-block:: yaml
+
+ # SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause)
+ %YAML 1.2
+ ---
+ # Numbered items, each with an optional label.
+ family: ctlv-example
+ inherits: ctlv
+
+The comment on the lines above an entry is its documentation, which the
+generated headers carry. The generator refuses a family, record, field, op,
+enum or flags without one, and an enum value or a flag without its own, so
+values go one to a line.
+
+Each system call is an op:
+
+.. code-block:: yaml
+
+ ops:
+ # Sets an item's label and data.
+ item-set:
+ type: action
+ request:
+ # Which item it is.
+ index: uint(..4095)
+ # What the item is called.
+ label?: string(..63)
+ # The item's contents, which the device reads.
+ data?: blob(in, ..1M, align 8, va)
+
+ # Lists the items.
+ item-get:
+ type: query
+ reply:
+ # Every item the device keeps.
+ items[]:
+ # Which item it is.
+ index: uint(..4095)
+ # What the item is called.
+ label?: string(..63)
+
+``request`` is what an op takes and ``reply`` what it returns. An op
+without a ``request`` takes no arguments, and an action without a ``reply``
+returns only its status. An action or a query may also set these keys:
+
+=========================== ========== ========================================
+Key Default What it does
+=========================== ========== ========================================
+``fd-access`` read-write ``read`` lets an ``O_RDONLY`` fd run it
+``suggested-reply-length`` 64K the buffer a caller starts a query with
+``optional`` false see `Optional operations`_
+``availability`` fixed see `Dynamic availability`_
+``pre-post`` false see `Hooks`_
+=========================== ========== ========================================
+
+An op that reads is named ``<object>-get``, the object in the singular, and
+each kind of object has one. A get over a collection, as ``item-get`` is, is a
+query even while it returns one object, as an op's type never changes; its
+request fields are optional filters, none meaning every object. A filter
+matching nothing gives an empty answer, and an ID naming nothing fails with
+``ENOENT``. An action get returns exactly one thing, such as the device.
+
+What an op takes or returns is a record: the fields one level of a message
+may hold, one per line. ``name?`` is an optional field, ``name[N]`` one that
+occurs up to N times, ``name[enum]`` one that occurs up to once per value of
+the enum, and ``name[]`` one a query's reply repeats any number of times. A
+field with fields indented below it is a record, the one declared there, as
+``items[]`` is. Each field has generated helpers named after its op and the
+records it is in: ``ctlv_example_item_set_index_get()`` reads ``index`` in an
+``item-set`` request, and ``ctlv_example_item_get_items_index_put()`` writes
+it in an item of the ``item-get`` reply.
+
+Fields that several messages repeat can be declared once as a named record,
+and then the specification reads:
+
+.. code-block:: yaml
+
+ records:
+ # One item, as item-set takes it and item-get lists it.
+ item:
+ # Which item it is.
+ index: uint(..4095)
+ # What the item is called.
+ label?: string(..63)
+
+ ops:
+ # Sets an item's label and data.
+ item-set:
+ type: action
+ request:
+ $include: item
+ # The item's contents, which the device reads.
+ data?: blob(in, ..1M, align 8, va)
+
+ # Lists the items.
+ item-get:
+ type: query
+ reply:
+ # Every item the device keeps.
+ items[]: record item
+
+``$include: item`` puts the record's fields in place, and ``record item`` makes
+it a whole message or a record's field. The field is then the same wherever it
+appears: ``item.index`` has one type, one bound and one generated helper in
+the ``item-set`` request and in the items ``item-get`` returns, and
+``ctlv_example_item_index_put()`` writes it in the latter. This guide keeps
+the fields spelled out.
+
+A field's type, the ID it has in a message, is made of the family's number,
+its record and its place in that record. A named record is numbered by its
+place in ``records``, an op's request and reply by the op's place in ``ops``,
+and a record declared in place by its place among the records declared beside
+it. Every one of those places is ABI, so records, fields and ops are only ever
+appended. Records declared in place go at most four deep and fifteen side by
+side below a named record or an op's message; a deeper or wider tree takes
+another named record.
+
+A field has one schema kind. Bounds follow it in parentheses, inclusive, with
+limits such as ``u32-max`` written by name and sizes such as ``4K``, which an
+integer takes only with the ``bytes`` unit, as in ``uint(1..4K, bytes)``:
+
+============ ============================== ==================================
+Kind Holds Declared as, for example
+============ ============================== ==================================
+``uint`` an unsigned 64-bit integer ``uint(..4095)``
+``sint`` a signed 64-bit integer ``sint(-8..8)``
+``bool`` true or false ``bool``
+``string`` text, bounded without its NUL ``string(..63)``
+``binary`` bytes ``binary(16..64)``
+enum one value of an enum the enum's name
+flags a set of flags the flags' name
+record fields ``record item``, or fields below
+``blob`` caller memory, see `Blobs`_ ``blob(in, ..1M, align 8, va)``
+============ ============================== ==================================
+
+Bounds, record membership and whether a field is required are ABI.
+
+The top-level keys may come in any order; the in-tree specifications put
+``enums`` and ``flags`` first, then ``records`` and ``ops``.
+
+An enum a uapi header already defines names the header in ``$header``, a path
+under ``include/uapi``, and the enum in ``$enum``. The enum's constants are its
+values, under their own names and numbers, whatever they start from and
+whatever gaps they leave, and need no comments. ``$end`` names a sentinel,
+leaving it and every constant after it out:
+
+.. code-block:: yaml
+
+ enums:
+ # The kernel's hash algorithms.
+ hash-algo:
+ $header: linux/hash_info.h
+ $enum: hash_algo
+ $end: HASH_ALGO__LAST
+
+The generated code includes the header rather than repeating the enum. CTLV
+takes any number from the lowest value up to one below ``$end``, or to the
+highest value without it, so a number in a gap reaches the callback, which
+refuses it. ``$end`` may move later as the header grows, never earlier. An
+enum written as ``extension`` has no values of its own; see
+`Inheritance and HW driver fields`_.
+
+No message outgrows the core's inline limit, ``CTLV_MAX_INLINE_MESSAGE_LEN``
+(64 KiB): the generator refuses an operation whose request or action reply
+could be longer, or a query whose longest field could not fit in one
+fragment. Larger data goes in a blob.
+
+Generate the family
+===================
+
+A new family is one change holding its specification, its registry entry and
+its output manifest entry::
+
+ Documentation/ctlv/specs/ctlv-example.yaml
+ Documentation/ctlv/families.yaml
+ Documentation/ctlv/generated-files.yaml
+
+The family's position in ``families.yaml`` is its family ID, part of every
+type and op ID it generates. Append only: never reorder, remove,
+rename or reuse an entry. Agree the position with the CTLV maintainers (CTLV
+SUBSYSTEM in MAINTAINERS) before adding a family, since families added in
+parallel trees would otherwise end up with the same ID. The output manifest
+says where the generated UAPI, kernel and userspace files go.
+
+Generate the files and record the new family's ABI::
+
+ make -C tools/ctlv regen
+
+``regen`` names each file it writes. A new family should add its generated
+files, its snapshot, ``Documentation/ctlv/abi/ctlv-example.yaml``, its entry
+in ``Documentation/ctlv/abi/families.registry.yaml`` and one line in
+``include/uapi/ctlv/ctlv_families_gen.h``, and change nothing generated for
+another family. Anything else means the specification or the generator
+changed more than intended. ``regen`` refuses a specification change that
+breaks the ABI as last committed unless ``ABI_BREAK=1`` says it is meant, so a
+snapshot's diff shows every ABI change for review. ``make -C tools/ctlv``
+checks the same without writing anything.
+
+Never edit a generated file; change the YAML or the generator and regenerate.
+
+Create and destroy a device
+===========================
+
+CTLV comes in two parts. The engine in ``lib/ctlv/`` is what a subsystem embeds
+in its devices and calls from their file operations. The nodes, declared in
+``<linux/ctlv_device.h>``, are character devices of the subsystem's own class,
+with the engine's file operations.
+
+A device's CTLV state is a ``struct ctlv``, embedded in the structure of
+whoever gives the device. A callback gets the instance and reaches that
+structure with ``container_of()``. The node is a ``struct ctlv_device``, which
+the structure embeds beside the instance:
+
+.. code-block:: c
+
+ struct example_device {
+ struct ctlv_device ctlvdev;
+ struct ctlv ctlv;
+ u64 items_count;
+ u64 generation;
+ };
+
+ static struct example_device *to_example(struct ctlv *ctlv)
+ {
+ return container_of(ctlv, struct example_device, ctlv);
+ }
+
+The subsystem's class is a ``struct ctlv_device_class``, registered once with
+``ctlv_device_class_register()``, which takes its device numbers. Its
+``dev_release`` frees the structure, so an open node holds the module that
+registered the class:
+
+.. code-block:: c
+
+ static void example_release(struct device *dev)
+ {
+ struct example_device *example =
+ container_of(dev, struct example_device, ctlvdev.dev);
+
+ ctlv_fini(&example->ctlv);
+ ctlv_device_fini(&example->ctlvdev);
+ kfree(example);
+ }
+
+ static struct ctlv_device_class example_class = {
+ .class = {
+ .name = "example",
+ .dev_release = example_release,
+ },
+ .max_devices = 16,
+ };
+
+A device of the class is then made:
+
+.. code-block:: c
+
+ example = kzalloc(sizeof(*example), GFP_KERNEL);
+ if (!example)
+ return -ENOMEM;
+ ret = ctlv_device_init(&example->ctlvdev, &example_class, parent,
+ &example->ctlv);
+ if (!ret)
+ ret = ctlv_init(&example->ctlv, &example->ctlvdev.dev,
+ &ctlv_example_family, NULL);
+ if (ret)
+ goto err_put;
+ ctlv_publish(&example->ctlv);
+ ret = ctlv_device_add(&example->ctlvdev);
+ if (ret)
+ goto err_unpublish;
+
+``ctlv_device_init()`` names the node after the class, ``/dev/example0``.
+``ctlv_init()`` resolves the family chain. Until the instance is published, the
+driver selects its optional operations and their initial dynamic state, see
+Hooks and availability. ``ctlv_publish()`` makes operations live, and only then
+may the node be added, as a program may open it as soon as it appears.
+``ctlv_device_add()`` adds the node, whose uevent names the family; that is how
+udev finds the device. Destruction is the reverse:
+
+.. code-block:: c
+
+ ctlv_device_del(&example->ctlvdev);
+ ctlv_unpublish(&example->ctlv);
+ put_device(&example->ctlvdev.dev);
+
+``ctlv_unpublish()`` waits for the operations in flight; no callback runs
+afterwards. Each open file description fails its operations with ``-ENODEV``.
+The structure is freed with the last reference, the device's or an open file's,
+so what the callbacks used may stay in it. From ``ctlv_device_init()`` on,
+``put_device()`` alone unwinds a failed initialization, as it does a failed add
+once the instance is unpublished.
+
+A file of another subsystem
+---------------------------
+
+A subsystem may serve the instance on a file of its own instead, a misc device
+whose other operations are the subsystem's, say. It publishes the instance
+around the life of that file, attaches CTLV to each open file and passes on its
+ioctls:
+
+.. code-block:: c
+
+ static int foo_open(struct inode *inode, struct file *filp)
+ {
+ struct foo *foo = container_of(filp->private_data, struct foo, misc);
+ struct ctlv_file *cfile = ctlv_file_open(&foo->ctlv, filp);
+
+ if (IS_ERR(cfile))
+ return PTR_ERR(cfile);
+ filp->private_data = cfile;
+ return 0;
+ }
+
+ static long foo_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
+ {
+ long ret = ctlv_file_ioctl(filp->private_data, cmd, arg);
+
+ return ret == -ENOIOCTLCMD ? foo_own_ioctl(filp, cmd, arg) : ret;
+ }
+
+Its release calls ``ctlv_file_close()``. The file's uevent is the other
+subsystem's and names no family, so a program opens the file by its name.
+
+The ``struct ctlv_hooks`` passed to ``ctlv_init()`` holds the device's pre and
+post hooks, see `Hooks`_. The device structure's release calls ``ctlv_fini()``
+when the last reference goes.
+
+Implement operations
+====================
+
+Action
+------
+
+Generated getters read only what validation accepted, and generated
+serializers write only what the reply declares:
+
+.. code-block:: c
+
+ int ctlv_example_item_set_action(struct ctlv *ctlv,
+ struct ctlv_op_ctx *ctx,
+ const struct ctlv_cursor *request,
+ struct ctlv_cursor *reply)
+ {
+ struct example_device *example = to_example(ctlv);
+ u64 index = ctlv_example_item_set_index_get(request);
+
+ if (item_busy(example, index))
+ return ctlv_op_error(ctx, -EBUSY, "item is busy");
+ return item_set(example, index);
+ }
+
+Use ``_present()`` for optional fields and ``_for_each()`` for repeated
+ones. ``ctlv_op_error()`` preserves the errno and adds a short diagnostic when
+the caller supplied enough reply space.
+
+Query
+-----
+
+.. code-block:: c
+
+ int ctlv_example_item_get_query(struct ctlv *ctlv,
+ struct ctlv_op_ctx *ctx,
+ const struct ctlv_cursor *request,
+ struct ctlv_cursor *reply)
+ {
+ struct example_device *example = to_example(ctlv);
+ u64 generation = READ_ONCE(example->generation);
+ u64 cursor = ctlv_op_query_cursor(ctx);
+ struct ctlv_cursor item;
+ u64 i = cursor ? cursor - 1 : 0;
+
+ if (cursor && ctlv_op_query_generation(ctx) != generation)
+ return -ESTALE;
+ for (; i < example->items_count; i++) {
+ if (!ctlv_example_item_get_items_start(reply, &item))
+ return ctlv_op_query_more(ctx, generation, i + 1);
+ if (!ctlv_example_item_get_items_index_put(&item, i)) {
+ ctlv_record_cancel(&item);
+ return ctlv_op_query_more(ctx, generation, i + 1);
+ }
+ ctlv_record_end(&item);
+ }
+ return ctlv_op_query_done(ctx, generation);
+ }
+
+A continuation cursor is nonzero and advances. Every continuing fragment
+completes a logical leaf. An item that runs out of room partway is dropped with
+``ctlv_record_cancel()``, for the next fragment to write again. Call
+``ctlv_op_query_more()`` while cursors for open continuation records are alive.
+A generation mismatch returns ``-ESTALE``.
+
+Hooks and availability
+======================
+
+Hooks
+-----
+
+The pre and post hooks of the ``struct ctlv_hooks`` passed to ``ctlv_init()``
+run around every action and query the core does not answer. An op declared
+with ``pre-post: true`` also runs hooks of its own family, inside the
+device's:
+
+.. code-block:: yaml
+
+ item-get:
+ type: query
+ pre-post: true
+
+The family then implements ``ctlv_example_item_get_pre()`` and
+``ctlv_example_item_get_post()``, and the order is::
+
+ device pre -> operation pre -> availability check -> callback
+ -> operation post -> device post
+
+Each successful pre hook has one post hook. A failing pre hook undoes its own
+work. Hooks may sleep and must not retain ``ctx``. Each hook and callback gets
+the instance.
+
+Optional operations
+-------------------
+
+An op declared ``optional: true`` is published only on a device whose driver
+provides it before publishing the instance:
+
+.. code-block:: yaml
+
+ item-set:
+ type: action
+ optional: true
+
+.. code-block:: c
+
+ ctlv_example_item_set_provide(&example->ctlv);
+
+On a device that does not provide it, a program finds no entry for the
+operation in ``op-get`` or ``op-schema-get``, and executing it fails with
+``-EOPNOTSUPP``.
+
+Dynamic availability
+--------------------
+
+An op declared ``availability: dynamic`` can be refused for a while on a
+device that publishes it:
+
+.. code-block:: yaml
+
+ item-get:
+ type: query
+ availability: dynamic
+
+.. code-block:: c
+
+ ctlv_example_item_get_disable(&example->ctlv);
+ ctlv_example_item_get_enable(&example->ctlv);
+
+A disabled operation stays in ``op-get``, which reports it disabled, and
+returns ``-EOPNOTSUPP``.
+
+Blobs
+=====
+
+A blob field describes caller-owned memory. Resolve it only in the callback
+that received its request and release every successful resolve before return:
+
+.. code-block:: c
+
+ blob = ctlv_example_item_set_data_resolve(ctx, request);
+ if (IS_ERR(blob))
+ return PTR_ERR(blob);
+
+ ret = ctlv_blob_read(blob, 0, buf, ctlv_blob_len(blob));
+ put_ret = ctlv_blob_put(blob);
+ return ret ?: put_ret;
+
+The schema fixes direction, backing, length, and alignment. Resolution holds
+the backing for the ioctl. An output blob too short for
+what the operation produces is the caller's to enlarge: return
+``ctlv_blob_too_short()`` with the length needed, rather than writing part of
+it.
+
+Only a request carries a blob; the generator refuses one in a reply. A request
+that shares fields with a reply includes them and declares its blob beside them,
+as ``item-set`` does when it includes ``item``.
+
+Inheritance and HW driver fields
+================================
+
+Several HW drivers can implement ``ctlv-example``, and each knows things
+about its devices that a program may want, such as which bank of the foo
+chip's SRAM holds the items. Those do not belong in ``ctlv-example``, which
+all of them share. Instead, ``ctlv-example`` leaves a place for them, an
+extension point, and each HW driver has a family of its own that inherits
+``ctlv-example`` and fills it.
+
+Say ``ctlv-example`` gains a system call reporting the device::
+
+ device_get_action() -> { details? };
+
+On a device of the foo driver, ``details`` holds foo's fields::
+
+ device_get_action() -> { details? { u64 sram_bank; }; };
+
+``ctlv-example`` declares the op and leaves the contents of ``details`` to the
+HW driver, with an extension point:
+
+.. code-block:: yaml
+
+ ops:
+ # Reports the device.
+ device-get:
+ type: action
+ fd-access: read
+ reply:
+ # Empty here; a HW driver's family fills it with fields of its own.
+ details?: extension device-details
+
+The foo driver's family fills it by declaring a record of the same name:
+
+.. code-block:: yaml
+
+ # The devices of the foo driver, which fill what ctlv-example leaves open.
+ family: ctlv-example-foo
+ inherits: ctlv-example
+
+ records:
+ # The extension point ctlv-example left empty, filled for foo devices.
+ device-details:
+ # The bank of the foo chip's SRAM that holds the items.
+ sram-bank: uint(..3)
+
+The foo driver initializes its instances with ``ctlv_example_foo_family``, and
+they answer every ``ctlv-example`` op. A family implements only the ops it
+declares, so the inherited ones keep the callbacks and hooks of
+``ctlv-example``, and the foo module depends on the module that implements
+``ctlv-example``.
+
+The ``device-get`` callback in ``ctlv-example`` opens the record and has the
+device's family fill it:
+
+.. code-block:: c
+
+ if (ctlv_example_device_get_details_start(reply, &details)) {
+ ret = ctlv_ext_fill(ctx, NULL, &details);
+ ctlv_record_end(&details);
+ if (ret)
+ return ret;
+ }
+
+The foo family implements the generated fill callback:
+
+.. code-block:: c
+
+ int ctlv_example_foo_device_details_fill(struct ctlv *ctlv,
+ struct ctlv_op_ctx *ctx,
+ const struct ctlv_cursor *request,
+ struct ctlv_cursor *record)
+ {
+ struct foo_device *foo = container_of(ctlv, struct foo_device,
+ example.ctlv);
+
+ ctlv_example_foo_device_details_sram_bank_put(record,
+ foo->sram_bank);
+ return 0;
+ }
+
+``request`` is the matching record of the request, for an op whose request has
+an extension point too; ``device-get`` has none and passes ``NULL``. An
+extension point no family fills adds no record and no length. ``ctlv-example``
+never names ``ctlv-example-foo``, and the foo family names nothing of
+``ctlv-example`` beyond the extension point it fills.
+
+A program knows whose fields it is reading without looking at them. A
+device's family chain runs from the root to the family of its instance,
+here ``ctlv``, ``ctlv-example`` and ``ctlv-example-foo``, and that last one,
+the leaf, is the HW driver's. ``family-get`` reports the chain on the open
+device, so a program picks its handler for the HW driver's fields once, when
+it opens the device; the uevent carries the same chain for udev rules. A tool
+that knows only ``ctlv-example`` skips the TLVs it does not know, as on any
+reply. Each TLV in a filled record also carries the family that declared its
+field, in bits 63..48 of its type: ``CTLV_TYPE_FAMILY()`` extracts it and
+``family-get`` maps it to a name.
+
+An enum can be left open the same way, when each HW driver has values of its
+own, such as the data formats its devices produce. ``ctlv-example`` declares
+the enum as an enum extension point, and its fields name it as any enum:
+
+.. code-block:: yaml
+
+ enums:
+ # The format of a device's data; a HW driver's family gives the values.
+ data-format: extension
+
+The foo driver's family gives the values, by declaring an enum of that name:
+
+.. code-block:: yaml
+
+ enums:
+ # Fills ctlv-example's data-format.
+ data-format:
+ # The foo device's own layout.
+ - foo-raw
+
+One family of a chain gives the values, numbered from 1 and appended to as any
+enum's. The core takes only those values on a device of that family, in a
+request and in what its driver puts in a reply, and none on a device whose
+chain gives none. The names are in the uapi header of the family that gives
+them, such as ``CTLV_EXAMPLE_FOO_DATA_FORMAT_FOO_RAW``. A family that checks
+values a HW driver hands it, before any reaches a reply, asks
+``ctlv_enum_ext_max()`` for the highest the driver's family gives.
+
+The structures embed one another as the families inherit. The module
+implementing ``ctlv-example`` keeps what every device of that family has in a
+structure around the instance, and exports what initializes it for a family
+that inherits ``ctlv-example``. A HW driver embeds that structure in its own
+and describes itself there, for the ``ctlv-example`` callbacks to use:
+
+.. code-block:: c
+
+ struct ctlv_example_device {
+ struct ctlv ctlv;
+ const struct ctlv_example_provider *provider;
+ };
+
+ struct foo_device {
+ struct ctlv_device ctlvdev;
+ struct ctlv_example_device example;
+ u32 sram_bank;
+ };
+
+ static const struct ctlv_example_provider foo_provider = {
+ .name = "foo",
+ .ops = &foo_provider_ops,
+ };
+
+ foo = kzalloc(sizeof(*foo), GFP_KERNEL);
+ if (!foo)
+ return -ENOMEM;
+ ret = ctlv_device_init(&foo->ctlvdev, &foo_class, parent,
+ &foo->example.ctlv);
+ if (!ret)
+ ret = ctlv_example_device_init(&foo->example, &foo->ctlvdev.dev,
+ &ctlv_example_foo_family,
+ &foo_provider);
+
+The node is of the foo driver's own class, ``foo_class``, registered as
+``example_class`` is above. ``ctlv_example_device_init()`` refuses a family
+that ``ctlv_family_inherits()`` says is not ``ctlv-example`` or a descendant,
+then calls ``ctlv_init()``.
+
+Reporting an error
+------------------
+
+A callback returns a negative errno, and that is what the ioctl returns. Where
+CTLV itself refuses a message it also writes error information into the
+caller's reply buffer, naming the reason and the offset of the offending
+TLV. A family implementation does not build that; it adds its own errno
+and its reply.
+
+Configuration
+=============
+
+``CONFIG_CTLV``
+ CTLV. A family implementation selects it.
+
+Kernel API
+==========
+
+.. kernel-doc:: include/linux/ctlv.h
+.. kernel-doc:: lib/ctlv/core.c
+ :export:
+.. kernel-doc:: lib/ctlv/builder.c
+ :export:
+.. kernel-doc:: lib/ctlv/schema.c
+ :export:
+.. kernel-doc:: lib/ctlv/blob.c
+ :export:
+.. kernel-doc:: include/linux/ctlv_device.h
+.. kernel-doc:: lib/ctlv/device.c
+ :export:
diff --git a/Documentation/driver-api/index.rst b/Documentation/driver-api/index.rst
index aaf5d76ed074..0c5b9d2165d8 100644
--- a/Documentation/driver-api/index.rst
+++ b/Documentation/driver-api/index.rst
@@ -37,6 +37,7 @@ of interest to most developers working on device drivers.
early-userspace/index
connector
+ ctlv
device-io
devfreq
dma-buf
diff --git a/MAINTAINERS b/MAINTAINERS
index 29bdb4de915d..b16bedbbe0b6 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -7188,6 +7188,7 @@ M: Jiri Pirko <jiri@resnulli.us>
L: linux-kernel@vger.kernel.org
S: Maintained
F: Documentation/ctlv/
+F: Documentation/driver-api/ctlv.rst
F: Documentation/userspace-api/ctlv.rst
F: include/linux/ctlv*.h
F: include/uapi/ctlv/
--
2.56.0
^ permalink raw reply related [flat|nested] 24+ messages in thread
* [PATCH RFC v2 14/15] tools/ctlv: Add the userspace library
2026-11-13 15:54 [PATCH RFC v2 00/15] ctlv: Add a schema-driven TLV uAPI for character devices Jiri Pirko
` (12 preceding siblings ...)
2026-11-13 15:55 ` [PATCH RFC v2 13/15] Documentation: ctlv: Describe how to write a family Jiri Pirko
@ 2026-11-13 15:55 ` Jiri Pirko
2026-11-13 16:28 ` sashiko-bot
2026-11-13 15:55 ` [PATCH RFC v2 15/15] tools/ctlv: Add ctlvctl Jiri Pirko
14 siblings, 1 reply; 24+ messages in thread
From: Jiri Pirko @ 2026-11-13 15:55 UTC (permalink / raw)
To: linux-coco
Cc: linux-kernel, linux-api, x86, kvm, linux-doc, jgg, david.quigley,
lukas, ksinyuk, rodrigo.vivi, cedric.xing, sameo, drdeeglaze,
James.Bottomley, aik, jpiotrowski, qinkun, pgonda, jic23,
alistair.francis, ankita, suzuki.poulose, kas, rick.p.edgecombe,
elena.reshetova, dave.hansen, sathyanarayanan.kuppuswamy,
peter.fang, jarkko, zohar, yeoreum.yun, aneesh.kumar, dave.jiang,
nik.borisov, sergii.dmytruk
From: Jiri Pirko <jiri@nvidia.com>
Provide family-independent userspace transport, message handling,
discovery, schemas and blob descriptors. Generated bindings build on
this API.
Assisted-by: Cursor: GPT-5.6 Sol
Assisted-by: Cursor: Claude Opus 5.5
Signed-off-by: Jiri Pirko <jiri@nvidia.com>
---
.../ctlv/generated-files-schema.yaml | 4 +
Documentation/ctlv/generated-files.yaml | 1 +
Documentation/driver-api/ctlv.rst | 2 +-
Documentation/userspace-api/ctlv.rst | 17 +
tools/ctlv/Makefile | 8 +-
tools/ctlv/ctlvgen/cli.py | 8 +-
tools/ctlv/ctlvgen/lib/gen_user.py | 808 ++++++++++++++++++
tools/ctlv/ctlvgen/lib/outputs.py | 12 +-
tools/ctlv/libctlv/Makefile | 41 +
tools/ctlv/libctlv/ctlv.h | 413 +++++++++
tools/ctlv/libctlv/ctlv_user_gen.c | 551 ++++++++++++
tools/ctlv/libctlv/device.c | 412 +++++++++
tools/ctlv/libctlv/internal.h | 161 ++++
tools/ctlv/libctlv/introspection.c | 238 ++++++
tools/ctlv/libctlv/message.c | 518 +++++++++++
tools/ctlv/libctlv/schema.c | 742 ++++++++++++++++
tools/ctlv/libctlv/spec.c | 186 ++++
tools/ctlv/libctlv/transport.c | 572 +++++++++++++
18 files changed, 4689 insertions(+), 5 deletions(-)
create mode 100644 tools/ctlv/ctlvgen/lib/gen_user.py
create mode 100644 tools/ctlv/libctlv/Makefile
create mode 100644 tools/ctlv/libctlv/ctlv.h
create mode 100644 tools/ctlv/libctlv/ctlv_user_gen.c
create mode 100644 tools/ctlv/libctlv/device.c
create mode 100644 tools/ctlv/libctlv/internal.h
create mode 100644 tools/ctlv/libctlv/introspection.c
create mode 100644 tools/ctlv/libctlv/message.c
create mode 100644 tools/ctlv/libctlv/schema.c
create mode 100644 tools/ctlv/libctlv/spec.c
create mode 100644 tools/ctlv/libctlv/transport.c
diff --git a/Documentation/ctlv/generated-files-schema.yaml b/Documentation/ctlv/generated-files-schema.yaml
index d3529ce6e8e7..d5f2d3763c48 100644
--- a/Documentation/ctlv/generated-files-schema.yaml
+++ b/Documentation/ctlv/generated-files-schema.yaml
@@ -20,6 +20,10 @@ $defs:
$ref: '#/$defs/path'
kernel-source:
$ref: '#/$defs/path'
+ user-header:
+ $ref: '#/$defs/path'
+ user-source:
+ $ref: '#/$defs/path'
registry:
type: object
diff --git a/Documentation/ctlv/generated-files.yaml b/Documentation/ctlv/generated-files.yaml
index 5488ddf40e17..d4766b176ceb 100644
--- a/Documentation/ctlv/generated-files.yaml
+++ b/Documentation/ctlv/generated-files.yaml
@@ -15,3 +15,4 @@ families:
uapi-header: include/uapi/ctlv/ctlv_gen.h
kernel-header: include/linux/ctlv_gen.h
kernel-source: lib/ctlv/ctlv_gen.c
+ user-source: tools/ctlv/libctlv/ctlv_user_gen.c
diff --git a/Documentation/driver-api/ctlv.rst b/Documentation/driver-api/ctlv.rst
index 34d20c14a9bf..f6a4f9632e3a 100644
--- a/Documentation/driver-api/ctlv.rst
+++ b/Documentation/driver-api/ctlv.rst
@@ -312,7 +312,7 @@ driver selects its optional operations and their initial dynamic state, see
Hooks and availability. ``ctlv_publish()`` makes operations live, and only then
may the node be added, as a program may open it as soon as it appears.
``ctlv_device_add()`` adds the node, whose uevent names the family; that is how
-udev finds the device. Destruction is the reverse:
+libctlv and udev find the device. Destruction is the reverse:
.. code-block:: c
diff --git a/Documentation/userspace-api/ctlv.rst b/Documentation/userspace-api/ctlv.rst
index f1f775071a78..8a8ade33b490 100644
--- a/Documentation/userspace-api/ctlv.rst
+++ b/Documentation/userspace-api/ctlv.rst
@@ -118,6 +118,21 @@ The op ID and the types come from the family's generated UAPI header.
Before the family sees the request, the kernel checks it against the
specification: ``index`` must be at most 4095 and ``label`` at most 63 bytes.
+``libctlv`` and the generated bindings build and send the same message:
+
+.. code-block:: c
+
+ struct ctlv_error error;
+ struct ctlv_msg *req;
+ struct ctlv_dev *dev;
+
+ /* Error checks left out. */
+ ctlv_open("ctlv-example", 0, O_RDWR, &dev);
+ ctlv_example_item_set_request_new(&req);
+ ctlv_example_item_set_index_put(req, 3);
+ ctlv_example_item_set_label_put(req, "ab");
+ ctlv_example_item_set(dev, req, &error);
+
The sections below define each piece: the device, the message format, the
ioctl, queries, blobs and introspection, and then the rules of compatibility.
@@ -410,6 +425,8 @@ defined flags. The root cannot carry the flag, and replies never do.
So the kernel acts on what it knows and ignores the rest. A caller that needs
to know which of its fields will be acted on asks ``op-schema-get`` first.
+``libctlv`` sets the flag with ``ctlv_msg_ignore_if_unknown()`` on the TLV
+written last.
Executing an operation
======================
diff --git a/tools/ctlv/Makefile b/tools/ctlv/Makefile
index bdec011595aa..881fae9127c7 100644
--- a/tools/ctlv/Makefile
+++ b/tools/ctlv/Makefile
@@ -30,10 +30,16 @@ lint:
python3 -m pyflakes $(PYSRC)
python3 -m mypy $(PYSRC)
+# The C half of this directory: the userspace library, which is not generated
+# and not Python, so it is its own target rather than part of the checks above.
+programs:
+ $(MAKE) -C libctlv
+
clean:
rm -rf ctlvgen/__pycache__
rm -rf ctlvgen/lib/__pycache__
rm -rf ctlvgen.egg-info
rm -rf build
+ $(MAKE) -C libctlv clean
-.PHONY: all regen lint clean
+.PHONY: all regen lint programs clean
diff --git a/tools/ctlv/ctlvgen/cli.py b/tools/ctlv/ctlvgen/cli.py
index 78e09cfd620e..59180f9757e8 100755
--- a/tools/ctlv/ctlvgen/cli.py
+++ b/tools/ctlv/ctlvgen/cli.py
@@ -16,6 +16,7 @@ sys.path.append(pathlib.Path(__file__).resolve().parents[1].as_posix())
from ctlvgen.lib import cgen # noqa: E402
from ctlvgen.lib import gen_kernel # noqa: E402
from ctlvgen.lib import gen_uapi # noqa: E402
+from ctlvgen.lib import gen_user # noqa: E402
from ctlvgen.lib import semantics # noqa: E402
from ctlvgen.lib import spec # noqa: E402
from ctlvgen.lib import tree # noqa: E402
@@ -27,6 +28,8 @@ GENERATORS: dict[tuple[str, str], Callable[..., str]] = {
('uapi', 'header'): gen_uapi.render,
('kernel', 'header'): gen_kernel.render_header,
('kernel', 'source'): gen_kernel.render_source,
+ ('user', 'header'): gen_user.render_header,
+ ('user', 'source'): gen_user.render_source,
}
@@ -50,7 +53,7 @@ def main() -> int:
help='regenerate every file in the output manifests '
'and every ABI snapshot')
action.add_argument('--mode',
- choices=('uapi', 'kernel'),
+ choices=('uapi', 'kernel', 'user'),
help='generate the files of this mode')
parser.add_argument('--schema-only', action='store_true',
help='omit the family API from kernel output')
@@ -105,6 +108,9 @@ def main() -> int:
family = semantics.load(paths)
if args.validate:
cgen.symbols(family)
+ # The root family has no binding: libctlv is that binding.
+ if family.name != spec.CORE_FAMILY_NAME:
+ cgen.user_symbols(family)
else:
assert file is not None
_generate(args, file, family)
diff --git a/tools/ctlv/ctlvgen/lib/gen_user.py b/tools/ctlv/ctlvgen/lib/gen_user.py
new file mode 100644
index 000000000000..060a142ba813
--- /dev/null
+++ b/tools/ctlv/ctlvgen/lib/gen_user.py
@@ -0,0 +1,808 @@
+# SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+"""The userspace binding of one family.
+
+The header is names, and a child's includes its parent's. The source is the
+specification as data: one struct ctlv_node_schema tree per op message, in the
+shape op-schema-get reports, so a device can be compared with it.
+"""
+
+from __future__ import annotations
+
+import dataclasses
+
+from . import cgen
+from . import semantics
+from . import sizes
+from . import spec
+from .errors import CtlvError
+
+# What every wrapper starts and ends with. A caller wanting only the errno
+# passes NULL for the error.
+DEV_PARAM: str = 'struct ctlv_dev *dev'
+ERROR_PARAM: str = 'struct ctlv_error *error'
+
+# What a request is built in, and what an answer arrives in.
+REQUEST_PARAM: str = 'struct ctlv_msg *request'
+REPLY_PARAM: str = 'struct ctlv_reply **reply'
+
+# The record a reader reads from, and the one TLV it found there.
+RECORD_PARAM: str = 'const struct ctlv_hdr *record'
+FOUND: str = 'hdr'
+
+# What a getter of each schema kind hands back, and the libctlv reader that
+# answers it. An enum and a flags payload are read as the integer they are.
+READERS: dict[str, tuple[tuple[str, ...], str, tuple[str, ...]]] = {
+ 'uint': (('__u64 *value',), 'ctlv_get_u64', ()),
+ 'sint': (('__s64 *value',), 'ctlv_get_s64', ()),
+ 'bool': (('bool *value',), 'ctlv_get_bool', ()),
+ 'string': (('const char **value',), 'ctlv_get_str', ()),
+ 'binary': (('const void **value', '__u32 *len'), 'ctlv_get_bin',
+ ('value', 'len')),
+ 'enum': (('__u64 *value', 'bool *known'), 'ctlv_get_enum',
+ ('value', 'known')),
+ 'flags': (('__u64 *value', '__u64 *unknown'), 'ctlv_get_flags',
+ ('value', 'unknown')),
+}
+
+# What a putter of each schema kind takes, and the libctlv append it wraps.
+WRITERS: dict[str, tuple[tuple[str, ...], str]] = {
+ 'uint': (('__u64 value',), 'ctlv_put_u64'),
+ 'sint': (('__s64 value',), 'ctlv_put_s64'),
+ 'bool': (('bool value',), 'ctlv_put_bool'),
+ 'string': (('const char *value',), 'ctlv_put_str'),
+ 'binary': (('const void *value', '__u32 len'), 'ctlv_put_bin'),
+ 'enum': (('__u64 value',), 'ctlv_put_u64'),
+ 'flags': (('__u64 value',), 'ctlv_put_u64'),
+}
+
+# Which schema member each length of an op fills, and its name in the header.
+LENGTHS: tuple[tuple[str, str], ...] = tuple(cgen.USER_LENGTHS.items())
+
+# The lengths an op table entry holds, by sizes.OpSizes member.
+TABLE_LENGTHS: tuple[str, ...] = tuple(
+ field.name for field in dataclasses.fields(sizes.OpSizes))
+
+# Bits of struct ctlv_node_schema's present mask, by the member they cover.
+PRESENT: dict[str, str] = {
+ 'min': 'CTLV_NODE_SCHEMA_HAS_MIN',
+ 'max': 'CTLV_NODE_SCHEMA_HAS_MAX',
+ 'min_length': 'CTLV_NODE_SCHEMA_HAS_MIN_LENGTH',
+ 'max_length': 'CTLV_NODE_SCHEMA_HAS_MAX_LENGTH',
+ 'flags_mask': 'CTLV_NODE_SCHEMA_HAS_FLAGS_MASK',
+ 'unit': 'CTLV_NODE_SCHEMA_HAS_UNIT',
+ 'scale': 'CTLV_NODE_SCHEMA_HAS_SCALE',
+ 'blob': 'CTLV_NODE_SCHEMA_HAS_BLOB',
+ 'reenters': 'CTLV_NODE_SCHEMA_HAS_REENTERS',
+}
+
+# What a blob node contributes, in the order the core reports it, and the
+# root family's enum naming the backings it allows.
+BLOB_MEMBERS: tuple[str, ...] = ('blob_direction', 'blob_backings',
+ 'blob_max_length', 'blob_alignment')
+BLOB_TYPES: str = 'blob-type'
+
+# The extremes of an unbounded integer bound, stored as the unsigned pattern of
+# the value. The most negative signed one has no literal.
+EXTREMES: dict[tuple[str, int], str] = {
+ ('uint', spec.U64_MAX): 'UINT64_MAX',
+ ('sint', spec.S64_MIN): '(__u64)INT64_MIN',
+ ('sint', spec.S64_MAX): 'INT64_MAX',
+}
+
+OP_TYPES: dict[str, str] = {
+ op_type: cgen.macro(spec.CORE_FAMILY_NAME, 'op-type', op_type)
+ for op_type in ('action', 'query')
+}
+
+FD_ACCESSES: dict[str, str] = {
+ access: cgen.macro(spec.CORE_FAMILY_NAME, 'op-fd-access', access)
+ for access in spec.FD_ACCESSES
+}
+
+AVAILABILITIES: dict[str, str] = {
+ availability: cgen.macro(spec.CORE_FAMILY_NAME, 'op-availability',
+ availability)
+ for availability in spec.AVAILABILITIES
+}
+
+
+def _reject_root(family: semantics.Family) -> None:
+ """The root family's userspace API is provided by libctlv."""
+ if family.name == spec.CORE_FAMILY_NAME:
+ raise CtlvError(f'{family.path}: family {family.name}',
+ 'the root family has no generated userspace binding, '
+ 'because libctlv implements its ops directly as the '
+ 'bootstrap every caller starts from')
+
+
+def render_header(family: semantics.Family) -> str:
+ """Return the userspace binding header of family as text."""
+ _reject_root(family)
+ names = cgen.Names(family)
+ cgen.symbols(family)
+ cgen.user_symbols(family)
+ guard = names.user_guard()
+
+ writer = cgen.preamble(family, 'user', 'header')
+ writer.comment(f'Userspace binding of the {family.name} CTLV family.')
+ writer.line()
+ writer.open_guard(guard)
+ writer.includes(*_includes(family, names))
+
+ writer.comment('What this family\'s specification says, for holding a '
+ 'device against it.')
+ writer.declaration('extern const struct ctlv_family_spec',
+ f'{names.user_spec()};')
+ _ops(writer, names, family)
+ _writers(writer, names, family)
+ _readers(writer, names, family)
+ writer.line()
+ writer.close_guard(guard)
+ return writer.text()
+
+
+def _includes(family: semantics.Family, names: cgen.Names) -> tuple[str, ...]:
+ """libctlv, this family's constants, and the parent's binding."""
+ headers = ['<ctlv.h>', '', names.uapi_header()]
+ if family.parent != spec.CORE_FAMILY_NAME:
+ headers += ['', cgen.Names(family.chain[-2]).user_header()]
+ return tuple(headers)
+
+
+def _ops(writer: cgen.Writer, names: cgen.Names,
+ family: semantics.Family) -> None:
+ """One call per action and query this family declares.
+
+ A wrapper needing a reply buffer asks the device for the length.
+ """
+ ops = family.local_ops
+ if not ops:
+ return
+ writer.line()
+ writer.comment('Executing an op. A query is reassembled into the one '
+ 'reply it fills.')
+ for op in ops:
+ writer.line()
+ if op.request is not None:
+ _lengths(writer, names, op)
+ _request_new(writer, names, op)
+ writer.line()
+ _wrapper(writer, names, op)
+
+
+def _lengths(writer: cgen.Writer, names: cgen.Names, op: semantics.Op) -> None:
+ """The longest request of one op, as this specification computed."""
+ for field, what in LENGTHS:
+ value = getattr(op.sizes, field)
+ if value:
+ writer.define(names.user_op_len(op, what), str(value))
+ writer.line()
+
+
+def _request_new(writer: cgen.Writer, names: cgen.Names,
+ op: semantics.Op) -> None:
+ """A request buffer this op's longest request fits in.
+
+ Only the size to start at: past it the message grows to what the transport
+ allows, a descendant's device taking more.
+ """
+ writer.signature('static inline int',
+ names.user_op_helper(op, 'request_new'),
+ ('struct ctlv_msg **request',), ')')
+ writer.line('{')
+ writer.declaration('\tconst __u32 len =',
+ f'{names.user_op_len(op, "request-max-len")};')
+ writer.line()
+ writer.line('\treturn ctlv_msg_new(len, request);')
+ writer.line('}')
+
+
+def _wrapper(writer: cgen.Writer, names: cgen.Names, op: semantics.Op) -> None:
+ """The one call that executes one op."""
+ params = [DEV_PARAM]
+ if op.request is not None:
+ params.append(REQUEST_PARAM)
+ if op.reply is not None:
+ params.append(REPLY_PARAM)
+ params.append(ERROR_PARAM)
+ writer.signature('static inline int', names.user_op(op), tuple(params),
+ ')')
+ writer.line('{')
+ request = 'request' if op.request is not None else 'NULL'
+ reply = 'reply' if op.reply is not None else 'NULL'
+ call = 'ctlv_query' if op.type == 'query' else 'ctlv_action'
+ arguments = ('dev', names.op_id(op), request, reply, 'error')
+ writer.parenthesized(f'\treturn {call}(', arguments, ');')
+ writer.line('}')
+
+
+def _writers(writer: cgen.Writer, names: cgen.Names,
+ family: semantics.Family) -> None:
+ """What an application puts into a request, one putter per field.
+
+ Every field has one, so a test can put a field where it does not belong.
+ """
+ fields = family.local_fields
+ if not fields:
+ return
+ writer.line()
+ writer.comment('Building a request. ctlv_msg_err() reports the first '
+ 'failure.')
+ for field in fields:
+ writer.line()
+ writer.doc(field.decl.doc)
+ if field.decl.kind == 'blob':
+ _blob_putters(writer, names, field)
+ elif field.decl.kind == 'record':
+ _record_opener(writer, names, field)
+ else:
+ _putter(writer, names, field)
+
+
+def _record_opener(writer: cgen.Writer, names: cgen.Names,
+ field: semantics.Field) -> None:
+ writer.signature('static inline int', names.field_helper(field, 'start'),
+ ('struct ctlv_msg *msg',), ')')
+ writer.line('{')
+ writer.parenthesized('\treturn ctlv_record_start(',
+ ('msg', names.field(field)), ');')
+ writer.line('}')
+
+
+def _putter(writer: cgen.Writer, names: cgen.Names,
+ field: semantics.Field) -> None:
+ params, put = WRITERS[field.decl.kind]
+ values = tuple(param.split()[-1].lstrip('*') for param in params)
+ writer.signature('static inline int', names.field_helper(field, 'put'),
+ ('struct ctlv_msg *msg', *params), ')')
+ writer.line('{')
+ writer.parenthesized(f'\treturn {put}(',
+ ('msg', names.field(field), *values), ');')
+ writer.line('}')
+
+
+def _blob_putters(writer: cgen.Writer, names: cgen.Names,
+ field: semantics.Field) -> None:
+ """One descriptor writer per backing this field allows."""
+ assert field.decl.blob is not None
+ for first, backing in enumerate(field.decl.blob.backings):
+ suffix, params = cgen.BLOB_PUTTERS[backing]
+ if first:
+ writer.line()
+ values = tuple(param.split()[-1].lstrip('*') for param in params)
+ writer.signature('static inline int',
+ names.field_helper(field, f'put_{suffix}'),
+ ('struct ctlv_msg *msg', *params), ')')
+ writer.line('{')
+ writer.parenthesized(f'\treturn ctlv_put_blob_{suffix}(',
+ ('msg', names.field(field), *values), ');')
+ writer.line('}')
+
+
+def _readers(writer: cgen.Writer, names: cgen.Names,
+ family: semantics.Family) -> None:
+ """What an application reads out of a reply.
+
+ A getter answers whether it read anything: a device wrote it, not the
+ validator.
+ """
+ fields = [field for field in family.local_fields
+ if cgen.user_readers(names, field)]
+ if not fields:
+ return
+ writer.line()
+ writer.comment('Reading a reply. A getter answers whether it could '
+ 'read.')
+ for field in fields:
+ writer.line()
+ writer.doc(field.decl.doc)
+ if field.decl.max_occurrences != 1:
+ _iterator(writer, names, field)
+ else:
+ _present(writer, names, field)
+ if cgen.getter_value(field):
+ writer.line()
+ _getter(writer, names, field)
+
+
+def _present(writer: cgen.Writer, names: cgen.Names,
+ field: semantics.Field) -> None:
+ writer.signature('static inline bool',
+ names.field_helper(field, 'present'), (RECORD_PARAM,),
+ ')')
+ writer.line('{')
+ writer.parenthesized('\treturn ctlv_hdr_find(',
+ ('record', names.field(field)), ');')
+ writer.line('}')
+
+
+def _iterator(writer: cgen.Writer, names: cgen.Names,
+ field: semantics.Field) -> None:
+ """Every occurrence of a repeated field, in the device's own order."""
+ writer.define_call(
+ f'{names.field_helper(field, "for_each")}(_hdr, _record)',
+ 'ctlv_for_each_type', '_hdr', '_record', names.field(field))
+
+
+def _getter(writer: cgen.Writer, names: cgen.Names,
+ field: semantics.Field) -> None:
+ """What one field holds, or that it did not hold it.
+
+ A single field is looked up in the record it is given, a repeated one is
+ read where its iterator left the cursor.
+ """
+ getter = names.field_helper(field, 'get')
+ single = field.decl.max_occurrences == 1
+ found = RECORD_PARAM if single else f'const struct ctlv_hdr *{FOUND}'
+
+ if field.decl.kind == 'record':
+ # A record holds no value of its own: what a caller wants is itself.
+ writer.signature('static inline const struct ctlv_hdr',
+ f'*{getter}', (RECORD_PARAM,), ')')
+ writer.line('{')
+ writer.parenthesized('\treturn ctlv_hdr_find(',
+ ('record', names.field(field)), ');')
+ writer.line('}')
+ return
+
+ kind = field.decl.kind
+ if _extension_enum(names, field):
+ # What is known depends on the device's family, so it is a number.
+ kind = 'uint'
+ params, reader, extra = READERS[kind]
+ writer.signature('static inline bool', getter, (found, *params), ')')
+ writer.line('{')
+ if single:
+ writer.line(f'\tconst struct ctlv_hdr *{FOUND};')
+ writer.line()
+ writer.parenthesized(f'\t{FOUND} = ctlv_hdr_find(',
+ ('record', names.field(field)), ');')
+ arguments = (FOUND,) + _reader_arguments(names, field, extra)
+ writer.parenthesized(f'\treturn {FOUND} && !{reader}(', arguments, ');')
+ writer.line('}')
+
+
+def _reader_arguments(names: cgen.Names, field: semantics.Field,
+ extra: tuple[str, ...]) -> tuple[str, ...]:
+ """What the libctlv reader takes beyond the TLV it reads.
+
+ An enum and a flags payload are read against what this caller knows, so
+ anything outside is reported as a newer device's.
+ """
+ if (field.decl.kind not in ('enum', 'flags') or
+ _extension_enum(names, field)):
+ return extra or ('value',)
+ assert field.decl.ref is not None
+ definition = names.effective.definitions[field.decl.ref]
+ if field.decl.kind == 'flags':
+ return (names.value_mask(definition),) + extra
+ if definition.header:
+ return cgen.header_bounds(definition) + extra
+ numbers = definition.numbers()
+ return (str(min(numbers)), str(max(numbers))) + extra
+
+
+def _extension_enum(names: cgen.Names, field: semantics.Field) -> bool:
+ if field.decl.kind != 'enum':
+ return False
+ assert field.decl.ref is not None
+ return names.effective.definitions[field.decl.ref].extension is not None
+
+
+class ComposedNode:
+ """One node of a composed tree, with the members that arrive at it.
+
+ A composed tree is what a device answers, extension points filled in.
+ """
+
+ def __init__(self, node: semantics.Node, reenters: int = 0) -> None:
+ self.node = node
+ self.reenters = reenters
+ self.fields: list[ComposedNode] = []
+ self.optional_fields: list[ComposedNode] = []
+ # The arrays holding this node's members, once they have a name.
+ self.fields_symbol: str | None = None
+ self.optional_symbol: str | None = None
+
+ @property
+ def members(self) -> list[ComposedNode]:
+ return self.fields + self.optional_fields
+
+
+def compose(family: semantics.Family, root: semantics.Node) -> ComposedNode:
+ """Return the composed tree of one op message."""
+ return _place(family, root, ())
+
+
+def _place(family: semantics.Family, node: semantics.Node,
+ stack: tuple[semantics.Node, ...]) -> ComposedNode:
+ """One node and everything below it, fills spliced in."""
+ if node.reenters is not None:
+ return ComposedNode(node, reenters=_levels(node, stack))
+ place = ComposedNode(node)
+ _splice(family, node, False, stack + (node,), place)
+ return place
+
+
+def _levels(node: semantics.Node, stack: tuple[semantics.Node, ...]) -> int:
+ """How far up the node whose members are this one's as well.
+
+ One means the node this one is a member of, as the core reports it.
+ """
+ for levels, above in enumerate(reversed(stack), start=1):
+ if above.expands == node.reenters:
+ return levels
+ # The resolver rejects a re-entry of a record nothing expanded above.
+ raise AssertionError(f'{node.child_field.full_name} re-enters '
+ f'{node.reenters} below nothing that expands it')
+
+
+def _splice(family: semantics.Family, node: semantics.Node,
+ force_optional: bool, stack: tuple[semantics.Node, ...],
+ place: ComposedNode) -> None:
+ """The members of one level: what it declares, then what filled it."""
+ for child in node.required_children:
+ target = place.optional_fields if force_optional else place.fields
+ target.append(_place(family, child, stack))
+ for child in node.optional_children:
+ place.optional_fields.append(_place(family, child, stack))
+ for extension in node.extensions:
+ fill = family.fill_of(extension)
+ if fill is None:
+ # Nothing in this chain filled it, so it contributes no member.
+ continue
+ _splice(family, fill.root, force_optional or extension.optional, stack,
+ place)
+
+
+def _sizing(place: ComposedNode) -> semantics.Node:
+ """One composed tree as an IR node, for the length arithmetic."""
+ if place.reenters:
+ return place.node
+ return dataclasses.replace(
+ place.node,
+ required_children=tuple(_sizing(member) for member in place.fields),
+ optional_children=tuple(_sizing(member)
+ for member in place.optional_fields),
+ extensions=())
+
+
+def render_source(family: semantics.Family) -> str:
+ """Return the userspace binding source of family as text."""
+ names = cgen.Names(family)
+ cgen.symbols(family)
+ if family.name != spec.CORE_FAMILY_NAME:
+ cgen.user_symbols(family)
+
+ ops = _spec_ops(family)
+ writer = cgen.preamble(family, 'user', 'source')
+ writer.includes(*_source_includes(family, names, ops))
+ writer.comment('Effective request and reply schemas.')
+ writer.line()
+ for entry in ops:
+ for which, place in (('request', entry.request),
+ ('reply', entry.reply)):
+ if place is not None:
+ _tree(writer, names, entry.op, which, place)
+ _table(writer, names, ops)
+ _spec(writer, names, family, ops)
+ return writer.text()
+
+
+def _source_includes(family: semantics.Family, names: cgen.Names,
+ ops: tuple[Entry, ...]) -> tuple[str, ...]:
+ """libctlv, and this family's own names where it has any."""
+ headers = ['<ctlv.h>']
+ if _has_integer(ops):
+ headers = ['<stdint.h>', '', '<ctlv.h>']
+ if family.name != spec.CORE_FAMILY_NAME:
+ headers += ['', names.user_header()]
+ return tuple(headers)
+
+
+def _has_integer(ops: tuple[Entry, ...]) -> bool:
+ """Whether any bound of any tree of these ops is an integer's."""
+ def integers(place: ComposedNode) -> bool:
+ node = place.node
+ if not node.is_root and node.child_field.decl.kind in ('uint', 'sint'):
+ return True
+ return any(integers(member) for member in place.members)
+
+ return any(integers(place) for entry in ops
+ for place in (entry.request, entry.reply) if place)
+
+
+class Entry:
+ """One op of a specification table: the op and its composed messages."""
+
+ def __init__(self, family: semantics.Family, op: semantics.Op) -> None:
+ self.op = op
+ self.request = compose(family, op.request) if op.request else None
+ self.reply = compose(family, op.reply) if op.reply else None
+ where = f'{family.path}: op {op.name}'
+ self.sizes = sizes.composed(where, op,
+ _sizing(self.request) if self.request
+ else None,
+ _sizing(self.reply) if self.reply
+ else None)
+
+
+def _spec_ops(family: semantics.Family) -> tuple[Entry, ...]:
+ """Every op a device of this family answers, as the family composes it.
+
+ An inherited op is described again, since a fill anywhere in the chain
+ widens its message. The root's ops are libctlv's own table.
+ """
+ if family.name == spec.CORE_FAMILY_NAME:
+ _reject_root_extension(family)
+ ops = family.local_ops
+ else:
+ ops = tuple(op for op in family.ops
+ if family.owners[op.family_id] != spec.CORE_FAMILY_NAME)
+ return tuple(Entry(family, op) for op in ops)
+
+
+def _reject_root_extension(family: semantics.Family) -> None:
+ """One table serves every chain, so the root may not be widened."""
+ for extension in family.local_extensions:
+ raise CtlvError(f'{family.path}: extension {extension.name}',
+ 'the root family cannot declare an extension '
+ 'point, because its ops are described once for '
+ 'every chain')
+
+
+def _tree(writer: cgen.Writer, names: cgen.Names, op: semantics.Op, which: str,
+ place: ComposedNode) -> None:
+ """One message: the arrays of its levels, deepest first, then its root."""
+ symbol = names.user_node_schema(op, which)
+ _arrays(writer, names, cgen.ident(op.name, which), place, set())
+ writer.declaration('static const struct ctlv_node_schema',
+ f'{symbol} = {{')
+ _members(writer, names, place, 1)
+ writer.line('};')
+ writer.line()
+
+
+def _arrays(writer: cgen.Writer, names: cgen.Names, path: str,
+ place: ComposedNode, taken: set[str]) -> None:
+ """The member arrays below place, each emitted before whoever holds it.
+
+ An array is named after the path down to the node it carries.
+ """
+ for member in place.members:
+ if member.members:
+ _arrays(writer, names, _path(path, member, taken), member, taken)
+ for what, members in (('fields', place.fields),
+ ('optional_fields', place.optional_fields)):
+ if not members:
+ continue
+ symbol = f'{path}_{what.removesuffix("s")}_node_schemas'
+ if what == 'fields':
+ place.fields_symbol = symbol
+ else:
+ place.optional_symbol = symbol
+ where = 'root' if place.node.is_root else \
+ place.node.child_field.full_name
+ writer.comment(f'{where}, {what.replace("_", " ")}')
+ writer.declaration('static const struct ctlv_node_schema',
+ f'{symbol}[] = {{')
+ for member in members:
+ _node(writer, names, member)
+ writer.line('};')
+ writer.line()
+
+
+def _path(path: str, place: ComposedNode, taken: set[str]) -> str:
+ """The name of one node below path, which no other node has.
+
+ One field twice in one record is two nodes, so repeats are counted.
+ """
+ candidate = f'{path}_{cgen.ident(place.node.child_field.name)}'
+ symbol = candidate
+ repeat = 1
+ while symbol in taken:
+ repeat += 1
+ symbol = f'{candidate}_{repeat}'
+ taken.add(symbol)
+ return symbol
+
+
+def _node(writer: cgen.Writer, names: cgen.Names, place: ComposedNode) -> None:
+ """One member, as an element of the array that holds it."""
+ writer.line('\t{')
+ _members(writer, names, place, 2)
+ writer.line('\t},')
+
+
+def _members(writer: cgen.Writer, names: cgen.Names, place: ComposedNode,
+ depth: int) -> None:
+ """What one node says about itself, in the order the core reports it."""
+ node = place.node
+ tab = '\t' * depth
+ if node.is_root:
+ writer.line(f'{tab}.type = CTLV_TYPE_ROOT,')
+ writer.line(f'{tab}.kind = CTLV_SCHEMA_RECORD,')
+ else:
+ writer.declaration(f'{tab}.type =',
+ f'{names.field(node.child_field)},')
+ writer.line(f'{tab}.kind = {cgen.KINDS[node.child_field.decl.kind]},')
+ if node.max_occurrences:
+ writer.line(f'{tab}.max_occurrences = {node.max_occurrences},')
+ else:
+ writer.line(f'{tab}.max_occurrences = 0, /* unlimited */')
+ _bounds(writer, names, place, tab)
+ if place.fields_symbol:
+ writer.declaration(f'{tab}.fields =', f'{place.fields_symbol},')
+ writer.line(f'{tab}.fields_count = {len(place.fields)},')
+ if place.optional_symbol:
+ writer.declaration(f'{tab}.optional_fields =',
+ f'{place.optional_symbol},')
+ writer.line(f'{tab}.optional_fields_count = '
+ f'{len(place.optional_fields)},')
+
+
+def _bounds(writer: cgen.Writer, names: cgen.Names, place: ComposedNode,
+ tab: str) -> None:
+ """The bounds of the schema kind this node has, with the present mask."""
+ bounds = _bounded(names, place)
+ if not bounds:
+ return
+ bits = [PRESENT[name] for name, _ in bounds]
+ _or_lines(writer, f'{tab}.present = ', bits)
+ for name, value in bounds:
+ if name == 'blob':
+ for member, part in zip(BLOB_MEMBERS, value):
+ writer.line(f'{tab}.{member} = {part},')
+ else:
+ writer.line(f'{tab}.{name} = {value},')
+
+
+def _or_lines(writer: cgen.Writer, opening: str, terms: list[str]) -> None:
+ """One or-ed list of bits, broken after an operator where it must be."""
+ column = cgen.width(opening)
+ indent = '\t' * (column // cgen.TAB_WIDTH) + ' ' * (column %
+ cgen.TAB_WIDTH)
+ line = opening
+ for first, term in enumerate(terms):
+ part = term if not first else f' | {term}'
+ if first and cgen.width(f'{line}{part} |') > cgen.LINE_LIMIT:
+ writer.line(f'{line} |')
+ line = indent + term
+ else:
+ line += part
+ writer.line(f'{line},')
+
+
+def _bounded(
+ names: cgen.Names,
+ place: ComposedNode) -> tuple[tuple[str, str | tuple[str, ...]], ...]:
+ """Every bounded member of one node, with the value the core reports."""
+ node = place.node
+ if node.is_root:
+ return ()
+ decl = node.child_field.decl
+ bounds: list[tuple[str, str | tuple[str, ...]]] = []
+ if decl.kind in ('uint', 'sint'):
+ assert decl.min is not None and decl.max is not None
+ bounds += [('min', _bound(decl.kind, decl.min)),
+ ('max', _bound(decl.kind, decl.max))]
+ elif decl.kind == 'enum':
+ assert decl.ref is not None
+ definition = names.effective.definitions[decl.ref]
+ if definition.extension is not None:
+ # As the device's family fills it, so nothing when none did.
+ bounds += [('min', '1'), ('max', str(definition.max_value))]
+ elif definition.header:
+ low, high = cgen.header_bounds(definition)
+ bounds += [('min', low), ('max', high)]
+ else:
+ numbers = definition.numbers()
+ bounds += [('min', str(min(numbers))),
+ ('max', str(max(numbers)))]
+ elif decl.kind == 'flags':
+ assert decl.ref is not None
+ mask = 0
+ for bit in _numbers(names, decl.ref):
+ mask |= 1 << bit
+ bounds.append(('flags_mask', _u64(mask)))
+ elif decl.kind in ('string', 'binary'):
+ assert decl.min_length is not None and decl.max_length is not None
+ bounds += [('min_length', _u64(decl.min_length)),
+ ('max_length', _u64(decl.max_length))]
+ elif decl.kind == 'blob':
+ assert decl.blob is not None
+ bounds.append(('blob', _blob(names, decl.blob)))
+ if decl.unit is not None:
+ bounds += [('unit', f'CTLV_UNIT_{cgen.macro(decl.unit)}'),
+ ('scale', str(decl.scale))]
+ if place.reenters:
+ bounds.append(('reenters', str(place.reenters)))
+ return tuple(bounds)
+
+
+def _numbers(names: cgen.Names, ref: str) -> tuple[int, ...]:
+ """The numbers of the definition one field refers to."""
+ return names.effective.definitions[ref].numbers()
+
+
+def _blob(names: cgen.Names, blob: spec.BlobDecl) -> tuple[str, ...]:
+ """What a blob node allows, in the order the core reports it."""
+ direction = cgen.macro(spec.CORE_FAMILY_NAME, 'blob-direction',
+ blob.direction)
+ types = dict(names.effective.definitions[BLOB_TYPES].values)
+ mask = 0
+ for backing in blob.backings:
+ mask |= 1 << types[backing]
+ return (direction, _u64(mask), _u64(blob.max_length),
+ _u64(blob.alignment))
+
+
+def _bound(kind: str, value: int) -> str:
+ extreme = EXTREMES.get((kind, value))
+ if extreme:
+ return extreme
+ if kind == 'sint':
+ return _s64(value)
+ return _u64(value)
+
+
+def _u64(value: int) -> str:
+ """One unsigned value, hexadecimal once decimal stops being readable."""
+ if value < 1 << 16:
+ return str(value)
+ return f'{value:#x}ULL'
+
+
+def _s64(value: int) -> str:
+ """One signed bound, as the unsigned payload a message carries."""
+ if value >= 0:
+ return _u64(value)
+ return f'(__u64)(__s64){value}'
+
+
+def _table(writer: cgen.Writer, names: cgen.Names,
+ ops: tuple[Entry, ...]) -> None:
+ """Every op of one table, with what its messages may hold."""
+ if not ops:
+ return
+ writer.comment('Effective ops in declaration order.')
+ writer.declaration('static const struct ctlv_op_spec',
+ f'{names.user_op_specs()}[] = {{')
+ for entry in ops:
+ op = entry.op
+ writer.line('\t{')
+ writer.declaration('\t\t.op_id =', f'{names.op_id(op)},')
+ writer.line(f'\t\t.type = {OP_TYPES[op.type]},')
+ writer.line(f'\t\t.fd_access = {FD_ACCESSES[op.fd_access]},')
+ writer.line('\t\t.availability = '
+ f'{AVAILABILITIES[op.availability]},')
+ if op.optional:
+ writer.line('\t\t.optional = true,')
+ for field in TABLE_LENGTHS:
+ value = getattr(entry.sizes, field)
+ if value:
+ writer.line(f'\t\t.{field} = {value},')
+ for which in ('request', 'reply'):
+ if getattr(entry, which) is not None:
+ writer.line(f'\t\t.{which} = '
+ f'&{names.user_node_schema(op, which)},')
+ writer.line('\t},')
+ writer.line('};')
+ writer.line()
+
+
+def _spec(writer: cgen.Writer, names: cgen.Names, family: semantics.Family,
+ ops: tuple[Entry, ...]) -> None:
+ """This family's node of the specification graph."""
+ writer.comment('Family specification descriptor.')
+ writer.declaration('const struct ctlv_family_spec',
+ f'{names.user_spec()} = {{')
+ writer.line(f'\t.name = "{family.name}",')
+ writer.line(f'\t.family_id = {family.family_id},')
+ if family.name != spec.CORE_FAMILY_NAME:
+ parent = cgen.Names(family.chain[-2]).user_spec()
+ writer.line(f'\t.parent = &{parent},')
+ if ops:
+ writer.line(f'\t.ops = {names.user_op_specs()},')
+ writer.line(f'\t.ops_count = {len(ops)},')
+ writer.line('};')
diff --git a/tools/ctlv/ctlvgen/lib/outputs.py b/tools/ctlv/ctlvgen/lib/outputs.py
index a295055c46b2..6ee8abe7f765 100644
--- a/tools/ctlv/ctlvgen/lib/outputs.py
+++ b/tools/ctlv/ctlvgen/lib/outputs.py
@@ -14,6 +14,7 @@ from typing import Optional
from . import cgen
from . import gen_kernel
from . import gen_uapi
+from . import gen_user
from . import semantics
from . import spec
from . import yamlload
@@ -24,16 +25,21 @@ FLAVOURS: dict[str, Callable[..., str]] = {
'uapi-header': gen_uapi.render,
'kernel-header': gen_kernel.render_header,
'kernel-source': gen_kernel.render_source,
+ 'user-header': gen_user.render_header,
+ 'user-source': gen_user.render_source,
}
IDENTITIES: dict[str, tuple[str, str, str]] = {
'uapi-header': ('uapi', 'header', '.h'),
'kernel-header': ('kernel', 'header', '.h'),
'kernel-source': ('kernel', 'source', '.c'),
+ 'user-header': ('user', 'header', '.h'),
+ 'user-source': ('user', 'source', '.c'),
}
-ROOT_FLAVOURS = frozenset(FLAVOURS)
-FAMILY_FLAVOURS: frozenset[str] = frozenset(FLAVOURS)
+USER_FLAVOURS = frozenset({'user-header', 'user-source'})
+ROOT_FLAVOURS = frozenset(FLAVOURS) - {'user-header'}
+FAMILY_FLAVOURS: frozenset[str] = frozenset(FLAVOURS) - USER_FLAVOURS
FIXTURE_FLAVOURS: frozenset[str] = frozenset({
'uapi-header', 'kernel-header', 'kernel-source',
})
@@ -143,6 +149,8 @@ def load(paths: spec.Paths) -> tuple[Output, ...]:
if not fixture and name == spec.CORE_FAMILY_NAME:
required = ROOT_FLAVOURS
flavours = frozenset(record['outputs'])
+ if required == FAMILY_FLAVOURS and flavours & USER_FLAVOURS:
+ required |= USER_FLAVOURS
if flavours != required:
missing = sorted(required - flavours)
extra = sorted(flavours - required)
diff --git a/tools/ctlv/libctlv/Makefile b/tools/ctlv/libctlv/Makefile
new file mode 100644
index 000000000000..8e4e40118db2
--- /dev/null
+++ b/tools/ctlv/libctlv/Makefile
@@ -0,0 +1,41 @@
+# SPDX-License-Identifier: GPL-2.0
+
+KDIR ?= ../../..
+
+CFLAGS ?= -O2 -g
+
+include ../../scripts/Makefile.include
+
+# What make headers_install leaves behind, which is where <ctlv/ctlv.h> and
+# every generated family header come from.
+KHDR_INCLUDES ?= -isystem $(or $(ABSOLUTE_O),$(KDIR))/usr/include
+
+CFLAGS += -Wall -Wextra -Wno-unused-parameter -Wno-missing-field-initializers
+CFLAGS += -D_GNU_SOURCE -I. $(KHDR_INCLUDES)
+
+LIB = $(OUTPUT)libctlv.a
+
+# ctlv_user_gen.o is the root family's specification, generated from the same
+# YAML the core is generated from: the ops this library implements directly are
+# described by the tool that describes every other family's.
+OBJS = $(addprefix $(OUTPUT), \
+ ctlv_user_gen.o \
+ device.o \
+ introspection.o \
+ message.o \
+ schema.o \
+ spec.o \
+ transport.o)
+
+all: $(LIB)
+
+$(LIB): $(OBJS)
+ $(QUIET_AR)$(AR) rcs $@ $^
+
+$(OUTPUT)%.o: %.c ctlv.h internal.h
+ $(QUIET_CC)$(CC) $(CFLAGS) -c -o $@ $<
+
+clean:
+ rm -f $(OBJS) $(LIB)
+
+.PHONY: all clean
diff --git a/tools/ctlv/libctlv/ctlv.h b/tools/ctlv/libctlv/ctlv.h
new file mode 100644
index 000000000000..cd643531ab64
--- /dev/null
+++ b/tools/ctlv/libctlv/ctlv.h
@@ -0,0 +1,413 @@
+/* SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause */
+/*
+ * Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES
+ *
+ * Family-independent userspace API; generated bindings build on it.
+ * int functions return zero or a negative errno; builders retain first error.
+ */
+#ifndef _CTLV_H
+#define _CTLV_H
+
+#include <ctlv/ctlv.h>
+#include <ctlv/ctlv_gen.h>
+#include <linux/types.h>
+#include <stdbool.h>
+#include <stddef.h>
+
+/* Room for the device's message in struct ctlv_error; a longer one is cut. */
+#define CTLV_ERROR_MESSAGE_LEN 256
+
+/* Which members of struct ctlv_error the failure came with. */
+#define CTLV_ERROR_HAS_REASON (1U << 0)
+#define CTLV_ERROR_HAS_MESSAGE (1U << 1)
+#define CTLV_ERROR_HAS_BAD_TLV_OFFSET (1U << 2)
+#define CTLV_ERROR_HAS_RECORD_OFFSET (1U << 3)
+#define CTLV_ERROR_HAS_MISSING_TYPE (1U << 4)
+#define CTLV_ERROR_HAS_EXPECTED_GENERATION (1U << 5)
+#define CTLV_ERROR_HAS_CURRENT_GENERATION (1U << 6)
+#define CTLV_ERROR_HAS_REQUIRED_LENGTH (1U << 7)
+
+/**
+ * struct ctlv_error - Why one op failed, as the device explained it.
+ * @err: the negative errno the op failed with, zero when it did not
+ * @present: CTLV_ERROR_HAS_* bits saying which members below were reported
+ * @reason: enum ctlv_validation_reason
+ * @message: what the device said, NUL terminated
+ * @bad_tlv_offset: offset in the request of the TLV that was refused
+ * @record_offset: offset in the request of the record holding it
+ * @missing_type: type of a required field that was absent
+ * @expected_generation: the generation a stale continuation carried
+ * @current_generation: the generation the device is on now
+ * @required_length: how long the output blob at @bad_tlv_offset has to be
+ *
+ * Everything beyond @err is optional, and a caller that wants only the errno
+ * may pass NULL wherever this is asked for.
+ */
+struct ctlv_error {
+ int err;
+ __u32 present;
+ __u32 reason;
+ char message[CTLV_ERROR_MESSAGE_LEN];
+ __u64 bad_tlv_offset;
+ __u64 record_offset;
+ __u64 missing_type;
+ __u64 expected_generation;
+ __u64 current_generation;
+ __u64 required_length;
+};
+
+/* Renders @error into @buf and returns it, for a diagnostic of one line. */
+const char *ctlv_error_str(const struct ctlv_error *error, char *buf,
+ size_t len);
+
+/* An open device, its family chain resolved. */
+struct ctlv_dev;
+/* A message being built. */
+struct ctlv_msg;
+/* A complete reply, however many fragments it arrived in. */
+struct ctlv_reply;
+/* One op's request or reply tree, as numbers. */
+struct ctlv_schema;
+
+int ctlv_open(const char *family, unsigned int which, int flags,
+ struct ctlv_dev **devp);
+int ctlv_open_path(const char *path, int flags, struct ctlv_dev **devp);
+/* A file its own subsystem opened, which ctlv_close() leaves open. */
+int ctlv_open_fd(int fd, struct ctlv_dev **devp);
+void ctlv_close(struct ctlv_dev *dev);
+
+int ctlv_dev_fd(const struct ctlv_dev *dev);
+const char *ctlv_dev_name(const struct ctlv_dev *dev);
+
+/* The chain of the opened device, root first and the device family last. */
+unsigned int ctlv_chain_len(const struct ctlv_dev *dev);
+__u16 ctlv_chain_id(const struct ctlv_dev *dev, unsigned int level);
+const char *ctlv_chain_name(const struct ctlv_dev *dev, unsigned int level);
+/* The family the device answers as. */
+const char *ctlv_dev_family(const struct ctlv_dev *dev);
+/* Whether @family is anywhere in the chain, so an inherited op applies. */
+bool ctlv_dev_inherits(const struct ctlv_dev *dev, const char *family);
+
+/**
+ * struct ctlv_dev_list - Every CTLV device this system has.
+ * @names: device names as they appear in /dev
+ * @names_count: number of names
+ */
+struct ctlv_dev_list {
+ char **names;
+ unsigned int names_count;
+};
+
+int ctlv_enumerate(struct ctlv_dev_list *list);
+void ctlv_dev_list_free(struct ctlv_dev_list *list);
+
+int ctlv_msg_new(__u32 capacity, struct ctlv_msg **msgp);
+/* The same with a ceiling, so an over-long request fails as it is built. */
+int ctlv_msg_new_max(__u32 capacity, __u32 max, struct ctlv_msg **msgp);
+void ctlv_msg_free(struct ctlv_msg *msg);
+void ctlv_msg_reset(struct ctlv_msg *msg);
+
+/* The first failure since the last reset, or 0. */
+int ctlv_msg_err(const struct ctlv_msg *msg);
+/* Closes whatever is still open and returns the message, or the failure. */
+int ctlv_msg_done(struct ctlv_msg *msg);
+const struct ctlv_hdr *ctlv_msg_root(const struct ctlv_msg *msg);
+__u32 ctlv_msg_len(const struct ctlv_msg *msg);
+
+int ctlv_put_u64(struct ctlv_msg *msg, __u64 type, __u64 value);
+int ctlv_put_s64(struct ctlv_msg *msg, __u64 type, __s64 value);
+int ctlv_put_bool(struct ctlv_msg *msg, __u64 type, bool value);
+int ctlv_put_str(struct ctlv_msg *msg, __u64 type, const char *value);
+int ctlv_put_bin(struct ctlv_msg *msg, __u64 type, const void *value,
+ __u32 len);
+int ctlv_record_start(struct ctlv_msg *msg, __u64 type);
+int ctlv_record_end(struct ctlv_msg *msg);
+
+/*
+ * Says of the TLV written last, a record just ended included, that a device
+ * whose schema does not declare its type should skip it rather than refuse
+ * the request. A type the device does declare is held to its schema as ever,
+ * so this covers a field a newer version of a family added and nothing more.
+ */
+int ctlv_msg_ignore_if_unknown(struct ctlv_msg *msg);
+
+/* A blob TLV carries a record describing memory; these write all of it. */
+int ctlv_put_blob_va(struct ctlv_msg *msg, __u64 type, const void *address,
+ __u64 length);
+
+/* Fails when framing, lengths or records are not self-consistent. */
+int ctlv_msg_check(const void *buf, __u32 len);
+
+bool ctlv_hdr_is_record(const struct ctlv_hdr *hdr);
+const void *ctlv_hdr_payload(const struct ctlv_hdr *hdr, __u32 *len);
+__u32 ctlv_hdr_payload_len(const struct ctlv_hdr *hdr);
+
+const struct ctlv_hdr *ctlv_hdr_first(const struct ctlv_hdr *record);
+const struct ctlv_hdr *ctlv_hdr_next(const struct ctlv_hdr *record,
+ const struct ctlv_hdr *child);
+const struct ctlv_hdr *ctlv_hdr_find(const struct ctlv_hdr *record,
+ __u64 type);
+const struct ctlv_hdr *ctlv_hdr_find_next(const struct ctlv_hdr *record,
+ const struct ctlv_hdr *hdr,
+ __u64 type);
+unsigned int ctlv_hdr_count(const struct ctlv_hdr *record, __u64 type);
+
+/* Every child of @record, in the order the device wrote them. */
+#define ctlv_for_each(_child, _record) \
+ for ((_child) = ctlv_hdr_first(_record); (_child); \
+ (_child) = ctlv_hdr_next((_record), (_child)))
+
+/* Every occurrence of a field that may repeat. */
+#define ctlv_for_each_type(_child, _record, _type) \
+ for ((_child) = ctlv_hdr_find((_record), (_type)); (_child); \
+ (_child) = ctlv_hdr_find_next((_record), (_child), \
+ (_type)))
+
+int ctlv_get_u64(const struct ctlv_hdr *hdr, __u64 *value);
+int ctlv_get_s64(const struct ctlv_hdr *hdr, __s64 *value);
+int ctlv_get_bool(const struct ctlv_hdr *hdr, bool *value);
+int ctlv_get_str(const struct ctlv_hdr *hdr, const char **value);
+int ctlv_get_bin(const struct ctlv_hdr *hdr, const void **value, __u32 *len);
+
+/*
+ * Neither refuses a value a device newer than its caller sent: @known says
+ * whether the value is from @min to @max, @unknown collects the bits outside
+ * @mask.
+ */
+int ctlv_get_enum(const struct ctlv_hdr *hdr, __u64 min, __u64 max,
+ __u64 *value, bool *known);
+int ctlv_get_flags(const struct ctlv_hdr *hdr, __u64 mask, __u64 *value,
+ __u64 *unknown);
+
+/*
+ * An action is one ioctl. A query is as many as its answer takes, reassembled
+ * here into the one reply the device would have sent given room for all of it.
+ */
+int ctlv_action(struct ctlv_dev *dev, __u64 op_id, struct ctlv_msg *request,
+ struct ctlv_reply **replyp, struct ctlv_error *error);
+int ctlv_query(struct ctlv_dev *dev, __u64 op_id, struct ctlv_msg *request,
+ struct ctlv_reply **replyp, struct ctlv_error *error);
+
+/*
+ * The same two with the reply buffer sized, sparing the fallback dump. A sized
+ * action keeps error information only if @reply_len has room for it, and a
+ * sized query does not restart a stale dump, so a caller learns that it went
+ * stale.
+ */
+int ctlv_action_sized(struct ctlv_dev *dev, __u64 op_id,
+ struct ctlv_msg *request, __u32 reply_len,
+ struct ctlv_reply **replyp, struct ctlv_error *error);
+int ctlv_query_sized(struct ctlv_dev *dev, __u64 op_id,
+ struct ctlv_msg *request, __u32 buf_len,
+ struct ctlv_reply **replyp, struct ctlv_error *error);
+
+const struct ctlv_hdr *ctlv_reply_root(const struct ctlv_reply *reply);
+/* The generation the answer belongs to. */
+__u64 ctlv_reply_generation(const struct ctlv_reply *reply);
+/* How many ioctls it took, a property of the buffer, not the reply. */
+unsigned int ctlv_reply_fragments(const struct ctlv_reply *reply);
+void ctlv_reply_free(struct ctlv_reply *reply);
+
+/*
+ * What op-schema-get reports, in the shape the generator computes the same
+ * numbers into, so a device and its specification compare member by member.
+ */
+
+/* Which members of struct ctlv_node_schema the node declares. */
+#define CTLV_NODE_SCHEMA_HAS_MIN (1U << 0)
+#define CTLV_NODE_SCHEMA_HAS_MAX (1U << 1)
+#define CTLV_NODE_SCHEMA_HAS_MIN_LENGTH (1U << 2)
+#define CTLV_NODE_SCHEMA_HAS_MAX_LENGTH (1U << 3)
+#define CTLV_NODE_SCHEMA_HAS_FLAGS_MASK (1U << 4)
+#define CTLV_NODE_SCHEMA_HAS_UNIT (1U << 5)
+#define CTLV_NODE_SCHEMA_HAS_SCALE (1U << 6)
+#define CTLV_NODE_SCHEMA_HAS_BLOB (1U << 7)
+#define CTLV_NODE_SCHEMA_HAS_REENTERS (1U << 8)
+
+/**
+ * struct ctlv_node_schema - One node of an op's request or reply tree.
+ * @type: the field's type, or CTLV_TYPE_ROOT for the message root
+ * @kind: enum ctlv_schema_kind, the one schema kind this payload takes
+ * @max_occurrences: how often it may appear, zero meaning as often as fits
+ * @present: CTLV_NODE_SCHEMA_HAS_* bits saying which members below apply
+ * @min: inclusive lower bound of an integer or enum payload
+ * @max: inclusive upper bound of the same
+ * @min_length: shortest string or binary payload, a terminator excluded
+ * @max_length: longest of the same
+ * @flags_mask: every bit a flags payload may carry
+ * @unit: enum ctlv_unit of an integer payload
+ * @scale: decimal scale that accompanies @unit
+ * @blob_direction: enum ctlv_blob_direction of a blob
+ * @blob_backings: mask of BIT(enum ctlv_blob_type) this blob takes
+ * @blob_max_length: longest external payload the blob may describe
+ * @blob_alignment: alignment the blob's address or offset must have
+ * @reenters: levels to climb to the node whose members are this one's too
+ * @fields: the required nodes one level in
+ * @fields_count: how many
+ * @optional_fields: the optional nodes one level in
+ * @optional_fields_count: how many
+ *
+ * The same field in two places is two nodes, each required or not.
+ */
+struct ctlv_node_schema {
+ __u64 type;
+ __u32 kind;
+ __u64 max_occurrences;
+ __u32 present;
+ __u64 min;
+ __u64 max;
+ __u64 min_length;
+ __u64 max_length;
+ __u64 flags_mask;
+ __u32 unit;
+ __s64 scale;
+ __u32 blob_direction;
+ __u64 blob_backings;
+ __u64 blob_max_length;
+ __u64 blob_alignment;
+ __u64 reenters;
+ const struct ctlv_node_schema *fields;
+ unsigned int fields_count;
+ const struct ctlv_node_schema *optional_fields;
+ unsigned int optional_fields_count;
+};
+
+/* Either tree is absent when the op has no such message. */
+int ctlv_op_schema_get(struct ctlv_dev *dev, __u64 op_id,
+ struct ctlv_schema **schemap, struct ctlv_error *error);
+const struct ctlv_node_schema *ctlv_schema_request(const struct ctlv_schema *s);
+const struct ctlv_node_schema *ctlv_schema_reply(const struct ctlv_schema *s);
+void ctlv_schema_free(struct ctlv_schema *schema);
+
+/*
+ * Whether two trees agree, and where they first differ. @expected is what a
+ * specification generated, @actual what a device reported.
+ */
+bool ctlv_schema_equal(const struct ctlv_node_schema *expected,
+ const struct ctlv_node_schema *actual, char *diff,
+ size_t len);
+
+/* One line describing a node; @indent is how deep it sits. */
+void ctlv_schema_render(const struct ctlv_node_schema *node,
+ unsigned int indent, char *buf, size_t len);
+
+/* What every device answers, whatever family it belongs to. */
+
+/**
+ * struct ctlv_op_info - One entry of an ops dump.
+ * @op_id: complete op ID
+ * @type: enum ctlv_op_type
+ * @fd_access: enum ctlv_op_fd_access
+ * @availability: enum ctlv_op_availability
+ * @disabled: whether this device refuses the op right now
+ * @request_max_len: longest request made from known fields
+ * @action_reply_max_len: longest action reply, zero when it is not an action
+ * @query_reply_min_len: smallest buffer a query fragment needs
+ * @query_reply_suggested_len: buffer size the device would rather have
+ * @error_info_max_len: longest error information
+ */
+struct ctlv_op_info {
+ __u64 op_id;
+ __u32 type;
+ __u32 fd_access;
+ __u32 availability;
+ bool disabled;
+ __u64 request_max_len;
+ __u64 action_reply_max_len;
+ __u64 query_reply_min_len;
+ __u64 query_reply_suggested_len;
+ __u64 error_info_max_len;
+};
+
+/**
+ * struct ctlv_ops - Every op one device publishes.
+ * @ops: entries in the order the device reported them
+ * @ops_count: number of entries
+ * @generation: the op-state generation the whole dump belongs to
+ */
+struct ctlv_ops {
+ struct ctlv_op_info *ops;
+ unsigned int ops_count;
+ __u64 generation;
+};
+
+int ctlv_ops_get(struct ctlv_dev *dev, struct ctlv_ops **opsp,
+ struct ctlv_error *error);
+void ctlv_ops_free(struct ctlv_ops *ops);
+const struct ctlv_op_info *ctlv_ops_find(const struct ctlv_ops *ops,
+ __u64 op_id);
+
+/*
+ * What a family's specification says, as its generated binding carries it.
+ * Comparing it against what a device reports says whether the two agree.
+ */
+
+/**
+ * struct ctlv_op_spec - What a specification says about one op.
+ * @op_id: complete op ID
+ * @type: enum ctlv_op_type
+ * @fd_access: enum ctlv_op_fd_access
+ * @availability: enum ctlv_op_availability
+ * @optional: whether a device may omit this op
+ * @request_max_len: longest request made from known fields
+ * @action_reply_max_len: longest action reply, zero when it is not an action
+ * @query_reply_min_len: smallest buffer a query fragment needs
+ * @query_reply_suggested_len: buffer size worth offering a query
+ * @error_info_max_len: longest error information
+ * @request: the request tree, NULL when the op takes no request
+ * @reply: the reply tree, NULL when it answers none
+ *
+ * An ops dump says whether a particular device offers an optional op.
+ */
+struct ctlv_op_spec {
+ __u64 op_id;
+ __u32 type;
+ __u32 fd_access;
+ __u32 availability;
+ bool optional;
+ __u64 request_max_len;
+ __u64 action_reply_max_len;
+ __u64 query_reply_min_len;
+ __u64 query_reply_suggested_len;
+ __u64 error_info_max_len;
+ const struct ctlv_node_schema *request;
+ const struct ctlv_node_schema *reply;
+};
+
+/**
+ * struct ctlv_family_spec - What a specification says about one family.
+ * @name: the family name, as a device's uevent carries it
+ * @family_id: its position in the central registry
+ * @parent: the family it inherits, NULL for the root
+ * @ops: every op a device of the family answers below the root, in the order
+ * its chain declared them
+ * @ops_count: number of entries
+ *
+ * A chain is these linked upwards, one per family. An inherited op is
+ * described again in each, because every length that touches it differs.
+ */
+struct ctlv_family_spec {
+ const char *name;
+ __u16 family_id;
+ const struct ctlv_family_spec *parent;
+ const struct ctlv_op_spec *ops;
+ unsigned int ops_count;
+};
+
+/* The root family, which every chain starts from and every device answers. */
+extern const struct ctlv_family_spec ctlv_spec;
+
+/* The op of @spec's chain with @op_id, or NULL for one it does not describe. */
+const struct ctlv_op_spec *ctlv_spec_find(const struct ctlv_family_spec *spec,
+ __u64 op_id);
+
+/*
+ * Whether @dev agrees with @spec about its published ops: 0 when they do,
+ * -ENOMSG when they do not with @diff saying where, -EINVAL when @spec is not
+ * the family @dev registered as, another errno when the device could not be
+ * asked. Every mandatory op must be published; an optional one may be absent.
+ */
+int ctlv_spec_check(struct ctlv_dev *dev, const struct ctlv_family_spec *spec,
+ char *diff, size_t len, struct ctlv_error *error);
+
+#endif /* _CTLV_H */
diff --git a/tools/ctlv/libctlv/ctlv_user_gen.c b/tools/ctlv/libctlv/ctlv_user_gen.c
new file mode 100644
index 000000000000..606dbf93b746
--- /dev/null
+++ b/tools/ctlv/libctlv/ctlv_user_gen.c
@@ -0,0 +1,551 @@
+// SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause)
+/*
+ * Generated from Documentation/ctlv/specs/ctlv.yaml by
+ * tools/ctlv/ctlv-regen.sh. Do not edit.
+ */
+/* CTLV-GEN user source */
+
+#include <stdint.h>
+
+#include <ctlv.h>
+
+/* Effective request and reply schemas. */
+
+static const struct ctlv_node_schema family_get_request_node_schema = {
+ .type = CTLV_TYPE_ROOT,
+ .kind = CTLV_SCHEMA_RECORD,
+ .max_occurrences = 1,
+};
+
+/* family-get families, fields */
+static const struct ctlv_node_schema family_get_reply_families_field_node_schemas[] = {
+ {
+ .type = CTLV_T_FAMILY_GET_FAMILIES_ID,
+ .kind = CTLV_SCHEMA_UINT,
+ .max_occurrences = 1,
+ .present = CTLV_NODE_SCHEMA_HAS_MIN | CTLV_NODE_SCHEMA_HAS_MAX,
+ .min = 0,
+ .max = UINT64_MAX,
+ },
+ {
+ .type = CTLV_T_FAMILY_GET_FAMILIES_NAME,
+ .kind = CTLV_SCHEMA_STRING,
+ .max_occurrences = 1,
+ .present = CTLV_NODE_SCHEMA_HAS_MIN_LENGTH | CTLV_NODE_SCHEMA_HAS_MAX_LENGTH,
+ .min_length = 0,
+ .max_length = 63,
+ },
+};
+
+/* root, fields */
+static const struct ctlv_node_schema family_get_reply_field_node_schemas[] = {
+ {
+ .type = CTLV_T_FAMILY_GET_FAMILIES,
+ .kind = CTLV_SCHEMA_RECORD,
+ .max_occurrences = 0, /* unlimited */
+ .fields = family_get_reply_families_field_node_schemas,
+ .fields_count = 2,
+ },
+};
+
+static const struct ctlv_node_schema family_get_reply_node_schema = {
+ .type = CTLV_TYPE_ROOT,
+ .kind = CTLV_SCHEMA_RECORD,
+ .max_occurrences = 1,
+ .fields = family_get_reply_field_node_schemas,
+ .fields_count = 1,
+};
+
+static const struct ctlv_node_schema op_get_request_node_schema = {
+ .type = CTLV_TYPE_ROOT,
+ .kind = CTLV_SCHEMA_RECORD,
+ .max_occurrences = 1,
+};
+
+/* op-get ops, fields */
+static const struct ctlv_node_schema op_get_reply_ops_field_node_schemas[] = {
+ {
+ .type = CTLV_T_OP_GET_OPS_ID,
+ .kind = CTLV_SCHEMA_UINT,
+ .max_occurrences = 1,
+ .present = CTLV_NODE_SCHEMA_HAS_MIN | CTLV_NODE_SCHEMA_HAS_MAX,
+ .min = 1,
+ .max = UINT64_MAX,
+ },
+ {
+ .type = CTLV_T_OP_GET_OPS_TYPE,
+ .kind = CTLV_SCHEMA_ENUM,
+ .max_occurrences = 1,
+ .present = CTLV_NODE_SCHEMA_HAS_MIN | CTLV_NODE_SCHEMA_HAS_MAX,
+ .min = 1,
+ .max = 2,
+ },
+};
+
+/* op-get ops, optional fields */
+static const struct ctlv_node_schema op_get_reply_ops_optional_field_node_schemas[] = {
+ {
+ .type = CTLV_T_OP_GET_OPS_AVAILABILITY,
+ .kind = CTLV_SCHEMA_ENUM,
+ .max_occurrences = 1,
+ .present = CTLV_NODE_SCHEMA_HAS_MIN | CTLV_NODE_SCHEMA_HAS_MAX,
+ .min = 1,
+ .max = 2,
+ },
+ {
+ .type = CTLV_T_OP_GET_OPS_DISABLED,
+ .kind = CTLV_SCHEMA_BOOL,
+ .max_occurrences = 1,
+ },
+ {
+ .type = CTLV_T_OP_GET_OPS_REQUEST_MAX_LEN,
+ .kind = CTLV_SCHEMA_UINT,
+ .max_occurrences = 1,
+ .present = CTLV_NODE_SCHEMA_HAS_MIN | CTLV_NODE_SCHEMA_HAS_MAX,
+ .min = 0,
+ .max = UINT64_MAX,
+ },
+ {
+ .type = CTLV_T_OP_GET_OPS_ACTION_REPLY_MAX_LEN,
+ .kind = CTLV_SCHEMA_UINT,
+ .max_occurrences = 1,
+ .present = CTLV_NODE_SCHEMA_HAS_MIN | CTLV_NODE_SCHEMA_HAS_MAX,
+ .min = 0,
+ .max = UINT64_MAX,
+ },
+ {
+ .type = CTLV_T_OP_GET_OPS_QUERY_REPLY_MIN_LEN,
+ .kind = CTLV_SCHEMA_UINT,
+ .max_occurrences = 1,
+ .present = CTLV_NODE_SCHEMA_HAS_MIN | CTLV_NODE_SCHEMA_HAS_MAX,
+ .min = 0,
+ .max = UINT64_MAX,
+ },
+ {
+ .type = CTLV_T_OP_GET_OPS_QUERY_REPLY_SUGGESTED_LEN,
+ .kind = CTLV_SCHEMA_UINT,
+ .max_occurrences = 1,
+ .present = CTLV_NODE_SCHEMA_HAS_MIN | CTLV_NODE_SCHEMA_HAS_MAX,
+ .min = 0,
+ .max = UINT64_MAX,
+ },
+ {
+ .type = CTLV_T_OP_GET_OPS_FD_ACCESS,
+ .kind = CTLV_SCHEMA_ENUM,
+ .max_occurrences = 1,
+ .present = CTLV_NODE_SCHEMA_HAS_MIN | CTLV_NODE_SCHEMA_HAS_MAX,
+ .min = 1,
+ .max = 2,
+ },
+ {
+ .type = CTLV_T_OP_GET_OPS_ERROR_INFO_MAX_LEN,
+ .kind = CTLV_SCHEMA_UINT,
+ .max_occurrences = 1,
+ .present = CTLV_NODE_SCHEMA_HAS_MIN | CTLV_NODE_SCHEMA_HAS_MAX,
+ .min = 0,
+ .max = UINT64_MAX,
+ },
+};
+
+/* root, fields */
+static const struct ctlv_node_schema op_get_reply_field_node_schemas[] = {
+ {
+ .type = CTLV_T_OP_GET_OPS,
+ .kind = CTLV_SCHEMA_RECORD,
+ .max_occurrences = 0, /* unlimited */
+ .fields = op_get_reply_ops_field_node_schemas,
+ .fields_count = 2,
+ .optional_fields = op_get_reply_ops_optional_field_node_schemas,
+ .optional_fields_count = 8,
+ },
+};
+
+static const struct ctlv_node_schema op_get_reply_node_schema = {
+ .type = CTLV_TYPE_ROOT,
+ .kind = CTLV_SCHEMA_RECORD,
+ .max_occurrences = 1,
+ .fields = op_get_reply_field_node_schemas,
+ .fields_count = 1,
+};
+
+/* root, fields */
+static const struct ctlv_node_schema op_schema_get_request_field_node_schemas[] = {
+ {
+ .type = CTLV_T_OP_SCHEMA_GET_ID,
+ .kind = CTLV_SCHEMA_UINT,
+ .max_occurrences = 1,
+ .present = CTLV_NODE_SCHEMA_HAS_MIN | CTLV_NODE_SCHEMA_HAS_MAX,
+ .min = 1,
+ .max = UINT64_MAX,
+ },
+};
+
+static const struct ctlv_node_schema op_schema_get_request_node_schema = {
+ .type = CTLV_TYPE_ROOT,
+ .kind = CTLV_SCHEMA_RECORD,
+ .max_occurrences = 1,
+ .fields = op_schema_get_request_field_node_schemas,
+ .fields_count = 1,
+};
+
+/* op-schema-get request, fields */
+static const struct ctlv_node_schema op_schema_get_reply_request_field_node_schemas[] = {
+ {
+ .type = CTLV_T_SCHEMA_NODE_TYPE,
+ .kind = CTLV_SCHEMA_UINT,
+ .max_occurrences = 1,
+ .present = CTLV_NODE_SCHEMA_HAS_MIN | CTLV_NODE_SCHEMA_HAS_MAX,
+ .min = 0,
+ .max = UINT64_MAX,
+ },
+ {
+ .type = CTLV_T_SCHEMA_NODE_KIND,
+ .kind = CTLV_SCHEMA_UINT,
+ .max_occurrences = 1,
+ .present = CTLV_NODE_SCHEMA_HAS_MIN | CTLV_NODE_SCHEMA_HAS_MAX,
+ .min = 0,
+ .max = UINT64_MAX,
+ },
+ {
+ .type = CTLV_T_SCHEMA_NODE_MAX_OCCURRENCES,
+ .kind = CTLV_SCHEMA_UINT,
+ .max_occurrences = 1,
+ .present = CTLV_NODE_SCHEMA_HAS_MIN | CTLV_NODE_SCHEMA_HAS_MAX,
+ .min = 0,
+ .max = UINT64_MAX,
+ },
+};
+
+/* op-schema-get request, optional fields */
+static const struct ctlv_node_schema op_schema_get_reply_request_optional_field_node_schemas[] = {
+ {
+ .type = CTLV_T_SCHEMA_NODE_MIN,
+ .kind = CTLV_SCHEMA_UINT,
+ .max_occurrences = 1,
+ .present = CTLV_NODE_SCHEMA_HAS_MIN | CTLV_NODE_SCHEMA_HAS_MAX,
+ .min = 0,
+ .max = UINT64_MAX,
+ },
+ {
+ .type = CTLV_T_SCHEMA_NODE_MAX,
+ .kind = CTLV_SCHEMA_UINT,
+ .max_occurrences = 1,
+ .present = CTLV_NODE_SCHEMA_HAS_MIN | CTLV_NODE_SCHEMA_HAS_MAX,
+ .min = 0,
+ .max = UINT64_MAX,
+ },
+ {
+ .type = CTLV_T_SCHEMA_NODE_MIN_LENGTH,
+ .kind = CTLV_SCHEMA_UINT,
+ .max_occurrences = 1,
+ .present = CTLV_NODE_SCHEMA_HAS_MIN | CTLV_NODE_SCHEMA_HAS_MAX,
+ .min = 0,
+ .max = UINT64_MAX,
+ },
+ {
+ .type = CTLV_T_SCHEMA_NODE_MAX_LENGTH,
+ .kind = CTLV_SCHEMA_UINT,
+ .max_occurrences = 1,
+ .present = CTLV_NODE_SCHEMA_HAS_MIN | CTLV_NODE_SCHEMA_HAS_MAX,
+ .min = 0,
+ .max = UINT64_MAX,
+ },
+ {
+ .type = CTLV_T_SCHEMA_NODE_FLAGS_MASK,
+ .kind = CTLV_SCHEMA_UINT,
+ .max_occurrences = 1,
+ .present = CTLV_NODE_SCHEMA_HAS_MIN | CTLV_NODE_SCHEMA_HAS_MAX,
+ .min = 0,
+ .max = UINT64_MAX,
+ },
+ {
+ .type = CTLV_T_SCHEMA_NODE_UNIT,
+ .kind = CTLV_SCHEMA_UINT,
+ .max_occurrences = 1,
+ .present = CTLV_NODE_SCHEMA_HAS_MIN | CTLV_NODE_SCHEMA_HAS_MAX,
+ .min = 0,
+ .max = UINT64_MAX,
+ },
+ {
+ .type = CTLV_T_SCHEMA_NODE_SCALE,
+ .kind = CTLV_SCHEMA_SINT,
+ .max_occurrences = 1,
+ .present = CTLV_NODE_SCHEMA_HAS_MIN | CTLV_NODE_SCHEMA_HAS_MAX,
+ .min = (__u64)INT64_MIN,
+ .max = INT64_MAX,
+ },
+ {
+ .type = CTLV_T_SCHEMA_NODE_BLOB_DIRECTION,
+ .kind = CTLV_SCHEMA_ENUM,
+ .max_occurrences = 1,
+ .present = CTLV_NODE_SCHEMA_HAS_MIN | CTLV_NODE_SCHEMA_HAS_MAX,
+ .min = 1,
+ .max = 3,
+ },
+ {
+ .type = CTLV_T_SCHEMA_NODE_BLOB_BACKINGS,
+ .kind = CTLV_SCHEMA_UINT,
+ .max_occurrences = 1,
+ .present = CTLV_NODE_SCHEMA_HAS_MIN | CTLV_NODE_SCHEMA_HAS_MAX,
+ .min = 0,
+ .max = UINT64_MAX,
+ },
+ {
+ .type = CTLV_T_SCHEMA_NODE_BLOB_MAX_LENGTH,
+ .kind = CTLV_SCHEMA_UINT,
+ .max_occurrences = 1,
+ .present = CTLV_NODE_SCHEMA_HAS_MIN | CTLV_NODE_SCHEMA_HAS_MAX,
+ .min = 0,
+ .max = UINT64_MAX,
+ },
+ {
+ .type = CTLV_T_SCHEMA_NODE_BLOB_ALIGNMENT,
+ .kind = CTLV_SCHEMA_UINT,
+ .max_occurrences = 1,
+ .present = CTLV_NODE_SCHEMA_HAS_MIN | CTLV_NODE_SCHEMA_HAS_MAX,
+ .min = 0,
+ .max = UINT64_MAX,
+ },
+ {
+ .type = CTLV_T_SCHEMA_NODE_REENTERS,
+ .kind = CTLV_SCHEMA_UINT,
+ .max_occurrences = 1,
+ .present = CTLV_NODE_SCHEMA_HAS_MIN | CTLV_NODE_SCHEMA_HAS_MAX,
+ .min = 1,
+ .max = UINT64_MAX,
+ },
+ {
+ .type = CTLV_T_SCHEMA_NODE_FIELDS,
+ .kind = CTLV_SCHEMA_RECORD,
+ .max_occurrences = 0, /* unlimited */
+ .present = CTLV_NODE_SCHEMA_HAS_REENTERS,
+ .reenters = 1,
+ },
+ {
+ .type = CTLV_T_SCHEMA_NODE_OPTIONAL_FIELDS,
+ .kind = CTLV_SCHEMA_RECORD,
+ .max_occurrences = 0, /* unlimited */
+ .present = CTLV_NODE_SCHEMA_HAS_REENTERS,
+ .reenters = 1,
+ },
+};
+
+/* op-schema-get reply, fields */
+static const struct ctlv_node_schema op_schema_get_reply_reply_field_node_schemas[] = {
+ {
+ .type = CTLV_T_SCHEMA_NODE_TYPE,
+ .kind = CTLV_SCHEMA_UINT,
+ .max_occurrences = 1,
+ .present = CTLV_NODE_SCHEMA_HAS_MIN | CTLV_NODE_SCHEMA_HAS_MAX,
+ .min = 0,
+ .max = UINT64_MAX,
+ },
+ {
+ .type = CTLV_T_SCHEMA_NODE_KIND,
+ .kind = CTLV_SCHEMA_UINT,
+ .max_occurrences = 1,
+ .present = CTLV_NODE_SCHEMA_HAS_MIN | CTLV_NODE_SCHEMA_HAS_MAX,
+ .min = 0,
+ .max = UINT64_MAX,
+ },
+ {
+ .type = CTLV_T_SCHEMA_NODE_MAX_OCCURRENCES,
+ .kind = CTLV_SCHEMA_UINT,
+ .max_occurrences = 1,
+ .present = CTLV_NODE_SCHEMA_HAS_MIN | CTLV_NODE_SCHEMA_HAS_MAX,
+ .min = 0,
+ .max = UINT64_MAX,
+ },
+};
+
+/* op-schema-get reply, optional fields */
+static const struct ctlv_node_schema op_schema_get_reply_reply_optional_field_node_schemas[] = {
+ {
+ .type = CTLV_T_SCHEMA_NODE_MIN,
+ .kind = CTLV_SCHEMA_UINT,
+ .max_occurrences = 1,
+ .present = CTLV_NODE_SCHEMA_HAS_MIN | CTLV_NODE_SCHEMA_HAS_MAX,
+ .min = 0,
+ .max = UINT64_MAX,
+ },
+ {
+ .type = CTLV_T_SCHEMA_NODE_MAX,
+ .kind = CTLV_SCHEMA_UINT,
+ .max_occurrences = 1,
+ .present = CTLV_NODE_SCHEMA_HAS_MIN | CTLV_NODE_SCHEMA_HAS_MAX,
+ .min = 0,
+ .max = UINT64_MAX,
+ },
+ {
+ .type = CTLV_T_SCHEMA_NODE_MIN_LENGTH,
+ .kind = CTLV_SCHEMA_UINT,
+ .max_occurrences = 1,
+ .present = CTLV_NODE_SCHEMA_HAS_MIN | CTLV_NODE_SCHEMA_HAS_MAX,
+ .min = 0,
+ .max = UINT64_MAX,
+ },
+ {
+ .type = CTLV_T_SCHEMA_NODE_MAX_LENGTH,
+ .kind = CTLV_SCHEMA_UINT,
+ .max_occurrences = 1,
+ .present = CTLV_NODE_SCHEMA_HAS_MIN | CTLV_NODE_SCHEMA_HAS_MAX,
+ .min = 0,
+ .max = UINT64_MAX,
+ },
+ {
+ .type = CTLV_T_SCHEMA_NODE_FLAGS_MASK,
+ .kind = CTLV_SCHEMA_UINT,
+ .max_occurrences = 1,
+ .present = CTLV_NODE_SCHEMA_HAS_MIN | CTLV_NODE_SCHEMA_HAS_MAX,
+ .min = 0,
+ .max = UINT64_MAX,
+ },
+ {
+ .type = CTLV_T_SCHEMA_NODE_UNIT,
+ .kind = CTLV_SCHEMA_UINT,
+ .max_occurrences = 1,
+ .present = CTLV_NODE_SCHEMA_HAS_MIN | CTLV_NODE_SCHEMA_HAS_MAX,
+ .min = 0,
+ .max = UINT64_MAX,
+ },
+ {
+ .type = CTLV_T_SCHEMA_NODE_SCALE,
+ .kind = CTLV_SCHEMA_SINT,
+ .max_occurrences = 1,
+ .present = CTLV_NODE_SCHEMA_HAS_MIN | CTLV_NODE_SCHEMA_HAS_MAX,
+ .min = (__u64)INT64_MIN,
+ .max = INT64_MAX,
+ },
+ {
+ .type = CTLV_T_SCHEMA_NODE_BLOB_DIRECTION,
+ .kind = CTLV_SCHEMA_ENUM,
+ .max_occurrences = 1,
+ .present = CTLV_NODE_SCHEMA_HAS_MIN | CTLV_NODE_SCHEMA_HAS_MAX,
+ .min = 1,
+ .max = 3,
+ },
+ {
+ .type = CTLV_T_SCHEMA_NODE_BLOB_BACKINGS,
+ .kind = CTLV_SCHEMA_UINT,
+ .max_occurrences = 1,
+ .present = CTLV_NODE_SCHEMA_HAS_MIN | CTLV_NODE_SCHEMA_HAS_MAX,
+ .min = 0,
+ .max = UINT64_MAX,
+ },
+ {
+ .type = CTLV_T_SCHEMA_NODE_BLOB_MAX_LENGTH,
+ .kind = CTLV_SCHEMA_UINT,
+ .max_occurrences = 1,
+ .present = CTLV_NODE_SCHEMA_HAS_MIN | CTLV_NODE_SCHEMA_HAS_MAX,
+ .min = 0,
+ .max = UINT64_MAX,
+ },
+ {
+ .type = CTLV_T_SCHEMA_NODE_BLOB_ALIGNMENT,
+ .kind = CTLV_SCHEMA_UINT,
+ .max_occurrences = 1,
+ .present = CTLV_NODE_SCHEMA_HAS_MIN | CTLV_NODE_SCHEMA_HAS_MAX,
+ .min = 0,
+ .max = UINT64_MAX,
+ },
+ {
+ .type = CTLV_T_SCHEMA_NODE_REENTERS,
+ .kind = CTLV_SCHEMA_UINT,
+ .max_occurrences = 1,
+ .present = CTLV_NODE_SCHEMA_HAS_MIN | CTLV_NODE_SCHEMA_HAS_MAX,
+ .min = 1,
+ .max = UINT64_MAX,
+ },
+ {
+ .type = CTLV_T_SCHEMA_NODE_FIELDS,
+ .kind = CTLV_SCHEMA_RECORD,
+ .max_occurrences = 0, /* unlimited */
+ .present = CTLV_NODE_SCHEMA_HAS_REENTERS,
+ .reenters = 1,
+ },
+ {
+ .type = CTLV_T_SCHEMA_NODE_OPTIONAL_FIELDS,
+ .kind = CTLV_SCHEMA_RECORD,
+ .max_occurrences = 0, /* unlimited */
+ .present = CTLV_NODE_SCHEMA_HAS_REENTERS,
+ .reenters = 1,
+ },
+};
+
+/* root, optional fields */
+static const struct ctlv_node_schema op_schema_get_reply_optional_field_node_schemas[] = {
+ {
+ .type = CTLV_T_OP_SCHEMA_GET_REQUEST,
+ .kind = CTLV_SCHEMA_RECORD,
+ .max_occurrences = 1,
+ .fields = op_schema_get_reply_request_field_node_schemas,
+ .fields_count = 3,
+ .optional_fields = op_schema_get_reply_request_optional_field_node_schemas,
+ .optional_fields_count = 14,
+ },
+ {
+ .type = CTLV_T_OP_SCHEMA_GET_REPLY,
+ .kind = CTLV_SCHEMA_RECORD,
+ .max_occurrences = 1,
+ .fields = op_schema_get_reply_reply_field_node_schemas,
+ .fields_count = 3,
+ .optional_fields = op_schema_get_reply_reply_optional_field_node_schemas,
+ .optional_fields_count = 14,
+ },
+};
+
+static const struct ctlv_node_schema op_schema_get_reply_node_schema = {
+ .type = CTLV_TYPE_ROOT,
+ .kind = CTLV_SCHEMA_RECORD,
+ .max_occurrences = 1,
+ .optional_fields = op_schema_get_reply_optional_field_node_schemas,
+ .optional_fields_count = 2,
+};
+
+/* Effective ops in declaration order. */
+static const struct ctlv_op_spec ctlv_op_specs[] = {
+ {
+ .op_id = CTLV_OP_FAMILY_GET,
+ .type = CTLV_OP_TYPE_QUERY,
+ .fd_access = CTLV_OP_FD_ACCESS_READ,
+ .availability = CTLV_OP_AVAILABILITY_FIXED,
+ .request_max_len = 16,
+ .query_reply_min_len = 112,
+ .query_reply_suggested_len = 65536,
+ .error_info_max_len = 456,
+ .request = &family_get_request_node_schema,
+ .reply = &family_get_reply_node_schema,
+ },
+ {
+ .op_id = CTLV_OP_OP_GET,
+ .type = CTLV_OP_TYPE_QUERY,
+ .fd_access = CTLV_OP_FD_ACCESS_READ,
+ .availability = CTLV_OP_AVAILABILITY_FIXED,
+ .request_max_len = 16,
+ .query_reply_min_len = 56,
+ .query_reply_suggested_len = 65536,
+ .error_info_max_len = 456,
+ .request = &op_get_request_node_schema,
+ .reply = &op_get_reply_node_schema,
+ },
+ {
+ .op_id = CTLV_OP_OP_SCHEMA_GET,
+ .type = CTLV_OP_TYPE_QUERY,
+ .fd_access = CTLV_OP_FD_ACCESS_READ,
+ .availability = CTLV_OP_AVAILABILITY_FIXED,
+ .request_max_len = 40,
+ .query_reply_min_len = 168,
+ .query_reply_suggested_len = 65536,
+ .error_info_max_len = 456,
+ .request = &op_schema_get_request_node_schema,
+ .reply = &op_schema_get_reply_node_schema,
+ },
+};
+
+/* Family specification descriptor. */
+const struct ctlv_family_spec ctlv_spec = {
+ .name = "ctlv",
+ .family_id = 0,
+ .ops = ctlv_op_specs,
+ .ops_count = 3,
+};
diff --git a/tools/ctlv/libctlv/device.c b/tools/ctlv/libctlv/device.c
new file mode 100644
index 000000000000..468ec2fb563a
--- /dev/null
+++ b/tools/ctlv/libctlv/device.c
@@ -0,0 +1,412 @@
+// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+/*
+ * Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES
+ *
+ * Device discovery, opening, and family-chain verification.
+ */
+
+#include <dirent.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#include "internal.h"
+
+/* Whichever subsystem a device is in announces its family, so all are here. */
+#define CHAR_DIR "/sys/dev/char"
+#define DEV_DIR "/dev"
+
+/* Longest device or family name, and longest path naming one. */
+#define NAME_LEN 256
+#define PATH_LEN 1024
+
+/* One line of a uevent file, PROPERTY=value. */
+#define UEVENT_LINE_LEN 512
+
+static int name_dup(const char *name, char **out)
+{
+ char *copy = strdup(name);
+
+ if (!copy)
+ return -ENOMEM;
+ *out = copy;
+ return 0;
+}
+
+/* The value @line gives @property, its newline dropped, or NULL. */
+static const char *uevent_value(char *line, const char *property)
+{
+ size_t prefix = strlen(property);
+ char *end;
+
+ if (strncmp(line, property, prefix) || line[prefix] != '=')
+ return NULL;
+ end = strchr(line, '\n');
+ if (end)
+ *end = '\0';
+ return line + prefix + 1;
+}
+
+/*
+ * The family of one char device and its name under /dev, NAME_LEN each, or
+ * -ENOENT for a device that is not a CTLV one.
+ */
+static int uevent_read(const char *devnum, char *family, char *devname)
+{
+ char line[UEVENT_LINE_LEN];
+ char path[PATH_LEN];
+ FILE *file;
+
+ family[0] = '\0';
+ devname[0] = '\0';
+ snprintf(path, sizeof(path), "%s/%s/uevent", CHAR_DIR, devnum);
+ file = fopen(path, "re");
+ if (!file)
+ return -errno;
+ while (fgets(line, sizeof(line), file)) {
+ const char *value = uevent_value(line, "CTLV_FAMILY");
+ char *out = family;
+ size_t len;
+
+ if (!value) {
+ value = uevent_value(line, "DEVNAME");
+ out = devname;
+ }
+ if (!value)
+ continue;
+ len = strlen(value);
+ /* A property this reader cannot hold is not one it matched. */
+ if (len < NAME_LEN)
+ memcpy(out, value, len + 1);
+ }
+ fclose(file);
+ return *family && *devname ? 0 : -ENOENT;
+}
+
+static int name_cmp(const void *a, const void *b)
+{
+ return strcmp(*(const char *const *)a, *(const char *const *)b);
+}
+
+/* Every CTLV device, or only those registered as @family. */
+static int dev_scan(struct ctlv_dev_list *list, const char *family)
+{
+ char found[NAME_LEN], devname[NAME_LEN];
+ struct dirent *entry;
+ char **name = NULL;
+ unsigned int n = 0;
+ DIR *dir;
+ int ret = 0;
+
+ memset(list, 0, sizeof(*list));
+ dir = opendir(CHAR_DIR);
+ if (!dir)
+ return -errno;
+ while ((entry = readdir(dir))) {
+ char **grown;
+
+ if (entry->d_name[0] == '.' ||
+ uevent_read(entry->d_name, found, devname) ||
+ (family && strcmp(found, family)))
+ continue;
+ grown = realloc(name, (n + 1) * sizeof(*name));
+ if (!grown) {
+ ret = -ENOMEM;
+ break;
+ }
+ name = grown;
+ ret = name_dup(devname, &name[n]);
+ if (ret)
+ break;
+ n++;
+ }
+ closedir(dir);
+ list->names_count = n;
+ list->names = name;
+ if (ret) {
+ ctlv_dev_list_free(list);
+ return ret;
+ }
+ /* Ordered, so that a tool listing devices lists them the same twice. */
+ if (name)
+ qsort(name, n, sizeof(*name), name_cmp);
+ return 0;
+}
+
+int ctlv_enumerate(struct ctlv_dev_list *list)
+{
+ return dev_scan(list, NULL);
+}
+
+void ctlv_dev_list_free(struct ctlv_dev_list *list)
+{
+ unsigned int i;
+
+ for (i = 0; i < list->names_count; i++)
+ free(list->names[i]);
+ free(list->names);
+ memset(list, 0, sizeof(*list));
+}
+
+/* The chain the device reports, root family first and device family last. */
+int ctlv_chain_load(struct ctlv_dev *dev)
+{
+ const struct ctlv_hdr *root, *entry;
+ struct ctlv_reply *reply = NULL;
+ unsigned int n = 0;
+ int ret;
+
+ ret = ctlv_query_sized(dev, CTLV_OP_FAMILY_GET, NULL, 0, &reply,
+ NULL);
+ if (ret)
+ return ret;
+
+ root = ctlv_reply_root(reply);
+ ret = ctlv_bootstrap_validate(root, reply->msg->len,
+ CTLV_OP_FAMILY_GET);
+ if (ret)
+ goto out;
+ n = ctlv_hdr_count(root, CTLV_T_FAMILY_GET_FAMILIES);
+ dev->chain_id = calloc(n, sizeof(*dev->chain_id));
+ dev->chain_name = calloc(n, sizeof(*dev->chain_name));
+ if (!dev->chain_id || !dev->chain_name) {
+ ret = -ENOMEM;
+ goto out;
+ }
+
+ ctlv_for_each_type(entry, root, CTLV_T_FAMILY_GET_FAMILIES) {
+ const struct ctlv_hdr *hdr;
+ const char *name;
+ __u64 id;
+
+ hdr = ctlv_hdr_find(entry, CTLV_T_FAMILY_GET_FAMILIES_ID);
+ if (!hdr || ctlv_get_u64(hdr, &id) || id > UINT16_MAX) {
+ ret = -EPROTO;
+ goto out;
+ }
+ hdr = ctlv_hdr_find(entry, CTLV_T_FAMILY_GET_FAMILIES_NAME);
+ if (!hdr || ctlv_get_str(hdr, &name)) {
+ ret = -EPROTO;
+ goto out;
+ }
+ dev->chain_id[dev->chain_len] = id;
+ ret = name_dup(name, &dev->chain_name[dev->chain_len]);
+ if (ret)
+ goto out;
+ dev->chain_len++;
+ }
+ ret = 0;
+out:
+ ctlv_reply_free(reply);
+ return ret;
+}
+
+static void dev_free(struct ctlv_dev *dev)
+{
+ unsigned int i;
+
+ for (i = 0; i < dev->chain_len; i++)
+ free(dev->chain_name[i]);
+ free(dev->chain_name);
+ free(dev->chain_id);
+ free(dev->name);
+ ctlv_ops_free(dev->sizes);
+ if (dev->fd >= 0 && dev->owns_fd)
+ close(dev->fd);
+ free(dev);
+}
+
+/* @fd's device, whose family chain it is asked for over that same fd. */
+static int dev_attach(const char *name, int fd, bool owns_fd,
+ struct ctlv_dev **devp)
+{
+ struct ctlv_dev *dev;
+ int ret;
+
+ dev = calloc(1, sizeof(*dev));
+ if (!dev) {
+ if (owns_fd)
+ close(fd);
+ return -ENOMEM;
+ }
+ dev->fd = fd;
+ dev->owns_fd = owns_fd;
+ ret = name_dup(name, &dev->name);
+ if (!ret)
+ ret = ctlv_chain_load(dev);
+ if (ret) {
+ dev_free(dev);
+ return ret;
+ }
+ *devp = dev;
+ return 0;
+}
+
+static int dev_open(const char *name, const char *path, int flags,
+ struct ctlv_dev **devp)
+{
+ int fd = open(path, flags | O_CLOEXEC);
+
+ if (fd < 0)
+ return -errno;
+ return dev_attach(name, fd, true, devp);
+}
+
+int ctlv_open_path(const char *path, int flags, struct ctlv_dev **devp)
+{
+ const char *name = strrchr(path, '/');
+
+ return dev_open(name ? name + 1 : path, path, flags, devp);
+}
+
+int ctlv_open_fd(int fd, struct ctlv_dev **devp)
+{
+ char link[PATH_LEN], target[PATH_LEN];
+ const char *name = target;
+ ssize_t len;
+
+ if (fd < 0)
+ return -EBADF;
+ /* Named for messages only, after the file if /proc can say which. */
+ snprintf(link, sizeof(link), "/proc/self/fd/%d", fd);
+ len = readlink(link, target, sizeof(target) - 1);
+ if (len < 0) {
+ snprintf(target, sizeof(target), "fd%d", fd);
+ } else {
+ target[len] = '\0';
+ if (strrchr(target, '/'))
+ name = strrchr(target, '/') + 1;
+ }
+ return dev_attach(name, fd, false, devp);
+}
+
+/*
+ * The @which'th device of @family, matched against its registered family
+ * rather than any it inherits.
+ */
+int ctlv_open(const char *family, unsigned int which, int flags,
+ struct ctlv_dev **devp)
+{
+ char path[PATH_LEN];
+ struct ctlv_dev_list list;
+ int ret;
+
+ ret = dev_scan(&list, family);
+ if (ret)
+ return ret;
+ ret = -ENODEV;
+ if (which < list.names_count) {
+ snprintf(path, sizeof(path), "%s/%s", DEV_DIR,
+ list.names[which]);
+ ret = dev_open(list.names[which], path, flags, devp);
+ }
+ ctlv_dev_list_free(&list);
+ return ret;
+}
+
+void ctlv_close(struct ctlv_dev *dev)
+{
+ if (dev)
+ dev_free(dev);
+}
+
+int ctlv_dev_fd(const struct ctlv_dev *dev)
+{
+ return dev->fd;
+}
+
+const char *ctlv_dev_name(const struct ctlv_dev *dev)
+{
+ return dev->name;
+}
+
+unsigned int ctlv_chain_len(const struct ctlv_dev *dev)
+{
+ return dev->chain_len;
+}
+
+__u16 ctlv_chain_id(const struct ctlv_dev *dev, unsigned int level)
+{
+ return level < dev->chain_len ? dev->chain_id[level] : 0;
+}
+
+const char *ctlv_chain_name(const struct ctlv_dev *dev, unsigned int level)
+{
+ return level < dev->chain_len ? dev->chain_name[level] : NULL;
+}
+
+const char *ctlv_dev_family(const struct ctlv_dev *dev)
+{
+ return ctlv_chain_name(dev, dev->chain_len - 1);
+}
+
+bool ctlv_dev_inherits(const struct ctlv_dev *dev, const char *family)
+{
+ unsigned int i;
+
+ for (i = 0; i < dev->chain_len; i++)
+ if (!strcmp(dev->chain_name[i], family))
+ return true;
+ return false;
+}
+
+/*
+ * The dump is taken once: its lengths follow from the schema and cannot go out
+ * of date.
+ */
+const struct ctlv_ops *ctlv_dev_sizes(struct ctlv_dev *dev)
+{
+ if (!dev->sizes && ctlv_ops_get(dev, &dev->sizes, NULL))
+ return NULL;
+ return dev->sizes;
+}
+
+/* What one op answers with, for the two calls that were not told. */
+static const struct ctlv_op_info *op_sizes(struct ctlv_dev *dev, __u64 op_id)
+{
+ const struct ctlv_ops *sizes = ctlv_dev_sizes(dev);
+
+ return sizes ? ctlv_ops_find(sizes, op_id) : NULL;
+}
+
+/*
+ * Room for a refusal's error information as well: an action cannot be retried
+ * into a larger buffer the way a query can, it has happened.
+ */
+int ctlv_action(struct ctlv_dev *dev, __u64 op_id, struct ctlv_msg *request,
+ struct ctlv_reply **replyp, struct ctlv_error *error)
+{
+ const struct ctlv_op_info *info = op_sizes(dev, op_id);
+ __u64 reply_len = 0;
+
+ if (info) {
+ reply_len = info->action_reply_max_len;
+ if (reply_len < info->error_info_max_len)
+ reply_len = info->error_info_max_len;
+ }
+ return ctlv_action_sized(dev, op_id, request, reply_len, replyp,
+ error);
+}
+
+/*
+ * A dump that went stale is started again, the fragments of two answers not
+ * being an answer. A caller wanting the -ESTALE instead asks for a size.
+ */
+int ctlv_query(struct ctlv_dev *dev, __u64 op_id, struct ctlv_msg *request,
+ struct ctlv_reply **replyp, struct ctlv_error *error)
+{
+ const struct ctlv_op_info *info = op_sizes(dev, op_id);
+ __u32 buf_len = 0;
+ unsigned int try;
+ int ret;
+
+ if (info)
+ buf_len = info->query_reply_suggested_len;
+ for (try = 0; try < QUERY_RESTARTS; try++) {
+ ret = ctlv_query_sized(dev, op_id, request, buf_len, replyp,
+ error);
+ if (ret != -ESTALE)
+ return ret;
+ }
+ return ret;
+}
diff --git a/tools/ctlv/libctlv/internal.h b/tools/ctlv/libctlv/internal.h
new file mode 100644
index 000000000000..ff3553561ac6
--- /dev/null
+++ b/tools/ctlv/libctlv/internal.h
@@ -0,0 +1,161 @@
+/* SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause */
+/*
+ * Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES
+ *
+ * Private libctlv declarations.
+ */
+#ifndef _CTLV_INTERNAL_H
+#define _CTLV_INTERNAL_H
+
+#include <errno.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "ctlv.h"
+
+#define CTLV_ARRAY_SIZE(_a) (sizeof(_a) / sizeof((_a)[0]))
+
+/*
+ * How many times a query is started over when the answer changed underneath
+ * it. More than this is a device changing faster than it can be described.
+ */
+#define QUERY_RESTARTS 8
+
+/*
+ * The deepest level a message may reach, the root at depth zero. No device
+ * goes deeper today, so a message that does is malformed.
+ */
+#define CTLV_RECORD_DEPTH_MAX 32
+
+/* The root is at depth zero, hence one more than the levels below it. */
+#define CTLV_DEPTH_MAX (CTLV_RECORD_DEPTH_MAX + 1)
+
+/* Stops a runaway builder: no device today takes a longer request. */
+#define CTLV_REQUEST_LEN_MAX 0x10000
+
+/*
+ * Reassembly needs a bound of its own, a query reply not being held to the
+ * inline limit. A message cannot describe more anyway: a record's length is
+ * u32.
+ */
+#define CTLV_REPLY_LEN_MAX (64U << 20)
+
+/* Bounds the cursor history retained for one query. */
+#define CTLV_QUERY_FRAGMENTS_MAX (64U << 10)
+
+/**
+ * struct ctlv_msg - One message being written.
+ * @buf: the bytes, grown as needed and aligned as a root must be
+ * @capacity: how much @buf holds
+ * @max: how far @capacity may grow before appending fails
+ * @len: how much of it is written
+ * @open: offsets of the record headers not yet ended, the root first
+ * @depth: how many of those there are
+ * @last: offset of the TLV written last, or of a record just ended; zero while
+ * that is the root, which no per-TLV choice applies to
+ * @err: the first failure since the last reset
+ *
+ * A TLV is tracked by offset because a record's length is only known once its
+ * children are written, and because growing @buf moves it.
+ */
+struct ctlv_msg {
+ __u8 *buf;
+ __u32 capacity;
+ __u32 max;
+ __u32 len;
+ __u32 open[CTLV_DEPTH_MAX];
+ unsigned int depth;
+ __u32 last;
+ int err;
+};
+
+int ctlv_msg_new_max(__u32 capacity, __u32 max, struct ctlv_msg **msgp);
+int ctlv_msg_put(struct ctlv_msg *msg, __u64 type, __u32 flags,
+ const void *payload, __u32 len);
+/* Appends a copy of one TLV, header and payload, without its flags. */
+int ctlv_msg_copy(struct ctlv_msg *msg, const struct ctlv_hdr *hdr);
+
+struct ctlv_frag_path {
+ __u64 type[CTLV_RECORD_DEPTH_MAX];
+ unsigned int depth;
+};
+
+int ctlv_frag_splice(struct ctlv_msg *out, const struct ctlv_hdr *root,
+ struct ctlv_frag_path *open, bool *progress);
+
+struct ctlv_query_guard {
+ __u64 *cursors;
+ unsigned int cursors_capacity;
+ unsigned int cursors_count;
+};
+
+int ctlv_query_guard_next(struct ctlv_query_guard *guard, __u64 cursor,
+ bool progress);
+void ctlv_query_guard_free(struct ctlv_query_guard *guard);
+
+/**
+ * struct ctlv_dev - One open device.
+ * @fd: the character device
+ * @owns_fd: whether ctlv_close() closes @fd
+ * @name: what it is called under /dev
+ * @chain_len: how many families it answers
+ * @chain_id: their registry IDs, the root first
+ * @chain_name: their names, in the same order
+ * @sizes: one ops dump, kept for the message lengths it reports
+ *
+ * @sizes is filled the first time a length is needed and was not named, and
+ * only its length members are read: they cannot go out of date.
+ */
+struct ctlv_dev {
+ int fd;
+ bool owns_fd;
+ char *name;
+ unsigned int chain_len;
+ __u16 *chain_id;
+ char **chain_name;
+ struct ctlv_ops *sizes;
+};
+
+/**
+ * struct ctlv_reply - One complete answer.
+ * @msg: the reassembled message, whose root is the reply's
+ * @generation: what the device reported the answer belongs to
+ * @fragments: how many ioctls it took
+ */
+struct ctlv_reply {
+ struct ctlv_msg *msg;
+ __u64 generation;
+ unsigned int fragments;
+};
+
+/**
+ * struct ctlv_schema - One op's trees and their node schemas.
+ * @node_schemas: every node schema of both trees, owned as one allocation
+ * @node_schemas_count: number of node schemas
+ * @request: the root of the request tree, or NULL
+ * @reply: the root of the reply tree, or NULL
+ */
+struct ctlv_schema {
+ struct ctlv_node_schema *node_schemas;
+ unsigned int node_schemas_count;
+ const struct ctlv_node_schema *request;
+ const struct ctlv_node_schema *reply;
+};
+
+void ctlv_error_clear(struct ctlv_error *error);
+void ctlv_error_set(struct ctlv_error *error, int err);
+/* Reads whatever error information a failed op wrote into the reply buffer. */
+void ctlv_error_parse(struct ctlv_error *error, int err, const void *buf,
+ __u32 len);
+
+int ctlv_chain_load(struct ctlv_dev *dev);
+/* @dev's @sizes, dumped on first use; NULL when the dump failed. */
+const struct ctlv_ops *ctlv_dev_sizes(struct ctlv_dev *dev);
+int ctlv_bootstrap_validate(const void *buf, __u32 len, __u64 op_id);
+int ctlv_schema_parse(const struct ctlv_hdr *root, __u32 len,
+ struct ctlv_schema **sp);
+int ctlv_ops_parse(const struct ctlv_hdr *root, __u32 len,
+ __u64 generation, struct ctlv_ops **opsp);
+
+#endif /* _CTLV_INTERNAL_H */
diff --git a/tools/ctlv/libctlv/introspection.c b/tools/ctlv/libctlv/introspection.c
new file mode 100644
index 000000000000..1bd2662e5831
--- /dev/null
+++ b/tools/ctlv/libctlv/introspection.c
@@ -0,0 +1,238 @@
+// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+/*
+ * Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES
+ *
+ * Root-family introspection.
+ */
+
+#include "internal.h"
+
+enum op_info_ctype {
+ OP_INFO_U32 = 0,
+ OP_INFO_U64 = 1,
+ OP_INFO_BOOL = 2,
+};
+
+enum op_info_type {
+ OP_INFO_ACTION = 1,
+ OP_INFO_QUERY = 2,
+};
+
+static int op_info_read(struct ctlv_op_info *info,
+ const struct ctlv_hdr *entry)
+{
+ static const struct {
+ __u64 type;
+ size_t offset;
+ unsigned int applies;
+ enum op_info_ctype ctype;
+ } fields[] = {
+ { CTLV_T_OP_GET_OPS_FD_ACCESS,
+ offsetof(struct ctlv_op_info, fd_access),
+ OP_INFO_ACTION | OP_INFO_QUERY, OP_INFO_U32 },
+ { CTLV_T_OP_GET_OPS_AVAILABILITY,
+ offsetof(struct ctlv_op_info, availability),
+ OP_INFO_ACTION | OP_INFO_QUERY, OP_INFO_U32 },
+ { CTLV_T_OP_GET_OPS_DISABLED,
+ offsetof(struct ctlv_op_info, disabled),
+ OP_INFO_ACTION | OP_INFO_QUERY, OP_INFO_BOOL },
+ { CTLV_T_OP_GET_OPS_REQUEST_MAX_LEN,
+ offsetof(struct ctlv_op_info, request_max_len),
+ OP_INFO_ACTION | OP_INFO_QUERY, OP_INFO_U64 },
+ { CTLV_T_OP_GET_OPS_ACTION_REPLY_MAX_LEN,
+ offsetof(struct ctlv_op_info, action_reply_max_len),
+ OP_INFO_ACTION, OP_INFO_U64 },
+ { CTLV_T_OP_GET_OPS_QUERY_REPLY_MIN_LEN,
+ offsetof(struct ctlv_op_info, query_reply_min_len),
+ OP_INFO_QUERY, OP_INFO_U64 },
+ { CTLV_T_OP_GET_OPS_QUERY_REPLY_SUGGESTED_LEN,
+ offsetof(struct ctlv_op_info, query_reply_suggested_len),
+ OP_INFO_QUERY, OP_INFO_U64 },
+ { CTLV_T_OP_GET_OPS_ERROR_INFO_MAX_LEN,
+ offsetof(struct ctlv_op_info, error_info_max_len),
+ OP_INFO_ACTION | OP_INFO_QUERY, OP_INFO_U64 },
+ };
+ const struct ctlv_hdr *hdr;
+ unsigned int applies = 0;
+ unsigned int i;
+ __u64 value;
+ bool boolean;
+
+ hdr = ctlv_hdr_find(entry, CTLV_T_OP_GET_OPS_ID);
+ if (!hdr || ctlv_get_u64(hdr, &info->op_id))
+ return -EPROTO;
+ hdr = ctlv_hdr_find(entry, CTLV_T_OP_GET_OPS_TYPE);
+ if (!hdr || ctlv_get_u64(hdr, &value) || value > UINT32_MAX)
+ return -EPROTO;
+ info->type = value;
+ if (value == CTLV_OP_TYPE_ACTION)
+ applies = OP_INFO_ACTION;
+ else if (value == CTLV_OP_TYPE_QUERY)
+ applies = OP_INFO_QUERY;
+
+ for (i = 0; i < CTLV_ARRAY_SIZE(fields); i++) {
+ hdr = ctlv_hdr_find(entry, fields[i].type);
+ if (applies && !!hdr != !!(fields[i].applies & applies))
+ return -EPROTO;
+ if (!hdr)
+ continue;
+ if (fields[i].ctype == OP_INFO_BOOL) {
+ if (ctlv_get_bool(hdr, &boolean))
+ return -EPROTO;
+ *(bool *)((char *)info + fields[i].offset) = boolean;
+ continue;
+ }
+ if (ctlv_get_u64(hdr, &value))
+ return -EPROTO;
+ if (fields[i].ctype == OP_INFO_U32) {
+ if (value > UINT32_MAX)
+ return -EPROTO;
+ *(__u32 *)((char *)info + fields[i].offset) = value;
+ } else {
+ *(__u64 *)((char *)info + fields[i].offset) = value;
+ }
+ }
+ if ((applies & (OP_INFO_ACTION | OP_INFO_QUERY)) &&
+ info->availability == CTLV_OP_AVAILABILITY_FIXED &&
+ info->disabled)
+ return -EPROTO;
+ if (applies == OP_INFO_QUERY &&
+ info->query_reply_suggested_len < info->query_reply_min_len)
+ return -EPROTO;
+ return 0;
+}
+
+static int op_id_compare(const void *left, const void *right)
+{
+ const __u64 *a = left;
+ const __u64 *b = right;
+
+ return (*a > *b) - (*a < *b);
+}
+
+int ctlv_ops_parse(const struct ctlv_hdr *root, __u32 len,
+ __u64 generation, struct ctlv_ops **opsp)
+{
+ const struct ctlv_hdr *entry;
+ struct ctlv_ops *ops;
+ __u64 *ids = NULL;
+ unsigned int i;
+ unsigned int n = 0;
+ int ret;
+
+ ret = ctlv_bootstrap_validate(root, len, CTLV_OP_OP_GET);
+ if (ret)
+ return ret;
+
+ ops = calloc(1, sizeof(*ops));
+ if (!ops)
+ return -ENOMEM;
+ n = ctlv_hdr_count(root, CTLV_T_OP_GET_OPS);
+ if (n) {
+ ops->ops = calloc(n, sizeof(*ops->ops));
+ if (!ops->ops) {
+ ret = -ENOMEM;
+ goto out_free_ops;
+ }
+ ids = calloc(n, sizeof(*ids));
+ if (!ids) {
+ ret = -ENOMEM;
+ goto out_free_op;
+ }
+ }
+ ctlv_for_each_type(entry, root, CTLV_T_OP_GET_OPS) {
+ ret = op_info_read(&ops->ops[ops->ops_count], entry);
+ if (ret)
+ goto out_free_ids;
+ ids[ops->ops_count] = ops->ops[ops->ops_count].op_id;
+ ops->ops_count++;
+ }
+ if (n > 1)
+ qsort(ids, n, sizeof(*ids), op_id_compare);
+ for (i = 1; i < n; i++) {
+ if (ids[i] == ids[i - 1]) {
+ ret = -EPROTO;
+ goto out_free_ids;
+ }
+ }
+ free(ids);
+ ops->generation = generation;
+
+ *opsp = ops;
+ return 0;
+
+out_free_ids:
+ free(ids);
+out_free_op:
+ free(ops->ops);
+out_free_ops:
+ free(ops);
+ return ret;
+}
+
+int ctlv_ops_get(struct ctlv_dev *dev, struct ctlv_ops **opsp,
+ struct ctlv_error *error)
+{
+ struct ctlv_reply *reply = NULL;
+ int ret;
+
+ /* Sized, or the dump kept for op lengths would ask for itself. */
+ ret = ctlv_query_sized(dev, CTLV_OP_OP_GET, NULL, 0, &reply, error);
+ if (ret)
+ return ret;
+
+ ret = ctlv_ops_parse(ctlv_reply_root(reply), reply->msg->len,
+ ctlv_reply_generation(reply), opsp);
+ if (ret)
+ ctlv_error_set(error, ret);
+ ctlv_reply_free(reply);
+ return ret;
+}
+
+void ctlv_ops_free(struct ctlv_ops *ops)
+{
+ if (!ops)
+ return;
+ free(ops->ops);
+ free(ops);
+}
+
+const struct ctlv_op_info *ctlv_ops_find(const struct ctlv_ops *ops,
+ __u64 op_id)
+{
+ unsigned int i;
+
+ if (!ops)
+ return NULL;
+ for (i = 0; i < ops->ops_count; i++)
+ if (ops->ops[i].op_id == op_id)
+ return &ops->ops[i];
+ return NULL;
+}
+
+int ctlv_op_schema_get(struct ctlv_dev *dev, __u64 op_id,
+ struct ctlv_schema **schemap, struct ctlv_error *error)
+{
+ struct ctlv_reply *reply = NULL;
+ struct ctlv_msg *request;
+ int ret;
+
+ ret = ctlv_msg_new(64, &request);
+ if (ret) {
+ ctlv_error_set(error, ret);
+ return ret;
+ }
+ ctlv_put_u64(request, CTLV_T_OP_SCHEMA_GET_ID, op_id);
+
+ ret = ctlv_query(dev, CTLV_OP_OP_SCHEMA_GET, request, &reply, error);
+ ctlv_msg_free(request);
+ if (ret)
+ return ret;
+
+ ret = ctlv_schema_parse(ctlv_reply_root(reply), reply->msg->len,
+ schemap);
+ if (ret)
+ ctlv_error_set(error, ret);
+ ctlv_reply_free(reply);
+ return ret;
+}
diff --git a/tools/ctlv/libctlv/message.c b/tools/ctlv/libctlv/message.c
new file mode 100644
index 000000000000..ab4464fb8af2
--- /dev/null
+++ b/tools/ctlv/libctlv/message.c
@@ -0,0 +1,518 @@
+// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+/*
+ * Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES
+ *
+ * CTLV message builder and parser.
+ */
+
+#include <stdint.h>
+
+#include "internal.h"
+
+/* The TLV alignment applied to a length that may be close to overflowing. */
+static int aligned(__u32 len, __u32 *out)
+{
+ if (len > UINT32_MAX - (CTLV_ALIGNTO - 1))
+ return -EMSGSIZE;
+ *out = CTLV_ALIGN(len);
+ return 0;
+}
+
+static int msg_room(struct ctlv_msg *msg, __u32 need)
+{
+ __u32 capacity = msg->capacity;
+ __u8 *buf;
+
+ if (need > msg->max - msg->len)
+ return -EMSGSIZE;
+ need += msg->len;
+ if (need <= capacity)
+ return 0;
+ while (capacity < need) {
+ if (capacity > msg->max / 2) {
+ capacity = msg->max;
+ break;
+ }
+ capacity *= 2;
+ }
+ buf = realloc(msg->buf, capacity);
+ if (!buf)
+ return -ENOMEM;
+ msg->buf = buf;
+ msg->capacity = capacity;
+ return 0;
+}
+
+/* Zeroes up to the next TLV boundary; a message's padding is zero. */
+static int msg_pad(struct ctlv_msg *msg)
+{
+ __u32 padded;
+ int ret;
+
+ ret = aligned(msg->len, &padded);
+ if (ret)
+ return ret;
+ ret = msg_room(msg, padded - msg->len);
+ if (ret)
+ return ret;
+ memset(msg->buf + msg->len, 0, padded - msg->len);
+ msg->len = padded;
+ return 0;
+}
+
+static struct ctlv_hdr *msg_hdr(struct ctlv_msg *msg, __u32 offset)
+{
+ return (struct ctlv_hdr *)(msg->buf + offset);
+}
+
+/* One TLV at the next boundary; a writer holding an error appends none. */
+int ctlv_msg_put(struct ctlv_msg *msg, __u64 type, __u32 flags,
+ const void *payload, __u32 len)
+{
+ struct ctlv_hdr hdr = {
+ .flags = flags,
+ .type = type,
+ };
+ int ret;
+
+ if (msg->err)
+ return msg->err;
+ if (len > UINT32_MAX - sizeof(hdr)) {
+ msg->err = -EMSGSIZE;
+ return msg->err;
+ }
+ hdr.len = sizeof(hdr) + len;
+
+ ret = msg_pad(msg);
+ if (!ret)
+ ret = msg_room(msg, hdr.len);
+ if (ret) {
+ msg->err = ret;
+ return ret;
+ }
+ msg->last = msg->len;
+ memcpy(msg->buf + msg->len, &hdr, sizeof(hdr));
+ msg->len += sizeof(hdr);
+ if (len) {
+ memcpy(msg->buf + msg->len, payload, len);
+ msg->len += len;
+ }
+ return 0;
+}
+
+int ctlv_msg_copy(struct ctlv_msg *msg, const struct ctlv_hdr *hdr)
+{
+ return ctlv_msg_put(msg, hdr->type,
+ hdr->flags & ~CTLV_HDR_F_FRAG_MORE, hdr + 1,
+ hdr->len - sizeof(*hdr));
+}
+
+int ctlv_msg_new_max(__u32 capacity, __u32 max, struct ctlv_msg **msgp)
+{
+ struct ctlv_msg *msg;
+
+ if (!capacity || capacity > max)
+ return -EINVAL;
+ msg = calloc(1, sizeof(*msg));
+ if (!msg)
+ return -ENOMEM;
+ /* Separate from the handle, so growing the buffer does not move it. */
+ msg->buf = malloc(capacity);
+ if (!msg->buf) {
+ free(msg);
+ return -ENOMEM;
+ }
+ msg->capacity = capacity;
+ msg->max = max;
+ *msgp = msg;
+ return ctlv_record_start(msg, CTLV_TYPE_ROOT);
+}
+
+int ctlv_msg_new(__u32 capacity, struct ctlv_msg **msgp)
+{
+ return ctlv_msg_new_max(capacity, CTLV_REQUEST_LEN_MAX, msgp);
+}
+
+void ctlv_msg_free(struct ctlv_msg *msg)
+{
+ if (!msg)
+ return;
+ free(msg->buf);
+ free(msg);
+}
+
+void ctlv_msg_reset(struct ctlv_msg *msg)
+{
+ msg->len = 0;
+ msg->depth = 0;
+ msg->last = 0;
+ msg->err = 0;
+ ctlv_record_start(msg, CTLV_TYPE_ROOT);
+}
+
+int ctlv_msg_err(const struct ctlv_msg *msg)
+{
+ return msg->err;
+}
+
+int ctlv_record_start(struct ctlv_msg *msg, __u64 type)
+{
+ __u32 offset;
+ int ret;
+
+ if (msg->err)
+ return msg->err;
+ if (msg->depth == CTLV_DEPTH_MAX) {
+ msg->err = -ELOOP;
+ return msg->err;
+ }
+ /* Pad first, so the saved offset is where the header lands. */
+ ret = msg_pad(msg);
+ if (ret) {
+ msg->err = ret;
+ return ret;
+ }
+ offset = msg->len;
+ ret = ctlv_msg_put(msg, type, CTLV_HDR_F_RECORD, NULL, 0);
+ if (ret)
+ return ret;
+ msg->open[msg->depth++] = offset;
+ return 0;
+}
+
+/* A record counts the padding of its last child, so a walk steps aligned. */
+int ctlv_record_end(struct ctlv_msg *msg)
+{
+ int ret;
+
+ if (msg->err)
+ return msg->err;
+ if (!msg->depth) {
+ msg->err = -EINVAL;
+ return msg->err;
+ }
+ ret = msg_pad(msg);
+ if (ret) {
+ msg->err = ret;
+ return ret;
+ }
+ msg->depth--;
+ msg_hdr(msg, msg->open[msg->depth])->len =
+ msg->len - msg->open[msg->depth];
+ /* A closed record is the TLV a per-TLV choice now applies to. */
+ msg->last = msg->open[msg->depth];
+ return 0;
+}
+
+/*
+ * The bit is a per-occurrence choice, so it belongs to a call of its own after
+ * whatever wrote the TLV, a generated putter included.
+ */
+int ctlv_msg_ignore_if_unknown(struct ctlv_msg *msg)
+{
+ if (msg->err)
+ return msg->err;
+ /* The root frames the message and is nothing a device may skip. */
+ if (!msg->last) {
+ msg->err = -EINVAL;
+ return msg->err;
+ }
+ msg_hdr(msg, msg->last)->flags |= CTLV_HDR_F_IGNORE_IF_UNKNOWN;
+ return 0;
+}
+
+int ctlv_msg_done(struct ctlv_msg *msg)
+{
+ while (msg->depth && !msg->err)
+ ctlv_record_end(msg);
+ return msg->err;
+}
+
+const struct ctlv_hdr *ctlv_msg_root(const struct ctlv_msg *msg)
+{
+ return (const struct ctlv_hdr *)msg->buf;
+}
+
+__u32 ctlv_msg_len(const struct ctlv_msg *msg)
+{
+ return msg->len;
+}
+
+int ctlv_put_u64(struct ctlv_msg *msg, __u64 type, __u64 value)
+{
+ return ctlv_msg_put(msg, type, 0, &value, sizeof(value));
+}
+
+int ctlv_put_s64(struct ctlv_msg *msg, __u64 type, __s64 value)
+{
+ return ctlv_msg_put(msg, type, 0, &value, sizeof(value));
+}
+
+int ctlv_put_bool(struct ctlv_msg *msg, __u64 type, bool value)
+{
+ __u8 byte = value;
+
+ return ctlv_msg_put(msg, type, 0, &byte, sizeof(byte));
+}
+
+/* The terminator is part of the payload, and a declared length excludes it. */
+int ctlv_put_str(struct ctlv_msg *msg, __u64 type, const char *value)
+{
+ size_t len = strlen(value) + 1;
+
+ if (len > UINT32_MAX)
+ return -EMSGSIZE;
+ return ctlv_msg_put(msg, type, 0, value, len);
+}
+
+int ctlv_put_bin(struct ctlv_msg *msg, __u64 type, const void *value,
+ __u32 len)
+{
+ return ctlv_msg_put(msg, type, 0, value, len);
+}
+
+/* A blob TLV carries a record of core members, chosen by the backing. */
+int ctlv_put_blob_va(struct ctlv_msg *msg, __u64 type, const void *address,
+ __u64 length)
+{
+ ctlv_record_start(msg, type);
+ ctlv_put_u64(msg, CTLV_T_BLOB_TYPE, CTLV_BLOB_TYPE_VA);
+ ctlv_put_u64(msg, CTLV_T_BLOB_ADDRESS, (__u64)(uintptr_t)address);
+ ctlv_put_u64(msg, CTLV_T_BLOB_LENGTH, length);
+ return ctlv_record_end(msg);
+}
+
+bool ctlv_hdr_is_record(const struct ctlv_hdr *hdr)
+{
+ return hdr->flags & CTLV_HDR_F_RECORD;
+}
+
+__u32 ctlv_hdr_payload_len(const struct ctlv_hdr *hdr)
+{
+ return hdr->len - sizeof(*hdr);
+}
+
+const void *ctlv_hdr_payload(const struct ctlv_hdr *hdr, __u32 *len)
+{
+ if (len)
+ *len = ctlv_hdr_payload_len(hdr);
+ return hdr + 1;
+}
+
+const struct ctlv_hdr *ctlv_hdr_first(const struct ctlv_hdr *record)
+{
+ if (record->len <= sizeof(*record))
+ return NULL;
+ /* A record too short to hold the header it announces holds no child. */
+ if (record->len - sizeof(*record) < sizeof(*record))
+ return NULL;
+ return record + 1;
+}
+
+/*
+ * The next child is a step of this one's aligned length. Both the step and the
+ * header it lands on have to stay inside the record.
+ */
+const struct ctlv_hdr *ctlv_hdr_next(const struct ctlv_hdr *record,
+ const struct ctlv_hdr *child)
+{
+ const char *end = (const char *)record + record->len;
+ const char *next = (const char *)child;
+ __u32 step;
+
+ if (aligned(child->len, &step))
+ return NULL;
+ next += step;
+ if (next < (const char *)child || next > end)
+ return NULL;
+ if ((size_t)(end - next) < sizeof(*child))
+ return NULL;
+ return (const struct ctlv_hdr *)next;
+}
+
+const struct ctlv_hdr *ctlv_hdr_find(const struct ctlv_hdr *record,
+ __u64 type)
+{
+ const struct ctlv_hdr *child;
+
+ ctlv_for_each(child, record)
+ if (child->type == type)
+ return child;
+ return NULL;
+}
+
+const struct ctlv_hdr *ctlv_hdr_find_next(const struct ctlv_hdr *record,
+ const struct ctlv_hdr *hdr,
+ __u64 type)
+{
+ while ((hdr = ctlv_hdr_next(record, hdr)))
+ if (hdr->type == type)
+ return hdr;
+ return NULL;
+}
+
+unsigned int ctlv_hdr_count(const struct ctlv_hdr *record, __u64 type)
+{
+ const struct ctlv_hdr *child;
+ unsigned int n = 0;
+
+ ctlv_for_each(child, record)
+ if (child->type == type)
+ n++;
+ return n;
+}
+
+/*
+ * Framing, checked before anything reads it: a message that fails this is not
+ * walked, which is what lets the walkers above trust the lengths they step.
+ */
+static int check_record(const struct ctlv_hdr *record, unsigned int depth)
+{
+ const char *p = (const char *)(record + 1);
+ const char *end = (const char *)record + record->len;
+
+ if (depth >= CTLV_DEPTH_MAX)
+ return -ELOOP;
+ while (p < end) {
+ const struct ctlv_hdr *child = (const struct ctlv_hdr *)p;
+ __u32 step;
+ int ret;
+
+ if ((size_t)(end - p) < sizeof(*child))
+ return -EBADMSG;
+ if (child->len < sizeof(*child))
+ return -EBADMSG;
+ if (child->len > (__u32)(end - p))
+ return -EBADMSG;
+ if (child->flags & CTLV_HDR_F_RECORD) {
+ ret = check_record(child, depth + 1);
+ if (ret)
+ return ret;
+ }
+ ret = aligned(child->len, &step);
+ if (ret)
+ return ret;
+ /* Padding is in the record, so the step lands on its end. */
+ if (step > (__u32)(end - p))
+ return -EBADMSG;
+ p += step;
+ }
+ return 0;
+}
+
+int ctlv_msg_check(const void *buf, __u32 len)
+{
+ const struct ctlv_hdr *root = buf;
+
+ if (len < sizeof(*root))
+ return -EBADMSG;
+ if (root->type != CTLV_TYPE_ROOT)
+ return -EBADMSG;
+ if (!(root->flags & CTLV_HDR_F_RECORD))
+ return -EBADMSG;
+ if (root->len < sizeof(*root) || root->len > len)
+ return -EBADMSG;
+ return check_record(root, 0);
+}
+
+/*
+ * A scalar payload is eight bytes in a message whatever it holds, so another
+ * width is a corrupt message rather than something to interpret.
+ */
+static int get_scalar(const struct ctlv_hdr *hdr, __u64 *value)
+{
+ __u64 raw;
+
+ if (ctlv_hdr_is_record(hdr))
+ return -EINVAL;
+ if (ctlv_hdr_payload_len(hdr) != sizeof(raw))
+ return -EBADMSG;
+ memcpy(&raw, hdr + 1, sizeof(raw));
+ *value = raw;
+ return 0;
+}
+
+int ctlv_get_u64(const struct ctlv_hdr *hdr, __u64 *value)
+{
+ return get_scalar(hdr, value);
+}
+
+int ctlv_get_s64(const struct ctlv_hdr *hdr, __s64 *value)
+{
+ __u64 raw;
+ int ret;
+
+ ret = get_scalar(hdr, &raw);
+ if (ret)
+ return ret;
+ *value = (__s64)raw;
+ return 0;
+}
+
+int ctlv_get_bool(const struct ctlv_hdr *hdr, bool *value)
+{
+ const __u8 *byte;
+
+ if (ctlv_hdr_is_record(hdr))
+ return -EINVAL;
+ if (ctlv_hdr_payload_len(hdr) != sizeof(*byte))
+ return -EBADMSG;
+ byte = (const __u8 *)(hdr + 1);
+ if (*byte > 1)
+ return -EBADMSG;
+ *value = *byte;
+ return 0;
+}
+
+int ctlv_get_str(const struct ctlv_hdr *hdr, const char **value)
+{
+ const char *str = (const char *)(hdr + 1);
+ __u32 len = ctlv_hdr_payload_len(hdr);
+
+ if (ctlv_hdr_is_record(hdr))
+ return -EINVAL;
+ if (!len || str[len - 1] != '\0')
+ return -EBADMSG;
+ *value = str;
+ return 0;
+}
+
+int ctlv_get_bin(const struct ctlv_hdr *hdr, const void **value, __u32 *len)
+{
+ if (ctlv_hdr_is_record(hdr))
+ return -EINVAL;
+ *value = hdr + 1;
+ if (len)
+ *len = ctlv_hdr_payload_len(hdr);
+ return 0;
+}
+
+/*
+ * Neither refuses a value from a device newer than its caller: dropping the
+ * reply would make every added value an ABI break.
+ */
+int ctlv_get_enum(const struct ctlv_hdr *hdr, __u64 min, __u64 max,
+ __u64 *value, bool *known)
+{
+ int ret;
+
+ ret = get_scalar(hdr, value);
+ if (ret)
+ return ret;
+ if (known)
+ *known = *value >= min && *value <= max;
+ return 0;
+}
+
+int ctlv_get_flags(const struct ctlv_hdr *hdr, __u64 mask, __u64 *value,
+ __u64 *unknown)
+{
+ int ret;
+
+ ret = get_scalar(hdr, value);
+ if (ret)
+ return ret;
+ if (unknown)
+ *unknown = *value & ~mask;
+ return 0;
+}
diff --git a/tools/ctlv/libctlv/schema.c b/tools/ctlv/libctlv/schema.c
new file mode 100644
index 000000000000..cd68d237b568
--- /dev/null
+++ b/tools/ctlv/libctlv/schema.c
@@ -0,0 +1,742 @@
+// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+/*
+ * Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES
+ *
+ * Numeric schema parsing and comparison.
+ */
+
+#include <limits.h>
+#include <stddef.h>
+#include <stdarg.h>
+#include <stdio.h>
+
+#include "internal.h"
+
+struct validate {
+ const struct ctlv_node_schema *node[CTLV_DEPTH_MAX];
+};
+
+static const struct ctlv_node_schema *
+schema_member(const struct ctlv_node_schema *parent, __u64 type)
+{
+ unsigned int i;
+
+ for (i = 0; i < parent->fields_count; i++)
+ if (parent->fields[i].type == type)
+ return &parent->fields[i];
+ for (i = 0; i < parent->optional_fields_count; i++)
+ if (parent->optional_fields[i].type == type)
+ return &parent->optional_fields[i];
+ return NULL;
+}
+
+static int schema_members(const struct ctlv_hdr *record,
+ const struct ctlv_node_schema *parent)
+{
+ const struct ctlv_node_schema *child;
+ unsigned int count;
+ unsigned int i;
+
+ for (i = 0; i < parent->fields_count; i++) {
+ child = &parent->fields[i];
+ count = ctlv_hdr_count(record, child->type);
+ if (!count)
+ return -EPROTO;
+ if (child->max_occurrences &&
+ count > child->max_occurrences)
+ return -EPROTO;
+ }
+ for (i = 0; i < parent->optional_fields_count; i++) {
+ child = &parent->optional_fields[i];
+ count = ctlv_hdr_count(record, child->type);
+ if (child->max_occurrences &&
+ count > child->max_occurrences)
+ return -EPROTO;
+ }
+ return 0;
+}
+
+static bool schema_length_ok(const struct ctlv_node_schema *node, __u64 len)
+{
+ if ((node->present & CTLV_NODE_SCHEMA_HAS_MIN_LENGTH) &&
+ len < node->min_length)
+ return false;
+ if ((node->present & CTLV_NODE_SCHEMA_HAS_MAX_LENGTH) &&
+ len > node->max_length)
+ return false;
+ return true;
+}
+
+static int schema_scalar(const struct ctlv_hdr *hdr,
+ const struct ctlv_node_schema *node)
+{
+ const char *string;
+ __s64 signed_value;
+ __u64 value;
+ __u32 len;
+ bool boolean;
+ int ret;
+
+ switch (node->kind) {
+ case CTLV_SCHEMA_UINT:
+ case CTLV_SCHEMA_ENUM:
+ case CTLV_SCHEMA_FLAGS:
+ ret = ctlv_get_u64(hdr, &value);
+ if (ret)
+ return -EPROTO;
+ if (node->kind == CTLV_SCHEMA_FLAGS)
+ return 0;
+ if ((node->present & CTLV_NODE_SCHEMA_HAS_MIN) &&
+ value < node->min)
+ return -EPROTO;
+ /* New output enum values are appended above the known range. */
+ if (node->kind == CTLV_SCHEMA_ENUM)
+ return 0;
+ if ((node->present & CTLV_NODE_SCHEMA_HAS_MAX) &&
+ value > node->max)
+ return -EPROTO;
+ return 0;
+ case CTLV_SCHEMA_SINT:
+ ret = ctlv_get_s64(hdr, &signed_value);
+ if (ret)
+ return -EPROTO;
+ if ((node->present & CTLV_NODE_SCHEMA_HAS_MIN) &&
+ signed_value < (__s64)node->min)
+ return -EPROTO;
+ if ((node->present & CTLV_NODE_SCHEMA_HAS_MAX) &&
+ signed_value > (__s64)node->max)
+ return -EPROTO;
+ return 0;
+ case CTLV_SCHEMA_BOOL:
+ return ctlv_get_bool(hdr, &boolean) ? -EPROTO : 0;
+ case CTLV_SCHEMA_STRING:
+ ret = ctlv_get_str(hdr, &string);
+ if (ret)
+ return -EPROTO;
+ len = ctlv_hdr_payload_len(hdr);
+ if (memchr(string, '\0', len - 1))
+ return -EPROTO;
+ return schema_length_ok(node, len - 1) ? 0 : -EPROTO;
+ case CTLV_SCHEMA_BINARY:
+ len = ctlv_hdr_payload_len(hdr);
+ return schema_length_ok(node, len) ? 0 : -EPROTO;
+ default:
+ return -EPROTO;
+ }
+}
+
+static int schema_record(struct validate *validate,
+ const struct ctlv_hdr *record, unsigned int depth);
+
+static int schema_tlv(struct validate *validate,
+ const struct ctlv_hdr *hdr,
+ const struct ctlv_node_schema *node,
+ unsigned int depth)
+{
+ bool is_record = node->kind == CTLV_SCHEMA_RECORD ||
+ node->kind == CTLV_SCHEMA_BLOB;
+ __u32 flags = is_record ? CTLV_HDR_F_RECORD : 0;
+
+ if (is_record && depth >= CTLV_DEPTH_MAX)
+ return -ELOOP;
+ if (hdr->type != node->type || hdr->flags != flags)
+ return -EPROTO;
+ if (!is_record)
+ return schema_scalar(hdr, node);
+ if (node->kind == CTLV_SCHEMA_BLOB)
+ return -EPROTO;
+
+ validate->node[depth] = node;
+ return schema_record(validate, hdr, depth);
+}
+
+static int schema_record(struct validate *validate,
+ const struct ctlv_hdr *record, unsigned int depth)
+{
+ const struct ctlv_node_schema *parent = validate->node[depth];
+ const struct ctlv_node_schema *child_node;
+ const struct ctlv_hdr *child;
+ unsigned int level = depth;
+ int ret;
+
+ while (parent->present & CTLV_NODE_SCHEMA_HAS_REENTERS) {
+ if (!parent->reenters || parent->reenters > level)
+ return -EPROTO;
+ level -= parent->reenters;
+ parent = validate->node[level];
+ }
+
+ ret = schema_members(record, parent);
+ if (ret)
+ return ret;
+ ctlv_for_each(child, record) {
+ child_node = schema_member(parent, child->type);
+ if (!child_node) {
+ if (!child->type ||
+ child->flags & ~CTLV_HDR_F_RECORD)
+ return -EPROTO;
+ continue;
+ }
+ ret = schema_tlv(validate, child, child_node, depth + 1);
+ if (ret)
+ return ret;
+ }
+ return 0;
+}
+
+int ctlv_bootstrap_validate(const void *buf, __u32 len, __u64 op_id)
+{
+ const struct ctlv_node_schema *schema;
+ const struct ctlv_op_spec *op;
+ const struct ctlv_hdr *root = buf;
+ struct validate validate = {};
+ int ret;
+
+ op = ctlv_spec_find(&ctlv_spec, op_id);
+ if (!op || !op->reply)
+ return -EINVAL;
+ ret = ctlv_msg_check(buf, len);
+ if (ret)
+ return ret;
+ if (root->len != len)
+ return -EPROTO;
+
+ schema = op->reply;
+ if (schema->type != CTLV_TYPE_ROOT ||
+ schema->kind != CTLV_SCHEMA_RECORD)
+ return -EINVAL;
+ return schema_tlv(&validate, root, schema, 0);
+}
+
+/* A node's members occupy one contiguous range in the pool. */
+struct build {
+ struct ctlv_node_schema *node_schemas;
+ const struct ctlv_node_schema *path[CTLV_DEPTH_MAX];
+ unsigned int node_schemas_count;
+ unsigned int used;
+};
+
+static size_t text_append(char *buf, size_t len, size_t used,
+ const char *format, ...)
+{
+ va_list args;
+ int written;
+
+ if (!len)
+ return 0;
+ if (used >= len - 1) {
+ buf[len - 1] = '\0';
+ return len - 1;
+ }
+
+ va_start(args, format);
+ written = vsnprintf(buf + used, len - used, format, args);
+ va_end(args);
+ if (written < 0) {
+ buf[used] = '\0';
+ return used;
+ }
+ if ((size_t)written >= len - used)
+ return len - 1;
+ return used + written;
+}
+
+static bool node_u64(const struct ctlv_hdr *src, __u64 type, __u64 *value)
+{
+ const struct ctlv_hdr *hdr = ctlv_hdr_find(src, type);
+
+ return hdr && !ctlv_get_u64(hdr, value);
+}
+
+/* An optional member, and the bit that says it was there. */
+static void node_opt(const struct ctlv_hdr *src, __u64 type, __u32 bit,
+ __u64 *value, __u32 *present)
+{
+ if (node_u64(src, type, value))
+ *present |= bit;
+}
+
+static bool node_has(const struct ctlv_hdr *src, __u64 type)
+{
+ return ctlv_hdr_find(src, type);
+}
+
+static int node_id_compare(const void *left, const void *right)
+{
+ const __u64 *a = left;
+ const __u64 *b = right;
+
+ return (*a > *b) - (*a < *b);
+}
+
+static int node_children_check(const struct ctlv_hdr *src,
+ unsigned int count)
+{
+ const struct ctlv_hdr *child;
+ __u64 *ids;
+ unsigned int i = 0;
+ int ret = 0;
+
+ if (!count)
+ return 0;
+ ids = calloc(count, sizeof(*ids));
+ if (!ids)
+ return -ENOMEM;
+ ctlv_for_each_type(child, src, CTLV_T_SCHEMA_NODE_FIELDS) {
+ if (!node_u64(child, CTLV_T_SCHEMA_NODE_TYPE, &ids[i++])) {
+ ret = -EPROTO;
+ goto out;
+ }
+ }
+ ctlv_for_each_type(child, src, CTLV_T_SCHEMA_NODE_OPTIONAL_FIELDS) {
+ if (!node_u64(child, CTLV_T_SCHEMA_NODE_TYPE, &ids[i++])) {
+ ret = -EPROTO;
+ goto out;
+ }
+ }
+ if (i != count) {
+ ret = -EPROTO;
+ goto out;
+ }
+ if (count > 1)
+ qsort(ids, count, sizeof(*ids), node_id_compare);
+ for (i = 1; i < count; i++) {
+ if (ids[i] == ids[i - 1]) {
+ ret = -EPROTO;
+ break;
+ }
+ }
+out:
+ free(ids);
+ return ret;
+}
+
+static int node_kind_check(const struct ctlv_hdr *src,
+ const struct ctlv_node_schema *node,
+ unsigned int depth)
+{
+ bool range = node->kind == CTLV_SCHEMA_UINT ||
+ node->kind == CTLV_SCHEMA_SINT ||
+ node->kind == CTLV_SCHEMA_ENUM;
+ bool length = node->kind == CTLV_SCHEMA_STRING ||
+ node->kind == CTLV_SCHEMA_BINARY;
+ bool blob = node->kind == CTLV_SCHEMA_BLOB;
+ bool flags = node->kind == CTLV_SCHEMA_FLAGS;
+ bool children = node->fields_count || node->optional_fields_count;
+ bool reenters = node_has(src, CTLV_T_SCHEMA_NODE_REENTERS);
+ bool unit = node_has(src, CTLV_T_SCHEMA_NODE_UNIT);
+ bool scale = node_has(src, CTLV_T_SCHEMA_NODE_SCALE);
+
+ if ((!depth && (node->type != CTLV_TYPE_ROOT ||
+ node->kind != CTLV_SCHEMA_RECORD ||
+ node->max_occurrences != 1)) ||
+ (depth && node->type == CTLV_TYPE_ROOT))
+ return -EPROTO;
+
+ /* A later library may understand an appended schema kind. */
+ if (node->kind > CTLV_SCHEMA_BLOB)
+ return 0;
+ if (node_has(src, CTLV_T_SCHEMA_NODE_MIN) != range ||
+ node_has(src, CTLV_T_SCHEMA_NODE_MAX) != range ||
+ node_has(src, CTLV_T_SCHEMA_NODE_MIN_LENGTH) != length ||
+ node_has(src, CTLV_T_SCHEMA_NODE_MAX_LENGTH) != length ||
+ node_has(src, CTLV_T_SCHEMA_NODE_FLAGS_MASK) != flags ||
+ node_has(src, CTLV_T_SCHEMA_NODE_BLOB_DIRECTION) != blob ||
+ node_has(src, CTLV_T_SCHEMA_NODE_BLOB_BACKINGS) != blob ||
+ node_has(src, CTLV_T_SCHEMA_NODE_BLOB_MAX_LENGTH) != blob ||
+ node_has(src, CTLV_T_SCHEMA_NODE_BLOB_ALIGNMENT) != blob)
+ return -EPROTO;
+ if (unit != scale ||
+ (unit && !node->unit) ||
+ (unit && node->kind != CTLV_SCHEMA_UINT &&
+ node->kind != CTLV_SCHEMA_SINT))
+ return -EPROTO;
+ if ((children && node->kind != CTLV_SCHEMA_RECORD) ||
+ (reenters && (node->kind != CTLV_SCHEMA_RECORD || children)))
+ return -EPROTO;
+
+ if (range) {
+ if (node->kind == CTLV_SCHEMA_SINT)
+ return (__s64)node->min <= (__s64)node->max ?
+ 0 : -EPROTO;
+ /* An enum extension point the device's family leaves unfilled. */
+ if (node->kind == CTLV_SCHEMA_ENUM && node->min == 1 &&
+ !node->max)
+ return 0;
+ return node->min <= node->max ? 0 : -EPROTO;
+ }
+ if (length && node->min_length > node->max_length)
+ return -EPROTO;
+ if (flags && !node->flags_mask)
+ return -EPROTO;
+ if (blob && (!node->blob_backings || !node->blob_max_length ||
+ !node->blob_alignment ||
+ node->blob_alignment & (node->blob_alignment - 1)))
+ return -EPROTO;
+ return 0;
+}
+
+static bool node_is_a_node(const struct ctlv_hdr *hdr)
+{
+ return hdr->type == CTLV_T_SCHEMA_NODE_FIELDS ||
+ hdr->type == CTLV_T_SCHEMA_NODE_OPTIONAL_FIELDS;
+}
+
+static int count_nodes(const struct ctlv_hdr *src, unsigned int depth,
+ unsigned int *count)
+{
+ const struct ctlv_hdr *child;
+ int ret;
+
+ if (depth > CTLV_RECORD_DEPTH_MAX)
+ return -ELOOP;
+ if (*count == UINT_MAX)
+ return -EOVERFLOW;
+ (*count)++;
+ ctlv_for_each(child, src) {
+ if (!node_is_a_node(child))
+ continue;
+ if (!ctlv_hdr_is_record(child))
+ return -EPROTO;
+ ret = count_nodes(child, depth + 1, count);
+ if (ret)
+ return ret;
+ }
+ return 0;
+}
+
+static int fill(struct build *build, const struct ctlv_hdr *src,
+ struct ctlv_node_schema *dst, unsigned int depth)
+{
+ struct ctlv_node_schema *required, *optional;
+ const struct ctlv_hdr *child;
+ unsigned int required_count, optional_count, i;
+ __u64 value;
+ int ret;
+
+ if (depth > CTLV_RECORD_DEPTH_MAX)
+ return -ELOOP;
+ build->path[depth] = dst;
+ if (!node_u64(src, CTLV_T_SCHEMA_NODE_TYPE, &dst->type))
+ return -EPROTO;
+ if (!node_u64(src, CTLV_T_SCHEMA_NODE_KIND, &value))
+ return -EPROTO;
+ if (value > UINT32_MAX)
+ return -EPROTO;
+ dst->kind = value;
+ if (!node_u64(src, CTLV_T_SCHEMA_NODE_MAX_OCCURRENCES,
+ &dst->max_occurrences))
+ return -EPROTO;
+
+ node_opt(src, CTLV_T_SCHEMA_NODE_MIN, CTLV_NODE_SCHEMA_HAS_MIN,
+ &dst->min, &dst->present);
+ node_opt(src, CTLV_T_SCHEMA_NODE_MAX, CTLV_NODE_SCHEMA_HAS_MAX,
+ &dst->max, &dst->present);
+ node_opt(src, CTLV_T_SCHEMA_NODE_MIN_LENGTH,
+ CTLV_NODE_SCHEMA_HAS_MIN_LENGTH, &dst->min_length,
+ &dst->present);
+ node_opt(src, CTLV_T_SCHEMA_NODE_MAX_LENGTH,
+ CTLV_NODE_SCHEMA_HAS_MAX_LENGTH, &dst->max_length,
+ &dst->present);
+ node_opt(src, CTLV_T_SCHEMA_NODE_FLAGS_MASK,
+ CTLV_NODE_SCHEMA_HAS_FLAGS_MASK, &dst->flags_mask,
+ &dst->present);
+ node_opt(src, CTLV_T_SCHEMA_NODE_REENTERS,
+ CTLV_NODE_SCHEMA_HAS_REENTERS, &dst->reenters, &dst->present);
+ if (node_u64(src, CTLV_T_SCHEMA_NODE_UNIT, &value)) {
+ if (value > UINT32_MAX)
+ return -EPROTO;
+ dst->unit = value;
+ dst->present |= CTLV_NODE_SCHEMA_HAS_UNIT;
+ }
+ /* A scale only ever accompanies a unit, and may be negative. */
+ if (node_u64(src, CTLV_T_SCHEMA_NODE_SCALE, &value)) {
+ dst->scale = (__s64)value;
+ dst->present |= CTLV_NODE_SCHEMA_HAS_SCALE;
+ }
+ if (node_u64(src, CTLV_T_SCHEMA_NODE_BLOB_DIRECTION, &value)) {
+ if (value > UINT32_MAX)
+ return -EPROTO;
+ dst->blob_direction = value;
+ dst->present |= CTLV_NODE_SCHEMA_HAS_BLOB;
+ node_u64(src, CTLV_T_SCHEMA_NODE_BLOB_BACKINGS,
+ &dst->blob_backings);
+ node_u64(src, CTLV_T_SCHEMA_NODE_BLOB_MAX_LENGTH,
+ &dst->blob_max_length);
+ node_u64(src, CTLV_T_SCHEMA_NODE_BLOB_ALIGNMENT,
+ &dst->blob_alignment);
+ }
+
+ required_count = ctlv_hdr_count(src, CTLV_T_SCHEMA_NODE_FIELDS);
+ optional_count = ctlv_hdr_count(src,
+ CTLV_T_SCHEMA_NODE_OPTIONAL_FIELDS);
+ if (build->used > build->node_schemas_count ||
+ required_count > build->node_schemas_count - build->used ||
+ optional_count > build->node_schemas_count - build->used - required_count)
+ return -EPROTO;
+ ret = node_children_check(src, required_count + optional_count);
+ if (ret)
+ return ret;
+ required = &build->node_schemas[build->used];
+ build->used += required_count;
+ optional = &build->node_schemas[build->used];
+ build->used += optional_count;
+ dst->fields = required;
+ dst->fields_count = required_count;
+ dst->optional_fields = optional;
+ dst->optional_fields_count = optional_count;
+
+ i = 0;
+ ctlv_for_each_type(child, src, CTLV_T_SCHEMA_NODE_FIELDS) {
+ ret = fill(build, child, &required[i++], depth + 1);
+ if (ret)
+ return ret;
+ }
+ i = 0;
+ ctlv_for_each_type(child, src, CTLV_T_SCHEMA_NODE_OPTIONAL_FIELDS) {
+ ret = fill(build, child, &optional[i++], depth + 1);
+ if (ret)
+ return ret;
+ }
+ if ((dst->present & CTLV_NODE_SCHEMA_HAS_REENTERS) &&
+ (!dst->reenters || dst->reenters > depth ||
+ required_count || optional_count ||
+ build->path[depth - dst->reenters]->kind !=
+ CTLV_SCHEMA_RECORD))
+ return -EPROTO;
+ return node_kind_check(src, dst, depth);
+}
+
+/* The two trees of one op. Either may be absent. */
+int ctlv_schema_parse(const struct ctlv_hdr *root, __u32 len,
+ struct ctlv_schema **sp)
+{
+ const struct ctlv_hdr *request, *reply;
+ struct ctlv_schema *schema;
+ struct build build = {};
+ int ret;
+
+ ret = ctlv_bootstrap_validate(root, len, CTLV_OP_OP_SCHEMA_GET);
+ if (ret)
+ return ret;
+ request = ctlv_hdr_find(root, CTLV_T_OP_SCHEMA_GET_REQUEST);
+ reply = ctlv_hdr_find(root, CTLV_T_OP_SCHEMA_GET_REPLY);
+
+ schema = calloc(1, sizeof(*schema));
+ if (!schema)
+ return -ENOMEM;
+ if (request) {
+ ret = count_nodes(request, 0, &build.node_schemas_count);
+ if (ret)
+ goto err;
+ }
+ if (reply) {
+ ret = count_nodes(reply, 0, &build.node_schemas_count);
+ if (ret)
+ goto err;
+ }
+ if (build.node_schemas_count) {
+ build.node_schemas = calloc(build.node_schemas_count, sizeof(*build.node_schemas));
+ if (!build.node_schemas) {
+ free(schema);
+ return -ENOMEM;
+ }
+ }
+ schema->node_schemas = build.node_schemas;
+ schema->node_schemas_count = build.node_schemas_count;
+
+ if (request) {
+ schema->request = &build.node_schemas[build.used++];
+ ret = fill(&build, request,
+ (struct ctlv_node_schema *)schema->request, 0);
+ if (ret)
+ goto err;
+ }
+ if (reply) {
+ schema->reply = &build.node_schemas[build.used++];
+ ret = fill(&build, reply,
+ (struct ctlv_node_schema *)schema->reply, 0);
+ if (ret)
+ goto err;
+ }
+ if (build.used != build.node_schemas_count) {
+ ret = -EPROTO;
+ goto err;
+ }
+ *sp = schema;
+ return 0;
+err:
+ ctlv_schema_free(schema);
+ return ret;
+}
+
+void ctlv_schema_free(struct ctlv_schema *schema)
+{
+ if (!schema)
+ return;
+ free(schema->node_schemas);
+ free(schema);
+}
+
+const struct ctlv_node_schema *ctlv_schema_request(const struct ctlv_schema *s)
+{
+ return s->request;
+}
+
+const struct ctlv_node_schema *ctlv_schema_reply(const struct ctlv_schema *s)
+{
+ return s->reply;
+}
+
+/*
+ * Whether two descriptions of one op agree. The first difference is reported,
+ * with the path to it, a drift in one place having many consequences.
+ */
+
+#define DIFFER(_member, _expected, _actual) do { \
+ if ((_expected) != (_actual)) { \
+ snprintf(diff, len, "%s: %s %llu != %llu", path, \
+ _member, (unsigned long long)(_expected), \
+ (unsigned long long)(_actual)); \
+ return false; \
+ } \
+} while (0)
+
+static bool node_equal(const struct ctlv_node_schema *expected,
+ const struct ctlv_node_schema *actual, char *path,
+ size_t path_len, size_t at, char *diff, size_t len)
+{
+ unsigned int i;
+
+ DIFFER("type", expected->type, actual->type);
+ DIFFER("kind", expected->kind, actual->kind);
+ DIFFER("max-occurrences", expected->max_occurrences,
+ actual->max_occurrences);
+ DIFFER("present", expected->present, actual->present);
+ DIFFER("min", expected->min, actual->min);
+ DIFFER("max", expected->max, actual->max);
+ DIFFER("min-length", expected->min_length, actual->min_length);
+ DIFFER("max-length", expected->max_length, actual->max_length);
+ DIFFER("flags-mask", expected->flags_mask, actual->flags_mask);
+ DIFFER("unit", expected->unit, actual->unit);
+ DIFFER("scale", expected->scale, actual->scale);
+ DIFFER("blob-direction", expected->blob_direction,
+ actual->blob_direction);
+ DIFFER("blob-backings", expected->blob_backings,
+ actual->blob_backings);
+ DIFFER("blob-max-length", expected->blob_max_length,
+ actual->blob_max_length);
+ DIFFER("blob-alignment", expected->blob_alignment,
+ actual->blob_alignment);
+ DIFFER("reenters", expected->reenters, actual->reenters);
+ DIFFER("fields", expected->fields_count, actual->fields_count);
+ DIFFER("optional-fields", expected->optional_fields_count,
+ actual->optional_fields_count);
+
+ for (i = 0; i < expected->fields_count; i++) {
+ size_t used = text_append(path, path_len, at, ".fields[%u]", i);
+
+ if (!node_equal(&expected->fields[i], &actual->fields[i], path,
+ path_len, used, diff, len))
+ return false;
+ }
+ for (i = 0; i < expected->optional_fields_count; i++) {
+ size_t used = text_append(path, path_len, at,
+ ".optional-fields[%u]", i);
+
+ if (!node_equal(&expected->optional_fields[i],
+ &actual->optional_fields[i], path, path_len,
+ used, diff, len))
+ return false;
+ }
+ path[at] = '\0';
+ return true;
+}
+
+bool ctlv_schema_equal(const struct ctlv_node_schema *expected,
+ const struct ctlv_node_schema *actual, char *diff,
+ size_t len)
+{
+ char path[512] = "root";
+
+ if (!expected || !actual) {
+ if (expected == actual)
+ return true;
+ snprintf(diff, len, "one tree is absent: %s expected, %s got",
+ expected ? "present" : "absent",
+ actual ? "present" : "absent");
+ return false;
+ }
+ return node_equal(expected, actual, path, sizeof(path), strlen(path),
+ diff, len);
+}
+
+static const char *const kinds[] = {
+ [CTLV_SCHEMA_UINT] = "uint",
+ [CTLV_SCHEMA_SINT] = "sint",
+ [CTLV_SCHEMA_BOOL] = "bool",
+ [CTLV_SCHEMA_STRING] = "string",
+ [CTLV_SCHEMA_BINARY] = "binary",
+ [CTLV_SCHEMA_ENUM] = "enum",
+ [CTLV_SCHEMA_FLAGS] = "flags",
+ [CTLV_SCHEMA_RECORD] = "record",
+ [CTLV_SCHEMA_BLOB] = "blob",
+};
+
+/* One line for one node, for a tool that has no names to print. */
+void ctlv_schema_render(const struct ctlv_node_schema *node,
+ unsigned int indent, char *buf, size_t len)
+{
+ const char *kind = "?";
+ size_t used;
+
+ if (node->kind < CTLV_ARRAY_SIZE(kinds) && kinds[node->kind])
+ kind = kinds[node->kind];
+
+ used = text_append(buf, len, 0, "%*s", indent * 2, "");
+ if (node->type == CTLV_TYPE_ROOT)
+ used = text_append(buf, len, used, "root %s", kind);
+ else
+ used = text_append(buf, len, used, "field %#llx %s",
+ (unsigned long long)node->type, kind);
+
+ if (node->max_occurrences == 1)
+ ;
+ else if (!node->max_occurrences)
+ used = text_append(buf, len, used, " xN");
+ else
+ used = text_append(buf, len, used, " x%llu",
+ (unsigned long long)node->max_occurrences);
+
+ /* One pair of members serves sint and uint, read by the schema kind. */
+ if (node->present & (CTLV_NODE_SCHEMA_HAS_MIN | CTLV_NODE_SCHEMA_HAS_MAX)) {
+ if (node->kind == CTLV_SCHEMA_SINT)
+ used = text_append(buf, len, used, " [%lld, %lld]",
+ (long long)node->min,
+ (long long)node->max);
+ else
+ used = text_append(buf, len, used, " [%llu, %llu]",
+ (unsigned long long)node->min,
+ (unsigned long long)node->max);
+ }
+ if (node->present & CTLV_NODE_SCHEMA_HAS_MAX_LENGTH)
+ used = text_append(buf, len, used, " len [%llu, %llu]",
+ (unsigned long long)node->min_length,
+ (unsigned long long)node->max_length);
+ if (node->present & CTLV_NODE_SCHEMA_HAS_FLAGS_MASK)
+ used = text_append(buf, len, used, " mask %#llx",
+ (unsigned long long)node->flags_mask);
+ if (node->present & CTLV_NODE_SCHEMA_HAS_UNIT)
+ used = text_append(buf, len, used, " unit %u scale %lld",
+ node->unit, (long long)node->scale);
+ if (node->present & CTLV_NODE_SCHEMA_HAS_BLOB)
+ used = text_append(buf, len, used,
+ " blob dir %u backings %#llx max %llu align %llu",
+ node->blob_direction,
+ (unsigned long long)node->blob_backings,
+ (unsigned long long)node->blob_max_length,
+ (unsigned long long)node->blob_alignment);
+ if (node->present & CTLV_NODE_SCHEMA_HAS_REENTERS)
+ text_append(buf, len, used, " reenters %llu",
+ (unsigned long long)node->reenters);
+}
diff --git a/tools/ctlv/libctlv/spec.c b/tools/ctlv/libctlv/spec.c
new file mode 100644
index 000000000000..194966e29e2d
--- /dev/null
+++ b/tools/ctlv/libctlv/spec.c
@@ -0,0 +1,186 @@
+// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+/*
+ * Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES
+ *
+ * Runtime device/specification conformance.
+ */
+
+#include <stdio.h>
+
+#include "internal.h"
+
+/* Where a difference is written, long enough for a path and two numbers. */
+#define DIFF_LEN 512
+
+#define UNEQUAL(_what, _expected, _actual) do { \
+ if ((_expected) != (_actual)) { \
+ snprintf(diff, len, "op %#llx: %s %llu != %llu", \
+ (unsigned long long)spec->op_id, _what, \
+ (unsigned long long)(_expected), \
+ (unsigned long long)(_actual)); \
+ return -ENOMSG; \
+ } \
+} while (0)
+
+const struct ctlv_op_spec *ctlv_spec_find(const struct ctlv_family_spec *spec,
+ __u64 op_id)
+{
+ unsigned int i;
+
+ for (; spec; spec = spec->parent)
+ for (i = 0; i < spec->ops_count; i++)
+ if (spec->ops[i].op_id == op_id)
+ return &spec->ops[i];
+ return NULL;
+}
+
+/* What an ops dump reports about an op, against what the family says it is. */
+static int info_check(const struct ctlv_op_spec *spec,
+ const struct ctlv_op_info *info, char *diff, size_t len)
+{
+ UNEQUAL("type", spec->type, info->type);
+ UNEQUAL("fd-access", spec->fd_access, info->fd_access);
+ UNEQUAL("availability", spec->availability, info->availability);
+ UNEQUAL("request-max-len", spec->request_max_len,
+ info->request_max_len);
+ UNEQUAL("action-reply-max-len", spec->action_reply_max_len,
+ info->action_reply_max_len);
+ UNEQUAL("query-reply-min-len", spec->query_reply_min_len,
+ info->query_reply_min_len);
+ UNEQUAL("query-reply-suggested-len", spec->query_reply_suggested_len,
+ info->query_reply_suggested_len);
+ UNEQUAL("error-info-max-len", spec->error_info_max_len,
+ info->error_info_max_len);
+ return 0;
+}
+
+/* Both trees of one op, as the device describes them. */
+static int schema_check(struct ctlv_dev *dev, const struct ctlv_op_spec *spec,
+ char *diff, size_t len, struct ctlv_error *error)
+{
+ char where[DIFF_LEN];
+ struct ctlv_schema *schema;
+ int ret;
+
+ ret = ctlv_op_schema_get(dev, spec->op_id, &schema, error);
+ if (ret)
+ return ret;
+
+ ret = 0;
+ if (!ctlv_schema_equal(spec->request, ctlv_schema_request(schema),
+ where, sizeof(where))) {
+ snprintf(diff, len, "op %#llx request: %s",
+ (unsigned long long)spec->op_id, where);
+ ret = -ENOMSG;
+ } else if (!ctlv_schema_equal(spec->reply, ctlv_schema_reply(schema),
+ where, sizeof(where))) {
+ snprintf(diff, len, "op %#llx reply: %s",
+ (unsigned long long)spec->op_id, where);
+ ret = -ENOMSG;
+ }
+ ctlv_schema_free(schema);
+ return ret;
+}
+
+/* Whether the chain @spec describes is the chain @dev resolved. */
+static int chain_check(struct ctlv_dev *dev,
+ const struct ctlv_family_spec *spec, char *diff,
+ size_t len)
+{
+ const struct ctlv_family_spec *walk;
+ unsigned int level;
+
+ if (strcmp(spec->name, ctlv_dev_family(dev))) {
+ snprintf(diff, len, "%s describes a %s, not a %s",
+ spec->name, spec->name, ctlv_dev_family(dev));
+ return -EINVAL;
+ }
+
+ level = ctlv_chain_len(dev);
+ for (walk = spec; walk; walk = walk->parent) {
+ if (!level--) {
+ snprintf(diff, len,
+ "the device's chain never reaches %s",
+ walk->name);
+ return -ENOMSG;
+ }
+ if (walk->family_id != ctlv_chain_id(dev, level) ||
+ strcmp(walk->name, ctlv_chain_name(dev, level))) {
+ snprintf(diff, len,
+ "chain level %u: %s (%u) != %s (%u)", level,
+ walk->name, walk->family_id,
+ ctlv_chain_name(dev, level),
+ ctlv_chain_id(dev, level));
+ return -ENOMSG;
+ }
+ }
+ if (level) {
+ snprintf(diff, len, "%s describes %u families too few",
+ spec->name, level);
+ return -ENOMSG;
+ }
+ return 0;
+}
+
+static int required_ops_check(const struct ctlv_family_spec *spec,
+ const struct ctlv_ops *ops, char *diff,
+ size_t len)
+{
+ const struct ctlv_family_spec *family;
+ unsigned int i;
+
+ /* Parsing already rejected duplicate published IDs. */
+ for (family = spec; family; family = family->parent) {
+ for (i = 0; i < family->ops_count; i++) {
+ const struct ctlv_op_spec *op = &family->ops[i];
+
+ if (op->optional || ctlv_ops_find(ops, op->op_id))
+ continue;
+ snprintf(diff, len, "op %#llx: mandatory, but not published",
+ (unsigned long long)op->op_id);
+ return -ENOMSG;
+ }
+ }
+ return 0;
+}
+
+int ctlv_spec_check(struct ctlv_dev *dev, const struct ctlv_family_spec *spec,
+ char *diff, size_t len, struct ctlv_error *error)
+{
+ struct ctlv_ops *ops;
+ unsigned int i;
+ int ret;
+
+ if (diff && len)
+ *diff = '\0';
+ ret = chain_check(dev, spec, diff, len);
+ if (ret)
+ return ret;
+ ret = ctlv_ops_get(dev, &ops, error);
+ if (ret)
+ return ret;
+
+ for (i = 0; i < ops->ops_count; i++) {
+ const struct ctlv_op_info *info = &ops->ops[i];
+ const struct ctlv_op_spec *described;
+
+ described = ctlv_spec_find(spec, info->op_id);
+ if (!described) {
+ snprintf(diff, len,
+ "op %#llx: published, and %s has no such op",
+ (unsigned long long)info->op_id, spec->name);
+ ret = -ENOMSG;
+ break;
+ }
+ ret = info_check(described, info, diff, len);
+ if (ret)
+ break;
+ ret = schema_check(dev, described, diff, len, error);
+ if (ret)
+ break;
+ }
+ if (!ret)
+ ret = required_ops_check(spec, ops, diff, len);
+ ctlv_ops_free(ops);
+ return ret;
+}
diff --git a/tools/ctlv/libctlv/transport.c b/tools/ctlv/libctlv/transport.c
new file mode 100644
index 000000000000..647bab085c9e
--- /dev/null
+++ b/tools/ctlv/libctlv/transport.c
@@ -0,0 +1,572 @@
+// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+/*
+ * Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES
+ *
+ * Operation execution, query reassembly, and error decoding.
+ */
+
+#include <stdio.h>
+#include <sys/ioctl.h>
+
+#include "internal.h"
+
+/* Where a query's buffer starts when nothing said how long its answer is. */
+#define QUERY_BUF_LEN 0x10000
+
+void ctlv_error_clear(struct ctlv_error *error)
+{
+ if (error)
+ memset(error, 0, sizeof(*error));
+}
+
+void ctlv_error_set(struct ctlv_error *error, int err)
+{
+ if (!error)
+ return;
+ memset(error, 0, sizeof(*error));
+ error->err = err;
+}
+
+static void error_u64(struct ctlv_error *error, const struct ctlv_hdr *root,
+ __u64 type, __u32 bit, __u64 *value)
+{
+ const struct ctlv_hdr *hdr = ctlv_hdr_find(root, type);
+
+ if (!hdr || ctlv_get_u64(hdr, value))
+ return;
+ error->present |= bit;
+}
+
+/*
+ * What a device wrote about the failure. A buffer holding nothing, or nothing
+ * this reader can parse, leaves the errno as the whole of the report.
+ */
+void ctlv_error_parse(struct ctlv_error *error, int err, const void *buf,
+ __u32 len)
+{
+ const struct ctlv_hdr *root = buf;
+ const struct ctlv_hdr *hdr;
+ __u64 value;
+
+ if (!error)
+ return;
+ memset(error, 0, sizeof(*error));
+ error->err = err;
+ if (!buf || ctlv_msg_check(buf, len))
+ return;
+
+ hdr = ctlv_hdr_find(root, CTLV_T_ERROR_INFO_REASON);
+ if (hdr && !ctlv_get_enum(hdr, 1,
+ CTLV_VALIDATION_REASON_STALE_GENERATION,
+ &value, NULL)) {
+ error->reason = value;
+ error->present |= CTLV_ERROR_HAS_REASON;
+ }
+ hdr = ctlv_hdr_find(root, CTLV_T_ERROR_INFO_MESSAGE);
+ if (hdr) {
+ const char *message;
+
+ if (!ctlv_get_str(hdr, &message)) {
+ snprintf(error->message, sizeof(error->message), "%s",
+ message);
+ error->present |= CTLV_ERROR_HAS_MESSAGE;
+ }
+ }
+ error_u64(error, root, CTLV_T_ERROR_INFO_BAD_TLV_OFFSET,
+ CTLV_ERROR_HAS_BAD_TLV_OFFSET, &error->bad_tlv_offset);
+ error_u64(error, root, CTLV_T_ERROR_INFO_RECORD_OFFSET,
+ CTLV_ERROR_HAS_RECORD_OFFSET, &error->record_offset);
+ error_u64(error, root, CTLV_T_ERROR_INFO_MISSING_TYPE,
+ CTLV_ERROR_HAS_MISSING_TYPE, &error->missing_type);
+ error_u64(error, root, CTLV_T_ERROR_INFO_EXPECTED_GENERATION,
+ CTLV_ERROR_HAS_EXPECTED_GENERATION,
+ &error->expected_generation);
+ error_u64(error, root, CTLV_T_ERROR_INFO_CURRENT_GENERATION,
+ CTLV_ERROR_HAS_CURRENT_GENERATION,
+ &error->current_generation);
+ error_u64(error, root, CTLV_T_ERROR_INFO_REQUIRED_LENGTH,
+ CTLV_ERROR_HAS_REQUIRED_LENGTH, &error->required_length);
+}
+
+static const char *const reasons[] = {
+ [CTLV_VALIDATION_REASON_MALFORMED_MESSAGE] = "malformed message",
+ [CTLV_VALIDATION_REASON_UNKNOWN_TYPE] = "unknown type",
+ [CTLV_VALIDATION_REASON_UNKNOWN_FLAG] = "unknown flag",
+ [CTLV_VALIDATION_REASON_RECORD_TOO_DEEP] = "record too deep",
+ [CTLV_VALIDATION_REASON_MISSING_FIELD] = "missing field",
+ [CTLV_VALIDATION_REASON_TOO_MANY_OCCURRENCES] = "too many occurrences",
+ [CTLV_VALIDATION_REASON_VALUE_OUT_OF_RANGE] = "value out of range",
+ [CTLV_VALIDATION_REASON_BAD_LENGTH] = "bad length",
+ [CTLV_VALIDATION_REASON_BAD_STRING] = "bad string",
+ [CTLV_VALIDATION_REASON_BAD_BLOB_DESCRIPTOR] = "bad blob descriptor",
+ [CTLV_VALIDATION_REASON_STALE_GENERATION] = "stale generation",
+ [CTLV_VALIDATION_REASON_NONCONTIGUOUS_TYPE] = "noncontiguous type",
+};
+
+const char *ctlv_error_str(const struct ctlv_error *error, char *buf,
+ size_t len)
+{
+ const char *reason = NULL;
+ size_t used;
+
+ if (error->present & CTLV_ERROR_HAS_REASON &&
+ error->reason < CTLV_ARRAY_SIZE(reasons))
+ reason = reasons[error->reason];
+ used = snprintf(buf, len, "%s", strerror(-error->err));
+ if (reason && used < len)
+ used += snprintf(buf + used, len - used, ": %s", reason);
+ if (error->present & CTLV_ERROR_HAS_MESSAGE && used < len)
+ used += snprintf(buf + used, len - used, ": %s",
+ error->message);
+ if (error->present & CTLV_ERROR_HAS_MISSING_TYPE && used < len)
+ used += snprintf(buf + used, len - used, " (type %#llx)",
+ (unsigned long long)error->missing_type);
+ if (error->present & CTLV_ERROR_HAS_REQUIRED_LENGTH && used < len)
+ used += snprintf(buf + used, len - used, " (needs %llu bytes)",
+ (unsigned long long)error->required_length);
+ if (error->present & CTLV_ERROR_HAS_BAD_TLV_OFFSET && used < len)
+ snprintf(buf + used, len - used, " (at offset %llu)",
+ (unsigned long long)error->bad_tlv_offset);
+ return buf;
+}
+
+static int exec(struct ctlv_dev *dev, struct ctlv_op_exec *arg)
+{
+ if (ioctl(dev->fd, CTLV_IOC_OP_EXEC, arg))
+ return -errno;
+ return 0;
+}
+
+static int reply_new(__u32 capacity, struct ctlv_reply **replyp)
+{
+ struct ctlv_reply *reply;
+ int ret;
+
+ reply = calloc(1, sizeof(*reply));
+ if (!reply)
+ return -ENOMEM;
+ ret = ctlv_msg_new_max(capacity, CTLV_REPLY_LEN_MAX, &reply->msg);
+ if (ret) {
+ free(reply);
+ return ret;
+ }
+ *replyp = reply;
+ return 0;
+}
+
+void ctlv_reply_free(struct ctlv_reply *reply)
+{
+ if (!reply)
+ return;
+ ctlv_msg_free(reply->msg);
+ free(reply);
+}
+
+const struct ctlv_hdr *ctlv_reply_root(const struct ctlv_reply *reply)
+{
+ return ctlv_msg_root(reply->msg);
+}
+
+__u64 ctlv_reply_generation(const struct ctlv_reply *reply)
+{
+ return reply->generation;
+}
+
+unsigned int ctlv_reply_fragments(const struct ctlv_reply *reply)
+{
+ return reply->fragments;
+}
+
+static const struct ctlv_hdr empty_request = {
+ .len = sizeof(struct ctlv_hdr),
+ .flags = CTLV_HDR_F_RECORD,
+ .type = CTLV_TYPE_ROOT,
+};
+
+/* The request bytes an op takes, an empty root when it takes no fields. */
+static int request_of(struct ctlv_msg *request, __u64 *ptr, __u32 *len)
+{
+ int ret;
+
+ if (!request) {
+ *ptr = (__u64)(uintptr_t)&empty_request;
+ *len = sizeof(empty_request);
+ return 0;
+ }
+ ret = ctlv_msg_done(request);
+ if (ret)
+ return ret;
+ *ptr = (__u64)(uintptr_t)request->buf;
+ *len = request->len;
+ return 0;
+}
+
+int ctlv_action_sized(struct ctlv_dev *dev, __u64 op_id,
+ struct ctlv_msg *request, __u32 reply_len,
+ struct ctlv_reply **replyp, struct ctlv_error *error)
+{
+ struct ctlv_op_exec arg = { .size = sizeof(arg), .op_id = op_id };
+ struct ctlv_reply *reply = NULL;
+ int ret;
+
+ ctlv_error_clear(error);
+ ret = request_of(request, &arg.request, &arg.request_len);
+ if (ret) {
+ ctlv_error_set(error, ret);
+ return ret;
+ }
+
+ /* A zeroed root says nothing was written, a real one being longer. */
+ if (reply_len < sizeof(struct ctlv_hdr))
+ reply_len = sizeof(struct ctlv_hdr);
+ ret = reply_new(reply_len, &reply);
+ if (ret) {
+ ctlv_error_set(error, ret);
+ return ret;
+ }
+ memset(reply->msg->buf, 0, sizeof(struct ctlv_hdr));
+ arg.reply = (__u64)(uintptr_t)reply->msg->buf;
+ arg.reply_len = reply_len;
+
+ ret = exec(dev, &arg);
+ if (ret) {
+ ctlv_error_parse(error, ret, reply->msg->buf, reply_len);
+ ctlv_reply_free(reply);
+ return ret;
+ }
+
+ reply->fragments = 1;
+ reply->msg->len = ctlv_msg_root(reply->msg)->len;
+ if (!reply->msg->len) {
+ /* Acknowledged, and there is nothing to read. */
+ ctlv_reply_free(reply);
+ reply = NULL;
+ } else {
+ ret = ctlv_msg_check(reply->msg->buf, reply_len);
+ if (ret) {
+ ctlv_error_set(error, ret);
+ ctlv_reply_free(reply);
+ return ret;
+ }
+ }
+ if (replyp)
+ *replyp = reply;
+ else
+ ctlv_reply_free(reply);
+ return 0;
+}
+
+static unsigned int query_cursor_slot(__u64 cursor, unsigned int capacity)
+{
+ cursor ^= cursor >> 30;
+ cursor *= 0xbf58476d1ce4e5b9ULL;
+ cursor ^= cursor >> 27;
+ cursor *= 0x94d049bb133111ebULL;
+ cursor ^= cursor >> 31;
+ return cursor & (capacity - 1);
+}
+
+static void query_cursor_insert(__u64 *slots, unsigned int capacity,
+ __u64 cursor)
+{
+ unsigned int slot = query_cursor_slot(cursor, capacity);
+
+ while (slots[slot])
+ slot = (slot + 1) & (capacity - 1);
+ slots[slot] = cursor;
+}
+
+static int query_guard_grow(struct ctlv_query_guard *guard)
+{
+ unsigned int capacity = guard->cursors_capacity ?
+ guard->cursors_capacity * 2 : 64;
+ __u64 *cursor;
+ unsigned int i;
+
+ cursor = calloc(capacity, sizeof(*cursor));
+ if (!cursor)
+ return -ENOMEM;
+ for (i = 0; i < guard->cursors_capacity; i++)
+ if (guard->cursors[i])
+ query_cursor_insert(cursor, capacity, guard->cursors[i]);
+ free(guard->cursors);
+ guard->cursors = cursor;
+ guard->cursors_capacity = capacity;
+ return 0;
+}
+
+int ctlv_query_guard_next(struct ctlv_query_guard *guard, __u64 cursor,
+ bool progress)
+{
+ unsigned int slot;
+ int ret;
+
+ if (!cursor)
+ return 0;
+ if (!progress)
+ return -EPROTO;
+ if (guard->cursors_count >= CTLV_QUERY_FRAGMENTS_MAX - 1)
+ return -E2BIG;
+ if (guard->cursors_count * 2 >= guard->cursors_capacity) {
+ ret = query_guard_grow(guard);
+ if (ret)
+ return ret;
+ }
+ slot = query_cursor_slot(cursor, guard->cursors_capacity);
+ while (guard->cursors[slot]) {
+ if (guard->cursors[slot] == cursor)
+ return -ELOOP;
+ slot = (slot + 1) & (guard->cursors_capacity - 1);
+ }
+ guard->cursors[slot] = cursor;
+ guard->cursors_count++;
+ return 0;
+}
+
+void ctlv_query_guard_free(struct ctlv_query_guard *guard)
+{
+ free(guard->cursors);
+ *guard = (struct ctlv_query_guard){};
+}
+
+/* No continuation marker may hide below a record that was completed. */
+static int frag_closed(const struct ctlv_hdr *record)
+{
+ const struct ctlv_hdr *child;
+ int ret;
+
+ ctlv_for_each(child, record) {
+ if (child->flags & CTLV_HDR_F_FRAG_MORE)
+ return -EPROTO;
+ if (!ctlv_hdr_is_record(child))
+ continue;
+ ret = frag_closed(child);
+ if (ret)
+ return ret;
+ }
+ return 0;
+}
+
+/* Remember the one last-child path this fragment leaves open. */
+static int frag_path_build(const struct ctlv_hdr *record,
+ struct ctlv_frag_path *path)
+{
+ const struct ctlv_hdr *child, *last = NULL;
+ int ret;
+
+ ctlv_for_each(child, record)
+ last = child;
+ ctlv_for_each(child, record) {
+ if (child == last)
+ break;
+ if (child->flags & CTLV_HDR_F_FRAG_MORE)
+ return -EPROTO;
+ if (!ctlv_hdr_is_record(child))
+ continue;
+ ret = frag_closed(child);
+ if (ret)
+ return ret;
+ }
+ if (!last)
+ return 0;
+ if (!(last->flags & CTLV_HDR_F_FRAG_MORE))
+ return ctlv_hdr_is_record(last) ? frag_closed(last) : 0;
+ if (!ctlv_hdr_is_record(last))
+ return -EPROTO;
+ if (path->depth == CTLV_ARRAY_SIZE(path->type))
+ return -ELOOP;
+ path->type[path->depth++] = last->type;
+ return frag_path_build(last, path);
+}
+
+static int frag_path_read(const struct ctlv_hdr *root,
+ struct ctlv_frag_path *path)
+{
+ *path = (struct ctlv_frag_path){};
+ if (!(root->flags & CTLV_HDR_F_FRAG_MORE))
+ return frag_closed(root);
+ return frag_path_build(root, path);
+}
+
+/* The next fragment begins by repeating every record still open. */
+static int frag_path_repeat(const struct ctlv_hdr *root,
+ const struct ctlv_frag_path *path)
+{
+ const struct ctlv_hdr *record = root;
+ const struct ctlv_hdr *child;
+ unsigned int i;
+
+ for (i = 0; i < path->depth; i++) {
+ child = ctlv_hdr_first(record);
+ if (!child || !ctlv_hdr_is_record(child) ||
+ child->type != path->type[i])
+ return -EPROTO;
+ record = child;
+ }
+ return 0;
+}
+
+/*
+ * One fragment, appended to the reply being assembled. @skip levels of
+ * re-emitted path are descended into rather than appended, being still open in
+ * @out; a record left marked open stays open for the next fragment.
+ */
+static int splice(struct ctlv_msg *out, const struct ctlv_hdr *record,
+ unsigned int skip, bool *progress)
+{
+ const struct ctlv_hdr *child;
+ bool first = true;
+ int ret;
+
+ ctlv_for_each(child, record) {
+ bool more = child->flags & CTLV_HDR_F_FRAG_MORE;
+
+ if (first && skip) {
+ ret = splice(out, child, skip - 1, progress);
+ if (!ret && !more)
+ ret = ctlv_record_end(out);
+ } else if (more) {
+ ret = ctlv_record_start(out, child->type);
+ if (!ret)
+ ret = splice(out, child, 0, progress);
+ } else {
+ /* Complete: nothing inside waits for a continuation. */
+ ret = ctlv_msg_copy(out, child);
+ if (!ret)
+ *progress = true;
+ }
+ if (ret)
+ return ret;
+ first = false;
+ }
+ return 0;
+}
+
+int ctlv_frag_splice(struct ctlv_msg *out, const struct ctlv_hdr *root,
+ struct ctlv_frag_path *open, bool *progress)
+{
+ struct ctlv_frag_path next;
+ bool made_progress = false;
+ int ret;
+
+ if (progress)
+ *progress = false;
+ ret = frag_path_read(root, &next);
+ if (ret)
+ return ret;
+ ret = frag_path_repeat(root, open);
+ if (ret)
+ return ret;
+ ret = splice(out, root, open->depth, &made_progress);
+ if (!ret) {
+ *open = next;
+ if (progress)
+ *progress = made_progress;
+ }
+ return ret;
+}
+
+int ctlv_query_sized(struct ctlv_dev *dev, __u64 op_id,
+ struct ctlv_msg *request, __u32 buf_len,
+ struct ctlv_reply **replyp, struct ctlv_error *error)
+{
+ struct ctlv_op_exec arg = { .size = sizeof(arg), .op_id = op_id };
+ struct ctlv_query_guard guard = {};
+ struct ctlv_reply *reply = NULL;
+ struct ctlv_frag_path open = {};
+ void *buf;
+ int ret;
+
+ ctlv_error_clear(error);
+ if (!buf_len)
+ buf_len = QUERY_BUF_LEN;
+ if (buf_len < sizeof(struct ctlv_hdr))
+ buf_len = sizeof(struct ctlv_hdr);
+ ret = request_of(request, &arg.request, &arg.request_len);
+ if (ret)
+ goto out_err;
+
+ buf = malloc(buf_len);
+ if (!buf) {
+ ret = -ENOMEM;
+ goto out_err;
+ }
+ arg.reply = (__u64)(uintptr_t)buf;
+ arg.reply_len = buf_len;
+
+ ret = reply_new(buf_len, &reply);
+ if (ret)
+ goto out_free_buf;
+
+ do {
+ const struct ctlv_hdr *root = buf;
+ bool progress;
+
+ /* A failure that writes nothing leaves the root zeroed. */
+ memset(buf, 0, sizeof(struct ctlv_hdr));
+ ret = exec(dev, &arg);
+ if (ret) {
+ ctlv_error_parse(error, ret, buf, buf_len);
+ goto out_free_reply;
+ }
+ ret = ctlv_msg_check(buf, buf_len);
+ if (ret)
+ goto out_set_err;
+
+ /*
+ * The root's mark and the cursor both say whether there is
+ * more, and disagreeing is not something to reassemble.
+ */
+ if (!(root->flags & CTLV_HDR_F_FRAG_MORE) != !arg.cursor) {
+ ret = -EPROTO;
+ goto out_set_err;
+ }
+
+ ret = ctlv_frag_splice(reply->msg, root, &open, &progress);
+ if (ret)
+ goto out_set_err;
+ ret = ctlv_query_guard_next(&guard, arg.cursor, progress);
+ if (ret)
+ goto out_set_err;
+ if (!arg.cursor && open.depth) {
+ ret = -EPROTO;
+ goto out_set_err;
+ }
+
+ reply->fragments++;
+ reply->generation = arg.generation;
+ } while (arg.cursor);
+
+ /* The answer having ended, the root is the only record still open. */
+ if (reply->msg->depth != 1) {
+ ret = -EPROTO;
+ goto out_set_err;
+ }
+ ret = ctlv_msg_done(reply->msg);
+ if (ret)
+ goto out_set_err;
+ /* The reassembled tree is a message in its own right, or nothing is. */
+ ret = ctlv_msg_check(reply->msg->buf, reply->msg->len);
+ if (ret)
+ goto out_set_err;
+
+ ctlv_query_guard_free(&guard);
+ free(buf);
+ if (replyp)
+ *replyp = reply;
+ else
+ ctlv_reply_free(reply);
+ return 0;
+
+out_set_err:
+ ctlv_error_set(error, ret);
+out_free_reply:
+ ctlv_reply_free(reply);
+out_free_buf:
+ ctlv_query_guard_free(&guard);
+ free(buf);
+ return ret;
+out_err:
+ ctlv_error_set(error, ret);
+ return ret;
+}
--
2.56.0
^ permalink raw reply related [flat|nested] 24+ messages in thread
* [PATCH RFC v2 15/15] tools/ctlv: Add ctlvctl
2026-11-13 15:54 [PATCH RFC v2 00/15] ctlv: Add a schema-driven TLV uAPI for character devices Jiri Pirko
` (13 preceding siblings ...)
2026-11-13 15:55 ` [PATCH RFC v2 14/15] tools/ctlv: Add the userspace library Jiri Pirko
@ 2026-11-13 15:55 ` Jiri Pirko
2026-11-13 16:19 ` sashiko-bot
14 siblings, 1 reply; 24+ messages in thread
From: Jiri Pirko @ 2026-11-13 15:55 UTC (permalink / raw)
To: linux-coco
Cc: linux-kernel, linux-api, x86, kvm, linux-doc, jgg, david.quigley,
lukas, ksinyuk, rodrigo.vivi, cedric.xing, sameo, drdeeglaze,
James.Bottomley, aik, jpiotrowski, qinkun, pgonda, jic23,
alistair.francis, ankita, suzuki.poulose, kas, rick.p.edgecombe,
elena.reshetova, dave.hansen, sathyanarayanan.kuppuswamy,
peter.fang, jarkko, zohar, yeoreum.yun, aneesh.kumar, dave.jiang,
nik.borisov, sergii.dmytruk
From: Jiri Pirko <jiri@nvidia.com>
Add a command-line client that discovers and queries any CTLV device,
with optional names from a specification directory.
Examples:
Root-family discovery on any CTLV device:
ctlvctl --spec-dir Documentation/ctlv/specs \
exec ctlv-example family-get
The operations the same device publishes:
ctlvctl --spec-dir Documentation/ctlv/specs ops ctlv-example
Assisted-by: Cursor: GPT-5.6 Sol
Assisted-by: Cursor: Claude Opus 5.5
Signed-off-by: Jiri Pirko <jiri@nvidia.com>
---
Documentation/driver-api/ctlv.rst | 29 +-
Documentation/userspace-api/ctlv.rst | 80 +-
tools/Makefile | 9 +-
tools/ctlv/Makefile | 23 +-
tools/ctlv/ctlvctl/.gitignore | 4 +
tools/ctlv/ctlvctl/Makefile | 64 +
tools/ctlv/ctlvctl/ctlvctl.c | 2787 +++++++++++++++++++++++++
tools/ctlv/ctlvctl/spec.c | 672 ++++++
tools/ctlv/ctlvctl/spec.h | 60 +
tools/ctlv/ctlvgen/cli.py | 47 +-
tools/ctlv/ctlvgen/lib/dump.py | 161 ++
tools/ctlv/ctlvgen/lib/gen_doc.py | 684 ++++++
tools/ctlv/ctlvgen/lib/gen_runtime.py | 66 +
13 files changed, 4661 insertions(+), 25 deletions(-)
create mode 100644 tools/ctlv/ctlvctl/.gitignore
create mode 100644 tools/ctlv/ctlvctl/Makefile
create mode 100644 tools/ctlv/ctlvctl/ctlvctl.c
create mode 100644 tools/ctlv/ctlvctl/spec.c
create mode 100644 tools/ctlv/ctlvctl/spec.h
create mode 100644 tools/ctlv/ctlvgen/lib/dump.py
create mode 100644 tools/ctlv/ctlvgen/lib/gen_doc.py
create mode 100644 tools/ctlv/ctlvgen/lib/gen_runtime.py
diff --git a/Documentation/driver-api/ctlv.rst b/Documentation/driver-api/ctlv.rst
index f6a4f9632e3a..e8e6ecdf6ab6 100644
--- a/Documentation/driver-api/ctlv.rst
+++ b/Documentation/driver-api/ctlv.rst
@@ -235,6 +235,9 @@ checks the same without writing anything.
Never edit a generated file; change the YAML or the generator and regenerate.
+``make -C tools/ctlv docs`` writes optional RST reference pages to
+``Documentation/output/ctlv/families``.
+
Create and destroy a device
===========================
@@ -684,20 +687,24 @@ The node is of the foo driver's own class, ``foo_class``, registered as
that ``ctlv_family_inherits()`` says is not ``ctlv-example`` or a descendant,
then calls ``ctlv_init()``.
-Reporting an error
-------------------
+Tests and examples
+==================
+
+``Documentation/ctlv/specs/ctlv-test.yaml``
+ Every schema kind, records, actions, queries, blobs, availability, hooks,
+ and an extension point.
+
+``drivers/ctlv/tests/ctlv_test.c``
+ The ``ctlv-test`` family implementation.
-A callback returns a negative errno, and that is what the ioctl returns. Where
-CTLV itself refuses a message it also writes error information into the
-caller's reply buffer, naming the reason and the offset of the offending
-TLV. A family implementation does not build that; it adds its own errno
-and its reply.
+``drivers/ctlv/tests/ctlv_test_child.c``
+ A separately built inheriting family and extension-point fill.
-Configuration
-=============
+``lib/ctlv/tests/*_kunit.c``
+ Unit tests of the engine and of its nodes.
-``CONFIG_CTLV``
- CTLV. A family implementation selects it.
+``tools/testing/selftests/ctlv/``
+ Raw-message, generated-binding, allocation-failure, and mutation selftests.
Kernel API
==========
diff --git a/Documentation/userspace-api/ctlv.rst b/Documentation/userspace-api/ctlv.rst
index 8a8ade33b490..dc590f334545 100644
--- a/Documentation/userspace-api/ctlv.rst
+++ b/Documentation/userspace-api/ctlv.rst
@@ -58,8 +58,8 @@ Each part of the design serves one of the needs:
so that a caller whose answer changed between the parts gets ``ESTALE``
rather than a mix of two states.
- Introspection. Every device answers ``family-get``, ``op-get`` and
- ``op-schema-get``, so a program built without a family still learns what
- the device accepts.
+ ``op-schema-get``, so a program built without a family, such as ``ctlvctl``,
+ still learns what the device accepts.
`Compatibility`_ collects the rules for growing a family.
@@ -133,6 +133,11 @@ specification: ``index`` must be at most 4095 and ``label`` at most 63 bytes.
ctlv_example_item_set_label_put(req, "ab");
ctlv_example_item_set(dev, req, &error);
+and so does ``ctlvctl``, given the family's specification::
+
+ ctlvctl --spec-dir Documentation/ctlv/specs \
+ exec /dev/ctlv-example item-set index=3 label=ab
+
The sections below define each piece: the device, the message format, the
ioctl, queries, blobs and introspection, and then the rules of compatibility.
@@ -564,7 +569,7 @@ cut. The first call passes ``generation`` and ``cursor`` zero. On return:
To continue, the caller repeats the call with the same ``op_id`` and request,
the returned ``cursor`` and the returned ``generation``, and reassembles by
-concatenating the children of each still-open record.
+concatenating the children of each still-open record, which ``libctlv`` does.
One answer of three items, cut by a buffer that holds two::
@@ -591,7 +596,8 @@ it, so a reader knows whether to append or to open a new record beside it.
Every continuing fragment adds at least one complete logical TLV beyond its
repeated path, and a cursor value cannot recur within one logical reply.
-A reader must reject either violation.
+``libctlv`` rejects either violation. It also limits one query to 65,536
+fragments and a 64 MiB reassembled reply.
A continuation whose generation no longer matches fails with ``-ESTALE``, and
the error information carries the expected and the current generation.
@@ -709,3 +715,69 @@ removing or renumbering anything, changing a field's kind or an operation's
type, access or availability, narrowing a bound or an occurrence limit,
making a field that the kernel writes optional, and, in any message,
making a field required or adding a required one.
+
+Library and tools
+=================
+
+``tools/ctlv/libctlv`` is a small C library for the mechanical parts: opening
+a device, checking the chain, building requests, parsing replies, reassembling
+fragmented queries and building blob descriptors. Per-family
+bindings are generated beside it, turning operations into typed functions.
+``ctlv_open()`` finds a device by the family its uevent announces, whichever
+subsystem it is in. ``ctlv_open_fd()`` takes a file the caller opened, through
+another subsystem's interface if need be, and ``ctlv_close()`` leaves it open.
+
+``tools/ctlv/ctlvctl`` defaults to runtime introspection alone. Compiled
+against no family, it can list devices, print the operations one publishes and
+their schemas, and execute its actions and queries alike. A device is
+named as ``list`` prints it, relative to ``/dev``, or by a path::
+
+ ctlvctl list
+ ctlvctl ops /dev/ctlv-example
+ ctlvctl schema /dev/ctlv-example 0x0001000000000008
+ ctlvctl exec /dev/ctlv-example 0x0001000000000001
+ ctlvctl exec /dev/ctlv-example 0x0001000000000002 0x0001800200000001=7
+
+Passing the directory of the kernel's family specifications adds the names
+that are deliberately absent from introspection. This is the ``specs``
+directory of a CTLV documentation bundle; its registry and meta-schemas are in
+the parent directory::
+
+ ctlvctl --spec-dir Documentation/ctlv/specs ops /dev/ctlv-example
+ ctlvctl --spec-dir Documentation/ctlv/specs \
+ schema /dev/ctlv-example series-get
+ ctlvctl --spec-dir Documentation/ctlv/specs \
+ exec /dev/ctlv-example probe-get token=7
+ ctlvctl --spec-dir Documentation/ctlv/specs \
+ exec /dev/ctlv-example echo-reply \
+ --json '{"u":17,"mode":"busy","count":[3,5]}'
+ ctlvctl --spec-dir Documentation/ctlv/specs \
+ exec /dev/ctlv-example transfer --json \
+ '{"source":{"file":"in"},"target":{"file":"out","length":64}}'
+
+``CTLV_SPEC_DIR`` avoids repeating the directory. Known items are printed by
+name without IDs; ``-n`` adds them, while an item unknown to the selected
+specifications stays numeric. ``schema --json-template`` prints a minimal JSON
+request. ``--output-json`` emits one object for an action or query. A JSON
+request may be inline, read from ``@file``, or read from standard input with
+``--json -``; it cannot be combined with assignments. Specification and JSON
+support are built when json-c is available.
+
+In JSON, a blob is ``{"file": "<path>"}``, passed as ``va`` memory. An input
+blob holds the file. An output blob is written to the file whole, so give it
+the ``"length"`` the operation writes; without one it starts at a byte and
+grows to each ``required-length`` the operation fails with, the operation
+being retried. An ``inout`` blob is read from the file and written back.
+
+In the directory, ``ctlvctl`` reads a family's names from ``<family>.json``
+when it is there, and otherwise has ``ctlvgen``, with its Python dependencies,
+resolve ``<family>.yaml``. Building ``ctlvctl`` writes the ``.json`` of each of
+the tree's families beside it, so a machine with neither the source tree nor
+Python needs only that directory and json-c. ``ctlvgen --spec <family>.yaml
+--runtime-json`` prints the file for a family outside the tree.
+
+``make -C tools/ctlv programs`` builds both in the source tree. After ``make
+O=<dir> headers``, ``make O=<dir> tools/ctlv`` builds ``ctlvctl`` into
+``<dir>/tools/ctlv/ctlvctl`` against that tree's headers, for another
+architecture with ``CROSS_COMPILE`` or ``LLVM`` as the kernel takes them;
+``PKG_CONFIG`` names the ``pkg-config`` that finds the target's json-c.
diff --git a/tools/Makefile b/tools/Makefile
index cb40961a740f..d8d0003b16a5 100644
--- a/tools/Makefile
+++ b/tools/Makefile
@@ -13,6 +13,7 @@ help:
@echo ' bpf - misc BPF tools'
@echo ' counter - counter tools'
@echo ' cpupower - a tool for all things x86 CPU power'
+ @echo ' ctlv - CTLV library and ctlvctl, after make headers'
@echo ' debugging - tools for debugging'
@echo ' dma - tools for DMA mapping'
@echo ' firewire - the userspace part of nosy, an IEEE-1394 traffic sniffer'
@@ -70,6 +71,9 @@ acpi: FORCE
cpupower: FORCE
$(call descend,power/$@)
+ctlv: FORCE
+ $(call descend,ctlv/ctlvctl)
+
counter dma firewire hv guest bootconfig spi usb virtio mm bpf iio gpio objtool leds wmi firmware debugging tracing: FORCE
$(call descend,$@)
@@ -179,6 +183,9 @@ acpi_clean:
cpupower_clean:
$(call descend,power/cpupower,clean)
+ctlv_clean:
+ $(call descend,ctlv/ctlvctl,clean)
+
counter_clean dma_clean hv_clean firewire_clean bootconfig_clean spi_clean usb_clean virtio_clean mm_clean wmi_clean bpf_clean iio_clean gpio_clean objtool_clean leds_clean firmware_clean debugging_clean tracing_clean:
$(call descend,$(@:_clean=),clean)
@@ -225,7 +232,7 @@ build_clean:
ynl_clean:
$(call descend,net/$(@:_clean=),clean)
-clean: acpi_clean counter_clean cpupower_clean dma_clean hv_clean firewire_clean \
+clean: acpi_clean counter_clean cpupower_clean ctlv_clean dma_clean hv_clean firewire_clean \
perf_clean selftests_clean turbostat_clean bootconfig_clean spi_clean usb_clean virtio_clean \
mm_clean bpf_clean iio_clean x86_energy_perf_policy_clean tmon_clean \
freefall_clean build_clean libbpf_clean libsubcmd_clean \
diff --git a/tools/ctlv/Makefile b/tools/ctlv/Makefile
index 881fae9127c7..83c0ece690e8 100644
--- a/tools/ctlv/Makefile
+++ b/tools/ctlv/Makefile
@@ -2,6 +2,10 @@
KDIR = ../..
DOCDIR = $(KDIR)/Documentation/ctlv
+SPECDIR = $(DOCDIR)/specs
+SPECS = $(wildcard $(SPECDIR)/*.yaml)
+TOOL = ./ctlvgen/cli.py
+DOC_OUTPUT ?= $(KDIR)/Documentation/output/ctlv/families
# Writes nothing. Fails on an invalid specification, and on any generated file
# or ABI snapshot that regen would change.
@@ -30,16 +34,29 @@ lint:
python3 -m pyflakes $(PYSRC)
python3 -m mypy $(PYSRC)
-# The C half of this directory: the userspace library, which is not generated
-# and not Python, so it is its own target rather than part of the checks above.
+docs:
+ @mkdir -p $(DOC_OUTPUT)
+ @for spec in $(SPECS) ; do \
+ name=$$(basename $$spec .yaml) ; \
+ echo -e "\tDOC $$name.rst" ; \
+ $(TOOL) --spec $$spec --mode doc --rst \
+ -o $(DOC_OUTPUT)/$$name.rst || exit 1 ; \
+ done
+
+# The C half of this directory: the userspace library, and the tool that drives
+# a device it was never compiled against. Neither is generated and neither is
+# Python, so they are their own target rather than part of the checks above.
programs:
$(MAKE) -C libctlv
+ $(MAKE) -C ctlvctl
clean:
rm -rf ctlvgen/__pycache__
rm -rf ctlvgen/lib/__pycache__
rm -rf ctlvgen.egg-info
rm -rf build
+ rm -rf $(DOC_OUTPUT)
$(MAKE) -C libctlv clean
+ $(MAKE) -C ctlvctl clean
-.PHONY: all regen lint programs clean
+.PHONY: all regen lint docs programs clean
diff --git a/tools/ctlv/ctlvctl/.gitignore b/tools/ctlv/ctlvctl/.gitignore
new file mode 100644
index 000000000000..dcade74af00e
--- /dev/null
+++ b/tools/ctlv/ctlvctl/.gitignore
@@ -0,0 +1,4 @@
+# SPDX-License-Identifier: GPL-2.0-only
+/*.json
+/ctlvctl
+/libctlv/
diff --git a/tools/ctlv/ctlvctl/Makefile b/tools/ctlv/ctlvctl/Makefile
new file mode 100644
index 000000000000..dcfca390d402
--- /dev/null
+++ b/tools/ctlv/ctlvctl/Makefile
@@ -0,0 +1,64 @@
+# SPDX-License-Identifier: GPL-2.0
+
+KDIR ?= ../../..
+LIBDIR = ../libctlv
+
+CFLAGS ?= -O2 -g
+
+include ../../scripts/Makefile.include
+
+KHDR_INCLUDES ?= -isystem $(or $(ABSOLUTE_O),$(KDIR))/usr/include
+
+CFLAGS += -Wall -Wextra -Wno-unused-parameter -Wno-missing-field-initializers
+CFLAGS += -D_GNU_SOURCE -I$(LIBDIR) $(KHDR_INCLUDES)
+
+# The one that knows the target's json-c, when cross-compiling.
+PKG_CONFIG ?= $(CROSS_COMPILE)pkg-config
+
+JSON_C_CFLAGS := $(shell $(PKG_CONFIG) --atleast-version=0.14 json-c \
+ 2>/dev/null && $(PKG_CONFIG) --cflags json-c)
+JSON_C_LIBS := $(shell $(PKG_CONFIG) --atleast-version=0.14 json-c \
+ 2>/dev/null && $(PKG_CONFIG) --libs json-c)
+ifneq ($(JSON_C_LIBS),)
+CFLAGS += -DHAVE_JSON_C $(JSON_C_CFLAGS)
+LDLIBS += $(JSON_C_LIBS)
+endif
+
+# With a build directory, libctlv is built for ctlvctl inside it.
+LIBCTLV_OUTPUT ?= $(if $(OUTPUT),$(OUTPUT)libctlv/)
+LIB = $(or $(LIBCTLV_OUTPUT),$(LIBDIR)/)libctlv.a
+OBJS = $(OUTPUT)ctlvctl.o $(OUTPUT)spec.o
+
+# What --spec-dir reads where there is neither the source tree nor Python:
+# the names of each of the tree's families and of the families it inherits.
+DOCDIR = $(KDIR)/Documentation/ctlv
+SPECDIR = $(DOCDIR)/specs
+SPECS = $(wildcard $(SPECDIR)/*.yaml)
+CTLVGEN = ../ctlvgen/cli.py
+CTLVGEN_INPUTS = $(SPECS) $(wildcard $(DOCDIR)/*.yaml) \
+ $(wildcard ../ctlvgen/*.py ../ctlvgen/lib/*.py)
+MANIFESTS = $(patsubst $(SPECDIR)/%.yaml,$(OUTPUT)%.json,$(SPECS))
+
+all: $(OUTPUT)ctlvctl $(MANIFESTS)
+
+$(OUTPUT)ctlvctl: $(OBJS) $(LIB)
+ $(QUIET_LINK)$(CC) $(CFLAGS) -o $@ $(OBJS) $(LIB) $(LDLIBS)
+
+$(OUTPUT)%.o: %.c spec.h
+ $(QUIET_CC)$(CC) $(CFLAGS) -c -o $@ $<
+
+$(OUTPUT)%.json: $(SPECDIR)/%.yaml $(CTLVGEN_INPUTS)
+ $(QUIET_GEN)$(CTLVGEN) --spec $< --runtime-json > $@
+
+$(LIB): FORCE
+ $(if $(LIBCTLV_OUTPUT),$(Q)mkdir -p $(LIBCTLV_OUTPUT))
+ $(Q)$(MAKE) -C $(LIBDIR) OUTPUT=$(LIBCTLV_OUTPUT)
+
+clean:
+ rm -f $(OUTPUT)ctlvctl $(OBJS) $(MANIFESTS)
+ $(if $(LIBCTLV_OUTPUT),rm -rf $(LIBCTLV_OUTPUT))
+
+FORCE:
+
+.PHONY: all clean FORCE
+.DELETE_ON_ERROR:
diff --git a/tools/ctlv/ctlvctl/ctlvctl.c b/tools/ctlv/ctlvctl/ctlvctl.c
new file mode 100644
index 000000000000..ef9bb8536c47
--- /dev/null
+++ b/tools/ctlv/ctlvctl/ctlvctl.c
@@ -0,0 +1,2787 @@
+// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+/*
+ * Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES
+ *
+ * Family-independent CTLV command-line client.
+ */
+
+#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <getopt.h>
+#include <limits.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#ifdef HAVE_JSON_C
+#include <json.h>
+#endif
+
+#include <ctlv.h>
+
+#include "spec.h"
+
+#define DEV_DIR "/dev"
+#define PATH_LEN 1024
+#define LINE_LEN 512
+#define ARRAY_SIZE(array) (sizeof(array) / sizeof((array)[0]))
+
+/*
+ * How deep a request built from the command line may go: the root and 32
+ * levels below it, as deep as any device goes today.
+ */
+#define PATH_DEPTH 33
+
+/* Where a request buffer starts; it grows as fields are added. */
+#define REQUEST_HINT 512
+
+static const char *prog = "ctlvctl";
+
+static struct {
+ const char *spec_dir;
+ bool show_ids;
+ bool output_json;
+} options;
+
+#ifdef HAVE_JSON_C
+static int json_template_print(const struct ctlv_node_schema *root,
+ const struct ctlvctl_spec *spec);
+static void json_add_id(struct json_object *object, const char *key, __u64 id);
+static void json_add_op(struct json_object *object,
+ const struct ctlvctl_spec *spec, __u64 op_id);
+static int json_print(struct json_object *object);
+static int json_schema_print(const struct ctlv_dev *dev,
+ const struct ctlvctl_spec *spec, __u64 op_id,
+ const struct ctlv_schema *schema);
+
+static void json_add_string(struct json_object *object, const char *key,
+ const char *value)
+{
+ json_object_object_add(object, key, json_object_new_string(value));
+}
+
+static void json_add_u64(struct json_object *object, const char *key,
+ __u64 value)
+{
+ json_object_object_add(object, key, json_object_new_uint64(value));
+}
+
+static void json_add_s64(struct json_object *object, const char *key,
+ __s64 value)
+{
+ json_object_object_add(object, key, json_object_new_int64(value));
+}
+
+static void json_add_int(struct json_object *object, const char *key, int value)
+{
+ json_object_object_add(object, key, json_object_new_int(value));
+}
+
+static void json_add_bool(struct json_object *object, const char *key,
+ bool value)
+{
+ json_object_object_add(object, key, json_object_new_boolean(value));
+}
+
+static int json_array_add_string(struct json_object *array, const char *value)
+{
+ struct json_object *string = json_object_new_string(value);
+
+ if (!string)
+ return -ENOMEM;
+ if (json_object_array_add(array, string)) {
+ json_object_put(string);
+ return -ENOMEM;
+ }
+ return 0;
+}
+#endif
+
+static void help(FILE *out)
+{
+ fprintf(out,
+ "usage: %s [options] <command> [arguments]\n"
+ "\n"
+ " list every CTLV device and its family chain\n"
+ " ops <device> every op the device publishes\n"
+ " schema [--json-template] <device> <op>\n"
+ " the request and reply ABI of one op\n"
+ " exec [-l len] <device> <op> [a=v]\n"
+ " run an op, reassembling a query's answer\n"
+ "\n"
+ " --spec-dir DIR use DIR/<family>.json or .yaml\n"
+ " -n, --show-ids show IDs beside specification names\n"
+ " --output-json print structured JSON\n"
+ " --json TEXT|@FILE|- build a request from JSON\n"
+ "\n"
+ "In --json, a blob is {\"file\": PATH}. Input is read from the file, and\n"
+ "output written to it: \"length\" bytes, or as many as the op needs.\n"
+ "\n"
+ "-l names the reply buffer, which for a query decides how many fragments\n"
+ "the answer arrives in; the device reports its smallest usable size in ops.\n"
+ "\n"
+ "With --spec-dir, operation names and top-level scalar names may be\n"
+ "used. Otherwise an operation goes by its ID and a field by its type.\n"
+ "A dotted numeric path enters a record:\n"
+ "\n"
+ " %s exec ctlv-test0 0x3000000000009 0x3800900000001=7\n"
+ " %s exec ctlv-test0 0x3000000000006 0x3000100000001=1 \\\n"
+ " 0x3000100000009.0x3000110000001=3\n",
+ prog, prog, prog);
+}
+
+static int parse_u64(const char *text, __u64 *value)
+{
+ char *end;
+
+ if (!*text || *text == '-' || isspace((unsigned char)*text))
+ return -EINVAL;
+ errno = 0;
+ *value = strtoull(text, &end, 0);
+ if (errno || end == text || *end)
+ return -EINVAL;
+ return 0;
+}
+
+static int parse_s64(const char *text, __s64 *value)
+{
+ char *end;
+
+ errno = 0;
+ *value = strtoll(text, &end, 0);
+ if (errno || end == text || *end)
+ return -EINVAL;
+ return 0;
+}
+
+static int dev_open(const char *name, int flags, struct ctlv_dev **devp)
+{
+ char path[PATH_LEN];
+ int ret;
+
+ /* A listed name is under /dev even when it has a directory of its own. */
+ if (name[0] == '/' || name[0] == '.')
+ snprintf(path, sizeof(path), "%s", name);
+ else
+ snprintf(path, sizeof(path), "%s/%s", DEV_DIR, name);
+ ret = ctlv_open_path(path, flags, devp);
+ if (ret)
+ fprintf(stderr, "%s: %s: %s\n", prog, path, strerror(-ret));
+ return ret;
+}
+
+static int spec_load(const struct ctlv_dev *dev, struct ctlvctl_spec **spec)
+{
+ if (!options.spec_dir) {
+ *spec = NULL;
+ return 0;
+ }
+ return ctlvctl_spec_load(prog, options.spec_dir, dev, spec);
+}
+
+static int op_resolve(const struct ctlvctl_spec *spec, const char *text,
+ __u64 *op_id)
+{
+ const struct ctlvctl_op_name *op;
+
+ if (!parse_u64(text, op_id))
+ return 0;
+ op = ctlvctl_spec_op_name(spec, text);
+ if (op) {
+ *op_id = op->id;
+ return 0;
+ }
+ if (spec)
+ fprintf(stderr, "%s: no operation \"%s\"\n", prog, text);
+ else
+ fprintf(stderr, "%s: %s: not an op ID\n", prog, text);
+ return -ENOENT;
+}
+
+static void chain_print(const struct ctlv_dev *dev)
+{
+ unsigned int i;
+
+ for (i = 0; i < ctlv_chain_len(dev); i++)
+ printf("%s%s", i ? "/" : "", ctlv_chain_name(dev, i));
+}
+
+static int cmd_list(int argc, char **argv)
+{
+#ifdef HAVE_JSON_C
+ struct json_object *json = NULL;
+#endif
+ struct ctlv_dev_list list;
+ unsigned int i;
+ int ret;
+
+ if (argc) {
+ help(stderr);
+ return -EINVAL;
+ }
+ ret = ctlv_enumerate(&list);
+ if (ret) {
+ fprintf(stderr, "%s: no CTLV devices: %s\n", prog,
+ strerror(-ret));
+ return ret;
+ }
+#ifdef HAVE_JSON_C
+ if (options.output_json) {
+ json = json_object_new_array();
+ if (!json) {
+ ctlv_dev_list_free(&list);
+ return -ENOMEM;
+ }
+ }
+#endif
+ for (i = 0; i < list.names_count; i++) {
+ struct ctlv_dev *dev;
+
+#ifdef HAVE_JSON_C
+ if (json) {
+ struct json_object *chain = json_object_new_array();
+ struct json_object *item = json_object_new_object();
+ unsigned int level;
+
+ json_add_string(item, "device", list.names[i]);
+ if (!dev_open(list.names[i], O_RDONLY, &dev)) {
+ for (level = 0; level < ctlv_chain_len(dev);
+ level++) {
+ struct json_object *family =
+ json_object_new_object();
+
+ json_add_string(family, "name",
+ ctlv_chain_name(dev,
+ level));
+ if (options.show_ids)
+ json_add_int(family, "id",
+ ctlv_chain_id(dev, level));
+ json_object_array_add(chain, family);
+ }
+ ctlv_close(dev);
+ }
+ json_object_object_add(item, "chain", chain);
+ json_object_array_add(json, item);
+ continue;
+ }
+#endif
+ printf("%s", list.names[i]);
+ /* Listing should not need the right to change a device. */
+ if (!dev_open(list.names[i], O_RDONLY, &dev)) {
+ printf("\t");
+ chain_print(dev);
+ ctlv_close(dev);
+ }
+ printf("\n");
+ }
+ ctlv_dev_list_free(&list);
+#ifdef HAVE_JSON_C
+ if (json)
+ return json_print(json);
+#endif
+ return 0;
+}
+
+static const char *type_name(__u32 type)
+{
+ switch (type) {
+ case CTLV_OP_TYPE_ACTION:
+ return "action";
+ case CTLV_OP_TYPE_QUERY:
+ return "query";
+ default:
+ return "?";
+ }
+}
+
+static const char *fd_access_name(__u32 fd_access)
+{
+ return fd_access == CTLV_OP_FD_ACCESS_READ ? "read" : "read-write";
+}
+
+static bool op_matches(const struct ctlvctl_op_name *name,
+ const struct ctlv_op_info *op)
+{
+ return name->type == op->type &&
+ name->fd_access == op->fd_access &&
+ name->availability == op->availability;
+}
+
+static int op_print(const struct ctlvctl_spec *spec,
+ const struct ctlv_op_info *op)
+{
+ const struct ctlvctl_op_name *name;
+
+ name = ctlvctl_spec_op_id(spec, op->op_id);
+ if (name && !op_matches(name, op)) {
+ fprintf(stderr,
+ "%s: specification disagrees with operation %#llx\n",
+ prog, (unsigned long long)op->op_id);
+ return -ENOMSG;
+ }
+ if (name) {
+ printf("%-20s", name->name);
+ if (options.show_ids)
+ printf("\t%#018llx",
+ (unsigned long long)op->op_id);
+ } else {
+ printf("%#018llx", (unsigned long long)op->op_id);
+ }
+ printf("\t%-6s", type_name(op->type));
+ if (op->fd_access)
+ printf("\t%s", fd_access_name(op->fd_access));
+ if (op->availability == CTLV_OP_AVAILABILITY_DYNAMIC)
+ printf("\tdynamic %s", op->disabled ? "disabled" : "enabled");
+ else if (op->availability == CTLV_OP_AVAILABILITY_FIXED)
+ printf("\tfixed");
+ if (op->request_max_len)
+ printf("\trequest %llu",
+ (unsigned long long)op->request_max_len);
+ if (op->action_reply_max_len)
+ printf("\treply %llu",
+ (unsigned long long)op->action_reply_max_len);
+ if (op->query_reply_min_len)
+ printf("\treply %llu..%llu",
+ (unsigned long long)op->query_reply_min_len,
+ (unsigned long long)op->query_reply_suggested_len);
+ printf("\n");
+ return 0;
+}
+
+static int cmd_ops(int argc, char **argv)
+{
+#ifdef HAVE_JSON_C
+ struct json_object *json = NULL;
+ struct json_object *json_ops = NULL;
+#endif
+ struct ctlvctl_spec *spec = NULL;
+ struct ctlv_error error;
+ struct ctlv_dev *dev;
+ struct ctlv_ops *ops;
+ char text[LINE_LEN];
+ unsigned int i;
+ int ret;
+
+ if (argc != 1) {
+ help(stderr);
+ return -EINVAL;
+ }
+ ret = dev_open(argv[0], O_RDONLY, &dev);
+ if (ret)
+ return ret;
+ ret = spec_load(dev, &spec);
+ if (ret)
+ goto out;
+
+ ret = ctlv_ops_get(dev, &ops, &error);
+ if (ret) {
+ fprintf(stderr, "%s: op-get: %s\n", prog,
+ ctlv_error_str(&error, text, sizeof(text)));
+ goto out;
+ }
+#ifdef HAVE_JSON_C
+ if (options.output_json) {
+ json = json_object_new_object();
+ json_ops = json_object_new_array();
+ if (!json || !json_ops) {
+ ret = -ENOMEM;
+ goto out_json;
+ }
+ json_add_string(json, "device", ctlv_dev_name(dev));
+ json_add_u64(json, "generation", ops->generation);
+ json_object_object_add(json, "ops", json_ops);
+ }
+#endif
+ if (!options.output_json) {
+ printf("# ");
+ chain_print(dev);
+ printf(", generation %llu\n",
+ (unsigned long long)ops->generation);
+ }
+ for (i = 0; i < ops->ops_count; i++) {
+#ifdef HAVE_JSON_C
+ if (json) {
+ const struct ctlvctl_op_name *name;
+ struct json_object *item;
+
+ name = ctlvctl_spec_op_id(spec, ops->ops[i].op_id);
+ if (name && !op_matches(name, &ops->ops[i])) {
+ fprintf(stderr,
+ "%s: specification disagrees with operation %#llx\n",
+ prog, (unsigned long long)
+ ops->ops[i].op_id);
+ ret = -ENOMSG;
+ break;
+ }
+ item = json_object_new_object();
+ if (!item) {
+ ret = -ENOMEM;
+ break;
+ }
+ if (name)
+ json_add_string(item, "name", name->name);
+ if (!name || options.show_ids)
+ json_add_id(item, "id", ops->ops[i].op_id);
+ json_add_string(item, "type",
+ type_name(ops->ops[i].type));
+ if (ops->ops[i].fd_access) {
+ const char *access =
+ fd_access_name(ops->ops[i].fd_access);
+
+ json_add_string(item, "fd-access",
+ access);
+ }
+ if (ops->ops[i].availability) {
+ const char *availability;
+
+ availability = ops->ops[i].availability ==
+ CTLV_OP_AVAILABILITY_DYNAMIC ?
+ "dynamic" : "fixed";
+ json_add_string(item, "availability",
+ availability);
+ if (ops->ops[i].availability ==
+ CTLV_OP_AVAILABILITY_DYNAMIC)
+ json_add_bool(item, "disabled",
+ ops->ops[i].disabled);
+ }
+ json_object_array_add(json_ops, item);
+ continue;
+ }
+#endif
+ ret = op_print(spec, &ops->ops[i]);
+ if (ret)
+ break;
+ }
+#ifdef HAVE_JSON_C
+ if (!ret && json)
+ ret = json_print(json);
+ else if (json)
+ json_object_put(json);
+ json = NULL;
+ json_ops = NULL;
+out_json:
+ if (json)
+ json_object_put(json);
+ else if (json_ops)
+ json_object_put(json_ops);
+#endif
+ ctlv_ops_free(ops);
+out:
+ ctlvctl_spec_free(spec);
+ ctlv_close(dev);
+ return ret;
+}
+
+static const struct ctlvctl_field_name *
+field_name(const struct ctlvctl_spec *spec,
+ const struct ctlv_node_schema *node)
+{
+ const struct ctlvctl_field_name *name;
+
+ if (!node)
+ return NULL;
+ name = ctlvctl_spec_field(spec, node->type);
+ if (name && name->kind != node->kind)
+ return NULL;
+ return name;
+}
+
+static int tree_names_check(const struct ctlvctl_spec *spec,
+ const struct ctlv_node_schema *node)
+{
+ const struct ctlvctl_field_name *name;
+ __u64 known = 0;
+ unsigned int i;
+ int ret;
+
+ if (!node)
+ return 0;
+ name = ctlvctl_spec_field(spec, node->type);
+ if (name && name->kind != node->kind) {
+ fprintf(stderr,
+ "%s: specification schema kind for %s disagrees with the device\n",
+ prog, name->name);
+ return -ENOMSG;
+ }
+ if (name && name->definition) {
+ for (i = 0; i < name->definition->values_count; i++) {
+ __u64 value = name->definition->values[i].value;
+
+ known |= value;
+ if (name->kind == CTLV_SCHEMA_ENUM &&
+ (!(node->present & CTLV_NODE_SCHEMA_HAS_MIN) ||
+ !(node->present & CTLV_NODE_SCHEMA_HAS_MAX) ||
+ value < node->min || value > node->max)) {
+ fprintf(stderr,
+ "%s: specification values for %s disagree with the device\n",
+ prog, name->name);
+ return -ENOMSG;
+ }
+ }
+ if (name->kind == CTLV_SCHEMA_FLAGS &&
+ (!(node->present & CTLV_NODE_SCHEMA_HAS_FLAGS_MASK) ||
+ (known & ~node->flags_mask))) {
+ fprintf(stderr,
+ "%s: specification flags for %s disagree with the device\n",
+ prog, name->name);
+ return -ENOMSG;
+ }
+ }
+ for (i = 0; i < node->fields_count; i++) {
+ ret = tree_names_check(spec, &node->fields[i]);
+ if (ret)
+ return ret;
+ }
+ for (i = 0; i < node->optional_fields_count; i++) {
+ ret = tree_names_check(spec, &node->optional_fields[i]);
+ if (ret)
+ return ret;
+ }
+ return 0;
+}
+
+static void tree_print(const struct ctlvctl_spec *spec,
+ const struct ctlv_node_schema *node, unsigned int indent,
+ int required)
+{
+ char line[LINE_LEN];
+ const struct ctlvctl_field_name *name;
+ char *detail;
+ unsigned int i;
+
+ name = field_name(spec, node);
+ ctlv_schema_render(node, 0, line, sizeof(line));
+ if (!name || node->type == CTLV_TYPE_ROOT) {
+ printf("%*s%s\n", indent * 2, "", line);
+ } else {
+ detail = strchr(line + strlen("field "), ' ');
+ printf("%*s%s", indent * 2, "", name->name);
+ if (options.show_ids)
+ printf(" (%#018llx)",
+ (unsigned long long)node->type);
+ if (required >= 0)
+ printf(" %s", required ? "required" : "optional");
+ printf("%s", detail ? detail : "");
+ if (name->definition) {
+ unsigned int j;
+
+ printf(" {");
+ for (j = 0; j < name->definition->values_count; j++) {
+ printf("%s%s", j ? ", " : "",
+ name->definition->values[j].name);
+ if (options.show_ids)
+ printf("=%#llx",
+ (unsigned long long)
+ name->definition->values[j].value);
+ }
+ printf("}");
+ }
+ printf("\n");
+ }
+ for (i = 0; i < node->fields_count; i++)
+ tree_print(spec, &node->fields[i], indent + 1, true);
+ for (i = 0; i < node->optional_fields_count; i++)
+ tree_print(spec, &node->optional_fields[i], indent + 1, false);
+}
+
+static int cmd_schema(int argc, char **argv)
+{
+ const struct ctlvctl_op_name *op_name;
+ const struct ctlv_op_info *op_info;
+ const struct ctlv_node_schema *node;
+ struct ctlvctl_spec *spec = NULL;
+ struct ctlv_schema *schema;
+ struct ctlv_ops *ops;
+ struct ctlv_error error;
+ struct ctlv_dev *dev;
+ char text[LINE_LEN];
+ __u64 op_id;
+ __u32 op_type = 0;
+ bool template = false;
+ int ret;
+
+ if (argc && !strcmp(argv[0], "--json-template")) {
+ template = true;
+ argc--;
+ argv++;
+ }
+ if (argc != 2) {
+ help(stderr);
+ return -EINVAL;
+ }
+ ret = dev_open(argv[0], O_RDONLY, &dev);
+ if (ret)
+ return ret;
+ ret = spec_load(dev, &spec);
+ if (ret)
+ goto out;
+ ret = op_resolve(spec, argv[1], &op_id);
+ if (ret)
+ goto out;
+ op_name = ctlvctl_spec_op_id(spec, op_id);
+ if (spec) {
+ ret = ctlv_ops_get(dev, &ops, &error);
+ if (ret) {
+ fprintf(stderr, "%s: op-get: %s\n", prog,
+ ctlv_error_str(&error, text, sizeof(text)));
+ goto out;
+ }
+ op_info = ctlv_ops_find(ops, op_id);
+ if (!op_info) {
+ fprintf(stderr,
+ "%s: operation is not available on %s\n",
+ prog, ctlv_dev_name(dev));
+ ret = -ENOENT;
+ } else {
+ op_type = op_info->type;
+ if (op_name && !op_matches(op_name, op_info)) {
+ fprintf(stderr,
+ "%s: specification disagrees with operation %s\n",
+ prog, op_name->name);
+ ret = -ENOMSG;
+ }
+ }
+ ctlv_ops_free(ops);
+ if (ret)
+ goto out;
+ }
+
+ ret = ctlv_op_schema_get(dev, op_id, &schema, &error);
+ if (ret) {
+ fprintf(stderr, "%s: op-schema-get: %s\n", prog,
+ ctlv_error_str(&error, text, sizeof(text)));
+ goto out;
+ }
+ ret = tree_names_check(spec, ctlv_schema_request(schema));
+ if (!ret)
+ ret = tree_names_check(spec, ctlv_schema_reply(schema));
+ if (ret)
+ goto out_schema;
+ if (template) {
+ node = ctlv_schema_request(schema);
+ if (!spec) {
+ fprintf(stderr,
+ "%s: --json-template requires --spec-dir\n", prog);
+ ret = -EINVAL;
+ } else if (!node) {
+ printf("{}\n");
+ } else {
+#ifdef HAVE_JSON_C
+ ret = json_template_print(node, spec);
+#else
+ ret = -EOPNOTSUPP;
+#endif
+ }
+ goto out_schema;
+ }
+#ifdef HAVE_JSON_C
+ if (options.output_json) {
+ ret = json_schema_print(dev, spec, op_id, schema);
+ goto out_schema;
+ }
+#endif
+ if (op_name) {
+ printf("%s (%s", op_name->name, type_name(op_name->type));
+ if (options.show_ids)
+ printf(", %#018llx", (unsigned long long)op_id);
+ printf(")\n");
+ } else if (spec) {
+ printf("%#018llx (%s)\n", (unsigned long long)op_id,
+ type_name(op_type));
+ }
+ node = ctlv_schema_request(schema);
+ if (node) {
+ printf("request:\n");
+ tree_print(spec, node, 1, -1);
+ }
+ node = ctlv_schema_reply(schema);
+ if (node) {
+ printf("reply:\n");
+ tree_print(spec, node, 1, -1);
+ }
+out_schema:
+ ctlv_schema_free(schema);
+out:
+ ctlvctl_spec_free(spec);
+ ctlv_close(dev);
+ return ret;
+}
+
+/*
+ * An assignment names a field by its type and a dotted path enters a record.
+ * What a value means is decided by the schema kind the device reports.
+ */
+
+/* The member of @parent for @type, following any re-entry. */
+static const struct ctlv_node_schema *
+node_member(const struct ctlv_node_schema **stack, unsigned int depth,
+ __u64 type)
+{
+ const struct ctlv_node_schema *parent = stack[depth];
+ unsigned int level = depth;
+ unsigned int i;
+
+ if (!parent)
+ return NULL;
+ while (parent->reenters) {
+ if (parent->reenters > level)
+ return NULL;
+ level -= parent->reenters;
+ parent = stack[level];
+ if (!parent)
+ return NULL;
+ }
+ for (i = 0; i < parent->fields_count; i++)
+ if (parent->fields[i].type == type)
+ return &parent->fields[i];
+ for (i = 0; i < parent->optional_fields_count; i++)
+ if (parent->optional_fields[i].type == type)
+ return &parent->optional_fields[i];
+ return NULL;
+}
+
+static int hex_digit(char c)
+{
+ if (c >= '0' && c <= '9')
+ return c - '0';
+ if (c >= 'a' && c <= 'f')
+ return c - 'a' + 10;
+ if (c >= 'A' && c <= 'F')
+ return c - 'A' + 10;
+ return -EINVAL;
+}
+
+static int put_hex(struct ctlv_msg *msg, __u64 type, const char *text)
+{
+ size_t text_len = strlen(text);
+ size_t len = text_len / 2;
+ unsigned char *bytes;
+ size_t i;
+ int ret;
+
+ if (text_len & 1)
+ return -EINVAL;
+ bytes = malloc(len ? len : 1);
+ if (!bytes)
+ return -ENOMEM;
+ for (i = 0; i < len; i++) {
+ int high = hex_digit(text[i * 2]);
+ int low = hex_digit(text[i * 2 + 1]);
+
+ if (high < 0 || low < 0) {
+ free(bytes);
+ return -EINVAL;
+ }
+ bytes[i] = high << 4 | low;
+ }
+ ret = ctlv_put_bin(msg, type, bytes, len);
+ free(bytes);
+ return ret;
+}
+
+static int flags_parse(const struct ctlvctl_field_name *field, const char *text,
+ __u64 *value)
+{
+ char *copy, *part, *walk;
+ int ret = 0;
+
+ if (!parse_u64(text, value))
+ return 0;
+ copy = strdup(text);
+ if (!copy)
+ return -ENOMEM;
+ *value = 0;
+ walk = copy;
+ while ((part = strsep(&walk, ","))) {
+ __u64 bit;
+
+ if (!*part || ctlvctl_spec_value(field, part, &bit)) {
+ ret = -EINVAL;
+ break;
+ }
+ *value |= bit;
+ }
+ free(copy);
+ return ret;
+}
+
+static int put_value(struct ctlv_msg *msg, const struct ctlv_node_schema *node,
+ const struct ctlvctl_spec *spec, const char *text)
+{
+ const struct ctlvctl_field_name *field = field_name(spec, node);
+ __u64 value;
+ __s64 signed_value;
+
+ switch (node->kind) {
+ case CTLV_SCHEMA_UINT:
+ if (parse_u64(text, &value))
+ return -EINVAL;
+ return ctlv_put_u64(msg, node->type, value);
+ case CTLV_SCHEMA_ENUM:
+ if (parse_u64(text, &value) &&
+ ctlvctl_spec_value(field, text, &value))
+ return -EINVAL;
+ return ctlv_put_u64(msg, node->type, value);
+ case CTLV_SCHEMA_FLAGS:
+ if (flags_parse(field, text, &value))
+ return -EINVAL;
+ return ctlv_put_u64(msg, node->type, value);
+ case CTLV_SCHEMA_SINT:
+ if (parse_s64(text, &signed_value))
+ return -EINVAL;
+ return ctlv_put_s64(msg, node->type, signed_value);
+ case CTLV_SCHEMA_BOOL:
+ if (!strcmp(text, "true") || !strcmp(text, "1"))
+ return ctlv_put_bool(msg, node->type, true);
+ if (!strcmp(text, "false") || !strcmp(text, "0"))
+ return ctlv_put_bool(msg, node->type, false);
+ return -EINVAL;
+ case CTLV_SCHEMA_STRING:
+ return ctlv_put_str(msg, node->type, text);
+ case CTLV_SCHEMA_BINARY:
+ if (field) {
+ if (strncmp(text, "hex:", 4))
+ return -EINVAL;
+ return put_hex(msg, node->type, text + 4);
+ }
+ return ctlv_put_bin(msg, node->type, text, strlen(text));
+ default:
+ /* A command line has no memory of the caller's to describe. */
+ return -ENOTSUP;
+ }
+}
+
+static const struct ctlv_node_schema *
+node_named(const struct ctlv_node_schema *parent,
+ const struct ctlvctl_spec *spec, const char *name, bool *ambiguous)
+{
+ const struct ctlv_node_schema *match = NULL;
+ unsigned int i;
+
+ *ambiguous = false;
+ for (i = 0; i < parent->fields_count + parent->optional_fields_count; i++) {
+ const struct ctlv_node_schema *node;
+ const struct ctlvctl_field_name *field;
+
+ if (i < parent->fields_count)
+ node = &parent->fields[i];
+ else
+ node = &parent->optional_fields[i - parent->fields_count];
+ field = field_name(spec, node);
+ if (!field)
+ continue;
+ if (!strcmp(field->name, name))
+ return node;
+ if (strcmp(field->short_name, name))
+ continue;
+ if (match)
+ *ambiguous = true;
+ match = node;
+ }
+ return *ambiguous ? NULL : match;
+}
+
+/* One path element: a type and which occurrence of it. */
+static int path_element(char *name, __u64 *type, __u64 *occurrence)
+{
+ char *at = strchr(name, '@');
+
+ *occurrence = 0;
+ if (at) {
+ *at++ = '\0';
+ if (parse_u64(at, occurrence))
+ return -EINVAL;
+ }
+ return parse_u64(name, type);
+}
+
+static int no_such_field(__u64 type)
+{
+ fprintf(stderr, "%s: %#llx: not a field of this record\n", prog,
+ (unsigned long long)type);
+ return -ENOENT;
+}
+
+static int not_a_record(__u64 type)
+{
+ fprintf(stderr, "%s: %#llx: not a record of this record\n", prog,
+ (unsigned long long)type);
+ return -ENOENT;
+}
+
+/**
+ * request_build - Turns assignments into one request.
+ * @msg: the message to append to
+ * @root: the request tree the device reported
+ * @spec: the names from --spec-dir, or NULL
+ * @argc: how many assignments
+ * @argv: the assignments, each field[@n][.field[@n]...]=value
+ *
+ * Records are opened as a path descends and closed as the next path leaves, so
+ * assignments have to be given in the order the message holds them. Two
+ * occurrences of one record are told apart by the @n suffix.
+ *
+ * Return: 0, or a negative errno naming the assignment that failed.
+ */
+static int request_build(struct ctlv_msg *msg,
+ const struct ctlv_node_schema *root,
+ const struct ctlvctl_spec *spec, int argc, char **argv)
+{
+ const struct ctlv_node_schema *stack[PATH_DEPTH] = { root };
+ __u64 path[PATH_DEPTH], occurrence[PATH_DEPTH];
+ unsigned int depth = 0;
+ int i, ret;
+
+ for (i = 0; i < argc; i++) {
+ char *text = strchr(argv[i], '=');
+ unsigned int level = 0;
+ char *name, *rest;
+
+ if (!text) {
+ fprintf(stderr, "%s: %s: not an assignment\n", prog,
+ argv[i]);
+ return -EINVAL;
+ }
+ *text++ = '\0';
+ rest = argv[i];
+
+ if (spec) {
+ const struct ctlv_node_schema *node;
+ bool ambiguous;
+
+ node = node_named(root, spec, rest, &ambiguous);
+ if (ambiguous) {
+ fprintf(stderr, "%s: %s: ambiguous field\n",
+ prog, rest);
+ return -EINVAL;
+ }
+ if (node) {
+ if (node->kind == CTLV_SCHEMA_RECORD ||
+ node->kind == CTLV_SCHEMA_BLOB) {
+ fprintf(stderr,
+ "%s: %s: use --json for this field\n",
+ prog, rest);
+ return -ENOTSUP;
+ }
+ /* A named field is the root's. */
+ while (depth) {
+ ctlv_record_end(msg);
+ depth--;
+ }
+ ret = put_value(msg, node, spec, text);
+ if (ret) {
+ fprintf(stderr, "%s: %s=%s: %s\n",
+ prog, rest, text,
+ strerror(-ret));
+ return ret;
+ }
+ continue;
+ }
+ }
+
+ while ((name = strsep(&rest, "."))) {
+ const struct ctlv_node_schema *node;
+ __u64 type, which;
+
+ if (level == PATH_DEPTH - 1)
+ return -E2BIG;
+ if (path_element(name, &type, &which)) {
+ fprintf(stderr, "%s: %s: not a type\n",
+ prog, name);
+ return -EINVAL;
+ }
+
+ /*
+ * A different record here, or the same one named again
+ * as a second occurrence, ends what came before.
+ */
+ if (level < depth && (path[level] != type ||
+ occurrence[level] != which))
+ while (depth > level) {
+ ctlv_record_end(msg);
+ depth--;
+ }
+
+ if (!rest) {
+ /* The leaf, which carries the value. */
+ while (depth > level) {
+ ctlv_record_end(msg);
+ depth--;
+ }
+ node = node_member(stack, depth, type);
+ if (!node)
+ return no_such_field(type);
+ ret = put_value(msg, node, spec, text);
+ if (ret) {
+ fprintf(stderr,
+ "%s: %#llx=%s: %s\n",
+ prog,
+ (unsigned long long)type,
+ text,
+ strerror(-ret));
+ return ret;
+ }
+ break;
+ }
+
+ if (level == depth) {
+ node = node_member(stack, depth, type);
+ if (!node)
+ return no_such_field(type);
+ if (node->kind != CTLV_SCHEMA_RECORD)
+ return not_a_record(type);
+ ret = ctlv_record_start(msg, type);
+ if (ret)
+ return ret;
+ path[depth] = type;
+ occurrence[depth] = which;
+ stack[depth + 1] = node;
+ depth++;
+ }
+ level++;
+ }
+ }
+ return ctlv_msg_err(msg);
+}
+
+static int stream_read(FILE *file, char **textp, size_t *lenp)
+{
+ size_t used = 0, size = 4096;
+ char *text;
+
+ text = malloc(size);
+ if (!text)
+ return -ENOMEM;
+ for (;;) {
+ size_t n;
+
+ if (used == size - 1) {
+ char *grown;
+
+ size *= 2;
+ grown = realloc(text, size);
+ if (!grown) {
+ free(text);
+ return -ENOMEM;
+ }
+ text = grown;
+ }
+ n = fread(text + used, 1, size - used - 1, file);
+ used += n;
+ if (n)
+ continue;
+ if (feof(file))
+ break;
+ if (errno == EINTR) {
+ clearerr(file);
+ continue;
+ }
+ free(text);
+ return -errno;
+ }
+ text[used] = '\0';
+ *textp = text;
+ *lenp = used;
+ return 0;
+}
+
+static int file_read(const char *path, char **textp, size_t *lenp)
+{
+ FILE *file;
+ int ret;
+
+ file = fopen(path, "re");
+ if (!file)
+ return -errno;
+ ret = stream_read(file, textp, lenp);
+ fclose(file);
+ return ret;
+}
+
+static int json_source_read(const char *source, char **textp, size_t *lenp)
+{
+ if (source[0] != '@' && strcmp(source, "-")) {
+ *textp = strdup(source);
+ if (!*textp)
+ return -ENOMEM;
+ *lenp = strlen(*textp);
+ return 0;
+ }
+ if (!strcmp(source, "-"))
+ return stream_read(stdin, textp, lenp);
+ return file_read(source + 1, textp, lenp);
+}
+
+/* A blob --json names by its file, and the memory its descriptor names. */
+struct blob_file {
+ const struct ctlv_node_schema *node;
+ char *path;
+ void *data;
+ __u64 len;
+ /* Where its descriptor starts in the request. */
+ __u64 offset;
+ /* Whether the length is settled, rather than the op's to ask for. */
+ bool sized;
+ unsigned int grown;
+};
+
+/* A rebuilt request meets its blobs in the order the first build did. */
+struct blob_files {
+ struct blob_file *files;
+ unsigned int count;
+ /* How many of them the request being built has reached. */
+ unsigned int used;
+};
+
+/* How often one blob grows for an op that keeps asking for more. */
+#define BLOB_GROWN_MAX 2
+
+/* Zeroed and aligned as the blob requires. */
+static int blob_alloc(struct blob_file *blob, __u64 len)
+{
+ size_t align = blob->node->blob_alignment;
+ void *data;
+ int ret;
+
+ if (align < sizeof(void *))
+ align = sizeof(void *);
+ if (len > SIZE_MAX)
+ return -EFBIG;
+ ret = posix_memalign(&data, align, len);
+ if (ret)
+ return -ret;
+ memset(data, 0, len);
+ free(blob->data);
+ blob->data = data;
+ blob->len = len;
+ return 0;
+}
+
+/*
+ * Whether the op failed for want of a longer output blob that ctlvctl sizes,
+ * which then grows so that the op can be retried.
+ */
+static bool blobs_grow(struct blob_files *blobs, const struct ctlv_error *error)
+{
+ const __u32 need = CTLV_ERROR_HAS_BAD_TLV_OFFSET |
+ CTLV_ERROR_HAS_REQUIRED_LENGTH;
+ unsigned int i;
+
+ if (error->err != -EMSGSIZE || (error->present & need) != need)
+ return false;
+ for (i = 0; i < blobs->count; i++) {
+ struct blob_file *blob = &blobs->files[i];
+
+ if (blob->offset != error->bad_tlv_offset)
+ continue;
+ if (blob->sized || blob->grown == BLOB_GROWN_MAX ||
+ error->required_length <= blob->len ||
+ error->required_length > blob->node->blob_max_length)
+ return false;
+ blob->grown++;
+ return !blob_alloc(blob, error->required_length);
+ }
+ return false;
+}
+
+/* The device does not say how much it wrote, so a file gets the whole blob. */
+static int blobs_save(const struct blob_files *blobs)
+{
+ unsigned int i;
+
+ for (i = 0; i < blobs->count; i++) {
+ const struct blob_file *blob = &blobs->files[i];
+ FILE *file;
+ int ret = 0;
+
+ if (blob->node->blob_direction == CTLV_BLOB_DIRECTION_INPUT)
+ continue;
+ file = fopen(blob->path, "we");
+ if (!file)
+ ret = -errno;
+ else if (fwrite(blob->data, 1, blob->len, file) != blob->len)
+ ret = errno ? -errno : -EIO;
+ if (file && fclose(file) && !ret)
+ ret = errno ? -errno : -EIO;
+ if (ret) {
+ fprintf(stderr, "%s: %s: %s\n", prog, blob->path,
+ strerror(-ret));
+ return ret;
+ }
+ }
+ return 0;
+}
+
+static void blobs_free(struct blob_files *blobs)
+{
+ unsigned int i;
+
+ for (i = 0; i < blobs->count; i++) {
+ free(blobs->files[i].path);
+ free(blobs->files[i].data);
+ }
+ free(blobs->files);
+}
+
+#ifdef HAVE_JSON_C
+
+static const struct ctlv_node_schema *
+node_key(const struct ctlv_node_schema *parent,
+ const struct ctlvctl_spec *spec, const char *key, bool *ambiguous)
+{
+ __u64 id;
+
+ if (!parse_u64(key, &id))
+ return node_member((const struct ctlv_node_schema *[]){ parent },
+ 0, id);
+ return node_named(parent, spec, key, ambiguous);
+}
+
+static int json_node_value(struct json_object *object,
+ const struct ctlv_node_schema *parent,
+ const struct ctlv_node_schema *wanted,
+ const struct ctlvctl_spec *spec,
+ struct json_object **value, bool *present)
+{
+ struct json_object *found = NULL;
+
+ json_object_object_foreach(object, key, item) {
+ const struct ctlv_node_schema *node;
+ bool ambiguous = false;
+
+ node = node_key(parent, spec, key, &ambiguous);
+ if (ambiguous) {
+ fprintf(stderr, "%s: %s: ambiguous field\n",
+ prog, key);
+ return -EINVAL;
+ }
+ if (!node) {
+ fprintf(stderr, "%s: %s: no such field here\n",
+ prog, key);
+ return -ENOENT;
+ }
+ if (node != wanted)
+ continue;
+ if (found) {
+ fprintf(stderr, "%s: %s: field given twice\n",
+ prog, key);
+ return -EINVAL;
+ }
+ found = item;
+ }
+ *present = !!found;
+ *value = found;
+ return 0;
+}
+
+static int json_text_value(struct json_object *value, const char **text)
+{
+ if (!json_object_is_type(value, json_type_string))
+ return -EINVAL;
+ *text = json_object_get_string(value);
+ if (strlen(*text) != (size_t)json_object_get_string_len(value))
+ return -EINVAL;
+ return 0;
+}
+
+static int json_u64_value(struct json_object *value, __u64 *number)
+{
+ const char *text;
+
+ if (json_object_is_type(value, json_type_int)) {
+ text = json_object_get_string(value);
+ return parse_u64(text, number);
+ }
+ if (!json_text_value(value, &text))
+ return parse_u64(text, number);
+ return -EINVAL;
+}
+
+static int json_s64_value(struct json_object *value, __s64 *number)
+{
+ if (json_object_is_type(value, json_type_int))
+ return parse_s64(json_object_get_string(value), number);
+ {
+ const char *text;
+
+ if (json_text_value(value, &text))
+ return -EINVAL;
+ return parse_s64(text, number);
+ }
+}
+
+static int json_flags_value(const struct ctlvctl_field_name *field,
+ struct json_object *json, __u64 *value)
+{
+ size_t i, n;
+
+ if (!json_object_is_type(json, json_type_array))
+ return json_u64_value(json, value);
+ *value = 0;
+ n = json_object_array_length(json);
+ for (i = 0; i < n; i++) {
+ struct json_object *item = json_object_array_get_idx(json, i);
+ const char *name;
+ __u64 bit;
+
+ if (json_text_value(item, &name) ||
+ ctlvctl_spec_value(field, name, &bit))
+ return -EINVAL;
+ *value |= bit;
+ }
+ return 0;
+}
+
+static int json_put_value(struct ctlv_msg *msg,
+ const struct ctlv_node_schema *node,
+ const struct ctlvctl_spec *spec,
+ struct json_object *json)
+{
+ const struct ctlvctl_field_name *field = field_name(spec, node);
+ const char *text;
+ __u64 value;
+ __s64 signed_value;
+
+ switch (node->kind) {
+ case CTLV_SCHEMA_UINT:
+ if (json_u64_value(json, &value))
+ return -EINVAL;
+ return ctlv_put_u64(msg, node->type, value);
+ case CTLV_SCHEMA_SINT:
+ if (json_s64_value(json, &signed_value))
+ return -EINVAL;
+ return ctlv_put_s64(msg, node->type, signed_value);
+ case CTLV_SCHEMA_BOOL:
+ if (!json_object_is_type(json, json_type_boolean))
+ return -EINVAL;
+ return ctlv_put_bool(msg, node->type,
+ json_object_get_boolean(json));
+ case CTLV_SCHEMA_STRING:
+ if (json_text_value(json, &text))
+ return -EINVAL;
+ return ctlv_put_str(msg, node->type, text);
+ case CTLV_SCHEMA_BINARY:
+ if (json_text_value(json, &text))
+ return -EINVAL;
+ if (strncmp(text, "hex:", 4))
+ return -EINVAL;
+ return put_hex(msg, node->type, text + 4);
+ case CTLV_SCHEMA_ENUM:
+ if (json_object_is_type(json, json_type_string)) {
+ if (json_text_value(json, &text) ||
+ ctlvctl_spec_value(field, text, &value))
+ return -EINVAL;
+ return ctlv_put_u64(msg, node->type, value);
+ }
+ if (json_u64_value(json, &value))
+ return -EINVAL;
+ return ctlv_put_u64(msg, node->type, value);
+ case CTLV_SCHEMA_FLAGS:
+ if (json_flags_value(field, json, &value))
+ return -EINVAL;
+ return ctlv_put_u64(msg, node->type, value);
+ default:
+ return -ENOTSUP;
+ }
+}
+
+static const struct ctlv_node_schema *
+node_members(const struct ctlv_node_schema **stack, unsigned int depth)
+{
+ const struct ctlv_node_schema *node = stack[depth];
+ unsigned int level = depth;
+
+ if (!node)
+ return NULL;
+ while (node->reenters) {
+ if (node->reenters > level)
+ return NULL;
+ level -= node->reenters;
+ node = stack[level];
+ if (!node)
+ return NULL;
+ }
+ return node;
+}
+
+/* {"file": PATH}, and "length" for a blob the op writes. */
+static int json_blob_parse(const struct ctlv_node_schema *node,
+ struct json_object *value, const char **path,
+ __u64 *len, bool *sized)
+{
+ struct json_object *file = NULL, *length = NULL;
+
+ if (!json_object_is_type(value, json_type_object))
+ return -EINVAL;
+ json_object_object_foreach(value, key, item) {
+ if (!strcmp(key, "file"))
+ file = item;
+ else if (!strcmp(key, "length"))
+ length = item;
+ else
+ return -EINVAL;
+ }
+ if (!file || json_text_value(file, path) || !**path)
+ return -EINVAL;
+ *sized = !!length;
+ if (!length)
+ return 0;
+ if (node->blob_direction == CTLV_BLOB_DIRECTION_INPUT)
+ return -EINVAL;
+ return json_u64_value(length, len);
+}
+
+/* An output blob starts at a byte unless the request says how long it is. */
+static int json_blob_new(const struct ctlv_node_schema *node,
+ struct json_object *value, struct blob_files *blobs)
+{
+ bool output = node->blob_direction == CTLV_BLOB_DIRECTION_OUTPUT;
+ struct blob_file *files, *blob;
+ char *contents = NULL;
+ const char *path;
+ size_t size = 0;
+ __u64 len = 1;
+ bool sized;
+ int ret;
+
+ if (!(node->present & CTLV_NODE_SCHEMA_HAS_BLOB) ||
+ !(node->blob_backings & (1ULL << CTLV_BLOB_TYPE_VA)))
+ return -EOPNOTSUPP;
+ ret = json_blob_parse(node, value, &path, &len, &sized);
+ if (ret)
+ return ret;
+ if (!output) {
+ ret = file_read(path, &contents, &size);
+ if (ret)
+ return ret;
+ if (!sized)
+ len = size;
+ }
+ ret = -EINVAL;
+ if (!len)
+ goto out;
+ ret = -EFBIG;
+ if (len > node->blob_max_length || size > len)
+ goto out;
+ ret = -ENOMEM;
+ files = realloc(blobs->files, (blobs->count + 1) * sizeof(*files));
+ if (!files)
+ goto out;
+ blobs->files = files;
+ blob = &files[blobs->count];
+ *blob = (struct blob_file){
+ .node = node,
+ .path = strdup(path),
+ .sized = sized || !output,
+ };
+ if (!blob->path)
+ goto out;
+ ret = blob_alloc(blob, len);
+ if (ret) {
+ free(blob->path);
+ goto out;
+ }
+ if (size)
+ memcpy(blob->data, contents, size);
+ blobs->count++;
+out:
+ free(contents);
+ return ret;
+}
+
+static int json_blob_put(struct ctlv_msg *msg,
+ const struct ctlv_node_schema *node,
+ struct json_object *value, struct blob_files *blobs)
+{
+ struct blob_file *blob;
+ int ret;
+
+ if (blobs->used == blobs->count) {
+ ret = json_blob_new(node, value, blobs);
+ if (ret)
+ return ret;
+ }
+ blob = &blobs->files[blobs->used++];
+ if (blob->node != node)
+ return -EINVAL;
+ blob->offset = CTLV_ALIGN(ctlv_msg_len(msg));
+ return ctlv_put_blob_va(msg, node->type, blob->data, blob->len);
+}
+
+static int json_build_object(struct ctlv_msg *msg,
+ const struct ctlv_node_schema **stack,
+ unsigned int depth, struct json_object *object,
+ const struct ctlvctl_spec *spec,
+ struct blob_files *blobs);
+
+static int json_build_one(struct ctlv_msg *msg,
+ const struct ctlv_node_schema **stack,
+ unsigned int depth,
+ const struct ctlv_node_schema *node,
+ struct json_object *value,
+ const struct ctlvctl_spec *spec,
+ struct blob_files *blobs)
+{
+ int ret;
+
+ if (node->kind == CTLV_SCHEMA_BLOB)
+ return json_blob_put(msg, node, value, blobs);
+ if (node->kind != CTLV_SCHEMA_RECORD)
+ return json_put_value(msg, node, spec, value);
+ if (!json_object_is_type(value, json_type_object))
+ return -EINVAL;
+ if (depth + 1 >= PATH_DEPTH)
+ return -E2BIG;
+
+ ret = ctlv_record_start(msg, node->type);
+ if (ret)
+ return ret;
+ stack[depth + 1] = node;
+ ret = json_build_object(msg, stack, depth + 1, value, spec, blobs);
+ ctlv_record_end(msg);
+ return ret;
+}
+
+static int json_build_node(struct ctlv_msg *msg,
+ const struct ctlv_node_schema **stack,
+ unsigned int depth,
+ const struct ctlv_node_schema *node,
+ struct json_object *value,
+ const struct ctlvctl_spec *spec,
+ struct blob_files *blobs)
+{
+ size_t i, n;
+ int ret;
+
+ if (node->max_occurrences == 1)
+ return json_build_one(msg, stack, depth, node, value, spec,
+ blobs);
+ if (!json_object_is_type(value, json_type_array))
+ return -EINVAL;
+ n = json_object_array_length(value);
+ if (node->max_occurrences && n > node->max_occurrences)
+ return -E2BIG;
+ for (i = 0; i < n; i++) {
+ ret = json_build_one(msg, stack, depth, node,
+ json_object_array_get_idx(value, i), spec,
+ blobs);
+ if (ret)
+ return ret;
+ }
+ return 0;
+}
+
+static int json_build_members(struct ctlv_msg *msg,
+ const struct ctlv_node_schema **stack,
+ unsigned int depth,
+ const struct ctlv_node_schema *parent,
+ const struct ctlv_node_schema *node,
+ unsigned int n, bool required,
+ struct json_object *object,
+ const struct ctlvctl_spec *spec,
+ struct blob_files *blobs)
+{
+ unsigned int i;
+
+ for (i = 0; i < n; i++) {
+ const struct ctlvctl_field_name *name = field_name(spec, &node[i]);
+ struct json_object *value;
+ bool present;
+ int ret;
+
+ ret = json_node_value(object, parent, &node[i], spec,
+ &value, &present);
+ if (ret)
+ return ret;
+ if (!present) {
+ if (!required)
+ continue;
+ fprintf(stderr, "%s: %s: required field is missing\n",
+ prog, name ? name->name : "field");
+ return -EINVAL;
+ }
+ if (required && node[i].max_occurrences != 1 &&
+ json_object_is_type(value, json_type_array) &&
+ !json_object_array_length(value)) {
+ fprintf(stderr, "%s: %s: at least one value is required\n",
+ prog, name ? name->name : "field");
+ return -EINVAL;
+ }
+ ret = json_build_node(msg, stack, depth, &node[i], value,
+ spec, blobs);
+ if (ret) {
+ fprintf(stderr, "%s: %s: %s\n", prog,
+ name ? name->name : "field", strerror(-ret));
+ return ret;
+ }
+ }
+ return 0;
+}
+
+static int json_build_object(struct ctlv_msg *msg,
+ const struct ctlv_node_schema **stack,
+ unsigned int depth, struct json_object *object,
+ const struct ctlvctl_spec *spec,
+ struct blob_files *blobs)
+{
+ const struct ctlv_node_schema *parent;
+ int ret;
+
+ if (!json_object_is_type(object, json_type_object))
+ return -EINVAL;
+ parent = node_members(stack, depth);
+ if (!parent)
+ return -EINVAL;
+ json_object_object_foreach(object, key, item) {
+ bool ambiguous = false;
+
+ if (!node_key(parent, spec, key, &ambiguous)) {
+ fprintf(stderr, "%s: %s: %s field\n", prog, key,
+ ambiguous ? "ambiguous" : "no such");
+ return ambiguous ? -EINVAL : -ENOENT;
+ }
+ }
+
+ ret = json_build_members(msg, stack, depth, parent, parent->fields,
+ parent->fields_count, true, object, spec,
+ blobs);
+ if (ret)
+ return ret;
+ return json_build_members(msg, stack, depth, parent,
+ parent->optional_fields,
+ parent->optional_fields_count, false, object,
+ spec, blobs);
+}
+
+/* A rebuild for a retry parses @text again and starts the message over. */
+static int json_request_build(struct ctlv_msg *msg,
+ const struct ctlv_node_schema *root,
+ const struct ctlvctl_spec *spec,
+ const char *text, size_t text_len,
+ struct blob_files *blobs)
+{
+ const struct ctlv_node_schema *stack[PATH_DEPTH] = { root };
+ struct json_tokener *tokener;
+ struct json_object *object;
+ enum json_tokener_error error;
+ int ret;
+
+ if (memchr(text, '\0', text_len))
+ return -EINVAL;
+ if (text_len >= INT_MAX)
+ return -E2BIG;
+ tokener = json_tokener_new_ex(PATH_DEPTH * 2 + 2);
+ if (!tokener)
+ return -ENOMEM;
+ json_tokener_set_flags(tokener, JSON_TOKENER_STRICT |
+ JSON_TOKENER_VALIDATE_UTF8);
+ object = json_tokener_parse_ex(tokener, text, text_len + 1);
+ error = json_tokener_get_error(tokener);
+ if (error != json_tokener_success || !object) {
+ fprintf(stderr, "%s: invalid JSON request: %s\n", prog,
+ json_tokener_error_desc(error));
+ ret = -EINVAL;
+ } else {
+ ctlv_msg_reset(msg);
+ blobs->used = 0;
+ ret = json_build_object(msg, stack, 0, object, spec, blobs);
+ json_object_put(object);
+ }
+ json_tokener_free(tokener);
+ return ret;
+}
+
+#else /* HAVE_JSON_C */
+
+static int json_request_build(struct ctlv_msg *msg,
+ const struct ctlv_node_schema *root,
+ const struct ctlvctl_spec *spec,
+ const char *text, size_t text_len,
+ struct blob_files *blobs)
+{
+ fprintf(stderr, "%s: JSON support was built without json-c\n", prog);
+ return -EOPNOTSUPP;
+}
+
+#endif /* HAVE_JSON_C */
+
+#ifdef HAVE_JSON_C
+
+static char *hex_encode(const void *bytes, __u32 len, const char *prefix)
+{
+ const unsigned char *data = bytes;
+ size_t prefix_len = strlen(prefix);
+ char *text;
+ __u32 i;
+
+ text = malloc(prefix_len + (size_t)len * 2 + 1);
+ if (!text)
+ return NULL;
+ memcpy(text, prefix, prefix_len);
+ for (i = 0; i < len; i++)
+ snprintf(text + prefix_len + i * 2, 3, "%02x", data[i]);
+ return text;
+}
+
+static struct json_object *json_binary(const void *bytes, __u32 len)
+{
+ struct json_object *json;
+ char *text;
+
+ text = hex_encode(bytes, len, "hex:");
+ if (!text)
+ return NULL;
+ json = json_object_new_string(text);
+ free(text);
+ return json;
+}
+
+static struct json_object *json_invalid_utf8(const void *bytes, __u32 len)
+{
+ struct json_object *json;
+ char *text;
+
+ json = json_object_new_object();
+ text = hex_encode(bytes, len, "");
+ if (!json || !text) {
+ json_object_put(json);
+ free(text);
+ return NULL;
+ }
+ json_add_string(json, "encoding", "hex");
+ json_add_string(json, "data", text);
+ free(text);
+ return json;
+}
+
+static bool utf8_valid(const unsigned char *text, size_t len)
+{
+ size_t i = 0;
+
+ while (i < len) {
+ unsigned char first = text[i++];
+ unsigned int continuation;
+
+ if (first < 0x80)
+ continue;
+ if (first >= 0xc2 && first <= 0xdf) {
+ continuation = 1;
+ } else if (first >= 0xe0 && first <= 0xef) {
+ continuation = 2;
+ if (i >= len ||
+ (first == 0xe0 && text[i] < 0xa0) ||
+ (first == 0xed && text[i] >= 0xa0))
+ return false;
+ } else if (first >= 0xf0 && first <= 0xf4) {
+ continuation = 3;
+ if (i >= len ||
+ (first == 0xf0 && text[i] < 0x90) ||
+ (first == 0xf4 && text[i] >= 0x90))
+ return false;
+ } else {
+ return false;
+ }
+ if (continuation > len - i)
+ return false;
+ while (continuation--)
+ if ((text[i++] & 0xc0) != 0x80)
+ return false;
+ }
+ return true;
+}
+
+static struct json_object *
+json_payload(const struct ctlv_hdr *hdr,
+ const struct ctlv_node_schema *node,
+ const struct ctlvctl_spec *spec)
+{
+ const struct ctlvctl_field_name *name = field_name(spec, node);
+ const void *bytes;
+ const char *text;
+ struct json_object *json;
+ __u64 value;
+ __s64 signed_value;
+ __u32 len;
+ bool flag;
+ unsigned int i;
+
+ switch (node ? node->kind : CTLV_SCHEMA_BINARY) {
+ case CTLV_SCHEMA_UINT:
+ if (ctlv_get_u64(hdr, &value))
+ return NULL;
+ return json_object_new_uint64(value);
+ case CTLV_SCHEMA_SINT:
+ if (ctlv_get_s64(hdr, &signed_value))
+ return NULL;
+ return json_object_new_int64(signed_value);
+ case CTLV_SCHEMA_BOOL:
+ if (ctlv_get_bool(hdr, &flag))
+ return NULL;
+ return json_object_new_boolean(flag);
+ case CTLV_SCHEMA_STRING:
+ if (ctlv_get_str(hdr, &text))
+ return NULL;
+ len = ctlv_hdr_payload_len(hdr) - 1;
+ if (!utf8_valid((const unsigned char *)text, len))
+ return json_invalid_utf8(text, len);
+ return json_object_new_string_len(text, len);
+ case CTLV_SCHEMA_ENUM:
+ if (ctlv_get_u64(hdr, &value))
+ return NULL;
+ text = ctlvctl_spec_value_name(name, value);
+ return text ? json_object_new_string(text) :
+ json_object_new_uint64(value);
+ case CTLV_SCHEMA_FLAGS:
+ if (ctlv_get_u64(hdr, &value))
+ return NULL;
+ if (!name || !name->definition)
+ return json_object_new_uint64(value);
+ json = json_object_new_array();
+ if (!json)
+ return NULL;
+ for (i = 0; i < name->definition->values_count; i++) {
+ const struct ctlvctl_value_name *item =
+ &name->definition->values[i];
+
+ if (!(value & item->value))
+ continue;
+ if (json_array_add_string(json, item->name)) {
+ json_object_put(json);
+ return NULL;
+ }
+ value &= ~item->value;
+ }
+ if (value) {
+ char unknown[32];
+
+ snprintf(unknown, sizeof(unknown), "%#llx",
+ (unsigned long long)value);
+ if (json_array_add_string(json, unknown)) {
+ json_object_put(json);
+ return NULL;
+ }
+ }
+ return json;
+ default:
+ if (ctlv_get_bin(hdr, &bytes, &len))
+ return NULL;
+ return json_binary(bytes, len);
+ }
+}
+
+static int json_object_add_value(struct json_object *object, const char *key,
+ struct json_object *value, bool repeated)
+{
+ struct json_object *array, *existing;
+ int ret;
+
+ if (!json_object_object_get_ex(object, key, &existing) && !repeated) {
+ ret = json_object_object_add(object, key, value);
+ if (ret)
+ json_object_put(value);
+ return ret ? -ENOMEM : 0;
+ }
+ if (!json_object_object_get_ex(object, key, &existing)) {
+ array = json_object_new_array();
+ if (!array) {
+ json_object_put(value);
+ return -ENOMEM;
+ }
+ ret = json_object_object_add(object, key, array);
+ if (ret) {
+ json_object_put(array);
+ json_object_put(value);
+ return -ENOMEM;
+ }
+ ret = json_object_array_add(array, value);
+ if (ret)
+ json_object_put(value);
+ return ret ? -ENOMEM : 0;
+ }
+ if (json_object_is_type(existing, json_type_array)) {
+ ret = json_object_array_add(existing, value);
+ if (ret)
+ json_object_put(value);
+ return ret ? -ENOMEM : 0;
+ }
+
+ array = json_object_new_array();
+ if (!array) {
+ json_object_put(value);
+ return -ENOMEM;
+ }
+ json_object_get(existing);
+ if (json_object_array_add(array, existing)) {
+ json_object_put(existing);
+ json_object_put(array);
+ json_object_put(value);
+ return -ENOMEM;
+ }
+ if (json_object_object_add(object, key, array)) {
+ json_object_put(array);
+ json_object_put(value);
+ return -ENOMEM;
+ }
+ ret = json_object_array_add(array, value);
+ if (ret)
+ json_object_put(value);
+ return ret ? -ENOMEM : 0;
+}
+
+static struct json_object *
+json_reply_object(const struct ctlv_hdr *record,
+ const struct ctlv_node_schema **stack,
+ unsigned int depth, const struct ctlvctl_spec *spec)
+{
+ struct json_object *object;
+ const struct ctlv_hdr *child;
+
+ object = json_object_new_object();
+ if (!object)
+ return NULL;
+ ctlv_for_each(child, record) {
+ const struct ctlv_node_schema *node = NULL;
+ const struct ctlvctl_field_name *name;
+ struct json_object *value;
+ char numeric[32];
+ const char *key;
+
+ if (depth < PATH_DEPTH)
+ node = node_member(stack, depth, child->type);
+ name = node ? field_name(spec, node) : NULL;
+ if (name) {
+ key = name->name;
+ } else {
+ snprintf(numeric, sizeof(numeric), "%#018llx",
+ (unsigned long long)child->type);
+ key = numeric;
+ }
+ if (ctlv_hdr_is_record(child)) {
+ if (depth + 1 >= PATH_DEPTH) {
+ json_object_put(object);
+ return NULL;
+ }
+ stack[depth + 1] = node;
+ value = json_reply_object(child, stack, depth + 1,
+ spec);
+ } else {
+ value = json_payload(child, node, spec);
+ }
+ if (!value ||
+ json_object_add_value(object, key, value,
+ node &&
+ node->max_occurrences != 1)) {
+ json_object_put(object);
+ return NULL;
+ }
+ }
+ return object;
+}
+
+static struct json_object *
+json_reply(const struct ctlv_hdr *root,
+ const struct ctlv_node_schema *tree,
+ const struct ctlvctl_spec *spec)
+{
+ const struct ctlv_node_schema *stack[PATH_DEPTH] = { tree };
+
+ return json_reply_object(root, stack, 0, spec);
+}
+
+static void json_add_id(struct json_object *object, const char *key, __u64 id)
+{
+ char text[32];
+
+ snprintf(text, sizeof(text), "%#018llx", (unsigned long long)id);
+ json_object_object_add(object, key, json_object_new_string(text));
+}
+
+static void json_add_op(struct json_object *object,
+ const struct ctlvctl_spec *spec, __u64 op_id)
+{
+ const struct ctlvctl_op_name *name = ctlvctl_spec_op_id(spec, op_id);
+
+ if (name)
+ json_add_string(object, "op", name->name);
+ if (!name || options.show_ids)
+ json_add_id(object, "id", op_id);
+}
+
+static const char *schema_kind_name(__u32 kind)
+{
+ static const char *const names[] = {
+ [CTLV_SCHEMA_UINT] = "uint",
+ [CTLV_SCHEMA_SINT] = "sint",
+ [CTLV_SCHEMA_BOOL] = "bool",
+ [CTLV_SCHEMA_STRING] = "string",
+ [CTLV_SCHEMA_BINARY] = "binary",
+ [CTLV_SCHEMA_ENUM] = "enum",
+ [CTLV_SCHEMA_FLAGS] = "flags",
+ [CTLV_SCHEMA_RECORD] = "record",
+ [CTLV_SCHEMA_BLOB] = "blob",
+ };
+
+ if (kind >= ARRAY_SIZE(names) || !names[kind])
+ return "unknown";
+ return names[kind];
+}
+
+static struct json_object *
+json_schema_node(const struct ctlv_node_schema *node,
+ const struct ctlvctl_spec *spec, bool required)
+{
+ const struct ctlvctl_field_name *name = field_name(spec, node);
+ struct json_object *object, *array;
+ unsigned int i;
+
+ object = json_object_new_object();
+ if (!object)
+ return NULL;
+ if (node->type != CTLV_TYPE_ROOT) {
+ if (name)
+ json_add_string(object, "name", name->name);
+ if (!name || options.show_ids)
+ json_add_id(object, "type", node->type);
+ json_add_bool(object, "required", required);
+ }
+ json_add_string(object, "kind", schema_kind_name(node->kind));
+ if (!node->max_occurrences)
+ json_add_string(object, "max-occurrences", "unlimited");
+ else
+ json_add_u64(object, "max-occurrences",
+ node->max_occurrences);
+ if (node->present & CTLV_NODE_SCHEMA_HAS_MIN) {
+ if (node->kind == CTLV_SCHEMA_SINT)
+ json_add_s64(object, "min", (__s64)node->min);
+ else
+ json_add_u64(object, "min", node->min);
+ }
+ if (node->present & CTLV_NODE_SCHEMA_HAS_MAX) {
+ if (node->kind == CTLV_SCHEMA_SINT)
+ json_add_s64(object, "max", (__s64)node->max);
+ else
+ json_add_u64(object, "max", node->max);
+ }
+ if (node->present & CTLV_NODE_SCHEMA_HAS_MIN_LENGTH)
+ json_add_u64(object, "min-length", node->min_length);
+ if (node->present & CTLV_NODE_SCHEMA_HAS_MAX_LENGTH)
+ json_add_u64(object, "max-length", node->max_length);
+ if (name && name->definition) {
+ array = json_object_new_array();
+ if (!array) {
+ json_object_put(object);
+ return NULL;
+ }
+ for (i = 0; i < name->definition->values_count; i++) {
+ const char *value_name =
+ name->definition->values[i].name;
+
+ if (json_array_add_string(array, value_name)) {
+ json_object_put(array);
+ json_object_put(object);
+ return NULL;
+ }
+ }
+ json_object_object_add(object, "values", array);
+ }
+ if (node->reenters)
+ json_add_u64(object, "reenters", node->reenters);
+
+ array = json_object_new_array();
+ if (!array) {
+ json_object_put(object);
+ return NULL;
+ }
+ for (i = 0; i < node->fields_count; i++) {
+ struct json_object *child =
+ json_schema_node(&node->fields[i], spec, true);
+
+ if (!child) {
+ json_object_put(array);
+ json_object_put(object);
+ return NULL;
+ }
+ json_object_array_add(array, child);
+ }
+ for (i = 0; i < node->optional_fields_count; i++) {
+ struct json_object *child =
+ json_schema_node(&node->optional_fields[i], spec, false);
+
+ if (!child) {
+ json_object_put(array);
+ json_object_put(object);
+ return NULL;
+ }
+ json_object_array_add(array, child);
+ }
+ if (json_object_array_length(array))
+ json_object_object_add(object, "fields", array);
+ else
+ json_object_put(array);
+ return object;
+}
+
+static int json_schema_print(const struct ctlv_dev *dev,
+ const struct ctlvctl_spec *spec, __u64 op_id,
+ const struct ctlv_schema *schema)
+{
+ const struct ctlv_node_schema *node;
+ struct json_object *object, *tree;
+
+ object = json_object_new_object();
+ if (!object)
+ return -ENOMEM;
+ json_add_string(object, "device", ctlv_dev_name(dev));
+ json_add_op(object, spec, op_id);
+ node = ctlv_schema_request(schema);
+ if (node) {
+ tree = json_schema_node(node, spec, true);
+ if (!tree)
+ goto err;
+ json_object_object_add(object, "request", tree);
+ }
+ node = ctlv_schema_reply(schema);
+ if (node) {
+ tree = json_schema_node(node, spec, true);
+ if (!tree)
+ goto err;
+ json_object_object_add(object, "reply", tree);
+ }
+ return json_print(object);
+err:
+ json_object_put(object);
+ return -ENOMEM;
+}
+
+static int json_print(struct json_object *object)
+{
+ const char *text;
+ size_t len;
+ int ret = 0;
+
+ if (!object)
+ return -ENOMEM;
+ text = json_object_to_json_string_ext(object,
+ JSON_C_TO_STRING_PLAIN);
+ if (!text) {
+ ret = -ENOMEM;
+ goto out;
+ }
+ len = strlen(text);
+ if (fwrite(text, 1, len, stdout) != len ||
+ fputc('\n', stdout) == EOF || fflush(stdout))
+ ret = errno ? -errno : -EIO;
+out:
+ json_object_put(object);
+ return ret;
+}
+
+static struct json_object *
+json_template_object(const struct ctlv_node_schema **stack,
+ unsigned int depth, const struct ctlvctl_spec *spec);
+
+static struct json_object *
+json_template_value(const struct ctlv_node_schema **stack,
+ unsigned int depth, const struct ctlv_node_schema *node,
+ const struct ctlvctl_spec *spec)
+{
+ const struct ctlvctl_field_name *name = field_name(spec, node);
+ const char *text;
+ struct json_object *value;
+ __u64 number;
+
+ switch (node->kind) {
+ case CTLV_SCHEMA_UINT:
+ number = node->present & CTLV_NODE_SCHEMA_HAS_MIN ? node->min : 0;
+ value = json_object_new_uint64(number);
+ break;
+ case CTLV_SCHEMA_SINT:
+ number = node->present & CTLV_NODE_SCHEMA_HAS_MIN ? node->min : 0;
+ value = json_object_new_int64((__s64)number);
+ break;
+ case CTLV_SCHEMA_BOOL:
+ value = json_object_new_boolean(false);
+ break;
+ case CTLV_SCHEMA_STRING: {
+ size_t len = node->present & CTLV_NODE_SCHEMA_HAS_MIN_LENGTH ?
+ node->min_length : 0;
+ char *text = calloc(len + 1, 1);
+
+ if (!text)
+ return NULL;
+ memset(text, 'x', len);
+ value = json_object_new_string_len(text, len);
+ free(text);
+ break;
+ }
+ case CTLV_SCHEMA_BINARY: {
+ size_t len = node->present & CTLV_NODE_SCHEMA_HAS_MIN_LENGTH ?
+ node->min_length : 0;
+ unsigned char *bytes = calloc(len ? len : 1, 1);
+
+ if (!bytes)
+ return NULL;
+ value = json_binary(bytes, len);
+ free(bytes);
+ break;
+ }
+ case CTLV_SCHEMA_ENUM:
+ number = node->present & CTLV_NODE_SCHEMA_HAS_MIN ? node->min : 0;
+ text = ctlvctl_spec_value_name(name, number);
+ if (text)
+ value = json_object_new_string(text);
+ else
+ value = json_object_new_uint64(number);
+ break;
+ case CTLV_SCHEMA_FLAGS:
+ value = json_object_new_array();
+ break;
+ case CTLV_SCHEMA_RECORD:
+ if (depth + 1 >= PATH_DEPTH)
+ return NULL;
+ stack[depth + 1] = node;
+ value = json_template_object(stack, depth + 1, spec);
+ break;
+ case CTLV_SCHEMA_BLOB:
+ value = json_object_new_object();
+ if (value)
+ json_add_string(value, "file", "");
+ break;
+ default:
+ fprintf(stderr, "%s: cannot template schema kind %u\n", prog,
+ node->kind);
+ return NULL;
+ }
+ if (!value)
+ return NULL;
+ if (node->max_occurrences == 1)
+ return value;
+
+ {
+ struct json_object *array = json_object_new_array();
+
+ if (!array) {
+ json_object_put(value);
+ return NULL;
+ }
+ json_object_array_add(array, value);
+ return array;
+ }
+}
+
+static struct json_object *
+json_template_object(const struct ctlv_node_schema **stack,
+ unsigned int depth, const struct ctlvctl_spec *spec)
+{
+ const struct ctlv_node_schema *parent = node_members(stack, depth);
+ struct json_object *object;
+ unsigned int i;
+
+ if (!parent)
+ return NULL;
+ object = json_object_new_object();
+ if (!object)
+ return NULL;
+ for (i = 0; i < parent->fields_count; i++) {
+ const struct ctlv_node_schema *node = &parent->fields[i];
+ const struct ctlvctl_field_name *name = field_name(spec, node);
+ struct json_object *value;
+ char numeric[32];
+ const char *key;
+
+ value = json_template_value(stack, depth, node, spec);
+ if (!value) {
+ json_object_put(object);
+ return NULL;
+ }
+ if (name) {
+ key = name->name;
+ } else {
+ snprintf(numeric, sizeof(numeric), "%#018llx",
+ (unsigned long long)node->type);
+ key = numeric;
+ }
+ json_object_object_add(object, key, value);
+ }
+ return object;
+}
+
+static int json_template_print(const struct ctlv_node_schema *root,
+ const struct ctlvctl_spec *spec)
+{
+ const struct ctlv_node_schema *stack[PATH_DEPTH] = { root };
+
+ return json_print(json_template_object(stack, 0, spec));
+}
+
+#endif /* HAVE_JSON_C */
+
+static void payload_print(const struct ctlv_hdr *hdr,
+ const struct ctlv_node_schema *node,
+ const struct ctlvctl_spec *spec)
+{
+ const struct ctlvctl_field_name *name = field_name(spec, node);
+ const void *bytes;
+ const char *text;
+ __u64 value;
+ __s64 signed_value;
+ bool flag;
+ __u32 len, i;
+
+ switch (node ? node->kind : CTLV_SCHEMA_BINARY) {
+ case CTLV_SCHEMA_UINT:
+ if (!ctlv_get_u64(hdr, &value))
+ printf("%llu", (unsigned long long)value);
+ return;
+ case CTLV_SCHEMA_ENUM:
+ if (ctlv_get_u64(hdr, &value))
+ return;
+ text = ctlvctl_spec_value_name(name, value);
+ if (!text) {
+ printf("%llu", (unsigned long long)value);
+ return;
+ }
+ printf("%s", text);
+ if (options.show_ids)
+ printf(" (%llu)", (unsigned long long)value);
+ return;
+ case CTLV_SCHEMA_FLAGS:
+ if (ctlv_get_u64(hdr, &value))
+ return;
+ if (name && name->definition) {
+ __u64 unknown = value;
+ bool first = true;
+
+ for (i = 0; i < name->definition->values_count; i++) {
+ const struct ctlvctl_value_name *item =
+ &name->definition->values[i];
+
+ if (!(value & item->value))
+ continue;
+ printf("%s%s", first ? "" : ",", item->name);
+ unknown &= ~item->value;
+ first = false;
+ }
+ if (unknown || first)
+ printf("%s%#llx", first ? "" : ",",
+ (unsigned long long)unknown);
+ if (options.show_ids)
+ printf(" (%#llx)", (unsigned long long)value);
+ } else {
+ printf("%#llx", (unsigned long long)value);
+ }
+ return;
+ case CTLV_SCHEMA_SINT:
+ if (!ctlv_get_s64(hdr, &signed_value))
+ printf("%lld", (long long)signed_value);
+ return;
+ case CTLV_SCHEMA_BOOL:
+ if (!ctlv_get_bool(hdr, &flag))
+ printf("%s", flag ? "true" : "false");
+ return;
+ case CTLV_SCHEMA_STRING:
+ if (!ctlv_get_str(hdr, &text))
+ printf("%s", text);
+ return;
+ default:
+ if (ctlv_get_bin(hdr, &bytes, &len))
+ return;
+ if (name)
+ printf("hex:");
+ for (i = 0; i < len; i++)
+ printf("%02x", ((const __u8 *)bytes)[i]);
+ return;
+ }
+}
+
+/*
+ * Printed as the schema says, and a TLV the schema does not mention is
+ * printed as bytes with a "?": a device newer than this tool is not an error.
+ */
+static void reply_print(const struct ctlv_hdr *record,
+ const struct ctlv_node_schema **stack,
+ unsigned int depth, const struct ctlvctl_spec *spec)
+{
+ const struct ctlv_hdr *child;
+
+ ctlv_for_each(child, record) {
+ const struct ctlv_node_schema *node = NULL;
+ const struct ctlvctl_field_name *name;
+
+ if (depth < PATH_DEPTH)
+ node = node_member(stack, depth, child->type);
+ name = node ? field_name(spec, node) : NULL;
+ printf("%*s", (depth + 1) * 2, "");
+ if (name) {
+ printf("%s", name->name);
+ if (options.show_ids)
+ printf(" (%#018llx)",
+ (unsigned long long)child->type);
+ } else {
+ printf("%#018llx%s",
+ (unsigned long long)child->type,
+ node ? "" : "?");
+ }
+ if (ctlv_hdr_is_record(child)) {
+ printf("\n");
+ if (depth + 1 < PATH_DEPTH) {
+ stack[depth + 1] = node;
+ reply_print(child, stack, depth + 1, spec);
+ }
+ continue;
+ }
+ printf(" ");
+ payload_print(child, node, spec);
+ printf("\n");
+ }
+}
+
+static void reply_dump(const struct ctlv_hdr *root,
+ const struct ctlv_node_schema *tree,
+ const struct ctlvctl_spec *spec)
+{
+ const struct ctlv_node_schema *stack[PATH_DEPTH] = { tree };
+
+ reply_print(root, stack, 0, spec);
+}
+
+static int run_op_info(const struct ctlv_dev *dev,
+ const struct ctlvctl_spec *spec,
+ const struct ctlv_ops *ops, __u64 op_id,
+ const struct ctlv_op_info **infop)
+{
+ const struct ctlvctl_op_name *name = ctlvctl_spec_op_id(spec, op_id);
+ const struct ctlv_op_info *info = ctlv_ops_find(ops, op_id);
+
+ if (!info) {
+ fprintf(stderr, "%s: operation is not available on %s\n",
+ prog, ctlv_dev_name(dev));
+ return -ENOENT;
+ }
+ if (name && !op_matches(name, info)) {
+ fprintf(stderr, "%s: specification disagrees with operation %s\n",
+ prog, name->name);
+ return -ENOMSG;
+ }
+ if (info->fd_access != CTLV_OP_FD_ACCESS_READ &&
+ info->fd_access != CTLV_OP_FD_ACCESS_READ_WRITE)
+ return -EPROTO;
+ *infop = info;
+ return 0;
+}
+
+static int cmd_run(int argc, char **argv)
+{
+ const struct ctlvctl_op_name *name;
+ const struct ctlv_node_schema *request_tree;
+ const struct ctlv_op_info *info;
+ struct ctlvctl_spec *spec = NULL;
+ struct ctlv_msg *request = NULL;
+ struct ctlv_reply *reply = NULL;
+ struct blob_files blobs = {};
+ struct ctlv_schema *schema;
+ struct ctlv_ops *ops = NULL;
+ struct ctlv_error error;
+ struct ctlv_dev *dev;
+ const char *json_source = NULL;
+ char *json_text = NULL;
+ char text[LINE_LEN];
+ size_t json_len = 0;
+ __u64 reply_len = 0;
+ __u64 op_id;
+ __u32 fd_access;
+ __u32 type;
+ int ret;
+
+ if (argc > 1 && !strcmp(argv[0], "-l")) {
+ ret = parse_u64(argv[1], &reply_len);
+ if (ret || !reply_len || reply_len > UINT32_MAX) {
+ fprintf(stderr, "%s: %s: not a reply length\n", prog,
+ argv[1]);
+ return ret ? ret : -EINVAL;
+ }
+ argc -= 2;
+ argv += 2;
+ }
+
+ if (argc < 2) {
+ help(stderr);
+ return -EINVAL;
+ }
+ if (argc > 2 && !strcmp(argv[2], "--json")) {
+ if (argc != 4) {
+ fprintf(stderr,
+ "%s: --json needs one source and no assignments\n",
+ prog);
+ return -EINVAL;
+ }
+ json_source = argv[3];
+ }
+ /* Discovery is always available through an observation fd. */
+ ret = dev_open(argv[0], O_RDONLY, &dev);
+ if (ret)
+ return ret;
+ ret = spec_load(dev, &spec);
+ if (ret)
+ goto out_close;
+ ret = op_resolve(spec, argv[1], &op_id);
+ if (ret)
+ goto out_close;
+
+ name = ctlvctl_spec_op_id(spec, op_id);
+ ret = ctlv_ops_get(dev, &ops, &error);
+ if (ret) {
+ fprintf(stderr, "%s: op-get: %s\n", prog,
+ ctlv_error_str(&error, text, sizeof(text)));
+ goto out_close;
+ }
+ ret = run_op_info(dev, spec, ops, op_id, &info);
+ if (ret)
+ goto out_free_ops;
+ fd_access = info->fd_access;
+ type = info->type;
+ ctlv_ops_free(ops);
+ ops = NULL;
+
+ if (fd_access == CTLV_OP_FD_ACCESS_READ_WRITE) {
+ __u64 reopened_id;
+
+ ctlvctl_spec_free(spec);
+ spec = NULL;
+ ctlv_close(dev);
+ dev = NULL;
+ ret = dev_open(argv[0], O_RDWR, &dev);
+ if (ret)
+ return ret;
+ ret = spec_load(dev, &spec);
+ if (ret)
+ goto out_close;
+ ret = op_resolve(spec, argv[1], &reopened_id);
+ if (ret)
+ goto out_close;
+ if (reopened_id != op_id) {
+ ret = -ENODEV;
+ goto out_close;
+ }
+ name = ctlvctl_spec_op_id(spec, op_id);
+ ret = ctlv_ops_get(dev, &ops, &error);
+ if (ret) {
+ fprintf(stderr, "%s: op-get: %s\n", prog,
+ ctlv_error_str(&error, text, sizeof(text)));
+ goto out_close;
+ }
+ ret = run_op_info(dev, spec, ops, op_id, &info);
+ if (ret)
+ goto out_free_ops;
+ if (info->fd_access != fd_access || info->type != type) {
+ ret = -ENODEV;
+ goto out_free_ops;
+ }
+ ctlv_ops_free(ops);
+ ops = NULL;
+ }
+
+ /* The schema first: it says how to read values and print the reply. */
+ ret = ctlv_op_schema_get(dev, op_id, &schema, &error);
+ if (ret) {
+ fprintf(stderr, "%s: op-schema-get: %s\n", prog,
+ ctlv_error_str(&error, text, sizeof(text)));
+ goto out_close;
+ }
+ ret = tree_names_check(spec, ctlv_schema_request(schema));
+ if (!ret)
+ ret = tree_names_check(spec, ctlv_schema_reply(schema));
+ if (ret)
+ goto out_free_schema;
+
+ request_tree = ctlv_schema_request(schema);
+ if ((argc > 2 || json_source) && !request_tree) {
+ fprintf(stderr, "%s: this op takes no request\n", prog);
+ ret = -EINVAL;
+ goto out_free_schema;
+ }
+ if (request_tree) {
+ ret = ctlv_msg_new(REQUEST_HINT, &request);
+ if (ret)
+ goto out_free_schema;
+ if (json_source) {
+ ret = json_source_read(json_source, &json_text,
+ &json_len);
+ if (!ret)
+ ret = json_request_build(request, request_tree,
+ spec, json_text,
+ json_len, &blobs);
+ } else {
+ ret = request_build(request, request_tree, spec,
+ argc - 2, argv + 2);
+ }
+ if (ret)
+ goto out_free_request;
+ }
+
+ for (;;) {
+ if (type == CTLV_OP_TYPE_QUERY && reply_len)
+ ret = ctlv_query_sized(dev, op_id, request, reply_len,
+ &reply, &error);
+ else if (type == CTLV_OP_TYPE_QUERY)
+ ret = ctlv_query(dev, op_id, request, &reply, &error);
+ else if (reply_len)
+ ret = ctlv_action_sized(dev, op_id, request, reply_len,
+ &reply, &error);
+ else
+ ret = ctlv_action(dev, op_id, request, &reply, &error);
+ if (!ret || !blobs_grow(&blobs, &error))
+ break;
+ ret = json_request_build(request, request_tree, spec,
+ json_text, json_len, &blobs);
+ if (ret)
+ goto out_free_request;
+ }
+ if (ret) {
+ fprintf(stderr, "%s: %s\n", prog,
+ ctlv_error_str(&error, text, sizeof(text)));
+ goto out_free_request;
+ }
+ ret = blobs_save(&blobs);
+ if (ret)
+ goto out_free_reply;
+
+ if (options.output_json) {
+#ifdef HAVE_JSON_C
+ struct json_object *output = json_object_new_object();
+
+ if (!output) {
+ ret = -ENOMEM;
+ goto out_free_reply;
+ }
+ json_add_string(output, "device", ctlv_dev_name(dev));
+ json_add_op(output, spec, op_id);
+ if (!reply) {
+ json_add_bool(output, "acknowledged", true);
+ } else {
+ struct json_object *body;
+
+ json_add_u64(output, "generation",
+ ctlv_reply_generation(reply));
+ json_add_int(output, "fragments",
+ ctlv_reply_fragments(reply));
+ body = json_reply(ctlv_reply_root(reply),
+ ctlv_schema_reply(schema), spec);
+ if (!body) {
+ json_object_put(output);
+ ret = -ENOMEM;
+ goto out_free_reply;
+ }
+ json_object_object_add(output, "reply", body);
+ }
+ ret = json_print(output);
+#endif
+ } else if (!reply) {
+ printf("# acknowledged\n");
+ } else {
+ printf("# ");
+ if (name) {
+ printf("%s", name->name);
+ if (options.show_ids)
+ printf(" (%#018llx)",
+ (unsigned long long)op_id);
+ printf(", ");
+ }
+ printf("%u fragment%s, generation %llu\n",
+ ctlv_reply_fragments(reply),
+ ctlv_reply_fragments(reply) == 1 ? "" : "s",
+ (unsigned long long)ctlv_reply_generation(reply));
+ reply_dump(ctlv_reply_root(reply), ctlv_schema_reply(schema),
+ spec);
+ }
+out_free_reply:
+ ctlv_reply_free(reply);
+out_free_request:
+ ctlv_msg_free(request);
+ blobs_free(&blobs);
+ free(json_text);
+out_free_schema:
+ ctlv_schema_free(schema);
+out_close:
+ ctlvctl_spec_free(spec);
+ ctlv_close(dev);
+ return ret;
+out_free_ops:
+ ctlv_ops_free(ops);
+ goto out_close;
+}
+
+int main(int argc, char **argv)
+{
+ static const struct option long_options[] = {
+ { "spec-dir", required_argument, NULL, 1 },
+ { "show-ids", no_argument, NULL, 'n' },
+ { "output-json", no_argument, NULL, 2 },
+ { "help", no_argument, NULL, 'h' },
+ {},
+ };
+ const char *command;
+ const char *spec_dir;
+ int opt;
+
+ if (argc > 0)
+ prog = argv[0];
+ while ((opt = getopt_long(argc, argv, "+nh", long_options, NULL)) != -1) {
+ switch (opt) {
+ case 1:
+ options.spec_dir = optarg;
+ break;
+ case 2:
+ options.output_json = true;
+ break;
+ case 'n':
+ options.show_ids = true;
+ break;
+ case 'h':
+ help(stdout);
+ return EXIT_SUCCESS;
+ default:
+ help(stderr);
+ return EXIT_FAILURE;
+ }
+ }
+ if (!options.spec_dir) {
+ spec_dir = getenv("CTLV_SPEC_DIR");
+ if (spec_dir && *spec_dir)
+ options.spec_dir = spec_dir;
+ }
+#ifndef HAVE_JSON_C
+ if (options.spec_dir || options.output_json) {
+ fprintf(stderr,
+ "%s: specification and JSON support require json-c\n",
+ prog);
+ return EXIT_FAILURE;
+ }
+#endif
+ if (optind == argc) {
+ help(stderr);
+ return EXIT_FAILURE;
+ }
+ command = argv[optind++];
+ argc -= optind;
+ argv += optind;
+
+ if (!strcmp(command, "list"))
+ return cmd_list(argc, argv) ? EXIT_FAILURE : EXIT_SUCCESS;
+ if (!strcmp(command, "ops"))
+ return cmd_ops(argc, argv) ? EXIT_FAILURE : EXIT_SUCCESS;
+ if (!strcmp(command, "schema"))
+ return cmd_schema(argc, argv) ? EXIT_FAILURE : EXIT_SUCCESS;
+ if (!strcmp(command, "exec"))
+ return cmd_run(argc, argv) ? EXIT_FAILURE : EXIT_SUCCESS;
+ if (!strcmp(command, "help") || !strcmp(command, "--help")) {
+ help(stdout);
+ return EXIT_SUCCESS;
+ }
+ fprintf(stderr, "%s: %s: no such command\n", prog, command);
+ help(stderr);
+ return EXIT_FAILURE;
+}
diff --git a/tools/ctlv/ctlvctl/spec.c b/tools/ctlv/ctlvctl/spec.c
new file mode 100644
index 000000000000..7fa1e3de2d33
--- /dev/null
+++ b/tools/ctlv/ctlvctl/spec.c
@@ -0,0 +1,672 @@
+// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include <ctlv.h>
+
+#include "spec.h"
+
+#define ARRAY_SIZE(array) (sizeof(array) / sizeof((array)[0]))
+
+struct ctlvctl_spec {
+ struct ctlvctl_definition *definitions;
+ unsigned int definitions_count;
+ struct ctlvctl_field_name *fields;
+ unsigned int fields_count;
+ struct ctlvctl_op_name *ops;
+ unsigned int ops_count;
+};
+
+#ifdef HAVE_JSON_C
+
+#include <json.h>
+
+static int parse_u64(const char *text, __u64 *value)
+{
+ char *end;
+
+ if (!*text || *text == '-' || isspace((unsigned char)*text))
+ return -EINVAL;
+ errno = 0;
+ *value = strtoull(text, &end, 0);
+ if (errno || end == text || *end)
+ return -EINVAL;
+ return 0;
+}
+
+static int json_member(struct json_object *parent, const char *name,
+ enum json_type type, struct json_object **value)
+{
+ if (!json_object_object_get_ex(parent, name, value) ||
+ !json_object_is_type(*value, type))
+ return -EINVAL;
+ return 0;
+}
+
+static int json_string_dup(struct json_object *parent, const char *name,
+ char **value)
+{
+ struct json_object *item;
+
+ if (json_member(parent, name, json_type_string, &item))
+ return -EINVAL;
+ *value = strdup(json_object_get_string(item));
+ return *value ? 0 : -ENOMEM;
+}
+
+static int json_u64(struct json_object *parent, const char *name, __u64 *value)
+{
+ struct json_object *item;
+
+ if (!json_object_object_get_ex(parent, name, &item))
+ return -EINVAL;
+ if (json_object_is_type(item, json_type_string))
+ return parse_u64(json_object_get_string(item), value);
+ if (!json_object_is_type(item, json_type_int))
+ return -EINVAL;
+ *value = json_object_get_uint64(item);
+ return 0;
+}
+
+static int kind_parse(const char *name, __u32 *kind)
+{
+ static const char *const names[] = {
+ [CTLV_SCHEMA_UINT] = "uint",
+ [CTLV_SCHEMA_SINT] = "sint",
+ [CTLV_SCHEMA_BOOL] = "bool",
+ [CTLV_SCHEMA_STRING] = "string",
+ [CTLV_SCHEMA_BINARY] = "binary",
+ [CTLV_SCHEMA_ENUM] = "enum",
+ [CTLV_SCHEMA_FLAGS] = "flags",
+ [CTLV_SCHEMA_RECORD] = "record",
+ [CTLV_SCHEMA_BLOB] = "blob",
+ };
+ unsigned int i;
+
+ for (i = 0; i < ARRAY_SIZE(names); i++)
+ if (names[i] && !strcmp(names[i], name)) {
+ *kind = i;
+ return 0;
+ }
+ return -EINVAL;
+}
+
+static int op_type_parse(const char *name, __u32 *type)
+{
+ if (!strcmp(name, "action"))
+ *type = CTLV_OP_TYPE_ACTION;
+ else if (!strcmp(name, "query"))
+ *type = CTLV_OP_TYPE_QUERY;
+ else
+ return -EINVAL;
+ return 0;
+}
+
+static int availability_parse(const char *name, __u32 *availability)
+{
+ if (!strcmp(name, "fixed"))
+ *availability = CTLV_OP_AVAILABILITY_FIXED;
+ else if (!strcmp(name, "dynamic"))
+ *availability = CTLV_OP_AVAILABILITY_DYNAMIC;
+ else
+ return -EINVAL;
+ return 0;
+}
+
+static int fd_access_parse(const char *name, __u32 *fd_access)
+{
+ if (!strcmp(name, "read"))
+ *fd_access = CTLV_OP_FD_ACCESS_READ;
+ else if (!strcmp(name, "read-write"))
+ *fd_access = CTLV_OP_FD_ACCESS_READ_WRITE;
+ else
+ return -EINVAL;
+ return 0;
+}
+
+static struct ctlvctl_definition *
+definition_find(struct ctlvctl_spec *spec, const char *name)
+{
+ unsigned int i;
+
+ for (i = 0; i < spec->definitions_count; i++)
+ if (!strcmp(spec->definitions[i].name, name))
+ return &spec->definitions[i];
+ return NULL;
+}
+
+static int definition_parse(struct json_object *json,
+ struct ctlvctl_definition *definition)
+{
+ struct json_object *kind, *values;
+ size_t i, n;
+ int ret;
+
+ ret = json_string_dup(json, "name", &definition->name);
+ if (ret)
+ return ret;
+ if (json_member(json, "kind", json_type_string, &kind) ||
+ kind_parse(json_object_get_string(kind), &definition->kind))
+ return -EINVAL;
+ if (definition->kind != CTLV_SCHEMA_ENUM &&
+ definition->kind != CTLV_SCHEMA_FLAGS)
+ return -EINVAL;
+ if (json_member(json, "values", json_type_array, &values))
+ return -EINVAL;
+
+ n = json_object_array_length(values);
+ definition->values = calloc(n, sizeof(*definition->values));
+ if (n && !definition->values)
+ return -ENOMEM;
+ definition->values_count = n;
+ for (i = 0; i < n; i++) {
+ struct json_object *item = json_object_array_get_idx(values, i);
+
+ if (!item ||
+ json_string_dup(item, "name",
+ &definition->values[i].name) ||
+ json_u64(item, "value", &definition->values[i].value))
+ return -EINVAL;
+ }
+ return 0;
+}
+
+static int definitions_parse(struct json_object *root,
+ struct ctlvctl_spec *spec)
+{
+ struct json_object *items;
+ size_t i, n;
+ int ret;
+
+ if (json_member(root, "definitions", json_type_array, &items))
+ return -EINVAL;
+ n = json_object_array_length(items);
+ spec->definitions = calloc(n, sizeof(*spec->definitions));
+ if (n && !spec->definitions)
+ return -ENOMEM;
+ spec->definitions_count = n;
+ for (i = 0; i < n; i++) {
+ ret = definition_parse(json_object_array_get_idx(items, i),
+ &spec->definitions[i]);
+ if (ret)
+ return ret;
+ }
+ return 0;
+}
+
+static int field_parse(struct json_object *json, struct ctlvctl_spec *spec,
+ struct ctlvctl_field_name *field)
+{
+ struct json_object *item;
+ const char *definition;
+ int ret;
+
+ ret = json_string_dup(json, "name", &field->name);
+ if (ret)
+ return ret;
+ ret = json_string_dup(json, "short", &field->short_name);
+ if (ret)
+ return ret;
+ ret = json_u64(json, "type", &field->type);
+ if (ret)
+ return ret;
+ if (json_member(json, "kind", json_type_string, &item) ||
+ kind_parse(json_object_get_string(item), &field->kind))
+ return -EINVAL;
+
+ if (!json_object_object_get_ex(json, "definition", &item))
+ return 0;
+ if (!json_object_is_type(item, json_type_string))
+ return -EINVAL;
+ definition = json_object_get_string(item);
+ field->definition = definition_find(spec, definition);
+ return field->definition ? 0 : -EINVAL;
+}
+
+static int fields_parse(struct json_object *root, struct ctlvctl_spec *spec)
+{
+ struct json_object *items;
+ size_t i, n;
+ int ret;
+
+ if (json_member(root, "fields", json_type_array, &items))
+ return -EINVAL;
+ n = json_object_array_length(items);
+ spec->fields = calloc(n, sizeof(*spec->fields));
+ if (n && !spec->fields)
+ return -ENOMEM;
+ spec->fields_count = n;
+ for (i = 0; i < n; i++) {
+ ret = field_parse(json_object_array_get_idx(items, i),
+ spec, &spec->fields[i]);
+ if (ret)
+ return ret;
+ }
+ return 0;
+}
+
+static int op_parse(struct json_object *json, struct ctlvctl_op_name *op)
+{
+ struct json_object *item;
+ int ret;
+
+ ret = json_string_dup(json, "name", &op->name);
+ if (ret)
+ return ret;
+ ret = json_u64(json, "id", &op->id);
+ if (ret)
+ return ret;
+ if (json_member(json, "type", json_type_string, &item) ||
+ op_type_parse(json_object_get_string(item), &op->type))
+ return -EINVAL;
+ if (json_member(json, "fd-access", json_type_string, &item) ||
+ fd_access_parse(json_object_get_string(item), &op->fd_access) ||
+ json_member(json, "availability", json_type_string, &item) ||
+ availability_parse(json_object_get_string(item),
+ &op->availability))
+ return -EINVAL;
+ if (json_u64(json, "request-max-len", &op->request_max_len) ||
+ json_u64(json, "action-reply-max-len",
+ &op->action_reply_max_len) ||
+ json_u64(json, "query-reply-min-len",
+ &op->query_reply_min_len) ||
+ json_u64(json, "query-reply-suggested-len",
+ &op->query_reply_suggested_len))
+ return -EINVAL;
+ return 0;
+}
+
+static int ops_parse(struct json_object *root, struct ctlvctl_spec *spec)
+{
+ struct json_object *items;
+ size_t i, n;
+ int ret;
+
+ if (json_member(root, "ops", json_type_array, &items))
+ return -EINVAL;
+ n = json_object_array_length(items);
+ spec->ops = calloc(n, sizeof(*spec->ops));
+ if (n && !spec->ops)
+ return -ENOMEM;
+ spec->ops_count = n;
+ for (i = 0; i < n; i++) {
+ ret = op_parse(json_object_array_get_idx(items, i), &spec->ops[i]);
+ if (ret)
+ return ret;
+ }
+ return 0;
+}
+
+static int chain_check(struct json_object *root, const struct ctlv_dev *dev)
+{
+ struct json_object *family, *chain;
+ size_t i, n;
+
+ if (json_member(root, "family", json_type_string, &family) ||
+ strcmp(json_object_get_string(family), ctlv_dev_family(dev)))
+ return -ENOMSG;
+ if (json_member(root, "chain", json_type_array, &chain))
+ return -EINVAL;
+ n = json_object_array_length(chain);
+ if (n != ctlv_chain_len(dev))
+ return -ENOMSG;
+
+ for (i = 0; i < n; i++) {
+ struct json_object *item, *name;
+ __u64 id;
+
+ item = json_object_array_get_idx(chain, i);
+ if (!item ||
+ json_member(item, "name", json_type_string, &name) ||
+ json_u64(item, "id", &id))
+ return -EINVAL;
+ if (id != ctlv_chain_id(dev, i) ||
+ strcmp(json_object_get_string(name),
+ ctlv_chain_name(dev, i)))
+ return -ENOMSG;
+ }
+ return 0;
+}
+
+static int manifest_parse(const char *prog, const char *text,
+ const struct ctlv_dev *dev,
+ struct ctlvctl_spec **specp)
+{
+ struct json_tokener *tokener;
+ struct json_object *root;
+ struct ctlvctl_spec *spec;
+ enum json_tokener_error error;
+ size_t text_len;
+ __u64 version;
+ int ret;
+
+ if (!text)
+ return -EINVAL;
+ text_len = strlen(text);
+ if (text_len >= INT_MAX)
+ return -E2BIG;
+ tokener = json_tokener_new();
+ if (!tokener)
+ return -ENOMEM;
+ json_tokener_set_flags(tokener, JSON_TOKENER_STRICT |
+ JSON_TOKENER_VALIDATE_UTF8);
+ root = json_tokener_parse_ex(tokener, text, text_len + 1);
+ error = json_tokener_get_error(tokener);
+ json_tokener_free(tokener);
+ if (error != json_tokener_success || !root) {
+ fprintf(stderr, "%s: invalid ctlvgen manifest: %s\n", prog,
+ json_tokener_error_desc(error));
+ return -EINVAL;
+ }
+ if (!json_object_is_type(root, json_type_object) ||
+ json_u64(root, "version", &version) || version != 1) {
+ fprintf(stderr, "%s: unsupported ctlvgen manifest\n", prog);
+ ret = -EINVAL;
+ goto out_root;
+ }
+
+ spec = calloc(1, sizeof(*spec));
+ if (!spec) {
+ ret = -ENOMEM;
+ goto out_root;
+ }
+ ret = chain_check(root, dev);
+ if (!ret)
+ ret = definitions_parse(root, spec);
+ if (!ret)
+ ret = fields_parse(root, spec);
+ if (!ret)
+ ret = ops_parse(root, spec);
+ if (ret) {
+ if (ret == -ENOMSG)
+ fprintf(stderr,
+ "%s: specifications do not match device %s\n",
+ prog, ctlv_dev_name(dev));
+ else
+ fprintf(stderr, "%s: malformed ctlvgen manifest\n",
+ prog);
+ ctlvctl_spec_free(spec);
+ goto out_root;
+ }
+ *specp = spec;
+out_root:
+ json_object_put(root);
+ return ret;
+}
+
+static int read_all(int fd, char **textp)
+{
+ size_t used = 0, size = 4096;
+ char *text;
+
+ text = malloc(size);
+ if (!text)
+ return -ENOMEM;
+ for (;;) {
+ ssize_t n;
+
+ if (used == size - 1) {
+ char *grown;
+
+ size *= 2;
+ grown = realloc(text, size);
+ if (!grown) {
+ free(text);
+ return -ENOMEM;
+ }
+ text = grown;
+ }
+ n = read(fd, text + used, size - used - 1);
+ if (n < 0) {
+ if (errno == EINTR)
+ continue;
+ free(text);
+ return -errno;
+ }
+ if (!n)
+ break;
+ used += n;
+ }
+ text[used] = '\0';
+ *textp = text;
+ return 0;
+}
+
+static void generator_exec(const char *prog, const char *spec_path)
+{
+ const char *override = getenv("CTLVGEN");
+ char executable[PATH_MAX];
+ char candidate[PATH_MAX];
+ char *slash;
+ ssize_t n;
+
+ if (override)
+ execl(override, override, "--spec", spec_path,
+ "--runtime-json", NULL);
+
+ n = readlink("/proc/self/exe", executable, sizeof(executable) - 1);
+ if (n > 0) {
+ static const char suffix[] = "/../ctlvgen/cli.py";
+ size_t len;
+
+ executable[n] = '\0';
+ slash = strrchr(executable, '/');
+ if (slash) {
+ *slash = '\0';
+ len = strlen(executable);
+ if (len + sizeof(suffix) <= sizeof(candidate)) {
+ memcpy(candidate, executable, len);
+ memcpy(candidate + len, suffix, sizeof(suffix));
+ execl(candidate, candidate, "--spec",
+ spec_path, "--runtime-json", NULL);
+ }
+ }
+ }
+ execlp("ctlvgen", "ctlvgen", "--spec", spec_path,
+ "--runtime-json", NULL);
+ dprintf(STDERR_FILENO, "%s: cannot execute ctlvgen: %s\n", prog,
+ strerror(errno));
+ _exit(127);
+}
+
+static int manifest_generate(const char *prog, const char *spec_path,
+ char **textp)
+{
+ int pipefd[2];
+ int status;
+ pid_t pid;
+ int ret;
+
+ if (pipe2(pipefd, O_CLOEXEC))
+ return -errno;
+ pid = fork();
+ if (pid < 0) {
+ ret = -errno;
+ goto out_pipe;
+ }
+ if (!pid) {
+ close(pipefd[0]);
+ if (pipefd[1] == STDOUT_FILENO) {
+ int flags = fcntl(STDOUT_FILENO, F_GETFD);
+
+ if (flags < 0 ||
+ fcntl(STDOUT_FILENO, F_SETFD,
+ flags & ~FD_CLOEXEC) < 0)
+ _exit(127);
+ } else {
+ if (dup2(pipefd[1], STDOUT_FILENO) < 0)
+ _exit(127);
+ close(pipefd[1]);
+ }
+ generator_exec(prog, spec_path);
+ }
+ close(pipefd[1]);
+ ret = read_all(pipefd[0], textp);
+ close(pipefd[0]);
+ while (waitpid(pid, &status, 0) < 0) {
+ if (errno != EINTR) {
+ ret = -errno;
+ goto out_text;
+ }
+ }
+ if (!ret && (!WIFEXITED(status) || WEXITSTATUS(status)))
+ ret = -EINVAL;
+out_text:
+ if (ret) {
+ free(*textp);
+ *textp = NULL;
+ }
+ return ret;
+out_pipe:
+ close(pipefd[0]);
+ close(pipefd[1]);
+ return ret;
+}
+
+static int manifest_read(const char *path, char **textp)
+{
+ int fd, ret;
+
+ fd = open(path, O_RDONLY | O_CLOEXEC);
+ if (fd < 0)
+ return -errno;
+ ret = read_all(fd, textp);
+ close(fd);
+ return ret;
+}
+
+int ctlvctl_spec_load(const char *prog, const char *dir,
+ const struct ctlv_dev *dev, struct ctlvctl_spec **specp)
+{
+ char path[PATH_MAX];
+ char *text = NULL;
+ int ret;
+
+ if (snprintf(path, sizeof(path), "%s/%s.json", dir,
+ ctlv_dev_family(dev)) >= (int)sizeof(path))
+ return -ENAMETOOLONG;
+ ret = manifest_read(path, &text);
+ if (ret == -ENOENT) {
+ snprintf(path, sizeof(path), "%s/%s.yaml", dir,
+ ctlv_dev_family(dev));
+ ret = manifest_generate(prog, path, &text);
+ } else if (ret) {
+ fprintf(stderr, "%s: %s: %s\n", prog, path, strerror(-ret));
+ }
+ if (ret)
+ return ret;
+ ret = manifest_parse(prog, text, dev, specp);
+ free(text);
+ return ret;
+}
+
+#else /* HAVE_JSON_C */
+
+int ctlvctl_spec_load(const char *prog, const char *dir,
+ const struct ctlv_dev *dev, struct ctlvctl_spec **specp)
+{
+ fprintf(stderr,
+ "%s: specification support was built without json-c\n", prog);
+ return -EOPNOTSUPP;
+}
+
+#endif /* HAVE_JSON_C */
+
+void ctlvctl_spec_free(struct ctlvctl_spec *spec)
+{
+ unsigned int i, j;
+
+ if (!spec)
+ return;
+ for (i = 0; i < spec->definitions_count; i++) {
+ for (j = 0; j < spec->definitions[i].values_count; j++)
+ free(spec->definitions[i].values[j].name);
+ free(spec->definitions[i].values);
+ free(spec->definitions[i].name);
+ }
+ for (i = 0; i < spec->fields_count; i++) {
+ free(spec->fields[i].name);
+ free(spec->fields[i].short_name);
+ }
+ for (i = 0; i < spec->ops_count; i++)
+ free(spec->ops[i].name);
+ free(spec->definitions);
+ free(spec->fields);
+ free(spec->ops);
+ free(spec);
+}
+
+const struct ctlvctl_op_name *
+ctlvctl_spec_op_id(const struct ctlvctl_spec *spec, __u64 id)
+{
+ unsigned int i;
+
+ if (!spec)
+ return NULL;
+ for (i = 0; i < spec->ops_count; i++)
+ if (spec->ops[i].id == id)
+ return &spec->ops[i];
+ return NULL;
+}
+
+const struct ctlvctl_op_name *
+ctlvctl_spec_op_name(const struct ctlvctl_spec *spec, const char *name)
+{
+ unsigned int i;
+
+ if (!spec)
+ return NULL;
+ for (i = 0; i < spec->ops_count; i++)
+ if (!strcmp(spec->ops[i].name, name))
+ return &spec->ops[i];
+ return NULL;
+}
+
+const struct ctlvctl_field_name *
+ctlvctl_spec_field(const struct ctlvctl_spec *spec, __u64 type)
+{
+ unsigned int i;
+
+ if (!spec)
+ return NULL;
+ for (i = 0; i < spec->fields_count; i++)
+ if (spec->fields[i].type == type)
+ return &spec->fields[i];
+ return NULL;
+}
+
+const char *ctlvctl_spec_value_name(const struct ctlvctl_field_name *field,
+ __u64 value)
+{
+ unsigned int i;
+
+ if (!field || !field->definition)
+ return NULL;
+ for (i = 0; i < field->definition->values_count; i++)
+ if (field->definition->values[i].value == value)
+ return field->definition->values[i].name;
+ return NULL;
+}
+
+int ctlvctl_spec_value(const struct ctlvctl_field_name *field, const char *name,
+ __u64 *value)
+{
+ unsigned int i;
+
+ if (!field || !field->definition)
+ return -ENOENT;
+ for (i = 0; i < field->definition->values_count; i++)
+ if (!strcmp(field->definition->values[i].name, name)) {
+ *value = field->definition->values[i].value;
+ return 0;
+ }
+ return -ENOENT;
+}
diff --git a/tools/ctlv/ctlvctl/spec.h b/tools/ctlv/ctlvctl/spec.h
new file mode 100644
index 000000000000..0ac6e314232e
--- /dev/null
+++ b/tools/ctlv/ctlvctl/spec.h
@@ -0,0 +1,60 @@
+/* SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause */
+#ifndef CTLVCTL_SPEC_H
+#define CTLVCTL_SPEC_H
+
+#include <linux/types.h>
+#include <stdbool.h>
+
+struct ctlv_dev;
+
+struct ctlvctl_value_name {
+ __u64 value;
+ char *name;
+};
+
+struct ctlvctl_definition {
+ char *name;
+ __u32 kind;
+ struct ctlvctl_value_name *values;
+ unsigned int values_count;
+};
+
+struct ctlvctl_field_name {
+ __u64 type;
+ __u32 kind;
+ char *name;
+ char *short_name;
+ const struct ctlvctl_definition *definition;
+};
+
+struct ctlvctl_op_name {
+ __u64 id;
+ __u32 type;
+ __u32 fd_access;
+ __u32 availability;
+ __u64 request_max_len;
+ __u64 action_reply_max_len;
+ __u64 query_reply_min_len;
+ __u64 query_reply_suggested_len;
+ char *name;
+};
+
+struct ctlvctl_spec;
+
+int ctlvctl_spec_load(const char *prog, const char *dir,
+ const struct ctlv_dev *dev, struct ctlvctl_spec **specp);
+void ctlvctl_spec_free(struct ctlvctl_spec *spec);
+
+const struct ctlvctl_op_name *
+ctlvctl_spec_op_id(const struct ctlvctl_spec *spec, __u64 id);
+const struct ctlvctl_op_name *
+ctlvctl_spec_op_name(const struct ctlvctl_spec *spec, const char *name);
+const struct ctlvctl_field_name *
+ctlvctl_spec_field(const struct ctlvctl_spec *spec, __u64 type);
+
+const char *ctlvctl_spec_value_name(const struct ctlvctl_field_name *field,
+ __u64 value);
+int ctlvctl_spec_value(const struct ctlvctl_field_name *field, const char *name,
+ __u64 *value);
+
+#endif /* CTLVCTL_SPEC_H */
diff --git a/tools/ctlv/ctlvgen/cli.py b/tools/ctlv/ctlvgen/cli.py
index 59180f9757e8..5da444984bd5 100755
--- a/tools/ctlv/ctlvgen/cli.py
+++ b/tools/ctlv/ctlvgen/cli.py
@@ -14,7 +14,10 @@ from collections.abc import Callable
sys.path.append(pathlib.Path(__file__).resolve().parents[1].as_posix())
from ctlvgen.lib import cgen # noqa: E402
+from ctlvgen.lib import dump # noqa: E402
+from ctlvgen.lib import gen_doc # noqa: E402
from ctlvgen.lib import gen_kernel # noqa: E402
+from ctlvgen.lib import gen_runtime # noqa: E402
from ctlvgen.lib import gen_uapi # noqa: E402
from ctlvgen.lib import gen_user # noqa: E402
from ctlvgen.lib import semantics # noqa: E402
@@ -30,6 +33,7 @@ GENERATORS: dict[tuple[str, str], Callable[..., str]] = {
('kernel', 'source'): gen_kernel.render_source,
('user', 'header'): gen_user.render_header,
('user', 'source'): gen_user.render_source,
+ ('doc', 'rst'): gen_doc.render,
}
@@ -46,6 +50,12 @@ def main() -> int:
action = parser.add_mutually_exclusive_group(required=True)
action.add_argument('--validate', action='store_true',
help='validate the specification and exit')
+ action.add_argument('--dump', action='store_true',
+ help='print the resolved schema in text form')
+ action.add_argument('--runtime-json', action='store_true',
+ help='print the resolved runtime manifest as JSON')
+ action.add_argument('--signatures', action='store_true',
+ help='print each op the family declares as a call')
action.add_argument('--outputs-check', action='store_true',
help='check every file in the output manifests and '
'every ABI snapshot')
@@ -53,7 +63,7 @@ def main() -> int:
help='regenerate every file in the output manifests '
'and every ABI snapshot')
action.add_argument('--mode',
- choices=('uapi', 'kernel', 'user'),
+ choices=('uapi', 'kernel', 'user', 'doc'),
help='generate the files of this mode')
parser.add_argument('--schema-only', action='store_true',
help='omit the family API from kernel output')
@@ -62,6 +72,8 @@ def main() -> int:
help='generate the header of --mode')
files.add_argument('--source', action='store_true',
help='generate the source of --mode')
+ files.add_argument('--rst', action='store_true',
+ help='generate the reStructuredText of --mode')
parser.add_argument('-o', '--output', type=str,
help='write to this file instead of standard output')
parser.add_argument('--cmp-out', action='store_true',
@@ -73,12 +85,12 @@ def main() -> int:
help='let --outputs-update break the committed ABI')
args = parser.parse_args()
- file = next((name for name in ('header', 'source')
+ file = next((name for name in ('header', 'source', 'rst')
if getattr(args, name)), None)
if args.mode and not file:
- parser.error('--mode needs --header or --source')
+ parser.error('--mode needs --header, --source or --rst')
if file and not args.mode:
- parser.error('--header and --source need --mode')
+ parser.error('--header, --source and --rst need --mode')
if args.mode and (args.mode, file) not in GENERATORS:
parser.error(f'--mode {args.mode} cannot generate --{file}')
if args.schema_only and args.mode != 'kernel':
@@ -111,20 +123,43 @@ def main() -> int:
# The root family has no binding: libctlv is that binding.
if family.name != spec.CORE_FAMILY_NAME:
cgen.user_symbols(family)
+ elif args.dump:
+ sys.stdout.write(dump.render(family))
+ elif args.runtime_json:
+ sys.stdout.write(gen_runtime.render(family))
+ elif args.signatures:
+ sys.stdout.write(gen_doc.signatures(family))
else:
assert file is not None
- _generate(args, file, family)
+ _generate(args, paths, file, family)
except CtlvError as exc:
print(f'ctlvgen: {exc}', file=sys.stderr)
return 1
return 0
-def _generate(args: argparse.Namespace, file: str,
+def _descendants(args: argparse.Namespace, paths: spec.Paths,
+ family: semantics.Family) -> tuple[semantics.Family, ...]:
+ """Every family of the registry that inherits family."""
+ found = []
+ for entry in spec.load_registry(paths).entries:
+ path = paths.spec_dir / f'{entry.name}.yaml'
+ if entry.name == family.name or not path.exists():
+ continue
+ child = semantics.load(spec.Paths(str(path), schema=args.schema,
+ registry=args.registry))
+ if family.name in (item.name for item in child.chain[:-1]):
+ found.append(child)
+ return tuple(found)
+
+
+def _generate(args: argparse.Namespace, paths: spec.Paths, file: str,
family: semantics.Family) -> None:
render = GENERATORS[(args.mode, file)]
if args.mode == 'kernel':
text = render(family, family_api=not args.schema_only)
+ elif args.mode == 'doc':
+ text = render(family, _descendants(args, paths, family))
else:
text = render(family)
if args.output is None:
diff --git a/tools/ctlv/ctlvgen/lib/dump.py b/tools/ctlv/ctlvgen/lib/dump.py
new file mode 100644
index 000000000000..20eca794aa11
--- /dev/null
+++ b/tools/ctlv/ctlvgen/lib/dump.py
@@ -0,0 +1,161 @@
+# SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+"""Deterministic text rendering of an effective family.
+
+A debugging window and a golden-test surface, not an interchange format: every
+generated ID, effective bound and computed length, in declaration order.
+"""
+
+from __future__ import annotations
+
+from . import semantics
+from . import sizes
+from . import spec
+
+INDENT: str = ' '
+
+
+def render(family: semantics.Family) -> str:
+ """Return the dump of one effective family as a string."""
+ lines = []
+ lines.append(f'family: {family.name}')
+ lines.append(f'family-id: {family.family_id}')
+ lines.append('chain: ' + ' -> '.join(item.name for item in family.chain))
+
+ _definitions(lines, family)
+ _records(lines, family)
+ _extensions(lines, family)
+ _ops(lines, family)
+ return '\n'.join(lines) + '\n'
+
+
+def _definitions(lines: list[str], family: semantics.Family) -> None:
+ if not family.definitions:
+ return
+ lines.append('')
+ lines.append('definitions:')
+ for definition in family.definitions.values():
+ extension = definition.extension
+ what = ''
+ if extension is not None:
+ what = f', extension {extension.ext_id} of family ' \
+ f'{extension.family_id}'
+ lines.append(f'{INDENT}{definition.kind} {definition.name} '
+ f'(family {definition.family_id}{what})')
+ for name, number in definition.values:
+ if definition.kind == 'enum':
+ lines.append(f'{INDENT * 2}{name} = {number}')
+ else:
+ lines.append(f'{INDENT * 2}{name} = bit {number}')
+
+
+def _records(lines: list[str], family: semantics.Family) -> None:
+ if not family.records:
+ return
+ lines.append('')
+ lines.append('records:')
+ for record in family.records.values():
+ lines.append(f'{INDENT}{record.path} = {record.record_id:#010x} '
+ f'{record.role} (family {record.family_id})')
+ for member in record.members:
+ field = member.field
+ requirement = '' if member.required else '?'
+ lines.append(f'{INDENT * 2}{field.name}{requirement} = '
+ f'{_id(field.type)} {_field_detail(field)}')
+
+
+def _field_detail(field: semantics.Field) -> str:
+ decl = field.decl
+ detail = [decl.kind]
+ if decl.kind in ('uint', 'sint'):
+ detail.append(f'[{decl.min}, {decl.max}]')
+ if decl.unit is not None:
+ detail.append(f'unit {decl.unit} scale {decl.scale}')
+ elif decl.kind in ('string', 'binary'):
+ detail.append(f'length [{decl.min_length}, {decl.max_length}]')
+ elif decl.kind in ('enum', 'flags'):
+ assert decl.ref is not None
+ detail.append(decl.ref)
+ elif decl.kind == 'blob':
+ assert decl.blob is not None
+ detail.append(decl.blob.direction)
+ detail.append('backings ' + ','.join(decl.blob.backings))
+ detail.append(f'max-length {decl.blob.max_length}')
+ detail.append(f'alignment {decl.blob.alignment}')
+ elif decl.extension is not None:
+ detail.append(f'extension {decl.extension}')
+ else:
+ detail.append(f'record {field.holds}')
+ detail.append(_occurrences(decl.max_occurrences))
+ return ' '.join(detail)
+
+
+def _extensions(lines: list[str], family: semantics.Family) -> None:
+ if not family.extensions:
+ return
+ lines.append('')
+ lines.append('extensions:')
+ for extension in family.extensions.values():
+ fill = family.fill_of(extension)
+ filled = f', filled by family {fill.owner_family_id}' if fill else ''
+ lines.append(f'{INDENT}{extension.name} = {extension.ext_id} '
+ f'(family {extension.family_id}{filled})')
+
+
+def _ops(lines: list[str], family: semantics.Family) -> None:
+ if not family.ops:
+ return
+ lines.append('')
+ lines.append('ops:')
+ for op in family.ops:
+ lines.append(f'{INDENT}{op.name} = {_id(op.op_id)} {op.type}')
+ lines.append(f'{INDENT * 2}fd-access: {op.fd_access}')
+ if op.request is not None:
+ _tree(lines, 'request', op.request)
+ if op.reply is None:
+ lines.append(f'{INDENT * 2}reply: none')
+ else:
+ _tree(lines, 'reply', op.reply)
+ _sizes(lines, op.sizes)
+
+
+def _tree(lines: list[str], what: str, root: semantics.Node) -> None:
+ lines.append(f'{INDENT * 2}{what}:')
+ if not root.children():
+ lines.append(f'{INDENT * 3}(empty)')
+ return
+ _children(lines, root, 3)
+
+
+def _children(lines: list[str], node: semantics.Node, depth: int) -> None:
+ for child in node.children():
+ requirement = 'required' if child.required else 'optional'
+ line = (f'{INDENT * depth}{child.child_field.full_name} '
+ f'{_occurrences(child.max_occurrences)} {requirement}')
+ if child.reenters is not None:
+ line += f' -> {child.reenters}'
+ lines.append(line)
+ _children(lines, child, depth + 1)
+
+
+def _sizes(lines: list[str], op_sizes: sizes.OpSizes) -> None:
+ lines.append(f'{INDENT * 2}sizes:')
+ reported = (
+ ('request-max-len', op_sizes.request_max_len),
+ ('action-reply-max-len', op_sizes.action_reply_max_len),
+ ('query-reply-min-len', op_sizes.query_reply_min_len),
+ ('query-reply-suggested-len', op_sizes.query_reply_suggested_len),
+ ('error-info-max-len', op_sizes.error_info_max_len),
+ )
+ for name, value in reported:
+ if value:
+ lines.append(f'{INDENT * 3}{name}: {value}')
+
+
+def _occurrences(max_occurrences: int) -> str:
+ if max_occurrences == spec.UNLIMITED:
+ return 'x*'
+ return f'x{max_occurrences}'
+
+
+def _id(value: int) -> str:
+ return f'0x{value:016x}'
diff --git a/tools/ctlv/ctlvgen/lib/gen_doc.py b/tools/ctlv/ctlvgen/lib/gen_doc.py
new file mode 100644
index 000000000000..7d36d4a7dd9b
--- /dev/null
+++ b/tools/ctlv/ctlvgen/lib/gen_doc.py
@@ -0,0 +1,684 @@
+# SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+"""The reference documentation of one family.
+
+Each op's request and reply as the tree the core admits, with every field's
+number, schema kind, bounds and doc, and what each descendant puts into an
+extension point shown where it lands. An inherited declaration is documented
+by the family that owns it.
+"""
+
+from __future__ import annotations
+
+import textwrap
+from typing import Union
+
+from . import cgen
+from . import semantics
+from . import spec
+
+# Documentation/doc-guide/sphinx.rst asks for lines that fit a terminal, which
+# is why a generated tree is an indented literal block.
+LINE_LIMIT: int = 79
+
+# A literal block's indent, the columns of one tree row inside it, and how far
+# in from its row a doc goes.
+BLOCK_INDENT: int = 4
+NAME_WIDTH: int = 30
+NUMBER_WIDTH: int = 4
+DOC_INDENT: int = 4
+
+TYPES: dict[str, str] = {
+ 'action': 'action',
+ 'query': 'query',
+}
+
+# A blob's direction as a spec writes it.
+DIRECTIONS: dict[str, str] = {value: key
+ for key, value in spec.DIRECTIONS.items()}
+
+
+def render(family: semantics.Family,
+ descendants: tuple[semantics.Family, ...] = ()) -> str:
+ """Return the reference page of family as reStructuredText.
+
+ descendants are the families inheriting this one, whose fills appear in
+ its trees.
+ """
+ writer = _Writer()
+ _preamble(writer, family)
+ _title(writer, family)
+ _intro(writer, family)
+ _definitions(writer, family)
+ _records(writer, family, descendants)
+ _extensions(writer, family, descendants)
+ _fills(writer, family, descendants)
+ _ops(writer, family, descendants)
+ _messages(writer, family, descendants)
+ return writer.text()
+
+
+def signatures(family: semantics.Family, width: int = 100) -> str:
+ """Return every op this family declares as one call per line."""
+ lines = []
+ for op in family.local_ops:
+ lines += _signature(family, op, width)
+ return '\n'.join(lines) + '\n' if lines else ''
+
+
+class _Writer:
+ """The lines of one generated page."""
+
+ def __init__(self) -> None:
+ self._lines: list[str] = []
+
+ def line(self, text: str = '') -> None:
+ self._lines.append(text.rstrip())
+
+ def lines(self, *texts: str) -> None:
+ for text in texts:
+ self.line(text)
+
+ def paragraph(self, text: str) -> None:
+ """One paragraph, wrapped, followed by a blank line.
+
+ A hyphenated family name is never broken, so a grep for one finds it.
+ """
+ for line in _wrap(text, LINE_LIMIT):
+ self.line(line)
+ self.line()
+
+ def title(self, text: str) -> None:
+ """The one title of the page, overlined and underlined."""
+ self.line('=' * len(text))
+ self.line(text)
+ self.line('=' * len(text))
+ self.line()
+
+ def heading(self, text: str, char: str) -> None:
+ """One section heading, underlined the width of its title."""
+ self.line(text)
+ self.line(char * len(text))
+ self.line()
+
+ def block(self, *texts: str) -> None:
+ """One literal block, indented, followed by a blank line."""
+ self.line('::')
+ self.line()
+ for text in texts:
+ self.line(f'{" " * BLOCK_INDENT}{text}' if text else '')
+ self.line()
+
+ def field(self, name: str, value: int | str) -> None:
+ self.line(f':{name}: {value}')
+
+ def text(self) -> str:
+ while self._lines and not self._lines[-1]:
+ self._lines.pop()
+ return '\n'.join(self._lines) + '\n'
+
+
+def _wrap(text: str, width: int) -> list[str]:
+ return textwrap.wrap(' '.join(text.split()), width,
+ break_on_hyphens=False, break_long_words=False)
+
+
+def _preamble(writer: _Writer, family: semantics.Family) -> None:
+ """The comment ctlv-regen.sh recovers the generation parameters from."""
+ writer.lines(
+ f'.. SPDX-License-Identifier: {family.license}',
+ f'.. Generated from {cgen.spec_path(family.path)} by',
+ f' {cgen.REGEN}. Do not edit.',
+ '.. CTLV-GEN doc rst',
+ )
+ writer.line()
+
+
+def _title(writer: _Writer, family: semantics.Family) -> None:
+ writer.title(family.name)
+
+
+def _intro(writer: _Writer, family: semantics.Family) -> None:
+ writer.field('Family ID', family.family_id)
+ writer.field('Chain', ' -> '.join(item.name for item in family.chain))
+ writer.field('Specification', cgen.spec_path(family.path))
+ writer.line()
+ if family.doc:
+ writer.paragraph(family.doc)
+ if family.parent:
+ writer.paragraph(
+ f'This family inherits ``{family.parent}`` and its page describes '
+ f'what ``{family.name}`` declares itself, together with the '
+ 'extension points it fills. An inherited op or field keeps the '
+ 'name and the description its own family gives it, so a program '
+ 'speaking to a device of this family uses both pages.')
+ else:
+ writer.paragraph(
+ 'This is the family every other family inherits: its ops are the '
+ 'ones the core answers itself, on every device, whatever '
+ 'family registered it.')
+ writer.paragraph(
+ 'A tree lists one field per line: its name as the specification '
+ 'writes it, its number and its schema kind. A field\'s type is the '
+ 'family, the record above it and that number. A field of records: '
+ 'is a record of its own, which its line names.')
+
+
+def _definitions(writer: _Writer, family: semantics.Family) -> None:
+ own = family.local_definitions
+ if not own:
+ return
+ writer.heading('Enumerations and flags', '=')
+ for definition in own:
+ writer.heading(definition.name, '-')
+ if definition.doc:
+ writer.paragraph(definition.doc)
+ if definition.is_extension_point:
+ writer.paragraph(
+ 'An enum extension point: the family of a device gives the '
+ 'values, by declaring an enum of this name. On a device '
+ 'whose family gives none, no value is valid.')
+ continue
+ if definition.extension is not None:
+ owner = family.owners[definition.extension.family_id]
+ writer.paragraph(f'The values this family gives the enum '
+ f'extension point of {owner}.')
+ if definition.kind == 'enum':
+ writer.paragraph(
+ 'A payload restricted to these values. An unknown value is '
+ 'refused on a request and tolerated on a reply, so a caller '
+ 'built against an older specification keeps working.')
+ header = definition.header
+ if header:
+ before = f' before ``{header.end}``' if header.end else ''
+ writer.paragraph(
+ f'These are the constants of ``enum {header.tag}`` in '
+ f'``<{header.path}>``{before}, under its names and '
+ 'numbers. A number between two of them passes CTLV, for '
+ 'the family to refuse.')
+ rows = _value_rows(definition, '')
+ else:
+ writer.paragraph(
+ 'A payload restricted to these bits. An unknown bit is '
+ 'refused on a request and tolerated on a reply.')
+ rows = _value_rows(definition, 'bit ')
+ writer.block(*rows)
+
+
+def _value_rows(definition: semantics.Definition, prefix: str) -> list[str]:
+ """One line per value, its doc below."""
+ rows = []
+ width = LINE_LIMIT - BLOCK_INDENT - DOC_INDENT
+ for (name, number), doc in zip(definition.values, definition.value_docs):
+ rows.append(f'{prefix}{number} = {name}')
+ if doc:
+ rows += [f'{" " * DOC_INDENT}{line}' for line in _wrap(doc, width)]
+ return rows
+
+
+def _records(writer: _Writer, family: semantics.Family,
+ descendants: tuple[semantics.Family, ...]) -> None:
+ """The named records and the fields of records: this family declares."""
+ own = [record for record in family.local_records
+ if record.role in ('named', 'field')]
+ if not own:
+ return
+ writer.heading('Records', '=')
+ writer.paragraph(
+ 'What an op places by name. A named record is placed with record or '
+ 'spliced with $include; a field of records: is placed with a bare '
+ 'record and has one type wherever it is.')
+ for record in own:
+ writer.heading(record.name, '-')
+ if record.doc:
+ writer.paragraph(record.doc)
+ if record.role == 'field':
+ field = record.fields[0]
+ writer.block(f'record {record.record_id:#010x}, field '
+ f'{field.number}: {_kind_text(family, field)}')
+ continue
+ rows = [f'record {record.record_id:#010x}', '']
+ rows += _record_rows(family, descendants, record, 0, (record.name,))
+ writer.block(*rows)
+
+
+def _record_rows(family: semantics.Family,
+ descendants: tuple[semantics.Family, ...],
+ record: semantics.Record, depth: int,
+ opened: tuple[str, ...] = ()) -> list[str]:
+ """One line per member of a record, in spec order, records expanded.
+
+ opened are the named records above, which a record of one re-enters.
+ """
+ rows = []
+ included = None
+ for member in record.members:
+ field = member.field
+ origin = None
+ if field.record_id != record.record_id and not field.shared:
+ origin = field.record
+ if origin is not None and origin != included:
+ source = family.records[origin]
+ rows.append(f'{" " * depth}$include {origin}, record '
+ f'{source.record_id:#010x}')
+ included = origin
+ inner, reenters = None, False
+ if field.decl.kind == 'record' and field.decl.extension is None:
+ assert field.holds is not None
+ inner = family.records[field.holds]
+ reenters = inner.role == 'named' and inner.name in opened
+ name = field.name + _suffix(member.required, field.decl)
+ rows += _row(family, name, field, depth, _doc(family, member),
+ reenters)
+ if inner is not None and not reenters:
+ below = opened + ((inner.name,) if inner.role == 'named'
+ else ())
+ rows += _record_rows(family, descendants, inner, depth + 1,
+ below)
+ if field.decl.extension is not None:
+ rows += _fill_rows(family, descendants,
+ family.extensions[field.decl.extension],
+ depth + 1)
+ return rows
+
+
+def _doc(family: semantics.Family, member: semantics.Member) -> str | None:
+ """A member's doc: where it is placed, else where it is declared."""
+ if member.doc:
+ return member.doc
+ if member.field.decl.doc:
+ return member.field.decl.doc
+ if member.field.shared:
+ return family.records[member.field.record].doc
+ return None
+
+
+def _extensions(writer: _Writer, family: semantics.Family,
+ descendants: tuple[semantics.Family, ...]) -> None:
+ own = family.local_extensions
+ if not own:
+ return
+ writer.heading('Extension points', '=')
+ writer.paragraph(
+ 'A place a descendant adds members to, by declaring a record of the '
+ 'same name. It is empty on this family\'s own devices; a descendant '
+ 'that fills it lengthens every message that reaches it.')
+ rows = []
+ for extension in own:
+ fillers = []
+ for child in descendants:
+ fill = child.fill_of(extension)
+ if fill is not None:
+ fillers.append(child.owners[fill.owner_family_id])
+ filled = ', filled by ' + ', '.join(fillers) if fillers else ''
+ rows.append(f'{extension.name}, ID {extension.ext_id}{filled}')
+ writer.block(*rows)
+
+
+def _fills(writer: _Writer, family: semantics.Family,
+ descendants: tuple[semantics.Family, ...]) -> None:
+ if not family.fills:
+ return
+ writer.heading('Extension points this family fills', '=')
+ writer.paragraph(
+ 'Each of these completes an extension point an ancestor declared. '
+ 'What it adds appears in every message of every op that reaches that '
+ 'point, so an inherited op admits a longer message on a device of '
+ 'this family than on a device of the family that declared it.')
+ for fill in family.fills:
+ owner = family.owners.get(fill.family_id, str(fill.family_id))
+ writer.heading(f'{fill.name} of {owner}', '-')
+ if fill.record.doc:
+ writer.paragraph(fill.record.doc)
+ rows = [f'record {fill.record.record_id:#010x}', '']
+ rows += _record_rows(family, descendants, fill.record, 0)
+ writer.block(*rows)
+
+
+def _ops(writer: _Writer, family: semantics.Family,
+ descendants: tuple[semantics.Family, ...]) -> None:
+ own = family.local_ops
+ if not own:
+ return
+ writer.heading('Ops', '=')
+ writer.paragraph(
+ 'One section per op this family declares. An action is executed and '
+ 'acknowledged, and a query is executed and answered in as many '
+ 'fragments as its answer needs. In short:')
+ lines = []
+ for op in own:
+ lines += _signature(family, op, LINE_LIMIT - BLOCK_INDENT)
+ writer.block(*lines)
+ for op in own:
+ _op(writer, family, descendants, op)
+
+
+def _op(writer: _Writer, family: semantics.Family,
+ descendants: tuple[semantics.Family, ...], op: semantics.Op) -> None:
+ writer.heading(op.name, '-')
+ writer.field('Op ID', f'``{_id(op.op_id)}``, op {op.local_id}')
+ writer.field('Type', TYPES[op.type])
+ writer.field('FD access', _fd_access(op.fd_access))
+ writer.field('Availability', _availability(op.availability))
+ writer.line()
+ if op.doc:
+ writer.paragraph(op.doc)
+ _message(writer, family, descendants, 'Request', op.request_record)
+ _message(writer, family, descendants, 'Reply', op.reply_record)
+ _sizes(writer, op)
+
+
+def _availability(availability: str) -> str:
+ if availability == 'dynamic':
+ return 'dynamic, so a device may disable and enable it at any time'
+ return 'fixed, so a device that publishes it always answers it'
+
+
+def _fd_access(fd_access: str) -> str:
+ if fd_access == 'read':
+ return 'read, so an observation or full-control fd may execute it'
+ return 'read-write, so only a full-control fd may execute it'
+
+
+def _message(writer: _Writer, family: semantics.Family,
+ descendants: tuple[semantics.Family, ...], what: str,
+ record: semantics.Record | None) -> None:
+ if record is None:
+ writer.paragraph(f'{what}: none.')
+ return
+ if record.role == 'named':
+ writer.paragraph(f'{what}: record {record.name}, '
+ f'{record.record_id:#010x}, as Records has it.')
+ return
+ if record.doc:
+ writer.paragraph(record.doc)
+ rows = _record_rows(family, descendants, record, 0)
+ if not rows:
+ writer.paragraph(f'{what}: an empty root, record '
+ f'{record.record_id:#010x}.')
+ return
+ writer.paragraph(f'{what}, record {record.record_id:#010x}:')
+ writer.block(*rows)
+
+
+def _fill_rows(family: semantics.Family,
+ descendants: tuple[semantics.Family, ...],
+ extension: semantics.Extension, depth: int) -> list[str]:
+ """What each family below puts into one extension point."""
+ rows = []
+ for child in (family, *descendants):
+ fill = child.fill_of(extension)
+ if fill is None or (child is not family and
+ fill.owner_family_id != child.family_id):
+ continue
+ owner = child.owners[fill.owner_family_id]
+ rows.append(f'{" " * depth}{owner}: record '
+ f'{fill.record.record_id:#010x} of family '
+ f'{fill.owner_family_id}')
+ rows += _record_rows(child, descendants, fill.record, depth + 1)
+ return rows
+
+
+def _row(family: semantics.Family, name: str, field: semantics.Field,
+ depth: int, doc: str | None, reenters: bool = False) -> list[str]:
+ """One field: name, number and schema kind, its doc below."""
+ indent = ' ' * depth
+ label = f'{indent}{name}'
+ number = str(field.number)
+ what = _kind_text(family, field, reenters)
+ if field.shared:
+ what += f', record {field.record_id:#010x}'
+ column = NAME_WIDTH + NUMBER_WIDTH
+ parts = _wrap(what, LINE_LIMIT - BLOCK_INDENT - column) or ['']
+ if len(label) >= NAME_WIDTH:
+ rows = [label, f'{" " * NAME_WIDTH}{number:<{NUMBER_WIDTH}}{parts[0]}']
+ else:
+ rows = [f'{label:<{NAME_WIDTH}}{number:<{NUMBER_WIDTH}}{parts[0]}']
+ rows += [f'{" " * column}{part}' for part in parts[1:]]
+ if doc:
+ doc_indent = indent + ' ' * DOC_INDENT
+ width = LINE_LIMIT - BLOCK_INDENT - len(doc_indent)
+ rows += [f'{doc_indent}{line}' for line in _wrap(doc, width)]
+ return rows
+
+
+def _suffix(required: bool, decl: spec.FieldDecl) -> str:
+ """The [N] and ? a spec writes after a field name."""
+ suffix = ''
+ if decl.count_enum is not None:
+ suffix = f'[{decl.count_enum}]'
+ elif decl.max_occurrences == spec.UNLIMITED:
+ suffix = '[]'
+ elif decl.max_occurrences != 1:
+ suffix = f'[{decl.max_occurrences}]'
+ return suffix + ('' if required else '?')
+
+
+def _kind_text(family: semantics.Family, field: semantics.Field,
+ reenters: bool = False) -> str:
+ """A field's schema kind and bounds, as a spec reads."""
+ decl = field.decl
+ if decl.kind in ('uint', 'sint'):
+ return decl.kind + _bounds(_integer_bounds(decl), _unit(decl))
+ if decl.kind == 'bool':
+ return 'bool'
+ if decl.kind in ('string', 'binary'):
+ return decl.kind + _bounds(_length_bounds(decl), ())
+ if decl.kind == 'enum':
+ assert decl.ref is not None
+ return decl.ref
+ if decl.kind == 'flags':
+ assert decl.ref is not None
+ names = ', '.join(name for name, _ in
+ family.definitions[decl.ref].values)
+ return f'{decl.ref}: {names}'
+ if decl.kind == 'blob':
+ assert decl.blob is not None
+ return 'blob' + _bounds((), _blob_parts(decl.blob))
+ if decl.extension is not None:
+ return f'extension {decl.extension}'
+ assert field.holds is not None
+ record = family.records[field.holds]
+ what = f'record {record.record_id:#010x}'
+ if record.role == 'named':
+ what = f'record {record.name} {record.record_id:#010x}'
+ if reenters:
+ what += ', which this re-enters'
+ return what
+
+
+def _bounds(range_parts: tuple[str, ...], other_parts: tuple[str, ...]) -> str:
+ parts = list(range_parts) + list(other_parts)
+ return f' {", ".join(parts)}' if parts else ''
+
+
+def _integer_bounds(decl: spec.FieldDecl) -> tuple[str, ...]:
+ low = None if decl.min == _default_min(decl.kind) else decl.min
+ high = None if decl.max == _default_max(decl.kind) else decl.max
+ if low is None and high is None:
+ return ()
+ text = spec.size_text if decl.unit == 'bytes' else spec.number_text
+ low_text = text(low) if low is not None else ''
+ high_text = text(high) if high is not None else ''
+ return (f'{low_text}..{high_text}',)
+
+
+def _default_min(kind: str) -> int:
+ return 0 if kind == 'uint' else spec.S64_MIN
+
+
+def _default_max(kind: str) -> int:
+ return spec.U64_MAX if kind == 'uint' else spec.S64_MAX
+
+
+def _unit(decl: spec.FieldDecl) -> tuple[str, ...]:
+ if decl.unit is None:
+ return ()
+ parts = [decl.unit]
+ if decl.scale:
+ parts.append(f'scale {decl.scale}')
+ return tuple(parts)
+
+
+def _length_bounds(decl: spec.FieldDecl) -> tuple[str]:
+ assert decl.min_length is not None and decl.max_length is not None
+ if decl.min_length == decl.max_length:
+ return (spec.size_text(decl.max_length),)
+ low = spec.size_text(decl.min_length) if decl.min_length else ''
+ return (f'{low}..{spec.size_text(decl.max_length)}',)
+
+
+def _blob_parts(blob: spec.BlobDecl) -> tuple[str, ...]:
+ parts = [DIRECTIONS[blob.direction]]
+ if blob.max_length != spec.U32_MAX:
+ parts.append(f'..{spec.size_text(blob.max_length)}')
+ if blob.alignment != 1:
+ parts.append(f'align {spec.size_text(blob.alignment)}')
+ parts.append(' '.join(blob.backings))
+ return tuple(parts)
+
+
+def _sizes(writer: _Writer, op: semantics.Op) -> None:
+ """What a caller has to bring and what it gets back, in bytes."""
+ reported = (
+ ('a schema-known request of at most {} bytes',
+ op.sizes.request_max_len),
+ ('a reply of at most {} bytes', op.sizes.action_reply_max_len),
+ ('a reply buffer of at least {} bytes',
+ op.sizes.query_reply_min_len),
+ ('{} bytes of reply buffer suggested',
+ op.sizes.query_reply_suggested_len),
+ )
+ rows = [label.format(value) for label, value in reported if value]
+ if not rows:
+ return
+ writer.paragraph('Lengths on a device of this family:')
+ writer.block(*rows)
+
+
+def _messages(writer: _Writer, family: semantics.Family,
+ descendants: tuple[semantics.Family, ...]) -> None:
+ if not family.messages:
+ return
+ writer.heading('Messages no op declares', '=')
+ writer.paragraph(
+ 'The core writes these itself, so they belong to no op: a '
+ 'caller reads one out of the buffer it offered for a reply.')
+ for message in family.messages:
+ writer.heading(message.name, '-')
+ record = family.records[message.name]
+ writer.block(f'record {message.record_id:#010x}', '',
+ *_record_rows(family, descendants, record, 0,
+ (record.name,)))
+ writer.paragraph(f'At most {message.max_len} bytes, bounded by '
+ f'{message.limit}.')
+
+
+def _signature(family: semantics.Family, op: semantics.Op,
+ width: int) -> list[str]:
+ """One op as a call: what it takes, and what it answers."""
+ request = _members(family, op.request_record) if op.request_record \
+ else []
+ call = (f'{op.name}(', request, ')')
+ if op.reply_record is None:
+ return _layout((call,), width)
+ return _layout((call, ' -> ', _shape(family, op.reply_record)), width)
+
+
+_Piece = Union[str, tuple[str, list['_Piece'], str]]
+
+
+def _shape(family: semantics.Family, record: semantics.Record) -> _Piece:
+ """A message root: a named record by name, else its members."""
+ if record.role == 'named':
+ return record.name
+ members = _members(family, record)
+ return ('{ ', members, ' }') if members else '{}'
+
+
+def _layout(pieces: tuple[_Piece, ...], width: int) -> list[str]:
+ """One signature, wrapped inside its braces where it is too long.
+
+ A piece is text or a group, an opener, its members and a closer. A member
+ that does not fit goes on a line of its own, below the first one; a reply
+ that does not fit goes on an indented line.
+ """
+ lines = ['']
+ for piece in pieces:
+ if piece == ' -> ':
+ lines[-1] += piece
+ continue
+ if lines[-1].endswith(' -> ') and \
+ len(lines[-1]) + len(_flat(piece)) > width:
+ lines[-1] = lines[-1].rstrip()
+ lines.append(' ')
+ _place(piece, lines, width)
+ return lines
+
+
+def _place(item: _Piece, lines: list[str], width: int) -> None:
+ """Append item to lines, wrapping inside it where it does not fit."""
+ flat = _flat(item)
+ if isinstance(item, str) or len(lines[-1]) + len(flat) <= width:
+ lines[-1] += flat
+ return
+ opener, members, closer = item
+ lines[-1] += opener
+ inner = len(lines[-1])
+ for index, member in enumerate(members):
+ tail = closer if index == len(members) - 1 else ','
+ if index:
+ if len(lines[-1]) + 1 + len(_flat(member)) + len(tail) <= width:
+ lines[-1] += ' '
+ else:
+ lines.append(' ' * inner)
+ _place(member, lines, width - len(tail))
+ lines[-1] += tail
+
+
+def _flat(item: _Piece) -> str:
+ if isinstance(item, str):
+ return item
+ opener, members, closer = item
+ return opener + ', '.join(_flat(member) for member in members) + closer
+
+
+def _members(family: semantics.Family,
+ record: semantics.Record) -> list[_Piece]:
+ """Each member of one record, a record written out in place."""
+ parts: list[_Piece] = []
+ included = None
+ for member in record.members:
+ field = member.field
+ origin = None
+ if field.record_id != record.record_id and not field.shared:
+ origin = field.record
+ if origin is not None:
+ if origin != included:
+ parts.append(origin)
+ included = origin
+ continue
+ included = None
+ name = field.name + _suffix(member.required, field.decl)
+ parts.append(_member(family, field, name))
+ return parts
+
+
+def _member(family: semantics.Family, field: semantics.Field,
+ name: str) -> _Piece:
+ decl = field.decl
+ if decl.kind == 'blob':
+ assert decl.blob is not None
+ return f'{name}: {DIRECTIONS[decl.blob.direction]} blob'
+ if decl.kind != 'record':
+ return name
+ if decl.extension is not None:
+ return f'{name}: {decl.extension}'
+ assert field.holds is not None
+ record = family.records[field.holds]
+ if record.role == 'named':
+ return f'{name}: {record.name}'
+ return (f'{name}: {{ ', _members(family, record), ' }')
+
+
+def _id(value: int) -> str:
+ return f'0x{value:016x}'
diff --git a/tools/ctlv/ctlvgen/lib/gen_runtime.py b/tools/ctlv/ctlvgen/lib/gen_runtime.py
new file mode 100644
index 000000000000..bfb65d7d7b54
--- /dev/null
+++ b/tools/ctlv/ctlvgen/lib/gen_runtime.py
@@ -0,0 +1,66 @@
+# SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+"""Resolved names and values consumed by specification-aware tools."""
+
+from __future__ import annotations
+
+import json
+from typing import Any
+
+from . import semantics
+
+
+def _definition(item: semantics.Definition) -> dict[str, Any]:
+ values = []
+ for name, number in item.values:
+ value = 1 << number if item.kind == 'flags' else number
+ values.append({'name': name, 'value': value})
+ return {
+ 'name': item.name,
+ 'kind': item.kind,
+ 'values': values,
+ }
+
+
+def _field(item: semantics.Field) -> dict[str, str]:
+ out = {
+ 'name': item.full_name,
+ 'short': item.name,
+ 'type': f'0x{item.type:016x}',
+ 'kind': item.decl.kind,
+ }
+ if item.decl.kind in ('enum', 'flags'):
+ assert item.decl.ref is not None
+ out['definition'] = item.decl.ref
+ return out
+
+
+def _op(item: semantics.Op) -> dict[str, Any]:
+ return {
+ 'name': item.name,
+ 'id': f'0x{item.op_id:016x}',
+ 'type': item.type,
+ 'availability': item.availability,
+ 'request-max-len': item.sizes.request_max_len,
+ 'action-reply-max-len': item.sizes.action_reply_max_len,
+ 'query-reply-min-len': item.sizes.query_reply_min_len,
+ 'query-reply-suggested-len':
+ item.sizes.query_reply_suggested_len,
+ 'error-info-max-len': item.sizes.error_info_max_len,
+ 'fd-access': item.fd_access,
+ }
+
+
+def render(family: semantics.Family) -> str:
+ """Return a versioned manifest for one effective family."""
+ chain = [{'name': name, 'id': family_id}
+ for family_id, name in family.owners.items()]
+ data = {
+ 'version': 1,
+ 'family': family.name,
+ 'chain': chain,
+ 'definitions': [_definition(item)
+ for item in family.definitions.values()],
+ 'fields': [_field(item) for item in family.fields.values()],
+ 'ops': [_op(item) for item in family.ops],
+ }
+ return json.dumps(data, separators=(',', ':')) + '\n'
--
2.56.0
^ permalink raw reply related [flat|nested] 24+ messages in thread
* Re: [PATCH RFC v2 13/15] Documentation: ctlv: Describe how to write a family
2026-11-13 15:55 ` [PATCH RFC v2 13/15] Documentation: ctlv: Describe how to write a family Jiri Pirko
@ 2026-11-13 16:13 ` sashiko-bot
0 siblings, 0 replies; 24+ messages in thread
From: sashiko-bot @ 2026-11-13 16:13 UTC (permalink / raw)
To: Jiri Pirko; +Cc: kvm
Thank you for your contribution! Sashiko AI review found 2 potential
issues to consider:
- [Low] docs: ctlv: direct -ESTALE return in query example skips
error-info
- [Low] docs: ctlv: inaccurate claim that unfilled extension adds no
record
--
commit 37d8b6224a7e92519379e383dd7fb525d99c7126
Author: Jiri Pirko <jiri@resnulli.us>
Documentation: ctlv: Describe how to write a family
This commit adds driver-api documentation explaining how to define a CTLV
family specification in YAML and implement its kernel callbacks.
> diff --git a/Documentation/driver-api/ctlv.rst b/Documentation/driver-api/ctlv.rst
> new file mode 100644
> index 0000000000000..34d20c14a9bfa
> --- /dev/null
> +++ b/Documentation/driver-api/ctlv.rst
> @@ -0,0 +1,716 @@
[ ... ]
> + int ctlv_example_item_get_query(struct ctlv *ctlv,
> + struct ctlv_op_ctx *ctx,
> + const struct ctlv_cursor *request,
> + struct ctlv_cursor *reply)
> + {
> + struct example_device *example = to_example(ctlv);
> + u64 generation = READ_ONCE(example->generation);
> + u64 cursor = ctlv_op_query_cursor(ctx);
> + struct ctlv_cursor item;
> + u64 i = cursor ? cursor - 1 : 0;
> +
> + if (cursor && ctlv_op_query_generation(ctx) != generation)
> + return -ESTALE;
[Severity: Low]
Does returning -ESTALE directly here skip populating ctx->error for
ctlv_error_info()?
ctlv_query_fragment_end() (called by ctlv_op_query_done() and
ctlv_op_query_more()) is the only place that sets
CTLV_VALIDATION_REASON_STALE_GENERATION along with expected_generation and
current_generation:
lib/ctlv/builder.c:ctlv_query_fragment_end() {
...
if (ctx->frag.cursor && generation != ctx->frag.generation) {
ctx->error.reason = CTLV_VALIDATION_REASON_STALE_GENERATION;
ctx->error.expected_generation = ctx->frag.generation;
ctx->error.current_generation = generation;
return -ESTALE;
}
...
}
When a callback returns -ESTALE directly, ctlv_error_info() sees
ctx->error.reason == 0 and omits the stale-generation error-info message
documented in Documentation/userspace-api/ctlv.rst. Calling
ctlv_op_query_done(ctx, generation) on mismatch or letting the loop finish
into ctlv_op_query_done() would populate the error info and return
-ESTALE.
[ ... ]
> +The ``device-get`` callback in ``ctlv-example`` opens the record and has the
> +device's family fill it:
> +
> +.. code-block:: c
> +
> + if (ctlv_example_device_get_details_start(reply, &details)) {
> + ret = ctlv_ext_fill(ctx, NULL, &details);
> + ctlv_record_end(&details);
> + if (ret)
> + return ret;
> + }
> +
> +The foo family implements the generated fill callback:
> +
> +.. code-block:: c
> +
> + int ctlv_example_foo_device_details_fill(struct ctlv *ctlv,
> + struct ctlv_op_ctx *ctx,
> + const struct ctlv_cursor *request,
> + struct ctlv_cursor *record)
> + {
> + struct foo_device *foo = container_of(ctlv, struct foo_device,
> + example.ctlv);
> +
> + ctlv_example_foo_device_details_sram_bank_put(record,
> + foo->sram_bank);
> + return 0;
> + }
> +
> +``request`` is the matching record of the request, for an op whose request has
> +an extension point too; ``device-get`` has none and passes ``NULL``. An
> +extension point no family fills adds no record and no length.
[Severity: Low]
Is it accurate that an unfilled extension point adds no record and no
length?
ctlv_example_device_get_details_start() calls ctlv_record_start(), which
writes a 16-byte struct ctlv_hdr with CTLV_HDR_F_RECORD into the reply and
adds 16 bytes to each ancestor record's length. When no family in the
chain fills the extension point, ctlv_ext_fill() returns 0 and
ctlv_record_end() leaves that empty 16-byte record in the output buffer
(and the schema size calculations in schema.c and sizes.py likewise count
16 bytes for the empty extension record header).
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261113155515.2510194-1-jiri@resnulli.us?part=13
^ permalink raw reply [flat|nested] 24+ messages in thread
* Re: [PATCH RFC v2 09/15] ctlv: Prepare VA blob handling
2026-11-13 15:55 ` [PATCH RFC v2 09/15] ctlv: Prepare VA blob handling Jiri Pirko
@ 2026-11-13 16:13 ` sashiko-bot
0 siblings, 0 replies; 24+ messages in thread
From: sashiko-bot @ 2026-11-13 16:13 UTC (permalink / raw)
To: Jiri Pirko; +Cc: kvm
Thank you for your contribution! Sashiko AI review found 2 potential
issues to consider:
- [Medium] ctlv: pages allocated before RLIMIT_MEMLOCK check in
ctlv_blob_va_acquire()
- [Medium] ctlv: inconsistent record_offset on bad blob in
ctlv_blob_resolve()
--
commit 58ccd80be391ba28c6e0a0c8cd4d357c3b9b3b37
Author: Jiri Pirko <jiri@resnulli.us>
ctlv: Prepare VA blob handling
This commit adds task-bound VA blob resolution and CPU read/write access
helpers for CTLV families.
> diff --git a/lib/ctlv/blob.c b/lib/ctlv/blob.c
> new file mode 100644
> index 0000000000000..14cbbc4bcab1c
> --- /dev/null
> +++ b/lib/ctlv/blob.c
[ ... ]
> +static int ctlv_blob_va_range(u64 address, u64 len, unsigned long *first_page,
> + unsigned long *pages_count)
> +{
> + unsigned long first, last, span, count;
> + size_t bytes;
> + u64 end;
> +
> + if (!len)
> + return -EINVAL;
> + if (check_add_overflow(address, len, &end))
> + return -EOVERFLOW;
> + if (end > TASK_SIZE)
> + return -EFAULT;
> +
> + /* Bounded by an unsigned long above, so nothing below loses a bit. */
> + first = (unsigned long)address & PAGE_MASK;
> + last = ((unsigned long)end - 1) & PAGE_MASK;
> + if (check_sub_overflow(last, first, &span) ||
> + check_add_overflow(span >> PAGE_SHIFT, 1UL, &count) ||
> + check_mul_overflow(count, sizeof(struct page *), &bytes))
> + return -EOVERFLOW;
> +
> + *first_page = first;
> + *pages_count = count;
> + return 0;
> +}
> +
> +static int ctlv_blob_va_acquire(struct ctlv_blob *blob, u64 address)
> +{
> + unsigned long pinned = 0, first, pages_count;
> + unsigned int gup_flags = 0;
> + struct page **pages;
> + int ret;
> +
> + /* Before charging or allocating, both of which this sizes. */
> + ret = ctlv_blob_va_range(address, blob->len, &first, &pages_count);
> + if (ret)
> + return ret;
> +
> + pages = kvmalloc_array(pages_count, sizeof(*pages), GFP_KERNEL_ACCOUNT);
> + if (!pages)
> + return -ENOMEM;
> +
> + if (ctlv_blob_writable(blob))
> + gup_flags |= FOLL_WRITE;
> + ret = ctlv_blob_pin_charge(current->mm, pages_count);
> + if (ret)
> + goto err_free_pages;
[Severity: Medium]
Should ctlv_blob_pin_charge() run before kvmalloc_array()?
An unprivileged caller with a small RLIMIT_MEMLOCK can pass a large blob
length to force a multi-megabyte vmalloc allocation that is immediately
freed when ctlv_blob_pin_charge() rejects the page count. In addition,
check_mul_overflow() only bounds bytes to SIZE_MAX, so if a schema allows
a blob whose page array exceeds INT_MAX bytes, kvmalloc_array() without
__GFP_NOWARN will trigger the size warning in __kvmalloc_node_noprof().
[ ... ]
> +/*
> + * Which of an op's blobs was the unusable one, in the shape validation would
> + * have used. The errno the family returns says what went wrong.
> + */
> +static void ctlv_blob_context_fail(struct ctlv_op_ctx *ctx, u32 tlv_offset, u32 record_offset,
> + u64 required_length)
> +{
> + if (ctx->error.reason)
> + return;
> + ctx->error.reason = CTLV_VALIDATION_REASON_BAD_BLOB_DESCRIPTOR;
> + ctx->error.bad_tlv_offset = tlv_offset;
> + ctx->error.record_offset = record_offset;
> + ctx->error.required_length = required_length;
> +}
[ ... ]
> +struct ctlv_blob *ctlv_blob_resolve(struct ctlv_op_ctx *ctx, const struct ctlv_cursor *blob)
> +{
> + const struct ctlv_hdr *bad = NULL;
> + const struct ctlv_node_schema *node;
> + u32 tlv_offset, record_offset;
> + struct ctlv_blob_desc desc;
> + struct ctlv_blob *acquired;
> +
> + /* An absent blob, or a cursor from some other execution entirely. */
> + if (WARN_ON_ONCE(blob->msg != &ctx->request || !ctlv_cursor_ok(blob)))
> + return ERR_PTR(-EINVAL);
> + node = &blob->tree->node_schemas[blob->node];
> + if (WARN_ON_ONCE(node->kind != CTLV_SCHEMA_BLOB))
> + return ERR_PTR(-EINVAL);
> + if (WARN_ON_ONCE(!ctx->blobs_live || !ctx->submitter || ctx->submitter != current))
> + return ERR_PTR(-EINVAL);
> + /* Validated before the callback, so anything wrong with it is a bug. */
> + if (WARN_ON_ONCE(ctlv_blob_desc_read(blob->hdr, &desc, &bad) ||
> + ctlv_blob_desc_check(&desc, node)))
> + return ERR_PTR(-EINVAL);
> +
> + tlv_offset = ctlv_message_offset(blob->msg, blob->hdr);
> + record_offset = ctlv_message_offset(blob->msg, blob->parent);
> + acquired = ctlv_blob_acquire(&desc, node->blob.direction);
> + if (IS_ERR(acquired)) {
> + ctlv_blob_context_fail(ctx, tlv_offset, record_offset, 0);
> + return acquired;
> + }
[Severity: Medium]
Does record_offset here match what schema validation reports for
CTLV_VALIDATION_REASON_BAD_BLOB_DESCRIPTOR?
ctlv_blob_resolve() sets record_offset to blob->parent, the record
containing the blob descriptor. However, when ctlv_blob_desc_check()
fails during validation, ctlv_schema_check_blob() passes the blob TLV
itself as the record argument to ctlv_schema_bad():
lib/ctlv/schema.c:ctlv_schema_check_blob() {
...
err = ctlv_blob_desc_check(&desc, node);
if (err)
ctlv_schema_bad(v, CTLV_VALIDATION_REASON_BAD_BLOB_DESCRIPTOR, blob, NULL);
return err;
}
Because hdr is NULL there, ctlv_schema_bad() sets both bad_tlv_offset and
record_offset to the offset of blob, so userspace sees a different
record_offset depending on whether the blob failed validation or
resolution.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261113155515.2510194-1-jiri@resnulli.us?part=9
^ permalink raw reply [flat|nested] 24+ messages in thread
* Re: [PATCH RFC v2 15/15] tools/ctlv: Add ctlvctl
2026-11-13 15:55 ` [PATCH RFC v2 15/15] tools/ctlv: Add ctlvctl Jiri Pirko
@ 2026-11-13 16:19 ` sashiko-bot
0 siblings, 0 replies; 24+ messages in thread
From: sashiko-bot @ 2026-11-13 16:19 UTC (permalink / raw)
To: Jiri Pirko; +Cc: kvm
Thank you for your contribution! Sashiko AI review found 2 potential
issues to consider:
- [Low] Documentation: ctlv: driver-api guide drops sections and cites
missing tests
- [Low] tools/ctlv: uncleaned libctlv build artifacts in ctlv_clean
target
--
commit 44c8b60bf07b7ca2b82f88cb4a596bb865f2b940
Author: Jiri Pirko <jiri@resnulli.us>
tools/ctlv: Add ctlvctl
This commit adds the ctlvctl command-line client for discovering and
querying CTLV devices, along with runtime manifest and RST documentation
generators in ctlvgen.
> diff --git a/Documentation/driver-api/ctlv.rst b/Documentation/driver-api/ctlv.rst
> index f6a4f9632e3a5..e8e6ecdf6ab66 100644
> --- a/Documentation/driver-api/ctlv.rst
> +++ b/Documentation/driver-api/ctlv.rst
[ ... ]
> @@ -684,20 +687,24 @@ The node is of the foo driver's own class, ``foo_class``, registered as
> that ``ctlv_family_inherits()`` says is not ``ctlv-example`` or a descendant,
> then calls ``ctlv_init()``.
>
> -Reporting an error
> -------------------
> +Tests and examples
> +==================
> +
> +``Documentation/ctlv/specs/ctlv-test.yaml``
> + Every schema kind, records, actions, queries, blobs, availability, hooks,
> + and an extension point.
> +
> +``drivers/ctlv/tests/ctlv_test.c``
> + The ``ctlv-test`` family implementation.
>
> -A callback returns a negative errno, and that is what the ioctl returns. Where
> -CTLV itself refuses a message it also writes error information into the
> -caller's reply buffer, naming the reason and the offset of the offending
> -TLV. A family implementation does not build that; it adds its own errno
> -and its reply.
> +``drivers/ctlv/tests/ctlv_test_child.c``
> + A separately built inheriting family and extension-point fill.
>
> -Configuration
> -=============
> +``lib/ctlv/tests/*_kunit.c``
> + Unit tests of the engine and of its nodes.
>
> -``CONFIG_CTLV``
> - CTLV. A family implementation selects it.
> +``tools/testing/selftests/ctlv/``
> + Raw-message, generated-binding, allocation-failure, and mutation selftests.
>
> Kernel API
> ==========
[Severity: Low]
Were the "Reporting an error" and "Configuration" sections added in
commit "Documentation: ctlv: Describe how to write a family" dropped here
by mistake? Also, none of the five test paths listed under "Tests and
examples" exist in the tree at this commit.
[ ... ]
> diff --git a/tools/Makefile b/tools/Makefile
> index cb40961a740f7..d8d0003b16a5f 100644
> --- a/tools/Makefile
> +++ b/tools/Makefile
[ ... ]
> @@ -179,6 +183,9 @@ acpi_clean:
> cpupower_clean:
> $(call descend,power/cpupower,clean)
>
> +ctlv_clean:
> + $(call descend,ctlv/ctlvctl,clean)
> +
[ ... ]
> diff --git a/tools/ctlv/ctlvctl/Makefile b/tools/ctlv/ctlvctl/Makefile
> new file mode 100644
> index 0000000000000..dcfca390d4024
> --- /dev/null
> +++ b/tools/ctlv/ctlvctl/Makefile
[ ... ]
> +$(LIB): FORCE
> + $(if $(LIBCTLV_OUTPUT),$(Q)mkdir -p $(LIBCTLV_OUTPUT))
> + $(Q)$(MAKE) -C $(LIBDIR) OUTPUT=$(LIBCTLV_OUTPUT)
> +
> +clean:
> + rm -f $(OUTPUT)ctlvctl $(OBJS) $(MANIFESTS)
> + $(if $(LIBCTLV_OUTPUT),rm -rf $(LIBCTLV_OUTPUT))
[Severity: Low]
Does the clean target leave libctlv build artifacts behind when building in
tree without O=? When OUTPUT and LIBCTLV_OUTPUT are empty, $(LIB) builds
objects and libctlv.a inside tools/ctlv/libctlv/, but neither ctlv_clean in
tools/Makefile nor clean in tools/ctlv/ctlvctl/Makefile cleans $(LIBDIR).
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261113155515.2510194-1-jiri@resnulli.us?part=15
^ permalink raw reply [flat|nested] 24+ messages in thread
* Re: [PATCH RFC v2 07/15] ctlv: Prepare schema validation
2026-11-13 15:55 ` [PATCH RFC v2 07/15] ctlv: Prepare schema validation Jiri Pirko
@ 2026-11-13 16:22 ` sashiko-bot
0 siblings, 0 replies; 24+ messages in thread
From: sashiko-bot @ 2026-11-13 16:22 UTC (permalink / raw)
To: Jiri Pirko; +Cc: kvm
Thank you for your contribution! Sashiko AI review found 1 potential
issue to consider:
- [Medium] ctlv: ctlv_schema_min_len() enforces record depth limit on
leaf nodes
--
commit f7e5c872905b61fc69a0da34a919232ff36227be
Author: Jiri Pirko <jiri@resnulli.us>
ctlv: Prepare schema validation
This commit adds generated-schema walking, message validation, and request
indexing for CTLV schemas.
> diff --git a/lib/ctlv/schema.c b/lib/ctlv/schema.c
> new file mode 100644
> index 0000000000000..8ff3c7c5b5fe0
> --- /dev/null
> +++ b/lib/ctlv/schema.c
> @@ -0,0 +1,1092 @@
[ ... ]
> +/* Buffer length that the largest indivisible leaf and its records need. */
> +static u64 ctlv_schema_min_len(struct ctlv_schema_length_walk *walk,
> + const struct ctlv_schema_tree *tree, u32 index, u32 above, u32 depth)
> +{
> + const struct ctlv_node_schema *node = &tree->node_schemas[index];
> + bool has_children = false;
> + u64 deepest = 0;
> + u32 i;
> +
> + if (depth > CTLV_MAX_DECLARED_RECORD_DEPTH) {
> + walk->bad = true;
> + return 0;
> + }
> +
> + for (i = 0; i < node->children_count; i++) {
> + u32 child = node->first_child + i;
> +
> + has_children = true;
> + deepest = max(deepest,
> + ctlv_schema_min_len(walk, tree, child,
> + above + sizeof(struct ctlv_hdr), depth + 1));
> + }
> +
> + for (i = 0; i < tree->ext_schemas_count; i++) {
> + const struct ctlv_ext_schema *ext_schema = &tree->ext_schemas[i];
> + const struct ctlv_ext_fill_schema *fill_schema;
> + const struct ctlv_node_schema *root;
> + u32 member;
> +
> + if (ext_schema->node != index)
> + continue;
> + fill_schema =
> + ctlv_schema_fill(walk->family, ext_schema->family_id, ext_schema->ext_id);
> + if (!fill_schema)
> + continue;
> + root = &fill_schema->members.node_schemas[0];
> + for (member = 0; member < root->children_count; member++) {
> + has_children = true;
> + deepest = max(deepest, ctlv_schema_min_len(walk, &fill_schema->members,
> + root->first_child + member,
> + above + sizeof(struct ctlv_hdr),
> + depth + 1));
> + }
> + }
> +
> + if (has_children)
> + return deepest;
> + /* An empty root is a message of its own header alone. */
> + if (!index)
> + return above + sizeof(struct ctlv_hdr);
> + return above + ALIGN(ctlv_schema_tlv_max_len(walk, tree, index, depth), CTLV_ALIGNTO);
> +}
[Severity: Medium]
Does ctlv_schema_min_len() reject valid leaf fields inside a record at
CTLV_MAX_DECLARED_RECORD_DEPTH?
Unlike ctlv_schema_children_max_len(), which is only entered for
CTLV_SCHEMA_RECORD nodes, ctlv_schema_min_len() recurses into every child
and extension fill member with depth + 1 and checks depth >
CTLV_MAX_DECLARED_RECORD_DEPTH at entry for all nodes. When a non-reentering
query reply has a record at depth CTLV_MAX_DECLARED_RECORD_DEPTH (7), its
leaf children are visited at depth 8, setting walk->bad = true and causing
ctlv_schema_op_sizes() to fail with -EINVAL.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261113155515.2510194-1-jiri@resnulli.us?part=7
^ permalink raw reply [flat|nested] 24+ messages in thread
* Re: [PATCH RFC v2 01/15] ctlv: Add the schema language and generator
2026-11-13 15:55 ` [PATCH RFC v2 01/15] ctlv: Add the schema language and generator Jiri Pirko
@ 2026-11-13 16:23 ` sashiko-bot
0 siblings, 0 replies; 24+ messages in thread
From: sashiko-bot @ 2026-11-13 16:23 UTC (permalink / raw)
To: Jiri Pirko; +Cc: kvm
Thank you for your contribution! Sashiko AI review found 7 potential
issues to consider:
- [Low] ctlv: compat: broken $include and shared field reconstruction in
_foreign()
- [Low] ctlv: semantics: filled extension point accepted as normal
record in _named()
- [Low] ctlv: semantics: order-dependent false record cycle error in
_held()
- [Low] ctlv: spec: unresolved relative Path parent traversal in Paths
and load()
- [Low] ctlv: semantics: single-pass _check_fills() misses nested
extension sites
- [Low] ctlv: spec: _blob() accepts empty argument as valid backing list
- [Low] ctlv: missing composed size and fill depth checks in
_check_composed()
--
commit c7763c8379c0de88c3274d5b9d07a09e53102368
Author: Jiri Pirko <jiri@resnulli.us>
ctlv: Add the schema language and generator
This commit adds the YAML meta-schemas for CTLV family specifications, the
family registry, and generated-file manifests, along with the ctlvgen tool
to validate specifications, check ABI compatibility, and emit UAPI and
kernel C files.
> diff --git a/tools/ctlv/ctlvgen/lib/compat.py b/tools/ctlv/ctlvgen/lib/compat.py
> new file mode 100644
> index 0000000000000..76bd14f60750b
> --- /dev/null
> +++ b/tools/ctlv/ctlvgen/lib/compat.py
[ ... ]
> +def _foreign(family: semantics.Family, record: semantics.Record,
> + own: set[int]) -> dict[str, bool]:
> + """What a record has that it does not declare: placed records and includes.
> +
> + An include is one member, optional when it made a field optional that the
> + record it splices requires.
> + """
> + members = {}
> + for member in record.members:
> + field = member.field
> + if field.type in own:
> + continue
> + if field.shared:
> + members[f'record {field.name}'] = member.required
> + continue
> + key = f'$include {field.record}'
> + source = family.records[field.record]
> + declared = any(item.required for item in source.members
> + if item.field.type == field.type)
> + members.setdefault(key, True)
> + if declared and not member.required:
> + members[key] = False
> + return members
[Severity: Low]
Does reconstructing foreign members from the flattened record.members
corrupt ABI snapshots and trigger false ABI breaks?
Because _include() splices target.members directly into the including
record while keeping each field's original field.record and field.shared
values, if record A places a shared field f or includes record C and
record B has "$include: A", _foreign() records "record f" or "$include C"
in B's snapshot instead of "$include A".
In addition, _foreign() infers whether "$include X" was written with "?"
by checking if any field in X is required while its spliced member is
optional. If a request record has "$include: X?" and the last required
field in X is later relaxed to optional, declared becomes False for every
field of X, so members['$include X'] flips from False to True and
_compare_members() reports a false "$include X became required" ABI break.
> diff --git a/tools/ctlv/ctlvgen/lib/semantics.py b/tools/ctlv/ctlvgen/lib/semantics.py
> new file mode 100644
> index 0000000000000..86725e7c1d693
> --- /dev/null
> +++ b/tools/ctlv/ctlvgen/lib/semantics.py
[ ... ]
> + def _named(self, name: str, where: str | None) -> Record:
> + """Return the named record or field of records: called name."""
> + if name in self.named:
> + return self.named[name]
> + if name not in self.declared:
> + if name in self.extensions:
> + raise CtlvError(where, f'{name} is an extension point, which '
> + 'extension names')
> + raise CtlvError(where, f'no record is called {name}')
[Severity: Low]
Can a filled extension point be referenced as a normal named record via
"record <name>" or "$include: <name>"?
When a descendant family fills an inherited extension point in records:,
_add_records() adds that name to self.declared and self.named. Because
_named() only checks self.extensions when name is absent from self.named
and self.declared, referencing the extension point via "record <name>" or
"$include: <name>" in the filling family or a further descendant bypasses
the self.extensions check and treats the fill as a regular reusable
record.
> + if name in self.building:
> + raise CtlvError(where, 'record cycle: ' +
> + ' -> '.join(self.building + (name,)))
[ ... ]
> + def _held(self, name: str, where: str) -> str:
> + """Return the path of the named record a field holds."""
> + self.used.add(name)
> + if name in self.building:
> + # A record holding itself, which the tree re-enters.
> + is_field = self.declared[name][3].body is None
> + else:
> + is_field = self._named(name, where).role == 'field'
> + if is_field:
> + raise CtlvError(where, f'record {name} holds a named record, and '
> + f'{name} is a field of records:, which '
> + 'bare record places')
> + return name
[Severity: Low]
Does _held() raise an order-dependent false "record cycle" error when a
held record includes the enclosing record?
If record A has a field "b?: record B" and record B has "$include: A",
declaring B before A succeeds because _held('B') sees 'B' in
self.building and inspects self.declared['B'][3].body directly. If A is
declared before B, _named('A') calls _held('B') while 'A' is in
self.building, and _held('B') calls _named('B'), which resolves
"$include: A" and fails with "record cycle: A -> B -> A". Checking
self.declared[name][3].body whenever name is in self.declared avoids
resolving B while A is still being built.
[ ... ]
> + def _check_fills(self, fills: tuple[Fill, ...]) -> None:
> + """Hold each fill to every place its extension point sits.
> +
> + A fill nested in another is met after it, so its places are known.
> + """
> + for fill in fills:
> + where = self.declared[fill.name][1]
> + for site, context, level in self.sites.get(fill.name, []):
> + self._check_subtree(f'{where} in {site}', fill.root, context,
> + level)
[Severity: Low]
Can a single pass in _check_fills() miss extension sites when a later fill
reuses a record containing an earlier-filled extension point?
If an ancestor family declares extension point ext-b inside a named record
rec-b and extension point ext-a in an op, and a descendant fills ext-b
before ext-a (either earlier in records: or in an intermediate parent
family) while filling ext-a using rec-b, ext-b precedes ext-a in fills.
_check_fills() then checks ext-b before visiting ext-a, so the site for
ext-b inside ext-a is appended to self.sites['ext-b'] after ext-b's loop
has already finished, skipping context and depth validation for ext-b at
that site.
[ ... ]
> +def _check_composed(family: Family) -> None:
> + def walk_composed(where: str, node: Node) -> int:
> + members = composed_members(family, node)
> + required = sum(not optional for _child, optional in members)
> + optional = len(members) - required
> +
> + _check_count(where, 'composed required fields', required,
> + spec.U32_MAX)
> + _check_count(where, 'composed optional fields', optional,
> + spec.U32_MAX)
> + count = 1
> + for child, _optional in members:
> + count += walk_composed(where, child)
> + return count
> +
> + for op in family.ops:
> + total = 0
> + for which, root in (('request', op.request), ('reply', op.reply)):
> + if root is None:
> + continue
> + where = f'{family.path}: op {op.name} {which}'
> + count = walk_composed(where, root)
> + _check_count(where, 'composed schema nodes', count,
> + spec.U32_MAX)
> + total += count
> + _check_count(f'{family.path}: op {op.name}',
> + 'composed request and reply nodes', total,
> + spec.U32_MAX)
[Severity: Low]
Does _check_composed() miss checking composed op message sizes against
MAX_INLINE_MESSAGE_LEN?
_add_ops() runs sizes.compute() on the uncomposed op tree where extension
points are empty, while sizes.composed() in tools/ctlv/ctlvgen/lib/sizes.py
is never called here. Even after commit "tools/ctlv: Add the userspace
library" calls sizes.composed() in gen_user.py, running ctlvgen with
--validate or --mode kernel (or reconciling manifests without optional
user-source outputs) will not reject extension fills that push an
inherited op's composed request, action reply, or minimum query reply
length past MAX_INLINE_MESSAGE_LEN.
Similarly, in tools/ctlv/ctlvgen/lib/gen_kernel.py:_limits(), returning
early when family.local_ops is empty skips emitting the
CTLV_MAX_DECLARED_RECORD_DEPTH static_assert for family.fills on
descendant families that only fill inherited extension points.
> diff --git a/tools/ctlv/ctlvgen/lib/spec.py b/tools/ctlv/ctlvgen/lib/spec.py
> new file mode 100644
> index 0000000000000..7d1f2e14f70fd
> --- /dev/null
> +++ b/tools/ctlv/ctlvgen/lib/spec.py
[ ... ]
> +class Paths:
> + """Where the specs, the registry and the meta-schemas are."""
> +
> + def __init__(self, spec: pathlib.Path | str,
> + schema: pathlib.Path | str | None = None,
> + registry: pathlib.Path | str | None = None,
> + abi: pathlib.Path | str | None = None) -> None:
> + self.spec = pathlib.Path(spec)
> + self.spec_dir = self.spec.parent
> + self.doc_dir = self.spec.parent.parent
> + self.schema = pathlib.Path(schema or self.doc_dir / 'ctlv-schema.yaml')
> + self.registry = pathlib.Path(registry or
> + self.doc_dir / 'families.yaml')
> + # The two meta-schemas ship together, so overriding one locates both.
> + self.registry_schema = (self.schema.parent /
> + 'ctlv-registry-schema.yaml')
> + self.abi_dir = pathlib.Path(abi or self.doc_dir / 'abi')
> + self.registry_abi = self.abi_dir / 'families.registry.yaml'
> + self.generated = self.doc_dir / 'generated-files.yaml'
> + self.generated_schema = self.doc_dir / 'generated-files-schema.yaml'
> + self.kernel_dir = self.schema.parent.parent.parent
[ ... ]
> +def load(path: pathlib.Path | str, schema: pathlib.Path) -> FamilySpec:
> + """Load, syntax-check and normalize one family specification."""
> + path = pathlib.Path(path)
> + data, lines = yamlload.load_with_lines(path)
> + _validate_syntax(path, data, schema)
> +
> + reader = _Reader(path, lines,
> + pathlib.Path(schema).parent.parent.parent / UAPI_DIR)
[Severity: Low]
Does traversing .parent on unresolved relative Path objects in
Paths.__init__() and load() misresolve kernel_dir and the UAPI directory?
In pathlib, taking .parent on a relative path strips components until it
reaches Path('.'), where .parent stays Path('.'). If ctlvgen is invoked
from Documentation/ctlv/ with "--spec specs/ctlv.yaml --validate",
self.schema is Path('ctlv-schema.yaml') and self.schema.parent.parent.parent
collapses to Path('.') instead of the kernel tree root, mislocating both
self.kernel_dir and include/uapi. Resolving the paths first (or using
os.path.abspath) avoids this.
[ ... ]
> + def _blob(self, where: str, args: str) -> BlobDecl:
> + parts = _arguments(args) if args is not None else ['']
> + direction = DIRECTIONS.get(parts[0])
> + if direction is None:
> + raise CtlvError(where, 'a blob starts with its direction, in, '
> + 'out or inout')
> + maximum, alignment, backings = None, None, None
> + for arg in parts[1:]:
> + aligned = _ALIGN.fullmatch(arg)
> + if arg.startswith('..') and maximum is None:
> + maximum = self._size(where, arg[2:].strip(), 'blob length',
> + U64_MAX)
> + elif aligned is not None and alignment is None:
> + alignment = self._size(where, aligned['value'],
> + 'blob alignment')
> + elif backings is None and all(word in BACKINGS
> + for word in arg.split()):
> + backings = tuple(arg.split())
[Severity: Low]
Does _blob() silently accept an empty comma-separated argument such as
"blob(in, )"?
When arg is empty, arg.split() is [], so all(word in BACKINGS for word in
arg.split()) evaluates to True and sets backings = (), which is then
replaced by DEFAULT_BACKINGS via "backings or DEFAULT_BACKINGS". Checking
that arg.split() is non-empty would reject malformed empty arguments.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261113155515.2510194-1-jiri@resnulli.us?part=1
^ permalink raw reply [flat|nested] 24+ messages in thread
* Re: [PATCH RFC v2 10/15] ctlv: Prepare runtime introspection
2026-11-13 15:55 ` [PATCH RFC v2 10/15] ctlv: Prepare runtime introspection Jiri Pirko
@ 2026-11-13 16:24 ` sashiko-bot
0 siblings, 0 replies; 24+ messages in thread
From: sashiko-bot @ 2026-11-13 16:24 UTC (permalink / raw)
To: Jiri Pirko; +Cc: kvm
Thank you for your contribution! Sashiko AI review found 1 potential
issue to consider:
- [Medium] ctlv: premature depth limit in ctlv_introspection_children()
triggers WARN
--
commit 04c73a263bc43bc51c0a09b62604d548973da60c
Author: Jiri Pirko <jiri@resnulli.us>
ctlv: Prepare runtime introspection
This commit adds runtime introspection handlers for registered families,
effective operations, and message schemas through the CTLV root family, and
introduces struct ctlv.
> diff --git a/lib/ctlv/introspection.c b/lib/ctlv/introspection.c
> new file mode 100644
> index 0000000000000..d030debe69e3c
> --- /dev/null
> +++ b/lib/ctlv/introspection.c
[ ... ]
> +/* The members of a fill inherit the optionality of its extension point. */
> +static bool ctlv_introspection_children(struct ctlv_introspection_walk *walk, u32 depth,
> + const struct ctlv_schema_tree *tree, u32 index,
> + bool force_optional)
> +{
> + const struct ctlv_node_schema *node = &tree->node_schemas[index];
> + u32 i;
> +
> + if (WARN_ON_ONCE(depth + 1 >= CTLV_MAX_RECORD_DEPTH)) {
> + /* Deeper than a specification may declare, so unreachable. */
> + walk->error = -EINVAL;
> + return false;
> + }
[ ... ]
> +/* One node, into the record that stands for it. */
> +static bool ctlv_introspection_node(struct ctlv_introspection_walk *walk, u32 depth,
> + const struct ctlv_schema_tree *tree, u32 index)
> +{
> + const struct ctlv_node_schema *node = &tree->node_schemas[index];
> + u32 reenters = 0;
> +
> + if (node->reenters) {
> + reenters = ctlv_introspection_reenters(walk, depth, tree, node);
> + if (!reenters)
> + return false;
> + }
> + if (!ctlv_introspection_node_fields(walk, depth, node, reenters))
> + return false;
> + /*
> + * A node that re-enters a level says so instead of listing that level's
> + * members again, which is what makes unbounded depth describable.
> + */
> + if (!node->reenters && !ctlv_introspection_children(walk, depth, tree, index, false))
> + return false;
> + if (walk->frames[depth].open)
> + ctlv_record_end(&walk->frames[depth].record);
> + return true;
> +}
[ ... ]
> +int ctlv_op_schema_get_query(struct ctlv *ctlv, struct ctlv_op_ctx *ctx,
> + const struct ctlv_cursor *request, struct ctlv_cursor *reply)
> +{
[ ... ]
> + /*
> + * One frame per level the reply may reach, too many for the stack: a
> + * description is one level deeper than the message it describes.
> + */
> + struct ctlv_introspection_frame *frames __free(kfree) =
> + kcalloc(CTLV_MAX_RECORD_DEPTH, sizeof(*frames), GFP_KERNEL);
[Severity: Medium]
Can ctlv_op_schema_get_query() trigger WARN_ON_ONCE() in
ctlv_introspection_children() and fail with -EINVAL on valid schemas near
CTLV_MAX_DECLARED_RECORD_DEPTH?
ctlv_introspection_node() calls ctlv_introspection_children() on every
non-reentering node, including scalar leaves with no children or extension
points, and ctlv_introspection_children() checks depth + 1 >=
CTLV_MAX_RECORD_DEPTH (8) before checking whether the node has any children.
A leaf field inside 6 nested records sits at walk depth 7, so describing it
hits the WARN_ON_ONCE() and returns -EINVAL.
In addition, CTLV_MAX_DECLARED_RECORD_DEPTH (7) counts only record levels,
whereas op-schema-get wraps both the message root (walk->frames[0]) and the
leaf fields of the deepest record in CTLV_SCHEMA_RECORD nodes. A message at
the maximum declared record depth of 7 needs walk depth 8 (9 record levels in
the reply), which exceeds both the kcalloc(CTLV_MAX_RECORD_DEPTH, ...)
allocation and the CTLV_MAX_RECORD_DEPTH limit in ctlv_record_start().
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261113155515.2510194-1-jiri@resnulli.us?part=10
^ permalink raw reply [flat|nested] 24+ messages in thread
* Re: [PATCH RFC v2 14/15] tools/ctlv: Add the userspace library
2026-11-13 15:55 ` [PATCH RFC v2 14/15] tools/ctlv: Add the userspace library Jiri Pirko
@ 2026-11-13 16:28 ` sashiko-bot
0 siblings, 0 replies; 24+ messages in thread
From: sashiko-bot @ 2026-11-13 16:28 UTC (permalink / raw)
To: Jiri Pirko; +Cc: kvm
Thank you for your contribution! Sashiko AI review found 7 potential
issues to consider:
- [Medium] tools/ctlv: missing ctlv_hdr_is_record() check in record
walkers and getters
- [Low] tools/ctlv: ctlv_put_str() bypasses sticky msg->err on length
overflow
- [Low] tools/ctlv: memory leak in ctlv_msg_new_max() when
ctlv_record_start() fails
- [Low] tools/ctlv: unpopulated ctlv_error in ctlv_spec_check() and
ctlv_query_sized()
- [Low] tools/ctlv: stale bound and truncation of reason in
ctlv_error_parse()
- [Low] tools/ctlv: missing progress check on final fragment in
ctlv_query_guard_next()
- [Low] tools/ctlv: missing -ESTALE retry loop in ctlv_ops_get()
--
commit 128e9e8121ea589172c7fc1358fcc6d31f685bab
Author: Jiri Pirko <jiri@resnulli.us>
tools/ctlv: Add the userspace library
This commit adds the libctlv userspace library for transport, message
building and parsing, device discovery, and schema checking, along with
the userspace code generator in ctlvgen.
> diff --git a/tools/ctlv/libctlv/introspection.c b/tools/ctlv/libctlv/introspection.c
> new file mode 100644
> index 0000000000000..1bd2662e58317
> --- /dev/null
> +++ b/tools/ctlv/libctlv/introspection.c
[ ... ]
> +int ctlv_ops_get(struct ctlv_dev *dev, struct ctlv_ops **opsp,
> + struct ctlv_error *error)
> +{
> + struct ctlv_reply *reply = NULL;
> + int ret;
> +
> + /* Sized, or the dump kept for op lengths would ask for itself. */
> + ret = ctlv_query_sized(dev, CTLV_OP_OP_GET, NULL, 0, &reply, error);
> + if (ret)
> + return ret;
[Severity: Low]
Should ctlv_ops_get() retry up to QUERY_RESTARTS times on -ESTALE like
ctlv_query() does?
Because ctlv_ops_get() calls ctlv_query_sized() directly to avoid
recursing into ctlv_dev_sizes(), a dynamic op state change between
fragments of an op-get dump causes ctlv_ops_get(), ctlv_dev_sizes(), and
ctlv_spec_check() to fail with -ESTALE instead of restarting the dump.
[ ... ]
> diff --git a/tools/ctlv/libctlv/message.c b/tools/ctlv/libctlv/message.c
> new file mode 100644
> index 0000000000000..ab4464fb8af2d
> --- /dev/null
> +++ b/tools/ctlv/libctlv/message.c
[ ... ]
> +int ctlv_msg_new_max(__u32 capacity, __u32 max, struct ctlv_msg **msgp)
> +{
> + struct ctlv_msg *msg;
> +
> + if (!capacity || capacity > max)
> + return -EINVAL;
> + msg = calloc(1, sizeof(*msg));
> + if (!msg)
> + return -ENOMEM;
> + /* Separate from the handle, so growing the buffer does not move it. */
> + msg->buf = malloc(capacity);
> + if (!msg->buf) {
> + free(msg);
> + return -ENOMEM;
> + }
> + msg->capacity = capacity;
> + msg->max = max;
> + *msgp = msg;
> + return ctlv_record_start(msg, CTLV_TYPE_ROOT);
> +}
[Severity: Low]
Does ctlv_msg_new_max() leak msg and msg->buf if ctlv_record_start()
fails?
When capacity is smaller than sizeof(struct ctlv_hdr) (16 bytes),
ctlv_record_start() has to grow msg->buf; if max < 16 or realloc() fails,
ctlv_msg_new_max() returns a negative errno with *msgp pointing to the
allocated struct, which callers such as reply_new() do not free on error.
[ ... ]
> +/* The terminator is part of the payload, and a declared length excludes it. */
> +int ctlv_put_str(struct ctlv_msg *msg, __u64 type, const char *value)
> +{
> + size_t len = strlen(value) + 1;
> +
> + if (len > UINT32_MAX)
> + return -EMSGSIZE;
> + return ctlv_msg_put(msg, type, 0, value, len);
> +}
[Severity: Low]
Does the length overflow path in ctlv_put_str() bypass msg->err?
If len > UINT32_MAX, returning -EMSGSIZE directly neither preserves an
earlier msg->err nor records -EMSGSIZE in msg->err, so a caller checking
ctlv_msg_err() or ctlv_msg_done() at the end will miss the failure and
send an incomplete message.
[ ... ]
> +const struct ctlv_hdr *ctlv_hdr_first(const struct ctlv_hdr *record)
> +{
> + if (record->len <= sizeof(*record))
> + return NULL;
> + /* A record too short to hold the header it announces holds no child. */
> + if (record->len - sizeof(*record) < sizeof(*record))
> + return NULL;
> + return record + 1;
> +}
> +
> +/*
> + * The next child is a step of this one's aligned length. Both the step and the
> + * header it lands on have to stay inside the record.
> + */
> +const struct ctlv_hdr *ctlv_hdr_next(const struct ctlv_hdr *record,
> + const struct ctlv_hdr *child)
> +{
> + const char *end = (const char *)record + record->len;
> + const char *next = (const char *)child;
> + __u32 step;
> +
> + if (aligned(child->len, &step))
> + return NULL;
> + next += step;
> + if (next < (const char *)child || next > end)
> + return NULL;
> + if ((size_t)(end - next) < sizeof(*child))
> + return NULL;
> + return (const struct ctlv_hdr *)next;
> +}
[Severity: Medium]
Should ctlv_hdr_first(), ctlv_hdr_next(), and the generated record
getters verify ctlv_hdr_is_record()?
check_record() only validates nested TLV headers when CTLV_HDR_F_RECORD is
set, and the scalar getters reject records via ctlv_hdr_is_record(hdr),
but ctlv_hdr_first() and ctlv_hdr_next() walk any TLV whose len is at
least 32 bytes. Generated record getters in
tools/ctlv/ctlvgen/lib/gen_user.py:_getter() also return ctlv_hdr_find()
without checking ctlv_hdr_is_record().
If a non-record TLV is passed to ctlv_hdr_first() or returned by a record
getter, its unvalidated payload bytes are interpreted as struct ctlv_hdr,
which can spin forever in ctlv_for_each() when the first 4 payload bytes
are zero (step == 0) or read out of bounds when a child's len is smaller
than sizeof(struct ctlv_hdr).
[ ... ]
> diff --git a/tools/ctlv/libctlv/spec.c b/tools/ctlv/libctlv/spec.c
> new file mode 100644
> index 0000000000000..194966e29e2d8
> --- /dev/null
> +++ b/tools/ctlv/libctlv/spec.c
[ ... ]
> +int ctlv_spec_check(struct ctlv_dev *dev, const struct ctlv_family_spec *spec,
> + char *diff, size_t len, struct ctlv_error *error)
> +{
> + struct ctlv_ops *ops;
> + unsigned int i;
> + int ret;
> +
> + if (diff && len)
> + *diff = '\0';
> + ret = chain_check(dev, spec, diff, len);
> + if (ret)
> + return ret;
> + ret = ctlv_ops_get(dev, &ops, error);
> + if (ret)
> + return ret;
[Severity: Low]
Does ctlv_spec_check() leave *error uninitialized or cleared with
error->err == 0 on failure?
If chain_check() fails, *error is never initialized, and if a later check
fails with -ENOMSG (such as an unknown op, info_check(),
ctlv_schema_equal() in schema_check(), or required_ops_check()), error->err
remains 0 from ctlv_ops_get(). Similarly, in
tools/ctlv/libctlv/transport.c:ctlv_query_sized(), if reply_new() fails it
jumps to out_free_buf instead of out_set_err, leaving error->err as 0.
> diff --git a/tools/ctlv/libctlv/transport.c b/tools/ctlv/libctlv/transport.c
> new file mode 100644
> index 0000000000000..647bab085c9e1
> --- /dev/null
> +++ b/tools/ctlv/libctlv/transport.c
[ ... ]
> + hdr = ctlv_hdr_find(root, CTLV_T_ERROR_INFO_REASON);
> + if (hdr && !ctlv_get_enum(hdr, 1,
> + CTLV_VALIDATION_REASON_STALE_GENERATION,
> + &value, NULL)) {
> + error->reason = value;
> + error->present |= CTLV_ERROR_HAS_REASON;
> + }
[Severity: Low]
Does ctlv_error_parse() ignore the enum bounds and truncate value into
error->reason?
ctlv_get_enum() only checks min and max when the known pointer is
non-NULL, so passing NULL accepts any 64-bit value (including 0 or values
above UINT32_MAX) and truncates it into the 32-bit error->reason field.
In addition, the upper bound passed here is
CTLV_VALIDATION_REASON_STALE_GENERATION (11) rather than
CTLV_VALIDATION_REASON_NONCONTIGUOUS_TYPE (12).
[ ... ]
> +int ctlv_query_guard_next(struct ctlv_query_guard *guard, __u64 cursor,
> + bool progress)
> +{
> + unsigned int slot;
> + int ret;
> +
> + if (!cursor)
> + return 0;
> + if (!progress)
> + return -EPROTO;
[Severity: Low]
Does returning early on !cursor skip the progress check for the final
fragment of a multi-fragment query?
When guard->cursors_count > 0 and the final continuation fragment ends
with cursor == 0 without adding any complete TLV (progress == false),
ctlv_query_guard_next() returns 0 instead of rejecting it with -EPROTO.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261113155515.2510194-1-jiri@resnulli.us?part=14
^ permalink raw reply [flat|nested] 24+ messages in thread
* Re: [PATCH RFC v2 11/15] ctlv: Add operation dispatch
2026-11-13 15:55 ` [PATCH RFC v2 11/15] ctlv: Add operation dispatch Jiri Pirko
@ 2026-11-13 16:29 ` sashiko-bot
0 siblings, 0 replies; 24+ messages in thread
From: sashiko-bot @ 2026-11-13 16:29 UTC (permalink / raw)
To: Jiri Pirko; +Cc: kvm
Thank you for your contribution! Sashiko AI review found 2 potential
issues to consider:
- [Medium] ctlv: broken request rebinding and WARN_ON_ONCE in
ctlv_ext_fill()
- [Low] ctlv: unclaimed ancestor families and bad log in ctlv_init()
--
commit d099e8b67fe7bf2eb3f3e45b3042be1ff1f88e98
Author: Jiri Pirko <jiri@resnulli.us>
ctlv: Add operation dispatch
This commit adds CTLV family resolution, instance registration, file
operations, and ioctl operation dispatch.
> diff --git a/lib/ctlv/core.c b/lib/ctlv/core.c
> new file mode 100644
> index 0000000000000..605f191de77ba
> --- /dev/null
> +++ b/lib/ctlv/core.c
[ ... ]
> +/**
> + * ctlv_ext_fill - Delegate an extension-point output record.
> + * @ctx: operation context
> + * @request: validated request root
> + * @record: output record selected by the generated helper
> + *
> + * Return: filling callback result, or 0 for an unfilled point.
> + */
> +int ctlv_ext_fill(struct ctlv_op_ctx *ctx, const struct ctlv_cursor *request,
> + struct ctlv_cursor *record)
> +{
> + const struct ctlv_schema_tree *tree;
> + u32 index;
> +
> + if (WARN_ON_ONCE(!record->tree))
> + return -EINVAL;
> + tree = record->tree;
> +
> + for (index = 0; index < tree->ext_schemas_count; index++) {
> + const struct ctlv_ext_schema *ext_schema = &tree->ext_schemas[index];
> + const struct ctlv_cursor *filled_request = request;
> + const struct ctlv_ext_fill_schema *fill_schema;
> + const struct ctlv_ext_ops *ext_ops;
> + struct ctlv_cursor filled;
> + int ret;
> +
> + if (ext_schema->node != record->node)
> + continue;
> + ext_ops = ctlv_ext_ops_find(ctx->ctlv, ext_schema);
> + if (!ext_ops || !ext_ops->fill)
> + continue;
> +
> + if (request) {
> + fill_schema = ctlv_schema_fill(ctx->family, ext_schema->family_id,
> + ext_schema->ext_id);
> + if (WARN_ON_ONCE(!fill_schema))
> + return -EINVAL;
> + filled = *request;
> + if (ctlv_message_input_rebind(&filled, &fill_schema->members, 0))
> + filled_request = &filled;
> + else
> + filled_request = NULL;
> + }
[Severity: Medium]
Can ctlv_ext_fill() trigger WARN_ON_ONCE(!record->hdr) in
ctlv_message_input_rebind() when userspace omits an optional request
extension record?
When a parent operation looks up the request extension-point record with
its generated helper and the optional record was omitted in the request
message, ctlv_input_field() sets request->hdr = NULL. Passing that cursor
to ctlv_ext_fill() enters the if (request) branch and passes filled.hdr ==
NULL to ctlv_message_input_rebind():
lib/ctlv/message.c:ctlv_message_input_rebind() {
if (WARN_ON_ONCE(!record->hdr || !record->msg || !record->msg->index))
return false;
...
}
Also, the kernel-doc above says @request is the validated request root,
whereas extension fill scopes are attached to the child extension record
rather than index->records[0], so passing the request root cursor always
fails to rebind and sets filled_request = NULL. Should ctlv_ext_fill()
check ctlv_cursor_ok(request) (setting filled_request = NULL when absent)
and document @request as the matching request extension record?
[ ... ]
> +int ctlv_init(struct ctlv *ctlv, struct device *dev,
> + const struct ctlv_family *family, const struct ctlv_hooks *hooks)
> +{
> + int ret;
> +
> + /* Nothing in common means no pair, not half of one. */
> + if (!dev || !family || (hooks && (!hooks->pre || !hooks->post)))
> + return -EINVAL;
> +
> + ctlv->dev = dev;
> + ctlv->hooks = hooks;
> + init_rwsem(&ctlv->registration_lock);
> + spin_lock_init(&ctlv->transition_lock);
> +
> + /* Before anything reads the schema through this instance. */
> + ret = ctlv_op_resolution_init(ctlv, family);
> + if (ret)
> + return ret;
> +
> + /* Before a subsystem announces a device that could not be. */
> + ret = ctlv_uevent_check(family);
> + if (ret)
> + return ret;
> +
> + if (ctlv->dynamic_ops_count) {
> + ctlv->disabled = bitmap_zalloc(ctlv->dynamic_ops_count, GFP_KERNEL);
> + if (!ctlv->disabled)
> + return -ENOMEM;
> + }
> +
> + ret = ctlv_family_claim_add(family->schema);
> + if (ret)
> + return ret;
> + ctlv->family = family;
> + return 0;
> +}
[Severity: Low]
Does ctlv_init() leave intermediate ancestor families unclaimed in
ctlv_family_claims?
ctlv_init() only calls ctlv_family_claim_add(family->schema) on the leaf
family, so base families that are only inherited and never instantiated on
their own are not recorded in ctlv_family_claims, allowing conflicting
ancestor names or family_ids across chains.
In addition, when ctlv_family_claim_add() hits a name collision with a
different family_id (same_name && !same_id), its pr_err() reports that
schema->family_id belongs to claim->name instead of claim->family_id.
Finally, ctlv_family_chain_check() and ctlv_family_descriptor_check() do
not check that child->schema->name[len + 1] is non-NUL after '-' or that
strlen(schema->name) fits the 63-byte limit of
CTLV_T_FAMILY_GET_FAMILIES_NAME before CTLV_OP_FAMILY_GET hits a runtime
WARN_ON_ONCE in ctlv_put_string().
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261113155515.2510194-1-jiri@resnulli.us?part=11
^ permalink raw reply [flat|nested] 24+ messages in thread
end of thread, other threads:[~2026-11-13 16:29 UTC | newest]
Thread overview: 24+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-13 15:54 [PATCH RFC v2 00/15] ctlv: Add a schema-driven TLV uAPI for character devices Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 01/15] ctlv: Add the schema language and generator Jiri Pirko
2026-11-13 16:23 ` sashiko-bot
2026-11-13 15:55 ` [PATCH RFC v2 02/15] ctlv: Add the UAPI message format and family API Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 03/15] ctlv: Add the root family specification and generated files Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 04/15] Documentation: ctlv: Add userspace ABI Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 05/15] ctlv: Prepare message input handling Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 06/15] ctlv: Prepare blob descriptor validation Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 07/15] ctlv: Prepare schema validation Jiri Pirko
2026-11-13 16:22 ` sashiko-bot
2026-11-13 15:55 ` [PATCH RFC v2 08/15] ctlv: Prepare message output handling Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 09/15] ctlv: Prepare VA blob handling Jiri Pirko
2026-11-13 16:13 ` sashiko-bot
2026-11-13 15:55 ` [PATCH RFC v2 10/15] ctlv: Prepare runtime introspection Jiri Pirko
2026-11-13 16:24 ` sashiko-bot
2026-11-13 15:55 ` [PATCH RFC v2 11/15] ctlv: Add operation dispatch Jiri Pirko
2026-11-13 16:29 ` sashiko-bot
2026-11-13 15:55 ` [PATCH RFC v2 12/15] ctlv: Add character device nodes Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 13/15] Documentation: ctlv: Describe how to write a family Jiri Pirko
2026-11-13 16:13 ` sashiko-bot
2026-11-13 15:55 ` [PATCH RFC v2 14/15] tools/ctlv: Add the userspace library Jiri Pirko
2026-11-13 16:28 ` sashiko-bot
2026-11-13 15:55 ` [PATCH RFC v2 15/15] tools/ctlv: Add ctlvctl Jiri Pirko
2026-11-13 16:19 ` sashiko-bot
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox