github.com/hugh712/snapd@v0.0.0-20200910133618-1a99902bd583/interfaces/seccomp/backend.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2016-2018 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  // Package seccomp implements integration between snapd and snap-confine around
    21  // seccomp.
    22  //
    23  // Snappy creates so-called seccomp profiles for each application (for each
    24  // snap) present in the system.  Upon each execution of snap-confine, the
    25  // profile is read and "compiled" to an eBPF program and injected into the
    26  // kernel for the duration of the execution of the process.
    27  //
    28  // There is no binary cache for seccomp, each time the launcher starts an
    29  // application the profile is parsed and re-compiled.
    30  //
    31  // The actual profiles are stored in /var/lib/snappy/seccomp/bpf/*.{src,bin}.
    32  // This directory is hard-coded in snap-confine.
    33  package seccomp
    34  
    35  import (
    36  	"bytes"
    37  	"fmt"
    38  	"os"
    39  	"path/filepath"
    40  	"runtime"
    41  	"strings"
    42  
    43  	"github.com/snapcore/snapd/arch"
    44  	"github.com/snapcore/snapd/dirs"
    45  	"github.com/snapcore/snapd/interfaces"
    46  	"github.com/snapcore/snapd/osutil"
    47  	"github.com/snapcore/snapd/release"
    48  	"github.com/snapcore/snapd/sandbox/apparmor"
    49  	"github.com/snapcore/snapd/sandbox/seccomp"
    50  	"github.com/snapcore/snapd/snap"
    51  	"github.com/snapcore/snapd/snapdtool"
    52  	"github.com/snapcore/snapd/strutil"
    53  	"github.com/snapcore/snapd/timings"
    54  )
    55  
    56  var (
    57  	kernelFeatures         = seccomp.Actions
    58  	dpkgKernelArchitecture = arch.DpkgKernelArchitecture
    59  	releaseInfoId          = release.ReleaseInfo.ID
    60  	releaseInfoVersionId   = release.ReleaseInfo.VersionID
    61  	requiresSocketcall     = requiresSocketcallImpl
    62  
    63  	snapSeccompVersionInfo = snapSeccompVersionInfoImpl
    64  	seccompCompilerLookup  = snapdtool.InternalToolPath
    65  )
    66  
    67  func snapSeccompVersionInfoImpl(c Compiler) (seccomp.VersionInfo, error) {
    68  	return c.VersionInfo()
    69  }
    70  
    71  type Compiler interface {
    72  	Compile(in, out string) error
    73  	VersionInfo() (seccomp.VersionInfo, error)
    74  }
    75  
    76  // Backend is responsible for maintaining seccomp profiles for snap-confine.
    77  type Backend struct {
    78  	snapSeccomp Compiler
    79  	versionInfo seccomp.VersionInfo
    80  }
    81  
    82  var globalProfileLE = []byte{
    83  	0x20, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x04, 0x3e, 0x00, 0x00, 0xc0,
    84  	0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x35, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x40,
    85  	0x15, 0x00, 0x00, 0x0d, 0xff, 0xff, 0xff, 0xff, 0x15, 0x00, 0x06, 0x0c, 0x10, 0x00, 0x00, 0x00,
    86  	0x15, 0x00, 0x00, 0x02, 0xb7, 0x00, 0x00, 0xc0, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    87  	0x15, 0x00, 0x03, 0x09, 0x1d, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x08, 0x15, 0x00, 0x00, 0xc0,
    88  	0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x06, 0x36, 0x00, 0x00, 0x00,
    89  	0x20, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    90  	0x15, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00,
    91  	0x15, 0x00, 0x00, 0x01, 0x12, 0x54, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x01, 0x00, 0x05, 0x00,
    92  	0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x7f,
    93  }
    94  
    95  var globalProfileBE = []byte{
    96  	0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x15, 0x00, 0x08, 0x80, 0x00, 0x00, 0x16,
    97  	0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x15, 0x00, 0x06, 0x00, 0x00, 0x00, 0x36,
    98  	0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    99  	0x00, 0x15, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1c,
   100  	0x00, 0x15, 0x00, 0x01, 0x00, 0x00, 0x54, 0x12, 0x00, 0x06, 0x00, 0x00, 0x00, 0x05, 0x00, 0x01,
   101  	0x00, 0x06, 0x00, 0x00, 0x7f, 0xff, 0x00, 0x00,
   102  }
   103  
   104  func isBigEndian() bool {
   105  	switch runtime.GOARCH {
   106  	case "s390x":
   107  		return true
   108  	case "ppc":
   109  		return true
   110  		// TODO: perhaps more here?
   111  	}
   112  	return false
   113  }
   114  
   115  // Initialize ensures that the global profile is on disk and interrogates
   116  // libseccomp wrapper to generate a version string that will be used to
   117  // determine if we need to recompile seccomp policy due to system
   118  // changes outside of snapd.
   119  func (b *Backend) Initialize(*interfaces.SecurityBackendOptions) error {
   120  	dir := dirs.SnapSeccompDir
   121  	fname := "global.bin"
   122  	var globalProfile []byte
   123  	if isBigEndian() {
   124  		globalProfile = globalProfileBE
   125  	} else {
   126  		globalProfile = globalProfileLE
   127  	}
   128  	content := map[string]osutil.FileState{
   129  		fname: &osutil.MemoryFileState{Content: globalProfile, Mode: 0644},
   130  	}
   131  	if err := os.MkdirAll(dir, 0755); err != nil {
   132  		return fmt.Errorf("cannot create directory for seccomp profiles %q: %s", dir, err)
   133  	}
   134  	_, _, err := osutil.EnsureDirState(dir, fname, content)
   135  	if err != nil {
   136  		return fmt.Errorf("cannot synchronize global seccomp profile: %s", err)
   137  	}
   138  
   139  	b.snapSeccomp, err = seccomp.NewCompiler(seccompCompilerLookup)
   140  	if err != nil {
   141  		return fmt.Errorf("cannot initialize seccomp profile compiler: %v", err)
   142  	}
   143  
   144  	versionInfo, err := snapSeccompVersionInfo(b.snapSeccomp)
   145  	if err != nil {
   146  		return fmt.Errorf("cannot obtain snap-seccomp version information: %v", err)
   147  	}
   148  	b.versionInfo = versionInfo
   149  	return nil
   150  }
   151  
   152  // Name returns the name of the backend.
   153  func (b *Backend) Name() interfaces.SecuritySystem {
   154  	return interfaces.SecuritySecComp
   155  }
   156  
   157  func bpfSrcPath(srcName string) string {
   158  	return filepath.Join(dirs.SnapSeccompDir, srcName)
   159  }
   160  
   161  func bpfBinPath(srcName string) string {
   162  	return filepath.Join(dirs.SnapSeccompDir, strings.TrimSuffix(srcName, ".src")+".bin")
   163  }
   164  
   165  func parallelCompile(compiler Compiler, profiles []string) error {
   166  	if len(profiles) == 0 {
   167  		// no profiles, nothing to do
   168  		return nil
   169  	}
   170  
   171  	profilesQueue := make(chan string, len(profiles))
   172  	numWorkers := runtime.NumCPU()
   173  	if numWorkers >= 2 {
   174  		numWorkers -= 1
   175  	}
   176  	if numWorkers > len(profiles) {
   177  		numWorkers = len(profiles)
   178  	}
   179  	resultsBufferSize := numWorkers * 2
   180  	if resultsBufferSize > len(profiles) {
   181  		resultsBufferSize = len(profiles)
   182  	}
   183  	res := make(chan error, resultsBufferSize)
   184  
   185  	// launch as many workers as we have CPUs
   186  	for i := 0; i < numWorkers; i++ {
   187  		go func() {
   188  			for {
   189  				profile, ok := <-profilesQueue
   190  				if !ok {
   191  					break
   192  				}
   193  				in := bpfSrcPath(profile)
   194  				out := bpfBinPath(profile)
   195  				// remove the old profile first so that we are
   196  				// not loading it accidentally should the
   197  				// compilation fail
   198  				if err := os.Remove(out); err != nil && !os.IsNotExist(err) {
   199  					res <- err
   200  					continue
   201  				}
   202  
   203  				// snap-seccomp uses AtomicWriteFile internally, on failure the
   204  				// output file is unlinked
   205  				if err := compiler.Compile(in, out); err != nil {
   206  					res <- fmt.Errorf("cannot compile %s: %v", in, err)
   207  				} else {
   208  					res <- nil
   209  				}
   210  			}
   211  		}()
   212  	}
   213  
   214  	for _, p := range profiles {
   215  		profilesQueue <- p
   216  	}
   217  	// signal workers to exit
   218  	close(profilesQueue)
   219  
   220  	var firstErr error
   221  	for i := 0; i < len(profiles); i++ {
   222  		maybeErr := <-res
   223  		if maybeErr != nil && firstErr == nil {
   224  			firstErr = maybeErr
   225  		}
   226  
   227  	}
   228  
   229  	// not expecting any more results
   230  	close(res)
   231  
   232  	if firstErr != nil {
   233  		for _, p := range profiles {
   234  			out := bpfBinPath(p)
   235  			// unlink all profiles that could have been successfully
   236  			// compiled
   237  			os.Remove(out)
   238  		}
   239  
   240  	}
   241  	return firstErr
   242  }
   243  
   244  // Setup creates seccomp profiles specific to a given snap.
   245  // The snap can be in developer mode to make security violations non-fatal to
   246  // the offending application process.
   247  //
   248  // This method should be called after changing plug, slots, connections between
   249  // them or application present in the snap.
   250  func (b *Backend) Setup(snapInfo *snap.Info, opts interfaces.ConfinementOptions, repo *interfaces.Repository, tm timings.Measurer) error {
   251  	snapName := snapInfo.InstanceName()
   252  	// Get the snippets that apply to this snap
   253  	spec, err := repo.SnapSpecification(b.Name(), snapName)
   254  	if err != nil {
   255  		return fmt.Errorf("cannot obtain seccomp specification for snap %q: %s", snapName, err)
   256  	}
   257  
   258  	// Get the snippets that apply to this snap
   259  	content, err := b.deriveContent(spec.(*Specification), opts, snapInfo)
   260  	if err != nil {
   261  		return fmt.Errorf("cannot obtain expected security files for snap %q: %s", snapName, err)
   262  	}
   263  
   264  	glob := interfaces.SecurityTagGlob(snapName) + ".src"
   265  	dir := dirs.SnapSeccompDir
   266  	if err := os.MkdirAll(dir, 0755); err != nil {
   267  		return fmt.Errorf("cannot create directory for seccomp profiles %q: %s", dir, err)
   268  	}
   269  	// There is a delicate interaction between `snap run`, `snap-confine`
   270  	// and compilation of profiles:
   271  	// - whenever profiles need to be rebuilt due to system-key change,
   272  	//   `snap run` detects the system-key mismatch and waits for snapd
   273  	//   (system key is only updated once all security backends have
   274  	//   finished their job)
   275  	// - whenever the binary file does not exist, `snap-confine` will poll
   276  	//   and wait for SNAP_CONFINE_MAX_PROFILE_WAIT, if the profile does not
   277  	//   appear in that time, `snap-confine` will fail and die
   278  	changed, removed, err := osutil.EnsureDirState(dir, glob, content)
   279  	if err != nil {
   280  		return fmt.Errorf("cannot synchronize security files for snap %q: %s", snapName, err)
   281  	}
   282  	for _, c := range removed {
   283  		err := os.Remove(bpfBinPath(c))
   284  		if err != nil && !os.IsNotExist(err) {
   285  			return err
   286  		}
   287  	}
   288  
   289  	return parallelCompile(b.snapSeccomp, changed)
   290  }
   291  
   292  // Remove removes seccomp profiles of a given snap.
   293  func (b *Backend) Remove(snapName string) error {
   294  	glob := interfaces.SecurityTagGlob(snapName)
   295  	_, _, err := osutil.EnsureDirState(dirs.SnapSeccompDir, glob, nil)
   296  	if err != nil {
   297  		return fmt.Errorf("cannot synchronize security files for snap %q: %s", snapName, err)
   298  	}
   299  	return nil
   300  }
   301  
   302  // Obtain the privilege dropping snippet
   303  func uidGidChownSnippet(name string) (string, error) {
   304  	tmp := strings.Replace(privDropAndChownSyscalls, "###USERNAME###", name, -1)
   305  	return strings.Replace(tmp, "###GROUP###", name, -1), nil
   306  }
   307  
   308  // deriveContent combines security snippets collected from all the interfaces
   309  // affecting a given snap into a content map applicable to EnsureDirState.
   310  func (b *Backend) deriveContent(spec *Specification, opts interfaces.ConfinementOptions, snapInfo *snap.Info) (content map[string]osutil.FileState, err error) {
   311  	// Some base snaps and systems require the socketcall() in the default
   312  	// template
   313  	addSocketcall := requiresSocketcall(snapInfo.Base)
   314  
   315  	var uidGidChownSyscalls bytes.Buffer
   316  	if len(snapInfo.SystemUsernames) == 0 {
   317  		uidGidChownSyscalls.WriteString(barePrivDropSyscalls)
   318  	} else {
   319  		for _, id := range snapInfo.SystemUsernames {
   320  			syscalls, err := uidGidChownSnippet(id.Name)
   321  			if err != nil {
   322  				return nil, fmt.Errorf(`cannot calculate syscalls for "%s": %s`, id, err)
   323  			}
   324  			uidGidChownSyscalls.WriteString(syscalls)
   325  		}
   326  		uidGidChownSyscalls.WriteString(rootSetUidGidSyscalls)
   327  	}
   328  
   329  	for _, hookInfo := range snapInfo.Hooks {
   330  		if content == nil {
   331  			content = make(map[string]osutil.FileState)
   332  		}
   333  		securityTag := hookInfo.SecurityTag()
   334  
   335  		path := securityTag + ".src"
   336  		content[path] = &osutil.MemoryFileState{
   337  			Content: generateContent(opts, spec.SnippetForTag(securityTag), addSocketcall, b.versionInfo, uidGidChownSyscalls.String()),
   338  			Mode:    0644,
   339  		}
   340  	}
   341  	for _, appInfo := range snapInfo.Apps {
   342  		if content == nil {
   343  			content = make(map[string]osutil.FileState)
   344  		}
   345  		securityTag := appInfo.SecurityTag()
   346  		path := securityTag + ".src"
   347  		content[path] = &osutil.MemoryFileState{
   348  			Content: generateContent(opts, spec.SnippetForTag(securityTag), addSocketcall, b.versionInfo, uidGidChownSyscalls.String()),
   349  			Mode:    0644,
   350  		}
   351  	}
   352  
   353  	return content, nil
   354  }
   355  
   356  func generateContent(opts interfaces.ConfinementOptions, snippetForTag string, addSocketcall bool, versionInfo seccomp.VersionInfo, uidGidChownSyscalls string) []byte {
   357  	var buffer bytes.Buffer
   358  
   359  	if versionInfo != "" {
   360  		buffer.WriteString("# snap-seccomp version information:\n")
   361  		fmt.Fprintf(&buffer, "# %s\n", versionInfo)
   362  	}
   363  
   364  	if opts.Classic && !opts.JailMode {
   365  		// NOTE: This is understood by snap-confine
   366  		buffer.WriteString("@unrestricted\n")
   367  	}
   368  	if opts.DevMode && !opts.JailMode {
   369  		// NOTE: This is understood by snap-confine
   370  		buffer.WriteString("@complain\n")
   371  		if !seccomp.SupportsAction("log") {
   372  			buffer.WriteString("# complain mode logging unavailable\n")
   373  		}
   374  	}
   375  
   376  	buffer.Write(defaultTemplate)
   377  	buffer.WriteString(snippetForTag)
   378  	buffer.WriteString(uidGidChownSyscalls)
   379  
   380  	// For systems with partial or missing AppArmor support we need to apply
   381  	// a workaround to avoid failing hooks. See description in template.go
   382  	// for more details.
   383  	if apparmor.ProbedLevel() != apparmor.Full {
   384  		buffer.WriteString(bindSyscallWorkaround)
   385  	}
   386  
   387  	if addSocketcall {
   388  		buffer.WriteString(socketcallSyscallDeprecated)
   389  	}
   390  
   391  	return buffer.Bytes()
   392  }
   393  
   394  // NewSpecification returns an empty seccomp specification.
   395  func (b *Backend) NewSpecification() interfaces.Specification {
   396  	return &Specification{}
   397  }
   398  
   399  // SandboxFeatures returns the list of seccomp features supported by the kernel
   400  // and userspace.
   401  func (b *Backend) SandboxFeatures() []string {
   402  	features := kernelFeatures()
   403  	tags := make([]string, 0, len(features)+1)
   404  	for _, feature := range features {
   405  		// Prepend "kernel:" to apparmor kernel features to namespace
   406  		// them.
   407  		tags = append(tags, "kernel:"+feature)
   408  	}
   409  	tags = append(tags, "bpf-argument-filtering")
   410  
   411  	if res, err := b.versionInfo.HasFeature("bpf-actlog"); err == nil && res {
   412  		tags = append(tags, "bpf-actlog")
   413  	}
   414  
   415  	return tags
   416  }
   417  
   418  // Determine if the system requires the use of socketcall(). Factors:
   419  // - if the kernel architecture is amd64, armhf or arm64, do not require
   420  //   socketcall (unused on these architectures)
   421  // - if the kernel architecture is i386 or s390x
   422  //   - if the kernel is < 4.3, force the use of socketcall()
   423  //   - for backwards compatibility, if the system is Ubuntu 14.04 or lower,
   424  //     force use of socketcall()
   425  //   - for backwards compatibility, if the base snap is unspecified, "core" or
   426  //     "core16", then force use of socketcall()
   427  //   - otherwise (ie, if new enough kernel, not 14.04, and a non-16 base
   428  //     snap), don't force use of socketcall()
   429  // - if the kernel architecture is not any of the above, force the use of
   430  //   socketcall()
   431  func requiresSocketcallImpl(baseSnap string) bool {
   432  	switch dpkgKernelArchitecture() {
   433  	case "i386", "s390x":
   434  		// glibc sysdeps/unix/sysv/linux/i386/kernel-features.h and
   435  		// sysdeps/unix/sysv/linux/s390/kernel-features.h added the
   436  		// individual socket syscalls in 4.3.
   437  		if cmp, _ := strutil.VersionCompare(osutil.KernelVersion(), "4.3"); cmp < 0 {
   438  			return true
   439  		}
   440  
   441  		// For now, on 14.04, always require socketcall()
   442  		if releaseInfoId == "ubuntu" {
   443  			if cmp, _ := strutil.VersionCompare(releaseInfoVersionId, "14.04"); cmp <= 0 {
   444  				return true
   445  			}
   446  		}
   447  
   448  		// Detect when the base snap requires the use of socketcall().
   449  		//
   450  		// TODO: eventually try to auto-detect this. For now, err on
   451  		// the side of security and only require it for base snaps
   452  		// where we know we want it added. Technically, core16's glibc
   453  		// is new enough, but it always had socketcall in the template,
   454  		// so ensure backwards compatibility.
   455  		if baseSnap == "" || baseSnap == "core" || baseSnap == "core16" {
   456  			return true
   457  		}
   458  
   459  		// If none of the above, we don't need the syscall
   460  		return false
   461  	case "powerpc":
   462  		// glibc's sysdeps/unix/sysv/linux/powerpc/kernel-features.h
   463  		// states that the individual syscalls are all available as of
   464  		// 2.6.37. snapd isn't expected to run on these kernels so just
   465  		// default to unneeded.
   466  		return false
   467  	case "sparc", "sparc64":
   468  		// glibc's sysdeps/unix/sysv/linux/sparc/kernel-features.h
   469  		// indicates that socketcall() is used and the individual
   470  		// syscalls are undefined.
   471  		return true
   472  	default:
   473  		// amd64, arm64, armhf, ppc64el, etc
   474  		// glibc's sysdeps/unix/sysv/linux/kernel-features.h says that
   475  		// __ASSUME_SOCKETCALL will be defined on archs that need it.
   476  		// Modern architectures do not implement the socketcall()
   477  		// syscall and all the older architectures except sparc (see
   478  		// above) have introduced the individual syscalls, so default
   479  		// to unneeded.
   480  		return false
   481  	}
   482  
   483  	// If we got here, something went wrong, but if the code above changes
   484  	// the compiler will complain about the lack of 'return'.
   485  }
   486  
   487  // MockSnapSeccompVersionInfo is for use in tests only.
   488  func MockSnapSeccompVersionInfo(versionInfo string) (restore func()) {
   489  	old := snapSeccompVersionInfo
   490  	snapSeccompVersionInfo = func(c Compiler) (seccomp.VersionInfo, error) {
   491  		return seccomp.VersionInfo(versionInfo), nil
   492  	}
   493  	return func() {
   494  		snapSeccompVersionInfo = old
   495  	}
   496  }