Add a per-VE allowlist of /proc paths exposed through the cgroup file
ve.proc_permissions, the procfs counterpart of ve.sysfs_permissions.
Each proc_dir_entry gains a kmapset map keyed by VE (proc_perms_key on
ve_struct), so the single shared proc tree yields per-VE answers. The
filesystem agnostic leaf logic is reused from fs/ve_perms.c, this commit
adds the proc tree walk, the locking, and the VFS hooks: visibility in
proc_lookup_de/proc_readdir_de and a .permission inode op.

Paths are written relative to the proc root like sysfs, path mask where
mask is r/w/x or - to remove. The host (ve0) is unaffected and an empty
list exposes nothing extra. The lock-free readers load the map under rcu
against the writer's copy-on-write swap, and the seq read is serialised
against the writer by proc_perms_mutex.

Signed-off-by: Mirian Shilakadze <[email protected]>
---
 fs/proc/Makefile   |   1 +
 fs/proc/generic.c  |  48 ++++++-
 fs/proc/inode.c    |   2 +
 fs/proc/internal.h |  25 ++++
 fs/proc/root.c     |   1 +
 fs/proc/ve.c       | 345 +++++++++++++++++++++++++++++++++++++++++++++
 include/linux/ve.h |   1 +
 kernel/ve/ve.c     |   7 +
 8 files changed, 423 insertions(+), 7 deletions(-)
 create mode 100644 fs/proc/ve.c

diff --git a/fs/proc/Makefile b/fs/proc/Makefile
index 7b4db9c56e6a..61a999c03663 100644
--- a/fs/proc/Makefile
+++ b/fs/proc/Makefile
@@ -11,6 +11,7 @@ proc-$(CONFIG_MMU)    := task_mmu.o
 
 proc-y       += inode.o root.o base.o generic.o array.o \
                fd.o
+proc-$(CONFIG_VE)      += ve.o
 proc-$(CONFIG_TTY)      += proc_tty.o
 proc-y += cmdline.o
 proc-y += consoles.o
diff --git a/fs/proc/generic.c b/fs/proc/generic.c
index e8fd7c2d1c3a..791c38c49a86 100644
--- a/fs/proc/generic.c
+++ b/fs/proc/generic.c
@@ -30,7 +30,7 @@
 
 #include "internal.h"
 
-static DEFINE_RWLOCK(proc_subdir_lock);
+DEFINE_RWLOCK(proc_subdir_lock);
 
 struct kmem_cache *proc_dir_entry_cache __ro_after_init;
 
@@ -65,9 +65,9 @@ static struct proc_dir_entry *pde_subdir_next(struct 
proc_dir_entry *dir)
                             subdir_node);
 }
 
-static struct proc_dir_entry *pde_subdir_find(struct proc_dir_entry *dir,
-                                             const char *name,
-                                             unsigned int len)
+struct proc_dir_entry *pde_subdir_find(struct proc_dir_entry *dir,
+                                      const char *name,
+                                      unsigned int len)
 {
        struct rb_node *node = dir->subdir.rb_node;
 
@@ -120,6 +120,31 @@ static bool proc_in_container(struct super_block *sb)
        return !ve_is_super(get_exec_env());
 }
 
