diff --git a/cli/main.go b/cli/main.go index 40bef70526..ef220196ad 100644 --- a/cli/main.go +++ b/cli/main.go @@ -18,6 +18,7 @@ import ( "syscall" "github.com/kata-containers/runtime/pkg/katautils" + "github.com/kata-containers/runtime/pkg/rootless" "github.com/kata-containers/runtime/pkg/signals" vc "github.com/kata-containers/runtime/virtcontainers" vf "github.com/kata-containers/runtime/virtcontainers/factory" @@ -241,6 +242,9 @@ func setExternalLoggers(ctx context.Context, logger *logrus.Entry) { // Set the katautils package logger katautils.SetLogger(ctx, logger, originalLoggerLevel) + + // Set the rootless package logger + rootless.SetLogger(ctx, logger) } // beforeSubcommands is the function to perform preliminary checks diff --git a/pkg/rootless/rootless.go b/pkg/rootless/rootless.go new file mode 100644 index 0000000000..676fc77274 --- /dev/null +++ b/pkg/rootless/rootless.go @@ -0,0 +1,123 @@ +// Copyright (c) 2019 Intel Corporation +// +// SPDX-License-Identifier: Apache-2.0 +// + +package rootless + +import ( + "bufio" + "context" + "io" + "os" + "strconv" + "strings" + "sync" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +var ( + // initRootless states whether the isRootless variable + // has been set yet + initRootless bool + + // isRootless states whether execution is rootless or not + isRootless bool + + // lock for the initRootless and isRootless variables + rLock sync.Mutex + + // XDG_RUNTIME_DIR defines the base directory relative to + // which user-specific non-essential runtime files are stored. + rootlessDir = os.Getenv("XDG_RUNTIME_DIR") + + // uidMapPath defines the location of the uid_map file to + // determine whether a user is root or not + uidMapPath = "/proc/self/uid_map" + + rootlessLog = logrus.WithFields(logrus.Fields{ + "source": "rootless", + }) +) + +// SetLogger sets up a logger for the rootless pkg +func SetLogger(ctx context.Context, logger *logrus.Entry) { + fields := rootlessLog.Data + rootlessLog = logger.WithFields(fields) +} + +// setRootless reads a uid_map file, compares the UID of the +// user inside the container vs on the host. If the host UID +// is not root, but the container ID is, it can be determined +// the user is running rootlessly. +func setRootless() error { + initRootless = true + file, err := os.Open(uidMapPath) + if err != nil { + return err + } + defer file.Close() + + buf := bufio.NewReader(file) + for { + line, _, err := buf.ReadLine() + if err != nil { + if err == io.EOF { + return nil + } + return err + } + if line == nil { + return nil + } + + var parseError = errors.Errorf("Failed to parse uid map file %s", uidMapPath) + // if the container id (id[0]) is 0 (root inside the container) + // has a mapping to the host id (id[1]) that is not root, then + // it can be determined that the host user is running rootless + ids := strings.Fields(string(line)) + // do some sanity checks + if len(ids) != 3 { + return parseError + } + userNSUid, err := strconv.ParseUint(ids[0], 10, 0) + if err != nil { + return parseError + } + hostUid, err := strconv.ParseUint(ids[1], 10, 0) + if err != nil { + return parseError + } + rangeUid, err := strconv.ParseUint(ids[1], 10, 0) + if err != nil || rangeUid == 0 { + return parseError + } + + if userNSUid == 0 && hostUid != 0 { + rootlessLog.Info("Running as rootless") + isRootless = true + return nil + } + } +} + +// IsRootless states whether kata is being ran with root or not +func IsRootless() bool { + rLock.Lock() + if !initRootless { + err := setRootless() + if err != nil { + rootlessLog.WithError(err).Error("Unable to determine if running rootless") + } + } + rLock.Unlock() + return isRootless +} + +// GetRootlessDir returns the path to the location for rootless +// container and sandbox storage +func GetRootlessDir() string { + return rootlessDir +} diff --git a/pkg/rootless/rootless_test.go b/pkg/rootless/rootless_test.go new file mode 100644 index 0000000000..91e8dc4034 --- /dev/null +++ b/pkg/rootless/rootless_test.go @@ -0,0 +1,167 @@ +// Copyright (c) 2019 Intel Corporation +// +// SPDX-License-Identifier: Apache-2.0 +// + +package rootless + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +var uidMapPathStore = uidMapPath + +func createTestUIDMapFile(input string) error { + f, err := os.Create(uidMapPath) + if err != nil { + return err + } + defer f.Close() + + _, err = f.WriteString(input) + if err != nil { + return err + } + + return nil +} + +type uidMapping struct { + userNSUid int + hostUid int + uidRange int +} + +type testScenario struct { + name string + isRootless bool + uidMap []uidMapping +} + +func TestIsRootless(t *testing.T) { + assert := assert.New(t) + + // by default isRootless should be set to false initially + assert.False(isRootless) + + allScenarios := []testScenario{ + testScenario{ + name: "User NS UID is not root UID", + isRootless: false, + uidMap: []uidMapping{ + {1, 0, 1}, + {1, 0, 1000}, + + {1, 1000, 1}, + {1, 1000, 1000}, + + {1000, 1000, 1}, + {1000, 1000, 1000}, + + {1000, 1000, 5555}, + }, + }, + + testScenario{ + name: "Host NS UID is root UID", + isRootless: false, + uidMap: []uidMapping{ + {0, 0, 1}, + {0, 0, 1000}, + + {1, 0, 1}, + {1, 0, 1000}, + + {1000, 0, 0}, + {1000, 0, 1}, + {1000, 0, 1000}, + }, + }, + + testScenario{ + name: "UID range is zero", + isRootless: false, + uidMap: []uidMapping{ + {0, 0, 0}, + {1, 0, 0}, + {1, 1000, 0}, + {1000, 1000, 0}, + }, + }, + + testScenario{ + name: "Negative UIDs", + isRootless: false, + uidMap: []uidMapping{ + {-1, 0, 0}, + {-1, 0, 1}, + {-1, 0, 1000}, + + {0, -1, 0}, + {0, -1, 1}, + {0, -1, 1000}, + + {1000, 1000, -1}, + {1000, 1000, -1}, + {1000, 1000, -1000}, + }, + }, + + testScenario{ + name: "User NS UID is root UID, host UID is not root UID", + isRootless: true, + uidMap: []uidMapping{ + {0, 1, 1}, + {0, 1000, 1}, + {0, 1000, 5555}, + }, + }, + } + + // Run the tests + for _, scenario := range allScenarios { + for _, uidMap := range scenario.uidMap { + testUIDMap(uidMap, scenario.isRootless, t) + } + } + + testWithMapFileContent("", false, t) + + testWithMapFileContent("This is not a mapping", false, t) +} + +func testUIDMap(uidMap uidMapping, expectedRootless bool, t *testing.T) { + mapping := fmt.Sprintf("\t%d\t%d\t%d", uidMap.userNSUid, uidMap.hostUid, uidMap.uidRange) + testWithMapFileContent(mapping, expectedRootless, t) +} + +func testWithMapFileContent(content string, expectedRootless bool, t *testing.T) { + // Create a test-specific message that is added to each assert + // call. It will be displayed if any assert test fails. + msg := fmt.Sprintf("isRootless[%t]: %s", expectedRootless, content) + + tmpDir, err := ioutil.TempDir("", "") + assert.NoError(t, err) + + uidMapPath = filepath.Join(tmpDir, "testUIDMapFile") + defer func() { + uidMapPath = uidMapPathStore + os.RemoveAll(uidMapPath) + os.RemoveAll(tmpDir) + isRootless = false + initRootless = false + }() + + err = createTestUIDMapFile(content) + assert.NoError(t, err, msg) + + // make call to IsRootless, this should also call + // SetRootless + assert.Equal(t, expectedRootless, IsRootless(), msg) +}