aboutsummaryrefslogtreecommitdiff
path: root/pkg/rootless/rootless.go
blob: 94535f45e6aa784012058ce6648a948dc1f89334 (plain)
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
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
package rootless

import (
	"errors"
	"fmt"
	"os"
	"sort"
	"sync"

	"github.com/containers/storage/pkg/lockfile"
	"github.com/opencontainers/runc/libcontainer/user"
	spec "github.com/opencontainers/runtime-spec/specs-go"
)

// TryJoinPauseProcess attempts to join the namespaces of the pause PID via
// TryJoinFromFilePaths.  If joining fails, it attempts to delete the specified
// file.
func TryJoinPauseProcess(pausePidPath string) (bool, int, error) {
	if _, err := os.Stat(pausePidPath); err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return false, -1, nil
		}
		return false, -1, err
	}

	became, ret, err := TryJoinFromFilePaths("", false, []string{pausePidPath})
	if err == nil {
		return became, ret, nil
	}

	// It could not join the pause process, let's lock the file before trying to delete it.
	pidFileLock, err := lockfile.GetLockfile(pausePidPath)
	if err != nil {
		// The file was deleted by another process.
		if os.IsNotExist(err) {
			return false, -1, nil
		}
		return false, -1, fmt.Errorf("acquiring lock on %s: %w", pausePidPath, err)
	}

	pidFileLock.Lock()
	defer func() {
		if pidFileLock.Locked() {
			pidFileLock.Unlock()
		}
	}()

	// Now the pause PID file is locked.  Try to join once again in case it changed while it was not locked.
	became, ret, err = TryJoinFromFilePaths("", false, []string{pausePidPath})
	if err != nil {
		// It is still failing.  We can safely remove it.
		os.Remove(pausePidPath)
		return false, -1, nil //nolint: nilerr
	}
	return became, ret, err
}

var (
	uidMap      []user.IDMap
	uidMapError error
	uidMapOnce  sync.Once

	gidMap      []user.IDMap
	gidMapError error
	gidMapOnce  sync.Once
)

// GetAvailableUIDMap returns the UID mappings in the
// current user namespace.
func GetAvailableUIDMap() ([]user.IDMap, error) {
	uidMapOnce.Do(func() {
		var err error
		uidMap, err = user.ParseIDMapFile("/proc/self/uid_map")
		if err != nil {
			uidMapError = err
			return
		}
	})
	return uidMap, uidMapError
}

// GetAvailableGIDMap returns the GID mappings in the
// current user namespace.
func GetAvailableGIDMap() ([]user.IDMap, error) {
	gidMapOnce.Do(func() {
		var err error
		gidMap, err = user.ParseIDMapFile("/proc/self/gid_map")
		if err != nil {
			gidMapError = err
			return
		}
	})
	return gidMap, gidMapError
}

// GetAvailableIDMaps returns the UID and GID mappings in the
// current user namespace.
func GetAvailableIDMaps() ([]user.IDMap, []user.IDMap, error) {
	u, err := GetAvailableUIDMap()
	if err != nil {
		return nil, nil, err
	}
	g, err := GetAvailableGIDMap()
	if err != nil {
		return nil, nil, err
	}
	return u, g, nil
}

func countAvailableIDs(mappings []user.IDMap) int64 {
	availableUids := int64(0)
	for _, r := range mappings {
		availableUids += r.Count
	}
	return availableUids
}

// GetAvailableUids returns how many UIDs are available in the
// current user namespace.
func GetAvailableUids() (int64, error) {
	uids, err := GetAvailableUIDMap()
	if err != nil {
		return -1, err
	}

	return countAvailableIDs(uids), nil
}

// GetAvailableGids returns how many GIDs are available in the
// current user namespace.
func GetAvailableGids() (int64, error) {
	gids, err := GetAvailableGIDMap()
	if err != nil {
		return -1, err
	}

	return countAvailableIDs(gids), nil
}

// findIDInMappings find the the mapping that contains the specified ID.
// It assumes availableMappings is sorted by ID.
func findIDInMappings(id int64, availableMappings []user.IDMap) *user.IDMap {
	i := sort.Search(len(availableMappings), func(i int) bool {
		return availableMappings[i].ID <= id
	})
	if i < 0 || i >= len(availableMappings) {
		return nil
	}
	r := &availableMappings[i]
	if id >= r.ID && id < r.ID+r.Count {
		return r
	}
	return nil
}

// MaybeSplitMappings checks whether the specified OCI mappings are possible
// in the current user namespace or the specified ranges must be split.
func MaybeSplitMappings(mappings []spec.LinuxIDMapping, availableMappings []user.IDMap) []spec.LinuxIDMapping {
	var ret []spec.LinuxIDMapping
	var overflow spec.LinuxIDMapping
	overflow.Size = 0
	consumed := 0
	sort.Slice(availableMappings, func(i, j int) bool {
		return availableMappings[i].ID > availableMappings[j].ID
	})
	for {
		cur := overflow
		// if there is no overflow left from the previous request, get the next one
		if cur.Size == 0 {
			if consumed == len(mappings) {
				// all done
				return ret
			}
			cur = mappings[consumed]
			consumed++
		}

		// Find the range where the first specified ID is present
		r := findIDInMappings(int64(cur.HostID), availableMappings)
		if r == nil {
			// The requested range is not available.  Just return the original request
			// and let other layers deal with it.
			return mappings
		}

		offsetInRange := cur.HostID - uint32(r.ID)

		usableIDs := uint32(r.Count) - offsetInRange

		// the current range can satisfy the whole request
		if usableIDs >= cur.Size {
			// reset the overflow
			overflow.Size = 0
		} else {
			// the current range can satisfy the request partially
			// so move the rest to overflow
			overflow.Size = cur.Size - usableIDs
			overflow.ContainerID = cur.ContainerID + usableIDs
			overflow.HostID = cur.HostID + usableIDs

			// and cap to the usableIDs count
			cur.Size = usableIDs
		}
		ret = append(ret, cur)
	}
}