+/* Visible to the current VE: globally published (S_ISVTX) or per-VE allowed. 
*/
+static bool pde_visible_to_ve(struct proc_dir_entry *de)
+{
+       return (de->mode & S_ISVTX) || proc_d_visible(de);
+}
+
+#ifdef CONFIG_VE
+static int proc_iop_permission(struct mnt_idmap *idmap, struct inode *inode,
+                              int mask)
+{
+       struct proc_dir_entry *de = PDE(inode);
+       int ret = 0;
+
+       /*
+        * Runs safely during rcu-walk: proc_ve_permission() is a lockless rcu
+        * kmapset lookup and generic_permission() copes with rcu-walk on its
+        * own, so MAY_NOT_BLOCK needs no special handling here.
+        */
+       if (proc_in_container(inode->i_sb) && !(de->mode & S_ISVTX))
+               ret = proc_ve_permission(de, mask);
+
+       return ret ? ret : generic_permission(idmap, inode, mask);
+}
+#endif
+
 static int proc_notify_change(struct mnt_idmap *idmap,
                              struct dentry *dentry, struct iattr *iattr)
 {
@@ -167,6 +192,9 @@ static int proc_getattr(struct mnt_idmap *idmap,
 
 static const struct inode_operations proc_file_inode_operations = {
        .setattr        = proc_notify_change,
+#ifdef CONFIG_VE
+       .permission     = proc_iop_permission,
+#endif
 };
 
 /*
@@ -264,7 +292,7 @@ struct dentry *proc_lookup_de(struct inode *dir, struct 
dentry *dentry,
        read_lock(&proc_subdir_lock);
        de = pde_subdir_find(de, dentry->d_name.name, dentry->d_name.len);
        if (de) {
-               if (in_container && !(de->mode & S_ISVTX)) {
+               if (in_container && !pde_visible_to_ve(de)) {
                        read_unlock(&proc_subdir_lock);
                        return ERR_PTR(-ENOENT);
                }
@@ -317,7 +345,7 @@ int proc_readdir_de(struct file *file, struct dir_context 
*ctx,
                        read_unlock(&proc_subdir_lock);
                        return 0;
                }
-               if (!in_container || (de->mode & S_ISVTX)) {
+               if (!in_container || pde_visible_to_ve(de)) {
                        if (!i)
                                break;
                        i--;
@@ -328,7 +356,7 @@ int proc_readdir_de(struct file *file, struct dir_context 
*ctx,
        do {
                struct proc_dir_entry *next;
 
-               if (in_container && !(de->mode & S_ISVTX)) {
+               if (in_container && !pde_visible_to_ve(de)) {
                        de = pde_subdir_next(de);
                        continue;
                }
@@ -389,6 +417,9 @@ static const struct inode_operations 
proc_dir_inode_operations = {
        .lookup         = proc_lookup,
        .getattr        = proc_getattr,
        .setattr        = proc_notify_change,
+#ifdef CONFIG_VE
+       .permission     = proc_iop_permission,
+#endif
 };
 
 /* returns the registered entry, or frees dp and returns NULL on failure */
@@ -413,6 +444,7 @@ struct proc_dir_entry *proc_register(struct proc_dir_entry 
*dir,
 out_free_inum:
        proc_free_inum(dp->low_ino);
 out_free_entry:
+       proc_put_ve_perms(dp);
        pde_free(dp);
        return NULL;
 }
@@ -471,6 +503,7 @@ static struct proc_dir_entry *__proc_create(struct 
proc_dir_entry **parent,
        ent->nlink = nlink;
        ent->subdir = RB_ROOT;
        refcount_set(&ent->refcnt, 1);
+       proc_get_ve_perms(ent);
        spin_lock_init(&ent->pde_unload_lock);
        INIT_LIST_HEAD(&ent->pde_openers);
        proc_set_user(ent, (*parent)->uid, (*parent)->gid);
@@ -498,6 +531,7 @@ struct proc_dir_entry *proc_symlink_mode(const char *name, 
umode_t mode,
                        ent->proc_iops = &proc_link_inode_operations;
                        ent = proc_register(parent, ent);
                } else {
+                       proc_put_ve_perms(ent);
                        pde_free(ent);
                        ent = NULL;
                }
diff --git a/fs/proc/inode.c b/fs/proc/inode.c
index 5d1a75408aa4..ac943a9768f4 100644
--- a/fs/proc/inode.c
+++ b/fs/proc/inode.c
@@ -272,6 +272,8 @@ void proc_entry_rundown(struct proc_dir_entry *de)
                spin_lock(&de->pde_unload_lock);
        }
        spin_unlock(&de->pde_unload_lock);
+
+       proc_put_ve_perms(de);
 }
 
 static loff_t proc_reg_llseek(struct file *file, loff_t offset, int whence)
diff --git a/fs/proc/internal.h b/fs/proc/internal.h
index 77a517f91821..4a28da7d5dee 100644
--- a/fs/proc/internal.h
+++ b/fs/proc/internal.h
@@ -17,6 +17,7 @@
 
 struct ctl_table_header;
 struct mempolicy;
+struct kmapset_map;
 
 /*
  * This is not completely implemented yet. The idea is to
@@ -64,6 +65,9 @@ struct proc_dir_entry {
        umode_t mode;
        u8 flags;
        u8 namelen;
+#ifdef CONFIG_VE
+       struct kmapset_map __rcu *ve_perms_map; /* per-VE r/w/x mask for this 
node */
+#endif
        char inline_name[];
 } __randomize_layout;
 
@@ -102,6 +106,27 @@ static inline bool pde_has_proc_compat_ioctl(const struct 
proc_dir_entry *pde)
 extern struct kmem_cache *proc_dir_entry_cache;
 void pde_free(struct proc_dir_entry *pde);
 
+extern rwlock_t proc_subdir_lock;
+struct proc_dir_entry *pde_subdir_find(struct proc_dir_entry *dir,
+                                      const char *name, unsigned int len);
+
+#ifdef CONFIG_VE
+void proc_init_ve_perms(void);
+void proc_get_ve_perms(struct proc_dir_entry *de);
+void proc_put_ve_perms(struct proc_dir_entry *de);
+bool proc_d_visible(struct proc_dir_entry *de);
+int proc_ve_permission(struct proc_dir_entry *de, int mask);
+#else
+static inline void proc_init_ve_perms(void) { }
+static inline void proc_get_ve_perms(struct proc_dir_entry *de) { }
+static inline void proc_put_ve_perms(struct proc_dir_entry *de) { }
+static inline bool proc_d_visible(struct proc_dir_entry *de) { return false; }
+static inline int proc_ve_permission(struct proc_dir_entry *de, int mask)
+{
+       return 0;
+}
+#endif
+
 union proc_op {
        int (*proc_get_link)(struct dentry *, struct path *);
        int (*proc_show)(struct seq_file *m,
diff --git a/fs/proc/root.c b/fs/proc/root.c
index 3f61de56ffff..9e0c5bf87602 100644
--- a/fs/proc/root.c
+++ b/fs/proc/root.c
@@ -297,6 +297,7 @@ static struct file_system_type proc_fs_type = {
 void __init proc_root_init(void)
 {
        proc_init_kmemcache();
+       proc_init_ve_perms();
        set_proc_pid_nlink();
        proc_self_init();
        proc_thread_self_init();
diff --git a/fs/proc/ve.c b/fs/proc/ve.c
new file mode 100644
index 000000000000..10106c2a4e53
--- /dev/null
+++ b/fs/proc/ve.c
@@ -0,0 +1,345 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ *  Per-VE /proc permissions (ve.proc_permissions), the procfs counterpart of
+ *  the sysfs ve.sysfs_permissions mechanism. Each proc_dir_entry carries a
+ *  kmapset map keyed by VE, so the single shared proc tree gives per-VE
+ *  answers. The filesystem agnostic leaf logic lives in fs/ve_perms.c, this
+ *  file owns the proc tree walk and the locking.
+ *
+ *  Copyright (c) 2026 Virtuozzo International GmbH. All rights reserved.
+ */
+
+#include <linux/module.h>
+#include <linux/errno.h>
+#include <linux/slab.h>
+#include <linux/string.h>
+#include <linux/rbtree.h>
+#include <linux/rcupdate.h>
+#include <linux/seq_file.h>
+#include <linux/fs.h>
+#include <linux/cgroup.h>
+#include <linux/ve.h>
+#include <linux/kmapset.h>
+#include <linux/ve-perms.h>
+#include <linux/proc_fs.h>
+
+#include "internal.h"
+
+struct kmapset_set proc_ve_perms_set;
+
+static bool proc_ve_perms_inited;
+
+static DEFINE_MUTEX(proc_perms_mutex);
+
+void __init proc_init_ve_perms(void)
+{
+       struct kmapset_map *map;
+
+       kmapset_init_set(&proc_ve_perms_set);
+       map = kmapset_new(&proc_ve_perms_set);
+       if (map)
+               RCU_INIT_POINTER(proc_root.ve_perms_map, kmapset_commit(map));
+       proc_ve_perms_inited = true;
+}
+
+void proc_get_ve_perms(struct proc_dir_entry *de)
+{
+       struct kmapset_map *map;
+
+       if (!proc_ve_perms_inited)
+               return;
+
+       map = kmapset_new(&proc_ve_perms_set);
+       if (map)
+               rcu_assign_pointer(de->ve_perms_map, kmapset_commit(map));
+       else
+               pr_warn_once("proc: no ve_perms_map for %s, hidden from 
containers\n",
+                            de->name);
+}
+
+/*
+ * Drop the node's permission map. kmapset_put() can sleep (it takes the
+ * kmapset set mutex on the last reference), so every caller must be in process
+ * context. Registered entries drop the map from proc_entry_rundown() when they
+ * are removed. Entries that never reach the tree drop it on their creation 
error
+ * path (proc_register(), proc_symlink_mode()). pde_free() therefore never
+ * touches the map and stays safe to run from the .free_inode RCU callback, 
which
+ * is atomic. The mutex here serialises against a concurrent proc_perms_set() 
on
+ * a registered entry. The NULL fast path skips it when there is nothing to 
drop.
+ */
+void proc_put_ve_perms(struct proc_dir_entry *de)
+{
+       struct kmapset_map *map;
+
+       /* Atomic-safe fast path: already dropped at rundown, or never set. */
+       if (!rcu_access_pointer(de->ve_perms_map))
+               return;
+
+       /* Serialise against a concurrent proc_perms_set() on this entry. */
+       mutex_lock(&proc_perms_mutex);
+       map = rcu_dereference_protected(de->ve_perms_map,
+                                      lockdep_is_held(&proc_perms_mutex));
+       rcu_assign_pointer(de->ve_perms_map, NULL);
+       mutex_unlock(&proc_perms_mutex);
+       kmapset_put(map);
+}
+
+bool proc_d_visible(struct proc_dir_entry *de)
+{
+       struct ve_struct *ve = get_exec_env();
+       struct kmapset_map *map;
+       bool visible;
+
+       if (ve_is_super(ve))
+               return true;
+
+       /*
+        * proc_perms_set() can swap this map pointer concurrently and free the
+        * old map through kfree_rcu(). Hold rcu across both the load and the
+        * lookup so the map cannot be freed under us.
+        */
+       rcu_read_lock();
+       map = rcu_dereference(de->ve_perms_map);
+       visible = map && ve_perms_visible(map, &ve->proc_perms_key);
+       rcu_read_unlock();
+       return visible;
+}
+
+int proc_ve_permission(struct proc_dir_entry *de, int mask)
+{
+       struct ve_struct *ve = get_exec_env();
+       struct kmapset_map *map;
+       int ret;
+
+       if (ve_is_super(ve))
+               return 0;
+
+       rcu_read_lock();
+       map = rcu_dereference(de->ve_perms_map);
+       ret = map ? ve_perms_check(map, &ve->proc_perms_key, mask) : -EACCES;
+       rcu_read_unlock();
+       return ret;
+}
+
+static int proc_perms_set(char *path, struct ve_struct *ve, int mask)
+{
+       struct proc_dir_entry *de, *nde;
+       char *sep = path, *dname;
+       int ret = 0;
+
+       read_lock(&proc_subdir_lock);
+       de = &proc_root;
+       pde_get(de);
+       do {
+               dname = sep;
+
+               sep = strchr(sep, '/');
+               if (sep)
+                       *sep++ = 0;
+
+               if (!*dname)
+                       break;
+
+               nde = pde_subdir_find(de, dname, strlen(dname));
+               if (!nde) {
+                       read_unlock(&proc_subdir_lock);
+                       ret = -ENOENT;
+                       goto out;
+               }
+               pde_get(nde);
+               pde_put(de);
+               de = nde;
+       } while (sep);
+       read_unlock(&proc_subdir_lock);
+
+       /* empty or leading-slash path walks to nothing, reject it */
+       if (de == &proc_root) {
+               ret = -EINVAL;
+               goto out;
+       }
+
+       if (!rcu_access_pointer(de->ve_perms_map)) {
+               ret = -EPERM;
+               goto out;
+       }
+
+       ret = ve_perms_apply(&de->ve_perms_map, &ve->proc_perms_key,
+                            ve_is_super(ve), mask);
+out:
+       pde_put(de);
+       return ret;
+}
+
+static int proc_perms_line(struct ve_struct *ve, char *line)
+{
+       int mask, ret;
+
+       ret = ve_perms_parse(line, &mask);
+       if (ret)
+               return ret;
+       return proc_perms_set(line, ve, mask);
+}
+
+static struct proc_dir_entry *proc_next_recursive(struct proc_dir_entry *de)
+{
+       struct rb_node *node;
+
+       node = rb_first(&de->subdir);
+       if (node)
+               return rb_entry(node, struct proc_dir_entry, subdir_node);
+
+       while (de->parent != de) {
+               node = rb_next(&de->subdir_node);
+               if (node)
+                       return rb_entry(node, struct proc_dir_entry,
+                                       subdir_node);
+               de = de->parent;
+       }
+       return NULL;
+}
+
+static bool proc_perms_shown(struct ve_struct *ve, struct proc_dir_entry *de)
+{
+       bool shown;
+
+       if (!rcu_access_pointer(de->ve_perms_map))
+               return false;
+
+       /* ve_perms_shown calls kmapset_lookup, an rcu list walk, guard it. */
+       rcu_read_lock();
+       shown = ve_perms_shown(rcu_dereference(de->ve_perms_map),
+                              &ve->proc_perms_key, ve_is_super(ve));
+       rcu_read_unlock();
+       return shown;
+}
+
+static void *proc_perms_start(struct seq_file *m, loff_t *ppos)
+       __acquires(&proc_subdir_lock)
+{
+       struct ve_struct *ve = css_to_ve(seq_css(m));
+       struct proc_dir_entry *de;
+       loff_t pos = *ppos;
+
+       mutex_lock(&proc_perms_mutex);
+       read_lock(&proc_subdir_lock);
+       for (de = &proc_root; de; de = proc_next_recursive(de)) {
+               if (proc_perms_shown(ve, de) && !pos--)
+                       break;
+       }
+       return de;
+}
+
+static void *proc_perms_next(struct seq_file *m, void *v, loff_t *ppos)
+{
+       struct ve_struct *ve = css_to_ve(seq_css(m));
+       struct proc_dir_entry *de = v;
+
+       (*ppos)++;
+       while ((de = proc_next_recursive(de))) {
+               if (proc_perms_shown(ve, de))
+                       break;
+       }
+       return de;
+}
+
+static void proc_perms_stop(struct seq_file *m, void *v)
+       __releases(&proc_subdir_lock)
+{
+       read_unlock(&proc_subdir_lock);
+       mutex_unlock(&proc_perms_mutex);
+}
+
+static int proc_perms_show(struct seq_file *m, void *v)
+{
+       struct ve_struct *ve = css_to_ve(seq_css(m));
+       struct proc_dir_entry *de = v;
+       struct kmapset_map *map;
+       char *buf;
+       size_t size, len, off;
+       int mask;
+
+       map = rcu_dereference_protected(de->ve_perms_map,
+                                      lockdep_is_held(&proc_perms_mutex));
+       if (ve_is_super(ve))
+               mask = map->default_value;
+       else
+               mask = kmapset_get_value(map, &ve->proc_perms_key);
+
+       size = seq_get_buf(m, &buf);
+       if (size) {
+               off = size;
+               do {
+                       len = strlen(de->name);
+                       if (len >= off) {
+                               seq_commit(m, -1);
+                               return 0;
+                       }
+                       if (S_ISDIR(de->mode))
+                               buf[--off] = '/';
+                       off -= len;
+                       memcpy(buf + off, de->name, len);
+                       de = de->parent;
+               } while (de && de != &proc_root);
+               memmove(buf, buf + off, size - off);
+               seq_commit(m, size - off);
+       }
+
+       ve_perms_emit(m, mask);
+       return 0;
+}
+
+static ssize_t proc_perms_write(struct kernfs_open_file *of, char *buf,
+                               size_t nbytes, loff_t off)
+{
+       struct ve_struct *ve = css_to_ve(of_css(of));
+       char *line, *next = buf;
+       int ret = -EINVAL;
+
+       mutex_lock(&proc_perms_mutex);
+       do {
+               line = skip_spaces(next);
+               if (!*line)
+                       break;
+
+               next = strchr(line, '\n');
+               if (next)
+                       *next++ = '\0';
+
+               if (*line != '#') {
+                       ret = proc_perms_line(ve, line);
+                       if (ret)
+                               break;
+               }
+       } while (next);
+       mutex_unlock(&proc_perms_mutex);
+
+       return ret ? ret : nbytes;
+}
+
+static struct cftype proc_ve_cftypes[] = {
+       {
+               .name           = "default_proc_permissions",
+               .flags          = CFTYPE_ONLY_ON_ROOT,
+               .seq_start      = proc_perms_start,
+               .seq_next       = proc_perms_next,
+               .seq_stop       = proc_perms_stop,
+               .seq_show       = proc_perms_show,
+               .write          = proc_perms_write,
+       },
+       {
+               .name           = "proc_permissions",
+               .flags          = CFTYPE_NOT_ON_ROOT,
+               .seq_start      = proc_perms_start,
+               .seq_next       = proc_perms_next,
+               .seq_stop       = proc_perms_stop,
+               .seq_show       = proc_perms_show,
+               .write          = proc_perms_write,
+       },
+       { },
+};
+
+static int init_proc_ve_perms(void)
+{
+       return cgroup_add_cftypes(&ve_cgrp_subsys, proc_ve_cftypes);
+}
+module_init(init_proc_ve_perms);
diff --git a/include/linux/ve.h b/include/linux/ve.h
index b037f60225bb..cba827260d07 100644
--- a/include/linux/ve.h
+++ b/include/linux/ve.h
@@ -69,6 +69,7 @@ struct ve_struct {
        int                     fsync_enable;
 
        struct kmapset_key      sysfs_perms_key;
+       struct kmapset_key      proc_perms_key;
 
        atomic_t                netns_avail_nr;
        int                     netns_max_nr;
diff --git a/kernel/ve/ve.c b/kernel/ve/ve.c
index e58ffb22da87..d8ef28eedabd 100644
--- a/kernel/ve/ve.c
+++ b/kernel/ve/ve.c
@@ -44,6 +44,9 @@
 #include "../sched/sched.h"            /* For css_tg() */
 
 extern struct kmapset_set sysfs_ve_perms_set;
+#ifdef CONFIG_PROC_FS
+extern struct kmapset_set proc_ve_perms_set;
+#endif
 
 static struct kmem_cache *ve_cachep;
 
@@ -771,6 +774,7 @@ static struct cgroup_subsys_state *ve_create(struct 
cgroup_subsys_state *parent_
        init_rwsem(&ve->op_sem);
        INIT_LIST_HEAD(&ve->ve_list);
        kmapset_init_key(&ve->sysfs_perms_key);
+       kmapset_init_key(&ve->proc_perms_key);
 
        atomic_set(&ve->arp_neigh_nr, 0);
        atomic_set(&ve->nd_neigh_nr, 0);
@@ -866,6 +870,9 @@ static void ve_destroy(struct cgroup_subsys_state *css)
        free_ve_devmnts(ve);
 
        kmapset_unlink(&ve->sysfs_perms_key, &sysfs_ve_perms_set);
+#ifdef CONFIG_PROC_FS
+       kmapset_unlink(&ve->proc_perms_key, &proc_ve_perms_set);
+#endif
        ve_log_destroy(ve);
        ve_free_vdso(ve);
        mntput(ve->devtmpfs_mnt);
-- 
2.43.0

_______________________________________________
Devel mailing list
[email protected]
https://lists.openvz.org/mailman/listinfo/devel

Reply via email to