-
Notifications
You must be signed in to change notification settings - Fork 243
/
userns.go
344 lines (309 loc) · 9.82 KB
/
userns.go
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
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
//go:build linux
package storage
import (
"fmt"
"os"
"os/user"
"strconv"
drivers "github.com/containers/storage/drivers"
"github.com/containers/storage/pkg/idtools"
"github.com/containers/storage/pkg/unshare"
"github.com/containers/storage/types"
securejoin "github.com/cyphar/filepath-securejoin"
libcontainerUser "github.com/moby/sys/user"
"github.com/sirupsen/logrus"
"golang.org/x/sys/unix"
)
// getAdditionalSubIDs looks up the additional IDs configured for
// the specified user.
// The argument USERNAME is ignored for rootless users, as it is not
// possible to use an arbitrary entry in /etc/sub*id.
// Differently, if the username is not specified for root users, a
// default name is used.
func getAdditionalSubIDs(username string) (*idSet, *idSet, error) {
var uids, gids *idSet
if unshare.IsRootless() {
username = os.Getenv("USER")
if username == "" {
var id string
if os.Geteuid() == 0 {
id = strconv.Itoa(unshare.GetRootlessUID())
} else {
id = strconv.Itoa(os.Geteuid())
}
userID, err := user.LookupId(id)
if err == nil {
username = userID.Username
}
}
} else if username == "" {
username = RootAutoUserNsUser
}
mappings, err := idtools.NewIDMappings(username, username)
if err != nil {
logrus.Errorf("Cannot find mappings for user %q: %v", username, err)
} else {
uids = getHostIDs(mappings.UIDs())
gids = getHostIDs(mappings.GIDs())
}
return uids, gids, nil
}
// getAvailableIDs returns the list of ranges that are usable by the current user.
// When running as root, it looks up the additional IDs assigned to the specified user.
// When running as rootless, the mappings assigned to the unprivileged user are converted
// to the IDs inside of the initial rootless user namespace.
func (s *store) getAvailableIDs() (*idSet, *idSet, error) {
if s.additionalUIDs == nil {
uids, gids, err := getAdditionalSubIDs(s.autoUsernsUser)
if err != nil {
return nil, nil, err
}
// Store the result so we don't need to look it up again next time
s.additionalUIDs, s.additionalGIDs = uids, gids
}
if !unshare.IsRootless() {
// No mapping to inner namespace needed
return s.additionalUIDs, s.additionalGIDs, nil
}
// We are already inside of the rootless user namespace.
// We need to remap the configured mappings to what is available
// inside of the rootless userns.
u := newIDSet([]interval{{start: 1, end: s.additionalUIDs.size() + 1}})
g := newIDSet([]interval{{start: 1, end: s.additionalGIDs.size() + 1}})
return u, g, nil
}
// nobodyUser returns the UID and GID of the "nobody" user. Hardcode its value
// for simplicity.
const nobodyUser = 65534
// parseMountedFiles returns the maximum UID and GID found in the /etc/passwd and
// /etc/group files.
func parseMountedFiles(containerMount, passwdFile, groupFile string) uint32 {
var (
passwd *os.File
group *os.File
size int
err error
)
if passwdFile == "" {
passwd, err = secureOpen(containerMount, "/etc/passwd")
} else {
// User-specified override from a volume. Will not be in
// container root.
passwd, err = os.Open(passwdFile)
}
if err == nil {
defer passwd.Close()
users, err := libcontainerUser.ParsePasswd(passwd)
if err == nil {
for _, u := range users {
// Skip the "nobody" user otherwise we end up with 65536
// ids with most images
if u.Name == "nobody" || u.Name == "nogroup" {
continue
}
if u.Uid > size && u.Uid != nobodyUser {
size = u.Uid + 1
}
if u.Gid > size && u.Gid != nobodyUser {
size = u.Gid + 1
}
}
}
}
if groupFile == "" {
group, err = secureOpen(containerMount, "/etc/group")
} else {
// User-specified override from a volume. Will not be in
// container root.
group, err = os.Open(groupFile)
}
if err == nil {
defer group.Close()
groups, err := libcontainerUser.ParseGroup(group)
if err == nil {
for _, g := range groups {
if g.Name == "nobody" || g.Name == "nogroup" {
continue
}
if g.Gid > size && g.Gid != nobodyUser {
size = g.Gid + 1
}
}
}
}
return uint32(size)
}
// getMaxSizeFromImage returns the maximum ID used by the specified image.
// On entry, rlstore must be locked for writing, and lstores must be locked for reading.
func (s *store) getMaxSizeFromImage(image *Image, rlstore rwLayerStore, lstores []roLayerStore, passwdFile, groupFile string) (_ uint32, retErr error) {
layerStores := append([]roLayerStore{rlstore}, lstores...)
size := uint32(0)
var topLayer *Layer
layerName := image.TopLayer
outer:
for {
for _, ls := range layerStores {
layer, err := ls.Get(layerName)
if err != nil {
continue
}
if image.TopLayer == layerName {
topLayer = layer
}
for _, uid := range layer.UIDs {
if uid >= size {
size = uid + 1
}
}
for _, gid := range layer.GIDs {
if gid >= size {
size = gid + 1
}
}
layerName = layer.Parent
if layerName == "" {
break outer
}
continue outer
}
return 0, fmt.Errorf("cannot find layer %q", layerName)
}
layerOptions := &LayerOptions{
IDMappingOptions: types.IDMappingOptions{
HostUIDMapping: true,
HostGIDMapping: true,
UIDMap: nil,
GIDMap: nil,
},
}
// We need to create a temporary layer so we can mount it and lookup the
// maximum IDs used.
clayer, _, err := rlstore.create("", topLayer, nil, "", nil, layerOptions, false, nil, nil)
if err != nil {
return 0, err
}
defer func() {
if err2 := rlstore.Delete(clayer.ID); err2 != nil {
if retErr == nil {
retErr = fmt.Errorf("deleting temporary layer %#v: %w", clayer.ID, err2)
} else {
logrus.Errorf("Error deleting temporary layer %#v: %v", clayer.ID, err2)
}
}
}()
mountOptions := drivers.MountOpts{
MountLabel: "",
UidMaps: nil,
GidMaps: nil,
Options: nil,
}
mountpoint, err := rlstore.Mount(clayer.ID, mountOptions)
if err != nil {
return 0, err
}
defer func() {
if _, err2 := rlstore.unmount(clayer.ID, true, false); err2 != nil {
if retErr == nil {
retErr = fmt.Errorf("unmounting temporary layer %#v: %w", clayer.ID, err2)
} else {
logrus.Errorf("Error unmounting temporary layer %#v: %v", clayer.ID, err2)
}
}
}()
userFilesSize := parseMountedFiles(mountpoint, passwdFile, groupFile)
if userFilesSize > size {
size = userFilesSize
}
return size, nil
}
// getAutoUserNS creates an automatic user namespace
// If image != nil, On entry, rlstore must be locked for writing, and lstores must be locked for reading.
func (s *store) getAutoUserNS(options *types.AutoUserNsOptions, image *Image, rlstore rwLayerStore, lstores []roLayerStore) ([]idtools.IDMap, []idtools.IDMap, error) {
requestedSize := uint32(0)
initialSize := uint32(1)
if options.Size > 0 {
requestedSize = options.Size
}
if options.InitialSize > 0 {
initialSize = options.InitialSize
}
availableUIDs, availableGIDs, err := s.getAvailableIDs()
if err != nil {
return nil, nil, fmt.Errorf("cannot read mappings: %w", err)
}
// Look at every container that is using a user namespace and store
// the intervals that are already used.
containers, err := s.Containers()
if err != nil {
return nil, nil, err
}
var usedUIDs, usedGIDs []idtools.IDMap
for _, c := range containers {
usedUIDs = append(usedUIDs, c.UIDMap...)
usedGIDs = append(usedGIDs, c.GIDMap...)
}
size := requestedSize
// If there is no requestedSize, lookup the maximum used IDs in the layers
// metadata. Make sure the size is at least s.autoNsMinSize and it is not
// bigger than s.autoNsMaxSize.
// This is a best effort heuristic.
if requestedSize == 0 {
size = initialSize
if s.autoNsMinSize > size {
size = s.autoNsMinSize
}
if image != nil {
sizeFromImage, err := s.getMaxSizeFromImage(image, rlstore, lstores, options.PasswdFile, options.GroupFile)
if err != nil {
return nil, nil, err
}
if sizeFromImage > size {
size = sizeFromImage
}
}
if s.autoNsMaxSize > 0 && size > s.autoNsMaxSize {
return nil, nil, fmt.Errorf("the container needs a user namespace with size %v that is bigger than the maximum value allowed with userns=auto %v", size, s.autoNsMaxSize)
}
}
return getAutoUserNSIDMappings(
int(size),
availableUIDs, availableGIDs,
usedUIDs, usedGIDs,
options.AdditionalUIDMappings, options.AdditionalGIDMappings,
)
}
// getAutoUserNSIDMappings computes the user/group id mappings for the automatic user namespace.
func getAutoUserNSIDMappings(
size int,
availableUIDs, availableGIDs *idSet,
usedUIDMappings, usedGIDMappings, additionalUIDMappings, additionalGIDMappings []idtools.IDMap,
) ([]idtools.IDMap, []idtools.IDMap, error) {
usedUIDs := getHostIDs(append(usedUIDMappings, additionalUIDMappings...))
usedGIDs := getHostIDs(append(usedGIDMappings, additionalGIDMappings...))
// Exclude additional uids and gids from requested range.
targetIDs := newIDSet([]interval{{start: 0, end: size}})
requestedContainerUIDs := targetIDs.subtract(getContainerIDs(additionalUIDMappings))
requestedContainerGIDs := targetIDs.subtract(getContainerIDs(additionalGIDMappings))
// Make sure the specified additional IDs are not used as part of the automatic
// mapping
availableUIDs, err := availableUIDs.subtract(usedUIDs).findAvailable(requestedContainerUIDs.size())
if err != nil {
return nil, nil, err
}
availableGIDs, err = availableGIDs.subtract(usedGIDs).findAvailable(requestedContainerGIDs.size())
if err != nil {
return nil, nil, err
}
uidMap := append(availableUIDs.zip(requestedContainerUIDs), additionalUIDMappings...)
gidMap := append(availableGIDs.zip(requestedContainerGIDs), additionalGIDMappings...)
return uidMap, gidMap, nil
}
// Securely open (read-only) a file in a container mount.
func secureOpen(containerMount, file string) (*os.File, error) {
tmpFile, err := securejoin.OpenInRoot(containerMount, file)
if err != nil {
return nil, err
}
defer tmpFile.Close()
return securejoin.Reopen(tmpFile, unix.O_RDONLY)
}