Linux Documentation
 help / color / mirror / Atom feed
* [RFC PATCH v2 0/6] security: introduce Kernel Assurance Plane
@ 2026-10-28 13:33 Ketan Bhardwaj
  2026-10-28 13:33 ` [RFC PATCH v2 1/6] docs: security: Describe the Kernel Assurance Plane design Ketan Bhardwaj
                   ` (5 more replies)
  0 siblings, 6 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

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.

Linux decides whether a task may perform an operation. It does not decide
whether the operation still belongs to the objective the task was started for,
which matters once long-running agents start programs, edit files and open
connections on a user's behalf. KAP binds a task and its descendants to an
igroup holding the authority compiled for that objective, and refuses
operations outside it at existing LSM hooks. Planning and policy validation
stay in privileged userspace.

v1 [1] was the design document alone. This version adds the implementation it
describes. An earlier revision of this code runs on Linux 6.18 as the guest
kernel of Kata microVMs. There, under nested virtualization and in a single
run, running the same binary unbound and then bound, enforcement added 154 ns
to a read and 787 ns to an open, while the unhooked getpid control did not
move. This version has not been measured yet.

Changes since v1:

* Add the implementation: the LSM and its igroup decision engine (3/6), the
  securityfs control plane (4/6), and checks on unshare and setns (5/6).
* Documentation: drop the admin-guide/LSM index entry, which pointed at a
  page that does not exist (Randy Dunlap). Describe the authority format,
  kap.igfd, kap.transition and the event fields as implemented, and drop
  "It does not add code".
* Add a MAINTAINERS entry (6/6).

The control plane follows the shape discussed for v1: /sys/kernel/security/kap
holds kap.default and an igroups link to a small internal filesystem, as
AppArmor publishes its policy namespaces, so an igroup is created with mkdir as
a cgroup is.

Known gaps, where feedback is welcome:

* The authority format is a strict subset of JSON and is experimental.
* Decisions are serialized by one spinlock, and those that resolve a path
  allocate a PATH_MAX buffer before taking it. Both are prototype choices.
* igroups cannot be removed yet, and kap.procs keeps the pids written to it
  after those tasks exit. A task's binding lives in its task blob and is
  released with it.
* A thread created while its process is being bound can miss the binding.
  cgroups closes the same race with cgroup_threadgroup_rwsem; KAP does not
  yet.
* Control writes need CAP_SYS_ADMIN. CAP_MAC_ADMIN may be the better fit.
* unshare and setns call KAP directly because they have no LSM hook
  (question 5 in the document).
* There are no selftests in this series yet. It was built with and without
  KAP and with networking, namespaces and io_uring disabled, and boot tested
  in QEMU with lock debugging: the control plane, file and exec decisions,
  epoch revocation of a running task and kap.default.
* The LSM ID is provisional and is for the LSM maintainers to confirm.

[1] https://lore.kernel.org/linux-security-module/20261021165748.19304-1-ketan@armoriq.io/

Base commit: 24a19b953e2a3d0ea5a8c4951a0ab8f32720c907 (Linux 7.3-rc7)

Ketan Bhardwaj (6):
  docs: security: Describe the Kernel Assurance Plane design
  security: kap: add the LSM ID and internal headers
  security: add the Kernel Assurance Plane LSM
  security: kap: add the securityfs control plane
  kernel: add KAP checks for unshare and setns
  MAINTAINERS: add an entry for the Kernel Assurance Plane

 Documentation/ABI/testing/securityfs-kap | 111 +++
 Documentation/security/index.rst         |   1 +
 Documentation/security/kap.rst           | 236 ++++++
 MAINTAINERS                              |  10 +
 include/linux/kap.h                      |  16 +
 include/linux/lsm_count.h                |   9 +-
 include/uapi/linux/lsm.h                 |   1 +
 kernel/fork.c                            |   4 +
 kernel/nsproxy.c                         |   5 +
 security/Kconfig                         |   1 +
 security/Makefile                        |   1 +
 security/kap/Kconfig                     |  14 +
 security/kap/Makefile                    |   2 +
 security/kap/igroup.c                    | 945 +++++++++++++++++++++++
 security/kap/kap.h                       | 159 ++++
 security/kap/kap_lsm.c                   | 866 +++++++++++++++++++++
 security/kap/kapfs.c                     | 524 +++++++++++++
 17 files changed, 2904 insertions(+), 1 deletion(-)
 create mode 100644 Documentation/ABI/testing/securityfs-kap
 create mode 100644 Documentation/security/kap.rst
 create mode 100644 include/linux/kap.h
 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.h
 create mode 100644 security/kap/kap_lsm.c
 create mode 100644 security/kap/kapfs.c


base-commit: 24a19b953e2a3d0ea5a8c4951a0ab8f32720c907
-- 
2.51.0


^ permalink raw reply	[flat|nested] 9+ messages in thread

