/*
* Copyright (C) 2011-2016 Red Hat, Inc.
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; If not, see <http://www.gnu.org/licenses/>.
*
* Author: tasleson
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <getopt.h>
#include <string.h>
#include <ctype.h>
#include <signal.h>
#include <sys/types.h>
#include <pwd.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <syslog.h>
#include <stdarg.h>
#include <dirent.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/queue.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <libgen.h>
#include <assert.h>
#include <grp.h>
#include <limits.h>
#include <libconfig.h>
#define BASE_DIR "/var/run/lsm"
#define SOCKET_DIR BASE_DIR"/ipc"
#define PLUGIN_DIR "/usr/bin"
#define LSM_USER "libstoragemgmt"
#define LSM_CONF_DIR "/etc/lsm/"
#define LSM_PLUGIN_CONF_DIR_NAME "pluginconf.d"
#define LSMD_CONF_FILE "lsmd.conf"
#define LSM_CONF_ALLOW_ROOT_OPT_NAME "allow-plugin-root-privilege"
#define LSM_CONF_REQUIRE_ROOT_OPT_NAME "require-root-privilege"
#define max(a,b) \
({ __typeof__ (a) _a = (a); \
__typeof__ (b) _b = (b); \
_a > _b ? _a : _b; })
int verbose_flag = 0;
int systemd = 0;
const char *socket_dir = SOCKET_DIR;
const char *plugin_dir = PLUGIN_DIR;
const char *conf_dir = LSM_CONF_DIR;
char plugin_extension[] = "_lsmplugin";
char plugin_conf_extension[] = ".conf";
typedef enum { RUNNING, RESTART, EXIT } serve_type;
serve_type serve_state = RUNNING;
int plugin_mem_debug = 0;
int allow_root_plugin = 0;
int has_root_plugin = 0;
/**
* Each item in plugin list contains this information
*/
struct plugin {
char *file_path;
int require_root;
int fd;
LIST_ENTRY(plugin) pointers;
};
/**
* Linked list of plug-ins
*/
LIST_HEAD(plugin_list, plugin) head;
/**
* Logs messages to the appropriate place
* @param severity Severity of message, LOG_ERR causes daemon to exit
* @param fmt String with format
* @param ... Format parameters
*/
void logger(int severity, const char *fmt, ...)
{
char buf[2048];
if (verbose_flag || LOG_WARNING == severity || LOG_ERR == severity) {
va_list arg;
va_start(arg, fmt);
vsnprintf(buf, sizeof(buf), fmt, arg);
va_end(arg);
if (!systemd) {
if (verbose_flag) {
syslog(LOG_ERR, "%s", buf);
} else {
syslog(severity, "%s", buf);
}
} else {
fprintf(stdout, "%s", buf);
fflush(stdout);
}
if (LOG_ERR == severity) {
exit(1);
}
}
}
#define log_and_exit(fmt, ...) logger(LOG_ERR, fmt, ##__VA_ARGS__)
#define warn(fmt, ...) logger(LOG_WARNING, fmt, ##__VA_ARGS__)
#define info(fmt, ...) logger(LOG_INFO, fmt, ##__VA_ARGS__)
/**
* Our signal handler.
* @param s Received signal
*/
void signal_handler(int s)
{
if (SIGTERM == s) {
serve_state = EXIT;
} else if (SIGHUP == s) {
serve_state = RESTART;
}
}
/**
* Installs our signal handler
*/
void install_sh(void)
{
if (signal(SIGTERM, signal_handler) == SIG_ERR) {
log_and_exit("Can't catch signal SIGTERM\n");
}
if (signal(SIGHUP, signal_handler) == SIG_ERR) {
log_and_exit("Can't catch signal SIGHUP\n");
}
}
/**
* If we are running as root, we will try to drop our privs. to our default
* user.
*/
void drop_privileges(void)
{
int err = 0;
struct passwd *pw = NULL;
pw = getpwnam(LSM_USER);
if (pw) {
if (!geteuid()) {
if (-1 == setgid(pw->pw_gid)) {
err = errno;
log_and_exit("Unexpected error on setgid(errno %d)\n", err);
}
if (-1 == setgroups(1, &pw->pw_gid)) {
err = errno;
log_and_exit("Unexpected error on setgroups(errno %d)\n", err);
}
if (-1 == setuid(pw->pw_uid)) {
err = errno;
log_and_exit("Unexpected error on setuid(errno %d)\n", err);
}
} else if (pw->pw_uid != getuid()) {
warn("Daemon not running as correct user\n");
}
} else {
info("Warn: Missing %s user, running as existing user!\n", LSM_USER);
}
}
/**
* Check to make sure we have access to the directories of interest
*/
void flight_check(void)
{
int err = 0;
if (-1 == access(socket_dir, R_OK | W_OK)) {
err = errno;
log_and_exit("Unable to access socket directory %s, errno= %d\n",
socket_dir, err);
}
if (-1 == access(plugin_dir, R_OK | X_OK)) {
err = errno;
log_and_exit("Unable to access plug-in directory %s, errno= %d\n",
plugin_dir, err);
}
}
/**
* Print help.
*/
void usage(void)
{
printf("libStorageMgmt plug-in daemon.\n");
printf("lsmd [--plugindir <directory>] [--socketdir <dir>] [-v] [-d]\n");
printf(" --plugindir = The directory where the plugins are located\n");
printf(" --socketdir = The directory where the Unix domain sockets will "
"be created\n");
printf(" --confdir = The directory where the config files are "
"located\n");
printf(" -v = Verbose logging\n");
printf(" -d = New style daemon (systemd)\n");
}
/**
* Concatenates a path and a file name.
* @param path Fully qualified path
* @param name File name
* @return Concatenated string, caller must call free when done
*/
char *path_form(const char *path, const char *name)
{
size_t s = strlen(path) + strlen(name) + 2;
char *full = calloc(1, s);
if (full) {
snprintf(full, s, "%s/%s", path, name);
} else {
log_and_exit("malloc failure while trying to allocate %d bytes\n", s);
}
return full;
}
/* Call back signature */
typedef int (*file_op) (void *p, char *full_file_path);
/**
* For a given directory iterate through each directory item and exec the
* callback, recursively process nested directories too.
* @param dir Directory to transverse
* @param p Pointer to user data (Optional)
* @param call_back Function to call against file
* @return
*/
void process_directory(const char *dir, void *p, file_op call_back)
{
int err = 0;
if (call_back && dir && strlen(dir)) {
DIR *dp = NULL;
struct dirent *entry = NULL;
char *full_name = NULL;
dp = opendir(dir);
if (dp) {
while ((entry = readdir(dp)) != NULL) {
struct stat entry_st;
free(full_name);
full_name = path_form(dir, entry->d_name);
if (lstat(full_name, &entry_st) != 0) {
continue;
}
if (S_ISDIR(entry_st.st_mode)) {
if (strncmp(entry->d_name, ".", 1) == 0) {
continue;
}
process_directory(full_name, p, call_back);
} else {
if (call_back(p, full_name)) {
break;
}
}
}
free(full_name);
if (closedir(dp)) {
err = errno;
log_and_exit("Error on closing dir %s: %s\n", dir,
strerror(err));
}
} else {
err = errno;
log_and_exit("Error on processing directory %s: %s\n", dir,
strerror(err));
}
}
}
/**
* Callback to remove a unix domain socket by deleting it.
* @param p Call back data
* @param full_name Full path an and file name
* @return 0 to continue processing, anything else to stop.
*/
int delete_socket(void *p, char *full_name)
{
struct stat statbuf;
int err;
assert(p == NULL);
if (!lstat(full_name, &statbuf)) {
if (S_ISSOCK(statbuf.st_mode)) {
if (unlink(full_name)) {
err = errno;
log_and_exit("Error on unlinking file %s: %s\n",
full_name, strerror(err));
}
}
}
return 0;
}
/**
* Walk the IPC socket directory and remove the socket files.
*/
void clean_sockets(void)
{
process_directory(socket_dir, NULL, delete_socket);
}
/**
* Given a socket file name, create the IPC socket.
* @param name socket file name for plug-in
* @return listening socket descriptor for IPC
*/
int setup_socket(char *name)
{
int err = 0;
char *socket_file = path_form(socket_dir, name);
delete_socket(NULL, socket_file);
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (-1 != fd) {
struct sockaddr_un addr;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, socket_file, sizeof(addr.sun_path) - 1);
if (-1 ==
bind(fd, (struct sockaddr *) &addr, sizeof(struct sockaddr_un))) {
err = errno;
log_and_exit("Error on binding socket %s: %s\n", socket_file,
strerror(err));
}
if (-1 == chmod(socket_file, S_IREAD | S_IWRITE | S_IRGRP | S_IWGRP
| S_IROTH | S_IWOTH)) {
err = errno;
log_and_exit("Error on chmod socket file %s: %s\n", socket_file,
strerror(err));
}
if (-1 == listen(fd, 5)) {
err = errno;
log_and_exit("Error on listening %s: %s\n", socket_file,
strerror(err));
}
} else {
err = errno;
log_and_exit("Error on socket create %s: %s\n",
socket_file, strerror(err));
}
free(socket_file);
return fd;
}
/**
* Closes all the listening sockets and re-claims memory in linked list.
* @param list
*/
void empty_plugin_list(struct plugin_list *list)
{
int err;
struct plugin *item = NULL;
while (!LIST_EMPTY(list)) {
item = LIST_FIRST(list);
LIST_REMOVE(item, pointers);
if (-1 == close(item->fd)) {
err = errno;
info("Error on closing fd %d for file %s: %s\n", item->fd,
item->file_path, strerror(err));
}
free(item->file_path);
item->file_path = NULL;
item->fd = INT_MAX;
free(item);
}
}
/**
* Parse config and seeking provided key name bool
* 1. Keep value untouched if file not exist
* 2. If file is not readable, abort via log_and_exit()
* 3. Keep value untouched if provided key not found
* 4. Abort via log_and_exit() if no enough memory.
* @param conf_path config file path
* @param key_name string, searching key
* @param value int, output, value of this config key
*/
void parse_conf_bool(const char *conf_path, const char *key_name, int *value)
{
if (access(conf_path, F_OK) == -1) {
/* file not exist. */
return;
}
config_t *cfg = (config_t *) malloc(sizeof(config_t));
if (cfg) {
config_init(cfg);
if (CONFIG_TRUE == config_read_file(cfg, conf_path)) {
config_lookup_bool(cfg, key_name, value);
} else {
log_and_exit("configure %s parsing failed: %s at line %d\n",
conf_path, config_error_text(cfg),
config_error_line(cfg));
}
} else {
log_and_exit
("malloc failure while trying to allocate memory for config_t\n");
}
config_destroy(cfg);
free(cfg);
}
/**
* Load plugin config for root privilege setting.
* If config not found, return 0 for no root privilege required.
* @param plugin_name plugin name.
* @return 1 for require root privilege, 0 or not.
*/
int chk_pconf_root_pri(char *plugin_name)
{
int require_root = 0;
size_t plugin_name_len = strlen(plugin_name);
size_t conf_ext_len = strlen(plugin_conf_extension);
ssize_t conf_file_name_len = plugin_name_len + conf_ext_len + 1;
char *plugin_conf_filename = (char *) malloc(conf_file_name_len);
if (plugin_conf_filename) {
snprintf(plugin_conf_filename, conf_file_name_len, "%s%s", plugin_name,
plugin_conf_extension);
char *plugin_conf_dir_path = path_form(conf_dir,
LSM_PLUGIN_CONF_DIR_NAME);
char *plugin_conf_path = path_form(plugin_conf_dir_path,
plugin_conf_filename);
parse_conf_bool(plugin_conf_path, LSM_CONF_REQUIRE_ROOT_OPT_NAME,
&require_root);
if (require_root == 1 && allow_root_plugin == 0) {
warn("Plugin %s require root privilege while %s disable globally\n",
plugin_name, LSMD_CONF_FILE);
}
free(plugin_conf_dir_path);
free(plugin_conf_filename);
free(plugin_conf_path);
} else {
log_and_exit("malloc failure while trying to allocate %d "
"bytes\n", conf_file_name_len);
}
return require_root;
}
/**
* Call back for plug-in processing.
* @param p Private data
* @param full_name Full path and file name
* @return 0 to continue, else abort directory processing
*/
int process_plugin(void *p, char *full_name)
{
char * base_nm = NULL;
size_t base_nm_len = 0;
size_t no_ext_len = 0;
char plugin_name[128];
size_t ext_len = strlen(plugin_extension);
size_t plugin_name_max_len = sizeof(plugin_name)/sizeof(char);
if (full_name == NULL)
return 0;
base_nm = basename(full_name);
base_nm_len = strlen(base_nm);
if (base_nm_len <= ext_len)
return 0;
if (strncmp(base_nm + base_nm_len - ext_len, plugin_extension, ext_len))
return 0;
struct plugin *item = calloc(1, sizeof(struct plugin));
if (item == NULL) {
log_and_exit("Memory allocation failure!\n");
return 0; // no use, just trick covscan;
}
/* Strip off _lsmplugin from the file name, not sure
* why I chose to do this */
memset(plugin_name, 0, plugin_name_max_len);
strncpy(plugin_name, base_nm, plugin_name_max_len - 1);
no_ext_len = base_nm_len - ext_len;
// Already check, no_ext_len is bigger than 0 here.
if (no_ext_len < plugin_name_max_len - 1)
plugin_name[no_ext_len] = '\0';
item->file_path = strdup(full_name);
item->fd = setup_socket(plugin_name);
item->require_root = chk_pconf_root_pri(plugin_name);
has_root_plugin |= item->require_root;
if (item->file_path && item->fd >= 0) {
LIST_INSERT_HEAD((struct plugin_list *) p, item,
pointers);
info("Plugin %s added\n", full_name);
} else {
/* The only real way to get here is failed strdup as
setup_socket will exit on error. */
free(item);
item = NULL;
log_and_exit("strdup failed %s\n", full_name);
}
return 0;
}
/**
* Cleans up any children that have exited.
*/
void child_cleanup(void)
{
int rc;
int err;
do {
siginfo_t si;
memset(&si, 0, sizeof(siginfo_t));
rc = waitid(P_ALL, 0, &si, WNOHANG | WEXITED);
if (-1 == rc) {
err = errno;
if (err != ECHILD) {
info("waitid %d - %s\n", err, strerror(err));
}
break;
} else {
if (0 == rc && si.si_pid == 0) {
break;
} else {
if (si.si_code == CLD_EXITED && si.si_status != 0) {
info("Plug-in process %d exited with %d\n", si.si_pid,
si.si_status);
}
}
}
} while (1);
}
/**
* Closes and frees memory and removes Unix domain sockets.
*/
void clean_up(void)
{
empty_plugin_list(&head);
clean_sockets();
}
/**
* Walks the plugin directory creating IPC sockets for each one.
* @return
*/
int process_plugins(void)
{
clean_up();
info("Scanning plug-in directory %s\n", plugin_dir);
process_directory(plugin_dir, &head, process_plugin);
if (allow_root_plugin == 1 && has_root_plugin == 0) {
info("No plugin requires root privilege, dropping root privilege\n");
flight_check();
drop_privileges();
}
return 0;
}
/**
* Given a socket descriptor looks it up and returns the plug-in
* @param fd Socket descriptor to lookup
* @return struct plugin
*/
struct plugin *plugin_lookup(int fd)
{
struct plugin *plug = NULL;
LIST_FOREACH(plug, &head, pointers) {
if (plug->fd == fd) {
return plug;
}
}
return NULL;
}
/**
* Does the actual fork and exec of the plug-in
* @param plugin Full filename and path of plug-in to exec.
* @param client_fd Client connected file descriptor
* @param require_root int, indicate whether this plugin require root
* privilege or not
*/
void exec_plugin(char *plugin, int client_fd, int require_root)
{
int err = 0;
info("Exec'ing plug-in = %s\n", plugin);
pid_t process = fork();
if (process) {
/* Parent */
int rc = close(client_fd);
if (-1 == rc) {
err = errno;
info("Error on closing accepted socket in parent: %s\n",
strerror(err));
}
} else {
/* Child */
int exec_rc = 0;
char fd_str[12];
const char *plugin_argv[7];
extern char **environ;
struct ucred cli_user_cred;
socklen_t cli_user_cred_len = sizeof(cli_user_cred);
/*
* The plugin will still run no matter with root privilege or not.
* so that client could get detailed error message.
*/
if (require_root == 0) {
drop_privileges();
} else {
if (getuid()) {
warn("Plugin %s require root privilege, but lsmd daemon "
"is not run as root user\n", plugin);
} else if (allow_root_plugin == 0) {
warn("Plugin %s require root privilege, but %s disabled "
"it globally\n", LSMD_CONF_FILE);
drop_privileges();
} else {
/* Check socket client uid */
int rc_get_cli_uid =
getsockopt(client_fd, SOL_SOCKET, SO_PEERCRED,
&cli_user_cred, &cli_user_cred_len);
if (0 == rc_get_cli_uid) {
if (cli_user_cred.uid != 0) {
warn("Plugin %s require root privilege, but "
"client is not run as root user\n", plugin);
drop_privileges();
} else {
info("Plugin %s is running as root privilege\n",
plugin);
}
} else {
warn("Failed to get client socket uid, getsockopt() "
"error: %d\n", errno);
drop_privileges();
}
}
}
/* Make copy of plug-in string as once we call empty_plugin_list it
* will be deleted :-) */
char *p_copy = strdup(plugin);
empty_plugin_list(&head);
sprintf(fd_str, "%d", client_fd);
if (plugin_mem_debug) {
char debug_out[64];
snprintf(debug_out, (sizeof(debug_out) - 1),
"--log-file=/tmp/leaking_%d-%d", getppid(), getpid());
plugin_argv[0] = "valgrind";
plugin_argv[1] = "--leak-check=full";
plugin_argv[2] = "--show-reachable=no";
plugin_argv[3] = debug_out;
plugin_argv[4] = p_copy;
plugin_argv[5] = fd_str;
plugin_argv[6] = NULL;
exec_rc = execve("/usr/bin/valgrind", (char * const*) plugin_argv,
environ);
} else {
plugin_argv[0] = basename(p_copy);
plugin_argv[1] = fd_str;
plugin_argv[2] = NULL;
exec_rc = execve(p_copy, (char * const*) plugin_argv, environ);
}
if (-1 == exec_rc) {
err = errno;
log_and_exit("Error on exec'ing Plugin %s: %s\n",
p_copy, strerror(err));
}
}
}
/**
* Main event loop
*/
void _serving(void)
{
struct plugin *plug = NULL;
struct timeval tmo;
fd_set readfds;
int nfds = 0;
int err = 0;
process_plugins();
while (serve_state == RUNNING) {
FD_ZERO(&readfds);
nfds = 0;
tmo.tv_sec = 15;
tmo.tv_usec = 0;
LIST_FOREACH(plug, &head, pointers) {
nfds = max(plug->fd, nfds);
FD_SET(plug->fd, &readfds);
}
if (!nfds) {
log_and_exit("No plugins found in directory %s\n", plugin_dir);
}
nfds += 1;
int ready = select(nfds, &readfds, NULL, NULL, &tmo);
if (-1 == ready) {
if (serve_state != RUNNING) {
return;
} else {
err = errno;
log_and_exit("Error on selecting Plugin: %s", strerror(err));
}
} else if (ready > 0) {
int fd = 0;
for (fd = 0; fd < nfds; fd++) {
if (FD_ISSET(fd, &readfds)) {
int cfd = accept(fd, NULL, NULL);
if (-1 != cfd) {
struct plugin *p = plugin_lookup(fd);
exec_plugin(p->file_path, cfd, p->require_root);
} else {
err = errno;
info("Error on accepting request: %s", strerror(err));
}
}
}
}
child_cleanup();
}
clean_up();
}
/**
* Main entry for daemon to work
*/
void serve(void)
{
while (serve_state != EXIT) {
if (serve_state == RESTART) {
info("Reloading plug-ins\n");
serve_state = RUNNING;
}
_serving();
}
clean_up();
}
int main(int argc, char *argv[])
{
int c = 0;
LIST_INIT(&head);
/* Process command line arguments */
while (1) {
static struct option l_options[] = {
{"help", no_argument, 0, 'h'}, //Index 0
{"plugindir", required_argument, 0, 0}, //Index 1
{"socketdir", required_argument, 0, 0}, //Index 2
{"confdir", required_argument, 0, 0}, //Index 3
{0, 0, 0, 0}
};
int option_index = 0;
c = getopt_long(argc, argv, "hvd", l_options, &option_index);
if (c == -1) {
break;
}
switch (c) {
case 0:
switch (option_index) {
case 1:
plugin_dir = optarg;
break;
case 2:
socket_dir = optarg;
break;
case 3:
conf_dir = optarg;
break;
}
break;
case 'h':
usage();
return EXIT_SUCCESS;
case 'v':
verbose_flag = 1;
break;
case 'd':
systemd = 1;
break;
case '?':
break;
default:
abort();
}
}
/* Print any remaining command line arguments (not options). */
if (optind < argc) {
printf("non-option ARGV-elements: ");
while (optind < argc) {
printf("%s \n", argv[optind++]);
}
printf("\n");
exit(1);
}
/* Setup syslog if needed */
if (!systemd) {
openlog("lsmd", LOG_ODELAY, LOG_USER);
}
/* Check lsmd.conf */
char *lsmd_conf_path = path_form(conf_dir, LSMD_CONF_FILE);
parse_conf_bool(lsmd_conf_path, (char *) LSM_CONF_ALLOW_ROOT_OPT_NAME,
&allow_root_plugin);
free(lsmd_conf_path);
/* Check to see if we want to check plugin for memory errors */
if (getenv("LSM_VALGRIND")) {
plugin_mem_debug = 1;
}
install_sh();
if (allow_root_plugin == 0) {
drop_privileges();
}
flight_check();
/* Become a daemon if told we are not using systemd */
if (!systemd) {
if (-1 == daemon(0, 0)) {
int err = errno;
log_and_exit("Error on calling daemon: %s\n", strerror(err));
}
}
serve();
return EXIT_SUCCESS;
}