cmd/snap-{confine,update-ns}: apply mount profiles using snap-update-ns #3621

Merged
merged 97 commits into from Sep 23, 2017

Conversation

Projects
None yet
7 participants
Contributor

zyga commented Jul 25, 2017

This branch teaches snap-confine how to use snap-update-ns to apply the mount profile.

Over time I'd like to move more responsibility into fstab profiles and thus allow us to implement
layouts entirely through mount profiles. This brings us closer by allowing us to mutate only
one implementation. For now I kept the old implementation intact because I want to remove it
along with a cleanup of the appamor profile, which is non-trivial.

Because of the additional security impact, snap-update-ns has undergone a retroactive security
review and has been hardened. It now executes under a child apparmor profile (when started
by snap-confine) and has some modifications for better testability and integrity.

Signed-off-by: Zygmunt Krynicki zygmunt.krynicki@canonical.com

zyga added some commits Jul 25, 2017

cmd: adjust 'make hack' to use .real suffix
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/libsnap: add default value argument to getenv_bool
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/libsnap: add sc_is_reexec_enabled
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>

@zyga zyga changed the title from many: apply mount proifles using snap-update-ns to cmd/snap-{confine,update-ns}: apply mount proifles using snap-update-ns Jul 25, 2017

cmd/snap-update-ns: add support for --from-snap-confine option
This patch adds an option, to snap-update-ns, that indicates the caller
has is snap-confine and already locked the mount namespace and moved the
process to the right mount namespace. This will soon be used by
snap-confine to delegate part of the namespace construction to
snap-update-ns.

Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>

@zyga zyga changed the title from cmd/snap-{confine,update-ns}: apply mount proifles using snap-update-ns to cmd/snap-{confine,update-ns}: apply mount profiles using snap-update-ns Jul 25, 2017

cmd/snap-confine: use snap-update-ns as late initializer
This patch switches snap-confine to use an *unconfined* snap-update-ns
as a late initializer of the mount namespace. By "late" I mean after the
basic structure is put in place and the .fstab processing is about to
occurr. This essentially moves all of the understanding of .fstab files
to snap-update-ns, so that it can be evolved there in one go.

In case we cannot find snap-update-ns (for whatever reason) the old
code-path that uses the same logic implemented in C is left as-is.
It will be dropped in a subsequent patch, alongside with a cleanup
of the apparmor profile.

Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>

codecov-io commented Jul 26, 2017

Codecov Report

Merging #3621 into master will decrease coverage by <.01%.
The diff coverage is 53.84%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #3621      +/-   ##
==========================================
- Coverage   75.88%   75.88%   -0.01%     
==========================================
  Files         417      417              
  Lines       36194    36219      +25     
==========================================
+ Hits        27466    27483      +17     
- Misses       6802     6811       +9     
+ Partials     1926     1925       -1
Impacted Files Coverage Δ
cmd/snap-update-ns/main.go 0% <0%> (ø) ⬆️
cmd/snap-update-ns/bootstrap.go 86.04% <100%> (+16.04%) ⬆️
snap/validate.go 95.75% <100%> (+0.05%) ⬆️
cmd/snap/cmd_aliases.go 93.33% <0%> (-1.67%) ⬇️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 33ff99d...c32a9fe. Read the comment docs.

Comments from live chat.

