mirror of
https://github.com/kata-containers/kata-containers.git
synced 2025-08-02 00:02:01 +00:00
Merge pull request #2432 from fengwang666/qemu-rootless
runtime: run the QEMU VMM process with a non-root user
This commit is contained in:
commit
2bee8bc6bd
@ -24,6 +24,11 @@ machine_type = "@MACHINETYPE@"
|
||||
# Default false
|
||||
# confidential_guest = true
|
||||
|
||||
# Enable running QEMU VMM as a non-root user.
|
||||
# By default QEMU VMM run as root. When this is set to true, QEMU VMM process runs as
|
||||
# a non-root random user. See documentation for the limitations of this mode.
|
||||
# rootless = true
|
||||
|
||||
# List of valid annotation names for the hypervisor
|
||||
# Each member of the list is a regular expression, which is the base name
|
||||
# of the annotation, e.g. "path" for io.katacontainers.config.hypervisor.path"
|
||||
|
@ -10,8 +10,15 @@ package containerdshim
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/kata-containers/kata-containers/src/runtime/pkg/utils"
|
||||
"github.com/kata-containers/kata-containers/src/runtime/virtcontainers/pkg/rootless"
|
||||
"math/rand"
|
||||
"os"
|
||||
"os/user"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
containerd_types "github.com/containerd/containerd/api/types"
|
||||
"github.com/containerd/containerd/mount"
|
||||
@ -103,6 +110,12 @@ func create(ctx context.Context, s *service, r *taskAPI.CreateTaskRequest) (*con
|
||||
}()
|
||||
|
||||
katautils.HandleFactory(ctx, vci, s.config)
|
||||
rootless.SetRootless(s.config.HypervisorConfig.Rootless)
|
||||
if rootless.IsRootless() {
|
||||
if err := configureNonRootHypervisor(s.config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Pass service's context instead of local ctx to CreateSandbox(), since local
|
||||
// ctx will be canceled after this rpc service call, but the sandbox will live
|
||||
@ -259,3 +272,112 @@ func doMount(mounts []*containerd_types.Mount, rootfs string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func configureNonRootHypervisor(runtimeConfig *oci.RuntimeConfig) error {
|
||||
userName, err := createVmmUser()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
removeVmmUser(userName)
|
||||
}
|
||||
}()
|
||||
|
||||
u, err := user.Lookup(userName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uid, err := strconv.Atoi(u.Uid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gid, err := strconv.Atoi(u.Gid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
runtimeConfig.HypervisorConfig.Uid = uint32(uid)
|
||||
runtimeConfig.HypervisorConfig.Gid = uint32(gid)
|
||||
|
||||
userTmpDir := path.Join("/run/user/", fmt.Sprint(uid))
|
||||
dir, err := os.Stat(userTmpDir)
|
||||
if os.IsNotExist(err) {
|
||||
if err = os.Mkdir(userTmpDir, vc.DirMode); err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
if err = os.RemoveAll(userTmpDir); err != nil {
|
||||
shimLog.WithField("userTmpDir", userTmpDir).WithError(err).Warn("failed to remove userTmpDir")
|
||||
}
|
||||
}
|
||||
}()
|
||||
if err = syscall.Chown(userTmpDir, uid, gid); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if dir != nil && !dir.IsDir() {
|
||||
return fmt.Errorf("%s is expected to be a directory", userTmpDir)
|
||||
}
|
||||
|
||||
if err := os.Setenv("XDG_RUNTIME_DIR", userTmpDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
info, err := os.Stat("/dev/kvm")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if stat, ok := info.Sys().(*syscall.Stat_t); ok {
|
||||
// Add the kvm group to the hypervisor supplemental group so that the hypervisor process can access /dev/kvm
|
||||
runtimeConfig.HypervisorConfig.Groups = append(runtimeConfig.HypervisorConfig.Groups, stat.Gid)
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to get the gid of /dev/kvm")
|
||||
}
|
||||
|
||||
func createVmmUser() (string, error) {
|
||||
var (
|
||||
err error
|
||||
userName string
|
||||
)
|
||||
|
||||
useraddPath, err := utils.FirstValidExecutable([]string{"/usr/sbin/useradd", "/sbin/useradd", "/bin/useradd"})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
nologinPath, err := utils.FirstValidExecutable([]string{"/usr/sbin/nologin", "/sbin/nologin", "/bin/nologin"})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Add retries to mitigate temporary errors and race conditions. For example, the user already exists
|
||||
// or another instance of the runtime is also creating a user.
|
||||
maxAttempt := 5
|
||||
for i := 0; i < maxAttempt; i++ {
|
||||
userName = fmt.Sprintf("kata-%v", rand.Intn(100000))
|
||||
_, err = utils.RunCommand([]string{useraddPath, "-M", "-s", nologinPath, userName, "-c", "\"Kata Containers temporary hypervisor user\""})
|
||||
if err == nil {
|
||||
return userName, nil
|
||||
}
|
||||
shimLog.WithField("attempt", i+1).WithField("username", userName).
|
||||
WithError(err).Warn("failed to add user, will try again")
|
||||
}
|
||||
return "", fmt.Errorf("could not create VMM user: %v", err)
|
||||
}
|
||||
|
||||
func removeVmmUser(user string) {
|
||||
userdelPath, err := utils.FirstValidExecutable([]string{"/usr/sbin/userdel", "/sbin/userdel", "/bin/userdel"})
|
||||
if err != nil {
|
||||
shimLog.WithField("username", user).WithError(err).Warn("failed to remove user")
|
||||
}
|
||||
// Add retries to mitigate temporary errors and race conditions.
|
||||
for i := 0; i < 5; i++ {
|
||||
_, err := utils.RunCommand([]string{userdelPath, "-f", user})
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
shimLog.WithField("username", user).WithField("attempt", i+1).WithError(err).Warn("failed to remove user")
|
||||
}
|
||||
}
|
||||
|
@ -86,6 +86,7 @@ const defaultRxRateLimiterMaxRate = uint64(0)
|
||||
const defaultTxRateLimiterMaxRate = uint64(0)
|
||||
const defaultConfidentialGuest = false
|
||||
const defaultGuestSwap = false
|
||||
const defaultRootlessHypervisor = false
|
||||
|
||||
var defaultSGXEPCSize = int64(0)
|
||||
|
||||
|
@ -105,15 +105,15 @@ type hypervisor struct {
|
||||
EnableAnnotations []string `toml:"enable_annotations"`
|
||||
RxRateLimiterMaxRate uint64 `toml:"rx_rate_limiter_max_rate"`
|
||||
TxRateLimiterMaxRate uint64 `toml:"tx_rate_limiter_max_rate"`
|
||||
MemOffset uint64 `toml:"memory_offset"`
|
||||
VirtioFSCacheSize uint32 `toml:"virtio_fs_cache_size"`
|
||||
NumVCPUs int32 `toml:"default_vcpus"`
|
||||
DefaultMaxVCPUs uint32 `toml:"default_maxvcpus"`
|
||||
MemorySize uint32 `toml:"default_memory"`
|
||||
MemSlots uint32 `toml:"memory_slots"`
|
||||
MemOffset uint64 `toml:"memory_offset"`
|
||||
DefaultBridges uint32 `toml:"default_bridges"`
|
||||
Msize9p uint32 `toml:"msize_9p"`
|
||||
PCIeRootPort uint32 `toml:"pcie_root_port"`
|
||||
NumVCPUs int32 `toml:"default_vcpus"`
|
||||
BlockDeviceCacheSet bool `toml:"block_device_cache_set"`
|
||||
BlockDeviceCacheDirect bool `toml:"block_device_cache_direct"`
|
||||
BlockDeviceCacheNoflush bool `toml:"block_device_cache_noflush"`
|
||||
@ -134,6 +134,7 @@ type hypervisor struct {
|
||||
GuestMemoryDumpPaging bool `toml:"guest_memory_dump_paging"`
|
||||
ConfidentialGuest bool `toml:"confidential_guest"`
|
||||
GuestSwap bool `toml:"enable_guest_swap"`
|
||||
Rootless bool `toml:"rootless"`
|
||||
}
|
||||
|
||||
type runtime struct {
|
||||
@ -713,6 +714,7 @@ func newQemuHypervisorConfig(h hypervisor) (vc.HypervisorConfig, error) {
|
||||
GuestMemoryDumpPaging: h.GuestMemoryDumpPaging,
|
||||
ConfidentialGuest: h.ConfidentialGuest,
|
||||
GuestSwap: h.GuestSwap,
|
||||
Rootless: h.Rootless,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -1069,6 +1071,7 @@ func GetDefaultHypervisorConfig() vc.HypervisorConfig {
|
||||
SGXEPCSize: defaultSGXEPCSize,
|
||||
ConfidentialGuest: defaultConfidentialGuest,
|
||||
GuestSwap: defaultGuestSwap,
|
||||
Rootless: defaultRootlessHypervisor,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -80,3 +80,22 @@ func EnsureDir(path string, mode os.FileMode) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func FirstValidExecutable(paths []string) (string, error) {
|
||||
for _, p := range paths {
|
||||
info, err := os.Stat(p)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
mode := info.Mode()
|
||||
// check whether the file is an executable
|
||||
if mode&0111 == 0 {
|
||||
continue
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
return "", fmt.Errorf("all the executables are invalid")
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@ -117,3 +118,80 @@ func TestEnsureDir(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFirstValidExecutable(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
tmpdir, err := ioutil.TempDir("", "TestFirstValidPath")
|
||||
assert.NoError(err)
|
||||
defer os.RemoveAll(tmpdir)
|
||||
|
||||
// nolint: govet
|
||||
testCases := []struct {
|
||||
before func()
|
||||
paths []string
|
||||
validPath string
|
||||
isValid bool
|
||||
msg string
|
||||
}{
|
||||
{
|
||||
before: nil,
|
||||
paths: []string{"a/b/c", "c/d"},
|
||||
validPath: "",
|
||||
isValid: false,
|
||||
msg: "all the executables are invalid",
|
||||
},
|
||||
{
|
||||
before: func() {
|
||||
err := os.MkdirAll(path.Join(tmpdir, "a", "b"), 0755)
|
||||
assert.NoError(err)
|
||||
// create a non-executable file
|
||||
err = ioutil.WriteFile(path.Join(tmpdir, "a", "b", "c"), []byte("test\n"), 0644)
|
||||
assert.NoError(err)
|
||||
},
|
||||
paths: []string{path.Join(tmpdir, "a", "b", "c"), "c/d"},
|
||||
validPath: "",
|
||||
isValid: false,
|
||||
msg: "all the executables are invalid",
|
||||
},
|
||||
{
|
||||
before: func() {
|
||||
err := os.MkdirAll(path.Join(tmpdir, "d", "e"), 0755)
|
||||
assert.NoError(err)
|
||||
// create an executable file
|
||||
err = ioutil.WriteFile(path.Join(tmpdir, "d", "e", "f"), []byte("test\n"), 0755)
|
||||
assert.NoError(err)
|
||||
},
|
||||
paths: []string{path.Join(tmpdir, "d", "e", "f"), "c/d"},
|
||||
validPath: fmt.Sprintf("%s/d/e/f", tmpdir),
|
||||
isValid: true,
|
||||
msg: "",
|
||||
},
|
||||
{
|
||||
before: func() {
|
||||
err := os.MkdirAll(path.Join(tmpdir, "g", "h"), 0755)
|
||||
assert.NoError(err)
|
||||
// create an executable file
|
||||
err = ioutil.WriteFile(path.Join(tmpdir, "g", "h", "i"), []byte("test\n"), 0755)
|
||||
assert.NoError(err)
|
||||
},
|
||||
paths: []string{"c/d", path.Join(tmpdir, "g", "h", "i")},
|
||||
validPath: path.Join(tmpdir, "g", "h", "i"),
|
||||
isValid: true,
|
||||
msg: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
if tc.before != nil {
|
||||
tc.before()
|
||||
}
|
||||
path, err := FirstValidExecutable(tc.paths)
|
||||
assert.Equal(tc.isValid, err == nil)
|
||||
if tc.isValid {
|
||||
assert.Equal(tc.validPath, path)
|
||||
} else {
|
||||
assert.Equal(err.Error(), tc.msg)
|
||||
assert.Equal(tc.validPath, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/kata-containers/kata-containers/src/runtime/virtcontainers/persist/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@ -136,6 +137,11 @@ func TestCreateSandboxNoopAgentSuccessful(t *testing.T) {
|
||||
}
|
||||
defer cleanUp()
|
||||
|
||||
// Pre-create the directory path to avoid panic error. Without this change, ff the test is run as a non-root user,
|
||||
// this test will fail because of permission denied error in chown syscall in the utils.MkdirAllWithInheritedOwner() method
|
||||
err := os.MkdirAll(fs.MockRunStoragePath(), DirMode)
|
||||
assert.NoError(err)
|
||||
|
||||
config := newTestSandboxConfigNoop()
|
||||
|
||||
ctx := WithNewAgentFunc(context.Background(), newMockAgent)
|
||||
|
@ -228,6 +228,9 @@ type HypervisorConfig struct {
|
||||
// it will be used for the sandbox's kernel path instead of KernelPath.
|
||||
customAssets map[types.AssetType]*types.Asset
|
||||
|
||||
// Supplementary group IDs.
|
||||
Groups []uint32
|
||||
|
||||
// KernelPath is the guest kernel host path.
|
||||
KernelPath string
|
||||
|
||||
@ -382,6 +385,12 @@ type HypervisorConfig struct {
|
||||
// VirtioFSCacheSize is the DAX cache size in MiB
|
||||
VirtioFSCacheSize uint32
|
||||
|
||||
// User ID.
|
||||
Uid uint32
|
||||
|
||||
// Group ID.
|
||||
Gid uint32
|
||||
|
||||
// BlockDeviceCacheSet specifies cache-related options will be set to block devices or not.
|
||||
BlockDeviceCacheSet bool
|
||||
|
||||
@ -461,6 +470,9 @@ type HypervisorConfig struct {
|
||||
|
||||
// GuestSwap Used to enable/disable swap in the guest
|
||||
GuestSwap bool
|
||||
|
||||
// Rootless is used to enable rootless VMM process
|
||||
Rootless bool
|
||||
}
|
||||
|
||||
// vcpu mapping from vcpu number to thread number
|
||||
|
@ -9,6 +9,7 @@ package fs
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/kata-containers/kata-containers/src/runtime/virtcontainers/utils"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@ -86,7 +87,7 @@ func (fs *FS) ToDisk(ss persistapi.SandboxState, cs map[string]persistapi.Contai
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(sandboxDir, dirMode); err != nil {
|
||||
if err := utils.MkdirAllWithInheritedOwner(sandboxDir, dirMode); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -123,7 +124,7 @@ func (fs *FS) ToDisk(ss persistapi.SandboxState, cs map[string]persistapi.Contai
|
||||
// persist container configuration data
|
||||
for cid, cstate := range fs.containerState {
|
||||
cdir := filepath.Join(sandboxDir, cid)
|
||||
if dirCreationErr = os.MkdirAll(cdir, dirMode); dirCreationErr != nil {
|
||||
if dirCreationErr = utils.MkdirAllWithInheritedOwner(cdir, dirMode); dirCreationErr != nil {
|
||||
return dirCreationErr
|
||||
}
|
||||
createdDirs = append(createdDirs, cdir)
|
||||
@ -288,7 +289,7 @@ func (fs *FS) GlobalWrite(relativePath string, data []byte) error {
|
||||
|
||||
_, err = os.Stat(dir)
|
||||
if os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(dir, dirMode); err != nil {
|
||||
if err := utils.MkdirAllWithInheritedOwner(dir, dirMode); err != nil {
|
||||
fs.Logger().WithError(err).WithField("directory", dir).Error("failed to create dir")
|
||||
return err
|
||||
}
|
||||
|
@ -223,6 +223,9 @@ const (
|
||||
|
||||
// EnableGuestSwap is a sandbox annotation to enable swap in the guest.
|
||||
EnableGuestSwap = kataAnnotHypervisorPrefix + "enable_guest_swap"
|
||||
|
||||
// EnableRootlessHypervisor is a sandbox annotation to enable rootless hypervisor (only supported in QEMU currently).
|
||||
EnableRootlessHypervisor = kataAnnotHypervisorPrefix + "rootless"
|
||||
)
|
||||
|
||||
// Runtime related annotations
|
||||
|
@ -622,6 +622,12 @@ func addHypervisorMemoryOverrides(ocispec specs.Spec, sbConfig *vc.SandboxConfig
|
||||
return err
|
||||
}
|
||||
|
||||
if err := newAnnotationConfiguration(ocispec, vcAnnotations.EnableRootlessHypervisor).setBool(func(enableRootlessHypervisor bool) {
|
||||
sbConfig.HypervisorConfig.Rootless = enableRootlessHypervisor
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -23,6 +23,7 @@ import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"github.com/kata-containers/kata-containers/src/runtime/virtcontainers/utils"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
@ -106,7 +107,7 @@ func NewNS() (ns.NetNS, error) {
|
||||
// Create the directory for mounting network namespaces
|
||||
// This needs to be a shared mountpoint in case it is mounted in to
|
||||
// other namespaces (containers)
|
||||
err = os.MkdirAll(nsRunDir, 0755)
|
||||
err = utils.MkdirAllWithInheritedOwner(nsRunDir, 0755)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -11,9 +11,11 @@ import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/kata-containers/kata-containers/src/runtime/virtcontainers/pkg/rootless"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -83,6 +85,7 @@ type QemuState struct {
|
||||
}
|
||||
|
||||
// qemu is an Hypervisor interface implementation for the Linux qemu hypervisor.
|
||||
// nolint: govet
|
||||
type qemu struct {
|
||||
arch qemuArch
|
||||
|
||||
@ -271,7 +274,7 @@ func (q *qemu) setup(ctx context.Context, id string, hypervisorConfig *Hyperviso
|
||||
|
||||
// The path might already exist, but in case of VM templating,
|
||||
// we have to create it since the sandbox has not created it yet.
|
||||
if err = os.MkdirAll(filepath.Join(q.store.RunStoragePath(), id), DirMode); err != nil {
|
||||
if err = utils.MkdirAllWithInheritedOwner(filepath.Join(q.store.RunStoragePath(), id), DirMode); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@ -583,6 +586,9 @@ func (q *qemu) createSandbox(ctx context.Context, id string, networkNS NetworkNa
|
||||
UUID: q.state.UUID,
|
||||
Path: qemuPath,
|
||||
Ctx: q.qmpMonitorCh.ctx,
|
||||
Uid: q.config.Uid,
|
||||
Gid: q.config.Gid,
|
||||
Groups: q.config.Groups,
|
||||
Machine: machine,
|
||||
SMP: smp,
|
||||
Memory: memory,
|
||||
@ -775,10 +781,11 @@ func (q *qemu) startSandbox(ctx context.Context, timeout int) error {
|
||||
}()
|
||||
|
||||
vmPath := filepath.Join(q.store.RunVMStoragePath(), q.id)
|
||||
err := os.MkdirAll(vmPath, DirMode)
|
||||
err := utils.MkdirAllWithInheritedOwner(vmPath, DirMode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
q.Logger().WithField("vm path", vmPath).Info("created vm path")
|
||||
// append logfile only on debug
|
||||
if q.config.Debug {
|
||||
q.qemuConfig.LogFile = filepath.Join(vmPath, "qemu.log")
|
||||
@ -1007,6 +1014,27 @@ func (q *qemu) cleanupVM() error {
|
||||
}
|
||||
}
|
||||
|
||||
if rootless.IsRootless() {
|
||||
rootlessDir := os.Getenv("XDG_RUNTIME_DIR")
|
||||
if err := os.RemoveAll(rootlessDir); err != nil {
|
||||
q.Logger().WithError(err).WithField("root-path", rootlessDir).
|
||||
Warnf("failed to remove vm run-as-user root path")
|
||||
}
|
||||
|
||||
u, err := user.LookupId(strconv.Itoa(int(q.config.Uid)))
|
||||
if err != nil {
|
||||
q.Logger().WithError(err).WithField("uid", q.config.Uid).Warn("failed to find the user")
|
||||
}
|
||||
userdelPath, err := pkgUtils.FirstValidExecutable([]string{"/usr/sbin/userdel", "/sbin/userdel", "/bin/userdel"})
|
||||
if err != nil {
|
||||
q.Logger().WithError(err).WithField("user", u.Username).Warn("failed to delete the user")
|
||||
}
|
||||
_, err = pkgUtils.RunCommand([]string{userdelPath, "-f", u.Username})
|
||||
if err != nil {
|
||||
q.Logger().WithError(err).WithField("user", u.Username).Warn("failed to delete the user")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -390,3 +390,85 @@ outer:
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MkdirAllWithInheritedOwner creates a directory named path, along with any necessary parents.
|
||||
// It creates the missing directories with the ownership of the last existing parent.
|
||||
// The path needs to be absolute and the method doesn't handle symlink.
|
||||
func MkdirAllWithInheritedOwner(path string, perm os.FileMode) error {
|
||||
if len(path) == 0 {
|
||||
return fmt.Errorf("the path is empty")
|
||||
}
|
||||
|
||||
// By default, use the uid and gid of the calling process.
|
||||
var uid = os.Getuid()
|
||||
var gid = os.Getgid()
|
||||
|
||||
paths := getAllParentPaths(path)
|
||||
for _, curPath := range paths {
|
||||
info, err := os.Stat(curPath)
|
||||
|
||||
if err != nil {
|
||||
if err = os.Mkdir(curPath, perm); err != nil {
|
||||
return fmt.Errorf("mkdir call failed: %v", err.Error())
|
||||
}
|
||||
if err = syscall.Chown(curPath, uid, gid); err != nil {
|
||||
return fmt.Errorf("chown syscall failed: %v", err.Error())
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if !info.IsDir() {
|
||||
return &os.PathError{Op: "mkdir", Path: curPath, Err: syscall.ENOTDIR}
|
||||
}
|
||||
if stat, ok := info.Sys().(*syscall.Stat_t); ok {
|
||||
uid = int(stat.Uid)
|
||||
gid = int(stat.Gid)
|
||||
} else {
|
||||
return fmt.Errorf("fail to retrieve the uid and gid of path %s", curPath)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ChownToParent changes the owners of the path to the same of parent directory.
|
||||
// The path needs to be absolute and the method doesn't handle symlink.
|
||||
func ChownToParent(path string) error {
|
||||
if len(path) == 0 {
|
||||
return fmt.Errorf("the path is empty")
|
||||
}
|
||||
|
||||
if !filepath.IsAbs(path) {
|
||||
return fmt.Errorf("the path is not absolute")
|
||||
}
|
||||
|
||||
info, err := os.Stat(filepath.Dir(path))
|
||||
if err != nil {
|
||||
return fmt.Errorf("os.Stat() error on %s: %s", filepath.Dir(path), err.Error())
|
||||
}
|
||||
if stat, ok := info.Sys().(*syscall.Stat_t); ok {
|
||||
if err = syscall.Chown(path, int(stat.Uid), int(stat.Gid)); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("fail to retrieve the uid and gid of path %s", path)
|
||||
}
|
||||
|
||||
// getAllParentPaths returns all the parent directories of a path, including itself but excluding root directory "/".
|
||||
// For example, "/foo/bar/biz" returns {"/foo", "/foo/bar", "/foo/bar/biz"}
|
||||
func getAllParentPaths(path string) []string {
|
||||
if path == "/" || path == "." {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
paths := []string{filepath.Clean(path)}
|
||||
cur := path
|
||||
var parent string
|
||||
for cur != "/" && cur != "." {
|
||||
parent = filepath.Dir(cur)
|
||||
paths = append([]string{parent}, paths...)
|
||||
cur = parent
|
||||
}
|
||||
// remove the "/" or "." from the return result
|
||||
return paths[1:]
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
@ -469,3 +470,124 @@ func TestWaitLocalProcessInvalidPid(t *testing.T) {
|
||||
assert.Error(err, msg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMkdirAllWithInheritedOwnerSuccessful(t *testing.T) {
|
||||
if os.Getuid() != 0 {
|
||||
t.Skip("Test disabled as requires root user")
|
||||
}
|
||||
assert := assert.New(t)
|
||||
tmpDir1, err := ioutil.TempDir("", "test")
|
||||
assert.NoError(err)
|
||||
defer os.RemoveAll(tmpDir1)
|
||||
tmpDir2, err := ioutil.TempDir("", "test")
|
||||
assert.NoError(err)
|
||||
defer os.RemoveAll(tmpDir2)
|
||||
|
||||
testCases := []struct {
|
||||
before func(rootDir string, uid, gid int)
|
||||
rootDir string
|
||||
targetDir string
|
||||
uid int
|
||||
gid int
|
||||
}{
|
||||
{
|
||||
before: func(rootDir string, uid, gid int) {
|
||||
err = syscall.Chown(rootDir, uid, gid)
|
||||
assert.NoError(err)
|
||||
},
|
||||
rootDir: tmpDir1,
|
||||
targetDir: path.Join(tmpDir1, "foo", "bar"),
|
||||
uid: 1234,
|
||||
gid: 5678,
|
||||
},
|
||||
{
|
||||
before: func(rootDir string, uid, gid int) {
|
||||
// remove the tmpDir2 so the MkdirAllWithInheritedOwner() call creates them from /tmp
|
||||
err = os.RemoveAll(tmpDir2)
|
||||
assert.NoError(err)
|
||||
},
|
||||
rootDir: tmpDir2,
|
||||
targetDir: path.Join(tmpDir2, "foo", "bar"),
|
||||
uid: 0,
|
||||
gid: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
if tc.before != nil {
|
||||
tc.before(tc.rootDir, tc.uid, tc.gid)
|
||||
}
|
||||
|
||||
err := MkdirAllWithInheritedOwner(tc.targetDir, 0700)
|
||||
assert.NoError(err)
|
||||
// remove the first parent "/tmp" from the assertion as it's owned by root
|
||||
for _, p := range getAllParentPaths(tc.targetDir)[1:] {
|
||||
info, err := os.Stat(p)
|
||||
assert.NoError(err)
|
||||
assert.True(info.IsDir())
|
||||
stat, ok := info.Sys().(*syscall.Stat_t)
|
||||
assert.True(ok)
|
||||
assert.Equal(tc.uid, int(stat.Uid))
|
||||
assert.Equal(tc.gid, int(stat.Gid))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestChownToParent(t *testing.T) {
|
||||
if os.Getuid() != 0 {
|
||||
t.Skip("Test disabled as requires root user")
|
||||
}
|
||||
assert := assert.New(t)
|
||||
rootDir, err := ioutil.TempDir("", "root")
|
||||
assert.NoError(err)
|
||||
defer os.RemoveAll(rootDir)
|
||||
uid := 1234
|
||||
gid := 5678
|
||||
err = syscall.Chown(rootDir, uid, gid)
|
||||
assert.NoError(err)
|
||||
|
||||
targetDir := path.Join(rootDir, "foo")
|
||||
|
||||
err = os.MkdirAll(targetDir, 0700)
|
||||
assert.NoError(err)
|
||||
|
||||
err = ChownToParent(targetDir)
|
||||
assert.NoError(err)
|
||||
|
||||
info, err := os.Stat(targetDir)
|
||||
assert.NoError(err)
|
||||
stat, ok := info.Sys().(*syscall.Stat_t)
|
||||
assert.True(ok)
|
||||
assert.Equal(uid, int(stat.Uid))
|
||||
assert.Equal(gid, int(stat.Gid))
|
||||
}
|
||||
|
||||
func TestGetAllParentPaths(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
testCases := []struct {
|
||||
targetPath string
|
||||
parents []string
|
||||
}{
|
||||
{
|
||||
targetPath: "/",
|
||||
parents: []string{},
|
||||
},
|
||||
{
|
||||
targetPath: ".",
|
||||
parents: []string{},
|
||||
},
|
||||
{
|
||||
targetPath: "foo",
|
||||
parents: []string{"foo"},
|
||||
},
|
||||
{
|
||||
targetPath: "/tmp/bar",
|
||||
parents: []string{"/tmp", "/tmp/bar"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
assert.Equal(tc.parents, getAllParentPaths(tc.targetPath))
|
||||
}
|
||||
}
|
||||
|
@ -79,6 +79,12 @@ func (v *virtiofsd) getSocketFD() (*os.File, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Need to change the filesystem ownership of the socket because virtiofsd runs as root while qemu can run as non-root.
|
||||
// This can be removed once virtiofsd can also run as non-root (https://github.com/kata-containers/kata-containers/issues/2542)
|
||||
if err := utils.ChownToParent(v.socketPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// no longer needed since fd is a dup
|
||||
defer listener.Close()
|
||||
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@ -37,7 +38,7 @@ func TestVirtiofsdStart(t *testing.T) {
|
||||
assert.NoError(err)
|
||||
defer os.RemoveAll(socketDir)
|
||||
|
||||
socketPath := socketDir + "socket.s"
|
||||
socketPath := path.Join(socketDir, "socket.s")
|
||||
|
||||
validConfig := fields{
|
||||
path: "/usr/bin/virtiofsd-path",
|
||||
|
Loading…
Reference in New Issue
Block a user