blob: 721f089fb6683aa6bc5762c46808ac7004aa8e23 [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 containermount implements the tests for ARC related mount points.
package containermount
import (
"context"
"io/ioutil"
"path/filepath"
"reflect"
"regexp"
"sort"
"strconv"
"strings"
"chromiumos/tast/common/testexec"
upstartcommon "chromiumos/tast/common/upstart"
"chromiumos/tast/errors"
"chromiumos/tast/local/arc"
"chromiumos/tast/local/sysutil"
"chromiumos/tast/local/upstart"
"chromiumos/tast/testing"
)
// mountsForMinijail returns a list of mount points of the minijail'ed process
// whose PID file is at pidFile.
func mountsForMinijail(pidFile string) ([]sysutil.MountInfo, error) {
b, err := ioutil.ReadFile(pidFile)
if err != nil {
return nil, errors.Wrapf(err, "failed to read %s", pidFile)
}
pid, err := strconv.Atoi(strings.TrimSpace(string(b)))
if err != nil {
return nil, errors.Wrapf(err, "failed to parse %s", b)
}
return sysutil.MountInfoForPID(pid)
}
// arcMounts returns a list of mount point info for ARC's mount namespace.
func arcMounts() ([]sysutil.MountInfo, error) {
pid, err := arc.InitPID()
if err != nil {
return nil, err
}
return sysutil.MountInfoForPID(int(pid))
}
// adbdMounts returns a list of mount point info for arc-adbd's mount namespace.
func adbdMounts(ctx context.Context) ([]sysutil.MountInfo, error) {
goal, state, _, err := upstart.JobStatus(ctx, "arc-adbd")
if err != nil {
return nil, err
}
if goal == upstartcommon.StopGoal && state == upstartcommon.WaitingState {
// On the current platform arc-adbd is not used.
return nil, nil
}
return mountsForMinijail("/run/arc/adbd.pid")
}
// sdcardMounts returns a list of mount point info for sdcard.
// In ARC N, it returns a list of mount point info for sdcard daemon's mount
// namespace. In ARC P, it returns a list of esdfs typed mount points.
func sdcardMounts() ([]sysutil.MountInfo, error) {
// In ARC P, esdfs is used. Returns the mount points of esdfs file
// system type.
global, err := sysutil.MountInfoForPID(sysutil.SelfPID)
if err != nil {
return nil, err
}
var ret []sysutil.MountInfo
for _, m := range global {
if strings.HasPrefix(m.Fstype, "esdfs") {
ret = append(ret, m)
}
}
return ret, nil
}
// mountPassthroughMounts returns a list of mount point info for
// mount-passthrough daemons. Currently there are 8 mount-passthrough daemon
// jobs for MyFiles and removable media.
// The name should be matched to the following regexp:
// "arc-(myfiles|removable-media)(-(default|read|write))?
func mountPassthroughMounts(ctx context.Context) ([]sysutil.MountInfo, error) {
var ret []sysutil.MountInfo
for _, job := range []string{
"arc-myfiles",
"arc-myfiles-default",
"arc-myfiles-read",
"arc-myfiles-write",
"arc-removable-media",
"arc-removable-media-default",
"arc-removable-media-read",
"arc-removable-media-write",
} {
_, _, pid, err := upstart.JobStatus(ctx, job)
if err != nil {
return nil, err
}
if pid == 0 {
testing.ContextLogf(ctx, "%s is not running, skipped", job)
continue
}
// mount-passthrough runs as a child of the upstart job process.
out, err := testexec.CommandContext(ctx, "pgrep", "--parent", strconv.Itoa(pid)).Output(testexec.DumpLogOnError)
if err != nil {
return nil, err
}
pid, err = strconv.Atoi(strings.TrimSpace(string(out)))
if err != nil {
return nil, errors.Wrapf(err, "failed to parse PID: %s", out)
}
mounts, err := sysutil.MountInfoForPID(pid)
if err != nil {
return nil, err
}
ret = append(ret, mounts...)
}
return ret, nil
}
// obbMounts returns a list of mount point info for obb-mounter's namespace.
func obbMounts(ctx context.Context) ([]sysutil.MountInfo, error) {
out, err := testexec.CommandContext(ctx, "pgrep", "-f", "-u", "root", "^/usr/bin/arc-obb-mounter").Output(testexec.DumpLogOnError)
if err != nil {
return nil, err
}
pid, err := strconv.Atoi(strings.TrimSpace(string(out)))
if err != nil {
return nil, errors.Wrapf(err, "failed to parse PID: %s", out)
}
return sysutil.MountInfoForPID(pid)
}
func joinMounts(mss ...[]sysutil.MountInfo) []sysutil.MountInfo {
var ret []sysutil.MountInfo
for _, ms := range mss {
ret = append(ret, ms...)
}
return ret
}
func testNoARCLeak(ctx context.Context, s *testing.State, global []sysutil.MountInfo) {
s.Log("Running testNoARCLeak")
const root = "/opt/google/containers/android/rootfs/"
var paths []string
for _, m := range global {
if !strings.HasPrefix(m.MountPath, root) {
// Not the mount point under the container root.
continue
}
p, err := filepath.Rel(root, m.MountPath)
if err != nil {
s.Errorf("Couldn't take relative path of %s from %s: %v", m.MountPath, root, err)
return
}
// system/lib/arm is only required when houdini is used.
if p == "system/lib/arm" {
continue
}
paths = append(paths, p)
}
sort.Strings(paths)
expect := []string{
"android-data",
"root",
}
if !reflect.DeepEqual(paths, expect) {
s.Errorf("Unexpected mount paths: got %v; want %v", paths, expect)
}
}
func testNoARCSharedLeak(ctx context.Context, s *testing.State, arc, nonARC []sysutil.MountInfo) {
s.Log("Running testNoARCSharedLeak")
// Set of peer groups which are visible from outside of ARC container.
visibles := make(map[int]struct{})
for _, m := range nonARC {
if m.Shared > 0 {
visibles[m.Shared] = struct{}{}
}
// TODO(chavey): crbug/1126921 - replace master in structure.
if m.Master > 0 {
visibles[m.Master] = struct{}{}
}
}
// Peer groups in ARC container must not be visible from outside of
// ARC container.
for _, m := range arc {
if m.Shared == 0 {
// Not shared. Skip.
continue
}
if _, ok := visibles[m.Shared]; ok {
s.Errorf("Peer group in ARC container is being leaked: %s:%d", m.MountPath, m.Shared)
}
}
}
func testDebugfsTracefs(ctx context.Context, s *testing.State, arc []sysutil.MountInfo) {
s.Log("Running testDebugfsTracefs")
// If debugfs/tracefs is mounted somewhere in the container,
// - It should be mounted at /sys/kernel/debug/tracing, and
// - It should be /tracing portion of debugfs (or the root of tracefs for kernels >= 4.4)
// And there is at most one such mount.
// Or there could be sync debugfs mounted.
// And there can be zero or one sync mounts.
var numTracing, numSync int
for _, m := range arc {
switch m.Fstype {
case "debugfs":
if m.Root == "/tracing" && m.MountPath == "/sys/kernel/debug/tracing" {
numTracing++
} else if m.Root == "/sync" && m.MountPath == "/sys/kernel/debug/sync" {
numSync++
} else {
s.Errorf("Unexpected debugfs mount point at %s", m.MountPath)
}
case "tracefs":
if m.Root == "/" && m.MountPath == "/sys/kernel/debug/tracing" {
numTracing++
} else {
s.Errorf("Unexpected tracefs mount point at %s", m.MountPath)
}
}
}
if numTracing != 1 {
s.Errorf("Unexpected debugfs/tracefs mount points: got %d; want 1", numTracing)
}
if numSync != 0 && numSync != 1 {
s.Errorf("Unexpected sync debug fs mount points: got %d; want 0 or 1", numSync)
}
}
func testCgroup(ctx context.Context, s *testing.State, arc []sysutil.MountInfo) {
s.Log("Running testCgroup")
var paths []string
for _, m := range arc {
if m.Fstype != "cgroup" {
continue
}
// This device exists only on some ARM boards like kevin.
if m.MountPath == "/dev/stune" {
continue
}
paths = append(paths, m.MountPath)
}
sort.Strings(paths)
expect := []string{"/acct", "/dev/cpuctl", "/dev/cpuset"}
if !reflect.DeepEqual(paths, expect) {
s.Errorf("Unexpected cgroup paths: got %v; want %v", paths, expect)
return
}
}
func testADBD(ctx context.Context, s *testing.State, adbd []sysutil.MountInfo) {
s.Log("Running testADBD")
re := regexp.MustCompile(`^/run/arc/adbd(/ep[12])?$`)
for _, m := range adbd {
if m.Master > 0 {
s.Error("adbd proxy container has unknown slave mount at ", m.MountPath)
continue
}
if m.Shared == 0 {
continue
}
if !re.MatchString(m.MountPath) {
s.Error("adbd proxy container exposes unknown mount point at ", m.MountPath)
}
}
}
func testSDCard(ctx context.Context, s *testing.State, sdcard []sysutil.MountInfo) {
s.Log("Running testSDCard")
ver, err := arc.SDKVersion()
if err != nil {
s.Error("Failed to get SDK version: ", err)
return
}
// If the mount point is shared it should be either:
// - /mnt/runtime
// - /mnt/runtime/{default,read,write}/$label
// In ARC P, the following points are also shared:
// - /run/arc/sdcard
// - /run/arc/sdcard/{default,read,write}/$label
// In ARC Q, the follow points are also shared:
// - /run/arc/sdcard/full/$label
pat := `^/mnt/runtime(/(default|read|write)/[^/]+)?$`
pat += `|^/run/arc/sdcard(/(default|read|write)/[^/]+)?$`
if ver >= arc.SDKQ {
pat += `|^/run/arc/sdcard/full/[^/]+$`
}
re := regexp.MustCompile(pat)
for _, m := range sdcard {
if m.Master > 0 {
s.Errorf("Unexpected SDCard slave mount at %s", m.MountPath)
continue
}
if m.Shared == 0 {
continue
}
if !re.MatchString(m.MountPath) {
s.Errorf("Unexpected SDCard shared mount at %s", m.MountPath)
}
}
}
func testMountPassthrough(ctx context.Context, s *testing.State, mountPassthrough []sysutil.MountInfo) {
s.Log("Running testMountPassthrough")
for _, m := range mountPassthrough {
// The only shared mount point is /mnt/dest.
// Note that there might be multiple shared mount points at
// the exactly same path.
if m.Shared > 0 && m.MountPath != "/mnt/dest" {
s.Errorf("Unexpected mount-passthrough shared mount at %s", m.MountPath)
}
}
}
func testOBBMount(ctx context.Context, s *testing.State, obb []sysutil.MountInfo) {
s.Log("Running testOBBMount")
for _, m := range obb {
// The only shared mount point is /var/run/arc/obb.
if m.Shared > 0 && m.MountPath != "/var/run/arc/obb" {
s.Errorf("Unexpected OBB shared mount at %s", m.MountPath)
}
}
}
func testMountShared(ctx context.Context, s *testing.State, arcMs, adbd, sdcard, mountPassthrough, obb []sysutil.MountInfo) {
ignored := make(map[string]struct{})
if adbd == nil {
// ADBD proxy container does not run on all boards because it
// needs to have hardware and kernel support.
ignored["/dev/usb-ffs/adb"] = struct{}{}
}
// In ARC P or later, ignore initial tmpfs mount for /run/arc/sdcard
// because it is slave mount but has the initns as its parent.
ignored["/var/run/arc/sdcard"] = struct{}{}
// Ignore unix domain socket for ADB communication.
ignored["/var/run/arc/adb"] = struct{}{}
// Ignore /data since this is a side-effect of /home/root in
// the root mount namespace being marked MS_SHARED.
// TODO(crbug.com/1069501): Remove once bug is fixed.
ignored["/data"] = struct{}{}
if len(ignored) > 0 {
var paths []string
for p := range ignored {
paths = append(paths, p)
}
s.Log("Ignored mount paths: ", paths)
}
peerGroups := make(map[int]struct{})
for _, ms := range [][]sysutil.MountInfo{arcMs, adbd, sdcard, mountPassthrough, obb} {
for _, m := range ms {
if m.Shared > 0 {
peerGroups[m.Shared] = struct{}{}
}
}
}
for _, m := range arcMs {
if _, ok := ignored[m.MountPath]; ok {
continue
}
// Masters of all non-allowed SLAVE mount points in ARC
// container must be in containers.
if m.Master == 0 {
continue
}
if _, ok := peerGroups[m.Master]; !ok {
s.Error("Unexpected slave mount at ", m.MountPath)
}
}
}
// RunTest exercises the ARC related mount point conditions.
func RunTest(ctx context.Context, s *testing.State, a *arc.ARC) {
global, err := sysutil.MountInfoForPID(sysutil.SelfPID)
if err != nil {
s.Fatal("Failed to get mountinfo list: ", err)
}
arc, err := arcMounts()
if err != nil {
s.Fatal("Failed to get mountinfo list for ARC: ", err)
}
adbd, err := adbdMounts(ctx)
if err != nil {
s.Fatal("Failed to get mountinfo list for arc-adbd: ", err)
}
sdcard, err := sdcardMounts()
if err != nil {
s.Fatal("Failed to get mountinfo list for sdcard: ", err)
}
mountPassthrough, err := mountPassthroughMounts(ctx)
if err != nil {
s.Fatal("Failed to get mountinfo list for mount-passthrough: ", err)
}
obb, err := obbMounts(ctx)
if err != nil {
s.Fatal("Failed to get mountinfo list for arc-obb-mounter: ", err)
}
testNoARCLeak(ctx, s, global)
testNoARCSharedLeak(ctx, s, arc, joinMounts(global, adbd, sdcard, obb))
testDebugfsTracefs(ctx, s, arc)
testCgroup(ctx, s, arc)
testADBD(ctx, s, adbd)
testSDCard(ctx, s, sdcard)
testMountPassthrough(ctx, s, mountPassthrough)
testOBBMount(ctx, s, obb)
testMountShared(ctx, s, arc, adbd, sdcard, mountPassthrough, obb)
}