github.com/rigado/snapd@v2.42.5-go-mod+incompatible/cmd/snap-update-ns/change_test.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2017 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 "errors" 24 "io/ioutil" 25 "os" 26 "strings" 27 "syscall" 28 29 . "gopkg.in/check.v1" 30 31 update "github.com/snapcore/snapd/cmd/snap-update-ns" 32 "github.com/snapcore/snapd/dirs" 33 "github.com/snapcore/snapd/features" 34 "github.com/snapcore/snapd/osutil" 35 "github.com/snapcore/snapd/testutil" 36 ) 37 38 type changeSuite struct { 39 testutil.BaseTest 40 sys *testutil.SyscallRecorder 41 as *update.Assumptions 42 } 43 44 var ( 45 errTesting = errors.New("testing") 46 ) 47 48 var _ = Suite(&changeSuite{}) 49 50 func (s *changeSuite) SetUpTest(c *C) { 51 s.BaseTest.SetUpTest(c) 52 // Mock and record system interactions. 53 s.sys = &testutil.SyscallRecorder{} 54 s.BaseTest.AddCleanup(update.MockSystemCalls(s.sys)) 55 s.as = &update.Assumptions{} 56 } 57 58 func (s *changeSuite) TearDownTest(c *C) { 59 s.BaseTest.TearDownTest(c) 60 s.sys.CheckForStrayDescriptors(c) 61 } 62 63 func (s *changeSuite) disableRobustMountNamespaceUpdates(c *C) { 64 if dirs.GlobalRootDir == "/" { 65 dirs.SetRootDir(c.MkDir()) 66 s.BaseTest.AddCleanup(func() { dirs.SetRootDir("/") }) 67 } 68 c.Assert(os.MkdirAll(dirs.FeaturesDir, 0755), IsNil) 69 err := os.Remove(features.RobustMountNamespaceUpdates.ControlFile()) 70 if err != nil && !os.IsNotExist(err) { 71 c.Assert(err, IsNil) 72 } 73 } 74 75 func (s *changeSuite) enableRobustMountNamespaceUpdates(c *C) { 76 if dirs.GlobalRootDir == "/" { 77 dirs.SetRootDir(c.MkDir()) 78 s.BaseTest.AddCleanup(func() { dirs.SetRootDir("/") }) 79 } 80 c.Assert(os.MkdirAll(dirs.FeaturesDir, 0755), IsNil) 81 err := ioutil.WriteFile(features.RobustMountNamespaceUpdates.ControlFile(), []byte(nil), 0644) 82 c.Assert(err, IsNil) 83 } 84 85 func (s *changeSuite) TestFakeFileInfo(c *C) { 86 c.Assert(testutil.FileInfoDir.IsDir(), Equals, true) 87 c.Assert(testutil.FileInfoFile.IsDir(), Equals, false) 88 c.Assert(testutil.FileInfoSymlink.IsDir(), Equals, false) 89 } 90 91 func (s *changeSuite) TestString(c *C) { 92 change := update.Change{ 93 Entry: osutil.MountEntry{Dir: "/a/b", Name: "/dev/sda1"}, 94 Action: update.Mount, 95 } 96 c.Assert(change.String(), Equals, "mount (/dev/sda1 /a/b none defaults 0 0)") 97 } 98 99 // When there are no profiles we don't do anything. 100 func (s *changeSuite) TestNeededChangesNoProfilesOld(c *C) { 101 s.disableRobustMountNamespaceUpdates(c) 102 103 current := &osutil.MountProfile{} 104 desired := &osutil.MountProfile{} 105 changes := update.NeededChanges(current, desired) 106 c.Assert(changes, IsNil) 107 } 108 109 // When there are no profiles we don't do anything. 110 func (s *changeSuite) TestNeededChangesNoProfilesNew(c *C) { 111 s.enableRobustMountNamespaceUpdates(c) 112 113 current := &osutil.MountProfile{} 114 desired := &osutil.MountProfile{} 115 changes := update.NeededChanges(current, desired) 116 c.Assert(changes, IsNil) 117 } 118 119 // When the profiles are the same we don't do anything. 120 func (s *changeSuite) TestNeededChangesNoChangeOld(c *C) { 121 s.disableRobustMountNamespaceUpdates(c) 122 123 current := &osutil.MountProfile{Entries: []osutil.MountEntry{{Dir: "/common/stuff"}}} 124 desired := &osutil.MountProfile{Entries: []osutil.MountEntry{{Dir: "/common/stuff"}}} 125 changes := update.NeededChanges(current, desired) 126 c.Assert(changes, DeepEquals, []*update.Change{ 127 {Entry: osutil.MountEntry{Dir: "/common/stuff"}, Action: update.Keep}, 128 }) 129 } 130 131 func (s *changeSuite) TestNeededChangesNoChangeNew(c *C) { 132 s.enableRobustMountNamespaceUpdates(c) 133 134 current := &osutil.MountProfile{Entries: []osutil.MountEntry{{Dir: "/common/stuff"}}} 135 desired := &osutil.MountProfile{Entries: []osutil.MountEntry{{Dir: "/common/stuff"}}} 136 changes := update.NeededChanges(current, desired) 137 c.Assert(changes, DeepEquals, []*update.Change{ 138 {Entry: osutil.MountEntry{Dir: "/common/stuff"}, Action: update.Unmount}, 139 {Entry: osutil.MountEntry{Dir: "/common/stuff"}, Action: update.Mount}, 140 }) 141 } 142 143 // When the content interface is connected we should mount the new entry. 144 func (s *changeSuite) TestNeededChangesTrivialMountOld(c *C) { 145 s.disableRobustMountNamespaceUpdates(c) 146 147 current := &osutil.MountProfile{} 148 desired := &osutil.MountProfile{Entries: []osutil.MountEntry{{Dir: "/common/stuff"}}} 149 changes := update.NeededChanges(current, desired) 150 c.Assert(changes, DeepEquals, []*update.Change{ 151 {Entry: desired.Entries[0], Action: update.Mount}, 152 }) 153 } 154 155 func (s *changeSuite) TestNeededChangesTrivialMountNew(c *C) { 156 s.enableRobustMountNamespaceUpdates(c) 157 158 current := &osutil.MountProfile{} 159 desired := &osutil.MountProfile{Entries: []osutil.MountEntry{{Dir: "/common/stuff"}}} 160 changes := update.NeededChanges(current, desired) 161 c.Assert(changes, DeepEquals, []*update.Change{ 162 {Entry: desired.Entries[0], Action: update.Mount}, 163 }) 164 } 165 166 // When the content interface is disconnected we should unmount the mounted entry. 167 func (s *changeSuite) TestNeededChangesTrivialUnmountOld(c *C) { 168 s.disableRobustMountNamespaceUpdates(c) 169 170 current := &osutil.MountProfile{Entries: []osutil.MountEntry{{Dir: "/common/stuff"}}} 171 desired := &osutil.MountProfile{} 172 changes := update.NeededChanges(current, desired) 173 c.Assert(changes, DeepEquals, []*update.Change{ 174 {Entry: current.Entries[0], Action: update.Unmount}, 175 }) 176 } 177 178 func (s *changeSuite) TestNeededChangesTrivialUnmountNew(c *C) { 179 s.enableRobustMountNamespaceUpdates(c) 180 181 current := &osutil.MountProfile{Entries: []osutil.MountEntry{{Dir: "/common/stuff"}}} 182 desired := &osutil.MountProfile{} 183 changes := update.NeededChanges(current, desired) 184 c.Assert(changes, DeepEquals, []*update.Change{ 185 {Entry: current.Entries[0], Action: update.Unmount}, 186 }) 187 } 188 189 // When umounting we unmount children before parents. 190 func (s *changeSuite) TestNeededChangesUnmountOrderOld(c *C) { 191 s.disableRobustMountNamespaceUpdates(c) 192 193 current := &osutil.MountProfile{Entries: []osutil.MountEntry{ 194 {Dir: "/common/stuff/extra"}, 195 {Dir: "/common/stuff"}, 196 }} 197 desired := &osutil.MountProfile{} 198 changes := update.NeededChanges(current, desired) 199 c.Assert(changes, DeepEquals, []*update.Change{ 200 {Entry: osutil.MountEntry{Dir: "/common/stuff/extra"}, Action: update.Unmount}, 201 {Entry: osutil.MountEntry{Dir: "/common/stuff"}, Action: update.Unmount}, 202 }) 203 } 204 205 func (s *changeSuite) TestNeededChangesUnmountOrderNew(c *C) { 206 s.enableRobustMountNamespaceUpdates(c) 207 208 current := &osutil.MountProfile{Entries: []osutil.MountEntry{ 209 {Dir: "/common/stuff/extra"}, 210 {Dir: "/common/stuff"}, 211 }} 212 desired := &osutil.MountProfile{} 213 changes := update.NeededChanges(current, desired) 214 c.Assert(changes, DeepEquals, []*update.Change{ 215 {Entry: osutil.MountEntry{Dir: "/common/stuff/extra"}, Action: update.Unmount}, 216 {Entry: osutil.MountEntry{Dir: "/common/stuff"}, Action: update.Unmount}, 217 }) 218 } 219 220 // When mounting we mount the parents before the children. 221 func (s *changeSuite) TestNeededChangesMountOrderOld(c *C) { 222 s.disableRobustMountNamespaceUpdates(c) 223 224 current := &osutil.MountProfile{} 225 desired := &osutil.MountProfile{Entries: []osutil.MountEntry{ 226 {Dir: "/common/stuff/extra"}, 227 {Dir: "/common/stuff"}, 228 }} 229 changes := update.NeededChanges(current, desired) 230 c.Assert(changes, DeepEquals, []*update.Change{ 231 {Entry: osutil.MountEntry{Dir: "/common/stuff"}, Action: update.Mount}, 232 {Entry: osutil.MountEntry{Dir: "/common/stuff/extra"}, Action: update.Mount}, 233 }) 234 } 235 236 func (s *changeSuite) TestNeededChangesMountOrderNew(c *C) { 237 s.enableRobustMountNamespaceUpdates(c) 238 239 current := &osutil.MountProfile{} 240 desired := &osutil.MountProfile{Entries: []osutil.MountEntry{ 241 {Dir: "/common/stuff/extra"}, 242 {Dir: "/common/stuff"}, 243 }} 244 changes := update.NeededChanges(current, desired) 245 c.Assert(changes, DeepEquals, []*update.Change{ 246 {Entry: osutil.MountEntry{Dir: "/common/stuff"}, Action: update.Mount}, 247 {Entry: osutil.MountEntry{Dir: "/common/stuff/extra"}, Action: update.Mount}, 248 }) 249 } 250 251 // When parent changes we don't reuse its children 252 253 func (s *changeSuite) TestNeededChangesChangedParentSameChildOld(c *C) { 254 s.disableRobustMountNamespaceUpdates(c) 255 256 current := &osutil.MountProfile{Entries: []osutil.MountEntry{ 257 {Dir: "/common/stuff", Name: "/dev/sda1"}, 258 {Dir: "/common/stuff/extra"}, 259 {Dir: "/common/unrelated"}, 260 }} 261 desired := &osutil.MountProfile{Entries: []osutil.MountEntry{ 262 {Dir: "/common/stuff", Name: "/dev/sda2"}, 263 {Dir: "/common/stuff/extra"}, 264 {Dir: "/common/unrelated"}, 265 }} 266 changes := update.NeededChanges(current, desired) 267 c.Assert(changes, DeepEquals, []*update.Change{ 268 {Entry: osutil.MountEntry{Dir: "/common/unrelated"}, Action: update.Keep}, 269 {Entry: osutil.MountEntry{Dir: "/common/stuff/extra"}, Action: update.Unmount}, 270 {Entry: osutil.MountEntry{Dir: "/common/stuff", Name: "/dev/sda1"}, Action: update.Unmount}, 271 {Entry: osutil.MountEntry{Dir: "/common/stuff", Name: "/dev/sda2"}, Action: update.Mount}, 272 {Entry: osutil.MountEntry{Dir: "/common/stuff/extra"}, Action: update.Mount}, 273 }) 274 } 275 276 func (s *changeSuite) TestNeededChangesChangedParentSameChildNew(c *C) { 277 s.enableRobustMountNamespaceUpdates(c) 278 279 current := &osutil.MountProfile{Entries: []osutil.MountEntry{ 280 {Dir: "/common/stuff", Name: "/dev/sda1"}, 281 {Dir: "/common/stuff/extra"}, 282 {Dir: "/common/unrelated"}, 283 }} 284 desired := &osutil.MountProfile{Entries: []osutil.MountEntry{ 285 {Dir: "/common/stuff", Name: "/dev/sda2"}, 286 {Dir: "/common/stuff/extra"}, 287 {Dir: "/common/unrelated"}, 288 }} 289 changes := update.NeededChanges(current, desired) 290 c.Assert(changes, DeepEquals, []*update.Change{ 291 {Entry: osutil.MountEntry{Dir: "/common/unrelated"}, Action: update.Unmount}, 292 {Entry: osutil.MountEntry{Dir: "/common/stuff/extra"}, Action: update.Unmount}, 293 {Entry: osutil.MountEntry{Dir: "/common/stuff", Name: "/dev/sda1"}, Action: update.Unmount}, 294 {Entry: osutil.MountEntry{Dir: "/common/stuff", Name: "/dev/sda2"}, Action: update.Mount}, 295 {Entry: osutil.MountEntry{Dir: "/common/stuff/extra"}, Action: update.Mount}, 296 {Entry: osutil.MountEntry{Dir: "/common/unrelated"}, Action: update.Mount}, 297 }) 298 } 299 300 // When child changes we don't touch the unchanged parent 301 func (s *changeSuite) TestNeededChangesSameParentChangedChildOld(c *C) { 302 s.disableRobustMountNamespaceUpdates(c) 303 304 current := &osutil.MountProfile{Entries: []osutil.MountEntry{ 305 {Dir: "/common/stuff"}, 306 {Dir: "/common/stuff/extra", Name: "/dev/sda1"}, 307 {Dir: "/common/unrelated"}, 308 }} 309 desired := &osutil.MountProfile{Entries: []osutil.MountEntry{ 310 {Dir: "/common/stuff"}, 311 {Dir: "/common/stuff/extra", Name: "/dev/sda2"}, 312 {Dir: "/common/unrelated"}, 313 }} 314 changes := update.NeededChanges(current, desired) 315 c.Assert(changes, DeepEquals, []*update.Change{ 316 {Entry: osutil.MountEntry{Dir: "/common/unrelated"}, Action: update.Keep}, 317 {Entry: osutil.MountEntry{Dir: "/common/stuff/extra", Name: "/dev/sda1"}, Action: update.Unmount}, 318 {Entry: osutil.MountEntry{Dir: "/common/stuff"}, Action: update.Keep}, 319 {Entry: osutil.MountEntry{Dir: "/common/stuff/extra", Name: "/dev/sda2"}, Action: update.Mount}, 320 }) 321 } 322 323 func (s *changeSuite) TestNeededChangesSameParentChangedChildNew(c *C) { 324 s.enableRobustMountNamespaceUpdates(c) 325 326 current := &osutil.MountProfile{Entries: []osutil.MountEntry{ 327 {Dir: "/common/stuff"}, 328 {Dir: "/common/stuff/extra", Name: "/dev/sda1"}, 329 {Dir: "/common/unrelated"}, 330 }} 331 desired := &osutil.MountProfile{Entries: []osutil.MountEntry{ 332 {Dir: "/common/stuff"}, 333 {Dir: "/common/stuff/extra", Name: "/dev/sda2"}, 334 {Dir: "/common/unrelated"}, 335 }} 336 changes := update.NeededChanges(current, desired) 337 c.Assert(changes, DeepEquals, []*update.Change{ 338 {Entry: osutil.MountEntry{Dir: "/common/unrelated"}, Action: update.Unmount}, 339 {Entry: osutil.MountEntry{Dir: "/common/stuff/extra", Name: "/dev/sda1"}, Action: update.Unmount}, 340 {Entry: osutil.MountEntry{Dir: "/common/stuff"}, Action: update.Unmount}, 341 {Entry: osutil.MountEntry{Dir: "/common/stuff"}, Action: update.Mount}, 342 {Entry: osutil.MountEntry{Dir: "/common/stuff/extra", Name: "/dev/sda2"}, Action: update.Mount}, 343 {Entry: osutil.MountEntry{Dir: "/common/unrelated"}, Action: update.Mount}, 344 }) 345 } 346 347 // Unused bind mount farms are unmounted. 348 func (s *changeSuite) TestNeededChangesTmpfsBindMountFarmUnusedOld(c *C) { 349 s.disableRobustMountNamespaceUpdates(c) 350 351 current := &osutil.MountProfile{Entries: []osutil.MountEntry{{ 352 // The tmpfs that lets us write into immutable squashfs. We mock 353 // x-snapd.needed-by to the last entry in the current profile (the bind 354 // mount). Mark it synthetic since it is a helper mount that is needed 355 // to facilitate the following mounts. 356 Name: "tmpfs", 357 Dir: "/snap/name/42/subdir", 358 Type: "tmpfs", 359 Options: []string{"x-snapd.needed-by=/snap/name/42/subdir", "x-snapd.synthetic"}, 360 }, { 361 // A bind mount to preserve a directory hidden by the tmpfs (the mount 362 // point is created elsewhere). We mock x-snapd.needed-by to the 363 // location of the bind mount below that is no longer desired. 364 Name: "/var/lib/snapd/hostfs/snap/name/42/subdir/existing", 365 Dir: "/snap/name/42/subdir/existing", 366 Options: []string{"bind", "ro", "x-snapd.needed-by=/snap/name/42/subdir", "x-snapd.synthetic"}, 367 }, { 368 // A bind mount to put some content from another snap. The bind mount 369 // is nothing special but the fact that it is possible is the reason 370 // the two entries above exist. The mount point (created) is created 371 // elsewhere. 372 Name: "/snap/other/123/libs", 373 Dir: "/snap/name/42/subdir/created", 374 Options: []string{"bind", "ro"}, 375 }}} 376 377 desired := &osutil.MountProfile{} 378 379 changes := update.NeededChanges(current, desired) 380 381 c.Assert(changes, DeepEquals, []*update.Change{ 382 {Entry: osutil.MountEntry{ 383 Name: "/var/lib/snapd/hostfs/snap/name/42/subdir/existing", 384 Dir: "/snap/name/42/subdir/existing", 385 Options: []string{"bind", "ro", "x-snapd.needed-by=/snap/name/42/subdir", "x-snapd.synthetic", "x-snapd.detach"}, 386 }, Action: update.Unmount}, 387 {Entry: osutil.MountEntry{ 388 Name: "/snap/other/123/libs", 389 Dir: "/snap/name/42/subdir/created", 390 Options: []string{"bind", "ro", "x-snapd.detach"}, 391 }, Action: update.Unmount}, 392 {Entry: osutil.MountEntry{ 393 Name: "tmpfs", 394 Dir: "/snap/name/42/subdir", 395 Type: "tmpfs", 396 Options: []string{"x-snapd.needed-by=/snap/name/42/subdir", "x-snapd.synthetic", "x-snapd.detach"}, 397 }, Action: update.Unmount}, 398 }) 399 } 400 401 func (s *changeSuite) TestNeededChangesTmpfsBindMountFarmUnusedNew(c *C) { 402 s.enableRobustMountNamespaceUpdates(c) 403 404 current := &osutil.MountProfile{Entries: []osutil.MountEntry{{ 405 // The tmpfs that lets us write into immutable squashfs. We mock 406 // x-snapd.needed-by to the last entry in the current profile (the bind 407 // mount). Mark it synthetic since it is a helper mount that is needed 408 // to facilitate the following mounts. 409 Name: "tmpfs", 410 Dir: "/snap/name/42/subdir", 411 Type: "tmpfs", 412 Options: []string{"x-snapd.needed-by=/snap/name/42/subdir", "x-snapd.synthetic"}, 413 }, { 414 // A bind mount to preserve a directory hidden by the tmpfs (the mount 415 // point is created elsewhere). We mock x-snapd.needed-by to the 416 // location of the bind mount below that is no longer desired. 417 Name: "/var/lib/snapd/hostfs/snap/name/42/subdir/existing", 418 Dir: "/snap/name/42/subdir/existing", 419 Options: []string{"bind", "ro", "x-snapd.needed-by=/snap/name/42/subdir", "x-snapd.synthetic"}, 420 }, { 421 // A bind mount to put some content from another snap. The bind mount 422 // is nothing special but the fact that it is possible is the reason 423 // the two entries above exist. The mount point (created) is created 424 // elsewhere. 425 Name: "/snap/other/123/libs", 426 Dir: "/snap/name/42/subdir/created", 427 Options: []string{"bind", "ro"}, 428 }}} 429 430 desired := &osutil.MountProfile{} 431 432 changes := update.NeededChanges(current, desired) 433 434 c.Assert(changes, DeepEquals, []*update.Change{ 435 {Entry: osutil.MountEntry{ 436 Name: "/snap/other/123/libs", 437 Dir: "/snap/name/42/subdir/created", 438 Options: []string{"bind", "ro"}, 439 }, Action: update.Unmount}, 440 {Entry: osutil.MountEntry{ 441 Name: "tmpfs", 442 Dir: "/snap/name/42/subdir", 443 Type: "tmpfs", 444 Options: []string{"x-snapd.needed-by=/snap/name/42/subdir", "x-snapd.synthetic", "x-snapd.detach"}, 445 }, Action: update.Unmount}, 446 }) 447 } 448 449 func (s *changeSuite) TestNeededChangesTmpfsBindMountFarmUsedOld(c *C) { 450 s.disableRobustMountNamespaceUpdates(c) 451 452 // NOTE: the current profile is the same as in the test 453 // TestNeededChangesTmpfsBindMountFarmUnused written above. 454 current := &osutil.MountProfile{Entries: []osutil.MountEntry{{ 455 Name: "tmpfs", 456 Dir: "/snap/name/42/subdir", 457 Type: "tmpfs", 458 Options: []string{"x-snapd.needed-by=/snap/name/42/subdir/created", "x-snapd.synthetic"}, 459 }, { 460 Name: "/var/lib/snapd/hostfs/snap/name/42/subdir/existing", 461 Dir: "/snap/name/42/subdir/existing", 462 Options: []string{"bind", "ro", "x-snapd.needed-by=/snap/name/42/subdir/created", "x-snapd.synthetic"}, 463 }, { 464 Name: "/snap/other/123/libs", 465 Dir: "/snap/name/42/subdir/created", 466 Options: []string{"bind", "ro"}, 467 }}} 468 469 desired := &osutil.MountProfile{Entries: []osutil.MountEntry{{ 470 // This is the only entry that we explicitly want but in order to 471 // support it we need to keep the remaining implicit entries. 472 Name: "/snap/other/123/libs", 473 Dir: "/snap/name/42/subdir/created", 474 Options: []string{"bind", "ro"}, 475 }}} 476 477 changes := update.NeededChanges(current, desired) 478 479 c.Assert(changes, DeepEquals, []*update.Change{ 480 {Entry: osutil.MountEntry{ 481 Name: "/var/lib/snapd/hostfs/snap/name/42/subdir/existing", 482 Dir: "/snap/name/42/subdir/existing", 483 Options: []string{"bind", "ro", "x-snapd.needed-by=/snap/name/42/subdir/created", "x-snapd.synthetic"}, 484 }, Action: update.Keep}, 485 {Entry: osutil.MountEntry{ 486 Name: "/snap/other/123/libs", 487 Dir: "/snap/name/42/subdir/created", 488 Options: []string{"bind", "ro"}, 489 }, Action: update.Keep}, 490 {Entry: osutil.MountEntry{ 491 Name: "tmpfs", 492 Dir: "/snap/name/42/subdir", 493 Type: "tmpfs", 494 Options: []string{"x-snapd.needed-by=/snap/name/42/subdir/created", "x-snapd.synthetic"}, 495 }, Action: update.Keep}, 496 }) 497 } 498 499 func (s *changeSuite) TestNeededChangesTmpfsBindMountFarmUsedNew(c *C) { 500 s.enableRobustMountNamespaceUpdates(c) 501 502 // NOTE: the current profile is the same as in the test 503 // TestNeededChangesTmpfsBindMountFarmUnused written above. 504 current := &osutil.MountProfile{Entries: []osutil.MountEntry{{ 505 Name: "tmpfs", 506 Dir: "/snap/name/42/subdir", 507 Type: "tmpfs", 508 Options: []string{"x-snapd.needed-by=/snap/name/42/subdir/created", "x-snapd.synthetic"}, 509 }, { 510 Name: "/var/lib/snapd/hostfs/snap/name/42/subdir/existing", 511 Dir: "/snap/name/42/subdir/existing", 512 Options: []string{"bind", "ro", "x-snapd.needed-by=/snap/name/42/subdir/created", "x-snapd.synthetic"}, 513 }, { 514 Name: "/snap/other/123/libs", 515 Dir: "/snap/name/42/subdir/created", 516 Options: []string{"bind", "ro"}, 517 }}} 518 519 desired := &osutil.MountProfile{Entries: []osutil.MountEntry{{ 520 // This is the only entry that we explicitly want but in order to 521 // support it we need to keep the remaining implicit entries. 522 Name: "/snap/other/123/libs", 523 Dir: "/snap/name/42/subdir/created", 524 Options: []string{"bind", "ro"}, 525 }}} 526 527 changes := update.NeededChanges(current, desired) 528 529 c.Assert(changes, DeepEquals, []*update.Change{ 530 {Entry: osutil.MountEntry{ 531 Name: "/snap/other/123/libs", 532 Dir: "/snap/name/42/subdir/created", 533 Options: []string{"bind", "ro"}, 534 }, Action: update.Unmount}, 535 {Entry: osutil.MountEntry{ 536 Name: "tmpfs", 537 Dir: "/snap/name/42/subdir", 538 Type: "tmpfs", 539 Options: []string{"x-snapd.needed-by=/snap/name/42/subdir/created", "x-snapd.synthetic", "x-snapd.detach"}, 540 }, Action: update.Unmount}, 541 {Entry: osutil.MountEntry{ 542 Name: "/snap/other/123/libs", 543 Dir: "/snap/name/42/subdir/created", 544 Options: []string{"bind", "ro"}, 545 }, Action: update.Mount}, 546 }) 547 } 548 549 // cur = ['/a/b', '/a/b-1', '/a/b-1/3', '/a/b/c'] 550 // des = ['/a/b', '/a/b-1', '/a/b/c' 551 // 552 // We are smart about comparing entries as directories. Here even though "/a/b" 553 // is a prefix of "/a/b-1" it is correctly reused. 554 func (s *changeSuite) TestNeededChangesSmartEntryComparisonOld(c *C) { 555 s.disableRobustMountNamespaceUpdates(c) 556 557 current := &osutil.MountProfile{Entries: []osutil.MountEntry{ 558 {Dir: "/a/b", Name: "/dev/sda1"}, 559 {Dir: "/a/b-1"}, 560 {Dir: "/a/b-1/3"}, 561 {Dir: "/a/b/c"}, 562 }} 563 desired := &osutil.MountProfile{Entries: []osutil.MountEntry{ 564 {Dir: "/a/b", Name: "/dev/sda2"}, 565 {Dir: "/a/b-1"}, 566 {Dir: "/a/b/c"}, 567 }} 568 changes := update.NeededChanges(current, desired) 569 c.Assert(changes, DeepEquals, []*update.Change{ 570 {Entry: osutil.MountEntry{Dir: "/a/b/c"}, Action: update.Unmount}, 571 {Entry: osutil.MountEntry{Dir: "/a/b", Name: "/dev/sda1"}, Action: update.Unmount}, 572 {Entry: osutil.MountEntry{Dir: "/a/b-1/3"}, Action: update.Unmount}, 573 {Entry: osutil.MountEntry{Dir: "/a/b-1"}, Action: update.Keep}, 574 575 {Entry: osutil.MountEntry{Dir: "/a/b", Name: "/dev/sda2"}, Action: update.Mount}, 576 {Entry: osutil.MountEntry{Dir: "/a/b/c"}, Action: update.Mount}, 577 }) 578 } 579 580 func (s *changeSuite) TestNeededChangesSmartEntryComparisonNew(c *C) { 581 s.enableRobustMountNamespaceUpdates(c) 582 583 current := &osutil.MountProfile{Entries: []osutil.MountEntry{ 584 {Dir: "/a/b", Name: "/dev/sda1"}, 585 {Dir: "/a/b-1"}, 586 {Dir: "/a/b-1/3"}, 587 {Dir: "/a/b/c"}, 588 }} 589 desired := &osutil.MountProfile{Entries: []osutil.MountEntry{ 590 {Dir: "/a/b", Name: "/dev/sda2"}, 591 {Dir: "/a/b-1"}, 592 {Dir: "/a/b/c"}, 593 }} 594 changes := update.NeededChanges(current, desired) 595 c.Assert(changes, DeepEquals, []*update.Change{ 596 {Entry: osutil.MountEntry{Dir: "/a/b/c"}, Action: update.Unmount}, 597 {Entry: osutil.MountEntry{Dir: "/a/b", Name: "/dev/sda1"}, Action: update.Unmount}, 598 {Entry: osutil.MountEntry{Dir: "/a/b-1/3"}, Action: update.Unmount}, 599 {Entry: osutil.MountEntry{Dir: "/a/b-1"}, Action: update.Unmount}, 600 601 {Entry: osutil.MountEntry{Dir: "/a/b-1"}, Action: update.Mount}, 602 {Entry: osutil.MountEntry{Dir: "/a/b", Name: "/dev/sda2"}, Action: update.Mount}, 603 {Entry: osutil.MountEntry{Dir: "/a/b/c"}, Action: update.Mount}, 604 }) 605 } 606 607 // Parallel instance changes are executed first 608 func (s *changeSuite) TestNeededChangesParallelInstancesManyComeFirstOld(c *C) { 609 s.disableRobustMountNamespaceUpdates(c) 610 611 desired := &osutil.MountProfile{Entries: []osutil.MountEntry{ 612 {Dir: "/common/stuff", Name: "/dev/sda1"}, 613 {Dir: "/common/stuff/extra"}, 614 {Dir: "/common/unrelated"}, 615 {Dir: "/foo/bar", Name: "/foo/bar_bar", Options: []string{osutil.XSnapdOriginOvername()}}, 616 {Dir: "/snap/foo", Name: "/snap/foo_bar", Options: []string{osutil.XSnapdOriginOvername()}}, 617 }} 618 changes := update.NeededChanges(&osutil.MountProfile{}, desired) 619 c.Assert(changes, DeepEquals, []*update.Change{ 620 {Entry: osutil.MountEntry{Dir: "/foo/bar", Name: "/foo/bar_bar", Options: []string{osutil.XSnapdOriginOvername()}}, Action: update.Mount}, 621 {Entry: osutil.MountEntry{Dir: "/snap/foo", Name: "/snap/foo_bar", Options: []string{osutil.XSnapdOriginOvername()}}, Action: update.Mount}, 622 {Entry: osutil.MountEntry{Dir: "/common/stuff", Name: "/dev/sda1"}, Action: update.Mount}, 623 {Entry: osutil.MountEntry{Dir: "/common/stuff/extra"}, Action: update.Mount}, 624 {Entry: osutil.MountEntry{Dir: "/common/unrelated"}, Action: update.Mount}, 625 }) 626 } 627 628 func (s *changeSuite) TestNeededChangesParallelInstancesManyComeFirstNew(c *C) { 629 s.enableRobustMountNamespaceUpdates(c) 630 631 desired := &osutil.MountProfile{Entries: []osutil.MountEntry{ 632 {Dir: "/common/stuff", Name: "/dev/sda1"}, 633 {Dir: "/common/stuff/extra"}, 634 {Dir: "/common/unrelated"}, 635 {Dir: "/foo/bar", Name: "/foo/bar_bar", Options: []string{osutil.XSnapdOriginOvername()}}, 636 {Dir: "/snap/foo", Name: "/snap/foo_bar", Options: []string{osutil.XSnapdOriginOvername()}}, 637 }} 638 changes := update.NeededChanges(&osutil.MountProfile{}, desired) 639 c.Assert(changes, DeepEquals, []*update.Change{ 640 {Entry: osutil.MountEntry{Dir: "/foo/bar", Name: "/foo/bar_bar", Options: []string{osutil.XSnapdOriginOvername()}}, Action: update.Mount}, 641 {Entry: osutil.MountEntry{Dir: "/snap/foo", Name: "/snap/foo_bar", Options: []string{osutil.XSnapdOriginOvername()}}, Action: update.Mount}, 642 {Entry: osutil.MountEntry{Dir: "/common/stuff", Name: "/dev/sda1"}, Action: update.Mount}, 643 {Entry: osutil.MountEntry{Dir: "/common/stuff/extra"}, Action: update.Mount}, 644 {Entry: osutil.MountEntry{Dir: "/common/unrelated"}, Action: update.Mount}, 645 }) 646 } 647 648 // Parallel instance changes are kept if already present 649 func (s *changeSuite) TestNeededChangesParallelInstancesKeepOld(c *C) { 650 s.disableRobustMountNamespaceUpdates(c) 651 652 desired := &osutil.MountProfile{Entries: []osutil.MountEntry{ 653 {Dir: "/common/stuff", Name: "/dev/sda1"}, 654 {Dir: "/common/unrelated"}, 655 {Dir: "/foo/bar", Name: "/foo/bar_bar", Options: []string{osutil.XSnapdOriginOvername()}}, 656 {Dir: "/snap/foo", Name: "/snap/foo_bar", Options: []string{osutil.XSnapdOriginOvername()}}, 657 }} 658 current := &osutil.MountProfile{Entries: []osutil.MountEntry{ 659 {Dir: "/snap/foo", Name: "/snap/foo_bar", Options: []string{osutil.XSnapdOriginOvername()}}, 660 {Dir: "/foo/bar", Name: "/foo/bar_bar", Options: []string{osutil.XSnapdOriginOvername()}}, 661 }} 662 changes := update.NeededChanges(current, desired) 663 c.Assert(changes, DeepEquals, []*update.Change{ 664 {Entry: osutil.MountEntry{Dir: "/snap/foo", Name: "/snap/foo_bar", Options: []string{osutil.XSnapdOriginOvername()}}, Action: update.Keep}, 665 {Entry: osutil.MountEntry{Dir: "/foo/bar", Name: "/foo/bar_bar", Options: []string{osutil.XSnapdOriginOvername()}}, Action: update.Keep}, 666 {Entry: osutil.MountEntry{Dir: "/common/stuff", Name: "/dev/sda1"}, Action: update.Mount}, 667 {Entry: osutil.MountEntry{Dir: "/common/unrelated"}, Action: update.Mount}, 668 }) 669 } 670 671 func (s *changeSuite) TestNeededChangesParallelInstancesKeepNew(c *C) { 672 s.enableRobustMountNamespaceUpdates(c) 673 674 desired := &osutil.MountProfile{Entries: []osutil.MountEntry{ 675 {Dir: "/common/stuff", Name: "/dev/sda1"}, 676 {Dir: "/common/unrelated"}, 677 {Dir: "/foo/bar", Name: "/foo/bar_bar", Options: []string{osutil.XSnapdOriginOvername()}}, 678 {Dir: "/snap/foo", Name: "/snap/foo_bar", Options: []string{osutil.XSnapdOriginOvername()}}, 679 }} 680 current := &osutil.MountProfile{Entries: []osutil.MountEntry{ 681 {Dir: "/snap/foo", Name: "/snap/foo_bar", Options: []string{osutil.XSnapdOriginOvername()}}, 682 {Dir: "/foo/bar", Name: "/foo/bar_bar", Options: []string{osutil.XSnapdOriginOvername()}}, 683 }} 684 changes := update.NeededChanges(current, desired) 685 c.Assert(changes, DeepEquals, []*update.Change{ 686 {Entry: osutil.MountEntry{Dir: "/snap/foo", Name: "/snap/foo_bar", Options: []string{osutil.XSnapdOriginOvername()}}, Action: update.Unmount}, 687 {Entry: osutil.MountEntry{Dir: "/foo/bar", Name: "/foo/bar_bar", Options: []string{osutil.XSnapdOriginOvername()}}, Action: update.Unmount}, 688 {Entry: osutil.MountEntry{Dir: "/foo/bar", Name: "/foo/bar_bar", Options: []string{osutil.XSnapdOriginOvername()}}, Action: update.Mount}, 689 {Entry: osutil.MountEntry{Dir: "/snap/foo", Name: "/snap/foo_bar", Options: []string{osutil.XSnapdOriginOvername()}}, Action: update.Mount}, 690 {Entry: osutil.MountEntry{Dir: "/common/stuff", Name: "/dev/sda1"}, Action: update.Mount}, 691 {Entry: osutil.MountEntry{Dir: "/common/unrelated"}, Action: update.Mount}, 692 }) 693 } 694 695 // Parallel instance with mounts inside 696 func (s *changeSuite) TestNeededChangesParallelInstancesInsideMountOld(c *C) { 697 s.disableRobustMountNamespaceUpdates(c) 698 699 desired := &osutil.MountProfile{Entries: []osutil.MountEntry{ 700 {Dir: "/foo/bar/baz"}, 701 {Dir: "/foo/bar", Name: "/foo/bar_bar", Options: []string{osutil.XSnapdOriginOvername()}}, 702 {Dir: "/snap/foo", Name: "/snap/foo_bar", Options: []string{osutil.XSnapdOriginOvername()}}, 703 }} 704 current := &osutil.MountProfile{Entries: []osutil.MountEntry{ 705 {Dir: "/foo/bar/zed"}, 706 {Dir: "/snap/foo", Name: "/snap/foo_bar", Options: []string{osutil.XSnapdOriginOvername()}}, 707 {Dir: "/foo/bar", Name: "/foo/bar_bar", Options: []string{osutil.XSnapdOriginOvername()}}, 708 }} 709 changes := update.NeededChanges(current, desired) 710 c.Assert(changes, DeepEquals, []*update.Change{ 711 {Entry: osutil.MountEntry{Dir: "/foo/bar/zed"}, Action: update.Unmount}, 712 {Entry: osutil.MountEntry{Dir: "/snap/foo", Name: "/snap/foo_bar", Options: []string{osutil.XSnapdOriginOvername()}}, Action: update.Keep}, 713 {Entry: osutil.MountEntry{Dir: "/foo/bar", Name: "/foo/bar_bar", Options: []string{osutil.XSnapdOriginOvername()}}, Action: update.Keep}, 714 {Entry: osutil.MountEntry{Dir: "/foo/bar/baz"}, Action: update.Mount}, 715 }) 716 } 717 718 func (s *changeSuite) TestNeededChangesParallelInstancesInsideMountNew(c *C) { 719 s.enableRobustMountNamespaceUpdates(c) 720 721 desired := &osutil.MountProfile{Entries: []osutil.MountEntry{ 722 {Dir: "/foo/bar/baz"}, 723 {Dir: "/foo/bar", Name: "/foo/bar_bar", Options: []string{osutil.XSnapdOriginOvername()}}, 724 {Dir: "/snap/foo", Name: "/snap/foo_bar", Options: []string{osutil.XSnapdOriginOvername()}}, 725 }} 726 current := &osutil.MountProfile{Entries: []osutil.MountEntry{ 727 {Dir: "/foo/bar/zed"}, 728 {Dir: "/snap/foo", Name: "/snap/foo_bar", Options: []string{osutil.XSnapdOriginOvername()}}, 729 {Dir: "/foo/bar", Name: "/foo/bar_bar", Options: []string{osutil.XSnapdOriginOvername()}}, 730 }} 731 changes := update.NeededChanges(current, desired) 732 c.Assert(changes, DeepEquals, []*update.Change{ 733 {Entry: osutil.MountEntry{Dir: "/foo/bar/zed"}, Action: update.Unmount}, 734 {Entry: osutil.MountEntry{Dir: "/snap/foo", Name: "/snap/foo_bar", Options: []string{osutil.XSnapdOriginOvername()}}, Action: update.Unmount}, 735 {Entry: osutil.MountEntry{Dir: "/foo/bar", Name: "/foo/bar_bar", Options: []string{osutil.XSnapdOriginOvername()}}, Action: update.Unmount}, 736 {Entry: osutil.MountEntry{Dir: "/foo/bar", Name: "/foo/bar_bar", Options: []string{osutil.XSnapdOriginOvername()}}, Action: update.Mount}, 737 {Entry: osutil.MountEntry{Dir: "/snap/foo", Name: "/snap/foo_bar", Options: []string{osutil.XSnapdOriginOvername()}}, Action: update.Mount}, 738 {Entry: osutil.MountEntry{Dir: "/foo/bar/baz"}, Action: update.Mount}, 739 }) 740 } 741 742 func mustReadProfile(profileStr string) *osutil.MountProfile { 743 profile, err := osutil.ReadMountProfile(strings.NewReader(profileStr)) 744 if err != nil { 745 panic(err) 746 } 747 return profile 748 } 749 750 func (s *changeSuite) TestRuntimeUsingSymlinksOld(c *C) { 751 s.disableRobustMountNamespaceUpdates(c) 752 753 // We start with a runtime shared from one snap to another and then exposed 754 // to /opt with a symbolic link. This is the initial state of the 755 // application in version v1. 756 initial := &osutil.MountProfile{} 757 desiredV1 := &osutil.MountProfile{Entries: []osutil.MountEntry{ 758 {Name: "none", Dir: "/opt/runtime", Type: "none", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=/snap/app/x1/runtime", "x-snapd.origin=layout"}}, 759 {Name: "/snap/runtime/x1/opt/runtime", Dir: "/snap/app/x1/runtime", Type: "none", Options: []string{"bind", "ro"}}, 760 }} 761 // The changes we compute are trivial, simply perform each operation in order. 762 changes := update.NeededChanges(initial, desiredV1) 763 c.Assert(changes, DeepEquals, []*update.Change{ 764 {Entry: osutil.MountEntry{Name: "none", Dir: "/opt/runtime", Type: "none", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=/snap/app/x1/runtime", "x-snapd.origin=layout"}}, Action: update.Mount}, 765 {Entry: osutil.MountEntry{Name: "/snap/runtime/x1/opt/runtime", Dir: "/snap/app/x1/runtime", Type: "none", Options: []string{"bind", "ro"}}, Action: update.Mount}, 766 }) 767 // After performing both changes we have a new synthesized entry. We get an 768 // extra writable mimic over /opt so that we can add our symlink. The 769 // content sharing into $SNAP is applied as expected since the snap ships 770 // the required mount point. 771 currentV1 := &osutil.MountProfile{Entries: []osutil.MountEntry{ 772 {Name: "/snap/runtime/x1/opt/runtime", Dir: "/snap/app/x1/runtime", Type: "none", Options: []string{"bind", "ro"}}, 773 {Name: "none", Dir: "/opt/runtime", Type: "none", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=/snap/app/x1/runtime", "x-snapd.origin=layout"}}, 774 {Name: "tmpfs", Dir: "/opt", Type: "tmpfs", Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/opt/runtime", "mode=0755", "uid=0", "gid=0"}}, 775 }} 776 // We now proceed to replace app v1 with v2 which uses a bind mount instead 777 // of a symlink. First, let's start with the updated desired profile: 778 desiredV2 := &osutil.MountProfile{Entries: []osutil.MountEntry{ 779 {Name: "/snap/app/x2/runtime", Dir: "/opt/runtime", Type: "none", Options: []string{"rbind", "rw", "x-snapd.origin=layout"}}, 780 {Name: "/snap/runtime/x1/opt/runtime", Dir: "/snap/app/x2/runtime", Type: "none", Options: []string{"bind", "ro"}}, 781 }} 782 783 // Let's see what the update algorithm thinks. 784 changes = update.NeededChanges(currentV1, desiredV2) 785 c.Assert(changes, DeepEquals, []*update.Change{ 786 // We are dropping the content interface bind mount because app changed revision 787 {Entry: osutil.MountEntry{Name: "/snap/runtime/x1/opt/runtime", Dir: "/snap/app/x1/runtime", Type: "none", Options: []string{"bind", "ro", "x-snapd.detach"}}, Action: update.Unmount}, 788 // We are not keeping /opt, it's safer this way. 789 {Entry: osutil.MountEntry{Name: "none", Dir: "/opt/runtime", Type: "none", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=/snap/app/x1/runtime", "x-snapd.origin=layout"}}, Action: update.Unmount}, 790 // We are re-creating /opt from scratch. 791 {Entry: osutil.MountEntry{Name: "tmpfs", Dir: "/opt", Type: "tmpfs", Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/opt/runtime", "mode=0755", "uid=0", "gid=0"}}, Action: update.Keep}, 792 // We are adding a new bind mount for /opt/runtime 793 {Entry: osutil.MountEntry{Name: "/snap/app/x2/runtime", Dir: "/opt/runtime", Type: "none", Options: []string{"rbind", "rw", "x-snapd.origin=layout"}}, Action: update.Mount}, 794 // We also adding the updated path of the content interface (for revision x2) 795 {Entry: osutil.MountEntry{Name: "/snap/runtime/x1/opt/runtime", Dir: "/snap/app/x2/runtime", Type: "none", Options: []string{"bind", "ro"}}, Action: update.Mount}, 796 }) 797 798 // After performing all those changes this is the profile we observe. 799 currentV2 := &osutil.MountProfile{Entries: []osutil.MountEntry{ 800 {Name: "tmpfs", Dir: "/opt", Type: "tmpfs", Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/opt/runtime", "mode=0755", "uid=0", "gid=0", "x-snapd.detach"}}, 801 {Name: "/snap/app/x2/runtime", Dir: "/opt/runtime", Type: "none", Options: []string{"rbind", "rw", "x-snapd.origin=layout"}}, 802 {Name: "/snap/runtime/x1/opt/runtime", Dir: "/snap/app/x2/runtime", Type: "none", Options: []string{"bind", "ro"}}, 803 }} 804 805 // So far so good. To trigger the issue we now revert or refresh to v1 806 // again. Let's see what happens here. The desired profiles are already 807 // known so let's see what the algorithm thinks now. 808 changes = update.NeededChanges(currentV2, desiredV1) 809 c.Assert(changes, DeepEquals, []*update.Change{ 810 // We are, again, dropping the content interface bind mount because app changed revision 811 {Entry: osutil.MountEntry{Name: "/snap/runtime/x1/opt/runtime", Dir: "/snap/app/x2/runtime", Type: "none", Options: []string{"bind", "ro", "x-snapd.detach"}}, Action: update.Unmount}, 812 // We are also dropping the bind mount from /opt/runtime since we want a symlink instead 813 {Entry: osutil.MountEntry{Name: "/snap/app/x2/runtime", Dir: "/opt/runtime", Type: "none", Options: []string{"rbind", "rw", "x-snapd.origin=layout", "x-snapd.detach"}}, Action: update.Unmount}, 814 // Again, recreate the tmpfs. 815 {Entry: osutil.MountEntry{Name: "tmpfs", Dir: "/opt", Type: "tmpfs", Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/opt/runtime", "mode=0755", "uid=0", "gid=0", "x-snapd.detach"}}, Action: update.Keep}, 816 // We are providing a symlink /opt/runtime -> to $SNAP/runtime. 817 {Entry: osutil.MountEntry{Name: "none", Dir: "/opt/runtime", Type: "none", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=/snap/app/x1/runtime", "x-snapd.origin=layout"}}, Action: update.Mount}, 818 // We are bind mounting the runtime from another snap into $SNAP/runtime 819 {Entry: osutil.MountEntry{Name: "/snap/runtime/x1/opt/runtime", Dir: "/snap/app/x1/runtime", Type: "none", Options: []string{"bind", "ro"}}, Action: update.Mount}, 820 }) 821 822 // The problem is that the tmpfs contains leftovers from the things we 823 // created and those prevent the execution of this mount profile. 824 } 825 826 func (s *changeSuite) TestRuntimeUsingSymlinksNew(c *C) { 827 s.enableRobustMountNamespaceUpdates(c) 828 829 // We start with a runtime shared from one snap to another and then exposed 830 // to /opt with a symbolic link. This is the initial state of the 831 // application in version v1. 832 initial := &osutil.MountProfile{} 833 desiredV1 := &osutil.MountProfile{Entries: []osutil.MountEntry{ 834 {Name: "none", Dir: "/opt/runtime", Type: "none", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=/snap/app/x1/runtime", "x-snapd.origin=layout"}}, 835 {Name: "/snap/runtime/x1/opt/runtime", Dir: "/snap/app/x1/runtime", Type: "none", Options: []string{"bind", "ro"}}, 836 }} 837 // The changes we compute are trivial, simply perform each operation in order. 838 changes := update.NeededChanges(initial, desiredV1) 839 c.Assert(changes, DeepEquals, []*update.Change{ 840 {Entry: osutil.MountEntry{Name: "none", Dir: "/opt/runtime", Type: "none", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=/snap/app/x1/runtime", "x-snapd.origin=layout"}}, Action: update.Mount}, 841 {Entry: osutil.MountEntry{Name: "/snap/runtime/x1/opt/runtime", Dir: "/snap/app/x1/runtime", Type: "none", Options: []string{"bind", "ro"}}, Action: update.Mount}, 842 }) 843 // After performing both changes we have a new synthesized entry. We get an 844 // extra writable mimic over /opt so that we can add our symlink. The 845 // content sharing into $SNAP is applied as expected since the snap ships 846 // the required mount point. 847 currentV1 := &osutil.MountProfile{Entries: []osutil.MountEntry{ 848 {Name: "/snap/runtime/x1/opt/runtime", Dir: "/snap/app/x1/runtime", Type: "none", Options: []string{"bind", "ro"}}, 849 {Name: "none", Dir: "/opt/runtime", Type: "none", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=/snap/app/x1/runtime", "x-snapd.origin=layout"}}, 850 {Name: "tmpfs", Dir: "/opt", Type: "tmpfs", Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/opt/runtime", "mode=0755", "uid=0", "gid=0"}}, 851 }} 852 // We now proceed to replace app v1 with v2 which uses a bind mount instead 853 // of a symlink. First, let's start with the updated desired profile: 854 desiredV2 := &osutil.MountProfile{Entries: []osutil.MountEntry{ 855 {Name: "/snap/app/x2/runtime", Dir: "/opt/runtime", Type: "none", Options: []string{"rbind", "rw", "x-snapd.origin=layout"}}, 856 {Name: "/snap/runtime/x1/opt/runtime", Dir: "/snap/app/x2/runtime", Type: "none", Options: []string{"bind", "ro"}}, 857 }} 858 859 // Let's see what the update algorithm thinks. 860 changes = update.NeededChanges(currentV1, desiredV2) 861 c.Assert(changes, DeepEquals, []*update.Change{ 862 // We are dropping the content interface bind mount because app changed revision 863 {Entry: osutil.MountEntry{Name: "/snap/runtime/x1/opt/runtime", Dir: "/snap/app/x1/runtime", Type: "none", Options: []string{"bind", "ro"}}, Action: update.Unmount}, 864 // We are not keeping /opt, it's safer this way. 865 {Entry: osutil.MountEntry{Name: "none", Dir: "/opt/runtime", Type: "none", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=/snap/app/x1/runtime", "x-snapd.origin=layout"}}, Action: update.Unmount}, 866 // We are re-creating /opt from scratch. 867 {Entry: osutil.MountEntry{Name: "tmpfs", Dir: "/opt", Type: "tmpfs", Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/opt/runtime", "mode=0755", "uid=0", "gid=0", "x-snapd.detach"}}, Action: update.Unmount}, 868 // We are adding a new bind mount for /opt/runtime 869 {Entry: osutil.MountEntry{Name: "/snap/app/x2/runtime", Dir: "/opt/runtime", Type: "none", Options: []string{"rbind", "rw", "x-snapd.origin=layout"}}, Action: update.Mount}, 870 // We also adding the updated path of the content interface (for revision x2) 871 {Entry: osutil.MountEntry{Name: "/snap/runtime/x1/opt/runtime", Dir: "/snap/app/x2/runtime", Type: "none", Options: []string{"bind", "ro"}}, Action: update.Mount}, 872 }) 873 874 // After performing all those changes this is the profile we observe. 875 currentV2 := &osutil.MountProfile{Entries: []osutil.MountEntry{ 876 {Name: "tmpfs", Dir: "/opt", Type: "tmpfs", Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/opt/runtime", "mode=0755", "uid=0", "gid=0", "x-snapd.detach"}}, 877 {Name: "/snap/app/x2/runtime", Dir: "/opt/runtime", Type: "none", Options: []string{"rbind", "rw", "x-snapd.origin=layout"}}, 878 {Name: "/snap/runtime/x1/opt/runtime", Dir: "/snap/app/x2/runtime", Type: "none", Options: []string{"bind", "ro"}}, 879 }} 880 881 // So far so good. To trigger the issue we now revert or refresh to v1 882 // again. Let's see what happens here. The desired profiles are already 883 // known so let's see what the algorithm thinks now. 884 changes = update.NeededChanges(currentV2, desiredV1) 885 c.Assert(changes, DeepEquals, []*update.Change{ 886 // We are, again, dropping the content interface bind mount because app changed revision 887 {Entry: osutil.MountEntry{Name: "/snap/runtime/x1/opt/runtime", Dir: "/snap/app/x2/runtime", Type: "none", Options: []string{"bind", "ro"}}, Action: update.Unmount}, 888 // We are also dropping the bind mount from /opt/runtime since we want a symlink instead 889 {Entry: osutil.MountEntry{Name: "/snap/app/x2/runtime", Dir: "/opt/runtime", Type: "none", Options: []string{"rbind", "rw", "x-snapd.origin=layout", "x-snapd.detach"}}, Action: update.Unmount}, 890 // Again, recreate the tmpfs. 891 {Entry: osutil.MountEntry{Name: "tmpfs", Dir: "/opt", Type: "tmpfs", Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/opt/runtime", "mode=0755", "uid=0", "gid=0", "x-snapd.detach"}}, Action: update.Unmount}, 892 // We are providing a symlink /opt/runtime -> to $SNAP/runtime. 893 {Entry: osutil.MountEntry{Name: "none", Dir: "/opt/runtime", Type: "none", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=/snap/app/x1/runtime", "x-snapd.origin=layout"}}, Action: update.Mount}, 894 // We are bind mounting the runtime from another snap into $SNAP/runtime 895 {Entry: osutil.MountEntry{Name: "/snap/runtime/x1/opt/runtime", Dir: "/snap/app/x1/runtime", Type: "none", Options: []string{"bind", "ro"}}, Action: update.Mount}, 896 }) 897 898 // The problem is that the tmpfs contains leftovers from the things we 899 // created and those prevent the execution of this mount profile. 900 } 901 902 // ######################################## 903 // Topic: mounting & unmounting filesystems 904 // ######################################## 905 906 // Change.Perform returns errors from os.Lstat (apart from ErrNotExist) 907 func (s *changeSuite) TestPerformFilesystemMountLstatError(c *C) { 908 s.sys.InsertFault(`lstat "/target"`, errTesting) 909 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}} 910 synth, err := chg.Perform(s.as) 911 c.Assert(err, ErrorMatches, `cannot inspect "/target": testing`) 912 c.Assert(synth, HasLen, 0) 913 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 914 {C: `lstat "/target"`, E: errTesting}, 915 }) 916 } 917 918 // Change.Perform wants to mount a filesystem. 919 func (s *changeSuite) TestPerformFilesystemMount(c *C) { 920 s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir) 921 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}} 922 synth, err := chg.Perform(s.as) 923 c.Assert(err, IsNil) 924 c.Assert(synth, HasLen, 0) 925 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 926 {C: `lstat "/target"`, R: testutil.FileInfoDir}, 927 {C: `mount "device" "/target" "type" 0 ""`}, 928 }) 929 } 930 931 // Change.Perform wants to mount a filesystem with sharing changes. 932 func (s *changeSuite) TestPerformFilesystemMountAndShareChanges(c *C) { 933 s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir) 934 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type", Options: []string{"shared"}}} 935 synth, err := chg.Perform(s.as) 936 c.Assert(err, IsNil) 937 c.Assert(synth, HasLen, 0) 938 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 939 {C: `lstat "/target"`, R: testutil.FileInfoDir}, 940 {C: `mount "device" "/target" "type" 0 ""`}, 941 {C: `mount "none" "/target" "" MS_SHARED ""`}, 942 }) 943 } 944 945 // Change.Perform wants to mount a filesystem but it fails. 946 func (s *changeSuite) TestPerformFilesystemMountWithError(c *C) { 947 s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir) 948 s.sys.InsertFault(`mount "device" "/target" "type" 0 ""`, errTesting) 949 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}} 950 synth, err := chg.Perform(s.as) 951 c.Assert(err, Equals, errTesting) 952 c.Assert(synth, HasLen, 0) 953 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 954 {C: `lstat "/target"`, R: testutil.FileInfoDir}, 955 {C: `mount "device" "/target" "type" 0 ""`, E: errTesting}, 956 }) 957 } 958 959 // Change.Perform wants to mount a filesystem with sharing changes but mounting fails. 960 func (s *changeSuite) TestPerformFilesystemMountAndShareWithError(c *C) { 961 s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir) 962 s.sys.InsertFault(`mount "device" "/target" "type" 0 ""`, errTesting) 963 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type", Options: []string{"shared"}}} 964 synth, err := chg.Perform(s.as) 965 c.Assert(err, Equals, errTesting) 966 c.Assert(synth, HasLen, 0) 967 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 968 {C: `lstat "/target"`, R: testutil.FileInfoDir}, 969 {C: `mount "device" "/target" "type" 0 ""`, E: errTesting}, 970 }) 971 } 972 973 // Change.Perform wants to mount a filesystem but the mount point isn't there. 974 func (s *changeSuite) TestPerformFilesystemMountWithoutMountPoint(c *C) { 975 defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. 976 s.sys.InsertFault(`lstat "/target"`, syscall.ENOENT) 977 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}} 978 synth, err := chg.Perform(s.as) 979 c.Assert(err, IsNil) 980 c.Assert(synth, HasLen, 0) 981 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 982 {C: `lstat "/target"`, E: syscall.ENOENT}, 983 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, 984 {C: `mkdirat 3 "target" 0755`}, 985 {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, 986 {C: `fchown 4 0 0`}, 987 {C: `close 4`}, 988 {C: `close 3`}, 989 {C: `mount "device" "/target" "type" 0 ""`}, 990 }) 991 } 992 993 // Change.Perform wants to create a filesystem but the mount point isn't there and cannot be created. 994 func (s *changeSuite) TestPerformFilesystemMountWithoutMountPointWithErrors(c *C) { 995 defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. 996 s.sys.InsertFault(`lstat "/target"`, syscall.ENOENT) 997 s.sys.InsertFault(`mkdirat 3 "target" 0755`, errTesting) 998 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}} 999 synth, err := chg.Perform(s.as) 1000 c.Assert(err, ErrorMatches, `cannot create directory "/target": testing`) 1001 c.Assert(synth, HasLen, 0) 1002 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 1003 {C: `lstat "/target"`, E: syscall.ENOENT}, 1004 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, 1005 {C: `mkdirat 3 "target" 0755`, E: errTesting}, 1006 {C: `close 3`}, 1007 }) 1008 } 1009 1010 // Change.Perform wants to mount a filesystem but the mount point isn't there and the parent is read-only. 1011 func (s *changeSuite) TestPerformFilesystemMountWithoutMountPointAndReadOnlyBase(c *C) { 1012 defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. 1013 s.sys.InsertFault(`lstat "/rofs/target"`, syscall.ENOENT) 1014 s.sys.InsertFault(`mkdirat 3 "rofs" 0755`, syscall.EEXIST) 1015 s.sys.InsertFault(`openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, syscall.ENOENT, nil) // works on 2nd try 1016 s.sys.InsertFault(`mkdirat 4 "target" 0755`, syscall.EROFS, nil) // works on 2nd try 1017 s.sys.InsertSysLstatResult(`lstat "/rofs" <ptr>`, syscall.Stat_t{Uid: 0, Gid: 0, Mode: 0755}) 1018 s.sys.InsertReadDirResult(`readdir "/rofs"`, nil) // pretend /rofs is empty. 1019 s.sys.InsertFault(`lstat "/tmp/.snap/rofs"`, syscall.ENOENT) 1020 s.sys.InsertOsLstatResult(`lstat "/rofs"`, testutil.FileInfoDir) 1021 s.sys.InsertFstatResult(`fstat 4 <ptr>`, syscall.Stat_t{}) 1022 s.sys.InsertFstatResult(`fstat 7 <ptr>`, syscall.Stat_t{}) 1023 s.sys.InsertFstatResult(`fstat 6 <ptr>`, syscall.Stat_t{}) 1024 s.sys.InsertFstatfsResult(`fstatfs 6 <ptr>`, syscall.Statfs_t{}) 1025 1026 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/rofs/target", Type: "type"}} 1027 synth, err := chg.Perform(s.as) 1028 c.Assert(err, IsNil) 1029 c.Assert(synth, DeepEquals, []*update.Change{ 1030 {Action: update.Mount, Entry: osutil.MountEntry{ 1031 Name: "tmpfs", Dir: "/rofs", Type: "tmpfs", 1032 Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/rofs/target", "mode=0755", "uid=0", "gid=0"}}, 1033 }, 1034 }) 1035 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 1036 // sniff mount target 1037 {C: `lstat "/rofs/target"`, E: syscall.ENOENT}, 1038 1039 // /rofs/target is missing, create it 1040 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, 1041 {C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST}, 1042 {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, 1043 {C: `close 3`}, 1044 {C: `mkdirat 4 "target" 0755`, E: syscall.EROFS}, 1045 {C: `close 4`}, 1046 1047 // error, read only filesystem, create a mimic 1048 {C: `lstat "/rofs" <ptr>`, R: syscall.Stat_t{Uid: 0, Gid: 0, Mode: 0755}}, 1049 {C: `readdir "/rofs"`, R: []os.FileInfo(nil)}, 1050 {C: `lstat "/tmp/.snap/rofs"`, E: syscall.ENOENT}, 1051 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, 1052 {C: `mkdirat 3 "tmp" 0755`}, 1053 {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, 1054 {C: `fchown 4 0 0`}, 1055 {C: `mkdirat 4 ".snap" 0755`}, 1056 {C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 5}, 1057 {C: `fchown 5 0 0`}, 1058 {C: `close 4`}, 1059 {C: `close 3`}, 1060 {C: `mkdirat 5 "rofs" 0755`}, 1061 {C: `openat 5 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, 1062 {C: `fchown 3 0 0`}, 1063 {C: `close 3`}, 1064 {C: `close 5`}, 1065 {C: `lstat "/rofs"`, R: testutil.FileInfoDir}, 1066 1067 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, 1068 {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, 1069 {C: `fstat 4 <ptr>`, R: syscall.Stat_t{}}, 1070 {C: `close 3`}, 1071 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, 1072 {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5}, 1073 {C: `openat 5 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 6}, 1074 {C: `openat 6 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 7}, 1075 {C: `fstat 7 <ptr>`, R: syscall.Stat_t{}}, 1076 {C: `close 6`}, 1077 {C: `close 5`}, 1078 {C: `close 3`}, 1079 {C: `mount "/proc/self/fd/4" "/proc/self/fd/7" "" MS_BIND|MS_REC ""`}, 1080 {C: `close 7`}, 1081 {C: `close 4`}, 1082 1083 {C: `lstat "/rofs"`, R: testutil.FileInfoDir}, 1084 {C: `mount "tmpfs" "/rofs" "tmpfs" 0 "mode=0755,uid=0,gid=0"`}, 1085 {C: `mount "none" "/tmp/.snap/rofs" "" MS_REC|MS_PRIVATE ""`}, 1086 {C: `unmount "/tmp/.snap/rofs" UMOUNT_NOFOLLOW|MNT_DETACH`}, 1087 1088 // Perform clean up after the unmount operation. 1089 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, 1090 {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, 1091 {C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5}, 1092 {C: `openat 5 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 6}, 1093 {C: `fstat 6 <ptr>`, R: syscall.Stat_t{}}, 1094 {C: `close 5`}, 1095 {C: `close 4`}, 1096 {C: `close 3`}, 1097 {C: `fstatfs 6 <ptr>`, R: syscall.Statfs_t{}}, 1098 {C: `remove "/tmp/.snap/rofs"`}, 1099 {C: `close 6`}, 1100 1101 // mimic ready, re-try initial mkdir 1102 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, 1103 {C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST}, 1104 {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, 1105 {C: `close 3`}, 1106 {C: `mkdirat 4 "target" 0755`}, 1107 {C: `openat 4 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, 1108 {C: `fchown 3 0 0`}, 1109 {C: `close 3`}, 1110 {C: `close 4`}, 1111 1112 // mount the filesystem 1113 {C: `mount "device" "/rofs/target" "type" 0 ""`}, 1114 }) 1115 } 1116 1117 // Change.Perform wants to mount a filesystem but the mount point isn't there and the parent is read-only and mimic fails during planning. 1118 func (s *changeSuite) TestPerformFilesystemMountWithoutMountPointAndReadOnlyBaseErrorWhilePlanning(c *C) { 1119 defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. 1120 s.sys.InsertFault(`lstat "/rofs/target"`, syscall.ENOENT) 1121 s.sys.InsertFault(`mkdirat 3 "rofs" 0755`, syscall.EEXIST) 1122 s.sys.InsertFault(`openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, syscall.ENOENT) 1123 s.sys.InsertFault(`mkdirat 4 "target" 0755`, syscall.EROFS) 1124 s.sys.InsertFault(`lstat "/tmp/.snap/rofs"`, syscall.ENOENT) 1125 s.sys.InsertOsLstatResult(`lstat "/rofs"`, testutil.FileInfoDir) 1126 s.sys.InsertSysLstatResult(`lstat "/rofs" <ptr>`, syscall.Stat_t{Uid: 0, Gid: 0, Mode: 0755}) 1127 s.sys.InsertReadDirResult(`readdir "/rofs"`, nil) 1128 s.sys.InsertFault(`readdir "/rofs"`, errTesting) // make the writable mimic fail 1129 1130 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/rofs/target", Type: "type"}} 1131 synth, err := chg.Perform(s.as) 1132 c.Assert(err, ErrorMatches, `cannot create writable mimic over "/rofs": testing`) 1133 c.Assert(synth, HasLen, 0) 1134 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 1135 // sniff mount target 1136 {C: `lstat "/rofs/target"`, E: syscall.ENOENT}, 1137 1138 // /rofs/target is missing, create it 1139 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, 1140 {C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST}, 1141 {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, 1142 {C: `close 3`}, 1143 {C: `mkdirat 4 "target" 0755`, E: syscall.EROFS}, 1144 {C: `close 4`}, 1145 1146 // error, read only filesystem, create a mimic 1147 {C: `lstat "/rofs" <ptr>`, R: syscall.Stat_t{Uid: 0, Gid: 0, Mode: 0755}}, 1148 {C: `readdir "/rofs"`, E: errTesting}, 1149 // cannot create mimic, that's it 1150 }) 1151 } 1152 1153 // Change.Perform wants to mount a filesystem but the mount point isn't there and the parent is read-only and mimic fails during execution. 1154 func (s *changeSuite) TestPerformFilesystemMountWithoutMountPointAndReadOnlyBaseErrorWhileExecuting(c *C) { 1155 defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. 1156 s.sys.InsertFault(`lstat "/rofs/target"`, syscall.ENOENT) 1157 s.sys.InsertFault(`mkdirat 3 "rofs" 0755`, syscall.EEXIST) 1158 s.sys.InsertFault(`openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, syscall.ENOENT) 1159 s.sys.InsertFault(`mkdirat 4 "target" 0755`, syscall.EROFS) 1160 s.sys.InsertFault(`lstat "/tmp/.snap/rofs"`, syscall.ENOENT) 1161 s.sys.InsertOsLstatResult(`lstat "/rofs"`, testutil.FileInfoDir) 1162 s.sys.InsertSysLstatResult(`lstat "/rofs" <ptr>`, syscall.Stat_t{Uid: 0, Gid: 0, Mode: 0755}) 1163 s.sys.InsertReadDirResult(`readdir "/rofs"`, nil) 1164 s.sys.InsertFault(`mkdirat 4 ".snap" 0755`, errTesting) // make the writable mimic fail 1165 1166 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/rofs/target", Type: "type"}} 1167 synth, err := chg.Perform(s.as) 1168 c.Assert(err, ErrorMatches, `cannot create writable mimic over "/rofs": cannot create directory "/tmp/.snap": testing`) 1169 c.Assert(synth, HasLen, 0) 1170 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 1171 // sniff mount target 1172 {C: `lstat "/rofs/target"`, E: syscall.ENOENT}, 1173 1174 // /rofs/target is missing, create it 1175 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, 1176 {C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST}, 1177 {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, 1178 {C: `close 3`}, 1179 {C: `mkdirat 4 "target" 0755`, E: syscall.EROFS}, 1180 {C: `close 4`}, 1181 1182 // error, read only filesystem, create a mimic 1183 {C: `lstat "/rofs" <ptr>`, R: syscall.Stat_t{Uid: 0, Gid: 0, Mode: 0755}}, 1184 {C: `readdir "/rofs"`, R: []os.FileInfo(nil)}, 1185 {C: `lstat "/tmp/.snap/rofs"`, E: syscall.ENOENT}, 1186 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, 1187 {C: `mkdirat 3 "tmp" 0755`}, 1188 {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, 1189 {C: `fchown 4 0 0`}, 1190 {C: `mkdirat 4 ".snap" 0755`, E: errTesting}, 1191 {C: `close 4`}, 1192 {C: `close 3`}, 1193 // cannot create mimic, that's it 1194 }) 1195 } 1196 1197 // Change.Perform wants to mount a filesystem but there's a symlink in mount point. 1198 func (s *changeSuite) TestPerformFilesystemMountWithSymlinkInMountPoint(c *C) { 1199 s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoSymlink) 1200 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}} 1201 synth, err := chg.Perform(s.as) 1202 c.Assert(err, ErrorMatches, `cannot use "/target" as mount point: not a directory`) 1203 c.Assert(synth, HasLen, 0) 1204 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 1205 {C: `lstat "/target"`, R: testutil.FileInfoSymlink}, 1206 }) 1207 } 1208 1209 // Change.Perform wants to mount a filesystem but there's a file in mount point. 1210 func (s *changeSuite) TestPerformFilesystemMountWithFileInMountPoint(c *C) { 1211 s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoFile) 1212 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}} 1213 synth, err := chg.Perform(s.as) 1214 c.Assert(err, ErrorMatches, `cannot use "/target" as mount point: not a directory`) 1215 c.Assert(synth, HasLen, 0) 1216 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 1217 {C: `lstat "/target"`, R: testutil.FileInfoFile}, 1218 }) 1219 } 1220 1221 // Change.Perform wants to unmount a filesystem. 1222 func (s *changeSuite) TestPerformFilesystemUnmount(c *C) { 1223 s.sys.InsertFstatResult(`fstat 4 <ptr>`, syscall.Stat_t{}) 1224 s.sys.InsertFstatfsResult(`fstatfs 4 <ptr>`, syscall.Statfs_t{}) 1225 chg := &update.Change{Action: update.Unmount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}} 1226 synth, err := chg.Perform(s.as) 1227 c.Assert(err, IsNil) 1228 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 1229 {C: `unmount "/target" UMOUNT_NOFOLLOW`}, 1230 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, 1231 {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, 1232 {C: `fstat 4 <ptr>`, R: syscall.Stat_t{}}, 1233 {C: `close 3`}, 1234 {C: `fstatfs 4 <ptr>`, R: syscall.Statfs_t{}}, 1235 {C: `remove "/target"`}, 1236 {C: `close 4`}, 1237 }) 1238 c.Assert(synth, HasLen, 0) 1239 } 1240 1241 // Change.Perform wants to detach a bind mount. 1242 func (s *changeSuite) TestPerformFilesystemDetch(c *C) { 1243 s.sys.InsertFstatResult(`fstat 4 <ptr>`, syscall.Stat_t{}) 1244 s.sys.InsertFstatfsResult(`fstatfs 4 <ptr>`, syscall.Statfs_t{}) 1245 chg := &update.Change{Action: update.Unmount, Entry: osutil.MountEntry{Name: "/something", Dir: "/target", Options: []string{"x-snapd.detach"}}} 1246 synth, err := chg.Perform(s.as) 1247 c.Assert(err, IsNil) 1248 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 1249 {C: `mount "none" "/target" "" MS_REC|MS_PRIVATE ""`}, 1250 {C: `unmount "/target" UMOUNT_NOFOLLOW|MNT_DETACH`}, 1251 1252 // Perform clean up after the unmount operation. 1253 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, 1254 {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, 1255 {C: `fstat 4 <ptr>`, R: syscall.Stat_t{}}, 1256 {C: `close 3`}, 1257 {C: `fstatfs 4 <ptr>`, R: syscall.Statfs_t{}}, 1258 {C: `remove "/target"`}, 1259 {C: `close 4`}, 1260 }) 1261 c.Assert(synth, HasLen, 0) 1262 } 1263 1264 // Change.Perform wants to unmount a filesystem but it fails. 1265 func (s *changeSuite) TestPerformFilesystemUnmountError(c *C) { 1266 s.sys.InsertFault(`unmount "/target" UMOUNT_NOFOLLOW`, errTesting) 1267 chg := &update.Change{Action: update.Unmount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}} 1268 synth, err := chg.Perform(s.as) 1269 c.Assert(err, Equals, errTesting) 1270 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 1271 {C: `unmount "/target" UMOUNT_NOFOLLOW`, E: errTesting}, 1272 }) 1273 c.Assert(synth, HasLen, 0) 1274 } 1275 1276 // Change.Perform passes non-flag options to the kernel. 1277 func (s *changeSuite) TestPerformFilesystemMountWithOptions(c *C) { 1278 s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir) 1279 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type", Options: []string{"ro", "funky"}}} 1280 synth, err := chg.Perform(s.as) 1281 c.Assert(err, IsNil) 1282 c.Assert(synth, HasLen, 0) 1283 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 1284 {C: `lstat "/target"`, R: testutil.FileInfoDir}, 1285 {C: `mount "device" "/target" "type" MS_RDONLY "funky"`}, 1286 }) 1287 } 1288 1289 // Change.Perform doesn't pass snapd-specific options to the kernel. 1290 func (s *changeSuite) TestPerformFilesystemMountWithSnapdSpecificOptions(c *C) { 1291 s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir) 1292 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type", Options: []string{"ro", "x-snapd.funky"}}} 1293 synth, err := chg.Perform(s.as) 1294 c.Assert(err, IsNil) 1295 c.Assert(synth, HasLen, 0) 1296 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 1297 {C: `lstat "/target"`, R: testutil.FileInfoDir}, 1298 {C: `mount "device" "/target" "type" MS_RDONLY ""`}, 1299 }) 1300 } 1301 1302 // ############################################### 1303 // Topic: bind-mounting and unmounting directories 1304 // ############################################### 1305 1306 // Change.Perform wants to bind mount a directory but the target cannot be stat'ed. 1307 func (s *changeSuite) TestPerformDirectoryBindMountTargetLstatError(c *C) { 1308 s.sys.InsertFault(`lstat "/target"`, errTesting) 1309 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}} 1310 synth, err := chg.Perform(s.as) 1311 c.Assert(err, ErrorMatches, `cannot inspect "/target": testing`) 1312 c.Assert(synth, HasLen, 0) 1313 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 1314 {C: `lstat "/target"`, E: errTesting}, 1315 }) 1316 } 1317 1318 // Change.Perform wants to bind mount a directory but the source cannot be stat'ed. 1319 func (s *changeSuite) TestPerformDirectoryBindMountSourceLstatError(c *C) { 1320 s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir) 1321 s.sys.InsertFault(`lstat "/source"`, errTesting) 1322 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}} 1323 synth, err := chg.Perform(s.as) 1324 c.Assert(err, ErrorMatches, `cannot inspect "/source": testing`) 1325 c.Assert(synth, HasLen, 0) 1326 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 1327 {C: `lstat "/target"`, R: testutil.FileInfoDir}, 1328 {C: `lstat "/source"`, E: errTesting}, 1329 }) 1330 } 1331 1332 // Change.Perform wants to bind mount a directory. 1333 func (s *changeSuite) TestPerformDirectoryBindMount(c *C) { 1334 s.sys.InsertOsLstatResult(`lstat "/source"`, testutil.FileInfoDir) 1335 s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir) 1336 s.sys.InsertFstatResult(`fstat 4 <ptr>`, syscall.Stat_t{}) 1337 s.sys.InsertFstatResult(`fstat 5 <ptr>`, syscall.Stat_t{}) 1338 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}} 1339 synth, err := chg.Perform(s.as) 1340 c.Assert(err, IsNil) 1341 c.Assert(synth, HasLen, 0) 1342 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 1343 {C: `lstat "/target"`, R: testutil.FileInfoDir}, 1344 {C: `lstat "/source"`, R: testutil.FileInfoDir}, 1345 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, 1346 {C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, 1347 {C: `fstat 4 <ptr>`, R: syscall.Stat_t{}}, 1348 {C: `close 3`}, 1349 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, 1350 {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5}, 1351 {C: `fstat 5 <ptr>`, R: syscall.Stat_t{}}, 1352 {C: `close 3`}, 1353 {C: `mount "/proc/self/fd/4" "/proc/self/fd/5" "" MS_BIND ""`}, 1354 {C: `close 5`}, 1355 {C: `close 4`}, 1356 }) 1357 } 1358 1359 // Change.Perform wants to bind mount a directory with sharing changes. 1360 func (s *changeSuite) TestPerformRecursiveDirectorySharedBindMount(c *C) { 1361 s.sys.InsertOsLstatResult(`lstat "/source"`, testutil.FileInfoDir) 1362 s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir) 1363 s.sys.InsertFstatResult(`fstat 4 <ptr>`, syscall.Stat_t{}) 1364 s.sys.InsertFstatResult(`fstat 5 <ptr>`, syscall.Stat_t{}) 1365 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"rshared", "rbind"}}} 1366 synth, err := chg.Perform(s.as) 1367 c.Assert(err, IsNil) 1368 c.Assert(synth, HasLen, 0) 1369 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 1370 {C: `lstat "/target"`, R: testutil.FileInfoDir}, 1371 {C: `lstat "/source"`, R: testutil.FileInfoDir}, 1372 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, 1373 {C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, 1374 {C: `fstat 4 <ptr>`, R: syscall.Stat_t{}}, 1375 {C: `close 3`}, 1376 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, 1377 {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5}, 1378 {C: `fstat 5 <ptr>`, R: syscall.Stat_t{}}, 1379 {C: `close 3`}, 1380 {C: `mount "/proc/self/fd/4" "/proc/self/fd/5" "" MS_BIND|MS_REC ""`}, 1381 {C: `close 5`}, 1382 {C: `close 4`}, 1383 {C: `mount "none" "/target" "" MS_REC|MS_SHARED ""`}, 1384 }) 1385 } 1386 1387 // Change.Perform wants to bind mount a directory but it fails. 1388 func (s *changeSuite) TestPerformDirectoryBindMountWithError(c *C) { 1389 s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir) 1390 s.sys.InsertOsLstatResult(`lstat "/source"`, testutil.FileInfoDir) 1391 s.sys.InsertFstatResult(`fstat 4 <ptr>`, syscall.Stat_t{}) 1392 s.sys.InsertFstatResult(`fstat 5 <ptr>`, syscall.Stat_t{}) 1393 s.sys.InsertFault(`mount "/proc/self/fd/4" "/proc/self/fd/5" "" MS_BIND ""`, errTesting) 1394 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}} 1395 synth, err := chg.Perform(s.as) 1396 c.Assert(err, Equals, errTesting) 1397 c.Assert(synth, HasLen, 0) 1398 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 1399 {C: `lstat "/target"`, R: testutil.FileInfoDir}, 1400 {C: `lstat "/source"`, R: testutil.FileInfoDir}, 1401 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, 1402 {C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, 1403 {C: `fstat 4 <ptr>`, R: syscall.Stat_t{}}, 1404 {C: `close 3`}, 1405 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, 1406 {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5}, 1407 {C: `fstat 5 <ptr>`, R: syscall.Stat_t{}}, 1408 {C: `close 3`}, 1409 {C: `mount "/proc/self/fd/4" "/proc/self/fd/5" "" MS_BIND ""`, E: errTesting}, 1410 {C: `close 5`}, 1411 {C: `close 4`}, 1412 }) 1413 } 1414 1415 // Change.Perform wants to bind mount a directory but the mount point isn't there. 1416 func (s *changeSuite) TestPerformDirectoryBindMountWithoutMountPoint(c *C) { 1417 defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. 1418 s.sys.InsertOsLstatResult(`lstat "/source"`, testutil.FileInfoDir) 1419 s.sys.InsertFault(`lstat "/target"`, syscall.ENOENT) 1420 s.sys.InsertFstatResult(`fstat 4 <ptr>`, syscall.Stat_t{}) 1421 s.sys.InsertFstatResult(`fstat 5 <ptr>`, syscall.Stat_t{}) 1422 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}} 1423 synth, err := chg.Perform(s.as) 1424 c.Assert(err, IsNil) 1425 c.Assert(synth, HasLen, 0) 1426 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 1427 {C: `lstat "/target"`, E: syscall.ENOENT}, 1428 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, 1429 {C: `mkdirat 3 "target" 0755`}, 1430 {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, 1431 {C: `fchown 4 0 0`}, 1432 {C: `close 4`}, 1433 {C: `close 3`}, 1434 {C: `lstat "/source"`, R: testutil.FileInfoDir}, 1435 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, 1436 {C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, 1437 {C: `fstat 4 <ptr>`, R: syscall.Stat_t{}}, 1438 {C: `close 3`}, 1439 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, 1440 {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5}, 1441 {C: `fstat 5 <ptr>`, R: syscall.Stat_t{}}, 1442 {C: `close 3`}, 1443 {C: `mount "/proc/self/fd/4" "/proc/self/fd/5" "" MS_BIND ""`}, 1444 {C: `close 5`}, 1445 {C: `close 4`}, 1446 }) 1447 } 1448 1449 // Change.Perform wants to bind mount a directory but the mount source isn't there. 1450 func (s *changeSuite) TestPerformDirectoryBindMountWithoutMountSource(c *C) { 1451 defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. 1452 s.sys.InsertFault(`lstat "/source"`, syscall.ENOENT) 1453 s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir) 1454 s.sys.InsertFstatResult(`fstat 4 <ptr>`, syscall.Stat_t{}) 1455 s.sys.InsertFstatResult(`fstat 5 <ptr>`, syscall.Stat_t{}) 1456 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}} 1457 synth, err := chg.Perform(s.as) 1458 c.Assert(err, IsNil) 1459 c.Assert(synth, HasLen, 0) 1460 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 1461 {C: `lstat "/target"`, R: testutil.FileInfoDir}, 1462 {C: `lstat "/source"`, E: syscall.ENOENT}, 1463 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, 1464 {C: `mkdirat 3 "source" 0755`}, 1465 {C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, 1466 {C: `fchown 4 0 0`}, 1467 {C: `close 4`}, 1468 {C: `close 3`}, 1469 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, 1470 {C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, 1471 {C: `fstat 4 <ptr>`, R: syscall.Stat_t{}}, 1472 {C: `close 3`}, 1473 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, 1474 {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5}, 1475 {C: `fstat 5 <ptr>`, R: syscall.Stat_t{}}, 1476 {C: `close 3`}, 1477 {C: `mount "/proc/self/fd/4" "/proc/self/fd/5" "" MS_BIND ""`}, 1478 {C: `close 5`}, 1479 {C: `close 4`}, 1480 }) 1481 } 1482 1483 // Change.Perform wants to create a directory bind mount but the mount point isn't there and cannot be created. 1484 func (s *changeSuite) TestPerformDirectoryBindMountWithoutMountPointWithErrors(c *C) { 1485 defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. 1486 s.sys.InsertFault(`lstat "/target"`, syscall.ENOENT) 1487 s.sys.InsertFault(`mkdirat 3 "target" 0755`, errTesting) 1488 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}} 1489 synth, err := chg.Perform(s.as) 1490 c.Assert(err, ErrorMatches, `cannot create directory "/target": testing`) 1491 c.Assert(synth, HasLen, 0) 1492 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 1493 {C: `lstat "/target"`, E: syscall.ENOENT}, 1494 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, 1495 {C: `mkdirat 3 "target" 0755`, E: errTesting}, 1496 {C: `close 3`}, 1497 }) 1498 } 1499 1500 // Change.Perform wants to create a directory bind mount but the mount source isn't there and cannot be created. 1501 func (s *changeSuite) TestPerformDirectoryBindMountWithoutMountSourceWithErrors(c *C) { 1502 defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. 1503 s.sys.InsertFault(`lstat "/source"`, syscall.ENOENT) 1504 s.sys.InsertFault(`mkdirat 3 "source" 0755`, errTesting) 1505 s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir) 1506 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}} 1507 synth, err := chg.Perform(s.as) 1508 c.Assert(err, ErrorMatches, `cannot create directory "/source": testing`) 1509 c.Assert(synth, HasLen, 0) 1510 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 1511 {C: `lstat "/target"`, R: testutil.FileInfoDir}, 1512 {C: `lstat "/source"`, E: syscall.ENOENT}, 1513 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, 1514 {C: `mkdirat 3 "source" 0755`, E: errTesting}, 1515 {C: `close 3`}, 1516 }) 1517 } 1518 1519 // Change.Perform wants to bind mount a directory but the mount point isn't there and the parent is read-only. 1520 func (s *changeSuite) TestPerformDirectoryBindMountWithoutMountPointAndReadOnlyBase(c *C) { 1521 defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. 1522 s.sys.InsertFault(`lstat "/rofs/target"`, syscall.ENOENT) 1523 s.sys.InsertFault(`mkdirat 3 "rofs" 0755`, syscall.EEXIST) 1524 s.sys.InsertFault(`openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, syscall.ENOENT, nil) // works on 2nd try 1525 s.sys.InsertFault(`mkdirat 4 "target" 0755`, syscall.EROFS, nil) // works on 2nd try 1526 s.sys.InsertSysLstatResult(`lstat "/rofs" <ptr>`, syscall.Stat_t{Uid: 0, Gid: 0, Mode: 0755}) 1527 s.sys.InsertReadDirResult(`readdir "/rofs"`, nil) // pretend /rofs is empty. 1528 s.sys.InsertFault(`lstat "/tmp/.snap/rofs"`, syscall.ENOENT) 1529 s.sys.InsertOsLstatResult(`lstat "/rofs"`, testutil.FileInfoDir) 1530 s.sys.InsertOsLstatResult(`lstat "/source"`, testutil.FileInfoDir) 1531 s.sys.InsertFstatResult(`fstat 4 <ptr>`, syscall.Stat_t{}) 1532 s.sys.InsertFstatResult(`fstat 7 <ptr>`, syscall.Stat_t{}) 1533 s.sys.InsertFstatResult(`fstat 6 <ptr>`, syscall.Stat_t{}) 1534 s.sys.InsertFstatResult(`fstat 6 <ptr>`, syscall.Stat_t{}) 1535 s.sys.InsertFstatfsResult(`fstatfs 6 <ptr>`, syscall.Statfs_t{}) 1536 1537 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/rofs/target", Options: []string{"bind"}}} 1538 synth, err := chg.Perform(s.as) 1539 c.Assert(err, IsNil) 1540 c.Assert(synth, DeepEquals, []*update.Change{ 1541 {Action: update.Mount, Entry: osutil.MountEntry{ 1542 Name: "tmpfs", Dir: "/rofs", Type: "tmpfs", 1543 Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/rofs/target", "mode=0755", "uid=0", "gid=0"}}, 1544 }, 1545 }) 1546 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 1547 // sniff mount target 1548 {C: `lstat "/rofs/target"`, E: syscall.ENOENT}, 1549 1550 // /rofs/target is missing, create it 1551 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, 1552 {C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST}, 1553 {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, 1554 {C: `close 3`}, 1555 {C: `mkdirat 4 "target" 0755`, E: syscall.EROFS}, 1556 {C: `close 4`}, 1557 1558 // error, read only filesystem, create a mimic 1559 {C: `lstat "/rofs" <ptr>`, R: syscall.Stat_t{Uid: 0, Gid: 0, Mode: 0755}}, 1560 {C: `readdir "/rofs"`, R: []os.FileInfo(nil)}, 1561 {C: `lstat "/tmp/.snap/rofs"`, E: syscall.ENOENT}, 1562 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, 1563 {C: `mkdirat 3 "tmp" 0755`}, 1564 {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, 1565 {C: `fchown 4 0 0`}, 1566 {C: `mkdirat 4 ".snap" 0755`}, 1567 {C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 5}, 1568 {C: `fchown 5 0 0`}, 1569 {C: `close 4`}, 1570 {C: `close 3`}, 1571 {C: `mkdirat 5 "rofs" 0755`}, 1572 {C: `openat 5 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, 1573 {C: `fchown 3 0 0`}, 1574 {C: `close 3`}, 1575 {C: `close 5`}, 1576 {C: `lstat "/rofs"`, R: testutil.FileInfoDir}, 1577 1578 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, 1579 {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, 1580 {C: `fstat 4 <ptr>`, R: syscall.Stat_t{}}, 1581 {C: `close 3`}, 1582 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, 1583 {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5}, 1584 {C: `openat 5 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 6}, 1585 {C: `openat 6 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 7}, 1586 {C: `fstat 7 <ptr>`, R: syscall.Stat_t{}}, 1587 {C: `close 6`}, 1588 {C: `close 5`}, 1589 {C: `close 3`}, 1590 {C: `mount "/proc/self/fd/4" "/proc/self/fd/7" "" MS_BIND|MS_REC ""`}, 1591 {C: `close 7`}, 1592 {C: `close 4`}, 1593 1594 {C: `lstat "/rofs"`, R: testutil.FileInfoDir}, 1595 {C: `mount "tmpfs" "/rofs" "tmpfs" 0 "mode=0755,uid=0,gid=0"`}, 1596 {C: `mount "none" "/tmp/.snap/rofs" "" MS_REC|MS_PRIVATE ""`}, 1597 {C: `unmount "/tmp/.snap/rofs" UMOUNT_NOFOLLOW|MNT_DETACH`}, 1598 1599 // Perform clean up after the unmount operation. 1600 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, 1601 {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, 1602 {C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5}, 1603 {C: `openat 5 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 6}, 1604 {C: `fstat 6 <ptr>`, R: syscall.Stat_t{}}, 1605 {C: `close 5`}, 1606 {C: `close 4`}, 1607 {C: `close 3`}, 1608 {C: `fstatfs 6 <ptr>`, R: syscall.Statfs_t{}}, 1609 {C: `remove "/tmp/.snap/rofs"`}, 1610 {C: `close 6`}, 1611 1612 // mimic ready, re-try initial mkdir 1613 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, 1614 {C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST}, 1615 {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, 1616 {C: `close 3`}, 1617 {C: `mkdirat 4 "target" 0755`}, 1618 {C: `openat 4 "target" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, 1619 {C: `fchown 3 0 0`}, 1620 {C: `close 3`}, 1621 {C: `close 4`}, 1622 1623 // sniff mount source 1624 {C: `lstat "/source"`, R: testutil.FileInfoDir}, 1625 1626 // mount the filesystem 1627 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, 1628 {C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, 1629 {C: `fstat 4 <ptr>`, R: syscall.Stat_t{}}, 1630 {C: `close 3`}, 1631 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, 1632 {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5}, 1633 {C: `openat 5 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 6}, 1634 {C: `fstat 6 <ptr>`, R: syscall.Stat_t{}}, 1635 {C: `close 5`}, 1636 {C: `close 3`}, 1637 {C: `mount "/proc/self/fd/4" "/proc/self/fd/6" "" MS_BIND ""`}, 1638 {C: `close 6`}, 1639 {C: `close 4`}, 1640 }) 1641 } 1642 1643 // Change.Perform wants to bind mount a directory but the mount source isn't there and the parent is read-only. 1644 func (s *changeSuite) TestPerformDirectoryBindMountWithoutMountSourceAndReadOnlyBase(c *C) { 1645 defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. 1646 s.sys.InsertOsLstatResult(`lstat "/rofs"`, testutil.FileInfoDir) 1647 s.sys.InsertFault(`lstat "/rofs/source"`, syscall.ENOENT) 1648 s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir) 1649 s.sys.InsertFault(`mkdirat 3 "rofs" 0755`, syscall.EEXIST) 1650 s.sys.InsertFault(`mkdirat 4 "source" 0755`, syscall.EROFS) 1651 1652 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/rofs/source", Dir: "/target", Options: []string{"bind"}}} 1653 synth, err := chg.Perform(s.as) 1654 c.Assert(err, ErrorMatches, `cannot operate on read-only filesystem at /rofs`) 1655 c.Assert(synth, HasLen, 0) 1656 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 1657 {C: `lstat "/target"`, R: testutil.FileInfoDir}, 1658 {C: `lstat "/rofs/source"`, E: syscall.ENOENT}, 1659 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, 1660 {C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST}, 1661 {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, 1662 {C: `close 3`}, 1663 {C: `mkdirat 4 "source" 0755`, E: syscall.EROFS}, 1664 {C: `close 4`}, 1665 }) 1666 } 1667 1668 // Change.Perform wants to bind mount a directory but the mount source isn't there and the parent is read-only but this is for a layout. 1669 func (s *changeSuite) TestPerformDirectoryBindMountWithoutMountSourceAndReadOnlyBaseForLayout(c *C) { 1670 defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. 1671 s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir) 1672 s.sys.InsertFault(`lstat "/rofs/source"`, syscall.ENOENT) 1673 s.sys.InsertFault(`mkdirat 3 "rofs" 0755`, syscall.EEXIST) 1674 s.sys.InsertFault(`openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, syscall.ENOENT, nil) // works on 2nd try 1675 s.sys.InsertFault(`mkdirat 4 "source" 0755`, syscall.EROFS, nil) // works on 2nd try 1676 s.sys.InsertReadDirResult(`readdir "/rofs"`, nil) // pretend /rofs is empty. 1677 s.sys.InsertFault(`lstat "/tmp/.snap/rofs"`, syscall.ENOENT) 1678 s.sys.InsertOsLstatResult(`lstat "/rofs"`, testutil.FileInfoDir) 1679 s.sys.InsertSysLstatResult(`lstat "/rofs" <ptr>`, syscall.Stat_t{Uid: 0, Gid: 0, Mode: 0755}) 1680 s.sys.InsertFstatResult(`fstat 4 <ptr>`, syscall.Stat_t{}) 1681 s.sys.InsertFstatResult(`fstat 5 <ptr>`, syscall.Stat_t{}) 1682 s.sys.InsertFstatResult(`fstat 7 <ptr>`, syscall.Stat_t{}) 1683 s.sys.InsertFstatResult(`fstat 6 <ptr>`, syscall.Stat_t{}) 1684 s.sys.InsertFstatfsResult(`fstatfs 6 <ptr>`, syscall.Statfs_t{}) 1685 1686 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/rofs/source", Dir: "/target", Options: []string{"bind", "x-snapd.origin=layout"}}} 1687 synth, err := chg.Perform(s.as) 1688 c.Assert(err, IsNil) 1689 c.Check(synth, DeepEquals, []*update.Change{ 1690 {Action: update.Mount, Entry: osutil.MountEntry{ 1691 Name: "tmpfs", Dir: "/rofs", Type: "tmpfs", 1692 Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/rofs/source", "mode=0755", "uid=0", "gid=0"}}, 1693 }, 1694 }) 1695 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 1696 // sniff mount target and source 1697 {C: `lstat "/target"`, R: testutil.FileInfoDir}, 1698 {C: `lstat "/rofs/source"`, E: syscall.ENOENT}, 1699 1700 // /rofs/source is missing, create it 1701 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, 1702 {C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST}, 1703 {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, 1704 {C: `close 3`}, 1705 {C: `mkdirat 4 "source" 0755`, E: syscall.EROFS}, 1706 {C: `close 4`}, 1707 1708 // error /rofs is a read-only filesystem, create a mimic 1709 {C: `lstat "/rofs" <ptr>`, R: syscall.Stat_t{Mode: 0755}}, 1710 {C: `readdir "/rofs"`, R: []os.FileInfo(nil)}, 1711 {C: `lstat "/tmp/.snap/rofs"`, E: syscall.ENOENT}, 1712 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, 1713 {C: `mkdirat 3 "tmp" 0755`}, 1714 {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, 1715 {C: `fchown 4 0 0`}, 1716 {C: `mkdirat 4 ".snap" 0755`}, 1717 {C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 5}, 1718 {C: `fchown 5 0 0`}, 1719 {C: `close 4`}, 1720 {C: `close 3`}, 1721 {C: `mkdirat 5 "rofs" 0755`}, 1722 {C: `openat 5 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, 1723 {C: `fchown 3 0 0`}, 1724 {C: `close 3`}, 1725 {C: `close 5`}, 1726 {C: `lstat "/rofs"`, R: testutil.FileInfoDir}, 1727 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, 1728 {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, 1729 {C: `fstat 4 <ptr>`, R: syscall.Stat_t{}}, 1730 {C: `close 3`}, 1731 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, 1732 {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5}, 1733 {C: `openat 5 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 6}, 1734 {C: `openat 6 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 7}, 1735 {C: `fstat 7 <ptr>`, R: syscall.Stat_t{}}, 1736 {C: `close 6`}, 1737 {C: `close 5`}, 1738 {C: `close 3`}, 1739 {C: `mount "/proc/self/fd/4" "/proc/self/fd/7" "" MS_BIND|MS_REC ""`}, 1740 {C: `close 7`}, 1741 {C: `close 4`}, 1742 {C: `lstat "/rofs"`, R: testutil.FileInfoDir}, 1743 {C: `mount "tmpfs" "/rofs" "tmpfs" 0 "mode=0755,uid=0,gid=0"`}, 1744 {C: `mount "none" "/tmp/.snap/rofs" "" MS_REC|MS_PRIVATE ""`}, 1745 {C: `unmount "/tmp/.snap/rofs" UMOUNT_NOFOLLOW|MNT_DETACH`}, 1746 1747 // Perform clean up after the unmount operation. 1748 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, 1749 {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, 1750 {C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5}, 1751 {C: `openat 5 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 6}, 1752 {C: `fstat 6 <ptr>`, R: syscall.Stat_t{}}, 1753 {C: `close 5`}, 1754 {C: `close 4`}, 1755 {C: `close 3`}, 1756 {C: `fstatfs 6 <ptr>`, R: syscall.Statfs_t{}}, 1757 {C: `remove "/tmp/.snap/rofs"`}, 1758 {C: `close 6`}, 1759 1760 // /rofs/source was missing (we checked earlier), create it 1761 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, 1762 {C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST}, 1763 {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, 1764 {C: `close 3`}, 1765 {C: `mkdirat 4 "source" 0755`}, 1766 {C: `openat 4 "source" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, 1767 {C: `fchown 3 0 0`}, 1768 {C: `close 3`}, 1769 {C: `close 4`}, 1770 1771 // bind mount /rofs/source -> /target 1772 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, 1773 {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, 1774 {C: `openat 4 "source" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5}, 1775 {C: `fstat 5 <ptr>`, R: syscall.Stat_t{}}, 1776 {C: `close 4`}, 1777 {C: `close 3`}, 1778 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, 1779 {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, 1780 {C: `fstat 4 <ptr>`, R: syscall.Stat_t{}}, 1781 {C: `close 3`}, 1782 {C: `mount "/proc/self/fd/5" "/proc/self/fd/4" "" MS_BIND ""`}, 1783 {C: `close 4`}, 1784 {C: `close 5`}, 1785 }) 1786 } 1787 1788 // Change.Perform wants to bind mount a directory but there's a symlink in mount point. 1789 func (s *changeSuite) TestPerformDirectoryBindMountWithSymlinkInMountPoint(c *C) { 1790 s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoSymlink) 1791 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}} 1792 synth, err := chg.Perform(s.as) 1793 c.Assert(err, ErrorMatches, `cannot use "/target" as mount point: not a directory`) 1794 c.Assert(synth, HasLen, 0) 1795 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 1796 {C: `lstat "/target"`, R: testutil.FileInfoSymlink}, 1797 }) 1798 } 1799 1800 // Change.Perform wants to bind mount a directory but there's a file in mount mount. 1801 func (s *changeSuite) TestPerformDirectoryBindMountWithFileInMountPoint(c *C) { 1802 s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoFile) 1803 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}} 1804 synth, err := chg.Perform(s.as) 1805 c.Assert(err, ErrorMatches, `cannot use "/target" as mount point: not a directory`) 1806 c.Assert(synth, HasLen, 0) 1807 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 1808 {C: `lstat "/target"`, R: testutil.FileInfoFile}, 1809 }) 1810 } 1811 1812 // Change.Perform wants to bind mount a directory but there's a symlink in source. 1813 func (s *changeSuite) TestPerformDirectoryBindMountWithSymlinkInMountSource(c *C) { 1814 s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir) 1815 s.sys.InsertOsLstatResult(`lstat "/source"`, testutil.FileInfoSymlink) 1816 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}} 1817 synth, err := chg.Perform(s.as) 1818 c.Assert(err, ErrorMatches, `cannot use "/source" as bind-mount source: not a directory`) 1819 c.Assert(synth, HasLen, 0) 1820 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 1821 {C: `lstat "/target"`, R: testutil.FileInfoDir}, 1822 {C: `lstat "/source"`, R: testutil.FileInfoSymlink}, 1823 }) 1824 } 1825 1826 // Change.Perform wants to bind mount a directory but there's a file in source. 1827 func (s *changeSuite) TestPerformDirectoryBindMountWithFileInMountSource(c *C) { 1828 s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir) 1829 s.sys.InsertOsLstatResult(`lstat "/source"`, testutil.FileInfoFile) 1830 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}} 1831 synth, err := chg.Perform(s.as) 1832 c.Assert(err, ErrorMatches, `cannot use "/source" as bind-mount source: not a directory`) 1833 c.Assert(synth, HasLen, 0) 1834 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 1835 {C: `lstat "/target"`, R: testutil.FileInfoDir}, 1836 {C: `lstat "/source"`, R: testutil.FileInfoFile}, 1837 }) 1838 } 1839 1840 // Change.Perform wants to unmount a directory bind mount. 1841 func (s *changeSuite) TestPerformDirectoryBindUnmount(c *C) { 1842 s.sys.InsertFstatResult(`fstat 4 <ptr>`, syscall.Stat_t{}) 1843 s.sys.InsertFstatfsResult(`fstatfs 4 <ptr>`, syscall.Statfs_t{}) 1844 chg := &update.Change{Action: update.Unmount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}} 1845 synth, err := chg.Perform(s.as) 1846 c.Assert(err, IsNil) 1847 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 1848 {C: `unmount "/target" UMOUNT_NOFOLLOW`}, 1849 1850 // Perform clean up after the unmount operation. 1851 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, 1852 {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, 1853 {C: `fstat 4 <ptr>`, R: syscall.Stat_t{}}, 1854 {C: `close 3`}, 1855 {C: `fstatfs 4 <ptr>`, R: syscall.Statfs_t{}}, 1856 {C: `remove "/target"`}, 1857 {C: `close 4`}, 1858 }) 1859 c.Assert(synth, HasLen, 0) 1860 } 1861 1862 // Change.Perform wants to unmount a directory bind mount but it fails. 1863 func (s *changeSuite) TestPerformDirectoryBindUnmountError(c *C) { 1864 s.sys.InsertFault(`unmount "/target" UMOUNT_NOFOLLOW`, errTesting) 1865 chg := &update.Change{Action: update.Unmount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind"}}} 1866 synth, err := chg.Perform(s.as) 1867 c.Assert(err, Equals, errTesting) 1868 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 1869 {C: `unmount "/target" UMOUNT_NOFOLLOW`, E: errTesting}, 1870 }) 1871 c.Assert(synth, HasLen, 0) 1872 } 1873 1874 // ######################################### 1875 // Topic: bind-mounting and unmounting files 1876 // ######################################### 1877 1878 // Change.Perform wants to bind mount a file but the target cannot be stat'ed. 1879 func (s *changeSuite) TestPerformFileBindMountTargetLstatError(c *C) { 1880 s.sys.InsertFault(`lstat "/target"`, errTesting) 1881 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} 1882 synth, err := chg.Perform(s.as) 1883 c.Assert(err, ErrorMatches, `cannot inspect "/target": testing`) 1884 c.Assert(synth, HasLen, 0) 1885 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 1886 {C: `lstat "/target"`, E: errTesting}, 1887 }) 1888 } 1889 1890 // Change.Perform wants to bind mount a file but the source cannot be stat'ed. 1891 func (s *changeSuite) TestPerformFileBindMountSourceLstatError(c *C) { 1892 s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoFile) 1893 s.sys.InsertFault(`lstat "/source"`, errTesting) 1894 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} 1895 synth, err := chg.Perform(s.as) 1896 c.Assert(err, ErrorMatches, `cannot inspect "/source": testing`) 1897 c.Assert(synth, HasLen, 0) 1898 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 1899 {C: `lstat "/target"`, R: testutil.FileInfoFile}, 1900 {C: `lstat "/source"`, E: errTesting}, 1901 }) 1902 } 1903 1904 // Change.Perform wants to bind mount a file. 1905 func (s *changeSuite) TestPerformFileBindMount(c *C) { 1906 s.sys.InsertOsLstatResult(`lstat "/source"`, testutil.FileInfoFile) 1907 s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoFile) 1908 s.sys.InsertFstatResult(`fstat 4 <ptr>`, syscall.Stat_t{}) 1909 s.sys.InsertFstatResult(`fstat 5 <ptr>`, syscall.Stat_t{}) 1910 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} 1911 synth, err := chg.Perform(s.as) 1912 c.Assert(err, IsNil) 1913 c.Assert(synth, HasLen, 0) 1914 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 1915 {C: `lstat "/target"`, R: testutil.FileInfoFile}, 1916 {C: `lstat "/source"`, R: testutil.FileInfoFile}, 1917 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, 1918 {C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, 1919 {C: `fstat 4 <ptr>`, R: syscall.Stat_t{}}, 1920 {C: `close 3`}, 1921 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, 1922 {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5}, 1923 {C: `fstat 5 <ptr>`, R: syscall.Stat_t{}}, 1924 {C: `close 3`}, 1925 {C: `mount "/proc/self/fd/4" "/proc/self/fd/5" "" MS_BIND ""`}, 1926 {C: `close 5`}, 1927 {C: `close 4`}, 1928 }) 1929 } 1930 1931 // Change.Perform wants to bind mount a file but it fails. 1932 func (s *changeSuite) TestPerformFileBindMountWithError(c *C) { 1933 s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoFile) 1934 s.sys.InsertOsLstatResult(`lstat "/source"`, testutil.FileInfoFile) 1935 s.sys.InsertFstatResult(`fstat 4 <ptr>`, syscall.Stat_t{}) 1936 s.sys.InsertFstatResult(`fstat 5 <ptr>`, syscall.Stat_t{}) 1937 s.sys.InsertFault(`mount "/proc/self/fd/4" "/proc/self/fd/5" "" MS_BIND ""`, errTesting) 1938 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} 1939 synth, err := chg.Perform(s.as) 1940 c.Assert(err, Equals, errTesting) 1941 c.Assert(synth, HasLen, 0) 1942 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 1943 {C: `lstat "/target"`, R: testutil.FileInfoFile}, 1944 {C: `lstat "/source"`, R: testutil.FileInfoFile}, 1945 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, 1946 {C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, 1947 {C: `fstat 4 <ptr>`, R: syscall.Stat_t{}}, 1948 {C: `close 3`}, 1949 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, 1950 {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5}, 1951 {C: `fstat 5 <ptr>`, R: syscall.Stat_t{}}, 1952 {C: `close 3`}, 1953 {C: `mount "/proc/self/fd/4" "/proc/self/fd/5" "" MS_BIND ""`, E: errTesting}, 1954 {C: `close 5`}, 1955 {C: `close 4`}, 1956 }) 1957 } 1958 1959 // Change.Perform wants to bind mount a file but the mount point isn't there. 1960 func (s *changeSuite) TestPerformFileBindMountWithoutMountPoint(c *C) { 1961 defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. 1962 s.sys.InsertOsLstatResult(`lstat "/source"`, testutil.FileInfoFile) 1963 s.sys.InsertFault(`lstat "/target"`, syscall.ENOENT) 1964 s.sys.InsertFstatResult(`fstat 4 <ptr>`, syscall.Stat_t{}) 1965 s.sys.InsertFstatResult(`fstat 5 <ptr>`, syscall.Stat_t{}) 1966 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} 1967 synth, err := chg.Perform(s.as) 1968 c.Assert(err, IsNil) 1969 c.Assert(synth, HasLen, 0) 1970 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 1971 {C: `lstat "/target"`, E: syscall.ENOENT}, 1972 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, 1973 {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, R: 4}, 1974 {C: `fchown 4 0 0`}, 1975 {C: `close 4`}, 1976 {C: `close 3`}, 1977 {C: `lstat "/source"`, R: testutil.FileInfoFile}, 1978 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, 1979 {C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, 1980 {C: `fstat 4 <ptr>`, R: syscall.Stat_t{}}, 1981 {C: `close 3`}, 1982 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, 1983 {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5}, 1984 {C: `fstat 5 <ptr>`, R: syscall.Stat_t{}}, 1985 {C: `close 3`}, 1986 {C: `mount "/proc/self/fd/4" "/proc/self/fd/5" "" MS_BIND ""`}, 1987 {C: `close 5`}, 1988 {C: `close 4`}, 1989 }) 1990 } 1991 1992 // Change.Perform wants to create a directory bind mount but the mount point isn't there and cannot be created. 1993 func (s *changeSuite) TestPerformFileBindMountWithoutMountPointWithErrors(c *C) { 1994 defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. 1995 s.sys.InsertFault(`lstat "/target"`, syscall.ENOENT) 1996 s.sys.InsertFault(`openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, errTesting) 1997 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} 1998 synth, err := chg.Perform(s.as) 1999 c.Assert(err, ErrorMatches, `cannot open file "/target": testing`) 2000 c.Assert(synth, HasLen, 0) 2001 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 2002 {C: `lstat "/target"`, E: syscall.ENOENT}, 2003 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, 2004 {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, E: errTesting}, 2005 {C: `close 3`}, 2006 }) 2007 } 2008 2009 // Change.Perform wants to bind mount a file but the mount source isn't there. 2010 func (s *changeSuite) TestPerformFileBindMountWithoutMountSource(c *C) { 2011 defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. 2012 s.sys.InsertFault(`lstat "/source"`, syscall.ENOENT) 2013 s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoFile) 2014 s.sys.InsertFstatResult(`fstat 4 <ptr>`, syscall.Stat_t{}) 2015 s.sys.InsertFstatResult(`fstat 5 <ptr>`, syscall.Stat_t{}) 2016 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} 2017 synth, err := chg.Perform(s.as) 2018 c.Assert(err, IsNil) 2019 c.Assert(synth, HasLen, 0) 2020 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 2021 {C: `lstat "/target"`, R: testutil.FileInfoFile}, 2022 {C: `lstat "/source"`, E: syscall.ENOENT}, 2023 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, 2024 {C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, R: 4}, 2025 {C: `fchown 4 0 0`}, 2026 {C: `close 4`}, 2027 {C: `close 3`}, 2028 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, 2029 {C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, 2030 {C: `fstat 4 <ptr>`, R: syscall.Stat_t{}}, 2031 {C: `close 3`}, 2032 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, 2033 {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5}, 2034 {C: `fstat 5 <ptr>`, R: syscall.Stat_t{}}, 2035 {C: `close 3`}, 2036 {C: `mount "/proc/self/fd/4" "/proc/self/fd/5" "" MS_BIND ""`}, 2037 {C: `close 5`}, 2038 {C: `close 4`}, 2039 }) 2040 } 2041 2042 // Change.Perform wants to create a file bind mount but the mount source isn't there and cannot be created. 2043 func (s *changeSuite) TestPerformFileBindMountWithoutMountSourceWithErrors(c *C) { 2044 defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. 2045 s.sys.InsertFault(`lstat "/source"`, syscall.ENOENT) 2046 s.sys.InsertFault(`openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, errTesting) 2047 s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoFile) 2048 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} 2049 synth, err := chg.Perform(s.as) 2050 c.Assert(err, ErrorMatches, `cannot open file "/source": testing`) 2051 c.Assert(synth, HasLen, 0) 2052 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 2053 {C: `lstat "/target"`, R: testutil.FileInfoFile}, 2054 {C: `lstat "/source"`, E: syscall.ENOENT}, 2055 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, 2056 {C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, E: errTesting}, 2057 {C: `close 3`}, 2058 }) 2059 } 2060 2061 // Change.Perform wants to bind mount a file but the mount point isn't there and the parent is read-only. 2062 func (s *changeSuite) TestPerformFileBindMountWithoutMountPointAndReadOnlyBase(c *C) { 2063 defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. 2064 s.sys.InsertFault(`lstat "/rofs/target"`, syscall.ENOENT) 2065 s.sys.InsertFault(`mkdirat 3 "rofs" 0755`, syscall.EEXIST) 2066 s.sys.InsertFault(`openat 4 "target" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, syscall.EROFS, nil) // works on 2nd try 2067 s.sys.InsertSysLstatResult(`lstat "/rofs" <ptr>`, syscall.Stat_t{Uid: 0, Gid: 0, Mode: 0755}) 2068 s.sys.InsertReadDirResult(`readdir "/rofs"`, nil) // pretend /rofs is empty. 2069 s.sys.InsertFault(`lstat "/tmp/.snap/rofs"`, syscall.ENOENT) 2070 s.sys.InsertOsLstatResult(`lstat "/rofs"`, testutil.FileInfoDir) 2071 s.sys.InsertOsLstatResult(`lstat "/source"`, testutil.FileInfoFile) 2072 s.sys.InsertFstatResult(`fstat 4 <ptr>`, syscall.Stat_t{}) 2073 s.sys.InsertFstatResult(`fstat 6 <ptr>`, syscall.Stat_t{}) 2074 s.sys.InsertFstatResult(`fstat 7 <ptr>`, syscall.Stat_t{}) 2075 s.sys.InsertFstatResult(`fstat 6 <ptr>`, syscall.Stat_t{}) 2076 s.sys.InsertFstatfsResult(`fstatfs 6 <ptr>`, syscall.Statfs_t{}) 2077 2078 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/rofs/target", Options: []string{"bind", "x-snapd.kind=file"}}} 2079 synth, err := chg.Perform(s.as) 2080 c.Assert(err, IsNil) 2081 c.Assert(synth, DeepEquals, []*update.Change{ 2082 {Action: update.Mount, Entry: osutil.MountEntry{ 2083 Name: "tmpfs", Dir: "/rofs", Type: "tmpfs", 2084 Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/rofs/target", "mode=0755", "uid=0", "gid=0"}}, 2085 }, 2086 }) 2087 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 2088 // sniff mount target 2089 {C: `lstat "/rofs/target"`, E: syscall.ENOENT}, 2090 2091 // /rofs/target is missing, create it 2092 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, 2093 {C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST}, 2094 {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, 2095 {C: `close 3`}, 2096 {C: `openat 4 "target" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, E: syscall.EROFS}, 2097 {C: `close 4`}, 2098 2099 // error, read only filesystem, create a mimic 2100 {C: `lstat "/rofs" <ptr>`, R: syscall.Stat_t{Mode: 0755}}, 2101 {C: `readdir "/rofs"`, R: []os.FileInfo(nil)}, 2102 {C: `lstat "/tmp/.snap/rofs"`, E: syscall.ENOENT}, 2103 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, 2104 {C: `mkdirat 3 "tmp" 0755`}, 2105 {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, 2106 {C: `fchown 4 0 0`}, 2107 {C: `mkdirat 4 ".snap" 0755`}, 2108 {C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 5}, 2109 {C: `fchown 5 0 0`}, 2110 {C: `close 4`}, 2111 {C: `close 3`}, 2112 {C: `mkdirat 5 "rofs" 0755`}, 2113 {C: `openat 5 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, 2114 {C: `fchown 3 0 0`}, 2115 {C: `close 3`}, 2116 {C: `close 5`}, 2117 {C: `lstat "/rofs"`, R: testutil.FileInfoDir}, 2118 2119 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, 2120 {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, 2121 {C: `fstat 4 <ptr>`, R: syscall.Stat_t{}}, 2122 {C: `close 3`}, 2123 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, 2124 {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5}, 2125 {C: `openat 5 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 6}, 2126 {C: `openat 6 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 7}, 2127 {C: `fstat 7 <ptr>`, R: syscall.Stat_t{}}, 2128 {C: `close 6`}, 2129 {C: `close 5`}, 2130 {C: `close 3`}, 2131 {C: `mount "/proc/self/fd/4" "/proc/self/fd/7" "" MS_BIND|MS_REC ""`}, 2132 {C: `close 7`}, 2133 {C: `close 4`}, 2134 2135 {C: `lstat "/rofs"`, R: testutil.FileInfoDir}, 2136 {C: `mount "tmpfs" "/rofs" "tmpfs" 0 "mode=0755,uid=0,gid=0"`}, 2137 {C: `mount "none" "/tmp/.snap/rofs" "" MS_REC|MS_PRIVATE ""`}, 2138 {C: `unmount "/tmp/.snap/rofs" UMOUNT_NOFOLLOW|MNT_DETACH`}, 2139 2140 // Perform clean up after the unmount operation. 2141 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, 2142 {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, 2143 {C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5}, 2144 {C: `openat 5 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 6}, 2145 {C: `fstat 6 <ptr>`, R: syscall.Stat_t{}}, 2146 {C: `close 5`}, 2147 {C: `close 4`}, 2148 {C: `close 3`}, 2149 {C: `fstatfs 6 <ptr>`, R: syscall.Statfs_t{}}, 2150 {C: `remove "/tmp/.snap/rofs"`}, 2151 {C: `close 6`}, 2152 2153 // mimic ready, re-try initial mkdir 2154 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, 2155 {C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST}, 2156 {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, 2157 {C: `close 3`}, 2158 {C: `openat 4 "target" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, R: 3}, 2159 {C: `fchown 3 0 0`}, 2160 {C: `close 3`}, 2161 {C: `close 4`}, 2162 2163 // sniff mount source 2164 {C: `lstat "/source"`, R: testutil.FileInfoFile}, 2165 2166 // mount the filesystem 2167 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, 2168 {C: `openat 3 "source" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, 2169 {C: `fstat 4 <ptr>`, R: syscall.Stat_t{}}, 2170 {C: `close 3`}, 2171 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, 2172 {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5}, 2173 {C: `openat 5 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 6}, 2174 {C: `fstat 6 <ptr>`, R: syscall.Stat_t{}}, 2175 {C: `close 5`}, 2176 {C: `close 3`}, 2177 {C: `mount "/proc/self/fd/4" "/proc/self/fd/6" "" MS_BIND ""`}, 2178 {C: `close 6`}, 2179 {C: `close 4`}, 2180 }) 2181 } 2182 2183 // Change.Perform wants to bind mount a file but there's a symlink in mount point. 2184 func (s *changeSuite) TestPerformFileBindMountWithSymlinkInMountPoint(c *C) { 2185 s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoSymlink) 2186 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} 2187 synth, err := chg.Perform(s.as) 2188 c.Assert(err, ErrorMatches, `cannot use "/target" as mount point: not a regular file`) 2189 c.Assert(synth, HasLen, 0) 2190 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 2191 {C: `lstat "/target"`, R: testutil.FileInfoSymlink}, 2192 }) 2193 } 2194 2195 // Change.Perform wants to bind mount a file but there's a directory in mount point. 2196 func (s *changeSuite) TestPerformBindMountFileWithDirectoryInMountPoint(c *C) { 2197 s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir) 2198 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} 2199 synth, err := chg.Perform(s.as) 2200 c.Assert(err, ErrorMatches, `cannot use "/target" as mount point: not a regular file`) 2201 c.Assert(synth, HasLen, 0) 2202 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 2203 {C: `lstat "/target"`, R: testutil.FileInfoDir}, 2204 }) 2205 } 2206 2207 // Change.Perform wants to bind mount a file but there's a symlink in source. 2208 func (s *changeSuite) TestPerformFileBindMountWithSymlinkInMountSource(c *C) { 2209 s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoFile) 2210 s.sys.InsertOsLstatResult(`lstat "/source"`, testutil.FileInfoSymlink) 2211 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} 2212 synth, err := chg.Perform(s.as) 2213 c.Assert(err, ErrorMatches, `cannot use "/source" as bind-mount source: not a regular file`) 2214 c.Assert(synth, HasLen, 0) 2215 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 2216 {C: `lstat "/target"`, R: testutil.FileInfoFile}, 2217 {C: `lstat "/source"`, R: testutil.FileInfoSymlink}, 2218 }) 2219 } 2220 2221 // Change.Perform wants to bind mount a file but there's a directory in source. 2222 func (s *changeSuite) TestPerformFileBindMountWithDirectoryInMountSource(c *C) { 2223 s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoFile) 2224 s.sys.InsertOsLstatResult(`lstat "/source"`, testutil.FileInfoDir) 2225 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} 2226 synth, err := chg.Perform(s.as) 2227 c.Assert(err, ErrorMatches, `cannot use "/source" as bind-mount source: not a regular file`) 2228 c.Assert(synth, HasLen, 0) 2229 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 2230 {C: `lstat "/target"`, R: testutil.FileInfoFile}, 2231 {C: `lstat "/source"`, R: testutil.FileInfoDir}, 2232 }) 2233 } 2234 2235 // Change.Perform wants to unmount a file bind mount made on empty squashfs placeholder. 2236 func (s *changeSuite) TestPerformFileBindUnmountOnSquashfs(c *C) { 2237 s.sys.InsertFstatfsResult(`fstatfs 4 <ptr>`, syscall.Statfs_t{Type: update.SquashfsMagic}) 2238 s.sys.InsertFstatResult(`fstat 4 <ptr>`, syscall.Stat_t{Size: 0}) 2239 chg := &update.Change{Action: update.Unmount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} 2240 synth, err := chg.Perform(s.as) 2241 c.Assert(err, IsNil) 2242 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 2243 {C: `unmount "/target" UMOUNT_NOFOLLOW`}, 2244 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, 2245 {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, 2246 {C: `fstat 4 <ptr>`, R: syscall.Stat_t{}}, 2247 {C: `close 3`}, 2248 {C: `fstatfs 4 <ptr>`, R: syscall.Statfs_t{Type: update.SquashfsMagic}}, 2249 {C: `close 4`}, 2250 }) 2251 c.Assert(synth, HasLen, 0) 2252 } 2253 2254 // Change.Perform wants to unmount a file bind mount made on non-empty ext4 placeholder. 2255 func (s *changeSuite) TestPerformFileBindUnmountOnExt4NonEmpty(c *C) { 2256 s.sys.InsertFstatfsResult(`fstatfs 4 <ptr>`, syscall.Statfs_t{Type: update.Ext4Magic}) 2257 s.sys.InsertFstatResult(`fstat 4 <ptr>`, syscall.Stat_t{Size: 1}) 2258 chg := &update.Change{Action: update.Unmount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} 2259 synth, err := chg.Perform(s.as) 2260 c.Assert(err, IsNil) 2261 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 2262 {C: `unmount "/target" UMOUNT_NOFOLLOW`}, 2263 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, 2264 {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, 2265 {C: `fstat 4 <ptr>`, R: syscall.Stat_t{Size: 1}}, 2266 {C: `close 3`}, 2267 {C: `fstatfs 4 <ptr>`, R: syscall.Statfs_t{Type: update.Ext4Magic}}, 2268 {C: `fstat 4 <ptr>`, R: syscall.Stat_t{Size: 1}}, 2269 {C: `close 4`}, 2270 }) 2271 c.Assert(synth, HasLen, 0) 2272 } 2273 2274 // Change.Perform wants to unmount a file bind mount made on empty tmpfs placeholder. 2275 func (s *changeSuite) TestPerformFileBindUnmountOnTmpfsEmpty(c *C) { 2276 s.sys.InsertFstatfsResult(`fstatfs 4 <ptr>`, syscall.Statfs_t{Type: update.TmpfsMagic}) 2277 s.sys.InsertFstatResult(`fstat 4 <ptr>`, syscall.Stat_t{Size: 0}) 2278 chg := &update.Change{Action: update.Unmount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} 2279 synth, err := chg.Perform(s.as) 2280 c.Assert(err, IsNil) 2281 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 2282 {C: `unmount "/target" UMOUNT_NOFOLLOW`}, 2283 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, 2284 {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, 2285 {C: `fstat 4 <ptr>`, R: syscall.Stat_t{Size: 0}}, 2286 {C: `close 3`}, 2287 {C: `fstatfs 4 <ptr>`, R: syscall.Statfs_t{Type: update.TmpfsMagic}}, 2288 {C: `fstat 4 <ptr>`, R: syscall.Stat_t{Size: 0}}, 2289 {C: `remove "/target"`}, 2290 {C: `close 4`}, 2291 }) 2292 c.Assert(synth, HasLen, 0) 2293 } 2294 2295 // Change.Perform wants to unmount a file bind mount made on empty tmpfs placeholder but it is busy!. 2296 func (s *changeSuite) TestPerformFileBindUnmountOnTmpfsEmptyButBusy(c *C) { 2297 s.sys.InsertFstatfsResult(`fstatfs 4 <ptr>`, syscall.Statfs_t{Type: update.TmpfsMagic}) 2298 s.sys.InsertFstatResult(`fstat 4 <ptr>`, syscall.Stat_t{Size: 0}) 2299 s.sys.InsertFault(`remove "/target"`, syscall.EBUSY) 2300 chg := &update.Change{Action: update.Unmount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} 2301 synth, err := chg.Perform(s.as) 2302 c.Assert(err, ErrorMatches, "device or resource busy") 2303 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 2304 {C: `unmount "/target" UMOUNT_NOFOLLOW`}, 2305 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, 2306 {C: `openat 3 "target" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, 2307 {C: `fstat 4 <ptr>`, R: syscall.Stat_t{Size: 0}}, 2308 {C: `close 3`}, 2309 {C: `fstatfs 4 <ptr>`, R: syscall.Statfs_t{Type: update.TmpfsMagic}}, 2310 {C: `fstat 4 <ptr>`, R: syscall.Stat_t{Size: 0}}, 2311 {C: `remove "/target"`, E: syscall.EBUSY}, 2312 {C: `close 4`}, 2313 }) 2314 c.Assert(synth, HasLen, 0) 2315 } 2316 2317 // Change.Perform wants to unmount a file bind mount but it fails. 2318 func (s *changeSuite) TestPerformFileBindUnmountError(c *C) { 2319 s.sys.InsertFault(`unmount "/target" UMOUNT_NOFOLLOW`, errTesting) 2320 chg := &update.Change{Action: update.Unmount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} 2321 synth, err := chg.Perform(s.as) 2322 c.Assert(err, Equals, errTesting) 2323 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 2324 {C: `unmount "/target" UMOUNT_NOFOLLOW`, E: errTesting}, 2325 }) 2326 c.Assert(synth, HasLen, 0) 2327 } 2328 2329 // ############################################################# 2330 // Topic: handling mounts with the x-snapd.ignore-missing option 2331 // ############################################################# 2332 2333 func (s *changeSuite) TestPerformMountWithIgnoredMissingMountSource(c *C) { 2334 s.sys.InsertFault(`lstat "/source"`, syscall.ENOENT) 2335 s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir) 2336 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.ignore-missing"}}} 2337 synth, err := chg.Perform(s.as) 2338 c.Assert(err, Equals, update.ErrIgnoredMissingMount) 2339 c.Assert(synth, HasLen, 0) 2340 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 2341 {C: `lstat "/target"`, R: testutil.FileInfoDir}, 2342 {C: `lstat "/source"`, E: syscall.ENOENT}, 2343 }) 2344 } 2345 2346 func (s *changeSuite) TestPerformMountWithIgnoredMissingMountPoint(c *C) { 2347 s.sys.InsertFault(`lstat "/target"`, syscall.ENOENT) 2348 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.ignore-missing"}}} 2349 synth, err := chg.Perform(s.as) 2350 c.Assert(err, Equals, update.ErrIgnoredMissingMount) 2351 c.Assert(synth, HasLen, 0) 2352 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 2353 {C: `lstat "/target"`, E: syscall.ENOENT}, 2354 }) 2355 } 2356 2357 // ######################## 2358 // Topic: creating symlinks 2359 // ######################## 2360 2361 // Change.Perform wants to create a symlink but name cannot be stat'ed. 2362 func (s *changeSuite) TestPerformCreateSymlinkNameLstatError(c *C) { 2363 s.sys.InsertFault(`lstat "/name"`, errTesting) 2364 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "unused", Dir: "/name", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=/oldname"}}} 2365 synth, err := chg.Perform(s.as) 2366 c.Assert(err, ErrorMatches, `cannot inspect "/name": testing`) 2367 c.Assert(synth, HasLen, 0) 2368 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 2369 {C: `lstat "/name"`, E: errTesting}, 2370 }) 2371 } 2372 2373 // Change.Perform wants to create a symlink. 2374 func (s *changeSuite) TestPerformCreateSymlink(c *C) { 2375 defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. 2376 s.sys.InsertFault(`lstat "/name"`, syscall.ENOENT) 2377 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "unused", Dir: "/name", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=/oldname"}}} 2378 synth, err := chg.Perform(s.as) 2379 c.Assert(err, IsNil) 2380 c.Assert(synth, HasLen, 0) 2381 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 2382 {C: `lstat "/name"`, E: syscall.ENOENT}, 2383 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, 2384 {C: `symlinkat "/oldname" 3 "name"`}, 2385 {C: `close 3`}, 2386 }) 2387 } 2388 2389 // Change.Perform wants to create a symlink but it fails. 2390 func (s *changeSuite) TestPerformCreateSymlinkWithError(c *C) { 2391 defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. 2392 s.sys.InsertFault(`lstat "/name"`, syscall.ENOENT) 2393 s.sys.InsertFault(`symlinkat "/oldname" 3 "name"`, errTesting) 2394 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "unused", Dir: "/name", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=/oldname"}}} 2395 synth, err := chg.Perform(s.as) 2396 c.Assert(err, ErrorMatches, `cannot create symlink "/name": testing`) 2397 c.Assert(synth, HasLen, 0) 2398 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 2399 {C: `lstat "/name"`, E: syscall.ENOENT}, 2400 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, 2401 {C: `symlinkat "/oldname" 3 "name"`, E: errTesting}, 2402 {C: `close 3`}, 2403 }) 2404 } 2405 2406 // Change.Perform wants to create a symlink but the target is empty. 2407 func (s *changeSuite) TestPerformCreateSymlinkWithNoTargetError(c *C) { 2408 s.sys.InsertFault(`lstat "/name"`, syscall.ENOENT) 2409 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "unused", Dir: "/name", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink="}}} 2410 synth, err := chg.Perform(s.as) 2411 c.Assert(err, ErrorMatches, `cannot create symlink with empty target: "/name"`) 2412 c.Assert(synth, HasLen, 0) 2413 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 2414 {C: `lstat "/name"`, E: syscall.ENOENT}, 2415 }) 2416 } 2417 2418 // Change.Perform wants to create a symlink but the base directory isn't there. 2419 func (s *changeSuite) TestPerformCreateSymlinkWithoutBaseDir(c *C) { 2420 defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. 2421 s.sys.InsertFault(`lstat "/base/name"`, syscall.ENOENT) 2422 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "unused", Dir: "/base/name", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=/oldname"}}} 2423 synth, err := chg.Perform(s.as) 2424 c.Assert(err, IsNil) 2425 c.Assert(synth, HasLen, 0) 2426 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 2427 {C: `lstat "/base/name"`, E: syscall.ENOENT}, 2428 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, 2429 {C: `mkdirat 3 "base" 0755`}, 2430 {C: `openat 3 "base" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, 2431 {C: `fchown 4 0 0`}, 2432 {C: `close 3`}, 2433 {C: `symlinkat "/oldname" 4 "name"`}, 2434 {C: `close 4`}, 2435 }) 2436 } 2437 2438 // Change.Perform wants to create a symlink but the base directory isn't there and cannot be created. 2439 func (s *changeSuite) TestPerformCreateSymlinkWithoutBaseDirWithErrors(c *C) { 2440 defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. 2441 s.sys.InsertFault(`lstat "/base/name"`, syscall.ENOENT) 2442 s.sys.InsertFault(`mkdirat 3 "base" 0755`, errTesting) 2443 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "unused", Dir: "/base/name", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=/oldname"}}} 2444 synth, err := chg.Perform(s.as) 2445 c.Assert(err, ErrorMatches, `cannot create directory "/base": testing`) 2446 c.Assert(synth, HasLen, 0) 2447 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 2448 {C: `lstat "/base/name"`, E: syscall.ENOENT}, 2449 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, 2450 {C: `mkdirat 3 "base" 0755`, E: errTesting}, 2451 {C: `close 3`}, 2452 }) 2453 } 2454 2455 // Change.Perform wants to create a symlink but the base directory isn't there and the parent is read-only. 2456 func (s *changeSuite) TestPerformCreateSymlinkWithoutBaseDirAndReadOnlyBase(c *C) { 2457 defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. 2458 s.sys.InsertFault(`lstat "/rofs/name"`, syscall.ENOENT) 2459 s.sys.InsertFault(`mkdirat 3 "rofs" 0755`, syscall.EEXIST) 2460 s.sys.InsertFault(`symlinkat "/oldname" 4 "name"`, syscall.EROFS, nil) // works on 2nd try 2461 s.sys.InsertSysLstatResult(`lstat "/rofs" <ptr>`, syscall.Stat_t{Uid: 0, Gid: 0, Mode: 0755}) 2462 s.sys.InsertReadDirResult(`readdir "/rofs"`, nil) // pretend /rofs is empty. 2463 s.sys.InsertFault(`lstat "/tmp/.snap/rofs"`, syscall.ENOENT) 2464 s.sys.InsertOsLstatResult(`lstat "/rofs"`, testutil.FileInfoDir) 2465 s.sys.InsertFstatResult(`fstat 4 <ptr>`, syscall.Stat_t{}) 2466 s.sys.InsertFstatResult(`fstat 7 <ptr>`, syscall.Stat_t{}) 2467 s.sys.InsertFstatResult(`fstat 6 <ptr>`, syscall.Stat_t{}) 2468 s.sys.InsertFstatfsResult(`fstatfs 6 <ptr>`, syscall.Statfs_t{}) 2469 2470 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "unused", Dir: "/rofs/name", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=/oldname"}}} 2471 synth, err := chg.Perform(s.as) 2472 c.Assert(err, IsNil) 2473 c.Assert(synth, DeepEquals, []*update.Change{ 2474 {Action: update.Mount, Entry: osutil.MountEntry{ 2475 Name: "tmpfs", Dir: "/rofs", Type: "tmpfs", 2476 Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/rofs/name", "mode=0755", "uid=0", "gid=0"}}, 2477 }, 2478 }) 2479 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 2480 // sniff symlink name 2481 {C: `lstat "/rofs/name"`, E: syscall.ENOENT}, 2482 2483 // create base name (/rofs) 2484 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, 2485 {C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST}, 2486 {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, 2487 {C: `close 3`}, 2488 // create symlink 2489 {C: `symlinkat "/oldname" 4 "name"`, E: syscall.EROFS}, 2490 {C: `close 4`}, 2491 2492 // error, read only filesystem, create a mimic 2493 {C: `lstat "/rofs" <ptr>`, R: syscall.Stat_t{Mode: 0755}}, 2494 {C: `readdir "/rofs"`, R: []os.FileInfo(nil)}, 2495 {C: `lstat "/tmp/.snap/rofs"`, E: syscall.ENOENT}, 2496 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, 2497 {C: `mkdirat 3 "tmp" 0755`}, 2498 {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, 2499 {C: `fchown 4 0 0`}, 2500 {C: `mkdirat 4 ".snap" 0755`}, 2501 {C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 5}, 2502 {C: `fchown 5 0 0`}, 2503 {C: `close 4`}, 2504 {C: `close 3`}, 2505 {C: `mkdirat 5 "rofs" 0755`}, 2506 {C: `openat 5 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, 2507 {C: `fchown 3 0 0`}, 2508 {C: `close 3`}, 2509 {C: `close 5`}, 2510 {C: `lstat "/rofs"`, R: testutil.FileInfoDir}, 2511 2512 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, 2513 {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, 2514 {C: `fstat 4 <ptr>`, R: syscall.Stat_t{}}, 2515 {C: `close 3`}, 2516 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, 2517 {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5}, 2518 {C: `openat 5 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 6}, 2519 {C: `openat 6 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 7}, 2520 {C: `fstat 7 <ptr>`, R: syscall.Stat_t{}}, 2521 {C: `close 6`}, 2522 {C: `close 5`}, 2523 {C: `close 3`}, 2524 {C: `mount "/proc/self/fd/4" "/proc/self/fd/7" "" MS_BIND|MS_REC ""`}, 2525 {C: `close 7`}, 2526 {C: `close 4`}, 2527 2528 {C: `lstat "/rofs"`, R: testutil.FileInfoDir}, 2529 {C: `mount "tmpfs" "/rofs" "tmpfs" 0 "mode=0755,uid=0,gid=0"`}, 2530 {C: `mount "none" "/tmp/.snap/rofs" "" MS_REC|MS_PRIVATE ""`}, 2531 {C: `unmount "/tmp/.snap/rofs" UMOUNT_NOFOLLOW|MNT_DETACH`}, 2532 2533 // Perform clean up after the unmount operation. 2534 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, 2535 {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, 2536 {C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5}, 2537 {C: `openat 5 "rofs" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 6}, 2538 {C: `fstat 6 <ptr>`, R: syscall.Stat_t{}}, 2539 {C: `close 5`}, 2540 {C: `close 4`}, 2541 {C: `close 3`}, 2542 {C: `fstatfs 6 <ptr>`, R: syscall.Statfs_t{}}, 2543 {C: `remove "/tmp/.snap/rofs"`}, 2544 {C: `close 6`}, 2545 2546 // mimic ready, re-try initial base mkdir 2547 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, 2548 {C: `mkdirat 3 "rofs" 0755`, E: syscall.EEXIST}, 2549 {C: `openat 3 "rofs" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, 2550 {C: `close 3`}, 2551 // create symlink 2552 {C: `symlinkat "/oldname" 4 "name"`}, 2553 {C: `close 4`}, 2554 }) 2555 } 2556 2557 // Change.Perform wants to create a symlink but there's a file in the way. 2558 func (s *changeSuite) TestPerformCreateSymlinkWithFileInTheWay(c *C) { 2559 s.sys.InsertOsLstatResult(`lstat "/name"`, testutil.FileInfoFile) 2560 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "unused", Dir: "/name", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=/oldname"}}} 2561 synth, err := chg.Perform(s.as) 2562 c.Assert(err, ErrorMatches, `cannot create symlink in "/name": existing file in the way`) 2563 c.Assert(synth, HasLen, 0) 2564 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 2565 {C: `lstat "/name"`, R: testutil.FileInfoFile}, 2566 }) 2567 } 2568 2569 // Change.Perform wants to create a symlink but a correct symlink is already present. 2570 func (s *changeSuite) TestPerformCreateSymlinkWithGoodSymlinkPresent(c *C) { 2571 defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. 2572 s.sys.InsertOsLstatResult(`lstat "/name"`, testutil.FileInfoSymlink) 2573 s.sys.InsertFault(`symlinkat "/oldname" 3 "name"`, syscall.EEXIST) 2574 s.sys.InsertFstatResult(`fstat 4 <ptr>`, syscall.Stat_t{Mode: syscall.S_IFLNK}) 2575 s.sys.InsertReadlinkatResult(`readlinkat 4 "" <ptr>`, "/oldname") 2576 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "unused", Dir: "/name", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=/oldname"}}} 2577 synth, err := chg.Perform(s.as) 2578 c.Assert(err, IsNil) 2579 c.Assert(synth, HasLen, 0) 2580 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 2581 {C: `lstat "/name"`, R: testutil.FileInfoSymlink}, 2582 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, 2583 {C: `symlinkat "/oldname" 3 "name"`, E: syscall.EEXIST}, 2584 {C: `openat 3 "name" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, 2585 {C: `fstat 4 <ptr>`, R: syscall.Stat_t{Mode: syscall.S_IFLNK}}, 2586 {C: `readlinkat 4 "" <ptr>`, R: "/oldname"}, 2587 {C: `close 4`}, 2588 {C: `close 3`}, 2589 }) 2590 } 2591 2592 // Change.Perform wants to create a symlink but a incorrect symlink is already present. 2593 func (s *changeSuite) TestPerformCreateSymlinkWithBadSymlinkPresent(c *C) { 2594 defer s.as.MockUnrestrictedPaths("/")() // Treat test path as unrestricted. 2595 s.sys.InsertOsLstatResult(`lstat "/name"`, testutil.FileInfoSymlink) 2596 s.sys.InsertFault(`symlinkat "/oldname" 3 "name"`, syscall.EEXIST) 2597 s.sys.InsertFstatResult(`fstat 4 <ptr>`, syscall.Stat_t{Mode: syscall.S_IFLNK}) 2598 s.sys.InsertReadlinkatResult(`readlinkat 4 "" <ptr>`, "/evil") 2599 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "unused", Dir: "/name", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=/oldname"}}} 2600 synth, err := chg.Perform(s.as) 2601 c.Assert(err, ErrorMatches, `cannot create symbolic link "/name": existing symbolic link in the way`) 2602 c.Assert(synth, HasLen, 0) 2603 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 2604 {C: `lstat "/name"`, R: testutil.FileInfoSymlink}, 2605 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, 2606 {C: `symlinkat "/oldname" 3 "name"`, E: syscall.EEXIST}, 2607 {C: `openat 3 "name" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, 2608 {C: `fstat 4 <ptr>`, R: syscall.Stat_t{Mode: syscall.S_IFLNK}}, 2609 {C: `readlinkat 4 "" <ptr>`, R: "/evil"}, 2610 {C: `close 4`}, 2611 {C: `close 3`}, 2612 }) 2613 } 2614 2615 func (s *changeSuite) TestPerformRemoveSymlink(c *C) { 2616 chg := &update.Change{Action: update.Unmount, Entry: osutil.MountEntry{Name: "unused", Dir: "/name", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=/oldname"}}} 2617 synth, err := chg.Perform(s.as) 2618 c.Assert(err, IsNil) 2619 c.Assert(synth, HasLen, 0) 2620 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 2621 {C: `remove "/name"`}, 2622 }) 2623 } 2624 2625 // Change.Perform wants to create a symlink in /etc and the write is made private. 2626 func (s *changeSuite) TestPerformCreateSymlinkWithAvoidedTrespassing(c *C) { 2627 defer s.as.MockUnrestrictedPaths("/tmp/")() // Allow writing to /tmp 2628 2629 s.sys.InsertFault(`lstat "/etc/demo.conf"`, syscall.ENOENT) 2630 s.sys.InsertFstatfsResult(`fstatfs 3 <ptr>`, syscall.Statfs_t{Type: update.SquashfsMagic}) 2631 s.sys.InsertFstatResult(`fstat 3 <ptr>`, syscall.Stat_t{}) 2632 s.sys.InsertFault(`mkdirat 3 "etc" 0755`, syscall.EEXIST) 2633 s.sys.InsertFstatfsResult(`fstatfs 4 <ptr>`, 2634 // On 1st call ext4, on 2nd call tmpfs 2635 syscall.Statfs_t{Type: update.Ext4Magic}, 2636 syscall.Statfs_t{Type: update.TmpfsMagic}) 2637 s.sys.InsertFstatResult(`fstat 4 <ptr>`, syscall.Stat_t{}) 2638 s.sys.InsertSysLstatResult(`lstat "/etc" <ptr>`, syscall.Stat_t{Mode: 0755}) 2639 otherConf := testutil.FakeFileInfo("other.conf", 0755) 2640 s.sys.InsertReadDirResult(`readdir "/etc"`, []os.FileInfo{otherConf}) 2641 s.sys.InsertFault(`lstat "/tmp/.snap/etc"`, syscall.ENOENT) 2642 s.sys.InsertFault(`lstat "/tmp/.snap/etc/other.conf"`, syscall.ENOENT) 2643 s.sys.InsertOsLstatResult(`lstat "/etc"`, testutil.FileInfoDir) 2644 s.sys.InsertOsLstatResult(`lstat "/etc/other.conf"`, otherConf) 2645 s.sys.InsertFault(`mkdirat 3 "tmp" 0755`, syscall.EEXIST) 2646 s.sys.InsertFstatResult(`fstat 5 <ptr>`, syscall.Stat_t{Mode: syscall.S_IFREG}) 2647 s.sys.InsertFstatResult(`fstat 4 <ptr>`, syscall.Stat_t{Mode: syscall.S_IFDIR}) 2648 s.sys.InsertFstatResult(`fstat 7 <ptr>`, syscall.Stat_t{Mode: syscall.S_IFDIR}) 2649 s.sys.InsertFstatResult(`fstat 6 <ptr>`, syscall.Stat_t{}) 2650 s.sys.InsertFstatfsResult(`fstatfs 6 <ptr>`, syscall.Statfs_t{}) 2651 2652 // This is the change we want to perform: 2653 // put a layout symlink at /etc/demo.conf -> /oldname 2654 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "unused", Dir: "/etc/demo.conf", Options: []string{"x-snapd.kind=symlink", "x-snapd.symlink=/oldname"}}} 2655 synth, err := chg.Perform(s.as) 2656 c.Check(err, IsNil) 2657 c.Check(synth, HasLen, 2) 2658 // We have created some synthetic change (made /etc a new tmpfs and re-populate it) 2659 c.Assert(synth[0], DeepEquals, &update.Change{ 2660 Entry: osutil.MountEntry{Name: "tmpfs", Dir: "/etc", Type: "tmpfs", Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/etc/demo.conf", "mode=0755", "uid=0", "gid=0"}}, 2661 Action: "mount"}) 2662 c.Assert(synth[1], DeepEquals, &update.Change{ 2663 Entry: osutil.MountEntry{Name: "/etc/other.conf", Dir: "/etc/other.conf", Options: []string{"bind", "x-snapd.kind=file", "x-snapd.synthetic", "x-snapd.needed-by=/etc/demo.conf"}}, 2664 Action: "mount"}) 2665 2666 // And this is exactly how we made that happen: 2667 c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ 2668 // Attempt to construct a symlink /etc/demo.conf -> /oldname. 2669 // This stops as soon as we notice that /etc is an ext4 filesystem. 2670 // To avoid writing to it directly we need a writable mimic. 2671 {C: `lstat "/etc/demo.conf"`, E: syscall.ENOENT}, 2672 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, 2673 {C: `fstatfs 3 <ptr>`, R: syscall.Statfs_t{Type: update.SquashfsMagic}}, 2674 {C: `fstat 3 <ptr>`, R: syscall.Stat_t{}}, 2675 {C: `mkdirat 3 "etc" 0755`, E: syscall.EEXIST}, 2676 {C: `openat 3 "etc" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, 2677 {C: `close 3`}, 2678 {C: `fstatfs 4 <ptr>`, R: syscall.Statfs_t{Type: update.Ext4Magic}}, 2679 {C: `fstat 4 <ptr>`, R: syscall.Stat_t{Mode: 0x4000}}, 2680 {C: `close 4`}, 2681 2682 // Create a writable mimic over /etc, scan the contents of /etc first. 2683 // For convenience we pretend that /etc is empty. The mimic 2684 // replicates /etc in /tmp/.snap/etc for subsequent re-construction. 2685 {C: `lstat "/etc" <ptr>`, R: syscall.Stat_t{Mode: 0755}}, 2686 {C: `readdir "/etc"`, R: []os.FileInfo{otherConf}}, 2687 {C: `lstat "/tmp/.snap/etc"`, E: syscall.ENOENT}, 2688 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, 2689 {C: `mkdirat 3 "tmp" 0755`, E: syscall.EEXIST}, 2690 {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, 2691 {C: `mkdirat 4 ".snap" 0755`}, 2692 {C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 5}, 2693 {C: `fchown 5 0 0`}, 2694 {C: `close 4`}, 2695 {C: `close 3`}, 2696 {C: `mkdirat 5 "etc" 0755`}, 2697 {C: `openat 5 "etc" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, 2698 {C: `fchown 3 0 0`}, 2699 {C: `close 3`}, 2700 {C: `close 5`}, 2701 2702 // Prepare a secure bind mount operation /etc -> /tmp/.snap/etc 2703 {C: `lstat "/etc"`, R: testutil.FileInfoDir}, 2704 2705 // Open an O_PATH descriptor to /etc. We need this as a source of a 2706 // secure bind mount operation. We also ensure that the descriptor 2707 // refers to a directory. 2708 // NOTE: we keep fd 4 open for subsequent use. 2709 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, 2710 {C: `openat 3 "etc" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 4}, 2711 {C: `fstat 4 <ptr>`, R: syscall.Stat_t{Mode: syscall.S_IFDIR}}, 2712 {C: `close 3`}, 2713 2714 // Open an O_PATH descriptor to /tmp/.snap/etc. We need this as a 2715 // target of a secure bind mount operation. We also ensure that the 2716 // descriptor refers to a directory. 2717 // NOTE: we keep fd 7 open for subsequent use. 2718 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, 2719 {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5}, 2720 {C: `openat 5 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 6}, 2721 {C: `openat 6 "etc" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 7}, 2722 {C: `fstat 7 <ptr>`, R: syscall.Stat_t{Mode: syscall.S_IFDIR}}, 2723 {C: `close 6`}, 2724 {C: `close 5`}, 2725 {C: `close 3`}, 2726 2727 // Perform the secure bind mount operation /etc -> /tmp/.snap/etc 2728 // and release the two associated file descriptors. 2729 {C: `mount "/proc/self/fd/4" "/proc/self/fd/7" "" MS_BIND|MS_REC ""`}, 2730 {C: `close 7`}, 2731 {C: `close 4`}, 2732 2733 // Mount a tmpfs over /etc, re-constructing the original mode and 2734 // ownership. Bind mount each original file over and detach the copy 2735 // of /etc we had in /tmp/.snap/etc. 2736 2737 {C: `lstat "/etc"`, R: testutil.FileInfoDir}, 2738 {C: `mount "tmpfs" "/etc" "tmpfs" 0 "mode=0755,uid=0,gid=0"`}, 2739 // Here we restore the contents of /etc: here it's just one file - other.conf 2740 {C: `lstat "/etc/other.conf"`, R: otherConf}, 2741 {C: `lstat "/tmp/.snap/etc/other.conf"`, E: syscall.ENOENT}, 2742 2743 // Create /tmp/.snap/etc/other.conf as an empty file. 2744 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, 2745 {C: `mkdirat 3 "tmp" 0755`, E: syscall.EEXIST}, 2746 {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, 2747 {C: `mkdirat 4 ".snap" 0755`}, 2748 {C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 5}, 2749 {C: `fchown 5 0 0`}, 2750 {C: `mkdirat 5 "etc" 0755`}, 2751 {C: `openat 5 "etc" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 6}, 2752 {C: `fchown 6 0 0`}, 2753 {C: `close 5`}, 2754 {C: `close 4`}, 2755 {C: `close 3`}, 2756 // NOTE: This is without O_DIRECTORY and with O_CREAT|O_EXCL, 2757 // we are creating an empty file for the subsequent bind mount. 2758 {C: `openat 6 "other.conf" O_NOFOLLOW|O_CLOEXEC|O_CREAT|O_EXCL 0755`, R: 3}, 2759 {C: `fchown 3 0 0`}, 2760 {C: `close 3`}, 2761 {C: `close 6`}, 2762 2763 // Open O_PATH to /tmp/.snap/etc/other.conf 2764 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, 2765 {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, 2766 {C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5}, 2767 {C: `openat 5 "etc" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 6}, 2768 {C: `openat 6 "other.conf" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 7}, 2769 {C: `fstat 7 <ptr>`, R: syscall.Stat_t{Mode: syscall.S_IFDIR}}, 2770 {C: `close 6`}, 2771 {C: `close 5`}, 2772 {C: `close 4`}, 2773 {C: `close 3`}, 2774 2775 // Open O_PATH to /etc/other.conf 2776 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, 2777 {C: `openat 3 "etc" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, 2778 {C: `openat 4 "other.conf" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 5}, 2779 {C: `fstat 5 <ptr>`, R: syscall.Stat_t{Mode: syscall.S_IFREG}}, 2780 {C: `close 4`}, 2781 {C: `close 3`}, 2782 2783 // Restore the /etc/other.conf file with a secure bind mount. 2784 {C: `mount "/proc/self/fd/7" "/proc/self/fd/5" "" MS_BIND ""`}, 2785 {C: `close 5`}, 2786 {C: `close 7`}, 2787 2788 // We're done restoring now. 2789 {C: `mount "none" "/tmp/.snap/etc" "" MS_REC|MS_PRIVATE ""`}, 2790 {C: `unmount "/tmp/.snap/etc" UMOUNT_NOFOLLOW|MNT_DETACH`}, 2791 2792 // Perform clean up after the unmount operation. 2793 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, 2794 {C: `openat 3 "tmp" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 4}, 2795 {C: `openat 4 ".snap" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 5}, 2796 {C: `openat 5 "etc" O_NOFOLLOW|O_CLOEXEC|O_PATH 0`, R: 6}, 2797 {C: `fstat 6 <ptr>`, R: syscall.Stat_t{}}, 2798 {C: `close 5`}, 2799 {C: `close 4`}, 2800 {C: `close 3`}, 2801 {C: `fstatfs 6 <ptr>`, R: syscall.Statfs_t{}}, 2802 {C: `remove "/tmp/.snap/etc"`}, 2803 {C: `close 6`}, 2804 2805 // The mimic is now complete and subsequent writes to /etc are private 2806 // to the mount namespace of the process. 2807 2808 {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, 2809 {C: `fstatfs 3 <ptr>`, R: syscall.Statfs_t{Type: update.SquashfsMagic}}, 2810 {C: `fstat 3 <ptr>`, R: syscall.Stat_t{}}, 2811 {C: `mkdirat 3 "etc" 0755`, E: syscall.EEXIST}, 2812 {C: `openat 3 "etc" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, 2813 {C: `close 3`}, 2814 {C: `fstatfs 4 <ptr>`, R: syscall.Statfs_t{Type: update.TmpfsMagic}}, 2815 {C: `fstat 4 <ptr>`, R: syscall.Stat_t{Mode: 0x4000}}, 2816 {C: `symlinkat "/oldname" 4 "demo.conf"`}, 2817 {C: `close 4`}, 2818 }) 2819 } 2820 2821 // ########### 2822 // Topic: misc 2823 // ########### 2824 2825 // Change.Perform handles unknown actions. 2826 func (s *changeSuite) TestPerformUnknownAction(c *C) { 2827 chg := &update.Change{Action: update.Action(42)} 2828 synth, err := chg.Perform(s.as) 2829 c.Assert(err, ErrorMatches, `cannot process mount change: unknown action: .*`) 2830 c.Assert(synth, HasLen, 0) 2831 c.Assert(s.sys.RCalls(), HasLen, 0) 2832 } 2833 2834 // Change.Perform wants to keep a mount entry unchanged. 2835 func (s *changeSuite) TestPerformKeep(c *C) { 2836 chg := &update.Change{Action: update.Keep} 2837 synth, err := chg.Perform(s.as) 2838 c.Assert(err, IsNil) 2839 c.Assert(synth, HasLen, 0) 2840 c.Assert(s.sys.RCalls(), HasLen, 0) 2841 } 2842 2843 // ############################################ 2844 // Topic: change history tracked in Assumptions 2845 // ############################################ 2846 2847 func (s *changeSuite) TestPerformedChangesAreTracked(c *C) { 2848 s.sys.InsertOsLstatResult(`lstat "/target"`, testutil.FileInfoDir) 2849 c.Assert(s.as.PastChanges(), HasLen, 0) 2850 2851 chg := &update.Change{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}} 2852 _, err := chg.Perform(s.as) 2853 c.Assert(err, IsNil) 2854 c.Assert(s.as.PastChanges(), DeepEquals, []*update.Change{ 2855 {Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}}, 2856 }) 2857 2858 s.sys.InsertFstatResult(`fstat 4 <ptr>`, syscall.Stat_t{}) 2859 s.sys.InsertFstatfsResult(`fstatfs 4 <ptr>`, syscall.Statfs_t{}) 2860 chg = &update.Change{Action: update.Unmount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}} 2861 _, err = chg.Perform(s.as) 2862 c.Assert(err, IsNil) 2863 2864 chg = &update.Change{Action: update.Keep, Entry: osutil.MountEntry{Name: "tmpfs", Dir: "/target", Type: "tmpfs"}} 2865 _, err = chg.Perform(s.as) 2866 c.Assert(err, IsNil) 2867 c.Assert(s.as.PastChanges(), DeepEquals, []*update.Change{ 2868 // past changes stack in order. 2869 {Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}}, 2870 {Action: update.Unmount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}}, 2871 {Action: update.Keep, Entry: osutil.MountEntry{Name: "tmpfs", Dir: "/target", Type: "tmpfs"}}, 2872 }) 2873 } 2874 2875 func (s *changeSuite) TestComplexPropagatingChanges(c *C) { 2876 // This problem is more subtle. It is a variant of the regression test 2877 // implemented in tests/regression/lp-1831010. Here, we have four directories: 2878 // 2879 // - $SNAP/a 2880 // - $SNAP/b 2881 // - $SNAP/b/c 2882 // - $SNAP/d 2883 // 2884 // but snapd's mount profile contains only two entries: 2885 // 2886 // 1) recursive-bind $SNAP/a -> $SNAP/b/c (ie, mount --rbind $SNAP/a $SNAP/b/c) 2887 // 2) recursive-bind $SNAP/b -> $SNAP/d (ie, mount --rbind $SNAP/b $SNAP/d) 2888 // 2889 // Both mount operations are performed under a substrate that is MS_SHARED. 2890 // Therefore, due to the rules that decide upon propagation of bind mounts 2891 // the propagation of the new mount entries is also shared. This is 2892 // documented in section 5b of 2893 // https://www.kernel.org/doc/Documentation/filesystems/sharedsubtree.txt. 2894 // 2895 // Interactive experimentation shows that the following three mount points exist 2896 // after this operation, as illustrated by findmnt: 2897 // 2898 // TARGET SOURCE FSTYPE OPTIONS 2899 // ... 2900 // └─/snap/test-snapd-layout/x1 /dev/loop1 squashfs ro,nodev,relatime 2901 // ├─/snap/test-snapd-layout/x1/b/c /dev/loop1[/a] squashfs ro,nodev,relatime 2902 // └─/snap/test-snapd-layout/x1/d /dev/loop1[/b] squashfs ro,nodev,relatime 2903 // └─/snap/test-snapd-layout/x1/d/c /dev/loop1[/a] squashfs ro,nodev,relatime 2904 // 2905 // Note that after the first mount operation only one mount point is created, namely 2906 // $SNAP/a -> $SNAP/b/c. The second recursive bind mount not only creates 2907 // $SNAP/b -> $SNAP/d, but also replicates $SNAP/a -> $SNAP/b/c as 2908 // $SNAP/a -> $SNAP/d/c. 2909 // 2910 // The test will simulate a refresh of the snap from revision x1 to revision 2911 // x2. When this happens the mount profile associated with x1 must be undone 2912 // and the mount profile associated with x2 must be constructed. Because 2913 // ordering matters, let's first consider the order of construction of x1 2914 // itself. Starting from nothing, apply x1 as follows: 2915 x1 := &osutil.MountProfile{ 2916 Entries: []osutil.MountEntry{ 2917 {Name: "/snap/app/x1/a", Dir: "/snap/app/x1/b/c", Type: "none", Options: []string{"rbind", "rw", "x-snapd.origin=layout"}}, 2918 {Name: "/snap/app/x1/b", Dir: "/snap/app/x1/d", Type: "none", Options: []string{"rbind", "rw", "x-snapd.origin=layout"}}, 2919 }, 2920 } 2921 changes := update.NeededChanges(&osutil.MountProfile{}, x1) 2922 c.Assert(changes, DeepEquals, []*update.Change{ 2923 {Action: update.Mount, Entry: osutil.MountEntry{Name: "/snap/app/x1/a", Dir: "/snap/app/x1/b/c", Type: "none", Options: []string{"rbind", "rw", "x-snapd.origin=layout"}}}, 2924 {Action: update.Mount, Entry: osutil.MountEntry{Name: "/snap/app/x1/b", Dir: "/snap/app/x1/d", Type: "none", Options: []string{"rbind", "rw", "x-snapd.origin=layout"}}}, 2925 }) 2926 // We can see that x1 is constructed in alphabetical order, first recursively 2927 // bind mount at $SNAP/a the directory $SNAP/b/c, second recursively bind 2928 // mount at $SNAP/b the directory $SNAP/d. 2929 x2 := &osutil.MountProfile{ 2930 Entries: []osutil.MountEntry{ 2931 {Name: "/snap/app/x2/a", Dir: "/snap/app/x2/b/c", Type: "none", Options: []string{"rbind", "rw", "x-snapd.origin=layout"}}, 2932 {Name: "/snap/app/x2/b", Dir: "/snap/app/x2/d", Type: "none", Options: []string{"rbind", "rw", "x-snapd.origin=layout"}}, 2933 }, 2934 } 2935 // When we are asked to refresh to revision x2, using the same layout, we 2936 // simply undo x1 and then create x2, which apart from the difference in 2937 // revision name, is exactly the same. The undo code, however, does not take 2938 // the replicated mount point under consideration and therefore attempts to 2939 // detach "x1/d", which normally fails with EBUSY. To counter this, the 2940 // unmount operation first switches the mount point to recursive private 2941 // propagation, before actually unmounting it. This ensures that propagation 2942 // doesn't self-conflict, simply because there isn't any left. 2943 changes = update.NeededChanges(x1, x2) 2944 c.Assert(changes, DeepEquals, []*update.Change{ 2945 {Action: update.Unmount, Entry: osutil.MountEntry{Name: "/snap/app/x1/b", Dir: "/snap/app/x1/d", Type: "none", Options: []string{"rbind", "rw", "x-snapd.origin=layout", "x-snapd.detach"}}}, 2946 {Action: update.Unmount, Entry: osutil.MountEntry{Name: "/snap/app/x1/a", Dir: "/snap/app/x1/b/c", Type: "none", Options: []string{"rbind", "rw", "x-snapd.origin=layout", "x-snapd.detach"}}}, 2947 {Action: update.Mount, Entry: osutil.MountEntry{Name: "/snap/app/x2/a", Dir: "/snap/app/x2/b/c", Type: "none", Options: []string{"rbind", "rw", "x-snapd.origin=layout"}}}, 2948 {Action: update.Mount, Entry: osutil.MountEntry{Name: "/snap/app/x2/b", Dir: "/snap/app/x2/d", Type: "none", Options: []string{"rbind", "rw", "x-snapd.origin=layout"}}}, 2949 }) 2950 }