github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/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 }