Skip to content

Commit 4516c25

Browse files
authored
Merge pull request #4124 from cyphar/ns-path-handling
*: fix several issues with namespace path handling
2 parents 9fffada + c045886 commit 4516c25

File tree

23 files changed

+884
-196
lines changed

23 files changed

+884
-196
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ vendor/pkg
77
/contrib/cmd/fs-idmap/fs-idmap
88
/contrib/cmd/memfd-bind/memfd-bind
99
/contrib/cmd/pidfd-kill/pidfd-kill
10+
/contrib/cmd/remap-rootfs/remap-rootfs
1011
man/man8
1112
release
1213
Vagrantfile

Makefile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,10 @@ runc-bin: runc-dmz
7171
$(GO_BUILD) -o runc .
7272

7373
.PHONY: all
74-
all: runc recvtty sd-helper seccompagent fs-idmap memfd-bind pidfd-kill
74+
all: runc recvtty sd-helper seccompagent fs-idmap memfd-bind pidfd-kill remap-rootfs
7575

76-
.PHONY: recvtty sd-helper seccompagent fs-idmap memfd-bind pidfd-kill
77-
recvtty sd-helper seccompagent fs-idmap memfd-bind pidfd-kill:
76+
.PHONY: recvtty sd-helper seccompagent fs-idmap memfd-bind pidfd-kill remap-rootfs
77+
recvtty sd-helper seccompagent fs-idmap memfd-bind pidfd-kill remap-rootfs:
7878
$(GO_BUILD) -o contrib/cmd/$@/$@ ./contrib/cmd/$@
7979

8080
.PHONY: static
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"fmt"
7+
"os"
8+
"path/filepath"
9+
"syscall"
10+
11+
"github.com/urfave/cli"
12+
13+
"github.com/opencontainers/runtime-spec/specs-go"
14+
)
15+
16+
const usage = `contrib/cmd/remap-rootfs
17+
18+
remap-rootfs is a helper tool to remap the root filesystem of a Open Container
19+
Initiative bundle using user namespaces such that the file owners are remapped
20+
from "host" mappings to the user namespace's mappings.
21+
22+
Effectively, this is a slightly more complicated 'chown -R', and is primarily
23+
used within runc's integration tests to remap the test filesystem to match the
24+
test user namespace. Note that calling remap-rootfs multiple times, or changing
25+
the mapping and then calling remap-rootfs will likely produce incorrect results
26+
because we do not "un-map" any pre-applied mappings from previous remap-rootfs
27+
calls.
28+
29+
Note that the bundle is assumed to be produced by a trusted source, and thus
30+
malicious configuration files will likely not be handled safely.
31+
32+
To use remap-rootfs, simply pass it the path to an OCI bundle (a directory
33+
containing a config.json):
34+
35+
$ sudo remap-rootfs ./bundle
36+
`
37+
38+
func toHostID(mappings []specs.LinuxIDMapping, id uint32) (int, bool) {
39+
for _, m := range mappings {
40+
if m.ContainerID <= id && id < m.ContainerID+m.Size {
41+
return int(m.HostID + id), true
42+
}
43+
}
44+
return -1, false
45+
}
46+
47+
type inodeID struct {
48+
Dev, Ino uint64
49+
}
50+
51+
func toInodeID(st *syscall.Stat_t) inodeID {
52+
return inodeID{Dev: st.Dev, Ino: st.Ino}
53+
}
54+
55+
func remapRootfs(root string, uidMap, gidMap []specs.LinuxIDMapping) error {
56+
seenInodes := make(map[inodeID]struct{})
57+
return filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
58+
if err != nil {
59+
return err
60+
}
61+
62+
mode := info.Mode()
63+
st := info.Sys().(*syscall.Stat_t)
64+
65+
// Skip symlinks.
66+
if mode.Type() == os.ModeSymlink {
67+
return nil
68+
}
69+
// Skip hard-links to files we've already remapped.
70+
id := toInodeID(st)
71+
if _, seen := seenInodes[id]; seen {
72+
return nil
73+
}
74+
seenInodes[id] = struct{}{}
75+
76+
// Calculate the new uid:gid.
77+
uid := st.Uid
78+
newUID, ok1 := toHostID(uidMap, uid)
79+
gid := st.Gid
80+
newGID, ok2 := toHostID(gidMap, gid)
81+
82+
// Skip files that cannot be mapped.
83+
if !ok1 || !ok2 {
84+
niceName := path
85+
if relName, err := filepath.Rel(root, path); err == nil {
86+
niceName = "/" + relName
87+
}
88+
fmt.Printf("skipping file %s: cannot remap user %d:%d -> %d:%d\n", niceName, uid, gid, newUID, newGID)
89+
return nil
90+
}
91+
if err := os.Lchown(path, newUID, newGID); err != nil {
92+
return err
93+
}
94+
// Re-apply any setid bits that would be cleared due to chown(2).
95+
return os.Chmod(path, mode)
96+
})
97+
}
98+
99+
func main() {
100+
app := cli.NewApp()
101+
app.Name = "remap-rootfs"
102+
app.Usage = usage
103+
104+
app.Action = func(ctx *cli.Context) error {
105+
args := ctx.Args()
106+
if len(args) != 1 {
107+
return errors.New("exactly one bundle argument must be provided")
108+
}
109+
bundle := args[0]
110+
111+
configFile, err := os.Open(filepath.Join(bundle, "config.json"))
112+
if err != nil {
113+
return err
114+
}
115+
defer configFile.Close()
116+
117+
var spec specs.Spec
118+
if err := json.NewDecoder(configFile).Decode(&spec); err != nil {
119+
return fmt.Errorf("parsing config.json: %w", err)
120+
}
121+
122+
if spec.Root == nil {
123+
return errors.New("invalid config.json: root section is null")
124+
}
125+
rootfs := filepath.Join(bundle, spec.Root.Path)
126+
127+
if spec.Linux == nil {
128+
return errors.New("invalid config.json: linux section is null")
129+
}
130+
uidMap := spec.Linux.UIDMappings
131+
gidMap := spec.Linux.GIDMappings
132+
if len(uidMap) == 0 && len(gidMap) == 0 {
133+
fmt.Println("skipping remapping -- no userns mappings specified")
134+
return nil
135+
}
136+
137+
return remapRootfs(rootfs, uidMap, gidMap)
138+
}
139+
if err := app.Run(os.Args); err != nil {
140+
fmt.Fprintln(os.Stderr, "error:", err)
141+
os.Exit(1)
142+
}
143+
}

