[RFC PATCH 09/12] drm/fabric: implement mutation netlink operations

From: Konstantin Sinyuk

Date: Mon Aug 24 2026 - 04:22:04 EST


Connect the six provisioning operations to the core mutation helpers and
add the generated operation and policy source that dispatches to them.

Resolve and pin mutation targets in pre_doit, run each transaction under
drm_fabric_mutation_lock, call the provider without drm_fabric_lock held,
and release references in post_doit. All six require GENL_ADMIN_PERM and
are confined to init_net.

A failed provider callback returns its error with core state unchanged.
Successful mutations emit notifications after commit, carrying the
resulting topology generation.

Co-developed-by: Ilia Levi <ilia.levi@xxxxxxxxx>
Signed-off-by: Ilia Levi <ilia.levi@xxxxxxxxx>
Signed-off-by: Konstantin Sinyuk <ksinyuk@xxxxxxxxxx>
Assisted-by: GitHub-Copilot:claude-opus-4.8
---
drivers/gpu/drm/fabric/drm_fabric.c | 5 +-
drivers/gpu/drm/fabric/drm_fabric_netlink.c | 275 ++++++++++++++++++++
drivers/gpu/drm/fabric/drm_fabric_nl.c | 106 ++++++++
3 files changed, 384 insertions(+), 2 deletions(-)

diff --git a/drivers/gpu/drm/fabric/drm_fabric.c b/drivers/gpu/drm/fabric/drm_fabric.c
index 1aae5ff68798..50c34b4fd722 100644
--- a/drivers/gpu/drm/fabric/drm_fabric.c
+++ b/drivers/gpu/drm/fabric/drm_fabric.c
@@ -793,7 +793,8 @@ int drm_fabric_user_fabric_del(u32 fabric_id)
/* Symmetric with FABRIC_NEW: only an empty fabric may be removed. */
if (drm_fabric_has_members(fabric))
return -EBUSY;
- drm_fabric_base_seq_inc();
+ /* Notify while the userspace-owned fabric is still addressable by id. */
+ drm_fabric_emit_fabric_delete(fabric, drm_fabric_base_seq_inc());
xa_erase(&drm_fabric_xa, fabric->id);
}

@@ -906,7 +907,7 @@ int drm_fabric_endpoint_set(struct drm_fabric_endpoint *ep,
if (change.valid & DRM_FABRIC_EP_CHANGE_ADMIN)
ep->admin_state = change.admin;

- drm_fabric_base_seq_inc();
+ drm_fabric_emit_endpoint_change(ep, drm_fabric_base_seq_inc());
}

