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 }} 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/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 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() }