From: sashiko-bot@kernel.org
Cc: linux-security-module@vger.kernel.org
Subject: Re: [RFC PATCH v2 4/6] security: kap: add the securityfs control plane
Date: Wed, 28 Oct 2026 13:57:44 +0000 [thread overview]
Message-ID: <sashiko-outbox-187950@kernel.org> (raw)
In-Reply-To: <20261028133335.75899-5-ketan@armoriq.io>
Thank you for your contribution! Sashiko AI review found 9 potential issue(s) to consider:
- [High] security: kap: detached kapfs_mnt breaks d_path() in kap_igroups_get_link()
- [Medium] security: kap: dentry reference leak on control files in kapfs_mkdir()
- [Medium] security: kap: missing superblock ops and blocksize in kapfs_fill_super()
- [Medium] security: kap: unbounded PID list growth in kap_bind_pid_locked()
- [Medium] security: kap: silent no-op on newline or NUL writes in kapfs_write()
- [Medium] security: kap: unlocked kap_task_state race in kap_ptrace_target()
- [Medium] security: kap: CLONE_THREAD bind race in kap_bind_pid_locked()
- [Medium] security: kap: predicate boundary overrun in kap_compile_kind_array()
- [Medium] security: kap: short sockaddr stack leak in kap_socket_connect()
--
commit b34e00991514a9c6893e4f407d13e24385bf26ea
Author: Ketan Bhardwaj <ketan@armoriq.io>
security: kap: add the securityfs control plane
This commit adds the securityfs control plane for the Kernel Assurance Plane
under /sys/kernel/security/kap and mounts an internal kapfs filesystem
reached via the igroups symlink to manage per-igroup directories and control
files.
> diff --git a/security/kap/kapfs.c b/security/kap/kapfs.c
> new file mode 100644
> index 0000000000000..36ef1e33a14e1
> --- /dev/null
> +++ b/security/kap/kapfs.c
[ ... ]
> +static int kapfs_show(struct seq_file *m, void *v)
> +{
> + struct kap_file_ctx *ctx = m->private;
> + struct kap_igroup *igroup = ctx->igroup;
> + struct kap_bound_pid *entry;
> +
> + spin_lock(&kap_lock);
> + switch (ctx->kind) {
> + case KAP_FILE_PROCS:
> + list_for_each_entry(entry, &igroup->pids, node)
> + seq_printf(m, "%d\n", entry->pid);
> + break;
[Severity: Medium]
Does igroup->pids grow without bound and report stale pids here?
When a pid is written to kap.procs in kapfs_write(), an entry is allocated
with kzalloc_obj(*entry, GFP_KERNEL) and appended to igroup->pids in
kap_bind_pid_locked():
security/kap/igroup.c:kap_bind_pid_locked() {
...
if (kap_pid_in_igroup_locked(igroup, pid))
return 0;
(*entry)->pid = pid;
list_add_tail(&(*entry)->node, &igroup->pids);
*entry = NULL;
return 0;
}
However, kap_task_free() in security/kap/kap_lsm.c only zeroes the task blob
when a task exits, and rebinding a task to another igroup in
kap_bind_pid_locked() does not remove it from the previous igroup's pids
list:
security/kap/kap_lsm.c:kap_task_free() {
struct kap_task_state *state = kap_task_state(task);
if (state)
memset(state, 0, sizeof(*state));
}
As bound processes exit or rebind over the system lifetime, won't the
kap_bound_pid allocations in igroup->pids leak permanently and cause
kapfs_show() to print dead, recycled, or rebound pids?
[ ... ]
> + case KAP_FILE_EVENTS:
> + seq_write(m, igroup->events, igroup->events_len);
> + break;
[Severity: Medium]
Can kapfs_show() expose uninitialized kernel stack bytes through
igroup->events when a bound task calls connect() with a short address length?
In security/kap/kap_lsm.c, kap_socket_connect() only verifies that addrlen is
at least offsetofend(struct sockaddr, sa_family) (2 bytes) before passing
address to kap_check_current_emit():
security/kap/kap_lsm.c:kap_socket_connect() {
if (addrlen < offsetofend(struct sockaddr, sa_family) ||
address->sa_family == AF_UNSPEC || address->sa_family == AF_UNIX)
return 0;
return kap_check_current_emit(KAP_HOOK_SOCKET_CONNECT, address,
"net_predicate_denied", "net_allowed");
}
In __sys_connect(), the kernel stack sockaddr_storage buffer is not
zero-initialized, and move_addr_to_kernel() only copies addrlen bytes from
userspace. When kap_object_target() in security/kap/igroup.c formats the
target for igroup->events, it reads sin_addr and sin_port or sin6_addr and
sin6_port without checking addrlen:
security/kap/igroup.c:kap_object_target() {
if (addr->sa_family == AF_INET) {
const struct sockaddr_in *in = object;
scnprintf(target, target_len, "%pI4:%u",
&in->sin_addr, ntohs(in->sin_port));
return 0;
}
if (addr->sa_family == AF_INET6) {
const struct sockaddr_in6 *in6 = object;
scnprintf(target, target_len, "%pI6c:%u",
&in6->sin6_addr, ntohs(in6->sin6_port));
return 0;
}
}
If a bound task calls connect() with addrlen == 2 and sa_family set to
AF_INET or AF_INET6, won't kap_object_target() format uninitialized kernel
stack memory into igroup->events and return it to userspace here?
[ ... ]
> +static ssize_t kapfs_write(struct file *file, const char __user *buf, size_t len, loff_t *ppos)
> +{
[ ... ]
> + tmp = memdup_user_nul(buf, len);
> + if (IS_ERR(tmp))
> + return PTR_ERR(tmp);
> + kap_trim_newline(tmp);
> + if (!tmp[0]) {
> + kfree(tmp);
> + return len;
> + }
[Severity: Medium]
Should writing a newline-only string or a NUL-prefixed buffer to a control
file return -EINVAL instead of succeeding as a silent no-op?
Because kapfs_write() returns len whenever tmp[0] is NUL after
kap_trim_newline(tmp), writing "\n" or "\0..." to kap.install, kap.procs,
kap.authority, kap.authority_epoch, kap.expires_at, kap.transition,
kap.mode, kap.default, or kap.lineage_root reports success to userspace
without validating the input or updating any state.
> +
> + /* Parse, allocate and compile first: kap_lock is a spinlock. */
> + switch (ctx->kind) {
> + case KAP_FILE_PROCS:
> + rc = kstrtoint(tmp, 10, &pid);
> + if (!rc) {
> + entry = kzalloc_obj(*entry, GFP_KERNEL);
> + if (!entry)
> + rc = -ENOMEM;
> + }
> + break;
> + case KAP_FILE_LINEAGE_ROOT:
> + if (strlen(tmp) > KAP_MAX_ROOT_LEN)
> + rc = -EINVAL;
> + break;
> + case KAP_FILE_INSTALL:
> + rc = kap_install_parse(tmp, &req);
> + if (!rc)
> + rc = kap_authority_prepare(req.authority, &compiled);
> + break;
> + case KAP_FILE_AUTHORITY:
> + rc = kap_authority_prepare(tmp, &compiled);
> + break;
[Severity: Medium]
When kapfs_write() passes an authority document to kap_authority_prepare(),
can predicates bleed across adjacent objects in kap_compile_kind_array() and
kap_validate_authority_payload()?
In security/kap/igroup.c, both kap_compile_kind_array() and
kap_validate_authority_payload() bound each predicate by scanning backwards
from "kind": to the nearest '{' character and scanning forwards to the next
"kind": substring:
security/kap/igroup.c:kap_compile_kind_array() {
while ((predicate = kap_next_predicate(predicate, end, kind_name))) {
const char *next = strstr(predicate + 1, "\"kind\":");
const char *predicate_start = predicate;
const char *predicate_end = next && next < end ? next : end;
int rc;
while (predicate_start > auth && *predicate_start != '{')
predicate_start--;
rc = kap_compile_array(compiled, set, predicate_start,
predicate_end, array_name);
}
If the next predicate places its "allow":[...] or "deny":[...] array before
its "kind":"..." key, that array precedes next and falls inside the previous
predicate's [predicate_start, predicate_end) range. Similarly, if an array
before "kind" contains a '{' character in a path pattern, scanning backwards
for '{' stops inside the string literal rather than at the start of the
predicate object, skipping the array before it.
[ ... ]
> + spin_lock(&kap_lock);
> + switch (ctx->kind) {
> + case KAP_FILE_PROCS:
> + rc = kap_bind_pid_locked(igroup, pid, &entry);
> + break;
[Severity: Medium]
Can binding a live process via kap_bind_pid_locked() race with concurrent
ptrace checks in kap_ptrace_target() and miss threads being created via
clone(CLONE_THREAD)?
First, kap_bind_pid_locked() mutates each thread's kap_task_state in place
under kap_lock, setting state->active = true before writing state->epoch and
state->igroup_name:
security/kap/igroup.c:kap_bind_pid_locked() {
for_each_thread(task, t) {
struct kap_task_state *state = kap_task_state(t);
if (!state)
continue;
state->active = true;
state->epoch = igroup->epoch;
strscpy(state->igroup_name, igroup->name, sizeof(state->igroup_name));
}
}
In security/kap/kap_lsm.c, kap_ptrace_access_check() and kap_ptrace_traceme()
call kap_ptrace_target() without holding kap_lock:
security/kap/kap_lsm.c:kap_ptrace_target() {
struct kap_task_state *current_state = kap_task_state(current);
struct kap_task_state *child_state = kap_task_state(child);
if (current_state && current_state->active &&
child_state && child_state->active &&
!strcmp(current_state->igroup_name, child_state->igroup_name) &&
current_state->epoch == child_state->epoch) {
strscpy(target, "same-igroup", target_len);
return;
}
if (child_state && child_state->active) {
scnprintf(target, target_len, "igroup=%s", child_state->igroup_name);
return;
}
}
Because kap_ptrace_target() reads active, igroup_name, and epoch unlocked,
can it observe state->active == true while igroup_name is still empty or
partially written, or while epoch is stale?
Second, in copy_process(), security_task_alloc() -> kap_task_alloc() runs
before copy_process() acquires current->sighand->siglock and links the new
thread onto signal->thread_head:
security/kap/kap_lsm.c:kap_task_alloc() {
/* Under kap_lock, so a bind running now is copied whole or not at all. */
spin_lock(&kap_lock);
if (old && old->active)
*new = *old;
spin_unlock(&kap_lock);
}
If a thread in the target process is between kap_task_alloc() and
list_add_tail_rcu(&p->thread_node, &p->signal->thread_head) when
kap_bind_pid_locked() iterates for_each_thread(task, t), won't that new
thread be missed by both kap_task_alloc() and for_each_thread() and run
unconfined?
[ ... ]
> +/* Returns NULL on success: the passed dentry is the one instantiated. */
> +static struct dentry *kapfs_mkdir(struct mnt_idmap *idmap, struct inode *dir,
> + struct dentry *dentry, umode_t mode)
> +{
[ ... ]
> + for (i = 0; i < KAP_FILE_COUNT; i++) {
> + files[i] = d_alloc_name(dentry, kap_file_names[i]);
> + inodes[i] = kapfs_make_inode(dir->i_sb, S_IFREG | kap_file_modes[i]);
[ ... ]
> + }
[ ... ]
> + /* Lookups in the new directory wait until its files are all there. */
> + inode_lock(inode);
> + kapfs_instantiate(dentry, inode);
> + igroup->dentry = dentry;
> + for (i = 0; i < KAP_FILE_COUNT; i++)
> + kapfs_instantiate(files[i], inodes[i]);
> + inode_unlock(inode);
> + return NULL;
[Severity: Medium]
Does kapfs_mkdir() leak a dentry reference on each of the files[i] dentries?
Each files[i] is allocated via d_alloc_name(dentry, kap_file_names[i]) with
an initial reference count of 1 owned by kapfs_mkdir(). Calling
kapfs_instantiate(files[i], inodes[i]) invokes d_make_persistent(), which
sets DCACHE_PERSISTENT and increments d_lockref.count to 2 via dget_dlock():
fs/dcache.c:d_make_persistent() {
...
__d_instantiate(dentry, inode);
dentry->d_flags |= DCACHE_PERSISTENT;
dget_dlock(dentry);
if (d_unhashed(dentry))
__d_rehash(dentry);
...
}
Unlike the directory dentry passed into kapfs_mkdir(), whose lookup/creation
reference is dropped by the VFS caller after vfs_mkdir() returns, nothing
calls dput(files[i]) after kapfs_instantiate(files[i], inodes[i]), leaving
every control file dentry with an extra reference count of 2 when idle.
[ ... ]
> +static int kapfs_fill_super(struct super_block *sb, struct fs_context *fc)
> +{
> + struct inode *inode;
> +
> + sb->s_magic = KAPFS_MAGIC;
> + inode = kapfs_make_inode(sb, S_IFDIR | 0700);
> + if (!inode)
> + return -ENOMEM;
> + inode->i_op = &kapfs_root_inode_ops;
> + inode->i_fop = &simple_dir_operations;
> + inc_nlink(inode);
> + sb->s_root = d_make_root(inode);
> + if (!sb->s_root)
> + return -ENOMEM;
> + return 0;
> +}
[Severity: Medium]
Should kapfs_fill_super() also initialize sb->s_op, sb->s_blocksize, and
sb->s_blocksize_bits?
Without setting sb->s_op = &simple_super_operations, sb->s_op remains at
&default_op where .statfs is NULL, causing statfs() and fstatfs() on paths
under /sys/kernel/security/kap/igroups to fail with -ENOSYS.
In addition, leaving sb->s_blocksize_bits as 0 causes new_inode() to set
inode->i_blkbits = 0 on all kapfs inodes, so stat() reports st_blksize = 1.
Userspace tools using stdio buffered writes sized from st_blksize will then
split writes into 1-byte write() calls, which kapfs_write() rejects with
-EINVAL on multi-byte control files such as kap.install and kap.authority.
[ ... ]
> +static const char *kap_igroups_get_link(struct dentry *dentry,
> + struct inode *inode,
> + struct delayed_call *done)
> +{
> + struct path path;
> + int rc;
> +
> + if (!dentry)
> + return ERR_PTR(-ECHILD);
> + path.mnt = mntget(kapfs_mnt);
> + path.dentry = dget(kapfs_mnt->mnt_root);
> + rc = nd_jump_link(&path);
> + return ERR_PTR(rc);
> +}
[Severity: High]
Does jumping to the unattached kapfs_mnt via nd_jump_link() break d_path()
resolution in the LSM hooks and allow a confined task to bypass path rules?
In kap_securityfs_init(), kapfs_mnt is created with kern_mount(&kapfs_type),
so it is an internal mount with no parent mount and mnt_ns == NULL. When a
process traverses /sys/kernel/security/kap/igroups, kap_igroups_get_link()
replaces nd->path with (kapfs_mnt, kapfs_mnt->mnt_root).
When KAP's LSM hooks resolve paths inside kapfs using d_path() in
kap_file_target(), kap_lsm_file_path_alloc(), kap_lsm_path_target(), and
kap_lsm_check_dir_entry():
security/kap/igroup.c:kap_file_target() {
...
path = d_path(&file->f_path, path_buf, PATH_MAX);
if (IS_ERR(path))
rc = PTR_ERR(path);
else
strscpy(target, path, target_len);
...
}
d_path() stops at kapfs_mnt->mnt_root and formats paths relative to the root
of kapfs_mnt as "/<igroup>" (for mkdir) and "/<igroup>/<file>" (for open,
read, and write) rather than "/sys/kernel/security/kap/igroups/<igroup>/...".
Won't this cause two problems for bound tasks:
1. A bound task with CAP_SYS_ADMIN that is denied "/sys/**" but allowed a
top-level path such as "/tmp" or "/tmp/**" can call
mkdir("/sys/kernel/security/kap/igroups/tmp", 0700) (evaluated by
kap_lsm_check_dir_entry() as "/tmp"), write an unrestricted policy to
"/sys/kernel/security/kap/igroups/tmp/kap.install" (evaluated as
"/tmp/kap.install"), and write its own pid to
"/sys/kernel/security/kap/igroups/tmp/kap.procs" (evaluated as
"/tmp/kap.procs"), which overwrites state->igroup_name in
kap_bind_pid_locked() and escapes its confinement?
2. A bound orchestrator task granted "/sys/kernel/security/kap/**" will have
its accesses to "/sys/kernel/security/kap/igroups/<igroup>/..." evaluated
as "/<igroup>/..." and rejected with -EACCES, while an igroup named "proc"
will match kap_path_is_procfs() and be evaluated against procfs rules?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261028133335.75899-1-ketan@armoriq.io?part=4
next prev parent reply other threads:[~2026-10-28 13:57 UTC|newest]
Thread overview: 14+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-28 13:33 [RFC PATCH v2 0/6] security: introduce Kernel Assurance Plane Ketan Bhardwaj
2026-10-28 13:33 ` [RFC PATCH v2 1/6] docs: security: Describe the Kernel Assurance Plane design Ketan Bhardwaj
2026-10-28 13:44 ` sashiko-bot
2026-10-28 15:17 ` Günther Noack
[not found] ` <CAKS=Y5WqExXSEu3PPbFY5J0GDaYJyjxqQc8HYUKAnQ6fpEfAsQ@mail.gmail.com>
2026-10-28 16:36 ` Ketan Bhardwaj
2026-10-28 13:33 ` [RFC PATCH v2 2/6] security: kap: add the LSM ID and internal headers Ketan Bhardwaj
2026-10-28 13:42 ` sashiko-bot
2026-10-28 13:33 ` [RFC PATCH v2 3/6] security: add the Kernel Assurance Plane LSM Ketan Bhardwaj
2026-10-28 13:57 ` sashiko-bot
2026-10-28 13:33 ` [RFC PATCH v2 4/6] security: kap: add the securityfs control plane Ketan Bhardwaj
2026-10-28 13:57 ` sashiko-bot [this message]
2026-10-28 13:33 ` [RFC PATCH v2 5/6] kernel: add KAP checks for unshare and setns Ketan Bhardwaj
2026-10-28 13:40 ` sashiko-bot
2026-10-28 13:33 ` [RFC PATCH v2 6/6] MAINTAINERS: add an entry for the Kernel Assurance Plane Ketan Bhardwaj
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=sashiko-outbox-187950@kernel.org \
--to=sashiko-bot@kernel.org \
--cc=linux-security-module@vger.kernel.org \
--cc=sashiko-reviews@lists.linux.dev \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox