Skip to contentSkip to conversation

procfs: add privacy options

RFC PATCH In review Rene Scharfe 3 participants 5 messages Jul 24, 2006 – Jul 27, 2006

Subsystems (3)
lore
Share & tools
lore
#0 lore raw

RFC PATCH 1/1 procfs: add privacy options

Roughly a year ago I sent out a few patches intended to give normal users a bit of privacy in their parts the /proc filesystem. The first incarnations were described as rootkits, later ones met a bit less resistance. :-D Then I got distracted; the patches never went anywhere. Now that Wolfgang Draxinger asked about something like it, I think it's time to revive the thing.

So I dusted off the last version and ported it to 2.6.18-rc2. It's inspired by the Openwall kernel option CONFIG_HARDEN_PROC. The patch introduces two kernel boot options, proc.privacy and proc.gid. They can be used to restrict visibility of process details for regular users.

Setting proc.privacy to 2 let's users only enter their own /proc/<pid> directories, while a setting of 1 allows them to enter root's process dirs, too. In this way tools like pstree keep working, because all parents of a user process up to init (e.g. sshd, getty, init itself) keep being visible. It's a rough heuristic, but I think it makes sense:
root can alway see you, and in turn you can see root. If root is shy he can set proc.privacy=2; the price is that his users get slightly strange results from pstools etc.

proc.gid is the GID of the group that has read and execute access to all /proc/<pid> dirs, regardless what the file mode and group ownership says. Unlike in Openwall and in my previous attempts I implemented it as a .permission function this time. That means owner and group attributes of /proc/<pid> dirs are not changed; tools like top and ps can still gather euid and egid that way.

Normally .permission functions are not the way to implement access control in procfs, because the condition on which to grant/deny access could change between open and actual access (think setuid). This is of no concern in the case of this patch, because the it unconditionally grants some access to the members of one group, independent from /proc data or meta-data.

I briefly tested the patch on a Fedora Core 6 test1 system on top of a vanilla 2.6.18-rc2 kernel and it seems to work as described here: it boots, it restricts, and if I'm in the right group I see every process in ps' output again.

Questions, suggestions or even flames are very much welcome. Did I manage to do something stupid in these few lines of code?

Andrew, are you (still) interested in test-driving the patch in -mm?

Documentation/kernel-parameters.txt +10 -0
index e11f772..c4de276 100644
@@ -1284,6 +1284,16 @@ running once the system is up.
 			Param: <number> - step/bucket size as a power of 2 for
 				statistical time based profiling.
 
+	proc.gid=	[KNL] ID of the group that is allowed to read and
+			enter all /proc/<pid> directories, regardless of the
+			proc.privacy setting (i.e., the process admin group).
+			Default is 0.
+	proc.privacy=	[KNL] Restrict access to the /proc/<pid> directories
+			to their respective owners ("chmod 500").
+			0 = disabled (default), 1 = restrict access to all
+			process dirs except those owned by root (uid 0),
+			2 = restrict access to all process dirs.
+
 	processor.max_cstate=	[HW,ACPI]
 			Limit processor to maximum C-state
 			max_cstate=9 overrides any DMI blacklist limit.
fs/proc/base.c +49 -0

100 lines not on this page · raw message

#1 lore raw
Quoted 35 lines · Rene Scharfe

Roughly a year ago I sent out a few patches intended to give normal users a bit of privacy in their parts the /proc filesystem. The first incarnations were described as rootkits, later ones met a bit less resistance. :-D Then I got distracted; the patches never went anywhere. Now that Wolfgang Draxinger asked about something like it, I think it's time to revive the thing.

So I dusted off the last version and ported it to 2.6.18-rc2. It's inspired by the Openwall kernel option CONFIG_HARDEN_PROC. The patch introduces two kernel boot options, proc.privacy and proc.gid. They can be used to restrict visibility of process details for regular users.

Setting proc.privacy to 2 let's users only enter their own /proc/<pid> directories, while a setting of 1 allows them to enter root's process dirs, too. In this way tools like pstree keep working, because all parents of a user process up to init (e.g. sshd, getty, init itself) keep being visible. It's a rough heuristic, but I think it makes sense:
root can alway see you, and in turn you can see root. If root is shy he can set proc.privacy=2; the price is that his users get slightly strange results from pstools etc.

proc.gid is the GID of the group that has read and execute access to all /proc/<pid> dirs, regardless what the file mode and group ownership says. Unlike in Openwall and in my previous attempts I implemented it as a .permission function this time. That means owner and group attributes of /proc/<pid> dirs are not changed; tools like top and ps can still gather euid and egid that way.

