* [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 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