Skip to content

Commit

Permalink
feature: restrict symbols which can exit the interpreter process
Browse files Browse the repository at this point in the history
* feature: restrict symbols which can exit the interpreter process

Some symbols such as os.Exit or log.Fatal, which make the current process
to exit, are now restricted. They are replaced by a version which panics
instead of exiting, as panics are recovered by Eval.

The restricted os.FindProcess version is identical to the original
except it errors when trying to return the self process, in order to
forbid killing or signaling the interpreter process from script.

The os/exec symbols are available only through unrestricted package.

The original symbols are stored in an unrestricted package, which
requires an explicit Use, as for unsafe and syscall packages.

The Use() interpreter method has been slightly modified to allow inplace
updating of package symbols, allowing to replace some symbols but not
the entire imported package.

A command line option -unrestricted has been added to yaegi CLI to use
the unrestricted symbols.

Fixes #486.

* fix: lint
  • Loading branch information
mvertes committed Jul 8, 2020
1 parent bc2b224 commit b376650
Show file tree
Hide file tree
Showing 18 changed files with 220 additions and 68 deletions.
18 changes: 18 additions & 0 deletions _test/restricted0.go
@@ -0,0 +1,18 @@
package main

import (
"fmt"
"log"
)

func main() {
defer func() {
r := recover()
fmt.Println("recover:", r)
}()
log.Fatal("log.Fatal does not exit")
println("not printed")
}

// Output:
// recover: log.Fatal does not exit
18 changes: 18 additions & 0 deletions _test/restricted1.go
@@ -0,0 +1,18 @@
package main

import (
"fmt"
"os"
)

func main() {
defer func() {
r := recover()
fmt.Println("recover:", r)
}()
os.Exit(1)
println("not printed")
}

// Output:
// recover: os.Exit(1)
14 changes: 14 additions & 0 deletions _test/restricted2.go
@@ -0,0 +1,14 @@
package main

import (
"fmt"
"os"
)

func main() {
p, err := os.FindProcess(os.Getpid())
fmt.Println(p, err)
}

// Output:
// <nil> restricted
23 changes: 23 additions & 0 deletions _test/restricted3.go
@@ -0,0 +1,23 @@
package main

import (
"bytes"
"fmt"
"log"
)

var (
buf bytes.Buffer
logger = log.New(&buf, "logger: ", log.Lshortfile)
)

func main() {
defer func() {
r := recover()
fmt.Println("recover:", r, buf.String())
}()
logger.Fatal("test log")
}

// Output:
// recover: test log logger: restricted.go:39: test log
15 changes: 15 additions & 0 deletions cmd/goexports/goexports.go
Expand Up @@ -115,6 +115,17 @@ type Wrap struct {
Method []Method
}

// restricted map defines symbols for which a special implementation is provided.
var restricted = map[string]bool{
"osExit": true,
"osFindProcess": true,
"logFatal": true,
"logFatalf": true,
"logFatalln": true,
"logLogger": true,
"logNew": true,
}

