当我具有正确的功能时,无法打开/ proc / self / oom_score_adj

时间:2018-06-14 17:54:47

标签: c linux

我正在尝试为受oom_adjust_setup in OpenSSH's port_linux.c启发的流程设置OOM杀手分数调整。为此,我打开/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

为什么这样做?我错过了什么?

进一步的谷歌搜索引导我this lkml post by Azat Khuzhin on 20 Oct 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。我的问题类似但与this question不同:那是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;
}

2 个答案:

答案 0 :(得分:13)

破解这个很有趣,花了我一段时间。

第一个真正的提示是对另一个问题的答案:https://unix.stackexchange.com/questions/364568/how-to-read-the-proc-pid-fd-directory-of-a-process-which-has-a-linux-capabil-只是想感谢。

它不能按原样工作的原因

如果该进程具有 any 功能,则您得到“权限被拒绝”的真正原因是/proc/self/下的文件归root所有-与CAP_SYS_RESOURCE或{ {1}}个文件。您可以通过调用oom_*并使用其他功能来验证这一点。引用stat

  

/ 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 5 proc

  

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))设置用户ID或设置组ID程序,导致有效用户ID或有效组ID发生更改。

    < / li>   
  • 但该过程将执行(execve(2))具有文件功能的程序(请参阅功能(7)),但前提是所获得的允许能力超过了该过程已允许的能力。

  •   
     

不能转储的进程不能通过ptrace(2)PTRACE_ATTACH附加;有关更多详细信息,请参见ptrace(2)。

     

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

现在很清楚:我们的进程具有启动它的外壳所没有的功能,因此dumpable属性设置为false,因此man prctl下的文件由root拥有,而不是当前用户拥有。

如何使其工作

此修复很简单,只需在尝试打开文件之前重新设置该dumpable属性即可。打开文件前,粘贴以下内容或类似内容:

/proc/self/

希望有帮助;)

答案 1 :(得分:2)

这不是答案(对上述问题的dvk already provided the answer),而是扩展的注释,描述了减少/proc/self/oom_score_adj时经常被忽视的,可能非常危险的副作用。

总而言之,使用prctl(PR_SET_DUMPABLE, 1, 0, 0, 0)将允许具有CAP_SYS_RESOURCE功能(例如通过文件系统功能传达)的进程修改同一用户(包括他们自己)拥有的任何其他进程的oom_score_adj

(默认情况下,具有此功能的进程是不可转储的,因此即使该进程被其处置目的是生成内核的信号杀死,也不会生成核心转储。)

我要评论的危险是oom_score_adj range 的继承方式,以及对创建子进程的进程进行更改的含义。 (感谢dvk的一些更正。)


Linux内核为每个进程维护一个内部值oom_score_adj_min。用户(或进程本身)可以将oom_score_adj修改为oom_score_adj_minOOM_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也设置为相同的值。

(您可以通过检查Linux内核中的fs/proc/base.c:__set_oom_adj()进行验证;请参阅对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值(从-10001000,包括端点值),只有一千个减少了进程被杀死的机会(否定的,默认为零),与“默认”相比,一个邪恶的进程只需要对二进制文件/proc/self/oom_score_adj进行十到十一次写操作,就可以通过使用二进制搜索使OOM杀手尽可能避免这种情况:首先,它将尝试-500。如果成功,则oom_score_adj_min在-1000和-500之间;如果失败,则oom_score_adj_min在-499和1000之间。尝试时,可以根据初始oom_score_adj的值,在10或11次写入中将oom_score_adj_min设置为该进程oom_score_adj的内核内部最小值。)


当然,有一些缓解措施和策略可以避免继承问题。

例如,如果您有一个重要的进程应由OOM杀手单独处理,而不应该创建子进程,则应使用专用的用户帐户(将RLIMIT_NPROC设置为适当的较小值)运行该进程

如果您拥有创建新的子进程的服务,但是您希望父进程比其他进程少被OOM杀死,并且您不希望子进程继承该进程,则有两种方法可行。 / p>

  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_min下面。即使修改了oom_score_adj_minoom_score_adj也保持不变。

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