From a8ebe60206af1af33ec8c339c29f056625168517 Mon Sep 17 00:00:00 2001 From: Kir Kolyshkin Date: Mon, 26 Oct 2026 13:27:12 -0700 Subject: [PATCH] capability: Load: read status of the current thread Capabilities (including ambient ones) are per-thread, and capget(2) with pid of 0, as well as Apply, operate on the calling thread. Yet, Load reads ambient and bounding sets from /proc/self/status, which describes the thread group leader (the main thread). As a result, if Load is called from a thread other than the main one (e.g. a goroutine which did runtime.LockOSThread), it returns ambient and bounding sets of the main thread, not the current one. This is the cause of occasional TestAmbientCapSet failures in CI: capability_test.go:171: Get(AMBIENT, kill): want true, got false Fix this by reading /proc/thread-self/status, with fallbacks to /proc/self/task//status (for pre-3.17 kernels) and /proc/self/status (when our pid namespace differs from that of /proc), similar to what mountinfo does. Also, lock the OS thread in Load so that capget and the status file refer to the same thread. To make the test failure deterministic rather than occasional, lock the main goroutine of a test helper process to the main thread, so that tests never run on it. Signed-off-by: Kir Kolyshkin --- capability/CHANGELOG.md | 8 ++++++++ capability/capability_linux.go | 35 +++++++++++++++++++++++++++++----- capability/capability_test.go | 10 ++++++++++ 3 files changed, 48 insertions(+), 5 deletions(-) diff --git a/capability/CHANGELOG.md b/capability/CHANGELOG.md index 1dd54ad..23614c9 100644 --- a/capability/CHANGELOG.md +++ b/capability/CHANGELOG.md @@ -5,6 +5,13 @@ from https://github.com/syndtr/gocapability/commit/42c35b4376354fd5. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Fixed +* [Load] now reads ambient and bounding capabilities of the current thread, + rather than of the main thread. Before this change, when called from a + thread other than the main one, it could return wrong values. (#273) + ## [0.5.0] - 2026-10-23 ### Breaking changes @@ -111,6 +118,7 @@ This is an initial release since the fork. [ListKnown]: https://pkg.go.dev/github.com/moby/sys/capability#ListKnown [ListSupported]: https://pkg.go.dev/github.com/moby/sys/capability#ListSupported [List]: https://pkg.go.dev/github.com/moby/sys/capability#List +[Load]: https://pkg.go.dev/github.com/moby/sys/capability#Capabilities.Load [NewFile2]: https://pkg.go.dev/github.com/moby/sys/capability#NewFile2 [NewFile]: https://pkg.go.dev/github.com/moby/sys/capability#NewFile [NewPid2]: https://pkg.go.dev/github.com/moby/sys/capability#NewPid2 diff --git a/capability/capability_linux.go b/capability/capability_linux.go index af40f85..7f40302 100644 --- a/capability/capability_linux.go +++ b/capability/capability_linux.go @@ -13,6 +13,8 @@ import ( "fmt" "io" "os" + "runtime" + "strconv" "strings" "sync" "syscall" @@ -338,18 +340,41 @@ func (c *capsV3) String() (ret string) { return mkString(c, BOUNDING) } +// openStatus opens the status file of the current thread (rather than of +// the thread group leader, as /proc/self/status is), since capabilities +// are per-thread. +func openStatus() (*os.File, error) { + f, err := os.Open("/proc/thread-self/status") + if !errors.Is(err, os.ErrNotExist) { + return f, err + } + // Pre-3.17 kernel, no /proc/thread-self. + f, err = os.Open("/proc/self/task/" + strconv.Itoa(syscall.Gettid()) + "/status") + if !errors.Is(err, os.ErrNotExist) { + return f, err + } + // Our pid namespace doesn't match the one of /proc mount. + return os.Open("/proc/self/status") +} + func (c *capsV3) Load() (err error) { + if c.hdr.pid == 0 { + // Make sure capget and status file refer to the same thread. + runtime.LockOSThread() + defer runtime.UnlockOSThread() + } + err = capget(&c.hdr, &c.data[0]) if err != nil { return } - path := "/proc/self/status" - if c.hdr.pid != 0 { - path = fmt.Sprintf("/proc/%d/status", c.hdr.pid) + var f *os.File + if c.hdr.pid == 0 { + f, err = openStatus() + } else { + f, err = os.Open(fmt.Sprintf("/proc/%d/status", c.hdr.pid)) } - - f, err := os.Open(path) if err != nil { return } diff --git a/capability/capability_test.go b/capability/capability_test.go index fa6d076..2f48c8f 100644 --- a/capability/capability_test.go +++ b/capability/capability_test.go @@ -16,6 +16,16 @@ import ( . "github.com/moby/sys/capability" ) +func init() { + if os.Getenv("GO_WANT_HELPER_PROCESS") == "1" { + // Lock the main goroutine to the main thread, so that tests + // (which run in separate goroutines) never run on the main + // thread. This ensures per-thread capabilities are handled + // properly. + runtime.LockOSThread() + } +} + func requirePCapSet(t testing.TB) { t.Helper() pid, err := NewPid2(0)