1

Consider the following simplified example which illustrates my problem.
The server's OS is Debian 11 armhf.

we're in directory /botm/test/:

drwx------ 2 b b 4096 Sep 27 19:27 hide
-rwsr-xr-x 1 b b 8260 Sep 27 19:54 test1
-rwsr-xr-x 1 b b 8264 Sep 27 19:58 test2
-rw-r--r-- 1 b b   56 Sep 27 19:52 test1.awk
-rw-r--r-- 1 b b  172 Sep 27 19:54 test1.c
-rw-r--r-- 1 b b  186 Sep 27 19:58 test2.c

As can be seen, only the owner, b can access the directory /botm/test/hide.

We are now user test
We can not access the file /botm/test/hide/test.txt:

$ cat hide/test.txt
cat: hide/test.txt: Permission denied

Ok. The programs test1 and test2 have the SETUID bit set.
So even if we are the user test we can run these programs as if we were user b.

First, test2.c:

/* test2.c */
#include <unistd.h>
#include <stdio.h>
int main(int argc, char **argv)
{
  int r;
  r = execl("/usr/bin/cat","/usr/bin/cat","/botm/test/hide/test.txt", (char *)0);
  printf("%d\n",r);
  return r;
}
$ ./test2
hidden content
$ 

As expected, it works.

Now, test1.c, test1.awk:

/* test1.c */
#include <unistd.h>
#include <stdio.h>
int main(int argc, char **argv)
{
  int r;
  r = execl("/usr/bin/mawk","/usr/bin/mawk","-f","/botm/test/test1.awk", (char *)0);
  printf("%d\n",r);
  return r;
}
# test1.awk
BEGIN {
  system("cat /botm/test/hide/test.txt");
};
$ ./test1
cat: /botm/test/hide/test.txt: Permission denied
$

Surprisingly, this does not work.

What I expect:
If I run the program test1 which has SETUID, then it should run as if the user b.
So if the program calls /usr/bin/mawk then that too should run as user b.
So if the awk script then calls cat, then cat should also run as user b.
So cat should be able to access /botm/test/hide/test.txt which is available only to user b.
But this does not happen.

Some real context now.
This above was just a simplified example. I have a project with multiple C and AWK programs. The AWK programs are called from the C programs to process some text and generate some output. This includes sometimes calling cat or other system tools. Some of these programs are run from the Apache server as the user www-data which does not have access (and shouldn't have) to some of the temporary files created by these programs. That's why SETUID is used. And this kind of workflow is used a lot in this project (from 2014).

I'm now moving this from my old server (where this worked) to a new one (where this doesn't work). Changing the project to not rely on this mechanism would be a very big redesign which I don't want to do now.

Why is SETUID not preserved when a C program (with SETUID) calls an AWK script which calls a system tool (cat, ...)?
What to do to preserve it again?
On a previous version of the system it worked.

EDITED TO ADD:

I verified that the AWK program still has access to the hidden file, only things called by system() don't.

So I found out that the reason of this behavior is that when using system() (or getline with a pipe) then mawk calls sh to perform the command.
AWK doesn't have exec or anything like that.

sh will drop the SETUID unless called with -p.
This is new behaviour. This part of man sh is not present on my old server:

           -p priviliged    Do not attempt to reset effective uid if it does not match uid. This is not set by default to help avoid incorrect usage by
                            setuid root programs via system(3) or popen(3).

So to make my programs work again I have to achieve at least 1 of these things:

  • make /usr/bin/sh not drop SETUID by default.
  • make mawk or other compatible AWK interpreter use -p when using /usr/bin/sh for system()
  • find a different way to run programs from AWK
  • redesign all the programs to not rely on this. For example do them in C, Perl, or Python which do have proper exec functionality not relying on sh.

Redesigning the entire project (and some other projects which also use this workflow) would take effort and time which I currently don't have so I really need to restore the full SETUID functionality somehow.

Actually, the functionality of cat can be easily recreated inside the AWK program but also other tools are called with system() in my project: [ -f, cat, cp, mkdir, mv, sleep, wget. And in most cases calling them without SETUID is not acceptable.

  • 2
    Does this answer your question? setuid root does not work – Chris Davies Sep 28 '22 at 10:07
  • @roaima thanks for the links.
    First link: does show the reason but the proposed solution is not relevant to my situation.
    Second link: Since my AWK scripts are always called from C programs, the trick with setresuid looks promising. I will try it.
    – bicyclesonthemoon Sep 28 '22 at 20:32
  • If the only thing you need root privileges for is cat(1), you can instead read and output the file directly from your C program, as you noted in the edit with regards to awk(1). In such a case there is no need for execl and much less awk. – Vilinkameni Sep 29 '22 at 08:26
  • 1
    It looks like half of this question is a (partial) answer, so should probably be moved out of the question and into an answer of its own. And perhaps that leads to a follow-up question. (As a bonus, you'll earn reputation from all three posts). – Toby Speight Oct 13 '22 at 16:03

1 Answers1

2

In this answer to similar problem:
https://unix.stackexchange.com/a/565254/543092
There is a proposition to use setresuid().

Since in my project the AWK scripts are always called from C programs this is a solution available to me.

I do something like this:

#include <unistd.h>
#include <stdio.h>
#include <errno.h>
int main(int argc, char **argv)
{
    uid_t euid;
    int r;
    euid = geteuid();
    r = setreuid(euid, euid);
    if (r != 0)
        return (r = errno);
    r = execl("/usr/bin/mawk","/usr/bin/mawk","-f","/botm/test/test1.awk", (char *)0);
    printf("%d\n",r);
}

This way the "disguise" is complete and sh will have no idea that there was a SETUID before. Everything works.

And this is a better solution than somehow making sh always behave as if there was -p. It will preserve my SETUID if I know what I'm doing.

I use setreuid() instead of the setresuid() proposed there because:

  • setreuid() is enough
  • setresuid() is a GNU extension.