diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4793091870..1249f0a85f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -357,7 +357,7 @@ jobs: run: | set -e sudo apt-get -q update - sudo DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential libseccomp-dev fakeroot cryptsetup dbus-user-session + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential libseccomp-dev libtalloc-dev libattr1-dev libprotobuf-c-dev fakeroot cryptsetup dbus-user-session sudo DEBIAN_FRONTEND=noninteractive apt-get install -y autoconf automake libtool pkg-config libfuse3-dev zlib1g-dev liblzo2-dev liblz4-dev liblzma-dev libzstd-dev - uses: actions/cache/restore@v4 @@ -414,7 +414,7 @@ jobs: go-version: 1.25.6 - name: Fetch deps - run: sudo apt-get -q update && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential libseccomp-dev cryptsetup dbus-user-session + run: sudo apt-get -q update && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential libseccomp-dev libtalloc-dev libattr1-dev libprotobuf-c-dev cryptsetup dbus-user-session - name: Build and install Apptainer run: | @@ -463,7 +463,7 @@ jobs: run: | set -e sudo apt-get -q update - sudo DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential libseccomp-dev uidmap fakeroot cryptsetup dbus-user-session + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential libseccomp-dev libtalloc-dev libattr1-dev libprotobuf-c-dev uidmap fakeroot cryptsetup dbus-user-session sudo DEBIAN_FRONTEND=noninteractive apt-get install -y autoconf automake libtool pkg-config libfuse3-dev zlib1g-dev liblzo2-dev liblz4-dev liblzma-dev libzstd-dev - name: Download, compile, and install dependent packages diff --git a/.gitignore b/.gitignore index 27cd65c1cf..0fb53768fc 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ apptainer-*.tar.gz e2fsprogs-* fuse-overlayfs-* gocryptfs-* +PRoot-* squashfuse-* squashfs-tools-* *.m4 diff --git a/CHANGELOG.md b/CHANGELOG.md index 2978104e19..7f323c74bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,20 @@ For older changes see the [archived Singularity change log](https://github.com/a `APPTAINER_NOENV` environment variable that can provide a comma-separated list of environment variables to skip importing from the host environment into the container. +- Preserve owner and group information on files in containers downloaded from + OCI registries when building SIF files, even for unprivileged users. + This takes advantage of the fact that the library (umoci) that downloads + containers preserves owner and group information in an extended attribute. + Adds bundled tool `proot` which is modified from the upstream tool by the + rootless-containers project to make the owner and group appear to be in the + ordinary `stat()` information. That tool is now used when invoking + `mksquashfs` to create the filesystem partition in a SIF file. It can + be disabled with the hidden build option `--ignore-proot`. +- When unsquashing an image while running under a root-mapped user + namespace (such as when using fakeroot without subuid mapping), insert + another namespace mapping back to the original user so unsquashfs + doesn't try (and fail) to change the owner and group information on the + unpacked files. - Record image digest metadata (sha256 from `RepoDigests`), for OCI registry images. Also add the image name (ref) of the image from "docker", with registry and tag. This is useful for traceability, when using `docker.io` or a tag like `latest`. @@ -201,7 +215,7 @@ Changes since 1.3.6 available and to ensure that all compression types are available. This includes the programs `mksquashfs` and `unsquashfs`. - Statistics are now normally available for instances that are - started by non-root users on cgroups v2 systems. + started by non-root users on cgroups v2 systems. The instance will be started in the current cgroup. Information about configuration issues that prevent collection of statistics are displayed as INFO messages by default. diff --git a/INSTALL.md b/INSTALL.md index 12edeb2a73..4f6e74f8e2 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -20,6 +20,9 @@ sudo apt-get update sudo apt-get install -y \ build-essential \ libseccomp-dev \ + libtalloc-dev \ + libattr1-dev \ + libprotobuf-c-dev \ uidmap \ fakeroot \ cryptsetup \ @@ -39,11 +42,16 @@ On RHEL or its derivatives or Fedora: ```sh # Install basic tools for compiling sudo dnf groupinstall -y 'Development Tools' -# Ensure EPEL repository is available +# Ensure EPEL repository is available (skip on Fedora) sudo dnf install -y epel-release +# Enable the CodeReady Builder repository (skip on Fedora) +sudo crb enable # Install RPM packages for dependencies sudo dnf install -y \ libseccomp-devel \ + libtalloc-devel \ + libattr-devel \ + protobuf-c-devel \ fakeroot \ cryptsetup \ wget git @@ -62,6 +70,9 @@ On SLE/openSUSE # Install RPM packages for dependencies sudo zypper install -y \ libseccomp-devel \ + libtalloc-devel \ + libattr-devel \ + libprotobuf-c-devel \ libuuid-devel \ openssl-devel \ fakeroot \ diff --git a/cmd/internal/cli/build.go b/cmd/internal/cli/build.go index 60dd2574b4..6270822a0c 100644 --- a/cmd/internal/cli/build.go +++ b/cmd/internal/cli/build.go @@ -55,6 +55,7 @@ var buildArgs struct { userns bool // Enable user namespaces ignoreSubuid bool // Ignore /etc/subuid entries (hidden) ignoreFakerootCmd bool // Ignore fakeroot command (hidden) + ignoreProot bool // Ignore proot command (hidden) ignoreUserns bool // Ignore user namespace(hidden) remote bool // Remote flag(hidden, only for helpful error message) buildVarArgs []string // Variables passed to build procedure. @@ -313,6 +314,17 @@ var buildIgnoreFakerootCommand = cmdline.Flag{ Hidden: true, } +// --ignore-proot +var buildIgnoreProot = cmdline.Flag{ + ID: "buildIgnoreProotFlag", + Value: &buildArgs.ignoreProot, + DefaultValue: false, + Name: "ignore-proot", + Usage: "ignore proot", + EnvKeys: []string{"IGNORE_PROOT"}, + Hidden: true, +} + // --ignore-userns var buildIgnoreUsernsFlag = cmdline.Flag{ ID: "buildIgnoreUsernsFlag", @@ -402,6 +414,7 @@ func init() { cmdManager.RegisterFlagForCmd(&buildUsernsFlag, buildCmd) cmdManager.RegisterFlagForCmd(&buildIgnoreSubuidFlag, buildCmd) cmdManager.RegisterFlagForCmd(&buildIgnoreFakerootCommand, buildCmd) + cmdManager.RegisterFlagForCmd(&buildIgnoreProot, buildCmd) cmdManager.RegisterFlagForCmd(&buildIgnoreUsernsFlag, buildCmd) cmdManager.RegisterFlagForCmd(&buildRemoteFlag, buildCmd) diff --git a/cmd/internal/cli/build_linux.go b/cmd/internal/cli/build_linux.go index 8f95cac0aa..6921c9d3e5 100644 --- a/cmd/internal/cli/build_linux.go +++ b/cmd/internal/cli/build_linux.go @@ -203,6 +203,10 @@ func runBuild(cmd *cobra.Command, args []string) { } } + if buildArgs.ignoreProot { + os.Setenv("APPTAINER_IGNORE_PROOT", "1") + } + if buildArgs.nvidia { os.Setenv("APPTAINER_NV", "1") } diff --git a/dist/debian/control b/dist/debian/control index 7264f26095..fedc8832d3 100644 --- a/dist/debian/control +++ b/dist/debian/control @@ -13,6 +13,9 @@ Build-Depends: uuid-dev, devscripts, libseccomp-dev, + libtalloc-dev, + libattr1-dev, + libprotobuf-c-dev, cryptsetup, golang-go (>= 2:1.13~~), autoconf, diff --git a/dist/rpm/apptainer.spec.in b/dist/rpm/apptainer.spec.in index 881c74cf92..502409a9dc 100644 --- a/dist/rpm/apptainer.spec.in +++ b/dist/rpm/apptainer.spec.in @@ -34,6 +34,7 @@ %global e2fsprogs_version 1.47.3 %global fuse_overlayfs_version 1.16 %global squashfs_tools_version 4.7.5 +%global PRoot_version 5.4.0-rootless.2 # The last singularity version number in EPEL/Fedora %global last_singularity_version 3.8.7-3 @@ -71,10 +72,15 @@ Patch122: e2fsprogs-250.patch # URL: https://github.com/tytso/e2fsprogs/pull/251.patch Patch123: e2fsprogs-251.patch %endif +%if "%{?fuse_overlayfs_version}" != "" Source13: https://github.com/containers/fuse-overlayfs/archive/v%{fuse_overlayfs_version}/fuse-overlayfs-%{fuse_overlayfs_version}.tar.gz +%endif %if "%{?squashfs_tools_version}" != "" Source14: https://github.com/plougher/squashfs-tools/archive/%{squashfs_tools_version}/squashfs-tools-%{squashfs_tools_version}.tar.gz %endif +%if "%{?PRoot_version}" != "" +Source15: https://github.com/rootless-containers/PRoot/archive/v%{PRoot_version}/PRoot-%{PRoot_version}.tar.gz +%endif # This Conflicts is in case someone tries to install the main apptainer # package when an old singularity package is installed. An Obsoletes is on @@ -97,12 +103,25 @@ Obsoletes: singularity-runtime < 3.0 Provides: sif-runtime Conflicts: sif-runtime +%if "%{?gocryptfs_version}" != "" Provides: bundled(gocryptfs) = %{gocryptfs_version} +%endif +%if "%{?squashfuse_version}" != "" Provides: bundled(squashfuse) = %{squashfuse_version} +%endif +%if "%{?e2fsprogs_version}" != "" Provides: bundled(e2fsprogs) = %{e2fsprogs_version} Provides: bundled(fuse2fs) = %{e2fsprogs_version} +%endif +%if "%{?fuse_overlayfs_version}" != "" Provides: bundled(fuse-overlayfs) = %{fuse_overlayfs_version} +%endif +%if "%{?squashfs_tools_version}" != "" Provides: bundled(squashfs-tools) = %{squashfs_tools_version} +%endif +%if "%{?PRoot_version}" != "" +Provides: bundled(PRoot) = %{PRoot_version} +%endif @BUNDLED_PROVIDES@ %if "%{_target_vendor}" == "suse" @@ -120,6 +139,9 @@ BuildRequires: git BuildRequires: gcc BuildRequires: make BuildRequires: libseccomp-devel +BuildRequires: libtalloc-devel +BuildRequires: libattr-devel +BuildRequires: protobuf-c BuildRequires: cryptsetup BuildRequires: fuse3-devel %if ("%{?squashfuse_version}" != "") || ("%{e2fsprogs_version}" != "") || ("%{fuse_overlayfs_version}" != "") || ("%{?squashfs_tools_version}" != "") @@ -224,6 +246,10 @@ install -m 755 squashfs-tools-%{squashfs_tools_version}/squashfs-tools/mksquashf install -m 755 squashfs-tools-%{squashfs_tools_version}/squashfs-tools/unsquashfs %{buildroot}%{_libexecdir}/%{name}/bin/unsquashfs %endif +%if "%{?PRoot_version}" != "" +install -m 755 PRoot-%{PRoot_version}/src/proot %{buildroot}%{_libexecdir}/%{name}/bin/proot +%endif + %post # $1 in %%posttrans cannot distinguish between fresh installs and upgrades, # so check it here and create a file to pass the knowledge to that step @@ -295,6 +321,9 @@ fi %{_libexecdir}/%{name}/bin/mksquashfs %{_libexecdir}/%{name}/bin/unsquashfs %endif +%if "%{?PRoot_version}" != "" +%{_libexecdir}/%{name}/bin/proot +%endif %{_libexecdir}/%{name}/cni %{_libexecdir}/%{name}/lib %dir %{_sysconfdir}/%{name} diff --git a/e2e/imgbuild/imgbuild.go b/e2e/imgbuild/imgbuild.go index 3e71653686..49ff826dde 100644 --- a/e2e/imgbuild/imgbuild.go +++ b/e2e/imgbuild/imgbuild.go @@ -237,11 +237,11 @@ func (c imgBuildTests) nonRootBuild(t *testing.T) { args: []string{"--sandbox"}, }, { - name: "library sif", + name: "oras sif", buildSpec: "oras://ghcr.io/apptainer/busybox:1.31.1", }, { - name: "library sif sandbox", + name: "oras sif sandbox", buildSpec: "oras://ghcr.io/apptainer/busybox:1.31.1", args: []string{"--sandbox"}, }, @@ -1991,6 +1991,7 @@ echo 'export LEGACY_TEST_ENV=legacy-value' >> $SINGULARITY_ENVIRONMENT ) } +// build images under all the different fakeroot modes func (c *imgBuildTests) testContainerBuildUnderFakerootModes(t *testing.T) { e2e.EnsureDebianImage(t, c.env) @@ -2003,23 +2004,15 @@ func (c *imgBuildTests) testContainerBuildUnderFakerootModes(t *testing.T) { // Make the DebianImagePath available for Bootstrap: localimage sif := c.env.DebianImagePath - basesif := filepath.Base(sif) - err := os.Symlink(sif, basesif) - if err != nil { - t.Fatalf("while symlinking %s to %s: %v", sif, basesif, err) - } - t.Cleanup(func() { - if !t.Failed() { - os.Remove(basesif) - } - }) + + args := []string{"--force", "--build-arg", "FROMSIF=" + sif} // running under the mode 1, 1a (--with-suid) (https://apptainer.org/docs/user/main/fakeroot.html) c.env.RunApptainer( t, e2e.WithProfile(e2e.UserProfile), e2e.WithCommand("build"), - e2e.WithArgs("--force", fmt.Sprintf("%s/openssh-mode1a.sif", tmpDir), "testdata/unprivileged_build.def"), + e2e.WithArgs(append(args, fmt.Sprintf("%s/openssh-mode1a.sif", tmpDir), "testdata/unprivileged_build.def")...), e2e.ExpectExit(0), ) @@ -2028,7 +2021,7 @@ func (c *imgBuildTests) testContainerBuildUnderFakerootModes(t *testing.T) { t, e2e.WithProfile(e2e.UserProfile), e2e.WithCommand("build"), - e2e.WithArgs("--force", "--userns", fmt.Sprintf("%s/openssh-mode1b.sif", tmpDir), "testdata/unprivileged_build.def"), + e2e.WithArgs(append(args, "--userns", fmt.Sprintf("%s/openssh-mode1b.sif", tmpDir), "testdata/unprivileged_build.def")...), e2e.ExpectExit(0), ) @@ -2037,7 +2030,7 @@ func (c *imgBuildTests) testContainerBuildUnderFakerootModes(t *testing.T) { t, e2e.WithProfile(e2e.UserProfile), e2e.WithCommand("build"), - e2e.WithArgs("--force", "--userns", "--ignore-subuid", "--ignore-fakeroot-command", fmt.Sprintf("%s/openssh-mode2a.sif", tmpDir), "testdata/unprivileged_build.def"), + e2e.WithArgs(append(args, "--userns", "--ignore-subuid", "--ignore-fakeroot-command", fmt.Sprintf("%s/openssh-mode2a.sif", tmpDir), "testdata/unprivileged_build.def")...), e2e.ExpectExit(255), // because chown will fail ) @@ -2046,7 +2039,7 @@ func (c *imgBuildTests) testContainerBuildUnderFakerootModes(t *testing.T) { t, e2e.WithProfile(e2e.UserProfile), e2e.WithCommand("build"), - e2e.WithArgs("--force", "--userns", "--ignore-subuid", "--ignore-fakeroot-command", fmt.Sprintf("%s/openssh-mode2b.sif", tmpDir), "testdata/unprivileged_build_2.def"), + e2e.WithArgs(append(args, "--userns", "--ignore-subuid", "--ignore-fakeroot-command", fmt.Sprintf("%s/openssh-mode2b.sif", tmpDir), "testdata/unprivileged_build_2.def")...), e2e.ExpectExit(0), ) @@ -2055,7 +2048,7 @@ func (c *imgBuildTests) testContainerBuildUnderFakerootModes(t *testing.T) { t, e2e.WithProfile(e2e.UserProfile), e2e.WithCommand("build"), - e2e.WithArgs("--force", "--userns", "--ignore-subuid", fmt.Sprintf("%s/openssh-mode3.sif", tmpDir), "testdata/unprivileged_build.def"), + e2e.WithArgs(append(args, "--userns", "--ignore-subuid", fmt.Sprintf("%s/openssh-mode3.sif", tmpDir), "testdata/unprivileged_build.def")...), e2e.ExpectExit(0), ) @@ -2064,11 +2057,71 @@ func (c *imgBuildTests) testContainerBuildUnderFakerootModes(t *testing.T) { t, e2e.WithProfile(e2e.UserProfile), e2e.WithCommand("build"), - e2e.WithArgs("--force", "--ignore-userns", "--ignore-subuid", fmt.Sprintf("%s/openssh-mode4.sif", tmpDir), "testdata/unprivileged_build_4.def"), + e2e.WithArgs(append(args, "--ignore-userns", "--ignore-subuid", fmt.Sprintf("%s/openssh-mode4.sif", tmpDir), "testdata/unprivileged_build_4.def")...), e2e.ExpectExit(0), ) } +// check file ownership after various build types +func (c imgBuildTests) checkBuildFileOwnership(t *testing.T) { + e2e.EnsureDebianImage(t, c.env) + + tmpDir, cleanup := e2e.MakeTempDir(t, c.env.TestDir, "check-build-file-owners-", "") + t.Cleanup(func() { + if !t.Failed() { + cleanup(t) + } + }) + + // Make the DebianImagePath available for Bootstrap: localimage + sif := c.env.DebianImagePath + + profiles := []e2e.Profile{ + e2e.RootProfile, + e2e.UserProfile, + e2e.UserNamespaceProfile, + } + + flags := [][]string{ + {}, + {"--ignore-subuid"}, + {"--ignore-subuid", "--ignore-fakeroot-command"}, + } + + outsif := filepath.Join(tmpDir, "out.sif") + for _, p := range profiles { + for _, f := range flags { + args := append(f, + "--force", + "--build-arg", + "FROMSIF="+sif, + outsif, + "testdata/unprivileged_build_2.def") + c.env.RunApptainer( + t, + e2e.WithProfile(p), + e2e.WithCommand("build"), + e2e.WithArgs(args...), + e2e.ExpectExit(0), + ) + c.env.RunApptainer( + t, + e2e.WithProfile(e2e.UserNamespaceProfile), + e2e.WithCommand("exec"), + e2e.WithArgs("--fakeroot", + outsif, + "stat", + "-c", + "%G", + "/etc/shadow"), + e2e.ExpectExit(0, + e2e.ExpectOutput(e2e.ContainMatch, "shadow"), + ), + ) + } + } +} + func (c *imgBuildTests) testSIFHeaderAndExecute(t *testing.T) { tmpDir, cleanup := e2e.MakeTempDir(t, c.env.TestDir, "test-SIF-header-fields-", "") t.Cleanup(func() { @@ -2486,7 +2539,8 @@ func E2ETests(env e2e.TestEnv) testhelper.Tests { "customShebang": c.buildCustomShebang, // build image with custom #! in %test and %runscript "test with writable tmpfs": c.testWritableTmpfs, // build image, using writable tmpfs in the test step "test build system environment": c.testBuildEnvironmentVariables, // build image with build system environment variables set in definition - "test build under fakeroot modes": c.testContainerBuildUnderFakerootModes, // build image under different fakeroot modes + "test build under fakeroot modes": c.testContainerBuildUnderFakerootModes, // build image under all the different fakeroot modes + "check file ownership after build": c.checkBuildFileOwnership, // check file ownership after various build types "issue 2347": c.issue2347, // https://github.com/apptainer/apptainer/issues/2347 "issue 3848": c.issue3848, // https://github.com/apptainer/singularity/issues/3848 "issue 4203": c.issue4203, // https://github.com/apptainer/singularity/issues/4203 diff --git a/e2e/run/run.go b/e2e/run/run.go index 97e7959f29..4b1b820488 100644 --- a/e2e/run/run.go +++ b/e2e/run/run.go @@ -545,6 +545,15 @@ func (c ctx) testAddPackageWithFakerootAndTmpfs(t *testing.T) { if err != nil { t.Fatalf("could not create overlaymode3 folder inside tempdir: %s", tempDir) } + singleOwnerSif := filepath.Join(tempDir, "singleowner.sif") + // first make a copy of the container that is all one owner + c.env.RunApptainer( + t, + e2e.WithProfile(e2e.UserProfile), + e2e.WithCommand("build"), + e2e.WithArgs("--ignore-proot", "--force", singleOwnerSif, sif), + e2e.ExpectExit(0), + ) c.env.RunApptainer( t, e2e.WithProfile(e2e.FakerootProfile), @@ -553,8 +562,8 @@ func (c ctx) testAddPackageWithFakerootAndTmpfs(t *testing.T) { // gives postinstall errors about a missing 'diff' command when // using it on ubuntu 22.04, so use overlay instead. // See https://github.com/apptainer/apptainer/issues/1124 - e2e.WithArgs("--userns", "--overlay", overlaydir, "--ignore-subuid", sif, "sh", "-c", "apt-get update && apt-get install -y openssh-client"), - // e2e.WithArgs("--userns", "--writable-tmpfs", "--ignore-subuid", sif, "sh", "-c", "apt-get update && apt-get install -y openssh-client"), + e2e.WithArgs("--userns", "--overlay", overlaydir, "--ignore-subuid", singleOwnerSif, "sh", "-c", "apt-get update && apt-get install -y openssh-client"), + // e2e.WithArgs("--userns", "--writable-tmpfs", "--ignore-subuid", singleOwnerSif, "sh", "-c", "apt-get update && apt-get install -y openssh-client"), e2e.ExpectExit(0), ) diff --git a/e2e/testdata/unprivileged_build.def b/e2e/testdata/unprivileged_build.def index 5261d50fca..f1d561daae 100644 --- a/e2e/testdata/unprivileged_build.def +++ b/e2e/testdata/unprivileged_build.def @@ -1,5 +1,5 @@ Bootstrap: localimage -From: test-debian.sif +From: {{ FROMSIF }} %post apt-get update diff --git a/e2e/testdata/unprivileged_build_2.def b/e2e/testdata/unprivileged_build_2.def index c00e4452fb..bd13b2098d 100644 --- a/e2e/testdata/unprivileged_build_2.def +++ b/e2e/testdata/unprivileged_build_2.def @@ -1,5 +1,5 @@ Bootstrap: localimage -From: test-debian.sif +From: {{ FROMSIF }} # In mode 2 there's no fakeroot command so cannot install any # packages on Debian, since apt-get always does privileged operations diff --git a/e2e/testdata/unprivileged_build_4.def b/e2e/testdata/unprivileged_build_4.def index 3a2339be66..d688a6c12c 100644 --- a/e2e/testdata/unprivileged_build_4.def +++ b/e2e/testdata/unprivileged_build_4.def @@ -1,5 +1,5 @@ Bootstrap: localimage -From: test-debian.sif +From: {{ FROMSIF }} # mode 4 can't handle openssh-client, so install something simple instead %post diff --git a/internal/pkg/build/assemblers/sif.go b/internal/pkg/build/assemblers/sif.go index 927e8b2dc8..190878da50 100644 --- a/internal/pkg/build/assemblers/sif.go +++ b/internal/pkg/build/assemblers/sif.go @@ -186,11 +186,6 @@ func (a *SIFAssembler) Assemble(b *types.Bundle, path string) error { defer os.Remove(fsPath) flags := []string{"-noappend"} - // build squashfs with all-root flag when building as a user - if syscall.Getuid() != 0 { - flags = append(flags, "-all-root") - } - if a.MksquashfsMem != "" { flags = append(flags, "-mem", a.MksquashfsMem) } diff --git a/internal/pkg/image/packer/squashfs.go b/internal/pkg/image/packer/squashfs.go index 8fb63b74df..b1b9f4b41a 100644 --- a/internal/pkg/image/packer/squashfs.go +++ b/internal/pkg/image/packer/squashfs.go @@ -19,6 +19,7 @@ import ( "github.com/apptainer/apptainer/internal/pkg/client" "github.com/apptainer/apptainer/internal/pkg/util/bin" "github.com/apptainer/apptainer/pkg/sylog" + "github.com/apptainer/apptainer/pkg/util/namespaces" "github.com/blang/semver/v4" ) @@ -71,10 +72,27 @@ func (s Squashfs) create(files []string, dest string, opts []string) error { args = append(args, "-quiet", "-percentage") } + prog := s.MksquashfsPath + if namespaces.IsUnprivileged() { + // building as unprivileged user, make the files appear as root + ignore_proot := os.Getenv("APPTAINER_IGNORE_PROOT") + proot, err := bin.FindBin("proot") + if ignore_proot == "" && err == nil { + // Insert proot around mksquashfs to take advantage of + // file owner and group information stored by umoci in + // a "rootlesscontainers" extended attribute. + // https://github.com/apptainer/apptainer/issues/2830 + args = append([]string{"-S", "/", prog}, args...) + prog = proot + } else { + args = append(args, "-all-root") + } + } + // mksquashfs -reproducible automatically clamps everything to SOURCE_DATE_EPOCH // (note: -reproducible is the default, there is also a -not-reproducible option) - sylog.Verbosef("Executing %s %s", s.MksquashfsPath, strings.Join(args, " ")) - cmd := exec.Command(s.MksquashfsPath, args...) + sylog.Verbosef("Executing %s %s", prog, strings.Join(args, " ")) + cmd := exec.Command(prog, args...) if sylog.GetLevel() >= int(sylog.VerboseLevel) { cmd.Stdout = os.Stdout } else if hasPercentage { diff --git a/internal/pkg/image/unpacker/squashfs_apptainer.go b/internal/pkg/image/unpacker/squashfs_apptainer.go index 8418b03c03..bd05e41426 100644 --- a/internal/pkg/image/unpacker/squashfs_apptainer.go +++ b/internal/pkg/image/unpacker/squashfs_apptainer.go @@ -22,9 +22,11 @@ import ( "os/exec" "path/filepath" "strings" + "syscall" "github.com/apptainer/apptainer/internal/pkg/buildcfg" "github.com/apptainer/apptainer/pkg/sylog" + "github.com/apptainer/apptainer/pkg/util/namespaces" ) func init() { @@ -281,6 +283,12 @@ func unsquashfsSandboxCmd(unsquashfs string, dest string, filename string, filte "-B", fmt.Sprintf("%s:%s", tmpdir, rootfsImageDir), } + isOnlyRootMapped := namespaces.IsOnlyRootMapped() + if isOnlyRootMapped { + // suid mode doesn't work in a root-mapped user namespace + args = append(args, "--userns") + } + if filename != stdinFile { filename = filepath.Join(rootfsImageDir, filepath.Base(filename)) } @@ -370,5 +378,38 @@ func unsquashfsSandboxCmd(unsquashfs string, dest string, filename string, filte fmt.Sprintf("APPTAINER_MESSAGELEVEL=%s", os.Getenv("APPTAINER_MESSAGELEVEL")), } + // If running in a root-mapped user namespace, switch to unprivileged + // because otherwise unsquashfs thinks it is running as root and + // tries to chown files that are not owned and grouped by root + if isOnlyRootMapped { + uid, err := namespaces.HostUID() + gid, err2 := namespaces.HostGID() + if err != nil || err2 != nil { + if err == nil { + err = err2 + } + sylog.Debugf("Skipping adding namespace because %v", err) + } else { + sylog.Debugf("Using namespace with uid %d, gid %d", uid, gid) + cmd.SysProcAttr = &syscall.SysProcAttr{ + Cloneflags: syscall.CLONE_NEWUSER, + UidMappings: []syscall.SysProcIDMap{ + { + HostID: 0, + ContainerID: int(uid), + Size: 1, + }, + }, + GidMappings: []syscall.SysProcIDMap{ + { + HostID: 0, + ContainerID: int(gid), + Size: 1, + }, + }, + } + } + } + return cmd, nil } diff --git a/internal/pkg/util/bin/bin.go b/internal/pkg/util/bin/bin.go index 180875bcbc..378203345d 100644 --- a/internal/pkg/util/bin/bin.go +++ b/internal/pkg/util/bin/bin.go @@ -47,7 +47,9 @@ func FindBin(name string) (path string, err error) { } return findOnPath("ldconfig", false) // All other executables - // We will always search the user's PATH first for these + // We will search ${prefix}/libexec/apptainer/bin first for these + // followed by the user's PATH, ahead of the system directories + // by default case "curl", "debootstrap", "dnf", @@ -61,6 +63,7 @@ func FindBin(name string) (path string, err error) { "newuidmap", "nvidia-container-cli", "pacstrap", + "proot", "rpm", "rpmkeys", "squashfuse", diff --git a/pkg/util/namespaces/user_linux.go b/pkg/util/namespaces/user_linux.go index cd46ca77f8..2ea9e348f7 100644 --- a/pkg/util/namespaces/user_linux.go +++ b/pkg/util/namespaces/user_linux.go @@ -85,10 +85,6 @@ func HostGID() (uint32, error) { } func getHostID(typ string, currentID uint32) (uint32, error) { - if currentID != 0 { - return currentID, nil - } - idMap := fmt.Sprintf("/proc/self/%s_map", typ) f, err := os.Open(idMap) @@ -105,30 +101,29 @@ func getHostID(typ string, currentID uint32) (uint32, error) { for scanner.Scan() { fields := strings.Fields(scanner.Text()) - size, err := strconv.ParseUint(fields[2], 10, 32) + parsedID, err := strconv.ParseUint(fields[2], 10, 32) if err != nil { return 0, fmt.Errorf("failed to convert size field %s: %s", fields[2], err) } + size := uint32(parsedID) // not in a user namespace, use current ID - if uint32(size) == ^uint32(0) { + if size == ^uint32(0) { break } // we are inside a user namespace - parsedID, err := strconv.ParseUint(fields[0], 10, 32) + parsedID, err = strconv.ParseUint(fields[0], 10, 32) if err != nil { return 0, fmt.Errorf("failed to convert container %s field %s: %s", typ, fields[0], err) } containerID := uint32(parsedID) - // we can safely assume that a user won't have two - // consequent ID and we look if current ID match - // a 1:1 user mapping - if size == 1 && currentID == containerID { - id, err := strconv.ParseUint(fields[1], 10, 32) - if err != nil { - return 0, fmt.Errorf("failed to convert host %v field %s: %s", typ, fields[1], err) - } - return uint32(id), nil + parsedID, err = strconv.ParseUint(fields[1], 10, 32) + if err != nil { + return 0, fmt.Errorf("failed to convert container %s field %s: %s", typ, fields[1], err) + } + hostID := uint32(parsedID) + if currentID >= containerID && currentID < containerID+size { + return hostID + currentID - containerID, nil } } @@ -149,3 +144,21 @@ func IsUnprivileged() bool { } return uid != 0 } + +// IsOnlyRootMapped() returns true if running in a root-mapped user +// namespace, without any other user ids mapped +func IsOnlyRootMapped() bool { + if os.Geteuid() != 0 { + return false + } + // are running as root + uid, err := HostUID() + if err != nil || uid == 0 { + return false + } + // root is mapped to non-root + // but make sure id 1 is not also mapped elsewhere + // (as it would be with subuid-based fakeroot) + uid, err = getHostID("uid", 1) + return err != nil || uid == 1 +} diff --git a/scripts/ci-deb-build-test b/scripts/ci-deb-build-test index d4c2671119..65a1336de1 100755 --- a/scripts/ci-deb-build-test +++ b/scripts/ci-deb-build-test @@ -16,6 +16,9 @@ export DEBIAN_FRONTEND=noninteractive apt-get install -y \ build-essential \ libseccomp-dev \ + libtalloc-dev \ + libattr1-dev \ + libprotobuf-c-dev \ uidmap \ fakeroot \ cryptsetup \ diff --git a/scripts/ci-rpm-build-test b/scripts/ci-rpm-build-test index c586cfb614..272bc29479 100755 --- a/scripts/ci-rpm-build-test +++ b/scripts/ci-rpm-build-test @@ -9,8 +9,8 @@ # install dependencies if [[ $OS_TYPE == *suse* ]]; then zypper install -y --allow-downgrade \ - libseccomp-devel libuuid-devel openssl-devel \ - fakeroot cryptsetup sysuser-tools \ + libseccomp-devel libtalloc-devel libattr-devel libuuid-devel openssl-devel \ + libprotobuf-c-devel fakeroot cryptsetup sysuser-tools \ diffutils wget which git zypper install -y --replacefiles --allow-downgrade -t pattern devel_basis if [[ $OS_TYPE == *tumbleweed* ]]; then @@ -24,8 +24,10 @@ else dnf groupinstall -y 'Development Tools' if [ $OS_TYPE != fedora ]; then dnf install -y epel-release + crb enable fi - dnf install -y libseccomp-devel fakeroot cryptsetup wget git + dnf install -y libseccomp-devel libtalloc-devel libattr-devel \ + protobuf-c-devel fakeroot cryptsetup wget git dnf --enablerepo=devel install -y shadow-utils-subid-devel dnf install -y golang dnf install -y fuse3-devel lzo-devel lz4-devel diff --git a/scripts/clean-dependencies b/scripts/clean-dependencies index bd51f9771a..6262b5ada2 100755 --- a/scripts/clean-dependencies +++ b/scripts/clean-dependencies @@ -8,6 +8,6 @@ # FUSE-based packages. set -ex -for PKG in squashfs-tools squashfuse e2fsprogs fuse-overlayfs gocryptfs; do +for PKG in squashfs-tools squashfuse e2fsprogs fuse-overlayfs gocryptfs PRoot; do rm -rf $PKG-* done diff --git a/scripts/compile-dependencies b/scripts/compile-dependencies index a5903cd963..1c06f7c9a3 100755 --- a/scripts/compile-dependencies +++ b/scripts/compile-dependencies @@ -18,7 +18,7 @@ if [ -n "$1" ]; then fi set -ex -for PKG in squashfs-tools squashfuse e2fsprogs fuse-overlayfs gocryptfs; do +for PKG in squashfs-tools squashfuse e2fsprogs fuse-overlayfs gocryptfs PRoot; do TGZ="$(echo $SRC/${PKG}-*.tar.gz)" if [ ! -f "$TGZ" ]; then echo "$PKG-*.tar.gz not found in $SRC" >&2 @@ -66,6 +66,9 @@ for PKG in squashfs-tools squashfuse e2fsprogs fuse-overlayfs gocryptfs; do -buildvcs=false -ldflags="-X main.GitVersion=v$VER \ -B 0x`head -c20 /dev/urandom|od -An -tx1|tr -d ' \n'`" ;; + PRoot) + make -C src proot + ;; *) echo "unrecognized package $PKG" >&2 exit 1 diff --git a/scripts/install-dependencies b/scripts/install-dependencies index 5811954824..72f0a5d36d 100755 --- a/scripts/install-dependencies +++ b/scripts/install-dependencies @@ -33,7 +33,7 @@ if [ ! -w "$DIR" ]; then fi set -ex -for CMD in squashfs-tools-*/squashfs-tools/{mksquashfs,unsquashfs} squashfuse-*/squashfuse_ll e2fsprogs-*/fuse2fs fuse-overlayfs-*/fuse-overlayfs gocryptfs-*/gocryptfs; do +for CMD in squashfs-tools-*/squashfs-tools/{mksquashfs,unsquashfs} squashfuse-*/squashfuse_ll e2fsprogs-*/fuse2fs fuse-overlayfs-*/fuse-overlayfs gocryptfs-*/gocryptfs PRoot-*/src/proot; do if [ ! -f "$CMD" ]; then echo "$CMD not found" >&2 exit 1 diff --git a/tools/install-unprivileged.sh b/tools/install-unprivileged.sh index 62db45ae81..28b713cba1 100755 --- a/tools/install-unprivileged.sh +++ b/tools/install-unprivileged.sh @@ -346,30 +346,31 @@ if [ "$DIST" = el7 ]; then elif [ "$DIST" = el8 ]; then # lz4-libs are installed by default on rhel but needed when # these binaries are used on suse - OSUTILS="$OSUTILS lzo squashfs-tools lz4-libs libzstd fuse3-libs libsepol bzip2-libs audit-libs libcap-ng libattr libacl pcre2 libxcrypt libselinux libsemanage shadow-utils-subid" + OSUTILS="$OSUTILS lzo squashfs-tools lz4-libs libzstd fuse3-libs libsepol bzip2-libs audit-libs libcap-ng libattr libtalloc libacl pcre2 libxcrypt libselinux libsemanage shadow-utils-subid" if $NEEDSFUSE2FS; then OSUTILS="$OSUTILS fuse-libs e2fsprogs-libs e2fsprogs" fi + EXTRASUTILS="$EXTRASUTILS protobuf-c" elif [[ "$DIST" == suse20* ]] || [ "$DIST" = "opensuse-tumbleweed" ]; then FUSELIBSO=3 if [ "$DIST" = "opensuse-tumbleweed" ]; then FUSELIBSO=4 fi - OSUTILS="$OSUTILS squashfs liblzo2-2 liblz4-1 libzstd1 libfuse3-$FUSELIBSO $EXTRASUTILS $EPELUTILS" + OSUTILS="$OSUTILS squashfs libtalloc2 libprotobuf-c1 liblzo2-2 liblz4-1 libzstd1 libfuse3-$FUSELIBSO $EXTRASUTILS $EPELUTILS" if $NEEDSFUSE2FS; then OSUTILS="$OSUTILS fuse2fs" fi EXTRASUTILS="" EPELUTILS="" elif [ "$DIST" = "suse15" ] || [ "$DIST" = "opensuse-leap" ]; then - OSUTILS="$OSUTILS squashfs liblzo2-2 liblz4-1 libzstd1 libfuse3-3 $EPELUTILS" + OSUTILS="$OSUTILS squashfs libtalloc2 libprotobuf-c1 liblzo2-2 liblz4-1 libzstd1 libfuse3-3 $EPELUTILS" if $NEEDSFUSE2FS; then EXTRASUTILS="$EXTRASUTILS libext2fs2 libfuse2 fuse2fs" fi EPELUTILS="" else # el9+ & fc* - OSUTILS="$OSUTILS lzo squashfs-tools lz4-libs libzstd libsepol bzip2-libs audit-libs libcap-ng libattr libacl pcre2 libxcrypt libselinux libsemanage shadow-utils-subid" + OSUTILS="$OSUTILS lzo squashfs-tools lz4-libs libzstd libsepol bzip2-libs audit-libs libcap-ng libattr libtalloc protobuf-c libacl pcre2 libxcrypt libselinux libsemanage shadow-utils-subid" if $NEEDSFUSE2FS; then OSUTILS="$OSUTILS fuse-libs e2fsprogs-libs e2fsprogs" fi