Skip to content

Commit bb7a399

Browse files
committed
expand: avoid panics on division by zero
Arithm now returns one more type of error for this case. Update the interpreter accordingly, and add tests. The behavior we adopt follows what Bash does when not run interactively, so start documenting that in the package godoc. Also add a TODO as we should have some standard to tell whether errors coming from the expand package or the user's handlers are fatal. That is, whether they should exit the shell, and with what status code. Fixes #892.
1 parent 7bf9ee0 commit bb7a399

File tree

6 files changed

+82
-48
lines changed

6 files changed

+82
-48
lines changed

expand/arith.go

+28-22
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ func Arithm(cfg *Config, expr syntax.ArithmExpr) (int, error) {
9292
if err != nil {
9393
return 0, err
9494
}
95-
return binArit(x.Op, left, right), nil
95+
return binArit(x.Op, left, right)
9696
default:
9797
panic(fmt.Sprintf("unexpected arithm expr: %T", x))
9898
}
@@ -129,6 +129,9 @@ func (cfg *Config) assgnArit(b *syntax.BinaryArithm) (int, error) {
129129
case syntax.MulAssgn:
130130
val *= arg
131131
case syntax.QuoAssgn:
132+
if arg == 0 {
133+
return 0, fmt.Errorf("division by zero")
134+
}
132135
val /= arg
133136
case syntax.RemAssgn:
134137
val %= arg
@@ -161,48 +164,51 @@ func intPow(a, b int) int {
161164
return p
162165
}
163166

164-
func binArit(op syntax.BinAritOperator, x, y int) int {
167+
func binArit(op syntax.BinAritOperator, x, y int) (int, error) {
165168
switch op {
166169
case syntax.Add:
167-
return x + y
170+
return x + y, nil
168171
case syntax.Sub:
169-
return x - y
172+
return x - y, nil
170173
case syntax.Mul:
171-
return x * y
174+
return x * y, nil
172175
case syntax.Quo:
173-
return x / y
176+
if y == 0 {
177+
return 0, fmt.Errorf("division by zero")
178+
}
179+
return x / y, nil
174180
case syntax.Rem:
175-
return x % y
181+
return x % y, nil
176182
case syntax.Pow:
177-
return intPow(x, y)
183+
return intPow(x, y), nil
178184
case syntax.Eql:
179-
return oneIf(x == y)
185+
return oneIf(x == y), nil
180186
case syntax.Gtr:
181-
return oneIf(x > y)
187+
return oneIf(x > y), nil
182188
case syntax.Lss:
183-
return oneIf(x < y)
189+
return oneIf(x < y), nil
184190
case syntax.Neq:
185-
return oneIf(x != y)
191+
return oneIf(x != y), nil
186192
case syntax.Leq:
187-
return oneIf(x <= y)
193+
return oneIf(x <= y), nil
188194
case syntax.Geq:
189-
return oneIf(x >= y)
195+
return oneIf(x >= y), nil
190196
case syntax.And:
191-
return x & y
197+
return x & y, nil
192198
case syntax.Or:
193-
return x | y
199+
return x | y, nil
194200
case syntax.Xor:
195-
return x ^ y
201+
return x ^ y, nil
196202
case syntax.Shr:
197-
return x >> uint(y)
203+
return x >> uint(y), nil
198204
case syntax.Shl:
199-
return x << uint(y)
205+
return x << uint(y), nil
200206
case syntax.AndArit:
201-
return oneIf(x != 0 && y != 0)
207+
return oneIf(x != 0 && y != 0), nil
202208
case syntax.OrArit:
203-
return oneIf(x != 0 || y != 0)
209+
return oneIf(x != 0 || y != 0), nil
204210
default: // syntax.Comma
205211
// x is executed but its result discarded
206-
return y
212+
return y, nil
207213
}
208214
}

interp/api.go

+7
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@
44
// Package interp implements an interpreter that executes shell
55
// programs. It aims to support POSIX, but its support is not complete
66
// yet. It also supports some Bash features.
7+
//
8+
// The interpreter generally aims to behave like Bash,
9+
// but it does not support all of its features.
10+
//
11+
// The interpreter currently aims to behave like a non-interactive shell,
12+
// which how most shells run scripts, and is more useful to machines.
13+
// In the future, it may gain an option to behave like an interactive shell.
714
package interp
815

916
import (

interp/handler.go

+4
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ type HandlerContext struct {
6767
// Returning a non-nil error will halt the Runner.
6868
type CallHandlerFunc func(ctx context.Context, args []string) ([]string, error)
6969

70+
// TODO: consistently treat handler errors as non-fatal by default,
71+
// but have an interface or API to specify fatal errors which should make
72+
// the shell exit with a particular status code.
73+
7074
// ExecHandlerFunc is a handler which executes simple commands.
7175
// It is called for all CallExpr nodes where the first argument is neither a
7276
// declared function nor a builtin.

interp/handler_test.go

+24-24
Original file line numberDiff line numberDiff line change
@@ -21,29 +21,29 @@ import (
2121
"mvdan.cc/sh/v3/syntax"
2222
)
2323

24-
func blacklistBuiltinExec(name string) ExecHandlerFunc {
24+
func blocklistBuiltinExec(name string) ExecHandlerFunc {
2525
return func(ctx context.Context, args []string) error {
2626
if args[0] == name {
27-
return fmt.Errorf("%s: blacklisted builtin", name)
27+
return fmt.Errorf("%s: blocklisted builtin", name)
2828
}
2929
return testExecHandler(ctx, args)
3030
}
3131
}
3232

33-
func blacklistAllExec(ctx context.Context, args []string) error {
34-
return fmt.Errorf("blacklisted: %s", args[0])
33+
func blocklistAllExec(ctx context.Context, args []string) error {
34+
return fmt.Errorf("blocklisted: %s", args[0])
3535
}
3636

37-
func blacklistNondevOpen(ctx context.Context, path string, flags int, mode os.FileMode) (io.ReadWriteCloser, error) {
37+
func blocklistNondevOpen(ctx context.Context, path string, flags int, mode os.FileMode) (io.ReadWriteCloser, error) {
3838
if path != "/dev/null" {
3939
return nil, fmt.Errorf("non-dev: %s", path)
4040
}
4141

4242
return testOpenHandler(ctx, path, flags, mode)
4343
}
4444

45-
func blacklistGlob(ctx context.Context, path string) ([]os.FileInfo, error) {
46-
return nil, fmt.Errorf("blacklisted: glob")
45+
func blocklistGlob(ctx context.Context, path string) ([]os.FileInfo, error) {
46+
return nil, fmt.Errorf("blocklisted: glob")
4747
}
4848

4949
// runnerCtx allows us to give handler functions access to the Runner, if needed.
@@ -67,40 +67,40 @@ var modCases = []struct {
6767
want string
6868
}{
6969
{
70-
name: "ExecBlacklist",
71-
exec: blacklistBuiltinExec("sleep"),
70+
name: "Execblocklist",
71+
exec: blocklistBuiltinExec("sleep"),
7272
src: "echo foo; sleep 1",
73-
want: "foo\nsleep: blacklisted builtin",
73+
want: "foo\nsleep: blocklisted builtin",
7474
},
7575
{
7676
name: "ExecWhitelist",
77-
exec: blacklistBuiltinExec("faa"),
77+
exec: blocklistBuiltinExec("faa"),
7878
src: "a=$(echo foo | sed 's/o/a/g'); echo $a; $a args",
79-
want: "faa\nfaa: blacklisted builtin",
79+
want: "faa\nfaa: blocklisted builtin",
8080
},
8181
{
8282
name: "ExecSubshell",
83-
exec: blacklistAllExec,
83+
exec: blocklistAllExec,
8484
src: "(malicious)",
85-
want: "blacklisted: malicious",
85+
want: "blocklisted: malicious",
8686
},
8787
{
8888
name: "ExecPipe",
89-
exec: blacklistAllExec,
89+
exec: blocklistAllExec,
9090
src: "malicious | echo foo",
91-
want: "foo\nblacklisted: malicious",
91+
want: "foo\nblocklisted: malicious",
9292
},
9393
{
9494
name: "ExecCmdSubst",
95-
exec: blacklistAllExec,
95+
exec: blocklistAllExec,
9696
src: "a=$(malicious)",
97-
want: "blacklisted: malicious\nexit status 1",
97+
want: "blocklisted: malicious\n", // TODO: why the newline?
9898
},
9999
{
100100
name: "ExecBackground",
101-
exec: blacklistAllExec,
101+
exec: blocklistAllExec,
102102
src: "{ malicious; true; } & { malicious; true; } & wait",
103-
want: "blacklisted: malicious",
103+
want: "blocklisted: malicious",
104104
},
105105
{
106106
name: "ExecBuiltin",
@@ -110,13 +110,13 @@ var modCases = []struct {
110110
},
111111
{
112112
name: "OpenForbidNonDev",
113-
open: blacklistNondevOpen,
113+
open: blocklistNondevOpen,
114114
src: "echo foo >/dev/null; echo bar >/tmp/x",
115115
want: "non-dev: /tmp/x",
116116
},
117117
{
118118
name: "CallReplaceWithBlank",
119-
open: blacklistNondevOpen,
119+
open: blocklistNondevOpen,
120120
call: func(ctx context.Context, args []string) ([]string, error) {
121121
return []string{"echo", "blank"}, nil
122122
},
@@ -144,9 +144,9 @@ var modCases = []struct {
144144
},
145145
{
146146
name: "GlobForbid",
147-
readdir: blacklistGlob,
147+
readdir: blocklistGlob,
148148
src: "echo *",
149-
want: "blacklisted: glob\nexit status 1",
149+
want: "blocklisted: glob\n",
150150
},
151151
}
152152

interp/interp_test.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -1818,6 +1818,10 @@ var runTests = []runTest{
18181818
"a=b b=a; echo $(($a))",
18191819
"0\n #IGNORE bash prints a warning",
18201820
},
1821+
{
1822+
"let x=3; let 3/0; ((3/0)); echo $((x/y)); let x/=0",
1823+
"division by zero\ndivision by zero\ndivision by zero\ndivision by zero\nexit status 1 #JUSTERR",
1824+
},
18211825

18221826
// set/shift
18231827
{
@@ -2307,7 +2311,7 @@ set +o pipefail
23072311
},
23082312
{
23092313
`a=(b); echo ${a[-2]}`,
2310-
"negative array index\nexit status 1 #JUSTERR",
2314+
"negative array index\n #JUSTERR",
23112315
},
23122316
// TODO: also test with gaps in arrays.
23132317
{

interp/runner.go

+14-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package interp
66
import (
77
"bytes"
88
"context"
9+
"errors"
910
"fmt"
1011
"io"
1112
"math"
@@ -155,7 +156,19 @@ func (r *Runner) updateExpandOpts() {
155156

156157
func (r *Runner) expandErr(err error) {
157158
if err != nil {
158-
r.errf("%v\n", err)
159+
errMsg := err.Error()
160+
fmt.Fprintln(r.stderr, errMsg)
161+
switch {
162+
case errors.As(err, &expand.UnsetParameterError{}):
163+
case errMsg == "invalid indirect expansion":
164+
// TODO: These errors are treated as fatal by bash.
165+
// Make the error type reflect that.
166+
case strings.HasSuffix(errMsg, "not supported"):
167+
// TODO: This "contains" is a temporary measure until the expand
168+
// package supports all syntax nodes like extended globbing.
169+
default:
170+
return // other cases do not exit
171+
}
159172
r.exitShell(context.TODO(), 1)
160173
}
161174
}

0 commit comments

Comments
 (0)