[PATCH 1/2] proc: use subset option to hide some top-level procfs entries

From: Alexey Gladkov
Date: Thu Jun 04 2020 - 16:04:50 EST


In addition to subset=pid, added the ability to specify top-level
directory and file names.

Not all directories in procfs have proc directory entries. For example,
/proc/sys has its own directory operations and does not have proc
directory entries. But all paths in procfs have at least one top-level
proc directory entry.

Since files or directories can be created by kernel modules as they are
loaded, we use a list of names to check a proc directory entries.

Signed-off-by: Alexey Gladkov <gladkov.alexey@xxxxxxxxx>
---
fs/proc/base.c | 15 +++++++-
fs/proc/generic.c | 75 ++++++++++++++++++++++++++++++--------
fs/proc/inode.c | 18 ++++++---
fs/proc/internal.h | 12 ++++++
fs/proc/root.c | 81 +++++++++++++++++++++++++++++++++--------
include/linux/proc_fs.h | 11 +++---
6 files changed, 169 insertions(+), 43 deletions(-)

diff --git a/fs/proc/base.c b/fs/proc/base.c
index b1d94d14ed5a..9851a1f80e8c 100644
--- a/fs/proc/base.c
+++ b/fs/proc/base.c
@@ -3329,6 +3329,10 @@ struct dentry *proc_pid_lookup(struct dentry *dentry, unsigned int flags)
goto out;

fs_info = proc_sb_info(dentry->d_sb);
+
+ if (!is_pids_visible(fs_info))
+ goto out;
+
ns = fs_info->pid_ns;
rcu_read_lock();
task = find_task_by_pid_ns(tgid, ns);
@@ -3388,13 +3392,18 @@ static struct tgid_iter next_tgid(struct pid_namespace *ns, struct tgid_iter ite
int proc_pid_readdir(struct file *file, struct dir_context *ctx)
{
struct tgid_iter iter;
- struct proc_fs_info *fs_info = proc_sb_info(file_inode(file)->i_sb);
- struct pid_namespace *ns = proc_pid_ns(file_inode(file)->i_sb);
+ struct proc_fs_info *fs_info;
+ struct pid_namespace *ns;
loff_t pos = ctx->pos;

if (pos >= PID_MAX_LIMIT + TGID_OFFSET)
return 0;

+ fs_info = proc_sb_info(file_inode(file)->i_sb);
+
+ if (!is_pids_visible(fs_info))
+ goto out;
+
if (pos == TGID_OFFSET - 2) {
struct inode *inode = d_inode(fs_info->proc_self);
if (!dir_emit(ctx, "self", 4, inode->i_ino, DT_LNK))
@@ -3407,6 +3416,7 @@ int proc_pid_readdir(struct file *file, struct dir_context *ctx)
return 0;
ctx->pos = pos = pos + 1;
}
+ ns = proc_pid_ns(file_inode(file)->i_sb);
iter.tgid = pos - TGID_OFFSET;
iter.task = NULL;
for (iter = next_tgid(ns, iter);
@@ -3427,6 +3437,7 @@ int proc_pid_readdir(struct file *file, struct dir_context *ctx)
return 0;
}
}
+out:
ctx->pos = PID_MAX_LIMIT + TGID_OFFSET;
return 0;
}
diff --git a/fs/proc/generic.c b/fs/proc/generic.c
index 2f9fa179194d..e96a593d3b09 100644
--- a/fs/proc/generic.c
+++ b/fs/proc/generic.c
@@ -196,6 +196,48 @@ static int xlate_proc_name(const char *name, struct proc_dir_entry **ret,
return rv;
}

