Niels de Vos bec6090996 build: move e2e dependencies into e2e/go.mod
Several packages are only used while running the e2e suite. These
packages are less important to update, as the they can not influence the
final executable that is part of the Ceph-CSI container-image.

By moving these dependencies out of the main Ceph-CSI go.mod, it is
easier to identify if a reported CVE affects Ceph-CSI, or only the
testing (like most of the Kubernetes CVEs).

Signed-off-by: Niels de Vos <ndevos@ibm.com>
2025-03-07 16:05:04 +00:00

1297 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 {
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 {
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 {
return &os.PathError{Op: fmt.Sprintf("lsetxattr(label=%s)", label), 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 {
return &os.PathError{Op: fmt.Sprintf("setxattr(label=%s)", label), 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
}
// TODO: remove min and max once Go < 1.21 is not supported.
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(p); 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)
}