From cb31d62f1ca71843dc9851c9be993b1a0d30c8a9 Mon Sep 17 00:00:00 2001 From: Kir Kolyshkin Date: Mon, 26 Jan 2026 16:46:36 -0800 Subject: [PATCH] Fix exec vs Go 1.26 Since [PR 4812], runc exec tries to use clone3 syscall with CLONE_INTO_CGROUP, falling back to the old method if it is not supported. One issue with that approach is, a > Cmd cannot be reused after calling its [Cmd.Start], [Cmd.Run], > [Cmd.Output], or [Cmd.CombinedOutput] methods. (from https://pkg.go.dev/os/exec#Cmd). This is enforced since Go 1.26, see [CL 728642], and so runc exec actually fails in specific scenarios (go1.26 and no CLONE_INTO_CGROUP support). The easiest workaround is to pre-copy the p.cmd structure (copy = *cmd). From the [CL 734200] it looks like it is an acceptable way, but it might break in the future as it also copies the private fields, so let's do a proper field-by-field copy. If the upstream will add cmd.Clone method, we will switch to it. Also, we can probably be fine with a post-copy (once the first Start has failed), but let's be conservative here and do a pre-copy. [PR 4812]: https://github.com/opencontainers/runc/pull/4812 [CL 728642]: https://go.dev/cl/728642 [CL 734200]: https://go.dev/cl/734200 Reported-by: Efim Verzakov Signed-off-by: Kir Kolyshkin --- libcontainer/cmd_clone.go | 37 +++++++++++++++++++++++++++++++++++ libcontainer/process_linux.go | 5 ++++- 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 libcontainer/cmd_clone.go diff --git a/libcontainer/cmd_clone.go b/libcontainer/cmd_clone.go new file mode 100644 index 000000000..67418070f --- /dev/null +++ b/libcontainer/cmd_clone.go @@ -0,0 +1,37 @@ +package libcontainer + +import "os/exec" + +// cloneCmd creates a copy of exec.Cmd. It is needed because cmd.Start +// must only be used once, and go1.26 actually enforces that (see +// https://go-review.googlesource.com/c/go/+/728642). The implementation +// is similar to +// +// cmd = *c +// return &cmd +// +// except it does not copy private fields, or fields populated +// after the call to cmd.Start. +// +// NOTE if Go will add exec.Cmd.Clone, we should switch to it. +func cloneCmd(c *exec.Cmd) *exec.Cmd { + cmd := &exec.Cmd{ + Path: c.Path, + Args: c.Args, + Env: c.Env, + Dir: c.Dir, + Stdin: c.Stdin, + Stdout: c.Stdout, + Stderr: c.Stderr, + ExtraFiles: c.ExtraFiles, + SysProcAttr: c.SysProcAttr, + // Don't copy Process, ProcessState, Err since + // these fields are populated after the start. + + // Technically, we do not use Cancel or WaitDelay, + // but they are here for the sake of completeness. + Cancel: c.Cancel, + WaitDelay: c.WaitDelay, + } + return cmd +} diff --git a/libcontainer/process_linux.go b/libcontainer/process_linux.go index b3487fd0b..fd075d4b9 100644 --- a/libcontainer/process_linux.go +++ b/libcontainer/process_linux.go @@ -380,11 +380,14 @@ func (p *setnsProcess) startWithCgroupFD() error { defer fd.Close() } + cmdCopy := cloneCmd(p.cmd) err = p.startWithCPUAffinity() if err != nil && p.cmd.SysProcAttr.UseCgroupFD { logrus.Debugf("exec with CLONE_INTO_CGROUP failed: %v; retrying without", err) // SysProcAttr.CgroupFD is never used when UseCgroupFD is unset. - p.cmd.SysProcAttr.UseCgroupFD = false + cmdCopy.SysProcAttr.UseCgroupFD = false + // Must not reuse exec.Cmd. + p.cmd = cmdCopy err = p.startWithCPUAffinity() }