我试图为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;
}
这个非常有趣破解,花了我一段时间。
第一个真正的暗示是对另一个问题的回答: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);
希望有所帮助;)
这不是一个答案(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_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
也设置为相同的值。
(您可以通过检查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
,而没有任何特权或功能。
(因为只有两千零一个可能的-1000
值(1000
到/proc/self/oom_score_adj
,包括在内),只有一千个减少了一个进程被杀死的机会(负面的,零是默认值)与“默认”相比一个邪恶的过程只需要对oom_score_adj_min
进行十次或十次写入,以使OOM杀手尽可能地避免使用二进制搜索:首先,它将尝试-500。如果成功,则oom_score_adj_min
介于-1000之间如果它失败了,oom_score_adj
介于-499和1000之间。通过在每次尝试中将范围减半,它可以将oom_score_adj_min
设置为该过程的内核内部最小值oom_score_adj
,在十或十一次写入中,取决于什么最初的RLIMIT_NPROC
值是。)
当然,有缓解和策略来避免继承问题。
例如,如果您有一个OOM杀手应该单独留下的重要进程,那不应该创建子进程,则应该使用将oom_score_adj
设置为适当小值的专用用户帐户运行它。
如果您有一个创建新子进程的服务,但您希望父进程比其他进程更少被OOM杀死,并且您不希望子进程继承该进程,则有两种方法可用。
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情况下可以取消/杀死的工作放入具有更高qazxswpoi值的子进程中是有意义的。如果确实发生了OOM情况 - 例如,在嵌入式设备上 - 核心服务守护程序具有更高的生存机会,即使工作者子进程被终止也是如此。当然,核心守护进程本身不应该为响应客户端请求而分配动态内存,因为它中的任何错误可能不会使该守护进程崩溃,而是使整个系统停止(在OOM情况下基本上除了原始之外的所有内容)因为核心守护进程被杀死了。