From c2db79002297e61dd06b4baa1aba3fee88b1d231 Mon Sep 17 00:00:00 2001 From: Bryan Lu Date: Thu, 12 Nov 2026 09:17:37 +0000 Subject: [PATCH] fix(envd): run the command when oom_score_adj cannot be set The process wrapper chained exec to the oom_score_adj write with &&. Lowering oom_score_adj needs CAP_SYS_RESOURCE, so in a container that starts above 100 without that capability every write fails with EACCES (dash: "echo: I/O error") and no command can run. Make the write best effort, like the ionice/nice helpers, and keep its error off the user's stderr. --- .../process/handler/spawn_linux_test.go | 54 +++++++++++++++++++ .../services/process/handler/spawn_unix.go | 11 ++-- 2 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 packages/envd/internal/services/process/handler/spawn_linux_test.go diff --git a/packages/envd/internal/services/process/handler/spawn_linux_test.go b/packages/envd/internal/services/process/handler/spawn_linux_test.go new file mode 100644 index 0000000000..b1d544221f --- /dev/null +++ b/packages/envd/internal/services/process/handler/spawn_linux_test.go @@ -0,0 +1,54 @@ +//go:build linux + +package handler + +import ( + "fmt" + "os" + "os/exec" + "strings" + "syscall" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func runOomWrapper(t *testing.T, oomScore int, credential *syscall.Credential) string { + t.Helper() + + cmd := exec.CommandContext(t.Context(), "/bin/sh", "-c", fmt.Sprintf(oomWrapperFormat, oomScore, ""), "--", "cat", "/proc/self/oom_score_adj") + cmd.Dir = "/" + cmd.SysProcAttr = &syscall.SysProcAttr{Credential: credential} + out, err := cmd.CombinedOutput() + require.NoError(t, err, string(out)) + + return strings.TrimSpace(string(out)) +} + +func TestOomWrapper(t *testing.T) { + t.Parallel() + + raw, err := os.ReadFile("/proc/self/oom_score_adj") + require.NoError(t, err) + inherited := strings.TrimSpace(string(raw)) + + t.Run("applies the score", func(t *testing.T) { + t.Parallel() + // Raising the score needs no privilege. + assert.Equal(t, "1000", runOomWrapper(t, 1000, nil)) + }) + + t.Run("runs the command when the write is refused", func(t *testing.T) { + t.Parallel() + if inherited == "-1000" { + t.Skip("oom_score_adj is already at its minimum") + } + // Lowering the score needs CAP_SYS_RESOURCE; as root, drop it by running as nobody. + var credential *syscall.Credential + if os.Geteuid() == 0 { + credential = &syscall.Credential{Uid: 65534, Gid: 65534, Groups: []uint32{65534}} + } + assert.Equal(t, inherited, runOomWrapper(t, -1000, credential)) + }) +} diff --git a/packages/envd/internal/services/process/handler/spawn_unix.go b/packages/envd/internal/services/process/handler/spawn_unix.go index 67b192f65b..4d2d5a9afe 100644 --- a/packages/envd/internal/services/process/handler/spawn_unix.go +++ b/packages/envd/internal/services/process/handler/spawn_unix.go @@ -17,14 +17,19 @@ import ( rpc "github.com/e2b-dev/infra/packages/envd/internal/services/spec/process" ) +// oomWrapperFormat is the wrapper script: it sets the shell's oom_score_adj and +// execs the command through the ionice/nice prefix even if the write fails. +const oomWrapperFormat = `echo %d 2>/dev/null > /proc/$$/oom_score_adj; exec %s"${@}"` + // newCommand builds the child for a start request: the user's command wrapped // in a shell that resets oom_score_adj, ioprio (ionice best-effort/4) and // nice, running with the user's credentials inside the request's cgroup. The -// oom_score_adj write is pure /proc and always applied; the priority helpers -// are used only where the image provides them. +// oom_score_adj write is best effort, since lowering the score needs +// CAP_SYS_RESOURCE; the priority helpers are used only where the image +// provides them. func newCommand(ctx context.Context, user *user.User, req *rpc.StartRequest, logger *zerolog.Logger, cgroupManager cgroups.Manager) (*exec.Cmd, *sysState, error) { niceDelta := defaultNice - currentNice() - oomWrapperScript := fmt.Sprintf(`echo %d > /proc/$$/oom_score_adj && exec %s"${@}"`, defaultOomScore, ioniceNicePrefix(defaultIoClass, defaultIoPrio, niceDelta, exec.LookPath)) + oomWrapperScript := fmt.Sprintf(oomWrapperFormat, defaultOomScore, ioniceNicePrefix(defaultIoClass, defaultIoPrio, niceDelta, exec.LookPath)) wrapperArgs := append([]string{"-c", oomWrapperScript, "--", req.GetProcess().GetCmd()}, req.GetProcess().GetArgs()...) cmd := exec.CommandContext(ctx, "/bin/sh", wrapperArgs...)