+bool is_pde_visible(struct proc_fs_info *fs_info, struct proc_dir_entry *pde)
+{
+ int i;
+
+ if (!fs_info->whitelist || pde == &proc_root)
+ return 1;
+
+ read_lock(&proc_subdir_lock);
+
+ for (i = 0; fs_info->whitelist[i]; i++) {
+ struct proc_dir_entry *ent, *de;
+
+ if (!strcmp(fs_info->whitelist[i], "pid"))
+ continue;
+
+ ent = pde_subdir_find(&proc_root, fs_info->whitelist[i],
+ strlen(fs_info->whitelist[i]));
+ if (!ent)
+ continue;
+
+ if (ent == pde) {
+ read_unlock(&proc_subdir_lock);
+ return 1;
+ }
+
+ if (!S_ISDIR(ent->mode))
+ continue;
+
+ de = pde->parent;
+ while (de != &proc_root) {
+ if (ent == de) {
+ read_unlock(&proc_subdir_lock);
+ return 1;
+ }
+ de = de->parent;
+ }
+ }
+
+ read_unlock(&proc_subdir_lock);
+ return 0;
+}
+
static DEFINE_IDA(proc_inum_ida);

#define PROC_DYNAMIC_FIRST 0xF0000000U
@@ -251,6 +293,9 @@ struct dentry *proc_lookup_de(struct inode *dir, struct dentry *dentry,
{
struct inode *inode;

+ if (!is_visible(dir))
+ return ERR_PTR(-ENOENT);
+
read_lock(&proc_subdir_lock);
de = pde_subdir_find(de, dentry->d_name.name, dentry->d_name.len);
if (de) {
@@ -259,6 +304,8 @@ struct dentry *proc_lookup_de(struct inode *dir, struct dentry *dentry,
inode = proc_get_inode(dir->i_sb, de);
if (!inode)
return ERR_PTR(-ENOMEM);
+ if (!is_visible(inode))
+ return ERR_PTR(-ENOENT);
d_set_d_op(dentry, de->proc_dops);
return d_splice_alias(inode, dentry);
}
@@ -269,11 +316,6 @@ struct dentry *proc_lookup_de(struct inode *dir, struct dentry *dentry,
struct dentry *proc_lookup(struct inode *dir, struct dentry *dentry,
unsigned int flags)
{
- struct proc_fs_info *fs_info = proc_sb_info(dir->i_sb);
-
- if (fs_info->pidonly == PROC_PIDONLY_ON)
- return ERR_PTR(-ENOENT);
-
return proc_lookup_de(dir, dentry, PDE(dir));
}

@@ -289,11 +331,17 @@ struct dentry *proc_lookup(struct inode *dir, struct dentry *dentry,
int proc_readdir_de(struct file *file, struct dir_context *ctx,
struct proc_dir_entry *de)
{
+ struct proc_fs_info *fs_info;
int i;

if (!dir_emit_dots(file, ctx))
return 0;

+ fs_info = proc_sb_info(file_inode(file)->i_sb);
+
+ if (!is_pde_visible(fs_info, de))
+ return 0;
+
i = ctx->pos - 2;
read_lock(&proc_subdir_lock);
de = pde_subdir_first(de);
@@ -312,12 +360,14 @@ int proc_readdir_de(struct file *file, struct dir_context *ctx,
struct proc_dir_entry *next;
pde_get(de);
read_unlock(&proc_subdir_lock);
- if (!dir_emit(ctx, de->name, de->namelen,
- de->low_ino, de->mode >> 12)) {
- pde_put(de);
- return 0;
+ if (is_pde_visible(fs_info, de)) {
+ if (!dir_emit(ctx, de->name, de->namelen,
+ de->low_ino, de->mode >> 12)) {
+ pde_put(de);
+ return 0;
+ }
+ ctx->pos++;
}
- ctx->pos++;
read_lock(&proc_subdir_lock);
next = pde_subdir_next(de);
pde_put(de);
@@ -330,11 +380,6 @@ int proc_readdir_de(struct file *file, struct dir_context *ctx,
int proc_readdir(struct file *file, struct dir_context *ctx)
{
struct inode *inode = file_inode(file);
- struct proc_fs_info *fs_info = proc_sb_info(inode->i_sb);
-
- if (fs_info->pidonly == PROC_PIDONLY_ON)
- return 1;
-
return proc_readdir_de(file, ctx, PDE(inode));
}

diff --git a/fs/proc/inode.c b/fs/proc/inode.c
index f40c2532c057..61374eb76ce4 100644
--- a/fs/proc/inode.c
+++ b/fs/proc/inode.c
@@ -181,13 +181,20 @@ static inline const char *hidepid2str(enum proc_hidepid v)
static int proc_show_options(struct seq_file *seq, struct dentry *root)
{
struct proc_fs_info *fs_info = proc_sb_info(root->d_sb);
+ int i;

if (!gid_eq(fs_info->pid_gid, GLOBAL_ROOT_GID))
seq_printf(seq, ",gid=%u", from_kgid_munged(&init_user_ns, fs_info->pid_gid));
if (fs_info->hide_pid != HIDEPID_OFF)
seq_printf(seq, ",hidepid=%s", hidepid2str(fs_info->hide_pid));
- if (fs_info->pidonly != PROC_PIDONLY_OFF)
- seq_printf(seq, ",subset=pid");
+ if (fs_info->whitelist) {
+ seq_puts(seq, ",subset=");
+ for (i = 0; fs_info->whitelist[i]; i++) {
+ if (i)
+ seq_puts(seq, ":");
+ seq_printf(seq, "%s", fs_info->whitelist[i]);
+ }
+ }

return 0;
}
@@ -478,13 +485,15 @@ proc_reg_get_unmapped_area(struct file *file, unsigned long orig_addr,

static int proc_reg_open(struct inode *inode, struct file *file)
{
- struct proc_fs_info *fs_info = proc_sb_info(inode->i_sb);
struct proc_dir_entry *pde = PDE(inode);
int rv = 0;
typeof_member(struct proc_ops, proc_open) open;
typeof_member(struct proc_ops, proc_release) release;
struct pde_opener *pdeo;

+ if (!is_visible(inode))
+ return -ENOENT;
+
if (pde_is_permanent(pde)) {
open = pde->proc_ops->proc_open;
if (open)
@@ -492,9 +501,6 @@ static int proc_reg_open(struct inode *inode, struct file *file)
return rv;
}

- if (fs_info->pidonly == PROC_PIDONLY_ON)
- return -ENOENT;
-
/*
* Ensure that
* 1) PDE's ->release hook will be called no matter what
diff --git a/fs/proc/internal.h b/fs/proc/internal.h
index 917cc85e3466..2f14a3692fc1 100644
--- a/fs/proc/internal.h
+++ b/fs/proc/internal.h
@@ -203,6 +203,18 @@ static inline bool is_empty_pde(const struct proc_dir_entry *pde)
}
extern ssize_t proc_simple_write(struct file *, const char __user *, size_t, loff_t *);

+extern bool is_pde_visible(struct proc_fs_info *fs_info, struct proc_dir_entry *pde);
+
+static inline bool is_pids_visible(struct proc_fs_info *fs_info)
+{
+ return (fs_info->pids_visibility == PROC_PIDS_VISIBLE);
+}
+
+static inline bool is_visible(struct inode *inode)
+{
+ return is_pde_visible(proc_sb_info(inode->i_sb), PDE(inode));
+}
+
/*
* inode.c
*/
diff --git a/fs/proc/root.c b/fs/proc/root.c
index ffebed1999e5..5401d88abe9d 100644
--- a/fs/proc/root.c
+++ b/fs/proc/root.c
@@ -34,7 +34,8 @@ struct proc_fs_context {
unsigned int mask;
enum proc_hidepid hidepid;
int gid;
- enum proc_pidonly pidonly;
+ enum proc_pids_visible pids_visibility;
+ char **whitelist;
};

enum proc_param {
@@ -89,26 +90,71 @@ static int proc_parse_hidepid_param(struct fs_context *fc, struct fs_parameter *
return 0;
}

-static int proc_parse_subset_param(struct fs_context *fc, char *value)
+static int proc_parse_subset_param(struct fs_context *fc, const char *value)
{
+ int i, elemsiz, siz;
+ const char *elem, *endelem, *delim, *endval;
+ char *ptr;
struct proc_fs_context *ctx = fc->fs_private;

- while (value) {
- char *ptr = strchr(value, ',');
+ if (strpbrk(value, "/ "))
+ return invalf(fc, "proc: unsupported subset value - `%s'", value);

- if (ptr != NULL)
- *ptr++ = '\0';
+ endval = value + strlen(value) + 1;
+ siz = i = 0;

- if (*value != '\0') {
- if (!strcmp(value, "pid")) {
- ctx->pidonly = PROC_PIDONLY_ON;
- } else {
- return invalf(fc, "proc: unsupported subset option - %s\n", value);
- }
+ for (delim = elem = value; delim; elem = endelem) {
+ delim = strchr(elem, ':');
+
+ endelem = delim ? ++delim : endval;
+ elemsiz = endelem - elem;
+
+ if (elemsiz > 1) {
+ siz += sizeof(char *) + elemsiz;
+ i++;
}
- value = ptr;
}

+ if (!i)
+ return invalf(fc, "proc: empty subset value is not valid");
+
+ siz += sizeof(char *);
+ i++;
+
+ kfree(ctx->whitelist);
+ ctx->whitelist = kmalloc(siz, GFP_KERNEL);
+
+ if (!ctx->whitelist) {
+ errorf(fc, "proc: unable to allocate enough memory to store subset filter");
+ return -ENOMEM;
+ }
+
+ ctx->pids_visibility = PROC_PIDS_INVISIBLE;
+
+ ptr = (char *)(ctx->whitelist + i);
+ siz = i = 0;
+
+ for (delim = elem = value; delim; elem = endelem) {
+ delim = strchr(elem, ':');
+
+ endelem = delim ? ++delim : endval;
+ elemsiz = endelem - elem;
+
+ if (elemsiz <= 1)
+ continue;
+
+ if (!strncmp("pid", elem, elemsiz - 1))
+ ctx->pids_visibility = PROC_PIDS_VISIBLE;
+
+ strncpy(ptr, elem, elemsiz);
+ ctx->whitelist[i] = ptr;
+ ctx->whitelist[i][elemsiz - 1] = '\0';
+
+ ptr += elemsiz;
+ i++;
+ }
+ ctx->whitelist[i] = NULL;
+
return 0;
}

@@ -155,8 +201,11 @@ static void proc_apply_options(struct proc_fs_info *fs_info,
fs_info->pid_gid = make_kgid(user_ns, ctx->gid);
if (ctx->mask & (1 << Opt_hidepid))
fs_info->hide_pid = ctx->hidepid;
- if (ctx->mask & (1 << Opt_subset))
- fs_info->pidonly = ctx->pidonly;
+ if (ctx->mask & (1 << Opt_subset)) {
+ kfree(fs_info->whitelist);
+ fs_info->whitelist = ctx->whitelist;
+ fs_info->pids_visibility = ctx->pids_visibility;
+ }
}

static int proc_fill_super(struct super_block *s, struct fs_context *fc)
@@ -270,6 +319,8 @@ static void proc_kill_sb(struct super_block *sb)
if (fs_info->proc_thread_self)
dput(fs_info->proc_thread_self);

+ kfree(fs_info->whitelist);
+
kill_anon_super(sb);
put_pid_ns(fs_info->pid_ns);
kfree(fs_info);
diff --git a/include/linux/proc_fs.h b/include/linux/proc_fs.h
index 6ec524d8842c..74d56ddb67b6 100644
--- a/include/linux/proc_fs.h
+++ b/include/linux/proc_fs.h
@@ -50,10 +50,10 @@ enum proc_hidepid {
HIDEPID_NOT_PTRACEABLE = 4, /* Limit pids to only ptraceable pids */
};

-/* definitions for proc mount option pidonly */
-enum proc_pidonly {
- PROC_PIDONLY_OFF = 0,
- PROC_PIDONLY_ON = 1,
+/* definitions for pid visibility */
+enum proc_pids_visible {
+ PROC_PIDS_VISIBLE = 0,
+ PROC_PIDS_INVISIBLE = 1,
};

struct proc_fs_info {
@@ -62,7 +62,8 @@ struct proc_fs_info {
struct dentry *proc_thread_self; /* For /proc/thread-self */
kgid_t pid_gid;
enum proc_hidepid hide_pid;
- enum proc_pidonly pidonly;
+ enum proc_pids_visible pids_visibility;
+ char **whitelist;
};

static inline struct proc_fs_info *proc_sb_info(struct super_block *sb)
--
2.25.4