Programmaufrufe innerhalb von C/C++-Programmen

weiter zu Aufruf von Programmen in Windows

Aufruf von Programmen in Linux, Teil 4: Weitere Aufräumarbeiten - Dateideskriptoren

zurück zu Teil 3: Aufräumarbeiten - Fehlerbehandlung

Zur Erinnerung das Vorgehen in Kurzform:

  1. Einen Kind-Prozess erzeugen mit vfork() oder fork()
  2. Mit exec() den Kind-Prozess durch das neue Programm ersetzen
  3. Aufräumarbeiten:
    • Im Fehlerfall von exec() den Kind-Prozess beenden (Teil 1)
    • Das Auftreten von Zombies verhindern (Teil 2)
    • Fehlerbehandlung (Teil 3)
    • Unerwünschte Dateideskriptoren aufräumen:
      • entweder ihr Auftreten verhindern über fcntl(fd, F_SETFD, FD_CLOEXEC)
      • oder sie explizit schließen über close(fd).


Handles oder Dateideskriptoren (file descriptors )

Dateideskriptoren sind Referenzen zur Verwendung von Dateien durch andere Prozesse, mit denen Zugriffe geregelt werden. Sie bestehen nur aus einer einzigen Zahl. Die Zahlen 0, 1 und 2 sind für die Standard-Deskriptoren stdin, stdout, stderr reserviert. Allen anderen Dateideskriptoren wird ein Integer-Wert > 2 zugewiesen, üblicherweise der nächst verfügbare Wert.
In Linux ist alles eine Datei, dem entsprechend finden wir geöffnete Dateideskriptoren in einem Ordner: /proc/PID/fd/, wobei PID durch die Prozesskennung ersetzt werden muss.

Das Problem

Im Falle von fork() und vfork() werden alle geöffneten Dateideskriptoren des Eltern-Prozesses an den Kind-Prozess weitergegeben. exec() überschreibt eigentlich den Kind-Prozess, aber die Dateideskriptoren als Kopie werden übernommen. Das ermöglicht eine Kommunikation zwischen diesen beiden Prozessen und ist für viele Anwendungen praktisch. Wenn diese Kommunikation nicht beabsichtigt ist, wie im hiesigen Falle, muss entweder die Übergabe verhindert oder die Dateideskriptoren müssen manuell geschlossen werden. Andernfalls tauchen sie beim Kind-Prozess wieder auf.
Dateideskriptoren nehmen wenig Speicherplatz ein (64 Bytes), deshalb fallen sie zunächst nicht weiter auf. Es besteht eher die Gefahr - vor allem bei Servern -, dass irgendwann die maximal zugelassene Anzahl geöffneter Dateideskriptoren überschritten wird und ein anderer Prozess sich deshalb unerwartet beendet.
file descriptor "leaks" können auch ein Sicherhetsproblem werden, denn sie erlauben Kind-Prozessen über die Dateideskriptoren des Eltern-Prozesses eigentlich unautorisierte I/O-Prozesse auszuführen. Im hiesigen Fall der silidock dürfte das kaum eine Rolle spielen, denn nur wenn die Zugriffsrechte des Kind-Prozesses geringer sind als die des Eltern-Prozesses bedeutet das ein Sicherheitsrisiko.
Im Falle der silidock werden nicht viele Dateideskriptoren geöffnet. Neben den drei Standard-Deskriptoren werden Dateideskriptoren geöffnet, wenn ein Info-Fenster angezeigt wird, das einen Text aus einer Datei liest, wenn z.B. die Position oder Größe der silidock verändert wird durch das Bearbeiten der Konfigurationsdatei oder wenn neue Programme/Button hinzugefügt werden. Diese Dateideskriptoren würden ohne weiteres Zutun an sämtliche aufgerufenen Programme vererbt. Die Anzahl der Dateideskriptoren ist für den Anwendungsfall hier noch überschaubar und kann kaum größer als acht sein; in anderen Programmen ist deren Anzahl wesentlich größer.
Es ist auf jeden Fall eine gute Idee, unerwünschte offene Dateideskriptoren zu schließen.

Es ist jedoch keine gute Idee, die Standard-Dateideskriptoren 0, 1, 2 zu schließen, denn diese werden von vielen Programmen benötigt und deren Vererbung stellt auch kein Sicherheitsrisiko dar.

