diff options
author | Doug Rabson <dfr@rabson.org> | 2022-08-27 14:46:48 +0100 |
---|---|---|
committer | Doug Rabson <dfr@rabson.org> | 2022-09-05 10:20:50 +0100 |
commit | eab4291d996e8aab34f97d79d76816afa976687e (patch) | |
tree | 56ef126abeef38933688b49732a3baf79fc91282 /libpod | |
parent | b3989be76851fc1aa0c29e579681be1ed56dafab (diff) | |
download | podman-eab4291d996e8aab34f97d79d76816afa976687e.tar.gz podman-eab4291d996e8aab34f97d79d76816afa976687e.tar.bz2 podman-eab4291d996e8aab34f97d79d76816afa976687e.zip |
libpod: Move functions related to /etc bind mounts to container_internal_common.go
[NO NEW TESTS NEEDED]
Signed-off-by: Doug Rabson <dfr@rabson.org>
Diffstat (limited to 'libpod')
-rw-r--r-- | libpod/container_internal_common.go | 956 | ||||
-rw-r--r-- | libpod/container_internal_freebsd.go | 924 | ||||
-rw-r--r-- | libpod/container_internal_linux.go | 962 |
3 files changed, 956 insertions, 1886 deletions
diff --git a/libpod/container_internal_common.go b/libpod/container_internal_common.go index c5a6d4ff3..11e791063 100644 --- a/libpod/container_internal_common.go +++ b/libpod/container_internal_common.go @@ -11,6 +11,7 @@ import ( "io/ioutil" "math" "os" + "os/user" "path" "path/filepath" "strconv" @@ -24,8 +25,12 @@ import ( "github.com/containers/buildah/pkg/chrootuser" "github.com/containers/buildah/pkg/overlay" butil "github.com/containers/buildah/util" + "github.com/containers/common/libnetwork/etchosts" + "github.com/containers/common/libnetwork/resolvconf" "github.com/containers/common/libnetwork/types" "github.com/containers/common/pkg/apparmor" + "github.com/containers/common/pkg/config" + "github.com/containers/common/pkg/subscriptions" cutil "github.com/containers/common/pkg/util" is "github.com/containers/image/v5/storage" "github.com/containers/podman/v4/libpod/define" @@ -39,6 +44,7 @@ import ( "github.com/containers/podman/v4/version" "github.com/containers/storage/pkg/archive" "github.com/containers/storage/pkg/idtools" + "github.com/containers/storage/pkg/lockfile" securejoin "github.com/cyphar/filepath-securejoin" runcuser "github.com/opencontainers/runc/libcontainer/user" spec "github.com/opencontainers/runtime-spec/specs-go" @@ -1595,3 +1601,953 @@ func (c *Container) getRootNetNsDepCtr() (depCtr *Container, err error) { } return depCtr, nil } + +// Ensure standard bind mounts are mounted into all root directories (including chroot directories) +func (c *Container) mountIntoRootDirs(mountName string, mountPath string) error { + c.state.BindMounts[mountName] = mountPath + + for _, chrootDir := range c.config.ChrootDirs { + c.state.BindMounts[filepath.Join(chrootDir, mountName)] = mountPath + } + + return nil +} + +// Make standard bind mounts to include in the container +func (c *Container) makeBindMounts() error { + if err := os.Chown(c.state.RunDir, c.RootUID(), c.RootGID()); err != nil { + return fmt.Errorf("cannot chown run directory: %w", err) + } + + if c.state.BindMounts == nil { + c.state.BindMounts = make(map[string]string) + } + netDisabled, err := c.NetworkDisabled() + if err != nil { + return err + } + + if !netDisabled { + // If /etc/resolv.conf and /etc/hosts exist, delete them so we + // will recreate. Only do this if we aren't sharing them with + // another container. + if c.config.NetNsCtr == "" { + if resolvePath, ok := c.state.BindMounts["/etc/resolv.conf"]; ok { + if err := os.Remove(resolvePath); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("container %s: %w", c.ID(), err) + } + delete(c.state.BindMounts, "/etc/resolv.conf") + } + if hostsPath, ok := c.state.BindMounts["/etc/hosts"]; ok { + if err := os.Remove(hostsPath); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("container %s: %w", c.ID(), err) + } + delete(c.state.BindMounts, "/etc/hosts") + } + } + + if c.config.NetNsCtr != "" && (!c.config.UseImageResolvConf || !c.config.UseImageHosts) { + // We share a net namespace. + // We want /etc/resolv.conf and /etc/hosts from the + // other container. Unless we're not creating both of + // them. + depCtr, err := c.getRootNetNsDepCtr() + if err != nil { + return fmt.Errorf("error fetching network namespace dependency container for container %s: %w", c.ID(), err) + } + + // We need that container's bind mounts + bindMounts, err := depCtr.BindMounts() + if err != nil { + return fmt.Errorf("error fetching bind mounts from dependency %s of container %s: %w", depCtr.ID(), c.ID(), err) + } + + // The other container may not have a resolv.conf or /etc/hosts + // If it doesn't, don't copy them + resolvPath, exists := bindMounts["/etc/resolv.conf"] + if !c.config.UseImageResolvConf && exists { + err := c.mountIntoRootDirs("/etc/resolv.conf", resolvPath) + + if err != nil { + return fmt.Errorf("error assigning mounts to container %s: %w", c.ID(), err) + } + } + + // check if dependency container has an /etc/hosts file. + // It may not have one, so only use it if it does. + hostsPath, exists := bindMounts[config.DefaultHostsFile] + if !c.config.UseImageHosts && exists { + // we cannot use the dependency container lock due ABBA deadlocks in cleanup() + lock, err := lockfile.GetLockfile(hostsPath) + if err != nil { + return fmt.Errorf("failed to lock hosts file: %w", err) + } + lock.Lock() + + // add the newly added container to the hosts file + // we always use 127.0.0.1 as ip since they have the same netns + err = etchosts.Add(hostsPath, getLocalhostHostEntry(c)) + lock.Unlock() + if err != nil { + return fmt.Errorf("error creating hosts file for container %s which depends on container %s: %w", c.ID(), depCtr.ID(), err) + } + + // finally, save it in the new container + err = c.mountIntoRootDirs(config.DefaultHostsFile, hostsPath) + if err != nil { + return fmt.Errorf("error assigning mounts to container %s: %w", c.ID(), err) + } + } + + if !hasCurrentUserMapped(c) { + if err := makeAccessible(resolvPath, c.RootUID(), c.RootGID()); err != nil { + return err + } + if err := makeAccessible(hostsPath, c.RootUID(), c.RootGID()); err != nil { + return err + } + } + } else { + if !c.config.UseImageResolvConf { + if err := c.generateResolvConf(); err != nil { + return fmt.Errorf("error creating resolv.conf for container %s: %w", c.ID(), err) + } + } + + if !c.config.UseImageHosts { + if err := c.createHosts(); err != nil { + return fmt.Errorf("error creating hosts file for container %s: %w", c.ID(), err) + } + } + } + + if c.state.BindMounts["/etc/hosts"] != "" { + if err := c.relabel(c.state.BindMounts["/etc/hosts"], c.config.MountLabel, true); err != nil { + return err + } + } + + if c.state.BindMounts["/etc/resolv.conf"] != "" { + if err := c.relabel(c.state.BindMounts["/etc/resolv.conf"], c.config.MountLabel, true); err != nil { + return err + } + } + } else if !c.config.UseImageHosts && c.state.BindMounts["/etc/hosts"] == "" { + if err := c.createHosts(); err != nil { + return fmt.Errorf("error creating hosts file for container %s: %w", c.ID(), err) + } + } + + if c.config.ShmDir != "" { + // If ShmDir has a value SHM is always added when we mount the container + c.state.BindMounts["/dev/shm"] = c.config.ShmDir + } + + if c.config.Passwd == nil || *c.config.Passwd { + newPasswd, newGroup, err := c.generatePasswdAndGroup() + if err != nil { + return fmt.Errorf("error creating temporary passwd file for container %s: %w", c.ID(), err) + } + if newPasswd != "" { + // Make /etc/passwd + // If it already exists, delete so we can recreate + delete(c.state.BindMounts, "/etc/passwd") + c.state.BindMounts["/etc/passwd"] = newPasswd + } + if newGroup != "" { + // Make /etc/group + // If it already exists, delete so we can recreate + delete(c.state.BindMounts, "/etc/group") + c.state.BindMounts["/etc/group"] = newGroup + } + } + + // Make /etc/hostname + // This should never change, so no need to recreate if it exists + if _, ok := c.state.BindMounts["/etc/hostname"]; !ok { + hostnamePath, err := c.writeStringToRundir("hostname", c.Hostname()) + if err != nil { + return fmt.Errorf("error creating hostname file for container %s: %w", c.ID(), err) + } + c.state.BindMounts["/etc/hostname"] = hostnamePath + } + + // Make /etc/localtime + ctrTimezone := c.Timezone() + if ctrTimezone != "" { + // validate the format of the timezone specified if it's not "local" + if ctrTimezone != "local" { + _, err = time.LoadLocation(ctrTimezone) + if err != nil { + return fmt.Errorf("error finding timezone for container %s: %w", c.ID(), err) + } + } + if _, ok := c.state.BindMounts["/etc/localtime"]; !ok { + var zonePath string + if ctrTimezone == "local" { + zonePath, err = filepath.EvalSymlinks("/etc/localtime") + if err != nil { + return fmt.Errorf("error finding local timezone for container %s: %w", c.ID(), err) + } + } else { + zone := filepath.Join("/usr/share/zoneinfo", ctrTimezone) + zonePath, err = filepath.EvalSymlinks(zone) + if err != nil { + return fmt.Errorf("error setting timezone for container %s: %w", c.ID(), err) + } + } + localtimePath, err := c.copyTimezoneFile(zonePath) + if err != nil { + return fmt.Errorf("error setting timezone for container %s: %w", c.ID(), err) + } + c.state.BindMounts["/etc/localtime"] = localtimePath + } + } + + _, hasRunContainerenv := c.state.BindMounts["/run/.containerenv"] + if !hasRunContainerenv { + // check in the spec mounts + for _, m := range c.config.Spec.Mounts { + if m.Destination == "/run/.containerenv" || m.Destination == "/run" { + hasRunContainerenv = true + break + } + } + } + + // Make .containerenv if it does not exist + if !hasRunContainerenv { + containerenv := c.runtime.graphRootMountedFlag(c.config.Spec.Mounts) + isRootless := 0 + if rootless.IsRootless() { + isRootless = 1 + } + imageID, imageName := c.Image() + + if c.Privileged() { + // Populate the .containerenv with container information + containerenv = fmt.Sprintf(`engine="podman-%s" +name=%q +id=%q +image=%q +imageid=%q +rootless=%d +%s`, version.Version.String(), c.Name(), c.ID(), imageName, imageID, isRootless, containerenv) + } + containerenvPath, err := c.writeStringToRundir(".containerenv", containerenv) + if err != nil { + return fmt.Errorf("error creating containerenv file for container %s: %w", c.ID(), err) + } + c.state.BindMounts["/run/.containerenv"] = containerenvPath + } + + // Add Subscription Mounts + subscriptionMounts := subscriptions.MountsWithUIDGID(c.config.MountLabel, c.state.RunDir, c.runtime.config.Containers.DefaultMountsFile, c.state.Mountpoint, c.RootUID(), c.RootGID(), rootless.IsRootless(), false) + for _, mount := range subscriptionMounts { + if _, ok := c.state.BindMounts[mount.Destination]; !ok { + c.state.BindMounts[mount.Destination] = mount.Source + } + } + + // Secrets are mounted by getting the secret data from the secrets manager, + // copying the data into the container's static dir, + // then mounting the copied dir into /run/secrets. + // The secrets mounting must come after subscription mounts, since subscription mounts + // creates the /run/secrets dir in the container where we mount as well. + if len(c.Secrets()) > 0 { + // create /run/secrets if subscriptions did not create + if err := c.createSecretMountDir(); err != nil { + return fmt.Errorf("error creating secrets mount: %w", err) + } + for _, secret := range c.Secrets() { + secretFileName := secret.Name + base := "/run/secrets" + if secret.Target != "" { + secretFileName = secret.Target + // If absolute path for target given remove base. + if filepath.IsAbs(secretFileName) { + base = "" + } + } + src := filepath.Join(c.config.SecretsPath, secret.Name) + dest := filepath.Join(base, secretFileName) + c.state.BindMounts[dest] = src + } + } + + return nil +} + +// generateResolvConf generates a containers resolv.conf +func (c *Container) generateResolvConf() error { + var ( + networkNameServers []string + networkSearchDomains []string + ) + + netStatus := c.getNetworkStatus() + for _, status := range netStatus { + if status.DNSServerIPs != nil { + for _, nsIP := range status.DNSServerIPs { + networkNameServers = append(networkNameServers, nsIP.String()) + } + logrus.Debugf("Adding nameserver(s) from network status of '%q'", status.DNSServerIPs) + } + if status.DNSSearchDomains != nil { + networkSearchDomains = append(networkSearchDomains, status.DNSSearchDomains...) + logrus.Debugf("Adding search domain(s) from network status of '%q'", status.DNSSearchDomains) + } + } + + ipv6, err := c.checkForIPv6(netStatus) + if err != nil { + return err + } + + nameservers := make([]string, 0, len(c.runtime.config.Containers.DNSServers)+len(c.config.DNSServer)) + nameservers = append(nameservers, c.runtime.config.Containers.DNSServers...) + for _, ip := range c.config.DNSServer { + nameservers = append(nameservers, ip.String()) + } + // If the user provided dns, it trumps all; then dns masq; then resolv.conf + var search []string + keepHostServers := false + if len(nameservers) == 0 { + keepHostServers = true + // first add the nameservers from the networks status + nameservers = networkNameServers + // when we add network dns server we also have to add the search domains + search = networkSearchDomains + // slirp4netns has a built in DNS forwarder. + if c.config.NetMode.IsSlirp4netns() { + slirp4netnsDNS, err := GetSlirp4netnsDNS(c.slirp4netnsSubnet) + if err != nil { + logrus.Warn("Failed to determine Slirp4netns DNS: ", err.Error()) + } else { + nameservers = append(nameservers, slirp4netnsDNS.String()) + } + } + } + + if len(c.config.DNSSearch) > 0 || len(c.runtime.config.Containers.DNSSearches) > 0 { + customSearch := make([]string, 0, len(c.config.DNSSearch)+len(c.runtime.config.Containers.DNSSearches)) + customSearch = append(customSearch, c.runtime.config.Containers.DNSSearches...) + customSearch = append(customSearch, c.config.DNSSearch...) + search = customSearch + } + + options := make([]string, 0, len(c.config.DNSOption)+len(c.runtime.config.Containers.DNSOptions)) + options = append(options, c.runtime.config.Containers.DNSOptions...) + options = append(options, c.config.DNSOption...) + + destPath := filepath.Join(c.state.RunDir, "resolv.conf") + + if err := resolvconf.New(&resolvconf.Params{ + IPv6Enabled: ipv6, + KeepHostServers: keepHostServers, + Nameservers: nameservers, + Namespaces: c.config.Spec.Linux.Namespaces, + Options: options, + Path: destPath, + Searches: search, + }); err != nil { + return fmt.Errorf("error building resolv.conf for container %s: %w", c.ID(), err) + } + + return c.bindMountRootFile(destPath, resolvconf.DefaultResolvConf) +} + +// Check if a container uses IPv6. +func (c *Container) checkForIPv6(netStatus map[string]types.StatusBlock) (bool, error) { + for _, status := range netStatus { + for _, netInt := range status.Interfaces { + for _, netAddress := range netInt.Subnets { + // Note: only using To16() does not work since it also returns a valid ip for ipv4 + if netAddress.IPNet.IP.To4() == nil && netAddress.IPNet.IP.To16() != nil { + return true, nil + } + } + } + } + + if c.config.NetMode.IsSlirp4netns() { + ctrNetworkSlipOpts := []string{} + if c.config.NetworkOptions != nil { + ctrNetworkSlipOpts = append(ctrNetworkSlipOpts, c.config.NetworkOptions["slirp4netns"]...) + } + slirpOpts, err := parseSlirp4netnsNetworkOptions(c.runtime, ctrNetworkSlipOpts) + if err != nil { + return false, err + } + return slirpOpts.enableIPv6, nil + } + + return false, nil +} + +// Add a new nameserver to the container's resolv.conf, ensuring that it is the +// first nameserver present. +// Usable only with running containers. +func (c *Container) addNameserver(ips []string) error { + // Take no action if container is not running. + if !c.ensureState(define.ContainerStateRunning, define.ContainerStateCreated) { + return nil + } + + // Do we have a resolv.conf at all? + path, ok := c.state.BindMounts[resolvconf.DefaultResolvConf] + if !ok { + return nil + } + + if err := resolvconf.Add(path, ips); err != nil { + return fmt.Errorf("adding new nameserver to container %s resolv.conf: %w", c.ID(), err) + } + + return nil +} + +// Remove an entry from the existing resolv.conf of the container. +// Usable only with running containers. +func (c *Container) removeNameserver(ips []string) error { + // Take no action if container is not running. + if !c.ensureState(define.ContainerStateRunning, define.ContainerStateCreated) { + return nil + } + + // Do we have a resolv.conf at all? + path, ok := c.state.BindMounts[resolvconf.DefaultResolvConf] + if !ok { + return nil + } + + if err := resolvconf.Remove(path, ips); err != nil { + return fmt.Errorf("removing nameservers from container %s resolv.conf: %w", c.ID(), err) + } + + return nil +} + +func getLocalhostHostEntry(c *Container) etchosts.HostEntries { + return etchosts.HostEntries{{IP: "127.0.0.1", Names: []string{c.Hostname(), c.config.Name}}} +} + +// getHostsEntries returns the container ip host entries for the correct netmode +func (c *Container) getHostsEntries() (etchosts.HostEntries, error) { + var entries etchosts.HostEntries + names := []string{c.Hostname(), c.config.Name} + switch { + case c.config.NetMode.IsBridge(): + entries = etchosts.GetNetworkHostEntries(c.state.NetworkStatus, names...) + case c.config.NetMode.IsSlirp4netns(): + ip, err := GetSlirp4netnsIP(c.slirp4netnsSubnet) + if err != nil { + return nil, err + } + entries = etchosts.HostEntries{{IP: ip.String(), Names: names}} + default: + // check for net=none + if !c.config.CreateNetNS { + for _, ns := range c.config.Spec.Linux.Namespaces { + if ns.Type == spec.NetworkNamespace { + if ns.Path == "" { + entries = etchosts.HostEntries{{IP: "127.0.0.1", Names: names}} + } + break + } + } + } + } + return entries, nil +} + +func (c *Container) createHosts() error { + var containerIPsEntries etchosts.HostEntries + var err error + // if we configure the netns after the container create we should not add + // the hosts here since we have no information about the actual ips + // instead we will add them in c.completeNetworkSetup() + if !c.config.PostConfigureNetNS { + containerIPsEntries, err = c.getHostsEntries() + if err != nil { + return fmt.Errorf("failed to get container ip host entries: %w", err) + } + } + baseHostFile, err := etchosts.GetBaseHostFile(c.runtime.config.Containers.BaseHostsFile, c.state.Mountpoint) + if err != nil { + return err + } + + targetFile := filepath.Join(c.state.RunDir, "hosts") + err = etchosts.New(&etchosts.Params{ + BaseFile: baseHostFile, + ExtraHosts: c.config.HostAdd, + ContainerIPs: containerIPsEntries, + HostContainersInternalIP: etchosts.GetHostContainersInternalIP(c.runtime.config, c.state.NetworkStatus, c.runtime.network), + TargetFile: targetFile, + }) + if err != nil { + return err + } + + return c.bindMountRootFile(targetFile, config.DefaultHostsFile) +} + +// bindMountRootFile will chown and relabel the source file to make it usable in the container. +// It will also add the path to the container bind mount map. +// source is the path on the host, dest is the path in the container. +func (c *Container) bindMountRootFile(source, dest string) error { + if err := os.Chown(source, c.RootUID(), c.RootGID()); err != nil { + return err + } + if err := label.Relabel(source, c.MountLabel(), false); err != nil { + return err + } + + return c.mountIntoRootDirs(dest, source) +} + +// generateGroupEntry generates an entry or entries into /etc/group as +// required by container configuration. +// Generally speaking, we will make an entry under two circumstances: +// 1. The container is started as a specific user:group, and that group is both +// numeric, and does not already exist in /etc/group. +// 2. It is requested that Libpod add the group that launched Podman to +// /etc/group via AddCurrentUserPasswdEntry (though this does not trigger if +// the group in question already exists in /etc/passwd). +// +// Returns group entry (as a string that can be appended to /etc/group) and any +// error that occurred. +func (c *Container) generateGroupEntry() (string, error) { + groupString := "" + + // Things we *can't* handle: adding the user we added in + // generatePasswdEntry to any *existing* groups. + addedGID := 0 + if c.config.AddCurrentUserPasswdEntry { + entry, gid, err := c.generateCurrentUserGroupEntry() + if err != nil { + return "", err + } + groupString += entry + addedGID = gid + } + if c.config.User != "" { + entry, err := c.generateUserGroupEntry(addedGID) + if err != nil { + return "", err + } + groupString += entry + } + + return groupString, nil +} + +// Make an entry in /etc/group for the group of the user running podman iff we +// are rootless. +func (c *Container) generateCurrentUserGroupEntry() (string, int, error) { + gid := rootless.GetRootlessGID() + if gid == 0 { + return "", 0, nil + } + + g, err := user.LookupGroupId(strconv.Itoa(gid)) + if err != nil { + return "", 0, fmt.Errorf("failed to get current group: %w", err) + } + + // Look up group name to see if it exists in the image. + _, err = lookup.GetGroup(c.state.Mountpoint, g.Name) + if err != runcuser.ErrNoGroupEntries { + return "", 0, err + } + + // Look up GID to see if it exists in the image. + _, err = lookup.GetGroup(c.state.Mountpoint, g.Gid) + if err != runcuser.ErrNoGroupEntries { + return "", 0, err + } + + // We need to get the username of the rootless user so we can add it to + // the group. + username := "" + uid := rootless.GetRootlessUID() + if uid != 0 { + u, err := user.LookupId(strconv.Itoa(uid)) + if err != nil { + return "", 0, fmt.Errorf("failed to get current user to make group entry: %w", err) + } + username = u.Username + } + + // Make the entry. + return fmt.Sprintf("%s:x:%s:%s\n", g.Name, g.Gid, username), gid, nil +} + +// Make an entry in /etc/group for the group the container was specified to run +// as. +func (c *Container) generateUserGroupEntry(addedGID int) (string, error) { + if c.config.User == "" { + return "", nil + } + + splitUser := strings.SplitN(c.config.User, ":", 2) + group := splitUser[0] + if len(splitUser) > 1 { + group = splitUser[1] + } + + gid, err := strconv.ParseUint(group, 10, 32) + if err != nil { + return "", nil //nolint: nilerr + } + + if addedGID != 0 && addedGID == int(gid) { + return "", nil + } + + // Check if the group already exists + _, err = lookup.GetGroup(c.state.Mountpoint, group) + if err != runcuser.ErrNoGroupEntries { + return "", err + } + + return fmt.Sprintf("%d:x:%d:%s\n", gid, gid, splitUser[0]), nil +} + +// generatePasswdEntry generates an entry or entries into /etc/passwd as +// required by container configuration. +// Generally speaking, we will make an entry under two circumstances: +// 1. The container is started as a specific user who is not in /etc/passwd. +// This only triggers if the user is given as a *numeric* ID. +// 2. It is requested that Libpod add the user that launched Podman to +// /etc/passwd via AddCurrentUserPasswdEntry (though this does not trigger if +// the user in question already exists in /etc/passwd) or the UID to be added +// is 0). +// 3. The user specified additional host user accounts to add the the /etc/passwd file +// +// Returns password entry (as a string that can be appended to /etc/passwd) and +// any error that occurred. +func (c *Container) generatePasswdEntry() (string, error) { + passwdString := "" + + addedUID := 0 + for _, userid := range c.config.HostUsers { + // Look up User on host + u, err := util.LookupUser(userid) + if err != nil { + return "", err + } + entry, err := c.userPasswdEntry(u) + if err != nil { + return "", err + } + passwdString += entry + } + if c.config.AddCurrentUserPasswdEntry { + entry, uid, _, err := c.generateCurrentUserPasswdEntry() + if err != nil { + return "", err + } + passwdString += entry + addedUID = uid + } + if c.config.User != "" { + entry, err := c.generateUserPasswdEntry(addedUID) + if err != nil { + return "", err + } + passwdString += entry + } + + return passwdString, nil +} + +// generateCurrentUserPasswdEntry generates an /etc/passwd entry for the user +// running the container engine. +// Returns a passwd entry for the user, and the UID and GID of the added entry. +func (c *Container) generateCurrentUserPasswdEntry() (string, int, int, error) { + uid := rootless.GetRootlessUID() + if uid == 0 { + return "", 0, 0, nil + } + + u, err := user.LookupId(strconv.Itoa(uid)) + if err != nil { + return "", 0, 0, fmt.Errorf("failed to get current user: %w", err) + } + pwd, err := c.userPasswdEntry(u) + if err != nil { + return "", 0, 0, err + } + + return pwd, uid, rootless.GetRootlessGID(), nil +} + +func (c *Container) userPasswdEntry(u *user.User) (string, error) { + // Look up the user to see if it exists in the container image. + _, err := lookup.GetUser(c.state.Mountpoint, u.Username) + if err != runcuser.ErrNoPasswdEntries { + return "", err + } + + // Look up the UID to see if it exists in the container image. + _, err = lookup.GetUser(c.state.Mountpoint, u.Uid) + if err != runcuser.ErrNoPasswdEntries { + return "", err + } + + // If the user's actual home directory exists, or was mounted in - use + // that. + homeDir := c.WorkingDir() + hDir := u.HomeDir + for hDir != "/" { + if MountExists(c.config.Spec.Mounts, hDir) { + homeDir = u.HomeDir + break + } + hDir = filepath.Dir(hDir) + } + if homeDir != u.HomeDir { + for _, hDir := range c.UserVolumes() { + if hDir == u.HomeDir { + homeDir = u.HomeDir + break + } + } + } + // Set HOME environment if not already set + hasHomeSet := false + for _, s := range c.config.Spec.Process.Env { + if strings.HasPrefix(s, "HOME=") { + hasHomeSet = true + break + } + } + if !hasHomeSet { + c.config.Spec.Process.Env = append(c.config.Spec.Process.Env, fmt.Sprintf("HOME=%s", homeDir)) + } + if c.config.PasswdEntry != "" { + return c.passwdEntry(u.Username, u.Uid, u.Gid, u.Name, homeDir), nil + } + + return fmt.Sprintf("%s:*:%s:%s:%s:%s:/bin/sh\n", u.Username, u.Uid, u.Gid, u.Name, homeDir), nil +} + +// generateUserPasswdEntry generates an /etc/passwd entry for the container user +// to run in the container. +// The UID and GID of the added entry will also be returned. +// Accepts one argument, that being any UID that has already been added to the +// passwd file by other functions; if it matches the UID we were given, we don't +// need to do anything. +func (c *Container) generateUserPasswdEntry(addedUID int) (string, error) { + var ( + groupspec string + gid int + ) + if c.config.User == "" { + return "", nil + } + splitSpec := strings.SplitN(c.config.User, ":", 2) + userspec := splitSpec[0] + if len(splitSpec) > 1 { + groupspec = splitSpec[1] + } + // If a non numeric User, then don't generate passwd + uid, err := strconv.ParseUint(userspec, 10, 32) + if err != nil { + return "", nil //nolint: nilerr + } + + if addedUID != 0 && int(uid) == addedUID { + return "", nil + } + + // Look up the user to see if it exists in the container image + _, err = lookup.GetUser(c.state.Mountpoint, userspec) + if err != runcuser.ErrNoPasswdEntries { + return "", err + } + + if groupspec != "" { + ugid, err := strconv.ParseUint(groupspec, 10, 32) + if err == nil { + gid = int(ugid) + } else { + group, err := lookup.GetGroup(c.state.Mountpoint, groupspec) + if err != nil { + return "", fmt.Errorf("unable to get gid %s from group file: %w", groupspec, err) + } + gid = group.Gid + } + } + + if c.config.PasswdEntry != "" { + entry := c.passwdEntry(fmt.Sprintf("%d", uid), fmt.Sprintf("%d", uid), fmt.Sprintf("%d", gid), "container user", c.WorkingDir()) + return entry, nil + } + + return fmt.Sprintf("%d:*:%d:%d:container user:%s:/bin/sh\n", uid, uid, gid, c.WorkingDir()), nil +} + +func (c *Container) passwdEntry(username string, uid, gid, name, homeDir string) string { + s := c.config.PasswdEntry + s = strings.ReplaceAll(s, "$USERNAME", username) + s = strings.ReplaceAll(s, "$UID", uid) + s = strings.ReplaceAll(s, "$GID", gid) + s = strings.ReplaceAll(s, "$NAME", name) + s = strings.ReplaceAll(s, "$HOME", homeDir) + return s + "\n" +} + +// generatePasswdAndGroup generates container-specific passwd and group files +// iff g.config.User is a number or we are configured to make a passwd entry for +// the current user or the user specified HostsUsers +// Returns path to file to mount at /etc/passwd, path to file to mount at +// /etc/group, and any error that occurred. If no passwd/group file were +// required, the empty string will be returned for those path (this may occur +// even if no error happened). +// This may modify the mounted container's /etc/passwd and /etc/group instead of +// making copies to bind-mount in, so we don't break useradd (it wants to make a +// copy of /etc/passwd and rename the copy to /etc/passwd, which is impossible +// with a bind mount). This is done in cases where the container is *not* +// read-only. In this case, the function will return nothing ("", "", nil). +func (c *Container) generatePasswdAndGroup() (string, string, error) { + if !c.config.AddCurrentUserPasswdEntry && c.config.User == "" && + len(c.config.HostUsers) == 0 { + return "", "", nil + } + + needPasswd := true + needGroup := true + + // First, check if there's a mount at /etc/passwd or group, we don't + // want to interfere with user mounts. + if MountExists(c.config.Spec.Mounts, "/etc/passwd") { + needPasswd = false + } + if MountExists(c.config.Spec.Mounts, "/etc/group") { + needGroup = false + } + + // Next, check if we already made the files. If we didn't, don't need to + // do anything more. + if needPasswd { + passwdPath := filepath.Join(c.config.StaticDir, "passwd") + if _, err := os.Stat(passwdPath); err == nil { + needPasswd = false + } + } + if needGroup { + groupPath := filepath.Join(c.config.StaticDir, "group") + if _, err := os.Stat(groupPath); err == nil { + needGroup = false + } + } + + // If we don't need a /etc/passwd or /etc/group at this point we can + // just return. + if !needPasswd && !needGroup { + return "", "", nil + } + + passwdPath := "" + groupPath := "" + + ro := c.IsReadOnly() + + if needPasswd { + passwdEntry, err := c.generatePasswdEntry() + if err != nil { + return "", "", err + } + + needsWrite := passwdEntry != "" + switch { + case ro && needsWrite: + logrus.Debugf("Making /etc/passwd for container %s", c.ID()) + originPasswdFile, err := securejoin.SecureJoin(c.state.Mountpoint, "/etc/passwd") + if err != nil { + return "", "", fmt.Errorf("error creating path to container %s /etc/passwd: %w", c.ID(), err) + } + orig, err := ioutil.ReadFile(originPasswdFile) + if err != nil && !os.IsNotExist(err) { + return "", "", err + } + passwdFile, err := c.writeStringToStaticDir("passwd", string(orig)+passwdEntry) + if err != nil { + return "", "", fmt.Errorf("failed to create temporary passwd file: %w", err) + } + if err := os.Chmod(passwdFile, 0644); err != nil { + return "", "", err + } + passwdPath = passwdFile + case !ro && needsWrite: + logrus.Debugf("Modifying container %s /etc/passwd", c.ID()) + containerPasswd, err := securejoin.SecureJoin(c.state.Mountpoint, "/etc/passwd") + if err != nil { + return "", "", fmt.Errorf("error looking up location of container %s /etc/passwd: %w", c.ID(), err) + } + + f, err := os.OpenFile(containerPasswd, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600) + if err != nil { + return "", "", fmt.Errorf("container %s: %w", c.ID(), err) + } + defer f.Close() + + if _, err := f.WriteString(passwdEntry); err != nil { + return "", "", fmt.Errorf("unable to append to container %s /etc/passwd: %w", c.ID(), err) + } + default: + logrus.Debugf("Not modifying container %s /etc/passwd", c.ID()) + } + } + if needGroup { + groupEntry, err := c.generateGroupEntry() + if err != nil { + return "", "", err + } + + needsWrite := groupEntry != "" + switch { + case ro && needsWrite: + logrus.Debugf("Making /etc/group for container %s", c.ID()) + originGroupFile, err := securejoin.SecureJoin(c.state.Mountpoint, "/etc/group") + if err != nil { + return "", "", fmt.Errorf("error creating path to container %s /etc/group: %w", c.ID(), err) + } + orig, err := ioutil.ReadFile(originGroupFile) + if err != nil && !os.IsNotExist(err) { + return "", "", err + } + groupFile, err := c.writeStringToStaticDir("group", string(orig)+groupEntry) + if err != nil { + return "", "", fmt.Errorf("failed to create temporary group file: %w", err) + } + if err := os.Chmod(groupFile, 0644); err != nil { + return "", "", err + } + groupPath = groupFile + case !ro && needsWrite: + logrus.Debugf("Modifying container %s /etc/group", c.ID()) + containerGroup, err := securejoin.SecureJoin(c.state.Mountpoint, "/etc/group") + if err != nil { + return "", "", fmt.Errorf("error looking up location of container %s /etc/group: %w", c.ID(), err) + } + + f, err := os.OpenFile(containerGroup, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600) + if err != nil { + return "", "", fmt.Errorf("container %s: %w", c.ID(), err) + } + defer f.Close() + + if _, err := f.WriteString(groupEntry); err != nil { + return "", "", fmt.Errorf("unable to append to container %s /etc/group: %w", c.ID(), err) + } + default: + logrus.Debugf("Not modifying container %s /etc/group", c.ID()) + } + } + + return passwdPath, groupPath, nil +} diff --git a/libpod/container_internal_freebsd.go b/libpod/container_internal_freebsd.go index c8dbb8e15..aa4983291 100644 --- a/libpod/container_internal_freebsd.go +++ b/libpod/container_internal_freebsd.go @@ -7,33 +7,20 @@ import ( "errors" "fmt" "io" - "io/ioutil" "os" - "os/user" "path/filepath" - "strconv" "strings" "sync" "syscall" - "time" "github.com/containers/buildah/pkg/overlay" - "github.com/containers/common/libnetwork/etchosts" - "github.com/containers/common/libnetwork/resolvconf" "github.com/containers/common/libnetwork/types" "github.com/containers/common/pkg/chown" - "github.com/containers/common/pkg/config" - "github.com/containers/common/pkg/subscriptions" "github.com/containers/common/pkg/umask" "github.com/containers/podman/v4/libpod/define" - "github.com/containers/podman/v4/pkg/lookup" "github.com/containers/podman/v4/pkg/rootless" - "github.com/containers/podman/v4/pkg/util" - "github.com/containers/podman/v4/version" "github.com/containers/storage/pkg/idtools" - "github.com/containers/storage/pkg/lockfile" securejoin "github.com/cyphar/filepath-securejoin" - runcuser "github.com/opencontainers/runc/libcontainer/user" spec "github.com/opencontainers/runtime-spec/specs-go" "github.com/opencontainers/runtime-tools/generate" "github.com/opencontainers/selinux/go-selinux" @@ -190,917 +177,6 @@ func (c *Container) addNetworkContainer(g *generate.Generator, ctr string) error return nil } -// Ensure standard bind mounts are mounted into all root directories (including chroot directories) -func (c *Container) mountIntoRootDirs(mountName string, mountPath string) error { - c.state.BindMounts[mountName] = mountPath - - for _, chrootDir := range c.config.ChrootDirs { - c.state.BindMounts[filepath.Join(chrootDir, mountName)] = mountPath - } - - return nil -} - -// Make standard bind mounts to include in the container -func (c *Container) makeBindMounts() error { - if err := os.Chown(c.state.RunDir, c.RootUID(), c.RootGID()); err != nil { - return fmt.Errorf("cannot chown run directory: %w", err) - } - - if c.state.BindMounts == nil { - c.state.BindMounts = make(map[string]string) - } - netDisabled, err := c.NetworkDisabled() - if err != nil { - return err - } - - if !netDisabled { - // If /etc/resolv.conf and /etc/hosts exist, delete them so we - // will recreate. Only do this if we aren't sharing them with - // another container. - if c.config.NetNsCtr == "" { - if resolvePath, ok := c.state.BindMounts["/etc/resolv.conf"]; ok { - if err := os.Remove(resolvePath); err != nil && !os.IsNotExist(err) { - return fmt.Errorf("container %s: %w", c.ID(), err) - } - delete(c.state.BindMounts, "/etc/resolv.conf") - } - if hostsPath, ok := c.state.BindMounts["/etc/hosts"]; ok { - if err := os.Remove(hostsPath); err != nil && !os.IsNotExist(err) { - return fmt.Errorf("container %s: %w", c.ID(), err) - } - delete(c.state.BindMounts, "/etc/hosts") - } - } - - if c.config.NetNsCtr != "" && (!c.config.UseImageResolvConf || !c.config.UseImageHosts) { - // We share a net namespace. - // We want /etc/resolv.conf and /etc/hosts from the - // other container. Unless we're not creating both of - // them. - depCtr, err := c.getRootNetNsDepCtr() - if err != nil { - return fmt.Errorf("error fetching network namespace dependency container for container %s: %w", c.ID(), err) - } - - // We need that container's bind mounts - bindMounts, err := depCtr.BindMounts() - if err != nil { - return fmt.Errorf("error fetching bind mounts from dependency %s of container %s: %w", depCtr.ID(), c.ID(), err) - } - - // The other container may not have a resolv.conf or /etc/hosts - // If it doesn't, don't copy them - resolvPath, exists := bindMounts["/etc/resolv.conf"] - if !c.config.UseImageResolvConf && exists { - err := c.mountIntoRootDirs("/etc/resolv.conf", resolvPath) - - if err != nil { - return fmt.Errorf("error assigning mounts to container %s: %w", c.ID(), err) - } - } - - // check if dependency container has an /etc/hosts file. - // It may not have one, so only use it if it does. - hostsPath, exists := bindMounts[config.DefaultHostsFile] - if !c.config.UseImageHosts && exists { - // we cannot use the dependency container lock due ABBA deadlocks in cleanup() - lock, err := lockfile.GetLockfile(hostsPath) - if err != nil { - return fmt.Errorf("failed to lock hosts file: %w", err) - } - lock.Lock() - - // add the newly added container to the hosts file - // we always use 127.0.0.1 as ip since they have the same netns - err = etchosts.Add(hostsPath, getLocalhostHostEntry(c)) - lock.Unlock() - if err != nil { - return fmt.Errorf("error creating hosts file for container %s which depends on container %s: %w", c.ID(), depCtr.ID(), err) - } - - // finally, save it in the new container - err = c.mountIntoRootDirs(config.DefaultHostsFile, hostsPath) - if err != nil { - return fmt.Errorf("error assigning mounts to container %s: %w", c.ID(), err) - } - } - - if !hasCurrentUserMapped(c) { - if err := makeAccessible(resolvPath, c.RootUID(), c.RootGID()); err != nil { - return err - } - if err := makeAccessible(hostsPath, c.RootUID(), c.RootGID()); err != nil { - return err - } - } - } else { - if !c.config.UseImageResolvConf { - if err := c.generateResolvConf(); err != nil { - return fmt.Errorf("error creating resolv.conf for container %s: %w", c.ID(), err) - } - } - - if !c.config.UseImageHosts { - if err := c.createHosts(); err != nil { - return fmt.Errorf("error creating hosts file for container %s: %w", c.ID(), err) - } - } - } - - if c.state.BindMounts["/etc/hosts"] != "" { - if err := c.relabel(c.state.BindMounts["/etc/hosts"], c.config.MountLabel, true); err != nil { - return err - } - } - - if c.state.BindMounts["/etc/resolv.conf"] != "" { - if err := c.relabel(c.state.BindMounts["/etc/resolv.conf"], c.config.MountLabel, true); err != nil { - return err - } - } - } else { - if err := c.createHosts(); err != nil { - return fmt.Errorf("error creating hosts file for container %s: %w", c.ID(), err) - } - } - - if c.config.Passwd == nil || *c.config.Passwd { - newPasswd, newGroup, err := c.generatePasswdAndGroup() - if err != nil { - return fmt.Errorf("error creating temporary passwd file for container %s: %w", c.ID(), err) - } - if newPasswd != "" { - // Make /etc/passwd - // If it already exists, delete so we can recreate - delete(c.state.BindMounts, "/etc/passwd") - c.state.BindMounts["/etc/passwd"] = newPasswd - } - if newGroup != "" { - // Make /etc/group - // If it already exists, delete so we can recreate - delete(c.state.BindMounts, "/etc/group") - c.state.BindMounts["/etc/group"] = newGroup - } - } - - // Make /etc/localtime - ctrTimezone := c.Timezone() - if ctrTimezone != "" { - // validate the format of the timezone specified if it's not "local" - if ctrTimezone != "local" { - _, err = time.LoadLocation(ctrTimezone) - if err != nil { - return fmt.Errorf("error finding timezone for container %s: %w", c.ID(), err) - } - } - if _, ok := c.state.BindMounts["/etc/localtime"]; !ok { - var zonePath string - if ctrTimezone == "local" { - zonePath, err = filepath.EvalSymlinks("/etc/localtime") - if err != nil { - return fmt.Errorf("error finding local timezone for container %s: %w", c.ID(), err) - } - } else { - zone := filepath.Join("/usr/share/zoneinfo", ctrTimezone) - zonePath, err = filepath.EvalSymlinks(zone) - if err != nil { - return fmt.Errorf("error setting timezone for container %s: %w", c.ID(), err) - } - } - localtimePath, err := c.copyTimezoneFile(zonePath) - if err != nil { - return fmt.Errorf("error setting timezone for container %s: %w", c.ID(), err) - } - c.state.BindMounts["/etc/localtime"] = localtimePath - } - } - - // Make .containerenv if it does not exist - if _, ok := c.state.BindMounts["/run/.containerenv"]; !ok { - containerenv := c.runtime.graphRootMountedFlag(c.config.Spec.Mounts) - isRootless := 0 - if rootless.IsRootless() { - isRootless = 1 - } - imageID, imageName := c.Image() - - if c.Privileged() { - // Populate the .containerenv with container information - containerenv = fmt.Sprintf(`engine="podman-%s" -name=%q -id=%q -image=%q -imageid=%q -rootless=%d -%s`, version.Version.String(), c.Name(), c.ID(), imageName, imageID, isRootless, containerenv) - } - containerenvPath, err := c.writeStringToRundir(".containerenv", containerenv) - if err != nil { - return fmt.Errorf("error creating containerenv file for container %s: %w", c.ID(), err) - } - c.state.BindMounts["/run/.containerenv"] = containerenvPath - } - - // Add Subscription Mounts - subscriptionMounts := subscriptions.MountsWithUIDGID(c.config.MountLabel, c.state.RunDir, c.runtime.config.Containers.DefaultMountsFile, c.state.Mountpoint, c.RootUID(), c.RootGID(), rootless.IsRootless(), false) - for _, mount := range subscriptionMounts { - if _, ok := c.state.BindMounts[mount.Destination]; !ok { - c.state.BindMounts[mount.Destination] = mount.Source - } - } - - // Secrets are mounted by getting the secret data from the secrets manager, - // copying the data into the container's static dir, - // then mounting the copied dir into /run/secrets. - // The secrets mounting must come after subscription mounts, since subscription mounts - // creates the /run/secrets dir in the container where we mount as well. - if len(c.Secrets()) > 0 { - // create /run/secrets if subscriptions did not create - if err := c.createSecretMountDir(); err != nil { - return fmt.Errorf("error creating secrets mount: %w", err) - } - for _, secret := range c.Secrets() { - secretFileName := secret.Name - base := "/run/secrets" - if secret.Target != "" { - secretFileName = secret.Target - //If absolute path for target given remove base. - if filepath.IsAbs(secretFileName) { - base = "" - } - } - src := filepath.Join(c.config.SecretsPath, secret.Name) - dest := filepath.Join(base, secretFileName) - c.state.BindMounts[dest] = src - } - } - - return nil -} - -// generateResolvConf generates a containers resolv.conf -func (c *Container) generateResolvConf() error { - var ( - networkNameServers []string - networkSearchDomains []string - ) - - netStatus := c.getNetworkStatus() - for _, status := range netStatus { - if status.DNSServerIPs != nil { - for _, nsIP := range status.DNSServerIPs { - networkNameServers = append(networkNameServers, nsIP.String()) - } - logrus.Debugf("Adding nameserver(s) from network status of '%q'", status.DNSServerIPs) - } - if status.DNSSearchDomains != nil { - networkSearchDomains = append(networkSearchDomains, status.DNSSearchDomains...) - logrus.Debugf("Adding search domain(s) from network status of '%q'", status.DNSSearchDomains) - } - } - - ipv6, err := c.checkForIPv6(netStatus) - if err != nil { - return err - } - - nameservers := make([]string, 0, len(c.runtime.config.Containers.DNSServers)+len(c.config.DNSServer)) - nameservers = append(nameservers, c.runtime.config.Containers.DNSServers...) - for _, ip := range c.config.DNSServer { - nameservers = append(nameservers, ip.String()) - } - // If the user provided dns, it trumps all; then dns masq; then resolv.conf - var search []string - keepHostServers := false - if len(nameservers) == 0 { - keepHostServers = true - // first add the nameservers from the networks status - nameservers = networkNameServers - // when we add network dns server we also have to add the search domains - search = networkSearchDomains - } - - if len(c.config.DNSSearch) > 0 || len(c.runtime.config.Containers.DNSSearches) > 0 { - customSearch := make([]string, 0, len(c.config.DNSSearch)+len(c.runtime.config.Containers.DNSSearches)) - customSearch = append(customSearch, c.runtime.config.Containers.DNSSearches...) - customSearch = append(customSearch, c.config.DNSSearch...) - search = customSearch - } - - options := make([]string, 0, len(c.config.DNSOption)+len(c.runtime.config.Containers.DNSOptions)) - options = append(options, c.runtime.config.Containers.DNSOptions...) - options = append(options, c.config.DNSOption...) - - destPath := filepath.Join(c.state.RunDir, "resolv.conf") - - if err := resolvconf.New(&resolvconf.Params{ - IPv6Enabled: ipv6, - KeepHostServers: keepHostServers, - Nameservers: nameservers, - Options: options, - Path: destPath, - Searches: search, - }); err != nil { - return fmt.Errorf("error building resolv.conf for container %s: %w", c.ID(), err) - } - - return c.bindMountRootFile(destPath, resolvconf.DefaultResolvConf) -} - -// Check if a container uses IPv6. -func (c *Container) checkForIPv6(netStatus map[string]types.StatusBlock) (bool, error) { - for _, status := range netStatus { - for _, netInt := range status.Interfaces { - for _, netAddress := range netInt.Subnets { - // Note: only using To16() does not work since it also returns a valid ip for ipv4 - if netAddress.IPNet.IP.To4() == nil && netAddress.IPNet.IP.To16() != nil { - return true, nil - } - } - } - } - - return false, nil -} - -// Add a new nameserver to the container's resolv.conf, ensuring that it is the -// first nameserver present. -// Usable only with running containers. -func (c *Container) addNameserver(ips []string) error { - // Take no action if container is not running. - if !c.ensureState(define.ContainerStateRunning, define.ContainerStateCreated) { - return nil - } - - // Do we have a resolv.conf at all? - path, ok := c.state.BindMounts[resolvconf.DefaultResolvConf] - if !ok { - return nil - } - - if err := resolvconf.Add(path, ips); err != nil { - return fmt.Errorf("adding new nameserver to container %s resolv.conf: %w", c.ID(), err) - } - - return nil -} - -// Remove an entry from the existing resolv.conf of the container. -// Usable only with running containers. -func (c *Container) removeNameserver(ips []string) error { - // Take no action if container is not running. - if !c.ensureState(define.ContainerStateRunning, define.ContainerStateCreated) { - return nil - } - - // Do we have a resolv.conf at all? - path, ok := c.state.BindMounts[resolvconf.DefaultResolvConf] - if !ok { - return nil - } - - if err := resolvconf.Remove(path, ips); err != nil { - return fmt.Errorf("removing nameservers from container %s resolv.conf: %w", c.ID(), err) - } - - return nil -} - -func getLocalhostHostEntry(c *Container) etchosts.HostEntries { - return etchosts.HostEntries{{IP: "127.0.0.1", Names: []string{c.Hostname(), c.config.Name}}} -} - -// getHostsEntries returns the container ip host entries for the correct netmode -func (c *Container) getHostsEntries() (etchosts.HostEntries, error) { - var entries etchosts.HostEntries - names := []string{c.Hostname(), c.config.Name} - switch { - case c.config.NetMode.IsBridge(): - entries = etchosts.GetNetworkHostEntries(c.state.NetworkStatus, names...) - default: - // check for net=none - /*if !c.config.CreateNetNS { - for _, ns := range c.config.Spec.Linux.Namespaces { - if ns.Type == spec.NetworkNamespace { - if ns.Path == "" { - entries = etchosts.HostEntries{{IP: "127.0.0.1", Names: names}} - } - break - } - } - }*/ - } - return entries, nil -} - -func (c *Container) createHosts() error { - var containerIPsEntries etchosts.HostEntries - var err error - // if we configure the netns after the container create we should not add - // the hosts here since we have no information about the actual ips - // instead we will add them in c.completeNetworkSetup() - if !c.config.PostConfigureNetNS { - containerIPsEntries, err = c.getHostsEntries() - if err != nil { - return fmt.Errorf("failed to get container ip host entries: %w", err) - } - } - baseHostFile, err := etchosts.GetBaseHostFile(c.runtime.config.Containers.BaseHostsFile, c.state.Mountpoint) - if err != nil { - return err - } - - targetFile := filepath.Join(c.state.RunDir, "hosts") - err = etchosts.New(&etchosts.Params{ - BaseFile: baseHostFile, - ExtraHosts: c.config.HostAdd, - ContainerIPs: containerIPsEntries, - HostContainersInternalIP: etchosts.GetHostContainersInternalIP(c.runtime.config, c.state.NetworkStatus, c.runtime.network), - TargetFile: targetFile, - }) - if err != nil { - return err - } - - return c.bindMountRootFile(targetFile, config.DefaultHostsFile) -} - -// bindMountRootFile will chown and relabel the source file to make it usable in the container. -// It will also add the path to the container bind mount map. -// source is the path on the host, dest is the path in the container. -func (c *Container) bindMountRootFile(source, dest string) error { - if err := os.Chown(source, c.RootUID(), c.RootGID()); err != nil { - return err - } - if err := label.Relabel(source, c.MountLabel(), false); err != nil { - return err - } - - return c.mountIntoRootDirs(dest, source) -} - -// generateGroupEntry generates an entry or entries into /etc/group as -// required by container configuration. -// Generally speaking, we will make an entry under two circumstances: -// 1. The container is started as a specific user:group, and that group is both -// numeric, and does not already exist in /etc/group. -// 2. It is requested that Libpod add the group that launched Podman to -// /etc/group via AddCurrentUserPasswdEntry (though this does not trigger if -// the group in question already exists in /etc/passwd). -// Returns group entry (as a string that can be appended to /etc/group) and any -// error that occurred. -func (c *Container) generateGroupEntry() (string, error) { - groupString := "" - - // Things we *can't* handle: adding the user we added in - // generatePasswdEntry to any *existing* groups. - addedGID := 0 - if c.config.AddCurrentUserPasswdEntry { - entry, gid, err := c.generateCurrentUserGroupEntry() - if err != nil { - return "", err - } - groupString += entry - addedGID = gid - } - if c.config.User != "" { - entry, _, err := c.generateUserGroupEntry(addedGID) - if err != nil { - return "", err - } - groupString += entry - } - - return groupString, nil -} - -// Make an entry in /etc/group for the group of the user running podman iff we -// are rootless. -func (c *Container) generateCurrentUserGroupEntry() (string, int, error) { - gid := rootless.GetRootlessGID() - if gid == 0 { - return "", 0, nil - } - - g, err := user.LookupGroupId(strconv.Itoa(gid)) - if err != nil { - return "", 0, fmt.Errorf("failed to get current group: %w", err) - } - - // Lookup group name to see if it exists in the image. - _, err = lookup.GetGroup(c.state.Mountpoint, g.Name) - if err != runcuser.ErrNoGroupEntries { - return "", 0, err - } - - // Lookup GID to see if it exists in the image. - _, err = lookup.GetGroup(c.state.Mountpoint, g.Gid) - if err != runcuser.ErrNoGroupEntries { - return "", 0, err - } - - // We need to get the username of the rootless user so we can add it to - // the group. - username := "" - uid := rootless.GetRootlessUID() - if uid != 0 { - u, err := user.LookupId(strconv.Itoa(uid)) - if err != nil { - return "", 0, fmt.Errorf("failed to get current user to make group entry: %w", err) - } - username = u.Username - } - - // Make the entry. - return fmt.Sprintf("%s:x:%s:%s\n", g.Name, g.Gid, username), gid, nil -} - -// Make an entry in /etc/group for the group the container was specified to run -// as. -func (c *Container) generateUserGroupEntry(addedGID int) (string, int, error) { - if c.config.User == "" { - return "", 0, nil - } - - splitUser := strings.SplitN(c.config.User, ":", 2) - group := splitUser[0] - if len(splitUser) > 1 { - group = splitUser[1] - } - - gid, err := strconv.ParseUint(group, 10, 32) - if err != nil { - return "", 0, nil // nolint: nilerr - } - - if addedGID != 0 && addedGID == int(gid) { - return "", 0, nil - } - - // Check if the group already exists - _, err = lookup.GetGroup(c.state.Mountpoint, group) - if err != runcuser.ErrNoGroupEntries { - return "", 0, err - } - - return fmt.Sprintf("%d:x:%d:%s\n", gid, gid, splitUser[0]), int(gid), nil -} - -// generatePasswdEntry generates an entry or entries into /etc/passwd as -// required by container configuration. -// Generally speaking, we will make an entry under two circumstances: -// 1. The container is started as a specific user who is not in /etc/passwd. -// This only triggers if the user is given as a *numeric* ID. -// 2. It is requested that Libpod add the user that launched Podman to -// /etc/passwd via AddCurrentUserPasswdEntry (though this does not trigger if -// the user in question already exists in /etc/passwd) or the UID to be added -// is 0). -// 3. The user specified additional host user accounts to add the the /etc/passwd file -// Returns password entry (as a string that can be appended to /etc/passwd) and -// any error that occurred. -func (c *Container) generatePasswdEntry() (string, error) { - passwdString := "" - - addedUID := 0 - for _, userid := range c.config.HostUsers { - // Lookup User on host - u, err := util.LookupUser(userid) - if err != nil { - return "", err - } - entry, err := c.userPasswdEntry(u) - if err != nil { - return "", err - } - passwdString += entry - } - if c.config.AddCurrentUserPasswdEntry { - entry, uid, _, err := c.generateCurrentUserPasswdEntry() - if err != nil { - return "", err - } - passwdString += entry - addedUID = uid - } - if c.config.User != "" { - entry, _, _, err := c.generateUserPasswdEntry(addedUID) - if err != nil { - return "", err - } - passwdString += entry - } - - return passwdString, nil -} - -// generateCurrentUserPasswdEntry generates an /etc/passwd entry for the user -// running the container engine. -// Returns a passwd entry for the user, and the UID and GID of the added entry. -func (c *Container) generateCurrentUserPasswdEntry() (string, int, int, error) { - uid := rootless.GetRootlessUID() - if uid == 0 { - return "", 0, 0, nil - } - - u, err := user.LookupId(strconv.Itoa(uid)) - if err != nil { - return "", 0, 0, fmt.Errorf("failed to get current user: %w", err) - } - pwd, err := c.userPasswdEntry(u) - if err != nil { - return "", 0, 0, err - } - - return pwd, uid, rootless.GetRootlessGID(), nil -} - -func (c *Container) userPasswdEntry(u *user.User) (string, error) { - // Lookup the user to see if it exists in the container image. - _, err := lookup.GetUser(c.state.Mountpoint, u.Username) - if err != runcuser.ErrNoPasswdEntries { - return "", err - } - - // Lookup the UID to see if it exists in the container image. - _, err = lookup.GetUser(c.state.Mountpoint, u.Uid) - if err != runcuser.ErrNoPasswdEntries { - return "", err - } - - // If the user's actual home directory exists, or was mounted in - use - // that. - homeDir := c.WorkingDir() - hDir := u.HomeDir - for hDir != "/" { - if MountExists(c.config.Spec.Mounts, hDir) { - homeDir = u.HomeDir - break - } - hDir = filepath.Dir(hDir) - } - if homeDir != u.HomeDir { - for _, hDir := range c.UserVolumes() { - if hDir == u.HomeDir { - homeDir = u.HomeDir - break - } - } - } - // Set HOME environment if not already set - hasHomeSet := false - for _, s := range c.config.Spec.Process.Env { - if strings.HasPrefix(s, "HOME=") { - hasHomeSet = true - break - } - } - if !hasHomeSet { - c.config.Spec.Process.Env = append(c.config.Spec.Process.Env, fmt.Sprintf("HOME=%s", homeDir)) - } - if c.config.PasswdEntry != "" { - return c.passwdEntry(u.Username, u.Uid, u.Gid, u.Name, homeDir), nil - } - - return fmt.Sprintf("%s:*:%s:%s:%s:%s:/bin/sh\n", u.Username, u.Uid, u.Gid, u.Name, homeDir), nil -} - -// generateUserPasswdEntry generates an /etc/passwd entry for the container user -// to run in the container. -// The UID and GID of the added entry will also be returned. -// Accepts one argument, that being any UID that has already been added to the -// passwd file by other functions; if it matches the UID we were given, we don't -// need to do anything. -func (c *Container) generateUserPasswdEntry(addedUID int) (string, int, int, error) { - var ( - groupspec string - gid int - ) - if c.config.User == "" { - return "", 0, 0, nil - } - splitSpec := strings.SplitN(c.config.User, ":", 2) - userspec := splitSpec[0] - if len(splitSpec) > 1 { - groupspec = splitSpec[1] - } - // If a non numeric User, then don't generate passwd - uid, err := strconv.ParseUint(userspec, 10, 32) - if err != nil { - return "", 0, 0, nil // nolint: nilerr - } - - if addedUID != 0 && int(uid) == addedUID { - return "", 0, 0, nil - } - - // Lookup the user to see if it exists in the container image - _, err = lookup.GetUser(c.state.Mountpoint, userspec) - if err != runcuser.ErrNoPasswdEntries { - return "", 0, 0, err - } - - if groupspec != "" { - ugid, err := strconv.ParseUint(groupspec, 10, 32) - if err == nil { - gid = int(ugid) - } else { - group, err := lookup.GetGroup(c.state.Mountpoint, groupspec) - if err != nil { - return "", 0, 0, fmt.Errorf("unable to get gid %s from group file: %w", groupspec, err) - } - gid = group.Gid - } - } - - if c.config.PasswdEntry != "" { - entry := c.passwdEntry(fmt.Sprintf("%d", uid), fmt.Sprintf("%d", uid), fmt.Sprintf("%d", gid), "container user", c.WorkingDir()) - return entry, int(uid), gid, nil - } - - return fmt.Sprintf("%d:*:%d:%d:container user:%s:/bin/sh\n", uid, uid, gid, c.WorkingDir()), int(uid), gid, nil -} - -func (c *Container) passwdEntry(username string, uid, gid, name, homeDir string) string { - s := c.config.PasswdEntry - s = strings.Replace(s, "$USERNAME", username, -1) - s = strings.Replace(s, "$UID", uid, -1) - s = strings.Replace(s, "$GID", gid, -1) - s = strings.Replace(s, "$NAME", name, -1) - s = strings.Replace(s, "$HOME", homeDir, -1) - return s + "\n" -} - -// generatePasswdAndGroup generates container-specific passwd and group files -// iff g.config.User is a number or we are configured to make a passwd entry for -// the current user or the user specified HostsUsers -// Returns path to file to mount at /etc/passwd, path to file to mount at -// /etc/group, and any error that occurred. If no passwd/group file were -// required, the empty string will be returned for those path (this may occur -// even if no error happened). -// This may modify the mounted container's /etc/passwd and /etc/group instead of -// making copies to bind-mount in, so we don't break useradd (it wants to make a -// copy of /etc/passwd and rename the copy to /etc/passwd, which is impossible -// with a bind mount). This is done in cases where the container is *not* -// read-only. In this case, the function will return nothing ("", "", nil). -func (c *Container) generatePasswdAndGroup() (string, string, error) { - if !c.config.AddCurrentUserPasswdEntry && c.config.User == "" && - len(c.config.HostUsers) == 0 { - return "", "", nil - } - - needPasswd := true - needGroup := true - - // First, check if there's a mount at /etc/passwd or group, we don't - // want to interfere with user mounts. - if MountExists(c.config.Spec.Mounts, "/etc/passwd") { - needPasswd = false - } - if MountExists(c.config.Spec.Mounts, "/etc/group") { - needGroup = false - } - - // Next, check if we already made the files. If we didn't, don't need to - // do anything more. - if needPasswd { - passwdPath := filepath.Join(c.config.StaticDir, "passwd") - if _, err := os.Stat(passwdPath); err == nil { - needPasswd = false - } - } - if needGroup { - groupPath := filepath.Join(c.config.StaticDir, "group") - if _, err := os.Stat(groupPath); err == nil { - needGroup = false - } - } - - // Next, check if the container even has a /etc/passwd or /etc/group. - // If it doesn't we don't want to create them ourselves. - if needPasswd { - exists, err := c.checkFileExistsInRootfs("/etc/passwd") - if err != nil { - return "", "", err - } - needPasswd = exists - } - if needGroup { - exists, err := c.checkFileExistsInRootfs("/etc/group") - if err != nil { - return "", "", err - } - needGroup = exists - } - - // If we don't need a /etc/passwd or /etc/group at this point we can - // just return. - if !needPasswd && !needGroup { - return "", "", nil - } - - passwdPath := "" - groupPath := "" - - ro := c.IsReadOnly() - - if needPasswd { - passwdEntry, err := c.generatePasswdEntry() - if err != nil { - return "", "", err - } - - needsWrite := passwdEntry != "" - switch { - case ro && needsWrite: - logrus.Debugf("Making /etc/passwd for container %s", c.ID()) - originPasswdFile, err := securejoin.SecureJoin(c.state.Mountpoint, "/etc/passwd") - if err != nil { - return "", "", fmt.Errorf("error creating path to container %s /etc/passwd: %w", c.ID(), err) - } - orig, err := ioutil.ReadFile(originPasswdFile) - if err != nil && !os.IsNotExist(err) { - return "", "", err - } - passwdFile, err := c.writeStringToStaticDir("passwd", string(orig)+passwdEntry) - if err != nil { - return "", "", fmt.Errorf("failed to create temporary passwd file: %w", err) - } - if err := os.Chmod(passwdFile, 0644); err != nil { - return "", "", err - } - passwdPath = passwdFile - case !ro && needsWrite: - logrus.Debugf("Modifying container %s /etc/passwd", c.ID()) - containerPasswd, err := securejoin.SecureJoin(c.state.Mountpoint, "/etc/passwd") - if err != nil { - return "", "", fmt.Errorf("error looking up location of container %s /etc/passwd: %w", c.ID(), err) - } - - f, err := os.OpenFile(containerPasswd, os.O_APPEND|os.O_WRONLY, 0600) - if err != nil { - return "", "", fmt.Errorf("container %s: %w", c.ID(), err) - } - defer f.Close() - - if _, err := f.WriteString(passwdEntry); err != nil { - return "", "", fmt.Errorf("unable to append to container %s /etc/passwd: %w", c.ID(), err) - } - default: - logrus.Debugf("Not modifying container %s /etc/passwd", c.ID()) - } - } - if needGroup { - groupEntry, err := c.generateGroupEntry() - if err != nil { - return "", "", err - } - - needsWrite := groupEntry != "" - switch { - case ro && needsWrite: - logrus.Debugf("Making /etc/group for container %s", c.ID()) - originGroupFile, err := securejoin.SecureJoin(c.state.Mountpoint, "/etc/group") - if err != nil { - return "", "", fmt.Errorf("error creating path to container %s /etc/group: %w", c.ID(), err) - } - orig, err := ioutil.ReadFile(originGroupFile) - if err != nil && !os.IsNotExist(err) { - return "", "", err - } - groupFile, err := c.writeStringToStaticDir("group", string(orig)+groupEntry) - if err != nil { - return "", "", fmt.Errorf("failed to create temporary group file: %w", err) - } - if err := os.Chmod(groupFile, 0644); err != nil { - return "", "", err - } - groupPath = groupFile - case !ro && needsWrite: - logrus.Debugf("Modifying container %s /etc/group", c.ID()) - containerGroup, err := securejoin.SecureJoin(c.state.Mountpoint, "/etc/group") - if err != nil { - return "", "", fmt.Errorf("error looking up location of container %s /etc/group: %w", c.ID(), err) - } - - f, err := os.OpenFile(containerGroup, os.O_APPEND|os.O_WRONLY, 0600) - if err != nil { - return "", "", fmt.Errorf("container %s: %w", c.ID(), err) - } - defer f.Close() - - if _, err := f.WriteString(groupEntry); err != nil { - return "", "", fmt.Errorf("unable to append to container %s /etc/group: %w", c.ID(), err) - } - default: - logrus.Debugf("Not modifying container %s /etc/group", c.ID()) - } - } - - return passwdPath, groupPath, nil -} - func isRootlessCgroupSet(cgroup string) bool { return false } diff --git a/libpod/container_internal_linux.go b/libpod/container_internal_linux.go index b2a77c9bb..752ecae77 100644 --- a/libpod/container_internal_linux.go +++ b/libpod/container_internal_linux.go @@ -7,12 +7,9 @@ import ( "errors" "fmt" "io" - "io/ioutil" "os" - "os/user" "path" "path/filepath" - "strconv" "strings" "sync" "syscall" @@ -20,24 +17,15 @@ import ( "github.com/containernetworking/plugins/pkg/ns" "github.com/containers/buildah/pkg/overlay" - "github.com/containers/common/libnetwork/etchosts" - "github.com/containers/common/libnetwork/resolvconf" "github.com/containers/common/libnetwork/types" "github.com/containers/common/pkg/cgroups" "github.com/containers/common/pkg/chown" "github.com/containers/common/pkg/config" - "github.com/containers/common/pkg/subscriptions" "github.com/containers/common/pkg/umask" "github.com/containers/podman/v4/libpod/define" - "github.com/containers/podman/v4/pkg/lookup" "github.com/containers/podman/v4/pkg/rootless" - "github.com/containers/podman/v4/pkg/util" "github.com/containers/podman/v4/utils" - "github.com/containers/podman/v4/version" "github.com/containers/storage/pkg/idtools" - "github.com/containers/storage/pkg/lockfile" - securejoin "github.com/cyphar/filepath-securejoin" - runcuser "github.com/opencontainers/runc/libcontainer/user" spec "github.com/opencontainers/runtime-spec/specs-go" "github.com/opencontainers/runtime-tools/generate" "github.com/opencontainers/selinux/go-selinux" @@ -343,956 +331,6 @@ func (c *Container) addNamespaceContainer(g *generate.Generator, ns LinuxNS, ctr return nil } -// Ensure standard bind mounts are mounted into all root directories (including chroot directories) -func (c *Container) mountIntoRootDirs(mountName string, mountPath string) error { - c.state.BindMounts[mountName] = mountPath - - for _, chrootDir := range c.config.ChrootDirs { - c.state.BindMounts[filepath.Join(chrootDir, mountName)] = mountPath - } - - return nil -} - -// Make standard bind mounts to include in the container -func (c *Container) makeBindMounts() error { - if err := os.Chown(c.state.RunDir, c.RootUID(), c.RootGID()); err != nil { - return fmt.Errorf("cannot chown run directory: %w", err) - } - - if c.state.BindMounts == nil { - c.state.BindMounts = make(map[string]string) - } - netDisabled, err := c.NetworkDisabled() - if err != nil { - return err - } - - if !netDisabled { - // If /etc/resolv.conf and /etc/hosts exist, delete them so we - // will recreate. Only do this if we aren't sharing them with - // another container. - if c.config.NetNsCtr == "" { - if resolvePath, ok := c.state.BindMounts["/etc/resolv.conf"]; ok { - if err := os.Remove(resolvePath); err != nil && !os.IsNotExist(err) { - return fmt.Errorf("container %s: %w", c.ID(), err) - } - delete(c.state.BindMounts, "/etc/resolv.conf") - } - if hostsPath, ok := c.state.BindMounts["/etc/hosts"]; ok { - if err := os.Remove(hostsPath); err != nil && !os.IsNotExist(err) { - return fmt.Errorf("container %s: %w", c.ID(), err) - } - delete(c.state.BindMounts, "/etc/hosts") - } - } - - if c.config.NetNsCtr != "" && (!c.config.UseImageResolvConf || !c.config.UseImageHosts) { - // We share a net namespace. - // We want /etc/resolv.conf and /etc/hosts from the - // other container. Unless we're not creating both of - // them. - depCtr, err := c.getRootNetNsDepCtr() - if err != nil { - return fmt.Errorf("error fetching network namespace dependency container for container %s: %w", c.ID(), err) - } - - // We need that container's bind mounts - bindMounts, err := depCtr.BindMounts() - if err != nil { - return fmt.Errorf("error fetching bind mounts from dependency %s of container %s: %w", depCtr.ID(), c.ID(), err) - } - - // The other container may not have a resolv.conf or /etc/hosts - // If it doesn't, don't copy them - resolvPath, exists := bindMounts["/etc/resolv.conf"] - if !c.config.UseImageResolvConf && exists { - err := c.mountIntoRootDirs("/etc/resolv.conf", resolvPath) - - if err != nil { - return fmt.Errorf("error assigning mounts to container %s: %w", c.ID(), err) - } - } - - // check if dependency container has an /etc/hosts file. - // It may not have one, so only use it if it does. - hostsPath, exists := bindMounts[config.DefaultHostsFile] - if !c.config.UseImageHosts && exists { - // we cannot use the dependency container lock due ABBA deadlocks in cleanup() - lock, err := lockfile.GetLockfile(hostsPath) - if err != nil { - return fmt.Errorf("failed to lock hosts file: %w", err) - } - lock.Lock() - - // add the newly added container to the hosts file - // we always use 127.0.0.1 as ip since they have the same netns - err = etchosts.Add(hostsPath, getLocalhostHostEntry(c)) - lock.Unlock() - if err != nil { - return fmt.Errorf("error creating hosts file for container %s which depends on container %s: %w", c.ID(), depCtr.ID(), err) - } - - // finally, save it in the new container - err = c.mountIntoRootDirs(config.DefaultHostsFile, hostsPath) - if err != nil { - return fmt.Errorf("error assigning mounts to container %s: %w", c.ID(), err) - } - } - - if !hasCurrentUserMapped(c) { - if err := makeAccessible(resolvPath, c.RootUID(), c.RootGID()); err != nil { - return err - } - if err := makeAccessible(hostsPath, c.RootUID(), c.RootGID()); err != nil { - return err - } - } - } else { - if !c.config.UseImageResolvConf { - if err := c.generateResolvConf(); err != nil { - return fmt.Errorf("error creating resolv.conf for container %s: %w", c.ID(), err) - } - } - - if !c.config.UseImageHosts { - if err := c.createHosts(); err != nil { - return fmt.Errorf("error creating hosts file for container %s: %w", c.ID(), err) - } - } - } - - if c.state.BindMounts["/etc/hosts"] != "" { - if err := c.relabel(c.state.BindMounts["/etc/hosts"], c.config.MountLabel, true); err != nil { - return err - } - } - - if c.state.BindMounts["/etc/resolv.conf"] != "" { - if err := c.relabel(c.state.BindMounts["/etc/resolv.conf"], c.config.MountLabel, true); err != nil { - return err - } - } - } else if !c.config.UseImageHosts && c.state.BindMounts["/etc/hosts"] == "" { - if err := c.createHosts(); err != nil { - return fmt.Errorf("error creating hosts file for container %s: %w", c.ID(), err) - } - } - - if c.config.ShmDir != "" { - // If ShmDir has a value SHM is always added when we mount the container - c.state.BindMounts["/dev/shm"] = c.config.ShmDir - } - - if c.config.Passwd == nil || *c.config.Passwd { - newPasswd, newGroup, err := c.generatePasswdAndGroup() - if err != nil { - return fmt.Errorf("error creating temporary passwd file for container %s: %w", c.ID(), err) - } - if newPasswd != "" { - // Make /etc/passwd - // If it already exists, delete so we can recreate - delete(c.state.BindMounts, "/etc/passwd") - c.state.BindMounts["/etc/passwd"] = newPasswd - } - if newGroup != "" { - // Make /etc/group - // If it already exists, delete so we can recreate - delete(c.state.BindMounts, "/etc/group") - c.state.BindMounts["/etc/group"] = newGroup - } - } - - // Make /etc/hostname - // This should never change, so no need to recreate if it exists - if _, ok := c.state.BindMounts["/etc/hostname"]; !ok { - hostnamePath, err := c.writeStringToRundir("hostname", c.Hostname()) - if err != nil { - return fmt.Errorf("error creating hostname file for container %s: %w", c.ID(), err) - } - c.state.BindMounts["/etc/hostname"] = hostnamePath - } - - // Make /etc/localtime - ctrTimezone := c.Timezone() - if ctrTimezone != "" { - // validate the format of the timezone specified if it's not "local" - if ctrTimezone != "local" { - _, err = time.LoadLocation(ctrTimezone) - if err != nil { - return fmt.Errorf("error finding timezone for container %s: %w", c.ID(), err) - } - } - if _, ok := c.state.BindMounts["/etc/localtime"]; !ok { - var zonePath string - if ctrTimezone == "local" { - zonePath, err = filepath.EvalSymlinks("/etc/localtime") - if err != nil { - return fmt.Errorf("error finding local timezone for container %s: %w", c.ID(), err) - } - } else { - zone := filepath.Join("/usr/share/zoneinfo", ctrTimezone) - zonePath, err = filepath.EvalSymlinks(zone) - if err != nil { - return fmt.Errorf("error setting timezone for container %s: %w", c.ID(), err) - } - } - localtimePath, err := c.copyTimezoneFile(zonePath) - if err != nil { - return fmt.Errorf("error setting timezone for container %s: %w", c.ID(), err) - } - c.state.BindMounts["/etc/localtime"] = localtimePath - } - } - - _, hasRunContainerenv := c.state.BindMounts["/run/.containerenv"] - if !hasRunContainerenv { - // check in the spec mounts - for _, m := range c.config.Spec.Mounts { - if m.Destination == "/run/.containerenv" || m.Destination == "/run" { - hasRunContainerenv = true - break - } - } - } - - // Make .containerenv if it does not exist - if !hasRunContainerenv { - containerenv := c.runtime.graphRootMountedFlag(c.config.Spec.Mounts) - isRootless := 0 - if rootless.IsRootless() { - isRootless = 1 - } - imageID, imageName := c.Image() - - if c.Privileged() { - // Populate the .containerenv with container information - containerenv = fmt.Sprintf(`engine="podman-%s" -name=%q -id=%q -image=%q -imageid=%q -rootless=%d -%s`, version.Version.String(), c.Name(), c.ID(), imageName, imageID, isRootless, containerenv) - } - containerenvPath, err := c.writeStringToRundir(".containerenv", containerenv) - if err != nil { - return fmt.Errorf("error creating containerenv file for container %s: %w", c.ID(), err) - } - c.state.BindMounts["/run/.containerenv"] = containerenvPath - } - - // Add Subscription Mounts - subscriptionMounts := subscriptions.MountsWithUIDGID(c.config.MountLabel, c.state.RunDir, c.runtime.config.Containers.DefaultMountsFile, c.state.Mountpoint, c.RootUID(), c.RootGID(), rootless.IsRootless(), false) - for _, mount := range subscriptionMounts { - if _, ok := c.state.BindMounts[mount.Destination]; !ok { - c.state.BindMounts[mount.Destination] = mount.Source - } - } - - // Secrets are mounted by getting the secret data from the secrets manager, - // copying the data into the container's static dir, - // then mounting the copied dir into /run/secrets. - // The secrets mounting must come after subscription mounts, since subscription mounts - // creates the /run/secrets dir in the container where we mount as well. - if len(c.Secrets()) > 0 { - // create /run/secrets if subscriptions did not create - if err := c.createSecretMountDir(); err != nil { - return fmt.Errorf("error creating secrets mount: %w", err) - } - for _, secret := range c.Secrets() { - secretFileName := secret.Name - base := "/run/secrets" - if secret.Target != "" { - secretFileName = secret.Target - // If absolute path for target given remove base. - if filepath.IsAbs(secretFileName) { - base = "" - } - } - src := filepath.Join(c.config.SecretsPath, secret.Name) - dest := filepath.Join(base, secretFileName) - c.state.BindMounts[dest] = src - } - } - - return nil -} - -// generateResolvConf generates a containers resolv.conf -func (c *Container) generateResolvConf() error { - var ( - networkNameServers []string - networkSearchDomains []string - ) - - netStatus := c.getNetworkStatus() - for _, status := range netStatus { - if status.DNSServerIPs != nil { - for _, nsIP := range status.DNSServerIPs { - networkNameServers = append(networkNameServers, nsIP.String()) - } - logrus.Debugf("Adding nameserver(s) from network status of '%q'", status.DNSServerIPs) - } - if status.DNSSearchDomains != nil { - networkSearchDomains = append(networkSearchDomains, status.DNSSearchDomains...) - logrus.Debugf("Adding search domain(s) from network status of '%q'", status.DNSSearchDomains) - } - } - - ipv6, err := c.checkForIPv6(netStatus) - if err != nil { - return err - } - - nameservers := make([]string, 0, len(c.runtime.config.Containers.DNSServers)+len(c.config.DNSServer)) - nameservers = append(nameservers, c.runtime.config.Containers.DNSServers...) - for _, ip := range c.config.DNSServer { - nameservers = append(nameservers, ip.String()) - } - // If the user provided dns, it trumps all; then dns masq; then resolv.conf - var search []string - keepHostServers := false - if len(nameservers) == 0 { - keepHostServers = true - // first add the nameservers from the networks status - nameservers = networkNameServers - // when we add network dns server we also have to add the search domains - search = networkSearchDomains - // slirp4netns has a built in DNS forwarder. - if c.config.NetMode.IsSlirp4netns() { - slirp4netnsDNS, err := GetSlirp4netnsDNS(c.slirp4netnsSubnet) - if err != nil { - logrus.Warn("Failed to determine Slirp4netns DNS: ", err.Error()) - } else { - nameservers = append(nameservers, slirp4netnsDNS.String()) - } - } - } - - if len(c.config.DNSSearch) > 0 || len(c.runtime.config.Containers.DNSSearches) > 0 { - customSearch := make([]string, 0, len(c.config.DNSSearch)+len(c.runtime.config.Containers.DNSSearches)) - customSearch = append(customSearch, c.runtime.config.Containers.DNSSearches...) - customSearch = append(customSearch, c.config.DNSSearch...) - search = customSearch - } - - options := make([]string, 0, len(c.config.DNSOption)+len(c.runtime.config.Containers.DNSOptions)) - options = append(options, c.runtime.config.Containers.DNSOptions...) - options = append(options, c.config.DNSOption...) - - destPath := filepath.Join(c.state.RunDir, "resolv.conf") - - if err := resolvconf.New(&resolvconf.Params{ - IPv6Enabled: ipv6, - KeepHostServers: keepHostServers, - Nameservers: nameservers, - Namespaces: c.config.Spec.Linux.Namespaces, - Options: options, - Path: destPath, - Searches: search, - }); err != nil { - return fmt.Errorf("error building resolv.conf for container %s: %w", c.ID(), err) - } - - return c.bindMountRootFile(destPath, resolvconf.DefaultResolvConf) -} - -// Check if a container uses IPv6. -func (c *Container) checkForIPv6(netStatus map[string]types.StatusBlock) (bool, error) { - for _, status := range netStatus { - for _, netInt := range status.Interfaces { - for _, netAddress := range netInt.Subnets { - // Note: only using To16() does not work since it also returns a valid ip for ipv4 - if netAddress.IPNet.IP.To4() == nil && netAddress.IPNet.IP.To16() != nil { - return true, nil - } - } - } - } - - if c.config.NetMode.IsSlirp4netns() { - ctrNetworkSlipOpts := []string{} - if c.config.NetworkOptions != nil { - ctrNetworkSlipOpts = append(ctrNetworkSlipOpts, c.config.NetworkOptions["slirp4netns"]...) - } - slirpOpts, err := parseSlirp4netnsNetworkOptions(c.runtime, ctrNetworkSlipOpts) - if err != nil { - return false, err - } - return slirpOpts.enableIPv6, nil - } - - return false, nil -} - -// Add a new nameserver to the container's resolv.conf, ensuring that it is the -// first nameserver present. -// Usable only with running containers. -func (c *Container) addNameserver(ips []string) error { - // Take no action if container is not running. - if !c.ensureState(define.ContainerStateRunning, define.ContainerStateCreated) { - return nil - } - - // Do we have a resolv.conf at all? - path, ok := c.state.BindMounts[resolvconf.DefaultResolvConf] - if !ok { - return nil - } - - if err := resolvconf.Add(path, ips); err != nil { - return fmt.Errorf("adding new nameserver to container %s resolv.conf: %w", c.ID(), err) - } - - return nil -} - -// Remove an entry from the existing resolv.conf of the container. -// Usable only with running containers. -func (c *Container) removeNameserver(ips []string) error { - // Take no action if container is not running. - if !c.ensureState(define.ContainerStateRunning, define.ContainerStateCreated) { - return nil - } - - // Do we have a resolv.conf at all? - path, ok := c.state.BindMounts[resolvconf.DefaultResolvConf] - if !ok { - return nil - } - - if err := resolvconf.Remove(path, ips); err != nil { - return fmt.Errorf("removing nameservers from container %s resolv.conf: %w", c.ID(), err) - } - - return nil -} - -func getLocalhostHostEntry(c *Container) etchosts.HostEntries { - return etchosts.HostEntries{{IP: "127.0.0.1", Names: []string{c.Hostname(), c.config.Name}}} -} - -// getHostsEntries returns the container ip host entries for the correct netmode -func (c *Container) getHostsEntries() (etchosts.HostEntries, error) { - var entries etchosts.HostEntries - names := []string{c.Hostname(), c.config.Name} - switch { - case c.config.NetMode.IsBridge(): - entries = etchosts.GetNetworkHostEntries(c.state.NetworkStatus, names...) - case c.config.NetMode.IsSlirp4netns(): - ip, err := GetSlirp4netnsIP(c.slirp4netnsSubnet) - if err != nil { - return nil, err - } - entries = etchosts.HostEntries{{IP: ip.String(), Names: names}} - default: - // check for net=none - if !c.config.CreateNetNS { - for _, ns := range c.config.Spec.Linux.Namespaces { - if ns.Type == spec.NetworkNamespace { - if ns.Path == "" { - entries = etchosts.HostEntries{{IP: "127.0.0.1", Names: names}} - } - break - } - } - } - } - return entries, nil -} - -func (c *Container) createHosts() error { - var containerIPsEntries etchosts.HostEntries - var err error - // if we configure the netns after the container create we should not add - // the hosts here since we have no information about the actual ips - // instead we will add them in c.completeNetworkSetup() - if !c.config.PostConfigureNetNS { - containerIPsEntries, err = c.getHostsEntries() - if err != nil { - return fmt.Errorf("failed to get container ip host entries: %w", err) - } - } - baseHostFile, err := etchosts.GetBaseHostFile(c.runtime.config.Containers.BaseHostsFile, c.state.Mountpoint) - if err != nil { - return err - } - - targetFile := filepath.Join(c.state.RunDir, "hosts") - err = etchosts.New(&etchosts.Params{ - BaseFile: baseHostFile, - ExtraHosts: c.config.HostAdd, - ContainerIPs: containerIPsEntries, - HostContainersInternalIP: etchosts.GetHostContainersInternalIP(c.runtime.config, c.state.NetworkStatus, c.runtime.network), - TargetFile: targetFile, - }) - if err != nil { - return err - } - - return c.bindMountRootFile(targetFile, config.DefaultHostsFile) -} - -// bindMountRootFile will chown and relabel the source file to make it usable in the container. -// It will also add the path to the container bind mount map. -// source is the path on the host, dest is the path in the container. -func (c *Container) bindMountRootFile(source, dest string) error { - if err := os.Chown(source, c.RootUID(), c.RootGID()); err != nil { - return err - } - if err := label.Relabel(source, c.MountLabel(), false); err != nil { - return err - } - - return c.mountIntoRootDirs(dest, source) -} - -// generateGroupEntry generates an entry or entries into /etc/group as -// required by container configuration. -// Generally speaking, we will make an entry under two circumstances: -// 1. The container is started as a specific user:group, and that group is both -// numeric, and does not already exist in /etc/group. -// 2. It is requested that Libpod add the group that launched Podman to -// /etc/group via AddCurrentUserPasswdEntry (though this does not trigger if -// the group in question already exists in /etc/passwd). -// -// Returns group entry (as a string that can be appended to /etc/group) and any -// error that occurred. -func (c *Container) generateGroupEntry() (string, error) { - groupString := "" - - // Things we *can't* handle: adding the user we added in - // generatePasswdEntry to any *existing* groups. - addedGID := 0 - if c.config.AddCurrentUserPasswdEntry { - entry, gid, err := c.generateCurrentUserGroupEntry() - if err != nil { - return "", err - } - groupString += entry - addedGID = gid - } - if c.config.User != "" { - entry, err := c.generateUserGroupEntry(addedGID) - if err != nil { - return "", err - } - groupString += entry - } - - return groupString, nil -} - -// Make an entry in /etc/group for the group of the user running podman iff we -// are rootless. -func (c *Container) generateCurrentUserGroupEntry() (string, int, error) { - gid := rootless.GetRootlessGID() - if gid == 0 { - return "", 0, nil - } - - g, err := user.LookupGroupId(strconv.Itoa(gid)) - if err != nil { - return "", 0, fmt.Errorf("failed to get current group: %w", err) - } - - // Look up group name to see if it exists in the image. - _, err = lookup.GetGroup(c.state.Mountpoint, g.Name) - if err != runcuser.ErrNoGroupEntries { - return "", 0, err - } - - // Look up GID to see if it exists in the image. - _, err = lookup.GetGroup(c.state.Mountpoint, g.Gid) - if err != runcuser.ErrNoGroupEntries { - return "", 0, err - } - - // We need to get the username of the rootless user so we can add it to - // the group. - username := "" - uid := rootless.GetRootlessUID() - if uid != 0 { - u, err := user.LookupId(strconv.Itoa(uid)) - if err != nil { - return "", 0, fmt.Errorf("failed to get current user to make group entry: %w", err) - } - username = u.Username - } - - // Make the entry. - return fmt.Sprintf("%s:x:%s:%s\n", g.Name, g.Gid, username), gid, nil -} - -// Make an entry in /etc/group for the group the container was specified to run -// as. -func (c *Container) generateUserGroupEntry(addedGID int) (string, error) { - if c.config.User == "" { - return "", nil - } - - splitUser := strings.SplitN(c.config.User, ":", 2) - group := splitUser[0] - if len(splitUser) > 1 { - group = splitUser[1] - } - - gid, err := strconv.ParseUint(group, 10, 32) - if err != nil { - return "", nil //nolint: nilerr - } - - if addedGID != 0 && addedGID == int(gid) { - return "", nil - } - - // Check if the group already exists - _, err = lookup.GetGroup(c.state.Mountpoint, group) - if err != runcuser.ErrNoGroupEntries { - return "", err - } - - return fmt.Sprintf("%d:x:%d:%s\n", gid, gid, splitUser[0]), nil -} - -// generatePasswdEntry generates an entry or entries into /etc/passwd as -// required by container configuration. -// Generally speaking, we will make an entry under two circumstances: -// 1. The container is started as a specific user who is not in /etc/passwd. -// This only triggers if the user is given as a *numeric* ID. -// 2. It is requested that Libpod add the user that launched Podman to -// /etc/passwd via AddCurrentUserPasswdEntry (though this does not trigger if -// the user in question already exists in /etc/passwd) or the UID to be added -// is 0). -// 3. The user specified additional host user accounts to add the the /etc/passwd file -// -// Returns password entry (as a string that can be appended to /etc/passwd) and -// any error that occurred. -func (c *Container) generatePasswdEntry() (string, error) { - passwdString := "" - - addedUID := 0 - for _, userid := range c.config.HostUsers { - // Look up User on host - u, err := util.LookupUser(userid) - if err != nil { - return "", err - } - entry, err := c.userPasswdEntry(u) - if err != nil { - return "", err - } - passwdString += entry - } - if c.config.AddCurrentUserPasswdEntry { - entry, uid, _, err := c.generateCurrentUserPasswdEntry() - if err != nil { - return "", err - } - passwdString += entry - addedUID = uid - } - if c.config.User != "" { - entry, err := c.generateUserPasswdEntry(addedUID) - if err != nil { - return "", err - } - passwdString += entry - } - - return passwdString, nil -} - -// generateCurrentUserPasswdEntry generates an /etc/passwd entry for the user -// running the container engine. -// Returns a passwd entry for the user, and the UID and GID of the added entry. -func (c *Container) generateCurrentUserPasswdEntry() (string, int, int, error) { - uid := rootless.GetRootlessUID() - if uid == 0 { - return "", 0, 0, nil - } - - u, err := user.LookupId(strconv.Itoa(uid)) - if err != nil { - return "", 0, 0, fmt.Errorf("failed to get current user: %w", err) - } - pwd, err := c.userPasswdEntry(u) - if err != nil { - return "", 0, 0, err - } - - return pwd, uid, rootless.GetRootlessGID(), nil -} - -func (c *Container) userPasswdEntry(u *user.User) (string, error) { - // Look up the user to see if it exists in the container image. - _, err := lookup.GetUser(c.state.Mountpoint, u.Username) - if err != runcuser.ErrNoPasswdEntries { - return "", err - } - - // Look up the UID to see if it exists in the container image. - _, err = lookup.GetUser(c.state.Mountpoint, u.Uid) - if err != runcuser.ErrNoPasswdEntries { - return "", err - } - - // If the user's actual home directory exists, or was mounted in - use - // that. - homeDir := c.WorkingDir() - hDir := u.HomeDir - for hDir != "/" { - if MountExists(c.config.Spec.Mounts, hDir) { - homeDir = u.HomeDir - break - } - hDir = filepath.Dir(hDir) - } - if homeDir != u.HomeDir { - for _, hDir := range c.UserVolumes() { - if hDir == u.HomeDir { - homeDir = u.HomeDir - break - } - } - } - // Set HOME environment if not already set - hasHomeSet := false - for _, s := range c.config.Spec.Process.Env { - if strings.HasPrefix(s, "HOME=") { - hasHomeSet = true - break - } - } - if !hasHomeSet { - c.config.Spec.Process.Env = append(c.config.Spec.Process.Env, fmt.Sprintf("HOME=%s", homeDir)) - } - if c.config.PasswdEntry != "" { - return c.passwdEntry(u.Username, u.Uid, u.Gid, u.Name, homeDir), nil - } - - return fmt.Sprintf("%s:*:%s:%s:%s:%s:/bin/sh\n", u.Username, u.Uid, u.Gid, u.Name, homeDir), nil -} - -// generateUserPasswdEntry generates an /etc/passwd entry for the container user -// to run in the container. -// The UID and GID of the added entry will also be returned. -// Accepts one argument, that being any UID that has already been added to the -// passwd file by other functions; if it matches the UID we were given, we don't -// need to do anything. -func (c *Container) generateUserPasswdEntry(addedUID int) (string, error) { - var ( - groupspec string - gid int - ) - if c.config.User == "" { - return "", nil - } - splitSpec := strings.SplitN(c.config.User, ":", 2) - userspec := splitSpec[0] - if len(splitSpec) > 1 { - groupspec = splitSpec[1] - } - // If a non numeric User, then don't generate passwd - uid, err := strconv.ParseUint(userspec, 10, 32) - if err != nil { - return "", nil //nolint: nilerr - } - - if addedUID != 0 && int(uid) == addedUID { - return "", nil - } - - // Look up the user to see if it exists in the container image - _, err = lookup.GetUser(c.state.Mountpoint, userspec) - if err != runcuser.ErrNoPasswdEntries { - return "", err - } - - if groupspec != "" { - ugid, err := strconv.ParseUint(groupspec, 10, 32) - if err == nil { - gid = int(ugid) - } else { - group, err := lookup.GetGroup(c.state.Mountpoint, groupspec) - if err != nil { - return "", fmt.Errorf("unable to get gid %s from group file: %w", groupspec, err) - } - gid = group.Gid - } - } - - if c.config.PasswdEntry != "" { - entry := c.passwdEntry(fmt.Sprintf("%d", uid), fmt.Sprintf("%d", uid), fmt.Sprintf("%d", gid), "container user", c.WorkingDir()) - return entry, nil - } - - return fmt.Sprintf("%d:*:%d:%d:container user:%s:/bin/sh\n", uid, uid, gid, c.WorkingDir()), nil -} - -func (c *Container) passwdEntry(username string, uid, gid, name, homeDir string) string { - s := c.config.PasswdEntry - s = strings.ReplaceAll(s, "$USERNAME", username) - s = strings.ReplaceAll(s, "$UID", uid) - s = strings.ReplaceAll(s, "$GID", gid) - s = strings.ReplaceAll(s, "$NAME", name) - s = strings.ReplaceAll(s, "$HOME", homeDir) - return s + "\n" -} - -// generatePasswdAndGroup generates container-specific passwd and group files -// iff g.config.User is a number or we are configured to make a passwd entry for -// the current user or the user specified HostsUsers -// Returns path to file to mount at /etc/passwd, path to file to mount at -// /etc/group, and any error that occurred. If no passwd/group file were -// required, the empty string will be returned for those path (this may occur -// even if no error happened). -// This may modify the mounted container's /etc/passwd and /etc/group instead of -// making copies to bind-mount in, so we don't break useradd (it wants to make a -// copy of /etc/passwd and rename the copy to /etc/passwd, which is impossible -// with a bind mount). This is done in cases where the container is *not* -// read-only. In this case, the function will return nothing ("", "", nil). -func (c *Container) generatePasswdAndGroup() (string, string, error) { - if !c.config.AddCurrentUserPasswdEntry && c.config.User == "" && - len(c.config.HostUsers) == 0 { - return "", "", nil - } - - needPasswd := true - needGroup := true - - // First, check if there's a mount at /etc/passwd or group, we don't - // want to interfere with user mounts. - if MountExists(c.config.Spec.Mounts, "/etc/passwd") { - needPasswd = false - } - if MountExists(c.config.Spec.Mounts, "/etc/group") { - needGroup = false - } - - // Next, check if we already made the files. If we didn't, don't need to - // do anything more. - if needPasswd { - passwdPath := filepath.Join(c.config.StaticDir, "passwd") - if _, err := os.Stat(passwdPath); err == nil { - needPasswd = false - } - } - if needGroup { - groupPath := filepath.Join(c.config.StaticDir, "group") - if _, err := os.Stat(groupPath); err == nil { - needGroup = false - } - } - - // If we don't need a /etc/passwd or /etc/group at this point we can - // just return. - if !needPasswd && !needGroup { - return "", "", nil - } - - passwdPath := "" - groupPath := "" - - ro := c.IsReadOnly() - - if needPasswd { - passwdEntry, err := c.generatePasswdEntry() - if err != nil { - return "", "", err - } - - needsWrite := passwdEntry != "" - switch { - case ro && needsWrite: - logrus.Debugf("Making /etc/passwd for container %s", c.ID()) - originPasswdFile, err := securejoin.SecureJoin(c.state.Mountpoint, "/etc/passwd") - if err != nil { - return "", "", fmt.Errorf("error creating path to container %s /etc/passwd: %w", c.ID(), err) - } - orig, err := ioutil.ReadFile(originPasswdFile) - if err != nil && !os.IsNotExist(err) { - return "", "", err - } - passwdFile, err := c.writeStringToStaticDir("passwd", string(orig)+passwdEntry) - if err != nil { - return "", "", fmt.Errorf("failed to create temporary passwd file: %w", err) - } - if err := os.Chmod(passwdFile, 0644); err != nil { - return "", "", err - } - passwdPath = passwdFile - case !ro && needsWrite: - logrus.Debugf("Modifying container %s /etc/passwd", c.ID()) - containerPasswd, err := securejoin.SecureJoin(c.state.Mountpoint, "/etc/passwd") - if err != nil { - return "", "", fmt.Errorf("error looking up location of container %s /etc/passwd: %w", c.ID(), err) - } - - f, err := os.OpenFile(containerPasswd, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600) - if err != nil { - return "", "", fmt.Errorf("container %s: %w", c.ID(), err) - } - defer f.Close() - - if _, err := f.WriteString(passwdEntry); err != nil { - return "", "", fmt.Errorf("unable to append to container %s /etc/passwd: %w", c.ID(), err) - } - default: - logrus.Debugf("Not modifying container %s /etc/passwd", c.ID()) - } - } - if needGroup { - groupEntry, err := c.generateGroupEntry() - if err != nil { - return "", "", err - } - - needsWrite := groupEntry != "" - switch { - case ro && needsWrite: - logrus.Debugf("Making /etc/group for container %s", c.ID()) - originGroupFile, err := securejoin.SecureJoin(c.state.Mountpoint, "/etc/group") - if err != nil { - return "", "", fmt.Errorf("error creating path to container %s /etc/group: %w", c.ID(), err) - } - orig, err := ioutil.ReadFile(originGroupFile) - if err != nil && !os.IsNotExist(err) { - return "", "", err - } - groupFile, err := c.writeStringToStaticDir("group", string(orig)+groupEntry) - if err != nil { - return "", "", fmt.Errorf("failed to create temporary group file: %w", err) - } - if err := os.Chmod(groupFile, 0644); err != nil { - return "", "", err - } - groupPath = groupFile - case !ro && needsWrite: - logrus.Debugf("Modifying container %s /etc/group", c.ID()) - containerGroup, err := securejoin.SecureJoin(c.state.Mountpoint, "/etc/group") - if err != nil { - return "", "", fmt.Errorf("error looking up location of container %s /etc/group: %w", c.ID(), err) - } - - f, err := os.OpenFile(containerGroup, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600) - if err != nil { - return "", "", fmt.Errorf("container %s: %w", c.ID(), err) - } - defer f.Close() - - if _, err := f.WriteString(groupEntry); err != nil { - return "", "", fmt.Errorf("unable to append to container %s /etc/group: %w", c.ID(), err) - } - default: - logrus.Debugf("Not modifying container %s /etc/group", c.ID()) - } - } - - return passwdPath, groupPath, nil -} - func isRootlessCgroupSet(cgroup string) bool { // old versions of podman were setting the CgroupParent to CgroupfsDefaultCgroupParent // by default. Avoid breaking these versions and check whether the cgroup parent is |