cmd/libsnap: add helper for dropping permissions #2866

Merged
merged 11 commits into from Feb 16, 2017

Conversation

Projects
None yet
3 participants
Contributor

zyga commented Feb 15, 2017

This patch adds a helper function for dropping elevated privileges.

Since snap-confine runs as root via the setuid-root mechanism we always
start with elevated permissions. For added security and usability,
before exiting the program in case of a specific error, we can drop the
extra permissions and then run any extra helper / debug support code,
such as a string manipulation routine.

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

@zyga zyga requested a review from jdstrand Feb 15, 2017

cmd/libsnap: add helper for dropping permissions
This patch adds a helper function for dropping elevated privileges.

Since snap-confine runs as root via the setuid-root mechanism we always
start with elevated permissions. For added security and usability,
before exiting the program in case of a specific error, we can drop the
extra permissions and then run any extra helper / debug support code,
such as a string manipulation routine.

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

zyga commented Feb 15, 2017

The test failure is related to some breakage in ubuntu-image and is not related to this pull request.

cmd/libsnap-confine-private/privs.c
+ gid_t rgid, egid, sgid;
+ if (getresgid(&rgid, &egid, &sgid) < 0) {
+ die("cannot get real, effective and saved group identifiers");
+ }
@jdstrand

jdstrand Feb 15, 2017

Contributor

I think with the suggested changes below, you can reduce this to ruid and rgid with getuid() and getgid().

cmd/libsnap-confine-private/privs.c
+ // Drop extra group membership.
+ if (setgroups(0, NULL) < 0) {
+ die("cannot set supplementary group identifiers");
+ }
@jdstrand

jdstrand Feb 15, 2017

Contributor

Since only setgroups needs privs (ie, CAP_SETGID), because snap-confine may be installed on systems with fscaps and since this is a library function, let's do this a bit differently.

Eg, add something like this:

#include <sys/capability.h>

bool has_capability(const char *s) {
	int i, res;
	bool rc = false;

	cap_t caps;
	cap_value_t cap;
	cap_flag_value_t cap_flags_value;

	if (cap_from_name(s, &cap) < 0) {
		die("could not cap_from_name()");
	}

	if ((caps = cap_get_proc()) == NULL) {
		die("could not cap_get_proc()");
	}

	res = cap_get_flag(caps, cap, CAP_EFFECTIVE, &cap_flags_value);
	cap_free(caps);
	if (res < 0) {
		die("could not cap_get_flag()");
	}

	if (cap_flags_value == CAP_SET) {
		rc = true;
	}

	return rc;
}

Then, check if we have the capability of using setgroups, and only then call it:

// Drop extra group memberships when we can and use our primary
// real gid for portability.
gid_t gid_list[1];
gid_list[0] = rgid;
if (has_capability("cap_setgid") && setgroups(1, gid_list) < 0) {
    die("cannot set supplementary group identifiers");
}
// Switch to real group ID
...
@jdstrand

jdstrand Feb 15, 2017

Contributor

Note, for the above, need libcap-dev and compile with -lcap.

@zyga

zyga Feb 15, 2017

Contributor

Done, something like that but with security-irrelevant details tweaked.

cmd/libsnap-confine-private/privs.c
+ if (setuid(ruid) < 0) {
+ die("cannot set user identifier to %d", ruid);
+ }
+ // Verify everything
@jdstrand

jdstrand Feb 15, 2017

Contributor

This is where you could put a comment about being paranoid (but see below):

// With the above, this should never happen but be paranoid to help
// future-proof code changes. Specifically, if our real gid was not
// root, but one of uid/euid still are root, die(). Same for if our
// real uid was not root, but one of gid/egid are root, die().
@zyga

zyga Feb 15, 2017

Contributor

Done

cmd/libsnap-confine-private/privs.c
+ // Verify everything
+ if (rgid != 0 && (getuid() == 0 || geteuid() == 0)) {
+ die("cannot permanently drop permissions (gid still elevated)");
+ }
@jdstrand

jdstrand Feb 15, 2017

Contributor

This should say 'uid and/or euid still elevated'.

