Open a file as root, but drop privileges before reading from it?

2.3k views Asked by At

TL;DR

I am writing a C program. I need to have root privileges to fopen a sysfs file, and I still need root privileges in order to read from it. However, since my program will need to continuously read the sysfs file, this implies that it will need to have elevated privileges the whole time. I would like to drop root privilege as soon as possible. What's the accepted way of approaching this problem?

Details

I am writing a program that interacts with sysfs. If I was running the commands on the shell, I would use:

myuser@mymachine:~$ sudo su
root@mymachine:/home/myhomedir# cd /sys/class/gpio
root@mymachine:/sys/class/gpio# echo 971 > export
root@mymachine:/sys/class/gpio# cat gpio971/value
0
root@mymachine:/sys/class/gpio# exit

I need to run these commands in a C program that is callable by a non-privileged user. One way to do this is to write the program in the usual way using fopen, fprintf, fscanf, etc and have the user run the program through sudo. However, this means the user needs to be a sudoer, and the program will have root privilege the whole time.

Another solution, which I strongly prefer (since the user will not have to be added to sudoers) is to change the program's owner to root, and set the setuid bit. (I learned this from here).

However, there's something I'm wondering about. What I would like to do is open the sysfs files while the program's euid is 0, but then drop all privileges right away (for safety). Then, now that the file has been opened, we simply setuid() to the user's UID. However, although I can't be entirely sure, this isn't working. Here is the relevant part of my code:

//At this point, due to the file permissions on the executable,
//euid = 0 and ruid = 1000. I know the following 4 lines work.
FILE *export = fopen("/sys/class/gpio/export", "wb");
fprintf(export, "971\n");
fclose(export);

FILE *sw_gpio = fopen("/sys/class/gpio971/value", "rb");

setuid(1000);
//Now euid = 1000 and ruid = 1000

int switch_val = -1;
fscanf(sw_gpio, "%d", &switch_val);
printf("Switch value: %d\n", switch_val); //-1
//Even though the only possible values in this sysfs file are 0 and 1,
//switch_val is still equal to -1

fclose(sw_gpio);

So it seems that I will need to keep elevated permissions to be able to read from /sys/class/gpio/gpio971/value. But this is exactly what I don't want! This program will need to poll the value throughout execution of the program, and I don't want root privileges the whole time.

Finally for the sake of completion, here are the permissions I've set on my executable:

-rwsr-xr-x 1 root myuser 10943 Jan 1 20:17 main*

So how does one drop root privilege, but continue to read from an access-controlled sysfs file?

2

There are 2 answers

7
Kevin Boone On BEST ANSWER

I haven't tried this with /sysfs, but even with plain files my understanding is that file streams do not retain access permissions after a call to setuid(). File handles do, however, for reasons I don't understand. So, if your system behaves like mine (Fedora 20 on x64) you might be able to use open()/read() instead of fopen()/fscanf().

0
Alexis Wilke On

Once the file is open, you have access to it. The access is only checked at the time you open the file. The type of access depends on the mode used to open the file (i.e. read-only, write-only, append, read-write...)

So the algorithm can be:

seteuid(0);
f = fopen(...);
seteuid(getuid());

...

fread(..., f);
fwrite(..., f);

Actually, this is a known issue when you use fork() + execve() because the new process may be given a new user/group ownership, but any file still opened in the parent process is passed down to the child. Including files that the child would not be able to access otherwise. This is actually the reason for the O_CLOEXEC flag accepted by the open(2) function. A file opened with that flag is automatically closed by fork() before the new process has a chance to access it. Further, servers such as Apache2, that use fork() use that feature by doing something similar to:

// in parent process
s = socket(...);
bind(s, addr, sizeof(addr));   // <- this fails for port 80 or 443 unless you're root
listen(s, 100);

...

fork();
setuid(<apache-uid>);
// here the child would not itself be able to do the bind() shown above