blob: f202759698aa4cf06aeaa7033af3f6617de6f1ef [file] [log] [blame]
// Copyright 2019 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"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strings"
"syscall"
"time"
"chromiumos/tast/common/testexec"
"chromiumos/tast/errors"
"chromiumos/tast/testing"
)
type linkMode int
const (
staticLink linkMode = iota
dynamicLink
)
func init() {
testing.AddTest(&testing.Test{
Func: Minijail,
Desc: "Verifies minijail0's basic functionality",
Contacts: []string{
"jorgelo@chromium.org", // Security team
"chromeos-security@google.com",
},
Params: []testing.Param{{
Name: "dynamic",
Val: dynamicLink,
}, {
Name: "static",
Val: staticLink,
// Sanitizer builds do not support static linking
ExtraSoftwareDeps: []string{"no_asan", "no_msan", "no_ubsan"},
}},
Attr: []string{"group:mainline"},
})
}
func Minijail(ctx context.Context, s *testing.State) {
const (
minijailPath = "/sbin/minijail0"
bashPath = "/bin/bash"
// This is installed by the chromeos-base/tast-local-helpers-cros package.
staticBashPath = "/usr/local/libexec/tast/helpers/local/cros/security.Minijail.staticbashexec"
)
// Create a directory that can be written to by test cases running in user namespaces.
usernsDir, err := ioutil.TempDir("", "tast.security.Minijail.userns.")
if err != nil {
s.Fatal("Failed to create userns dir: ", err)
}
defer os.RemoveAll(usernsDir)
if err := os.Chmod(usernsDir, 0777); err != nil {
s.Fatal("Failed to chmod userns dir: ", err)
}
type setupFunc func(tempDir string) error
type checkFunc func(stdout string) error
// testCase describes a minijail0 invocation.
type testCase struct {
name string // human-readable test case name
cmd string // shell-quoted command and arguments to run via "bash -c"
args []string // minijail0-specific args; "%T" is replaced by temp dir
args64 []string // like args, but only added if /lib64 exists
setup setupFunc // optional function to run before test
check checkFunc // optional function to run after test
}
runTestCase := func(tc *testCase, lm linkMode) {
// Construct a human-readable test name.
name := tc.name
if lm == staticLink {
name += ".static"
}
// Create a temp dir that the test's setup function (if any) can write to.
td, err := ioutil.TempDir("", "tast.security.Minijail."+name+".")
if err != nil {
s.Fatal("Failed to create temp dir: ", err)
}
defer os.RemoveAll(td)
if tc.setup != nil {
if err := tc.setup(td); err != nil {
s.Errorf("Failed %v setup: %v", name, err)
return
}
}
// We need to make a copy of the args slice before modifying it, as each test case is
// used twice with a static and non-static shell executable.
args := append([]string{}, tc.args...)
if _, err := os.Stat("/lib64"); err == nil {
args = append(args, tc.args64...)
}
for i, a := range args {
args[i] = strings.Replace(a, "%T", td, -1)
}
shell := bashPath
if lm == staticLink {
shell = staticBashPath
if _, err := os.Stat(staticBashPath); err != nil {
s.Fatalf("Failed to stat %v: %v", staticBashPath, err)
}
}
s.Log("Running test case ", name)
args = append(args, shell, "-c", tc.cmd)
ctx, cancel := context.WithTimeout(ctx, 15*time.Second)
defer cancel()
cmd := testexec.CommandContext(ctx, minijailPath, args...)
out, err := cmd.Output()
if err != nil {
s.Errorf("%v failed: %v", name, err)
cmd.DumpLog(ctx)
return
}
if tc.check != nil {
if err := tc.check(string(out)); err != nil {
s.Errorf("%v command %q produced bad output: %v", name, tc.cmd, err)
}
}
}
// subdirSetup returns a setupFunc that creates the supplied subdirectories with the temp dir.
subdirSetup := func(dirs ...string) setupFunc {
return func(td string) error {
for _, d := range dirs {
if err := os.MkdirAll(filepath.Join(td, d), 0755); err != nil {
return err
}
}
return nil
}
}
// mountTestCase returns a testCase based on common settings needed by chroot and pivotroot test cases.
mountTestCase := func(name, cmd string, extraArgs []string) testCase {
return testCase{
name: name,
cmd: cmd,
setup: subdirSetup("c/bin", "c/lib64", "c/lib", "c/usr/lib", "c/usr/lib64", "c/usr/local", "c/tmp-rw", "c/tmp-ro", "tmp"),
args: append([]string{
"-b", "/bin,/bin",
"-b", "/lib,/lib",
"-b", "/usr/lib,/usr/lib",
"-b", "/usr/local,/usr/local",
"-b", "%T/tmp,/tmp-rw,1",
"-b", "%T/tmp,/tmp-ro",
"-v",
}, extraArgs...),
args64: []string{"-b", "/lib64,/lib64", "-b", "/usr/lib64,/usr/lib64"},
}
}
// checkRegexp returns a checkFunc that verifies that re matches stdout.
checkRegexp := func(re string) checkFunc {
r := regexp.MustCompile(re)
return func(stdout string) error {
if !r.MatchString(stdout) {
return errors.Errorf("stdout %q not matched by %q", stdout, re)
}
return nil
}
}
// checkFilePerms returns a checkFunc that verifies that p is owned by expUID and expGID.
checkFilePerms := func(p string, expUID, expGID uint32) checkFunc {
return func(stdout string) error {
if fi, err := os.Stat(p); err != nil {
return err
} else if st := fi.Sys().(*syscall.Stat_t); st.Uid != expUID || st.Gid != expGID {
return errors.Errorf("%v has UID %v (want %v) and GID %v (want %v)",
p, st.Uid, expUID, st.Gid, expGID)
}
return nil
}
}
chrootArgs := []string{"-C", "%T/c"}
pivotrootArgs := []string{"-P", "%T/c"}
usernsArgs := []string{"-m0 1000 1", "-M0 1000 1"}
for _, tc := range []testCase{
{
name: "caps",
cmd: `[ -w /usr/local/bin ] && cat /proc/self/status`, // check that we kept CAP_DAC_OVERRIDE
args: []string{"-u", "1000", "-g", "1000", "-c", "2", "--ambient"}, // 2 is CAP_DAC_OVERRIDE
check: checkRegexp(`(?m)^CapEff:\s*0000000000000002$`),
},
mountTestCase("chroot-cwd-is-root", "[ $(pwd) = / ]", chrootArgs),
mountTestCase("chroot-lib-exists", "[ -d /lib ]", chrootArgs),
mountTestCase("chroot-tmp-rw-exists", "[ -d /tmp-rw ]", chrootArgs),
mountTestCase("chroot-tmp-ro-exists", "[ -d /tmp-ro ]", chrootArgs),
mountTestCase("chroot-tmp-rw-is-writable", "echo x > /tmp-rw/test-rw", chrootArgs),
mountTestCase("chroot-tmp-ro-is-read-only", "! echo x > /tmp-ro/test-ro", chrootArgs),
{
name: "create-mount-destination",
cmd: "cat /proc/mounts",
args: []string{"-v", "-C", "/", "-k", "tmpfs,%T,tmpfs", "-b", "/dev/null,%T/test_null"},
check: checkRegexp("test_null"),
},
{
name: "gid",
cmd: "id -rg && id -g",
args: []string{"-g", "1000"},
check: checkRegexp("^1000\n1000\n$"),
},
{
name: "group",
cmd: "id -rg && id -g",
args: []string{"-g", "chronos"},
check: checkRegexp("^1000\n1000\n$"),
},
{
name: "init",
cmd: "echo $$",
args: []string{"-I"},
check: checkRegexp("^1\n$"),
},
{
name: "mountns-enter",
// We run a long one-liner within a new mount namespace:
cmd: strings.Join([]string{
// Create a temp dir and mount it as tmpfs.
`dir=$(mktemp -d)`,
`f="${dir}/test"`,
`mount tmpfs "${dir}" -t tmpfs`,
// Write a file within the dir.
`echo inaccessible >"$f"`,
// Try to cat the file while running within init's mountns.
`(/sbin/minijail0 -V /proc/1/ns/mnt -- /bin/cat "${f}" || true) 2>&1`,
// Clean up.
`umount "${dir}"`,
`rm -r "${dir}"`,
}, " && "),
args: []string{"-v"},
// The cat process should be unable to access the file that we wrote.
check: checkRegexp(`cat: \S+: No such file or directory`),
},
{
name: "mount-tmpfs",
cmd: "cat /proc/mounts",
args: []string{"-v", "-C", "/", "-k", "tmpfs,%T,tmpfs,0x1,uid=5446"},
check: checkRegexp("tmpfs.*ro.*uid=5446"),
},
{
name: "netns",
cmd: `wc -l </proc/net/dev`, // look in /proc/net/dev so we get even downed devices
args: []string{"-e"},
check: checkRegexp("^3\n$"),
},
{
name: "pid-file",
cmd: `read pid < pidfile && [ $$ = "${pid}" ]`,
args: []string{
"-b", "/bin,/bin",
"-b", "/lib,/lib",
"-b", "/usr/lib,/usr/lib",
"-b", "/usr/local,/usr/local",
"-C", "%T/c",
"-f", "%T/c/pidfile",
},
args64: []string{"-b", "/lib64,/lib64", "-b", "/usr/lib64,/usr/lib64"},
setup: subdirSetup("c/bin", "c/lib64", "c/lib", "c/usr/bin", "c/usr/lib", "c/usr/lib64", "c/usr/local"),
},
{
name: "pidns",
cmd: "echo $$",
args: []string{"-p"},
check: checkRegexp("^2\n$"),
},
mountTestCase("pivotroot-cwd-is-root", "[ $(pwd) = / ]", pivotrootArgs),
mountTestCase("pivotroot-lib-exists", "[ -d /lib ]", pivotrootArgs),
mountTestCase("pivotroot-tmp-rw-exists", "[ -d /tmp-rw ]", pivotrootArgs),
mountTestCase("pivotroot-tmp-ro-exists", "[ -d /tmp-ro ]", pivotrootArgs),
mountTestCase("pivotroot-tmp-rw-is-writable", "echo x > /tmp-rw/test-rw", pivotrootArgs),
mountTestCase("pivotroot-tmp-ro-is-read-only", "! echo x > /tmp-ro/test-ro", pivotrootArgs),
{
name: "remount",
cmd: "[ ! -w /proc/sys/kernel/printk ]",
args: []string{"-r"},
},
{
name: "rlimits",
cmd: "cat /proc/self/limits",
args: []string{"-R", "13,10,11"}, // 13 is RLIMIT_NICE
check: checkRegexp(`Max nice priority\s*10\s*11`),
},
{
name: "tmpfs",
cmd: "stat -f /tmp -c %T", // the %T here is a format string to print the FS type
setup: subdirSetup("c/bin", "c/lib64", "c/lib", "c/usr/lib", "c/usr/lib64", "c/usr/local", "c/usr/bin", "c/tmp"),
args: []string{
"-b", "/bin,/bin",
"-b", "/lib,/lib",
"-b", "/usr/lib,/usr/lib",
"-b", "/usr/bin,/usr/bin",
"-b", "/usr/local,/usr/local",
"-C", "%T/c",
"-t",
"-v",
},
args64: []string{"-b", "/lib64,/lib64", "-b", "/usr/lib64,/usr/lib64"},
check: checkRegexp("^tmpfs\n$"),
},
{
name: "uid",
cmd: "id -ru && id -u",
args: []string{"-u", "1000"},
check: checkRegexp("^1000\n1000\n$"),
},
{
name: "user",
cmd: "id -ru && id -u",
args: []string{"-u", "chronos"},
check: checkRegexp("^1000\n1000\n$"),
},
{
name: "usergroups-add-new",
cmd: "groups",
args: []string{"-u", "chronos", "-g", "chronos", "-G"},
check: checkRegexp(`\bcras\b`),
},
{
name: "usergroups-remove-orig",
cmd: "groups",
args: []string{"-u", "chronos", "-g", "chronos", "-G"},
check: func(stdout string) error {
if strings.Contains(stdout, "root") {
return errors.New("still in group 'root'")
}
return nil
},
},
{
name: "userns-file",
cmd: fmt.Sprintf("[ $(id -u) = 65534 ] && [ $(id -g) = 65534 ] && touch %q", filepath.Join(usernsDir, "userns-file")),
args: []string{"-U"},
check: checkFilePerms(filepath.Join(usernsDir, "userns-file"), 0, 0),
},
{
name: "userns-file-gid",
cmd: fmt.Sprintf("[ $(id -u) = 65534 ] && [ $(id -g) = 0 ] && touch %q", filepath.Join(usernsDir, "userns-file-gid")),
args: []string{"-M0 1000 1"},
check: checkFilePerms(filepath.Join(usernsDir, "userns-file-gid"), 0, 1000),
},
{
name: "userns-gid",
cmd: "id -rg && id -g",
args: usernsArgs,
check: checkRegexp("^0\n0\n$"),
},
{
name: "userns-init",
cmd: "echo $$",
args: append(usernsArgs, "-I"),
check: checkRegexp("^1\n$"),
},
{
name: "userns-netns",
cmd: `wc -l </proc/net/dev`, // look in /proc/net/dev so we get even downed devices
args: append(usernsArgs, "-e"),
check: checkRegexp("^3\n$"),
},
{
name: "userns-pidns",
cmd: "echo $$",
args: append(usernsArgs, "-p"),
check: checkRegexp("^2\n$"),
},
{
name: "userns-uid",
cmd: "id -ru && id -u",
args: usernsArgs,
check: checkRegexp("^0\n0\n$"),
},
} {
runTestCase(&tc, s.Param().(linkMode))
}
}