2

I am extending some software (of which I am not the author) that runs under GNU / Linux (Ubuntu 14.04) and consists of a manager process and several worker processes. The manager can start a worker by means of a command line that I can specify in a configuration file.

After starting a worker, the manager communicates with it using a pipe. For security reasons, we have decided to let the workers run under a different user than the manager (let us call them manager-user and worker-user). This is achieved by writing a small wrapper script that switches user with su and starts a new worker. After this, the manager can communicate via a pipe with the worker process. This approach has been working for many months now.

As an alternative to su, we have considered using the setuid bit to run the workers. So we have written a C wrapper that can be invoked by the manager to start a worker. If we configure the wrapper to be owned by manager-user, the worker is started correctly (but, of course, with the wrong privileges). If we configure the wrapper to be owned by worker-user and set the setuid bit, then the workers are started but then exit because they cannot connect to the manager.

So my question is: how does running a setuid executable affect the permission on pipes created by both the parent and the child process? Can it be that the worker processes started through the setuid-wrapper do not have permission to open the manager's pipes (or the other way round)? If this can be the case, how can we change these permissions?

I have little experience using setuid so any information / explanation is welcome.

Giorgio
  • 5,023
  • 6
  • 41
  • 71

2 Answers2

4

A Unix pipe can be named or unnamed. A named pipe is implemented as a file, which has standard user, group, and world ownership permission bits.

An unnamed pipe also has permissions, but these are constrained by euid, egid, and umask in place at the time that the pipe is created.

So, if your worker-user is setuid to another user, with different group permissions, unless the egid is in common with the master process, it will not be able to use user or group perms to access the pipe that was created by the parent process.

Of course, with certain umask values, the unnamed pipe world-permissions would allow the processes to communicate over the pipe, but any process would be able to read/write that pipe. Being unnamed is more secure than a named pipe, but granting world-perms to any pipe is not a good security practice.

A possible solution for this use case (of wanting the two communicating processes to be run under different users) would be to have both the manager-user and worker-user processes be in the same group, and have the umask cleared of group-perm bits at the time of pipe creation, so that both processes can read and write the unnamed pipe.

So, if

  1. the manager-user is group-owned by team,
  2. the worker-user is also group-owned by team,
  3. both processes have their group umask bits cleared (no values of 1x .. 7x)
  4. before the unnamed pipe is created

Then the manager-user should be able to write on the unnamed pipe and the worker-user should be able to read it (or vice-versa, depending on how the pipe is used) even if they are running as separate users.

See the man page on chmod for details on permission bits.

aks
  • 2,328
  • 1
  • 16
  • 15
  • The one thing I'd add is that the `umask` value also applies to named pipes created by processes. So, if it's set to 022 and the pipe isn't owned by the worker process, the worker process will not be able to write. – Jason Jul 29 '15 at 18:31
  • Yes, that's true. On the other hand, a named pipe can be more easily managed by the process after its creation -- by `chmod`ing it, so I didn't focus on that comparatively easy path. – aks Jul 29 '15 at 18:37
  • Thanks a lot for the hints! I will try them out tomorrow. – Giorgio Jul 29 '15 at 22:12
0

Use an anonymous pipe using the pipe() function, like this (the example is borrowed from german Wikipedia):

# check the link above for #includes and const definitons

int main(void) {
    int fd[2], n, i;
    pid_t pid;
    char line[MAX_CHARS];

    // Create the pipe
    if (pipe(fd) < 0)
        fprintf(stderr, "Failed to create pipe()");

    // Fork child
    if ((pid = fork()) > 0) {
        // Parent process

        close(fd[0]);
        fprintf(stdout, "Parent : ");
        fgets(line, MAX_CHARS, stdin);
        write(fd[1], line, strlen(line));

        if (waitpid(pid, NULL, 0) < 0)
            fprintf(stderr, "Error: waitpid()");
    }

    else {
        // Child process
        close(fd[1]);
        n = read(fd[0], line, MAX_CHARS);

        for (i = 0; i < n; i++)
            line[i] = toupper(line[i]);
        fprintf(stderr, "Child : ");

        write(STDOUT_FILENO, line, n);
    }
    exit(0);
}

The above program creates an unidirectional pipe where the parent process holds the read end and the child process holds the write end.

hek2mgl
  • 152,036
  • 28
  • 249
  • 266
  • There's still a zeile present, instead of line (this comment likely has a short shelf-life). MAX_CHARS needs to be defined too, and I guess you've left the needed #includes as an exercise for the reader. – sjnarv Jul 29 '15 at 18:58
  • Thanks! I should have used `sed`! :) You can find the `#includes` in the Wikipedia page I've linked. I like the simple example even if it is the German Wikipedia. – hek2mgl Jul 29 '15 at 19:10