/*
    Copyright (C) 2009 RedHat inc.

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    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, write to the Free Software Foundation, Inc.,
    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include <sys/statvfs.h>
#include "internal_libabrt.h"

int abrt_low_free_space(unsigned setting_MaxCrashReportsSize, const char *dump_location)
{
    struct statvfs vfs;
    if (statvfs(dump_location, &vfs) != 0)
    {
        perror_msg("statvfs('%s')", dump_location);
        return 0;
    }

    /* Check that at least MaxCrashReportsSize/4 MBs are free */

    /* fs_free_mb_x4 ~= vfs.f_bfree * vfs.f_bsize * 4, expressed in MBytes.
     * Need to neither overflow nor round f_bfree down too much. */
    unsigned long fs_free_mb_x4 = ((unsigned long long)vfs.f_bfree / (1024/4)) * vfs.f_bsize / 1024;
    if (fs_free_mb_x4 < setting_MaxCrashReportsSize)
    {
        error_msg("Only %luMiB is available on %s",
                          fs_free_mb_x4 / 4, dump_location);
        return 1;
    }
    return 0;
}

/* rhbz#539551: "abrt going crazy when crashing process is respawned".
 * Check total size of problem dirs, if it overflows,
 * delete oldest/biggest dirs.
 */
void abrt_trim_problem_dirs(const char *dirname, double cap_size, const char *exclude_path)
{
    const char *excluded_basename = NULL;
    if (exclude_path)
    {
        unsigned len_dirname = strlen(dirname);
        /* Trim trailing '/'s, but dont trim name "/" to "" */
        while (len_dirname > 1 && dirname[len_dirname-1] == '/')
            len_dirname--;
        if (strncmp(dirname, exclude_path, len_dirname) == 0
         && exclude_path[len_dirname] == '/'
        ) {
            /* exclude_path is "dirname/something" */
            excluded_basename = exclude_path + len_dirname + 1;
        }
    }
    log_debug("excluded_basename:'%s'", excluded_basename);

    int count = 20;
    while (--count >= 0)
    {
        /* We exclude our own dir from candidates for deletion (3rd param): */
        g_autofree char *worst_basename = NULL;
        double cur_size = libreport_get_dirsize_find_largest_dir(dirname, &worst_basename, excluded_basename, NULL);
        if (cur_size <= cap_size || !worst_basename)
        {
            log_info("cur_size:%.0f cap_size:%.0f, no (more) trimming", cur_size, cap_size);
            break;
        }
        log_warning("%s is %.0f bytes (more than %.0fMiB), deleting '%s'",
                dirname, cur_size, cap_size / (1024*1024), worst_basename);
        g_autofree char *d = g_build_filename(dirname ? dirname : "", worst_basename, NULL);
        delete_dump_dir(d);
    }
}

/**
 *
 * @param[out] status See `man 2 wait` for status information.
 * @return Malloc'ed string
 */
static char* exec_vp(char **args, int redirect_stderr, int exec_timeout_sec, int *status)
{
    /* Nuke everything which may make setlocale() switch to non-POSIX locale:
     * we need to avoid having gdb output in some obscure language.
     */
    static const char *const env_vec[] = {
        "LANG",
        "LC_ALL",
        "LC_COLLATE",
        "LC_CTYPE",
        "LC_MESSAGES",
        "LC_MONETARY",
        "LC_NUMERIC",
        "LC_TIME",
        /* Workaround for
         * http://sourceware.org/bugzilla/show_bug.cgi?id=9622
         * (gdb emitting ESC sequences even with -batch)
         */
        "TERM",
        NULL
    };

    int flags = EXECFLG_INPUT_NUL | EXECFLG_OUTPUT | EXECFLG_SETSID | EXECFLG_QUIET;
    if (redirect_stderr)
        flags |= EXECFLG_ERR2OUT;
    VERB1 flags &= ~EXECFLG_QUIET;

    int pipeout[2];
    pid_t child = libreport_fork_execv_on_steroids(flags, args, pipeout, (char**)env_vec, /*dir:*/ NULL, /*uid(unused):*/ 0);

    /* We use this function to run gdb and unstrip. Bugs in gdb or corrupted
     * coredumps were observed to cause gdb to enter infinite loop.
     * Therefore we have a (largish) timeout, after which we kill the child.
     */
    libreport_ndelay_on(pipeout[0]);
    time_t starttime = time(NULL);
    time_t endtime = starttime + exec_timeout_sec;
    GString *buf_out = g_string_new(NULL);
    while (1)
    {
        double timeout = difftime(endtime, starttime);
        if (timeout < 0)
        {
            kill(child, SIGKILL);
            g_string_append_printf(buf_out, "\n"
                        "Timeout exceeded: %u seconds, killing %s.\n"
                        "Looks like gdb hung while generating backtrace.\n"
                        "This may be a bug in gdb. Consider submitting a bug report to gdb developers.\n"
                        "Please attach coredump from this crash to the bug report if you do.\n",
                        exec_timeout_sec, args[0]
            );
            break;
        }

        struct pollfd pfd;
        pfd.fd = pipeout[0];
        pfd.events = POLLIN;
        if (poll(&pfd, 1, timeout * 1000) < 0)
            pwarn_msg("poll() failed on output of %s", args[0]);

        char buff[1024];
        int r = read(pipeout[0], buff, sizeof(buff) - 1);
        if (r <= 0)
        {
            /* I did see EAGAIN happening here */
            if (r < 0 && errno == EAGAIN)
                goto next;
            break;
        }
        buff[r] = '\0';
        g_string_append(buf_out, buff);
 next:
        starttime = time(NULL);
    }
    close(pipeout[0]);

    /* Prevent having zombie child process, and maybe collect status
     * (note that status == NULL is ok too) */
    libreport_safe_waitpid(child, status, 0);

    return g_string_free(buf_out, FALSE);
}

