* [PATCH RFC 01/20] ctlv: Add the schema language and generator
2026-10-15 14:50 [PATCH RFC 00/20] attestation: Add a common subsystem and TDX support Jiri Pirko
@ 2026-10-15 14:50 ` Jiri Pirko
2026-10-15 14:58 ` sashiko-bot
2026-10-15 14:50 ` [PATCH RFC 02/20] ctlv: Add the UAPI wire format and family API Jiri Pirko
` (18 subsequent siblings)
19 siblings, 1 reply; 32+ messages in thread
From: Jiri Pirko @ 2026-10-15 14:50 UTC (permalink / raw)
To: linux-coco
Cc: linux-kernel, linux-api, x86, kvm, linux-doc, jgg, david.quigley,
lukas, ksinyuk, rodrigo.vivi, cedric.xing, sameo, drdeeglaze,
James.Bottomley, aik, jpiotrowski, qinkun, pgonda, jic23,
alistair.francis, ankita, suzuki.poulose, kas, rick.p.edgecombe,
elena.reshetova, dave.hansen, sathyanarayanan.kuppuswamy,
peter.fang
From: Jiri Pirko <jiri@nvidia.com>
Define the YAML schema and append-only family registry. Add ctlvgen to
validate specifications and emit kernel and UAPI outputs.
Assisted-by: Cursor: GPT-5.6 Sol
Assisted-by: Cursor: Claude Opus 5.5
Signed-off-by: Jiri Pirko <jiri@nvidia.com>
---
Documentation/ctlv/ctlv-registry-schema.yaml | 38 +
Documentation/ctlv/ctlv-schema.yaml | 359 +++++++
.../ctlv/generated-files-schema.yaml | 64 ++
MAINTAINERS | 7 +
tools/ctlv/Makefile | 59 ++
tools/ctlv/ctlv-regen.sh | 55 ++
tools/ctlv/ctlvgen/__init__.py | 1 +
tools/ctlv/ctlvgen/cli.py | 172 ++++
tools/ctlv/ctlvgen/lib/__init__.py | 1 +
tools/ctlv/ctlvgen/lib/cgen.py | 825 ++++++++++++++++
tools/ctlv/ctlvgen/lib/compat.py | 821 ++++++++++++++++
tools/ctlv/ctlvgen/lib/errors.py | 14 +
tools/ctlv/ctlvgen/lib/gen_family.py | 347 +++++++
tools/ctlv/ctlvgen/lib/gen_kernel.py | 517 ++++++++++
tools/ctlv/ctlvgen/lib/gen_uapi.py | 223 +++++
tools/ctlv/ctlvgen/lib/outputs.py | 329 +++++++
tools/ctlv/ctlvgen/lib/semantics.py | 893 ++++++++++++++++++
tools/ctlv/ctlvgen/lib/sizes.py | 238 +++++
tools/ctlv/ctlvgen/lib/spec.py | 583 ++++++++++++
tools/ctlv/ctlvgen/lib/tree.py | 71 ++
tools/ctlv/ctlvgen/lib/yamlload.py | 83 ++
tools/ctlv/pyproject.toml | 22 +
tools/ctlv/requirements.txt | 2 +
23 files changed, 5724 insertions(+)
create mode 100644 Documentation/ctlv/ctlv-registry-schema.yaml
create mode 100644 Documentation/ctlv/ctlv-schema.yaml
create mode 100644 Documentation/ctlv/generated-files-schema.yaml
create mode 100644 tools/ctlv/Makefile
create mode 100755 tools/ctlv/ctlv-regen.sh
create mode 100644 tools/ctlv/ctlvgen/__init__.py
create mode 100755 tools/ctlv/ctlvgen/cli.py
create mode 100644 tools/ctlv/ctlvgen/lib/__init__.py
create mode 100644 tools/ctlv/ctlvgen/lib/cgen.py
create mode 100644 tools/ctlv/ctlvgen/lib/compat.py
create mode 100644 tools/ctlv/ctlvgen/lib/errors.py
create mode 100644 tools/ctlv/ctlvgen/lib/gen_family.py
create mode 100644 tools/ctlv/ctlvgen/lib/gen_kernel.py
create mode 100644 tools/ctlv/ctlvgen/lib/gen_uapi.py
create mode 100644 tools/ctlv/ctlvgen/lib/outputs.py
create mode 100644 tools/ctlv/ctlvgen/lib/semantics.py
create mode 100644 tools/ctlv/ctlvgen/lib/sizes.py
create mode 100644 tools/ctlv/ctlvgen/lib/spec.py
create mode 100644 tools/ctlv/ctlvgen/lib/tree.py
create mode 100644 tools/ctlv/ctlvgen/lib/yamlload.py
create mode 100644 tools/ctlv/pyproject.toml
create mode 100644 tools/ctlv/requirements.txt
diff --git a/Documentation/ctlv/ctlv-registry-schema.yaml b/Documentation/ctlv/ctlv-registry-schema.yaml
new file mode 100644
index 000000000000..d9a2840fc230
--- /dev/null
+++ b/Documentation/ctlv/ctlv-registry-schema.yaml
@@ -0,0 +1,38 @@
+# SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause)
+%YAML 1.2
+---
+$id: http://kernel.org/schemas/ctlv/ctlv-registry-schema.yaml#
+$schema: https://json-schema.org/draft-07/schema
+
+$defs:
+ name:
+ type: string
+ maxLength: 63
+ pattern: ^[a-z0-9]+(-[a-z0-9]+)*$
+
+title: CTLV family registry
+description: Append-only list of CTLV family names, indexed by family ID
+type: object
+required: [ families ]
+additionalProperties: false
+properties:
+ families:
+ description: |
+ Family names in family ID order: the entry at index N has family ID N.
+ The root family owns ID 0 and so is the first entry, and cannot be
+ deprecated. Every other entry comes after the entry of the family it
+ inherits. A deprecated entry keeps its ID reserved.
+ type: array
+ minItems: 1
+ maxItems: 65536
+ items:
+ oneOf:
+ - $ref: '#/$defs/name'
+ - type: object
+ required: [ name, deprecated ]
+ additionalProperties: false
+ properties:
+ name:
+ $ref: '#/$defs/name'
+ deprecated:
+ const: true
diff --git a/Documentation/ctlv/ctlv-schema.yaml b/Documentation/ctlv/ctlv-schema.yaml
new file mode 100644
index 000000000000..38fd56e6627c
--- /dev/null
+++ b/Documentation/ctlv/ctlv-schema.yaml
@@ -0,0 +1,359 @@
+# SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause)
+%YAML 1.2
+---
+$id: http://kernel.org/schemas/ctlv/ctlv-schema.yaml#
+$schema: https://json-schema.org/draft-07/schema
+
+# The syntax gate for a CTLV family specification: permitted keys, their shapes
+# and the closed sets of keywords. Anything needing more than one declaration at
+# a time is checked by tools/ctlv/ctlvgen/lib/semantics.py.
+
+$defs:
+ name:
+ description: Kebab-case identifier. A dot is reserved for selectors.
+ type: string
+ pattern: ^[a-z0-9]+(-[a-z0-9]+)*$
+
+ family-name:
+ description: Name carried by family discovery.
+ allOf:
+ - $ref: '#/$defs/name'
+ - maxLength: 63
+
+ selector:
+ description: |
+ A named attr set, a whole attr group, or one exact attr as
+ "group.attr". There are no tuple selectors and no cross-family
+ qualification.
+ type: string
+ pattern: ^[a-z0-9]+(-[a-z0-9]+)*(\.[a-z0-9]+(-[a-z0-9]+)*)?$
+
+ selectors:
+ oneOf:
+ - $ref: '#/$defs/selector'
+ - type: array
+ minItems: 1
+ items:
+ $ref: '#/$defs/selector'
+
+ uint:
+ type: integer
+ minimum: 0
+ maximum: 4294967295
+
+ integer-bound:
+ type: integer
+ minimum: -9223372036854775808
+ maximum: 18446744073709551615
+
+ max-occurrences:
+ description: |
+ A finite positive count, or unlimited, which is reachable only from
+ query replies.
+ oneOf:
+ - type: integer
+ minimum: 1
+ maximum: 4294967295
+ - const: unlimited
+
+ unit:
+ description: CTLV-owned unit, matching enum ctlv_unit.
+ enum: [ seconds, hertz, bytes, celsius ]
+
+ value-list:
+ description: An enum or a flags definition.
+ type: object
+ required: [ name, values ]
+ additionalProperties: false
+ properties:
+ name:
+ $ref: '#/$defs/name'
+ values:
+ description: |
+ Value names in declaration order. Enum values are numbered from 1
+ because 0 is reserved; flag bits are numbered from bit 0. Both are
+ append-only.
+ type: array
+ minItems: 1
+ items:
+ $ref: '#/$defs/name'
+
+ enum-list:
+ allOf:
+ - $ref: '#/$defs/value-list'
+ - properties:
+ values:
+ maxItems: 4294967295
+
+ flags-list:
+ allOf:
+ - $ref: '#/$defs/value-list'
+ - properties:
+ values:
+ maxItems: 64
+
+ blob:
+ type: object
+ required: [ direction, allowed-backings, max-length ]
+ additionalProperties: false
+ properties:
+ direction:
+ description: How the family may use the described memory.
+ enum: [ input, output, inout ]
+ allowed-backings:
+ type: array
+ minItems: 1
+ uniqueItems: true
+ items:
+ enum: [ va ]
+ max-length:
+ description: |
+ Authoritative maximum external payload length for this placement,
+ independent of the inline message limit.
+ type: integer
+ minimum: 1
+ maximum: 18446744073709551615
+ alignment:
+ description: Required alignment of the described address or offset.
+ type: integer
+ minimum: 1
+ maximum: 4294967295
+
+ attr:
+ description: |
+ One attr declaration. Exactly one primary form out of type, enum,
+ flags, attrs and blob must be present. optional-attrs and recursive
+ may only accompany attrs.
+ type: object
+ required: [ name ]
+ additionalProperties: false
+ properties:
+ name:
+ $ref: '#/$defs/name'
+ type:
+ description: |
+ Primitive payload. Integers are always 64-bit, and a bool is one
+ byte holding zero or one.
+ enum: [ uint, sint, bool, string, binary ]
+ enum:
+ description: Name of an enum definition; implies an unsigned payload.
+ $ref: '#/$defs/name'
+ flags:
+ description: Name of a flags definition; implies an unsigned payload.
+ $ref: '#/$defs/name'
+ attrs:
+ description: Nested payload, given as selectors.
+ $ref: '#/$defs/selectors'
+ optional-attrs:
+ description: Further nested payload whose placements are optional.
+ $ref: '#/$defs/selectors'
+ recursive:
+ description: |
+ The nested payload is one attr set that may contain this attr
+ again, describing a structure of any depth. Only a query reply can
+ hold one, since such a nest has no maximum length.
+ const: true
+ blob:
+ $ref: '#/$defs/blob'
+ min:
+ description: Inclusive minimum of an integer payload.
+ $ref: '#/$defs/integer-bound'
+ max:
+ description: Inclusive maximum of an integer payload.
+ $ref: '#/$defs/integer-bound'
+ length:
+ description: Exact payload length of a binary attr.
+ $ref: '#/$defs/uint'
+ min-length:
+ description: |
+ Minimum payload length, excluding the terminator of a string.
+ $ref: '#/$defs/uint'
+ max-length:
+ description: |
+ Maximum payload length, excluding the terminator of a string.
+ $ref: '#/$defs/uint'
+ unit:
+ $ref: '#/$defs/unit'
+ scale:
+ description: Decimal exponent applied to the payload.
+ type: integer
+ minimum: -128
+ maximum: 127
+ max-occurrences:
+ $ref: '#/$defs/max-occurrences'
+
+ attr-group:
+ description: |
+ A namespace of attr declarations. The group name is also the generated
+ symbol prefix of its attrs.
+ type: object
+ required: [ name, attrs ]
+ additionalProperties: false
+ properties:
+ name:
+ $ref: '#/$defs/name'
+ attrs:
+ type: array
+ minItems: 1
+ maxItems: 4294967295
+ items:
+ $ref: '#/$defs/attr'
+
+ attr-set:
+ description: |
+ A compositional schema unit. Either a stub, which may be filled once
+ by one descendant family, or a set of members. After fills are composed,
+ every attr ID at one nesting level must still have one placement.
+ type: object
+ required: [ name ]
+ additionalProperties: false
+ properties:
+ name:
+ $ref: '#/$defs/name'
+ stub:
+ const: true
+ attrs:
+ $ref: '#/$defs/selectors'
+ optional-attrs:
+ $ref: '#/$defs/selectors'
+
+ placements:
+ description: The required and optional attr placements at one level.
+ type: object
+ additionalProperties: false
+ minProperties: 1
+ properties:
+ attrs:
+ $ref: '#/$defs/selectors'
+ optional-attrs:
+ $ref: '#/$defs/selectors'
+
+ op:
+ type: object
+ required: [ name, type ]
+ additionalProperties: false
+ properties:
+ name:
+ $ref: '#/$defs/name'
+ type:
+ description: |
+ action is invoked once and never fragments, query may fragment its
+ reply, and event is an unsolicited reply-only schema.
+ enum: [ action, query, event ]
+ fd-access:
+ description: |
+ Authority needed to execute an action or query. read permits an
+ observation fd; read-write, the default, requires a full-control fd.
+ Events are always readable and cannot declare this property.
+ enum: [ read, read-write ]
+ availability:
+ description: |
+ Whether a family may enable and disable this op per device. fixed,
+ the default, is always enabled. dynamic is action and query only,
+ and is a permission rather than a state: every device starts with
+ the op enabled.
+ enum: [ fixed, dynamic ]
+ optional:
+ description: |
+ Whether family initialization must opt in before publication for
+ this op to exist on the device. Action and query only, and false by
+ default. An op not opted into is absent from introspection.
+ type: boolean
+ pre-post:
+ description: |
+ Whether this op gets a generated pre/post hook pair of its own,
+ which its declaring family implements. It runs inside the pair of
+ the device family, or the core family for a core op.
+ Action and query only, and false by default. Kernel implementation
+ metadata: it reaches neither UAPI nor op-schema introspection.
+ type: boolean
+ request:
+ description: |
+ Omitted means a request root with no children. An event has no
+ request.
+ $ref: '#/$defs/placements'
+ reply:
+ description: |
+ Required on a query. Omitted on an action means ACK-only, while an
+ event always has a reply root, so omitting it there means a root
+ with no children.
+ $ref: '#/$defs/placements'
+ suggested-reply-length:
+ description: |
+ Query only. An omitted value is CTLV_DEFAULT_QUERY_REPLY_LEN raised
+ to the computed minimum. A declared value must be between that
+ minimum and CTLV_MAX_INLINE_MESSAGE_LEN.
+ type: integer
+ minimum: 1
+ maximum: 1048576
+
+title: CTLV family
+description: Specification of one CTLV family schema
+type: object
+required: [ family ]
+additionalProperties: false
+properties:
+ family:
+ type: object
+ required: [ name ]
+ additionalProperties: false
+ properties:
+ name:
+ description: |
+ Registered family name. It carries no numeric ID; the ID comes
+ from the position in Documentation/ctlv/families.yaml.
+ $ref: '#/$defs/family-name'
+ inherits:
+ description: |
+ Name of the single direct parent family, which every family but
+ the root one names. The complete parent schema is inherited and
+ its definitions keep the parent's IDs. The name above must be
+ this name, one dash, and a local suffix.
+ $ref: '#/$defs/family-name'
+ pre-init:
+ description: |
+ Whether this family implements the generated pre-init callback
+ which translates its init info for its parent. False by default;
+ without one, the parent receives NULL.
+ type: boolean
+ post-init:
+ description: |
+ Whether this family implements the generated post-init callback
+ after every family priv has been initialized. False by default.
+ type: boolean
+ pre-fini:
+ description: |
+ Whether this family implements the generated pre-fini callback
+ before any family priv is finalized. False by default.
+ type: boolean
+ enums:
+ type: array
+ minItems: 1
+ items:
+ $ref: '#/$defs/enum-list'
+
+ flags:
+ type: array
+ minItems: 1
+ items:
+ $ref: '#/$defs/flags-list'
+
+ attr-groups:
+ type: array
+ minItems: 1
+ maxItems: 65535
+ items:
+ $ref: '#/$defs/attr-group'
+
+ attr-sets:
+ type: array
+ minItems: 1
+ items:
+ $ref: '#/$defs/attr-set'
+
+ ops:
+ type: array
+ minItems: 1
+ maxItems: 4294967295
+ items:
+ $ref: '#/$defs/op'
diff --git a/Documentation/ctlv/generated-files-schema.yaml b/Documentation/ctlv/generated-files-schema.yaml
new file mode 100644
index 000000000000..d3529ce6e8e7
--- /dev/null
+++ b/Documentation/ctlv/generated-files-schema.yaml
@@ -0,0 +1,64 @@
+# SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause)
+%YAML 1.2
+---
+$schema: https://json-schema.org/draft-07/schema
+
+$defs:
+ path:
+ type: string
+ minLength: 1
+ pattern: ^[^/].*$
+
+ outputs:
+ type: object
+ minProperties: 1
+ additionalProperties: false
+ properties:
+ uapi-header:
+ $ref: '#/$defs/path'
+ kernel-header:
+ $ref: '#/$defs/path'
+ kernel-source:
+ $ref: '#/$defs/path'
+
+ registry:
+ type: object
+ required: [ source, outputs ]
+ additionalProperties: false
+ properties:
+ source:
+ $ref: '#/$defs/path'
+ outputs:
+ type: object
+ required: [ uapi-header ]
+ additionalProperties: false
+ properties:
+ uapi-header:
+ $ref: '#/$defs/path'
+
+ family:
+ type: object
+ required: [ name, source, outputs ]
+ additionalProperties: false
+ properties:
+ name:
+ type: string
+ source:
+ $ref: '#/$defs/path'
+ outputs:
+ $ref: '#/$defs/outputs'
+
+title: CTLV generated-file manifest
+type: object
+required: [ version, registry, families ]
+additionalProperties: false
+properties:
+ version:
+ const: 1
+ registry:
+ $ref: '#/$defs/registry'
+ families:
+ type: array
+ minItems: 1
+ items:
+ $ref: '#/$defs/family'
diff --git a/MAINTAINERS b/MAINTAINERS
index 31699b112d3e..a8508fbae37d 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -7001,6 +7001,13 @@ M: Jaya Kumar <jayakumar.alsa@gmail.com>
S: Maintained
F: sound/pci/cs5535audio/
+CTLV SUBSYSTEM
+M: Jiri Pirko <jiri@resnulli.us>
+L: linux-kernel@vger.kernel.org
+S: Maintained
+F: Documentation/ctlv/
+F: tools/ctlv/
+
CTU CAN FD DRIVER
M: Pavel Pisa <pisa@cmp.felk.cvut.cz>
M: Ondrej Ille <ondrej.ille@gmail.com>
diff --git a/tools/ctlv/Makefile b/tools/ctlv/Makefile
new file mode 100644
index 000000000000..bb3129405fb5
--- /dev/null
+++ b/tools/ctlv/Makefile
@@ -0,0 +1,59 @@
+# SPDX-License-Identifier: GPL-2.0
+
+KDIR = ../..
+DOCDIR = $(KDIR)/Documentation/ctlv
+SPECDIR = $(DOCDIR)/specs
+SPECS = $(wildcard $(SPECDIR)/*.yaml)
+TOOL = ./ctlvgen/cli.py
+
+all: validate abi-check
+
+validate:
+ @for spec in $(SPECS) ; do \
+ echo -e "\tVALIDATE $$(basename $$spec)" ; \
+ $(TOOL) --spec $$spec --validate || exit 1 ; \
+ done
+
+# Fails when a specification no longer matches its checked-in ABI snapshot.
+abi-check:
+ @for spec in $(SPECS) ; do \
+ echo -e "\tABICHECK $$(basename $$spec)" ; \
+ $(TOOL) --spec $$spec --abi-check || exit 1 ; \
+ done
+
+# Deliberate: an updated snapshot is how a UAPI change gets reviewed.
+abi-update:
+ @for spec in $(SPECS) ; do \
+ $(TOOL) --spec $$spec --abi-update || exit 1 ; \
+ done
+
+# Style checks, needing yamllint, pycodestyle and pyflakes, which the tool
+# itself does not use.
+#
+# A flow sequence is written "[ one, two ]" here, as in the tree's other
+# meta-schemas, which stock yamllint rejects; the same relaxation is in
+# Documentation/devicetree/bindings/.yamllint.
+YAMLLINT_CONF = {extends: default, rules: {brackets: {max-spaces-inside: 1}}}
+
+PYSRC = ctlvgen
+
+lint:
+ yamllint -d "$(YAMLLINT_CONF)" $(DOCDIR)
+ python3 -m pycodestyle $(PYSRC)
+ python3 -m pyflakes $(PYSRC)
+
+regen:
+ @./ctlv-regen.sh
+
+# Fails when a checked-in generated file does not match what the generator
+# produces from the current specifications. Writes nothing.
+check-regen:
+ @./ctlv-regen.sh -c
+
+clean:
+ rm -rf ctlvgen/__pycache__
+ rm -rf ctlvgen/lib/__pycache__
+ rm -rf ctlvgen.egg-info
+ rm -rf build
+
+.PHONY: all validate abi-check abi-update lint regen check-regen clean
diff --git a/tools/ctlv/ctlv-regen.sh b/tools/ctlv/ctlv-regen.sh
new file mode 100755
index 000000000000..575114ba93e7
--- /dev/null
+++ b/tools/ctlv/ctlv-regen.sh
@@ -0,0 +1,55 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+#
+# Reconcile the checked-in files against the independent output manifests.
+#
+# -f accepted for compatibility; reconciliation always renders everything
+# -c do not write anything, fail if any output differs from what the
+# generator produces now
+# -p search the given directory instead of the kernel tree
+
+TOOL=$(dirname "$(realpath "$0")")/ctlvgen/cli.py
+check=
+search=
+
+while getopts ':fcp:' option; do
+ case "$option" in
+ f)
+ ;;
+ c)
+ check=yes
+ ;;
+ p)
+ if [ -z "$OPTARG" ]; then
+ echo "Option -p needs a nonempty directory" >&2
+ exit 1
+ fi
+ search=$OPTARG
+ ;;
+ :)
+ echo "Option -$OPTARG needs an argument" >&2
+ exit 1
+ ;;
+ ?)
+ echo "Unrecognized option '-$OPTARG'" >&2
+ exit 1
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+if [ "$#" -ne 0 ]; then
+ echo "Unexpected argument '$1'" >&2
+ exit 1
+fi
+
+KDIR=$(realpath "$(dirname "$0")/../..")
+args=(--spec "$KDIR/Documentation/ctlv/specs/ctlv.yaml")
+if [ -n "$search" ]; then
+ args+=(--output-root "$search")
+fi
+if [ -n "$check" ]; then
+ args+=(--outputs-check)
+else
+ args+=(--outputs-update)
+fi
+exec "$TOOL" "${args[@]}"
diff --git a/tools/ctlv/ctlvgen/__init__.py b/tools/ctlv/ctlvgen/__init__.py
new file mode 100644
index 000000000000..f84069cb2c3d
--- /dev/null
+++ b/tools/ctlv/ctlvgen/__init__.py
@@ -0,0 +1 @@
+# SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
diff --git a/tools/ctlv/ctlvgen/cli.py b/tools/ctlv/ctlvgen/cli.py
new file mode 100755
index 000000000000..c6bfafeb2852
--- /dev/null
+++ b/tools/ctlv/ctlvgen/cli.py
@@ -0,0 +1,172 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+"""Command line front end of the CTLV schema tool."""
+
+import argparse
+import pathlib
+import sys
+
+# So that this file works both as a module of the ctlvgen package and as the
+# script the Makefile and ctlv-regen.sh run. The imports below have to follow.
+sys.path.append(pathlib.Path(__file__).resolve().parent.as_posix())
+
+from lib import cgen # noqa: E402
+from lib import compat # noqa: E402
+from lib import gen_kernel # noqa: E402
+from lib import gen_uapi # noqa: E402
+from lib import outputs # noqa: E402
+from lib import semantics # noqa: E402
+from lib import spec # noqa: E402
+from lib import tree # noqa: E402
+from lib.errors import CtlvError # noqa: E402
+
+
+# What each non-kernel --mode and file kind generates.
+GENERATORS = {
+ ('uapi', 'header'): gen_uapi.render,
+}
+
+
+def main():
+ parser = argparse.ArgumentParser(description='CTLV schema tool')
+ parser.add_argument('--spec', type=str, required=True,
+ help='path to a CTLV family specification')
+ parser.add_argument('--schema', type=str,
+ help='path to the meta-schema, derived from --spec '
+ 'when not given')
+ parser.add_argument('--registry', type=str,
+ help='path to the family registry, derived from '
+ '--spec when not given')
+ parser.add_argument('--abi', type=str,
+ help='directory of the ABI snapshots, derived from '
+ '--spec when not given')
+ action = parser.add_mutually_exclusive_group(required=True)
+ action.add_argument('--validate', action='store_true',
+ help='validate the specification and exit')
+ action.add_argument('--abi-check', action='store_true',
+ help='check the specification against its ABI '
+ 'snapshot')
+ action.add_argument('--abi-update', action='store_true',
+ help='rewrite the ABI snapshot from the '
+ 'specification')
+ action.add_argument('--tree-check', action='store_true',
+ help='check registry, spec, snapshot and output '
+ 'coverage')
+ action.add_argument('--tree-check-update', action='store_true',
+ help='check whole-tree inputs before an ABI update')
+ action.add_argument('--registry-abi-update', action='store_true',
+ help='update the ordered family registry baseline')
+ action.add_argument('--outputs-check', action='store_true',
+ help='check every file in the output manifests')
+ action.add_argument('--outputs-update', action='store_true',
+ help='regenerate every file in the output manifests')
+ action.add_argument('--mode',
+ choices=('uapi', 'kernel'),
+ help='generate the files of this mode')
+ parser.add_argument('--schema-only', action='store_true',
+ help='omit the family API from kernel output')
+ kind = parser.add_mutually_exclusive_group()
+ kind.add_argument('--header', action='store_true',
+ help='generate the header of --mode')
+ kind.add_argument('--source', action='store_true',
+ help='generate the source of --mode')
+ parser.add_argument('-o', '--output', type=str,
+ help='write to this file instead of standard output')
+ parser.add_argument('--cmp-out', action='store_true',
+ help='leave the output file alone when its contents '
+ 'would not change')
+ parser.add_argument('--output-root', type=str,
+ help='alternate destination root for generated files')
+ args = parser.parse_args()
+
+ kinds = (args.header, args.source)
+ if args.mode and not any(kinds):
+ parser.error('--mode needs --header or --source')
+ if any(kinds) and not args.mode:
+ parser.error('--header and --source need --mode')
+ if args.schema_only and args.mode != 'kernel':
+ parser.error('--schema-only needs --mode kernel')
+ output_action = args.outputs_check or args.outputs_update
+ if args.output is not None and not args.mode:
+ parser.error('-o/--output needs --mode')
+ if args.cmp_out and (not args.mode or args.output is None):
+ parser.error('--cmp-out needs --mode and --output')
+ if args.output_root is not None:
+ if not output_action:
+ parser.error('--output-root needs an outputs action')
+ if not args.output_root:
+ parser.error('--output-root cannot be empty')
+
+ paths = spec.Paths(args.spec, schema=args.schema, registry=args.registry,
+ abi=args.abi)
+ try:
+ if args.tree_check:
+ tree.check(paths)
+ return 0
+ elif args.tree_check_update:
+ tree.check_for_update(paths)
+ return 0
+ elif args.registry_abi_update:
+ registry = tree.finish_update(paths)
+ paths.registry_abi.write_text(
+ compat.registry_render(registry), encoding='utf-8')
+ print(f'ctlvgen: wrote {paths.registry_abi}', file=sys.stderr)
+ return 0
+ elif args.outputs_check:
+ tree.check(paths)
+ outputs.reconcile(paths, args.output_root, check=True)
+ return 0
+ elif args.outputs_update:
+ tree.check_for_update(paths)
+ changed = outputs.reconcile(paths, args.output_root)
+ for item in changed:
+ print(f'ctlvgen: {item}', file=sys.stderr)
+ return 0
+ family = semantics.load(paths)
+ if args.validate:
+ cgen.symbols(family)
+ elif args.abi_check:
+ compat.registry_check(paths, spec.load_registry(paths))
+ compat.check(family, paths.abi(family.name))
+ elif args.abi_update:
+ _abi_update(paths, family)
+ else:
+ _generate(args, family)
+ except CtlvError as exc:
+ print(f'ctlvgen: {exc}', file=sys.stderr)
+ return 1
+ return 0
+
+
+def _abi_update(paths, family):
+ path = paths.abi(family.name)
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text(compat.render(family), encoding='utf-8')
+ print(f'ctlvgen: wrote {path}', file=sys.stderr)
+
+
+def _generate(args, family):
+ if args.header:
+ kind = 'header'
+ else:
+ kind = 'source'
+ if args.mode == 'kernel':
+ renderer = (gen_kernel.render_header if args.header
+ else gen_kernel.render_source)
+ text = renderer(family, family_api=not args.schema_only)
+ else:
+ text = GENERATORS[(args.mode, kind)](family)
+ if args.output is None:
+ sys.stdout.write(text)
+ return
+ path = pathlib.Path(args.output)
+ # Leaving an unchanged file untouched keeps its timestamp, which is what
+ # ctlv-regen.sh compares against the specification.
+ if args.cmp_out and path.exists() and \
+ path.read_text(encoding='utf-8') == text:
+ return
+ path.write_text(text, encoding='utf-8')
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/tools/ctlv/ctlvgen/lib/__init__.py b/tools/ctlv/ctlvgen/lib/__init__.py
new file mode 100644
index 000000000000..f84069cb2c3d
--- /dev/null
+++ b/tools/ctlv/ctlvgen/lib/__init__.py
@@ -0,0 +1 @@
+# SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
diff --git a/tools/ctlv/ctlvgen/lib/cgen.py b/tools/ctlv/ctlvgen/lib/cgen.py
new file mode 100644
index 000000000000..2cb754b42106
--- /dev/null
+++ b/tools/ctlv/ctlvgen/lib/cgen.py
@@ -0,0 +1,825 @@
+# SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+"""C output scaffolding shared by the CTLV generators.
+
+The preamble ctlv-regen.sh recovers a file's specification and mode from, which
+is why the specification stays directly above the marker.
+"""
+
+from __future__ import annotations
+
+import os
+
+from . import semantics
+from . import spec
+from .errors import CtlvError
+
+REGEN = 'tools/ctlv/ctlv-regen.sh'
+
+LINE_LIMIT = 100
+HARD_LINE_LIMIT = 100
+TAB_WIDTH = 8
+
+# C spelling of each primary form. A test checks these against
+# enum ctlv_schema_kind.
+KINDS = {
+ 'uint': 'CTLV_SCHEMA_UINT',
+ 'sint': 'CTLV_SCHEMA_SINT',
+ 'bool': 'CTLV_SCHEMA_BOOL',
+ 'string': 'CTLV_SCHEMA_STRING',
+ 'binary': 'CTLV_SCHEMA_BINARY',
+ 'enum': 'CTLV_SCHEMA_ENUM',
+ 'flags': 'CTLV_SCHEMA_FLAGS',
+ 'nested': 'CTLV_SCHEMA_NEST',
+ 'blob': 'CTLV_SCHEMA_BLOB',
+}
+
+LIFECYCLE_API_SYMBOLS = (
+ 'ctlv_create',
+ 'ctlv_destroy',
+ 'ctlv_family_priv_size_fn',
+ 'ctlv_family_pre_init_fn',
+ 'ctlv_family_init_fn',
+ 'ctlv_family_post_init_fn',
+ 'ctlv_family_pre_fini_fn',
+ 'ctlv_family_fini_fn',
+ 'ctlv_priv_parent',
+ 'ctlv_priv_child',
+)
+
+
+def width(line):
+ """Return the display width of one line, a tab being eight columns."""
+ return len(line.expandtabs(TAB_WIDTH))
+
+
+def ident(*names):
+ """Return the lower case C spelling of one or more schema names."""
+ return '_'.join(name.replace('-', '_') for name in names)
+
+
+def macro(*names):
+ """Return the upper case C spelling of one or more schema names."""
+ return ident(*names).upper()
+
+
+def enum_tag(*names):
+ return f'_{ident(*names)}'
+
+
+def family_suffix(family_name):
+ suffix = family_name
+ if family_name.startswith('ctlv-'):
+ suffix = family_name.removeprefix('ctlv-')
+ return macro(suffix)
+
+
+def family_const(family_name):
+ if family_name == 'ctlv':
+ return 'CTLV_FAMILY_ROOT'
+ return f'CTLV_FAMILY_{family_suffix(family_name)}'
+
+
+def uapi_header(family_name, in_kernel=False):
+ """Return the include of the generated UAPI header of family_name."""
+ prefix = 'uapi/' if in_kernel else ''
+ return f'<{prefix}linux/{ident(family_name)}_gen.h>'
+
+
+def families_uapi_header(in_kernel=False):
+ prefix = 'uapi/' if in_kernel else ''
+ return f'<{prefix}linux/ctlv_families_gen.h>'
+
+
+def kernel_header(family_name):
+ """Return the include of the generated kernel header of family_name.
+
+ The root family's interface is installed beside <linux/ctlv.h>; any other
+ family's is private to the family implementation compiling it.
+ """
+ name = f'{ident(family_name)}_gen.h'
+ if family_name == spec.CORE_FAMILY_NAME:
+ return f'<linux/{name}>'
+ return f'"{name}"'
+
+
+def spec_path(path):
+ """Return path relative to the kernel tree that holds it.
+
+ ctlv-regen.sh resolves it from the tree root, so it must not depend on how
+ the generator was invoked. Outside a kernel tree it is recorded as given.
+ """
+ full = os.path.abspath(path)
+ relative = ''
+ while True:
+ relative = os.path.join(os.path.basename(full), relative)
+ parent = os.path.dirname(full)
+ if os.path.exists(os.path.join(parent, 'MAINTAINERS')):
+ return relative[:-1]
+ if parent == full:
+ return str(path)
+ full = parent
+
+
+def wrapped(opening, items, closing, trailer='', adaptive=False):
+ """Return the lines of one parenthesized list, wrapped at the limit.
+
+ trailer is what a continued line ends with beyond the item: a backslash
+ inside a macro definition, nothing elsewhere.
+ """
+ parts = [f'{item}, ' for item in items[:-1]] + [items[-1] + closing]
+ column = width(opening)
+ part_width = max(width(part.rstrip() + trailer) for part in parts)
+ indent_column = column
+ if adaptive:
+ indent_column = min(column, max(0, LINE_LIMIT - part_width))
+ indent = '\t' * (indent_column // TAB_WIDTH) + \
+ ' ' * (indent_column % TAB_WIDTH)
+ lines = []
+ line = opening
+ for part in parts:
+ if width(line + part.rstrip() + trailer) > LINE_LIMIT:
+ if line != opening:
+ lines.append(line.rstrip())
+ elif adaptive and opening.rstrip():
+ lines.append(opening.rstrip())
+ else:
+ line += part
+ continue
+ line = indent
+ line += part
+ lines.append(line.rstrip())
+ return lines
+
+
+class Writer:
+ """The lines of one generated file."""
+
+ def __init__(self):
+ self._lines = []
+
+ def line(self, text=''):
+ self._lines.append(text)
+
+ def comment(self, *lines):
+ """One comment block, one input line per output line."""
+ if len(lines) == 1:
+ self.line(f'/* {lines[0]} */')
+ return
+ self.line('/*')
+ for text in lines:
+ self.line(f' * {text}'.rstrip())
+ self.line(' */')
+
+ def define_block(self, name, *lines):
+ """One #define whose value takes more than the one line."""
+ header = f'#define {name} \\'
+ if width(header) > LINE_LIMIT and '(' in name:
+ macro, params = name.removesuffix(')').split('(', 1)
+ for text in wrapped(f'#define {macro}(',
+ tuple(params.split(', ')), ')', ' \\',
+ adaptive=True):
+ self.line(f'{text} \\')
+ else:
+ self.line(header)
+ for text in lines[:-1]:
+ self.line(f'{text} \\')
+ self.line(lines[-1])
+
+ def define(self, name, value):
+ """One #define, continued when it would run past the line limit."""
+ text = f'#define {name} {value}'
+ if width(text) <= LINE_LIMIT:
+ self.line(text)
+ return
+ self.define_block(name, f'\t{value}')
+
+ def define_call(self, name, function, *args):
+ """One #define whose value is a call, wrapped where it has to be."""
+ call = f'{function}({", ".join(args)})'
+ if width(f'#define {name} {call}') <= LINE_LIMIT:
+ self.line(f'#define {name} {call}')
+ return
+ if width(f'\t{call}') <= LINE_LIMIT:
+ self.define_block(name, f'\t{call}')
+ return
+ self.define_block(
+ name, *wrapped(f'\t{function}(', args, ')', ' \\',
+ adaptive=True))
+
+ def declaration(self, prefix, declarator):
+ """One declaration, continued when it would run past the limit."""
+ text = f'{prefix} {declarator}'
+ if width(text) <= LINE_LIMIT:
+ self.line(text)
+ return
+ indent = prefix[:len(prefix) - len(prefix.lstrip('\t'))]
+ self.line(prefix)
+ continuation = f'{indent}\t'
+ if width(f'{continuation}{declarator}') > LINE_LIMIT:
+ continuation = '\t'
+ self.line(f'{continuation}{declarator}')
+
+ def parenthesized(self, opening, items, closing):
+ """One parenthesized list, wrapped at the line limit."""
+ for text in wrapped(opening, items, closing, adaptive=True):
+ self.line(text)
+
+ def assertion(self, left, operator, right):
+ """One static_assert, broken after its operator when it must be."""
+ opening = 'static_assert('
+ text = f'{opening}{left} {operator} {right});'
+ if width(text) <= LINE_LIMIT:
+ self.line(text)
+ return
+ column = width(opening)
+ indent = '\t' * (column // TAB_WIDTH) + ' ' * (column % TAB_WIDTH)
+ self.line(f'{opening}{left} {operator}')
+ self.line(f'{indent}{right});')
+
+ def signature(self, prefix, name, params, closing=');'):
+ """One function signature, wrapped at the line limit."""
+ lines = wrapped(f'{prefix} {name}(', params, closing)
+ one_line = f'{prefix} {name}({", ".join(params)}{closing}'
+ if lines[0].endswith('(') and width(one_line) <= HARD_LINE_LIMIT:
+ self.line(one_line)
+ return
+ if max(width(line) for line in lines) <= LINE_LIMIT:
+ for text in lines:
+ self.line(text)
+ return
+ if name.startswith('*'):
+ self.line(f'{prefix} *')
+ name = name.removeprefix('*')
+ else:
+ self.line(prefix)
+ declarator = f'{name}({", ".join(params)}{closing}'
+ if width(declarator) <= HARD_LINE_LIMIT:
+ self.line(declarator)
+ return
+ opening = f'{name}('
+ lines = wrapped(opening, params, closing)
+ if max(width(line) for line in lines) > LINE_LIMIT:
+ self.line(opening)
+ self.parenthesized('\t', params, closing)
+ else:
+ for text in lines:
+ self.line(text)
+
+ def open_guard(self, guard):
+ self.line(f'#ifndef {guard}')
+ self.line(f'#define {guard}')
+ self.line()
+
+ def close_guard(self, guard):
+ self.line(f'#endif /* {guard} */')
+
+ def includes(self, *headers):
+ """One include block, an empty entry separating two groups."""
+ for header in headers:
+ self.line(f'#include {header}' if header else '')
+ self.line()
+
+ def text(self):
+ while self._lines and not self._lines[-1]:
+ self._lines.pop()
+ return '\n'.join(self._lines) + '\n'
+
+
+def preamble(origin, mode, kind, source=None):
+ """Return a Writer holding the preamble of one generated file."""
+ writer = Writer()
+ if kind == 'source':
+ writer.line(f'// SPDX-License-Identifier: {origin.license}')
+ else:
+ writer.line(f'/* SPDX-License-Identifier: {origin.license} */')
+ writer.line(f'/* Do not edit, regenerate with {REGEN}, from: */')
+ writer.line(f'/*\t{spec_path(source or origin.path)} */')
+ writer.line(f'/* CTLV-GEN {mode} {kind} */')
+ writer.line()
+ return writer
+
+
+class Names:
+ """The C names of one effective family's declarations.
+
+ A declaration is named after the family that owns it, so an inherited attr
+ keeps the name its own family's header gives it.
+ """
+
+ def __init__(self, family):
+ self.family = family
+ self.prefix = macro(family.name)
+ self.ident = ident(family.name)
+
+ def owner_prefix(self, family_id):
+ return macro(self.family.owners[family_id])
+
+ def owner_ident(self, family_id):
+ return ident(self.family.owners[family_id])
+
+ # Names that a family's UAPI header defines.
+
+ def family_const(self):
+ return family_const(self.family.name)
+
+ def group(self, group):
+ return f'{self.owner_prefix(group.family_id)}_GROUP_' \
+ f'{macro(group.name)}'
+
+ def group_enum(self, group):
+ return enum_tag(self.family.owners[group.family_id], 'group')
+
+ def group_attr_enum(self, group):
+ return enum_tag(self.family.owners[group.family_id],
+ 'attr', group.name)
+
+ def group_metadata_enum(self, group):
+ return enum_tag(self.family.owners[group.family_id],
+ 'attr', group.name, 'metadata')
+
+ def group_attr(self, group):
+ return f'_{self.owner_prefix(group.family_id)}_ATTR_' \
+ f'{macro(group.name)}'
+
+ def family_attr(self, group):
+ return f'_{self.owner_prefix(group.family_id)}_ATTR'
+
+ def local_attr_prefix(self, group):
+ return f'_{self.owner_prefix(group.family_id)}_ATTR_' \
+ f'{macro(group.name)}_'
+
+ def local_attr(self, attr):
+ group = self.family.groups[attr.group]
+ return f'{self.local_attr_prefix(group)}{macro(attr.name)}'
+
+ def attr(self, attr):
+ return f'{self.owner_prefix(attr.family_id)}_ATTR_' \
+ f'{macro(attr.group, attr.name)}'
+
+ def attr_unit(self, attr):
+ return f'{self.attr(attr)}_UNIT'
+
+ def attr_scale(self, attr):
+ return f'{self.attr(attr)}_SCALE'
+
+ def attr_set(self, attr_set):
+ return f'{self.owner_prefix(attr_set.family_id)}_SET_' \
+ f'{macro(attr_set.name)}'
+
+ def set_enum(self, attr_set):
+ return enum_tag(self.family.owners[attr_set.family_id], 'set')
+
+ def op_prefix(self, op):
+ return f'_{self.owner_prefix(op.family_id)}_OP_'
+
+ def op(self, op):
+ return f'{self.op_prefix(op)}{macro(op.name)}'
+
+ def operation(self, op):
+ return f'_{self.owner_prefix(op.family_id)}_OP'
+
+ def op_enum(self, op):
+ return enum_tag(self.family.owners[op.family_id], 'op')
+
+ def op_id(self, op):
+ return f'{self.owner_prefix(op.family_id)}_OP_{macro(op.name)}'
+
+ def definition(self, definition):
+ """The C type name of an enum definition."""
+ return enum_tag(self.family.owners[definition.family_id],
+ definition.name)
+
+ def value(self, definition, value_name):
+ """One enum value or flag bit."""
+ return f'{self.owner_prefix(definition.family_id)}_' \
+ f'{macro(definition.name, value_name)}'
+
+ def value_bit(self, definition, value_name):
+ return f'{self.value(definition, value_name)}_BIT'
+
+ def bit_enum(self, definition):
+ return enum_tag(self.family.owners[definition.family_id],
+ definition.name, 'bit')
+
+ def value_mask(self, definition):
+ """The mask of every bit a flags definition declares."""
+ return f'{self.owner_prefix(definition.family_id)}_' \
+ f'{macro(definition.name)}_MASK'
+
+ # Names that a family's kernel metadata defines: its own ops and the
+ # stubs it fills.
+
+ def op_schemas(self):
+ return f'{self.ident}_op_schemas'
+
+ def schema(self):
+ return f'{self.ident}_schema'
+
+ def stub_fill_schemas(self):
+ return f'{self.ident}_stub_fill_schemas'
+
+ def node_schemas(self, op, which):
+ # File scope, so no family prefix: an op name is unique within its
+ # effective family.
+ return f'{ident(op.name)}_{which}_node_schemas'
+
+ def stub_schemas(self, op, which):
+ return f'{ident(op.name)}_{which}_stub_schemas'
+
+ def fill_node_schemas(self, fill):
+ return f'{ident(fill.name)}_fill_node_schemas'
+
+ def message_node_schemas(self, message):
+ return f'{ident(message.name)}_node_schemas'
+
+ def message_schema(self, message):
+ """The tree the core builds one of its own messages against."""
+ return f'{self.ident}_{ident(message.name)}_schema'
+
+ def message_max_len(self, message):
+ return f'{self.prefix}_{macro(message.name)}_MAX_LEN'
+
+ # Names that a family's device descriptor defines. Named after the family
+ # that declared what they answer: an inherited op is answered by that
+ # family implementation, on whatever device it arrived at.
+
+ def descriptor(self):
+ return f'{self.ident}_family'
+
+ def ops(self):
+ return f'{self.ident}_ops'
+
+ def stub_ops(self):
+ return f'{self.ident}_stub_ops'
+
+ def stub_ident(self, fill):
+ return f'{self.ident}_{ident(fill.name)}'
+
+ def stub_callback(self, fill, which):
+ """One half of what this family does with an extension point."""
+ return f'{self.stub_ident(fill)}_{which}'
+
+ def lifecycle_hook(self, which):
+ """One operation in the lifecycle of this family's priv."""
+ return f'{self.ident}_{which}'
+
+ def op_ident(self, op):
+ return f'{self.owner_ident(op.family_id)}_{ident(op.name)}'
+
+ def callback(self, op):
+ return f'{self.op_ident(op)}_{op.type}'
+
+ def op_helper(self, op, what):
+ """What one device decides about one op."""
+ return f'{self.op_ident(op)}_{what}'
+
+ def attr_helper(self, attr, what):
+ """One helper for one attr: reading it, or appending it.
+
+ Named after the family that declares the attr, like the constant that
+ names it on the wire.
+ """
+ return f'{self.owner_ident(attr.family_id)}_' \
+ f'{ident(attr.group, attr.name)}_{what}'
+
+ def attr_iterator(self, attr):
+ """What walks every occurrence of one repeated attr."""
+ return f'{self.owner_ident(attr.family_id)}_{ident(attr.group)}_' \
+ f'for_each_{ident(attr.name)}'
+
+ def input_name(self, scope, attr):
+ """The stem of one request-placement-specific reader."""
+ owner_parts = self.family.name.split('-')
+ group = ()
+ if attr.group not in (*scope.name, *owner_parts):
+ group = (attr.group,)
+ parts = (*scope.name, *group, attr.name)
+ parts = tuple(part for index, part in enumerate(parts)
+ if not index or part != parts[index - 1])
+ return f'{self.ident}_{ident(*parts)}'
+
+ def input_helper(self, scope, attr, what):
+ return f'{self.input_name(scope, attr)}_{what}'
+
+ def input_iterator(self, scope, attr):
+ return self.input_helper(scope, attr, 'for_each')
+
+ def input_ref(self, scope, attr):
+ return self.input_helper(scope, attr, 'ref')
+
+ def family_hook(self, which):
+ """One half of the pair that wraps every op of this family."""
+ return f'{self.ident}_op_{which}'
+
+ def op_hook(self, op, which):
+ """One half of the pair that wraps this op alone."""
+ return f'{self.op_ident(op)}_{which}'
+
+ # Where a generated file lives: the installed constants, the schema
+ # metadata a family compiles, and the descriptor it registers.
+
+ def uapi_guard(self):
+ return f'_UAPI_LINUX_{self.prefix}_GEN_H'
+
+ def uapi_header(self, in_kernel=False):
+ return uapi_header(self.family.name, in_kernel)
+
+ def kernel_guard(self):
+ return f'_LINUX_{self.prefix}_GEN_H'
+
+ def kernel_header(self):
+ return kernel_header(self.family.name)
+
+ def user_guard(self):
+ return f'_CTLV_{self.prefix}_USER_GEN_H'
+
+ def user_header(self):
+ return f'"{self.ident}_user_gen.h"'
+
+ # Names that a family's userspace binding defines, after the family that
+ # declared the op, as a per-op helper is in the kernel.
+
+ def user_op(self, op):
+ return f'{self.owner_ident(op.family_id)}_{ident(op.name)}'
+
+ def user_op_helper(self, op, what):
+ return f'{self.user_op(op)}_{what}'
+
+ def user_op_len(self, op, what):
+ """One length of one op, as its specification computed it."""
+ return f'{self.op_id(op)}_{macro(what)}'
+
+ def user_node_schema(self, op, which):
+ # File scope, so no family prefix, as on the kernel side.
+ return f'{ident(op.name)}_{which}_node_schema'
+
+ def user_spec(self):
+ """What this family's specification says, to compare a device to."""
+ return f'{self.ident}_spec'
+
+ def user_op_specs(self):
+ return f'{self.ident}_op_specs'
+
+
+def attr_getter_value(attr):
+ """Whether an attr has a getter beside the reader that locates it."""
+ return attr.decl.kind != 'blob' and \
+ not (attr.decl.kind == 'nested' and attr.decl.max_occurrences != 1)
+
+
+def input_readers(names, scope, attr):
+ readers = [names.input_ref(scope, attr)]
+ readers.append(names.input_iterator(scope, attr)
+ if attr.decl.max_occurrences != 1
+ else names.input_helper(scope, attr, 'present'))
+ if attr.decl.kind == 'blob':
+ readers.append(names.input_helper(scope, attr, 'resolve'))
+ elif attr_getter_value(attr):
+ readers.append(names.input_helper(scope, attr, 'get'))
+ return tuple(readers)
+
+
+def attr_writers(names, attr):
+ """The names of what appends one attr, as the family API declares them.
+
+ A blob has none: a request carries one and a reply never does.
+ """
+ if attr.decl.kind == 'blob':
+ return ()
+ what = 'start' if attr.decl.kind == 'nested' else 'put'
+ return (names.attr_helper(attr, what),)
+
+
+# What a blob attr may be backed by, and the libctlv call describing one. A
+# putter exists per backing the placement permits, so what a caller may
+# describe is decided by the specification.
+BLOB_PUTTERS = {
+ 'va': ('va', ('const void *address', '__u64 length')),
+}
+
+# What an event gets in the place of a wrapper.
+EVENT_HELPERS = ('is', 'subscribe', 'unsubscribe')
+
+# The lengths a user-mode header states for one op, by sizes.OpSizes member.
+USER_LENGTHS = {
+ 'request_max_len': 'request-max-len',
+ 'action_reply_max_len': 'action-reply-max-len',
+ 'query_reply_min_len': 'query-reply-min-len',
+ 'query_reply_suggested_len': 'query-reply-suggested-len',
+ 'event_record_max_len': 'event-record-max-len',
+}
+
+
+def user_attr_readers(names, attr):
+ """The names of what reads one attr in the userspace binding."""
+ if attr.decl.kind == 'blob':
+ return ()
+ readers = [names.attr_iterator(attr) if attr.decl.max_occurrences != 1
+ else names.attr_helper(attr, 'present')]
+ if attr_getter_value(attr):
+ readers.append(names.attr_helper(attr, 'get'))
+ return tuple(readers)
+
+
+def user_attr_writers(names, attr):
+ """The names of what appends one attr in the userspace binding.
+
+ A blob has one putter per backing the placement allows, where the kernel
+ side has none.
+ """
+ if attr.decl.kind == 'blob':
+ return tuple(names.attr_helper(attr, f'put_{BLOB_PUTTERS[backing][0]}')
+ for backing in attr.decl.blob.backings)
+ what = 'start' if attr.decl.kind == 'nested' else 'put'
+ return (names.attr_helper(attr, what),)
+
+
+def user_symbols(family):
+ """Return every C name of family's binding, mapped to its origin.
+
+ The binding covers this family's own attrs and ops, reaching the rest
+ through the parent's header.
+ """
+ names = Names(family)
+ table = {}
+
+ def add(name, origin):
+ previous = table.get(name)
+ if previous is not None:
+ raise CtlvError(f'{family.path}: {origin}',
+ f'generated name {name} is already used by '
+ f'{previous}')
+ table[name] = origin
+
+ for group in family.local_groups:
+ for attr in group.attrs:
+ what = f'attr {attr.full_name}'
+ for helper in user_attr_readers(names, attr) + \
+ user_attr_writers(names, attr):
+ add(helper, what)
+ for op in family.local_ops:
+ what = f'op {op.name}'
+ # An event has those helpers and no wrapper of its own name.
+ if op.type == 'event':
+ for helper in EVENT_HELPERS:
+ add(names.user_op_helper(op, helper), what)
+ else:
+ add(names.user_op(op), what)
+ if op.request is not None:
+ add(names.user_op_helper(op, 'request_new'), what)
+ for field, what_len in USER_LENGTHS.items():
+ if getattr(op.sizes, field):
+ add(names.user_op_len(op, what_len), what)
+ return table
+
+
+def _stubs_anywhere(node):
+ return bool(node.stubs) or any(_stubs_anywhere(child)
+ for child in node.children())
+
+
+def symbols(family):
+ """Return every generated C name of family, mapped to its declaration.
+
+ Dashes become underscores, so two YAML names can spell one C name. That
+ has to be an error: one of the two constants would silently not exist.
+ """
+ names = Names(family)
+ table = {}
+
+ def add(name, origin):
+ previous = table.get(name)
+ if previous is not None:
+ raise CtlvError(f'{family.path}: {origin}',
+ f'generated name {name} is already used by '
+ f'{previous}')
+ table[name] = origin
+
+ for name in LIFECYCLE_API_SYMBOLS:
+ add(name, 'the CTLV lifecycle API')
+ add(names.family_const(), f'family {family.name}')
+ # A child header may include every ancestor's family API, and built-in
+ # families also share one linker namespace with the root.
+ for family_id, owner in family.owners.items():
+ owner_ident = ident(owner)
+ if any(op.family_id == family_id for op in family.ops):
+ add(f'{owner_ident}_op_schemas', f'family {owner}')
+ add(f'{owner_ident}_schema', f'family {owner}')
+ add(f'{owner_ident}_family', f'family {owner}')
+ for which in ('pre', 'post'):
+ add(f'{owner_ident}_op_{which}', f'family {owner}')
+ for which in ('priv_size', 'pre_init', 'init', 'post_init',
+ 'pre_fini', 'fini'):
+ add(f'{owner_ident}_device_{which}', f'family {owner}')
+ for fill in family.fills:
+ for which in ('fill', 'read'):
+ add(names.stub_callback(fill, which), f'attr set {fill.name}')
+ for definition in family.definitions.values():
+ what = f'{definition.kind} {definition.name}'
+ if definition.kind == 'enum':
+ add(f'enum {names.definition(definition)}', what)
+ else:
+ add(f'enum {names.bit_enum(definition)}', what)
+ add(names.value_mask(definition), what)
+ for value_name, _ in definition.values:
+ if definition.kind == 'flags':
+ add(names.value_bit(definition, value_name), what)
+ add(names.value(definition, value_name), what)
+ group_enums = set()
+ family_attrs = set()
+ for group in family.groups.values():
+ group_enum = f'enum {names.group_enum(group)}'
+ if group_enum not in group_enums:
+ add(group_enum, f'family {family.owners[group.family_id]} groups')
+ group_enums.add(group_enum)
+ family_attr = names.family_attr(group)
+ if family_attr not in family_attrs:
+ add(family_attr, f'family {family.owners[group.family_id]} attrs')
+ family_attrs.add(family_attr)
+ add(names.group(group), f'attr group {group.name}')
+ add(f'enum {names.group_attr_enum(group)}',
+ f'attr group {group.name}')
+ if any(attr.decl.unit is not None for attr in group.attrs):
+ add(f'enum {names.group_metadata_enum(group)}',
+ f'attr group {group.name} metadata')
+ add(names.group_attr(group), f'attr group {group.name}')
+ for attr in group.attrs:
+ what = f'attr {attr.full_name}'
+ add(names.local_attr(attr), what)
+ add(names.attr(attr), what)
+ if attr.decl.unit is not None:
+ add(names.attr_unit(attr), what)
+ add(names.attr_scale(attr), what)
+ for helper in attr_writers(names, attr):
+ add(helper, what)
+ for ancestor in family.chain[:-1]:
+ ancestor_names = Names(ancestor)
+ owner_family_id = next(
+ family_id for family_id, owner in family.owners.items()
+ if owner == ancestor.name)
+
+ for scope in semantics.input_scopes(family, owner_family_id):
+ for child in scope.node.children():
+ what = f'attr {child.attr.full_name} in ancestor ' \
+ f'{ancestor.name}'
+ for helper in input_readers(ancestor_names, scope,
+ child.attr):
+ add(helper, what)
+ for scope in semantics.input_scopes(family):
+ for child in scope.node.children():
+ what = f'attr {child.attr.full_name} in {".".join(scope.name)}'
+ for helper in input_readers(names, scope, child.attr):
+ add(helper, what)
+ set_enums = set()
+ for attr_set in family.sets.values():
+ set_enum = f'enum {names.set_enum(attr_set)}'
+ if set_enum not in set_enums:
+ add(set_enum, f'family {family.owners[attr_set.family_id]} sets')
+ set_enums.add(set_enum)
+ add(names.attr_set(attr_set), f'attr set {attr_set.name}')
+ operation_enums = set()
+ for op in family.ops:
+ what = f'op {op.name}'
+ operation_enum = f'enum {names.op_enum(op)}'
+ if operation_enum not in operation_enums:
+ add(operation_enum,
+ f'family {family.owners[op.family_id]} operations')
+ add(names.operation(op),
+ f'family {family.owners[op.family_id]} operations')
+ operation_enums.add(operation_enum)
+ add(names.op(op), what)
+ add(names.op_id(op), what)
+ if op.type == 'event':
+ add(names.op_helper(op, 'create'), what)
+ continue
+ add(names.callback(op), what)
+ if op.optional:
+ add(names.op_helper(op, 'provide'), what)
+ if op.availability == 'dynamic':
+ add(names.op_helper(op, 'enable'), what)
+ add(names.op_helper(op, 'disable'), what)
+ if op.pre_post:
+ add(names.op_hook(op, 'pre'), what)
+ add(names.op_hook(op, 'post'), what)
+ for message in family.messages:
+ what = f'attr set {message.name}'
+ add(names.message_node_schemas(message), what)
+ add(names.message_schema(message), what)
+ add(names.message_max_len(message), what)
+ if family.fills:
+ add(names.stub_fill_schemas(), f'family {family.name}')
+ add(names.stub_ops(), f'family {family.name}')
+ if any(op.type != 'event' for op in family.local_ops):
+ add(names.ops(), f'family {family.name}')
+ for op in family.local_ops:
+ what = f'op {op.name}'
+ for which in ('request', 'reply'):
+ root = getattr(op, which)
+ if root is None:
+ continue
+ add(names.node_schemas(op, which), what)
+ if _stubs_anywhere(root):
+ add(names.stub_schemas(op, which), what)
+ for fill in family.fills:
+ add(names.fill_node_schemas(fill), f'attr set {fill.name}')
+ return table
diff --git a/tools/ctlv/ctlvgen/lib/compat.py b/tools/ctlv/ctlvgen/lib/compat.py
new file mode 100644
index 000000000000..4cf93af06ea9
--- /dev/null
+++ b/tools/ctlv/ctlvgen/lib/compat.py
@@ -0,0 +1,821 @@
+# SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+"""ABI snapshots and the compatibility checker.
+
+A change is compatible only if the new schema still contains everything the
+checked-in snapshot describes, with the same IDs, no narrowed bound and no
+placement that became required. A new placement has to be optional. Updating a
+snapshot is deliberate: it is how the change gets reviewed.
+"""
+
+from __future__ import annotations
+
+import yaml
+
+from . import spec
+from . import yamlload
+from .errors import CtlvError
+
+HEADER = '''\
+# SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause)
+%YAML 1.2
+---
+# Checked-in ABI snapshot of the {name} family, written by
+# "make -C tools/ctlv abi-update".
+#
+# The checker compares this family's declarations and inherited stub fills
+# against this file. Updating it is how a UAPI change gets reviewed; an update
+# that removes or renumbers anything, narrows a bound, or makes a placement
+# required is an ABI break, whatever this file says afterwards.
+'''
+
+REGISTRY_HEADER = '''\
+# SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause)
+%YAML 1.2
+---
+# Checked-in ABI baseline of the ordered CTLV family registry.
+# Updated deliberately by "make -C tools/ctlv abi-update".
+'''
+
+
+class _Dumper(yaml.SafeDumper):
+ """Dumper that never emits anchors, which the strict loader rejects."""
+
+ def ignore_aliases(self, data):
+ return True
+
+ def increase_indent(self, flow=False, indentless=False):
+ # Indent a sequence under its key, not flush left as pyyaml does.
+ return super().increase_indent(flow, False)
+
+
+class _FlowMap(dict):
+ pass
+
+
+class _FlowSeq(list):
+ pass
+
+
+_Dumper.add_representer(
+ _FlowMap,
+ lambda dumper, value: dumper.represent_mapping(
+ 'tag:yaml.org,2002:map', value, flow_style=True))
+_Dumper.add_representer(
+ _FlowSeq,
+ lambda dumper, value: dumper.represent_sequence(
+ 'tag:yaml.org,2002:seq', value, flow_style=True))
+
+
+def build(family):
+ """Return one family's ABI in the comparison form."""
+ return {
+ 'family': family.name,
+ 'family-id': family.family_id,
+ 'parent': family.parent,
+ 'pre-init': family.pre_init,
+ 'post-init': family.post_init,
+ 'pre-fini': family.pre_fini,
+ 'definitions': _definitions(family.local_definitions),
+ 'groups': _groups(family.local_groups),
+ 'sets': _sets(family.local_sets),
+ 'fills': _fills(family.fills),
+ 'ops': _ops(family.local_ops),
+ }
+
+
+def render(family):
+ """Return one family's compact snapshot as YAML text."""
+ return render_data(build(family))
+
+
+def render_data(data):
+ """Render comparison-form data as a compact snapshot."""
+ body = yaml.dump(_compact(data), Dumper=_Dumper, sort_keys=False,
+ default_flow_style=False, width=1000000)
+ body = '\n'.join(line for source in body.splitlines()
+ for line in _wrap_flow(source)) + '\n'
+ return HEADER.format(name=data['family']) + body
+
+
+def _wrap_flow(line, width=80):
+ """Wrap one flow collection with continuations aligned after its opener."""
+ if len(line) <= width:
+ return (line,)
+ starts = [index for index in (line.find('{'), line.find('['))
+ if index >= 0]
+ if not starts:
+ return (line,)
+ start = min(starts)
+ end, items = _flow_items(line, start)
+ if end is None:
+ return (line,)
+
+ prefix = line[:start + 1]
+ suffix = line[end:]
+ indent = ' ' * (start + 1)
+ wrapped = []
+ current = prefix
+ for index, item in enumerate(items):
+ last = index + 1 == len(items)
+ token = item + ('' if last else ',')
+ separator = '' if current.endswith(('{', '[')) else ' '
+ closing = len(suffix) if last else 0
+ if len(current) + len(separator) + len(token) + closing > width:
+ wrapped.append(current)
+ current = indent + token
+ else:
+ current += separator + token
+ current += suffix
+ wrapped.append(current)
+ return tuple(wrapped)
+
+
+def _flow_items(line, start):
+ pairs = {'{': '}', '[': ']'}
+ stack = [line[start]]
+ quote = None
+ escape = False
+ item_start = start + 1
+ items = []
+ for index in range(start + 1, len(line)):
+ char = line[index]
+ if quote is not None:
+ if escape:
+ escape = False
+ elif char == '\\':
+ escape = True
+ elif char == quote:
+ quote = None
+ continue
+ if char in ('"', "'"):
+ quote = char
+ elif char in pairs:
+ stack.append(char)
+ elif char in pairs.values():
+ if pairs[stack[-1]] != char:
+ return None, ()
+ stack.pop()
+ if not stack:
+ items.append(line[item_start:index].strip())
+ return index, tuple(item for item in items if item)
+ elif char == ',' and len(stack) == 1:
+ items.append(line[item_start:index].strip())
+ item_start = index + 1
+ return None, ()
+
+
+def check(family, path):
+ """Compare a family against its snapshot, or raise with every problem."""
+ snapshot = _expand(yamlload.load(path))
+ problems = _compare(snapshot, build(family))
+ if problems:
+ raise CtlvError(f'{family.name} against {path}',
+ 'the schema is not compatible with its snapshot\n' +
+ '\n'.join(f' {problem}' for problem in problems))
+
+
+def _compact(data):
+ family = _FlowMap({
+ 'name': data['family'],
+ 'id': data['family-id'],
+ })
+ if data['parent'] is not None:
+ family['inherits'] = data['parent']
+ for name in ('pre-init', 'post-init', 'pre-fini'):
+ if data[name]:
+ family[name] = True
+
+ compact = {'family': family}
+ for kind, section in (('enum', 'enums'), ('flags', 'flags')):
+ definitions = {
+ name: _FlowMap(entry['values'])
+ for name, entry in data['definitions'].items()
+ if entry['kind'] == kind
+ }
+ if definitions:
+ compact[section] = definitions
+ if data['groups']:
+ compact['attr-groups'] = _compact_groups(data['groups'])
+ if data['sets']:
+ compact['attr-sets'] = {
+ name: _FlowMap({
+ 'id': entry['set-id'],
+ **({'stub': True} if entry.get('stub') else {}),
+ })
+ for name, entry in data['sets'].items()
+ }
+ if data['fills']:
+ compact['fills'] = {
+ name: _compact_tree(nodes)
+ for name, nodes in data['fills'].items()
+ }
+ if data['ops']:
+ compact['ops'] = {
+ name: _compact_op(entry)
+ for name, entry in data['ops'].items()
+ }
+ return compact
+
+
+def _compact_groups(groups):
+ compact = {}
+ for name, group in groups.items():
+ compact[name] = {
+ 'id': group['group-id'],
+ 'attrs': {
+ attr_name: _compact_attr(attr)
+ for attr_name, attr in group['attrs'].items()
+ },
+ }
+ return compact
+
+
+def _compact_attr(attr):
+ kind = attr['kind']
+ compact = _FlowMap({'id': attr['local-id']})
+ if kind in ('enum', 'flags'):
+ compact[kind] = attr['definition']
+ elif kind == 'blob':
+ blob = attr['blob']
+ compact['blob'] = blob['direction']
+ compact['backings'] = _FlowSeq(blob['backings'])
+ compact['max-length'] = blob['max-length']
+ if blob['alignment'] != 1:
+ compact['alignment'] = blob['alignment']
+ else:
+ compact['type'] = kind
+
+ if kind in ('uint', 'sint'):
+ low, high = ((0, spec.U64_MAX) if kind == 'uint'
+ else (spec.S64_MIN, spec.S64_MAX))
+ minimum = attr.get('min', low)
+ maximum = attr.get('max', high)
+ if minimum != low:
+ compact['min'] = minimum
+ if maximum != high:
+ compact['max'] = maximum
+ if 'unit' in attr:
+ compact['unit'] = attr['unit']
+ if attr['scale']:
+ compact['scale'] = attr['scale']
+ elif kind in ('string', 'binary'):
+ minimum = attr.get('min-length', 0)
+ maximum = attr['max-length']
+ if kind == 'binary' and minimum == maximum:
+ compact['length'] = maximum
+ else:
+ if minimum:
+ compact['min-length'] = minimum
+ compact['max-length'] = maximum
+
+ maximum = attr['max-occurrences']
+ if maximum != 1:
+ compact['max-occurrences'] = (
+ 'unlimited' if maximum == spec.UNLIMITED else maximum)
+ return compact
+
+
+def _compact_op(op):
+ abi = _FlowMap({
+ 'id': op['op-id'],
+ 'type': op['type'],
+ })
+ if (op['type'] != 'event' and
+ op.get('fd-access') != spec.DEFAULT_FD_ACCESS):
+ abi['fd-access'] = op.get('fd-access')
+ availability = op.get('availability', spec.DEFAULT_AVAILABILITY)
+ if availability != spec.DEFAULT_AVAILABILITY:
+ abi['availability'] = availability
+ if op.get('optional', False):
+ abi['optional'] = True
+ if op.get('pre-post', False):
+ abi['pre-post'] = True
+ compact = {'abi': abi}
+ if op.get('request'):
+ compact['request'] = _compact_tree(op['request'])
+ if op.get('reply') is not None:
+ compact['reply'] = _compact_tree(op['reply'])
+ return compact
+
+
+def _compact_tree(nodes):
+ required = []
+ optional = []
+ for node in nodes:
+ destination = required if node['required'] else optional
+ destination.append(_compact_node(node))
+ compact = {}
+ if required:
+ compact['attrs'] = _compact_nodes(required)
+ if optional:
+ compact['optional-attrs'] = _compact_nodes(optional)
+ if compact and _simple_tree_length(compact) <= 54:
+ return _FlowMap(compact)
+ return compact
+
+
+def _simple_tree_length(tree):
+ length = 2
+ for index, (name, selectors) in enumerate(tree.items()):
+ if not isinstance(selectors, _FlowSeq):
+ return 55
+ if index:
+ length += 2
+ length += len(name) + 4
+ length += sum(len(selector) + 2 for selector in selectors)
+ return length
+
+
+def _compact_nodes(nodes):
+ if all(isinstance(node, str) for node in nodes):
+ return _FlowSeq(nodes)
+ return nodes
+
+
+def _compact_node(node):
+ name = node.get('attr', node.get('stub'))
+ children = _compact_tree(node.get('children', []))
+ if 'reenters' in node:
+ children['reenters'] = node['reenters']
+ if not children:
+ return name
+ return {name: children}
+
+
+def _expand(compact):
+ family = compact['family']
+ family_id = family['id']
+ definitions = {}
+ for kind, section in (('enum', 'enums'), ('flags', 'flags')):
+ for name, values in compact.get(section, {}).items():
+ definitions[name] = {
+ 'kind': kind,
+ 'values': values,
+ }
+ return {
+ 'family': family['name'],
+ 'family-id': family_id,
+ 'parent': family.get('inherits'),
+ 'pre-init': family.get('pre-init', False),
+ 'post-init': family.get('post-init', False),
+ 'pre-fini': family.get('pre-fini', False),
+ 'definitions': definitions,
+ 'groups': _expand_groups(compact.get('attr-groups', {})),
+ 'sets': _expand_sets(compact.get('attr-sets', {})),
+ 'fills': {
+ name: _expand_tree(tree)
+ for name, tree in compact.get('fills', {}).items()
+ },
+ 'ops': _expand_ops(compact.get('ops', {})),
+ }
+
+
+def _expand_groups(groups):
+ expanded = {}
+ for name, group in groups.items():
+ expanded[name] = {
+ 'group-id': group['id'],
+ 'attrs': {
+ attr_name: _expand_attr(attr)
+ for attr_name, attr in group['attrs'].items()
+ },
+ }
+ return expanded
+
+
+def _expand_attr(compact):
+ primary = [name for name in ('type', 'enum', 'flags', 'blob')
+ if name in compact]
+ if len(primary) != 1:
+ raise CtlvError('ABI snapshot attr',
+ 'expected one of type, enum, flags or blob')
+ kind = compact[primary[0]] if primary[0] == 'type' else primary[0]
+ attr = {
+ 'local-id': compact['id'],
+ 'kind': kind,
+ 'max-occurrences': compact.get('max-occurrences', 1),
+ }
+ if attr['max-occurrences'] == 'unlimited':
+ attr['max-occurrences'] = spec.UNLIMITED
+ if kind in ('enum', 'flags'):
+ attr['definition'] = compact[kind]
+ elif kind in ('uint', 'sint'):
+ low, high = ((0, spec.U64_MAX) if kind == 'uint'
+ else (spec.S64_MIN, spec.S64_MAX))
+ attr['min'] = compact.get('min', low)
+ attr['max'] = compact.get('max', high)
+ if 'unit' in compact:
+ attr['unit'] = compact['unit']
+ attr['scale'] = compact.get('scale', 0)
+ elif kind in ('string', 'binary'):
+ if 'length' in compact:
+ attr['min-length'] = compact['length']
+ attr['max-length'] = compact['length']
+ else:
+ attr['min-length'] = compact.get('min-length', 0)
+ attr['max-length'] = compact['max-length']
+ elif kind == 'blob':
+ attr['blob'] = {
+ 'direction': compact['blob'],
+ 'backings': list(compact['backings']),
+ 'max-length': compact['max-length'],
+ 'alignment': compact.get('alignment', 1),
+ }
+ return attr
+
+
+def _expand_sets(attr_sets):
+ return {
+ name: {
+ 'set-id': entry['id'],
+ **({'stub': True} if entry.get('stub') else {}),
+ }
+ for name, entry in attr_sets.items()
+ }
+
+
+def _expand_ops(ops):
+ expanded = {}
+ for name, op in ops.items():
+ abi = op['abi']
+ op_type = abi['type']
+ entry = {
+ 'op-id': abi['id'],
+ 'type': op_type,
+ 'availability': abi.get(
+ 'availability', spec.DEFAULT_AVAILABILITY),
+ 'optional': abi.get('optional', False),
+ 'pre-post': abi.get('pre-post', False),
+ 'request': (None if op_type == 'event'
+ else _expand_tree(op.get('request', {}))),
+ 'reply': (None if 'reply' not in op
+ else _expand_tree(op['reply'])),
+ }
+ if op_type != 'event':
+ entry['fd-access'] = abi.get(
+ 'fd-access', spec.DEFAULT_FD_ACCESS)
+ expanded[name] = entry
+ return expanded
+
+
+def _expand_tree(tree):
+ nodes = []
+ for required, key in ((True, 'attrs'), (False, 'optional-attrs')):
+ for selector in tree.get(key, []):
+ nodes.append(_expand_node(selector, required))
+ return nodes
+
+
+def _expand_node(selector, required):
+ if isinstance(selector, str):
+ name = selector
+ nested = {}
+ else:
+ if not isinstance(selector, dict) or len(selector) != 1:
+ raise CtlvError('ABI snapshot tree',
+ 'expected an attr name or one nested attr')
+ name, nested = next(iter(selector.items()))
+ if '.' not in name:
+ return {'stub': name, 'required': required}
+ node = {'attr': name, 'required': required}
+ children = _expand_tree(nested)
+ if children:
+ node['children'] = children
+ if 'reenters' in nested:
+ node['reenters'] = nested['reenters']
+ return node
+
+
+def registry_build(registry):
+ """Return the registry in the same normalized form as its source."""
+ families = []
+ for entry in registry.entries:
+ if entry.deprecated:
+ families.append({'name': entry.name, 'deprecated': True})
+ else:
+ families.append(entry.name)
+ return {'families': families}
+
+
+def registry_render(registry):
+ body = yaml.dump(registry_build(registry), Dumper=_Dumper,
+ sort_keys=False, default_flow_style=False, width=76)
+ return REGISTRY_HEADER + body
+
+
+def registry_problems(old, current, exact):
+ problems = []
+ for family_id, was in enumerate(old.entries):
+ if family_id >= len(current.entries):
+ problems.append(f'family ID {family_id} ({was.name}) was removed')
+ continue
+ now = current.entries[family_id]
+ if now.name != was.name:
+ problems.append(f'family ID {family_id} was {was.name}, now '
+ f'{now.name}')
+ elif was.deprecated and not now.deprecated:
+ problems.append(f'family ID {family_id} ({was.name}) was reused '
+ 'after deprecation')
+ if exact and not problems and old.entries != current.entries:
+ problems.append('the registry ABI baseline needs updating')
+ return problems
+
+
+def registry_check(paths, registry, exact=True):
+ baseline = spec.load_registry(paths, paths.registry_abi)
+ problems = registry_problems(baseline, registry, exact)
+ if problems:
+ raise CtlvError(f'{paths.registry} against {paths.registry_abi}',
+ 'the family registry is not compatible with its '
+ 'baseline\n' +
+ '\n'.join(f' {problem}' for problem in problems))
+
+
+def _definitions(items):
+ definitions = {}
+ for definition in items:
+ definitions[definition.name] = {
+ 'kind': definition.kind,
+ 'values': dict(definition.values),
+ }
+ return definitions
+
+
+def _groups(items):
+ groups = {}
+ for group in items:
+ groups[group.name] = {
+ 'group-id': group.group_id,
+ 'attrs': {attr.name: _attr(attr) for attr in group.attrs},
+ }
+ return groups
+
+
+def _attr(attr):
+ decl = attr.decl
+ entry = {'local-id': attr.local_id, 'kind': decl.kind}
+ if decl.ref is not None:
+ entry['definition'] = decl.ref
+ for name, value in (('min', decl.min), ('max', decl.max),
+ ('min-length', decl.min_length),
+ ('max-length', decl.max_length),
+ ('unit', decl.unit), ('scale', decl.scale)):
+ if value is not None:
+ entry[name] = value
+ if decl.blob is not None:
+ entry['blob'] = {
+ 'direction': decl.blob.direction,
+ 'backings': list(decl.blob.backings),
+ 'max-length': decl.blob.max_length,
+ 'alignment': decl.blob.alignment,
+ }
+ entry['max-occurrences'] = decl.max_occurrences
+ return entry
+
+
+def _sets(attr_sets):
+ sets = {}
+ for attr_set in attr_sets:
+ entry = {
+ 'set-id': attr_set.set_id,
+ }
+ if attr_set.stub:
+ entry['stub'] = True
+ sets[attr_set.name] = entry
+ return sets
+
+
+def _fills(fills):
+ return {fill.name: _tree(fill.root) for fill in fills}
+
+
+def _ops(items):
+ ops = {}
+ for op in items:
+ entry = {
+ 'op-id': op.local_id,
+ 'type': op.type,
+ 'availability': op.availability,
+ 'optional': op.optional,
+ 'pre-post': op.pre_post,
+ 'request': _tree(op.request),
+ 'reply': _tree(op.reply),
+ }
+ if op.type != 'event':
+ entry['fd-access'] = op.fd_access
+ ops[op.name] = entry
+ return ops
+
+
+def _tree(root):
+ if root is None:
+ return None
+ children = [_node(child) for child in root.children()]
+ children += [_stub(stub) for stub in root.stubs]
+ return children
+
+
+def _node(node):
+ entry = {'attr': node.attr.full_name, 'required': node.required}
+ if node.reenters is not None:
+ # The members are recorded above, so record where they come from.
+ entry['reenters'] = node.reenters
+ return entry
+ children = _tree(node)
+ if children:
+ entry['children'] = children
+ return entry
+
+
+def _stub(stub):
+ return {'stub': stub.name, 'required': not stub.optional}
+
+
+def _compare(old, new):
+ problems = []
+ if old['family'] != new['family']:
+ problems.append(f'family was renamed from {old["family"]} to '
+ f'{new["family"]}')
+ if old['family-id'] != new['family-id']:
+ problems.append(f'family-id was {old["family-id"]}, now '
+ f'{new["family-id"]}')
+ # A name carries its ancestry, so reparenting renames every symbol.
+ if old['parent'] != new['parent']:
+ problems.append(f'the inherited family was {old["parent"]}, now '
+ f'{new["parent"]}')
+ for key in ('pre-init', 'post-init', 'pre-fini'):
+ if old.get(key, False) != new[key]:
+ problems.append(f'family: {key} was {old.get(key, False)}, now '
+ f'{new[key]}')
+ _compare_definitions(problems, old['definitions'], new['definitions'])
+ _compare_groups(problems, old['groups'], new['groups'])
+ _compare_sets(problems, old['sets'], new['sets'])
+ _compare_fills(problems, old['fills'], new['fills'])
+ _compare_ops(problems, old['ops'], new['ops'])
+ return problems
+
+
+def _compare_definitions(problems, old, new):
+ for name, was in old.items():
+ now = new.get(name)
+ if now is None:
+ problems.append(f'{was["kind"]} {name} was removed')
+ continue
+ where = f'{was["kind"]} {name}'
+ if now['kind'] != was['kind']:
+ problems.append(f'{where} is now a {now["kind"]} definition')
+ for value, number in was['values'].items():
+ if value not in now['values']:
+ problems.append(f'{where}: value {value} was removed')
+ elif now['values'][value] != number:
+ problems.append(f'{where}: value {value} was {number}, now '
+ f'{now["values"][value]}')
+
+
+def _compare_groups(problems, old, new):
+ for name, was in old.items():
+ now = new.get(name)
+ if now is None:
+ problems.append(f'attr group {name} was removed')
+ continue
+ where = f'attr group {name}'
+ if was['group-id'] != now['group-id']:
+ problems.append(f'{where}: group-id was {was["group-id"]}, now '
+ f'{now["group-id"]}')
+ for attr, attr_was in was['attrs'].items():
+ attr_now = now['attrs'].get(attr)
+ if attr_now is None:
+ problems.append(f'attr {name}.{attr} was removed')
+ continue
+ _compare_attr(problems, f'attr {name}.{attr}', attr_was, attr_now)
+
+
+def _compare_attr(problems, where, was, now):
+ for key in ('local-id', 'kind', 'definition', 'unit', 'scale'):
+ if was.get(key) != now.get(key):
+ problems.append(f'{where}: {key} was {was.get(key)}, now '
+ f'{now.get(key)}')
+ for key in ('min', 'min-length'):
+ if key in was and now.get(key, 0) > was[key]:
+ problems.append(f'{where}: {key} was narrowed from {was[key]} to '
+ f'{now[key]}')
+ for key in ('max', 'max-length'):
+ if key in was and now.get(key, 0) < was[key]:
+ problems.append(f'{where}: {key} was narrowed from {was[key]} to '
+ f'{now[key]}')
+ _compare_occurrences(problems, where, was, now)
+ if 'blob' in was:
+ _compare_blob(problems, where, was['blob'], now.get('blob', {}))
+
+
+def _compare_occurrences(problems, where, was, now):
+ old_max = was['max-occurrences']
+ new_max = now['max-occurrences']
+ if old_max == new_max:
+ return
+ if old_max == spec.UNLIMITED or new_max < old_max:
+ problems.append(f'{where}: max-occurrences was narrowed from '
+ f'{_occurrences(old_max)} to {_occurrences(new_max)}')
+
+
+def _compare_blob(problems, where, was, now):
+ for key in ('direction', 'alignment'):
+ if was.get(key) != now.get(key):
+ problems.append(f'{where}: blob {key} was {was.get(key)}, now '
+ f'{now.get(key)}')
+ for backing in was.get('backings', []):
+ if backing not in now.get('backings', []):
+ problems.append(f'{where}: blob backing {backing} was removed')
+ if now.get('max-length', 0) < was.get('max-length', 0):
+ problems.append(f'{where}: blob max-length was narrowed from '
+ f'{was["max-length"]} to {now["max-length"]}')
+
+
+def _compare_sets(problems, old, new):
+ for name, was in old.items():
+ now = new.get(name)
+ if now is None:
+ problems.append(f'attr set {name} was removed')
+ continue
+ for key in ('set-id', 'stub'):
+ was_value = was.get(key, False)
+ now_value = now.get(key, False)
+ if was_value != now_value:
+ problems.append(f'attr set {name}: {key} was {was_value}, '
+ f'now {now_value}')
+
+
+def _compare_fills(problems, old, new):
+ for name, was in old.items():
+ now = new.get(name)
+ if now is None:
+ problems.append(f'stub fill {name} was removed')
+ continue
+ _compare_nodes(problems, f'stub fill {name}', was, now)
+
+
+def _compare_ops(problems, old, new):
+ for name, was in old.items():
+ now = new.get(name)
+ if now is None:
+ problems.append(f'op {name} was removed')
+ continue
+ where = f'op {name}'
+ # Whether an op may be disabled or withheld is what userspace compiles
+ # against; whether it is either right now is per device.
+ for key in ('op-id', 'type', 'fd-access',
+ 'availability', 'optional', 'pre-post'):
+ if was.get(key) != now.get(key):
+ problems.append(f'{where}: {key} was {was.get(key)}, now '
+ f'{now.get(key)}')
+ for what in ('request', 'reply'):
+ if (what == 'reply' and was['type'] == 'action' and
+ was[what] is None and now[what] is not None):
+ problems.append(f'{where}: an ACK-only action gained a '
+ 'reply schema')
+ elif was[what] is not None and now[what] is None:
+ problems.append(f'{where}: the {what} schema was removed')
+ elif was[what] is not None:
+ _compare_nodes(problems, f'{where} {what}', was[what],
+ now[what])
+
+
+def _compare_nodes(problems, where, old, new):
+ by_name = {_node_key(node): node for node in new}
+ for was in old:
+ key = _node_key(was)
+ name = _node_name(was)
+ now = by_name.pop(key, None)
+ if now is None:
+ problems.append(f'{where}: {name} was removed')
+ continue
+ if now['required'] and not was['required']:
+ problems.append(f'{where}: {name} became required')
+ if was.get('reenters') != now.get('reenters'):
+ problems.append(f'{where}: {name} re-entered '
+ f'{was.get("reenters")}, now '
+ f'{now.get("reenters")}')
+ _compare_nodes(problems, f'{where} {name}',
+ was.get('children', []), now.get('children', []))
+ for node in by_name.values():
+ if node['required']:
+ problems.append(
+ f'{where}: new {_node_name(node)} must be optional')
+
+
+def _node_key(node):
+ if 'attr' in node:
+ return 'attr', node['attr']
+ return 'stub', node['stub']
+
+
+def _node_name(node):
+ if 'attr' in node:
+ return node['attr']
+ return f'stub {node["stub"]}'
+
+
+def _occurrences(max_occurrences):
+ if max_occurrences == spec.UNLIMITED:
+ return 'unlimited'
+ return str(max_occurrences)
diff --git a/tools/ctlv/ctlvgen/lib/errors.py b/tools/ctlv/ctlvgen/lib/errors.py
new file mode 100644
index 000000000000..a165218a381e
--- /dev/null
+++ b/tools/ctlv/ctlvgen/lib/errors.py
@@ -0,0 +1,14 @@
+# SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+"""The single error type reported by the CTLV schema tool."""
+
+
+class CtlvError(Exception):
+ """A specification, registry or ABI snapshot is not acceptable.
+
+ The test suite matches diagnostics, so a message's wording is contract.
+ """
+
+ def __init__(self, where, what):
+ self.where = where
+ self.what = what
+ super().__init__(f'{where}: {what}' if where else what)
diff --git a/tools/ctlv/ctlvgen/lib/gen_family.py b/tools/ctlv/ctlvgen/lib/gen_family.py
new file mode 100644
index 000000000000..92309334f979
--- /dev/null
+++ b/tools/ctlv/ctlvgen/lib/gen_family.py
@@ -0,0 +1,347 @@
+# SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+"""The descriptor of one family: who answers the wire.
+
+One struct ctlv_family and one struct ctlv_op per action or query
+this family declares, plus the helpers a device uses to decide them. A family
+answers what it declared: an inherited op is answered through the descriptor
+of the family that declared it, by that family implementation.
+"""
+
+from __future__ import annotations
+
+import dataclasses
+
+from . import cgen
+from . import spec
+
+# The family's priv, the request/reply, and a context for core helpers.
+CALLBACK_PARAMS = ('void *priv', 'struct ctlv_op_ctx *ctx',
+ 'const struct ctlv_cursor *request',
+ 'struct ctlv_cursor *reply')
+
+# What a hook takes, and what each half returns: a pre may refuse the op.
+HOOK_PARAMS = ('void *priv', 'struct ctlv_op_ctx *ctx')
+HOOK_RETURNS = (('pre', 'int'), ('post', 'void'))
+
+# How every family prepares, initializes and finalizes its priv.
+LIFECYCLE_HOOKS = (
+ ('priv_size', 'size_t', ('void',)),
+ ('pre_init', 'const void', ('const void *init_info',)),
+ ('init', 'int', ('struct ctlv_device *ctlvdev',
+ 'const void *init_info', 'void *priv')),
+ ('post_init', 'int', ('struct ctlv_device *ctlvdev', 'void *priv')),
+ ('pre_fini', 'void', ('struct ctlv_device *ctlvdev', 'void *priv')),
+ ('fini', 'void', ('struct ctlv_device *ctlvdev', 'void *priv')),
+)
+OPTIONAL_LIFECYCLE_HOOKS = ('pre_init', 'post_init', 'pre_fini')
+
+# The reply-side callback for an extension point.
+STUB_HOOKS = (
+ ('fill', 'int', ('void *priv', 'struct ctlv_op_ctx *ctx',
+ 'const struct ctlv_cursor *request',
+ 'struct ctlv_cursor *nest')),
+)
+
+# The state helpers of a dynamically available op.
+STATES = ('enable', 'disable')
+
+
+@dataclasses.dataclass(frozen=True)
+class ReaderScope:
+ name: tuple
+ node: object
+ tree: str
+ node_index: int
+
+
+INPUT_MACROS = {
+ 'uint': 'U64',
+ 'sint': 'S64',
+ 'bool': 'BOOL',
+ 'string': 'STRING',
+ 'binary': 'BINARY',
+ 'enum': 'U64',
+ 'flags': 'U64',
+ 'nested': 'NEST',
+ 'blob': 'BLOB',
+}
+
+PUT_MACROS = {
+ 'uint': 'U64',
+ 'sint': 'S64',
+ 'bool': 'BOOL',
+ 'string': 'STRING',
+ 'binary': 'BINARY',
+ 'enum': 'U64',
+ 'flags': 'U64',
+}
+
+
+def bindings(family):
+ """The ops family binds, in the order it binds them.
+
+ Its own actions and queries, in declaration order. An op it inherited is
+ answered by the family that declared it, and an event by nobody.
+ """
+ return tuple(op for op in family.local_ops if op.type != 'event')
+
+
+def _implementer(family):
+ """Who writes the callbacks of a family."""
+ if family.name == spec.CORE_FAMILY_NAME:
+ return 'the CTLV core'
+ return 'this family'
+
+
+def _macro(writer, name, args):
+ writer.parenthesized(f'{name}(', args, ')')
+
+
+def emit_header(writer, names, family, reader_scopes):
+ """Append the family API to a generated kernel header."""
+ bound = bindings(family)
+ if bound:
+ writer.comment(f'Implemented by {_implementer(family)}, one per '
+ 'action and query it declares.')
+ for op in bound:
+ writer.signature('int', names.callback(op), CALLBACK_PARAMS)
+ if bound:
+ writer.line()
+ _hooks(writer, names, family)
+ _lifecycle_hooks(writer, names, family)
+ _stub_hooks(writer, names, family)
+ writer.line()
+ writer.declaration('extern const struct ctlv_family',
+ f'{names.descriptor()};')
+ writer.line()
+ writer.line('#ifndef _CTLV_GENERATED_METADATA_ONLY')
+ _helpers(writer, names, family)
+ _readers(writer, names, reader_scopes)
+ _writers(writer, names, family)
+ _creators(writer, names, family)
+ writer.line('#endif /* _CTLV_GENERATED_METADATA_ONLY */')
+ writer.line()
+
+
+def _hooks(writer, names, family):
+ """The pair around every op, and the pair around one op."""
+ if family.name == spec.CORE_FAMILY_NAME:
+ writer.comment('Root-family hooks around every core action or query.')
+ else:
+ writer.comment('Family-wide hooks around every effective non-core op.')
+ for which, returns in HOOK_RETURNS:
+ writer.signature(returns, names.family_hook(which), HOOK_PARAMS)
+
+ marked = [op for op in bindings(family) if op.pre_post]
+ if not marked:
+ return
+ writer.line()
+ writer.comment('Per-op hooks nested inside the family-wide pair.')
+ for first, op in enumerate(marked):
+ if first:
+ writer.line()
+ for which, returns in HOOK_RETURNS:
+ writer.signature(returns, names.op_hook(op, which), HOOK_PARAMS)
+
+
+def _declared_lifecycle_hooks(family):
+ return tuple(hook for hook in LIFECYCLE_HOOKS
+ if hook[0] not in OPTIONAL_LIFECYCLE_HOOKS or
+ getattr(family, hook[0]))
+
+
+def _lifecycle_hooks(writer, names, family):
+ """The generated lifecycle of this family's priv."""
+ writer.line()
+ writer.comment('Core-allocated family priv lifecycle.')
+ for which, returns, params in _declared_lifecycle_hooks(family):
+ name = names.lifecycle_hook(which)
+
+ writer.signature(returns, f'*{name}' if which == 'pre_init' else name,
+ params)
+
+
+def _stub_hooks(writer, names, family):
+ """What this family puts into each stub it fills."""
+ if not family.fills:
+ return
+ for fill in family.fills:
+ owner = family.owners[fill.family_id]
+ writer.line()
+ writer.comment(f'{fill.name} of {owner}: fill callback.')
+ for which, returns, params in STUB_HOOKS:
+ writer.signature(returns, names.stub_callback(fill, which),
+ params)
+
+
+def _helpers(writer, names, family):
+ """What one device may decide about each op this family declares."""
+ ops = [op for op in family.local_ops if op.type != 'event' and
+ (op.optional or op.availability == 'dynamic')]
+ if not ops:
+ return
+ writer.line()
+ writer.comment('Optional publication and dynamic availability helpers.')
+ for op in ops:
+ if op.optional:
+ _macro(writer, 'CTLV_OP_PROVIDE',
+ (names.op_ident(op), names.op_id(op)))
+ if op.availability == 'dynamic':
+ for state in STATES:
+ _macro(writer, f'CTLV_OP_{state.upper()}',
+ (names.op_ident(op), names.op_id(op)))
+
+
+def _readers(writer, names, scopes):
+ """Placement-specific indexed access to callback requests."""
+ if not any(scope.node.children() for scope in scopes):
+ return
+ writer.line()
+ writer.comment('Reading a validated message. A blob has a resolver '
+ 'instead of a getter.')
+ for scope in scopes:
+ for slot, child in enumerate(scope.node.children()):
+ attr = child.attr
+ repeated = attr.decl.max_occurrences != 1
+ name = _input_name(names, scope, attr)
+ _macro(writer, 'CTLV_IN_REF',
+ _input_args(names, scope, slot, attr))
+ if repeated:
+ _iterator(writer, names, scope, attr)
+ else:
+ _macro(writer, 'CTLV_IN_HAS', (name,))
+ if attr.decl.kind == 'blob':
+ macro = 'CTLV_IN_' + ('EACH_' if repeated else '') + 'BLOB'
+ _macro(writer, macro, (name,))
+ elif cgen.attr_getter_value(attr):
+ macro = 'CTLV_IN_' + ('EACH_' if repeated else '') + \
+ INPUT_MACROS[attr.decl.kind]
+ _macro(writer, macro, (name,))
+
+
+def _writers(writer, names, family):
+ """What a callback appends to the reply it was handed.
+
+ One helper per attr, returning whether the attr made it into the message.
+ """
+ attrs = [attr for group in family.local_groups for attr in group.attrs
+ if cgen.attr_writers(names, attr)]
+ if not attrs:
+ return
+ writer.line()
+ writer.comment('Writing a reply. An append is atomic; a nest ends with '
+ 'ctlv_nest_end().')
+ for attr in attrs:
+ if attr.decl.kind == 'nested':
+ macro = 'CTLV_NEST_START'
+ else:
+ macro = f'CTLV_PUT_{PUT_MACROS[attr.decl.kind]}'
+ what = 'start' if attr.decl.kind == 'nested' else 'put'
+ name = names.attr_helper(attr, what).removesuffix(f'_{what}')
+ _macro(writer, macro,
+ (name,
+ names.attr(attr), cgen.KINDS[attr.decl.kind]))
+
+
+def _creators(writer, names, family):
+ """What begins one event of each kind this family declares.
+
+ A creator hands back a context and the cursor of its root.
+ """
+ events = [op for op in family.local_ops if op.type == 'event']
+ if not events:
+ return
+ writer.line()
+ writer.comment('Building one event, passed to ctlv_event_emit() or '
+ 'ctlv_event_abort() once.')
+ for op in events:
+ _macro(writer, 'CTLV_EVENT_CREATE',
+ (names.op_ident(op), names.op_id(op)))
+
+
+def _input_name(names, scope, attr):
+ return names.input_ref(scope, attr).removesuffix('_ref')
+
+
+def _input_args(names, scope, slot, attr):
+ return (_input_name(names, scope, attr), scope.tree, names.attr(attr),
+ str(scope.node_index), str(slot), cgen.KINDS[attr.decl.kind])
+
+
+def _iterator(writer, names, scope, attr):
+ """Every occurrence of a repeated attr, in wire order."""
+ writer.define_call(
+ f'{names.input_iterator(scope, attr)}(_attr, _nest)',
+ 'CTLV_IN_FOR_EACH', names.input_name(scope, attr), '_attr', '_nest')
+
+
+def emit_source(writer, names, family):
+ """Append the family bindings to a generated kernel source."""
+ _bindings(writer, names, family)
+ _stub_ops(writer, names, family)
+ _descriptor(writer, names, family)
+
+
+def _bindings(writer, names, family):
+ bound = bindings(family)
+ if not bound:
+ return
+ writer.comment('Local action and query bindings in declaration order.')
+ writer.declaration('static const struct ctlv_op',
+ f'{names.ops()}[] = {{')
+ for op in bound:
+ suffix = '_PRE_POST' if op.pre_post else ''
+ writer.line(f'\tCTLV_OP_{op.type.upper()}{suffix}('
+ f'{names.op_ident(op)}),')
+ writer.line('};')
+ writer.line()
+
+
+def _stub_ops(writer, names, family):
+ """One entry per extension point this family fills, in the same order."""
+ if not family.fills:
+ return
+ writer.comment('Extension-point callbacks.')
+ writer.declaration('static const struct ctlv_stub_ops',
+ f'{names.stub_ops()}[] = {{')
+ for fill in family.fills:
+ attr_set = family.sets[fill.name]
+ args = (cgen.family_const(family.owners[fill.family_id]),
+ names.attr_set(attr_set), names.stub_ident(fill))
+ writer.parenthesized('\tCTLV_STUB_OPS(', args, '),')
+ writer.line('};')
+ writer.line()
+
+
+def _descriptor(writer, names, family):
+ if family.name == spec.CORE_FAMILY_NAME:
+ writer.comment('Root-family descriptor used by the CTLV core.')
+ else:
+ writer.comment('Family descriptor used by ctlv_create().')
+ writer.declaration('const struct ctlv_family',
+ f'{names.descriptor()} = {{')
+ writer.line(f'\t.schema = &{names.schema()},')
+ parent = _parent_descriptor(family)
+ if parent:
+ writer.line(f'\t.parent = &{parent},')
+ if bindings(family):
+ writer.line(f'\t.ops = {names.ops()},')
+ writer.line(f'\t.ops_count = ARRAY_SIZE({names.ops()}),')
+ for which, _ in HOOK_RETURNS:
+ writer.line(f'\t.{which} = {names.family_hook(which)},')
+ for which, _, _ in _declared_lifecycle_hooks(family):
+ writer.line(f'\t.{which} = {names.lifecycle_hook(which)},')
+ if family.fills:
+ stub_ops = names.stub_ops()
+ writer.line(f'\t.stub_ops = {stub_ops},')
+ writer.line(f'\t.stub_ops_count = ARRAY_SIZE({stub_ops}),')
+ writer.line('};')
+ # The one symbol an inheriting family's module needs of this one.
+ writer.line(f'EXPORT_SYMBOL_NS_GPL({names.descriptor()}, "CTLV");')
+
+
+def _parent_descriptor(family):
+ """The direct parent descriptor, none only for the root family."""
+ if family.parent is None:
+ return None
+ return cgen.Names(family.chain[-2]).descriptor()
diff --git a/tools/ctlv/ctlvgen/lib/gen_kernel.py b/tools/ctlv/ctlvgen/lib/gen_kernel.py
new file mode 100644
index 000000000000..1854bd004f36
--- /dev/null
+++ b/tools/ctlv/ctlvgen/lib/gen_kernel.py
@@ -0,0 +1,517 @@
+# SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+"""The kernel metadata of one family.
+
+Each family describes only what it declares. Each op request and reply becomes
+one flat, breadth-first array of struct ctlv_node_schema the core walks by
+index.
+"""
+
+from __future__ import annotations
+
+import dataclasses
+
+from . import cgen
+from . import gen_family
+from . import semantics
+from . import spec
+
+
+def root_macro(*names):
+ """Return a constant of the root family's generated UAPI header."""
+ return cgen.macro(spec.CORE_FAMILY_NAME, *names)
+
+
+FD_ACCESSES = {access: root_macro('op-fd-access', access)
+ for access in spec.FD_ACCESSES}
+
+AVAILABILITIES = {availability: root_macro('op-availability', availability)
+ for availability in spec.AVAILABILITIES}
+
+# Lengths that have to fit the inline message limit. An event record is checked
+# against the event queue instead.
+INLINE_FIELDS = ('request_max_len', 'action_reply_max_len',
+ 'query_reply_min_len', 'query_reply_suggested_len')
+
+# A bound written as its symbolic extreme stays readable.
+U64_MAX = (1 << 64) - 1
+S64_MIN = -(1 << 63)
+S64_MAX = (1 << 63) - 1
+EXTREMES = {
+ ('uint', spec.U32_MAX): 'U32_MAX',
+ ('uint', U64_MAX): 'U64_MAX',
+ ('sint', S64_MIN): 'S64_MIN',
+ ('sint', S64_MAX): 'S64_MAX',
+}
+
+
+@dataclasses.dataclass(frozen=True)
+class FlatNode:
+ """One node of a flattened tree, with its children as an index range."""
+
+ node: object
+ first_child: int
+ children_count: int
+ required_children_count: int
+
+
+def flatten(root):
+ """Return the nodes of one tree breadth first, the root first.
+
+ A re-entering node points back at nodes already laid out, which is what
+ keeps the array finite.
+ """
+ flat = []
+ queue = [(root, {})]
+ next_index = 1
+ while queue:
+ node, levels = queue.pop(0)
+ if node.reenters is not None:
+ first_child, children_count, required_count = levels[node.reenters]
+ flat.append(FlatNode(node=node, first_child=first_child,
+ children_count=children_count,
+ required_children_count=required_count))
+ continue
+ children = node.children()
+ level = (next_index if children else 0, len(children),
+ len(node.required_children))
+ flat.append(FlatNode(node=node, first_child=level[0],
+ children_count=level[1],
+ required_children_count=level[2]))
+ next_index += len(children)
+ if node.expands:
+ levels = levels | {node.expands: level}
+ queue += [(child, levels) for child in children]
+ return tuple(flat)
+
+
+def stub_schemas(root):
+ """Return each stub schema with the node index it joins."""
+ schemas = []
+ for index, flat in enumerate(flatten(root)):
+ for stub in flat.node.stubs:
+ schemas.append((index, stub))
+ return tuple(schemas)
+
+
+def render_header(family, *, family_api):
+ """Return one family's generated kernel header."""
+ names = cgen.Names(family)
+ cgen.symbols(family)
+ guard = names.kernel_guard()
+ local = family.local_ops
+
+ writer = cgen.preamble(family, 'kernel', 'header')
+ what = 'Kernel metadata and family API' if family_api else \
+ 'Schema metadata'
+ writer.comment(f'{what} of the {family.name} CTLV family.')
+ writer.line()
+ writer.open_guard(guard)
+ writer.includes(*_header_includes(family, names, family_api))
+
+ if local:
+ writer.comment('Ops this family declares, in declaration order.')
+ writer.declaration('extern const struct ctlv_op_schema',
+ f'{names.op_schemas()}[];')
+ writer.line()
+ writer.declaration('extern const struct ctlv_family_schema',
+ f'{names.schema()};')
+ writer.line()
+ _message_decls(writer, names, family)
+ if family_api:
+ gen_family.emit_header(writer, names, family,
+ _reader_scopes(family, names))
+ writer.close_guard(guard)
+ return writer.text()
+
+
+def _reader_scopes(family, names):
+ scopes = []
+ indexes = {}
+ for scope in semantics.input_scopes(family):
+ if scope.kind == 'op':
+ root = family.local_ops[scope.index].request
+ tree = f'&{names.op_schemas()}[{scope.index}].request'
+ else:
+ root = family.fills[scope.index].root
+ tree = f'&{names.schema()}.stub_fill_schemas[' \
+ f'{scope.index}].members'
+ key = (scope.kind, scope.index)
+ if key not in indexes:
+ indexes[key] = {id(flat.node): index
+ for index, flat in enumerate(flatten(root))}
+ scopes.append(gen_family.ReaderScope(
+ name=scope.name, node=scope.node, tree=tree,
+ node_index=indexes[key][id(scope.node)]))
+ return tuple(scopes)
+
+
+def _message_decls(writer, names, family):
+ """The messages the core builds itself, and the room each one needs."""
+ for message in family.messages:
+ writer.comment(f'The {message.name} message, built by the core.')
+ writer.define(names.message_max_len(message), str(message.max_len))
+ writer.line()
+ writer.declaration('extern const struct ctlv_schema_tree',
+ f'{names.message_schema(message)};')
+ writer.line()
+
+
+def _header_includes(family, names, family_api):
+ """The generated types, constants and direct parent API."""
+ headers = ['<linux/ctlv_family.h>']
+ if not family_api:
+ return tuple(headers)
+ headers += ['<linux/err.h>', '<linux/errno.h>', '',
+ names.uapi_header(in_kernel=True)]
+ if family.parent not in (None, spec.CORE_FAMILY_NAME):
+ headers += ['', cgen.Names(family.chain[-2]).kernel_header()]
+ return tuple(headers)
+
+
+def render_source(family, *, family_api):
+ """Return one family's generated kernel source."""
+ names = cgen.Names(family)
+ cgen.symbols(family)
+ trees = _trees(family)
+
+ writer = cgen.preamble(family, 'kernel', 'source')
+ if family_api:
+ writer.line('#define _CTLV_GENERATED_METADATA_ONLY')
+ writer.includes(*_includes(names, family, trees, family_api))
+ for op, which, root in trees:
+ _node_schemas(writer, family, names,
+ names.node_schemas(op, which), root)
+ _stub_schemas(writer, family, names, op, which, root)
+ _stub_fill_schemas(writer, family, names)
+ _messages(writer, family, names)
+ _table(writer, names, family)
+ _descriptor(writer, names, family)
+ _uevent(writer, family)
+ _limits(writer, family, trees)
+ if family_api:
+ if family.local_ops:
+ writer.line()
+ gen_family.emit_source(writer, names, family)
+ return writer.text()
+
+
+def _trees(family):
+ """Every tree of every local op, in the order they are emitted."""
+ trees = []
+ for op in family.local_ops:
+ if op.request is not None:
+ trees.append((op, 'request', op.request))
+ if op.reply is not None:
+ trees.append((op, 'reply', op.reply))
+ return trees
+
+
+def _roots(family, trees):
+ """Every emitted tree root: an op's, a fill's and a core message's."""
+ return [root for _, _, root in trees] + \
+ [fill.root for fill in family.fills] + \
+ [message.root for message in family.messages]
+
+
+def _kinds(roots):
+ return {flat.node.attr.decl.kind
+ for root in roots for flat in flatten(root)
+ if not flat.node.is_root}
+
+
+def _includes(names, family, trees, family_api):
+ headers = ['<linux/array_size.h>', '<linux/build_bug.h>',
+ '<linux/ctlv_family.h>']
+ if family_api:
+ headers.append('<linux/export.h>')
+ # The root family's enums name op types, availabilities and blob backings.
+ uapi = [cgen.uapi_header(spec.CORE_FAMILY_NAME, in_kernel=True)]
+ if family.name != spec.CORE_FAMILY_NAME:
+ uapi.append(names.uapi_header(in_kernel=family_api))
+ kinds = _kinds(_roots(family, trees))
+ if 'blob' in kinds:
+ # BIT(), for the mask of permitted blob backings.
+ headers.append('<linux/bits.h>')
+ if kinds & {'uint', 'sint'}:
+ # The symbolic extremes of an unbounded integer bound.
+ headers.append('<linux/limits.h>')
+ groups = [sorted(headers), uapi, [names.kernel_header()]]
+ lines = []
+ for group in groups:
+ if not group:
+ continue
+ if lines:
+ lines.append('')
+ lines += group
+ return lines
+
+
+def _has_event(family):
+ return any(op.type == 'event' for op in family.local_ops)
+
+
+def _node_schemas(writer, family, names, symbol, root):
+ writer.declaration('static const struct ctlv_node_schema',
+ f'{symbol}[] = {{')
+ for flat in flatten(root):
+ _node(writer, family, names, flat)
+ writer.line('};')
+ writer.line()
+
+
+def _node(writer, family, names, flat):
+ macro, args = _node_initializer(family, names, flat)
+ if macro == 'CTLV_NODE_SCHEMA_BLOB':
+ _blob_node(writer, macro, args)
+ else:
+ writer.parenthesized(f'\t{macro}(', args, '),')
+
+
+def _blob_node(writer, macro, args):
+ opening = f'\t{macro}('
+
+ for line in cgen.wrapped(opening, args[:4], ','):
+ writer.line(line)
+ column = cgen.width(opening)
+ indent = '\t' * (column // cgen.TAB_WIDTH) + \
+ ' ' * (column % cgen.TAB_WIDTH)
+ backings = args[4].split(' | ')
+ for backing in backings[:-1]:
+ writer.line(f'{indent}{backing} |')
+ writer.line(f'{indent}{backings[-1]},')
+ writer.line(f'{indent}{args[5]}),')
+
+
+def _node_initializer(family, names, flat):
+ node = flat.node
+
+ if node.is_root:
+ return 'CTLV_NODE_SCHEMA_ROOT', (
+ str(flat.first_child),
+ str(flat.children_count),
+ str(flat.required_children_count),
+ )
+
+ decl = node.attr.decl
+ kind = cgen.KINDS[decl.kind].removeprefix('CTLV_SCHEMA_')
+ macro = f'CTLV_NODE_SCHEMA_{kind}'
+ occurrences = str(node.max_occurrences) if node.max_occurrences else \
+ 'CTLV_NODE_SCHEMA_UNLIMITED'
+ args = [names.attr(node.attr), occurrences]
+ if decl.kind == 'nested':
+ if node.reenters is not None:
+ macro += '_REENTERS'
+ args += [str(flat.first_child), str(flat.children_count),
+ str(flat.required_children_count)]
+ else:
+ if decl.kind in ('uint', 'sint') and decl.unit is not None:
+ macro += '_UNIT'
+ args += _constraint_args(family, names, decl)
+ return macro, tuple(args)
+
+
+def _stub_schemas(writer, family, names, op, which, root):
+ """The extension points of one tree, by the node they widen."""
+ schemas = stub_schemas(root)
+ if not schemas:
+ return
+ writer.comment('Extension points selected by this tree.')
+ writer.declaration('static const struct ctlv_stub_schema',
+ f'{names.stub_schemas(op, which)}[] = {{')
+ for index, stub in schemas:
+ macro = 'CTLV_STUB_SCHEMA_OPTIONAL' if stub.optional else \
+ 'CTLV_STUB_SCHEMA'
+ attr_set = family.sets[stub.name]
+ args = (cgen.family_const(family.owners[stub.family_id]),
+ names.attr_set(attr_set), str(index))
+ writer.parenthesized(f'\t{macro}(', args, '),')
+ writer.line('};')
+ writer.line()
+
+
+def _stub_fill_schemas(writer, family, names):
+ """The members this family puts into the stubs it inherited."""
+ if not family.fills:
+ return
+ for fill in family.fills:
+ _node_schemas(writer, family, names,
+ names.fill_node_schemas(fill), fill.root)
+ writer.comment('The extension points this family fills.')
+ writer.declaration('static const struct ctlv_stub_fill_schema',
+ f'{names.stub_fill_schemas()}[] = {{')
+ for fill in family.fills:
+ attr_set = family.sets[fill.name]
+ args = (cgen.family_const(family.owners[fill.family_id]),
+ names.attr_set(attr_set), cgen.ident(fill.name))
+ writer.parenthesized('\tCTLV_STUB_FILL_SCHEMA(', args, '),')
+ writer.line('};')
+ writer.line()
+
+
+def _messages(writer, family, names):
+ """The messages the core builds where no op declared one."""
+ for message in family.messages:
+ _node_schemas(writer, family, names,
+ names.message_node_schemas(message), message.root)
+ writer.declaration('const struct ctlv_schema_tree',
+ f'{names.message_schema(message)} = {{')
+ node_schemas = names.message_node_schemas(message)
+ writer.line(f'\t.node_schemas = {node_schemas},')
+ writer.line('\t.node_schemas_count =')
+ writer.line(f'\t\tARRAY_SIZE({node_schemas}),')
+ writer.line('};')
+ writer.line()
+ writer.assertion(names.message_max_len(message), '<=', message.limit)
+ writer.line()
+
+
+def _constraint_args(family, names, decl):
+ """The kind-specific arguments of one node-schema macro."""
+ if decl.kind in ('uint', 'sint'):
+ low = _bound(decl.kind, decl.min)
+ high = _bound(decl.kind, decl.max)
+ args = [low, high]
+ if decl.unit is not None:
+ args += [f'CTLV_UNIT_{cgen.macro(decl.unit)}', str(decl.scale)]
+ return args
+ if decl.kind == 'enum':
+ highest = max(family.definitions[decl.ref].numbers())
+ return [str(highest)]
+ if decl.kind == 'flags':
+ return [names.value_mask(family.definitions[decl.ref])]
+ if decl.kind in ('string', 'binary'):
+ return [str(decl.min_length), str(decl.max_length)]
+ if decl.kind == 'blob':
+ return _blob_args(decl.blob)
+ return []
+
+
+def _bound(kind, value):
+ extreme = EXTREMES.get((kind, value))
+
+ if extreme:
+ return extreme
+ if kind == 'uint' and value > spec.S64_MAX:
+ return f'{value}ULL'
+ return str(value)
+
+
+def _blob_args(blob):
+ direction = root_macro('blob-direction', blob.direction)
+ return [_bound('uint', blob.max_length), str(blob.alignment),
+ _backings(blob.backings), direction]
+
+
+def _backings(backings):
+ terms = [f'BIT({root_macro("blob-type", backing)})'
+ for backing in backings]
+ return ' | '.join(terms)
+
+
+def _table(writer, names, family):
+ local = family.local_ops
+ if not local:
+ return
+ writer.comment('The ops this family declares, in declaration order.')
+ writer.declaration('const struct ctlv_op_schema',
+ f'{names.op_schemas()}[] = {{')
+ for op in local:
+ macro, args = _op_schema_initializer(names, op)
+ writer.parenthesized(f'\t{macro}(', args, '),')
+ writer.line('};')
+ writer.line()
+
+
+def _op_schema_initializer(names, op):
+ sizes = op.sizes
+ if op.type == 'action':
+ macro = 'CTLV_OP_SCHEMA_ACTION'
+ args = [names.op_id(op), FD_ACCESSES[op.fd_access],
+ AVAILABILITIES[op.availability],
+ str(sizes.request_max_len),
+ str(sizes.action_reply_max_len),
+ _op_tree(op, 'request'), _op_tree(op, 'reply')]
+ elif op.type == 'query':
+ macro = 'CTLV_OP_SCHEMA_QUERY'
+ args = [names.op_id(op), FD_ACCESSES[op.fd_access],
+ AVAILABILITIES[op.availability],
+ str(sizes.request_max_len),
+ str(sizes.query_reply_min_len),
+ str(sizes.query_reply_suggested_len),
+ _op_tree(op, 'request'), _op_tree(op, 'reply')]
+ else:
+ macro = 'CTLV_OP_SCHEMA_EVENT'
+ args = [names.op_id(op), str(sizes.event_record_max_len),
+ _op_tree(op, 'reply')]
+ if op.optional:
+ args.append('.optional = true')
+ if op.pre_post:
+ args.append('.pre_post = true')
+ return macro, tuple(args)
+
+
+def _op_tree(op, which):
+ root = getattr(op, which)
+ if root is None:
+ return 'CTLV_SCHEMA_TREE_NONE()'
+ macro = 'CTLV_SCHEMA_TREE_STUBS' if stub_schemas(root) else \
+ 'CTLV_SCHEMA_TREE'
+ return f'{macro}({cgen.ident(op.name, which)})'
+
+
+def _descriptor(writer, names, family):
+ """This family's node of the schema graph."""
+ writer.comment('The family ID is its position in the central registry.')
+ writer.declaration('const struct ctlv_family_schema',
+ f'{names.schema()} = {{')
+ writer.line(f'\t.name = "{family.name}",')
+ writer.line(f'\t.family_id = {family.family_id},')
+ if family.local_ops:
+ writer.line(f'\t.ops = {names.op_schemas()},')
+ writer.line(f'\t.ops_count = ARRAY_SIZE({names.op_schemas()}),')
+ if family.fills:
+ stub_fill_schemas = names.stub_fill_schemas()
+ writer.line(f'\t.stub_fill_schemas = {stub_fill_schemas},')
+ writer.line(f'\t.stub_fill_schemas_count = '
+ f'ARRAY_SIZE({stub_fill_schemas}),')
+ writer.line('};')
+ writer.line()
+
+
+def _uevent(writer, family):
+ """What this chain's names cost a device's uevent environment."""
+ names = [item.name for item in family.chain]
+ variables = [f'CTLV_FAMILY={names[-1]}']
+ variables += [f'CTLV_COMPATIBLE_{index}={name}'
+ for index, name in enumerate(names)]
+ variables.append(f'CTLV_COMPATIBLE_N={len(names)}')
+ # "KEY=value" plus the NUL, as add_uevent_var() charges it.
+ writer.comment('Maximum uevent cost for this family chain.')
+ writer.line(f'static_assert({sum(len(item) + 1 for item in variables)} <= '
+ 'CTLV_UEVENT_MAX_LEN);')
+ writer.line(f'static_assert({len(variables)} <= CTLV_UEVENT_MAX_VARS);')
+ writer.line()
+
+
+def _limits(writer, family, trees):
+ local = family.local_ops
+ if not local:
+ return
+ writer.comment('Guards for generated maxima.')
+ inline = max(getattr(op.sizes, field)
+ for op in local for field in INLINE_FIELDS)
+ writer.line(f'static_assert({inline} <= CTLV_MAX_INLINE_MESSAGE_LEN);')
+ if _has_event(family):
+ record = max(op.sizes.event_record_max_len for op in local)
+ writer.line(f'static_assert({record} <= CTLV_MAX_EVENT_QUEUE_LEN);')
+ depth = max(_nest_depth(root) for root in _roots(family, trees))
+ if depth:
+ writer.line(f'static_assert({depth} <= '
+ 'CTLV_MAX_DECLARED_NEST_DEPTH);')
+
+
+def _nest_depth(node):
+ """Deepest nest below node, which is itself at depth zero."""
+ depths = [1 + _nest_depth(child) for child in node.children()
+ if child.attr.decl.kind == 'nested']
+ return max(depths, default=0)
diff --git a/tools/ctlv/ctlvgen/lib/gen_uapi.py b/tools/ctlv/ctlvgen/lib/gen_uapi.py
new file mode 100644
index 000000000000..14330248f144
--- /dev/null
+++ b/tools/ctlv/ctlvgen/lib/gen_uapi.py
@@ -0,0 +1,223 @@
+# SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+"""The UAPI header of one family.
+
+Names for numbers: complete attr and op IDs, group and attr-set IDs, enum
+values, flag bits and units. The wire format is the transport
+header's. An inherited declaration is not repeated, already having a name in
+the header of the family that owns it.
+"""
+
+from __future__ import annotations
+
+import textwrap
+
+from . import cgen
+
+# " * " plus the text has to stay inside the line limit.
+COMMENT_WIDTH = cgen.LINE_LIMIT - 3
+
+
+def render(family):
+ """Return the UAPI header of family as text."""
+ names = cgen.Names(family)
+ cgen.symbols(family)
+ guard = names.uapi_guard()
+
+ writer = cgen.preamble(family, 'uapi', 'header')
+ _file_comment(writer, family)
+ writer.line()
+ writer.open_guard(guard)
+ writer.includes(*_includes(family))
+
+ _definitions(writer, family, names)
+ _groups(writer, family, names)
+ _sets(writer, family, names)
+ _ops(writer, family, names)
+
+ writer.close_guard(guard)
+ return writer.text()
+
+
+def render_families(registry):
+ """Return the family-number header of one registry."""
+ guard = '_UAPI_LINUX_CTLV_FAMILIES_GEN_H'
+ values = [(cgen.family_const(entry.name), family_id)
+ for family_id, entry in enumerate(registry.entries)]
+ writer = cgen.preamble(registry, 'uapi', 'header', registry.path)
+
+ writer.comment('CTLV family numbers.')
+ writer.line()
+ writer.open_guard(guard)
+ _sequential_enum(writer, '_ctlv_family', values)
+ writer.line()
+ writer.close_guard(guard)
+ return writer.text()
+
+
+def _includes(family):
+ headers = ['<linux/ctlv.h>', cgen.families_uapi_header()]
+ if family.parent:
+ # Inherited constants live in the parent's header, so one include is
+ # enough for the whole effective schema.
+ headers.append(cgen.uapi_header(family.parent))
+ if any(definition.kind == 'flags'
+ for definition in _own(family, family.definitions.values())):
+ # _BITULL(), which is the form of BIT_ULL() that userspace has.
+ headers.append('<linux/const.h>')
+ return sorted(headers)
+
+
+def _file_comment(writer, family):
+ paragraphs = [
+ f'Constants of the {family.name} CTLV family. Attr and op IDs are '
+ 'complete; the wire format carrying them is in <linux/ctlv.h>.',
+ ]
+ if family.parent:
+ paragraphs.append(
+ f'The inherited {family.parent} header is included here, so one '
+ 'include covers the whole effective schema.')
+ lines = []
+ for paragraph in paragraphs:
+ if lines:
+ lines.append('')
+ lines += textwrap.wrap(paragraph, COMMENT_WIDTH)
+ writer.comment(*lines)
+
+
+def _own(family, declarations):
+ """Only what this family declares itself, in declaration order."""
+ return [item for item in declarations
+ if item.family_id == family.family_id]
+
+
+def _definitions(writer, family, names):
+ for definition in _own(family, family.definitions.values()):
+ if definition.kind == 'enum':
+ _enum(writer, names, definition)
+ else:
+ _flags(writer, names, definition)
+ writer.line()
+
+
+def _enum(writer, names, definition):
+ writer.comment(f'enum {definition.name}')
+ values = [(names.value(definition, value_name), number)
+ for value_name, number in definition.values]
+ _sequential_enum(writer, names.definition(definition), values)
+
+
+def _flags(writer, names, definition):
+ writer.comment(f'flags {definition.name}')
+ bits = [(names.value_bit(definition, value_name), bit)
+ for value_name, bit in definition.values]
+ _sequential_enum(writer, names.bit_enum(definition), bits)
+ writer.line()
+ values = []
+ for value_name, _bit in definition.values:
+ value = names.value(definition, value_name)
+ writer.define(value,
+ f'_BITULL({names.value_bit(definition, value_name)})')
+ values.append(value)
+ _flags_mask(writer, names.value_mask(definition), values)
+
+
+def _groups(writer, family, names):
+ groups = _own(family, family.groups.values())
+ if not groups:
+ return
+ writer.comment('attr groups')
+ _sequential_enum(writer, names.group_enum(groups[0]),
+ [(names.group(group), group.group_id)
+ for group in groups])
+ writer.line()
+ writer.define_call(
+ f'{names.family_attr(groups[0])}(_group, _attr)', 'CTLV_ATTR',
+ names.family_const(), '(_group)', '(_attr)')
+ writer.line()
+ for group in groups:
+ if not group.attrs:
+ continue
+ writer.comment(f'attrs of group {group.name}')
+ writer.define_call(f'{names.group_attr(group)}(_attr)',
+ names.family_attr(group),
+ names.group(group),
+ f'{names.local_attr_prefix(group)}##_attr')
+ writer.line()
+ _sequential_enum(writer, names.group_attr_enum(group),
+ [(names.local_attr(attr), attr.local_id)
+ for attr in group.attrs])
+ writer.line()
+ for attr in group.attrs:
+ _attr(writer, names, group, attr)
+ _attr_metadata(writer, names, group)
+ writer.line()
+
+
+def _attr(writer, names, group, attr):
+ writer.define_call(names.attr(attr), names.group_attr(group),
+ cgen.macro(attr.name))
+
+
+def _attr_metadata(writer, names, group):
+ attrs = [attr for attr in group.attrs if attr.decl.unit is not None]
+ if not attrs:
+ return
+ writer.line()
+ writer.comment(f'metadata of group {group.name}')
+ writer.line(f'enum {names.group_metadata_enum(group)} {{')
+ for attr in attrs:
+ writer.line(f'\t{names.attr_unit(attr)} = '
+ f'CTLV_UNIT_{cgen.macro(attr.decl.unit)},')
+ writer.line(f'\t{names.attr_scale(attr)} = {attr.decl.scale},')
+ writer.line('};')
+
+
+def _sequential_enum(writer, name, values):
+ first = values[0][1]
+
+ assert all(number == first + index
+ for index, (_value, number) in enumerate(values))
+ writer.line(f'enum {name} {{')
+ for index, (value, number) in enumerate(values):
+ assignment = f' = {number}' if not index and number else ''
+ writer.line(f'\t{value}{assignment},')
+ writer.line('};')
+
+
+def _flags_mask(writer, name, values):
+ if len(values) == 1:
+ writer.define(name, values[0])
+ return
+ lines = [f'\t({values[0]} |']
+ lines += [f'\t {value} |' for value in values[1:-1]]
+ lines.append(f'\t {values[-1]})')
+ writer.define_block(name, *lines)
+
+
+def _sets(writer, family, names):
+ own = _own(family, family.sets.values())
+ if not own:
+ return
+ writer.comment('attr sets')
+ _sequential_enum(writer, names.set_enum(own[0]),
+ [(names.attr_set(attr_set), attr_set.set_id)
+ for attr_set in own])
+ writer.line()
+
+
+def _ops(writer, family, names):
+ own = _own(family, family.ops)
+ if not own:
+ return
+ writer.comment('ops')
+ _sequential_enum(writer, names.op_enum(own[0]),
+ [(names.op(op), op.local_id)
+ for op in own])
+ writer.line()
+ writer.define_call(f'{names.operation(own[0])}(_op)',
+ 'CTLV_OP', names.family_const(),
+ f'{names.op_prefix(own[0])}##_op')
+ for op in own:
+ writer.define_call(names.op_id(op), names.operation(op),
+ cgen.macro(op.name))
+ writer.line()
diff --git a/tools/ctlv/ctlvgen/lib/outputs.py b/tools/ctlv/ctlvgen/lib/outputs.py
new file mode 100644
index 000000000000..4df7ae60c1c1
--- /dev/null
+++ b/tools/ctlv/ctlvgen/lib/outputs.py
@@ -0,0 +1,329 @@
+# SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+"""The complete set of generated files and its reconciliation."""
+
+from __future__ import annotations
+
+import dataclasses
+import os
+import pathlib
+import subprocess
+import tempfile
+
+from . import gen_kernel
+from . import gen_uapi
+from . import semantics
+from . import spec
+from . import yamlload
+from .errors import CtlvError
+
+
+FLAVOURS = {
+ 'uapi-header': gen_uapi.render,
+ 'kernel-header': gen_kernel.render_header,
+ 'kernel-source': gen_kernel.render_source,
+}
+
+IDENTITIES = {
+ 'uapi-header': ('uapi', 'header', '.h'),
+ 'kernel-header': ('kernel', 'header', '.h'),
+ 'kernel-source': ('kernel', 'source', '.c'),
+}
+
+ROOT_FLAVOURS = frozenset(FLAVOURS)
+FAMILY_FLAVOURS = frozenset(FLAVOURS)
+FIXTURE_FLAVOURS = frozenset({
+ 'uapi-header', 'kernel-header', 'kernel-source',
+})
+
+
+@dataclasses.dataclass(frozen=True)
+class Output:
+ family: str
+ source: pathlib.PurePosixPath
+ registry: pathlib.PurePosixPath
+ flavour: str
+ path: pathlib.PurePosixPath
+
+
+def _spec_paths(paths, source, registry):
+ return spec.Paths(paths.kernel_dir / source, schema=paths.schema,
+ registry=paths.kernel_dir / registry)
+
+
+def _relative(where, value):
+ path = pathlib.PurePosixPath(value)
+ if path.is_absolute() or '..' in path.parts or \
+ any(part.startswith('.') for part in path.parts) or \
+ str(path) != value:
+ raise CtlvError(where, f'"{value}" is not a normalized relative path')
+ return path
+
+
+def _repository_path(paths, path):
+ return pathlib.PurePosixPath(
+ pathlib.Path(path).relative_to(paths.kernel_dir).as_posix())
+
+
+def _manifest_contexts(paths):
+ fixtures_dir = paths.kernel_dir / 'tools/ctlv/tests/specs'
+ contexts = [
+ (paths.generated, _repository_path(paths, paths.registry),
+ _repository_path(paths, paths.spec_dir), None, False),
+ ]
+ manifest = fixtures_dir.parent / 'generated-files.yaml'
+ if manifest.exists():
+ fixtures = sorted(path.stem for path in fixtures_dir.glob('*.yaml'))
+ contexts.append((
+ manifest,
+ _repository_path(paths, fixtures_dir.parent / 'families.yaml'),
+ _repository_path(paths, fixtures_dir), fixtures, True))
+ return tuple(contexts)
+
+
+def load(paths):
+ """Load and validate every generated-file manifest."""
+ outputs = []
+ families = set()
+ destinations = set()
+ for manifest, expected_registry, source_dir, expected_names, fixture in \
+ _manifest_contexts(paths):
+ data = yamlload.load(manifest)
+ spec._validate_syntax(manifest, data, paths.generated_schema)
+ registry_record = data['registry']
+ where = f'{manifest}: registry'
+ registry_source = _relative(where, registry_record['source'])
+ if registry_source != expected_registry:
+ raise CtlvError(where, f'source is {registry_source}, expected '
+ f'{expected_registry}')
+ registry = spec.load_registry(
+ paths, paths.kernel_dir / registry_source)
+ if expected_names is None:
+ expected_names = [entry.name for entry in registry.entries]
+ records = data['families']
+ if [record['name'] for record in records] != expected_names:
+ raise CtlvError(str(manifest),
+ 'manifest families do not match their sources')
+
+ for flavour, value in registry_record['outputs'].items():
+ destination = _relative(f'{where} {flavour}', value)
+ suffix = IDENTITIES[flavour][2]
+ if destination.suffix != suffix:
+ raise CtlvError(f'{where} {flavour}',
+ f'output must end in {suffix}')
+ if destination in destinations:
+ raise CtlvError(f'{where} {flavour}',
+ f'duplicate output path {destination}')
+ destinations.add(destination)
+ outputs.append(Output(None, registry_source, registry_source,
+ flavour, destination))
+
+ for record in records:
+ name = record['name']
+ where = f'{manifest}: family {name}'
+ if name in families:
+ raise CtlvError(where, 'duplicate family')
+ families.add(name)
+
+ expected_source = source_dir / f'{name}.yaml'
+ source = _relative(where, record['source'])
+ if source != expected_source:
+ raise CtlvError(where, f'source is {source}, expected '
+ f'{expected_source}')
+
+ required = FIXTURE_FLAVOURS if fixture else FAMILY_FLAVOURS
+ if not fixture and name == spec.CORE_FAMILY_NAME:
+ required = ROOT_FLAVOURS
+ flavours = frozenset(record['outputs'])
+ if flavours != required:
+ missing = sorted(required - flavours)
+ extra = sorted(flavours - required)
+ detail = []
+ if missing:
+ detail.append(f'missing {", ".join(missing)}')
+ if extra:
+ detail.append(f'unexpected {", ".join(extra)}')
+ raise CtlvError(where, '; '.join(detail))
+
+ family = semantics.load(
+ _spec_paths(paths, source, registry_source))
+ if family.name != name:
+ raise CtlvError(where, f'source declares family '
+ f'{family.name}')
+ for flavour, value in record['outputs'].items():
+ destination = _relative(f'{where} {flavour}', value)
+ suffix = IDENTITIES[flavour][2]
+ if destination.suffix != suffix:
+ raise CtlvError(f'{where} {flavour}',
+ f'output must end in {suffix}')
+ if destination in destinations:
+ raise CtlvError(f'{where} {flavour}',
+ f'duplicate output path {destination}')
+ destinations.add(destination)
+ outputs.append(Output(name, source, registry_source, flavour,
+ destination))
+ return tuple(outputs)
+
+
+def render(paths, plan):
+ """Render every planned output before any destination is changed."""
+ families = {}
+ rendered = {}
+ registered_registry = _repository_path(paths, paths.registry)
+ for output in plan:
+ source = _output_source(output)
+ if output.family is None:
+ registry = spec.load_registry(
+ paths, paths.kernel_dir / output.registry)
+ text = gen_uapi.render_families(registry)
+ else:
+ key = (output.source, output.registry)
+ family = families.get(key)
+ if family is None:
+ family = semantics.load(
+ _spec_paths(paths, output.source, output.registry))
+ families[key] = family
+ renderer = FLAVOURS[output.flavour]
+ if output.flavour in ('kernel-header', 'kernel-source'):
+ text = renderer(
+ family, family_api=output.registry == registered_registry)
+ else:
+ text = renderer(family)
+ mode, kind, _ = IDENTITIES[output.flavour]
+ if _identity(text) != (source, mode, kind):
+ raise CtlvError(str(output.path),
+ 'generator emitted the wrong output marker')
+ rendered[output.path] = text
+ return rendered
+
+
+def _output_source(output):
+ return output.source.as_posix()
+
+
+def _marked_files(root):
+ if (root / '.git').exists():
+ result = subprocess.run(
+ ['git', 'grep', '--untracked', '--files-with-matches',
+ 'CTLV-GEN', '--', '*.c', '*.h', '*.rst'],
+ cwd=root, capture_output=True, text=True, check=False)
+ if result.returncode not in (0, 1):
+ raise CtlvError(str(root), result.stderr.strip())
+ candidates = (root / line for line in result.stdout.splitlines())
+ else:
+ candidates = (path for suffix in ('*.c', '*.h', '*.rst')
+ for path in root.rglob(suffix))
+
+ marked = set()
+ for path in candidates:
+ if path.is_symlink() or not path.is_file():
+ continue
+ text = path.read_text(encoding='utf-8', errors='replace')
+ if _identity(text) is not None:
+ marked.add(path.relative_to(root).as_posix())
+ return marked
+
+
+def _destination(root, relative):
+ destination = root.joinpath(*relative.parts)
+ if destination.is_symlink():
+ raise CtlvError(str(destination),
+ 'generated output cannot be a symlink')
+ parent = destination.parent
+ while parent != root and not parent.exists():
+ parent = parent.parent
+ if parent.is_symlink() or root.resolve() not in (
+ parent.resolve(), *parent.resolve().parents):
+ raise CtlvError(str(destination),
+ 'generated output escapes its output root')
+ return destination
+
+
+def _replace(destination, text):
+ destination.parent.mkdir(parents=True, exist_ok=True)
+ fd, temporary = tempfile.mkstemp(dir=destination.parent,
+ prefix=f'.{destination.name}.')
+ try:
+ with os.fdopen(fd, 'w', encoding='utf-8') as stream:
+ stream.write(text)
+ os.chmod(temporary, 0o644)
+ os.replace(temporary, destination)
+ finally:
+ if os.path.exists(temporary):
+ os.unlink(temporary)
+
+
+def _identity(text):
+ lines = text.splitlines()
+ if len(lines) < 4:
+ return None
+ c_spdx = (lines[0].startswith('// SPDX-License-Identifier: ') or
+ (lines[0].startswith('/* SPDX-License-Identifier: ') and
+ lines[0].endswith(' */')))
+ if c_spdx and \
+ lines[1] == ('/* Do not edit, regenerate with '
+ 'tools/ctlv/ctlv-regen.sh, from: */') and \
+ lines[2].startswith('/*\t') and lines[2].endswith(' */') and \
+ lines[3].startswith('/* CTLV-GEN ') and lines[3].endswith(' */'):
+ source = lines[2][3:-3]
+ fields = lines[3][3:-3].split()
+ elif lines[0].startswith('.. SPDX-License-Identifier: ') and \
+ lines[1] == ('.. Do not edit, regenerate with '
+ 'tools/ctlv/ctlv-regen.sh, from:') and \
+ lines[2].startswith('..\t') and \
+ lines[3].startswith('.. CTLV-GEN '):
+ source = lines[2][3:]
+ fields = lines[3][3:].split()
+ else:
+ return None
+ if len(fields) == 3 and fields[0] == 'CTLV-GEN':
+ return source, fields[1], fields[2]
+ return None
+
+
+def reconcile(paths, output_root=None, check=False):
+ """Check or atomically update every generated output."""
+ plan = load(paths)
+ rendered = render(paths, plan)
+ root = pathlib.Path(paths.kernel_dir if output_root is None
+ else output_root)
+ if not root.is_dir():
+ raise CtlvError(str(root), 'output root is not a directory')
+ expected = {output.path.as_posix() for output in plan}
+ unexpected = sorted(_marked_files(root) - expected)
+ if unexpected:
+ raise CtlvError('generated outputs',
+ 'unexpected files\n' +
+ '\n'.join(f' {path}' for path in unexpected))
+
+ states = {}
+ problems = []
+ unsafe = []
+ by_path = {output.path: output for output in plan}
+ for relative, text in rendered.items():
+ destination = _destination(root, relative)
+ if not destination.exists():
+ problems.append(f'missing {relative}')
+ states[relative] = (destination, text)
+ continue
+ if not destination.is_file():
+ raise CtlvError(str(destination),
+ 'generated output is not a regular file')
+ current = destination.read_text(encoding='utf-8')
+ if current == text:
+ continue
+ problems.append(f'stale {relative}')
+ output = by_path[relative]
+ mode, kind, _ = IDENTITIES[output.flavour]
+ if not check and _identity(current) != (
+ _output_source(output), mode, kind):
+ unsafe.append(f'{relative} is not the planned generated file')
+ states[relative] = (destination, text)
+ if unsafe:
+ raise CtlvError('generated outputs',
+ 'refusing to overwrite files\n' +
+ '\n'.join(f' {problem}' for problem in unsafe))
+ if check and problems:
+ raise CtlvError('generated outputs', '\n'.join(problems))
+ for destination, text in states.values():
+ _replace(destination, text)
+ return tuple(problems)
diff --git a/tools/ctlv/ctlvgen/lib/semantics.py b/tools/ctlv/ctlvgen/lib/semantics.py
new file mode 100644
index 000000000000..83290bfb2f46
--- /dev/null
+++ b/tools/ctlv/ctlvgen/lib/semantics.py
@@ -0,0 +1,893 @@
+# SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+"""The effective form of a CTLV family.
+
+The specifications of one inheritance chain resolved into one schema: numeric
+IDs from declaration positions, selectors expanded into placements, and one
+tree per op request and reply. A rule enforced here is not re-enforced
+downstream.
+"""
+
+from __future__ import annotations
+
+import dataclasses
+
+from . import sizes
+from . import spec
+from .errors import CtlvError
+
+MAX_FLAG_BITS = 64
+
+# These root enums are stored more narrowly than a generic enum range.
+ROOT_ENUM_VALUES_MAX = {
+ 'op-type': spec.U8_MAX,
+ 'op-availability': spec.U8_MAX,
+ 'op-fd-access': spec.U8_MAX,
+ 'blob-direction': spec.U8_MAX,
+ # BIT(value) is stored in struct ctlv_node_schema.blob.backings.
+ 'blob-type': 15,
+}
+
+
+def _check_count(where, what, count, maximum):
+ if count > maximum:
+ raise CtlvError(where, f'{count} {what} exceed the {maximum} maximum')
+
+
+@dataclasses.dataclass(frozen=True)
+class Definition:
+ """An effective enum or flags definition."""
+
+ name: str
+ kind: str
+ family_id: int
+ values: tuple
+
+ def numbers(self):
+ return tuple(number for _, number in self.values)
+
+
+@dataclasses.dataclass(frozen=True)
+class Attr:
+ """An effective attr, with its complete wire ID."""
+
+ group: str
+ name: str
+ family_id: int
+ group_id: int
+ local_id: int
+ attr_id: int
+ decl: spec.AttrDecl
+
+ @property
+ def full_name(self):
+ return f'{self.group}.{self.name}'
+
+
+@dataclasses.dataclass(frozen=True)
+class Group:
+ name: str
+ family_id: int
+ group_id: int
+ attrs: tuple
+
+
+@dataclasses.dataclass(frozen=True)
+class AttrSet:
+ """An effective attr set. A stub that nobody filled stays a stub."""
+
+ name: str
+ family_id: int
+ set_id: int
+ stub: bool
+ attrs: tuple
+ optional_attrs: tuple
+
+
+@dataclasses.dataclass(frozen=True)
+class Stub:
+ """One place where a descendant's fill of a stub splices in.
+
+ The declaring family's metadata carries this in place of those members.
+ """
+
+ name: str
+ family_id: int
+ set_id: int
+ optional: bool
+
+
+@dataclasses.dataclass(frozen=True)
+class Fill:
+ """The members one family puts into a stub it inherited.
+
+ root stands for the stub itself, so its children are what splices in.
+ """
+
+ name: str
+ owner_family_id: int
+ family_id: int
+ set_id: int
+ root: Node
+
+
+@dataclasses.dataclass(frozen=True)
+class Node:
+ """One node of an expanded op tree. The root has no attr.
+
+ stubs are the extension points selected here and still unfilled. A
+ recursive nest becomes a node naming the set in expands and childless
+ nodes below it naming it in reenters, so walking children terminates.
+ """
+
+ attr: Attr
+ required: bool
+ max_occurrences: int
+ required_children: tuple
+ optional_children: tuple
+ stubs: tuple = ()
+ expands: str = None
+ reenters: str = None
+
+ @property
+ def is_root(self):
+ return self.attr is None
+
+ def children(self):
+ return self.required_children + self.optional_children
+
+
+@dataclasses.dataclass(frozen=True)
+class Message:
+ """One message root that no op declares, built by the core unasked.
+
+ Only the root family declares one.
+ """
+
+ name: str
+ family_id: int
+ set_id: int
+ root: Node
+ max_len: int
+ limit: str
+
+
+@dataclasses.dataclass(frozen=True)
+class Op:
+ """An effective op.
+
+ availability is the permission to enable and disable the op per device,
+ not a state. optional means a device has the op only once its family opts
+ in. pre_post asks for a pair of this op's own, on top of the family-wide
+ one.
+ """
+
+ name: str
+ type: str
+ fd_access: str
+ availability: str
+ optional: bool
+ pre_post: bool
+ family_id: int
+ local_id: int
+ op_id: int
+ request: Node
+ reply: Node
+ sizes: sizes.OpSizes
+
+
+@dataclasses.dataclass(frozen=True)
+class Family:
+ """One effective family: itself plus everything it inherits.
+
+ owners maps a family ID to its name for every family in the chain. fills
+ is what a generator emits, chain_fills what a device answers with.
+ """
+
+ name: str
+ family_id: int
+ path: str
+ chain: tuple
+ owners: dict
+ definitions: dict
+ groups: dict
+ attrs: dict
+ sets: dict
+ ops: tuple
+ fills: tuple
+ chain_fills: tuple
+ messages: tuple
+
+ @property
+ def parent(self):
+ return self.chain[-1].inherits
+
+ @property
+ def license(self):
+ return self.chain[-1].license
+
+ @property
+ def pre_init(self):
+ return self.chain[-1].pre_init
+
+ @property
+ def post_init(self):
+ return self.chain[-1].post_init
+
+ @property
+ def pre_fini(self):
+ return self.chain[-1].pre_fini
+
+ def _local(self, items):
+ return tuple(item for item in items
+ if item.family_id == self.family_id)
+
+ @property
+ def local_definitions(self):
+ return self._local(self.definitions.values())
+
+ @property
+ def local_groups(self):
+ return self._local(self.groups.values())
+
+ @property
+ def local_sets(self):
+ """Sets this family declares, a stub it inherited excluded."""
+ return self._local(self.sets.values())
+
+ @property
+ def local_ops(self):
+ return self._local(self.ops)
+
+
+@dataclasses.dataclass(frozen=True)
+class InputScope:
+ kind: str
+ index: int
+ name: tuple
+ node: Node
+
+
+def input_scopes(family, owner_family_id=None):
+ """Every schema scope family code may read, with a unique name."""
+ def walk(kind, index, name, node):
+ yield InputScope(kind, index, name, node)
+ for child in node.children():
+ if child.attr.decl.kind != 'nested':
+ continue
+ child_name = name + (child.attr.name,)
+ yield from walk(kind, index, child_name, child)
+
+ if owner_family_id is None:
+ ops = family.local_ops
+ fills = family.fills
+ else:
+ ops = tuple(op for op in family.ops
+ if op.family_id == owner_family_id)
+ fills = tuple(fill for fill in family.chain_fills
+ if fill.owner_family_id == owner_family_id)
+
+ for index, op in enumerate(ops):
+ if op.request is not None:
+ yield from walk('op', index, (op.name,), op.request)
+ for index, fill in enumerate(fills):
+ yield from walk('fill', index, (fill.name,), fill.root)
+
+
+def load(paths):
+ """Load the family at paths.spec, resolved against its whole chain."""
+ registry = spec.load_registry(paths)
+ return _Resolver(paths, registry).resolve()
+
+
+class _Resolver:
+ def __init__(self, paths, registry):
+ self.paths = paths
+ self.registry = registry
+ self.definitions = {}
+ self.groups = {}
+ self.attrs = {}
+ self.sets = {}
+ self.ops = []
+ # Which family filled each stub, to name it if a second one tries.
+ self.stub_fillers = {}
+ # Where each set was declared, for diagnostics.
+ self.set_paths = {}
+
+ def resolve(self):
+ chain = self._load_chain()
+ self._check_names(chain)
+ self._check_order(chain)
+ target = chain[-1]
+ for family in chain:
+ self._add_family(family)
+ self._check_sets()
+ for family in chain:
+ self._add_ops(family)
+ fills = self._fills(target)
+ chain_fills = self._fills(None)
+ _check_count(f'{target.path}: family {target.name}', 'stub fills',
+ len(fills), spec.U32_MAX)
+ resolved = Family(
+ name=target.name,
+ family_id=self._family_id(target),
+ path=target.path,
+ chain=chain,
+ owners={self._family_id(item): item.name for item in chain},
+ definitions=self.definitions,
+ groups=self.groups,
+ attrs=self.attrs,
+ sets=self.sets,
+ ops=tuple(self.ops),
+ fills=fills,
+ chain_fills=chain_fills,
+ messages=self._messages(target),
+ )
+ _check_composed(resolved)
+ return resolved
+
+ def _messages(self, family):
+ """The message roots this family declares that no op selects.
+
+ The root family alone has them, and has to have them.
+ """
+ messages = []
+ for name in sizes.CORE_MESSAGES:
+ attr_set = self.sets.get(name)
+ if attr_set is None:
+ if family.name != spec.CORE_FAMILY_NAME:
+ continue
+ raise CtlvError(f'{family.path}: family {family.name}',
+ f'the root family has to declare the "{name}" '
+ 'attr set, which the core builds a message of')
+ if attr_set.family_id != self._family_id(family):
+ continue
+ where = f'{self.set_paths[name]}: attr set {name}'
+ nodes, stubs = self._expand_placements(
+ spec.Placements(attr_set.attrs, attr_set.optional_attrs),
+ where, (), 0, (name,))
+ if stubs:
+ raise CtlvError(where, 'a message the core builds cannot '
+ 'select an extension point')
+ root = _root(nodes)
+ _check_tree_size(where, root)
+ messages.append(Message(
+ name=name,
+ family_id=attr_set.family_id,
+ set_id=attr_set.set_id,
+ root=root,
+ max_len=sizes.message_max_len(where, name, root),
+ limit=sizes.CORE_MESSAGES[name][0],
+ ))
+ return tuple(messages)
+
+ def _fills(self, family):
+ """The stubs one family fills, with the members it puts in each.
+
+ Passing None asks for the fills of the whole chain.
+ """
+ fills = []
+ for name, filler in self.stub_fillers.items():
+ if family is not None and filler != family.name:
+ continue
+ attr_set = self.sets[name]
+ nodes, stubs = self._expand_placements(
+ spec.Placements(attr_set.attrs, attr_set.optional_attrs),
+ f'{self.set_paths[name]}: attr set {name}', (), 0, (name,))
+ root = _root(nodes, stubs)
+ where = f'{self.set_paths[name]}: attr set {name}'
+ _check_tree_size(where, root)
+ if _reenters_anywhere(root):
+ raise CtlvError(
+ where, 'a stub fill cannot contain a recursive attr')
+ fills.append(Fill(
+ name=name,
+ owner_family_id=self.registry.family_id(filler),
+ family_id=attr_set.family_id,
+ set_id=attr_set.set_id,
+ root=root,
+ ))
+ return tuple(fills)
+
+ # Loading and identity
+
+ def _load_chain(self):
+ family = spec.load(self.paths.spec, self.paths.schema)
+ if self.paths.spec.stem != family.name:
+ raise CtlvError(f'{family.path}: family {family.name}',
+ 'a specification must be named after its family, '
+ f'so this file must be {family.name}.yaml')
+ chain = [family]
+ seen = [family.name]
+ self._check_root(family)
+ while chain[0].inherits:
+ parent_name = chain[0].inherits
+ child = chain[0]
+ if parent_name in seen:
+ # seen runs from the selected family to its ancestors.
+ raise CtlvError(f'{child.path}: family {child.name}',
+ 'inheritance cycle: ' +
+ ' -> '.join(seen + [parent_name]))
+ parent_path = self.paths.sibling_spec(parent_name)
+ parent = spec.load(parent_path, self.paths.schema)
+ if parent.name != parent_name:
+ raise CtlvError(f'{parent_path}: family {parent.name}',
+ f'expected family {parent_name}')
+ self._check_root(parent)
+ seen.append(parent_name)
+ chain.insert(0, parent)
+ return tuple(chain)
+
+ def _check_root(self, family):
+ """Require every family but the root one to name what it inherits."""
+ if family.name == spec.CORE_FAMILY_NAME:
+ if family.inherits:
+ raise CtlvError(f'{family.path}: family {family.name}',
+ 'the root family heads every chain, so it '
+ 'inherits nothing')
+ elif not family.inherits:
+ raise CtlvError(f'{family.path}: family {family.name}',
+ 'must name the family it inherits, which is the '
+ f'root {spec.CORE_FAMILY_NAME} for a family '
+ 'directly below it')
+
+ def _check_names(self, chain):
+ """Require every child family name to carry its whole ancestry.
+
+ A dash is the only separator; a dot selects group.attr.
+ """
+ for parent, child in zip(chain, chain[1:]):
+ suffix = child.name.removeprefix(f'{parent.name}-')
+ if not suffix or suffix == child.name:
+ raise CtlvError(f'{child.path}: family {child.name}',
+ f'a family that inherits {parent.name} must '
+ f'be named {parent.name}-<suffix>')
+
+ def _check_order(self, chain):
+ """Require a parent to be registered before what inherits it.
+
+ Ascending positions let the core walk a chain once and index it.
+ """
+ for parent, child in zip(chain, chain[1:]):
+ if self._family_id(parent) >= self._family_id(child):
+ raise CtlvError(f'{child.path}: family {child.name}',
+ f'is registered before {parent.name}, the '
+ 'family it inherits')
+
+ def _family_id(self, family):
+ family_id = self.registry.family_id(family.name)
+ if family_id is None:
+ raise CtlvError(f'{family.path}: family {family.name}',
+ f'not registered in {self.registry.path}')
+ return family_id
+
+ # Declarations
+
+ def _add_family(self, family):
+ family_id = self._family_id(family)
+ self._add_definitions(family, family_id, family.enums, 'enum')
+ self._add_definitions(family, family_id, family.flags, 'flags')
+ self._add_groups(family, family_id)
+ self._add_sets(family, family_id)
+
+ def _add_definitions(self, family, family_id, definitions, kind):
+ for definition in definitions:
+ where = f'{family.path}: {kind} {definition.name}'
+ self._reject_redeclaration(where, self.definitions,
+ definition.name, 'enum or flags')
+ if kind == 'flags' and len(definition.values) > MAX_FLAG_BITS:
+ raise CtlvError(where, f'more than {MAX_FLAG_BITS} flag bits')
+ if kind == 'enum':
+ _check_count(where, 'enum values', len(definition.values),
+ spec.U32_MAX)
+ if family_id == spec.CORE_FAMILY_ID:
+ maximum = ROOT_ENUM_VALUES_MAX.get(definition.name)
+ if maximum is not None:
+ _check_count(where, 'enum values',
+ len(definition.values), maximum)
+ # Enum values from one, zero being reserved; flag bits from zero.
+ first = 1 if kind == 'enum' else 0
+ values = tuple((name, first + index)
+ for index, name in enumerate(definition.values))
+ self.definitions[definition.name] = Definition(
+ definition.name, kind, family_id, values)
+
+ def _add_groups(self, family, family_id):
+ _check_count(f'{family.path}: family {family.name}', 'attr groups',
+ len(family.groups), spec.U16_MAX)
+ for group_id, group in enumerate(family.groups, start=1):
+ where = f'{family.path}: attr group {group.name}'
+ self._reject_redeclaration(where, self.groups, group.name,
+ 'attr group')
+ self._reject_redeclaration(where, self.sets, group.name,
+ 'attr set')
+ attrs = []
+ names = set()
+ _check_count(where, 'attrs', len(group.attrs), spec.U32_MAX)
+ for local_id, decl in enumerate(group.attrs, start=1):
+ attr_where = f'{family.path}: attr {group.name}.{decl.name}'
+ if decl.name in names:
+ raise CtlvError(attr_where, 'duplicate attr')
+ names.add(decl.name)
+ self._check_reference(attr_where, decl)
+ attrs.append(Attr(
+ group=group.name,
+ name=decl.name,
+ family_id=family_id,
+ group_id=group_id,
+ local_id=local_id,
+ attr_id=(family_id << 48) | (group_id << 32) | local_id,
+ decl=decl,
+ ))
+ effective = Group(group.name, family_id, group_id, tuple(attrs))
+ self.groups[group.name] = effective
+ for attr in attrs:
+ self.attrs[attr.full_name] = attr
+
+ def _check_reference(self, where, decl):
+ if decl.kind not in ('enum', 'flags'):
+ return
+ definition = self.definitions.get(decl.ref)
+ if definition is None:
+ raise CtlvError(where, f'unknown {decl.kind} "{decl.ref}"')
+ if definition.kind != decl.kind:
+ raise CtlvError(where, f'"{decl.ref}" is a {definition.kind} '
+ f'definition, not {decl.kind}')
+
+ def _add_sets(self, family, family_id):
+ set_id = 0
+ for attr_set in family.sets:
+ where = f'{family.path}: attr set {attr_set.name}'
+ existing = self.sets.get(attr_set.name)
+ if existing is not None:
+ self._fill_stub(where, family, existing, attr_set)
+ continue
+ self._reject_redeclaration(where, self.groups, attr_set.name,
+ 'attr group')
+ _check_count(where, 'new attr sets', set_id + 1,
+ spec.U32_MAX)
+ set_id += 1
+ self.set_paths[attr_set.name] = family.path
+ self.sets[attr_set.name] = AttrSet(
+ name=attr_set.name,
+ family_id=family_id,
+ set_id=set_id,
+ stub=attr_set.stub,
+ attrs=attr_set.attrs,
+ optional_attrs=attr_set.optional_attrs,
+ )
+
+ def _fill_stub(self, where, family, existing, filler):
+ """Replace an inherited empty stub with a descendant's members."""
+ filled_by = self.stub_fillers.get(existing.name)
+ if filled_by is not None:
+ raise CtlvError(where, f'stub was already filled by family '
+ f'{filled_by}')
+ if not existing.stub:
+ raise CtlvError(where, 'attr set is already declared by an '
+ 'inherited family')
+ if filler.stub:
+ raise CtlvError(where, 'a stub cannot be redeclared; a further '
+ 'extension point needs a new name')
+ self.stub_fillers[existing.name] = family.name
+ self.set_paths[existing.name] = family.path
+ # A fill completes an inherited set, so the IDs are the declarer's.
+ self.sets[existing.name] = dataclasses.replace(
+ existing, stub=False, attrs=filler.attrs,
+ optional_attrs=filler.optional_attrs)
+
+ def _check_sets(self):
+ """Expand every set, so one that no op selects is still checked."""
+ for name, attr_set in self.sets.items():
+ where = f'{self.set_paths[name]}: attr set {name}'
+ # Seeding the stack makes a cycle read from the set expanded.
+ _nodes, _stubs = self._expand_placements(
+ spec.Placements(attr_set.attrs, attr_set.optional_attrs),
+ where, (), 0, set_stack=(attr_set.name,),
+ opened=(attr_set.name,))
+
+ def _reject_redeclaration(self, where, namespace, name, what):
+ if name in namespace:
+ raise CtlvError(where, f'{what} "{name}" is already declared')
+
+ # Ops
+
+ def _add_ops(self, family):
+ family_id = self._family_id(family)
+ _check_count(f'{family.path}: family {family.name}', 'ops',
+ len(family.ops), spec.U32_MAX)
+ _check_count(f'{family.path}: family {family.name}', 'effective ops',
+ len(self.ops) + len(family.ops), spec.U32_MAX)
+ for local_id, op in enumerate(family.ops, start=1):
+ where = f'{family.path}: op {op.name}'
+ if any(existing.name == op.name for existing in self.ops):
+ raise CtlvError(where, f'op "{op.name}" is already declared')
+ request = None
+ if op.request is not None:
+ request = self._tree(op.request, where + ' request')
+ reply = None
+ if op.reply is not None:
+ reply = self._tree(op.reply, where + ' reply')
+ self._check_context(where, op, request, reply)
+ self.ops.append(Op(
+ name=op.name,
+ type=op.type,
+ fd_access=op.fd_access,
+ availability=op.availability,
+ optional=op.optional,
+ pre_post=op.pre_post,
+ family_id=family_id,
+ local_id=local_id,
+ op_id=(family_id << 48) | local_id,
+ request=request,
+ reply=reply,
+ sizes=sizes.compute(where, op, request, reply),
+ ))
+
+ def _tree(self, placements, where):
+ children, stubs = self._expand_placements(placements, where, (), 0)
+ root = _root(children, stubs)
+ _check_tree_size(where, root)
+ return root
+
+ def _expand_placements(self, placements, where, stack, depth,
+ set_stack=(), opened=()):
+ """Return the child nodes of one level and the stubs selected there."""
+ merged = {}
+ expanded, stubs = self._expand(placements.attrs, False, where,
+ set_stack)
+ optional, more = self._expand(placements.optional_attrs, True, where,
+ set_stack)
+ expanded += optional
+ stubs += more
+ for attr, required, fill in expanded:
+ entry = merged.get(attr.attr_id)
+ if entry is None:
+ merged[attr.attr_id] = [attr, required, fill]
+ elif entry[2] != fill and (entry[2] is not None or
+ fill is not None):
+ raise CtlvError(
+ where, f'attr {attr.full_name} is selected more than '
+ 'once in one composed nest')
+ elif required:
+ # A required selection at this level beats an optional one.
+ entry[1] = True
+ nodes = tuple(self._node(attr, required, where, stack, depth, opened)
+ for attr, required, _fill in merged.values())
+ return nodes, _merge_stubs(stubs)
+
+ def _expand(self, selectors, force_optional, where, stack):
+ expanded = []
+ stubs = ()
+ for selector in selectors:
+ attrs, found = self._expand_one(selector, force_optional, where,
+ stack)
+ expanded += attrs
+ stubs += found
+ return expanded, stubs
+
+ def _expand_one(self, selector, force_optional, where, stack):
+ if '.' in selector:
+ attr = self.attrs.get(selector)
+ if attr is None:
+ raise CtlvError(where, f'unknown attr "{selector}"')
+ return [(attr, not force_optional, None)], ()
+ group = self.groups.get(selector)
+ if group is not None:
+ return [(attr, not force_optional, None)
+ for attr in group.attrs], ()
+ attr_set = self.sets.get(selector)
+ if attr_set is None:
+ raise CtlvError(where, f'unknown selector "{selector}"')
+ if attr_set.stub:
+ # Nothing to expand until a descendant fills it.
+ return [], (Stub(attr_set.name, attr_set.family_id,
+ attr_set.set_id, force_optional),)
+ if selector in stack:
+ raise CtlvError(where, 'attr set cycle: ' +
+ ' -> '.join(stack + (selector,)))
+ stack += (selector,)
+ # Under an optional selection every member becomes optional.
+ expanded, stubs = self._expand(attr_set.attrs, force_optional, where,
+ stack)
+ optional, more = self._expand(attr_set.optional_attrs, True, where,
+ stack)
+ expanded += optional
+ if selector in self.stub_fillers:
+ expanded = [(attr, required, selector)
+ for attr, required, _fill in expanded]
+ return expanded, stubs + more
+
+ def _node(self, attr, required, where, stack, depth, opened=()):
+ decl = attr.decl
+ if decl.kind != 'nested':
+ return Node(attr, required, decl.max_occurrences, (), ())
+ holds = self._nested_set(where, attr)
+ if decl.recursive and holds in opened:
+ # Open above, so one level further down rather than a new one.
+ return Node(attr, required, decl.max_occurrences, (), (),
+ reenters=holds)
+ if attr.attr_id in stack:
+ names = [self._attr_by_id(i).full_name for i in stack]
+ raise CtlvError(where, 'nesting cycle: ' +
+ ' -> '.join(names + [attr.full_name]))
+ if depth + 1 > sizes.MAX_DECLARED_NEST_DEPTH:
+ raise CtlvError(where, f'{attr.full_name} nests deeper than the '
+ f'{sizes.MAX_DECLARED_NEST_DEPTH} level '
+ 'limit')
+ children, stubs = self._expand_placements(
+ spec.Placements(decl.attrs, decl.optional_attrs), where,
+ stack + (attr.attr_id,), depth + 1,
+ opened=opened + ((holds,) if holds else ()))
+ node = Node(attr, required, decl.max_occurrences,
+ tuple(child for child in children if child.required),
+ tuple(child for child in children if not child.required),
+ stubs, expands=holds)
+ if decl.recursive and not _reenters(node, holds):
+ raise CtlvError(where, f'{attr.full_name} is recursive, but attr '
+ f'set "{holds}" never selects it again')
+ return node
+
+ def _nested_set(self, where, attr):
+ """The one attr set a nest holds, when that is all it holds.
+
+ Only such a nest can be re-entered, a re-entry meaning the members
+ open above.
+ """
+ decl = attr.decl
+ if not decl.recursive:
+ if decl.optional_attrs or len(decl.attrs) != 1:
+ return None
+ return decl.attrs[0] if decl.attrs[0] in self.sets else None
+ target = decl.attrs[0]
+ if target in self.groups:
+ raise CtlvError(where, f'{attr.full_name} is recursive, so it '
+ f'nests an attr set, and "{target}" is an '
+ 'attr group')
+ if target not in self.sets:
+ raise CtlvError(where, f'unknown selector "{target}"')
+ return target
+
+ def _attr_by_id(self, attr_id):
+ for attr in self.attrs.values():
+ if attr.attr_id == attr_id:
+ return attr
+ raise AssertionError(f'no attr with ID {attr_id:#x}')
+
+ def _check_context(self, where, op, request, reply):
+ if request is not None:
+ self._check_subtree(where, request, 'request')
+ if reply is None:
+ return
+ if op.type == 'query':
+ self._check_subtree(where, reply, 'query reply')
+ self._check_reentry(where, reply)
+ elif op.type == 'event':
+ self._check_subtree(where, reply, 'event')
+ else:
+ self._check_subtree(where, reply, 'action reply')
+
+ def _check_reentry(self, where, root):
+ """A repeating level cannot also be widened by a descendant."""
+ if not _reenters_anywhere(root) or not _stubs_anywhere(root):
+ return
+ raise CtlvError(f'{where} query reply',
+ 're-enters a level and selects an extension point, '
+ 'and a descendant cannot widen a level that repeats')
+
+ def _check_subtree(self, where, node, context):
+ if node.attr is not None:
+ name = node.attr.full_name
+ if (node.max_occurrences == spec.UNLIMITED and
+ context != 'query reply'):
+ raise CtlvError(f'{where} {context}',
+ f'{name} has unlimited occurrences, which '
+ 'only a query reply may contain')
+ if node.reenters is not None and context != 'query reply':
+ raise CtlvError(f'{where} {context}',
+ f'{name} re-enters "{node.reenters}", so it '
+ 'has no maximum length, which only a query '
+ 'reply may contain')
+ if node.attr.decl.kind == 'blob' and context != 'request':
+ raise CtlvError(f'{where} {context}',
+ f'{name} is a blob attr, which only a '
+ 'request may contain')
+ for child in node.children():
+ self._check_subtree(where, child, context)
+
+
+def _reenters(node, name):
+ """Whether anything below node re-enters name."""
+ return any(child.reenters == name or _reenters(child, name)
+ for child in node.children())
+
+
+def _reenters_anywhere(node):
+ return any(child.reenters is not None or _reenters_anywhere(child)
+ for child in node.children())
+
+
+def _stubs_anywhere(node):
+ return bool(node.stubs) or any(_stubs_anywhere(child)
+ for child in node.children())
+
+
+def _composed_members(fills, node, force_optional=False):
+ members = [(child, force_optional)
+ for child in node.required_children]
+ members += [(child, True) for child in node.optional_children]
+ for stub in node.stubs:
+ fill = fills.get((stub.family_id, stub.set_id))
+ if fill is not None:
+ members += _composed_members(
+ fills, fill.root, force_optional or stub.optional)
+ return members
+
+
+def _check_composed(family):
+ fills = {(fill.family_id, fill.set_id): fill
+ for fill in family.chain_fills}
+
+ def walk(where, node):
+ members = _composed_members(fills, node)
+ required = sum(not optional for _child, optional in members)
+ optional = len(members) - required
+ seen = set()
+
+ _check_count(where, 'composed required attrs', required,
+ spec.U32_MAX)
+ _check_count(where, 'composed optional attrs', optional,
+ spec.U32_MAX)
+ count = 1
+ for child, _optional in members:
+ if child.attr.attr_id in seen:
+ raise CtlvError(
+ where, f'attr {child.attr.full_name} is selected more '
+ 'than once in one composed nest')
+ seen.add(child.attr.attr_id)
+ count += walk(where, child)
+ return count
+
+ for op in family.ops:
+ total = 0
+ for which, root in (('request', op.request), ('reply', op.reply)):
+ if root is None:
+ continue
+ where = f'{family.path}: op {op.name} {which}'
+ count = walk(where, root)
+ _check_count(where, 'composed schema nodes', count,
+ spec.U32_MAX)
+ total += count
+ _check_count(f'{family.path}: op {op.name}',
+ 'composed request and reply nodes', total,
+ spec.U32_MAX)
+
+
+def _node_count(node):
+ return 1 + sum(_node_count(child) for child in node.children())
+
+
+def _stub_count(node):
+ return len(node.stubs) + sum(_stub_count(child)
+ for child in node.children())
+
+
+def _check_tree_size(where, root):
+ _check_count(where, 'schema nodes', _node_count(root), spec.U32_MAX)
+ _check_count(where, 'schema stubs', _stub_count(root), spec.U32_MAX)
+
+
+def _root(children, stubs=()):
+ return Node(None, True, 1,
+ tuple(node for node in children if node.required),
+ tuple(node for node in children if not node.required),
+ stubs)
+
+
+def _merge_stubs(stubs):
+ """One reference per stub selected at a level, required winning."""
+ merged = {}
+ for stub in stubs:
+ previous = merged.get(stub.name)
+ if previous is None or not stub.optional:
+ merged[stub.name] = stub
+ return tuple(merged.values())
diff --git a/tools/ctlv/ctlvgen/lib/sizes.py b/tools/ctlv/ctlvgen/lib/sizes.py
new file mode 100644
index 000000000000..864e3b6873e7
--- /dev/null
+++ b/tools/ctlv/ctlvgen/lib/sizes.py
@@ -0,0 +1,238 @@
+# SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+"""Length arithmetic over an expanded op tree.
+
+These numbers decide whether an op can run at all, so they are kept apart from
+the rest of resolution and tested against independently calculated lengths.
+The constants mirror include/uapi/linux/ctlv.h, and a test checks that.
+"""
+
+from __future__ import annotations
+
+import dataclasses
+
+from . import spec
+from .errors import CtlvError
+
+ALIGNTO = 8
+ATTR_HDR_LEN = 16
+EVENT_HDR_LEN = 24
+
+MAX_INLINE_MESSAGE_LEN = 0x100000
+MAX_ERROR_LEN = 0x4000
+MAX_NEST_DEPTH = 32
+DEFAULT_QUERY_REPLY_LEN = 0x10000
+MAX_EVENT_QUEUE_LEN = 0x1000000
+
+# One level short of what the transport carries: op-schema-get describes a
+# message, and a description nests one level deeper than what it describes.
+MAX_DECLARED_NEST_DEPTH = MAX_NEST_DEPTH - 1
+
+# Every integer, enum and flags payload is 64-bit.
+SCALAR_PAYLOAD_LEN = 8
+
+# A bool payload is one byte, padded to eight only as a placement.
+BOOL_PAYLOAD_LEN = 1
+
+# How many core scalar descriptor attrs each blob backing carries. A test
+# checks these against the blob group of the core family.
+BLOB_DESCRIPTOR_ATTRS = {
+ 'va': 3,
+}
+
+# Attr sets of the root family that are message roots in their own right, each
+# with the transport header's limit and the name the header gives it.
+CORE_MESSAGES = {
+ 'error-info': ('CTLV_MAX_ERROR_LEN', MAX_ERROR_LEN),
+}
+
+
+@dataclasses.dataclass(frozen=True)
+class OpSizes:
+ """Generated lengths of one op. Zero means "does not apply"."""
+
+ request_max_len: int
+ action_reply_max_len: int
+ query_reply_min_len: int
+ query_reply_suggested_len: int
+ event_record_max_len: int
+
+
+def align(length):
+ return (length + ALIGNTO - 1) & ~(ALIGNTO - 1)
+
+
+def attr_max_len(node):
+ """Maximum length of one occurrence of node, excluding its own padding."""
+ return ATTR_HDR_LEN + _payload_max_len(node)
+
+
+def root_max_len(root):
+ """Maximum length of a complete message root."""
+ return ATTR_HDR_LEN + _payload_max_len(root)
+
+
+def min_reply_len(root):
+ """Buffer length needed for the largest indivisible leaf and its path.
+
+ A leaf below a re-entered level sits as deep as the transport allows.
+ """
+ if _reenters(root):
+ return (MAX_NEST_DEPTH + 1) * ATTR_HDR_LEN + _largest_leaf(root)
+ return _min_walk(root, 0)
+
+
+def compute(where, op, request, reply):
+ """Return the generated lengths of one op."""
+ request_max_len = 0
+ if request is not None:
+ request_max_len = root_max_len(request)
+ _check_inline(where, 'request', request_max_len)
+
+ action_reply_max_len = 0
+ query_reply_min_len = 0
+ query_reply_suggested_len = 0
+ event_record_max_len = 0
+
+ if op.type == 'action' and reply is not None:
+ action_reply_max_len = root_max_len(reply)
+ _check_inline(where, 'action reply', action_reply_max_len)
+ elif op.type == 'query':
+ query_reply_min_len = min_reply_len(reply)
+ _check_inline(where, 'largest query reply leaf and its path',
+ query_reply_min_len)
+ query_reply_suggested_len = _suggested_len(
+ where, op.suggested_reply_length, query_reply_min_len)
+ elif op.type == 'event':
+ payload_max_len = root_max_len(reply)
+ _check_inline(where, 'event payload', payload_max_len)
+ event_record_max_len = EVENT_HDR_LEN + align(payload_max_len)
+
+ return OpSizes(request_max_len=request_max_len,
+ action_reply_max_len=action_reply_max_len,
+ query_reply_min_len=query_reply_min_len,
+ query_reply_suggested_len=query_reply_suggested_len,
+ event_record_max_len=event_record_max_len)
+
+
+def composed(where, op, request, reply):
+ """Return the lengths one op needs for one family.
+
+ compute()'s numbers over the composed tree, fills spliced in. A suggestion
+ is raised here rather than refused.
+ """
+ sizes = OpSizes(request_max_len=root_max_len(request) if request else 0,
+ action_reply_max_len=0, query_reply_min_len=0,
+ query_reply_suggested_len=0, event_record_max_len=0)
+ _check_inline(where, 'request', sizes.request_max_len)
+
+ if op.type == 'action' and reply is not None:
+ sizes = dataclasses.replace(sizes,
+ action_reply_max_len=root_max_len(reply))
+ _check_inline(where, 'action reply', sizes.action_reply_max_len)
+ elif op.type == 'query':
+ minimum = min_reply_len(reply)
+ _check_inline(where, 'largest query reply leaf and its path', minimum)
+ sizes = dataclasses.replace(
+ sizes, query_reply_min_len=minimum,
+ query_reply_suggested_len=max(op.sizes.query_reply_suggested_len,
+ minimum))
+ elif op.type == 'event':
+ payload_max_len = root_max_len(reply)
+ _check_inline(where, 'event payload', payload_max_len)
+ record = EVENT_HDR_LEN + align(payload_max_len)
+ if record > MAX_EVENT_QUEUE_LEN:
+ raise CtlvError(where, f'event record needs {record} bytes, more '
+ f'than the {MAX_EVENT_QUEUE_LEN} byte '
+ 'event queue limit')
+ sizes = dataclasses.replace(sizes, event_record_max_len=record)
+ return sizes
+
+
+def message_max_len(where, name, root):
+ """Return the maximum length of one core message, against its limit."""
+ limit_name, limit = CORE_MESSAGES[name]
+ length = root_max_len(root)
+ if length > limit:
+ raise CtlvError(where, f'needs {length} bytes, more than the {limit} '
+ f'of {limit_name}')
+ return length
+
+
+def _payload_max_len(node):
+ if node.is_root:
+ return _children_max_len(node)
+
+ decl = node.attr.decl
+ if decl.kind in ('uint', 'sint', 'enum', 'flags'):
+ return SCALAR_PAYLOAD_LEN
+ if decl.kind == 'bool':
+ return BOOL_PAYLOAD_LEN
+ if decl.kind == 'string':
+ # One terminating NUL, which max-length excludes.
+ return decl.max_length + 1
+ if decl.kind == 'binary':
+ return decl.max_length
+ if decl.kind == 'blob':
+ widest = max(BLOB_DESCRIPTOR_ATTRS[backing]
+ for backing in decl.blob.backings)
+ return widest * align(ATTR_HDR_LEN + SCALAR_PAYLOAD_LEN)
+ return _children_max_len(node)
+
+
+def _children_max_len(node):
+ total = 0
+ for child in node.children():
+ if child.max_occurrences == spec.UNLIMITED:
+ raise AssertionError(
+ f'{child.attr.full_name} has unlimited occurrences, so it has '
+ 'no maximum length')
+ if child.reenters is not None:
+ raise AssertionError(
+ f'{child.attr.full_name} re-enters {child.reenters}, so it '
+ 'has no maximum length')
+ total += child.max_occurrences * align(attr_max_len(child))
+ return total
+
+
+def _min_walk(node, ancestors_len):
+ children = node.children()
+ if children:
+ return max(_min_walk(child, ancestors_len + ATTR_HDR_LEN)
+ for child in children)
+ if node.is_root:
+ return ancestors_len + ATTR_HDR_LEN
+ return ancestors_len + align(attr_max_len(node))
+
+
+def _reenters(node):
+ return any(child.reenters is not None or _reenters(child)
+ for child in node.children())
+
+
+def _largest_leaf(node):
+ """The longest placement of the tree that a fragment cannot split."""
+ lengths = [align(attr_max_len(child))
+ for child in node.children()
+ if child.attr.decl.kind != 'nested']
+ lengths += [_largest_leaf(child) for child in node.children()]
+ return max(lengths, default=0)
+
+
+def _check_inline(where, what, length):
+ if length > MAX_INLINE_MESSAGE_LEN:
+ raise CtlvError(where, f'{what} needs {length} bytes, more than the '
+ f'{MAX_INLINE_MESSAGE_LEN} byte inline '
+ 'message limit')
+
+
+def _suggested_len(where, declared, minimum):
+ if declared is None:
+ return max(DEFAULT_QUERY_REPLY_LEN, minimum)
+ if declared < minimum:
+ raise CtlvError(where, f'suggested-reply-length {declared} is below '
+ f'the computed minimum of {minimum}')
+ if declared > MAX_INLINE_MESSAGE_LEN:
+ raise CtlvError(where, f'suggested-reply-length {declared} exceeds '
+ f'the {MAX_INLINE_MESSAGE_LEN} byte inline '
+ 'message limit')
+ return declared
diff --git a/tools/ctlv/ctlvgen/lib/spec.py b/tools/ctlv/ctlvgen/lib/spec.py
new file mode 100644
index 000000000000..8942664d88a7
--- /dev/null
+++ b/tools/ctlv/ctlvgen/lib/spec.py
@@ -0,0 +1,583 @@
+# SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+"""The as-written form of a CTLV family specification.
+
+The strict YAML subset, the meta-schema, and every rule that one declaration
+decides on its own. Comparing declarations and walking a chain are
+semantics.py's. Omitted values are normalized here, once.
+"""
+
+from __future__ import annotations
+
+import dataclasses
+import pathlib
+import re
+
+import jsonschema
+
+from . import yamlload
+from .errors import CtlvError
+
+# A generated file carries the license of its specification's first line.
+LICENSE = re.compile(r'#\s*SPDX-License-Identifier:\s*(\S.*?)\s*$')
+
+# The registry is indexed by family ID, so the root family is its first entry.
+CORE_FAMILY_NAME = 'ctlv'
+CORE_FAMILY_ID = 0
+
+MAX_FAMILY_NAME_LEN = 63
+U8_MAX = (1 << 8) - 1
+U16_MAX = (1 << 16) - 1
+U32_MAX = (1 << 32) - 1
+MAX_FAMILY_ID = U16_MAX
+
+# Zero means unlimited, as the introspection wire form has it.
+UNLIMITED = 0
+
+U64_MAX = (1 << 64) - 1
+S64_MIN = -(1 << 63)
+S64_MAX = (1 << 63) - 1
+S8_MIN = -(1 << 7)
+S8_MAX = (1 << 7) - 1
+
+PRIMARY_KEYS = ('type', 'enum', 'flags', 'attrs', 'blob')
+
+INTEGER_KINDS = ('uint', 'sint')
+SCALAR_KINDS = ('uint', 'sint', 'bool', 'string', 'binary', 'enum', 'flags')
+
+FD_ACCESSES = ('read', 'read-write')
+DEFAULT_FD_ACCESS = 'read-write'
+
+# Every op has one of these, and only an action or a query may be dynamic.
+AVAILABILITIES = ('fixed', 'dynamic')
+DEFAULT_AVAILABILITY = 'fixed'
+
+# Keys each primary form may add, beyond its own, name and max-occurrences.
+_ACCESSORY_KEYS = {
+ 'uint': {'min', 'max', 'unit', 'scale'},
+ 'sint': {'min', 'max', 'unit', 'scale'},
+ 'bool': set(),
+ 'string': {'min-length', 'max-length'},
+ 'binary': {'length', 'min-length', 'max-length'},
+ 'enum': set(),
+ 'flags': set(),
+ 'nested': {'optional-attrs', 'recursive'},
+ 'blob': set(),
+}
+_COMMON_KEYS = {'name', 'max-occurrences'}
+
+
+@dataclasses.dataclass(frozen=True)
+class ValueList:
+ """An enum or a flags definition."""
+
+ name: str
+ values: tuple
+
+
+@dataclasses.dataclass(frozen=True)
+class BlobDecl:
+ """The blob properties of one blob attr declaration."""
+
+ direction: str
+ backings: tuple
+ max_length: int
+ alignment: int
+
+
+@dataclasses.dataclass(frozen=True)
+class AttrDecl:
+ """One attr declaration inside an attr group.
+
+ kind is the resolved primary form, and a field that does not apply stays
+ None. recursive marks a nest whose attr set may select this attr again.
+ """
+
+ name: str
+ kind: str
+ ref: str = None
+ attrs: tuple = ()
+ optional_attrs: tuple = ()
+ recursive: bool = False
+ blob: BlobDecl = None
+ min: int = None
+ max: int = None
+ min_length: int = None
+ max_length: int = None
+ unit: str = None
+ scale: int = None
+ max_occurrences: int = 1
+
+
+@dataclasses.dataclass(frozen=True)
+class AttrGroupSpec:
+ name: str
+ attrs: tuple
+
+
+@dataclasses.dataclass(frozen=True)
+class AttrSetSpec:
+ name: str
+ stub: bool
+ attrs: tuple
+ optional_attrs: tuple
+
+
+@dataclasses.dataclass(frozen=True)
+class Placements:
+ """The required and optional selectors declared at one level."""
+
+ attrs: tuple = ()
+ optional_attrs: tuple = ()
+
+
+@dataclasses.dataclass(frozen=True)
+class OpSpec:
+ name: str
+ type: str
+ request: Placements
+ reply: Placements = None
+ fd_access: str = DEFAULT_FD_ACCESS
+ availability: str = DEFAULT_AVAILABILITY
+ optional: bool = False
+ pre_post: bool = False
+ suggested_reply_length: int = None
+
+
+@dataclasses.dataclass(frozen=True)
+class FamilySpec:
+ """One specification file, syntax-checked and normalized."""
+
+ path: str
+ name: str
+ license: str
+ inherits: str
+ enums: tuple
+ flags: tuple
+ groups: tuple
+ sets: tuple
+ ops: tuple
+ pre_init: bool = False
+ post_init: bool = False
+ pre_fini: bool = False
+
+
+@dataclasses.dataclass(frozen=True)
+class RegistryEntry:
+ name: str
+ deprecated: bool
+
+
+@dataclasses.dataclass(frozen=True)
+class Registry:
+ """The append-only family registry, indexed by family ID."""
+
+ path: str
+ entries: tuple
+
+ @property
+ def license(self):
+ return _license(self.path)
+
+ def family_id(self, name):
+ """Return the registered family ID of name, or None if unregistered."""
+ for family_id, entry in enumerate(self.entries):
+ if entry.name == name:
+ return family_id
+ return None
+
+
+class Paths:
+ """Where the specs, the registry and the meta-schemas are."""
+
+ def __init__(self, spec, schema=None, registry=None, abi=None):
+ self.spec = pathlib.Path(spec)
+ self.spec_dir = self.spec.parent
+ self.doc_dir = self.spec_dir.parent
+ self.schema = pathlib.Path(schema or self.doc_dir / 'ctlv-schema.yaml')
+ self.registry = pathlib.Path(registry or
+ self.doc_dir / 'families.yaml')
+ # The two meta-schemas ship together, so overriding one locates both.
+ self.registry_schema = (self.schema.parent /
+ 'ctlv-registry-schema.yaml')
+ self.abi_dir = pathlib.Path(abi or self.doc_dir / 'abi')
+ self.registry_abi = self.abi_dir / 'families.registry.yaml'
+ self.generated = self.doc_dir / 'generated-files.yaml'
+ self.generated_schema = self.doc_dir / 'generated-files-schema.yaml'
+ self.kernel_dir = self.schema.parent.parent.parent
+
+ def sibling_spec(self, name):
+ """Return the path of another family's specification.
+
+ The one root family lives where the meta-schema does, so a corpus of
+ fixtures inherits it rather than a copy.
+ """
+ if name == CORE_FAMILY_NAME:
+ return self.schema.parent / 'specs' / f'{name}.yaml'
+ return self.spec_dir / f'{name}.yaml'
+
+ def abi(self, name):
+ """Return the path of one family's checked-in ABI snapshot."""
+ return self.abi_dir / f'{name}.yaml'
+
+
+def load_registry(paths, path=None):
+ """Load and check the family registry."""
+ path = pathlib.Path(path or paths.registry)
+ data = yamlload.load(path)
+ _validate_syntax(path, data, paths.registry_schema)
+
+ entries = []
+ seen = set()
+ for item in data['families']:
+ if isinstance(item, dict):
+ entry = RegistryEntry(item['name'], item['deprecated'])
+ else:
+ entry = RegistryEntry(item, False)
+ _family_name(f'{path}: family {entry.name}', entry.name)
+ if entry.name in seen:
+ raise CtlvError(f'{path}: {entry.name}', 'duplicate family')
+ seen.add(entry.name)
+ entries.append(entry)
+
+ core = entries[CORE_FAMILY_ID]
+ if core.name != CORE_FAMILY_NAME:
+ raise CtlvError(f'{path}: {core.name}',
+ f'family ID {CORE_FAMILY_ID} belongs to '
+ f'{CORE_FAMILY_NAME}, so it must be the first entry')
+ if core.deprecated:
+ raise CtlvError(f'{path}: {core.name}',
+ 'the core family cannot be deprecated')
+ if len(entries) - 1 > MAX_FAMILY_ID:
+ raise CtlvError(str(path),
+ f'{len(entries)} entries need a family ID past the '
+ f'{MAX_FAMILY_ID} maximum')
+ return Registry(str(path), tuple(entries))
+
+
+def load(path, schema=None):
+ """Load, syntax-check and normalize one family specification."""
+ path = pathlib.Path(path)
+ data = yamlload.load(path)
+ _validate_syntax(path, data, schema)
+
+ family = data['family']
+ name = _family_name(f'{path}: family {family["name"]}', family['name'])
+ inherits = family.get('inherits')
+ if inherits is not None:
+ inherits = _family_name(f'{path}: family {name} parent', inherits)
+ return FamilySpec(
+ path=str(path),
+ name=name,
+ license=_license(path),
+ inherits=inherits,
+ enums=_value_lists(path, data.get('enums', []), 'enum'),
+ flags=_value_lists(path, data.get('flags', []), 'flags'),
+ groups=_groups(path, data.get('attr-groups', [])),
+ sets=_sets(path, data.get('attr-sets', [])),
+ ops=_ops(path, data.get('ops', [])),
+ pre_init=family.get('pre-init', False),
+ post_init=family.get('post-init', False),
+ pre_fini=family.get('pre-fini', False),
+ )
+
+
+def _license(path):
+ """Return the SPDX identifier the specification's first line carries."""
+ with open(path, 'r', encoding='utf-8') as stream:
+ found = LICENSE.match(stream.readline())
+ if found is None:
+ raise CtlvError(str(path),
+ 'the first line must be an SPDX-License-Identifier '
+ 'comment, which every generated file inherits')
+ return found.group(1)
+
+
+def _validate_syntax(path, data, schema_path):
+ schema = yamlload.load(schema_path)
+ validator = jsonschema.Draft7Validator(schema)
+ error = jsonschema.exceptions.best_match(validator.iter_errors(data))
+ if error is None:
+ return
+ where = f'{path}: {error.json_path}' if error.absolute_path else str(path)
+ raise CtlvError(where, error.message)
+
+
+def _family_name(where, name):
+ if len(name) > MAX_FAMILY_NAME_LEN:
+ raise CtlvError(where, f'name is {len(name)} characters, more than '
+ f'the {MAX_FAMILY_NAME_LEN} maximum')
+ return name
+
+
+def _value_lists(path, raw_lists, what):
+ lists = []
+ for raw in raw_lists:
+ name = raw['name']
+ where = f'{path}: {what} {name}'
+ values = []
+ seen = set()
+ for raw_value in raw['values']:
+ value = raw_value
+ if value in seen:
+ raise CtlvError(where, f'duplicate value "{value}"')
+ seen.add(value)
+ values.append(value)
+ lists.append(ValueList(name, tuple(values)))
+ return tuple(lists)
+
+
+def _groups(path, raw_groups):
+ groups = []
+ for raw in raw_groups:
+ name = raw['name']
+ attrs = tuple(_attr(f'{path}: attr {name}.{a["name"]}', a)
+ for a in raw['attrs'])
+ groups.append(AttrGroupSpec(name, attrs))
+ return tuple(groups)
+
+
+def _attr(where, raw):
+ primary = _primary_key(where, raw)
+ kind = _kind(primary, raw)
+ _reject_unusable_keys(where, raw, primary, kind)
+
+ attr = {
+ 'name': raw['name'],
+ 'kind': kind,
+ 'max_occurrences': _max_occurrences(where, raw),
+ }
+ if kind in INTEGER_KINDS:
+ attr.update(_integer_bounds(where, raw, kind))
+ elif kind == 'string':
+ attr.update(_string_lengths(where, raw))
+ elif kind == 'binary':
+ attr.update(_binary_lengths(where, raw))
+ elif kind in ('enum', 'flags'):
+ attr['ref'] = raw[kind]
+ elif kind == 'nested':
+ attr['attrs'] = _selectors(raw['attrs'])
+ attr['optional_attrs'] = _selectors(raw.get('optional-attrs'))
+ attr['recursive'] = raw.get('recursive', False)
+ if attr['recursive']:
+ _check_recursive(where, attr)
+ elif kind == 'blob':
+ attr['blob'] = _blob(where, raw['blob'])
+ # A bool has nothing to normalize.
+ return AttrDecl(**attr)
+
+
+def _check_recursive(where, attr):
+ """A recursive nest is one named set, which is what it re-enters."""
+ if attr['optional_attrs']:
+ raise CtlvError(where, 'a recursive attr cannot declare '
+ 'optional-attrs; the attr set it names says '
+ 'which of its own members are optional')
+ if len(attr['attrs']) != 1:
+ raise CtlvError(where, 'a recursive attr nests exactly one attr set')
+ if '.' in attr['attrs'][0]:
+ raise CtlvError(where, f'"{attr["attrs"][0]}" is one attr, and a '
+ 'recursive attr nests an attr set')
+
+
+def _primary_key(where, raw):
+ present = [key for key in PRIMARY_KEYS if key in raw]
+ if not present:
+ raise CtlvError(where, 'no primary declaration; expected one of '
+ 'type, enum, flags, attrs or blob')
+ if len(present) > 1:
+ raise CtlvError(where, 'conflicting primary declarations: ' +
+ ', '.join(present))
+ return present[0]
+
+
+def _kind(primary, raw):
+ if primary == 'type':
+ return raw['type']
+ if primary == 'attrs':
+ return 'nested'
+ return primary
+
+
+def _reject_unusable_keys(where, raw, primary, kind):
+ allowed = _COMMON_KEYS | {primary} | _ACCESSORY_KEYS[kind]
+ unusable = sorted(set(raw) - allowed)
+ if unusable:
+ raise CtlvError(where, f'a {kind} attr cannot declare ' +
+ ', '.join(unusable))
+
+
+def _max_occurrences(where, raw):
+ value = raw.get('max-occurrences', 1)
+ if value == 'unlimited':
+ return UNLIMITED
+ if value < 1 or value > U32_MAX:
+ raise CtlvError(where, f'max-occurrences {value} is outside the '
+ 'u32 range')
+ return value
+
+
+def _integer_bounds(where, raw, kind):
+ low, high = (0, U64_MAX) if kind == 'uint' else (S64_MIN, S64_MAX)
+ minimum = raw.get('min', low)
+ maximum = raw.get('max', high)
+ if minimum < low or minimum > high:
+ raise CtlvError(where, f'min {minimum} is outside the {kind} range')
+ if maximum < low or maximum > high:
+ raise CtlvError(where, f'max {maximum} is outside the {kind} range')
+ if minimum > maximum:
+ raise CtlvError(where, f'min {minimum} is greater than max {maximum}')
+ if 'scale' in raw and 'unit' not in raw:
+ raise CtlvError(where, 'scale requires unit')
+ scale = raw.get('scale', 0) if 'unit' in raw else None
+ if scale is not None and (scale < S8_MIN or scale > S8_MAX):
+ raise CtlvError(where, f'scale {scale} is outside the s8 range')
+ return {
+ 'min': minimum,
+ 'max': maximum,
+ 'unit': raw.get('unit'),
+ 'scale': scale,
+ }
+
+
+def _u32_value(where, what, value):
+ if value < 0 or value > U32_MAX:
+ raise CtlvError(where, f'{what} {value} is outside the u32 range')
+ return value
+
+
+def _string_lengths(where, raw):
+ if 'max-length' not in raw:
+ raise CtlvError(where, 'a string attr requires max-length')
+ minimum = _u32_value(where, 'min-length', raw.get('min-length', 0))
+ maximum = _u32_value(where, 'max-length', raw['max-length'])
+ if minimum > maximum:
+ raise CtlvError(where, f'min-length {minimum} is greater than '
+ f'max-length {maximum}')
+ return {'min_length': minimum, 'max_length': maximum}
+
+
+def _binary_lengths(where, raw):
+ if 'length' in raw:
+ if 'min-length' in raw or 'max-length' in raw:
+ raise CtlvError(where, 'length cannot be combined with '
+ 'min-length or max-length')
+ length = _u32_value(where, 'length', raw['length'])
+ return {'min_length': length, 'max_length': length}
+ if 'max-length' not in raw:
+ raise CtlvError(where, 'a binary attr requires length or max-length')
+ minimum = _u32_value(where, 'min-length', raw.get('min-length', 0))
+ maximum = _u32_value(where, 'max-length', raw['max-length'])
+ if minimum > maximum:
+ raise CtlvError(where, f'min-length {minimum} is greater than '
+ f'max-length {maximum}')
+ return {'min_length': minimum, 'max_length': maximum}
+
+
+def _blob(where, raw):
+ alignment = raw.get('alignment', 1)
+ maximum = raw['max-length']
+ if maximum < 1 or maximum > U64_MAX:
+ raise CtlvError(where, f'blob max-length {maximum} is outside the '
+ 'u64 range')
+ if alignment < 1 or alignment > U32_MAX:
+ raise CtlvError(where, f'blob alignment {alignment} is outside the '
+ 'u32 range')
+ if alignment & (alignment - 1):
+ raise CtlvError(where, f'blob alignment {alignment} is not a power '
+ 'of two')
+ if maximum % alignment:
+ raise CtlvError(where, f'blob max-length {maximum} is not '
+ f'a multiple of alignment {alignment}')
+ return BlobDecl(direction=raw['direction'],
+ backings=tuple(raw['allowed-backings']),
+ max_length=maximum,
+ alignment=alignment)
+
+
+def _sets(path, raw_sets):
+ sets = []
+ for raw in raw_sets:
+ name = raw['name']
+ where = f'{path}: attr set {name}'
+ stub = raw.get('stub', False)
+ attrs = _selectors(raw.get('attrs'))
+ optional = _selectors(raw.get('optional-attrs'))
+ if stub and (attrs or optional):
+ raise CtlvError(where, 'a stub attr set cannot declare members')
+ if not stub and not attrs and not optional:
+ raise CtlvError(where, 'attr set has no members; declare '
+ 'stub: true for an extension point')
+ sets.append(AttrSetSpec(name, stub, attrs, optional))
+ return tuple(sets)
+
+
+def _ops(path, raw_ops):
+ ops = []
+ for raw in raw_ops:
+ name = raw['name']
+ where = f'{path}: op {name}'
+ op_type = raw['type']
+ suggested = raw.get('suggested-reply-length')
+ if suggested is not None:
+ suggested = _u32_value(where, 'suggested-reply-length',
+ suggested)
+ if op_type == 'event' and 'request' in raw:
+ raise CtlvError(where, 'an event op cannot declare a request')
+ if op_type == 'query' and 'reply' not in raw:
+ raise CtlvError(where, 'a query op must declare a reply')
+ if 'suggested-reply-length' in raw and op_type != 'query':
+ raise CtlvError(where, 'suggested-reply-length is only valid on '
+ 'a query op')
+ if 'fd-access' in raw and op_type == 'event':
+ raise CtlvError(where, 'fd-access is only valid on an action or '
+ 'a query op')
+ # An event is not executed, so there is nothing to enable or disable.
+ if 'availability' in raw and op_type == 'event':
+ raise CtlvError(where, 'availability is only valid on an action '
+ 'or a query op')
+ # An event is produced rather than answered, so there is nothing for a
+ # device to opt in to.
+ if 'optional' in raw and op_type == 'event':
+ raise CtlvError(where, 'optional is only valid on an action or a '
+ 'query op')
+ # A hook pair wraps the execution of an op, and an event has none.
+ if 'pre-post' in raw and op_type == 'event':
+ raise CtlvError(where, 'pre-post is only valid on an action or a '
+ 'query op')
+ # An omitted action or query request means a root with no children.
+ request = None
+ if op_type != 'event':
+ request = _placements(raw.get('request'))
+ # Omitted: an action reply is ACK-only, an event reply payload-free.
+ reply = None
+ if 'reply' in raw:
+ reply = _placements(raw['reply'])
+ elif op_type == 'event':
+ reply = Placements()
+ ops.append(OpSpec(
+ name=name,
+ type=op_type,
+ request=request,
+ reply=reply,
+ fd_access=raw.get('fd-access', DEFAULT_FD_ACCESS),
+ availability=raw.get('availability', DEFAULT_AVAILABILITY),
+ optional=raw.get('optional', False),
+ pre_post=raw.get('pre-post', False),
+ suggested_reply_length=suggested,
+ ))
+ return tuple(ops)
+
+
+def _placements(raw):
+ if raw is None:
+ return Placements()
+ return Placements(_selectors(raw.get('attrs')),
+ _selectors(raw.get('optional-attrs')))
+
+
+def _selectors(raw):
+ if raw is None:
+ return ()
+ if isinstance(raw, str):
+ return (raw,)
+ return tuple(raw)
diff --git a/tools/ctlv/ctlvgen/lib/tree.py b/tools/ctlv/ctlvgen/lib/tree.py
new file mode 100644
index 000000000000..62f4fb363469
--- /dev/null
+++ b/tools/ctlv/ctlvgen/lib/tree.py
@@ -0,0 +1,71 @@
+# SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+"""Whole-tree registry, specification and ABI coverage checks."""
+
+from __future__ import annotations
+
+from . import compat
+from . import outputs
+from . import spec
+from .errors import CtlvError
+
+
+def _stems(directory, exclude=()):
+ return {path.stem for path in directory.glob('*.yaml')
+ if path.name not in exclude}
+
+
+def coverage_problems(paths, registry, snapshots=True,
+ allowed_missing_snapshots=()):
+ expected = {entry.name for entry in registry.entries}
+ problems = []
+
+ actual_specs = _stems(paths.spec_dir)
+ for name in sorted(expected - actual_specs):
+ problems.append(f'missing specification {name}.yaml')
+ for name in sorted(actual_specs - expected):
+ problems.append(f'unexpected specification {name}.yaml')
+
+ if snapshots:
+ actual_abi = _stems(paths.abi_dir,
+ exclude=(paths.registry_abi.name,))
+ missing = expected - actual_abi - set(allowed_missing_snapshots)
+ for name in sorted(missing):
+ problems.append(f'missing ABI snapshot {name}.yaml')
+ for name in sorted(actual_abi - expected):
+ problems.append(f'unexpected ABI snapshot {name}.yaml')
+ return problems
+
+
+def check(paths):
+ """Check all inputs whose expected set comes from the registry."""
+ registry = spec.load_registry(paths)
+ compat.registry_check(paths, registry)
+ problems = coverage_problems(paths, registry)
+ if problems:
+ raise CtlvError('CTLV tree', '\n'.join(problems))
+ outputs.load(paths)
+ return registry
+
+
+def check_for_update(paths):
+ """Validate a registry update without requiring new snapshots yet."""
+ registry = spec.load_registry(paths)
+ compat.registry_check(paths, registry, exact=False)
+ baseline = spec.load_registry(paths, paths.registry_abi)
+ appended = {entry.name
+ for entry in registry.entries[len(baseline.entries):]}
+ problems = coverage_problems(paths, registry,
+ allowed_missing_snapshots=appended)
+ if problems:
+ raise CtlvError('CTLV tree', '\n'.join(problems))
+ outputs.load(paths)
+ return registry
+
+
+def finish_update(paths):
+ """Require complete snapshots before advancing the registry baseline."""
+ registry = check_for_update(paths)
+ problems = coverage_problems(paths, registry)
+ if problems:
+ raise CtlvError('CTLV tree', '\n'.join(problems))
+ return registry
diff --git a/tools/ctlv/ctlvgen/lib/yamlload.py b/tools/ctlv/ctlvgen/lib/yamlload.py
new file mode 100644
index 000000000000..f043d87c9818
--- /dev/null
+++ b/tools/ctlv/ctlvgen/lib/yamlload.py
@@ -0,0 +1,83 @@
+# SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+"""Strict YAML subset loader.
+
+Anchors, aliases, merge keys, explicit tags, duplicate mapping keys and
+multiple documents per file are all rejected: they let two readers of a file
+that defines an ABI disagree about what it says.
+"""
+
+import yaml
+
+from .errors import CtlvError
+
+
+def _mark(path, mark):
+ if mark is None:
+ return str(path)
+ return f'{path}:{mark.line + 1}'
+
+
+class _StrictLoader(yaml.SafeLoader):
+ """SafeLoader without the features that make a document ambiguous."""
+
+ def __init__(self, stream, path):
+ super().__init__(stream)
+ self.ctlv_path = path
+
+ def _reject(self, mark, what):
+ raise CtlvError(_mark(self.ctlv_path, mark), what)
+
+ def compose_node(self, parent, index):
+ event = self.peek_event()
+ if isinstance(event, yaml.events.AliasEvent):
+ self._reject(event.start_mark, 'YAML aliases are not allowed')
+ if getattr(event, 'anchor', None) is not None:
+ self._reject(event.start_mark, 'YAML anchors are not allowed')
+ if _has_explicit_tag(event):
+ self._reject(event.start_mark,
+ f'explicit YAML tag "{event.tag}" is not allowed')
+ return super().compose_node(parent, index)
+
+ def construct_mapping(self, node, deep=False):
+ seen = set()
+ for key_node, _ in node.value:
+ if key_node.tag == 'tag:yaml.org,2002:merge':
+ self._reject(key_node.start_mark,
+ 'YAML merge keys are not allowed')
+ key = self.construct_object(key_node, deep=deep)
+ if key in seen:
+ self._reject(key_node.start_mark, f'duplicate key "{key}"')
+ seen.add(key)
+ return super().construct_mapping(node, deep=deep)
+
+
+def _has_explicit_tag(event):
+ if event.tag is None:
+ return False
+ implicit = getattr(event, 'implicit', False)
+ if isinstance(implicit, tuple):
+ return not (implicit[0] or implicit[1])
+ return not implicit
+
+
+def load(path):
+ """Return the single YAML document in path as plain Python objects."""
+ try:
+ with open(path, 'r', encoding='utf-8') as stream:
+ loader = _StrictLoader(stream, path)
+ try:
+ data = loader.get_single_data()
+ finally:
+ loader.dispose()
+ except OSError as exc:
+ raise CtlvError(str(path), f'cannot read: {exc.strerror}') from exc
+ except yaml.YAMLError as exc:
+ mark = getattr(exc, 'problem_mark', None)
+ described = [getattr(exc, 'context', None),
+ getattr(exc, 'problem', None)]
+ problem = ', '.join(part for part in described if part) or str(exc)
+ raise CtlvError(_mark(path, mark), problem) from exc
+
+ if not isinstance(data, dict):
+ raise CtlvError(str(path), 'expected a YAML mapping at the top level')
+ return data
diff --git a/tools/ctlv/pyproject.toml b/tools/ctlv/pyproject.toml
new file mode 100644
index 000000000000..7ecae2de3065
--- /dev/null
+++ b/tools/ctlv/pyproject.toml
@@ -0,0 +1,22 @@
+[build-system]
+requires = ["setuptools>=61.0"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "ctlvgen"
+authors = [
+ {name = "Jiri Pirko", email = "jiri@resnulli.us"},
+]
+description = "CTLV schema tool"
+version = "0.0.1"
+requires-python = ">=3.9"
+dependencies = [
+ "pyyaml==6.*",
+ "jsonschema==4.*"
+]
+
+[tool.setuptools.packages.find]
+include = ["ctlvgen", "ctlvgen.lib"]
+
+[project.scripts]
+ctlvgen = "ctlvgen.cli:main"
diff --git a/tools/ctlv/requirements.txt b/tools/ctlv/requirements.txt
new file mode 100644
index 000000000000..2a73e762f9b9
--- /dev/null
+++ b/tools/ctlv/requirements.txt
@@ -0,0 +1,2 @@
+pyyaml==6.*
+jsonschema==4.*
--
2.56.0
^ permalink raw reply related [flat|nested] 32+ messages in thread* Re: [PATCH RFC 01/20] ctlv: Add the schema language and generator
2026-10-15 14:50 ` [PATCH RFC 01/20] ctlv: Add the schema language and generator Jiri Pirko
@ 2026-10-15 14:58 ` sashiko-bot
0 siblings, 0 replies; 32+ messages in thread
From: sashiko-bot @ 2026-10-15 14:58 UTC (permalink / raw)
To: Jiri Pirko; +Cc: kvm
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] ctlvgen: ABI compatibility checker incorrectly flags widening to unlimited as an error
- [Low] ctlvgen: missing validation for --mode uapi --source causes KeyError
--
commit dbbe210aabdf036fa0f92c7b8932bca4fd127ce7
Author: Jiri Pirko <jiri@nvidia.com>
ctlv: Add the schema language and generator
Define the YAML schema and append-only family registry. Add ctlvgen to
validate specifications and emit kernel and UAPI outputs.
> diff --git a/tools/ctlv/ctlvgen/cli.py b/tools/ctlv/ctlvgen/cli.py
> new file mode 100755
> index 0000000000000..c6bfafeb28525
> --- /dev/null
> +++ b/tools/ctlv/ctlvgen/cli.py
[ ... ]
> @@ -0,0 +1,172 @@
[ ... ]
> + kinds = (args.header, args.source)
> + if args.mode and not any(kinds):
> + parser.error('--mode needs --header or --source')
> + if any(kinds) and not args.mode:
> + parser.error('--header and --source need --mode')
[Severity: Low]
Should the command-line parsing prevent the combination of --mode uapi
and --source here?
Right now, this combination passes validation, but the GENERATORS dictionary
only maps ('uapi', 'header'). If a user provides --mode uapi --source, it
will cause an unhandled KeyError later in _generate() when looking up the
output renderer.
> + if args.schema_only and args.mode != 'kernel':
> + parser.error('--schema-only needs --mode kernel')
[ ... ]
> + if args.mode == 'kernel':
> + renderer = (gen_kernel.render_header if args.header
> + else gen_kernel.render_source)
> + text = renderer(family, family_api=not args.schema_only)
> + else:
> + text = GENERATORS[(args.mode, kind)](family)
[ ... ]
> diff --git a/tools/ctlv/ctlvgen/lib/compat.py b/tools/ctlv/ctlvgen/lib/compat.py
> new file mode 100644
> index 0000000000000..4cf93af06ea90
> --- /dev/null
> +++ b/tools/ctlv/ctlvgen/lib/compat.py
[ ... ]
> @@ -0,0 +1,821 @@
[ ... ]
> +def _compare_occurrences(problems, where, was, now):
> + old_max = was['max-occurrences']
> + new_max = now['max-occurrences']
> + if old_max == new_max:
> + return
> + if old_max == spec.UNLIMITED or new_max < old_max:
[Severity: Medium]
Will this incorrectly flag a compatible widening as an ABI break?
Because spec.UNLIMITED is represented as 0, widening max-occurrences
from a finite value like 10 to unlimited means new_max is 0 and
old_max is 10. The condition new_max < old_max evaluates to true
(0 < 10), misidentifying the widened bound as a narrowed bound.
Does this check need to specifically ignore comparisons where new_max
is spec.UNLIMITED?
> + problems.append(f'{where}: max-occurrences was narrowed from '
> + f'{_occurrences(old_max)} to {_occurrences(new_max)}')
> +
> +
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261015145053.3664509-1-jiri@resnulli.us?part=1
^ permalink raw reply [flat|nested] 32+ messages in thread
* [PATCH RFC 02/20] ctlv: Add the UAPI wire format and family API
2026-10-15 14:50 [PATCH RFC 00/20] attestation: Add a common subsystem and TDX support Jiri Pirko
2026-10-15 14:50 ` [PATCH RFC 01/20] ctlv: Add the schema language and generator Jiri Pirko
@ 2026-10-15 14:50 ` Jiri Pirko
2026-10-15 14:50 ` [PATCH RFC 03/20] ctlv: Add the root family specification and generated files Jiri Pirko
` (17 subsequent siblings)
19 siblings, 0 replies; 32+ messages in thread
From: Jiri Pirko @ 2026-10-15 14:50 UTC (permalink / raw)
To: linux-coco
Cc: linux-kernel, linux-api, x86, kvm, linux-doc, jgg, david.quigley,
lukas, ksinyuk, rodrigo.vivi, cedric.xing, sameo, drdeeglaze,
James.Bottomley, aik, jpiotrowski, qinkun, pgonda, jic23,
alistair.francis, ankita, suzuki.poulose, kas, rick.p.edgecombe,
elena.reshetova, dave.hansen, sathyanarayanan.kuppuswamy,
peter.fang
From: Jiri Pirko <jiri@nvidia.com>
Define CTLV's common TLV transport, ioctl, event records, identifiers
and limits. Add the in-kernel API used by family implementations and
generated code.
Assisted-by: Cursor: GPT-5.6 Sol
Assisted-by: Cursor: Claude Opus 5.5
Signed-off-by: Jiri Pirko <jiri@nvidia.com>
---
.../userspace-api/ioctl/ioctl-number.rst | 2 +
MAINTAINERS | 2 +
include/linux/ctlv.h | 55 +++
include/linux/ctlv_family.h | 316 ++++++++++++++++++
include/uapi/linux/ctlv.h | 183 ++++++++++
5 files changed, 558 insertions(+)
create mode 100644 include/linux/ctlv.h
create mode 100644 include/linux/ctlv_family.h
create mode 100644 include/uapi/linux/ctlv.h
diff --git a/Documentation/userspace-api/ioctl/ioctl-number.rst b/Documentation/userspace-api/ioctl/ioctl-number.rst
index 2fc53093752d..e545669c773c 100644
--- a/Documentation/userspace-api/ioctl/ioctl-number.rst
+++ b/Documentation/userspace-api/ioctl/ioctl-number.rst
@@ -386,6 +386,8 @@ Code Seq# Include File Comments
<mailto:linux-hyperv@vger.kernel.org>
0xBA 00-0F uapi/linux/liveupdate.h Pasha Tatashin
<mailto:pasha.tatashin@soleen.com>
+0xBB 00-0F uapi/linux/ctlv.h CTLV
+ <mailto:jiri@resnulli.us>
0xC0 00-0F linux/usb/iowarrior.h
0xCA 00-0F uapi/misc/cxl.h Dead since 6.15
0xCA 10-2F uapi/misc/ocxl.h
diff --git a/MAINTAINERS b/MAINTAINERS
index a8508fbae37d..8ab0d6a7a2fd 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -7006,6 +7006,8 @@ M: Jiri Pirko <jiri@resnulli.us>
L: linux-kernel@vger.kernel.org
S: Maintained
F: Documentation/ctlv/
+F: include/linux/ctlv*.h
+F: include/uapi/linux/ctlv*.h
F: tools/ctlv/
CTU CAN FD DRIVER
diff --git a/include/linux/ctlv.h b/include/linux/ctlv.h
new file mode 100644
index 000000000000..addb893f4b8d
--- /dev/null
+++ b/include/linux/ctlv.h
@@ -0,0 +1,55 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES
+ *
+ * CTLV family API.
+ */
+#ifndef _LINUX_CTLV_H
+#define _LINUX_CTLV_H
+
+#include <linux/types.h>
+
+#include <uapi/linux/ctlv.h>
+
+struct ctlv_device;
+struct ctlv_family;
+struct ctlv_msg;
+struct ctlv_op_ctx;
+struct ctlv_schema_tree;
+
+/**
+ * struct ctlv_cursor - One attr position in one message.
+ * @msg: the message this position is in, core-private bookkeeping
+ * @attr: the attr itself, or NULL when a lookup found nothing
+ * @parent: reading, the nest containing @attr, NULL at the root
+ * @up: writing, cursor of the nest containing @attr, NULL at the root. The
+ * chain of them is the open-nest stack
+ * @start: writing, where @attr begins in the message. An append reaches what
+ * it lengthens through the buffer rather than through @attr, that buffer
+ * being the core's own
+ * @tree: schema tree the node belongs to, which is a fill's own tree for an
+ * attr that arrived through one
+ * @node: index of the node within @tree
+ * @depth: nesting levels above this position, the root being zero
+ * @index_nest_scope: schema scope used to read members of the physical nest
+ * @index_scope: core-private schema scope containing this attr
+ *
+ * Fixed-size, so family code can put a child cursor on the stack. The members
+ * belong to the core: a family reaches a message through what was generated
+ * for the op it answers. One direction at a time, a helper of the other
+ * direction refusing the cursor.
+ */
+struct ctlv_cursor {
+ struct ctlv_msg *msg;
+ const struct ctlv_attr *attr;
+ const struct ctlv_attr *parent;
+ struct ctlv_cursor *up;
+ size_t start;
+ const struct ctlv_schema_tree *tree;
+ u32 node;
+ u32 depth;
+ u32 index_nest_scope;
+ u32 index_scope;
+};
+
+#endif /* _LINUX_CTLV_H */
diff --git a/include/linux/ctlv_family.h b/include/linux/ctlv_family.h
new file mode 100644
index 000000000000..1ae4d3a536f7
--- /dev/null
+++ b/include/linux/ctlv_family.h
@@ -0,0 +1,316 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES
+ *
+ * CTLV family metadata and generated-code support.
+ */
+#ifndef _LINUX_CTLV_FAMILY_H
+#define _LINUX_CTLV_FAMILY_H
+
+#include <linux/ctlv.h>
+#include <linux/gfp_types.h>
+#include <linux/kobject.h>
+#include <linux/types.h>
+
+#include <uapi/linux/ctlv.h>
+
+/* op-schema-get needs one level to describe the declared schema. */
+#define CTLV_MAX_DECLARED_NEST_DEPTH (CTLV_MAX_NEST_DEPTH - 1)
+
+struct ctlv_node_schema {
+ u64 attr_id;
+ u8 kind;
+ u8 unit;
+ s8 scale;
+ bool reenters;
+ u32 max_occurrences;
+ u32 first_child;
+ u32 children_count;
+ u32 required_children_count;
+ union {
+ struct {
+ u64 min;
+ u64 max;
+ } range;
+ u64 flags_mask;
+ struct {
+ u32 min_len;
+ u32 max_len;
+ } length;
+ struct {
+ u64 max_len;
+ u32 alignment;
+ u16 backings;
+ u8 direction;
+ } blob;
+ };
+};
+
+#define CTLV_NODE_SCHEMA_UNLIMITED 0
+
+#define __CTLV_NODE_SCHEMA(_attr, _kind, _occurrences, ...) \
+ { .attr_id = (_attr), .kind = (_kind), \
+ .max_occurrences = (_occurrences), __VA_ARGS__ }
+
+#define __CTLV_NODE_SCHEMA_CHILDREN(_first, _count, _required) \
+ .first_child = (_first), .children_count = (_count), \
+ .required_children_count = (_required)
+
+#define CTLV_NODE_SCHEMA_ROOT(_first, _count, _required) \
+ __CTLV_NODE_SCHEMA(CTLV_ATTR_ROOT, CTLV_SCHEMA_NEST, 1, \
+ __CTLV_NODE_SCHEMA_CHILDREN(_first, _count, _required))
+
+#define CTLV_NODE_SCHEMA_NEST(_attr, _occurrences, _first, _count, _required) \
+ __CTLV_NODE_SCHEMA(_attr, CTLV_SCHEMA_NEST, _occurrences, \
+ __CTLV_NODE_SCHEMA_CHILDREN(_first, _count, _required))
+
+#define CTLV_NODE_SCHEMA_NEST_REENTERS(_attr, _occurrences, _first, _count, _required) \
+ __CTLV_NODE_SCHEMA(_attr, CTLV_SCHEMA_NEST, _occurrences, \
+ .reenters = true, \
+ __CTLV_NODE_SCHEMA_CHILDREN(_first, _count, _required))
+
+#define CTLV_NODE_SCHEMA_UINT(_attr, _occurrences, _min, _max) \
+ __CTLV_NODE_SCHEMA(_attr, CTLV_SCHEMA_UINT, _occurrences, \
+ .range = { .min = (_min), .max = (_max) })
+
+#define CTLV_NODE_SCHEMA_UINT_UNIT(_attr, _occurrences, _min, _max, _unit, _scale) \
+ __CTLV_NODE_SCHEMA(_attr, CTLV_SCHEMA_UINT, _occurrences, \
+ .range = { .min = (_min), .max = (_max) }, \
+ .unit = (_unit), .scale = (_scale))
+
+#define CTLV_NODE_SCHEMA_SINT(_attr, _occurrences, _min, _max) \
+ __CTLV_NODE_SCHEMA(_attr, CTLV_SCHEMA_SINT, _occurrences, \
+ .range = { .min = (_min), .max = (_max) })
+
+#define CTLV_NODE_SCHEMA_SINT_UNIT(_attr, _occurrences, _min, _max, _unit, _scale) \
+ __CTLV_NODE_SCHEMA(_attr, CTLV_SCHEMA_SINT, _occurrences, \
+ .range = { .min = (_min), .max = (_max) }, \
+ .unit = (_unit), .scale = (_scale))
+
+#define CTLV_NODE_SCHEMA_ENUM(_attr, _occurrences, _max) \
+ __CTLV_NODE_SCHEMA(_attr, CTLV_SCHEMA_ENUM, _occurrences, \
+ .range = { .min = 1, .max = (_max) })
+
+#define CTLV_NODE_SCHEMA_FLAGS(_attr, _occurrences, _mask) \
+ __CTLV_NODE_SCHEMA(_attr, CTLV_SCHEMA_FLAGS, _occurrences, \
+ .flags_mask = (_mask))
+
+#define CTLV_NODE_SCHEMA_BOOL(_attr, _occurrences) \
+ __CTLV_NODE_SCHEMA(_attr, CTLV_SCHEMA_BOOL, _occurrences)
+
+#define CTLV_NODE_SCHEMA_STRING(_attr, _occurrences, _min, _max) \
+ __CTLV_NODE_SCHEMA(_attr, CTLV_SCHEMA_STRING, _occurrences, \
+ .length = { .min_len = (_min), .max_len = (_max) })
+
+#define CTLV_NODE_SCHEMA_BINARY(_attr, _occurrences, _min, _max) \
+ __CTLV_NODE_SCHEMA(_attr, CTLV_SCHEMA_BINARY, _occurrences, \
+ .length = { .min_len = (_min), .max_len = (_max) })
+
+#define CTLV_NODE_SCHEMA_BLOB(_attr, _occurrences, _max, _alignment, _backings, _dir) \
+ __CTLV_NODE_SCHEMA(_attr, CTLV_SCHEMA_BLOB, _occurrences, \
+ .blob = { .max_len = (_max), \
+ .alignment = (_alignment), \
+ .backings = (_backings), \
+ .direction = (_dir) })
+
+struct ctlv_stub_schema {
+ u16 family_id;
+ u32 set_id;
+ u32 node;
+ bool optional;
+};
+
+#define __CTLV_STUB_SCHEMA(_family, _set, _node, _optional) \
+ { .family_id = (_family), .set_id = (_set), \
+ .node = (_node), .optional = (_optional) }
+
+#define CTLV_STUB_SCHEMA(_family, _set, _node) \
+ __CTLV_STUB_SCHEMA(_family, _set, _node, false)
+
+#define CTLV_STUB_SCHEMA_OPTIONAL(_family, _set, _node) \
+ __CTLV_STUB_SCHEMA(_family, _set, _node, true)
+
+struct ctlv_schema_tree {
+ const struct ctlv_node_schema *node_schemas;
+ u32 node_schemas_count;
+ const struct ctlv_stub_schema *stub_schemas;
+ u32 stub_schemas_count;
+};
+
+#define CTLV_SCHEMA_TREE(_name) { \
+ .node_schemas = _name##_node_schemas, \
+ .node_schemas_count = ARRAY_SIZE(_name##_node_schemas), \
+}
+
+#define CTLV_SCHEMA_TREE_STUBS(_name) { \
+ .node_schemas = _name##_node_schemas, \
+ .node_schemas_count = ARRAY_SIZE(_name##_node_schemas), \
+ .stub_schemas = _name##_stub_schemas, \
+ .stub_schemas_count = ARRAY_SIZE(_name##_stub_schemas), \
+}
+
+#define CTLV_SCHEMA_TREE_NONE() { }
+
+struct ctlv_stub_fill_schema {
+ u16 family_id;
+ u32 set_id;
+ struct ctlv_schema_tree members;
+};
+
+#define CTLV_STUB_FILL_SCHEMA(_family, _set, _name) { \
+ .family_id = (_family), \
+ .set_id = (_set), \
+ .members = CTLV_SCHEMA_TREE(_name##_fill), \
+}
+
+struct ctlv_op_sizes {
+ u32 request_max_len;
+ u32 action_reply_max_len;
+ u32 query_reply_min_len;
+ u32 query_reply_suggested_len;
+ u32 event_record_max_len;
+};
+
+struct ctlv_op_schema {
+ u64 op_id;
+ u8 type;
+ u8 fd_access;
+ u8 availability;
+ bool optional;
+ bool pre_post;
+ struct ctlv_schema_tree request;
+ struct ctlv_schema_tree reply;
+ struct ctlv_op_sizes sizes;
+};
+
+#define __CTLV_OP_SCHEMA(_op, _type, _availability, ...) \
+ { .op_id = (_op), .type = (_type), \
+ .availability = (_availability), __VA_ARGS__ }
+
+#define CTLV_OP_SCHEMA_ACTION(_op, _access, _availability, _request_len, _reply_len, \
+ _request, _reply, ...) \
+ __CTLV_OP_SCHEMA(_op, CTLV_OP_TYPE_ACTION, _availability, \
+ .fd_access = (_access), \
+ .request = _request, .reply = _reply, \
+ .sizes = { \
+ .request_max_len = (_request_len), \
+ .action_reply_max_len = (_reply_len), \
+ }, \
+ __VA_ARGS__)
+
+#define CTLV_OP_SCHEMA_QUERY(_op, _access, _availability, _request_len, _reply_min, \
+ _reply_suggested, _request, _reply, ...) \
+ __CTLV_OP_SCHEMA(_op, CTLV_OP_TYPE_QUERY, _availability, \
+ .fd_access = (_access), \
+ .request = _request, .reply = _reply, \
+ .sizes = { \
+ .request_max_len = (_request_len), \
+ .query_reply_min_len = (_reply_min), \
+ .query_reply_suggested_len = \
+ (_reply_suggested), \
+ }, \
+ __VA_ARGS__)
+
+#define CTLV_OP_SCHEMA_EVENT(_op, _record_len, _reply, ...) \
+ __CTLV_OP_SCHEMA(_op, CTLV_OP_TYPE_EVENT, \
+ CTLV_OP_AVAILABILITY_FIXED, \
+ .reply = _reply, \
+ .sizes = { \
+ .event_record_max_len = (_record_len), \
+ }, \
+ __VA_ARGS__)
+
+struct ctlv_family_schema {
+ const char *name;
+ u16 family_id;
+ const struct ctlv_op_schema *ops;
+ u32 ops_count;
+ const struct ctlv_stub_fill_schema *stub_fill_schemas;
+ u32 stub_fill_schemas_count;
+};
+
+/* Leave half of a uevent environment to the device model. */
+#define CTLV_UEVENT_MAX_LEN (UEVENT_BUFFER_SIZE / 2)
+#define CTLV_UEVENT_MAX_VARS (UEVENT_NUM_ENVP / 2)
+
+struct ctlv_input_ref {
+ const struct ctlv_schema_tree *tree;
+ u64 attr_id;
+ u32 node;
+ u32 slot;
+ u8 kind;
+};
+
+typedef int (*ctlv_action_fn)(void *priv, struct ctlv_op_ctx *ctx,
+ const struct ctlv_cursor *request,
+ struct ctlv_cursor *reply);
+typedef int (*ctlv_query_fn)(void *priv, struct ctlv_op_ctx *ctx,
+ const struct ctlv_cursor *request,
+ struct ctlv_cursor *reply);
+typedef int (*ctlv_op_pre_fn)(void *priv, struct ctlv_op_ctx *ctx);
+typedef void (*ctlv_op_post_fn)(void *priv, struct ctlv_op_ctx *ctx);
+
+struct ctlv_op {
+ union {
+ ctlv_action_fn action;
+ ctlv_query_fn query;
+ };
+ ctlv_op_pre_fn pre;
+ ctlv_op_post_fn post;
+};
+
+#define CTLV_OP_ACTION(_name) { .action = _name##_action }
+#define CTLV_OP_QUERY(_name) { .query = _name##_query }
+#define CTLV_OP_ACTION_PRE_POST(_name) \
+ { .action = _name##_action, .pre = _name##_pre, \
+ .post = _name##_post }
+#define CTLV_OP_QUERY_PRE_POST(_name) \
+ { .query = _name##_query, .pre = _name##_pre, \
+ .post = _name##_post }
+
+typedef size_t (*ctlv_family_priv_size_fn)(void);
+typedef const void *(*ctlv_family_pre_init_fn)(const void *init_info);
+typedef int (*ctlv_family_init_fn)(struct ctlv_device *ctlvdev,
+ const void *init_info, void *priv);
+typedef int (*ctlv_family_post_init_fn)(struct ctlv_device *ctlvdev,
+ void *priv);
+typedef void (*ctlv_family_pre_fini_fn)(struct ctlv_device *ctlvdev,
+ void *priv);
+typedef void (*ctlv_family_fini_fn)(struct ctlv_device *ctlvdev, void *priv);
+
+typedef int (*ctlv_stub_fill_fn)(void *priv, struct ctlv_op_ctx *ctx,
+ const struct ctlv_cursor *request,
+ struct ctlv_cursor *nest);
+
+struct ctlv_stub_ops {
+ u16 family_id;
+ u32 set_id;
+ ctlv_stub_fill_fn fill;
+};
+
+#define CTLV_STUB_OPS(_family, _set, _name) { \
+ .family_id = (_family), \
+ .set_id = (_set), \
+ .fill = _name##_fill, \
+}
+
+struct ctlv_family {
+ const struct ctlv_family_schema *schema;
+ const struct ctlv_family *parent;
+ const struct ctlv_op *ops;
+ u32 ops_count;
+ ctlv_op_pre_fn pre;
+ ctlv_op_post_fn post;
+ ctlv_family_priv_size_fn priv_size;
+ ctlv_family_pre_init_fn pre_init;
+ ctlv_family_init_fn init;
+ ctlv_family_post_init_fn post_init;
+ ctlv_family_pre_fini_fn pre_fini;
+ ctlv_family_fini_fn fini;
+ const struct ctlv_stub_ops *stub_ops;
+ u32 stub_ops_count;
+};
+
+extern const struct ctlv_family ctlv_family;
+
+#endif /* _LINUX_CTLV_FAMILY_H */
diff --git a/include/uapi/linux/ctlv.h b/include/uapi/linux/ctlv.h
new file mode 100644
index 000000000000..6bc0d1a296e6
--- /dev/null
+++ b/include/uapi/linux/ctlv.h
@@ -0,0 +1,183 @@
+/* SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause) */
+/*
+ * Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES
+ *
+ * CTLV transport UAPI: the TLV wire format, the op-execution ioctl, the event
+ * record header and transport limits. Schema-free; a family's constants
+ * are generated into <linux/<family>_gen.h>, the root family's into
+ * <linux/ctlv_gen.h>.
+ */
+#ifndef _UAPI_LINUX_CTLV_H
+#define _UAPI_LINUX_CTLV_H
+
+#include <linux/ioctl.h>
+#include <linux/types.h>
+
+/**
+ * struct ctlv_attr - Attr header.
+ * @len: this header plus its payload, excluding this attr's trailing padding
+ * @flags: CTLV_ATTR_F_* bits
+ * @attr_id: complete attr ID, or CTLV_ATTR_ROOT for a message root
+ *
+ * Attrs are aligned to CTLV_ALIGNTO, which is why @attr_id is __aligned_u64: a
+ * 32-bit ABI would otherwise align this structure to four bytes. A nested
+ * payload is the concatenation of CTLV_ALIGN(child.len) bytes per child, so
+ * child padding counts inside the containing attr's @len while a leaf's own
+ * padding does not. Scalar payloads are native-endian. Within each request
+ * root or nest, occurrences of one @attr_id are contiguous; different IDs
+ * otherwise have no required order.
+ */
+struct ctlv_attr {
+ __u32 len;
+ __u32 flags;
+ __aligned_u64 attr_id;
+};
+
+#define CTLV_ALIGNTO 8
+#define CTLV_ALIGN(_len) (((_len) + CTLV_ALIGNTO - 1) & ~(CTLV_ALIGNTO - 1))
+
+/* Set exactly on root and nested attrs, clear on scalar attrs. */
+#define CTLV_ATTR_F_NESTED (1U << 0)
+/* Query fragments: set on the root and each nest logically open at the end. */
+#define CTLV_ATTR_F_FRAG_MORE (1U << 1)
+/*
+ * A request attr, never the root, that a device whose schema does not declare
+ * its ID skips whole instead of refusing the message. An ID that schema does
+ * declare is held to it wherever it appears, so this asks to be forgiven what
+ * this kernel has never heard of and nothing else.
+ */
+#define CTLV_ATTR_F_IGNORE_IF_UNKNOWN (1U << 2)
+
+/* Root attr: carries CTLV_ATTR_F_NESTED, valid only at a message start. */
+#define CTLV_ATTR_ROOT 0ULL
+
+/* Complete attr: family in bits 63..48, group in 47..32, attr in 31..0. */
+#define CTLV_ATTR(_family, _group, _attr) \
+ (((__u64)(_family) << 48) | \
+ ((__u64)(_group) << 32) | \
+ (__u64)(_attr))
+
+#define CTLV_ATTR_FAMILY(_attr) ((__u16)((_attr) >> 48))
+#define CTLV_ATTR_GROUP(_attr) ((__u16)((_attr) >> 32))
+#define CTLV_ATTR_NUMBER(_attr) ((__u32)(_attr))
+
+/* Complete op: family in bits 63..48, zero in 47..32, op in 31..0. */
+#define CTLV_OP(_family, _op) \
+ (((__u64)(_family) << 48) | (__u64)(_op))
+
+#define CTLV_OP_FAMILY(_op) ((__u16)((_op) >> 48))
+#define CTLV_OP_NUMBER(_op) ((__u32)(_op))
+
+/**
+ * enum ctlv_unit - CTLV-defined unit of an integer attr payload.
+ * @CTLV_UNIT_NONE: dimensionless
+ * @CTLV_UNIT_SECONDS: seconds
+ * @CTLV_UNIT_HERTZ: hertz
+ * @CTLV_UNIT_BYTES: bytes
+ * @CTLV_UNIT_CELSIUS: degrees Celsius
+ *
+ * A payload is value * 10^scale of this unit, from the decimal scale the attr
+ * declares alongside. Changing either is an ABI break.
+ */
+enum ctlv_unit {
+ CTLV_UNIT_NONE = 0,
+ CTLV_UNIT_SECONDS,
+ CTLV_UNIT_HERTZ,
+ CTLV_UNIT_BYTES,
+ CTLV_UNIT_CELSIUS,
+};
+
+/**
+ * enum ctlv_schema_kind - CTLV-defined form of one attr payload.
+ * @CTLV_SCHEMA_UINT: unsigned integer payload bounded by a range
+ * @CTLV_SCHEMA_SINT: signed integer payload bounded by a range
+ * @CTLV_SCHEMA_BOOL: one payload byte holding zero or one
+ * @CTLV_SCHEMA_STRING: NUL-terminated string payload bounded by length
+ * @CTLV_SCHEMA_BINARY: opaque payload bounded by length
+ * @CTLV_SCHEMA_ENUM: unsigned integer payload restricted to a range
+ * @CTLV_SCHEMA_FLAGS: unsigned integer payload restricted to a mask
+ * @CTLV_SCHEMA_NEST: nested payload described by further nodes
+ * @CTLV_SCHEMA_BLOB: blob descriptor nest
+ *
+ * Exactly one form applies to an attr. These are the values op-schema-get
+ * reports, and the set is append-only.
+ */
+enum ctlv_schema_kind {
+ CTLV_SCHEMA_UINT,
+ CTLV_SCHEMA_SINT,
+ CTLV_SCHEMA_BOOL,
+ CTLV_SCHEMA_STRING,
+ CTLV_SCHEMA_BINARY,
+ CTLV_SCHEMA_ENUM,
+ CTLV_SCHEMA_FLAGS,
+ CTLV_SCHEMA_NEST,
+ CTLV_SCHEMA_BLOB,
+};
+
+/**
+ * struct ctlv_op_exec - Op-execution ioctl argument.
+ * @op_id: complete op ID; zero is invalid
+ * @request: struct ctlv_attr __user *, the mandatory complete request root
+ * @reply: struct ctlv_attr __user *, the success reply or the error
+ * information
+ * @request_len: readable bytes beginning at @request, at least one root header;
+ * the transport copy boundary, which may exceed the root's own length
+ * @reply_len: writable bytes beginning at @reply; input only, never
+ * overwritten by the kernel
+ * @generation: query only; zero on a first fragment, the previously returned
+ * value on continuation, and the query generation on output. Actions must
+ * set zero.
+ * @cursor: query only; zero starts a query and a nonzero value resumes it. On
+ * output it is the next cursor when the root carries
+ * CTLV_ATTR_F_FRAG_MORE and zero when no further fragment exists. Actions
+ * must set zero.
+ * @reserved: must be zero
+ *
+ * No size field: the ioctl number encodes the structure size and @reserved
+ * covers compatible growth. @request and @reply are fixed-width
+ * representations of userspace pointers so that the compat layout is identical.
+ */
+struct ctlv_op_exec {
+ __aligned_u64 op_id;
+ __aligned_u64 request;
+ __aligned_u64 reply;
+ __u32 request_len;
+ __u32 reply_len;
+ __aligned_u64 generation;
+ __aligned_u64 cursor;
+ __aligned_u64 reserved[2];
+};
+
+#define CTLV_IOC_MAGIC 0xBB
+
+#define CTLV_IOC_OP_EXEC _IOWR(CTLV_IOC_MAGIC, 0x00, struct ctlv_op_exec)
+
+/**
+ * struct ctlv_event_hdr - Header of one queued event record.
+ * @length: complete record length, this header plus the aligned root
+ * @flags: must be zero
+ * @op_id: complete op ID of the event op that produced the record
+ * @sequence: per open file description sequence number, assigned to every
+ * eligible enqueue attempt including dropped ones
+ *
+ * Exactly one root attr follows the header, and one read() returns one record.
+ */
+struct ctlv_event_hdr {
+ __u32 length;
+ __u32 flags;
+ __aligned_u64 op_id;
+ __aligned_u64 sequence;
+};
+
+/* Implementation limits, not wire-field maxima: raising them is compatible. */
+
+/* Request roots, action replies, query reply fragments and event roots. */
+#define CTLV_MAX_INLINE_MESSAGE_LEN 0x100000 /* 1 MiB */
+/* Includes the error information root. */
+#define CTLV_MAX_ERROR_LEN 0x4000 /* 16 KiB */
+/* The root attr is at depth zero. */
+#define CTLV_MAX_NEST_DEPTH 32
+#define CTLV_DEFAULT_QUERY_REPLY_LEN 0x10000 /* 64 KiB */
+#define CTLV_MAX_EVENT_QUEUE_LEN 0x1000000 /* 16 MiB */
+
+#endif /* _UAPI_LINUX_CTLV_H */
--
2.56.0
^ permalink raw reply related [flat|nested] 32+ messages in thread* [PATCH RFC 03/20] ctlv: Add the root family specification and generated files
2026-10-15 14:50 [PATCH RFC 00/20] attestation: Add a common subsystem and TDX support Jiri Pirko
2026-10-15 14:50 ` [PATCH RFC 01/20] ctlv: Add the schema language and generator Jiri Pirko
2026-10-15 14:50 ` [PATCH RFC 02/20] ctlv: Add the UAPI wire format and family API Jiri Pirko
@ 2026-10-15 14:50 ` Jiri Pirko
2026-10-15 14:50 ` [PATCH RFC 04/20] Documentation: ctlv: Add userspace ABI Jiri Pirko
` (16 subsequent siblings)
19 siblings, 0 replies; 32+ messages in thread
From: Jiri Pirko @ 2026-10-15 14:50 UTC (permalink / raw)
To: linux-coco
Cc: linux-kernel, linux-api, x86, kvm, linux-doc, jgg, david.quigley,
lukas, ksinyuk, rodrigo.vivi, cedric.xing, sameo, drdeeglaze,
James.Bottomley, aik, jpiotrowski, qinkun, pgonda, jic23,
alistair.francis, ankita, suzuki.poulose, kas, rick.p.edgecombe,
elena.reshetova, dave.hansen, sathyanarayanan.kuppuswamy,
peter.fang
From: Jiri Pirko <jiri@nvidia.com>
Define the root family used by every CTLV device. Add its ABI snapshot,
registry and manifest entries, and generated outputs.
The generated kernel descriptor is preparatory and is linked after its
handlers arrive with device and operation dispatch.
Assisted-by: Cursor: GPT-5.6 Sol
Assisted-by: Cursor: Claude Opus 5.5
Signed-off-by: Jiri Pirko <jiri@nvidia.com>
---
Documentation/ctlv/abi/ctlv.yaml | 186 +++++++++
Documentation/ctlv/abi/families.registry.yaml | 7 +
Documentation/ctlv/families.yaml | 13 +
Documentation/ctlv/generated-files.yaml | 17 +
Documentation/ctlv/specs/ctlv.yaml | 379 ++++++++++++++++++
drivers/ctlv/ctlv_gen.c | 229 +++++++++++
include/linux/ctlv_gen.h | 137 +++++++
include/uapi/linux/ctlv_families_gen.h | 15 +
include/uapi/linux/ctlv_gen.h | 271 +++++++++++++
9 files changed, 1254 insertions(+)
create mode 100644 Documentation/ctlv/abi/ctlv.yaml
create mode 100644 Documentation/ctlv/abi/families.registry.yaml
create mode 100644 Documentation/ctlv/families.yaml
create mode 100644 Documentation/ctlv/generated-files.yaml
create mode 100644 Documentation/ctlv/specs/ctlv.yaml
create mode 100644 drivers/ctlv/ctlv_gen.c
create mode 100644 include/linux/ctlv_gen.h
create mode 100644 include/uapi/linux/ctlv_families_gen.h
create mode 100644 include/uapi/linux/ctlv_gen.h
diff --git a/Documentation/ctlv/abi/ctlv.yaml b/Documentation/ctlv/abi/ctlv.yaml
new file mode 100644
index 000000000000..fa11bce98f46
--- /dev/null
+++ b/Documentation/ctlv/abi/ctlv.yaml
@@ -0,0 +1,186 @@
+# SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause)
+%YAML 1.2
+---
+# Checked-in ABI snapshot of the ctlv family, written by
+# "make -C tools/ctlv abi-update".
+#
+# The checker compares this family's declarations and inherited stub fills
+# against this file. Updating it is how a UAPI change gets reviewed; an update
+# that removes or renumbers anything, narrows a bound, or makes a placement
+# required is an ABI break, whatever this file says afterwards.
+family: {name: ctlv, id: 0}
+enums:
+ op-type: {action: 1, query: 2, event: 3}
+ op-availability: {fixed: 1, dynamic: 2}
+ blob-type: {va: 1}
+ blob-direction: {input: 1, output: 2, inout: 3}
+ validation-reason: {malformed-message: 1, unknown-attr: 2, unknown-flag: 3,
+ nesting-too-deep: 4, missing-attr: 5,
+ too-many-occurrences: 6, value-out-of-range: 7,
+ bad-length: 8, bad-string: 9, bad-blob-descriptor: 10,
+ stale-generation: 11, noncontiguous-attr: 12}
+ op-fd-access: {read: 1, read-write: 2}
+attr-groups:
+ family:
+ id: 1
+ attrs:
+ id: {id: 1, type: uint}
+ name: {id: 2, type: string, max-length: 63}
+ entry: {id: 3, type: nested, max-occurrences: unlimited}
+ blob:
+ id: 2
+ attrs:
+ type: {id: 1, enum: blob-type}
+ address: {id: 2, type: uint}
+ fd: {id: 3, type: uint, max: 2147483647}
+ offset: {id: 4, type: uint}
+ length: {id: 5, type: uint, min: 1}
+ error:
+ id: 3
+ attrs:
+ reason: {id: 1, enum: validation-reason}
+ message: {id: 2, type: string, max-length: 255}
+ bad-attr-offset: {id: 3, type: uint}
+ nest-offset: {id: 4, type: uint}
+ missing-attr-id: {id: 5, type: uint}
+ expected-generation: {id: 6, type: uint}
+ current-generation: {id: 7, type: uint}
+ op:
+ id: 4
+ attrs:
+ id: {id: 1, type: uint, min: 1}
+ type: {id: 2, enum: op-type}
+ availability: {id: 3, enum: op-availability}
+ disabled: {id: 4, type: bool}
+ request-max-len: {id: 5, type: uint}
+ action-reply-max-len: {id: 6, type: uint}
+ query-reply-min-len: {id: 7, type: uint}
+ query-reply-suggested-len: {id: 8, type: uint}
+ event-record-max-len: {id: 9, type: uint}
+ state-generation: {id: 10, type: uint}
+ entry: {id: 11, type: nested, max-occurrences: unlimited}
+ fd-access: {id: 12, enum: op-fd-access}
+ schema:
+ id: 5
+ attrs:
+ attr-id: {id: 1, type: uint}
+ kind: {id: 2, type: uint}
+ max-occurrences: {id: 3, type: uint}
+ min: {id: 4, type: uint}
+ max: {id: 5, type: uint}
+ min-length: {id: 6, type: uint}
+ max-length: {id: 7, type: uint}
+ flags-mask: {id: 8, type: uint}
+ unit: {id: 9, type: uint}
+ scale: {id: 10, type: sint}
+ blob-direction: {id: 11, enum: blob-direction}
+ blob-backings: {id: 12, type: uint}
+ blob-max-length: {id: 13, type: uint}
+ blob-alignment: {id: 14, type: uint}
+ reenters: {id: 15, type: uint, min: 1}
+ attrs: {id: 16, type: nested, max-occurrences: unlimited}
+ optional-attrs: {id: 17, type: nested, max-occurrences: unlimited}
+ request: {id: 18, type: nested}
+ reply: {id: 19, type: nested}
+ event:
+ id: 6
+ attrs:
+ op-id: {id: 1, type: uint, min: 1, max-occurrences: 256}
+ queue-len: {id: 2, type: uint, min: 1, max: 16777216, unit: bytes}
+ queue-used: {id: 3, type: uint, unit: bytes}
+ first-sequence: {id: 4, type: uint}
+ last-sequence: {id: 5, type: uint}
+ dropped-count: {id: 6, type: uint}
+attr-sets:
+ family-entry: {id: 1}
+ op-entry: {id: 2}
+ op-schema-request: {id: 3}
+ schema-node: {id: 4}
+ op-schema-info: {id: 5}
+ op-change-info: {id: 6}
+ error-info: {id: 7}
+ event-subscription: {id: 8}
+ event-queue-limit: {id: 9}
+ event-queue-state: {id: 10}
+ event-overflow-info: {id: 11}
+ops:
+ families-dump:
+ abi: {id: 1, type: query, fd-access: read}
+ reply:
+ attrs:
+ - family.entry: {attrs: [family.id, family.name]}
+ ops-dump:
+ abi: {id: 2, type: query, fd-access: read}
+ reply:
+ attrs:
+ - op.entry:
+ attrs: [op.id, op.type]
+ optional-attrs: [op.availability, op.disabled, op.request-max-len,
+ op.action-reply-max-len, op.query-reply-min-len,
+ op.query-reply-suggested-len,
+ op.event-record-max-len, op.fd-access]
+ op-schema-get:
+ abi: {id: 3, type: query, fd-access: read}
+ request: {attrs: [op.id]}
+ reply:
+ optional-attrs:
+ - schema.request:
+ attrs: [schema.attr-id, schema.kind, schema.max-occurrences]
+ optional-attrs:
+ - schema.min
+ - schema.max
+ - schema.min-length
+ - schema.max-length
+ - schema.flags-mask
+ - schema.unit
+ - schema.scale
+ - schema.blob-direction
+ - schema.blob-backings
+ - schema.blob-max-length
+ - schema.blob-alignment
+ - schema.reenters
+ - schema.attrs:
+ reenters: schema-node
+ - schema.optional-attrs:
+ reenters: schema-node
+ - schema.reply:
+ attrs: [schema.attr-id, schema.kind, schema.max-occurrences]
+ optional-attrs:
+ - schema.min
+ - schema.max
+ - schema.min-length
+ - schema.max-length
+ - schema.flags-mask
+ - schema.unit
+ - schema.scale
+ - schema.blob-direction
+ - schema.blob-backings
+ - schema.blob-max-length
+ - schema.blob-alignment
+ - schema.reenters
+ - schema.attrs:
+ reenters: schema-node
+ - schema.optional-attrs:
+ reenters: schema-node
+ subscriptions-add:
+ abi: {id: 4, type: action, fd-access: read}
+ request: {attrs: [event.op-id]}
+ subscriptions-remove:
+ abi: {id: 5, type: action, fd-access: read}
+ request: {attrs: [event.op-id]}
+ event-queue-get:
+ abi: {id: 6, type: query, fd-access: read}
+ reply: {attrs: [event.queue-len, event.queue-used]}
+ event-queue-set:
+ abi: {id: 7, type: action, fd-access: read}
+ request: {attrs: [event.queue-len]}
+ op-changed:
+ abi: {id: 8, type: event}
+ reply: {attrs: [op.id, op.disabled, op.state-generation]}
+ event-overflow:
+ abi: {id: 9, type: event}
+ reply:
+ attrs: [event.first-sequence, event.last-sequence, event.dropped-count]
+ device-removed:
+ abi: {id: 10, type: event}
+ reply: {}
diff --git a/Documentation/ctlv/abi/families.registry.yaml b/Documentation/ctlv/abi/families.registry.yaml
new file mode 100644
index 000000000000..2c9b9e7eee15
--- /dev/null
+++ b/Documentation/ctlv/abi/families.registry.yaml
@@ -0,0 +1,7 @@
+# SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause)
+%YAML 1.2
+---
+# Checked-in ABI baseline of the ordered CTLV family registry.
+# Updated deliberately by "make -C tools/ctlv abi-update".
+families:
+ - ctlv
diff --git a/Documentation/ctlv/families.yaml b/Documentation/ctlv/families.yaml
new file mode 100644
index 000000000000..bf1dabdba5c9
--- /dev/null
+++ b/Documentation/ctlv/families.yaml
@@ -0,0 +1,13 @@
+# SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause)
+%YAML 1.2
+---
+# Append-only registry of CTLV family names, indexed by family ID: the entry at
+# index N has family ID N, up to 65535. The root family owns ID 0 and heads the
+# list, and every other entry comes after the entry of the family it inherits.
+# An entry may be marked deprecated, but never reordered, removed, renamed or
+# reused, its position being its wire identity. Every entry must have a
+# specification in specs/<name>.yaml, and is added by the change that adds the
+# implementation. Fixtures under tools/ctlv/tests/ are not registered.
+
+families:
+ - ctlv
diff --git a/Documentation/ctlv/generated-files.yaml b/Documentation/ctlv/generated-files.yaml
new file mode 100644
index 000000000000..6773e0be0883
--- /dev/null
+++ b/Documentation/ctlv/generated-files.yaml
@@ -0,0 +1,17 @@
+# SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause)
+%YAML 1.2
+---
+# Generated files for registered families.
+version: 1
+registry:
+ source: Documentation/ctlv/families.yaml
+ outputs:
+ uapi-header: include/uapi/linux/ctlv_families_gen.h
+
+families:
+ - name: ctlv
+ source: Documentation/ctlv/specs/ctlv.yaml
+ outputs:
+ uapi-header: include/uapi/linux/ctlv_gen.h
+ kernel-header: include/linux/ctlv_gen.h
+ kernel-source: drivers/ctlv/ctlv_gen.c
diff --git a/Documentation/ctlv/specs/ctlv.yaml b/Documentation/ctlv/specs/ctlv.yaml
new file mode 100644
index 000000000000..d58f02747e0f
--- /dev/null
+++ b/Documentation/ctlv/specs/ctlv.yaml
@@ -0,0 +1,379 @@
+# SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause)
+%YAML 1.2
+---
+# The root CTLV family, family ID 0 and the first entry in families.yaml. It
+# inherits nothing, and every other family name begins with "ctlv-". Every CTLV
+# device answers these ops in addition to its own family's, and the attrs
+# declared here appear inside family messages: the blob group is the descriptor
+# nest of every family blob attr, the error group the error information written
+# on failure. Nothing here may be reordered or removed.
+
+family:
+ name: ctlv
+
+enums:
+ - name: op-type
+ values:
+ - action
+ - query
+ - event
+
+ # A permission, not a state: what a device currently allows is op.disabled.
+ - name: op-availability
+ values:
+ - fixed
+ - dynamic
+
+ - name: blob-type
+ values:
+ - va
+
+ - name: blob-direction
+ values:
+ - input
+ - output
+ - inout
+
+ - name: validation-reason
+ values:
+ - malformed-message
+ - unknown-attr
+ - unknown-flag
+ - nesting-too-deep
+ - missing-attr
+ - too-many-occurrences
+ - value-out-of-range
+ - bad-length
+ - bad-string
+ - bad-blob-descriptor
+ - stale-generation
+ - noncontiguous-attr
+
+ - name: op-fd-access
+ values:
+ - read
+ - read-write
+
+attr-groups:
+ # One record per family of the chain a device answers, as reported by
+ # families-dump.
+ - name: family
+ attrs:
+ - name: id
+ type: uint
+ # The registry name, also what a uevent carries. Bounded here, this being
+ # the only place a family name is on the wire.
+ - name: name
+ type: string
+ max-length: 63
+ - name: entry
+ attrs: family-entry
+ max-occurrences: unlimited
+
+ # The nest carried by every family blob attr. va uses address; fd and offset
+ # are reserved for later fd-backed types.
+ - name: blob
+ attrs:
+ - name: type
+ enum: blob-type
+ - name: address
+ type: uint
+ - name: fd
+ type: uint
+ max: 2147483647
+ - name: offset
+ type: uint
+ - name: length
+ type: uint
+ min: 1
+
+ # Error information: only what describes the first failure is present.
+ - name: error
+ attrs:
+ - name: reason
+ enum: validation-reason
+ - name: message
+ type: string
+ max-length: 255
+ - name: bad-attr-offset
+ type: uint
+ - name: nest-offset
+ type: uint
+ - name: missing-attr-id
+ type: uint
+ - name: expected-generation
+ type: uint
+ - name: current-generation
+ type: uint
+
+ # One entry per effective op, as reported by ops-dump. Length members that do
+ # not apply to an entry's op type are absent, as are fd access, availability
+ # and disabled for an event.
+ - name: op
+ attrs:
+ - name: id
+ type: uint
+ min: 1
+ - name: type
+ enum: op-type
+ - name: availability
+ enum: op-availability
+ - name: disabled
+ type: bool
+ - name: request-max-len
+ type: uint
+ - name: action-reply-max-len
+ type: uint
+ - name: query-reply-min-len
+ type: uint
+ - name: query-reply-suggested-len
+ type: uint
+ - name: event-record-max-len
+ type: uint
+ # Advances once per op state change on one device.
+ - name: state-generation
+ type: uint
+ - name: entry
+ attrs: op-entry
+ max-occurrences: unlimited
+ - name: fd-access
+ enum: op-fd-access
+
+ # One node of an op's request or reply tree, as reported by op-schema-get. A
+ # node describes a node, so attrs and optional-attrs hold these same members
+ # one level further in. Nothing here is a name: a caller matches the family
+ # chain from families-dump to find the generated UAPI.
+ - name: schema
+ attrs:
+ # Zero in a message root node, as on the wire.
+ - name: attr-id
+ type: uint
+ # enum ctlv_schema_kind.
+ - name: kind
+ type: uint
+ # Zero where the placement may repeat as often as the message has room.
+ - name: max-occurrences
+ type: uint
+ # Inclusive bounds of an integer or enum payload, raw 64-bit values read
+ # according to kind. An enum's values are numbered without gaps.
+ - name: min
+ type: uint
+ - name: max
+ type: uint
+ # Payload length bounds of a string or binary attr, a string's
+ # terminator excluded.
+ - name: min-length
+ type: uint
+ - name: max-length
+ type: uint
+ - name: flags-mask
+ type: uint
+ # enum ctlv_unit, absent when the payload is dimensionless, and a scale
+ # only ever accompanies a unit.
+ - name: unit
+ type: uint
+ - name: scale
+ type: sint
+ - name: blob-direction
+ enum: blob-direction
+ # Mask of BIT(enum ctlv_blob_type): the backings this placement takes.
+ - name: blob-backings
+ type: uint
+ # Maximum external payload length, unrelated to the inline message limit.
+ - name: blob-max-length
+ type: uint
+ - name: blob-alignment
+ type: uint
+ # Levels to climb to the node whose members are this one's as well, one
+ # meaning the node this one is a member of. A node that carries it lists
+ # no members itself.
+ - name: reenters
+ type: uint
+ min: 1
+ # The nodes one level in; required and optional are different attrs.
+ - name: attrs
+ attrs: schema-node
+ recursive: true
+ max-occurrences: unlimited
+ - name: optional-attrs
+ attrs: schema-node
+ recursive: true
+ max-occurrences: unlimited
+ # The root of each of an op's two trees, a node like any other.
+ - name: request
+ attrs: schema-node
+ - name: reply
+ attrs: schema-node
+
+ - name: event
+ attrs:
+ - name: op-id
+ type: uint
+ min: 1
+ max-occurrences: 256
+ - name: queue-len
+ type: uint
+ unit: bytes
+ min: 1
+ max: 16777216
+ - name: queue-used
+ type: uint
+ unit: bytes
+ - name: first-sequence
+ type: uint
+ - name: last-sequence
+ type: uint
+ - name: dropped-count
+ type: uint
+
+attr-sets:
+ - name: family-entry
+ attrs:
+ - family.id
+ - family.name
+
+ - name: op-entry
+ attrs:
+ - op.id
+ - op.type
+ optional-attrs:
+ - op.availability
+ - op.disabled
+ - op.request-max-len
+ - op.action-reply-max-len
+ - op.query-reply-min-len
+ - op.query-reply-suggested-len
+ - op.event-record-max-len
+ - op.fd-access
+
+ - name: op-schema-request
+ attrs: op.id
+
+ # Only the members the node's form has are present.
+ - name: schema-node
+ attrs:
+ - schema.attr-id
+ - schema.kind
+ - schema.max-occurrences
+ optional-attrs:
+ - schema.min
+ - schema.max
+ - schema.min-length
+ - schema.max-length
+ - schema.flags-mask
+ - schema.unit
+ - schema.scale
+ - schema.blob-direction
+ - schema.blob-backings
+ - schema.blob-max-length
+ - schema.blob-alignment
+ - schema.reenters
+ - schema.attrs
+ - schema.optional-attrs
+
+ # The two trees of one op, each absent when the op has no such message. An
+ # event's one tree is reported as the reply.
+ - name: op-schema-info
+ optional-attrs:
+ - schema.request
+ - schema.reply
+
+ # What changed about one op, in the same attrs a dump entry uses.
+ - name: op-change-info
+ attrs:
+ - op.id
+ - op.disabled
+ - op.state-generation
+
+ # The root of an error information message.
+ - name: error-info
+ optional-attrs:
+ - error.reason
+ - error.message
+ - error.bad-attr-offset
+ - error.nest-offset
+ - error.missing-attr-id
+ - error.expected-generation
+ - error.current-generation
+
+ - name: event-subscription
+ attrs: event.op-id
+
+ - name: event-queue-limit
+ attrs: event.queue-len
+
+ - name: event-queue-state
+ attrs:
+ - event.queue-len
+ - event.queue-used
+
+ - name: event-overflow-info
+ attrs:
+ - event.first-sequence
+ - event.last-sequence
+ - event.dropped-count
+
+ops:
+ # The families this device answers, this one first and the device family
+ # last. The only names any reply of this family carries.
+ - name: families-dump
+ type: query
+ fd-access: read
+ reply:
+ attrs: family.entry
+
+ - name: ops-dump
+ type: query
+ fd-access: read
+ reply:
+ attrs: op.entry
+
+ # The request and reply ABI of one op, whether or not it can be called right
+ # now. A removed op answers as one that was never declared.
+ - name: op-schema-get
+ type: query
+ fd-access: read
+ request:
+ attrs: op-schema-request
+ reply:
+ attrs: op-schema-info
+
+ - name: subscriptions-add
+ type: action
+ fd-access: read
+ request:
+ attrs: event-subscription
+
+ - name: subscriptions-remove
+ type: action
+ fd-access: read
+ request:
+ attrs: event-subscription
+
+ - name: event-queue-get
+ type: query
+ fd-access: read
+ reply:
+ attrs: event-queue-state
+
+ - name: event-queue-set
+ type: action
+ fd-access: read
+ request:
+ attrs: event-queue-limit
+
+ # A dynamically available op was enabled or disabled. The name is not about
+ # availability: a later mutable op property reports its state here too.
+ - name: op-changed
+ type: event
+ reply:
+ attrs: op-change-info
+
+ - name: event-overflow
+ type: event
+ reply:
+ attrs: event-overflow-info
+
+ # Payload-free: the record itself is the whole indication.
+ - name: device-removed
+ type: event
diff --git a/drivers/ctlv/ctlv_gen.c b/drivers/ctlv/ctlv_gen.c
new file mode 100644
index 000000000000..daf54495658f
--- /dev/null
+++ b/drivers/ctlv/ctlv_gen.c
@@ -0,0 +1,229 @@
+// SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause)
+/* Do not edit, regenerate with tools/ctlv/ctlv-regen.sh, from: */
+/* Documentation/ctlv/specs/ctlv.yaml */
+/* CTLV-GEN kernel source */
+
+#define _CTLV_GENERATED_METADATA_ONLY
+#include <linux/array_size.h>
+#include <linux/build_bug.h>
+#include <linux/ctlv_family.h>
+#include <linux/export.h>
+#include <linux/limits.h>
+
+#include <uapi/linux/ctlv_gen.h>
+
+#include <linux/ctlv_gen.h>
+
+static const struct ctlv_node_schema families_dump_request_node_schemas[] = {
+ CTLV_NODE_SCHEMA_ROOT(0, 0, 0),
+};
+
+static const struct ctlv_node_schema families_dump_reply_node_schemas[] = {
+ CTLV_NODE_SCHEMA_ROOT(1, 1, 1),
+ CTLV_NODE_SCHEMA_NEST(CTLV_ATTR_FAMILY_ENTRY, CTLV_NODE_SCHEMA_UNLIMITED, 2, 2, 2),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTR_FAMILY_ID, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_STRING(CTLV_ATTR_FAMILY_NAME, 1, 0, 63),
+};
+
+static const struct ctlv_node_schema ops_dump_request_node_schemas[] = {
+ CTLV_NODE_SCHEMA_ROOT(0, 0, 0),
+};
+
+static const struct ctlv_node_schema ops_dump_reply_node_schemas[] = {
+ CTLV_NODE_SCHEMA_ROOT(1, 1, 1),
+ CTLV_NODE_SCHEMA_NEST(CTLV_ATTR_OP_ENTRY, CTLV_NODE_SCHEMA_UNLIMITED, 2, 10, 2),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTR_OP_ID, 1, 1, U64_MAX),
+ CTLV_NODE_SCHEMA_ENUM(CTLV_ATTR_OP_TYPE, 1, 3),
+ CTLV_NODE_SCHEMA_ENUM(CTLV_ATTR_OP_AVAILABILITY, 1, 2),
+ CTLV_NODE_SCHEMA_BOOL(CTLV_ATTR_OP_DISABLED, 1),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTR_OP_REQUEST_MAX_LEN, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTR_OP_ACTION_REPLY_MAX_LEN, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTR_OP_QUERY_REPLY_MIN_LEN, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTR_OP_QUERY_REPLY_SUGGESTED_LEN, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTR_OP_EVENT_RECORD_MAX_LEN, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_ENUM(CTLV_ATTR_OP_FD_ACCESS, 1, 2),
+};
+
+static const struct ctlv_node_schema op_schema_get_request_node_schemas[] = {
+ CTLV_NODE_SCHEMA_ROOT(1, 1, 1),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTR_OP_ID, 1, 1, U64_MAX),
+};
+
+static const struct ctlv_node_schema op_schema_get_reply_node_schemas[] = {
+ CTLV_NODE_SCHEMA_ROOT(1, 2, 0),
+ CTLV_NODE_SCHEMA_NEST(CTLV_ATTR_SCHEMA_REQUEST, 1, 3, 17, 3),
+ CTLV_NODE_SCHEMA_NEST(CTLV_ATTR_SCHEMA_REPLY, 1, 20, 17, 3),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTR_SCHEMA_ATTR_ID, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTR_SCHEMA_KIND, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTR_SCHEMA_MAX_OCCURRENCES, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTR_SCHEMA_MIN, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTR_SCHEMA_MAX, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTR_SCHEMA_MIN_LENGTH, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTR_SCHEMA_MAX_LENGTH, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTR_SCHEMA_FLAGS_MASK, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTR_SCHEMA_UNIT, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_SINT(CTLV_ATTR_SCHEMA_SCALE, 1, S64_MIN, S64_MAX),
+ CTLV_NODE_SCHEMA_ENUM(CTLV_ATTR_SCHEMA_BLOB_DIRECTION, 1, 3),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTR_SCHEMA_BLOB_BACKINGS, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTR_SCHEMA_BLOB_MAX_LENGTH, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTR_SCHEMA_BLOB_ALIGNMENT, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTR_SCHEMA_REENTERS, 1, 1, U64_MAX),
+ CTLV_NODE_SCHEMA_NEST_REENTERS(CTLV_ATTR_SCHEMA_ATTRS, CTLV_NODE_SCHEMA_UNLIMITED, 3, 17,
+ 3),
+ CTLV_NODE_SCHEMA_NEST_REENTERS(CTLV_ATTR_SCHEMA_OPTIONAL_ATTRS, CTLV_NODE_SCHEMA_UNLIMITED,
+ 3, 17, 3),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTR_SCHEMA_ATTR_ID, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTR_SCHEMA_KIND, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTR_SCHEMA_MAX_OCCURRENCES, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTR_SCHEMA_MIN, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTR_SCHEMA_MAX, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTR_SCHEMA_MIN_LENGTH, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTR_SCHEMA_MAX_LENGTH, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTR_SCHEMA_FLAGS_MASK, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTR_SCHEMA_UNIT, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_SINT(CTLV_ATTR_SCHEMA_SCALE, 1, S64_MIN, S64_MAX),
+ CTLV_NODE_SCHEMA_ENUM(CTLV_ATTR_SCHEMA_BLOB_DIRECTION, 1, 3),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTR_SCHEMA_BLOB_BACKINGS, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTR_SCHEMA_BLOB_MAX_LENGTH, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTR_SCHEMA_BLOB_ALIGNMENT, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTR_SCHEMA_REENTERS, 1, 1, U64_MAX),
+ CTLV_NODE_SCHEMA_NEST_REENTERS(CTLV_ATTR_SCHEMA_ATTRS, CTLV_NODE_SCHEMA_UNLIMITED, 20, 17,
+ 3),
+ CTLV_NODE_SCHEMA_NEST_REENTERS(CTLV_ATTR_SCHEMA_OPTIONAL_ATTRS, CTLV_NODE_SCHEMA_UNLIMITED,
+ 20, 17, 3),
+};
+
+static const struct ctlv_node_schema subscriptions_add_request_node_schemas[] = {
+ CTLV_NODE_SCHEMA_ROOT(1, 1, 1),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTR_EVENT_OP_ID, 256, 1, U64_MAX),
+};
+
+static const struct ctlv_node_schema subscriptions_remove_request_node_schemas[] = {
+ CTLV_NODE_SCHEMA_ROOT(1, 1, 1),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTR_EVENT_OP_ID, 256, 1, U64_MAX),
+};
+
+static const struct ctlv_node_schema event_queue_get_request_node_schemas[] = {
+ CTLV_NODE_SCHEMA_ROOT(0, 0, 0),
+};
+
+static const struct ctlv_node_schema event_queue_get_reply_node_schemas[] = {
+ CTLV_NODE_SCHEMA_ROOT(1, 2, 2),
+ CTLV_NODE_SCHEMA_UINT_UNIT(CTLV_ATTR_EVENT_QUEUE_LEN, 1, 1, 16777216, CTLV_UNIT_BYTES, 0),
+ CTLV_NODE_SCHEMA_UINT_UNIT(CTLV_ATTR_EVENT_QUEUE_USED, 1, 0, U64_MAX, CTLV_UNIT_BYTES, 0),
+};
+
+static const struct ctlv_node_schema event_queue_set_request_node_schemas[] = {
+ CTLV_NODE_SCHEMA_ROOT(1, 1, 1),
+ CTLV_NODE_SCHEMA_UINT_UNIT(CTLV_ATTR_EVENT_QUEUE_LEN, 1, 1, 16777216, CTLV_UNIT_BYTES, 0),
+};
+
+static const struct ctlv_node_schema op_changed_reply_node_schemas[] = {
+ CTLV_NODE_SCHEMA_ROOT(1, 3, 3),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTR_OP_ID, 1, 1, U64_MAX),
+ CTLV_NODE_SCHEMA_BOOL(CTLV_ATTR_OP_DISABLED, 1),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTR_OP_STATE_GENERATION, 1, 0, U64_MAX),
+};
+
+static const struct ctlv_node_schema event_overflow_reply_node_schemas[] = {
+ CTLV_NODE_SCHEMA_ROOT(1, 3, 3),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTR_EVENT_FIRST_SEQUENCE, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTR_EVENT_LAST_SEQUENCE, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTR_EVENT_DROPPED_COUNT, 1, 0, U64_MAX),
+};
+
+static const struct ctlv_node_schema device_removed_reply_node_schemas[] = {
+ CTLV_NODE_SCHEMA_ROOT(0, 0, 0),
+};
+
+static const struct ctlv_node_schema error_info_node_schemas[] = {
+ CTLV_NODE_SCHEMA_ROOT(1, 7, 0),
+ CTLV_NODE_SCHEMA_ENUM(CTLV_ATTR_ERROR_REASON, 1, 12),
+ CTLV_NODE_SCHEMA_STRING(CTLV_ATTR_ERROR_MESSAGE, 1, 0, 255),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTR_ERROR_BAD_ATTR_OFFSET, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTR_ERROR_NEST_OFFSET, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTR_ERROR_MISSING_ATTR_ID, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTR_ERROR_EXPECTED_GENERATION, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTR_ERROR_CURRENT_GENERATION, 1, 0, U64_MAX),
+};
+
+const struct ctlv_schema_tree ctlv_error_info_schema = {
+ .node_schemas = error_info_node_schemas,
+ .node_schemas_count =
+ ARRAY_SIZE(error_info_node_schemas),
+};
+
+static_assert(CTLV_ERROR_INFO_MAX_LEN <= CTLV_MAX_ERROR_LEN);
+
+/* The ops this family declares, in declaration order. */
+const struct ctlv_op_schema ctlv_op_schemas[] = {
+ CTLV_OP_SCHEMA_QUERY(CTLV_OP_FAMILIES_DUMP, CTLV_OP_FD_ACCESS_READ,
+ CTLV_OP_AVAILABILITY_FIXED, 16, 112, 65536,
+ CTLV_SCHEMA_TREE(families_dump_request),
+ CTLV_SCHEMA_TREE(families_dump_reply)),
+ CTLV_OP_SCHEMA_QUERY(CTLV_OP_OPS_DUMP, CTLV_OP_FD_ACCESS_READ, CTLV_OP_AVAILABILITY_FIXED,
+ 16, 56, 65536, CTLV_SCHEMA_TREE(ops_dump_request),
+ CTLV_SCHEMA_TREE(ops_dump_reply)),
+ CTLV_OP_SCHEMA_QUERY(CTLV_OP_OP_SCHEMA_GET, CTLV_OP_FD_ACCESS_READ,
+ CTLV_OP_AVAILABILITY_FIXED, 40, 552, 65536,
+ CTLV_SCHEMA_TREE(op_schema_get_request),
+ CTLV_SCHEMA_TREE(op_schema_get_reply)),
+ CTLV_OP_SCHEMA_ACTION(CTLV_OP_SUBSCRIPTIONS_ADD, CTLV_OP_FD_ACCESS_READ,
+ CTLV_OP_AVAILABILITY_FIXED, 6160, 0,
+ CTLV_SCHEMA_TREE(subscriptions_add_request), CTLV_SCHEMA_TREE_NONE()),
+ CTLV_OP_SCHEMA_ACTION(CTLV_OP_SUBSCRIPTIONS_REMOVE, CTLV_OP_FD_ACCESS_READ,
+ CTLV_OP_AVAILABILITY_FIXED, 6160, 0,
+ CTLV_SCHEMA_TREE(subscriptions_remove_request),
+ CTLV_SCHEMA_TREE_NONE()),
+ CTLV_OP_SCHEMA_QUERY(CTLV_OP_EVENT_QUEUE_GET, CTLV_OP_FD_ACCESS_READ,
+ CTLV_OP_AVAILABILITY_FIXED, 16, 40, 65536,
+ CTLV_SCHEMA_TREE(event_queue_get_request),
+ CTLV_SCHEMA_TREE(event_queue_get_reply)),
+ CTLV_OP_SCHEMA_ACTION(CTLV_OP_EVENT_QUEUE_SET, CTLV_OP_FD_ACCESS_READ,
+ CTLV_OP_AVAILABILITY_FIXED, 40, 0,
+ CTLV_SCHEMA_TREE(event_queue_set_request), CTLV_SCHEMA_TREE_NONE()),
+ CTLV_OP_SCHEMA_EVENT(CTLV_OP_OP_CHANGED, 112, CTLV_SCHEMA_TREE(op_changed_reply)),
+ CTLV_OP_SCHEMA_EVENT(CTLV_OP_EVENT_OVERFLOW, 112, CTLV_SCHEMA_TREE(event_overflow_reply)),
+ CTLV_OP_SCHEMA_EVENT(CTLV_OP_DEVICE_REMOVED, 40, CTLV_SCHEMA_TREE(device_removed_reply)),
+};
+
+/* The family ID is its position in the central registry. */
+const struct ctlv_family_schema ctlv_schema = {
+ .name = "ctlv",
+ .family_id = 0,
+ .ops = ctlv_op_schemas,
+ .ops_count = ARRAY_SIZE(ctlv_op_schemas),
+};
+
+/* Maximum uevent cost for this family chain. */
+static_assert(60 <= CTLV_UEVENT_MAX_LEN);
+static_assert(3 <= CTLV_UEVENT_MAX_VARS);
+
+/* Guards for generated maxima. */
+static_assert(65536 <= CTLV_MAX_INLINE_MESSAGE_LEN);
+static_assert(112 <= CTLV_MAX_EVENT_QUEUE_LEN);
+static_assert(2 <= CTLV_MAX_DECLARED_NEST_DEPTH);
+
+/* Local action and query bindings in declaration order. */
+static const struct ctlv_op ctlv_ops[] = {
+ CTLV_OP_QUERY(ctlv_families_dump),
+ CTLV_OP_QUERY(ctlv_ops_dump),
+ CTLV_OP_QUERY(ctlv_op_schema_get),
+ CTLV_OP_ACTION(ctlv_subscriptions_add),
+ CTLV_OP_ACTION(ctlv_subscriptions_remove),
+ CTLV_OP_QUERY(ctlv_event_queue_get),
+ CTLV_OP_ACTION(ctlv_event_queue_set),
+};
+
+/* Root-family descriptor used by the CTLV core. */
+const struct ctlv_family ctlv_family = {
+ .schema = &ctlv_schema,
+ .ops = ctlv_ops,
+ .ops_count = ARRAY_SIZE(ctlv_ops),
+ .pre = ctlv_op_pre,
+ .post = ctlv_op_post,
+ .priv_size = ctlv_priv_size,
+ .init = ctlv_init,
+ .fini = ctlv_fini,
+};
+EXPORT_SYMBOL_NS_GPL(ctlv_family, "CTLV");
diff --git a/include/linux/ctlv_gen.h b/include/linux/ctlv_gen.h
new file mode 100644
index 000000000000..e389803449a0
--- /dev/null
+++ b/include/linux/ctlv_gen.h
@@ -0,0 +1,137 @@
+/* SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause) */
+/* Do not edit, regenerate with tools/ctlv/ctlv-regen.sh, from: */
+/* Documentation/ctlv/specs/ctlv.yaml */
+/* CTLV-GEN kernel header */
+
+/* Kernel metadata and family API of the ctlv CTLV family. */
+
+#ifndef _LINUX_CTLV_GEN_H
+#define _LINUX_CTLV_GEN_H
+
+#include <linux/ctlv_family.h>
+#include <linux/err.h>
+#include <linux/errno.h>
+
+#include <uapi/linux/ctlv_gen.h>
+
+/* Ops this family declares, in declaration order. */
+extern const struct ctlv_op_schema ctlv_op_schemas[];
+
+extern const struct ctlv_family_schema ctlv_schema;
+
+/* The error-info message, built by the core. */
+#define CTLV_ERROR_INFO_MAX_LEN 432
+
+extern const struct ctlv_schema_tree ctlv_error_info_schema;
+
+/* Implemented by the CTLV core, one per action and query it declares. */
+int ctlv_families_dump_query(void *priv, struct ctlv_op_ctx *ctx, const struct ctlv_cursor *request,
+ struct ctlv_cursor *reply);
+int ctlv_ops_dump_query(void *priv, struct ctlv_op_ctx *ctx, const struct ctlv_cursor *request,
+ struct ctlv_cursor *reply);
+int ctlv_op_schema_get_query(void *priv, struct ctlv_op_ctx *ctx, const struct ctlv_cursor *request,
+ struct ctlv_cursor *reply);
+int ctlv_subscriptions_add_action(void *priv, struct ctlv_op_ctx *ctx,
+ const struct ctlv_cursor *request, struct ctlv_cursor *reply);
+int ctlv_subscriptions_remove_action(void *priv, struct ctlv_op_ctx *ctx,
+ const struct ctlv_cursor *request, struct ctlv_cursor *reply);
+int ctlv_event_queue_get_query(void *priv, struct ctlv_op_ctx *ctx,
+ const struct ctlv_cursor *request, struct ctlv_cursor *reply);
+int ctlv_event_queue_set_action(void *priv, struct ctlv_op_ctx *ctx,
+ const struct ctlv_cursor *request, struct ctlv_cursor *reply);
+
+/* Root-family hooks around every core action or query. */
+int ctlv_op_pre(void *priv, struct ctlv_op_ctx *ctx);
+void ctlv_op_post(void *priv, struct ctlv_op_ctx *ctx);
+
+/* Core-allocated family priv lifecycle. */
+size_t ctlv_priv_size(void);
+int ctlv_init(struct ctlv_device *ctlvdev, const void *init_info, void *priv);
+void ctlv_fini(struct ctlv_device *ctlvdev, void *priv);
+
+extern const struct ctlv_family ctlv_family;
+
+#ifndef _CTLV_GENERATED_METADATA_ONLY
+
+/* Reading a validated message. A blob has a resolver instead of a getter. */
+CTLV_IN_REF(ctlv_op_schema_get_op_id, &ctlv_op_schemas[2].request, CTLV_ATTR_OP_ID, 0, 0,
+ CTLV_SCHEMA_UINT)
+CTLV_IN_HAS(ctlv_op_schema_get_op_id)
+CTLV_IN_U64(ctlv_op_schema_get_op_id)
+CTLV_IN_REF(ctlv_subscriptions_add_event_op_id, &ctlv_op_schemas[3].request, CTLV_ATTR_EVENT_OP_ID,
+ 0, 0, CTLV_SCHEMA_UINT)
+#define ctlv_subscriptions_add_event_op_id_for_each(_attr, _nest) \
+ CTLV_IN_FOR_EACH(ctlv_subscriptions_add_event_op_id, _attr, _nest)
+CTLV_IN_EACH_U64(ctlv_subscriptions_add_event_op_id)
+CTLV_IN_REF(ctlv_subscriptions_remove_event_op_id, &ctlv_op_schemas[4].request,
+ CTLV_ATTR_EVENT_OP_ID, 0, 0, CTLV_SCHEMA_UINT)
+#define ctlv_subscriptions_remove_event_op_id_for_each(_attr, _nest) \
+ CTLV_IN_FOR_EACH(ctlv_subscriptions_remove_event_op_id, _attr, _nest)
+CTLV_IN_EACH_U64(ctlv_subscriptions_remove_event_op_id)
+CTLV_IN_REF(ctlv_event_queue_set_event_queue_len, &ctlv_op_schemas[6].request,
+ CTLV_ATTR_EVENT_QUEUE_LEN, 0, 0, CTLV_SCHEMA_UINT)
+CTLV_IN_HAS(ctlv_event_queue_set_event_queue_len)
+CTLV_IN_U64(ctlv_event_queue_set_event_queue_len)
+
+/* Writing a reply. An append is atomic; a nest ends with ctlv_nest_end(). */
+CTLV_PUT_U64(ctlv_family_id, CTLV_ATTR_FAMILY_ID, CTLV_SCHEMA_UINT)
+CTLV_PUT_STRING(ctlv_family_name, CTLV_ATTR_FAMILY_NAME, CTLV_SCHEMA_STRING)
+CTLV_NEST_START(ctlv_family_entry, CTLV_ATTR_FAMILY_ENTRY, CTLV_SCHEMA_NEST)
+CTLV_PUT_U64(ctlv_blob_type, CTLV_ATTR_BLOB_TYPE, CTLV_SCHEMA_ENUM)
+CTLV_PUT_U64(ctlv_blob_address, CTLV_ATTR_BLOB_ADDRESS, CTLV_SCHEMA_UINT)
+CTLV_PUT_U64(ctlv_blob_fd, CTLV_ATTR_BLOB_FD, CTLV_SCHEMA_UINT)
+CTLV_PUT_U64(ctlv_blob_offset, CTLV_ATTR_BLOB_OFFSET, CTLV_SCHEMA_UINT)
+CTLV_PUT_U64(ctlv_blob_length, CTLV_ATTR_BLOB_LENGTH, CTLV_SCHEMA_UINT)
+CTLV_PUT_U64(ctlv_error_reason, CTLV_ATTR_ERROR_REASON, CTLV_SCHEMA_ENUM)
+CTLV_PUT_STRING(ctlv_error_message, CTLV_ATTR_ERROR_MESSAGE, CTLV_SCHEMA_STRING)
+CTLV_PUT_U64(ctlv_error_bad_attr_offset, CTLV_ATTR_ERROR_BAD_ATTR_OFFSET, CTLV_SCHEMA_UINT)
+CTLV_PUT_U64(ctlv_error_nest_offset, CTLV_ATTR_ERROR_NEST_OFFSET, CTLV_SCHEMA_UINT)
+CTLV_PUT_U64(ctlv_error_missing_attr_id, CTLV_ATTR_ERROR_MISSING_ATTR_ID, CTLV_SCHEMA_UINT)
+CTLV_PUT_U64(ctlv_error_expected_generation, CTLV_ATTR_ERROR_EXPECTED_GENERATION, CTLV_SCHEMA_UINT)
+CTLV_PUT_U64(ctlv_error_current_generation, CTLV_ATTR_ERROR_CURRENT_GENERATION, CTLV_SCHEMA_UINT)
+CTLV_PUT_U64(ctlv_op_id, CTLV_ATTR_OP_ID, CTLV_SCHEMA_UINT)
+CTLV_PUT_U64(ctlv_op_type, CTLV_ATTR_OP_TYPE, CTLV_SCHEMA_ENUM)
+CTLV_PUT_U64(ctlv_op_availability, CTLV_ATTR_OP_AVAILABILITY, CTLV_SCHEMA_ENUM)
+CTLV_PUT_BOOL(ctlv_op_disabled, CTLV_ATTR_OP_DISABLED, CTLV_SCHEMA_BOOL)
+CTLV_PUT_U64(ctlv_op_request_max_len, CTLV_ATTR_OP_REQUEST_MAX_LEN, CTLV_SCHEMA_UINT)
+CTLV_PUT_U64(ctlv_op_action_reply_max_len, CTLV_ATTR_OP_ACTION_REPLY_MAX_LEN, CTLV_SCHEMA_UINT)
+CTLV_PUT_U64(ctlv_op_query_reply_min_len, CTLV_ATTR_OP_QUERY_REPLY_MIN_LEN, CTLV_SCHEMA_UINT)
+CTLV_PUT_U64(ctlv_op_query_reply_suggested_len, CTLV_ATTR_OP_QUERY_REPLY_SUGGESTED_LEN,
+ CTLV_SCHEMA_UINT)
+CTLV_PUT_U64(ctlv_op_event_record_max_len, CTLV_ATTR_OP_EVENT_RECORD_MAX_LEN, CTLV_SCHEMA_UINT)
+CTLV_PUT_U64(ctlv_op_state_generation, CTLV_ATTR_OP_STATE_GENERATION, CTLV_SCHEMA_UINT)
+CTLV_NEST_START(ctlv_op_entry, CTLV_ATTR_OP_ENTRY, CTLV_SCHEMA_NEST)
+CTLV_PUT_U64(ctlv_op_fd_access, CTLV_ATTR_OP_FD_ACCESS, CTLV_SCHEMA_ENUM)
+CTLV_PUT_U64(ctlv_schema_attr_id, CTLV_ATTR_SCHEMA_ATTR_ID, CTLV_SCHEMA_UINT)
+CTLV_PUT_U64(ctlv_schema_kind, CTLV_ATTR_SCHEMA_KIND, CTLV_SCHEMA_UINT)
+CTLV_PUT_U64(ctlv_schema_max_occurrences, CTLV_ATTR_SCHEMA_MAX_OCCURRENCES, CTLV_SCHEMA_UINT)
+CTLV_PUT_U64(ctlv_schema_min, CTLV_ATTR_SCHEMA_MIN, CTLV_SCHEMA_UINT)
+CTLV_PUT_U64(ctlv_schema_max, CTLV_ATTR_SCHEMA_MAX, CTLV_SCHEMA_UINT)
+CTLV_PUT_U64(ctlv_schema_min_length, CTLV_ATTR_SCHEMA_MIN_LENGTH, CTLV_SCHEMA_UINT)
+CTLV_PUT_U64(ctlv_schema_max_length, CTLV_ATTR_SCHEMA_MAX_LENGTH, CTLV_SCHEMA_UINT)
+CTLV_PUT_U64(ctlv_schema_flags_mask, CTLV_ATTR_SCHEMA_FLAGS_MASK, CTLV_SCHEMA_UINT)
+CTLV_PUT_U64(ctlv_schema_unit, CTLV_ATTR_SCHEMA_UNIT, CTLV_SCHEMA_UINT)
+CTLV_PUT_S64(ctlv_schema_scale, CTLV_ATTR_SCHEMA_SCALE, CTLV_SCHEMA_SINT)
+CTLV_PUT_U64(ctlv_schema_blob_direction, CTLV_ATTR_SCHEMA_BLOB_DIRECTION, CTLV_SCHEMA_ENUM)
+CTLV_PUT_U64(ctlv_schema_blob_backings, CTLV_ATTR_SCHEMA_BLOB_BACKINGS, CTLV_SCHEMA_UINT)
+CTLV_PUT_U64(ctlv_schema_blob_max_length, CTLV_ATTR_SCHEMA_BLOB_MAX_LENGTH, CTLV_SCHEMA_UINT)
+CTLV_PUT_U64(ctlv_schema_blob_alignment, CTLV_ATTR_SCHEMA_BLOB_ALIGNMENT, CTLV_SCHEMA_UINT)
+CTLV_PUT_U64(ctlv_schema_reenters, CTLV_ATTR_SCHEMA_REENTERS, CTLV_SCHEMA_UINT)
+CTLV_NEST_START(ctlv_schema_attrs, CTLV_ATTR_SCHEMA_ATTRS, CTLV_SCHEMA_NEST)
+CTLV_NEST_START(ctlv_schema_optional_attrs, CTLV_ATTR_SCHEMA_OPTIONAL_ATTRS, CTLV_SCHEMA_NEST)
+CTLV_NEST_START(ctlv_schema_request, CTLV_ATTR_SCHEMA_REQUEST, CTLV_SCHEMA_NEST)
+CTLV_NEST_START(ctlv_schema_reply, CTLV_ATTR_SCHEMA_REPLY, CTLV_SCHEMA_NEST)
+CTLV_PUT_U64(ctlv_event_op_id, CTLV_ATTR_EVENT_OP_ID, CTLV_SCHEMA_UINT)
+CTLV_PUT_U64(ctlv_event_queue_len, CTLV_ATTR_EVENT_QUEUE_LEN, CTLV_SCHEMA_UINT)
+CTLV_PUT_U64(ctlv_event_queue_used, CTLV_ATTR_EVENT_QUEUE_USED, CTLV_SCHEMA_UINT)
+CTLV_PUT_U64(ctlv_event_first_sequence, CTLV_ATTR_EVENT_FIRST_SEQUENCE, CTLV_SCHEMA_UINT)
+CTLV_PUT_U64(ctlv_event_last_sequence, CTLV_ATTR_EVENT_LAST_SEQUENCE, CTLV_SCHEMA_UINT)
+CTLV_PUT_U64(ctlv_event_dropped_count, CTLV_ATTR_EVENT_DROPPED_COUNT, CTLV_SCHEMA_UINT)
+
+/* Building one event, passed to ctlv_event_emit() or ctlv_event_abort() once. */
+CTLV_EVENT_CREATE(ctlv_op_changed, CTLV_OP_OP_CHANGED)
+CTLV_EVENT_CREATE(ctlv_event_overflow, CTLV_OP_EVENT_OVERFLOW)
+CTLV_EVENT_CREATE(ctlv_device_removed, CTLV_OP_DEVICE_REMOVED)
+#endif /* _CTLV_GENERATED_METADATA_ONLY */
+
+#endif /* _LINUX_CTLV_GEN_H */
diff --git a/include/uapi/linux/ctlv_families_gen.h b/include/uapi/linux/ctlv_families_gen.h
new file mode 100644
index 000000000000..a649483ab3f5
--- /dev/null
+++ b/include/uapi/linux/ctlv_families_gen.h
@@ -0,0 +1,15 @@
+/* SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause) */
+/* Do not edit, regenerate with tools/ctlv/ctlv-regen.sh, from: */
+/* Documentation/ctlv/families.yaml */
+/* CTLV-GEN uapi header */
+
+/* CTLV family numbers. */
+
+#ifndef _UAPI_LINUX_CTLV_FAMILIES_GEN_H
+#define _UAPI_LINUX_CTLV_FAMILIES_GEN_H
+
+enum _ctlv_family {
+ CTLV_FAMILY_ROOT,
+};
+
+#endif /* _UAPI_LINUX_CTLV_FAMILIES_GEN_H */
diff --git a/include/uapi/linux/ctlv_gen.h b/include/uapi/linux/ctlv_gen.h
new file mode 100644
index 000000000000..64eba093a6d4
--- /dev/null
+++ b/include/uapi/linux/ctlv_gen.h
@@ -0,0 +1,271 @@
+/* SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause) */
+/* Do not edit, regenerate with tools/ctlv/ctlv-regen.sh, from: */
+/* Documentation/ctlv/specs/ctlv.yaml */
+/* CTLV-GEN uapi header */
+
+/*
+ * Constants of the ctlv CTLV family. Attr and op IDs are complete; the wire format carrying them is
+ * in <linux/ctlv.h>.
+ */
+
+#ifndef _UAPI_LINUX_CTLV_GEN_H
+#define _UAPI_LINUX_CTLV_GEN_H
+
+#include <linux/ctlv.h>
+#include <linux/ctlv_families_gen.h>
+
+/* enum op-type */
+enum _ctlv_op_type {
+ CTLV_OP_TYPE_ACTION = 1,
+ CTLV_OP_TYPE_QUERY,
+ CTLV_OP_TYPE_EVENT,
+};
+
+/* enum op-availability */
+enum _ctlv_op_availability {
+ CTLV_OP_AVAILABILITY_FIXED = 1,
+ CTLV_OP_AVAILABILITY_DYNAMIC,
+};
+
+/* enum blob-type */
+enum _ctlv_blob_type {
+ CTLV_BLOB_TYPE_VA = 1,
+};
+
+/* enum blob-direction */
+enum _ctlv_blob_direction {
+ CTLV_BLOB_DIRECTION_INPUT = 1,
+ CTLV_BLOB_DIRECTION_OUTPUT,
+ CTLV_BLOB_DIRECTION_INOUT,
+};
+
+/* enum validation-reason */
+enum _ctlv_validation_reason {
+ CTLV_VALIDATION_REASON_MALFORMED_MESSAGE = 1,
+ CTLV_VALIDATION_REASON_UNKNOWN_ATTR,
+ CTLV_VALIDATION_REASON_UNKNOWN_FLAG,
+ CTLV_VALIDATION_REASON_NESTING_TOO_DEEP,
+ CTLV_VALIDATION_REASON_MISSING_ATTR,
+ CTLV_VALIDATION_REASON_TOO_MANY_OCCURRENCES,
+ CTLV_VALIDATION_REASON_VALUE_OUT_OF_RANGE,
+ CTLV_VALIDATION_REASON_BAD_LENGTH,
+ CTLV_VALIDATION_REASON_BAD_STRING,
+ CTLV_VALIDATION_REASON_BAD_BLOB_DESCRIPTOR,
+ CTLV_VALIDATION_REASON_STALE_GENERATION,
+ CTLV_VALIDATION_REASON_NONCONTIGUOUS_ATTR,
+};
+
+/* enum op-fd-access */
+enum _ctlv_op_fd_access {
+ CTLV_OP_FD_ACCESS_READ = 1,
+ CTLV_OP_FD_ACCESS_READ_WRITE,
+};
+
+/* attr groups */
+enum _ctlv_group {
+ CTLV_GROUP_FAMILY = 1,
+ CTLV_GROUP_BLOB,
+ CTLV_GROUP_ERROR,
+ CTLV_GROUP_OP,
+ CTLV_GROUP_SCHEMA,
+ CTLV_GROUP_EVENT,
+};
+
+#define _CTLV_ATTR(_group, _attr) CTLV_ATTR(CTLV_FAMILY_ROOT, (_group), (_attr))
+
+/* attrs of group family */
+#define _CTLV_ATTR_FAMILY(_attr) _CTLV_ATTR(CTLV_GROUP_FAMILY, _CTLV_ATTR_FAMILY_##_attr)
+
+enum _ctlv_attr_family {
+ _CTLV_ATTR_FAMILY_ID = 1,
+ _CTLV_ATTR_FAMILY_NAME,
+ _CTLV_ATTR_FAMILY_ENTRY,
+};
+
+#define CTLV_ATTR_FAMILY_ID _CTLV_ATTR_FAMILY(ID)
+#define CTLV_ATTR_FAMILY_NAME _CTLV_ATTR_FAMILY(NAME)
+#define CTLV_ATTR_FAMILY_ENTRY _CTLV_ATTR_FAMILY(ENTRY)
+
+/* attrs of group blob */
+#define _CTLV_ATTR_BLOB(_attr) _CTLV_ATTR(CTLV_GROUP_BLOB, _CTLV_ATTR_BLOB_##_attr)
+
+enum _ctlv_attr_blob {
+ _CTLV_ATTR_BLOB_TYPE = 1,
+ _CTLV_ATTR_BLOB_ADDRESS,
+ _CTLV_ATTR_BLOB_FD,
+ _CTLV_ATTR_BLOB_OFFSET,
+ _CTLV_ATTR_BLOB_LENGTH,
+};
+
+#define CTLV_ATTR_BLOB_TYPE _CTLV_ATTR_BLOB(TYPE)
+#define CTLV_ATTR_BLOB_ADDRESS _CTLV_ATTR_BLOB(ADDRESS)
+#define CTLV_ATTR_BLOB_FD _CTLV_ATTR_BLOB(FD)
+#define CTLV_ATTR_BLOB_OFFSET _CTLV_ATTR_BLOB(OFFSET)
+#define CTLV_ATTR_BLOB_LENGTH _CTLV_ATTR_BLOB(LENGTH)
+
+/* attrs of group error */
+#define _CTLV_ATTR_ERROR(_attr) _CTLV_ATTR(CTLV_GROUP_ERROR, _CTLV_ATTR_ERROR_##_attr)
+
+enum _ctlv_attr_error {
+ _CTLV_ATTR_ERROR_REASON = 1,
+ _CTLV_ATTR_ERROR_MESSAGE,
+ _CTLV_ATTR_ERROR_BAD_ATTR_OFFSET,
+ _CTLV_ATTR_ERROR_NEST_OFFSET,
+ _CTLV_ATTR_ERROR_MISSING_ATTR_ID,
+ _CTLV_ATTR_ERROR_EXPECTED_GENERATION,
+ _CTLV_ATTR_ERROR_CURRENT_GENERATION,
+};
+
+#define CTLV_ATTR_ERROR_REASON _CTLV_ATTR_ERROR(REASON)
+#define CTLV_ATTR_ERROR_MESSAGE _CTLV_ATTR_ERROR(MESSAGE)
+#define CTLV_ATTR_ERROR_BAD_ATTR_OFFSET _CTLV_ATTR_ERROR(BAD_ATTR_OFFSET)
+#define CTLV_ATTR_ERROR_NEST_OFFSET _CTLV_ATTR_ERROR(NEST_OFFSET)
+#define CTLV_ATTR_ERROR_MISSING_ATTR_ID _CTLV_ATTR_ERROR(MISSING_ATTR_ID)
+#define CTLV_ATTR_ERROR_EXPECTED_GENERATION _CTLV_ATTR_ERROR(EXPECTED_GENERATION)
+#define CTLV_ATTR_ERROR_CURRENT_GENERATION _CTLV_ATTR_ERROR(CURRENT_GENERATION)
+
+/* attrs of group op */
+#define _CTLV_ATTR_OP(_attr) _CTLV_ATTR(CTLV_GROUP_OP, _CTLV_ATTR_OP_##_attr)
+
+enum _ctlv_attr_op {
+ _CTLV_ATTR_OP_ID = 1,
+ _CTLV_ATTR_OP_TYPE,
+ _CTLV_ATTR_OP_AVAILABILITY,
+ _CTLV_ATTR_OP_DISABLED,
+ _CTLV_ATTR_OP_REQUEST_MAX_LEN,
+ _CTLV_ATTR_OP_ACTION_REPLY_MAX_LEN,
+ _CTLV_ATTR_OP_QUERY_REPLY_MIN_LEN,
+ _CTLV_ATTR_OP_QUERY_REPLY_SUGGESTED_LEN,
+ _CTLV_ATTR_OP_EVENT_RECORD_MAX_LEN,
+ _CTLV_ATTR_OP_STATE_GENERATION,
+ _CTLV_ATTR_OP_ENTRY,
+ _CTLV_ATTR_OP_FD_ACCESS,
+};
+
+#define CTLV_ATTR_OP_ID _CTLV_ATTR_OP(ID)
+#define CTLV_ATTR_OP_TYPE _CTLV_ATTR_OP(TYPE)
+#define CTLV_ATTR_OP_AVAILABILITY _CTLV_ATTR_OP(AVAILABILITY)
+#define CTLV_ATTR_OP_DISABLED _CTLV_ATTR_OP(DISABLED)
+#define CTLV_ATTR_OP_REQUEST_MAX_LEN _CTLV_ATTR_OP(REQUEST_MAX_LEN)
+#define CTLV_ATTR_OP_ACTION_REPLY_MAX_LEN _CTLV_ATTR_OP(ACTION_REPLY_MAX_LEN)
+#define CTLV_ATTR_OP_QUERY_REPLY_MIN_LEN _CTLV_ATTR_OP(QUERY_REPLY_MIN_LEN)
+#define CTLV_ATTR_OP_QUERY_REPLY_SUGGESTED_LEN _CTLV_ATTR_OP(QUERY_REPLY_SUGGESTED_LEN)
+#define CTLV_ATTR_OP_EVENT_RECORD_MAX_LEN _CTLV_ATTR_OP(EVENT_RECORD_MAX_LEN)
+#define CTLV_ATTR_OP_STATE_GENERATION _CTLV_ATTR_OP(STATE_GENERATION)
+#define CTLV_ATTR_OP_ENTRY _CTLV_ATTR_OP(ENTRY)
+#define CTLV_ATTR_OP_FD_ACCESS _CTLV_ATTR_OP(FD_ACCESS)
+
+/* attrs of group schema */
+#define _CTLV_ATTR_SCHEMA(_attr) _CTLV_ATTR(CTLV_GROUP_SCHEMA, _CTLV_ATTR_SCHEMA_##_attr)
+
+enum _ctlv_attr_schema {
+ _CTLV_ATTR_SCHEMA_ATTR_ID = 1,
+ _CTLV_ATTR_SCHEMA_KIND,
+ _CTLV_ATTR_SCHEMA_MAX_OCCURRENCES,
+ _CTLV_ATTR_SCHEMA_MIN,
+ _CTLV_ATTR_SCHEMA_MAX,
+ _CTLV_ATTR_SCHEMA_MIN_LENGTH,
+ _CTLV_ATTR_SCHEMA_MAX_LENGTH,
+ _CTLV_ATTR_SCHEMA_FLAGS_MASK,
+ _CTLV_ATTR_SCHEMA_UNIT,
+ _CTLV_ATTR_SCHEMA_SCALE,
+ _CTLV_ATTR_SCHEMA_BLOB_DIRECTION,
+ _CTLV_ATTR_SCHEMA_BLOB_BACKINGS,
+ _CTLV_ATTR_SCHEMA_BLOB_MAX_LENGTH,
+ _CTLV_ATTR_SCHEMA_BLOB_ALIGNMENT,
+ _CTLV_ATTR_SCHEMA_REENTERS,
+ _CTLV_ATTR_SCHEMA_ATTRS,
+ _CTLV_ATTR_SCHEMA_OPTIONAL_ATTRS,
+ _CTLV_ATTR_SCHEMA_REQUEST,
+ _CTLV_ATTR_SCHEMA_REPLY,
+};
+
+#define CTLV_ATTR_SCHEMA_ATTR_ID _CTLV_ATTR_SCHEMA(ATTR_ID)
+#define CTLV_ATTR_SCHEMA_KIND _CTLV_ATTR_SCHEMA(KIND)
+#define CTLV_ATTR_SCHEMA_MAX_OCCURRENCES _CTLV_ATTR_SCHEMA(MAX_OCCURRENCES)
+#define CTLV_ATTR_SCHEMA_MIN _CTLV_ATTR_SCHEMA(MIN)
+#define CTLV_ATTR_SCHEMA_MAX _CTLV_ATTR_SCHEMA(MAX)
+#define CTLV_ATTR_SCHEMA_MIN_LENGTH _CTLV_ATTR_SCHEMA(MIN_LENGTH)
+#define CTLV_ATTR_SCHEMA_MAX_LENGTH _CTLV_ATTR_SCHEMA(MAX_LENGTH)
+#define CTLV_ATTR_SCHEMA_FLAGS_MASK _CTLV_ATTR_SCHEMA(FLAGS_MASK)
+#define CTLV_ATTR_SCHEMA_UNIT _CTLV_ATTR_SCHEMA(UNIT)
+#define CTLV_ATTR_SCHEMA_SCALE _CTLV_ATTR_SCHEMA(SCALE)
+#define CTLV_ATTR_SCHEMA_BLOB_DIRECTION _CTLV_ATTR_SCHEMA(BLOB_DIRECTION)
+#define CTLV_ATTR_SCHEMA_BLOB_BACKINGS _CTLV_ATTR_SCHEMA(BLOB_BACKINGS)
+#define CTLV_ATTR_SCHEMA_BLOB_MAX_LENGTH _CTLV_ATTR_SCHEMA(BLOB_MAX_LENGTH)
+#define CTLV_ATTR_SCHEMA_BLOB_ALIGNMENT _CTLV_ATTR_SCHEMA(BLOB_ALIGNMENT)
+#define CTLV_ATTR_SCHEMA_REENTERS _CTLV_ATTR_SCHEMA(REENTERS)
+#define CTLV_ATTR_SCHEMA_ATTRS _CTLV_ATTR_SCHEMA(ATTRS)
+#define CTLV_ATTR_SCHEMA_OPTIONAL_ATTRS _CTLV_ATTR_SCHEMA(OPTIONAL_ATTRS)
+#define CTLV_ATTR_SCHEMA_REQUEST _CTLV_ATTR_SCHEMA(REQUEST)
+#define CTLV_ATTR_SCHEMA_REPLY _CTLV_ATTR_SCHEMA(REPLY)
+
+/* attrs of group event */
+#define _CTLV_ATTR_EVENT(_attr) _CTLV_ATTR(CTLV_GROUP_EVENT, _CTLV_ATTR_EVENT_##_attr)
+
+enum _ctlv_attr_event {
+ _CTLV_ATTR_EVENT_OP_ID = 1,
+ _CTLV_ATTR_EVENT_QUEUE_LEN,
+ _CTLV_ATTR_EVENT_QUEUE_USED,
+ _CTLV_ATTR_EVENT_FIRST_SEQUENCE,
+ _CTLV_ATTR_EVENT_LAST_SEQUENCE,
+ _CTLV_ATTR_EVENT_DROPPED_COUNT,
+};
+
+#define CTLV_ATTR_EVENT_OP_ID _CTLV_ATTR_EVENT(OP_ID)
+#define CTLV_ATTR_EVENT_QUEUE_LEN _CTLV_ATTR_EVENT(QUEUE_LEN)
+#define CTLV_ATTR_EVENT_QUEUE_USED _CTLV_ATTR_EVENT(QUEUE_USED)
+#define CTLV_ATTR_EVENT_FIRST_SEQUENCE _CTLV_ATTR_EVENT(FIRST_SEQUENCE)
+#define CTLV_ATTR_EVENT_LAST_SEQUENCE _CTLV_ATTR_EVENT(LAST_SEQUENCE)
+#define CTLV_ATTR_EVENT_DROPPED_COUNT _CTLV_ATTR_EVENT(DROPPED_COUNT)
+
+/* metadata of group event */
+enum _ctlv_attr_event_metadata {
+ CTLV_ATTR_EVENT_QUEUE_LEN_UNIT = CTLV_UNIT_BYTES,
+ CTLV_ATTR_EVENT_QUEUE_LEN_SCALE = 0,
+ CTLV_ATTR_EVENT_QUEUE_USED_UNIT = CTLV_UNIT_BYTES,
+ CTLV_ATTR_EVENT_QUEUE_USED_SCALE = 0,
+};
+
+/* attr sets */
+enum _ctlv_set {
+ CTLV_SET_FAMILY_ENTRY = 1,
+ CTLV_SET_OP_ENTRY,
+ CTLV_SET_OP_SCHEMA_REQUEST,
+ CTLV_SET_SCHEMA_NODE,
+ CTLV_SET_OP_SCHEMA_INFO,
+ CTLV_SET_OP_CHANGE_INFO,
+ CTLV_SET_ERROR_INFO,
+ CTLV_SET_EVENT_SUBSCRIPTION,
+ CTLV_SET_EVENT_QUEUE_LIMIT,
+ CTLV_SET_EVENT_QUEUE_STATE,
+ CTLV_SET_EVENT_OVERFLOW_INFO,
+};
+
+/* ops */
+enum _ctlv_op {
+ _CTLV_OP_FAMILIES_DUMP = 1,
+ _CTLV_OP_OPS_DUMP,
+ _CTLV_OP_OP_SCHEMA_GET,
+ _CTLV_OP_SUBSCRIPTIONS_ADD,
+ _CTLV_OP_SUBSCRIPTIONS_REMOVE,
+ _CTLV_OP_EVENT_QUEUE_GET,
+ _CTLV_OP_EVENT_QUEUE_SET,
+ _CTLV_OP_OP_CHANGED,
+ _CTLV_OP_EVENT_OVERFLOW,
+ _CTLV_OP_DEVICE_REMOVED,
+};
+
+#define _CTLV_OP(_op) CTLV_OP(CTLV_FAMILY_ROOT, _CTLV_OP_##_op)
+#define CTLV_OP_FAMILIES_DUMP _CTLV_OP(FAMILIES_DUMP)
+#define CTLV_OP_OPS_DUMP _CTLV_OP(OPS_DUMP)
+#define CTLV_OP_OP_SCHEMA_GET _CTLV_OP(OP_SCHEMA_GET)
+#define CTLV_OP_SUBSCRIPTIONS_ADD _CTLV_OP(SUBSCRIPTIONS_ADD)
+#define CTLV_OP_SUBSCRIPTIONS_REMOVE _CTLV_OP(SUBSCRIPTIONS_REMOVE)
+#define CTLV_OP_EVENT_QUEUE_GET _CTLV_OP(EVENT_QUEUE_GET)
+#define CTLV_OP_EVENT_QUEUE_SET _CTLV_OP(EVENT_QUEUE_SET)
+#define CTLV_OP_OP_CHANGED _CTLV_OP(OP_CHANGED)
+#define CTLV_OP_EVENT_OVERFLOW _CTLV_OP(EVENT_OVERFLOW)
+#define CTLV_OP_DEVICE_REMOVED _CTLV_OP(DEVICE_REMOVED)
+
+#endif /* _UAPI_LINUX_CTLV_GEN_H */
--
2.56.0
^ permalink raw reply related [flat|nested] 32+ messages in thread* [PATCH RFC 04/20] Documentation: ctlv: Add userspace ABI
2026-10-15 14:50 [PATCH RFC 00/20] attestation: Add a common subsystem and TDX support Jiri Pirko
` (2 preceding siblings ...)
2026-10-15 14:50 ` [PATCH RFC 03/20] ctlv: Add the root family specification and generated files Jiri Pirko
@ 2026-10-15 14:50 ` Jiri Pirko
2026-10-15 14:50 ` [PATCH RFC 05/20] ctlv: Prepare message input handling Jiri Pirko
` (15 subsequent siblings)
19 siblings, 0 replies; 32+ messages in thread
From: Jiri Pirko @ 2026-10-15 14:50 UTC (permalink / raw)
To: linux-coco
Cc: linux-kernel, linux-api, x86, kvm, linux-doc, jgg, david.quigley,
lukas, ksinyuk, rodrigo.vivi, cedric.xing, sameo, drdeeglaze,
James.Bottomley, aik, jpiotrowski, qinkun, pgonda, jic23,
alistair.francis, ankita, suzuki.poulose, kas, rick.p.edgecombe,
elena.reshetova, dave.hansen, sathyanarayanan.kuppuswamy,
peter.fang
From: Jiri Pirko <jiri@nvidia.com>
Describe the CTLV transport, discovery, wire format and userspace
compatibility rules.
Assisted-by: Cursor: GPT-5.6 Sol
Assisted-by: Cursor: Claude Opus 5.5
Signed-off-by: Jiri Pirko <jiri@nvidia.com>
---
Documentation/userspace-api/ctlv.rst | 507 ++++++++++++++++++++++++++
Documentation/userspace-api/index.rst | 1 +
MAINTAINERS | 1 +
3 files changed, 509 insertions(+)
create mode 100644 Documentation/userspace-api/ctlv.rst
diff --git a/Documentation/userspace-api/ctlv.rst b/Documentation/userspace-api/ctlv.rst
new file mode 100644
index 000000000000..5ebca3d4d94a
--- /dev/null
+++ b/Documentation/userspace-api/ctlv.rst
@@ -0,0 +1,507 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+==================
+CTLV userspace ABI
+==================
+
+:Author: Jiri Pirko
+
+CTLV is a transport for device control interfaces. A family describes what its
+devices accept in YAML, a generator turns that into kernel metadata and UAPI
+headers, and CTLV validates every message before a family callback sees it.
+Userspace sees one character device per instance, one ioctl, and ``read()`` for
+events.
+
+This document is the ABI. What each family accepts is specified under
+``Documentation/ctlv/specs``; writing a family is :doc:`/driver-api/ctlv`.
+
+Why a TLV transport
+===================
+
+CTLV puts the rules of every operation in one description and validates them
+once:
+
+- One ioctl carries every operation of every family, so a new operation is a
+ new specification entry, not a new ioctl number.
+- Attributes are optional or required by declaration, so a message can grow
+ without breaking a caller that does not know the new attribute.
+- An unknown attribute is refused on a request and tolerated on a reply, so a
+ new kernel can say more to an old program. A request may mark an attribute an
+ old kernel is welcome to skip, so a new program can say more too.
+- Bounds, lengths, occurrence counts and nesting depth are checked before a
+ family callback runs.
+
+Devices
+=======
+
+Each registered instance appears as a character device in the ``ctlv`` class
+with a dynamic minor::
+
+ /dev/ctlv0
+ /sys/class/ctlv/ctlv0
+
+Sysfs shows class membership, the ``dev`` number and the device-model link to
+the parent device. There is no family sysfs attribute; ``families-dump`` on an
+open descriptor is the authoritative answer to what a device is.
+
+Each open file description has its own event subscriptions and event queue.
+``O_RDONLY`` opens an observation fd, ``O_RDWR`` opens a full-control fd, and
+``O_WRONLY`` is rejected with ``-EACCES``. Every action and query declares the
+authority it needs: ``read`` permits either accepted fd, while ``read-write``
+requires full control. Events are readable and subscribable through both.
+``dup()`` and ``SCM_RIGHTS`` preserve that authority; attenuation requires a
+separate observation open.
+
+Discovery
+---------
+
+Both uevents, addition and removal, carry the family chain as numbered
+properties::
+
+ CTLV_FAMILY=ctlv-accelerator-foo
+ CTLV_COMPATIBLE_0=ctlv
+ CTLV_COMPATIBLE_1=ctlv-accelerator
+ CTLV_COMPATIBLE_2=ctlv-accelerator-foo
+ CTLV_COMPATIBLE_N=3
+
+``CTLV_FAMILY`` is the family the device answers as, the numbered properties
+are its ancestry from the root to that family, and
+``CTLV_COMPATIBLE_N`` is how many there are. An index never moves: changing a
+family's parent is an ABI
+break, adding a descendant only appends.
+
+A program that wants every device, including ones appearing while it starts,
+must listen before it looks, which closes the window between enumerating and
+subscribing::
+
+ 1. create a udev monitor filtered to the ctlv subsystem, and enable it
+ 2. enumerate the existing ctlv class devices
+ 3. drain the events already queued on the monitor
+ 4. deduplicate by sysfs path
+ 5. open the device node
+ 6. ask families-dump on the open descriptor for the authoritative chain
+
+Steps 1 to 4 are pre-open discovery, all a udev rule can match on; step 6 does
+not need a udev database. A rule matches a family through the compatibility
+properties::
+
+ # /etc/udev/rules.d/70-ctlv.rules
+ #
+ # Anything of one family, whatever family implementation registered it.
+ SUBSYSTEM=="ctlv", ENV{CTLV_FAMILY}=="ctlv-accelerator-foo", \
+ GROUP="accel", MODE="0660"
+
+ # Anything descended from a family.
+ SUBSYSTEM=="ctlv", ENV{CTLV_COMPATIBLE_1}=="ctlv-accelerator", \
+ TAG+="uaccess"
+
+ # A stable name from the parent device, which is deployment policy.
+ SUBSYSTEM=="ctlv", ENV{CTLV_FAMILY}=="ctlv-accelerator-foo", \
+ SYMLINK+="accel/by-path/$env{ID_PATH}"
+
+CTLV does not standardise such aliases and does not promise that a generic
+identifier survives a reboot or a reprobe; a family with a meaningful identity
+exposes it through an operation of its own.
+
+Wire format
+===========
+
+A message is a tree of attributes. Every attribute begins with the same
+header, at an eight-byte boundary::
+
+ struct ctlv_attr {
+ __u32 len; /* header + payload, without padding */
+ __u32 flags; /* CTLV_ATTR_F_* */
+ __aligned_u64 attr_id; /* complete ID, or 0 for a root */
+ };
+
+One unsigned integer attribute with an eight-byte payload, on the wire::
+
+ 0 1 2 3
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ +---------------------------------------------------------------+
+ | len = 24 |
+ +---------------------------------------------------------------+
+ | flags = 0 |
+ +---------------------------------------------------------------+
+ | |
+ + attr_id +
+ | |
+ +---------------------------------------------------------------+
+ | |
+ + payload +
+ | |
+ +---------------------------------------------------------------+
+
+The rules that make a tree out of that:
+
+- ``CTLV_ALIGNTO`` is 8 and ``CTLV_ALIGN()`` rounds up to it. Every attribute
+ starts at a multiple of eight.
+- ``len`` counts this header plus this attribute's payload, not its own
+ trailing padding.
+- A nested payload is the concatenation of ``CTLV_ALIGN(child.len)`` bytes per
+ child, so a child's padding is counted inside its parent's ``len`` and a
+ leaf's own padding by nobody.
+- ``CTLV_ATTR_F_NESTED`` is set exactly on a root and on a nest.
+- A message begins with one root attribute, whose ``attr_id`` is
+ ``CTLV_ATTR_ROOT`` (zero) and whose payload is the message. A root never
+ appears inside a nest, and a nest never carries the root ID.
+- Scalar payloads are native-endian and their lengths exact: eight bytes for
+ an integer, one byte holding zero or one for a boolean, the string including
+ its NUL terminator, the declared length for binary data.
+- Nesting is limited to ``CTLV_MAX_NEST_DEPTH`` levels, the root being level
+ zero.
+
+A whole message with every offset, for a request of one integer and one nest
+carrying a string and a further nest::
+
+ offset attribute len flags contents
+ ------ ------------------------------ --- ------ ----------------
+ 0 root 120 NESTED attr_id = 0
+ 16 .. chunks 24 - 8-byte integer
+ 40 .. chunk 80 NESTED
+ 56 .. label 19 - "ab" and its NUL
+ 80 .. item 40 NESTED
+ 96 .. value 24 - 8-byte integer
+ 120 end of message
+
+ +--------------------------------------------------------------+ 0
+ | root: len = 120, flags = NESTED, attr_id = 0 |
+ +--------------------------------------------------------------+ 16
+ | chunks: len = 24 | payload: 8 bytes |
+ +--------------------------------------------------------------+ 40
+ | chunk: len = 80, flags = NESTED |
+ | +--------------------------------------------------------+ | 56
+ | | label: len = 19 | payload: 'a' 'b' 0 | 5 bytes padding | |
+ | +--------------------------------------------------------+ | 80
+ | | item: len = 40, flags = NESTED | |
+ | | +--------------------------------------------------+ | | 96
+ | | | value: len = 24 | payload: 8 bytes | | |
+ | | +--------------------------------------------------+ | | 120
+ +--------------------------------------------------------------+
+
+``label.len`` is 19: its header and three payload bytes, not the five bytes of
+padding after it. ``chunk`` counts those five, its ``len`` of 80 being its
+header plus ``CTLV_ALIGN(19)`` plus ``CTLV_ALIGN(40)``. A walk over a nest's
+children advances by ``CTLV_ALIGN(child.len)``, a walk of one payload by
+``child.len``.
+
+An attribute ID names the family that declared it, a group within that family
+and an attribute within that group::
+
+ 63 48 47 32 31 0
+ +--------------+--------------+--------------------------------+
+ | family | group | attr |
+ +--------------+--------------+--------------------------------+
+
+ CTLV_ATTR(family, group, attr)
+ CTLV_ATTR_FAMILY(attr) / CTLV_ATTR_GROUP(attr) / CTLV_ATTR_NUMBER(attr)
+
+An operation ID is the same shape with the group bits zero::
+
+ CTLV_OP(family, op)
+ CTLV_OP_FAMILY(op) / CTLV_OP_NUMBER(op)
+
+Use generated complete IDs. A family's header defines every ID it declares
+and includes its parent's.
+
+Request occurrence order
+------------------------
+
+Occurrences of one attribute ID must be contiguous within each request root or
+nest. ``A A B`` is valid, while ``A B A`` is not. An ignored unknown attribute
+is not an occurrence of any known attribute, but it ends the preceding run, so
+``A ignored-unknown A`` is also invalid. Different IDs otherwise have no
+required numeric or schema order.
+
+Attributes a kernel may skip
+----------------------------
+
+A request attribute, the root aside, may carry
+``CTLV_ATTR_F_IGNORE_IF_UNKNOWN``. If no family the device answers declares
+that complete ID, the kernel steps over the whole attribute without looking
+inside it; without the bit, an attribute the schema does not place where it
+appears fails the request as it always has.
+
+The bit forgives an ID this kernel has never heard of and nothing else. An ID
+the device's schema declares somewhere is held to that schema, so an attribute
+sent where it does not belong fails whether the bit is set or not, and one sent
+where it does belong is checked as usual. Every other rule applies too: the
+length has to fit its nest and the header may carry no bit the format does not
+define. Replies and events never carry it.
+
+This is how a program built against a newer version of a family talks to an
+older kernel. What that kernel knows is validated, what it does not is
+ignored, and a caller that has to know which is which asks ``op-schema-get``
+first.
+
+Executing an operation
+======================
+
+One ioctl executes every operation::
+
+ struct ctlv_op_exec {
+ __aligned_u64 op_id;
+ __aligned_u64 request; /* struct ctlv_attr __user * */
+ __aligned_u64 reply; /* struct ctlv_attr __user * */
+ __u32 request_len;
+ __u32 reply_len;
+ __aligned_u64 generation; /* queries only */
+ __aligned_u64 cursor; /* queries only */
+ __aligned_u64 reserved[2]; /* must be zero */
+ };
+
+ #define CTLV_IOC_OP_EXEC _IOWR(0xBB, 0x00, struct ctlv_op_exec)
+
+``request_len`` is how many bytes the kernel may read at ``request`` and
+``reply_len`` how many it may write at ``reply``. They are copy boundaries, not
+message lengths: a root's ``len`` may be shorter than ``request_len``, the
+bytes after it being ignored, and may not be longer. ``reply_len`` is never
+overwritten; the length written is the reply root's ``len``. The pointers are
+fixed-width so a 32-bit and a 64-bit process pass the same structure, and in
+place of a size field the ioctl number encodes the size and ``reserved``, which
+must be zero, covers compatible growth.
+
+Every action and query carries a physical request root, including an empty
+request. ``request_len`` is therefore at least one attr header and at most the
+1 MiB transport limit. The ``request-max-len`` reported by ``ops-dump`` is the
+maximum request composed from the known schema; a newer caller may exceed it
+with well-formed unknown attrs marked ``IGNORE_IF_UNKNOWN``, up to the
+transport limit.
+
+The three kinds of operation differ in what they answer:
+
+action
+ Executed once and acknowledged. An action with no reply takes
+ ``reply_len`` zero; one with a bounded reply has its maximum in
+ ``ops-dump``.
+
+query
+ Executed once and answered in as many fragments as it needs, using
+ ``generation`` and ``cursor``.
+
+event
+ Not executed. A device produces records and a subscribed file description
+ reads them.
+
+Errors
+------
+
+A failed operation returns a negative errno. Where the failure is a rule the
+message broke and the caller offered a reply buffer, CTLV also writes
+error information there in TLV form: a ``validation-reason`` value, a
+human-readable message, the offset of the offending attribute, the offset of
+the nest it was in, the ID of a missing attribute, and for a stale query the
+expected and the current generation. A buffer too small leaves it unwritten;
+the errno is authoritative either way.
+
+What the transport answers with, as against a family's own callback:
+
+``ENOTTY``
+ Not ``CTLV_IOC_OP_EXEC``; this transport has one ioctl.
+
+``EINVAL``
+ A reserved field was not zero, ``op_id`` was zero, ``generation`` or
+ ``cursor`` was set on something that is not a query, or the message broke
+ a rule of the format or of the schema.
+
+``EFAULT``
+ The structure, the request or the reply was not accessible.
+
+``EOPNOTSUPP``
+ The device does not publish the operation, publishes it but has it
+ disabled, or the operation is an event, which is read rather than executed.
+
+``EACCES``
+ The fd lacks the operation's declared authority, or an ``O_WRONLY`` open
+ was attempted.
+
+``EMSGSIZE``
+ ``reply_len`` is smaller than the smallest fragment the operation can
+ answer with. Nothing was executed.
+
+``ESTALE``
+ A continuation whose ``generation`` no longer matches. Start the query
+ again.
+
+``ENODEV``
+ The device was unregistered. Descriptors stay open and every operation on
+ them fails this way.
+
+``EBADF``, ``EIO``
+ A blob descriptor named something that is not a file, or the file refused
+ the transfer.
+
+``ENOMEM``
+ The kernel could not allocate what the operation needed.
+
+``read()`` answers ``EAGAIN`` when no record is queued on a non-blocking
+description, ``EINTR`` when a blocking read was interrupted, ``EMSGSIZE`` when
+the buffer cannot hold the record at the head of the queue, and ``ENODEV``
+once a removed device's queue has been drained.
+
+Queries and fragments
+=====================
+
+A query's answer may be larger than the buffer the caller wants to commit, so
+the kernel fills the buffer with as much of it as fits where the tree can be
+cut. The first call passes ``generation`` and ``cursor`` zero. On return:
+
+- the kernel puts the query's generation in ``generation``;
+- ``cursor`` is nonzero when there is more to come, zero when the answer is
+ complete;
+- the reply root carries ``CTLV_ATTR_F_FRAG_MORE`` when the answer continues,
+ and so does every nest still logically open at the cut.
+
+To continue, the caller repeats the call with the same ``op_id`` and request,
+the returned ``cursor`` and the returned ``generation``, and reassembles by
+concatenating the children of each still-open nest.
+
+One answer of three items, cut by a buffer that holds two::
+
+ call 1: cursor in = 0 call 2: cursor in = c
+ cursor out = c cursor out = 0
+
+ +----------------------------+ +----------------------------+
+ | root: FRAG_MORE | | root: (no FRAG_MORE) |
+ | +-----------------------+ | | +-----------------------+ |
+ | | chunk: FRAG_MORE | | | | chunk: | |
+ | | +------------------+ | | | | +------------------+ | |
+ | | | item: value = 1 | | | | | | item: value = 3 | | |
+ | | +------------------+ | | | | +------------------+ | |
+ | | | item: value = 2 | | | | +-----------------------+ |
+ | | +------------------+ | | +----------------------------+
+ | +-----------------------+ |
+ +----------------------------+
+
+ reassembled: root -> chunk -> item(1), item(2), item(3)
+
+``FRAG_MORE`` on the root says the answer continues; on ``chunk`` it says the
+next fragment resumes inside that same chunk. A complete nest does not carry
+it, so a reader knows whether to append or to open a new nest beside it.
+
+Every continuing fragment adds at least one complete logical attr beyond its
+repeated path, and a cursor value cannot recur within one logical reply.
+A reader must reject either violation.
+
+A continuation whose generation no longer matches fails with ``-ESTALE``, and
+the error information carries the expected and the current generation.
+
+Events
+======
+
+An event reaches a file description that asked for it, and only that one:
+
+1. subscribe with the ``subscriptions-add`` action, naming operation IDs;
+2. read records with ``read()``, one complete record per call;
+3. unsubscribe with ``subscriptions-remove``.
+
+``poll()`` reports readability when a record is queued. Each record is a
+header followed by exactly one root attribute::
+
+ struct ctlv_event_hdr {
+ __u32 length; /* header + aligned root */
+ __u32 flags; /* zero */
+ __aligned_u64 op_id; /* the event op that produced it */
+ __aligned_u64 sequence; /* per file description */
+ };
+
+One ``read()`` returns one record, the header and the aligned root::
+
+ +---------------------------------------------------------------+ 0
+ | length = 24 + CTLV_ALIGN(root.len) | flags = 0 |
+ +---------------------------------------------------------------+ 8
+ | op_id: the event operation that produced this record |
+ +---------------------------------------------------------------+ 16
+ | sequence: counted per file description |
+ +---------------------------------------------------------------+ 24
+ | root: len, flags = NESTED, attr_id = 0 |
+ | the event's attributes, exactly as a reply carries them |
+ +---------------------------------------------------------------+ length
+
+A buffer shorter than ``length`` reads nothing and fails with ``-EMSGSIZE``. A
+record is one message, bounded by ``CTLV_MAX_INLINE_MESSAGE_LEN``. A sequence
+number is assigned to every eligible record, dropped ones included, so a gap in
+the sequence is exactly what was lost.
+
+The queue is per file description: ``event-queue-get`` reports its length and
+how much is in use, ``event-queue-set`` changes it up to
+``CTLV_MAX_EVENT_QUEUE_LEN``. A record that does not fit is dropped, and the
+next read returns an ``event-overflow`` record carrying the first and last
+sequence numbers lost and how many records that was.
+
+Two events come from the root family:
+
+op-changed
+ An operation with dynamic availability was enabled or disabled. The record
+ carries the operation ID, the new state, and the state generation that
+ ``ops-dump`` also reports, so a client can order dump against event.
+
+device-removed
+ The device is gone. Operations fail from then on and the descriptor stays
+ valid until it is closed.
+
+Blobs
+=====
+
+An operation that moves more data than a message should carry declares a blob
+attribute, and the caller passes a descriptor instead of the data: a nest
+carrying the backing type, the length and the identifier of the memory. One
+backing is defined:
+
+``va``
+ Anonymous userspace memory of the calling process, named by an address in
+ ``blob.address``.
+
+The backings an attribute accepts, the direction data may flow, the maximum
+length and the required alignment are declared by the family and reported by
+``op-schema-get``. Memory is acquired for the operation and released before it
+returns; a descriptor the operation may not use is refused before the callback.
+While the operation is in flight, userspace must not modify an ``input`` blob
+or access an ``output`` or ``inout`` blob. Whether two descriptors overlap is
+also the caller's responsibility. Keep every named VA mapping valid until the
+operation returns.
+
+Introspection
+=============
+
+Three queries describe a device to a program that was not compiled against it.
+The CTLV core answers all three on every device.
+
+``families-dump``
+ The device's family chain, root first, as a numeric ID and a name per
+ entry. The authoritative answer to what the device is.
+
+``ops-dump``
+ One entry per published operation: its ID, kind, required fd access,
+ whether its availability is fixed or dynamic, whether it is currently
+ disabled, and the message lengths that apply on this device. An
+ unpublished operation has no entry and is indistinguishable from one no
+ family ever declared. Event entries have no fd-access field.
+
+``op-schema-get``
+ The whole tree of one operation: every attribute of the request and the
+ reply, with its payload form, its bounds, its occurrence limit, and its
+ unit and scale where it has them.
+
+A schema that describes a schema does not end, so it is reported through
+re-entry: a node whose members are those of a level already described carries
+``schema.reenters``, saying how many levels up that level is. A consumer that
+follows ``reenters`` handles any depth.
+
+The lengths in ``ops-dump`` include whatever a descendant family added to an
+inherited operation, so ask rather than compute: a family's generated maximum
+is right for its own devices and a lower bound on a descendant's.
+
+Limits
+======
+
+Implementation limits, not encoded maxima of the wire fields, so raising one
+later is a compatible change::
+
+ CTLV_MAX_INLINE_MESSAGE_LEN 1 MiB request roots, action replies,
+ query fragments, event roots
+ CTLV_MAX_ERROR_LEN 16 KiB error information
+ CTLV_MAX_NEST_DEPTH 32 levels, root at zero
+ CTLV_DEFAULT_QUERY_REPLY_LEN 64 KiB suggested query buffer
+ CTLV_MAX_EVENT_QUEUE_LEN 16 MiB per file description
diff --git a/Documentation/userspace-api/index.rst b/Documentation/userspace-api/index.rst
index a68b1bea57a8..d1b53c4e16eb 100644
--- a/Documentation/userspace-api/index.rst
+++ b/Documentation/userspace-api/index.rst
@@ -45,6 +45,7 @@ Devices and I/O
:maxdepth: 1
accelerators/ocxl
+ ctlv
dma-buf-heaps
dma-buf-alloc-exchange
fwctl/index
diff --git a/MAINTAINERS b/MAINTAINERS
index 8ab0d6a7a2fd..882836d85125 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -7006,6 +7006,7 @@ M: Jiri Pirko <jiri@resnulli.us>
L: linux-kernel@vger.kernel.org
S: Maintained
F: Documentation/ctlv/
+F: Documentation/userspace-api/ctlv.rst
F: include/linux/ctlv*.h
F: include/uapi/linux/ctlv*.h
F: tools/ctlv/
--
2.56.0
^ permalink raw reply related [flat|nested] 32+ messages in thread* [PATCH RFC 05/20] ctlv: Prepare message input handling
2026-10-15 14:50 [PATCH RFC 00/20] attestation: Add a common subsystem and TDX support Jiri Pirko
` (3 preceding siblings ...)
2026-10-15 14:50 ` [PATCH RFC 04/20] Documentation: ctlv: Add userspace ABI Jiri Pirko
@ 2026-10-15 14:50 ` Jiri Pirko
2026-10-15 15:06 ` sashiko-bot
2026-10-15 14:50 ` [PATCH RFC 06/20] ctlv: Prepare blob descriptor validation Jiri Pirko
` (14 subsequent siblings)
19 siblings, 1 reply; 32+ messages in thread
From: Jiri Pirko @ 2026-10-15 14:50 UTC (permalink / raw)
To: linux-coco
Cc: linux-kernel, linux-api, x86, kvm, linux-doc, jgg, david.quigley,
lukas, ksinyuk, rodrigo.vivi, cedric.xing, sameo, drdeeglaze,
James.Bottomley, aik, jpiotrowski, qinkun, pgonda, jic23,
alistair.francis, ankita, suzuki.poulose, kas, rick.p.edgecombe,
elena.reshetova, dave.hansen, sathyanarayanan.kuppuswamy,
peter.fang
From: Jiri Pirko <jiri@nvidia.com>
Add wire walking, request indexing and validated input accessors for
generated family readers.
Keep CONFIG_CTLV unwired until dispatch completes the module.
Assisted-by: Cursor: GPT-5.6 Sol
Assisted-by: Cursor: Claude Opus 5.5
Signed-off-by: Jiri Pirko <jiri@nvidia.com>
---
MAINTAINERS | 1 +
drivers/ctlv/Kconfig | 12 ++
drivers/ctlv/Makefile | 4 +
drivers/ctlv/internal.h | 66 ++++++
drivers/ctlv/message.c | 403 ++++++++++++++++++++++++++++++++++++
drivers/ctlv/message.h | 92 ++++++++
include/linux/ctlv_family.h | 138 ++++++++++++
7 files changed, 716 insertions(+)
create mode 100644 drivers/ctlv/Kconfig
create mode 100644 drivers/ctlv/Makefile
create mode 100644 drivers/ctlv/internal.h
create mode 100644 drivers/ctlv/message.c
create mode 100644 drivers/ctlv/message.h
diff --git a/MAINTAINERS b/MAINTAINERS
index 882836d85125..b308e3a5f0b6 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -7007,6 +7007,7 @@ L: linux-kernel@vger.kernel.org
S: Maintained
F: Documentation/ctlv/
F: Documentation/userspace-api/ctlv.rst
+F: drivers/ctlv/
F: include/linux/ctlv*.h
F: include/uapi/linux/ctlv*.h
F: tools/ctlv/
diff --git a/drivers/ctlv/Kconfig b/drivers/ctlv/Kconfig
new file mode 100644
index 000000000000..aed3243c55fd
--- /dev/null
+++ b/drivers/ctlv/Kconfig
@@ -0,0 +1,12 @@
+# SPDX-License-Identifier: GPL-2.0-only
+menuconfig CTLV
+ tristate "CTLV character device TLV UAPI"
+ help
+ CTLV provides character device UAPIs described by a checked-in YAML
+ specification. It owns the character device lifetime, the strict TLV
+ message format, request validation, reply serialization, the large
+ payloads a request describes rather than carries, per-open event
+ queues and runtime schema introspection, so families only implement
+ semantics.
+
+ If you don't know what to do here, say N.
diff --git a/drivers/ctlv/Makefile b/drivers/ctlv/Makefile
new file mode 100644
index 000000000000..4f0fe41bc9e1
--- /dev/null
+++ b/drivers/ctlv/Makefile
@@ -0,0 +1,4 @@
+# SPDX-License-Identifier: GPL-2.0
+obj-$(CONFIG_CTLV) += ctlv.o
+
+ctlv-y += message.o
diff --git a/drivers/ctlv/internal.h b/drivers/ctlv/internal.h
new file mode 100644
index 000000000000..7d2b43857299
--- /dev/null
+++ b/drivers/ctlv/internal.h
@@ -0,0 +1,66 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES
+ *
+ * CTLV core internals.
+ */
+#ifndef _CTLV_INTERNAL_H
+#define _CTLV_INTERNAL_H
+
+#include <linux/ctlv_family.h>
+#include <linux/types.h>
+
+#include <uapi/linux/ctlv.h>
+
+#include "message.h"
+
+#define CTLV_ERROR_MESSAGE_LEN 256
+
+/* An op that is always enabled has no per-device availability state. */
+#define CTLV_OP_STATE_NONE U32_MAX
+
+struct ctlv_op_entry {
+ const struct ctlv_op_schema *schema;
+ const struct ctlv_op *binding;
+ void *priv;
+ struct ctlv_op_sizes sizes;
+ u32 state_index;
+ bool absent;
+};
+
+struct ctlv_error {
+ u32 reason;
+ char message[CTLV_ERROR_MESSAGE_LEN];
+ u32 bad_attr_offset;
+ u32 nest_offset;
+ u64 missing_attr_id;
+ u64 expected_generation;
+ u64 current_generation;
+};
+
+struct ctlv_query_fragment {
+ u64 generation;
+ u64 cursor;
+ u64 reported;
+ u64 next_cursor;
+ bool progress;
+ bool answered;
+ int result;
+};
+
+struct ctlv_op_ctx {
+ const struct ctlv_family *family;
+ const struct ctlv_op_entry *op;
+
+ struct ctlv_msg request;
+ struct ctlv_cursor request_root;
+
+ struct ctlv_msg output;
+ struct ctlv_cursor output_root;
+ struct ctlv_cursor *deepest;
+
+ struct ctlv_query_fragment frag;
+ struct ctlv_error error;
+};
+
+#endif /* _CTLV_INTERNAL_H */
diff --git a/drivers/ctlv/message.c b/drivers/ctlv/message.c
new file mode 100644
index 000000000000..7e5742ba8f70
--- /dev/null
+++ b/drivers/ctlv/message.c
@@ -0,0 +1,403 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES
+ *
+ * Message walking, indexing, and generated-accessor primitives.
+ */
+#include <linux/align.h>
+#include <linux/bits.h>
+#include <linux/bug.h>
+#include <linux/ctlv.h>
+#include <linux/err.h>
+#include <linux/errno.h>
+#include <linux/export.h>
+#include <linux/string.h>
+#include <linux/types.h>
+
+#include <uapi/linux/ctlv.h>
+#include <uapi/linux/ctlv_gen.h>
+
+#include "internal.h"
+#include "message.h"
+
+enum ctlv_message_step ctlv_message_next(const struct ctlv_attr *nest,
+ const struct ctlv_attr **child)
+{
+ const u8 *end = (const u8 *)nest + nest->len;
+ const u8 *next;
+ size_t room;
+
+ if (nest->len < sizeof(*nest))
+ return CTLV_MESSAGE_STEP_BAD;
+ if (*child)
+ next = (const u8 *)*child + ALIGN((size_t)(*child)->len, CTLV_ALIGNTO);
+ else
+ next = (const u8 *)(nest + 1);
+ if (next >= end)
+ return next == end ? CTLV_MESSAGE_STEP_END : CTLV_MESSAGE_STEP_BAD;
+
+ room = end - next;
+ if (room < sizeof(**child))
+ return CTLV_MESSAGE_STEP_BAD;
+ *child = (const struct ctlv_attr *)next;
+ /*
+ * Length before alignment, so that aligning a length this nest could
+ * never hold is not what overflows.
+ */
+ if ((*child)->len < sizeof(**child) || (*child)->len > room)
+ return CTLV_MESSAGE_STEP_BAD;
+ if (ALIGN((size_t)(*child)->len, CTLV_ALIGNTO) > room)
+ return CTLV_MESSAGE_STEP_BAD;
+ return CTLV_MESSAGE_STEP_ATTR;
+}
+
+u32 ctlv_message_offset(const struct ctlv_msg *msg, const struct ctlv_attr *attr)
+{
+ return (const u8 *)attr - (const u8 *)msg->root;
+}
+
+void ctlv_message_root(struct ctlv_cursor *cursor, struct ctlv_msg *msg)
+{
+ *cursor = (struct ctlv_cursor){
+ .msg = msg,
+ .attr = msg->root,
+ .tree = msg->tree,
+ .index_nest_scope = CTLV_MESSAGE_INPUT_INDEX_NONE,
+ .index_scope = CTLV_MESSAGE_INPUT_INDEX_NONE,
+ };
+ if (msg->request && msg->index)
+ cursor->index_nest_scope = msg->index->nests[0].first_scope;
+}
+
+static bool ctlv_message_nest_index_ok(const struct ctlv_cursor *nest)
+{
+ const struct ctlv_message_input_index *index = nest->msg->index;
+ const struct ctlv_message_input_nest *indexed;
+ const struct ctlv_message_input_scope *scope;
+ const struct ctlv_attr *attr;
+
+ if (WARN_ON_ONCE(!nest->msg->request || !index ||
+ nest->index_nest_scope >= index->scopes_count))
+ return false;
+ scope = &index->scopes[nest->index_nest_scope];
+ if (WARN_ON_ONCE(scope->nest >= index->nests_count))
+ return false;
+ indexed = &index->nests[scope->nest];
+ attr = (const void *)((const u8 *)nest->msg->root + indexed->offset);
+ if (WARN_ON_ONCE(attr != nest->attr))
+ return false;
+ return !WARN_ON_ONCE(scope->tree != nest->tree || scope->node != nest->node);
+}
+
+static bool ctlv_message_input_scope_ok(const struct ctlv_message_input_index *index,
+ const struct ctlv_message_input_scope *scope)
+{
+ return !WARN_ON_ONCE(scope->first_slot > index->slots_count ||
+ scope->slots_count > index->slots_count - scope->first_slot);
+}
+
+static const struct ctlv_node_schema *ctlv_message_input_ref_node(struct ctlv_input_ref ref)
+{
+ const struct ctlv_node_schema *parent;
+ const struct ctlv_node_schema *node;
+
+ if (WARN_ON_ONCE(!ref.tree->node_schemas || ref.node >= ref.tree->node_schemas_count))
+ return NULL;
+ parent = &ref.tree->node_schemas[ref.node];
+ if (WARN_ON_ONCE(ref.slot >= parent->children_count ||
+ parent->first_child + ref.slot >= ref.tree->node_schemas_count))
+ return NULL;
+ node = &ref.tree->node_schemas[parent->first_child + ref.slot];
+ if (WARN_ON_ONCE(node->attr_id != ref.attr_id || node->kind != ref.kind))
+ return NULL;
+ return node;
+}
+
+static const struct ctlv_message_input_slot *
+ctlv_message_cursor_index(const struct ctlv_cursor *cursor)
+{
+ const struct ctlv_message_input_index *index;
+ const struct ctlv_message_input_scope *scope;
+ const struct ctlv_node_schema *parent;
+ const struct ctlv_node_schema *node;
+ const struct ctlv_message_input_slot *slot;
+ const struct ctlv_message_input_nest *nest;
+ u32 local_slot;
+ u32 offset;
+
+ if (WARN_ON_ONCE(!cursor->msg || !cursor->tree || !cursor->msg->request ||
+ !cursor->msg->index))
+ return NULL;
+ index = cursor->msg->index;
+ if (WARN_ON_ONCE(cursor->index_scope >= index->scopes_count))
+ return NULL;
+ scope = &index->scopes[cursor->index_scope];
+ if (!ctlv_message_input_scope_ok(index, scope))
+ return NULL;
+ parent = &scope->tree->node_schemas[scope->node];
+ if (WARN_ON_ONCE(scope->tree != cursor->tree || cursor->node < parent->first_child ||
+ cursor->node >= parent->first_child + scope->slots_count))
+ return NULL;
+ local_slot = cursor->node - parent->first_child;
+ node = &cursor->tree->node_schemas[cursor->node];
+ slot = &index->slots[scope->first_slot + local_slot];
+ if (!cursor->attr)
+ return slot;
+ if (WARN_ON_ONCE(cursor->attr->attr_id != node->attr_id))
+ return NULL;
+ if (WARN_ON_ONCE(scope->nest >= index->nests_count))
+ return NULL;
+ nest = &index->nests[scope->nest];
+ if (WARN_ON_ONCE(cursor->parent !=
+ (const void *)((const u8 *)cursor->msg->root + nest->offset)))
+ return NULL;
+ offset = ctlv_message_offset(cursor->msg, cursor->attr);
+ if (WARN_ON_ONCE(offset < slot->first_offset || offset >= slot->end_offset ||
+ offset + ALIGN((size_t)cursor->attr->len, CTLV_ALIGNTO) >
+ slot->end_offset))
+ return NULL;
+ if (node->kind == CTLV_SCHEMA_NEST) {
+ const struct ctlv_message_input_scope *nest_scope;
+
+ if (WARN_ON_ONCE(cursor->index_nest_scope >= index->scopes_count))
+ return NULL;
+ nest_scope = &index->scopes[cursor->index_nest_scope];
+ if (WARN_ON_ONCE(nest_scope->nest >= index->nests_count ||
+ index->nests[nest_scope->nest].offset != offset ||
+ nest_scope->tree != cursor->tree ||
+ nest_scope->node != cursor->node))
+ return NULL;
+ } else if (WARN_ON_ONCE(cursor->index_nest_scope != CTLV_MESSAGE_INPUT_INDEX_NONE)) {
+ return NULL;
+ }
+ return slot;
+}
+
+bool ctlv_message_input_rebind(struct ctlv_cursor *nest, const struct ctlv_schema_tree *tree,
+ u32 node)
+{
+ const struct ctlv_message_input_scope *bound_scope;
+ const struct ctlv_message_input_nest *indexed;
+ u32 i;
+
+ if (WARN_ON_ONCE(!nest->attr || !nest->msg || !nest->msg->index))
+ return false;
+ if (!ctlv_message_nest_index_ok(nest))
+ return false;
+ bound_scope = &nest->msg->index->scopes[nest->index_nest_scope];
+ indexed = &nest->msg->index->nests[bound_scope->nest];
+ for (i = 0; i < indexed->scopes_count; i++) {
+ u32 scope_index = indexed->first_scope + i;
+ const struct ctlv_message_input_scope *scope =
+ &nest->msg->index->scopes[scope_index];
+
+ if (scope->tree != tree || scope->node != node)
+ continue;
+ nest->tree = tree;
+ nest->node = node;
+ nest->index_nest_scope = scope_index;
+ return true;
+ }
+ return false;
+}
+
+struct ctlv_cursor *ctlv_input_attr(struct ctlv_cursor *cursor, const struct ctlv_cursor *nest,
+ struct ctlv_input_ref ref)
+{
+ const struct ctlv_message_input_index *index;
+ const struct ctlv_message_input_scope *scope;
+ const struct ctlv_message_input_slot *indexed;
+ const struct ctlv_node_schema *schema;
+
+ *cursor = (struct ctlv_cursor){
+ .index_nest_scope = CTLV_MESSAGE_INPUT_INDEX_NONE,
+ .index_scope = CTLV_MESSAGE_INPUT_INDEX_NONE,
+ };
+ if (WARN_ON_ONCE(!nest->attr || !nest->msg))
+ return cursor;
+ if (!ctlv_message_nest_index_ok(nest))
+ return cursor;
+ index = nest->msg->index;
+ if (WARN_ON_ONCE(nest->index_nest_scope >= index->scopes_count))
+ return cursor;
+ scope = &index->scopes[nest->index_nest_scope];
+ if (!ctlv_message_input_scope_ok(index, scope))
+ return cursor;
+ if (WARN_ON_ONCE(scope->tree != ref.tree || scope->node != ref.node ||
+ nest->tree != ref.tree || nest->node != ref.node ||
+ ref.slot >= scope->slots_count))
+ return cursor;
+ schema = ctlv_message_input_ref_node(ref);
+ if (!schema)
+ return cursor;
+ indexed = &index->slots[scope->first_slot + ref.slot];
+ *cursor = (struct ctlv_cursor){
+ .msg = nest->msg,
+ .parent = nest->attr,
+ .attr = indexed->count ? (const void *)((const u8 *)nest->msg->root +
+ indexed->first_offset) :
+ NULL,
+ .tree = ref.tree,
+ .node = ref.tree->node_schemas[ref.node].first_child + ref.slot,
+ .depth = nest->depth + 1,
+ .index_nest_scope = CTLV_MESSAGE_INPUT_INDEX_NONE,
+ .index_scope = nest->index_nest_scope,
+ };
+ if (indexed->count && schema->kind == CTLV_SCHEMA_NEST) {
+ if (WARN_ON_ONCE(indexed->first_nest >= index->nests_count))
+ return cursor;
+ cursor->index_nest_scope = index->nests[indexed->first_nest].first_scope;
+ }
+ return cursor;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_input_attr, "CTLV");
+
+bool ctlv_input_cursor(const struct ctlv_cursor *cursor, struct ctlv_input_ref ref)
+{
+ const struct ctlv_message_input_scope *scope;
+ const struct ctlv_node_schema *schema;
+
+ if (!ctlv_message_cursor_index(cursor))
+ return false;
+ schema = ctlv_message_input_ref_node(ref);
+ if (!schema)
+ return false;
+ scope = &cursor->msg->index->scopes[cursor->index_scope];
+ return !WARN_ON_ONCE(scope->tree != ref.tree ||
+ scope->node != ref.node ||
+ cursor->tree != ref.tree ||
+ cursor->node !=
+ ref.tree->node_schemas[ref.node].first_child +
+ ref.slot);
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_input_cursor, "CTLV");
+
+void ctlv_attr_next(struct ctlv_cursor *cursor)
+{
+ const struct ctlv_message_input_slot *slot;
+ const struct ctlv_message_input_scope *scope;
+ const struct ctlv_node_schema *node;
+ const struct ctlv_attr *next;
+ u32 next_nest;
+ u32 next_offset;
+
+ slot = ctlv_message_cursor_index(cursor);
+ if (!slot || WARN_ON_ONCE(!cursor->attr))
+ return;
+ next = (const void *)((const u8 *)cursor->attr +
+ ALIGN((size_t)cursor->attr->len, CTLV_ALIGNTO));
+ next_offset = ctlv_message_offset(cursor->msg, next);
+ if (next_offset == slot->end_offset) {
+ cursor->attr = NULL;
+ cursor->index_nest_scope = CTLV_MESSAGE_INPUT_INDEX_NONE;
+ return;
+ }
+ if (WARN_ON_ONCE(next_offset > slot->end_offset))
+ return;
+ cursor->attr = next;
+ node = &cursor->tree->node_schemas[cursor->node];
+ if (node->kind == CTLV_SCHEMA_NEST) {
+ if (WARN_ON_ONCE(cursor->index_nest_scope >= cursor->msg->index->scopes_count))
+ return;
+ scope = &cursor->msg->index->scopes[cursor->index_nest_scope];
+ if (WARN_ON_ONCE(scope->nest >= cursor->msg->index->nests_count))
+ return;
+ next_nest = cursor->msg->index->nests[scope->nest].next;
+ if (WARN_ON_ONCE(next_nest == CTLV_MESSAGE_INPUT_INDEX_NONE ||
+ next_nest >= cursor->msg->index->nests_count))
+ return;
+ cursor->index_nest_scope = cursor->msg->index->nests[next_nest].first_scope;
+ }
+ WARN_ON_ONCE(!ctlv_message_cursor_index(cursor));
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_attr_next, "CTLV");
+
+bool ctlv_attr_ok(const struct ctlv_cursor *cursor)
+{
+ return cursor && cursor->attr && ctlv_message_cursor_index(cursor);
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_attr_ok, "CTLV");
+
+/* The payload, checked against the kinds a reader of this C type accepts. */
+static const void *ctlv_message_cursor_payload(const struct ctlv_cursor *cursor, u32 kinds,
+ u32 *len)
+{
+ const struct ctlv_node_schema *node;
+
+ if (!ctlv_message_cursor_index(cursor) || WARN_ON_ONCE(!cursor->attr))
+ return NULL;
+ node = &cursor->tree->node_schemas[cursor->node];
+ if (WARN_ON_ONCE(!(kinds & BIT(node->kind))))
+ return NULL;
+ *len = ctlv_message_attr_payload_len(cursor->attr);
+ return ctlv_message_attr_payload(cursor->attr);
+}
+
+/* A length disagreeing with a validated kind is this core's mistake. */
+static const void *ctlv_message_cursor_fixed(const struct ctlv_cursor *cursor, u32 kinds,
+ u32 expected)
+{
+ const void *payload;
+ u32 len;
+
+ payload = ctlv_message_cursor_payload(cursor, kinds, &len);
+ if (!payload || WARN_ON_ONCE(len != expected))
+ return NULL;
+ return payload;
+}
+
+u64 ctlv_attr_u64(const struct ctlv_cursor *cursor)
+{
+ const u64 *value = ctlv_message_cursor_fixed(cursor,
+ BIT(CTLV_SCHEMA_UINT) | BIT(CTLV_SCHEMA_ENUM) |
+ BIT(CTLV_SCHEMA_FLAGS),
+ sizeof(u64));
+
+ return value ? *value : 0;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_attr_u64, "CTLV");
+
+s64 ctlv_attr_s64(const struct ctlv_cursor *cursor)
+{
+ const s64 *value = ctlv_message_cursor_fixed(cursor, BIT(CTLV_SCHEMA_SINT), sizeof(s64));
+
+ return value ? *value : 0;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_attr_s64, "CTLV");
+
+bool ctlv_attr_bool(const struct ctlv_cursor *cursor)
+{
+ const u8 *value = ctlv_message_cursor_fixed(cursor, BIT(CTLV_SCHEMA_BOOL), sizeof(u8));
+
+ return value ? *value : false;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_attr_bool, "CTLV");
+
+const char *ctlv_attr_string(const struct ctlv_cursor *cursor)
+{
+ const char *value;
+ u32 len;
+
+ value = ctlv_message_cursor_payload(cursor, BIT(CTLV_SCHEMA_STRING), &len);
+ if (!value || WARN_ON_ONCE(!len || value[len - 1]))
+ return NULL;
+ return value;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_attr_string, "CTLV");
+
+const void *ctlv_attr_binary(const struct ctlv_cursor *cursor, u32 *len)
+{
+ const void *value;
+
+ *len = 0;
+ value = ctlv_message_cursor_payload(cursor, BIT(CTLV_SCHEMA_BINARY), len);
+ if (!value)
+ *len = 0;
+ return value;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_attr_binary, "CTLV");
+
+/*
+ * The room is measured before anything is written, which is what leaves a
+ * refused attr's message exactly as it was, padding included.
+ */
diff --git a/drivers/ctlv/message.h b/drivers/ctlv/message.h
new file mode 100644
index 000000000000..efd781af9b37
--- /dev/null
+++ b/drivers/ctlv/message.h
@@ -0,0 +1,92 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES
+ *
+ * Private message representation and nest walking.
+ */
+#ifndef _CTLV_MESSAGE_H
+#define _CTLV_MESSAGE_H
+
+#include <linux/ctlv_family.h>
+#include <linux/types.h>
+
+#include <uapi/linux/ctlv.h>
+
+#define CTLV_MESSAGE_INPUT_INDEX_NONE U32_MAX
+
+/* Attr identity remains in the generated schema, not in this request index. */
+struct ctlv_message_input_slot {
+ u32 first_offset;
+ u32 end_offset;
+ u32 count;
+ u32 excess_offset;
+ u32 first_nest;
+ u32 last_nest;
+};
+
+struct ctlv_message_input_scope {
+ const struct ctlv_schema_tree *tree;
+ u32 nest;
+ u32 node;
+ u32 first_slot;
+ u32 slots_count;
+ bool optional;
+};
+
+struct ctlv_message_input_nest {
+ u32 offset;
+ u32 first_scope;
+ u32 scopes_count;
+ u32 next;
+};
+
+struct ctlv_message_input_index {
+ struct ctlv_message_input_nest *nests;
+ struct ctlv_message_input_scope *scopes;
+ struct ctlv_message_input_slot *slots;
+ u32 nests_count;
+ u32 nests_capacity;
+ u32 scopes_count;
+ u32 scopes_capacity;
+ u32 slots_count;
+ u32 slots_capacity;
+};
+
+struct ctlv_msg {
+ struct ctlv_attr *root;
+ size_t len;
+ const struct ctlv_schema_tree *tree;
+ const struct ctlv_family *family;
+ struct ctlv_op_ctx *ctx;
+ struct ctlv_message_input_index *index;
+ bool request;
+ int poison;
+};
+
+enum ctlv_message_step {
+ CTLV_MESSAGE_STEP_ATTR,
+ CTLV_MESSAGE_STEP_END,
+ CTLV_MESSAGE_STEP_BAD,
+};
+
+enum ctlv_message_step ctlv_message_next(const struct ctlv_attr *nest,
+ const struct ctlv_attr **child);
+u32 ctlv_message_offset(const struct ctlv_msg *msg,
+ const struct ctlv_attr *attr);
+void ctlv_message_root(struct ctlv_cursor *cursor, struct ctlv_msg *msg);
+void ctlv_message_index_free(struct ctlv_msg *msg);
+bool ctlv_message_input_rebind(struct ctlv_cursor *nest,
+ const struct ctlv_schema_tree *tree, u32 node);
+
+static inline u32 ctlv_message_attr_payload_len(const struct ctlv_attr *attr)
+{
+ return attr->len - sizeof(*attr);
+}
+
+static inline const void *
+ctlv_message_attr_payload(const struct ctlv_attr *attr)
+{
+ return attr + 1;
+}
+
+#endif /* _CTLV_MESSAGE_H */
diff --git a/include/linux/ctlv_family.h b/include/linux/ctlv_family.h
index 1ae4d3a536f7..eae690b9c1d5 100644
--- a/include/linux/ctlv_family.h
+++ b/include/linux/ctlv_family.h
@@ -313,4 +313,142 @@ struct ctlv_family {
extern const struct ctlv_family ctlv_family;
+struct ctlv_cursor *
+ctlv_input_attr(struct ctlv_cursor *cursor, const struct ctlv_cursor *nest,
+ struct ctlv_input_ref ref);
+bool ctlv_input_cursor(const struct ctlv_cursor *cursor,
+ struct ctlv_input_ref ref);
+void ctlv_attr_next(struct ctlv_cursor *cursor);
+bool ctlv_attr_ok(const struct ctlv_cursor *cursor);
+u64 ctlv_attr_u64(const struct ctlv_cursor *cursor);
+s64 ctlv_attr_s64(const struct ctlv_cursor *cursor);
+bool ctlv_attr_bool(const struct ctlv_cursor *cursor);
+const char *ctlv_attr_string(const struct ctlv_cursor *cursor);
+const void *ctlv_attr_binary(const struct ctlv_cursor *cursor, u32 *len);
+
+#define CTLV_IN_REF(_name, _tree, _attr_id, _node, _slot, _kind) \
+static inline struct ctlv_input_ref _name##_ref(void) \
+{ \
+ return (struct ctlv_input_ref){ \
+ .tree = (_tree), \
+ .attr_id = (_attr_id), \
+ .node = (_node), \
+ .slot = (_slot), \
+ .kind = (_kind), \
+ }; \
+}
+
+#define CTLV_IN_FOR_EACH(_name, _attr, _nest) \
+ for (struct ctlv_cursor *__ctlv_attr = \
+ ctlv_input_attr(_attr, _nest, _name##_ref()); \
+ ctlv_attr_ok(__ctlv_attr); \
+ ctlv_attr_next(__ctlv_attr))
+
+#define CTLV_IN_HAS(_name) \
+static inline bool _name##_present(const struct ctlv_cursor *nest) \
+{ \
+ struct ctlv_cursor attr; \
+ \
+ ctlv_input_attr(&attr, nest, _name##_ref()); \
+ return ctlv_attr_ok(&attr); \
+}
+
+#define CTLV_IN_U64(_name) \
+static inline u64 _name##_get(const struct ctlv_cursor *nest) \
+{ \
+ struct ctlv_cursor attr; \
+ \
+ ctlv_input_attr(&attr, nest, _name##_ref()); \
+ return ctlv_attr_u64(&attr); \
+}
+
+#define CTLV_IN_S64(_name) \
+static inline s64 _name##_get(const struct ctlv_cursor *nest) \
+{ \
+ struct ctlv_cursor attr; \
+ \
+ ctlv_input_attr(&attr, nest, _name##_ref()); \
+ return ctlv_attr_s64(&attr); \
+}
+
+#define CTLV_IN_BOOL(_name) \
+static inline bool _name##_get(const struct ctlv_cursor *nest) \
+{ \
+ struct ctlv_cursor attr; \
+ \
+ ctlv_input_attr(&attr, nest, _name##_ref()); \
+ return ctlv_attr_bool(&attr); \
+}
+
+#define CTLV_IN_STRING(_name) \
+static inline const char * \
+_name##_get(const struct ctlv_cursor *nest) \
+{ \
+ struct ctlv_cursor attr; \
+ \
+ ctlv_input_attr(&attr, nest, _name##_ref()); \
+ return ctlv_attr_string(&attr); \
+}
+
+#define CTLV_IN_BINARY(_name) \
+static inline const void * \
+_name##_get(const struct ctlv_cursor *nest, u32 *len) \
+{ \
+ struct ctlv_cursor attr; \
+ \
+ ctlv_input_attr(&attr, nest, _name##_ref()); \
+ return ctlv_attr_binary(&attr, len); \
+}
+
+#define CTLV_IN_NEST(_name) \
+static inline void _name##_get(struct ctlv_cursor *nested, \
+ const struct ctlv_cursor *nest) \
+{ \
+ ctlv_input_attr(nested, nest, _name##_ref()); \
+}
+
+#define CTLV_IN_EACH_U64(_name) \
+static inline u64 _name##_get(const struct ctlv_cursor *attr) \
+{ \
+ if (!ctlv_input_cursor(attr, _name##_ref())) \
+ return 0; \
+ return ctlv_attr_u64(attr); \
+}
+
+#define CTLV_IN_EACH_S64(_name) \
+static inline s64 _name##_get(const struct ctlv_cursor *attr) \
+{ \
+ if (!ctlv_input_cursor(attr, _name##_ref())) \
+ return 0; \
+ return ctlv_attr_s64(attr); \
+}
+
+#define CTLV_IN_EACH_BOOL(_name) \
+static inline bool _name##_get(const struct ctlv_cursor *attr) \
+{ \
+ if (!ctlv_input_cursor(attr, _name##_ref())) \
+ return false; \
+ return ctlv_attr_bool(attr); \
+}
+
+#define CTLV_IN_EACH_STRING(_name) \
+static inline const char * \
+_name##_get(const struct ctlv_cursor *attr) \
+{ \
+ if (!ctlv_input_cursor(attr, _name##_ref())) \
+ return NULL; \
+ return ctlv_attr_string(attr); \
+}
+
+#define CTLV_IN_EACH_BINARY(_name) \
+static inline const void * \
+_name##_get(const struct ctlv_cursor *attr, u32 *len) \
+{ \
+ if (!ctlv_input_cursor(attr, _name##_ref())) { \
+ *len = 0; \
+ return NULL; \
+ } \
+ return ctlv_attr_binary(attr, len); \
+}
+
#endif /* _LINUX_CTLV_FAMILY_H */
--
2.56.0
^ permalink raw reply related [flat|nested] 32+ messages in thread* Re: [PATCH RFC 05/20] ctlv: Prepare message input handling
2026-10-15 14:50 ` [PATCH RFC 05/20] ctlv: Prepare message input handling Jiri Pirko
@ 2026-10-15 15:06 ` sashiko-bot
0 siblings, 0 replies; 32+ messages in thread
From: sashiko-bot @ 2026-10-15 15:06 UTC (permalink / raw)
To: Jiri Pirko; +Cc: kvm
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] ctlv: infinite loop in ctlv_attr_next on out-of-bounds error path
--
commit 5ae11d979c8fbb4e21430610d57f8866b9d74025
Author: Jiri Pirko <jiri@resnulli.us>
ctlv: Prepare message input handling
This commit introduces wire walking, request indexing, and validated input
accessors for CTLV generated family readers. It keeps CONFIG_CTLV unwired
until module dispatch is complete.
> diff --git a/drivers/ctlv/message.c b/drivers/ctlv/message.c
> new file mode 100644
> index 0000000000000..7e5742ba8f708
> --- /dev/null
> +++ b/drivers/ctlv/message.c
[ ... ]
> +void ctlv_attr_next(struct ctlv_cursor *cursor)
> +{
> + const struct ctlv_message_input_slot *slot;
> + const struct ctlv_message_input_scope *scope;
> + const struct ctlv_node_schema *node;
> + const struct ctlv_attr *next;
> + u32 next_nest;
> + u32 next_offset;
> +
> + slot = ctlv_message_cursor_index(cursor);
> + if (!slot || WARN_ON_ONCE(!cursor->attr))
> + return;
> + next = (const void *)((const u8 *)cursor->attr +
> + ALIGN((size_t)cursor->attr->len, CTLV_ALIGNTO));
> + next_offset = ctlv_message_offset(cursor->msg, next);
> + if (next_offset == slot->end_offset) {
> + cursor->attr = NULL;
> + cursor->index_nest_scope = CTLV_MESSAGE_INPUT_INDEX_NONE;
> + return;
> + }
> + if (WARN_ON_ONCE(next_offset > slot->end_offset))
> + return;
[Severity: High]
Does this error path fail to invalidate the cursor, leading to an
inescapable infinite loop during attribute iteration?
In ctlv_attr_next(), if the computed next_offset exceeds slot->end_offset,
the function triggers a WARN_ON_ONCE and returns early. Because it returns
without clearing cursor->attr (unlike the successful termination path which
explicitly sets it to NULL), the cursor's state remains exactly as it was.
When using iterator macros like CTLV_IN_FOR_EACH, the loop continues as long
as ctlv_attr_ok() returns true. Since the cursor state is unmodified, it
evaluates as valid, causing the loop to re-process the exact same state and
hit the error path again, resulting in a hard CPU lockup.
Should the error path explicitly set cursor->attr = NULL before returning
to safely terminate the iteration?
> + cursor->attr = next;
> + node = &cursor->tree->node_schemas[cursor->node];
> + if (node->kind == CTLV_SCHEMA_NEST) {
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261015145053.3664509-1-jiri@resnulli.us?part=5
^ permalink raw reply [flat|nested] 32+ messages in thread
* [PATCH RFC 06/20] ctlv: Prepare blob descriptor validation
2026-10-15 14:50 [PATCH RFC 00/20] attestation: Add a common subsystem and TDX support Jiri Pirko
` (4 preceding siblings ...)
2026-10-15 14:50 ` [PATCH RFC 05/20] ctlv: Prepare message input handling Jiri Pirko
@ 2026-10-15 14:50 ` Jiri Pirko
2026-10-15 14:50 ` [PATCH RFC 07/20] ctlv: Prepare schema validation Jiri Pirko
` (13 subsequent siblings)
19 siblings, 0 replies; 32+ messages in thread
From: Jiri Pirko @ 2026-10-15 14:50 UTC (permalink / raw)
To: linux-coco
Cc: linux-kernel, linux-api, x86, kvm, linux-doc, jgg, david.quigley,
lukas, ksinyuk, rodrigo.vivi, cedric.xing, sameo, drdeeglaze,
James.Bottomley, aik, jpiotrowski, qinkun, pgonda, jic23,
alistair.francis, ankita, suzuki.poulose, kas, rick.p.edgecombe,
elena.reshetova, dave.hansen, sathyanarayanan.kuppuswamy,
peter.fang
From: Jiri Pirko <jiri@nvidia.com>
Add parsing and VA checks for blob descriptors shared by schema
validation and runtime acquisition.
Keep CONFIG_CTLV unwired until dispatch completes the module.
Assisted-by: Cursor: GPT-5.6 Sol
Assisted-by: Cursor: Claude Opus 5.5
Signed-off-by: Jiri Pirko <jiri@nvidia.com>
---
drivers/ctlv/Makefile | 2 +-
drivers/ctlv/blob_desc.c | 102 +++++++++++++++++++++++++++++++++++++++
drivers/ctlv/blob_desc.h | 29 +++++++++++
3 files changed, 132 insertions(+), 1 deletion(-)
create mode 100644 drivers/ctlv/blob_desc.c
create mode 100644 drivers/ctlv/blob_desc.h
diff --git a/drivers/ctlv/Makefile b/drivers/ctlv/Makefile
index 4f0fe41bc9e1..5edc0b4b973f 100644
--- a/drivers/ctlv/Makefile
+++ b/drivers/ctlv/Makefile
@@ -1,4 +1,4 @@
# SPDX-License-Identifier: GPL-2.0
obj-$(CONFIG_CTLV) += ctlv.o
-ctlv-y += message.o
+ctlv-y += blob_desc.o message.o
diff --git a/drivers/ctlv/blob_desc.c b/drivers/ctlv/blob_desc.c
new file mode 100644
index 000000000000..b84be3d723ab
--- /dev/null
+++ b/drivers/ctlv/blob_desc.c
@@ -0,0 +1,102 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES
+ *
+ * Blob descriptor parsing and validation.
+ */
+#include <linux/align.h>
+#include <linux/bits.h>
+#include <linux/overflow.h>
+#include <linux/types.h>
+
+#include <uapi/linux/ctlv.h>
+#include <uapi/linux/ctlv_gen.h>
+
+#include "blob_desc.h"
+#include "message.h"
+
+static int ctlv_blob_member(u64 attr_id)
+{
+ switch (attr_id) {
+ case CTLV_ATTR_BLOB_TYPE:
+ return CTLV_BLOB_MEMBER_TYPE;
+ case CTLV_ATTR_BLOB_ADDRESS:
+ return CTLV_BLOB_MEMBER_ADDRESS;
+ case CTLV_ATTR_BLOB_FD:
+ return CTLV_BLOB_MEMBER_FD;
+ case CTLV_ATTR_BLOB_OFFSET:
+ return CTLV_BLOB_MEMBER_OFFSET;
+ case CTLV_ATTR_BLOB_LENGTH:
+ return CTLV_BLOB_MEMBER_LENGTH;
+ default:
+ return -1;
+ }
+}
+
+u32 ctlv_blob_desc_read(const struct ctlv_attr *blob, struct ctlv_blob_desc *desc,
+ const struct ctlv_attr **bad)
+{
+ const struct ctlv_attr *child = NULL;
+ enum ctlv_message_step step;
+
+ *desc = (struct ctlv_blob_desc){};
+ while ((step = ctlv_message_next(blob, &child)) == CTLV_MESSAGE_STEP_ATTR) {
+ int member = ctlv_blob_member(child->attr_id);
+
+ *bad = child;
+ if (child->flags & ~CTLV_ATTR_F_NESTED)
+ return CTLV_VALIDATION_REASON_UNKNOWN_FLAG;
+ if (child->flags & CTLV_ATTR_F_NESTED)
+ return CTLV_VALIDATION_REASON_MALFORMED_MESSAGE;
+ if (member < 0)
+ return CTLV_VALIDATION_REASON_UNKNOWN_ATTR;
+ if (ctlv_message_attr_payload_len(child) != sizeof(u64))
+ return CTLV_VALIDATION_REASON_BAD_LENGTH;
+ if (desc->present & BIT(member))
+ return CTLV_VALIDATION_REASON_BAD_BLOB_DESCRIPTOR;
+ desc->present |= BIT(member);
+ desc->member[member] = *(const u64 *)ctlv_message_attr_payload(child);
+ }
+ *bad = NULL;
+ if (step == CTLV_MESSAGE_STEP_BAD)
+ return CTLV_VALIDATION_REASON_MALFORMED_MESSAGE;
+ return 0;
+}
+
+static u32 ctlv_blob_variant(const struct ctlv_blob_desc *desc)
+{
+ u32 required = BIT(CTLV_BLOB_MEMBER_TYPE) | BIT(CTLV_BLOB_MEMBER_LENGTH);
+
+ switch (desc->member[CTLV_BLOB_MEMBER_TYPE]) {
+ case CTLV_BLOB_TYPE_VA:
+ required |= BIT(CTLV_BLOB_MEMBER_ADDRESS);
+ break;
+ default:
+ return CTLV_VALIDATION_REASON_BAD_BLOB_DESCRIPTOR;
+ }
+ if (desc->present != required)
+ return CTLV_VALIDATION_REASON_BAD_BLOB_DESCRIPTOR;
+ return 0;
+}
+
+u32 ctlv_blob_desc_check(const struct ctlv_blob_desc *desc, const struct ctlv_node_schema *node)
+{
+ u64 start, length, end;
+ u32 reason;
+
+ reason = ctlv_blob_variant(desc);
+ if (reason)
+ return reason;
+ if (!(node->blob.backings & BIT(desc->member[CTLV_BLOB_MEMBER_TYPE])))
+ return CTLV_VALIDATION_REASON_BAD_BLOB_DESCRIPTOR;
+
+ length = desc->member[CTLV_BLOB_MEMBER_LENGTH];
+ if (!length || length > node->blob.max_len)
+ return CTLV_VALIDATION_REASON_BAD_BLOB_DESCRIPTOR;
+ start = desc->member[desc->present & BIT(CTLV_BLOB_MEMBER_ADDRESS) ?
+ CTLV_BLOB_MEMBER_ADDRESS :
+ CTLV_BLOB_MEMBER_OFFSET];
+ if (!IS_ALIGNED(start, node->blob.alignment) || check_add_overflow(start, length, &end))
+ return CTLV_VALIDATION_REASON_BAD_BLOB_DESCRIPTOR;
+ return 0;
+}
diff --git a/drivers/ctlv/blob_desc.h b/drivers/ctlv/blob_desc.h
new file mode 100644
index 000000000000..27f77af16f01
--- /dev/null
+++ b/drivers/ctlv/blob_desc.h
@@ -0,0 +1,29 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef _CTLV_BLOB_DESC_H
+#define _CTLV_BLOB_DESC_H
+
+#include <linux/ctlv_family.h>
+#include <linux/types.h>
+
+#include <uapi/linux/ctlv.h>
+
+/* The members of a descriptor, in the order the root family declares them. */
+enum {
+ CTLV_BLOB_MEMBER_TYPE,
+ CTLV_BLOB_MEMBER_ADDRESS,
+ CTLV_BLOB_MEMBER_FD,
+ CTLV_BLOB_MEMBER_OFFSET,
+ CTLV_BLOB_MEMBER_LENGTH,
+ CTLV_BLOB_MEMBERS,
+};
+
+struct ctlv_blob_desc {
+ u64 member[CTLV_BLOB_MEMBERS];
+ u32 present;
+};
+
+u32 ctlv_blob_desc_read(const struct ctlv_attr *blob, struct ctlv_blob_desc *desc,
+ const struct ctlv_attr **bad);
+u32 ctlv_blob_desc_check(const struct ctlv_blob_desc *desc, const struct ctlv_node_schema *node);
+
+#endif /* _CTLV_BLOB_DESC_H */
--
2.56.0
^ permalink raw reply related [flat|nested] 32+ messages in thread* [PATCH RFC 07/20] ctlv: Prepare schema validation
2026-10-15 14:50 [PATCH RFC 00/20] attestation: Add a common subsystem and TDX support Jiri Pirko
` (5 preceding siblings ...)
2026-10-15 14:50 ` [PATCH RFC 06/20] ctlv: Prepare blob descriptor validation Jiri Pirko
@ 2026-10-15 14:50 ` Jiri Pirko
2026-10-15 15:14 ` sashiko-bot
2026-10-15 14:50 ` [PATCH RFC 08/20] ctlv: Prepare message output handling Jiri Pirko
` (12 subsequent siblings)
19 siblings, 1 reply; 32+ messages in thread
From: Jiri Pirko @ 2026-10-15 14:50 UTC (permalink / raw)
To: linux-coco
Cc: linux-kernel, linux-api, x86, kvm, linux-doc, jgg, david.quigley,
lukas, ksinyuk, rodrigo.vivi, cedric.xing, sameo, drdeeglaze,
James.Bottomley, aik, jpiotrowski, qinkun, pgonda, jic23,
alistair.francis, ankita, suzuki.poulose, kas, rick.p.edgecombe,
elena.reshetova, dave.hansen, sathyanarayanan.kuppuswamy,
peter.fang
From: Jiri Pirko <jiri@nvidia.com>
Add generated-schema walking, message validation and request indexing.
Use the descriptor layer for blob placement checks.
Keep CONFIG_CTLV unwired until dispatch completes the module.
Assisted-by: Cursor: GPT-5.6 Sol
Assisted-by: Cursor: Claude Opus 5.5
Signed-off-by: Jiri Pirko <jiri@nvidia.com>
---
drivers/ctlv/Makefile | 2 +-
drivers/ctlv/schema.c | 1047 +++++++++++++++++++++++++++++++++++++++++
drivers/ctlv/schema.h | 50 ++
3 files changed, 1098 insertions(+), 1 deletion(-)
create mode 100644 drivers/ctlv/schema.c
create mode 100644 drivers/ctlv/schema.h
diff --git a/drivers/ctlv/Makefile b/drivers/ctlv/Makefile
index 5edc0b4b973f..3fd656835b8a 100644
--- a/drivers/ctlv/Makefile
+++ b/drivers/ctlv/Makefile
@@ -1,4 +1,4 @@
# SPDX-License-Identifier: GPL-2.0
obj-$(CONFIG_CTLV) += ctlv.o
-ctlv-y += blob_desc.o message.o
+ctlv-y += blob_desc.o message.o schema.o
diff --git a/drivers/ctlv/schema.c b/drivers/ctlv/schema.c
new file mode 100644
index 000000000000..b74f9a358bc3
--- /dev/null
+++ b/drivers/ctlv/schema.c
@@ -0,0 +1,1047 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES
+ *
+ * Generated metadata remains owner-local; effective schemas walk the chain.
+ */
+#include <linux/align.h>
+#include <linux/bits.h>
+#include <linux/ctlv.h>
+#include <linux/err.h>
+#include <linux/errno.h>
+#include <linux/limits.h>
+#include <linux/minmax.h>
+#include <linux/overflow.h>
+#include <linux/slab.h>
+#include <linux/string.h>
+#include <linux/types.h>
+
+#include <uapi/linux/ctlv.h>
+#include <uapi/linux/ctlv_gen.h>
+
+#include "blob_desc.h"
+#include "internal.h"
+#include "message.h"
+#include "schema.h"
+
+/* Every 64-bit payload, and every blob descriptor member, is this long. */
+#define CTLV_SCHEMA_SCALAR_ATTR_LEN (sizeof(struct ctlv_attr) + sizeof(u64))
+
+/*
+ * Where composing a tree's maximum length gives up, which keeps the check on a
+ * composed length from being passed by a total that wrapped.
+ */
+#define CTLV_SCHEMA_MAX_COMPOSED_LEN CTLV_MAX_EVENT_QUEUE_LEN
+
+const struct ctlv_family *ctlv_family_ancestor(const struct ctlv_family *family, u32 up)
+{
+ while (up-- && family)
+ family = family->parent;
+ return family;
+}
+
+u32 ctlv_family_levels(const struct ctlv_family *family)
+{
+ const struct ctlv_family *ancestor;
+ u32 levels = 0;
+
+ for (ancestor = family; ancestor; ancestor = ancestor->parent)
+ levels++;
+ return levels;
+}
+
+void ctlv_schema_cursor_init(struct ctlv_schema_cursor *cursor, const struct ctlv_family *family)
+{
+ cursor->target = family;
+ cursor->level = ctlv_family_levels(family);
+ cursor->family = NULL;
+ cursor->index = 0;
+}
+
+const struct ctlv_op_schema *ctlv_schema_next_op(struct ctlv_schema_cursor *cursor,
+ const struct ctlv_family **family)
+{
+ while (cursor->level) {
+ const struct ctlv_family_schema *schema;
+
+ if (!cursor->family)
+ cursor->family = ctlv_family_ancestor(cursor->target, cursor->level - 1);
+ schema = cursor->family->schema;
+ if (cursor->index < schema->ops_count) {
+ *family = cursor->family;
+ return &schema->ops[cursor->index++];
+ }
+ cursor->level--;
+ cursor->family = NULL;
+ cursor->index = 0;
+ }
+ return NULL;
+}
+
+bool ctlv_schema_member(const struct ctlv_family *family, const struct ctlv_schema_tree *tree,
+ u32 node, u64 attr_id, struct ctlv_schema_pos *member)
+{
+ const struct ctlv_node_schema *nest;
+ u32 index;
+
+ if (!tree->node_schemas || node >= tree->node_schemas_count)
+ return false;
+ nest = &tree->node_schemas[node];
+
+ for (index = 0; index < nest->children_count; index++) {
+ u32 child = nest->first_child + index;
+
+ if (tree->node_schemas[child].attr_id == attr_id) {
+ *member = (struct ctlv_schema_pos){
+ .tree = tree,
+ .node = child,
+ };
+ return true;
+ }
+ }
+ for (index = 0; index < tree->stub_schemas_count; index++) {
+ const struct ctlv_stub_schema *stub_schema = &tree->stub_schemas[index];
+ const struct ctlv_stub_fill_schema *fill_schema;
+
+ if (stub_schema->node != node)
+ continue;
+ fill_schema = ctlv_schema_fill(family, stub_schema->family_id, stub_schema->set_id);
+ /* Filled-stub members are children of the fill schema's root. */
+ if (fill_schema &&
+ ctlv_schema_member(family, &fill_schema->members, 0, attr_id, member))
+ return true;
+ }
+ return false;
+}
+
+const struct ctlv_stub_fill_schema *ctlv_schema_fill(const struct ctlv_family *family,
+ u16 family_id, u32 set_id)
+{
+ const struct ctlv_family *owner;
+ u32 index;
+
+ for (owner = family; owner; owner = owner->parent) {
+ const struct ctlv_family_schema *schema = owner->schema;
+
+ for (index = 0; index < schema->stub_fill_schemas_count; index++)
+ if (schema->stub_fill_schemas[index].family_id == family_id &&
+ schema->stub_fill_schemas[index].set_id == set_id)
+ return &schema->stub_fill_schemas[index];
+ }
+ return NULL;
+}
+
+/*
+ * The same arithmetic the generator did, over the tree it emitted plus this
+ * family's stub fill schemas, since a fill makes an extension point longer.
+ */
+
+struct ctlv_schema_length_walk {
+ const struct ctlv_family *family;
+ bool bad;
+};
+
+static u64 ctlv_schema_children_max_len(struct ctlv_schema_length_walk *walk,
+ const struct ctlv_schema_tree *tree, u32 index, u32 depth);
+
+static u64 ctlv_schema_payload_max_len(struct ctlv_schema_length_walk *walk,
+ const struct ctlv_schema_tree *tree, u32 index, u32 depth)
+{
+ const struct ctlv_node_schema *node = &tree->node_schemas[index];
+
+ switch (node->kind) {
+ case CTLV_SCHEMA_UINT:
+ case CTLV_SCHEMA_SINT:
+ case CTLV_SCHEMA_ENUM:
+ case CTLV_SCHEMA_FLAGS:
+ return sizeof(u64);
+ case CTLV_SCHEMA_BOOL:
+ return 1;
+ case CTLV_SCHEMA_STRING:
+ /*
+ * One terminating NUL, which the declared maximum excludes.
+ * Widened first so that a maximum at U32_MAX cannot wrap.
+ */
+ return (u64)node->length.max_len + 1;
+ case CTLV_SCHEMA_BINARY:
+ return node->length.max_len;
+ case CTLV_SCHEMA_BLOB:
+ /*
+ * Type, length and an address; anything but a bare VA is one
+ * member wider, an fd with an optional offset.
+ */
+ return (node->blob.backings == BIT(CTLV_BLOB_TYPE_VA) ? 3 : 4) *
+ ALIGN(CTLV_SCHEMA_SCALAR_ATTR_LEN, CTLV_ALIGNTO);
+ default:
+ return ctlv_schema_children_max_len(walk, tree, index, depth);
+ }
+}
+
+static u64 ctlv_schema_attr_max_len(struct ctlv_schema_length_walk *walk,
+ const struct ctlv_schema_tree *tree, u32 index, u32 depth)
+{
+ return sizeof(struct ctlv_attr) + ctlv_schema_payload_max_len(walk, tree, index, depth);
+}
+
+static u64 ctlv_schema_children_max_len(struct ctlv_schema_length_walk *walk,
+ const struct ctlv_schema_tree *tree, u32 index, u32 depth)
+{
+ const struct ctlv_node_schema *node = &tree->node_schemas[index];
+ u64 total = 0;
+ u32 i;
+
+ if (depth > CTLV_MAX_DECLARED_NEST_DEPTH) {
+ walk->bad = true;
+ return 0;
+ }
+ for (i = 0; i < node->children_count; i++) {
+ u32 child = node->first_child + i;
+ u64 each, all;
+
+ /*
+ * Neither an unlimited placement nor one that re-enters has a
+ * maximum, and only a query reply may hold either.
+ */
+ if (tree->node_schemas[child].reenters ||
+ !tree->node_schemas[child].max_occurrences) {
+ walk->bad = true;
+ return 0;
+ }
+ each = ALIGN(ctlv_schema_attr_max_len(walk, tree, child, depth + 1), CTLV_ALIGNTO);
+ if (check_mul_overflow((u64)tree->node_schemas[child].max_occurrences, each,
+ &all) ||
+ check_add_overflow(total, all, &total) ||
+ total > CTLV_SCHEMA_MAX_COMPOSED_LEN) {
+ walk->bad = true;
+ return 0;
+ }
+ }
+ for (i = 0; i < tree->stub_schemas_count; i++) {
+ const struct ctlv_stub_schema *stub_schema = &tree->stub_schemas[i];
+ const struct ctlv_stub_fill_schema *fill_schema;
+
+ if (stub_schema->node != index)
+ continue;
+ fill_schema =
+ ctlv_schema_fill(walk->family, stub_schema->family_id, stub_schema->set_id);
+ if (!fill_schema)
+ continue;
+ if (check_add_overflow(total,
+ ctlv_schema_children_max_len(walk, &fill_schema->members, 0,
+ depth),
+ &total) ||
+ total > CTLV_SCHEMA_MAX_COMPOSED_LEN) {
+ walk->bad = true;
+ return 0;
+ }
+ }
+ return total;
+}
+
+/* Buffer length that the largest indivisible leaf and its nests need. */
+static u64 ctlv_schema_min_len(struct ctlv_schema_length_walk *walk,
+ const struct ctlv_schema_tree *tree, u32 index, u32 above, u32 depth)
+{
+ const struct ctlv_node_schema *node = &tree->node_schemas[index];
+ bool has_children = false;
+ u64 deepest = 0;
+ u32 i;
+
+ if (depth > CTLV_MAX_DECLARED_NEST_DEPTH) {
+ walk->bad = true;
+ return 0;
+ }
+ for (i = 0; i < node->children_count; i++) {
+ u32 child = node->first_child + i;
+
+ has_children = true;
+ deepest = max(deepest,
+ ctlv_schema_min_len(walk, tree, child,
+ above + sizeof(struct ctlv_attr), depth + 1));
+ }
+ for (i = 0; i < tree->stub_schemas_count; i++) {
+ const struct ctlv_stub_schema *stub_schema = &tree->stub_schemas[i];
+ const struct ctlv_stub_fill_schema *fill_schema;
+ const struct ctlv_node_schema *root;
+ u32 member;
+
+ if (stub_schema->node != index)
+ continue;
+ fill_schema =
+ ctlv_schema_fill(walk->family, stub_schema->family_id, stub_schema->set_id);
+ if (!fill_schema)
+ continue;
+ root = &fill_schema->members.node_schemas[0];
+ for (member = 0; member < root->children_count; member++) {
+ has_children = true;
+ deepest = max(deepest, ctlv_schema_min_len(walk, &fill_schema->members,
+ root->first_child + member,
+ above + sizeof(struct ctlv_attr),
+ depth + 1));
+ }
+ }
+ if (has_children)
+ return deepest;
+ /* An empty root is a message of its own header alone. */
+ if (!index)
+ return above + sizeof(struct ctlv_attr);
+ return above + ALIGN(ctlv_schema_attr_max_len(walk, tree, index, depth), CTLV_ALIGNTO);
+}
+
+static u64 ctlv_schema_root_max_len(struct ctlv_schema_length_walk *walk,
+ const struct ctlv_schema_tree *tree)
+{
+ return sizeof(struct ctlv_attr) + ctlv_schema_children_max_len(walk, tree, 0, 0);
+}
+
+/* Size one deepest legal leaf and every nest above it. */
+static u64 ctlv_schema_reentering_min_len(struct ctlv_schema_length_walk *walk,
+ const struct ctlv_schema_tree *tree)
+{
+ u64 leaf = 0;
+ u32 index;
+
+ for (index = 1; index < tree->node_schemas_count; index++) {
+ if (tree->node_schemas[index].kind == CTLV_SCHEMA_NEST)
+ continue;
+ leaf = max(leaf,
+ ALIGN(ctlv_schema_attr_max_len(walk, tree, index, 0), CTLV_ALIGNTO));
+ }
+ return (CTLV_MAX_NEST_DEPTH + 1) * sizeof(struct ctlv_attr) + leaf;
+}
+
+static bool ctlv_schema_tree_reenters(const struct ctlv_schema_tree *tree)
+{
+ u32 index;
+
+ for (index = 0; index < tree->node_schemas_count; index++)
+ if (tree->node_schemas[index].reenters)
+ return true;
+ return false;
+}
+
+int ctlv_schema_op_sizes(const struct ctlv_family *family, const struct ctlv_op_schema *op,
+ struct ctlv_op_sizes *sizes)
+{
+ struct ctlv_schema_length_walk walk = { .family = family };
+ u64 request = 0, reply = 0, record = 0;
+
+ if (op->request.node_schemas)
+ request = ctlv_schema_root_max_len(&walk, &op->request);
+
+ switch (op->type) {
+ case CTLV_OP_TYPE_ACTION:
+ if (op->reply.node_schemas)
+ reply = ctlv_schema_root_max_len(&walk, &op->reply);
+ break;
+ case CTLV_OP_TYPE_QUERY:
+ if (ctlv_schema_tree_reenters(&op->reply))
+ reply = ctlv_schema_reentering_min_len(&walk, &op->reply);
+ else
+ reply = ctlv_schema_min_len(&walk, &op->reply, 0, 0, 0);
+ break;
+ case CTLV_OP_TYPE_EVENT:
+ record = sizeof(struct ctlv_event_hdr) +
+ ALIGN(ctlv_schema_root_max_len(&walk, &op->reply), CTLV_ALIGNTO);
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ if (walk.bad || request > CTLV_MAX_INLINE_MESSAGE_LEN ||
+ reply > CTLV_MAX_INLINE_MESSAGE_LEN || record > CTLV_MAX_EVENT_QUEUE_LEN)
+ return -EINVAL;
+
+ *sizes = (struct ctlv_op_sizes){ .request_max_len = request };
+ switch (op->type) {
+ case CTLV_OP_TYPE_ACTION:
+ sizes->action_reply_max_len = reply;
+ break;
+ case CTLV_OP_TYPE_QUERY:
+ sizes->query_reply_min_len = reply;
+ /*
+ * What the specification asked for, unless a filled extension
+ * point made one fragment longer than that.
+ */
+ sizes->query_reply_suggested_len =
+ max_t(u32, op->sizes.query_reply_suggested_len, reply);
+ break;
+ default:
+ sizes->event_record_max_len = record;
+ break;
+ }
+ return 0;
+}
+
+/*
+ * Only a completely valid request reaches family code; nothing is repaired or
+ * normalized. The first failure is the one recorded, and the walk stops there.
+ */
+
+struct ctlv_schema_validation {
+ struct ctlv_msg *msg;
+ struct ctlv_error *error;
+};
+
+void ctlv_message_index_free(struct ctlv_msg *msg)
+{
+ struct ctlv_message_input_index *index;
+
+ if (!msg || !msg->index)
+ return;
+ index = msg->index;
+ kvfree(index->slots);
+ kvfree(index->scopes);
+ kvfree(index->nests);
+ kfree(index);
+ msg->index = NULL;
+}
+
+static void *ctlv_schema_index_grow(void *items, u32 *capacity, u32 needed, size_t item_size)
+{
+ u32 grown = max_t(u32, *capacity, 16);
+ size_t bytes;
+ void *new;
+
+ if (needed <= *capacity)
+ return items;
+ while (grown < needed) {
+ if (grown > U32_MAX / 2)
+ return ERR_PTR(-EOVERFLOW);
+ grown *= 2;
+ }
+ if (check_mul_overflow((size_t)grown, item_size, &bytes))
+ return ERR_PTR(-EOVERFLOW);
+ new = kvrealloc(items, bytes, GFP_KERNEL_ACCOUNT);
+ if (!new)
+ return ERR_PTR(-ENOMEM);
+ *capacity = grown;
+ return new;
+}
+
+static int ctlv_schema_index_slots_add(struct ctlv_message_input_index *index, u32 count,
+ u32 *first)
+{
+ struct ctlv_message_input_slot *slots;
+ u32 needed;
+ u32 i;
+
+ if (check_add_overflow(index->slots_count, count, &needed))
+ return -EOVERFLOW;
+ slots = ctlv_schema_index_grow(index->slots, &index->slots_capacity, needed,
+ sizeof(*slots));
+ if (IS_ERR(slots))
+ return PTR_ERR(slots);
+ index->slots = slots;
+ *first = index->slots_count;
+ for (i = index->slots_count; i < needed; i++)
+ index->slots[i] = (struct ctlv_message_input_slot){
+ .excess_offset = CTLV_MESSAGE_INPUT_INDEX_NONE,
+ .first_nest = CTLV_MESSAGE_INPUT_INDEX_NONE,
+ .last_nest = CTLV_MESSAGE_INPUT_INDEX_NONE,
+ };
+ index->slots_count = needed;
+ return 0;
+}
+
+static int ctlv_schema_index_scope_add(struct ctlv_schema_validation *v, u32 nest_index,
+ const struct ctlv_schema_tree *tree, u32 node, bool optional)
+{
+ struct ctlv_message_input_index *index = v->msg->index;
+ const struct ctlv_node_schema *parent = &tree->node_schemas[node];
+ struct ctlv_message_input_scope *scopes;
+ u32 needed;
+ u32 first_slot;
+ u32 scope_index;
+ u32 i;
+ int ret;
+
+ if (check_add_overflow(index->scopes_count, 1U, &needed))
+ return -EOVERFLOW;
+ scopes = ctlv_schema_index_grow(index->scopes, &index->scopes_capacity, needed,
+ sizeof(*scopes));
+ if (IS_ERR(scopes))
+ return PTR_ERR(scopes);
+ index->scopes = scopes;
+ ret = ctlv_schema_index_slots_add(index, parent->children_count, &first_slot);
+ if (ret)
+ return ret;
+ scope_index = index->scopes_count++;
+ index->scopes[scope_index] = (struct ctlv_message_input_scope){
+ .tree = tree,
+ .nest = nest_index,
+ .node = node,
+ .first_slot = first_slot,
+ .slots_count = parent->children_count,
+ .optional = optional,
+ };
+ index->nests[nest_index].scopes_count++;
+
+ for (i = 0; i < tree->stub_schemas_count; i++) {
+ const struct ctlv_stub_schema *stub_schema = &tree->stub_schemas[i];
+ const struct ctlv_stub_fill_schema *fill_schema;
+
+ if (stub_schema->node != node)
+ continue;
+ fill_schema = ctlv_schema_fill(v->msg->family, stub_schema->family_id,
+ stub_schema->set_id);
+ if (!fill_schema)
+ continue;
+ ret = ctlv_schema_index_scope_add(v, nest_index, &fill_schema->members, 0,
+ optional || stub_schema->optional);
+ if (ret)
+ return ret;
+ }
+ return 0;
+}
+
+static int ctlv_schema_index_nest_add(struct ctlv_schema_validation *v,
+ const struct ctlv_schema_tree *tree, u32 node,
+ const struct ctlv_attr *nest_attr, u32 *nest_index)
+{
+ struct ctlv_message_input_index *index = v->msg->index;
+ struct ctlv_message_input_nest *nests;
+ u32 needed;
+ u32 added;
+ int ret;
+
+ if (check_add_overflow(index->nests_count, 1U, &needed))
+ return -EOVERFLOW;
+ nests = ctlv_schema_index_grow(index->nests, &index->nests_capacity, needed,
+ sizeof(*nests));
+ if (IS_ERR(nests))
+ return PTR_ERR(nests);
+ index->nests = nests;
+ added = index->nests_count++;
+ index->nests[added] = (struct ctlv_message_input_nest){
+ .offset = ctlv_message_offset(v->msg, nest_attr),
+ .first_scope = index->scopes_count,
+ .next = CTLV_MESSAGE_INPUT_INDEX_NONE,
+ };
+ ret = ctlv_schema_index_scope_add(v, added, tree, node, false);
+ if (ret)
+ return ret;
+ *nest_index = added;
+ return 0;
+}
+
+/* A request is never a fragment: only a reply of a query is ever incomplete. */
+#define CTLV_SCHEMA_ATTR_F_REQUEST (CTLV_ATTR_F_NESTED | CTLV_ATTR_F_IGNORE_IF_UNKNOWN)
+#define CTLV_SCHEMA_ATTR_F_REPLY CTLV_ATTR_F_NESTED
+
+/*
+ * Record the first failure and refuse the message. @attr is NULL when what is
+ * wrong with the nest is something it does not carry.
+ */
+static int ctlv_schema_bad(struct ctlv_schema_validation *v, u32 reason,
+ const struct ctlv_attr *nest, const struct ctlv_attr *attr)
+{
+ *v->error = (struct ctlv_error){
+ .reason = reason,
+ .bad_attr_offset = ctlv_message_offset(v->msg, attr ?: nest),
+ .nest_offset = ctlv_message_offset(v->msg, nest),
+ };
+ return -EINVAL;
+}
+
+/* The one failure that names an attr the message does not contain. */
+static int ctlv_schema_bad_missing(struct ctlv_schema_validation *v, const struct ctlv_attr *nest,
+ u64 attr_id)
+{
+ ctlv_schema_bad(v, CTLV_VALIDATION_REASON_MISSING_ATTR, nest, NULL);
+ v->error->missing_attr_id = attr_id;
+ return -EINVAL;
+}
+
+static struct ctlv_message_input_slot *ctlv_schema_index_slot(struct ctlv_schema_validation *v,
+ u32 nest_index,
+ const struct ctlv_schema_pos *pos)
+{
+ struct ctlv_message_input_index *index = v->msg->index;
+ const struct ctlv_message_input_nest *nest;
+ u32 i;
+
+ if (WARN_ON_ONCE(nest_index >= index->nests_count))
+ return NULL;
+ nest = &index->nests[nest_index];
+ for (i = 0; i < nest->scopes_count; i++) {
+ const struct ctlv_message_input_scope *scope =
+ &index->scopes[nest->first_scope + i];
+ const struct ctlv_node_schema *parent;
+ u32 slot;
+
+ if (scope->tree != pos->tree)
+ continue;
+ parent = &scope->tree->node_schemas[scope->node];
+ if (pos->node < parent->first_child ||
+ pos->node >= parent->first_child + scope->slots_count)
+ continue;
+ slot = pos->node - parent->first_child;
+ return &index->slots[scope->first_slot + slot];
+ }
+ WARN_ON_ONCE(1);
+ return NULL;
+}
+
+static int ctlv_schema_index_record(struct ctlv_schema_validation *v, u32 nest_index,
+ const struct ctlv_schema_pos *pos, const struct ctlv_attr *attr,
+ bool *seen)
+{
+ struct ctlv_message_input_slot *slot = ctlv_schema_index_slot(v, nest_index, pos);
+ const struct ctlv_node_schema *schema = &pos->tree->node_schemas[pos->node];
+ u32 offset = ctlv_message_offset(v->msg, attr);
+
+ if (!slot)
+ return -EINVAL;
+ *seen = slot->count;
+ if (!slot->count)
+ slot->first_offset = offset;
+ slot->count++;
+ if (schema->max_occurrences && slot->count > schema->max_occurrences &&
+ slot->excess_offset == CTLV_MESSAGE_INPUT_INDEX_NONE)
+ slot->excess_offset = offset;
+ slot->end_offset = offset + ALIGN((size_t)attr->len, CTLV_ALIGNTO);
+ return 0;
+}
+
+static void ctlv_schema_index_nest_link(struct ctlv_message_input_index *index,
+ struct ctlv_message_input_slot *slot, u32 nest_index)
+{
+ if (slot->first_nest == CTLV_MESSAGE_INPUT_INDEX_NONE)
+ slot->first_nest = nest_index;
+ else
+ index->nests[slot->last_nest].next = nest_index;
+ slot->last_nest = nest_index;
+}
+
+static int ctlv_schema_index_required(struct ctlv_schema_validation *v, u32 nest_index,
+ const struct ctlv_attr *nest_attr)
+{
+ const struct ctlv_message_input_index *index = v->msg->index;
+ const struct ctlv_message_input_nest *nest = &index->nests[nest_index];
+ u32 i;
+
+ for (i = 0; i < nest->scopes_count; i++) {
+ const struct ctlv_message_input_scope *scope =
+ &index->scopes[nest->first_scope + i];
+ const struct ctlv_node_schema *parent = &scope->tree->node_schemas[scope->node];
+ u32 slot;
+
+ for (slot = 0; slot < scope->slots_count; slot++) {
+ const struct ctlv_message_input_slot *indexed;
+ const struct ctlv_node_schema *child =
+ &scope->tree->node_schemas[parent->first_child + slot];
+ const struct ctlv_attr *excess;
+
+ indexed = &index->slots[scope->first_slot + slot];
+ if (!indexed->count && slot < parent->required_children_count &&
+ !scope->optional)
+ return ctlv_schema_bad_missing(v, nest_attr, child->attr_id);
+ if (!child->max_occurrences || indexed->count <= child->max_occurrences)
+ continue;
+ if (WARN_ON_ONCE(indexed->excess_offset == CTLV_MESSAGE_INPUT_INDEX_NONE))
+ return -EINVAL;
+ excess = (const void *)((const u8 *)v->msg->root + indexed->excess_offset);
+ return ctlv_schema_bad(v, CTLV_VALIDATION_REASON_TOO_MANY_OCCURRENCES,
+ nest_attr, excess);
+ }
+ }
+ return 0;
+}
+
+/* The bits an attr of this message may carry, whichever way it travelled. */
+static u32 ctlv_schema_flags_allowed(const struct ctlv_schema_validation *v)
+{
+ return v->msg->request ? CTLV_SCHEMA_ATTR_F_REQUEST : CTLV_SCHEMA_ATTR_F_REPLY;
+}
+
+static u64 ctlv_schema_scalar_value(const struct ctlv_attr *attr)
+{
+ /* Aligned by the wire format: the root began an allocation. */
+ return *(const u64 *)ctlv_message_attr_payload(attr);
+}
+
+bool ctlv_schema_value_ok(const struct ctlv_node_schema *node, u64 value)
+{
+ switch (node->kind) {
+ case CTLV_SCHEMA_SINT:
+ return (s64)value >= (s64)node->range.min && (s64)value <= (s64)node->range.max;
+ case CTLV_SCHEMA_FLAGS:
+ /* A bit no family declared is one nothing can be told about. */
+ return !(value & ~node->flags_mask);
+ case CTLV_SCHEMA_BOOL:
+ return value <= 1;
+ default:
+ /*
+ * An enum's values are numbered from one in declaration order
+ * and never skipped, so its range bounds it like an integer.
+ */
+ return value >= node->range.min && value <= node->range.max;
+ }
+}
+
+bool ctlv_schema_length_ok(const struct ctlv_node_schema *node, u32 len)
+{
+ return len >= node->length.min_len && len <= node->length.max_len;
+}
+
+static int ctlv_schema_check_integer(struct ctlv_schema_validation *v,
+ const struct ctlv_node_schema *node,
+ const struct ctlv_attr *nest, const struct ctlv_attr *attr)
+{
+ u64 value;
+
+ if (ctlv_message_attr_payload_len(attr) != sizeof(value))
+ return ctlv_schema_bad(v, CTLV_VALIDATION_REASON_BAD_LENGTH, nest, attr);
+ value = ctlv_schema_scalar_value(attr);
+ if (ctlv_schema_value_ok(node, value))
+ return 0;
+ if (node->kind == CTLV_SCHEMA_FLAGS)
+ return ctlv_schema_bad(v, CTLV_VALIDATION_REASON_UNKNOWN_FLAG, nest, attr);
+ return ctlv_schema_bad(v, CTLV_VALIDATION_REASON_VALUE_OUT_OF_RANGE, nest, attr);
+}
+
+static int ctlv_schema_check_bool(struct ctlv_schema_validation *v,
+ const struct ctlv_node_schema *node, const struct ctlv_attr *nest,
+ const struct ctlv_attr *attr)
+{
+ const u8 *value = ctlv_message_attr_payload(attr);
+
+ if (ctlv_message_attr_payload_len(attr) != sizeof(*value))
+ return ctlv_schema_bad(v, CTLV_VALIDATION_REASON_BAD_LENGTH, nest, attr);
+ if (!ctlv_schema_value_ok(node, *value))
+ return ctlv_schema_bad(v, CTLV_VALIDATION_REASON_VALUE_OUT_OF_RANGE, nest, attr);
+ return 0;
+}
+
+static int ctlv_schema_check_string(struct ctlv_schema_validation *v,
+ const struct ctlv_node_schema *node,
+ const struct ctlv_attr *nest, const struct ctlv_attr *attr)
+{
+ u32 len = ctlv_message_attr_payload_len(attr);
+ const char *value = ctlv_message_attr_payload(attr);
+
+ /*
+ * One terminating NUL and no other, so a family may hand the payload
+ * to anything that takes a C string.
+ */
+ if (!len || value[len - 1] || strnlen(value, len) != len - 1)
+ return ctlv_schema_bad(v, CTLV_VALIDATION_REASON_BAD_STRING, nest, attr);
+ if (!ctlv_schema_length_ok(node, len - 1))
+ return ctlv_schema_bad(v, CTLV_VALIDATION_REASON_BAD_LENGTH, nest, attr);
+ return 0;
+}
+
+static int ctlv_schema_check_binary(struct ctlv_schema_validation *v,
+ const struct ctlv_node_schema *node,
+ const struct ctlv_attr *nest, const struct ctlv_attr *attr)
+{
+ if (!ctlv_schema_length_ok(node, ctlv_message_attr_payload_len(attr)))
+ return ctlv_schema_bad(v, CTLV_VALIDATION_REASON_BAD_LENGTH, nest, attr);
+ return 0;
+}
+
+/*
+ * The one shape the tree cannot describe: a nest of core-owned attrs
+ * whose members depend on its type. Nothing here acquires anything.
+ */
+static int ctlv_schema_check_blob(struct ctlv_schema_validation *v,
+ const struct ctlv_node_schema *node, const struct ctlv_attr *blob)
+{
+ const struct ctlv_attr *bad = NULL;
+ struct ctlv_blob_desc desc;
+ u32 reason;
+
+ reason = ctlv_blob_desc_read(blob, &desc, &bad);
+ if (!reason)
+ reason = ctlv_blob_desc_check(&desc, node);
+ if (reason)
+ return ctlv_schema_bad(v, reason, blob, bad);
+ return 0;
+}
+
+static int ctlv_schema_check_nest(struct ctlv_schema_validation *v,
+ const struct ctlv_schema_tree *tree, u32 node,
+ const struct ctlv_attr *nest, u32 depth);
+static int ctlv_schema_check_indexed_nest(struct ctlv_schema_validation *v,
+ const struct ctlv_schema_tree *tree, u32 node,
+ const struct ctlv_attr *nest, u32 depth, u32 *nest_index);
+
+static int ctlv_schema_check_attr(struct ctlv_schema_validation *v,
+ const struct ctlv_schema_pos *pos, const struct ctlv_attr *nest,
+ const struct ctlv_attr *attr, u32 depth, u32 *nest_index)
+{
+ const struct ctlv_node_schema *node = &pos->tree->node_schemas[pos->node];
+ bool nested = node->kind == CTLV_SCHEMA_NEST || node->kind == CTLV_SCHEMA_BLOB;
+
+ if (attr->flags & ~ctlv_schema_flags_allowed(v))
+ return ctlv_schema_bad(v, CTLV_VALIDATION_REASON_UNKNOWN_FLAG, nest, attr);
+ /* Whether an attr is a nest is the schema's to say, not the sender's. */
+ if (!(attr->flags & CTLV_ATTR_F_NESTED) != !nested)
+ return ctlv_schema_bad(v, CTLV_VALIDATION_REASON_MALFORMED_MESSAGE, nest, attr);
+
+ switch (node->kind) {
+ case CTLV_SCHEMA_BOOL:
+ return ctlv_schema_check_bool(v, node, nest, attr);
+ case CTLV_SCHEMA_STRING:
+ return ctlv_schema_check_string(v, node, nest, attr);
+ case CTLV_SCHEMA_BINARY:
+ return ctlv_schema_check_binary(v, node, nest, attr);
+ case CTLV_SCHEMA_BLOB:
+ return ctlv_schema_check_blob(v, node, attr);
+ case CTLV_SCHEMA_NEST:
+ if (v->msg->request)
+ return ctlv_schema_check_indexed_nest(v, pos->tree, pos->node, attr,
+ depth + 1, nest_index);
+ return ctlv_schema_check_nest(v, pos->tree, pos->node, attr, depth + 1);
+ default:
+ return ctlv_schema_check_integer(v, node, nest, attr);
+ }
+}
+
+/* How many times one member occurs, and where the one too many is. */
+static u32 ctlv_schema_occurrences(const struct ctlv_attr *nest, u64 attr_id, u32 max,
+ const struct ctlv_attr **excess)
+{
+ const struct ctlv_attr *child = NULL;
+ u32 count = 0;
+
+ while (ctlv_message_next(nest, &child) == CTLV_MESSAGE_STEP_ATTR) {
+ if (child->attr_id != attr_id)
+ continue;
+ count++;
+ if (max && count == max + 1)
+ *excess = child;
+ }
+ return count;
+}
+
+/*
+ * An optional stub makes everything it was filled with optional too: what a
+ * descendant added cannot be something an ancestor's caller had to send.
+ */
+static int ctlv_schema_check_members(struct ctlv_schema_validation *v,
+ const struct ctlv_schema_tree *tree, u32 node,
+ const struct ctlv_attr *nest, bool optional)
+{
+ const struct ctlv_node_schema *parent = &tree->node_schemas[node];
+ u32 index;
+
+ for (index = 0; index < parent->children_count; index++) {
+ const struct ctlv_attr *excess = NULL;
+ const struct ctlv_node_schema *child;
+ u32 count;
+
+ child = &tree->node_schemas[parent->first_child + index];
+ count = ctlv_schema_occurrences(nest, child->attr_id, child->max_occurrences,
+ &excess);
+ if (!count) {
+ if (index < parent->required_children_count && !optional)
+ return ctlv_schema_bad_missing(v, nest, child->attr_id);
+ continue;
+ }
+ /* An unlimited placement, which only a reply may hold. */
+ if (child->max_occurrences && count > child->max_occurrences)
+ return ctlv_schema_bad(v, CTLV_VALIDATION_REASON_TOO_MANY_OCCURRENCES, nest,
+ excess);
+ }
+
+ for (index = 0; index < tree->stub_schemas_count; index++) {
+ const struct ctlv_stub_schema *stub_schema = &tree->stub_schemas[index];
+ const struct ctlv_stub_fill_schema *fill_schema;
+ int ret;
+
+ if (stub_schema->node != node)
+ continue;
+ fill_schema = ctlv_schema_fill(v->msg->family, stub_schema->family_id,
+ stub_schema->set_id);
+ if (!fill_schema)
+ continue;
+ ret = ctlv_schema_check_members(v, &fill_schema->members, 0, nest,
+ optional || stub_schema->optional);
+ if (ret)
+ return ret;
+ }
+ return 0;
+}
+
+/* Whether one tree places one attr ID anywhere, filled stubs included. */
+static bool ctlv_schema_tree_declares(const struct ctlv_family *family,
+ const struct ctlv_schema_tree *tree, u64 attr_id)
+{
+ u32 index;
+
+ for (index = 0; index < tree->node_schemas_count; index++)
+ if (tree->node_schemas[index].attr_id == attr_id)
+ return true;
+ for (index = 0; index < tree->stub_schemas_count; index++) {
+ const struct ctlv_stub_schema *stub_schema = &tree->stub_schemas[index];
+ const struct ctlv_stub_fill_schema *fill_schema;
+
+ fill_schema = ctlv_schema_fill(family, stub_schema->family_id, stub_schema->set_id);
+ if (fill_schema &&
+ ctlv_schema_tree_declares(family, &fill_schema->members, attr_id))
+ return true;
+ }
+ return false;
+}
+
+/* Search every attr ID known anywhere in the effective schema. */
+static bool ctlv_schema_declares(const struct ctlv_family *family, u64 attr_id)
+{
+ const struct ctlv_family *owner;
+ struct ctlv_schema_cursor cursor;
+ const struct ctlv_op_schema *op;
+
+ ctlv_schema_cursor_init(&cursor, family);
+ while ((op = ctlv_schema_next_op(&cursor, &owner)))
+ if (ctlv_schema_tree_declares(family, &op->request, attr_id) ||
+ ctlv_schema_tree_declares(family, &op->reply, attr_id))
+ return true;
+ return false;
+}
+
+/*
+ * Unknown IDs remain reserved unless explicitly ignorable; ignored unknown
+ * nests are opaque.
+ */
+static int ctlv_schema_check_unknown(struct ctlv_schema_validation *v, const struct ctlv_attr *nest,
+ const struct ctlv_attr *attr)
+{
+ /* Which refuses the bit itself on a reply, where it means nothing. */
+ if (attr->flags & ~ctlv_schema_flags_allowed(v))
+ return ctlv_schema_bad(v, CTLV_VALIDATION_REASON_UNKNOWN_FLAG, nest, attr);
+ if (!(attr->flags & CTLV_ATTR_F_IGNORE_IF_UNKNOWN) ||
+ ctlv_schema_declares(v->msg->family, attr->attr_id))
+ return ctlv_schema_bad(v, CTLV_VALIDATION_REASON_UNKNOWN_ATTR, nest, attr);
+ return 0;
+}
+
+static int ctlv_schema_check_indexed_nest(struct ctlv_schema_validation *v,
+ const struct ctlv_schema_tree *tree, u32 node,
+ const struct ctlv_attr *nest, u32 depth, u32 *nest_index)
+{
+ struct ctlv_message_input_index *index = v->msg->index;
+ const struct ctlv_attr *child = NULL;
+ const struct ctlv_attr *split = NULL;
+ u64 previous = 0;
+ bool previous_valid = false;
+ u32 added;
+ enum ctlv_message_step step;
+ int ret;
+
+ if (depth > CTLV_MAX_NEST_DEPTH)
+ return ctlv_schema_bad(v, CTLV_VALIDATION_REASON_NESTING_TOO_DEEP, nest, NULL);
+ ret = ctlv_schema_index_nest_add(v, tree, node, nest, &added);
+ if (ret)
+ return ret;
+
+ while ((step = ctlv_message_next(nest, &child)) == CTLV_MESSAGE_STEP_ATTR) {
+ struct ctlv_schema_pos pos;
+ bool seen;
+
+ if (!ctlv_schema_member(v->msg->family, tree, node, child->attr_id, &pos)) {
+ previous_valid = false;
+ continue;
+ }
+ ret = ctlv_schema_index_record(v, added, &pos, child, &seen);
+ if (ret)
+ return ret;
+ if (seen && (!previous_valid || previous != child->attr_id) && !split)
+ split = child;
+ previous = child->attr_id;
+ previous_valid = true;
+ }
+ ret = ctlv_schema_index_required(v, added, nest);
+ if (ret)
+ return ret;
+ if (step == CTLV_MESSAGE_STEP_BAD)
+ return ctlv_schema_bad(v, CTLV_VALIDATION_REASON_MALFORMED_MESSAGE, nest, NULL);
+
+ child = NULL;
+ while (ctlv_message_next(nest, &child) == CTLV_MESSAGE_STEP_ATTR) {
+ struct ctlv_schema_pos pos;
+ struct ctlv_message_input_slot *slot;
+ u32 child_nest = CTLV_MESSAGE_INPUT_INDEX_NONE;
+
+ if (!ctlv_schema_member(v->msg->family, tree, node, child->attr_id, &pos)) {
+ ret = ctlv_schema_check_unknown(v, nest, child);
+ if (ret)
+ return ret;
+ continue;
+ }
+ if (child == split)
+ return ctlv_schema_bad(v, CTLV_VALIDATION_REASON_NONCONTIGUOUS_ATTR, nest,
+ child);
+ ret = ctlv_schema_check_attr(v, &pos, nest, child, depth, &child_nest);
+ if (ret)
+ return ret;
+ if (child_nest != CTLV_MESSAGE_INPUT_INDEX_NONE) {
+ slot = ctlv_schema_index_slot(v, added, &pos);
+ if (!slot)
+ return -EINVAL;
+ ctlv_schema_index_nest_link(index, slot, child_nest);
+ }
+ }
+ *nest_index = added;
+ return 0;
+}
+
+/*
+ * Required members first, then every attr in the order the nest carries them,
+ * so that what is reported is the first thing a caller has to fix.
+ */
+static int ctlv_schema_check_nest(struct ctlv_schema_validation *v,
+ const struct ctlv_schema_tree *tree, u32 node,
+ const struct ctlv_attr *nest, u32 depth)
+{
+ const struct ctlv_attr *child = NULL;
+ enum ctlv_message_step step;
+ int ret;
+
+ if (depth > CTLV_MAX_NEST_DEPTH)
+ return ctlv_schema_bad(v, CTLV_VALIDATION_REASON_NESTING_TOO_DEEP, nest, NULL);
+
+ ret = ctlv_schema_check_members(v, tree, node, nest, false);
+ if (ret)
+ return ret;
+
+ while ((step = ctlv_message_next(nest, &child)) == CTLV_MESSAGE_STEP_ATTR) {
+ struct ctlv_schema_pos pos;
+
+ if (!ctlv_schema_member(v->msg->family, tree, node, child->attr_id, &pos)) {
+ ret = ctlv_schema_check_unknown(v, nest, child);
+ if (ret)
+ return ret;
+ /* Skipped whole: the step past it checked its length. */
+ continue;
+ }
+ ret = ctlv_schema_check_attr(v, &pos, nest, child, depth, NULL);
+ if (ret)
+ return ret;
+ }
+ if (step == CTLV_MESSAGE_STEP_BAD)
+ return ctlv_schema_bad(v, CTLV_VALIDATION_REASON_MALFORMED_MESSAGE, nest, NULL);
+ return 0;
+}
+
+int ctlv_schema_validate(struct ctlv_msg *msg, struct ctlv_error *error)
+{
+ struct ctlv_schema_validation validate = { .msg = msg, .error = error };
+ u32 root_index;
+ int ret;
+
+ if (!msg->request)
+ return ctlv_schema_check_nest(&validate, msg->tree, 0, msg->root, 0);
+ if (WARN_ON_ONCE(msg->index))
+ return -EINVAL;
+ msg->index = kzalloc_obj(*msg->index, GFP_KERNEL_ACCOUNT);
+ if (!msg->index)
+ return -ENOMEM;
+ ret = ctlv_schema_check_indexed_nest(&validate, msg->tree, 0, msg->root, 0, &root_index);
+ if (ret || WARN_ON_ONCE(root_index)) {
+ ctlv_message_index_free(msg);
+ return ret ?: -EINVAL;
+ }
+ return 0;
+}
diff --git a/drivers/ctlv/schema.h b/drivers/ctlv/schema.h
new file mode 100644
index 000000000000..4552a392da60
--- /dev/null
+++ b/drivers/ctlv/schema.h
@@ -0,0 +1,50 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES
+ *
+ * Private generated-schema walkers.
+ */
+#ifndef _CTLV_SCHEMA_H
+#define _CTLV_SCHEMA_H
+
+#include <linux/ctlv_family.h>
+#include <linux/types.h>
+
+struct ctlv_schema_cursor {
+ const struct ctlv_family *target;
+ const struct ctlv_family *family;
+ u32 level;
+ u32 index;
+};
+
+struct ctlv_schema_pos {
+ const struct ctlv_schema_tree *tree;
+ u32 node;
+};
+
+const struct ctlv_family *
+ctlv_family_ancestor(const struct ctlv_family *family, u32 up);
+u32 ctlv_family_levels(const struct ctlv_family *family);
+void ctlv_schema_cursor_init(struct ctlv_schema_cursor *cursor,
+ const struct ctlv_family *family);
+const struct ctlv_op_schema *
+ctlv_schema_next_op(struct ctlv_schema_cursor *cursor,
+ const struct ctlv_family **family);
+const struct ctlv_stub_fill_schema *
+ctlv_schema_fill(const struct ctlv_family *family, u16 family_id,
+ u32 set_id);
+int ctlv_schema_op_sizes(const struct ctlv_family *family,
+ const struct ctlv_op_schema *op,
+ struct ctlv_op_sizes *sizes);
+bool ctlv_schema_member(const struct ctlv_family *family,
+ const struct ctlv_schema_tree *tree, u32 node,
+ u64 attr_id, struct ctlv_schema_pos *member);
+bool ctlv_schema_value_ok(const struct ctlv_node_schema *node, u64 value);
+bool ctlv_schema_length_ok(const struct ctlv_node_schema *node, u32 len);
+
+struct ctlv_error;
+struct ctlv_msg;
+
+int ctlv_schema_validate(struct ctlv_msg *msg, struct ctlv_error *error);
+
+#endif /* _CTLV_SCHEMA_H */
--
2.56.0
^ permalink raw reply related [flat|nested] 32+ messages in thread* Re: [PATCH RFC 07/20] ctlv: Prepare schema validation
2026-10-15 14:50 ` [PATCH RFC 07/20] ctlv: Prepare schema validation Jiri Pirko
@ 2026-10-15 15:14 ` sashiko-bot
0 siblings, 0 replies; 32+ messages in thread
From: sashiko-bot @ 2026-10-15 15:14 UTC (permalink / raw)
To: Jiri Pirko; +Cc: kvm
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] ctlv: schema traversal ignores stub fills in reentering calculations
--
commit c53ad2ae9e8a16f911d52d764c978f30d89b6d14
Author: Jiri Pirko <jiri@resnulli.us>
ctlv: Prepare schema validation
Add generated-schema walking, message validation and request indexing.
Use the descriptor layer for blob placement checks. Keep CONFIG_CTLV unwired
until dispatch completes the module.
> diff --git a/drivers/ctlv/schema.c b/drivers/ctlv/schema.c
> new file mode 100644
> index 0000000000000..b74f9a358bc34
> --- /dev/null
> +++ b/drivers/ctlv/schema.c
[ ... ]
> +/* Size one deepest legal leaf and every nest above it. */
> +static u64 ctlv_schema_reentering_min_len(struct ctlv_schema_length_walk *walk,
> + const struct ctlv_schema_tree *tree)
> +{
> + u64 leaf = 0;
> + u32 index;
> +
> + for (index = 1; index < tree->node_schemas_count; index++) {
> + if (tree->node_schemas[index].kind == CTLV_SCHEMA_NEST)
> + continue;
> + leaf = max(leaf,
> + ALIGN(ctlv_schema_attr_max_len(walk, tree, index, 0), CTLV_ALIGNTO));
> + }
[Severity: Medium]
Does this calculation miss injected stub schemas? It iterates only over
tree->node_schemas_count, completely ignoring tree->stub_schemas.
This seems like it could miss any large leaves injected by stub fills,
underestimating the query reply buffer size (query_reply_min_len) and
potentially leading to message truncation in userspace.
> + return (CTLV_MAX_NEST_DEPTH + 1) * sizeof(struct ctlv_attr) + leaf;
> +}
> +
> +static bool ctlv_schema_tree_reenters(const struct ctlv_schema_tree *tree)
> +{
> + u32 index;
> +
> + for (index = 0; index < tree->node_schemas_count; index++)
> + if (tree->node_schemas[index].reenters)
> + return true;
[Severity: Medium]
Will this loop return false when reentering nodes are present only inside stub
fills? Because it only checks tree->node_schemas_count, it might cause
ctlv_schema_op_sizes() to incorrectly fall back to ctlv_schema_min_len().
If that fallback occurs, ctlv_schema_min_len() would recursively descend into
the reentering stub fill until its depth exceeds CTLV_MAX_DECLARED_NEST_DEPTH.
At that point, walk->bad becomes true and valid schemas could be incorrectly
rejected with -EINVAL.
> + return false;
> +}
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261015145053.3664509-1-jiri@resnulli.us?part=7
^ permalink raw reply [flat|nested] 32+ messages in thread
* [PATCH RFC 08/20] ctlv: Prepare message output handling
2026-10-15 14:50 [PATCH RFC 00/20] attestation: Add a common subsystem and TDX support Jiri Pirko
` (6 preceding siblings ...)
2026-10-15 14:50 ` [PATCH RFC 07/20] ctlv: Prepare schema validation Jiri Pirko
@ 2026-10-15 14:50 ` Jiri Pirko
2026-10-15 14:50 ` [PATCH RFC 09/20] ctlv: Prepare VA blob handling Jiri Pirko
` (11 subsequent siblings)
19 siblings, 0 replies; 32+ messages in thread
From: Jiri Pirko @ 2026-10-15 14:50 UTC (permalink / raw)
To: linux-coco
Cc: linux-kernel, linux-api, x86, kvm, linux-doc, jgg, david.quigley,
lukas, ksinyuk, rodrigo.vivi, cedric.xing, sameo, drdeeglaze,
James.Bottomley, aik, jpiotrowski, qinkun, pgonda, jic23,
alistair.francis, ankita, suzuki.poulose, kas, rick.p.edgecombe,
elena.reshetova, dave.hansen, sathyanarayanan.kuppuswamy,
peter.fang
From: Jiri Pirko <jiri@nvidia.com>
Add schema-aware reply construction, generated output accessors,
operation errors and query fragment completion.
Keep CONFIG_CTLV unwired until dispatch completes the module.
Assisted-by: Cursor: GPT-5.6 Sol
Assisted-by: Cursor: Claude Opus 5.5
Signed-off-by: Jiri Pirko <jiri@nvidia.com>
---
drivers/ctlv/Makefile | 2 +-
drivers/ctlv/builder.c | 374 ++++++++++++++++++++++++++++++++++++
drivers/ctlv/builder.h | 14 ++
include/linux/ctlv.h | 71 +++++++
include/linux/ctlv_family.h | 41 ++++
5 files changed, 501 insertions(+), 1 deletion(-)
create mode 100644 drivers/ctlv/builder.c
create mode 100644 drivers/ctlv/builder.h
diff --git a/drivers/ctlv/Makefile b/drivers/ctlv/Makefile
index 3fd656835b8a..e1a22a199ef1 100644
--- a/drivers/ctlv/Makefile
+++ b/drivers/ctlv/Makefile
@@ -1,4 +1,4 @@
# SPDX-License-Identifier: GPL-2.0
obj-$(CONFIG_CTLV) += ctlv.o
-ctlv-y += blob_desc.o message.o schema.o
+ctlv-y += blob_desc.o builder.o message.o schema.o
diff --git a/drivers/ctlv/builder.c b/drivers/ctlv/builder.c
new file mode 100644
index 000000000000..41748ceae1a3
--- /dev/null
+++ b/drivers/ctlv/builder.c
@@ -0,0 +1,374 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES
+ *
+ * Schema-aware reply construction and operation completion.
+ */
+#include <linux/align.h>
+#include <linux/bug.h>
+#include <linux/ctlv.h>
+#include <linux/errno.h>
+#include <linux/export.h>
+#include <linux/stdarg.h>
+#include <linux/string.h>
+#include <linux/types.h>
+
+#include <uapi/linux/ctlv.h>
+#include <uapi/linux/ctlv_gen.h>
+
+#include "builder.h"
+#include "internal.h"
+#include "message.h"
+#include "schema.h"
+
+static void ctlv_message_output_frag_mark(struct ctlv_op_ctx *ctx);
+
+void ctlv_message_output_init(struct ctlv_msg *msg, struct ctlv_op_ctx *ctx, void *buf, size_t len,
+ const struct ctlv_schema_tree *tree)
+{
+ *msg = (struct ctlv_msg){
+ .root = buf,
+ .len = len,
+ .tree = tree,
+ .family = ctx->family,
+ .ctx = ctx,
+ };
+ *msg->root = (struct ctlv_attr){
+ .len = sizeof(*msg->root),
+ .flags = CTLV_ATTR_F_NESTED,
+ .attr_id = CTLV_ATTR_ROOT,
+ };
+}
+
+/* The first thing that went wrong is what the op ends up failing with. */
+static bool ctlv_message_output_poison(struct ctlv_cursor *nest, int error)
+{
+ if (!nest->msg->poison)
+ nest->msg->poison = error;
+ return false;
+}
+
+/* A poisoned message is refused silently: whatever poisoned it has warned. */
+static bool ctlv_message_output_ready(struct ctlv_cursor *nest)
+{
+ if (WARN_ON_ONCE(!nest->msg || !nest->attr))
+ return false;
+ /* Also what refuses a cursor that reads a request. */
+ if (WARN_ON_ONCE(!nest->msg->ctx))
+ return false;
+ if (nest->msg->poison)
+ return false;
+ if (WARN_ON_ONCE(nest->msg->ctx->deepest != nest))
+ return ctlv_message_output_poison(nest, -EINVAL);
+ return true;
+}
+
+/* What the schema places in this nest under one ID, as one kind. */
+static const struct ctlv_node_schema *ctlv_message_output_member(struct ctlv_cursor *nest,
+ u64 attr_id, u8 kind,
+ struct ctlv_schema_pos *pos)
+{
+ const struct ctlv_node_schema *node;
+
+ if (!ctlv_message_output_ready(nest))
+ return NULL;
+ if (WARN_ON_ONCE(!ctlv_schema_member(nest->msg->family, nest->tree,
+ nest->node, attr_id, pos))) {
+ ctlv_message_output_poison(nest, -EINVAL);
+ return NULL;
+ }
+ node = &pos->tree->node_schemas[pos->node];
+ if (WARN_ON_ONCE(node->kind != kind)) {
+ ctlv_message_output_poison(nest, -EINVAL);
+ return NULL;
+ }
+ return node;
+}
+
+/*
+ * A query is offered whatever room its caller had, so running out ends a
+ * fragment. Anything else wrote a message its own schema does not describe.
+ */
+static bool ctlv_message_output_full(struct ctlv_cursor *nest)
+{
+ const struct ctlv_op_schema *op = nest->msg->ctx->op->schema;
+
+ if (op->type == CTLV_OP_TYPE_QUERY)
+ return false;
+ WARN_ONCE(1, "ctlv: op %#018llx wrote past the reply length its schema declares\n",
+ op->op_id);
+ return ctlv_message_output_poison(nest, -EMSGSIZE);
+}
+
+/* What one cursor stands on, as the buffer this direction may change. */
+static struct ctlv_attr *ctlv_message_output_attr(const struct ctlv_cursor *nest)
+{
+ return (struct ctlv_attr *)((u8 *)nest->msg->root + nest->start);
+}
+
+/* The pad is written rather than left as it was: it belongs to the nest. */
+static struct ctlv_attr *ctlv_message_output_write(struct ctlv_cursor *nest, u64 attr_id, u32 flags,
+ const void *payload, u32 payload_len)
+{
+ u32 len = sizeof(struct ctlv_attr) + payload_len;
+ size_t step = ALIGN((size_t)len, CTLV_ALIGNTO);
+ struct ctlv_msg *msg = nest->msg;
+ struct ctlv_cursor *open;
+ struct ctlv_attr *attr;
+ size_t offset;
+
+ offset = nest->start + ctlv_message_output_attr(nest)->len;
+ if (offset + step > msg->len) {
+ ctlv_message_output_full(nest);
+ return NULL;
+ }
+
+ attr = (struct ctlv_attr *)((u8 *)msg->root + offset);
+ *attr = (struct ctlv_attr){
+ .len = len,
+ .flags = flags,
+ .attr_id = attr_id,
+ };
+ if (payload_len)
+ memcpy(attr + 1, payload, payload_len);
+ memset((u8 *)attr + len, 0, step - len);
+ if (!(flags & CTLV_ATTR_F_NESTED))
+ msg->ctx->frag.progress = true;
+
+ /* Every nest the attr landed in is that much longer, the root too. */
+ for (open = nest; open; open = open->up)
+ ctlv_message_output_attr(open)->len += step;
+ return attr;
+}
+
+/* The 64-bit payload that every integer, enum and flags attr carries. */
+static bool ctlv_message_put_scalar(struct ctlv_cursor *nest, u64 attr_id, u8 kind, u64 value)
+{
+ const struct ctlv_node_schema *node;
+ struct ctlv_schema_pos pos;
+
+ node = ctlv_message_output_member(nest, attr_id, kind, &pos);
+ if (!node)
+ return false;
+ /* A value outside what the attr declares is one nothing may be told. */
+ if (WARN_ON_ONCE(!ctlv_schema_value_ok(node, value)))
+ return ctlv_message_output_poison(nest, -EINVAL);
+ return ctlv_message_output_write(nest, attr_id, 0, &value, sizeof(value));
+}
+
+bool ctlv_put_u64(struct ctlv_cursor *nest, u64 attr_id, u8 kind, u64 value)
+{
+ return ctlv_message_put_scalar(nest, attr_id, kind, value);
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_put_u64, "CTLV");
+
+bool ctlv_put_s64(struct ctlv_cursor *nest, u64 attr_id, u8 kind, s64 value)
+{
+ return ctlv_message_put_scalar(nest, attr_id, kind, value);
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_put_s64, "CTLV");
+
+bool ctlv_put_bool(struct ctlv_cursor *nest, u64 attr_id, u8 kind, bool value)
+{
+ u8 payload = value;
+ struct ctlv_schema_pos pos;
+
+ if (!ctlv_message_output_member(nest, attr_id, kind, &pos))
+ return false;
+ return ctlv_message_output_write(nest, attr_id, 0, &payload, sizeof(payload));
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_put_bool, "CTLV");
+
+bool ctlv_put_string(struct ctlv_cursor *nest, u64 attr_id, u8 kind, const char *value)
+{
+ const struct ctlv_node_schema *node;
+ struct ctlv_schema_pos pos;
+ size_t len;
+
+ node = ctlv_message_output_member(nest, attr_id, kind, &pos);
+ if (!node)
+ return false;
+ len = strnlen(value, (size_t)node->length.max_len + 1);
+ if (WARN_ON_ONCE(len > node->length.max_len || !ctlv_schema_length_ok(node, len)))
+ return ctlv_message_output_poison(nest, -EINVAL);
+ return ctlv_message_output_write(nest, attr_id, 0, value, len + 1);
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_put_string, "CTLV");
+
+bool ctlv_put_binary(struct ctlv_cursor *nest, u64 attr_id, u8 kind, const void *value, u32 len)
+{
+ const struct ctlv_node_schema *node;
+ struct ctlv_schema_pos pos;
+
+ node = ctlv_message_output_member(nest, attr_id, kind, &pos);
+ if (!node)
+ return false;
+ if (WARN_ON_ONCE(!ctlv_schema_length_ok(node, len)))
+ return ctlv_message_output_poison(nest, -EINVAL);
+ return ctlv_message_output_write(nest, attr_id, 0, value, len);
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_put_binary, "CTLV");
+
+bool ctlv_nest_start(struct ctlv_cursor *nest, struct ctlv_cursor *child, u64 attr_id, u8 kind)
+{
+ struct ctlv_schema_pos pos;
+ struct ctlv_attr *attr;
+
+ *child = (struct ctlv_cursor){};
+ if (!ctlv_message_output_member(nest, attr_id, kind, &pos))
+ return false;
+ /* Unreachable through any schema the generator would have accepted. */
+ if (WARN_ON_ONCE(nest->depth + 1 > CTLV_MAX_NEST_DEPTH))
+ return ctlv_message_output_poison(nest, -EINVAL);
+
+ attr = ctlv_message_output_write(nest, attr_id, CTLV_ATTR_F_NESTED, NULL, 0);
+ if (!attr)
+ return false;
+
+ *child = (struct ctlv_cursor){
+ .msg = nest->msg,
+ .attr = attr,
+ .up = nest,
+ .start = ctlv_message_offset(nest->msg, attr),
+ .tree = pos.tree,
+ .node = pos.node,
+ .depth = nest->depth + 1,
+ };
+ nest->msg->ctx->deepest = child;
+ return true;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_nest_start, "CTLV");
+
+void ctlv_nest_end(struct ctlv_cursor *child)
+{
+ struct ctlv_op_ctx *ctx;
+
+ if (WARN_ON_ONCE(!child->msg || !child->msg->ctx || !child->up))
+ return;
+ ctx = child->msg->ctx;
+ if (child->msg->poison)
+ return;
+ if (WARN_ON_ONCE(ctx->deepest != child)) {
+ ctlv_message_output_poison(child, -EINVAL);
+ return;
+ }
+ if (ctlv_message_output_attr(child)->len == sizeof(struct ctlv_attr))
+ ctx->frag.progress = true;
+ ctx->deepest = child->up;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_nest_end, "CTLV");
+
+static bool ctlv_op_error_valid(int error)
+{
+ return !WARN_ON_ONCE(error >= 0 || error < -MAX_ERRNO);
+}
+
+int ctlv_op_error(struct ctlv_op_ctx *ctx, int error, const char *message)
+{
+ if (!ctlv_op_error_valid(error))
+ return error < 0 && error >= -MAX_ERRNO ? error : -EINVAL;
+ if (!ctx->error.message[0])
+ strscpy(ctx->error.message, message, sizeof(ctx->error.message));
+ return error;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_op_error, "CTLV");
+
+int ctlv_op_errorf(struct ctlv_op_ctx *ctx, int error, const char *fmt, ...)
+{
+ va_list args;
+
+ if (!ctlv_op_error_valid(error))
+ return error < 0 && error >= -MAX_ERRNO ? error : -EINVAL;
+ if (ctx->error.message[0])
+ return error;
+ va_start(args, fmt);
+ vscnprintf(ctx->error.message, sizeof(ctx->error.message), fmt, args);
+ va_end(args);
+ return error;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_op_errorf, "CTLV");
+
+/* Continuation is a query's, so asking about one anywhere else is a bug. */
+static bool ctlv_op_is_query(const struct ctlv_op_ctx *ctx)
+{
+ return !WARN_ON_ONCE(ctx->op->schema->type != CTLV_OP_TYPE_QUERY);
+}
+
+u64 ctlv_op_query_generation(const struct ctlv_op_ctx *ctx)
+{
+ if (!ctlv_op_is_query(ctx))
+ return 0;
+ return ctx->frag.generation;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_op_query_generation, "CTLV");
+
+u64 ctlv_op_query_cursor(const struct ctlv_op_ctx *ctx)
+{
+ if (!ctlv_op_is_query(ctx))
+ return 0;
+ return ctx->frag.cursor;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_op_query_cursor, "CTLV");
+
+/*
+ * What both endings check: a query, one ending, and a generation that has not
+ * moved under a continuation, which no fragment can be delivered across.
+ */
+static int ctlv_query_fragment_end(struct ctlv_op_ctx *ctx, u64 generation)
+{
+ if (!ctlv_op_is_query(ctx))
+ return -EINVAL;
+ if (WARN_ON_ONCE(ctx->frag.answered))
+ return -EINVAL;
+ ctx->frag.answered = true;
+ /* Whatever the builder gave up on has warned about it already. */
+ if (ctx->output.poison)
+ return ctx->output.poison;
+ if (ctx->frag.cursor && generation != ctx->frag.generation) {
+ ctx->error.reason = CTLV_VALIDATION_REASON_STALE_GENERATION;
+ ctx->error.expected_generation = ctx->frag.generation;
+ ctx->error.current_generation = generation;
+ return -ESTALE;
+ }
+ ctx->frag.reported = generation;
+ return 0;
+}
+
+int ctlv_op_query_more(struct ctlv_op_ctx *ctx, u64 generation, u64 next_cursor)
+{
+ int ret = ctlv_query_fragment_end(ctx, generation);
+
+ if (!ret &&
+ WARN_ON_ONCE(!ctx->frag.progress || !next_cursor || next_cursor == ctx->frag.cursor))
+ ret = -EINVAL;
+ if (!ret) {
+ ctlv_message_output_frag_mark(ctx);
+ ctx->frag.next_cursor = next_cursor;
+ }
+ ctx->frag.result = ret;
+ return ret;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_op_query_more, "CTLV");
+
+int ctlv_op_query_done(struct ctlv_op_ctx *ctx, u64 generation)
+{
+ int ret = ctlv_query_fragment_end(ctx, generation);
+
+ if (!ret && WARN_ON_ONCE(ctx->deepest != &ctx->output_root)) {
+ /* A cursor of a frame that has returned is not followed. */
+ ctx->deepest = &ctx->output_root;
+ ret = -EINVAL;
+ }
+ ctx->frag.result = ret;
+ return ret;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_op_query_done, "CTLV");
+
+static void ctlv_message_output_frag_mark(struct ctlv_op_ctx *ctx)
+{
+ struct ctlv_cursor *open;
+
+ for (open = ctx->deepest; open; open = open->up)
+ ctlv_message_output_attr(open)->flags |= CTLV_ATTR_F_FRAG_MORE;
+ ctx->deepest = &ctx->output_root;
+}
diff --git a/drivers/ctlv/builder.h b/drivers/ctlv/builder.h
new file mode 100644
index 000000000000..744595ebd88e
--- /dev/null
+++ b/drivers/ctlv/builder.h
@@ -0,0 +1,14 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef _CTLV_BUILDER_H
+#define _CTLV_BUILDER_H
+
+#include <linux/types.h>
+
+struct ctlv_msg;
+struct ctlv_op_ctx;
+struct ctlv_schema_tree;
+
+void ctlv_message_output_init(struct ctlv_msg *msg, struct ctlv_op_ctx *ctx, void *buf, size_t len,
+ const struct ctlv_schema_tree *tree);
+
+#endif /* _CTLV_BUILDER_H */
diff --git a/include/linux/ctlv.h b/include/linux/ctlv.h
index addb893f4b8d..45f0c53b56ea 100644
--- a/include/linux/ctlv.h
+++ b/include/linux/ctlv.h
@@ -52,4 +52,75 @@ struct ctlv_cursor {
u32 index_scope;
};
+/**
+ * ctlv_nest_end - Finish an output nest.
+ * @child: nest cursor returned by a generated start helper
+ */
+void ctlv_nest_end(struct ctlv_cursor *child);
+
+/**
+ * ctlv_op_error - Record an operation error message.
+ * @ctx: operation context
+ * @error: negative error to return
+ * @message: message copied into the operation result
+ *
+ * Return: @error, or -EINVAL if @error is invalid.
+ */
+int ctlv_op_error(struct ctlv_op_ctx *ctx, int error, const char *message);
+
+/**
+ * ctlv_op_errorf - Format an operation error message.
+ * @ctx: operation context
+ * @error: negative error to return
+ * @fmt: printf format and arguments
+ *
+ * Return: @error, or -EINVAL if @error is invalid.
+ */
+__printf(3, 4)
+int ctlv_op_errorf(struct ctlv_op_ctx *ctx, int error, const char *fmt, ...);
+
+/*
+ * The core retains no query state. A continuation repeats the request and
+ * passes back the family's generation and nonzero cursor. It fails with
+ * -ESTALE if the generation changed. Each fragment must complete a logical
+ * leaf and return a cursor different from its input.
+ *
+ * ctlv_op_query_more() marks still-open nests, whose cursors must remain live.
+ */
+/**
+ * ctlv_op_query_generation - Get a query continuation generation.
+ * @ctx: query operation context
+ *
+ * Return: generation supplied by the previous fragment, or zero initially.
+ */
+u64 ctlv_op_query_generation(const struct ctlv_op_ctx *ctx);
+
+/**
+ * ctlv_op_query_cursor - Get a query continuation cursor.
+ * @ctx: query operation context
+ *
+ * Return: cursor supplied by the previous fragment, or zero initially.
+ */
+u64 ctlv_op_query_cursor(const struct ctlv_op_ctx *ctx);
+
+/**
+ * ctlv_op_query_more - Complete a query fragment with more data pending.
+ * @ctx: query operation context
+ * @generation: current data generation
+ * @next_cursor: nonzero cursor for the next fragment
+ *
+ * Return: 0, or a negative error.
+ */
+int ctlv_op_query_more(struct ctlv_op_ctx *ctx, u64 generation,
+ u64 next_cursor);
+
+/**
+ * ctlv_op_query_done - Complete the last query fragment.
+ * @ctx: query operation context
+ * @generation: current data generation
+ *
+ * Return: 0, or a negative error.
+ */
+int ctlv_op_query_done(struct ctlv_op_ctx *ctx, u64 generation);
+
#endif /* _LINUX_CTLV_H */
diff --git a/include/linux/ctlv_family.h b/include/linux/ctlv_family.h
index eae690b9c1d5..d2598ec686f1 100644
--- a/include/linux/ctlv_family.h
+++ b/include/linux/ctlv_family.h
@@ -326,6 +326,47 @@ bool ctlv_attr_bool(const struct ctlv_cursor *cursor);
const char *ctlv_attr_string(const struct ctlv_cursor *cursor);
const void *ctlv_attr_binary(const struct ctlv_cursor *cursor, u32 *len);
+bool ctlv_put_u64(struct ctlv_cursor *nest, u64 attr_id, u8 kind, u64 value);
+bool ctlv_put_s64(struct ctlv_cursor *nest, u64 attr_id, u8 kind, s64 value);
+bool ctlv_put_bool(struct ctlv_cursor *nest, u64 attr_id, u8 kind, bool value);
+bool ctlv_put_string(struct ctlv_cursor *nest, u64 attr_id, u8 kind,
+ const char *value);
+bool ctlv_put_binary(struct ctlv_cursor *nest, u64 attr_id, u8 kind,
+ const void *value, u32 len);
+bool ctlv_nest_start(struct ctlv_cursor *nest, struct ctlv_cursor *child,
+ u64 attr_id, u8 kind);
+
+#define __CTLV_DEFINE_PUT(_name, _type, _put_fn, _attr_id, _kind) \
+static inline bool _name##_put(struct ctlv_cursor *nest, \
+ _type value) \
+{ \
+ return _put_fn(nest, (_attr_id), (_kind), value); \
+}
+
+#define CTLV_PUT_U64(_name, _attr_id, _kind) \
+ __CTLV_DEFINE_PUT(_name, u64, ctlv_put_u64, _attr_id, _kind)
+#define CTLV_PUT_S64(_name, _attr_id, _kind) \
+ __CTLV_DEFINE_PUT(_name, s64, ctlv_put_s64, _attr_id, _kind)
+#define CTLV_PUT_BOOL(_name, _attr_id, _kind) \
+ __CTLV_DEFINE_PUT(_name, bool, ctlv_put_bool, _attr_id, _kind)
+#define CTLV_PUT_STRING(_name, _attr_id, _kind) \
+ __CTLV_DEFINE_PUT(_name, const char *, ctlv_put_string, \
+ _attr_id, _kind)
+
+#define CTLV_PUT_BINARY(_name, _attr_id, _kind) \
+static inline bool _name##_put(struct ctlv_cursor *nest, \
+ const void *value, u32 len) \
+{ \
+ return ctlv_put_binary(nest, (_attr_id), (_kind), value, len); \
+}
+
+#define CTLV_NEST_START(_name, _attr_id, _kind) \
+static inline bool _name##_start(struct ctlv_cursor *nest, \
+ struct ctlv_cursor *nested) \
+{ \
+ return ctlv_nest_start(nest, nested, (_attr_id), (_kind)); \
+}
+
#define CTLV_IN_REF(_name, _tree, _attr_id, _node, _slot, _kind) \
static inline struct ctlv_input_ref _name##_ref(void) \
{ \
--
2.56.0
^ permalink raw reply related [flat|nested] 32+ messages in thread* [PATCH RFC 09/20] ctlv: Prepare VA blob handling
2026-10-15 14:50 [PATCH RFC 00/20] attestation: Add a common subsystem and TDX support Jiri Pirko
` (7 preceding siblings ...)
2026-10-15 14:50 ` [PATCH RFC 08/20] ctlv: Prepare message output handling Jiri Pirko
@ 2026-10-15 14:50 ` Jiri Pirko
2026-10-15 15:14 ` sashiko-bot
2026-10-15 14:50 ` [PATCH RFC 10/20] ctlv: Prepare event queue support Jiri Pirko
` (10 subsequent siblings)
19 siblings, 1 reply; 32+ messages in thread
From: Jiri Pirko @ 2026-10-15 14:50 UTC (permalink / raw)
To: linux-coco
Cc: linux-kernel, linux-api, x86, kvm, linux-doc, jgg, david.quigley,
lukas, ksinyuk, rodrigo.vivi, cedric.xing, sameo, drdeeglaze,
James.Bottomley, aik, jpiotrowski, qinkun, pgonda, jic23,
alistair.francis, ankita, suzuki.poulose, kas, rick.p.edgecombe,
elena.reshetova, dave.hansen, sathyanarayanan.kuppuswamy,
peter.fang
From: Jiri Pirko <jiri@nvidia.com>
Add task-bound VA blob resolution and CPU read/write access for CTLV
families.
The blob object can be compile-checked individually. CONFIG_CTLV remains
unwired until device dispatch completes the module.
Assisted-by: Cursor: GPT-5.6 Sol
Assisted-by: Cursor: Claude Opus 5.5
Signed-off-by: Jiri Pirko <jiri@nvidia.com>
---
drivers/ctlv/Makefile | 2 +-
drivers/ctlv/blob.c | 355 ++++++++++++++++++++++++++++++++++++
drivers/ctlv/blob.h | 15 ++
drivers/ctlv/internal.h | 8 +
include/linux/ctlv.h | 41 +++++
include/linux/ctlv_family.h | 23 +++
6 files changed, 443 insertions(+), 1 deletion(-)
create mode 100644 drivers/ctlv/blob.c
create mode 100644 drivers/ctlv/blob.h
diff --git a/drivers/ctlv/Makefile b/drivers/ctlv/Makefile
index e1a22a199ef1..4455062f83d7 100644
--- a/drivers/ctlv/Makefile
+++ b/drivers/ctlv/Makefile
@@ -1,4 +1,4 @@
# SPDX-License-Identifier: GPL-2.0
obj-$(CONFIG_CTLV) += ctlv.o
-ctlv-y += blob_desc.o builder.o message.o schema.o
+ctlv-y += blob.o blob_desc.o builder.o message.o schema.o
diff --git a/drivers/ctlv/blob.c b/drivers/ctlv/blob.c
new file mode 100644
index 000000000000..2c93da62b201
--- /dev/null
+++ b/drivers/ctlv/blob.c
@@ -0,0 +1,355 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES
+ *
+ * Blob backing is acquired lazily in the submitting task and held until put.
+ */
+#include <linux/align.h>
+#include <linux/atomic.h>
+#include <linux/bits.h>
+#include <linux/capability.h>
+#include <linux/ctlv.h>
+#include <linux/err.h>
+#include <linux/errno.h>
+#include <linux/highmem.h>
+#include <linux/limits.h>
+#include <linux/minmax.h>
+#include <linux/mm.h>
+#include <linux/mm_types.h>
+#include <linux/overflow.h>
+#include <linux/sched.h>
+#include <linux/sched/mm.h>
+#include <linux/sched/signal.h>
+#include <linux/slab.h>
+#include <linux/types.h>
+
+#include <uapi/linux/ctlv.h>
+#include <uapi/linux/ctlv_gen.h>
+
+#include "blob.h"
+#include "blob_desc.h"
+#include "internal.h"
+#include "message.h"
+
+struct ctlv_blob {
+ u64 len;
+ u8 direction;
+ u8 type;
+ struct ctlv_op_ctx *ctx;
+ struct list_head entry;
+ union {
+ struct {
+ struct mm_struct *mm;
+ struct page **pages;
+ unsigned long pages_count;
+ u32 start;
+ } va;
+ };
+};
+
+static bool ctlv_blob_readable(const struct ctlv_blob *blob)
+{
+ return blob->direction != CTLV_BLOB_DIRECTION_OUTPUT;
+}
+
+static bool ctlv_blob_writable(const struct ctlv_blob *blob)
+{
+ return blob->direction != CTLV_BLOB_DIRECTION_INPUT;
+}
+
+static bool ctlv_blob_same_task(const struct ctlv_blob *blob)
+{
+ return !blob->ctx || !blob->ctx->submitter || blob->ctx->submitter == current;
+}
+
+static bool ctlv_blob_access_live(const struct ctlv_blob *blob)
+{
+ return ctlv_blob_same_task(blob) && (!blob->ctx || blob->ctx->blobs_live);
+}
+
+/*
+ * Charged against RLIMIT_MEMLOCK of the address space the pages came from. A
+ * pin lasts one op, which is why it is not FOLL_LONGTERM.
+ */
+static int ctlv_blob_pin_charge(struct mm_struct *mm, unsigned long pages_count)
+{
+ unsigned long limit = rlimit(RLIMIT_MEMLOCK) >> PAGE_SHIFT;
+
+ if (atomic64_add_return(pages_count, &mm->pinned_vm) > limit && !capable(CAP_IPC_LOCK)) {
+ atomic64_sub(pages_count, &mm->pinned_vm);
+ return -ENOMEM;
+ }
+ return 0;
+}
+
+static int ctlv_blob_va_range(u64 address, u64 len, unsigned long *first_page,
+ unsigned long *pages_count)
+{
+ unsigned long first, last, span, count;
+ size_t bytes;
+ u64 end;
+
+ if (!len)
+ return -EINVAL;
+ if (check_add_overflow(address, len, &end) || end > TASK_SIZE)
+ return -EFAULT;
+
+ /* Bounded by an unsigned long above, so nothing below loses a bit. */
+ first = (unsigned long)address & PAGE_MASK;
+ last = ((unsigned long)end - 1) & PAGE_MASK;
+ if (check_sub_overflow(last, first, &span) ||
+ check_add_overflow(span >> PAGE_SHIFT, 1UL, &count) ||
+ check_mul_overflow(count, sizeof(struct page *), &bytes))
+ return -EFAULT;
+
+ *first_page = first;
+ *pages_count = count;
+ return 0;
+}
+
+static int ctlv_blob_va_acquire(struct ctlv_blob *blob, u64 address)
+{
+ unsigned long pinned = 0, first, pages_count;
+ unsigned int gup_flags = 0;
+ struct page **pages;
+ int ret;
+
+ /* Before charging or allocating, both of which this sizes. */
+ ret = ctlv_blob_va_range(address, blob->len, &first, &pages_count);
+ if (ret)
+ return ret;
+
+ pages = kvmalloc_array(pages_count, sizeof(*pages), GFP_KERNEL_ACCOUNT);
+ if (!pages)
+ return -ENOMEM;
+
+ if (ctlv_blob_writable(blob))
+ gup_flags |= FOLL_WRITE;
+ ret = ctlv_blob_pin_charge(current->mm, pages_count);
+ if (ret)
+ goto err_free_pages;
+
+ while (pinned < pages_count) {
+ long got = pin_user_pages_fast(first + (pinned << PAGE_SHIFT),
+ min_t(unsigned long, pages_count - pinned, INT_MAX),
+ gup_flags, pages + pinned);
+
+ /* A short pin is progress; nothing pinned is the caller's. */
+ if (got <= 0) {
+ ret = got ? (int)got : -EFAULT;
+ goto err_unpin;
+ }
+ pinned += got;
+ }
+
+ /* Referenced, so the unpin is safe however far the submitter got. */
+ mmgrab(current->mm);
+ blob->va.start = offset_in_page(address);
+ blob->va.mm = current->mm;
+ blob->va.pages = pages;
+ blob->va.pages_count = pages_count;
+ return 0;
+
+ /* The pins taken, and the whole charge, which covered every page. */
+err_unpin:
+ unpin_user_pages(pages, pinned);
+ atomic64_sub(pages_count, ¤t->mm->pinned_vm);
+err_free_pages:
+ kvfree(pages);
+ return ret;
+}
+
+static void ctlv_blob_va_release(struct ctlv_blob *blob)
+{
+ /* Dirtied, so that what the kernel wrote survives the unpin. */
+ unpin_user_pages_dirty_lock(blob->va.pages, blob->va.pages_count, ctlv_blob_writable(blob));
+ atomic64_sub(blob->va.pages_count, &blob->va.mm->pinned_vm);
+ kvfree(blob->va.pages);
+ mmdrop(blob->va.mm);
+}
+
+static void ctlv_blob_va_copy(struct ctlv_blob *blob, u64 offset, void *buf, size_t len, bool write)
+{
+ u64 at = blob->va.start + offset;
+ unsigned long index = at >> PAGE_SHIFT;
+ u32 in_page = offset_in_page(at);
+ u8 *bytes = buf;
+
+ while (len) {
+ struct page *page = blob->va.pages[index++];
+ size_t step = min_t(size_t, len, PAGE_SIZE - in_page);
+
+ if (write)
+ memcpy_to_page(page, in_page, bytes, step);
+ else
+ memcpy_from_page(bytes, page, in_page, step);
+ bytes += step;
+ len -= step;
+ in_page = 0;
+ }
+}
+
+u64 ctlv_blob_len(const struct ctlv_blob *blob)
+{
+ return blob->len;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_blob_len, "CTLV");
+
+/*
+ * The length, the direction and addressing from zero are all the family's own
+ * to know, so getting one wrong is a kernel bug rather than a caller's.
+ */
+static bool ctlv_blob_range_ok(struct ctlv_blob *blob, u64 offset, size_t len, bool write)
+{
+ u64 end;
+
+ if (WARN_ON_ONCE(write ? !ctlv_blob_writable(blob) : !ctlv_blob_readable(blob)))
+ return false;
+ if (WARN_ON_ONCE(!ctlv_blob_access_live(blob)))
+ return false;
+ if (WARN_ON_ONCE(check_add_overflow(offset, (u64)len, &end) || end > blob->len))
+ return false;
+ return true;
+}
+
+static int ctlv_blob_copy(struct ctlv_blob *blob, u64 offset, void *buf, size_t len, bool write)
+{
+ if (!ctlv_blob_range_ok(blob, offset, len, write))
+ return -EINVAL;
+ if (!len)
+ return 0;
+
+ switch (blob->type) {
+ case CTLV_BLOB_TYPE_VA:
+ ctlv_blob_va_copy(blob, offset, buf, len, write);
+ return 0;
+ default:
+ return -EINVAL;
+ }
+}
+
+int ctlv_blob_read(struct ctlv_blob *blob, u64 offset, void *dst, size_t len)
+{
+ return ctlv_blob_copy(blob, offset, dst, len, false);
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_blob_read, "CTLV");
+
+int ctlv_blob_write(struct ctlv_blob *blob, u64 offset, const void *src, size_t len)
+{
+ return ctlv_blob_copy(blob, offset, (void *)src, len, true);
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_blob_write, "CTLV");
+
+int ctlv_blob_put(struct ctlv_blob *blob)
+{
+ if (!blob)
+ return 0;
+ if (WARN_ON_ONCE(!ctlv_blob_same_task(blob)))
+ return -EINVAL;
+
+ switch (blob->type) {
+ case CTLV_BLOB_TYPE_VA:
+ ctlv_blob_va_release(blob);
+ break;
+ }
+ if (blob->ctx) {
+ list_del(&blob->entry);
+ blob->ctx = NULL;
+ }
+ kfree(blob);
+ return 0;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_blob_put, "CTLV");
+
+void ctlv_blob_context_init(struct ctlv_op_ctx *ctx)
+{
+ INIT_LIST_HEAD(&ctx->blobs);
+ ctx->blobs_live = true;
+ ctx->blob_error = 0;
+}
+
+void ctlv_blob_context_track(struct ctlv_blob *blob, struct ctlv_op_ctx *ctx)
+{
+ blob->ctx = ctx;
+ list_add_tail(&blob->entry, &ctx->blobs);
+}
+
+int ctlv_blob_context_finish(struct ctlv_op_ctx *ctx)
+{
+ ctx->blobs_live = false;
+ if (WARN_ON_ONCE(!list_empty(&ctx->blobs)) && !ctx->blob_error)
+ ctx->blob_error = -EINVAL;
+ while (!list_empty(&ctx->blobs)) {
+ struct ctlv_blob *blob = list_first_entry(&ctx->blobs, struct ctlv_blob, entry);
+
+ ctlv_blob_put(blob);
+ }
+ return ctx->blob_error;
+}
+
+/*
+ * Which of an op's blobs was the unusable one, in the shape validation would
+ * have used. The errno the family returns says what went wrong.
+ */
+static void ctlv_blob_context_fail(struct ctlv_op_ctx *ctx, const struct ctlv_cursor *cursor)
+{
+ if (ctx->error.reason)
+ return;
+ ctx->error = (struct ctlv_error){
+ .reason = CTLV_VALIDATION_REASON_BAD_BLOB_DESCRIPTOR,
+ .bad_attr_offset = ctlv_message_offset(cursor->msg, cursor->attr),
+ .nest_offset = ctlv_message_offset(cursor->msg, cursor->parent),
+ };
+}
+
+static struct ctlv_blob *ctlv_blob_acquire(const struct ctlv_blob_desc *desc, u8 direction)
+{
+ u64 address = desc->member[CTLV_BLOB_MEMBER_ADDRESS];
+ u64 len = desc->member[CTLV_BLOB_MEMBER_LENGTH];
+ struct ctlv_blob *blob;
+ int ret;
+
+ blob = kzalloc_obj(*blob, GFP_KERNEL_ACCOUNT);
+ if (!blob)
+ return ERR_PTR(-ENOMEM);
+ INIT_LIST_HEAD(&blob->entry);
+ blob->len = len;
+ blob->direction = direction;
+ blob->type = desc->member[CTLV_BLOB_MEMBER_TYPE];
+
+ ret = ctlv_blob_va_acquire(blob, address);
+ if (ret) {
+ kfree(blob);
+ return ERR_PTR(ret);
+ }
+ return blob;
+}
+
+struct ctlv_blob *ctlv_blob_resolve(struct ctlv_op_ctx *ctx, const struct ctlv_cursor *blob)
+{
+ const struct ctlv_attr *bad = NULL;
+ const struct ctlv_node_schema *node;
+ struct ctlv_blob_desc desc;
+ struct ctlv_blob *acquired;
+
+ /* An absent blob, or a cursor from some other execution entirely. */
+ if (WARN_ON_ONCE(blob->msg != &ctx->request || !ctlv_attr_ok(blob)))
+ return ERR_PTR(-EINVAL);
+ node = &blob->tree->node_schemas[blob->node];
+ if (WARN_ON_ONCE(node->kind != CTLV_SCHEMA_BLOB))
+ return ERR_PTR(-EINVAL);
+ if (WARN_ON_ONCE(!ctx->blobs_live || !ctx->submitter || ctx->submitter != current))
+ return ERR_PTR(-EINVAL);
+ /* Validated before the callback, so anything wrong with it is a bug. */
+ if (WARN_ON_ONCE(ctlv_blob_desc_read(blob->attr, &desc, &bad) ||
+ ctlv_blob_desc_check(&desc, node)))
+ return ERR_PTR(-EINVAL);
+
+ acquired = ctlv_blob_acquire(&desc, node->blob.direction);
+ if (IS_ERR(acquired))
+ ctlv_blob_context_fail(ctx, blob);
+ else
+ ctlv_blob_context_track(acquired, ctx);
+ return acquired;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_blob_resolve, "CTLV");
diff --git a/drivers/ctlv/blob.h b/drivers/ctlv/blob.h
new file mode 100644
index 000000000000..bb7b933a1ced
--- /dev/null
+++ b/drivers/ctlv/blob.h
@@ -0,0 +1,15 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef _CTLV_BLOB_H
+#define _CTLV_BLOB_H
+
+#include <linux/types.h>
+
+struct ctlv_blob;
+struct ctlv_op_ctx;
+
+void ctlv_blob_context_init(struct ctlv_op_ctx *ctx);
+void ctlv_blob_context_track(struct ctlv_blob *blob,
+ struct ctlv_op_ctx *ctx);
+int ctlv_blob_context_finish(struct ctlv_op_ctx *ctx);
+
+#endif /* _CTLV_BLOB_H */
diff --git a/drivers/ctlv/internal.h b/drivers/ctlv/internal.h
index 7d2b43857299..f74724269628 100644
--- a/drivers/ctlv/internal.h
+++ b/drivers/ctlv/internal.h
@@ -8,12 +8,15 @@
#define _CTLV_INTERNAL_H
#include <linux/ctlv_family.h>
+#include <linux/list.h>
#include <linux/types.h>
#include <uapi/linux/ctlv.h>
#include "message.h"
+struct task_struct;
+
#define CTLV_ERROR_MESSAGE_LEN 256
/* An op that is always enabled has no per-device availability state. */
@@ -50,6 +53,8 @@ struct ctlv_query_fragment {
struct ctlv_op_ctx {
const struct ctlv_family *family;
+ /* Compared against current and never followed: identity, not a hold. */
+ const struct task_struct *submitter;
const struct ctlv_op_entry *op;
struct ctlv_msg request;
@@ -60,6 +65,9 @@ struct ctlv_op_ctx {
struct ctlv_cursor *deepest;
struct ctlv_query_fragment frag;
+ struct list_head blobs;
+ bool blobs_live;
+ int blob_error;
struct ctlv_error error;
};
diff --git a/include/linux/ctlv.h b/include/linux/ctlv.h
index 45f0c53b56ea..dd2206cccfb5 100644
--- a/include/linux/ctlv.h
+++ b/include/linux/ctlv.h
@@ -123,4 +123,45 @@ int ctlv_op_query_more(struct ctlv_op_ctx *ctx, u64 generation,
*/
int ctlv_op_query_done(struct ctlv_op_ctx *ctx, u64 generation);
+struct ctlv_blob;
+
+/**
+ * ctlv_blob_put - Release a resolved blob.
+ * @blob: blob returned by a generated resolve helper
+ *
+ * Return: 0, or a negative error.
+ */
+int ctlv_blob_put(struct ctlv_blob *blob);
+
+/**
+ * ctlv_blob_len - Get a resolved blob's length.
+ * @blob: resolved blob
+ *
+ * Return: blob length in bytes.
+ */
+u64 ctlv_blob_len(const struct ctlv_blob *blob);
+
+/**
+ * ctlv_blob_read - Copy bytes from a resolved input blob.
+ * @blob: resolved blob
+ * @offset: byte offset
+ * @dst: destination buffer
+ * @len: number of bytes
+ *
+ * Return: 0, or a negative error.
+ */
+int ctlv_blob_read(struct ctlv_blob *blob, u64 offset, void *dst, size_t len);
+
+/**
+ * ctlv_blob_write - Copy bytes to a resolved output blob.
+ * @blob: resolved blob
+ * @offset: byte offset
+ * @src: source buffer
+ * @len: number of bytes
+ *
+ * Return: 0, or a negative error.
+ */
+int ctlv_blob_write(struct ctlv_blob *blob, u64 offset, const void *src,
+ size_t len);
+
#endif /* _LINUX_CTLV_H */
diff --git a/include/linux/ctlv_family.h b/include/linux/ctlv_family.h
index d2598ec686f1..5cdcdfeec651 100644
--- a/include/linux/ctlv_family.h
+++ b/include/linux/ctlv_family.h
@@ -336,6 +336,8 @@ bool ctlv_put_binary(struct ctlv_cursor *nest, u64 attr_id, u8 kind,
bool ctlv_nest_start(struct ctlv_cursor *nest, struct ctlv_cursor *child,
u64 attr_id, u8 kind);
+struct ctlv_blob *ctlv_blob_resolve(struct ctlv_op_ctx *ctx,
+ const struct ctlv_cursor *blob);
#define __CTLV_DEFINE_PUT(_name, _type, _put_fn, _attr_id, _kind) \
static inline bool _name##_put(struct ctlv_cursor *nest, \
_type value) \
@@ -448,6 +450,17 @@ static inline void _name##_get(struct ctlv_cursor *nested, \
ctlv_input_attr(nested, nest, _name##_ref()); \
}
+#define CTLV_IN_BLOB(_name) \
+static inline struct ctlv_blob * \
+_name##_resolve(struct ctlv_op_ctx *ctx, \
+ const struct ctlv_cursor *nest) \
+{ \
+ struct ctlv_cursor attr; \
+ \
+ ctlv_input_attr(&attr, nest, _name##_ref()); \
+ return ctlv_blob_resolve(ctx, &attr); \
+}
+
#define CTLV_IN_EACH_U64(_name) \
static inline u64 _name##_get(const struct ctlv_cursor *attr) \
{ \
@@ -492,4 +505,14 @@ _name##_get(const struct ctlv_cursor *attr, u32 *len) \
return ctlv_attr_binary(attr, len); \
}
+#define CTLV_IN_EACH_BLOB(_name) \
+static inline struct ctlv_blob * \
+_name##_resolve(struct ctlv_op_ctx *ctx, \
+ const struct ctlv_cursor *attr) \
+{ \
+ if (!ctlv_input_cursor(attr, _name##_ref())) \
+ return ERR_PTR(-EINVAL); \
+ return ctlv_blob_resolve(ctx, attr); \
+}
+
#endif /* _LINUX_CTLV_FAMILY_H */
--
2.56.0
^ permalink raw reply related [flat|nested] 32+ messages in thread* Re: [PATCH RFC 09/20] ctlv: Prepare VA blob handling
2026-10-15 14:50 ` [PATCH RFC 09/20] ctlv: Prepare VA blob handling Jiri Pirko
@ 2026-10-15 15:14 ` sashiko-bot
0 siblings, 0 replies; 32+ messages in thread
From: sashiko-bot @ 2026-10-15 15:14 UTC (permalink / raw)
To: Jiri Pirko; +Cc: kvm
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] ctlv: infinite loop in ctlv_blob_context_finish() on task mismatch
--
commit f68d871d01d9258f8b2e576464f1c447eac3313e
Author: Jiri Pirko <jiri@resnulli.us>
ctlv: Prepare VA blob handling
Add task-bound VA blob resolution and CPU read/write access for CTLV
families. The blob object can be compile-checked individually.
> diff --git a/drivers/ctlv/blob.c b/drivers/ctlv/blob.c
> new file mode 100644
> index 0000000000000..2c93da62b2010
> --- /dev/null
> +++ b/drivers/ctlv/blob.c
[ ... ]
> +int ctlv_blob_context_finish(struct ctlv_op_ctx *ctx)
> +{
> + ctx->blobs_live = false;
> + if (WARN_ON_ONCE(!list_empty(&ctx->blobs)) && !ctx->blob_error)
> + ctx->blob_error = -EINVAL;
> + while (!list_empty(&ctx->blobs)) {
> + struct ctlv_blob *blob = list_first_entry(&ctx->blobs, struct ctlv_blob, entry);
> +
> + ctlv_blob_put(blob);
> + }
> + return ctx->blob_error;
> +}
[Severity: High]
Does this loop risk a hard lockup if ctlv_blob_context_finish() runs from a
task context different from the submitter?
If the contexts do not match, ctlv_blob_put() evaluates
WARN_ON_ONCE(!ctlv_blob_same_task(blob)) and returns -EINVAL early, bypassing
the list_del(&blob->entry) cleanup:
drivers/ctlv/blob.c:ctlv_blob_put() {
...
if (WARN_ON_ONCE(!ctlv_blob_same_task(blob)))
return -EINVAL;
...
if (blob->ctx) {
list_del(&blob->entry);
blob->ctx = NULL;
}
...
}
Because the return value of ctlv_blob_put() is ignored in this loop and the
blob is never removed from the list, the list remains non-empty. This will
cause the loop to continuously pop the exact same blob and fail repeatedly,
resulting in an infinite loop.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261015145053.3664509-1-jiri@resnulli.us?part=9
^ permalink raw reply [flat|nested] 32+ messages in thread
* [PATCH RFC 10/20] ctlv: Prepare event queue support
2026-10-15 14:50 [PATCH RFC 00/20] attestation: Add a common subsystem and TDX support Jiri Pirko
` (8 preceding siblings ...)
2026-10-15 14:50 ` [PATCH RFC 09/20] ctlv: Prepare VA blob handling Jiri Pirko
@ 2026-10-15 14:50 ` Jiri Pirko
2026-10-15 15:22 ` sashiko-bot
2026-10-15 14:50 ` [PATCH RFC 11/20] ctlv: Prepare runtime introspection Jiri Pirko
` (9 subsequent siblings)
19 siblings, 1 reply; 32+ messages in thread
From: Jiri Pirko @ 2026-10-15 14:50 UTC (permalink / raw)
To: linux-coco
Cc: linux-kernel, linux-api, x86, kvm, linux-doc, jgg, david.quigley,
lukas, ksinyuk, rodrigo.vivi, cedric.xing, sameo, drdeeglaze,
James.Bottomley, aik, jpiotrowski, qinkun, pgonda, jic23,
alistair.francis, ankita, suzuki.poulose, kas, rick.p.edgecombe,
elena.reshetova, dave.hansen, sathyanarayanan.kuppuswamy,
peter.fang
From: Jiri Pirko <jiri@nvidia.com>
Add per-open subscriptions, bounded event queues and event creation for
CTLV families. Add the generated root family kernel bindings now that their
message and event helpers are available.
The event and generated root objects can be compile-checked here.
CONFIG_CTLV remains unwired until device dispatch completes the module.
Assisted-by: Cursor: GPT-5.6 Sol
Assisted-by: Cursor: Claude Opus 5.5
Signed-off-by: Jiri Pirko <jiri@nvidia.com>
---
drivers/ctlv/Makefile | 2 +-
drivers/ctlv/device.h | 38 ++
drivers/ctlv/event.c | 713 ++++++++++++++++++++++++++++++++++++
drivers/ctlv/event.h | 69 ++++
drivers/ctlv/internal.h | 7 +
include/linux/ctlv.h | 27 ++
include/linux/ctlv_family.h | 11 +
7 files changed, 866 insertions(+), 1 deletion(-)
create mode 100644 drivers/ctlv/device.h
create mode 100644 drivers/ctlv/event.c
create mode 100644 drivers/ctlv/event.h
diff --git a/drivers/ctlv/Makefile b/drivers/ctlv/Makefile
index 4455062f83d7..e631ccd68994 100644
--- a/drivers/ctlv/Makefile
+++ b/drivers/ctlv/Makefile
@@ -1,4 +1,4 @@
# SPDX-License-Identifier: GPL-2.0
obj-$(CONFIG_CTLV) += ctlv.o
-ctlv-y += blob.o blob_desc.o builder.o message.o schema.o
+ctlv-y += blob.o blob_desc.o builder.o event.o message.o schema.o
diff --git a/drivers/ctlv/device.h b/drivers/ctlv/device.h
new file mode 100644
index 000000000000..916a346c30ed
--- /dev/null
+++ b/drivers/ctlv/device.h
@@ -0,0 +1,38 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef _CTLV_DEVICE_H
+#define _CTLV_DEVICE_H
+
+#include <linux/device.h>
+#include <linux/list.h>
+#include <linux/spinlock.h>
+#include <linux/types.h>
+#include <linux/wait.h>
+
+struct ctlv_event_record;
+struct ctlv_family;
+struct ctlv_op_entry;
+
+struct ctlv_device {
+ struct device dev;
+
+ const struct ctlv_family *family;
+
+ struct ctlv_op_entry *ops;
+ u32 ops_count;
+
+ /* Everything about the queues, and nothing family code runs under. */
+ spinlock_t events_lock;
+ struct list_head files;
+ bool dead_announced;
+ wait_queue_head_t event_context_wait;
+ u32 active_event_contexts;
+ bool events_live;
+ u32 events_count;
+ const struct ctlv_op_entry *overflow_op;
+ const struct ctlv_op_entry *removed_op;
+ u32 queue_min;
+ u32 queue_reserved;
+ u32 queue_default;
+};
+
+#endif /* _CTLV_DEVICE_H */
diff --git a/drivers/ctlv/event.c b/drivers/ctlv/event.c
new file mode 100644
index 000000000000..0f68d33e031c
--- /dev/null
+++ b/drivers/ctlv/event.c
@@ -0,0 +1,713 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES
+ *
+ * Per-open event queues and loss reporting.
+ */
+
+#include <linux/bitmap.h>
+#include <linux/build_bug.h>
+#include <linux/ctlv.h>
+#include <linux/ctlv_gen.h>
+#include <linux/err.h>
+#include <linux/errno.h>
+#include <linux/list.h>
+#include <linux/minmax.h>
+#include <linux/overflow.h>
+#include <linux/poll.h>
+#include <linux/sizes.h>
+#include <linux/slab.h>
+#include <linux/spinlock.h>
+#include <linux/string.h>
+#include <linux/uaccess.h>
+#include <linux/wait.h>
+
+#include "builder.h"
+#include "device.h"
+#include "event.h"
+#include "internal.h"
+#include "message.h"
+#include "schema.h"
+
+/* Header and root form one naturally aligned copy range. */
+static_assert(offsetof(struct ctlv_event_record, root) ==
+ offsetof(struct ctlv_event_record, hdr) + sizeof(struct ctlv_event_hdr));
+static_assert(sizeof(struct ctlv_event_hdr) % CTLV_ALIGNTO == 0);
+
+static u32 ctlv_event_len(const struct ctlv_op_entry *op)
+{
+ return op->sizes.event_record_max_len;
+}
+
+int ctlv_event_sizes(struct ctlv_device *ctlvdev)
+{
+ u32 largest = 0;
+ u32 index;
+
+ for (index = 0; index < ctlvdev->ops_count; index++) {
+ const struct ctlv_op_entry *op = &ctlvdev->ops[index];
+
+ if (op->schema->type != CTLV_OP_TYPE_EVENT)
+ continue;
+ largest = max(largest, ctlv_event_len(op));
+ if (op->schema->op_id == CTLV_OP_EVENT_OVERFLOW)
+ ctlvdev->overflow_op = op;
+ else if (op->schema->op_id == CTLV_OP_DEVICE_REMOVED)
+ ctlvdev->removed_op = op;
+ }
+ /* Every chain inherits the root, so every device has both of them. */
+ if (WARN_ON_ONCE(!ctlvdev->overflow_op || !ctlvdev->removed_op))
+ return -EINVAL;
+
+ ctlvdev->queue_reserved =
+ ctlv_event_len(ctlvdev->overflow_op) + ctlv_event_len(ctlvdev->removed_op);
+ ctlvdev->queue_min = largest + ctlvdev->queue_reserved;
+ if (ctlvdev->queue_min > CTLV_MAX_EVENT_QUEUE_LEN)
+ return -EMSGSIZE;
+ ctlvdev->queue_default =
+ clamp(max(SZ_256K, 4 * largest), ctlvdev->queue_min, CTLV_MAX_EVENT_QUEUE_LEN);
+ return 0;
+}
+
+void ctlv_event_device_init(struct ctlv_device *ctlvdev)
+{
+ spin_lock_init(&ctlvdev->events_lock);
+ INIT_LIST_HEAD(&ctlvdev->files);
+ init_waitqueue_head(&ctlvdev->event_context_wait);
+}
+
+void ctlv_event_contexts_start(struct ctlv_device *ctlvdev)
+{
+ guard(spinlock)
+ (&ctlvdev->events_lock);
+ if (WARN_ON_ONCE(ctlvdev->events_live || ctlvdev->active_event_contexts))
+ return;
+ ctlvdev->events_live = true;
+}
+
+void ctlv_event_contexts_stop(struct ctlv_device *ctlvdev)
+{
+ guard(spinlock)
+ (&ctlvdev->events_lock);
+ ctlvdev->events_live = false;
+}
+
+static bool ctlv_event_contexts_idle(struct ctlv_device *ctlvdev)
+{
+ guard(spinlock)
+ (&ctlvdev->events_lock);
+ return !ctlvdev->active_event_contexts;
+}
+
+void ctlv_event_contexts_wait(struct ctlv_device *ctlvdev)
+{
+ wait_event(ctlvdev->event_context_wait, ctlv_event_contexts_idle(ctlvdev));
+}
+
+/* @room bounds storage from the header through the root. */
+static struct ctlv_event_record *ctlv_event_record_alloc(u32 room, gfp_t gfp)
+{
+ struct ctlv_event_record *rec;
+
+ rec = kmalloc(struct_size(rec, root, room - sizeof(rec->hdr)), gfp);
+ if (!rec)
+ return NULL;
+ INIT_LIST_HEAD(&rec->node);
+ rec->len = 0;
+ rec->room = room;
+ rec->reserved = false;
+ return rec;
+}
+
+/* Core events use root schemas and may outlive the device family. */
+static void ctlv_event_record_build(struct ctlv_op_ctx *ctx, struct ctlv_event_record *rec,
+ const struct ctlv_op_entry *op)
+{
+ ctx->family = &ctlv_family;
+ ctx->op = op;
+ ctlv_message_output_init(&ctx->output, ctx, rec->root, rec->room - sizeof(rec->hdr),
+ &op->schema->reply);
+ ctlv_message_root(&ctx->output_root, &ctx->output);
+ ctx->deepest = &ctx->output_root;
+}
+
+/* The padding is written rather than left as the allocation found it. */
+static void ctlv_event_record_done(struct ctlv_op_ctx *ctx, struct ctlv_event_record *rec)
+{
+ u32 root_len = ctx->output.root->len;
+
+ rec->len = sizeof(rec->hdr) + CTLV_ALIGN(root_len);
+ rec->hdr.length = rec->len;
+ memset(rec->root + root_len, 0, rec->len - sizeof(rec->hdr) - root_len);
+}
+
+static struct ctlv_event_record *ctlv_event_record_reserve(struct ctlv_device *ctlvdev,
+ const struct ctlv_op_entry *op)
+{
+ struct ctlv_event_record *rec;
+
+ rec = ctlv_event_record_alloc(ctlv_event_len(op), GFP_KERNEL_ACCOUNT);
+ if (!rec)
+ return NULL;
+ rec->hdr.flags = 0;
+ rec->hdr.op_id = op->schema->op_id;
+ rec->reserved = true;
+ return rec;
+}
+
+int ctlv_event_file_init(struct ctlv_file *cfile)
+{
+ struct ctlv_device *ctlvdev = cfile->ctlvdev;
+ struct ctlv_op_ctx ctx = { .ctlvdev = ctlvdev };
+
+ INIT_LIST_HEAD(&cfile->node);
+ INIT_LIST_HEAD(&cfile->records);
+ init_waitqueue_head(&cfile->readers);
+ mutex_init(&cfile->reading);
+ cfile->queue_len = ctlvdev->queue_default;
+ cfile->queue_used = ctlvdev->queue_reserved;
+
+ cfile->subscribed = bitmap_zalloc(ctlvdev->events_count, GFP_KERNEL_ACCOUNT);
+ cfile->overflow = ctlv_event_record_reserve(ctlvdev, ctlvdev->overflow_op);
+ cfile->removed = ctlv_event_record_reserve(ctlvdev, ctlvdev->removed_op);
+ if (!cfile->subscribed || !cfile->overflow || !cfile->removed) {
+ ctlv_event_file_exit(cfile);
+ return -ENOMEM;
+ }
+ ctlv_event_record_build(&ctx, cfile->removed, ctlvdev->removed_op);
+ ctlv_event_record_done(&ctx, cfile->removed);
+
+ guard(spinlock)
+ (&ctlvdev->events_lock);
+ list_add_tail(&cfile->node, &ctlvdev->files);
+ return 0;
+}
+
+void ctlv_event_file_exit(struct ctlv_file *cfile)
+{
+ struct ctlv_event_record *rec, *next;
+
+ scoped_guard(spinlock, &cfile->ctlvdev->events_lock)
+ list_del_init(&cfile->node);
+
+ /* Unreachable records are ours; reserved records free by identity. */
+ list_for_each_entry_safe(rec, next, &cfile->records, node)
+ if (!rec->reserved)
+ kfree(rec);
+ kfree(cfile->overflow);
+ kfree(cfile->removed);
+ bitmap_free(cfile->subscribed);
+ mutex_destroy(&cfile->reading);
+}
+
+static void ctlv_event_queue_add(struct ctlv_file *cfile, struct ctlv_event_record *rec)
+{
+ list_add_tail(&rec->node, &cfile->records);
+ wake_up_interruptible(&cfile->readers);
+}
+
+static void ctlv_event_queue_link(struct ctlv_file *cfile, struct ctlv_event_record *rec)
+{
+ rec->hdr.sequence = ++cfile->sequence;
+ ctlv_event_queue_add(cfile, rec);
+}
+
+/* One reserved marker aggregates each contiguous drop epoch. */
+static void ctlv_event_queue_dropped(struct ctlv_file *cfile, u64 sequence)
+{
+ if (!cfile->overflowing) {
+ cfile->overflowing = true;
+ cfile->first_dropped = sequence;
+ cfile->dropped = 0;
+ if (!cfile->reading_overflow)
+ ctlv_event_queue_add(cfile, cfile->overflow);
+ }
+ cfile->last_dropped = sequence;
+ cfile->dropped++;
+}
+
+/* Delivered and dropped subscribed events both consume a sequence number. */
+static void ctlv_event_queue_event(struct ctlv_file *cfile, const struct ctlv_op_entry *op,
+ struct ctlv_event_record *rec)
+{
+ u64 sequence;
+
+ lockdep_assert_held(&cfile->ctlvdev->events_lock);
+ if (!test_bit(op->event_index, cfile->subscribed))
+ return;
+ sequence = ++cfile->sequence;
+ if (cfile->overflowing || !rec || cfile->queue_used + rec->len > cfile->queue_len) {
+ ctlv_event_queue_dropped(cfile, sequence);
+ return;
+ }
+ cfile->queue_used += rec->len;
+ rec->hdr.sequence = sequence;
+ list_move_tail(&rec->node, &cfile->records);
+ wake_up_interruptible(&cfile->readers);
+}
+
+static void ctlv_event_free(struct ctlv_op_ctx *ctx);
+
+struct ctlv_op_ctx *ctlv_event_create(struct ctlv_device *ctlvdev, u64 op_id,
+ struct ctlv_cursor **reply, gfp_t gfp)
+{
+ const struct ctlv_op_entry *op;
+ struct ctlv_op_ctx *ctx;
+ void *out;
+ u32 room;
+ int ret;
+
+ ctx = kzalloc_obj(*ctx, gfp);
+ if (!ctx)
+ return ERR_PTR(-ENOMEM);
+ scoped_guard(spinlock, &ctlvdev->events_lock) {
+ const struct ctlv_family *family;
+
+ if (!ctlvdev->events_live) {
+ ret = -ENODEV;
+ goto err_free_ctx;
+ }
+ if (WARN_ON_ONCE(ctlvdev->active_event_contexts == U32_MAX)) {
+ ret = -EOVERFLOW;
+ goto err_free_ctx;
+ }
+ /* Admission pins the op until emit or abort. */
+ family = READ_ONCE(ctlvdev->family);
+ if (!family) {
+ ret = -ENODEV;
+ goto err_free_ctx;
+ }
+ get_device(&ctlvdev->dev);
+ ctlvdev->active_event_contexts++;
+ ctx->ctlvdev = ctlvdev;
+ ctx->family = family;
+ }
+
+ op = ctlv_op_find(ctlvdev, op_id);
+ if (WARN_ON_ONCE(!op) || WARN_ON_ONCE(op->schema->type != CTLV_OP_TYPE_EVENT)) {
+ ret = -EINVAL;
+ goto err_put_ctx;
+ }
+ room = ctlv_event_len(op) - sizeof(struct ctlv_event_hdr);
+ out = kmalloc(room, gfp);
+ if (!out) {
+ ret = -ENOMEM;
+ goto err_put_ctx;
+ }
+
+ ctx->gfp = gfp;
+ ctx->op = op;
+ ctlv_message_output_init(&ctx->output, ctx, out, room, &op->schema->reply);
+ ctlv_message_root(&ctx->output_root, &ctx->output);
+ ctx->deepest = &ctx->output_root;
+ *reply = &ctx->output_root;
+ return ctx;
+
+err_put_ctx:
+ ctlv_event_free(ctx);
+ return ERR_PTR(ret);
+err_free_ctx:
+ kfree(ctx);
+ return ERR_PTR(ret);
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_event_create, "CTLV");
+
+static void ctlv_event_free(struct ctlv_op_ctx *ctx)
+{
+ struct ctlv_device *ctlvdev = ctx->ctlvdev;
+ bool wake;
+
+ kfree(ctx->output.root);
+ kfree(ctx);
+
+ scoped_guard(spinlock, &ctlvdev->events_lock) {
+ if (WARN_ON_ONCE(!ctlvdev->active_event_contexts))
+ wake = true;
+ else
+ wake = !--ctlvdev->active_event_contexts;
+ }
+ if (wake)
+ wake_up_all(&ctlvdev->event_context_wait);
+ put_device(&ctlvdev->dev);
+}
+
+/* Reject an incomplete or schema-invalid family event before queueing. */
+static int ctlv_event_finish(struct ctlv_op_ctx *ctx)
+{
+ struct ctlv_error refused = {};
+
+ /* Whatever poisoned it has warned about it already. */
+ if (ctx->output.poison)
+ return ctx->output.poison;
+ if (WARN_ON_ONCE(ctx->deepest != &ctx->output_root)) {
+ /* A cursor of a frame that has returned is not followed. */
+ ctx->deepest = &ctx->output_root;
+ return -EINVAL;
+ }
+ if (WARN_ON_ONCE(ctlv_schema_validate(&ctx->output, &refused)))
+ return -EINVAL;
+ return 0;
+}
+
+/* Allocate subscriber records before taking queue spinlocks. */
+static void ctlv_event_copies(struct ctlv_op_ctx *ctx, u32 len, struct list_head *spare, u32 wanted)
+{
+ const struct ctlv_attr *root = ctx->output.root;
+ struct ctlv_event_record *rec;
+
+ while (wanted--) {
+ rec = ctlv_event_record_alloc(len, ctx->gfp);
+ if (!rec)
+ return;
+ rec->len = len;
+ rec->hdr.length = len;
+ rec->hdr.flags = 0;
+ rec->hdr.op_id = ctx->op->schema->op_id;
+ /* Padded, since what is copied out is the whole record. */
+ memcpy_and_pad(rec->root, len - sizeof(rec->hdr), root, root->len, 0);
+ list_add_tail(&rec->node, spare);
+ }
+}
+
+static void ctlv_event_deliver(struct ctlv_op_ctx *ctx, struct list_head *spare)
+{
+ struct ctlv_device *ctlvdev = ctx->ctlvdev;
+ struct ctlv_event_record *rec;
+ struct ctlv_file *cfile;
+
+ guard(spinlock)
+ (&ctlvdev->events_lock);
+ if (ctlvdev->dead_announced)
+ return;
+ list_for_each_entry(cfile, &ctlvdev->files, node) {
+ rec = list_first_entry_or_null(spare, struct ctlv_event_record, node);
+ ctlv_event_queue_event(cfile, ctx->op, rec);
+ }
+}
+
+int ctlv_event_emit(struct ctlv_op_ctx *ctx)
+{
+ struct ctlv_device *ctlvdev = ctx->ctlvdev;
+ struct ctlv_event_record *rec, *next;
+ struct ctlv_file *cfile;
+ u32 len, wanted = 0;
+ LIST_HEAD(spare);
+ int ret;
+
+ if (WARN_ON_ONCE(ctx->file))
+ return -EINVAL;
+ ret = ctlv_event_finish(ctx);
+ if (ret)
+ goto out_free;
+
+ len = sizeof(struct ctlv_event_hdr) + CTLV_ALIGN(ctx->output.root->len);
+ scoped_guard(spinlock, &ctlvdev->events_lock) {
+ if (ctlvdev->dead_announced)
+ goto out_free;
+ list_for_each_entry(cfile, &ctlvdev->files, node)
+ if (test_bit(ctx->op->event_index, cfile->subscribed))
+ wanted++;
+ }
+ if (wanted) {
+ ctlv_event_copies(ctx, len, &spare, wanted);
+ ctlv_event_deliver(ctx, &spare);
+ list_for_each_entry_safe(rec, next, &spare, node)
+ kfree(rec);
+ }
+
+out_free:
+ ctlv_event_free(ctx);
+ return ret;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_event_emit, "CTLV");
+
+void ctlv_event_abort(struct ctlv_op_ctx *ctx)
+{
+ if (WARN_ON_ONCE(ctx->file))
+ return;
+ ctlv_event_free(ctx);
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_event_abort, "CTLV");
+
+void ctlv_event_lost(struct ctlv_device *ctlvdev, u64 op_id)
+{
+ const struct ctlv_op_entry *op;
+ struct ctlv_file *cfile;
+
+ guard(spinlock)
+ (&ctlvdev->events_lock);
+ if (!ctlvdev->events_live || ctlvdev->dead_announced)
+ return;
+ op = ctlv_op_find(ctlvdev, op_id);
+ if (WARN_ON_ONCE(!op) || WARN_ON_ONCE(op->schema->type != CTLV_OP_TYPE_EVENT))
+ return;
+ list_for_each_entry(cfile, &ctlvdev->files, node)
+ ctlv_event_queue_event(cfile, op, NULL);
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_event_lost, "CTLV");
+
+void ctlv_event_op_changed(struct ctlv_device *ctlvdev, const struct ctlv_op_entry *op,
+ bool disabled, u64 generation)
+{
+ struct ctlv_cursor *reply;
+ struct ctlv_op_ctx *ctx;
+
+ ctx = ctlv_op_changed_create(ctlvdev, &reply, GFP_NOWAIT);
+ if (IS_ERR(ctx)) {
+ ctlv_event_lost(ctlvdev, CTLV_OP_OP_CHANGED);
+ return;
+ }
+ ctlv_op_id_put(reply, op->schema->op_id);
+ ctlv_op_disabled_put(reply, disabled);
+ ctlv_op_state_generation_put(reply, generation);
+ ctlv_event_emit(ctx);
+}
+
+void ctlv_event_device_dead(struct ctlv_device *ctlvdev)
+{
+ struct ctlv_file *cfile;
+
+ guard(spinlock)
+ (&ctlvdev->events_lock);
+ if (WARN_ON_ONCE(ctlvdev->dead_announced))
+ return;
+ list_for_each_entry(cfile, &ctlvdev->files, node)
+ ctlv_event_queue_link(cfile, cfile->removed);
+ /* Publish dead only after terminal records are queued. */
+ ctlvdev->dead_announced = true;
+ list_for_each_entry(cfile, &ctlvdev->files, node)
+ wake_up_interruptible(&cfile->readers);
+}
+
+/* Materialize the final loss count when taking the marker. */
+static void ctlv_event_overflow_fill(struct ctlv_file *cfile)
+{
+ struct ctlv_device *ctlvdev = cfile->ctlvdev;
+ struct ctlv_op_ctx ctx = { .ctlvdev = ctlvdev };
+ struct ctlv_cursor *root;
+
+ ctlv_event_record_build(&ctx, cfile->overflow, ctlvdev->overflow_op);
+ cfile->overflow->hdr.sequence = cfile->first_dropped;
+ root = &ctx.output_root;
+ ctlv_event_first_sequence_put(root, cfile->first_dropped);
+ ctlv_event_last_sequence_put(root, cfile->last_dropped);
+ ctlv_event_dropped_count_put(root, cfile->dropped);
+ /* Reserved storage is the most this event can be, so it all fits. */
+ WARN_ON_ONCE(ctx.output.poison);
+ ctlv_event_record_done(&ctx, cfile->overflow);
+
+ cfile->reading_overflow = true;
+ cfile->overflowing = false;
+ cfile->first_reported = cfile->first_dropped;
+ cfile->last_reported = cfile->last_dropped;
+ cfile->dropped_reported = cfile->dropped;
+ cfile->dropped = 0;
+}
+
+static struct ctlv_event_record *ctlv_event_queue_take(struct ctlv_file *cfile)
+{
+ struct ctlv_event_record *rec;
+
+ guard(spinlock)
+ (&cfile->ctlvdev->events_lock);
+ rec = list_first_entry_or_null(&cfile->records, struct ctlv_event_record, node);
+ if (!rec)
+ return NULL;
+ list_del_init(&rec->node);
+ if (rec == cfile->overflow)
+ ctlv_event_overflow_fill(cfile);
+ else if (!rec->reserved)
+ cfile->queue_used -= rec->len;
+ return rec;
+}
+
+/* Restore an undelivered record, merging an overflow marker's epoch. */
+static void ctlv_event_queue_untake(struct ctlv_file *cfile, struct ctlv_event_record *rec)
+{
+ guard(spinlock)
+ (&cfile->ctlvdev->events_lock);
+ if (rec == cfile->overflow) {
+ cfile->reading_overflow = false;
+ cfile->first_dropped = cfile->first_reported;
+ if (!cfile->overflowing)
+ cfile->last_dropped = cfile->last_reported;
+ cfile->dropped += cfile->dropped_reported;
+ cfile->overflowing = true;
+ } else if (!rec->reserved) {
+ cfile->queue_used += rec->len;
+ }
+ list_add(&rec->node, &cfile->records);
+}
+
+/* Requeue a marker if another loss epoch opened while it was out. */
+static void ctlv_event_queue_consumed(struct ctlv_file *cfile, struct ctlv_event_record *rec)
+{
+ if (rec == cfile->overflow) {
+ guard(spinlock)
+ (&cfile->ctlvdev->events_lock);
+ cfile->reading_overflow = false;
+ if (cfile->overflowing)
+ ctlv_event_queue_add(cfile, rec);
+ return;
+ }
+ if (!rec->reserved)
+ kfree(rec);
+}
+
+static bool ctlv_event_waiting(struct ctlv_file *cfile)
+{
+ struct ctlv_device *ctlvdev = cfile->ctlvdev;
+
+ guard(spinlock)
+ (&ctlvdev->events_lock);
+ return !list_empty(&cfile->records) || ctlvdev->dead_announced;
+}
+
+ssize_t ctlv_event_file_read(struct ctlv_file *cfile, char __user *buf, size_t len, bool blocking)
+{
+ struct ctlv_device *ctlvdev = cfile->ctlvdev;
+ struct ctlv_event_record *rec;
+ ssize_t ret;
+
+ /* dup() and SCM_RIGHTS share one serialized queue. */
+ if (mutex_lock_interruptible(&cfile->reading))
+ return -ERESTARTSYS;
+
+ while (!(rec = ctlv_event_queue_take(cfile))) {
+ /* Drained, and the device is gone. */
+ if (READ_ONCE(ctlvdev->dead_announced)) {
+ ret = -ENODEV;
+ goto out_unlock;
+ }
+ if (!blocking) {
+ ret = -EAGAIN;
+ goto out_unlock;
+ }
+ ret = wait_event_interruptible(cfile->readers, ctlv_event_waiting(cfile));
+ if (ret) {
+ ret = -ERESTARTSYS;
+ goto out_unlock;
+ }
+ }
+
+ if (rec->len > len) {
+ ctlv_event_queue_untake(cfile, rec);
+ ret = -EMSGSIZE;
+ goto out_unlock;
+ }
+ if (copy_to_user(buf, &rec->hdr, rec->len)) {
+ ctlv_event_queue_untake(cfile, rec);
+ ret = -EFAULT;
+ goto out_unlock;
+ }
+ ret = rec->len;
+ ctlv_event_queue_consumed(cfile, rec);
+
+out_unlock:
+ mutex_unlock(&cfile->reading);
+ return ret;
+}
+
+__poll_t ctlv_event_file_poll(struct ctlv_file *cfile, struct file *filp,
+ struct poll_table_struct *wait)
+{
+ struct ctlv_device *ctlvdev = cfile->ctlvdev;
+ __poll_t mask = 0;
+
+ poll_wait(filp, &cfile->readers, wait);
+
+ guard(spinlock)
+ (&ctlvdev->events_lock);
+ if (!list_empty(&cfile->records))
+ mask |= EPOLLIN | EPOLLRDNORM;
+ /* Which a reader sees beside what it has not drained yet. */
+ if (ctlvdev->dead_announced)
+ mask |= EPOLLHUP;
+ return mask;
+}
+
+/* Validate every ID before changing any subscription. */
+static const struct ctlv_op_entry *ctlv_event_subscription_find(struct ctlv_device *ctlvdev,
+ u64 op_id)
+{
+ const struct ctlv_op_entry *op = ctlv_op_find(ctlvdev, op_id);
+
+ return op && op->schema->type == CTLV_OP_TYPE_EVENT ? op : NULL;
+}
+
+static int ctlv_event_subscriptions_set(struct ctlv_op_ctx *ctx, const struct ctlv_cursor *request,
+ struct ctlv_input_ref ref, bool subscribe)
+{
+ struct ctlv_device *ctlvdev = ctx->ctlvdev;
+ struct ctlv_cursor attr;
+
+ for (ctlv_input_attr(&attr, request, ref); ctlv_attr_ok(&attr); ctlv_attr_next(&attr)) {
+ u64 op_id = ctlv_attr_u64(&attr);
+
+ if (!ctlv_event_subscription_find(ctlvdev, op_id))
+ return ctlv_op_errorf(ctx, -EOPNOTSUPP,
+ "op %#llx is not an event this device emits",
+ (unsigned long long)op_id);
+ }
+
+ guard(spinlock)
+ (&ctlvdev->events_lock);
+ for (ctlv_input_attr(&attr, request, ref); ctlv_attr_ok(&attr); ctlv_attr_next(&attr)) {
+ u64 op_id = ctlv_attr_u64(&attr);
+ const struct ctlv_op_entry *op = ctlv_event_subscription_find(ctlvdev, op_id);
+
+ if (subscribe)
+ set_bit(op->event_index, ctx->file->subscribed);
+ else
+ clear_bit(op->event_index, ctx->file->subscribed);
+ }
+ return 0;
+}
+
+int ctlv_subscriptions_add_action(void *priv, struct ctlv_op_ctx *ctx,
+ const struct ctlv_cursor *request, struct ctlv_cursor *reply)
+{
+ return ctlv_event_subscriptions_set(ctx, request, ctlv_subscriptions_add_event_op_id_ref(),
+ true);
+}
+
+int ctlv_subscriptions_remove_action(void *priv, struct ctlv_op_ctx *ctx,
+ const struct ctlv_cursor *request, struct ctlv_cursor *reply)
+{
+ return ctlv_event_subscriptions_set(ctx, request,
+ ctlv_subscriptions_remove_event_op_id_ref(), false);
+}
+
+int ctlv_event_queue_get_query(void *priv, struct ctlv_op_ctx *ctx,
+ const struct ctlv_cursor *request, struct ctlv_cursor *reply)
+{
+ struct ctlv_file *cfile = ctx->file;
+
+ scoped_guard(spinlock, &ctx->ctlvdev->events_lock) {
+ ctlv_event_queue_len_put(reply, cfile->queue_len);
+ ctlv_event_queue_used_put(reply, cfile->queue_used);
+ }
+ /* One state, whole, so there is never a second fragment of it. */
+ return ctlv_op_query_done(ctx, 0);
+}
+
+/* Resizing never discards queued records or reserved capacity. */
+int ctlv_event_queue_set_action(void *priv, struct ctlv_op_ctx *ctx,
+ const struct ctlv_cursor *request, struct ctlv_cursor *reply)
+{
+ struct ctlv_device *ctlvdev = ctx->ctlvdev;
+ u64 len = ctlv_event_queue_set_event_queue_len_get(request);
+
+ if (len < ctlvdev->queue_min || len > CTLV_MAX_EVENT_QUEUE_LEN)
+ return ctlv_op_errorf(ctx, -EINVAL, "event queue length %llu is outside %u..%u",
+ (unsigned long long)len, ctlvdev->queue_min,
+ CTLV_MAX_EVENT_QUEUE_LEN);
+
+ guard(spinlock)
+ (&ctlvdev->events_lock);
+ if (len < ctx->file->queue_used)
+ return ctlv_op_error(ctx, -EBUSY,
+ "event queue contains more data than the requested limit");
+ ctx->file->queue_len = len;
+ return 0;
+}
diff --git a/drivers/ctlv/event.h b/drivers/ctlv/event.h
new file mode 100644
index 000000000000..80898c770034
--- /dev/null
+++ b/drivers/ctlv/event.h
@@ -0,0 +1,69 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef _CTLV_EVENT_H
+#define _CTLV_EVENT_H
+
+#include <linux/gfp_types.h>
+#include <linux/list.h>
+#include <linux/mutex.h>
+#include <linux/poll.h>
+#include <linux/types.h>
+#include <linux/wait.h>
+
+#include <uapi/linux/ctlv.h>
+
+struct ctlv_device;
+struct ctlv_op_entry;
+struct file;
+struct poll_table_struct;
+
+struct ctlv_event_record {
+ struct list_head node;
+ u32 len;
+ u32 room;
+ bool reserved;
+ struct ctlv_event_hdr hdr;
+ u8 root[] __aligned(8);
+};
+
+struct ctlv_file {
+ struct ctlv_device *ctlvdev;
+ struct list_head node;
+
+ struct list_head records;
+ u32 queue_len;
+ u32 queue_used;
+ u64 sequence;
+ unsigned long *subscribed;
+ struct ctlv_event_record *overflow;
+ struct ctlv_event_record *removed;
+ bool overflowing;
+ u64 first_dropped;
+ u64 last_dropped;
+ u64 dropped;
+ bool reading_overflow;
+ u64 first_reported;
+ u64 last_reported;
+ u64 dropped_reported;
+
+ wait_queue_head_t readers;
+ /* One reader of this description at a time, however many hold it. */
+ struct mutex reading;
+};
+
+int ctlv_event_sizes(struct ctlv_device *ctlvdev);
+void ctlv_event_device_init(struct ctlv_device *ctlvdev);
+void ctlv_event_contexts_start(struct ctlv_device *ctlvdev);
+void ctlv_event_contexts_stop(struct ctlv_device *ctlvdev);
+void ctlv_event_contexts_wait(struct ctlv_device *ctlvdev);
+void ctlv_event_device_dead(struct ctlv_device *ctlvdev);
+int ctlv_event_file_init(struct ctlv_file *cfile);
+void ctlv_event_file_exit(struct ctlv_file *cfile);
+ssize_t ctlv_event_file_read(struct ctlv_file *cfile, char __user *buf,
+ size_t len, bool blocking);
+__poll_t ctlv_event_file_poll(struct ctlv_file *cfile, struct file *filp,
+ struct poll_table_struct *wait);
+void ctlv_event_op_changed(struct ctlv_device *ctlvdev,
+ const struct ctlv_op_entry *op, bool disabled,
+ u64 generation);
+
+#endif /* _CTLV_EVENT_H */
diff --git a/drivers/ctlv/internal.h b/drivers/ctlv/internal.h
index f74724269628..7e6ec9d1f73e 100644
--- a/drivers/ctlv/internal.h
+++ b/drivers/ctlv/internal.h
@@ -16,6 +16,7 @@
#include "message.h"
struct task_struct;
+struct ctlv_file;
#define CTLV_ERROR_MESSAGE_LEN 256
@@ -29,6 +30,7 @@ struct ctlv_op_entry {
struct ctlv_op_sizes sizes;
u32 state_index;
bool absent;
+ u32 event_index;
};
struct ctlv_error {
@@ -52,9 +54,12 @@ struct ctlv_query_fragment {
};
struct ctlv_op_ctx {
+ struct ctlv_device *ctlvdev;
const struct ctlv_family *family;
+ struct ctlv_file *file;
/* Compared against current and never followed: identity, not a hold. */
const struct task_struct *submitter;
+ gfp_t gfp;
const struct ctlv_op_entry *op;
struct ctlv_msg request;
@@ -71,4 +76,6 @@ struct ctlv_op_ctx {
struct ctlv_error error;
};
+struct ctlv_op_entry *ctlv_op_find(struct ctlv_device *ctlvdev, u64 op_id);
+
#endif /* _CTLV_INTERNAL_H */
diff --git a/include/linux/ctlv.h b/include/linux/ctlv.h
index dd2206cccfb5..cdfe3e8e930d 100644
--- a/include/linux/ctlv.h
+++ b/include/linux/ctlv.h
@@ -164,4 +164,31 @@ int ctlv_blob_read(struct ctlv_blob *blob, u64 offset, void *dst, size_t len);
int ctlv_blob_write(struct ctlv_blob *blob, u64 offset, const void *src,
size_t len);
+/**
+ * ctlv_event_lost - Report an event that could not be created.
+ * @ctlvdev: CTLV device
+ * @op_id: event operation ID
+ *
+ * This does not allocate or sleep. It is not for interrupt context.
+ */
+void ctlv_event_lost(struct ctlv_device *ctlvdev, u64 op_id);
+
+/**
+ * ctlv_event_emit - Validate and publish a completed event.
+ * @ctx: context returned by a generated event create helper
+ *
+ * This consumes @ctx. It is not for interrupt context.
+ *
+ * Return: 0, or a negative error.
+ */
+int ctlv_event_emit(struct ctlv_op_ctx *ctx);
+
+/**
+ * ctlv_event_abort - Discard an event under construction.
+ * @ctx: context returned by a generated event create helper
+ *
+ * This consumes @ctx. It is not for interrupt context.
+ */
+void ctlv_event_abort(struct ctlv_op_ctx *ctx);
+
#endif /* _LINUX_CTLV_H */
diff --git a/include/linux/ctlv_family.h b/include/linux/ctlv_family.h
index 5cdcdfeec651..4f5141fc7ccf 100644
--- a/include/linux/ctlv_family.h
+++ b/include/linux/ctlv_family.h
@@ -338,6 +338,17 @@ bool ctlv_nest_start(struct ctlv_cursor *nest, struct ctlv_cursor *child,
struct ctlv_blob *ctlv_blob_resolve(struct ctlv_op_ctx *ctx,
const struct ctlv_cursor *blob);
+struct ctlv_op_ctx *ctlv_event_create(struct ctlv_device *ctlvdev, u64 op_id,
+ struct ctlv_cursor **event, gfp_t gfp);
+
+#define CTLV_EVENT_CREATE(_name, _op_id) \
+static inline struct ctlv_op_ctx * \
+_name##_create(struct ctlv_device *ctlvdev, \
+ struct ctlv_cursor **reply, gfp_t gfp) \
+{ \
+ return ctlv_event_create(ctlvdev, (_op_id), reply, gfp); \
+}
+
#define __CTLV_DEFINE_PUT(_name, _type, _put_fn, _attr_id, _kind) \
static inline bool _name##_put(struct ctlv_cursor *nest, \
_type value) \
--
2.56.0
^ permalink raw reply related [flat|nested] 32+ messages in thread* Re: [PATCH RFC 10/20] ctlv: Prepare event queue support
2026-10-15 14:50 ` [PATCH RFC 10/20] ctlv: Prepare event queue support Jiri Pirko
@ 2026-10-15 15:22 ` sashiko-bot
0 siblings, 0 replies; 32+ messages in thread
From: sashiko-bot @ 2026-10-15 15:22 UTC (permalink / raw)
To: Jiri Pirko; +Cc: kvm
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [High] race: TOCTOU in ctlv_event_file_read causes lost terminal events
- [High] mm: Out-of-bounds write when finalizing reserved event records with unaligned sizes
- [Medium] race: Queue misordering appends overflow event after terminal removed event
- [Low] cleanup: Mixing goto-based error cleanup with scope-based guards
--
commit 77cd7b72d04e886f1416198fdf738e02c8632520
Author: Jiri Pirko <jiri@resnulli.us>
Subject: ctlv: Prepare event queue support
Add per-open subscriptions, bounded event queues and event creation for
CTLV families. Add the generated root family kernel bindings now that their
message and event helpers are available.
> diff --git a/drivers/ctlv/event.c b/drivers/ctlv/event.c
> new file mode 100644
> index 0000000000000..0f68d33e031c8
> --- /dev/null
> +++ b/drivers/ctlv/event.c
> @@ -0,0 +1,713 @@
[ ... ]
> +static void ctlv_event_record_done(struct ctlv_op_ctx *ctx, struct ctlv_event_record *rec)
> +{
> + u32 root_len = ctx->output.root->len;
> +
> + rec->len = sizeof(rec->hdr) + CTLV_ALIGN(root_len);
> + rec->hdr.length = rec->len;
> + memset(rec->root + root_len, 0, rec->len - sizeof(rec->hdr) - root_len);
> +}
> +
> +static struct ctlv_event_record *ctlv_event_record_reserve(struct ctlv_device *ctlvdev,
> + const struct ctlv_op_entry *op)
> +{
> + struct ctlv_event_record *rec;
> +
> + rec = ctlv_event_record_alloc(ctlv_event_len(op), GFP_KERNEL_ACCOUNT);
[Severity: High]
Does this sequence cause an out-of-bounds write when finalizing reserved
event records with unaligned sizes?
In ctlv_event_record_reserve(), the record is allocated with a capacity of
ctlv_event_len(op). If the maximum record length from the schema is not a
multiple of 8, this length will be unaligned.
When root_len approaches this unaligned maximum in ctlv_event_record_done(),
CTLV_ALIGN(root_len) rounds up past the allocated size. The subsequent
memset() then writes padding zeros up to the aligned boundary, which can
cause an out-of-bounds write on the kernel heap.
[ ... ]
> +struct ctlv_op_ctx *ctlv_event_create(struct ctlv_device *ctlvdev, u64 op_id,
> + struct ctlv_cursor **reply, gfp_t gfp)
> +{
[ ... ]
> + scoped_guard(spinlock, &ctlvdev->events_lock) {
> + const struct ctlv_family *family;
> +
> + if (!ctlvdev->events_live) {
> + ret = -ENODEV;
> + goto err_free_ctx;
[Severity: Low]
Is it safe to mix goto-based error cleanup with scope-based guards here?
The cleanup subsystem guide mandates that functions must never mix
goto-based error handling and guard()/scoped_guard() declarations.
Using goto to jump out of a scoped_guard creates confusing ownership
semantics.
[ ... ]
> +int ctlv_event_emit(struct ctlv_op_ctx *ctx)
> +{
[ ... ]
> + scoped_guard(spinlock, &ctlvdev->events_lock) {
> + if (ctlvdev->dead_announced)
> + goto out_free;
[Severity: Low]
Does this goto also violate the cleanup guidelines?
Similar to ctlv_event_create(), jumping out of a scoped_guard to a cleanup
label is discouraged and violates the cleanup rules.
[ ... ]
> +static void ctlv_event_queue_consumed(struct ctlv_file *cfile, struct ctlv_event_record *rec)
> +{
> + if (rec == cfile->overflow) {
> + guard(spinlock)
> + (&cfile->ctlvdev->events_lock);
> + cfile->reading_overflow = false;
> + if (cfile->overflowing)
> + ctlv_event_queue_add(cfile, rec);
[Severity: Medium]
Can this queue misordering append the overflow event after the removed event?
If a reader is consuming the overflow marker while the queue receives another
dropped event, cfile->overflowing becomes true again. If
ctlv_event_device_dead() is then called, it enqueues the removed terminal
event.
When the reader finishes and calls ctlv_event_queue_consumed(), it sees
overflowing == true and re-enqueues the overflow marker. Because
ctlv_event_queue_add() appends to the tail, the overflow marker is
incorrectly queued after the removed event, breaking monotonicity and the
terminal property of the removal event.
[ ... ]
> +ssize_t ctlv_event_file_read(struct ctlv_file *cfile, char __user *buf, size_t len, bool blocking)
> +{
[ ... ]
> + while (!(rec = ctlv_event_queue_take(cfile))) {
> + /* Drained, and the device is gone. */
> + if (READ_ONCE(ctlvdev->dead_announced)) {
> + ret = -ENODEV;
> + goto out_unlock;
> + }
[Severity: High]
Is there a race condition between the queue check and checking dead_announced?
The queue is checked locklessly (via ctlv_event_queue_take() returning NULL
and dropping the lock) before checking READ_ONCE(ctlvdev->dead_announced).
If ctlv_event_device_dead() runs exactly between these two checks, it will
queue the removed event and set dead_announced = true. The reader will then
see dead_announced as true and return -ENODEV immediately, completely
missing the freshly queued terminal removed event. This race prevents
userspace from observing the final state of the device.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261015145053.3664509-1-jiri@resnulli.us?part=10
^ permalink raw reply [flat|nested] 32+ messages in thread
* [PATCH RFC 11/20] ctlv: Prepare runtime introspection
2026-10-15 14:50 [PATCH RFC 00/20] attestation: Add a common subsystem and TDX support Jiri Pirko
` (9 preceding siblings ...)
2026-10-15 14:50 ` [PATCH RFC 10/20] ctlv: Prepare event queue support Jiri Pirko
@ 2026-10-15 14:50 ` Jiri Pirko
2026-10-15 14:50 ` [PATCH RFC 12/20] ctlv: Add device and operation dispatch Jiri Pirko
` (8 subsequent siblings)
19 siblings, 0 replies; 32+ messages in thread
From: Jiri Pirko @ 2026-10-15 14:50 UTC (permalink / raw)
To: linux-coco
Cc: linux-kernel, linux-api, x86, kvm, linux-doc, jgg, david.quigley,
lukas, ksinyuk, rodrigo.vivi, cedric.xing, sameo, drdeeglaze,
James.Bottomley, aik, jpiotrowski, qinkun, pgonda, jic23,
alistair.francis, ankita, suzuki.poulose, kas, rick.p.edgecombe,
elena.reshetova, dave.hansen, sathyanarayanan.kuppuswamy,
peter.fang
From: Jiri Pirko <jiri@nvidia.com>
Add handlers that expose registered families, effective operations and
message schemas through the CTLV root family.
The introspection object can be compile-checked here. CONFIG_CTLV remains
unwired until device dispatch completes the module.
Assisted-by: Cursor: GPT-5.6 Sol
Assisted-by: Cursor: Claude Opus 5.5
Signed-off-by: Jiri Pirko <jiri@nvidia.com>
---
drivers/ctlv/Makefile | 3 +-
drivers/ctlv/introspection.c | 512 +++++++++++++++++++++++++++++++++++
2 files changed, 514 insertions(+), 1 deletion(-)
create mode 100644 drivers/ctlv/introspection.c
diff --git a/drivers/ctlv/Makefile b/drivers/ctlv/Makefile
index e631ccd68994..4d09fe26c1d0 100644
--- a/drivers/ctlv/Makefile
+++ b/drivers/ctlv/Makefile
@@ -1,4 +1,5 @@
# SPDX-License-Identifier: GPL-2.0
obj-$(CONFIG_CTLV) += ctlv.o
-ctlv-y += blob.o blob_desc.o builder.o event.o message.o schema.o
+ctlv-y += blob.o blob_desc.o builder.o event.o introspection.o message.o \
+ schema.o
diff --git a/drivers/ctlv/introspection.c b/drivers/ctlv/introspection.c
new file mode 100644
index 000000000000..7460dfa6d661
--- /dev/null
+++ b/drivers/ctlv/introspection.c
@@ -0,0 +1,512 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES
+ *
+ * Root-family discovery queries and their fragmentation.
+ */
+
+#include <linux/bitmap.h>
+#include <linux/bits.h>
+#include <linux/cleanup.h>
+#include <linux/ctlv.h>
+#include <linux/ctlv_gen.h>
+#include <linux/errno.h>
+#include <linux/slab.h>
+#include <linux/spinlock.h>
+#include <linux/types.h>
+
+#include "device.h"
+#include "internal.h"
+#include "schema.h"
+
+/* A cursor packs the record and member where fragmentation resumes. */
+#define CTLV_INTROSPECTION_MEMBER_SHIFT 48
+#define CTLV_INTROSPECTION_RECORD_MASK GENMASK_ULL(CTLV_INTROSPECTION_MEMBER_SHIFT - 1, 0)
+
+struct ctlv_introspection_dump_position {
+ u64 record;
+ u32 member;
+};
+
+static void ctlv_introspection_dump_resume(struct ctlv_introspection_dump_position *pos, u64 cursor)
+{
+ pos->record = cursor & CTLV_INTROSPECTION_RECORD_MASK;
+ pos->member = cursor >> CTLV_INTROSPECTION_MEMBER_SHIFT;
+}
+
+static u64 ctlv_introspection_dump_cursor(const struct ctlv_introspection_dump_position *pos)
+{
+ return pos->record | (u64)pos->member << CTLV_INTROSPECTION_MEMBER_SHIFT;
+}
+
+/* The immutable root-first chain always has generation zero. */
+static bool ctlv_introspection_family_record(struct ctlv_cursor *reply, struct ctlv_cursor *entry,
+ const struct ctlv_family *family,
+ struct ctlv_introspection_dump_position *pos)
+{
+ const struct ctlv_family_schema *schema = family->schema;
+
+ if (!ctlv_family_entry_start(reply, entry))
+ return false;
+ if (pos->member == 0) {
+ if (!ctlv_family_id_put(entry, schema->family_id))
+ return false;
+ pos->member = 1;
+ }
+ if (pos->member == 1) {
+ if (!ctlv_family_name_put(entry, schema->name))
+ return false;
+ pos->member = 2;
+ }
+ ctlv_nest_end(entry);
+ pos->member = 0;
+ return true;
+}
+
+int ctlv_families_dump_query(void *priv, struct ctlv_op_ctx *ctx, const struct ctlv_cursor *request,
+ struct ctlv_cursor *reply)
+{
+ const struct ctlv_family *family = ctx->family;
+ u32 levels = ctlv_family_levels(family);
+ struct ctlv_cursor entry;
+ struct ctlv_introspection_dump_position pos;
+
+ ctlv_introspection_dump_resume(&pos, ctlv_op_query_cursor(ctx));
+ for (; pos.record < levels; pos.record++) {
+ const struct ctlv_family *ancestor =
+ ctlv_family_ancestor(family, levels - 1 - pos.record);
+
+ if (!ctlv_introspection_family_record(reply, &entry, ancestor, &pos))
+ return ctlv_op_query_more(ctx, 0, ctlv_introspection_dump_cursor(&pos));
+ }
+ return ctlv_op_query_done(ctx, 0);
+}
+
+/* Snapshot published-op state and generation once per fragment. */
+struct ctlv_op_snapshot {
+ unsigned long *disabled;
+ u64 generation;
+};
+
+static int ctlv_introspection_snapshot_take(struct ctlv_device *ctlvdev,
+ struct ctlv_op_snapshot *snapshot)
+{
+ if (ctlvdev->dynamic_ops_count) {
+ snapshot->disabled = bitmap_zalloc(ctlvdev->dynamic_ops_count, GFP_KERNEL);
+ if (!snapshot->disabled)
+ return -ENOMEM;
+ }
+ guard(spinlock)
+ (&ctlvdev->transition_lock);
+ snapshot->generation = ctlvdev->state_generation;
+ if (snapshot->disabled)
+ bitmap_copy(snapshot->disabled, ctlvdev->disabled, ctlvdev->dynamic_ops_count);
+ return 0;
+}
+
+static bool ctlv_introspection_op_disabled(const struct ctlv_op_snapshot *snapshot,
+ const struct ctlv_op_entry *op)
+{
+ if (op->state_index == CTLV_OP_STATE_NONE)
+ return false;
+ return test_bit(op->state_index, snapshot->disabled);
+}
+
+/* Report effective lengths, including descendant fills. */
+enum ctlv_introspection_op_field {
+ CTLV_INTROSPECTION_OP_FIELD_ID,
+ CTLV_INTROSPECTION_OP_FIELD_TYPE,
+ CTLV_INTROSPECTION_OP_FIELD_AVAILABILITY,
+ CTLV_INTROSPECTION_OP_FIELD_DISABLED,
+ CTLV_INTROSPECTION_OP_FIELD_REQUEST_MAX_LEN,
+ CTLV_INTROSPECTION_OP_FIELD_ACTION_REPLY_MAX_LEN,
+ CTLV_INTROSPECTION_OP_FIELD_QUERY_REPLY_MIN_LEN,
+ CTLV_INTROSPECTION_OP_FIELD_QUERY_REPLY_SUGGESTED_LEN,
+ CTLV_INTROSPECTION_OP_FIELD_EVENT_RECORD_MAX_LEN,
+ CTLV_INTROSPECTION_OP_FIELD_FD_ACCESS,
+ CTLV_INTROSPECTION_OP_FIELD_END,
+};
+
+static bool ctlv_introspection_op_field_put(struct ctlv_cursor *entry,
+ const struct ctlv_op_snapshot *snapshot,
+ const struct ctlv_op_entry *op, u32 member)
+{
+ const struct ctlv_op_sizes *sizes = &op->sizes;
+
+ switch (member) {
+ case CTLV_INTROSPECTION_OP_FIELD_ID:
+ return ctlv_op_id_put(entry, op->schema->op_id);
+ case CTLV_INTROSPECTION_OP_FIELD_TYPE:
+ return ctlv_op_type_put(entry, op->schema->type);
+ case CTLV_INTROSPECTION_OP_FIELD_AVAILABILITY:
+ return ctlv_op_availability_put(entry, op->schema->availability);
+ case CTLV_INTROSPECTION_OP_FIELD_DISABLED:
+ return ctlv_op_disabled_put(entry, ctlv_introspection_op_disabled(snapshot, op));
+ case CTLV_INTROSPECTION_OP_FIELD_REQUEST_MAX_LEN:
+ return ctlv_op_request_max_len_put(entry, sizes->request_max_len);
+ case CTLV_INTROSPECTION_OP_FIELD_ACTION_REPLY_MAX_LEN:
+ /* Zero is a reply that acknowledges and no more. */
+ return ctlv_op_action_reply_max_len_put(entry, sizes->action_reply_max_len);
+ case CTLV_INTROSPECTION_OP_FIELD_QUERY_REPLY_MIN_LEN:
+ return ctlv_op_query_reply_min_len_put(entry, sizes->query_reply_min_len);
+ case CTLV_INTROSPECTION_OP_FIELD_QUERY_REPLY_SUGGESTED_LEN:
+ return ctlv_op_query_reply_suggested_len_put(entry,
+ sizes->query_reply_suggested_len);
+ case CTLV_INTROSPECTION_OP_FIELD_FD_ACCESS:
+ return ctlv_op_fd_access_put(entry, op->schema->fd_access);
+ default:
+ return ctlv_op_event_record_max_len_put(entry, sizes->event_record_max_len);
+ }
+}
+
+static bool ctlv_introspection_op_field_applies(const struct ctlv_op_entry *op, u32 member)
+{
+ switch (member) {
+ case CTLV_INTROSPECTION_OP_FIELD_ID:
+ case CTLV_INTROSPECTION_OP_FIELD_TYPE:
+ return true;
+ case CTLV_INTROSPECTION_OP_FIELD_AVAILABILITY:
+ case CTLV_INTROSPECTION_OP_FIELD_DISABLED:
+ case CTLV_INTROSPECTION_OP_FIELD_REQUEST_MAX_LEN:
+ case CTLV_INTROSPECTION_OP_FIELD_FD_ACCESS:
+ return op->schema->type != CTLV_OP_TYPE_EVENT;
+ case CTLV_INTROSPECTION_OP_FIELD_ACTION_REPLY_MAX_LEN:
+ return op->schema->type == CTLV_OP_TYPE_ACTION;
+ case CTLV_INTROSPECTION_OP_FIELD_EVENT_RECORD_MAX_LEN:
+ return op->schema->type == CTLV_OP_TYPE_EVENT;
+ default:
+ return op->schema->type == CTLV_OP_TYPE_QUERY;
+ }
+}
+
+static bool ctlv_introspection_op_record(struct ctlv_cursor *reply, struct ctlv_cursor *entry,
+ const struct ctlv_op_snapshot *snapshot,
+ const struct ctlv_op_entry *op,
+ struct ctlv_introspection_dump_position *pos)
+{
+ if (!ctlv_op_entry_start(reply, entry))
+ return false;
+ for (; pos->member < CTLV_INTROSPECTION_OP_FIELD_END; pos->member++) {
+ if (!ctlv_introspection_op_field_applies(op, pos->member))
+ continue;
+ if (!ctlv_introspection_op_field_put(entry, snapshot, op, pos->member))
+ return false;
+ }
+ ctlv_nest_end(entry);
+ pos->member = 0;
+ return true;
+}
+
+int ctlv_ops_dump_query(void *priv, struct ctlv_op_ctx *ctx, const struct ctlv_cursor *request,
+ struct ctlv_cursor *reply)
+{
+ struct ctlv_device *ctlvdev = ctx->ctlvdev;
+ struct ctlv_op_snapshot snapshot = {};
+ struct ctlv_cursor entry;
+ struct ctlv_introspection_dump_position pos;
+ int ret;
+
+ ret = ctlv_introspection_snapshot_take(ctlvdev, &snapshot);
+ if (ret)
+ return ret;
+ unsigned long *disabled __free(bitmap) = snapshot.disabled;
+
+ /* The published index is immutable, so a cursor into it holds. */
+ ctlv_introspection_dump_resume(&pos, ctlv_op_query_cursor(ctx));
+ for (; pos.record < ctlvdev->ops_count; pos.record++)
+ if (!ctlv_introspection_op_record(reply, &entry, &snapshot,
+ &ctlvdev->ops[pos.record], &pos))
+ return ctlv_op_query_more(ctx, snapshot.generation,
+ ctlv_introspection_dump_cursor(&pos));
+ return ctlv_op_query_done(ctx, snapshot.generation);
+}
+
+/* A leaf-count cursor permits a fragment boundary at any tree depth. */
+
+/* One nest of the description: the placement of one node of the schema. */
+struct ctlv_introspection_frame {
+ struct ctlv_cursor nest;
+ u64 attr_id;
+ bool open;
+ /* Which node this stands for, so that a re-entry can be measured. */
+ const struct ctlv_schema_tree *tree;
+ u32 node;
+};
+
+/* The frames outlive the walk, a fragment's open nests having to stay alive. */
+struct ctlv_introspection_walk {
+ const struct ctlv_family *family;
+ struct ctlv_cursor *root;
+ struct ctlv_introspection_frame *frames;
+ u64 ordinal;
+ u64 resume;
+ int error;
+};
+
+/* One member of one node, which is every leaf this reply has. */
+struct ctlv_introspection_field {
+ u64 attr_id;
+ u8 kind;
+ u64 value;
+};
+
+/* Three every node has, the widest bounds, a unit, a scale, a re-entry. */
+#define CTLV_INTROSPECTION_MAX_FIELDS 10
+
+struct ctlv_introspection_fields {
+ struct ctlv_introspection_field fields[CTLV_INTROSPECTION_MAX_FIELDS];
+ u32 fields_count;
+};
+
+static void ctlv_introspection_field(struct ctlv_introspection_fields *fields, u64 attr_id, u8 kind,
+ u64 value)
+{
+ if (WARN_ON_ONCE(fields->fields_count == CTLV_INTROSPECTION_MAX_FIELDS))
+ return;
+ fields->fields[fields->fields_count++] = (struct ctlv_introspection_field){
+ .attr_id = attr_id,
+ .kind = kind,
+ .value = value,
+ };
+}
+
+static void ctlv_introspection_uint(struct ctlv_introspection_fields *fields, u64 attr_id,
+ u64 value)
+{
+ ctlv_introspection_field(fields, attr_id, CTLV_SCHEMA_UINT, value);
+}
+
+/* Emit only constraints for this node's payload kind. */
+static void ctlv_introspection_node_fields(struct ctlv_introspection_fields *fields,
+ const struct ctlv_node_schema *node, u32 reenters)
+{
+ ctlv_introspection_uint(fields, CTLV_ATTR_SCHEMA_ATTR_ID, node->attr_id);
+ ctlv_introspection_uint(fields, CTLV_ATTR_SCHEMA_KIND, node->kind);
+ ctlv_introspection_uint(fields, CTLV_ATTR_SCHEMA_MAX_OCCURRENCES, node->max_occurrences);
+
+ switch (node->kind) {
+ case CTLV_SCHEMA_UINT:
+ case CTLV_SCHEMA_SINT:
+ case CTLV_SCHEMA_ENUM:
+ ctlv_introspection_uint(fields, CTLV_ATTR_SCHEMA_MIN, node->range.min);
+ ctlv_introspection_uint(fields, CTLV_ATTR_SCHEMA_MAX, node->range.max);
+ break;
+ case CTLV_SCHEMA_FLAGS:
+ ctlv_introspection_uint(fields, CTLV_ATTR_SCHEMA_FLAGS_MASK, node->flags_mask);
+ break;
+ case CTLV_SCHEMA_STRING:
+ case CTLV_SCHEMA_BINARY:
+ ctlv_introspection_uint(fields, CTLV_ATTR_SCHEMA_MIN_LENGTH, node->length.min_len);
+ ctlv_introspection_uint(fields, CTLV_ATTR_SCHEMA_MAX_LENGTH, node->length.max_len);
+ break;
+ case CTLV_SCHEMA_BLOB:
+ ctlv_introspection_field(fields, CTLV_ATTR_SCHEMA_BLOB_DIRECTION, CTLV_SCHEMA_ENUM,
+ node->blob.direction);
+ ctlv_introspection_uint(fields, CTLV_ATTR_SCHEMA_BLOB_BACKINGS,
+ node->blob.backings);
+ ctlv_introspection_uint(fields, CTLV_ATTR_SCHEMA_BLOB_MAX_LENGTH,
+ node->blob.max_len);
+ ctlv_introspection_uint(fields, CTLV_ATTR_SCHEMA_BLOB_ALIGNMENT,
+ node->blob.alignment);
+ break;
+ default:
+ break;
+ }
+
+ if (node->unit != CTLV_UNIT_NONE) {
+ ctlv_introspection_uint(fields, CTLV_ATTR_SCHEMA_UNIT, node->unit);
+ ctlv_introspection_field(fields, CTLV_ATTR_SCHEMA_SCALE, CTLV_SCHEMA_SINT,
+ node->scale);
+ }
+ if (reenters)
+ ctlv_introspection_uint(fields, CTLV_ATTR_SCHEMA_REENTERS, reenters);
+}
+
+/* A nest is opened by the first thing written inside it. */
+static bool ctlv_introspection_open(struct ctlv_introspection_walk *walk, u32 depth)
+{
+ struct ctlv_introspection_frame *frame = &walk->frames[depth];
+ struct ctlv_cursor *up = walk->root;
+
+ if (frame->open)
+ return true;
+ if (depth) {
+ if (!ctlv_introspection_open(walk, depth - 1))
+ return false;
+ up = &walk->frames[depth - 1].nest;
+ }
+ if (!ctlv_nest_start(up, &frame->nest, frame->attr_id, CTLV_SCHEMA_NEST))
+ return false;
+ frame->open = true;
+ return true;
+}
+
+/* Skipped leaves advance the ordinal; a failed write remains the resume point. */
+static bool ctlv_introspection_put(struct ctlv_introspection_walk *walk, u32 depth,
+ const struct ctlv_introspection_field *field)
+{
+ struct ctlv_cursor *nest = &walk->frames[depth].nest;
+ bool ok;
+
+ if (walk->ordinal < walk->resume) {
+ walk->ordinal++;
+ return true;
+ }
+ if (!ctlv_introspection_open(walk, depth))
+ return false;
+ if (field->kind == CTLV_SCHEMA_SINT)
+ ok = ctlv_put_s64(nest, field->attr_id, field->kind, field->value);
+ else
+ ok = ctlv_put_u64(nest, field->attr_id, field->kind, field->value);
+ if (!ok)
+ return false;
+ walk->ordinal++;
+ return true;
+}
+
+/* Re-entry targets an ancestor with the same child range. */
+static u32 ctlv_introspection_reenters(struct ctlv_introspection_walk *walk, u32 depth,
+ const struct ctlv_schema_tree *tree,
+ const struct ctlv_node_schema *node)
+{
+ u32 levels;
+
+ for (levels = 1; levels <= depth; levels++) {
+ const struct ctlv_introspection_frame *up = &walk->frames[depth - levels];
+
+ if (up->tree == tree &&
+ tree->node_schemas[up->node].first_child == node->first_child)
+ return levels;
+ }
+ /* A back edge to nothing is metadata this core cannot describe. */
+ WARN_ON_ONCE(1);
+ walk->error = -EINVAL;
+ return 0;
+}
+
+static bool ctlv_introspection_node(struct ctlv_introspection_walk *walk, u32 depth,
+ const struct ctlv_schema_tree *tree, u32 index);
+
+/* Filled stub members inherit the stub placement's optionality. */
+static bool ctlv_introspection_children(struct ctlv_introspection_walk *walk, u32 depth,
+ const struct ctlv_schema_tree *tree, u32 index,
+ bool force_optional)
+{
+ const struct ctlv_node_schema *node = &tree->node_schemas[index];
+ u32 i;
+
+ if (WARN_ON_ONCE(depth + 1 >= CTLV_MAX_NEST_DEPTH)) {
+ /* Deeper than a specification may declare, so unreachable. */
+ walk->error = -EINVAL;
+ return false;
+ }
+ for (i = 0; i < node->children_count; i++) {
+ u32 child = node->first_child + i;
+ bool required = !force_optional && i < node->required_children_count;
+
+ walk->frames[depth + 1] = (struct ctlv_introspection_frame){
+ .attr_id = required ? CTLV_ATTR_SCHEMA_ATTRS :
+ CTLV_ATTR_SCHEMA_OPTIONAL_ATTRS,
+ .tree = tree,
+ .node = child,
+ };
+ if (!ctlv_introspection_node(walk, depth + 1, tree, child))
+ return false;
+ }
+ for (i = 0; i < tree->stub_schemas_count; i++) {
+ const struct ctlv_stub_schema *stub_schema = &tree->stub_schemas[i];
+ const struct ctlv_stub_fill_schema *fill_schema;
+
+ if (stub_schema->node != index)
+ continue;
+ fill_schema =
+ ctlv_schema_fill(walk->family, stub_schema->family_id, stub_schema->set_id);
+ /* Nothing of this chain filled it, so it contributes none. */
+ if (!fill_schema)
+ continue;
+ /* A fill schema's root is the stub, its children the members. */
+ if (!ctlv_introspection_children(walk, depth, &fill_schema->members, 0,
+ force_optional || stub_schema->optional))
+ return false;
+ }
+ return true;
+}
+
+/* One node, into the nest that stands for it. */
+static bool ctlv_introspection_node(struct ctlv_introspection_walk *walk, u32 depth,
+ const struct ctlv_schema_tree *tree, u32 index)
+{
+ const struct ctlv_node_schema *node = &tree->node_schemas[index];
+ struct ctlv_introspection_fields fields = {};
+ u32 reenters = 0;
+ u32 i;
+
+ if (node->reenters) {
+ reenters = ctlv_introspection_reenters(walk, depth, tree, node);
+ if (!reenters)
+ return false;
+ }
+ ctlv_introspection_node_fields(&fields, node, reenters);
+ for (i = 0; i < fields.fields_count; i++)
+ if (!ctlv_introspection_put(walk, depth, &fields.fields[i]))
+ return false;
+ /*
+ * A node that re-enters a level says so instead of listing that level's
+ * members again, which is what makes an unbounded nesting describable.
+ */
+ if (!node->reenters && !ctlv_introspection_children(walk, depth, tree, index, false))
+ return false;
+ if (walk->frames[depth].open)
+ ctlv_nest_end(&walk->frames[depth].nest);
+ return true;
+}
+
+/* One of the two messages of an op, the root of it being a node like any. */
+static bool ctlv_introspection_message(struct ctlv_introspection_walk *walk, u64 attr_id,
+ const struct ctlv_schema_tree *tree)
+{
+ /* An op without such a message has nothing to describe. */
+ if (!tree->node_schemas)
+ return true;
+ walk->frames[0] = (struct ctlv_introspection_frame){
+ .attr_id = attr_id,
+ .tree = tree,
+ };
+ return ctlv_introspection_node(walk, 0, tree, 0);
+}
+
+int ctlv_op_schema_get_query(void *priv, struct ctlv_op_ctx *ctx, const struct ctlv_cursor *request,
+ struct ctlv_cursor *reply)
+{
+ struct ctlv_introspection_walk walk = {
+ .family = ctx->family,
+ .root = reply,
+ .resume = ctlv_op_query_cursor(ctx),
+ };
+ const struct ctlv_op_entry *op;
+
+ /* Describe published ops regardless of current dynamic state. */
+ op = ctlv_op_find(ctx->ctlvdev, ctlv_op_schema_get_op_id_get(request));
+ if (!op)
+ return -EOPNOTSUPP;
+
+ /*
+ * One frame per level the reply may nest, too many for the stack: a
+ * description is one level deeper than the message it describes.
+ */
+ struct ctlv_introspection_frame *frames __free(kfree) =
+ kcalloc(CTLV_MAX_NEST_DEPTH, sizeof(*frames), GFP_KERNEL);
+
+ if (!frames)
+ return -ENOMEM;
+ walk.frames = frames;
+
+ if (!ctlv_introspection_message(&walk, CTLV_ATTR_SCHEMA_REQUEST, &op->schema->request) ||
+ !ctlv_introspection_message(&walk, CTLV_ATTR_SCHEMA_REPLY, &op->schema->reply)) {
+ if (walk.error)
+ return walk.error;
+ /* A schema is immutable, so there is nothing to be stale. */
+ return ctlv_op_query_more(ctx, 0, walk.ordinal);
+ }
+ return ctlv_op_query_done(ctx, 0);
+}
--
2.56.0
^ permalink raw reply related [flat|nested] 32+ messages in thread* [PATCH RFC 12/20] ctlv: Add device and operation dispatch
2026-10-15 14:50 [PATCH RFC 00/20] attestation: Add a common subsystem and TDX support Jiri Pirko
` (10 preceding siblings ...)
2026-10-15 14:50 ` [PATCH RFC 11/20] ctlv: Prepare runtime introspection Jiri Pirko
@ 2026-10-15 14:50 ` Jiri Pirko
2026-10-15 15:28 ` sashiko-bot
2026-10-15 14:50 ` [PATCH RFC 13/20] Documentation: ctlv: Describe how to write a family Jiri Pirko
` (7 subsequent siblings)
19 siblings, 1 reply; 32+ messages in thread
From: Jiri Pirko @ 2026-10-15 14:50 UTC (permalink / raw)
To: linux-coco
Cc: linux-kernel, linux-api, x86, kvm, linux-doc, jgg, david.quigley,
lukas, ksinyuk, rodrigo.vivi, cedric.xing, sameo, drdeeglaze,
James.Bottomley, aik, jpiotrowski, qinkun, pgonda, jic23,
alistair.francis, ankita, suzuki.poulose, kas, rick.p.edgecombe,
elena.reshetova, dave.hansen, sathyanarayanan.kuppuswamy,
peter.fang
From: Jiri Pirko <jiri@nvidia.com>
Register CTLV devices, resolve family chains and dispatch operations
through validated messages. Wire CONFIG_CTLV and the generated root family
after all module objects are present.
Assisted-by: Cursor: GPT-5.6 Sol
Assisted-by: Cursor: Claude Opus 5.5
Signed-off-by: Jiri Pirko <jiri@nvidia.com>
---
drivers/Kconfig | 2 +
drivers/Makefile | 1 +
drivers/ctlv/Makefile | 4 +-
drivers/ctlv/core.c | 1517 +++++++++++++++++++++++++++++++++++
drivers/ctlv/device.h | 17 +
include/linux/ctlv.h | 55 ++
include/linux/ctlv_family.h | 22 +
7 files changed, 1616 insertions(+), 2 deletions(-)
create mode 100644 drivers/ctlv/core.c
diff --git a/drivers/Kconfig b/drivers/Kconfig
index f2bed2ddeb66..9acc7f74c5c8 100644
--- a/drivers/Kconfig
+++ b/drivers/Kconfig
@@ -25,6 +25,8 @@ source "drivers/cache/Kconfig"
source "drivers/connector/Kconfig"
+source "drivers/ctlv/Kconfig"
+
source "drivers/firmware/Kconfig"
source "drivers/fwctl/Kconfig"
diff --git a/drivers/Makefile b/drivers/Makefile
index 0841ea851847..f2c0c3a367ea 100644
--- a/drivers/Makefile
+++ b/drivers/Makefile
@@ -135,6 +135,7 @@ obj-$(CONFIG_MEMSTICK) += memstick/
obj-$(CONFIG_INFINIBAND) += infiniband/
obj-y += firmware/
obj-$(CONFIG_FWCTL) += fwctl/
+obj-$(CONFIG_CTLV) += ctlv/
obj-$(CONFIG_CRYPTO) += crypto/
obj-$(CONFIG_SUPERH) += sh/
obj-y += clocksource/
diff --git a/drivers/ctlv/Makefile b/drivers/ctlv/Makefile
index 4d09fe26c1d0..2f6fe8d2db73 100644
--- a/drivers/ctlv/Makefile
+++ b/drivers/ctlv/Makefile
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: GPL-2.0
obj-$(CONFIG_CTLV) += ctlv.o
-ctlv-y += blob.o blob_desc.o builder.o event.o introspection.o message.o \
- schema.o
+ctlv-y += blob.o blob_desc.o builder.o core.o event.o introspection.o \
+ message.o schema.o ctlv_gen.o
diff --git a/drivers/ctlv/core.c b/drivers/ctlv/core.c
new file mode 100644
index 000000000000..aeab29fe2fdd
--- /dev/null
+++ b/drivers/ctlv/core.c
@@ -0,0 +1,1517 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES
+ */
+#define pr_fmt(fmt) "ctlv: " fmt
+#include <linux/align.h>
+#include <linux/bitmap.h>
+#include <linux/bitops.h>
+#include <linux/build_bug.h>
+#include <linux/cache.h>
+#include <linux/cleanup.h>
+#include <linux/container_of.h>
+#include <linux/ctlv.h>
+#include <linux/ctlv_gen.h>
+#include <linux/device.h>
+#include <linux/err.h>
+#include <linux/fs.h>
+#include <linux/idr.h>
+#include <linux/kdev_t.h>
+#include <linux/kobject.h>
+#include <linux/list.h>
+#include <linux/lockdep.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/overflow.h>
+#include <linux/poll.h>
+#include <linux/slab.h>
+#include <linux/stddef.h>
+#include <linux/string.h>
+#include <linux/uaccess.h>
+#include <linux/vmalloc.h>
+
+#include <uapi/linux/ctlv.h>
+
+#include "blob.h"
+#include "builder.h"
+#include "device.h"
+#include "event.h"
+#include "internal.h"
+#include "schema.h"
+
+enum {
+ CTLV_MAX_DEVICES = 4096,
+};
+
+/*
+ * One layout on every ABI, including compat. Alignment is asserted too because
+ * a 32-bit ABI aligns a plain __u64 to four bytes.
+ */
+static_assert(sizeof(struct ctlv_attr) == 16);
+static_assert(__alignof__(struct ctlv_attr) == CTLV_ALIGNTO);
+
+static_assert(sizeof(struct ctlv_op_exec) == 64);
+static_assert(__alignof__(struct ctlv_op_exec) == CTLV_ALIGNTO);
+
+static_assert(sizeof(struct ctlv_event_hdr) == 24);
+static_assert(__alignof__(struct ctlv_event_hdr) == CTLV_ALIGNTO);
+
+static dev_t ctlv_device_number;
+static DEFINE_IDA(ctlv_device_ids);
+
+/* Every family priv has core-owned links immediately before it. */
+#define CTLV_PRIV_ALIGN SMP_CACHE_BYTES
+
+struct ctlv_priv_container {
+ struct ctlv_priv_container *parent;
+ struct ctlv_priv_container *child;
+ u8 priv[] __aligned(CTLV_PRIV_ALIGN);
+};
+
+static inline const struct ctlv_priv_container *ctlv_priv_container(const void *priv)
+{
+ return container_of_const(priv, struct ctlv_priv_container, priv);
+}
+
+struct ctlv_family_priv {
+ const struct ctlv_family *family;
+ void *priv;
+ size_t priv_offset;
+ const void *init_info;
+ bool initialized;
+};
+
+/* Claims persist across unload so one ID cannot later name another family. */
+struct ctlv_family_claim {
+ struct list_head entry;
+ char *name;
+ u16 family_id;
+};
+
+static LIST_HEAD(ctlv_family_claims);
+static DEFINE_MUTEX(ctlv_family_claims_lock);
+
+static int ctlv_family_claim_add(const struct ctlv_family_schema *schema)
+{
+ struct ctlv_family_claim *claim;
+
+ guard(mutex)
+ (&ctlv_family_claims_lock);
+ list_for_each_entry(claim, &ctlv_family_claims, entry) {
+ bool same_id = claim->family_id == schema->family_id;
+ bool same_name = !strcmp(claim->name, schema->name);
+
+ if (same_id && same_name)
+ return 0;
+ if (same_id || same_name) {
+ pr_err("family %s wants ID %u, which belongs to %s\n", schema->name,
+ schema->family_id, claim->name);
+ return -EEXIST;
+ }
+ }
+
+ claim = kzalloc_obj(*claim, GFP_KERNEL);
+ if (!claim)
+ return -ENOMEM;
+ claim->name = kstrdup(schema->name, GFP_KERNEL);
+ if (!claim->name) {
+ kfree(claim);
+ return -ENOMEM;
+ }
+ claim->family_id = schema->family_id;
+ list_add_tail(&claim->entry, &ctlv_family_claims);
+ return 0;
+}
+
+static void ctlv_family_claims_free(void)
+{
+ struct ctlv_family_claim *claim, *next;
+
+ list_for_each_entry_safe(claim, next, &ctlv_family_claims, entry) {
+ list_del(&claim->entry);
+ kfree(claim->name);
+ kfree(claim);
+ }
+}
+
+/*
+ * Everything resolved below is generated, so a failure here is a bug, checked
+ * anyway because an out-of-tree family comes from another generator.
+ */
+
+static const char *ctlv_family_name(const struct ctlv_family *family)
+{
+ if (!family->schema || !family->schema->name)
+ return "(unnamed)";
+ return family->schema->name;
+}
+
+static int ctlv_family_bad(const struct ctlv_family *family, const char *what)
+{
+ pr_err("family %s: %s\n", ctlv_family_name(family), what);
+ return -EINVAL;
+}
+
+static int ctlv_op_bad(const struct ctlv_family *family, u64 op_id, const char *what)
+{
+ pr_err("family %s: op %#018llx %s\n", ctlv_family_name(family), op_id, what);
+ return -EINVAL;
+}
+
+/*
+ * A family is named its parent's whole name plus a suffix, so one name states
+ * the chain it belongs to and every family of this core is ctlv-something.
+ */
+static int ctlv_family_chain_check(const struct ctlv_family *family)
+{
+ const struct ctlv_family *child;
+
+ for (child = family; child != &ctlv_family; child = child->parent) {
+ const struct ctlv_family *parent;
+ size_t len;
+
+ if (!child)
+ return ctlv_family_bad(family, "heads no chain of the core's");
+ parent = child->parent;
+ if (!child->schema || !parent || !parent->schema)
+ return ctlv_family_bad(child, "does not name a complete parent descriptor");
+ if (!child->schema->name || !parent->schema->name)
+ return ctlv_family_bad(family, "inherits a family with no name");
+ if (parent->schema->family_id >= child->schema->family_id)
+ return ctlv_family_bad(child, "does not follow its parent in the registry");
+ len = strlen(parent->schema->name);
+ if (!len || strncmp(child->schema->name, parent->schema->name, len) ||
+ child->schema->name[len] != '-')
+ return ctlv_family_bad(family, "is not named after the family it inherits");
+ }
+ return 0;
+}
+
+/* One descriptor of a chain: what it answers with, and what it fills. */
+static int ctlv_family_descriptor_check(const struct ctlv_family *family)
+{
+ const struct ctlv_family_schema *schema = family->schema;
+ u32 index;
+
+ /*
+ * A family that declares no op of its own binds nothing: what it
+ * inherited is answered where it was declared.
+ */
+ if (!schema || !schema->name || !*schema->name || (family->ops_count && !family->ops))
+ return ctlv_family_bad(family, "descriptor is incomplete");
+ /* Nothing in common means an empty pair, not an absent one. */
+ if (!family->pre || !family->post)
+ return ctlv_family_bad(family, "has no hook pair");
+ if (!family->priv_size || !family->init || !family->fini)
+ return ctlv_family_bad(family, "has no priv lifecycle");
+ /* Every device answers the root family already, as the core. */
+ if (schema->family_id == CTLV_FAMILY_ROOT && family != &ctlv_family)
+ return ctlv_family_bad(family, "claims the root family ID");
+ /* An extension point is answered by whichever family filled it. */
+ if (family->stub_ops_count != schema->stub_fill_schemas_count)
+ return ctlv_family_bad(family, "answers other extension points than it fills");
+ for (index = 0; index < family->stub_ops_count; index++) {
+ const struct ctlv_stub_fill_schema *fill_schema = &schema->stub_fill_schemas[index];
+ const struct ctlv_stub_ops *stub_ops = &family->stub_ops[index];
+
+ if (stub_ops->family_id == fill_schema->family_id &&
+ stub_ops->set_id == fill_schema->set_id)
+ continue;
+ return ctlv_family_bad(family, "answers an extension point it does not fill");
+ }
+ return 0;
+}
+
+/* Every descriptor binds its local schema and supplies a complete lifecycle. */
+static int ctlv_family_descriptors_check(const struct ctlv_family *family)
+{
+ const struct ctlv_family *descriptor;
+
+ for (descriptor = family; descriptor; descriptor = descriptor->parent) {
+ int ret;
+
+ ret = ctlv_family_descriptor_check(descriptor);
+ if (ret)
+ return ret;
+ }
+ return 0;
+}
+
+/* The graph a device answers over, and the descriptors standing on it. */
+static int ctlv_family_check(const struct ctlv_family *family)
+{
+ int ret;
+
+ if (!family->schema)
+ return ctlv_family_bad(family, "descriptor is incomplete");
+ ret = ctlv_family_chain_check(family);
+ if (ret)
+ return ret;
+ return ctlv_family_descriptors_check(family);
+}
+
+static struct ctlv_family_priv *ctlv_family_priv_find(const struct ctlv_device *ctlvdev,
+ const struct ctlv_family *family)
+{
+ u32 index;
+
+ for (index = 0; index < ctlvdev->privs_count; index++)
+ if (ctlvdev->privs[index].family == family)
+ return &ctlvdev->privs[index];
+ return NULL;
+}
+
+static void ctlv_family_privs_free(struct ctlv_device *ctlvdev)
+{
+ kvfree(ctlvdev->priv_block);
+ ctlvdev->priv_block = NULL;
+ kfree(ctlvdev->privs);
+ ctlvdev->privs = NULL;
+ ctlvdev->privs_count = 0;
+}
+
+static int ctlv_family_privs_alloc(struct ctlv_device *ctlvdev, const struct ctlv_family *family)
+{
+ struct ctlv_priv_container *parent = NULL;
+ void *aligned;
+ size_t alloc_len;
+ size_t len = 0;
+ u32 index;
+
+ ctlvdev->privs_count = ctlv_family_levels(family);
+ ctlvdev->privs = kcalloc(ctlvdev->privs_count, sizeof(*ctlvdev->privs), GFP_KERNEL);
+ if (!ctlvdev->privs)
+ return -ENOMEM;
+
+ for (index = 0; index < ctlvdev->privs_count; index++) {
+ struct ctlv_family_priv *family_priv = &ctlvdev->privs[index];
+ size_t size, aligned_len;
+
+ family_priv->family =
+ ctlv_family_ancestor(family, ctlvdev->privs_count - index - 1);
+ size = max_t(size_t, family_priv->family->priv_size(), 1);
+ if (check_add_overflow(len, offsetof(struct ctlv_priv_container, priv),
+ &aligned_len) ||
+ check_add_overflow(aligned_len, CTLV_PRIV_ALIGN - 1, &aligned_len))
+ goto overflow;
+ aligned_len = ALIGN_DOWN(aligned_len, CTLV_PRIV_ALIGN);
+ family_priv->priv_offset = aligned_len;
+ if (check_add_overflow(aligned_len, size, &len))
+ goto overflow;
+ }
+ if (check_add_overflow(len, CTLV_PRIV_ALIGN - 1, &alloc_len))
+ goto overflow;
+ ctlvdev->priv_block = kvzalloc(alloc_len, GFP_KERNEL);
+ if (!ctlvdev->priv_block) {
+ ctlv_family_privs_free(ctlvdev);
+ return -ENOMEM;
+ }
+ aligned = PTR_ALIGN(ctlvdev->priv_block, CTLV_PRIV_ALIGN);
+
+ for (index = 0; index < ctlvdev->privs_count; index++) {
+ struct ctlv_family_priv *family_priv = &ctlvdev->privs[index];
+ struct ctlv_priv_container *container;
+ void *priv;
+
+ priv = (u8 *)aligned + family_priv->priv_offset;
+ family_priv->priv = priv;
+ container = container_of_const(priv, struct ctlv_priv_container, priv);
+ container->parent = parent;
+ container->child = NULL;
+ if (parent)
+ parent->child = container;
+ parent = container;
+ }
+ return 0;
+
+overflow:
+ ctlv_family_privs_free(ctlvdev);
+ return -EOVERFLOW;
+}
+
+static int ctlv_family_privs_pre_init(struct ctlv_device *ctlvdev, const void *init_info)
+{
+ u32 index;
+
+ for (index = ctlvdev->privs_count; index; index--) {
+ struct ctlv_family_priv *family_priv = &ctlvdev->privs[index - 1];
+
+ family_priv->init_info = init_info;
+ if (!family_priv->family->pre_init) {
+ init_info = NULL;
+ continue;
+ }
+ init_info = family_priv->family->pre_init(init_info);
+ if (IS_ERR(init_info))
+ return PTR_ERR(init_info);
+ }
+ if (WARN_ON_ONCE(init_info))
+ return -EINVAL;
+ return 0;
+}
+
+static void ctlv_family_privs_pre_fini(struct ctlv_device *ctlvdev)
+{
+ u32 index;
+
+ lockdep_assert_not_held(&ctlvdev->registration_lock);
+ lockdep_assert_not_held(&ctlvdev->transition_lock);
+ lockdep_assert_not_held(&ctlvdev->events_lock);
+
+ for (index = 0; index < ctlvdev->privs_count; index++) {
+ struct ctlv_family_priv *family_priv = &ctlvdev->privs[index];
+
+ if (!family_priv->initialized || !family_priv->family->pre_fini)
+ continue;
+ family_priv->family->pre_fini(ctlvdev, family_priv->priv);
+ }
+}
+
+static void ctlv_family_privs_fini_only(struct ctlv_device *ctlvdev)
+{
+ u32 index;
+
+ for (index = ctlvdev->privs_count; index; index--) {
+ struct ctlv_family_priv *family_priv = &ctlvdev->privs[index - 1];
+
+ if (!family_priv->initialized)
+ continue;
+ family_priv->family->fini(ctlvdev, family_priv->priv);
+ family_priv->initialized = false;
+ }
+}
+
+static int ctlv_family_privs_init(struct ctlv_device *ctlvdev)
+{
+ u32 index;
+ int ret;
+
+ for (index = 0; index < ctlvdev->privs_count; index++) {
+ struct ctlv_family_priv *family_priv = &ctlvdev->privs[index];
+
+ ret = family_priv->family->init(ctlvdev, family_priv->init_info, family_priv->priv);
+ family_priv->init_info = NULL;
+ if (ret) {
+ ctlv_family_privs_fini_only(ctlvdev);
+ return ret;
+ }
+ family_priv->initialized = true;
+ }
+ return 0;
+}
+
+static int ctlv_family_privs_post_init(struct ctlv_device *ctlvdev)
+{
+ u32 index;
+ int ret;
+
+ for (index = ctlvdev->privs_count; index; index--) {
+ struct ctlv_family_priv *family_priv = &ctlvdev->privs[index - 1];
+
+ if (!family_priv->family->post_init)
+ continue;
+ ret = family_priv->family->post_init(ctlvdev, family_priv->priv);
+ if (ret)
+ return ret;
+ }
+ return 0;
+}
+
+static void ctlv_device_fini(struct ctlv_device *ctlvdev)
+{
+ ctlv_family_privs_pre_fini(ctlvdev);
+
+ /* No provider can remain after pre_fini has drained its consumers. */
+ scoped_guard(spinlock, &ctlvdev->transition_lock) {
+ ctlvdev->destroying = true;
+ ctlv_event_contexts_stop(ctlvdev);
+ }
+ ctlv_event_contexts_wait(ctlvdev);
+ ctlv_event_device_dead(ctlvdev);
+
+ ctlv_family_privs_fini_only(ctlvdev);
+}
+
+/* What one walk of the chain has accounted for so far. */
+struct ctlv_op_resolution {
+ struct ctlv_device *ctlvdev;
+ const struct ctlv_family *target;
+ const struct ctlv_family *family;
+ u32 bound;
+ u32 dynamic;
+ u32 events;
+ u64 previous;
+};
+
+/* Every binding of one family answers an op, so a spare one answers nothing. */
+static int ctlv_op_resolution_family_bound(const struct ctlv_op_resolution *state)
+{
+ const struct ctlv_family *family = state->family;
+
+ if (!family || state->bound == family->ops_count)
+ return 0;
+ /* The core's own table is this core's bug, not a family's. */
+ if (WARN_ON_ONCE(family == &ctlv_family))
+ return -EINVAL;
+ return ctlv_family_bad(family, "binds more than its schema asks for");
+}
+
+/*
+ * The ops of one family are contiguous in the walk, so the family before this
+ * one is complete as soon as another family's op arrives.
+ */
+static int ctlv_op_resolution_family_enter(struct ctlv_op_resolution *state,
+ const struct ctlv_family *family)
+{
+ int ret;
+
+ if (state->family == family)
+ return 0;
+ ret = ctlv_op_resolution_family_bound(state);
+ if (ret)
+ return ret;
+ state->family = family;
+ state->bound = 0;
+ return 0;
+}
+
+static int ctlv_op_resolution_add(struct ctlv_op_resolution *state, const struct ctlv_family *owner,
+ const struct ctlv_op_schema *op, struct ctlv_op_entry *entry)
+{
+ const struct ctlv_family *target = state->target;
+ const struct ctlv_family *family = state->family;
+ struct ctlv_family_priv *family_priv;
+ int ret;
+
+ family_priv = ctlv_family_priv_find(state->ctlvdev, family);
+ if (WARN_ON_ONCE(!family_priv))
+ return -EINVAL;
+ entry->schema = op;
+ entry->priv = family_priv->priv;
+ entry->state_index = CTLV_OP_STATE_NONE;
+
+ /*
+ * An op ID is its family's ID and a number within it, so a walk from
+ * the oldest ancestor down ascends, which covers uniqueness too.
+ */
+ if (!CTLV_OP_NUMBER(op->op_id) || CTLV_OP_FAMILY(op->op_id) != owner->schema->family_id)
+ return ctlv_op_bad(target, op->op_id, "belongs to another family");
+ if (op->op_id <= state->previous)
+ return ctlv_op_bad(target, op->op_id, "is out of order");
+ state->previous = op->op_id;
+
+ switch (op->type) {
+ case CTLV_OP_TYPE_ACTION:
+ case CTLV_OP_TYPE_QUERY:
+ if (op->fd_access != CTLV_OP_FD_ACCESS_READ &&
+ op->fd_access != CTLV_OP_FD_ACCESS_READ_WRITE)
+ return ctlv_op_bad(family, op->op_id, "has an unknown fd access");
+ /* The family that declared the op is the one that answers it. */
+ if (state->bound >= family->ops_count)
+ return ctlv_op_bad(family, op->op_id, "has nothing bound to it");
+ entry->binding = &family->ops[state->bound++];
+ /* Required even for an op this device disables or withholds. */
+ if (!entry->binding->action)
+ return ctlv_op_bad(family, op->op_id, "has no callback");
+ /* An optional op arrives absent, for the opt-in to clear. */
+ entry->absent = op->optional;
+ /* An op has a whole pair of hooks of its own, or none. */
+ if (!entry->binding->pre != !entry->binding->post)
+ return ctlv_op_bad(family, op->op_id, "has half a hook pair");
+ /* A pair is on no wire, so this is the only check of it. */
+ if (!entry->binding->pre != !op->pre_post)
+ return ctlv_op_bad(family, op->op_id, "disagrees about its hook pair");
+ break;
+ case CTLV_OP_TYPE_EVENT:
+ if (op->fd_access)
+ return ctlv_op_bad(target, op->op_id, "is an event with fd access");
+ if (op->optional)
+ return ctlv_op_bad(target, op->op_id, "is an event a device could omit");
+ /*
+ * Produced rather than answered, so nothing to bind: an event
+ * gets a subscription bit, numbered as the walk meets it.
+ */
+ entry->event_index = state->events++;
+ break;
+ default:
+ return ctlv_op_bad(target, op->op_id, "has an unknown type");
+ }
+
+ switch (op->availability) {
+ case CTLV_OP_AVAILABILITY_FIXED:
+ break;
+ case CTLV_OP_AVAILABILITY_DYNAMIC:
+ if (op->type == CTLV_OP_TYPE_EVENT)
+ return ctlv_op_bad(target, op->op_id, "is an event a device could disable");
+ /*
+ * Numbered from zero as the walk meets them, per device: two
+ * families of one chain need not have the same ops.
+ */
+ entry->state_index = state->dynamic++;
+ break;
+ default:
+ return ctlv_op_bad(target, op->op_id, "has an unknown availability");
+ }
+
+ /*
+ * Once per device: what an inherited op may carry depends on the
+ * extension points this family filled and on nothing that changes later.
+ */
+ ret = ctlv_schema_op_sizes(target, op, &entry->sizes);
+ if (ret)
+ return ctlv_op_bad(target, op->op_id, "needs messages this core cannot carry");
+ return 0;
+}
+
+static int ctlv_op_resolution_init(struct ctlv_device *ctlvdev, const struct ctlv_family *family)
+{
+ struct ctlv_op_resolution state = {
+ .ctlvdev = ctlvdev,
+ .target = family,
+ };
+ const struct ctlv_family *owner;
+ const struct ctlv_op_schema *op;
+ struct ctlv_schema_cursor cursor;
+ u32 ops_count = 0;
+ int ret;
+
+ ret = ctlv_family_check(family);
+ if (ret)
+ return ret;
+ ret = ctlv_family_privs_alloc(ctlvdev, family);
+ if (ret)
+ return ret;
+
+ ctlv_schema_cursor_init(&cursor, family);
+ while (ctlv_schema_next_op(&cursor, &owner))
+ ops_count++;
+ if (!ops_count)
+ return ctlv_family_bad(family, "has an effective schema with no op");
+
+ ctlvdev->ops = kcalloc(ops_count, sizeof(*ctlvdev->ops), GFP_KERNEL);
+ if (!ctlvdev->ops)
+ return -ENOMEM;
+ ctlvdev->ops_count = ops_count;
+
+ ops_count = 0;
+ ctlv_schema_cursor_init(&cursor, family);
+ while ((op = ctlv_schema_next_op(&cursor, &owner))) {
+ ret = ctlv_op_resolution_family_enter(&state, owner);
+ if (ret)
+ return ret;
+ ret = ctlv_op_resolution_add(&state, owner, op, &ctlvdev->ops[ops_count++]);
+ if (ret)
+ return ret;
+ }
+ /* The device family, no op of another having ended its table. */
+ ret = ctlv_op_resolution_family_bound(&state);
+ if (ret)
+ return ret;
+ ctlvdev->dynamic_ops_count = state.dynamic;
+ ctlvdev->events_count = state.events;
+ return 0;
+}
+
+void *ctlv_priv_parent(const void *priv)
+{
+ const struct ctlv_priv_container *container = ctlv_priv_container(priv);
+
+ return container->parent ? container->parent->priv : NULL;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_priv_parent, "CTLV");
+
+void *ctlv_priv_child(const void *priv)
+{
+ const struct ctlv_priv_container *container = ctlv_priv_container(priv);
+
+ return container->child ? container->child->priv : NULL;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_priv_child, "CTLV");
+
+struct device *ctlv_device_parent(const struct ctlv_device *ctlvdev)
+{
+ return ctlvdev->dev.parent;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_device_parent, "CTLV");
+
+/*
+ * Dispatch reads the availability bit under no lock, so an op already admitted
+ * is neither cancelled nor waited for by a disable.
+ */
+
+struct ctlv_op_entry *ctlv_op_find(struct ctlv_device *ctlvdev, u64 op_id)
+{
+ u32 index;
+
+ /* The index ascends by op ID, so a miss stops early. */
+ for (index = 0; index < ctlvdev->ops_count; index++) {
+ if (ctlvdev->ops[index].schema->op_id == op_id)
+ return &ctlvdev->ops[index];
+ if (ctlvdev->ops[index].schema->op_id > op_id)
+ break;
+ }
+ return NULL;
+}
+
+static bool ctlv_op_disabled(struct ctlv_device *ctlvdev, const struct ctlv_op_entry *op)
+{
+ if (op->state_index == CTLV_OP_STATE_NONE)
+ return false;
+ /* Acquire, so what the family prepared before enabling is visible. */
+ return test_bit_acquire(op->state_index, ctlvdev->disabled);
+}
+
+static void ctlv_op_publish_all(struct ctlv_device *ctlvdev)
+{
+ u32 published = 0;
+ u32 index;
+
+ /* Against a family still opting in as this is called. */
+ guard(spinlock)
+ (&ctlvdev->transition_lock);
+ for (index = 0; index < ctlvdev->ops_count; index++) {
+ if (ctlvdev->ops[index].absent)
+ continue;
+ ctlvdev->ops[published++] = ctlvdev->ops[index];
+ }
+ ctlvdev->ops_count = published;
+ ctlvdev->published = true;
+}
+
+void ctlv_device_op_provide(struct ctlv_device *ctlvdev, u64 op_id)
+{
+ struct ctlv_op_entry *op;
+
+ guard(spinlock)
+ (&ctlvdev->transition_lock);
+ if (WARN_ON_ONCE(ctlvdev->published || ctlvdev->destroying))
+ return;
+ op = ctlv_op_find(ctlvdev, op_id);
+ /* An event is produced rather than answered, so there is none. */
+ if (WARN_ON_ONCE(!op) || WARN_ON_ONCE(!op->binding))
+ return;
+ op->absent = false;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_device_op_provide, "CTLV");
+
+void ctlv_device_op_state_set(struct ctlv_device *ctlvdev, u64 op_id, bool disabled)
+{
+ const struct ctlv_op_entry *op;
+
+ /*
+ * Also serializes lookup against publication, which compacts the
+ * array, and against teardown stopping state changes.
+ */
+ guard(spinlock)
+ (&ctlvdev->transition_lock);
+ if (ctlvdev->destroying)
+ return;
+ op = ctlv_op_find(ctlvdev, op_id);
+ if (WARN_ON_ONCE(!op) || WARN_ON_ONCE(op->state_index == CTLV_OP_STATE_NONE))
+ return;
+ /* An op this device never took on has no state to change. */
+ if (WARN_ON_ONCE(op->absent))
+ return;
+ /*
+ * Fully ordered, releasing what the family prepared to the acquire in
+ * ctlv_op_disabled(). The old state makes a repeat change nothing.
+ */
+ if (disabled ? test_and_set_bit(op->state_index, ctlvdev->disabled) :
+ !test_and_clear_bit(op->state_index, ctlvdev->disabled))
+ return;
+ if (!ctlvdev->published)
+ return;
+ ctlvdev->state_generation++;
+ /* Still holding the serializer, so records reach queues in order. */
+ ctlv_event_op_changed(ctlvdev, op, disabled, ctlvdev->state_generation);
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_device_op_state_set, "CTLV");
+
+static int ctlv_op_check_mode(const struct file *filp, const struct ctlv_op_entry *op)
+{
+ if (op->schema->type == CTLV_OP_TYPE_EVENT)
+ return -EOPNOTSUPP;
+
+ switch (op->schema->fd_access) {
+ case CTLV_OP_FD_ACCESS_READ:
+ return filp->f_mode & FMODE_READ ? 0 : -EACCES;
+ case CTLV_OP_FD_ACCESS_READ_WRITE:
+ return (filp->f_mode & (FMODE_READ | FMODE_WRITE)) == (FMODE_READ | FMODE_WRITE) ?
+ 0 :
+ -EACCES;
+ default:
+ /* Registration rejects this before the device is visible. */
+ return -EACCES;
+ }
+}
+
+static u32 ctlv_reply_needed(const struct ctlv_op_entry *op)
+{
+ if (op->schema->type == CTLV_OP_TYPE_QUERY)
+ return op->sizes.query_reply_min_len;
+ return op->sizes.action_reply_max_len;
+}
+
+/*
+ * request_len is the copy boundary; root length frames the message. Ignored
+ * unknown attrs may exceed the schema maximum up to the transport limit.
+ */
+static int ctlv_request_frame(struct ctlv_op_ctx *ctx, const struct ctlv_op_exec *exec,
+ const struct ctlv_attr *root)
+{
+ if (root->len < sizeof(*root) || root->len > exec->request_len ||
+ root->attr_id != CTLV_ATTR_ROOT || root->flags != CTLV_ATTR_F_NESTED) {
+ ctx->error.reason = CTLV_VALIDATION_REASON_MALFORMED_MESSAGE;
+ return -EINVAL;
+ }
+ return 0;
+}
+
+static int ctlv_request_validate(struct ctlv_op_ctx *ctx, const struct ctlv_op_exec *exec,
+ struct ctlv_attr *root)
+{
+ int ret;
+
+ ret = ctlv_request_frame(ctx, exec, root);
+ if (ret)
+ return ret;
+ if (!ctx->op->schema->request.node_schemas)
+ return 0;
+
+ ctx->request = (struct ctlv_msg){
+ .root = root,
+ .len = exec->request_len,
+ .tree = &ctx->op->schema->request,
+ .family = ctx->family,
+ .request = true,
+ };
+ return ctlv_schema_validate(&ctx->request, &ctx->error);
+}
+
+/*
+ * An action is given exactly the maximum its schema allows, so running out of
+ * room is this kernel's mistake. A query is given whatever the caller offered.
+ */
+static u32 ctlv_reply_capacity(const struct ctlv_op_entry *op, const struct ctlv_op_exec *exec)
+{
+ if (!op->schema->reply.node_schemas)
+ return 0;
+ if (op->schema->type == CTLV_OP_TYPE_QUERY)
+ return exec->reply_len;
+ return op->sizes.action_reply_max_len;
+}
+
+/* Actions and final query fragments must be complete schema-valid replies. */
+static int ctlv_reply_finish(struct ctlv_op_ctx *ctx)
+{
+ struct ctlv_error refused = {};
+
+ if (!ctx->output.root)
+ return 0;
+ /* Whatever poisoned it has warned about it already. */
+ if (ctx->output.poison)
+ return ctx->output.poison;
+ /* A query was held to its open nests by the ending it came to. */
+ if (ctx->op->schema->type == CTLV_OP_TYPE_QUERY) {
+ if (ctx->frag.cursor || ctx->frag.next_cursor)
+ return 0;
+ } else if (WARN_ON_ONCE(ctx->deepest != &ctx->output_root)) {
+ /* A cursor of a returned frame is reset, never followed. */
+ ctx->deepest = &ctx->output_root;
+ return -EINVAL;
+ }
+ if (WARN_ON_ONCE(ctlv_schema_validate(&ctx->output, &refused)))
+ return -EINVAL;
+ return 0;
+}
+
+/* A successful query callback must end exactly one fragment. */
+static int ctlv_query_fragment_result(struct ctlv_op_ctx *ctx)
+{
+ if (WARN_ON_ONCE(!ctx->frag.answered))
+ return -EINVAL;
+ return ctx->frag.result;
+}
+
+static int ctlv_op_call(struct ctlv_op_ctx *ctx)
+{
+ const struct ctlv_op_entry *op = ctx->op;
+ struct ctlv_cursor *request = NULL;
+ struct ctlv_cursor *reply = NULL;
+ int blob_ret;
+ int ret;
+
+ if (ctx->request.root) {
+ ctlv_message_root(&ctx->request_root, &ctx->request);
+ request = &ctx->request_root;
+ }
+ if (ctx->output.root)
+ reply = &ctx->output_root;
+
+ ctlv_blob_context_init(ctx);
+ if (op->schema->type == CTLV_OP_TYPE_ACTION) {
+ ret = op->binding->action(op->priv, ctx, request, reply);
+ } else {
+ ret = op->binding->query(op->priv, ctx, request, reply);
+ if (!ret)
+ ret = ctlv_query_fragment_result(ctx);
+ }
+ blob_ret = ctlv_blob_context_finish(ctx);
+ if (!ret)
+ ret = blob_ret;
+ if (ret)
+ return ret;
+ return ctlv_reply_finish(ctx);
+}
+
+/*
+ * The device family's hooks around the declaring op's, except that a core op
+ * uses the core family. The availability check is innermost.
+ */
+static int ctlv_op_dispatch(struct ctlv_op_ctx *ctx)
+{
+ const struct ctlv_op *op = ctx->op->binding;
+ const struct ctlv_family *family;
+ void *family_priv;
+ int ret;
+
+ if (CTLV_OP_FAMILY(ctx->op->schema->op_id) == CTLV_FAMILY_ROOT) {
+ family = &ctlv_family;
+ family_priv = ctx->ctlvdev->privs[0].priv;
+ } else {
+ family = ctx->ctlvdev->family;
+ family_priv = ctx->ctlvdev->privs[ctx->ctlvdev->privs_count - 1].priv;
+ }
+ ret = family->pre(family_priv, ctx);
+ if (ret)
+ return ret;
+
+ if (op->pre) {
+ ret = op->pre(ctx->op->priv, ctx);
+ if (ret)
+ goto out;
+ }
+
+ /* The only authoritative check: introspection is advisory. */
+ if (ctlv_op_disabled(ctx->ctlvdev, ctx->op))
+ ret = -EOPNOTSUPP;
+ else
+ ret = ctlv_op_call(ctx);
+
+ if (op->post)
+ op->post(ctx->op->priv, ctx);
+out:
+ family->post(family_priv, ctx);
+ return ret;
+}
+
+/* Error information replaces the reply atomically; errno remains authoritative. */
+static void ctlv_error_info(struct ctlv_op_ctx *ctx, u32 room, void **reply, u32 *reply_len)
+{
+ const struct ctlv_error *error = &ctx->error;
+ struct ctlv_cursor *root = &ctx->output_root;
+ void *buf;
+
+ if ((!error->reason && !error->message[0]) || !room)
+ return;
+ buf = kvmalloc(CTLV_ERROR_INFO_MAX_LEN, GFP_KERNEL_ACCOUNT);
+ if (!buf)
+ return;
+
+ /* The same builder over another schema, and a buffer of its own. */
+ ctlv_message_output_init(&ctx->output, ctx, buf, CTLV_ERROR_INFO_MAX_LEN,
+ &ctlv_error_info_schema);
+ ctlv_message_root(root, &ctx->output);
+ ctx->deepest = root;
+
+ if (error->reason)
+ ctlv_put_u64(root, CTLV_ATTR_ERROR_REASON, CTLV_SCHEMA_ENUM, error->reason);
+ if (error->message[0])
+ ctlv_error_message_put(root, error->message);
+ if (error->reason == CTLV_VALIDATION_REASON_STALE_GENERATION) {
+ /* The one failure that is about no part of any message. */
+ ctlv_put_u64(root, CTLV_ATTR_ERROR_EXPECTED_GENERATION, CTLV_SCHEMA_UINT,
+ error->expected_generation);
+ ctlv_put_u64(root, CTLV_ATTR_ERROR_CURRENT_GENERATION, CTLV_SCHEMA_UINT,
+ error->current_generation);
+ } else if (error->reason) {
+ ctlv_put_u64(root, CTLV_ATTR_ERROR_BAD_ATTR_OFFSET, CTLV_SCHEMA_UINT,
+ error->bad_attr_offset);
+ ctlv_put_u64(root, CTLV_ATTR_ERROR_NEST_OFFSET, CTLV_SCHEMA_UINT,
+ error->nest_offset);
+ /* No attr is numbered zero, so a zero names nothing. */
+ if (error->missing_attr_id)
+ ctlv_put_u64(root, CTLV_ATTR_ERROR_MISSING_ATTR_ID, CTLV_SCHEMA_UINT,
+ error->missing_attr_id);
+ }
+
+ if (ctx->output.poison || ctx->output.root->len > room) {
+ kvfree(buf);
+ return;
+ }
+ *reply = buf;
+ *reply_len = ctx->output.root->len;
+}
+
+/*
+ * Everything that needs the device to still have a family. @reply is built
+ * here and becomes the caller's to free, to copy out once the family is gone.
+ */
+static int ctlv_op_exec(struct ctlv_file *cfile, struct file *filp, struct ctlv_op_exec *exec,
+ void **reply, u32 *reply_len)
+{
+ struct ctlv_device *ctlvdev = cfile->ctlvdev;
+ const struct ctlv_op_entry *op;
+ struct ctlv_op_ctx ctx = {};
+ u32 capacity;
+ int ret;
+
+ /* No family numbers an op zero, so a zero here is an unset field. */
+ if (!exec->op_id)
+ return -EINVAL;
+ op = ctlv_op_find(ctlvdev, exec->op_id);
+ if (!op)
+ return -EOPNOTSUPP;
+ ret = ctlv_op_check_mode(filp, op);
+ if (ret)
+ return ret;
+ /*
+ * Only a query resumes. A cursor says which fragment this is, so a
+ * generation without one continues what was never started.
+ */
+ if (op->schema->type == CTLV_OP_TYPE_QUERY) {
+ if (!exec->cursor && exec->generation)
+ return -EINVAL;
+ ctx.frag.generation = exec->generation;
+ ctx.frag.cursor = exec->cursor;
+ } else if (exec->generation || exec->cursor) {
+ return -EINVAL;
+ }
+
+ /* Before the callback, so a buffer too small mutates nothing. */
+ if (exec->reply_len < ctlv_reply_needed(op))
+ return -EMSGSIZE;
+ /* And no longer than a message of this core may be. */
+ if (exec->reply_len > CTLV_MAX_INLINE_MESSAGE_LEN)
+ return -EINVAL;
+
+ ctx.ctlvdev = ctlvdev;
+ ctx.family = ctlvdev->family;
+ ctx.file = cfile;
+ /* Whose memory a blob descriptor of this request describes. */
+ ctx.submitter = current;
+ ctx.op = op;
+
+ void *snapshot __free(kvfree) = NULL;
+ void *out __free(kvfree) = NULL;
+
+ if (exec->request_len < sizeof(struct ctlv_attr) ||
+ exec->request_len > CTLV_MAX_INLINE_MESSAGE_LEN)
+ return -EINVAL;
+ /* Everything after this reads the snapshot, never userspace. */
+ snapshot = kvmalloc(exec->request_len, GFP_KERNEL_ACCOUNT);
+ if (!snapshot)
+ return -ENOMEM;
+ if (copy_from_user(snapshot, u64_to_user_ptr(exec->request), exec->request_len))
+ return -EFAULT;
+ /* Before any hook, so that no family code sees an unchecked attr. */
+ ret = ctlv_request_validate(&ctx, exec, snapshot);
+ if (ret)
+ goto out_error_info;
+
+ capacity = ctlv_reply_capacity(op, exec);
+ if (capacity) {
+ /*
+ * Not zeroed: a byte is copied out only once the root's
+ * length covers it, padding included.
+ */
+ out = kvmalloc(capacity, GFP_KERNEL_ACCOUNT);
+ if (!out) {
+ ret = -ENOMEM;
+ goto out_request;
+ }
+ ctlv_message_output_init(&ctx.output, &ctx, out, capacity, &op->schema->reply);
+ ctlv_message_root(&ctx.output_root, &ctx.output);
+ ctx.deepest = &ctx.output_root;
+ }
+
+ ret = ctlv_op_dispatch(&ctx);
+ if (ret)
+ goto out_error_info;
+
+ /* Where the caller carries on from, zero for anything but a query. */
+ exec->generation = ctx.frag.reported;
+ exec->cursor = ctx.frag.next_cursor;
+ *reply_len = ctx.output.root ? ctx.output.root->len : 0;
+ *reply = no_free_ptr(out);
+ ret = 0;
+ goto out_request;
+
+out_error_info:
+ ctlv_error_info(&ctx, exec->reply_len, reply, reply_len);
+out_request:
+ ctlv_message_index_free(&ctx.request);
+ return ret;
+}
+
+/* What one family of this device's chain does with one extension point. */
+static const struct ctlv_stub_ops *ctlv_stub_ops_find(const struct ctlv_device *ctlvdev,
+ const struct ctlv_stub_schema *stub_schema,
+ void **priv)
+{
+ const struct ctlv_family *family;
+ u32 index;
+
+ /* From the device family towards the core, where the fill was found. */
+ for (family = ctlvdev->family; family; family = family->parent)
+ for (index = 0; index < family->stub_ops_count; index++)
+ if (family->stub_ops[index].family_id == stub_schema->family_id &&
+ family->stub_ops[index].set_id == stub_schema->set_id) {
+ struct ctlv_family_priv *family_priv;
+
+ family_priv = ctlv_family_priv_find(ctlvdev, family);
+ if (WARN_ON_ONCE(!family_priv))
+ return NULL;
+ *priv = family_priv->priv;
+ return &family->stub_ops[index];
+ }
+ return NULL;
+}
+
+int ctlv_stub_fill(struct ctlv_op_ctx *ctx, const struct ctlv_cursor *request,
+ struct ctlv_cursor *nest)
+{
+ const struct ctlv_schema_tree *tree;
+ u32 index;
+
+ if (WARN_ON_ONCE(!nest->tree))
+ return -EINVAL;
+ tree = nest->tree;
+ for (index = 0; index < tree->stub_schemas_count; index++) {
+ const struct ctlv_stub_schema *stub_schema = &tree->stub_schemas[index];
+ const struct ctlv_cursor *filled_request = request;
+ const struct ctlv_stub_fill_schema *fill_schema;
+ const struct ctlv_stub_ops *stub_ops;
+ struct ctlv_cursor filled;
+ void *priv;
+ int ret;
+
+ if (stub_schema->node != nest->node)
+ continue;
+ stub_ops = ctlv_stub_ops_find(ctx->ctlvdev, stub_schema, &priv);
+ if (!stub_ops || !stub_ops->fill)
+ continue;
+ if (request) {
+ fill_schema = ctlv_schema_fill(ctx->family, stub_schema->family_id,
+ stub_schema->set_id);
+ if (WARN_ON_ONCE(!fill_schema))
+ return -EINVAL;
+ filled = *request;
+ if (ctlv_message_input_rebind(&filled, &fill_schema->members, 0))
+ filled_request = &filled;
+ else
+ filled_request = NULL;
+ }
+ ret = stub_ops->fill(priv, ctx, filled_request, nest);
+ if (ret)
+ return ret;
+ }
+ return 0;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_stub_fill, "CTLV");
+
+/*
+ * The core answers the root family through the same generated descriptor a
+ * family creates with, so it needs the same callbacks.
+ */
+int ctlv_op_pre(void *priv, struct ctlv_op_ctx *ctx)
+{
+ return 0;
+}
+
+void ctlv_op_post(void *priv, struct ctlv_op_ctx *ctx)
+{
+}
+
+size_t ctlv_priv_size(void)
+{
+ return 0;
+}
+
+int ctlv_init(struct ctlv_device *ctlvdev, const void *init_info, void *priv)
+{
+ if (WARN_ON_ONCE(init_info) || WARN_ON_ONCE(ctlv_priv_parent(priv)))
+ return -EINVAL;
+ return 0;
+}
+
+void ctlv_fini(struct ctlv_device *ctlvdev, void *priv)
+{
+}
+
+static long ctlv_file_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
+{
+ struct ctlv_op_exec __user *uexec = (struct ctlv_op_exec __user *)arg;
+ struct ctlv_file *cfile = filp->private_data;
+ struct ctlv_device *ctlvdev = cfile->ctlvdev;
+ struct ctlv_op_exec exec;
+ u32 reply_len = 0;
+ int ret;
+
+ if (cmd != CTLV_IOC_OP_EXEC)
+ return -ENOIOCTLCMD;
+ if (copy_from_user(&exec, uexec, sizeof(exec)))
+ return -EFAULT;
+ /* Reserved fields are what a compatible growth of this ioctl uses. */
+ if (exec.reserved[0] || exec.reserved[1])
+ return -EINVAL;
+
+ void *reply __free(kvfree) = NULL;
+
+ scoped_guard(rwsem_read, &ctlvdev->registration_lock) {
+ if (!ctlvdev->registered || !ctlvdev->family)
+ return -ENODEV;
+ ret = ctlv_op_exec(cfile, filp, &exec, &reply, &reply_len);
+ }
+ /* Do not make destruction wait for a faulting userspace copy. */
+ if (reply_len && copy_to_user(u64_to_user_ptr(exec.reply), reply, reply_len))
+ return ret ?: -EFAULT;
+ /*
+ * The only fields the kernel writes back, a field at a time so that
+ * what the caller stated about its own buffers stays the caller's.
+ */
+ if (!ret && (put_user(exec.generation, &uexec->generation) ||
+ put_user(exec.cursor, &uexec->cursor)))
+ return -EFAULT;
+ return ret;
+}
+
+/*
+ * One record per read, so a reader never gets a partial event. The offset says
+ * nothing about a queue and is neither read nor advanced.
+ */
+static ssize_t ctlv_file_read(struct file *filp, char __user *buf, size_t len, loff_t *ppos)
+{
+ struct ctlv_file *cfile = filp->private_data;
+
+ return ctlv_event_file_read(cfile, buf, len, !(filp->f_flags & O_NONBLOCK));
+}
+
+static __poll_t ctlv_file_poll(struct file *filp, struct poll_table_struct *wait)
+{
+ struct ctlv_file *cfile = filp->private_data;
+
+ return ctlv_event_file_poll(cfile, filp, wait);
+}
+
+static bool ctlv_file_open_allowed(struct ctlv_device *ctlvdev)
+{
+ lockdep_assert_held_read(&ctlvdev->registration_lock);
+ return ctlvdev->registered && ctlvdev->family;
+}
+
+static void ctlv_file_open_pinned(struct ctlv_device *ctlvdev)
+{
+}
+
+static int ctlv_file_open(struct inode *inode, struct file *filp)
+{
+ struct ctlv_device *ctlvdev = container_of(inode->i_cdev, struct ctlv_device, cdev);
+ struct ctlv_file *cfile;
+ int ret;
+
+ /*
+ * A failed create can withdraw the cdev while an open is waiting for
+ * the lifecycle lock, so admission itself first pins core storage.
+ */
+ get_device(&ctlvdev->dev);
+ ctlv_file_open_pinned(ctlvdev);
+ scoped_guard(rwsem_read, &ctlvdev->registration_lock) {
+ if (!ctlv_file_open_allowed(ctlvdev)) {
+ ret = -ENODEV;
+ break;
+ }
+ if (!(filp->f_mode & FMODE_READ)) {
+ ret = -EACCES;
+ break;
+ }
+
+ cfile = kzalloc_obj(*cfile, GFP_KERNEL_ACCOUNT);
+ if (!cfile) {
+ ret = -ENOMEM;
+ break;
+ }
+ cfile->ctlvdev = ctlvdev;
+ /*
+ * Commits this description to the device. An open that failed
+ * after this would have to take it back.
+ */
+ ret = ctlv_event_file_init(cfile);
+ if (ret) {
+ kfree(cfile);
+ break;
+ }
+ /* This description keeps the reference taken on entry. */
+ filp->private_data = cfile;
+ return 0;
+ }
+
+ put_device(&ctlvdev->dev);
+ return ret;
+}
+
+static int ctlv_file_release(struct inode *inode, struct file *filp)
+{
+ struct ctlv_file *cfile = filp->private_data;
+ struct ctlv_device *ctlvdev = cfile->ctlvdev;
+
+ ctlv_event_file_exit(cfile);
+ kfree(cfile);
+ put_device(&ctlvdev->dev);
+ return 0;
+}
+
+static const struct file_operations ctlv_file_operations = {
+ .owner = THIS_MODULE,
+ .open = ctlv_file_open,
+ .release = ctlv_file_release,
+ .read = ctlv_file_read,
+ .poll = ctlv_file_poll,
+ .unlocked_ioctl = ctlv_file_ioctl,
+ /* Every ioctl field is fixed width, so one entry point serves both. */
+ .compat_ioctl = compat_ptr_ioctl,
+};
+
+/* Uevent identity is available to rules before the device can be opened. */
+static int ctlv_device_uevent_families(struct kobj_uevent_env *env,
+ const struct ctlv_family *family)
+{
+ u32 levels = ctlv_family_levels(family);
+ u32 level;
+ int ret;
+
+ ret = add_uevent_var(env, "CTLV_FAMILY=%s", family->schema->name);
+ if (ret)
+ return ret;
+ for (level = 0; level < levels; level++) {
+ const struct ctlv_family *ancestor =
+ ctlv_family_ancestor(family, levels - 1 - level);
+
+ ret = add_uevent_var(env, "CTLV_COMPATIBLE_%u=%s", level, ancestor->schema->name);
+ if (ret)
+ return ret;
+ }
+ return add_uevent_var(env, "CTLV_COMPATIBLE_N=%u", levels);
+}
+
+static int ctlv_device_uevent_check(const struct ctlv_family *family)
+{
+ struct kobj_uevent_env *env;
+ int ret;
+
+ env = kzalloc_obj(*env, GFP_KERNEL);
+ if (!env)
+ return -ENOMEM;
+ ret = ctlv_device_uevent_families(env, family);
+ if (!ret && (env->buflen > CTLV_UEVENT_MAX_LEN || env->envp_idx > CTLV_UEVENT_MAX_VARS)) {
+ pr_err("family %s: its chain of names needs %d bytes in %d properties of a uevent, more than the %d in %d it may take\n",
+ family->schema->name, env->buflen, env->envp_idx, CTLV_UEVENT_MAX_LEN,
+ CTLV_UEVENT_MAX_VARS);
+ ret = -E2BIG;
+ }
+ kfree(env);
+ return ret;
+}
+
+/* Read unlocked: a uevent may be asked for under the registration lock. */
+static int ctlv_device_uevent(const struct device *dev, struct kobj_uevent_env *env)
+{
+ const struct ctlv_device *ctlvdev = container_of_const(dev, struct ctlv_device, dev);
+ const struct ctlv_family *family = READ_ONCE(ctlvdev->family);
+
+ if (!family)
+ return 0;
+ return ctlv_device_uevent_families(env, family);
+}
+
+static void ctlv_device_release(struct device *dev)
+{
+ struct ctlv_device *ctlvdev = container_of(dev, struct ctlv_device, dev);
+
+ ida_free(&ctlv_device_ids, ctlvdev->dev.devt - ctlv_device_number);
+ bitmap_free(ctlvdev->disabled);
+ ctlv_family_privs_free(ctlvdev);
+ kfree(ctlvdev->ops);
+ kfree(ctlvdev);
+}
+
+static struct class ctlv_device_class = {
+ .name = "ctlv",
+ .dev_release = ctlv_device_release,
+ .dev_uevent = ctlv_device_uevent,
+};
+
+static int ctlv_device_add(struct ctlv_device *ctlvdev)
+{
+ lockdep_assert_held_write(&ctlvdev->registration_lock);
+
+ return cdev_device_add(&ctlvdev->cdev, &ctlvdev->dev);
+}
+
+static void ctlv_device_del(struct ctlv_device *ctlvdev)
+{
+ cdev_device_del(&ctlvdev->cdev, &ctlvdev->dev);
+}
+
+struct ctlv_device *ctlv_create(struct device *parent_dev, const struct ctlv_family *family,
+ const void *init_info)
+{
+ struct ctlv_device *ctlvdev;
+ int devnum, ret;
+
+ if (!parent_dev || !family)
+ return ERR_PTR(-EINVAL);
+
+ ctlvdev = kzalloc_obj(*ctlvdev, GFP_KERNEL);
+ if (!ctlvdev)
+ return ERR_PTR(-ENOMEM);
+ ctlvdev->family = family;
+ init_rwsem(&ctlvdev->registration_lock);
+ spin_lock_init(&ctlvdev->transition_lock);
+ ctlv_event_device_init(ctlvdev);
+
+ /* Before anything reads the schema through this device. */
+ ret = ctlv_op_resolution_init(ctlvdev, family);
+ if (ret)
+ goto err_free;
+ /* What this family's events need of every queue opened on it. */
+ ret = ctlv_event_sizes(ctlvdev);
+ if (ret)
+ goto err_free;
+ /* Before a minor is spent on a device that could not be announced. */
+ ret = ctlv_device_uevent_check(family);
+ if (ret)
+ goto err_free;
+ if (ctlvdev->dynamic_ops_count) {
+ ctlvdev->disabled = bitmap_zalloc(ctlvdev->dynamic_ops_count, GFP_KERNEL);
+ if (!ctlvdev->disabled) {
+ ret = -ENOMEM;
+ goto err_free;
+ }
+ }
+ devnum = ida_alloc_max(&ctlv_device_ids, CTLV_MAX_DEVICES - 1, GFP_KERNEL);
+ if (devnum < 0) {
+ ret = devnum;
+ goto err_free;
+ }
+
+ ctlvdev->dev.devt = ctlv_device_number + devnum;
+ ctlvdev->dev.class = &ctlv_device_class;
+ ctlvdev->dev.parent = parent_dev;
+ device_initialize(&ctlvdev->dev);
+ /*
+ * The core owns the file operations, so an open holds the core module
+ * and not the family's, which may unload while readers remain.
+ */
+ cdev_init(&ctlvdev->cdev, &ctlv_file_operations);
+ ctlvdev->cdev.owner = THIS_MODULE;
+
+ ret = dev_set_name(&ctlvdev->dev, "ctlv%d", devnum);
+ if (ret)
+ goto err_put;
+ ret = ctlv_family_claim_add(family->schema);
+ if (ret)
+ goto err_put;
+ ret = ctlv_family_privs_pre_init(ctlvdev, init_info);
+ if (ret)
+ goto err_put;
+ ret = ctlv_family_privs_init(ctlvdev);
+ if (ret)
+ goto err_put;
+ ctlv_op_publish_all(ctlvdev);
+ ctlv_event_contexts_start(ctlvdev);
+ ret = ctlv_family_privs_post_init(ctlvdev);
+ if (ret)
+ goto err_fini;
+ scoped_guard(rwsem_write, &ctlvdev->registration_lock) {
+ ret = ctlv_device_add(ctlvdev);
+ if (!ret)
+ ctlvdev->registered = true;
+ }
+ if (ret)
+ goto err_fini;
+ return ctlvdev;
+
+err_fini:
+ ctlv_device_fini(ctlvdev);
+err_put:
+ put_device(&ctlvdev->dev);
+ return ERR_PTR(ret);
+err_free:
+ bitmap_free(ctlvdev->disabled);
+ ctlv_family_privs_free(ctlvdev);
+ kfree(ctlvdev->ops);
+ kfree(ctlvdev);
+ return ERR_PTR(ret);
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_create, "CTLV");
+
+void ctlv_destroy(struct ctlv_device *ctlvdev)
+{
+ u32 index;
+
+ /* No new opens, no discovery entry, and no node to open. */
+ ctlv_device_del(ctlvdev);
+
+ /*
+ * The write side waits for admitted callbacks while their events still
+ * work. Once held, registration state keeps every later ioctl out.
+ */
+ scoped_guard(rwsem_write, &ctlvdev->registration_lock)
+ ctlvdev->registered = false;
+
+ ctlv_device_fini(ctlvdev);
+
+ /* No callback can reach what the family owns after finalization. */
+ scoped_guard(rwsem_write, &ctlvdev->registration_lock)
+ WRITE_ONCE(ctlvdev->family, NULL);
+ for (index = 0; index < ctlvdev->ops_count; index++) {
+ ctlvdev->ops[index].binding = NULL;
+ ctlvdev->ops[index].priv = NULL;
+ }
+ ctlv_family_privs_free(ctlvdev);
+ put_device(&ctlvdev->dev);
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_destroy, "CTLV");
+
+static int __init ctlv_module_init(void)
+{
+ int ret;
+
+ ret = alloc_chrdev_region(&ctlv_device_number, 0, CTLV_MAX_DEVICES, "ctlv");
+ if (ret)
+ return ret;
+
+ ret = class_register(&ctlv_device_class);
+ if (ret)
+ goto err_unregister_chrdev_region;
+
+ return 0;
+
+err_unregister_chrdev_region:
+ unregister_chrdev_region(ctlv_device_number, CTLV_MAX_DEVICES);
+ return ret;
+}
+
+static void __exit ctlv_module_exit(void)
+{
+ class_unregister(&ctlv_device_class);
+ unregister_chrdev_region(ctlv_device_number, CTLV_MAX_DEVICES);
+ ctlv_family_claims_free();
+ ida_destroy(&ctlv_device_ids);
+}
+
+subsys_initcall(ctlv_module_init);
+module_exit(ctlv_module_exit);
+MODULE_DESCRIPTION("CTLV character device TLV UAPI core");
+MODULE_LICENSE("GPL");
diff --git a/drivers/ctlv/device.h b/drivers/ctlv/device.h
index 916a346c30ed..e51366d3fca1 100644
--- a/drivers/ctlv/device.h
+++ b/drivers/ctlv/device.h
@@ -2,24 +2,41 @@
#ifndef _CTLV_DEVICE_H
#define _CTLV_DEVICE_H
+#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/list.h>
+#include <linux/rwsem.h>
#include <linux/spinlock.h>
#include <linux/types.h>
#include <linux/wait.h>
struct ctlv_event_record;
struct ctlv_family;
+struct ctlv_family_priv;
struct ctlv_op_entry;
struct ctlv_device {
struct device dev;
+ struct cdev cdev;
+ struct rw_semaphore registration_lock;
+ bool registered;
const struct ctlv_family *family;
+ struct ctlv_family_priv *privs;
+ u32 privs_count;
+ void *priv_block;
struct ctlv_op_entry *ops;
u32 ops_count;
+ /* Op publication and state writers, never the dispatch path. */
+ spinlock_t transition_lock;
+ bool published;
+ bool destroying;
+ u64 state_generation;
+ u32 dynamic_ops_count;
+ unsigned long *disabled;
+
/* Everything about the queues, and nothing family code runs under. */
spinlock_t events_lock;
struct list_head files;
diff --git a/include/linux/ctlv.h b/include/linux/ctlv.h
index cdfe3e8e930d..5f59acac7dc4 100644
--- a/include/linux/ctlv.h
+++ b/include/linux/ctlv.h
@@ -52,12 +52,67 @@ struct ctlv_cursor {
u32 index_scope;
};
+/**
+ * ctlv_priv_parent - Get the direct parent family private data.
+ * @priv: core-allocated private data passed to a family callback
+ *
+ * Return: parent private data, or NULL for the root family.
+ */
+void *ctlv_priv_parent(const void *priv);
+
+/**
+ * ctlv_priv_child - Get the direct child family private data.
+ * @priv: core-allocated private data passed to a family callback
+ *
+ * Return: child private data, or NULL for the device family.
+ */
+void *ctlv_priv_child(const void *priv);
+
+struct device;
+
+/**
+ * ctlv_device_parent - Get the backing device.
+ * @ctlvdev: CTLV device
+ *
+ * Return: backing device passed to ctlv_create().
+ */
+struct device *ctlv_device_parent(const struct ctlv_device *ctlvdev);
+
+/**
+ * ctlv_create - Create and register a CTLV device.
+ * @parent_dev: backing device
+ * @family: device family
+ * @init_info: family initialization data
+ *
+ * Return: CTLV device, or an error pointer.
+ */
+struct ctlv_device *ctlv_create(struct device *parent_dev,
+ const struct ctlv_family *family,
+ const void *init_info);
+
+/**
+ * ctlv_destroy - Unregister and destroy a CTLV device.
+ * @ctlvdev: CTLV device
+ */
+void ctlv_destroy(struct ctlv_device *ctlvdev);
+
/**
* ctlv_nest_end - Finish an output nest.
* @child: nest cursor returned by a generated start helper
*/
void ctlv_nest_end(struct ctlv_cursor *child);
+/**
+ * ctlv_stub_fill - Delegate an extension-point output nest.
+ * @ctx: operation context
+ * @request: validated request root
+ * @nest: output nest selected by the generated helper
+ *
+ * Return: filling callback result, or 0 for an unfilled point.
+ */
+int ctlv_stub_fill(struct ctlv_op_ctx *ctx, const struct ctlv_cursor *request,
+ struct ctlv_cursor *nest);
+
/**
* ctlv_op_error - Record an operation error message.
* @ctx: operation context
diff --git a/include/linux/ctlv_family.h b/include/linux/ctlv_family.h
index 4f5141fc7ccf..66e62733f905 100644
--- a/include/linux/ctlv_family.h
+++ b/include/linux/ctlv_family.h
@@ -313,6 +313,10 @@ struct ctlv_family {
extern const struct ctlv_family ctlv_family;
+void ctlv_device_op_provide(struct ctlv_device *ctlvdev, u64 op_id);
+void ctlv_device_op_state_set(struct ctlv_device *ctlvdev, u64 op_id,
+ bool disabled);
+
struct ctlv_cursor *
ctlv_input_attr(struct ctlv_cursor *cursor, const struct ctlv_cursor *nest,
struct ctlv_input_ref ref);
@@ -341,6 +345,24 @@ struct ctlv_blob *ctlv_blob_resolve(struct ctlv_op_ctx *ctx,
struct ctlv_op_ctx *ctlv_event_create(struct ctlv_device *ctlvdev, u64 op_id,
struct ctlv_cursor **event, gfp_t gfp);
+#define CTLV_OP_PROVIDE(_name, _op_id) \
+static inline void _name##_provide(struct ctlv_device *ctlvdev) \
+{ \
+ ctlv_device_op_provide(ctlvdev, (_op_id)); \
+}
+
+#define CTLV_OP_ENABLE(_name, _op_id) \
+static inline void _name##_enable(struct ctlv_device *ctlvdev) \
+{ \
+ ctlv_device_op_state_set(ctlvdev, (_op_id), false); \
+}
+
+#define CTLV_OP_DISABLE(_name, _op_id) \
+static inline void _name##_disable(struct ctlv_device *ctlvdev) \
+{ \
+ ctlv_device_op_state_set(ctlvdev, (_op_id), true); \
+}
+
#define CTLV_EVENT_CREATE(_name, _op_id) \
static inline struct ctlv_op_ctx * \
_name##_create(struct ctlv_device *ctlvdev, \
--
2.56.0
^ permalink raw reply related [flat|nested] 32+ messages in thread* Re: [PATCH RFC 12/20] ctlv: Add device and operation dispatch
2026-10-15 14:50 ` [PATCH RFC 12/20] ctlv: Add device and operation dispatch Jiri Pirko
@ 2026-10-15 15:28 ` sashiko-bot
0 siblings, 0 replies; 32+ messages in thread
From: sashiko-bot @ 2026-10-15 15:28 UTC (permalink / raw)
To: Jiri Pirko; +Cc: kvm
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] ctlv: mixing goto-based error handling with scope-based cleanup (__free)
--
commit d40367eef6344eb381abe711a6b81ec26de22d13
Author: Jiri Pirko <jiri@nvidia.com>
ctlv: Add device and operation dispatch
Register CTLV devices, resolve family chains and dispatch operations
through validated messages. Wire CONFIG_CTLV and the generated root family
after all module objects are present.
> diff --git a/drivers/ctlv/core.c b/drivers/ctlv/core.c
> new file mode 100644
> index 0000000000000..aeab29fe2fdd4
> --- /dev/null
> +++ b/drivers/ctlv/core.c
[ ... ]
> +static int ctlv_op_exec(struct ctlv_file *cfile, struct file *filp, struct ctlv_op_exec *exec,
> + void **reply, u32 *reply_len)
> +{
> + struct ctlv_device *ctlvdev = cfile->ctlvdev;
> + const struct ctlv_op_entry *op;
> + struct ctlv_op_ctx ctx = {};
> + u32 capacity;
> + int ret;
[ ... ]
> + void *snapshot __free(kvfree) = NULL;
> + void *out __free(kvfree) = NULL;
> +
> + if (exec->request_len < sizeof(struct ctlv_attr) ||
> + exec->request_len > CTLV_MAX_INLINE_MESSAGE_LEN)
> + return -EINVAL;
> + /* Everything after this reads the snapshot, never userspace. */
> + snapshot = kvmalloc(exec->request_len, GFP_KERNEL_ACCOUNT);
> + if (!snapshot)
> + return -ENOMEM;
> + if (copy_from_user(snapshot, u64_to_user_ptr(exec->request), exec->request_len))
> + return -EFAULT;
> + /* Before any hook, so that no family code sees an unchecked attr. */
> + ret = ctlv_request_validate(&ctx, exec, snapshot);
> + if (ret)
> + goto out_error_info;
[ ... ]
> + /* Where the caller carries on from, zero for anything but a query. */
> + exec->generation = ctx.frag.reported;
> + exec->cursor = ctx.frag.next_cursor;
> + *reply_len = ctx.output.root ? ctx.output.root->len : 0;
> + *reply = no_free_ptr(out);
> + ret = 0;
> + goto out_request;
> +
> +out_error_info:
> + ctlv_error_info(&ctx, exec->reply_len, reply, reply_len);
> +out_request:
> + ctlv_message_index_free(&ctx.request);
> + return ret;
> +}
[Severity: Low]
Does this mix scope-based cleanup (__free) with goto-based error handling?
The cleanup guidelines (include/linux/cleanup.h) state that functions
should either convert all resources to scope-based cleanup or convert none
of them, but never mix the two in the same function.
[ ... ]
> +struct ctlv_device *ctlv_create(struct device *parent_dev, const struct ctlv_family *family,
> + const void *init_info)
> +{
> + struct ctlv_device *ctlvdev;
> + int devnum, ret;
[ ... ]
> + ctlv_event_contexts_start(ctlvdev);
> + ret = ctlv_family_privs_post_init(ctlvdev);
> + if (ret)
> + goto err_fini;
> + scoped_guard(rwsem_write, &ctlvdev->registration_lock) {
> + ret = ctlv_device_add(ctlvdev);
> + if (!ret)
> + ctlvdev->registered = true;
> + }
> + if (ret)
> + goto err_fini;
> + return ctlvdev;
> +
> +err_fini:
> + ctlv_device_fini(ctlvdev);
> +err_put:
> + put_device(&ctlvdev->dev);
> + return ERR_PTR(ret);
> +err_free:
> + bitmap_free(ctlvdev->disabled);
> + ctlv_family_privs_free(ctlvdev);
> + kfree(ctlvdev->ops);
> + kfree(ctlvdev);
> + return ERR_PTR(ret);
> +}
[Severity: Low]
Does this mix scoped_guard with goto-based error handling?
The kernel guidelines expect functions to avoid mixing goto error labels and
scope-based cleanup macros like scoped_guard.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261015145053.3664509-1-jiri@resnulli.us?part=12
^ permalink raw reply [flat|nested] 32+ messages in thread
* [PATCH RFC 13/20] Documentation: ctlv: Describe how to write a family
2026-10-15 14:50 [PATCH RFC 00/20] attestation: Add a common subsystem and TDX support Jiri Pirko
` (11 preceding siblings ...)
2026-10-15 14:50 ` [PATCH RFC 12/20] ctlv: Add device and operation dispatch Jiri Pirko
@ 2026-10-15 14:50 ` Jiri Pirko
2026-10-15 15:20 ` sashiko-bot
2026-10-15 14:50 ` [PATCH RFC 14/20] attestation: Add the base family specification and generated files Jiri Pirko
` (6 subsequent siblings)
19 siblings, 1 reply; 32+ messages in thread
From: Jiri Pirko @ 2026-10-15 14:50 UTC (permalink / raw)
To: linux-coco
Cc: linux-kernel, linux-api, x86, kvm, linux-doc, jgg, david.quigley,
lukas, ksinyuk, rodrigo.vivi, cedric.xing, sameo, drdeeglaze,
James.Bottomley, aik, jpiotrowski, qinkun, pgonda, jic23,
alistair.francis, ankita, suzuki.poulose, kas, rick.p.edgecombe,
elena.reshetova, dave.hansen, sathyanarayanan.kuppuswamy,
peter.fang
From: Jiri Pirko <jiri@nvidia.com>
Describe the YAML language, generated family descriptors, lifecycle,
operations, events, blobs, inheritance and compatible extension rules.
Assisted-by: Cursor: GPT-5.6 Sol
Assisted-by: Cursor: Claude Opus 5.5
Signed-off-by: Jiri Pirko <jiri@nvidia.com>
---
Documentation/driver-api/ctlv.rst | 422 +++++++++++++++++++++++++++++
Documentation/driver-api/index.rst | 1 +
MAINTAINERS | 1 +
3 files changed, 424 insertions(+)
create mode 100644 Documentation/driver-api/ctlv.rst
diff --git a/Documentation/driver-api/ctlv.rst b/Documentation/driver-api/ctlv.rst
new file mode 100644
index 000000000000..461e2977a483
--- /dev/null
+++ b/Documentation/driver-api/ctlv.rst
@@ -0,0 +1,422 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+=====================
+Writing a CTLV family
+=====================
+
+:Author: Jiri Pirko
+
+A CTLV family is a YAML specification plus the callbacks named by its generated
+header. CTLV validates requests before a callback sees them. The wire ABI is
+:doc:`/userspace-api/ctlv`; family specifications are under
+``Documentation/ctlv/specs``.
+
+The code here is a small invented family, ``ctlv-example``.
+
+Add the specification, registry entry, and output manifest entry in one change::
+
+ Documentation/ctlv/specs/ctlv-example.yaml
+ Documentation/ctlv/families.yaml
+ Documentation/ctlv/generated-files.yaml
+
+``families.yaml`` is append-only. Its array index is the family ID and is part
+of every generated attribute and operation ID. Never reorder, remove, rename,
+or reuse an entry. ``Documentation/ctlv/abi/families.registry.yaml`` is the
+checked-in registry ABI. The output manifest selects the generated UAPI,
+kernel and userspace paths and generation modes.
+
+Generate and verify them with::
+
+ make -C tools/ctlv regen
+ make -C tools/ctlv all
+
+Never edit a generated output. Change the YAML or generator and regenerate it.
+Optional RST reference pages are written to
+``Documentation/output/ctlv/families`` by::
+
+ make -C tools/ctlv docs
+
+Write the specification
+=======================
+
+.. code-block:: yaml
+
+ # SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause)
+ %YAML 1.2
+ ---
+ family:
+ name: ctlv-example
+ inherits: ctlv
+ post-init: true
+ pre-fini: true
+
+ attr-groups:
+ - name: item
+ attrs:
+ - name: index
+ type: uint
+ max: 4095
+ - name: label
+ type: string
+ max-length: 63
+ - name: data
+ blob:
+ direction: input
+ allowed-backings: [ va ]
+ max-length: 1048576
+ alignment: 8
+ - name: list
+ attrs:
+ - name: item
+ attrs: item-reply
+ max-occurrences: unlimited
+
+ attr-sets:
+ - name: item-request
+ attrs: item.index
+ optional-attrs:
+ - item.label
+ - item.data
+ - name: item-reply
+ attrs: item.index
+ optional-attrs: item.label
+ - name: items-reply
+ optional-attrs: list.item
+
+ ops:
+ - name: item-set
+ type: action
+ optional: true
+ request:
+ attrs: item-request
+ - name: items-dump
+ type: query
+ availability: dynamic
+ pre-post: true
+ reply:
+ attrs: items-reply
+ suggested-reply-length: 4096
+ - name: item-changed
+ type: event
+ reply:
+ attrs: item-reply
+
+An attribute has one payload form: ``type``, ``enum``, ``flags``, ``attrs``, or
+``blob``. An attr set names one message level. ``attrs`` are required and
+``optional-attrs`` are optional. Its bounds and membership are ABI.
+
+Inheritance and extension points
+================================
+
+A descendant implements only the operations it declares. Inherited operations
+keep the callbacks and operation hooks of their declaring family. Every module
+therefore depends on the module containing its parent implementation.
+
+A parent reserves an extension point with an empty stub set:
+
+.. code-block:: yaml
+
+ attr-groups:
+ - name: device
+ attrs:
+ - name: details
+ attrs: details-extension
+
+ attr-sets:
+ - name: details-extension
+ stub: true
+ - name: device-reply
+ optional-attrs: device.details
+
+A child fills that same set:
+
+.. code-block:: yaml
+
+ family:
+ name: ctlv-example-vendor
+ inherits: ctlv-example
+ pre-init: true
+
+ attr-groups:
+ - name: vendor
+ attrs:
+ - name: serial
+ type: uint
+
+ attr-sets:
+ - name: details-extension
+ optional-attrs: vendor.serial
+
+The parent implementation opens the physical nest and delegates its members:
+
+.. code-block:: c
+
+ if (ctlv_example_device_details_start(reply, &details)) {
+ ret = ctlv_stub_fill(ctx, request_details, &details);
+ ctlv_nest_end(&details);
+ if (ret)
+ return ret;
+ }
+
+The generated child callback fills it:
+
+.. code-block:: c
+
+ int ctlv_example_vendor_details_extension_fill(
+ void *priv, struct ctlv_op_ctx *ctx,
+ const struct ctlv_cursor *request,
+ struct ctlv_cursor *nest)
+ {
+ ctlv_example_vendor_vendor_serial_put(nest,
+ example_serial(priv));
+ return 0;
+ }
+
+An unfilled stub contributes no nest or message length. Neither side names the
+family on the other side.
+
+Create and destroy a device
+===========================
+
+``ctlv_create()`` takes the leaf family descriptor and leaf initialization
+information:
+
+.. code-block:: c
+
+ struct ctlv_example_vendor_init_info info = {
+ .dev = dev,
+ };
+ struct ctlv_device *ctlvdev;
+
+ ctlvdev = ctlv_create(dev, &ctlv_example_vendor_ctlv_family, &info);
+ if (IS_ERR(ctlvdev))
+ return PTR_ERR(ctlvdev);
+
+A family with ``pre-init: true`` supplies initialization information to its
+direct parent. A typical provider descriptor is static and immutable:
+
+.. code-block:: c
+
+ static const struct ctlv_example_provider example_provider = {
+ .name = "vendor-example",
+ .ops = &example_provider_ops,
+ };
+
+ const void *ctlv_example_vendor_pre_init(const void *init_info)
+ {
+ return &example_provider;
+ }
+
+The leaf's per-device ``init_info`` still reaches its own ``init`` callback; it
+is not forwarded as the parent's descriptor. ``pre_init`` runs leaf to root and
+may return an ``ERR_PTR()``. It must not acquire resources or return automatic
+storage.
+
+Each family supplies ``priv_size``, ``init``, and ``fini``. The YAML flags add
+``pre_init``, ``post_init``, and ``pre_fini`` callbacks. Creation order is::
+
+ leaf -> root pre_init
+ root -> leaf init
+ freeze published operations; start event admission
+ leaf -> root post_init
+ register the character device
+
+Select optional operations and their initial dynamic state in ``init``. A
+``post_init`` callback may publish the device to kernel consumers because all
+family privs and event emission are then available.
+
+Normal destruction is ordered as follows::
+
+ withdraw userspace access; drain admitted operation callbacks
+ root -> leaf pre_fini
+ stop and drain event contexts; queue DEVICE_REMOVED
+ leaf -> root fini
+
+A family published in ``post_init`` withdraws and drains its consumers in
+``pre_fini``. Provider state and events remain available until all
+``pre_fini`` callbacks finish. ``ctlv_destroy()`` consumes the reference from
+``ctlv_create()``; open file descriptions remain only to drain queued records
+and otherwise fail with ``-ENODEV``.
+
+If ``init`` fails, that callback releases its partial resources and CTLV calls
+``fini`` only for initialized ancestors. A failure after initialization uses
+the normal ``pre_fini`` and ``fini`` ordering.
+
+CTLV allocates one zeroed, cache-line-aligned priv per family. Direct neighbors
+are available without naming their types globally:
+
+.. code-block:: c
+
+ parent = ctlv_priv_parent(priv);
+ child = ctlv_priv_child(priv);
+
+During ``init``, the child is not initialized yet. During ``pre_fini`` it still
+is; during ``fini`` it has already been finalized. Allocate a separately
+aligned object when cache-line alignment is insufficient.
+
+Implement operations
+====================
+
+Action
+------
+
+Generated getters read only validated placements and generated serializers
+write only declared replies:
+
+.. code-block:: c
+
+ int ctlv_example_item_set_action(void *priv, struct ctlv_op_ctx *ctx,
+ const struct ctlv_cursor *request,
+ struct ctlv_cursor *reply)
+ {
+ u64 index = ctlv_example_item_index_get(request);
+
+ if (item_busy(priv, index))
+ return ctlv_op_error(ctx, -EBUSY, "item is busy");
+ return item_set(priv, index);
+ }
+
+Use ``_present()`` for optional attributes and ``_for_each()`` for repeated
+ones. ``ctlv_op_error()`` preserves the errno and adds a short diagnostic when
+the caller supplied enough reply space.
+
+Query
+-----
+
+.. code-block:: c
+
+ int ctlv_example_items_dump_query(void *priv, struct ctlv_op_ctx *ctx,
+ const struct ctlv_cursor *request,
+ struct ctlv_cursor *reply)
+ {
+ struct ctlv_example *example = priv;
+ u64 generation = READ_ONCE(example->generation);
+ u64 cursor = ctlv_op_query_cursor(ctx);
+ struct ctlv_cursor item;
+ u64 i = cursor ? cursor - 1 : 0;
+
+ if (cursor && ctlv_op_query_generation(ctx) != generation)
+ return -ESTALE;
+ for (; i < example->items_count; i++) {
+ if (!ctlv_example_list_item_start(reply, &item))
+ return ctlv_op_query_more(ctx, generation, i + 1);
+ ctlv_example_item_index_put(&item, i);
+ ctlv_nest_end(&item);
+ }
+ return ctlv_op_query_done(ctx, generation);
+ }
+
+A continuation cursor is nonzero and advances. Every continuing fragment
+completes a logical leaf. Call ``ctlv_op_query_more()`` while cursors for open
+continuation nests are alive. A generation mismatch returns ``-ESTALE``.
+
+Hooks and availability
+======================
+
+The leaf family's hooks wrap each effective non-core action or query. An
+operation with ``pre-post: true`` has its declaring family's pair inside them::
+
+ family pre -> operation pre -> availability check -> callback
+ -> operation post -> family post
+
+Each successful pre hook has one post hook. A failing pre hook undoes its own
+work. Hooks may sleep and must not retain ``ctx``. Each hook and callback gets
+the priv of the family implementing it.
+
+Optional and dynamic operations are separate choices:
+
+.. code-block:: c
+
+ /* During init: publish this optional operation on this device. */
+ ctlv_example_item_set_provide(ctlvdev);
+
+ /* At runtime: temporarily refuse a published dynamic operation. */
+ ctlv_example_items_dump_disable(ctlvdev);
+ ctlv_example_items_dump_enable(ctlvdev);
+
+Declare the latter with ``availability: dynamic``. An unpublished optional
+operation has no schema entry. A disabled operation remains discoverable and
+returns ``-EOPNOTSUPP``; a state change emits ``op-changed``.
+
+Blobs
+=====
+
+A blob attribute describes caller-owned memory. Resolve it only in the callback
+that received its request and release every successful resolve before return:
+
+.. code-block:: c
+
+ blob = ctlv_example_item_data_resolve(ctx, request);
+ if (IS_ERR(blob))
+ return PTR_ERR(blob);
+
+ ret = ctlv_blob_read(blob, 0, buf, ctlv_blob_len(blob));
+ put_ret = ctlv_blob_put(blob);
+ return ret ?: put_ret;
+
+The schema fixes direction, backing, length, and alignment. Resolution holds
+the backing for the ioctl.
+
+Events
+======
+
+.. code-block:: c
+
+ ctx = ctlv_example_item_changed_create(ctlvdev, &event, GFP_KERNEL);
+ if (IS_ERR(ctx))
+ return PTR_ERR(ctx);
+ ctlv_example_item_index_put(event, index);
+ return ctlv_event_emit(ctx);
+
+Exactly one of ``ctlv_event_emit()`` and ``ctlv_event_abort()`` ends the context. The
+GFP argument covers subscriber copies; atomic producers use ``GFP_NOWAIT`` or
+``GFP_ATOMIC``. Event APIs are not called from interrupt context, and hooks do
+not run around events.
+
+If an event occurred but allocation failed, commit the state change and call
+``ctlv_event_lost()``. It neither allocates nor sleeps. Do not use it for an
+aborted event or for per-subscriber drops already tracked by
+``ctlv_event_emit()``.
+
+If hardware changed before an event context could be allocated, the family
+must commit its invalidation and explicitly report the missing notification:
+
+.. code-block:: c
+
+ invalidate_stale_state(priv);
+ priv->generation++;
+
+ ctx = ctlv_example_thing_happened_create(ctlvdev, &event, GFP_ATOMIC);
+ if (IS_ERR(ctx)) {
+ if (PTR_ERR(ctx) == -ENOMEM)
+ ctlv_event_lost(ctlvdev,
+ CTLV_EXAMPLE_OP_THING_HAPPENED);
+ return;
+ }
+ ctlv_example_generation_put(event, priv->generation);
+ ctlv_op_emit(ctx);
+
+``ctlv_event_lost()`` allocates nothing and does not sleep. For each subscribed
+file description it advances the event sequence and aggregates the loss into
+the preallocated ``EVENT_OVERFLOW`` record. The caller keeps the device alive
+and serializes the report with normal emission of that event. Like event
+emission, it is not called from interrupt context. It is only for an event that
+actually occurred but could not be constructed: do not call it for an aborted
+event or for copy-allocation and full-queue drops already accounted for by
+``ctlv_op_emit()``. Reports after event shutdown or the terminal
+``DEVICE_REMOVED`` record are ignored.
+
+Reporting an error
+------------------
+
+A callback returns a negative errno, and that is what the ioctl returns. Where
+CTLV itself refuses a message it also writes error information into the
+caller's reply buffer, naming the reason and the offset of the offending
+attribute. A family implementation does not build that; it adds its own errno
+and its reply.
+
+Configuration
+=============
+
+``CONFIG_CTLV``
+ CTLV. A family implementation selects it.
diff --git a/Documentation/driver-api/index.rst b/Documentation/driver-api/index.rst
index 6601a258690f..e85a7a982bfc 100644
--- a/Documentation/driver-api/index.rst
+++ b/Documentation/driver-api/index.rst
@@ -37,6 +37,7 @@ of interest to most developers working on device drivers.
early-userspace/index
connector
+ ctlv
device-io
devfreq
dma-buf
diff --git a/MAINTAINERS b/MAINTAINERS
index b308e3a5f0b6..983437acd388 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -7006,6 +7006,7 @@ M: Jiri Pirko <jiri@resnulli.us>
L: linux-kernel@vger.kernel.org
S: Maintained
F: Documentation/ctlv/
+F: Documentation/driver-api/ctlv.rst
F: Documentation/userspace-api/ctlv.rst
F: drivers/ctlv/
F: include/linux/ctlv*.h
--
2.56.0
^ permalink raw reply related [flat|nested] 32+ messages in thread* Re: [PATCH RFC 13/20] Documentation: ctlv: Describe how to write a family
2026-10-15 14:50 ` [PATCH RFC 13/20] Documentation: ctlv: Describe how to write a family Jiri Pirko
@ 2026-10-15 15:20 ` sashiko-bot
0 siblings, 0 replies; 32+ messages in thread
From: sashiko-bot @ 2026-10-15 15:20 UTC (permalink / raw)
To: Jiri Pirko; +Cc: kvm
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Low] docs: Incorrect function ctlv_op_emit() in ctlv.rst example
- [Low] docs: Incorrect generated family descriptor name in ctlv.rst example
--
commit 44307a0d2c96bec6fed10bf4f0610aa2e3baf41c
Author: Jiri Pirko <jiri@resnulli.us>
Documentation: ctlv: Describe how to write a family
Describe the YAML language, generated family descriptors, lifecycle,
operations, events, blobs, inheritance and compatible extension rules.
> diff --git a/Documentation/driver-api/ctlv.rst b/Documentation/driver-api/ctlv.rst
> new file mode 100644
> index 0000000000000..461e2977a4839
> --- /dev/null
> +++ b/Documentation/driver-api/ctlv.rst
[ ... ]
> + struct ctlv_example_vendor_init_info info = {
> + .dev = dev,
> + };
> + struct ctlv_device *ctlvdev;
> +
> + ctlvdev = ctlv_create(dev, &ctlv_example_vendor_ctlv_family, &info);
[Severity: Low]
Does this code use the correct generated family descriptor name? Based on the
YAML family name ctlv-example-vendor, the expected generated C struct name
would be ctlv_example_vendor_family. It seems the _ctlv part is improperly
inserted here.
> + if (IS_ERR(ctlvdev))
[ ... ]
> + if (IS_ERR(ctx)) {
> + if (PTR_ERR(ctx) == -ENOMEM)
> + ctlv_event_lost(ctlvdev,
> + CTLV_EXAMPLE_OP_THING_HAPPENED);
> + return;
> + }
> + ctlv_example_generation_put(event, priv->generation);
> + ctlv_op_emit(ctx);
[Severity: Low]
Is ctlv_op_emit() the correct API call here? Earlier in the documentation,
event emission is shown using ctlv_event_emit(). Using an incorrect API
name here could lead to compile-time errors for driver authors attempting to
follow the guide.
> +
> +ctlv_event_lost() allocates nothing and does not sleep. For each subscribed
> +file description it advances the event sequence and aggregates the loss into
> +the preallocated EVENT_OVERFLOW record. The caller keeps the device alive
> +and serializes the report with normal emission of that event. Like event
> +emission, it is not called from interrupt context. It is only for an event that
> +actually occurred but could not be constructed: do not call it for an aborted
> +event or for copy-allocation and full-queue drops already accounted for by
> +ctlv_op_emit(). Reports after event shutdown or the terminal
[Severity: Low]
Should this text reference ctlv_event_emit() instead of the non-existent
ctlv_op_emit() function?
> +DEVICE_REMOVED record are ignored.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261015145053.3664509-1-jiri@resnulli.us?part=13
^ permalink raw reply [flat|nested] 32+ messages in thread
* [PATCH RFC 14/20] attestation: Add the base family specification and generated files
2026-10-15 14:50 [PATCH RFC 00/20] attestation: Add a common subsystem and TDX support Jiri Pirko
` (12 preceding siblings ...)
2026-10-15 14:50 ` [PATCH RFC 13/20] Documentation: ctlv: Describe how to write a family Jiri Pirko
@ 2026-10-15 14:50 ` Jiri Pirko
2026-10-15 14:50 ` [PATCH RFC 15/20] attestation: Prepare provider and instance core Jiri Pirko
` (5 subsequent siblings)
19 siblings, 0 replies; 32+ messages in thread
From: Jiri Pirko @ 2026-10-15 14:50 UTC (permalink / raw)
To: linux-coco
Cc: linux-kernel, linux-api, x86, kvm, linux-doc, jgg, david.quigley,
lukas, ksinyuk, rodrigo.vivi, cedric.xing, sameo, drdeeglaze,
James.Bottomley, aik, jpiotrowski, qinkun, pgonda, jic23,
alistair.francis, ankita, suzuki.poulose, kas, rick.p.edgecombe,
elena.reshetova, dave.hansen, sathyanarayanan.kuppuswamy,
peter.fang
From: Jiri Pirko <jiri@nvidia.com>
Define the base attestation family. Reserve its family ID and add the ABI
snapshot, registry entry and generated kernel and UAPI files.
The ABI is intentionally specification-first. The generated descriptor is
linked after the provider, journal and operation handlers arrive.
Assisted-by: Cursor: GPT-5.6 Sol
Assisted-by: Cursor: Claude Opus 5.5
Signed-off-by: Jiri Pirko <jiri@nvidia.com>
---
Documentation/ctlv/abi/ctlv-attestation.yaml | 184 ++++++++++
Documentation/ctlv/abi/families.registry.yaml | 1 +
Documentation/ctlv/families.yaml | 1 +
Documentation/ctlv/generated-files.yaml | 7 +
.../ctlv/specs/ctlv-attestation.yaml | 347 ++++++++++++++++++
MAINTAINERS | 9 +
.../generated/ctlv_attestation_gen.c | 234 ++++++++++++
.../generated/ctlv_attestation_gen.h | 198 ++++++++++
include/uapi/linux/ctlv_attestation_gen.h | 252 +++++++++++++
include/uapi/linux/ctlv_families_gen.h | 1 +
10 files changed, 1234 insertions(+)
create mode 100644 Documentation/ctlv/abi/ctlv-attestation.yaml
create mode 100644 Documentation/ctlv/specs/ctlv-attestation.yaml
create mode 100644 drivers/attestation/generated/ctlv_attestation_gen.c
create mode 100644 drivers/attestation/generated/ctlv_attestation_gen.h
create mode 100644 include/uapi/linux/ctlv_attestation_gen.h
diff --git a/Documentation/ctlv/abi/ctlv-attestation.yaml b/Documentation/ctlv/abi/ctlv-attestation.yaml
new file mode 100644
index 000000000000..a857e5ce0039
--- /dev/null
+++ b/Documentation/ctlv/abi/ctlv-attestation.yaml
@@ -0,0 +1,184 @@
+# SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause)
+%YAML 1.2
+---
+# Checked-in ABI snapshot of the ctlv-attestation family, written by
+# "make -C tools/ctlv abi-update".
+#
+# The checker compares this family's declarations and inherited stub fills
+# against this file. Updating it is how a UAPI change gets reviewed; an update
+# that removes or renumbers anything, narrows a bound, or makes a placement
+# required is an ABI break, whatever this file says afterwards.
+family: {name: ctlv-attestation, id: 1, inherits: ctlv, post-init: true,
+ pre-fini: true}
+enums:
+ evidence-representation: {raw: 1}
+ evidence-blob-class: {primary: 1}
+ mr-log-event: {info: 1, extend: 2}
+flags:
+ mr-device-caps: {read: 0, extend: 1}
+ mr-caps: {read: 0, extend: 1}
+attr-groups:
+ device:
+ id: 1
+ attrs:
+ generation: {id: 1, type: uint, max: 4294967295}
+ caps: {id: 2, type: nested}
+ info: {id: 3, type: nested}
+ cap:
+ id: 2
+ attrs:
+ evidence: {id: 1, type: nested}
+ mr: {id: 2, type: nested}
+ min-challenge-length: {id: 3, type: uint}
+ max-challenge-length: {id: 4, type: uint}
+ max-total-output-length: {id: 5, type: uint}
+ mr-flags: {id: 6, flags: mr-device-caps}
+ object: {id: 7, type: nested, max-occurrences: 16}
+ blob-class: {id: 8, enum: evidence-blob-class}
+ format: {id: 9, type: string, min-length: 1, max-length: 128}
+ representation: {id: 10, enum: evidence-representation}
+ max-length: {id: 11, type: uint}
+ evidence:
+ id: 3
+ attrs:
+ challenge: {id: 1, type: binary, max-length: 4096}
+ object: {id: 2, type: nested, max-occurrences: 16}
+ blob-class: {id: 3, enum: evidence-blob-class}
+ format: {id: 4, type: string, min-length: 1, max-length: 128}
+ representation: {id: 5, enum: evidence-representation}
+ data: {id: 6, blob: output, backings: [va], max-length: 4294967295}
+ object-length: {id: 7, type: uint, max-occurrences: 16}
+ mr:
+ id: 4
+ attrs:
+ id: {id: 1, type: uint, max: 4294967295}
+ name: {id: 2, type: string, min-length: 1, max-length: 128}
+ hash-algo: {id: 3, type: uint, max: 20}
+ supported-hash-algo: {id: 4, type: uint, max: 20, max-occurrences: 21}
+ register-index: {id: 5, type: uint, max: 4294967295}
+ caps: {id: 6, flags: mr-caps}
+ item: {id: 7, type: nested, max-occurrences: unlimited}
+ value: {id: 8, type: nested, max-occurrences: 21}
+ data: {id: 9, type: binary, min-length: 16, max-length: 64}
+ generation: {id: 10, type: uint, max: 4294967295}
+ selection: {id: 11, type: nested, max-occurrences: 256}
+ start-generation: {id: 12, type: uint, max: 4294967295}
+ end-generation: {id: 13, type: uint, max: 4294967295}
+ log:
+ id: 5
+ attrs:
+ entry: {id: 1, type: nested, max-occurrences: unlimited}
+ event: {id: 2, enum: mr-log-event}
+ mr: {id: 3, type: nested}
+ notification:
+ id: 6
+ attrs:
+ entry: {id: 1, type: nested}
+attr-sets:
+ device-info: {id: 1, stub: true}
+ evidence-object-cap: {id: 2}
+ evidence-cap: {id: 3}
+ mr-device-cap: {id: 4}
+ device-caps: {id: 5}
+ device-reply: {id: 6}
+ evidence-object-request: {id: 7}
+ evidence-request: {id: 8}
+ evidence-reply: {id: 9}
+ mr-props: {id: 10}
+ mr-dump-entry: {id: 11}
+ mr-id-request: {id: 12}
+ mr-read-request: {id: 13}
+ mr-value: {id: 14}
+ mr-read-reply: {id: 15}
+ mr-extend-request: {id: 16}
+ mr-change-reply: {id: 17}
+ mr-log-info: {id: 18}
+ mr-log-entry: {id: 19}
+ mr-log-event: {id: 20}
+ mr-log-selection: {id: 21}
+ mr-log-request: {id: 22}
+ mr-log-reply: {id: 23}
+ops:
+ device-get:
+ abi: {id: 1, type: action, fd-access: read}
+ reply:
+ attrs:
+ - device.caps:
+ optional-attrs:
+ - cap.evidence:
+ attrs:
+ - cap.min-challenge-length
+ - cap.max-challenge-length
+ - cap.max-total-output-length
+ - cap.object:
+ attrs: [cap.blob-class, cap.format, cap.representation,
+ cap.max-length]
+ - cap.mr: {attrs: [cap.mr-flags]}
+ - device.info: {attrs: [device-info]}
+ evidence-get:
+ abi: {id: 2, type: action, availability: dynamic, optional: true}
+ request:
+ attrs:
+ - evidence.challenge
+ - evidence.object:
+ attrs: [evidence.blob-class, evidence.format,
+ evidence.representation, evidence.data]
+ reply:
+ attrs: [evidence.object-length]
+ optional-attrs: [mr.generation]
+ mr-get:
+ abi: {id: 3, type: action, fd-access: read, optional: true}
+ request: {attrs: [mr.id]}
+ reply:
+ attrs: [mr.name, mr.supported-hash-algo, mr.caps]
+ optional-attrs: [mr.register-index]
+ mr-dump:
+ abi: {id: 4, type: query, fd-access: read, optional: true}
+ reply:
+ attrs:
+ - mr.item:
+ attrs: [mr.id, mr.name, mr.supported-hash-algo, mr.caps]
+ optional-attrs: [mr.register-index]
+ mr-read:
+ abi: {id: 5, type: query, fd-access: read, optional: true}
+ request: {attrs: [mr.id]}
+ reply:
+ attrs:
+ - mr.generation
+ - mr.value: {attrs: [mr.hash-algo, mr.data]}
+ mr-extend:
+ abi: {id: 6, type: action, optional: true}
+ request:
+ attrs:
+ - mr.id
+ - mr.value: {attrs: [mr.hash-algo, mr.data]}
+ reply: {attrs: [mr.generation]}
+ mr-log-entry-appended:
+ abi: {id: 7, type: event}
+ reply:
+ attrs:
+ - notification.entry:
+ attrs: [mr.generation, mr.id, log.event]
+ optional-attrs:
+ - mr.value: {attrs: [mr.hash-algo, mr.data]}
+ - log.mr:
+ attrs: [mr.name, mr.supported-hash-algo, mr.caps]
+ optional-attrs: [mr.register-index]
+ mr-log-get:
+ abi: {id: 8, type: query, fd-access: read, optional: true}
+ request:
+ optional-attrs:
+ - mr.selection: {attrs: [mr.id]}
+ - mr.start-generation
+ - mr.end-generation
+ reply:
+ optional-attrs:
+ - mr.start-generation
+ - mr.end-generation
+ - log.entry:
+ attrs: [mr.generation, mr.id, log.event]
+ optional-attrs:
+ - mr.value: {attrs: [mr.hash-algo, mr.data]}
+ - log.mr:
+ attrs: [mr.name, mr.supported-hash-algo, mr.caps]
+ optional-attrs: [mr.register-index]
diff --git a/Documentation/ctlv/abi/families.registry.yaml b/Documentation/ctlv/abi/families.registry.yaml
index 2c9b9e7eee15..fa511b709fc1 100644
--- a/Documentation/ctlv/abi/families.registry.yaml
+++ b/Documentation/ctlv/abi/families.registry.yaml
@@ -5,3 +5,4 @@
# Updated deliberately by "make -C tools/ctlv abi-update".
families:
- ctlv
+ - ctlv-attestation
diff --git a/Documentation/ctlv/families.yaml b/Documentation/ctlv/families.yaml
index bf1dabdba5c9..d78025a9072c 100644
--- a/Documentation/ctlv/families.yaml
+++ b/Documentation/ctlv/families.yaml
@@ -11,3 +11,4 @@
families:
- ctlv
+ - ctlv-attestation
diff --git a/Documentation/ctlv/generated-files.yaml b/Documentation/ctlv/generated-files.yaml
index 6773e0be0883..669f1f8d51b4 100644
--- a/Documentation/ctlv/generated-files.yaml
+++ b/Documentation/ctlv/generated-files.yaml
@@ -15,3 +15,10 @@ families:
uapi-header: include/uapi/linux/ctlv_gen.h
kernel-header: include/linux/ctlv_gen.h
kernel-source: drivers/ctlv/ctlv_gen.c
+
+ - name: ctlv-attestation
+ source: Documentation/ctlv/specs/ctlv-attestation.yaml
+ outputs:
+ uapi-header: include/uapi/linux/ctlv_attestation_gen.h
+ kernel-header: drivers/attestation/generated/ctlv_attestation_gen.h
+ kernel-source: drivers/attestation/generated/ctlv_attestation_gen.c
diff --git a/Documentation/ctlv/specs/ctlv-attestation.yaml b/Documentation/ctlv/specs/ctlv-attestation.yaml
new file mode 100644
index 000000000000..c0d7fc8db18e
--- /dev/null
+++ b/Documentation/ctlv/specs/ctlv-attestation.yaml
@@ -0,0 +1,347 @@
+# SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause)
+%YAML 1.2
+---
+family:
+ name: ctlv-attestation
+ inherits: ctlv
+ post-init: true
+ pre-fini: true
+
+enums:
+ - name: evidence-representation
+ values:
+ - raw
+
+ - name: evidence-blob-class
+ values:
+ - primary
+
+ - name: mr-log-event
+ values:
+ - info
+ - extend
+
+flags:
+ - name: mr-device-caps
+ values:
+ - read
+ - extend
+
+ - name: mr-caps
+ values:
+ - read
+ - extend
+
+attr-groups:
+ - name: device
+ attrs:
+ # Reserved until provider-driven state changes are introduced.
+ - name: generation
+ type: uint
+ max: 4294967295
+ - name: caps
+ attrs: device-caps
+ - name: info
+ attrs: device-info
+
+ - name: cap
+ attrs:
+ - name: evidence
+ attrs: evidence-cap
+ - name: mr
+ attrs: mr-device-cap
+ - name: min-challenge-length
+ type: uint
+ - name: max-challenge-length
+ type: uint
+ - name: max-total-output-length
+ type: uint
+ - name: mr-flags
+ flags: mr-device-caps
+ - name: object
+ attrs: evidence-object-cap
+ max-occurrences: 16
+ - name: blob-class
+ enum: evidence-blob-class
+ - name: format
+ type: string
+ min-length: 1
+ max-length: 128
+ - name: representation
+ enum: evidence-representation
+ - name: max-length
+ type: uint
+
+ - name: evidence
+ attrs:
+ - name: challenge
+ type: binary
+ max-length: 4096
+ - name: object
+ attrs: evidence-object-request
+ max-occurrences: 16
+ - name: blob-class
+ enum: evidence-blob-class
+ - name: format
+ type: string
+ min-length: 1
+ max-length: 128
+ - name: representation
+ enum: evidence-representation
+ - name: data
+ blob:
+ direction: output
+ allowed-backings: [ va ]
+ max-length: 4294967295
+ alignment: 1
+ - name: object-length
+ type: uint
+ max-occurrences: 16
+
+ - name: mr
+ attrs:
+ - name: id
+ type: uint
+ max: 4294967295
+ - name: name
+ type: string
+ min-length: 1
+ max-length: 128
+ - name: hash-algo
+ type: uint
+ max: 20
+ - name: supported-hash-algo
+ type: uint
+ max: 20
+ max-occurrences: 21
+ - name: register-index
+ type: uint
+ max: 4294967295
+ - name: caps
+ flags: mr-caps
+ - name: item
+ attrs: mr-dump-entry
+ max-occurrences: unlimited
+ - name: value
+ attrs: mr-value
+ max-occurrences: 21
+ - name: data
+ type: binary
+ min-length: 16
+ max-length: 64
+ - name: generation
+ type: uint
+ max: 4294967295
+ - name: selection
+ attrs: mr-log-selection
+ max-occurrences: 256
+ - name: start-generation
+ type: uint
+ max: 4294967295
+ - name: end-generation
+ type: uint
+ max: 4294967295
+
+ - name: log
+ attrs:
+ - name: entry
+ attrs: mr-log-entry
+ max-occurrences: unlimited
+ - name: event
+ enum: mr-log-event
+ - name: mr
+ attrs: mr-log-info
+
+ - name: notification
+ attrs:
+ - name: entry
+ attrs: mr-log-entry
+
+attr-sets:
+ - name: device-info
+ stub: true
+
+ - name: evidence-object-cap
+ attrs:
+ - cap.blob-class
+ - cap.format
+ - cap.representation
+ - cap.max-length
+
+ - name: evidence-cap
+ attrs:
+ - cap.min-challenge-length
+ - cap.max-challenge-length
+ - cap.max-total-output-length
+ - cap.object
+
+ - name: mr-device-cap
+ attrs: cap.mr-flags
+
+ - name: device-caps
+ optional-attrs:
+ - cap.evidence
+ - cap.mr
+
+ - name: device-reply
+ attrs:
+ - device.caps
+ - device.info
+
+ - name: evidence-object-request
+ attrs:
+ - evidence.blob-class
+ - evidence.format
+ - evidence.representation
+ - evidence.data
+
+ - name: evidence-request
+ attrs:
+ - evidence.challenge
+ - evidence.object
+
+ - name: evidence-reply
+ attrs: evidence.object-length
+ optional-attrs:
+ - mr.generation
+
+ - name: mr-props
+ attrs:
+ - mr.name
+ - mr.supported-hash-algo
+ - mr.caps
+ optional-attrs: mr.register-index
+
+ - name: mr-dump-entry
+ attrs:
+ - mr.id
+ - mr.name
+ - mr.supported-hash-algo
+ - mr.caps
+ optional-attrs: mr.register-index
+
+ - name: mr-id-request
+ attrs: mr.id
+
+ - name: mr-read-request
+ attrs: mr.id
+
+ - name: mr-value
+ attrs:
+ - mr.hash-algo
+ - mr.data
+
+ - name: mr-read-reply
+ attrs:
+ - mr.generation
+ - mr.value
+
+ - name: mr-extend-request
+ attrs:
+ - mr.id
+ - mr.value
+
+ - name: mr-change-reply
+ attrs: mr.generation
+
+ - name: mr-log-info
+ attrs:
+ - mr.name
+ - mr.supported-hash-algo
+ - mr.caps
+ optional-attrs: mr.register-index
+
+ - name: mr-log-entry
+ attrs:
+ - mr.generation
+ - mr.id
+ - log.event
+ optional-attrs:
+ - mr.value
+ - log.mr
+
+ - name: mr-log-event
+ attrs: notification.entry
+
+ - name: mr-log-selection
+ attrs: mr.id
+
+ - name: mr-log-request
+ optional-attrs:
+ - mr.selection
+ - mr.start-generation
+ - mr.end-generation
+
+ - name: mr-log-reply
+ optional-attrs:
+ - mr.start-generation
+ - mr.end-generation
+ - log.entry
+
+ops:
+ - name: device-get
+ type: action
+ fd-access: read
+ reply:
+ attrs: device-reply
+
+ - name: evidence-get
+ type: action
+ fd-access: read-write
+ availability: dynamic
+ optional: true
+ request:
+ attrs: evidence-request
+ reply:
+ attrs: evidence-reply
+
+ - name: mr-get
+ type: action
+ fd-access: read
+ optional: true
+ request:
+ attrs: mr-id-request
+ reply:
+ attrs: mr-props
+
+ - name: mr-dump
+ type: query
+ fd-access: read
+ optional: true
+ reply:
+ attrs: mr.item
+ suggested-reply-length: 4096
+
+ - name: mr-read
+ type: query
+ fd-access: read
+ optional: true
+ request:
+ attrs: mr-read-request
+ reply:
+ attrs: mr-read-reply
+ suggested-reply-length: 4096
+
+ - name: mr-extend
+ type: action
+ fd-access: read-write
+ optional: true
+ request:
+ attrs: mr-extend-request
+ reply:
+ attrs: mr-change-reply
+
+ - name: mr-log-entry-appended
+ type: event
+ reply:
+ attrs: mr-log-event
+
+ - name: mr-log-get
+ type: query
+ fd-access: read
+ optional: true
+ request:
+ attrs: mr-log-request
+ reply:
+ attrs: mr-log-reply
+ suggested-reply-length: 131072
diff --git a/MAINTAINERS b/MAINTAINERS
index 983437acd388..c8ff980cce13 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -4331,6 +4331,15 @@ F: rust/kernel/sync/atomic.rs
F: rust/kernel/sync/atomic/
F: rust/kernel/sync/refcount.rs
+ATTESTATION SUBSYSTEM
+M: Jiri Pirko <jiri@nvidia.com>
+L: linux-security-module@vger.kernel.org
+S: Supported
+F: Documentation/ctlv/abi/ctlv-attestation*
+F: Documentation/ctlv/specs/ctlv-attestation*
+F: drivers/attestation/
+F: include/uapi/linux/ctlv_attestation*
+
ATTO EXPRESSSAS SAS/SATA RAID SCSI DRIVER
M: Bradley Grove <linuxdrivers@attotech.com>
L: linux-scsi@vger.kernel.org
diff --git a/drivers/attestation/generated/ctlv_attestation_gen.c b/drivers/attestation/generated/ctlv_attestation_gen.c
new file mode 100644
index 000000000000..c9fbeae00e38
--- /dev/null
+++ b/drivers/attestation/generated/ctlv_attestation_gen.c
@@ -0,0 +1,234 @@
+// SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause)
+/* Do not edit, regenerate with tools/ctlv/ctlv-regen.sh, from: */
+/* Documentation/ctlv/specs/ctlv-attestation.yaml */
+/* CTLV-GEN kernel source */
+
+#define _CTLV_GENERATED_METADATA_ONLY
+#include <linux/array_size.h>
+#include <linux/bits.h>
+#include <linux/build_bug.h>
+#include <linux/ctlv_family.h>
+#include <linux/export.h>
+#include <linux/limits.h>
+
+#include <uapi/linux/ctlv_gen.h>
+#include <uapi/linux/ctlv_attestation_gen.h>
+
+#include "ctlv_attestation_gen.h"
+
+static const struct ctlv_node_schema device_get_request_node_schemas[] = {
+ CTLV_NODE_SCHEMA_ROOT(0, 0, 0),
+};
+
+static const struct ctlv_node_schema device_get_reply_node_schemas[] = {
+ CTLV_NODE_SCHEMA_ROOT(1, 2, 2),
+ CTLV_NODE_SCHEMA_NEST(CTLV_ATTESTATION_ATTR_DEVICE_CAPS, 1, 3, 2, 0),
+ CTLV_NODE_SCHEMA_NEST(CTLV_ATTESTATION_ATTR_DEVICE_INFO, 1, 0, 0, 0),
+ CTLV_NODE_SCHEMA_NEST(CTLV_ATTESTATION_ATTR_CAP_EVIDENCE, 1, 5, 4, 4),
+ CTLV_NODE_SCHEMA_NEST(CTLV_ATTESTATION_ATTR_CAP_MR, 1, 9, 1, 1),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTESTATION_ATTR_CAP_MIN_CHALLENGE_LENGTH, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTESTATION_ATTR_CAP_MAX_CHALLENGE_LENGTH, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTESTATION_ATTR_CAP_MAX_TOTAL_OUTPUT_LENGTH, 1, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_NEST(CTLV_ATTESTATION_ATTR_CAP_OBJECT, 16, 10, 4, 4),
+ CTLV_NODE_SCHEMA_FLAGS(CTLV_ATTESTATION_ATTR_CAP_MR_FLAGS, 1,
+ CTLV_ATTESTATION_MR_DEVICE_CAPS_MASK),
+ CTLV_NODE_SCHEMA_ENUM(CTLV_ATTESTATION_ATTR_CAP_BLOB_CLASS, 1, 1),
+ CTLV_NODE_SCHEMA_STRING(CTLV_ATTESTATION_ATTR_CAP_FORMAT, 1, 1, 128),
+ CTLV_NODE_SCHEMA_ENUM(CTLV_ATTESTATION_ATTR_CAP_REPRESENTATION, 1, 1),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTESTATION_ATTR_CAP_MAX_LENGTH, 1, 0, U64_MAX),
+};
+
+/* Extension points selected by this tree. */
+static const struct ctlv_stub_schema device_get_reply_stub_schemas[] = {
+ CTLV_STUB_SCHEMA(CTLV_FAMILY_ATTESTATION, CTLV_ATTESTATION_SET_DEVICE_INFO, 2),
+};
+
+static const struct ctlv_node_schema evidence_get_request_node_schemas[] = {
+ CTLV_NODE_SCHEMA_ROOT(1, 2, 2),
+ CTLV_NODE_SCHEMA_BINARY(CTLV_ATTESTATION_ATTR_EVIDENCE_CHALLENGE, 1, 0, 4096),
+ CTLV_NODE_SCHEMA_NEST(CTLV_ATTESTATION_ATTR_EVIDENCE_OBJECT, 16, 3, 4, 4),
+ CTLV_NODE_SCHEMA_ENUM(CTLV_ATTESTATION_ATTR_EVIDENCE_BLOB_CLASS, 1, 1),
+ CTLV_NODE_SCHEMA_STRING(CTLV_ATTESTATION_ATTR_EVIDENCE_FORMAT, 1, 1, 128),
+ CTLV_NODE_SCHEMA_ENUM(CTLV_ATTESTATION_ATTR_EVIDENCE_REPRESENTATION, 1, 1),
+ CTLV_NODE_SCHEMA_BLOB(CTLV_ATTESTATION_ATTR_EVIDENCE_DATA, 1, U32_MAX, 1,
+ BIT(CTLV_BLOB_TYPE_VA),
+ CTLV_BLOB_DIRECTION_OUTPUT),
+};
+
+static const struct ctlv_node_schema evidence_get_reply_node_schemas[] = {
+ CTLV_NODE_SCHEMA_ROOT(1, 2, 1),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTESTATION_ATTR_EVIDENCE_OBJECT_LENGTH, 16, 0, U64_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTESTATION_ATTR_MR_GENERATION, 1, 0, U32_MAX),
+};
+
+static const struct ctlv_node_schema mr_get_request_node_schemas[] = {
+ CTLV_NODE_SCHEMA_ROOT(1, 1, 1),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTESTATION_ATTR_MR_ID, 1, 0, U32_MAX),
+};
+
+static const struct ctlv_node_schema mr_get_reply_node_schemas[] = {
+ CTLV_NODE_SCHEMA_ROOT(1, 4, 3),
+ CTLV_NODE_SCHEMA_STRING(CTLV_ATTESTATION_ATTR_MR_NAME, 1, 1, 128),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTESTATION_ATTR_MR_SUPPORTED_HASH_ALGO, 21, 0, 20),
+ CTLV_NODE_SCHEMA_FLAGS(CTLV_ATTESTATION_ATTR_MR_CAPS, 1, CTLV_ATTESTATION_MR_CAPS_MASK),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTESTATION_ATTR_MR_REGISTER_INDEX, 1, 0, U32_MAX),
+};
+
+static const struct ctlv_node_schema mr_dump_request_node_schemas[] = {
+ CTLV_NODE_SCHEMA_ROOT(0, 0, 0),
+};
+
+static const struct ctlv_node_schema mr_dump_reply_node_schemas[] = {
+ CTLV_NODE_SCHEMA_ROOT(1, 1, 1),
+ CTLV_NODE_SCHEMA_NEST(CTLV_ATTESTATION_ATTR_MR_ITEM, CTLV_NODE_SCHEMA_UNLIMITED, 2, 5, 4),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTESTATION_ATTR_MR_ID, 1, 0, U32_MAX),
+ CTLV_NODE_SCHEMA_STRING(CTLV_ATTESTATION_ATTR_MR_NAME, 1, 1, 128),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTESTATION_ATTR_MR_SUPPORTED_HASH_ALGO, 21, 0, 20),
+ CTLV_NODE_SCHEMA_FLAGS(CTLV_ATTESTATION_ATTR_MR_CAPS, 1, CTLV_ATTESTATION_MR_CAPS_MASK),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTESTATION_ATTR_MR_REGISTER_INDEX, 1, 0, U32_MAX),
+};
+
+static const struct ctlv_node_schema mr_read_request_node_schemas[] = {
+ CTLV_NODE_SCHEMA_ROOT(1, 1, 1),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTESTATION_ATTR_MR_ID, 1, 0, U32_MAX),
+};
+
+static const struct ctlv_node_schema mr_read_reply_node_schemas[] = {
+ CTLV_NODE_SCHEMA_ROOT(1, 2, 2),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTESTATION_ATTR_MR_GENERATION, 1, 0, U32_MAX),
+ CTLV_NODE_SCHEMA_NEST(CTLV_ATTESTATION_ATTR_MR_VALUE, 21, 3, 2, 2),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTESTATION_ATTR_MR_HASH_ALGO, 1, 0, 20),
+ CTLV_NODE_SCHEMA_BINARY(CTLV_ATTESTATION_ATTR_MR_DATA, 1, 16, 64),
+};
+
+static const struct ctlv_node_schema mr_extend_request_node_schemas[] = {
+ CTLV_NODE_SCHEMA_ROOT(1, 2, 2),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTESTATION_ATTR_MR_ID, 1, 0, U32_MAX),
+ CTLV_NODE_SCHEMA_NEST(CTLV_ATTESTATION_ATTR_MR_VALUE, 21, 3, 2, 2),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTESTATION_ATTR_MR_HASH_ALGO, 1, 0, 20),
+ CTLV_NODE_SCHEMA_BINARY(CTLV_ATTESTATION_ATTR_MR_DATA, 1, 16, 64),
+};
+
+static const struct ctlv_node_schema mr_extend_reply_node_schemas[] = {
+ CTLV_NODE_SCHEMA_ROOT(1, 1, 1),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTESTATION_ATTR_MR_GENERATION, 1, 0, U32_MAX),
+};
+
+static const struct ctlv_node_schema mr_log_entry_appended_reply_node_schemas[] = {
+ CTLV_NODE_SCHEMA_ROOT(1, 1, 1),
+ CTLV_NODE_SCHEMA_NEST(CTLV_ATTESTATION_ATTR_NOTIFICATION_ENTRY, 1, 2, 5, 3),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTESTATION_ATTR_MR_GENERATION, 1, 0, U32_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTESTATION_ATTR_MR_ID, 1, 0, U32_MAX),
+ CTLV_NODE_SCHEMA_ENUM(CTLV_ATTESTATION_ATTR_LOG_EVENT, 1, 2),
+ CTLV_NODE_SCHEMA_NEST(CTLV_ATTESTATION_ATTR_MR_VALUE, 21, 7, 2, 2),
+ CTLV_NODE_SCHEMA_NEST(CTLV_ATTESTATION_ATTR_LOG_MR, 1, 9, 4, 3),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTESTATION_ATTR_MR_HASH_ALGO, 1, 0, 20),
+ CTLV_NODE_SCHEMA_BINARY(CTLV_ATTESTATION_ATTR_MR_DATA, 1, 16, 64),
+ CTLV_NODE_SCHEMA_STRING(CTLV_ATTESTATION_ATTR_MR_NAME, 1, 1, 128),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTESTATION_ATTR_MR_SUPPORTED_HASH_ALGO, 21, 0, 20),
+ CTLV_NODE_SCHEMA_FLAGS(CTLV_ATTESTATION_ATTR_MR_CAPS, 1, CTLV_ATTESTATION_MR_CAPS_MASK),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTESTATION_ATTR_MR_REGISTER_INDEX, 1, 0, U32_MAX),
+};
+
+static const struct ctlv_node_schema mr_log_get_request_node_schemas[] = {
+ CTLV_NODE_SCHEMA_ROOT(1, 3, 0),
+ CTLV_NODE_SCHEMA_NEST(CTLV_ATTESTATION_ATTR_MR_SELECTION, 256, 4, 1, 1),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTESTATION_ATTR_MR_START_GENERATION, 1, 0, U32_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTESTATION_ATTR_MR_END_GENERATION, 1, 0, U32_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTESTATION_ATTR_MR_ID, 1, 0, U32_MAX),
+};
+
+static const struct ctlv_node_schema mr_log_get_reply_node_schemas[] = {
+ CTLV_NODE_SCHEMA_ROOT(1, 3, 0),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTESTATION_ATTR_MR_START_GENERATION, 1, 0, U32_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTESTATION_ATTR_MR_END_GENERATION, 1, 0, U32_MAX),
+ CTLV_NODE_SCHEMA_NEST(CTLV_ATTESTATION_ATTR_LOG_ENTRY, CTLV_NODE_SCHEMA_UNLIMITED, 4, 5, 3),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTESTATION_ATTR_MR_GENERATION, 1, 0, U32_MAX),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTESTATION_ATTR_MR_ID, 1, 0, U32_MAX),
+ CTLV_NODE_SCHEMA_ENUM(CTLV_ATTESTATION_ATTR_LOG_EVENT, 1, 2),
+ CTLV_NODE_SCHEMA_NEST(CTLV_ATTESTATION_ATTR_MR_VALUE, 21, 9, 2, 2),
+ CTLV_NODE_SCHEMA_NEST(CTLV_ATTESTATION_ATTR_LOG_MR, 1, 11, 4, 3),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTESTATION_ATTR_MR_HASH_ALGO, 1, 0, 20),
+ CTLV_NODE_SCHEMA_BINARY(CTLV_ATTESTATION_ATTR_MR_DATA, 1, 16, 64),
+ CTLV_NODE_SCHEMA_STRING(CTLV_ATTESTATION_ATTR_MR_NAME, 1, 1, 128),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTESTATION_ATTR_MR_SUPPORTED_HASH_ALGO, 21, 0, 20),
+ CTLV_NODE_SCHEMA_FLAGS(CTLV_ATTESTATION_ATTR_MR_CAPS, 1, CTLV_ATTESTATION_MR_CAPS_MASK),
+ CTLV_NODE_SCHEMA_UINT(CTLV_ATTESTATION_ATTR_MR_REGISTER_INDEX, 1, 0, U32_MAX),
+};
+
+/* The ops this family declares, in declaration order. */
+const struct ctlv_op_schema ctlv_attestation_op_schemas[] = {
+ CTLV_OP_SCHEMA_ACTION(CTLV_ATTESTATION_OP_DEVICE_GET, CTLV_OP_FD_ACCESS_READ,
+ CTLV_OP_AVAILABILITY_FIXED, 16, 4016,
+ CTLV_SCHEMA_TREE(device_get_request),
+ CTLV_SCHEMA_TREE_STUBS(device_get_reply)),
+ CTLV_OP_SCHEMA_ACTION(CTLV_ATTESTATION_OP_EVIDENCE_GET, CTLV_OP_FD_ACCESS_READ_WRITE,
+ CTLV_OP_AVAILABILITY_DYNAMIC, 8992, 424,
+ CTLV_SCHEMA_TREE(evidence_get_request),
+ CTLV_SCHEMA_TREE(evidence_get_reply), .optional = true),
+ CTLV_OP_SCHEMA_ACTION(CTLV_ATTESTATION_OP_MR_GET, CTLV_OP_FD_ACCESS_READ,
+ CTLV_OP_AVAILABILITY_FIXED, 40, 720, CTLV_SCHEMA_TREE(mr_get_request),
+ CTLV_SCHEMA_TREE(mr_get_reply), .optional = true),
+ CTLV_OP_SCHEMA_QUERY(CTLV_ATTESTATION_OP_MR_DUMP, CTLV_OP_FD_ACCESS_READ,
+ CTLV_OP_AVAILABILITY_FIXED, 16, 184, 4096,
+ CTLV_SCHEMA_TREE(mr_dump_request), CTLV_SCHEMA_TREE(mr_dump_reply),
+ .optional = true),
+ CTLV_OP_SCHEMA_QUERY(CTLV_ATTESTATION_OP_MR_READ, CTLV_OP_FD_ACCESS_READ,
+ CTLV_OP_AVAILABILITY_FIXED, 40, 112, 4096,
+ CTLV_SCHEMA_TREE(mr_read_request), CTLV_SCHEMA_TREE(mr_read_reply),
+ .optional = true),
+ CTLV_OP_SCHEMA_ACTION(CTLV_ATTESTATION_OP_MR_EXTEND, CTLV_OP_FD_ACCESS_READ_WRITE,
+ CTLV_OP_AVAILABILITY_FIXED, 2560, 40,
+ CTLV_SCHEMA_TREE(mr_extend_request),
+ CTLV_SCHEMA_TREE(mr_extend_reply), .optional = true),
+ CTLV_OP_SCHEMA_EVENT(CTLV_ATTESTATION_OP_MR_LOG_ENTRY_APPENDED, 3368,
+ CTLV_SCHEMA_TREE(mr_log_entry_appended_reply)),
+ CTLV_OP_SCHEMA_QUERY(CTLV_ATTESTATION_OP_MR_LOG_GET, CTLV_OP_FD_ACCESS_READ,
+ CTLV_OP_AVAILABILITY_FIXED, 10304, 200, 131072,
+ CTLV_SCHEMA_TREE(mr_log_get_request),
+ CTLV_SCHEMA_TREE(mr_log_get_reply), .optional = true),
+};
+
+/* The family ID is its position in the central registry. */
+const struct ctlv_family_schema ctlv_attestation_schema = {
+ .name = "ctlv-attestation",
+ .family_id = 1,
+ .ops = ctlv_attestation_op_schemas,
+ .ops_count = ARRAY_SIZE(ctlv_attestation_op_schemas),
+};
+
+/* Maximum uevent cost for this family chain. */
+static_assert(107 <= CTLV_UEVENT_MAX_LEN);
+static_assert(4 <= CTLV_UEVENT_MAX_VARS);
+
+/* Guards for generated maxima. */
+static_assert(131072 <= CTLV_MAX_INLINE_MESSAGE_LEN);
+static_assert(3368 <= CTLV_MAX_EVENT_QUEUE_LEN);
+static_assert(3 <= CTLV_MAX_DECLARED_NEST_DEPTH);
+
+/* Local action and query bindings in declaration order. */
+static const struct ctlv_op ctlv_attestation_ops[] = {
+ CTLV_OP_ACTION(ctlv_attestation_device_get),
+ CTLV_OP_ACTION(ctlv_attestation_evidence_get),
+ CTLV_OP_ACTION(ctlv_attestation_mr_get),
+ CTLV_OP_QUERY(ctlv_attestation_mr_dump),
+ CTLV_OP_QUERY(ctlv_attestation_mr_read),
+ CTLV_OP_ACTION(ctlv_attestation_mr_extend),
+ CTLV_OP_QUERY(ctlv_attestation_mr_log_get),
+};
+
+/* Family descriptor used by ctlv_create(). */
+const struct ctlv_family ctlv_attestation_family = {
+ .schema = &ctlv_attestation_schema,
+ .parent = &ctlv_family,
+ .ops = ctlv_attestation_ops,
+ .ops_count = ARRAY_SIZE(ctlv_attestation_ops),
+ .pre = ctlv_attestation_op_pre,
+ .post = ctlv_attestation_op_post,
+ .priv_size = ctlv_attestation_priv_size,
+ .init = ctlv_attestation_init,
+ .post_init = ctlv_attestation_post_init,
+ .pre_fini = ctlv_attestation_pre_fini,
+ .fini = ctlv_attestation_fini,
+};
+EXPORT_SYMBOL_NS_GPL(ctlv_attestation_family, "CTLV");
diff --git a/drivers/attestation/generated/ctlv_attestation_gen.h b/drivers/attestation/generated/ctlv_attestation_gen.h
new file mode 100644
index 000000000000..a40ed0757756
--- /dev/null
+++ b/drivers/attestation/generated/ctlv_attestation_gen.h
@@ -0,0 +1,198 @@
+/* SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause) */
+/* Do not edit, regenerate with tools/ctlv/ctlv-regen.sh, from: */
+/* Documentation/ctlv/specs/ctlv-attestation.yaml */
+/* CTLV-GEN kernel header */
+
+/* Kernel metadata and family API of the ctlv-attestation CTLV family. */
+
+#ifndef _LINUX_CTLV_ATTESTATION_GEN_H
+#define _LINUX_CTLV_ATTESTATION_GEN_H
+
+#include <linux/ctlv_family.h>
+#include <linux/err.h>
+#include <linux/errno.h>
+
+#include <uapi/linux/ctlv_attestation_gen.h>
+
+/* Ops this family declares, in declaration order. */
+extern const struct ctlv_op_schema ctlv_attestation_op_schemas[];
+
+extern const struct ctlv_family_schema ctlv_attestation_schema;
+
+/* Implemented by this family, one per action and query it declares. */
+int ctlv_attestation_device_get_action(void *priv, struct ctlv_op_ctx *ctx,
+ const struct ctlv_cursor *request,
+ struct ctlv_cursor *reply);
+int ctlv_attestation_evidence_get_action(void *priv, struct ctlv_op_ctx *ctx,
+ const struct ctlv_cursor *request,
+ struct ctlv_cursor *reply);
+int ctlv_attestation_mr_get_action(void *priv, struct ctlv_op_ctx *ctx,
+ const struct ctlv_cursor *request, struct ctlv_cursor *reply);
+int ctlv_attestation_mr_dump_query(void *priv, struct ctlv_op_ctx *ctx,
+ const struct ctlv_cursor *request, struct ctlv_cursor *reply);
+int ctlv_attestation_mr_read_query(void *priv, struct ctlv_op_ctx *ctx,
+ const struct ctlv_cursor *request, struct ctlv_cursor *reply);
+int ctlv_attestation_mr_extend_action(void *priv, struct ctlv_op_ctx *ctx,
+ const struct ctlv_cursor *request, struct ctlv_cursor *reply);
+int ctlv_attestation_mr_log_get_query(void *priv, struct ctlv_op_ctx *ctx,
+ const struct ctlv_cursor *request, struct ctlv_cursor *reply);
+
+/* Family-wide hooks around every effective non-core op. */
+int ctlv_attestation_op_pre(void *priv, struct ctlv_op_ctx *ctx);
+void ctlv_attestation_op_post(void *priv, struct ctlv_op_ctx *ctx);
+
+/* Core-allocated family priv lifecycle. */
+size_t ctlv_attestation_priv_size(void);
+int ctlv_attestation_init(struct ctlv_device *ctlvdev, const void *init_info, void *priv);
+int ctlv_attestation_post_init(struct ctlv_device *ctlvdev, void *priv);
+void ctlv_attestation_pre_fini(struct ctlv_device *ctlvdev, void *priv);
+void ctlv_attestation_fini(struct ctlv_device *ctlvdev, void *priv);
+
+extern const struct ctlv_family ctlv_attestation_family;
+
+#ifndef _CTLV_GENERATED_METADATA_ONLY
+
+/* Optional publication and dynamic availability helpers. */
+CTLV_OP_PROVIDE(ctlv_attestation_evidence_get, CTLV_ATTESTATION_OP_EVIDENCE_GET)
+CTLV_OP_ENABLE(ctlv_attestation_evidence_get, CTLV_ATTESTATION_OP_EVIDENCE_GET)
+CTLV_OP_DISABLE(ctlv_attestation_evidence_get, CTLV_ATTESTATION_OP_EVIDENCE_GET)
+CTLV_OP_PROVIDE(ctlv_attestation_mr_get, CTLV_ATTESTATION_OP_MR_GET)
+CTLV_OP_PROVIDE(ctlv_attestation_mr_dump, CTLV_ATTESTATION_OP_MR_DUMP)
+CTLV_OP_PROVIDE(ctlv_attestation_mr_read, CTLV_ATTESTATION_OP_MR_READ)
+CTLV_OP_PROVIDE(ctlv_attestation_mr_extend, CTLV_ATTESTATION_OP_MR_EXTEND)
+CTLV_OP_PROVIDE(ctlv_attestation_mr_log_get, CTLV_ATTESTATION_OP_MR_LOG_GET)
+
+/* Reading a validated message. A blob has a resolver instead of a getter. */
+CTLV_IN_REF(ctlv_attestation_evidence_get_evidence_challenge,
+ &ctlv_attestation_op_schemas[1].request, CTLV_ATTESTATION_ATTR_EVIDENCE_CHALLENGE, 0, 0,
+ CTLV_SCHEMA_BINARY)
+CTLV_IN_HAS(ctlv_attestation_evidence_get_evidence_challenge)
+CTLV_IN_BINARY(ctlv_attestation_evidence_get_evidence_challenge)
+CTLV_IN_REF(ctlv_attestation_evidence_get_evidence_object, &ctlv_attestation_op_schemas[1].request,
+ CTLV_ATTESTATION_ATTR_EVIDENCE_OBJECT, 0, 1, CTLV_SCHEMA_NEST)
+#define ctlv_attestation_evidence_get_evidence_object_for_each(_attr, _nest) \
+ CTLV_IN_FOR_EACH(ctlv_attestation_evidence_get_evidence_object, _attr, _nest)
+CTLV_IN_REF(ctlv_attestation_evidence_get_object_evidence_blob_class,
+ &ctlv_attestation_op_schemas[1].request, CTLV_ATTESTATION_ATTR_EVIDENCE_BLOB_CLASS, 2,
+ 0, CTLV_SCHEMA_ENUM)
+CTLV_IN_HAS(ctlv_attestation_evidence_get_object_evidence_blob_class)
+CTLV_IN_U64(ctlv_attestation_evidence_get_object_evidence_blob_class)
+CTLV_IN_REF(ctlv_attestation_evidence_get_object_evidence_format,
+ &ctlv_attestation_op_schemas[1].request, CTLV_ATTESTATION_ATTR_EVIDENCE_FORMAT, 2, 1,
+ CTLV_SCHEMA_STRING)
+CTLV_IN_HAS(ctlv_attestation_evidence_get_object_evidence_format)
+CTLV_IN_STRING(ctlv_attestation_evidence_get_object_evidence_format)
+CTLV_IN_REF(ctlv_attestation_evidence_get_object_evidence_representation,
+ &ctlv_attestation_op_schemas[1].request, CTLV_ATTESTATION_ATTR_EVIDENCE_REPRESENTATION,
+ 2, 2, CTLV_SCHEMA_ENUM)
+CTLV_IN_HAS(ctlv_attestation_evidence_get_object_evidence_representation)
+CTLV_IN_U64(ctlv_attestation_evidence_get_object_evidence_representation)
+CTLV_IN_REF(ctlv_attestation_evidence_get_object_evidence_data,
+ &ctlv_attestation_op_schemas[1].request, CTLV_ATTESTATION_ATTR_EVIDENCE_DATA, 2, 3,
+ CTLV_SCHEMA_BLOB)
+CTLV_IN_HAS(ctlv_attestation_evidence_get_object_evidence_data)
+CTLV_IN_BLOB(ctlv_attestation_evidence_get_object_evidence_data)
+CTLV_IN_REF(ctlv_attestation_mr_get_mr_id, &ctlv_attestation_op_schemas[2].request,
+ CTLV_ATTESTATION_ATTR_MR_ID, 0, 0, CTLV_SCHEMA_UINT)
+CTLV_IN_HAS(ctlv_attestation_mr_get_mr_id)
+CTLV_IN_U64(ctlv_attestation_mr_get_mr_id)
+CTLV_IN_REF(ctlv_attestation_mr_read_mr_id, &ctlv_attestation_op_schemas[4].request,
+ CTLV_ATTESTATION_ATTR_MR_ID, 0, 0, CTLV_SCHEMA_UINT)
+CTLV_IN_HAS(ctlv_attestation_mr_read_mr_id)
+CTLV_IN_U64(ctlv_attestation_mr_read_mr_id)
+CTLV_IN_REF(ctlv_attestation_mr_extend_mr_id, &ctlv_attestation_op_schemas[5].request,
+ CTLV_ATTESTATION_ATTR_MR_ID, 0, 0, CTLV_SCHEMA_UINT)
+CTLV_IN_HAS(ctlv_attestation_mr_extend_mr_id)
+CTLV_IN_U64(ctlv_attestation_mr_extend_mr_id)
+CTLV_IN_REF(ctlv_attestation_mr_extend_mr_value, &ctlv_attestation_op_schemas[5].request,
+ CTLV_ATTESTATION_ATTR_MR_VALUE, 0, 1, CTLV_SCHEMA_NEST)
+#define ctlv_attestation_mr_extend_mr_value_for_each(_attr, _nest) \
+ CTLV_IN_FOR_EACH(ctlv_attestation_mr_extend_mr_value, _attr, _nest)
+CTLV_IN_REF(ctlv_attestation_mr_extend_value_mr_hash_algo, &ctlv_attestation_op_schemas[5].request,
+ CTLV_ATTESTATION_ATTR_MR_HASH_ALGO, 2, 0, CTLV_SCHEMA_UINT)
+CTLV_IN_HAS(ctlv_attestation_mr_extend_value_mr_hash_algo)
+CTLV_IN_U64(ctlv_attestation_mr_extend_value_mr_hash_algo)
+CTLV_IN_REF(ctlv_attestation_mr_extend_value_mr_data, &ctlv_attestation_op_schemas[5].request,
+ CTLV_ATTESTATION_ATTR_MR_DATA, 2, 1, CTLV_SCHEMA_BINARY)
+CTLV_IN_HAS(ctlv_attestation_mr_extend_value_mr_data)
+CTLV_IN_BINARY(ctlv_attestation_mr_extend_value_mr_data)
+CTLV_IN_REF(ctlv_attestation_mr_log_get_mr_selection, &ctlv_attestation_op_schemas[7].request,
+ CTLV_ATTESTATION_ATTR_MR_SELECTION, 0, 0, CTLV_SCHEMA_NEST)
+#define ctlv_attestation_mr_log_get_mr_selection_for_each(_attr, _nest) \
+ CTLV_IN_FOR_EACH(ctlv_attestation_mr_log_get_mr_selection, _attr, _nest)
+CTLV_IN_REF(ctlv_attestation_mr_log_get_mr_start_generation,
+ &ctlv_attestation_op_schemas[7].request, CTLV_ATTESTATION_ATTR_MR_START_GENERATION, 0,
+ 1, CTLV_SCHEMA_UINT)
+CTLV_IN_HAS(ctlv_attestation_mr_log_get_mr_start_generation)
+CTLV_IN_U64(ctlv_attestation_mr_log_get_mr_start_generation)
+CTLV_IN_REF(ctlv_attestation_mr_log_get_mr_end_generation, &ctlv_attestation_op_schemas[7].request,
+ CTLV_ATTESTATION_ATTR_MR_END_GENERATION, 0, 2, CTLV_SCHEMA_UINT)
+CTLV_IN_HAS(ctlv_attestation_mr_log_get_mr_end_generation)
+CTLV_IN_U64(ctlv_attestation_mr_log_get_mr_end_generation)
+CTLV_IN_REF(ctlv_attestation_mr_log_get_selection_mr_id, &ctlv_attestation_op_schemas[7].request,
+ CTLV_ATTESTATION_ATTR_MR_ID, 1, 0, CTLV_SCHEMA_UINT)
+CTLV_IN_HAS(ctlv_attestation_mr_log_get_selection_mr_id)
+CTLV_IN_U64(ctlv_attestation_mr_log_get_selection_mr_id)
+
+/* Writing a reply. An append is atomic; a nest ends with ctlv_nest_end(). */
+CTLV_PUT_U64(ctlv_attestation_device_generation, CTLV_ATTESTATION_ATTR_DEVICE_GENERATION,
+ CTLV_SCHEMA_UINT)
+CTLV_NEST_START(ctlv_attestation_device_caps, CTLV_ATTESTATION_ATTR_DEVICE_CAPS, CTLV_SCHEMA_NEST)
+CTLV_NEST_START(ctlv_attestation_device_info, CTLV_ATTESTATION_ATTR_DEVICE_INFO, CTLV_SCHEMA_NEST)
+CTLV_NEST_START(ctlv_attestation_cap_evidence, CTLV_ATTESTATION_ATTR_CAP_EVIDENCE, CTLV_SCHEMA_NEST)
+CTLV_NEST_START(ctlv_attestation_cap_mr, CTLV_ATTESTATION_ATTR_CAP_MR, CTLV_SCHEMA_NEST)
+CTLV_PUT_U64(ctlv_attestation_cap_min_challenge_length,
+ CTLV_ATTESTATION_ATTR_CAP_MIN_CHALLENGE_LENGTH, CTLV_SCHEMA_UINT)
+CTLV_PUT_U64(ctlv_attestation_cap_max_challenge_length,
+ CTLV_ATTESTATION_ATTR_CAP_MAX_CHALLENGE_LENGTH, CTLV_SCHEMA_UINT)
+CTLV_PUT_U64(ctlv_attestation_cap_max_total_output_length,
+ CTLV_ATTESTATION_ATTR_CAP_MAX_TOTAL_OUTPUT_LENGTH, CTLV_SCHEMA_UINT)
+CTLV_PUT_U64(ctlv_attestation_cap_mr_flags, CTLV_ATTESTATION_ATTR_CAP_MR_FLAGS, CTLV_SCHEMA_FLAGS)
+CTLV_NEST_START(ctlv_attestation_cap_object, CTLV_ATTESTATION_ATTR_CAP_OBJECT, CTLV_SCHEMA_NEST)
+CTLV_PUT_U64(ctlv_attestation_cap_blob_class, CTLV_ATTESTATION_ATTR_CAP_BLOB_CLASS,
+ CTLV_SCHEMA_ENUM)
+CTLV_PUT_STRING(ctlv_attestation_cap_format, CTLV_ATTESTATION_ATTR_CAP_FORMAT, CTLV_SCHEMA_STRING)
+CTLV_PUT_U64(ctlv_attestation_cap_representation, CTLV_ATTESTATION_ATTR_CAP_REPRESENTATION,
+ CTLV_SCHEMA_ENUM)
+CTLV_PUT_U64(ctlv_attestation_cap_max_length, CTLV_ATTESTATION_ATTR_CAP_MAX_LENGTH,
+ CTLV_SCHEMA_UINT)
+CTLV_PUT_BINARY(ctlv_attestation_evidence_challenge, CTLV_ATTESTATION_ATTR_EVIDENCE_CHALLENGE,
+ CTLV_SCHEMA_BINARY)
+CTLV_NEST_START(ctlv_attestation_evidence_object, CTLV_ATTESTATION_ATTR_EVIDENCE_OBJECT,
+ CTLV_SCHEMA_NEST)
+CTLV_PUT_U64(ctlv_attestation_evidence_blob_class, CTLV_ATTESTATION_ATTR_EVIDENCE_BLOB_CLASS,
+ CTLV_SCHEMA_ENUM)
+CTLV_PUT_STRING(ctlv_attestation_evidence_format, CTLV_ATTESTATION_ATTR_EVIDENCE_FORMAT,
+ CTLV_SCHEMA_STRING)
+CTLV_PUT_U64(ctlv_attestation_evidence_representation,
+ CTLV_ATTESTATION_ATTR_EVIDENCE_REPRESENTATION, CTLV_SCHEMA_ENUM)
+CTLV_PUT_U64(ctlv_attestation_evidence_object_length, CTLV_ATTESTATION_ATTR_EVIDENCE_OBJECT_LENGTH,
+ CTLV_SCHEMA_UINT)
+CTLV_PUT_U64(ctlv_attestation_mr_id, CTLV_ATTESTATION_ATTR_MR_ID, CTLV_SCHEMA_UINT)
+CTLV_PUT_STRING(ctlv_attestation_mr_name, CTLV_ATTESTATION_ATTR_MR_NAME, CTLV_SCHEMA_STRING)
+CTLV_PUT_U64(ctlv_attestation_mr_hash_algo, CTLV_ATTESTATION_ATTR_MR_HASH_ALGO, CTLV_SCHEMA_UINT)
+CTLV_PUT_U64(ctlv_attestation_mr_supported_hash_algo, CTLV_ATTESTATION_ATTR_MR_SUPPORTED_HASH_ALGO,
+ CTLV_SCHEMA_UINT)
+CTLV_PUT_U64(ctlv_attestation_mr_register_index, CTLV_ATTESTATION_ATTR_MR_REGISTER_INDEX,
+ CTLV_SCHEMA_UINT)
+CTLV_PUT_U64(ctlv_attestation_mr_caps, CTLV_ATTESTATION_ATTR_MR_CAPS, CTLV_SCHEMA_FLAGS)
+CTLV_NEST_START(ctlv_attestation_mr_item, CTLV_ATTESTATION_ATTR_MR_ITEM, CTLV_SCHEMA_NEST)
+CTLV_NEST_START(ctlv_attestation_mr_value, CTLV_ATTESTATION_ATTR_MR_VALUE, CTLV_SCHEMA_NEST)
+CTLV_PUT_BINARY(ctlv_attestation_mr_data, CTLV_ATTESTATION_ATTR_MR_DATA, CTLV_SCHEMA_BINARY)
+CTLV_PUT_U64(ctlv_attestation_mr_generation, CTLV_ATTESTATION_ATTR_MR_GENERATION, CTLV_SCHEMA_UINT)
+CTLV_NEST_START(ctlv_attestation_mr_selection, CTLV_ATTESTATION_ATTR_MR_SELECTION, CTLV_SCHEMA_NEST)
+CTLV_PUT_U64(ctlv_attestation_mr_start_generation, CTLV_ATTESTATION_ATTR_MR_START_GENERATION,
+ CTLV_SCHEMA_UINT)
+CTLV_PUT_U64(ctlv_attestation_mr_end_generation, CTLV_ATTESTATION_ATTR_MR_END_GENERATION,
+ CTLV_SCHEMA_UINT)
+CTLV_NEST_START(ctlv_attestation_log_entry, CTLV_ATTESTATION_ATTR_LOG_ENTRY, CTLV_SCHEMA_NEST)
+CTLV_PUT_U64(ctlv_attestation_log_event, CTLV_ATTESTATION_ATTR_LOG_EVENT, CTLV_SCHEMA_ENUM)
+CTLV_NEST_START(ctlv_attestation_log_mr, CTLV_ATTESTATION_ATTR_LOG_MR, CTLV_SCHEMA_NEST)
+CTLV_NEST_START(ctlv_attestation_notification_entry, CTLV_ATTESTATION_ATTR_NOTIFICATION_ENTRY,
+ CTLV_SCHEMA_NEST)
+
+/* Building one event, passed to ctlv_event_emit() or ctlv_event_abort() once. */
+CTLV_EVENT_CREATE(ctlv_attestation_mr_log_entry_appended, CTLV_ATTESTATION_OP_MR_LOG_ENTRY_APPENDED)
+#endif /* _CTLV_GENERATED_METADATA_ONLY */
+
+#endif /* _LINUX_CTLV_ATTESTATION_GEN_H */
diff --git a/include/uapi/linux/ctlv_attestation_gen.h b/include/uapi/linux/ctlv_attestation_gen.h
new file mode 100644
index 000000000000..888bbf06618b
--- /dev/null
+++ b/include/uapi/linux/ctlv_attestation_gen.h
@@ -0,0 +1,252 @@
+/* SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause) */
+/* Do not edit, regenerate with tools/ctlv/ctlv-regen.sh, from: */
+/* Documentation/ctlv/specs/ctlv-attestation.yaml */
+/* CTLV-GEN uapi header */
+
+/*
+ * Constants of the ctlv-attestation CTLV family. Attr and op IDs are complete; the wire format
+ * carrying them is in <linux/ctlv.h>.
+ *
+ * The inherited ctlv header is included here, so one include covers the whole effective schema.
+ */
+
+#ifndef _UAPI_LINUX_CTLV_ATTESTATION_GEN_H
+#define _UAPI_LINUX_CTLV_ATTESTATION_GEN_H
+
+#include <linux/const.h>
+#include <linux/ctlv.h>
+#include <linux/ctlv_families_gen.h>
+#include <linux/ctlv_gen.h>
+
+/* enum evidence-representation */
+enum _ctlv_attestation_evidence_representation {
+ CTLV_ATTESTATION_EVIDENCE_REPRESENTATION_RAW = 1,
+};
+
+/* enum evidence-blob-class */
+enum _ctlv_attestation_evidence_blob_class {
+ CTLV_ATTESTATION_EVIDENCE_BLOB_CLASS_PRIMARY = 1,
+};
+
+/* enum mr-log-event */
+enum _ctlv_attestation_mr_log_event {
+ CTLV_ATTESTATION_MR_LOG_EVENT_INFO = 1,
+ CTLV_ATTESTATION_MR_LOG_EVENT_EXTEND,
+};
+
+/* flags mr-device-caps */
+enum _ctlv_attestation_mr_device_caps_bit {
+ CTLV_ATTESTATION_MR_DEVICE_CAPS_READ_BIT,
+ CTLV_ATTESTATION_MR_DEVICE_CAPS_EXTEND_BIT,
+};
+
+#define CTLV_ATTESTATION_MR_DEVICE_CAPS_READ _BITULL(CTLV_ATTESTATION_MR_DEVICE_CAPS_READ_BIT)
+#define CTLV_ATTESTATION_MR_DEVICE_CAPS_EXTEND _BITULL(CTLV_ATTESTATION_MR_DEVICE_CAPS_EXTEND_BIT)
+#define CTLV_ATTESTATION_MR_DEVICE_CAPS_MASK \
+ (CTLV_ATTESTATION_MR_DEVICE_CAPS_READ | \
+ CTLV_ATTESTATION_MR_DEVICE_CAPS_EXTEND)
+
+/* flags mr-caps */
+enum _ctlv_attestation_mr_caps_bit {
+ CTLV_ATTESTATION_MR_CAPS_READ_BIT,
+ CTLV_ATTESTATION_MR_CAPS_EXTEND_BIT,
+};
+
+#define CTLV_ATTESTATION_MR_CAPS_READ _BITULL(CTLV_ATTESTATION_MR_CAPS_READ_BIT)
+#define CTLV_ATTESTATION_MR_CAPS_EXTEND _BITULL(CTLV_ATTESTATION_MR_CAPS_EXTEND_BIT)
+#define CTLV_ATTESTATION_MR_CAPS_MASK \
+ (CTLV_ATTESTATION_MR_CAPS_READ | \
+ CTLV_ATTESTATION_MR_CAPS_EXTEND)
+
+/* attr groups */
+enum _ctlv_attestation_group {
+ CTLV_ATTESTATION_GROUP_DEVICE = 1,
+ CTLV_ATTESTATION_GROUP_CAP,
+ CTLV_ATTESTATION_GROUP_EVIDENCE,
+ CTLV_ATTESTATION_GROUP_MR,
+ CTLV_ATTESTATION_GROUP_LOG,
+ CTLV_ATTESTATION_GROUP_NOTIFICATION,
+};
+
+#define _CTLV_ATTESTATION_ATTR(_group, _attr) CTLV_ATTR(CTLV_FAMILY_ATTESTATION, (_group), (_attr))
+
+/* attrs of group device */
+#define _CTLV_ATTESTATION_ATTR_DEVICE(_attr) \
+ _CTLV_ATTESTATION_ATTR(CTLV_ATTESTATION_GROUP_DEVICE, _CTLV_ATTESTATION_ATTR_DEVICE_##_attr)
+
+enum _ctlv_attestation_attr_device {
+ _CTLV_ATTESTATION_ATTR_DEVICE_GENERATION = 1,
+ _CTLV_ATTESTATION_ATTR_DEVICE_CAPS,
+ _CTLV_ATTESTATION_ATTR_DEVICE_INFO,
+};
+
+#define CTLV_ATTESTATION_ATTR_DEVICE_GENERATION _CTLV_ATTESTATION_ATTR_DEVICE(GENERATION)
+#define CTLV_ATTESTATION_ATTR_DEVICE_CAPS _CTLV_ATTESTATION_ATTR_DEVICE(CAPS)
+#define CTLV_ATTESTATION_ATTR_DEVICE_INFO _CTLV_ATTESTATION_ATTR_DEVICE(INFO)
+
+/* attrs of group cap */
+#define _CTLV_ATTESTATION_ATTR_CAP(_attr) \
+ _CTLV_ATTESTATION_ATTR(CTLV_ATTESTATION_GROUP_CAP, _CTLV_ATTESTATION_ATTR_CAP_##_attr)
+
+enum _ctlv_attestation_attr_cap {
+ _CTLV_ATTESTATION_ATTR_CAP_EVIDENCE = 1,
+ _CTLV_ATTESTATION_ATTR_CAP_MR,
+ _CTLV_ATTESTATION_ATTR_CAP_MIN_CHALLENGE_LENGTH,
+ _CTLV_ATTESTATION_ATTR_CAP_MAX_CHALLENGE_LENGTH,
+ _CTLV_ATTESTATION_ATTR_CAP_MAX_TOTAL_OUTPUT_LENGTH,
+ _CTLV_ATTESTATION_ATTR_CAP_MR_FLAGS,
+ _CTLV_ATTESTATION_ATTR_CAP_OBJECT,
+ _CTLV_ATTESTATION_ATTR_CAP_BLOB_CLASS,
+ _CTLV_ATTESTATION_ATTR_CAP_FORMAT,
+ _CTLV_ATTESTATION_ATTR_CAP_REPRESENTATION,
+ _CTLV_ATTESTATION_ATTR_CAP_MAX_LENGTH,
+};
+
+#define CTLV_ATTESTATION_ATTR_CAP_EVIDENCE _CTLV_ATTESTATION_ATTR_CAP(EVIDENCE)
+#define CTLV_ATTESTATION_ATTR_CAP_MR _CTLV_ATTESTATION_ATTR_CAP(MR)
+#define CTLV_ATTESTATION_ATTR_CAP_MIN_CHALLENGE_LENGTH \
+ _CTLV_ATTESTATION_ATTR_CAP(MIN_CHALLENGE_LENGTH)
+#define CTLV_ATTESTATION_ATTR_CAP_MAX_CHALLENGE_LENGTH \
+ _CTLV_ATTESTATION_ATTR_CAP(MAX_CHALLENGE_LENGTH)
+#define CTLV_ATTESTATION_ATTR_CAP_MAX_TOTAL_OUTPUT_LENGTH \
+ _CTLV_ATTESTATION_ATTR_CAP(MAX_TOTAL_OUTPUT_LENGTH)
+#define CTLV_ATTESTATION_ATTR_CAP_MR_FLAGS _CTLV_ATTESTATION_ATTR_CAP(MR_FLAGS)
+#define CTLV_ATTESTATION_ATTR_CAP_OBJECT _CTLV_ATTESTATION_ATTR_CAP(OBJECT)
+#define CTLV_ATTESTATION_ATTR_CAP_BLOB_CLASS _CTLV_ATTESTATION_ATTR_CAP(BLOB_CLASS)
+#define CTLV_ATTESTATION_ATTR_CAP_FORMAT _CTLV_ATTESTATION_ATTR_CAP(FORMAT)
+#define CTLV_ATTESTATION_ATTR_CAP_REPRESENTATION _CTLV_ATTESTATION_ATTR_CAP(REPRESENTATION)
+#define CTLV_ATTESTATION_ATTR_CAP_MAX_LENGTH _CTLV_ATTESTATION_ATTR_CAP(MAX_LENGTH)
+
+/* attrs of group evidence */
+#define _CTLV_ATTESTATION_ATTR_EVIDENCE(_attr) \
+ _CTLV_ATTESTATION_ATTR(CTLV_ATTESTATION_GROUP_EVIDENCE, \
+ _CTLV_ATTESTATION_ATTR_EVIDENCE_##_attr)
+
+enum _ctlv_attestation_attr_evidence {
+ _CTLV_ATTESTATION_ATTR_EVIDENCE_CHALLENGE = 1,
+ _CTLV_ATTESTATION_ATTR_EVIDENCE_OBJECT,
+ _CTLV_ATTESTATION_ATTR_EVIDENCE_BLOB_CLASS,
+ _CTLV_ATTESTATION_ATTR_EVIDENCE_FORMAT,
+ _CTLV_ATTESTATION_ATTR_EVIDENCE_REPRESENTATION,
+ _CTLV_ATTESTATION_ATTR_EVIDENCE_DATA,
+ _CTLV_ATTESTATION_ATTR_EVIDENCE_OBJECT_LENGTH,
+};
+
+#define CTLV_ATTESTATION_ATTR_EVIDENCE_CHALLENGE _CTLV_ATTESTATION_ATTR_EVIDENCE(CHALLENGE)
+#define CTLV_ATTESTATION_ATTR_EVIDENCE_OBJECT _CTLV_ATTESTATION_ATTR_EVIDENCE(OBJECT)
+#define CTLV_ATTESTATION_ATTR_EVIDENCE_BLOB_CLASS _CTLV_ATTESTATION_ATTR_EVIDENCE(BLOB_CLASS)
+#define CTLV_ATTESTATION_ATTR_EVIDENCE_FORMAT _CTLV_ATTESTATION_ATTR_EVIDENCE(FORMAT)
+#define CTLV_ATTESTATION_ATTR_EVIDENCE_REPRESENTATION \
+ _CTLV_ATTESTATION_ATTR_EVIDENCE(REPRESENTATION)
+#define CTLV_ATTESTATION_ATTR_EVIDENCE_DATA _CTLV_ATTESTATION_ATTR_EVIDENCE(DATA)
+#define CTLV_ATTESTATION_ATTR_EVIDENCE_OBJECT_LENGTH _CTLV_ATTESTATION_ATTR_EVIDENCE(OBJECT_LENGTH)
+
+/* attrs of group mr */
+#define _CTLV_ATTESTATION_ATTR_MR(_attr) \
+ _CTLV_ATTESTATION_ATTR(CTLV_ATTESTATION_GROUP_MR, _CTLV_ATTESTATION_ATTR_MR_##_attr)
+
+enum _ctlv_attestation_attr_mr {
+ _CTLV_ATTESTATION_ATTR_MR_ID = 1,
+ _CTLV_ATTESTATION_ATTR_MR_NAME,
+ _CTLV_ATTESTATION_ATTR_MR_HASH_ALGO,
+ _CTLV_ATTESTATION_ATTR_MR_SUPPORTED_HASH_ALGO,
+ _CTLV_ATTESTATION_ATTR_MR_REGISTER_INDEX,
+ _CTLV_ATTESTATION_ATTR_MR_CAPS,
+ _CTLV_ATTESTATION_ATTR_MR_ITEM,
+ _CTLV_ATTESTATION_ATTR_MR_VALUE,
+ _CTLV_ATTESTATION_ATTR_MR_DATA,
+ _CTLV_ATTESTATION_ATTR_MR_GENERATION,
+ _CTLV_ATTESTATION_ATTR_MR_SELECTION,
+ _CTLV_ATTESTATION_ATTR_MR_START_GENERATION,
+ _CTLV_ATTESTATION_ATTR_MR_END_GENERATION,
+};
+
+#define CTLV_ATTESTATION_ATTR_MR_ID _CTLV_ATTESTATION_ATTR_MR(ID)
+#define CTLV_ATTESTATION_ATTR_MR_NAME _CTLV_ATTESTATION_ATTR_MR(NAME)
+#define CTLV_ATTESTATION_ATTR_MR_HASH_ALGO _CTLV_ATTESTATION_ATTR_MR(HASH_ALGO)
+#define CTLV_ATTESTATION_ATTR_MR_SUPPORTED_HASH_ALGO _CTLV_ATTESTATION_ATTR_MR(SUPPORTED_HASH_ALGO)
+#define CTLV_ATTESTATION_ATTR_MR_REGISTER_INDEX _CTLV_ATTESTATION_ATTR_MR(REGISTER_INDEX)
+#define CTLV_ATTESTATION_ATTR_MR_CAPS _CTLV_ATTESTATION_ATTR_MR(CAPS)
+#define CTLV_ATTESTATION_ATTR_MR_ITEM _CTLV_ATTESTATION_ATTR_MR(ITEM)
+#define CTLV_ATTESTATION_ATTR_MR_VALUE _CTLV_ATTESTATION_ATTR_MR(VALUE)
+#define CTLV_ATTESTATION_ATTR_MR_DATA _CTLV_ATTESTATION_ATTR_MR(DATA)
+#define CTLV_ATTESTATION_ATTR_MR_GENERATION _CTLV_ATTESTATION_ATTR_MR(GENERATION)
+#define CTLV_ATTESTATION_ATTR_MR_SELECTION _CTLV_ATTESTATION_ATTR_MR(SELECTION)
+#define CTLV_ATTESTATION_ATTR_MR_START_GENERATION _CTLV_ATTESTATION_ATTR_MR(START_GENERATION)
+#define CTLV_ATTESTATION_ATTR_MR_END_GENERATION _CTLV_ATTESTATION_ATTR_MR(END_GENERATION)
+
+/* attrs of group log */
+#define _CTLV_ATTESTATION_ATTR_LOG(_attr) \
+ _CTLV_ATTESTATION_ATTR(CTLV_ATTESTATION_GROUP_LOG, _CTLV_ATTESTATION_ATTR_LOG_##_attr)
+
+enum _ctlv_attestation_attr_log {
+ _CTLV_ATTESTATION_ATTR_LOG_ENTRY = 1,
+ _CTLV_ATTESTATION_ATTR_LOG_EVENT,
+ _CTLV_ATTESTATION_ATTR_LOG_MR,
+};
+
+#define CTLV_ATTESTATION_ATTR_LOG_ENTRY _CTLV_ATTESTATION_ATTR_LOG(ENTRY)
+#define CTLV_ATTESTATION_ATTR_LOG_EVENT _CTLV_ATTESTATION_ATTR_LOG(EVENT)
+#define CTLV_ATTESTATION_ATTR_LOG_MR _CTLV_ATTESTATION_ATTR_LOG(MR)
+
+/* attrs of group notification */
+#define _CTLV_ATTESTATION_ATTR_NOTIFICATION(_attr) \
+ _CTLV_ATTESTATION_ATTR(CTLV_ATTESTATION_GROUP_NOTIFICATION, \
+ _CTLV_ATTESTATION_ATTR_NOTIFICATION_##_attr)
+
+enum _ctlv_attestation_attr_notification {
+ _CTLV_ATTESTATION_ATTR_NOTIFICATION_ENTRY = 1,
+};
+
+#define CTLV_ATTESTATION_ATTR_NOTIFICATION_ENTRY _CTLV_ATTESTATION_ATTR_NOTIFICATION(ENTRY)
+
+/* attr sets */
+enum _ctlv_attestation_set {
+ CTLV_ATTESTATION_SET_DEVICE_INFO = 1,
+ CTLV_ATTESTATION_SET_EVIDENCE_OBJECT_CAP,
+ CTLV_ATTESTATION_SET_EVIDENCE_CAP,
+ CTLV_ATTESTATION_SET_MR_DEVICE_CAP,
+ CTLV_ATTESTATION_SET_DEVICE_CAPS,
+ CTLV_ATTESTATION_SET_DEVICE_REPLY,
+ CTLV_ATTESTATION_SET_EVIDENCE_OBJECT_REQUEST,
+ CTLV_ATTESTATION_SET_EVIDENCE_REQUEST,
+ CTLV_ATTESTATION_SET_EVIDENCE_REPLY,
+ CTLV_ATTESTATION_SET_MR_PROPS,
+ CTLV_ATTESTATION_SET_MR_DUMP_ENTRY,
+ CTLV_ATTESTATION_SET_MR_ID_REQUEST,
+ CTLV_ATTESTATION_SET_MR_READ_REQUEST,
+ CTLV_ATTESTATION_SET_MR_VALUE,
+ CTLV_ATTESTATION_SET_MR_READ_REPLY,
+ CTLV_ATTESTATION_SET_MR_EXTEND_REQUEST,
+ CTLV_ATTESTATION_SET_MR_CHANGE_REPLY,
+ CTLV_ATTESTATION_SET_MR_LOG_INFO,
+ CTLV_ATTESTATION_SET_MR_LOG_ENTRY,
+ CTLV_ATTESTATION_SET_MR_LOG_EVENT,
+ CTLV_ATTESTATION_SET_MR_LOG_SELECTION,
+ CTLV_ATTESTATION_SET_MR_LOG_REQUEST,
+ CTLV_ATTESTATION_SET_MR_LOG_REPLY,
+};
+
+/* ops */
+enum _ctlv_attestation_op {
+ _CTLV_ATTESTATION_OP_DEVICE_GET = 1,
+ _CTLV_ATTESTATION_OP_EVIDENCE_GET,
+ _CTLV_ATTESTATION_OP_MR_GET,
+ _CTLV_ATTESTATION_OP_MR_DUMP,
+ _CTLV_ATTESTATION_OP_MR_READ,
+ _CTLV_ATTESTATION_OP_MR_EXTEND,
+ _CTLV_ATTESTATION_OP_MR_LOG_ENTRY_APPENDED,
+ _CTLV_ATTESTATION_OP_MR_LOG_GET,
+};
+
+#define _CTLV_ATTESTATION_OP(_op) CTLV_OP(CTLV_FAMILY_ATTESTATION, _CTLV_ATTESTATION_OP_##_op)
+#define CTLV_ATTESTATION_OP_DEVICE_GET _CTLV_ATTESTATION_OP(DEVICE_GET)
+#define CTLV_ATTESTATION_OP_EVIDENCE_GET _CTLV_ATTESTATION_OP(EVIDENCE_GET)
+#define CTLV_ATTESTATION_OP_MR_GET _CTLV_ATTESTATION_OP(MR_GET)
+#define CTLV_ATTESTATION_OP_MR_DUMP _CTLV_ATTESTATION_OP(MR_DUMP)
+#define CTLV_ATTESTATION_OP_MR_READ _CTLV_ATTESTATION_OP(MR_READ)
+#define CTLV_ATTESTATION_OP_MR_EXTEND _CTLV_ATTESTATION_OP(MR_EXTEND)
+#define CTLV_ATTESTATION_OP_MR_LOG_ENTRY_APPENDED _CTLV_ATTESTATION_OP(MR_LOG_ENTRY_APPENDED)
+#define CTLV_ATTESTATION_OP_MR_LOG_GET _CTLV_ATTESTATION_OP(MR_LOG_GET)
+
+#endif /* _UAPI_LINUX_CTLV_ATTESTATION_GEN_H */
diff --git a/include/uapi/linux/ctlv_families_gen.h b/include/uapi/linux/ctlv_families_gen.h
index a649483ab3f5..2ea3068868be 100644
--- a/include/uapi/linux/ctlv_families_gen.h
+++ b/include/uapi/linux/ctlv_families_gen.h
@@ -10,6 +10,7 @@
enum _ctlv_family {
CTLV_FAMILY_ROOT,
+ CTLV_FAMILY_ATTESTATION,
};
#endif /* _UAPI_LINUX_CTLV_FAMILIES_GEN_H */
--
2.56.0
^ permalink raw reply related [flat|nested] 32+ messages in thread* [PATCH RFC 15/20] attestation: Prepare provider and instance core
2026-10-15 14:50 [PATCH RFC 00/20] attestation: Add a common subsystem and TDX support Jiri Pirko
` (13 preceding siblings ...)
2026-10-15 14:50 ` [PATCH RFC 14/20] attestation: Add the base family specification and generated files Jiri Pirko
@ 2026-10-15 14:50 ` Jiri Pirko
2026-10-15 15:30 ` sashiko-bot
2026-10-15 14:50 ` [PATCH RFC 16/20] attestation: Prepare measurement register journal Jiri Pirko
` (4 subsequent siblings)
19 siblings, 1 reply; 32+ messages in thread
From: Jiri Pirko @ 2026-10-15 14:50 UTC (permalink / raw)
To: linux-coco
Cc: linux-kernel, linux-api, x86, kvm, linux-doc, jgg, david.quigley,
lukas, ksinyuk, rodrigo.vivi, cedric.xing, sameo, drdeeglaze,
James.Bottomley, aik, jpiotrowski, qinkun, pgonda, jic23,
alistair.francis, ankita, suzuki.poulose, kas, rick.p.edgecombe,
elena.reshetova, dave.hansen, sathyanarayanan.kuppuswamy,
peter.fang
From: Jiri Pirko <jiri@nvidia.com>
Add provider validation, instance registration and lifecycle management for
the attestation family.
The provider descriptor is one immutable evidence and MR contract; journal
storage follows in the next patch. The core object can be compile-checked
here. CONFIG_ATTESTATION remains unwired until the CTLV handlers land.
Assisted-by: Cursor: GPT-5.6 Sol
Assisted-by: Cursor: Claude Opus 5.5
Signed-off-by: Jiri Pirko <jiri@nvidia.com>
---
Documentation/driver-api/attestation.rst | 36 +++
Documentation/driver-api/index.rst | 1 +
MAINTAINERS | 2 +
drivers/attestation/Kconfig | 12 +
drivers/attestation/Makefile | 4 +
drivers/attestation/core/core.c | 280 +++++++++++++++++++++++
drivers/attestation/core/internal.h | 35 +++
include/linux/attestation.h | 138 +++++++++++
8 files changed, 508 insertions(+)
create mode 100644 Documentation/driver-api/attestation.rst
create mode 100644 drivers/attestation/Kconfig
create mode 100644 drivers/attestation/Makefile
create mode 100644 drivers/attestation/core/core.c
create mode 100644 drivers/attestation/core/internal.h
create mode 100644 include/linux/attestation.h
diff --git a/Documentation/driver-api/attestation.rst b/Documentation/driver-api/attestation.rst
new file mode 100644
index 000000000000..6d0cf9095c66
--- /dev/null
+++ b/Documentation/driver-api/attestation.rst
@@ -0,0 +1,36 @@
+.. SPDX-License-Identifier: GPL-2.0-only
+
+===========
+Attestation
+===========
+
+The attestation subsystem publishes provider devices as descendants of the
+``ctlv-attestation`` family. It owns common evidence retrieval,
+measurement-register topology and an append-only extension journal.
+Providers retain ownership of native hardware access and immutable capability
+descriptions.
+
+Registering a provider
+======================
+
+A provider defines ``struct attestation_provider`` and passes it to the base
+family through its generated ``pre_init`` callback. The descriptor, evidence
+capabilities, fixed MR properties, strings and arrays remain immutable until
+``ctlv_destroy()`` or failed-creation unwind completes.
+
+The provider family creates the CTLV device with its own generated family
+descriptor. Its family-wide operation hooks delegate once to
+``ctlv_attestation_op_pre()`` and ``ctlv_attestation_op_post()`` using the
+attestation parent priv. The hooks serialize an operation, its native
+callback, journal commit and event emission with one instance mutex.
+
+Fixed MRs are listed in the provider descriptor. Their property storage
+remains valid until ``ctlv_destroy()`` or failed-creation unwind completes.
+
+Teardown
+========
+
+CTLV first drains userspace operations. The base ``pre_fini`` callback then
+withdraws the instance from the kernel registry while provider state and CTLV
+event emission remain available. CTLV stops events only after that callback
+returns; provider and base ``fini`` callbacks may then release native storage.
diff --git a/Documentation/driver-api/index.rst b/Documentation/driver-api/index.rst
index e85a7a982bfc..ecf82a9330fc 100644
--- a/Documentation/driver-api/index.rst
+++ b/Documentation/driver-api/index.rst
@@ -38,6 +38,7 @@ of interest to most developers working on device drivers.
early-userspace/index
connector
ctlv
+ attestation
device-io
devfreq
dma-buf
diff --git a/MAINTAINERS b/MAINTAINERS
index c8ff980cce13..5972a643b3c3 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -4337,7 +4337,9 @@ L: linux-security-module@vger.kernel.org
S: Supported
F: Documentation/ctlv/abi/ctlv-attestation*
F: Documentation/ctlv/specs/ctlv-attestation*
+F: Documentation/driver-api/attestation.rst
F: drivers/attestation/
+F: include/linux/attestation*
F: include/uapi/linux/ctlv_attestation*
ATTO EXPRESSSAS SAS/SATA RAID SCSI DRIVER
diff --git a/drivers/attestation/Kconfig b/drivers/attestation/Kconfig
new file mode 100644
index 000000000000..e2fb4b4205fe
--- /dev/null
+++ b/drivers/attestation/Kconfig
@@ -0,0 +1,12 @@
+# SPDX-License-Identifier: GPL-2.0-only
+menuconfig ATTESTATION
+ tristate "Attestation"
+ depends on CTLV
+ help
+ Provides a CTLV family for evidence, measurement registers,
+ and an append-only measurement journal.
+
+ Providers register one instance for a physical or logical device and
+ translate the common operations to their native service.
+
+ If you are unsure, say N.
diff --git a/drivers/attestation/Makefile b/drivers/attestation/Makefile
new file mode 100644
index 000000000000..8a75558a7013
--- /dev/null
+++ b/drivers/attestation/Makefile
@@ -0,0 +1,4 @@
+# SPDX-License-Identifier: GPL-2.0
+obj-$(CONFIG_ATTESTATION) += attestation.o
+
+attestation-y := core/core.o
diff --git a/drivers/attestation/core/core.c b/drivers/attestation/core/core.c
new file mode 100644
index 000000000000..e112d3a0ff2b
--- /dev/null
+++ b/drivers/attestation/core/core.c
@@ -0,0 +1,280 @@
+// SPDX-License-Identifier: GPL-2.0-only
+#include <crypto/hash_info.h>
+#include <linux/cleanup.h>
+#include <linux/ctlv.h>
+#include <linux/device.h>
+#include <linux/err.h>
+#include <linux/limits.h>
+#include <linux/module.h>
+#include <linux/overflow.h>
+#include <linux/slab.h>
+#include <linux/string.h>
+
+#include "internal.h"
+
+static DEFINE_XARRAY(attestation_instances);
+
+static DEFINE_MUTEX(attestation_registry_lock);
+static unsigned long attestation_next_index;
+
+static bool attestation_algorithm_valid(enum hash_algo algorithm)
+{
+ return algorithm >= 0 && algorithm < HASH_ALGO__LAST && hash_digest_size[algorithm] &&
+ hash_digest_size[algorithm] <= HASH_MAX_DIGESTSIZE;
+}
+
+static int attestation_mr_props_validate(const struct attestation_provider *provider,
+ const struct attestation_mr_props *props)
+{
+ size_t index;
+
+ if (!props || !props->name || !*props->name || strlen(props->name) > 128 ||
+ !props->algorithms || !props->algorithms_count || props->algorithms_count > 21)
+ return -EINVAL;
+ for (index = 0; index < props->algorithms_count; index++) {
+ enum hash_algo algorithm = props->algorithms[index];
+
+ if (!attestation_algorithm_valid(algorithm) ||
+ (index && props->algorithms[index - 1] >= algorithm))
+ return -EINVAL;
+ }
+ if (props->readable && !provider->mr_read)
+ return -EINVAL;
+ if (props->extendable && !provider->mr_extend)
+ return -EINVAL;
+ return 0;
+}
+
+static int attestation_evidence_caps_validate(const struct attestation_provider *provider)
+{
+ const struct attestation_evidence_caps *caps = provider->evidence_caps;
+ size_t object_index;
+
+ if (!caps)
+ return provider->evidence_get ? -EINVAL : 0;
+ if (!provider->evidence_get || !caps->objects || !caps->objects_count ||
+ caps->objects_count > 16 || caps->min_challenge_length > caps->max_challenge_length ||
+ caps->max_challenge_length > 4096 || !caps->max_total_output_length)
+ return -EINVAL;
+ for (object_index = 0; object_index < caps->objects_count; object_index++) {
+ const struct attestation_evidence_supported_object *object =
+ &caps->objects[object_index];
+ size_t index;
+
+ if (!object->format || !*object->format || strlen(object->format) > 128 ||
+ !object->max_length || object->max_length > UINT_MAX ||
+ object->max_length > caps->max_total_output_length)
+ return -EINVAL;
+ for (index = 0; index < object_index; index++) {
+ const struct attestation_evidence_supported_object *prior =
+ &caps->objects[index];
+
+ if (!strcmp(prior->format, object->format))
+ return -EINVAL;
+ }
+ }
+ return 0;
+}
+
+int attestation_provider_validate(const struct attestation_provider *provider)
+{
+ size_t index;
+ int ret;
+
+ if (!provider)
+ return -EINVAL;
+ ret = attestation_evidence_caps_validate(provider);
+ if (ret)
+ return ret;
+ if (!!provider->mrs != !!provider->mrs_count)
+ return -EINVAL;
+ if ((provider->mr_read || provider->mr_extend) && !provider->mrs_count)
+ return -EINVAL;
+ for (index = 0; index < provider->mrs_count; index++) {
+ ret = attestation_mr_props_validate(provider, &provider->mrs[index]);
+ if (ret)
+ return ret;
+ }
+ return 0;
+}
+
+static bool attestation_parent_exists(struct device *parent)
+{
+ struct attestation *att;
+ unsigned long index;
+
+ xa_for_each(&attestation_instances, index, att)
+ if (ctlv_device_parent(att->ctlvdev) == parent)
+ return true;
+ return false;
+}
+
+static int attestation_registry_insert(struct attestation *att)
+{
+ unsigned long index;
+ void *old;
+ int ret;
+
+ guard(mutex)
+ (&attestation_registry_lock);
+ if (attestation_next_index > UINT_MAX)
+ return -ENOSPC;
+ index = attestation_next_index;
+ ret = xa_reserve(&attestation_instances, index, GFP_KERNEL);
+ if (ret)
+ return ret;
+
+ xa_lock(&attestation_instances);
+ if (attestation_parent_exists(ctlv_device_parent(att->ctlvdev))) {
+ __xa_erase(&attestation_instances, index);
+ ret = -EEXIST;
+ goto out_unlock;
+ }
+ old = __xa_store(&attestation_instances, index, att, GFP_NOWAIT);
+ ret = xa_err(old);
+ if (!ret) {
+ att->index = index;
+ attestation_next_index++;
+ }
+out_unlock:
+ xa_unlock(&attestation_instances);
+ return ret;
+}
+
+static void attestation_registry_remove(struct attestation *att)
+{
+ xa_erase(&attestation_instances, att->index);
+}
+
+bool attestation_registered(struct attestation *att)
+{
+ bool registered;
+
+ lockdep_assert_held(&att->lock);
+ xa_lock(&attestation_instances);
+ registered = xa_get_mark(&attestation_instances, att->index, ATTESTATION_REGISTERED);
+ xa_unlock(&attestation_instances);
+ return registered;
+}
+
+void attestation_stop(struct attestation *att)
+{
+ lockdep_assert_held(&att->lock);
+ xa_lock(&attestation_instances);
+ __xa_clear_mark(&attestation_instances, att->index, ATTESTATION_REGISTERED);
+ xa_unlock(&attestation_instances);
+}
+
+void attestation_provide_ops(struct attestation *att)
+{
+ const struct attestation_provider *provider = att->provider;
+ struct ctlv_device *ctlvdev = att->ctlvdev;
+
+ if (provider->evidence_caps) {
+ ctlv_attestation_evidence_get_provide(ctlvdev);
+ ctlv_attestation_evidence_get_enable(ctlvdev);
+ }
+}
+
+int attestation_core_init(struct attestation *att, struct ctlv_device *ctlvdev,
+ const struct attestation_provider *provider)
+{
+ int ret;
+
+ ret = attestation_provider_validate(provider);
+ if (ret)
+ return ret;
+ mutex_init(&att->lock);
+ att->ctlvdev = ctlvdev;
+ att->provider = provider;
+ ret = attestation_registry_insert(att);
+ if (ret)
+ goto err_core;
+ attestation_provide_ops(att);
+ att->initialized = true;
+ return 0;
+
+err_core:
+ attestation_core_fini(att);
+ return ret;
+}
+
+void attestation_core_fini(struct attestation *att)
+{
+ if (!att->ctlvdev)
+ return;
+ if (att->initialized)
+ attestation_registry_remove(att);
+ mutex_destroy(&att->lock);
+ att->ctlvdev = NULL;
+}
+
+size_t ctlv_attestation_priv_size(void)
+{
+ return sizeof(struct attestation);
+}
+
+int ctlv_attestation_init(struct ctlv_device *ctlvdev, const void *init_info, void *priv)
+{
+ return attestation_core_init(priv, ctlvdev, init_info);
+}
+
+int ctlv_attestation_post_init(struct ctlv_device *ctlvdev, void *priv)
+{
+ struct attestation *att = priv;
+
+ xa_lock(&attestation_instances);
+ __xa_set_mark(&attestation_instances, att->index, ATTESTATION_REGISTERED);
+ xa_unlock(&attestation_instances);
+ return 0;
+}
+
+void ctlv_attestation_pre_fini(struct ctlv_device *ctlvdev, void *priv)
+{
+ struct attestation *att = priv;
+
+ xa_lock(&attestation_instances);
+ __xa_clear_mark(&attestation_instances, att->index, ATTESTATION_REGISTERED);
+ xa_unlock(&attestation_instances);
+}
+
+void ctlv_attestation_fini(struct ctlv_device *ctlvdev, void *priv)
+{
+ attestation_core_fini(priv);
+}
+
+int ctlv_attestation_op_pre(void *priv, struct ctlv_op_ctx *ctx)
+{
+ struct attestation *att = priv;
+
+ mutex_lock(&att->lock);
+ if (!attestation_registered(att)) {
+ mutex_unlock(&att->lock);
+ return -ENODEV;
+ }
+ return 0;
+}
+
+void ctlv_attestation_op_post(void *priv, struct ctlv_op_ctx *ctx)
+{
+ struct attestation *att = priv;
+
+ mutex_unlock(&att->lock);
+}
+
+static int __init attestation_module_init(void)
+{
+ return 0;
+}
+
+static void __exit attestation_module_exit(void)
+{
+ WARN_ON_ONCE(!xa_empty(&attestation_instances));
+ xa_destroy(&attestation_instances);
+}
+
+module_init(attestation_module_init);
+module_exit(attestation_module_exit);
+MODULE_DESCRIPTION("Attestation core");
+MODULE_LICENSE("GPL");
+MODULE_IMPORT_NS("CTLV");
diff --git a/drivers/attestation/core/internal.h b/drivers/attestation/core/internal.h
new file mode 100644
index 000000000000..767c02275d0f
--- /dev/null
+++ b/drivers/attestation/core/internal.h
@@ -0,0 +1,35 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef _ATTESTATION_INTERNAL_H
+#define _ATTESTATION_INTERNAL_H
+
+#include <linux/attestation.h>
+#include <linux/ctlv.h>
+#include <linux/mutex.h>
+#include <linux/xarray.h>
+
+#include "../generated/ctlv_attestation_gen.h"
+
+#define ATTESTATION_REGISTERED XA_MARK_1
+
+struct attestation {
+ struct ctlv_device *ctlvdev;
+ const struct attestation_provider *provider;
+ unsigned int index;
+
+ /* Mutable instance state. */
+ struct mutex lock;
+
+ bool initialized;
+};
+
+int attestation_core_init(struct attestation *att,
+ struct ctlv_device *ctlvdev,
+ const struct attestation_provider *provider);
+void attestation_core_fini(struct attestation *att);
+bool attestation_registered(struct attestation *att);
+void attestation_stop(struct attestation *att);
+
+int attestation_provider_validate(const struct attestation_provider *provider);
+void attestation_provide_ops(struct attestation *att);
+
+#endif /* _ATTESTATION_INTERNAL_H */
diff --git a/include/linux/attestation.h b/include/linux/attestation.h
new file mode 100644
index 000000000000..0fd806ea77d8
--- /dev/null
+++ b/include/linux/attestation.h
@@ -0,0 +1,138 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef _LINUX_ATTESTATION_H
+#define _LINUX_ATTESTATION_H
+
+#include <crypto/hash.h>
+#include <linux/types.h>
+
+#include <uapi/linux/ctlv_attestation_gen.h>
+#include <uapi/linux/hash_info.h>
+
+struct attestation;
+struct ctlv_blob;
+
+/**
+ * struct attestation_digest - One MR digest.
+ * @algorithm: hash algorithm
+ * @data: digest bytes
+ */
+struct attestation_digest {
+ enum hash_algo algorithm;
+ u8 data[HASH_MAX_DIGESTSIZE];
+};
+
+/**
+ * struct attestation_digest_set - Digests ordered as the MR algorithms.
+ * @values_count: number of digests
+ * @values: digest array
+ */
+struct attestation_digest_set {
+ size_t values_count;
+ struct attestation_digest *values;
+};
+
+/**
+ * struct attestation_mr_props - Immutable MR properties.
+ * @id: provider-unique MR ID
+ * @name: MR name
+ * @algorithms: ordered hash algorithms
+ * @algorithms_count: number of algorithms
+ * @has_register_index: whether @register_index is valid
+ * @register_index: native register index
+ * @readable: MR can be read
+ * @extendable: MR can be extended
+ */
+struct attestation_mr_props {
+ unsigned int id;
+ const char *name;
+ const enum hash_algo *algorithms;
+ size_t algorithms_count;
+ bool has_register_index;
+ unsigned int register_index;
+ bool readable;
+ bool extendable;
+};
+
+/**
+ * struct attestation_evidence_supported_object - Evidence output description.
+ * @format: evidence format name
+ * @max_length: maximum output bytes
+ * @binds_mrs: evidence validity depends on MR state
+ */
+struct attestation_evidence_supported_object {
+ const char *format;
+ size_t max_length;
+ bool binds_mrs;
+};
+
+/**
+ * struct attestation_evidence_caps - Immutable evidence capabilities.
+ * @min_challenge_length: minimum challenge bytes
+ * @max_challenge_length: maximum challenge bytes
+ * @max_total_output_length: maximum bytes across all objects
+ * @objects: supported evidence objects
+ * @objects_count: number of objects
+ */
+struct attestation_evidence_caps {
+ size_t min_challenge_length;
+ size_t max_challenge_length;
+ size_t max_total_output_length;
+ const struct attestation_evidence_supported_object *objects;
+ size_t objects_count;
+};
+
+/**
+ * struct attestation_evidence_object_request - One resolved output object.
+ * @supported_object: matching capability entry
+ * @output: resolved output blob
+ */
+struct attestation_evidence_object_request {
+ const struct attestation_evidence_supported_object *supported_object;
+ struct ctlv_blob *output;
+};
+
+/**
+ * struct attestation_evidence_request - Validated evidence request.
+ * @challenge: challenge bytes
+ * @challenge_length: challenge length
+ * @objects: requested output objects
+ * @objects_count: number of objects
+ */
+struct attestation_evidence_request {
+ const u8 *challenge;
+ size_t challenge_length;
+ const struct attestation_evidence_object_request *objects;
+ size_t objects_count;
+};
+
+/**
+ * struct attestation_evidence_result - Provider evidence result.
+ * @object_lengths: produced length for every requested object
+ */
+struct attestation_evidence_result {
+ size_t *object_lengths;
+};
+
+/**
+ * struct attestation_provider - Immutable provider operations and properties.
+ * @evidence_get: produce requested evidence
+ * @mr_read: read one MR
+ * @mr_extend: extend one MR
+ * @evidence_caps: evidence capabilities, or NULL
+ * @mrs: fixed MR properties
+ * @mrs_count: number of fixed MRs
+ */
+struct attestation_provider {
+ int (*evidence_get)(struct attestation *att,
+ const struct attestation_evidence_request *request,
+ struct attestation_evidence_result *result);
+ int (*mr_read)(struct attestation *att, unsigned int mr_id,
+ struct attestation_digest_set *values);
+ int (*mr_extend)(struct attestation *att, unsigned int mr_id,
+ const struct attestation_digest_set *values);
+ const struct attestation_evidence_caps *evidence_caps;
+ const struct attestation_mr_props *mrs;
+ size_t mrs_count;
+};
+
+#endif /* _LINUX_ATTESTATION_H */
--
2.56.0
^ permalink raw reply related [flat|nested] 32+ messages in thread* Re: [PATCH RFC 15/20] attestation: Prepare provider and instance core
2026-10-15 14:50 ` [PATCH RFC 15/20] attestation: Prepare provider and instance core Jiri Pirko
@ 2026-10-15 15:30 ` sashiko-bot
0 siblings, 0 replies; 32+ messages in thread
From: sashiko-bot @ 2026-10-15 15:30 UTC (permalink / raw)
To: Jiri Pirko; +Cc: kvm
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] attestation: Mixing guard() and goto in attestation_registry_insert
- [Low] attestation: ID space exhaustion due to non-recycling monotonic index
--
commit f05c5cea6b4219fc6f6c64ba092550bdd10f870b
Author: Jiri Pirko <jiri@resnulli.us>
attestation: Prepare provider and instance core
Add provider validation, instance registration and lifecycle management for
the attestation family.
> diff --git a/drivers/attestation/core/core.c b/drivers/attestation/core/core.c
> new file mode 100644
> index 0000000000000..e112d3a0ff2b3
> --- /dev/null
> +++ b/drivers/attestation/core/core.c
[ ... ]
> +static int attestation_registry_insert(struct attestation *att)
> +{
> + unsigned long index;
> + void *old;
> + int ret;
> +
> + guard(mutex)
> + (&attestation_registry_lock);
> + if (attestation_next_index > UINT_MAX)
> + return -ENOSPC;
[Severity: Low]
Because attestation_next_index is a monotonically increasing counter that is
never recycled upon instance unregistration, will this eventually exhaust the
32-bit ID space after enough hotplug or module reload cycles, preventing any
new providers from registering until reboot?
Could we use ida_alloc() or xa_alloc() to safely allocate and recycle
identifiers instead?
> + index = attestation_next_index;
> + ret = xa_reserve(&attestation_instances, index, GFP_KERNEL);
> + if (ret)
> + return ret;
> +
> + xa_lock(&attestation_instances);
> + if (attestation_parent_exists(ctlv_device_parent(att->ctlvdev))) {
> + __xa_erase(&attestation_instances, index);
> + ret = -EEXIST;
> + goto out_unlock;
[Severity: Medium]
Does this manual goto mix incorrectly with the scope-based guard(mutex)
declared at the start of this function?
According to the cleanup guidelines, using goto-based error handling in the
same function as scope-based cleanup creates confusing ownership semantics and
increases the risk of bugs.
Could the xa_lock() be converted to use scope-based cleanup (e.g.,
guard(spinlock)), or the goto be eliminated to align with the guidelines?
> + }
> + old = __xa_store(&attestation_instances, index, att, GFP_NOWAIT);
> + ret = xa_err(old);
> + if (!ret) {
> + att->index = index;
> + attestation_next_index++;
> + }
> +out_unlock:
> + xa_unlock(&attestation_instances);
> + return ret;
> +}
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261015145053.3664509-1-jiri@resnulli.us?part=15
^ permalink raw reply [flat|nested] 32+ messages in thread
* [PATCH RFC 16/20] attestation: Prepare measurement register journal
2026-10-15 14:50 [PATCH RFC 00/20] attestation: Add a common subsystem and TDX support Jiri Pirko
` (14 preceding siblings ...)
2026-10-15 14:50 ` [PATCH RFC 15/20] attestation: Prepare provider and instance core Jiri Pirko
@ 2026-10-15 14:50 ` Jiri Pirko
2026-10-15 14:50 ` [PATCH RFC 17/20] attestation: Add CTLV operation handling Jiri Pirko
` (3 subsequent siblings)
19 siblings, 0 replies; 32+ messages in thread
From: Jiri Pirko @ 2026-10-15 14:50 UTC (permalink / raw)
To: linux-coco
Cc: linux-kernel, linux-api, x86, kvm, linux-doc, jgg, david.quigley,
lukas, ksinyuk, rodrigo.vivi, cedric.xing, sameo, drdeeglaze,
James.Bottomley, aik, jpiotrowski, qinkun, pgonda, jic23,
alistair.francis, ankita, suzuki.poulose, kas, rick.p.edgecombe,
elena.reshetova, dave.hansen, sathyanarayanan.kuppuswamy,
peter.fang
From: Jiri Pirko <jiri@nvidia.com>
Add static MR state, append-only INFO and EXTEND records, and journal
notifications.
The journal objects can be compile-checked here. CONFIG_ATTESTATION remains
unwired until the CTLV operation handlers complete the module.
Assisted-by: Cursor: GPT-5.6 Sol
Assisted-by: Cursor: Claude Opus 5.5
Signed-off-by: Jiri Pirko <jiri@nvidia.com>
---
Documentation/driver-api/attestation.rst | 8 +
drivers/attestation/Makefile | 2 +-
drivers/attestation/core/core.c | 76 ++++-
drivers/attestation/core/events.c | 65 ++++
drivers/attestation/core/internal.h | 79 ++++-
drivers/attestation/core/mr.c | 376 +++++++++++++++++++++++
6 files changed, 600 insertions(+), 6 deletions(-)
create mode 100644 drivers/attestation/core/events.c
create mode 100644 drivers/attestation/core/mr.c
diff --git a/Documentation/driver-api/attestation.rst b/Documentation/driver-api/attestation.rst
index 6d0cf9095c66..3cddd853aaa0 100644
--- a/Documentation/driver-api/attestation.rst
+++ b/Documentation/driver-api/attestation.rst
@@ -27,6 +27,14 @@ callback, journal commit and event emission with one instance mutex.
Fixed MRs are listed in the provider descriptor. Their property storage
remains valid until ``ctlv_destroy()`` or failed-creation unwind completes.
+MR journal
+==========
+
+MR extension is transactional. The core allocates the complete journal entry
+and event before invoking ``mr_extend``. A successful callback is followed
+only by infallible publication, so userspace observes the same ordered
+journal.
+
Teardown
========
diff --git a/drivers/attestation/Makefile b/drivers/attestation/Makefile
index 8a75558a7013..e7c9cb2dfec0 100644
--- a/drivers/attestation/Makefile
+++ b/drivers/attestation/Makefile
@@ -1,4 +1,4 @@
# SPDX-License-Identifier: GPL-2.0
obj-$(CONFIG_ATTESTATION) += attestation.o
-attestation-y := core/core.o
+attestation-y := core/core.o core/events.o core/mr.o
diff --git a/drivers/attestation/core/core.c b/drivers/attestation/core/core.c
index e112d3a0ff2b..6db8ed093e93 100644
--- a/drivers/attestation/core/core.c
+++ b/drivers/attestation/core/core.c
@@ -157,6 +157,13 @@ bool attestation_registered(struct attestation *att)
return registered;
}
+void attestation_mr_changed(struct attestation *att)
+{
+ lockdep_assert_held(&att->lock);
+ att->log->generation++;
+ att->mr_read_generation++;
+}
+
void attestation_stop(struct attestation *att)
{
lockdep_assert_held(&att->lock);
@@ -165,6 +172,38 @@ void attestation_stop(struct attestation *att)
xa_unlock(&attestation_instances);
}
+static struct attestation_mr *attestation_mr_alloc(struct attestation *att,
+ const struct attestation_mr_props *props)
+{
+ struct attestation_mr *mr;
+ int ret;
+
+ ret = attestation_mr_props_validate(att->provider, props);
+ if (ret)
+ return ERR_PTR(ret);
+ if (xa_load(&att->mrs, props->id))
+ return ERR_PTR(-EEXIST);
+ mr = kzalloc_obj(*mr, GFP_KERNEL);
+ if (!mr)
+ return ERR_PTR(-ENOMEM);
+ mr->props = props;
+ ret = xa_insert(&att->mrs, props->id, mr, GFP_KERNEL);
+ if (ret) {
+ kfree(mr);
+ return ERR_PTR(ret);
+ }
+ return mr;
+}
+
+struct attestation_mr *attestation_mr_lookup_live(struct attestation *att, unsigned int mr_id)
+{
+ struct attestation_mr *mr;
+
+ lockdep_assert_held(&att->lock);
+ mr = xa_load(&att->mrs, mr_id);
+ return mr ? mr : ERR_PTR(-ENOENT);
+}
+
void attestation_provide_ops(struct attestation *att)
{
const struct attestation_provider *provider = att->provider;
@@ -174,37 +213,66 @@ void attestation_provide_ops(struct attestation *att)
ctlv_attestation_evidence_get_provide(ctlvdev);
ctlv_attestation_evidence_get_enable(ctlvdev);
}
+ if (provider->mrs_count) {
+ ctlv_attestation_mr_get_provide(ctlvdev);
+ ctlv_attestation_mr_dump_provide(ctlvdev);
+ ctlv_attestation_mr_log_get_provide(ctlvdev);
+ if (provider->mr_read)
+ ctlv_attestation_mr_read_provide(ctlvdev);
+ if (provider->mr_extend)
+ ctlv_attestation_mr_extend_provide(ctlvdev);
+ }
}
int attestation_core_init(struct attestation *att, struct ctlv_device *ctlvdev,
const struct attestation_provider *provider)
{
+ size_t index;
int ret;
ret = attestation_provider_validate(provider);
if (ret)
return ret;
mutex_init(&att->lock);
+ xa_init(&att->mrs);
att->ctlvdev = ctlvdev;
att->provider = provider;
+ for (index = 0; index < provider->mrs_count; index++) {
+ struct attestation_mr *mr;
+
+ mr = attestation_mr_alloc(att, &provider->mrs[index]);
+ if (IS_ERR(mr)) {
+ ret = PTR_ERR(mr);
+ goto err_mrs;
+ }
+ }
ret = attestation_registry_insert(att);
if (ret)
- goto err_core;
+ goto err_mrs;
attestation_provide_ops(att);
att->initialized = true;
return 0;
-err_core:
+err_mrs:
attestation_core_fini(att);
return ret;
}
void attestation_core_fini(struct attestation *att)
{
+ struct attestation_mr *mr;
+ unsigned long index;
+
if (!att->ctlvdev)
return;
if (att->initialized)
attestation_registry_remove(att);
+ xa_for_each(&att->mrs, index, mr) {
+ xa_erase(&att->mrs, index);
+ kfree(mr);
+ }
+ attestation_mr_log_free(att->log);
+ xa_destroy(&att->mrs);
mutex_destroy(&att->lock);
att->ctlvdev = NULL;
}
@@ -222,7 +290,11 @@ int ctlv_attestation_init(struct ctlv_device *ctlvdev, const void *init_info, vo
int ctlv_attestation_post_init(struct ctlv_device *ctlvdev, void *priv)
{
struct attestation *att = priv;
+ int ret;
+ ret = attestation_mr_log_init(att);
+ if (ret)
+ return ret;
xa_lock(&attestation_instances);
__xa_set_mark(&attestation_instances, att->index, ATTESTATION_REGISTERED);
xa_unlock(&attestation_instances);
diff --git a/drivers/attestation/core/events.c b/drivers/attestation/core/events.c
new file mode 100644
index 000000000000..2cf8df4b6479
--- /dev/null
+++ b/drivers/attestation/core/events.c
@@ -0,0 +1,65 @@
+// SPDX-License-Identifier: GPL-2.0-only
+#include <linux/err.h>
+
+#include "internal.h"
+
+int attestation_events_prepare(struct attestation *att, struct attestation_event_batch *events)
+{
+ struct attestation_event *event = &events->mr_log;
+
+ lockdep_assert_held(&att->lock);
+ event->ctx = ctlv_attestation_mr_log_entry_appended_create(att->ctlvdev, &event->reply,
+ GFP_KERNEL);
+ if (IS_ERR(event->ctx)) {
+ int ret = PTR_ERR(event->ctx);
+
+ event->ctx = NULL;
+ event->reply = NULL;
+ return ret;
+ }
+ return 0;
+}
+
+void attestation_events_abort(struct attestation_event_batch *events)
+{
+ struct attestation_event *event = &events->mr_log;
+
+ if (!event->ctx)
+ return;
+ ctlv_event_abort(event->ctx);
+ event->ctx = NULL;
+ event->reply = NULL;
+}
+
+static void attestation_event_finish(struct attestation_event *event, int ret)
+{
+ if (ret) {
+ pr_err("attestation event encoding failed: %d\n", ret);
+ ctlv_event_abort(event->ctx);
+ } else {
+ ret = ctlv_event_emit(event->ctx);
+ if (ret)
+ pr_err("attestation event emission failed: %d\n", ret);
+ }
+ event->ctx = NULL;
+ event->reply = NULL;
+}
+
+void attestation_mr_log_event_emit(struct attestation_event_batch *events,
+ const struct attestation_mr_log_entry *entry)
+{
+ struct attestation_event *event = &events->mr_log;
+ struct ctlv_cursor nested;
+ int ret;
+
+ if (!event->ctx)
+ return;
+ if (!ctlv_attestation_notification_entry_start(event->reply, &nested)) {
+ ret = -EMSGSIZE;
+ goto out;
+ }
+ ret = attestation_mr_log_entry_encode(&nested, entry);
+ ctlv_nest_end(&nested);
+out:
+ attestation_event_finish(event, ret);
+}
diff --git a/drivers/attestation/core/internal.h b/drivers/attestation/core/internal.h
index 767c02275d0f..64dca12a3d04 100644
--- a/drivers/attestation/core/internal.h
+++ b/drivers/attestation/core/internal.h
@@ -11,25 +11,98 @@
#define ATTESTATION_REGISTERED XA_MARK_1
+enum attestation_mr_log_entry_type {
+ ATTESTATION_MR_LOG_ENTRY_INFO,
+ ATTESTATION_MR_LOG_ENTRY_EXTEND,
+};
+
+struct attestation_mr_log_entry {
+ unsigned int mr_generation;
+ unsigned int mr_id;
+ enum attestation_mr_log_entry_type type;
+};
+
+struct attestation_mr_log_entry_info {
+ struct attestation_mr_log_entry entry;
+ struct attestation_mr_props props;
+};
+
+struct attestation_mr_log_entry_extend {
+ struct attestation_mr_log_entry entry;
+ struct attestation_digest_set values;
+};
+
+struct attestation_mr_log {
+ unsigned int generation;
+ struct xarray entries;
+};
+
struct attestation {
struct ctlv_device *ctlvdev;
const struct attestation_provider *provider;
unsigned int index;
- /* Mutable instance state. */
+ /* Mutable instance, MR, and journal state. */
struct mutex lock;
+ unsigned int mr_read_generation;
+ struct xarray mrs;
+ struct attestation_mr_log *log;
bool initialized;
};
-int attestation_core_init(struct attestation *att,
- struct ctlv_device *ctlvdev,
+struct attestation_mr {
+ const struct attestation_mr_props *props;
+};
+
+struct attestation_event {
+ struct ctlv_op_ctx *ctx;
+ struct ctlv_cursor *reply;
+};
+
+struct attestation_event_batch {
+ struct attestation_event mr_log;
+};
+
+struct attestation_mr_change {
+ struct attestation_mr *mr;
+ struct attestation_mr_log_entry_extend *entry;
+ struct attestation_event_batch events;
+};
+
+int attestation_core_init(struct attestation *att, struct ctlv_device *ctlvdev,
const struct attestation_provider *provider);
void attestation_core_fini(struct attestation *att);
bool attestation_registered(struct attestation *att);
void attestation_stop(struct attestation *att);
+void attestation_mr_changed(struct attestation *att);
int attestation_provider_validate(const struct attestation_provider *provider);
void attestation_provide_ops(struct attestation *att);
+struct attestation_mr *attestation_mr_lookup_live(struct attestation *att, unsigned int mr_id);
+int attestation_mr_values_validate(const struct attestation_mr *mr,
+ const struct attestation_digest_set *values);
+int attestation_mr_log_init(struct attestation *att);
+struct attestation_mr_log_entry_info *attestation_mr_info_alloc(const struct attestation_mr *mr);
+void attestation_mr_log_entry_free(struct attestation_mr_log_entry *entry);
+void attestation_mr_log_free(struct attestation_mr_log *log);
+void attestation_mr_log_append(struct attestation *att, struct attestation_mr_log_entry *entry);
+struct attestation_mr_change *
+attestation_mr_change_prepare(struct attestation *att, struct attestation_mr *mr,
+ const struct attestation_digest_set *values);
+const struct attestation_mr_log_entry *
+attestation_mr_extend_locked(struct attestation *att, struct attestation_mr_change *change);
+void attestation_mr_change_free(struct attestation_mr_change *change);
+
+int attestation_events_prepare(struct attestation *att, struct attestation_event_batch *events);
+void attestation_events_abort(struct attestation_event_batch *events);
+void attestation_mr_log_event_emit(struct attestation_event_batch *events,
+ const struct attestation_mr_log_entry *entry);
+
+int attestation_mr_log_entry_encode(struct ctlv_cursor *reply,
+ const struct attestation_mr_log_entry *entry);
+int attestation_mr_props_encode(struct ctlv_cursor *reply, const struct attestation_mr *mr,
+ bool include_id);
+
#endif /* _ATTESTATION_INTERNAL_H */
diff --git a/drivers/attestation/core/mr.c b/drivers/attestation/core/mr.c
new file mode 100644
index 000000000000..db80cbd2b582
--- /dev/null
+++ b/drivers/attestation/core/mr.c
@@ -0,0 +1,376 @@
+// SPDX-License-Identifier: GPL-2.0-only
+#include <crypto/hash_info.h>
+#include <linux/cleanup.h>
+#include <linux/err.h>
+#include <linux/slab.h>
+#include <linux/string.h>
+
+#include "internal.h"
+
+int attestation_mr_values_validate(const struct attestation_mr *mr,
+ const struct attestation_digest_set *values)
+{
+ size_t index;
+
+ if (!values || !values->values || values->values_count != mr->props->algorithms_count)
+ return -EINVAL;
+ for (index = 0; index < values->values_count; index++)
+ if (values->values[index].algorithm != mr->props->algorithms[index])
+ return -EINVAL;
+ return 0;
+}
+
+static int attestation_digest_set_copy(struct attestation_digest_set *dst,
+ const struct attestation_digest_set *src)
+{
+ size_t index;
+
+ if (!src || !src->values || !src->values_count)
+ return -EINVAL;
+ for (index = 0; index < src->values_count; index++) {
+ enum hash_algo algorithm = src->values[index].algorithm;
+
+ if (algorithm >= HASH_ALGO__LAST || !hash_digest_size[algorithm] ||
+ (index && src->values[index - 1].algorithm >= algorithm))
+ return -EINVAL;
+ }
+ dst->values =
+ kmemdup_array(src->values, src->values_count, sizeof(*src->values), GFP_KERNEL);
+ if (!dst->values)
+ return -ENOMEM;
+ dst->values_count = src->values_count;
+ return 0;
+}
+
+static void attestation_digest_set_free(struct attestation_digest_set *set)
+{
+ kfree(set->values);
+ set->values = NULL;
+ set->values_count = 0;
+}
+
+struct attestation_mr_log_entry_info *attestation_mr_info_alloc(const struct attestation_mr *mr)
+{
+ struct attestation_mr_log_entry_info *info;
+ enum hash_algo *algorithms;
+ char *name;
+
+ info = kzalloc_obj(*info, GFP_KERNEL);
+ if (!info)
+ return ERR_PTR(-ENOMEM);
+ name = kstrdup(mr->props->name, GFP_KERNEL);
+ if (!name)
+ goto err_info;
+ algorithms = kmemdup_array(mr->props->algorithms, mr->props->algorithms_count,
+ sizeof(*algorithms), GFP_KERNEL);
+ if (!algorithms)
+ goto err_name;
+ info->entry.type = ATTESTATION_MR_LOG_ENTRY_INFO;
+ info->entry.mr_id = mr->props->id;
+ info->props = *mr->props;
+ info->props.name = name;
+ info->props.algorithms = algorithms;
+ return info;
+
+err_name:
+ kfree(name);
+err_info:
+ kfree(info);
+ return ERR_PTR(-ENOMEM);
+}
+
+void attestation_mr_log_entry_free(struct attestation_mr_log_entry *entry)
+{
+ if (!entry)
+ return;
+ switch (entry->type) {
+ case ATTESTATION_MR_LOG_ENTRY_INFO: {
+ struct attestation_mr_log_entry_info *info =
+ container_of(entry, typeof(*info), entry);
+
+ kfree(info->props.algorithms);
+ kfree(info->props.name);
+ kfree(info);
+ break;
+ }
+ case ATTESTATION_MR_LOG_ENTRY_EXTEND: {
+ struct attestation_mr_log_entry_extend *extend =
+ container_of(entry, typeof(*extend), entry);
+
+ attestation_digest_set_free(&extend->values);
+ kfree(extend);
+ break;
+ }
+ }
+}
+
+void attestation_mr_log_free(struct attestation_mr_log *log)
+{
+ struct attestation_mr_log_entry *entry;
+ unsigned long index;
+
+ if (!log)
+ return;
+ xa_for_each(&log->entries, index, entry) {
+ xa_erase(&log->entries, index);
+ attestation_mr_log_entry_free(entry);
+ }
+ xa_destroy(&log->entries);
+ kfree(log);
+}
+
+void attestation_mr_log_append(struct attestation *att, struct attestation_mr_log_entry *entry)
+{
+ struct attestation_mr_log *log = att->log;
+
+ xa_store(&log->entries, entry->mr_generation, entry, GFP_NOWAIT);
+}
+
+int attestation_mr_log_init(struct attestation *att)
+{
+ struct attestation_mr_log_entry_info **infos;
+ struct attestation_mr_log *log;
+ struct attestation_mr *mr;
+ unsigned long mr_id;
+ size_t count = 0;
+ size_t index = 0;
+ int ret = -ENOMEM;
+
+ xa_for_each(&att->mrs, mr_id, mr)
+ count++;
+ log = kzalloc_obj(*log, GFP_KERNEL);
+ if (!log)
+ return -ENOMEM;
+ xa_init(&log->entries);
+ infos = kcalloc(count, sizeof(*infos), GFP_KERNEL);
+ if (count && !infos)
+ goto err_log;
+ xa_for_each(&att->mrs, mr_id, mr) {
+ infos[index] = attestation_mr_info_alloc(mr);
+ if (IS_ERR(infos[index])) {
+ ret = PTR_ERR(infos[index]);
+ infos[index] = NULL;
+ goto err_infos;
+ }
+ index++;
+ }
+
+ att->log = log;
+ index = 0;
+ xa_for_each(&att->mrs, mr_id, mr) {
+ struct attestation_mr_log_entry *entry = &infos[index]->entry;
+
+ if (log->generation == UINT_MAX) {
+ ret = -EOVERFLOW;
+ goto err_attached;
+ }
+ entry->mr_generation = ++log->generation;
+ ret = xa_insert(&log->entries, entry->mr_generation, entry, GFP_KERNEL);
+ if (ret)
+ goto err_attached;
+ infos[index++] = NULL;
+ }
+ kfree(infos);
+ return 0;
+
+err_attached:
+ att->log = NULL;
+ attestation_mr_log_free(log);
+ log = NULL;
+err_infos:
+ for (index = 0; index < count; index++)
+ if (infos[index])
+ attestation_mr_log_entry_free(&infos[index]->entry);
+ kfree(infos);
+err_log:
+ attestation_mr_log_free(log);
+ return ret;
+}
+
+static struct attestation_mr_log_entry_extend *
+attestation_extend_alloc(enum attestation_mr_log_entry_type type, size_t size,
+ struct attestation_mr *mr, const struct attestation_digest_set *values)
+{
+ struct attestation_mr_log_entry_extend *extend;
+ int ret;
+
+ extend = kzalloc(size, GFP_KERNEL);
+ if (!extend)
+ return ERR_PTR(-ENOMEM);
+ extend->entry.type = type;
+ extend->entry.mr_id = mr->props->id;
+ ret = attestation_digest_set_copy(&extend->values, values);
+ if (ret) {
+ kfree(extend);
+ return ERR_PTR(ret);
+ }
+ return extend;
+}
+
+struct attestation_mr_change *
+attestation_mr_change_prepare(struct attestation *att, struct attestation_mr *mr,
+ const struct attestation_digest_set *values)
+{
+ struct attestation_mr_change *change;
+ int ret;
+
+ lockdep_assert_held(&att->lock);
+ change = kzalloc_obj(*change, GFP_KERNEL);
+ if (!change)
+ return ERR_PTR(-ENOMEM);
+ change->mr = mr;
+ change->entry = attestation_extend_alloc(ATTESTATION_MR_LOG_ENTRY_EXTEND,
+ sizeof(*change->entry), mr, values);
+ if (IS_ERR(change->entry)) {
+ ret = PTR_ERR(change->entry);
+ goto err_change;
+ }
+ ret = attestation_events_prepare(att, &change->events);
+ if (ret)
+ goto err_entry;
+ return change;
+
+err_entry:
+ attestation_mr_log_entry_free(&change->entry->entry);
+err_change:
+ kfree(change);
+ return ERR_PTR(ret);
+}
+
+const struct attestation_mr_log_entry *
+attestation_mr_extend_locked(struct attestation *att, struct attestation_mr_change *change)
+{
+ struct attestation_mr_log_entry_extend *extend = change->entry;
+ struct attestation_mr_log_entry *entry = &extend->entry;
+ int ret;
+
+ lockdep_assert_held(&att->lock);
+ if (att->log->generation == UINT_MAX) {
+ attestation_stop(att);
+ return ERR_PTR(-EOVERFLOW);
+ }
+ entry->mr_generation = att->log->generation + 1;
+ ret = xa_reserve(&att->log->entries, entry->mr_generation, GFP_KERNEL);
+ if (ret)
+ return ERR_PTR(ret);
+ ret = att->provider->mr_extend(att, change->mr->props->id, &extend->values);
+ if (ret) {
+ xa_release(&att->log->entries, entry->mr_generation);
+ return ERR_PTR(ret);
+ }
+ attestation_mr_changed(att);
+ attestation_mr_log_append(att, entry);
+ attestation_mr_log_event_emit(&change->events, entry);
+ change->entry = NULL;
+ return entry;
+}
+
+void attestation_mr_change_free(struct attestation_mr_change *change)
+{
+ if (!change)
+ return;
+ attestation_events_abort(&change->events);
+ if (change->entry)
+ attestation_mr_log_entry_free(&change->entry->entry);
+ kfree(change);
+}
+
+int attestation_mr_props_encode(struct ctlv_cursor *reply, const struct attestation_mr *mr,
+ bool include_id)
+{
+ u64 caps = 0;
+ size_t index;
+ bool ok = true;
+
+ if (include_id)
+ ok &= ctlv_attestation_mr_id_put(reply, mr->props->id);
+ ok &= ctlv_attestation_mr_name_put(reply, mr->props->name);
+ for (index = 0; index < mr->props->algorithms_count; index++)
+ ok &= ctlv_attestation_mr_supported_hash_algo_put(reply,
+ mr->props->algorithms[index]);
+ if (mr->props->has_register_index)
+ ok &= ctlv_attestation_mr_register_index_put(reply, mr->props->register_index);
+ if (mr->props->readable)
+ caps |= CTLV_ATTESTATION_MR_CAPS_READ;
+ if (mr->props->extendable)
+ caps |= CTLV_ATTESTATION_MR_CAPS_EXTEND;
+ ok &= ctlv_attestation_mr_caps_put(reply, caps);
+ return ok ? 0 : -EMSGSIZE;
+}
+
+static int attestation_values_encode(struct ctlv_cursor *reply,
+ const struct attestation_digest_set *values)
+{
+ struct ctlv_cursor nested;
+ size_t index;
+ bool ok = true;
+
+ for (index = 0; index < values->values_count; index++) {
+ const struct attestation_digest *value = &values->values[index];
+
+ if (!ctlv_attestation_mr_value_start(reply, &nested))
+ return -EMSGSIZE;
+ ok &= ctlv_attestation_mr_hash_algo_put(&nested, value->algorithm);
+ ok &= ctlv_attestation_mr_data_put(&nested, value->data,
+ hash_digest_size[value->algorithm]);
+ ctlv_nest_end(&nested);
+ }
+ return ok ? 0 : -EMSGSIZE;
+}
+
+static int attestation_mr_info_encode(struct ctlv_cursor *reply,
+ const struct attestation_mr_props *props)
+{
+ u64 caps = 0;
+ size_t index;
+ bool ok;
+
+ ok = ctlv_attestation_mr_name_put(reply, props->name);
+ for (index = 0; index < props->algorithms_count; index++)
+ ok &= ctlv_attestation_mr_supported_hash_algo_put(reply,
+ props->algorithms[index]);
+ if (props->has_register_index)
+ ok &= ctlv_attestation_mr_register_index_put(reply, props->register_index);
+ if (props->readable)
+ caps |= CTLV_ATTESTATION_MR_CAPS_READ;
+ if (props->extendable)
+ caps |= CTLV_ATTESTATION_MR_CAPS_EXTEND;
+ ok &= ctlv_attestation_mr_caps_put(reply, caps);
+ return ok ? 0 : -EMSGSIZE;
+}
+
+int attestation_mr_log_entry_encode(struct ctlv_cursor *reply,
+ const struct attestation_mr_log_entry *entry)
+{
+ struct ctlv_cursor nested;
+ bool ok;
+ int ret;
+
+ ok = ctlv_attestation_mr_generation_put(reply, entry->mr_generation);
+ ok &= ctlv_attestation_mr_id_put(reply, entry->mr_id);
+ ok &= ctlv_attestation_log_event_put(reply, entry->type == ATTESTATION_MR_LOG_ENTRY_INFO ?
+ CTLV_ATTESTATION_MR_LOG_EVENT_INFO :
+ CTLV_ATTESTATION_MR_LOG_EVENT_EXTEND);
+ if (!ok)
+ return -EMSGSIZE;
+
+ if (entry->type == ATTESTATION_MR_LOG_ENTRY_INFO) {
+ const struct attestation_mr_log_entry_info *info =
+ container_of(entry, typeof(*info), entry);
+
+ if (!ctlv_attestation_log_mr_start(reply, &nested))
+ return -EMSGSIZE;
+ ret = attestation_mr_info_encode(&nested, &info->props);
+ ctlv_nest_end(&nested);
+ return ret;
+ }
+
+ ret = attestation_values_encode(reply,
+ &container_of(entry,
+ const struct attestation_mr_log_entry_extend,
+ entry)
+ ->values);
+ if (ret)
+ return ret;
+ return 0;
+}
--
2.56.0
^ permalink raw reply related [flat|nested] 32+ messages in thread* [PATCH RFC 17/20] attestation: Add CTLV operation handling
2026-10-15 14:50 [PATCH RFC 00/20] attestation: Add a common subsystem and TDX support Jiri Pirko
` (15 preceding siblings ...)
2026-10-15 14:50 ` [PATCH RFC 16/20] attestation: Prepare measurement register journal Jiri Pirko
@ 2026-10-15 14:50 ` Jiri Pirko
2026-10-15 15:33 ` sashiko-bot
2026-10-15 14:50 ` [PATCH RFC 18/20] tdx-guest: Expose the native attestation service API Jiri Pirko
` (2 subsequent siblings)
19 siblings, 1 reply; 32+ messages in thread
From: Jiri Pirko @ 2026-10-15 14:50 UTC (permalink / raw)
To: linux-coco
Cc: linux-kernel, linux-api, x86, kvm, linux-doc, jgg, david.quigley,
lukas, ksinyuk, rodrigo.vivi, cedric.xing, sameo, drdeeglaze,
James.Bottomley, aik, jpiotrowski, qinkun, pgonda, jic23,
alistair.francis, ankita, suzuki.poulose, kas, rick.p.edgecombe,
elena.reshetova, dave.hansen, sathyanarayanan.kuppuswamy,
peter.fang
From: Jiri Pirko <jiri@nvidia.com>
Translate CTLV evidence, MR and journal operations to the provider core and
wire the attestation family into the kernel build.
Assisted-by: Cursor: GPT-5.6 Sol
Assisted-by: Cursor: Claude Opus 5.5
Signed-off-by: Jiri Pirko <jiri@nvidia.com>
---
Documentation/driver-api/attestation.rst | 4 +
drivers/Kconfig | 2 +
drivers/Makefile | 1 +
drivers/attestation/Makefile | 3 +-
drivers/attestation/core/core.c | 2 +
drivers/attestation/core/ctlv.c | 520 +++++++++++++++++++++++
6 files changed, 531 insertions(+), 1 deletion(-)
create mode 100644 drivers/attestation/core/ctlv.c
diff --git a/Documentation/driver-api/attestation.rst b/Documentation/driver-api/attestation.rst
index 3cddd853aaa0..5ce121ded05f 100644
--- a/Documentation/driver-api/attestation.rst
+++ b/Documentation/driver-api/attestation.rst
@@ -35,6 +35,10 @@ and event before invoking ``mr_extend``. A successful callback is followed
only by infallible publication, so userspace observes the same ordered
journal.
+``MR_READ`` uses ``mr_read_generation`` only to validate query continuation;
+the returned ``mr.generation`` is the journal boundary at which the values
+were read.
+
Teardown
========
diff --git a/drivers/Kconfig b/drivers/Kconfig
index 9acc7f74c5c8..971db1f0192c 100644
--- a/drivers/Kconfig
+++ b/drivers/Kconfig
@@ -27,6 +27,8 @@ source "drivers/connector/Kconfig"
source "drivers/ctlv/Kconfig"
+source "drivers/attestation/Kconfig"
+
source "drivers/firmware/Kconfig"
source "drivers/fwctl/Kconfig"
diff --git a/drivers/Makefile b/drivers/Makefile
index f2c0c3a367ea..2b4dfc8e256b 100644
--- a/drivers/Makefile
+++ b/drivers/Makefile
@@ -136,6 +136,7 @@ obj-$(CONFIG_INFINIBAND) += infiniband/
obj-y += firmware/
obj-$(CONFIG_FWCTL) += fwctl/
obj-$(CONFIG_CTLV) += ctlv/
+obj-$(CONFIG_ATTESTATION) += attestation/
obj-$(CONFIG_CRYPTO) += crypto/
obj-$(CONFIG_SUPERH) += sh/
obj-y += clocksource/
diff --git a/drivers/attestation/Makefile b/drivers/attestation/Makefile
index e7c9cb2dfec0..e520fee2e90d 100644
--- a/drivers/attestation/Makefile
+++ b/drivers/attestation/Makefile
@@ -1,4 +1,5 @@
# SPDX-License-Identifier: GPL-2.0
obj-$(CONFIG_ATTESTATION) += attestation.o
-attestation-y := core/core.o core/events.o core/mr.o
+attestation-y := core/core.o core/ctlv.o core/events.o core/mr.o \
+ generated/ctlv_attestation_gen.o
diff --git a/drivers/attestation/core/core.c b/drivers/attestation/core/core.c
index 6db8ed093e93..3fb58f344663 100644
--- a/drivers/attestation/core/core.c
+++ b/drivers/attestation/core/core.c
@@ -326,6 +326,7 @@ int ctlv_attestation_op_pre(void *priv, struct ctlv_op_ctx *ctx)
}
return 0;
}
+EXPORT_SYMBOL_NS_GPL(ctlv_attestation_op_pre, "ATTESTATION");
void ctlv_attestation_op_post(void *priv, struct ctlv_op_ctx *ctx)
{
@@ -333,6 +334,7 @@ void ctlv_attestation_op_post(void *priv, struct ctlv_op_ctx *ctx)
mutex_unlock(&att->lock);
}
+EXPORT_SYMBOL_NS_GPL(ctlv_attestation_op_post, "ATTESTATION");
static int __init attestation_module_init(void)
{
diff --git a/drivers/attestation/core/ctlv.c b/drivers/attestation/core/ctlv.c
new file mode 100644
index 000000000000..ff19400843c2
--- /dev/null
+++ b/drivers/attestation/core/ctlv.c
@@ -0,0 +1,520 @@
+// SPDX-License-Identifier: GPL-2.0-only
+#include <crypto/hash_info.h>
+#include <linux/cleanup.h>
+#include <linux/err.h>
+#include <linux/limits.h>
+#include <linux/slab.h>
+#include <linux/sort.h>
+#include <linux/string.h>
+
+#include "internal.h"
+
+static const struct attestation_evidence_supported_object *
+attestation_evidence_object_find(const struct attestation_evidence_caps *caps, const char *format)
+{
+ size_t index;
+
+ for (index = 0; index < caps->objects_count; index++) {
+ const struct attestation_evidence_supported_object *object = &caps->objects[index];
+
+ if (!strcmp(object->format, format))
+ return object;
+ }
+ return NULL;
+}
+
+static int attestation_device_caps_put(struct attestation *att, struct ctlv_cursor *reply)
+{
+ const struct attestation_provider *provider = att->provider;
+ struct ctlv_cursor caps_nest, group, object;
+ bool ok = true;
+ size_t index;
+ u64 flags;
+
+ if (!ctlv_attestation_device_caps_start(reply, &caps_nest))
+ return -EMSGSIZE;
+ if (provider->evidence_caps) {
+ const struct attestation_evidence_caps *caps = provider->evidence_caps;
+ u64 blob_class = CTLV_ATTESTATION_EVIDENCE_BLOB_CLASS_PRIMARY;
+ u64 representation = CTLV_ATTESTATION_EVIDENCE_REPRESENTATION_RAW;
+ u64 max_output_length = caps->max_total_output_length;
+
+ if (!ctlv_attestation_cap_evidence_start(&caps_nest, &group))
+ return -EMSGSIZE;
+ ok &= ctlv_attestation_cap_min_challenge_length_put(&group,
+ caps->min_challenge_length);
+ ok &= ctlv_attestation_cap_max_challenge_length_put(&group,
+ caps->max_challenge_length);
+ ok &= ctlv_attestation_cap_max_total_output_length_put(&group, max_output_length);
+ for (index = 0; index < caps->objects_count; index++) {
+ const struct attestation_evidence_supported_object *cap =
+ &caps->objects[index];
+
+ if (!ctlv_attestation_cap_object_start(&group, &object)) {
+ ok = false;
+ break;
+ }
+ ok &= ctlv_attestation_cap_blob_class_put(&object, blob_class);
+ ok &= ctlv_attestation_cap_format_put(&object, cap->format);
+ ok &= ctlv_attestation_cap_representation_put(&object, representation);
+ ok &= ctlv_attestation_cap_max_length_put(&object, cap->max_length);
+ ctlv_nest_end(&object);
+ }
+ ctlv_nest_end(&group);
+ }
+ if (provider->mrs_count) {
+ if (!ctlv_attestation_cap_mr_start(&caps_nest, &group))
+ return -EMSGSIZE;
+ flags = 0;
+ if (provider->mr_read)
+ flags |= CTLV_ATTESTATION_MR_DEVICE_CAPS_READ;
+ if (provider->mr_extend)
+ flags |= CTLV_ATTESTATION_MR_DEVICE_CAPS_EXTEND;
+ ok &= ctlv_attestation_cap_mr_flags_put(&group, flags);
+ ctlv_nest_end(&group);
+ }
+ ctlv_nest_end(&caps_nest);
+ return ok ? 0 : -EMSGSIZE;
+}
+
+int ctlv_attestation_device_get_action(void *priv, struct ctlv_op_ctx *ctx,
+ const struct ctlv_cursor *request, struct ctlv_cursor *reply)
+{
+ struct attestation *att = priv;
+ struct ctlv_cursor info;
+ int ret;
+
+ ret = attestation_device_caps_put(att, reply);
+ if (ret)
+ return ret;
+ if (!ctlv_attestation_device_info_start(reply, &info))
+ return -EMSGSIZE;
+ ret = ctlv_stub_fill(ctx, request, &info);
+ ctlv_nest_end(&info);
+ return ret;
+}
+
+static int
+attestation_evidence_decode_objects(struct attestation *att, struct ctlv_op_ctx *ctx,
+ const struct ctlv_cursor *request,
+ struct attestation_evidence_request *decoded,
+ struct attestation_evidence_object_request **objects_out)
+{
+ const struct attestation_evidence_caps *caps = att->provider->evidence_caps;
+ struct attestation_evidence_object_request *objects;
+ struct ctlv_cursor object;
+ size_t count = 0, index = 0, previous;
+
+ ctlv_attestation_evidence_get_evidence_object_for_each(&object, request) count++;
+ objects = kcalloc(count, sizeof(*objects), GFP_KERNEL);
+ if (!objects)
+ return -ENOMEM;
+ ctlv_attestation_evidence_get_evidence_object_for_each(&object, request)
+ {
+ const struct attestation_evidence_supported_object *supported;
+ const char *format;
+ size_t total = 0;
+
+ format = ctlv_attestation_evidence_get_object_evidence_format_get(&object);
+ supported = attestation_evidence_object_find(caps, format);
+ if (!supported) {
+ kfree(objects);
+ return -EOPNOTSUPP;
+ }
+ for (previous = 0; previous < index; previous++)
+ if (objects[previous].supported_object == supported) {
+ kfree(objects);
+ return -EINVAL;
+ }
+ objects[index].supported_object = supported;
+ objects[index].output =
+ ctlv_attestation_evidence_get_object_evidence_data_resolve(ctx, &object);
+ if (IS_ERR(objects[index].output)) {
+ int ret = PTR_ERR(objects[index].output);
+
+ objects[index].output = NULL;
+ while (index)
+ ctlv_blob_put(objects[--index].output);
+ kfree(objects);
+ return ret;
+ }
+ if (ctlv_blob_len(objects[index].output) < supported->max_length) {
+ for (previous = 0; previous <= index; previous++)
+ ctlv_blob_put(objects[previous].output);
+ kfree(objects);
+ return -EMSGSIZE;
+ }
+ for (previous = 0; previous <= index; previous++) {
+ if (check_add_overflow(total, ctlv_blob_len(objects[previous].output),
+ &total) ||
+ total > caps->max_total_output_length) {
+ for (previous = 0; previous <= index; previous++)
+ ctlv_blob_put(objects[previous].output);
+ kfree(objects);
+ return -E2BIG;
+ }
+ }
+ index++;
+ }
+ decoded->objects = objects;
+ decoded->objects_count = count;
+ *objects_out = objects;
+ return 0;
+}
+
+static int attestation_evidence_request_decode(struct attestation *att, struct ctlv_op_ctx *ctx,
+ const struct ctlv_cursor *request,
+ struct attestation_evidence_request *decoded,
+ struct attestation_evidence_object_request **objects)
+{
+ const struct attestation_evidence_caps *caps = att->provider->evidence_caps;
+ u32 challenge_length;
+
+ decoded->challenge =
+ ctlv_attestation_evidence_get_evidence_challenge_get(request, &challenge_length);
+ decoded->challenge_length = challenge_length;
+ if (challenge_length < caps->min_challenge_length ||
+ challenge_length > caps->max_challenge_length)
+ return -EINVAL;
+ return attestation_evidence_decode_objects(att, ctx, request, decoded, objects);
+}
+
+static void attestation_evidence_request_free(struct attestation_evidence_object_request *objects,
+ size_t count)
+{
+ while (count) {
+ count--;
+ if (objects[count].output)
+ ctlv_blob_put(objects[count].output);
+ }
+ kfree(objects);
+}
+
+int ctlv_attestation_evidence_get_action(void *priv, struct ctlv_op_ctx *ctx,
+ const struct ctlv_cursor *request,
+ struct ctlv_cursor *reply)
+{
+ struct attestation_evidence_object_request *objects = NULL;
+ struct attestation_evidence_request decoded = {};
+ struct attestation_evidence_result result = {};
+ struct attestation *att = priv;
+ bool mr_dependent = false;
+ size_t index;
+ int put_ret;
+ int ret;
+
+ ret = attestation_evidence_request_decode(att, ctx, request, &decoded, &objects);
+ if (ret)
+ goto out;
+ result.object_lengths =
+ kcalloc(decoded.objects_count, sizeof(*result.object_lengths), GFP_KERNEL);
+ if (!result.object_lengths) {
+ ret = -ENOMEM;
+ goto out;
+ }
+ for (index = 0; index < decoded.objects_count; index++)
+ mr_dependent |= decoded.objects[index].supported_object->binds_mrs;
+ ret = att->provider->evidence_get(att, &decoded, &result);
+ if (ret)
+ goto out;
+ for (index = 0; index < decoded.objects_count; index++) {
+ if (result.object_lengths[index] >
+ decoded.objects[index].supported_object->max_length ||
+ result.object_lengths[index] > ctlv_blob_len(decoded.objects[index].output)) {
+ ret = -E2BIG;
+ goto out;
+ }
+ }
+ for (index = 0; index < decoded.objects_count; index++) {
+ put_ret = ctlv_blob_put(decoded.objects[index].output);
+ objects[index].output = NULL;
+ if (!ret)
+ ret = put_ret;
+ }
+ if (ret)
+ goto out;
+ if (mr_dependent && !ctlv_attestation_mr_generation_put(reply, att->log->generation)) {
+ ret = -EMSGSIZE;
+ goto out;
+ }
+ for (index = 0; index < decoded.objects_count; index++)
+ if (!ctlv_attestation_evidence_object_length_put(reply,
+ result.object_lengths[index])) {
+ ret = -EMSGSIZE;
+ goto out;
+ }
+out:
+ kfree(result.object_lengths);
+ attestation_evidence_request_free(objects, decoded.objects_count);
+ return ret;
+}
+
+int ctlv_attestation_mr_get_action(void *priv, struct ctlv_op_ctx *ctx,
+ const struct ctlv_cursor *request, struct ctlv_cursor *reply)
+{
+ struct attestation *att = priv;
+ struct attestation_mr *mr;
+ u64 id = ctlv_attestation_mr_get_mr_id_get(request);
+
+ if (id > UINT_MAX)
+ return -ERANGE;
+ mr = attestation_mr_lookup_live(att, id);
+ return IS_ERR(mr) ? PTR_ERR(mr) : attestation_mr_props_encode(reply, mr, false);
+}
+
+int ctlv_attestation_mr_dump_query(void *priv, struct ctlv_op_ctx *ctx,
+ const struct ctlv_cursor *request, struct ctlv_cursor *reply)
+{
+ struct attestation *att = priv;
+ struct attestation_mr *mr;
+ struct ctlv_cursor nested;
+ unsigned long id;
+ u64 cursor = ctlv_op_query_cursor(ctx);
+ unsigned int generation = att->log->generation;
+ int ret;
+
+ if (cursor && ctlv_op_query_generation(ctx) != generation)
+ return -ESTALE;
+ id = cursor ? cursor - 1 : 0;
+ mr = xa_find(&att->mrs, &id, UINT_MAX, XA_PRESENT);
+ while (mr) {
+ if (!ctlv_attestation_mr_item_start(reply, &nested))
+ return ctlv_op_query_more(ctx, generation, id + 1);
+ ret = attestation_mr_props_encode(&nested, mr, true);
+ ctlv_nest_end(&nested);
+ if (ret)
+ return ret;
+ mr = xa_find_after(&att->mrs, &id, UINT_MAX, XA_PRESENT);
+ }
+ return ctlv_op_query_done(ctx, generation);
+}
+
+static int attestation_mr_values_put(struct ctlv_cursor *reply,
+ const struct attestation_digest_set *values)
+{
+ struct ctlv_cursor nested;
+ size_t index;
+ bool ok;
+
+ for (index = 0; index < values->values_count; index++) {
+ const struct attestation_digest *value = &values->values[index];
+ enum hash_algo algorithm = value->algorithm;
+ size_t length = hash_digest_size[algorithm];
+
+ if (!ctlv_attestation_mr_value_start(reply, &nested))
+ return -EMSGSIZE;
+ ok = ctlv_attestation_mr_hash_algo_put(&nested, algorithm);
+ ok &= ctlv_attestation_mr_data_put(&nested, value->data, length);
+ ctlv_nest_end(&nested);
+ if (!ok)
+ return -EMSGSIZE;
+ }
+ return 0;
+}
+
+int ctlv_attestation_mr_read_query(void *priv, struct ctlv_op_ctx *ctx,
+ const struct ctlv_cursor *request, struct ctlv_cursor *reply)
+{
+ struct attestation_digest_set values = {};
+ struct attestation *att = priv;
+ struct attestation_mr *mr;
+ u64 id;
+ int ret;
+
+ if (ctlv_op_query_cursor(ctx) &&
+ ctlv_op_query_generation(ctx) != att->mr_read_generation)
+ return -ESTALE;
+ id = ctlv_attestation_mr_read_mr_id_get(request);
+ if (id > UINT_MAX)
+ return -ERANGE;
+ mr = attestation_mr_lookup_live(att, id);
+ if (IS_ERR(mr))
+ return PTR_ERR(mr);
+ if (!mr->props->readable)
+ return -EOPNOTSUPP;
+ values.values_count = mr->props->algorithms_count;
+ values.values = kcalloc(values.values_count, sizeof(*values.values), GFP_KERNEL);
+ if (!values.values)
+ return -ENOMEM;
+ for (id = 0; id < values.values_count; id++)
+ values.values[id].algorithm = mr->props->algorithms[id];
+ ret = att->provider->mr_read(att, mr->props->id, &values);
+ if (!ret)
+ ret = attestation_mr_values_validate(mr, &values);
+ if (!ret && !ctlv_attestation_mr_generation_put(reply, att->log->generation))
+ ret = -EMSGSIZE;
+ if (!ret)
+ ret = attestation_mr_values_put(reply, &values);
+ kfree(values.values);
+ if (ret)
+ return ret;
+ return ctlv_op_query_done(ctx, att->mr_read_generation);
+}
+
+static int attestation_digest_compare(const void *left, const void *right)
+{
+ const struct attestation_digest *a = left;
+ const struct attestation_digest *b = right;
+
+ return (int)a->algorithm - (int)b->algorithm;
+}
+
+static int attestation_mr_extend_decode(const struct ctlv_cursor *request,
+ struct attestation_digest_set *values)
+{
+ struct ctlv_cursor item;
+ size_t count = 0, index = 0;
+
+ ctlv_attestation_mr_extend_mr_value_for_each(&item, request) count++;
+ values->values = kcalloc(count, sizeof(*values->values), GFP_KERNEL);
+ if (!values->values)
+ return -ENOMEM;
+ values->values_count = count;
+ ctlv_attestation_mr_extend_mr_value_for_each(&item, request)
+ {
+ struct attestation_digest *value = &values->values[index++];
+ u32 length;
+ const void *data;
+ u64 algorithm;
+
+ algorithm =
+ ctlv_attestation_mr_extend_value_mr_hash_algo_get(&item);
+ if (algorithm >= HASH_ALGO__LAST || !hash_digest_size[algorithm])
+ return -EINVAL;
+ data = ctlv_attestation_mr_extend_value_mr_data_get(&item, &length);
+ if (length != hash_digest_size[algorithm])
+ return -EINVAL;
+ value->algorithm = algorithm;
+ memcpy(value->data, data, length);
+ }
+ sort(values->values, values->values_count, sizeof(*values->values),
+ attestation_digest_compare, NULL);
+ return 0;
+}
+
+int ctlv_attestation_mr_extend_action(void *priv, struct ctlv_op_ctx *ctx,
+ const struct ctlv_cursor *request, struct ctlv_cursor *reply)
+{
+ struct attestation_digest_set values = {};
+ struct attestation_mr_change *change;
+ const struct attestation_mr_log_entry *entry;
+ struct attestation *att = priv;
+ struct attestation_mr *mr;
+ u64 id;
+ int ret;
+
+ id = ctlv_attestation_mr_extend_mr_id_get(request);
+ if (id > UINT_MAX)
+ return -ERANGE;
+ mr = attestation_mr_lookup_live(att, id);
+ if (IS_ERR(mr))
+ return PTR_ERR(mr);
+ if (!mr->props->extendable)
+ return -EOPNOTSUPP;
+ ret = attestation_mr_extend_decode(request, &values);
+ if (ret)
+ goto out_values;
+ ret = attestation_mr_values_validate(mr, &values);
+ if (ret)
+ goto out_values;
+ change = attestation_mr_change_prepare(att, mr, &values);
+ if (IS_ERR(change)) {
+ ret = PTR_ERR(change);
+ goto out_values;
+ }
+ entry = attestation_mr_extend_locked(att, change);
+ if (IS_ERR(entry))
+ ret = PTR_ERR(entry);
+ else if (!ctlv_attestation_mr_generation_put(reply, entry->mr_generation))
+ ret = -EMSGSIZE;
+ attestation_mr_change_free(change);
+out_values:
+ kfree(values.values);
+ return ret;
+}
+
+static bool attestation_log_selected(const unsigned int *ids, size_t count, unsigned int id)
+{
+ size_t index;
+
+ if (!count)
+ return true;
+ for (index = 0; index < count; index++)
+ if (ids[index] == id)
+ return true;
+ return false;
+}
+
+int ctlv_attestation_mr_log_get_query(void *priv, struct ctlv_op_ctx *ctx,
+ const struct ctlv_cursor *request, struct ctlv_cursor *reply)
+{
+ struct attestation *att = priv;
+ struct attestation_mr_log_entry *entry;
+ struct ctlv_cursor item, nested;
+ unsigned int ids[256];
+ size_t ids_count = 0, index;
+ unsigned int start, end;
+ unsigned long xa_index;
+ u64 boundary;
+ u64 cursor = ctlv_op_query_cursor(ctx);
+ int ret;
+
+ ctlv_attestation_mr_log_get_mr_selection_for_each(&item, request)
+ {
+ u64 id = ctlv_attestation_mr_log_get_selection_mr_id_get(&item);
+
+ if (id > UINT_MAX)
+ return -ERANGE;
+ for (index = 0; index < ids_count; index++)
+ if (ids[index] == id)
+ return -EINVAL;
+ ids[ids_count++] = id;
+ }
+ if (cursor)
+ boundary = ctlv_op_query_generation(ctx);
+ else
+ boundary = att->log->generation;
+ if (boundary > UINT_MAX || boundary > att->log->generation)
+ return -ESTALE;
+ end = boundary;
+ if (ctlv_attestation_mr_log_get_mr_end_generation_present(request)) {
+ u64 requested_end = ctlv_attestation_mr_log_get_mr_end_generation_get(request);
+
+ end = min_t(u64, end, requested_end);
+ }
+ start = 1;
+ if (ctlv_attestation_mr_log_get_mr_start_generation_present(request)) {
+ u64 value = ctlv_attestation_mr_log_get_mr_start_generation_get(request);
+
+ if (value > UINT_MAX)
+ return -EINVAL;
+ start = value;
+ }
+ if (att->log->generation && start > end)
+ return -EINVAL;
+ if (!cursor && att->log->generation) {
+ if (!ctlv_attestation_mr_start_generation_put(reply, start) ||
+ !ctlv_attestation_mr_end_generation_put(reply, end))
+ return -EMSGSIZE;
+ }
+ xa_index = cursor ? cursor : start;
+ entry = xa_find(&att->log->entries, &xa_index, end, XA_PRESENT);
+ while (entry && !attestation_log_selected(ids, ids_count, entry->mr_id))
+ entry = xa_find_after(&att->log->entries, &xa_index, end, XA_PRESENT);
+ if (!entry)
+ return ctlv_op_query_done(ctx, boundary);
+ if (!cursor)
+ return ctlv_op_query_more(ctx, boundary, entry->mr_generation);
+ if (!ctlv_attestation_log_entry_start(reply, &nested))
+ return -EMSGSIZE;
+ ret = attestation_mr_log_entry_encode(&nested, entry);
+ ctlv_nest_end(&nested);
+ if (ret)
+ return ret;
+ entry = xa_find_after(&att->log->entries, &xa_index, end, XA_PRESENT);
+ while (entry && !attestation_log_selected(ids, ids_count, entry->mr_id))
+ entry = xa_find_after(&att->log->entries, &xa_index, end, XA_PRESENT);
+ if (entry)
+ return ctlv_op_query_more(ctx, boundary, entry->mr_generation);
+ return ctlv_op_query_done(ctx, boundary);
+}
--
2.56.0
^ permalink raw reply related [flat|nested] 32+ messages in thread* Re: [PATCH RFC 17/20] attestation: Add CTLV operation handling
2026-10-15 14:50 ` [PATCH RFC 17/20] attestation: Add CTLV operation handling Jiri Pirko
@ 2026-10-15 15:33 ` sashiko-bot
0 siblings, 0 replies; 32+ messages in thread
From: sashiko-bot @ 2026-10-15 15:33 UTC (permalink / raw)
To: Jiri Pirko; +Cc: kvm
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Critical] core: stack buffer overflow in ctlv_attestation_mr_log_get_query()
- [Medium] core: ctlv_blob memory leak in attestation_evidence_decode_objects()
- [Medium] attestation: missing loop in ctlv_attestation_mr_log_get_query restricts pagination to one entry
--
commit e751f49f55526c0e053324e8d2f25a2d22385dc9
Author: Jiri Pirko <jiri@resnulli.us>
attestation: Add CTLV operation handling
Translate CTLV evidence, MR and journal operations to the provider core and
wire the attestation family into the kernel build.
> diff --git a/drivers/attestation/core/ctlv.c b/drivers/attestation/core/ctlv.c
> new file mode 100644
> index 0000000000000..ff19400843c2c
> --- /dev/null
> +++ b/drivers/attestation/core/ctlv.c
[ ... ]
> +static int
> +attestation_evidence_decode_objects(struct attestation *att, struct ctlv_op_ctx *ctx,
> + const struct ctlv_cursor *request,
> + struct attestation_evidence_request *decoded,
> + struct attestation_evidence_object_request **objects_out)
> +{
[ ... ]
> + ctlv_attestation_evidence_get_evidence_object_for_each(&object, request)
> + {
> + const struct attestation_evidence_supported_object *supported;
> + const char *format;
> + size_t total = 0;
> +
> + format = ctlv_attestation_evidence_get_object_evidence_format_get(&object);
> + supported = attestation_evidence_object_find(caps, format);
> + if (!supported) {
> + kfree(objects);
> + return -EOPNOTSUPP;
> + }
[Severity: Medium]
Will this return leak any successfully resolved blobs from earlier iterations?
On iterations where the index is greater than 0, previous objects have their
output resolved via
ctlv_attestation_evidence_get_object_evidence_data_resolve(). Because this
error path only calls kfree(objects), those earlier ctlv_blob references
seem to be leaked.
> + for (previous = 0; previous < index; previous++)
> + if (objects[previous].supported_object == supported) {
> + kfree(objects);
> + return -EINVAL;
> + }
[Severity: Medium]
Does this face the same memory leak? If a duplicate format is found after some
blobs have already been resolved, it appears the accumulated references in
objects[0] to objects[index - 1] are not freed before returning here.
[ ... ]
> +int ctlv_attestation_mr_log_get_query(void *priv, struct ctlv_op_ctx *ctx,
> + const struct ctlv_cursor *request, struct ctlv_cursor *reply)
> +{
> + struct attestation *att = priv;
> + struct attestation_mr_log_entry *entry;
> + struct ctlv_cursor item, nested;
> + unsigned int ids[256];
> + size_t ids_count = 0, index;
> + unsigned int start, end;
> + unsigned long xa_index;
> + u64 boundary;
> + u64 cursor = ctlv_op_query_cursor(ctx);
> + int ret;
> +
> + ctlv_attestation_mr_log_get_mr_selection_for_each(&item, request)
> + {
> + u64 id = ctlv_attestation_mr_log_get_selection_mr_id_get(&item);
> +
> + if (id > UINT_MAX)
> + return -ERANGE;
> + for (index = 0; index < ids_count; index++)
> + if (ids[index] == id)
> + return -EINVAL;
> + ids[ids_count++] = id;
> + }
[Severity: Critical]
Can this loop overflow the fixed-size ids[] stack array?
If a request contains more than 256 selections, it looks like ids_count
could exceed the size of ids[256] since there isn't a bounds check prior to
appending the id. Would it be possible to add a check to stop writing once
the array is full or return an error?
[ ... ]
> + if (!cursor)
> + return ctlv_op_query_more(ctx, boundary, entry->mr_generation);
> + if (!ctlv_attestation_log_entry_start(reply, &nested))
> + return -EMSGSIZE;
> + ret = attestation_mr_log_entry_encode(&nested, entry);
> + ctlv_nest_end(&nested);
> + if (ret)
> + return ret;
> + entry = xa_find_after(&att->log->entries, &xa_index, end, XA_PRESENT);
> + while (entry && !attestation_log_selected(ids, ids_count, entry->mr_id))
> + entry = xa_find_after(&att->log->entries, &xa_index, end, XA_PRESENT);
> + if (entry)
> + return ctlv_op_query_more(ctx, boundary, entry->mr_generation);
> + return ctlv_op_query_done(ctx, boundary);
> +}
[Severity: Medium]
Is it intentional that this function only encodes one entry per query?
Unlike ctlv_attestation_mr_dump_query() which has a loop over its entries,
this lacks a while (entry) loop wrapping the encode block. It serializes
exactly one entry and then returns ctlv_op_query_more() if more exist,
artificially restricting pagination to a single entry and causing multiple
requests to read the log. Should this iterate until the buffer fills up
(-EMSGSIZE)?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261015145053.3664509-1-jiri@resnulli.us?part=17
^ permalink raw reply [flat|nested] 32+ messages in thread
* [PATCH RFC 18/20] tdx-guest: Expose the native attestation service API
2026-10-15 14:50 [PATCH RFC 00/20] attestation: Add a common subsystem and TDX support Jiri Pirko
` (16 preceding siblings ...)
2026-10-15 14:50 ` [PATCH RFC 17/20] attestation: Add CTLV operation handling Jiri Pirko
@ 2026-10-15 14:50 ` Jiri Pirko
2026-10-15 15:40 ` sashiko-bot
2026-10-15 14:50 ` [PATCH RFC 19/20] attestation: Add the TDX family specification and generated files Jiri Pirko
2026-10-15 14:50 ` [PATCH RFC 20/20] attestation: Add the TDX family implementation Jiri Pirko
19 siblings, 1 reply; 32+ messages in thread
From: Jiri Pirko @ 2026-10-15 14:50 UTC (permalink / raw)
To: linux-coco
Cc: linux-kernel, linux-api, x86, kvm, linux-doc, jgg, david.quigley,
lukas, ksinyuk, rodrigo.vivi, cedric.xing, sameo, drdeeglaze,
James.Bottomley, aik, jpiotrowski, qinkun, pgonda, jic23,
alistair.francis, ankita, suzuki.poulose, kas, rick.p.edgecombe,
elena.reshetova, dave.hansen, sathyanarayanan.kuppuswamy,
peter.fang
From: Jiri Pirko <jiri@nvidia.com>
Share TDREPORT, Quote and measurement-register operations with one
in-kernel client while preserving the existing misc, TSM report and TSM
MR interfaces. Tie clients to service initialization and removal.
Assisted-by: Cursor: GPT-5.6 Sol
Assisted-by: Cursor: Claude Opus 5.5
Signed-off-by: Jiri Pirko <jiri@nvidia.com>
---
Documentation/virt/coco/tdx-guest.rst | 18 ++
MAINTAINERS | 3 +
drivers/virt/coco/tdx-guest/tdx-guest.c | 258 +++++++++++++++++++++---
include/linux/tdx-guest.h | 75 +++++++
4 files changed, 326 insertions(+), 28 deletions(-)
create mode 100644 include/linux/tdx-guest.h
diff --git a/Documentation/virt/coco/tdx-guest.rst b/Documentation/virt/coco/tdx-guest.rst
index 46e316db6bb4..759497b1434a 100644
--- a/Documentation/virt/coco/tdx-guest.rst
+++ b/Documentation/virt/coco/tdx-guest.rst
@@ -50,3 +50,21 @@ TDX reference material is collected here:
https://www.intel.com/content/www/us/en/developer/articles/technical/intel-trust-domain-extensions.html
The driver is based on TDX module specification v1.0 and TDX GHCI specification v1.0.
+
+Kernel clients
+==============
+
+``<linux/tdx-guest.h>`` exposes the same native service to one in-kernel
+client. The client receives the misc device as its lifetime anchor and a
+complete ``struct tdx_guest_ops``. The operations retrieve TDREPORT or Quote
+bytes, read MRTD/RTMR values, extend an RTMR, and return immutable construction
+fields.
+
+Registering a client attaches it immediately when the service is already
+available. Otherwise attachment occurs when the TDX guest driver finishes
+initialization. Driver removal calls the client's remove callback before
+freeing report or Quote buffers. Client unregistration similarly drains the
+attachment before returning.
+
+The misc ioctl, TSM-report configfs interface, and TSM-MR sysfs attributes
+remain independent frontends over the same native serialization and buffers.
diff --git a/MAINTAINERS b/MAINTAINERS
index 5972a643b3c3..a05d10976011 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -29669,6 +29669,9 @@ L: linux-coco@lists.linux.dev
L: kvm@vger.kernel.org
S: Supported
T: git git://git.kernel.org/pub/scm/linux/kernel/git/tip/tip.git x86/tdx
+F: Documentation/virt/coco/tdx-guest.rst
+F: drivers/virt/coco/tdx-guest/
+F: include/linux/tdx-guest.h
N: tdx
K: \b(tdx)
diff --git a/drivers/virt/coco/tdx-guest/tdx-guest.c b/drivers/virt/coco/tdx-guest/tdx-guest.c
index d0303e31e816..0d5317060246 100644
--- a/drivers/virt/coco/tdx-guest/tdx-guest.c
+++ b/drivers/virt/coco/tdx-guest/tdx-guest.c
@@ -19,6 +19,7 @@
#include <linux/sockptr.h>
#include <linux/tsm.h>
#include <linux/tsm-mr.h>
+#include <linux/tdx-guest.h>
#include <uapi/linux/tdx-guest.h>
@@ -27,10 +28,21 @@
/* TDREPORT buffer */
static u8 *tdx_report_buf;
+static struct tdx_guest_info tdx_guest_info;
+static struct miscdevice tdx_misc_dev;
+
+static_assert(TDX_REPORTDATA_LEN == TDX_GUEST_REPORTDATA_LEN);
+static_assert(TDX_REPORT_LEN == TDX_GUEST_REPORT_LEN);
/* Lock to serialize TDG.MR.REPORT and TDG.MR.RTMR.EXTEND TDCALLs */
static DEFINE_MUTEX(mr_lock);
+/* Serializes native client registration and service attachment. */
+static DEFINE_MUTEX(client_lock);
+static const struct tdx_guest_client *tdx_client;
+static void *tdx_client_priv;
+static bool tdx_service_ready;
+
/* TDREPORT fields */
enum {
TDREPORT_reportdata = 128,
@@ -49,24 +61,30 @@ enum {
TDREPORT_servtd_hash = TDREPORT_rtmr3 + SHA384_DIGEST_SIZE,
};
-static int tdx_do_report(sockptr_t data, sockptr_t tdreport)
+static int __tdx_do_report(sockptr_t data, sockptr_t tdreport)
{
- scoped_cond_guard(mutex_intr, return -EINTR, &mr_lock) {
- u8 *reportdata = tdx_report_buf + TDREPORT_reportdata;
- int ret;
+ u8 *reportdata = tdx_report_buf + TDREPORT_reportdata;
+ int ret;
- if (!sockptr_is_null(data) &&
- copy_from_sockptr(reportdata, data, TDX_REPORTDATA_LEN))
- return -EFAULT;
+ lockdep_assert_held(&mr_lock);
+ if (!sockptr_is_null(data) &&
+ copy_from_sockptr(reportdata, data, TDX_REPORTDATA_LEN))
+ return -EFAULT;
- ret = tdx_mcall_get_report0(reportdata, tdx_report_buf);
- if (WARN_ONCE(ret, "tdx_mcall_get_report0() failed: %d", ret))
- return ret;
+ ret = tdx_mcall_get_report0(reportdata, tdx_report_buf);
+ if (WARN_ONCE(ret, "tdx_mcall_get_report0() failed: %d", ret))
+ return ret;
- if (!sockptr_is_null(tdreport) &&
- copy_to_sockptr(tdreport, tdx_report_buf, TDX_REPORT_LEN))
- return -EFAULT;
- }
+ if (!sockptr_is_null(tdreport) &&
+ copy_to_sockptr(tdreport, tdx_report_buf, TDX_REPORT_LEN))
+ return -EFAULT;
+ return 0;
+}
+
+static int tdx_do_report(sockptr_t data, sockptr_t tdreport)
+{
+ scoped_cond_guard(mutex_intr, return -EINTR, &mr_lock)
+ return __tdx_do_report(data, tdreport);
return 0;
}
@@ -90,6 +108,76 @@ static int tdx_do_extend(u8 mr_ind, const u8 *data)
return 0;
}
+static bool tdx_guest_device_valid(struct device *dev)
+{
+ return dev && dev == tdx_misc_dev.this_device;
+}
+
+static const struct tdx_guest_info *tdx_guest_info_get(struct device *dev)
+{
+ return tdx_guest_device_valid(dev) ? &tdx_guest_info : NULL;
+}
+
+static int tdx_guest_report_get(struct device *dev, const u8 *reportdata,
+ size_t length, void *out, size_t capacity,
+ size_t *out_length)
+{
+ int ret;
+
+ if (!tdx_guest_device_valid(dev))
+ return -ENODEV;
+ if (!reportdata || length != TDX_REPORTDATA_LEN || !out || !out_length)
+ return -EINVAL;
+ if (capacity < TDX_REPORT_LEN)
+ return -EMSGSIZE;
+ scoped_cond_guard(mutex_intr, return -EINTR, &mr_lock) {
+ ret = __tdx_do_report(KERNEL_SOCKPTR((void *)reportdata),
+ KERNEL_SOCKPTR(NULL));
+ if (ret)
+ return ret;
+ memcpy(out, tdx_report_buf, TDX_REPORT_LEN);
+ }
+ *out_length = TDX_REPORT_LEN;
+ return 0;
+}
+
+static int tdx_guest_mr_read(struct device *dev, enum tdx_guest_mr mr,
+ u8 value[SHA384_DIGEST_SIZE])
+{
+ static const size_t offsets[] = {
+ [TDX_GUEST_MR_MRTD] = TDREPORT_mrtd,
+ [TDX_GUEST_MR_RTMR0] = TDREPORT_rtmr0,
+ [TDX_GUEST_MR_RTMR1] = TDREPORT_rtmr1,
+ [TDX_GUEST_MR_RTMR2] = TDREPORT_rtmr2,
+ [TDX_GUEST_MR_RTMR3] = TDREPORT_rtmr3,
+ };
+ int ret;
+
+ if (!tdx_guest_device_valid(dev))
+ return -ENODEV;
+ if (mr >= ARRAY_SIZE(offsets) || !value)
+ return -EINVAL;
+ scoped_cond_guard(mutex_intr, return -EINTR, &mr_lock) {
+ ret = __tdx_do_report(KERNEL_SOCKPTR(NULL),
+ KERNEL_SOCKPTR(NULL));
+ if (ret)
+ return ret;
+ memcpy(value, tdx_report_buf + offsets[mr],
+ SHA384_DIGEST_SIZE);
+ }
+ return 0;
+}
+
+static int tdx_guest_rtmr_extend(struct device *dev, unsigned int rtmr,
+ const u8 value[SHA384_DIGEST_SIZE])
+{
+ if (!tdx_guest_device_valid(dev))
+ return -ENODEV;
+ if (rtmr >= 4 || !value)
+ return -EINVAL;
+ return tdx_do_extend(rtmr, value);
+}
+
#define TDX_MR_(r) .mr_value = (void *)TDREPORT_##r, TSM_MR_(r, SHA384)
static struct tsm_measurement_register tdx_mrs[] = {
{ TDX_MR_(rtmr0) | TSM_MR_F_RTMR },
@@ -135,6 +223,12 @@ static const struct attribute_group *tdx_mr_init(void)
rc = tdx_mr_refresh(&tdx_measurements);
if (rc)
return ERR_PTR(rc);
+ memcpy(tdx_guest_info.mrconfigid,
+ buf + TDREPORT_mrconfigid, SHA384_DIGEST_SIZE);
+ memcpy(tdx_guest_info.mrowner,
+ buf + TDREPORT_mrowner, SHA384_DIGEST_SIZE);
+ memcpy(tdx_guest_info.mrownerconfig,
+ buf + TDREPORT_mrownerconfig, SHA384_DIGEST_SIZE);
/*
* @mr_value was initialized with the offset only, while the base
@@ -191,6 +285,8 @@ struct tdx_quote_buf {
u8 data[];
};
+static_assert(TDX_QUOTE_MAX_LEN == TDX_GUEST_QUOTE_MAX_LEN);
+
/* Quote data buffer */
static void *quote_data;
@@ -265,15 +361,18 @@ static int wait_for_quote_completion(struct tdx_quote_buf *quote_buf, u32 timeou
return (i == timeout) ? -ETIMEDOUT : 0;
}
-static int tdx_report_new_locked(struct tsm_report *report, void *data)
+static int __tdx_guest_quote_get(const u8 *reportdata, size_t length,
+ void *out, size_t capacity,
+ size_t *out_length)
{
- u8 *buf;
struct tdx_quote_buf *quote_buf = quote_data;
- struct tsm_report_desc *desc = &report->desc;
u32 out_len;
int ret;
u64 err;
+ lockdep_assert_held("e_lock);
+ if (!reportdata || length != TDX_REPORTDATA_LEN || !out || !out_length)
+ return -EINVAL;
/*
* If the previous request is timedout or interrupted, and the
* Quote buf status is still in GET_QUOTE_IN_FLIGHT (owned by
@@ -282,16 +381,13 @@ static int tdx_report_new_locked(struct tsm_report *report, void *data)
if (quote_buf->status == GET_QUOTE_IN_FLIGHT)
return -EBUSY;
- if (desc->inblob_len != TDX_REPORTDATA_LEN)
- return -EINVAL;
-
memset(quote_data, 0, GET_QUOTE_BUF_SIZE);
/* Update Quote buffer header */
quote_buf->version = GET_QUOTE_CMD_VER;
quote_buf->in_len = TDX_REPORT_LEN;
- ret = tdx_do_report(KERNEL_SOCKPTR(desc->inblob),
+ ret = tdx_do_report(KERNEL_SOCKPTR((void *)reportdata),
KERNEL_SOCKPTR(quote_buf->data));
if (ret)
return ret;
@@ -317,13 +413,10 @@ static int tdx_report_new_locked(struct tsm_report *report, void *data)
if (out_len > TDX_QUOTE_MAX_LEN)
return -EFBIG;
-
- buf = kvmemdup(quote_buf->data, out_len, GFP_KERNEL);
- if (!buf)
- return -ENOMEM;
-
- report->outblob = buf;
- report->outblob_len = out_len;
+ if (out_len > capacity)
+ return -EMSGSIZE;
+ memcpy(out, quote_buf->data, out_len);
+ *out_length = out_len;
/*
* TODO: parse the PEM-formatted cert chain out of the quote buffer when
@@ -333,6 +426,39 @@ static int tdx_report_new_locked(struct tsm_report *report, void *data)
return ret;
}
+static int tdx_guest_quote_get(struct device *dev, const u8 *reportdata,
+ size_t length, void *out, size_t capacity,
+ size_t *out_length)
+{
+ if (!tdx_guest_device_valid(dev))
+ return -ENODEV;
+ scoped_cond_guard(mutex_intr, return -EINTR, "e_lock)
+ return __tdx_guest_quote_get(reportdata, length, out,
+ capacity, out_length);
+ return 0;
+}
+
+static int tdx_report_new_locked(struct tsm_report *report, void *data)
+{
+ struct tsm_report_desc *desc = &report->desc;
+ size_t length;
+ void *buf;
+ int ret;
+
+ buf = kvmalloc(TDX_QUOTE_MAX_LEN, GFP_KERNEL);
+ if (!buf)
+ return -ENOMEM;
+ ret = __tdx_guest_quote_get(desc->inblob, desc->inblob_len,
+ buf, TDX_QUOTE_MAX_LEN, &length);
+ if (ret) {
+ kvfree(buf);
+ return ret;
+ }
+ report->outblob = buf;
+ report->outblob_len = length;
+ return 0;
+}
+
static int tdx_report_new(struct tsm_report *report, void *data)
{
scoped_cond_guard(mutex_intr, return -EINTR, "e_lock)
@@ -402,6 +528,68 @@ static const struct tsm_report_ops tdx_tsm_ops = {
.report_bin_attr_visible = tdx_report_bin_attr_visible,
};
+static const struct tdx_guest_ops tdx_guest_ops = {
+ .info_get = tdx_guest_info_get,
+ .report_get = tdx_guest_report_get,
+ .quote_get = tdx_guest_quote_get,
+ .mr_read = tdx_guest_mr_read,
+ .rtmr_extend = tdx_guest_rtmr_extend,
+};
+
+static bool tdx_client_attached;
+
+static int tdx_guest_client_attach(void)
+{
+ void *priv;
+
+ lockdep_assert_held(&client_lock);
+ if (!tdx_client || !tdx_service_ready)
+ return 0;
+ priv = tdx_client->add(tdx_misc_dev.this_device, &tdx_guest_ops);
+ if (IS_ERR(priv))
+ return PTR_ERR(priv);
+ tdx_client_priv = priv;
+ tdx_client_attached = true;
+ return 0;
+}
+
+static void tdx_guest_client_detach(void)
+{
+ lockdep_assert_held(&client_lock);
+ if (!tdx_client_attached)
+ return;
+ tdx_client->remove(tdx_client_priv);
+ tdx_client_priv = NULL;
+ tdx_client_attached = false;
+}
+
+int tdx_guest_client_register(const struct tdx_guest_client *client)
+{
+ int ret;
+
+ if (!client || !client->add || !client->remove)
+ return -EINVAL;
+ guard(mutex)(&client_lock);
+ if (tdx_client)
+ return -EBUSY;
+ tdx_client = client;
+ ret = tdx_guest_client_attach();
+ if (ret)
+ tdx_client = NULL;
+ return ret;
+}
+EXPORT_SYMBOL_NS_GPL(tdx_guest_client_register, "TDX_GUEST");
+
+void tdx_guest_client_unregister(const struct tdx_guest_client *client)
+{
+ guard(mutex)(&client_lock);
+ if (WARN_ON_ONCE(client != tdx_client))
+ return;
+ tdx_guest_client_detach();
+ tdx_client = NULL;
+}
+EXPORT_SYMBOL_NS_GPL(tdx_guest_client_unregister, "TDX_GUEST");
+
static int __init tdx_guest_init(void)
{
int ret;
@@ -427,9 +615,19 @@ static int __init tdx_guest_init(void)
ret = tsm_report_register(&tdx_tsm_ops, NULL);
if (ret)
goto free_quote;
+ scoped_guard(mutex, &client_lock) {
+ tdx_service_ready = true;
+ ret = tdx_guest_client_attach();
+ if (ret)
+ tdx_service_ready = false;
+ }
+ if (ret)
+ goto unregister_tsm;
return 0;
+unregister_tsm:
+ tsm_report_unregister(&tdx_tsm_ops);
free_quote:
free_quote_buf(quote_data);
free_misc:
@@ -443,6 +641,10 @@ module_init(tdx_guest_init);
static void __exit tdx_guest_exit(void)
{
+ scoped_guard(mutex, &client_lock) {
+ tdx_guest_client_detach();
+ tdx_service_ready = false;
+ }
tsm_report_unregister(&tdx_tsm_ops);
free_quote_buf(quote_data);
misc_deregister(&tdx_misc_dev);
diff --git a/include/linux/tdx-guest.h b/include/linux/tdx-guest.h
new file mode 100644
index 000000000000..65f367e4956a
--- /dev/null
+++ b/include/linux/tdx-guest.h
@@ -0,0 +1,75 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef _LINUX_TDX_GUEST_H
+#define _LINUX_TDX_GUEST_H
+
+#include <crypto/sha2.h>
+#include <linux/types.h>
+
+struct device;
+
+#define TDX_GUEST_REPORTDATA_LEN 64
+#define TDX_GUEST_REPORT_LEN 1024
+#define TDX_GUEST_QUOTE_MAX_LEN ((128 * 1024) - 24)
+
+struct tdx_guest_info {
+ u8 mrconfigid[SHA384_DIGEST_SIZE];
+ u8 mrowner[SHA384_DIGEST_SIZE];
+ u8 mrownerconfig[SHA384_DIGEST_SIZE];
+};
+
+enum tdx_guest_mr {
+ TDX_GUEST_MR_MRTD,
+ TDX_GUEST_MR_RTMR0,
+ TDX_GUEST_MR_RTMR1,
+ TDX_GUEST_MR_RTMR2,
+ TDX_GUEST_MR_RTMR3,
+};
+
+/**
+ * struct tdx_guest_ops - Native TDX guest operations
+ * @info_get: Return immutable TD construction fields
+ * @report_get: Get a TDREPORT bound to report data
+ * @quote_get: Get a Quote bound to report data
+ * @mr_read: Read MRTD or an RTMR
+ * @rtmr_extend: Extend an RTMR
+ */
+struct tdx_guest_ops {
+ const struct tdx_guest_info *(*info_get)(struct device *dev);
+ int (*report_get)(struct device *dev, const u8 *reportdata,
+ size_t length, void *out, size_t capacity,
+ size_t *out_length);
+ int (*quote_get)(struct device *dev, const u8 *reportdata,
+ size_t length, void *out, size_t capacity,
+ size_t *out_length);
+ int (*mr_read)(struct device *dev, enum tdx_guest_mr mr,
+ u8 value[SHA384_DIGEST_SIZE]);
+ int (*rtmr_extend)(struct device *dev, unsigned int rtmr,
+ const u8 value[SHA384_DIGEST_SIZE]);
+};
+
+/**
+ * struct tdx_guest_client - Native TDX service consumer
+ * @add: Attach to the service and return client private data or an ERR_PTR
+ * @remove: Detach the client private data returned by @add
+ */
+struct tdx_guest_client {
+ void *(*add)(struct device *parent_dev,
+ const struct tdx_guest_ops *ops);
+ void (*remove)(void *client_priv);
+};
+
+/**
+ * tdx_guest_client_register - Register one native TDX service consumer
+ * @client: Immutable add/remove callback pair
+ *
+ * Return: 0, or a negative errno.
+ */
+int tdx_guest_client_register(const struct tdx_guest_client *client);
+
+/**
+ * tdx_guest_client_unregister - Unregister a native TDX service consumer
+ * @client: Descriptor passed to tdx_guest_client_register()
+ */
+void tdx_guest_client_unregister(const struct tdx_guest_client *client);
+
+#endif /* _LINUX_TDX_GUEST_H */
--
2.56.0
^ permalink raw reply related [flat|nested] 32+ messages in thread* Re: [PATCH RFC 18/20] tdx-guest: Expose the native attestation service API
2026-10-15 14:50 ` [PATCH RFC 18/20] tdx-guest: Expose the native attestation service API Jiri Pirko
@ 2026-10-15 15:40 ` sashiko-bot
0 siblings, 0 replies; 32+ messages in thread
From: sashiko-bot @ 2026-10-15 15:40 UTC (permalink / raw)
To: Jiri Pirko; +Cc: kvm
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] tdx: client attach failure aborts core driver initialization
- [Low] tdx: memory bloat per TSM report due to missing right-sizing
- [Low] Unreachable return statements after scoped_cond_guard
--
commit b1dd6a27af0398a52344b35c7786c4b3fbbf85c3
Author: Jiri Pirko <jiri@resnulli.us>
tdx-guest: Expose the native attestation service API
This patch shares TDREPORT, Quote and measurement-register operations with one
in-kernel client while preserving the existing misc, TSM report and TSM
MR interfaces. It ties clients to service initialization and removal.
> diff --git a/drivers/virt/coco/tdx-guest/tdx-guest.c b/drivers/virt/coco/tdx-guest/tdx-guest.c
> index d0303e31e8161..0d53170602463 100644
> --- a/drivers/virt/coco/tdx-guest/tdx-guest.c
> +++ b/drivers/virt/coco/tdx-guest/tdx-guest.c
[ ... ]
> +static int tdx_do_report(sockptr_t data, sockptr_t tdreport)
> +{
> + scoped_cond_guard(mutex_intr, return -EINTR, &mr_lock)
> + return __tdx_do_report(data, tdreport);
> return 0;
> }
[Severity: Low]
Is the trailing return 0 unreachable? When scoped_cond_guard is used with a
single return statement as its body, both the success and failure branches
return immediately. The loop's internal break is never reached, preventing
execution from falling through to the statements following the macro.
[ ... ]
> +static int tdx_guest_quote_get(struct device *dev, const u8 *reportdata,
> + size_t length, void *out, size_t capacity,
> + size_t *out_length)
> +{
> + if (!tdx_guest_device_valid(dev))
> + return -ENODEV;
> + scoped_cond_guard(mutex_intr, return -EINTR, "e_lock)
> + return __tdx_guest_quote_get(reportdata, length, out,
> + capacity, out_length);
> + return 0;
> +}
[Severity: Low]
Is this trailing return 0 also unreachable for the same reason as in
tdx_do_report()?
[ ... ]
> +static int tdx_report_new_locked(struct tsm_report *report, void *data)
> +{
> + struct tsm_report_desc *desc = &report->desc;
> + size_t length;
> + void *buf;
> + int ret;
> +
> + buf = kvmalloc(TDX_QUOTE_MAX_LEN, GFP_KERNEL);
> + if (!buf)
> + return -ENOMEM;
> + ret = __tdx_guest_quote_get(desc->inblob, desc->inblob_len,
> + buf, TDX_QUOTE_MAX_LEN, &length);
> + if (ret) {
> + kvfree(buf);
> + return ret;
> + }
> + report->outblob = buf;
> + report->outblob_len = length;
> + return 0;
> +}
[Severity: Low]
Does this code introduce persistent memory bloat for each TSM report?
The buffer is allocated at the maximum possible size of 128KB but is never
shrunk to the actual quote length returned in the length variable.
The previous implementation used kvmemdup() to size the buffer exactly to
the output length, which is typically much smaller than the 128KB maximum.
By leaving it at 128KB, could this increase the memory footprint for
concurrent reports?
[ ... ]
> static int __init tdx_guest_init(void)
> {
> int ret;
[ ... ]
> ret = tsm_report_register(&tdx_tsm_ops, NULL);
> if (ret)
> goto free_quote;
> + scoped_guard(mutex, &client_lock) {
> + tdx_service_ready = true;
> + ret = tdx_guest_client_attach();
> + if (ret)
> + tdx_service_ready = false;
> + }
> + if (ret)
> + goto unregister_tsm;
>
> return 0;
>
> +unregister_tsm:
> + tsm_report_unregister(&tdx_tsm_ops);
[Severity: High]
Does a failure to attach a client here abort the entire core driver
initialization?
If a built-in client registers before tdx_guest_init() runs, a failure in
the client's add() callback will cause tdx_guest_client_attach() to fail.
This error is returned, causing a jump to unregister_tsm, which aborts the
initialization of the TDX guest driver and breaks other frontends like the
misc ioctl and configfs.
If the client is loaded after the driver, its failure does not affect the
core driver. Should the core provider ignore an optional downstream client's
failure and continue its initialization?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261015145053.3664509-1-jiri@resnulli.us?part=18
^ permalink raw reply [flat|nested] 32+ messages in thread
* [PATCH RFC 19/20] attestation: Add the TDX family specification and generated files
2026-10-15 14:50 [PATCH RFC 00/20] attestation: Add a common subsystem and TDX support Jiri Pirko
` (17 preceding siblings ...)
2026-10-15 14:50 ` [PATCH RFC 18/20] tdx-guest: Expose the native attestation service API Jiri Pirko
@ 2026-10-15 14:50 ` Jiri Pirko
2026-10-15 14:50 ` [PATCH RFC 20/20] attestation: Add the TDX family implementation Jiri Pirko
19 siblings, 0 replies; 32+ messages in thread
From: Jiri Pirko @ 2026-10-15 14:50 UTC (permalink / raw)
To: linux-coco
Cc: linux-kernel, linux-api, x86, kvm, linux-doc, jgg, david.quigley,
lukas, ksinyuk, rodrigo.vivi, cedric.xing, sameo, drdeeglaze,
James.Bottomley, aik, jpiotrowski, qinkun, pgonda, jic23,
alistair.francis, ankita, suzuki.poulose, kas, rick.p.edgecombe,
elena.reshetova, dave.hansen, sathyanarayanan.kuppuswamy,
peter.fang
From: Jiri Pirko <jiri@nvidia.com>
Define the TDX attestation family and reserve family ID 2. Add its ABI
snapshot, registry entry and generated kernel and UAPI files.
The ABI is intentionally specification-first. The generated descriptor is
linked with the provider implementation in the following patch.
Assisted-by: Cursor: GPT-5.6 Sol
Assisted-by: Cursor: Claude Opus 5.5
Signed-off-by: Jiri Pirko <jiri@nvidia.com>
---
.../ctlv/abi/ctlv-attestation-tdx.yaml | 22 +++++++
Documentation/ctlv/abi/families.registry.yaml | 1 +
Documentation/ctlv/families.yaml | 1 +
Documentation/ctlv/generated-files.yaml | 7 ++
.../ctlv/specs/ctlv-attestation-tdx.yaml | 30 +++++++++
.../generated/ctlv_attestation_tdx_gen.c | 61 +++++++++++++++++
.../generated/ctlv_attestation_tdx_gen.h | 66 +++++++++++++++++++
include/uapi/linux/ctlv_attestation_tdx_gen.h | 44 +++++++++++++
include/uapi/linux/ctlv_families_gen.h | 1 +
9 files changed, 233 insertions(+)
create mode 100644 Documentation/ctlv/abi/ctlv-attestation-tdx.yaml
create mode 100644 Documentation/ctlv/specs/ctlv-attestation-tdx.yaml
create mode 100644 drivers/attestation/generated/ctlv_attestation_tdx_gen.c
create mode 100644 drivers/attestation/generated/ctlv_attestation_tdx_gen.h
create mode 100644 include/uapi/linux/ctlv_attestation_tdx_gen.h
diff --git a/Documentation/ctlv/abi/ctlv-attestation-tdx.yaml b/Documentation/ctlv/abi/ctlv-attestation-tdx.yaml
new file mode 100644
index 000000000000..89b80d84e7f1
--- /dev/null
+++ b/Documentation/ctlv/abi/ctlv-attestation-tdx.yaml
@@ -0,0 +1,22 @@
+# SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause)
+%YAML 1.2
+---
+# Checked-in ABI snapshot of the ctlv-attestation-tdx family, written by
+# "make -C tools/ctlv abi-update".
+#
+# The checker compares this family's declarations and inherited stub fills
+# against this file. Updating it is how a UAPI change gets reviewed; an update
+# that removes or renumbers anything, narrows a bound, or makes a placement
+# required is an ABI break, whatever this file says afterwards.
+family: {name: ctlv-attestation-tdx, id: 2, inherits: ctlv-attestation,
+ pre-init: true}
+attr-groups:
+ info:
+ id: 1
+ attrs:
+ mrconfigid: {id: 1, type: binary, length: 48}
+ mrowner: {id: 2, type: binary, length: 48}
+ mrownerconfig: {id: 3, type: binary, length: 48}
+fills:
+ device-info:
+ attrs: [info.mrconfigid, info.mrowner, info.mrownerconfig]
diff --git a/Documentation/ctlv/abi/families.registry.yaml b/Documentation/ctlv/abi/families.registry.yaml
index fa511b709fc1..193e3570ab95 100644
--- a/Documentation/ctlv/abi/families.registry.yaml
+++ b/Documentation/ctlv/abi/families.registry.yaml
@@ -6,3 +6,4 @@
families:
- ctlv
- ctlv-attestation
+ - ctlv-attestation-tdx
diff --git a/Documentation/ctlv/families.yaml b/Documentation/ctlv/families.yaml
index d78025a9072c..d0e1493db498 100644
--- a/Documentation/ctlv/families.yaml
+++ b/Documentation/ctlv/families.yaml
@@ -12,3 +12,4 @@
families:
- ctlv
- ctlv-attestation
+ - ctlv-attestation-tdx
diff --git a/Documentation/ctlv/generated-files.yaml b/Documentation/ctlv/generated-files.yaml
index 669f1f8d51b4..00ad23af7f65 100644
--- a/Documentation/ctlv/generated-files.yaml
+++ b/Documentation/ctlv/generated-files.yaml
@@ -22,3 +22,10 @@ families:
uapi-header: include/uapi/linux/ctlv_attestation_gen.h
kernel-header: drivers/attestation/generated/ctlv_attestation_gen.h
kernel-source: drivers/attestation/generated/ctlv_attestation_gen.c
+
+ - name: ctlv-attestation-tdx
+ source: Documentation/ctlv/specs/ctlv-attestation-tdx.yaml
+ outputs:
+ uapi-header: include/uapi/linux/ctlv_attestation_tdx_gen.h
+ kernel-header: drivers/attestation/generated/ctlv_attestation_tdx_gen.h
+ kernel-source: drivers/attestation/generated/ctlv_attestation_tdx_gen.c
diff --git a/Documentation/ctlv/specs/ctlv-attestation-tdx.yaml b/Documentation/ctlv/specs/ctlv-attestation-tdx.yaml
new file mode 100644
index 000000000000..09f4d9916f82
--- /dev/null
+++ b/Documentation/ctlv/specs/ctlv-attestation-tdx.yaml
@@ -0,0 +1,30 @@
+# SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause)
+%YAML 1.2
+---
+family:
+ name: ctlv-attestation-tdx
+ inherits: ctlv-attestation
+ pre-init: true
+
+attr-groups:
+ - name: info
+ attrs:
+ - name: mrconfigid
+ type: binary
+ min-length: 48
+ max-length: 48
+ - name: mrowner
+ type: binary
+ min-length: 48
+ max-length: 48
+ - name: mrownerconfig
+ type: binary
+ min-length: 48
+ max-length: 48
+
+attr-sets:
+ - name: device-info
+ attrs:
+ - info.mrconfigid
+ - info.mrowner
+ - info.mrownerconfig
diff --git a/drivers/attestation/generated/ctlv_attestation_tdx_gen.c b/drivers/attestation/generated/ctlv_attestation_tdx_gen.c
new file mode 100644
index 000000000000..25f03a8d68fe
--- /dev/null
+++ b/drivers/attestation/generated/ctlv_attestation_tdx_gen.c
@@ -0,0 +1,61 @@
+// SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause)
+/* Do not edit, regenerate with tools/ctlv/ctlv-regen.sh, from: */
+/* Documentation/ctlv/specs/ctlv-attestation-tdx.yaml */
+/* CTLV-GEN kernel source */
+
+#define _CTLV_GENERATED_METADATA_ONLY
+#include <linux/array_size.h>
+#include <linux/build_bug.h>
+#include <linux/ctlv_family.h>
+#include <linux/export.h>
+
+#include <uapi/linux/ctlv_gen.h>
+#include <uapi/linux/ctlv_attestation_tdx_gen.h>
+
+#include "ctlv_attestation_tdx_gen.h"
+
+static const struct ctlv_node_schema device_info_fill_node_schemas[] = {
+ CTLV_NODE_SCHEMA_ROOT(1, 3, 3),
+ CTLV_NODE_SCHEMA_BINARY(CTLV_ATTESTATION_TDX_ATTR_INFO_MRCONFIGID, 1, 48, 48),
+ CTLV_NODE_SCHEMA_BINARY(CTLV_ATTESTATION_TDX_ATTR_INFO_MROWNER, 1, 48, 48),
+ CTLV_NODE_SCHEMA_BINARY(CTLV_ATTESTATION_TDX_ATTR_INFO_MROWNERCONFIG, 1, 48, 48),
+};
+
+/* The extension points this family fills. */
+static const struct ctlv_stub_fill_schema ctlv_attestation_tdx_stub_fill_schemas[] = {
+ CTLV_STUB_FILL_SCHEMA(CTLV_FAMILY_ATTESTATION, CTLV_ATTESTATION_SET_DEVICE_INFO,
+ device_info),
+};
+
+/* The family ID is its position in the central registry. */
+const struct ctlv_family_schema ctlv_attestation_tdx_schema = {
+ .name = "ctlv-attestation-tdx",
+ .family_id = 2,
+ .stub_fill_schemas = ctlv_attestation_tdx_stub_fill_schemas,
+ .stub_fill_schemas_count = ARRAY_SIZE(ctlv_attestation_tdx_stub_fill_schemas),
+};
+
+/* Maximum uevent cost for this family chain. */
+static_assert(150 <= CTLV_UEVENT_MAX_LEN);
+static_assert(5 <= CTLV_UEVENT_MAX_VARS);
+
+/* Extension-point callbacks. */
+static const struct ctlv_stub_ops ctlv_attestation_tdx_stub_ops[] = {
+ CTLV_STUB_OPS(CTLV_FAMILY_ATTESTATION, CTLV_ATTESTATION_SET_DEVICE_INFO,
+ ctlv_attestation_tdx_device_info),
+};
+
+/* Family descriptor used by ctlv_create(). */
+const struct ctlv_family ctlv_attestation_tdx_family = {
+ .schema = &ctlv_attestation_tdx_schema,
+ .parent = &ctlv_attestation_family,
+ .pre = ctlv_attestation_tdx_op_pre,
+ .post = ctlv_attestation_tdx_op_post,
+ .priv_size = ctlv_attestation_tdx_priv_size,
+ .pre_init = ctlv_attestation_tdx_pre_init,
+ .init = ctlv_attestation_tdx_init,
+ .fini = ctlv_attestation_tdx_fini,
+ .stub_ops = ctlv_attestation_tdx_stub_ops,
+ .stub_ops_count = ARRAY_SIZE(ctlv_attestation_tdx_stub_ops),
+};
+EXPORT_SYMBOL_NS_GPL(ctlv_attestation_tdx_family, "CTLV");
diff --git a/drivers/attestation/generated/ctlv_attestation_tdx_gen.h b/drivers/attestation/generated/ctlv_attestation_tdx_gen.h
new file mode 100644
index 000000000000..d182d04cb086
--- /dev/null
+++ b/drivers/attestation/generated/ctlv_attestation_tdx_gen.h
@@ -0,0 +1,66 @@
+/* SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause) */
+/* Do not edit, regenerate with tools/ctlv/ctlv-regen.sh, from: */
+/* Documentation/ctlv/specs/ctlv-attestation-tdx.yaml */
+/* CTLV-GEN kernel header */
+
+/* Kernel metadata and family API of the ctlv-attestation-tdx CTLV family. */
+
+#ifndef _LINUX_CTLV_ATTESTATION_TDX_GEN_H
+#define _LINUX_CTLV_ATTESTATION_TDX_GEN_H
+
+#include <linux/ctlv_family.h>
+#include <linux/err.h>
+#include <linux/errno.h>
+
+#include <uapi/linux/ctlv_attestation_tdx_gen.h>
+
+#include "ctlv_attestation_gen.h"
+
+extern const struct ctlv_family_schema ctlv_attestation_tdx_schema;
+
+/* Family-wide hooks around every effective non-core op. */
+int ctlv_attestation_tdx_op_pre(void *priv, struct ctlv_op_ctx *ctx);
+void ctlv_attestation_tdx_op_post(void *priv, struct ctlv_op_ctx *ctx);
+
+/* Core-allocated family priv lifecycle. */
+size_t ctlv_attestation_tdx_priv_size(void);
+const void *ctlv_attestation_tdx_pre_init(const void *init_info);
+int ctlv_attestation_tdx_init(struct ctlv_device *ctlvdev, const void *init_info, void *priv);
+void ctlv_attestation_tdx_fini(struct ctlv_device *ctlvdev, void *priv);
+
+/* device-info of ctlv-attestation: fill callback. */
+int ctlv_attestation_tdx_device_info_fill(void *priv, struct ctlv_op_ctx *ctx,
+ const struct ctlv_cursor *request,
+ struct ctlv_cursor *nest);
+
+extern const struct ctlv_family ctlv_attestation_tdx_family;
+
+#ifndef _CTLV_GENERATED_METADATA_ONLY
+
+/* Reading a validated message. A blob has a resolver instead of a getter. */
+CTLV_IN_REF(ctlv_attestation_tdx_device_info_info_mrconfigid,
+ &ctlv_attestation_tdx_schema.stub_fill_schemas[0].members,
+ CTLV_ATTESTATION_TDX_ATTR_INFO_MRCONFIGID, 0, 0, CTLV_SCHEMA_BINARY)
+CTLV_IN_HAS(ctlv_attestation_tdx_device_info_info_mrconfigid)
+CTLV_IN_BINARY(ctlv_attestation_tdx_device_info_info_mrconfigid)
+CTLV_IN_REF(ctlv_attestation_tdx_device_info_info_mrowner,
+ &ctlv_attestation_tdx_schema.stub_fill_schemas[0].members,
+ CTLV_ATTESTATION_TDX_ATTR_INFO_MROWNER, 0, 1, CTLV_SCHEMA_BINARY)
+CTLV_IN_HAS(ctlv_attestation_tdx_device_info_info_mrowner)
+CTLV_IN_BINARY(ctlv_attestation_tdx_device_info_info_mrowner)
+CTLV_IN_REF(ctlv_attestation_tdx_device_info_info_mrownerconfig,
+ &ctlv_attestation_tdx_schema.stub_fill_schemas[0].members,
+ CTLV_ATTESTATION_TDX_ATTR_INFO_MROWNERCONFIG, 0, 2, CTLV_SCHEMA_BINARY)
+CTLV_IN_HAS(ctlv_attestation_tdx_device_info_info_mrownerconfig)
+CTLV_IN_BINARY(ctlv_attestation_tdx_device_info_info_mrownerconfig)
+
+/* Writing a reply. An append is atomic; a nest ends with ctlv_nest_end(). */
+CTLV_PUT_BINARY(ctlv_attestation_tdx_info_mrconfigid, CTLV_ATTESTATION_TDX_ATTR_INFO_MRCONFIGID,
+ CTLV_SCHEMA_BINARY)
+CTLV_PUT_BINARY(ctlv_attestation_tdx_info_mrowner, CTLV_ATTESTATION_TDX_ATTR_INFO_MROWNER,
+ CTLV_SCHEMA_BINARY)
+CTLV_PUT_BINARY(ctlv_attestation_tdx_info_mrownerconfig,
+ CTLV_ATTESTATION_TDX_ATTR_INFO_MROWNERCONFIG, CTLV_SCHEMA_BINARY)
+#endif /* _CTLV_GENERATED_METADATA_ONLY */
+
+#endif /* _LINUX_CTLV_ATTESTATION_TDX_GEN_H */
diff --git a/include/uapi/linux/ctlv_attestation_tdx_gen.h b/include/uapi/linux/ctlv_attestation_tdx_gen.h
new file mode 100644
index 000000000000..04c8dbc3d07c
--- /dev/null
+++ b/include/uapi/linux/ctlv_attestation_tdx_gen.h
@@ -0,0 +1,44 @@
+/* SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause) */
+/* Do not edit, regenerate with tools/ctlv/ctlv-regen.sh, from: */
+/* Documentation/ctlv/specs/ctlv-attestation-tdx.yaml */
+/* CTLV-GEN uapi header */
+
+/*
+ * Constants of the ctlv-attestation-tdx CTLV family. Attr and op IDs are complete; the wire format
+ * carrying them is in <linux/ctlv.h>.
+ *
+ * The inherited ctlv-attestation header is included here, so one include covers the whole effective
+ * schema.
+ */
+
+#ifndef _UAPI_LINUX_CTLV_ATTESTATION_TDX_GEN_H
+#define _UAPI_LINUX_CTLV_ATTESTATION_TDX_GEN_H
+
+#include <linux/ctlv.h>
+#include <linux/ctlv_attestation_gen.h>
+#include <linux/ctlv_families_gen.h>
+
+/* attr groups */
+enum _ctlv_attestation_tdx_group {
+ CTLV_ATTESTATION_TDX_GROUP_INFO = 1,
+};
+
+#define _CTLV_ATTESTATION_TDX_ATTR(_group, _attr) \
+ CTLV_ATTR(CTLV_FAMILY_ATTESTATION_TDX, (_group), (_attr))
+
+/* attrs of group info */
+#define _CTLV_ATTESTATION_TDX_ATTR_INFO(_attr) \
+ _CTLV_ATTESTATION_TDX_ATTR(CTLV_ATTESTATION_TDX_GROUP_INFO, \
+ _CTLV_ATTESTATION_TDX_ATTR_INFO_##_attr)
+
+enum _ctlv_attestation_tdx_attr_info {
+ _CTLV_ATTESTATION_TDX_ATTR_INFO_MRCONFIGID = 1,
+ _CTLV_ATTESTATION_TDX_ATTR_INFO_MROWNER,
+ _CTLV_ATTESTATION_TDX_ATTR_INFO_MROWNERCONFIG,
+};
+
+#define CTLV_ATTESTATION_TDX_ATTR_INFO_MRCONFIGID _CTLV_ATTESTATION_TDX_ATTR_INFO(MRCONFIGID)
+#define CTLV_ATTESTATION_TDX_ATTR_INFO_MROWNER _CTLV_ATTESTATION_TDX_ATTR_INFO(MROWNER)
+#define CTLV_ATTESTATION_TDX_ATTR_INFO_MROWNERCONFIG _CTLV_ATTESTATION_TDX_ATTR_INFO(MROWNERCONFIG)
+
+#endif /* _UAPI_LINUX_CTLV_ATTESTATION_TDX_GEN_H */
diff --git a/include/uapi/linux/ctlv_families_gen.h b/include/uapi/linux/ctlv_families_gen.h
index 2ea3068868be..25c90509d8ad 100644
--- a/include/uapi/linux/ctlv_families_gen.h
+++ b/include/uapi/linux/ctlv_families_gen.h
@@ -11,6 +11,7 @@
enum _ctlv_family {
CTLV_FAMILY_ROOT,
CTLV_FAMILY_ATTESTATION,
+ CTLV_FAMILY_ATTESTATION_TDX,
};
#endif /* _UAPI_LINUX_CTLV_FAMILIES_GEN_H */
--
2.56.0
^ permalink raw reply related [flat|nested] 32+ messages in thread* [PATCH RFC 20/20] attestation: Add the TDX family implementation
2026-10-15 14:50 [PATCH RFC 00/20] attestation: Add a common subsystem and TDX support Jiri Pirko
` (18 preceding siblings ...)
2026-10-15 14:50 ` [PATCH RFC 19/20] attestation: Add the TDX family specification and generated files Jiri Pirko
@ 2026-10-15 14:50 ` Jiri Pirko
2026-10-15 15:40 ` sashiko-bot
19 siblings, 1 reply; 32+ messages in thread
From: Jiri Pirko @ 2026-10-15 14:50 UTC (permalink / raw)
To: linux-coco
Cc: linux-kernel, linux-api, x86, kvm, linux-doc, jgg, david.quigley,
lukas, ksinyuk, rodrigo.vivi, cedric.xing, sameo, drdeeglaze,
James.Bottomley, aik, jpiotrowski, qinkun, pgonda, jic23,
alistair.francis, ankita, suzuki.poulose, kas, rick.p.edgecombe,
elena.reshetova, dave.hansen, sathyanarayanan.kuppuswamy,
peter.fang
From: Jiri Pirko <jiri@nvidia.com>
Connect the attestation family to the native TDX service. Expose
TDREPORT, Quote, MRTD and RTMR operations while preserving the existing
native interfaces.
Assisted-by: Cursor: GPT-5.6 Sol
Assisted-by: Cursor: Claude Opus 5.5
Signed-off-by: Jiri Pirko <jiri@nvidia.com>
---
Documentation/driver-api/attestation.rst | 16 ++
drivers/attestation/Kconfig | 14 ++
drivers/attestation/Makefile | 5 +
drivers/attestation/providers/tdx.c | 277 +++++++++++++++++++++++
4 files changed, 312 insertions(+)
create mode 100644 drivers/attestation/providers/tdx.c
diff --git a/Documentation/driver-api/attestation.rst b/Documentation/driver-api/attestation.rst
index 5ce121ded05f..f253c848e856 100644
--- a/Documentation/driver-api/attestation.rst
+++ b/Documentation/driver-api/attestation.rst
@@ -46,3 +46,19 @@ CTLV first drains userspace operations. The base ``pre_fini`` callback then
withdraws the instance from the kernel registry while provider state and CTLV
event emission remain available. CTLV stops events only after that callback
returns; provider and base ``fini`` callbacks may then release native storage.
+
+TDX provider
+============
+
+``CONFIG_ATTESTATION_TDX`` registers a ``ctlv-attestation-tdx`` instance for
+the native TDX guest service. MRTD has stable MR ID 0 and is read-only.
+RTMR0 through RTMR3 have IDs 1 through 4 and are readable and extendable with
+SHA-384.
+
+``EVIDENCE_GET`` supports complete raw TDREPORT and Quote objects. Both
+formats cryptographically bind all architectural TDX measurement registers,
+so successful replies carry the matching MR-journal boundary. The provider
+exposes no verdict or device-security operation.
+
+The provider consumes only ``<linux/tdx-guest.h>``. It does not call the TDX
+misc ioctl, configfs TSM-report, or TSM-MR sysfs frontends.
diff --git a/drivers/attestation/Kconfig b/drivers/attestation/Kconfig
index e2fb4b4205fe..139bcc710019 100644
--- a/drivers/attestation/Kconfig
+++ b/drivers/attestation/Kconfig
@@ -10,3 +10,17 @@ menuconfig ATTESTATION
translate the common operations to their native service.
If you are unsure, say N.
+
+if ATTESTATION
+
+config ATTESTATION_TDX
+ tristate "TDX attestation provider"
+ depends on TDX_GUEST_DRIVER
+ help
+ Exposes TDREPORT, Quote, MRTD and RTMR operations through the common
+ attestation CTLV family. The native TDX misc, TSM-report and TSM-MR
+ interfaces remain available independently.
+
+ If you are unsure, say N.
+
+endif # ATTESTATION
diff --git a/drivers/attestation/Makefile b/drivers/attestation/Makefile
index e520fee2e90d..d56df9adaa75 100644
--- a/drivers/attestation/Makefile
+++ b/drivers/attestation/Makefile
@@ -3,3 +3,8 @@ obj-$(CONFIG_ATTESTATION) += attestation.o
attestation-y := core/core.o core/ctlv.o core/events.o core/mr.o \
generated/ctlv_attestation_gen.o
+
+obj-$(CONFIG_ATTESTATION_TDX) += attestation-tdx.o
+
+attestation-tdx-y := providers/tdx.o \
+ generated/ctlv_attestation_tdx_gen.o
diff --git a/drivers/attestation/providers/tdx.c b/drivers/attestation/providers/tdx.c
new file mode 100644
index 000000000000..9b41b985c783
--- /dev/null
+++ b/drivers/attestation/providers/tdx.c
@@ -0,0 +1,277 @@
+// SPDX-License-Identifier: GPL-2.0-only
+#include <crypto/sha2.h>
+#include <linux/attestation.h>
+#include <linux/ctlv.h>
+#include <linux/err.h>
+#include <linux/module.h>
+#include <linux/slab.h>
+#include <linux/tdx-guest.h>
+
+#include "../generated/ctlv_attestation_tdx_gen.h"
+
+struct attestation_tdx_init_info {
+ struct device *parent_dev;
+ const struct tdx_guest_ops *ops;
+};
+
+struct attestation_tdx {
+ struct device *parent_dev;
+ const struct tdx_guest_ops *ops;
+};
+
+static const enum hash_algo attestation_tdx_algorithms[] = {
+ HASH_ALGO_SHA384,
+};
+
+static const struct attestation_mr_props attestation_tdx_mrs[] = {
+ {
+ .id = 0,
+ .name = "mrtd",
+ .algorithms = attestation_tdx_algorithms,
+ .algorithms_count = ARRAY_SIZE(attestation_tdx_algorithms),
+ .readable = true,
+ },
+ {
+ .id = 1,
+ .name = "rtmr0",
+ .algorithms = attestation_tdx_algorithms,
+ .algorithms_count = ARRAY_SIZE(attestation_tdx_algorithms),
+ .has_register_index = true,
+ .register_index = 0,
+ .readable = true,
+ .extendable = true,
+ },
+ {
+ .id = 2,
+ .name = "rtmr1",
+ .algorithms = attestation_tdx_algorithms,
+ .algorithms_count = ARRAY_SIZE(attestation_tdx_algorithms),
+ .has_register_index = true,
+ .register_index = 1,
+ .readable = true,
+ .extendable = true,
+ },
+ {
+ .id = 3,
+ .name = "rtmr2",
+ .algorithms = attestation_tdx_algorithms,
+ .algorithms_count = ARRAY_SIZE(attestation_tdx_algorithms),
+ .has_register_index = true,
+ .register_index = 2,
+ .readable = true,
+ .extendable = true,
+ },
+ {
+ .id = 4,
+ .name = "rtmr3",
+ .algorithms = attestation_tdx_algorithms,
+ .algorithms_count = ARRAY_SIZE(attestation_tdx_algorithms),
+ .has_register_index = true,
+ .register_index = 3,
+ .readable = true,
+ .extendable = true,
+ },
+};
+
+static const struct attestation_evidence_supported_object
+attestation_tdx_evidence_objects[] = {
+ {
+ .format = "tdx-tdreport-1.0",
+ .max_length = TDX_GUEST_REPORT_LEN,
+ .binds_mrs = true,
+ },
+ {
+ .format = "tdx-quote-1.0",
+ .max_length = TDX_GUEST_QUOTE_MAX_LEN,
+ .binds_mrs = true,
+ },
+};
+
+static const struct attestation_evidence_caps attestation_tdx_evidence_caps = {
+ .min_challenge_length = TDX_GUEST_REPORTDATA_LEN,
+ .max_challenge_length = TDX_GUEST_REPORTDATA_LEN,
+ .max_total_output_length = TDX_GUEST_QUOTE_MAX_LEN,
+ .objects = attestation_tdx_evidence_objects,
+ .objects_count = ARRAY_SIZE(attestation_tdx_evidence_objects),
+};
+
+static int
+attestation_tdx_evidence_get(struct attestation *att,
+ const struct attestation_evidence_request *request,
+ struct attestation_evidence_result *result)
+{
+ const struct attestation_evidence_object_request *object;
+ struct attestation_tdx *tdx = ctlv_priv_child(att);
+ size_t length;
+ void *buffer;
+ int ret;
+
+ if (request->objects_count != 1)
+ return -EOPNOTSUPP;
+ object = &request->objects[0];
+ buffer = kvmalloc(object->supported_object->max_length, GFP_KERNEL);
+ if (!buffer)
+ return -ENOMEM;
+ if (object->supported_object == &attestation_tdx_evidence_objects[0])
+ ret = tdx->ops->report_get(tdx->parent_dev,
+ request->challenge,
+ request->challenge_length, buffer,
+ object->supported_object->max_length, &length);
+ else if (object->supported_object ==
+ &attestation_tdx_evidence_objects[1])
+ ret = tdx->ops->quote_get(tdx->parent_dev,
+ request->challenge,
+ request->challenge_length, buffer,
+ object->supported_object->max_length, &length);
+ else
+ ret = -EOPNOTSUPP;
+ if (!ret)
+ ret = ctlv_blob_write(object->output, 0, buffer, length);
+ if (!ret)
+ result->object_lengths[0] = length;
+ kvfree(buffer);
+ return ret;
+}
+
+static int attestation_tdx_mr_read(struct attestation *att, unsigned int mr_id,
+ struct attestation_digest_set *values)
+{
+ static const enum tdx_guest_mr native_mrs[] = {
+ TDX_GUEST_MR_MRTD,
+ TDX_GUEST_MR_RTMR0,
+ TDX_GUEST_MR_RTMR1,
+ TDX_GUEST_MR_RTMR2,
+ TDX_GUEST_MR_RTMR3,
+ };
+ struct attestation_tdx *tdx = ctlv_priv_child(att);
+
+ if (mr_id >= ARRAY_SIZE(native_mrs) ||
+ values->values_count != 1 ||
+ values->values[0].algorithm != HASH_ALGO_SHA384)
+ return -EINVAL;
+ return tdx->ops->mr_read(tdx->parent_dev, native_mrs[mr_id],
+ values->values[0].data);
+}
+
+static int
+attestation_tdx_mr_extend(struct attestation *att, unsigned int mr_id,
+ const struct attestation_digest_set *values)
+{
+ struct attestation_tdx *tdx = ctlv_priv_child(att);
+
+ if (mr_id < 1 || mr_id > 4)
+ return -EOPNOTSUPP;
+ if (values->values_count != 1 ||
+ values->values[0].algorithm != HASH_ALGO_SHA384)
+ return -EINVAL;
+ return tdx->ops->rtmr_extend(tdx->parent_dev, mr_id - 1,
+ values->values[0].data);
+}
+
+static const struct attestation_provider attestation_tdx_provider = {
+ .evidence_get = attestation_tdx_evidence_get,
+ .mr_read = attestation_tdx_mr_read,
+ .mr_extend = attestation_tdx_mr_extend,
+ .evidence_caps = &attestation_tdx_evidence_caps,
+ .mrs = attestation_tdx_mrs,
+ .mrs_count = ARRAY_SIZE(attestation_tdx_mrs),
+};
+
+size_t ctlv_attestation_tdx_priv_size(void)
+{
+ return sizeof(struct attestation_tdx);
+}
+
+const void *ctlv_attestation_tdx_pre_init(const void *init_info)
+{
+ return &attestation_tdx_provider;
+}
+
+int ctlv_attestation_tdx_init(struct ctlv_device *ctlvdev,
+ const void *init_info, void *priv)
+{
+ const struct attestation_tdx_init_info *info = init_info;
+ struct attestation_tdx *tdx = priv;
+
+ if (!info || !info->parent_dev || !info->ops)
+ return -EINVAL;
+ tdx->parent_dev = info->parent_dev;
+ tdx->ops = info->ops;
+ return 0;
+}
+
+void ctlv_attestation_tdx_fini(struct ctlv_device *ctlvdev, void *priv)
+{
+}
+
+int ctlv_attestation_tdx_op_pre(void *priv, struct ctlv_op_ctx *ctx)
+{
+ return ctlv_attestation_op_pre(ctlv_priv_parent(priv), ctx);
+}
+
+void ctlv_attestation_tdx_op_post(void *priv, struct ctlv_op_ctx *ctx)
+{
+ ctlv_attestation_op_post(ctlv_priv_parent(priv), ctx);
+}
+
+int ctlv_attestation_tdx_device_info_fill(void *priv,
+ struct ctlv_op_ctx *ctx,
+ const struct ctlv_cursor *request,
+ struct ctlv_cursor *nest)
+{
+ struct attestation_tdx *tdx = priv;
+ const struct tdx_guest_info *info =
+ tdx->ops->info_get(tdx->parent_dev);
+ bool ok;
+
+ if (!info)
+ return -ENODEV;
+ ok = ctlv_attestation_tdx_info_mrconfigid_put(nest, info->mrconfigid,
+ sizeof(info->mrconfigid));
+ ok &= ctlv_attestation_tdx_info_mrowner_put(nest, info->mrowner,
+ sizeof(info->mrowner));
+ ok &= ctlv_attestation_tdx_info_mrownerconfig_put(nest,
+ info->mrownerconfig,
+ sizeof(info->mrownerconfig));
+ return ok ? 0 : -EMSGSIZE;
+}
+
+static void *attestation_tdx_add(struct device *parent_dev,
+ const struct tdx_guest_ops *ops)
+{
+ struct attestation_tdx_init_info info = {
+ .parent_dev = parent_dev,
+ .ops = ops,
+ };
+
+ return ctlv_create(parent_dev, &ctlv_attestation_tdx_family,
+ &info);
+}
+
+static void attestation_tdx_remove(void *client_priv)
+{
+ ctlv_destroy(client_priv);
+}
+
+static const struct tdx_guest_client attestation_tdx_client = {
+ .add = attestation_tdx_add,
+ .remove = attestation_tdx_remove,
+};
+
+static int __init attestation_tdx_init_module(void)
+{
+ return tdx_guest_client_register(&attestation_tdx_client);
+}
+
+static void __exit attestation_tdx_exit_module(void)
+{
+ tdx_guest_client_unregister(&attestation_tdx_client);
+}
+
+module_init(attestation_tdx_init_module);
+module_exit(attestation_tdx_exit_module);
+MODULE_DESCRIPTION("TDX attestation provider");
+MODULE_LICENSE("GPL");
+MODULE_IMPORT_NS("ATTESTATION");
+MODULE_IMPORT_NS("CTLV");
+MODULE_IMPORT_NS("TDX_GUEST");
--
2.56.0
^ permalink raw reply related [flat|nested] 32+ messages in thread* Re: [PATCH RFC 20/20] attestation: Add the TDX family implementation
2026-10-15 14:50 ` [PATCH RFC 20/20] attestation: Add the TDX family implementation Jiri Pirko
@ 2026-10-15 15:40 ` sashiko-bot
0 siblings, 0 replies; 32+ messages in thread
From: sashiko-bot @ 2026-10-15 15:40 UTC (permalink / raw)
To: Jiri Pirko; +Cc: kvm
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] mm: memory accounting bypass via missing __GFP_ACCOUNT in attestation_tdx_evidence_get
--
commit 70d54896a73342be7665edff009c1c4b80f13008
Author: Jiri Pirko <jiri@nvidia.com>
attestation: Add the TDX family implementation
This commit connects the attestation family to the native TDX service. It
exposes TDREPORT, Quote, MRTD, and RTMR operations while preserving the
existing native interfaces.
> diff --git a/drivers/attestation/providers/tdx.c b/drivers/attestation/providers/tdx.c
> new file mode 100644
> --- /dev/null
> +++ b/drivers/attestation/providers/tdx.c
[ ... ]
> +static int
> +attestation_tdx_evidence_get(struct attestation *att,
> + const struct attestation_evidence_request *request,
> + struct attestation_evidence_result *result)
> +{
> + const struct attestation_evidence_object_request *object;
> + struct attestation_tdx *tdx = ctlv_priv_child(att);
> + size_t length;
> + void *buffer;
> + int ret;
> +
> + if (request->objects_count != 1)
> + return -EOPNOTSUPP;
> + object = &request->objects[0];
> + buffer = kvmalloc(object->supported_object->max_length, GFP_KERNEL);
[Severity: Medium]
Does this allocation need the __GFP_ACCOUNT flag?
The buffer size for a TDX Quote can be up to TDX_GUEST_QUOTE_MAX_LEN
(approximately 128KB). This operation proceeds to call
tdx->ops->quote_get(), which acquires the global quote_lock mutex in the
tdx-guest driver.
If userspace issues concurrent quote requests, tasks will block on this
mutex while holding these large, unaccounted buffers. Can this allow a
container to accumulate a significant amount of kernel memory that bypasses
memcg limits, potentially leading to host out-of-memory conditions?
> + if (!buffer)
> + return -ENOMEM;
> + if (object->supported_object == &attestation_tdx_evidence_objects[0])
> + ret = tdx->ops->report_get(tdx->parent_dev,
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261015145053.3664509-1-jiri@resnulli.us?part=20
^ permalink raw reply [flat|nested] 32+ messages in thread