[lxc-devel] [lxd/master] Multi idmap

tych0 on Github lxc-bot at linuxcontainers.org
Thu Nov 17 01:17:20 UTC 2016


A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 443 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20161117/878d1882/attachment.bin>
-------------- next part --------------
From 43b2164c38fbae4f736e21b364926610ecbe4bcc Mon Sep 17 00:00:00 2001
From: Tycho Andersen <tycho.andersen at 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.andersen at 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.andersen at 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.andersen at 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.andersen at 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.andersen at 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
 


More information about the lxc-devel mailing list