Ein anderes Problem im Zusammenhang mit den Dateideskriptoren: Wenn der Elternprozess stdin (0) stdout(1) und stderr(2) schließt und dann eine Datei öffnet, kann für diesem Dateizugriff ein nun freie file descriptor 0, 1 oder 2 zugewiesen werden, die eigentlich für den Standard-Input-Output vorgesehen sind. Das Benutzen des Standard-Output kann dann unerwarteterweise dazu führen, dass in die Datei geschrieben wird. Mit
#include <fcntl.h>
int set_lowest_fd( int fd ); // fd: The minimum file descriptor that you want to get
kann am Anfang der main-Funktion verhindern, dass bei einer Funktion wie open() oder pipe() ein Dateideskriptor zurückgegeben wird, der kleiner als 3 ist, also nicht die Standard-File-Deskriptoren überschreiben kann (beeinflusst nicht dub() ).

Lösungsmöglichkeiten

Zwei Lösungen sind üblich:
1. Alle geöffneten Dateideskriptoren manuell schließen vor dem Aufruf von exec().
Das Schließen ist einfach, ein
close(fd);
genügt. Allerdings müssen wir dafür den Dateideskriptor (fd), den Integer-Wert, ermitteln. Das ist komplizierter. In Linux werden alle geöffneten Dateideskriptoren eines Prozesses als Datei im Verzeichnis /proc/PID/fd/ abgelegt (PID ist die jeweilige Prozesskennung und kann mit getpid() ermittelt werden).

				
// C++ Header
#include <iostream>
#include <cerrno>

// C-Header
extern "C" {
// for directory content of /proc/PID/fd/
#include <dirent.h>
}
				
const char* prefix = "/proc/";
const char* suffix = "/fd/";
char child_proc_dir[16]; // directory contains file descriptors
sprintf(child_proc_dir,"%s%d%s",prefix,child_pid, suffix);

DIR *dir;
struct dirent *ent;

if ((dir = opendir (child_proc_dir)) != NULL) {
   // get files and directories within directory
   while ((ent = readdir (dir)) != NULL) {
      // convert file name to int
      char* end;
      int fd = strtol(ent->d_name, &end, 32);
      if (!*end) // Converted successfully: valid file descriptor
      {
         if (fd > 2) // do not close standard file descriptors stdin, stdout, stderr
         {
            close(fd); // close the file descriptor
         }
      }
   }
   closedir (dir);
} else {
   cerr<< "can not open directory: " << child_proc_dir << endl;
}		    			
    			

Diese Variante ist mehr oder weniger Linux-spezifisch. Auf FreeBSD dürfte sie beispielsweise nicht laufen. Eine Unix-weit laufende Variante ist, alle möglichen Dateideskriptoren durchzugehen und zu schließen. Dafür muss zunächst mit sysconf(_SC_OPEN_MAX) die maximale Anzahl ermittelt werden:

				
// close file descriptors before exec:
// This should work for all UNIX, but with worse performance
int fd;
int MAX_FD_NUMBER = sysconf(_SC_OPEN_MAX);
// get all eventually open file descriptors
for (fd = 3;  fd < MAX_FD_NUMBER;  ++fd)
{
   close(fd);// ignore if it fails with EBADF
}			

				
Das sind viele close()-Aufrufe dafür, dass oft - wie im Beispiel der silidock - lediglich die drei Standard-Deskriptoren geöffnet sind.
Wenn das Programm bekannt und somit die Anzahl der möglicherweise geöffneten Dateideskriptoren bekannt ist, kann statt der maximalen Anzahl der Dateideskriptoren auch eine niedrigere gewählt werden. Im Falle der silidock dürften beispielsweise 64 einen ausreichenden Sicherheitsabstand darstellen. Zusätzlichen Dateideskriptoren zu den Standard-Deskriptoren wird in der Regel der nächst höhere freie Integer-Wert zugeordnet, also 3, 4,...
Alle diese Code-Beispiele müssen NACH dem Aufruf von fork() und VOR dem Aufruf von exec() stehen, damit sie ihre Wirkung entfalten können.

2. Das "close-on-exec"-Flag:
Eine weitere Lösungsmöglichkeit ist das Setzen eines bestimmten Flags für die betreffenden file descriptors. Das close-on-exec-Flag "FD_CLOEXEC" sorgt dafür, dass die damit versehenen file descriptors beim Aufruf von exec*() geschlossen werden. Wie in den Beispielen oben müssen dafür zunächst die file descriptors ermittelt werden. Im obigen Code kann also lediglich der close(fd)-Aufruf durch folgende Zeile ersetzt werden:
fcntl( fd, F_SETFD, FD_CLOEXEC )
"F_SETFD" kennzeichnet, dass ein Flag für den file descriptor gesetzt wird, nämlich das close-on-exec-Flag "FD_CLOEXEC".
Wir müssen dafür noch einen Header einbinden:
#include <fcntl.h>
Die zweite Variante ist üblich, wenn mit externen Bibliotheken gearbeitet wird, in denen exec*() aufgerufen wird, aber möglicherweise die file descriptors nicht geschlossen werden.
Die close-on-exec-Schleife kann auch vor dem fork()-Aufruf stehen, sofern dazwischen keine weiteren file descriptors geöffnet werden.

