Wofür soll ich "O_PATH" verwenden und wie?

8

Ich verwende eine Linux 4.x-basierte Distribution und habe kürzlich festgestellt, dass der open()Systemaufruf des Kernels ein O_PATHoffenes Flag unterstützt.

Während die manSeite dafür eine Liste von Systemaufrufen enthält, mit denen sie theoretisch verwendet werden könnte, verstehe ich die Idee nicht ganz. Mache ich open(O_PATH)nur Verzeichnisse anstatt Dateien? Und wenn ja, warum möchte ich anstelle des Verzeichnispfads einen Dateideskriptor verwenden? Außerdem scheinen die meisten der dort aufgeführten Systemaufrufe nicht spezifisch für Verzeichnisse zu sein. öffne ich also auch reguläre Dateien mit O_PATH, um deren Verzeichnis irgendwie als Dateideskriptor zu erhalten? Oder um einen Dateideskriptor für sie zu bekommen, aber mit eingeschränkter Funktionalität?

Kann jemand eine überzeugende Erklärung geben, worum es O_PATHgeht und wie und wofür wir es verwenden sollen?

Anmerkungen:

  • Es ist nicht erforderlich, den Verlauf der Entwicklung zu beschreiben (in den entsprechenden Manpages werden Änderungen in Linux 2.6.x, 3.5 und 3.6 erwähnt), es sei denn, dies ist erforderlich. Ich kümmere mich nur darum, wie die Dinge jetzt sind.
  • Bitte sag mir nicht, ich soll nur libc oder andere übergeordnete Einrichtungen verwenden, das weiß ich.
einpoklum
quelle
@sebasth: Es ist in der Tat verwandt, aber: 1. Es ist mittlerweile ein bisschen alt und die Dinge haben sich möglicherweise geändert. 2. Ehrlich gesagt verstehe ich die Antwort nicht ganz.
Einpoklum
1
Sie können in dieser Frage einen Kommentar schreiben und fragen, ob sich etwas geändert hat.
Barmar

Antworten:

8

Die Beschreibung in der open(2)Manpage gibt zunächst einige Hinweise:

   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.

Manchmal möchten wir keine Datei oder ein Verzeichnis öffnen. Stattdessen möchten wir nur einen Verweis auf dieses Dateisystemobjekt, um bestimmte Vorgänge auszuführen (z. B. fchdir()auf ein Verzeichnis, auf das durch einen Dateideskriptor verwiesen wird, den wir mit geöffnet haben O_PATH). Ein trivialer Punkt: Wenn dies unser Ziel ist, O_PATHsollte das Öffnen mit etwas billiger sein, da die Datei selbst nicht geöffnet wird.

Und ein weniger trivialer Punkt: Vor der Existenz von O_PATHbestand die Möglichkeit, einen solchen Verweis auf ein Dateisystemobjekt zu erhalten, darin, das Objekt mit zu öffnen O_RDONLY. Für die Verwendung von ist O_RDONLYjedoch eine Leseberechtigung für das Objekt erforderlich. Es gibt jedoch verschiedene Anwendungsfälle, in denen das Objekt nicht tatsächlich gelesen werden muss: Zum Beispiel das Ausführen einer Binärdatei oder der Zugriff auf ein Verzeichnis ( fchdir()) oder das Greifen über ein Verzeichnis, um ein Objekt im Verzeichnis zu berühren.

Verwendung mit Systemaufrufen "* at ()"