libcontainer/configs/config_linux.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
package configs
22

3-
import "errors"
3+
import (
4+
"errors"
5+
"fmt"
6+
)
47

58
var (
6-
errNoUIDMap = errors.New("User namespaces enabled, but no uid mappings found.")
7-
errNoUserMap = errors.New("User namespaces enabled, but no user mapping found.")
8-
errNoGIDMap = errors.New("User namespaces enabled, but no gid mappings found.")
9-
errNoGroupMap = errors.New("User namespaces enabled, but no group mapping found.")
9+
errNoUIDMap = errors.New("user namespaces enabled, but no uid mappings found")
10+
errNoGIDMap = errors.New("user namespaces enabled, but no gid mappings found")
1011
)
1112

1213
// Please check https://man7.org/linux/man-pages/man2/personality.2.html for const details.
@@ -31,7 +32,7 @@ func (c Config) HostUID(containerId int) (int, error) {
3132
}
3233
id, found := c.hostIDFromMapping(containerId, c.UIDMappings)
3334
if !found {
34-
return -1, errNoUserMap
35+
return -1, fmt.Errorf("user namespaces enabled, but no mapping found for uid %d", containerId)
3536
}
3637
return id, nil
3738
}
@@ -54,7 +55,7 @@ func (c Config) HostGID(containerId int) (int, error) {
5455
}
5556
id, found := c.hostIDFromMapping(containerId, c.GIDMappings)
5657
if !found {
57-
return -1, errNoGroupMap
58+
return -1, fmt.Errorf("user namespaces enabled, but no mapping found for gid %d", containerId)
5859
}
5960
return id, nil
6061
}

libcontainer/configs/validate/rootless.go

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package validate
22