Normally .permission functions are not the way to implement access control in procfs, because the condition on which to grant/deny access could change between open and actual access (think setuid). This is of no concern in the case of this patch, because the it unconditionally grants some access to the members of one group, independent from /proc data or meta-data.

Quoted 7 lines

I briefly tested the patch on a Fedora Core 6 test1 system on top of a vanilla 2.6.18-rc2 kernel and it seems to work as described here: it boots, it restricts, and if I'm in the right group I see every process in ps' output again.

Questions, suggestions or even flames are very much welcome. Did I manage to do something stupid in these few lines of code?

I don't really like filesystem magic options as kernel boot time options. Mount time or runtime options are probably more interesting.

How is it expected that users will use this?

A lot of the privacy you are talking about is provided by the may_ptrace checks in the more sensitive parts of proc so we may want to extend that.

Eric

#2 lore raw
Quoted 4 lines · Eric W. Biederman

I don't really like filesystem magic options as kernel boot time options. Mount time or runtime options are probably more interesting.

How is it expected that users will use this?

I don't expect admins to switch "privacy" on and off very often. Once would be enough, I hope.

Mount options would be easier to use, I agree, but I doubt the added complexity is worth it. Kernel options for procfs are not _that_ magical because the kernel mounts it internally, so it's a kernel part, not a real filesystem ;-)

One question I couldn't find a good answer for regarding remount
options: what to do with processes that have cd'd into a /proc/<pid> dir belonging to another user when the privacy option is being turned on? Letting them keep their access is counter-intuitive and taking it away would need quite invasive changes compared to my patch, I think.

Quoted 3 lines

A lot of the privacy you are talking about is provided by the may_ptrace checks in the more sensitive parts of proc so we may want to extend that.

You mean using ptrace_may_attach() and/or MAY_PTRACE() for determining access to all (or at least more) files in /proc/<pid> instead of my proposed "chmod 500"? What are the advantages?

Thanks,
René

#3 lore raw
Quoted 5 lines

You mean using ptrace_may_attach() and/or MAY_PTRACE() for determining access to all (or at least more) files in /proc/<pid> instead of my proposed "chmod 500"? What are the advantages?

Hi,

file permissions are simple, but too simplistic to express a full "am I allowed to see that guy" rules as general principle. Just think of SELinux or any other kind of role based access control mechanism where "root is not full root". But it goes beyond that really; applications that drop their ptrace capability because they KNOW they won't use it and by dropping the capability deny an exploit that takes over from using it. There's just too many such cases that file permissions don't capture where ptrace is a more detailed description of the permission check you want (idea being that if you can ptrace someone you own them). It's good general principle to at least try to not duplicate such permission checks; history shows that those will be gotten incorrect.

Greetings,
Arjan van de Ven

Signature · 1 line
if you want to mail me at work (you don't), use arjan (at) linux.intel.com
#4 lore raw

[I had used a bouncing address for Al Viro, hopefully this one is correct.]

Quoted 4 lines · Arjan van de Ven
Quoted 3 lines

You mean using ptrace_may_attach() and/or MAY_PTRACE() for determining access to all (or at least more) files in /proc/<pid> instead of my proposed "chmod 500"? What are the advantages?

Quoted 11 lines

file permissions are simple, but too simplistic to express a full "am I allowed to see that guy" rules as general principle. Just think of SELinux or any other kind of role based access control mechanism where "root is not full root". But it goes beyond that really; applications that drop their ptrace capability because they KNOW they won't use it and by dropping the capability deny an exploit that takes over from using it. There's just too many such cases that file permissions don't capture where ptrace is a more detailed description of the permission check you want (idea being that if you can ptrace someone you own them). It's good general principle to at least try to not duplicate such permission checks; history shows that those will be gotten incorrect.

OK, makes sense. What do y'all think about the following attempt?

It is configurable via mount options; it should work even if someone has opened a proc file before the admin changed the option, because the permissions are checked at every read.

There are two pairs of options: privacy/noprivacy and gid=n/nogid. The first one can be used to prevent all users from looking into /proc/<pid> files for processes hat they arent't allowed to ptrace. The second one can be used to lift this restriction from one user group.

I'm not sure whether the llseek checks are really needed. Is the file length really worth protecting?

I'm also not sure if "gid" is a good option name. There once was a procfs option that did something else. Is it OK to recycle the name? Would "pgid" be better? Or "nonprivacygid"?

For the loginuid and oom_adjust files I made sure that those users who are allowed to write (err, not forbidden to write) are allowed to read them, too, regardless of the result of ptrace_may_attach().

Unlike my previous patch this one has only two modes: on and off. If privacy is on, pstree stops working for normal users. This could be worked around with a "soft" privacy mode again by taking any privacy away from root-owned processes, if needed.

I tested the patch lightly by booting Fedora Core 6 test1 and turning the options on and off while watching top. One thing I noticed is that the normal boot process doesn't seem to pick up mount options from /etc/fstab for /proc.

The patch doesn't change any permission bits, that behaviour might be confusing. On the other hand, I already _am_ confused by symlinks that I can't get the target for (cwd, root, exe) ;-). Is this worth fixing?

