问题 当我具有正确的功能时,无法打开/ 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帖子由Azat Khuzhin于2013年10月20日发布。显然地 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;
}

2882
2018-06-14 17:54


起源

对于初学者来说,之后 write_value(fp);, 你需要 rewind(fp); 在你面前 read_value(fp); 再次。否则文件位置指示符为 EOF 为第二次阅读。 - David C. Rankin
我很困惑。你引用一个消息来源说你做不了你想做的事。你有特别的理由怀疑这个来源吗? - John Bollinger
据我所知,该邮件列表帖子从未获得权威答案。我想我的问题与那篇文章中的问题相同:“这是否符合设计,和/或是否有另一种方法可以在没有suid / root的情况下执行此操作?” - legoscia
如何使用 jdebp.eu./Softwares/nosh/guide/oom-kill-protect.html ? - Tarun Lalwani


答案:


这个非常有趣的破解,花了我一段时间。

第一个真正的提示是对不同问题的回答: 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]

每个正在运行的进程都有一个数字子目录;子目录由进程ID命名。

每个/ proc / [pid]子目录都包含下面描述的伪文件和目录。这些文件通常由进程的有效用户和有效组ID拥有。但是,作为安全措施,如果进程的“dumpable”属性设置为1以外的值,则将所有权设置为root:root。此属性可能会因以下原因而更改:

  • 通过prctl(2)PR_SET_DUMPABLE操作显式设置该属性。

  • 由于prctl(2)中描述的原因,该属性被重置为文件/ proc / sys / fs / suid_dumpable(如下所述)中的值。

将“dumpable”属性重置为1会将/ proc / [pid] / *文件的所有权还原为进程的真实UID和实际GID。

这已经暗示了解决方案,但首先让我们深入挖掘并看到它 man prctl

PR_SET_DUMPABLE(自Linux 2.3.20起)

设置“dumpable”标志的状态,该标志确定在传递其默认行为是产生核心转储的信号时是否为调用进程生成核心转储。

在内核(包括2.6.12)中,arg2必须为0(SUID_DUMP_DISABLE,进程不可转储)或1(SUID_DUMP_USER,进程可转储)。在内核2.6.13和2.6.17之间,也允许值2,这导致通常不会被转储的任何二进制文件只能被root用户转储。出于安全原因,此功能已被删除。 (另请参阅proc(5)中/ proc / sys / fs / suid_dumpable的说明。)

通常,此标志设置为1.但是,在以下情况下,它将重置为文件/ proc / sys / fs / suid_dumpable(默认情况下值为0)中包含的当前值:

  • 进程的有效用户或组ID已更改。

  • 进程的文件系统用户或组ID已更改(请参阅凭据(7))。

  • 该过程执行(execve(2))set-user-ID或set-group-ID程序,从而导致有效用户ID或有效组ID的更改。

  • 该进程执行(execve(2))具有文件功能的程序(参见功能(7)),但前提是所获得的允许功能超过了该进程已经允许的功能。

无法转储的进程无法通过ptrace(2)PTRACE_ATTACH附加;有关详细信息,请参阅ptrace(2)。

如果进程不可转储,则进程的/ proc / [pid]目录中的文件所有权会受到影响,如proc(5)中所述。

现在很明显:我们的进程具有用于启动它的shell没有的功能,因此dumpable属性设置为false,因此 /proc/self/ 由root而不是当前用户拥有。

如何使它工作

修复程序就像在尝试打开文件之前重新设置dumpable属性一样简单。在打开文件之前粘贴以下内容或类似内容:

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

希望有所帮助;)


12
2018-06-20 17:05



我已经证实这样做了 long dumpable = prctl(PR_GET_DUMPABLE, 0, 0, 0, 0); prctl(PR_SET_DUMPABLE, 1, 0, 0, 0); 在开放和写作之前 oom_score_adj 文件 prctl(PR_SET_DUMPABLE, dumpable, 0, 0, 0); 之后,允许进程使用CAP_SYS_RESOURCE但没有其他权限来设置它们 oom_score_adj 到OOM_SCORE_ADJ_MIN和OOM_SCORE_ADJ_MAX之间的任何值,包括在内。做得好! - Nominal Animal
就那么简单。谢谢! - legoscia


这不是答案(dvk已经提供了答案 对于所陈述的问题),但是延伸的评论描述了经常被忽视的,可能非常危险的副作用 /proc/self/oom_score_adj

总之,使用 prctl(PR_SET_DUMPABLE, 1, 0, 0, 0) 将允许具有CAP_SYS_RESOURCE功能的进程(通过例如文件系统功能传送)来修改 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且可转储的进程,将新分数写入 /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 通过二进制搜索使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_min (和 oom_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_minoom_score_adj_min 即使在 oom_score_adj 被修改了。

将在OOM情况下可以取消/杀死的工作放入具有更高级别的子进程中是有意义的 oom_score_adj 值。如果确实发生了OOM情况 - 例如,在嵌入式设备上 - 核心服务守护程序具有更高的生存机会,即使工作者子进程被终止也是如此。当然,核心守护进程本身不应该为响应客户端请求分配动态内存,因为它中的任何错误可能不会使该守护进程崩溃,而是使整个系统停止(在OOM情况下基本上除了原始之外的所有内容)因为核心守护进程被杀死了。


2
2018-06-21 03:30



关于“使用prctl(PR_SET_DUMPABLE,1,0,0,0)将允许具有CAP_SYS_RESOURCE功能的进程(通过例如文件系统功能传送)来修改任何其他进程的oom_score_adj”的语句“ - 这不太正确。只能调整在同一用户下运行的进程,而不是 任何 其他过程 - dvk
我可能会挑剔,但“唯一的”副作用“是,如果进程被导致核心转储的信号杀死,实际上会生成核心转储。”也不是100%正确。它不一定会产生核心转储 能够 生成,但受制于通常的控制机制,如 ulimit小号 - dvk
“一个邪恶的过程只需要对/ proc / self / oom_score_adj进行十次或十次写入,以使OOM杀手尽可能地避免它” - 你能详细说明吗?一个过程如何低于父母的过程 oom_score_adj_min?如果父母有 -10 作为分数调整,孩子继承并写作 -10 再一次,总调整不会 -20,它仍然会 -10 - dvk
“如果新值小于oom_score_adj_min,oom_score_adj_min也会设置为相同的值。无法显式设置或提升oom_score_adj_min。它只能被减少。这就是问题所在。” - 这不是它的工作原理,至少在最新的内核中(但实际上我怀疑它在所有合理的现代内核中都是一样的)。该 oom_score_adj_min 是 总是 设置为 oom_score_adj 如果用户有 CAP_SYS_RESOURCE 能力,不仅在它较低时。因此,如果某个流程具有此功能,则可以增加该限制。 - dvk
所以,如果一个过程 CAP_SYS_RESOURCE 套 oom_score_adj 到100然后放弃功能(有效和允许)然后它将无法设置 oom_score_adj 低于100的值 - dvk