system? No, grazie! – considerazioni sul perché non usare la system(3) in C e C++

  ICT, Rassegna Stampa
image_pdfimage_print
[frase sulla t-shirt regalata a N.Van Orton/M.Douglas]: Sono stato drogato e lasciato per morto in Messico, e tutto quello che ho ottenuto è questa stupida maglietta.

Ebbene si, l’ho fatto un altra volta: nell’ultimo articolo avevo promesso una imminente seconda parte, e invece oggi parleremo d’altro (ma niente paura, la seconda parte su ZeroMQ arriverà presto! Fidatevi!). Succede che, come al solito, ottengo uno spunto da fatti reali (di solito per cose che faccio sul lavoro) e dico “Ma qui c’è materiale per un articolo!“, per cui oggi parleremo di system(3). Uhm, ma abbiamo un film collegato? Mah, visto che appartiene alla serie dei “No Grazie!” (ah: ne ho scritti altri e vi invito a leggerli o rileggerli, qui, qui, qui e qui) non farò giochi di parole con il titolo, e quindi ne sceglierò uno a caso tra quelli che ho visto/rivisto ultimamente… si, The Game, va benissimo: è un bel film del grande David Fincher, e non può mancare nella lista di un Cinefilo. Anzi, una attinenza film/articolo c’è: system(3) è una funzione anti-pattern, quindi chi la usa troppo può ridursi come il protagonista della foto qui sotto…

syetem(3)
…guardate come mi sono ridotto usando la system(3)…

system(3), è una funzione della libc usata e abusata: è molto comoda e semplice da usare e permette di fare cose, anche complicate, sfruttando applicazioni esterne: proprio questo è, allo stesso tempo, il suo maggior pregio e difetto. Chiariamolo subito: se avete bisogno di scrivere rapidamente una applicazione, diciamo, “sperimentale“, o un programma di test (o testing) ad uso interno, o volete semplicemente verificare (rapidamente) se una idea è realizzabile, nessuno vi può criticare se usate la system(3). Ma per una applicazione reale che andrà in produzione è corretto usarla? E vabbè, la risposta è una sola:

NO, ASSOLUTAMENTE NO!

Ok, è ora di entrare nel dettaglio. Cosa è la system(3) e cosa permette di fare? Il manuale della glibc titola semplicemente questo:

system - execute a shell command

quindi ci permette di chiamare, dal nostro programma, un comando esterno del sistema operativo (o meglio: un comando nostro o del sistema operativo chiamabile tramite la Shell).

(…apro una parentesi: come al solito l’articolo è focalizzato sui sistemi POSIX, ma le note che seguono relative all’uso, pregi e difetti della system(3) sono perfettamente adattabili anche a Windows, dove la stessa funzione è disponibile, con l’unica differenza che usa CMD.EXE invece della UNIX Shell o della Linux Bash. Chiudo la parentesi…)

Per cui, ad esempio, se vogliamo che il nostro programma mostri che file sono contenuti in una directory  che si chiama “test” possiamo, molto semplicemente, usare system(3) per chiamare il comando “ls ./test” usando ls(1) di Linux. Facilissimo, no? Vediamo un piccolo esempio di codice che vi farà gioire (spoiler: e subito dopo ve lo smonterò!):

#include <stdlib.h> // main() - funzione main
int main(int argc, char *argv[])
{ // uso system(3) per leggere il contenuto della directory ./test system("ls ./test"); exit(EXIT_SUCCESS);
}

Ok, system(3) è indubbiamente comoda da usare, ma si porta dietro una lista di problemi così lunga che si potrebbe scriverne in un libro. Vediamone alcuni:

  1. Non è per nulla portabile: non è possibile garantire al 100% che su tutti i sistemi dove verrà installata la mia applicazione i comandi da eseguire con system(3) siano disponibili.
  2. Apre, come minimo, due processi supplementari: su Linux invoca bash(1) che, a sua volta, invoca il comando da eseguire (alla faccia dell’efficienza).
  3.  Quando si chiama la system(3) il programma principale viene sospeso fino al termine del comando invocato, e non c’è nessuna maniera di controllare efficacemente quello che sta succedendo.
  4. Questo, poi, lo dice direttamente il manuale della system(3): “During execution of the command, SIGCHLD will be blocked, and SIGINT and SIGQUIT will be ignored, in the process that calls system().“, e questo non è una semplice descrizione di funzionamento, è la descrizione di un possibile problema.
  5. Anche questo, poi, lo dice direttamente il manuale della system(3): “Do not use system() from a privileged program (a set-user-ID or set-group-ID program, or a program with capabilities)“. La sicurezza del sistema può essere molto compromessa se la stringa di comando viene ad una fonte esterna e può essere manipolata: il grande pericolo si chiama Shell Command Injection.
  6. Un altro buco di sicurezza è possibile se il sistema non è completamente sicuro (cosa frequente…) e un utente malintenzionato può, in qualche maniera, alterare il path di ricerca degli eseguibili o sostituire un eseguibile con un altro con lo stesso nome.
  7. Non c’è alcun modo di leggere direttamente l’output del comando eseguito nel programma che invoca la system(3).
  8. Problemi vari ed eventuali: la system(3) in un loop non controllato, la system(3) e la thread-safety problematica, la system(3) per eseguire un comando in background, ecc. Una lunga lista che vi risparmio.

