...

Source file src/github.com/opencontainers/selinux/go-selinux/selinux_linux.go

Documentation: github.com/opencontainers/selinux/go-selinux

     1  package selinux
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"crypto/rand"
     7  	"encoding/binary"
     8  	"errors"
     9  	"fmt"
    10  	"io"
    11  	"io/fs"
    12  	"math/big"
    13  	"os"
    14  	"os/user"
    15  	"path/filepath"
    16  	"strconv"
    17  	"strings"
    18  	"sync"
    19  
    20  	"github.com/opencontainers/selinux/pkg/pwalkdir"
    21  	"golang.org/x/sys/unix"
    22  )
    23  
    24  const (
    25  	minSensLen       = 2
    26  	contextFile      = "/usr/share/containers/selinux/contexts"
    27  	selinuxDir       = "/etc/selinux/"
    28  	selinuxUsersDir  = "contexts/users"
    29  	defaultContexts  = "contexts/default_contexts"
    30  	selinuxConfig    = selinuxDir + "config"
    31  	selinuxfsMount   = "/sys/fs/selinux"
    32  	selinuxTypeTag   = "SELINUXTYPE"
    33  	selinuxTag       = "SELINUX"
    34  	xattrNameSelinux = "security.selinux"
    35  )
    36  
    37  type selinuxState struct {
    38  	mcsList       map[string]bool
    39  	selinuxfs     string
    40  	selinuxfsOnce sync.Once
    41  	enabledSet    bool
    42  	enabled       bool
    43  	sync.Mutex
    44  }
    45  
    46  type level struct {
    47  	cats *big.Int
    48  	sens uint
    49  }
    50  
    51  type mlsRange struct {
    52  	low  *level
    53  	high *level
    54  }
    55  
    56  type defaultSECtx struct {
    57  	userRdr           io.Reader
    58  	verifier          func(string) error
    59  	defaultRdr        io.Reader
    60  	user, level, scon string
    61  }
    62  
    63  type levelItem byte
    64  
    65  const (
    66  	sensitivity levelItem = 's'
    67  	category    levelItem = 'c'
    68  )
    69  
    70  var (
    71  	readOnlyFileLabel string
    72  	state             = selinuxState{
    73  		mcsList: make(map[string]bool),
    74  	}
    75  
    76  	// for attrPath()
    77  	attrPathOnce   sync.Once
    78  	haveThreadSelf bool
    79  
    80  	// for policyRoot()
    81  	policyRootOnce sync.Once
    82  	policyRootVal  string
    83  
    84  	// for label()
    85  	loadLabelsOnce sync.Once
    86  	labels         map[string]string
    87  )
    88  
    89  func policyRoot() string {
    90  	policyRootOnce.Do(func() {
    91  		policyRootVal = filepath.Join(selinuxDir, readConfig(selinuxTypeTag))
    92  	})
    93  
    94  	return policyRootVal
    95  }
    96  
    97  func (s *selinuxState) setEnable(enabled bool) bool {
    98  	s.Lock()
    99  	defer s.Unlock()
   100  	s.enabledSet = true
   101  	s.enabled = enabled
   102  	return s.enabled
   103  }
   104  
   105  func (s *selinuxState) getEnabled() bool {
   106  	s.Lock()
   107  	enabled := s.enabled
   108  	enabledSet := s.enabledSet
   109  	s.Unlock()
   110  	if enabledSet {
   111  		return enabled
   112  	}
   113  
   114  	enabled = false
   115  	if fs := getSelinuxMountPoint(); fs != "" {
   116  		if con, _ := CurrentLabel(); con != "kernel" {
   117  			enabled = true
   118  		}
   119  	}
   120  	return s.setEnable(enabled)
   121  }
   122  
   123  // setDisabled disables SELinux support for the package
   124  func setDisabled() {
   125  	state.setEnable(false)
   126  }
   127  
   128  func verifySELinuxfsMount(mnt string) bool {
   129  	var buf unix.Statfs_t
   130  	for {
   131  		err := unix.Statfs(mnt, &buf)
   132  		if err == nil {
   133  			break
   134  		}
   135  		if err == unix.EAGAIN || err == unix.EINTR { //nolint:errorlint // unix errors are bare
   136  			continue
   137  		}
   138  		return false
   139  	}
   140  
   141  	if uint32(buf.Type) != uint32(unix.SELINUX_MAGIC) {
   142  		return false
   143  	}
   144  	if (buf.Flags & unix.ST_RDONLY) != 0 {
   145  		return false
   146  	}
   147  
   148  	return true
   149  }
   150  
   151  func findSELinuxfs() string {
   152  	// fast path: check the default mount first
   153  	if verifySELinuxfsMount(selinuxfsMount) {
   154  		return selinuxfsMount
   155  	}
   156  
   157  	// check if selinuxfs is available before going the slow path
   158  	fs, err := os.ReadFile("/proc/filesystems")
   159  	if err != nil {
   160  		return ""
   161  	}
   162  	if !bytes.Contains(fs, []byte("\tselinuxfs\n")) {
   163  		return ""
   164  	}
   165  
   166  	// slow path: try to find among the mounts
   167  	f, err := os.Open("/proc/self/mountinfo")
   168  	if err != nil {
   169  		return ""
   170  	}
   171  	defer f.Close()
   172  
   173  	scanner := bufio.NewScanner(f)
   174  	for {
   175  		mnt := findSELinuxfsMount(scanner)
   176  		if mnt == "" { // error or not found
   177  			return ""
   178  		}
   179  		if verifySELinuxfsMount(mnt) {
   180  			return mnt
   181  		}
   182  	}
   183  }
   184  
   185  // findSELinuxfsMount returns a next selinuxfs mount point found,
   186  // if there is one, or an empty string in case of EOF or error.
   187  func findSELinuxfsMount(s *bufio.Scanner) string {
   188  	for s.Scan() {
   189  		txt := s.Bytes()
   190  		// The first field after - is fs type.
   191  		// Safe as spaces in mountpoints are encoded as \040
   192  		if !bytes.Contains(txt, []byte(" - selinuxfs ")) {
   193  			continue
   194  		}
   195  		const mPos = 5 // mount point is 5th field
   196  		fields := bytes.SplitN(txt, []byte(" "), mPos+1)
   197  		if len(fields) < mPos+1 {
   198  			continue
   199  		}
   200  		return string(fields[mPos-1])
   201  	}
   202  
   203  	return ""
   204  }
   205  
   206  func (s *selinuxState) getSELinuxfs() string {
   207  	s.selinuxfsOnce.Do(func() {
   208  		s.selinuxfs = findSELinuxfs()
   209  	})
   210  
   211  	return s.selinuxfs
   212  }
   213  
   214  // getSelinuxMountPoint returns the path to the mountpoint of an selinuxfs
   215  // filesystem or an empty string if no mountpoint is found.  Selinuxfs is
   216  // a proc-like pseudo-filesystem that exposes the SELinux policy API to
   217  // processes.  The existence of an selinuxfs mount is used to determine
   218  // whether SELinux is currently enabled or not.
   219  func getSelinuxMountPoint() string {
   220  	return state.getSELinuxfs()
   221  }
   222  
   223  // getEnabled returns whether SELinux is currently enabled.
   224  func getEnabled() bool {
   225  	return state.getEnabled()
   226  }
   227  
   228  func readConfig(target string) string {
   229  	in, err := os.Open(selinuxConfig)
   230  	if err != nil {
   231  		return ""
   232  	}
   233  	defer in.Close()
   234  
   235  	scanner := bufio.NewScanner(in)
   236  
   237  	for scanner.Scan() {
   238  		line := bytes.TrimSpace(scanner.Bytes())
   239  		if len(line) == 0 {
   240  			// Skip blank lines
   241  			continue
   242  		}
   243  		if line[0] == ';' || line[0] == '#' {
   244  			// Skip comments
   245  			continue
   246  		}
   247  		fields := bytes.SplitN(line, []byte{'='}, 2)
   248  		if len(fields) != 2 {
   249  			continue
   250  		}
   251  		if bytes.Equal(fields[0], []byte(target)) {
   252  			return string(bytes.Trim(fields[1], `"`))
   253  		}
   254  	}
   255  	return ""
   256  }
   257  
   258  func isProcHandle(fh *os.File) error {
   259  	var buf unix.Statfs_t
   260  
   261  	for {
   262  		err := unix.Fstatfs(int(fh.Fd()), &buf)
   263  		if err == nil {
   264  			break
   265  		}
   266  		if err != unix.EINTR { //nolint:errorlint // unix errors are bare
   267  			return &os.PathError{Op: "fstatfs", Path: fh.Name(), Err: err}
   268  		}
   269  	}
   270  	if buf.Type != unix.PROC_SUPER_MAGIC {
   271  		return fmt.Errorf("file %q is not on procfs", fh.Name())
   272  	}
   273  
   274  	return nil
   275  }
   276  
   277  func readCon(fpath string) (string, error) {
   278  	if fpath == "" {
   279  		return "", ErrEmptyPath
   280  	}
   281  
   282  	in, err := os.Open(fpath)
   283  	if err != nil {
   284  		return "", err
   285  	}
   286  	defer in.Close()
   287  
   288  	if err := isProcHandle(in); err != nil {
   289  		return "", err
   290  	}
   291  	return readConFd(in)
   292  }
   293  
   294  func readConFd(in *os.File) (string, error) {
   295  	data, err := io.ReadAll(in)
   296  	if err != nil {
   297  		return "", err
   298  	}
   299  	return string(bytes.TrimSuffix(data, []byte{0})), nil
   300  }
   301  
   302  // classIndex returns the int index for an object class in the loaded policy,
   303  // or -1 and an error
   304  func classIndex(class string) (int, error) {
   305  	permpath := fmt.Sprintf("class/%s/index", class)
   306  	indexpath := filepath.Join(getSelinuxMountPoint(), permpath)
   307  
   308  	indexB, err := os.ReadFile(indexpath)
   309  	if err != nil {
   310  		return -1, err
   311  	}
   312  	index, err := strconv.Atoi(string(indexB))
   313  	if err != nil {
   314  		return -1, err
   315  	}
   316  
   317  	return index, nil
   318  }
   319  
   320  // lSetFileLabel sets the SELinux label for this path, not following symlinks,
   321  // or returns an error.
   322  func lSetFileLabel(fpath string, label string) error {
   323  	if fpath == "" {
   324  		return ErrEmptyPath
   325  	}
   326  	for {
   327  		err := unix.Lsetxattr(fpath, xattrNameSelinux, []byte(label), 0)
   328  		if err == nil {
   329  			break
   330  		}
   331  		if err != unix.EINTR { //nolint:errorlint // unix errors are bare
   332  			return &os.PathError{Op: "lsetxattr", Path: fpath, Err: err}
   333  		}
   334  	}
   335  
   336  	return nil
   337  }
   338  
   339  // setFileLabel sets the SELinux label for this path, following symlinks,
   340  // or returns an error.
   341  func setFileLabel(fpath string, label string) error {
   342  	if fpath == "" {
   343  		return ErrEmptyPath
   344  	}
   345  	for {
   346  		err := unix.Setxattr(fpath, xattrNameSelinux, []byte(label), 0)
   347  		if err == nil {
   348  			break
   349  		}
   350  		if err != unix.EINTR { //nolint:errorlint // unix errors are bare
   351  			return &os.PathError{Op: "setxattr", Path: fpath, Err: err}
   352  		}
   353  	}
   354  
   355  	return nil
   356  }
   357  
   358  // fileLabel returns the SELinux label for this path, following symlinks,
   359  // or returns an error.
   360  func fileLabel(fpath string) (string, error) {
   361  	if fpath == "" {
   362  		return "", ErrEmptyPath
   363  	}
   364  
   365  	label, err := getxattr(fpath, xattrNameSelinux)
   366  	if err != nil {
   367  		return "", &os.PathError{Op: "getxattr", Path: fpath, Err: err}
   368  	}
   369  	// Trim the NUL byte at the end of the byte buffer, if present.
   370  	if len(label) > 0 && label[len(label)-1] == '\x00' {
   371  		label = label[:len(label)-1]
   372  	}
   373  	return string(label), nil
   374  }
   375  
   376  // lFileLabel returns the SELinux label for this path, not following symlinks,
   377  // or returns an error.
   378  func lFileLabel(fpath string) (string, error) {
   379  	if fpath == "" {
   380  		return "", ErrEmptyPath
   381  	}
   382  
   383  	label, err := lgetxattr(fpath, xattrNameSelinux)
   384  	if err != nil {
   385  		return "", &os.PathError{Op: "lgetxattr", Path: fpath, Err: err}
   386  	}
   387  	// Trim the NUL byte at the end of the byte buffer, if present.
   388  	if len(label) > 0 && label[len(label)-1] == '\x00' {
   389  		label = label[:len(label)-1]
   390  	}
   391  	return string(label), nil
   392  }
   393  
   394  func setFSCreateLabel(label string) error {
   395  	return writeCon(attrPath("fscreate"), label)
   396  }
   397  
   398  // fsCreateLabel returns the default label the kernel which the kernel is using
   399  // for file system objects created by this task. "" indicates default.
   400  func fsCreateLabel() (string, error) {
   401  	return readCon(attrPath("fscreate"))
   402  }
   403  
   404  // currentLabel returns the SELinux label of the current process thread, or an error.
   405  func currentLabel() (string, error) {
   406  	return readCon(attrPath("current"))
   407  }
   408  
   409  // pidLabel returns the SELinux label of the given pid, or an error.
   410  func pidLabel(pid int) (string, error) {
   411  	return readCon(fmt.Sprintf("/proc/%d/attr/current", pid))
   412  }
   413  
   414  // ExecLabel returns the SELinux label that the kernel will use for any programs
   415  // that are executed by the current process thread, or an error.
   416  func execLabel() (string, error) {
   417  	return readCon(attrPath("exec"))
   418  }
   419  
   420  func writeCon(fpath, val string) error {
   421  	if fpath == "" {
   422  		return ErrEmptyPath
   423  	}
   424  	if val == "" {
   425  		if !getEnabled() {
   426  			return nil
   427  		}
   428  	}
   429  
   430  	out, err := os.OpenFile(fpath, os.O_WRONLY, 0)
   431  	if err != nil {
   432  		return err
   433  	}
   434  	defer out.Close()
   435  
   436  	if err := isProcHandle(out); err != nil {
   437  		return err
   438  	}
   439  
   440  	if val != "" {
   441  		_, err = out.Write([]byte(val))
   442  	} else {
   443  		_, err = out.Write(nil)
   444  	}
   445  	if err != nil {
   446  		return err
   447  	}
   448  	return nil
   449  }
   450  
   451  func attrPath(attr string) string {
   452  	// Linux >= 3.17 provides this
   453  	const threadSelfPrefix = "/proc/thread-self/attr"
   454  
   455  	attrPathOnce.Do(func() {
   456  		st, err := os.Stat(threadSelfPrefix)
   457  		if err == nil && st.Mode().IsDir() {
   458  			haveThreadSelf = true
   459  		}
   460  	})
   461  
   462  	if haveThreadSelf {
   463  		return filepath.Join(threadSelfPrefix, attr)
   464  	}
   465  
   466  	return filepath.Join("/proc/self/task", strconv.Itoa(unix.Gettid()), "attr", attr)
   467  }
   468  
   469  // canonicalizeContext takes a context string and writes it to the kernel
   470  // the function then returns the context that the kernel will use. Use this
   471  // function to check if two contexts are equivalent
   472  func canonicalizeContext(val string) (string, error) {
   473  	return readWriteCon(filepath.Join(getSelinuxMountPoint(), "context"), val)
   474  }
   475  
   476  // computeCreateContext requests the type transition from source to target for
   477  // class from the kernel.
   478  func computeCreateContext(source string, target string, class string) (string, error) {
   479  	classidx, err := classIndex(class)
   480  	if err != nil {
   481  		return "", err
   482  	}
   483  
   484  	return readWriteCon(filepath.Join(getSelinuxMountPoint(), "create"), fmt.Sprintf("%s %s %d", source, target, classidx))
   485  }
   486  
   487  // catsToBitset stores categories in a bitset.
   488  func catsToBitset(cats string) (*big.Int, error) {
   489  	bitset := new(big.Int)
   490  
   491  	catlist := strings.Split(cats, ",")
   492  	for _, r := range catlist {
   493  		ranges := strings.SplitN(r, ".", 2)
   494  		if len(ranges) > 1 {
   495  			catstart, err := parseLevelItem(ranges[0], category)
   496  			if err != nil {
   497  				return nil, err
   498  			}
   499  			catend, err := parseLevelItem(ranges[1], category)
   500  			if err != nil {
   501  				return nil, err
   502  			}
   503  			for i := catstart; i <= catend; i++ {
   504  				bitset.SetBit(bitset, int(i), 1)
   505  			}
   506  		} else {
   507  			cat, err := parseLevelItem(ranges[0], category)
   508  			if err != nil {
   509  				return nil, err
   510  			}
   511  			bitset.SetBit(bitset, int(cat), 1)
   512  		}
   513  	}
   514  
   515  	return bitset, nil
   516  }
   517  
   518  // parseLevelItem parses and verifies that a sensitivity or category are valid
   519  func parseLevelItem(s string, sep levelItem) (uint, error) {
   520  	if len(s) < minSensLen || levelItem(s[0]) != sep {
   521  		return 0, ErrLevelSyntax
   522  	}
   523  	val, err := strconv.ParseUint(s[1:], 10, 32)
   524  	if err != nil {
   525  		return 0, err
   526  	}
   527  
   528  	return uint(val), nil
   529  }
   530  
   531  // parseLevel fills a level from a string that contains
   532  // a sensitivity and categories
   533  func (l *level) parseLevel(levelStr string) error {
   534  	lvl := strings.SplitN(levelStr, ":", 2)
   535  	sens, err := parseLevelItem(lvl[0], sensitivity)
   536  	if err != nil {
   537  		return fmt.Errorf("failed to parse sensitivity: %w", err)
   538  	}
   539  	l.sens = sens
   540  	if len(lvl) > 1 {
   541  		cats, err := catsToBitset(lvl[1])
   542  		if err != nil {
   543  			return fmt.Errorf("failed to parse categories: %w", err)
   544  		}
   545  		l.cats = cats
   546  	}
   547  
   548  	return nil
   549  }
   550  
   551  // rangeStrToMLSRange marshals a string representation of a range.
   552  func rangeStrToMLSRange(rangeStr string) (*mlsRange, error) {
   553  	r := &mlsRange{}
   554  	l := strings.SplitN(rangeStr, "-", 2)
   555  
   556  	switch len(l) {
   557  	// rangeStr that has a low and a high level, e.g. s4:c0.c1023-s6:c0.c1023
   558  	case 2:
   559  		r.high = &level{}
   560  		if err := r.high.parseLevel(l[1]); err != nil {
   561  			return nil, fmt.Errorf("failed to parse high level %q: %w", l[1], err)
   562  		}
   563  		fallthrough
   564  	// rangeStr that is single level, e.g. s6:c0,c3,c5,c30.c1023
   565  	case 1:
   566  		r.low = &level{}
   567  		if err := r.low.parseLevel(l[0]); err != nil {
   568  			return nil, fmt.Errorf("failed to parse low level %q: %w", l[0], err)
   569  		}
   570  	}
   571  
   572  	if r.high == nil {
   573  		r.high = r.low
   574  	}
   575  
   576  	return r, nil
   577  }
   578  
   579  // bitsetToStr takes a category bitset and returns it in the
   580  // canonical selinux syntax
   581  func bitsetToStr(c *big.Int) string {
   582  	var str string
   583  
   584  	length := 0
   585  	for i := int(c.TrailingZeroBits()); i < c.BitLen(); i++ {
   586  		if c.Bit(i) == 0 {
   587  			continue
   588  		}
   589  		if length == 0 {
   590  			if str != "" {
   591  				str += ","
   592  			}
   593  			str += "c" + strconv.Itoa(i)
   594  		}
   595  		if c.Bit(i+1) == 1 {
   596  			length++
   597  			continue
   598  		}
   599  		if length == 1 {
   600  			str += ",c" + strconv.Itoa(i)
   601  		} else if length > 1 {
   602  			str += ".c" + strconv.Itoa(i)
   603  		}
   604  		length = 0
   605  	}
   606  
   607  	return str
   608  }
   609  
   610  func (l *level) equal(l2 *level) bool {
   611  	if l2 == nil || l == nil {
   612  		return l == l2
   613  	}
   614  	if l2.sens != l.sens {
   615  		return false
   616  	}
   617  	if l2.cats == nil || l.cats == nil {
   618  		return l2.cats == l.cats
   619  	}
   620  	return l.cats.Cmp(l2.cats) == 0
   621  }
   622  
   623  // String returns an mlsRange as a string.
   624  func (m mlsRange) String() string {
   625  	low := "s" + strconv.Itoa(int(m.low.sens))
   626  	if m.low.cats != nil && m.low.cats.BitLen() > 0 {
   627  		low += ":" + bitsetToStr(m.low.cats)
   628  	}
   629  
   630  	if m.low.equal(m.high) {
   631  		return low
   632  	}
   633  
   634  	high := "s" + strconv.Itoa(int(m.high.sens))
   635  	if m.high.cats != nil && m.high.cats.BitLen() > 0 {
   636  		high += ":" + bitsetToStr(m.high.cats)
   637  	}
   638  
   639  	return low + "-" + high
   640  }
   641  
   642  func max(a, b uint) uint {
   643  	if a > b {
   644  		return a
   645  	}
   646  	return b
   647  }
   648  
   649  func min(a, b uint) uint {
   650  	if a < b {
   651  		return a
   652  	}
   653  	return b
   654  }
   655  
   656  // calculateGlbLub computes the glb (greatest lower bound) and lub (least upper bound)
   657  // of a source and target range.
   658  // The glblub is calculated as the greater of the low sensitivities and
   659  // the lower of the high sensitivities and the and of each category bitset.
   660  func calculateGlbLub(sourceRange, targetRange string) (string, error) {
   661  	s, err := rangeStrToMLSRange(sourceRange)
   662  	if err != nil {
   663  		return "", err
   664  	}
   665  	t, err := rangeStrToMLSRange(targetRange)
   666  	if err != nil {
   667  		return "", err
   668  	}
   669  
   670  	if s.high.sens < t.low.sens || t.high.sens < s.low.sens {
   671  		/* these ranges have no common sensitivities */
   672  		return "", ErrIncomparable
   673  	}
   674  
   675  	outrange := &mlsRange{low: &level{}, high: &level{}}
   676  
   677  	/* take the greatest of the low */
   678  	outrange.low.sens = max(s.low.sens, t.low.sens)
   679  
   680  	/* take the least of the high */
   681  	outrange.high.sens = min(s.high.sens, t.high.sens)
   682  
   683  	/* find the intersecting categories */
   684  	if s.low.cats != nil && t.low.cats != nil {
   685  		outrange.low.cats = new(big.Int)
   686  		outrange.low.cats.And(s.low.cats, t.low.cats)
   687  	}
   688  	if s.high.cats != nil && t.high.cats != nil {
   689  		outrange.high.cats = new(big.Int)
   690  		outrange.high.cats.And(s.high.cats, t.high.cats)
   691  	}
   692  
   693  	return outrange.String(), nil
   694  }
   695  
   696  func readWriteCon(fpath string, val string) (string, error) {
   697  	if fpath == "" {
   698  		return "", ErrEmptyPath
   699  	}
   700  	f, err := os.OpenFile(fpath, os.O_RDWR, 0)
   701  	if err != nil {
   702  		return "", err
   703  	}
   704  	defer f.Close()
   705  
   706  	_, err = f.Write([]byte(val))
   707  	if err != nil {
   708  		return "", err
   709  	}
   710  
   711  	return readConFd(f)
   712  }
   713  
   714  // peerLabel retrieves the label of the client on the other side of a socket
   715  func peerLabel(fd uintptr) (string, error) {
   716  	l, err := unix.GetsockoptString(int(fd), unix.SOL_SOCKET, unix.SO_PEERSEC)
   717  	if err != nil {
   718  		return "", &os.PathError{Op: "getsockopt", Path: "fd " + strconv.Itoa(int(fd)), Err: err}
   719  	}
   720  	return l, nil
   721  }
   722  
   723  // setKeyLabel takes a process label and tells the kernel to assign the
   724  // label to the next kernel keyring that gets created
   725  func setKeyLabel(label string) error {
   726  	err := writeCon("/proc/self/attr/keycreate", label)
   727  	if errors.Is(err, os.ErrNotExist) {
   728  		return nil
   729  	}
   730  	if label == "" && errors.Is(err, os.ErrPermission) {
   731  		return nil
   732  	}
   733  	return err
   734  }
   735  
   736  // get returns the Context as a string
   737  func (c Context) get() string {
   738  	if l := c["level"]; l != "" {
   739  		return c["user"] + ":" + c["role"] + ":" + c["type"] + ":" + l
   740  	}
   741  	return c["user"] + ":" + c["role"] + ":" + c["type"]
   742  }
   743  
   744  // newContext creates a new Context struct from the specified label
   745  func newContext(label string) (Context, error) {
   746  	c := make(Context)
   747  
   748  	if len(label) != 0 {
   749  		con := strings.SplitN(label, ":", 4)
   750  		if len(con) < 3 {
   751  			return c, ErrInvalidLabel
   752  		}
   753  		c["user"] = con[0]
   754  		c["role"] = con[1]
   755  		c["type"] = con[2]
   756  		if len(con) > 3 {
   757  			c["level"] = con[3]
   758  		}
   759  	}
   760  	return c, nil
   761  }
   762  
   763  // clearLabels clears all reserved labels
   764  func clearLabels() {
   765  	state.Lock()
   766  	state.mcsList = make(map[string]bool)
   767  	state.Unlock()
   768  }
   769  
   770  // reserveLabel reserves the MLS/MCS level component of the specified label
   771  func reserveLabel(label string) {
   772  	if len(label) != 0 {
   773  		con := strings.SplitN(label, ":", 4)
   774  		if len(con) > 3 {
   775  			_ = mcsAdd(con[3])
   776  		}
   777  	}
   778  }
   779  
   780  func selinuxEnforcePath() string {
   781  	return filepath.Join(getSelinuxMountPoint(), "enforce")
   782  }
   783  
   784  // isMLSEnabled checks if MLS is enabled.
   785  func isMLSEnabled() bool {
   786  	enabledB, err := os.ReadFile(filepath.Join(getSelinuxMountPoint(), "mls"))
   787  	if err != nil {
   788  		return false
   789  	}
   790  	return bytes.Equal(enabledB, []byte{'1'})
   791  }
   792  
   793  // enforceMode returns the current SELinux mode Enforcing, Permissive, Disabled
   794  func enforceMode() int {
   795  	var enforce int
   796  
   797  	enforceB, err := os.ReadFile(selinuxEnforcePath())
   798  	if err != nil {
   799  		return -1
   800  	}
   801  	enforce, err = strconv.Atoi(string(enforceB))
   802  	if err != nil {
   803  		return -1
   804  	}
   805  	return enforce
   806  }
   807  
   808  // setEnforceMode sets the current SELinux mode Enforcing, Permissive.
   809  // Disabled is not valid, since this needs to be set at boot time.
   810  func setEnforceMode(mode int) error {
   811  	//nolint:gosec // ignore G306: permissions to be 0600 or less.
   812  	return os.WriteFile(selinuxEnforcePath(), []byte(strconv.Itoa(mode)), 0o644)
   813  }
   814  
   815  // defaultEnforceMode returns the systems default SELinux mode Enforcing,
   816  // Permissive or Disabled. Note this is just the default at boot time.
   817  // EnforceMode tells you the systems current mode.
   818  func defaultEnforceMode() int {
   819  	switch readConfig(selinuxTag) {
   820  	case "enforcing":
   821  		return Enforcing
   822  	case "permissive":
   823  		return Permissive
   824  	}
   825  	return Disabled
   826  }
   827  
   828  func mcsAdd(mcs string) error {
   829  	if mcs == "" {
   830  		return nil
   831  	}
   832  	state.Lock()
   833  	defer state.Unlock()
   834  	if state.mcsList[mcs] {
   835  		return ErrMCSAlreadyExists
   836  	}
   837  	state.mcsList[mcs] = true
   838  	return nil
   839  }
   840  
   841  func mcsDelete(mcs string) {
   842  	if mcs == "" {
   843  		return
   844  	}
   845  	state.Lock()
   846  	defer state.Unlock()
   847  	state.mcsList[mcs] = false
   848  }
   849  
   850  func intToMcs(id int, catRange uint32) string {
   851  	var (
   852  		SETSIZE = int(catRange)
   853  		TIER    = SETSIZE
   854  		ORD     = id
   855  	)
   856  
   857  	if id < 1 || id > 523776 {
   858  		return ""
   859  	}
   860  
   861  	for ORD > TIER {
   862  		ORD -= TIER
   863  		TIER--
   864  	}
   865  	TIER = SETSIZE - TIER
   866  	ORD += TIER
   867  	return fmt.Sprintf("s0:c%d,c%d", TIER, ORD)
   868  }
   869  
   870  func uniqMcs(catRange uint32) string {
   871  	var (
   872  		n      uint32
   873  		c1, c2 uint32
   874  		mcs    string
   875  	)
   876  
   877  	for {
   878  		_ = binary.Read(rand.Reader, binary.LittleEndian, &n)
   879  		c1 = n % catRange
   880  		_ = binary.Read(rand.Reader, binary.LittleEndian, &n)
   881  		c2 = n % catRange
   882  		if c1 == c2 {
   883  			continue
   884  		} else if c1 > c2 {
   885  			c1, c2 = c2, c1
   886  		}
   887  		mcs = fmt.Sprintf("s0:c%d,c%d", c1, c2)
   888  		if err := mcsAdd(mcs); err != nil {
   889  			continue
   890  		}
   891  		break
   892  	}
   893  	return mcs
   894  }
   895  
   896  // releaseLabel un-reserves the MLS/MCS Level field of the specified label,
   897  // allowing it to be used by another process.
   898  func releaseLabel(label string) {
   899  	if len(label) != 0 {
   900  		con := strings.SplitN(label, ":", 4)
   901  		if len(con) > 3 {
   902  			mcsDelete(con[3])
   903  		}
   904  	}
   905  }
   906  
   907  // roFileLabel returns the specified SELinux readonly file label
   908  func roFileLabel() string {
   909  	return readOnlyFileLabel
   910  }
   911  
   912  func openContextFile() (*os.File, error) {
   913  	if f, err := os.Open(contextFile); err == nil {
   914  		return f, nil
   915  	}
   916  	return os.Open(filepath.Join(policyRoot(), "contexts", "lxc_contexts"))
   917  }
   918  
   919  func loadLabels() {
   920  	labels = make(map[string]string)
   921  	in, err := openContextFile()
   922  	if err != nil {
   923  		return
   924  	}
   925  	defer in.Close()
   926  
   927  	scanner := bufio.NewScanner(in)
   928  
   929  	for scanner.Scan() {
   930  		line := bytes.TrimSpace(scanner.Bytes())
   931  		if len(line) == 0 {
   932  			// Skip blank lines
   933  			continue
   934  		}
   935  		if line[0] == ';' || line[0] == '#' {
   936  			// Skip comments
   937  			continue
   938  		}
   939  		fields := bytes.SplitN(line, []byte{'='}, 2)
   940  		if len(fields) != 2 {
   941  			continue
   942  		}
   943  		key, val := bytes.TrimSpace(fields[0]), bytes.TrimSpace(fields[1])
   944  		labels[string(key)] = string(bytes.Trim(val, `"`))
   945  	}
   946  
   947  	con, _ := NewContext(labels["file"])
   948  	con["level"] = fmt.Sprintf("s0:c%d,c%d", maxCategory-2, maxCategory-1)
   949  	privContainerMountLabel = con.get()
   950  	reserveLabel(privContainerMountLabel)
   951  }
   952  
   953  func label(key string) string {
   954  	loadLabelsOnce.Do(func() {
   955  		loadLabels()
   956  	})
   957  	return labels[key]
   958  }
   959  
   960  // kvmContainerLabels returns the default processLabel and mountLabel to be used
   961  // for kvm containers by the calling process.
   962  func kvmContainerLabels() (string, string) {
   963  	processLabel := label("kvm_process")
   964  	if processLabel == "" {
   965  		processLabel = label("process")
   966  	}
   967  
   968  	return addMcs(processLabel, label("file"))
   969  }
   970  
   971  // initContainerLabels returns the default processLabel and file labels to be
   972  // used for containers running an init system like systemd by the calling process.
   973  func initContainerLabels() (string, string) {
   974  	processLabel := label("init_process")
   975  	if processLabel == "" {
   976  		processLabel = label("process")
   977  	}
   978  
   979  	return addMcs(processLabel, label("file"))
   980  }
   981  
   982  // containerLabels returns an allocated processLabel and fileLabel to be used for
   983  // container labeling by the calling process.
   984  func containerLabels() (processLabel string, fileLabel string) {
   985  	if !getEnabled() {
   986  		return "", ""
   987  	}
   988  
   989  	processLabel = label("process")
   990  	fileLabel = label("file")
   991  	readOnlyFileLabel = label("ro_file")
   992  
   993  	if processLabel == "" || fileLabel == "" {
   994  		return "", fileLabel
   995  	}
   996  
   997  	if readOnlyFileLabel == "" {
   998  		readOnlyFileLabel = fileLabel
   999  	}
  1000  
  1001  	return addMcs(processLabel, fileLabel)
  1002  }
  1003  
  1004  func addMcs(processLabel, fileLabel string) (string, string) {
  1005  	scon, _ := NewContext(processLabel)
  1006  	if scon["level"] != "" {
  1007  		mcs := uniqMcs(CategoryRange)
  1008  		scon["level"] = mcs
  1009  		processLabel = scon.Get()
  1010  		scon, _ = NewContext(fileLabel)
  1011  		scon["level"] = mcs
  1012  		fileLabel = scon.Get()
  1013  	}
  1014  	return processLabel, fileLabel
  1015  }
  1016  
  1017  // securityCheckContext validates that the SELinux label is understood by the kernel
  1018  func securityCheckContext(val string) error {
  1019  	//nolint:gosec // ignore G306: permissions to be 0600 or less.
  1020  	return os.WriteFile(filepath.Join(getSelinuxMountPoint(), "context"), []byte(val), 0o644)
  1021  }
  1022  
  1023  // copyLevel returns a label with the MLS/MCS level from src label replaced on
  1024  // the dest label.
  1025  func copyLevel(src, dest string) (string, error) {
  1026  	if src == "" {
  1027  		return "", nil
  1028  	}
  1029  	if err := SecurityCheckContext(src); err != nil {
  1030  		return "", err
  1031  	}
  1032  	if err := SecurityCheckContext(dest); err != nil {
  1033  		return "", err
  1034  	}
  1035  	scon, err := NewContext(src)
  1036  	if err != nil {
  1037  		return "", err
  1038  	}
  1039  	tcon, err := NewContext(dest)
  1040  	if err != nil {
  1041  		return "", err
  1042  	}
  1043  	mcsDelete(tcon["level"])
  1044  	_ = mcsAdd(scon["level"])
  1045  	tcon["level"] = scon["level"]
  1046  	return tcon.Get(), nil
  1047  }
  1048  
  1049  // chcon changes the fpath file object to the SELinux label.
  1050  // If fpath is a directory and recurse is true, then chcon walks the
  1051  // directory tree setting the label.
  1052  func chcon(fpath string, label string, recurse bool) error {
  1053  	if fpath == "" {
  1054  		return ErrEmptyPath
  1055  	}
  1056  	if label == "" {
  1057  		return nil
  1058  	}
  1059  
  1060  	excludePaths := map[string]bool{
  1061  		"/":           true,
  1062  		"/bin":        true,
  1063  		"/boot":       true,
  1064  		"/dev":        true,
  1065  		"/etc":        true,
  1066  		"/etc/passwd": true,
  1067  		"/etc/pki":    true,
  1068  		"/etc/shadow": true,
  1069  		"/home":       true,
  1070  		"/lib":        true,
  1071  		"/lib64":      true,
  1072  		"/media":      true,
  1073  		"/opt":        true,
  1074  		"/proc":       true,
  1075  		"/root":       true,
  1076  		"/run":        true,
  1077  		"/sbin":       true,
  1078  		"/srv":        true,
  1079  		"/sys":        true,
  1080  		"/tmp":        true,
  1081  		"/usr":        true,
  1082  		"/var":        true,
  1083  		"/var/lib":    true,
  1084  		"/var/log":    true,
  1085  	}
  1086  
  1087  	if home := os.Getenv("HOME"); home != "" {
  1088  		excludePaths[home] = true
  1089  	}
  1090  
  1091  	if sudoUser := os.Getenv("SUDO_USER"); sudoUser != "" {
  1092  		if usr, err := user.Lookup(sudoUser); err == nil {
  1093  			excludePaths[usr.HomeDir] = true
  1094  		}
  1095  	}
  1096  
  1097  	if fpath != "/" {
  1098  		fpath = strings.TrimSuffix(fpath, "/")
  1099  	}
  1100  	if excludePaths[fpath] {
  1101  		return fmt.Errorf("SELinux relabeling of %s is not allowed", fpath)
  1102  	}
  1103  
  1104  	if !recurse {
  1105  		err := lSetFileLabel(fpath, label)
  1106  		if err != nil {
  1107  			// Check if file doesn't exist, must have been removed
  1108  			if errors.Is(err, os.ErrNotExist) {
  1109  				return nil
  1110  			}
  1111  			// Check if current label is correct on disk
  1112  			flabel, nerr := lFileLabel(fpath)
  1113  			if nerr == nil && flabel == label {
  1114  				return nil
  1115  			}
  1116  			// Check if file doesn't exist, must have been removed
  1117  			if errors.Is(nerr, os.ErrNotExist) {
  1118  				return nil
  1119  			}
  1120  			return err
  1121  		}
  1122  		return nil
  1123  	}
  1124  
  1125  	return rchcon(fpath, label)
  1126  }
  1127  
  1128  func rchcon(fpath, label string) error { //revive:disable:cognitive-complexity
  1129  	fastMode := false
  1130  	// If the current label matches the new label, assume
  1131  	// other labels are correct.
  1132  	if cLabel, err := lFileLabel(fpath); err == nil && cLabel == label {
  1133  		fastMode = true
  1134  	}
  1135  	return pwalkdir.Walk(fpath, func(p string, _ fs.DirEntry, _ error) error {
  1136  		if fastMode {
  1137  			if cLabel, err := lFileLabel(fpath); err == nil && cLabel == label {
  1138  				return nil
  1139  			}
  1140  		}
  1141  		err := lSetFileLabel(p, label)
  1142  		// Walk a file tree can race with removal, so ignore ENOENT.
  1143  		if errors.Is(err, os.ErrNotExist) {
  1144  			return nil
  1145  		}
  1146  		return err
  1147  	})
  1148  }
  1149  
  1150  // dupSecOpt takes an SELinux process label and returns security options that
  1151  // can be used to set the SELinux Type and Level for future container processes.
  1152  func dupSecOpt(src string) ([]string, error) {
  1153  	if src == "" {
  1154  		return nil, nil
  1155  	}
  1156  	con, err := NewContext(src)
  1157  	if err != nil {
  1158  		return nil, err
  1159  	}
  1160  	if con["user"] == "" ||
  1161  		con["role"] == "" ||
  1162  		con["type"] == "" {
  1163  		return nil, nil
  1164  	}
  1165  	dup := []string{
  1166  		"user:" + con["user"],
  1167  		"role:" + con["role"],
  1168  		"type:" + con["type"],
  1169  	}
  1170  
  1171  	if con["level"] != "" {
  1172  		dup = append(dup, "level:"+con["level"])
  1173  	}
  1174  
  1175  	return dup, nil
  1176  }
  1177  
  1178  // findUserInContext scans the reader for a valid SELinux context
  1179  // match that is verified with the verifier. Invalid contexts are
  1180  // skipped. It returns a matched context or an empty string if no
  1181  // match is found. If a scanner error occurs, it is returned.
  1182  func findUserInContext(context Context, r io.Reader, verifier func(string) error) (string, error) {
  1183  	fromRole := context["role"]
  1184  	fromType := context["type"]
  1185  	scanner := bufio.NewScanner(r)
  1186  
  1187  	for scanner.Scan() {
  1188  		fromConns := strings.Fields(scanner.Text())
  1189  		if len(fromConns) == 0 {
  1190  			// Skip blank lines
  1191  			continue
  1192  		}
  1193  
  1194  		line := fromConns[0]
  1195  
  1196  		if line[0] == ';' || line[0] == '#' {
  1197  			// Skip comments
  1198  			continue
  1199  		}
  1200  
  1201  		// user context files contexts are formatted as
  1202  		// role_r:type_t:s0 where the user is missing.
  1203  		lineArr := strings.SplitN(line, ":", 4)
  1204  		// skip context with typo, or role and type do not match
  1205  		if len(lineArr) != 3 ||
  1206  			lineArr[0] != fromRole ||
  1207  			lineArr[1] != fromType {
  1208  			continue
  1209  		}
  1210  
  1211  		for _, cc := range fromConns[1:] {
  1212  			toConns := strings.SplitN(cc, ":", 4)
  1213  			if len(toConns) != 3 {
  1214  				continue
  1215  			}
  1216  
  1217  			context["role"] = toConns[0]
  1218  			context["type"] = toConns[1]
  1219  
  1220  			outConn := context.get()
  1221  			if err := verifier(outConn); err != nil {
  1222  				continue
  1223  			}
  1224  
  1225  			return outConn, nil
  1226  		}
  1227  	}
  1228  	if err := scanner.Err(); err != nil {
  1229  		return "", fmt.Errorf("failed to scan for context: %w", err)
  1230  	}
  1231  
  1232  	return "", nil
  1233  }
  1234  
  1235  func getDefaultContextFromReaders(c *defaultSECtx) (string, error) {
  1236  	if c.verifier == nil {
  1237  		return "", ErrVerifierNil
  1238  	}
  1239  
  1240  	context, err := newContext(c.scon)
  1241  	if err != nil {
  1242  		return "", fmt.Errorf("failed to create label for %s: %w", c.scon, err)
  1243  	}
  1244  
  1245  	// set so the verifier validates the matched context with the provided user and level.
  1246  	context["user"] = c.user
  1247  	context["level"] = c.level
  1248  
  1249  	conn, err := findUserInContext(context, c.userRdr, c.verifier)
  1250  	if err != nil {
  1251  		return "", err
  1252  	}
  1253  
  1254  	if conn != "" {
  1255  		return conn, nil
  1256  	}
  1257  
  1258  	conn, err = findUserInContext(context, c.defaultRdr, c.verifier)
  1259  	if err != nil {
  1260  		return "", err
  1261  	}
  1262  
  1263  	if conn != "" {
  1264  		return conn, nil
  1265  	}
  1266  
  1267  	return "", fmt.Errorf("context %q not found: %w", c.scon, ErrContextMissing)
  1268  }
  1269  
  1270  func getDefaultContextWithLevel(user, level, scon string) (string, error) {
  1271  	userPath := filepath.Join(policyRoot(), selinuxUsersDir, user)
  1272  	fu, err := os.Open(userPath)
  1273  	if err != nil {
  1274  		return "", err
  1275  	}
  1276  	defer fu.Close()
  1277  
  1278  	defaultPath := filepath.Join(policyRoot(), defaultContexts)
  1279  	fd, err := os.Open(defaultPath)
  1280  	if err != nil {
  1281  		return "", err
  1282  	}
  1283  	defer fd.Close()
  1284  
  1285  	c := defaultSECtx{
  1286  		user:       user,
  1287  		level:      level,
  1288  		scon:       scon,
  1289  		userRdr:    fu,
  1290  		defaultRdr: fd,
  1291  		verifier:   securityCheckContext,
  1292  	}
  1293  
  1294  	return getDefaultContextFromReaders(&c)
  1295  }
  1296  

View as plain text