Skip to content

Commit eea12a9

Browse files
committed
Delete Windows sub-agent API-key temp file after use
writeKeyToUnlinkedFile now returns a cleanup callback that removes the 0600 temp file on Windows after the child has exited and the parent's handle is closed. POSIX behaviour is unchanged: the file is unlinked at creation and cleanup is a no-op.
1 parent eec7854 commit eea12a9

4 files changed

Lines changed: 60 additions & 17 deletions

File tree

cmd/odek/subagent_key.go

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,28 +14,39 @@ import (
1414
// cmd.ExtraFiles without ever leaving a readable file on disk.
1515
//
1616
// On Windows we cannot unlink an open file, so we fall back to a
17-
// 0600 file in the user's TempDir; the caller deletes it after Start.
18-
// (Windows is a degraded path; the env-passing concern is largely a
19-
// Unix /proc problem anyway.)
20-
func writeKeyToUnlinkedFile(key string) (*os.File, error) {
17+
// 0600 file in the user's TempDir. The returned cleanup function must be
18+
// called after the child has exited and the file has been closed; on
19+
// Windows it deletes the temp file, and on POSIX it is a no-op (the file
20+
// was already unlinked).
21+
func writeKeyToUnlinkedFile(key string) (*os.File, func(), error) {
2122
f, err := os.CreateTemp("", "odek-key-*")
2223
if err != nil {
23-
return nil, fmt.Errorf("create temp: %w", err)
24+
return nil, nil, fmt.Errorf("create temp: %w", err)
2425
}
26+
27+
cleanup := func() {
28+
// POSIX: file was already unlinked, nothing to do.
29+
// Windows: delete the temp file after the child has exited and
30+
// the parent's handle has been closed.
31+
if runtime.GOOS == "windows" {
32+
_ = os.Remove(f.Name())
33+
}
34+
}
35+
2536
if err := f.Chmod(0600); err != nil {
2637
f.Close()
2738
os.Remove(f.Name())
28-
return nil, fmt.Errorf("chmod: %w", err)
39+
return nil, nil, fmt.Errorf("chmod: %w", err)
2940
}
3041
if _, err := f.WriteString(key); err != nil {
3142
f.Close()
3243
os.Remove(f.Name())
33-
return nil, fmt.Errorf("write: %w", err)
44+
return nil, nil, fmt.Errorf("write: %w", err)
3445
}
3546
if _, err := f.Seek(0, io.SeekStart); err != nil {
3647
f.Close()
3748
os.Remove(f.Name())
38-
return nil, fmt.Errorf("seek: %w", err)
49+
return nil, nil, fmt.Errorf("seek: %w", err)
3950
}
4051
// Unlink immediately on POSIX. The open FD keeps the inode alive
4152
// for the parent and the soon-to-be-forked child.
@@ -46,7 +57,7 @@ func writeKeyToUnlinkedFile(key string) (*os.File, error) {
4657
fmt.Fprintf(os.Stderr, "odek: warning: could not unlink key tempfile: %v\n", err)
4758
}
4859
}
49-
return f, nil
60+
return f, cleanup, nil
5061
}
5162

5263
// keyFDEnvVar is the signal env var the parent sets when it passes an

cmd/odek/subagent_key_branches_test.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,18 @@ func TestReadKeyFromInheritedFD_UnsetsEnvVarBeforeRead(t *testing.T) {
5353
// degenerate but valid case of an empty key — the function should still
5454
// hand back a readable FD (containing zero bytes) and unlink the file.
5555
func TestWriteKeyToUnlinkedFile_EmptyKeyStillProducesValidFD(t *testing.T) {
56-
f, err := writeKeyToUnlinkedFile("")
56+
f, cleanup, err := writeKeyToUnlinkedFile("")
5757
if err != nil {
5858
t.Fatalf("writeKeyToUnlinkedFile(\"\"): %v", err)
5959
}
60-
defer f.Close()
6160
buf := make([]byte, 16)
6261
n, _ := f.Read(buf)
6362
if n != 0 {
63+
f.Close()
64+
cleanup()
6465
t.Errorf("expected 0 bytes for empty key, got %d (%q)", n, string(buf[:n]))
66+
return
6567
}
68+
f.Close()
69+
cleanup()
6670
}

cmd/odek/subagent_key_test.go

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,43 @@ import (
1414
// the unlink behaviour.
1515
func TestWriteKeyToUnlinkedFile_RoundtripsThroughFD(t *testing.T) {
1616
const want = "sk-test-1234567890"
17-
f, err := writeKeyToUnlinkedFile(want)
17+
f, cleanup, err := writeKeyToUnlinkedFile(want)
1818
if err != nil {
1919
t.Fatalf("writeKeyToUnlinkedFile: %v", err)
2020
}
21-
defer f.Close()
2221

2322
got, err := io.ReadAll(f)
2423
if err != nil {
24+
f.Close()
25+
cleanup()
2526
t.Fatalf("read: %v", err)
2627
}
2728
if string(got) != want {
29+
f.Close()
30+
cleanup()
2831
t.Errorf("read = %q, want %q", string(got), want)
32+
return
2933
}
3034

3135
// On POSIX, the file must already be unlinked — the inode lives on
3236
// only because we hold the FD. Verify by stat'ing the path.
3337
if runtime.GOOS != "windows" {
3438
if _, err := os.Stat(f.Name()); !os.IsNotExist(err) {
39+
f.Close()
40+
cleanup()
3541
t.Errorf("expected file to be unlinked on disk, stat err = %v", err)
42+
return
43+
}
44+
}
45+
46+
// Cleanup must be called after close. On POSIX it is a no-op; on
47+
// Windows it deletes the temp file.
48+
f.Close()
49+
cleanup()
50+
51+
if runtime.GOOS == "windows" {
52+
if _, err := os.Stat(f.Name()); !os.IsNotExist(err) {
53+
t.Errorf("expected temp file to be deleted on Windows, stat err = %v", err)
3654
}
3755
}
3856
}
@@ -73,11 +91,14 @@ else
7391
echo "env_leak=false"
7492
fi
7593
`
76-
keyFile, err := writeKeyToUnlinkedFile(want)
94+
keyFile, cleanup, err := writeKeyToUnlinkedFile(want)
7795
if err != nil {
7896
t.Fatalf("writeKeyToUnlinkedFile: %v", err)
7997
}
80-
defer keyFile.Close()
98+
defer func() {
99+
_ = keyFile.Close()
100+
cleanup()
101+
}()
81102

82103
cmd := exec.Command("/bin/sh", "-c", script)
83104
// Strip any inherited key env vars so the test cannot pass by

cmd/odek/subagent_tool.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,16 +235,23 @@ func (t *delegateTasksTool) runTask(taskIdx int, goal, taskContext, guidance, tr
235235
// as it has read the key.
236236
cmd.Env = os.Environ() // parent already stripped *_API_KEY in LoadConfig
237237
var keyFile *os.File
238+
var keyCleanup func()
238239
if t.apiKey != "" {
239-
f, err := writeKeyToUnlinkedFile(t.apiKey)
240+
f, cleanup, err := writeKeyToUnlinkedFile(t.apiKey)
240241
if err != nil {
241242
return fmt.Sprintf(`{"error":"key handoff: %v"}`, err)
242243
}
243244
keyFile = f
245+
keyCleanup = cleanup
244246
cmd.ExtraFiles = []*os.File{keyFile}
245247
// FD 3 in the child = the first ExtraFiles entry.
246248
cmd.Env = append(cmd.Env, keyFDEnvVar+"=3")
247-
defer keyFile.Close()
249+
defer func() {
250+
_ = keyFile.Close()
251+
if keyCleanup != nil {
252+
keyCleanup()
253+
}
254+
}()
248255
}
249256

250257
if err := cmd.Start(); err != nil {

0 commit comments

Comments
 (0)