Skip to content

Commit 87e88a4

Browse files
committed
interp: make a full copy of the environment for background subshells
If a parent shell sets a variable once a background subshell is running, the subshell should not see that change, nor should the change cause any data races. We allowed the parent and child shells to share a map of variables when the child was a background subshell, which caused one of the added tests to fail often: --- FAIL: TestRunnerRun/#456 (0.00s) interp_test.go:3662: wrong output in "{ for n in {0..9}; do { echo $n; } & done; wait; } | sort": want: "0\n1\n2\n3\n4\n5\n6\n7\n8\n9\n" got: "9\n9\n9\n9\n9\n9\n9\n9\n9\n9\n" Moreover, the newly added background shell tests would cause a number of "concurrent map read and write" and data race detector panics too. overlayEnviron can be clever in sharing a variables map with the parent only when the subshell is not being run in the background. When the subshell is intended for background (concurrent) use, we need to make a full copy of the variables to avoid races.
1 parent d48750a commit 87e88a4

File tree

4 files changed

+52
-17
lines changed

4 files changed

+52
-17
lines changed

interp/api.go

+9-4
Original file line numberDiff line numberDiff line change
@@ -869,9 +869,16 @@ func (r *Runner) Exited() bool {
869869
// Subshell is not safe to use concurrently with [Run]. Orchestrating this is
870870
// left up to the caller; no locking is performed.
871871
//
872-
// To replace e.g. stdin/out/err, do StdIO(r.stdin, r.stdout, r.stderr)(r) on
872+
// To replace e.g. stdin/out/err, do [StdIO](r.stdin, r.stdout, r.stderr)(r) on
873873
// the copy.
874874
func (r *Runner) Subshell() *Runner {
875+
return r.subshell(true)
876+
}
877+
878+
// subshell is like [Runner.subshell], but allows skipping some allocations and copies
879+
// when creating subshells which will not be used concurrently with the parent shell.
880+
// TODO(v4): we should expose this, e.g. SubshellForeground and SubshellBackground.
881+
func (r *Runner) subshell(background bool) *Runner {
875882
if !r.didReset {
876883
r.Reset()
877884
}
@@ -897,10 +904,8 @@ func (r *Runner) Subshell() *Runner {
897904

898905
origStdout: r.origStdout, // used for process substitutions
899906
}
907+
r2.writeEnv = newOverlayEnviron(r.writeEnv, background)
900908
// Funcs are copied, since they might be modified.
901-
// Env vars aren't copied; setVar will copy lists and maps as needed.
902-
oenv := &overlayEnviron{parent: r.writeEnv}
903-
r2.writeEnv = oenv
904909
r2.Funcs = maps.Clone(r.Funcs)
905910
r2.Vars = make(map[string]expand.Variable)
906911
r2.alias = maps.Clone(r.alias)

interp/interp_test.go

+13
Original file line numberDiff line numberDiff line change
@@ -1422,6 +1422,19 @@ var runTests = []runTest{
14221422
{"echo foo | true | false & wait $!", "exit status 1"},
14231423
{"echo foo | false | true & wait $!", ""},
14241424
{"f() { false & true; }; f; wait $!", "exit status 1"},
1425+
// The parent and child shells should not cause data races when setting env vars.
1426+
{
1427+
"{ for n in {0..9}; do { echo $n; } & done; wait; } | sort",
1428+
"0\n1\n2\n3\n4\n5\n6\n7\n8\n9\n",
1429+
},
1430+
{
1431+
"outer=val; for n in {0..9}; do { echo $outer; } & outer=val; done; wait",
1432+
"val\nval\nval\nval\nval\nval\nval\nval\nval\nval\n",
1433+
},
1434+
{
1435+
"for n in {0..9}; do { inner=val; } & echo $inner; done",
1436+
"\n\n\n\n\n\n\n\n\n\n",
1437+
},
14251438

14261439
// bash test
14271440
{

interp/runner.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ func (r *Runner) fillExpandConfig(ctx context.Context) {
6161
f.Close()
6262
return err
6363
}
64-
r2 := r.Subshell()
64+
r2 := r.subshell(false)
6565
r2.stdout = w
6666
r2.stmts(ctx, cs.Stmts)
6767
r.lastExpandExit = r2.exit
@@ -99,7 +99,7 @@ func (r *Runner) fillExpandConfig(ctx context.Context) {
9999
}
100100
}
101101

102-
r2 := r.Subshell()
102+
r2 := r.subshell(true)
103103
stdout := r.origStdout
104104
// TODO: note that `man bash` mentions that `wait` only waits for the last
105105
// process substitution as long as it is $!; the logic here would mean we wait for all of them.
@@ -299,7 +299,7 @@ func (r *Runner) stmt(ctx context.Context, st *syntax.Stmt) {
299299
}
300300
r.exit = 0
301301
if st.Background {
302-
r2 := r.Subshell()
302+
r2 := r.subshell(true)
303303
st2 := *st
304304
st2.Background = false
305305
bg := bgProc{
@@ -364,7 +364,7 @@ func (r *Runner) cmd(ctx context.Context, cm syntax.Command) {
364364
case *syntax.Block:
365365
r.stmts(ctx, cm.Stmts)
366366
case *syntax.Subshell:
367-
r2 := r.Subshell()
367+
r2 := r.subshell(false)
368368
r2.stmts(ctx, cm.Stmts)
369369
r.exit = r2.exit
370370
r.setErr(r2.err)
@@ -467,7 +467,7 @@ func (r *Runner) cmd(ctx context.Context, cm syntax.Command) {
467467
r.setErr(err)
468468
return
469469
}
470-
r2 := r.Subshell()
470+
r2 := r.subshell(false)
471471
r2.stdout = pw
472472
if cm.Op == syntax.PipeAll {
473473
r2.stderr = pw

interp/vars.go

+25-8
Original file line numberDiff line numberDiff line change
@@ -16,32 +16,47 @@ import (
1616
"mvdan.cc/sh/v3/syntax"
1717
)
1818

19+
func newOverlayEnviron(parent expand.Environ, background bool) *overlayEnviron {
20+
oenv := &overlayEnviron{}
21+
if !background {
22+
oenv.parent = parent
23+
} else {
24+
// We could do better here if the parent is also an overlayEnviron;
25+
// measure with profiles or benchmarks before we choose to do so.
26+
oenv.values = make(map[string]expand.Variable)
27+
maps.Insert(oenv.values, parent.Each)
28+
}
29+
return oenv
30+
}
31+
1932
type overlayEnviron struct {
2033
parent expand.Environ
2134
values map[string]expand.Variable
2235

2336
// We need to know if the current scope is a function's scope, because
24-
// functions can modify global variables.
37+
// functions can modify global variables. When true, [parent] must not be nil.
2538
funcScope bool
2639
}
2740

2841
func (o *overlayEnviron) Get(name string) expand.Variable {
2942
if vr, ok := o.values[name]; ok {
3043
return vr
3144
}
32-
return o.parent.Get(name)
45+
if o.parent != nil {
46+
return o.parent.Get(name)
47+
}
48+
return expand.Variable{}
3349
}
3450

3551
func (o *overlayEnviron) Set(name string, vr expand.Variable) error {
36-
prevOverlay, inOverlay := o.values[name]
37-
prev := o.parent.Get(name)
52+
prev, inOverlay := o.values[name]
3853
// Manipulation of a global var inside a function.
39-
if o.funcScope && !vr.Local && !prevOverlay.Local {
54+
if o.funcScope && !vr.Local && !prev.Local {
4055
// In a function, the parent environment is ours, so it's always read-write.
4156
return o.parent.(expand.WriteEnviron).Set(name, vr)
4257
}
43-
if inOverlay {
44-
prev = prevOverlay
58+
if !inOverlay && o.parent != nil {
59+
prev = o.parent.Get(name)
4560
}
4661

4762
if o.values == nil {
@@ -70,7 +85,9 @@ func (o *overlayEnviron) Set(name string, vr expand.Variable) error {
7085
}
7186

7287
func (o *overlayEnviron) Each(f func(name string, vr expand.Variable) bool) {
73-
o.parent.Each(f)
88+
if o.parent != nil {
89+
o.parent.Each(f)
90+
}
7491
for name, vr := range o.values {
7592
if !f(name, vr) {
7693
return

0 commit comments

Comments
 (0)