/*
 * 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 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>

/* copied from libsnap-confine-private/utils.h */
#define SC_ARRAY_SIZE(arr)                                                                                  \
    (sizeof(arr) / sizeof((arr)[0]) + ((int)sizeof(struct {                                                 \
         _Static_assert(!__builtin_types_compatible_p(typeof(arr), typeof(&(arr)[0])), "must be an array"); \
     })))

// 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;
}

/* TODO:nonsetuid: drop this code, move clearing and verification of
 * CAP_SYS_ADMIN to single function */
// keep_sys_admin sets CAP_SYS_ADMIN in the effective capability set (required
// for mount()) and drops everything else.
static int keep_sys_admin(void) {
    // _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);
    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 enable capabilities after switching to real user";
        return -1;
    }

    return 0;
}

// verify we have required capabilities
static int verify_caps(void) {
    /* nothing to do if running as a root */
    if (getuid() == 0) {
        return 0;
    }

    // _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 = {0};

    if (capget(&hdr, &data) != 0) {
        bootstrap_errno = errno;
        bootstrap_msg = "cannot query capabilities";
        return -1;
    }

    struct {
        int cap;
        const char *err;
    } expected_caps[] = {{
                             .cap = CAP_SYS_ADMIN,
                             .err = "CAP_SYS_ADMIN capability not in effective set",
                         },
                         {
                             .cap = CAP_CHOWN,
                             .err = "CAP_CHOWN capability not in effective set",
                         },
                         {
                             .cap = CAP_DAC_OVERRIDE,
                             .err = "CAP_DAC_OVERRIDE capability not in effective set",
                         }};
    size_t i;
    for (i = 0; i < SC_ARRAY_SIZE(expected_caps); i++) {
        if ((data.effective & CAP_TO_MASK(expected_caps[i].cap)) == 0) {
            bootstrap_errno = EPERM;
            bootstrap_msg = expected_caps[i].err;
            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.
static 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 snap-confine with capabilities carried over
    // across exec. 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);
    }

    if (verify_caps() != 0) {
        return;
    }

    // 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) {
        // since privileged are passed as capabilities, we re already invoked as
        // a target user, so we can drop evrything else only keeping
        // CAP_SYS_ADMIN for mount()
        keep_sys_admin();
        // keep_sys_admin sets bootstrap_{errno,msg}
    } else if (snap_name != NULL && should_setns) {
        setns_into_snap(snap_name);
        // setns_into_snap sets bootstrap_{errno,msg}
    }
}
