Join GitHub today
GitHub is home to over 20 million developers working together to host and review code, manage projects, and build software together.
cmd/snap-{confine,update-ns}: apply mount profiles using snap-update-ns #3621
Conversation
zyga
added some commits
Jul 25, 2017
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
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
codecov-io
commented
Jul 26, 2017
•
Codecov Report
@@ 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
Continue to review full report at Codecov.
|
| + // 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
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.
| @@ -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
added this to the 2.28 milestone
Aug 28, 2017
zyga
added some commits
Aug 28, 2017
pedronis
requested a review
from
jdstrand
Aug 29, 2017
| @@ -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
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
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
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
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,
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
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
Aug 31, 2017
Contributor
Note they aren't regular expressions. They are AARE which is much more limited (see man apparmor.d for details).
jdstrand
requested changes
Aug 30, 2017
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.
| @@ -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 |
| @@ -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
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
Aug 31, 2017
Contributor
Ah, nice catch wrt default_value and how str2bool operates, I missed that. Let me tweak that.
zyga
Sep 1, 2017
Contributor
I reworked the default value, fixed the bug, added testes and documented how it operates.
| - **/ | ||
| -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
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.
| + "snap-update-ns", "--from-snap-confine", snap_name_copy, | ||
| + NULL | ||
| + }; | ||
| + char *envp[] = { "SNAP_CONFINE_DEBUG=yes", NULL }; |
jdstrand
Aug 30, 2017
Contributor
Why are you setting this unconditionally rather than setting this conditional on getenv_bool()?
| + }; | ||
| + char *envp[] = { "SNAP_CONFINE_DEBUG=yes", NULL }; | ||
| + debug("fexecv(%d, %s %s %s,)", snap_update_ns_fd, argv[0], | ||
| + argv[1], argv[2]); |
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]);
| + argv[1], argv[2]); | ||
| + fexecve(snap_update_ns_fd, argv, envp); | ||
| + die("cannot execute snap-update-ns"); | ||
| + } else { |
jdstrand
Aug 30, 2017
Contributor
No need for an else here. fexecve() either never returns or if it does with error, you call die().
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.
| + 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"); |
| - 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
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.
| + WEXITSTATUS(status)); | ||
| + } | ||
| + } else { | ||
| + die("snap-update-ns failed abnormally"); |
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).
| @@ -502,6 +476,29 @@ static bool __attribute__ ((used)) | ||
| return false; | ||
| } | ||
| +static int sc_open_snap_update_ns() | ||
| +{ | ||
| + char buf[PATH_MAX] = { 0 }; |
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
Sep 1, 2017
Contributor
Done. I'm worried now, should all path handling code use PATH_MAX + 1 buffers?
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.
| +static int sc_open_snap_update_ns() | ||
| +{ | ||
| + char buf[PATH_MAX] = { 0 }; | ||
| + if (readlink("/proc/self/exe", buf, sizeof buf) < 0) { |
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.
| + if (s == NULL) { | ||
| + die("cannot find trailing forward slash in %s", buf); | ||
| + } | ||
| + s += 1; |
| + die("cannot find trailing forward slash in %s", buf); | ||
| + } | ||
| + s += 1; | ||
| + sc_must_snprintf(s, sizeof buf - (s - buf), "snap-update-ns"); |
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
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.
| @@ -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
Aug 30, 2017
•
Contributor
Perhaps rephrase as:
// Find and open snap-update-ns from the same filesystem as where we (snap-confine)
// were called.
| @@ -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
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
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
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
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,
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
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
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
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
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
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
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
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/release" which imports:
- "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/snap" (via backend.go, core.go, naming.go, repo.go, sorting.go) which imports:
- "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
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
Sep 1, 2017
Contributor
FYI, here is the exploratory PR for reducing the imports in snap-update-ns: #3840
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
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
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.
| + argv[1], argv[2]); | ||
| + fexecve(snap_update_ns_fd, argv, envp); | ||
| + die("cannot execute snap-update-ns"); | ||
| + } else { |
jdstrand
Aug 30, 2017
Contributor
No need for an else here. fexecve() either never returns or if it does with error, you call die().
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.
| + if (s == NULL) { | ||
| + die("cannot find trailing forward slash in %s", buf); | ||
| + } | ||
| + s += 1; |
| + die("cannot find trailing forward slash in %s", buf); | ||
| + } | ||
| + s += 1; | ||
| + sc_must_snprintf(s, sizeof buf - (s - buf), "snap-update-ns"); |
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
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.
| @@ -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
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
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
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
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,
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
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
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
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
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
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
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
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/release" which imports:
- "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/snap" (via backend.go, core.go, naming.go, repo.go, sorting.go) which imports:
- "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
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
Sep 1, 2017
Contributor
FYI, here is the exploratory PR for reducing the imports in snap-update-ns: #3840
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
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
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
jdstrand
referenced this pull request
Sep 1, 2017
Closed
snap-update-ns intial refactor for setuid [EXPLORATORY, DO NOT COMMIT] #3840
|
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. |
|
I'm working on addressing build failures and the aforementioned features now. |
zyga
added some commits
Sep 1, 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. |
|
I'm testing the child profile locally and I will push it when it passes tests. |
| @@ -136,10 +160,19 @@ int partially_validate_snap_name(const char* snap_name) | ||
| } | ||
| } | ||
| +static void neuter_environment() |
mvo5
Sep 4, 2017
Collaborator
That term feels unusual to me, maybe just clearenv_all() or something?
pedronis
Sep 5, 2017
•
Contributor
yes, the typical abstract term would be neutralize,
neuter (verb) meaning 1 is from the wrong domain:
zyga
added some commits
Sep 4, 2017
|
This is just failing on openSUSE due to packaging quirk I need to resolve. |
mvo5
removed this from the 2.28 milestone
Sep 6, 2017
| * | ||
| * 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. |
| @@ -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
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.
| } | ||
| - if (desired == NULL) { | ||
| - die("cannot open desired mount profile: %s", profile_path); | ||
| + // Wait for snap-update-ns to finish. |
| @@ -502,6 +476,29 @@ static bool __attribute__ ((used)) | ||
| return false; | ||
| } | ||
| +static int sc_open_snap_update_ns() | ||
| +{ | ||
| + char buf[PATH_MAX] = { 0 }; |
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
Sep 1, 2017
Contributor
Done. I'm worried now, should all path handling code use PATH_MAX + 1 buffers?
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
•
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 |
| + # ..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. |
| + # 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
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
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.
| + | ||
| + # Allow arbitrary unmounts | ||
| + # TODO: make this rule much more strict. | ||
| + umount, |
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?
| + // 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
Sep 6, 2017
Contributor
Please check the return value and die() if non-zero (man clearenv).
| @@ -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
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
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
added some commits
Sep 11, 2017
jdstrand
requested changes
Sep 14, 2017
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
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
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, |
| + | ||
| + # 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
Sep 14, 2017
•
Contributor
In the snap-confine profile, this rule was /{tmp/snap.rootfs_*/,}var/lib/snapd/mount/*.fstab r,. Two questions:
- why didn't you use this rule for snap-update-ns?
- why didn't you remove this rule from snap-confine?
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
Sep 15, 2017
Contributor
Both of the rules from snap-confine can be removed now. I will make that happen.
zyga
Sep 18, 2017
Contributor
In snap-update-ns we only deal with paths after pivot_root so the rule can be simpler.
| + # 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 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. |
|
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 |
zyga
added some commits
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
| + | ||
| + `--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
requested changes
Sep 18, 2017
•
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!
|
Thanks for the careful re-review, @jdstrand! |
|
Hello @jdstrand - I'm working on all the feedback provided. I have one comment about You suggested that I use something like this:
I'm worried about How about something like this instead?
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
|
@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. |
jdstrand
requested changes
Sep 19, 2017
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.
| - return NULL; | ||
| - } | ||
| - size_t pos = argv0_len + 1; | ||
| + size_t pos = strlen(buf) + 1; | ||
| if (buf[pos] == '-') { | ||
| return &buf[pos]; | ||
| } |
jdstrand
Sep 19, 2017
Contributor
Thanks for this. A comment about NULL-termination like you did elsewhere would be nice but not a blocker.
| @@ -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
Sep 19, 2017
Contributor
This is fine, but perhaps a comment on how cmdline is guaranteed to be NULL-terminated would be nice.
| 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
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
added some commits
Sep 19, 2017
jdstrand
approved these changes
Sep 19, 2017
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
Sep 19, 2017
Contributor
@zyga, all these rules are fine, but they have extra whitespace. Please fix before committing.
| @@ -25,35 +25,48 @@ static void test_str2bool() | ||
| int err; | ||
| bool value; | ||
| - err = str2bool("yes", &value); | ||
| + value = false; | ||
| + err = str2bool("yes", &value, false); |
| + // 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
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() ?
| @@ -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 |
zyga commentedJul 25, 2017
•
Edited 1 time
-
zyga
Sep 20, 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