blob: c7bb8c6a49776f7370cc9901c9503e5fb2eb2da0 [file] [log] [blame] [edit]
// Copyright 2021 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package main
import (
"io"
"log"
"os"
"syscall"
lxd "github.com/lxc/lxd/client"
"github.com/lxc/lxd/shared"
)
type InstanceFileServer interface {
GetInstanceFile(path string) (content io.ReadCloser, resp *lxd.InstanceFileResponse, err error)
CreateInstanceFile(path string, args lxd.InstanceFileArgs) (err error)
DeleteInstanceFile(path string) (err error)
}
type FsInstanceFileServer struct {
containerName string
needsRemap bool
}
var cfsOverride InstanceFileServer
// OverrideInstanceFileServerForTesting overrides the container file server
// that NewInstanceFileServer will create, so you can inject a
// mock/fake/stub/etc for testing.
func OverrideInstanceFileServerForTesting(cfs InstanceFileServer) {
cfsOverride = cfs
}
// NewInstanceFileServer creates a new FsInstanceFileServer (i.e. one backed
// by the filesystem).
func NewInstanceFileServer(containerName string) InstanceFileServer {
if cfsOverride != nil {
return cfsOverride
}
// Old containers do not use idmapped mounts, so remapping is required.
needsRemap := false
var s syscall.Stat_t
if err := syscall.Stat(shared.VarPath("containers", containerName, "rootfs"), &s); err != nil {
log.Printf("Failed to check if remap needed for container %q, assuming no: %v", containerName, err)
} else {
if s.Uid >= lxdSubuidStart {
needsRemap = true
}
}
return &FsInstanceFileServer{
containerName: containerName,
needsRemap: needsRemap,
}
}
// mapIdToContainerNamespace maps a uid or gid from host number to container number.
func (cfs *FsInstanceFileServer) mapIdToContainerNamespace(id int) int {
if !cfs.needsRemap || id == PrimaryUserID || id == ChronosAccessID || id == AndroidRootID || id == AndroidEverybodyID {
return id
}
return id + 1000000
}
// mapIdFromContainerNamespace maps a uid or gid from container number to host number.
func (cfs *FsInstanceFileServer) mapIdFromContainerNamespace(id int) int {
if !cfs.needsRemap || id == PrimaryUserID || id == ChronosAccessID || id == AndroidRootID || id == AndroidEverybodyID {
return id
} else if id < 1000000 {
// Before we first start the container our IDs will all be unmapped
// (e.g. /etc/skel in the container is owned by UID 0), so if our ID is
// below 1000000 assume it's not mapped otherwise assume it is.
// TODO(crbug/1244460): Try getting the actual UID map from LXD and using
// that.
return id
}
return id - 1000000
}
// containerPathToHost maps a path from something relative to the container root
// to an absolute path.
func (cfs *FsInstanceFileServer) containerPathToHost(path string) string {
return shared.VarPath("containers", cfs.containerName, "rootfs", path)
}
// GetInstanceFile reads a file or directory from the container given a path
// relative to the container root. If the file is a directory then content is
// empty and resp.entries contains a list of files, otherwise content contains
// the file contents and entries is empty. If you call this on anything other
// than a file or directory (e.g. a symlink) it probably won't do what you want.
// Note: Mode inside resp is the mode you get from the stat syscall, this is
// not a FileMode, and different from what os.Stat gives you.
// Note: GID and UID in resp are container IDs (e.g. a file owned by container
// root will have UID 0, even though on the filesystem its UID will be 1000000).
func (cfs *FsInstanceFileServer) GetInstanceFile(path string) (content io.ReadCloser, resp *lxd.InstanceFileResponse, err error) {
p := cfs.containerPathToHost(path)
var s syscall.Stat_t
if err := syscall.Stat(p, &s); err != nil {
return nil, nil, err
}
var t string
var entries []string
var reader io.ReadCloser
// Careful! s.Mode from syscall.Stat is very similar, but not quite the
// same, as the FileMode you get from os.Stat. So we do our own masking
// instead of using FileMode.IsDir().
if (s.Mode & syscall.S_IFDIR) != 0 {
t = "directory"
list, err := os.ReadDir(p)
if err != nil {
return nil, nil, err
}
for _, l := range list {
entries = append(entries, l.Name())
}
reader = nil
} else {
t = "file"
entries = make([]string, 0)
reader, err = os.Open(p)
if err != nil {
return nil, nil, err
}
}
resp = &lxd.InstanceFileResponse{
UID: int64(cfs.mapIdFromContainerNamespace(int(s.Uid))),
GID: int64(cfs.mapIdFromContainerNamespace(int(s.Gid))),
Mode: int(s.Mode),
Type: t,
Entries: entries,
}
return reader, resp, nil
}
// CreateInstanceFile creates a file or directory at the given path relative to
// the container root. if args.Type is "directory" it will create the directory
// and any parents if needed, if args.Type is "file" then a file will be created
// with the contents of args.Content, unless args.Content is nil, in which case
// the file will be created with the specified mode if it doesn't exist, or if
// the file does already exist nothing will happen.
// The mode, UID and GID of the target will always be set to that provided, even
// if the file or directory already existed. If parent directories were created
// then the parents will have the specified mode but the owner will be the user
// tremplin is running as, any parent directories which already existed will be
// unchanged.
// Note: UID and GID are container ids (e.g. root is 0, not 1000000).
// Note: Only file and directory are supported, anything else e.g. symlinks will
// panic.
// Note: if args.Type is "file" and args.Content != nil then args.WriteMode must
// be "overwrite", anything else is unsupported. If args.Type is "directory" or
// args.Type is "file" and args.Content is nil then args.WriteMode is ignored.
func (cfs *FsInstanceFileServer) CreateInstanceFile(path string, args lxd.InstanceFileArgs) (err error) {
p := cfs.containerPathToHost(path)
mode := os.FileMode(args.Mode)
if args.Type == "directory" {
if err := os.MkdirAll(p, mode); err != nil {
return err
}
} else if args.Type == "file" {
if args.Content != nil {
if args.WriteMode != "overwrite" {
log.Panic("Only overwrite supported by CreateInstanceFile")
}
var b []byte
b, err = io.ReadAll(args.Content)
if err != nil {
return err
}
if err := os.WriteFile(p, b, mode); err != nil {
return err
}
} else {
file, err := os.OpenFile(p, os.O_CREATE, mode)
if err != nil {
return err
}
file.Close()
}
} else {
log.Panic("Only file and directory types are supported by CreateInstanceFile")
}
if err := os.Chown(p, cfs.mapIdToContainerNamespace(int(args.UID)), cfs.mapIdToContainerNamespace(int(args.GID))); err != nil {
return err
}
log.Printf("Creating container file with metadata: %+v\n", args)
// We set the mode above, but if the file already exists its mode won't be
// changed so we always set the mode again here.
return os.Chmod(p, mode)
}
// DeleteInstanceFile deletes a file or directory in the container at path
// relative to the container root. If the target is a directory will also delete
// everything within the directory,
func (cfs *FsInstanceFileServer) DeleteInstanceFile(path string) (err error) {
return os.RemoveAll(cfs.containerPathToHost(path))
}