Dennis: Verständnisfehler Set-UID

Hallo liebes Forum ;-)

Ich verzweifle gerade an der Verwendung des Set-UID Bits, das funktioniert einfach nicht so, wie ich verstanden habe, dass es funktionieren sollte. Folgendes Szenario:

vps@debian:~/bin$ chmod 4755 *
vps@debian:~/bin$ ls -la
total 16
drwxr-xr-x 2 vps vps 4096 2007-03-07 23:06 .
drwxr-xr-x 8 vps vps 4096 2007-03-07 23:06 ..
-rwsr-xr-x 1 vps vps 840 2007-03-07 22:50 post-update
-rwsr-xr-x 1 vps vps 283 2007-03-07 23:06 pre-update

Der User vps hat also zwei Scripte, die *jeder* User auf dem System ausführen können dürfen soll, und zwar unter der Userkennung des Users vps. Die Scripte sind im Prinzip sehr simpel, exemplarisch hier ein Teil von pre-update:

#!/bin/bash

whoami
rm -rf /home/vps/tmp/*

Rufe ich das Script nun jedoch als entsprechender anderer User auf, passiert nur folgendes:

driehle@debian:/home/vps/bin$ ./pre-update
driehle
rm: cannot lstat `/home/vps/tmp/*': Permission denied

Führe ich das Script direkt als User vps aus, so klappt alles problemlos. Es liegt also irgendwie daran, dass Set-UID nicht funktioniert, was vermutlich daran liegt, dass ich irgendetwas falsch mache ;-)

Woran liegt es also, dass der effektiv das Script ausführende User nicht vps ist? Bei Programmen wie passwd funktioniert das doch auch, und die Rechte für mein Script sind genauso gesetzt wie die Rechte für passwd.

Viele Grüße,
  ~ Dennis.

  1. Hallo Dennis,

    Woran liegt es also, dass der effektiv das Script ausführende User nicht vps ist?

    Ok, unter UNIX hat ein Prozess traditionell _DREI_ verschiedene UIDs:

    * Die reale User-ID
     * Die effektive User-ID
     * Die Saved-Set-User-ID

    Ich könnte jetzt ziemlich ins Detail gehen und ne ganze Menge rumerklären dazu, allerdings genügt hier nur folgende Erklärung: Punkt 1: Es kommt fast immer auf die effektive User-ID an. Punkt 2: Wenn Du ein setuid-Progrmam hast, dann wird *nicht* sofort die effektive User-ID gesetzt, das Programm muss zuerst den Systemaufruf setuid() mit der User-ID der Datei selbst als Parameter aufrufen. *Dann* erst wird die effektive User-ID auf die User-ID des Users vps (in Deinem Fall) gesetzt.

    Mir ist leider keine Möglichkeit bekannt, dies in Shell-Scripts zu tun. Du müsstest einen kleinen C-Wrapper schreiben, der das Shell-Script ausführt, damit das geht. Wenn jemand ne bessere Möglichkeit kennt, das sogar im Shellscript zu machen: Immer her damit. ;-)

    Viele Grüße,
    Christian

    --
    "I have always wished for my computer to be as easy to use as my telephone; my wish has come true because I can no longer figure out how to use my telephone." - Bjarne Stroustrup
    1. Hi Christian,

      * Die Saved-Set-User-ID

      Die ersten beiden IDs waren mir verständlich, doch was ist das hier? Auch wenn ich es vermutlich nicht beachten muss, der Interesse halber würde mich ein kurzer Kommentar dazu interessieren ;-)

      Mir ist leider keine Möglichkeit bekannt, dies in Shell-Scripts zu tun. Du müsstest einen kleinen C-Wrapper schreiben, der das Shell-Script ausführt, damit das geht. Wenn jemand ne bessere Möglichkeit kennt, das sogar im Shellscript zu machen: Immer her damit. ;-)

      Ich hab das mit Hilfe von Google jetzt soweit hinbekommen:

      debian:/home/vps# ls -la bin/
      total 44
      -rwsr-xr-x 1 vps vps 7061 2007-03-08 22:23 post-update
      -rw-r--r-- 1 vps vps  201 2007-03-08 22:23 post-update.c
      -rw-r--r-- 1 vps vps  827 2007-03-08 22:22 post-update.sh
      -rwsr-xr-x 1 vps vps 7060 2007-03-08 22:23 pre-update
      -rw-r--r-- 1 vps vps  200 2007-03-08 22:22 pre-update.c
      -rw-r--r-- 1 vps vps  263 2007-03-08 22:22 pre-update.sh

      Die C-Scripte sehen im Prinzip nur so aus:

      #include <stdio.h>
      #include <stdlib.h>

      int main(int argc, char *argv[])
      {
        system("/bin/bash /home/vps/bin/pre-update.sh");
        return 0;
      }

      Leider ist das Resultat immer noch dasselbe - die Permission denied Meldungen :-(

      Ich vermute, dass dies wieder damit zusammenhängt, dass die Shell das mit dem Benutzer wechseln nicht so wirklich kann, wie du bereits erwähnt hast. Aus diesem Grund habe ich mal ein PHP-Script gemacht, welches mit posix_setuid() die ID setzt und dann mit posix_geteuid() und posix_getuid() die effektive und reale User-ID anzeigt. Dazu habe ich natürlich dann auch noch den Wrapper angepasst, sodass er das PHP-Script ausführt.

      Und siehe da - die Ausgaben haben für den realen User driehle ergeben und vps für den effektiven User. Ich werden also morgen mal testweise versuchen meine Shellscripte auf PHP-Scripte umzuschreiben.

      Viele Grüße,
        ~ Dennis.

      1. Hallo Dennis,

        * Die Saved-Set-User-ID

        Wenn Du per setuid() die Benutzerkennung wechselst, dann werden reale und effektive Benutzer-ID auf eben die neue Benutzerkennung angepasst. Die Saved-Set-User-ID wird dann auf die alte Benutzerkennung gesetzt - damit weiß der Kernel, dass bei einem eventuellen, neuen Zurückwechseln-Wollen der Prozess berechtigt ist, zur *alten* Benutzerkennung zurückzukehren.

        Ok, wenn Du ein normales Programm hast, dann hat das ja alle 3 Benutzerkennungen gleich:

        R | E | S
        ---+---+---
         1 | 1 | 1

        Wenn das Programm dem User mit der UID 2 gehört und setuid ist, dann wird (*falls* es kein Script ist) es mit der effektiven und Saved-Set-User-ID 2 gestartet (der weiter unten erklärte Systemaufruf execve() macht dies):

        R | E | S
        ---+---+---
         1 | 2 | 2

        Das Programm greift auf's Dateisystem etc. zu wie ein Programm des Users 2. Wenn das Programm aber wieder als User 1 arbeiten will, dann kann es setuid (1) machen (das darf es, weil 1 eine der UIDs ist, die das Programm besitzt; wenn es setuid (3) machen wollte, bekäme es eine Fehlermeldung).

        R | E | S
        ---+---+---
         1 | 1 | 2

        Das Programm kann dann wieder ein setuid (2) machen, um wieder zum ursprünglichen Zustand zurückzukehren:

        R | E | S
        ---+---+---
         1 | 2 | 2

        Zusätzlich gibt's noch die Funktion setreuid (), mit der man die reale User-ID auch ändern kann. Dazu gleich mehr:

        #include <stdio.h>
        #include <stdlib.h>

        int main(int argc, char *argv[])
        {
          system("/bin/bash /home/vps/bin/pre-update.sh");
          return 0;
        }

        Ok, hier spielt Dir Deine Shell einen Streich.

        Dein ursprüngliches Problem ist, dass der Kernel das setuid-Bit nicht auf Scripte anwendet. Dein jetztiges Problem ist, dass die Bash beim Start die effektive User-ID auf die reale User-ID setzt - d.h. Dein Script ist wieder "unprivilegiert". Du kannst übrigens auch etwas anderes als /bin/bash ins system() reinschreiben, system() führt immer /bin/sh -c parameter aus, wobei parameter das ist, was Du system() übergibst.

        #include <stdio.h>  
        #include <stdlib.h>  
        #include <sys/types.h>  
        #include <unistd.h>  
          
        int main (int argc, char **argv) {  
          // hier kann dann entweder die UID fest kodiert stehen wie in  
          // meinem einfachen Beispiel oder eben eine Logik, die den  
          // Pfad des ausgeführten Programms herausfindet und die UID  
          // über stat() ausliest (wird relativ kompliziert, wenn man es  
          // richtig machen will)  
          if (setreuid (555, 555) == -1) { // 555 sei hier die UID vom user 'vps'  
            // Fehler  
            perror (argv[0]);  
            return 1;  
          }  
          // system() ist eigentlich böse, wenn bei setuid-Prozessen eingesetzt  
          // (hat mit möglichen Attacken über Umgebungsvariablen zu tun)  
          // richtig[tm] wäre es eigentlich, selbst per execve() das richtige  
          // Script auszuführern, der Code wäre allerdings nicht so anschaulich  
          return system ("...");  
        }
        

        Wenn Du's über execve() machen willst, dann wird's komplizierter, dafür hast Du dann die Sicherheit, dass Dir niemand über Umgebungsvariablen reinpfuschen kann. Der wichtigste Unterschied zwischen execve() und system() ist der: execve() führt ein neues Programm aus. Allerdings wird das in der Form ausgeführt, als dass das aktuelle Programm durch das neue Programm ersetzt wird, d.h. wenn execve() erfolgreich war, dann wird alles, was *nach* execve() kommt, *nicht* mehr ausgeführt. Wie funktioniert nun system()? Es ruft *zuerst* fork() auf (fork() erzeugt eine 1:1-Kopie des aktuellen Prozesses und an Hand des Rückgabewerts kann man unterscheiden, in welchem Prozess die Ausführung gerade weitergeht), ruft dann im *Kindprozess* mit ein paar Defaultwerten execve() auf und wartet im *Elternprozess* mit wait() oder waitpid() o.ä. bis der Kindprozess fertig ist und gibt den Rückgabewert des Kindprozesses zurück.

        Da Du *nach* der Ausführung Deines Scripts ja die Ausführung sowieso beenden willst, brauchst Du den ganzen fork()-Gedöns gar nicht. Daher reicht es für Dich aus, wenn Du folgendes machst:

        int main (int argc, char **argv) {  
          char *const p_env[] = {  
            "PATH=/bin:/usr/bin",  
            NULL  
          };  
          char *const p_argv[] = {  
            "/bin/bash",  
            "/home/vps/bin/pre-update.sh",  
            NULL  
          };  
          const char *p_prog = "/bin/bash";  
          // ... hier der setreuid-code  
          execve (p_prog, p_argv, p_env);  
          perror (argv[0]);  
          return 1;  
        }
        

        execve() ist natürlich komplizierter: Bei system() übernimmt Dir system() das fork(), execve() etc. und die Shell übernimmt Dir das Trennen der Argumente, das Suchen der ausführbaren Datei in PATH etc. Bei execve() musst Du das alles manuell machen. execve() erwartet 3 Parameter:

        1. Der Pfad zur ausführbaren Datei selbst. Das muss wirklich ein Pfad sein, den Du auch per fopen() verwenden könntest.

        2. Die Parameter, die im neuen Programm über argv[] abrufbar sein sollen, mit NULL terminiert.

        3. Die Umgebungsvariablen als String-Liste, ebenfalls mit NULL terminiert.

        Wenn Du also system("ls") ausführst, wird im Endeffekt execve() von system() mit folgenden Parametern aufgerufen:

        1. "/bin/sh"
        2. { "sh", "-c", "ls", NULL }
        3. environ  // (die globale Variable, die die Umgebung des *aktuellen* Programms enthält)

        Die Shell analysiert nun für Dich das, was nach dem "-c" kommt, löst dann den Programmnamen durch die PATH-Umgebungsvariable auf und führt dann *selbst* wiederum ein execve() aus mit folgenden Argumenten:

        1. "/bin/ls"
        2. { "ls", NULL }
        3. environ

        Wenn Du dagegen system ("ls -l") eingibst, dann wird folgendes gemacht: system() macht im Kindprozess execve() mit folgenden Parametern:

        1. "/bin/sh"
        2. { "sh", "-c", "ls -l", NULL }
        3. environ

        Die Shell macht nun execve() mit folgenden Argumenten:

        1. "/bin/ls"
        2. { "ls", "-l", NULL }
        3. environ

        Ok, was ist jetzt an meinem execve von oben anders? Schauen wir uns die 3 Argumente nochmal an:

        1. "/bin/bash"
        2. { "/bin/bash", "/home/vps/bin/pre-update.sh", NULL }
        3. { "PATH=/bin:/usr/bin", NULL }

        Ok, die ersten beiden Argumente sind identisch denen, die die Shell im Durchlauf mit system() auch genutzt hätte. Das dritte Argument ist jedoch anders: Es wird nicht die Umgebung des aktuellen Prozesses übergeben, sondern eine komplett neue, leere, in der nur PATH gesetzt ist (auf etwas minimalistisches). Die Frage: Warum?

        Ok, stell Dir folgendes vor: Du hast den Code mit system ("/bin/bash /pfad/zum/script"); bei Dir drin. Jemand ruft dann Dein setuid-Programm auf, setzt aber vorher *irgendwelche* Umgebungsvariablen, die eine besondere Bedeutung haben, auf bösartige Werte. Dann werden die Umgebungsvariablen zum Script durchgereicht. Nehmen wir (das einfachste Beispiel) PATH: Wenn der Aufrufer bei sich PATH auf /boeser/pfad:/bin:/usr/bin:... setzt und bei sich in /boeser/pfad eine ausführbare Datei namens 'ls' existiert und Dein Script selbst ruft 'ls' auf - dann würde Dein Script plötzlich eben diese bösartige Datei ausführen. Du hattest diesmal sogar Glück, dass Du im system()-Aufruf "/bin/bash ..." stehen hattest und nicht "bash ..." - dann wäre nämlich schon in der Phase ein Angriff möglich. Und PATH ist nicht die einzige Umgebungsvariable, mit der man Sachen anstellen kann, es ist nur die einfachste und anschaulichste Lösung. Weitere böse Umgebungsvariablen beinhalten beispielsweise LD_LIBRARY_PATH und LD_PRELOAD.

        Mit der execve()-Lösung hast Du also eine saubere Umgebung. Wenn Dir PATH nicht ausreicht, sondern Du mehr Umgebungsvariablen schon gesetzt haben musst, dann kannst Du diese natürlich zu obigem Code hinzufügen.

        Der Gesamtcode sähe also wie folgt aus:

        #include <stdio.h>  
        #include <stdlib.h>  
        #include <sys/types.h>  
        #include <unistd.h>  
          
        int main (int argc, char **argv) {  
          char *const p_env[] = {  
            "PATH=/bin:/usr/bin",  
            // eventuell noch weitere Umgebungsvariablen, wie z.B.  
            "EDITOR=/usr/bin/nano",  
            NULL  
          };  
          char *const p_argv[] = {  
            "/bin/bash",  
            "/home/vps/bin/pre-update.sh",  
            NULL  
          };  
          const char *p_prog = "/bin/bash";  
          // hier kann dann entweder die UID fest kodiert stehen wie in  
          // meinem einfachen Beispiel oder eben eine Logik, die den  
          // Pfad des ausgeführten Programms herausfindet und die UID  
          // über stat() ausliest (wird relativ kompliziert, wenn man es  
          // richtig machen will)  
          if (setreuid (555, 555) == -1) { // 555 sei hier die UID vom user 'vps'  
            // Fehler  
            perror (argv[0]);  
            return 1;  
          }  
          // Script ausführen  
          execve (p_prog, p_argv, p_env);  
          perror (argv[0]);  
          return 1;  
        }
        

        Damit hast Du dann ein kleines C-Programm, das nichts anderes tut, als Dein Shell-Script mit sauberen (!) Umgebungsvariablen unter einem anderen User aufzurufen.

        Viele Grüße,
        Christian

        --
        "I have always wished for my computer to be as easy to use as my telephone; my wish has come true because I can no longer figure out how to use my telephone." - Bjarne Stroustrup
        1. Hi Christian,

          erstmal: Wow, Wahnsinn was du mal wieder für einen langen Beitrag geschrieben hast, Herzlichen Dank dafür schon mal vorab :-)

          Wenn Du per setuid() die Benutzerkennung wechselst, dann werden reale und effektive Benutzer-ID auf eben die neue Benutzerkennung angepasst. Die Saved-Set-User-ID wird dann auf die alte Benutzerkennung gesetzt - damit weiß der Kernel, dass bei einem eventuellen, neuen Zurückwechseln-Wollen der Prozess berechtigt ist, zur *alten* Benutzerkennung zurückzukehren.

          Ok, die Idee dahinter habe ich verstanden, das Prinzip glaube ich auch ;-) Letztendlich ist die Saved-Set nur ein "Speicherplatz" für eine User-ID, zu der das Script (zurück-) wechseln darf und kann vom Script nicht geändert werden (deshalb ja Saved-Set).

          Dein ursprüngliches Problem ist, dass der Kernel das setuid-Bit nicht auf Scripte anwendet.

          Das scheint übrigens eine Debian Security-Policy zu sein oder so etwas in der Art, zumindest nach dem was meine bisherigen Recherchen ergeben haben. Offensichtlich wird das aber von den meisten anderen Distributionen genauso gehandhabt.

          1. "/bin/ls"
          2. { "ls", "-l", NULL }
          3. environ

          Woher kommt das eigentlich, dass man dem Programm als ersten Parameter immer noch mal seinen eigenen Namen (ls) bzw. den Pfad zum eigenen Namen (/bin/ls) übergibt? Macht man das einfach, weil das so üblich ist und jeder das so macht, oder steckt da ein tieferer Sinn dahinter?

          Ok, stell Dir folgendes vor: Du hast den Code mit system ("/bin/bash /pfad/zum/script"); bei Dir drin. Jemand ruft dann Dein setuid-Programm auf, setzt aber vorher *irgendwelche* Umgebungsvariablen, die eine besondere Bedeutung haben, auf bösartige Werte. Dann werden die Umgebungsvariablen zum Script durchgereicht. Nehmen wir (das einfachste Beispiel) PATH: Wenn der Aufrufer bei sich PATH auf /boeser/pfad:/bin:/usr/bin:... setzt und bei sich in /boeser/pfad eine ausführbare Datei namens 'ls' existiert und Dein Script selbst ruft 'ls' auf - dann würde Dein Script plötzlich eben diese bösartige Datei ausführen. Du hattest diesmal sogar Glück, dass Du im system()-Aufruf "/bin/bash ..." stehen hattest und nicht "bash ..." - dann wäre nämlich schon in der Phase ein Angriff möglich. Und PATH ist nicht die einzige Umgebungsvariable, mit der man Sachen anstellen kann, es ist nur die einfachste und anschaulichste Lösung. Weitere böse Umgebungsvariablen beinhalten beispielsweise LD_LIBRARY_PATH und LD_PRELOAD.

          Danke für diese Sicherheits-Aufklärungen - zwar hab ich davon schon mal gehört und auch derweil hier im Forum drüber gelesen, aber dass dies bei so kleinen Programmen wie diesem hier schon so relevant wird, da denke ich als !C-Programmierer natürlich nicht dran ;-) Letztendlich läuft es aber doch immer darauf hinaus, dass Programme die man in seinem Script (oder Programm, je nach dem) verwendet weitere Programme, Libraries oder Scripte über Pfade aus Umgebungsvariablen nachladen wollen - oder übersehe ich da was und pauschalisiere das deshalb fälschlicherweise?

          Zu dem Code hätte ich noch zwei Fragen:

          int main (int argc, char **argv) {

          Was ist Unterschied von **argv zu *argv[]?

          char *const p_env[] = {  [...] };
            char *const p_argv[] = { [...] };
            const char *p_prog = "/bin/bash";

          Warum heißt es hier in den ersten beiden Fällen char const und im letzten Fall const char, also anders herum?

          hier kann dann entweder die UID fest kodiert stehen wie in
          meinem einfachen Beispiel oder eben eine Logik, die den
          Pfad des ausgeführten Programms herausfindet und die UID
          über stat() ausliest (wird relativ kompliziert, wenn man es
          richtig machen will)

          Im Prinzip ist es doch keine Gefahr, die UID hart zu kodieren, weil wenn es nicht die UID des Scriptes ist, scheitert der User-Wechsel ja sowieso, korrekt so weit?

          Damit hast Du dann ein kleines C-Programm, das nichts anderes tut, als Dein Shell-Script mit sauberen (!) Umgebungsvariablen unter einem anderen User aufzurufen.

          Danke, ich habe das Programm zweimal kompiliert (einmal für pre-update.sh und einmal für post-update.sh), set-UID Bit gesetzt und siehe da, es funktioniert alles einwandfrei :-)

          Vielleicht noch kurz ein Wort zum Anwendungszweck: Auf meinem Server sind mehrere SVN-Repositores, die (wie unter Debian üblich) vom User svn verwaltet werden. Nun will ich dort in einem Repository einen Pre-Hook und einen Post-Hook einbauen, das sind Scripte die von svn aufgerufen werden. Da ich dabei dem User svn keinen Lesezugriff auf sämtliche Verzeichnisse von vps geben will, kann mit obigem Script nun der User vps sich selber die neuesten Updates aus dem SVN-Repository holen und in die richtigen Zielverzeichinsse packen. Schönerweise sind dann nämlich auch Owner und Gruppe direkt richtig gesetzt und ich brauch nicht mit root noch mal den Owner von svn auf vps ändern.

          Viele Grüße,
            ~ Dennis.

          1. Hallo Dennis,

            Was mir übrigens noch einfällt: Wenn der User root (id 0) im Spiel ist, dann ändert sich das Verhalten der set*uid-Funktionen übrigens teils gewaltig, d.h. meine Erklärungen vom vorigen Posting bitte nur auf User <-> User, nicht auf Root <-> User o.ä. beziehen!

            Dein ursprüngliches Problem ist, dass der Kernel das setuid-Bit nicht auf Scripte anwendet.

            Das scheint übrigens eine Debian Security-Policy zu sein oder so etwas in der Art, zumindest nach dem was meine bisherigen Recherchen ergeben haben. Offensichtlich wird das aber von den meisten anderen Distributionen genauso gehandhabt.

            Naja, ok, man kann das glaub ich über Capabilities ändern, aber an sich macht das der Kernel immer so, außer man redet's ihm explizit aus.

            1. "/bin/ls"
            2. { "ls", "-l", NULL }
            3. environ

            Woher kommt das eigentlich, dass man dem Programm als ersten Parameter immer noch mal seinen eigenen Namen (ls) bzw. den Pfad zum eigenen Namen (/bin/ls) übergibt? Macht man das einfach, weil das so üblich ist und jeder das so macht, oder steckt da ein tieferer Sinn dahinter?

            Ganz einfach: Gebe ich auf der Shell folgenden Befehl ein:

            ls -l datei

            Dann steht ja hinterher im argv des ls-Programms folgendes: { 'ls', '-l', 'datei', NULL } - halt exakt so, wie's auf der Kommandozeile eingegeben wurde modulo Interpretationen durch die Shell.

            Das Problem ist: Der Kernel weiß an Hand von 'ls' nicht, welche Datei auszufüren ist. Daher muss man execve() den Pfad zum _ausführbaren_ Programm direkt übergeben, das er dann wie eine Datei einliest. Wenn die Argumente das 'ls' jedoch nicht enthielten, dann wäre die einzige Möglichkeit, argv wieder so hinzukriegen, wie's ist, indem man '/bin/ls' in argv[0] schreibt - dann wäre das aber nicht das, was der User eingegeben hat.

            Der Sinn des ganzen ist: Wenn irgend ein Programm eine Fehlermeldung ausgibt, dann ist es Konvention, dass das Programm seinen eigenen Namen mit angibt. Woher bekommt das Programm den? Über argv[0]. Wenn in argv[0] nur die ausführbare Datei stünde, wäre das eher schlecht ("/bin/ls: Datei oder Verzeichnis nicht gefunden" würde nur unnötig verwirren).

            Gut, man könnte sich fragen, wozu überhaupt argv[0] und dort nicht schon gleich den ersten Parameter reinschreiben (und auf das Fehlermeldungsgedöns pfeifen). Die einfache Antwort ist: Damit kann das Programm selbst feststellen, als *was* es aufgerufen wurde. Schau Dir z.B. bunzip2 an: ls -l which bunzip2 - Du wirst feststellen, dass das ein Symlink auf bzip2 ist. bzip2 stellt dann in der main-Funktion fest, ob argv[0] bunzip2 ist - wenn ja, dann reagiert es, als ob man bzip2 -d aufgerufen hätte. Dadurch kann man viele kleine Programme, die von der Programmlogik her fast das gleiche machen, zu einem einzigen Programm zusammenzufassen.

            Danke für diese Sicherheits-Aufklärungen - zwar hab ich davon schon mal gehört und auch derweil hier im Forum drüber gelesen, aber dass dies bei so kleinen Programmen wie diesem hier schon so relevant wird, da denke ich als !C-Programmierer natürlich nicht dran ;-) Letztendlich läuft es aber doch immer darauf hinaus, dass Programme die man in seinem Script (oder Programm, je nach dem) verwendet weitere Programme, Libraries oder Scripte über Pfade aus Umgebungsvariablen nachladen wollen

            Im Prinzip schon.

            int main (int argc, char **argv) {

            Was ist Unterschied von **argv zu *argv[]?

            In dem Fall: Geschmackssache. Bei *argv[] weist Du den Compiler an, dass Deine Funktion explizit ein Array von Zeigern erwartet, bei **argv kann auch ein Zeiger auf einen Zeiger übergeben werden (nachdem beim *Zugriff* kein Unterschied zwischen Zeiger und Array gemacht wird, ist das *hier* vollkommen egal). Der Unterschied entsteht genau dann, wenn Du Variablen deklarierst, d.h.:

            char bla_array[]; // geht nicht - ist auch unsinn  
            char *bla_zeiger; // geht - ist halt nur uninitialisiert  
            char foo_array[] = "hallo"; // geht  
            char *foo_zeiger = "hallo"; // geht
            

            Und dann ist noch die Frage, was Du hinterher mit den Variablen anstellen kannst:

            bla_zeiger = "ciao"; // geht  
            foo_array = "ciao"; // geht *nicht*  
            foo_zeiger = "ciao"; // geht
            

            Warum meckert der Compiler bei foo_array? Weil foo_array eben wirklich ein Array und kein Zeiger ist - foo_zeiger ist dagegen nur ein Zeiger auf ein "unbenanntes" Array, das irgendwo im Speicher sitzt. Man greift zwar *identisch* auf foo_array und foo_zeiger zu - allerdings sind Arrays halt doch nicht exakt das gleiche wie Zeiger in C.

            Weitere Erklärungen gibt's im Standardwerk von Kernighan/Ritchie: Programmieren in C.

            char *const p_env[] = {  [...] };
              char *const p_argv[] = { [...] };
              const char *p_prog = "/bin/bash";

            Warum heißt es hier in den ersten beiden Fällen char const und im letzten Fall const char, also anders herum?

            Es ist nicht "anders herum" - nur so halb.

            const char *p_prog = "/bin/bash";  
              
            p_prog = "foo"; // geht  
            p_prog[0] = '!'; // geht *nicht*  
              
            char *const p_prog2 = "/blub";  
            p_prog2 = "bar"; // geht *nicht*  
            p_prog2[0] = '!'; // kompiliert, allerdings gibt's Speicherzugriffsfehler  
                              // da man konstante Strings nicht ändern darf
            

            Bei const char * ist also der *Inhalt* des Zeigers nicht veränderbar, bei char *const ist der Zeiger selbst nicht veränderbar.

            Ok, was haben wir jetzt bei char *const p_env[]? Da bezieht sich das const klar auf die Variable - allerdings steht bei der Variable direkt auch ein [] - d.h. die Variable ist eigentlich ein Array (von Zeigern) - und dieses Array von Zeigern darf nicht geändert werden. Gut, Arrays in C etwas zuzuweisen geht sowieso nicht so wirklich, wie wir oben gesehen haben, aber den Inhalt kann man ja grundsätzlich ändern - außer es steht const davor.

            Nun kann man sich fragen: Warum steht da kein const vor dem char - damit der *Inhalt* des Arrays auch nicht modifizierbar ist? *Sehr* gute Frage, keine Ahnung, wissen wohl nur die, die die API dazu entworfen haben - korrekterweise müsste das nämlich auch dabei stehen, d.h. const char *const name;

            hier kann dann entweder die UID fest kodiert stehen wie in
            meinem einfachen Beispiel oder eben eine Logik, die den
            Pfad des ausgeführten Programms herausfindet und die UID
            über stat() ausliest (wird relativ kompliziert, wenn man es
            richtig machen will)

            Im Prinzip ist es doch keine Gefahr, die UID hart zu kodieren, weil wenn es nicht die UID des Scriptes ist, scheitert der User-Wechsel ja sowieso, korrekt so weit?

            Ja.

            Viele Grüße,
            Christian

            --
            "I have always wished for my computer to be as easy to use as my telephone; my wish has come true because I can no longer figure out how to use my telephone." - Bjarne Stroustrup