return 0;
diff --git a/drivers/gpu/drm/fabric/drm_fabric_netlink.c b/drivers/gpu/drm/fabric/drm_fabric_netlink.c
index fef2d4c8f5bb..1a0f293723e9 100644
--- a/drivers/gpu/drm/fabric/drm_fabric_netlink.c
+++ b/drivers/gpu/drm/fabric/drm_fabric_netlink.c
@@ -80,6 +80,7 @@ static int drm_fabric_fill_endpoint(struct sk_buff *skb,
nla_put_u64_64bit(skb, DRM_FABRIC_A_ENDPOINT_ATTRS_FABRIC_EP_ID,
ep->fabric_ep_id, DRM_FABRIC_A_ENDPOINT_ATTRS_PAD) ||
nla_put_string(skb, DRM_FABRIC_A_ENDPOINT_ATTRS_NAME, ep->name) ||
+ nla_put_u32(skb, DRM_FABRIC_A_ENDPOINT_ATTRS_ADMIN_STATE, ep->admin_state) ||
nla_put_string(skb, DRM_FABRIC_A_ENDPOINT_ATTRS_DEV_NAME,
dev_name(ep->parent)) ||
nla_put_string(skb, DRM_FABRIC_A_ENDPOINT_ATTRS_BUS_NAME,
@@ -132,6 +133,8 @@ static int drm_fabric_fill_port(struct sk_buff *skb,
nla_put_u32(skb, DRM_FABRIC_A_PORT_ATTRS_ENDPOINT_ID, port->endpoint->id) ||
nla_put_u32(skb, DRM_FABRIC_A_PORT_ATTRS_OPER_STATE,
port->oper_state) ||
+ nla_put_u32(skb, DRM_FABRIC_A_PORT_ATTRS_ADMIN_STATE,
+ port->admin_state) ||
nla_put_u32(skb, DRM_FABRIC_A_PORT_ATTRS_MAX_LANE_COUNT,
port->max_lane_count) ||
nla_put_u32(skb, DRM_FABRIC_A_PORT_ATTRS_MAX_LANE_SIGNALING_RATE_MBPS,
@@ -846,6 +849,272 @@ int drm_fabric_nl_port_stats_get_dumpit(struct sk_buff *skb,
return ret;
}

+/* Fabric create/delete have no target to resolve; only serialize mutation. */
+int drm_fabric_nl_pre_doit(const struct genl_split_ops *ops,
+ struct sk_buff *skb, struct genl_info *info)
+{
+ int ret = drm_fabric_nl_host_only(genl_info_net(info));
+
+ if (ret)
+ return ret;
+
+ mutex_lock(&drm_fabric_mutation_lock);
+ return 0;
+}
+
+void drm_fabric_nl_post_doit(const struct genl_split_ops *ops,
+ struct sk_buff *skb, struct genl_info *info)
+{
+ mutex_unlock(&drm_fabric_mutation_lock);
+}
+
+/*
+ * Pin the target in user_ptr[0]. Drop the mutation lock on failure because
+ * post_doit does not run when pre_doit fails.
+ */
+int drm_fabric_nl_endpoint_pre_doit(const struct genl_split_ops *ops,
+ struct sk_buff *skb, struct genl_info *info)
+{
+ struct drm_fabric_endpoint *ep;
+ int ret = drm_fabric_nl_host_only(genl_info_net(info));
+
+ if (ret)
+ return ret;
+
+ mutex_lock(&drm_fabric_mutation_lock);
+
+ scoped_guard(mutex, &drm_fabric_lock) {
+ ep = drm_fabric_resolve_endpoint(info);
+ if (!IS_ERR(ep))
+ drm_fabric_endpoint_get(ep);
+ }
+
+ if (IS_ERR(ep)) {
+ mutex_unlock(&drm_fabric_mutation_lock);
+ return PTR_ERR(ep);
+ }
+
+ info->user_ptr[0] = ep;
+ return 0;
+}
+
+void drm_fabric_nl_endpoint_post_doit(const struct genl_split_ops *ops,
+ struct sk_buff *skb,
+ struct genl_info *info)
+{
+ drm_fabric_endpoint_put(info->user_ptr[0]);
+ mutex_unlock(&drm_fabric_mutation_lock);
+}
+
+int drm_fabric_nl_port_pre_doit(const struct genl_split_ops *ops,
+ struct sk_buff *skb, struct genl_info *info)
+{
+ struct drm_fabric_port *port;
+ u32 ep_id, port_idx;
+ int ret;
+
+ ret = drm_fabric_nl_host_only(genl_info_net(info));
+ if (ret)
+ return ret;
+
+ ret = drm_fabric_port_key(info, &ep_id, &port_idx);
+ if (ret)
+ return ret;
+
+ mutex_lock(&drm_fabric_mutation_lock);
+
+ port = drm_fabric_port_find_get(ep_id, port_idx);
+ if (IS_ERR(port)) {
+ mutex_unlock(&drm_fabric_mutation_lock);
+ return PTR_ERR(port);
+ }
+
+ info->user_ptr[0] = port;
+ return 0;
+}
+
+void drm_fabric_nl_port_post_doit(const struct genl_split_ops *ops,
+ struct sk_buff *skb, struct genl_info *info)
+{
+ drm_fabric_port_put(info->user_ptr[0]);
+ mutex_unlock(&drm_fabric_mutation_lock);
+}
+
+/* A nested policy cannot mark members required; check type and instance-id here. */
+static int drm_fabric_parse_new_params(struct genl_info *info,
+ enum drm_fabric_type *type,
+ const char **name, u64 *instance_id)
+{
+ struct nlattr *pa[DRM_FABRIC_A_FABRIC_NEW_PARAMS_MAX + 1];
+ struct nlattr *nest;
+ int ret;
+
+ if (GENL_REQ_ATTR_CHECK(info, DRM_FABRIC_A_FABRIC_NEW_PARAMS))
+ return -EINVAL;
+
+ nest = info->attrs[DRM_FABRIC_A_FABRIC_NEW_PARAMS];
+ ret = nla_parse_nested(pa, DRM_FABRIC_A_FABRIC_NEW_PARAMS_MAX, nest,
+ drm_fabric_fabric_new_params_nl_policy,
+ info->extack);
+ if (ret)
+ return ret;
+
+ if (NL_REQ_ATTR_CHECK(info->extack, nest, pa,
+ DRM_FABRIC_A_FABRIC_NEW_PARAMS_TYPE) ||
+ NL_REQ_ATTR_CHECK(info->extack, nest, pa,
+ DRM_FABRIC_A_FABRIC_NEW_PARAMS_INSTANCE_ID))
+ return -EINVAL;
+
+ *type = nla_get_u32(pa[DRM_FABRIC_A_FABRIC_NEW_PARAMS_TYPE]);
+ *instance_id = nla_get_u64(pa[DRM_FABRIC_A_FABRIC_NEW_PARAMS_INSTANCE_ID]);
+ *name = pa[DRM_FABRIC_A_FABRIC_NEW_PARAMS_NAME] ?
+ nla_data(pa[DRM_FABRIC_A_FABRIC_NEW_PARAMS_NAME]) : NULL;
+ return 0;
+}
+
+int drm_fabric_nl_fabric_new_doit(struct sk_buff *skb,
+ struct genl_info *info)
+{
+ enum drm_fabric_type type;
+ const char *name = NULL;
+ u64 instance_id;
+ struct sk_buff *msg;
+ struct nlattr *id_attr;
+ u32 fabric_id;
+ void *hdr;
+ int ret;
+
+ ret = drm_fabric_parse_new_params(info, &type, &name, &instance_id);
+ if (ret)
+ return ret;
+
+ /*
+ * Reserve the id attribute before publishing: with the space already
+ * committed the store cannot fail, so there is no create-then-withdraw
+ * window.
+ */
+ msg = nlmsg_new(NLMSG_DEFAULT_SIZE, GFP_KERNEL);
+ if (!msg)
+ return -ENOMEM;
+
+ hdr = genlmsg_put(msg, info->snd_portid, info->snd_seq,
+ &drm_fabric_nl_family, 0,
+ DRM_FABRIC_CMD_FABRIC_NEW);
+ if (!hdr) {
+ nlmsg_free(msg);
+ return -EMSGSIZE;
+ }
+
+ id_attr = nla_reserve(msg, DRM_FABRIC_A_FABRIC_ID, sizeof(u32));
+ if (!id_attr) {
+ nlmsg_free(msg);
+ return -EMSGSIZE;
+ }
+
+ ret = drm_fabric_user_fabric_new(type, instance_id, name, &fabric_id);
+ if (ret) {
+ nlmsg_free(msg);
+ return ret;
+ }
+
+ /* nla_put_u32() copies a host-order u32 verbatim; so does this store. */
+ *(u32 *)nla_data(id_attr) = fabric_id;
+
+ genlmsg_end(msg, hdr);
+ return genlmsg_reply(msg, info);
+}
+
+int drm_fabric_nl_fabric_del_doit(struct sk_buff *skb,
+ struct genl_info *info)
+{
+ u32 fabric_id;
+
+ if (GENL_REQ_ATTR_CHECK(info, DRM_FABRIC_A_FABRIC_ID))
+ return -EINVAL;
+
+ fabric_id = nla_get_u32(info->attrs[DRM_FABRIC_A_FABRIC_ID]);
+ return drm_fabric_user_fabric_del(fabric_id);
+}
+
+int drm_fabric_nl_endpoint_set_doit(struct sk_buff *skb,
+ struct genl_info *info)
+{
+ struct drm_fabric_endpoint *ep = info->user_ptr[0];
+ struct drm_fabric_endpoint_change change = {};
+
+ if (info->attrs[DRM_FABRIC_A_FABRIC_ID]) {
+ change.valid |= DRM_FABRIC_EP_CHANGE_FABRIC;
+ change.fabric_id =
+ nla_get_u32(info->attrs[DRM_FABRIC_A_FABRIC_ID]);
+ }
+
+ if (info->attrs[DRM_FABRIC_A_ADMIN_STATE]) {
+ change.valid |= DRM_FABRIC_EP_CHANGE_ADMIN;
+ change.admin =
+ nla_get_u32(info->attrs[DRM_FABRIC_A_ADMIN_STATE]);
+ }
+
+ if (!change.valid)
+ return -EINVAL;
+
+ return drm_fabric_endpoint_set(ep, &change);
+}
+
+int drm_fabric_nl_port_set_doit(struct sk_buff *skb,
+ struct genl_info *info)
+{
+ struct drm_fabric_port *port = info->user_ptr[0];
+ enum drm_fabric_admin_state admin;
+
+ if (GENL_REQ_ATTR_CHECK(info, DRM_FABRIC_A_ADMIN_STATE))
+ return -EINVAL;
+
+ admin = nla_get_u32(info->attrs[DRM_FABRIC_A_ADMIN_STATE]);
+
+ return drm_fabric_port_set_admin(port, admin);
+}
+
+int drm_fabric_nl_port_peer_new_doit(struct sk_buff *skb,
+ struct genl_info *info)
+{
+ struct drm_fabric_port *port = info->user_ptr[0];
+ struct nlattr *pa[DRM_FABRIC_A_PEER_ATTRS_MAX + 1];
+ struct drm_fabric_peer peer = {};
+ int ret;
+
+ if (GENL_REQ_ATTR_CHECK(info, DRM_FABRIC_A_PEER))
+ return -EINVAL;
+
+ ret = nla_parse_nested(pa, DRM_FABRIC_A_PEER_ATTRS_MAX,
+ info->attrs[DRM_FABRIC_A_PEER],
+ drm_fabric_peer_nl_policy, info->extack);
+ if (ret)
+ return ret;
+
+ /*
+ * A nested policy cannot require members; require the complete peer
+ * descriptor here.
+ */
+ if (!pa[DRM_FABRIC_A_PEER_ATTRS_PEER_ID] ||
+ !pa[DRM_FABRIC_A_PEER_ATTRS_TYPE] ||
+ !pa[DRM_FABRIC_A_PEER_ATTRS_PORT_INDEX])
+ return -EINVAL;
+
+ peer.peer_id = nla_get_u64(pa[DRM_FABRIC_A_PEER_ATTRS_PEER_ID]);
+ peer.peer_type = nla_get_u32(pa[DRM_FABRIC_A_PEER_ATTRS_TYPE]);
+ peer.port_index = nla_get_u32(pa[DRM_FABRIC_A_PEER_ATTRS_PORT_INDEX]);
+
+ return drm_fabric_port_peer_new(port, &peer);
+}
+
+int drm_fabric_nl_port_peer_del_doit(struct sk_buff *skb,
+ struct genl_info *info)
+{
+ struct drm_fabric_port *port = info->user_ptr[0];
+
+ return drm_fabric_port_peer_del(port);
+}
+
void drm_fabric_emit_port_change(struct drm_fabric_port *port, u32 generation)
{
struct sk_buff *msg;
@@ -982,6 +1251,12 @@ void drm_fabric_emit_endpoint_delete(struct drm_fabric_endpoint *ep, u32 generat
generation);
}

+void drm_fabric_emit_endpoint_change(struct drm_fabric_endpoint *ep, u32 generation)
+{
+ drm_fabric_endpoint_event_send(DRM_FABRIC_CMD_ENDPOINT_CHANGE_NTF, ep,
+ generation);
+}
+
static void drm_fabric_fabric_event_send(enum drm_fabric_cmd cmd,
struct drm_fabric *fabric,
u32 generation)
diff --git a/drivers/gpu/drm/fabric/drm_fabric_nl.c b/drivers/gpu/drm/fabric/drm_fabric_nl.c
index 032548405146..20b277f27ae9 100644
--- a/drivers/gpu/drm/fabric/drm_fabric_nl.c
+++ b/drivers/gpu/drm/fabric/drm_fabric_nl.c
@@ -11,6 +11,19 @@

#include <uapi/drm/drm_fabric.h>

+/* Common nested types */
+const struct nla_policy drm_fabric_fabric_new_params_nl_policy[DRM_FABRIC_A_FABRIC_NEW_PARAMS_INSTANCE_ID + 1] = {
+ [DRM_FABRIC_A_FABRIC_NEW_PARAMS_TYPE] = NLA_POLICY_RANGE(NLA_U32, 1, 1),
+ [DRM_FABRIC_A_FABRIC_NEW_PARAMS_NAME] = { .type = NLA_NUL_STRING, .len = 31, },
+ [DRM_FABRIC_A_FABRIC_NEW_PARAMS_INSTANCE_ID] = { .type = NLA_U64, },
+};
+
+const struct nla_policy drm_fabric_peer_nl_policy[DRM_FABRIC_A_PEER_ATTRS_PORT_INDEX + 1] = {
+ [DRM_FABRIC_A_PEER_ATTRS_PEER_ID] = { .type = NLA_U64, },
+ [DRM_FABRIC_A_PEER_ATTRS_TYPE] = NLA_POLICY_RANGE(NLA_U32, 1, 2),
+ [DRM_FABRIC_A_PEER_ATTRS_PORT_INDEX] = { .type = NLA_U32, },
+};
+
/* DRM_FABRIC_CMD_FABRIC_GET - do */
static const struct nla_policy drm_fabric_fabric_get_nl_policy[DRM_FABRIC_A_FABRIC_ID + 1] = {
[DRM_FABRIC_A_FABRIC_ID] = { .type = NLA_U32, },
@@ -50,6 +63,45 @@ static const struct nla_policy drm_fabric_port_stats_get_dump_nl_policy[DRM_FABR
[DRM_FABRIC_A_ENDPOINT_ID] = { .type = NLA_U32, },
};

+/* DRM_FABRIC_CMD_FABRIC_NEW - do */
+static const struct nla_policy drm_fabric_fabric_new_nl_policy[DRM_FABRIC_A_FABRIC_NEW_PARAMS + 1] = {
+ [DRM_FABRIC_A_FABRIC_NEW_PARAMS] = NLA_POLICY_NESTED(drm_fabric_fabric_new_params_nl_policy),
+};
+
+/* DRM_FABRIC_CMD_FABRIC_DEL - do */
+static const struct nla_policy drm_fabric_fabric_del_nl_policy[DRM_FABRIC_A_FABRIC_ID + 1] = {
+ [DRM_FABRIC_A_FABRIC_ID] = { .type = NLA_U32, },
+};
+
+/* DRM_FABRIC_CMD_ENDPOINT_SET - do */
+static const struct nla_policy drm_fabric_endpoint_set_nl_policy[DRM_FABRIC_A_ADMIN_STATE + 1] = {
+ [DRM_FABRIC_A_ENDPOINT_ID] = { .type = NLA_U32, },
+ [DRM_FABRIC_A_DEV_NAME] = { .type = NLA_NUL_STRING, },
+ [DRM_FABRIC_A_BUS_NAME] = { .type = NLA_NUL_STRING, },
+ [DRM_FABRIC_A_FABRIC_ID] = { .type = NLA_U32, },
+ [DRM_FABRIC_A_ADMIN_STATE] = NLA_POLICY_RANGE(NLA_U32, 1, 2),
+};
+
+/* DRM_FABRIC_CMD_PORT_SET - do */
+static const struct nla_policy drm_fabric_port_set_nl_policy[DRM_FABRIC_A_ADMIN_STATE + 1] = {
+ [DRM_FABRIC_A_ENDPOINT_ID] = { .type = NLA_U32, },
+ [DRM_FABRIC_A_PORT_INDEX] = { .type = NLA_U32, },
+ [DRM_FABRIC_A_ADMIN_STATE] = NLA_POLICY_RANGE(NLA_U32, 1, 2),
+};
+
+/* DRM_FABRIC_CMD_PORT_PEER_NEW - do */
+static const struct nla_policy drm_fabric_port_peer_new_nl_policy[DRM_FABRIC_A_PEER + 1] = {
+ [DRM_FABRIC_A_ENDPOINT_ID] = { .type = NLA_U32, },
+ [DRM_FABRIC_A_PORT_INDEX] = { .type = NLA_U32, },
+ [DRM_FABRIC_A_PEER] = NLA_POLICY_NESTED(drm_fabric_peer_nl_policy),
+};
+
+/* DRM_FABRIC_CMD_PORT_PEER_DEL - do */
+static const struct nla_policy drm_fabric_port_peer_del_nl_policy[DRM_FABRIC_A_PORT_INDEX + 1] = {
+ [DRM_FABRIC_A_ENDPOINT_ID] = { .type = NLA_U32, },
+ [DRM_FABRIC_A_PORT_INDEX] = { .type = NLA_U32, },
+};
+
/* Ops table for drm_fabric */
static const struct genl_split_ops drm_fabric_nl_ops[] = {
{
@@ -106,6 +158,60 @@ static const struct genl_split_ops drm_fabric_nl_ops[] = {
.maxattr = DRM_FABRIC_A_ENDPOINT_ID,
.flags = GENL_CMD_CAP_DUMP,
},
+ {
+ .cmd = DRM_FABRIC_CMD_FABRIC_NEW,
+ .pre_doit = drm_fabric_nl_pre_doit,
+ .doit = drm_fabric_nl_fabric_new_doit,
+ .post_doit = drm_fabric_nl_post_doit,
+ .policy = drm_fabric_fabric_new_nl_policy,
+ .maxattr = DRM_FABRIC_A_FABRIC_NEW_PARAMS,
+ .flags = GENL_ADMIN_PERM | GENL_CMD_CAP_DO,
+ },
+ {
+ .cmd = DRM_FABRIC_CMD_FABRIC_DEL,
+ .pre_doit = drm_fabric_nl_pre_doit,
+ .doit = drm_fabric_nl_fabric_del_doit,
+ .post_doit = drm_fabric_nl_post_doit,
+ .policy = drm_fabric_fabric_del_nl_policy,
+ .maxattr = DRM_FABRIC_A_FABRIC_ID,
+ .flags = GENL_ADMIN_PERM | GENL_CMD_CAP_DO,
+ },
+ {
+ .cmd = DRM_FABRIC_CMD_ENDPOINT_SET,
+ .pre_doit = drm_fabric_nl_endpoint_pre_doit,
+ .doit = drm_fabric_nl_endpoint_set_doit,
+ .post_doit = drm_fabric_nl_endpoint_post_doit,
+ .policy = drm_fabric_endpoint_set_nl_policy,
+ .maxattr = DRM_FABRIC_A_ADMIN_STATE,
+ .flags = GENL_ADMIN_PERM | GENL_CMD_CAP_DO,
+ },
+ {
+ .cmd = DRM_FABRIC_CMD_PORT_SET,
+ .pre_doit = drm_fabric_nl_port_pre_doit,
+ .doit = drm_fabric_nl_port_set_doit,
+ .post_doit = drm_fabric_nl_port_post_doit,
+ .policy = drm_fabric_port_set_nl_policy,
+ .maxattr = DRM_FABRIC_A_ADMIN_STATE,
+ .flags = GENL_ADMIN_PERM | GENL_CMD_CAP_DO,
+ },
+ {
+ .cmd = DRM_FABRIC_CMD_PORT_PEER_NEW,
+ .pre_doit = drm_fabric_nl_port_pre_doit,
+ .doit = drm_fabric_nl_port_peer_new_doit,
+ .post_doit = drm_fabric_nl_port_post_doit,
+ .policy = drm_fabric_port_peer_new_nl_policy,
+ .maxattr = DRM_FABRIC_A_PEER,
+ .flags = GENL_ADMIN_PERM | GENL_CMD_CAP_DO,
+ },
+ {
+ .cmd = DRM_FABRIC_CMD_PORT_PEER_DEL,
+ .pre_doit = drm_fabric_nl_port_pre_doit,
+ .doit = drm_fabric_nl_port_peer_del_doit,
+ .post_doit = drm_fabric_nl_port_post_doit,
+ .policy = drm_fabric_port_peer_del_nl_policy,
+ .maxattr = DRM_FABRIC_A_PORT_INDEX,
+ .flags = GENL_ADMIN_PERM | GENL_CMD_CAP_DO,
+ },
};

static const struct genl_multicast_group drm_fabric_nl_mcgrps[] = {
--
2.43.0