diff --git a/cmd/snap-confine/mount-support.c b/cmd/snap-confine/mount-support.c index 6bf38e1014f..9b75fb99d44 100644 --- a/cmd/snap-confine/mount-support.c +++ b/cmd/snap-confine/mount-support.c @@ -81,8 +81,6 @@ static const char *sc_get_outer_core_mount_point() // TODO: fold this into bootstrap static void setup_private_mount(const char *snap_name) { - uid_t uid = getuid(); - gid_t gid = getgid(); char tmpdir[MAX_BUF] = { 0 }; // Create a 0700 base directory, this is the base dir that is @@ -121,7 +119,7 @@ static void setup_private_mount(const char *snap_name) // MS_PRIVATE needs linux > 2.6.11 sc_do_mount("none", "/tmp", NULL, MS_PRIVATE, NULL); // do the chown after the bind mount to avoid potential shenanigans - if (chown("/tmp/", uid, gid) < 0) { + if (chown("/tmp/", 0, 0) < 0) { die("cannot change ownership of /tmp"); } // chdir to original directory diff --git a/cmd/snap-confine/mount-support.c.orig b/cmd/snap-confine/mount-support.c.orig new file mode 100644 index 00000000000..6bf38e1014f --- /dev/null +++ b/cmd/snap-confine/mount-support.c.orig @@ -0,0 +1,640 @@ +/* + * Copyright (C) 2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +#include "config.h" +#include "mount-support.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../libsnap-confine-private/classic.h" +#include "../libsnap-confine-private/cleanup-funcs.h" +#include "../libsnap-confine-private/mount-opt.h" +#include "../libsnap-confine-private/mountinfo.h" +#include "../libsnap-confine-private/snap.h" +#include "../libsnap-confine-private/string-utils.h" +#include "../libsnap-confine-private/utils.h" +#include "mount-support-nvidia.h" +#include "quirks.h" + +#define MAX_BUF 1000 + +/*! + * The void directory. + * + * Snap confine moves to that directory in case it cannot retain the current + * working directory across the pivot_root call. + **/ +#define SC_VOID_DIR "/var/lib/snapd/void" + +/** + * Get the path to the mounted core snap on the host distribution. + * + * The core snap may be named just "core" (preferred) or "ubuntu-core" + * (legacy). The mount point dependes on build-time configuration and may + * differ from distribution to distribution. + **/ +static const char *sc_get_outer_core_mount_point() +{ + const char *core_path = SNAP_MOUNT_DIR "/core/current/"; + const char *ubuntu_core_path = SNAP_MOUNT_DIR "/ubuntu-core/current/"; + static const char *result = NULL; + if (result == NULL) { + if (access(core_path, F_OK) == 0) { + // Use the "core" snap if available. + result = core_path; + } else if (access(ubuntu_core_path, F_OK) == 0) { + // If not try to fall back to the "ubuntu-core" snap. + result = ubuntu_core_path; + } else { + die("cannot locate the core snap"); + } + } + return result; +} + +// TODO: simplify this, after all it is just a tmpfs +// TODO: fold this into bootstrap +static void setup_private_mount(const char *snap_name) +{ + uid_t uid = getuid(); + gid_t gid = getgid(); + char tmpdir[MAX_BUF] = { 0 }; + + // Create a 0700 base directory, this is the base dir that is + // protected from other users. + // + // Under that basedir, we put a 1777 /tmp dir that is then bind + // mounted for the applications to use + sc_must_snprintf(tmpdir, sizeof(tmpdir), "/tmp/snap.%d_%s_XXXXXX", uid, + snap_name); + if (mkdtemp(tmpdir) == NULL) { + die("cannot create temporary directory essential for private /tmp"); + } + // now we create a 1777 /tmp inside our private dir + mode_t old_mask = umask(0); + char *d = strdup(tmpdir); + if (!d) { + die("cannot allocate memory for string copy"); + } + sc_must_snprintf(tmpdir, sizeof(tmpdir), "%s/tmp", d); + free(d); + + if (mkdir(tmpdir, 01777) != 0) { + die("cannot create temporary directory for private /tmp"); + } + umask(old_mask); + + // chdir to '/' since the mount won't apply to the current directory + char *pwd = get_current_dir_name(); + if (pwd == NULL) + die("cannot get current working directory"); + if (chdir("/") != 0) + die("cannot change directory to '/'"); + + // MS_BIND is there from linux 2.4 + sc_do_mount(tmpdir, "/tmp", NULL, MS_BIND, NULL); + // MS_PRIVATE needs linux > 2.6.11 + sc_do_mount("none", "/tmp", NULL, MS_PRIVATE, NULL); + // do the chown after the bind mount to avoid potential shenanigans + if (chown("/tmp/", uid, gid) < 0) { + die("cannot change ownership of /tmp"); + } + // chdir to original directory + if (chdir(pwd) != 0) + die("cannot change current working directory to the original directory"); + free(pwd); +} + +// TODO: fold this into bootstrap +static void setup_private_pts() +{ + // See https://www.kernel.org/doc/Documentation/filesystems/devpts.txt + // + // Ubuntu by default uses devpts 'single-instance' mode where + // /dev/pts/ptmx is mounted with ptmxmode=0000. We don't want to change + // the startup scripts though, so we follow the instructions in point + // '4' of 'User-space changes' in the above doc. In other words, after + // unshare(CLONE_NEWNS), we mount devpts with -o + // newinstance,ptmxmode=0666 and then bind mount /dev/pts/ptmx onto + // /dev/ptmx + + struct stat st; + + // Make sure /dev/pts/ptmx exists, otherwise we are in legacy mode + // which doesn't provide the isolation we require. + if (stat("/dev/pts/ptmx", &st) != 0) { + die("cannot stat /dev/pts/ptmx"); + } + // Make sure /dev/ptmx exists so we can bind mount over it + if (stat("/dev/ptmx", &st) != 0) { + die("cannot stat /dev/ptmx"); + } + // Since multi-instance, use ptmxmode=0666. The other options are + // copied from /etc/default/devpts + sc_do_mount("devpts", "/dev/pts", "devpts", MS_MGC_VAL, + "newinstance,ptmxmode=0666,mode=0620,gid=5"); + sc_do_mount("/dev/pts/ptmx", "/dev/ptmx", "none", MS_BIND, 0); +} + +/* + * Setup mount profiles as described by snapd. + * + * This function reads /var/lib/snapd/mount/$security_tag.fstab as a fstab(5) file + * and executes the mount requests described there. + * + * Currently only bind mounts are allowed. All bind mounts are read only by + * default though the `rw` flag can be used. + * + * This function is called with the rootfs being "consistent" so that it is + * either the core snap on an all-snap system or the core snap + punched holes + * on a classic system. + **/ +static void sc_setup_mount_profiles(const char *snap_name) +{ + debug("%s: %s", __FUNCTION__, snap_name); + + FILE *desired __attribute__ ((cleanup(sc_cleanup_endmntent))) = NULL; + FILE *current __attribute__ ((cleanup(sc_cleanup_endmntent))) = NULL; + char profile_path[PATH_MAX]; + + sc_must_snprintf(profile_path, sizeof(profile_path), + "/run/snapd/ns/snap.%s.fstab", snap_name); + debug("opening current mount profile %s", profile_path); + current = setmntent(profile_path, "w"); + if (current == NULL) { + die("cannot open current mount profile: %s", profile_path); + } + + sc_must_snprintf(profile_path, sizeof(profile_path), + "/var/lib/snapd/mount/snap.%s.fstab", snap_name); + debug("opening desired mount profile %s", profile_path); + desired = setmntent(profile_path, "r"); + if (desired == NULL && errno == ENOENT) { + // It is ok for the desired profile to not exist. Note that in this + // case we also "update" the current profile as we already opened and + // truncated it above. + return; + } + if (desired == NULL) { + die("cannot open desired mount profile: %s", profile_path); + } + + struct mntent *m = NULL; + while ((m = getmntent(desired)) != NULL) { + debug("read mount entry\n" + "\tmnt_fsname: %s\n" + "\tmnt_dir: %s\n" + "\tmnt_type: %s\n" + "\tmnt_opts: %s\n" + "\tmnt_freq: %d\n" + "\tmnt_passno: %d", + m->mnt_fsname, m->mnt_dir, m->mnt_type, + m->mnt_opts, m->mnt_freq, m->mnt_passno); + int flags = MS_BIND | MS_RDONLY | MS_NODEV | MS_NOSUID; + debug("initial flags are: bind,ro,nodev,nosuid"); + if (strcmp(m->mnt_type, "none") != 0) { + die("cannot honor mount profile, only 'none' filesystem type is supported"); + } + if (hasmntopt(m, "bind") == NULL) { + die("cannot honor mount profile, the bind mount flag is mandatory"); + } + if (hasmntopt(m, "rw") != NULL) { + flags &= ~MS_RDONLY; + } + sc_do_mount(m->mnt_fsname, m->mnt_dir, NULL, flags, NULL); + if (addmntent(current, m) != 0) { // NOTE: returns 1 on error. + die("cannot append entry to the current mount profile"); + } + } +} + +struct sc_mount { + const char *path; + bool is_bidirectional; +}; + +struct sc_mount_config { + const char *rootfs_dir; + // The struct is terminated with an entry with NULL path. + const struct sc_mount *mounts; + bool on_classic_distro; +}; + +/** + * Bootstrap mount namespace. + * + * This is a chunk of tricky code that lets us have full control over the + * layout and direction of propagation of mount events. The documentation below + * assumes knowledge of the 'sharedsubtree.txt' document from the kernel source + * tree. + * + * As a reminder two definitions are quoted below: + * + * A 'propagation event' is defined as event generated on a vfsmount + * that leads to mount or unmount actions in other vfsmounts. + * + * A 'peer group' is defined as a group of vfsmounts that propagate + * events to each other. + * + * (end of quote). + * + * The main idea is to setup a mount namespace that has a root filesystem with + * vfsmounts and peer groups that, depending on the location, either isolate + * or share with the rest of the system. + * + * The vast majority of the filesystem is shared in one direction. Events from + * the outside (from the main mount namespace) propagate inside (to namespaces + * of particular snaps) so things like new snap revisions, mounted drives, etc, + * just show up as expected but even if a snap is exploited or malicious in + * nature it cannot affect anything in another namespace where it might cause + * security or stability issues. + * + * Selected directories (today just /media) can be shared in both directions. + * This allows snaps with sufficient privileges to either create, through the + * mount system call, additional mount points that are visible by the rest of + * the system (both the main mount namespace and namespaces of individual + * snaps) or remove them, through the unmount system call. + **/ +static void sc_bootstrap_mount_namespace(const struct sc_mount_config *config) +{ + char scratch_dir[] = "/tmp/snap.rootfs_XXXXXX"; + char src[PATH_MAX]; + char dst[PATH_MAX]; + if (mkdtemp(scratch_dir) == NULL) { + die("cannot create temporary directory for the root file system"); + } + // NOTE: at this stage we just called unshare(CLONE_NEWNS). We are in a new + // mount namespace and have a private list of mounts. + debug("scratch directory for constructing namespace: %s", scratch_dir); + // Make the root filesystem recursively shared. This way propagation events + // will be shared with main mount namespace. + sc_do_mount("none", "/", NULL, MS_REC | MS_SHARED, NULL); + // Bind mount the temporary scratch directory for root filesystem over + // itself so that it is a mount point. This is done so that it can become + // unbindable as explained below. + sc_do_mount(scratch_dir, scratch_dir, NULL, MS_BIND, NULL); + // Make the scratch directory unbindable. + // + // This is necessary as otherwise a mount loop can occur and the kernel + // would crash. The term unbindable simply states that it cannot be bind + // mounted anywhere. When we construct recursive bind mounts below this + // guarantees that this directory will not be replicated anywhere. + sc_do_mount("none", scratch_dir, NULL, MS_UNBINDABLE, NULL); + // Recursively bind mount desired root filesystem directory over the + // scratch directory. This puts the initial content into the scratch space + // and serves as a foundation for all subsequent operations below. + // + // The mount is recursive because it can either be applied to the root + // filesystem of a core system (aka all-snap) or the core snap on a classic + // system. In the former case we need recursive bind mounts to accurately + // replicate the state of the root filesystem into the scratch directory. + sc_do_mount(config->rootfs_dir, scratch_dir, NULL, MS_REC | MS_BIND, + NULL); + // Make the scratch directory recursively private. Nothing done there will + // be shared with any peer group, This effectively detaches us from the + // original namespace and coupled with pivot_root below serves as the + // foundation of the mount sandbox. + sc_do_mount("none", scratch_dir, NULL, MS_REC | MS_SLAVE, NULL); + // Bind mount certain directories from the host filesystem to the scratch + // directory. By default mount events will propagate in both into and out + // of the peer group. This way the running application can alter any global + // state visible on the host and in other snaps. This can be restricted by + // disabling the "is_bidirectional" flag as can be seen below. + for (const struct sc_mount * mnt = config->mounts; mnt->path != NULL; + mnt++) { + if (mnt->is_bidirectional && mkdir(mnt->path, 0755) < 0 && + errno != EEXIST) { + die("cannot create %s", mnt->path); + } + sc_must_snprintf(dst, sizeof dst, "%s/%s", scratch_dir, + mnt->path); + sc_do_mount(mnt->path, dst, NULL, MS_REC | MS_BIND, NULL); + if (!mnt->is_bidirectional) { + // Mount events will only propagate inwards to the namespace. This + // way the running application cannot alter any global state apart + // from that of its own snap. + sc_do_mount("none", dst, NULL, MS_REC | MS_SLAVE, NULL); + } + } + // Since we mounted /etc from the host filesystem to the scratch directory, + // we may need to put /etc/alternatives from the desired root filesystem + // (e.g. the core snap) back. This way the behavior of running snaps is not + // affected by the alternatives directory from the host, if one exists. + // + // https://bugs.launchpad.net/snap-confine/+bug/1580018 + const char *etc_alternatives = "/etc/alternatives"; + if (access(etc_alternatives, F_OK) == 0) { + sc_must_snprintf(src, sizeof src, "%s%s", config->rootfs_dir, + etc_alternatives); + sc_must_snprintf(dst, sizeof dst, "%s%s", scratch_dir, + etc_alternatives); + sc_do_mount(src, dst, NULL, MS_BIND, NULL); + sc_do_mount("none", dst, NULL, MS_SLAVE, NULL); + } + // Bind mount the directory where all snaps are mounted. The location of + // the this directory on the host filesystem may not match the location in + // the desired root filesystem. In the "core" and "ubuntu-core" snaps the + // directory is always /snap. On the host it is a build-time configuration + // option stored in SNAP_MOUNT_DIR. + sc_must_snprintf(dst, sizeof dst, "%s/snap", scratch_dir); + sc_do_mount(SNAP_MOUNT_DIR, dst, NULL, MS_BIND | MS_REC | MS_SLAVE, + NULL); + sc_do_mount("none", dst, NULL, MS_REC | MS_SLAVE, NULL); + // Create the hostfs directory if one is missing. This directory is a part + // of packaging now so perhaps this code can be removed later. + if (access(SC_HOSTFS_DIR, F_OK) != 0) { + debug("creating missing hostfs directory"); + if (mkdir(SC_HOSTFS_DIR, 0755) != 0) { + die("cannot perform operation: mkdir %s", + SC_HOSTFS_DIR); + } + } + // Ensure that hostfs isgroup owned by root. We may have (now or earlier) + // created the directory as the user who first ran a snap on a given + // system and the group identity of that user is visilbe on disk. + // This was LP:#1665004 + struct stat sb; + if (stat(SC_HOSTFS_DIR, &sb) < 0) { + die("cannot stat %s", SC_HOSTFS_DIR); + } + if (sb.st_uid != 0 || sb.st_gid != 0) { + if (chown(SC_HOSTFS_DIR, 0, 0) < 0) { + die("cannot change user/group owner of %s to root", + SC_HOSTFS_DIR); + } + } + // Make the upcoming "put_old" directory for pivot_root private so that + // mount events don't propagate to any peer group. In practice pivot root + // has a number of undocumented requirements and one of them is that the + // "put_old" directory (the second argument) cannot be shared in any way. + sc_must_snprintf(dst, sizeof dst, "%s/%s", scratch_dir, SC_HOSTFS_DIR); + sc_do_mount(dst, dst, NULL, MS_BIND, NULL); + sc_do_mount("none", dst, NULL, MS_PRIVATE, NULL); + // On classic mount the nvidia driver. Ideally this would be done in an + // uniform way after pivot_root but this is good enough and requires less + // code changes the nvidia code assumes it has access to the existing + // pre-pivot filesystem. + if (config->on_classic_distro) { + sc_mount_nvidia_driver(scratch_dir); + } + // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + // pivot_root + // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + // Use pivot_root to "chroot" into the scratch directory. + // + // Q: Why are we using something as esoteric as pivot_root(2)? + // A: Because this makes apparmor handling easy. Using a normal chroot + // makes all apparmor rules conditional. We are either running on an + // all-snap system where this would-be chroot didn't happen and all the + // rules see / as the root file system _OR_ we are running on top of a + // classic distribution and this chroot has now moved all paths to + // /tmp/snap.rootfs_*. + // + // Because we are using unshare(2) with CLONE_NEWNS we can essentially use + // pivot_root just like chroot but this makes apparmor unaware of the old + // root so everything works okay. + // + // HINT: If you are debugging this and are trying to see why pivot_root + // happens to return EINVAL with any changes you may be making, please + // consider applying + // misc/0001-Add-printk-based-debugging-to-pivot_root.patch to your tree + // kernel. + debug("performing operation: pivot_root %s %s", scratch_dir, dst); + if (syscall(SYS_pivot_root, scratch_dir, dst) < 0) { + die("cannot perform operation: pivot_root %s %s", scratch_dir, + dst); + } + // Unmount the self-bind mount over the scratch directory created earlier + // in the original root filesystem (which is now mounted on SC_HOSTFS_DIR). + // This way we can remove the temporary directory we created and "clean up" + // after ourselves nicely. + sc_must_snprintf(dst, sizeof dst, "%s/%s", SC_HOSTFS_DIR, scratch_dir); + sc_do_umount(dst, 0); + // Remove the scratch directory. Note that we are using the path that is + // based on the old root filesystem as after pivot_root we cannot guarantee + // what is present at the same location normally. (It is probably an empty + // /tmp directory that is populated in another place). + debug("performing operation: rmdir %s", dst); + if (rmdir(scratch_dir) < 0) { + die("cannot perform operation: rmdir %s", dst); + }; + // Make the old root filesystem recursively slave. This way operations + // performed in this mount namespace will not propagate to the peer group. + // This is another essential part of the confinement system. + sc_do_mount("none", SC_HOSTFS_DIR, NULL, MS_REC | MS_SLAVE, NULL); + // Detach the redundant hostfs version of sysfs since it shows up in the + // mount table and software inspecting the mount table may become confused + // (eg, docker and LP:# 162601). + sc_must_snprintf(src, sizeof src, "%s/sys", SC_HOSTFS_DIR); + sc_do_umount(src, UMOUNT_NOFOLLOW | MNT_DETACH); + // Detach the redundant hostfs version of /dev since it shows up in the + // mount table and software inspecting the mount table may become confused. + sc_must_snprintf(src, sizeof src, "%s/dev", SC_HOSTFS_DIR); + sc_do_umount(src, UMOUNT_NOFOLLOW | MNT_DETACH); + // Detach the redundant hostfs version of /proc since it shows up in the + // mount table and software inspecting the mount table may become confused. + sc_must_snprintf(src, sizeof src, "%s/proc", SC_HOSTFS_DIR); + sc_do_umount(src, UMOUNT_NOFOLLOW | MNT_DETACH); +} + +/** + * @path: a pathname where / replaced with '\0'. + * @offsetp: pointer to int showing which path segment was last seen. + * Updated on return to reflect the next segment. + * @fulllen: full original path length. + * Returns a pointer to the next path segment, or NULL if done. + */ +static char * __attribute__ ((used)) + get_nextpath(char *path, size_t * offsetp, size_t fulllen) +{ + int offset = *offsetp; + + if (offset >= fulllen) + return NULL; + + while (offset < fulllen && path[offset] != '\0') + offset++; + while (offset < fulllen && path[offset] == '\0') + offset++; + + *offsetp = offset; + return (offset < fulllen) ? &path[offset] : NULL; +} + +/** + * Check that @subdir is a subdir of @dir. +**/ +static bool __attribute__ ((used)) + is_subdir(const char *subdir, const char *dir) +{ + size_t dirlen = strlen(dir); + size_t subdirlen = strlen(subdir); + + // @dir has to be at least as long as @subdir + if (subdirlen < dirlen) + return false; + // @dir has to be a prefix of @subdir + if (strncmp(subdir, dir, dirlen) != 0) + return false; + // @dir can look like "path/" (that is, end with the directory separator). + // When that is the case then given the test above we can be sure @subdir + // is a real subdirectory. + if (dirlen > 0 && dir[dirlen - 1] == '/') + return true; + // @subdir can look like "path/stuff" and when the directory separator + // is exactly at the spot where @dir ends (that is, it was not caught + // by the test above) then @subdir is a real subdirectory. + if (subdir[dirlen] == '/' && dirlen > 0) + return true; + // If both @dir and @subdir have identical length then given that the + // prefix check above @subdir is a real subdirectory. + if (subdirlen == dirlen) + return true; + return false; +} + +void sc_populate_mount_ns(const char *snap_name) +{ + // Get the current working directory before we start fiddling with + // mounts and possibly pivot_root. At the end of the whole process, we + // will try to re-locate to the same directory (if possible). + char *vanilla_cwd __attribute__ ((cleanup(sc_cleanup_string))) = NULL; + vanilla_cwd = get_current_dir_name(); + if (vanilla_cwd == NULL) { + die("cannot get the current working directory"); + } + // Remember if we are on classic, some things behave differently there. + bool on_classic_distro = is_running_on_classic_distribution(); + if (on_classic_distro) { + const struct sc_mount mounts[] = { + {"/dev"}, // because it contains devices on host OS + {"/etc"}, // because that's where /etc/resolv.conf lives, perhaps a bad idea + {"/home"}, // to support /home/*/snap and home interface + {"/root"}, // because that is $HOME for services + {"/proc"}, // fundamental filesystem + {"/sys"}, // fundamental filesystem + {"/tmp"}, // to get writable tmp + {"/var/snap"}, // to get access to global snap data + {"/var/lib/snapd"}, // to get access to snapd state and seccomp profiles + {"/var/tmp"}, // to get access to the other temporary directory + {"/run"}, // to get /run with sockets and what not + {"/lib/modules"}, // access to the modules of the running kernel + {"/usr/src"}, // FIXME: move to SecurityMounts in system-trace interface + {"/var/log"}, // FIXME: move to SecurityMounts in log-observe interface +#ifdef MERGED_USR + {"/run/media", true}, // access to the users removable devices +#else + {"/media", true}, // access to the users removable devices +#endif // MERGED_USR + {"/run/netns", true}, // access to the 'ip netns' network namespaces + {}, + }; + struct sc_mount_config classic_config = { + .rootfs_dir = sc_get_outer_core_mount_point(), + .mounts = mounts, + .on_classic_distro = true, + }; + sc_bootstrap_mount_namespace(&classic_config); + } else { + // This is what happens on an all-snap system. The rootfs we start with + // is the real outer rootfs. There are no unidirectional bind mounts + // needed because everything is already OK. We still keep the + // bidirectional /media mount point so that snaps designed for mounting + // filesystems can use that space for whatever they need. + const struct sc_mount mounts[] = { + {"/media", true}, + {"/run/netns", true}, + {}, + }; + struct sc_mount_config all_snap_config = { + .rootfs_dir = "/", + .mounts = mounts, + }; + sc_bootstrap_mount_namespace(&all_snap_config); + } + + // set up private mounts + // TODO: rename this and fold it into bootstrap + setup_private_mount(snap_name); + + // set up private /dev/pts + // TODO: fold this into bootstrap + setup_private_pts(); + + // setup quirks for specific snaps + if (on_classic_distro) { + sc_setup_quirks(); + } + // setup the security backend bind mounts + sc_setup_mount_profiles(snap_name); + + // Try to re-locate back to vanilla working directory. This can fail + // because that directory is no longer present. + if (chdir(vanilla_cwd) != 0) { + debug("cannot remain in %s, moving to the void directory", + vanilla_cwd); + if (chdir(SC_VOID_DIR) != 0) { + die("cannot change directory to %s", SC_VOID_DIR); + } + debug("successfully moved to %s", SC_VOID_DIR); + } +} + +static bool is_mounted_with_shared_option(const char *dir) + __attribute__ ((nonnull(1))); + +static bool is_mounted_with_shared_option(const char *dir) +{ + struct sc_mountinfo *sm + __attribute__ ((cleanup(sc_cleanup_mountinfo))) = NULL; + sm = sc_parse_mountinfo(NULL); + if (sm == NULL) { + die("cannot parse /proc/self/mountinfo"); + } + struct sc_mountinfo_entry *entry = sc_first_mountinfo_entry(sm); + while (entry != NULL) { + const char *mount_dir = entry->mount_dir; + if (sc_streq(mount_dir, dir)) { + const char *optional_fields = entry->optional_fields; + if (strstr(optional_fields, "shared:") != NULL) { + return true; + } + } + entry = sc_next_mountinfo_entry(entry); + } + return false; +} + +void sc_ensure_shared_snap_mount() +{ + if (!is_mounted_with_shared_option("/") + && !is_mounted_with_shared_option("/snap")) { + sc_do_mount("/snap", "/snap", "none", MS_BIND | MS_REC, 0); + sc_do_mount("none", "/snap", NULL, MS_SHARED | MS_REC, NULL); + } +} diff --git a/cmd/snap/cmd_run.go b/cmd/snap/cmd_run.go index b31ea72e7d0..70beb7edfba 100644 --- a/cmd/snap/cmd_run.go +++ b/cmd/snap/cmd_run.go @@ -172,6 +172,10 @@ func createUserDataDirs(info *snap.Info) error { return fmt.Errorf(i18n.G("cannot get the current user: %v"), err) } + snapDir := filepath.Join(usr.HomeDir, dirs.UserHomeSnapDir) + if err := os.MkdirAll(snapDir, 0700); err != nil { + return fmt.Errorf(i18n.G("cannot create snap home dir: %w"), err) + } // see snapenv.User userData := info.UserDataDir(usr.HomeDir) commonUserData := info.UserCommonDataDir(usr.HomeDir) diff --git a/cmd/snap/cmd_run.go.orig b/cmd/snap/cmd_run.go.orig new file mode 100644 index 00000000000..b31ea72e7d0 --- /dev/null +++ b/cmd/snap/cmd_run.go.orig @@ -0,0 +1,420 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package main + +import ( + "fmt" + "io" + "os" + "os/user" + "path/filepath" + "strings" + "syscall" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snapenv" + "github.com/snapcore/snapd/x11" +) + +var ( + syscallExec = syscall.Exec + userCurrent = user.Current + osGetenv = os.Getenv +) + +type cmdRun struct { + Command string `long:"command" hidden:"yes"` + Hook string `long:"hook" hidden:"yes"` + Revision string `short:"r" default:"unset" hidden:"yes"` + Shell bool `long:"shell" ` +} + +func init() { + addCommand("run", + i18n.G("Run the given snap command"), + i18n.G("Run the given snap command with the right confinement and environment"), + func() flags.Commander { + return &cmdRun{} + }, map[string]string{ + "command": i18n.G("Alternative command to run"), + "hook": i18n.G("Hook to run"), + "r": i18n.G("Use a specific snap revision when running hook"), + "shell": i18n.G("Run a shell instead of the command (useful for debugging)"), + }, nil) +} + +func (x *cmdRun) Execute(args []string) error { + if len(args) == 0 { + return fmt.Errorf(i18n.G("need the application to run as argument")) + } + snapApp := args[0] + args = args[1:] + + // Catch some invalid parameter combinations, provide helpful errors + if x.Hook != "" && x.Command != "" { + return fmt.Errorf(i18n.G("cannot use --hook and --command together")) + } + if x.Revision != "unset" && x.Revision != "" && x.Hook == "" { + return fmt.Errorf(i18n.G("-r can only be used with --hook")) + } + if x.Hook != "" && len(args) > 0 { + // TRANSLATORS: %q is the hook name; %s a space-separated list of extra arguments + return fmt.Errorf(i18n.G("too many arguments for hook %q: %s"), x.Hook, strings.Join(args, " ")) + } + + // Now actually handle the dispatching + if x.Hook != "" { + return snapRunHook(snapApp, x.Revision, x.Hook) + } + + // pass shell as a special command to snap-exec + if x.Shell { + x.Command = "shell" + } + + return snapRunApp(snapApp, x.Command, args) +} + +func getSnapInfo(snapName string, revision snap.Revision) (*snap.Info, error) { + if revision.Unset() { + curFn := filepath.Join(dirs.SnapMountDir, snapName, "current") + realFn, err := os.Readlink(curFn) + if err != nil { + return nil, fmt.Errorf("cannot find current revision for snap %s: %s", snapName, err) + } + rev := filepath.Base(realFn) + revision, err = snap.ParseRevision(rev) + if err != nil { + return nil, fmt.Errorf("cannot read revision %s: %s", rev, err) + } + } + + info, err := snap.ReadInfo(snapName, &snap.SideInfo{ + Revision: revision, + }) + if err != nil { + return nil, err + } + + return info, nil +} + +func createOrUpdateUserDataSymlink(info *snap.Info, usr *user.User) error { + // 'current' symlink for user data (SNAP_USER_DATA) + userData := info.UserDataDir(usr.HomeDir) + wantedSymlinkValue := filepath.Base(userData) + currentActiveSymlink := filepath.Join(userData, "..", "current") + + var err error + var currentSymlinkValue string + for i := 0; i < 5; i++ { + currentSymlinkValue, err = os.Readlink(currentActiveSymlink) + // Failure other than non-existing symlink is fatal + if err != nil && !os.IsNotExist(err) { + // TRANSLATORS: %v the error message + return fmt.Errorf(i18n.G("cannot read symlink: %v"), err) + } + + if currentSymlinkValue == wantedSymlinkValue { + break + } + + if err == nil { + // We may be racing with other instances of snap-run that try to do the same thing + // If the symlink is already removed then we can ignore this error. + err = os.Remove(currentActiveSymlink) + if err != nil && !os.IsNotExist(err) { + // abort with error + break + } + } + + err = os.Symlink(wantedSymlinkValue, currentActiveSymlink) + // Error other than symlink already exists will abort and be propagated + if err == nil || !os.IsExist(err) { + break + } + // If we arrived here it means the symlink couldn't be created because it got created + // in the meantime by another instance, so we will try again. + } + if err != nil { + return fmt.Errorf(i18n.G("cannot update the 'current' symlink of %q: %v"), currentActiveSymlink, err) + } + return nil +} + +func createUserDataDirs(info *snap.Info) error { + usr, err := userCurrent() + if err != nil { + return fmt.Errorf(i18n.G("cannot get the current user: %v"), err) + } + + // see snapenv.User + userData := info.UserDataDir(usr.HomeDir) + commonUserData := info.UserCommonDataDir(usr.HomeDir) + for _, d := range []string{userData, commonUserData} { + if err := os.MkdirAll(d, 0755); err != nil { + // TRANSLATORS: %q is the directory whose creation failed, %v the error message + return fmt.Errorf(i18n.G("cannot create %q: %v"), d, err) + } + } + + return createOrUpdateUserDataSymlink(info, usr) +} + +func snapRunApp(snapApp, command string, args []string) error { + snapName, appName := snap.SplitSnapApp(snapApp) + info, err := getSnapInfo(snapName, snap.R(0)) + if err != nil { + return err + } + + app := info.Apps[appName] + if app == nil { + return fmt.Errorf(i18n.G("cannot find app %q in %q"), appName, snapName) + } + + return runSnapConfine(info, app.SecurityTag(), snapApp, command, "", args) +} + +func snapRunHook(snapName, snapRevision, hookName string) error { + revision, err := snap.ParseRevision(snapRevision) + if err != nil { + return err + } + + info, err := getSnapInfo(snapName, revision) + if err != nil { + return err + } + + hook := info.Hooks[hookName] + if hook == nil { + return fmt.Errorf(i18n.G("cannot find hook %q in %q"), hookName, snapName) + } + + return runSnapConfine(info, hook.SecurityTag(), snapName, "", hook.Name, nil) +} + +var osReadlink = os.Readlink + +func isReexeced() bool { + exe, err := osReadlink("/proc/self/exe") + if err != nil { + logger.Noticef("cannot read /proc/self/exe: %v", err) + return false + } + return strings.HasPrefix(exe, dirs.SnapMountDir) +} + +func migrateXauthority(info *snap.Info) (string, error) { + u, err := userCurrent() + if err != nil { + return "", fmt.Errorf(i18n.G("cannot get the current user: %s"), err) + } + + // If our target directory (XDG_RUNTIME_DIR) doesn't exist we + // don't attempt to create it. + baseTargetDir := filepath.Join(dirs.XdgRuntimeDirBase, u.Uid) + if !osutil.FileExists(baseTargetDir) { + return "", nil + } + + xauthPath := osGetenv("XAUTHORITY") + if len(xauthPath) == 0 || !osutil.FileExists(xauthPath) { + // Nothing to do for us. Most likely running outside of any + // graphical X11 session. + return "", nil + } + + fin, err := os.Open(xauthPath) + if err != nil { + return "", err + } + defer fin.Close() + + // Abs() also calls Clean(); see https://golang.org/pkg/path/filepath/#Abs + xauthPathAbs, err := filepath.Abs(fin.Name()) + if err != nil { + return "", nil + } + + // Remove all symlinks from path + xauthPathCan, err := filepath.EvalSymlinks(xauthPathAbs) + if err != nil { + return "", nil + } + + // Ensure the XAUTHORITY env is not abused by checking that + // it point to exactly the file we just opened (no symlinks, + // no funny "../.." etc) + if fin.Name() != xauthPathCan { + logger.Noticef("WARNING: XAUTHORITY environment value is not a clean path: %q", xauthPathCan) + return "", nil + } + + // Only do the migration from /tmp since the real /tmp is not visible for snaps + if !strings.HasPrefix(fin.Name(), "/tmp/") { + return "", nil + } + + // We are performing a Stat() here to make sure that the user can't + // steal another user's Xauthority file. Note that while Stat() uses + // fstat() on the file descriptor created during Open(), the file might + // have changed ownership between the Open() and the Stat(). That's ok + // because we aren't trying to block access that the user already has: + // if the user has the privileges to chown another user's Xauthority + // file, we won't block that since the user can just steal it without + // having to use snap run. This code is just to ensure that a user who + // doesn't have those privileges can't steal the file via snap run + // (also note that the (potentially untrusted) snap isn't running yet). + fi, err := fin.Stat() + if err != nil { + return "", err + } + sys := fi.Sys() + if sys == nil { + return "", fmt.Errorf(i18n.G("cannot validate owner of file %s"), fin.Name()) + } + // cheap comparison as the current uid is only available as a string + // but it is better to convert the uid from the stat result to a + // string than a string into a number. + if fmt.Sprintf("%d", sys.(*syscall.Stat_t).Uid) != u.Uid { + return "", fmt.Errorf(i18n.G("Xauthority file isn't owned by the current user %s"), u.Uid) + } + + targetPath := filepath.Join(baseTargetDir, ".Xauthority") + + // Only validate Xauthority file again when both files don't match + // otherwise we can continue using the existing Xauthority file. + // This is ok to do here because we aren't trying to protect against + // the user changing the Xauthority file in XDG_RUNTIME_DIR outside + // of snapd. + if osutil.FileExists(targetPath) { + var fout *os.File + if fout, err = os.Open(targetPath); err != nil { + return "", err + } + if osutil.StreamsEqual(fin, fout) { + fout.Close() + return targetPath, nil + } + + fout.Close() + if err := os.Remove(targetPath); err != nil { + return "", err + } + + // Ensure we're validating the Xauthority file from the beginning + if _, err := fin.Seek(int64(os.SEEK_SET), 0); err != nil { + return "", err + } + } + + // To guard against setting XAUTHORITY to non-xauth files, check + // that we have a valid Xauthority. Specifically, the file must be + // parseable as an Xauthority file and not be empty. + if err := x11.ValidateXauthority(fin); err != nil { + return "", err + } + + // Read data from the beginning of the file + if _, err = fin.Seek(int64(os.SEEK_SET), 0); err != nil { + return "", err + } + + fout, err := os.OpenFile(targetPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600) + if err != nil { + return "", err + } + defer fout.Close() + + // Read and write validated Xauthority file to its right location + if _, err = io.Copy(fout, fin); err != nil { + if err := os.Remove(targetPath); err != nil { + logger.Noticef("WARNING: cannot remove file at %s: %s", targetPath, err) + } + return "", fmt.Errorf(i18n.G("cannot write new Xauthority file at %s: %s"), targetPath, err) + } + + return targetPath, nil +} + +func runSnapConfine(info *snap.Info, securityTag, snapApp, command, hook string, args []string) error { + snapConfine := filepath.Join(dirs.DistroLibExecDir, "snap-confine") + // if we re-exec, we must run the snap-confine from the core snap + // as well, if they get out of sync, havoc will happen + if isReexeced() { + // run snap-confine from the core snap. that will work because + // snap-confine on the core snap is mostly statically linked + // (except libudev and libc) + snapConfine = filepath.Join(dirs.SnapMountDir, "core/current", dirs.CoreLibExecDir, "snap-confine") + } + + if !osutil.FileExists(snapConfine) { + if hook != "" { + logger.Noticef("WARNING: skipping running hook %q of snap %q: missing snap-confine", hook, info.Name()) + return nil + } + return fmt.Errorf(i18n.G("missing snap-confine: try updating your snapd package")) + } + + if err := createUserDataDirs(info); err != nil { + logger.Noticef("WARNING: cannot create user data directory: %s", err) + } + + xauthPath, err := migrateXauthority(info) + if err != nil { + logger.Noticef("WARNING: cannot copy user Xauthority file: %s", err) + } + + cmd := []string{snapConfine} + if info.NeedsClassic() { + cmd = append(cmd, "--classic") + } + cmd = append(cmd, securityTag) + cmd = append(cmd, filepath.Join(dirs.CoreLibExecDir, "snap-exec")) + + if command != "" { + cmd = append(cmd, "--command="+command) + } + + if hook != "" { + cmd = append(cmd, "--hook="+hook) + } + + // snap-exec is POSIXly-- options must come before positionals. + cmd = append(cmd, snapApp) + cmd = append(cmd, args...) + + extraEnv := make(map[string]string) + if len(xauthPath) > 0 { + extraEnv["XAUTHORITY"] = xauthPath + } + env := snapenv.ExecEnv(info, extraEnv) + + return syscallExec(cmd[0], cmd, env) +} diff --git a/cmd/snap/cmd_run_test.go b/cmd/snap/cmd_run_test.go index 7f4f361fbd7..8cf8eaea5d0 100644 --- a/cmd/snap/cmd_run_test.go +++ b/cmd/snap/cmd_run_test.go @@ -566,3 +566,20 @@ func (s *SnapSuite) TestSnapRunXauthorityMigration(c *check.C) { err = x11.ValidateXauthorityFile(expectedXauthPath) c.Assert(err, check.IsNil) } + +func (s *RunSuite) TestCreateSnapDirPermissions(c *check.C) { + usr, err := user.Current() + c.Assert(err, check.IsNil) + + usr.HomeDir = s.fakeHome + snaprun.MockUserCurrent(func() (*user.User, error) { + return usr, nil + }) + + info := &snap.Info{SuggestedName: "some-snap"} + c.Assert(snaprun.CreateUserDataDirs(info), check.IsNil) + + fi, err := os.Stat(filepath.Join(s.fakeHome, dirs.UserHomeSnapDir)) + c.Assert(err, check.IsNil) + c.Assert(fi.Mode()&os.ModePerm, check.Equals, os.FileMode(0700)) +} diff --git a/cmd/snap/cmd_run_test.go.orig b/cmd/snap/cmd_run_test.go.orig new file mode 100644 index 00000000000..7f4f361fbd7 --- /dev/null +++ b/cmd/snap/cmd_run_test.go.orig @@ -0,0 +1,568 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package main_test + +import ( + "fmt" + "os" + "os/user" + "path/filepath" + + "gopkg.in/check.v1" + + snaprun "github.com/snapcore/snapd/cmd/snap" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snaptest" + "github.com/snapcore/snapd/testutil" + "github.com/snapcore/snapd/x11" +) + +var mockYaml = []byte(`name: snapname +version: 1.0 +apps: + app: + command: run-app +hooks: + configure: +`) +var mockContents = "SNAP" + +func (s *SnapSuite) TestInvalidParameters(c *check.C) { + invalidParameters := []string{"run", "--hook=configure", "--command=command-name", "snap-name"} + _, err := snaprun.Parser().ParseArgs(invalidParameters) + c.Check(err, check.ErrorMatches, ".*cannot use --hook and --command together.*") + + invalidParameters = []string{"run", "-r=1", "--command=command-name", "snap-name"} + _, err = snaprun.Parser().ParseArgs(invalidParameters) + c.Check(err, check.ErrorMatches, ".*-r can only be used with --hook.*") + + invalidParameters = []string{"run", "-r=1", "snap-name"} + _, err = snaprun.Parser().ParseArgs(invalidParameters) + c.Check(err, check.ErrorMatches, ".*-r can only be used with --hook.*") + + invalidParameters = []string{"run", "--hook=configure", "foo", "bar", "snap-name"} + _, err = snaprun.Parser().ParseArgs(invalidParameters) + c.Check(err, check.ErrorMatches, ".*too many arguments for hook \"configure\": bar.*") +} + +func (s *SnapSuite) TestSnapRunWhenMissingConfine(c *check.C) { + dirs.SetRootDir(c.MkDir()) + + // mock installed snap + si := snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{ + Revision: snap.R("x2"), + }) + err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current")) + c.Assert(err, check.IsNil) + + // redirect exec + var execs [][]string + restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { + execs = append(execs, args) + return nil + }) + defer restorer() + + // and run it! + // a regular run will fail + _, err = snaprun.Parser().ParseArgs([]string{"run", "snapname.app", "--arg1", "arg2"}) + c.Assert(err, check.ErrorMatches, `.* your snapd package`) + // a hook run will not fail + _, err = snaprun.Parser().ParseArgs([]string{"run", "--hook=configure", "snapname"}) + c.Assert(err, check.IsNil) + + // but nothing is run ever + c.Check(execs, check.IsNil) +} + +func (s *SnapSuite) TestSnapRunAppIntegration(c *check.C) { + // mock installed snap + dirs.SetRootDir(c.MkDir()) + defer func() { dirs.SetRootDir("/") }() + defer mockSnapConfine(dirs.DistroLibExecDir)() + + si := snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{ + Revision: snap.R("x2"), + }) + err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current")) + c.Assert(err, check.IsNil) + + // redirect exec + execArg0 := "" + execArgs := []string{} + execEnv := []string{} + restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { + execArg0 = arg0 + execArgs = args + execEnv = envv + return nil + }) + defer restorer() + + // and run it! + rest, err := snaprun.Parser().ParseArgs([]string{"run", "snapname.app", "--arg1", "arg2"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"}) + c.Check(execArg0, check.Equals, filepath.Join(dirs.DistroLibExecDir, "snap-confine")) + c.Check(execArgs, check.DeepEquals, []string{ + filepath.Join(dirs.DistroLibExecDir, "snap-confine"), + "snap.snapname.app", + filepath.Join(dirs.CoreLibExecDir, "snap-exec"), + "snapname.app", "--arg1", "arg2"}) + c.Check(execEnv, testutil.Contains, "SNAP_REVISION=x2") +} + +func (s *SnapSuite) TestSnapRunClassicAppIntegration(c *check.C) { + // mock installed snap + dirs.SetRootDir(c.MkDir()) + defer func() { dirs.SetRootDir("/") }() + defer mockSnapConfine(dirs.DistroLibExecDir)() + + si := snaptest.MockSnap(c, string(mockYaml)+"confinement: classic\n", string(mockContents), &snap.SideInfo{ + Revision: snap.R("x2"), + }) + err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current")) + c.Assert(err, check.IsNil) + + // redirect exec + execArg0 := "" + execArgs := []string{} + execEnv := []string{} + restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { + execArg0 = arg0 + execArgs = args + execEnv = envv + return nil + }) + defer restorer() + + // and run it! + rest, err := snaprun.Parser().ParseArgs([]string{"run", "snapname.app", "--arg1", "arg2"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"}) + c.Check(execArg0, check.Equals, filepath.Join(dirs.DistroLibExecDir, "snap-confine")) + c.Check(execArgs, check.DeepEquals, []string{ + filepath.Join(dirs.DistroLibExecDir, "snap-confine"), "--classic", + "snap.snapname.app", + filepath.Join(dirs.CoreLibExecDir, "snap-exec"), + "snapname.app", "--arg1", "arg2"}) + c.Check(execEnv, testutil.Contains, "SNAP_REVISION=x2") +} + +func (s *SnapSuite) TestSnapRunAppWithCommandIntegration(c *check.C) { + // mock installed snap + dirs.SetRootDir(c.MkDir()) + defer func() { dirs.SetRootDir("/") }() + defer mockSnapConfine(dirs.DistroLibExecDir)() + + si := snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{ + Revision: snap.R(42), + }) + err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current")) + c.Assert(err, check.IsNil) + + // redirect exec + execArg0 := "" + execArgs := []string{} + execEnv := []string{} + restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { + execArg0 = arg0 + execArgs = args + execEnv = envv + return nil + }) + defer restorer() + + // and run it! + err = snaprun.SnapRunApp("snapname.app", "my-command", []string{"arg1", "arg2"}) + c.Assert(err, check.IsNil) + c.Check(execArg0, check.Equals, filepath.Join(dirs.DistroLibExecDir, "snap-confine")) + c.Check(execArgs, check.DeepEquals, []string{ + filepath.Join(dirs.DistroLibExecDir, "snap-confine"), + "snap.snapname.app", + filepath.Join(dirs.CoreLibExecDir, "snap-exec"), + "--command=my-command", "snapname.app", "arg1", "arg2"}) + c.Check(execEnv, testutil.Contains, "SNAP_REVISION=42") +} + +func (s *SnapSuite) TestSnapRunCreateDataDirs(c *check.C) { + info, err := snap.InfoFromSnapYaml(mockYaml) + c.Assert(err, check.IsNil) + info.SideInfo.Revision = snap.R(42) + + fakeHome := c.MkDir() + restorer := snaprun.MockUserCurrent(func() (*user.User, error) { + return &user.User{HomeDir: fakeHome}, nil + }) + defer restorer() + + err = snaprun.CreateUserDataDirs(info) + c.Assert(err, check.IsNil) + c.Check(osutil.FileExists(filepath.Join(fakeHome, "/snap/snapname/42")), check.Equals, true) + c.Check(osutil.FileExists(filepath.Join(fakeHome, "/snap/snapname/common")), check.Equals, true) +} + +func (s *SnapSuite) TestSnapRunHookIntegration(c *check.C) { + // mock installed snap + dirs.SetRootDir(c.MkDir()) + defer func() { dirs.SetRootDir("/") }() + defer mockSnapConfine(dirs.DistroLibExecDir)() + + si := snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{ + Revision: snap.R(42), + }) + err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current")) + c.Assert(err, check.IsNil) + + // redirect exec + execArg0 := "" + execArgs := []string{} + execEnv := []string{} + restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { + execArg0 = arg0 + execArgs = args + execEnv = envv + return nil + }) + defer restorer() + + // Run a hook from the active revision + _, err = snaprun.Parser().ParseArgs([]string{"run", "--hook=configure", "snapname"}) + c.Assert(err, check.IsNil) + c.Check(execArg0, check.Equals, filepath.Join(dirs.DistroLibExecDir, "snap-confine")) + c.Check(execArgs, check.DeepEquals, []string{ + filepath.Join(dirs.DistroLibExecDir, "snap-confine"), + "snap.snapname.hook.configure", + filepath.Join(dirs.CoreLibExecDir, "snap-exec"), + "--hook=configure", "snapname"}) + c.Check(execEnv, testutil.Contains, "SNAP_REVISION=42") +} + +func (s *SnapSuite) TestSnapRunHookUnsetRevisionIntegration(c *check.C) { + // mock installed snap + dirs.SetRootDir(c.MkDir()) + defer func() { dirs.SetRootDir("/") }() + defer mockSnapConfine(dirs.DistroLibExecDir)() + + si := snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{ + Revision: snap.R(42), + }) + err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current")) + c.Assert(err, check.IsNil) + + // redirect exec + execArg0 := "" + execArgs := []string{} + execEnv := []string{} + restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { + execArg0 = arg0 + execArgs = args + execEnv = envv + return nil + }) + defer restorer() + + // Specifically pass "unset" which would use the active version. + _, err = snaprun.Parser().ParseArgs([]string{"run", "--hook=configure", "-r=unset", "snapname"}) + c.Assert(err, check.IsNil) + c.Check(execArg0, check.Equals, filepath.Join(dirs.DistroLibExecDir, "snap-confine")) + c.Check(execArgs, check.DeepEquals, []string{ + filepath.Join(dirs.DistroLibExecDir, "snap-confine"), + "snap.snapname.hook.configure", + filepath.Join(dirs.CoreLibExecDir, "snap-exec"), + "--hook=configure", "snapname"}) + c.Check(execEnv, testutil.Contains, "SNAP_REVISION=42") +} + +func (s *SnapSuite) TestSnapRunHookSpecificRevisionIntegration(c *check.C) { + // mock installed snap + dirs.SetRootDir(c.MkDir()) + defer func() { dirs.SetRootDir("/") }() + defer mockSnapConfine(dirs.DistroLibExecDir)() + + // Create both revisions 41 and 42 + snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{ + Revision: snap.R(41), + }) + snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{ + Revision: snap.R(42), + }) + + // redirect exec + execArg0 := "" + execArgs := []string{} + execEnv := []string{} + restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { + execArg0 = arg0 + execArgs = args + execEnv = envv + return nil + }) + defer restorer() + + // Run a hook on revision 41 + _, err := snaprun.Parser().ParseArgs([]string{"run", "--hook=configure", "-r=41", "snapname"}) + c.Assert(err, check.IsNil) + c.Check(execArg0, check.Equals, filepath.Join(dirs.DistroLibExecDir, "snap-confine")) + c.Check(execArgs, check.DeepEquals, []string{ + filepath.Join(dirs.DistroLibExecDir, "snap-confine"), + "snap.snapname.hook.configure", + filepath.Join(dirs.CoreLibExecDir, "snap-exec"), + "--hook=configure", "snapname"}) + c.Check(execEnv, testutil.Contains, "SNAP_REVISION=41") +} + +func (s *SnapSuite) TestSnapRunHookMissingRevisionIntegration(c *check.C) { + // mock installed snap + dirs.SetRootDir(c.MkDir()) + defer func() { dirs.SetRootDir("/") }() + + // Only create revision 42 + si := snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{ + Revision: snap.R(42), + }) + err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current")) + c.Assert(err, check.IsNil) + + // redirect exec + restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { + return nil + }) + defer restorer() + + // Attempt to run a hook on revision 41, which doesn't exist + _, err = snaprun.Parser().ParseArgs([]string{"run", "--hook=configure", "-r=41", "snapname"}) + c.Assert(err, check.NotNil) + c.Check(err, check.ErrorMatches, "cannot find .*") +} + +func (s *SnapSuite) TestSnapRunHookInvalidRevisionIntegration(c *check.C) { + _, err := snaprun.Parser().ParseArgs([]string{"run", "--hook=configure", "-r=invalid", "snapname"}) + c.Assert(err, check.NotNil) + c.Check(err, check.ErrorMatches, "invalid snap revision: \"invalid\"") +} + +func (s *SnapSuite) TestSnapRunHookMissingHookIntegration(c *check.C) { + // mock installed snap + dirs.SetRootDir(c.MkDir()) + defer func() { dirs.SetRootDir("/") }() + + // Only create revision 42 + si := snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{ + Revision: snap.R(42), + }) + err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current")) + c.Assert(err, check.IsNil) + + // redirect exec + called := false + restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { + called = true + return nil + }) + defer restorer() + + err = snaprun.SnapRunHook("snapname", "unset", "missing-hook") + c.Assert(err, check.ErrorMatches, `cannot find hook "missing-hook" in "snapname"`) + c.Check(called, check.Equals, false) +} + +func (s *SnapSuite) TestSnapRunErorsForUnknownRunArg(c *check.C) { + _, err := snaprun.Parser().ParseArgs([]string{"run", "--unknown", "snapname.app", "--arg1", "arg2"}) + c.Assert(err, check.ErrorMatches, "unknown flag `unknown'") +} + +func (s *SnapSuite) TestSnapRunErorsForMissingApp(c *check.C) { + _, err := snaprun.Parser().ParseArgs([]string{"run", "--command=shell"}) + c.Assert(err, check.ErrorMatches, "need the application to run as argument") +} + +func (s *SnapSuite) TestSnapRunErorrForUnavailableApp(c *check.C) { + _, err := snaprun.Parser().ParseArgs([]string{"run", "not-there"}) + c.Assert(err, check.ErrorMatches, fmt.Sprintf("cannot find current revision for snap not-there: readlink %s/not-there/current: no such file or directory", dirs.SnapMountDir)) +} + +func (s *SnapSuite) TestSnapRunSaneEnvironmentHandling(c *check.C) { + // mock installed snap + dirs.SetRootDir(c.MkDir()) + defer func() { dirs.SetRootDir("/") }() + defer mockSnapConfine(dirs.DistroLibExecDir)() + + si := snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{ + Revision: snap.R(42), + }) + err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current")) + c.Assert(err, check.IsNil) + + // redirect exec + execEnv := []string{} + restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { + execEnv = envv + return nil + }) + defer restorer() + + // set a SNAP{,_*} variable in the environment + os.Setenv("SNAP_NAME", "something-else") + os.Setenv("SNAP_ARCH", "PDP-7") + defer os.Unsetenv("SNAP_NAME") + defer os.Unsetenv("SNAP_ARCH") + // but unrelated stuff is ok + os.Setenv("SNAP_THE_WORLD", "YES") + defer os.Unsetenv("SNAP_THE_WORLD") + + // and ensure those SNAP_ vars get overridden + rest, err := snaprun.Parser().ParseArgs([]string{"run", "snapname.app", "--arg1", "arg2"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"}) + c.Check(execEnv, testutil.Contains, "SNAP_REVISION=42") + c.Check(execEnv, check.Not(testutil.Contains), "SNAP_NAME=something-else") + c.Check(execEnv, check.Not(testutil.Contains), "SNAP_ARCH=PDP-7") + c.Check(execEnv, testutil.Contains, "SNAP_THE_WORLD=YES") +} + +func (s *SnapSuite) TestSnapRunIsReexeced(c *check.C) { + var osReadlinkResult string + restore := snaprun.MockOsReadlink(func(name string) (string, error) { + return osReadlinkResult, nil + }) + defer restore() + + for _, t := range []struct { + readlink string + expected bool + }{ + {filepath.Join(dirs.SnapMountDir, dirs.CoreLibExecDir, "snapd"), true}, + {filepath.Join(dirs.DistroLibExecDir, "snapd"), false}, + } { + osReadlinkResult = t.readlink + c.Check(snaprun.IsReexeced(), check.Equals, t.expected) + } +} + +func (s *SnapSuite) TestSnapRunAppIntegrationFromCore(c *check.C) { + // mock installed snap + dirs.SetRootDir(c.MkDir()) + defer func() { dirs.SetRootDir("/") }() + defer mockSnapConfine(filepath.Join(dirs.SnapMountDir, "core", "current", dirs.CoreLibExecDir))() + + si := snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{ + Revision: snap.R("x2"), + }) + err := os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current")) + c.Assert(err, check.IsNil) + + // pretend to be running from core + restorer := snaprun.MockOsReadlink(func(string) (string, error) { + return filepath.Join(dirs.SnapMountDir, "core/111/usr/bin/snap"), nil + }) + defer restorer() + + // redirect exec + execArg0 := "" + execArgs := []string{} + execEnv := []string{} + restorer = snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { + execArg0 = arg0 + execArgs = args + execEnv = envv + return nil + }) + defer restorer() + + // and run it! + rest, err := snaprun.Parser().ParseArgs([]string{"run", "snapname.app", "--arg1", "arg2"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"}) + c.Check(execArg0, check.Equals, filepath.Join(dirs.SnapMountDir, "/core/current", dirs.CoreLibExecDir, "snap-confine")) + c.Check(execArgs, check.DeepEquals, []string{ + filepath.Join(dirs.SnapMountDir, "/core/current", dirs.CoreLibExecDir, "snap-confine"), + "snap.snapname.app", + filepath.Join(dirs.CoreLibExecDir, "snap-exec"), + "snapname.app", "--arg1", "arg2"}) + c.Check(execEnv, testutil.Contains, "SNAP_REVISION=x2") +} + +func (s *SnapSuite) TestSnapRunXauthorityMigration(c *check.C) { + // mock installed snap; happily this also gives us a directory + // below /tmp which the Xauthority migration expects. + dirs.SetRootDir(c.MkDir()) + defer func() { dirs.SetRootDir("/") }() + defer mockSnapConfine(dirs.DistroLibExecDir)() + + u, err := user.Current() + c.Assert(err, check.IsNil) + + // Ensure XDG_RUNTIME_DIR exists for the user we're testing with + err = os.MkdirAll(filepath.Join(dirs.XdgRuntimeDirBase, u.Uid), 0700) + c.Assert(err, check.IsNil) + + si := snaptest.MockSnap(c, string(mockYaml), string(mockContents), &snap.SideInfo{ + Revision: snap.R("x2"), + }) + err = os.Symlink(si.MountDir(), filepath.Join(si.MountDir(), "../current")) + c.Assert(err, check.IsNil) + + // redirect exec + execArg0 := "" + execArgs := []string{} + execEnv := []string{} + restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { + execArg0 = arg0 + execArgs = args + execEnv = envv + return nil + }) + defer restorer() + + xauthPath, err := x11.MockXauthority(2) + c.Assert(err, check.IsNil) + defer os.Remove(xauthPath) + + defer snaprun.MockGetEnv(func(name string) string { + if name == "XAUTHORITY" { + return xauthPath + } + return "" + })() + + // and run it! + rest, err := snaprun.Parser().ParseArgs([]string{"run", "snapname.app"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{"snapname.app"}) + c.Check(execArg0, check.Equals, filepath.Join(dirs.DistroLibExecDir, "snap-confine")) + c.Check(execArgs, check.DeepEquals, []string{ + filepath.Join(dirs.DistroLibExecDir, "snap-confine"), + "snap.snapname.app", + filepath.Join(dirs.CoreLibExecDir, "snap-exec"), + "snapname.app"}) + + expectedXauthPath := filepath.Join(dirs.XdgRuntimeDirBase, u.Uid, ".Xauthority") + c.Check(execEnv, testutil.Contains, fmt.Sprintf("XAUTHORITY=%s", expectedXauthPath)) + + info, err := os.Stat(expectedXauthPath) + c.Assert(err, check.IsNil) + c.Assert(info.Mode().Perm(), check.Equals, os.FileMode(0600)) + + err = x11.ValidateXauthorityFile(expectedXauthPath) + c.Assert(err, check.IsNil) +}