Pourquoi suis-je censé utiliser «O_PATH» et comment?

8

J'utilise une distribution basée sur Linux 4.x, et j'ai récemment remarqué que l' open()appel système du noyau prend en charge un O_PATHindicateur ouvert.

Bien que la manpage correspondante contienne une liste d'appels système avec lesquels elle pourrait théoriquement être utilisée, je ne comprends pas très bien quelle est l'idée. Dois-je open(O_PATH)uniquement des répertoires, plutôt que des fichiers? Et si je le fais, pourquoi est-ce que je veux utiliser un descripteur de fichier au lieu du chemin du répertoire? De plus, la plupart des appels système répertoriés ne semblent pas être particuliers aux répertoires; alors, est-ce que j'ouvre également des fichiers réguliers avec O_PATHpour obtenir en quelque sorte leur répertoire en tant que descripteur de fichier? Ou pour obtenir un descripteur de fichier pour eux mais avec des fonctionnalités limitées?

Quelqu'un peut-il donner une explication convaincante de quoi il O_PATHs'agit et comment, et pourquoi, nous sommes censés l'utiliser?

Remarques:

  • Pas besoin de décrire l'historique de la façon dont cela a évolué (les pages de manuel pertinentes mentionnent les changements dans Linux 2.6.x, 3.5 et 3.6) à moins que cela ne soit nécessaire - je me soucie juste de la façon dont les choses sont maintenant.
  • S'il vous plaît, ne me dites pas d'utiliser simplement libc ou d'autres installations de niveau supérieur, je le sais.
einpoklum
la source
@sebasth: C'est en effet lié, mais: 1. C'est un peu vieux maintenant et les choses ont peut-être changé. 2. Franchement, je ne comprends pas vraiment l'essentiel de la réponse.
einpoklum
1
Vous pouvez poster un commentaire dans cette question en demandant si quelque chose a changé.
Barmar

Réponses:

8

La description dans la open(2)page de manuel donne quelques indices pour commencer:

   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.

Parfois, nous ne voulons pas ouvrir un fichier ou un répertoire. Au lieu de cela, nous voulons juste une référence à cet objet de système de fichiers afin d'effectuer certaines opérations (par exemple, fchdir()vers un répertoire auquel fait référence un descripteur de fichier que nous avons ouvert à l'aide O_PATH). Donc, un point trivial: si c'est notre objectif, alors l'ouverture avec O_PATHdevrait être un peu moins chère, car le fichier lui-même n'est pas réellement ouvert.

Et un point moins trivial: avant l'existence de O_PATH, la manière d'obtenir une telle référence à un objet de système de fichiers était d'ouvrir l'objet avec O_RDONLY. Mais l'utilisation de O_RDONLYnécessite que nous ayons l'autorisation de lecture sur l'objet. Cependant, il existe différents cas d'utilisation où nous n'avons pas besoin de lire réellement l'objet: par exemple, exécuter un binaire ou accéder à un répertoire ( fchdir()) ou atteindre un répertoire pour toucher un objet à l'intérieur du répertoire.

Utilisation avec les appels système "* at ()"

La commune, mais pas la seule, l' utilisation O_PATHest d'ouvrir un répertoire, afin d'avoir une référence à ce répertoire pour être utilisé avec le « * » à des appels système, tels que openat(), fstatat(), fchownat()et ainsi de suite. Cette famille d'appels système, que nous pouvons penser à peu près comme les successeurs modernes aux anciens appels système avec des noms similaires ( open(), fstat(), fchown(), etc.), servent deux buts, dont le premier vous touchez quand vous demandez " pourquoi est-ce que je veux utiliser un descripteur de fichier au lieu du chemin du répertoire? ". Si nous regardons plus loin dans la open(2)page de manuel, nous trouvons ce texte (sous une sous-rubrique avec la justification des appels système "* 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().

Pour rendre cela plus concret ... Supposons que nous ayons un programme qui souhaite effectuer plusieurs opérations dans un répertoire autre que son répertoire de travail actuel, ce qui signifie que nous devons spécifier un préfixe de répertoire dans le cadre des noms de fichiers que nous utilisons. Supposons, par exemple, que le chemin d'accès soit /dir1/dir2/fileet que nous voulons effectuer deux opérations:

  1. Effectuez une vérification /dir1/dir2/file(par exemple, à qui appartient le fichier ou à quelle heure a-t-il été modifié pour la dernière fois).
  2. Si nous sommes satisfaits du résultat de cette vérification, nous souhaitons peut-être alors effectuer une autre opération du système de fichiers dans le même répertoire, par exemple en créant un fichier appelé /dir1/dir2/file.new.

Supposons maintenant que nous ayons tout fait en utilisant des appels système basés sur des chemins traditionnels:

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 */
}