char *abrt_run_unstrip_n(const char *dump_dir_name, unsigned timeout_sec)
{
    int flags = EXECFLG_INPUT_NUL | EXECFLG_OUTPUT | EXECFLG_SETSID | EXECFLG_QUIET;
    VERB1 flags &= ~EXECFLG_QUIET;
    int pipeout[2];
    char* args[4];
    args[0] = (char*)"eu-unstrip";
    args[1] = g_strdup_printf("--core=%s/"FILENAME_COREDUMP, dump_dir_name);
    args[2] = (char*)"-n";
    args[3] = NULL;
    pid_t child = libreport_fork_execv_on_steroids(flags, args, pipeout, /*env_vec:*/ NULL, /*dir:*/ NULL, /*uid(unused):*/ 0);
    free(args[1]);

    /* Bugs in unstrip or corrupted coredumps can cause it to enter infinite loop.
     * Therefore we have a (largish) timeout, after which we kill the child.
     */
    libreport_ndelay_on(pipeout[0]);
    time_t starttime = time(NULL);
    time_t endtime = starttime + timeout_sec;
    GString *buf_out = g_string_new(NULL);
    while (1)
    {
        double timeout = difftime(endtime, starttime);
        if (timeout < 0)
        {
            kill(child, SIGKILL);
            g_string_free(buf_out, TRUE);
            buf_out = NULL;
            break;
        }

        struct pollfd pfd;
        pfd.fd = pipeout[0];
        pfd.events = POLLIN;
        if (poll(&pfd, 1, timeout * 1000) < 0)
            pwarn_msg("poll() failed on output of eu-unstrip");

        char buff[1024];
        int r = read(pipeout[0], buff, sizeof(buff) - 1);
        if (r <= 0)
        {
            /* I did see EAGAIN happening here */
            if (r < 0 && errno == EAGAIN)
                goto next;
            break;
        }
        buff[r] = '\0';
        g_string_append(buf_out, buff);
 next:
        starttime = time(NULL);
    }
    close(pipeout[0]);

    /* Prevent having zombie child process */
    int status;
    libreport_safe_waitpid(child, &status, 0);

    if (status != 0 || buf_out == NULL)
    {
        /* unstrip didnt exit with exit code 0, or we timed out */
        g_string_free(buf_out, TRUE);
        return NULL;
    }

    return g_string_free(buf_out, FALSE);
}

