Para que devo usar o `O_PATH` e como?

8

Eu uso uma distribuição baseada no Linux 4.x e recentemente notei que a open()chamada de sistema do kernel suporta um O_PATHsinalizador aberto.

Embora a manpágina contenha uma lista de chamadas de sistema com as quais teoricamente poderia ser usada, não entendo bem qual é a ideia. Eu open(O_PATH)apenas diretórios, em vez de arquivos? E se sim, por que quero usar um descritor de arquivo em vez do caminho do diretório? Além disso, a maioria das chamadas de sistema listadas não parece ser específica para diretórios; então, também abro arquivos regulares O_PATHpara obter seu diretório como um descritor de arquivos? Ou para obter um descritor de arquivo para eles, mas com funcionalidade limitada?

Alguém pode dar uma explicação convincente sobre o que O_PATHé, como e para que devemos usá-lo?

Notas:

  • Não há necessidade de descrever o histórico de como isso evoluiu (as páginas de manual relevantes mencionam alterações no Linux 2.6.x, 3.5 e 3.6), a menos que seja necessário - eu apenas me preocupo com como as coisas estão agora.
  • Por favor, não me diga para usar apenas libc ou outras instalações de nível superior, eu sei disso.
einpoklum
fonte
@ sebasth: De fato, está relacionado, mas: 1. Já é um pouco antigo e as coisas podem ter mudado. 2. Francamente, não entendi bem a essência da resposta.
Einpoklum 15/09
1
Você pode postar um comentário nessa pergunta perguntando se algo mudou.
Barmar 16/09

Respostas:

8

A descrição na open(2)página do manual fornece algumas dicas para começar:

   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.

Às vezes, não queremos abrir um arquivo ou diretório. Em vez disso, queremos apenas uma referência a esse objeto do sistema de arquivos para executar determinadas operações (por exemplo, fchdir()para um diretório referido por um descritor de arquivo que abrimos usando O_PATH). Portanto, um ponto trivial: se esse é o nosso objetivo, abrir com O_PATHdeve ser um pouco mais barato, pois o arquivo em si não é realmente aberto.

E um ponto menos trivial: antes da existência de O_PATH, a maneira de obter tal referência a um objeto do sistema de arquivos era abrir o objeto O_RDONLY. Mas o uso de O_RDONLYrequer que tenhamos permissão de leitura no objeto. No entanto, existem vários casos de uso em que não precisamos realmente ler o objeto: por exemplo, executar um binário ou acessar um diretório ( fchdir()) ou acessar um diretório para tocar em um objeto dentro do diretório.

Uso com chamadas do sistema "* at ()"

O comum, mas não o único, o uso de O_PATHé abrir um diretório, a fim de ter uma referência a esse diretório para uso com o "* a" chamadas de sistema, como openat(), fstatat(), fchownat()e assim por diante. Esta família de chamadas de sistema, que podemos mais ou menos pensam como os sucessores modernos para as chamadas de sistema mais velhos com nomes semelhantes ( open(), fstat(), fchown()e assim por diante), servem para dois propósitos, o primeiro dos quais você tocar em quando você perguntar " por que eu quero usar um descritor de arquivo em vez do caminho do diretório? ". Se olharmos mais abaixo na open(2)página de manual, encontramos este texto (em um subtítulo com a justificativa para as chamadas do 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().

Para tornar isso mais concreto ... Suponha que tenhamos um programa que deseja executar várias operações em um diretório que não seja o diretório de trabalho atual, o que significa que devemos especificar algum prefixo de diretório como parte dos nomes de arquivos que usamos. Suponha, por exemplo, que o nome do caminho seja /dir1/dir2/filee desejemos executar duas operações:

  1. Execute algumas verificações /dir1/dir2/file(por exemplo, quem possui o arquivo ou a que horas foi modificado pela última vez).
  2. Se estivermos satisfeitos com o resultado dessa verificação, talvez desejemos fazer alguma outra operação do sistema de arquivos no mesmo diretório, por exemplo, criando um arquivo chamado /dir1/dir2/file.new.

Agora, suponha que fizemos tudo usando chamadas de sistema tradicionais baseadas em nomes de caminhos:

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

Agora, além disso, suponha que no prefixo do diretório /dir1/dir2um dos componentes (digamos dir2) fosse realmente um link simbólico (que se refere a um diretório) e que entre a chamada para stat()e a chamada paraopen() uma pessoa mal-intencionada fosse possível alterar o destino do link simbólico dir2para apontar para um diretório diferente. Essa é uma condição clássica de corrida no momento do check-in-time. Nosso programa verificou um arquivo em um diretório, mas foi levado a criar um arquivo em um diretório diferente - talvez um diretório sensível à segurança. O ponto principal aqui é que o nome do caminho /dir/dir2parecia o mesmo, mas o que se refere mudou completamente.

Podemos evitar esse tipo de problema usando as chamadas "* at". Primeiro, obtemos um identificador referente ao diretório em que faremos nosso trabalho:

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

O ponto crítico aqui é que dirfdé uma referência estável ao diretório que foi referido pelo caminho /dir1/dir2no momento da open()chamada. Se o destino do link simbólico dir2for alterado posteriormente, isso não afetará o que dirfdse refere. Agora, podemos fazer nossa operação de verificação + usando as chamadas "* at" equivalentes às chamadas stat()e open()acima:

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 essas etapas, qualquer manipulação de links simbólicos no nome do caminho /dir/dir2não terá impacto: a verificação ( fstatat()) e a operação ( openat()) são garantidas no mesmo diretório.

Há outro propósito em usar as chamadas "* at ()", relacionadas à idéia de "diretórios de trabalho atuais por thread" em programas multithread (e novamente poderíamos abrir os diretórios usando O_PATH), mas acho que esse uso provavelmente é menos relevante para sua pergunta e deixo que você leia a open(2)página de manual, se quiser saber mais.

Uso com descritores de arquivo para arquivos regulares

Um uso de O_PATHarquivos regulares é abrir um binário para o qual temos permissão de execução (mas não necessariamente permissão de leitura, para que não possamos abrir o arquivo O_RDONLY). Esse descritor de arquivo pode ser passado fexecve(3)para executar o programa. Tudo o que fexecve(fd, argv, envp)está fazendo com seu fdargumento é essencialmente:

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

(Embora, começando com glibc 2.27, a implementação faça uso da execveat(2)chamada do sistema, nos kernels que fornecem essa chamada do sistema.)

mtk
fonte
The problem is that between the existence check and the file creation step, path or to ... could be modified - não pode analisar esta frase. Mas eu entendo, eu acho. Portanto, serve como uma espécie de mecanismo de bloqueio em um diretório. Mas por que usar o open()resultado em vez de um bloqueio real?
einpoklum
@einpoklum o problema é que 'caminho' e 'para' não têm a formatação mostrada na página de manual original. Esses são componentes do nome do caminho hipotético "/ path / to / xxx". E não é como uma trava: é uma referência estável a um objeto do sistema de arquivos; vários programas podem ter essa referência ao mesmo objeto.
mtk