New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
osutil/sys, client: add sys.RunAsUidGid, use it for auth.json #4983
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,7 +20,9 @@ | |
package sys | ||
|
||
import ( | ||
"fmt" | ||
"os" | ||
"runtime" | ||
"syscall" | ||
"unsafe" | ||
) | ||
|
@@ -107,3 +109,79 @@ func FcntlGetFl(fd int) (int, error) { | |
} | ||
return int(flags), nil | ||
} | ||
|
||
func retryRawSyscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err syscall.Errno) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this function warrants a comment |
||
ms := &syscall.Timespec{Nsec: 1000} | ||
for i := 0; i < 30; i++ { | ||
r1, r2, err = syscall.RawSyscall(trap, a1, a2, a3) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a need to use RawSyscall vs Syscall? The difference, AFAIK, is that raw syscall blocks directly and Syscall will ping back to the runtime to say its about to enter and has just left a system call. If there's a need then please document why we use RawSyscall here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm trusting the go standard library on the distinction here ( |
||
if err != syscall.EAGAIN { | ||
break | ||
} | ||
// this could fail, making the loop a little too aggressive | ||
syscall.Nanosleep(ms, nil) | ||
} | ||
return r1, r2, err | ||
} | ||
|
||
type UnrecoverableError struct { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please document this type. |
||
Call string | ||
Err error | ||
} | ||
|
||
func (e UnrecoverableError) Error() string { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please document this error. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing docs :) |
||
return fmt.Sprintf("%s: %v", e.Call, e.Err) | ||
} | ||
|
||
// RunAsUidGid starts a goroutine, pins it to the OS thread, sets euid and egid, | ||
// and runs the function; after the function returns, it restores euid and egid. | ||
// | ||
// If restoring the original euid and egid fails this function will panic with | ||
// an UnrecoverableError, and you should _not_ try to recover from it: the | ||
// runtime itself is going to be in trouble. | ||
func RunAsUidGid(uid UserID, gid GroupID, f func() error) error { | ||
ch := make(chan error, 1) | ||
go func() { | ||
// from the docs: | ||
// until the goroutine exits or calls UnlockOSThread, it will | ||
// always execute in this thread, and no other goroutine can. | ||
// that last bit means it's safe to setuid/setgid in here, as no | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nitpick, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm torn between fixing it now, or landing it now and fixing it later :-) |
||
// other code will run. | ||
runtime.LockOSThread() | ||
|
||
// from Go 1.10 on we could just not unlock, which would make | ||
// the thread get reaped at goroutine exit. Instead we need to | ||
// carefully restore thread state. | ||
defer runtime.UnlockOSThread() | ||
|
||
ruid := Getuid() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. While reading I read the code for those. Is there any reason we are not using |
||
rgid := Getgid() | ||
|
||
// do the setregid first =) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you document if the order is significant and if so, why, please? |
||
if _, _, err := retryRawSyscall(_SYS_SETREGID, FlagID, uintptr(gid), 0); err != 0 { | ||
ch <- fmt.Errorf("setregid: %v", err) | ||
return | ||
} | ||
defer func() { | ||
// try to restore egid | ||
if _, _, err := retryRawSyscall(_SYS_SETREGID, FlagID, uintptr(rgid), 0); err != 0 { | ||
// ¯\_(ツ)_/¯ | ||
panic(UnrecoverableError{Call: "setregid", Err: err}) | ||
} | ||
}() | ||
|
||
if _, _, err := retryRawSyscall(_SYS_SETREUID, FlagID, uintptr(uid), 0); err != 0 { | ||
ch <- fmt.Errorf("setreuid: %v", err) | ||
return | ||
} | ||
defer func() { | ||
// try to restore euid | ||
if _, _, err := retryRawSyscall(_SYS_SETREUID, FlagID, uintptr(ruid), 0); err != 0 { | ||
// ¯\_(ツ)_/¯ | ||
panic(UnrecoverableError{Call: "setreuid", Err: err}) | ||
} | ||
}() | ||
|
||
ch <- f() | ||
}() | ||
return <-ch | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
summary: test chattr | ||
# ubuntu-core doesn't have go :-) | ||
systems: [-ubuntu-core-16-*] | ||
execute: | | ||
go build ./testit.go | ||
test "$(./testit)" = "before: 0/0, during: 12345/12345 (<nil>), after: 0/0; status: OK" | ||
test "$(sudo -u '#12345' -g '#12345' ./testit)" = "before: 12345/12345, during: 12345/12345 (<nil>), after: 12345/12345; status: OK" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
package main | ||
|
||
import ( | ||
"fmt" | ||
"runtime" | ||
"sync" | ||
|
||
"github.com/snapcore/snapd/osutil/sys" | ||
) | ||
|
||
var wg sync.WaitGroup | ||
var mu sync.Mutex | ||
|
||
func check(uids []sys.UserID, n int) { | ||
// spin | ||
for i := 0; i < 1<<30; i++ { | ||
} | ||
|
||
mu.Lock() | ||
uids[n] = sys.Geteuid() | ||
mu.Unlock() | ||
|
||
wg.Done() | ||
} | ||
|
||
func main() { | ||
orig := sys.Geteuid() | ||
before := fmt.Sprintf("%d/%d", sys.Geteuid(), sys.Getegid()) | ||
var during string | ||
err := sys.RunAsUidGid(12345, 12345, func() error { | ||
during = fmt.Sprintf("%d/%d", sys.Geteuid(), sys.Getegid()) | ||
return nil | ||
}) | ||
after := fmt.Sprintf("%d/%d", sys.Geteuid(), sys.Getegid()) | ||
|
||
N := 2 * runtime.NumCPU() | ||
uids := make([]sys.UserID, N) | ||
// launch a lot of goroutines so we cover all threads with space to spare | ||
for i := 0; i < N; i++ { | ||
wg.Add(1) | ||
go check(uids, i) | ||
} | ||
wg.Wait() | ||
|
||
bad := 0 | ||
for _, uid := range uids { | ||
if uid != orig { | ||
bad++ | ||
} | ||
} | ||
status := "OK" | ||
if bad != 0 { | ||
status = fmt.Sprintf("%d BAD!", bad) | ||
} | ||
|
||
fmt.Printf("before: %s, during: %s (%v), after: %s; status: %s\n", before, during, err, after, status) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, just return the error directly? :-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, the diff formatting confused me, never mind :)