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/libsnap: add helper for dropping permissions #2866
Conversation
zyga
requested a review
from
jdstrand
Feb 15, 2017
|
The test failure is related to some breakage in |
| + gid_t rgid, egid, sgid; | ||
| + if (getresgid(&rgid, &egid, &sgid) < 0) { | ||
| + die("cannot get real, effective and saved group identifiers"); | ||
| + } |
jdstrand
Feb 15, 2017
Contributor
I think with the suggested changes below, you can reduce this to ruid and rgid with getuid() and getgid().
| + // Drop extra group membership. | ||
| + if (setgroups(0, NULL) < 0) { | ||
| + die("cannot set supplementary group identifiers"); | ||
| + } |
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
...
zyga
Feb 15, 2017
Contributor
Done, something like that but with security-irrelevant details tweaked.
| + if (setuid(ruid) < 0) { | ||
| + die("cannot set user identifier to %d", ruid); | ||
| + } | ||
| + // Verify everything |
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().
| + // Verify everything | ||
| + if (rgid != 0 && (getuid() == 0 || geteuid() == 0)) { | ||
| + die("cannot permanently drop permissions (gid still elevated)"); | ||
| + } |
| + die("cannot permanently drop permissions (gid still elevated)"); | ||
| + } | ||
| + if (ruid != 0 && (getgid() == 0 || getegid() == 0)) { | ||
| + die("cannot permanently drop permissions (uid still elevated)"); |
| + if (ruid != 0 && (getgid() == 0 || getegid() == 0)) { | ||
| + die("cannot permanently drop permissions (uid still elevated)"); | ||
| + } | ||
| + // XXX Should we verify supplementary groups? |
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
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
| + // Free the representation of the capability state of the current process. | ||
| + if (cap_free(caps) < 0) { | ||
| + die("cannot free capability flag (cap_free)"); | ||
| + } |
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.
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.
| + if (getresgid(&rgid, &egid, &sgid) < 0) { | ||
| + die("cannot get real, effective and saved group identifiers"); | ||
| + } | ||
| + if (euid == 0) { |
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
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.
| + if (ruid != 0 && (getgid() == 0 || getegid() == 0)) { | ||
| + die("cannot permanently drop permissions (gid still elevated)"); | ||
| + } | ||
| + // XXX Should we verify supplementary groups? |
jdstrand
Feb 16, 2017
Contributor
Well, again, IMO you can drop these checks. See previous comments.
| + gid_t rgid, egid, sgid; | ||
| + if (getresgid(&rgid, &egid, &sgid) < 0) { | ||
| + die("cannot get real, effective and saved group identifiers"); | ||
| + } |
jdstrand
Feb 16, 2017
Contributor
You don't need euid, suid, egid or sgid here (after you remove the next line).
zyga
added some commits
Feb 16, 2017
|
libcap-dev on 14.04 doesn't ship the .pc file, eh ... |
|
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
closed this
Feb 16, 2017
| + if (getegid() != 0 || getgid() == 0) { | ||
| + g_test_skip("run this test after chown root.root; chmod g+s"); | ||
| + return; | ||
| + } |
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
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.
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
Feb 16, 2017
Contributor
You had a debug() statement here that you dropped. That is fine to keep.
zyga
reopened this
Feb 16, 2017
|
We don't need to remove libcap as the particular issue with apt doesn't affect it :-) |
zyga
merged commit 6e4fbe1
into
snapcore:master
Feb 16, 2017
5 of 6 checks passed
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
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
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
Feb 16, 2017
Contributor
This uid variable is only used in an error message. Is that intended?
zyga commentedFeb 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