From c9a8d67d7ea94ba488d6eb6beb90e70f7f951d6f Mon Sep 17 00:00:00 2001 From: Kir Kolyshkin Date: Tue, 23 Jul 2024 17:03:12 -0700 Subject: [PATCH 1/2] Move capVers out of init Currently, capVers is initialized in func init, but its value is only used from NewPid*. Let's use sync.OnceValues for lazy initialization. While at it, stop ignoring syscall return value. Signed-off-by: Kir Kolyshkin --- capability_linux.go | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/capability_linux.go b/capability_linux.go index baee24d..8334530 100644 --- a/capability_linux.go +++ b/capability_linux.go @@ -13,6 +13,7 @@ import ( "io" "os" "strings" + "sync" "syscall" ) @@ -22,16 +23,9 @@ const ( linuxCapVer3 = 0x20080522 ) -var ( - capVers uint32 - capLastCap Cap -) +var capLastCap Cap func init() { - var hdr capHeader - _ = capget(&hdr, nil) - capVers = hdr.version - if initLastCap() == nil { CAP_LAST_CAP = capLastCap if capLastCap > 31 { @@ -96,18 +90,27 @@ func mkString(c Capabilities, max CapType) (ret string) { return } -func newPid(pid int) (c Capabilities, err error) { - switch capVers { - case 0: - err = errors.New("unable to get capability version from the kernel") +var capVersion = sync.OnceValues(func() (uint32, error) { + var hdr capHeader + err := capget(&hdr, nil) + return hdr.version, err +}) + +func newPid(pid int) (c Capabilities, retErr error) { + ver, err := capVersion() + if err != nil { + retErr = fmt.Errorf("unable to get capability version from the kernel: %w", err) + return + } + switch ver { case linuxCapVer1, linuxCapVer2: - err = errors.New("old/unsupported capability version (kernel older than 2.6.26?)") + retErr = errors.New("old/unsupported capability version (kernel older than 2.6.26?)") default: // Either linuxCapVer3, or an unknown/future version such as v4. // In the latter case, we fall back to v3 hoping the kernel is // backward-compatible to v3. p := new(capsV3) - p.hdr.version = capVers + p.hdr.version = ver p.hdr.pid = int32(pid) c = p } From 58343c8d0d09e37700d457496fa83d9f78c2df96 Mon Sep 17 00:00:00 2001 From: Kir Kolyshkin Date: Tue, 23 Jul 2024 17:49:36 -0700 Subject: [PATCH 2/2] Remove init and CAP_LAST_CAP, add LastCap One (minor) issue with this package is it has func init which reads a file in /proc, making the start of any program which imports the package a bit slower. Let's switch to lazy initialization, i.e. only read the file when needed. Unfortunately, this can not be done in a non-disruptive manner, since we have a public variable CAP_LAST_CAP. So, this is a disruptive change, and anyone who's using CAP_LAST_CAP should change their code to something like this: last, err := capability.LastCap() if err != nil { return err } Also, add a test case for LastCap. Reported-by: ningmingxiao Signed-off-by: Kir Kolyshkin --- capability_linux.go | 66 ++++++++++++++++++++++------------------ capability_linux_test.go | 29 ++++++++++++++++++ capability_test.go | 12 +++++--- enum.go | 7 ----- 4 files changed, 73 insertions(+), 41 deletions(-) create mode 100644 capability_linux_test.go diff --git a/capability_linux.go b/capability_linux.go index 8334530..994bb13 100644 --- a/capability_linux.go +++ b/capability_linux.go @@ -12,6 +12,7 @@ import ( "fmt" "io" "os" + "strconv" "strings" "sync" "syscall" @@ -23,43 +24,42 @@ const ( linuxCapVer3 = 0x20080522 ) -var capLastCap Cap - -func init() { - if initLastCap() == nil { - CAP_LAST_CAP = capLastCap - if capLastCap > 31 { - capUpperMask = (uint32(1) << (uint(capLastCap) - 31)) - 1 - } else { - capUpperMask = 0 - } - } -} - -func initLastCap() error { - if capLastCap != 0 { - return nil - } - +// LastCap returns highest valid capability of the running kernel. +var LastCap = sync.OnceValues(func() (Cap, error) { f, err := os.Open("/proc/sys/kernel/cap_last_cap") if err != nil { - return err + return 0, err } - defer f.Close() - var b []byte = make([]byte, 11) - _, err = f.Read(b) + buf := make([]byte, 11) + l, err := f.Read(buf) + f.Close() if err != nil { - return err + return 0, err } + buf = buf[:l] - fmt.Sscanf(string(b), "%d", &capLastCap) + last, err := strconv.Atoi(strings.TrimSpace(string(buf))) + if err != nil { + return 0, err + } + return Cap(last), nil +}) - return nil +func capUpperMask() uint32 { + last, err := LastCap() + if err != nil || last < 32 { + return 0 + } + return (uint32(1) << (uint(last) - 31)) - 1 } func mkStringCap(c Capabilities, which CapType) (ret string) { - for i, first := Cap(0), true; i <= CAP_LAST_CAP; i++ { + last, err := LastCap() + if err != nil { + return "" + } + for i, first := Cap(0), true; i <= last; i++ { if !c.Get(which, i) { continue } @@ -179,7 +179,8 @@ func (c *capsV3) Full(which CapType) bool { if (data[0] & 0xffffffff) != 0xffffffff { return false } - return (data[1] & capUpperMask) == capUpperMask + mask := capUpperMask() + return (data[1] & mask) == mask } func (c *capsV3) Set(which CapType, caps ...Cap) { @@ -327,6 +328,10 @@ func (c *capsV3) Load() (err error) { } func (c *capsV3) Apply(kind CapType) (err error) { + last, err := LastCap() + if err != nil { + return err + } if kind&BOUNDS == BOUNDS { var data [2]capData err = capget(&c.hdr, &data[0]) @@ -334,7 +339,7 @@ func (c *capsV3) Apply(kind CapType) (err error) { return } if (1<= 2.6.32, and + // - CAP_MAC_ADMIN (33) was added in 2.6.25; + // - CAP_SYSLOG (34) was added in 2.6.38; + // - CAP_CHECKPOINT_RESTORE (40) was added in 5.9, and it is + // the last added capability as of today (July 2024); + // LastCap return value should be between minCap and maxCap. + minCap := CAP_MAC_ADMIN + maxCap := CAP_CHECKPOINT_RESTORE + if last < minCap { + t.Fatalf("LastCap returned %d (%s), expected >= %d (%s)", + last, last, minCap, minCap) + } + if last > maxCap { + t.Fatalf("LastCap returned %d, expected <= %d (%s). Package needs to be updated.", + last, maxCap, maxCap) + } +} diff --git a/capability_test.go b/capability_test.go index f974086..f44e9cf 100644 --- a/capability_test.go +++ b/capability_test.go @@ -43,6 +43,10 @@ func TestState(t *testing.T) { } } + last, err := LastCap() + if err != nil { + t.Fatal(err) + } capf := new(capsFile) capf.data.version = 2 for _, tc := range []struct { @@ -51,9 +55,9 @@ func TestState(t *testing.T) { sets CapType max Cap }{ - {"v3", new(capsV3), EFFECTIVE | PERMITTED | BOUNDING, CAP_LAST_CAP}, + {"v3", new(capsV3), EFFECTIVE | PERMITTED | BOUNDING, last}, {"file_v1", new(capsFile), EFFECTIVE | PERMITTED, CAP_AUDIT_CONTROL}, - {"file_v2", capf, EFFECTIVE | PERMITTED, CAP_LAST_CAP}, + {"file_v2", capf, EFFECTIVE | PERMITTED, last}, } { testEmpty(tc.name, tc.c, tc.sets) tc.c.Fill(CAPS | BOUNDS) @@ -62,14 +66,14 @@ func TestState(t *testing.T) { tc.c.Clear(CAPS | BOUNDS) testEmpty(tc.name, tc.c, tc.sets) for i := CapType(1); i <= BOUNDING; i <<= 1 { - for j := Cap(0); j <= CAP_LAST_CAP; j++ { + for j := Cap(0); j <= last; j++ { tc.c.Set(i, j) } } testFull(tc.name, tc.c, tc.sets) testGet(tc.name, tc.c, tc.sets, tc.max) for i := CapType(1); i <= BOUNDING; i <<= 1 { - for j := Cap(0); j <= CAP_LAST_CAP; j++ { + for j := Cap(0); j <= last; j++ { tc.c.Unset(i, j) } } diff --git a/enum.go b/enum.go index ad10785..27c89a6 100644 --- a/enum.go +++ b/enum.go @@ -300,10 +300,3 @@ const ( // Introduced in kernel 5.9 CAP_CHECKPOINT_RESTORE = Cap(40) ) - -var ( - // Highest valid capability of the running kernel. - CAP_LAST_CAP = Cap(63) - - capUpperMask = ^uint32(0) -)