Linux userland API discussions
 help / color / mirror / Atom feed
From: Jiri Pirko <jiri@resnulli.us>
To: linux-coco@lists.linux.dev
Cc: linux-kernel@vger.kernel.org, linux-api@vger.kernel.org,
	x86@kernel.org, kvm@vger.kernel.org, linux-doc@vger.kernel.org,
	jgg@ziepe.ca, david.quigley@intel.com, lukas@wunner.de,
	ksinyuk@kernel.org, rodrigo.vivi@intel.com,
	cedric.xing@intel.com, sameo@meta.com, drdeeglaze@gmail.com,
	James.Bottomley@HansenPartnership.com, aik@amd.com,
	jpiotrowski@nvidia.com, qinkun@google.com, pgonda@google.com,
	jic23@kernel.org, alistair.francis@wdc.com, ankita@nvidia.com,
	suzuki.poulose@arm.com, kas@kernel.org,
	rick.p.edgecombe@intel.com, elena.reshetova@intel.com,
	dave.hansen@linux.intel.com,
	sathyanarayanan.kuppuswamy@linux.intel.com, peter.fang@intel.com
Subject: [PATCH RFC 12/20] ctlv: Add device and operation dispatch
Date: Thu, 15 Oct 2026 16:50:45 +0200	[thread overview]
Message-ID: <20261015145053.3664509-13-jiri@resnulli.us> (raw)
In-Reply-To: <20261015145053.3664509-1-jiri@resnulli.us>

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


  parent reply	other threads:[~2026-10-15 14:51 UTC|newest]

Thread overview: 21+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
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 ` [PATCH RFC 03/20] ctlv: Add the root family specification and generated files Jiri Pirko
2026-10-15 14:50 ` [PATCH RFC 04/20] Documentation: ctlv: Add userspace ABI Jiri Pirko
2026-10-15 14:50 ` [PATCH RFC 05/20] ctlv: Prepare message input handling Jiri Pirko
2026-10-15 14:50 ` [PATCH RFC 06/20] ctlv: Prepare blob descriptor validation Jiri Pirko
2026-10-15 14:50 ` [PATCH RFC 07/20] ctlv: Prepare schema validation Jiri Pirko
2026-10-15 14:50 ` [PATCH RFC 08/20] ctlv: Prepare message output handling Jiri Pirko
2026-10-15 14:50 ` [PATCH RFC 09/20] ctlv: Prepare VA blob handling Jiri Pirko
2026-10-15 14:50 ` [PATCH RFC 10/20] ctlv: Prepare event queue support Jiri Pirko
2026-10-15 14:50 ` [PATCH RFC 11/20] ctlv: Prepare runtime introspection Jiri Pirko
2026-10-15 14:50 ` Jiri Pirko [this message]
2026-10-15 14:50 ` [PATCH RFC 13/20] Documentation: ctlv: Describe how to write a family Jiri Pirko
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 ` [PATCH RFC 15/20] attestation: Prepare provider and instance core Jiri Pirko
2026-10-15 14:50 ` [PATCH RFC 16/20] attestation: Prepare measurement register journal Jiri Pirko
2026-10-15 14:50 ` [PATCH RFC 17/20] attestation: Add CTLV operation handling Jiri Pirko
2026-10-15 14:50 ` [PATCH RFC 18/20] tdx-guest: Expose the native attestation service API Jiri Pirko
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

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20261015145053.3664509-13-jiri@resnulli.us \
    --to=jiri@resnulli.us \
    --cc=James.Bottomley@HansenPartnership.com \
    --cc=aik@amd.com \
    --cc=alistair.francis@wdc.com \
    --cc=ankita@nvidia.com \
    --cc=cedric.xing@intel.com \
    --cc=dave.hansen@linux.intel.com \
    --cc=david.quigley@intel.com \
    --cc=drdeeglaze@gmail.com \
    --cc=elena.reshetova@intel.com \
    --cc=jgg@ziepe.ca \
    --cc=jic23@kernel.org \
    --cc=jpiotrowski@nvidia.com \
    --cc=kas@kernel.org \
    --cc=ksinyuk@kernel.org \
    --cc=kvm@vger.kernel.org \
    --cc=linux-api@vger.kernel.org \
    --cc=linux-coco@lists.linux.dev \
    --cc=linux-doc@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=lukas@wunner.de \
    --cc=peter.fang@intel.com \
    --cc=pgonda@google.com \
    --cc=qinkun@google.com \
    --cc=rick.p.edgecombe@intel.com \
    --cc=rodrigo.vivi@intel.com \
    --cc=sameo@meta.com \
    --cc=sathyanarayanan.kuppuswamy@linux.intel.com \
    --cc=suzuki.poulose@arm.com \
    --cc=x86@kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox