blob: 891cf3fa0f6589de3577abd63dd168375ac3b53e [file] [log] [blame]
// Copyright 2021 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 hwsec
import (
"context"
"fmt"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
"github.com/shirou/gopsutil/v3/disk"
"chromiumos/tast/errors"
"chromiumos/tast/testing"
"chromiumos/tast/timing"
)
const (
// GuestUser is the name representing a guest user account.
// Defined in libbrillo/brillo/cryptohome.cc.
GuestUser = "$guest"
// KioskUser is the name representing a kiosk user account.
KioskUser = "kiosk"
// WaitForUserTimeout is the maximum time until a user mount is available.
WaitForUserTimeout = 80 * time.Second
// mounterExe is the full executable for the out-of-process cryptohome
// mounter.
// Defined in cryptohome/BUILD.gn.
mounterExe = "/usr/sbin/cryptohome-namespace-mounter"
// cryptohomedExe is the full path of the daemon cryptohomed.
//
// TODO(crbug.com/1074735): Remove this and use mounterExe above for
// normal user mounts as well once the out-of-process cryptohome
// mounter is ready for that.
cryptohomedExe = "/usr/sbin/cryptohomed"
// mountPollInterval contains the delay between WaitForUserMount's parses of mtab.
mountPollInterval = 10 * time.Millisecond
// The user session mount namespace path.
mountNsPath = "/run/namespaces/mnt_chrome"
// The root directory of vault.
shadowRoot = "/home/.shadow"
)
var (
// matches a path to vault under /home/shadow.
// Example: "/home/.shadow/118c4648065f5cd3660e17a53533ec7bc924d01f/vault"
shadowRegexp = regexp.MustCompile(`^/home/\.shadow/[^/]*/vault$`)
// matches a path to /dev/*.
// Example: "/dev/sda1"
devRegexp = regexp.MustCompile(`^/dev(/[^/]*){1,2}$`)
// matches a path to /dev/loop\d+.
// Example: "/dev/loop0"
devLoopRegexp = regexp.MustCompile(`^/dev/loop[0-9]+$`)
)
// MountType is a type of the user mount.
type MountType int
const (
// Ephemeral is used to specify that the expected user mount type is ephemeral.
Ephemeral MountType = iota
// Permanent is used to specify that the expected user mount type is permanent.
Permanent
)
// CryptohomeMountInfo is a helper to get cryptohome mount information.
type CryptohomeMountInfo struct {
runner CmdRunner
cryptohome *CryptohomeClient
}
// NewCryptohomeMountInfo creates a new CryptohomeMountInfo
func NewCryptohomeMountInfo(r CmdRunner, c *CryptohomeClient) *CryptohomeMountInfo {
return &CryptohomeMountInfo{r, c}
}
// IsMounted checks if the vault for the user is mounted.
func (c *CryptohomeMountInfo) IsMounted(ctx context.Context, user string) (bool, error) {
mounter := cryptohomedExe
validatePartition := validatePermanentPartition
if user == GuestUser {
mounter = mounterExe
validatePartition = validateGuestPartition
}
userpath, err := c.cryptohome.GetHomeUserPath(ctx, user)
if err != nil {
return false, errors.Wrap(err, "failed to get user home path")
}
systempath, err := c.cryptohome.GetRootUserPath(ctx, user)
if err != nil {
return false, errors.Wrap(err, "failed to get user root path")
}
partitions, err := c.findMounts(ctx, mounter)
if err != nil {
return false, errors.Wrap(err, "failed to find mount points")
}
up := findPartition(partitions, userpath)
if up == nil {
return false, nil
}
if !validatePartition(up) {
return false, nil
}
sp := findPartition(partitions, systempath)
if sp == nil {
return false, nil
}
if !validatePartition(sp) {
return false, nil
}
return true, nil
}
// CleanUpMount cleans up the mount point for the user, and check it's unmounted.
func (c *CryptohomeMountInfo) CleanUpMount(ctx context.Context, user string) error {
if _, err := c.cryptohome.Unmount(ctx, user); err != nil {
return errors.Wrap(err, "failed to unmount")
}
if _, err := c.cryptohome.RemoveVault(ctx, user); err != nil {
return errors.Wrap(err, "failed to remove vault")
}
mounted, err := c.IsMounted(ctx, user)
if err != nil {
return errors.Wrap(err, "failed to get mount info")
}
if mounted {
return errors.Errorf("mount point of %q still exists", user)
}
return nil
}
// WaitForUserMountAndValidateType waits for user's encrypted home directory to
// be mounted and validates that it is of correct type.
func (c *CryptohomeMountInfo) WaitForUserMountAndValidateType(ctx context.Context, user string, mountType MountType) error {
ctx, st := timing.Start(ctx, "wait_for_user_mount")
defer st.End()
mounter := cryptohomedExe
validatePartition := validatePermanentPartition
if mountType == Ephemeral {
mounter = mounterExe
validatePartition = validateGuestPartition
}
userpath, err := c.cryptohome.GetHomeUserPath(ctx, user)
if err != nil {
return errors.Wrap(err, "failed to get user home path")
}
systempath, err := c.cryptohome.GetRootUserPath(ctx, user)
if err != nil {
return errors.Wrap(err, "failed to get user root path")
}
testing.ContextLogf(ctx, "Waiting for cryptohome for user %q with timeout %v", user, WaitForUserTimeout)
err = testing.Poll(ctx, func(ctx context.Context) error {
partitions, err := c.findMounts(ctx, mounter)
if err != nil {
return err
}
up := findPartition(partitions, userpath)
if up == nil {
return errors.Errorf("%v not found", userpath)
}
if !validatePartition(up) {
return errors.Errorf("%v not a valid partition", up)
}
sp := findPartition(partitions, systempath)
if sp == nil {
return errors.Errorf("%v not found", systempath)
}
if !validatePartition(sp) {
return errors.Errorf("%v not a valid partition", sp)
}
return nil
}, &testing.PollOptions{Timeout: WaitForUserTimeout, Interval: mountPollInterval})
if err != nil {
return errors.Wrapf(err, "not mounted for %s", user)
}
return nil
}
// WaitForUserMount waits for user's encrypted home directory to be mounted and
// validates that it is of permanent type for all users except guest.
func (c *CryptohomeMountInfo) WaitForUserMount(ctx context.Context, user string) error {
mountType := Permanent
if user == GuestUser {
mountType = Ephemeral
}
return c.WaitForUserMountAndValidateType(ctx, user, mountType)
}
// CheckMountNamespace checks whether the user session mount namespace has been created.
func (c *CryptohomeMountInfo) CheckMountNamespace(ctx context.Context) error {
mounts, err := c.readMountsInfo(ctx, "/proc/mounts")
if err != nil {
return errors.Wrap(err, "failed to read mount information")
}
mp := findPartition(mounts, mountNsPath)
if mp == nil {
return errors.Errorf("%v not found", mountNsPath)
}
if mp.Fstype != "nsfs" {
return errors.Errorf("user session mount namespace has not been created at %s", mountNsPath)
}
return nil
}
// UserCryptohomePath returns the path where the cryptohome data for the user is located.
func (c *CryptohomeMountInfo) UserCryptohomePath(ctx context.Context, user string) (string, error) {
hash, err := c.cryptohome.GetUserHash(ctx, user)
if err != nil {
return "", err
}
return filepath.Join(shadowRoot, hash), nil
}
// MountedVaultPath returns the path where the decrypted data for the user is located.
func (c *CryptohomeMountInfo) MountedVaultPath(ctx context.Context, user string) (string, error) {
path, err := c.UserCryptohomePath(ctx, user)
if err != nil {
return "", err
}
return filepath.Join(path, "mount"), nil
}
// findMounterPID finds the pid of the given mounter process.
func (c *CryptohomeMountInfo) findMounterPID(ctx context.Context, mounter string) (int32, error) {
bs, err := c.runner.Run(ctx, "pidof", "-s", mounter)
var ee *CmdExitError
// If the mounter process is not found, don't return an error.
if errors.As(err, &ee) && ee.ExitCode == 1 {
return -1, nil
}
if err != nil {
return -1, errors.Wrap(err, "could not get the mounter pid")
}
// Convert the result to pid
msg := string(bs)
pid, err := strconv.ParseInt(strings.TrimSpace(msg), 10, 32)
if err != nil {
return -1, errors.Wrapf(err, "failed to convert the pid %q", msg)
}
return int32(pid), nil
}
// readMountsInfo returns the list of mounts from a mount information file.
func (c *CryptohomeMountInfo) readMountsInfo(ctx context.Context, path string) ([]disk.PartitionStat, error) {
bs, err := c.runner.Run(ctx, "cat", "--", path)
if err != nil {
return nil, errors.Wrapf(err, "failed to get list of mounts from %v", path)
}
output := strings.Trim(string(bs), "\n")
mounts := strings.Split(output, "\n")
var res []disk.PartitionStat
for _, mount := range mounts {
fields := strings.Fields(mount)
// The mount information should match this format:
// https://man7.org/linux/man-pages/man5/fstab.5.html
if len(fields) != 6 {
return nil, errors.Errorf("mount information doesn't match 6 fields: %q", mount)
}
d := disk.PartitionStat{
Device: fields[0],
Mountpoint: fields[1],
Fstype: fields[2],
Opts: []string{fields[3]},
}
res = append(res, d)
}
return res, nil
}
// findMountsForPID returns the list of mounts in pid's mount namespace.
func (c *CryptohomeMountInfo) findMountsForPID(ctx context.Context, pid int32) ([]disk.PartitionStat, error) {
path := fmt.Sprintf("/proc/%d/mounts", pid)
res, err := c.readMountsInfo(ctx, path)
if err != nil {
return nil, errors.Wrapf(err, "failed to get list of mounts for pid %d", pid)
}
return res, nil
}
// findMounts returns the list of mounts in the given mounter's mount namespace.
func (c *CryptohomeMountInfo) findMounts(ctx context.Context, mounter string) ([]disk.PartitionStat, error) {
mounterPID, err := c.findMounterPID(ctx, mounter)
if err != nil {
return nil, errors.Wrap(err, "could not list running processes")
} else if mounterPID == -1 {
// If the mounter process is not running, the list of mounts is
// empty.
return nil, nil
}
return c.findMountsForPID(ctx, mounterPID)
}
// findPartition returns a pointer to the entry in ps corresponding to path,
// or nil if no matching entry is present.
func findPartition(ps []disk.PartitionStat, path string) *disk.PartitionStat {
for i := range ps {
if ps[i].Mountpoint == path {
return &ps[i]
}
}
return nil
}
// validatePermanentPartition checks if the given partition is valid for a
// (non-guest) user mount.
func validatePermanentPartition(p *disk.PartitionStat) bool {
switch p.Fstype {
case "ext4":
if !devRegexp.MatchString(p.Device) || devLoopRegexp.MatchString(p.Device) {
return false
}
case "ecryptfs":
if !shadowRegexp.MatchString(p.Device) {
return false
}
default:
return false
}
return true
}
// validateGuestPartition checks if the given partition is valid for a guest
// user mount.
func validateGuestPartition(p *disk.PartitionStat) bool {
switch p.Fstype {
case "ext4":
if !devLoopRegexp.MatchString(p.Device) {
return false
}
case "tmpfs":
if p.Device != "guestfs" {
return false
}
default:
return false
}
return true
}