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)
+ }
+ }
+}