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