mirror of
https://github.com/ceph/ceph-csi.git
synced 2024-12-22 13:00:19 +00:00
e5d9b68d36
Bumps the golang-dependencies group with 1 update: [golang.org/x/crypto](https://github.com/golang/crypto). Updates `golang.org/x/crypto` from 0.16.0 to 0.17.0 - [Commits](https://github.com/golang/crypto/compare/v0.16.0...v0.17.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-dependencies ... Signed-off-by: dependabot[bot] <support@github.com>
1296 lines
29 KiB
Go
1296 lines
29 KiB
Go
package selinux
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"crypto/rand"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"math/big"
|
|
"os"
|
|
"os/user"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/opencontainers/selinux/pkg/pwalkdir"
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
const (
|
|
minSensLen = 2
|
|
contextFile = "/usr/share/containers/selinux/contexts"
|
|
selinuxDir = "/etc/selinux/"
|
|
selinuxUsersDir = "contexts/users"
|
|
defaultContexts = "contexts/default_contexts"
|
|
selinuxConfig = selinuxDir + "config"
|
|
selinuxfsMount = "/sys/fs/selinux"
|
|
selinuxTypeTag = "SELINUXTYPE"
|
|
selinuxTag = "SELINUX"
|
|
xattrNameSelinux = "security.selinux"
|
|
)
|
|
|
|
type selinuxState struct {
|
|
mcsList map[string]bool
|
|
selinuxfs string
|
|
selinuxfsOnce sync.Once
|
|
enabledSet bool
|
|
enabled bool
|
|
sync.Mutex
|
|
}
|
|
|
|
type level struct {
|
|
cats *big.Int
|
|
sens uint
|
|
}
|
|
|
|
type mlsRange struct {
|
|
low *level
|
|
high *level
|
|
}
|
|
|
|
type defaultSECtx struct {
|
|
userRdr io.Reader
|
|
verifier func(string) error
|
|
defaultRdr io.Reader
|
|
user, level, scon string
|
|
}
|
|
|
|
type levelItem byte
|
|
|
|
const (
|
|
sensitivity levelItem = 's'
|
|
category levelItem = 'c'
|
|
)
|
|
|
|
var (
|
|
readOnlyFileLabel string
|
|
state = selinuxState{
|
|
mcsList: make(map[string]bool),
|
|
}
|
|
|
|
// for attrPath()
|
|
attrPathOnce sync.Once
|
|
haveThreadSelf bool
|
|
|
|
// for policyRoot()
|
|
policyRootOnce sync.Once
|
|
policyRootVal string
|
|
|
|
// for label()
|
|
loadLabelsOnce sync.Once
|
|
labels map[string]string
|
|
)
|
|
|
|
func policyRoot() string {
|
|
policyRootOnce.Do(func() {
|
|
policyRootVal = filepath.Join(selinuxDir, readConfig(selinuxTypeTag))
|
|
})
|
|
|
|
return policyRootVal
|
|
}
|
|
|
|
func (s *selinuxState) setEnable(enabled bool) bool {
|
|
s.Lock()
|
|
defer s.Unlock()
|
|
s.enabledSet = true
|
|
s.enabled = enabled
|
|
return s.enabled
|
|
}
|
|
|
|
func (s *selinuxState) getEnabled() bool {
|
|
s.Lock()
|
|
enabled := s.enabled
|
|
enabledSet := s.enabledSet
|
|
s.Unlock()
|
|
if enabledSet {
|
|
return enabled
|
|
}
|
|
|
|
enabled = false
|
|
if fs := getSelinuxMountPoint(); fs != "" {
|
|
if con, _ := CurrentLabel(); con != "kernel" {
|
|
enabled = true
|
|
}
|
|
}
|
|
return s.setEnable(enabled)
|
|
}
|
|
|
|
// setDisabled disables SELinux support for the package
|
|
func setDisabled() {
|
|
state.setEnable(false)
|
|
}
|
|
|
|
func verifySELinuxfsMount(mnt string) bool {
|
|
var buf unix.Statfs_t
|
|
for {
|
|
err := unix.Statfs(mnt, &buf)
|
|
if err == nil {
|
|
break
|
|
}
|
|
if err == unix.EAGAIN || err == unix.EINTR { //nolint:errorlint // unix errors are bare
|
|
continue
|
|
}
|
|
return false
|
|
}
|
|
|
|
if uint32(buf.Type) != uint32(unix.SELINUX_MAGIC) {
|
|
return false
|
|
}
|
|
if (buf.Flags & unix.ST_RDONLY) != 0 {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func findSELinuxfs() string {
|
|
// fast path: check the default mount first
|
|
if verifySELinuxfsMount(selinuxfsMount) {
|
|
return selinuxfsMount
|
|
}
|
|
|
|
// check if selinuxfs is available before going the slow path
|
|
fs, err := os.ReadFile("/proc/filesystems")
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
if !bytes.Contains(fs, []byte("\tselinuxfs\n")) {
|
|
return ""
|
|
}
|
|
|
|
// slow path: try to find among the mounts
|
|
f, err := os.Open("/proc/self/mountinfo")
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
defer f.Close()
|
|
|
|
scanner := bufio.NewScanner(f)
|
|
for {
|
|
mnt := findSELinuxfsMount(scanner)
|
|
if mnt == "" { // error or not found
|
|
return ""
|
|
}
|
|
if verifySELinuxfsMount(mnt) {
|
|
return mnt
|
|
}
|
|
}
|
|
}
|
|
|
|
// findSELinuxfsMount returns a next selinuxfs mount point found,
|
|
// if there is one, or an empty string in case of EOF or error.
|
|
func findSELinuxfsMount(s *bufio.Scanner) string {
|
|
for s.Scan() {
|
|
txt := s.Bytes()
|
|
// The first field after - is fs type.
|
|
// Safe as spaces in mountpoints are encoded as \040
|
|
if !bytes.Contains(txt, []byte(" - selinuxfs ")) {
|
|
continue
|
|
}
|
|
const mPos = 5 // mount point is 5th field
|
|
fields := bytes.SplitN(txt, []byte(" "), mPos+1)
|
|
if len(fields) < mPos+1 {
|
|
continue
|
|
}
|
|
return string(fields[mPos-1])
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func (s *selinuxState) getSELinuxfs() string {
|
|
s.selinuxfsOnce.Do(func() {
|
|
s.selinuxfs = findSELinuxfs()
|
|
})
|
|
|
|
return s.selinuxfs
|
|
}
|
|
|
|
// getSelinuxMountPoint returns the path to the mountpoint of an selinuxfs
|
|
// filesystem or an empty string if no mountpoint is found. Selinuxfs is
|
|
// a proc-like pseudo-filesystem that exposes the SELinux policy API to
|
|
// processes. The existence of an selinuxfs mount is used to determine
|
|
// whether SELinux is currently enabled or not.
|
|
func getSelinuxMountPoint() string {
|
|
return state.getSELinuxfs()
|
|
}
|
|
|
|
// getEnabled returns whether SELinux is currently enabled.
|
|
func getEnabled() bool {
|
|
return state.getEnabled()
|
|
}
|
|
|
|
func readConfig(target string) string {
|
|
in, err := os.Open(selinuxConfig)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
defer in.Close()
|
|
|
|
scanner := bufio.NewScanner(in)
|
|
|
|
for scanner.Scan() {
|
|
line := bytes.TrimSpace(scanner.Bytes())
|
|
if len(line) == 0 {
|
|
// Skip blank lines
|
|
continue
|
|
}
|
|
if line[0] == ';' || line[0] == '#' {
|
|
// Skip comments
|
|
continue
|
|
}
|
|
fields := bytes.SplitN(line, []byte{'='}, 2)
|
|
if len(fields) != 2 {
|
|
continue
|
|
}
|
|
if bytes.Equal(fields[0], []byte(target)) {
|
|
return string(bytes.Trim(fields[1], `"`))
|
|
}
|
|
}
|
|
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 { //nolint:errorlint // unix errors are bare
|
|
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 {
|
|
return "", err
|
|
}
|
|
return string(bytes.TrimSuffix(data, []byte{0})), 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) {
|
|
permpath := fmt.Sprintf("class/%s/index", class)
|
|
indexpath := filepath.Join(getSelinuxMountPoint(), permpath)
|
|
|
|
indexB, err := os.ReadFile(indexpath)
|
|
if err != nil {
|
|
return -1, err
|
|
}
|
|
index, err := strconv.Atoi(string(indexB))
|
|
if err != nil {
|
|
return -1, err
|
|
}
|
|
|
|
return index, nil
|
|
}
|
|
|
|
// lSetFileLabel sets the SELinux label for this path, not following symlinks,
|
|
// or returns an error.
|
|
func lSetFileLabel(fpath string, label string) error {
|
|
if fpath == "" {
|
|
return ErrEmptyPath
|
|
}
|
|
for {
|
|
err := unix.Lsetxattr(fpath, xattrNameSelinux, []byte(label), 0)
|
|
if err == nil {
|
|
break
|
|
}
|
|
if err != unix.EINTR { //nolint:errorlint // unix errors are bare
|
|
return &os.PathError{Op: "lsetxattr", Path: fpath, Err: err}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// setFileLabel sets the SELinux label for this path, following symlinks,
|
|
// or returns an error.
|
|
func setFileLabel(fpath string, label string) error {
|
|
if fpath == "" {
|
|
return ErrEmptyPath
|
|
}
|
|
for {
|
|
err := unix.Setxattr(fpath, xattrNameSelinux, []byte(label), 0)
|
|
if err == nil {
|
|
break
|
|
}
|
|
if err != unix.EINTR { //nolint:errorlint // unix errors are bare
|
|
return &os.PathError{Op: "setxattr", Path: fpath, Err: err}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// fileLabel returns the SELinux label for this path, following symlinks,
|
|
// or returns an error.
|
|
func fileLabel(fpath string) (string, error) {
|
|
if fpath == "" {
|
|
return "", ErrEmptyPath
|
|
}
|
|
|
|
label, err := getxattr(fpath, xattrNameSelinux)
|
|
if err != nil {
|
|
return "", &os.PathError{Op: "getxattr", Path: fpath, Err: err}
|
|
}
|
|
// Trim the NUL byte at the end of the byte buffer, if present.
|
|
if len(label) > 0 && label[len(label)-1] == '\x00' {
|
|
label = label[:len(label)-1]
|
|
}
|
|
return string(label), nil
|
|
}
|
|
|
|
// lFileLabel returns the SELinux label for this path, not following symlinks,
|
|
// or returns an error.
|
|
func lFileLabel(fpath string) (string, error) {
|
|
if fpath == "" {
|
|
return "", ErrEmptyPath
|
|
}
|
|
|
|
label, err := lgetxattr(fpath, xattrNameSelinux)
|
|
if err != nil {
|
|
return "", &os.PathError{Op: "lgetxattr", Path: fpath, Err: err}
|
|
}
|
|
// Trim the NUL byte at the end of the byte buffer, if present.
|
|
if len(label) > 0 && label[len(label)-1] == '\x00' {
|
|
label = label[:len(label)-1]
|
|
}
|
|
return string(label), nil
|
|
}
|
|
|
|
func setFSCreateLabel(label string) error {
|
|
return writeCon(attrPath("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"))
|
|
}
|
|
|
|
// currentLabel returns the SELinux label of the current process thread, or an error.
|
|
func currentLabel() (string, error) {
|
|
return readCon(attrPath("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))
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// canonicalizeContext takes a context string and writes it to the kernel
|
|
// the function then returns the context that the kernel will use. Use this
|
|
// function to check if two contexts are equivalent
|
|
func canonicalizeContext(val string) (string, error) {
|
|
return readWriteCon(filepath.Join(getSelinuxMountPoint(), "context"), val)
|
|
}
|
|
|
|
// computeCreateContext requests the type transition from source to target for
|
|
// class from the kernel.
|
|
func computeCreateContext(source string, target string, class string) (string, error) {
|
|
classidx, err := classIndex(class)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return readWriteCon(filepath.Join(getSelinuxMountPoint(), "create"), fmt.Sprintf("%s %s %d", source, target, classidx))
|
|
}
|
|
|
|
// catsToBitset stores categories in a bitset.
|
|
func catsToBitset(cats string) (*big.Int, error) {
|
|
bitset := new(big.Int)
|
|
|
|
catlist := strings.Split(cats, ",")
|
|
for _, r := range catlist {
|
|
ranges := strings.SplitN(r, ".", 2)
|
|
if len(ranges) > 1 {
|
|
catstart, err := parseLevelItem(ranges[0], category)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
catend, err := parseLevelItem(ranges[1], category)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for i := catstart; i <= catend; i++ {
|
|
bitset.SetBit(bitset, int(i), 1)
|
|
}
|
|
} else {
|
|
cat, err := parseLevelItem(ranges[0], category)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
bitset.SetBit(bitset, int(cat), 1)
|
|
}
|
|
}
|
|
|
|
return bitset, nil
|
|
}
|
|
|
|
// parseLevelItem parses and verifies that a sensitivity or category are valid
|
|
func parseLevelItem(s string, sep levelItem) (uint, error) {
|
|
if len(s) < minSensLen || levelItem(s[0]) != sep {
|
|
return 0, ErrLevelSyntax
|
|
}
|
|
val, err := strconv.ParseUint(s[1:], 10, 32)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return uint(val), nil
|
|
}
|
|
|
|
// parseLevel fills a level from a string that contains
|
|
// a sensitivity and categories
|
|
func (l *level) parseLevel(levelStr string) error {
|
|
lvl := strings.SplitN(levelStr, ":", 2)
|
|
sens, err := parseLevelItem(lvl[0], sensitivity)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse sensitivity: %w", err)
|
|
}
|
|
l.sens = sens
|
|
if len(lvl) > 1 {
|
|
cats, err := catsToBitset(lvl[1])
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse categories: %w", err)
|
|
}
|
|
l.cats = cats
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// rangeStrToMLSRange marshals a string representation of a range.
|
|
func rangeStrToMLSRange(rangeStr string) (*mlsRange, error) {
|
|
r := &mlsRange{}
|
|
l := strings.SplitN(rangeStr, "-", 2)
|
|
|
|
switch len(l) {
|
|
// rangeStr that has a low and a high level, e.g. s4:c0.c1023-s6:c0.c1023
|
|
case 2:
|
|
r.high = &level{}
|
|
if err := r.high.parseLevel(l[1]); err != nil {
|
|
return nil, fmt.Errorf("failed to parse high level %q: %w", l[1], err)
|
|
}
|
|
fallthrough
|
|
// rangeStr that is single level, e.g. s6:c0,c3,c5,c30.c1023
|
|
case 1:
|
|
r.low = &level{}
|
|
if err := r.low.parseLevel(l[0]); err != nil {
|
|
return nil, fmt.Errorf("failed to parse low level %q: %w", l[0], err)
|
|
}
|
|
}
|
|
|
|
if r.high == nil {
|
|
r.high = r.low
|
|
}
|
|
|
|
return r, nil
|
|
}
|
|
|
|
// bitsetToStr takes a category bitset and returns it in the
|
|
// canonical selinux syntax
|
|
func bitsetToStr(c *big.Int) string {
|
|
var str string
|
|
|
|
length := 0
|
|
for i := int(c.TrailingZeroBits()); i < c.BitLen(); i++ {
|
|
if c.Bit(i) == 0 {
|
|
continue
|
|
}
|
|
if length == 0 {
|
|
if str != "" {
|
|
str += ","
|
|
}
|
|
str += "c" + strconv.Itoa(i)
|
|
}
|
|
if c.Bit(i+1) == 1 {
|
|
length++
|
|
continue
|
|
}
|
|
if length == 1 {
|
|
str += ",c" + strconv.Itoa(i)
|
|
} else if length > 1 {
|
|
str += ".c" + strconv.Itoa(i)
|
|
}
|
|
length = 0
|
|
}
|
|
|
|
return str
|
|
}
|
|
|
|
func (l *level) equal(l2 *level) bool {
|
|
if l2 == nil || l == nil {
|
|
return l == l2
|
|
}
|
|
if l2.sens != l.sens {
|
|
return false
|
|
}
|
|
if l2.cats == nil || l.cats == nil {
|
|
return l2.cats == l.cats
|
|
}
|
|
return l.cats.Cmp(l2.cats) == 0
|
|
}
|
|
|
|
// String returns an mlsRange as a string.
|
|
func (m mlsRange) String() string {
|
|
low := "s" + strconv.Itoa(int(m.low.sens))
|
|
if m.low.cats != nil && m.low.cats.BitLen() > 0 {
|
|
low += ":" + bitsetToStr(m.low.cats)
|
|
}
|
|
|
|
if m.low.equal(m.high) {
|
|
return low
|
|
}
|
|
|
|
high := "s" + strconv.Itoa(int(m.high.sens))
|
|
if m.high.cats != nil && m.high.cats.BitLen() > 0 {
|
|
high += ":" + bitsetToStr(m.high.cats)
|
|
}
|
|
|
|
return low + "-" + high
|
|
}
|
|
|
|
func max(a, b uint) uint {
|
|
if a > b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
func min(a, b uint) uint {
|
|
if a < b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
// calculateGlbLub computes the glb (greatest lower bound) and lub (least upper bound)
|
|
// of a source and target range.
|
|
// The glblub is calculated as the greater of the low sensitivities and
|
|
// the lower of the high sensitivities and the and of each category bitset.
|
|
func calculateGlbLub(sourceRange, targetRange string) (string, error) {
|
|
s, err := rangeStrToMLSRange(sourceRange)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
t, err := rangeStrToMLSRange(targetRange)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if s.high.sens < t.low.sens || t.high.sens < s.low.sens {
|
|
/* these ranges have no common sensitivities */
|
|
return "", ErrIncomparable
|
|
}
|
|
|
|
outrange := &mlsRange{low: &level{}, high: &level{}}
|
|
|
|
/* take the greatest of the low */
|
|
outrange.low.sens = max(s.low.sens, t.low.sens)
|
|
|
|
/* take the least of the high */
|
|
outrange.high.sens = min(s.high.sens, t.high.sens)
|
|
|
|
/* find the intersecting categories */
|
|
if s.low.cats != nil && t.low.cats != nil {
|
|
outrange.low.cats = new(big.Int)
|
|
outrange.low.cats.And(s.low.cats, t.low.cats)
|
|
}
|
|
if s.high.cats != nil && t.high.cats != nil {
|
|
outrange.high.cats = new(big.Int)
|
|
outrange.high.cats.And(s.high.cats, t.high.cats)
|
|
}
|
|
|
|
return outrange.String(), nil
|
|
}
|
|
|
|
func readWriteCon(fpath string, val string) (string, error) {
|
|
if fpath == "" {
|
|
return "", ErrEmptyPath
|
|
}
|
|
f, err := os.OpenFile(fpath, os.O_RDWR, 0)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer f.Close()
|
|
|
|
_, err = f.Write([]byte(val))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return readConFd(f)
|
|
}
|
|
|
|
// peerLabel retrieves the label of the client on the other side of a socket
|
|
func peerLabel(fd uintptr) (string, error) {
|
|
l, err := unix.GetsockoptString(int(fd), unix.SOL_SOCKET, unix.SO_PEERSEC)
|
|
if err != nil {
|
|
return "", &os.PathError{Op: "getsockopt", Path: "fd " + strconv.Itoa(int(fd)), Err: err}
|
|
}
|
|
return l, nil
|
|
}
|
|
|
|
// 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)
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return nil
|
|
}
|
|
if label == "" && errors.Is(err, os.ErrPermission) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
// get returns the Context as a string
|
|
func (c Context) get() string {
|
|
if l := c["level"]; l != "" {
|
|
return c["user"] + ":" + c["role"] + ":" + c["type"] + ":" + l
|
|
}
|
|
return c["user"] + ":" + c["role"] + ":" + c["type"]
|
|
}
|
|
|
|
// newContext creates a new Context struct from the specified label
|
|
func newContext(label string) (Context, error) {
|
|
c := make(Context)
|
|
|
|
if len(label) != 0 {
|
|
con := strings.SplitN(label, ":", 4)
|
|
if len(con) < 3 {
|
|
return c, ErrInvalidLabel
|
|
}
|
|
c["user"] = con[0]
|
|
c["role"] = con[1]
|
|
c["type"] = con[2]
|
|
if len(con) > 3 {
|
|
c["level"] = con[3]
|
|
}
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
// clearLabels clears all reserved labels
|
|
func clearLabels() {
|
|
state.Lock()
|
|
state.mcsList = make(map[string]bool)
|
|
state.Unlock()
|
|
}
|
|
|
|
// reserveLabel reserves the MLS/MCS level component of the specified label
|
|
func reserveLabel(label string) {
|
|
if len(label) != 0 {
|
|
con := strings.SplitN(label, ":", 4)
|
|
if len(con) > 3 {
|
|
_ = mcsAdd(con[3])
|
|
}
|
|
}
|
|
}
|
|
|
|
func selinuxEnforcePath() string {
|
|
return filepath.Join(getSelinuxMountPoint(), "enforce")
|
|
}
|
|
|
|
// isMLSEnabled checks if MLS is enabled.
|
|
func isMLSEnabled() bool {
|
|
enabledB, err := os.ReadFile(filepath.Join(getSelinuxMountPoint(), "mls"))
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return bytes.Equal(enabledB, []byte{'1'})
|
|
}
|
|
|
|
// enforceMode returns the current SELinux mode Enforcing, Permissive, Disabled
|
|
func enforceMode() int {
|
|
var enforce int
|
|
|
|
enforceB, err := os.ReadFile(selinuxEnforcePath())
|
|
if err != nil {
|
|
return -1
|
|
}
|
|
enforce, err = strconv.Atoi(string(enforceB))
|
|
if err != nil {
|
|
return -1
|
|
}
|
|
return enforce
|
|
}
|
|
|
|
// setEnforceMode sets the current SELinux mode Enforcing, Permissive.
|
|
// Disabled is not valid, since this needs to be set at boot time.
|
|
func setEnforceMode(mode int) error {
|
|
//nolint:gosec // ignore G306: permissions to be 0600 or less.
|
|
return os.WriteFile(selinuxEnforcePath(), []byte(strconv.Itoa(mode)), 0o644)
|
|
}
|
|
|
|
// defaultEnforceMode returns the systems default SELinux mode Enforcing,
|
|
// Permissive or Disabled. Note this is just the default at boot time.
|
|
// EnforceMode tells you the systems current mode.
|
|
func defaultEnforceMode() int {
|
|
switch readConfig(selinuxTag) {
|
|
case "enforcing":
|
|
return Enforcing
|
|
case "permissive":
|
|
return Permissive
|
|
}
|
|
return Disabled
|
|
}
|
|
|
|
func mcsAdd(mcs string) error {
|
|
if mcs == "" {
|
|
return nil
|
|
}
|
|
state.Lock()
|
|
defer state.Unlock()
|
|
if state.mcsList[mcs] {
|
|
return ErrMCSAlreadyExists
|
|
}
|
|
state.mcsList[mcs] = true
|
|
return nil
|
|
}
|
|
|
|
func mcsDelete(mcs string) {
|
|
if mcs == "" {
|
|
return
|
|
}
|
|
state.Lock()
|
|
defer state.Unlock()
|
|
state.mcsList[mcs] = false
|
|
}
|
|
|
|
func intToMcs(id int, catRange uint32) string {
|
|
var (
|
|
SETSIZE = int(catRange)
|
|
TIER = SETSIZE
|
|
ORD = id
|
|
)
|
|
|
|
if id < 1 || id > 523776 {
|
|
return ""
|
|
}
|
|
|
|
for ORD > TIER {
|
|
ORD -= TIER
|
|
TIER--
|
|
}
|
|
TIER = SETSIZE - TIER
|
|
ORD += TIER
|
|
return fmt.Sprintf("s0:c%d,c%d", TIER, ORD)
|
|
}
|
|
|
|
func uniqMcs(catRange uint32) string {
|
|
var (
|
|
n uint32
|
|
c1, c2 uint32
|
|
mcs string
|
|
)
|
|
|
|
for {
|
|
_ = binary.Read(rand.Reader, binary.LittleEndian, &n)
|
|
c1 = n % catRange
|
|
_ = binary.Read(rand.Reader, binary.LittleEndian, &n)
|
|
c2 = n % catRange
|
|
if c1 == c2 {
|
|
continue
|
|
} else if c1 > c2 {
|
|
c1, c2 = c2, c1
|
|
}
|
|
mcs = fmt.Sprintf("s0:c%d,c%d", c1, c2)
|
|
if err := mcsAdd(mcs); err != nil {
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
return mcs
|
|
}
|
|
|
|
// releaseLabel un-reserves the MLS/MCS Level field of the specified label,
|
|
// allowing it to be used by another process.
|
|
func releaseLabel(label string) {
|
|
if len(label) != 0 {
|
|
con := strings.SplitN(label, ":", 4)
|
|
if len(con) > 3 {
|
|
mcsDelete(con[3])
|
|
}
|
|
}
|
|
}
|
|
|
|
// roFileLabel returns the specified SELinux readonly file label
|
|
func roFileLabel() string {
|
|
return readOnlyFileLabel
|
|
}
|
|
|
|
func openContextFile() (*os.File, error) {
|
|
if f, err := os.Open(contextFile); err == nil {
|
|
return f, nil
|
|
}
|
|
return os.Open(filepath.Join(policyRoot(), "contexts", "lxc_contexts"))
|
|
}
|
|
|
|
func loadLabels() {
|
|
labels = make(map[string]string)
|
|
in, err := openContextFile()
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer in.Close()
|
|
|
|
scanner := bufio.NewScanner(in)
|
|
|
|
for scanner.Scan() {
|
|
line := bytes.TrimSpace(scanner.Bytes())
|
|
if len(line) == 0 {
|
|
// Skip blank lines
|
|
continue
|
|
}
|
|
if line[0] == ';' || line[0] == '#' {
|
|
// Skip comments
|
|
continue
|
|
}
|
|
fields := bytes.SplitN(line, []byte{'='}, 2)
|
|
if len(fields) != 2 {
|
|
continue
|
|
}
|
|
key, val := bytes.TrimSpace(fields[0]), bytes.TrimSpace(fields[1])
|
|
labels[string(key)] = string(bytes.Trim(val, `"`))
|
|
}
|
|
|
|
con, _ := NewContext(labels["file"])
|
|
con["level"] = fmt.Sprintf("s0:c%d,c%d", maxCategory-2, maxCategory-1)
|
|
privContainerMountLabel = con.get()
|
|
reserveLabel(privContainerMountLabel)
|
|
}
|
|
|
|
func label(key string) string {
|
|
loadLabelsOnce.Do(func() {
|
|
loadLabels()
|
|
})
|
|
return labels[key]
|
|
}
|
|
|
|
// kvmContainerLabels returns the default processLabel and mountLabel to be used
|
|
// for kvm containers by the calling process.
|
|
func kvmContainerLabels() (string, string) {
|
|
processLabel := label("kvm_process")
|
|
if processLabel == "" {
|
|
processLabel = label("process")
|
|
}
|
|
|
|
return addMcs(processLabel, label("file"))
|
|
}
|
|
|
|
// initContainerLabels returns the default processLabel and file labels to be
|
|
// used for containers running an init system like systemd by the calling process.
|
|
func initContainerLabels() (string, string) {
|
|
processLabel := label("init_process")
|
|
if processLabel == "" {
|
|
processLabel = label("process")
|
|
}
|
|
|
|
return addMcs(processLabel, label("file"))
|
|
}
|
|
|
|
// containerLabels returns an allocated processLabel and fileLabel to be used for
|
|
// container labeling by the calling process.
|
|
func containerLabels() (processLabel string, fileLabel string) {
|
|
if !getEnabled() {
|
|
return "", ""
|
|
}
|
|
|
|
processLabel = label("process")
|
|
fileLabel = label("file")
|
|
readOnlyFileLabel = label("ro_file")
|
|
|
|
if processLabel == "" || fileLabel == "" {
|
|
return "", fileLabel
|
|
}
|
|
|
|
if readOnlyFileLabel == "" {
|
|
readOnlyFileLabel = fileLabel
|
|
}
|
|
|
|
return addMcs(processLabel, fileLabel)
|
|
}
|
|
|
|
func addMcs(processLabel, fileLabel string) (string, string) {
|
|
scon, _ := NewContext(processLabel)
|
|
if scon["level"] != "" {
|
|
mcs := uniqMcs(CategoryRange)
|
|
scon["level"] = mcs
|
|
processLabel = scon.Get()
|
|
scon, _ = NewContext(fileLabel)
|
|
scon["level"] = mcs
|
|
fileLabel = scon.Get()
|
|
}
|
|
return processLabel, fileLabel
|
|
}
|
|
|
|
// securityCheckContext validates that the SELinux label is understood by the kernel
|
|
func securityCheckContext(val string) error {
|
|
//nolint:gosec // ignore G306: permissions to be 0600 or less.
|
|
return os.WriteFile(filepath.Join(getSelinuxMountPoint(), "context"), []byte(val), 0o644)
|
|
}
|
|
|
|
// copyLevel returns a label with the MLS/MCS level from src label replaced on
|
|
// the dest label.
|
|
func copyLevel(src, dest string) (string, error) {
|
|
if src == "" {
|
|
return "", nil
|
|
}
|
|
if err := SecurityCheckContext(src); err != nil {
|
|
return "", err
|
|
}
|
|
if err := SecurityCheckContext(dest); err != nil {
|
|
return "", err
|
|
}
|
|
scon, err := NewContext(src)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
tcon, err := NewContext(dest)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
mcsDelete(tcon["level"])
|
|
_ = mcsAdd(scon["level"])
|
|
tcon["level"] = scon["level"]
|
|
return tcon.Get(), nil
|
|
}
|
|
|
|
// chcon changes the fpath file object to the SELinux label.
|
|
// If fpath is a directory and recurse is true, then chcon walks the
|
|
// directory tree setting the label.
|
|
func chcon(fpath string, label string, recurse bool) error {
|
|
if fpath == "" {
|
|
return ErrEmptyPath
|
|
}
|
|
if label == "" {
|
|
return nil
|
|
}
|
|
|
|
excludePaths := map[string]bool{
|
|
"/": true,
|
|
"/bin": true,
|
|
"/boot": true,
|
|
"/dev": true,
|
|
"/etc": true,
|
|
"/etc/passwd": true,
|
|
"/etc/pki": true,
|
|
"/etc/shadow": true,
|
|
"/home": true,
|
|
"/lib": true,
|
|
"/lib64": true,
|
|
"/media": true,
|
|
"/opt": true,
|
|
"/proc": true,
|
|
"/root": true,
|
|
"/run": true,
|
|
"/sbin": true,
|
|
"/srv": true,
|
|
"/sys": true,
|
|
"/tmp": true,
|
|
"/usr": true,
|
|
"/var": true,
|
|
"/var/lib": true,
|
|
"/var/log": true,
|
|
}
|
|
|
|
if home := os.Getenv("HOME"); home != "" {
|
|
excludePaths[home] = true
|
|
}
|
|
|
|
if sudoUser := os.Getenv("SUDO_USER"); sudoUser != "" {
|
|
if usr, err := user.Lookup(sudoUser); err == nil {
|
|
excludePaths[usr.HomeDir] = true
|
|
}
|
|
}
|
|
|
|
if fpath != "/" {
|
|
fpath = strings.TrimSuffix(fpath, "/")
|
|
}
|
|
if excludePaths[fpath] {
|
|
return fmt.Errorf("SELinux relabeling of %s is not allowed", fpath)
|
|
}
|
|
|
|
if !recurse {
|
|
err := lSetFileLabel(fpath, label)
|
|
if err != nil {
|
|
// Check if file doesn't exist, must have been removed
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return nil
|
|
}
|
|
// Check if current label is correct on disk
|
|
flabel, nerr := lFileLabel(fpath)
|
|
if nerr == nil && flabel == label {
|
|
return nil
|
|
}
|
|
// Check if file doesn't exist, must have been removed
|
|
if errors.Is(nerr, os.ErrNotExist) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
return rchcon(fpath, label)
|
|
}
|
|
|
|
func rchcon(fpath, label string) error { //revive:disable:cognitive-complexity
|
|
fastMode := false
|
|
// If the current label matches the new label, assume
|
|
// other labels are correct.
|
|
if cLabel, err := lFileLabel(fpath); err == nil && cLabel == label {
|
|
fastMode = true
|
|
}
|
|
return pwalkdir.Walk(fpath, func(p string, _ fs.DirEntry, _ error) error {
|
|
if fastMode {
|
|
if cLabel, err := lFileLabel(fpath); err == nil && cLabel == label {
|
|
return nil
|
|
}
|
|
}
|
|
err := lSetFileLabel(p, label)
|
|
// Walk a file tree can race with removal, so ignore ENOENT.
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return nil
|
|
}
|
|
return err
|
|
})
|
|
}
|
|
|
|
// dupSecOpt takes an SELinux process label and returns security options that
|
|
// can be used to set the SELinux Type and Level for future container processes.
|
|
func dupSecOpt(src string) ([]string, error) {
|
|
if src == "" {
|
|
return nil, nil
|
|
}
|
|
con, err := NewContext(src)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if con["user"] == "" ||
|
|
con["role"] == "" ||
|
|
con["type"] == "" {
|
|
return nil, nil
|
|
}
|
|
dup := []string{
|
|
"user:" + con["user"],
|
|
"role:" + con["role"],
|
|
"type:" + con["type"],
|
|
}
|
|
|
|
if con["level"] != "" {
|
|
dup = append(dup, "level:"+con["level"])
|
|
}
|
|
|
|
return dup, nil
|
|
}
|
|
|
|
// findUserInContext scans the reader for a valid SELinux context
|
|
// match that is verified with the verifier. Invalid contexts are
|
|
// skipped. It returns a matched context or an empty string if no
|
|
// match is found. If a scanner error occurs, it is returned.
|
|
func findUserInContext(context Context, r io.Reader, verifier func(string) error) (string, error) {
|
|
fromRole := context["role"]
|
|
fromType := context["type"]
|
|
scanner := bufio.NewScanner(r)
|
|
|
|
for scanner.Scan() {
|
|
fromConns := strings.Fields(scanner.Text())
|
|
if len(fromConns) == 0 {
|
|
// Skip blank lines
|
|
continue
|
|
}
|
|
|
|
line := fromConns[0]
|
|
|
|
if line[0] == ';' || line[0] == '#' {
|
|
// Skip comments
|
|
continue
|
|
}
|
|
|
|
// user context files contexts are formatted as
|
|
// role_r:type_t:s0 where the user is missing.
|
|
lineArr := strings.SplitN(line, ":", 4)
|
|
// skip context with typo, or role and type do not match
|
|
if len(lineArr) != 3 ||
|
|
lineArr[0] != fromRole ||
|
|
lineArr[1] != fromType {
|
|
continue
|
|
}
|
|
|
|
for _, cc := range fromConns[1:] {
|
|
toConns := strings.SplitN(cc, ":", 4)
|
|
if len(toConns) != 3 {
|
|
continue
|
|
}
|
|
|
|
context["role"] = toConns[0]
|
|
context["type"] = toConns[1]
|
|
|
|
outConn := context.get()
|
|
if err := verifier(outConn); err != nil {
|
|
continue
|
|
}
|
|
|
|
return outConn, nil
|
|
}
|
|
}
|
|
if err := scanner.Err(); err != nil {
|
|
return "", fmt.Errorf("failed to scan for context: %w", err)
|
|
}
|
|
|
|
return "", nil
|
|
}
|
|
|
|
func getDefaultContextFromReaders(c *defaultSECtx) (string, error) {
|
|
if c.verifier == nil {
|
|
return "", ErrVerifierNil
|
|
}
|
|
|
|
context, err := newContext(c.scon)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to create label for %s: %w", c.scon, err)
|
|
}
|
|
|
|
// set so the verifier validates the matched context with the provided user and level.
|
|
context["user"] = c.user
|
|
context["level"] = c.level
|
|
|
|
conn, err := findUserInContext(context, c.userRdr, c.verifier)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if conn != "" {
|
|
return conn, nil
|
|
}
|
|
|
|
conn, err = findUserInContext(context, c.defaultRdr, c.verifier)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if conn != "" {
|
|
return conn, nil
|
|
}
|
|
|
|
return "", fmt.Errorf("context %q not found: %w", c.scon, ErrContextMissing)
|
|
}
|
|
|
|
func getDefaultContextWithLevel(user, level, scon string) (string, error) {
|
|
userPath := filepath.Join(policyRoot(), selinuxUsersDir, user)
|
|
fu, err := os.Open(userPath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer fu.Close()
|
|
|
|
defaultPath := filepath.Join(policyRoot(), defaultContexts)
|
|
fd, err := os.Open(defaultPath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer fd.Close()
|
|
|
|
c := defaultSECtx{
|
|
user: user,
|
|
level: level,
|
|
scon: scon,
|
|
userRdr: fu,
|
|
defaultRdr: fd,
|
|
verifier: securityCheckContext,
|
|
}
|
|
|
|
return getDefaultContextFromReaders(&c)
|
|
}
|