char *abrt_get_backtrace(struct dump_dir *dd, unsigned timeout_sec, const char *debuginfo_dirs)
{
    INITIALIZE_LIBABRT();

    char *executable = NULL;
    if (dd_exist(dd, FILENAME_BINARY))
        executable = g_build_filename(dd->dd_dirname ? dd->dd_dirname : "", FILENAME_BINARY, NULL);
    else
        executable = dd_load_text(dd, FILENAME_EXECUTABLE);

    /* Let user know what's going on */
    log_warning(_("Generating backtrace"));

    unsigned i = 0;
    char *args[25];
    args[i++] = (char*)GDB;
    args[i++] = (char*)"-batch";
    GString *set_debug_file_directory = g_string_new(NULL);
    unsigned auto_load_base_index = 0;
    if(debuginfo_dirs != NULL)
    {
        g_string_append(set_debug_file_directory, "set debug-file-directory /usr/lib/debug:/usr/lib");

        GString *debug_directories = g_string_new(NULL);
        const char *p = debuginfo_dirs;
        while (1)
        {
            while (*p == ':')
                p++;
            if (*p == '\0')
                break;
            const char *colon_or_nul = strchrnul(p, ':');
            g_string_append_printf(debug_directories,
                               "%s%.*s/usr/lib/debug:%.*s/usr/lib",
                               (debug_directories->len == 0 ? "" : ":"),
                               (int)(colon_or_nul - p), p,
                               (int)(colon_or_nul - p), p);
            p = colon_or_nul;
        }

        g_string_append_printf(set_debug_file_directory, ":%s", debug_directories->str);

        args[i++] = (char*)"-iex";
        auto_load_base_index = i;
        args[i++] = g_strdup_printf("add-auto-load-safe-path %s", debug_directories->str);
        args[i++] = (char*)"-iex";
        args[i++] = g_strdup_printf("add-auto-load-scripts-directory %s", debug_directories->str);

        g_string_free(debug_directories, TRUE);
    }

    args[i++] = (char*)"-ex";
    args[i++] = (char*)"set debuginfod enabled on";
    const unsigned debug_dir_cmd_index = i++;
    args[debug_dir_cmd_index] = g_string_free(set_debug_file_directory, FALSE);

    /* "file BINARY_FILE" is needed, without it gdb cannot properly
     * unwind the stack. Currently the unwind information is located
     * in .eh_frame which is stored only in binary, not in coredump
     * or debuginfo.
     *
     * Fedora GDB does not strictly need it, it will find the binary
     * by its build-id.  But for binaries either without build-id
     * (= built on non-Fedora GCC) or which do not have
     * their debuginfo rpm installed gdb would not find BINARY_FILE
     * so it is still makes sense to supply "file BINARY_FILE".
     *
     * Unfortunately, "file BINARY_FILE" doesn't work well if BINARY_FILE
     * was deleted (as often happens during system updates):
     * gdb uses specified BINARY_FILE
     * even if it is completely unrelated to the coredump.
     * See https://bugzilla.redhat.com/show_bug.cgi?id=525721
     *
     * TODO: check mtimes on COREFILE and BINARY_FILE and not supply
     * BINARY_FILE if it is newer (to at least avoid gdb complaining).
     */
    args[i++] = (char*)"-ex";
    const unsigned file_cmd_index = i++;
    args[file_cmd_index] = g_strdup_printf("file %s", executable);
    g_free(executable);

    args[i++] = (char*)"-ex";
    const unsigned core_cmd_index = i++;
    args[core_cmd_index] = g_strdup_printf("core-file %s/"FILENAME_COREDUMP, dd->dd_dirname);

    args[i++] = (char*)"-ex";
    const unsigned bt_cmd_index = i++;
    /*args[9] = ... see below */
    args[i++] = (char*)"-ex";
    args[i++] = (char*)"info sharedlib";
    /* glibc's abort() stores its message in __abort_msg variable */
    args[i++] = (char*)"-ex";
    args[i++] = (char*)"print (char*)__abort_msg";
    args[i++] = (char*)"-ex";
    args[i++] = (char*)"print (char*)__glib_assert_msg";
    args[i++] = (char*)"-ex";
    args[i++] = (char*)"info all-registers";
    args[i++] = (char*)"-ex";
    const unsigned dis_cmd_index = i++;
    args[dis_cmd_index] = (char*)"disassemble";
    args[i++] = NULL;

    /* Get the backtrace, but try to cap its size */
    /* Limit bt depth. With no limit, gdb sometimes OOMs the machine */
    unsigned bt_depth = 1024;
    const char *thread_apply_all = "thread apply all -ascending";
    const char *full = "full ";
    char *bt = NULL;
    while (1)
    {
        args[bt_cmd_index] = g_strdup_printf("%s backtrace %s%u", thread_apply_all, full, bt_depth);
        bt = exec_vp(args, /*redirect_stderr:*/ 1, timeout_sec, NULL);
        free(args[bt_cmd_index]);
        if ((bt && strnlen(bt, 256*1024) < 256*1024) || bt_depth <= 32)
        {
            break;
        }

        bt_depth /= 2;
        if (bt)
        {
            log_warning("Backtrace is too big (%u bytes), reducing depth to %u",
                        (unsigned)strlen(bt), bt_depth);
        }
        else
        {
            /* (NB: in fact, current impl. of exec_vp() never returns NULL) */
            log_warning("Failed to generate backtrace, reducing depth to %u",
                        bt_depth);
            g_clear_pointer(&bt, free);
        }

        /* Replace -ex disassemble (which disasms entire function $pc points to)
         * to a version which analyzes limited, small patch of code around $pc.
         * (Users reported a case where bare "disassemble" attempted to process
         * entire .bss).
         * TODO: what if "$pc-N" underflows? in my test, this happens:
         * Dump of assembler code from 0xfffffffffffffff0 to 0x30:
         * End of assembler dump.
         * (IOW: "empty" dump)
         */
        args[dis_cmd_index] = (char*)"disassemble $pc-20, $pc+64";

        if (bt_depth <= 64 && thread_apply_all[0] != '\0')
        {
            /* This program likely has gazillion threads, dont try to bt them all */
            bt_depth = 128;
            thread_apply_all = "";
        }
        if (bt_depth <= 64 && full[0] != '\0')
        {
            /* Looks like there are gigantic local structures or arrays, disable "full" bt */
            bt_depth = 128;
            full = "";
        }
    }

    if (auto_load_base_index > 0)
    {
        free(args[auto_load_base_index]);
        free(args[auto_load_base_index + 2]);
    }

    free(args[debug_dir_cmd_index]);
    free(args[file_cmd_index]);
    free(args[core_cmd_index]);
    return bt;
}

