Blob Blame History Raw
From fd50472486365819fe95e164c4c67ecf0c5803b4 Mon Sep 17 00:00:00 2001
From: Cedric Clerget <cedric.clerget@gmail.com>
Date: Fri, 24 Jan 2020 15:52:08 +0100
Subject: [PATCH] Fix a logic error when 'allow setuid = no' with a privileged
 installation was forced root user to always fallback to user namespace. Add
 CAP_SYS_ADMIN check for root user to automatically fallback to user namespace
 if the capability is missing.

---
 cmd/internal/cli/actions_linux.go     | 35 +++++++++++-----
 pkg/util/capabilities/process.go      | 58 +++++++++++++++++++++++++++
 pkg/util/capabilities/process_test.go | 51 +++++++++++++++++++++++
 3 files changed, 134 insertions(+), 10 deletions(-)
 create mode 100644 pkg/util/capabilities/process.go
 create mode 100644 pkg/util/capabilities/process_test.go

diff --git a/cmd/internal/cli/actions_linux.go b/cmd/internal/cli/actions_linux.go
index 8cc823379e..1d68ae9876 100644
--- a/cmd/internal/cli/actions_linux.go
+++ b/cmd/internal/cli/actions_linux.go
@@ -33,9 +33,11 @@ import (
	"github.com/sylabs/singularity/pkg/image/unpacker"
	"github.com/sylabs/singularity/pkg/runtime/engine/config"
	singularityConfig "github.com/sylabs/singularity/pkg/runtime/engine/singularity/config"
+	"github.com/sylabs/singularity/pkg/util/capabilities"
	"github.com/sylabs/singularity/pkg/util/crypt"
	"github.com/sylabs/singularity/pkg/util/gpu"
	"github.com/sylabs/singularity/pkg/util/namespaces"
+	"golang.org/x/sys/unix"
 )
 
 // EnsureRootPriv ensures that a command is executed with root privileges.
@@ -216,23 +218,42 @@ func execStarter(cobraCmd *cobra.Command, image string, args []string, name stri
 		engineConfig.SetImage(abspath)
 	}
 
+	// privileged installation by default
 	useSuid := true
 
 	// singularity was compiled with '--without-suid' option
 	if buildcfg.SINGULARITY_SUID_INSTALL == 0 {
 		useSuid = false
+
+		if !UserNamespace && uid != 0 {
+			sylog.Verbosef("Unprivileged installation: using user namespace")
+			UserNamespace = true
+		}
 	}
 
 	// use non privileged starter binary:
-	// - if we are the root user
-	// - if we are already running inside a user namespace
+	// - if running as root
+	// - if already running inside a user namespace
 	// - if user namespace is requested
