Skip to content

Commit

Permalink
Merge pull request #176 from kolyshkin/cap-ambient
Browse files Browse the repository at this point in the history
capability: add separate ambient and bound API
  • Loading branch information
kolyshkin authored Nov 7, 2024
2 parents 4e88a80 + c1ade77 commit 638aa7c
Show file tree
Hide file tree
Showing 5 changed files with 278 additions and 29 deletions.
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
}
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)
}

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

0 comments on commit 638aa7c

Please sign in to comment.