Невозможно открыть /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 был убит, чем другие процессы, и вы не хотите, чтобы дети наследовали это, существуют два подхода.
-
При запуске вашего сервиса можно oom_score_adj
дочерний процесс для создания дополнительных дочерних процессов, прежде чем он снизит свой oom_score_adj
. Это заставляет дочерние процессы наследовать их oom_score_adj_min
(и oom_score_adj
) из процесса, который запустил службу.
-
Ваша служба может сохранять 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, где в основном все, кроме оригинала причина, демона ядра, убивается).