From: Jiri Pirko <jiri@resnulli.us>
To: linux-coco@lists.linux.dev
Cc: linux-kernel@vger.kernel.org, linux-api@vger.kernel.org,
x86@kernel.org, kvm@vger.kernel.org, linux-doc@vger.kernel.org,
jgg@ziepe.ca, david.quigley@intel.com, lukas@wunner.de,
ksinyuk@kernel.org, rodrigo.vivi@intel.com,
cedric.xing@intel.com, sameo@meta.com, drdeeglaze@gmail.com,
James.Bottomley@HansenPartnership.com, aik@amd.com,
jpiotrowski@nvidia.com, qinkun@google.com, pgonda@google.com,
jic23@kernel.org, alistair.francis@wdc.com, ankita@nvidia.com,
suzuki.poulose@arm.com, kas@kernel.org,
rick.p.edgecombe@intel.com, elena.reshetova@intel.com,
dave.hansen@linux.intel.com,
sathyanarayanan.kuppuswamy@linux.intel.com, peter.fang@intel.com,
jarkko@kernel.org, zohar@linux.ibm.com, yeoreum.yun@arm.com,
aneesh.kumar@kernel.org, dave.jiang@intel.com,
nik.borisov@suse.com, sergii.dmytruk@3mdeb.com
Subject: [PATCH RFC v2 14/15] tools/ctlv: Add the userspace library
Date: Fri, 13 Nov 2026 16:55:13 +0100 [thread overview]
Message-ID: <20261113155515.2510194-15-jiri@resnulli.us> (raw)
In-Reply-To: <20261113155515.2510194-1-jiri@resnulli.us>
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
next prev parent reply other threads:[~2026-11-13 15:56 UTC|newest]
Thread overview: 16+ messages / expand[flat|nested] mbox.gz Atom feed top
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 ` [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 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 15:55 ` [PATCH RFC v2 10/15] ctlv: Prepare runtime introspection Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 11/15] ctlv: Add operation dispatch Jiri Pirko
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 15:55 ` Jiri Pirko [this message]
2026-11-13 15:55 ` [PATCH RFC v2 15/15] tools/ctlv: Add ctlvctl Jiri Pirko
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261113155515.2510194-15-jiri@resnulli.us \
--to=jiri@resnulli.us \
--cc=James.Bottomley@HansenPartnership.com \
--cc=aik@amd.com \
--cc=alistair.francis@wdc.com \
--cc=aneesh.kumar@kernel.org \
--cc=ankita@nvidia.com \
--cc=cedric.xing@intel.com \
--cc=dave.hansen@linux.intel.com \
--cc=dave.jiang@intel.com \
--cc=david.quigley@intel.com \
--cc=drdeeglaze@gmail.com \
--cc=elena.reshetova@intel.com \
--cc=jarkko@kernel.org \
--cc=jgg@ziepe.ca \
--cc=jic23@kernel.org \
--cc=jpiotrowski@nvidia.com \
--cc=kas@kernel.org \
--cc=ksinyuk@kernel.org \
--cc=kvm@vger.kernel.org \
--cc=linux-api@vger.kernel.org \
--cc=linux-coco@lists.linux.dev \
--cc=linux-doc@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=lukas@wunner.de \
--cc=nik.borisov@suse.com \
--cc=peter.fang@intel.com \
--cc=pgonda@google.com \
--cc=qinkun@google.com \
--cc=rick.p.edgecombe@intel.com \
--cc=rodrigo.vivi@intel.com \
--cc=sameo@meta.com \
--cc=sathyanarayanan.kuppuswamy@linux.intel.com \
--cc=sergii.dmytruk@3mdeb.com \
--cc=suzuki.poulose@arm.com \
--cc=x86@kernel.org \
--cc=yeoreum.yun@arm.com \
--cc=zohar@linux.ibm.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox