* [RFC PATCH v2 1/6] docs: security: Describe the Kernel Assurance Plane design
2026-10-28 13:33 [RFC PATCH v2 0/6] security: introduce Kernel Assurance Plane Ketan Bhardwaj
@ 2026-10-28 13:33 ` Ketan Bhardwaj
2026-10-28 15:17 ` Günther Noack
2026-10-28 13:33 ` [RFC PATCH v2 2/6] security: kap: add the LSM ID and internal headers Ketan Bhardwaj
` (4 subsequent siblings)
5 siblings, 1 reply; 9+ messages in thread
From: Ketan Bhardwaj @ 2026-10-28 13:33 UTC (permalink / raw)
To: Paul Moore, James Morris, Serge E . Hallyn
Cc: linux-security-module, Jonathan Corbet, Shuah Khan, Randy Dunlap,
linux-doc, linux-api, Mickaël Salaün,
Günther Noack, linux-kernel, HARIHARA SUDHAN . R,
Ketan Bhardwaj
Describe the Kernel Assurance Plane (KAP) design.
Agents and other long-running tasks act on behalf of an objective that a
user approved. Linux decides whether a task may perform an operation,
but not whether the operation still belongs to that objective. Seccomp,
LSM profiles, namespaces and cgroups cannot hold several objectives apart
inside one environment, or revoke one objective's authority while its
task runs.
KAP introduces objective-scoped authority as a kernel subsystem. The
document covers Intent Lineage Groups, the authority model and the
control plane under /sys/kernel/security/kap, where igroups are created
with mkdir as cgroups are. For each design decision it lists the
alternatives considered, and it ends with the questions on which
maintainer feedback is wanted.
Assisted-by: Claude
Co-developed-by: HARIHARA SUDHAN. R <harisudhan@armoriq.io>
Signed-off-by: HARIHARA SUDHAN. R <harisudhan@armoriq.io>
Assisted-by: Codex
Signed-off-by: Ketan Bhardwaj <ketan@armoriq.io>
---
Documentation/ABI/testing/securityfs-kap | 111 +++++++++++
Documentation/security/index.rst | 1 +
Documentation/security/kap.rst | 236 +++++++++++++++++++++++
3 files changed, 348 insertions(+)
create mode 100644 Documentation/ABI/testing/securityfs-kap
create mode 100644 Documentation/security/kap.rst
diff --git a/Documentation/ABI/testing/securityfs-kap b/Documentation/ABI/testing/securityfs-kap
new file mode 100644
index 0000000..066de0b
--- /dev/null
+++ b/Documentation/ABI/testing/securityfs-kap
@@ -0,0 +1,111 @@
+What: /sys/kernel/security/kap/
+Date: October 2026
+KernelVersion: RFC
+Contact: Ketan Bhardwaj <ketan@armoriq.io>
+Description:
+ Kernel Assurance Plane control directory in securityfs.
+ The ABI is experimental and intended for RFC review.
+
+What: /sys/kernel/security/kap/kap.default
+Date: October 2026
+KernelVersion: RFC
+Contact: Ketan Bhardwaj <ketan@armoriq.io>
+Description:
+ Read or write "open" or "closed". When closed, a task
+ outside the initial PID namespace that is bound to no igroup
+ cannot start a program.
+
+What: /sys/kernel/security/kap/igroups/
+Date: October 2026
+KernelVersion: RFC
+Contact: Ketan Bhardwaj <ketan@armoriq.io>
+Description:
+ Directory of Intent Lineage Groups. Create an igroup with
+ mkdir, as a cgroup is created.
+
+What: /sys/kernel/security/kap/igroups/<igroup>/kap.install
+Date: October 2026
+KernelVersion: RFC
+Contact: Ketan Bhardwaj <ketan@armoriq.io>
+Description:
+ Write the lineage root, authority epoch, optional expiry and
+ transition, and the authority as one key=value document in a
+ single write. The fields commit together or not at all.
+
+What: /sys/kernel/security/kap/igroups/<igroup>/kap.procs
+Date: October 2026
+KernelVersion: RFC
+Contact: Ketan Bhardwaj <ketan@armoriq.io>
+Description:
+ Write a pid to bind that process, with all of its threads,
+ to the igroup. Reads return the pids written.
+
+What: /sys/kernel/security/kap/igroups/<igroup>/kap.lineage_root
+Date: October 2026
+KernelVersion: RFC
+Contact: Ketan Bhardwaj <ketan@armoriq.io>
+Description:
+ Read or write the opaque lineage root for the igroup.
+
+What: /sys/kernel/security/kap/igroups/<igroup>/kap.authority_epoch
+Date: October 2026
+KernelVersion: RFC
+Contact: Ketan Bhardwaj <ketan@armoriq.io>
+Description:
+ Read or write the current authority epoch. A lower value is
+ refused, and a write clears revoked mode. Bound tasks whose
+ recorded epoch is stale fail KAP checks closed.
+
+What: /sys/kernel/security/kap/igroups/<igroup>/kap.authority
+Date: October 2026
+KernelVersion: RFC
+Contact: Ketan Bhardwaj <ketan@armoriq.io>
+Description:
+ Read or write the authority document for the igroup, in a
+ strict subset of JSON, with a "predicates" array, each predicate having a
+ "kind" and "allow" and "deny" arrays of patterns. The
+ Kernel Assurance Plane design document lists the kinds and
+ how patterns match.
+
+What: /sys/kernel/security/kap/igroups/<igroup>/kap.expires_at
+Date: October 2026
+KernelVersion: RFC
+Contact: Ketan Bhardwaj <ketan@armoriq.io>
+Description:
+ Read or write the authority expiry in seconds since the Unix
+ epoch, or 0 for none. An expiry already in the past is
+ refused. Once it passes, every check for the igroup fails
+ closed.
+
+What: /sys/kernel/security/kap/igroups/<igroup>/kap.transition
+Date: October 2026
+KernelVersion: RFC
+Contact: Ketan Bhardwaj <ketan@armoriq.io>
+Description:
+ Write a transition record whose sequence number is higher
+ than the last one, to prevent replay. Reads return the last
+ sequence number.
+
+What: /sys/kernel/security/kap/igroups/<igroup>/kap.mode
+Date: October 2026
+KernelVersion: RFC
+Contact: Ketan Bhardwaj <ketan@armoriq.io>
+Description:
+ Read or write igroup mode. Supported values are "enforce" and
+ "revoked". A revoked igroup refuses every operation.
+
+What: /sys/kernel/security/kap/igroups/<igroup>/kap.events
+Date: October 2026
+KernelVersion: RFC
+Contact: Ketan Bhardwaj <ketan@armoriq.io>
+Description:
+ Read recent KAP decisions as JSON lines with the fields
+ ts_ns, igroup, decision, operation, target, reason, epoch,
+ root and pid.
+
+What: /sys/kernel/security/kap/igroups/<igroup>/kap.igfd
+Date: October 2026
+KernelVersion: RFC
+Contact: Ketan Bhardwaj <ketan@armoriq.io>
+Description:
+ Read the igroup's name, epoch and lineage root on one line.
diff --git a/Documentation/security/index.rst b/Documentation/security/index.rst
index 00650dc..937193b 100644
--- a/Documentation/security/index.rst
+++ b/Documentation/security/index.rst
@@ -18,6 +18,7 @@ Security Documentation
siphash
tpm/index
digsig
+ kap
landlock
secrets/index
ipe
diff --git a/Documentation/security/kap.rst b/Documentation/security/kap.rst
new file mode 100644
index 0000000..22c2729
--- /dev/null
+++ b/Documentation/security/kap.rst
@@ -0,0 +1,236 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+======================
+Kernel Assurance Plane
+======================
+
+Kernel Assurance Plane (KAP) introduces objective-scoped authority as a kernel
+subsystem. Intent Lineage Groups (igroups) are its core kernel object, LSM hooks
+are its enforcement mechanism, and securityfs is its control-plane interface.
+
+This document describes the design for review. It lists the alternatives that
+were considered for each decision and the questions on which maintainer feedback
+is wanted.
+
+Motivation
+==========
+
+A task increasingly runs on behalf of an objective that a user approved, for
+example an AI agent asked to fix a failing test. Over hours it starts programs,
+reads and writes files and opens connections. Linux decides whether the task
+may perform each operation. It does not decide whether the operation still
+belongs to the objective the task was started for.
+
+Existing mechanisms cover parts of this. Seccomp filters syscalls but cannot see
+paths or objectives. LSM profiles, such as AppArmor or SELinux, are attached to
+programs or labels, not to an objective, and cannot be narrowed for one task
+while it runs. Namespaces and cgroups partition resources rather than
+authority. None of them lets several objectives run in one environment, each
+held to its own authority, or fails a task closed the moment its authority is
+revoked.
+
+Model
+=====
+
+``igroup``
+ Intent Lineage Group, the unit KAP enforces. An igroup owns an authority
+ snapshot, a lineage root, the current epoch, an optional expiry, a transition
+ sequence, its bound tasks and an event log. A process is bound by writing its
+ pid to the igroup, which binds all of its threads. Children inherit the
+ binding, and it is kept across exec and setns.
+
+``authority snapshot``
+ The predicate set for one igroup epoch, with typed ``allow`` and ``deny``
+ arrays for exec, file, network connect, credential, IPC, namespace,
+ descriptor, procfs, executable memory, BPF, ptrace and io_uring checks. File
+ predicates also govern rename, link, unlink, mkdir, rmdir, mknod, symlink and
+ truncate, so a file cannot be moved into a denied path. A deny wins over an
+ allow, and an operation no predicate allows is refused. The snapshot is
+ compiled once when it is installed.
+
+ In this RFC the snapshot is a document in a strict subset of JSON, with no
+ whitespace around the ``"predicates"`` and ``"kind"`` keys and no escaped
+ quotes::
+
+ {"predicates":[{"kind":"file","allow":["/srv/work"],"deny":["/srv/work/keys"]},
+ {"kind":"exec","allow":["/usr/bin/*"],"deny":[]}]}
+
+ The kinds are ``exec``, ``file``, ``net``, ``cred``, ``ipc``, ``namespace``,
+ ``descriptor``, ``procfs``, ``execmem``, ``bpf``, ``ptrace`` and
+ ``io_uring``. File, descriptor and procfs patterns match a path and
+ everything below it. Other patterns match exactly, or by prefix when they
+ end in ``*``. A program is allowed by the path it resolves to, and refused
+ by a deny on either that path or the name it was started with.
+
+ Each kind is matched against a target string:
+
+ * ``exec``, ``file``, ``descriptor``, ``procfs``: a path. A rename, link or
+ other path operation is checked as its parent directory plus its name.
+ * ``execmem``: the path of the mapped file, or ``anonymous``.
+ * ``net``: the destination address and port, as in ``192.0.2.1:443``, or
+ ``family:N`` for an address family other than IPv4 and IPv6. A connect
+ with ``AF_UNSPEC`` is not checked; ``AF_UNIX`` is checked as ``ipc``.
+ * ``cred``: ``uid=U,euid=U,suid=U,fsuid=U``, ``gid=G,egid=G,sgid=G,fsgid=G``,
+ ``groups`` or ``capset``.
+ * ``ipc``: ``unix:stream_connect``, ``unix:may_send`` or ``sysv:flag=N``.
+ * ``namespace``: ``TYPE:OPERATION`` for clone, unshare and setns, where
+ TYPE is ``mount``, ``pid``, ``net``, ``user``, ``uts``, ``ipc``,
+ ``cgroup`` or ``time``; ``user`` for creating a user namespace; and
+ ``mount:mount:FSTYPE:PATH``, ``mount:umount:PATH``,
+ ``mount:pivotroot:OLD:NEW`` and ``mount:move:FROM:TO`` for mounts.
+ * ``ptrace``: ``same-igroup``, ``igroup=NAME`` or ``unconfined``, for the
+ other task.
+ * ``bpf``: ``cmd=N``, with ``map_type=N`` or ``prog_type=N`` when creating
+ a map or loading a program.
+ * ``io_uring``: ``override_creds``, ``sqpoll`` or ``uring_cmd``.
+
+``epoch``
+ Monotonic authority generation. Installing a higher epoch fails every task
+ still bound to an older one closed from its next check.
+
+``lineage root``
+ Opaque identifier supplied by userspace that ties the installed authority to
+ a committed execution lineage.
+
+The kernel does not parse planning or policy languages. A privileged userspace
+runtime validates those artifacts and installs kernel-ready state. Tasks that
+are bound to no igroup are not checked, except for the exec check that
+``kap.default`` enables.
+
+Control plane
+=============
+
+KAP state lives under ``/sys/kernel/security/kap``::
+
+ /sys/kernel/security/kap/
+ kap.default
+ igroups/ created with mkdir, as cgroups are
+ <igroup>/
+ kap.install
+ kap.procs
+ kap.lineage_root
+ kap.authority_epoch
+ kap.authority
+ kap.expires_at
+ kap.transition
+ kap.mode
+ kap.events
+ kap.igfd
+
+``kap.default``
+ ``open`` (the default) or ``closed``. When closed, a task outside the initial
+ PID namespace that is bound to no igroup cannot start a program, so a bind
+ that failed cannot leave a workload running without authority.
+
+``kap.install``
+ Write the lineage root, epoch, optional expiry and transition, and the
+ authority as one ``key=value`` document in a single write. They commit
+ together or not at all.
+
+``kap.procs``
+ Write a pid to bind that process, with all of its threads, to the igroup.
+ Reads return the pids written.
+
+``kap.lineage_root``, ``kap.authority``
+ Read or write the lineage root or the authority document. A write to
+ ``kap.authority`` is compiled before it replaces the current authority; a
+ document that does not compile leaves it unchanged.
+
+``kap.authority_epoch``, ``kap.expires_at``
+ Read or write the epoch, or the expiry in seconds since the Unix epoch (0 for
+ none). A lower epoch, or an expiry already in the past, is refused. Writing
+ an epoch, or installing authority, also clears revoked mode.
+
+``kap.transition``
+ Write a transition record whose sequence number is higher than the last one.
+ Reads return the last sequence number.
+
+``kap.mode``
+ ``enforce`` or ``revoked``. A revoked igroup refuses every operation.
+
+``kap.events``
+ Read recent decisions as JSON lines with the fields ``ts_ns``, ``igroup``,
+ ``decision``, ``operation``, ``target``, ``reason``, ``epoch``, ``root``
+ (the lineage root) and ``pid``.
+
+``kap.igfd``
+ Read the igroup's name, epoch and lineage root on one line. The name is
+ reserved for future igroup file descriptors.
+
+Design decisions
+================
+
+Where the control plane lives
+-----------------------------
+
+Igroups are created and destroyed by userspace, so the control plane needs
+directories that userspace can create. securityfs does not allow userspace
+mkdir.
+
+Considered:
+
+* A dedicated filesystem mounted at ``/sys/fs/kap``, the prototype's first
+ interface. It works, but puts security state outside securityfs, where the
+ other LSMs keep theirs.
+* securityfs with control files, for example writing a name to a ``create`` file
+ to make an igroup. This keeps everything in securityfs but replaces mkdir and
+ rmdir with a private protocol that every tool has to learn.
+
+Chosen: a small internal filesystem that the kernel mounts itself and links
+under securityfs, the approach AppArmor uses for policy namespaces
+(``apparmorfs``, linked as ``/sys/kernel/security/apparmor/policy``). Users only
+see paths under securityfs, and an igroup is created with mkdir, the way a
+cgroup is.
+
+Igroups rather than a cgroup controller
+---------------------------------------
+
+Considered: making KAP a cgroup controller, since igroups are created like
+cgroups and follow the process tree.
+
+Chosen: a separate hierarchy. A container usually places all of its tasks in
+one cgroup, while several objectives can run inside it, each needing its own
+authority. Tying authority to resource partitioning would prevent that, and
+authority must also follow setns, which cgroup membership does not.
+
+A static LSM rather than BPF LSM programs
+-----------------------------------------
+
+Considered: implementing the checks as BPF LSM programs.
+
+Chosen: a built-in LSM. Igroup membership, epochs and inheritance across fork,
+exec and setns are kernel object state with fixed semantics that reviewers can
+audit in one place. BPF LSM also needs ``CONFIG_BPF_LSM`` and BTF, which many
+minimal kernels do not enable.
+
+Policy stays in userspace
+-------------------------
+
+Considered: parsing a policy language in the kernel.
+
+Chosen: the kernel accepts only a compiled predicate snapshot. Planning,
+policy validation and signature checks stay in privileged userspace, and the
+snapshot is compiled into per-kind pattern sets once at install time.
+
+Questions for maintainers
+=========================
+
+1. Is an internal filesystem linked under securityfs, as AppArmor does,
+ acceptable for a control plane that needs userspace mkdir?
+2. Should igroup removal follow cgroups, with rmdir allowed once no task is
+ bound?
+3. Is a separate igroup hierarchy the right call, or should this be a cgroup
+ controller despite the cases above?
+4. The predicate format is a JSON document and experimental. Is a binary
+ format preferred, or reuse of an existing ruleset format such as
+ Landlock's?
+5. unshare and setns have no LSM hook, so the series calls KAP from them
+ directly. Would a hook for namespace transitions be acceptable, or is there
+ an existing one that should be used instead?
+6. The LSM ID is provisional and should be confirmed.
+
+RFC status
+==========
+
+This interface is experimental. The RFC is intended to make the authority and
+lineage model reviewable, not to freeze the userspace ABI.
--
2.51.0
^ permalink raw reply related [flat|nested] 9+ messages in thread* [RFC PATCH v2 3/6] security: add the Kernel Assurance Plane LSM
2026-10-28 13:33 [RFC PATCH v2 0/6] security: introduce Kernel Assurance Plane Ketan Bhardwaj
2026-10-28 13:33 ` [RFC PATCH v2 1/6] docs: security: Describe the Kernel Assurance Plane design Ketan Bhardwaj
2026-10-28 13:33 ` [RFC PATCH v2 2/6] security: kap: add the LSM ID and internal headers Ketan Bhardwaj
@ 2026-10-28 13:33 ` Ketan Bhardwaj
2026-10-28 13:33 ` [RFC PATCH v2 4/6] security: kap: add the securityfs control plane Ketan Bhardwaj
` (2 subsequent siblings)
5 siblings, 0 replies; 9+ messages in thread
From: Ketan Bhardwaj @ 2026-10-28 13:33 UTC (permalink / raw)
To: Paul Moore, James Morris, Serge E . Hallyn
Cc: linux-security-module, Jonathan Corbet, Shuah Khan, Randy Dunlap,
linux-doc, linux-api, Mickaël Salaün,
Günther Noack, linux-kernel, HARIHARA SUDHAN . R,
Ketan Bhardwaj
Add the KAP LSM: Intent Lineage Groups (igroups), the authority
decision, and the hooks that consult it.
A task bound to an igroup carries the igroup and the authority epoch it
was bound under in its task blob, and fork copies both, so a binding
follows the whole process tree. Each hooked operation is resolved to a
target (a path, an address, a credential change) and checked against the
igroup's compiled allow and deny predicates. The decision fails closed
when the authority is absent, revoked or expired, when the target cannot
be resolved, or when the task's epoch is older than the igroup's, which
is how authority is revoked from tasks that are already running.
Predicates are compiled once at install, so a decision walks an array
and allocates nothing. Each decision is appended to the igroup's event
log, and denials also go to the kernel log.
Unbound tasks are not checked, with one exception: when kap.default is
closed, a task outside the initial PID namespace that is bound to no
igroup cannot start a program, so a failed bind cannot leave a workload
running without authority.
Hooks that run under other locks only take a spinlock and do not
allocate, and an unbound task returns before taking any lock.
Nothing can be bound until the control plane is added in the next
patch.
Assisted-by: Claude
Co-developed-by: HARIHARA SUDHAN. R <harisudhan@armoriq.io>
Signed-off-by: HARIHARA SUDHAN. R <harisudhan@armoriq.io>
Assisted-by: Codex
Signed-off-by: Ketan Bhardwaj <ketan@armoriq.io>
---
include/linux/lsm_count.h | 9 +-
security/Kconfig | 1 +
security/Makefile | 1 +
security/kap/Kconfig | 14 +
security/kap/Makefile | 2 +
security/kap/igroup.c | 945 ++++++++++++++++++++++++++++++++++++++
security/kap/kap_lsm.c | 865 ++++++++++++++++++++++++++++++++++
7 files changed, 1836 insertions(+), 1 deletion(-)
create mode 100644 security/kap/Kconfig
create mode 100644 security/kap/Makefile
create mode 100644 security/kap/igroup.c
create mode 100644 security/kap/kap_lsm.c
diff --git a/include/linux/lsm_count.h b/include/linux/lsm_count.h
index 16eb497..720bdd6 100644
--- a/include/linux/lsm_count.h
+++ b/include/linux/lsm_count.h
@@ -102,6 +102,12 @@
#define IPE_ENABLED
#endif
+#if IS_ENABLED(CONFIG_SECURITY_KAP)
+#define KAP_ENABLED 1,
+#else
+#define KAP_ENABLED
+#endif
+
/*
* There is a trailing comma that we need to be accounted for. This is done by
* using a skipped argument in __COUNT_LSMS
@@ -124,7 +130,8 @@
LANDLOCK_ENABLED \
IMA_ENABLED \
EVM_ENABLED \
- IPE_ENABLED)
+ IPE_ENABLED \
+ KAP_ENABLED)
#else
diff --git a/security/Kconfig b/security/Kconfig
index f7bf6cd..c2e6249 100644
--- a/security/Kconfig
+++ b/security/Kconfig
@@ -228,6 +228,7 @@ source "security/safesetid/Kconfig"
source "security/lockdown/Kconfig"
source "security/landlock/Kconfig"
source "security/ipe/Kconfig"
+source "security/kap/Kconfig"
source "security/integrity/Kconfig"
diff --git a/security/Makefile b/security/Makefile
index 4601230..6fb547e 100644
--- a/security/Makefile
+++ b/security/Makefile
@@ -26,6 +26,7 @@ obj-$(CONFIG_CGROUPS) += device_cgroup.o
obj-$(CONFIG_BPF_LSM) += bpf/
obj-$(CONFIG_SECURITY_LANDLOCK) += landlock/
obj-$(CONFIG_SECURITY_IPE) += ipe/
+obj-$(CONFIG_SECURITY_KAP) += kap/
# Object integrity file lists
obj-$(CONFIG_INTEGRITY) += integrity/
diff --git a/security/kap/Kconfig b/security/kap/Kconfig
new file mode 100644
index 0000000..bd67759
--- /dev/null
+++ b/security/kap/Kconfig
@@ -0,0 +1,14 @@
+config SECURITY_KAP
+ bool "Kernel Assurance Plane security module"
+ depends on SECURITY
+ select SECURITYFS
+ select SECURITY_PATH
+ help
+ The Kernel Assurance Plane binds tasks to Intent Lineage Groups and
+ checks their operations at LSM hooks against the authority compiled
+ for the objective each group serves. Privileged userspace validates
+ policy and installs it through /sys/kernel/security/kap.
+
+ See Documentation/security/kap.rst for details.
+
+ If you are unsure how to answer this question, answer N.
diff --git a/security/kap/Makefile b/security/kap/Makefile
new file mode 100644
index 0000000..40a864c
--- /dev/null
+++ b/security/kap/Makefile
@@ -0,0 +1,2 @@
+obj-$(CONFIG_SECURITY_KAP) := kap.o
+kap-y := kap_lsm.o igroup.o
diff --git a/security/kap/igroup.c b/security/kap/igroup.c
new file mode 100644
index 0000000..069905a
--- /dev/null
+++ b/security/kap/igroup.c
@@ -0,0 +1,945 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Kernel Assurance Plane: igroups and the authority decision.
+ *
+ * An igroup holds the authority compiled for one objective. Privileged
+ * userspace validates policy before installing it; the kernel stores only the
+ * compiled state and decides each hooked operation against the current task's
+ * igroup, epoch and mode. The control plane that creates igroups and installs
+ * authority is in kapfs.c.
+ */
+
+#define pr_fmt(fmt) "KAP: " fmt
+
+#include <linux/binfmts.h>
+#include <linux/errno.h>
+#include <linux/in.h>
+#include <linux/in6.h>
+#include <linux/kernel.h>
+#include <linux/limits.h>
+#include <linux/list.h>
+#include <linux/mm.h>
+#include <linux/spinlock.h>
+#include <linux/pid.h>
+#include <linux/pid_namespace.h>
+#include <linux/printk.h>
+#include <linux/ratelimit.h>
+#include <linux/sched.h>
+#include <linux/sched/task.h>
+#include <linux/slab.h>
+#include <linux/string.h>
+#include <linux/timekeeping.h>
+
+#include "kap.h"
+
+LIST_HEAD(kap_igroups);
+/*
+ * Guards kap_igroups and the state of every igroup on it. A spinlock, because
+ * some hooks run under other spinlocks; nothing sleeps while it is held.
+ */
+DEFINE_SPINLOCK(kap_lock);
+/* Set once the control plane exists; until then no task can be bound. */
+bool kap_ready;
+/* kap.default: false is "open", the default. */
+bool kap_default_closed;
+
+static bool kap_current_epoch_stale_locked(struct kap_igroup *igroup);
+
+static const char *kap_hook_name(enum kap_hook_kind kind)
+{
+ switch (kind) {
+ case KAP_HOOK_EXEC:
+ return "exec";
+ case KAP_HOOK_FILE:
+ return "file";
+ case KAP_HOOK_SOCKET_CONNECT:
+ return "net";
+ case KAP_HOOK_CRED:
+ return "cred";
+ case KAP_HOOK_IPC:
+ return "ipc";
+ case KAP_HOOK_NAMESPACE:
+ return "namespace";
+ case KAP_HOOK_DESCRIPTOR:
+ return "descriptor";
+ case KAP_HOOK_BPF:
+ return "bpf";
+ case KAP_HOOK_PTRACE:
+ return "ptrace";
+ case KAP_HOOK_PROCFS:
+ return "procfs";
+ case KAP_HOOK_EXECMEM:
+ return "execmem";
+ case KAP_HOOK_IO_URING:
+ return "io_uring";
+ default:
+ return "unknown";
+ }
+}
+
+static int kap_file_target(const struct file *file, char *target,
+ size_t target_len)
+{
+ char *path_buf;
+ char *path;
+ int rc = 0;
+
+ path_buf = kmalloc(PATH_MAX, GFP_KERNEL);
+ if (!path_buf)
+ return -ENOMEM;
+ path = d_path(&file->f_path, path_buf, PATH_MAX);
+ if (IS_ERR(path))
+ rc = PTR_ERR(path);
+ else
+ strscpy(target, path, target_len);
+ kfree(path_buf);
+ return rc;
+}
+
+static int kap_object_target(enum kap_hook_kind kind, const void *object,
+ char *target, size_t target_len)
+{
+ if (!object)
+ return -EINVAL;
+
+ switch (kind) {
+ case KAP_HOOK_EXEC: {
+ const struct linux_binprm *bprm = object;
+
+ if (!bprm->file)
+ return -EINVAL;
+ return kap_file_target(bprm->file, target, target_len);
+ }
+ case KAP_HOOK_FILE:
+ return kap_file_target(object, target, target_len);
+ case KAP_HOOK_PROCFS:
+ return kap_file_target(object, target, target_len);
+ case KAP_HOOK_SOCKET_CONNECT: {
+ const struct sockaddr *addr = object;
+
+ if (addr->sa_family == AF_INET) {
+ const struct sockaddr_in *in = object;
+
+ scnprintf(target, target_len, "%pI4:%u",
+ &in->sin_addr, ntohs(in->sin_port));
+ return 0;
+ }
+ if (addr->sa_family == AF_INET6) {
+ const struct sockaddr_in6 *in6 = object;
+
+ scnprintf(target, target_len, "%pI6c:%u",
+ &in6->sin6_addr, ntohs(in6->sin6_port));
+ return 0;
+ }
+ /* Other families, vsock among them, are named so a grant can allow them. */
+ scnprintf(target, target_len, "family:%u", addr->sa_family);
+ return 0;
+ }
+ case KAP_HOOK_CRED:
+ strscpy(target, "cred", target_len);
+ return 0;
+ case KAP_HOOK_IPC:
+ strscpy(target, "ipc", target_len);
+ return 0;
+ case KAP_HOOK_NAMESPACE:
+ strscpy(target, "namespace", target_len);
+ return 0;
+ case KAP_HOOK_DESCRIPTOR:
+ return kap_file_target(object, target, target_len);
+ case KAP_HOOK_BPF:
+ strscpy(target, "bpf", target_len);
+ return 0;
+ case KAP_HOOK_PTRACE:
+ strscpy(target, "ptrace", target_len);
+ return 0;
+ case KAP_HOOK_EXECMEM:
+ return kap_file_target(object, target, target_len);
+ default:
+ return -EINVAL;
+ }
+}
+
+static bool kap_file_prefix_match(const char *target, const char *candidate)
+{
+ size_t len = strlen(candidate);
+ size_t target_len = strlen(target);
+
+ if (!strcmp(target, candidate))
+ return true;
+ if (!len || target_len <= len)
+ return false;
+ if (candidate[len - 1] == '/')
+ return !strncmp(target, candidate, len);
+ return target[len] == '/' && !strncmp(target, candidate, len);
+}
+
+static bool kap_string_match(const char *target, const char *candidate)
+{
+ size_t len = strlen(candidate);
+
+ if (!strcmp(candidate, "*"))
+ return true;
+ if (!strcmp(target, candidate))
+ return true;
+ if (len && candidate[len - 1] == '*')
+ return !strncmp(target, candidate, len - 1);
+ return false;
+}
+
+static bool kap_target_match(enum kap_hook_kind kind, const char *target,
+ const char *candidate)
+{
+ if (!strcmp(candidate, "*"))
+ return true;
+ if (kind == KAP_HOOK_FILE || kind == KAP_HOOK_DESCRIPTOR ||
+ kind == KAP_HOOK_PROCFS)
+ return kap_file_prefix_match(target, candidate);
+ return kap_string_match(target, candidate);
+}
+
+static const char *kap_next_predicate(const char *cursor, const char *end,
+ const char *kind_name)
+{
+ char needle[64];
+ const char *found;
+
+ scnprintf(needle, sizeof(needle), "\"kind\":\"%s\"", kind_name);
+ found = strstr(cursor, needle);
+ if (!found || found >= end)
+ return NULL;
+ return found;
+}
+
+/*
+ * Append one pattern to the pool. The caller compiles a whole set before moving
+ * on, so entries land contiguously and can be walked by length.
+ */
+static int kap_intern_pattern(struct kap_compiled_authority *compiled,
+ struct kap_pattern_set *set,
+ const char *value, size_t len)
+{
+ if ((size_t)compiled->pool_used + len + 1 > sizeof(compiled->pool))
+ return -E2BIG;
+ memcpy(compiled->pool + compiled->pool_used, value, len);
+ compiled->pool[compiled->pool_used + len] = '\0';
+ compiled->pool_used += len + 1;
+ set->count++;
+ return 0;
+}
+
+/* Walk one "allow" or "deny" array of a predicate block and intern its entries. */
+static int kap_compile_array(struct kap_compiled_authority *compiled,
+ struct kap_pattern_set *set,
+ const char *start, const char *end,
+ const char *array_name)
+{
+ char needle[32];
+ const char *array;
+ const char *cursor;
+
+ scnprintf(needle, sizeof(needle), "\"%s\":[", array_name);
+ array = strstr(start, needle);
+ if (!array || array >= end)
+ return 0;
+ cursor = array + strlen(needle);
+ while (cursor < end && *cursor && *cursor != ']') {
+ const char *value_start;
+ const char *value_end;
+ int rc;
+
+ while (cursor < end && *cursor && *cursor != '"' && *cursor != ']')
+ cursor++;
+ if (cursor >= end || *cursor == ']')
+ break;
+ value_start = ++cursor;
+ value_end = value_start;
+ while (value_end < end && *value_end && *value_end != '"')
+ value_end++;
+ if (value_end >= end || !*value_end)
+ break;
+ rc = kap_intern_pattern(compiled, set, value_start,
+ value_end - value_start);
+ if (rc)
+ return rc;
+ cursor = value_end + 1;
+ }
+ return 0;
+}
+
+/* Collect one array across every predicate of a kind, keeping the set contiguous. */
+static int kap_compile_kind_array(struct kap_compiled_authority *compiled,
+ struct kap_pattern_set *set,
+ const char *auth, const char *end,
+ const char *kind_name, const char *array_name)
+{
+ const char *predicate = auth;
+
+ set->offset = compiled->pool_used;
+ set->count = 0;
+ while ((predicate = kap_next_predicate(predicate, end, kind_name))) {
+ const char *next = strstr(predicate + 1, "\"kind\":");
+ const char *predicate_start = predicate;
+ const char *predicate_end = next && next < end ? next : end;
+ int rc;
+
+ while (predicate_start > auth && *predicate_start != '{')
+ predicate_start--;
+ rc = kap_compile_array(compiled, set, predicate_start,
+ predicate_end, array_name);
+ if (rc)
+ return rc;
+ predicate = predicate_end;
+ }
+ return 0;
+}
+
+/*
+ * Compile a document into per-kind pattern sets. Allow and deny are collected in
+ * separate passes per kind, because a document may contain several predicates
+ * of one kind and interleaving them would break the contiguous layout the
+ * matcher relies on.
+ */
+static int kap_compile_authority(struct kap_compiled_authority *compiled,
+ const char *auth, size_t auth_len)
+{
+ const char *end = auth + auth_len;
+ int kind;
+ int rc;
+
+ memset(compiled, 0, sizeof(*compiled));
+ for (kind = 0; kind <= KAP_HOOK_LAST; kind++) {
+ const char *kind_name = kap_hook_name(kind);
+
+ rc = kap_compile_kind_array(compiled, &compiled->allow[kind],
+ auth, end, kind_name, "allow");
+ if (rc)
+ return rc;
+ rc = kap_compile_kind_array(compiled, &compiled->deny[kind],
+ auth, end, kind_name, "deny");
+ if (rc)
+ return rc;
+ }
+ return 0;
+}
+
+static bool kap_authority_match(struct kap_igroup *igroup, enum kap_hook_kind kind,
+ const char *target, bool deny_array)
+{
+ const struct kap_compiled_authority *compiled = igroup->compiled;
+ const struct kap_pattern_set *set;
+ const char *pattern;
+ u32 i;
+
+ if (!compiled || kind > KAP_HOOK_LAST)
+ return false;
+ set = deny_array ? &compiled->deny[kind] : &compiled->allow[kind];
+ pattern = compiled->pool + set->offset;
+ for (i = 0; i < set->count; i++) {
+ if (kap_target_match(kind, target, pattern))
+ return true;
+ pattern += strlen(pattern) + 1;
+ }
+ return false;
+}
+
+static bool kap_is_known_kind_name(const char *value, size_t len)
+{
+ int kind;
+
+ for (kind = KAP_HOOK_EXEC; kind <= KAP_HOOK_LAST; kind++) {
+ const char *known = kap_hook_name(kind);
+
+ if (strlen(known) == len && !strncmp(value, known, len))
+ return true;
+ }
+ return false;
+}
+
+static bool kap_range_contains(const char *start, const char *end, const char *needle)
+{
+ const char *found = strstr(start, needle);
+
+ return found && found < end;
+}
+
+static int kap_validate_authority_payload(const char *payload)
+{
+ const char *cursor = payload;
+ const char *end = payload + strlen(payload);
+ bool saw_kind = false;
+
+ if (payload[0] != '{' || !strstr(payload, "\"predicates\":["))
+ return -EINVAL;
+
+ while ((cursor = strstr(cursor, "\"kind\":\""))) {
+ const char *kind_start = cursor + strlen("\"kind\":\"");
+ const char *kind_end = strchr(kind_start, '"');
+ const char *next_kind = strstr(kind_start, "\"kind\":\"");
+ const char *predicate_start = cursor;
+ const char *predicate_end = next_kind ? next_kind : end;
+
+ if (!kind_end)
+ return -EINVAL;
+ if (!kap_is_known_kind_name(kind_start, kind_end - kind_start))
+ return -EINVAL;
+ while (predicate_start > payload && *predicate_start != '{')
+ predicate_start--;
+ if (!kap_range_contains(predicate_start, predicate_end, "\"allow\":["))
+ return -EINVAL;
+ if (!kap_range_contains(predicate_start, predicate_end, "\"deny\":["))
+ return -EINVAL;
+ saw_kind = true;
+ cursor = kind_end + 1;
+ }
+ return saw_kind ? 0 : -EINVAL;
+}
+
+/*
+ * With kap.default = closed, an unbound task outside the initial PID namespace
+ * cannot start a program, so a workload whose bind failed does not run without
+ * authority.
+ *
+ * Only exec is refused. Setup work that runs in the new PID namespace before
+ * the bind, such as mounts, keeps working. Tasks in the initial PID namespace
+ * are never refused.
+ */
+static bool kap_unbound_refused(enum kap_hook_kind kind)
+{
+ if (!READ_ONCE(kap_default_closed))
+ return false;
+ if (kind != KAP_HOOK_EXEC && kind != KAP_HOOK_EXECMEM)
+ return false;
+ return task_active_pid_ns(current) != &init_pid_ns;
+}
+
+/*
+ * No igroup means nowhere to append an event, so the kernel log is the only
+ * record of an unbound refusal.
+ */
+static int kap_refuse_unbound(enum kap_hook_kind kind, const char *target)
+{
+ pr_warn_ratelimited("deny igroup=<unbound> op=%s target=%s reason=unbound_task pid=%d\n",
+ kap_hook_name(kind), target ? target : "",
+ task_pid_nr(current));
+ return -EACCES;
+}
+
+/*
+ * Exported so the LSM layer can tell whether to keep going for a task it would
+ * otherwise skip. Its hooks return early for any unbound task, which is exactly
+ * the task fail-closed exists to stop, so that shortcut has to consult this.
+ */
+bool kap_fail_closed_kind(enum kap_hook_kind kind)
+{
+ return kap_unbound_refused(kind);
+}
+
+/*
+ * Refuse and name the gate that did it. Only the authority-state gates set a
+ * reason: a predicate match leaves *why NULL so the caller's own allow/deny
+ * strings stay in use for the common path.
+ */
+static int kap_refuse(const char **why, const char *reason)
+{
+ if (why)
+ *why = reason;
+ return -EACCES;
+}
+
+static int kap_authority_decision_locked(struct kap_igroup *igroup,
+ enum kap_hook_kind kind,
+ const char *target,
+ const char **why)
+{
+ if (why)
+ *why = NULL;
+ if (kap_current_epoch_stale_locked(igroup))
+ return kap_refuse(why, "authority_epoch_stale");
+ /*
+ * Checked on every decision, not once at install. It bounds how long a
+ * running authority lives; it does not protect against a clock that
+ * has been moved back.
+ */
+ if (igroup->expires_at &&
+ (u64)ktime_get_real_seconds() >= igroup->expires_at)
+ return kap_refuse(why, "authority_expired");
+ if (igroup->revoked)
+ return kap_refuse(why, "authority_revoked");
+ if (!igroup->authority_len)
+ return kap_refuse(why, "authority_absent");
+ if (!target || !*target)
+ return kap_refuse(why, "target_unresolved");
+ if (kap_authority_match(igroup, kind, target, true))
+ return -EACCES;
+ if (!kap_authority_match(igroup, kind, target, false))
+ return -EACCES;
+ return 0;
+}
+
+struct kap_igroup *kap_find_igroup_locked(const char *name)
+{
+ struct kap_igroup *igroup;
+
+ list_for_each_entry(igroup, &kap_igroups, node) {
+ if (!strcmp(igroup->name, name))
+ return igroup;
+ }
+ return NULL;
+}
+
+static bool kap_pid_in_igroup_locked(struct kap_igroup *igroup, pid_t pid)
+{
+ struct kap_bound_pid *entry;
+
+ list_for_each_entry(entry, &igroup->pids, node) {
+ if (entry->pid == pid)
+ return true;
+ }
+ return false;
+}
+
+static struct kap_igroup *kap_find_current_igroup_locked(void)
+{
+ struct kap_task_state *state = kap_task_state(current);
+
+ if (!state || !state->active)
+ return NULL;
+ return kap_find_igroup_locked(state->igroup_name);
+}
+
+static bool kap_current_epoch_stale_locked(struct kap_igroup *igroup)
+{
+ struct kap_task_state *state = kap_task_state(current);
+
+ return state && state->active && !strcmp(state->igroup_name, igroup->name) &&
+ state->epoch != igroup->epoch;
+}
+
+static bool kap_current_task_active(void)
+{
+ struct kap_task_state *state = kap_task_state(current);
+
+ return state && state->active;
+}
+
+void kap_trim_newline(char *value)
+{
+ size_t len = strlen(value);
+
+ while (len && (value[len - 1] == '\n' || value[len - 1] == '\r')) {
+ value[len - 1] = '\0';
+ len--;
+ }
+}
+
+static void kap_append_event_locked(struct kap_igroup *igroup,
+ enum kap_hook_kind kind,
+ bool allowed,
+ const char *reason,
+ const char *target)
+{
+ char line[512];
+ char escaped_target[192];
+ u64 now = ktime_get_real_ns();
+ int len;
+ size_t out = 0;
+ size_t i;
+
+ escaped_target[0] = '\0';
+ if (target) {
+ for (i = 0; target[i] && out + 2 < sizeof(escaped_target); i++) {
+ if (target[i] == '"' || target[i] == '\\') {
+ if (out + 3 >= sizeof(escaped_target))
+ break;
+ escaped_target[out++] = '\\';
+ escaped_target[out++] = target[i];
+ } else if (target[i] == '\n' || target[i] == '\r' || target[i] == '\t') {
+ escaped_target[out++] = ' ';
+ } else if ((unsigned char)target[i] < 0x20) {
+ escaped_target[out++] = '?';
+ } else {
+ escaped_target[out++] = target[i];
+ }
+ }
+ escaped_target[out] = '\0';
+ }
+
+ /*
+ * kap.events halves itself when full, so allowed traffic can evict a
+ * denial. Mirror denials to the kernel log, before the ring write so
+ * the early returns below cannot drop the record.
+ */
+ if (!allowed)
+ pr_warn_ratelimited("deny igroup=%s op=%s target=%s reason=%s epoch=%llu pid=%d\n",
+ igroup->name, kap_hook_name(kind),
+ escaped_target, reason, igroup->epoch,
+ task_pid_nr(current));
+
+ len = scnprintf(line, sizeof(line),
+ "{\"ts_ns\":%llu,\"igroup\":\"%s\",\"decision\":\"%s\",\"operation\":\"%s\",\"target\":\"%s\",\"reason\":\"%s\",\"epoch\":%llu,\"root\":\"%s\",\"pid\":%d}\n",
+ now, igroup->name, allowed ? "allow" : "deny",
+ kap_hook_name(kind), escaped_target, reason, igroup->epoch,
+ igroup->lineage_root, task_pid_nr(current));
+ if (len <= 0)
+ return;
+ if (len >= KAP_MAX_EVENTS_BYTES) {
+ igroup->events_len = 0;
+ return;
+ }
+ if (igroup->events_len + len >= KAP_MAX_EVENTS_BYTES) {
+ size_t keep = KAP_MAX_EVENTS_BYTES / 2;
+
+ memmove(igroup->events, igroup->events + igroup->events_len - keep, keep);
+ igroup->events_len = keep;
+ }
+ memcpy(igroup->events + igroup->events_len, line, len);
+ igroup->events_len += len;
+}
+
+/* The common case: an unbound task is never checked and takes no lock. */
+static bool kap_skip_current(enum kap_hook_kind kind)
+{
+ if (!READ_ONCE(kap_ready))
+ return true;
+ return !kap_current_task_active() && !kap_unbound_refused(kind);
+}
+
+/*
+ * A program is allowed by the path it resolves to, and refused by a deny on
+ * either that path or the name it was started with. "..", relative names and
+ * symlinks can therefore neither step around a deny nor borrow an allow.
+ */
+static int kap_exec_decision_locked(struct kap_igroup *igroup,
+ const char *resolved, const char *name,
+ const char **why)
+{
+ int rc;
+
+ rc = kap_authority_decision_locked(igroup, KAP_HOOK_EXEC, resolved, why);
+ if (*why || !name || !*name || !strcmp(name, resolved))
+ return rc;
+ if (kap_authority_match(igroup, KAP_HOOK_EXEC, resolved, true) ||
+ kap_authority_match(igroup, KAP_HOOK_EXEC, name, true))
+ return -EACCES;
+ return rc;
+}
+
+static int kap_decide_locked(struct kap_igroup *igroup, enum kap_hook_kind kind,
+ const void *object, const char *target,
+ const char **why)
+{
+ if (kind == KAP_HOOK_EXEC) {
+ const struct linux_binprm *bprm = object;
+
+ return kap_exec_decision_locked(igroup, target, bprm->filename, why);
+ }
+ return kap_authority_decision_locked(igroup, kind, target, why);
+}
+
+int kap_check_current_target_why(enum kap_hook_kind kind, const char *target,
+ const char **why)
+{
+ const char *gate = NULL;
+ struct kap_igroup *igroup;
+ int rc = 0;
+
+ if (why)
+ *why = NULL;
+ if (kap_skip_current(kind))
+ return 0;
+
+ spin_lock(&kap_lock);
+ igroup = kap_find_current_igroup_locked();
+ if (!igroup) {
+ if (kap_unbound_refused(kind))
+ rc = kap_refuse_unbound(kind, target);
+ goto out;
+ }
+ rc = kap_authority_decision_locked(igroup, kind, target, &gate);
+ if (why)
+ *why = gate;
+out:
+ spin_unlock(&kap_lock);
+ return rc;
+}
+
+int kap_check_current_emit(enum kap_hook_kind kind, const void *object,
+ const char *deny_reason, const char *allow_reason)
+{
+ char *target;
+ struct kap_igroup *igroup;
+ const char *why = NULL;
+ int rc = 0;
+
+ if (kap_skip_current(kind))
+ return 0;
+
+ target = kmalloc(PATH_MAX, GFP_KERNEL);
+ if (!target)
+ return -ENOMEM;
+ if (kap_object_target(kind, object, target, PATH_MAX)) {
+ kfree(target);
+ return -EACCES;
+ }
+
+ spin_lock(&kap_lock);
+ igroup = kap_find_current_igroup_locked();
+ if (!igroup) {
+ if (kap_unbound_refused(kind))
+ rc = kap_refuse_unbound(kind, target);
+ goto out;
+ }
+ rc = kap_decide_locked(igroup, kind, object, target, &why);
+ kap_append_event_locked(igroup, kind, rc == 0,
+ rc ? (why ? why : deny_reason) : allow_reason, target);
+out:
+ spin_unlock(&kap_lock);
+ kfree(target);
+ return rc;
+}
+
+int kap_check_current_target_emit(enum kap_hook_kind kind, const char *target,
+ const char *deny_reason, const char *allow_reason)
+{
+ struct kap_igroup *igroup;
+ const char *why = NULL;
+ int rc = 0;
+
+ if (kap_skip_current(kind))
+ return 0;
+
+ spin_lock(&kap_lock);
+ igroup = kap_find_current_igroup_locked();
+ if (!igroup) {
+ if (kap_unbound_refused(kind))
+ rc = kap_refuse_unbound(kind, target);
+ goto out;
+ }
+ rc = kap_authority_decision_locked(igroup, kind, target, &why);
+ kap_append_event_locked(igroup, kind, rc == 0,
+ rc ? (why ? why : deny_reason) : allow_reason, target);
+out:
+ spin_unlock(&kap_lock);
+ return rc;
+}
+
+void kap_emit_current_event_target(enum kap_hook_kind kind, int decision,
+ const char *reason, const char *target)
+{
+ struct kap_igroup *igroup;
+
+ if (!READ_ONCE(kap_ready) || !kap_current_task_active())
+ return;
+
+ spin_lock(&kap_lock);
+ igroup = kap_find_current_igroup_locked();
+ if (igroup)
+ kap_append_event_locked(igroup, kind, decision, reason, target);
+ spin_unlock(&kap_lock);
+}
+
+int kap_parse_transition_sequence(const char *payload, u64 *sequence)
+{
+ const char *cursor = strstr(payload, "\"sequence\"");
+ u64 value = 0;
+ bool seen_digit = false;
+
+ if (!cursor)
+ cursor = strstr(payload, "sequence");
+ if (!cursor)
+ return -EINVAL;
+ cursor = strchr(cursor, ':');
+ if (!cursor)
+ return -EINVAL;
+ cursor++;
+ while (*cursor == ' ' || *cursor == '\t')
+ cursor++;
+ while (*cursor >= '0' && *cursor <= '9') {
+ seen_digit = true;
+ value = value * 10 + (*cursor - '0');
+ cursor++;
+ }
+ if (!seen_digit)
+ return -EINVAL;
+ *sequence = value;
+ return 0;
+}
+
+/*
+ * kap.install takes the whole document in one write so the lineage root, epoch,
+ * expiry and predicates commit together or not at all. Writing them one file at
+ * a time cannot be made safe: kap.authority_epoch refuses a lower value with
+ * -ESTALE, deliberately, so once the epoch has advanced there is no way back and
+ * a later refusal leaves every bound task stale against an epoch no authority
+ * was ever installed for.
+ *
+ * Line oriented rather than JSON, because the authority value is itself JSON and
+ * nesting it would mean parsing JSON with strstr twice. Every key may appear at
+ * most once and an unrecognised key is refused rather than ignored, so a typo
+ * fails the install instead of silently dropping a field.
+ *
+ * The whole document has to arrive in one write(). Each write is parsed on its
+ * own, so a caller that splits the document sees the first chunk refused for
+ * missing keys rather than a half-applied install, which is the usual contract
+ * for a command file.
+ */
+int kap_install_parse(char *payload, struct kap_install_request *req)
+{
+ char *cursor = payload;
+ char *line;
+
+ memset(req, 0, sizeof(*req));
+
+ while ((line = strsep(&cursor, "\n"))) {
+ char *value;
+
+ if (!*line)
+ continue;
+ value = strchr(line, '=');
+ if (!value)
+ return -EINVAL;
+ *value++ = '\0';
+
+ if (!strcmp(line, "lineage_root")) {
+ if (req->have_root || strlen(value) > KAP_MAX_ROOT_LEN)
+ return -EINVAL;
+ strscpy(req->lineage_root, value,
+ sizeof(req->lineage_root));
+ req->have_root = true;
+ } else if (!strcmp(line, "authority_epoch")) {
+ if (req->have_epoch || kstrtou64(value, 10, &req->epoch))
+ return -EINVAL;
+ req->have_epoch = true;
+ } else if (!strcmp(line, "expires_at")) {
+ if (req->have_expires_at ||
+ kstrtou64(value, 10, &req->expires_at))
+ return -EINVAL;
+ req->have_expires_at = true;
+ } else if (!strcmp(line, "transition")) {
+ if (req->have_transition ||
+ kap_parse_transition_sequence(value, &req->sequence))
+ return -EINVAL;
+ req->have_transition = true;
+ } else if (!strcmp(line, "authority")) {
+ if (req->have_authority)
+ return -EINVAL;
+ req->authority = value;
+ req->have_authority = true;
+ } else {
+ return -EINVAL;
+ }
+ }
+
+ if (!req->have_root || !req->have_epoch || !req->have_authority)
+ return -EINVAL;
+ return 0;
+}
+
+/*
+ * Validate and compile an authority document. Runs before kap_lock is taken,
+ * because compiling allocates.
+ */
+int kap_authority_prepare(const char *doc, struct kap_compiled_authority **out)
+{
+ struct kap_compiled_authority *next;
+ size_t len;
+ int rc;
+
+ *out = NULL;
+ rc = kap_validate_authority_payload(doc);
+ if (rc)
+ return rc;
+ len = strlen(doc);
+ /* Stored whole in the igroup, so it has to fit with its terminator. */
+ if (len >= KAP_MAX_PREDICATE_BYTES)
+ return -E2BIG;
+
+ next = kvzalloc_obj(*next, GFP_KERNEL);
+ if (!next)
+ return -ENOMEM;
+ rc = kap_compile_authority(next, doc, len);
+ if (rc) {
+ kvfree(next);
+ return rc;
+ }
+ *out = next;
+ return 0;
+}
+
+/*
+ * Commit a prepared install. On success *compiled is swapped with the
+ * igroup's previous authority, which the caller frees after dropping the lock.
+ */
+int kap_install_locked(struct kap_igroup *igroup,
+ const struct kap_install_request *req,
+ struct kap_compiled_authority **compiled)
+{
+ struct kap_compiled_authority *old;
+
+ /*
+ * Everything that can be refused is refused here, before the igroup is
+ * touched at all. The individual files apply the same rules; keep them
+ * in step.
+ */
+ if (req->epoch < igroup->epoch)
+ return -ESTALE;
+ if (req->expires_at &&
+ (u64)ktime_get_real_seconds() >= req->expires_at)
+ return -ESTALE;
+ if (req->have_transition && igroup->has_sequence &&
+ req->sequence <= igroup->last_sequence)
+ return -EALREADY;
+
+ /* Nothing below can fail, which is what makes this a transaction. */
+ strscpy(igroup->lineage_root, req->lineage_root,
+ sizeof(igroup->lineage_root));
+ igroup->epoch = req->epoch;
+ igroup->revoked = false;
+ igroup->expires_at = req->expires_at;
+ if (req->have_transition) {
+ igroup->last_sequence = req->sequence;
+ igroup->has_sequence = true;
+ }
+ strscpy(igroup->authority, req->authority, sizeof(igroup->authority));
+ igroup->authority_len = strlen(igroup->authority);
+ old = igroup->compiled;
+ igroup->compiled = *compiled;
+ *compiled = old;
+ return 0;
+}
+
+/*
+ * Bind every thread of the process with this pid. *entry is allocated by the
+ * caller before taking kap_lock and is consumed when the pid is recorded.
+ */
+int kap_bind_pid_locked(struct kap_igroup *igroup, pid_t pid,
+ struct kap_bound_pid **entry)
+{
+ struct task_struct *task, *t;
+
+ if (pid <= 0)
+ return -EINVAL;
+ rcu_read_lock();
+ task = pid_task(find_vpid(pid), PIDTYPE_PID);
+ if (!task) {
+ rcu_read_unlock();
+ return -ESRCH;
+ }
+ for_each_thread(task, t) {
+ struct kap_task_state *state = kap_task_state(t);
+
+ if (!state)
+ continue;
+ state->active = true;
+ state->epoch = igroup->epoch;
+ strscpy(state->igroup_name, igroup->name, sizeof(state->igroup_name));
+ }
+ rcu_read_unlock();
+
+ if (kap_pid_in_igroup_locked(igroup, pid))
+ return 0;
+ (*entry)->pid = pid;
+ list_add_tail(&(*entry)->node, &igroup->pids);
+ *entry = NULL;
+ return 0;
+}
diff --git a/security/kap/kap_lsm.c b/security/kap/kap_lsm.c
new file mode 100644
index 0000000..b148b34
--- /dev/null
+++ b/security/kap/kap_lsm.c
@@ -0,0 +1,865 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Kernel Assurance Plane LSM prototype.
+ *
+ * This file wires KAP into the LSM hooks. igroup state and decisions are in
+ * igroup.c, and the securityfs control plane that installs authority is in
+ * kapfs.c.
+ */
+
+#define pr_fmt(fmt) "KAP: " fmt
+
+#include <linux/binfmts.h>
+#include <linux/cred.h>
+#include <linux/bpf.h>
+#include <linux/dcache.h>
+#include <linux/fs.h>
+#include <linux/kap.h>
+#include <linux/limits.h>
+#include <linux/lsm_hooks.h>
+#include <linux/mm.h>
+#include <linux/mman.h>
+#include <linux/mount.h>
+#include <linux/nsproxy.h>
+#include <linux/sched.h>
+#include <linux/security.h>
+#include <linux/slab.h>
+#include <linux/socket.h>
+#include <linux/errno.h>
+#include <linux/string.h>
+#include <net/sock.h>
+
+#include "kap.h"
+
+#define KAP_LSM_TARGET_LEN 256
+
+struct lsm_blob_sizes kap_blob_sizes __ro_after_init = {
+ .lbs_task = sizeof(struct kap_task_state),
+};
+
+bool kap_lsm_active;
+struct io_uring_cmd;
+
+static char *kap_lsm_file_path_alloc(struct file *file);
+static int kap_lsm_path_target(const struct path *path, char *target, size_t target_len);
+static bool kap_path_is_memfd(const char *path);
+static bool kap_path_is_anonymous_channel(const char *path);
+static bool kap_lsm_current_active(void);
+
+struct kap_task_state *kap_task_state(struct task_struct *task)
+{
+ if (!READ_ONCE(kap_lsm_active) || !task || !task->security)
+ return NULL;
+ return (struct kap_task_state *)((char *)task->security + kap_blob_sizes.lbs_task);
+}
+
+static int kap_task_alloc(struct task_struct *task, u64 clone_flags)
+{
+ struct kap_task_state *old = kap_task_state(current);
+ struct kap_task_state *new = kap_task_state(task);
+ int rc;
+
+ if (!new)
+ return 0;
+
+ rc = kap_check_namespace_flags(clone_flags, "clone");
+ if (rc)
+ return rc;
+
+ memset(new, 0, sizeof(*new));
+ /* Under kap_lock, so a bind running now is copied whole or not at all. */
+ spin_lock(&kap_lock);
+ if (old && old->active)
+ *new = *old;
+ spin_unlock(&kap_lock);
+ return 0;
+}
+
+static void kap_task_free(struct task_struct *task)
+{
+ struct kap_task_state *state = kap_task_state(task);
+
+ if (state)
+ memset(state, 0, sizeof(*state));
+}
+
+static bool kap_lsm_current_active(void)
+{
+ struct kap_task_state *state = kap_task_state(current);
+
+ return state && state->active;
+}
+
+static int kap_bprm_check_security(struct linux_binprm *bprm)
+{
+ char *target;
+
+ /*
+ * An unbound task is skipped everywhere else, but under
+ * kap.default = closed an unbound exec is the one thing that must not
+ * proceed.
+ */
+ if (!kap_lsm_current_active() && !kap_fail_closed_kind(KAP_HOOK_EXEC))
+ return 0;
+ if (bprm->file) {
+ target = kap_lsm_file_path_alloc(bprm->file);
+ if (!IS_ERR(target)) {
+ if (kap_path_is_memfd(target)) {
+ int rc = kap_check_current_target_emit(KAP_HOOK_EXECMEM, target,
+ "memfd_exec_predicate_denied",
+ "memfd_exec_allowed");
+ kfree(target);
+ return rc;
+ }
+ kfree(target);
+ }
+ }
+ return kap_check_current_emit(KAP_HOOK_EXEC, bprm,
+ "exec_predicate_denied", "exec_allowed");
+}
+
+/*
+ * Resolves a file to a PATH_MAX buffer the caller must kfree, or an ERR_PTR.
+ * The buffer is allocated rather than kept on the stack: these hooks run deep
+ * in syscall paths and a 4K frame does not belong on a 16K kernel stack.
+ */
+static char *kap_lsm_file_path_alloc(struct file *file)
+{
+ char *buf;
+ char *path;
+
+ if (!file)
+ return ERR_PTR(-EINVAL);
+ buf = kmalloc(PATH_MAX, GFP_KERNEL);
+ if (!buf)
+ return ERR_PTR(-ENOMEM);
+ path = d_path(&file->f_path, buf, PATH_MAX);
+ if (IS_ERR(path)) {
+ kfree(buf);
+ return ERR_CAST(path);
+ }
+ /* d_path builds backwards from the end, so move the result to the front
+ * and hand back a single pointer.
+ */
+ memmove(buf, path, strlen(path) + 1);
+ return buf;
+}
+
+static int kap_lsm_path_target(const struct path *path, char *target, size_t target_len)
+{
+ char *path_buf;
+ char *rendered;
+ int rc = 0;
+
+ if (!path)
+ return -EINVAL;
+ path_buf = kmalloc(PATH_MAX, GFP_KERNEL);
+ if (!path_buf)
+ return -ENOMEM;
+ rendered = d_path(path, path_buf, PATH_MAX);
+ if (IS_ERR(rendered))
+ rc = PTR_ERR(rendered);
+ else
+ strscpy(target, rendered, target_len);
+ kfree(path_buf);
+ return rc;
+}
+
+static bool kap_path_is_procfs(const char *path)
+{
+ return !strcmp(path, "/proc") || !strncmp(path, "/proc/", strlen("/proc/"));
+}
+
+/*
+ * Rewrites /proc/<pid>/... to /proc/self/... in place. target is a PATH_MAX
+ * buffer from kap_lsm_file_path_alloc. Done in place rather than through a
+ * second PATH_MAX buffer, which would not fit on the stack.
+ */
+static void kap_normalize_procfs_self(char *target)
+{
+ static const char self[] = "/proc/self";
+ const size_t self_len = sizeof(self) - 1;
+ char prefix[32];
+ size_t prefix_len;
+ size_t tail_len;
+
+ prefix_len = scnprintf(prefix, sizeof(prefix), "/proc/%d", task_pid_nr(current));
+ if (strncmp(target, prefix, prefix_len))
+ return;
+ if (target[prefix_len] != '\0' && target[prefix_len] != '/')
+ return;
+ tail_len = strlen(target + prefix_len);
+ if (self_len + tail_len >= PATH_MAX)
+ return;
+ memmove(target + self_len, target + prefix_len, tail_len + 1);
+ memcpy(target, self, self_len);
+}
+
+static bool kap_path_is_memfd(const char *path)
+{
+ return strstr(path, "/memfd:") || strstr(path, "memfd:");
+}
+
+static bool kap_path_is_anonymous_channel(const char *path)
+{
+ return !strncmp(path, "pipe:", strlen("pipe:")) ||
+ !strncmp(path, "socket:", strlen("socket:")) ||
+ !strncmp(path, "anon_inode:", strlen("anon_inode:"));
+}
+
+static int kap_file_open(struct file *file)
+{
+ char *target;
+
+ if (!kap_lsm_current_active())
+ return 0;
+ target = kap_lsm_file_path_alloc(file);
+ if (!IS_ERR(target)) {
+ if (kap_path_is_procfs(target)) {
+ int rc;
+
+ kap_normalize_procfs_self(target);
+ rc = kap_check_current_target_emit(KAP_HOOK_PROCFS, target,
+ "procfs_predicate_denied",
+ "procfs_allowed");
+ kfree(target);
+ return rc;
+ }
+ kfree(target);
+ }
+ return kap_check_current_emit(KAP_HOOK_FILE, file,
+ "file_predicate_denied", "file_allowed");
+}
+
+static int kap_file_permission(struct file *file, int mask)
+{
+ const char *why = NULL;
+ char *target;
+ int rc;
+
+ if (!kap_lsm_current_active())
+ return 0;
+ if (!(mask & (MAY_READ | MAY_WRITE | MAY_APPEND | MAY_EXEC)))
+ return 0;
+ target = kap_lsm_file_path_alloc(file);
+ if (IS_ERR(target))
+ return kap_check_current_emit(KAP_HOOK_FILE, file,
+ "file_use_predicate_denied",
+ "file_use_allowed");
+ if (kap_path_is_anonymous_channel(target)) {
+ kfree(target);
+ return 0;
+ }
+ if (kap_path_is_procfs(target)) {
+ kap_normalize_procfs_self(target);
+ rc = kap_check_current_target_why(KAP_HOOK_PROCFS, target, &why);
+ if (rc)
+ kap_emit_current_event_target(KAP_HOOK_PROCFS, 0,
+ why ?: "procfs_use_predicate_denied", target);
+ } else {
+ rc = kap_check_current_target_why(KAP_HOOK_FILE, target, &why);
+ if (rc)
+ kap_emit_current_event_target(KAP_HOOK_FILE, 0,
+ why ?: "file_use_predicate_denied", target);
+ }
+ kfree(target);
+ return rc;
+}
+
+static int kap_mmap_file(struct file *file, unsigned long reqprot,
+ unsigned long prot, unsigned long flags)
+{
+ char *target;
+ int rc = 0;
+
+ if (!kap_lsm_current_active())
+ return 0;
+ if (!(reqprot & PROT_EXEC) && !(prot & PROT_EXEC))
+ return 0;
+ if (!file)
+ return kap_check_current_target_emit(KAP_HOOK_EXECMEM, "anonymous",
+ "execmem_predicate_denied",
+ "execmem_allowed");
+ target = kap_lsm_file_path_alloc(file);
+ if (IS_ERR(target))
+ return 0;
+ if (kap_path_is_memfd(target))
+ rc = kap_check_current_target_emit(KAP_HOOK_EXECMEM, target,
+ "execmem_predicate_denied",
+ "execmem_allowed");
+ kfree(target);
+ return rc;
+}
+
+static int kap_file_mprotect(struct vm_area_struct *vma,
+ unsigned long reqprot, unsigned long prot)
+{
+ char *target;
+ struct file *file;
+ int rc = 0;
+
+ if (!kap_lsm_current_active())
+ return 0;
+ if (!(reqprot & PROT_EXEC) && !(prot & PROT_EXEC))
+ return 0;
+ file = vma ? vma->vm_file : NULL;
+ if (!file)
+ return kap_check_current_target_emit(KAP_HOOK_EXECMEM, "anonymous",
+ "execmem_predicate_denied",
+ "execmem_allowed");
+ target = kap_lsm_file_path_alloc(file);
+ if (IS_ERR(target))
+ return 0;
+ if (kap_path_is_memfd(target))
+ rc = kap_check_current_target_emit(KAP_HOOK_EXECMEM, target,
+ "execmem_predicate_denied",
+ "execmem_allowed");
+ kfree(target);
+ return rc;
+}
+
+#ifdef CONFIG_SECURITY_NETWORK
+static int kap_socket_connect(struct socket *sock, struct sockaddr *address, int addrlen)
+{
+ /*
+ * AF_UNSPEC dissolves an association rather than making one; resolvers
+ * use it between candidate addresses. AF_UNIX is checked as ipc by the
+ * unix hooks.
+ */
+ if (addrlen < offsetofend(struct sockaddr, sa_family) ||
+ address->sa_family == AF_UNSPEC || address->sa_family == AF_UNIX)
+ return 0;
+ return kap_check_current_emit(KAP_HOOK_SOCKET_CONNECT, address,
+ "net_predicate_denied", "net_allowed");
+}
+#endif
+
+static int kap_check_static_target(enum kap_hook_kind kind, const char *target,
+ const char *deny_reason, const char *allow_reason)
+{
+ return kap_check_current_target_emit(kind, target, deny_reason, allow_reason);
+}
+
+static int kap_check_namespace_target(const char *ns, const char *operation)
+{
+ char target[64];
+
+ scnprintf(target, sizeof(target), "%s:%s", ns, operation);
+ return kap_check_static_target(KAP_HOOK_NAMESPACE, target,
+ "namespace_predicate_denied", "namespace_allowed");
+}
+
+static const struct {
+ unsigned long flag;
+ const char *name;
+} kap_namespaces[] = {
+ { CLONE_NEWNS, "mount" },
+ { CLONE_NEWPID, "pid" },
+ { CLONE_NEWNET, "net" },
+ { CLONE_NEWUSER, "user" },
+ { CLONE_NEWUTS, "uts" },
+ { CLONE_NEWIPC, "ipc" },
+ { CLONE_NEWCGROUP, "cgroup" },
+ { CLONE_NEWTIME, "time" },
+};
+
+int kap_check_namespace_flags(unsigned long flags, const char *operation)
+{
+ int rc;
+ int i;
+
+ if (!operation || !*operation)
+ operation = "transition";
+ for (i = 0; i < ARRAY_SIZE(kap_namespaces); i++) {
+ if (!(flags & kap_namespaces[i].flag))
+ continue;
+ /* Creating a user namespace is checked by userns_create. */
+ if (kap_namespaces[i].flag == CLONE_NEWUSER &&
+ strcmp(operation, "setns"))
+ continue;
+ rc = kap_check_namespace_target(kap_namespaces[i].name, operation);
+ if (rc)
+ return rc;
+ }
+ return 0;
+}
+
+/* A path we cannot render is a path we cannot judge. Emitting it as a normal
+ * target lets a broad allow such as "*" match the placeholder and pass, so
+ * refuse outright and record why.
+ */
+static int kap_lsm_refuse_unresolved(const char *why)
+{
+ kap_emit_current_event_target(KAP_HOOK_FILE, 0, why, NULL);
+ return -EACCES;
+}
+
+/*
+ * The path family (rename, link, unlink, mkdir, mknod, symlink) is checked
+ * like open, so a bound task cannot stage a file where it may write and move
+ * it into a denied directory. Render the entry as its parent directory plus
+ * its name and run it through the same file predicate as open, so one place
+ * decides and one reason string comes out.
+ *
+ * Rendering happens into a PATH_MAX buffer rather than KAP_LSM_TARGET_LEN. A
+ * truncated path stops matching a deny prefix that extends past the cut, which
+ * would turn a long path into a silent allow. Truncation and resolution
+ * failure are both refused instead.
+ */
+static int kap_lsm_check_dir_entry(const struct path *dir, struct dentry *dentry)
+{
+ char *buf;
+ char *rendered;
+ char *target;
+ int rc;
+ int len;
+
+ /* kap_path_link builds old_path by hand, so either field can be NULL.
+ * d_path would dereference them.
+ */
+ if (!dir || !dir->mnt || !dir->dentry || !dentry)
+ return kap_lsm_refuse_unresolved("file_path_unresolved");
+ buf = kmalloc(PATH_MAX, GFP_KERNEL);
+ if (!buf)
+ return -ENOMEM;
+ target = kmalloc(PATH_MAX, GFP_KERNEL);
+ if (!target) {
+ kfree(buf);
+ return -ENOMEM;
+ }
+ rendered = d_path(dir, buf, PATH_MAX);
+ if (IS_ERR(rendered)) {
+ /* No resolvable path means no way to know whether it is denied.
+ * Refuse rather than let an unresolved target meet a broad
+ * allow and pass.
+ */
+ kfree(target);
+ kfree(buf);
+ return kap_lsm_refuse_unresolved("file_path_unresolved");
+ }
+ /* d_path renders the root as "/", so joining blindly gives "//name". */
+ if (!strcmp(rendered, "/"))
+ len = snprintf(target, PATH_MAX, "/%pd", dentry);
+ else
+ len = snprintf(target, PATH_MAX, "%s/%pd", rendered, dentry);
+ if (len < 0 || len >= PATH_MAX) {
+ kfree(target);
+ kfree(buf);
+ return kap_lsm_refuse_unresolved("file_path_truncated");
+ }
+ rc = kap_check_current_target_emit(KAP_HOOK_FILE, target,
+ "file_predicate_denied",
+ "file_allowed");
+ kfree(target);
+ kfree(buf);
+ return rc;
+}
+
+static int kap_path_rename(const struct path *old_dir, struct dentry *old_dentry,
+ const struct path *new_dir, struct dentry *new_dentry,
+ unsigned int flags)
+{
+ int rc;
+
+ if (!kap_lsm_current_active())
+ return 0;
+ /* Both ends matter. The destination is the bypass being closed, and the
+ * source stops a bound task moving a file out of a path it was never
+ * allowed to read.
+ */
+ rc = kap_lsm_check_dir_entry(old_dir, old_dentry);
+ if (rc)
+ return rc;
+ return kap_lsm_check_dir_entry(new_dir, new_dentry);
+}
+
+static int kap_path_link(struct dentry *old_dentry, const struct path *new_dir,
+ struct dentry *new_dentry)
+{
+ struct path old_path;
+ int rc;
+
+ if (!kap_lsm_current_active())
+ return 0;
+ /* Checking only the destination would let a denied file be linked into
+ * an allowed directory and then read through the alias, since the later
+ * open sees the allowed name. A hard link cannot cross a mount, so the
+ * source shares new_dir's vfsmount.
+ */
+ old_path.mnt = new_dir ? new_dir->mnt : NULL;
+ old_path.dentry = old_dentry ? old_dentry->d_parent : NULL;
+ rc = kap_lsm_check_dir_entry(&old_path, old_dentry);
+ if (rc)
+ return rc;
+ return kap_lsm_check_dir_entry(new_dir, new_dentry);
+}
+
+static int kap_path_symlink(const struct path *dir, struct dentry *dentry,
+ const char *old_name)
+{
+ if (!kap_lsm_current_active())
+ return 0;
+ return kap_lsm_check_dir_entry(dir, dentry);
+}
+
+/*
+ * Creating or removing an entry under a denied directory never reaches open or
+ * rename, so without these the deny still leaks for those mutations.
+ */
+static int kap_path_mknod(const struct path *dir, struct dentry *dentry,
+ umode_t mode, unsigned int dev)
+{
+ if (!kap_lsm_current_active())
+ return 0;
+ return kap_lsm_check_dir_entry(dir, dentry);
+}
+
+static int kap_path_mkdir(const struct path *dir, struct dentry *dentry,
+ umode_t mode)
+{
+ if (!kap_lsm_current_active())
+ return 0;
+ return kap_lsm_check_dir_entry(dir, dentry);
+}
+
+static int kap_path_unlink(const struct path *dir, struct dentry *dentry)
+{
+ if (!kap_lsm_current_active())
+ return 0;
+ return kap_lsm_check_dir_entry(dir, dentry);
+}
+
+static int kap_path_rmdir(const struct path *dir, struct dentry *dentry)
+{
+ if (!kap_lsm_current_active())
+ return 0;
+ return kap_lsm_check_dir_entry(dir, dentry);
+}
+
+static int kap_sb_mount(const char *dev_name, const struct path *path,
+ const char *type, unsigned long flags, void *data)
+{
+ char path_target[KAP_LSM_TARGET_LEN];
+ char target[KAP_LSM_TARGET_LEN];
+
+ if (!kap_lsm_current_active())
+ return 0;
+
+ if (kap_lsm_path_target(path, path_target, sizeof(path_target)))
+ strscpy(path_target, "unknown", sizeof(path_target));
+ scnprintf(target, sizeof(target), "mount:mount:%s:%s",
+ type ?: "unknown", path_target);
+ return kap_check_static_target(KAP_HOOK_NAMESPACE, target,
+ "mount_predicate_denied", "mount_allowed");
+}
+
+static int kap_sb_umount(struct vfsmount *mnt, int flags)
+{
+ struct path path = { .mnt = mnt, .dentry = mnt ? mnt->mnt_root : NULL };
+ char path_target[KAP_LSM_TARGET_LEN];
+ char target[KAP_LSM_TARGET_LEN];
+
+ if (!kap_lsm_current_active())
+ return 0;
+
+ if (!mnt || kap_lsm_path_target(&path, path_target, sizeof(path_target)))
+ strscpy(path_target, "unknown", sizeof(path_target));
+ scnprintf(target, sizeof(target), "mount:umount:%s", path_target);
+ return kap_check_static_target(KAP_HOOK_NAMESPACE, target,
+ "mount_predicate_denied", "mount_allowed");
+}
+
+static int kap_sb_pivotroot(const struct path *old_path, const struct path *new_path)
+{
+ char old_target[KAP_LSM_TARGET_LEN];
+ char new_target[KAP_LSM_TARGET_LEN];
+ char target[KAP_LSM_TARGET_LEN];
+
+ if (!kap_lsm_current_active())
+ return 0;
+
+ if (kap_lsm_path_target(old_path, old_target, sizeof(old_target)))
+ strscpy(old_target, "unknown", sizeof(old_target));
+ if (kap_lsm_path_target(new_path, new_target, sizeof(new_target)))
+ strscpy(new_target, "unknown", sizeof(new_target));
+ scnprintf(target, sizeof(target), "mount:pivotroot:%s:%s",
+ old_target, new_target);
+ return kap_check_static_target(KAP_HOOK_NAMESPACE, target,
+ "mount_predicate_denied", "mount_allowed");
+}
+
+static int kap_move_mount(const struct path *from_path, const struct path *to_path)
+{
+ char from_target[KAP_LSM_TARGET_LEN];
+ char to_target[KAP_LSM_TARGET_LEN];
+ char target[KAP_LSM_TARGET_LEN];
+
+ if (!kap_lsm_current_active())
+ return 0;
+
+ if (kap_lsm_path_target(from_path, from_target, sizeof(from_target)))
+ strscpy(from_target, "unknown", sizeof(from_target));
+ if (kap_lsm_path_target(to_path, to_target, sizeof(to_target)))
+ strscpy(to_target, "unknown", sizeof(to_target));
+ scnprintf(target, sizeof(target), "mount:move:%s:%s",
+ from_target, to_target);
+ return kap_check_static_target(KAP_HOOK_NAMESPACE, target,
+ "mount_predicate_denied", "mount_allowed");
+}
+
+#ifdef CONFIG_SECURITY_NETWORK
+static int kap_unix_stream_connect(struct sock *sock, struct sock *other, struct sock *newsk)
+{
+ return kap_check_static_target(KAP_HOOK_IPC, "unix:stream_connect",
+ "ipc_predicate_denied", "ipc_allowed");
+}
+
+static int kap_unix_may_send(struct socket *sock, struct socket *other)
+{
+ return kap_check_static_target(KAP_HOOK_IPC, "unix:may_send",
+ "ipc_predicate_denied", "ipc_allowed");
+}
+#endif
+
+static int kap_file_receive(struct file *file)
+{
+ return kap_check_current_emit(KAP_HOOK_DESCRIPTOR, file,
+ "descriptor_predicate_denied", "descriptor_allowed");
+}
+
+static int kap_task_fix_setuid(struct cred *new, const struct cred *old, int flags)
+{
+ char target[128];
+
+ if (!kap_lsm_current_active())
+ return 0;
+
+ scnprintf(target, sizeof(target), "uid=%u,euid=%u,suid=%u,fsuid=%u",
+ __kuid_val(new->uid), __kuid_val(new->euid),
+ __kuid_val(new->suid), __kuid_val(new->fsuid));
+ return kap_check_current_target_emit(KAP_HOOK_CRED, target,
+ "cred_predicate_denied", "cred_allowed");
+}
+
+static int kap_task_fix_setgid(struct cred *new, const struct cred *old, int flags)
+{
+ char target[128];
+
+ if (!kap_lsm_current_active())
+ return 0;
+
+ scnprintf(target, sizeof(target), "gid=%u,egid=%u,sgid=%u,fsgid=%u",
+ __kgid_val(new->gid), __kgid_val(new->egid),
+ __kgid_val(new->sgid), __kgid_val(new->fsgid));
+ return kap_check_current_target_emit(KAP_HOOK_CRED, target,
+ "cred_predicate_denied", "cred_allowed");
+}
+
+static int kap_task_fix_setgroups(struct cred *new, const struct cred *old)
+{
+ return kap_check_static_target(KAP_HOOK_CRED, "groups",
+ "cred_predicate_denied", "cred_allowed");
+}
+
+static int kap_capset(struct cred *new, const struct cred *old,
+ const kernel_cap_t *effective,
+ const kernel_cap_t *inheritable,
+ const kernel_cap_t *permitted)
+{
+ return kap_check_static_target(KAP_HOOK_CRED, "capset",
+ "cred_predicate_denied", "cred_allowed");
+}
+
+static int kap_userns_create(const struct cred *cred)
+{
+ return kap_check_static_target(KAP_HOOK_NAMESPACE, "user",
+ "namespace_predicate_denied", "namespace_allowed");
+}
+
+static int kap_ipc_permission(struct kern_ipc_perm *ipcp, short flag)
+{
+ char target[64];
+
+ if (!kap_lsm_current_active())
+ return 0;
+
+ scnprintf(target, sizeof(target), "sysv:flag=%hd", flag);
+ return kap_check_current_target_emit(KAP_HOOK_IPC, target,
+ "ipc_predicate_denied", "ipc_allowed");
+}
+
+static void kap_ptrace_target(struct task_struct *child, char *target, size_t target_len)
+{
+ struct kap_task_state *current_state = kap_task_state(current);
+ struct kap_task_state *child_state = kap_task_state(child);
+
+ if (current_state && current_state->active &&
+ child_state && child_state->active &&
+ !strcmp(current_state->igroup_name, child_state->igroup_name) &&
+ current_state->epoch == child_state->epoch) {
+ strscpy(target, "same-igroup", target_len);
+ return;
+ }
+ if (child_state && child_state->active) {
+ scnprintf(target, target_len, "igroup=%s", child_state->igroup_name);
+ return;
+ }
+ strscpy(target, "unconfined", target_len);
+}
+
+static int kap_ptrace_access_check(struct task_struct *child, unsigned int mode)
+{
+ char target[128];
+
+ if (!kap_lsm_current_active())
+ return 0;
+
+ kap_ptrace_target(child, target, sizeof(target));
+ return kap_check_current_target_emit(KAP_HOOK_PTRACE, target,
+ "ptrace_predicate_denied", "ptrace_allowed");
+}
+
+static int kap_ptrace_traceme(struct task_struct *parent)
+{
+ char target[128];
+
+ if (!kap_lsm_current_active())
+ return 0;
+
+ kap_ptrace_target(parent, target, sizeof(target));
+ return kap_check_current_target_emit(KAP_HOOK_PTRACE, target,
+ "ptrace_predicate_denied", "ptrace_allowed");
+}
+
+#ifdef CONFIG_BPF_SYSCALL
+static int kap_bpf(int cmd, union bpf_attr *attr, unsigned int size, bool kernel)
+{
+ char target[128];
+
+ switch (cmd) {
+ case BPF_MAP_CREATE:
+ scnprintf(target, sizeof(target), "cmd=%d,map_type=%u",
+ cmd, attr ? attr->map_type : 0);
+ break;
+ case BPF_PROG_LOAD:
+ scnprintf(target, sizeof(target), "cmd=%d,prog_type=%u",
+ cmd, attr ? attr->prog_type : 0);
+ break;
+ default:
+ scnprintf(target, sizeof(target), "cmd=%d", cmd);
+ break;
+ }
+ return kap_check_current_target_emit(KAP_HOOK_BPF, target,
+ "bpf_predicate_denied", "bpf_allowed");
+}
+#endif
+
+#ifdef CONFIG_IO_URING
+static int kap_uring_override_creds(const struct cred *new)
+{
+ return kap_check_static_target(KAP_HOOK_IO_URING, "override_creds",
+ "io_uring_predicate_denied", "io_uring_allowed");
+}
+
+static int kap_uring_sqpoll(void)
+{
+ return kap_check_static_target(KAP_HOOK_IO_URING, "sqpoll",
+ "io_uring_predicate_denied", "io_uring_allowed");
+}
+
+static int kap_uring_cmd(struct io_uring_cmd *ioucmd)
+{
+ return kap_check_static_target(KAP_HOOK_IO_URING, "uring_cmd",
+ "io_uring_predicate_denied", "io_uring_allowed");
+}
+#endif
+
+/* truncate(2) changes the contents of an existing file without opening it, so
+ * none of the open or entry hooks see it. Without this a denied file can still
+ * be emptied.
+ */
+static int kap_path_truncate(const struct path *path)
+{
+ char *target;
+ int rc;
+
+ if (!kap_lsm_current_active())
+ return 0;
+ if (!path || !path->mnt || !path->dentry)
+ return kap_lsm_refuse_unresolved("file_path_unresolved");
+ target = kmalloc(PATH_MAX, GFP_KERNEL);
+ if (!target)
+ return -ENOMEM;
+ rc = kap_lsm_path_target(path, target, PATH_MAX);
+ if (rc) {
+ kfree(target);
+ return kap_lsm_refuse_unresolved("file_path_unresolved");
+ }
+ rc = kap_check_current_target_emit(KAP_HOOK_FILE, target,
+ "file_predicate_denied", "file_allowed");
+ kfree(target);
+ return rc;
+}
+
+static struct security_hook_list kap_hooks[] __ro_after_init = {
+ LSM_HOOK_INIT(task_alloc, kap_task_alloc),
+ LSM_HOOK_INIT(task_free, kap_task_free),
+ LSM_HOOK_INIT(bprm_check_security, kap_bprm_check_security),
+ LSM_HOOK_INIT(sb_mount, kap_sb_mount),
+ LSM_HOOK_INIT(sb_umount, kap_sb_umount),
+ LSM_HOOK_INIT(sb_pivotroot, kap_sb_pivotroot),
+ LSM_HOOK_INIT(move_mount, kap_move_mount),
+ LSM_HOOK_INIT(file_open, kap_file_open),
+ LSM_HOOK_INIT(file_permission, kap_file_permission),
+ LSM_HOOK_INIT(mmap_file, kap_mmap_file),
+ LSM_HOOK_INIT(file_mprotect, kap_file_mprotect),
+ LSM_HOOK_INIT(file_receive, kap_file_receive),
+ LSM_HOOK_INIT(path_rename, kap_path_rename),
+ LSM_HOOK_INIT(path_link, kap_path_link),
+ LSM_HOOK_INIT(path_symlink, kap_path_symlink),
+ LSM_HOOK_INIT(path_mknod, kap_path_mknod),
+ LSM_HOOK_INIT(path_mkdir, kap_path_mkdir),
+ LSM_HOOK_INIT(path_unlink, kap_path_unlink),
+ LSM_HOOK_INIT(path_rmdir, kap_path_rmdir),
+ LSM_HOOK_INIT(path_truncate, kap_path_truncate),
+#ifdef CONFIG_SECURITY_NETWORK
+ LSM_HOOK_INIT(socket_connect, kap_socket_connect),
+ LSM_HOOK_INIT(unix_stream_connect, kap_unix_stream_connect),
+ LSM_HOOK_INIT(unix_may_send, kap_unix_may_send),
+#endif
+ LSM_HOOK_INIT(task_fix_setuid, kap_task_fix_setuid),
+ LSM_HOOK_INIT(task_fix_setgid, kap_task_fix_setgid),
+ LSM_HOOK_INIT(task_fix_setgroups, kap_task_fix_setgroups),
+ LSM_HOOK_INIT(capset, kap_capset),
+ LSM_HOOK_INIT(userns_create, kap_userns_create),
+ LSM_HOOK_INIT(ipc_permission, kap_ipc_permission),
+ LSM_HOOK_INIT(ptrace_access_check, kap_ptrace_access_check),
+ LSM_HOOK_INIT(ptrace_traceme, kap_ptrace_traceme),
+#ifdef CONFIG_BPF_SYSCALL
+ LSM_HOOK_INIT(bpf, kap_bpf),
+#endif
+#ifdef CONFIG_IO_URING
+ LSM_HOOK_INIT(uring_override_creds, kap_uring_override_creds),
+ LSM_HOOK_INIT(uring_sqpoll, kap_uring_sqpoll),
+ LSM_HOOK_INIT(uring_cmd, kap_uring_cmd),
+#endif
+};
+
+static const struct lsm_id kap_lsmid = {
+ .name = "kap",
+ .id = LSM_ID_KAP,
+};
+
+static int __init kap_lsm_init(void)
+{
+ WRITE_ONCE(kap_lsm_active, true);
+ security_add_hooks(kap_hooks, ARRAY_SIZE(kap_hooks), &kap_lsmid);
+ pr_info("LSM hooks registered\n");
+ return 0;
+}
+
+DEFINE_LSM(kap) = {
+ .id = &kap_lsmid,
+ .blobs = &kap_blob_sizes,
+ .init = kap_lsm_init,
+};
--
2.51.0
^ permalink raw reply related [flat|nested] 9+ messages in thread* [RFC PATCH v2 4/6] security: kap: add the securityfs control plane
2026-10-28 13:33 [RFC PATCH v2 0/6] security: introduce Kernel Assurance Plane Ketan Bhardwaj
` (2 preceding siblings ...)
2026-10-28 13:33 ` [RFC PATCH v2 3/6] security: add the Kernel Assurance Plane LSM Ketan Bhardwaj
@ 2026-10-28 13:33 ` Ketan Bhardwaj
2026-10-28 13:33 ` [RFC PATCH v2 5/6] kernel: add KAP checks for unshare and setns Ketan Bhardwaj
2026-10-28 13:33 ` [RFC PATCH v2 6/6] MAINTAINERS: add an entry for the Kernel Assurance Plane Ketan Bhardwaj
5 siblings, 0 replies; 9+ messages in thread
From: Ketan Bhardwaj @ 2026-10-28 13:33 UTC (permalink / raw)
To: Paul Moore, James Morris, Serge E . Hallyn
Cc: linux-security-module, Jonathan Corbet, Shuah Khan, Randy Dunlap,
linux-doc, linux-api, Mickaël Salaün,
Günther Noack, linux-kernel, HARIHARA SUDHAN . R,
Ketan Bhardwaj
Add the KAP control plane under /sys/kernel/security/kap.
securityfs has no mkdir, so igroups live in a small internal filesystem
mounted with kern_mount() and reached through the igroups link in the
kap directory, as AppArmor publishes its policy namespaces. An igroup is
created with mkdir, as a cgroup is, and gets one file per control:
kap.install writes the lineage root, authority epoch, expiry, transition
and authority in a single write that commits together or not at all,
and kap.procs binds a task. kap.default sits next to igroups/.
Writes need CAP_SYS_ADMIN. The internal filesystem type is never
registered, so it cannot be mounted from userspace.
Assisted-by: Claude
Co-developed-by: HARIHARA SUDHAN. R <harisudhan@armoriq.io>
Signed-off-by: HARIHARA SUDHAN. R <harisudhan@armoriq.io>
Assisted-by: Codex
Signed-off-by: Ketan Bhardwaj <ketan@armoriq.io>
---
security/kap/Makefile | 2 +-
security/kap/kap_lsm.c | 1 +
security/kap/kapfs.c | 524 +++++++++++++++++++++++++++++++++++++++++
3 files changed, 526 insertions(+), 1 deletion(-)
create mode 100644 security/kap/kapfs.c
diff --git a/security/kap/Makefile b/security/kap/Makefile
index 40a864c..6ef7c5e 100644
--- a/security/kap/Makefile
+++ b/security/kap/Makefile
@@ -1,2 +1,2 @@
obj-$(CONFIG_SECURITY_KAP) := kap.o
-kap-y := kap_lsm.o igroup.o
+kap-y := kap_lsm.o igroup.o kapfs.o
diff --git a/security/kap/kap_lsm.c b/security/kap/kap_lsm.c
index b148b34..6750114 100644
--- a/security/kap/kap_lsm.c
+++ b/security/kap/kap_lsm.c
@@ -862,4 +862,5 @@ DEFINE_LSM(kap) = {
.id = &kap_lsmid,
.blobs = &kap_blob_sizes,
.init = kap_lsm_init,
+ .initcall_fs = kap_securityfs_init,
};
diff --git a/security/kap/kapfs.c b/security/kap/kapfs.c
new file mode 100644
index 0000000..36ef1e3
--- /dev/null
+++ b/security/kap/kapfs.c
@@ -0,0 +1,524 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Kernel Assurance Plane control plane in securityfs.
+ *
+ * /sys/kernel/security/kap/
+ * kap.default
+ * igroups/ link to the root of the internal kapfs mount
+ * <igroup>/ created with mkdir, as a cgroup is
+ * kap.install
+ * kap.procs
+ * kap.igfd
+ * kap.lineage_root
+ * kap.authority_epoch
+ * kap.authority
+ * kap.expires_at
+ * kap.transition
+ * kap.mode
+ * kap.events
+ *
+ * securityfs has no mkdir, so igroups live in a small internal filesystem that
+ * is mounted with kern_mount() and reached through a link under securityfs.
+ * AppArmor publishes its policy namespaces the same way.
+ */
+
+#define pr_fmt(fmt) "KAP: " fmt
+
+#include <linux/capability.h>
+#include <linux/ctype.h>
+#include <linux/dcache.h>
+#include <linux/errno.h>
+#include <linux/fs.h>
+#include <linux/fs_context.h>
+#include <linux/init.h>
+#include <linux/kernel.h>
+#include <linux/mm.h>
+#include <linux/mount.h>
+#include <linux/spinlock.h>
+#include <linux/namei.h>
+#include <linux/security.h>
+#include <linux/seq_file.h>
+#include <linux/slab.h>
+#include <linux/string.h>
+#include <linux/timekeeping.h>
+#include <linux/uaccess.h>
+
+#include "kap.h"
+
+#define KAPFS_MAGIC 0x4b415046
+
+static struct vfsmount *kapfs_mnt;
+static struct dentry *kap_sfs_dir;
+static struct dentry *kap_sfs_default;
+static struct dentry *kap_sfs_igroups;
+static struct kap_file_ctx kap_default_ctx = { .kind = KAP_FILE_DEFAULT };
+
+static const char * const kap_file_names[KAP_FILE_COUNT] = {
+ [KAP_FILE_PROCS] = "kap.procs",
+ [KAP_FILE_IGFD] = "kap.igfd",
+ [KAP_FILE_LINEAGE_ROOT] = "kap.lineage_root",
+ [KAP_FILE_AUTHORITY_EPOCH] = "kap.authority_epoch",
+ [KAP_FILE_AUTHORITY] = "kap.authority",
+ [KAP_FILE_TRANSITION] = "kap.transition",
+ [KAP_FILE_MODE] = "kap.mode",
+ [KAP_FILE_EVENTS] = "kap.events",
+ [KAP_FILE_EXPIRES_AT] = "kap.expires_at",
+ [KAP_FILE_INSTALL] = "kap.install",
+};
+
+static const umode_t kap_file_modes[KAP_FILE_COUNT] = {
+ [KAP_FILE_PROCS] = 0600,
+ [KAP_FILE_IGFD] = 0400,
+ [KAP_FILE_LINEAGE_ROOT] = 0600,
+ [KAP_FILE_AUTHORITY_EPOCH] = 0600,
+ [KAP_FILE_AUTHORITY] = 0600,
+ [KAP_FILE_TRANSITION] = 0600,
+ [KAP_FILE_MODE] = 0600,
+ [KAP_FILE_EXPIRES_AT] = 0600,
+ [KAP_FILE_INSTALL] = 0200,
+ [KAP_FILE_EVENTS] = 0400,
+};
+
+static int kapfs_show(struct seq_file *m, void *v)
+{
+ struct kap_file_ctx *ctx = m->private;
+ struct kap_igroup *igroup = ctx->igroup;
+ struct kap_bound_pid *entry;
+
+ spin_lock(&kap_lock);
+ switch (ctx->kind) {
+ case KAP_FILE_PROCS:
+ list_for_each_entry(entry, &igroup->pids, node)
+ seq_printf(m, "%d\n", entry->pid);
+ break;
+ case KAP_FILE_IGFD:
+ seq_printf(m, "igroup=%s epoch=%llu root=%s\n",
+ igroup->name, igroup->epoch, igroup->lineage_root);
+ break;
+ case KAP_FILE_LINEAGE_ROOT:
+ seq_printf(m, "%s\n", igroup->lineage_root);
+ break;
+ case KAP_FILE_AUTHORITY_EPOCH:
+ seq_printf(m, "%llu\n", igroup->epoch);
+ break;
+ case KAP_FILE_AUTHORITY:
+ seq_write(m, igroup->authority, igroup->authority_len);
+ if (igroup->authority_len && igroup->authority[igroup->authority_len - 1] != '\n')
+ seq_putc(m, '\n');
+ break;
+ case KAP_FILE_TRANSITION:
+ if (igroup->has_sequence)
+ seq_printf(m, "last_sequence=%llu\n", igroup->last_sequence);
+ break;
+ case KAP_FILE_MODE:
+ seq_printf(m, "%s\n", igroup->revoked ? "revoked" : "enforce");
+ break;
+ case KAP_FILE_EVENTS:
+ seq_write(m, igroup->events, igroup->events_len);
+ break;
+ case KAP_FILE_EXPIRES_AT:
+ seq_printf(m, "%llu\n", igroup->expires_at);
+ break;
+ case KAP_FILE_DEFAULT:
+ seq_printf(m, "%s\n", kap_default_closed ? "closed" : "open");
+ break;
+ default:
+ break;
+ }
+ spin_unlock(&kap_lock);
+ return 0;
+}
+
+static int kapfs_open(struct inode *inode, struct file *file)
+{
+ return single_open(file, kapfs_show, inode->i_private);
+}
+
+static ssize_t kapfs_write(struct file *file, const char __user *buf, size_t len, loff_t *ppos)
+{
+ struct seq_file *seq = file->private_data;
+ struct kap_file_ctx *ctx = seq->private;
+ struct kap_igroup *igroup = ctx->igroup;
+ struct kap_compiled_authority *compiled = NULL;
+ struct kap_bound_pid *entry = NULL;
+ struct kap_install_request req;
+ u64 value = 0;
+ pid_t pid = 0;
+ char *tmp;
+ int rc = 0;
+
+ /* The opener's credentials, so a handed-over descriptor gains nothing. */
+ if (!file_ns_capable(file, &init_user_ns, CAP_SYS_ADMIN))
+ return -EPERM;
+ if (len == 0)
+ return 0;
+ if (len > (ctx->kind == KAP_FILE_INSTALL ? KAP_MAX_INSTALL_BYTES
+ : KAP_MAX_PREDICATE_BYTES))
+ return -EINVAL;
+ if (ctx->kind == KAP_FILE_EVENTS || ctx->kind == KAP_FILE_IGFD)
+ return -EINVAL;
+
+ tmp = memdup_user_nul(buf, len);
+ if (IS_ERR(tmp))
+ return PTR_ERR(tmp);
+ kap_trim_newline(tmp);
+ if (!tmp[0]) {
+ kfree(tmp);
+ return len;
+ }
+
+ /* Parse, allocate and compile first: kap_lock is a spinlock. */
+ switch (ctx->kind) {
+ case KAP_FILE_PROCS:
+ rc = kstrtoint(tmp, 10, &pid);
+ if (!rc) {
+ entry = kzalloc_obj(*entry, GFP_KERNEL);
+ if (!entry)
+ rc = -ENOMEM;
+ }
+ break;
+ case KAP_FILE_LINEAGE_ROOT:
+ if (strlen(tmp) > KAP_MAX_ROOT_LEN)
+ rc = -EINVAL;
+ break;
+ case KAP_FILE_INSTALL:
+ rc = kap_install_parse(tmp, &req);
+ if (!rc)
+ rc = kap_authority_prepare(req.authority, &compiled);
+ break;
+ case KAP_FILE_AUTHORITY:
+ rc = kap_authority_prepare(tmp, &compiled);
+ break;
+ case KAP_FILE_AUTHORITY_EPOCH:
+ case KAP_FILE_EXPIRES_AT:
+ rc = kstrtou64(tmp, 10, &value);
+ break;
+ case KAP_FILE_TRANSITION:
+ rc = kap_parse_transition_sequence(tmp, &value);
+ break;
+ case KAP_FILE_MODE:
+ if (!sysfs_streq(tmp, "revoked") && !sysfs_streq(tmp, "enforce"))
+ rc = -EINVAL;
+ break;
+ case KAP_FILE_DEFAULT:
+ if (!sysfs_streq(tmp, "closed") && !sysfs_streq(tmp, "open"))
+ rc = -EINVAL;
+ break;
+ default:
+ rc = -EINVAL;
+ break;
+ }
+ if (rc)
+ goto out;
+
+ spin_lock(&kap_lock);
+ switch (ctx->kind) {
+ case KAP_FILE_PROCS:
+ rc = kap_bind_pid_locked(igroup, pid, &entry);
+ break;
+ case KAP_FILE_LINEAGE_ROOT:
+ strscpy(igroup->lineage_root, tmp, sizeof(igroup->lineage_root));
+ break;
+ case KAP_FILE_INSTALL:
+ rc = kap_install_locked(igroup, &req, &compiled);
+ break;
+ case KAP_FILE_AUTHORITY_EPOCH:
+ /* Writing an epoch also clears revoked mode. */
+ if (value < igroup->epoch) {
+ rc = -ESTALE;
+ } else {
+ igroup->epoch = value;
+ igroup->revoked = false;
+ }
+ break;
+ case KAP_FILE_AUTHORITY: {
+ struct kap_compiled_authority *old = igroup->compiled;
+
+ strscpy(igroup->authority, tmp, sizeof(igroup->authority));
+ igroup->authority_len = strlen(igroup->authority);
+ igroup->compiled = compiled;
+ compiled = old;
+ break;
+ }
+ case KAP_FILE_TRANSITION:
+ if (igroup->has_sequence && value <= igroup->last_sequence) {
+ rc = -EALREADY;
+ } else {
+ igroup->last_sequence = value;
+ igroup->has_sequence = true;
+ }
+ break;
+ case KAP_FILE_EXPIRES_AT:
+ /* An expiry already in the past is refused rather than installed. */
+ if (value && (u64)ktime_get_real_seconds() >= value)
+ rc = -ESTALE;
+ else
+ igroup->expires_at = value;
+ break;
+ case KAP_FILE_MODE:
+ igroup->revoked = sysfs_streq(tmp, "revoked");
+ break;
+ case KAP_FILE_DEFAULT:
+ WRITE_ONCE(kap_default_closed, sysfs_streq(tmp, "closed"));
+ break;
+ default:
+ break;
+ }
+ spin_unlock(&kap_lock);
+out:
+ /* Whatever was not consumed: a refused document, or the replaced one. */
+ kvfree(compiled);
+ kfree(entry);
+ kfree(tmp);
+ return rc ? rc : len;
+}
+
+static const struct file_operations kapfs_fops = {
+ .owner = THIS_MODULE,
+ .open = kapfs_open,
+ .read = seq_read,
+ .write = kapfs_write,
+ .llseek = seq_lseek,
+ .release = single_release,
+};
+
+static struct inode *kapfs_make_inode(struct super_block *sb, umode_t mode)
+{
+ struct inode *inode = new_inode(sb);
+
+ if (!inode)
+ return NULL;
+ inode->i_ino = get_next_ino();
+ inode->i_mode = mode;
+ simple_inode_init_ts(inode);
+ return inode;
+}
+
+static void kapfs_instantiate(struct dentry *dentry, struct inode *inode)
+{
+ d_make_persistent(dentry, inode);
+}
+
+/* Names end up in kap.events and the kernel log, so keep them plain. */
+static bool kapfs_valid_name(const struct qstr *name)
+{
+ unsigned int i;
+
+ if (name->len == 0 || name->len > KAP_MAX_NAME_LEN || name->name[0] == '.')
+ return false;
+ for (i = 0; i < name->len; i++) {
+ unsigned char c = name->name[i];
+
+ if (!isalnum(c) && c != '-' && c != '_' && c != '.')
+ return false;
+ }
+ return true;
+}
+
+/* Returns NULL on success: the passed dentry is the one instantiated. */
+static struct dentry *kapfs_mkdir(struct mnt_idmap *idmap, struct inode *dir,
+ struct dentry *dentry, umode_t mode)
+{
+ struct dentry *files[KAP_FILE_COUNT] = {};
+ struct inode *inodes[KAP_FILE_COUNT] = {};
+ struct kap_igroup *igroup;
+ struct inode *inode;
+ int rc = 0;
+ int i;
+
+ if (!capable(CAP_SYS_ADMIN))
+ return ERR_PTR(-EPERM);
+ if (!kapfs_valid_name(&dentry->d_name))
+ return ERR_PTR(-EINVAL);
+
+ /* Allocate everything first, so a failure leaves nothing behind. */
+ igroup = kvzalloc_obj(*igroup, GFP_KERNEL);
+ inode = kapfs_make_inode(dir->i_sb, S_IFDIR | 0700);
+ if (!igroup || !inode) {
+ rc = -ENOMEM;
+ goto err;
+ }
+ memcpy(igroup->name, dentry->d_name.name, dentry->d_name.len);
+ INIT_LIST_HEAD(&igroup->pids);
+ INIT_LIST_HEAD(&igroup->node);
+ for (i = 0; i < KAP_FILE_COUNT; i++) {
+ files[i] = d_alloc_name(dentry, kap_file_names[i]);
+ inodes[i] = kapfs_make_inode(dir->i_sb, S_IFREG | kap_file_modes[i]);
+ if (!files[i] || !inodes[i]) {
+ rc = -ENOMEM;
+ goto err;
+ }
+ igroup->files[i].kind = i;
+ igroup->files[i].igroup = igroup;
+ inodes[i]->i_fop = &kapfs_fops;
+ inodes[i]->i_private = &igroup->files[i];
+ }
+
+ spin_lock(&kap_lock);
+ if (kap_find_igroup_locked(igroup->name))
+ rc = -EEXIST;
+ else
+ list_add_tail(&igroup->node, &kap_igroups);
+ spin_unlock(&kap_lock);
+ if (rc)
+ goto err;
+
+ inode->i_op = &simple_dir_inode_operations;
+ inode->i_fop = &simple_dir_operations;
+ inc_nlink(inode);
+ inc_nlink(dir);
+ /* Lookups in the new directory wait until its files are all there. */
+ inode_lock(inode);
+ kapfs_instantiate(dentry, inode);
+ igroup->dentry = dentry;
+ for (i = 0; i < KAP_FILE_COUNT; i++)
+ kapfs_instantiate(files[i], inodes[i]);
+ inode_unlock(inode);
+ return NULL;
+
+err:
+ for (i = 0; i < KAP_FILE_COUNT; i++) {
+ if (inodes[i])
+ iput(inodes[i]);
+ if (files[i])
+ dput(files[i]);
+ }
+ if (inode)
+ iput(inode);
+ kvfree(igroup);
+ return ERR_PTR(rc);
+}
+
+static const struct inode_operations kapfs_root_inode_ops = {
+ .lookup = simple_lookup,
+ .mkdir = kapfs_mkdir,
+};
+
+static int kapfs_fill_super(struct super_block *sb, struct fs_context *fc)
+{
+ struct inode *inode;
+
+ sb->s_magic = KAPFS_MAGIC;
+ inode = kapfs_make_inode(sb, S_IFDIR | 0700);
+ if (!inode)
+ return -ENOMEM;
+ inode->i_op = &kapfs_root_inode_ops;
+ inode->i_fop = &simple_dir_operations;
+ inc_nlink(inode);
+ sb->s_root = d_make_root(inode);
+ if (!sb->s_root)
+ return -ENOMEM;
+ return 0;
+}
+
+static int kapfs_get_tree(struct fs_context *fc)
+{
+ return get_tree_single(fc, kapfs_fill_super);
+}
+
+static const struct fs_context_operations kapfs_context_ops = {
+ .get_tree = kapfs_get_tree,
+};
+
+static int kapfs_init_fs_context(struct fs_context *fc)
+{
+ fc->ops = &kapfs_context_ops;
+ return 0;
+}
+
+/* Never registered, so it cannot be mounted from userspace. */
+static struct file_system_type kapfs_type = {
+ .owner = THIS_MODULE,
+ .name = "kapfs",
+ .init_fs_context = kapfs_init_fs_context,
+ .kill_sb = kill_anon_super,
+};
+
+static const char *kap_igroups_get_link(struct dentry *dentry,
+ struct inode *inode,
+ struct delayed_call *done)
+{
+ struct path path;
+ int rc;
+
+ if (!dentry)
+ return ERR_PTR(-ECHILD);
+ path.mnt = mntget(kapfs_mnt);
+ path.dentry = dget(kapfs_mnt->mnt_root);
+ rc = nd_jump_link(&path);
+ return ERR_PTR(rc);
+}
+
+static int kap_igroups_readlink(struct dentry *dentry, char __user *buffer,
+ int buflen)
+{
+ char name[32];
+ int rc;
+
+ rc = snprintf(name, sizeof(name), "kapfs:[%llu]",
+ (unsigned long long)d_inode(kapfs_mnt->mnt_root)->i_ino);
+ if (rc <= 0 || rc >= sizeof(name))
+ return -ENOENT;
+ return readlink_copy(buffer, buflen, name, rc);
+}
+
+static const struct inode_operations kap_igroups_link_iops = {
+ .readlink = kap_igroups_readlink,
+ .get_link = kap_igroups_get_link,
+};
+
+int __init kap_securityfs_init(void)
+{
+ struct dentry *dent;
+ int rc;
+
+ if (!READ_ONCE(kap_lsm_active))
+ return 0;
+
+ kapfs_mnt = kern_mount(&kapfs_type);
+ if (IS_ERR(kapfs_mnt)) {
+ rc = PTR_ERR(kapfs_mnt);
+ kapfs_mnt = NULL;
+ goto err;
+ }
+ kapfs_mnt->mnt_sb->s_flags &= ~SB_NOUSER;
+
+ dent = securityfs_create_dir("kap", NULL);
+ if (IS_ERR(dent)) {
+ rc = PTR_ERR(dent);
+ goto err_unmount;
+ }
+ kap_sfs_dir = dent;
+
+ dent = securityfs_create_file("kap.default", 0600, kap_sfs_dir,
+ &kap_default_ctx, &kapfs_fops);
+ if (IS_ERR(dent)) {
+ rc = PTR_ERR(dent);
+ goto err_remove;
+ }
+ kap_sfs_default = dent;
+
+ dent = securityfs_create_symlink("igroups", kap_sfs_dir, NULL,
+ &kap_igroups_link_iops);
+ if (IS_ERR(dent)) {
+ rc = PTR_ERR(dent);
+ goto err_remove;
+ }
+ kap_sfs_igroups = dent;
+
+ WRITE_ONCE(kap_ready, true);
+ pr_info("control plane at /sys/kernel/security/kap\n");
+ return 0;
+
+err_remove:
+ securityfs_remove(kap_sfs_default);
+ securityfs_remove(kap_sfs_dir);
+ kap_sfs_default = NULL;
+ kap_sfs_dir = NULL;
+err_unmount:
+ kern_unmount(kapfs_mnt);
+ kapfs_mnt = NULL;
+err:
+ pr_err("could not create the securityfs control plane: %d\n", rc);
+ return rc;
+}
--
2.51.0
^ permalink raw reply related [flat|nested] 9+ messages in thread