Insomma, è un vero disastro. Come dite? Ah si, sento alcuni che dicono “A me i problemi della system(3) non interessano, tanto io uso la popen(3)!“. Ok, ho delle cattive notizie: almeno la metà dei problemi elencati li ha anche la popen(3), che forse non è un disastro ma, direi, è un mezzo disastro, sai che consolazione…

Ah, solo come curiosità: nella lista dei problemi qui sopra quello che mi molesta di più è il numero 3: una applicazione che usa system(3) non ha maniera di controllare quello che sta succedendo con i programmi esterni invocati, e deve sospendersi per aspettare che finiscano l’esecuzione: ma questa vi sembra la descrizione di una buona applicazione da mandare in produzione? E vabbè, continuiamo cosi, facciamoci del male…

Quindi: che bisogna fare se abbiamo una attività buona per la system(3) ma non vogliamo usarla visto che siamo programmatori consapevoli? Abbiamo due soluzioni:

  1. La soluzione semplice ma accettabile: usare fork(2) + execv(3) + waitpid(2): è un po’ una emulazione, un po’ laboriosa, della system(3), che però evita i problemi elencati sopra: non usa la Shell, ci permette di mantenere il controllo sull’esecuzione, ci permette di evitare i problemi di shell-iniection, ecc. ecc.
  2. La soluzione complicata ma impeccabile: scrivere nel nostro programma il codice che vorremmo far eseguire esternamente con la system(3): in alcuni casi potrebbe risultare abbastanza laborioso, ma avremo tutto dentro il nostro codice, quindi avremo il controllo totale sia dell’esecuzione che della implementazione.

Che dite? Volete un esempio dei due casi? Magari con lo stesso tema di eseguire/emulare un comando “ls ./test“? Eccoli!

Caso 1: fork(2) + execv(3) + waitpid(2)
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <errno.h> // funzione main()
int main(int argc, char* argv[])
{ pid_t pid = fork(); if (pid == 0) { // sono il figlio char *pathname = "/usr/bin/ls"; char *newargv[] = { "ls", "./test", NULL }; execv(pathname, newargv); exit(EXIT_FAILURE); // exec non ritorna mai } else if (pid > 0) { // sono il padre int status; waitpid(pid, &status, 0); exit(EXIT_SUCCESS); } else { // errore fork() printf("error: %s\n", strerror(errno)); }
}
Caso 2: implemEntazione interna
#include <stdio.h>
#include <stdlib.h>
#include <dirent.h> // main() - funzione main
int main(int argc, char *argv[])
{ // apro la directory DIR *dir = opendir("./test"); if (dir) { // leggo una entry alla volta struct dirent *dir_ent; while ((dir_ent = readdir(dir)) != NULL) { // mostro solo se è un file if (dir_ent->d_type == DT_REG) printf("%s ", dir_ent->d_name); } // chiudo la directory e la linea printf("\n"); closedir(dir); } exit(EXIT_SUCCESS);
}

Che ne dite? Entrambi i casi sono interessanti, funzionali e non usano la system(3)! Se volete testarli basta creare (nella directory dove avete compilato il programma) una directory test con dentro qualche file (io ci ho copiato i sorgenti mostrati in questo articolo). Poi eseguite i programmi, et voilà!

E, già che ci sono, posso mostrarvi dei risultati reali: ho eseguito direttamente (con bash) il comando “ls ./test”  e poi ho eseguito e i tre programmi di test (quello con system(3) e i due senza). Il risultato è stato il seguente:

aldo@Linux $ ls ./test/
forkexecwait.c readdir.c system.c
aldo@Linux $ ./system
forkexecwait.c readdir.c system.c
aldo@Linux $ ./forkexecwait
forkexecwait.c readdir.c system.c
aldo@Linux $ ./readdir
readdir.c forkexecwait.c system.c

che era esattamente quello aspettato. Provare per credere!

Ok, per oggi può bastare: spero di avervi convinto che system(3) è una funzione anti-pattern, e di più non posso aggiungere. Nella seconda parte dell’articolo penso che tratteremo usi di ZeroMQ più sofisticati e usando un approccio high-level… oops! Ma questo è quello che avevo scritto alla fine dello scorso articolo! Speriamo, almeno questa volta, di riuscire a mantenere la promessa…

Ciao, e al prossimo post!

https://italiancoders.it/system-no-grazie-considerazioni-sul-perche-non-usare-la-system3-in-c-e-c/