Невозможно открыть /proc/self/oom_score_adj, когда у меня есть правильная возможность

Я пытаюсь настроить корректировку оценки убийцы OOM для процесса, вдохновленного oom_adjust_setup в OpenSSH port_linux.c. Для этого я открываю /proc/self/oom_score_adj, читаю старое значение и записываю новое значение. Очевидно, что мой процесс должен быть root или иметь возможность CAP_SYS_RESOURCE сделать это.

Я получаю результат, который я не могу объяснить. Когда у моего процесса нет возможности, я могу открыть этот файл и прочитать и записать значения, хотя значение, которое я пишу, не вступает в силу (достаточно справедливо):

$ ./a.out 
CAP_SYS_RESOURCE: not effective, not permitted, not inheritable
oom_score_adj value: 0
wrote 5 bytes
oom_score_adj value: 0

Но когда у моего процесса есть возможность, я даже не могу открыть файл: он не работает с EACCES:

$ sudo setcap CAP_SYS_RESOURCE+eip a.out
$ ./a.out 
CAP_SYS_RESOURCE: effective, permitted, not inheritable
failed to open /proc/self/oom_score_adj: Permission denied

Почему это так? Что мне не хватает?


Еще один гуглинг привел меня на этот lkml пост Азата Хужина 20 октября 2013 года. По-видимому, CAP_SYS_RESOURCE позволяет вам изменять oom_score_adj для любого процесса, кроме вас. Чтобы изменить собственную настройку оценки, вам необходимо объединить ее с CAP_DAC_OVERRIDE то есть отключить элементы управления доступом для всех файлов. (Если бы я этого хотел, я бы сделал эту программу setuid root.)

Итак, мой вопрос: как я могу достичь этого без CAP_DAC_OVERRIDE?


Я запускаю Ubuntu xenial 16.04.4, версию ядра 4.13.0-45-generic. Моя проблема аналогична, но отличается от этого вопроса: об ошибке при write, когда у вас нет возможности.

Моя примерная программа:

#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <sys/capability.h>

void read_value(FILE *fp)
{
  int value;
  rewind(fp);
  if (fscanf(fp, "%d", &value) != 1) {
    fprintf(stderr, "read failed: %s\n", ferror(fp) ? strerror(errno) : "cannot parse");
  }
  else {
    fprintf(stderr, "oom_score_adj value: %d\n", value);
  }
}

void write_value(FILE *fp)
{
  int result;
  rewind(fp);
  result = fprintf(fp, "-1000");
  if (result < 0) {
    fprintf(stderr, "write failed: %s\n", strerror(errno));
  }
  else {
    fprintf(stderr, "wrote %d bytes\n", result);
  }
}

int main()
{
  FILE *fp;

  struct __user_cap_header_struct h;
  struct __user_cap_data_struct d;

  h.version = _LINUX_CAPABILITY_VERSION_3;
  h.pid = 0;
  if (0 != capget(&h, &d)) {
      fprintf(stderr, "capget failed: %s\n", strerror(errno));
  }
  else {
      fprintf(stderr, "CAP_SYS_RESOURCE: %s, %s, %s\n",
          d.effective & (1 << CAP_SYS_RESOURCE) ? "effective" : "not effective",
          d.permitted & (1 << CAP_SYS_RESOURCE) ? "permitted" : "not permitted",
          d.inheritable & (1 << CAP_SYS_RESOURCE) ? "inheritable" : "not inheritable");
  }

  fp = fopen("/proc/self/oom_score_adj", "r+");
  if (!fp) {
    fprintf(stderr, "failed to open /proc/self/oom_score_adj: %s\n", strerror(errno));
    return 1;
  }
  else {
    read_value(fp);
    write_value(fp);
    read_value(fp);
    fclose(fp);
  }
  return 0;
}

Ответы

Ответ 1

Это было очень интересно взломать, заняло некоторое время.

Первый реальный намек - это ответ на другой вопрос: https://unix.stackexchange.com/questions/364568/how-to-read-the-proc-pid-fd-directory-of-a-process-which- has-a-linux-capabil - просто хотел дать кредит.

Причина, по которой это не работает

Настоящая причина, по которой вы получаете "разрешенное разрешение", есть файлы под /proc/self/ принадлежат root, если процесс имеет какие-либо возможности - это не о CAP_SYS_RESOURCE или о oom_* файлах. Вы можете проверить это, вызвав stat и используя разные возможности. Цитирующий man 5 proc:

