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