* [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; 14+ 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] 14+ 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 13:44 ` sashiko-bot
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, 2 replies; 14+ 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] 14+ 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:42 ` sashiko-bot
2026-10-28 13:33 ` [RFC PATCH v2 3/6] security: add the Kernel Assurance Plane LSM Ketan Bhardwaj
` (3 subsequent siblings)
5 siblings, 1 reply; 14+ 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] 14+ 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:57 ` sashiko-bot
2026-10-28 13:33 ` [RFC PATCH v2 4/6] security: kap: add the securityfs control plane Ketan Bhardwaj
` (2 subsequent siblings)
5 siblings, 1 reply; 14+ 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] 14+ 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:57 ` sashiko-bot
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, 1 reply; 14+ 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] 14+ 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:40 ` sashiko-bot
2026-10-28 13:33 ` [RFC PATCH v2 6/6] MAINTAINERS: add an entry for the Kernel Assurance Plane Ketan Bhardwaj
5 siblings, 1 reply; 14+ 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] 14+ 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; 14+ 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] 14+ messages in thread
* Re: [RFC PATCH v2 5/6] kernel: add KAP checks for unshare and setns
2026-10-28 13:33 ` [RFC PATCH v2 5/6] kernel: add KAP checks for unshare and setns Ketan Bhardwaj
@ 2026-10-28 13:40 ` sashiko-bot
0 siblings, 0 replies; 14+ messages in thread
From: sashiko-bot @ 2026-10-28 13:40 UTC (permalink / raw)
Cc: linux-security-module
> 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>
Sashiko has reviewed this patch and found no issues.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261028133335.75899-1-ketan@armoriq.io?part=5
^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [RFC PATCH v2 2/6] security: kap: add the LSM ID and internal headers
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:42 ` sashiko-bot
0 siblings, 0 replies; 14+ messages in thread
From: sashiko-bot @ 2026-10-28 13:42 UTC (permalink / raw)
Cc: linux-security-module
> 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>
Sashiko has reviewed this patch and found no issues.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261028133335.75899-1-ketan@armoriq.io?part=2
^ permalink raw reply [flat|nested] 14+ 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 13:44 ` sashiko-bot
2026-10-28 15:17 ` Günther Noack
1 sibling, 0 replies; 14+ messages in thread
From: sashiko-bot @ 2026-10-28 13:44 UTC (permalink / raw)
Cc: linux-security-module
> 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>
Sashiko has reviewed this patch and found no issues.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261028133335.75899-1-ketan@armoriq.io?part=1
^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [RFC PATCH v2 4/6] security: kap: add the securityfs control plane
2026-10-28 13:33 ` [RFC PATCH v2 4/6] security: kap: add the securityfs control plane Ketan Bhardwaj
@ 2026-10-28 13:57 ` sashiko-bot
0 siblings, 0 replies; 14+ messages in thread
From: sashiko-bot @ 2026-10-28 13:57 UTC (permalink / raw)
Cc: linux-security-module
Thank you for your contribution! Sashiko AI review found 9 potential issue(s) to consider:
- [High] security: kap: detached kapfs_mnt breaks d_path() in kap_igroups_get_link()
- [Medium] security: kap: dentry reference leak on control files in kapfs_mkdir()
- [Medium] security: kap: missing superblock ops and blocksize in kapfs_fill_super()
- [Medium] security: kap: unbounded PID list growth in kap_bind_pid_locked()
- [Medium] security: kap: silent no-op on newline or NUL writes in kapfs_write()
- [Medium] security: kap: unlocked kap_task_state race in kap_ptrace_target()
- [Medium] security: kap: CLONE_THREAD bind race in kap_bind_pid_locked()
- [Medium] security: kap: predicate boundary overrun in kap_compile_kind_array()
- [Medium] security: kap: short sockaddr stack leak in kap_socket_connect()
--
commit b34e00991514a9c6893e4f407d13e24385bf26ea
Author: Ketan Bhardwaj <ketan@armoriq.io>
security: kap: add the securityfs control plane
This commit adds the securityfs control plane for the Kernel Assurance Plane
under /sys/kernel/security/kap and mounts an internal kapfs filesystem
reached via the igroups symlink to manage per-igroup directories and control
files.
> diff --git a/security/kap/kapfs.c b/security/kap/kapfs.c
> new file mode 100644
> index 0000000000000..36ef1e33a14e1
> --- /dev/null
> +++ b/security/kap/kapfs.c
[ ... ]
> +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;
[Severity: Medium]
Does igroup->pids grow without bound and report stale pids here?
When a pid is written to kap.procs in kapfs_write(), an entry is allocated
with kzalloc_obj(*entry, GFP_KERNEL) and appended to igroup->pids in
kap_bind_pid_locked():
security/kap/igroup.c:kap_bind_pid_locked() {
...
if (kap_pid_in_igroup_locked(igroup, pid))
return 0;
(*entry)->pid = pid;
list_add_tail(&(*entry)->node, &igroup->pids);
*entry = NULL;
return 0;
}
However, kap_task_free() in security/kap/kap_lsm.c only zeroes the task blob
when a task exits, and rebinding a task to another igroup in
kap_bind_pid_locked() does not remove it from the previous igroup's pids
list:
security/kap/kap_lsm.c:kap_task_free() {
struct kap_task_state *state = kap_task_state(task);
if (state)
memset(state, 0, sizeof(*state));
}
As bound processes exit or rebind over the system lifetime, won't the
kap_bound_pid allocations in igroup->pids leak permanently and cause
kapfs_show() to print dead, recycled, or rebound pids?
[ ... ]
> + case KAP_FILE_EVENTS:
> + seq_write(m, igroup->events, igroup->events_len);
> + break;
[Severity: Medium]
Can kapfs_show() expose uninitialized kernel stack bytes through
igroup->events when a bound task calls connect() with a short address length?
In security/kap/kap_lsm.c, kap_socket_connect() only verifies that addrlen is
at least offsetofend(struct sockaddr, sa_family) (2 bytes) before passing
address to kap_check_current_emit():
security/kap/kap_lsm.c:kap_socket_connect() {
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");
}
In __sys_connect(), the kernel stack sockaddr_storage buffer is not
zero-initialized, and move_addr_to_kernel() only copies addrlen bytes from
userspace. When kap_object_target() in security/kap/igroup.c formats the
target for igroup->events, it reads sin_addr and sin_port or sin6_addr and
sin6_port without checking addrlen:
security/kap/igroup.c:kap_object_target() {
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;
}
}
If a bound task calls connect() with addrlen == 2 and sa_family set to
AF_INET or AF_INET6, won't kap_object_target() format uninitialized kernel
stack memory into igroup->events and return it to userspace here?
[ ... ]
> +static ssize_t kapfs_write(struct file *file, const char __user *buf, size_t len, loff_t *ppos)
> +{
[ ... ]
> + 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;
> + }
[Severity: Medium]
Should writing a newline-only string or a NUL-prefixed buffer to a control
file return -EINVAL instead of succeeding as a silent no-op?
Because kapfs_write() returns len whenever tmp[0] is NUL after
kap_trim_newline(tmp), writing "\n" or "\0..." to kap.install, kap.procs,
kap.authority, kap.authority_epoch, kap.expires_at, kap.transition,
kap.mode, kap.default, or kap.lineage_root reports success to userspace
without validating the input or updating any state.
> +
> + /* 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;
[Severity: Medium]
When kapfs_write() passes an authority document to kap_authority_prepare(),
can predicates bleed across adjacent objects in kap_compile_kind_array() and
kap_validate_authority_payload()?
In security/kap/igroup.c, both kap_compile_kind_array() and
kap_validate_authority_payload() bound each predicate by scanning backwards
from "kind": to the nearest '{' character and scanning forwards to the next
"kind": substring:
security/kap/igroup.c:kap_compile_kind_array() {
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 the next predicate places its "allow":[...] or "deny":[...] array before
its "kind":"..." key, that array precedes next and falls inside the previous
predicate's [predicate_start, predicate_end) range. Similarly, if an array
before "kind" contains a '{' character in a path pattern, scanning backwards
for '{' stops inside the string literal rather than at the start of the
predicate object, skipping the array before it.
[ ... ]
> + spin_lock(&kap_lock);
> + switch (ctx->kind) {
> + case KAP_FILE_PROCS:
> + rc = kap_bind_pid_locked(igroup, pid, &entry);
> + break;
[Severity: Medium]
Can binding a live process via kap_bind_pid_locked() race with concurrent
ptrace checks in kap_ptrace_target() and miss threads being created via
clone(CLONE_THREAD)?
First, kap_bind_pid_locked() mutates each thread's kap_task_state in place
under kap_lock, setting state->active = true before writing state->epoch and
state->igroup_name:
security/kap/igroup.c:kap_bind_pid_locked() {
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));
}
}
In security/kap/kap_lsm.c, kap_ptrace_access_check() and kap_ptrace_traceme()
call kap_ptrace_target() without holding kap_lock:
security/kap/kap_lsm.c:kap_ptrace_target() {
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;
}
}
Because kap_ptrace_target() reads active, igroup_name, and epoch unlocked,
can it observe state->active == true while igroup_name is still empty or
partially written, or while epoch is stale?
Second, in copy_process(), security_task_alloc() -> kap_task_alloc() runs
before copy_process() acquires current->sighand->siglock and links the new
thread onto signal->thread_head:
security/kap/kap_lsm.c:kap_task_alloc() {
/* 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);
}
If a thread in the target process is between kap_task_alloc() and
list_add_tail_rcu(&p->thread_node, &p->signal->thread_head) when
kap_bind_pid_locked() iterates for_each_thread(task, t), won't that new
thread be missed by both kap_task_alloc() and for_each_thread() and run
unconfined?
[ ... ]
> +/* 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)
> +{
[ ... ]
> + 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]);
[ ... ]
> + }
[ ... ]
> + /* 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;
[Severity: Medium]
Does kapfs_mkdir() leak a dentry reference on each of the files[i] dentries?
Each files[i] is allocated via d_alloc_name(dentry, kap_file_names[i]) with
an initial reference count of 1 owned by kapfs_mkdir(). Calling
kapfs_instantiate(files[i], inodes[i]) invokes d_make_persistent(), which
sets DCACHE_PERSISTENT and increments d_lockref.count to 2 via dget_dlock():
fs/dcache.c:d_make_persistent() {
...
__d_instantiate(dentry, inode);
dentry->d_flags |= DCACHE_PERSISTENT;
dget_dlock(dentry);
if (d_unhashed(dentry))
__d_rehash(dentry);
...
}
Unlike the directory dentry passed into kapfs_mkdir(), whose lookup/creation
reference is dropped by the VFS caller after vfs_mkdir() returns, nothing
calls dput(files[i]) after kapfs_instantiate(files[i], inodes[i]), leaving
every control file dentry with an extra reference count of 2 when idle.
[ ... ]
> +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;
> +}
[Severity: Medium]
Should kapfs_fill_super() also initialize sb->s_op, sb->s_blocksize, and
sb->s_blocksize_bits?
Without setting sb->s_op = &simple_super_operations, sb->s_op remains at
&default_op where .statfs is NULL, causing statfs() and fstatfs() on paths
under /sys/kernel/security/kap/igroups to fail with -ENOSYS.
In addition, leaving sb->s_blocksize_bits as 0 causes new_inode() to set
inode->i_blkbits = 0 on all kapfs inodes, so stat() reports st_blksize = 1.
Userspace tools using stdio buffered writes sized from st_blksize will then
split writes into 1-byte write() calls, which kapfs_write() rejects with
-EINVAL on multi-byte control files such as kap.install and kap.authority.
[ ... ]
> +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);
> +}
[Severity: High]
Does jumping to the unattached kapfs_mnt via nd_jump_link() break d_path()
resolution in the LSM hooks and allow a confined task to bypass path rules?
In kap_securityfs_init(), kapfs_mnt is created with kern_mount(&kapfs_type),
so it is an internal mount with no parent mount and mnt_ns == NULL. When a
process traverses /sys/kernel/security/kap/igroups, kap_igroups_get_link()
replaces nd->path with (kapfs_mnt, kapfs_mnt->mnt_root).
When KAP's LSM hooks resolve paths inside kapfs using d_path() in
kap_file_target(), kap_lsm_file_path_alloc(), kap_lsm_path_target(), and
kap_lsm_check_dir_entry():
security/kap/igroup.c:kap_file_target() {
...
path = d_path(&file->f_path, path_buf, PATH_MAX);
if (IS_ERR(path))
rc = PTR_ERR(path);
else
strscpy(target, path, target_len);
...
}
d_path() stops at kapfs_mnt->mnt_root and formats paths relative to the root
of kapfs_mnt as "/<igroup>" (for mkdir) and "/<igroup>/<file>" (for open,
read, and write) rather than "/sys/kernel/security/kap/igroups/<igroup>/...".
Won't this cause two problems for bound tasks:
1. A bound task with CAP_SYS_ADMIN that is denied "/sys/**" but allowed a
top-level path such as "/tmp" or "/tmp/**" can call
mkdir("/sys/kernel/security/kap/igroups/tmp", 0700) (evaluated by
kap_lsm_check_dir_entry() as "/tmp"), write an unrestricted policy to
"/sys/kernel/security/kap/igroups/tmp/kap.install" (evaluated as
"/tmp/kap.install"), and write its own pid to
"/sys/kernel/security/kap/igroups/tmp/kap.procs" (evaluated as
"/tmp/kap.procs"), which overwrites state->igroup_name in
kap_bind_pid_locked() and escapes its confinement?
2. A bound orchestrator task granted "/sys/kernel/security/kap/**" will have
its accesses to "/sys/kernel/security/kap/igroups/<igroup>/..." evaluated
as "/<igroup>/..." and rejected with -EACCES, while an igroup named "proc"
will match kap_path_is_procfs() and be evaluated against procfs rules?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261028133335.75899-1-ketan@armoriq.io?part=4
^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [RFC PATCH v2 3/6] security: add the Kernel Assurance Plane LSM
2026-10-28 13:33 ` [RFC PATCH v2 3/6] security: add the Kernel Assurance Plane LSM Ketan Bhardwaj
@ 2026-10-28 13:57 ` sashiko-bot
0 siblings, 0 replies; 14+ messages in thread
From: sashiko-bot @ 2026-10-28 13:57 UTC (permalink / raw)
Cc: linux-security-module
Thank you for your contribution! Sashiko AI review found 15 potential issue(s) to consider:
- [Critical] security: kap: missing addrlen check in kap_socket_connect() leaks stack
- [High] security: kap: wrong PID namespace and TID in kap_normalize_procfs_self()
- [High] security: kap: execmem and memfd misclassification in mmap and exec hooks
- [High] security: kap: data race on kap_task_state in kap_ptrace_target()
- [High] security: kap: fork race between kap_task_alloc() and kap_bind_pid_locked()
- [High] security: kap: predicate boundary misparse in kap_compile_kind_array()
- [High] security: kap: mount hooks truncate paths and fail open on path errors
- [High] security: kap: deleted suffix bypasses exact deny in kap_file_prefix_match()
- [High] security: kap: unpinned old_dentry->d_parent race in kap_path_link()
- [High] security: kap: kap_file_permission() skips revocation for anon channels
- [Medium] security: kap: decision helpers fail open when bound igroup is missing
- [Medium] security: kap: missing kap in default CONFIG_LSM strings in Kconfig
- [Medium] security: kap: unconditional kap_lock acquisition in kap_task_alloc()
- [Medium] security: kap: u64 overflow in kap_parse_transition_sequence()
- [Medium] security: kap: leaked kap_bound_pid entries on task exit and rebind
--
commit d9df2395eaa207ea017067bab85ba4a25d760030
Author: Ketan Bhardwaj <ketan@armoriq.io>
security: add the Kernel Assurance Plane LSM
This commit adds the Kernel Assurance Plane (KAP) LSM, implementing Intent
Lineage Groups (igroups), authority predicate compilation and evaluation, and
the LSM hook callbacks that enforce igroup authority on bound tasks.
> diff --git a/security/Kconfig b/security/Kconfig
> index f7bf6cdc6229e..c2e6249fb8abb 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"
[Severity: Medium]
In security/kap/kap_lsm.c, DEFINE_LSM(kap) omits .order and therefore defaults
to LSM_ORDER_MUTABLE, which means security/lsm_init.c only initializes kap if
it is listed in CONFIG_LSM or on the lsm= kernel command line.
Since security/Kconfig does not add kap to any of the default strings for
config LSM, won't enabling CONFIG_SECURITY_KAP=y leave KAP inactive at boot on
default configurations unless the user manually overrides CONFIG_LSM or passes
lsm= on the command line?
> diff --git a/security/kap/igroup.c b/security/kap/igroup.c
> new file mode 100644
> index 0000000000000..069905a32e8ea
> --- /dev/null
> +++ b/security/kap/igroup.c
[ ... ]
> +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_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;
> + }
[Severity: Critical]
Can this read uninitialized kernel memory and leak it into kap.events and
dmesg?
In security/kap/kap_lsm.c, kap_socket_connect() only checks that addrlen is at
least offsetofend(struct sockaddr, sa_family) (2 bytes) and drops addrlen when
calling kap_check_current_emit(KAP_HOOK_SOCKET_CONNECT, address, ...):
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");
When a bound task calls connect() (via __sys_connect(), which allocates an
uninitialized struct sockaddr_storage on the kernel stack, or via io_uring
io_connect_prep()) with addrlen = 2 and sa_family = AF_INET or AF_INET6,
move_addr_to_kernel() copies only 2 bytes before invoking
security_socket_connect().
Here in kap_object_target(), casting object to struct sockaddr_in * or
struct sockaddr_in6 * and formatting sin_addr/sin_port or sin6_addr/sin6_port
without verifying addrlen >= sizeof(struct sockaddr_in) or
addrlen >= sizeof(struct sockaddr_in6) reads up to 22 bytes of uninitialized
kernel stack or heap memory, evaluates the policy against it, and writes the
formatted result to kap.events and the kernel log.
[ ... ]
> +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);
> +}
[Severity: High]
What happens in kap_file_prefix_match() and kap_exec_decision_locked() when a
file or binary is unlinked while open?
Both kap_file_target() and kap_lsm_file_path_alloc() resolve paths with
d_path(&file->f_path, ...), which appends " (deleted)" when
d_unlinked(path->dentry) is true.
If a denied file "/srv/work/keys" inside an allowed directory "/srv/work" is
unlinked while open (and later checked in kap_file_permission(), received in
kap_file_receive(), or opened via /proc/pid/fd/N), target becomes
"/srv/work/keys (deleted)". Because target[len] is a space rather than '/' or
'\0', the exact deny pattern "/srv/work/keys" fails to match in
kap_file_prefix_match(), while the parent directory allow pattern "/srv/work"
still matches.
Similarly, in kap_exec_decision_locked(), executing an unlinked binary with
execveat(fd, "", ..., AT_EMPTY_PATH) produces resolved =
"/usr/bin/prog (deleted)" and bprm->filename = "/dev/fd/N", which misses an
exact deny rule "/usr/bin/prog" while still matching an allow rule
"/usr/bin/*".
Should unlinked files be handled before matching against path predicates?
[ ... ]
> +/* 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;
> +}
[ ... ]
> +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;
> +}
[Severity: High]
Can kap_compile_kind_array() and kap_validate_authority_payload() misparse
predicate boundaries and compile allow/deny arrays into the wrong kind?
Both functions determine a predicate's span [predicate_start, predicate_end)
by scanning backward from "\"kind\":\"" to the first '{' and forward to the
next "\"kind\":".
First, if predicate N+1 places its "\"allow\":[...]" or "\"deny\":[...]" array
before its "\"kind\":\"...\"" key, predicate N's predicate_end extends past
predicate N's closing '}' into predicate N+1. As a result, kap_compile_array()
matches and compiles predicate N+1's allow/deny arrays into predicate N's
kind.
Second, if a pattern string placed before "\"kind\"" contains a '{' character
(for example, "/safe/{secret}"), scanning backward from "\"kind\"" for '{'
stops inside the string literal rather than at the predicate object's opening
'{', causing the arrays preceding it to be skipped.
[ ... ]
> +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);
> +}
[ ... ]
> +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;
> + }
[Severity: Medium]
Doesn't this fail open if a task is marked active (state->active == true),
but kap_find_igroup_locked(state->igroup_name) returns NULL?
Because kap_find_current_igroup_locked() returns NULL both when current is
unbound and when current is bound to an igroup_name that cannot be found in
kap_igroups, kap_check_current_target_why(), kap_check_current_emit(), and
kap_check_current_target_emit() only refuse when kap_unbound_refused(kind) is
true and otherwise jump to out with rc = 0.
Should a bound task whose igroup is missing fail closed with -EACCES?
[ ... ]
> +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;
> +}
[Severity: Medium]
Can value overflow u64 in kap_parse_transition_sequence()?
Unlike authority_epoch and expires_at in kap_install_parse(), which use
kstrtou64(), this loop accumulates decimal digits into value without checking
for 64-bit multiplication or addition overflow, allowing sequence numbers
larger than U64_MAX to wrap modulo 2^64 and bypass the monotonic sequence
check in kap_install_locked().
[ ... ]
> +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;
> +}
[Severity: Medium]
Does igroup->pids leak struct kap_bound_pid entries when bound tasks exit or
are rebound to another igroup?
Here in kap_bind_pid_locked(), *entry is added to igroup->pids via
list_add_tail(). However, when a task exits, kap_task_free() in
security/kap/kap_lsm.c only zeroes the task's struct kap_task_state without
removing or freeing any entry from igroup->pids, and rebinding a task to a
different igroup overwrites state->igroup_name without removing its pid from
the previous igroup's pids list.
> diff --git a/security/kap/kap_lsm.c b/security/kap/kap_lsm.c
> new file mode 100644
> index 0000000000000..b148b3437d900
> --- /dev/null
> +++ b/security/kap/kap_lsm.c
[ ... ]
> +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;
> +}
[Severity: High]
Can a newly cloned child or thread miss a concurrent bind in
kap_bind_pid_locked()?
In copy_process(), security_task_alloc() calls kap_task_alloc() early—before
copy_files(), copy_mm(), and alloc_pid(), and before tasklist_lock and siglock
are acquired to link the new task onto signal->thread_head.
If kap_bind_pid_locked() runs on another CPU after kap_task_alloc() drops
kap_lock, but before copy_process() links the new task onto
task->signal->thread_head, for_each_thread(task, t) in kap_bind_pid_locked()
will not see the in-flight task, leaving the new thread or child process
unbound (or running with the pre-bind igroup and epoch).
[Severity: Medium]
Also in kap_task_alloc(), is it intentional that spin_lock(&kap_lock) is
acquired unconditionally on every fork() and clone() across the system?
The commit message notes that an unbound task returns before taking any lock,
but kap_task_alloc() takes the global kap_lock even when kap_ready is false or
current is unbound.
[ ... ]
> +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");
> +}
[ ... ]
> +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);
> +}
[Severity: High]
Doesn't kap_normalize_procfs_self() use the wrong PID and PID namespace when
matching "/proc/%d"?
procfs resolves /proc/self (in fs/proc/self.c) using
task_tgid_nr_ns(current, proc_pid_ns(inode->i_sb)), which is the thread-group
leader's TGID in the PID namespace of that procfs mount. Here,
kap_normalize_procfs_self() formats "/proc/%d" using task_pid_nr(current),
which is the calling thread's TID in init_pid_ns.
Because of this mismatch:
1. For any non-leader thread (current->pid != current->tgid) or any task in a
child PID namespace with its own /proc mount, d_path() produces
/proc/<ns_tgid>/..., which does not match /proc/<init_tid> and is not
normalized to /proc/self/....
2. In a child PID namespace, if another process in the container happens to
have a container-local PID equal to current's init_pid_ns TID,
kap_normalize_procfs_self() rewrites that other process's /proc/<pid>/...
path to /proc/self/..., allowing current to access another process's procfs
files under a /proc/self grant.
Should this use task_tgid_nr_ns(current, proc_pid_ns(file_inode(file)->i_sb))
instead of task_pid_nr(current)?
> +
> +static bool kap_path_is_memfd(const char *path)
> +{
> + return strstr(path, "/memfd:") || strstr(path, "memfd:");
> +}
[ ... ]
> +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;
> + }
[Severity: High]
When kap_path_is_anonymous_channel(target) is true ("pipe:", "socket:", or
"anon_inode:"), kap_file_permission() returns 0 before calling
kap_check_current_target_why().
Since the authority-state checks (kap_current_epoch_stale_locked(),
igroup->expires_at, igroup->revoked, and !igroup->authority_len) only run
inside kap_authority_decision_locked(), won't returning 0 early here allow a
bound task whose authority has expired, been revoked, or become epoch-stale to
continue reading and writing already-open sockets, pipes, and anon_inodes?
[ ... ]
> +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;
> +}
[Severity: High]
Are there several ways to bypass KAP_HOOK_EXECMEM and KAP_HOOK_EXEC through
kap_mmap_file(), kap_file_mprotect(), kap_bprm_check_security(), and
kap_path_is_memfd()?
1. In kap_file_mprotect(), checking vma->vm_file == NULL instead of
vma_is_anonymous(vma) misses private mappings of /dev/zero (where
vma_is_anonymous(vma) is true but vma->vm_file points to /dev/zero) as well
as MAP_ANONYMOUS | MAP_SHARED mappings (where shmem_zero_setup() attaches an
internal "/dev/zero (deleted)" file to vma->vm_file).
2. In both kap_mmap_file() and kap_file_mprotect(), when file is non-NULL:
- if kap_lsm_file_path_alloc(file) returns an ERR_PTR (such as -ENOMEM or
-ENAMETOOLONG), returning 0 fails open; and
- if !kap_path_is_memfd(target), both hooks return 0 without consulting
KAP_HOOK_EXECMEM at all (which also leaves case KAP_HOOK_EXECMEM in
kap_object_target() unused), allowing executable mappings of /dev/zero,
MAP_ANONYMOUS | MAP_SHARED, O_TMPFILE, or any regular file.
3. kap_path_is_memfd() checks strstr(path, "/memfd:") || strstr(path, "memfd:"),
which matches any filesystem path containing the substring "memfd:". In
kap_bprm_check_security(), executing an on-disk file whose path contains
"memfd:" (e.g. /tmp/memfd:bin) is routed to KAP_HOOK_EXECMEM instead of
KAP_HOOK_EXEC (and skips kap_exec_decision_locked()'s check on
bprm->filename).
[ ... ]
> +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);
[ ... ]
> +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);
> +}
[Severity: High]
Is reading old_dentry->d_parent without dget_parent() in kap_path_link() safe
against a concurrent rename()?
When do_linkat() calls security_path_link(old_path.dentry, &new_path,
new_dentry), the VFS holds a reference on old_dentry and holds i_rwsem only on
new_dir->dentry->d_inode; old_dentry's parent directory is not locked and
old_dentry->d_parent is not separately pinned.
kap_path_link() copies old_dentry->d_parent into old_path.dentry without
taking a reference, and kap_lsm_check_dir_entry() then performs two sleeping
kmalloc(PATH_MAX, GFP_KERNEL) allocations before passing old_path to d_path().
If a concurrent rename moves old_dentry to another directory and the former
parent dentry is removed, old_path.dentry can be freed before d_path()
dereferences it. Why not construct old_path as
{ .mnt = new_dir->mnt, .dentry = old_dentry } and resolve it directly with
d_path()?
[ ... ]
> +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");
> +}
[Severity: High]
In kap_sb_mount(), kap_sb_umount(), kap_sb_pivotroot(), and kap_move_mount(),
what happens when a mount path is longer than KAP_LSM_TARGET_LEN (256 bytes)
or when kap_lsm_path_target() fails?
kap_lsm_path_target() copies the PATH_MAX result of d_path() into target using
strscpy(target, rendered, target_len) and ignores the -E2BIG truncation error,
and the mount hooks then format one or two 256-byte path buffers into a single
256-byte target buffer via scnprintf(). Furthermore, if kap_lsm_path_target()
returns an error, the mount hooks substitute "unknown" instead of refusing the
operation.
As noted in the comment above kap_lsm_check_dir_entry(), truncating a path or
substituting a placeholder like "unknown" lets a broad allow pattern (such as
"mount:*" or "*") pass while preventing a specific deny rule on the actual
path from matching. Shouldn't these hooks allocate full-length buffers and
refuse on path resolution failure or truncation?
[ ... ]
> +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);
> +}
[Severity: High]
Can kap_ptrace_target() race with kap_bind_pid_locked() when reading
current_state and child_state?
In security/kap/igroup.c, kap_bind_pid_locked() updates each thread's
struct kap_task_state in place under kap_lock by setting state->active = true
before writing state->epoch and strscpy(state->igroup_name, ...):
state->active = true;
state->epoch = igroup->epoch;
strscpy(state->igroup_name, igroup->name, sizeof(state->igroup_name));
However, kap_ptrace_target() (called by kap_ptrace_access_check() and
kap_ptrace_traceme() before kap_check_current_target_emit() acquires kap_lock)
reads active, igroup_name, and epoch of both current and child without holding
kap_lock. If a bind or rebind runs concurrently on another CPU,
kap_ptrace_target() can observe state->active == true while state->igroup_name
is still empty or partially written, or paired with an inconsistent epoch.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261028133335.75899-1-ketan@armoriq.io?part=3
^ permalink raw reply [flat|nested] 14+ 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 13:44 ` sashiko-bot
@ 2026-10-28 15:17 ` Günther Noack
[not found] ` <CAKS=Y5WqExXSEu3PPbFY5J0GDaYJyjxqQc8HYUKAnQ6fpEfAsQ@mail.gmail.com>
1 sibling, 1 reply; 14+ 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] 14+ 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; 14+ 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] 14+ messages in thread
end of thread, other threads:[~2026-10-28 16:36 UTC | newest]
Thread overview: 14+ 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 13:44 ` sashiko-bot
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:42 ` sashiko-bot
2026-10-28 13:33 ` [RFC PATCH v2 3/6] security: add the Kernel Assurance Plane LSM Ketan Bhardwaj
2026-10-28 13:57 ` sashiko-bot
2026-10-28 13:33 ` [RFC PATCH v2 4/6] security: kap: add the securityfs control plane Ketan Bhardwaj
2026-10-28 13:57 ` sashiko-bot
2026-10-28 13:33 ` [RFC PATCH v2 5/6] kernel: add KAP checks for unshare and setns Ketan Bhardwaj
2026-10-28 13:40 ` sashiko-bot
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