* [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 2/6] security: kap: add the LSM ID and internal headers
  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 ` Ketan Bhardwaj
  2026-10-28 13:33 ` [RFC PATCH v2 3/6] security: add the Kernel Assurance Plane LSM Ketan Bhardwaj
                   ` (3 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

Reserve an LSM ID for KAP and add the declarations its files share.

The ID is the next free value in the target tree. It is provisional
and is for the LSM maintainers to confirm.

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/kap.h      |  16 ++++
 include/uapi/linux/lsm.h |   1 +
 security/kap/kap.h       | 159 +++++++++++++++++++++++++++++++++++++++
 3 files changed, 176 insertions(+)
 create mode 100644 include/linux/kap.h
 create mode 100644 security/kap/kap.h

diff --git a/include/linux/kap.h b/include/linux/kap.h
new file mode 100644
index 0000000..8d11d20
--- /dev/null
+++ b/include/linux/kap.h
@@ -0,0 +1,16 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef _LINUX_KAP_H
+#define _LINUX_KAP_H
+
+#include <linux/types.h>
+
+#ifdef CONFIG_SECURITY_KAP
+int kap_check_namespace_flags(unsigned long flags, const char *operation);
+#else
+static inline int kap_check_namespace_flags(unsigned long flags, const char *operation)
+{
+	return 0;
+}
+#endif
+
+#endif /* _LINUX_KAP_H */
diff --git a/include/uapi/linux/lsm.h b/include/uapi/linux/lsm.h
index 938593d..7cb3d64 100644
--- a/include/uapi/linux/lsm.h
+++ b/include/uapi/linux/lsm.h
@@ -65,6 +65,7 @@ struct lsm_ctx {
 #define LSM_ID_IMA		111
 #define LSM_ID_EVM		112
 #define LSM_ID_IPE		113
+#define LSM_ID_KAP		114
 
 /*
  * LSM_ATTR_XXX definitions identify different LSM attributes
diff --git a/security/kap/kap.h b/security/kap/kap.h
new file mode 100644
index 0000000..14cfe4d
--- /dev/null
+++ b/security/kap/kap.h
@@ -0,0 +1,159 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef _SECURITY_KAP_H
+#define _SECURITY_KAP_H
+
+#include <linux/list.h>
+#include <linux/spinlock.h>
+#include <linux/types.h>
+
+#define KAP_TASK_IGROUP_NAME_LEN 64
+
+struct dentry;
+struct task_struct;
+
+enum kap_hook_kind {
+	KAP_HOOK_EXEC,
+	KAP_HOOK_FILE,
+	KAP_HOOK_SOCKET_CONNECT,
+	KAP_HOOK_CRED,
+	KAP_HOOK_IPC,
+	KAP_HOOK_NAMESPACE,
+	KAP_HOOK_DESCRIPTOR,
+	KAP_HOOK_BPF,
+	KAP_HOOK_PTRACE,
+	KAP_HOOK_PROCFS,
+	KAP_HOOK_EXECMEM,
+	KAP_HOOK_IO_URING,
+	KAP_HOOK_LAST = KAP_HOOK_IO_URING,
+};
+
+struct kap_task_state {
+	bool active;
+	u64 epoch;
+	char igroup_name[KAP_TASK_IGROUP_NAME_LEN];
+};
+
+struct kap_task_state *kap_task_state(struct task_struct *task);
+bool kap_fail_closed_kind(enum kap_hook_kind kind);
+int kap_check_current_target_why(enum kap_hook_kind kind, const char *target,
+				 const char **why);
+int kap_check_current_emit(enum kap_hook_kind kind, const void *object,
+			   const char *deny_reason, const char *allow_reason);
+int kap_check_current_target_emit(enum kap_hook_kind kind, const char *target,
+				  const char *deny_reason, const char *allow_reason);
+void kap_emit_current_event_target(enum kap_hook_kind kind, int decision,
+				   const char *reason, const char *target);
+
+/* Shared between the policy engine (igroup.c) and the control plane (kapfs.c). */
+#define KAP_MAX_NAME_LEN 63
+#define KAP_MAX_ROOT_LEN 64
+#define KAP_MAX_PREDICATE_BYTES 65536
+#define KAP_MAX_EVENTS_BYTES 65536
+/* The install document is the authority plus a few short header lines. */
+#define KAP_MAX_INSTALL_BYTES (KAP_MAX_PREDICATE_BYTES + 1024)
+
+enum kap_file_kind {
+	KAP_FILE_PROCS,
+	KAP_FILE_IGFD,
+	KAP_FILE_LINEAGE_ROOT,
+	KAP_FILE_AUTHORITY_EPOCH,
+	KAP_FILE_AUTHORITY,
+	KAP_FILE_TRANSITION,
+	KAP_FILE_MODE,
+	KAP_FILE_EVENTS,
+	KAP_FILE_EXPIRES_AT,
+	KAP_FILE_INSTALL,
+	KAP_FILE_COUNT,
+	/* Filesystem-wide, not created per igroup, so it sits past the count. */
+	KAP_FILE_DEFAULT = KAP_FILE_COUNT,
+};
+
+struct kap_igroup;
+
+struct kap_file_ctx {
+	enum kap_file_kind kind;
+	struct kap_igroup *igroup;
+};
+
+struct kap_bound_pid {
+	pid_t pid;
+	struct list_head node;
+};
+
+/*
+ * Predicates are compiled once at install, so a decision only walks an array.
+ * Patterns are interned into one pool and referenced by offset, so the whole
+ * thing is a flat allocation and a decision allocates nothing.
+ *
+ * The pool is sized to the authority document itself, so any document that is
+ * accepted can be compiled: a pattern costs strlen + 1 here and at least
+ * strlen + 2 in the document because of its surrounding quotes. Each set's
+ * patterns are laid out contiguously and walked by length, so there is no
+ * per-set entry limit either.
+ */
+struct kap_pattern_set {
+	u32 offset;
+	u32 count;
+};
+
+struct kap_compiled_authority {
+	char pool[KAP_MAX_PREDICATE_BYTES];
+	u32 pool_used;
+	struct kap_pattern_set allow[KAP_HOOK_LAST + 1];
+	struct kap_pattern_set deny[KAP_HOOK_LAST + 1];
+};
+
+struct kap_igroup {
+	char name[KAP_MAX_NAME_LEN + 1];
+	u64 epoch;
+	bool revoked;
+	bool has_sequence;
+	u64 last_sequence;
+	char lineage_root[KAP_MAX_ROOT_LEN + 1];
+	char authority[KAP_MAX_PREDICATE_BYTES];
+	size_t authority_len;
+	/* Expiry in Unix seconds, 0 for none. */
+	u64 expires_at;
+	/* Swapped wholesale on a successful install; NULL until the first one. */
+	struct kap_compiled_authority *compiled;
+	char events[KAP_MAX_EVENTS_BYTES];
+	size_t events_len;
+	struct kap_file_ctx files[KAP_FILE_COUNT];
+	struct list_head pids;
+	struct list_head node;
+	struct dentry *dentry;
+};
+
+struct kap_install_request {
+	char lineage_root[KAP_MAX_ROOT_LEN + 1];
+	u64 epoch;
+	u64 expires_at;
+	u64 sequence;
+	/* Points into the caller's buffer, which outlives this request. */
+	const char *authority;
+	bool have_root;
+	bool have_epoch;
+	bool have_expires_at;
+	bool have_transition;
+	bool have_authority;
+};
+
+extern struct list_head kap_igroups;
+extern spinlock_t kap_lock;
+extern bool kap_lsm_active;
+extern bool kap_ready;
+extern bool kap_default_closed;
+
+struct kap_igroup *kap_find_igroup_locked(const char *name);
+int kap_bind_pid_locked(struct kap_igroup *igroup, pid_t pid,
+			struct kap_bound_pid **entry);
+int kap_parse_transition_sequence(const char *payload, u64 *sequence);
+int kap_install_parse(char *payload, struct kap_install_request *req);
+int kap_authority_prepare(const char *doc, struct kap_compiled_authority **out);
+int kap_install_locked(struct kap_igroup *igroup,
+		       const struct kap_install_request *req,
+		       struct kap_compiled_authority **compiled);
+void kap_trim_newline(char *value);
+int kap_securityfs_init(void);
+
+#endif /* _SECURITY_KAP_H */
-- 
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

* [RFC PATCH v2 5/6] kernel: add KAP checks for unshare and setns
  2026-10-28 13:33 [RFC PATCH v2 0/6] security: introduce Kernel Assurance Plane Ketan Bhardwaj
                   ` (3 preceding siblings ...)
  2026-10-28 13:33 ` [RFC PATCH v2 4/6] security: kap: add the securityfs control plane Ketan Bhardwaj
@ 2026-10-28 13:33 ` 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

