Skip to content
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

capability: add separate ambient and bound API #176

Merged
merged 7 commits into from
Nov 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions capability/capability.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,35 @@ func NewFile2(path string) (Capabilities, error) {
func LastCap() (Cap, error) {
return lastCap()
}

// GetAmbient determines if a specific ambient capability is raised in the
// calling thread.
func GetAmbient(c Cap) (bool, error) {
return getAmbient(c)
}

// SetAmbient raises or lowers specified ambient capabilities for the calling
// thread. To complete successfully, the prevailing effective capability set
// must have a raised CAP_SETPCAP. Further, to raise a specific ambient
// capability the inheritable and permitted sets of the calling thread must
// already contain the specified capability.
func SetAmbient(raise bool, caps ...Cap) error {
return setAmbient(raise, caps...)
}

// ResetAmbient resets all of the ambient capabilities for the calling thread
// to their lowered value.
func ResetAmbient() error {
return resetAmbient()
}

// GetBound determines if a specific bounding capability is raised in the
// calling thread.
func GetBound(c Cap) (bool, error) {
return getBound(c)
}

// DropBound lowers the specified bounding set capability.
func DropBound(caps ...Cap) error {
return dropBound(caps...)
}
87 changes: 65 additions & 22 deletions capability/capability_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,13 @@ func newPid(pid int) (c Capabilities, retErr error) {
return
}

func ignoreEINVAL(err error) error {
if errors.Is(err, syscall.EINVAL) {
err = nil
Comment on lines +120 to +122
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reminds me that I stumbled on this package which had a "consider integrating with moby/sys todo; https://github.com/moby/moby/blob/dc225798cbddebd47bfaa0fd8337d145c91fc6ba/internal/unix_noeintr/fs_unix.go#L3-L5

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That thing in there is something entirely different -- it's a retry-on-EINTR, and here we have ignore-EINVAL.

As to autogenerating stuff, even github.com/golang/go doesn't do it -- they use helper functions here and there but it's all manual.

}
return err
}

type capsV3 struct {
hdr capHeader
data [2]capData
Expand Down Expand Up @@ -327,7 +334,7 @@ func (c *capsV3) Load() (err error) {
return
}

func (c *capsV3) Apply(kind CapType) (err error) {
func (c *capsV3) Apply(kind CapType) error {
if c.hdr.pid != 0 {
return errors.New("unable to modify capabilities of another process")
}
Expand All @@ -339,21 +346,17 @@ func (c *capsV3) Apply(kind CapType) (err error) {
var data [2]capData
err = capget(&c.hdr, &data[0])
if err != nil {
return
return err
}
if (1<<uint(CAP_SETPCAP))&data[0].effective != 0 {
for i := Cap(0); i <= last; i++ {
if c.Get(BOUNDING, i) {
continue
}
err = prctl(syscall.PR_CAPBSET_DROP, uintptr(i), 0, 0, 0)
// Ignore EINVAL since the capability may not be supported in this system.
err = ignoreEINVAL(dropBound(i))
if err != nil {
// Ignore EINVAL since the capability may not be supported in this system.
if err == syscall.EINVAL { //nolint:errorlint // Errors from syscall are bare.
err = nil
continue
}
return
return err
}
}
}
Expand All @@ -362,33 +365,73 @@ func (c *capsV3) Apply(kind CapType) (err error) {
if kind&CAPS == CAPS {
err = capset(&c.hdr, &c.data[0])
if err != nil {
return
return err
}
}

if kind&AMBS == AMBS {
err = prctl(pr_CAP_AMBIENT, pr_CAP_AMBIENT_CLEAR_ALL, 0, 0, 0)
if err != nil && err != syscall.EINVAL { //nolint:errorlint // Errors from syscall are bare.
// Ignore EINVAL as not supported on kernels before 4.3
return
// Ignore EINVAL as not supported on kernels before 4.3
err = ignoreEINVAL(resetAmbient())
if err != nil {
return err
}
for i := Cap(0); i <= last; i++ {
if !c.Get(AMBIENT, i) {
continue
}
err = prctl(pr_CAP_AMBIENT, pr_CAP_AMBIENT_RAISE, uintptr(i), 0, 0)
// Ignore EINVAL as not supported on kernels before 4.3
err = ignoreEINVAL(setAmbient(true, i))
if err != nil {
// Ignore EINVAL as not supported on kernels before 4.3
if err == syscall.EINVAL { //nolint:errorlint // Errors from syscall are bare.
err = nil
continue
}
return
return err
}
}
}

return
return nil
}

func getAmbient(c Cap) (bool, error) {
res, err := prctlRetInt(pr_CAP_AMBIENT, pr_CAP_AMBIENT_IS_SET, uintptr(c))
if err != nil {
return false, err
}
return res > 0, nil
}

func setAmbient(raise bool, caps ...Cap) error {
op := pr_CAP_AMBIENT_RAISE
if !raise {
op = pr_CAP_AMBIENT_LOWER
}
for _, val := range caps {
err := prctl(pr_CAP_AMBIENT, op, uintptr(val))
if err != nil {
return err
}
}
return nil
}

func resetAmbient() error {
return prctl(pr_CAP_AMBIENT, pr_CAP_AMBIENT_CLEAR_ALL, 0)
}

func getBound(c Cap) (bool, error) {
res, err := prctlRetInt(syscall.PR_CAPBSET_READ, uintptr(c), 0)
if err != nil {
return false, err
}
return res > 0, nil
}

func dropBound(caps ...Cap) error {
for _, val := range caps {
err := prctl(syscall.PR_CAPBSET_DROP, uintptr(val), 0)
if err != nil {
return err
}
}
return nil
}

func newFile(path string) (c Capabilities, err error) {
Expand Down
20 changes: 20 additions & 0 deletions capability/capability_noop.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,23 @@ func newFile(_ string) (Capabilities, error) {
func lastCap() (Cap, error) {
return -1, errNotSup
}

func getAmbient(_ Cap) (bool, error) {
return false, errNotSup
}

func setAmbient(_ bool, _ ...Cap) error {
return errNotSup
}

func resetAmbient() error {
return errNotSup
}

func getBound(_ Cap) (bool, error) {
return false, errNotSup
}

func dropBound(_ ...Cap) error {
return errNotSup
}
148 changes: 147 additions & 1 deletion capability/capability_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func requirePCapSet(t testing.TB) {
}

// testInChild runs fn as a separate process, and returns its output.
// This is useful for tests which manipulate capabilties, allowing to
// This is useful for tests which manipulate capabilities, allowing to
// preserve those of the main test process.
//
// The fn is a function which must end with os.Exit. In case exit code
Expand Down Expand Up @@ -150,6 +150,7 @@ func TestAmbientCapSet(t *testing.T) {
}

func childAmbientCapSet() {
runtime.LockOSThread()
// We can't use t.Log etc. here, yet filename and line number is nice
// to have. Set up and use the standard logger for this.
log.SetFlags(log.Lshortfile)
Expand Down Expand Up @@ -227,3 +228,148 @@ func TestApplyOtherProcess(t *testing.T) {
}
}
}

func TestGetSetResetAmbient(t *testing.T) {
if runtime.GOOS != "linux" {
_, err := GetAmbient(Cap(0))
if err == nil {
t.Error(runtime.GOOS, ": want error, got nil")
}
err = SetAmbient(false, Cap(0))
if err == nil {
t.Error(runtime.GOOS, ": want error, got nil")
}
err = ResetAmbient()
if err == nil {
t.Error(runtime.GOOS, ": want error, got nil")
}
return
}

requirePCapSet(t)
out := testInChild(t, childGetSetResetAmbient)
t.Logf("output from child:\n%s", out)
}

func childGetSetResetAmbient() {
runtime.LockOSThread()
log.SetFlags(log.Lshortfile)

pid, err := NewPid2(0)
if err != nil {
log.Fatal(err)
}

list := []Cap{CAP_KILL, CAP_CHOWN, CAP_SYS_CHROOT}
pid.Set(CAPS, list...)
if err = pid.Apply(CAPS); err != nil {
log.Fatal(err)
}

// Set ambient caps from list.
if err = SetAmbient(true, list...); err != nil {
log.Fatal(err)
}

// Check if they were set as expected.
for _, cap := range list {
want := true
got, err := GetAmbient(cap)
if err != nil {
log.Fatalf("GetAmbient(%s): want nil, got error %v", cap, err)
} else if want != got {
log.Fatalf("Get(AMBIENT, %s): want %v, got %v", cap, want, got)
}
}

// Lower one ambient cap.
const unsetIdx = 1
if err = SetAmbient(false, list[unsetIdx]); err != nil {
log.Fatal(err)
}
// Check they are set as expected.
for i, cap := range list {
want := i != unsetIdx
got, err := GetAmbient(cap)
if err != nil {
log.Fatalf("GetAmbient(%s): want nil, got error %v", cap, err)
} else if want != got {
log.Fatalf("Get(AMBIENT, %s): want %v, got %v", cap, want, got)
}
}

// Lower all ambient caps.
if err = ResetAmbient(); err != nil {
log.Fatal(err)
}
for _, cap := range list {
want := false
got, err := GetAmbient(cap)
if err != nil {
log.Fatalf("GetAmbient(%s): want nil, got error %v", cap, err)
} else if want != got {
log.Fatalf("Get(AMBIENT, %s): want %v, got %v", cap, want, got)
}
}
os.Exit(0)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LOL, had to look twice, but then saw it's called from testInChild

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it's somewhat complicated here since we modify capabilities and if we do it in a current process the following tests are toasted.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm way outside my understanding, but isn't that what runtime.LockOSThread is for/does?
(makes sure the capabilities are adjusted for the thread we're on, and then if we don't unlock the thread, it exits instead of being reused?)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are actually re-executing the test binary here (or, rather, in testInChild), so here we have a separate process (not a bare go thread) running.

It never occurred to be we can test all this in a separate go thread (rather than a process). Let me give it a try :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I quickly tried that and it did't work. I'll add it to TODO and merge this as is for now, since it's just a test case.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, the runtime.LockOSThread(), as added, is needed so that Go runtime won't suddenly switch the goroutine to other OS thread while we run the test.

Practically, for users of this package, it means we need to change capabilities (in a locked OS thread) and then re-exec (which is what runc does).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I was looking at capabilities being a per thread attribute and scratching my beard 😂 sorry it didn't work out, but at least now you know!

}

func TestGetBound(t *testing.T) {
if runtime.GOOS != "linux" {
_, err := GetBound(Cap(0))
if err == nil {
t.Error(runtime.GOOS, ": want error, got nil")
}
return
}

last, err := LastCap()
if err != nil {
t.Fatalf("LastCap: %v", err)
}
for i := Cap(0); i < Cap(63); i++ {
wantErr := i > last
set, err := GetBound(i)
t.Logf("GetBound(%q): %v, %v", i, set, err)
if wantErr && err == nil {
t.Errorf("GetBound(%q): want err, got nil", i)
} else if !wantErr && err != nil {
t.Errorf("GetBound(%q): want nil, got error %v", i, err)
}
}
}

func TestDropBound(t *testing.T) {
if runtime.GOOS != "linux" {
err := DropBound(Cap(0))
if err == nil {
t.Error(runtime.GOOS, ": want error, got nil")
}
return
}

requirePCapSet(t)
out := testInChild(t, childDropBound)
t.Logf("output from child:\n%s", out)
}

func childDropBound() {
runtime.LockOSThread()
log.SetFlags(log.Lshortfile)

for i := Cap(2); i < Cap(4); i++ {
err := DropBound(i)
if err != nil {
log.Fatalf("DropBound(%q): want nil, got error %v", i, err)
}
set, err := GetBound(i)
if err != nil {
log.Fatalf("GetBound(%q): want nil, got error %v", i, err)
}
if set {
log.Fatalf("GetBound(%q): want false, got true", i)
}
}

os.Exit(0)
}
Loading