Linux Security Modules development
 help / color / mirror / Atom feed
* [RFC PATCH v3 0/6] security: introduce Kernel Assurance Plane
@ 2026-11-10 20:22 HARIHARA SUDHAN. R
  2026-11-10 20:22 ` [RFC PATCH v3 1/6] docs: security: Describe the Kernel Assurance Plane design HARIHARA SUDHAN. R
                   ` (5 more replies)
  0 siblings, 6 replies; 12+ messages in thread
From: HARIHARA SUDHAN. R @ 2026-11-10 20:22 UTC (permalink / raw)
  To: linux-kernel, linux-security-module, Paul Moore, Kees Cook,
	James Morris, Serge E . Hallyn
  Cc: Andrew Morton, Jonathan Corbet, Randy Dunlap, Peter Zijlstra,
	Ingo Molnar, Steven Rostedt, Liam R . Howlett, Lorenzo Stoakes,
	Vlastimil Babka, David Hildenbrand, Mike Rapoport, Michal Hocko,
	Mel Gorman, Suren Baghdasaryan, Ben Segall, Dietmar Eggemann,
	Juri Lelli, K Prateek Nayak, Valentin Schneider, Vincent Guittot,
	Shuah Khan, Ketan Bhardwaj, bpf, linux-doc, linux-mm,
	HARIHARA SUDHAN . R

Kernel Assurance Plane (KAP) adds an objective-scoped authority context as a
kernel object. An Intent Lineage Group (igroup) is that object: an externally
managed authority whose lifetime is tied to an objective rather than to a
process, a thread group or a sandbox, that a trusted controller can update or
revoke in place while unmodified worker tasks keep running.

v1 [1] was the design document. v2 added the implementation and moved the
control plane under securityfs. This version answers the v2 review.

The review asked, in several forms, why this needs a new LSM rather than
Landlock or a BPF LSM. The short answer, now the spine of the design document,
is that path and syscall mediation is not the distinction: Landlock and seccomp
already do that, often better. What they do not provide is an authority context
that an outside controller can narrow or revoke, per objective, for a task that
is already running, while a second objective beside it is untouched. The
document opens with that and gives a two-objective example.

Changes since v2:

* Design document (1/6) reworked to answer the review:
  - leads with the authority-lifecycle distinction and the two-objective
    example, and states plainly that the mediation overlaps Landlock;
  - raises the central question (question 1): should KAP be a minimal
    authority-lifecycle facility that composes with Landlock and existing hooks
    for enforcement, rather than a self-contained LSM with its own mediation;
  - drops the incorrect "cgroup membership cannot follow setns" claim and
    reframes the cgroup and BPF-LSM rationales around which lifecycle semantics
    should be a shared contract, not which mechanism enforces;
  - states the JSON subset is a prototype wire form, not the model;
  - narrows the net (connect only), ipc (coarse Unix, no SysV confinement),
    descriptor (file_receive only), procfs and io_uring claims to what is
    implemented;
  - adds the deployment and privilege model and the lineage-root trust
    boundary (opaque, not kernel-verified in this version);
  - reframes question 5 to prefer existing namespace-transition hooks where
    the target tree has them.
* Code (4/6): the only code change is removing an unused securityfs dentry that
  the kernel test robot flagged (-Wunused-but-set-global). The code is otherwise
  identical to v2.

The prototype runs as the guest kernel of Kata microVMs (Linux 6.18) and of
Docker Sandboxes (Linux 7.0). In a Kata guest under nested virtualization,
running the same binary unbound and then bound, enforcement adds 154 ns to a
read and 787 ns to an open, while the unhooked getpid control does not move. On
Docker Sandboxes, allow, deny and revoke were exercised on a real agent.

Known gaps, where feedback is welcome:

* Whether KAP should keep its own mediation or compose with Landlock and
  existing hooks is open and is question 1; the current series is the
  self-contained form.
* The authority wire form 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 in this
  base (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)

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

* [RFC PATCH v3 1/6] docs: security: Describe the Kernel Assurance Plane design
  2026-11-10 20:22 [RFC PATCH v3 0/6] security: introduce Kernel Assurance Plane HARIHARA SUDHAN. R
@ 2026-11-10 20:22 ` HARIHARA SUDHAN. R
  2026-11-10 20:33   ` sashiko-bot
  2026-11-10 20:22 ` [RFC PATCH v3 2/6] security: kap: add the LSM ID and internal headers HARIHARA SUDHAN. R
                   ` (4 subsequent siblings)
  5 siblings, 1 reply; 12+ messages in thread
From: HARIHARA SUDHAN. R @ 2026-11-10 20:22 UTC (permalink / raw)
  To: linux-kernel, linux-security-module, Paul Moore, Kees Cook,
	James Morris, Serge E . Hallyn
  Cc: Andrew Morton, Jonathan Corbet, Randy Dunlap, Peter Zijlstra,
	Ingo Molnar, Steven Rostedt, Liam R . Howlett, Lorenzo Stoakes,
	Vlastimil Babka, David Hildenbrand, Mike Rapoport, Michal Hocko,
	Mel Gorman, Suren Baghdasaryan, Ben Segall, Dietmar Eggemann,
	Juri Lelli, K Prateek Nayak, Valentin Schneider, Vincent Guittot,
	Shuah Khan, Ketan Bhardwaj, bpf, linux-doc, linux-mm,
	HARIHARA SUDHAN . R

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: Ketan Bhardwaj <ketan@armoriq.io>
Signed-off-by: Ketan Bhardwaj <ketan@armoriq.io>
Signed-off-by: HARIHARA SUDHAN. R <harisudhan@armoriq.io>
---
 Documentation/ABI/testing/securityfs-kap | 111 ++++++++
 Documentation/security/index.rst         |   1 +
 Documentation/security/kap.rst           | 335 +++++++++++++++++++++++
 3 files changed, 447 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..e002a08
