aboutsummaryrefslogtreecommitdiff
path: root/vendor/github.com/opencontainers/runc/libcontainer
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/opencontainers/runc/libcontainer')
-rw-r--r--vendor/github.com/opencontainers/runc/libcontainer/configs/config.go5
-rw-r--r--vendor/github.com/opencontainers/runc/libcontainer/devices/devices.go5
-rw-r--r--vendor/github.com/opencontainers/runc/libcontainer/nsenter/nsexec.c63
-rw-r--r--vendor/github.com/opencontainers/runc/libcontainer/system/linux.go38
-rw-r--r--vendor/github.com/opencontainers/runc/libcontainer/system/unsupported.go18
-rw-r--r--vendor/github.com/opencontainers/runc/libcontainer/user/lookup.go62
-rw-r--r--vendor/github.com/opencontainers/runc/libcontainer/user/lookup_unix.go96
-rw-r--r--vendor/github.com/opencontainers/runc/libcontainer/user/lookup_windows.go40
-rw-r--r--vendor/github.com/opencontainers/runc/libcontainer/user/user.go173
9 files changed, 397 insertions, 103 deletions
diff --git a/vendor/github.com/opencontainers/runc/libcontainer/configs/config.go b/vendor/github.com/opencontainers/runc/libcontainer/configs/config.go
index 3cae4fd8d..b1c4762fe 100644
--- a/vendor/github.com/opencontainers/runc/libcontainer/configs/config.go
+++ b/vendor/github.com/opencontainers/runc/libcontainer/configs/config.go
@@ -141,9 +141,10 @@ type Config struct {
// OomScoreAdj specifies the adjustment to be made by the kernel when calculating oom scores
// for a process. Valid values are between the range [-1000, '1000'], where processes with
- // higher scores are preferred for being killed.
+ // higher scores are preferred for being killed. If it is unset then we don't touch the current
+ // value.
// More information about kernel oom score calculation here: https://lwn.net/Articles/317814/
- OomScoreAdj int `json:"oom_score_adj"`
+ OomScoreAdj *int `json:"oom_score_adj,omitempty"`
// UidMappings is an array of User ID mappings for User Namespaces
UidMappings []IDMap `json:"uid_mappings"`
diff --git a/vendor/github.com/opencontainers/runc/libcontainer/devices/devices.go b/vendor/github.com/opencontainers/runc/libcontainer/devices/devices.go
index 361925890..5e2ab0581 100644
--- a/vendor/github.com/opencontainers/runc/libcontainer/devices/devices.go
+++ b/vendor/github.com/opencontainers/runc/libcontainer/devices/devices.go
@@ -30,8 +30,9 @@ func DeviceFromPath(path, permissions string) (*configs.Device, error) {
}
var (
- devNumber = stat.Rdev
+ devNumber = uint64(stat.Rdev)
major = unix.Major(devNumber)
+ minor = unix.Minor(devNumber)
)
if major == 0 {
return nil, ErrNotADevice
@@ -51,7 +52,7 @@ func DeviceFromPath(path, permissions string) (*configs.Device, error) {
Type: devType,
Path: path,
Major: int64(major),
- Minor: int64(unix.Minor(devNumber)),
+ Minor: int64(minor),
Permissions: permissions,
FileMode: os.FileMode(mode),
Uid: stat.Uid,
diff --git a/vendor/github.com/opencontainers/runc/libcontainer/nsenter/nsexec.c b/vendor/github.com/opencontainers/runc/libcontainer/nsenter/nsexec.c
index 2c69cee5d..a4cd1399d 100644
--- a/vendor/github.com/opencontainers/runc/libcontainer/nsenter/nsexec.c
+++ b/vendor/github.com/opencontainers/runc/libcontainer/nsenter/nsexec.c
@@ -505,7 +505,8 @@ void join_namespaces(char *nslist)
ns->fd = fd;
ns->ns = nsflag(namespace);
- strncpy(ns->path, path, PATH_MAX);
+ strncpy(ns->path, path, PATH_MAX - 1);
+ ns->path[PATH_MAX - 1] = '\0';
} while ((namespace = strtok_r(NULL, ",", &saveptr)) != NULL);
/*
@@ -678,17 +679,15 @@ void nsexec(void)
/*
* Enable setgroups(2) if we've been asked to. But we also
* have to explicitly disable setgroups(2) if we're
- * creating a rootless container (this is required since
- * Linux 3.19).
+ * creating a rootless container for single-entry mapping.
+ * i.e. config.is_setgroup == false.
+ * (this is required since Linux 3.19).
+ *
+ * For rootless multi-entry mapping, config.is_setgroup shall be true and
+ * newuidmap/newgidmap shall be used.
*/
- if (config.is_rootless && config.is_setgroup) {
- kill(child, SIGKILL);
- bail("cannot allow setgroup in an unprivileged user namespace setup");
- }
- if (config.is_setgroup)
- update_setgroups(child, SETGROUPS_ALLOW);
- if (config.is_rootless)
+ if (config.is_rootless && !config.is_setgroup)
update_setgroups(child, SETGROUPS_DENY);
/* Set up mappings. */
@@ -810,24 +809,29 @@ void nsexec(void)
join_namespaces(config.namespaces);
/*
- * Unshare all of the namespaces. Now, it should be noted that this
- * ordering might break in the future (especially with rootless
- * containers). But for now, it's not possible to split this into
- * CLONE_NEWUSER + [the rest] because of some RHEL SELinux issues.
- *
- * Note that we don't merge this with clone() because there were
- * some old kernel versions where clone(CLONE_PARENT | CLONE_NEWPID)
- * was broken, so we'll just do it the long way anyway.
- */
- if (unshare(config.cloneflags) < 0)
- bail("failed to unshare namespaces");
-
- /*
* Deal with user namespaces first. They are quite special, as they
* affect our ability to unshare other namespaces and are used as
* context for privilege checks.
+ *
+ * We don't unshare all namespaces in one go. The reason for this
+ * is that, while the kernel documentation may claim otherwise,
+ * there are certain cases where unsharing all namespaces at once
+ * will result in namespace objects being owned incorrectly.
+ * Ideally we should just fix these kernel bugs, but it's better to
+ * be safe than sorry, and fix them separately.
+ *
+ * A specific case of this is that the SELinux label of the
+ * internal kern-mount that mqueue uses will be incorrect if the
+ * UTS namespace is cloned before the USER namespace is mapped.
+ * I've also heard of similar problems with the network namespace
+ * in some scenarios. This also mirrors how LXC deals with this
+ * problem.
*/
if (config.cloneflags & CLONE_NEWUSER) {
+ if (unshare(CLONE_NEWUSER) < 0)
+ bail("failed to unshare user namespace");
+ config.cloneflags &= ~CLONE_NEWUSER;
+
/*
* We don't have the privileges to do any mapping here (see the
* clone_parent rant). So signal our parent to hook us up.
@@ -853,9 +857,22 @@ void nsexec(void)
if (prctl(PR_SET_DUMPABLE, 0, 0, 0, 0) < 0)
bail("failed to set process as dumpable");
}
+
+ /* Become root in the namespace proper. */
+ if (setresuid(0, 0, 0) < 0)
+ bail("failed to become root in user namespace");
}
/*
+ * Unshare all of the namespaces. Note that we don't merge this
+ * with clone() because there were some old kernel versions where
+ * clone(CLONE_PARENT | CLONE_NEWPID) was broken, so we'll just do
+ * it the long way.
+ */
+ if (unshare(config.cloneflags) < 0)
+ bail("failed to unshare namespaces");
+
+ /*
* TODO: What about non-namespace clone flags that we're dropping here?
*
* We fork again because of PID namespace, setns(2) or unshare(2) don't
diff --git a/vendor/github.com/opencontainers/runc/libcontainer/system/linux.go b/vendor/github.com/opencontainers/runc/libcontainer/system/linux.go
index 5f124cd8b..a4ae8901a 100644
--- a/vendor/github.com/opencontainers/runc/libcontainer/system/linux.go
+++ b/vendor/github.com/opencontainers/runc/libcontainer/system/linux.go
@@ -3,13 +3,12 @@
package system
import (
- "bufio"
- "fmt"
"os"
"os/exec"
"syscall" // only for exec
"unsafe"
+ "github.com/opencontainers/runc/libcontainer/user"
"golang.org/x/sys/unix"
)
@@ -102,34 +101,43 @@ func Setctty() error {
}
// RunningInUserNS detects whether we are currently running in a user namespace.
-// Copied from github.com/lxc/lxd/shared/util.go
+// Originally copied from github.com/lxc/lxd/shared/util.go
func RunningInUserNS() bool {
- file, err := os.Open("/proc/self/uid_map")
+ uidmap, err := user.CurrentProcessUIDMap()
if err != nil {
// This kernel-provided file only exists if user namespaces are supported
return false
}
- defer file.Close()
-
- buf := bufio.NewReader(file)
- l, _, err := buf.ReadLine()
- if err != nil {
- return false
- }
+ return UIDMapInUserNS(uidmap)
+}
- line := string(l)
- var a, b, c int64
- fmt.Sscanf(line, "%d %d %d", &a, &b, &c)
+func UIDMapInUserNS(uidmap []user.IDMap) bool {
/*
* We assume we are in the initial user namespace if we have a full
* range - 4294967295 uids starting at uid 0.
*/
- if a == 0 && b == 0 && c == 4294967295 {
+ if len(uidmap) == 1 && uidmap[0].ID == 0 && uidmap[0].ParentID == 0 && uidmap[0].Count == 4294967295 {
return false
}
return true
}
+// GetParentNSeuid returns the euid within the parent user namespace
+func GetParentNSeuid() int64 {
+ euid := int64(os.Geteuid())
+ uidmap, err := user.CurrentProcessUIDMap()
+ if err != nil {
+ // This kernel-provided file only exists if user namespaces are supported
+ return euid
+ }
+ for _, um := range uidmap {
+ if um.ID <= euid && euid <= um.ID+um.Count-1 {
+ return um.ParentID + euid - um.ID
+ }
+ }
+ return euid
+}
+
// SetSubreaper sets the value i as the subreaper setting for the calling process
func SetSubreaper(i int) error {
return unix.Prctl(PR_SET_CHILD_SUBREAPER, uintptr(i), 0, 0, 0)
diff --git a/vendor/github.com/opencontainers/runc/libcontainer/system/unsupported.go b/vendor/github.com/opencontainers/runc/libcontainer/system/unsupported.go
index e7cfd62b2..b94be74a6 100644
--- a/vendor/github.com/opencontainers/runc/libcontainer/system/unsupported.go
+++ b/vendor/github.com/opencontainers/runc/libcontainer/system/unsupported.go
@@ -2,8 +2,26 @@
package system
+import (
+ "os"
+
+ "github.com/opencontainers/runc/libcontainer/user"
+)
+
// RunningInUserNS is a stub for non-Linux systems
// Always returns false
func RunningInUserNS() bool {
return false
}
+
+// UIDMapInUserNS is a stub for non-Linux systems
+// Always returns false
+func UIDMapInUserNS(uidmap []user.IDMap) bool {
+ return false
+}
+
+// GetParentNSeuid returns the euid within the parent user namespace
+// Always returns os.Geteuid on non-linux
+func GetParentNSeuid() int {
+ return os.Geteuid()
+}
diff --git a/vendor/github.com/opencontainers/runc/libcontainer/user/lookup.go b/vendor/github.com/opencontainers/runc/libcontainer/user/lookup.go
index 95e9eebc0..6fd8dd0d4 100644
--- a/vendor/github.com/opencontainers/runc/libcontainer/user/lookup.go
+++ b/vendor/github.com/opencontainers/runc/libcontainer/user/lookup.go
@@ -12,84 +12,30 @@ var (
ErrNoGroupEntries = errors.New("no matching entries in group file")
)
-func lookupUser(filter func(u User) bool) (User, error) {
- // Get operating system-specific passwd reader-closer.
- passwd, err := GetPasswd()
- if err != nil {
- return User{}, err
- }
- defer passwd.Close()
-
- // Get the users.
- users, err := ParsePasswdFilter(passwd, filter)
- if err != nil {
- return User{}, err
- }
-
- // No user entries found.
- if len(users) == 0 {
- return User{}, ErrNoPasswdEntries
- }
-
- // Assume the first entry is the "correct" one.
- return users[0], nil
-}
-
// LookupUser looks up a user by their username in /etc/passwd. If the user
// cannot be found (or there is no /etc/passwd file on the filesystem), then
// LookupUser returns an error.
func LookupUser(username string) (User, error) {
- return lookupUser(func(u User) bool {
- return u.Name == username
- })
+ return lookupUser(username)
}
// LookupUid looks up a user by their user id in /etc/passwd. If the user cannot
// be found (or there is no /etc/passwd file on the filesystem), then LookupId
// returns an error.
func LookupUid(uid int) (User, error) {
- return lookupUser(func(u User) bool {
- return u.Uid == uid
- })
-}
-
-func lookupGroup(filter func(g Group) bool) (Group, error) {
- // Get operating system-specific group reader-closer.
- group, err := GetGroup()
- if err != nil {
- return Group{}, err
- }
- defer group.Close()
-
- // Get the users.
- groups, err := ParseGroupFilter(group, filter)
- if err != nil {
- return Group{}, err
- }
-
- // No user entries found.
- if len(groups) == 0 {
- return Group{}, ErrNoGroupEntries
- }
-
- // Assume the first entry is the "correct" one.
- return groups[0], nil
+ return lookupUid(uid)
}
// LookupGroup looks up a group by its name in /etc/group. If the group cannot
// be found (or there is no /etc/group file on the filesystem), then LookupGroup
// returns an error.
func LookupGroup(groupname string) (Group, error) {
- return lookupGroup(func(g Group) bool {
- return g.Name == groupname
- })
+ return lookupGroup(groupname)
}
// LookupGid looks up a group by its group id in /etc/group. If the group cannot
// be found (or there is no /etc/group file on the filesystem), then LookupGid
// returns an error.
func LookupGid(gid int) (Group, error) {
- return lookupGroup(func(g Group) bool {
- return g.Gid == gid
- })
+ return lookupGid(gid)
}
diff --git a/vendor/github.com/opencontainers/runc/libcontainer/user/lookup_unix.go b/vendor/github.com/opencontainers/runc/libcontainer/user/lookup_unix.go
index c2bb9ec90..c1e634c94 100644
--- a/vendor/github.com/opencontainers/runc/libcontainer/user/lookup_unix.go
+++ b/vendor/github.com/opencontainers/runc/libcontainer/user/lookup_unix.go
@@ -15,6 +15,76 @@ const (
unixGroupPath = "/etc/group"
)
+func lookupUser(username string) (User, error) {
+ return lookupUserFunc(func(u User) bool {
+ return u.Name == username
+ })
+}
+
+func lookupUid(uid int) (User, error) {
+ return lookupUserFunc(func(u User) bool {
+ return u.Uid == uid
+ })
+}
+
+func lookupUserFunc(filter func(u User) bool) (User, error) {
+ // Get operating system-specific passwd reader-closer.
+ passwd, err := GetPasswd()
+ if err != nil {
+ return User{}, err
+ }
+ defer passwd.Close()
+
+ // Get the users.
+ users, err := ParsePasswdFilter(passwd, filter)
+ if err != nil {
+ return User{}, err
+ }
+
+ // No user entries found.
+ if len(users) == 0 {
+ return User{}, ErrNoPasswdEntries
+ }
+
+ // Assume the first entry is the "correct" one.
+ return users[0], nil
+}
+
+func lookupGroup(groupname string) (Group, error) {
+ return lookupGroupFunc(func(g Group) bool {
+ return g.Name == groupname
+ })
+}
+
+func lookupGid(gid int) (Group, error) {
+ return lookupGroupFunc(func(g Group) bool {
+ return g.Gid == gid
+ })
+}
+
+func lookupGroupFunc(filter func(g Group) bool) (Group, error) {
+ // Get operating system-specific group reader-closer.
+ group, err := GetGroup()
+ if err != nil {
+ return Group{}, err
+ }
+ defer group.Close()
+
+ // Get the users.
+ groups, err := ParseGroupFilter(group, filter)
+ if err != nil {
+ return Group{}, err
+ }
+
+ // No user entries found.
+ if len(groups) == 0 {
+ return Group{}, ErrNoGroupEntries
+ }
+
+ // Assume the first entry is the "correct" one.
+ return groups[0], nil
+}
+
func GetPasswdPath() (string, error) {
return unixPasswdPath, nil
}
@@ -44,3 +114,29 @@ func CurrentUser() (User, error) {
func CurrentGroup() (Group, error) {
return LookupGid(unix.Getgid())
}
+
+func CurrentUserSubUIDs() ([]SubID, error) {
+ u, err := CurrentUser()
+ if err != nil {
+ return nil, err
+ }
+ return ParseSubIDFileFilter("/etc/subuid",
+ func(entry SubID) bool { return entry.Name == u.Name })
+}
+
+func CurrentGroupSubGIDs() ([]SubID, error) {
+ g, err := CurrentGroup()
+ if err != nil {
+ return nil, err
+ }
+ return ParseSubIDFileFilter("/etc/subgid",
+ func(entry SubID) bool { return entry.Name == g.Name })
+}
+
+func CurrentProcessUIDMap() ([]IDMap, error) {
+ return ParseIDMapFile("/proc/self/uid_map")
+}
+
+func CurrentProcessGIDMap() ([]IDMap, error) {
+ return ParseIDMapFile("/proc/self/gid_map")
+}
diff --git a/vendor/github.com/opencontainers/runc/libcontainer/user/lookup_windows.go b/vendor/github.com/opencontainers/runc/libcontainer/user/lookup_windows.go
new file mode 100644
index 000000000..65cd40e92
--- /dev/null
+++ b/vendor/github.com/opencontainers/runc/libcontainer/user/lookup_windows.go
@@ -0,0 +1,40 @@
+// +build windows
+
+package user
+
+import (
+ "fmt"
+ "os/user"
+)
+
+func lookupUser(username string) (User, error) {
+ u, err := user.Lookup(username)
+ if err != nil {
+ return User{}, err
+ }
+ return userFromOS(u)
+}
+
+func lookupUid(uid int) (User, error) {
+ u, err := user.LookupId(fmt.Sprintf("%d", uid))
+ if err != nil {
+ return User{}, err
+ }
+ return userFromOS(u)
+}
+
+func lookupGroup(groupname string) (Group, error) {
+ g, err := user.LookupGroup(groupname)
+ if err != nil {
+ return Group{}, err
+ }
+ return groupFromOS(g)
+}
+
+func lookupGid(gid int) (Group, error) {
+ g, err := user.LookupGroupId(fmt.Sprintf("%d", gid))
+ if err != nil {
+ return Group{}, err
+ }
+ return groupFromOS(g)
+}
diff --git a/vendor/github.com/opencontainers/runc/libcontainer/user/user.go b/vendor/github.com/opencontainers/runc/libcontainer/user/user.go
index 8962cab33..7b912bbf8 100644
--- a/vendor/github.com/opencontainers/runc/libcontainer/user/user.go
+++ b/vendor/github.com/opencontainers/runc/libcontainer/user/user.go
@@ -5,6 +5,7 @@ import (
"fmt"
"io"
"os"
+ "os/user"
"strconv"
"strings"
)
@@ -28,6 +29,28 @@ type User struct {
Shell string
}
+// userFromOS converts an os/user.(*User) to local User
+//
+// (This does not include Pass, Shell or Gecos)
+func userFromOS(u *user.User) (User, error) {
+ newUser := User{
+ Name: u.Username,
+ Home: u.HomeDir,
+ }
+ id, err := strconv.Atoi(u.Uid)
+ if err != nil {
+ return newUser, err
+ }
+ newUser.Uid = id
+
+ id, err = strconv.Atoi(u.Gid)
+ if err != nil {
+ return newUser, err
+ }
+ newUser.Gid = id
+ return newUser, nil
+}
+
type Group struct {
Name string
Pass string
@@ -35,12 +58,46 @@ type Group struct {
List []string
}
+// groupFromOS converts an os/user.(*Group) to local Group
+//
+// (This does not include Pass, Shell or Gecos)
+func groupFromOS(g *user.Group) (Group, error) {
+ newGroup := Group{
+ Name: g.Name,
+ }
+
+ id, err := strconv.Atoi(g.Gid)
+ if err != nil {
+ return newGroup, err
+ }
+ newGroup.Gid = id
+
+ return newGroup, nil
+}
+
+// SubID represents an entry in /etc/sub{u,g}id
+type SubID struct {
+ Name string
+ SubID int64
+ Count int64
+}
+
+// IDMap represents an entry in /proc/PID/{u,g}id_map
+type IDMap struct {
+ ID int64
+ ParentID int64
+ Count int64
+}
+
func parseLine(line string, v ...interface{}) {
- if line == "" {
+ parseParts(strings.Split(line, ":"), v...)
+}
+
+func parseParts(parts []string, v ...interface{}) {
+ if len(parts) == 0 {
return
}
- parts := strings.Split(line, ":")
for i, p := range parts {
// Ignore cases where we don't have enough fields to populate the arguments.
// Some configuration files like to misbehave.
@@ -56,6 +113,8 @@ func parseLine(line string, v ...interface{}) {
case *int:
// "numbers", with conversion errors ignored because of some misbehaving configuration files.
*e, _ = strconv.Atoi(p)
+ case *int64:
+ *e, _ = strconv.ParseInt(p, 10, 64)
case *[]string:
// Comma-separated lists.
if p != "" {
@@ -65,7 +124,7 @@ func parseLine(line string, v ...interface{}) {
}
default:
// Someone goof'd when writing code using this function. Scream so they can hear us.
- panic(fmt.Sprintf("parseLine only accepts {*string, *int, *[]string} as arguments! %#v is not a pointer!", e))
+ panic(fmt.Sprintf("parseLine only accepts {*string, *int, *int64, *[]string} as arguments! %#v is not a pointer!", e))
}
}
}
@@ -439,3 +498,111 @@ func GetAdditionalGroupsPath(additionalGroups []string, groupPath string) ([]int
}
return GetAdditionalGroups(additionalGroups, group)
}
+
+func ParseSubIDFile(path string) ([]SubID, error) {
+ subid, err := os.Open(path)
+ if err != nil {
+ return nil, err
+ }
+ defer subid.Close()
+ return ParseSubID(subid)
+}
+
+func ParseSubID(subid io.Reader) ([]SubID, error) {
+ return ParseSubIDFilter(subid, nil)
+}
+
+func ParseSubIDFileFilter(path string, filter func(SubID) bool) ([]SubID, error) {
+ subid, err := os.Open(path)
+ if err != nil {
+ return nil, err
+ }
+ defer subid.Close()
+ return ParseSubIDFilter(subid, filter)
+}
+
+func ParseSubIDFilter(r io.Reader, filter func(SubID) bool) ([]SubID, error) {
+ if r == nil {
+ return nil, fmt.Errorf("nil source for subid-formatted data")
+ }
+
+ var (
+ s = bufio.NewScanner(r)
+ out = []SubID{}
+ )
+
+ for s.Scan() {
+ if err := s.Err(); err != nil {
+ return nil, err
+ }
+
+ line := strings.TrimSpace(s.Text())
+ if line == "" {
+ continue
+ }
+
+ // see: man 5 subuid
+ p := SubID{}
+ parseLine(line, &p.Name, &p.SubID, &p.Count)
+
+ if filter == nil || filter(p) {
+ out = append(out, p)
+ }
+ }
+
+ return out, nil
+}
+
+func ParseIDMapFile(path string) ([]IDMap, error) {
+ r, err := os.Open(path)
+ if err != nil {
+ return nil, err
+ }
+ defer r.Close()
+ return ParseIDMap(r)
+}
+
+func ParseIDMap(r io.Reader) ([]IDMap, error) {
+ return ParseIDMapFilter(r, nil)
+}
+
+func ParseIDMapFileFilter(path string, filter func(IDMap) bool) ([]IDMap, error) {
+ r, err := os.Open(path)
+ if err != nil {
+ return nil, err
+ }
+ defer r.Close()
+ return ParseIDMapFilter(r, filter)
+}
+
+func ParseIDMapFilter(r io.Reader, filter func(IDMap) bool) ([]IDMap, error) {
+ if r == nil {
+ return nil, fmt.Errorf("nil source for idmap-formatted data")
+ }
+
+ var (
+ s = bufio.NewScanner(r)
+ out = []IDMap{}
+ )
+
+ for s.Scan() {
+ if err := s.Err(); err != nil {
+ return nil, err
+ }
+
+ line := strings.TrimSpace(s.Text())
+ if line == "" {
+ continue
+ }
+
+ // see: man 7 user_namespaces
+ p := IDMap{}
+ parseParts(strings.Fields(line), &p.ID, &p.ParentID, &p.Count)
+
+ if filter == nil || filter(p) {
+ out = append(out, p)
+ }
+ }
+
+ return out, nil
+}