github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/cmd/snap-update-ns/trespassing_test.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2017-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 main_test
    21  
    22  import (
    23  	"syscall"
    24  
    25  	. "gopkg.in/check.v1"
    26  
    27  	update "github.com/snapcore/snapd/cmd/snap-update-ns"
    28  	"github.com/snapcore/snapd/osutil"
    29  	"github.com/snapcore/snapd/testutil"
    30  )
    31  
    32  type trespassingSuite struct {
    33  	testutil.BaseTest
    34  	sys *testutil.SyscallRecorder
    35  }
    36  
    37  var _ = Suite(&trespassingSuite{})
    38  
    39  func (s *trespassingSuite) SetUpTest(c *C) {
    40  	s.BaseTest.SetUpTest(c)
    41  	s.sys = &testutil.SyscallRecorder{}
    42  	s.BaseTest.AddCleanup(update.MockSystemCalls(s.sys))
    43  }
    44  
    45  func (s *trespassingSuite) TearDownTest(c *C) {
    46  	s.BaseTest.TearDownTest(c)
    47  	s.sys.CheckForStrayDescriptors(c)
    48  }
    49  
    50  // AddUnrestrictedPaths and IsRestricted
    51  
    52  func (s *trespassingSuite) TestAddUnrestrictedPaths(c *C) {
    53  	a := &update.Assumptions{}
    54  	c.Assert(a.IsRestricted("/etc/test.conf"), Equals, true)
    55  
    56  	a.AddUnrestrictedPaths("/etc")
    57  	c.Assert(a.IsRestricted("/etc/test.conf"), Equals, false)
    58  	c.Assert(a.IsRestricted("/etc/"), Equals, false)
    59  	c.Assert(a.IsRestricted("/etc"), Equals, false)
    60  	c.Assert(a.IsRestricted("/etc2"), Equals, true)
    61  
    62  	a.AddUnrestrictedPaths("/")
    63  	c.Assert(a.IsRestricted("/foo"), Equals, false)
    64  
    65  }
    66  
    67  func (s *trespassingSuite) TestMockUnrestrictedPaths(c *C) {
    68  	a := &update.Assumptions{}
    69  	c.Assert(a.IsRestricted("/etc/test.conf"), Equals, true)
    70  	restore := a.MockUnrestrictedPaths("/etc/")
    71  	c.Assert(a.IsRestricted("/etc/test.conf"), Equals, false)
    72  	restore()
    73  	c.Assert(a.IsRestricted("/etc/test.conf"), Equals, true)
    74  }
    75  
    76  // canWriteToDirectory and AddChange
    77  
    78  // We are not allowed to write to ext4.
    79  func (s *trespassingSuite) TestCanWriteToDirectoryWritableExt4(c *C) {
    80  	a := &update.Assumptions{}
    81  
    82  	path := "/etc"
    83  	fd, err := s.sys.Open(path, syscall.O_DIRECTORY, 0)
    84  	c.Assert(err, IsNil)
    85  	defer s.sys.Close(fd)
    86  
    87  	s.sys.InsertFstatfsResult(`fstatfs 3 <ptr>`, syscall.Statfs_t{Type: update.Ext4Magic})
    88  	s.sys.InsertFstatResult(`fstat 3 <ptr>`, syscall.Stat_t{})
    89  
    90  	ok, err := a.CanWriteToDirectory(fd, path)
    91  	c.Assert(err, IsNil)
    92  	c.Assert(ok, Equals, false)
    93  }
    94  
    95  // We are allowed to write to ext4 that was mounted read-only.
    96  func (s *trespassingSuite) TestCanWriteToDirectoryReadOnlyExt4(c *C) {
    97  	a := &update.Assumptions{}
    98  
    99  	path := "/etc"
   100  	fd, err := s.sys.Open(path, syscall.O_DIRECTORY, 0)
   101  	c.Assert(err, IsNil)
   102  	defer s.sys.Close(fd)
   103  
   104  	s.sys.InsertFstatfsResult(`fstatfs 3 <ptr>`, syscall.Statfs_t{Type: update.Ext4Magic, Flags: update.StReadOnly})
   105  	s.sys.InsertFstatResult(`fstat 3 <ptr>`, syscall.Stat_t{})
   106  
   107  	ok, err := a.CanWriteToDirectory(fd, path)
   108  	c.Assert(err, IsNil)
   109  	c.Assert(ok, Equals, true)
   110  }
   111  
   112  // We are not allowed to write to tmpfs.
   113  func (s *trespassingSuite) TestCanWriteToDirectoryTmpfs(c *C) {
   114  	a := &update.Assumptions{}
   115  
   116  	path := "/etc"
   117  	fd, err := s.sys.Open(path, syscall.O_DIRECTORY, 0)
   118  	c.Assert(err, IsNil)
   119  	defer s.sys.Close(fd)
   120  
   121  	s.sys.InsertFstatfsResult(`fstatfs 3 <ptr>`, syscall.Statfs_t{Type: update.TmpfsMagic})
   122  	s.sys.InsertFstatResult(`fstat 3 <ptr>`, syscall.Stat_t{})
   123  
   124  	ok, err := a.CanWriteToDirectory(fd, path)
   125  	c.Assert(err, IsNil)
   126  	c.Assert(ok, Equals, false)
   127  }
   128  
   129  // We are allowed to write to tmpfs that was mounted by snapd.
   130  func (s *trespassingSuite) TestCanWriteToDirectoryTmpfsMountedBySnapd(c *C) {
   131  	a := &update.Assumptions{}
   132  
   133  	path := "/etc"
   134  	fd, err := s.sys.Open(path, syscall.O_DIRECTORY, 0)
   135  	c.Assert(err, IsNil)
   136  	defer s.sys.Close(fd)
   137  
   138  	s.sys.InsertFstatfsResult(`fstatfs 3 <ptr>`, syscall.Statfs_t{Type: update.TmpfsMagic})
   139  	s.sys.InsertFstatResult(`fstat 3 <ptr>`, syscall.Stat_t{})
   140  
   141  	a.AddChange(&update.Change{
   142  		Action: update.Mount,
   143  		Entry:  osutil.MountEntry{Type: "tmpfs", Dir: path}})
   144  
   145  	ok, err := a.CanWriteToDirectory(fd, path)
   146  	c.Assert(err, IsNil)
   147  	c.Assert(ok, Equals, true)
   148  }
   149  
   150  // We are allowed to write to tmpfs that was mounted by snapd in another run.
   151  func (s *trespassingSuite) TestCanWriteToDirectoryTmpfsMountedBySnapdEarlier(c *C) {
   152  	a := &update.Assumptions{}
   153  
   154  	path := "/etc"
   155  	fd, err := s.sys.Open(path, syscall.O_DIRECTORY, 0)
   156  	c.Assert(err, IsNil)
   157  	defer s.sys.Close(fd)
   158  
   159  	s.sys.InsertFstatfsResult(`fstatfs 3 <ptr>`, syscall.Statfs_t{Type: update.TmpfsMagic})
   160  	s.sys.InsertFstatResult(`fstat 3 <ptr>`, syscall.Stat_t{})
   161  
   162  	a.AddChange(&update.Change{
   163  		Action: update.Keep,
   164  		Entry:  osutil.MountEntry{Type: "tmpfs", Dir: path}})
   165  
   166  	ok, err := a.CanWriteToDirectory(fd, path)
   167  	c.Assert(err, IsNil)
   168  	c.Assert(ok, Equals, true)
   169  }
   170  
   171  // We are allowed to write to directory beneath a tmpfs that was mounted by snapd.
   172  func (s *trespassingSuite) TestCanWriteToDirectoryUnderTmpfsMountedBySnapd(c *C) {
   173  	a := &update.Assumptions{}
   174  
   175  	fd, err := s.sys.Open("/etc", syscall.O_DIRECTORY, 0)
   176  	c.Assert(err, IsNil)
   177  	defer s.sys.Close(fd)
   178  
   179  	s.sys.InsertFstatfsResult(`fstatfs 3 <ptr>`, syscall.Statfs_t{Type: update.TmpfsMagic})
   180  	s.sys.InsertFstatResult(`fstat 3 <ptr>`, syscall.Stat_t{Dev: 0x42})
   181  
   182  	a.AddChange(&update.Change{
   183  		Action: update.Mount,
   184  		Entry:  osutil.MountEntry{Type: "tmpfs", Dir: "/etc"}})
   185  
   186  	ok, err := a.CanWriteToDirectory(fd, "/etc")
   187  	c.Assert(err, IsNil)
   188  	c.Assert(ok, Equals, true)
   189  
   190  	// Now we have primed the assumption state with knowledge of 0x42 device as
   191  	// a verified tmpfs.  We can now exploit it by trying to write to
   192  	// /etc/conf.d and seeing that is allowed even though /etc/conf.d itself is
   193  	// not a mount point representing tmpfs.
   194  
   195  	fd2, err := s.sys.Open("/etc/conf.d", syscall.O_DIRECTORY, 0)
   196  	c.Assert(err, IsNil)
   197  	defer s.sys.Close(fd2)
   198  
   199  	s.sys.InsertFstatfsResult(`fstatfs 4 <ptr>`, syscall.Statfs_t{Type: update.TmpfsMagic})
   200  	s.sys.InsertFstatResult(`fstat 4 <ptr>`, syscall.Stat_t{Dev: 0x42})
   201  
   202  	ok, err = a.CanWriteToDirectory(fd2, "/etc/conf.d")
   203  	c.Assert(err, IsNil)
   204  	c.Assert(ok, Equals, true)
   205  }
   206  
   207  // We are allowed to write to directory which is a bind mount of something, beneath a tmpfs that was mounted by snapd.
   208  func (s *trespassingSuite) TestCanWriteToDirectoryUnderReboundTmpfsMountedBySnapd(c *C) {
   209  	a := &update.Assumptions{}
   210  
   211  	fd, err := s.sys.Open("/etc", syscall.O_DIRECTORY, 0)
   212  	c.Assert(err, IsNil)
   213  	c.Assert(fd, Equals, 3)
   214  	defer s.sys.Close(fd)
   215  
   216  	s.sys.InsertFstatfsResult(`fstatfs 3 <ptr>`, syscall.Statfs_t{Type: update.TmpfsMagic})
   217  	s.sys.InsertFstatResult(`fstat 3 <ptr>`, syscall.Stat_t{Dev: 0x42})
   218  
   219  	a.AddChange(&update.Change{
   220  		Action: update.Mount,
   221  		Entry:  osutil.MountEntry{Type: "tmpfs", Dir: "/etc"}})
   222  
   223  	ok, err := a.CanWriteToDirectory(fd, "/etc")
   224  	c.Assert(err, IsNil)
   225  	c.Assert(ok, Equals, true)
   226  
   227  	// Now we have primed the assumption state with knowledge of 0x42 device as
   228  	// a verified tmpfs. Unlike in the test above though the directory
   229  	// /etc/conf.d is a bind mount from another tmpfs that we know nothing
   230  	// about.
   231  	fd2, err := s.sys.Open("/etc/conf.d", syscall.O_DIRECTORY, 0)
   232  	c.Assert(err, IsNil)
   233  	c.Assert(fd2, Equals, 4)
   234  	defer s.sys.Close(fd2)
   235  
   236  	s.sys.InsertFstatfsResult(`fstatfs 4 <ptr>`, syscall.Statfs_t{Type: update.TmpfsMagic})
   237  	s.sys.InsertFstatResult(`fstat 4 <ptr>`, syscall.Stat_t{Dev: 0xdeadbeef})
   238  
   239  	ok, err = a.CanWriteToDirectory(fd2, "/etc/conf.d")
   240  	c.Assert(err, IsNil)
   241  	c.Assert(ok, Equals, false)
   242  }
   243  
   244  // We are allowed to write to an unrestricted path.
   245  func (s *trespassingSuite) TestCanWriteToDirectoryUnrestricted(c *C) {
   246  	a := &update.Assumptions{}
   247  
   248  	path := "/var/snap/foo/common"
   249  	fd, err := s.sys.Open(path, syscall.O_DIRECTORY, 0)
   250  	c.Assert(err, IsNil)
   251  	defer s.sys.Close(fd)
   252  
   253  	s.sys.InsertFstatfsResult(`fstatfs 3 <ptr>`, syscall.Statfs_t{Type: update.Ext4Magic})
   254  	s.sys.InsertFstatResult(`fstat 3 <ptr>`, syscall.Stat_t{})
   255  
   256  	a.AddUnrestrictedPaths(path)
   257  
   258  	ok, err := a.CanWriteToDirectory(fd, path)
   259  	c.Assert(err, IsNil)
   260  	c.Assert(ok, Equals, true)
   261  }
   262  
   263  // Errors from fstatfs are propagated to the caller.
   264  func (s *trespassingSuite) TestCanWriteToDirectoryErrorsFstatfs(c *C) {
   265  	a := &update.Assumptions{}
   266  
   267  	path := "/etc"
   268  	fd, err := s.sys.Open(path, syscall.O_DIRECTORY, 0)
   269  	c.Assert(err, IsNil)
   270  	defer s.sys.Close(fd)
   271  
   272  	s.sys.InsertFault(`fstatfs 3 <ptr>`, errTesting)
   273  
   274  	ok, err := a.CanWriteToDirectory(fd, path)
   275  	c.Assert(err, ErrorMatches, `cannot fstatfs "/etc": testing`)
   276  	c.Assert(ok, Equals, false)
   277  }
   278  
   279  // Errors from fstat are propagated to the caller.
   280  func (s *trespassingSuite) TestCanWriteToDirectoryErrorsFstat(c *C) {
   281  	a := &update.Assumptions{}
   282  
   283  	path := "/etc"
   284  	fd, err := s.sys.Open(path, syscall.O_DIRECTORY, 0)
   285  	c.Assert(err, IsNil)
   286  	defer s.sys.Close(fd)
   287  
   288  	s.sys.InsertFstatfsResult(`fstatfs 3 <ptr>`, syscall.Statfs_t{})
   289  	s.sys.InsertFault(`fstat 3 <ptr>`, errTesting)
   290  
   291  	ok, err := a.CanWriteToDirectory(fd, path)
   292  	c.Assert(err, ErrorMatches, `cannot fstat "/etc": testing`)
   293  	c.Assert(ok, Equals, false)
   294  }
   295  
   296  // RestrictionsFor, Check and LiftRestrictions
   297  
   298  func (s *trespassingSuite) TestRestrictionsForEtc(c *C) {
   299  	a := &update.Assumptions{}
   300  
   301  	// There are restrictions for writing in /etc.
   302  	rs := a.RestrictionsFor("/etc/test.conf")
   303  	c.Assert(rs, NotNil)
   304  
   305  	fd, err := s.sys.Open("/etc", syscall.O_DIRECTORY, 0)
   306  	c.Assert(err, IsNil)
   307  	defer s.sys.Close(fd)
   308  	s.sys.InsertFstatfsResult(`fstatfs 3 <ptr>`, syscall.Statfs_t{Type: update.Ext4Magic})
   309  	s.sys.InsertFstatResult(`fstat 3 <ptr>`, syscall.Stat_t{})
   310  
   311  	// Check reports trespassing error, restrictions may be lifted though.
   312  	err = rs.Check(fd, "/etc")
   313  	c.Assert(err, ErrorMatches, `cannot write to "/etc/test.conf" because it would affect the host in "/etc"`)
   314  	c.Assert(err.(*update.TrespassingError).ViolatedPath, Equals, "/etc")
   315  	c.Assert(err.(*update.TrespassingError).DesiredPath, Equals, "/etc/test.conf")
   316  
   317  	rs.Lift()
   318  	c.Assert(rs.Check(fd, "/etc"), IsNil)
   319  }
   320  
   321  // Check returns errors from lower layers.
   322  func (s *trespassingSuite) TestRestrictionsForErrors(c *C) {
   323  	a := &update.Assumptions{}
   324  
   325  	rs := a.RestrictionsFor("/etc/test.conf")
   326  	c.Assert(rs, NotNil)
   327  
   328  	fd, err := s.sys.Open("/etc", syscall.O_DIRECTORY, 0)
   329  	c.Assert(err, IsNil)
   330  	defer s.sys.Close(fd)
   331  	s.sys.InsertFault(`fstatfs 3 <ptr>`, errTesting)
   332  
   333  	err = rs.Check(fd, "/etc")
   334  	c.Assert(err, ErrorMatches, `cannot fstatfs "/etc": testing`)
   335  }
   336  
   337  func (s *trespassingSuite) TestRestrictionsForVarSnap(c *C) {
   338  	a := &update.Assumptions{}
   339  	a.AddUnrestrictedPaths("/var/snap")
   340  
   341  	// There are no restrictions in $SNAP_COMMON.
   342  	rs := a.RestrictionsFor("/var/snap/foo/common/test.conf")
   343  	c.Assert(rs, IsNil)
   344  
   345  	// Nil restrictions have working Check and Lift methods.
   346  	c.Assert(rs.Check(3, "unused"), IsNil)
   347  	rs.Lift()
   348  }
   349  
   350  func (s *trespassingSuite) TestRestrictionsForRootfsEntries(c *C) {
   351  	a := &update.Assumptions{}
   352  
   353  	// The root directory is special, it's not a trespassing error we can
   354  	// recover from because we cannot construct a writable mimic for the root
   355  	// directory today.
   356  	rs := a.RestrictionsFor("/foo.conf")
   357  
   358  	fd, err := s.sys.Open("/", syscall.O_DIRECTORY, 0)
   359  	c.Assert(err, IsNil)
   360  	defer s.sys.Close(fd)
   361  	s.sys.InsertFstatfsResult(`fstatfs 3 <ptr>`, syscall.Statfs_t{Type: update.Ext4Magic})
   362  	s.sys.InsertFstatResult(`fstat 3 <ptr>`, syscall.Stat_t{})
   363  
   364  	// Nil restrictions have working Check and Lift methods.
   365  	c.Assert(rs.Check(fd, "/"), ErrorMatches, `cannot recover from trespassing over /`)
   366  }
   367  
   368  // isReadOnly
   369  
   370  func (s *trespassingSuite) TestIsReadOnlySquashfsMountedRo(c *C) {
   371  	path := "/some/path"
   372  	statfs := &syscall.Statfs_t{Type: update.SquashfsMagic, Flags: update.StReadOnly}
   373  	result := update.IsReadOnly(path, statfs)
   374  	c.Assert(result, Equals, true)
   375  }
   376  
   377  func (s *trespassingSuite) TestIsReadOnlySquashfsMountedRw(c *C) {
   378  	path := "/some/path"
   379  	statfs := &syscall.Statfs_t{Type: update.SquashfsMagic}
   380  	result := update.IsReadOnly(path, statfs)
   381  	c.Assert(result, Equals, true)
   382  }
   383  
   384  func (s *trespassingSuite) TestIsReadOnlyExt4MountedRw(c *C) {
   385  	path := "/some/path"
   386  	statfs := &syscall.Statfs_t{Type: update.Ext4Magic}
   387  	result := update.IsReadOnly(path, statfs)
   388  	c.Assert(result, Equals, false)
   389  }
   390  
   391  // isSnapdCreatedPrivateTmpfs
   392  
   393  func (s *trespassingSuite) TestIsPrivateTmpfsCreatedBySnapdNotATmpfs(c *C) {
   394  	path := "/some/path"
   395  	// An ext4 (which is not a tmpfs) is not a private tmpfs.
   396  	statfs := &syscall.Statfs_t{Type: update.Ext4Magic}
   397  	stat := &syscall.Stat_t{}
   398  	result := update.IsPrivateTmpfsCreatedBySnapd(path, statfs, stat, nil)
   399  	c.Assert(result, Equals, false)
   400  }
   401  
   402  func (s *trespassingSuite) TestIsPrivateTmpfsCreatedBySnapdNotTrusted(c *C) {
   403  	path := "/some/path"
   404  	// A tmpfs is not private if it doesn't come from a change we made.
   405  	statfs := &syscall.Statfs_t{Type: update.TmpfsMagic}
   406  	stat := &syscall.Stat_t{}
   407  	result := update.IsPrivateTmpfsCreatedBySnapd(path, statfs, stat, nil)
   408  	c.Assert(result, Equals, false)
   409  }
   410  
   411  func (s *trespassingSuite) TestIsPrivateTmpfsCreatedBySnapdViaChanges(c *C) {
   412  	path := "/some/path"
   413  	// A tmpfs is private because it was mounted by snap-update-ns.
   414  	statfs := &syscall.Statfs_t{Type: update.TmpfsMagic}
   415  	stat := &syscall.Stat_t{}
   416  
   417  	// A tmpfs was mounted in the past so it is private.
   418  	result := update.IsPrivateTmpfsCreatedBySnapd(path, statfs, stat, []*update.Change{
   419  		{Action: update.Mount, Entry: osutil.MountEntry{Name: "tmpfs", Dir: path, Type: "tmpfs"}},
   420  	})
   421  	c.Assert(result, Equals, true)
   422  
   423  	// A tmpfs was mounted but then it was unmounted so it is not private anymore.
   424  	result = update.IsPrivateTmpfsCreatedBySnapd(path, statfs, stat, []*update.Change{
   425  		{Action: update.Mount, Entry: osutil.MountEntry{Name: "tmpfs", Dir: path, Type: "tmpfs"}},
   426  		{Action: update.Unmount, Entry: osutil.MountEntry{Name: "tmpfs", Dir: path, Type: "tmpfs"}},
   427  	})
   428  	c.Assert(result, Equals, false)
   429  
   430  	// Finally, after the mounting and unmounting the tmpfs was mounted again.
   431  	result = update.IsPrivateTmpfsCreatedBySnapd(path, statfs, stat, []*update.Change{
   432  		{Action: update.Mount, Entry: osutil.MountEntry{Name: "tmpfs", Dir: path, Type: "tmpfs"}},
   433  		{Action: update.Unmount, Entry: osutil.MountEntry{Name: "tmpfs", Dir: path, Type: "tmpfs"}},
   434  		{Action: update.Mount, Entry: osutil.MountEntry{Name: "tmpfs", Dir: path, Type: "tmpfs"}},
   435  	})
   436  	c.Assert(result, Equals, true)
   437  }
   438  
   439  func (s *trespassingSuite) TestIsPrivateTmpfsCreatedBySnapdDeeper(c *C) {
   440  	path := "/some/path/below"
   441  	// A tmpfs is not private beyond the exact mount point from a change.
   442  	// That is, sub-directories of a private tmpfs are not recognized as private.
   443  	statfs := &syscall.Statfs_t{Type: update.TmpfsMagic}
   444  	stat := &syscall.Stat_t{}
   445  	result := update.IsPrivateTmpfsCreatedBySnapd(path, statfs, stat, []*update.Change{
   446  		{Action: update.Mount, Entry: osutil.MountEntry{Name: "tmpfs", Dir: "/some/path", Type: "tmpfs"}},
   447  	})
   448  	c.Assert(result, Equals, false)
   449  }