From faff4d66ba60734cf309d442266cb9a1d061e8a8 Mon Sep 17 00:00:00 2001 From: Maksim An Date: Thu, 25 Jun 2026 00:49:19 -0700 Subject: [PATCH 1/2] Set SystemTemp env var to config temp on Windows Since Go 1.21, os.MkdirTemp/os.TempDir resolve the temp directory via Windows' GetTempPath2W. For processes running as SYSTEM (as containerd does when running under the SCM), that API reads the temp location from the SystemTemp environment variable rather than TMP/TEMP. As a result, the existing TMP/TEMP overrides no longer steer the layer-extraction tempdir for the containerd service, so it falls back to the default C:\\Windows\\SystemTemp and unpacks on the SystemDrive, reintroducing the cross-volume copy the 'temp' config option was meant to avoid. Set SystemTemp to config.TempDir alongside TEMP/TMP so the override keeps working on Go 1.21+. Factor the env-var setting out of CreateTopLevelDirectories into a small setTempDirEnv helper and add a focused unit test (TestSetTempDirEnv) that verifies the expected variables are set: TEMP/TMP/SystemTemp on Windows, TMPDIR on other platforms. Ref: https://cs.opensource.google/go/go/+/refs/tags/go1.21.0:src/os/file_windows.go Signed-off-by: Maksim An --- cmd/containerd/server/server.go | 36 ++++++++++++++++++---------- cmd/containerd/server/server_test.go | 23 ++++++++++++++++++ 2 files changed, 47 insertions(+), 12 deletions(-) diff --git a/cmd/containerd/server/server.go b/cmd/containerd/server/server.go index 60e494889869d..35cb027d26549 100644 --- a/cmd/containerd/server/server.go +++ b/cmd/containerd/server/server.go @@ -92,22 +92,34 @@ func CreateTopLevelDirectories(config *srvconfig.Config) error { if err := os.Chmod(config.TempDir, 0o700); err != nil && !errors.Is(err, os.ErrPermission) { return err } - if runtime.GOOS == "windows" { - // On Windows, the Host Compute Service (vmcompute) will read the - // TEMP/TMP setting from the calling process when creating the - // tempdir to extract an image layer to. This allows the - // administrator to align the tempdir location with the same volume - // as the snapshot dir to avoid a copy operation when moving the - // extracted layer to the snapshot dir location. - os.Setenv("TEMP", config.TempDir) - os.Setenv("TMP", config.TempDir) - } else { - os.Setenv("TMPDIR", config.TempDir) - } + setTempDirEnv(config.TempDir) } return nil } +// setTempDirEnv points the process' temp-directory environment variables at +// tempDir so that components (and the OS) honor the configured temp location. +func setTempDirEnv(tempDir string) { + if runtime.GOOS == "windows" { + // On Windows, the Host Compute Service (vmcompute) will read the + // TEMP/TMP setting from the calling process when creating the + // tempdir to extract an image layer to. This allows the + // administrator to align the tempdir location with the same volume + // as the snapshot dir to avoid a copy operation when moving the + // extracted layer to the snapshot dir location. + os.Setenv("TEMP", tempDir) + os.Setenv("TMP", tempDir) + // Since Go 1.21, os.MkdirTemp/os.TempDir resolve the temp dir via + // Windows' GetTempPath2W. For processes running as SYSTEM (as + // containerd does under the SCM), that API reads SystemTemp rather + // than TEMP/TMP, so set it too to keep the override effective. + // https://cs.opensource.google/go/go/+/refs/tags/go1.21.0:src/os/file_windows.go + os.Setenv("SystemTemp", tempDir) + } else { + os.Setenv("TMPDIR", tempDir) + } +} + // New creates and initializes a new containerd server func New(ctx context.Context, config *srvconfig.Config) (*Server, error) { if err := apply(ctx, config); err != nil { diff --git a/cmd/containerd/server/server_test.go b/cmd/containerd/server/server_test.go index 769a1107431e0..afa3024a155ea 100644 --- a/cmd/containerd/server/server_test.go +++ b/cmd/containerd/server/server_test.go @@ -20,6 +20,7 @@ import ( "context" "iter" "os" + "runtime" "slices" "testing" @@ -172,3 +173,25 @@ func TestMigration(t *testing.T) { t.Fatal(err) } } + +func TestSetTempDirEnv(t *testing.T) { + const tempDir = "/tmp/path/for/testing/temp" + + var keys []string + if runtime.GOOS == "windows" { + keys = []string{"TEMP", "TMP", "SystemTemp"} + } else { + keys = []string{"TMPDIR"} + } + for _, k := range keys { + t.Setenv(k, "") + } + + setTempDirEnv(tempDir) + + for _, k := range keys { + if got := os.Getenv(k); got != tempDir { + t.Errorf("expected %s=%q, got %q", k, tempDir, got) + } + } +} From c2dae310af03dec3654ecbf8a0b846e86372766d Mon Sep 17 00:00:00 2001 From: Chris Henzie Date: Mon, 29 Jun 2026 10:02:06 -0700 Subject: [PATCH 2/2] Fix nil pointer dereference in NRI GetIPs Adds a nil guard to GetIPs on criPodSandbox before accessing promoted struct fields on the embedded Sandbox pointer. During pod sandbox teardown or race conditions during container exit events, nriPodSandbox can return a criPodSandbox instance where the embedded Sandbox pointer is nil. Assisted-by: Antigravity Signed-off-by: Chris Henzie --- internal/cri/nri/nri_api_linux.go | 3 +- internal/cri/nri/nri_api_linux_test.go | 81 ++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 internal/cri/nri/nri_api_linux_test.go diff --git a/internal/cri/nri/nri_api_linux.go b/internal/cri/nri/nri_api_linux.go index 1e1c69ffa1cc8..b1f6eacead09c 100644 --- a/internal/cri/nri/nri_api_linux.go +++ b/internal/cri/nri/nri_api_linux.go @@ -682,7 +682,8 @@ func (p *criPodSandbox) GetPid() uint32 { } func (p *criPodSandbox) GetIPs() []string { - if p.IP == "" { + // IP and AdditionalIPs are promoted fields from the embedded Sandbox struct. + if p.Sandbox == nil || p.IP == "" { return nil } ips := append([]string{p.IP}, p.AdditionalIPs...) diff --git a/internal/cri/nri/nri_api_linux_test.go b/internal/cri/nri/nri_api_linux_test.go new file mode 100644 index 0000000000000..5959087121136 --- /dev/null +++ b/internal/cri/nri/nri_api_linux_test.go @@ -0,0 +1,81 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package nri + +import ( + "testing" + + sstore "github.com/containerd/containerd/v2/internal/cri/store/sandbox" + "github.com/stretchr/testify/assert" +) + +func TestCRIPodSandboxGetIPs(t *testing.T) { + testCases := []struct { + name string + pod *criPodSandbox + expected []string + }{ + { + name: "nil Sandbox", + pod: &criPodSandbox{ + Sandbox: nil, + }, + expected: nil, + }, + { + name: "empty primary IP", + pod: &criPodSandbox{ + Sandbox: &sstore.Sandbox{ + Metadata: sstore.Metadata{ + IP: "", + }, + }, + }, + expected: nil, + }, + { + name: "single primary IP", + pod: &criPodSandbox{ + Sandbox: &sstore.Sandbox{ + Metadata: sstore.Metadata{ + IP: "10.0.0.1", + }, + }, + }, + expected: []string{"10.0.0.1"}, + }, + { + name: "primary and additional IPs", + pod: &criPodSandbox{ + Sandbox: &sstore.Sandbox{ + Metadata: sstore.Metadata{ + IP: "10.0.0.1", + AdditionalIPs: []string{"fd00::1"}, + }, + }, + }, + expected: []string{"10.0.0.1", "fd00::1"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ips := tc.pod.GetIPs() + assert.Equal(t, tc.expected, ips) + }) + } +}