Skip to content

Commit 6d1c239

Browse files
author
Bryan C. Mills
committed
cmd/buildlet: override the last PATH entry, not the first
If both the process environment and the "env" post field included entries for "PATH", the setPathEnv helper function would erroneously use the first one instead of the last. (In current versions of Go, os/exec always uses the last entry for each variable.) This change removes much of the complexity of setPathEnv, pushing some of the less obvious parts of the logic (skipping empty slices and suppressing no-op changes) to the caller side. For golang/go#31567 Change-Id: I7460bf65c61073a71f0ea11d4d69a16f3e9b7c16 Reviewed-on: https://go-review.googlesource.com/c/build/+/354754 Trust: Bryan C. Mills <[email protected]> Run-TryBot: Bryan C. Mills <[email protected]> TryBot-Result: Go Bot <[email protected]> Reviewed-by: Dmitri Shuralyov <[email protected]>
1 parent 79bb74b commit 6d1c239

File tree

2 files changed

+120
-107
lines changed

2 files changed

+120
-107
lines changed

cmd/buildlet/buildlet.go

+38-76
Original file line numberDiff line numberDiff line change
@@ -936,7 +936,11 @@ func handleExec(w http.ResponseWriter, r *http.Request) {
936936
if v := processGoCacheEnv; v != "" {
937937
env = append(env, "GOCACHE="+v)
938938
}
939-
env = setPathEnv(env, r.PostForm["path"], *workDir)
939+
if path := r.PostForm["path"]; len(path) > 0 {
940+
if kv, ok := pathEnv(runtime.GOOS, env, path, *workDir); ok {
941+
env = append(env, kv)
942+
}
943+
}
940944
env = envutil.Dedup(runtime.GOOS, env)
941945

942946
var cmd *exec.Cmd
@@ -1005,89 +1009,47 @@ func pathNotExist(path string) bool {
10051009
return os.IsNotExist(err)
10061010
}
10071011

1008-
// setPathEnv returns a copy of the provided environment with any existing
1009-
// PATH variables replaced by the user-provided path.
1010-
// These substitutions are applied to user-supplied path elements:
1011-
// - the string "$PATH" expands to the original PATH elements
1012-
// - the substring "$WORKDIR" expands to the provided workDir
1013-
// A path of just ["$EMPTY"] removes the PATH variable from the environment.
1014-
func setPathEnv(env, path []string, workDir string) []string {
1015-
if len(path) == 0 {
1016-
return env
1017-
}
1018-
1019-
var (
1020-
pathIdx = -1
1021-
pathOrig = ""
1012+
// pathEnv returns a key=value string for the system path variable
1013+
// (either PATH or path depending on the platform) with values
1014+
// substituted from env:
1015+
// - the string "$PATH" expands to the original value of the path variable
1016+
// - the string "$WORKDIR" expands to the provided workDir
1017+
// - the string "$EMPTY" expands to the empty string
1018+
//
1019+
// The "ok" result reports whether kv differs from the path found in env.
1020+
func pathEnv(goos string, env, path []string, workDir string) (kv string, ok bool) {
1021+
pathVar := "PATH"
1022+
if goos == "plan9" {
1023+
pathVar = "path"
1024+
}
1025+
1026+
orig := envutil.Get(goos, env, pathVar)
1027+
r := strings.NewReplacer(
1028+
"$PATH", orig,
1029+
"$WORKDIR", workDir,
1030+
"$EMPTY", "",
10221031
)
10231032

1024-
for i, s := range env {
1025-
if isPathEnvPair(s) {
1026-
pathIdx = i
1027-
pathOrig = s[len("PaTh="):] // in whatever case
1028-
break
1029-
}
1030-
}
1031-
if len(path) == 1 && path[0] == "$EMPTY" {
1032-
// Remove existing path variable if it exists.
1033-
if pathIdx >= 0 {
1034-
env = append(env[:pathIdx], env[pathIdx+1:]...)
1035-
}
1036-
return env
1037-
}
1038-
10391033
// Apply substitions to a copy of the path argument.
1040-
path = append([]string{}, path...)
1041-
for i, s := range path {
1042-
if s == "$PATH" {
1043-
path[i] = pathOrig // ok if empty
1044-
} else {
1045-
path[i] = strings.Replace(s, "$WORKDIR", workDir, -1)
1034+
subst := make([]string, 0, len(path))
1035+
for _, elem := range path {
1036+
if s := r.Replace(elem); s != "" {
1037+
subst = append(subst, s)
10461038
}
10471039
}
1048-
1049-
// Put the new PATH in env.
1050-
env = append([]string{}, env...)
1051-
pathEnv := pathEnvVar() + "=" + strings.Join(path, pathSeparator())
1052-
if pathIdx >= 0 {
1053-
env[pathIdx] = pathEnv
1054-
} else {
1055-
env = append(env, pathEnv)
1056-
}
1057-
1058-
return env
1059-
}
1060-
1061-
// isPathEnvPair reports whether the key=value pair s represents
1062-
// the operating system's path variable.
1063-
func isPathEnvPair(s string) bool {
1064-
// On Unix it's PATH.
1065-
// On Plan 9 it's path.
1066-
// On Windows it's pAtH case-insensitive.
1067-
if runtime.GOOS == "windows" {
1068-
return len(s) >= 5 && strings.EqualFold(s[:5], "PATH=")
1069-
}
1070-
if runtime.GOOS == "plan9" {
1071-
return strings.HasPrefix(s, "path=")
1072-
}
1073-
return strings.HasPrefix(s, "PATH=")
1040+
kv = pathVar + "=" + strings.Join(subst, pathListSeparator(goos))
1041+
v := kv[len(pathVar)+1:]
1042+
return kv, v != orig
10741043
}
10751044

1076-
// On Unix it's PATH.
1077-
// On Plan 9 it's path.
1078-
// On Windows it's pAtH case-insensitive.
1079-
func pathEnvVar() string {
1080-
if runtime.GOOS == "plan9" {
1081-
return "path"
1082-
}
1083-
return "PATH"
1084-
}
1085-
1086-
func pathSeparator() string {
1087-
if runtime.GOOS == "plan9" {
1045+
func pathListSeparator(goos string) string {
1046+
switch goos {
1047+
case "windows":
1048+
return ";"
1049+
case "plan9":
10881050
return "\x00"
1089-
} else {
1090-
return string(filepath.ListSeparator)
1051+
default:
1052+
return ":"
10911053
}
10921054
}
10931055

cmd/buildlet/buildlet_test.go

+82-31
Original file line numberDiff line numberDiff line change
@@ -5,57 +5,108 @@
55
package main
66

77
import (
8-
"fmt"
8+
"os"
99
"runtime"
1010
"testing"
1111
)
1212

13-
func TestSetPathEnv(t *testing.T) {
14-
if runtime.GOOS == "windows" {
15-
t.Skip("TODO(adg): make this test work on windows")
16-
}
17-
18-
const workDir = "/workdir"
19-
13+
func TestPathEnv(t *testing.T) {
2014
for _, c := range []struct {
15+
goos string // default "linux"
16+
wd string // default "/workdir"
2117
env []string
2218
path []string
23-
want []string
19+
want string
20+
noOp bool
2421
}{
2522
{ // No change to PATH
26-
[]string{"A=1", "PATH=/bin:/usr/bin", "B=2"},
27-
[]string{},
28-
[]string{"A=1", "PATH=/bin:/usr/bin", "B=2"},
23+
env: []string{"A=1", "PATH=/bin:/usr/bin", "B=2"},
24+
path: []string{"$PATH"},
25+
want: "PATH=/bin:/usr/bin",
26+
noOp: true,
27+
},
28+
{ // Test that $EMPTY rewrites the path to be empty
29+
env: []string{"A=1", "PATH=/bin:/usr/bin", "B=2"},
30+
path: []string{"$EMPTY"},
31+
want: "PATH=",
2932
},
30-
{ // Test sentinel $EMPTY value to clear PATH
31-
[]string{"A=1", "PATH=/bin:/usr/bin", "B=2"},
32-
[]string{"$EMPTY"},
33-
[]string{"A=1", "B=2"},
33+
{ // Test that clearing an already-unset PATH is a no-op
34+
env: []string{"A=1", "B=2"},
35+
path: []string{"$EMPTY"},
36+
want: "PATH=",
37+
noOp: true,
3438
},
3539
{ // Test $WORKDIR expansion
36-
[]string{"A=1", "PATH=/bin:/usr/bin", "B=2"},
37-
[]string{"/go/bin", "$WORKDIR/foo"},
38-
[]string{"A=1", "PATH=/go/bin:/workdir/foo", "B=2"},
40+
env: []string{"A=1", "PATH=/bin:/usr/bin", "B=2"},
41+
path: []string{"/go/bin", "$WORKDIR/foo"},
42+
want: "PATH=/go/bin:/workdir/foo",
3943
},
4044
{ // Test $PATH expansion
41-
[]string{"A=1", "PATH=/bin:/usr/bin", "B=2"},
42-
[]string{"/go/bin", "$PATH", "$WORKDIR/foo"},
43-
[]string{"A=1", "PATH=/go/bin:/bin:/usr/bin:/workdir/foo", "B=2"},
45+
env: []string{"A=1", "PATH=/bin:/usr/bin", "B=2"},
46+
path: []string{"/go/bin", "$PATH", "$WORKDIR/foo"},
47+
want: "PATH=/go/bin:/bin:/usr/bin:/workdir/foo",
4448
},
4549
{ // Test $PATH expansion (prepend only)
46-
[]string{"A=1", "PATH=/bin:/usr/bin", "B=2"},
47-
[]string{"/go/bin", "/a/b", "$PATH"},
48-
[]string{"A=1", "PATH=/go/bin:/a/b:/bin:/usr/bin", "B=2"},
50+
env: []string{"A=1", "PATH=/bin:/usr/bin", "B=2"},
51+
path: []string{"/go/bin", "/a/b", "$PATH"},
52+
want: "PATH=/go/bin:/a/b:/bin:/usr/bin",
4953
},
5054
{ // Test $PATH expansion (append only)
51-
[]string{"A=1", "PATH=/bin:/usr/bin", "B=2"},
52-
[]string{"$PATH", "/go/bin", "/a/b"},
53-
[]string{"A=1", "PATH=/bin:/usr/bin:/go/bin:/a/b", "B=2"},
55+
env: []string{"A=1", "PATH=/bin:/usr/bin", "B=2"},
56+
path: []string{"$PATH", "/go/bin", "/a/b"},
57+
want: "PATH=/bin:/usr/bin:/go/bin:/a/b",
58+
},
59+
{ // Test that empty $PATH expansion is a no-op
60+
env: []string{"A=1", "B=2"},
61+
path: []string{"$PATH"},
62+
want: "PATH=",
63+
noOp: true,
64+
},
65+
{ // Test that empty $PATH expansion does not add extraneous separators
66+
env: []string{"A=1", "B=2"},
67+
path: []string{"$PATH", "$WORKDIR/foo"},
68+
want: "PATH=/workdir/foo",
69+
},
70+
{ // Test that in case of multiple PATH entries we modify the last one,
71+
// not the first.
72+
env: []string{"PATH=/bin:/usr/bin", "PATH=/bin:/usr/bin:/usr/local/bin"},
73+
path: []string{"$WORKDIR/foo", "$PATH"},
74+
want: "PATH=/workdir/foo:/bin:/usr/bin:/usr/local/bin",
75+
},
76+
{ // Test that Windows reads the existing variable regardless of case
77+
goos: "windows",
78+
wd: `C:\workdir`,
79+
env: []string{"A=1", `PaTh=C:\Go\bin;C:\windows`, "B=2"},
80+
path: []string{"$PATH", `$WORKDIR\foo`},
81+
want: `PATH=C:\Go\bin;C:\windows;C:\workdir\foo`,
82+
},
83+
{ // Test that plan9 uses plan9 separators and "path" instead of "PATH"
84+
goos: "plan9",
85+
env: []string{"path=/bin\x00/usr/bin", "PATH=/bananas"},
86+
path: []string{"$PATH", "$WORKDIR/foo"},
87+
want: "path=/bin\x00/usr/bin\x00/workdir/foo",
5488
},
5589
} {
56-
got := setPathEnv(c.env, c.path, workDir)
57-
if g, w := fmt.Sprint(got), fmt.Sprint(c.want); g != w {
58-
t.Errorf("setPathEnv(%q, %q) = %q, want %q", c.env, c.path, g, w)
90+
goos := c.goos
91+
if goos == "" {
92+
goos = "linux"
93+
}
94+
wd := c.wd
95+
if wd == "" {
96+
wd = "/workdir"
97+
}
98+
got, gotOk := pathEnv(goos, c.env, c.path, wd)
99+
wantOk := !c.noOp
100+
if got != c.want || gotOk != wantOk {
101+
t.Errorf("pathEnv(%q, %q, %q, %q) =\n\t%q, %t\nwant:\n\t%q, %t", goos, c.env, c.path, wd, got, gotOk, c.want, wantOk)
59102
}
60103
}
61104
}
105+
106+
func TestPathListSeparator(t *testing.T) {
107+
sep := pathListSeparator(runtime.GOOS)
108+
want := string(os.PathListSeparator)
109+
if sep != want {
110+
t.Errorf("pathListSeparator(%q) = %q; want %q", runtime.GOOS, sep, want)
111+
}
112+
}

0 commit comments

Comments
 (0)