--- /dev/null
+++ b/Documentation/security/kap.rst
@@ -0,0 +1,335 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+======================
+Kernel Assurance Plane
+======================
+
+Kernel Assurance Plane (KAP) introduces an objective-scoped authority context as
+a kernel object. Intent Lineage Groups (igroups) are that object: an externally
+managed authority whose lifetime is tied to an objective rather than to a
+process, a thread group or a sandbox, that a trusted controller can update or
+revoke while unmodified worker tasks keep running.
+
+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. The central question is in `Authority lifecycle versus enforcement
+mechanism`_: whether the novel part below should be a small authority-lifecycle
+facility that composes with Landlock and existing LSM hooks for the actual
+mediation, rather than a self-contained LSM that carries its own.
+
+What this is and is not
+=======================
+
+The distinctive part of KAP is not path or syscall mediation. Landlock already
+restricts a task's access to the filesystem and to a few other operations, often
+more completely than the checks here, and seccomp already filters syscalls. KAP
+does not try to replace or improve on either.
+
+What Linux does not provide today is an authority context with these four
+properties together:
+
+* It is **externally managed**. A trusted controller, not the task itself,
+  installs and changes it. This is unlike Landlock and seccomp, where a task
+  restricts itself and cannot be restricted further from outside once running.
+* Its **lifetime is tied to an objective**, not to a process, a thread group or
+  a sandbox. Several objectives can run in one environment, each held to its own
+  authority.
+* It can be **updated or revoked in place** by the controller, and the change
+  takes effect at the next operation of an already running task, without that
+  task cooperating and without killing it.
+* It is **inherited** across fork, exec and setns, so the authority follows the
+  work rather than a particular executable or label.
+
+Everything else in this RFC, the set of checked operations and how each is
+matched, exists to make that authority context enforceable. Those checks overlap
+with Landlock and with existing hooks, and the open question below is whether to
+keep them in KAP or delegate them.
+
+A motivating example
+====================
+
+Two objectives, O1 and O2, run as separate worker tasks in one environment. Both
+start with authority to read ``/workspace`` and ``/package-cache`` and to reach
+the package registry. They are bound to two igroups.
+
+O1 finishes its first stage. The controller advances O1 to a new authority
+generation that is narrower: no package cache, no network. It writes the new
+authority and a higher epoch to O1's igroup. O2's igroup is untouched.
+
+From its next check, an already running O1 worker is held to the new authority.
+It cannot reach the cache or the network even though it could a moment ago, it
+was not asked to drop anything, it was not restarted, and O2 is unaffected. If
+instead the controller revokes O1, the next O1 check fails closed.
+
+This is the behaviour that a self-restriction model cannot express. Landlock and
+seccomp restrict the calling thread and its future children at the point the
+thread asks; they offer no way for an outside controller to narrow or revoke the
+authority of a task that is already running, per objective, while a second
+objective beside it is left alone. That per-objective, externally revocable
+lifecycle is what KAP adds.
+
+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. A deny wins over an allow, and an
+  operation that no predicate allows is refused. The snapshot is compiled once
+  when it is installed.
+
+  The snapshot is the model's authority representation; its on-the-wire form is
+  not. In this RFC that form is a document in a strict subset of JSON, used only
+  because it is easy to produce from the prototype runtime::
+
+    {"predicates":[{"kind":"file","allow":["/srv/work"],"deny":["/srv/work/keys"]},
+                   {"kind":"exec","allow":["/usr/bin/*"],"deny":[]}]}
+
+  The JSON subset is a prototype representation, not part of the model. Any
+  userspace policy format is translated to the snapshot in userspace, before
+  install. The kernel-facing questions are the representation's bounds, its
+  validation, and atomic install and update, not accommodating a configuration
+  language. A bounded, versioned binary form, or reuse of an existing ruleset
+  format such as Landlock's, is the intended direction and is question 4 under
+  `Questions for maintainers`_.
+
+  The predicate kinds, and the limits of each as currently implemented:
+
+  * ``exec``, ``file``: a path, matched against the path and everything below
+    it. File predicates also govern rename, link, unlink, mkdir, rmdir, mknod,
+    symlink and truncate, checked as the parent directory plus the name, so a
+    file cannot be moved into a denied path. A program is allowed by the path it
+    resolves to and refused by a deny on that path or on the name it was started
+    with. This is ordinary path mediation and overlaps Landlock.
+  * ``net``: connect() only, matched as ``address:port`` such as
+    ``192.0.2.1:443``, or ``family:N`` for a family other than IPv4 and IPv6. A
+    connect with ``AF_UNSPEC`` is not checked; ``AF_UNIX`` is checked as
+    ``ipc``. bind(), listen() and already connected or accepted sockets are not
+    covered. This is not a service-side network policy.
+  * ``ipc``: Unix domain checks are coarse: ``unix:stream_connect`` and
+    ``unix:may_send`` do not yet identify the endpoint. For a pathname socket
+    the resolved endpoint is the right target and is not yet used; an abstract
+    socket needs its own semantics. ``sysv:flag=N`` is only the flag passed to
+    ipc_permission() and does not identify the object, so this is not SysV IPC
+    confinement.
+  * ``descriptor``: a file_receive check on the resolved path of a file received
+    over a Unix socket. It is not a descriptor lifecycle model; using a
+    descriptor already held, and accessing a mapping already established, are
+    separate and are not covered by this kind.
+  * ``procfs``: path-based matching under ``/proc`` with the caller's own pid
+    normalised to ``/proc/self``. It is not a general procfs security model.
+  * ``execmem``: the path of the mapped file, or ``anonymous``.
+  * ``cred``: ``uid=U`` and the other id fields, ``groups`` or ``capset``.
+  * ``namespace``: ``TYPE:OPERATION`` for clone, unshare and setns. See
+    question 5 under `Questions for maintainers`_ on using existing hooks for
+    these instead.
+  * ``ptrace``: ``same-igroup``, ``igroup=NAME`` or ``unconfined``.
+  * ``bpf``: ``cmd=N``, with ``map_type=N`` or ``prog_type=N``.
+  * ``io_uring``: ``override_creds``, ``sqpoll`` and ``uring_cmd`` only. These
+    three checks are not a blanket disable of io_uring and do not carry the
+    submitting task's authority onto every asynchronous operation.
+
+``epoch``
+  Monotonic authority generation. Installing a higher epoch fails every task
+  still bound to an older one closed from its next check.
+
+``lineage root``
+  An opaque reference supplied by userspace that ties the installed authority to
+  a committed execution lineage. The kernel stores it and reports it in events;
+  it does not interpret or verify it in this version. Whether a refined snapshot
+  is a valid narrowing of its predecessor is a trusted-userspace responsibility,
+  not a kernel check.
+
+Deployment and privilege model
+==============================
+
+KAP is meant for a supervised agent runtime, not for an unprivileged application
+that installs its own sandbox. The task running under an igroup is not the
+authority for what that igroup may do; the fact that a user started the work is
+not itself a grant.
+
+A trusted, privileged runtime combines the task, its workspace, the tool
+contracts, credential bindings, administrator policy and any approvals into the
+compiled snapshot, installs it, binds the worker, and then runs the worker
+unprivileged. Writes to the control plane are authorised against the initial
+user namespace. The worker cannot widen its own authority; it can only have it
+narrowed or revoked from outside.
+
+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
+================
+
+Authority lifecycle versus enforcement mechanism
+------------------------------------------------
+
+This is the decision we most want feedback on, because it shapes the rest.
+
+The current series is a self-contained LSM: it holds the igroup authority
+lifecycle and also performs the operation mediation at existing LSM hooks. Most
+of that mediation, path access in particular, overlaps with Landlock and could
+be expressed with it.
+
+The alternative is to keep only the novel part in KAP, the externally managed,
+per-objective, revocable authority lifecycle with binding and inheritance, and
+to compose it with Landlock and existing hooks for the actual enforcement, so
+that an igroup's current generation drives a Landlock ruleset and other existing
+checks rather than a KAP-private predicate engine. That is a smaller and more
+upstreamable facility, but a larger change from this code.
+
+We implemented the self-contained form first because it is what runs today and
+makes the model concrete. We would rather present the authority-lifecycle
+facility that composes with existing enforcement if the subsystem agrees that is
+the right shape. Question 1 under `Questions for maintainers`_ asks exactly this.
+
+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. Linux can of course group tasks, including across
+setns; the point is not that it cannot. It is that we want the authority
+lifecycle to be independent of the resource hierarchy. A container usually
+places all of its tasks in one cgroup, while several objectives can run inside
+it, each needing its own authority and its own independent update and
+revocation. Tying authority to resource partitioning would prevent that.
+
+An authority-lifecycle object rather than BPF LSM programs
+----------------------------------------------------------
+
+Considered: carrying the model as BPF LSM programs.
+
+A BPF LSM can enforce at security hooks, and a BPF implementation could carry
+parts of this, the per-operation checks in particular. So the question is not
+which mechanism enforces, but which authority-lifecycle semantics should be a
+shared, in-tree contract: how an authority object is created, attached to a
+task, inherited across fork, exec and setns, updated to a new generation, and
+revoked, with those transitions taking effect for already running tasks. Those
+semantics are kernel object state that we think belongs in one auditable place
+rather than being re-implemented per deployment. Which mechanism then performs
+the mediation is the subject of the lifecycle-versus-enforcement decision above.
+
+The kernel enforces the snapshot, not a policy language
+-------------------------------------------------------
+
+Considered: parsing a policy language in the kernel.
+
+Chosen: the kernel accepts only a compiled predicate snapshot. From the
+kernel's point of view that installed snapshot is the policy. Objective
+interpretation, authorization decisions and validation of the upstream
+artifacts stay in privileged userspace; the kernel enforces the installed
+snapshot and the execution binding, and compiles the snapshot into per-kind
+pattern sets once at install time.
+
+Questions for maintainers
+=========================
+
+1. Should this be a minimal authority-lifecycle facility (bind and inheritance,
+   atomic generation update, revocation, with lifecycle kept separate from any
+   sandbox implementation) that composes with Landlock and existing hooks for
+   enforcement, rather than a self-contained LSM with its own mediation? See
+   `Authority lifecycle versus enforcement mechanism`_.
+2. Is an internal filesystem linked under securityfs, as AppArmor does,
+   acceptable for a control plane that needs userspace mkdir?
+3. Should igroup removal follow cgroups, with rmdir allowed once no task is
+   bound?
+4. The predicate snapshot's wire form is a JSON subset and experimental. Is a
+   bounded, versioned binary format preferred, or reuse of an existing ruleset
+   format such as Landlock's?
+5. For namespace transitions through unshare, clone and setns, this series calls
+   KAP directly. If the target tree has, or would accept, LSM hooks for those
+   transitions, we would use them instead of calling KAP directly. Which
+   existing or proposed hooks should we build on?
+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.

