From 6a057b84de06f149049a3942b187aa205501a057 Mon Sep 17 00:00:00 2001 From: Kir Kolyshkin Date: Thu, 29 Jan 2026 13:39:45 -0800 Subject: [PATCH 1/3] libct: check cmd.Err after exec.Command call Theoretically, exec.Command can set cmd.Err. Practically, this should never happen (Linux, Go <= 1.26, exePath is absolute), but in the unlikely case it does, let's fail early. This is related to the cloneCmd (to be introduced by the following commit) which chooses to not copy the Err field. Theoretically, exec.Command can set Err and so the first call to cmd.Start will fail (since Err != nil), and the second call to cmd.Start may succeed because Err == nil. Yet, this scenario is highly unlikely, but better be safe than sorry. Signed-off-by: Kir Kolyshkin (cherry picked from commit 82b7597a26726b6d52967f4142678e30be5ff870) Signed-off-by: lifubang --- libcontainer/container_linux.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/libcontainer/container_linux.go b/libcontainer/container_linux.go index ad904641723..2ec08ab9ade 100644 --- a/libcontainer/container_linux.go +++ b/libcontainer/container_linux.go @@ -528,6 +528,12 @@ func (c *Container) newParentProcess(p *Process) (parentProcess, error) { } cmd := exec.Command(exePath, "init") + // Theoretically, exec.Command can set cmd.Err. Practically, this + // should never happen (Linux, Go <= 1.26, exePath is absolute), + // but in the unlikely case it just did, let's fail early. + if cmd.Err != nil { + return nil, fmt.Errorf("exec.Command: %w", cmd.Err) + } cmd.Args[0] = os.Args[0] cmd.Stdin = p.Stdin cmd.Stdout = p.Stdout From 685bbe04e19e71b63de11181b2aeace181d9a61f Mon Sep 17 00:00:00 2001 From: Kir Kolyshkin Date: Mon, 26 Jan 2026 16:46:36 -0800 Subject: [PATCH 2/3] 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 (cherry picked from commit cb31d62f1ca71843dc9851c9be993b1a0d30c8a9) Signed-off-by: lifubang --- 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 00000000000..67418070f08 --- /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 8114cc50bbf..fd34056588b 100644 --- a/libcontainer/process_linux.go +++ b/libcontainer/process_linux.go @@ -367,11 +367,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() } From b9e3eec229e599c6ccea5ecd203c246dc14ff69d Mon Sep 17 00:00:00 2001 From: Kir Kolyshkin Date: Mon, 26 Jan 2026 16:29:05 -0800 Subject: [PATCH 3/3] ci: add go 1.26 This commit is a squash of the following two commits: 1. ci: add go 1.26 rc2 This is mostly to test whether https://go.dev/cl/728642 results in any test failures in the current CI matrix. Signed-off-by: Kir Kolyshkin (cherry picked from commit e4e05423e46f68e43870ecbfa5bfa71e75684885) Signed-off-by: lifubang 2. ci: bump Go 1.26rc2 -> 1.26.x Since Go 1.26.0 is released today. Signed-off-by: Kir Kolyshkin (cherry picked from commit daa5ffcc844d64daa2ae03095b6612e6b0719e16) Signed-off-by: lifubang --- .github/workflows/test.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bb7566fd3b3..eff3a6fa828 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,7 +25,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-24.04, ubuntu-24.04-arm] - go-version: [1.24.x, 1.25.x] + go-version: [1.24.x, 1.25.x, 1.26.x] rootless: ["rootless", ""] race: ["-race", ""] criu: ["", "criu-dev"] @@ -34,11 +34,15 @@ jobs: # (need to compile criu) and don't add much value/coverage. - criu: criu-dev go-version: 1.24.x + - criu: criu-dev + go-version: 1.25.x - criu: criu-dev rootless: rootless - # Do race detection only on latest Go. + # Do race detection only with latest stable Go version. - race: -race go-version: 1.24.x + - race: -race + go-version: 1.25.x runs-on: ${{ matrix.os }}