Socket race conditions

Материал из ALT Linux Wiki

Простая задача: создать сокет, поправить у него права (например, для группового доступа) и передать соответствующей группе (например №10).

Рассмотрим код, который ровно это реализует. Для того, чтобы убедиться в наличии небезопасных race conditions, добавим в него просмотр текущих прав на сокет.

#include <stdio.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/stat.h>
#define TESTSOCKET "/tmp/testsocket"

void pstat(const char *path) {
  static int iter = 1;
  struct stat info;

  stat(path, &info);
  printf("%d) %4d:%-4d %o\n",iter++, info.st_uid, info.st_gid, info.st_mode);
}

void main(void) {
  struct sockaddr_un addr = { AF_UNIX, TESTSOCKET };
  struct stat info;
  int sock = socket(AF_UNIX, SOCK_STREAM, 0);

  bind(sock, (struct sockaddr *) &addr, sizeof(struct sockaddr_un));
  pstat(TESTSOCKET);
  chmod(TESTSOCKET, 0660);
  pstat(TESTSOCKET);
  chown(TESTSOCKET, -1, 10);
  pstat(TESTSOCKET);
  shutdown(sock, SHUT_RDWR);
  unlink(TESTSOCKET);
}

компилируем, запускаем, наблюдаем следующее:

1)  500:500  140755
2)  500:500  140660
3)  500:10   140660

Это значит буквально вот что:

  1. После создания сокет получает права в соответствие с umask. В нашем случае umask не соответствует результату; хорошо ещё, что не 0 , а ведь бывает и так! В это время сокет доступен на чтение всем.
  2. После chmod() сокет доступен на запись кому не надо: членам группы 500. В нашем случае это не страшно, но если бы запускающий процесс имел какую-нибудь более популярную группу в качестве основной, на это время сокет стал бы доступен на чтение-запись всем её членам.
  3. После chown() наконец-то всё приходит в порядок.

Защита с помощью directory traversal

Есть два способа избежать небезопасных гонок. Самый простой — оставить в покое сам сокет и ограничивать права на каталог, в котором он заводится. В отличие от сокета, каталог можно завести заранее, выдать ему права, допустим "500:10 0750". Тогда сокет, заведённый в этом каталоге, не будет доступен кому не надо в любом случае. Что, конечно, не отменяет chmod() (да хоть 0666).

Защита с помощью явного указания umask

Если по каким-то причинам перемещать сокет нельзя, необходимо сначала выставить umask построже (например, 0777), затем делать chown(), и только затем — chmod().

#include <stdio.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/stat.h>
#define TESTSOCKET "/tmp/testsocket"

void pstat(const char *path) {
  static int iter=1;
  struct stat info;

  stat(path, &info);
  printf("%d) %4d:%-4d %o\n",iter++, info.st_uid, info.st_gid, info.st_mode);
}

void main(void) {
  struct sockaddr_un addr = { AF_UNIX, TESTSOCKET };
  struct stat info;
  int sock = socket(AF_UNIX, SOCK_STREAM, 0);
  mode_t oldumask = umask(0777);

  bind(sock, (struct sockaddr *) &addr, sizeof(struct sockaddr_un));
  umask(oldumask);
  pstat(TESTSOCKET);
  chown(TESTSOCKET, -1, 10);
  pstat(TESTSOCKET);
  chmod(TESTSOCKET, 0660);
  pstat(TESTSOCKET);
  shutdown(sock, SHUT_RDWR);
  unlink(TESTSOCKET);
}

Этот код работает так:

1)  500:500  140000
2)  500:10   140000
3)  500:10   140660

Соответственно, сокет становится доступен только после последней операции.