diff --git a/util/pidowner/pidowner.go b/util/pidowner/pidowner.go new file mode 100644 index 000000000..f8e46adf4 --- /dev/null +++ b/util/pidowner/pidowner.go @@ -0,0 +1,25 @@ +// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package pidowner handles lookups from process ID to its owning user. +package pidowner + +import ( + "errors" + "runtime" +) + +var ErrNotImplemented = errors.New("not implemented for GOOS=" + runtime.GOOS) + +var ErrProcessNotFound = errors.New("process not found") + +// OwnerOfPID returns the user ID that owns the given process ID. +// +// The returned user ID is suitable to passing to os/user.LookupId. +// +// The returned error will be ErrNotImplemented for operating systems where +// this isn't supported. +func OwnerOfPID(pid int) (userID string, err error) { + return ownerOfPID(pid) +} diff --git a/util/pidowner/pidowner_linux.go b/util/pidowner/pidowner_linux.go new file mode 100644 index 000000000..d835e0c54 --- /dev/null +++ b/util/pidowner/pidowner_linux.go @@ -0,0 +1,33 @@ +package pidowner + +import ( + "fmt" + "os" + "strings" + + "tailscale.com/util/lineread" +) + +func ownerOfPID(pid int) (userID string, err error) { + file := fmt.Sprintf("/proc/%d/status", pid) + err = lineread.File(file, func(line []byte) error { + if len(line) < 4 || string(line[:4]) != "Uid:" { + return nil + } + f := strings.Fields(string(line)) + if len(f) >= 2 { + userID = f[1] // real userid + } + return nil + }) + if os.IsNotExist(err) { + return "", ErrProcessNotFound + } + if err != nil { + return + } + if userID == "" { + return "", fmt.Errorf("missing Uid line in %s", file) + } + return userID, nil +} diff --git a/util/pidowner/pidowner_noimpl.go b/util/pidowner/pidowner_noimpl.go new file mode 100644 index 000000000..9be1a28dc --- /dev/null +++ b/util/pidowner/pidowner_noimpl.go @@ -0,0 +1,9 @@ +// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build !windows,!linux + +package pidowner + +func ownerOfPID(pid int) (userID string, err error) { return "", ErrNotImplemented } diff --git a/util/pidowner/pidowner_test.go b/util/pidowner/pidowner_test.go new file mode 100644 index 000000000..53eb3a5f7 --- /dev/null +++ b/util/pidowner/pidowner_test.go @@ -0,0 +1,47 @@ +package pidowner + +import ( + "math/rand" + "os" + "os/user" + "testing" +) + +func TestOwnerOfPID(t *testing.T) { + id, err := OwnerOfPID(os.Getpid()) + if err == ErrNotImplemented { + t.Skip(err) + } + if err != nil { + t.Fatal(err) + } + t.Logf("id=%q", id) + + u, err := user.LookupId(id) + if err != nil { + t.Fatalf("LookupId: %v", err) + } + t.Logf("Got: %+v", u) +} + +// validate that OS implementation returns ErrProcessNotFound. +func TestNotFoundError(t *testing.T) { + // Try a bunch of times to stumble upon a pid that doesn't exist... + const tries = 50 + for i := 0; i < tries; i++ { + _, err := OwnerOfPID(rand.Intn(1e9)) + if err == ErrNotImplemented { + t.Skip(err) + } + if err == nil { + // We got unlucky and this pid existed. Try again. + continue + } + if err == ErrProcessNotFound { + // Pass. + return + } + t.Fatalf("Error is not ErrProcessNotFound: %T %v", err, err) + } + t.Errorf("after %d tries, couldn't find a process that didn't exist", tries) +} diff --git a/util/pidowner/pidowner_windows.go b/util/pidowner/pidowner_windows.go new file mode 100644 index 000000000..95afa0cf2 --- /dev/null +++ b/util/pidowner/pidowner_windows.go @@ -0,0 +1,32 @@ +package pidowner + +import ( + "fmt" + "syscall" + + "golang.org/x/sys/windows" +) + +func ownerOfPID(pid int) (userID string, err error) { + procHnd, err := windows.OpenProcess(windows.PROCESS_QUERY_INFORMATION, false, uint32(pid)) + if err == syscall.Errno(0x57) { // invalid parameter, for PIDs that don't exist + return "", ErrProcessNotFound + } + if err != nil { + return "", fmt.Errorf("OpenProcess: %T %#v", err, err) + } + defer windows.CloseHandle(procHnd) + + var tok windows.Token + if err := windows.OpenProcessToken(procHnd, windows.TOKEN_QUERY, &tok); err != nil { + return "", fmt.Errorf("OpenProcessToken: %w", err) + } + + tokUser, err := tok.GetTokenUser() + if err != nil { + return "", fmt.Errorf("GetTokenUser: %w", err) + } + + sid := tokUser.User.Sid + return sid.String(), nil +}