cmd/libsnap-confine-private/privs.c
+ die("cannot permanently drop permissions (gid still elevated)");
+ }
+ if (ruid != 0 && (getgid() == 0 || getegid() == 0)) {
+ die("cannot permanently drop permissions (uid still elevated)");
@jdstrand

jdstrand Feb 15, 2017

Contributor

This should say 'gid and/or egid still elevated'.

cmd/libsnap-confine-private/privs.c
+ if (ruid != 0 && (getgid() == 0 || getegid() == 0)) {
+ die("cannot permanently drop permissions (uid still elevated)");
+ }
+ // XXX Should we verify supplementary groups?
@jdstrand

jdstrand Feb 15, 2017

Contributor

In considering this more, as a library function, I don't think we need the if (rgid != 0 && (getuid() == 0 || geteuid() == 0)) and if (ruid != 0 && (getgid() == 0 || getegid() == 0)) extra checks here, or a setgroups check because this function is self-contained and intended for one use-- to drop everything. Ie, you call sc_privs_drop() and we have a very clear line of permanent privilege dropping steps where if anything fails along the way, die() with a simple error. In other parts of the codebase, we drop, raise, drop, permanently drop, etc, each with a different context (eg, sometimes only raise euid) so the extra future-proofing checks in those places are worthwhile (imo).

@zyga

zyga Feb 15, 2017

Contributor

Have a look at the state now and tell me if you feel we should drop the extra checks. I don't mind having them and I would like to get back on the main update-ns track.

PS: Thanks a lot for reminding me about capabilities, I didn't consider that root permissions can be partitioned while writing this.

zyga added some commits Feb 15, 2017

cmd/libsnap: correct uid vs gid in death messages
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/libsnap: document tricky check
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
debian: build-depend on libcap
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/libsnap: detect libcap in configure
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/libsnap: add sc_has_capability
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/libsnap: only drop supplementary groups if we can
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/libsnap: fix whitespace
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
+ // Free the representation of the capability state of the current process.
+ if (cap_free(caps) < 0) {
+ die("cannot free capability flag (cap_free)");
+ }
@jdstrand

jdstrand Feb 16, 2017

Contributor

You still want to cap_free() if cap_get_flag() fails which is why it was where it was in my suggestion.

@zyga

zyga Feb 16, 2017

Contributor

Wait, even if we die? EDIT: done just in case it matters

@jdstrand

jdstrand Feb 16, 2017

Contributor

It isn't strictly required but is good practice, sure.

@zyga

zyga Feb 16, 2017

Contributor

Done now :-)

@jdstrand

jdstrand Feb 16, 2017

Contributor

Well, I used 'res' in my suggested function so that you could run cap_get_flag and store result in 'res', cap_free(), then check 'res' so you only needed to have cap_free() once. This is not a blocker if you prefer it this way.

