From: Jiri Pirko <jiri@resnulli.us>
To: linux-coco@lists.linux.dev
Cc: linux-kernel@vger.kernel.org, linux-api@vger.kernel.org,
x86@kernel.org, kvm@vger.kernel.org, linux-doc@vger.kernel.org,
jgg@ziepe.ca, david.quigley@intel.com, lukas@wunner.de,
ksinyuk@kernel.org, rodrigo.vivi@intel.com,
cedric.xing@intel.com, sameo@meta.com, drdeeglaze@gmail.com,
James.Bottomley@HansenPartnership.com, aik@amd.com,
jpiotrowski@nvidia.com, qinkun@google.com, pgonda@google.com,
jic23@kernel.org, alistair.francis@wdc.com, ankita@nvidia.com,
suzuki.poulose@arm.com, kas@kernel.org,
rick.p.edgecombe@intel.com, elena.reshetova@intel.com,
dave.hansen@linux.intel.com,
sathyanarayanan.kuppuswamy@linux.intel.com, peter.fang@intel.com,
jarkko@kernel.org, zohar@linux.ibm.com, yeoreum.yun@arm.com,
aneesh.kumar@kernel.org, dave.jiang@intel.com,
nik.borisov@suse.com, sergii.dmytruk@3mdeb.com
Subject: [PATCH RFC v2 11/15] ctlv: Add operation dispatch
Date: Fri, 13 Nov 2026 16:55:10 +0100 [thread overview]
Message-ID: <20261113155515.2510194-12-jiri@resnulli.us> (raw)
In-Reply-To: <20261113155515.2510194-1-jiri@resnulli.us>
From: Jiri Pirko <jiri@nvidia.com>
Let a subsystem embed a CTLV instance in a device of its own, resolve
family chains and dispatch operations through validated messages. Wire
CONFIG_CTLV and the generated root family into lib/ after all module
objects are present.
Assisted-by: Cursor: GPT-5.6 Sol
Assisted-by: Cursor: Claude Opus 5.5
Signed-off-by: Jiri Pirko <jiri@nvidia.com>
---
RFC->RFCv2:
- let the subsystem allocate the device and embed CTLV, as fwctl does
- let a subsystem route the CTLV ioctl from its own file operations
- moved to lib/ctlv/, and the device nodes to the next patch
- dropped the family lifecycle hooks and the per-family privs
- used only gotos in ctlv_op_exec()
- read struct ctlv_op_exec with copy_struct_from_user()
- took a reply buffer of any length
- made plain what the breaks in ctlv_file_open() leave
- kept each guard() on one line
- moved the function kernel-doc to the definitions
- dropped read(), poll() and the op state and removal events
- renamed ctlv_stub_fill() to ctlv_ext_fill()
- said TLV, type, record and field instead of attr
- said record instead of nest, the root being one
- reported the length an output blob needs as required-length
- separated the steps of the longer functions
- took the UAPI headers from include/uapi/ctlv/
- said message instead of wire
- registered the ioctl number, which this patch handles first
---
.../userspace-api/ioctl/ioctl-number.rst | 2 +
include/linux/ctlv.h | 33 +
include/linux/ctlv_family.h | 22 +
lib/Kconfig | 2 +
lib/Makefile | 2 +
lib/ctlv/Makefile | 4 +-
lib/ctlv/core.c | 1152 +++++++++++++++++
7 files changed, 1215 insertions(+), 2 deletions(-)
create mode 100644 lib/ctlv/core.c
diff --git a/Documentation/userspace-api/ioctl/ioctl-number.rst b/Documentation/userspace-api/ioctl/ioctl-number.rst
index dbe462040999..f257b1a22ec2 100644
--- a/Documentation/userspace-api/ioctl/ioctl-number.rst
+++ b/Documentation/userspace-api/ioctl/ioctl-number.rst
@@ -387,6 +387,8 @@ Code Seq# Include File Comments
<mailto:linux-hyperv@vger.kernel.org>
0xBA 00-0F uapi/linux/liveupdate.h Pasha Tatashin
<mailto:pasha.tatashin@soleen.com>
+0xBB 00-0F uapi/ctlv/ctlv.h CTLV
+ <mailto:jiri@resnulli.us>
0xC0 00-0F linux/usb/iowarrior.h
0xCA 00-0F uapi/misc/cxl.h Dead since 6.15
0xCA 10-2F uapi/misc/ocxl.h
diff --git a/include/linux/ctlv.h b/include/linux/ctlv.h
index 3847f8411edf..f075d0a99b80 100644
--- a/include/linux/ctlv.h
+++ b/include/linux/ctlv.h
@@ -8,17 +8,20 @@
#define _LINUX_CTLV_H
#include <linux/device.h>
+#include <linux/rwsem.h>
#include <linux/spinlock_types.h>
#include <linux/types.h>
#include <uapi/ctlv/ctlv.h>
struct ctlv_family;
+struct ctlv_file;
struct ctlv;
struct ctlv_msg;
struct ctlv_op_ctx;
struct ctlv_op_entry;
struct ctlv_schema_tree;
+struct file;
/**
* struct ctlv_cursor - One TLV position in one message.
@@ -60,6 +63,16 @@ struct ctlv_cursor {
bool progress;
};
+/**
+ * struct ctlv_hooks - What a device runs around the ops of its instance.
+ * @pre: entered before every op the core does not answer, and may refuse it
+ * @post: entered after every op whose @pre succeeded
+ */
+struct ctlv_hooks {
+ int (*pre)(struct ctlv *ctlv, struct ctlv_op_ctx *ctx);
+ void (*post)(struct ctlv *ctlv, struct ctlv_op_ctx *ctx);
+};
+
/**
* struct ctlv - The CTLV state of one device, embedded by its subsystem.
* @dev: device whose release frees the structure embedding this instance;
@@ -71,6 +84,9 @@ struct ctlv_cursor {
struct ctlv {
struct device *dev;
/* private: */
+ const struct ctlv_hooks *hooks;
+ struct rw_semaphore registration_lock;
+ bool registered;
const struct ctlv_family *family;
struct ctlv_op_entry *ops;
@@ -78,14 +94,31 @@ struct ctlv {
/* Op publication and state writers, never the dispatch path. */
spinlock_t transition_lock;
+ bool published;
+ bool destroying;
u64 state_generation;
u32 dynamic_ops_count;
unsigned long *disabled;
};
+int ctlv_init(struct ctlv *ctlv, struct device *dev,
+ const struct ctlv_family *family, const struct ctlv_hooks *hooks);
+void ctlv_publish(struct ctlv *ctlv);
+void ctlv_unpublish(struct ctlv *ctlv);
+void ctlv_fini(struct ctlv *ctlv);
+bool ctlv_family_inherits(const struct ctlv_family *family,
+ const struct ctlv_family *ancestor);
+
+struct ctlv_file *ctlv_file_open(struct ctlv *ctlv, struct file *filp);
+long ctlv_file_ioctl(struct ctlv_file *cfile, unsigned int cmd,
+ unsigned long arg);
+void ctlv_file_close(struct ctlv_file *cfile);
+
void ctlv_record_end(struct ctlv_cursor *child);
void ctlv_record_cancel(struct ctlv_cursor *child);
+int ctlv_ext_fill(struct ctlv_op_ctx *ctx, const struct ctlv_cursor *request,
+ struct ctlv_cursor *record);
int ctlv_op_error(struct ctlv_op_ctx *ctx, int error, const char *message);
__printf(3, 4)
int ctlv_op_errorf(struct ctlv_op_ctx *ctx, int error, const char *fmt, ...);
diff --git a/include/linux/ctlv_family.h b/include/linux/ctlv_family.h
index 61ae5b7cec83..d8abdd4964c9 100644
--- a/include/linux/ctlv_family.h
+++ b/include/linux/ctlv_family.h
@@ -428,6 +428,9 @@ extern const struct ctlv_family ctlv_family;
u32 ctlv_enum_ext_max(const struct ctlv_family *family, u16 family_id, u32 ext_id);
+void ctlv_op_provide(struct ctlv *ctlv, u64 op_id);
+void ctlv_op_state_set(struct ctlv *ctlv, u64 op_id, bool disabled);
+
struct ctlv_cursor *
ctlv_input_field(struct ctlv_cursor *cursor, const struct ctlv_cursor *record,
struct ctlv_input_ref ref);
@@ -453,6 +456,25 @@ bool ctlv_record_start(struct ctlv_cursor *record, struct ctlv_cursor *child,
struct ctlv_blob *ctlv_blob_resolve(struct ctlv_op_ctx *ctx,
const struct ctlv_cursor *blob);
+
+#define CTLV_OP_PROVIDE(_name, _op_id) \
+static inline void _name##_provide(struct ctlv *ctlv) \
+{ \
+ ctlv_op_provide(ctlv, (_op_id)); \
+}
+
+#define CTLV_OP_ENABLE(_name, _op_id) \
+static inline void _name##_enable(struct ctlv *ctlv) \
+{ \
+ ctlv_op_state_set(ctlv, (_op_id), false); \
+}
+
+#define CTLV_OP_DISABLE(_name, _op_id) \
+static inline void _name##_disable(struct ctlv *ctlv) \
+{ \
+ ctlv_op_state_set(ctlv, (_op_id), true); \
+}
+
#define __CTLV_DEFINE_PUT(_name, _ctype, _put_fn, _type, _kind) \
static inline bool _name##_put(struct ctlv_cursor *record, \
_ctype value) \
diff --git a/lib/Kconfig b/lib/Kconfig
index 1e42f5167c68..7d81e1d502d1 100644
--- a/lib/Kconfig
+++ b/lib/Kconfig
@@ -496,6 +496,8 @@ source "lib/vdso/Kconfig"
source "lib/fonts/Kconfig"
+source "lib/ctlv/Kconfig"
+
config SG_SPLIT
def_bool n
help
diff --git a/lib/Makefile b/lib/Makefile
index a2afe954805c..db7e4fcc2c6f 100644
--- a/lib/Makefile
+++ b/lib/Makefile
@@ -342,6 +342,8 @@ obj-$(CONFIG_OBJAGG) += objagg.o
# pldmfw library
obj-$(CONFIG_PLDMFW) += pldmfw/
+obj-$(CONFIG_CTLV) += ctlv/
+
obj-$(CONFIG_GENERIC_LIB_DEVMEM_IS_ALLOWED) += devmem_is_allowed.o
obj-$(CONFIG_FIRMWARE_TABLE) += fw_table.o
diff --git a/lib/ctlv/Makefile b/lib/ctlv/Makefile
index 76010d52dd8a..73acdbdf387d 100644
--- a/lib/ctlv/Makefile
+++ b/lib/ctlv/Makefile
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: GPL-2.0
obj-$(CONFIG_CTLV) += ctlv.o
-ctlv-y += blob.o blob_desc.o builder.o introspection.o message.o \
- schema.o
+ctlv-y += blob.o blob_desc.o builder.o core.o introspection.o \
+ message.o schema.o ctlv_gen.o
diff --git a/lib/ctlv/core.c b/lib/ctlv/core.c
new file mode 100644
index 000000000000..605f191de77b
--- /dev/null
+++ b/lib/ctlv/core.c
@@ -0,0 +1,1152 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES
+ */
+#define pr_fmt(fmt) "ctlv: " fmt
+#include <linux/bitmap.h>
+#include <linux/bitops.h>
+#include <linux/build_bug.h>
+#include <linux/cleanup.h>
+#include <linux/container_of.h>
+#include <linux/ctlv.h>
+#include <linux/ctlv_gen.h>
+#include <linux/device.h>
+#include <linux/err.h>
+#include <linux/fs.h>
+#include <linux/kobject.h>
+#include <linux/list.h>
+#include <linux/lockdep.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/overflow.h>
+#include <linux/slab.h>
+#include <linux/stddef.h>
+#include <linux/string.h>
+#include <linux/uaccess.h>
+#include <linux/vmalloc.h>
+
+#include <uapi/ctlv/ctlv.h>
+
+#include "blob.h"
+#include "builder.h"
+#include "internal.h"
+#include "schema.h"
+
+/*
+ * One layout on every ABI, including compat. Alignment is asserted too because
+ * a 32-bit ABI aligns a plain __u64 to four bytes.
+ */
+static_assert(sizeof(struct ctlv_hdr) == 16);
+static_assert(__alignof__(struct ctlv_hdr) == CTLV_ALIGNTO);
+
+static_assert(sizeof(struct ctlv_op_exec) == 56);
+static_assert(__alignof__(struct ctlv_op_exec) == CTLV_ALIGNTO);
+
+/* Claims persist across unload so one ID cannot later name another family. */
+struct ctlv_family_claim {
+ struct list_head entry;
+ char *name;
+ u16 family_id;
+};
+
+static LIST_HEAD(ctlv_family_claims);
+static DEFINE_MUTEX(ctlv_family_claims_lock);
+
+static int ctlv_family_claim_add(const struct ctlv_family_schema *schema)
+{
+ struct ctlv_family_claim *claim;
+
+ guard(mutex)(&ctlv_family_claims_lock);
+ list_for_each_entry(claim, &ctlv_family_claims, entry) {
+ bool same_id = claim->family_id == schema->family_id;
+ bool same_name = !strcmp(claim->name, schema->name);
+
+ if (same_id && same_name)
+ return 0;
+ if (same_id || same_name) {
+ pr_err("family %s wants ID %u, which belongs to %s\n", schema->name,
+ schema->family_id, claim->name);
+ return -EEXIST;
+ }
+ }
+
+ claim = kzalloc_obj(*claim, GFP_KERNEL);
+ if (!claim)
+ return -ENOMEM;
+ claim->name = kstrdup(schema->name, GFP_KERNEL);
+ if (!claim->name) {
+ kfree(claim);
+ return -ENOMEM;
+ }
+ claim->family_id = schema->family_id;
+ list_add_tail(&claim->entry, &ctlv_family_claims);
+ return 0;
+}
+
+static void ctlv_family_claims_free(void)
+{
+ struct ctlv_family_claim *claim, *next;
+
+ list_for_each_entry_safe(claim, next, &ctlv_family_claims, entry) {
+ list_del(&claim->entry);
+ kfree(claim->name);
+ kfree(claim);
+ }
+}
+
+/*
+ * Everything resolved below is generated, so a failure here is a bug, checked
+ * anyway because an out-of-tree family comes from another generator.
+ */
+
+static const char *ctlv_family_name(const struct ctlv_family *family)
+{
+ if (!family->schema || !family->schema->name)
+ return "(unnamed)";
+ return family->schema->name;
+}
+
+static int ctlv_family_bad(const struct ctlv_family *family, const char *what)
+{
+ pr_err("family %s: %s\n", ctlv_family_name(family), what);
+ return -EINVAL;
+}
+
+static int ctlv_op_bad(const struct ctlv_family *family, u64 op_id, const char *what)
+{
+ pr_err("family %s: op %#018llx %s\n", ctlv_family_name(family), op_id, what);
+ return -EINVAL;
+}
+
+/*
+ * A family is named its parent's whole name plus a suffix, so one name states
+ * the chain it belongs to and every family of this core is ctlv-something.
+ */
+static int ctlv_family_chain_check(const struct ctlv_family *family)
+{
+ const struct ctlv_family *child;
+
+ for (child = family; child != &ctlv_family; child = child->parent) {
+ const struct ctlv_family *parent;
+ size_t len;
+
+ if (!child)
+ return ctlv_family_bad(family, "heads no chain of the core's");
+ parent = child->parent;
+ if (!child->schema || !parent || !parent->schema)
+ return ctlv_family_bad(child, "does not name a complete parent descriptor");
+ if (!child->schema->name || !parent->schema->name)
+ return ctlv_family_bad(family, "inherits a family with no name");
+ if (parent->schema->family_id >= child->schema->family_id)
+ return ctlv_family_bad(child, "does not follow its parent in the registry");
+ len = strlen(parent->schema->name);
+ if (!len || strncmp(child->schema->name, parent->schema->name, len) ||
+ child->schema->name[len] != '-')
+ return ctlv_family_bad(family, "is not named after the family it inherits");
+ }
+ return 0;
+}
+
+/* One descriptor of a chain: what it answers with, and what it fills. */
+static int ctlv_family_descriptor_check(const struct ctlv_family *family)
+{
+ const struct ctlv_family_schema *schema = family->schema;
+ u32 index;
+
+ /*
+ * A family that declares no op of its own binds nothing: what it
+ * inherited is answered where it was declared.
+ */
+ if (!schema || !schema->name || !*schema->name || (family->ops_count && !family->ops))
+ return ctlv_family_bad(family, "descriptor is incomplete");
+ /* Every device answers the root family already, as the core. */
+ if (schema->family_id == CTLV_FAMILY_ROOT && family != &ctlv_family)
+ return ctlv_family_bad(family, "claims the root family ID");
+ /* An extension point is answered by whichever family filled it. */
+ if (family->ext_ops_count != schema->ext_fill_schemas_count)
+ return ctlv_family_bad(family, "answers other extension points than it fills");
+ for (index = 0; index < family->ext_ops_count; index++) {
+ const struct ctlv_ext_fill_schema *fill_schema = &schema->ext_fill_schemas[index];
+ const struct ctlv_ext_ops *ext_ops = &family->ext_ops[index];
+
+ if (ext_ops->family_id == fill_schema->family_id &&
+ ext_ops->ext_id == fill_schema->ext_id)
+ continue;
+ return ctlv_family_bad(family, "answers an extension point it does not fill");
+ }
+ return 0;
+}
+
+/* Every descriptor binds its local schema. */
+static int ctlv_family_descriptors_check(const struct ctlv_family *family)
+{
+ const struct ctlv_family *descriptor;
+
+ for (descriptor = family; descriptor; descriptor = descriptor->parent) {
+ int ret;
+
+ ret = ctlv_family_descriptor_check(descriptor);
+ if (ret)
+ return ret;
+ }
+ return 0;
+}
+
+/* The graph a device answers over, and the descriptors standing on it. */
+static int ctlv_family_check(const struct ctlv_family *family)
+{
+ int ret;
+
+ if (!family->schema)
+ return ctlv_family_bad(family, "descriptor is incomplete");
+ ret = ctlv_family_chain_check(family);
+ if (ret)
+ return ret;
+ return ctlv_family_descriptors_check(family);
+}
+
+/* What one walk of the chain has accounted for so far. */
+struct ctlv_op_resolution {
+ const struct ctlv_family *target;
+ const struct ctlv_family *family;
+ u32 bound;
+ u32 dynamic;
+ u64 previous;
+};
+
+/* Every binding of one family answers an op, so a spare one answers nothing. */
+static int ctlv_op_resolution_family_bound(const struct ctlv_op_resolution *state)
+{
+ const struct ctlv_family *family = state->family;
+
+ if (!family || state->bound == family->ops_count)
+ return 0;
+ /* The core's own table is this core's bug, not a family's. */
+ if (WARN_ON_ONCE(family == &ctlv_family))
+ return -EINVAL;
+ return ctlv_family_bad(family, "binds more than its schema asks for");
+}
+
+/*
+ * The ops of one family are contiguous in the walk, so the family before this
+ * one is complete as soon as another family's op arrives.
+ */
+static int ctlv_op_resolution_family_enter(struct ctlv_op_resolution *state,
+ const struct ctlv_family *family)
+{
+ int ret;
+
+ if (state->family == family)
+ return 0;
+ ret = ctlv_op_resolution_family_bound(state);
+ if (ret)
+ return ret;
+ state->family = family;
+ state->bound = 0;
+ return 0;
+}
+
+static int ctlv_op_resolution_add(struct ctlv_op_resolution *state, const struct ctlv_family *owner,
+ const struct ctlv_op_schema *op, struct ctlv_op_entry *entry)
+{
+ const struct ctlv_family *target = state->target;
+ const struct ctlv_family *family = state->family;
+ int ret;
+
+ entry->schema = op;
+ entry->state_index = CTLV_OP_STATE_NONE;
+
+ /*
+ * An op ID is its family's ID and a number within it, so a walk from
+ * the oldest ancestor down ascends, which covers uniqueness too.
+ */
+ if (!CTLV_OP_NUMBER(op->op_id) || CTLV_OP_FAMILY(op->op_id) != owner->schema->family_id)
+ return ctlv_op_bad(target, op->op_id, "belongs to another family");
+ if (op->op_id <= state->previous)
+ return ctlv_op_bad(target, op->op_id, "is out of order");
+ state->previous = op->op_id;
+
+ switch (op->type) {
+ case CTLV_OP_TYPE_ACTION:
+ case CTLV_OP_TYPE_QUERY:
+ if (op->fd_access != CTLV_OP_FD_ACCESS_READ &&
+ op->fd_access != CTLV_OP_FD_ACCESS_READ_WRITE)
+ return ctlv_op_bad(family, op->op_id, "has an unknown fd access");
+ /* The family that declared the op is the one that answers it. */
+ if (state->bound >= family->ops_count)
+ return ctlv_op_bad(family, op->op_id, "has nothing bound to it");
+ entry->binding = &family->ops[state->bound++];
+ /* Required even for an op this device disables or withholds. */
+ if (!entry->binding->action)
+ return ctlv_op_bad(family, op->op_id, "has no callback");
+ /* An optional op arrives absent, for the opt-in to clear. */
+ entry->absent = op->optional;
+ /* An op has a whole pair of hooks of its own, or none. */
+ if (!entry->binding->pre != !entry->binding->post)
+ return ctlv_op_bad(family, op->op_id, "has half a hook pair");
+ /* No message carries a pair, so this is the only check of it. */
+ if (!entry->binding->pre != !op->pre_post)
+ return ctlv_op_bad(family, op->op_id, "disagrees about its hook pair");
+ break;
+ default:
+ return ctlv_op_bad(target, op->op_id, "has an unknown type");
+ }
+
+ switch (op->availability) {
+ case CTLV_OP_AVAILABILITY_FIXED:
+ break;
+ case CTLV_OP_AVAILABILITY_DYNAMIC:
+ /*
+ * Numbered from zero as the walk meets them, per device: two
+ * families of one chain need not have the same ops.
+ */
+ entry->state_index = state->dynamic++;
+ break;
+ default:
+ return ctlv_op_bad(target, op->op_id, "has an unknown availability");
+ }
+
+ /*
+ * Once per device: what an inherited op may carry depends on the
+ * extension points this family filled and on nothing that changes later.
+ */
+ ret = ctlv_schema_op_sizes(target, op, &entry->sizes);
+ if (ret)
+ return ctlv_op_bad(target, op->op_id, "needs messages this core cannot carry");
+ return 0;
+}
+
+static int ctlv_op_resolution_init(struct ctlv *ctlv, const struct ctlv_family *family)
+{
+ struct ctlv_op_resolution state = {
+ .target = family,
+ };
+ const struct ctlv_family *owner;
+ const struct ctlv_op_schema *op;
+ struct ctlv_schema_cursor cursor;
+ u32 ops_count = 0;
+ int ret;
+
+ ret = ctlv_family_check(family);
+ if (ret)
+ return ret;
+
+ ctlv_schema_cursor_init(&cursor, family);
+ while (ctlv_schema_next_op(&cursor, &owner))
+ ops_count++;
+ if (!ops_count)
+ return ctlv_family_bad(family, "has an effective schema with no op");
+
+ ctlv->ops = kcalloc(ops_count, sizeof(*ctlv->ops), GFP_KERNEL);
+ if (!ctlv->ops)
+ return -ENOMEM;
+ ctlv->ops_count = ops_count;
+
+ ops_count = 0;
+ ctlv_schema_cursor_init(&cursor, family);
+ while ((op = ctlv_schema_next_op(&cursor, &owner))) {
+ ret = ctlv_op_resolution_family_enter(&state, owner);
+ if (ret)
+ return ret;
+ ret = ctlv_op_resolution_add(&state, owner, op, &ctlv->ops[ops_count++]);
+ if (ret)
+ return ret;
+ }
+ /* The device family, no op of another having ended its table. */
+ ret = ctlv_op_resolution_family_bound(&state);
+ if (ret)
+ return ret;
+ ctlv->dynamic_ops_count = state.dynamic;
+ return 0;
+}
+
+/**
+ * ctlv_family_inherits - Check a family's ancestry.
+ * @family: family
+ * @ancestor: family that may be @family or one of its ancestors
+ *
+ * Return: whether @family is @ancestor or inherits from it.
+ */
+bool ctlv_family_inherits(const struct ctlv_family *family,
+ const struct ctlv_family *ancestor)
+{
+ for (; family; family = family->parent)
+ if (family == ancestor)
+ return true;
+ return false;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_family_inherits, "CTLV");
+
+/*
+ * Dispatch reads the availability bit under no lock, so an op already admitted
+ * is neither cancelled nor waited for by a disable.
+ */
+
+struct ctlv_op_entry *ctlv_op_find(struct ctlv *ctlv, u64 op_id)
+{
+ u32 index;
+
+ /* The index ascends by op ID, so a miss stops early. */
+ for (index = 0; index < ctlv->ops_count; index++) {
+ if (ctlv->ops[index].schema->op_id == op_id)
+ return &ctlv->ops[index];
+ if (ctlv->ops[index].schema->op_id > op_id)
+ break;
+ }
+ return NULL;
+}
+
+static bool ctlv_op_disabled(struct ctlv *ctlv, const struct ctlv_op_entry *op)
+{
+ if (op->state_index == CTLV_OP_STATE_NONE)
+ return false;
+ /* Acquire, so what the family prepared before enabling is visible. */
+ return test_bit_acquire(op->state_index, ctlv->disabled);
+}
+
+static void ctlv_op_publish_all(struct ctlv *ctlv)
+{
+ u32 published = 0;
+ u32 index;
+
+ /* Against a family still opting in as this is called. */
+ guard(spinlock)(&ctlv->transition_lock);
+ for (index = 0; index < ctlv->ops_count; index++) {
+ if (ctlv->ops[index].absent)
+ continue;
+ ctlv->ops[published++] = ctlv->ops[index];
+ }
+ ctlv->ops_count = published;
+ ctlv->published = true;
+}
+
+void ctlv_op_provide(struct ctlv *ctlv, u64 op_id)
+{
+ struct ctlv_op_entry *op;
+
+ guard(spinlock)(&ctlv->transition_lock);
+ if (WARN_ON_ONCE(ctlv->published || ctlv->destroying))
+ return;
+ op = ctlv_op_find(ctlv, op_id);
+ if (WARN_ON_ONCE(!op))
+ return;
+ op->absent = false;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_op_provide, "CTLV");
+
+void ctlv_op_state_set(struct ctlv *ctlv, u64 op_id, bool disabled)
+{
+ const struct ctlv_op_entry *op;
+
+ /*
+ * Also serializes lookup against publication, which compacts the
+ * array, and against teardown stopping state changes.
+ */
+ guard(spinlock)(&ctlv->transition_lock);
+ if (ctlv->destroying)
+ return;
+
+ op = ctlv_op_find(ctlv, op_id);
+ if (WARN_ON_ONCE(!op) || WARN_ON_ONCE(op->state_index == CTLV_OP_STATE_NONE))
+ return;
+ /* An op this device never took on has no state to change. */
+ if (WARN_ON_ONCE(op->absent))
+ return;
+
+ /*
+ * Fully ordered, releasing what the family prepared to the acquire in
+ * ctlv_op_disabled(). The old state makes a repeat change nothing.
+ */
+ if (disabled ? test_and_set_bit(op->state_index, ctlv->disabled) :
+ !test_and_clear_bit(op->state_index, ctlv->disabled))
+ return;
+
+ if (!ctlv->published)
+ return;
+ ctlv->state_generation++;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_op_state_set, "CTLV");
+
+static int ctlv_op_check_mode(const struct file *filp, const struct ctlv_op_entry *op)
+{
+ switch (op->schema->fd_access) {
+ case CTLV_OP_FD_ACCESS_READ:
+ return filp->f_mode & FMODE_READ ? 0 : -EACCES;
+ case CTLV_OP_FD_ACCESS_READ_WRITE:
+ return (filp->f_mode & (FMODE_READ | FMODE_WRITE)) == (FMODE_READ | FMODE_WRITE) ?
+ 0 :
+ -EACCES;
+ default:
+ /* Registration rejects this before the device is visible. */
+ return -EACCES;
+ }
+}
+
+static u32 ctlv_reply_needed(const struct ctlv_op_entry *op)
+{
+ if (op->schema->type == CTLV_OP_TYPE_QUERY)
+ return op->sizes.query_reply_min_len;
+ return op->sizes.action_reply_max_len;
+}
+
+/*
+ * request_len is the copy boundary; root length frames the message. Ignored
+ * unknown TLVs may exceed the schema maximum up to the transport limit.
+ */
+static int ctlv_request_frame(struct ctlv_op_ctx *ctx, const struct ctlv_op_exec *exec,
+ const struct ctlv_hdr *root)
+{
+ if (root->len < sizeof(*root) || root->len > exec->request_len ||
+ root->type != CTLV_TYPE_ROOT || root->flags != CTLV_HDR_F_RECORD) {
+ ctx->error.reason = CTLV_VALIDATION_REASON_MALFORMED_MESSAGE;
+ return -EINVAL;
+ }
+ return 0;
+}
+
+static int ctlv_request_validate(struct ctlv_op_ctx *ctx, const struct ctlv_op_exec *exec,
+ struct ctlv_hdr *root)
+{
+ int ret;
+
+ ret = ctlv_request_frame(ctx, exec, root);
+ if (ret)
+ return ret;
+ if (!ctx->op->schema->request.node_schemas)
+ return 0;
+
+ ctx->request = (struct ctlv_msg){
+ .root = root,
+ .len = exec->request_len,
+ .tree = &ctx->op->schema->request,
+ .family = ctx->family,
+ .request = true,
+ };
+ return ctlv_schema_validate(&ctx->request, &ctx->error);
+}
+
+/*
+ * An action is given exactly the maximum its schema allows, so running out of
+ * room is this kernel's mistake. A query is given whatever the caller offered,
+ * up to the longest fragment this core builds.
+ */
+static u32 ctlv_reply_capacity(const struct ctlv_op_entry *op, const struct ctlv_op_exec *exec)
+{
+ if (!op->schema->reply.node_schemas)
+ return 0;
+ if (op->schema->type == CTLV_OP_TYPE_QUERY)
+ return min(exec->reply_len, CTLV_MAX_INLINE_MESSAGE_LEN);
+ return op->sizes.action_reply_max_len;
+}
+
+/* Actions and final query fragments must be complete schema-valid replies. */
+static int ctlv_reply_finish(struct ctlv_op_ctx *ctx)
+{
+ struct ctlv_error refused = {};
+
+ if (!ctx->output.root)
+ return 0;
+ /* Whatever poisoned it has warned about it already. */
+ if (ctx->output.poison)
+ return ctx->output.poison;
+ /* A query was held to its open records by the ending it came to. */
+ if (ctx->op->schema->type == CTLV_OP_TYPE_QUERY) {
+ if (ctx->frag.cursor || ctx->frag.next_cursor)
+ return 0;
+ } else if (WARN_ON_ONCE(ctx->deepest != &ctx->output_root)) {
+ /* A cursor of a returned frame is reset, never followed. */
+ ctx->deepest = &ctx->output_root;
+ return -EINVAL;
+ }
+ if (WARN_ON_ONCE(ctlv_schema_validate(&ctx->output, &refused)))
+ return -EINVAL;
+ return 0;
+}
+
+/* A successful query callback must end exactly one fragment. */
+static int ctlv_query_fragment_result(struct ctlv_op_ctx *ctx)
+{
+ if (WARN_ON_ONCE(!ctx->frag.answered))
+ return -EINVAL;
+ return ctx->frag.result;
+}
+
+static int ctlv_op_call(struct ctlv_op_ctx *ctx)
+{
+ const struct ctlv_op_entry *op = ctx->op;
+ struct ctlv_cursor *request = NULL;
+ struct ctlv_cursor *reply = NULL;
+ int blob_ret;
+ int ret;
+
+ if (ctx->request.root) {
+ ctlv_message_root(&ctx->request_root, &ctx->request);
+ request = &ctx->request_root;
+ }
+ if (ctx->output.root)
+ reply = &ctx->output_root;
+
+ ctlv_blob_context_init(ctx);
+ if (op->schema->type == CTLV_OP_TYPE_ACTION) {
+ ret = op->binding->action(ctx->ctlv, ctx, request, reply);
+ } else {
+ ret = op->binding->query(ctx->ctlv, ctx, request, reply);
+ if (!ret)
+ ret = ctlv_query_fragment_result(ctx);
+ }
+ blob_ret = ctlv_blob_context_finish(ctx);
+ if (!ret)
+ ret = blob_ret;
+ if (ret)
+ return ret;
+ return ctlv_reply_finish(ctx);
+}
+
+/*
+ * The device's hooks around the declaring op's, except around a core op. The
+ * availability check is innermost.
+ */
+static int ctlv_op_dispatch(struct ctlv_op_ctx *ctx)
+{
+ const struct ctlv_hooks *hooks = ctx->ctlv->hooks;
+ const struct ctlv_op *op = ctx->op->binding;
+ struct ctlv *ctlv = ctx->ctlv;
+ int ret;
+
+ if (CTLV_OP_FAMILY(ctx->op->schema->op_id) == CTLV_FAMILY_ROOT)
+ hooks = NULL;
+ if (hooks) {
+ ret = hooks->pre(ctlv, ctx);
+ if (ret)
+ return ret;
+ }
+
+ if (op->pre) {
+ ret = op->pre(ctlv, ctx);
+ if (ret)
+ goto out;
+ }
+
+ /* The only authoritative check: introspection is advisory. */
+ if (ctlv_op_disabled(ctlv, ctx->op))
+ ret = -EOPNOTSUPP;
+ else
+ ret = ctlv_op_call(ctx);
+
+ if (op->post)
+ op->post(ctlv, ctx);
+out:
+ if (hooks)
+ hooks->post(ctlv, ctx);
+ return ret;
+}
+
+/* Error information replaces the reply atomically; errno remains authoritative. */
+static void ctlv_error_info(struct ctlv_op_ctx *ctx, u32 room, void **reply, u32 *reply_len)
+{
+ const struct ctlv_error *error = &ctx->error;
+ struct ctlv_cursor *root = &ctx->output_root;
+ void *buf;
+
+ if ((!error->reason && !error->message[0]) || !room)
+ return;
+ buf = kvmalloc(CTLV_ERROR_INFO_MAX_LEN, GFP_KERNEL_ACCOUNT);
+ if (!buf)
+ return;
+
+ /* The same builder over another schema, and a buffer of its own. */
+ ctlv_message_output_init(&ctx->output, ctx, buf, CTLV_ERROR_INFO_MAX_LEN,
+ &ctlv_error_info_schema);
+ ctlv_message_root(root, &ctx->output);
+ ctx->deepest = root;
+
+ if (error->reason)
+ ctlv_put_u64(root, CTLV_T_ERROR_INFO_REASON, CTLV_SCHEMA_ENUM, error->reason);
+ if (error->message[0])
+ ctlv_error_info_message_put(root, error->message);
+ if (error->reason == CTLV_VALIDATION_REASON_STALE_GENERATION) {
+ /* The one failure that is about no part of any message. */
+ ctlv_put_u64(root, CTLV_T_ERROR_INFO_EXPECTED_GENERATION, CTLV_SCHEMA_UINT,
+ error->expected_generation);
+ ctlv_put_u64(root, CTLV_T_ERROR_INFO_CURRENT_GENERATION, CTLV_SCHEMA_UINT,
+ error->current_generation);
+ } else if (error->reason) {
+ ctlv_put_u64(root, CTLV_T_ERROR_INFO_BAD_TLV_OFFSET, CTLV_SCHEMA_UINT,
+ error->bad_tlv_offset);
+ ctlv_put_u64(root, CTLV_T_ERROR_INFO_RECORD_OFFSET, CTLV_SCHEMA_UINT,
+ error->record_offset);
+ /* No field's type is zero, so a zero names nothing. */
+ if (error->missing_type)
+ ctlv_put_u64(root, CTLV_T_ERROR_INFO_MISSING_TYPE, CTLV_SCHEMA_UINT,
+ error->missing_type);
+ if (error->required_length)
+ ctlv_put_u64(root, CTLV_T_ERROR_INFO_REQUIRED_LENGTH, CTLV_SCHEMA_UINT,
+ error->required_length);
+ }
+
+ if (ctx->output.poison || ctx->output.root->len > room) {
+ kvfree(buf);
+ return;
+ }
+ *reply = buf;
+ *reply_len = ctx->output.root->len;
+}
+
+struct ctlv_file {
+ struct ctlv *ctlv;
+ /* The device file, whose release frees this. */
+ struct file *filp;
+};
+
+/*
+ * Everything that needs the device to still have a family. @reply is built
+ * here and becomes the caller's to free, to copy out once the family is gone.
+ */
+static int ctlv_op_exec(struct ctlv_file *cfile, struct ctlv_op_exec *exec,
+ void *snapshot, void **reply, u32 *reply_len)
+{
+ struct ctlv *ctlv = cfile->ctlv;
+ const struct ctlv_op_entry *op;
+ struct ctlv_op_ctx ctx = {};
+ void *out = NULL;
+ u32 capacity;
+ int ret;
+
+ /* No family numbers an op zero, so a zero here is an unset field. */
+ if (!exec->op_id)
+ return -EINVAL;
+ op = ctlv_op_find(ctlv, exec->op_id);
+ if (!op)
+ return -EOPNOTSUPP;
+ ret = ctlv_op_check_mode(cfile->filp, op);
+ if (ret)
+ return ret;
+
+ /*
+ * Only a query resumes. A cursor says which fragment this is, so a
+ * generation without one continues what was never started.
+ */
+ if (op->schema->type == CTLV_OP_TYPE_QUERY) {
+ if (!exec->cursor && exec->generation)
+ return -EINVAL;
+ ctx.frag.generation = exec->generation;
+ ctx.frag.cursor = exec->cursor;
+ } else if (exec->generation || exec->cursor) {
+ return -EINVAL;
+ }
+
+ /* Before the callback, so a buffer too small mutates nothing. */
+ if (exec->reply_len < ctlv_reply_needed(op))
+ return -EMSGSIZE;
+
+ ctx.ctlv = ctlv;
+ ctx.family = ctlv->family;
+ /* Whose memory a blob descriptor of this request describes. */
+ ctx.submitter = current;
+ ctx.op = op;
+
+ /* Before any hook, so that no family code sees an unchecked TLV. */
+ ret = ctlv_request_validate(&ctx, exec, snapshot);
+ if (ret)
+ goto out_error_info;
+
+ capacity = ctlv_reply_capacity(op, exec);
+ if (capacity) {
+ /*
+ * Not zeroed: a byte is copied out only once the root's
+ * length covers it, padding included.
+ */
+ out = kvmalloc(capacity, GFP_KERNEL_ACCOUNT);
+ if (!out) {
+ ret = -ENOMEM;
+ goto out_request;
+ }
+ ctlv_message_output_init(&ctx.output, &ctx, out, capacity, &op->schema->reply);
+ ctlv_message_root(&ctx.output_root, &ctx.output);
+ ctx.deepest = &ctx.output_root;
+ }
+
+ ret = ctlv_op_dispatch(&ctx);
+ if (ret)
+ goto out_error_info;
+
+ /* Where the caller carries on from, zero for anything but a query. */
+ exec->generation = ctx.frag.reported;
+ exec->cursor = ctx.frag.next_cursor;
+ *reply_len = ctx.output.root ? ctx.output.root->len : 0;
+ *reply = out;
+ out = NULL;
+ ret = 0;
+ goto out_request;
+
+out_error_info:
+ ctlv_error_info(&ctx, exec->reply_len, reply, reply_len);
+out_request:
+ ctlv_message_index_free(&ctx.request);
+ kvfree(out);
+ return ret;
+}
+
+/* What one family of this device's chain does with one extension point. */
+static const struct ctlv_ext_ops *ctlv_ext_ops_find(const struct ctlv *ctlv,
+ const struct ctlv_ext_schema *ext_schema)
+{
+ const struct ctlv_family *family;
+ u32 index;
+
+ /* From the device family towards the core, where the fill was found. */
+ for (family = ctlv->family; family; family = family->parent)
+ for (index = 0; index < family->ext_ops_count; index++)
+ if (family->ext_ops[index].family_id == ext_schema->family_id &&
+ family->ext_ops[index].ext_id == ext_schema->ext_id)
+ return &family->ext_ops[index];
+ return NULL;
+}
+
+/**
+ * ctlv_ext_fill - Delegate an extension-point output record.
+ * @ctx: operation context
+ * @request: validated request root
+ * @record: output record selected by the generated helper
+ *
+ * Return: filling callback result, or 0 for an unfilled point.
+ */
+int ctlv_ext_fill(struct ctlv_op_ctx *ctx, const struct ctlv_cursor *request,
+ struct ctlv_cursor *record)
+{
+ const struct ctlv_schema_tree *tree;
+ u32 index;
+
+ if (WARN_ON_ONCE(!record->tree))
+ return -EINVAL;
+ tree = record->tree;
+
+ for (index = 0; index < tree->ext_schemas_count; index++) {
+ const struct ctlv_ext_schema *ext_schema = &tree->ext_schemas[index];
+ const struct ctlv_cursor *filled_request = request;
+ const struct ctlv_ext_fill_schema *fill_schema;
+ const struct ctlv_ext_ops *ext_ops;
+ struct ctlv_cursor filled;
+ int ret;
+
+ if (ext_schema->node != record->node)
+ continue;
+ ext_ops = ctlv_ext_ops_find(ctx->ctlv, ext_schema);
+ if (!ext_ops || !ext_ops->fill)
+ continue;
+
+ if (request) {
+ fill_schema = ctlv_schema_fill(ctx->family, ext_schema->family_id,
+ ext_schema->ext_id);
+ if (WARN_ON_ONCE(!fill_schema))
+ return -EINVAL;
+ filled = *request;
+ if (ctlv_message_input_rebind(&filled, &fill_schema->members, 0))
+ filled_request = &filled;
+ else
+ filled_request = NULL;
+ }
+
+ ret = ext_ops->fill(ctx->ctlv, ctx, filled_request, record);
+ if (ret)
+ return ret;
+ }
+ return 0;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_ext_fill, "CTLV");
+
+static long ctlv_op_exec_ioctl(struct ctlv_file *cfile, unsigned long arg)
+{
+ struct ctlv_op_exec __user *uexec = (struct ctlv_op_exec __user *)arg;
+ struct ctlv *ctlv = cfile->ctlv;
+ struct ctlv_op_exec exec;
+ u32 reply_len = 0;
+ u32 size;
+ int ret;
+
+ if (get_user(size, &uexec->size))
+ return -EFAULT;
+ /* The first version ends at cursor, so every size covers the write-back. */
+ if (size < offsetofend(struct ctlv_op_exec, cursor))
+ return -EINVAL;
+ ret = copy_struct_from_user(&exec, sizeof(exec), uexec, size);
+ if (ret)
+ return ret;
+ if (exec.flags)
+ return -EINVAL;
+ if (exec.request_len < sizeof(struct ctlv_hdr) ||
+ exec.request_len > CTLV_MAX_INLINE_MESSAGE_LEN)
+ return -EINVAL;
+
+ /*
+ * Before the lock, which destruction must not wait on for a faulting
+ * copy. Everything after this reads the snapshot, never userspace.
+ */
+ void *snapshot __free(kvfree) = kvmalloc(exec.request_len, GFP_KERNEL_ACCOUNT);
+ if (!snapshot)
+ return -ENOMEM;
+ if (copy_from_user(snapshot, u64_to_user_ptr(exec.request), exec.request_len))
+ return -EFAULT;
+
+ void *reply __free(kvfree) = NULL;
+
+ scoped_guard(rwsem_read, &ctlv->registration_lock) {
+ if (!ctlv->registered || !ctlv->family)
+ return -ENODEV;
+ ret = ctlv_op_exec(cfile, &exec, snapshot, &reply, &reply_len);
+ }
+ /* Do not make destruction wait for a faulting userspace copy. */
+ if (reply_len && copy_to_user(u64_to_user_ptr(exec.reply), reply, reply_len))
+ return ret ?: -EFAULT;
+ /*
+ * The only fields the kernel writes back, a field at a time so that
+ * what the caller stated about its own buffers stays the caller's.
+ */
+ if (!ret && (put_user(exec.generation, &uexec->generation) ||
+ put_user(exec.cursor, &uexec->cursor)))
+ return -EFAULT;
+ return ret;
+}
+
+/**
+ * ctlv_file_ioctl - Run a CTLV ioctl on a device file.
+ * @cfile: state returned by ctlv_file_open()
+ * @cmd: ioctl command
+ * @arg: ioctl argument
+ *
+ * Return: ioctl result, or -ENOIOCTLCMD for a command that is not CTLV's.
+ */
+long ctlv_file_ioctl(struct ctlv_file *cfile, unsigned int cmd, unsigned long arg)
+{
+ switch (cmd) {
+ case CTLV_IOC_OP_EXEC:
+ return ctlv_op_exec_ioctl(cfile, arg);
+ default:
+ return -ENOIOCTLCMD;
+ }
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_file_ioctl, "CTLV");
+
+static bool ctlv_file_open_allowed(struct ctlv *ctlv)
+{
+ lockdep_assert_held_read(&ctlv->registration_lock);
+ return ctlv->registered && ctlv->family;
+}
+
+/**
+ * ctlv_file_open - Attach CTLV to one open file of its device.
+ * @ctlv: published instance
+ * @filp: device file, open for reading
+ *
+ * Return: per-open state, or an error pointer.
+ */
+struct ctlv_file *ctlv_file_open(struct ctlv *ctlv, struct file *filp)
+{
+ struct ctlv_file *cfile;
+ int ret;
+
+ /*
+ * A failed registration can withdraw the node while an open is waiting
+ * for the registration lock, so admission itself first pins the device.
+ */
+ get_device(ctlv->dev);
+
+ do {
+ guard(rwsem_read)(&ctlv->registration_lock);
+ if (!ctlv_file_open_allowed(ctlv)) {
+ ret = -ENODEV;
+ break;
+ }
+ if (!(filp->f_mode & FMODE_READ)) {
+ ret = -EACCES;
+ break;
+ }
+
+ cfile = kzalloc_obj(*cfile, GFP_KERNEL_ACCOUNT);
+ if (!cfile) {
+ ret = -ENOMEM;
+ break;
+ }
+ cfile->ctlv = ctlv;
+ cfile->filp = filp;
+ /* This description keeps the reference taken on entry. */
+ return cfile;
+ } while (0);
+
+ put_device(ctlv->dev);
+ return ERR_PTR(ret);
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_file_open, "CTLV");
+
+/**
+ * ctlv_file_close - Detach CTLV from a device file.
+ * @cfile: state returned by ctlv_file_open()
+ *
+ * For the release of the device file.
+ */
+void ctlv_file_close(struct ctlv_file *cfile)
+{
+ struct device *dev = cfile->ctlv->dev;
+
+ kfree(cfile);
+ put_device(dev);
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_file_close, "CTLV");
+
+/* Uevent identity is available to rules before the device can be opened. */
+static int ctlv_uevent_families(struct kobj_uevent_env *env,
+ const struct ctlv_family *family)
+{
+ u32 levels = ctlv_family_levels(family);
+ u32 level;
+ int ret;
+
+ ret = add_uevent_var(env, "CTLV_FAMILY=%s", family->schema->name);
+ if (ret)
+ return ret;
+ for (level = 0; level < levels; level++) {
+ const struct ctlv_family *ancestor =
+ ctlv_family_ancestor(family, levels - 1 - level);
+
+ ret = add_uevent_var(env, "CTLV_COMPATIBLE_%u=%s", level, ancestor->schema->name);
+ if (ret)
+ return ret;
+ }
+ return add_uevent_var(env, "CTLV_COMPATIBLE_N=%u", levels);
+}
+
+static int ctlv_uevent_check(const struct ctlv_family *family)
+{
+ struct kobj_uevent_env *env;
+ int ret;
+
+ env = kzalloc_obj(*env, GFP_KERNEL);
+ if (!env)
+ return -ENOMEM;
+ ret = ctlv_uevent_families(env, family);
+ if (!ret && (env->buflen > CTLV_UEVENT_MAX_LEN || env->envp_idx > CTLV_UEVENT_MAX_VARS)) {
+ pr_err("family %s: its chain of names needs %d bytes in %d properties of a uevent, more than the %d in %d it may take\n",
+ family->schema->name, env->buflen, env->envp_idx, CTLV_UEVENT_MAX_LEN,
+ CTLV_UEVENT_MAX_VARS);
+ ret = -E2BIG;
+ }
+ kfree(env);
+ return ret;
+}
+
+/**
+ * ctlv_init - Prepare an embedded instance for one family.
+ * @ctlv: zeroed instance
+ * @dev: device
+ * @family: device family
+ * @hooks: pair around every op the core does not answer, or NULL
+ *
+ * Until ctlv_publish(), the family decides its optional and dynamic
+ * ops through its generated helpers.
+ *
+ * Return: 0, or a negative error. ctlv_fini() applies either way.
+ */
+int ctlv_init(struct ctlv *ctlv, struct device *dev,
+ const struct ctlv_family *family, const struct ctlv_hooks *hooks)
+{
+ int ret;
+
+ /* Nothing in common means no pair, not half of one. */
+ if (!dev || !family || (hooks && (!hooks->pre || !hooks->post)))
+ return -EINVAL;
+
+ ctlv->dev = dev;
+ ctlv->hooks = hooks;
+ init_rwsem(&ctlv->registration_lock);
+ spin_lock_init(&ctlv->transition_lock);
+
+ /* Before anything reads the schema through this instance. */
+ ret = ctlv_op_resolution_init(ctlv, family);
+ if (ret)
+ return ret;
+
+ /* Before a subsystem announces a device that could not be. */
+ ret = ctlv_uevent_check(family);
+ if (ret)
+ return ret;
+
+ if (ctlv->dynamic_ops_count) {
+ ctlv->disabled = bitmap_zalloc(ctlv->dynamic_ops_count, GFP_KERNEL);
+ if (!ctlv->disabled)
+ return -ENOMEM;
+ }
+
+ ret = ctlv_family_claim_add(family->schema);
+ if (ret)
+ return ret;
+ ctlv->family = family;
+ return 0;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_init, "CTLV");
+
+/**
+ * ctlv_publish - Fix the op table and accept operations.
+ * @ctlv: initialized instance
+ */
+void ctlv_publish(struct ctlv *ctlv)
+{
+ ctlv_op_publish_all(ctlv);
+ scoped_guard(rwsem_write, &ctlv->registration_lock)
+ ctlv->registered = true;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_publish, "CTLV");
+
+/**
+ * ctlv_unpublish - Stop operations.
+ * @ctlv: published instance
+ *
+ * Waits for the operations in flight. No family callback runs afterwards.
+ */
+void ctlv_unpublish(struct ctlv *ctlv)
+{
+ u32 index;
+
+ lockdep_assert_not_held(&ctlv->registration_lock);
+ lockdep_assert_not_held(&ctlv->transition_lock);
+
+ /*
+ * The write side waits for admitted callbacks. Once held, registration
+ * state keeps every later ioctl out.
+ */
+ scoped_guard(rwsem_write, &ctlv->registration_lock)
+ ctlv->registered = false;
+
+ scoped_guard(spinlock, &ctlv->transition_lock)
+ ctlv->destroying = true;
+
+ /* No callback can reach what the family owns after this. */
+ scoped_guard(rwsem_write, &ctlv->registration_lock)
+ WRITE_ONCE(ctlv->family, NULL);
+ for (index = 0; index < ctlv->ops_count; index++)
+ ctlv->ops[index].binding = NULL;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_unpublish, "CTLV");
+
+/**
+ * ctlv_fini - Free what an instance allocated.
+ * @ctlv: instance never published, or unpublished
+ *
+ * For the release of the device.
+ */
+void ctlv_fini(struct ctlv *ctlv)
+{
+ bitmap_free(ctlv->disabled);
+ ctlv->disabled = NULL;
+ kfree(ctlv->ops);
+ ctlv->ops = NULL;
+}
+EXPORT_SYMBOL_NS_GPL(ctlv_fini, "CTLV");
+
+static void __exit ctlv_module_exit(void)
+{
+ ctlv_family_claims_free();
+}
+
+module_exit(ctlv_module_exit);
+MODULE_DESCRIPTION("CTLV character device TLV UAPI core");
+MODULE_LICENSE("GPL");
--
2.56.0
next prev parent reply other threads:[~2026-11-13 15:56 UTC|newest]
Thread overview: 16+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-13 15:54 [PATCH RFC v2 00/15] ctlv: Add a schema-driven TLV uAPI for character devices Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 01/15] ctlv: Add the schema language and generator Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 02/15] ctlv: Add the UAPI message format and family API Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 03/15] ctlv: Add the root family specification and generated files Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 04/15] Documentation: ctlv: Add userspace ABI Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 05/15] ctlv: Prepare message input handling Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 06/15] ctlv: Prepare blob descriptor validation Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 07/15] ctlv: Prepare schema validation Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 08/15] ctlv: Prepare message output handling Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 09/15] ctlv: Prepare VA blob handling Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 10/15] ctlv: Prepare runtime introspection Jiri Pirko
2026-11-13 15:55 ` Jiri Pirko [this message]
2026-11-13 15:55 ` [PATCH RFC v2 12/15] ctlv: Add character device nodes Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 13/15] Documentation: ctlv: Describe how to write a family Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 14/15] tools/ctlv: Add the userspace library Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 15/15] tools/ctlv: Add ctlvctl Jiri Pirko
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261113155515.2510194-12-jiri@resnulli.us \
--to=jiri@resnulli.us \
--cc=James.Bottomley@HansenPartnership.com \
--cc=aik@amd.com \
--cc=alistair.francis@wdc.com \
--cc=aneesh.kumar@kernel.org \
--cc=ankita@nvidia.com \
--cc=cedric.xing@intel.com \
--cc=dave.hansen@linux.intel.com \
--cc=dave.jiang@intel.com \
--cc=david.quigley@intel.com \
--cc=drdeeglaze@gmail.com \
--cc=elena.reshetova@intel.com \
--cc=jarkko@kernel.org \
--cc=jgg@ziepe.ca \
--cc=jic23@kernel.org \
--cc=jpiotrowski@nvidia.com \
--cc=kas@kernel.org \
--cc=ksinyuk@kernel.org \
--cc=kvm@vger.kernel.org \
--cc=linux-api@vger.kernel.org \
--cc=linux-coco@lists.linux.dev \
--cc=linux-doc@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=lukas@wunner.de \
--cc=nik.borisov@suse.com \
--cc=peter.fang@intel.com \
--cc=pgonda@google.com \
--cc=qinkun@google.com \
--cc=rick.p.edgecombe@intel.com \
--cc=rodrigo.vivi@intel.com \
--cc=sameo@meta.com \
--cc=sathyanarayanan.kuppuswamy@linux.intel.com \
--cc=sergii.dmytruk@3mdeb.com \
--cc=suzuki.poulose@arm.com \
--cc=x86@kernel.org \
--cc=yeoreum.yun@arm.com \
--cc=zohar@linux.ibm.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®