Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions kill.c
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include <ctype.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <poll.h>
#include <signal.h>
Expand Down Expand Up @@ -233,6 +234,57 @@
notify_spawn_subprocess(args->kill_process_prehook, argv, victim, PREHOOK_STARTUP_SLEEP_MS);
}

/*
* Trigger the kernel OOM killer via /proc/sysrq-trigger
* This requires Linux v5.17+ to work correctly. OOM sysrq will always kill a process
* The victim passed to hooks is dummy. OOM killer will select its own victim
*
* See https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=f530243a172d2ff03f88d0056f838928d6445c6d
* for details about this feature.
*/
int trigger_kernel_oom_killer(const poll_loop_args_t* args)
{
const char* sysrq_path = "/proc/sysrq-trigger";
const char trigger = 'f';

// Check if we have permission to write to /proc/sysrq-trigger
// This check is also done in dryrun mode to warn about permission issues early
if (access(sysrq_path, W_OK) != 0) {
warn("%s: no permission to write to %s: %s\n", __func__, sysrq_path, strerror(errno));
if (args->dryrun) {
warn("dryrun: would fail to trigger kernel OOM killer due to permission denied\n");
}
return -1;
}

if (args->dryrun) {
warn("dryrun: not actually triggering kernel OOM killer\n");
return 0;
}

int fd = open(sysrq_path, O_WRONLY);
Comment thread Dismissed
if (fd < 0) {
warn("%s: failed to open %s: %s\n", __func__, sysrq_path, strerror(errno));
return -1;
}

ssize_t written = write(fd, &trigger, 1);
if (written != 1) {
warn("%s: failed to write to %s: %s\n", __func__, sysrq_path, strerror(errno));
close(fd);
return -1;
}

close(fd);
info("%s: successfully triggered kernel OOM killer", __func__);

if (args->notify) {
notify_dbus("Low memory! Triggered kernel OOM killer");
}

return 0;
}

// kill_release kills a process and calls process_mrelease to
// release the memory as quickly as possible.
//
Expand Down
3 changes: 3 additions & 0 deletions kill.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,13 @@ typedef struct {
int report_interval_ms;
/* Flag --dryrun was passed */
bool dryrun;
/* Flag --use-kernel-oom was passed, use kernel oom killer via /proc/sysrq-trigger */
bool use_kernel_oom_killer;
} poll_loop_args_t;

void kill_process(const poll_loop_args_t* args, int sig, const procinfo_t* victim);
procinfo_t find_largest_process(const poll_loop_args_t* args);
bool is_larger(const poll_loop_args_t* args, const procinfo_t* victim, procinfo_t* cur);
int trigger_kernel_oom_killer(const poll_loop_args_t* args);

#endif
30 changes: 29 additions & 1 deletion main.c
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,14 @@ enum {
LONG_OPT_IGNORE_ROOT,
LONG_OPT_USE_SYSLOG,
LONG_OPT_SORT_BY_RSS,
LONG_OPT_USE_KERNEL_OOM,
};

static int set_oom_score_adj(int);
static void poll_loop(const poll_loop_args_t* args);

extern int trigger_kernel_oom_killer(const poll_loop_args_t* args);