base-commit: 24a19b953e2a3d0ea5a8c4951a0ab8f32720c907
-- 
2.53.0


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

* [RFC PATCH v3 2/6] security: kap: add the LSM ID and internal headers
  2026-11-10 20:22 [RFC PATCH v3 0/6] security: introduce Kernel Assurance Plane HARIHARA SUDHAN. R
  2026-11-10 20:22 ` [RFC PATCH v3 1/6] docs: security: Describe the Kernel Assurance Plane design HARIHARA SUDHAN. R
@ 2026-11-10 20:22 ` HARIHARA SUDHAN. R
  2026-11-10 20:30   ` sashiko-bot
  2026-11-10 20:22 ` [RFC PATCH v3 3/6] security: add the Kernel Assurance Plane LSM HARIHARA SUDHAN. R
                   ` (3 subsequent siblings)
  5 siblings, 1 reply; 12+ messages in thread
From: HARIHARA SUDHAN. R @ 2026-11-10 20:22 UTC (permalink / raw)
  To: linux-kernel, linux-security-module, Paul Moore, Kees Cook,
	James Morris, Serge E . Hallyn
  Cc: Andrew Morton, Jonathan Corbet, Randy Dunlap, Peter Zijlstra,
	Ingo Molnar, Steven Rostedt, Liam R . Howlett, Lorenzo Stoakes,
	Vlastimil Babka, David Hildenbrand, Mike Rapoport, Michal Hocko,
	Mel Gorman, Suren Baghdasaryan, Ben Segall, Dietmar Eggemann,
	Juri Lelli, K Prateek Nayak, Valentin Schneider, Vincent Guittot,
	Shuah Khan, Ketan Bhardwaj, bpf, linux-doc, linux-mm,
	HARIHARA SUDHAN . R

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: Ketan Bhardwaj <ketan@armoriq.io>
Signed-off-by: Ketan Bhardwaj <ketan@armoriq.io>
Signed-off-by: HARIHARA SUDHAN. R <harisudhan@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.53.0


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

* [RFC PATCH v3 3/6] security: add the Kernel Assurance Plane LSM
  2026-11-10 20:22 [RFC PATCH v3 0/6] security: introduce Kernel Assurance Plane HARIHARA SUDHAN. R
  2026-11-10 20:22 ` [RFC PATCH v3 1/6] docs: security: Describe the Kernel Assurance Plane design HARIHARA SUDHAN. R
  2026-11-10 20:22 ` [RFC PATCH v3 2/6] security: kap: add the LSM ID and internal headers HARIHARA SUDHAN. R
@ 2026-11-10 20:22 ` HARIHARA SUDHAN. R
  2026-11-10 20:51   ` sashiko-bot
  2026-11-10 20:22 ` [RFC PATCH v3 4/6] security: kap: add the securityfs control plane HARIHARA SUDHAN. R
                   ` (2 subsequent siblings)
  5 siblings, 1 reply; 12+ messages in thread
From: HARIHARA SUDHAN. R @ 2026-11-10 20:22 UTC (permalink / raw)
  To: linux-kernel, linux-security-module, Paul Moore, Kees Cook,
	James Morris, Serge E . Hallyn
  Cc: Andrew Morton, Jonathan Corbet, Randy Dunlap, Peter Zijlstra,
	Ingo Molnar, Steven Rostedt, Liam R . Howlett, Lorenzo Stoakes,
	Vlastimil Babka, David Hildenbrand, Mike Rapoport, Michal Hocko,
	Mel Gorman, Suren Baghdasaryan, Ben Segall, Dietmar Eggemann,
	Juri Lelli, K Prateek Nayak, Valentin Schneider, Vincent Guittot,
	Shuah Khan, Ketan Bhardwaj, bpf, linux-doc, linux-mm,
	HARIHARA SUDHAN . R

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: Ketan Bhardwaj <ketan@armoriq.io>
Signed-off-by: Ketan Bhardwaj <ketan@armoriq.io>
Signed-off-by: HARIHARA SUDHAN. R <harisudhan@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.53.0


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

* [RFC PATCH v3 4/6] security: kap: add the securityfs control plane
  2026-11-10 20:22 [RFC PATCH v3 0/6] security: introduce Kernel Assurance Plane HARIHARA SUDHAN. R
                   ` (2 preceding siblings ...)
  2026-11-10 20:22 ` [RFC PATCH v3 3/6] security: add the Kernel Assurance Plane LSM HARIHARA SUDHAN. R
@ 2026-11-10 20:22 ` HARIHARA SUDHAN. R
  2026-11-10 20:47   ` sashiko-bot
  2026-11-10 20:22 ` [RFC PATCH v3 5/6] kernel: add KAP checks for unshare and setns HARIHARA SUDHAN. R
  2026-11-10 20:22 ` [RFC PATCH v3 6/6] MAINTAINERS: add an entry for the Kernel Assurance Plane HARIHARA SUDHAN. R
  5 siblings, 1 reply; 12+ messages in thread
From: HARIHARA SUDHAN. R @ 2026-11-10 20:22 UTC (permalink / raw)
  To: linux-kernel, linux-security-module, Paul Moore, Kees Cook,
	James Morris, Serge E . Hallyn
  Cc: Andrew Morton, Jonathan Corbet, Randy Dunlap, Peter Zijlstra,
	Ingo Molnar, Steven Rostedt, Liam R . Howlett, Lorenzo Stoakes,
	Vlastimil Babka, David Hildenbrand, Mike Rapoport, Michal Hocko,
	Mel Gorman, Suren Baghdasaryan, Ben Segall, Dietmar Eggemann,
	Juri Lelli, K Prateek Nayak, Valentin Schneider, Vincent Guittot,
	Shuah Khan, Ketan Bhardwaj, bpf, linux-doc, linux-mm,
	HARIHARA SUDHAN . R

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: Ketan Bhardwaj <ketan@armoriq.io>
Signed-off-by: Ketan Bhardwaj <ketan@armoriq.io>
Signed-off-by: HARIHARA SUDHAN. R <harisudhan@armoriq.io>
---
 security/kap/Makefile  |   2 +-
 security/kap/kap_lsm.c |   1 +
 security/kap/kapfs.c   | 522 +++++++++++++++++++++++++++++++++++++++++
 3 files changed, 524 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..b182ff2
--- /dev/null
+++ b/security/kap/kapfs.c
@@ -0,0 +1,522 @@
+// 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 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;
+	}
+
+	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.53.0


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

* [RFC PATCH v3 5/6] kernel: add KAP checks for unshare and setns
  2026-11-10 20:22 [RFC PATCH v3 0/6] security: introduce Kernel Assurance Plane HARIHARA SUDHAN. R
                   ` (3 preceding siblings ...)
  2026-11-10 20:22 ` [RFC PATCH v3 4/6] security: kap: add the securityfs control plane HARIHARA SUDHAN. R
@ 2026-11-10 20:22 ` HARIHARA SUDHAN. R
  2026-11-10 20:36   ` sashiko-bot
  2026-11-10 20:22 ` [RFC PATCH v3 6/6] MAINTAINERS: add an entry for the Kernel Assurance Plane HARIHARA SUDHAN. R
  5 siblings, 1 reply; 12+ messages in thread