char* problem_data_save(problem_data_t *pd)
{
    abrt_load_abrt_conf();

    struct dump_dir *dd = create_dump_dir_from_problem_data_ext(pd, abrt_g_settings_dump_location, /*fs owner*/0);

    char *problem_id = NULL;
    if (dd)
    {
        problem_id = g_strdup(dd->dd_dirname);
        dd_close(dd);
    }

    log_info("problem id: '%s'", problem_id);
    return problem_id;
}

int dump_suid_policy()
{
    /*
     - values are:
       0 - don't dump suided programs - in this case the hook is not called by kernel
       1 - create coredump readable by fs_uid
       2 - create coredump readable by root only
    */
    int c;
    int suid_dump_policy = 0;
    const char *filename = "/proc/sys/fs/suid_dumpable";
    FILE *f  = fopen(filename, "r");
    if (!f)
    {
        log_warning("Can't open %s", filename);
        return suid_dump_policy;
    }

    c = fgetc(f);
    fclose(f);
    if (c != EOF)
        suid_dump_policy = c - '0';

    //log_warning("suid dump policy is: %i", suid_dump_policy);
    return suid_dump_policy;
}

int signal_is_fatal(int signal_no, const char **name)
{
    const char *signame = NULL;
    switch (signal_no)
    {
        case SIGILL : signame = "ILL" ; break;
        case SIGFPE : signame = "FPE" ; break;
        case SIGSEGV: signame = "SEGV"; break;
        case SIGBUS : signame = "BUS" ; break; //Bus error (bad memory access)
        case SIGABRT: signame = "ABRT"; break; //usually when abort() was called
    // We have real-world reports from users who see buggy programs
    // dying with SIGTRAP, uncommented it too:
        case SIGTRAP: signame = "TRAP"; break; //Trace/breakpoint trap
    // SIGSYS is sent when a process uses a forbidden syscall, which is
    // definitely a program bug
        case SIGSYS : signame = "SYS" ; break; //Bad argument to routine (SVr4)
    // These usually aren't caused by bugs:
      //case SIGQUIT: signame = "QUIT"; break; //Quit from keyboard
      //case SIGXCPU: signame = "XCPU"; break; //CPU time limit exceeded (4.2BSD)
      //case SIGXFSZ: signame = "XFSZ"; break; //File size limit exceeded (4.2BSD)
    }

    if (name != NULL)
        *name = signame;

    return signame != NULL;
}

void abrt_ensure_writable_dir_uid_gid(const char *dir, mode_t mode, uid_t uid, gid_t gid)
{
    struct stat sb;
    int dir_fd;

    if (mkdir(dir, mode) != 0 && errno != EEXIST)
        perror_msg_and_die("Can't create '%s'", dir);

    dir_fd = open(dir, O_DIRECTORY | O_NOFOLLOW);
    if (dir_fd < 0)
        perror_msg_and_die("Can't open directory '%s'", dir);

    if (fstat(dir_fd, &sb) != 0)
        perror_msg_and_die("Can't stat directory '%s'", dir);

    if ((sb.st_uid != uid || sb.st_gid != gid) && fchown(dir_fd, uid, gid) != 0)
        perror_msg_and_die("Can't set owner %u:%u on '%s'", (unsigned int)uid, (unsigned int)gid, dir);

    if ((sb.st_mode & 07777) != mode && fchmod(dir_fd, mode) != 0)
        perror_msg_and_die("Can't set mode %o on '%s'", mode, dir);

    close(dir_fd);
}

