[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...]