Skip to content

Commit e31a03d

Browse files
committed
Write pidfile atomically
1 parent 14f317c commit e31a03d

File tree

4 files changed

+142
-4
lines changed

4 files changed

+142
-4
lines changed

runsc/cmd/BUILD

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ go_library(
5858
"mitigate_extras.go",
5959
"path.go",
6060
"pause.go",
61+
"pidfile.go",
6162
"platforms.go",
6263
"portforward.go",
6364
"ps.go",
@@ -145,6 +146,7 @@ go_test(
145146
"install_test.go",
146147
"list_test.go",
147148
"mitigate_test.go",
149+
"pidfile_test.go",
148150
],
149151
data = [
150152
"//runsc",

runsc/cmd/exec.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -197,17 +197,16 @@ func (ex *Exec) exec(conf *config.Config, c *container.Container, e *control.Exe
197197

198198
// Write the sandbox-internal pid if required.
199199
if ex.internalPidFile != "" {
200-
pidStr := []byte(strconv.Itoa(int(pid)))
201-
if err := os.WriteFile(ex.internalPidFile, pidStr, 0644); err != nil {
202-
return util.Errorf("writing internal pid file %q: %v", ex.internalPidFile, err)
200+
if err := WritePidFile(ex.internalPidFile, int(pid)); err != nil {
201+
return util.Errorf("writing internal pid file: %v", err)
203202
}
204203
}
205204

206205
// Generate the pid file after the internal pid file is generated, so that
207206
// users can safely assume that the internal pid file is ready after
208207
// `runsc exec -d` returns.
209208
if ex.pidFile != "" {
210-
if err := os.WriteFile(ex.pidFile, []byte(strconv.Itoa(os.Getpid())), 0644); err != nil {
209+
if err := WritePidFile(ex.pidFile, os.Getpid()); err != nil {
211210
return util.Errorf("writing pid file: %v", err)
212211
}
213212
}

runsc/cmd/pidfile.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright 2025 The gVisor Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package cmd
16+
17+
import (
18+
"fmt"
19+
"os"
20+
"path/filepath"
21+
"strconv"
22+
)
23+
24+
// WritePidFile writes pid file atomically if possible.
25+
func WritePidFile(path string, pid int) error {
26+
pidStr := []byte(strconv.Itoa(pid))
27+
28+
st, err := os.Stat(path)
29+
if err == nil && !st.Mode().IsRegular() {
30+
// If not regular file, write in place.
31+
if err := os.WriteFile(path, pidStr, 0644); err != nil {
32+
return fmt.Errorf("failed to write pid file %s: %w", path, err)
33+
}
34+
return nil
35+
}
36+
if err != nil && !os.IsNotExist(err) {
37+
return fmt.Errorf("stat file %s failed: %w", path, err)
38+
}
39+
40+
// Otherwise write using temp file to make write atomic.
41+
dir := filepath.Dir(path)
42+
tempFile, err := os.CreateTemp(dir, "pid-tmp-*")
43+
if err != nil {
44+
return fmt.Errorf("failed to create temp pid file in dir %s: %w", dir, err)
45+
}
46+
47+
tempFileRenamed := false
48+
defer func(tempFile *os.File) {
49+
_ = tempFile.Close()
50+
if !tempFileRenamed {
51+
_ = os.Remove(tempFile.Name())
52+
}
53+
}(tempFile)
54+
55+
if err := os.Chmod(tempFile.Name(), 0644); err != nil {
56+
return fmt.Errorf("failed to chmod pid file %s: %w", tempFile.Name(), err)
57+
}
58+
59+
if _, err := tempFile.Write(pidStr); err != nil {
60+
return fmt.Errorf("failed to write pid file %s: %w", tempFile.Name(), err)
61+
}
62+
63+
if err := tempFile.Close(); err != nil {
64+
return fmt.Errorf("failed to close temp pid file %s: %w", tempFile.Name(), err)
65+
}
66+
67+
if err := os.Rename(tempFile.Name(), path); err != nil {
68+
return fmt.Errorf("failed to rename temp pid file %s -> %s: %w", tempFile.Name(), path, err)
69+
}
70+
tempFileRenamed = true
71+
72+
return nil
73+
}

runsc/cmd/pidfile_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright 2025 The gVisor Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package cmd
16+
17+
import (
18+
"os"
19+
"path/filepath"
20+
"testing"
21+
)
22+
23+
func TestWritePidFile(t *testing.T) {
24+
tempDir, err := os.MkdirTemp("", "test-*")
25+
if err != nil {
26+
t.Fatalf("failed to create temp dir: %v", err)
27+
}
28+
defer func() {
29+
_ = os.RemoveAll(tempDir)
30+
}()
31+
32+
t.Run("Write new file", func(t *testing.T) {
33+
path := filepath.Join(tempDir, "test-new.pid")
34+
if err := WritePidFile(path, 17); err != nil {
35+
t.Fatalf("failed to write pid file: %v", err)
36+
}
37+
38+
pidStr, err := os.ReadFile(path)
39+
if err != nil {
40+
t.Fatalf("failed to read pid file: %v", err)
41+
}
42+
if string(pidStr) != "17" {
43+
t.Fatalf("pid file did not contain pid '17'")
44+
}
45+
})
46+
47+
t.Run("Overwrite existing file", func(t *testing.T) {
48+
path := filepath.Join(tempDir, "test-overwrite.pid")
49+
if err := os.WriteFile(path, []byte("11"), 0600); err != nil {
50+
t.Fatalf("failed to write pid file: %v", err)
51+
}
52+
53+
if err := WritePidFile(path, 19); err != nil {
54+
t.Fatalf("failed to overwrite write pid file: %v", err)
55+
}
56+
pidStr, err := os.ReadFile(path)
57+
if err != nil {
58+
t.Fatalf("failed to read pid file: %v", err)
59+
}
60+
if string(pidStr) != "19" {
61+
t.Fatalf("pid file did not contain pid '19'")
62+
}
63+
})
64+
}

0 commit comments

Comments
 (0)