void abrt_ensure_writable_dir(const char *dir, mode_t mode, const char *user)
{
    struct passwd *pw = getpwnam(user);
    if (!pw)
        perror_msg_and_die("Can't find user '%s'", user);

    abrt_ensure_writable_dir_uid_gid(dir, mode, pw->pw_uid, pw->pw_gid);
}

void abrt_ensure_writable_dir_group(const char *dir, mode_t mode, const char *user, const char *group)
{
    struct passwd *pw = getpwnam(user);
    if (!pw)
        perror_msg_and_die("Can't find user '%s'", user);

    struct group *gr = getgrnam(group);
    if (!gr)
        perror_msg_and_die("Can't find group '%s'", group);

    abrt_ensure_writable_dir_uid_gid(dir, mode, pw->pw_uid, gr->gr_gid);
}

bool abrt_dir_is_in_dump_location(const char *dir_name)
{
    unsigned len = strlen(abrt_g_settings_dump_location);

    /* The path must start with "abrt_g_settings_dump_location" */
    if (strncmp(dir_name, abrt_g_settings_dump_location, len) != 0)
    {
        log_debug("Bad parent directory: '%s' not in '%s'", abrt_g_settings_dump_location, dir_name);
        return false;
    }

    /* and must be a sub-directory of the abrt_g_settings_dump_location dir */
    const char *base_name = dir_name + len;
    while (*base_name && *base_name == '/')
        ++base_name;

    if (*(base_name - 1) != '/' || !libreport_str_is_correct_filename(base_name))
    {
        log_debug("Invalid dump directory name: '%s'", base_name);
        return false;
    }

    /* and we are sure it is a directory */
    struct stat sb;
    if (lstat(dir_name, &sb) < 0)
    {
        VERB2 perror_msg("stat('%s')", dir_name);
        return errno== ENOENT;
    }

    return S_ISDIR(sb.st_mode);
}

bool abrt_dir_has_correct_permissions(const char *dir_name, int flags)
{
    struct stat statbuf;
    if (lstat(dir_name, &statbuf) != 0 || !S_ISDIR(statbuf.st_mode))
    {
        error_msg("Path '%s' isn't directory", dir_name);
        return false;
    }

    /* Get ABRT's group id */
    struct group *gr = getgrnam("abrt");
    if (!gr)
    {
        error_msg("The group 'abrt' does not exist");
        return false;
    }

    /* The group must be root or abrt. */
    const bool correct_group =    statbuf.st_gid == 0
                               || statbuf.st_gid == gr->gr_gid;

    /* Require owner 'root' and group 'abrt' for the event shell scripts.
     * Because the shell scripts are vulnerable to hard link and symbolic link
     * attacks.
     */
    const bool events =    statbuf.st_uid == 0
                        && correct_group
                        && (statbuf.st_mode & S_IWGRP) == 0
                        && (statbuf.st_mode & S_IWOTH) == 0;

    if ((flags & DD_PERM_EVENTS))
        return events;

    /* Be less restrictive and allow the daemos (abrtd, abrt-dbus) to work with
     * dump directories having group 'root' or 'abrt'.
     *
     * 1. Chowning of dump directories switches the ownership to 'user':'abrt'
     * 2. We want to allow users to delete their problems
     * 3. We want to allow users to modify their data
     * 4. The daemons are hardened against hard link and symbolic link issues.
     */
    return correct_group;
}

bool problem_entry_is_post_create_condition(const char *name)
{
    return    strcmp(name, FILENAME_TYPE) == 0
           /* Replaced with FILENAME_TYPE on '2015-01-15'
            * commit a6efe199922165725b200298d5b276b52912f3dd
            * Users scripts might not be adapted to it yet.
            */
           || strcmp(name, FILENAME_ANALYZER) == 0
           /* Compatibility value used in abrt-server.
            * There still might some scripts using it.
            */
           || strcmp(name, "basename") == 0;
}

bool abrt_new_user_problem_entry_allowed(uid_t uid, const char *name, const char *value)
{
    /* Allow root to create everything */
    if (uid == 0)
        return true;

    /* Permit non-root users to create everything except post-create condition elements */
    if (!problem_entry_is_post_create_condition(name))
        return true;

    /* Permit non-root users to create all types except: C/C++, Koops, vmcore and xorg */
     if (strcmp(value, "CCpp") != 0
      && strcmp(value, "Kerneloops") != 0
      && strcmp(value, "vmcore") != 0
      && strcmp(value, "xorg") != 0)
        return true;

    error_msg("Only root is permitted to create element '%s' containing '%s'", name, value);
    return false;
}
