mirror of
https://github.com/token2/snapd.git
synced 2026-03-13 11:15:47 -07:00
I was putting this off because indent had changed once in the past but now working with C code is just so damn annoying when my edits are intermixed with the noise about how attributes are formatted or how the pointer start spacing is supposed to look like. Signed-off-by: Zygmunt Krynicki <me@zygoon.pl>
512 lines
14 KiB
C
512 lines
14 KiB
C
/*
|
|
* Copyright (C) 2017 Canonical Ltd
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License version 3 as
|
|
* published by the Free Software Foundation.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
// 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.
|
|
|
|
#include "bootstrap.h"
|
|
|
|
#include <ctype.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <grp.h>
|
|
#include <limits.h>
|
|
#include <sched.h>
|
|
#include <stdbool.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/capability.h>
|
|
#include <sys/prctl.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
#include <unistd.h>
|
|
|
|
// bootstrap_errno contains a copy of errno if a system call fails.
|
|
int bootstrap_errno = 0;
|
|
// bootstrap_msg contains a static string if something fails.
|
|
const char *bootstrap_msg = NULL;
|
|
|
|
// setns_into_snap switches mount namespace into that of a given snap.
|
|
static int setns_into_snap(const char *snap_name)
|
|
{
|
|
// Construct the name of the .mnt file to open.
|
|
char buf[PATH_MAX] = {
|
|
0,
|
|
};
|
|
int n = snprintf(buf, sizeof buf, "/run/snapd/ns/%s.mnt", snap_name);
|
|
if (n >= sizeof buf || n < 0) {
|
|
bootstrap_errno = 0;
|
|
bootstrap_msg = "cannot format mount namespace file name";
|
|
return -1;
|
|
}
|
|
// Open the mount namespace file.
|
|
int fd = open(buf, O_RDONLY | O_CLOEXEC | O_NOFOLLOW);
|
|
if (fd < 0) {
|
|
bootstrap_errno = errno;
|
|
bootstrap_msg = "cannot open mount namespace file";
|
|
return -1;
|
|
}
|
|
// Switch to the mount namespace of the given snap.
|
|
int err = setns(fd, CLONE_NEWNS);
|
|
if (err < 0) {
|
|
bootstrap_errno = errno;
|
|
bootstrap_msg = "cannot switch mount namespace";
|
|
};
|
|
|
|
close(fd);
|
|
return err;
|
|
}
|
|
|
|
// switch_to_privileged_user drops to the real user ID while retaining
|
|
// CAP_SYS_ADMIN, for operations such as mount().
|
|
static int switch_to_privileged_user()
|
|
{
|
|
uid_t real_uid;
|
|
gid_t real_gid;
|
|
|
|
real_uid = getuid();
|
|
if (real_uid == 0) {
|
|
// We're running as root: no need to switch IDs
|
|
return 0;
|
|
}
|
|
real_gid = getgid();
|
|
|
|
// _LINUX_CAPABILITY_VERSION_3 valid for kernel >= 2.6.26. See
|
|
// https://github.com/torvalds/linux/blob/master/kernel/capability.c
|
|
struct __user_cap_header_struct hdr =
|
|
{ _LINUX_CAPABILITY_VERSION_3, 0 };
|
|
struct __user_cap_data_struct data[2] = { {0} };
|
|
|
|
data[0].effective = (CAP_TO_MASK(CAP_SYS_ADMIN) |
|
|
CAP_TO_MASK(CAP_SETUID) | CAP_TO_MASK(CAP_SETGID));
|
|
data[0].permitted = data[0].effective;
|
|
data[0].inheritable = 0;
|
|
data[1].effective = 0;
|
|
data[1].permitted = 0;
|
|
data[1].inheritable = 0;
|
|
|
|
if (capset(&hdr, data) != 0) {
|
|
bootstrap_errno = errno;
|
|
bootstrap_msg = "cannot set permitted capabilities mask";
|
|
return -1;
|
|
}
|
|
|
|
if (prctl(PR_SET_KEEPCAPS, 1, 0, 0, 0) != 0) {
|
|
bootstrap_errno = errno;
|
|
bootstrap_msg =
|
|
"cannot tell kernel to keep capabilities over setuid";
|
|
return -1;
|
|
}
|
|
|
|
if (setgroups(1, &real_gid) != 0) {
|
|
bootstrap_errno = errno;
|
|
bootstrap_msg = "cannot drop supplementary groups";
|
|
return -1;
|
|
}
|
|
|
|
if (setgid(real_gid) != 0) {
|
|
bootstrap_errno = errno;
|
|
bootstrap_msg = "cannot switch to real group ID";
|
|
return -1;
|
|
}
|
|
|
|
if (setuid(real_uid) != 0) {
|
|
bootstrap_errno = errno;
|
|
bootstrap_msg = "cannot switch to real user ID";
|
|
return -1;
|
|
}
|
|
// After changing uid, our effective capabilities were dropped.
|
|
// Reacquire CAP_SYS_ADMIN, and discard CAP_SETUID/CAP_SETGID.
|
|
data[0].effective = CAP_TO_MASK(CAP_SYS_ADMIN);
|
|
data[0].permitted = data[0].effective;
|
|
if (capset(&hdr, data) != 0) {
|
|
bootstrap_errno = errno;
|
|
bootstrap_msg =
|
|
"cannot enable capabilities after switching to real user";
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// TODO: reuse the code from snap-confine, if possible.
|
|
static int skip_lowercase_letters(const char **p)
|
|
{
|
|
int skipped = 0;
|
|
const char *c;
|
|
for (c = *p; *c >= 'a' && *c <= 'z'; ++c) {
|
|
skipped += 1;
|
|
}
|
|
*p = (*p) + skipped;
|
|
return skipped;
|
|
}
|
|
|
|
// TODO: reuse the code from snap-confine, if possible.
|
|
static int skip_digits(const char **p)
|
|
{
|
|
int skipped = 0;
|
|
const char *c;
|
|
for (c = *p; *c >= '0' && *c <= '9'; ++c) {
|
|
skipped += 1;
|
|
}
|
|
*p = (*p) + skipped;
|
|
return skipped;
|
|
}
|
|
|
|
// TODO: reuse the code from snap-confine, if possible.
|
|
static int skip_one_char(const char **p, char c)
|
|
{
|
|
if (**p == c) {
|
|
*p += 1;
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// validate_snap_name performs full validation of the given name.
|
|
int validate_snap_name(const char *snap_name)
|
|
{
|
|
// NOTE: This function should be synchronized with the two other
|
|
// implementations: sc_snap_name_validate and snap.ValidateName.
|
|
|
|
// Ensure that name is not NULL
|
|
if (snap_name == NULL) {
|
|
bootstrap_msg = "snap name cannot be NULL";
|
|
return -1;
|
|
}
|
|
// This is a regexp-free routine hand-codes the following pattern:
|
|
//
|
|
// "^([a-z0-9]+-?)*[a-z](-?[a-z0-9])*$"
|
|
//
|
|
// The only motivation for not using regular expressions is so that we
|
|
// don't run untrusted input against a potentially complex regular
|
|
// expression engine.
|
|
const char *p = snap_name;
|
|
if (skip_one_char(&p, '-')) {
|
|
bootstrap_msg = "snap name cannot start with a dash";
|
|
return -1;
|
|
}
|
|
bool got_letter = false;
|
|
int n = 0, m;
|
|
for (; *p != '\0';) {
|
|
if ((m = skip_lowercase_letters(&p)) > 0) {
|
|
n += m;
|
|
got_letter = true;
|
|
continue;
|
|
}
|
|
if ((m = skip_digits(&p)) > 0) {
|
|
n += m;
|
|
continue;
|
|
}
|
|
if (skip_one_char(&p, '-') > 0) {
|
|
n++;
|
|
if (*p == '\0') {
|
|
bootstrap_msg =
|
|
"snap name cannot end with a dash";
|
|
return -1;
|
|
}
|
|
if (skip_one_char(&p, '-') > 0) {
|
|
bootstrap_msg =
|
|
"snap name cannot contain two consecutive dashes";
|
|
return -1;
|
|
}
|
|
continue;
|
|
}
|
|
bootstrap_msg =
|
|
"snap name must use lower case letters, digits or dashes";
|
|
return -1;
|
|
}
|
|
if (!got_letter) {
|
|
bootstrap_msg = "snap name must contain at least one letter";
|
|
return -1;
|
|
}
|
|
if (n < 2) {
|
|
bootstrap_msg = "snap name must be longer than 1 character";
|
|
return -1;
|
|
}
|
|
if (n > 40) {
|
|
bootstrap_msg = "snap name must be shorter than 40 characters";
|
|
return -1;
|
|
}
|
|
|
|
bootstrap_msg = NULL;
|
|
return 0;
|
|
}
|
|
|
|
static int instance_key_validate(const char *instance_key)
|
|
{
|
|
// NOTE: see snap.ValidateInstanceName for reference of a valid instance key
|
|
// format
|
|
|
|
// Ensure that name is not NULL
|
|
if (instance_key == NULL) {
|
|
bootstrap_msg = "instance key cannot be NULL";
|
|
return -1;
|
|
}
|
|
// This is a regexp-free routine hand-coding the following pattern:
|
|
//
|
|
// "^[a-z]{1,10}$"
|
|
//
|
|
// The only motivation for not using regular expressions is so that we don't
|
|
// run untrusted input against a potentially complex regular expression
|
|
// engine.
|
|
int i = 0;
|
|
for (i = 0; instance_key[i] != '\0'; i++) {
|
|
char c = instance_key[i];
|
|
/* NOTE: We are reimplementing islower() and isdigit()
|
|
* here. For context see
|
|
* https://github.com/golang/go/issues/29689 */
|
|
if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) {
|
|
continue;
|
|
}
|
|
bootstrap_msg =
|
|
"instance key must use lower case letters or digits";
|
|
return -1;
|
|
}
|
|
|
|
if (i == 0) {
|
|
bootstrap_msg =
|
|
"instance key must contain at least one letter or digit";
|
|
return -1;
|
|
} else if (i > 10) {
|
|
bootstrap_msg =
|
|
"instance key must be shorter than 10 characters";
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// validate_instance_name performs full validation of the given snap instance name.
|
|
int validate_instance_name(const char *instance_name)
|
|
{
|
|
// NOTE: This function should be synchronized with the two other
|
|
// implementations: sc_instance_name_validate and snap.ValidateInstanceName.
|
|
|
|
if (instance_name == NULL) {
|
|
bootstrap_msg = "snap instance name cannot be NULL";
|
|
return -1;
|
|
}
|
|
// 40 char snap_name + '_' + 10 char instance_key + 1 extra overflow + 1
|
|
// NULL
|
|
char s[53] = { 0 };
|
|
strncpy(s, instance_name, sizeof(s) - 1);
|
|
|
|
char *t = s;
|
|
const char *snap_name = strsep(&t, "_");
|
|
const char *instance_key = strsep(&t, "_");
|
|
const char *third_separator = strsep(&t, "_");
|
|
if (third_separator != NULL) {
|
|
bootstrap_msg =
|
|
"snap instance name can contain only one underscore";
|
|
return -1;
|
|
}
|
|
|
|
if (validate_snap_name(snap_name) < 0) {
|
|
return -1;
|
|
}
|
|
// When the instance_name is a normal snap name, instance_key will be
|
|
// NULL, so only validate instance_key when we found one.
|
|
if (instance_key != NULL && instance_key_validate(instance_key) < 0) {
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// parse the -u argument, returns -1 on failure or 0 on success.
|
|
static int parse_arg_u(int argc, char *const *argv, int *optind,
|
|
unsigned long *uid_out)
|
|
{
|
|
if (*optind + 1 == argc || argv[*optind + 1] == NULL) {
|
|
bootstrap_msg = "-u requires an argument";
|
|
bootstrap_errno = 0;
|
|
return -1;
|
|
}
|
|
const char *uid_text = argv[*optind + 1];
|
|
errno = 0;
|
|
char *uid_text_end = NULL;
|
|
unsigned long parsed_uid = strtoul(uid_text, &uid_text_end, 10);
|
|
int saved_errno = errno;
|
|
char c = *uid_text;
|
|
if (
|
|
/* Reject overflow in parsed representation */
|
|
(parsed_uid == ULONG_MAX && errno != 0)
|
|
/* Reject leading whitespace allowed by strtoul. */
|
|
/* NOTE: We are reimplementing isspace() here.
|
|
* For context see
|
|
* https://github.com/golang/go/issues/29689 */
|
|
|| c == ' ' || c == '\t' || c == '\v' || c == '\r'
|
|
|| c == '\n'
|
|
/* Reject empty string. */
|
|
|| (*uid_text == '\0')
|
|
/* Reject partially parsed strings. */
|
|
|| (*uid_text != '\0' && uid_text_end != NULL
|
|
&& *uid_text_end != '\0')) {
|
|
bootstrap_msg = "cannot parse user id";
|
|
bootstrap_errno = saved_errno;
|
|
return -1;
|
|
}
|
|
if ((long)parsed_uid < 0) {
|
|
bootstrap_msg = "user id cannot be negative";
|
|
bootstrap_errno = 0;
|
|
return -1;
|
|
}
|
|
if (uid_out != NULL) {
|
|
*uid_out = parsed_uid;
|
|
}
|
|
*optind += 1; // Account for the argument to -u.
|
|
return 0;
|
|
}
|
|
|
|
// process_arguments parses given a command line
|
|
// argc and argv are defined as for the main() function
|
|
void process_arguments(int argc, char *const *argv, const char **snap_name_out,
|
|
bool *should_setns_out, bool *process_user_fstab,
|
|
unsigned long *uid_out)
|
|
{
|
|
// Find the name of the called program. If it is ending with ".test" then do nothing.
|
|
// NOTE: This lets us use cgo/go to write tests without running the bulk
|
|
// of the code automatically.
|
|
//
|
|
if (argv == NULL || argc < 1) {
|
|
bootstrap_errno = 0;
|
|
bootstrap_msg = "argv0 is corrupted";
|
|
return;
|
|
}
|
|
const char *argv0 = argv[0];
|
|
const char *argv0_suffix_maybe = strstr(argv0, ".test");
|
|
if (argv0_suffix_maybe != NULL
|
|
&& argv0_suffix_maybe[strlen(".test")] == '\0') {
|
|
bootstrap_errno = 0;
|
|
bootstrap_msg = "bootstrap is not enabled while testing";
|
|
return;
|
|
}
|
|
|
|
bool should_setns = true;
|
|
bool user_fstab = false;
|
|
const char *snap_name = NULL;
|
|
|
|
// Sanity check the command line arguments. The go parts will
|
|
// scan this too.
|
|
int i;
|
|
for (i = 1; i < argc; i++) {
|
|
const char *arg = argv[i];
|
|
if (arg[0] == '-') {
|
|
/* We have an option */
|
|
if (!strcmp(arg, "--from-snap-confine")) {
|
|
// When we are running under "--from-snap-confine"
|
|
// option skip the setns call as snap-confine has
|
|
// already placed us in the right namespace.
|
|
should_setns = false;
|
|
} else if (!strcmp(arg, "--user-mounts")) {
|
|
user_fstab = true;
|
|
// Processing the user-fstab file implies we're being
|
|
// called from snap-confine.
|
|
should_setns = false;
|
|
} else if (!strcmp(arg, "-u")) {
|
|
if (parse_arg_u(argc, argv, &i, uid_out) < 0) {
|
|
return;
|
|
}
|
|
// Providing an user identifier implies we are performing an
|
|
// update of a specific user mount namespace and that we are
|
|
// invoked from snapd and we should setns ourselves. When
|
|
// invoked from snap-confine we are only called with
|
|
// --from-snap-confine and with --user-mounts.
|
|
should_setns = true;
|
|
user_fstab = true;
|
|
} else {
|
|
bootstrap_errno = 0;
|
|
bootstrap_msg = "unsupported option";
|
|
return;
|
|
}
|
|
} else {
|
|
// We expect a single positional argument: the snap name
|
|
if (snap_name != NULL) {
|
|
bootstrap_errno = 0;
|
|
bootstrap_msg = "too many positional arguments";
|
|
return;
|
|
}
|
|
snap_name = arg;
|
|
}
|
|
}
|
|
|
|
// If there's no snap name given, just bail out.
|
|
if (snap_name == NULL) {
|
|
bootstrap_errno = 0;
|
|
bootstrap_msg = "snap name not provided";
|
|
return;
|
|
}
|
|
// Ensure that the snap instance name is valid so that we don't blindly setns into
|
|
// something that is controlled by a potential attacker.
|
|
if (validate_instance_name(snap_name) < 0) {
|
|
bootstrap_errno = 0;
|
|
// bootstap_msg is set by validate_instance_name;
|
|
return;
|
|
}
|
|
// We have a valid snap name now so let's store it.
|
|
if (snap_name_out != NULL) {
|
|
*snap_name_out = snap_name;
|
|
}
|
|
if (should_setns_out != NULL) {
|
|
*should_setns_out = should_setns;
|
|
}
|
|
if (process_user_fstab != NULL) {
|
|
*process_user_fstab = user_fstab;
|
|
}
|
|
bootstrap_errno = 0;
|
|
bootstrap_msg = NULL;
|
|
}
|
|
|
|
// bootstrap prepares snap-update-ns to work in the namespace of the snap given
|
|
// on command line.
|
|
void bootstrap(int argc, char **argv, char **envp)
|
|
{
|
|
// 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.
|
|
char *snapd_debug = getenv("SNAPD_DEBUG");
|
|
if (clearenv() != 0) {
|
|
bootstrap_errno = 0;
|
|
bootstrap_msg = "bootstrap could not clear the environment";
|
|
return;
|
|
}
|
|
if (snapd_debug != NULL) {
|
|
setenv("SNAPD_DEBUG", snapd_debug, 0);
|
|
}
|
|
// Analyze the read process cmdline to find the snap name and decide if we
|
|
// should use setns to jump into the mount namespace of a particular snap.
|
|
// This is spread out for easier testability.
|
|
const char *snap_name = NULL;
|
|
bool should_setns = false;
|
|
bool process_user_fstab = false;
|
|
unsigned long uid = 0;
|
|
process_arguments(argc, argv, &snap_name, &should_setns,
|
|
&process_user_fstab, &uid);
|
|
if (process_user_fstab) {
|
|
switch_to_privileged_user();
|
|
// switch_to_privileged_user sets bootstrap_{errno,msg}
|
|
} else if (snap_name != NULL && should_setns) {
|
|
setns_into_snap(snap_name);
|
|
// setns_into_snap sets bootstrap_{errno,msg}
|
|
}
|
|
}
|