Files
netbird/client/ssh/server/executor_drop_privileged_linux_test.go
T

103 lines
2.9 KiB
Go

//go:build linux && privileged
package server
import (
"bufio"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const envDropPrivilegesChild = "NB_TEST_DROP_PRIVILEGES_CHILD"
const (
dropTargetUID = 65534
dropTargetGID = 65534
dropExtraGID = 65533
)
// TestDropPrivilegesAllThreads drops privileges in a child process, because the drop
// cannot be undone, and checks that every OS thread of that process carries the target
// credentials. A drop applied to the calling thread only would leave the others as root.
func TestDropPrivilegesAllThreads(t *testing.T) {
if os.Getenv(envDropPrivilegesChild) == "1" {
dropPrivilegesChild(t)
return
}
if os.Geteuid() != 0 {
t.Skip("requires root")
}
cmd := exec.Command(os.Args[0], "-test.run=^TestDropPrivilegesAllThreads$", "-test.v")
cmd.Env = append(os.Environ(), envDropPrivilegesChild+"=1")
out, err := cmd.CombinedOutput()
t.Logf("child output:\n%s", out)
require.NoError(t, err, "child must drop privileges on all threads")
assert.Contains(t, string(out), "--- PASS: TestDropPrivilegesAllThreads", "child test must run and pass")
}
func dropPrivilegesChild(t *testing.T) {
// Park goroutines on their own OS threads so the process has threads other than
// the one that performs the drop.
release := make(chan struct{})
defer close(release)
ready := make(chan struct{})
for range 4 {
go func() {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
ready <- struct{}{}
<-release
}()
<-ready
}
pd := NewPrivilegeDropper()
require.NoError(t, pd.DropPrivileges(dropTargetUID, dropTargetGID, []uint32{dropTargetGID, dropExtraGID}))
tasks, err := filepath.Glob("/proc/self/task/*/status")
require.NoError(t, err)
require.Greater(t, len(tasks), 4, "process must have several threads for the check to mean anything")
want := map[string]string{
"Uid": fmt.Sprintf("%d\t%d\t%d\t%d", dropTargetUID, dropTargetUID, dropTargetUID, dropTargetUID),
"Gid": fmt.Sprintf("%d\t%d\t%d\t%d", dropTargetGID, dropTargetGID, dropTargetGID, dropTargetGID),
"Groups": fmt.Sprintf("%d %d", dropExtraGID, dropTargetGID),
}
for _, task := range tasks {
got := readStatusFields(t, task)
for field, value := range want {
assert.Equal(t, value, got[field], "%s of %s", field, task)
}
}
assert.Error(t, seteuid(0), "euid 0 must not be restorable after the drop")
assert.Error(t, setegid(0), "egid 0 must not be restorable after the drop")
}
func readStatusFields(t *testing.T, path string) map[string]string {
f, err := os.Open(path)
require.NoError(t, err)
defer f.Close()
fields := make(map[string]string)
scanner := bufio.NewScanner(f)
for scanner.Scan() {
key, value, ok := strings.Cut(scanner.Text(), ":")
if !ok {
continue
}
fields[key] = strings.TrimSpace(value)
}
require.NoError(t, scanner.Err())
return fields
}