Doing the permission checks also at open seems like a good idea, too.

Am I on the right track?
Ren�

fs/proc/base.c +82 -9
index fe8d55f..cdf5743 100644
@@ -572,6 +572,42 @@ static struct inode_operations proc_def_
 	.setattr	= proc_setattr,
 };
 
+#define PROC_PRIVACY (proc_config.privacy && \
+	!(proc_config.usegid && in_group_p(proc_config.gid)))
+
+ssize_t proc_seq_read(struct file *file, char __user *buf, size_t size,
+                      loff_t *ppos)
+{
+	if (PROC_PRIVACY) {
+		struct inode * inode = file->f_dentry->d_inode;
+		struct task_struct *task = get_proc_task(inode);
+
+		if (task && !ptrace_may_attach(task)) {
+			put_task_struct(task);
+			return -EPERM;
+		}
+		put_task_struct(task);
+	}
+
+	return seq_read(file, buf, size, ppos);
+}
+
+loff_t proc_seq_lseek(struct file *file, loff_t offset, int origin)
+{
+	if (PROC_PRIVACY) {
+		struct inode * inode = file->f_dentry->d_inode;
+		struct task_struct *task = get_proc_task(inode);
+
+		if (task && !ptrace_may_attach(task)) {
+			put_task_struct(task);
+			return -EPERM;
+		}
+		put_task_struct(task);
+	}
+
+	return seq_lseek(file, offset, origin);
+}
+
 extern struct seq_operations mounts_op;
 struct proc_mounts {
 	struct seq_file m;
@@ -640,8 +676,8 @@ static unsigned mounts_poll(struct file 
 
 static struct file_operations proc_mounts_operations = {
 	.open		= mounts_open,
-	.read		= seq_read,
-	.llseek		= seq_lseek,
+	.read		= proc_seq_read,
+	.llseek		= proc_seq_lseek,
 	.release	= mounts_release,
 	.poll		= mounts_poll,
 };
@@ -677,8 +713,8 @@ static int mountstats_open(struct inode 
 
 static struct file_operations proc_mountstats_operations = {
 	.open		= mountstats_open,
-	.read		= seq_read,
-	.llseek		= seq_lseek,
+	.read		= proc_seq_read,
+	.llseek		= proc_seq_lseek,
 	.release	= mounts_release,
 };
 
@@ -696,6 +732,12 @@ static ssize_t proc_info_read(struct fil
 	if (!task)
 		goto out_no_task;
 
+	if (PROC_PRIVACY) {
+		length = -EPERM;
+		if (!ptrace_may_attach(task))
+			goto out;
+	}
+
 	if (count > PROC_BLOCK_SIZE)
 		count = PROC_BLOCK_SIZE;
 
@@ -865,6 +907,11 @@ static struct file_operations proc_mem_o
 	.open		= mem_open,
 };
 
+static int oom_adjust_may_write(void)
+{
+	return capable(CAP_SYS_RESOURCE);
+}
+
 static ssize_t oom_adjust_read(struct file *file, char __user *buf,
 				size_t count, loff_t *ppos)
 {
@@ -876,6 +923,14 @@ static ssize_t oom_adjust_read(struct fi
 
 	if (!task)
 		return -ESRCH;
+
+	if (PROC_PRIVACY) {
+		if (!oom_adjust_may_write() && !ptrace_may_attach(task)) {
+			put_task_struct(task);
+			return -EPERM;
+		}
+	}
+
 	oom_adjust = task->oomkilladj;
 	put_task_struct(task);
 
@@ -897,7 +952,7 @@ static ssize_t oom_adjust_write(struct f
 	char buffer[PROC_NUMBUF], *end;
 	int oom_adjust;
 
-	if (!capable(CAP_SYS_RESOURCE))
+	if (!oom_adjust_may_write())
 		return -EPERM;
 	memset(buffer, 0, sizeof(buffer));
 	if (count > sizeof(buffer) - 1)
@@ -926,6 +981,12 @@ static struct file_operations proc_oom_a
 
 #ifdef CONFIG_AUDITSYSCALL
 #define TMPBUFLEN 21
+static int proc_loginuid_may_write(struct inode *inode)
+{
+	return capable(CAP_AUDIT_CONTROL) &&
+	       (current == pid_task(proc_pid(inode), PIDTYPE_PID));
+}
+
 static ssize_t proc_loginuid_read(struct file * file, char __user * buf,
 				  size_t count, loff_t *ppos)
 {
@@ -936,6 +997,15 @@ static ssize_t proc_loginuid_read(struct
 
 	if (!task)
 		return -ESRCH;
+
+	if (PROC_PRIVACY) {
+		if (!proc_loginuid_may_write(inode) &&
+		    !ptrace_may_attach(task)) {
+			put_task_struct(task);
+			return -EPERM;
+		}
+	}
+
 	length = scnprintf(tmpbuf, TMPBUFLEN, "%u",
 				audit_get_loginuid(task->audit_context));
 	put_task_struct(task);
@@ -950,10 +1020,7 @@ static ssize_t proc_loginuid_write(struc
 	ssize_t length;
 	uid_t loginuid;
 
-	if (!capable(CAP_AUDIT_CONTROL))
-		return -EPERM;
-
-	if (current != pid_task(proc_pid(inode), PIDTYPE_PID))
+	if (!proc_loginuid_may_write(inode))
 		return -EPERM;
 
 	if (count >= PAGE_SIZE)
@@ -1574,6 +1641,12 @@ static ssize_t proc_pid_attr_read(struct
 	if (!task)
 		goto out_no_task;
 
+	if (PROC_PRIVACY) {
+		length = -EPERM;
+		if (!ptrace_may_attach(task))
+			goto out;
+	}
+
 	if (count > PAGE_SIZE)
 		count = PAGE_SIZE;
 	length = -ENOMEM;
fs/proc/inode.c +64 -0

85 lines not on this page · raw message

fs/proc/task_mmu.c +6 -6
index 0a163a4..9693193 100644
@@ -441,8 +441,8 @@ static int maps_open(struct inode *inode
 
 struct file_operations proc_maps_operations = {
 	.open		= maps_open,
-	.read		= seq_read,
-	.llseek		= seq_lseek,
+	.read		= proc_seq_read,
+	.llseek		= proc_seq_lseek,
 	.release	= seq_release_private,
 };
 
@@ -463,8 +463,8 @@ static int numa_maps_open(struct inode *
 
 struct file_operations proc_numa_maps_operations = {
 	.open		= numa_maps_open,
-	.read		= seq_read,
-	.llseek		= seq_lseek,
+	.read		= proc_seq_read,
+	.llseek		= proc_seq_lseek,
 	.release	= seq_release_private,
 };
 #endif
@@ -476,7 +476,7 @@ static int smaps_open(struct inode *inod
 
 struct file_operations proc_smaps_operations = {
 	.open		= smaps_open,
-	.read		= seq_read,
-	.llseek		= seq_lseek,
+	.read		= proc_seq_read,
+	.llseek		= proc_seq_lseek,
 	.release	= seq_release_private,
 };
include/linux/proc_fs.h +12 -0
index 17e7578..48d45f5 100644
@@ -200,6 +200,18 @@ static inline void proc_net_remove(const
 	remove_proc_entry(name,proc_net);
 }
 
+struct proc_config {
+	int privacy;
+	int usegid;
+	gid_t gid;
+};
+
+extern struct proc_config proc_config;
+
+extern ssize_t proc_seq_read(struct file *file, char __user *buf, size_t size,
+                             loff_t *ppos);
+extern loff_t proc_seq_lseek(struct file *file, loff_t offset, int origin);
+
 #else
 
 #define proc_root_driver NULL
kernel/cpuset.c +4 -2
index 1a649f2..f78057d 100644
@@ -2446,6 +2446,7 @@ void __cpuset_memory_pressure_bump(void)
 	task_unlock(current);
 }
 
+#ifdef CONFIG_PROC
 /*
  * proc_cpuset_show()
  *  - Print tasks cpuset path into seq_file.
@@ -2500,10 +2501,11 @@ static int cpuset_open(struct inode *ino
 
 struct file_operations proc_cpuset_operations = {
 	.open		= cpuset_open,
-	.read		= seq_read,
-	.llseek		= seq_lseek,
+	.read		= proc_seq_read,
+	.llseek		= proc_seq_lseek,
 	.release	= single_release,
 };
+#endif /* CONFIG_PROC */
 
 /* Display task cpus_allowed, mems_allowed in /proc/<pid>/status file. */
 char *cpuset_task_status_allowed(struct task_struct *task, char *buffer)