From: HARIHARA SUDHAN. R @ 2026-11-10 20:22 UTC (permalink / raw)
  To: linux-kernel, linux-security-module, Paul Moore, Kees Cook,
	James Morris, Serge E . Hallyn
  Cc: Andrew Morton, Jonathan Corbet, Randy Dunlap, Peter Zijlstra,
	Ingo Molnar, Steven Rostedt, Liam R . Howlett, Lorenzo Stoakes,
	Vlastimil Babka, David Hildenbrand, Mike Rapoport, Michal Hocko,
	Mel Gorman, Suren Baghdasaryan, Ben Segall, Dietmar Eggemann,
	Juri Lelli, K Prateek Nayak, Valentin Schneider, Vincent Guittot,
	Shuah Khan, Ketan Bhardwaj, bpf, linux-doc, linux-mm,
	HARIHARA SUDHAN . R

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: Ketan Bhardwaj <ketan@armoriq.io>
Signed-off-by: Ketan Bhardwaj <ketan@armoriq.io>
Signed-off-by: HARIHARA SUDHAN. R <harisudhan@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.53.0


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

* [RFC PATCH v3 6/6] MAINTAINERS: add an entry for the Kernel Assurance Plane
  2026-11-10 20:22 [RFC PATCH v3 0/6] security: introduce Kernel Assurance Plane HARIHARA SUDHAN. R
                   ` (4 preceding siblings ...)
  2026-11-10 20:22 ` [RFC PATCH v3 5/6] kernel: add KAP checks for unshare and setns HARIHARA SUDHAN. R
@ 2026-11-10 20:22 ` HARIHARA SUDHAN. R
  5 siblings, 0 replies; 12+ messages in thread
From: HARIHARA SUDHAN. R @ 2026-11-10 20:22 UTC (permalink / raw)
  To: linux-kernel, linux-security-module, Paul Moore, Kees Cook,
	James Morris, Serge E . Hallyn
  Cc: Andrew Morton, Jonathan Corbet, Randy Dunlap, Peter Zijlstra,
	Ingo Molnar, Steven Rostedt, Liam R . Howlett, Lorenzo Stoakes,
	Vlastimil Babka, David Hildenbrand, Mike Rapoport, Michal Hocko,
	Mel Gorman, Suren Baghdasaryan, Ben Segall, Dietmar Eggemann,
	Juri Lelli, K Prateek Nayak, Valentin Schneider, Vincent Guittot,
	Shuah Khan, Ketan Bhardwaj, bpf, linux-doc, linux-mm,
	HARIHARA SUDHAN . R

Add the KAP maintainers and the files they look after.

Assisted-by: Claude
Co-developed-by: Ketan Bhardwaj <ketan@armoriq.io>
Signed-off-by: Ketan Bhardwaj <ketan@armoriq.io>
Signed-off-by: HARIHARA SUDHAN. R <harisudhan@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.53.0


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

* Re: [RFC PATCH v3 2/6] security: kap: add the LSM ID and internal headers
  2026-11-10 20:22 ` [RFC PATCH v3 2/6] security: kap: add the LSM ID and internal headers HARIHARA SUDHAN. R
@ 2026-11-10 20:30   ` sashiko-bot
  0 siblings, 0 replies; 12+ messages in thread
From: sashiko-bot @ 2026-11-10 20:30 UTC (permalink / raw)
  To: 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: Ketan Bhardwaj <ketan@armoriq.io>
> Signed-off-by: Ketan Bhardwaj <ketan@armoriq.io>
> Signed-off-by: HARIHARA SUDHAN. R <harisudhan@armoriq.io>

Sashiko has reviewed this patch and found no issues.

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261110202246.274-1-harisudhan@armoriq.io?part=2


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

* Re: [RFC PATCH v3 1/6] docs: security: Describe the Kernel Assurance Plane design
  2026-11-10 20:22 ` [RFC PATCH v3 1/6] docs: security: Describe the Kernel Assurance Plane design HARIHARA SUDHAN. R
@ 2026-11-10 20:33   ` sashiko-bot
  0 siblings, 0 replies; 12+ messages in thread
From: sashiko-bot @ 2026-11-10 20:33 UTC (permalink / raw)
  To: HARIHARA SUDHAN. R, linux-security-module, Paul Moore, Kees Cook,
	James Morris, Serge E . Hallyn
  Cc: Andrew Morton, Jonathan Corbet, Randy Dunlap, Peter Zijlstra,
	Ingo Molnar, Steven Rostedt, Liam R . Howlett, Lorenzo Stoakes,
	Vlastimil Babka, David Hildenbrand, Mike Rapoport, Michal Hocko,
	Mel Gorman, Suren Baghdasaryan, Ben Segall, Dietmar Eggemann,
	Juri Lelli, K Prateek Nayak, Valentin Schneider, Vincent Guittot,
	Shuah Khan, Ketan Bhardwaj, bpf, linux-mm

Thank you for your contribution! Sashiko AI review found 1 potential
issue to consider:
- [Low] docs: security: kap: contradictory epoch semantics for bound
  tasks
--

commit 77da64268c0c9921ecf4f2c4a0222c86b4687cb2
Author: HARIHARA SUDHAN. R <harisudhan@armoriq.io>

docs: security: Describe the Kernel Assurance Plane design

This commit adds security documentation and securityfs ABI documentation
describing the Kernel Assurance Plane (KAP) design, Intent Lineage Groups,
and control plane files.

> diff --git a/Documentation/security/kap.rst b/Documentation/security/kap.rst
> new file mode 100644
> index 0000000000000..e002a081c6607
> --- /dev/null
> +++ b/Documentation/security/kap.rst

[ ... ]

> +O1 finishes its first stage. The controller advances O1 to a new authority
> +generation that is narrower: no package cache, no network. It writes the new
> +authority and a higher epoch to O1's igroup. O2's igroup is untouched.
> +
> +From its next check, an already running O1 worker is held to the new authority.
> +It cannot reach the cache or the network even though it could a moment ago, it
> +was not asked to drop anything, it was not restarted, and O2 is unaffected. If
> +instead the controller revokes O1, the next O1 check fails closed.

[ ... ]

> +``epoch``
> +  Monotonic authority generation. Installing a higher epoch fails every task
> +  still bound to an older one closed from its next check.

[Severity: Low]
Does advancing an igroup's authority_epoch hold already-bound tasks to the
new authority, or does it cause them to fail every check closed?

The motivating example above states that writing a new authority and a
higher epoch to O1's igroup holds an already-running O1 worker to the
narrower authority, whereas the epoch definition here and the
kap.authority_epoch entry in Documentation/ABI/testing/securityfs-kap say
that installing a higher epoch causes tasks bound to an older epoch to
fail KAP checks closed.

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261110202246.274-1-harisudhan@armoriq.io?part=1

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

* Re: [RFC PATCH v3 5/6] kernel: add KAP checks for unshare and setns
  2026-11-10 20:22 ` [RFC PATCH v3 5/6] kernel: add KAP checks for unshare and setns HARIHARA SUDHAN. R