Kontrolle

Um zu testen, ob tatsächlich file descriptors geschlossen wurden:
PID ermitteln und das selbe Programm einmal mit und einmal ohne close(fd) aufrufen und beide Male im Verzeichnis /proc/PID/fd nachsehen: im zweiten Fall müssten mehr file descriptors vorhanden sein als im ersten Fall.
In Linux können die file descriptors eines Prozesses im Terminal einfach angezeigt werden mit
ls -l /proc/PID/fd
Für "PID" muss die jeweilige Prozesskennung angegeben werden, die sich im Programm mit getpid() ermitteln lässt.


Der Code-Schnipsel zum Aufruf sieht dann folgendermaßen aus:
(dies ist ein Ausschnitt aus dem Quellcode von silidock):

// C++ Header
#include <iostream.h>
#include <cerrno.h>
// C-Header
extern "C" {
   #include <unistd.h> // fork, execv, getpid  
   #include <sys/wait.h> // for signal, waitpid
   #include <errno.h> // fork, execv, getpid  
   #include <dirent.h>// for directory content of /proc/PID/fd/   
}

void execute_program(const char* program_call, const char* param )
{
    pid_t child = vfork();

    if(child == 0) // executed by child process: 
    {
        int child_pid = getpid();
			
        // replace the child process with exec*-function
        char *args[2]; // arguments for exec
        args[0] = (char*)program_call; // first argument is program_call
        args[1] = (char*)param;
        args[2] = NULL; // no arguments...
        
        // close file descriptors > 2
        const char* prefix = "/proc/";
        const char* suffix = "/fd/";
        char child_proc_dir[16]; // directory contains file descriptors
        sprintf(child_proc_dir,"%s%d%s",prefix,child_pid, suffix);

        DIR *dir;
        struct dirent *ent;

        if ((dir = opendir (child_proc_dir)) != NULL) {
           // get files and directories within directory
           while ((ent = readdir (dir)) != NULL) 
           {
               // convert file name to int
               char* end;
               int fd = strtol(ent->d_name, &end, 32);
               if (!*end) // Converted successfully: valid file descriptor
               {
                  if (fd > 2)
                  {
                     close(fd); // close the file descriptor
                  }
               }
            }
            closedir (dir);
        } else {
           cerr<< "can not open directory: " << child_proc_dir << endl;
        }        

        execv(program_call,args);

        // executes if execv failed
        _exit(2);
    }
    else if (child == -1) // fork error: child < 0
    {
    	string fork_note = "fork failed for program: \n" + string(program_call);
    	string fork_error = "";

        if (errno == EAGAIN)
        {
           fork_error = "\n To much processes";
        }
        else if (errno == ENOMEM)
        {
           fork_error = "\n Not enough space available.";
        }
        else 
        {
           fork_error = "\n "\n Unexpected error: " + errno;
        }        
        string message = fork_note + fork_error;
        fl_alert( message.c_str() );
        Fl::run();
    }
    else // this is executed by parent process
    {
      usleep(50); // give some time to get status of child: 50 microseconds

      // get und store useful errors of exec:
      string child_error = "";
      if ( errno == EACCES)
      {
         child_error = "\n Permission denied or process file not executable.";
      }
      else if ( errno == ENOENT)
      {
         child_error = "\n Invalid path or file.";
      }
      else if ( errno == EPERM)
      {
         child_error = "\n Superuser privileges required.";
      }
      else if ( errno == ENOEXEC)
      {
         child_error = "\n Unsupported format of file.";
      }
      else
      {
         child_error = "\n unexpected error:" + errno;
      }      
      int child_status;
      if ( waitpid(child, &child_status, WNOHANG | WUNTRACED) < 0) // waitpid failed
      {
         string waitpid_message = "Error - Execution failed: \n   " + string(program_call);
         string message = waitpid_message  + child_error;
         fl_alert( message.c_str() );
         Fl::run();
      }
      else if ( WIFEXITED( child_status )  &&  WEXITSTATUS( child_status ) != 0) // child process failed although waitpid does not
      {
         string waitpid_message = "Error - Process failed: \n   " + string(program_call);
         string message = waitpid_message  + child_error;
         fl_alert( message.c_str() );
         Fl::run();
      } 
       // prevent zombies: 
       pid_t p;
       // Reap all pending child processes
       do {
          p = waitpid(-1, NULL, WNOHANG);
       } while (p != (pid_t)0 && p != (pid_t)-1);
    }
}    			
   			
    			
Der Code-Schnipsel zur Ermittlung des Verzeichnisinhalts stammt aus: stackoverflow

weiter zu "Programmaufrufe in Windows"