/Proc/[PID]

Существует цифровой подкаталог для каждого запущенного процесса; подкаталог назван идентификатором процесса.

В каждом подкадре /proc/[pid] содержатся псевдо файлы и каталоги, описанные ниже. Эти файлы обычно принадлежат эффективному пользователю и эффективному идентификатору группы процесса. Тем не менее, в качестве меры безопасности, права собственности становятся root: root, если атрибут "dumpable" для процесса имеет значение, отличное от 1. Этот атрибут может меняться по следующим причинам:

  • Атрибут был явно задан с помощью операции prctl (2) PR_SET_DUMPABLE.

  • Атрибут был сброшен до значения в файле /proc/sys/fs/suid_dumpable (описано ниже) по причинам, описанным в prctl (2).

Сброс атрибута "dumpable" до 1 возвращает права собственности на файлы /proc/[pid]/* на реальный UID процесса и реальный GID.

Это уже намекает на решение, но сначала позвольте немного углубиться и увидеть, что man prctl:

PR_SET_DUMPABLE (начиная с Linux 2.3.20)

Установите состояние флага "dumpable", который определяет, создаются ли дампы ядра для вызывающего процесса при доставке сигнала, поведение по умолчанию которого заключается в создании дампа ядра.

В ядрах до 2.6.12 включительно, arg2 должен быть равен 0 (SUID_DUMP_DISABLE, процесс не является dumpable) или 1 (SUID_DUMP_USER, процесс невозможен). Между ядрами 2.6.13 и 2.6.17 также допускалось значение 2, которое вызывало любые двоичные файлы, которые, как правило, не были бы сброшены, чтобы быть сброшенными, считываемыми только корнем; по соображениям безопасности эта функция была удалена. (См. Также описание /proc/sys/fs/suid_dumpable в proc (5).)

Обычно этот флаг установлен на 1. Однако он сбрасывается в текущее значение, содержащееся в файле /proc/sys/fs/suid_dumpable (который по умолчанию имеет значение 0), в следующих случаях:

  • Идентификатор работоспособного пользователя или группы изменяется.

  • Идентификатор пользователя или группы файловой системы процесса изменяется (см. Учетные данные (7)).

  • Процесс выполняет (execve (2)) идентификатор set-user-ID или set-group-ID, что приводит к изменению либо эффективного идентификатора пользователя, либо эффективного идентификатора группы.

  • Процесс выполняет (execve (2)) программу с файловыми возможностями (см. Возможности (7)), но только если разрешенные возможности превышают те, которые уже разрешены для процесса.

Процессы, которые не поддаются давлению, не могут быть присоединены через ptrace (2) PTRACE_ATTACH; подробнее см. ptrace (2).

Если процесс не является отказоустойчивым, право собственности на файлы в каталоге process/proc/[pid] затрагивается, как описано в proc (5).

Теперь ясно: у нашего процесса есть возможность, что оболочка, используемая для ее запуска, не имела, поэтому атрибут dumpable был установлен в false, поэтому файлы под /proc/self/ принадлежат root, а не текущему пользователю.

Как заставить его работать

Исправление так же просто, как переустановка этого атрибута dumpable перед попыткой открыть файл. Перед открытием файла придерживайтесь следующего или чего-то подобного:

prctl(PR_SET_DUMPABLE, 1, 0, 0, 0);

Надеюсь, это поможет ;)

Ответ 2

Это не ответ (dvk уже предоставил ответ на указанный вопрос), но расширенный комментарий, описывающий часто упущенные, возможно очень опасные побочные эффекты, сокращения /proc/self/oom_score_adj.

Таким образом, использование prctl(PR_SET_DUMPABLE, 1, 0, 0, 0) позволит процессу с возможностью CAP_SYS_RESOURCE (переданным через, например, возможности файловой системы) изменять oom_score_adj любого другого процесса, принадлежащего одному и тому же пользователю, включая их собственные.

(По умолчанию процесс с возможностями не является дампным, поэтому дамп ядра не генерируется даже тогда, когда процесс убивается сигналом, задачей которого является создание ядра.)

Опасности, которые я хотел бы прокомментировать, - это то, как oom_score_adj диапазон oom_score_adj, и что это значит изменить его для процессов, которые создают дочерние процессы. (Спасибо dvk за некоторые исправления.)


Ядро Linux поддерживает внутреннее значение oom_score_adj_min для каждого процесса. Пользователь (или сам процесс) может изменить oom_score_adj на любое значение между oom_score_adj_min и OOM_SCORE_ADJ_MAX. Чем выше значение, тем вероятнее, что процесс должен быть убит.

Когда процесс будет создан, он наследует свой oom_score_adj_min от своего родителя. Первоначальный родитель всех процессов init имеет начальный oom_score_adj_min из 0.

Чтобы уменьшить oom_score_adj ниже oom_score_adj_min, процесс, который имеет привилегии суперпользователя или имеет CAP_SYS_RESOURCE и является dumpable, записывает новый балл в /proc/PID/oom_score_adj. В этом случае oom_score_adj_min также устанавливается в одно и то же значение.

(Вы можете проверить это, изучив fs/proc/base.c: __ set_oom_adj() в ядре Linux, см. назначения task->signal->oom_score_adj_min.)

Проблема заключается в том, что значение oom_score_adj_min, за исключением случаев, когда оно обновляется процессом, имеющим возможность CAP_SYS_RESOURCE. (Примечание: изначально я думал, что его невозможно воспитывать вообще, но я был неправ.)

Например, если у вас есть высокопроизводительный сервисный демон с уменьшенным значением oom_score_adj_min, работающим без возможности CAP_SYS_RESOURCE, увеличение oom_score_adj перед обработкой дочерних процессов приведет к тому, что дочерние процессы наследуют новый oom_score_adj, но оригинальный oom_score_adj_min. Это означает, что такие дочерние процессы могут уменьшить их oom_score_adj до уровня их родительского сервиса, без каких-либо привилегий или возможностей.

(Потому что есть только две тысячи и одна из возможных значений oom_score_adj (-1000 до 1000 включительно), и только тысяча из них уменьшает вероятность того, что процесс будет убит (отрицательные, нуль по умолчанию) по сравнению с " по умолчанию ", гнусный процесс должен выполнить только десять или одиннадцать /proc/self/oom_score_adj в /proc/self/oom_score_adj чтобы сделать убийцу OOM как можно больше, используя двоичный поиск: во-первых, он попытается -500. выполняется успешно, oom_score_adj_min находится между -1000 и -500. Если он терпит неудачу, oom_score_adj_min находится между -499 и 1000. Понижая диапазон при каждой попытке, он может установить oom_score_adj внутреннему минимуму ядра для этого процесса, oom_score_adj_min, в десяти или одиннадцати случаях записи, в зависимости от того, каково было начальное значение oom_score_adj.)


Разумеется, существуют смягчения и стратегии, позволяющие избежать проблемы наследования.

Например, если у вас есть важный процесс, который убийца OOM должен оставить в покое, это не должно создавать дочерние процессы, вы должны запустить его, используя специальную учетную запись пользователя, для которой RLIMIT_NPROC настроен на подходящее значение.

Если у вас есть служба, которая создает новые дочерние процессы, но вы хотите, чтобы у родителя была меньше вероятность того, что OOM был убит, чем другие процессы, и вы не хотите, чтобы дети наследовали это, существуют два подхода.

  1. При запуске вашего сервиса можно oom_score_adj дочерний процесс для создания дополнительных дочерних процессов, прежде чем он снизит свой oom_score_adj. Это заставляет дочерние процессы наследовать их oom_score_adj_minoom_score_adj) из процесса, который запустил службу.

  2. Ваша служба может сохранять CAP_SYS_RESOURCE в наборе CAP_PERMITTED, но при необходимости добавить или удалить его из набора CAP_EFFECTIVE.

    Когда CAP_SYS_RESOURCE находится в наборе CAP_EFFECTIVE, настройка oom_score_adj также устанавливает значение oom_score_adj_min в это же значение.

    Когда CAP_SYS_RESOURCE не находится в наборе CAP_EFFECTIVE, вы не можете oom_score_adj ниже соответствующего oom_score_adj_min. oom_score_adj_min не изменяется, даже когда oom_score_adj изменен.

oom_score_adj смысл положить работу, которая может быть отменена/убита в ситуации OOM в дочерние процессы с более oom_score_adj значениями oom_score_adj. Если возникает ситуация с OOM - например, на встроенном устройстве - у демона основной службы намного больше шансов выжить, даже когда рабочие процессы-дети убиваются. Разумеется, сам демон ядра не должен выделять динамическую память в ответ на запросы клиентов, так как любая ошибка в ней может не просто свернуть этот демон, а привести к остановке всей системы (в ситуации OOM, где в основном все, кроме оригинала причина, демона ядра, убивается).