@ 2026-11-10 20:36   ` sashiko-bot
  0 siblings, 0 replies; 12+ messages in thread
From: sashiko-bot @ 2026-11-10 20:36 UTC (permalink / raw)
  To: HARIHARA SUDHAN. R, linux-security-module, Paul Moore, Kees Cook,
	James Morris, Serge E . Hallyn
  Cc: Andrew Morton, Jonathan Corbet, Randy Dunlap, Peter Zijlstra,
	Ingo Molnar, Steven Rostedt, Liam R . Howlett, Lorenzo Stoakes,
	Vlastimil Babka, David Hildenbrand, Mike Rapoport, Michal Hocko,
	Mel Gorman, Suren Baghdasaryan, Ben Segall, Dietmar Eggemann,
	Juri Lelli, K Prateek Nayak, Valentin Schneider, Vincent Guittot,
	Shuah Khan, Ketan Bhardwaj, bpf, linux-mm

Thank you for your contribution! Sashiko AI review found 1 potential
issue to consider:
- [High] security: kap: CLONE_NEWUSER skip in
  kap_check_namespace_flags() breaks user:unshare rules
--

commit 9adc8e1a3a53f48cafa9cf1ec8d4bd24d0a65150
Author: HARIHARA SUDHAN. R <harisudhan@armoriq.io>

kernel: add KAP checks for unshare and setns

This commit adds KAP namespace flag checks to ksys_unshare() and the setns
syscall.

> diff --git a/kernel/fork.c b/kernel/fork.c
> index 33760ece3bb30..3dda9f211ceae 100644
> --- a/kernel/fork.c
> +++ b/kernel/fork.c
[ ... ]
> @@ -3286,6 +3287,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;
>  	/*

[Severity: High]
Does kap_check_namespace_flags() fail to enforce "user:unshare" and "user:*"
predicates when unshare() is called with CLONE_NEWUSER?

security/kap/kap_lsm.c:kap_check_namespace_flags() {
	...
		if (kap_namespaces[i].flag == CLONE_NEWUSER &&
		    strcmp(operation, "setns"))
			continue;
	...
}

Because kap_check_namespace_flags() skips CLONE_NEWUSER for "unshare", the
check is deferred to kap_userns_create(), which passes the bare target string
"user" to kap_check_static_target().  In kap_string_match(), "user" does not
match "user:unshare" or "user:*", so allow rules using the documented
TYPE:OPERATION syntax reject unshare(CLONE_NEWUSER) with -EACCES, while deny
rules for "user:unshare" or "user:*" paired with a broad allow rule such as
"*" are bypassed.

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261110202246.274-1-harisudhan@armoriq.io?part=5

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

* Re: [RFC PATCH v3 4/6] security: kap: add the securityfs control plane
  2026-11-10 20:22 ` [RFC PATCH v3 4/6] security: kap: add the securityfs control plane HARIHARA SUDHAN. R
@ 2026-11-10 20:47   ` sashiko-bot
  0 siblings, 0 replies; 12+ messages in thread
From: sashiko-bot @ 2026-11-10 20:47 UTC (permalink / raw)
  To: HARIHARA SUDHAN. R, linux-security-module, Paul Moore, Kees Cook,
	James Morris, Serge E . Hallyn
  Cc: Andrew Morton, Jonathan Corbet, Randy Dunlap, Peter Zijlstra,
	Ingo Molnar, Steven Rostedt, Liam R . Howlett, Lorenzo Stoakes,
	Vlastimil Babka, David Hildenbrand, Mike Rapoport, Michal Hocko,
	Mel Gorman, Suren Baghdasaryan, Ben Segall, Dietmar Eggemann,
	Juri Lelli, K Prateek Nayak, Valentin Schneider, Vincent Guittot,
	Shuah Khan, Ketan Bhardwaj, bpf, linux-mm

Thank you for your contribution! Sashiko AI review found 9 potential
issues to consider:
- [Medium] security: kap: dentry reference leak on child files in
  kapfs_mkdir()
- [Medium] security: kap: unattached kapfs_mnt resolves as root-relative
  in d_path()
- [Medium] security: kap: missing superblock ops and blocksize in
  kapfs_fill_super()
- [Medium] security: kap: data race on kap_task_state in
  kap_bind_pid_locked()
- [Medium] security: kap: race between kap_bind_pid_locked() and
  copy_process()
- [Medium] security: kap: kap_authority_prepare() bounds predicates by
  next kind not '}'
- [Low] security: kap: unsanitized lineage_root in kapfs_write()
  corrupts output
- [Low] security: kap: kapfs_write() silently succeeds on newline-only
  writes
- [Low] security: kap: unchecked u64 overflow in
  kap_parse_transition_sequence()
--

commit 9864f112a0785ec3a3604d782064cf536e36889c
Author: HARIHARA SUDHAN. R <harisudhan@armoriq.io>

security: kap: add the securityfs control plane

This commit adds the KAP securityfs control plane at /sys/kernel/security/kap
and mounts an internal kapfs filesystem for managing igroups and their control
files.

> diff --git a/security/kap/kapfs.c b/security/kap/kapfs.c
> new file mode 100644
> index 0000000000000..b182ff24bb8c4
> --- /dev/null
> +++ b/security/kap/kapfs.c
[ ... ]
> +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: Low]
Should writing a newline-only string to a control file really succeed without
doing anything?

If a script writes an empty line (for example, echo "$PID" > kap.procs when
PID is unset), kapfs_write() returns len without validating the input or
updating any state, and this early return also prevents clearing
kap.lineage_root via a single-file write even though kap.install accepts an
empty lineage_root=.

> +
> +	/* 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;

[Severity: Low]
Can an unsanitized lineage_root corrupt kap.igfd and kap.events output?

KAP_FILE_LINEAGE_ROOT and kap_install_parse() only check the length of the
string, so embedded newlines break the single-line format of kap.igfd in
kapfs_show(), and quotes or backslashes are interpolated unescaped into the
"root" JSON field by kap_append_event_locked():

security/kap/igroup.c:kap_append_event_locked() {
    ...
	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));
    ...
}

> +	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]
Can kap_authority_prepare() bleed one predicate's allow or deny array into
the preceding predicate?

Both kap_validate_authority_payload() and kap_compile_kind_array() scan
backward from "kind": to '{' to find predicate_start, so keys appearing before
"kind" inside a predicate object are supported, but they set predicate_end to
the next "kind": rather than the closing '}' of the current predicate:

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 one predicate omits "allow" or "deny" and the next predicate places its
"allow":[...] or "deny":[...] before its "kind": key, the slice
[predicate_start, predicate_end) spans across the boundary between the two
objects, causing validation to accept the malformed document and compiling the
second predicate's patterns into the first predicate's hook kind.

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

[Severity: Low]
Can kap_parse_transition_sequence() silently wrap on u64 overflow?

Unlike authority_epoch and expires_at, which use kstrtou64(),
kap_parse_transition_sequence() accumulates decimal digits without checking
for overflow:

security/kap/igroup.c:kap_parse_transition_sequence() {
    ...
	while (*cursor >= '0' && *cursor <= '9') {
		seen_digit = true;
		value = value * 10 + (*cursor - '0');
		cursor++;
	}
    ...
}

Writing a sequence number of 20 or more digits to kap.transition or
kap.install wraps modulo 2^64, which can corrupt igroup->last_sequence if the
wrapped value exceeds the previous sequence number.

[ ... ]

> +	spin_lock(&kap_lock);
> +	switch (ctx->kind) {
> +	case KAP_FILE_PROCS:
> +		rc = kap_bind_pid_locked(igroup, pid, &entry);
> +		break;

[Severity: Medium]
Is there a data race on struct kap_task_state between kap_bind_pid_locked()
and kap_ptrace_target()?

kap_bind_pid_locked() sets state->active = true before populating state->epoch
and state->igroup_name, while kap_ptrace_target() reads those fields without
holding kap_lock:

CPU0                              CPU1
kapfs_write()
  spin_lock(&kap_lock)
  kap_bind_pid_locked()
    state->active = true
                                  kap_ptrace_access_check()
                                    kap_ptrace_target()
                                      if (child_state->active)
                                        strcmp(..., child_state->igroup_name)
                                          <-- reads empty or torn igroup_name
    state->epoch = igroup->epoch
    strscpy(state->igroup_name, ...)

A concurrent ptrace check can therefore misclassify same-igroup vs
cross-igroup ptrace operations and log a corrupted target in kap.events.

[Severity: Medium]
Can a thread or child being created in copy_process() escape binding when
kap_bind_pid_locked() runs concurrently?

kap_task_alloc() copies the parent's kap_task_state early in copy_process(),
long before the new task is linked onto signal->thread_head under siglock:

CPU0                              CPU1
copy_process()
  security_task_alloc()
    kap_task_alloc()
      spin_lock(&kap_lock)
      /* old->active is false */
      spin_unlock(&kap_lock)
                                  kapfs_write()
                                    spin_lock(&kap_lock)
                                    kap_bind_pid_locked()
                                      for_each_thread(task, t)
                                        state->active = true
                                    spin_unlock(&kap_lock)
  write_lock_irq(&tasklist_lock)
  list_add_tail_rcu(&p->thread_node,
                    &p->signal->thread_head)
    <-- p runs with state->active == false

The new thread or child process is missed by both kap_task_alloc() and
for_each_thread(), leaving it running unbound even though writing the process
PID to kap.procs succeeded.

[ ... ]

> +static void kapfs_instantiate(struct dentry *dentry, struct inode *inode)
> +{
> +	d_make_persistent(dentry, inode);
> +}
[ ... ]
> +/* 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]);
[ ... ]
> +	}
[ ... ]
> +	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 entry in files[]?

d_alloc_name() returns each child dentry with a refcount of 1, and
kapfs_instantiate() -> d_make_persistent() takes an additional reference via
dget_dlock().  Unlike the directory dentry argument whose lookup reference is
dropped by the VFS caller, files[i] is local to kapfs_mkdir() and needs
dput(files[i]) after kapfs_instantiate().

[ ... ]

> +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() initialize sb->s_op, sb->s_blocksize, and
sb->s_blocksize_bits?

Without sb->s_op = &simple_super_operations, statfs() and fstatfs() on
/sys/kernel/security/kap/igroups and its entries fail with -ENOSYS, and
leaving sb->s_blocksize and sb->s_blocksize_bits at zero causes stat() to
report st_blksize = 1.

[ ... ]

> +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: Medium]
Doesn't jumping onto the unattached kapfs_mnt cause d_path() in KAP's LSM
hooks to resolve paths under /sys/kernel/security/kap/igroups as
/<igroup>/<file>?

kapfs_mnt is created with kern_mount() and is not attached to any mountpoint,
so when kap_file_target(), kap_lsm_file_path_alloc(), or
kap_lsm_check_dir_entry() calls d_path(), walk-back stops at
kapfs_mnt->mnt_root and omits the /sys/kernel/security/kap/igroups prefix:

security/kap/kap_lsm.c:kap_lsm_file_path_alloc() {
    ...
	path = d_path(&file->f_path, buf, PATH_MAX);
	if (IS_ERR(path)) {
		kfree(buf);
		return NULL;
	}
    ...
}

File rules targeting /sys or /sys/kernel/security/kap will therefore not match
accesses under igroups/, and kap.events will log root-relative target paths.

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261110202246.274-1-harisudhan@armoriq.io?part=4

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

* Re: [RFC PATCH v3 3/6] security: add the Kernel Assurance Plane LSM
  2026-11-10 20:22 ` [RFC PATCH v3 3/6] security: add the Kernel Assurance Plane LSM HARIHARA SUDHAN. R
