blob: 405424fc5087c6c99f9ca6b7fe5e10f28d484d7e [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"
"regexp"
"strconv"
"strings"
"github.com/shirou/gopsutil/disk"
"chromiumos/tast/errors"
)
const (
// GuestUser is the name representing a guest user account.
// Defined in libbrillo/brillo/cryptohome.cc.
GuestUser = "$guest"
// 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"
)
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]+$`)
)
// 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
}
// 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
}
// 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)
bs, err := c.runner.Run(ctx, "cat", "--", path)
if err != nil {
return nil, errors.Wrapf(err, "failed to get list of mounts for pid %d", pid)
}
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: fields[3],
}
res = append(res, d)
}
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
}