blob: 4806ea39a121aabc069abe839f22c0c362256573 [file] [log] [blame]
// Copyright 2018 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package security
import (
"context"
"encoding/json"
"io/ioutil"
"os"
"path/filepath"
"strings"
"chromiumos/tast/common/testexec"
"chromiumos/tast/local/sysutil"
"chromiumos/tast/shutil"
"chromiumos/tast/testing"
)
func init() {
testing.AddTest(&testing.Test{
Func: RunOCI,
Desc: "Verifies the functionality of the run_oci command",
Contacts: []string{
"jorgelo@chromium.org", // Security team
"chromeos-security@google.com",
},
SoftwareDeps: []string{"oci"},
Attr: []string{"group:mainline"},
})
}
func RunOCI(ctx context.Context, s *testing.State) {
defaultCfg := func() ociConfig {
return ociConfig{
OCIVersion: "1.0.0-rc1",
Root: ociRoot{Path: "rootfs", ReadOnly: true},
Mounts: []ociMount{
{Destination: "/", Type: "bind", Source: "/", Options: []string{"rbind", "ro"}},
{Destination: "/proc", Type: "proc", Source: "proc", Options: []string{"nodev", "noexec", "nosuid"}},
},
Process: ociProcess{Terminal: true, User: ociUser{UID: 0, GID: 0}, Cwd: "/"},
Platform: ociPlatform{OS: "linux", Arch: "all"},
Hostname: "runc",
Linux: ociLinux{
Namespaces: []ociNamespace{{Type: "cgroup"}, {Type: "pid"}, {Type: "network"}, {Type: "ipc"},
{Type: "user"}, {Type: "uts"}, {Type: "mount"}},
Resources: ociResources{
Devices: []ociResourceDevice{
{Allow: false, Access: "rwm"},
{Allow: true, Type: "c", Major: 1, Minor: 5, Access: "r"},
},
},
UIDMappings: []ociMapping{{HostID: sysutil.ChronosUID, ContainerID: 0, Size: 1}},
GIDMappings: []ociMapping{{HostID: sysutil.ChronosGID, ContainerID: 0, Size: 1}},
},
}
}
type testCase struct {
name string // short human-readable name for test case, e.g. "do-stuff"
runOCIArgs []string // additional top-level run_oci command-line args
expStdout string // expected stdout from run_oci
expStderr string // expected stderr from run_oci
expFail bool // whether run_oci should return a non-zero exit status
modifyCfg func(cfg *ociConfig) // makes per-test modifications to default config
}
runTest := func(tc testCase) {
// Create temp dir under /tmp to ensure that it's accessible by the chronos user.
td, err := ioutil.TempDir("/tmp", "tast.security.RunOCI.")
if err != nil {
s.Fatal("Failed to create temp dir: ", err)
}
defer os.RemoveAll(td)
if err := os.Chmod(td, 0755); err != nil {
s.Fatal("Failed to chmod temp dir: ", err)
}
cfg := defaultCfg()
tc.modifyCfg(&cfg)
b, err := json.Marshal(&cfg)
if err != nil {
s.Fatal("Failed to marshal config to JSON: ", err)
}
cfgPath := filepath.Join(td, "config.json")
if err := ioutil.WriteFile(cfgPath, b, 0644); err != nil {
s.Fatal("Failed to create write file: ", err)
}
rootDir := filepath.Join(td, "rootfs")
if err := os.Mkdir(rootDir, 0755); err != nil {
s.Fatal("Failed to create root dir: ", err)
}
if err := os.Chown(rootDir, int(sysutil.ChronosUID), int(sysutil.ChronosGID)); err != nil {
s.Fatal("Failed to chown root dir: ", err)
}
var args []string
args = append(args, "--cgroup_parent=chronos_containers")
args = append(args, tc.runOCIArgs...)
args = append(args, "run", "-c", td, "test_container")
cmd := testexec.CommandContext(ctx, "/usr/bin/run_oci", args...)
s.Logf("Case %v: running %v", tc.name, shutil.EscapeSlice(cmd.Args))
stdout, stderr, err := cmd.SeparatedOutput()
failed := false
// TODO(b/194923131): Hack to disable failures which involve librt.so.1
if strings.Contains(string(stderr), "librt.so.1") {
return
}
if err != nil && !tc.expFail {
failed = true
s.Errorf("Case %v failed: %v", tc.name, err)
} else if err == nil && tc.expFail {
failed = true
s.Errorf("Case %v unexpectedly succeeded", tc.name)
}
if string(stdout) != tc.expStdout {
failed = true
s.Errorf("Case %v got stdout %q; want %q", tc.name, string(stdout), tc.expStdout)
}
if string(stderr) != tc.expStderr {
failed = true
s.Errorf("Case %v got stderr %q; want %q", tc.name, string(stderr), tc.expStderr)
}
if failed {
// Unfortunately, we can't pass --log_dir to run_oci to tell it to write log messages
// somewhere else where they could be reported by this test: doing so also causes the
// container's stdout and stderr to be redirected to the file.
s.Log("Check syslog for run_oci log messages")
}
}
for _, tc := range []testCase{
{
name: "alt-syscall-settime",
modifyCfg: func(cfg *ociConfig) {
cfg.Process.Args = []string{"/bin/date", "-u", "--set", "010101"}
cfg.Linux.AltSyscall = "third_party"
},
expStdout: "Mon Jan 1 00:00:00 UTC 2001\n",
expStderr: "date: cannot set date: Function not implemented\n",
expFail: true,
},
{
name: "bind-mount",
runOCIArgs: []string{"--bind_mount=/bin:/var/log"},
modifyCfg: func(cfg *ociConfig) { cfg.Process.Args = []string{"/bin/ls", "/var/log/bash"} },
expStdout: "/var/log/bash\n",
},
{
name: "device",
modifyCfg: func(cfg *ociConfig) {
cfg.Process.Args = []string{"/bin/ls", "/dev/null_test"}
cfg.Mounts = append(cfg.Mounts, ociMount{Source: "tmpfs", Destination: "/dev", Type: "tmpfs", Options: []string{"noexec", "nosuid"}, PerformInIntermediateNamespace: true})
cfg.Linux.Devices = append(cfg.Linux.Devices, ociLinuxDevice{Path: "/dev/null_test", Type: "c", Major: 1, Minor: 3, FileMode: 0666, UID: 0, GID: 0})
},
expStdout: "/dev/null_test\n",
},
{
name: "device-cgroup-allow",
modifyCfg: func(cfg *ociConfig) { cfg.Process.Args = []string{"/usr/bin/hexdump", "-n16", "/dev/zero"} },
expStdout: "0000000 0000 0000 0000 0000 0000 0000 0000 0000\n0000010\n",
},
{
name: "device-cgroup-deny",
modifyCfg: func(cfg *ociConfig) { cfg.Process.Args = []string{"/usr/bin/hexdump", "-n1", "/dev/urandom"} },
expStderr: "hexdump: /dev/urandom: Operation not permitted\nhexdump: all input file arguments failed\n",
expFail: true,
},
{
name: "gid",
modifyCfg: func(cfg *ociConfig) { cfg.Process.Args = []string{"/usr/bin/id", "-g"} },
expStdout: "0\n",
},
{
name: "hooks",
modifyCfg: func(cfg *ociConfig) {
cfg.Process.Args = []string{"/bin/echo", "-n", "3"}
cfg.Hooks.PreChroot = append(cfg.Hooks.PreChroot, ociHook{Path: "/bin/echo", Args: []string{"echo", "-n", "0"}})
cfg.Hooks.PreStart = append(cfg.Hooks.PreStart,
ociHook{Path: "/bin/echo", Args: []string{"echo", "-n", "1"}},
ociHook{Path: "/bin/echo", Args: []string{"echo", "-n", "2"}},
)
cfg.Hooks.PostStop = append(cfg.Hooks.PostStop, ociHook{Path: "/bin/echo", Args: []string{"echo", "-n", "4"}})
},
expStdout: "01234",
},
{
name: "hooks-failure",
modifyCfg: func(cfg *ociConfig) {
cfg.Process.Args = []string{"/bin/echo", "-n", "This should not run"}
cfg.Hooks.PreStart = append(cfg.Hooks.PreStart, ociHook{Path: "/bin/false", Args: []string{"false"}})
},
expFail: true,
},
{
name: "uid",
modifyCfg: func(cfg *ociConfig) { cfg.Process.Args = []string{"/usr/bin/id", "-u"} },
expStdout: "0\n",
},
} {
runTest(tc)
}
}
// ociConfig describes a JSON config file read by run_oci.
// See https://github.com/opencontainers/runtime-spec/blob/master/config.md and platform2/run_oci for details. // nocheck
// This struct only contains configuration fields that are needed for this test.
type ociConfig struct {
OCIVersion string `json:"ociVersion"`
Root ociRoot `json:"root,omitempty"`
Mounts []ociMount `json:"mounts,omitempty"`
Process ociProcess `json:"process,omitempty"`
Platform ociPlatform `json:"platform,omitempty"`
Hostname string `json:"hostname,omitempty"`
Hooks ociHooks `json:"hooks,omitempty"`
Linux ociLinux `json:"linux,omitempty"`
}
// ociRoot describes the top-level "root" config entry.
type ociRoot struct {
Path string `json:"path"`
ReadOnly bool `json:"readonly,omitempty"`
}
// ociMount describes an entry in the top-level "mounts" config entry.
type ociMount struct {
Destination string `json:"destination"`
Source string `json:"source,omitempty"`
Type string `json:"type,omitempty"`
Options []string `json:"options,omitempty"`
// PerformInIntermediateNamespace is a run_oci-specific extension.
PerformInIntermediateNamespace bool `json:"performInIntermediateNamespace,omitempty"`
}
// ociProcess represents the top-level "process" config entry.
type ociProcess struct {
Terminal bool `json:"terminal,omitempty"`
User ociUser `json:"user"`
Args []string `json:"args"`
Cwd string `json:"cwd"`
}
// ociUser describes the "process.user" config entry.
type ociUser struct {
UID int `json:"uid"`
GID int `json:"gid"`
}
// ociPlatform represents the top-level "platform" config entry.
type ociPlatform struct {
OS string `json:"os,omitempty"`
Arch string `json:"arch,omitempty"`
}
// ociHooks describes the top-level "hooks" config entry.
type ociHooks struct {
// PreChroot is a run_oci-specific extension.
PreChroot []ociHook `json:"prechroot"`
PreStart []ociHook `json:"prestart"`
PostStop []ociHook `json:"poststop"`
}
// ociHook describes an entry in e.g. "hooks.prechroot".
type ociHook struct {
Path string `json:"path"`
Args []string `json:"args"`
}
// ociLinux describes the top-level "linux" config entry.
type ociLinux struct {
Devices []ociLinuxDevice `json:"devices"`
Namespaces []ociNamespace `json:"namespaces"`
Resources ociResources `json:"resources"`
UIDMappings []ociMapping `json:"uidMappings"`
GIDMappings []ociMapping `json:"gidMappings"`
// AltSyscall is a run_oci-specific extension.
AltSyscall string `json:"altSyscall,omitempty"`
}
// ociLinuxDevice describes an entry in "linux.devices".
type ociLinuxDevice struct {
Path string `json:"path"`
Type string `json:"type"`
Major int `json:"major"`
Minor int `json:"minor"`
FileMode int `json:"fileMode"`
UID int `json:"uid"`
GID int `json:"gid"`
}
// ociNamespace describes an entry in "linux.namespaces".
type ociNamespace struct {
Type string `json:"type"`
}
// ociResources describes the "linux.resources" config entry.
type ociResources struct {
Devices []ociResourceDevice `json:"devices"`
}
// ociResources describes an entry in "linux.resources.devices".
type ociResourceDevice struct {
Allow bool `json:"allow"`
Type string `json:"type,omitempty"`
Major int `json:"major,omitempty"`
Minor int `json:"minor,omitempty"`
Access string `json:"access,omitempty"`
}
// ociMapping describes an entry in "linux.uidMappings" or "linux.gidMappings".
type ociMapping struct {
HostID uint32 `json:"hostID"`
ContainerID uint32 `json:"containerID"`
Size uint32 `json:"size"`
}