33
import (
44
"errors"
5+
"fmt"
56
"strconv"
67
"strings"
78

@@ -28,25 +29,18 @@ func rootlessEUIDCheck(config *configs.Config) error {
2829
return nil
2930
}
3031

31-
func hasIDMapping(id int, mappings []configs.IDMap) bool {
32-
for _, m := range mappings {
33-
if id >= m.ContainerID && id < m.ContainerID+m.Size {
34-
return true
35-
}
36-
}
37-
return false
38-
}
39-
4032
func rootlessEUIDMappings(config *configs.Config) error {
4133
if !config.Namespaces.Contains(configs.NEWUSER) {
4234
return errors.New("rootless container requires user namespaces")
4335
}
44-
45-
if len(config.UIDMappings) == 0 {
46-
return errors.New("rootless containers requires at least one UID mapping")
47-
}
48-
if len(config.GIDMappings) == 0 {
49-
return errors.New("rootless containers requires at least one GID mapping")
36+
// We only require mappings if we are not joining another userns.
37+
if path := config.Namespaces.PathOf(configs.NEWUSER); path == "" {
38+
if len(config.UIDMappings) == 0 {
39+
return errors.New("rootless containers requires at least one UID mapping")
40+
}
41+
if len(config.GIDMappings) == 0 {
42+
return errors.New("rootless containers requires at least one GID mapping")
43+
}
5044
}
5145
return nil
5246
}
@@ -68,8 +62,8 @@ func rootlessEUIDMount(config *configs.Config) error {
6862
// Ignore unknown mount options.
6963
continue
7064
}
71-
if !hasIDMapping(uid, config.UIDMappings) {
72-
return errors.New("cannot specify uid= mount options for unmapped uid in rootless containers")
65+
if _, err := config.HostUID(uid); err != nil {
66+
return fmt.Errorf("cannot specify uid=%d mount option for rootless container: %w", uid, err)
7367
}
7468
}
7569

@@ -79,8 +73,8 @@ func rootlessEUIDMount(config *configs.Config) error {
7973
// Ignore unknown mount options.
8074
continue
8175
}
82-
if !hasIDMapping(gid, config.GIDMappings) {
83-
return errors.New("cannot specify gid= mount options for unmapped gid in rootless containers")
76+
if _, err := config.HostGID(gid); err != nil {
77+
return fmt.Errorf("cannot specify gid=%d mount option for rootless container: %w", gid, err)
8478
}
8579
}
8680
}

libcontainer/configs/validate/validator.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,11 +104,19 @@ func security(config *configs.Config) error {
104104
func namespaces(config *configs.Config) error {
105105
if config.Namespaces.Contains(configs.NEWUSER) {
106106
if _, err := os.Stat("/proc/self/ns/user"); os.IsNotExist(err) {
107-
return errors.New("USER namespaces aren't enabled in the kernel")
107+
return errors.New("user namespaces aren't enabled in the kernel")
108108
}
109+
hasPath := config.Namespaces.PathOf(configs.NEWUSER) != ""
110+
hasMappings := config.UIDMappings != nil || config.GIDMappings != nil
111+
if !hasPath && !hasMappings {
112+
return errors.New("user namespaces enabled, but no namespace path to join nor mappings to apply specified")
113+
}
114+
// The hasPath && hasMappings validation case is handled in specconv --
115+
// we cache the mappings in Config during specconv in the hasPath case,
116+
// so we cannot do that validation here.
109117
} else {
110118
if config.UIDMappings != nil || config.GIDMappings != nil {
111-
return errors.New("User namespace mappings specified, but USER namespace isn't enabled in the config")
119+
return errors.New("user namespace mappings specified, but user namespace isn't enabled in the config")
112120
}
113121
}
114122

@@ -122,6 +130,11 @@ func namespaces(config *configs.Config) error {
122130
if _, err := os.Stat("/proc/self/timens_offsets"); os.IsNotExist(err) {
123131
return errors.New("time namespaces aren't enabled in the kernel")
124132
}
133+
hasPath := config.Namespaces.PathOf(configs.NEWTIME) != ""
134+
hasOffsets := config.TimeOffsets != nil
135+
if hasPath && hasOffsets {
136+
return errors.New("time namespace enabled, but both namespace path and time offsets specified -- you may only provide one")
137+
}
125138
} else {
126139
if config.TimeOffsets != nil {
127140
return errors.New("time namespace offsets specified, but time namespace isn't enabled in the config")

libcontainer/configs/validate/validator_test.go

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ func TestValidateSecurityWithoutNEWNS(t *testing.T) {
170170
}
171171
}
172172

173-
func TestValidateUsernamespace(t *testing.T) {
173+
func TestValidateUserNamespace(t *testing.T) {
174174
if _, err := os.Stat("/proc/self/ns/user"); os.IsNotExist(err) {
175175
t.Skip("Test requires userns.")
176176
}
@@ -181,6 +181,8 @@ func TestValidateUsernamespace(t *testing.T) {
181181
{Type: configs.NEWUSER},
182182
},
183183
),
184+
UIDMappings: []configs.IDMap{{HostID: 0, ContainerID: 123, Size: 100}},
185+
GIDMappings: []configs.IDMap{{HostID: 0, ContainerID: 123, Size: 100}},
184186
}
185187

186188
err := Validate(config)
@@ -189,11 +191,11 @@ func TestValidateUsernamespace(t *testing.T) {
189191
}
190192
}
191193

192-
func TestValidateUsernamespaceWithoutUserNS(t *testing.T) {
193-
uidMap := configs.IDMap{ContainerID: 123}
194+
func TestValidateUsernsMappingWithoutNamespace(t *testing.T) {
194195
config := &configs.Config{
195196
Rootfs: "/var",
196-
UIDMappings: []configs.IDMap{uidMap},
197+
UIDMappings: []configs.IDMap{{HostID: 0, ContainerID: 123, Size: 100}},
198+
GIDMappings: []configs.IDMap{{HostID: 0, ContainerID: 123, Size: 100}},
197199
}
198200

199201
err := Validate(config)
@@ -221,6 +223,29 @@ func TestValidateTimeNamespace(t *testing.T) {
221223
}
222224
}
223225

226+
func TestValidateTimeNamespaceWithBothPathAndTimeOffset(t *testing.T) {
227+
if _, err := os.Stat("/proc/self/ns/time"); os.IsNotExist(err) {
228+
t.Skip("Test requires timens.")
229+
}
230+
config := &configs.Config{
231+
Rootfs: "/var",
232+
Namespaces: configs.Namespaces(
233+
[]configs.Namespace{
234+
{Type: configs.NEWTIME, Path: "/proc/1/ns/time"},
235+
},
236+
),
237+
TimeOffsets: map[string]specs.LinuxTimeOffset{
238+
"boottime": {Secs: 150, Nanosecs: 314159},
239+
"monotonic": {Secs: 512, Nanosecs: 271818},
240+
},
241+
}
242+
243+
err := Validate(config)
244+
if err == nil {
245+
t.Error("Expected error to occur but it was nil")
246+
}
247+
}
248+
224249
func TestValidateTimeOffsetsWithoutTimeNamespace(t *testing.T) {
225250
config := &configs.Config{
226251
Rootfs: "/var",

libcontainer/init_linux.go

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -494,15 +494,6 @@ func setupUser(config *initConfig) error {
494494
}
495495
}
496496

497-
// Rather than just erroring out later in setuid(2) and setgid(2), check
498-
// that the user is mapped here.
499-
if _, err := config.Config.HostUID(execUser.Uid); err != nil {
500-
return errors.New("cannot set uid to unmapped user in user namespace")
501-
}
502-
if _, err := config.Config.HostGID(execUser.Gid); err != nil {
503-
return errors.New("cannot set gid to unmapped user in user namespace")
504-
}
505-
506497
if config.RootlessEUID {
507498
// We cannot set any additional groups in a rootless container and thus
508499
// we bail if the user asked us to do so. TODO: We currently can't do
@@ -538,9 +529,15 @@ func setupUser(config *initConfig) error {
538529
}
539530

540531
if err := unix.Setgid(execUser.Gid); err != nil {
532+
if err == unix.EINVAL {
533+
return fmt.Errorf("cannot setgid to unmapped gid %d in user namespace", execUser.Gid)
534+
}
541535
return err
542536
}
543537
if err := unix.Setuid(execUser.Uid); err != nil {
538+
if err == unix.EINVAL {
539+
return fmt.Errorf("cannot setuid to unmapped uid %d in user namespace", execUser.Uid)
540+
}
544541
return err
545542
}
546543

0 commit comments

Comments
 (0)