/*
Copyright (C) 2009 Zdenek Prikryl (zprikryl@redhat.com)
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 <fnmatch.h>
#include <glib.h>
#include "libabrt.h"
#include "rpm.h"
#define GPG_CONF "gpg_keys.conf"
/**
"python3.4, python3.5, python3.6, python3.7, perl, perl5.16.2"
* The regexes should cover interpreters with basename:
* Python:
* python
* python2
* python3
* python2.7
* python3.8
* platform-python
* platform-python3
* platform-python3.8
*
* Perl:
* perl
* perl5.30.1
*
* PHP:
* php
* php-cgi
*
* R:
* R
*
* tcl:
* tclsh
* tclsh8.6
**/
#define DEFAULT_INTERPRETERS_REGEX \
"^(perl ([[:digit:]][.][[:digit:]]+[.][[:digit:]])? |" \
"php (-cgi)? |" \
"(platform-)? python ([[:digit:]]([.][[:digit:]])?)? |" \
"R |" \
"tclsh ([[:digit:]][.][[:digit:]])?)$"
static bool settings_bOpenGPGCheck = false;
static GList *settings_setOpenGPGPublicKeys = NULL;
static GList *settings_setBlackListedPkgs = NULL;
static GList *settings_setBlackListedPaths = NULL;
static bool settings_bProcessUnpackaged = false;
static GList *settings_Interpreters = NULL;
static void ParseCommon(map_string_t *settings, const char *conf_filename)
{
const char *value;
value = get_map_string_item_or_NULL(settings, "OpenGPGCheck");
if (value)
{
settings_bOpenGPGCheck = string_to_bool(value);
remove_map_string_item(settings, "OpenGPGCheck");
}
value = get_map_string_item_or_NULL(settings, "BlackList");
if (value)
{
settings_setBlackListedPkgs = parse_list(value);
remove_map_string_item(settings, "BlackList");
}
value = get_map_string_item_or_NULL(settings, "BlackListedPaths");
if (value)
{
settings_setBlackListedPaths = parse_list(value);
remove_map_string_item(settings, "BlackListedPaths");
}
value = get_map_string_item_or_NULL(settings, "ProcessUnpackaged");
if (value)
{
settings_bProcessUnpackaged = string_to_bool(value);
remove_map_string_item(settings, "ProcessUnpackaged");
}
value = get_map_string_item_or_NULL(settings, "Interpreters");
if (value)
{
settings_Interpreters = parse_list(value);
remove_map_string_item(settings, "Interpreters");
}
map_string_iter_t iter;
const char *name;
/*char *value; - already declared */
init_map_string_iter(&iter, settings);
while (next_map_string_iter(&iter, &name, &value))
{
error_msg("Unrecognized variable '%s' in '%s'", name, conf_filename);
}
}
static void load_gpg_keys(void)
{
map_string_t *settings = new_map_string();
if (!load_abrt_conf_file(GPG_CONF, settings))
{
error_msg("Can't load '%s'", GPG_CONF);
return;
}
const char *gpg_keys_dir = get_map_string_item_or_NULL(settings, "GPGKeysDir");
if (gpg_keys_dir != NULL && strcmp(gpg_keys_dir, "") != 0)
{
log_debug("Reading gpg keys from '%s'", gpg_keys_dir);
GHashTable *done_set = g_hash_table_new(g_str_hash, g_str_equal);
GList *gpg_files = get_file_list(gpg_keys_dir, NULL /* we don't care about the file ext */);
for (GList *iter = gpg_files; iter; iter = g_list_next(iter))
{
const char *key_path = fo_get_fullpath((file_obj_t *)iter->data);
if (g_hash_table_contains(done_set, key_path))
continue;
g_hash_table_insert(done_set, (gpointer)key_path, NULL);
log_debug("Loading gpg key '%s'", key_path);
settings_setOpenGPGPublicKeys = g_list_append(settings_setOpenGPGPublicKeys, xstrdup(key_path));
}
g_list_free_full(gpg_files, (GDestroyNotify)free_file_obj);
g_hash_table_destroy(done_set);
}
}
static int load_conf(const char *conf_filename)
{
map_string_t *settings = new_map_string();
if (conf_filename != NULL)
{
if (!load_conf_file(conf_filename, settings, false))
error_msg("Can't open '%s'", conf_filename);
}
else
{
conf_filename = "abrt-action-save-package-data.conf";
if (!load_abrt_conf_file(conf_filename, settings))
error_msg("Can't load '%s'", conf_filename);
}
ParseCommon(settings, conf_filename);
free_map_string(settings);
load_gpg_keys();
return 0;
}
/**
* Returns the first full path argument in the command line or NULL.
* Skips options (params of the form "-XXX").
* Returns malloc'ed string.
* NB: the cmdline is delimited by (single, not multiple) spaces, not tabs!
* "abc def\t123" means there are two params: "abc", "def\t123".
* "abc def" means there are three params: "abc", "", "def".
*/
static char *get_argv1_if_full_path(const char* cmdline)
{
const char *argv1 = strchr(cmdline, ' ');
while (argv1 != NULL)
{
/* we found space in cmdline, so it might contain
* path to some script like:
* /usr/bin/python [-XXX] /usr/bin/system-control-network
*/
argv1++; /* skip the space */
if (*argv1 != '-')
break;
/* looks like -XXX in "perl -XXX /usr/bin/script.pl", skipping */
argv1 = strchr(argv1, ' ');
}
/* if the string following the space doesn't start
* with '/', it is not a full path to script
* and we can't use it to determine the package name
*/
if (argv1 == NULL || *argv1 != '/')
return NULL;
/* good, it has "/foo/bar" form, return it */
int len = strchrnul(argv1, ' ') - argv1;
return xstrndup(argv1, len);
}
static bool is_path_blacklisted(const char *path)
{
GList *li;
for (li = settings_setBlackListedPaths; li != NULL; li = g_list_next(li))
{
if (fnmatch((char*)li->data, path, /*flags:*/ 0) == 0)
{
return true;
}
}
return false;
}
static struct pkg_envra *get_script_name(const char *cmdline, char **executable, const char *chroot)
{
// TODO: we don't verify that python executable is not modified
// or that python package is properly signed
// (see CheckFingerprint/CheckHash below)
/* Try to find package for the script by looking at argv[1].
* This will work only if the cmdline contains the whole path.
* Example: python /usr/bin/system-control-network
*/
struct pkg_envra *script_pkg = NULL;
char *script_name = get_argv1_if_full_path(cmdline);
if (script_name)
{
script_pkg = rpm_get_package_nvr(script_name, chroot);
if (script_pkg)
{
/* There is a well-formed script name in argv[1],
* and it does belong to some package.
* Replace executable
* with data pertaining to the script.
*/
*executable = script_name;
}
}
return script_pkg;
}
static int SavePackageDescriptionToDebugDump(const char *dump_dir_name, const char *chroot)
{
struct dump_dir *dd = dd_opendir(dump_dir_name, /*flags:*/ 0);
if (!dd)
return 1;
char *type = dd_load_text(dd, FILENAME_TYPE);
bool kernel_oops = !strcmp(type, "Kerneloops") || !strcmp(type, "vmcore");
free(type);
char *cmdline = NULL;
char *executable = NULL;
char *rootdir = NULL;
char *package_short_name = NULL;
char *fingerprint = NULL;
struct pkg_envra *pkg_name = NULL;
char *component = NULL;
char *kernel = NULL;
int error = 1;
/* note: "goto ret" statements below free all the above variables,
* but they don't dd_close(dd) */
if (kernel_oops)
{
kernel = dd_load_text(dd, FILENAME_KERNEL);
if (!kernel)
{
log_warning("File 'kernel' containing kernel version not "
"found in current directory");
goto ret;
}
/* Trim trailing white-spaces. */
strchrnul(kernel, ' ')[0] = '\0';
log_info("Looking for kernel package");
executable = xasprintf("/boot/vmlinuz-%s", kernel);
}
else
{
cmdline = dd_load_text_ext(dd, FILENAME_CMDLINE, DD_FAIL_QUIETLY_ENOENT);
executable = dd_load_text(dd, FILENAME_EXECUTABLE);
}
/* Do not implicitly query rpm database in process's root dir, if
* ExploreChroots is disabled. */
if (g_settings_explorechroots && chroot == NULL)
chroot = rootdir = dd_load_text_ext(dd, FILENAME_ROOTDIR,
DD_FAIL_QUIETLY_ENOENT | DD_LOAD_TEXT_RETURN_NULL_ON_FAILURE);
/* Close dd while we query package database. It can take some time,
* don't want to keep dd locked longer than necessary */
dd_close(dd);
dd = NULL;
/* The check for kernel_oops is there because it could be an unexpected
* behaviour. If one wants to ignore kernel oops, she/he should disable
* the corresponding services. */
if (!kernel_oops && is_path_blacklisted(executable))
{
log_warning("Blacklisted executable '%s'", executable);
goto ret; /* return 1 (failure) */
}
pkg_name = rpm_get_package_nvr(executable, chroot);
if (!pkg_name)
{
if (settings_bProcessUnpackaged)
{
log_info("Crash in unpackaged executable '%s', "
"proceeding without packaging information", executable);
goto ret0; /* no error */
}
if (kernel_oops)
log_warning("Can't find kernel package corresponding to '%s'", kernel);
else
log_warning("Executable '%s' doesn't belong to any package"
" and ProcessUnpackaged is set to 'no'", executable);
goto ret; /* return 1 (failure) */
}
if (kernel_oops)
goto skip_interpreter;
/* Check well-known interpreter names */
const char *basename = strrchr(executable, '/');
if (basename)
basename++;
else
basename = executable;
/* if basename is known interpreter, we want to blame the running script
* not the interpreter
*/
if (g_regex_match_simple(DEFAULT_INTERPRETERS_REGEX, basename, G_REGEX_EXTENDED, /*MatchFlags*/0) ||
g_list_find_custom(settings_Interpreters, basename, (GCompareFunc)g_strcmp0))
{
struct pkg_envra *script_pkg = get_script_name(cmdline, &executable, chroot);
/* executable may have changed, check it again */
if (is_path_blacklisted(executable))
{
log_warning("Blacklisted executable '%s'", executable);
goto ret; /* return 1 (failure) */
}
if (!script_pkg)
{
/* Script name is not absolute, or it doesn't
* belong to any installed package.
*/
if (!settings_bProcessUnpackaged)
{
log_warning("Interpreter crashed, but no packaged script detected: '%s'", cmdline);
goto ret; /* return 1 (failure) */
}
/* Unpackaged script, but the settings says we want to keep it.
* BZ plugin wont allow to report this anyway, because component
* is missing, so there is no reason to mark it as not_reportable.
* Someone might want to use abrt to report it using ftp.
*/
goto ret0;
}
free_pkg_envra(pkg_name);
pkg_name = script_pkg;
}
skip_interpreter:
package_short_name = xasprintf("%s", pkg_name->p_name);
log_info("Package:'%s' short:'%s'", pkg_name->p_nvr, package_short_name);
/* The check for kernel_oops is there because it could be an unexpected
* behaviour. If one wants to ignore kernel oops, she/he should disable
* the corresponding services. */
if (!kernel_oops && g_list_find_custom(settings_setBlackListedPkgs, package_short_name, (GCompareFunc)g_strcmp0))
{
log_warning("Blacklisted package '%s'", package_short_name);
goto ret; /* return 1 (failure) */
}
fingerprint = rpm_get_fingerprint(package_short_name);
if (!(fingerprint != NULL && rpm_fingerprint_is_imported(fingerprint))
&& settings_bOpenGPGCheck)
{
log_warning("Package '%s' isn't signed with proper key", package_short_name);
goto ret; /* return 1 (failure) */
/* We used to also check the integrity of the executable here:
* if (!CheckHash(package_short_name.c_str(), executable)) BOOM();
* Checking the MD5 sum requires to run prelink to "un-prelink" the
* binaries - this is considered potential security risk so we don't
* do it now, until we find some non-intrusive way.
*/
}
component = rpm_get_component(executable, chroot);
dd = dd_opendir(dump_dir_name, /*flags:*/ 0);
if (!dd)
goto ret; /* return 1 (failure) */
if (pkg_name)
{
dd_save_text(dd, FILENAME_PACKAGE, pkg_name->p_nvr);
dd_save_text(dd, FILENAME_PKG_EPOCH, pkg_name->p_epoch);
dd_save_text(dd, FILENAME_PKG_NAME, pkg_name->p_name);
dd_save_text(dd, FILENAME_PKG_VERSION, pkg_name->p_version);
dd_save_text(dd, FILENAME_PKG_RELEASE, pkg_name->p_release);
dd_save_text(dd, FILENAME_PKG_ARCH, pkg_name->p_arch);
dd_save_text(dd, FILENAME_PKG_VENDOR, pkg_name->p_vendor);
if (fingerprint)
{
/* 16 character + 3 spaces + 1 '\0' + 2 Bytes for errors :) */
char key_fingerprint[22] = {0};
/* The condition is just a defense against errors */
for (size_t i = 0, j = 0; j < sizeof(key_fingerprint) - 2; )
{
key_fingerprint[j++] = toupper(fingerprint[i++]);
if (fingerprint[i] == '\0')
break;
if (!(i & (0x3)))
key_fingerprint[j++] = ' ';
}
dd_save_text(dd, FILENAME_PKG_FINGERPRINT, key_fingerprint);
}
}
if (component)
dd_save_text(dd, FILENAME_COMPONENT, component);
ret0:
error = 0;
ret:
if (dd)
dd_close(dd);
free(cmdline);
free(executable);
free(rootdir);
free(package_short_name);
free_pkg_envra(pkg_name);
free(component);
free(fingerprint);
return error;
}
int main(int argc, char **argv)
{
/* I18n */
setlocale(LC_ALL, "");
#if ENABLE_NLS
bindtextdomain(PACKAGE, LOCALEDIR);
textdomain(PACKAGE);
#endif
abrt_init(argv);
const char *dump_dir_name = ".";
const char *conf_filename = NULL;
const char *chroot = NULL;
/* Can't keep these strings/structs static: _() doesn't support that */
const char *program_usage_string = _(
"& [-v] [-c CONFFILE] [-r CHROOT] -d DIR\n"
"\n"
"Query package database and save package and component name"
);
enum {
OPT_v = 1 << 0,
OPT_d = 1 << 1,
OPT_c = 1 << 2,
OPT_r = 1 << 2,
};
/* Keep enum above and order of options below in sync! */
struct options program_options[] = {
OPT__VERBOSE(&g_verbose),
OPT_STRING('d', NULL, &dump_dir_name, "DIR" , _("Problem directory")),
OPT_STRING('c', NULL, &conf_filename, "CONFFILE", _("Configuration file")),
OPT_STRING('r', "chroot", &chroot, "CHROOT" , _("Use this directory as RPM root")),
OPT_END()
};
/*unsigned opts =*/ parse_opts(argc, argv, program_options, program_usage_string);
export_abrt_envvars(0);
log_notice("Loading settings");
if (load_conf(conf_filename) != 0)
return 1; /* syntax error (logged already by load_conf) */
log_notice("Initializing rpm library");
rpm_init();
GList *li;
for (li = settings_setOpenGPGPublicKeys; li != NULL; li = g_list_next(li))
{
log_notice("Loading GPG key '%s'", (char*)li->data);
rpm_load_gpgkey((char*)li->data);
}
int r = SavePackageDescriptionToDebugDump(dump_dir_name, chroot);
/* Close RPM database */
rpm_destroy();
return r;
}