func genContent(dest, pkgName, license string, skip map[string]bool) ([]byte, error) {
p, err := importer.ForCompiler(token.NewFileSet(), "source", nil).Import(pkgName)
if err != nil {
Expand Down Expand Up @@ -150,6 +161,10 @@ func genContent(dest, pkgName, license string, skip map[string]bool) ([]byte, er
if skip[pname] {
continue
}
if rname := path.Base(pkgName) + name; restricted[rname] {
// Restricted symbol, locally provided by stdlib wrapper.
pname = rname
}

switch o := o.(type) {
case *types.Const:
Expand Down
9 changes: 8 additions & 1 deletion cmd/yaegi/yaegi.go
Expand Up @@ -96,17 +96,20 @@ import (
"github.com/containous/yaegi/interp"
"github.com/containous/yaegi/stdlib"
"github.com/containous/yaegi/stdlib/syscall"
"github.com/containous/yaegi/stdlib/unrestricted"
"github.com/containous/yaegi/stdlib/unsafe"
)

func main() {
var interactive bool
var useUnsafe bool
var useSyscall bool
var useUnrestricted bool
var useUnsafe bool
var tags string
var cmd string
flag.BoolVar(&interactive, "i", false, "start an interactive REPL")
flag.BoolVar(&useSyscall, "syscall", false, "include syscall symbols")
flag.BoolVar(&useUnrestricted, "unrestricted", false, "include unrestricted symbols")
flag.StringVar(&tags, "tags", "", "set a list of build tags")
flag.BoolVar(&useUnsafe, "unsafe", false, "include usafe symbols")
flag.StringVar(&cmd, "e", "", "set the command to be executed (instead of script or/and shell)")
Expand All @@ -128,6 +131,10 @@ func main() {
if useUnsafe {
i.Use(unsafe.Symbols)
}
if useUnrestricted {
// Use of unrestricted symbols should always follow use of stdlib symbols, to update them.
i.Use(unrestricted.Symbols)
}

if cmd != `` {
i.REPL(strings.NewReader(cmd), os.Stderr)
Expand Down
9 changes: 8 additions & 1 deletion interp/interp.go
Expand Up @@ -474,7 +474,14 @@ func (interp *Interpreter) Use(values Exports) {
continue
}

interp.binPkg[k] = v
if interp.binPkg[k] == nil {
interp.binPkg[k] = v
continue
}

for s, sym := range v {
interp.binPkg[k][s] = sym
}
}
}

Expand Down
4 changes: 4 additions & 0 deletions interp/interp_consistent_test.go
Expand Up @@ -75,6 +75,10 @@ func TestInterpConsistencyBuild(t *testing.T) {
file.Name() == "redeclaration-global4.go" || // expect error
file.Name() == "redeclaration-global5.go" || // expect error
file.Name() == "redeclaration-global6.go" || // expect error
file.Name() == "restricted0.go" || // expect error
file.Name() == "restricted1.go" || // expect error
file.Name() == "restricted2.go" || // expect error
file.Name() == "restricted3.go" || // expect error
file.Name() == "server6.go" || // syntax parsing
file.Name() == "server5.go" || // syntax parsing
file.Name() == "server4.go" || // syntax parsing
Expand Down
2 changes: 1 addition & 1 deletion interp/run.go
Expand Up @@ -2763,7 +2763,7 @@ func convertLiteralValue(n *node, t reflect.Type) {
case n.typ.cat == nilT:
// Create a zero value of target type.
n.rval = reflect.New(t).Elem()
case !(n.kind == basicLit || n.rval.IsValid()) || t == nil || t.Kind() == reflect.Interface:
case !(n.kind == basicLit || n.rval.IsValid()) || t == nil || t.Kind() == reflect.Interface || t.Kind() == reflect.Slice && t.Elem().Kind() == reflect.Interface:
// Skip non-constant values, undefined target type or interface target type.
case n.rval.IsValid():
// Convert constant value to target type.
Expand Down
10 changes: 5 additions & 5 deletions stdlib/go1_13_log.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions stdlib/go1_13_os.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 0 additions & 25 deletions stdlib/go1_13_os_exec.go

This file was deleted.

10 changes: 5 additions & 5 deletions stdlib/go1_14_log.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions stdlib/go1_14_os.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 0 additions & 25 deletions stdlib/go1_14_os_exec.go

This file was deleted.

55 changes: 55 additions & 0 deletions stdlib/restricted.go
@@ -0,0 +1,55 @@
package stdlib

import (
"errors"
"io"
"log"
"os"
"strconv"
)

var errRestricted = errors.New("restricted")

// osExit invokes panic instead of exit.
func osExit(code int) { panic("os.Exit(" + strconv.Itoa(code) + ")") }

// osFindProcess returns os.FindProcess, except for self process.
func osFindProcess(pid int) (*os.Process, error) {
if pid == os.Getpid() {
return nil, errRestricted
}
return os.FindProcess(pid)
}

// The following functions call Panic instead of Fatal to avoid exit.
func logFatal(v ...interface{}) { log.Panic(v...) }
func logFatalf(f string, v ...interface{}) { log.Panicf(f, v...) }
func logFatalln(v ...interface{}) { log.Panicln(v...) }

type logLogger struct {
l *log.Logger
}

// logNew Returns a wrapped logger.
func logNew(out io.Writer, prefix string, flag int) *logLogger {
return &logLogger{log.New(out, prefix, flag)}
}

// The following methods call Panic instead of Fatal to avoid exit.
func (l *logLogger) Fatal(v ...interface{}) { l.l.Panic(v...) }
func (l *logLogger) Fatalf(f string, v ...interface{}) { l.l.Panicf(f, v...) }
func (l *logLogger) Fatalln(v ...interface{}) { l.l.Panicln(v...) }

// The following methods just forward to wrapped logger.
func (l *logLogger) Flags() int { return l.l.Flags() }
func (l *logLogger) Output(d int, s string) error { return l.l.Output(d, s) }
func (l *logLogger) Panic(v ...interface{}) { l.l.Panic(v...) }
func (l *logLogger) Panicf(f string, v ...interface{}) { l.l.Panicf(f, v...) }
func (l *logLogger) Panicln(v ...interface{}) { l.l.Panicln(v...) }
func (l *logLogger) Prefix() string { return l.l.Prefix() }
func (l *logLogger) Print(v ...interface{}) { l.l.Print(v...) }
func (l *logLogger) Printf(f string, v ...interface{}) { l.l.Printf(f, v...) }
func (l *logLogger) Println(v ...interface{}) { l.l.Println(v...) }
func (l *logLogger) SetFlags(flag int) { l.l.SetFlags(flag) }
func (l *logLogger) SetOutput(w io.Writer) { l.l.SetOutput(w) }
func (l *logLogger) Writer() io.Writer { return l.l.Writer() }
2 changes: 1 addition & 1 deletion stdlib/stdlib.go
Expand Up @@ -43,7 +43,7 @@ func init() {
//go:generate ../cmd/goexports/goexports net net/http net/http/cgi net/http/cookiejar net/http/fcgi
//go:generate ../cmd/goexports/goexports net/http/httptest net/http/httptrace net/http/httputil net/http/pprof
//go:generate ../cmd/goexports/goexports net/mail net/rpc net/rpc/jsonrpc net/smtp net/textproto net/url
//go:generate ../cmd/goexports/goexports os os/exec os/signal os/user
//go:generate ../cmd/goexports/goexports os os/signal os/user
//go:generate ../cmd/goexports/goexports path path/filepath reflect regexp regexp/syntax
//go:generate ../cmd/goexports/goexports runtime runtime/debug runtime/pprof runtime/trace
//go:generate ../cmd/goexports/goexports sort strconv strings sync sync/atomic
Expand Down

0 comments on commit b376650

Please sign in to comment.