diff options
| -rw-r--r-- | cmd/snap-update-ns/main.go | 9 | ||||
| -rw-r--r-- | cmd/snap-update-ns/system.go | 7 | ||||
| -rw-r--r-- | cmd/snap-update-ns/system_test.go | 6 | ||||
| -rw-r--r-- | cmd/snap-update-ns/utils.go | 21 | ||||
| -rw-r--r-- | cmd/snap-update-ns/utils_test.go | 21 | ||||
| -rw-r--r-- | tests/main/interfaces-shared-memory-private/task.yaml | 9 | ||||
| -rw-r--r-- | tests/main/snap-mgmt/task.yaml | 12 |
7 files changed, 77 insertions, 8 deletions
diff --git a/cmd/snap-update-ns/main.go b/cmd/snap-update-ns/main.go index 7ff0418598..f3f4ca9867 100644 --- a/cmd/snap-update-ns/main.go +++ b/cmd/snap-update-ns/main.go @@ -22,6 +22,7 @@ package main import ( "fmt" "os" + "syscall" "github.com/jessevdk/go-flags" @@ -78,6 +79,14 @@ func run() error { if err := parseArgs(os.Args[1:]); err != nil { return err } + + // Explicitly set the umask to 0 to prevent permission bits + // being masked out when creating files and directories. + // + // While snap-confine already does this for us, we inherit + // snapd's umask when it invokes us. + syscall.Umask(0) + var upCtx MountProfileUpdateContext if opts.UserMounts { upCtx = NewUserProfileUpdateContext(opts.Positionals.SnapName, opts.FromSnapConfine, os.Getuid()) diff --git a/cmd/snap-update-ns/system.go b/cmd/snap-update-ns/system.go index c66e23003e..bc7acd1c62 100644 --- a/cmd/snap-update-ns/system.go +++ b/cmd/snap-update-ns/system.go @@ -21,6 +21,7 @@ package main import ( "fmt" + "os" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/snap" @@ -80,14 +81,14 @@ func (upCtx *SystemProfileUpdateContext) Assumptions() *Assumptions { // As such, provide write access to all of /tmp. as.AddUnrestrictedPaths("/var/lib/snapd/hostfs/tmp") as.AddModeHint("/var/lib/snapd/hostfs/tmp/snap.*", 0700) - as.AddModeHint("/var/lib/snapd/hostfs/tmp/snap.*/tmp", 01777) + as.AddModeHint("/var/lib/snapd/hostfs/tmp/snap.*/tmp", 0777|os.ModeSticky) // This is to ensure that unprivileged users can create the socket. This // permission only matters if the plug-side app constructs its mount // namespace before the slot-side app is launched. - as.AddModeHint("/var/lib/snapd/hostfs/tmp/snap.*/tmp/.X11-unix", 01777) + as.AddModeHint("/var/lib/snapd/hostfs/tmp/snap.*/tmp/.X11-unix", 0777|os.ModeSticky) // This is to ensure private shared-memory directories have // the right permissions. - as.AddModeHint("/dev/shm/snap.*", 01777) + as.AddModeHint("/dev/shm/snap.*", 0777|os.ModeSticky) return as } diff --git a/cmd/snap-update-ns/system_test.go b/cmd/snap-update-ns/system_test.go index c19c3e5d6d..2d0f60a609 100644 --- a/cmd/snap-update-ns/system_test.go +++ b/cmd/snap-update-ns/system_test.go @@ -79,10 +79,10 @@ func (s *systemSuite) TestAssumptions(c *C) { c.Check(as.ModeForPath("/tmp"), Equals, os.FileMode(0755)) c.Check(as.ModeForPath("/var/lib/snapd/hostfs/tmp"), Equals, os.FileMode(0755)) c.Check(as.ModeForPath("/var/lib/snapd/hostfs/tmp/snap.x11-server"), Equals, os.FileMode(0700)) - c.Check(as.ModeForPath("/var/lib/snapd/hostfs/tmp/snap.x11-server/tmp"), Equals, os.FileMode(01777)) + c.Check(as.ModeForPath("/var/lib/snapd/hostfs/tmp/snap.x11-server/tmp"), Equals, os.FileMode(0777)|os.ModeSticky) c.Check(as.ModeForPath("/var/lib/snapd/hostfs/tmp/snap.x11-server/foo"), Equals, os.FileMode(0755)) - c.Check(as.ModeForPath("/var/lib/snapd/hostfs/tmp/snap.x11-server/tmp/.X11-unix"), Equals, os.FileMode(01777)) - c.Check(as.ModeForPath("/dev/shm/snap.some-snap"), Equals, os.FileMode(01777)) + c.Check(as.ModeForPath("/var/lib/snapd/hostfs/tmp/snap.x11-server/tmp/.X11-unix"), Equals, os.FileMode(0777)|os.ModeSticky) + c.Check(as.ModeForPath("/dev/shm/snap.some-snap"), Equals, os.FileMode(0777)|os.ModeSticky) // Instances can, in addition, access /snap/$SNAP_INSTANCE_NAME upCtx = update.NewSystemProfileUpdateContext("foo_instance", false) diff --git a/cmd/snap-update-ns/utils.go b/cmd/snap-update-ns/utils.go index ba675774de..dfa93f12e0 100644 --- a/cmd/snap-update-ns/utils.go +++ b/cmd/snap-update-ns/utils.go @@ -128,6 +128,23 @@ func OpenPath(path string) (int, error) { return fd, nil } +// syscallMode returns the syscall-specific mode bits from Go's portable mode bits. +// This is a copy of the same helper in Go's os package. +func syscallMode(i os.FileMode) (o uint32) { + o |= uint32(i.Perm()) + if i&os.ModeSetuid != 0 { + o |= syscall.S_ISUID + } + if i&os.ModeSetgid != 0 { + o |= syscall.S_ISGID + } + if i&os.ModeSticky != 0 { + o |= syscall.S_ISVTX + } + // No mapping for Go's ModeTemporary (plan9 only). + return o +} + // MkPrefix creates all the missing directories in a given base path and // returns the file descriptor to the leaf directory as well as the restricted // flag. This function is a base for secure variants of mkdir, touch and @@ -180,7 +197,7 @@ func MkDir(dirFd int, dirName string, name string, perm os.FileMode, uid sys.Use made := true const openFlags = syscall.O_NOFOLLOW | syscall.O_CLOEXEC | syscall.O_DIRECTORY - if err := sysMkdirat(dirFd, name, uint32(perm.Perm())); err != nil { + if err := sysMkdirat(dirFd, name, syscallMode(perm)); err != nil { switch err { case syscall.EEXIST: made = false @@ -239,7 +256,7 @@ func MkFile(dirFd int, dirName string, name string, perm os.FileMode, uid sys.Us // we know if we need to chown it) but fall back to just opening an // existing one. - newFd, err := sysOpenat(dirFd, name, openFlags|syscall.O_CREAT|syscall.O_EXCL, uint32(perm.Perm())) + newFd, err := sysOpenat(dirFd, name, openFlags|syscall.O_CREAT|syscall.O_EXCL, syscallMode(perm)) if err != nil { switch err { case syscall.EEXIST: diff --git a/cmd/snap-update-ns/utils_test.go b/cmd/snap-update-ns/utils_test.go index b7d91bb49c..16701c106a 100644 --- a/cmd/snap-update-ns/utils_test.go +++ b/cmd/snap-update-ns/utils_test.go @@ -136,6 +136,27 @@ func (s *utilsSuite) TestSecureMkdirAllLevel3(c *C) { }) } +// Ensure that we are not masking out the sticky bit when creating directories +func (s *utilsSuite) TestSecureMkdirAllAllowsStickyBit(c *C) { + s.sys.InsertFault(`mkdirat 3 "dev" 01777`, syscall.EEXIST) + s.sys.InsertFault(`mkdirat 4 "shm" 01777`, syscall.EEXIST) + c.Assert(update.MkdirAll("/dev/shm/snap.foo", 0777|os.ModeSticky, 0, 0, nil), IsNil) + c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ + {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `mkdirat 3 "dev" 01777`, E: syscall.EEXIST}, + {C: `openat 3 "dev" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4}, + {C: `mkdirat 4 "shm" 01777`, E: syscall.EEXIST}, + {C: `openat 4 "shm" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 5}, + {C: `close 4`}, + {C: `close 3`}, + {C: `mkdirat 5 "snap.foo" 01777`}, + {C: `openat 5 "snap.foo" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, + {C: `fchown 3 0 0`}, + {C: `close 3`}, + {C: `close 5`}, + }) +} + // Ensure that trespassing for prefix is matched using clean base path. func (s *utilsSuite) TestTrespassingMatcher(c *C) { // We mounted tmpfs at "/path". diff --git a/tests/main/interfaces-shared-memory-private/task.yaml b/tests/main/interfaces-shared-memory-private/task.yaml index 1f10f59cb0..186895f405 100644 --- a/tests/main/interfaces-shared-memory-private/task.yaml +++ b/tests/main/interfaces-shared-memory-private/task.yaml @@ -26,6 +26,9 @@ execute: | echo "shared-memory plugs with 'private: true' set are autoconnected" snap connections shm-private | MATCH "shared-memory +shm-private:shmem-private +:shared-memory +-" + echo "The private /dev/shm directory has the correct permissions" + shm-private.cmd stat -c %a /dev/shm | MATCH '^1777$' + echo "The snap can create arbitrary named segments under /dev/shm" shm-private.cmd touch /dev/shm/root-segment tests.session -u test exec shm-private.cmd touch /dev/shm/user-segment @@ -41,3 +44,9 @@ execute: | echo "The private shm segments are stored in a subdirectory of /dev/shm" test -f /dev/shm/snap.shm-private/root-segment test -f /dev/shm/snap.shm-private/user-segment + + echo "Mount namespace updates create directory with correct permissions" + snap disconnect shm-private:shmem-private + rm -rf /dev/shm/snap.shm-private + snap connect shm-private:shmem-private + shm-private.cmd stat -c %a /dev/shm | MATCH '^1777$' diff --git a/tests/main/snap-mgmt/task.yaml b/tests/main/snap-mgmt/task.yaml index a281be6237..ffbe9121b9 100644 --- a/tests/main/snap-mgmt/task.yaml +++ b/tests/main/snap-mgmt/task.yaml @@ -74,12 +74,24 @@ prepare: | test "$(find /etc/systemd/system -name 'snap.*.slice' | wc -l)" -gt 0 fi +restore: | + #shellcheck source=tests/lib/pkgdb.sh + . "$TESTSLIB/pkgdb.sh" + if [ -e pkg-removed ]; then + distro_install_build_snapd + rm pkg-removed + fi + +debug: | + systemctl --no-legend --full | grep -E 'snap\..*\.(service|timer|socket|slice)' || true + execute: | echo "Stop snapd before purging" systemctl stop snapd.service snapd.socket echo "A purge will really purge things" snapd.tool exec snap-mgmt --purge + touch pkg-removed echo "Data directories are empty" SNAP_MOUNT_DIR="$(os.paths snap-mount-dir)" |