cmd/snap-confine/mount-support.c
+ // Note that at this stage we _may_ be after pivot_root but we cannot rely
+ // on this. If we did pivot_root then the base snap _may_ contain the tool
+ // but we, again, cannot rely on that.
+ const char *const reexec_tools[] = {
@zyga

zyga Aug 21, 2017

Contributor

Let's keep an fd open (to snap-update-ns) and exec it this way. Also use /proc/self/exe to find the tool before.

cmd/snap-update-ns/bootstrap.c
@@ -82,13 +82,32 @@ find_snap_name(char* buf, size_t num_read)
if (argv0_len + 1 >= num_read) {
return NULL;
}
- char* snap_name = &buf[argv0_len + 1];
+ // Skip the --from-snap-confine option if we see one.
@zyga

zyga Aug 21, 2017

Contributor

Skip -option like arguments.

@zyga zyga added this to the 2.28 milestone Aug 28, 2017

zyga added some commits Aug 28, 2017

cmd/snap-confine: allow $hostfs/$snap_mount_dir
Apparmor maps back an open file description to a path and for whatever
reason it picks up the most unlikely mount point for the core snap, by
going through hostfs and then the core snap (instead of the "natural"
way by going throuh the core snap directly.

This fixes the following denial:

Aug 28 13:34:08 ubuntu kernel: audit: type=1400
audit(1503927248.610:29): apparmor="DENIED" operation="exec"
profile="/snap/core/2756/usr/lib/snapd/snap-confine"
name="/var/lib/snapd/hostfs/snap/core/2756/usr/lib/snapd/snap-update

In the future snap-confine should probably unmount /snap as seen through
hostfs so that each snap is mounted in exactly one location.

Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-update-ns: make option skipping logic more robust
The logic is now shorter and safer, it will also skip multiple options
if they happen to be passed. There's also one more test to check this
all works.

Signed-off-by: Zygmunt Krynicki <me@zygoon.pl>
cmd/snap-confine: call snap-update-ns via file descriptor
This patch significantly simplifies the logic for finding and running
snap-update-ns from snap-confine. Instead of elaborate checks we now
look at where snap-confine itself is (via /proc/self/exe) and then open
the associated snap-update-ns (with O_RDONLY|O_PATH|O_CLOEXEC|O_NOFOLLOW)
and then, after all the mount magic (pivot_root) we call that open
file descriptor with fexecve.

This allows us to drop the crazy "let me open this path", "or that one"
stat code. Thanks to Gustavo for the idea :-)

Signed-off-by: Zygmunt Krynicki <me@zygoon.pl>
cmd/snap-confine: drop the direct namespace setup code
This patch drops the direct namespace setup code. From now on
snap-confine will always use snap-update-ns for setting up the mount
namespace profiles.

This allows us to have just one logic, in snap-update-ns, that processes
profile code. This will make it easier to extend with new features in
the near future.

Signed-off-by: Zygmunt Krynicki <me@zygoon.pl>
tests: load updated apparmor profile of snap-confine
Signed-off-by: Zygmunt Krynicki <me@zygoon.pl>
tests: ensure snap-update-ns is repackaged
Signed-off-by: Zygmunt Krynicki <me@zygoon.pl>

@pedronis pedronis requested a review from jdstrand Aug 29, 2017

LGTM, just one question below.

@@ -396,4 +396,7 @@
# Allow snap-confine to be killed
signal (receive) peer=unconfined,
+
+ # Allow snap-confine to use snap-update-ns
+ /{var/lib/snapd/hostfs/,}{,{,var/lib/snapd/}snap/core/*/}usr/lib{,exec}/snapd/snap-update-ns Ux,
@stolowski

stolowski Aug 29, 2017

Contributor

I'm not clear about syntax of apparmor profiles here: it's clear /{var/lib/snapd/hostfs/,} is an obsolute path, but how about remaining paths?

@jdstrand

jdstrand Aug 29, 2017

Contributor

This rule is not clear. Can you enumerate the paths? Maybe we could then split the rule into two rules.

@zyga

zyga Aug 30, 2017

Contributor

This rule is a composition of the following possibilities:

  • /usr/lib/snapd/snap-update-ns
  • /usr/libexec/snapd/snap-update-ns
  • same as above but when using core snap mounted at /snap/core/*/
  • same as above but when using core snap mounted at /var/lib/snapd/snap/core/*
  • same as above but when all of those are visible through hostfs at /var/lib/snapd/hostfs
@jdstrand

jdstrand Aug 30, 2017

Contributor

From IRC, I think we should break this up a little to make it easier to audit:

// Allow executing snap-update-ns when:
// not re-execing
/usr/lib{,exec}/snapd/snap-update-ns Ux,
// when re-execing
/snap/core/*/usr/lib{,exec}/snapd/snap-update-ns Ux,
// when ???
/var/lib/snapd/{hostfs,snap/core/*}/usr/lib{,exec}/snapd-update-ns Ux,
@jdstrand

jdstrand Aug 30, 2017

Contributor

Note, if within the core snap, do we ever need lib{,exec}?

@zyga

zyga Aug 31, 2017

Contributor

I'll do something like this, the ??? case is actually always used in practice because when apparmor recovers the path for snap-update-ns this will be the path it chooses to use (/var/lib/snapd/hostfs/snap/core/*/usr/lib/snapd/snap-update). Seeing this makes me want to start to consider unmounting more hostfs things that we know are already present in the filesystem otherwise (like the whole /snap hierarchy).

@zyga

zyga Aug 31, 2017

Contributor

@stolowski all of the paths are absolute as the rule starts with /

Apparmor rules are just regular expressions coupled with some flags. Here the regular expression is using {a,b,c} as shell-like alternation syntax.

@jdstrand

jdstrand Aug 31, 2017

Contributor

Note they aren't regular expressions. They are AARE which is much more limited (see man apparmor.d for details).

I ran out of time before I could finish the PR, so commenting on what I have. What is left is bootstrap.c, bootstrap_exec.go and main.go.

cmd/Makefile.am
@@ -45,7 +45,7 @@ fmt: $(foreach dir,$(subdirs),$(wildcard $(srcdir)/$(dir)/*.[ch]))
.PHONY: hack
hack: snap-confine/snap-confine snap-confine/snap-confine.apparmor snap-update-ns/snap-update-ns
sudo install -D -m 4755 snap-confine/snap-confine $(DESTDIR)$(libexecdir)/snap-confine
- sudo install -m 644 snap-confine/snap-confine.apparmor $(DESTDIR)/etc/apparmor.d/$(patsubst .%,%,$(subst /,.,$(libexecdir))).snap-confine
+ sudo install -m 644 snap-confine/snap-confine.apparmor $(DESTDIR)/etc/apparmor.d/$(patsubst .%,%,$(subst /,.,$(libexecdir))).snap-confine.real
@jdstrand

jdstrand Aug 30, 2017

Contributor

This change seems unrelated. It seems fine though.

@zyga

zyga Aug 31, 2017

Contributor

Yes, this just fixes make hack.

cmd/libsnap-confine-private/utils.c
@@ -99,10 +99,10 @@ static int str2bool(const char *text, bool * value)
* and "0". All other values are treated as false and a diagnostic message is
* printed to stderr.
**/
-static bool getenv_bool(const char *name)
+static bool getenv_bool(const char *name, bool default_value)
@jdstrand

jdstrand Aug 30, 2017

Contributor

Can you add a comment about default_value? It wasn't clear why you added it since str2bool only assigns to &value, but you are doing this to return true when something is not set. I think default_value is a bit of a misnomer here in terms of the API and should probably be named inverted, but inverted doesn't really go with the implementation. Maybe it would be better to not have 'default_value' at all and instead use:

bool sc_is_reexec_enabled() {
    return !getenv_bool("SNAP_REEXEC");
}
@zyga

zyga Aug 31, 2017

Contributor

Ah, nice catch wrt default_value and how str2bool operates, I missed that. Let me tweak that.

@zyga

zyga Sep 1, 2017

Contributor

I reworked the default value, fixed the bug, added testes and documented how it operates.

cmd/snap-confine/mount-support.c
- **/
-static void sc_setup_mount_profiles(const char *snap_name)
+static void sc_setup_mount_profiles(int snap_update_ns_fd,
+ const char *snap_name)
@jdstrand

jdstrand Aug 30, 2017

Contributor

A comment here talking about snap_update_ns_fd and what snap-update-ns will do on our behalf would be great here.

@zyga

zyga Aug 31, 2017

Contributor

Ack, will add

@zyga

zyga Sep 1, 2017

Contributor

Done

cmd/snap-confine/mount-support.c
+ "snap-update-ns", "--from-snap-confine", snap_name_copy,
+ NULL
+ };
+ char *envp[] = { "SNAP_CONFINE_DEBUG=yes", NULL };
@jdstrand

jdstrand Aug 30, 2017

Contributor

Why are you setting this unconditionally rather than setting this conditional on getenv_bool()?

@zyga

zyga Aug 31, 2017

Contributor

Ouch, that's a debug-leftover :)

@zyga

zyga Sep 1, 2017

Contributor

Done

cmd/snap-confine/mount-support.c
+ };
+ char *envp[] = { "SNAP_CONFINE_DEBUG=yes", NULL };
+ debug("fexecv(%d, %s %s %s,)", snap_update_ns_fd, argv[0],
+ argv[1], argv[2]);
@jdstrand

jdstrand Aug 30, 2017

Contributor

This debug statement is, perhaps, not as clear as it could be. Perhaps this instead:

debug("fexecv(%d (snap-update-ns), %s %s %s,)", snap_update_ns_fd, argv[0], argv[1], argv[2]);
@zyga

zyga Sep 1, 2017

Contributor

Done

cmd/snap-confine/mount-support.c
+ argv[1], argv[2]);
+ fexecve(snap_update_ns_fd, argv, envp);
+ die("cannot execute snap-update-ns");
+ } else {
@jdstrand

jdstrand Aug 30, 2017

Contributor

No need for an else here. fexecve() either never returns or if it does with error, you call die().

@zyga

zyga Aug 31, 2017

Contributor

Hmm? This else is for the parent process.

@jdstrand

jdstrand Aug 31, 2017

Contributor

Yes. Because the parent won't enter the child's 'if' branch and the child never leaves the 'if' branch, you don't need the else.

@zyga

zyga Sep 1, 2017

Contributor

Ah, I see your point now. Changed

cmd/snap-confine/mount-support.c
+ int status = 0;
+ debug("waiting for snap-update-ns to finish...");
+ if (waitpid(child, &status, 0) < 0) {
+ die("cannot wait for snap-update-ns process");
@jdstrand

jdstrand Aug 30, 2017

Contributor

die("waitpid() failed for snap-update-ns process"); maybe?

@zyga

zyga Sep 1, 2017

Contributor

Done

cmd/snap-confine/mount-support.c
- if (addmntent(current, m) != 0) { // NOTE: returns 1 on error.
- die("cannot append entry to the current mount profile");
+ if (WIFEXITED(status)) {
+ if (WEXITSTATUS(status) != 0) {
@jdstrand

jdstrand Aug 30, 2017

Contributor

This is simpler:

if (WIFEXITED(status) && WEXITSTATUS(status) != 0) {
    die("snap-update-ns failed with code %d", WEXITSTATUS(status));
}

Also, while equivalent for our usage, we are using '%i' instead of '%d' in udev-support.c for printing the status. Not a blocker.

@zyga

zyga Sep 1, 2017

Contributor

Done, also the one below.

cmd/snap-confine/mount-support.c
+ WEXITSTATUS(status));
+ }
+ } else {
+ die("snap-update-ns failed abnormally");
@jdstrand

jdstrand Aug 30, 2017

Contributor

Instead of this, I suggest:

} else if (WIFSIGNALED(status)) {
    die("child died with signal %i", WTERMSIG(status));
}

This is sufficient with the options we pass to waitpid() (man waitpid).

@zyga

zyga Sep 1, 2017

Contributor

Right, I was wondering if we should be this precise. Done now.

cmd/snap-confine/mount-support.c
@@ -502,6 +476,29 @@ static bool __attribute__ ((used))
return false;
}
+static int sc_open_snap_update_ns()
+{
+ char buf[PATH_MAX] = { 0 };
@jdstrand

jdstrand Aug 30, 2017

Contributor

Please account for terminating null in case length of /proc/self/exe is PATH_MAX:

char buf[PATH_MAX+1] = { 0 };
@zyga

zyga Sep 1, 2017

Contributor

Done. I'm worried now, should all path handling code use PATH_MAX + 1 buffers?

@jdstrand

jdstrand Sep 6, 2017

Contributor

It depends on the api used. readlink() doesn't add a trailing '\0' so we need to account for it using all of buf, and then we come along and tack on '\0'. Because we are using sc_must_snprintf() down below, what you had wasn't a security issue-- sc_must_sprintf() would've die()d-- but this way in the off chance of a super long symlink that is exactly PATH_MAX, it won't die() unnecessarily.

cmd/snap-confine/mount-support.c
+static int sc_open_snap_update_ns()
+{
+ char buf[PATH_MAX] = { 0 };
+ if (readlink("/proc/self/exe", buf, sizeof buf) < 0) {
@jdstrand

jdstrand Aug 30, 2017

Contributor

Please add a comment stating:

// readlink doesn't add terminating null, but our initialization of
// buf handles this for us. 
@zyga

zyga Sep 1, 2017

Contributor

Done

cmd/snap-confine/mount-support.c
+ if (s == NULL) {
+ die("cannot find trailing forward slash in %s", buf);
+ }
+ s += 1;
@jdstrand

jdstrand Aug 30, 2017

Contributor

Perhaps:

s += 1; // advance pointer to basename of exe
@zyga

zyga Aug 31, 2017

Contributor

Done

cmd/snap-confine/mount-support.c
+ die("cannot find trailing forward slash in %s", buf);
+ }
+ s += 1;
+ sc_must_snprintf(s, sizeof buf - (s - buf), "snap-update-ns");
@jdstrand

jdstrand Aug 30, 2017

Contributor

This is unclear and an abuse of the API since you aren't giving a format string. I think something like this would be clearer:

#include <libgen.h>
...
	if (readlink("/proc/self/exe", buf, sizeof buf) < 0) {
		die("cannot readlink /proc/self/exe");
	}
	if (buf[0] != '/') { // this shouldn't happen, but make sure have absolute path
		die("readlink not an absolute path");
	}
	char *bufcopy __attribute__ ((cleanup(sc_cleanup_string))) = NULL;
	bufcopy = strdup(buf);
	char *dname = dirname(bufcopy);
	sc_must_snprintf(buf, sizeof buf, "%s/%s", dname, "snap-update-ns");
@zyga

zyga Aug 31, 2017

Contributor

Woah, I didn't mean to pass s as the format string. I just wanted to strcpy at s with the length limit intact. I'll rework this.

@zyga

zyga Sep 1, 2017

Contributor

Done, with slight tweaks.

cmd/snap-confine/mount-support.c
@@ -512,6 +509,12 @@ void sc_populate_mount_ns(const char *base_snap_name, const char *snap_name)
if (vanilla_cwd == NULL) {
die("cannot get the current working directory");
}
+ // Find and open our "peer" snap-update-ns executable. The "peer" in the
+ // sense that the executable is from the same release as ourselves,
+ // regardless of which re-exec events occurred earlier.
@jdstrand

jdstrand Aug 30, 2017

Contributor

Perhaps rephrase as:

// Find and open snap-update-ns from the same filesystem as where we (snap-confine)
// were called.
@zyga

zyga Sep 1, 2017

Contributor

Applied.

@@ -396,4 +396,7 @@
# Allow snap-confine to be killed
signal (receive) peer=unconfined,
+
+ # Allow snap-confine to use snap-update-ns
+ /{var/lib/snapd/hostfs/,}{,{,var/lib/snapd/}snap/core/*/}usr/lib{,exec}/snapd/snap-update-ns Ux,
@stolowski

stolowski Aug 29, 2017

Contributor

I'm not clear about syntax of apparmor profiles here: it's clear /{var/lib/snapd/hostfs/,} is an obsolute path, but how about remaining paths?

@jdstrand

jdstrand Aug 29, 2017

Contributor

This rule is not clear. Can you enumerate the paths? Maybe we could then split the rule into two rules.

@zyga

zyga Aug 30, 2017

Contributor

This rule is a composition of the following possibilities:

  • /usr/lib/snapd/snap-update-ns
  • /usr/libexec/snapd/snap-update-ns
  • same as above but when using core snap mounted at /snap/core/*/
  • same as above but when using core snap mounted at /var/lib/snapd/snap/core/*
  • same as above but when all of those are visible through hostfs at /var/lib/snapd/hostfs
@jdstrand

jdstrand Aug 30, 2017

Contributor

From IRC, I think we should break this up a little to make it easier to audit:

// Allow executing snap-update-ns when:
// not re-execing
/usr/lib{,exec}/snapd/snap-update-ns Ux,
// when re-execing
/snap/core/*/usr/lib{,exec}/snapd/snap-update-ns Ux,
// when ???
/var/lib/snapd/{hostfs,snap/core/*}/usr/lib{,exec}/snapd-update-ns Ux,
@jdstrand

jdstrand Aug 30, 2017

Contributor

Note, if within the core snap, do we ever need lib{,exec}?

@zyga

zyga Aug 31, 2017

Contributor

I'll do something like this, the ??? case is actually always used in practice because when apparmor recovers the path for snap-update-ns this will be the path it chooses to use (/var/lib/snapd/hostfs/snap/core/*/usr/lib/snapd/snap-update). Seeing this makes me want to start to consider unmounting more hostfs things that we know are already present in the filesystem otherwise (like the whole /snap hierarchy).

@zyga

zyga Aug 31, 2017

Contributor

@stolowski all of the paths are absolute as the rule starts with /

Apparmor rules are just regular expressions coupled with some flags. Here the regular expression is using {a,b,c} as shell-like alternation syntax.

@jdstrand

jdstrand Aug 31, 2017

Contributor

Note they aren't regular expressions. They are AARE which is much more limited (see man apparmor.d for details).

@@ -396,4 +396,7 @@
# Allow snap-confine to be killed
signal (receive) peer=unconfined,
+
+ # Allow snap-confine to use snap-update-ns
+ /{var/lib/snapd/hostfs/,}{,{,var/lib/snapd/}snap/core/*/}usr/lib{,exec}/snapd/snap-update-ns Ux,
@jdstrand

jdstrand Aug 30, 2017

Contributor

Why is this Ux? I thought we talked about this, but I thought we were going transition to another profile.... This could be done conveniently with a child profile without using libapparmor in snap-confine.

@zyga

zyga Aug 31, 2017

Contributor

This is Ux because we don't have any profile to transition to yet. Generation of snap-specific profiles for snap-update-ns is further down the line.

@zyga

zyga Aug 31, 2017

Contributor

Note that this is not any less safe than before:

  • snap-confine no longer performs those mounts
  • snap-confine runs unconfined snap-update-ns
    (which always ran unconfined when invoked from snapd)
  • snap-update-ns is not written in C, thus harder to attack and reads only files written by root (snapd)
@jdstrand

jdstrand Aug 31, 2017

Contributor

I disagree about 'not any less safe'. Because it is written in Go it potentially isn't as big of an attack surface but because snap-confine is setuid, it calls snap-update-ns with elevated privileges and therefore the user is able to call snap-update-ns with elevated privileges through snap-confine. Coding errors could therefore allow unintended access. The snapd calling snap-update-ns is different: snapd is not called by the user and it is invoked in a safe environment.

Today we have most of the rules in snap-confine already since snap-confine and snap-update-ns overlapped. As part of this PR, please add confinement for snap-update-ns. I know long term you have ideas about per-snap profiles, but we can at least not regress on confinement by doing something simple like:

/usr/lib/snapd/snap-confine (attach_disconnected) {
    ... remove mount rules no longer needed by snap-confine ...
    ...
    // Allow executing snap-update-ns when:
    // not re-execing
    /usr/lib{,exec}/snapd/snap-update-ns Cxr -> snap_update_ns,
    // when re-execing
    /snap/core/*/usr/lib{,exec}/snapd/snap-update-ns Cxr -> snap_update_ns,
    // when ???
    /var/lib/snapd/{hostfs,snap/core/*}/usr/lib{,exec}/snapd-update-ns Cxr -> snap_update_ns,
    ...
    profile snap_update_ns (attach_disconnected) {
        ...
        <mount rules previously in snap-confine>
        ...
    }

With this, you need no code changes to your PR. The profiling for the child profile will also inform your future work's rules, so it isn't wasted.

@jdstrand

jdstrand Aug 31, 2017

Contributor

"because snap-confine is setuid, it calls snap-update-ns with elevated privileges and therefore the user is able to call snap-update-ns with elevated privileges through snap-confine. Coding errors could therefore allow unintended access. The snapd calling snap-update-ns is different: snapd is not called by the user and it is invoked in a safe environment."

This reminds me of an important comment in my response in the forum that was not addressed:

"I do have concerns though that we are changing the assumptions of snap-update-ns; specifically that it was written with the understanding that it would be launched under snapd which is not an attack vector for privilege escalation. Now it would not only be launched from snapd but also from a setuid executable and now therefore be an attack vector for those trying to escalate privileges via implementation bugs in snap-confine/snap-update-ns. Put another way, while this makes snap-confine smaller, the attack surface is not actually reduced (it is even bigger considering snap-update-ns' imports (we tried very hard to limit the number of libraries snap-confine uses (best practice for writing setuid applications)). Because snap-update-ns will be running in the context of a setuid application, the lines within the codebase between what is running setuid becomes muddy (eg, today it is pretty easy to know when to ask the security team for reviews: "is the code in cmd/snap-confine changed?". This becomes difficult and easy to miss when to ask for security reviews-- eg, snap-update-ns imports "github.com/snapcore/snapd/logger" which all of a sudden will now at times run under setuid-- who will remember to ask the security team to do a review of changes to logger?). Furthermore, snap-update-ns and everything it imports will now need a proper security review. With the rapid pace of development of the go code within snappy, it is easy to imagine introducing security flaws....
"

I'll put it more simply. Here are the imports for things not part of the standard library:

cmd/snap-update-ns/main.go imports:

  • "github.com/jessevdk/go-flags"
  • "github.com/snapcore/snapd/dirs" which imports:
    • "github.com/snapcore/snapd/release" which imports:
      • "github.com/snapcore/snapd/apparmor"
  • "github.com/snapcore/snapd/interfaces/mount" which imports:
    • "github.com/snapcore/snapd/dirs" (via backend.go, lock.go, ns.go)
    • "github.com/snapcore/snapd/interfaces" (via backend.go, spec.go) which imports:
      • "github.com/snapcore/snapd/snap" (via backend.go, core.go, naming.go, repo.go, sorting.go) which imports:
        • "github.com/snapcore/snapd/dirs" (via broken.go, info.go)
        • "github.com/snapcore/snapd/osutil" (via container.go, implicit.go, seed_yaml.go)
        • "github.com/snapcore/snapd/snap/snapdir" (via container.go)
        • "github.com/snapcore/snapd/snap/squashfs" (via container.go) which imports:
          • "github.com/snapcore/snapd/osutil"
        • "gopkg.in/yaml.v2" (via gadget.go, info_snap_yaml.go)
        • "github.com/snapcore/snapd/strutil" (via info.go, info_snap_yaml.go) which imports:
          • "gopkg.in/yaml.v2" (via map.go)
        • "github.com/snapcore/snapd/timeout" (via info.go, info_snap_yaml.go)
    • "github.com/snapcore/snapd/osutil" (via backend.go, lock.go, ns.go, profile.go) which imports:
      • "github.com/snapcore/snapd/dirs" (via exec.go)
      • "github.com/snapcore/snapd/strutil" (via io.go)
    • "github.com/snapcore/snapd/snap" (via backend.go)
    • "github.com/snapcore/snapd/cmd" (via ns.go) which imports:
      • "github.com/snapcore/snapd/dirs"
      • "github.com/snapcore/snapd/logger" which imports:
        • "github.com/snapcore/snapd/osutil"
      • "github.com/snapcore/snapd/osutil"
      • "github.com/snapcore/snapd/release"
      • "github.com/snapcore/snapd/strutil"
  • "github.com/snapcore/snapd/logger"
  • "github.com/snapcore/snapd/snap"

That's a lot of code and I bet none of it was written with setuidness in mind. Obviously all of it isn't being used by snap-update-ns, but it is all in the address space and therefore available with certain types of memory attacks (this is why in the snap-confine profile we limit the libraries (what is in the address space) to load instead of simply allowing /usr/lib/*.so). I don't have time (today) to verify each API snap-update-ns uses and verify that all the code paths are safe under setuid, which perfectly illustrates my point: snap-update-ns is using too many external libs for a setuid application and even if we did audit them all, these imported packages are getting updates all the time. setuid applications should be small and should not change much because the changes might change assumptions in other parts of the code (we finally got to the point where we aren't modifying snap-confine weekly-- a good thing!). AppArmor confinement mitigates that to some degree (the snap-confine profile has a good bit of privilege as it is), but on systems without AppArmor, this is a pretty big attack surface.

I know the feature is important and I think we can move forward if we:

  • cut down on the imports in snap-update-ns (this might involve shuffling some things into cmd/snap-update-ns for other code to import from there rather than having snap-update-ns import from elsewhere)
  • adding a comment to the import list of snap-update-ns stating that we are intentionally keeping it small for security reasons
  • getting security team involvement in cmd/snap-update-ns PRs. This does not necessarily require deep reviews from the security team (ie, the security team is at liberty to perform a shallow review stating a deep review is not required)

@zyga, can you comment on this?

@jdstrand

jdstrand Sep 1, 2017

Contributor

This is an interesting idea. So long as that API is very controlled and does extensive input validation, this would work fine. It will likely add some overhead over a fork/exec that would need to be considered.

@jdstrand

jdstrand Sep 1, 2017

Contributor

FYI, here is the exploratory PR for reducing the imports in snap-update-ns: #3840

@jdstrand

jdstrand Sep 1, 2017

Contributor

@niemeyer and @zyga: I've thought through the security implications of snap-confine fork/execing snap-update-ns with minimized imports (eg, finishing #3840) vs @zyga's idea of having snap-confine ask snapd to call snap-update-ns on its behalf.

In terms of security, I don't think there is an appreciable difference with either approach (assuming we get things right, which I'll of course help with).

Outside of security, the fork/exec is simpler to understand and less overhead, but requires the security team have some involvement in changes to snap-update-ns. The IPC approach allows snap-update-ns to be updated more easily (eg, without mandatory security team involvement) since it will always be invoked from a safe environment, but it does mean snapd must be running for anything to execute, which might affect snaps starting on boot before snapd, launching while snapd is restarting, etc.

I'll leave the decision to which approach to pursue up to the snapd team. If you still want to discuss in a meeting with me, I'm available.

@jdstrand

jdstrand Sep 1, 2017

Contributor

After discussing with @niemeyer and @zyga, there is another option which should mitigate my concerns.

If we consider the situation where there is a bug in snap-confine that allows the unprivileged user to call the snap-update-ns executable as root in arbitrary ways, since snap-confine is calling snap-update-ns via its command line interface, the extent of direct influence over snap-update-ns is limited to what the snap-update-ns cli exposes. Beyond that there is the matter of how snap-confine can influence snap-update-ns indirectly which leaves the environment and the mount profiles. With that in mind, the plan of action is:

  • adjust snap-update-ns to unconditionally and immediately clear its environment upon its invocation (optionally adding back whitelisted variables in the future if required). It shall die() on error. There shall be comments in this part of the code stating that changes need a security review.
  • snap-update-ns cli argument parsing shall be hardened for processing malicious input and reviewed by the security team. There shall be comments in this part of the code stating that changes need a security review.
  • write a restrictive child AppArmor profile for snap-update-ns and allow snap-confine to call snap-update-ns only under that profile and with env scrubbing (ie, 'Cx')
  • ensure that the snap-confine AppArmor profile denies writes to mount profiles (this should already be handled)
  • ensure snap-confine is closing unneeded file descriptors before invoking snap-update-ns (ie use O_CLOEXEC for anything that isn't being passed)
  • in addition to snap-confine changes, the security team shall going forward audit changes to the snap-confine and snap-update-ns AppArmor profiles, changes to snap-update-ns environment cleaning (and future whitelisting) and snap-update-ns command line parsing code

On an AppArmor enabled system where snap-confine is confined, the above mitigates the concerns I have since snap-update-ns will be hardened and security-reviewed on its inputs (cli and environment) and snap-confine will be coded to not leak file descriptors and be confined to not allow updating the mount profiles.

On a system without AppArmor, a user-controlled snap-confine is in a position to modify the mount profiles to trigger bugs in the snap-update-ns code. This is true today however with a delayed attack: if snap-confine can change the mount profiles, snapd will happily at some future point call snap-update-ns on them. Obviously we desire to have robust mount profile parsing code, but since it is more likely that a malicious user exploiting bugs in snap-confine would manipulate mount points directly to carry out attacks (or modify the snap-update-ns binary or shell out if the exploit allowed it or ...), we don't need to put extra security reviews on mount profile parsing code.

@jdstrand

jdstrand Sep 1, 2017

Contributor

In addition to the security reviews, I've agreed to implementing the apparmor profile changes for snap-confine and snap-update-ns.

I'm working on addressing all the feedback from this.

cmd/snap-confine/mount-support.c
+ argv[1], argv[2]);
+ fexecve(snap_update_ns_fd, argv, envp);
+ die("cannot execute snap-update-ns");
+ } else {
@jdstrand

jdstrand Aug 30, 2017

Contributor

No need for an else here. fexecve() either never returns or if it does with error, you call die().

@zyga

zyga Aug 31, 2017

Contributor

Hmm? This else is for the parent process.

@jdstrand

jdstrand Aug 31, 2017

Contributor

Yes. Because the parent won't enter the child's 'if' branch and the child never leaves the 'if' branch, you don't need the else.

@zyga

zyga Sep 1, 2017

Contributor

Ah, I see your point now. Changed

cmd/snap-confine/mount-support.c
+ if (s == NULL) {
+ die("cannot find trailing forward slash in %s", buf);
+ }
+ s += 1;
@jdstrand

jdstrand Aug 30, 2017

Contributor

Perhaps:

s += 1; // advance pointer to basename of exe
@zyga

zyga Aug 31, 2017

Contributor

Done

cmd/snap-confine/mount-support.c
+ die("cannot find trailing forward slash in %s", buf);
+ }
+ s += 1;
+ sc_must_snprintf(s, sizeof buf - (s - buf), "snap-update-ns");
@jdstrand

jdstrand Aug 30, 2017

Contributor

This is unclear and an abuse of the API since you aren't giving a format string. I think something like this would be clearer:

#include <libgen.h>
...
	if (readlink("/proc/self/exe", buf, sizeof buf) < 0) {
		die("cannot readlink /proc/self/exe");
	}
	if (buf[0] != '/') { // this shouldn't happen, but make sure have absolute path
		die("readlink not an absolute path");
	}
	char *bufcopy __attribute__ ((cleanup(sc_cleanup_string))) = NULL;
	bufcopy = strdup(buf);
	char *dname = dirname(bufcopy);
	sc_must_snprintf(buf, sizeof buf, "%s/%s", dname, "snap-update-ns");
@zyga

zyga Aug 31, 2017

Contributor

Woah, I didn't mean to pass s as the format string. I just wanted to strcpy at s with the length limit intact. I'll rework this.

@zyga

zyga Sep 1, 2017

Contributor

Done, with slight tweaks.

@@ -396,4 +396,7 @@
# Allow snap-confine to be killed
signal (receive) peer=unconfined,
+
+ # Allow snap-confine to use snap-update-ns
+ /{var/lib/snapd/hostfs/,}{,{,var/lib/snapd/}snap/core/*/}usr/lib{,exec}/snapd/snap-update-ns Ux,
@stolowski

stolowski Aug 29, 2017

Contributor

I'm not clear about syntax of apparmor profiles here: it's clear /{var/lib/snapd/hostfs/,} is an obsolute path, but how about remaining paths?

@jdstrand

jdstrand Aug 29, 2017

Contributor

This rule is not clear. Can you enumerate the paths? Maybe we could then split the rule into two rules.

@zyga

zyga Aug 30, 2017

Contributor

This rule is a composition of the following possibilities:

  • /usr/lib/snapd/snap-update-ns
  • /usr/libexec/snapd/snap-update-ns
  • same as above but when using core snap mounted at /snap/core/*/
  • same as above but when using core snap mounted at /var/lib/snapd/snap/core/*
  • same as above but when all of those are visible through hostfs at /var/lib/snapd/hostfs
@jdstrand

jdstrand Aug 30, 2017

Contributor

From IRC, I think we should break this up a little to make it easier to audit:

// Allow executing snap-update-ns when:
// not re-execing
/usr/lib{,exec}/snapd/snap-update-ns Ux,
// when re-execing
/snap/core/*/usr/lib{,exec}/snapd/snap-update-ns Ux,
// when ???
/var/lib/snapd/{hostfs,snap/core/*}/usr/lib{,exec}/snapd-update-ns Ux,
@jdstrand

jdstrand Aug 30, 2017

Contributor

Note, if within the core snap, do we ever need lib{,exec}?

@zyga

zyga Aug 31, 2017

Contributor

I'll do something like this, the ??? case is actually always used in practice because when apparmor recovers the path for snap-update-ns this will be the path it chooses to use (/var/lib/snapd/hostfs/snap/core/*/usr/lib/snapd/snap-update). Seeing this makes me want to start to consider unmounting more hostfs things that we know are already present in the filesystem otherwise (like the whole /snap hierarchy).

@zyga

zyga Aug 31, 2017

Contributor

@stolowski all of the paths are absolute as the rule starts with /

Apparmor rules are just regular expressions coupled with some flags. Here the regular expression is using {a,b,c} as shell-like alternation syntax.

@jdstrand

jdstrand Aug 31, 2017

Contributor

Note they aren't regular expressions. They are AARE which is much more limited (see man apparmor.d for details).

@@ -396,4 +396,7 @@
# Allow snap-confine to be killed
signal (receive) peer=unconfined,
+
+ # Allow snap-confine to use snap-update-ns
+ /{var/lib/snapd/hostfs/,}{,{,var/lib/snapd/}snap/core/*/}usr/lib{,exec}/snapd/snap-update-ns Ux,
@jdstrand

jdstrand Aug 30, 2017

Contributor

Why is this Ux? I thought we talked about this, but I thought we were going transition to another profile.... This could be done conveniently with a child profile without using libapparmor in snap-confine.

@zyga

zyga Aug 31, 2017

Contributor

This is Ux because we don't have any profile to transition to yet. Generation of snap-specific profiles for snap-update-ns is further down the line.

@zyga

zyga Aug 31, 2017

Contributor

Note that this is not any less safe than before:

  • snap-confine no longer performs those mounts
  • snap-confine runs unconfined snap-update-ns
    (which always ran unconfined when invoked from snapd)
  • snap-update-ns is not written in C, thus harder to attack and reads only files written by root (snapd)
@jdstrand

jdstrand Aug 31, 2017

Contributor

I disagree about 'not any less safe'. Because it is written in Go it potentially isn't as big of an attack surface but because snap-confine is setuid, it calls snap-update-ns with elevated privileges and therefore the user is able to call snap-update-ns with elevated privileges through snap-confine. Coding errors could therefore allow unintended access. The snapd calling snap-update-ns is different: snapd is not called by the user and it is invoked in a safe environment.

Today we have most of the rules in snap-confine already since snap-confine and snap-update-ns overlapped. As part of this PR, please add confinement for snap-update-ns. I know long term you have ideas about per-snap profiles, but we can at least not regress on confinement by doing something simple like:

/usr/lib/snapd/snap-confine (attach_disconnected) {
    ... remove mount rules no longer needed by snap-confine ...
    ...
    // Allow executing snap-update-ns when:
    // not re-execing
    /usr/lib{,exec}/snapd/snap-update-ns Cxr -> snap_update_ns,
    // when re-execing
    /snap/core/*/usr/lib{,exec}/snapd/snap-update-ns Cxr -> snap_update_ns,
    // when ???
    /var/lib/snapd/{hostfs,snap/core/*}/usr/lib{,exec}/snapd-update-ns Cxr -> snap_update_ns,
    ...
    profile snap_update_ns (attach_disconnected) {
        ...
        <mount rules previously in snap-confine>
        ...
    }

With this, you need no code changes to your PR. The profiling for the child profile will also inform your future work's rules, so it isn't wasted.

@jdstrand

jdstrand Aug 31, 2017

Contributor

"because snap-confine is setuid, it calls snap-update-ns with elevated privileges and therefore the user is able to call snap-update-ns with elevated privileges through snap-confine. Coding errors could therefore allow unintended access. The snapd calling snap-update-ns is different: snapd is not called by the user and it is invoked in a safe environment."

This reminds me of an important comment in my response in the forum that was not addressed:

"I do have concerns though that we are changing the assumptions of snap-update-ns; specifically that it was written with the understanding that it would be launched under snapd which is not an attack vector for privilege escalation. Now it would not only be launched from snapd but also from a setuid executable and now therefore be an attack vector for those trying to escalate privileges via implementation bugs in snap-confine/snap-update-ns. Put another way, while this makes snap-confine smaller, the attack surface is not actually reduced (it is even bigger considering snap-update-ns' imports (we tried very hard to limit the number of libraries snap-confine uses (best practice for writing setuid applications)). Because snap-update-ns will be running in the context of a setuid application, the lines within the codebase between what is running setuid becomes muddy (eg, today it is pretty easy to know when to ask the security team for reviews: "is the code in cmd/snap-confine changed?". This becomes difficult and easy to miss when to ask for security reviews-- eg, snap-update-ns imports "github.com/snapcore/snapd/logger" which all of a sudden will now at times run under setuid-- who will remember to ask the security team to do a review of changes to logger?). Furthermore, snap-update-ns and everything it imports will now need a proper security review. With the rapid pace of development of the go code within snappy, it is easy to imagine introducing security flaws....
"

I'll put it more simply. Here are the imports for things not part of the standard library:

cmd/snap-update-ns/main.go imports:

  • "github.com/jessevdk/go-flags"
  • "github.com/snapcore/snapd/dirs" which imports:
    • "github.com/snapcore/snapd/release" which imports:
      • "github.com/snapcore/snapd/apparmor"
  • "github.com/snapcore/snapd/interfaces/mount" which imports:
    • "github.com/snapcore/snapd/dirs" (via backend.go, lock.go, ns.go)
    • "github.com/snapcore/snapd/interfaces" (via backend.go, spec.go) which imports:
      • "github.com/snapcore/snapd/snap" (via backend.go, core.go, naming.go, repo.go, sorting.go) which imports:
        • "github.com/snapcore/snapd/dirs" (via broken.go, info.go)
        • "github.com/snapcore/snapd/osutil" (via container.go, implicit.go, seed_yaml.go)
        • "github.com/snapcore/snapd/snap/snapdir" (via container.go)
        • "github.com/snapcore/snapd/snap/squashfs" (via container.go) which imports:
          • "github.com/snapcore/snapd/osutil"
        • "gopkg.in/yaml.v2" (via gadget.go, info_snap_yaml.go)
        • "github.com/snapcore/snapd/strutil" (via info.go, info_snap_yaml.go) which imports:
          • "gopkg.in/yaml.v2" (via map.go)
        • "github.com/snapcore/snapd/timeout" (via info.go, info_snap_yaml.go)
    • "github.com/snapcore/snapd/osutil" (via backend.go, lock.go, ns.go, profile.go) which imports:
      • "github.com/snapcore/snapd/dirs" (via exec.go)
      • "github.com/snapcore/snapd/strutil" (via io.go)
    • "github.com/snapcore/snapd/snap" (via backend.go)
    • "github.com/snapcore/snapd/cmd" (via ns.go) which imports:
      • "github.com/snapcore/snapd/dirs"
      • "github.com/snapcore/snapd/logger" which imports:
        • "github.com/snapcore/snapd/osutil"
      • "github.com/snapcore/snapd/osutil"
      • "github.com/snapcore/snapd/release"
      • "github.com/snapcore/snapd/strutil"
  • "github.com/snapcore/snapd/logger"
  • "github.com/snapcore/snapd/snap"

That's a lot of code and I bet none of it was written with setuidness in mind. Obviously all of it isn't being used by snap-update-ns, but it is all in the address space and therefore available with certain types of memory attacks (this is why in the snap-confine profile we limit the libraries (what is in the address space) to load instead of simply allowing /usr/lib/*.so). I don't have time (today) to verify each API snap-update-ns uses and verify that all the code paths are safe under setuid, which perfectly illustrates my point: snap-update-ns is using too many external libs for a setuid application and even if we did audit them all, these imported packages are getting updates all the time. setuid applications should be small and should not change much because the changes might change assumptions in other parts of the code (we finally got to the point where we aren't modifying snap-confine weekly-- a good thing!). AppArmor confinement mitigates that to some degree (the snap-confine profile has a good bit of privilege as it is), but on systems without AppArmor, this is a pretty big attack surface.

I know the feature is important and I think we can move forward if we:

  • cut down on the imports in snap-update-ns (this might involve shuffling some things into cmd/snap-update-ns for other code to import from there rather than having snap-update-ns import from elsewhere)
  • adding a comment to the import list of snap-update-ns stating that we are intentionally keeping it small for security reasons
  • getting security team involvement in cmd/snap-update-ns PRs. This does not necessarily require deep reviews from the security team (ie, the security team is at liberty to perform a shallow review stating a deep review is not required)

@zyga, can you comment on this?

@jdstrand

jdstrand Sep 1, 2017

Contributor

This is an interesting idea. So long as that API is very controlled and does extensive input validation, this would work fine. It will likely add some overhead over a fork/exec that would need to be considered.

@jdstrand

jdstrand Sep 1, 2017

Contributor

FYI, here is the exploratory PR for reducing the imports in snap-update-ns: #3840

@jdstrand

jdstrand Sep 1, 2017

Contributor

@niemeyer and @zyga: I've thought through the security implications of snap-confine fork/execing snap-update-ns with minimized imports (eg, finishing #3840) vs @zyga's idea of having snap-confine ask snapd to call snap-update-ns on its behalf.

In terms of security, I don't think there is an appreciable difference with either approach (assuming we get things right, which I'll of course help with).

Outside of security, the fork/exec is simpler to understand and less overhead, but requires the security team have some involvement in changes to snap-update-ns. The IPC approach allows snap-update-ns to be updated more easily (eg, without mandatory security team involvement) since it will always be invoked from a safe environment, but it does mean snapd must be running for anything to execute, which might affect snaps starting on boot before snapd, launching while snapd is restarting, etc.

I'll leave the decision to which approach to pursue up to the snapd team. If you still want to discuss in a meeting with me, I'm available.

@jdstrand

jdstrand Sep 1, 2017

Contributor

After discussing with @niemeyer and @zyga, there is another option which should mitigate my concerns.

If we consider the situation where there is a bug in snap-confine that allows the unprivileged user to call the snap-update-ns executable as root in arbitrary ways, since snap-confine is calling snap-update-ns via its command line interface, the extent of direct influence over snap-update-ns is limited to what the snap-update-ns cli exposes. Beyond that there is the matter of how snap-confine can influence snap-update-ns indirectly which leaves the environment and the mount profiles. With that in mind, the plan of action is:

  • adjust snap-update-ns to unconditionally and immediately clear its environment upon its invocation (optionally adding back whitelisted variables in the future if required). It shall die() on error. There shall be comments in this part of the code stating that changes need a security review.
  • snap-update-ns cli argument parsing shall be hardened for processing malicious input and reviewed by the security team. There shall be comments in this part of the code stating that changes need a security review.
  • write a restrictive child AppArmor profile for snap-update-ns and allow snap-confine to call snap-update-ns only under that profile and with env scrubbing (ie, 'Cx')
  • ensure that the snap-confine AppArmor profile denies writes to mount profiles (this should already be handled)
  • ensure snap-confine is closing unneeded file descriptors before invoking snap-update-ns (ie use O_CLOEXEC for anything that isn't being passed)
  • in addition to snap-confine changes, the security team shall going forward audit changes to the snap-confine and snap-update-ns AppArmor profiles, changes to snap-update-ns environment cleaning (and future whitelisting) and snap-update-ns command line parsing code

On an AppArmor enabled system where snap-confine is confined, the above mitigates the concerns I have since snap-update-ns will be hardened and security-reviewed on its inputs (cli and environment) and snap-confine will be coded to not leak file descriptors and be confined to not allow updating the mount profiles.

On a system without AppArmor, a user-controlled snap-confine is in a position to modify the mount profiles to trigger bugs in the snap-update-ns code. This is true today however with a delayed attack: if snap-confine can change the mount profiles, snapd will happily at some future point call snap-update-ns on them. Obviously we desire to have robust mount profile parsing code, but since it is more likely that a malicious user exploiting bugs in snap-confine would manipulate mount points directly to carry out attacks (or modify the snap-update-ns binary or shell out if the exploit allowed it or ...), we don't need to put extra security reviews on mount profile parsing code.

@jdstrand

jdstrand Sep 1, 2017

Contributor

In addition to the security reviews, I've agreed to implementing the apparmor profile changes for snap-confine and snap-update-ns.

zyga added some commits Aug 31, 2017

cmd/snap-confine: document why we advance the pointer
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-confine: ensure readlink /proc/self/exe is absolute
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/libsnap: fix handling of default values in getenv_bool
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
Contributor

zyga commented Sep 1, 2017

Pushed with all originally discussed changes. I will now sanitize environment and (unless @jdstrand already started), write a child profile just because I never did and I want to see how that looks like.

Contributor

zyga commented Sep 4, 2017

I'm working on addressing build failures and the aforementioned features now.

zyga added some commits Sep 1, 2017

cmd/snap-confine: rewrite and document uX rule for s-u-n
This patch updates the rule that allows snap-confine to run
snap-update-ns and to transition to the unconfined profile. Initial
somewhat convoluted expression is now exploded into a set of documented,
shorter rules.:

Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-confine: document sc_setup_mount_profiles
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-confine: remove leftover debug variable
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-confine: tweak debug statement
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-confine: inline the else branch
This patch replaces if { ..  exec-or-die } else { .. } with
if { .. exec-or-die } .. for simplicity. Thanks for Jamie for the
suggestion.

Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-confine: rework error handling for fork/exec
The error path now handles both process failing and being killed by a
signal. There are some tweaks for consistency with other code as to how
the numbers are formatted.

Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-confine: ensure space for string terminator
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-confine: document why we need extra space
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-confine: tweak error message
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-confine: find snap-update-ns safely
This fixes my totally incorrect use of sprintf that I actually didn't
mean to write this way. It also switches a hand-made implementation over
to that of libc, and uses dirname() to find the name of the directory
that contains the desired binary.

Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-confine: use comment suggested by jdstrand
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-update-ns: clear environment variables on startup
We may have been started via a setuid-root snap-confine. In order to
prevent environment-based attacks we start by erasing all environment
variables.

Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-update-ns: fix C formatting
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
Contributor

zyga commented Sep 4, 2017

@jdstrand this is now unsetting environment on the side of the started snap-update-ns process. I will now focus on a child apparmor profile.

Contributor

zyga commented Sep 4, 2017

I'm testing the child profile locally and I will push it when it passes tests.

cmd/snap-update-ns/bootstrap.c
@@ -136,10 +160,19 @@ int partially_validate_snap_name(const char* snap_name)
}
}
+static void neuter_environment()
@mvo5

mvo5 Sep 4, 2017

Collaborator

That term feels unusual to me, maybe just clearenv_all() or something?

@pedronis

pedronis Sep 5, 2017

Contributor

yes, the typical abstract term would be neutralize,

neuter (verb) meaning 1 is from the wrong domain:

https://en.oxforddictionaries.com/definition/neuter

@zyga

zyga Sep 5, 2017

Contributor

Thanks, I inlined this into bootstrap and removed the name altogether.

zyga added some commits Sep 4, 2017

cmd/snap-confine: use child profile for snap-update-ns
This patch allows snap-confine to run snap-update-ns with a very
specific child profile, that currently mirrors (actually moves) the same
mount rules that used to be allowed for snap-confine. At the same time
snap-confine can no longer do that.

The rules are incomplete and I will probably revise this patch.

Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
debian: link snap-update-ns statically
This patch passes a few oddly looking flags to the "go build" command to
ensure that the resulting binary, even though it links to libc, is
static. We need to do this to allow snap-update-ns to correctly execute
in an otherwise empty base snap where we cannot rely on the dynamic
linker to do its work.

Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-update-ns: inline clearenv to bootstrap
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
packaging: build snap-update-ns statically
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
tests/lib: fix tab-vs-spaces
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
Contributor

zyga commented Sep 5, 2017

This is just failing on openSUSE due to packaging quirk I need to resolve.

@mvo5 mvo5 removed this from the 2.28 milestone Sep 6, 2017

cmd/libsnap-confine-private/utils.c
*
* The return value is 0 in case of success or -1 when the string cannot be
* converted correctly. In such case errno is set to indicate the problem and
* the value is not written back to the caller-supplied pointer.
+ *
+ * If the text cannot be recognized the default value is used.
@jdstrand

jdstrand Sep 6, 2017

Contributor

s/recognized/recognized,/

@zyga

zyga Sep 12, 2017

Contributor

Done

cmd/libsnap-confine-private/utils.c
@@ -96,14 +98,15 @@ static int str2bool(const char *text, bool * value)
* Get an environment variable and convert it to a boolean.
*
* Supported values are those of str2bool(), namely "yes", "no" as well as "1"
- * and "0". All other values are treated as false and a diagnostic message is
- * printed to stderr.
+ * and "0". If the given environment variable is unset a default value is used.
@jdstrand

jdstrand Sep 6, 2017

Contributor

This still isn't quite clear. I suggest, "If the environment variable is unset, set value to the default_value as if the environment variable was set to default_value".

Other than that, this is much easier to follow.

@zyga

zyga Sep 12, 2017

Contributor

Done

cmd/snap-confine/mount-support.c
}
- if (desired == NULL) {
- die("cannot open desired mount profile: %s", profile_path);
+ // Wait for snap-update-ns to finish.
@jdstrand

jdstrand Sep 6, 2017

Contributor

// We are the parent, so wait for snap-update-ns to finish

@zyga

zyga Sep 12, 2017

Contributor

Done

cmd/snap-confine/mount-support.c
@@ -502,6 +476,29 @@ static bool __attribute__ ((used))
return false;
}
+static int sc_open_snap_update_ns()
+{
+ char buf[PATH_MAX] = { 0 };
@jdstrand

jdstrand Aug 30, 2017

Contributor

Please account for terminating null in case length of /proc/self/exe is PATH_MAX:

char buf[PATH_MAX+1] = { 0 };
@zyga

zyga Sep 1, 2017

Contributor

Done. I'm worried now, should all path handling code use PATH_MAX + 1 buffers?

@jdstrand

jdstrand Sep 6, 2017

Contributor

It depends on the api used. readlink() doesn't add a trailing '\0' so we need to account for it using all of buf, and then we come along and tack on '\0'. Because we are using sc_must_snprintf() down below, what you had wasn't a security issue-- sc_must_sprintf() would've die()d-- but this way in the off chance of a super long symlink that is exactly PATH_MAX, it won't die() unnecessarily.

jdstrand requested changes Sep 6, 2017 edited

Thanks for all the changes so far! I got farther than last time and also commented on the apparmor policy changes. It looks like there are other rules that could be removed though, eg the .fstab profile rules. I'd like to see the snap-confine profile have all the stuff snap-update-ns now needs removed. Let me know if you want me to do that.

What remains is a full-on security audit of bootstrap.c and all the go code that runs before BootstrapError()'s error is checked. I'm going to wait until you incorporate the latest round of feedback on that.

+
+ # Allow executing snap-update-ns when...
+
+ # ...snap-confine is not, conceptually, not re-executing and uses
@jdstrand

jdstrand Sep 6, 2017

Contributor

Please rephrase to not use a double negative.

@zyga

zyga Sep 11, 2017

Contributor

Done

+ # ..snap-confine is, conceptually, re-executing and uses snap-update-ns
+ # from the core snap. Note that the location of the core snap varies from
+ # distribution to distribution. The variants here represent different
+ # location of snap mount directory across distributions.
@jdstrand

jdstrand Sep 6, 2017

Contributor

s/location/locations/

@zyga

zyga Sep 11, 2017

Contributor

Fixed

+ # from the core snap but we are already inside the constructed mount
+ # namespace. Here the apparmor kernel module re-constructs the path to
+ # snap-update-ns using the "hostfs" mount entry rather than the more
+ # "natural" /snap mount entry but we have no control over that. The
@jdstrand

jdstrand Sep 6, 2017

Contributor

Huh. Is there a bug for this? If so, please reference it. Eg: ... we have no control over that (LP: #...).

@zyga

zyga Sep 11, 2017

Contributor

No, there's no bug for this yet. I'll file one so that we can discuss this and have a context point.

@zyga

zyga Sep 11, 2017

Contributor

Reported as http://pad.lv/1716339 and added inline to the comment.

+
+ # Allow arbitrary unmounts
+ # TODO: make this rule much more strict.
+ umount,
@jdstrand

jdstrand Sep 6, 2017

Contributor

I suspect the umount rule would be something along the lines of:

umount /snap/*/*/**,
umount /var/snap/*/**,

Are there others you can think of?

@zyga

zyga Sep 11, 2017

Contributor

For now this is sufficient. Done.

cmd/snap-update-ns/bootstrap.c
+ // We may have been started via a setuid-root snap-confine. In order to
+ // prevent environment-based attacks we start by erasing all environment
+ // variables.
+ clearenv();
@jdstrand

jdstrand Sep 6, 2017

Contributor

Please check the return value and die() if non-zero (man clearenv).

@zyga

zyga Sep 11, 2017

Contributor

Done, thanks! I didn't notice that :/

cmd/snap-update-ns/main.go
@@ -28,11 +28,13 @@ import (
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/interfaces/mount"
"github.com/snapcore/snapd/logger"
+ "github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/snap"
)
@jdstrand

jdstrand Sep 6, 2017

Contributor

We have a bit of chicken and egg here with all these imports and the fact that while we have bootstrap.c that runs before main(), but we aren't checking the BootstrapError() until after parseArgs(). I imagine that the intent was that we can try to validate the args easier in go rather than in bootstrap.c, but we are already parsing args in bootstrap.c, so I don't see any benefit in calling parseArgs() before we check for BootstrapError(). As it is now, we have all those imports in the address space and so parseArgs() runs "github.com/jessevdk/go-flags" and "github.com/snapcore/snapd/snap" while privileged. If clearenv() in bootstrap.c failed or any other issues, then these are run with privilege with unsanitized inputs.

I suggest making input validation in bootstrap.c bulletproof (perhaps it could use sc_snap_name_validate() from cmd/libsnap-confine-private) and checking for BootstrapError() as (essentially) the first line of main(), before parseArgs() or anything else. This would mitigate most of my concerns with the imports.

Furthermore, I'd like to see comments in the code for what is running under escalated privileges before BootstrapError() is evaluated. Ie, in bootstrap.c say:

// IMPORTANT: all the code in this file may be run with elevated privileges
// when invoking snap-update-ns from the setuid snap-confine
//
// This file is a preprocessor for snap-update-ns' main() function. It will perform
// input validation and clear the environment so that snap-update-ns' go code
// runs with safe inputs when called by the setuid() snap-confine.

Then in bootstrap.go, put a similar warning above BootstrapError():

// IMPORTANT: all the code in this section may be run with elevated privileges
// when invoking snap-update-ns from the setuid snap-confine

// BootstrapError returns error (if any) encountered in pre-main C code.
func BootstrapError() error {
...
}

// END IMPORTANT

// readCmdline is a wrapper around the C function read_cmdline.
...

Finally in main.go:

// IMPORTANT: all the code in main() until bootstrap is finished may be run
// with elevated privileges when invoking snap-update-ns from the setuid
// snap-confine

func main() {
	if err := run(); err != nil {
		fmt.Printf("cannot update snap namespace: %s\n", err)
		os.Exit(1)
	}
	// END IMPORTANT
}

...
// IMPORTANT: all the code in run() until BootStrapError() is finished may
// be run with elevated privileges when invoking snap-update-ns from
// the setuid snap-confine
func run() error {
	// There is some C code that runs before main() is started.
	// That code always runs and sets an error condition if it fails.
	// Here we just check for the error.
	if err := BootstrapError(); err != nil {
		// If there is no mount namespace to transition to let's just quit
		// instantly without any errors as there is nothing to do anymore.
		if err == ErrNoNamespace {
			return nil
		}
		return err
	}
	// END IMPORTANT

As further optional (though preferred) hardening, you could look at reducing imports as I did in my exploratory PR. For example, the only reason why "github.com/snapcore/snapd/snap" (and all its imports) is pulled in is for the teensy snap.ValidateName(). If bootstrap.c did this already as suggested, you wouldn't have to here. Again, if you make the changes above, this isn't strictly necessary.

@zyga

zyga Sep 11, 2017

Contributor

The amount of parsing of arguments in bootstrap.c is very limited and primitive and will allow certain things that are disallowed by bootstrap.go. We just check for absolutely bare essentials to prevent most obvious incorrect arguments from being passed (such as a SNAP_NAME that looks like a path).

I can adapt sc_snap_name_validate to work in bootstrap.c (though it would be a copy since linking this is already a bit messy) and then do as you suggested (check boostrap as early as possible). I'll also adopt the suggested comments.

As for the imports, I think we can indeed drop the 2nd validate once we do full validation in C and then try to reduce the rest as we go.

@zyga

zyga Sep 11, 2017

Contributor

All done. I'm considering how to push this now.

zyga added some commits Sep 11, 2017

cmd/snap-confine: reword comments to avoid double-negative
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-confine: fix typo: locations
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-confine: reference apparmor bug LP: #1716339
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-confine: use more strict unmount rules, cleanup
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-update-ns: check errors from clearenv
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-update-ns: perform SNAP_NAME validation in C
This patch switches the code to fully validate the provided SNAP_NAME in
the C bootstrap code, before go gets a chance to wake up.

In the past the C code just performed very simple checks and relied on
Go to abort and stop execution before we get to the potentially
dangerous mount operations. This was written with the assumption that
snap-update-ns is not a security-critical component because it is
started by snapd and cannot be started by arbitrary user to elevate its
privileges.

With the advent of snap layouts snap-confine will no longer construct
the full mount namespace and will instead perform the early setup that
is harder to from go. After that it will call snap-update-ns which will
process the mount profile and finish the setup process.

With this change snap-update-ns is directly attackable by any user on
the system in case snap-confine is somehow compromised.

To counter this risk, before we run golang code on top of a,
potentially, compromised execution environment, the bootstrap code will
do strict validation of the provided snap name.

Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>

Please make the requested updates to the apparmor profile. Also, I noticed that snap-confine.rst and snap-confine.5 need to be updated as a result of this PR. Can you update them as well?

+ /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libpthread{,-[0-9]*}.so* mr,
+ /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libresolv{,-[0-9]*}.so* mr,
+ /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}librt{,-[0-9]*}.so* mr,
+ /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libselinux.so* mr,
@jdstrand

jdstrand Sep 14, 2017

Contributor

I reviewed the apparmor profile in detail by doing on 17.04:

$ sudo snap install gnome-3-24
$ sudo snap install gnome-clocks
$ sudo apparmor_parser -r /path/to/snap-confine/profile && sudo /usr/lib/snapd/snap-discard-ns gnome-clocks && snap run --shell gnome-clocks

and found that because of the little snap-update-ns is using, this could be reduced to:

...
        # Allow reading the dynamic linker cache.
        /etc/ld.so.cache r,
        # Allow reading, mapping and executing the dynamic linker.
        /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}ld-*.so mrix,
        # Allow reading and mapping various parts of the standard library and
        # dynamically loaded nss modules and what not.
        /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libc{,-[0-9]*}.so* mr,
        /{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libpthread{,-[0-9]*}.so* mr,
...
@zyga

zyga Sep 18, 2017

Contributor

Done though I suspect it will be different than this on Solus. In any case before we have Solus CI have reduced those. I'll send distro-specific PRs if this is insufficient.

+ /{etc/,usr/lib/}os-release r,
+
+ # Allow creating/grabbing various snapd lock files.
+ /run/snapd/lock/*.lock wrk,
@jdstrand

jdstrand Sep 14, 2017

Contributor

Nitpick: please use 'rwk' for consistency.

@zyga

zyga Sep 18, 2017

Contributor

Done

+
+ # Allow reading per-snap desired mount profiles. Those are written by
+ # snapd and represent the desired layout and content connections.
+ /var/lib/snapd/mount/snap.*.fstab r,
@jdstrand

jdstrand Sep 14, 2017

Contributor

In the snap-confine profile, this rule was /{tmp/snap.rootfs_*/,}var/lib/snapd/mount/*.fstab r,. Two questions:

  1. why didn't you use this rule for snap-update-ns?
  2. why didn't you remove this rule from snap-confine?
@jdstrand

jdstrand Sep 14, 2017

Contributor

I also noticed this rule was in snap-confine's profile, that seems like it can be removed:

    /run/snapd/ns/*.fstab rw,
@zyga

zyga Sep 15, 2017

Contributor

Both of the rules from snap-confine can be removed now. I will make that happen.

@zyga

zyga Sep 18, 2017

Contributor

In snap-update-ns we only deal with paths after pivot_root so the rule can be simpler.

@zyga

zyga Sep 18, 2017

Contributor

I removed the rules from snap-confine now.

+ # writing them. Those are written by snap-update-ns and represent the
+ # actual layout at a given moment.
+ /run/snapd/ns/*.fstab wr,
+ /run/snapd/ns/*.fstab.* wr,
@jdstrand

jdstrand Sep 14, 2017

Contributor

Nitpick: please use 'rw' for consistency.

@zyga

zyga Sep 18, 2017

Contributor

Done

Contributor

zyga commented Sep 15, 2017

@jdstrand thank you for the details. I will update this before my EOD today. I still don't have a grasp for the spread failure on 14.04 that keeps me worried. I'll return here after finishing my current task and explore what is wrong interactively but ideas we very much welcome.

Contributor

zyga commented Sep 18, 2017

I've updated the manual page as requested.I will also re-read the earlier feedback to see if I missed something and then will focus on the systemd-detect-virt issue.

zyga added some commits Sep 18, 2017

cmd/snap-confine: update man page meta-data
Bump copyright date and move the manual page to section one. This fixes
Debian bug report 859375

Fixes: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=859375
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-confine: document the --base and --classic options
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-confine: re-format documentation paragraph
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-confine: re-format documentation paragraph, highlight paths
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-confine: document snap-confine's use of snap-update-ns
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-confine: highlight directory path
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-confine: reduce libraries allowed by snap-update-ns
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-confine: use rwk rather than wrk, for consistency
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-confine: disallow reading/writing mount profiles
This task is now handled by the child process which has a dedicated
child profile that allows that. Removing those rules will also prevent
any attempts to modify those files from snap-confine, to further
constrain attack surface on snap-update-ns.

Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-confine: use rw rather than wr, for consistency
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
Contributor

zyga commented Sep 18, 2017

For whatever reason I cannot reproduce the systemd issue even when I run this on linode. I pushed again to see what tests started from travis say.

zyga added some commits Sep 18, 2017

cmd/snap-update-ns: don't use C99 constructs
Those don't work when building on Ubuntu 14.04 with golang 1.2.1

Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-update-ns: use O_NOFOLLOW to open namespace
This just ensures parity with snap-confine codebase.

Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-update-ns: avoid memset which may be optimized-away
Under certain conditions a compiler may choose to optimize a memset
operation away to gain speed. Because snap-update-ns can be indirectly
setuid root we want to be extra paranoid about that and replace memset
with other constructs.

Reference: https://www.securecoding.cert.org/confluence/display/c/MSC06-C.+Beware+of+compiler+optimizations

Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-update-ns: snprintf doens't set errno
Thanks to jdstrand for noticing this.

Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
spread: work around temporary packaging issue in debian sid
Due to apparent packaging bug in Debian the manpages-dev package clashes
with the manpages package. This patch makes us remove manpages in
attempt to work around the problem.

Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-confine/snap-confine.rst
+
+ `--classic` requests the so-called _classic_ _confinement_ in which
+ applications are not confined at all (like in classic systems, hence the
+ name). This disables disables the use of a dedicated, per-snap mount
@jdstrand

jdstrand Sep 18, 2017

Contributor

s/disables disables/disables/

@zyga

zyga Sep 19, 2017

Contributor

Fixed now.

jdstrand requested changes Sep 18, 2017 edited

Thanks for the changes you made, but you missed a few things from my review of bootstrap.c:

  • You did not address the off-by-one in read_cmdline() yet
  • In bootstrap(), you didn't yet adjust the comment for '-test' vs '.test' and didn't add a comment as to how you set the environment variable and why that is safe.
  • You didn't yet simplify find_argv0()
  • You aren't yet setting bootstrap_error or bootstrap_msg if find_snap_name() returns NULL.
  • You didn't yet simplify find_snap_name()
  • You didn't add a TODO comment to refactor for the code copies in validate_snap_name(), skip_lowercase_letters(), skip_digits() and skip_one_char () (not a blocker)
  • you aren't erroring on unsupported option when calling find_1st_option()
  • you didn't initialize buf in setns_into_snap()

Please read through the above, referencing #3621 (comment) and my answers to your questions to that comment when making your changes.

The changes to snap-confine.apparmor.in look good.

The changes to the snap-confile.rst look fine, but I don't see that snap-confine.1 (previously snap-confine.5) was changed.

Thanks!

Contributor

niemeyer commented Sep 19, 2017

Thanks for the careful re-review, @jdstrand!

Contributor

zyga commented Sep 19, 2017

Hello @jdstrand - I'm working on all the feedback provided. I have one comment about read_cmdline:

You suggested that I use something like this:

    } else if (num_read > buf_size) {
        ...
    } else if (buf[num_read-1] != '\0') {
        // the kernel should always make this NULL terminated, but let's guard
        // against kernel bugs.
        bootstrap_errno = 0;
        bootstrap_msg = "short read on /proc/self/cmdline";
        return -1;
    }

I'm worried about } else if (num_read > buf_size) { as I don't think this can ever happen. Did you really mean that read(2) can return num_read that is bigger than buffer size.

How about something like this instead?

    ssize_t num_read = read(fd, buf, buf_size);
    if (num_read < 0) {
        bootstrap_errno = errno;
        bootstrap_msg = "cannot read /proc/self/cmdline";
    } else if (num_read == buf_size && buf_size > 0 && buf[buf_size-1] != '\0') {
        bootstrap_errno = 0;
        bootstrap_msg = "cannot fit all of /proc/self/cmdline, buffer too small";
        num_read = -1;
    }

This will ensure that when the amount of read data fits the buffer exactly we only check that the buffer is terminated properly.

zyga added some commits Sep 18, 2017

cmd/snap-update-ns: update comment regarding O_NOFOLLOW
That file is not a symbolic link as the property is lost after the
initial bind mount from /proc/$PID/ns/mnt.

This just ensures parity with snap-confine codebase.

Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-update-ns: test programs are suffixed .test
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-update-ns: initialize buffer in setns_into_snap
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-update-ns: refactor option checking code
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-update-ns: reject unknown options from C
The golang parser will also reject them but we want to be extra careful
and not rely on that.

Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-update-ns: use const if buffer doesn't need to be modified
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-update-ns: refactor bootstrap for testability
This patch allows us to test the argument processing code, in its
entirety, without opening any file or invoking setns.

This allows us to write a number of tests that invoke many of the
internal C functions and observe the high-level result.

Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-update-ns: remove stale reference to environment variable
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-update-ns: add TODO comments for reused functions
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-update-ns: add comment about read_cmdline and error handling
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-update-ns: allow read_cmdline to use entire buffer
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-update-ns: simplify find_argv0
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-update-ns: inline trivial find_argv0
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-update-ns: simplify find_snap_name
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-update-ns: simplify find_1st_option
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-update-ns: discard unneeded num_read variable
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
packaging: remove /run/snapd/ns/*.fstab when purging
Those files contain the applied changes to the preserved mount
namespace. Since we are removing the mount namespaces we must also reset
those.

Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-update-ns: add tests for find_1st_string
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-update-ns: add test for empty snap name
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-confine: fix repeated word
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
Contributor

jdstrand commented Sep 19, 2017

@zyga, regarding read returning num_read larger than buf_size, no, you are right it cannot. I made a last minute change and didn't mean to include that.

Thanks for all the changes! Super-close now. See the inline comment about adding a check for read() returning 0.

I'd like to see a comment above the find_1st_option() function about how bug is guaranteed to be NULL-terminated (which justifies size_t pos = strlen(buf) + 1 is ok), similar to how you did in find_snap_name().

Once these are done, +1.

cmd/snap-update-ns/bootstrap.c
- return NULL;
- }
- size_t pos = argv0_len + 1;
+ size_t pos = strlen(buf) + 1;
if (buf[pos] == '-') {
return &buf[pos];
}
@jdstrand

jdstrand Sep 19, 2017

Contributor

Thanks for this. A comment about NULL-termination like you did elsewhere would be nice but not a blocker.

@zyga

zyga Sep 19, 2017

Contributor

Done

cmd/snap-update-ns/bootstrap.c
@@ -224,7 +224,7 @@ int validate_snap_name(const char* snap_name)
}
// process_arguments parses given cmdline which must be list of strings separated with NUL bytes.
-void process_arguments(const char* cmdline, size_t num_read, const char** snap_name_out, bool* should_setns_out)
+void process_arguments(const char* cmdline, const char** snap_name_out, bool* should_setns_out)
@jdstrand

jdstrand Sep 19, 2017

Contributor

This is fine, but perhaps a comment on how cmdline is guaranteed to be NULL-terminated would be nice.

@zyga

zyga Sep 19, 2017

Contributor

Done

cmd/snap-update-ns/bootstrap.c
ssize_t num_read = read(fd, buf, buf_size);
if (num_read < 0) {
bootstrap_errno = errno;
bootstrap_msg = "cannot read /proc/self/cmdline";
- } else if (num_read == buf_size) {
+ } else if (num_read == buf_size && buf_size > 0 && buf[buf_size - 1] != '\0') {
@jdstrand

jdstrand Sep 19, 2017

Contributor

This change looks fine. In reading the man page I was reminded that read() may return 0 at EOF. The kernel isn't supposed to do this of course, but adding an 'if num_read is 0' check after the 'num_read < 0' is cheap.

@zyga

zyga Sep 19, 2017

Contributor

+1, I'll do that in a moment.

@zyga

zyga Sep 19, 2017

Contributor

Done

zyga added some commits Sep 19, 2017

cmd/snap-update-ns: handle empty read from /proc/self/cmdline
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-update-ns: move comment around
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-update-ns: add more comments
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>

Assuming the tests pass, +1.

Thanks for all your hard work on this! :)

# Allow unmounts matching possible mounts listed above.
umount /snap/*/*/**,
umount /var/snap/*/**,
+ umount /usr/share/fonts,
+ umount /usr/local/share/fonts,
+ umount /var/cache/fontconfig,
@jdstrand

jdstrand Sep 19, 2017

Contributor

@zyga, all these rules are fine, but they have extra whitespace. Please fix before committing.

@zyga

zyga Sep 19, 2017

Contributor

Doh, my vim expand tab, fixed.

Merge branch 'master' of github.com:snapcore/snapd into feature/late-…
…init-via-snap-update-ns

I also added unmount rules for snap-update-ns matching the recently
added font sharing mounts.
cmd/libsnap-confine-private/utils-test.c
@@ -25,35 +25,48 @@ static void test_str2bool()
int err;
bool value;
- err = str2bool("yes", &value);
+ value = false;
+ err = str2bool("yes", &value, false);
@mvo5

mvo5 Sep 21, 2017

Collaborator

(nitpick) ParseBool() is what it is called in go but shrug

@zyga

zyga Sep 21, 2017

Contributor

I will tweak this, thanks for noticing :)

+ // We are the child, execute snap-update-ns
+ char *snap_name_copy
+ __attribute__ ((cleanup(sc_cleanup_string))) = NULL;
+ snap_name_copy = strdup(snap_name);
@mvo5

mvo5 Sep 21, 2017

Collaborator

Curious, why the copy here? We have our own address space in the child and nothing else is running except the fexevce() ?

@zyga

zyga Sep 21, 2017

Contributor

Because we need a writable value for execve*.

cmd/snap-confine/mount-support.c
@@ -547,6 +553,11 @@ void sc_populate_mount_ns(const char *base_snap_name, const char *snap_name)
if (vanilla_cwd == NULL) {
die("cannot get the current working directory");
}
+ // Find and open snap-update-ns from the same filesystem as where we
@mvo5

mvo5 Sep 21, 2017

Collaborator

From the same "path", right?

@zyga

zyga Sep 21, 2017

Contributor

Right, nice catch.

zyga added some commits Sep 22, 2017

cmd/libsnap: rename str2bool to parse_bool
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-confine: reword comment
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>

@zyga zyga merged commit b5d41f5 into snapcore:master Sep 23, 2017

5 of 7 checks passed

artful-i386 autopkgtest finished (failure)
Details
zesty-amd64 autopkgtest finished (failure)
Details
artful-amd64 autopkgtest finished (success)
Details
continuous-integration/travis-ci/pr The Travis CI build passed
Details
xenial-amd64 autopkgtest finished (success)
Details
xenial-i386 autopkgtest finished (success)
Details
xenial-ppc64el autopkgtest finished (success)
Details

@zyga zyga deleted the zyga:feature/late-init-via-snap-update-ns branch Sep 23, 2017

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment