diff --git a/.gitignore b/.gitignore index 393349cc4..41fdba313 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .henv +.hermit testdata/env build pkgs diff --git a/.golangci.yml b/.golangci.yml index 450cce417..234e39a5c 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -44,6 +44,7 @@ linters: - paralleltest - ifshort # so annoying - prealloc + - nolintlint linters-settings: govet: diff --git a/app/validate_script_cmd.go b/app/validate_script_cmd.go index d6d96869d..289cb4894 100644 --- a/app/validate_script_cmd.go +++ b/app/validate_script_cmd.go @@ -11,7 +11,7 @@ import ( "github.com/cashapp/hermit" "github.com/cashapp/hermit/ui" "github.com/pkg/errors" - "mvdan.cc/sh/syntax" + "mvdan.cc/sh/v3/syntax" ) var ( diff --git a/go.mod b/go.mod index 4aca375bd..9581fa8a1 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,6 @@ require ( github.com/gobwas/glob v0.2.3 github.com/gofrs/flock v0.8.0 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 - github.com/kr/text v0.2.0 // indirect github.com/mattn/go-isatty v0.0.12 github.com/mitchellh/go-ps v1.0.0 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect @@ -36,9 +35,10 @@ require ( go.etcd.io/bbolt v1.3.5 go.uber.org/multierr v1.7.0 golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad - golang.org/x/net v0.0.0-20210916014120-12bc252f5db8 - golang.org/x/sys v0.0.0-20210423082822-04245dca01da + golang.org/x/net v0.0.0-20200421231249-e086a090c8fd + golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e + golang.org/x/text v0.3.2 // indirect gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect howett.net/plist v0.0.0-20200419221736-3b63eb3a43b5 - mvdan.cc/sh v2.6.4+incompatible + mvdan.cc/sh/v3 v3.3.0 ) diff --git a/go.sum b/go.sum index e28d6ad63..7ea1ee21e 100644 --- a/go.sum +++ b/go.sum @@ -25,6 +25,8 @@ github.com/avvmoto/buf-readerat v0.0.0-20171115124131-a17c8cb89270/go.mod h1:2Xt github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4= github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= +github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -36,6 +38,7 @@ github.com/gofrs/flock v0.8.0 h1:MSdYClljsF3PbENUUEx85nkWfJSGfzYI9yEBZOJz6CY= github.com/gofrs/flock v0.8.0/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/google/renameio v1.0.1-0.20210406141108-81588dbe0453/go.mod h1:t/HQoYBZSsWSNK35C6CO/TpPLDVWvxOHboWUAweKUpk= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= @@ -45,8 +48,9 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/klauspost/compress v1.11.7 h1:0hzRabrMN4tSTvMfnL3SCv1ZGeAP23ynzodBgaHeMeg= github.com/klauspost/compress v1.11.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -57,6 +61,7 @@ github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -66,6 +71,7 @@ github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXq github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/qdm12/reprint v0.0.0-20200326205758-722754a53494 h1:wSmWgpuccqS2IOfmYrbRiUgv+g37W5suLLLxwwniTSc= github.com/qdm12/reprint v0.0.0-20200326205758-722754a53494/go.mod h1:yipyliwI08eQ6XwDm1fEwKPdF/xdbkiHtrU+1Hg+vc4= +github.com/rogpeppe/go-internal v1.7.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/saracen/go7z v0.0.0-20191010121135-9c09b6bd7fda h1:h+YpzUB/bGVJcLqW+d5GghcCmE/A25KbzjXvWJQi/+o= github.com/saracen/go7z v0.0.0-20191010121135-9c09b6bd7fda/go.mod h1:MSotTrCv1PwoR8QgU1JurEx+lNNbtr25I+m0zbLyAGw= github.com/saracen/go7z-fixtures v0.0.0-20190623165746-aa6b8fba1d2f h1:PF9WV5j/x6MT+x/sauUHd4objCvJbZb0wdxZkHSdd5A= @@ -105,24 +111,26 @@ golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200421231249-e086a090c8fd h1:QPwSajcTUrFriMF1nJ3XzgoqakqQEsnZf9LdXdi2nkI= golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20210916014120-12bc252f5db8 h1:/6y1LfuqNuQdHAm0jjtPtgRcxIxjVZgm5OTu8/QhZvk= -golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210503080704-8803ae5d1324/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210503060354-a79de5458b56 h1:b8jxX3zqjpqb2LklXPzKSGJhzyxCOZSz8ncv8Nv+y7w= +golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191108193012-7d206e10da11 h1:Yq9t9jnGoR+dBuitxdo9l6Q7xh/zOyNnYUtDKaQ3x0E= @@ -132,6 +140,7 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= @@ -139,5 +148,6 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= howett.net/plist v0.0.0-20200419221736-3b63eb3a43b5 h1:AQkaJpH+/FmqRjmXZPELom5zIERYZfwTjnHpfoVMQEc= howett.net/plist v0.0.0-20200419221736-3b63eb3a43b5/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= -mvdan.cc/sh v2.6.4+incompatible h1:eD6tDeh0pw+/TOTI1BBEryZ02rD2nMcFsgcvde7jffM= -mvdan.cc/sh v2.6.4+incompatible/go.mod h1:IeeQbZq+x2SUGBensq/jge5lLQbS3XT2ktyp3wrt4x8= +mvdan.cc/editorconfig v0.2.0/go.mod h1:lvnnD3BNdBYkhq+B4uBuFFKatfp02eB6HixDvEz91C0= +mvdan.cc/sh/v3 v3.3.0 h1:ujzElMnry63f4I5sjPFxzo6xia+gwsHZM0yyauuyZ6k= +mvdan.cc/sh/v3 v3.3.0/go.mod h1:dh3avhLDhJJ/MJKzbak6FYn+DJKUWk7Fb6Dh5mGdv6Y= diff --git a/shell/sandbox/builtins.go b/shell/sandbox/builtins.go new file mode 100644 index 000000000..e0c187f95 --- /dev/null +++ b/shell/sandbox/builtins.go @@ -0,0 +1,88 @@ +package sandbox + +import ( + "fmt" + "path/filepath" + "reflect" + "strings" + + "github.com/alecthomas/kong" + "github.com/pkg/errors" + "mvdan.cc/sh/v3/interp" +) + +var builtins = map[string]builtinCmd{ + "ls": &lsCmd{}, + "mkdir": &mkdirCmd{}, + "rm": &rmCmd{}, + "cat": &catCmd{}, + "grep": &grepCmd{}, +} + +type cmdCtx struct { + *Sandbox + interp.HandlerContext + runner *interp.Runner +} + +// Sanitise a path within the sandbox. +func (c *cmdCtx) Sanitise(path string) (string, error) { + if !filepath.IsAbs(path) { + path = filepath.Join(c.Dir, path) + } + for _, dir := range c.allow { + if strings.HasPrefix(path, dir) { + return path, nil + } + } + return "", errors.Wrap(ErrSandboxViolation, path) +} + +type builtinCmd interface { + Run(bctx cmdCtx) error +} + +func runBuiltinCmd(bctx cmdCtx, args []string) (present bool, err error) { + // We make Kong report exit() via a panic so it doesn't continue. + defer func() { + value := recover() + if value != nil { + present = true + switch value := value.(type) { + case int: + if value != 0 { + err = interp.NewExitStatus(uint8(value)) + } + case error: + err = value + default: + err = errors.Errorf("%s", value) + } + } + }() + // fmt.Fprintf(bctx.Stderr, "+ %s\n", shellquote.Join(args...)) + factory, ok := builtins[args[0]] + if !ok { + return false, nil + } + cmd := reflect.New(reflect.TypeOf(factory).Elem()).Interface().(builtinCmd) + exitStatus := 0 + _, err = kong.Must(cmd, + kong.Exit(func(i int) { panic(i) }), + kong.ShortUsageOnError(), + kong.Name(args[0]), + ).Parse(args[1:]) + if err != nil { + fmt.Fprintf(bctx.Stderr, "%s: %s\n", args[0], err) + return true, interp.NewExitStatus(1) + } + if exitStatus != 0 { + return true, nil + } + err = cmd.Run(bctx) + if err != nil { + fmt.Fprintf(bctx.Stderr, "%s: %s\n", args[0], err) + return true, interp.NewExitStatus(1) + } + return true, nil +} diff --git a/shell/sandbox/cat.go b/shell/sandbox/cat.go new file mode 100644 index 000000000..b43aaad34 --- /dev/null +++ b/shell/sandbox/cat.go @@ -0,0 +1,36 @@ +package sandbox + +import ( + "io" + "os" + + "github.com/pkg/errors" +) + +type catCmd struct { + Paths []string `arg:"" optional:"" help:"Files to cat, if any."` +} + +func (c *catCmd) Run(ctx cmdCtx) error { + if len(c.Paths) == 0 { + _, err := io.Copy(ctx.Stdout, ctx.Stdin) + return errors.WithStack(err) + } + for _, path := range c.Paths { + var err error + path, err = ctx.Sanitise(path) + if err != nil { + return errors.WithStack(err) + } + r, err := os.Open(path) + if err != nil { + return errors.WithStack(err) + } + _, err = io.Copy(ctx.Stdout, r) + _ = r.Close() + if err != nil { + return errors.WithStack(err) + } + } + return nil +} diff --git a/shell/sandbox/grep.go b/shell/sandbox/grep.go new file mode 100644 index 000000000..15f9a2751 --- /dev/null +++ b/shell/sandbox/grep.go @@ -0,0 +1,61 @@ +package sandbox + +import ( + "bufio" + "fmt" + "io" + "os" + "regexp" + + "github.com/pkg/errors" +) + +type grepCmd struct { + Invert bool `short:"v" help:"Invert match."` + List bool `short:"l" help:"List matching filenames."` + Pattern string `arg:"" help:"Pattern to match."` + Files []string `arg:"" optional:"" help:"Files to search."` +} + +func (g *grepCmd) Run(ctx cmdCtx) error { + re, err := regexp.CompilePOSIX(g.Pattern) + if err != nil { + return errors.WithStack(err) + } + if len(g.Files) == 0 { + return errors.WithStack(g.grep(ctx, re, "-", ctx.Stdin)) + } + for _, file := range g.Files { + file, err = ctx.Sanitise(file) + if err != nil { + return errors.Wrap(err, "grep") + } + r, err := os.Open(file) + if err != nil { + return errors.WithStack(err) + } + err = g.grep(ctx, re, file, r) + _ = r.Close() + if err != nil { + return errors.WithStack(err) + } + } + return nil +} + +func (g *grepCmd) grep(ctx cmdCtx, re *regexp.Regexp, filename string, r io.Reader) error { + s := bufio.NewScanner(r) + for s.Scan() { + line := s.Bytes() + if re.Find(line) == nil { + continue + } + if g.List { + fmt.Fprintln(ctx.Stdout, filename) + return nil + } + fmt.Fprintln(ctx.Stdout, string(line)) + } + fmt.Fprint(ctx.Stdout) + return errors.WithStack(s.Err()) +} diff --git a/shell/sandbox/ls.go b/shell/sandbox/ls.go new file mode 100644 index 000000000..43a035e9c --- /dev/null +++ b/shell/sandbox/ls.go @@ -0,0 +1,103 @@ +package sandbox + +import ( + "fmt" + "io/fs" + "io/ioutil" + "os" + "os/user" + "path/filepath" + "strings" + "syscall" + "text/tabwriter" + + "github.com/pkg/errors" +) + +type lsCmd struct { + All bool `short:"a" help:"List all files."` + Long bool `short:"l" help:"Long listing."` + + Paths []string `arg:"" optional:"" help:"Paths to list."` +} + +func (l *lsCmd) Run(ctx cmdCtx) error { + paths := l.Paths + if len(paths) == 0 { + paths = append(paths, ctx.Dir) + } + w := tabwriter.NewWriter(ctx.Stdout, 4, 4, 1, ' ', 0) + for _, path := range paths { + var err error + path, err = ctx.Sanitise(path) + if err != nil { + return err + } + info, err := os.Stat(path) + if err != nil { + return errors.WithStack(err) + } + var entries []fs.FileInfo + if info.IsDir() { + entries, err = ioutil.ReadDir(path) + if err != nil { + return errors.WithStack(err) + } + // Synthesise . and .. + if l.All { + dotdot, err := os.Stat(filepath.Join(path, "..")) + if err != nil { + return errors.WithStack(err) + } + entries = append([]fs.FileInfo{ + &renamedFileInfo{info, "."}, + &renamedFileInfo{dotdot, ".."}, + }, entries...) + } + } else { + entries = append(entries, info) + } + for _, entry := range entries { + if !l.All && strings.HasPrefix(entry.Name(), ".") { + continue + } + if !l.Long { + fmt.Fprintln(w, entry.Name()) + continue + } + var ( + size uint64 + username = "user" + groupname = "group" + ) + if stat, ok := entry.Sys().(*syscall.Stat_t); ok { + usr, err := user.LookupId(fmt.Sprintf("%d", stat.Uid)) + if err != nil { + return errors.WithStack(err) + } + group, err := user.LookupGroupId(fmt.Sprintf("%d", stat.Gid)) + if err != nil { + return errors.WithStack(err) + } + username = usr.Username + groupname = group.Name + size = uint64(stat.Nlink) // nolint + + } + fmt.Fprintf(w, "%s\t%d\t%s\t%s\t%d\t%s\t%s\n", + entry.Mode(), size, username, groupname, entry.Size(), + entry.ModTime().Format("2 Jan 15:04"), + entry.Name()) + } + } + return w.Flush() +} + +type renamedFileInfo struct { + fs.FileInfo + name string +} + +func (r *renamedFileInfo) Name() string { + return r.name +} diff --git a/shell/sandbox/mkdir.go b/shell/sandbox/mkdir.go new file mode 100644 index 000000000..55d103710 --- /dev/null +++ b/shell/sandbox/mkdir.go @@ -0,0 +1,37 @@ +package sandbox + +import ( + "os" + + "github.com/pkg/errors" +) + +type mkdirCmd struct { + Mode os.FileMode `short:"m" help:"Set the file permission bits of the final created directory to the specified mode."` + Intermediate bool `short:"p" help:"Create intermediate directories as needed."` + Dirs []string `arg:"" help:"Directories to create"` +} + +func (m *mkdirCmd) Run(ctx cmdCtx) error { + mode := m.Mode + if mode == 0 { + mode = 0777 + } + for _, dir := range m.Dirs { + var err error + dir, err = ctx.Sanitise(dir) + if err != nil { + return err + } + if m.Intermediate { + if err := os.MkdirAll(dir, mode); err != nil { + return errors.Wrap(err, dir) + } + } else { + if err := os.Mkdir(dir, mode); err != nil { + return errors.Wrap(err, dir) + } + } + } + return nil +} diff --git a/shell/sandbox/rm.go b/shell/sandbox/rm.go new file mode 100644 index 000000000..91f7bcca9 --- /dev/null +++ b/shell/sandbox/rm.go @@ -0,0 +1,34 @@ +package sandbox + +import ( + "os" + + "github.com/pkg/errors" +) + +type rmCmd struct { + Recursive bool `short:"r" help:"Recursively delete."` + Force bool `short:"f" help:"Force deletion of read-only files."` + + Paths []string `arg:"" help:"Paths to delete."` +} + +func (r *rmCmd) Run(bctx cmdCtx) error { + for _, path := range r.Paths { + var err error + path, err = bctx.Sanitise(path) + if err != nil { + return err + } + if r.Recursive { + if err = os.RemoveAll(path); err != nil { + return errors.WithStack(err) + } + } else { + if err = os.Remove(path); err != nil { + return errors.WithStack(err) + } + } + } + return nil +} diff --git a/shell/sandbox/sandbox.go b/shell/sandbox/sandbox.go new file mode 100644 index 000000000..cd4442862 --- /dev/null +++ b/shell/sandbox/sandbox.go @@ -0,0 +1,103 @@ +// Package sandbox provides a loosely sandboxed shell interpreter for running +// Hermit hooks in a slightly safer manner. +package sandbox + +import ( + "context" + "io" + "os" + "strings" + + "github.com/pkg/errors" + "mvdan.cc/sh/v3/expand" + "mvdan.cc/sh/v3/interp" + "mvdan.cc/sh/v3/syntax" +) + +// An Option for altering how the sandbox is run. +type Option func(*Sandbox) + +// ErrSandboxViolation is returned when a script violates sandbox constraints. +var ErrSandboxViolation = errors.New("sandbox violation") + +// Path adds to the sandboxes $PATH. +func Path(path ...string) Option { + return func(c *Sandbox) { + c.path = append(c.path, path...) + } +} + +// Allow access to these additional directories. +func Allow(dir ...string) Option { + return func(s *Sandbox) { + s.allow = append(s.allow, dir...) + } +} + +// Sandbox shell script evaluation under a root. +// +// The sandbox prevents writes outside the root directory, and restricts what +// can be executed to: +// +// - A minimal set of builtin emulated POSIX utilities (ls, rm, ln, etc.) +// - Binaries under "root". +// - Binaries in the $PATH as provided vi Path(). +type Sandbox struct { + allow []string + path []string + runner *interp.Runner +} + +// New creates a Sandbox at root, which will also be the initial working directory. +func New(root string, options ...Option) (*Sandbox, error) { + sandbox := &Sandbox{ + allow: []string{root}, + } + for _, option := range options { + option(sandbox) + } + runner, err := interp.New( + interp.Dir(root), + interp.StdIO(os.Stdin, os.Stdout, os.Stderr), + interp.OpenHandler(sandbox.openHandler), + interp.ExecHandler(sandbox.execHandler), + interp.Params("-e"), + interp.Env(expand.FuncEnviron(func(s string) string { + return "" + })), + ) + if err != nil { + return nil, errors.WithStack(err) + } + sandbox.runner = runner + return sandbox, nil +} + +// Eval a shell script within the Sandbox. +func (s *Sandbox) Eval(script string) error { + node, err := syntax.NewParser().Parse(strings.NewReader(script), "") + if err != nil { + return errors.WithStack(err) + } + s.runner.Reset() + err = s.runner.Run(context.Background(), node) + return errors.WithStack(err) +} + +func (s *Sandbox) execHandler(ctx context.Context, args []string) error { + cctx := cmdCtx{s, interp.HandlerCtx(ctx), s.runner} + ok, err := runBuiltinCmd(cctx, args) + if !ok { + return errors.Errorf("unsupported command %q", args[0]) + } + return errors.WithStack(err) +} + +func (s *Sandbox) openHandler(ctx context.Context, path string, flag int, perm os.FileMode) (io.ReadWriteCloser, error) { + cctx := cmdCtx{s, interp.HandlerCtx(ctx), s.runner} + path, err := cctx.Sanitise(path) + if err != nil { + return nil, err + } + return os.OpenFile(path, flag, perm) +} diff --git a/shell/sandbox/sandbox_test.go b/shell/sandbox/sandbox_test.go new file mode 100644 index 000000000..2764b2f4a --- /dev/null +++ b/shell/sandbox/sandbox_test.go @@ -0,0 +1,36 @@ +package sandbox + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestEval(t *testing.T) { + root := t.TempDir() + sandbox, err := New(root) + require.NoError(t, err) + err = sandbox.Eval(` + set -euo pipefail + + ls --help + rm --help + ls --foo || true + ls asdfa || true + echo > .t + echo "Hello world" > t + mkdir test + echo hi > test/foo + echo CAT; cat test/foo + (cd test && ls -l) + ls + ls -la | cat + ls -la | grep test + ls -l t + rm -rf * + echo empty + ls -l + cd / + `) + require.NoError(t, err) +}