Skip to content

Commit a993b16

Browse files
committed
os: preserve sources during process fd remapping
1 parent d44997d commit a993b16

2 files changed

Lines changed: 165 additions & 3 deletions

File tree

src/os/exec_posix_spawn.go

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -281,14 +281,47 @@ func forkExec(argv0 string, argv []string, attr *ProcAttr) (pid int, err error)
281281
}
282282
}
283283

284-
// Entry i becomes descriptor i in the child, and a missing entry means
285-
// that the descriptor is closed.
284+
defer runtime.KeepAlive(attr.Files)
285+
fds := make([]int, len(attr.Files))
286+
nextfd := len(fds)
287+
if nextfd < 3 {
288+
nextfd = 3
289+
}
286290
for i, f := range attr.Files {
287291
fd := ^uintptr(0)
288292
if f != nil {
289293
fd = f.Fd()
290294
}
291-
if fd == ^uintptr(0) {
295+
fds[i] = -1
296+
if fd != ^uintptr(0) {
297+
if fd >= 1<<31-1 {
298+
return 0, syscall.EBADF
299+
}
300+
fds[i] = int(fd)
301+
if int(fd) >= nextfd {
302+
nextfd = int(fd) + 1
303+
}
304+
}
305+
}
306+
307+
// Save sources before an earlier action replaces or closes them.
308+
// See Go src/syscall/exec_linux.go, forkAndExecInChild, Pass 1.
309+
firstTemp := nextfd
310+
for i, fd := range fds {
311+
if fd >= 0 && fd < i {
312+
if nextfd >= 1<<31-1 {
313+
return 0, syscall.EINVAL
314+
}
315+
if errno := posix_spawn_file_actions_adddup2(&fa, int32(fd), int32(nextfd)); errno != 0 {
316+
return 0, syscall.Errno(errno)
317+
}
318+
fds[i] = nextfd
319+
nextfd++
320+
}
321+
}
322+
323+
for i, fd := range fds {
324+
if fd == -1 {
292325
if errno := posix_spawn_file_actions_addclose(&fa, int32(i)); errno != 0 {
293326
return 0, syscall.Errno(errno)
294327
}
@@ -308,6 +341,11 @@ func forkExec(argv0 string, argv []string, attr *ProcAttr) (pid int, err error)
308341
return 0, syscall.Errno(errno)
309342
}
310343
}
344+
for fd := firstTemp; fd < nextfd; fd++ {
345+
if errno := posix_spawn_file_actions_addclose(&fa, int32(fd)); errno != 0 {
346+
return 0, syscall.Errno(errno)
347+
}
348+
}
311349

312350
var childPid int32
313351
// ForkLock keeps a descriptor made without O_CLOEXEC out of a child that

src/os/exec_remap_test.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
//go:build (linux || darwin) && !baremetal && !tinygo.wasm && !nintendoswitch
2+
3+
package os_test
4+
5+
import (
6+
"errors"
7+
"fmt"
8+
. "os"
9+
"syscall"
10+
"testing"
11+
)
12+
13+
func TestForkExecFileRemapping(t *testing.T) {
14+
for _, name := range []string{"cycle", "repeated", "closed-source", "identity", "sparse"} {
15+
t.Run(name, func(t *testing.T) {
16+
a, err := CreateTemp(t.TempDir(), "source-a")
17+
if err != nil {
18+
t.Fatal(err)
19+
}
20+
defer a.Close()
21+
b, err := CreateTemp(t.TempDir(), "source-b")
22+
if err != nil {
23+
t.Fatal(err)
24+
}
25+
defer b.Close()
26+
out, err := CreateTemp(t.TempDir(), "output")
27+
if err != nil {
28+
t.Fatal(err)
29+
}
30+
defer out.Close()
31+
for i, f := range []*File{a, b} {
32+
if _, err := f.WriteString([]string{"A", "B"}[i]); err != nil {
33+
t.Fatal(err)
34+
}
35+
if _, err := f.Seek(0, 0); err != nil {
36+
t.Fatal(err)
37+
}
38+
}
39+
x, y := int(a.Fd()), int(b.Fd())
40+
if x < 3 || y <= x {
41+
t.Fatalf("unexpected source descriptors %d, %d", x, y)
42+
}
43+
files := make([]*File, y+3)
44+
files[1], files[2] = out, Stderr
45+
targets := []int{x, y}
46+
switch name {
47+
case "cycle":
48+
files[x], files[y] = b, a
49+
case "repeated":
50+
files[x], files[y], files[y+1] = b, a, a
51+
targets = []int{x, y, y + 1}
52+
case "closed-source":
53+
files[y] = a
54+
targets = []int{y}
55+
case "identity":
56+
files[x], files[y] = a, b
57+
case "sparse":
58+
files[y+2] = a
59+
targets = []int{y + 2}
60+
}
61+
argv := []string{"sh", "-c", "", "sh"}
62+
script := ""
63+
for i, fd := range targets {
64+
script += fmt.Sprintf("test /dev/fd/%d -ef \"$%d\" && ", fd, i+1)
65+
argv = append(argv, files[fd].Name())
66+
}
67+
nextfd := len(files)
68+
for _, f := range files {
69+
if f != nil && int(f.Fd()) >= nextfd {
70+
nextfd = int(f.Fd()) + 1
71+
}
72+
}
73+
for i, f := range files {
74+
if f != nil && int(f.Fd()) < i {
75+
script += fmt.Sprintf("test ! -e /dev/fd/%d && ", nextfd)
76+
nextfd++
77+
}
78+
}
79+
argv[2] = script + "printf ok"
80+
proc, err := StartProcess("/bin/sh", argv, &ProcAttr{Files: files})
81+
if err != nil {
82+
t.Fatal(err)
83+
}
84+
state, err := proc.Wait()
85+
if err != nil || !state.Success() {
86+
t.Fatalf("Wait = %v, %v", state, err)
87+
}
88+
got, err := ReadFile(out.Name())
89+
if err != nil || string(got) != "ok" {
90+
t.Fatalf("output = %q, %v, want ok", got, err)
91+
}
92+
for i, f := range []*File{a, b} {
93+
got := make([]byte, 1)
94+
_, err := f.ReadAt(got, 0)
95+
if err != nil || string(got) != []string{"A", "B"}[i] {
96+
t.Fatalf("parent source = %q, %v", got, err)
97+
}
98+
}
99+
})
100+
}
101+
}
102+
103+
func TestForkExecInvalidRemapSource(t *testing.T) {
104+
f, err := Open(DevNull)
105+
if err != nil {
106+
t.Fatal(err)
107+
}
108+
defer f.Close()
109+
files := make([]*File, int(f.Fd())+2)
110+
files[len(files)-1] = f
111+
files[0] = NewFile(1<<31, "invalid")
112+
proc, err := StartProcess("/bin/sh", []string{"sh", "-c", "exit 0"}, &ProcAttr{Files: files})
113+
if proc != nil {
114+
proc.Kill()
115+
proc.Wait()
116+
t.Fatal("StartProcess accepted an invalid descriptor")
117+
}
118+
if !errors.Is(err, syscall.EBADF) {
119+
t.Fatalf("StartProcess = %v, want EBADF", err)
120+
}
121+
if _, err := f.Stat(); err != nil {
122+
t.Fatalf("parent source stat = %v", err)
123+
}
124+
}

0 commit comments

Comments
 (0)