是否可以 运行 不同用户下的 goroutine 或 go 方法?

Is it possible to run a goroutine or go method under a different user?

我正在开发一个小型 Web 服务器,它提供文件服务并提供对每个用户主目录的访问权限。

如果源是在 C 中,我可以选择在不同的线程下回答每个请求,并确保每个线程都以调用者的用户作为其用户到达 运行。

是否有任何方法可以实现与 Go 中类似的东西? 理想情况下,处理请求的代码部分、被调用的 goroutine 或方法应该 运行 在调用者的用户帐户下。

我做了一些研究,似乎在 Go 中我们可以将单个 goroutine 粘贴到当前线程,但我看不出如何创建一个新线程然后将 goroutine 附加到该线程。

无法将 goroutine 或方法 运行 作为不同的用户,因为它们都 运行 在与父进程相同的上下文中。 Goroutines 相当于绿色线程,甚至不一定在每个例程中产生适当的 OS 线程。

这个答案可能也取决于 OS,但我认为这也不适用于 windows。

如果您通过 cmd 包生成另一个进程,那么这个答案可能会有用 Running external commands through os/exec under another user

是的,您可以使用 Linux 系统调用 setuid(不是内置函数 setuid)来做到这一点。我刚刚发现了这个问题并认为它必须是可能的,因为我也在其他编程语言中使用它。所以我解决了我的问题,并想报告如何做到这一点。

但是,SJP 写的关于线程的内容是正确的,并且正是我的问题的答案,但它不会解决您的问题,因为线程问题 - whole story in this very long issue 1435。其中还有一个关于如何解决 setuid 问题的特定子集的建议,这解决了我的问题。

但回到代码...您需要调用 LockOSThread 以便将当前的 go 例程固定到您当前正在执行的线程中,您可以使用系统调用更改上下文setuid.

这是 Linux 的工作示例:

package main

import (
        "fmt"
        "log"
        "os"
        "runtime"
        "sync"
        "syscall"
        "time"

)

func printUID() {
        fmt.Printf("Real UID: %d\n", syscall.Getuid())
        fmt.Printf("Effective UID: %d\n", syscall.Geteuid())
}

func main() {
        printUID()
        var wg sync.WaitGroup
        wg.Add(2)

        go func(wg *sync.WaitGroup) {
                defer wg.Done()
                time.Sleep(2 * time.Second)
                printUID()
        }(&wg)

        go func(wg *sync.WaitGroup) {
                runtime.LockOSThread()
                defer runtime.UnlockOSThread()
                defer wg.Done()

                _, _, serr := syscall.Syscall(syscall.SYS_SETUID, 1, 0, 0)
                if serr != 0 {
                        log.Fatal(serr)
                        os.Exit(1)
                }
                printUID()

        }(&wg)
        wg.Wait()
        printUID()
}

如果您使用syscall.Setuid,您将收到operation not supported

serr := syscall.Setuid(1)

而不是

_, _, serr := syscall.Syscall(syscall.SYS_SETUID, 1, 0, 0)

[这个答案与@A.Steinel 的答案相似,但是,唉,我没有足够的声誉来真正评论那个。希望这提供了更多完整的工作示例,并且重要的是,演示了保持 运行time 不会混淆线程 运行 不同的 UID。]

首先,要严格按照您的要求进行操作需要大量技巧,而且并不是那么安全...

[Go喜欢用POSIX semantics, and what you want to do is break POSIX semantics by operating with two or more UIDs at the same time in a single process. Go wants POSIX semantics because it runs goroutines on whatever thread is available, and the runtime needs them to all behave the same for this to work reliably. Since Linux's setuid() syscall doesn't honor POSIX semantics, Go opted to not implement syscall.Setuid() until very recently when it became possible to implement it with POSIX semantics in go1.16运算。

  • 请注意,glibc,如果您调用 setuid(),系统调用本身会包含一个修复机制 (glibc/nptl/setxid),并且会更改所有线程的 UID 值程序同时。因此,即使在 C 中,您也必须进行一些修改才能解决此细节问题。]

也就是说,您可以通过 runtime.LockOSThread() 调用让 goroutines 按您想要的方式工作,但不会在每次专门使用后立即丢弃锁定的线程来混淆 Go 运行time。

类似这样的东西(称之为 uidserve.go):

// Program uidserve serves content as different uids. This is adapted
// from the https://golang.org/pkg/net/http/#ListenAndServe example.
package main

import (
        "fmt"
        "log"
        "net/http"
        "runtime"
        "syscall"
)

// Simple username to uid mapping.
var prefixUIDs = map[string]uintptr{
        "apple":  100,
        "banana": 101,
        "cherry": 102,
}

type uidRunner struct {
        uid uintptr
}

func (u *uidRunner) ServeHTTP(w http.ResponseWriter, r *http.Request) {
        runtime.LockOSThread()
        // Note, we never runtime.UnlockOSThread().
        if _, _, e := syscall.RawSyscall(syscall.SYS_SETUID, u.uid, 0, 0); e != 0 {
                http.Error(w, "permission problem", http.StatusInternalServerError)
                return
        }
        fmt.Fprintf(w, "query %q executing as UID=%d\n", r.URL.Path, syscall.Getuid())
}

func main() {
        for u, uid := range prefixUIDs {
                h := &uidRunner{uid: uid}
                http.Handle(fmt.Sprint("/", u, "/"), h)
        }
        http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
                fmt.Fprintf(w, "general query %q executing as UID=%d\n", r.URL.Path, syscall.Getuid())
        })
        log.Fatal(http.ListenAndServe(":8080", nil))
}

像这样构建它:

$ go build uidserve.go

接下来,要使其正常工作,您必须授予该程序一些权限。那就是做一个或另一个(setcap 是来自 libcap suite 的工具):

$ sudo /sbin/setcap cap_setuid=ep ./uidserve

或者,更传统的方法 运行ning setuid-root:

$ sudo chown root ./uidserve
$ sudo chmod +s ./uidserve

现在,如果您 运行 ./uidserve 并将浏览器连接到 localhost:8080,您可以尝试获取以下网址:

  • localhost:8080/something 显示类似 general query "/something" executing as UID= 你的 UID 在这里 .
  • localhost:8080/apple/pie 显示类似 query "/apple/pie" executing as UID=100.
  • 等等

希望这有助于说明如何按照您的要求进行操作。 [但是,由于它涉及很多技巧,所以我不建议真正这样做...]