![Vasiliy Kulikov](/assets/img/avatar_default.png)
Add support for mount options to restrict access to /proc/PID/ directories. The default backward-compatible "relaxed" behaviour is left untouched. The first mount option is called "hidepid" and its value defines how much info about processes we want to be available for non-owners: hidepid=0 (default) means the old behavior - anybody may read all world-readable /proc/PID/* files. hidepid=1 means users may not access any /proc/<pid>/ directories, but their own. Sensitive files like cmdline, sched*, status are now protected against other users. As permission checking done in proc_pid_permission() and files' permissions are left untouched, programs expecting specific files' modes are not confused. hidepid=2 means hidepid=1 plus all /proc/PID/ will be invisible to other users. It doesn't mean that it hides whether a process exists (it can be learned by other means, e.g. by kill -0 $PID), but it hides process' euid and egid. It compicates intruder's task of gathering info about running processes, whether some daemon runs with elevated privileges, whether another user runs some sensitive program, whether other users run any program at all, etc. gid=XXX defines a group that will be able to gather all processes' info (as in hidepid=0 mode). This group should be used instead of putting nonroot user in sudoers file or something. However, untrusted users (like daemons, etc.) which are not supposed to monitor the tasks in the whole system should not be added to the group. hidepid=1 or higher is designed to restrict access to procfs files, which might reveal some sensitive private information like precise keystrokes timings: http://www.openwall.com/lists/oss-security/2011/11/05/3 hidepid=1/2 doesn't break monitoring userspace tools. ps, top, pgrep, and conky gracefully handle EPERM/ENOENT and behave as if the current user is the only user running processes. pstree shows the process subtree which contains "pstree" process. Note: the patch doesn't deal with setuid/setgid issues of keeping preopened descriptors of procfs files (like https://lkml.org/lkml/2011/2/7/368). We rely on that the leaked information like the scheduling counters of setuid apps doesn't threaten anybody's privacy - only the user started the setuid program may read the counters. Signed-off-by: Vasiliy Kulikov <segoon@openwall.com> Cc: Alexey Dobriyan <adobriyan@gmail.com> Cc: Al Viro <viro@zeniv.linux.org.uk> Cc: Randy Dunlap <rdunlap@xenotime.net> Cc: "H. Peter Anvin" <hpa@zytor.com> Cc: Greg KH <greg@kroah.com> Cc: Theodore Tso <tytso@MIT.EDU> Cc: Alan Cox <alan@lxorguk.ukuu.org.uk> Cc: James Morris <jmorris@namei.org> Cc: Oleg Nesterov <oleg@redhat.com> Cc: Hugh Dickins <hughd@google.com> Signed-off-by: Andrew Morton <akpm@linux-foundation.org> Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
515 lines
12 KiB
C
515 lines
12 KiB
C
/*
|
|
* linux/fs/proc/inode.c
|
|
*
|
|
* Copyright (C) 1991, 1992 Linus Torvalds
|
|
*/
|
|
|
|
#include <linux/time.h>
|
|
#include <linux/proc_fs.h>
|
|
#include <linux/kernel.h>
|
|
#include <linux/pid_namespace.h>
|
|
#include <linux/mm.h>
|
|
#include <linux/string.h>
|
|
#include <linux/stat.h>
|
|
#include <linux/completion.h>
|
|
#include <linux/poll.h>
|
|
#include <linux/file.h>
|
|
#include <linux/limits.h>
|
|
#include <linux/init.h>
|
|
#include <linux/module.h>
|
|
#include <linux/sysctl.h>
|
|
#include <linux/seq_file.h>
|
|
#include <linux/slab.h>
|
|
#include <linux/mount.h>
|
|
|
|
#include <asm/system.h>
|
|
#include <asm/uaccess.h>
|
|
|
|
#include "internal.h"
|
|
|
|
static void proc_evict_inode(struct inode *inode)
|
|
{
|
|
struct proc_dir_entry *de;
|
|
struct ctl_table_header *head;
|
|
const struct proc_ns_operations *ns_ops;
|
|
|
|
truncate_inode_pages(&inode->i_data, 0);
|
|
end_writeback(inode);
|
|
|
|
/* Stop tracking associated processes */
|
|
put_pid(PROC_I(inode)->pid);
|
|
|
|
/* Let go of any associated proc directory entry */
|
|
de = PROC_I(inode)->pde;
|
|
if (de)
|
|
pde_put(de);
|
|
head = PROC_I(inode)->sysctl;
|
|
if (head) {
|
|
rcu_assign_pointer(PROC_I(inode)->sysctl, NULL);
|
|
sysctl_head_put(head);
|
|
}
|
|
/* Release any associated namespace */
|
|
ns_ops = PROC_I(inode)->ns_ops;
|
|
if (ns_ops && ns_ops->put)
|
|
ns_ops->put(PROC_I(inode)->ns);
|
|
}
|
|
|
|
static struct kmem_cache * proc_inode_cachep;
|
|
|
|
static struct inode *proc_alloc_inode(struct super_block *sb)
|
|
{
|
|
struct proc_inode *ei;
|
|
struct inode *inode;
|
|
|
|
ei = (struct proc_inode *)kmem_cache_alloc(proc_inode_cachep, GFP_KERNEL);
|
|
if (!ei)
|
|
return NULL;
|
|
ei->pid = NULL;
|
|
ei->fd = 0;
|
|
ei->op.proc_get_link = NULL;
|
|
ei->pde = NULL;
|
|
ei->sysctl = NULL;
|
|
ei->sysctl_entry = NULL;
|
|
ei->ns = NULL;
|
|
ei->ns_ops = NULL;
|
|
inode = &ei->vfs_inode;
|
|
inode->i_mtime = inode->i_atime = inode->i_ctime = CURRENT_TIME;
|
|
return inode;
|
|
}
|
|
|
|
static void proc_i_callback(struct rcu_head *head)
|
|
{
|
|
struct inode *inode = container_of(head, struct inode, i_rcu);
|
|
kmem_cache_free(proc_inode_cachep, PROC_I(inode));
|
|
}
|
|
|
|
static void proc_destroy_inode(struct inode *inode)
|
|
{
|
|
call_rcu(&inode->i_rcu, proc_i_callback);
|
|
}
|
|
|
|
static void init_once(void *foo)
|
|
{
|
|
struct proc_inode *ei = (struct proc_inode *) foo;
|
|
|
|
inode_init_once(&ei->vfs_inode);
|
|
}
|
|
|
|
void __init proc_init_inodecache(void)
|
|
{
|
|
proc_inode_cachep = kmem_cache_create("proc_inode_cache",
|
|
sizeof(struct proc_inode),
|
|
0, (SLAB_RECLAIM_ACCOUNT|
|
|
SLAB_MEM_SPREAD|SLAB_PANIC),
|
|
init_once);
|
|
}
|
|
|
|
static int proc_show_options(struct seq_file *seq, struct dentry *root)
|
|
{
|
|
struct super_block *sb = root->d_sb;
|
|
struct pid_namespace *pid = sb->s_fs_info;
|
|
|
|
if (pid->pid_gid)
|
|
seq_printf(seq, ",gid=%lu", (unsigned long)pid->pid_gid);
|
|
if (pid->hide_pid != 0)
|
|
seq_printf(seq, ",hidepid=%u", pid->hide_pid);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static const struct super_operations proc_sops = {
|
|
.alloc_inode = proc_alloc_inode,
|
|
.destroy_inode = proc_destroy_inode,
|
|
.drop_inode = generic_delete_inode,
|
|
.evict_inode = proc_evict_inode,
|
|
.statfs = simple_statfs,
|
|
.remount_fs = proc_remount,
|
|
.show_options = proc_show_options,
|
|
};
|
|
|
|
static void __pde_users_dec(struct proc_dir_entry *pde)
|
|
{
|
|
pde->pde_users--;
|
|
if (pde->pde_unload_completion && pde->pde_users == 0)
|
|
complete(pde->pde_unload_completion);
|
|
}
|
|
|
|
void pde_users_dec(struct proc_dir_entry *pde)
|
|
{
|
|
spin_lock(&pde->pde_unload_lock);
|
|
__pde_users_dec(pde);
|
|
spin_unlock(&pde->pde_unload_lock);
|
|
}
|
|
|
|
static loff_t proc_reg_llseek(struct file *file, loff_t offset, int whence)
|
|
{
|
|
struct proc_dir_entry *pde = PDE(file->f_path.dentry->d_inode);
|
|
loff_t rv = -EINVAL;
|
|
loff_t (*llseek)(struct file *, loff_t, int);
|
|
|
|
spin_lock(&pde->pde_unload_lock);
|
|
/*
|
|
* remove_proc_entry() is going to delete PDE (as part of module
|
|
* cleanup sequence). No new callers into module allowed.
|
|
*/
|
|
if (!pde->proc_fops) {
|
|
spin_unlock(&pde->pde_unload_lock);
|
|
return rv;
|
|
}
|
|
/*
|
|
* Bump refcount so that remove_proc_entry will wail for ->llseek to
|
|
* complete.
|
|
*/
|
|
pde->pde_users++;
|
|
/*
|
|
* Save function pointer under lock, to protect against ->proc_fops
|
|
* NULL'ifying right after ->pde_unload_lock is dropped.
|
|
*/
|
|
llseek = pde->proc_fops->llseek;
|
|
spin_unlock(&pde->pde_unload_lock);
|
|
|
|
if (!llseek)
|
|
llseek = default_llseek;
|
|
rv = llseek(file, offset, whence);
|
|
|
|
pde_users_dec(pde);
|
|
return rv;
|
|
}
|
|
|
|
static ssize_t proc_reg_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
|
|
{
|
|
struct proc_dir_entry *pde = PDE(file->f_path.dentry->d_inode);
|
|
ssize_t rv = -EIO;
|
|
ssize_t (*read)(struct file *, char __user *, size_t, loff_t *);
|
|
|
|
spin_lock(&pde->pde_unload_lock);
|
|
if (!pde->proc_fops) {
|
|
spin_unlock(&pde->pde_unload_lock);
|
|
return rv;
|
|
}
|
|
pde->pde_users++;
|
|
read = pde->proc_fops->read;
|
|
spin_unlock(&pde->pde_unload_lock);
|
|
|
|
if (read)
|
|
rv = read(file, buf, count, ppos);
|
|
|
|
pde_users_dec(pde);
|
|
return rv;
|
|
}
|
|
|
|
static ssize_t proc_reg_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
|
|
{
|
|
struct proc_dir_entry *pde = PDE(file->f_path.dentry->d_inode);
|
|
ssize_t rv = -EIO;
|
|
ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *);
|
|
|
|
spin_lock(&pde->pde_unload_lock);
|
|
if (!pde->proc_fops) {
|
|
spin_unlock(&pde->pde_unload_lock);
|
|
return rv;
|
|
}
|
|
pde->pde_users++;
|
|
write = pde->proc_fops->write;
|
|
spin_unlock(&pde->pde_unload_lock);
|
|
|
|
if (write)
|
|
rv = write(file, buf, count, ppos);
|
|
|
|
pde_users_dec(pde);
|
|
return rv;
|
|
}
|
|
|
|
static unsigned int proc_reg_poll(struct file *file, struct poll_table_struct *pts)
|
|
{
|
|
struct proc_dir_entry *pde = PDE(file->f_path.dentry->d_inode);
|
|
unsigned int rv = DEFAULT_POLLMASK;
|
|
unsigned int (*poll)(struct file *, struct poll_table_struct *);
|
|
|
|
spin_lock(&pde->pde_unload_lock);
|
|
if (!pde->proc_fops) {
|
|
spin_unlock(&pde->pde_unload_lock);
|
|
return rv;
|
|
}
|
|
pde->pde_users++;
|
|
poll = pde->proc_fops->poll;
|
|
spin_unlock(&pde->pde_unload_lock);
|
|
|
|
if (poll)
|
|
rv = poll(file, pts);
|
|
|
|
pde_users_dec(pde);
|
|
return rv;
|
|
}
|
|
|
|
static long proc_reg_unlocked_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
|
|
{
|
|
struct proc_dir_entry *pde = PDE(file->f_path.dentry->d_inode);
|
|
long rv = -ENOTTY;
|
|
long (*ioctl)(struct file *, unsigned int, unsigned long);
|
|
|
|
spin_lock(&pde->pde_unload_lock);
|
|
if (!pde->proc_fops) {
|
|
spin_unlock(&pde->pde_unload_lock);
|
|
return rv;
|
|
}
|
|
pde->pde_users++;
|
|
ioctl = pde->proc_fops->unlocked_ioctl;
|
|
spin_unlock(&pde->pde_unload_lock);
|
|
|
|
if (ioctl)
|
|
rv = ioctl(file, cmd, arg);
|
|
|
|
pde_users_dec(pde);
|
|
return rv;
|
|
}
|
|
|
|
#ifdef CONFIG_COMPAT
|
|
static long proc_reg_compat_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
|
|
{
|
|
struct proc_dir_entry *pde = PDE(file->f_path.dentry->d_inode);
|
|
long rv = -ENOTTY;
|
|
long (*compat_ioctl)(struct file *, unsigned int, unsigned long);
|
|
|
|
spin_lock(&pde->pde_unload_lock);
|
|
if (!pde->proc_fops) {
|
|
spin_unlock(&pde->pde_unload_lock);
|
|
return rv;
|
|
}
|
|
pde->pde_users++;
|
|
compat_ioctl = pde->proc_fops->compat_ioctl;
|
|
spin_unlock(&pde->pde_unload_lock);
|
|
|
|
if (compat_ioctl)
|
|
rv = compat_ioctl(file, cmd, arg);
|
|
|
|
pde_users_dec(pde);
|
|
return rv;
|
|
}
|
|
#endif
|
|
|
|
static int proc_reg_mmap(struct file *file, struct vm_area_struct *vma)
|
|
{
|
|
struct proc_dir_entry *pde = PDE(file->f_path.dentry->d_inode);
|
|
int rv = -EIO;
|
|
int (*mmap)(struct file *, struct vm_area_struct *);
|
|
|
|
spin_lock(&pde->pde_unload_lock);
|
|
if (!pde->proc_fops) {
|
|
spin_unlock(&pde->pde_unload_lock);
|
|
return rv;
|
|
}
|
|
pde->pde_users++;
|
|
mmap = pde->proc_fops->mmap;
|
|
spin_unlock(&pde->pde_unload_lock);
|
|
|
|
if (mmap)
|
|
rv = mmap(file, vma);
|
|
|
|
pde_users_dec(pde);
|
|
return rv;
|
|
}
|
|
|
|
static int proc_reg_open(struct inode *inode, struct file *file)
|
|
{
|
|
struct proc_dir_entry *pde = PDE(inode);
|
|
int rv = 0;
|
|
int (*open)(struct inode *, struct file *);
|
|
int (*release)(struct inode *, struct file *);
|
|
struct pde_opener *pdeo;
|
|
|
|
/*
|
|
* What for, you ask? Well, we can have open, rmmod, remove_proc_entry
|
|
* sequence. ->release won't be called because ->proc_fops will be
|
|
* cleared. Depending on complexity of ->release, consequences vary.
|
|
*
|
|
* We can't wait for mercy when close will be done for real, it's
|
|
* deadlockable: rmmod foo </proc/foo . So, we're going to do ->release
|
|
* by hand in remove_proc_entry(). For this, save opener's credentials
|
|
* for later.
|
|
*/
|
|
pdeo = kmalloc(sizeof(struct pde_opener), GFP_KERNEL);
|
|
if (!pdeo)
|
|
return -ENOMEM;
|
|
|
|
spin_lock(&pde->pde_unload_lock);
|
|
if (!pde->proc_fops) {
|
|
spin_unlock(&pde->pde_unload_lock);
|
|
kfree(pdeo);
|
|
return -ENOENT;
|
|
}
|
|
pde->pde_users++;
|
|
open = pde->proc_fops->open;
|
|
release = pde->proc_fops->release;
|
|
spin_unlock(&pde->pde_unload_lock);
|
|
|
|
if (open)
|
|
rv = open(inode, file);
|
|
|
|
spin_lock(&pde->pde_unload_lock);
|
|
if (rv == 0 && release) {
|
|
/* To know what to release. */
|
|
pdeo->inode = inode;
|
|
pdeo->file = file;
|
|
/* Strictly for "too late" ->release in proc_reg_release(). */
|
|
pdeo->release = release;
|
|
list_add(&pdeo->lh, &pde->pde_openers);
|
|
} else
|
|
kfree(pdeo);
|
|
__pde_users_dec(pde);
|
|
spin_unlock(&pde->pde_unload_lock);
|
|
return rv;
|
|
}
|
|
|
|
static struct pde_opener *find_pde_opener(struct proc_dir_entry *pde,
|
|
struct inode *inode, struct file *file)
|
|
{
|
|
struct pde_opener *pdeo;
|
|
|
|
list_for_each_entry(pdeo, &pde->pde_openers, lh) {
|
|
if (pdeo->inode == inode && pdeo->file == file)
|
|
return pdeo;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static int proc_reg_release(struct inode *inode, struct file *file)
|
|
{
|
|
struct proc_dir_entry *pde = PDE(inode);
|
|
int rv = 0;
|
|
int (*release)(struct inode *, struct file *);
|
|
struct pde_opener *pdeo;
|
|
|
|
spin_lock(&pde->pde_unload_lock);
|
|
pdeo = find_pde_opener(pde, inode, file);
|
|
if (!pde->proc_fops) {
|
|
/*
|
|
* Can't simply exit, __fput() will think that everything is OK,
|
|
* and move on to freeing struct file. remove_proc_entry() will
|
|
* find slacker in opener's list and will try to do non-trivial
|
|
* things with struct file. Therefore, remove opener from list.
|
|
*
|
|
* But if opener is removed from list, who will ->release it?
|
|
*/
|
|
if (pdeo) {
|
|
list_del(&pdeo->lh);
|
|
spin_unlock(&pde->pde_unload_lock);
|
|
rv = pdeo->release(inode, file);
|
|
kfree(pdeo);
|
|
} else
|
|
spin_unlock(&pde->pde_unload_lock);
|
|
return rv;
|
|
}
|
|
pde->pde_users++;
|
|
release = pde->proc_fops->release;
|
|
if (pdeo) {
|
|
list_del(&pdeo->lh);
|
|
kfree(pdeo);
|
|
}
|
|
spin_unlock(&pde->pde_unload_lock);
|
|
|
|
if (release)
|
|
rv = release(inode, file);
|
|
|
|
pde_users_dec(pde);
|
|
return rv;
|
|
}
|
|
|
|
static const struct file_operations proc_reg_file_ops = {
|
|
.llseek = proc_reg_llseek,
|
|
.read = proc_reg_read,
|
|
.write = proc_reg_write,
|
|
.poll = proc_reg_poll,
|
|
.unlocked_ioctl = proc_reg_unlocked_ioctl,
|
|
#ifdef CONFIG_COMPAT
|
|
.compat_ioctl = proc_reg_compat_ioctl,
|
|
#endif
|
|
.mmap = proc_reg_mmap,
|
|
.open = proc_reg_open,
|
|
.release = proc_reg_release,
|
|
};
|
|
|
|
#ifdef CONFIG_COMPAT
|
|
static const struct file_operations proc_reg_file_ops_no_compat = {
|
|
.llseek = proc_reg_llseek,
|
|
.read = proc_reg_read,
|
|
.write = proc_reg_write,
|
|
.poll = proc_reg_poll,
|
|
.unlocked_ioctl = proc_reg_unlocked_ioctl,
|
|
.mmap = proc_reg_mmap,
|
|
.open = proc_reg_open,
|
|
.release = proc_reg_release,
|
|
};
|
|
#endif
|
|
|
|
struct inode *proc_get_inode(struct super_block *sb, struct proc_dir_entry *de)
|
|
{
|
|
struct inode * inode;
|
|
|
|
inode = iget_locked(sb, de->low_ino);
|
|
if (!inode)
|
|
return NULL;
|
|
if (inode->i_state & I_NEW) {
|
|
inode->i_mtime = inode->i_atime = inode->i_ctime = CURRENT_TIME;
|
|
PROC_I(inode)->fd = 0;
|
|
PROC_I(inode)->pde = de;
|
|
|
|
if (de->mode) {
|
|
inode->i_mode = de->mode;
|
|
inode->i_uid = de->uid;
|
|
inode->i_gid = de->gid;
|
|
}
|
|
if (de->size)
|
|
inode->i_size = de->size;
|
|
if (de->nlink)
|
|
set_nlink(inode, de->nlink);
|
|
if (de->proc_iops)
|
|
inode->i_op = de->proc_iops;
|
|
if (de->proc_fops) {
|
|
if (S_ISREG(inode->i_mode)) {
|
|
#ifdef CONFIG_COMPAT
|
|
if (!de->proc_fops->compat_ioctl)
|
|
inode->i_fop =
|
|
&proc_reg_file_ops_no_compat;
|
|
else
|
|
#endif
|
|
inode->i_fop = &proc_reg_file_ops;
|
|
} else {
|
|
inode->i_fop = de->proc_fops;
|
|
}
|
|
}
|
|
unlock_new_inode(inode);
|
|
} else
|
|
pde_put(de);
|
|
return inode;
|
|
}
|
|
|
|
int proc_fill_super(struct super_block *s)
|
|
{
|
|
struct inode * root_inode;
|
|
|
|
s->s_flags |= MS_NODIRATIME | MS_NOSUID | MS_NOEXEC;
|
|
s->s_blocksize = 1024;
|
|
s->s_blocksize_bits = 10;
|
|
s->s_magic = PROC_SUPER_MAGIC;
|
|
s->s_op = &proc_sops;
|
|
s->s_time_gran = 1;
|
|
|
|
pde_get(&proc_root);
|
|
root_inode = proc_get_inode(s, &proc_root);
|
|
if (!root_inode)
|
|
goto out_no_root;
|
|
root_inode->i_uid = 0;
|
|
root_inode->i_gid = 0;
|
|
s->s_root = d_alloc_root(root_inode);
|
|
if (!s->s_root)
|
|
goto out_no_root;
|
|
return 0;
|
|
|
|
out_no_root:
|
|
printk("proc_read_super: get root inode failed\n");
|
|
iput(root_inode);
|
|
pde_put(&proc_root);
|
|
return -ENOMEM;
|
|
}
|