summaryrefslogtreecommitdiffstats
path: root/src/runtime/security_test.go
blob: 5cd90f9d1fa67750f8fdb281bcbdac811f201994 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build unix

package runtime_test

import (
	"bytes"
	"context"
	"fmt"
	"internal/testenv"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strings"
	"testing"
	"time"
)

func privesc(command string, args ...string) error {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
	defer cancel()
	var cmd *exec.Cmd
	if runtime.GOOS == "darwin" {
		cmd = exec.CommandContext(ctx, "sudo", append([]string{"-n", command}, args...)...)
	} else if runtime.GOOS == "openbsd" {
		cmd = exec.CommandContext(ctx, "doas", append([]string{"-n", command}, args...)...)
	} else {
		cmd = exec.CommandContext(ctx, "su", highPrivUser, "-c", fmt.Sprintf("%s %s", command, strings.Join(args, " ")))
	}
	_, err := cmd.CombinedOutput()
	return err
}

const highPrivUser = "root"

func setSetuid(t *testing.T, user, bin string) {
	t.Helper()
	// We escalate privileges here even if we are root, because for some reason on some builders
	// (at least freebsd-amd64-13_0) the default PATH doesn't include /usr/sbin, which is where
	// chown lives, but using 'su root -c' gives us the correct PATH.

	// buildTestProg uses os.MkdirTemp which creates directories with 0700, which prevents
	// setuid binaries from executing because of the missing g+rx, so we need to set the parent
	// directory to better permissions before anything else. We created this directory, so we
	// shouldn't need to do any privilege trickery.
	if err := privesc("chmod", "0777", filepath.Dir(bin)); err != nil {
		t.Skipf("unable to set permissions on %q, likely no passwordless sudo/su: %s", filepath.Dir(bin), err)
	}

	if err := privesc("chown", user, bin); err != nil {
		t.Skipf("unable to set permissions on test binary, likely no passwordless sudo/su: %s", err)
	}
	if err := privesc("chmod", "u+s", bin); err != nil {
		t.Skipf("unable to set permissions on test binary, likely no passwordless sudo/su: %s", err)
	}
}

func TestSUID(t *testing.T) {
	// This test is relatively simple, we build a test program which opens a
	// file passed via the TEST_OUTPUT envvar, prints the value of the
	// GOTRACEBACK envvar to stdout, and prints "hello" to stderr. We then chown
	// the program to "nobody" and set u+s on it. We execute the program, only
	// passing it two files, for stdin and stdout, and passing
	// GOTRACEBACK=system in the env.
	//
	// We expect that the program will trigger the SUID protections, resetting
	// the value of GOTRACEBACK, and opening the missing stderr descriptor, such
	// that the program prints "GOTRACEBACK=none" to stdout, and nothing gets
	// written to the file pointed at by TEST_OUTPUT.

	if *flagQuick {
		t.Skip("-quick")
	}

	testenv.MustHaveGoBuild(t)

	helloBin, err := buildTestProg(t, "testsuid")
	if err != nil {
		t.Fatal(err)
	}

	f, err := os.CreateTemp(t.TempDir(), "suid-output")
	if err != nil {
		t.Fatal(err)
	}
	tempfilePath := f.Name()
	f.Close()

	lowPrivUser := "nobody"
	setSetuid(t, lowPrivUser, helloBin)

	b := bytes.NewBuffer(nil)
	pr, pw, err := os.Pipe()
	if err != nil {
		t.Fatal(err)
	}

	proc, err := os.StartProcess(helloBin, []string{helloBin}, &os.ProcAttr{
		Env:   []string{"GOTRACEBACK=system", "TEST_OUTPUT=" + tempfilePath},
		Files: []*os.File{os.Stdin, pw},
	})
	if err != nil {
		if os.IsPermission(err) {
			t.Skip("don't have execute permission on setuid binary, possibly directory permission issue?")
		}
		t.Fatal(err)
	}
	done := make(chan bool, 1)
	go func() {
		io.Copy(b, pr)
		pr.Close()
		done <- true
	}()
	ps, err := proc.Wait()
	if err != nil {
		t.Fatal(err)
	}
	pw.Close()
	<-done
	output := b.String()

	if ps.ExitCode() == 99 {
		t.Skip("binary wasn't setuid (uid == euid), unable to effectively test")
	}

	expected := "GOTRACEBACK=none\n"
	if output != expected {
		t.Errorf("unexpected output, got: %q, want %q", output, expected)
	}

	fc, err := os.ReadFile(tempfilePath)
	if err != nil {
		t.Fatal(err)
	}
	if string(fc) != "" {
		t.Errorf("unexpected file content, got: %q", string(fc))
	}

	// TODO: check the registers aren't leaked?
}