Die gemeinsame, aber nicht die einzigen, die Verwendung von O_PATHist ein Verzeichnis zu öffnen, um einen Verweis auf das Verzeichnis für die Verwendung mit der „* bei“ Systemaufrufen zu haben, wie openat(), fstatat(), fchownat(), und so weiter. Diese Familie von Systemaufrufen, die wir als den modernen Nachfolger der älteren Systemaufrufe mit ähnlichen Namen grob denken kann ( open(), fstat(), fchown(), usw.), dienen ein paar Zwecke, von denen die erste auf Sie berühren , wenn Sie fragen " Warum möchte ich einen Dateideskriptor anstelle des Verzeichnispfads verwenden? ". Wenn wir weiter unten in der open(2)Manpage nachsehen , finden wir diesen Text (unter einer Unterüberschrift mit der Begründung für die Systemaufrufe "* 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().

Um dies konkreter zu machen ... Angenommen, wir haben ein Programm, das mehrere Operationen in einem anderen Verzeichnis als dem aktuellen Arbeitsverzeichnis ausführen möchte, was bedeutet, dass wir als Teil der von uns verwendeten Dateinamen ein Verzeichnispräfix angeben müssen. Angenommen, der Pfadname lautet /dir1/dir2/fileund wir möchten zwei Operationen ausführen:

  1. Führen Sie eine Überprüfung durch /dir1/dir2/file(z. B. wem die Datei gehört oder wann sie zuletzt geändert wurde).
  2. Wenn wir mit dem Ergebnis dieser Prüfung zufrieden sind, möchten wir möglicherweise eine andere Dateisystemoperation im selben Verzeichnis ausführen, z. B. eine Datei mit dem Namen /dir1/dir2/file.new.

Nehmen wir zunächst an, wir haben alles mit herkömmlichen Systemaufrufen auf der Basis von Pfadnamen durchgeführt:

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

Angenommen, im Verzeichnispräfix war /dir1/dir2eine der Komponenten (z. B. dir2) tatsächlich eine symbolische Verknüpfung (die sich auf ein Verzeichnis bezieht), und zwischen dem Aufrufstat()open() einer böswilligen Person und dem Aufruf einer böswilligen Person konnte das Ziel der geändert werden symbolischer Link dir2, um auf ein anderes Verzeichnis zu verweisen. Dies ist eine klassische Race-of-Check-Time-of-Use-Rennbedingung. Unser Programm überprüfte eine Datei in einem Verzeichnis, wurde dann aber dazu verleitet, eine Datei in einem anderen Verzeichnis zu erstellen - möglicherweise in einem sicherheitsrelevanten Verzeichnis. Der entscheidende Punkt hier ist, dass der Pfadname gleich /dir/dir2aussah, aber was er bezieht, hat sich komplett geändert.

Wir können diese Art von Problemen mit den "* at" -Aufrufen vermeiden. Zunächst erhalten wir ein Handle, das auf das Verzeichnis verweist, in dem wir unsere Arbeit erledigen werden:

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

Der kritische Punkt hierbei ist, dass dirfdes sich um eine stabile Referenz auf das Verzeichnis handelt, auf das der Pfad /dir1/dir2zum Zeitpunkt des open()Aufrufs verwiesen hat . Wenn das Ziel der symbolischen Verknüpfung dir2anschließend geändert wird, hat dies keine Auswirkungen auf das, worauf dirfdBezug genommen wird. Jetzt können wir unsere check + -Operation mit den "* at" -Aufrufen ausführen, die den obigen Aufrufen stat()und entsprechen open():

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

Während dieser Schritte hat jede Manipulation symbolischer Links im Pfadnamen /dir/dir2keine Auswirkungen: Die Prüfung check ( fstatat()) und die Operation ( openat()) finden garantiert im selben Verzeichnis statt.

Die Verwendung der Aufrufe "* at ()" hat einen anderen Zweck, der sich auf die Idee von "aktuellen Arbeitsverzeichnissen pro Thread" in Multithread-Programmen bezieht (und wir könnten die Verzeichnisse wieder mit öffnen O_PATH), aber ich denke, diese Verwendung ist wahrscheinlich weniger relevant für Ihre Frage, und ich überlasse es Ihnen, die open(2)Manpage zu lesen, wenn Sie mehr wissen möchten.

Verwendung mit Dateideskriptoren für reguläre Dateien

Eine Verwendung von O_PATHregulären Dateien besteht darin, eine Binärdatei zu öffnen, für die wir die Ausführungsberechtigung haben (aber nicht unbedingt die Leseberechtigung, damit wir die Datei nicht mit öffnen können O_RDONLY). Dieser Dateideskriptor kann dann übergeben werden fexecve(3), um das Programm auszuführen. Alles, fexecve(fd, argv, envp)was mit seiner fdArgumentation zu tun hat , ist im Wesentlichen:

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

(Ab Glibc 2.27 verwendet die Implementierung stattdessen den execveat(2)Systemaufruf auf Kerneln, die diesen Systemaufruf bereitstellen.)

mtk
quelle
The problem is that between the existence check and the file creation step, path or to ... could be modified - kann diesen Satz nicht analysieren. Aber ich verstehe das Wesentliche, denke ich. Es dient also als eine Art Sperrmechanismus für ein Verzeichnis. Aber warum sollte man das open()Ergebnis anstelle einer tatsächlichen Sperre verwenden?
Einpoklum
@einpoklum das Problem ist, dass 'Pfad' und 'zu' nicht die Formatierung haben, die in der ursprünglichen Manpage angezeigt wird. Dies sind Bestandteile des hypothetischen Pfadnamens "/ path / to / xxx". Und es ist nicht wie eine Sperre: Es ist eine stabile Referenz auf ein Dateisystemobjekt. Mehrere Programme haben möglicherweise einen solchen Verweis auf dasselbe Objekt.
MTK