4

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?

Marco Merlini
  • 875
  • 7
  • 29
  • This works as expected for me. Try checking the return value of `fscanf`, and if it equals `EOF` then call `perror` to tell you why it failed. You should also do error checking on the `fopen` calls as well. – dbush Aug 30 '17 at 13:15
  • @dbush I have `perror` all over my actual code (I didn't clutter my original post with them). All calls returned success. – Marco Merlini Aug 30 '17 at 13:28
  • It's a while since I've played with SUID-bits and the like, but would `seteuid()` (set effective user id) do what you want? With the binary's setuid bit set, I _think_ you can toggle between the real (unprivileged) user and the owner (suid) user. – TripeHound Aug 30 '17 at 13:29
  • 1
    This is the sort of issue that is addressed very well in the design of [vsftpd](https://en.wikipedia.org/wiki/Vsftpd) ([see this video for an overview](https://youtu.be/ZgtBJuqd4WU?t=5m38s)). Basically, if a program has root privileges, then it shouldn't interact directly with untrusted clients at all. Instead, delegate all client interaction to separate child process with lower privileges, and use [IPC](https://en.wikipedia.org/wiki/Inter-process_communication) to convey this interaction in a carefully controlled manner. – r3mainer Aug 30 '17 at 13:48
  • @squeamishossifrage Thank you for pointing this out. This looks like a clean solution to these kinds of problems – Marco Merlini Aug 30 '17 at 14:21
  • Normally (with a regular disk file, for example), permissions are checked when the file is opened, and thereafter the permissions are not checked again. That means you normally can open the file with elevated privileges, drop the privileges, and continue reading merrily. It would be a bit surprising if a file system type didn't obey those semantics; it subverts a careful design decision in UNIX. However, your problem is not "continuous access" to a file, but repeatedly opening and closing a file. Permissions are checked by open; you need permission (elevated privileges) for each open. – Jonathan Leffler Aug 30 '17 at 14:23
  • 1
    The correct solution to the underlying problem is to set an udev rule that enables access to the gpio pseudofiles to a suitable group (`gpio` is common), and then add the users who are allowed to fiddle with the gpio pins to that group. For the rule itself, look at e.g. the final post in [this discussion](https://www.raspberrypi.org/forums/viewtopic.php?p=198148) at Raspberry Pi forums -- should be two rules, two lines; haven't tested it myself, though. This way, your program does not need to be setuid/setgid at all, nor care about the permissions. – Nominal Animal Aug 30 '17 at 20:21
  • Mahkoe> I second NominalAnimal on that: the proper way to do what you want is not to require admin permissions, it's to fix device permissions to grant access to whom needs it. – spectras Aug 31 '17 at 00:20
  • @Mahkoe> out of curiosity, can you run an `strace` of the non-working version and post the result somewhere? You need to attach the running process otherwise the SUID won't work: 1) add a `sleep(60)` at the start of your program. 2) run it and find its PID. 3) run `strace -p pid` in another console. When the program comes out of sleep, you should see the trace appear. – spectras Aug 31 '17 at 01:10

2 Answers2

0

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().

Iharob Al Asimi
  • 52,653
  • 6
  • 59
  • 97
Kevin Boone
  • 4,092
  • 1
  • 11
  • 15
  • Can you do some research, I personally don't like *"if your system behaves like mine"*, that's not a robust solution. – Iharob Al Asimi Aug 30 '17 at 13:23
  • I can confirm that this solution works on my system! (For prosperity, note that the sysfs file handle needs to be `fflush()`ed and `rewind()`ed for your program to track the current contents of the file. Otherwise, it will buffer the old value in the file). By the way, does anyone know why permissions for streams and file handles are treated differently? – Marco Merlini Aug 30 '17 at 13:26
  • @Mahkoe That's the interesting question now. I suppose, that streams can close the descriptor and reopen it if needed, so it might be related to that. But I don't know if the standard requires implementations not to do this. Remember that `fflush()` is needed because streams are buffered by default whereas if you use file descriptors directly, you don't need to flush anything unless you buffer something yourself. – Iharob Al Asimi Aug 30 '17 at 13:29
  • 2
    Streams or handles know nothing about permissions. Permissions are set on a *file* and govern your ability to open it (i.e. create a handle). A stream is typically a thin wrapper around a handle. Your explanation sounds rather suspicious. – n. m. could be an AI Aug 30 '17 at 14:31
  • 1
    I was interested enough in the question to spend ten minutes checking it out on my laptop. I posted what I found in the hope that it would be helpful to somebody. If anybody wants me to carry out an exhaustive survey of the differences between handles and streams across a range of platforms, compilers, and libraries, my rate for consultancy is £150 per hour plus VAT :) – Kevin Boone Aug 30 '17 at 15:09
  • 1
    There's no need to survey compilers an libraaries, it's enough to open POSIX documentation and read (or, for an expensive consultant like yourself, just recite off the top of your head :D). `fread` sets a completely different range of `errno` statuses from `fopen`. There's no way to report an error when a stream is closed and reopened out of the blue. Thus your alledged license to do so is rather unlikely. – n. m. could be an AI Aug 30 '17 at 15:47
  • 1
    I will go even further: the FILE structure as defined and used internally by glibc, which is the only full, widely used implementation of libc working on Linux kernels, does not store the path used to open the file, so without going further, it's certain it cannot `open()` it again once `fopen` has returned. Whatever is happening on OP's system, the alleged explanation is simply impossible. – spectras Aug 31 '17 at 01:02
0

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
Alexis Wilke
  • 19,179
  • 10
  • 84
  • 156