Maintenant, supposons en outre que dans le préfixe de répertoire, l' /dir1/dir2un des composants (disons dir2) était en fait un lien symbolique (qui fait référence à un répertoire), et qu'entre l'appel stat()et l'appel àopen() une personne malveillante a pu changer la cible du lien symbolique dir2pour pointer vers un répertoire différent. Il s'agit d'une condition de concurrence classique au moment de la vérification du temps d'utilisation. Notre programme a vérifié un fichier dans un répertoire mais a ensuite été amené à créer un fichier dans un répertoire différent - peut-être un répertoire sensible à la sécurité. Le point clé ici est que le chemin d'accès est /dir/dir2le même, mais ce à quoi il fait référence a complètement changé.

Nous pouvons éviter ce genre de problèmes en utilisant les appels "* at". Tout d'abord, nous obtenons un handle faisant référence au répertoire où nous ferons notre travail:

dirfd = open("/dir/dir2", O_PATH);

Le point critique ici est qu'il dirfds'agit d'une référence stable au répertoire auquel faisait référence le chemin /dir1/dir2au moment de l' open()appel. Si la cible du lien symbolique dir2est modifiée par la suite, cela n'affectera pas ce qui dirfdfait référence. Maintenant, nous pouvons effectuer notre opération check + en utilisant les appels "* at" qui sont équivalents aux appels stat()et open()ci-dessus:

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 */
}

Au cours de ces étapes, toute manipulation de liens symboliques dans le chemin /dir/dir2n'aura aucun impact: la vérification ( fstatat()) et l'opération ( openat()) sont assurées d'avoir lieu dans le même répertoire.

Il y a un autre but à utiliser les appels "* at ()", qui se rapporte à l'idée de "répertoires de travail actuels par thread" dans les programmes multithread (et encore une fois, nous pourrions ouvrir les répertoires en utilisant O_PATH), mais je pense que cette utilisation est probablement moins pertinent pour votre question, et je vous laisse lire la open(2)page de manuel si vous souhaitez en savoir plus.

Utilisation avec des descripteurs de fichiers pour les fichiers normaux

Une utilisation de O_PATHavec des fichiers normaux consiste à ouvrir un binaire pour lequel nous avons une autorisation d'exécution (mais pas nécessairement une autorisation de lecture, afin que nous ne puissions pas ouvrir le fichier avec O_RDONLY). Ce descripteur de fichier peut ensuite être transmis à fexecve(3)pour exécuter le programme. Tout ce que fexecve(fd, argv, envp)fait son fdargumentation est essentiellement:

snprintf(buf, "/proc/self/fd/%d", fd);
execve(buf, argv, envp);

(Bien que, à partir de la glibc 2.27, l'implémentation utilise à la place l' execveat(2)appel système, sur les noyaux qui fournissent cet appel système.)

mtk
la source
The problem is that between the existence check and the file creation step, path or to ... could be modified - ne peut pas analyser cette phrase. Mais je comprends l'essentiel, je pense. Il sert donc comme une sorte de mécanisme de verrouillage sur un répertoire. Mais pourquoi utiliser le open()résultat plutôt qu'un verrou réel?
einpoklum
@einpoklum, le problème est que 'chemin' et 'à' n'ont pas le formatage montré dans la page de manuel d'origine. Ce sont des composants du nom de chemin hypothétique "/ path / to / xxx". Et ce n'est pas comme un verrou: c'est une référence stable à un objet du système de fichiers; plusieurs programmes peuvent avoir une telle référence au même objet.
mtk