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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.