From e390d8534948a4d7d86bb8bc348214b2e614d446 Mon Sep 17 00:00:00 2001 From: David Son Date: Thu, 26 Sep 2024 20:01:50 +0000 Subject: [PATCH] Add idtools package Taken from containerd commit 83aaa89, this adds the necessary tools to add idmapping capabilities to SOCI. Signed-off-by: David Son --- idtools/idmap.go | 169 +++++++++++++++++++++++++++ idtools/idmap_test.go | 266 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 435 insertions(+) create mode 100644 idtools/idmap.go create mode 100644 idtools/idmap_test.go diff --git a/idtools/idmap.go b/idtools/idmap.go new file mode 100644 index 000000000..d5d677285 --- /dev/null +++ b/idtools/idmap.go @@ -0,0 +1,169 @@ +/* + Copyright The Soci Snapshotter Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +/* + Copyright The containerd Authors. + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +// Copied from https://github.com/containerd/containerd/blob/2ca3ff87255a4aa6b4244cb942033d45b6d44546/internal/userns/idmap.go + +/* + This file is copied and customized based on + https://github.com/moby/moby/blob/master/pkg/idtools/idtools.go +*/ + +package idtools + +import ( + "errors" + "fmt" + "strings" + + "github.com/opencontainers/runtime-spec/specs-go" +) + +const invalidID = 1<<32 - 1 + +var invalidUser = User{Uid: invalidID, Gid: invalidID} + +// User is a Uid and Gid pair of a user +// +//nolint:revive +type User struct { + Uid uint32 + Gid uint32 +} + +// IDMap contains the mappings of Uids and Gids. +// +//nolint:revive +type IDMap struct { + UidMap []specs.LinuxIDMapping `json:"UidMap"` + GidMap []specs.LinuxIDMapping `json:"GidMap"` +} + +// ToHost returns the host user ID pair for the container ID pair. +func (i IDMap) ToHost(pair User) (User, error) { + var ( + target User + err error + ) + + if i.Empty() { + return pair, nil + } + + target.Uid, err = toHost(pair.Uid, i.UidMap) + if err != nil { + return invalidUser, err + } + target.Gid, err = toHost(pair.Gid, i.GidMap) + if err != nil { + return invalidUser, err + } + return target, nil +} + +// Empty returns true if there are no id mappings +func (i IDMap) Empty() bool { + return len(i.UidMap) == 0 && len(i.GidMap) == 0 +} + +// toHost takes an id mapping and a remapped ID, and translates the +// ID to the mapped host ID. If no map is provided, then the translation +// assumes a 1-to-1 mapping and returns the passed in id # +func toHost(contID uint32, idMap []specs.LinuxIDMapping) (uint32, error) { + if idMap == nil { + return contID, nil + } + for _, m := range idMap { + high, err := safeSum(m.ContainerID, m.Size) + if err != nil { + break + } + if contID >= m.ContainerID && contID < high { + hostID, err := safeSum(m.HostID, contID-m.ContainerID) + if err != nil || hostID == invalidID { + break + } + return hostID, nil + } + } + return invalidID, fmt.Errorf("container ID %d cannot be mapped to a host ID", contID) +} + +// Unmarshal deserialize the passed uidmap and gidmap strings +// into a IDMap object. Error is returned in case of failure +func (i *IDMap) Unmarshal(uidMap, gidMap string) error { + unmarshal := func(str string, fn func(m specs.LinuxIDMapping)) error { + if len(str) == 0 { + return nil + } + for _, mapping := range strings.Split(str, ",") { + m, err := deserializeLinuxIDMapping(mapping) + if err != nil { + return err + } + fn(m) + } + return nil + } + if err := unmarshal(uidMap, func(m specs.LinuxIDMapping) { + i.UidMap = append(i.UidMap, m) + }); err != nil { + return err + } + return unmarshal(gidMap, func(m specs.LinuxIDMapping) { + i.GidMap = append(i.GidMap, m) + }) +} + +// deserializeLinuxIDMapping unmarshals a string to a LinuxIDMapping object +func deserializeLinuxIDMapping(str string) (specs.LinuxIDMapping, error) { + var ( + hostID, ctrID, length int64 + ) + _, err := fmt.Sscanf(str, "%d:%d:%d", &ctrID, &hostID, &length) + if err != nil { + return specs.LinuxIDMapping{}, fmt.Errorf("input value %s unparsable: %w", str, err) + } + if ctrID < 0 || ctrID >= invalidID || hostID < 0 || hostID >= invalidID || length < 0 || length >= invalidID { + return specs.LinuxIDMapping{}, fmt.Errorf("invalid mapping \"%s\"", str) + } + return specs.LinuxIDMapping{ + ContainerID: uint32(ctrID), + HostID: uint32(hostID), + Size: uint32(length), + }, nil +} + +// safeSum returns the sum of x and y. or an error if the result overflows +func safeSum(x, y uint32) (uint32, error) { + z := x + y + if z < x || z < y { + return invalidID, errors.New("ID overflow") + } + return z, nil +} diff --git a/idtools/idmap_test.go b/idtools/idmap_test.go new file mode 100644 index 000000000..8d1b5756c --- /dev/null +++ b/idtools/idmap_test.go @@ -0,0 +1,266 @@ +/* + Copyright The Soci Snapshotter Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +/* + Copyright The containerd Authors. + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +// Copied from https://github.com/containerd/containerd/blob/2ca3ff87255a4aa6b4244cb942033d45b6d44546/internal/userns/idmap_test.go + +package idtools + +import ( + "testing" + + "github.com/opencontainers/runtime-spec/specs-go" + "github.com/stretchr/testify/assert" +) + +func TestToHost(t *testing.T) { + idmap := IDMap{ + UidMap: []specs.LinuxIDMapping{ + { + ContainerID: 0, + HostID: 1, + Size: 2, + }, + { + ContainerID: 2, + HostID: 4, + Size: 1000, + }, + }, + GidMap: []specs.LinuxIDMapping{ + { + ContainerID: 0, + HostID: 2, + Size: 4, + }, + { + ContainerID: 4, + HostID: 8, + Size: 1000, + }, + }, + } + for _, test := range []struct { + container User + host User + }{ + { + container: User{ + Uid: 0, + Gid: 0, + }, + host: User{ + Uid: 1, + Gid: 2, + }, + }, + { + container: User{ + Uid: 1, + Gid: 1, + }, + host: User{ + Uid: 2, + Gid: 3, + }, + }, + { + container: User{ + Uid: 2, + Gid: 4, + }, + host: User{ + Uid: 4, + Gid: 8, + }, + }, + { + container: User{ + Uid: 100, + Gid: 200, + }, + host: User{ + Uid: 102, + Gid: 204, + }, + }, + { + container: User{ + Uid: 1001, + Gid: 1003, + }, + host: User{ + Uid: 1003, + Gid: 1007, + }, + }, + { + container: User{ + Uid: 1004, + Gid: 1008, + }, + host: invalidUser, + }, + { + container: User{ + Uid: 2000, + Gid: 2000, + }, + host: invalidUser, + }, + } { + r, err := idmap.ToHost(test.container) + assert.Equal(t, test.host, r) + if r == invalidUser { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + } +} +func TestToHostOverflow(t *testing.T) { + for _, test := range []struct { + idmap IDMap + user User + }{ + { + idmap: IDMap{ + UidMap: []specs.LinuxIDMapping{ + { + ContainerID: 1<<32 - 1000, + HostID: 1000, + Size: 10000, + }, + }, + GidMap: []specs.LinuxIDMapping{ + { + ContainerID: 0, + HostID: 1000, + Size: 10000, + }, + }, + }, + user: User{ + Uid: 1<<32 - 100, + Gid: 0, + }, + }, + { + idmap: IDMap{ + UidMap: []specs.LinuxIDMapping{ + { + ContainerID: 0, + HostID: 1000, + Size: 10000, + }, + }, + GidMap: []specs.LinuxIDMapping{ + { + ContainerID: 1<<32 - 1000, + HostID: 1000, + Size: 10000, + }, + }, + }, + user: User{ + Uid: 0, + Gid: 1<<32 - 100, + }, + }, + { + idmap: IDMap{ + UidMap: []specs.LinuxIDMapping{ + { + ContainerID: 0, + HostID: 1000, + Size: 1<<32 - 1, + }, + }, + GidMap: []specs.LinuxIDMapping{ + { + ContainerID: 0, + HostID: 1000, + Size: 1<<32 - 1, + }, + }, + }, + user: User{ + Uid: 1<<32 - 2, + Gid: 0, + }, + }, + { + idmap: IDMap{ + UidMap: []specs.LinuxIDMapping{ + { + ContainerID: 0, + HostID: 1000, + Size: 1<<32 - 1, + }, + }, + GidMap: []specs.LinuxIDMapping{ + { + ContainerID: 0, + HostID: 1000, + Size: 1<<32 - 1, + }, + }, + }, + user: User{ + Uid: 0, + Gid: 1<<32 - 2, + }, + }, + { + idmap: IDMap{ + UidMap: []specs.LinuxIDMapping{ + { + ContainerID: 0, + HostID: 1, + Size: 1<<32 - 1, + }, + }, + GidMap: []specs.LinuxIDMapping{ + { + ContainerID: 0, + HostID: 1, + Size: 1<<32 - 1, + }, + }, + }, + user: User{ + Uid: 1<<32 - 2, + Gid: 1<<32 - 2, + }, + }, + } { + r, err := test.idmap.ToHost(test.user) + assert.Error(t, err) + assert.Equal(t, r, invalidUser) + } +}