// Prevent Golang / Cgo name collision when the test suite runs -
// Cgo generates it's own main function.
#ifdef CGO
Expand All @@ -66,7 +69,17 @@ double min(double x, double y)
// (2) the stack grows to maximum size before calling mlockall()
static void startup_selftests(poll_loop_args_t* args)
{
{
if (args->use_kernel_oom_killer) {
// Check if we have permission to use kernel OOM killer
// Use a dummy dryrun arg to avoid actually triggering kernel OOM killer
debug("%s: checking kernel OOM killer permissions...\n", __func__);
poll_loop_args_t dummy_args = *args;
dummy_args.dryrun = true;
if (trigger_kernel_oom_killer(&dummy_args) != 0) {
warn("%s: kernel OOM killer permission check failed, use user mode instead\n", __func__);
args->use_kernel_oom_killer = false;
}
} else {
debug("%s: dry-running oom kill...\n", __func__);
procinfo_t victim = find_largest_process(args);
kill_process(args, 0, &victim);
Expand Down Expand Up @@ -185,6 +198,7 @@ int main(int argc, char* argv[])
{ "ignore-root-user", no_argument, NULL, LONG_OPT_IGNORE_ROOT },
{ "sort-by-rss", no_argument, NULL, LONG_OPT_SORT_BY_RSS },
{ "syslog", no_argument, NULL, LONG_OPT_USE_SYSLOG },
{ "use-kernel-oom", no_argument, NULL, LONG_OPT_USE_KERNEL_OOM },
{ "help", no_argument, NULL, 'h' },
{ "debug", no_argument, NULL, 'd' },
{ 0, 0, NULL, 0 } /* end-of-array marker */
Expand Down Expand Up @@ -298,6 +312,10 @@ int main(int argc, char* argv[])
case LONG_OPT_USE_SYSLOG:
earlyoom_syslog_init();
break;
case LONG_OPT_USE_KERNEL_OOM:
args.use_kernel_oom_killer = true;
fprintf(stderr, "Using kernel OOM killer (requires Linux v5.17+)\n");
break;
case LONG_OPT_IGNORE:
ignore_cmds = optarg;
break;
Expand Down Expand Up @@ -331,6 +349,9 @@ int main(int argc, char* argv[])
" --ignore REGEX ignore processes matching REGEX\n"
" --dryrun dry run (do not kill any processes)\n"
" --syslog use syslog instead of std streams\n"
" --use-kernel-oom use kernel OOM killer via /proc/sysrq-trigger\n"
" instead of killing processes directly. Requires\n"
" Linux v5.17+ and root to work correctly.\n"
" -h, --help this help text\n",
argv[0]);
exit(0);
Expand Down Expand Up @@ -524,6 +545,13 @@ static void poll_loop(const poll_loop_args_t* args)
args->mem_term_percent, args->swap_term_percent);
}
if (sig) {
if (args->use_kernel_oom_killer) {
trigger_kernel_oom_killer(args);
// Sleep a bit to give the kernel OOM killer time to do its work
struct timespec req = { .tv_sec = 0, .tv_nsec = 500 * 1000000 };
nanosleep(&req, NULL);
continue;
}
procinfo_t victim = find_largest_process(args);
/* The run time of find_largest_process is proportional to the number
* of processes, and takes 2.5ms on my box with a running Gnome desktop (try "make bench").
Expand Down
12 changes: 12 additions & 0 deletions testsuite_c_wrappers.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ func poll_loop_args_t(sort_by_rss bool) (args C.poll_loop_args_t) {
return
}

// Wrapper with use_kernel_oom_killer and dryrun support
func poll_loop_args_t_with_kernel_oom(sort_by_rss bool, use_kernel_oom bool, dryrun bool) (args C.poll_loop_args_t) {
args.sort_by_rss = C.bool(sort_by_rss)
args.use_kernel_oom_killer = C.bool(use_kernel_oom)
args.dryrun = C.bool(dryrun)
return
}

func procinfo_t() C.procinfo_t {
return C.procinfo_t{}
}
Expand Down Expand Up @@ -124,3 +132,7 @@ func parse_proc_pid_stat(pid int) (res bool, out C.pid_stat_t) {
res = bool(C.parse_proc_pid_stat(&out, C.int(pid)))
return res, out
}

func trigger_kernel_oom_killer(args C.poll_loop_args_t) int {
return int(C.trigger_kernel_oom_killer(&args))
}
3 changes: 3 additions & 0 deletions testsuite_cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ func TestCli(t *testing.T) {
{args: []string{"-s", "12.34"}, code: -1, stderrContains: "swap free <= 12.34%", stdoutContains: memReport},
// Use both -m/-M
{args: []string{"-m", "10", "-M", mem1percent}, code: -1, stderrContains: "SIGTERM when mem avail <= 1.00%", stdoutContains: memReport},
// Test --use-kernel-oom option
{args: []string{"--use-kernel-oom"}, code: -1, stderrContains: "Using kernel OOM killer", stdoutContains: memReport},
{args: []string{"--use-kernel-oom", "--dryrun"}, code: -1, stderrContains: "dryrun", stdoutContains: memReport},
}
if swapTotal > 0 {
// Tests that cannot work when there is no swap enabled
Expand Down
35 changes: 35 additions & 0 deletions testsuite_unit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -405,3 +405,38 @@ func Benchmark_parse_proc_pid_stat(b *testing.B) {
}
}
}

func TestTriggerKernelOomKiller_Dryrun(t *testing.T) {
// Test dryrun mode - should check permission and return 0 if has permission
// or -1 if no permission (even in dryrun mode)
args := poll_loop_args_t_with_kernel_oom(false, true, true)

res := trigger_kernel_oom_killer(args)

// If running as root, should have permission and return 0
// If not root, should return -1 (permission denied)
if os.Getuid() == 0 {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check does not work in a container (and in github actions).

I suggest you do syscall.Access() from Go and check that the trigger_kernel_oom_killer() result matches what we expect acc. to the syscall.Acecss() result

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. I have swithed to syscall.Access() and checked the result in CI

if res != 0 {
t.Errorf("dryrun mode as root should return 0, got %d", res)
}
} else {
if res != -1 {
t.Errorf("dryrun mode as non-root should return -1 (permission denied), got %d", res)
}
}
}

func TestTriggerKernelOomKiller_NonRoot(t *testing.T) {
// Non-root user test should fail (permission denied)
if os.Getuid() == 0 {
t.Skip("Skipping test that requires non-root user")
}

args := poll_loop_args_t_with_kernel_oom(false, true, false)

res := trigger_kernel_oom_killer(args)
// Expect -1 (permission denied, cannot open /proc/sysrq-trigger)
if res != -1 {
t.Errorf("Expected -1 for non-root user, got %d", res)
}
}
Loading