From f5c8a71698d0a7a16c68be220e49fe64bfee7f5c Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 15 Jan 2018 11:21:06 -0600 Subject: ABC-22: Plugin sandboxing for linux/amd64 (#8068) * plugin sandboxing * remove unused type * better symlink handling, better remounting, better test, whitespace fixes, and comment on the remounting * fix test compile error * big simplification for getting mount flags * mask statfs flags to the ones we're interested in --- app/plugin.go | 8 + plugin/pluginenv/options.go | 15 +- plugin/rpcplugin/ipc_test.go | 4 +- plugin/rpcplugin/main_test.go | 3 +- plugin/rpcplugin/process_test.go | 17 +- plugin/rpcplugin/rpcplugintest/rpcplugintest.go | 26 ++ plugin/rpcplugin/rpcplugintest/supervisor.go | 190 ++++++++++ plugin/rpcplugin/sandbox/sandbox.go | 34 ++ plugin/rpcplugin/sandbox/sandbox_linux.go | 468 ++++++++++++++++++++++++ plugin/rpcplugin/sandbox/sandbox_linux_test.go | 159 ++++++++ plugin/rpcplugin/sandbox/sandbox_other.go | 22 ++ plugin/rpcplugin/sandbox/sandbox_test.go | 25 ++ plugin/rpcplugin/sandbox/seccomp_linux.go | 178 +++++++++ plugin/rpcplugin/sandbox/seccomp_linux_amd64.go | 301 +++++++++++++++ plugin/rpcplugin/sandbox/seccomp_linux_other.go | 10 + plugin/rpcplugin/sandbox/seccomp_linux_test.go | 210 +++++++++++ plugin/rpcplugin/sandbox/supervisor.go | 33 ++ plugin/rpcplugin/sandbox/supervisor_test.go | 18 + plugin/rpcplugin/supervisor.go | 19 +- plugin/rpcplugin/supervisor_test.go | 170 +-------- 20 files changed, 1716 insertions(+), 194 deletions(-) create mode 100644 plugin/rpcplugin/rpcplugintest/rpcplugintest.go create mode 100644 plugin/rpcplugin/rpcplugintest/supervisor.go create mode 100644 plugin/rpcplugin/sandbox/sandbox.go create mode 100644 plugin/rpcplugin/sandbox/sandbox_linux.go create mode 100644 plugin/rpcplugin/sandbox/sandbox_linux_test.go create mode 100644 plugin/rpcplugin/sandbox/sandbox_other.go create mode 100644 plugin/rpcplugin/sandbox/sandbox_test.go create mode 100644 plugin/rpcplugin/sandbox/seccomp_linux.go create mode 100644 plugin/rpcplugin/sandbox/seccomp_linux_amd64.go create mode 100644 plugin/rpcplugin/sandbox/seccomp_linux_other.go create mode 100644 plugin/rpcplugin/sandbox/seccomp_linux_test.go create mode 100644 plugin/rpcplugin/sandbox/supervisor.go create mode 100644 plugin/rpcplugin/sandbox/supervisor_test.go diff --git a/app/plugin.go b/app/plugin.go index d96e6e990..3f06a000f 100644 --- a/app/plugin.go +++ b/app/plugin.go @@ -30,6 +30,8 @@ import ( "github.com/mattermost/mattermost-server/plugin" "github.com/mattermost/mattermost-server/plugin/pluginenv" + "github.com/mattermost/mattermost-server/plugin/rpcplugin" + "github.com/mattermost/mattermost-server/plugin/rpcplugin/sandbox" ) const ( @@ -382,6 +384,12 @@ func (a *App) InitPlugins(pluginPath, webappPath string, supervisorOverride plug if supervisorOverride != nil { options = append(options, pluginenv.SupervisorProvider(supervisorOverride)) + } else if err := sandbox.CheckSupport(); err != nil { + l4g.Warn(err.Error()) + l4g.Warn("plugin sandboxing is not supported. plugins will run with the same access level as the server") + options = append(options, pluginenv.SupervisorProvider(rpcplugin.SupervisorProvider)) + } else { + options = append(options, pluginenv.SupervisorProvider(sandbox.SupervisorProvider)) } if env, err := pluginenv.New(options...); err != nil { diff --git a/plugin/pluginenv/options.go b/plugin/pluginenv/options.go index 35ecf1d7f..43cbdac68 100644 --- a/plugin/pluginenv/options.go +++ b/plugin/pluginenv/options.go @@ -4,11 +4,10 @@ package pluginenv import ( - "fmt" - "github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/plugin" "github.com/mattermost/mattermost-server/plugin/rpcplugin" + "github.com/mattermost/mattermost-server/plugin/rpcplugin/sandbox" ) // APIProvider specifies a function that provides an API implementation to each plugin. @@ -40,14 +39,12 @@ func WebappPath(path string) Option { } } -// DefaultSupervisorProvider chooses a supervisor based on the plugin's manifest contents. E.g. if -// the manifest specifies a backend executable, it will be given an rpcplugin.Supervisor. +// DefaultSupervisorProvider chooses a supervisor based on the system and the plugin's manifest +// contents. E.g. if the manifest specifies a backend executable, it will be given an +// rpcplugin.Supervisor. func DefaultSupervisorProvider(bundle *model.BundleInfo) (plugin.Supervisor, error) { - if bundle.Manifest == nil { - return nil, fmt.Errorf("a manifest is required") - } - if bundle.Manifest.Backend == nil { - return nil, fmt.Errorf("invalid manifest: missing backend plugin") + if err := sandbox.CheckSupport(); err == nil { + return sandbox.SupervisorProvider(bundle) } return rpcplugin.SupervisorProvider(bundle) } diff --git a/plugin/rpcplugin/ipc_test.go b/plugin/rpcplugin/ipc_test.go index d6c8c0f6b..76699a11e 100644 --- a/plugin/rpcplugin/ipc_test.go +++ b/plugin/rpcplugin/ipc_test.go @@ -9,6 +9,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/mattermost/mattermost-server/plugin/rpcplugin/rpcplugintest" ) func TestIPC(t *testing.T) { @@ -17,7 +19,7 @@ func TestIPC(t *testing.T) { defer os.RemoveAll(dir) pingpong := filepath.Join(dir, "pingpong.exe") - compileGo(t, ` + rpcplugintest.CompileGo(t, ` package main import ( diff --git a/plugin/rpcplugin/main_test.go b/plugin/rpcplugin/main_test.go index 5a69971ac..6cdd46df0 100644 --- a/plugin/rpcplugin/main_test.go +++ b/plugin/rpcplugin/main_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/require" "github.com/mattermost/mattermost-server/plugin/plugintest" + "github.com/mattermost/mattermost-server/plugin/rpcplugin/rpcplugintest" ) func TestMain(t *testing.T) { @@ -19,7 +20,7 @@ func TestMain(t *testing.T) { defer os.RemoveAll(dir) plugin := filepath.Join(dir, "plugin.exe") - compileGo(t, ` + rpcplugintest.CompileGo(t, ` package main import ( diff --git a/plugin/rpcplugin/process_test.go b/plugin/rpcplugin/process_test.go index 473cc026b..8d1794293 100644 --- a/plugin/rpcplugin/process_test.go +++ b/plugin/rpcplugin/process_test.go @@ -4,25 +4,14 @@ import ( "context" "io/ioutil" "os" - "os/exec" "path/filepath" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" -) -func compileGo(t *testing.T, sourceCode, outputPath string) { - dir, err := ioutil.TempDir(".", "") - require.NoError(t, err) - defer os.RemoveAll(dir) - require.NoError(t, ioutil.WriteFile(filepath.Join(dir, "main.go"), []byte(sourceCode), 0600)) - cmd := exec.Command("go", "build", "-o", outputPath, "main.go") - cmd.Dir = dir - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - require.NoError(t, cmd.Run()) -} + "github.com/mattermost/mattermost-server/plugin/rpcplugin/rpcplugintest" +) func TestProcess(t *testing.T) { dir, err := ioutil.TempDir("", "") @@ -30,7 +19,7 @@ func TestProcess(t *testing.T) { defer os.RemoveAll(dir) ping := filepath.Join(dir, "ping.exe") - compileGo(t, ` + rpcplugintest.CompileGo(t, ` package main import ( diff --git a/plugin/rpcplugin/rpcplugintest/rpcplugintest.go b/plugin/rpcplugin/rpcplugintest/rpcplugintest.go new file mode 100644 index 000000000..185f741c1 --- /dev/null +++ b/plugin/rpcplugin/rpcplugintest/rpcplugintest.go @@ -0,0 +1,26 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package rpcplugintest + +import ( + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func CompileGo(t *testing.T, sourceCode, outputPath string) { + dir, err := ioutil.TempDir(".", "") + require.NoError(t, err) + defer os.RemoveAll(dir) + require.NoError(t, ioutil.WriteFile(filepath.Join(dir, "main.go"), []byte(sourceCode), 0600)) + cmd := exec.Command("go", "build", "-o", outputPath, "main.go") + cmd.Dir = dir + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + require.NoError(t, cmd.Run()) +} diff --git a/plugin/rpcplugin/rpcplugintest/supervisor.go b/plugin/rpcplugin/rpcplugintest/supervisor.go new file mode 100644 index 000000000..05dc8ed8f --- /dev/null +++ b/plugin/rpcplugin/rpcplugintest/supervisor.go @@ -0,0 +1,190 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package rpcplugintest + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/mattermost/mattermost-server/model" + "github.com/mattermost/mattermost-server/plugin" + "github.com/mattermost/mattermost-server/plugin/plugintest" +) + +type SupervisorProviderFunc = func(*model.BundleInfo) (plugin.Supervisor, error) + +func TestSupervisorProvider(t *testing.T, sp SupervisorProviderFunc) { + for name, f := range map[string]func(*testing.T, SupervisorProviderFunc){ + "Supervisor": testSupervisor, + "Supervisor_InvalidExecutablePath": testSupervisor_InvalidExecutablePath, + "Supervisor_NonExistentExecutablePath": testSupervisor_NonExistentExecutablePath, + "Supervisor_StartTimeout": testSupervisor_StartTimeout, + "Supervisor_PluginCrash": testSupervisor_PluginCrash, + } { + t.Run(name, func(t *testing.T) { f(t, sp) }) + } +} + +func testSupervisor(t *testing.T, sp SupervisorProviderFunc) { + dir, err := ioutil.TempDir("", "") + require.NoError(t, err) + defer os.RemoveAll(dir) + + backend := filepath.Join(dir, "backend.exe") + CompileGo(t, ` + package main + + import ( + "github.com/mattermost/mattermost-server/plugin/rpcplugin" + ) + + type MyPlugin struct {} + + func main() { + rpcplugin.Main(&MyPlugin{}) + } + `, backend) + + ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "backend.exe"}}`), 0600) + + bundle := model.BundleInfoForPath(dir) + supervisor, err := sp(bundle) + require.NoError(t, err) + require.NoError(t, supervisor.Start(nil)) + require.NoError(t, supervisor.Stop()) +} + +func testSupervisor_InvalidExecutablePath(t *testing.T, sp SupervisorProviderFunc) { + dir, err := ioutil.TempDir("", "") + require.NoError(t, err) + defer os.RemoveAll(dir) + + ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "/foo/../../backend.exe"}}`), 0600) + + bundle := model.BundleInfoForPath(dir) + supervisor, err := sp(bundle) + assert.Nil(t, supervisor) + assert.Error(t, err) +} + +func testSupervisor_NonExistentExecutablePath(t *testing.T, sp SupervisorProviderFunc) { + dir, err := ioutil.TempDir("", "") + require.NoError(t, err) + defer os.RemoveAll(dir) + + ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "thisfileshouldnotexist"}}`), 0600) + + bundle := model.BundleInfoForPath(dir) + supervisor, err := sp(bundle) + require.NotNil(t, supervisor) + require.NoError(t, err) + + require.Error(t, supervisor.Start(nil)) +} + +// If plugin development goes really wrong, let's make sure plugin activation won't block forever. +func testSupervisor_StartTimeout(t *testing.T, sp SupervisorProviderFunc) { + dir, err := ioutil.TempDir("", "") + require.NoError(t, err) + defer os.RemoveAll(dir) + + backend := filepath.Join(dir, "backend.exe") + CompileGo(t, ` + package main + + func main() { + for { + } + } + `, backend) + + ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "backend.exe"}}`), 0600) + + bundle := model.BundleInfoForPath(dir) + supervisor, err := sp(bundle) + require.NoError(t, err) + require.Error(t, supervisor.Start(nil)) +} + +// Crashed plugins should be relaunched. +func testSupervisor_PluginCrash(t *testing.T, sp SupervisorProviderFunc) { + dir, err := ioutil.TempDir("", "") + require.NoError(t, err) + defer os.RemoveAll(dir) + + backend := filepath.Join(dir, "backend.exe") + CompileGo(t, ` + package main + + import ( + "os" + + "github.com/mattermost/mattermost-server/plugin" + "github.com/mattermost/mattermost-server/plugin/rpcplugin" + ) + + type Configuration struct { + ShouldExit bool + } + + type MyPlugin struct { + config Configuration + } + + func (p *MyPlugin) OnActivate(api plugin.API) error { + api.LoadPluginConfiguration(&p.config) + return nil + } + + func (p *MyPlugin) OnDeactivate() error { + if p.config.ShouldExit { + os.Exit(1) + } + return nil + } + + func main() { + rpcplugin.Main(&MyPlugin{}) + } + `, backend) + + ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "backend.exe"}}`), 0600) + + var api plugintest.API + shouldExit := true + api.On("LoadPluginConfiguration", mock.MatchedBy(func(x interface{}) bool { return true })).Return(func(dest interface{}) error { + err := json.Unmarshal([]byte(fmt.Sprintf(`{"ShouldExit": %v}`, shouldExit)), dest) + shouldExit = false + return err + }) + + bundle := model.BundleInfoForPath(dir) + supervisor, err := sp(bundle) + require.NoError(t, err) + require.NoError(t, supervisor.Start(&api)) + + failed := false + recovered := false + for i := 0; i < 30; i++ { + if supervisor.Hooks().OnDeactivate() == nil { + require.True(t, failed) + recovered = true + break + } else { + failed = true + } + time.Sleep(time.Millisecond * 100) + } + assert.True(t, recovered) + require.NoError(t, supervisor.Stop()) +} diff --git a/plugin/rpcplugin/sandbox/sandbox.go b/plugin/rpcplugin/sandbox/sandbox.go new file mode 100644 index 000000000..96eff02dd --- /dev/null +++ b/plugin/rpcplugin/sandbox/sandbox.go @@ -0,0 +1,34 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package sandbox + +import ( + "context" + "io" + + "github.com/mattermost/mattermost-server/plugin/rpcplugin" +) + +type MountPoint struct { + Source string + Destination string + Type string + ReadOnly bool +} + +type Configuration struct { + MountPoints []*MountPoint + WorkingDirectory string +} + +// NewProcess is like rpcplugin.NewProcess, but launches the process in a sandbox. +func NewProcess(ctx context.Context, config *Configuration, path string) (rpcplugin.Process, io.ReadWriteCloser, error) { + return newProcess(ctx, config, path) +} + +// CheckSupport inspects the platform and environment to determine whether or not there are any +// expected issues with sandboxing. If nil is returned, sandboxing should be used. +func CheckSupport() error { + return checkSupport() +} diff --git a/plugin/rpcplugin/sandbox/sandbox_linux.go b/plugin/rpcplugin/sandbox/sandbox_linux.go new file mode 100644 index 000000000..d088652a7 --- /dev/null +++ b/plugin/rpcplugin/sandbox/sandbox_linux.go @@ -0,0 +1,468 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package sandbox + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "syscall" + "unsafe" + + "github.com/pkg/errors" + "golang.org/x/sys/unix" + + "github.com/mattermost/mattermost-server/plugin/rpcplugin" +) + +func init() { + if len(os.Args) < 3 || os.Args[0] != "sandbox.runProcess" { + return + } + + var config Configuration + if err := json.Unmarshal([]byte(os.Args[1]), &config); err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + if err := runProcess(&config, os.Args[2]); err != nil { + if eerr, ok := err.(*exec.ExitError); ok { + if status, ok := eerr.Sys().(syscall.WaitStatus); ok { + os.Exit(status.ExitStatus()) + } + } + fmt.Println(err.Error()) + os.Exit(1) + } + os.Exit(0) +} + +func systemMountPoints() (points []*MountPoint) { + points = append(points, &MountPoint{ + Source: "proc", + Destination: "/proc", + Type: "proc", + }, &MountPoint{ + Source: "/dev/null", + Destination: "/dev/null", + }, &MountPoint{ + Source: "/dev/zero", + Destination: "/dev/zero", + }, &MountPoint{ + Source: "/dev/full", + Destination: "/dev/full", + }) + + readOnly := []string{ + "/dev/random", + "/dev/urandom", + "/etc/resolv.conf", + "/lib", + "/lib32", + "/lib64", + "/etc/ssl/certs", + "/system/etc/security/cacerts", + "/usr/local/share/certs", + "/etc/pki/tls/certs", + "/etc/openssl/certs", + "/etc/ssl/ca-bundle.pem", + "/etc/pki/tls/cacert.pem", + "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem", + } + + for _, v := range []string{"SSL_CERT_FILE", "SSL_CERT_DIR"} { + if path := os.Getenv(v); path != "" { + readOnly = append(readOnly, path) + } + } + + for _, point := range readOnly { + points = append(points, &MountPoint{ + Source: point, + Destination: point, + ReadOnly: true, + }) + } + + return +} + +func runProcess(config *Configuration, path string) error { + root, err := ioutil.TempDir("", "") + if err != nil { + return err + } + defer os.RemoveAll(root) + + if err := syscall.Mount("", "/", "", syscall.MS_PRIVATE|syscall.MS_REC, ""); err != nil { + return errors.Wrapf(err, "unable to make root private") + } + + if err := mountMountPoints(root, systemMountPoints()); err != nil { + return errors.Wrapf(err, "unable to mount sandbox system mount points") + } + + if err := mountMountPoints(root, config.MountPoints); err != nil { + return errors.Wrapf(err, "unable to mount sandbox config mount points") + } + + if err := pivotRoot(root); err != nil { + return errors.Wrapf(err, "unable to pivot sandbox root") + } + + if err := os.Mkdir("/tmp", 0755); err != nil { + return errors.Wrapf(err, "unable to create /tmp") + } + + if config.WorkingDirectory != "" { + if err := os.Chdir(config.WorkingDirectory); err != nil { + return errors.Wrapf(err, "unable to set working directory") + } + } + + if err := dropInheritableCapabilities(); err != nil { + return errors.Wrapf(err, "unable to drop inheritable capabilities") + } + + if err := enableSeccompFilter(); err != nil { + return errors.Wrapf(err, "unable to enable seccomp filter") + } + + return runExecutable(path) +} + +func mountMountPoint(root string, mountPoint *MountPoint) error { + isDir := true + if mountPoint.Type == "" { + stat, err := os.Lstat(mountPoint.Source) + if err != nil { + return nil + } + if (stat.Mode() & os.ModeSymlink) != 0 { + if path, err := filepath.EvalSymlinks(mountPoint.Source); err == nil { + newMountPoint := *mountPoint + newMountPoint.Source = path + if err := mountMountPoint(root, &newMountPoint); err != nil { + return errors.Wrapf(err, "unable to mount symbolic link target: "+mountPoint.Source) + } + return nil + } + } + isDir = stat.IsDir() + } + + target := filepath.Join(root, mountPoint.Destination) + + if isDir { + if err := os.MkdirAll(target, 0755); err != nil { + return errors.Wrapf(err, "unable to create directory: "+target) + } + } else { + if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil { + return errors.Wrapf(err, "unable to create directory: "+target) + } + f, err := os.Create(target) + if err != nil { + return errors.Wrapf(err, "unable to create file: "+target) + } + f.Close() + } + + flags := uintptr(syscall.MS_NOSUID | syscall.MS_NODEV) + if mountPoint.Type == "" { + flags |= syscall.MS_BIND + } + if mountPoint.ReadOnly { + flags |= syscall.MS_RDONLY + } + + if err := syscall.Mount(mountPoint.Source, target, mountPoint.Type, flags, ""); err != nil { + return errors.Wrapf(err, "unable to mount "+mountPoint.Source) + } + + if (flags & syscall.MS_BIND) != 0 { + // If this was a bind mount, our other flags actually got silently ignored during the above syscall: + // + // If mountflags includes MS_BIND [...] The remaining bits in the mountflags argument are + // also ignored, with the exception of MS_REC. + // + // Furthermore, remounting will fail if we attempt to unset a bit that was inherited from + // the mount's parent: + // + // The mount(2) flags MS_RDONLY, MS_NOSUID, MS_NOEXEC, and the "atime" flags + // (MS_NOATIME, MS_NODIRATIME, MS_RELATIME) settings become locked when propagated from + // a more privileged to a less privileged mount namespace, and may not be changed in the + // less privileged mount namespace. + // + // So we need to get the actual flags, add our new ones, then do a remount if needed. + var stats syscall.Statfs_t + if err := syscall.Statfs(target, &stats); err != nil { + return errors.Wrap(err, "unable to get mount flags for target: "+target) + } + const lockedFlagsMask = unix.MS_RDONLY | unix.MS_NOSUID | unix.MS_NOEXEC | unix.MS_NOATIME | unix.MS_NODIRATIME | unix.MS_RELATIME + lockedFlags := uintptr(stats.Flags & lockedFlagsMask) + if lockedFlags != ((flags | lockedFlags) & lockedFlagsMask) { + if err := syscall.Mount("", target, "", flags|lockedFlags|syscall.MS_REMOUNT, ""); err != nil { + return errors.Wrapf(err, "unable to remount "+mountPoint.Source) + } + } + } + + return nil +} + +func mountMountPoints(root string, mountPoints []*MountPoint) error { + for _, mountPoint := range mountPoints { + if err := mountMountPoint(root, mountPoint); err != nil { + return err + } + } + + return nil +} + +func pivotRoot(newRoot string) error { + if err := syscall.Mount(newRoot, newRoot, "", syscall.MS_BIND|syscall.MS_REC, ""); err != nil { + return errors.Wrapf(err, "unable to mount new root") + } + + prevRoot := filepath.Join(newRoot, ".prev_root") + + if err := os.MkdirAll(prevRoot, 0700); err != nil { + return errors.Wrapf(err, "unable to create directory for previous root") + } + + if err := syscall.PivotRoot(newRoot, prevRoot); err != nil { + return errors.Wrapf(err, "syscall error") + } + + if err := os.Chdir("/"); err != nil { + return errors.Wrapf(err, "unable to change directory") + } + + prevRoot = "/.prev_root" + + if err := syscall.Unmount(prevRoot, syscall.MNT_DETACH); err != nil { + return errors.Wrapf(err, "unable to unmount previous root") + } + + if err := os.RemoveAll(prevRoot); err != nil { + return errors.Wrapf(err, "unable to remove previous root directory") + } + + return nil +} + +func dropInheritableCapabilities() error { + type capHeader struct { + version uint32 + pid int + } + + type capData struct { + effective uint32 + permitted uint32 + inheritable uint32 + } + + var hdr capHeader + var data [2]capData + + if _, _, errno := syscall.Syscall(syscall.SYS_CAPGET, uintptr(unsafe.Pointer(&hdr)), 0, 0); errno != 0 { + return errors.Wrapf(syscall.Errno(errno), "unable to get capabilities version") + } + + if _, _, errno := syscall.Syscall(syscall.SYS_CAPGET, uintptr(unsafe.Pointer(&hdr)), uintptr(unsafe.Pointer(&data[0])), 0); errno != 0 { + return errors.Wrapf(syscall.Errno(errno), "unable to get capabilities") + } + + data[0].inheritable = 0 + data[1].inheritable = 0 + if _, _, errno := syscall.Syscall(syscall.SYS_CAPSET, uintptr(unsafe.Pointer(&hdr)), uintptr(unsafe.Pointer(&data[0])), 0); errno != 0 { + return errors.Wrapf(syscall.Errno(errno), "unable to set inheritable capabilities") + } + + for i := 0; i < 64; i++ { + if _, _, errno := syscall.Syscall(syscall.SYS_PRCTL, syscall.PR_CAPBSET_DROP, uintptr(i), 0); errno != 0 && errno != syscall.EINVAL { + return errors.Wrapf(syscall.Errno(errno), "unable to drop bounding set capability") + } + } + + return nil +} + +func enableSeccompFilter() error { + return EnableSeccompFilter(SeccompFilter(NATIVE_AUDIT_ARCH, AllowedSyscalls)) +} + +func runExecutable(path string) error { + childFiles := []*os.File{ + os.NewFile(3, ""), os.NewFile(4, ""), + } + defer childFiles[0].Close() + defer childFiles[1].Close() + + cmd := exec.Command(path) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.ExtraFiles = childFiles + cmd.SysProcAttr = &syscall.SysProcAttr{ + Pdeathsig: syscall.SIGTERM, + } + + if err := cmd.Run(); err != nil { + return err + } + + return nil +} + +type process struct { + command *exec.Cmd +} + +func newProcess(ctx context.Context, config *Configuration, path string) (rpcplugin.Process, io.ReadWriteCloser, error) { + configJSON, err := json.Marshal(config) + if err != nil { + return nil, nil, err + } + + ipc, childFiles, err := rpcplugin.NewIPC() + if err != nil { + return nil, nil, err + } + defer childFiles[0].Close() + defer childFiles[1].Close() + + cmd := exec.CommandContext(ctx, "/proc/self/exe") + cmd.Args = []string{"sandbox.runProcess", string(configJSON), path} + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.ExtraFiles = childFiles + + cmd.SysProcAttr = &syscall.SysProcAttr{ + Cloneflags: syscall.CLONE_NEWNS | syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWUSER, + Pdeathsig: syscall.SIGTERM, + GidMappings: []syscall.SysProcIDMap{ + { + ContainerID: 0, + HostID: os.Getgid(), + Size: 1, + }, + }, + UidMappings: []syscall.SysProcIDMap{ + { + ContainerID: 0, + HostID: os.Getuid(), + Size: 1, + }, + }, + } + + err = cmd.Start() + if err != nil { + ipc.Close() + return nil, nil, err + } + + return &process{ + command: cmd, + }, ipc, nil +} + +func (p *process) Wait() error { + return p.command.Wait() +} + +func init() { + if len(os.Args) < 1 || os.Args[0] != "sandbox.checkSupportInNamespace" { + return + } + + if err := checkSupportInNamespace(); err != nil { + fmt.Fprintf(os.Stderr, "%v", err.Error()) + os.Exit(1) + } + + os.Exit(0) +} + +func checkSupportInNamespace() error { + root, err := ioutil.TempDir("", "") + if err != nil { + return err + } + defer os.RemoveAll(root) + + if err := syscall.Mount("", "/", "", syscall.MS_PRIVATE|syscall.MS_REC, ""); err != nil { + return errors.Wrapf(err, "unable to make root private") + } + + if err := mountMountPoints(root, systemMountPoints()); err != nil { + return errors.Wrapf(err, "unable to mount sandbox system mount points") + } + + if err := pivotRoot(root); err != nil { + return errors.Wrapf(err, "unable to pivot sandbox root") + } + + if err := dropInheritableCapabilities(); err != nil { + return errors.Wrapf(err, "unable to drop inheritable capabilities") + } + + if err := enableSeccompFilter(); err != nil { + return errors.Wrapf(err, "unable to enable seccomp filter") + } + + return nil +} + +func checkSupport() error { + if AllowedSyscalls == nil { + return fmt.Errorf("unsupported architecture") + } + + stderr := &bytes.Buffer{} + + cmd := exec.Command("/proc/self/exe") + cmd.Args = []string{"sandbox.checkSupportInNamespace"} + cmd.Stderr = stderr + cmd.SysProcAttr = &syscall.SysProcAttr{ + Cloneflags: syscall.CLONE_NEWNS | syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWUSER, + Pdeathsig: syscall.SIGTERM, + GidMappings: []syscall.SysProcIDMap{ + { + ContainerID: 0, + HostID: os.Getgid(), + Size: 1, + }, + }, + UidMappings: []syscall.SysProcIDMap{ + { + ContainerID: 0, + HostID: os.Getuid(), + Size: 1, + }, + }, + } + + if err := cmd.Start(); err != nil { + return errors.Wrapf(err, "unable to create user namespace") + } + + if err := cmd.Wait(); err != nil { + if _, ok := err.(*exec.ExitError); ok { + return errors.Wrapf(fmt.Errorf("%v", stderr.String()), "unable to prepare namespace") + } + return errors.Wrapf(err, "unable to prepare namespace") + } + + return nil +} diff --git a/plugin/rpcplugin/sandbox/sandbox_linux_test.go b/plugin/rpcplugin/sandbox/sandbox_linux_test.go new file mode 100644 index 000000000..2bcbf0c57 --- /dev/null +++ b/plugin/rpcplugin/sandbox/sandbox_linux_test.go @@ -0,0 +1,159 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package sandbox + +import ( + "context" + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/mattermost/mattermost-server/plugin/rpcplugin/rpcplugintest" +) + +func TestNewProcess(t *testing.T) { + if err := CheckSupport(); err != nil { + t.Skip("sandboxing not supported:", err) + } + + dir, err := ioutil.TempDir("", "") + require.NoError(t, err) + defer os.RemoveAll(dir) + + ping := filepath.Join(dir, "ping.exe") + rpcplugintest.CompileGo(t, ` + package main + + import ( + "crypto/rand" + "fmt" + "io/ioutil" + "net/http" + "os" + "os/exec" + "syscall" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/mattermost/mattermost-server/plugin/rpcplugin" + ) + + var failures int + + type T struct {} + func (T) Errorf(format string, args ...interface{}) { + fmt.Printf(format, args...) + failures++ + } + func (T) FailNow() { + os.Exit(1) + } + + func init() { + if len(os.Args) > 0 && os.Args[0] == "exitImmediately" { + os.Exit(0) + } + } + + func main() { + t := &T{} + + pwd, err := os.Getwd() + assert.NoError(t, err) + assert.Equal(t, "/dir", pwd) + + assert.Equal(t, 0, os.Getgid(), "we should see ourselves as root") + assert.Equal(t, 0, os.Getuid(), "we should see ourselves as root") + + f, err := ioutil.TempFile("", "") + require.NoError(t, err, "we should be able to create temporary files") + f.Close() + + _, err = os.Stat("ping.exe") + assert.NoError(t, err, "we should be able to read files in the working directory") + + buf := make([]byte, 20) + n, err := rand.Read(buf) + assert.Equal(t, 20, n) + assert.NoError(t, err, "we should be able to read from /dev/urandom") + + f, err = os.Create("/dev/zero") + require.NoError(t, err, "we should be able to write to /dev/zero") + defer f.Close() + n, err = f.Write([]byte("foo")) + assert.Equal(t, 3, n) + require.NoError(t, err, "we should be able to write to /dev/zero") + + f, err = os.Create("/dir/foo") + if f != nil { + defer f.Close() + } + assert.Error(t, err, "we shouldn't be able to write to this read-only mount point") + + _, err = ioutil.ReadFile("/etc/resolv.conf") + require.NoError(t, err, "we should be able to read /etc/resolv.conf") + + resp, err := http.Get("https://github.com") + require.NoError(t, err, "we should be able to use the network") + resp.Body.Close() + + status, err := ioutil.ReadFile("/proc/self/status") + require.NoError(t, err, "we should be able to read from /proc") + assert.Regexp(t, status, "CapEff:\\s+0000000000000000", "we should have no effective capabilities") + + require.NoError(t, os.MkdirAll("/tmp/dir2", 0755)) + err = syscall.Mount("/dir", "/tmp/dir2", "", syscall.MS_BIND, "") + assert.Equal(t, syscall.EPERM, err, "we shouldn't be allowed to mount things") + + cmd := exec.Command("/proc/self/exe") + cmd.Args = []string{"exitImmediately"} + cmd.SysProcAttr = &syscall.SysProcAttr{ + Pdeathsig: syscall.SIGTERM, + } + assert.NoError(t, cmd.Run(), "we should be able to re-exec ourself") + + cmd = exec.Command("/proc/self/exe") + cmd.Args = []string{"exitImmediately"} + cmd.SysProcAttr = &syscall.SysProcAttr{ + Cloneflags: syscall.CLONE_NEWNS | syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWUSER, + Pdeathsig: syscall.SIGTERM, + } + assert.Error(t, cmd.Run(), "we shouldn't be able to create new namespaces anymore") + + ipc, err := rpcplugin.InheritedProcessIPC() + require.NoError(t, err) + defer ipc.Close() + _, err = ipc.Write([]byte("ping")) + require.NoError(t, err) + + if failures > 0 { + os.Exit(1) + } + } + `, ping) + + p, ipc, err := NewProcess(context.Background(), &Configuration{ + MountPoints: []*MountPoint{ + { + Source: dir, + Destination: "/dir", + ReadOnly: true, + }, + }, + WorkingDirectory: "/dir", + }, "/dir/ping.exe") + require.NoError(t, err) + defer ipc.Close() + b := make([]byte, 10) + n, err := ipc.Read(b) + require.NoError(t, err) + assert.Equal(t, 4, n) + assert.Equal(t, "ping", string(b[:4])) + require.NoError(t, p.Wait()) +} diff --git a/plugin/rpcplugin/sandbox/sandbox_other.go b/plugin/rpcplugin/sandbox/sandbox_other.go new file mode 100644 index 000000000..3889ecdcc --- /dev/null +++ b/plugin/rpcplugin/sandbox/sandbox_other.go @@ -0,0 +1,22 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +// +build !linux + +package sandbox + +import ( + "context" + "fmt" + "io" + + "github.com/mattermost/mattermost-server/plugin/rpcplugin" +) + +func newProcess(ctx context.Context, config *Configuration, path string) (rpcplugin.Process, io.ReadWriteCloser, error) { + return nil, nil, checkSupport() +} + +func checkSupport() error { + return fmt.Errorf("sandboxing is not supported on this platform") +} diff --git a/plugin/rpcplugin/sandbox/sandbox_test.go b/plugin/rpcplugin/sandbox/sandbox_test.go new file mode 100644 index 000000000..e0149e28d --- /dev/null +++ b/plugin/rpcplugin/sandbox/sandbox_test.go @@ -0,0 +1,25 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package sandbox + +import ( + "testing" +) + +// TestCheckSupport is here for debugging purposes and has no assertions. You can quickly test +// sandboxing support with various systems by compiling the test executable and running this test on +// your target systems. For example, with docker, executed from the root of the repo: +// +// docker run --rm -it -w /go/src/github.com/mattermost/mattermost-server +// -v $(pwd):/go/src/github.com/mattermost/mattermost-server golang:1.9 +// go test -c ./plugin/rpcplugin +// +// docker run --rm -it --privileged -w /opt/mattermost +// -v $(pwd):/opt/mattermost centos:6 +// ./rpcplugin.test --test.v --test.run TestCheckSupport +func TestCheckSupport(t *testing.T) { + if err := CheckSupport(); err != nil { + t.Log(err.Error()) + } +} diff --git a/plugin/rpcplugin/sandbox/seccomp_linux.go b/plugin/rpcplugin/sandbox/seccomp_linux.go new file mode 100644 index 000000000..afe86e90a --- /dev/null +++ b/plugin/rpcplugin/sandbox/seccomp_linux.go @@ -0,0 +1,178 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package sandbox + +import ( + "syscall" + "unsafe" + + "github.com/pkg/errors" + "golang.org/x/net/bpf" + "golang.org/x/sys/unix" +) + +const ( + SECCOMP_RET_ALLOW = 0x7fff0000 + SECCOMP_RET_ERRNO = 0x00050000 +) + +const ( + EM_X86_64 = 62 + + __AUDIT_ARCH_64BIT = 0x80000000 + __AUDIT_ARCH_LE = 0x40000000 + + AUDIT_ARCH_X86_64 = EM_X86_64 | __AUDIT_ARCH_64BIT | __AUDIT_ARCH_LE + + nrSize = 4 + archOffset = nrSize + ipOffset = archOffset + 4 + argsOffset = ipOffset + 8 +) + +type SeccompCondition interface { + Filter(littleEndian bool, skipFalseSentinel uint8) []bpf.Instruction +} + +func seccompArgLowWord(arg int, littleEndian bool) uint32 { + offset := uint32(argsOffset + arg*8) + if !littleEndian { + offset += 4 + } + return offset +} + +func seccompArgHighWord(arg int, littleEndian bool) uint32 { + offset := uint32(argsOffset + arg*8) + if littleEndian { + offset += 4 + } + return offset +} + +type SeccompArgHasNoBits struct { + Arg int + Mask uint64 +} + +func (c SeccompArgHasNoBits) Filter(littleEndian bool, skipFalseSentinel uint8) []bpf.Instruction { + return []bpf.Instruction{ + bpf.LoadAbsolute{Off: seccompArgHighWord(c.Arg, littleEndian), Size: 4}, + bpf.JumpIf{Cond: bpf.JumpBitsSet, Val: uint32(c.Mask >> 32), SkipTrue: skipFalseSentinel}, + bpf.LoadAbsolute{Off: seccompArgLowWord(c.Arg, littleEndian), Size: 4}, + bpf.JumpIf{Cond: bpf.JumpBitsSet, Val: uint32(c.Mask), SkipTrue: skipFalseSentinel}, + } +} + +type SeccompArgHasAnyBit struct { + Arg int + Mask uint64 +} + +func (c SeccompArgHasAnyBit) Filter(littleEndian bool, skipFalseSentinel uint8) []bpf.Instruction { + return []bpf.Instruction{ + bpf.LoadAbsolute{Off: seccompArgHighWord(c.Arg, littleEndian), Size: 4}, + bpf.JumpIf{Cond: bpf.JumpBitsSet, Val: uint32(c.Mask >> 32), SkipTrue: 2}, + bpf.LoadAbsolute{Off: seccompArgLowWord(c.Arg, littleEndian), Size: 4}, + bpf.JumpIf{Cond: bpf.JumpBitsSet, Val: uint32(c.Mask), SkipFalse: skipFalseSentinel}, + } +} + +type SeccompArgEquals struct { + Arg int + Value uint64 +} + +func (c SeccompArgEquals) Filter(littleEndian bool, skipFalseSentinel uint8) []bpf.Instruction { + return []bpf.Instruction{ + bpf.LoadAbsolute{Off: seccompArgHighWord(c.Arg, littleEndian), Size: 4}, + bpf.JumpIf{Cond: bpf.JumpEqual, Val: uint32(c.Value >> 32), SkipFalse: skipFalseSentinel}, + bpf.LoadAbsolute{Off: seccompArgLowWord(c.Arg, littleEndian), Size: 4}, + bpf.JumpIf{Cond: bpf.JumpEqual, Val: uint32(c.Value), SkipFalse: skipFalseSentinel}, + } +} + +type SeccompConditions struct { + All []SeccompCondition +} + +type SeccompSyscall struct { + Syscall uint32 + Any []SeccompConditions +} + +func SeccompFilter(arch uint32, allowedSyscalls []SeccompSyscall) (filter []bpf.Instruction) { + filter = append(filter, + bpf.LoadAbsolute{Off: archOffset, Size: 4}, + bpf.JumpIf{Cond: bpf.JumpEqual, Val: arch, SkipTrue: 1}, + bpf.RetConstant{Val: uint32(SECCOMP_RET_ERRNO | unix.EPERM)}, + ) + + filter = append(filter, bpf.LoadAbsolute{Off: 0, Size: nrSize}) + for _, s := range allowedSyscalls { + if s.Any != nil { + syscallStart := len(filter) + filter = append(filter, bpf.Instruction(nil)) + for _, cs := range s.Any { + anyStart := len(filter) + for _, c := range cs.All { + filter = append(filter, c.Filter((arch&__AUDIT_ARCH_LE) != 0, 255)...) + } + filter = append(filter, bpf.RetConstant{Val: SECCOMP_RET_ALLOW}) + for i := anyStart; i < len(filter); i++ { + if jump, ok := filter[i].(bpf.JumpIf); ok { + if len(filter)-i-1 > 255 { + panic("condition too long") + } + if jump.SkipFalse == 255 { + jump.SkipFalse = uint8(len(filter) - i - 1) + } + if jump.SkipTrue == 255 { + jump.SkipTrue = uint8(len(filter) - i - 1) + } + filter[i] = jump + } + } + } + filter = append(filter, bpf.RetConstant{Val: uint32(SECCOMP_RET_ERRNO | unix.EPERM)}) + if len(filter)-syscallStart-1 > 255 { + panic("conditions too long") + } + filter[syscallStart] = bpf.JumpIf{Cond: bpf.JumpEqual, Val: uint32(s.Syscall), SkipFalse: uint8(len(filter) - syscallStart - 1)} + } else { + filter = append(filter, + bpf.JumpIf{Cond: bpf.JumpEqual, Val: uint32(s.Syscall), SkipFalse: 1}, + bpf.RetConstant{Val: SECCOMP_RET_ALLOW}, + ) + } + } + + return append(filter, bpf.RetConstant{Val: uint32(SECCOMP_RET_ERRNO | unix.EPERM)}) +} + +func EnableSeccompFilter(filter []bpf.Instruction) error { + assembled, err := bpf.Assemble(filter) + if err != nil { + return errors.Wrapf(err, "unable to assemble filter") + } + + sockFilter := make([]unix.SockFilter, len(filter)) + for i, instruction := range assembled { + sockFilter[i].Code = instruction.Op + sockFilter[i].Jt = instruction.Jt + sockFilter[i].Jf = instruction.Jf + sockFilter[i].K = instruction.K + } + + prog := unix.SockFprog{ + Len: uint16(len(sockFilter)), + Filter: &sockFilter[0], + } + + if _, _, errno := syscall.Syscall(syscall.SYS_PRCTL, unix.PR_SET_SECCOMP, unix.SECCOMP_MODE_FILTER, uintptr(unsafe.Pointer(&prog))); errno != 0 { + return errors.Wrapf(syscall.Errno(errno), "syscall error") + } + + return nil +} diff --git a/plugin/rpcplugin/sandbox/seccomp_linux_amd64.go b/plugin/rpcplugin/sandbox/seccomp_linux_amd64.go new file mode 100644 index 000000000..7338ebbe0 --- /dev/null +++ b/plugin/rpcplugin/sandbox/seccomp_linux_amd64.go @@ -0,0 +1,301 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package sandbox + +import ( + "golang.org/x/sys/unix" +) + +const NATIVE_AUDIT_ARCH = AUDIT_ARCH_X86_64 + +var AllowedSyscalls = []SeccompSyscall{ + {Syscall: unix.SYS_ACCEPT}, + {Syscall: unix.SYS_ACCEPT4}, + {Syscall: unix.SYS_ACCESS}, + {Syscall: unix.SYS_ADJTIMEX}, + {Syscall: unix.SYS_ALARM}, + {Syscall: unix.SYS_ARCH_PRCTL}, + {Syscall: unix.SYS_BIND}, + {Syscall: unix.SYS_BRK}, + {Syscall: unix.SYS_CAPGET}, + {Syscall: unix.SYS_CAPSET}, + {Syscall: unix.SYS_CHDIR}, + {Syscall: unix.SYS_CHMOD}, + {Syscall: unix.SYS_CHOWN}, + {Syscall: unix.SYS_CLOCK_GETRES}, + {Syscall: unix.SYS_CLOCK_GETTIME}, + {Syscall: unix.SYS_CLOCK_NANOSLEEP}, + { + Syscall: unix.SYS_CLONE, + Any: []SeccompConditions{{ + All: []SeccompCondition{SeccompArgHasNoBits{ + Arg: 0, + Mask: unix.CLONE_NEWCGROUP | unix.CLONE_NEWIPC | unix.CLONE_NEWNET | unix.CLONE_NEWNS | unix.CLONE_NEWPID | unix.CLONE_NEWUSER | unix.CLONE_NEWUTS, + }}, + }}, + }, + {Syscall: unix.SYS_CLOSE}, + {Syscall: unix.SYS_CONNECT}, + {Syscall: unix.SYS_COPY_FILE_RANGE}, + {Syscall: unix.SYS_CREAT}, + {Syscall: unix.SYS_DUP}, + {Syscall: unix.SYS_DUP2}, + {Syscall: unix.SYS_DUP3}, + {Syscall: unix.SYS_EPOLL_CREATE}, + {Syscall: unix.SYS_EPOLL_CREATE1}, + {Syscall: unix.SYS_EPOLL_CTL}, + {Syscall: unix.SYS_EPOLL_CTL_OLD}, + {Syscall: unix.SYS_EPOLL_PWAIT}, + {Syscall: unix.SYS_EPOLL_WAIT}, + {Syscall: unix.SYS_EPOLL_WAIT_OLD}, + {Syscall: unix.SYS_EVENTFD}, + {Syscall: unix.SYS_EVENTFD2}, + {Syscall: unix.SYS_EXECVE}, + {Syscall: unix.SYS_EXECVEAT}, + {Syscall: unix.SYS_EXIT}, + {Syscall: unix.SYS_EXIT_GROUP}, + {Syscall: unix.SYS_FACCESSAT}, + {Syscall: unix.SYS_FADVISE64}, + {Syscall: unix.SYS_FALLOCATE}, + {Syscall: unix.SYS_FANOTIFY_MARK}, + {Syscall: unix.SYS_FCHDIR}, + {Syscall: unix.SYS_FCHMOD}, + {Syscall: unix.SYS_FCHMODAT}, + {Syscall: unix.SYS_FCHOWN}, + {Syscall: unix.SYS_FCHOWNAT}, + {Syscall: unix.SYS_FCNTL}, + {Syscall: unix.SYS_FDATASYNC}, + {Syscall: unix.SYS_FGETXATTR}, + {Syscall: unix.SYS_FLISTXATTR}, + {Syscall: unix.SYS_FLOCK}, + {Syscall: unix.SYS_FORK}, + {Syscall: unix.SYS_FREMOVEXATTR}, + {Syscall: unix.SYS_FSETXATTR}, + {Syscall: unix.SYS_FSTAT}, + {Syscall: unix.SYS_FSTATFS}, + {Syscall: unix.SYS_FSYNC}, + {Syscall: unix.SYS_FTRUNCATE}, + {Syscall: unix.SYS_FUTEX}, + {Syscall: unix.SYS_FUTIMESAT}, + {Syscall: unix.SYS_GETCPU}, + {Syscall: unix.SYS_GETCWD}, + {Syscall: unix.SYS_GETDENTS}, + {Syscall: unix.SYS_GETDENTS64}, + {Syscall: unix.SYS_GETEGID}, + {Syscall: unix.SYS_GETEUID}, + {Syscall: unix.SYS_GETGID}, + {Syscall: unix.SYS_GETGROUPS}, + {Syscall: unix.SYS_GETITIMER}, + {Syscall: unix.SYS_GETPEERNAME}, + {Syscall: unix.SYS_GETPGID}, + {Syscall: unix.SYS_GETPGRP}, + {Syscall: unix.SYS_GETPID}, + {Syscall: unix.SYS_GETPPID}, + {Syscall: unix.SYS_GETPRIORITY}, + {Syscall: unix.SYS_GETRANDOM}, + {Syscall: unix.SYS_GETRESGID}, + {Syscall: unix.SYS_GETRESUID}, + {Syscall: unix.SYS_GETRLIMIT}, + {Syscall: unix.SYS_GET_ROBUST_LIST}, + {Syscall: unix.SYS_GETRUSAGE}, + {Syscall: unix.SYS_GETSID}, + {Syscall: unix.SYS_GETSOCKNAME}, + {Syscall: unix.SYS_GETSOCKOPT}, + {Syscall: unix.SYS_GET_THREAD_AREA}, + {Syscall: unix.SYS_GETTID}, + {Syscall: unix.SYS_GETTIMEOFDAY}, + {Syscall: unix.SYS_GETUID}, + {Syscall: unix.SYS_GETXATTR}, + {Syscall: unix.SYS_INOTIFY_ADD_WATCH}, + {Syscall: unix.SYS_INOTIFY_INIT}, + {Syscall: unix.SYS_INOTIFY_INIT1}, + {Syscall: unix.SYS_INOTIFY_RM_WATCH}, + {Syscall: unix.SYS_IO_CANCEL}, + {Syscall: unix.SYS_IOCTL}, + {Syscall: unix.SYS_IO_DESTROY}, + {Syscall: unix.SYS_IO_GETEVENTS}, + {Syscall: unix.SYS_IOPRIO_GET}, + {Syscall: unix.SYS_IOPRIO_SET}, + {Syscall: unix.SYS_IO_SETUP}, + {Syscall: unix.SYS_IO_SUBMIT}, + {Syscall: unix.SYS_KILL}, + {Syscall: unix.SYS_LCHOWN}, + {Syscall: unix.SYS_LGETXATTR}, + {Syscall: unix.SYS_LINK}, + {Syscall: unix.SYS_LINKAT}, + {Syscall: unix.SYS_LISTEN}, + {Syscall: unix.SYS_LISTXATTR}, + {Syscall: unix.SYS_LLISTXATTR}, + {Syscall: unix.SYS_LREMOVEXATTR}, + {Syscall: unix.SYS_LSEEK}, + {Syscall: unix.SYS_LSETXATTR}, + {Syscall: unix.SYS_LSTAT}, + {Syscall: unix.SYS_MADVISE}, + {Syscall: unix.SYS_MEMFD_CREATE}, + {Syscall: unix.SYS_MINCORE}, + {Syscall: unix.SYS_MKDIR}, + {Syscall: unix.SYS_MKDIRAT}, + {Syscall: unix.SYS_MKNOD}, + {Syscall: unix.SYS_MKNODAT}, + {Syscall: unix.SYS_MLOCK}, + {Syscall: unix.SYS_MLOCK2}, + {Syscall: unix.SYS_MLOCKALL}, + {Syscall: unix.SYS_MMAP}, + {Syscall: unix.SYS_MODIFY_LDT}, + {Syscall: unix.SYS_MPROTECT}, + {Syscall: unix.SYS_MQ_GETSETATTR}, + {Syscall: unix.SYS_MQ_NOTIFY}, + {Syscall: unix.SYS_MQ_OPEN}, + {Syscall: unix.SYS_MQ_TIMEDRECEIVE}, + {Syscall: unix.SYS_MQ_TIMEDSEND}, + {Syscall: unix.SYS_MQ_UNLINK}, + {Syscall: unix.SYS_MREMAP}, + {Syscall: unix.SYS_MSGCTL}, + {Syscall: unix.SYS_MSGGET}, + {Syscall: unix.SYS_MSGRCV}, + {Syscall: unix.SYS_MSGSND}, + {Syscall: unix.SYS_MSYNC}, + {Syscall: unix.SYS_MUNLOCK}, + {Syscall: unix.SYS_MUNLOCKALL}, + {Syscall: unix.SYS_MUNMAP}, + {Syscall: unix.SYS_NANOSLEEP}, + {Syscall: unix.SYS_NEWFSTATAT}, + {Syscall: unix.SYS_OPEN}, + {Syscall: unix.SYS_OPENAT}, + {Syscall: unix.SYS_PAUSE}, + { + Syscall: unix.SYS_PERSONALITY, + Any: []SeccompConditions{ + {All: []SeccompCondition{SeccompArgEquals{Arg: 0, Value: 0}}}, + {All: []SeccompCondition{SeccompArgEquals{Arg: 0, Value: 8}}}, + {All: []SeccompCondition{SeccompArgEquals{Arg: 0, Value: 0x20000}}}, + {All: []SeccompCondition{SeccompArgEquals{Arg: 0, Value: 0x20008}}}, + {All: []SeccompCondition{SeccompArgEquals{Arg: 0, Value: 0xffffffff}}}, + }, + }, + {Syscall: unix.SYS_PIPE}, + {Syscall: unix.SYS_PIPE2}, + {Syscall: unix.SYS_POLL}, + {Syscall: unix.SYS_PPOLL}, + {Syscall: unix.SYS_PRCTL}, + {Syscall: unix.SYS_PREAD64}, + {Syscall: unix.SYS_PREADV}, + {Syscall: unix.SYS_PREADV2}, + {Syscall: unix.SYS_PRLIMIT64}, + {Syscall: unix.SYS_PSELECT6}, + {Syscall: unix.SYS_PWRITE64}, + {Syscall: unix.SYS_PWRITEV}, + {Syscall: unix.SYS_PWRITEV2}, + {Syscall: unix.SYS_READ}, + {Syscall: unix.SYS_READAHEAD}, + {Syscall: unix.SYS_READLINK}, + {Syscall: unix.SYS_READLINKAT}, + {Syscall: unix.SYS_READV}, + {Syscall: unix.SYS_RECVFROM}, + {Syscall: unix.SYS_RECVMMSG}, + {Syscall: unix.SYS_RECVMSG}, + {Syscall: unix.SYS_REMAP_FILE_PAGES}, + {Syscall: unix.SYS_REMOVEXATTR}, + {Syscall: unix.SYS_RENAME}, + {Syscall: unix.SYS_RENAMEAT}, + {Syscall: unix.SYS_RENAMEAT2}, + {Syscall: unix.SYS_RESTART_SYSCALL}, + {Syscall: unix.SYS_RMDIR}, + {Syscall: unix.SYS_RT_SIGACTION}, + {Syscall: unix.SYS_RT_SIGPENDING}, + {Syscall: unix.SYS_RT_SIGPROCMASK}, + {Syscall: unix.SYS_RT_SIGQUEUEINFO}, + {Syscall: unix.SYS_RT_SIGRETURN}, + {Syscall: unix.SYS_RT_SIGSUSPEND}, + {Syscall: unix.SYS_RT_SIGTIMEDWAIT}, + {Syscall: unix.SYS_RT_TGSIGQUEUEINFO}, + {Syscall: unix.SYS_SCHED_GETAFFINITY}, + {Syscall: unix.SYS_SCHED_GETATTR}, + {Syscall: unix.SYS_SCHED_GETPARAM}, + {Syscall: unix.SYS_SCHED_GET_PRIORITY_MAX}, + {Syscall: unix.SYS_SCHED_GET_PRIORITY_MIN}, + {Syscall: unix.SYS_SCHED_GETSCHEDULER}, + {Syscall: unix.SYS_SCHED_RR_GET_INTERVAL}, + {Syscall: unix.SYS_SCHED_SETAFFINITY}, + {Syscall: unix.SYS_SCHED_SETATTR}, + {Syscall: unix.SYS_SCHED_SETPARAM}, + {Syscall: unix.SYS_SCHED_SETSCHEDULER}, + {Syscall: unix.SYS_SCHED_YIELD}, + {Syscall: unix.SYS_SECCOMP}, + {Syscall: unix.SYS_SELECT}, + {Syscall: unix.SYS_SEMCTL}, + {Syscall: unix.SYS_SEMGET}, + {Syscall: unix.SYS_SEMOP}, + {Syscall: unix.SYS_SEMTIMEDOP}, + {Syscall: unix.SYS_SENDFILE}, + {Syscall: unix.SYS_SENDMMSG}, + {Syscall: unix.SYS_SENDMSG}, + {Syscall: unix.SYS_SENDTO}, + {Syscall: unix.SYS_SETFSGID}, + {Syscall: unix.SYS_SETFSUID}, + {Syscall: unix.SYS_SETGID}, + {Syscall: unix.SYS_SETGROUPS}, + {Syscall: unix.SYS_SETITIMER}, + {Syscall: unix.SYS_SETPGID}, + {Syscall: unix.SYS_SETPRIORITY}, + {Syscall: unix.SYS_SETREGID}, + {Syscall: unix.SYS_SETRESGID}, + {Syscall: unix.SYS_SETRESUID}, + {Syscall: unix.SYS_SETREUID}, + {Syscall: unix.SYS_SETRLIMIT}, + {Syscall: unix.SYS_SET_ROBUST_LIST}, + {Syscall: unix.SYS_SETSID}, + {Syscall: unix.SYS_SETSOCKOPT}, + {Syscall: unix.SYS_SET_THREAD_AREA}, + {Syscall: unix.SYS_SET_TID_ADDRESS}, + {Syscall: unix.SYS_SETUID}, + {Syscall: unix.SYS_SETXATTR}, + {Syscall: unix.SYS_SHMAT}, + {Syscall: unix.SYS_SHMCTL}, + {Syscall: unix.SYS_SHMDT}, + {Syscall: unix.SYS_SHMGET}, + {Syscall: unix.SYS_SHUTDOWN}, + {Syscall: unix.SYS_SIGALTSTACK}, + {Syscall: unix.SYS_SIGNALFD}, + {Syscall: unix.SYS_SIGNALFD4}, + {Syscall: unix.SYS_SOCKET}, + {Syscall: unix.SYS_SOCKETPAIR}, + {Syscall: unix.SYS_SPLICE}, + {Syscall: unix.SYS_STAT}, + {Syscall: unix.SYS_STATFS}, + {Syscall: unix.SYS_SYMLINK}, + {Syscall: unix.SYS_SYMLINKAT}, + {Syscall: unix.SYS_SYNC}, + {Syscall: unix.SYS_SYNC_FILE_RANGE}, + {Syscall: unix.SYS_SYNCFS}, + {Syscall: unix.SYS_SYSINFO}, + {Syscall: unix.SYS_SYSLOG}, + {Syscall: unix.SYS_TEE}, + {Syscall: unix.SYS_TGKILL}, + {Syscall: unix.SYS_TIME}, + {Syscall: unix.SYS_TIMER_CREATE}, + {Syscall: unix.SYS_TIMER_DELETE}, + {Syscall: unix.SYS_TIMERFD_CREATE}, + {Syscall: unix.SYS_TIMERFD_GETTIME}, + {Syscall: unix.SYS_TIMERFD_SETTIME}, + {Syscall: unix.SYS_TIMER_GETOVERRUN}, + {Syscall: unix.SYS_TIMER_GETTIME}, + {Syscall: unix.SYS_TIMER_SETTIME}, + {Syscall: unix.SYS_TIMES}, + {Syscall: unix.SYS_TKILL}, + {Syscall: unix.SYS_TRUNCATE}, + {Syscall: unix.SYS_UMASK}, + {Syscall: unix.SYS_UNAME}, + {Syscall: unix.SYS_UNLINK}, + {Syscall: unix.SYS_UNLINKAT}, + {Syscall: unix.SYS_UTIME}, + {Syscall: unix.SYS_UTIMENSAT}, + {Syscall: unix.SYS_UTIMES}, + {Syscall: unix.SYS_VFORK}, + {Syscall: unix.SYS_VMSPLICE}, + {Syscall: unix.SYS_WAIT4}, + {Syscall: unix.SYS_WAITID}, + {Syscall: unix.SYS_WRITE}, + {Syscall: unix.SYS_WRITEV}, +} diff --git a/plugin/rpcplugin/sandbox/seccomp_linux_other.go b/plugin/rpcplugin/sandbox/seccomp_linux_other.go new file mode 100644 index 000000000..5573943cd --- /dev/null +++ b/plugin/rpcplugin/sandbox/seccomp_linux_other.go @@ -0,0 +1,10 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +// +build linux,!amd64 + +package sandbox + +const NATIVE_AUDIT_ARCH = 0 + +var AllowedSyscalls []SeccompSyscall diff --git a/plugin/rpcplugin/sandbox/seccomp_linux_test.go b/plugin/rpcplugin/sandbox/seccomp_linux_test.go new file mode 100644 index 000000000..46fe38fe0 --- /dev/null +++ b/plugin/rpcplugin/sandbox/seccomp_linux_test.go @@ -0,0 +1,210 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package sandbox + +import ( + "encoding/binary" + "syscall" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/net/bpf" +) + +func seccompData(nr int32, arch uint32, ip uint64, args ...uint64) []byte { + var buf [64]byte + binary.BigEndian.PutUint32(buf[0:], uint32(nr)) + binary.BigEndian.PutUint32(buf[4:], arch) + binary.BigEndian.PutUint64(buf[8:], ip) + for i := 0; i < 6 && i < len(args); i++ { + binary.BigEndian.PutUint64(buf[16+i*8:], args[i]) + } + return buf[:] +} + +func TestSeccompFilter(t *testing.T) { + for name, tc := range map[string]struct { + Filter []bpf.Instruction + Data []byte + Expected bool + }{ + "Allowed": { + Filter: SeccompFilter(0xf00, []SeccompSyscall{ + {Syscall: syscall.SYS_READ}, + {Syscall: syscall.SYS_WRITE}, + }), + Data: seccompData(syscall.SYS_READ, 0xf00, 0), + Expected: true, + }, + "AllFail": { + Filter: SeccompFilter(0xf00, []SeccompSyscall{ + { + Syscall: syscall.SYS_READ, + Any: []SeccompConditions{ + {All: []SeccompCondition{ + &SeccompArgHasAnyBit{Arg: 0, Mask: 2}, + &SeccompArgHasAnyBit{Arg: 1, Mask: 2}, + &SeccompArgHasAnyBit{Arg: 2, Mask: 2}, + &SeccompArgHasAnyBit{Arg: 3, Mask: 2}, + }}, + }, + }, + {Syscall: syscall.SYS_WRITE}, + }), + Data: seccompData(syscall.SYS_READ, 0xf00, 0, 1, 2, 3, 4), + Expected: false, + }, + "AllPass": { + Filter: SeccompFilter(0xf00, []SeccompSyscall{ + { + Syscall: syscall.SYS_READ, + Any: []SeccompConditions{ + {All: []SeccompCondition{ + &SeccompArgHasAnyBit{Arg: 0, Mask: 7}, + &SeccompArgHasAnyBit{Arg: 1, Mask: 7}, + &SeccompArgHasAnyBit{Arg: 2, Mask: 7}, + &SeccompArgHasAnyBit{Arg: 3, Mask: 7}, + }}, + }, + }, + {Syscall: syscall.SYS_WRITE}, + }), + Data: seccompData(syscall.SYS_READ, 0xf00, 0, 1, 2, 3, 4), + Expected: true, + }, + "AnyFail": { + Filter: SeccompFilter(0xf00, []SeccompSyscall{ + { + Syscall: syscall.SYS_READ, + Any: []SeccompConditions{ + {All: []SeccompCondition{&SeccompArgHasAnyBit{Arg: 0, Mask: 8}}}, + {All: []SeccompCondition{&SeccompArgHasAnyBit{Arg: 1, Mask: 8}}}, + {All: []SeccompCondition{&SeccompArgHasAnyBit{Arg: 2, Mask: 8}}}, + {All: []SeccompCondition{&SeccompArgHasAnyBit{Arg: 3, Mask: 8}}}, + }, + }, + {Syscall: syscall.SYS_WRITE}, + }), + Data: seccompData(syscall.SYS_READ, 0xf00, 0, 1, 2, 3, 4), + Expected: false, + }, + "AnyPass": { + Filter: SeccompFilter(0xf00, []SeccompSyscall{ + { + Syscall: syscall.SYS_READ, + Any: []SeccompConditions{ + {All: []SeccompCondition{&SeccompArgHasAnyBit{Arg: 0, Mask: 2}}}, + {All: []SeccompCondition{&SeccompArgHasAnyBit{Arg: 1, Mask: 2}}}, + {All: []SeccompCondition{&SeccompArgHasAnyBit{Arg: 2, Mask: 2}}}, + {All: []SeccompCondition{&SeccompArgHasAnyBit{Arg: 3, Mask: 2}}}, + }, + }, + {Syscall: syscall.SYS_WRITE}, + }), + Data: seccompData(syscall.SYS_READ, 0xf00, 0, 1, 2, 3, 4), + Expected: true, + }, + "BadArch": { + Filter: SeccompFilter(0xf00, []SeccompSyscall{ + {Syscall: syscall.SYS_READ}, + {Syscall: syscall.SYS_WRITE}, + }), + Data: seccompData(syscall.SYS_MOUNT, 0xf01, 0), + Expected: false, + }, + "BadSyscall": { + Filter: SeccompFilter(0xf00, []SeccompSyscall{ + {Syscall: syscall.SYS_READ}, + {Syscall: syscall.SYS_WRITE}, + }), + Data: seccompData(syscall.SYS_MOUNT, 0xf00, 0), + Expected: false, + }, + } { + t.Run(name, func(t *testing.T) { + vm, err := bpf.NewVM(tc.Filter) + require.NoError(t, err) + result, err := vm.Run(tc.Data) + require.NoError(t, err) + if tc.Expected { + assert.Equal(t, SECCOMP_RET_ALLOW, result) + } else { + assert.Equal(t, int(SECCOMP_RET_ERRNO|syscall.EPERM), result) + } + }) + } +} + +func TestSeccompFilter_Conditions(t *testing.T) { + for name, tc := range map[string]struct { + Condition SeccompCondition + Args []uint64 + Expected bool + }{ + "ArgHasAnyBitFail": { + Condition: SeccompArgHasAnyBit{Arg: 0, Mask: 0x0004}, + Args: []uint64{0x0400008000}, + Expected: false, + }, + "ArgHasAnyBitPass1": { + Condition: SeccompArgHasAnyBit{Arg: 0, Mask: 0x400000004}, + Args: []uint64{0x8000008004}, + Expected: true, + }, + "ArgHasAnyBitPass2": { + Condition: SeccompArgHasAnyBit{Arg: 0, Mask: 0x400000004}, + Args: []uint64{0x8400008000}, + Expected: true, + }, + "ArgHasNoBitsFail1": { + Condition: SeccompArgHasNoBits{Arg: 0, Mask: 0x1100000011}, + Args: []uint64{0x0000008007}, + Expected: false, + }, + "ArgHasNoBitsFail2": { + Condition: SeccompArgHasNoBits{Arg: 0, Mask: 0x1100000011}, + Args: []uint64{0x0700008000}, + Expected: false, + }, + "ArgHasNoBitsPass": { + Condition: SeccompArgHasNoBits{Arg: 0, Mask: 0x400000004}, + Args: []uint64{0x8000008000}, + Expected: true, + }, + "ArgEqualsPass": { + Condition: SeccompArgEquals{Arg: 0, Value: 0x123456789ABCDEF}, + Args: []uint64{0x123456789ABCDEF}, + Expected: true, + }, + "ArgEqualsFail1": { + Condition: SeccompArgEquals{Arg: 0, Value: 0x123456789ABCDEF}, + Args: []uint64{0x023456789ABCDEF}, + Expected: false, + }, + "ArgEqualsFail2": { + Condition: SeccompArgEquals{Arg: 0, Value: 0x123456789ABCDEF}, + Args: []uint64{0x123456789ABCDE0}, + Expected: false, + }, + } { + t.Run(name, func(t *testing.T) { + filter := SeccompFilter(0xf00, []SeccompSyscall{ + { + Syscall: 1, + Any: []SeccompConditions{{All: []SeccompCondition{tc.Condition}}}, + }, + }) + vm, err := bpf.NewVM(filter) + require.NoError(t, err) + result, err := vm.Run(seccompData(1, 0xf00, 0, tc.Args...)) + require.NoError(t, err) + if tc.Expected { + assert.Equal(t, SECCOMP_RET_ALLOW, result) + } else { + assert.Equal(t, int(SECCOMP_RET_ERRNO|syscall.EPERM), result) + } + }) + } +} diff --git a/plugin/rpcplugin/sandbox/supervisor.go b/plugin/rpcplugin/sandbox/supervisor.go new file mode 100644 index 000000000..0e63954fd --- /dev/null +++ b/plugin/rpcplugin/sandbox/supervisor.go @@ -0,0 +1,33 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package sandbox + +import ( + "context" + "fmt" + "io" + "path/filepath" + "strings" + + "github.com/mattermost/mattermost-server/model" + "github.com/mattermost/mattermost-server/plugin" + "github.com/mattermost/mattermost-server/plugin/rpcplugin" +) + +func SupervisorProvider(bundle *model.BundleInfo) (plugin.Supervisor, error) { + return rpcplugin.SupervisorWithNewProcessFunc(bundle, func(ctx context.Context) (rpcplugin.Process, io.ReadWriteCloser, error) { + executable := filepath.Clean(filepath.Join(".", bundle.Manifest.Backend.Executable)) + if strings.HasPrefix(executable, "..") { + return nil, nil, fmt.Errorf("invalid backend executable") + } + return NewProcess(ctx, &Configuration{ + MountPoints: []*MountPoint{{ + Source: bundle.Path, + Destination: "/plugin", + ReadOnly: true, + }}, + WorkingDirectory: "/plugin", + }, filepath.Join("/plugin", executable)) + }) +} diff --git a/plugin/rpcplugin/sandbox/supervisor_test.go b/plugin/rpcplugin/sandbox/supervisor_test.go new file mode 100644 index 000000000..245185dd5 --- /dev/null +++ b/plugin/rpcplugin/sandbox/supervisor_test.go @@ -0,0 +1,18 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package sandbox + +import ( + "testing" + + "github.com/mattermost/mattermost-server/plugin/rpcplugin/rpcplugintest" +) + +func TestSupervisorProvider(t *testing.T) { + if err := CheckSupport(); err != nil { + t.Skip("sandboxing not supported:", err) + } + + rpcplugintest.TestSupervisorProvider(t, SupervisorProvider) +} diff --git a/plugin/rpcplugin/supervisor.go b/plugin/rpcplugin/supervisor.go index ad3c8401d..6a48cb5e8 100644 --- a/plugin/rpcplugin/supervisor.go +++ b/plugin/rpcplugin/supervisor.go @@ -6,6 +6,7 @@ package rpcplugin import ( "context" "fmt" + "io" "path/filepath" "strings" "sync/atomic" @@ -20,10 +21,10 @@ import ( // // If the plugin unexpectedly exists, the supervisor will relaunch it after a short delay. type Supervisor struct { - executable string hooks atomic.Value done chan bool cancel context.CancelFunc + newProcess func(context.Context) (Process, io.ReadWriteCloser, error) } var _ plugin.Supervisor = (*Supervisor)(nil) @@ -78,7 +79,7 @@ func (s *Supervisor) run(ctx context.Context, start chan<- error, api plugin.API } func (s *Supervisor) runPlugin(ctx context.Context, start chan<- error, api plugin.API) error { - p, ipc, err := NewProcess(ctx, s.executable) + p, ipc, err := s.newProcess(ctx) if err != nil { if start != nil { start <- err @@ -127,6 +128,16 @@ func (s *Supervisor) runPlugin(ctx context.Context, start chan<- error, api plug } func SupervisorProvider(bundle *model.BundleInfo) (plugin.Supervisor, error) { + return SupervisorWithNewProcessFunc(bundle, func(ctx context.Context) (Process, io.ReadWriteCloser, error) { + executable := filepath.Clean(filepath.Join(".", bundle.Manifest.Backend.Executable)) + if strings.HasPrefix(executable, "..") { + return nil, nil, fmt.Errorf("invalid backend executable") + } + return NewProcess(ctx, filepath.Join(bundle.Path, executable)) + }) +} + +func SupervisorWithNewProcessFunc(bundle *model.BundleInfo, newProcess func(context.Context) (Process, io.ReadWriteCloser, error)) (plugin.Supervisor, error) { if bundle.Manifest == nil { return nil, fmt.Errorf("no manifest available") } else if bundle.Manifest.Backend == nil || bundle.Manifest.Backend.Executable == "" { @@ -136,7 +147,5 @@ func SupervisorProvider(bundle *model.BundleInfo) (plugin.Supervisor, error) { if strings.HasPrefix(executable, "..") { return nil, fmt.Errorf("invalid backend executable") } - return &Supervisor{ - executable: filepath.Join(bundle.Path, executable), - }, nil + return &Supervisor{newProcess: newProcess}, nil } diff --git a/plugin/rpcplugin/supervisor_test.go b/plugin/rpcplugin/supervisor_test.go index bd20158b5..06c1fafeb 100644 --- a/plugin/rpcplugin/supervisor_test.go +++ b/plugin/rpcplugin/supervisor_test.go @@ -1,172 +1,14 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + package rpcplugin import ( - "encoding/json" - "fmt" - "io/ioutil" - "os" - "path/filepath" "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - "github.com/mattermost/mattermost-server/model" - "github.com/mattermost/mattermost-server/plugin/plugintest" + "github.com/mattermost/mattermost-server/plugin/rpcplugin/rpcplugintest" ) -func TestSupervisor(t *testing.T) { - dir, err := ioutil.TempDir("", "") - require.NoError(t, err) - defer os.RemoveAll(dir) - - backend := filepath.Join(dir, "backend.exe") - compileGo(t, ` - package main - - import ( - "github.com/mattermost/mattermost-server/plugin/rpcplugin" - ) - - type MyPlugin struct {} - - func main() { - rpcplugin.Main(&MyPlugin{}) - } - `, backend) - - ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "backend.exe"}}`), 0600) - - bundle := model.BundleInfoForPath(dir) - supervisor, err := SupervisorProvider(bundle) - require.NoError(t, err) - require.NoError(t, supervisor.Start(nil)) - require.NoError(t, supervisor.Stop()) -} - -func TestSupervisor_InvalidExecutablePath(t *testing.T) { - dir, err := ioutil.TempDir("", "") - require.NoError(t, err) - defer os.RemoveAll(dir) - - ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "/foo/../../backend.exe"}}`), 0600) - - bundle := model.BundleInfoForPath(dir) - supervisor, err := SupervisorProvider(bundle) - assert.Nil(t, supervisor) - assert.Error(t, err) -} - -func TestSupervisor_NonExistentExecutablePath(t *testing.T) { - dir, err := ioutil.TempDir("", "") - require.NoError(t, err) - defer os.RemoveAll(dir) - - ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "thisfileshouldnotexist"}}`), 0600) - - bundle := model.BundleInfoForPath(dir) - supervisor, err := SupervisorProvider(bundle) - require.NotNil(t, supervisor) - require.NoError(t, err) - - require.Error(t, supervisor.Start(nil)) -} - -// If plugin development goes really wrong, let's make sure plugin activation won't block forever. -func TestSupervisor_StartTimeout(t *testing.T) { - dir, err := ioutil.TempDir("", "") - require.NoError(t, err) - defer os.RemoveAll(dir) - - backend := filepath.Join(dir, "backend.exe") - compileGo(t, ` - package main - - func main() { - for { - } - } - `, backend) - - ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "backend.exe"}}`), 0600) - - bundle := model.BundleInfoForPath(dir) - supervisor, err := SupervisorProvider(bundle) - require.NoError(t, err) - require.Error(t, supervisor.Start(nil)) -} - -// Crashed plugins should be relaunched. -func TestSupervisor_PluginCrash(t *testing.T) { - dir, err := ioutil.TempDir("", "") - require.NoError(t, err) - defer os.RemoveAll(dir) - - backend := filepath.Join(dir, "backend.exe") - compileGo(t, ` - package main - - import ( - "os" - - "github.com/mattermost/mattermost-server/plugin" - "github.com/mattermost/mattermost-server/plugin/rpcplugin" - ) - - type Configuration struct { - ShouldExit bool - } - - type MyPlugin struct { - config Configuration - } - - func (p *MyPlugin) OnActivate(api plugin.API) error { - api.LoadPluginConfiguration(&p.config) - return nil - } - - func (p *MyPlugin) OnDeactivate() error { - if p.config.ShouldExit { - os.Exit(1) - } - return nil - } - - func main() { - rpcplugin.Main(&MyPlugin{}) - } - `, backend) - - ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "backend.exe"}}`), 0600) - - var api plugintest.API - shouldExit := true - api.On("LoadPluginConfiguration", mock.MatchedBy(func(x interface{}) bool { return true })).Return(func(dest interface{}) error { - err := json.Unmarshal([]byte(fmt.Sprintf(`{"ShouldExit": %v}`, shouldExit)), dest) - shouldExit = false - return err - }) - - bundle := model.BundleInfoForPath(dir) - supervisor, err := SupervisorProvider(bundle) - require.NoError(t, err) - require.NoError(t, supervisor.Start(&api)) - - failed := false - recovered := false - for i := 0; i < 30; i++ { - if supervisor.Hooks().OnDeactivate() == nil { - require.True(t, failed) - recovered = true - break - } else { - failed = true - } - time.Sleep(time.Millisecond * 100) - } - assert.True(t, recovered) - require.NoError(t, supervisor.Stop()) +func TestSupervisorProvider(t *testing.T) { + rpcplugintest.TestSupervisorProvider(t, SupervisorProvider) } -- cgit v1.2.3-1-g7c22