The following pull request was submitted through Github. It can be accessed and reviewed at: https://github.com/lxc/lxd/pull/2622
This e-mail was sent by the LXC bot, direct replies will not reach the author unless they happen to be subscribed to this list. === Description (from pull-request) === This adds support for custom idmaps to LXD, and in particular adds a simple mechanism to isolate containers from each others' idmaps entirely.
From 43b2164c38fbae4f736e21b364926610ecbe4bcc Mon Sep 17 00:00:00 2001 From: Tycho Andersen <tycho.ander...@canonical.com> Date: Thu, 10 Nov 2016 09:38:40 +0200 Subject: [PATCH 1/3] add the id_map API extension This first bit adds support for security.idmap.{size,isolated}, which allow for configuring the idmaps of each individual container. Signed-off-by: Tycho Andersen <tycho.ander...@canonical.com> --- doc/api-extensions.md | 4 ++ lxd/api_1.0.go | 1 + lxd/container.go | 23 ++++++ lxd/container_lxc.go | 177 ++++++++++++++++++++++++++++++++++++++++++++++- lxd/container_test.go | 82 ++++++++++++++++++++++ lxd/main_test.go | 7 ++ shared/container.go | 18 +++++ shared/idmapset_linux.go | 14 ++++ 8 files changed, 323 insertions(+), 3 deletions(-) diff --git a/doc/api-extensions.md b/doc/api-extensions.md index fc592c4..3542f7a 100644 --- a/doc/api-extensions.md +++ b/doc/api-extensions.md @@ -156,3 +156,7 @@ Enables adding GPUs to a container. ## container\_image\_properties Introduces a new "image" config key space. Read-only, includes the properties of the parent image. + +## id\_map +Enables setting the `security.idmap.isolated` and `security.idmap.isolated`, +`security.idmap.size`, and `raw.id_map` fields. diff --git a/lxd/api_1.0.go b/lxd/api_1.0.go index c4654e2..8f44225 100644 --- a/lxd/api_1.0.go +++ b/lxd/api_1.0.go @@ -77,6 +77,7 @@ func api10Get(d *Daemon, r *http.Request) Response { "container_exec_signal_handling", "gpu_devices", "container_image_properties", + "id_map", }, "api_status": "stable", diff --git a/lxd/container.go b/lxd/container.go index 95e9c12..ad462b9 100644 --- a/lxd/container.go +++ b/lxd/container.go @@ -1,6 +1,7 @@ package main import ( + "encoding/json" "fmt" "io" "os" @@ -645,6 +646,28 @@ func containerCreateInternal(d *Daemon, args containerArgs) (container, error) { // Wipe any existing log for this container name os.RemoveAll(shared.LogPath(args.Name)) + idmap, base, err := findIdmap( + d, + args.Name, + args.Config["security.idmap.isolated"], + args.Config["security.idmap.size"], + ) + if err != nil { + return nil, err + } + var jsonIdmap string + if idmap != nil { + idmapBytes, err := json.Marshal(idmap.Idmap) + if err != nil { + return nil, err + } + jsonIdmap = string(idmapBytes) + } else { + jsonIdmap = "[]" + } + args.Config["volatile.idmap.next"] = jsonIdmap + args.Config["volatile.idmap.base"] = fmt.Sprintf("%v", base) + // Create the container entry id, err := dbContainerCreate(d.db, args) if err != nil { diff --git a/lxd/container_lxc.go b/lxd/container_lxc.go index d178fdb..f9b03ee 100644 --- a/lxd/container_lxc.go +++ b/lxd/container_lxc.go @@ -13,6 +13,7 @@ import ( "path" "path/filepath" "reflect" + "sort" "strconv" "strings" "sync" @@ -235,6 +236,12 @@ func containerLXCCreate(d *Daemon, args containerArgs) (container, error) { return nil, err } + err = c.ConfigKeySet("volatile.idmap.next", jsonIdmap) + if err != nil { + c.Delete() + return nil, err + } + // Update lease files networkUpdateStatic(d) @@ -345,6 +352,134 @@ func (c *containerLXC) waitOperation() error { return nil } +func idmapSize(daemon *Daemon, isolatedStr string, size string) (int, error) { + isolated := false + if isolatedStr == "true" { + isolated = true + } + + var idMapSize int + if size == "" || size == "auto" { + if isolated { + idMapSize = 65536 + } else { + if len(daemon.IdmapSet.Idmap) != 2 { + return 0, fmt.Errorf("bad initial idmap: %v", daemon.IdmapSet) + } + + idMapSize = daemon.IdmapSet.Idmap[0].Maprange + } + } else { + size, err := strconv.ParseInt(size, 10, 32) + if err != nil { + return 0, err + } + + idMapSize = int(size) + } + + return idMapSize, nil +} + +var idmapLock sync.Mutex + +func findIdmap(daemon *Daemon, cName string, isolatedStr string, configSize string) (*shared.IdmapSet, int, error) { + isolated := false + if isolatedStr == "true" { + isolated = true + } + + if !isolated { + return daemon.IdmapSet, 0, nil + } + + idmapLock.Lock() + defer idmapLock.Unlock() + + cs, err := dbContainersList(daemon.db, cTypeRegular) + if err != nil { + return nil, 0, err + } + + offset := daemon.IdmapSet.Idmap[0].Hostid + 65536 + size, err := idmapSize(daemon, isolatedStr, configSize) + if err != nil { + return nil, 0, err + } + + mapentries := shared.ByHostid{} + for _, name := range cs { + /* Don't change our map Just Because. */ + if name == cName { + continue + } + + container, err := containerLoadByName(daemon, name) + if err != nil { + return nil, 0, err + } + + if container.ExpandedConfig()["security.idmap.isolated"] != "true" { + continue + } + + cBase, err := strconv.ParseInt(container.ExpandedConfig()["volatile.idmap.base"], 10, 32) + if err != nil { + return nil, 0, err + } + + cSize, err := idmapSize(daemon, container.ExpandedConfig()["security.idmap.isolated"], container.ExpandedConfig()["security.idmap.size"]) + if err != nil { + return nil, 0, err + } + + mapentries = append(mapentries, &shared.IdmapEntry{Hostid: int(cBase), Maprange: cSize}) + } + + sort.Sort(mapentries) + + for i := range mapentries { + if i == 0 { + if mapentries[0].Hostid < offset+size { + offset = mapentries[0].Hostid + mapentries[0].Maprange + continue + } + + set := &shared.IdmapSet{Idmap: []shared.IdmapEntry{ + shared.IdmapEntry{Isuid: true, Nsid: 0, Hostid: offset, Maprange: size}, + shared.IdmapEntry{Isgid: true, Nsid: 0, Hostid: offset, Maprange: size}, + }} + + return set, offset, nil + } + + if mapentries[i-1].Hostid+mapentries[i-1].Maprange > offset { + continue + } + + offset = mapentries[i-1].Hostid + mapentries[i-1].Maprange + if offset+size < mapentries[i].Nsid { + set := &shared.IdmapSet{Idmap: []shared.IdmapEntry{ + shared.IdmapEntry{Isuid: true, Nsid: 0, Hostid: offset, Maprange: size}, + shared.IdmapEntry{Isgid: true, Nsid: 0, Hostid: offset, Maprange: size}, + }} + + return set, offset, nil + } + } + + if offset+size < daemon.IdmapSet.Idmap[0].Hostid+daemon.IdmapSet.Idmap[0].Maprange { + set := &shared.IdmapSet{Idmap: []shared.IdmapEntry{ + shared.IdmapEntry{Isuid: true, Nsid: 0, Hostid: offset, Maprange: size}, + shared.IdmapEntry{Isgid: true, Nsid: 0, Hostid: offset, Maprange: size}, + }} + + return set, offset, nil + } + + return nil, 0, fmt.Errorf("no map range available") +} + func (c *containerLXC) init() error { // Compute the expanded config and device list err := c.expandConfig() @@ -362,7 +497,11 @@ func (c *containerLXC) init() error { if c.daemon.IdmapSet == nil { return fmt.Errorf("LXD doesn't have a uid/gid allocation. In this mode, only privileged containers are supported.") } - c.idmapset = c.daemon.IdmapSet + + c.idmapset, err = c.NextIdmapSet() + if err != nil { + return err + } } return nil @@ -2521,6 +2660,29 @@ func (c *containerLXC) Update(args containerArgs, userRequested bool) error { } }() + // update the idmap + idmap, base, err := findIdmap( + c.daemon, + c.Name(), + args.Config["security.idmap.isolated"], + args.Config["security.idmap.size"], + ) + if err != nil { + return err + } + var jsonIdmap string + if idmap != nil { + idmapBytes, err := json.Marshal(idmap.Idmap) + if err != nil { + return err + } + jsonIdmap = string(idmapBytes) + } else { + jsonIdmap = "[]" + } + args.Config["volatile.idmap.next"] = jsonIdmap + args.Config["volatile.idmap.base"] = fmt.Sprintf("%v", base) + // Apply the various changes c.architecture = args.Architecture c.ephemeral = args.Ephemeral @@ -5648,8 +5810,8 @@ func (c *containerLXC) LocalDevices() shared.Devices { return c.localDevices } -func (c *containerLXC) LastIdmapSet() (*shared.IdmapSet, error) { - lastJsonIdmap := c.LocalConfig()["volatile.last_state.idmap"] +func (c *containerLXC) idmapsetFromConfig(k string) (*shared.IdmapSet, error) { + lastJsonIdmap := c.LocalConfig()[k] if lastJsonIdmap == "" { return c.IdmapSet(), nil @@ -5668,6 +5830,15 @@ func (c *containerLXC) LastIdmapSet() (*shared.IdmapSet, error) { return lastIdmap, nil } +func (c *containerLXC) NextIdmapSet() (*shared.IdmapSet, error) { + return c.idmapsetFromConfig("volatile.idmap.next") + +} + +func (c *containerLXC) LastIdmapSet() (*shared.IdmapSet, error) { + return c.idmapsetFromConfig("volatile.last_state.idmap") +} + func (c *containerLXC) Daemon() *Daemon { // FIXME: This function should go away return c.daemon diff --git a/lxd/container_test.go b/lxd/container_test.go index 13b608a..ea237b3 100644 --- a/lxd/container_test.go +++ b/lxd/container_test.go @@ -220,3 +220,85 @@ func (suite *lxdTestSuite) TestContainer_Rename() { suite.Req.Nil(c.Rename("testFoo2"), "Failed to rename the container.") suite.Req.Equal(shared.VarPath("containers", "testFoo2"), c.Path()) } + +func (suite *lxdTestSuite) TestContainer_findIdmap_isolated() { + c1, err := containerCreateInternal(suite.d, containerArgs{ + Ctype: cTypeRegular, + Name: "isol-1", + Config: map[string]string{ + "security.idmap.isolated": "true", + }, + }) + suite.Req.Nil(err) + defer c1.Delete() + + c2, err := containerCreateInternal(suite.d, containerArgs{ + Ctype: cTypeRegular, + Name: "isol-2", + Config: map[string]string{ + "security.idmap.isolated": "true", + }, + }) + suite.Req.Nil(err) + defer c2.Delete() + + map1, err := c1.(*containerLXC).NextIdmapSet() + suite.Req.Nil(err) + map2, err := c2.(*containerLXC).NextIdmapSet() + suite.Req.Nil(err) + + host := suite.d.IdmapSet.Idmap[0] + + for i := 0; i < 2; i++ { + suite.Req.Equal(host.Hostid+65536, map1.Idmap[i].Hostid, "hostids don't match %d", i) + suite.Req.Equal(0, map1.Idmap[i].Nsid, "nsid nonzero") + suite.Req.Equal(65536, map1.Idmap[i].Maprange, "incorrect maprange") + } + + for i := 0; i < 2; i++ { + suite.Req.Equal(host.Hostid+65536*2, map2.Idmap[i].Hostid, "hostids don't match") + suite.Req.Equal(0, map2.Idmap[i].Nsid, "nsid nonzero") + suite.Req.Equal(65536, map2.Idmap[i].Maprange, "incorrect maprange") + } +} + +func (suite *lxdTestSuite) TestContainer_findIdmap_mixed() { + c1, err := containerCreateInternal(suite.d, containerArgs{ + Ctype: cTypeRegular, + Name: "isol-1", + Config: map[string]string{ + "security.idmap.isolated": "false", + }, + }) + suite.Req.Nil(err) + defer c1.Delete() + + c2, err := containerCreateInternal(suite.d, containerArgs{ + Ctype: cTypeRegular, + Name: "isol-2", + Config: map[string]string{ + "security.idmap.isolated": "true", + }, + }) + suite.Req.Nil(err) + defer c2.Delete() + + map1, err := c1.(*containerLXC).NextIdmapSet() + suite.Req.Nil(err) + map2, err := c2.(*containerLXC).NextIdmapSet() + suite.Req.Nil(err) + + host := suite.d.IdmapSet.Idmap[0] + + for i := 0; i < 2; i++ { + suite.Req.Equal(host.Hostid, map1.Idmap[i].Hostid, "hostids don't match %d", i) + suite.Req.Equal(0, map1.Idmap[i].Nsid, "nsid nonzero") + suite.Req.Equal(host.Maprange, map1.Idmap[i].Maprange, "incorrect maprange") + } + + for i := 0; i < 2; i++ { + suite.Req.Equal(host.Hostid+65536+1, map2.Idmap[i].Hostid, "hostids don't match") + suite.Req.Equal(0, map2.Idmap[i].Nsid, "nsid nonzero") + suite.Req.Equal(65536, map2.Idmap[i].Maprange, "incorrect maprange") + } +} diff --git a/lxd/main_test.go b/lxd/main_test.go index 7a61429..262c541 100644 --- a/lxd/main_test.go +++ b/lxd/main_test.go @@ -8,6 +8,8 @@ import ( "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" + + "github.com/lxc/lxd/shared" ) func mockStartDaemon() (*Daemon, error) { @@ -21,6 +23,11 @@ func mockStartDaemon() (*Daemon, error) { return nil, err } + d.IdmapSet = &shared.IdmapSet{Idmap: []shared.IdmapEntry{ + shared.IdmapEntry{Isuid: true, Hostid: 100000, Nsid: 0, Maprange: 500000}, + shared.IdmapEntry{Isgid: true, Hostid: 100000, Nsid: 0, Maprange: 500000}, + }} + // Call this after Init so we have a log object. storageConfig := make(map[string]interface{}) d.Storage = &storageLogWrapper{w: &storageMock{d: d}} diff --git a/shared/container.go b/shared/container.go index 36964c7..aac0322 100644 --- a/shared/container.go +++ b/shared/container.go @@ -174,6 +174,19 @@ func IsInt64(value string) error { return nil } +func IsUint32(value string) error { + if value == "" { + return nil + } + + _, err := strconv.ParseInt(value, 10, 32) + if err != nil { + return fmt.Errorf("Invalid value for uint32: %s: %v", value, err) + } + + return nil +} + func IsPriority(value string) error { if value == "" { return nil @@ -302,6 +315,9 @@ var KnownContainerConfigKeys = map[string]func(value string) error{ "security.nesting": IsBool, "security.privileged": IsBool, + "security.idmap.size": IsUint32, + "security.idmap.isolated": IsBool, + "security.syscalls.blacklist_default": IsBool, "security.syscalls.blacklist_compat": IsBool, "security.syscalls.blacklist": IsAny, @@ -316,6 +332,8 @@ var KnownContainerConfigKeys = map[string]func(value string) error{ "volatile.base_image": IsAny, "volatile.last_state.idmap": IsAny, "volatile.last_state.power": IsAny, + "volatile.idmap.next": IsAny, + "volatile.idmap.base": IsAny, } // ConfigKeyChecker returns a function that will check whether or not diff --git a/shared/idmapset_linux.go b/shared/idmapset_linux.go index 4e59d69..b08601a 100644 --- a/shared/idmapset_linux.go +++ b/shared/idmapset_linux.go @@ -123,6 +123,20 @@ func (e *IdmapEntry) shift_from_ns(id int) (int, error) { return id - e.Hostid + e.Nsid, nil } +type ByHostid []*IdmapEntry + +func (s ByHostid) Len() int { + return len(s) +} + +func (s ByHostid) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +func (s ByHostid) Less(i, j int) bool { + return s[i].Hostid < s[j].Hostid +} + /* taken from http://blog.golang.org/slices (which is under BSD licence) */ func Extend(slice []IdmapEntry, element IdmapEntry) []IdmapEntry { n := len(slice) From 0bbcd15667ba7442f2f5fd39fc9719b17d548606 Mon Sep 17 00:00:00 2001 From: Tycho Andersen <tycho.ander...@canonical.com> Date: Wed, 16 Nov 2016 17:19:50 -0700 Subject: [PATCH 2/3] add support for raw.idmap Signed-off-by: Tycho Andersen <tycho.ander...@canonical.com> --- lxd/container.go | 1 + lxd/container_lxc.go | 132 ++++++++++++++++++++++++++++++++++++++++------- lxd/container_test.go | 38 +++++++++++++- shared/container.go | 1 + shared/idmapset_linux.go | 68 ++++++++++++++++++++++++ shared/idmapset_test.go | 83 +++++++++++++++++++++++++++++ test/suites/filemanip.sh | 5 ++ 7 files changed, 307 insertions(+), 21 deletions(-) create mode 100644 shared/idmapset_test.go diff --git a/lxd/container.go b/lxd/container.go index ad462b9..b40961a 100644 --- a/lxd/container.go +++ b/lxd/container.go @@ -651,6 +651,7 @@ func containerCreateInternal(d *Daemon, args containerArgs) (container, error) { args.Name, args.Config["security.idmap.isolated"], args.Config["security.idmap.size"], + args.Config["raw.idmap"], ) if err != nil { return nil, err diff --git a/lxd/container_lxc.go b/lxd/container_lxc.go index f9b03ee..7f2a08b 100644 --- a/lxd/container_lxc.go +++ b/lxd/container_lxc.go @@ -383,14 +383,107 @@ func idmapSize(daemon *Daemon, isolatedStr string, size string) (int, error) { var idmapLock sync.Mutex -func findIdmap(daemon *Daemon, cName string, isolatedStr string, configSize string) (*shared.IdmapSet, int, error) { +func parseRawIdmap(value string) ([]shared.IdmapEntry, error) { + getRange := func(r string) (int, int, error) { + entries := strings.Split(r, "-") + if len(entries) > 2 { + return -1, -1, fmt.Errorf("invalid raw.idmap range %s", r) + } + + base, err := strconv.Atoi(entries[0]) + if err != nil { + return -1, -1, err + } + + size := 1 + if len(entries) > 1 { + size, err = strconv.Atoi(entries[1]) + if err != nil { + return -1, -1, err + } + + size -= base + } + + return base, size, nil + } + + ret := shared.IdmapSet{} + + for _, line := range strings.Split(value, "\n") { + if line == "" { + continue + } + + entries := strings.Split(line, " ") + if len(entries) != 3 { + return nil, fmt.Errorf("invalid raw.idmap line %s", line) + } + + outsideBase, outsideSize, err := getRange(entries[1]) + if err != nil { + return nil, err + } + + insideBase, insideSize, err := getRange(entries[2]) + if err != nil { + return nil, err + } + + if insideSize != outsideSize { + return nil, fmt.Errorf("idmap ranges of different sizes %s", line) + } + + entry := shared.IdmapEntry{ + Hostid: outsideBase, + Nsid: insideBase, + Maprange: insideSize, + } + + switch entries[0] { + case "both": + entry.Isuid = true + ret.AddSafe(entry) + ret.AddSafe(shared.IdmapEntry{ + Isgid: true, + Hostid: entry.Hostid, + Nsid: entry.Nsid, + Maprange: entry.Maprange, + }) + case "uid": + entry.Isuid = true + ret.AddSafe(entry) + case "gid": + entry.Isgid = true + ret.AddSafe(entry) + default: + return nil, fmt.Errorf("invalid raw.idmap type %s", line) + } + } + + return ret.Idmap, nil +} + +func findIdmap(daemon *Daemon, cName string, isolatedStr string, configSize string, rawIdmap string) (*shared.IdmapSet, int, error) { isolated := false if isolatedStr == "true" { isolated = true } + rawMaps, err := parseRawIdmap(rawIdmap) + if err != nil { + return nil, 0, err + } + if !isolated { - return daemon.IdmapSet, 0, nil + newIdmapset := shared.IdmapSet{Idmap: make([]shared.IdmapEntry, len(daemon.IdmapSet.Idmap))} + copy(newIdmapset.Idmap, daemon.IdmapSet.Idmap) + + for _, ent := range rawMaps { + newIdmapset.AddSafe(ent) + } + + return &newIdmapset, 0, nil } idmapLock.Lock() @@ -438,6 +531,19 @@ func findIdmap(daemon *Daemon, cName string, isolatedStr string, configSize stri sort.Sort(mapentries) + mkIdmap := func(offset int, size int) *shared.IdmapSet { + set := &shared.IdmapSet{Idmap: []shared.IdmapEntry{ + shared.IdmapEntry{Isuid: true, Nsid: 0, Hostid: offset, Maprange: size}, + shared.IdmapEntry{Isgid: true, Nsid: 0, Hostid: offset, Maprange: size}, + }} + + for _, ent := range rawMaps { + set.AddSafe(ent) + } + + return set + } + for i := range mapentries { if i == 0 { if mapentries[0].Hostid < offset+size { @@ -445,12 +551,7 @@ func findIdmap(daemon *Daemon, cName string, isolatedStr string, configSize stri continue } - set := &shared.IdmapSet{Idmap: []shared.IdmapEntry{ - shared.IdmapEntry{Isuid: true, Nsid: 0, Hostid: offset, Maprange: size}, - shared.IdmapEntry{Isgid: true, Nsid: 0, Hostid: offset, Maprange: size}, - }} - - return set, offset, nil + return mkIdmap(offset, size), offset, nil } if mapentries[i-1].Hostid+mapentries[i-1].Maprange > offset { @@ -459,22 +560,12 @@ func findIdmap(daemon *Daemon, cName string, isolatedStr string, configSize stri offset = mapentries[i-1].Hostid + mapentries[i-1].Maprange if offset+size < mapentries[i].Nsid { - set := &shared.IdmapSet{Idmap: []shared.IdmapEntry{ - shared.IdmapEntry{Isuid: true, Nsid: 0, Hostid: offset, Maprange: size}, - shared.IdmapEntry{Isgid: true, Nsid: 0, Hostid: offset, Maprange: size}, - }} - - return set, offset, nil + return mkIdmap(offset, size), offset, nil } } if offset+size < daemon.IdmapSet.Idmap[0].Hostid+daemon.IdmapSet.Idmap[0].Maprange { - set := &shared.IdmapSet{Idmap: []shared.IdmapEntry{ - shared.IdmapEntry{Isuid: true, Nsid: 0, Hostid: offset, Maprange: size}, - shared.IdmapEntry{Isgid: true, Nsid: 0, Hostid: offset, Maprange: size}, - }} - - return set, offset, nil + return mkIdmap(offset, size), offset, nil } return nil, 0, fmt.Errorf("no map range available") @@ -2666,6 +2757,7 @@ func (c *containerLXC) Update(args containerArgs, userRequested bool) error { c.Name(), args.Config["security.idmap.isolated"], args.Config["security.idmap.size"], + args.Config["raw.idmap"], ) if err != nil { return err diff --git a/lxd/container_test.go b/lxd/container_test.go index ea237b3..f74d3b6 100644 --- a/lxd/container_test.go +++ b/lxd/container_test.go @@ -297,8 +297,44 @@ func (suite *lxdTestSuite) TestContainer_findIdmap_mixed() { } for i := 0; i < 2; i++ { - suite.Req.Equal(host.Hostid+65536+1, map2.Idmap[i].Hostid, "hostids don't match") + suite.Req.Equal(host.Hostid+65536, map2.Idmap[i].Hostid, "hostids don't match") suite.Req.Equal(0, map2.Idmap[i].Nsid, "nsid nonzero") suite.Req.Equal(65536, map2.Idmap[i].Maprange, "incorrect maprange") } } + +func (suite *lxdTestSuite) TestContainer_findIdmap_raw() { + c1, err := containerCreateInternal(suite.d, containerArgs{ + Ctype: cTypeRegular, + Name: "isol-1", + Config: map[string]string{ + "security.idmap.isolated": "false", + "raw.idmap": "both 1000 1000", + }, + }) + suite.Req.Nil(err) + defer c1.Delete() + + map1, err := c1.(*containerLXC).NextIdmapSet() + suite.Req.Nil(err) + + host := suite.d.IdmapSet.Idmap[0] + + for _, i := range []int{0, 3} { + suite.Req.Equal(host.Hostid, map1.Idmap[i].Hostid, "hostids don't match") + suite.Req.Equal(0, map1.Idmap[i].Nsid, "nsid nonzero") + suite.Req.Equal(1000, map1.Idmap[i].Maprange, "incorrect maprange") + } + + for _, i := range []int{1, 4} { + suite.Req.Equal(1000, map1.Idmap[i].Hostid, "hostids don't match") + suite.Req.Equal(1000, map1.Idmap[i].Nsid, "invalid nsid") + suite.Req.Equal(1, map1.Idmap[i].Maprange, "incorrect maprange") + } + + for _, i := range []int{2, 5} { + suite.Req.Equal(host.Hostid+1001, map1.Idmap[i].Hostid, "hostids don't match") + suite.Req.Equal(1001, map1.Idmap[i].Nsid, "invalid nsid") + suite.Req.Equal(host.Maprange-1000-1, map1.Idmap[i].Maprange, "incorrect maprange") + } +} diff --git a/shared/container.go b/shared/container.go index aac0322..fd15bc5 100644 --- a/shared/container.go +++ b/shared/container.go @@ -327,6 +327,7 @@ var KnownContainerConfigKeys = map[string]func(value string) error{ "raw.apparmor": IsAny, "raw.lxc": IsAny, "raw.seccomp": IsAny, + "raw.idmap": IsAny, "volatile.apply_template": IsAny, "volatile.base_image": IsAny, diff --git a/shared/idmapset_linux.go b/shared/idmapset_linux.go index b08601a..90ba6dc 100644 --- a/shared/idmapset_linux.go +++ b/shared/idmapset_linux.go @@ -35,6 +35,23 @@ func is_between(x, low, high int) bool { return x >= low && x < high } +func (e *IdmapEntry) HostidsIntersect(i IdmapEntry) bool { + if (e.Isuid && i.Isuid) || (e.Isgid && i.Isgid) { + switch { + case is_between(e.Hostid, i.Hostid, i.Hostid+i.Maprange): + return true + case is_between(i.Hostid, e.Hostid, e.Hostid+e.Maprange): + return true + case is_between(e.Hostid+e.Maprange, i.Hostid, i.Hostid+i.Maprange): + return true + case is_between(i.Hostid+i.Maprange, e.Hostid, e.Hostid+e.Maprange): + return true + } + } + + return false +} + func (e *IdmapEntry) Intersects(i IdmapEntry) bool { if (e.Isuid && i.Isuid) || (e.Isgid && i.Isgid) { switch { @@ -169,6 +186,57 @@ func (m IdmapSet) Intersects(i IdmapEntry) bool { return false } +/* AddSafe adds an entry to the idmap set, breaking apart any ranges that the + * new idmap intersects with in the process. + */ +func (m *IdmapSet) AddSafe(i IdmapEntry) error { + result := []IdmapEntry{} + added := false + for _, e := range m.Idmap { + if !e.Intersects(i) { + result = append(result, e) + continue + } + + if e.HostidsIntersect(i) { + return fmt.Errorf("can't map the same host UID twice") + } + + added = true + + lower := IdmapEntry{ + Isuid: e.Isuid, + Isgid: e.Isgid, + Hostid: e.Hostid, + Nsid: e.Nsid, + Maprange: i.Nsid - e.Nsid, + } + + upper := IdmapEntry{ + Isuid: e.Isuid, + Isgid: e.Isgid, + Hostid: e.Hostid + lower.Maprange + i.Maprange, + Nsid: i.Nsid + i.Maprange, + Maprange: e.Maprange - i.Maprange - lower.Maprange, + } + + if lower.Maprange > 0 { + result = append(result, lower) + } + result = append(result, i) + if upper.Maprange > 0 { + result = append(result, upper) + } + } + + if !added { + result = append(result, i) + } + + m.Idmap = result + return nil +} + func (m IdmapSet) ToLxcString() []string { var lines []string for _, e := range m.Idmap { diff --git a/shared/idmapset_test.go b/shared/idmapset_test.go new file mode 100644 index 0000000..a0e6280 --- /dev/null +++ b/shared/idmapset_test.go @@ -0,0 +1,83 @@ +package shared + +import ( + "fmt" + "testing" +) + +func TestIdmapSetAddSafe_split(t *testing.T) { + orig := IdmapSet{Idmap: []IdmapEntry{IdmapEntry{Isuid: true, Hostid: 1000, Nsid: 0, Maprange: 1000}}} + + if err := orig.AddSafe(IdmapEntry{Isuid: true, Hostid: 500, Nsid: 500, Maprange: 10}); err != nil { + t.Error(err) + return + } + + if orig.Idmap[0].Hostid != 1000 || orig.Idmap[0].Nsid != 0 || orig.Idmap[0].Maprange != 500 { + t.Error(fmt.Errorf("bad range: %v", orig.Idmap[0])) + return + } + + if orig.Idmap[1].Hostid != 500 || orig.Idmap[1].Nsid != 500 || orig.Idmap[1].Maprange != 10 { + t.Error(fmt.Errorf("bad range: %v", orig.Idmap[1])) + return + } + + if orig.Idmap[2].Hostid != 1510 || orig.Idmap[2].Nsid != 510 || orig.Idmap[2].Maprange != 490 { + t.Error(fmt.Errorf("bad range: %v", orig.Idmap[2])) + return + } + + if len(orig.Idmap) != 3 { + t.Error("too many idmap entries") + return + } +} + +func TestIdmapSetAddSafe_lower(t *testing.T) { + orig := IdmapSet{Idmap: []IdmapEntry{IdmapEntry{Isuid: true, Hostid: 1000, Nsid: 0, Maprange: 1000}}} + + if err := orig.AddSafe(IdmapEntry{Isuid: true, Hostid: 500, Nsid: 0, Maprange: 10}); err != nil { + t.Error(err) + return + } + + if orig.Idmap[0].Hostid != 500 || orig.Idmap[0].Nsid != 0 || orig.Idmap[0].Maprange != 10 { + t.Error(fmt.Errorf("bad range: %v", orig.Idmap[0])) + return + } + + if orig.Idmap[1].Hostid != 1010 || orig.Idmap[1].Nsid != 10 || orig.Idmap[1].Maprange != 990 { + t.Error(fmt.Errorf("bad range: %v", orig.Idmap[1])) + return + } + + if len(orig.Idmap) != 2 { + t.Error("too many idmap entries") + return + } +} + +func TestIdmapSetAddSafe_upper(t *testing.T) { + orig := IdmapSet{Idmap: []IdmapEntry{IdmapEntry{Isuid: true, Hostid: 1000, Nsid: 0, Maprange: 1000}}} + + if err := orig.AddSafe(IdmapEntry{Isuid: true, Hostid: 500, Nsid: 995, Maprange: 10}); err != nil { + t.Error(err) + return + } + + if orig.Idmap[0].Hostid != 1000 || orig.Idmap[0].Nsid != 0 || orig.Idmap[0].Maprange != 995 { + t.Error(fmt.Errorf("bad range: %v", orig.Idmap[0])) + return + } + + if orig.Idmap[1].Hostid != 500 || orig.Idmap[1].Nsid != 995 || orig.Idmap[1].Maprange != 10 { + t.Error(fmt.Errorf("bad range: %v", orig.Idmap[1])) + return + } + + if len(orig.Idmap) != 2 { + t.Error("too many idmap entries") + return + } +} diff --git a/test/suites/filemanip.sh b/test/suites/filemanip.sh index 64503a6..171d3b4 100644 --- a/test/suites/filemanip.sh +++ b/test/suites/filemanip.sh @@ -44,4 +44,9 @@ test_filemanip() { [ "$(lxc exec filemanip cat /foo)" = "foo" ] lxc delete filemanip -f + + if [ "${LXD_BACKEND}" != "lvm" ]; then + lxc launch testimage idmap -c "raw.idmap=\"both 0 0\"" + [ "$(stat -c %u "${LXD_DIR}/containers/idmap/rootfs")" = "0" ] + fi } From 6f5e21b7c05f9df907b03aac19e5f5886e384a91 Mon Sep 17 00:00:00 2001 From: Tycho Andersen <tycho.ander...@canonical.com> Date: Wed, 16 Nov 2016 17:57:21 -0700 Subject: [PATCH 3/3] add configuration information about idmap Signed-off-by: Tycho Andersen <tycho.ander...@canonical.com> --- doc/configuration.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/configuration.md b/doc/configuration.md index 16aba91..d33a833 100644 --- a/doc/configuration.md +++ b/doc/configuration.md @@ -89,6 +89,9 @@ linux.kernel\_modules | string | - | yes raw.apparmor | blob | - | yes | - | Apparmor profile entries to be appended to the generated profile raw.lxc | blob | - | no | - | Raw LXC configuration to be appended to the generated one raw.seccomp | blob | - | no | container\_syscall\_filtering | Raw Seccomp configuration +raw.idmap | blob | - | no | id\_map | Raw idmap configuration (e.g. "both 1000 1000") +security.idmap.isolated | boolean | false | no | id\_map | Use an idmap for this container that is unique among containers with isolated set. +security.idmap.size | integer | - | no | id\_map | The size of the idmap to use security.nesting | boolean | false | yes | - | Support running lxd (nested) inside the container security.privileged | boolean | false | no | - | Runs the container in privileged mode security.syscalls.blacklist\_default | boolean | true | no | container\_syscall\_filtering | Enables the default syscall blacklist @@ -105,6 +108,8 @@ volatile.\<name\>.hwaddr | string | - | Network device MAC add volatile.\<name\>.name | string | - | Network device name (when no name propery is set on the device itself) volatile.apply\_template | string | - | The name of a template hook which should be triggered upon next startup volatile.base\_image | string | - | The hash of the image the container was created from, if any. +volatile.idmap.base | integer | - | The first id in the container's primary idmap range +volatile.idmap.next | string | - | The idmap to use next time the container starts volatile.last\_state.idmap | string | - | Serialized container uid/gid map volatile.last\_state.power | string | - | Container state as of last host shutdown
_______________________________________________ lxc-devel mailing list lxc-devel@lists.linuxcontainers.org http://lists.linuxcontainers.org/listinfo/lxc-devel