-	// - if 'allow setuid = no' is set in singularity.conf
+	// - if running as user and 'allow setuid = no' is set in singularity.conf
 	if uid == 0 || insideUserNs || UserNamespace || !engineConfig.File.AllowSetuid {
 		useSuid = false
-		if buildcfg.SINGULARITY_SUID_INSTALL == 1 && !engineConfig.File.AllowSetuid {
+
+		// fallback to user namespace:
+		// - for non root user with setuid installation and 'allow setuid = no'
+		// - for root user without effective capability CAP_SYS_ADMIN
+		if uid != 0 && buildcfg.SINGULARITY_SUID_INSTALL == 1 && !engineConfig.File.AllowSetuid {
 			sylog.Verbosef("'allow setuid' set to 'no' by configuration, fallback to user namespace")
 			UserNamespace = true
+		} else if uid == 0 && !UserNamespace {
+			caps, err := capabilities.GetProcessEffective()
+			if err != nil {
+				sylog.Fatalf("Could not get process effective capabilities: %s", err)
+			}
+			if caps&uint64(1<<unix.CAP_SYS_ADMIN) == 0 {
+				sylog.Verbosef("Effective capability CAP_SYS_ADMIN is missing, fallback to user namespace")
+				UserNamespace = true
+			}
 		}
 	}
 
@@ -512,12 +533,6 @@ func execStarter(cobraCmd *cobra.Command, image string, args []string, name stri
 	if IpcNamespace {
 		generator.AddOrReplaceLinuxNamespace("ipc", "")
 	}
-	if !UserNamespace && uid != 0 && buildcfg.SINGULARITY_SUID_INSTALL == 0 {
-		sylog.Verbosef("Unprivileged installation: using user namespace")
-		UserNamespace = true
-		useSuid = false
-	}
-
 	if UserNamespace {
 		generator.AddOrReplaceLinuxNamespace("user", "")
 
diff --git a/pkg/util/capabilities/process.go b/pkg/util/capabilities/process.go
new file mode 100644
index 0000000000..d3ab8feebb
--- /dev/null
+++ b/pkg/util/capabilities/process.go
@@ -0,0 +1,58 @@
+// Copyright (c) 2020, Sylabs Inc. All rights reserved.
+// This software is licensed under a 3-clause BSD license. Please consult the
+// LICENSE.md file distributed with the sources of this project regarding your
+// rights to use or distribute this software.
+
+package capabilities
+
+import (
+	"fmt"
+
+	"golang.org/x/sys/unix"
+)
+
+// getProcessCapabilities returns capabilities either effective,
+// permitted or inheritable for the current process.
+func getProcessCapabilities(capType string) (uint64, error) {
+	var caps uint64
+	var data [2]unix.CapUserData
+	var header unix.CapUserHeader
+
+	header.Version = unix.LINUX_CAPABILITY_VERSION_3
+
+	if err := unix.Capget(&header, &data[0]); err != nil {
+		return caps, fmt.Errorf("while getting capability: %s", err)
+	}
+
+	switch capType {
+	case Effective:
+		caps = uint64(data[0].Effective)
+		caps |= uint64(data[1].Effective) << 32
+	case Permitted:
+		caps = uint64(data[0].Permitted)
+		caps |= uint64(data[1].Permitted) << 32
+	case Inheritable:
+		caps = uint64(data[0].Inheritable)
+		caps |= uint64(data[1].Inheritable) << 32
+	}
+
+	return caps, nil
+}
+
+// GetProcessEffective returns effective capabilities for
+// the current process.
+func GetProcessEffective() (uint64, error) {
+	return getProcessCapabilities(Effective)
+}
+
+// GetProcessPermitted returns permitted capabilities for
+// the current process.
+func GetProcessPermitted() (uint64, error) {
+	return getProcessCapabilities(Permitted)
+}
+
+// GetProcessInheritable returns inheritable capabilities for
+// the current process.
+func GetProcessInheritable() (uint64, error) {
+	return getProcessCapabilities(Inheritable)
+}
diff --git a/pkg/util/capabilities/process_test.go b/pkg/util/capabilities/process_test.go
new file mode 100644
index 0000000000..5aa9a7c8e0
--- /dev/null
+++ b/pkg/util/capabilities/process_test.go
@@ -0,0 +1,51 @@
+// Copyright (c) 2020, Sylabs Inc. All rights reserved.
+// This software is licensed under a 3-clause BSD license. Please consult the
+// LICENSE.md file distributed with the sources of this project regarding your
+// rights to use or distribute this software.
+
+package capabilities
+
+import (
+	"runtime"
+	"testing"
+
+	"github.com/sylabs/singularity/internal/pkg/test"
+)
+
+func TestGetProcess(t *testing.T) {
+	test.EnsurePrivilege(t)
+
+	runtime.LockOSThread()
+	defer runtime.UnlockOSThread()
+
+	tests := []struct {
+		name string
+		fn   func() (uint64, error)
+		cap  string
+	}{
+		{
+			name: "effective",
+			fn:   GetProcessEffective,
+			cap:  "CAP_SYS_ADMIN",
+		},
+		{
+			name: "permitted",
+			fn:   GetProcessPermitted,
+		},
+		{
+			name: "inheritable",
+			fn:   GetProcessInheritable,
+		},
+	}
+
+	for _, tt := range tests {
+		caps, err := tt.fn()
+		if err != nil {
+			t.Fatalf("unexpected error while getting process %s capabilities: %s", tt.name, err)
+		}
+		cap := Map[tt.cap]
+		if tt.cap != "" && caps&uint64(1<<cap.Value) == 0 {
+			t.Fatalf("%s capability %s missing", tt.name, tt.cap)
+		}
+	}
+}