Per cosa dovrei usare `O_PATH` e come?


8

Uso una distribuzione basata su Linux 4.x e di recente ho notato che la open()chiamata di sistema del kernel supporta un O_PATHflag aperto.

Sebbene la manpagina abbia un elenco di chiamate di sistema con cui potrebbe teoricamente essere utilizzata, non capisco bene quale sia l'idea. Devo open(O_PATH)solo le directory anziché i file? E se lo faccio, perché voglio usare un descrittore di file invece del percorso della directory? Inoltre, la maggior parte delle chiamate di sistema elencate non sembrano essere particolari per le directory; quindi, apro anche file regolari O_PATHper ottenere in qualche modo la loro directory come descrittore di file? O per ottenere un descrittore di file per loro ma con funzionalità limitate?

Qualcuno può dare una spiegazione convincente di cosa O_PATHsi tratta e come, e per cosa, dovremmo usarlo?

Appunti:

  • Non c'è bisogno di descrivere la storia di come questo si è evoluto (le relative pagine man menzionano i cambiamenti in Linux 2.6.x, 3.5 e 3.6) a meno che non sia necessario - mi interessa solo come stanno le cose adesso.
  • Per favore, non dirmi di usare solo libc o altre strutture di livello superiore, lo so.


@sebasth: È davvero correlato, ma: 1. Ormai è un po 'vecchio e le cose potrebbero essere cambiate. 2. Francamente, non riesco proprio a capire la risposta.
einpoklum,

1
Puoi pubblicare un commento in quella domanda chiedendo se qualcosa è cambiato.
Barmar,

Risposte:


8

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_PATHdovrebbe 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_RDONLYrichiede 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/filee vogliamo eseguire due operazioni:

  1. Eseguire un controllo /dir1/dir2/file(ad esempio, chi possiede il file o a che ora è stato modificato l'ultima volta).
  2. 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/dir2uno 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 dir2per 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/dir2sembrava 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/dir2al momento della open()chiamata. Se la destinazione del collegamento simbolico dir2viene successivamente modificata, ciò non influirà su ciò a cui si dirfdriferisce. 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/dir2non 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_PATHcon 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 fdargomento è 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.)


The problem is that between the existence check and the file creation step, path or to ... could be modified - Impossibile analizzare questa frase. Ma ne capisco l'essenza, penso. Quindi serve come una sorta di meccanismo di blocco in una directory. Ma perché usare il open()risultato piuttosto che un vero blocco?
einpoklum,

@einpoklum il problema è che 'path' e 'to' non hanno la formattazione mostrata nella pagina man originale. Questi sono componenti dell'ipotetico percorso "/ percorso / a / xxx". E non è come un lucchetto: è un riferimento stabile a un oggetto filesystem; diversi programmi potrebbero avere tale riferimento allo stesso oggetto.
mtk,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.