@ 2026-11-10 20:51   ` sashiko-bot
  0 siblings, 0 replies; 12+ messages in thread
From: sashiko-bot @ 2026-11-10 20:51 UTC (permalink / raw)
  To: HARIHARA SUDHAN. R, linux-security-module, Paul Moore, Kees Cook,
	James Morris, Serge E . Hallyn
  Cc: Andrew Morton, Jonathan Corbet, Randy Dunlap, Peter Zijlstra,
	Ingo Molnar, Steven Rostedt, Liam R . Howlett, Lorenzo Stoakes,
	Vlastimil Babka, David Hildenbrand, Mike Rapoport, Michal Hocko,
	Mel Gorman, Suren Baghdasaryan, Ben Segall, Dietmar Eggemann,
	Juri Lelli, K Prateek Nayak, Valentin Schneider, Vincent Guittot,
	Shuah Khan, Ketan Bhardwaj, bpf, linux-mm

Thank you for your contribution! Sashiko AI review found 22 potential
issues to consider:
- [Critical] security: kap: unpinned d_parent use-after-free in
  kap_path_link()
- [High] security: kap: missing addrlen check in kap_socket_connect()
- [High] security: kap: KAP_HOOK_EXEC bypass via strstr() in
  kap_path_is_memfd()
- [High] security: kap: fail-open and skipped file checks in
  kap_mmap_file()
- [High] security: kap: deny rule bypass via d_path() in
  kap_file_prefix_match()
- [High] security: kap: kthread crash and clone race in
  kap_bind_pid_locked()
- [High] security: kap: wrong PID and namespace in
  kap_normalize_procfs_self()
- [High] security: kap: kap_ptrace_traceme() checks tracee instead of
  tracer
- [High] security: kap: fork race in kap_task_alloc() and
  kap_bind_pid_locked()
- [Medium] security: kap: missing kap entry in default CONFIG_LSM lists
  in Kconfig
- [Medium] security: kap: predicate boundary misparsing in
  kap_compile_kind_array()
- [Medium] security: kap: unlocked kap_task_state reads in
  kap_ptrace_target()
- [Medium] security: kap: path prefix and wildcard mismatch in
  kap_target_match()
- [Medium] security: kap: path truncation and fail-open fallback in
  kap_lsm_path_target()
- [Medium] security: kap: leak of kap_bound_pid entries on igroup->pids
  on task exit
- [Medium] security: kap: unlocked kap_task_state access in
  kap_ptrace_target()
- [Medium] security: kap: userns target mismatch in kap_userns_create()
- [Medium] kap: missing file_truncate hook and error fallthrough in
  kap_file_open()
- [Low] security: kap: missing static qualifier on kap_blob_sizes in
  kap_lsm.c
- [Low] security: kap: u64 overflow in kap_parse_transition_sequence()
- [Low] security: kap: log injection and event slicing in
  kap_append_event_locked()
- [Low] security: kap: kap_lock drop between check and event in
  kap_file_permission()
--

commit 0e8ce0130aad17cba4372a8036b850cd9a71a976
Author: HARIHARA SUDHAN. R <harisudhan@armoriq.io>

security: add the Kernel Assurance Plane LSM

This commit adds the Kernel Assurance Plane (KAP) LSM, implementing Intent
Lineage Groups (igroups), authority compilation and decision logic, and the
LSM hooks that enforce policy against 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]
Should config LSM in security/Kconfig also include kap in its default strings?

DEFINE_LSM(kap) leaves .order at 0 (LSM_ORDER_MUTABLE), which lsm_order_parse()
only enables when the LSM name is listed in CONFIG_LSM or passed via the lsm=
boot parameter.  Without kap in the config LSM defaults, enabling
CONFIG_SECURITY_KAP=y leaves KAP disabled at boot unless CONFIG_LSM or the
kernel command line is manually overridden.

> 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
[ ... ]
> +	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;
> +		}

[Severity: High]
Can this read uninitialized kernel stack memory when a bound task calls
connect(2) with a short addrlen?

In __sys_connect(), move_addr_to_kernel() copies only addrlen bytes into an
uninitialized struct sockaddr_storage on the stack before calling
security_socket_connect().  kap_socket_connect() only checks that addrlen
covers sa_family (2 bytes) and does not pass addrlen here.  When sa_family is
AF_INET or AF_INET6 and addrlen is smaller than sizeof(struct sockaddr_in) or
sizeof(struct sockaddr_in6), formatting sin_addr/sin_port or
sin6_addr/sin6_port reads uninitialized stack bytes, evaluates the net
predicate against garbage, and leaks those bytes through kap.events and dmesg.

> +		/* Other families, vsock among them, are named so a grant can allow them. */
> +		scnprintf(target, target_len, "family:%u", addr->sa_family);
> +		return 0;
> +	}
[ ... ]
> +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]
Can a bound task bypass a file or exec deny rule by unlinking the target file
before accessing or executing its open file descriptor?

kap_file_target() and kap_lsm_file_path_alloc() resolve paths with d_path(),
which appends " (deleted)" when the dentry has been unlinked.  For a policy
that allows "/srv/work" and denies "/srv/work/keys", an unlinked file resolves
to "/srv/work/keys (deleted)": kap_file_prefix_match() rejects the deny
pattern because the character after "/srv/work/keys" is ' ' rather than '/' or
NUL, but still matches the parent directory allow pattern "/srv/work".
Also, because d_path() resolves paths relative to current->fs->root and
path_chroot is not hooked, a bound task that can call chroot(2) shifts all
d_path()-resolved paths relative to the new root.

> +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);
> +}

[Severity: Medium]
Should KAP_HOOK_EXEC also use kap_file_prefix_match(), and should
kap_file_prefix_match() handle trailing '*' wildcards?

Routing KAP_HOOK_EXEC to kap_string_match() means a directory pattern such as
"/usr/bin" fails to match "/usr/bin/id", while a wildcard pattern such as
"/usr/bin*" matches sibling paths like "/usr/bin_untrusted/app" without a '/'
boundary.  Conversely, in kap_file_prefix_match(), a trailing '*' in a file,
descriptor, or procfs pattern is treated as a literal '*' character rather
than a wildcard.

[ ... ]
> +/* 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;
> +}

[Severity: Medium]
Can kap_compile_kind_array() and kap_validate_authority_payload() misparse
predicate boundaries when "allow" or "deny" precedes "kind" in a JSON
predicate object?

predicate_end is set to the next "\"kind\":" token rather than the closing '}'
of the current predicate object, so [predicate_start, predicate_end) extends
across "},{" into the next predicate up to its "\"kind\":" key.  If a
serializer emits keys in alphabetical order ("allow", "deny", "kind") and a
pattern before "kind" contains '{' (such as "/srv/{secret}"), the backward
scan for '{' stops inside that pattern string, drops the current predicate's
earlier arrays, and compiles the next predicate's pre-"kind" arrays into this
predicate's hook kind.  In addition, kap_compile_array() scans for '"' without
skipping escaped quotes (\" inside pattern strings).

[ ... ]
> +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;
> +}

[Severity: Low]
Does kap_refuse_unbound() log target without escaping newlines or control
characters, and does kap_append_event_locked() slice JSON lines on eviction?

Unlike kap_append_event_locked(), kap_refuse_unbound() passes target directly
to pr_warn_ratelimited(), so an unbound task in a non-initial PID namespace
executing a path containing '\n' can inject forged lines into dmesg.  In
kap_append_event_locked(), when igroup->events_len + len >=
KAP_MAX_EVENTS_BYTES, memmove() retains the last KAP_MAX_EVENTS_BYTES / 2
bytes without advancing to the next '\n', leaving a truncated JSON fragment at
the start of kap.events.

[ ... ]
> +int kap_parse_transition_sequence(const char *payload, u64 *sequence)
> +{
[ ... ]
> +	while (*cursor >= '0' && *cursor <= '9') {
> +		seen_digit = true;
> +		value = value * 10 + (*cursor - '0');
> +		cursor++;
> +	}

[Severity: Low]
Can value = value * 10 + (*cursor - '0') overflow u64 when parsing sequence
numbers?

A decimal sequence string >= 2^64 wraps around modulo 2^64 without returning
an error, which can bypass or corrupt the sequence monotonicity check against
igroup->last_sequence 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();

[Severity: High]
What happens if kap_bind_pid_locked() is given the PID of a kernel thread?

pid_task() does not reject tasks with PF_KTHREAD set, which have current->fs
== NULL.  If a kthread is bound, the next KAP hook that calls d_path()
dereferences current->fs in get_fs_root_rcu() and crashes with a NULL pointer
dereference.

> +	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]
Are struct kap_bound_pid entries on igroup->pids leaked when bound processes
exit or are rebound?

kap_bind_pid_locked() appends *entry to igroup->pids, but kap_task_free() only
zeroes struct kap_task_state without removing the exiting task's entry from
igroup->pids, and rebinding a task to another igroup never removes it from the
previous igroup's list.  As a result, igroup->pids accumulates leaked entries
and stale or recycled PIDs for the lifetime of the igroup.

> 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
[ ... ]
> +struct lsm_blob_sizes kap_blob_sizes __ro_after_init = {
> +	.lbs_task = sizeof(struct kap_task_state),
> +};

[Severity: Low]
Should kap_blob_sizes be declared static?  It is only used in
security/kap/kap_lsm.c and has no declaration in security/kap/kap.h, which
triggers a -Wmissing-declarations warning under W=1.

[ ... ]
> +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 concurrently cloning thread miss an igroup binding in
kap_bind_pid_locked()?

In copy_process(), security_task_alloc() runs before the new task is linked
onto signal->thread_head under siglock:

CPU0                              CPU1
copy_process()
  security_task_alloc()
    kap_task_alloc()
      spin_lock(&kap_lock)
      /* old->active is false */
      spin_unlock(&kap_lock)
                                  kap_bind_pid_locked()
                                    spin_lock(&kap_lock)
                                    for_each_thread(task, t)
                                      /* new task not on thread_head yet */
                                    spin_unlock(&kap_lock)
  write_lock_irq(&tasklist_lock)
  list_add_tail_rcu(&p->thread_node,
                    &p->signal->thread_head)  <-- runs unbound

The new thread finishes clone() without the binding even after
kap_bind_pid_locked() returns success.  Also, kap_task_alloc() unconditionally
takes kap_lock on every fork() even when kap_ready is false and 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");
> +}

[Severity: High]
Can a bound task bypass KAP_HOOK_EXEC predicates by executing a file whose
path contains the substring "memfd:"?

kap_path_is_memfd() checks strstr(path, "/memfd:") || strstr(path, "memfd:"),
which matches any regular file or directory on disk containing "memfd:" in its
path (for example, "/tmp/memfd:payload").  When kap_path_is_memfd() returns
true, kap_bprm_check_security() checks only KAP_HOOK_EXECMEM and returns
without checking KAP_HOOK_EXEC, so any bound task whose igroup allows
KAP_HOOK_EXECMEM (such as "*" for JIT) can execute arbitrary binaries renamed
to include "memfd:".

[ ... ]
> +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;
> +}

[Severity: Medium]
Can long or unresolvable mount paths bypass deny rules in kap_sb_mount(),
kap_sb_umount(), kap_sb_pivotroot(), and kap_move_mount()?

kap_lsm_path_target() ignores the -E2BIG return from strscpy() when rendered
exceeds KAP_LSM_TARGET_LEN (256 bytes), and the mount hooks substitute
"unknown" when kap_lsm_path_target() fails instead of failing closed.  A
bound task in a mount namespace can use a mount point path >= 256 bytes so the
truncated target no longer matches a specific deny prefix while still matching
a broader allow rule.

[ ... ]
> +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]
Does kap_normalize_procfs_self() match the wrong PID when called from a
non-leader thread or inside a child PID namespace?

/proc/self resolves to the thread-group ID in the PID namespace of the procfs
superblock (task_tgid_nr_ns(current, proc_pid_ns(inode->i_sb))), whereas
task_pid_nr(current) returns the calling thread's TID in init_pid_ns.  For any
secondary thread (tid != tgid) or any task in a child PID namespace with its
own /proc mount, /proc/self accesses are not normalized to "/proc/self",
breaking /proc/self allow rules and bypassing /proc/self deny rules when a
broader /proc allow rule is present.  In a child PID namespace, if another
process has a container PID equal to current's init_pid_ns TID, its /proc/<pid>
entries are also falsely rewritten to /proc/self.

[ ... ]
> +static int kap_file_permission(struct file *file, int mask)
> +{
[ ... ]
> +	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;
> +}

[Severity: Low]
Can dropping kap_lock between kap_check_current_target_why() and
kap_emit_current_event_target() log the wrong epoch or lineage_root?

kap_check_current_target_why() releases kap_lock before returning, and
kap_emit_current_event_target() reacquires it to format the denial event.  If
an install or rebind happens in between, the event is recorded with the new
epoch and lineage_root rather than the state that caused the denial.

> +
> +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;
> +}

[Severity: High]
Why do kap_mmap_file() and kap_file_mprotect() return 0 when
kap_lsm_file_path_alloc() fails or when mapping a non-memfd file with
PROT_EXEC?

If kap_lsm_file_path_alloc() fails (for example with -ENOMEM or
-ENAMETOOLONG), returning 0 fails open and allows the executable mapping even
when the igroup is revoked or expired.  When target is a regular file rather
than a memfd, both hooks also skip KAP_HOOK_EXECMEM completely and return 0,
leaving the KAP_HOOK_EXECMEM file case in kap_object_target() unused.

[ ... ]
> +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;
> +}

[Severity: Medium]
Does deferring CLONE_NEWUSER to kap_userns_create() break policies that use
"user:clone", "user:unshare", or "user:*"?

kap_check_namespace_flags() skips CLONE_NEWUSER for clone and unshare so that
kap_userns_create() checks it, but kap_userns_create() passes the bare target
string "user" instead of the "TYPE:OPERATION" format used by all other
namespace checks.  In kap_string_match(), "user:clone", "user:unshare", and
"user:*" do not match "user", so user namespace creation is denied under those
predicates.

[ ... ]
> +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: Critical]
Can reading old_dentry->d_parent without dget_parent() cause a use-after-free
in d_path()?

In filename_linkat(), only new_dir's inode is locked; old_dentry's parent is
neither locked nor pinned by a reference.  kap_lsm_check_dir_entry() performs
two GFP_KERNEL allocations that can sleep before passing &old_path to
d_path():

CPU0                              CPU1
kap_path_link()
  old_path.dentry = old_dentry->d_parent
  kap_lsm_check_dir_entry(&old_path, ...)
    kmalloc(PATH_MAX, GFP_KERNEL)
                                  rename(old_dentry, ...)
                                  rmdir(former_parent)
                                    /* former_parent dentry freed */
    d_path(&old_path, ...)        <-- use-after-free on old_path.dentry

Taking a reference with dget_parent(old_dentry) and dropping it with dput()
after kap_lsm_check_dir_entry() would keep the parent dentry alive across the
allocations.

[ ... ]
> +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: Medium]
Can kap_ptrace_target() observe a partially written kap_task_state when racing
with kap_bind_pid_locked()?

kap_bind_pid_locked() sets state->active = true before writing state->epoch
and state->igroup_name under kap_lock, while kap_ptrace_target() reads
active, igroup_name, and epoch from both tasks without holding kap_lock:

CPU0                              CPU1
kap_bind_pid_locked()
  spin_lock(&kap_lock)
  state->active = true
                                  kap_ptrace_access_check()
                                    kap_ptrace_target()
                                      if (child_state->active)
                                        scnprintf(..., "igroup=%s",
                                                  child_state->igroup_name)
                                                  <-- empty or torn name
  state->epoch = igroup->epoch
  strscpy(state->igroup_name, ...)
  spin_unlock(&kap_lock)

Because kap_ptrace_target() runs without kap_lock, it can format "igroup="
with an empty or torn igroup_name or compare a mismatched (igroup_name, epoch)
pair.

[ ... ]
> +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");
> +}

[Severity: High]
Does kap_ptrace_traceme() check the tracee's authority instead of the tracer's?

In ptrace_traceme(), parent is the tracer and current is the tracee.  By
checking kap_lsm_current_active() and kap_check_current_target_emit(),
kap_ptrace_traceme() evaluates current's igroup policy against parent.  A
bound parent whose igroup denies ptrace can therefore trace an unbound child
(or a child in an igroup that allows ptrace) if the child calls
PTRACE_TRACEME, while an unconfined parent is blocked from tracing a bound
child via PTRACE_TRACEME.

[ ... ]
> +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),

[Severity: Medium]
Should kap_hooks[] also hook file_truncate, and should kap_file_open() and
kap_bprm_check_security() fail closed when kap_lsm_file_path_alloc() returns
an error?

do_ftruncate() calls security_file_truncate(file) and does not call
security_path_truncate() or security_file_permission(), so a bound task whose
authority is later narrowed, expired, or revoked can still empty an
already-open writable file descriptor via ftruncate(2).  In addition, when
kap_lsm_file_path_alloc() returns an ERR_PTR in kap_file_open() or
kap_bprm_check_security(), execution falls through to KAP_HOOK_FILE or
KAP_HOOK_EXEC instead of failing closed, evaluating /proc opens against
KAP_HOOK_FILE (without /proc/self normalization) and memfd execs against
KAP_HOOK_EXEC if the second allocation succeeds.

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261110202246.274-1-harisudhan@armoriq.io?part=3

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

end of thread, other threads:[~2026-11-10 20:51 UTC | newest]

Thread overview: 12+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-10 20:22 [RFC PATCH v3 0/6] security: introduce Kernel Assurance Plane HARIHARA SUDHAN. R
2026-11-10 20:22 ` [RFC PATCH v3 1/6] docs: security: Describe the Kernel Assurance Plane design HARIHARA SUDHAN. R
2026-11-10 20:33   ` sashiko-bot
2026-11-10 20:22 ` [RFC PATCH v3 2/6] security: kap: add the LSM ID and internal headers HARIHARA SUDHAN. R
2026-11-10 20:30   ` sashiko-bot
2026-11-10 20:22 ` [RFC PATCH v3 3/6] security: add the Kernel Assurance Plane LSM HARIHARA SUDHAN. R
2026-11-10 20:51   ` sashiko-bot
2026-11-10 20:22 ` [RFC PATCH v3 4/6] security: kap: add the securityfs control plane HARIHARA SUDHAN. R
2026-11-10 20:47   ` sashiko-bot
2026-11-10 20:22 ` [RFC PATCH v3 5/6] kernel: add KAP checks for unshare and setns HARIHARA SUDHAN. R
2026-11-10 20:36   ` sashiko-bot
2026-11-10 20:22 ` [RFC PATCH v3 6/6] MAINTAINERS: add an entry for the Kernel Assurance Plane HARIHARA SUDHAN. R

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