File: userns_maps_linux.go

package info (click to toggle)
runc 1.3.2%2Bds1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 2,684 kB
  • sloc: sh: 2,267; ansic: 1,125; makefile: 200
file content (186 lines) | stat: -rw-r--r-- 5,444 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
//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
}