cmd/libsnap-confine-private/privs.c
+ if (getresgid(&rgid, &egid, &sgid) < 0) {
+ die("cannot get real, effective and saved group identifiers");
+ }
+ if (euid == 0) {
@jdstrand

jdstrand Feb 16, 2017

Contributor

As mentioned, please get rid of this conditional. If non-root, the setgid() and setuid() calls are harmless. Consider if euid != 0 but egid == 0 for some reason; we still want to drop.

@zyga

zyga Feb 16, 2017

Contributor

Ack, this is why the first version of the branch had separate gid / uid checks but I see why it is wrong to check for just one here. Removed.

cmd/libsnap-confine-private/privs.c
+ if (ruid != 0 && (getgid() == 0 || getegid() == 0)) {
+ die("cannot permanently drop permissions (gid still elevated)");
+ }
+ // XXX Should we verify supplementary groups?
@jdstrand

jdstrand Feb 16, 2017

Contributor

Well, again, IMO you can drop these checks. See previous comments.

@zyga

zyga Feb 16, 2017

Contributor

Removed

cmd/libsnap-confine-private/privs.c
+ gid_t rgid, egid, sgid;
+ if (getresgid(&rgid, &egid, &sgid) < 0) {
+ die("cannot get real, effective and saved group identifiers");
+ }
@jdstrand

jdstrand Feb 16, 2017

Contributor

You don't need euid, suid, egid or sgid here (after you remove the next line).

@zyga

zyga Feb 16, 2017

Contributor

Removed

zyga added some commits Feb 16, 2017

cmd/libsnap: remove extra checks and simplify drop privs
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/libsnap: free caps in case we die too
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
Contributor

zyga commented Feb 16, 2017

libcap-dev on 14.04 doesn't ship the .pc file, eh ...

cmd/libsnap: don't use pkg-config for libcap
On Ubuntu 14.04 there's no libcap.pc yet so let's not overdo the '-lcap'
with extra machinery but just provide it manually.

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

zyga commented Feb 16, 2017

I'll change this so that we don't need to depend on libcap but only use it if it is available. This will match the expectations in particular distributions better and will avoid the issue with apt that makes it difficult to add a new dependency to an existing package.

@zyga zyga closed this Feb 16, 2017

Looks fine codewise, thanks for the updates.

+ if (getegid() != 0 || getgid() == 0) {
+ g_test_skip("run this test after chown root.root; chmod g+s");
+ return;
+ }
@jdstrand

jdstrand Feb 16, 2017

Contributor

Note, the gid test here isn't testing a real situation since snap-confine is either 4755 or has fscaps. It will never be 2755 or 6755 (unless you maybe do that in a spread test).

If you are concerned with these situations, perhaps add a comment:

// Practically, we can only test priv dropping when installed 4755 (setuid
// root) because the test can't fix up the uids/gids when this test runs as
// root (eg, under sudo or as root). This is because while the uid/gid are
// root, we can put euid/egid where we want, but glibc will helpfully and
// correctly set ruid/rgid, euid/egid and suid/sgid to non-root when using
// setuid/setgid from root to non-root. (man 2 setuid)
+ // Free the representation of the capability state of the current process.
+ if (cap_free(caps) < 0) {
+ die("cannot free capability flag (cap_free)");
+ }
@jdstrand

jdstrand Feb 16, 2017

Contributor

You still want to cap_free() if cap_get_flag() fails which is why it was where it was in my suggestion.

@zyga

zyga Feb 16, 2017

Contributor

Wait, even if we die? EDIT: done just in case it matters

@jdstrand

jdstrand Feb 16, 2017

Contributor

It isn't strictly required but is good practice, sure.

@zyga

zyga Feb 16, 2017

Contributor

Done now :-)

@jdstrand

jdstrand Feb 16, 2017

Contributor

Well, I used 'res' in my suggested function so that you could run cap_get_flag and store result in 'res', cap_free(), then check 'res' so you only needed to have cap_free() once. This is not a blocker if you prefer it this way.

+ // Switch to real user ID
+ if (setuid(getuid()) < 0) {
+ die("cannot set user identifier to %d", uid);
+ }
@jdstrand

jdstrand Feb 16, 2017

Contributor

You had a debug() statement here that you dropped. That is fine to keep.

@zyga zyga reopened this Feb 16, 2017

Contributor

zyga commented Feb 16, 2017

We don't need to remove libcap as the particular issue with apt doesn't affect it :-)

@zyga zyga merged commit 6e4fbe1 into snapcore:master Feb 16, 2017

5 of 6 checks passed

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

@zyga zyga deleted the zyga:privs-management-v2 branch Feb 16, 2017

+{
+ // Lookup capability with the given name.
+ cap_value_t cap;
+ if (cap_from_name(cap_name, &cap) < 0) {
@tyhicks

tyhicks Feb 16, 2017

Contributor

Nitpick: this is an unnecessary translation. sc_has_capability() could easily take a cap_value_t instead of a string and not lose any readability.

@jdstrand

jdstrand Feb 17, 2017

Contributor

You're right but I thought as a library it might be handy to take as a string ('cap_chown' looks nice as a security policy writer after all :). cap_from_name() also returns -1 for unknown so we didn't need to do CAP_IS_SUPPORTED which if we didn't use cap_from_name(), we'd want to add.

+void sc_privs_drop()
+{
+ gid_t gid = getgid();
+ uid_t uid = getuid();
@tyhicks

tyhicks Feb 16, 2017

Contributor

This uid variable is only used in an error message. Is that intended?

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