Check unshare and setns against the bound task's authority. Neither
has an LSM hook, so they call KAP directly. Every namespace type is
checked, and setns checks the type of the namespace it is given.

Unbound tasks are not checked.

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>
---
 kernel/fork.c    | 4 ++++
 kernel/nsproxy.c | 5 +++++
 2 files changed, 9 insertions(+)

diff --git a/kernel/fork.c b/kernel/fork.c
index eb198c8..4216a2d 100644
--- a/kernel/fork.c
+++ b/kernel/fork.c
@@ -52,6 +52,7 @@
 #include <linux/cpu.h>
 #include <linux/cgroup.h>
 #include <linux/security.h>
+#include <linux/kap.h>
 #include <linux/hugetlb.h>
 #include <linux/seccomp.h>
 #include <linux/swap.h>
@@ -3276,6 +3277,9 @@ int ksys_unshare(unsigned long unshare_flags)
 			 current->fs != current->real_fs);
 
 	err = check_unshare_flags(unshare_flags);
+	if (err)
+		goto bad_unshare_out;
+	err = kap_check_namespace_flags(unshare_flags, "unshare");
 	if (err)
 		goto bad_unshare_out;
 	/*
diff --git a/kernel/nsproxy.c b/kernel/nsproxy.c
index 3fb1595..d41d7f5 100644
--- a/kernel/nsproxy.c
+++ b/kernel/nsproxy.c
@@ -25,6 +25,7 @@
 #include <linux/proc_ns.h>
 #include <linux/file.h>
 #include <linux/syscalls.h>
+#include <linux/kap.h>
 #include <linux/cgroup.h>
 #include <linux/perf_event.h>
 #include <linux/nstree.h>
@@ -589,6 +590,10 @@ SYSCALL_DEFINE2(setns, int, fd, int, flags)
 	if (err)
 		goto out;
 
+	err = kap_check_namespace_flags(flags, "setns");
+	if (err)
+		goto out;
+
 	err = prepare_nsset(flags, &nsset);
 	if (err)
 		goto out;
-- 
2.51.0


^ permalink raw reply related	[flat|nested] 9+ messages in thread

* [RFC PATCH v2 6/6] MAINTAINERS: add an entry for the Kernel Assurance Plane
  2026-10-28 13:33 [RFC PATCH v2 0/6] security: introduce Kernel Assurance Plane Ketan Bhardwaj
                   ` (4 preceding siblings ...)
  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 ` 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 maintainers and the files they look after.

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>
---
 MAINTAINERS | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/MAINTAINERS b/MAINTAINERS
index f53047a..0946d15 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -14118,6 +14118,16 @@ W:	https://linuxtv.org
 T:	git git://linuxtv.org/media.git
 F:	drivers/media/radio/radio-keene*
 
+KERNEL ASSURANCE PLANE (KAP)
+M:	Ketan Bhardwaj <ketan@armoriq.io>
+M:	HARIHARA SUDHAN. R <harisudhan@armoriq.io>
+L:	linux-security-module@vger.kernel.org
+S:	Supported
+F:	Documentation/ABI/testing/securityfs-kap
+F:	Documentation/security/kap.rst
+F:	include/linux/kap.h
+F:	security/kap/
+
 KERNEL AUTOMOUNTER
 M:	Ian Kent <raven@themaw.net>
 L:	autofs@vger.kernel.org
-- 
2.51.0


^ permalink raw reply related	[flat|nested] 9+ messages in thread

* Re: [RFC PATCH v2 1/6] docs: security: Describe the Kernel Assurance Plane design
  2026-10-28 13:33 ` [RFC PATCH v2 1/6] docs: security: Describe the Kernel Assurance Plane design Ketan Bhardwaj
@ 2026-10-28 15:17   ` Günther Noack
       [not found]     ` <CAKS=Y5WqExXSEu3PPbFY5J0GDaYJyjxqQc8HYUKAnQ6fpEfAsQ@mail.gmail.com>
  0 siblings, 1 reply; 9+ messages in thread
From: Günther Noack @ 2026-10-28 15:17 UTC (permalink / raw)
  To: Ketan Bhardwaj
  Cc: Paul Moore, James Morris, Serge E . Hallyn, 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

Hello!

On Wed, Oct 28, 2026 at 09:33:30AM -0400, Ketan Bhardwaj wrote:
> 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.

Can you please also set apart how it is conceptually different to
Landlock?

Landlock can see paths (unlike Seccomp), and Landlock policies
(rulesets) are defined and enforced by unprivileged processes through
its userspace API.  It is already a popular choice for AI agent
sandboxing and orchestration frameworks.

What is missing in Landlock which does not make it usable for your use
case?

(Obviously, there are missing features that people would want in
Landlock, but that's an argument that doesn't count IMHO, since it's
still easier to add these features to Landlock than to create a new
separate LSM.)


> +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.

It's unclear to me why procfs is not covered with "file", since you
are already doing path-based matching.  What operations are restricted
by "descriptor" is also unclear to me.

> +  * ``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``.

FWIW since pathname Unix Domain Socket are also addressed by path, we
found it useful to express the rules about them with paths as well, in
Landlock.

In networking and IPC, I am a bit missing how that would work when
*offering* a service (listen(), bind()).  Do you have thoughts on
that?

What is `sysv:flag=N`?  You might be interested in the discussion we
had at [1] -- I found System V IPC tricky to restrict, because many of
its integer identifiers are not FD-like handles, but just live fully
accessible to the entire IPC namespace.

[1] https://lore.kernel.org/all/20260727230833.138165-1-utilityemal77@gmail.com/

> +  * ``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``.

Can it be turned off as well?


> +``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.

You are creating policies through the file system, but you are at the
same time restricting access to the file system with these policies.
How does that work?

> +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.

I find both arguments unconvincing.  If it's a BPF program, you can
also audit it in one place - in the BPF program.  And the lobbying to
get CONFIG_BPF_LSM enabled in these kernels is probably not
substantially different than the lobbying to get CONFIG_KAP_LSM
enabled.

> +
> +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.

I'm surprised that it's privileged, since existing AI agent software
tends to run unprivileged.  Who would be operating this and defining
the policies?  From your description I would have expected that the
"objectives" that inform the policies are roughly equivalent to the
goals that you give to the model in an agent prompt?  Does the agent
software communicate with a privileged agent?  Or does the agent
software spawn off namespaces to attain the necessary privileges?

More of a terminology remark, but when you consider the kernel layer
only, it feels like the compiled predicate snapshot *is* the policy
that you are enforcing?


> +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?

I don't understand why the JSON format is a question for the kernel
review; you can do the interface in a format that is suitable for the
kernel, and translate from JSON into that format in userspace, not
mixing the layering between that userspace component and kernel space.


> +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?

I believe we added such hooks for the Landlock namespace and
capability support.


> +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.

I have to admit, I've read the doc, but still don't understand how
that model is supposed to work.  Do you maybe have a concrete
motivational example which defines such a policy and demonstrates why
that is different and can not implemented with one of the other LSMs
or the BPF-LSM instead?

–Günther

^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [RFC PATCH v2 1/6] docs: security: Describe the Kernel Assurance Plane design
       [not found]     ` <CAKS=Y5WqExXSEu3PPbFY5J0GDaYJyjxqQc8HYUKAnQ6fpEfAsQ@mail.gmail.com>
@ 2026-10-28 16:36       ` Ketan Bhardwaj
  0 siblings, 0 replies; 9+ messages in thread
From: Ketan Bhardwaj @ 2026-10-28 16:36 UTC (permalink / raw)
  To: Günther Noack
  Cc: Paul Moore, James Morris, Serge E . Hallyn, 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

Günther,

Thanks for taking the time to go through this in detail. Your questions
help identify where the document mixes three things: the objective's
lifecycle, the policy derived from it, and the mechanisms enforcing that
policy. We need to distinguish those more clearly.

Replies inline.

> Can you please also set apart how it is conceptually different to
> Landlock?
>
> Landlock can see paths (unlike Seccomp), and Landlock policies
> (rulesets) are defined and enforced by unprivileged processes through
> its userspace API.
>
> What is missing in Landlock which does not make it usable for your
> use case?

Landlock is certainly usable for confining an agent workload. Path-based
restrictions, inherited confinement, and unprivileged policy installation
are not distinctions we should claim for KAP.

The distinction we are trying to establish is an independently managed
authority context whose lifetime is associated with an objective, rather
than a particular process or sandbox instance.

For example, an objective can have several already-running workers. Some
may have been launched separately by the runtime, rather than being
descendants of one application process. The objective's authority can
change while those workers continue running. We want a trusted controller
to update or revoke that authority, with the kernel checking subsequent
protected operations against the current state, without requiring the
workers to cooperate.

Landlock's documented enforcement API applies restrictions to the
calling thread, or its thread group with TSYNC, with inheritance for
future descendants [4]. The interface we are exploring instead lets an
authorized external controller manage a shared authority context to
which independently executing tasks are explicitly bound. That is a
lifecycle and control-model distinction, not a distinction about whether
paths can be restricted.

The kernel would not interpret the objective. Userspace supplies the
policy and an association with the objective's authority state. The
kernel maintains the task binding and enforces that state.

> Obviously, there are missing features that people would want in
> Landlock, but that's an argument that doesn't count IMHO, since it's
> still easier to add these features to Landlock than to create a new
> separate LSM.

Agreed that a list of missing access checks would not justify a separate
LSM.

Our preference is to keep objective identity, authority generation,
execution binding, and revocation as an independent abstraction that can
coexist with different confinement mechanisms. We do not want agent
runtime frameworks to make the objective's lifecycle specific to one
sandbox policy implementation.

That does not establish that every part of this RFC is necessary, or that
the current implementation is the right decomposition. It does establish
what we would like to preserve while revising it: a common
authority-lifecycle contract, rather than another collection of sandbox
rules.

I would appreciate your guidance on what a minimal, independently
reviewable version of that abstraction would need to demonstrate. I give
a concrete example below.

> It's unclear to me why procfs is not covered with "file", since you
> are already doing path-based matching. What operations are restricted
> by "descriptor" is also unclear to me.

In this implementation, procfs is a separate category selected for paths
under /proc, with normalization of the current task's numeric PID to
/proc/self. It is still path-based enforcement. There is no fundamental
objective-authority requirement for making it a separate category, and
the documentation should not suggest that it provides a distinct,
comprehensive procfs security model.

descriptor currently refers to checks at file_receive, using the
received file's resolved path. It is not a general policy over every
descriptor operation or a complete implementation of descriptor
inheritance and transfer semantics.

Regular file use is also checked through file_permission, but that
should not be conflated with complete descriptor-lifecycle coverage. We
will update documentation to identify the exact operations covered,
particularly the distinctions between receiving a descriptor, using an
existing descriptor, and accessing an existing mapping.

> FWIW since pathname Unix Domain Socket are also addressed by path,
> we found it useful to express the rules about them with paths as well,
> in Landlock.
>
> In networking and IPC, I am a bit missing how that would work when
> *offering* a service (listen(), bind()). Do you have thoughts on that?

That makes sense. The current Unix-domain checks use coarse operation
identifiers, unix:stream_connect and unix:may_send, rather than
identifying the particular endpoint. They are not sufficient for
expressing which pathname socket a task may communicate with.

For pathname sockets, using the resolved endpoint is a better direction
than treating all Unix-domain communication as one permission. Abstract
sockets would need their own explicit semantics.

The current network implementation covers connect(), not a complete
service-offering policy. bind(), listen(), and use of accepted or
already-connected sockets need to be addressed explicitly. A connect
restriction should not be presented as covering those cases.

These are gaps in the prototype's mediation coverage, not reasons for
introducing the independent authority object. I should make that
separation clear in the document.

> What is sysv:flag=N? You might be interested in the discussion we had
> at [1] ... many of its integer identifiers are not FD-like handles,
> but just live fully accessible to the entire IPC namespace.

It is the flag argument received by the ipc_permission hook. The current
implementation formats that value into a predicate target; it does not
use ipcp to identify the particular IPC object.

Your concern applies directly. This does not establish object-scoped
System V IPC confinement, and those identifiers cannot be treated as
descriptors carrying an authority association. Thanks for the reference.
We need either a properly specified object/namespace model and
corresponding tests, or a much narrower statement of what this check
provides.

> Can it be turned off as well?
>
> You are creating policies through the file system, but you are at the
> same time restricting access to the file system with these policies.
> How does that work?

On io_uring, the three checks shown cover override_creds, sqpoll, and
uring_cmd. They do not provide a blanket disable switch for the interface
or complete lineage handling for every asynchronous operation. The
documentation should not imply otherwise.

On KAP itself, a bound task has no per-task opt-out. The current group
modes are enforce and revoked; enforce means evaluating the installed
policy, not disabling enforcement. kap.default=open affects the
additional check on unbound execution, not enforcement for already-bound
tasks. If the policy evaluates to true, it is effectively turned off.
However, thanks
for the input; we will explore if a blanket turn-off is feasible if
desired / necessary.

The control-plane writer is a separate trusted runtime component, outside
the workload's restricted igroup. It installs and updates the policy; the
managed workload does not manage its own control files. There is no
blanket exemption allowing a bound workload to access those files.

The administrator remains trusted and can replace policy or change group
state. Writable control-plane descriptors must also remain with the
trusted component, not be passed to the managed workload. This is not
intended to protect against a malicious administrator.

> 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.
>
> I find both arguments unconvincing.

Thanks for pointing this out you are right. trhe current cgroup argument
is overstated as written. Userspace can create sub-cgroups and choose
their membership. Also, entering another cgroup namespace does not itself
change cgroup membership, so the statement about setns() may not be a
sound distinction [5].

The reason for wanting an independent association is that resource
organization and objective-authority organization need not coincide.

For example, one objective may have workers in both interactive and
background resource groups, while each resource group contains workers
serving several objectives. Updating the objective's authority should
not require reorganizing those resource groups.

The intended distinction is therefore not "Linux cannot group tasks by
something defined in userspace." It already can. It is that we want an
authority association and lifecycle independent of the resource
hierarchy.

That is our argument and the one document should make, together
with an example showing why that independence is useful.

> If it's a BPF program, you can also audit it in one place ... lobbying
> to get CONFIG_BPF_LSM enabled ... is probably not substantially
> different than ... CONFIG_KAP_LSM enabled.

Agreed. Auditability and configuration availability are not sufficient
arguments against BPF LSM. BPF LSM can implement enforcement at security
hooks, and I should not imply otherwise [6].

What we are seeking is a shared contract for authority-object lifetime,
task attachment, inheritance, updates, and revocation. A particular BPF
implementation could implement parts of that contract; the question is
which semantics should be common rather than independently reconstructed
by each agent runtime framework.

The built-in implementation makes the proposed contract concrete, but
its existence is not proof that this is the smallest implementation. I
would welcome guidance on reducing the mechanism while preserving an
independent objective-authority abstraction.

> I'm surprised that it's privileged, since existing AI agent software
> tends to run unprivileged. Who would be operating this and defining
> the policies?
>
> ... are roughly equivalent to the goals that you give to the model
> in an agent prompt?
>
> Does the agent software communicate with a privileged agent? Or does
> the agent software spawn off namespaces to attain the necessary
> privileges?

The initial deployment model is a supervised agent runtime, not an
unprivileged application installing its own sandbox.

A user supplies a task, but that prompt is not itself an authorization
grant. The trusted runtime combines the task with configured workspace
access, tool contracts, credential bindings, administrator policy, and
any required approvals to produce the executable policy.

Our intentd work maintains the objective's current authority and
lifecycle in userspace. A privileged runtime-side or guest-side component
installs the resulting state before starting the managed workload. The
agent itself runs unprivileged. The runtime integration is described in
[2].

In the submitted control path, writes require authorization against the
initial user namespace. Creating a new user namespace is not how an
agent obtains permission to install KAP policy.

This deployment assumption needs to be explicit. It is a different trust
model from unprivileged self-restriction, not an argument that
self-restriction is inadequate.

> More of a terminology remark, but when you consider the kernel layer
> only, it feels like the compiled predicate snapshot *is* the policy
> that you are enforcing?

Yes. From the kernel's perspective, the predicate snapshot is the policy.

The distinction I intended was between interpreting the objective and
enforcing the resulting policy. The former stays in userspace. The
current kernel code also parses and compiles the supplied pattern
representation, so "policy stays in userspace" may too broad and
misleading.

A better description is: objective interpretation, authorization
decisions, and validation of upstream artifacts remain in userspace;
the kernel enforces the installed policy and its execution binding.

> I don't understand why the JSON format is a question for the kernel
> review; you can do the interface in a format that is suitable for the
> kernel, and translate from JSON into that format in userspace ...

Agreed. JSON is a prototype representation, not a requirement of the
model. Translation from a userspace policy format should happen before
installation.

The kernel-facing questions should concern a bounded, versioned
representation, validation, atomic installation, and update semantics.
They should not require the kernel to accommodate the userspace
configuration language.

> I believe we added such hooks for the Landlock namespace and
> capability support.

Thanks for pointing that out. The current linux-next documentation
describes namespace and capability restrictions, including namespace
acquisition through unshare(), clone(), and setns() [7]. I will check the
available hook coverage against the intended base and remove the blanket
statement that these paths lack hooks.

Direct calls to KAP are not essential to the design. Where the common
hooks cover the operation, that is the integration point we should use.
Any remaining coverage gap should be described as a generic hook
requirement, not as a reason to hardwire KAP into the syscall path.

> I've read the doc, but still don't understand how that model is
> supposed to work. Do you maybe have a concrete motivational example
> which defines such a policy and demonstrates why that is different
> and can not implemented with one of the other LSMs or the BPF-LSM
> instead?

Yes. Here is a smaller example than the current architectural description.

An agent runtime has two objectives executing in the same environment
through separate worker processes. Both can initially access a package
cache and contact a package registry.

The relevant authority contents are:

    Objective O1, generation 7:
        file access: /workspace, /package-cache
        network connect: 192.0.2.1:443

    Objective O2, generation 4:
        file access: /workspace, /package-cache
        network connect: 192.0.2.1:443

These are abbreviated policy contents, not complete startup grants.

O1 finishes dependency installation and moves to offline testing. O2 is
still doing package-resolution work. The controller changes only O1:

    Objective O1, generation 8:
        file access: /workspace
        network connect: none

A worker created under O1/generation 7 is still alive and may have an
already-open regular file in /package-cache. We want its next covered
file-access or connection check to fail without relying on that worker
to restrict itself. O2's authority should be unaffected.

In the RFC's epoch model, advancing O1's generation invalidates old task
bindings. Authorized continuations must be explicitly rebound to the
current generation; old workers do not silently acquire the replacement
authority. This is fail-closed invalidation at the next covered check,
not termination of the process, recovery of data already read, or
cancellation of every operation already in flight.

The RFC still has a disclosed concurrent-binding race and needs
selftests. I am describing the intended contract and the behavior the
revised tests should establish, not claiming complete coverage today.

There is a related implementation experience behind this. In our Agent
Substrate/Kata integration, we had to ensure that a failed
authority-binding step could not leave the workload running unbound. We
also initially installed a grant that omitted operations needed during
startup and stranded the workload. Those experiences are described in
[2].

Suspend/resume exposed another boundary: a VM snapshot can restore an
older kernel authority state. Our runtime therefore checks the current
objective state outside the snapshot before allowing the workload to
resume [3]. A guest-local epoch alone cannot solve rollback of the entire
guest. That is a runtime responsibility, not a guarantee supplied by this
patch series.

These examples motivated separating the objective's authority lifecycle
from the process and sandbox lifecycle.

They do not demonstrate that equivalent behavior is impossible to
construct with BPF or another programmable security mechanism.
The question we are bringing upstream is whether
this lifecycle deserves a common, independent kernel-visible
abstraction, rather than remaining runtime-specific coordination around
separately installed policies.

There is also a distinction between the broader design and this RFC:
the lineage root is currently an opaque userspace-supplied reference.
The kernel is not verifying a proof or proving that one policy
is a valid refinement of another. Those decisions remain trusted
userspace responsibilities in this version.

Would you suggest a way to present and implement a minimal independent
authority-domain facility that composes with existing LSM enforcement,
while keeping objective lifecycle management separate from any
particular sandbox implementation?

More specifically, would a smaller series focused on task binding and
inheritance, atomic authority-generation updates, revocation, and a
narrowly defined set of enforcement points make the model easier to
evaluate? What comparison and tests would you want to see to assess
whether that independent abstraction is justified?

We are open to changing the interface and implementation substantially.
What we would like to preserve is the separation between
objective-authority management and any one agent runtime framework or
confinement policy implementation.

I have included the broader motivation in [1] and the implementation
accounts in [2] and [3] as background. The RFC itself should contain the
concrete model and evidence needed for review without requiring readers
to follow those posts.

Thanks again for the detailed feedback.

Ketan

[1] AI agents need an OS-level enforcement primitive:
https://armoriq.substack.com/p/ai-agents-need-a-os-level-enforcement

[2] From Objective to Syscall: Building the First Intent Container:
https://armoriq.substack.com/p/from-objective-to-syscall-building

[3] Execution State Can Travel Backward. Authority Cannot:
https://armoriq.substack.com/p/execution-state-can-travel-backward

[4] Landlock documentation:
https://docs.kernel.org/userspace-api/landlock.html

[5] Control Group v2 documentation:
https://docs.kernel.org/admin-guide/cgroup-v2.html

[6] BPF LSM documentation:
https://docs.kernel.org/bpf/prog_lsm.html

[7] Landlock documentation, linux-next:
https://docs.kernel.org/next/userspace-api/landlock.html

On Wed, Oct 28, 2026 at 12:03 PM Ketan Bhardwaj <ketan@armoriq.io> wrote:
>
> Hi Günther,
>
> Thanks for the detailed feedback. These are great points.
>
> I am working on a response to address the conceptual differences with Landlock and clarify the points on the control plane, IPC, and the privilege model. I will get back to you shortly with more details and a concrete example.
>
> Best,
> Ketan
>
> On Wed, Oct 28, 2026 at 11:17 AM Günther Noack <gnoack3000@gmail.com> wrote:
>>
>> Hello!
>>
>> On Wed, Oct 28, 2026 at 09:33:30AM -0400, Ketan Bhardwaj wrote:
>> > 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.
>>
>> Can you please also set apart how it is conceptually different to
>> Landlock?
>>
>> Landlock can see paths (unlike Seccomp), and Landlock policies
>> (rulesets) are defined and enforced by unprivileged processes through
>> its userspace API.  It is already a popular choice for AI agent
>> sandboxing and orchestration frameworks.
>>
>> What is missing in Landlock which does not make it usable for your use
>> case?
>>
>> (Obviously, there are missing features that people would want in
>> Landlock, but that's an argument that doesn't count IMHO, since it's
>> still easier to add these features to Landlock than to create a new
>> separate LSM.)
>>
>>
>> > +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.
>>
>> It's unclear to me why procfs is not covered with "file", since you
>> are already doing path-based matching.  What operations are restricted
>> by "descriptor" is also unclear to me.
>>
>> > +  * ``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``.
>>
>> FWIW since pathname Unix Domain Socket are also addressed by path, we
>> found it useful to express the rules about them with paths as well, in
>> Landlock.
>>
>> In networking and IPC, I am a bit missing how that would work when
>> *offering* a service (listen(), bind()).  Do you have thoughts on
>> that?
>>
>> What is `sysv:flag=N`?  You might be interested in the discussion we
>> had at [1] -- I found System V IPC tricky to restrict, because many of
>> its integer identifiers are not FD-like handles, but just live fully
>> accessible to the entire IPC namespace.
>>
>> [1] https://lore.kernel.org/all/20260727230833.138165-1-utilityemal77@gmail.com/
>>
>> > +  * ``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``.
>>
>> Can it be turned off as well?
>>
>>
>> > +``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.
>>
>> You are creating policies through the file system, but you are at the
>> same time restricting access to the file system with these policies.
>> How does that work?
>>
>> > +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.
>>
>> I find both arguments unconvincing.  If it's a BPF program, you can
>> also audit it in one place - in the BPF program.  And the lobbying to
>> get CONFIG_BPF_LSM enabled in these kernels is probably not
>> substantially different than the lobbying to get CONFIG_KAP_LSM
>> enabled.
>>
>> > +
>> > +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.
>>
>> I'm surprised that it's privileged, since existing AI agent software
>> tends to run unprivileged.  Who would be operating this and defining
>> the policies?  From your description I would have expected that the
>> "objectives" that inform the policies are roughly equivalent to the
>> goals that you give to the model in an agent prompt?  Does the agent
>> software communicate with a privileged agent?  Or does the agent
>> software spawn off namespaces to attain the necessary privileges?
>>
>> More of a terminology remark, but when you consider the kernel layer
>> only, it feels like the compiled predicate snapshot *is* the policy
>> that you are enforcing?
>>
>>
>> > +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?
>>
>> I don't understand why the JSON format is a question for the kernel
>> review; you can do the interface in a format that is suitable for the
>> kernel, and translate from JSON into that format in userspace, not
>> mixing the layering between that userspace component and kernel space.
>>
>>
>> > +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?
>>
>> I believe we added such hooks for the Landlock namespace and
>> capability support.
>>
>>
>> > +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.
>>
>> I have to admit, I've read the doc, but still don't understand how
>> that model is supposed to work.  Do you maybe have a concrete
>> motivational example which defines such a policy and demonstrates why
>> that is different and can not implemented with one of the other LSMs
>> or the BPF-LSM instead?
>>
>> –Günther

^ permalink raw reply	[flat|nested] 9+ messages in thread

end of thread, other threads:[~2026-10-28 16:36 UTC | newest]

Thread overview: 9+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
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 15:17   ` Günther Noack
     [not found]     ` <CAKS=Y5WqExXSEu3PPbFY5J0GDaYJyjxqQc8HYUKAnQ6fpEfAsQ@mail.gmail.com>
2026-10-28 16:36       ` 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 ` [RFC PATCH v2 3/6] security: add the Kernel Assurance Plane LSM Ketan Bhardwaj
2026-10-28 13:33 ` [RFC PATCH v2 4/6] security: kap: add the securityfs control plane 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

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox