【问题标题】:Cannot open /proc/self/oom_score_adj when I have the right capability当我有正确的能力时无法打开 /proc/self/oom_score_adj
【发布时间】:2018-06-21 21:00:19
【问题描述】:

我正在尝试为流程设置 OOM 杀手分数调整,灵感来自 oom_adjust_setup in OpenSSH's 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

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


进一步的谷歌搜索让我找到了this lkml post by Azat Khuzhin on 20 Oct 2013。显然CAP_SYS_RESOURCE 可以让您更改oom_score_adj 以用于除您自己以外的任何进程。要更改您自己的分数调整,您需要将其与CAP_DAC_OVERRIDE 结合使用——即禁用所有文件的访问控制。 (如果我想要的话,我会把这个程序设置为 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;
}

【问题讨论】:

  • 对于初学者,在write_value(fp); 之后,您需要在read_value(fp); 之前再次rewind(fp);。否则,文件位置指示器为EOF,用于第二次读取。
  • 我很困惑。你引用了一个消息来源说你不能做你说你想做的事。你有特别的理由怀疑这个来源吗?
  • 据我所知,该邮件列表帖子从未得到权威答案。我想我的问题与该帖子中的问题相同:“这是设计使然,和/或是否有另一种方法可以在没有 suid/root 的情况下做到这一点?”

标签: c linux


【解决方案1】:

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

第一个真正的提示是对另一个问题的回答: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_RESOURCEoom_* 文件无关.您可以通过调用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 所没有的能力,因此可转储属性设置为 false,因此/proc/self/ 下的文件归 root 而非当前用户所有。

如何让它发挥作用

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

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

希望有所帮助;)

【讨论】:

  • 我已经验证,在打开和写入 oom_score_adj 文件之前使用 prctl(PR_SET_DUMPABLE, dumpable, 0, 0, 0); 之前执行 long dumpable = prctl(PR_GET_DUMPABLE, 0, 0, 0, 0); prctl(PR_SET_DUMPABLE, 1, 0, 0, 0);,允许具有 CAP_SYS_RESOURCE 但没有其他权限的进程将其 oom_score_adj 设置为之间的任何值OOM_SCORE_ADJ_MIN 和 OOM_SCORE_ADJ_MAX,包括在内。干得好!
  • 就这么简单。谢谢!
【解决方案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 范围 是如何继承的,以及对创建子进程的进程更改它意味着什么。 (感谢 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-&gt;signal-&gt;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,而无需任何特权或能力。

(因为只有两千零一个可能的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 设置为内核-该进程的内部最小值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_RESOURCECAP_EFFECTIVE 集中时,调整oom_score_adj 也会将oom_score_adj_min 设置为相同的值。

    CAP_SYS_RESOURCE 不在CAP_EFFECTIVE 集合中时,您不能将oom_score_adj 递减到对应的oom_score_adj_min 之下。即使 oom_score_adj 被修改,oom_score_adj_min 也不会改变。

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

【讨论】:

  • 关于“使用 prctl(PR_SET_DUMPABLE, 1, 0, 0, 0) 将允许具有 CAP_SYS_RESOURCE 能力(通过例如文件系统能力传递)的进程修改任何其他进程的 oom_score_adj 的语句” - - 这不太正确。只能调整在同一用户下运行的进程,不能调整任何其他进程
  • 我可能在吹毛求疵,但“唯一的“副作用”是,如果进程被导致核心转储的信号杀死,则实际上会生成核心转储。”也不是100%正确。核心转储不一定会生成,它可以生成,但受制于通常的控制机制,例如ulimits
  • “一个邪恶的进程只需要对 /proc/self/oom_score_adj 进行 10 或 11 次写入,就可以让 OOM 杀手尽可能地避免它” - 你能详细说明一下吗?进程如何低于父级的oom_score_adj_min?如果父母有-10作为分数调整,孩子继承它并在那里再次写入-10,那么总调整不会是-20,它仍然是-10
  • "如果新值小于oom_score_adj_min,则oom_score_adj_min也设置为相同的值。没有办法显式设置或提高oom_score_adj_min。只能减小。问题就在这里。” - 这不是它的工作方式,至少在最新的内核中是这样(但我实际上怀疑它在所有相当现代的内核中都是一样的)。如果用户具有CAP_SYS_RESOURCE 能力,oom_score_adj_min始终设置为oom_score_adj,而不仅仅是在它较低时。因此,如果流程有能力,它可以增加该限制。
  • 因此,如果带有CAP_SYS_RESOURCE 的进程将oom_score_adj 设置为100,然后放弃了能力(有效和允许),那么它将无法将oom_score_adj 设置为低于100
猜你喜欢
  • 1970-01-01
  • 2021-06-12
  • 2023-03-17
  • 1970-01-01
  • 2015-01-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多