diff --git a/internal/pkg/runtime/launcher/oci/launcher_linux.go b/internal/pkg/runtime/launcher/oci/launcher_linux.go index 7f98e4e97a..4fcb601d48 100644 --- a/internal/pkg/runtime/launcher/oci/launcher_linux.go +++ b/internal/pkg/runtime/launcher/oci/launcher_linux.go @@ -51,6 +51,12 @@ var ( type Launcher struct { cfg launcher.Options apptainerConf *apptainerconf.File + // homeSrc is the computed source (on the host) for the user's home directory. + // An empty value indicates there is no source on the host, and a tmpfs will be used. + homeSrc string + // homeDest is the computed destination (in the container) for the user's home directory. + // An empty value is not valid at mount time. + homeDest string } // NewLauncher returns a oci.Launcher with an initial configuration set by opts. @@ -71,7 +77,17 @@ func NewLauncher(opts ...launcher.Option) (*Launcher, error) { return nil, fmt.Errorf("apptainer configuration is not initialized") } - return &Launcher{cfg: lo, apptainerConf: c}, nil + homeSrc, homeDest, err := parseHomeDir(lo.HomeDir, lo.CustomHome, lo.Fakeroot) + if err != nil { + return nil, err + } + + return &Launcher{ + cfg: lo, + apptainerConf: c, + homeSrc: homeSrc, + homeDest: homeDest, + }, nil } // checkOpts ensures that options set are supported by the oci.Launcher. @@ -183,6 +199,43 @@ func checkOpts(lo launcher.Options) error { return nil } +// parseHomeDir parses the homedir value passed from the CLI layer into a host source, and container dest. +// This includes handling fakeroot and custom --home dst, or --home src:dst specifications. +func parseHomeDir(homedir string, custom, fakeroot bool) (src, dest string, err error) { + // Get the host user's information, looking outside of a user namespace if necessary. + pw, err := rootless.GetUser() + if err != nil { + return "", "", err + } + + // By default in --oci mode there is no external source for $HOME, i.e. a `tmpfs` will be used. + src = "" + // By default the destination in the container matches the users's $HOME on the host. + dest = pw.HomeDir + + // --fakeroot means we are root in the container so $HOME=/root + if fakeroot { + dest = "/root" + } + + // If the user set a custom --home via the CLI, then override the defaults. + if custom { + homeSlice := strings.Split(homedir, ":") + if len(homeSlice) < 1 || len(homeSlice) > 2 { + return "", "", fmt.Errorf("home argument has incorrect number of elements: %v", homeSlice) + } + // A single path was provided, so we will be mounting a tmpfs on this custom destination. + dest = homeSlice[0] + + // Two paths provided (:), so we will be bind mounting from host to container. + if len(homeSlice) > 1 { + src = homeSlice[0] + dest = homeSlice[1] + } + } + return src, dest, nil +} + // createSpec creates an initial OCI runtime specification, suitable to launch a // container. This spec excludes the Process config, as this has to be computed // where the image config is available, to account for the image's CMD / diff --git a/internal/pkg/runtime/launcher/oci/launcher_linux_test.go b/internal/pkg/runtime/launcher/oci/launcher_linux_test.go index 7549b1be6e..49cefecf87 100644 --- a/internal/pkg/runtime/launcher/oci/launcher_linux_test.go +++ b/internal/pkg/runtime/launcher/oci/launcher_linux_test.go @@ -10,6 +10,7 @@ package oci import ( + "os/user" "reflect" "testing" @@ -28,6 +29,11 @@ func TestNewLauncher(t *testing.T) { } apptainerconf.SetCurrentConfig(sc) + u, err := user.Current() + if err != nil { + t.Fatalf("while getting current user: %s", err) + } + tests := []struct { name string opts []launcher.Option @@ -35,16 +41,38 @@ func TestNewLauncher(t *testing.T) { wantErr bool }{ { - name: "default", - want: &Launcher{apptainerConf: sc}, + name: "default", + want: &Launcher{ + apptainerConf: sc, + homeSrc: "", + homeDest: u.HomeDir, + }, + }, + { + name: "homeDest", + opts: []launcher.Option{ + launcher.OptHome("/home/dest", true, false), + }, + want: &Launcher{ + cfg: launcher.Options{HomeDir: "/home/dest", CustomHome: true}, + apptainerConf: sc, + homeSrc: "", + homeDest: "/home/dest", + }, wantErr: false, }, { - name: "validOption", + name: "homeSrcDest", opts: []launcher.Option{ - launcher.OptHome("/home/test", false, false), + launcher.OptHome("/home/src:/home/dest", true, false), }, - want: &Launcher{cfg: launcher.Options{HomeDir: "/home/test"}, apptainerConf: sc}, + want: &Launcher{ + cfg: launcher.Options{HomeDir: "/home/src:/home/dest", CustomHome: true}, + apptainerConf: sc, + homeSrc: "/home/src", + homeDest: "/home/dest", + }, + wantErr: false, }, { name: "unsupportedOption", diff --git a/internal/pkg/runtime/launcher/oci/mounts_linux.go b/internal/pkg/runtime/launcher/oci/mounts_linux.go index c0502f5f4c..e2a85efb38 100644 --- a/internal/pkg/runtime/launcher/oci/mounts_linux.go +++ b/internal/pkg/runtime/launcher/oci/mounts_linux.go @@ -265,101 +265,61 @@ func (l *Launcher) addSysMount(mounts *[]specs.Mount) error { return nil } -// addHomeMount adds a user home directory as a tmpfs mount, and sets the -// container home directory. We are currently emulating `--compat` / -// `--containall`, so the user must specifically bind in their home directory -// from the host for it to be available. +// addHomeMount adds the user home directory to the container, according to the +// src and dest computed by parseHomeDir from launcher.New. func (l *Launcher) addHomeMount(mounts *[]specs.Mount) error { - // If the $HOME mount is skipped by config or --no-home, we still need to - // handle setting the correct $HOME dir, but just skip adding the mount. - skipMount := false if !l.apptainerConf.MountHome { sylog.Debugf("Skipping mount of $HOME due to apptainer.conf") - skipMount = true + return nil } if l.cfg.NoHome { sylog.Debugf("Skipping mount of $HOME due to --no-home") - skipMount = true + return nil } - // Get the host user's data - pw, err := user.CurrentOriginal() - if err != nil { - return err + if l.homeDest == "" { + return fmt.Errorf("cannot add home mount with empty destination") } - if l.cfg.CustomHome { - // Handle any user request to override the home directory source/dest - homeSlice := strings.Split(l.cfg.HomeDir, ":") - if len(homeSlice) < 1 || len(homeSlice) > 2 { - return fmt.Errorf("home argument has incorrect number of elements: %v", homeSlice) - } - homeSrc := homeSlice[0] - l.cfg.HomeDir = homeSrc - - // User requested more than just a custom home dir; they want to bind-mount a directory in the host to this custom home dir. - // This means the home dir, as viewed from inside the container, is actually the second member of the ":"-separated slice. The first member is actually just the source portion of the requested bind-mount. - if len(homeSlice) > 1 { - homeDest := homeSlice[1] - l.cfg.HomeDir = homeDest + // If l.homeSrc is set, then we are simply bind mounting from the host. + if l.homeSrc != "" { + return addBindMount(mounts, bind.Path{ + Source: l.homeSrc, + Destination: l.homeDest, + }) + } - if skipMount { - return nil - } + // Otherwise we setup a tmpfs, mounted onto l.homeDst. + tmpfsOpt := []string{ + "nosuid", + "relatime", + "mode=755", + fmt.Sprintf("size=%dm", l.apptainerConf.SessiondirMaxSize), + } - // Since the home dir is a bind-mount in this case, we don't have to mount a tmpfs directory for the in-container home dir, and we can just do the bind-mount & return. - return addBindMount(mounts, bind.Path{ - Source: homeSrc, - Destination: homeDest, - }) + // If we aren't using fakeroot, ensure the tmpfs ownership is correct for our real uid/gid. + if !l.cfg.Fakeroot { + uid, err := rootless.Getuid() + if err != nil { + return fmt.Errorf("while fetching uid: %w", err) } - } else { - // If we're running in fake-root mode (and we haven't requested a custom home dir), we do need to create a tmpfs mount for the home dir, but it's a special case (because of its location & permissions), so we handle that here & return. - if l.cfg.Fakeroot { - l.cfg.HomeDir = "/root" - - if skipMount { - return nil - } - - *mounts = append(*mounts, - specs.Mount{ - Destination: "/root", - Type: "tmpfs", - Source: "tmpfs", - Options: []string{ - "nosuid", - "relatime", - "mode=755", - fmt.Sprintf("size=%dm", l.apptainerConf.SessiondirMaxSize), - }, - }) - - return nil + gid, err := rootless.Getgid() + if err != nil { + return fmt.Errorf("while fetching gid: %w", err) } - // No fakeroot and no custom home dir - so the in-container home dir will be named the same as the host user's home dir. (Though note that it'll still be a tmpfs mount! It'll get mounted by the catch-all mount append, below.) - l.cfg.HomeDir = pw.Dir - } - - if skipMount { - return nil + tmpfsOpt = append(tmpfsOpt, + fmt.Sprintf("uid=%d", uid), + fmt.Sprintf("gid=%d", gid), + ) } - // If we've not hit a special case (bind-mounted custom home dir, or fakeroot), then create a tmpfs mount as a home dir in the requested location (whether it's custom or not; by this point, l.cfg.HomeDir will reflect the right value). *mounts = append(*mounts, specs.Mount{ - Destination: l.cfg.HomeDir, + Destination: l.homeDest, Type: "tmpfs", Source: "tmpfs", - Options: []string{ - "nosuid", - "relatime", - "mode=755", - fmt.Sprintf("size=%dm", l.apptainerConf.SessiondirMaxSize), - fmt.Sprintf("uid=%d", pw.UID), - fmt.Sprintf("gid=%d", pw.GID), - }, + Options: tmpfsOpt, }) return nil diff --git a/internal/pkg/runtime/launcher/oci/process_linux.go b/internal/pkg/runtime/launcher/oci/process_linux.go index f9b36b5600..75a7df54db 100644 --- a/internal/pkg/runtime/launcher/oci/process_linux.go +++ b/internal/pkg/runtime/launcher/oci/process_linux.go @@ -53,7 +53,7 @@ func (l *Launcher) getProcess(ctx context.Context, imgSpec imgspecv1.Image, imag // Ensure HOME points to the required home directory, even if it is a custom one, unless the container explicitly specifies its USER, in which case we don't want to touch HOME. if imgSpec.Config.User == "" { - rtEnv["HOME"] = l.cfg.HomeDir + rtEnv["HOME"] = l.homeDest } cwd, err := l.getProcessCwd()