La descrizione nella open(2)
pagina man fornisce alcuni indizi per iniziare:
O_PATH (since Linux 2.6.39)
Obtain a file descriptor that can be used for two purposes:
to indicate a location in the filesystem tree and to per‐
form operations that act purely at the file descriptor
level. The file itself is not opened, and other file oper‐
ations (e.g., read(2), write(2), fchmod(2), fchown(2),
fgetxattr(2), ioctl(2), mmap(2)) fail with the error EBADF.
A volte, non vogliamo aprire un file o una directory. Invece, vogliamo solo un riferimento a quell'oggetto filesystem per eseguire determinate operazioni (ad esempio, fchdir()
verso una directory a cui fa riferimento un descrittore di file che abbiamo aperto usando O_PATH
). Quindi, un punto banale: se questo è il nostro scopo, l'apertura con O_PATH
dovrebbe essere un po 'più economica, poiché il file stesso non viene effettivamente aperto.
E un punto meno banale: prima dell'esistenza di O_PATH
, il modo di ottenere un tale riferimento a un oggetto filesystem era aprire l'oggetto O_RDONLY
. Ma l'uso di O_RDONLY
richiede che abbiamo il permesso di lettura sull'oggetto. Tuttavia, ci sono vari casi d'uso in cui non è necessario leggere effettivamente l'oggetto: ad esempio, eseguire un binario o accedere a una directory ( fchdir()
) o raggiungere una directory per toccare un oggetto all'interno della directory.
Utilizzo con chiamate di sistema "* at ()"
Comune, ma non l'unico, uso di O_PATH
è di aprire una directory, in modo da avere un riferimento a tale directory per l'utilizzo con il "*" a chiamate di sistema, quali openat()
, fstatat()
, fchownat()
e così via. Questa famiglia di chiamate di sistema, che possiamo grosso modo pensare come i successori moderni alle chiamate di sistema più anziani con nomi simili ( open()
, fstat()
, fchown()
e così via), servono un paio di scopi, il primo dei quali si tocca quando si chiede " perché voglio usare un descrittore di file invece del percorso della directory? ". Se guardiamo più in basso nella open(2)
pagina man, troviamo questo testo (sotto un sottotitolo con la logica delle chiamate di sistema "* at"):
First, openat() allows an application to avoid race conditions
that could occur when using open() to open files in directories
other than the current working directory. These race conditions
result from the fact that some component of the directory prefix
given to open() could be changed in parallel with the call to
open(). Suppose, for example, that we wish to create the file
path/to/xxx.dep if the file path/to/xxx exists. The problem is
that between the existence check and the file creation step, path
or to (which might be symbolic links) could be modified to point
to a different location. Such races can be avoided by opening a
file descriptor for the target directory, and then specifying that
file descriptor as the dirfd argument of (say) fstatat(2) and ope‐
nat().
Per rendere questo più concreto ... Supponiamo di avere un programma che vuole eseguire più operazioni in una directory diversa dalla sua directory di lavoro corrente, il che significa che dobbiamo specificare alcuni prefissi di directory come parte dei nomi di file che usiamo. Supponiamo, ad esempio, che il nome percorso sia /dir1/dir2/file
e vogliamo eseguire due operazioni:
- Eseguire un controllo
/dir1/dir2/file
(ad esempio, chi possiede il file o a che ora è stato modificato l'ultima volta).
- Se siamo soddisfatti del risultato di quel controllo, forse vogliamo fare qualche altra operazione di filesystem nella stessa directory, ad esempio creando un file chiamato
/dir1/dir2/file.new
.
Ora, supponiamo prima di tutto che abbiamo fatto tutto usando le tradizionali chiamate di sistema basate sul nome percorso:
struct stat stabuf;
stat("/dir1/dir2/file", &statbuf);
if ( /* Info returned in statbuf is to our liking */ ) {
fd = open("/dir1/dir2/file.new", O_CREAT | O_RDWR, 0600);
/* And then populate file referred to by fd */
}
Supponiamo inoltre che nel prefisso della directory /dir1/dir2
uno dei componenti (diciamo dir2
) fosse in realtà un collegamento simbolico (che si riferisce a una directory) e che tra la chiamata a stat()
e la chiamata aopen()
una persona malintenzionata è stato in grado di cambiare la destinazione del collegamento simbolico dir2
per puntare a una directory diversa. Questa è una classica condizione di gara al momento del check-time-of-use. Il nostro programma ha controllato un file in una directory ma è stato quindi indotto a creare un file in una directory diversa, forse una directory sensibile alla sicurezza. Il punto chiave qui è che il nome del percorso /dir/dir2
sembrava lo stesso, ma ciò che fa riferimento è cambiato completamente.
Possiamo evitare questo tipo di problemi usando le chiamate "* at". Prima di tutto, otteniamo un handle che fa riferimento alla directory in cui eseguiremo il nostro lavoro:
dirfd = open("/dir/dir2", O_PATH);
Il punto critico qui è che dirfd
è un riferimento stabile alla directory a cui faceva riferimento il percorso /dir1/dir2
al momento della open()
chiamata. Se la destinazione del collegamento simbolico dir2
viene successivamente modificata, ciò non influirà su ciò a cui si dirfd
riferisce. Ora, possiamo fare il nostro check + operazione utilizzando il "* a" chiamate che sono equivalenti al stat()
e open()
chiamate di cui sopra:
fstatat(dirfd, ""file", &statbuf)
struct stat stabuf;
fstatat(dirfd, "file", &statbuf);
if ( /* Info returned in statbuf is to our liking */ ) {
fd = openat(dirfd, "file.new", O_CREAT | O_RDWR, 0600);
/* And then populate file referred to by fd */
}
Durante questi passaggi qualsiasi manipolazione dei collegamenti simbolici nel percorso /dir/dir2
non avrà alcun impatto: il segno di spunta ( fstatat()
) e l'operazione ( openat()
) sono garantiti nella stessa directory.
C'è un altro scopo nell'uso delle chiamate "* at ()", che si riferisce all'idea di "directory di lavoro correnti per thread" nei programmi multithread (e di nuovo potremmo aprire le directory usando O_PATH
), ma penso che questo uso sia probabilmente meno pertinente alla tua domanda e ti lascio leggere la open(2)
pagina man se vuoi saperne di più.
Utilizzo con descrittori di file per file normali
Un utilizzo O_PATH
con i file regolari è quello di aprire un file binario per il quale abbiamo il permesso di esecuzione (ma non necessariamente il permesso di lettura, in modo che non possiamo aprire il file con O_RDONLY
). Quel descrittore di file può quindi essere passato a fexecve(3)
per eseguire il programma. Tutto ciò che fexecve(fd, argv, envp)
sta facendo con il suo fd
argomento è essenzialmente:
snprintf(buf, "/proc/self/fd/%d", fd);
execve(buf, argv, envp);
(Sebbene, a partire da glibc 2.27, l'implementazione utilizzerà invece la execveat(2)
chiamata di sistema, sui kernel che forniscono quella chiamata di sistema.)