selinux: use safe procfs API for labels

Due to the sensitive nature of these fixes, it was not possible to
submit these upstream and vendor the upstream library. Instead, this
patch uses a fork of github.com/opencontainers/selinux, branched at
commit opencontainers/selinux@879a755db5.

In order to permit downstreams to build with this patched version, a
snapshot of the forked version has been included in
internal/third_party/selinux. Note that since we use "go mod vendor",
the patched code is usable even without being "go get"-able. Once the
embargo for this issue is lifted we can submit the patches upstream and
switch back to a proper upstream go.mod entry.

Also, this requires us to temporarily disable the CI job we have that
disallows "replace" directives.

Fixes: GHSA-cgrx-mc8f-2prm CVE-2025-52881
Signed-off-by: Aleksa Sarai <cyphar@cyphar.com>
This commit is contained in:
Aleksa Sarai
2025-10-07 22:48:50 +11:00
parent d40b3439a9
commit ed6b1693b8
40 changed files with 4964 additions and 113 deletions
+3 -3
View File
@@ -18,7 +18,7 @@ var validOptions = map[string]bool{
"level": true,
}
var ErrIncompatibleLabel = errors.New("Bad SELinux option z and Z can not be used together")
var ErrIncompatibleLabel = errors.New("bad SELinux option: z and Z can not be used together")
// InitLabels returns the process label and file labels to be used within
// the container. A list of options can be passed into this function to alter
@@ -52,11 +52,11 @@ func InitLabels(options []string) (plabel string, mlabel string, retErr error) {
return "", selinux.PrivContainerMountLabel(), nil
}
if i := strings.Index(opt, ":"); i == -1 {
return "", "", fmt.Errorf("Bad label option %q, valid options 'disable' or \n'user, role, level, type, filetype' followed by ':' and a value", opt)
return "", "", fmt.Errorf("bad label option %q, valid options 'disable' or \n'user, role, level, type, filetype' followed by ':' and a value", opt)
}
con := strings.SplitN(opt, ":", 2)
if !validOptions[con[0]] {
return "", "", fmt.Errorf("Bad label option %q, valid options 'disable, user, role, level, type, filetype'", con[0])
return "", "", fmt.Errorf("bad label option %q, valid options 'disable, user, role, level, type, filetype'", con[0])
}
if con[0] == "filetype" {
mcon["type"] = con[1]
+5 -5
View File
@@ -153,7 +153,7 @@ func CalculateGlbLub(sourceRange, targetRange string) (string, error) {
// of the program is finished to guarantee another goroutine does not migrate to the current
// thread before execution is complete.
func SetExecLabel(label string) error {
return writeCon(attrPath("exec"), label)
return writeConThreadSelf("attr/exec", label)
}
// SetTaskLabel sets the SELinux label for the current thread, or an error.
@@ -161,7 +161,7 @@ func SetExecLabel(label string) error {
// be wrapped in runtime.LockOSThread()/runtime.UnlockOSThread() to guarantee
// the current thread does not run in a new mislabeled thread.
func SetTaskLabel(label string) error {
return writeCon(attrPath("current"), label)
return writeConThreadSelf("attr/current", label)
}
// SetSocketLabel takes a process label and tells the kernel to assign the
@@ -170,12 +170,12 @@ func SetTaskLabel(label string) error {
// the socket is created to guarantee another goroutine does not migrate
// to the current thread before execution is complete.
func SetSocketLabel(label string) error {
return writeCon(attrPath("sockcreate"), label)
return writeConThreadSelf("attr/sockcreate", label)
}
// SocketLabel retrieves the current socket label setting
func SocketLabel() (string, error) {
return readCon(attrPath("sockcreate"))
return readConThreadSelf("attr/sockcreate")
}
// PeerLabel retrieves the label of the client on the other side of a socket
@@ -198,7 +198,7 @@ func SetKeyLabel(label string) error {
// KeyLabel retrieves the current kernel keyring label setting
func KeyLabel() (string, error) {
return readCon("/proc/self/attr/keycreate")
return keyLabel()
}
// Get returns the Context as a string
+201 -97
View File
@@ -17,8 +17,11 @@ import (
"strings"
"sync"
"github.com/opencontainers/selinux/pkg/pwalkdir"
"github.com/cyphar/filepath-securejoin/pathrs-lite"
"github.com/cyphar/filepath-securejoin/pathrs-lite/procfs"
"golang.org/x/sys/unix"
"github.com/opencontainers/selinux/pkg/pwalkdir"
)
const (
@@ -73,10 +76,6 @@ var (
mcsList: make(map[string]bool),
}
// for attrPath()
attrPathOnce sync.Once
haveThreadSelf bool
// for policyRoot()
policyRootOnce sync.Once
policyRootVal string
@@ -256,42 +255,6 @@ func readConfig(target string) string {
return ""
}
func isProcHandle(fh *os.File) error {
var buf unix.Statfs_t
for {
err := unix.Fstatfs(int(fh.Fd()), &buf)
if err == nil {
break
}
if err != unix.EINTR {
return &os.PathError{Op: "fstatfs", Path: fh.Name(), Err: err}
}
}
if buf.Type != unix.PROC_SUPER_MAGIC {
return fmt.Errorf("file %q is not on procfs", fh.Name())
}
return nil
}
func readCon(fpath string) (string, error) {
if fpath == "" {
return "", ErrEmptyPath
}
in, err := os.Open(fpath)
if err != nil {
return "", err
}
defer in.Close()
if err := isProcHandle(in); err != nil {
return "", err
}
return readConFd(in)
}
func readConFd(in *os.File) (string, error) {
data, err := io.ReadAll(in)
if err != nil {
@@ -300,6 +263,181 @@ func readConFd(in *os.File) (string, error) {
return string(bytes.TrimSuffix(data, []byte{0})), nil
}
func writeConFd(out *os.File, val string) error {
var err error
if val != "" {
_, err = out.Write([]byte(val))
} else {
_, err = out.Write(nil)
}
return err
}
// openProcThreadSelf is a small wrapper around [OpenThreadSelf] and
// [pathrs.Reopen] to make "one-shot opens" slightly more ergonomic. The
// provided mode must be os.O_* flags to indicate what mode the returned file
// should be opened with (flags like os.O_CREAT and os.O_EXCL are not
// supported).
//
// If no error occurred, the returned handle is guaranteed to be exactly
// /proc/thread-self/<subpath> with no tricky mounts or symlinks causing you to
// operate on an unexpected path (with some caveats on pre-openat2 or
// pre-fsopen kernels).
//
// [OpenThreadSelf]: https://pkg.go.dev/github.com/cyphar/filepath-securejoin/pathrs-lite/procfs#Handle.OpenThreadSelf
func openProcThreadSelf(subpath string, mode int) (*os.File, procfs.ProcThreadSelfCloser, error) {
if subpath == "" {
return nil, nil, ErrEmptyPath
}
proc, err := procfs.OpenProcRoot()
if err != nil {
return nil, nil, err
}
defer proc.Close()
handle, closer, err := proc.OpenThreadSelf(subpath)
if err != nil {
return nil, nil, fmt.Errorf("open /proc/thread-self/%s handle: %w", subpath, err)
}
defer handle.Close() // we will return a re-opened handle
file, err := pathrs.Reopen(handle, mode)
if err != nil {
closer()
return nil, nil, fmt.Errorf("reopen /proc/thread-self/%s handle (%#x): %w", subpath, mode, err)
}
return file, closer, nil
}
// Read the contents of /proc/thread-self/<fpath>.
func readConThreadSelf(fpath string) (string, error) {
in, closer, err := openProcThreadSelf(fpath, os.O_RDONLY|unix.O_CLOEXEC)
if err != nil {
return "", err
}
defer closer()
defer in.Close()
return readConFd(in)
}
// Write <val> to /proc/thread-self/<fpath>.
func writeConThreadSelf(fpath, val string) error {
if val == "" {
if !getEnabled() {
return nil
}
}
out, closer, err := openProcThreadSelf(fpath, os.O_WRONLY|unix.O_CLOEXEC)
if err != nil {
return err
}
defer closer()
defer out.Close()
return writeConFd(out, val)
}
// openProcSelf is a small wrapper around [OpenSelf] and [pathrs.Reopen] to
// make "one-shot opens" slightly more ergonomic. The provided mode must be
// os.O_* flags to indicate what mode the returned file should be opened with
// (flags like os.O_CREAT and os.O_EXCL are not supported).
//
// If no error occurred, the returned handle is guaranteed to be exactly
// /proc/self/<subpath> with no tricky mounts or symlinks causing you to
// operate on an unexpected path (with some caveats on pre-openat2 or
// pre-fsopen kernels).
//
// [OpenSelf]: https://pkg.go.dev/github.com/cyphar/filepath-securejoin/pathrs-lite/procfs#Handle.OpenSelf
func openProcSelf(subpath string, mode int) (*os.File, error) {
if subpath == "" {
return nil, ErrEmptyPath
}
proc, err := procfs.OpenProcRoot()
if err != nil {
return nil, err
}
defer proc.Close()
handle, err := proc.OpenSelf(subpath)
if err != nil {
return nil, fmt.Errorf("open /proc/self/%s handle: %w", subpath, err)
}
defer handle.Close() // we will return a re-opened handle
file, err := pathrs.Reopen(handle, mode)
if err != nil {
return nil, fmt.Errorf("reopen /proc/self/%s handle (%#x): %w", subpath, mode, err)
}
return file, nil
}
// Read the contents of /proc/self/<fpath>.
func readConSelf(fpath string) (string, error) {
in, err := openProcSelf(fpath, os.O_RDONLY|unix.O_CLOEXEC)
if err != nil {
return "", err
}
defer in.Close()
return readConFd(in)
}
// Write <val> to /proc/self/<fpath>.
func writeConSelf(fpath, val string) error {
if val == "" {
if !getEnabled() {
return nil
}
}
out, err := openProcSelf(fpath, os.O_WRONLY|unix.O_CLOEXEC)
if err != nil {
return err
}
defer out.Close()
return writeConFd(out, val)
}
// openProcPid is a small wrapper around [OpenPid] and [pathrs.Reopen] to make
// "one-shot opens" slightly more ergonomic. The provided mode must be os.O_*
// flags to indicate what mode the returned file should be opened with (flags
// like os.O_CREAT and os.O_EXCL are not supported).
//
// If no error occurred, the returned handle is guaranteed to be exactly
// /proc/self/<subpath> with no tricky mounts or symlinks causing you to
// operate on an unexpected path (with some caveats on pre-openat2 or
// pre-fsopen kernels).
//
// [OpenPid]: https://pkg.go.dev/github.com/cyphar/filepath-securejoin/pathrs-lite/procfs#Handle.OpenPid
func openProcPid(pid int, subpath string, mode int) (*os.File, error) {
if subpath == "" {
return nil, ErrEmptyPath
}
proc, err := procfs.OpenProcRoot()
if err != nil {
return nil, err
}
defer proc.Close()
handle, err := proc.OpenPid(pid, subpath)
if err != nil {
return nil, fmt.Errorf("open /proc/%d/%s handle: %w", pid, subpath, err)
}
defer handle.Close() // we will return a re-opened handle
file, err := pathrs.Reopen(handle, mode)
if err != nil {
return nil, fmt.Errorf("reopen /proc/%d/%s handle (%#x): %w", pid, subpath, mode, err)
}
return file, nil
}
// classIndex returns the int index for an object class in the loaded policy,
// or -1 and an error
func classIndex(class string) (int, error) {
@@ -393,78 +531,34 @@ func lFileLabel(fpath string) (string, error) {
}
func setFSCreateLabel(label string) error {
return writeCon(attrPath("fscreate"), label)
return writeConThreadSelf("attr/fscreate", label)
}
// fsCreateLabel returns the default label the kernel which the kernel is using
// for file system objects created by this task. "" indicates default.
func fsCreateLabel() (string, error) {
return readCon(attrPath("fscreate"))
return readConThreadSelf("attr/fscreate")
}
// currentLabel returns the SELinux label of the current process thread, or an error.
func currentLabel() (string, error) {
return readCon(attrPath("current"))
return readConThreadSelf("attr/current")
}
// pidLabel returns the SELinux label of the given pid, or an error.
func pidLabel(pid int) (string, error) {
return readCon(fmt.Sprintf("/proc/%d/attr/current", pid))
it, err := openProcPid(pid, "attr/current", os.O_RDONLY|unix.O_CLOEXEC)
if err != nil {
return "", nil
}
defer it.Close()
return readConFd(it)
}
// ExecLabel returns the SELinux label that the kernel will use for any programs
// that are executed by the current process thread, or an error.
func execLabel() (string, error) {
return readCon(attrPath("exec"))
}
func writeCon(fpath, val string) error {
if fpath == "" {
return ErrEmptyPath
}
if val == "" {
if !getEnabled() {
return nil
}
}
out, err := os.OpenFile(fpath, os.O_WRONLY, 0)
if err != nil {
return err
}
defer out.Close()
if err := isProcHandle(out); err != nil {
return err
}
if val != "" {
_, err = out.Write([]byte(val))
} else {
_, err = out.Write(nil)
}
if err != nil {
return err
}
return nil
}
func attrPath(attr string) string {
// Linux >= 3.17 provides this
const threadSelfPrefix = "/proc/thread-self/attr"
attrPathOnce.Do(func() {
st, err := os.Stat(threadSelfPrefix)
if err == nil && st.Mode().IsDir() {
haveThreadSelf = true
}
})
if haveThreadSelf {
return filepath.Join(threadSelfPrefix, attr)
}
return filepath.Join("/proc/self/task", strconv.Itoa(unix.Gettid()), "attr", attr)
return readConThreadSelf("exec")
}
// canonicalizeContext takes a context string and writes it to the kernel
@@ -728,19 +822,29 @@ func peerLabel(fd uintptr) (string, error) {
// setKeyLabel takes a process label and tells the kernel to assign the
// label to the next kernel keyring that gets created
func setKeyLabel(label string) error {
err := writeCon("/proc/self/attr/keycreate", label)
// Rather than using /proc/thread-self, we want to use /proc/self to
// operate on the thread-group leader.
err := writeConSelf("attr/keycreate", label)
if errors.Is(err, os.ErrNotExist) {
return nil
}
if label == "" && errors.Is(err, os.ErrPermission) {
return nil
}
if errors.Is(err, unix.EACCES) && unix.Getuid() != unix.Gettid() {
if errors.Is(err, unix.EACCES) && unix.Getpid() != unix.Gettid() {
return ErrNotTGLeader
}
return err
}
// KeyLabel retrieves the current kernel keyring label setting for this
// thread-group.
func keyLabel() (string, error) {
// Rather than using /proc/thread-self, we want to use /proc/self to
// operate on the thread-group leader.
return readConSelf("attr/keycreate")
}
// get returns the Context as a string
func (c Context) get() string {
if l := c["level"]; l != "" {
+6 -2
View File
@@ -7,11 +7,11 @@ func attrPath(string) string {
return ""
}
func readCon(string) (string, error) {
func readConThreadSelf(string) (string, error) {
return "", nil
}
func writeCon(string, string) error {
func writeConThreadSelf(string, string) error {
return nil
}
@@ -81,6 +81,10 @@ func setKeyLabel(string) error {
return nil
}
func keyLabel() (string, error) {
return "", nil
}
func (c Context) get() string {
return ""
}