privs_test.go 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. //go:build linux || darwin || freebsd || openbsd || netbsd || dragonfly
  4. package tailssh
  5. import (
  6. "encoding/json"
  7. "errors"
  8. "os"
  9. "os/exec"
  10. "os/user"
  11. "path/filepath"
  12. "reflect"
  13. "regexp"
  14. "runtime"
  15. "slices"
  16. "strconv"
  17. "syscall"
  18. "testing"
  19. "tailscale.com/types/logger"
  20. )
  21. func TestDoDropPrivileges(t *testing.T) {
  22. type SubprocInput struct {
  23. UID int
  24. GID int
  25. AdditionalGroups []int
  26. }
  27. type SubprocOutput struct {
  28. UID int
  29. GID int
  30. EUID int
  31. EGID int
  32. AdditionalGroups []int
  33. }
  34. if v := os.Getenv("TS_TEST_DROP_PRIVILEGES_CHILD"); v != "" {
  35. t.Logf("in child process")
  36. var input SubprocInput
  37. if err := json.Unmarshal([]byte(v), &input); err != nil {
  38. t.Fatal(err)
  39. }
  40. // Get a handle to our provided JSON file before dropping privs.
  41. f := os.NewFile(3, "out.json")
  42. // We're in our subprocess; actually drop privileges now.
  43. doDropPrivileges(t.Logf, input.UID, input.GID, input.AdditionalGroups, "/")
  44. additional, _ := syscall.Getgroups()
  45. // Print our IDs
  46. json.NewEncoder(f).Encode(SubprocOutput{
  47. UID: os.Getuid(),
  48. GID: os.Getgid(),
  49. EUID: os.Geteuid(),
  50. EGID: os.Getegid(),
  51. AdditionalGroups: additional,
  52. })
  53. // Close output file to ensure that it's flushed to disk before we exit
  54. f.Close()
  55. // Always exit the process now that we have a different
  56. // UID/GID/etc.; we don't want the Go test framework to try and
  57. // clean anything up, since it might no longer have access.
  58. os.Exit(0)
  59. }
  60. if os.Getuid() != 0 {
  61. t.Skip("test only works when run as root")
  62. }
  63. rerunSelf := func(t *testing.T, input SubprocInput) []byte {
  64. fpath := filepath.Join(t.TempDir(), "out.json")
  65. outf, err := os.Create(fpath)
  66. if err != nil {
  67. t.Fatal(err)
  68. }
  69. inputb, err := json.Marshal(input)
  70. if err != nil {
  71. t.Fatal(err)
  72. }
  73. cmd := exec.Command(os.Args[0], "-test.v", "-test.run", "^"+regexp.QuoteMeta(t.Name())+"$")
  74. cmd.Env = append(os.Environ(), "TS_TEST_DROP_PRIVILEGES_CHILD="+string(inputb))
  75. cmd.ExtraFiles = []*os.File{outf}
  76. cmd.Stdout = logger.FuncWriter(logger.WithPrefix(t.Logf, "child: "))
  77. cmd.Stderr = logger.FuncWriter(logger.WithPrefix(t.Logf, "child: "))
  78. if err := cmd.Run(); err != nil {
  79. t.Fatal(err)
  80. }
  81. outf.Close()
  82. jj, err := os.ReadFile(fpath)
  83. if err != nil {
  84. t.Fatal(err)
  85. }
  86. return jj
  87. }
  88. // We want to ensure we're not colliding with existing users; find some
  89. // unused UIDs and GIDs for the tests we run.
  90. uid1 := findUnusedUID(t)
  91. gid1 := findUnusedGID(t)
  92. gid2 := findUnusedGID(t, gid1)
  93. gid3 := findUnusedGID(t, gid1, gid2)
  94. // For some tests, we want a UID/GID pair with the same numerical
  95. // value; this finds one.
  96. uidgid1 := findUnusedUIDGID(t, uid1, gid1, gid2, gid3)
  97. t.Logf("uid1=%d gid1=%d gid2=%d gid3=%d uidgid1=%d",
  98. uid1, gid1, gid2, gid3, uidgid1)
  99. testCases := []struct {
  100. name string
  101. uid int
  102. gid int
  103. additionalGroups []int
  104. }{
  105. {
  106. name: "all_different_values",
  107. uid: uid1,
  108. gid: gid1,
  109. additionalGroups: []int{gid2, gid3},
  110. },
  111. {
  112. name: "no_additional_groups",
  113. uid: uid1,
  114. gid: gid1,
  115. additionalGroups: []int{},
  116. },
  117. // This is a regression test for the following bug, triggered
  118. // on Darwin & FreeBSD:
  119. // https://github.com/tailscale/tailscale/issues/7616
  120. {
  121. name: "same_values",
  122. uid: uidgid1,
  123. gid: uidgid1,
  124. additionalGroups: []int{uidgid1},
  125. },
  126. }
  127. for _, tt := range testCases {
  128. t.Run(tt.name, func(t *testing.T) {
  129. subprocOut := rerunSelf(t, SubprocInput{
  130. UID: tt.uid,
  131. GID: tt.gid,
  132. AdditionalGroups: tt.additionalGroups,
  133. })
  134. var out SubprocOutput
  135. if err := json.Unmarshal(subprocOut, &out); err != nil {
  136. t.Logf("%s", subprocOut)
  137. t.Fatal(err)
  138. }
  139. t.Logf("output: %+v", out)
  140. if out.UID != tt.uid {
  141. t.Errorf("got uid %d; want %d", out.UID, tt.uid)
  142. }
  143. if out.GID != tt.gid {
  144. t.Errorf("got gid %d; want %d", out.GID, tt.gid)
  145. }
  146. if out.EUID != tt.uid {
  147. t.Errorf("got euid %d; want %d", out.EUID, tt.uid)
  148. }
  149. if out.EGID != tt.gid {
  150. t.Errorf("got egid %d; want %d", out.EGID, tt.gid)
  151. }
  152. // On FreeBSD and Darwin, the set of additional groups
  153. // is prefixed with the egid; handle that case by
  154. // modifying our expected set.
  155. wantGroups := make(map[int]bool)
  156. for _, id := range tt.additionalGroups {
  157. wantGroups[id] = true
  158. }
  159. if runtime.GOOS == "darwin" || runtime.GOOS == "freebsd" {
  160. wantGroups[tt.gid] = true
  161. }
  162. gotGroups := make(map[int]bool)
  163. for _, id := range out.AdditionalGroups {
  164. gotGroups[id] = true
  165. }
  166. if !reflect.DeepEqual(gotGroups, wantGroups) {
  167. t.Errorf("got additional groups %+v; want %+v", gotGroups, wantGroups)
  168. }
  169. })
  170. }
  171. }
  172. func findUnusedUID(t *testing.T, not ...int) int {
  173. for i := 1000; i < 65535; i++ {
  174. // Skip UIDs that might be valid
  175. if maybeValidUID(i) {
  176. continue
  177. }
  178. // Skip UIDs that we're avoiding
  179. if slices.Contains(not, i) {
  180. continue
  181. }
  182. // Not a valid UID, not one we're avoiding... all good!
  183. return i
  184. }
  185. t.Fatalf("unable to find an unused UID")
  186. return -1
  187. }
  188. func findUnusedGID(t *testing.T, not ...int) int {
  189. for i := 1000; i < 65535; i++ {
  190. if maybeValidGID(i) {
  191. continue
  192. }
  193. // Skip GIDs that we're avoiding
  194. if slices.Contains(not, i) {
  195. continue
  196. }
  197. // Not a valid GID, not one we're avoiding... all good!
  198. return i
  199. }
  200. t.Fatalf("unable to find an unused GID")
  201. return -1
  202. }
  203. func findUnusedUIDGID(t *testing.T, not ...int) int {
  204. for i := 1000; i < 65535; i++ {
  205. if maybeValidUID(i) || maybeValidGID(i) {
  206. continue
  207. }
  208. // Skip IDs that we're avoiding
  209. if slices.Contains(not, i) {
  210. continue
  211. }
  212. // Not a valid ID, not one we're avoiding... all good!
  213. return i
  214. }
  215. t.Fatalf("unable to find an unused UID/GID pair")
  216. return -1
  217. }
  218. func maybeValidUID(id int) bool {
  219. _, err := user.LookupId(strconv.Itoa(id))
  220. if err == nil {
  221. return true
  222. }
  223. var u1 user.UnknownUserIdError
  224. if errors.As(err, &u1) {
  225. return false
  226. }
  227. var u2 user.UnknownUserError
  228. if errors.As(err, &u2) {
  229. return false
  230. }
  231. // Some other error; might be valid
  232. return true
  233. }
  234. func maybeValidGID(id int) bool {
  235. _, err := user.LookupGroupId(strconv.Itoa(id))
  236. if err == nil {
  237. return true
  238. }
  239. var u1 user.UnknownGroupIdError
  240. if errors.As(err, &u1) {
  241. return false
  242. }
  243. var u2 user.UnknownGroupError
  244. if errors.As(err, &u2) {
  245. return false
  246. }
  247. // Some other error; might be valid
  248. return true
  249. }