mirror of
https://github.com/opencontainers/runc.git
synced 2026-07-11 06:03:57 +08:00
30b530ca94
Commit4316df8b53isolated RunningInUserNS to a separate package to make it easier to consume without bringing in additional dependencies, and with the potential to move it separate in a similar fashion as libcontainer/user was moved to a separate module in commitca32014adb. While RunningInUserNS is fairly trivial to implement, it (or variants of this utility) is used in many codebases, and moving to a separate module could consolidate those implementations, as well as making it easier to consume without large dependency trees (when being a package as part of a larger code base). Commit1912d5988band follow-ups introduced cgo code into the userns package, and code introduced in those commits are not intended for external use, therefore complicating the potential of moving the userns package separate. This commit moves the new code to a separate package; some of this code was included in v1.1.11 and up, but I could not find external consumers of `GetUserNamespaceMappings` and `IsSameMapping`. The `Mapping` and `Handles` types (added inba0b5e2698) only exist in main and in non-stable releases (v1.2.0-rc.x), so don't need an alias / deprecation. Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
187 lines
5.3 KiB
Go
187 lines
5.3 KiB
Go
//go:build linux
|
|
|
|
package userns
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"unsafe"
|
|
|
|
"github.com/opencontainers/runc/libcontainer/configs"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
/*
|
|
#include <stdlib.h>
|
|
extern int spawn_userns_cat(char *userns_path, char *path, int outfd, int errfd);
|
|
*/
|
|
import "C"
|
|
|
|
func parseIdmapData(data []byte) (ms []configs.IDMap, err error) {
|
|
scanner := bufio.NewScanner(bytes.NewReader(data))
|
|
for scanner.Scan() {
|
|
var m configs.IDMap
|
|
line := scanner.Text()
|
|
if _, err := fmt.Sscanf(line, "%d %d %d", &m.ContainerID, &m.HostID, &m.Size); err != nil {
|
|
return nil, fmt.Errorf("parsing id map failed: invalid format in line %q: %w", line, err)
|
|
}
|
|
ms = append(ms, m)
|
|
}
|
|
if err := scanner.Err(); err != nil {
|
|
return nil, fmt.Errorf("parsing id map failed: %w", err)
|
|
}
|
|
return ms, nil
|
|
}
|
|
|
|
// Do something equivalent to nsenter --user=<nsPath> cat <path>, but more
|
|
// efficiently. Returns the contents of the requested file from within the user
|
|
// namespace.
|
|
func spawnUserNamespaceCat(nsPath string, path string) ([]byte, error) {
|
|
rdr, wtr, err := os.Pipe()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create pipe for userns spawn failed: %w", err)
|
|
}
|
|
defer rdr.Close()
|
|
defer wtr.Close()
|
|
|
|
errRdr, errWtr, err := os.Pipe()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create error pipe for userns spawn failed: %w", err)
|
|
}
|
|
defer errRdr.Close()
|
|
defer errWtr.Close()
|
|
|
|
cNsPath := C.CString(nsPath)
|
|
defer C.free(unsafe.Pointer(cNsPath))
|
|
cPath := C.CString(path)
|
|
defer C.free(unsafe.Pointer(cPath))
|
|
|
|
childPid := C.spawn_userns_cat(cNsPath, cPath, C.int(wtr.Fd()), C.int(errWtr.Fd()))
|
|
|
|
if childPid < 0 {
|
|
return nil, fmt.Errorf("failed to spawn fork for userns")
|
|
} else if childPid == 0 {
|
|
// this should never happen
|
|
panic("runc executing inside fork child -- unsafe state!")
|
|
}
|
|
|
|
// We are in the parent -- close the write end of the pipe before reading.
|
|
wtr.Close()
|
|
output, err := io.ReadAll(rdr)
|
|
rdr.Close()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("reading from userns spawn failed: %w", err)
|
|
}
|
|
|
|
// Ditto for the error pipe.
|
|
errWtr.Close()
|
|
errOutput, err := io.ReadAll(errRdr)
|
|
errRdr.Close()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("reading from userns spawn error pipe failed: %w", err)
|
|
}
|
|
errOutput = bytes.TrimSpace(errOutput)
|
|
|
|
// Clean up the child.
|
|
child, err := os.FindProcess(int(childPid))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not find userns spawn process: %w", err)
|
|
}
|
|
state, err := child.Wait()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to wait for userns spawn process: %w", err)
|
|
}
|
|
if !state.Success() {
|
|
errStr := string(errOutput)
|
|
if errStr == "" {
|
|
errStr = fmt.Sprintf("unknown error (status code %d)", state.ExitCode())
|
|
}
|
|
return nil, fmt.Errorf("userns spawn: %s", errStr)
|
|
} else if len(errOutput) > 0 {
|
|
// We can just ignore weird output in the error pipe if the process
|
|
// didn't bail(), but for completeness output for debugging.
|
|
logrus.Debugf("userns spawn succeeded but unexpected error message found: %s", string(errOutput))
|
|
}
|
|
// The subprocess succeeded, return whatever it wrote to the pipe.
|
|
return output, nil
|
|
}
|
|
|
|
func GetUserNamespaceMappings(nsPath string) (uidMap, gidMap []configs.IDMap, err error) {
|
|
var (
|
|
pid int
|
|
extra rune
|
|
tryFastPath bool
|
|
)
|
|
|
|
// nsPath is usually of the form /proc/<pid>/ns/user, which means that we
|
|
// already have a pid that is part of the user namespace and thus we can
|
|
// just use the pid to read from /proc/<pid>/*id_map.
|
|
//
|
|
// Note that Sscanf doesn't consume the whole input, so we check for any
|
|
// trailing data with %c. That way, we can be sure the pattern matched
|
|
// /proc/$pid/ns/user _exactly_ iff n === 1.
|
|
if n, _ := fmt.Sscanf(nsPath, "/proc/%d/ns/user%c", &pid, &extra); n == 1 {
|
|
tryFastPath = pid > 0
|
|
}
|
|
|
|
for _, mapType := range []struct {
|
|
name string
|
|
idMap *[]configs.IDMap
|
|
}{
|
|
{"uid_map", &uidMap},
|
|
{"gid_map", &gidMap},
|
|
} {
|
|
var mapData []byte
|
|
|
|
if tryFastPath {
|
|
path := fmt.Sprintf("/proc/%d/%s", pid, mapType.name)
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
// Do not error out here -- we need to try the slow path if the
|
|
// fast path failed.
|
|
logrus.Debugf("failed to use fast path to read %s from userns %s (error: %s), falling back to slow userns-join path", mapType.name, nsPath, err)
|
|
} else {
|
|
mapData = data
|
|
}
|
|
} else {
|
|
logrus.Debugf("cannot use fast path to read %s from userns %s, falling back to slow userns-join path", mapType.name, nsPath)
|
|
}
|
|
|
|
if mapData == nil {
|
|
// We have to actually join the namespace if we cannot take the
|
|
// fast path. The path is resolved with respect to the child
|
|
// process, so just use /proc/self.
|
|
data, err := spawnUserNamespaceCat(nsPath, "/proc/self/"+mapType.name)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
mapData = data
|
|
}
|
|
idMap, err := parseIdmapData(mapData)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to parse %s of userns %s: %w", mapType.name, nsPath, err)
|
|
}
|
|
*mapType.idMap = idMap
|
|
}
|
|
|
|
return uidMap, gidMap, nil
|
|
}
|
|
|
|
// IsSameMapping returns whether or not the two id mappings are the same. Note
|
|
// that if the order of the mappings is different, or a mapping has been split,
|
|
// the mappings will be considered different.
|
|
func IsSameMapping(a, b []configs.IDMap) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
for idx := range a {
|
|
if a[idx] != b[idx] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|