Programmaufrufe innerhalb von C/C++-Programmen
weiter zu Aufruf von Programmen in WindowsAufruf von Programmen in Linux, Teil 4: Weitere Aufräumarbeiten - Dateideskriptoren
zurück zu Teil 3: Aufräumarbeiten - Fehlerbehandlung
Zur Erinnerung das Vorgehen in Kurzform:
- Einen Kind-Prozess erzeugen mit
vfork()oderfork() - Mit
exec()den Kind-Prozess durch das neue Programm ersetzen - 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).
- entweder ihr Auftreten verhindern über
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.
#include <fcntl.h>int set_lowest_fd( int fd ); // fd: The minimum file descriptor that you want to getkann 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"