Skip to content

Commit 8ab530b

Browse files
authored
Use destination dir for atomic temp files
1 parent b81744e commit 8ab530b

2 files changed

Lines changed: 78 additions & 2 deletions

File tree

pkg/utils/utilio/io.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ func InstallFile(filename string, r io.Reader, perm os.FileMode) error {
2424

2525
// InstallFileWithLimitedSize streams content to local file with limited size and specified permissions.
2626
// It ensures that the target directory exists and handles the file writing atomically.
27+
// The temp file is created in the same directory as the destination to preserve
28+
// the correct SELinux label.
2729
//
2830
// NOTE: we assume the filename is trusted and cleaned without path traversal characters.
2931
func InstallFileWithLimitedSize(filename string, r io.Reader, perm os.FileMode, maxBytes int64) error {
@@ -34,7 +36,7 @@ func InstallFileWithLimitedSize(filename string, r io.Reader, perm os.FileMode,
3436
return err
3537
}
3638

37-
pf, err := renameio.NewPendingFile(filename, renameio.WithPermissions(perm))
39+
pf, err := renameio.NewPendingFile(filename, renameio.WithPermissions(perm), renameio.WithTempDir(filepath.Dir(filename)))
3840
if err != nil {
3941
return err
4042
}
@@ -57,12 +59,14 @@ func InstallFileWithLimitedSize(filename string, r io.Reader, perm os.FileMode,
5759

5860
// WriteFile writes the provided content to a local file with specified permissions.
5961
// It ensures that the target directory exists and handles the file writing atomically.
62+
// The temp file is created in the same directory as the destination to preserve
63+
// the correct SELinux label.
6064
//
6165
// NOTE: we assume the filename is trusted and cleaned without path traversal characters.
6266
func WriteFile(filename string, content []byte, perm os.FileMode) error {
6367
if err := os.MkdirAll(filepath.Dir(filename), 0750); err != nil {
6468
return err
6569
}
6670

67-
return renameio.WriteFile(filename, content, perm)
71+
return renameio.WriteFile(filename, content, perm, renameio.WithTempDir(filepath.Dir(filename)))
6872
}

pkg/utils/utilio/io_test.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"path/filepath"
99
"strings"
1010
"testing"
11+
"time"
1112
)
1213

1314
func TestWriteFile(t *testing.T) {
@@ -159,6 +160,36 @@ func TestInstallFile(t *testing.T) {
159160
}
160161
}
161162

163+
func TestInstallFileWithLimitedSizeCreatesTempFileInTargetDir(t *testing.T) {
164+
targetDir := t.TempDir()
165+
tmpDir := t.TempDir()
166+
t.Setenv("TMPDIR", tmpDir)
167+
168+
filename := filepath.Join(targetDir, "target.txt")
169+
reader := &blockingReader{
170+
started: make(chan struct{}),
171+
unblock: make(chan struct{}),
172+
}
173+
errCh := make(chan error, 1)
174+
go func() {
175+
errCh <- InstallFileWithLimitedSize(filename, reader, 0o644, 1024)
176+
}()
177+
178+
<-reader.started
179+
180+
if !waitForTempFile(t, targetDir, filepath.Base(filename)) {
181+
t.Fatalf("expected pending temp file in target directory %q", targetDir)
182+
}
183+
if hasTempFile(t, tmpDir, filepath.Base(filename)) {
184+
t.Fatalf("expected no pending temp file in TMPDIR %q", tmpDir)
185+
}
186+
187+
close(reader.unblock)
188+
if err := <-errCh; err != nil {
189+
t.Fatalf("unexpected error: %v", err)
190+
}
191+
}
192+
162193
func TestInstallFileWithLimitedSize(t *testing.T) {
163194
tests := []struct {
164195
name string
@@ -391,3 +422,44 @@ type errReader struct {
391422
func (e *errReader) Read(_ []byte) (int, error) {
392423
return 0, e.err
393424
}
425+
426+
type blockingReader struct {
427+
started chan struct{}
428+
unblock chan struct{}
429+
sent bool
430+
}
431+
432+
func (r *blockingReader) Read(p []byte) (int, error) {
433+
if !r.sent {
434+
r.sent = true
435+
p[0] = 'x'
436+
close(r.started)
437+
return 1, nil
438+
}
439+
440+
<-r.unblock
441+
return 0, io.EOF
442+
}
443+
444+
func waitForTempFile(t *testing.T, dir, targetBase string) bool {
445+
t.Helper()
446+
447+
deadline := time.Now().Add(2 * time.Second)
448+
for time.Now().Before(deadline) {
449+
if hasTempFile(t, dir, targetBase) {
450+
return true
451+
}
452+
time.Sleep(10 * time.Millisecond)
453+
}
454+
return false
455+
}
456+
457+
func hasTempFile(t *testing.T, dir, targetBase string) bool {
458+
t.Helper()
459+
460+
matches, err := filepath.Glob(filepath.Join(dir, "."+targetBase+"*"))
461+
if err != nil {
462+
t.Fatalf("glob temp files: %v", err)
463+
}
464+
return len(matches) > 0
465+
}

0 commit comments

Comments
 (0)