1

I am working on a small web server that serves files and provides access to each user's home directory.

If the source was to be in C I had the option of answering each request under different threads and to make sure each thread gets to run with the user of the caller as its users.

Is there any approach to achieve something similar to that in Go? Ideally, the part of the code that handles the request, the goroutine or the method that gets called should be run under the user account of the caller.

I have done some research and it seems in Go we can stick a single goroutine to the current thread but I can't see how it is possible to create a new thread and then attach a goroutine to that thread.

Reza
  • 1,478
  • 1
  • 15
  • 25

3 Answers3

3

It is not possible to run a goroutine or method as a different user because they both run within the same context as the parent process. Goroutines are equivalent to green threads and don't even necessarily spawn off proper OS thread per routine.

This answer might also depend on OS, but I don't think this will work on windows either.

if you are spawning another process via the cmd package, then this answer may be useful Running external commands through os/exec under another user

SJP
  • 586
  • 1
  • 6
  • 13
  • 1
    Even if an OS supports sub-process permissions, Go doesn't expose that functionality. (I'm also not aware of any such OS in existence) – Jonathan Hall Jun 01 '19 at 08:31
2

Yes, you can do that with the use of the Linux syscall setuid (not the built in function setuid). I just found this question and thought that it has to be possible, as I use this in other programming languages too. So I got my problem solved and wanted to report back how to do this.

However, it is correct what SJP wrote about the threads and there lies exactly the answer to my problem, but it will not solve your problem, due to the threading issue - whole story in this very long issue 1435. Therein is also a suggestion in how to solve a specific subset of the setuid problem and that solved my problem.

But back to code ... you need to call LockOSThread in order to fix the current go routine to the thread you're currently executing in and in that, you can change the context with the syscall setuid.

Here is a working example for 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()
}

You will receive operation not supported if you use syscall.Setuid:

serr := syscall.Setuid(1)

instead of

_, _, serr := syscall.Syscall(syscall.SYS_SETUID, 1, 0, 0)
A.Steinel
  • 176
  • 5
2

[This answer is similar to the one by @A.Steinel but, alas, I have insufficient reputation to actually comment on that one. Hopefully, this offers a little more of a complete worked example and, importantly, demonstrates keeping the runtime free of the confusion of threads running with different UIDs.]

First, to strictly do what you asked requires a number of hacks and isn't all that secure...

[Go likes to operate with 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.

  • Note, glibc, if you call setuid(), wraps the syscall itself with a fix-up mechanism (glibc/nptl/setxid) and will change the UID values for all the threads in the program simultaneously. So, even in C, you will have to do some hacking to work around this detail.]

That being said, you can make goroutines work the way you want with the runtime.LockOSThread() call, but not confuse the Go runtime by discarding the locked threads immediately after each specialized use.

Something like this (call it 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))
}

Build it like this:

$ go build uidserve.go

Next, to get this to work, you have to grant this program some privilege. That is do one or the other of (setcap is a tool from the libcap suite):

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

or, alternatively, the more traditional way of running setuid-root:

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

Now, if you run ./uidserve and connect to your browser to localhost:8080 you can try fetching the following URLs:

  • localhost:8080/something which shows something like general query "/something" executing as UID=your UID here.
  • localhost:8080/apple/pie which shows something like query "/apple/pie" executing as UID=100.
  • etc.

Hope that helps show how to do what you asked. [Since it involves lots of hacks, however, I wouldn't recommend doing this for real though...]

Tinkerer
  • 865
  • 7
  • 9