summaryrefslogtreecommitdiff
path: root/secchan/executer.c
diff options
context:
space:
mode:
Diffstat (limited to 'secchan/executer.c')
-rw-r--r--secchan/executer.c519
1 files changed, 519 insertions, 0 deletions
diff --git a/secchan/executer.c b/secchan/executer.c
new file mode 100644
index 000000000..304df56c1
--- /dev/null
+++ b/secchan/executer.c
@@ -0,0 +1,519 @@
+/*
+ * Copyright (c) 2008, 2009 Nicira Networks.
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <config.h>
+#include "executer.h"
+#include <errno.h>
+#include <fcntl.h>
+#include <fnmatch.h>
+#include <poll.h>
+#include <signal.h>
+#include <stdlib.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <string.h>
+#include <unistd.h>
+#include "dirs.h"
+#include "dynamic-string.h"
+#include "fatal-signal.h"
+#include "openflow/nicira-ext.h"
+#include "ofpbuf.h"
+#include "openflow/openflow.h"
+#include "poll-loop.h"
+#include "rconn.h"
+#include "socket-util.h"
+#include "util.h"
+#include "vconn.h"
+
+#define THIS_MODULE VLM_executer
+#include "vlog.h"
+
+#define MAX_CHILDREN 8
+
+struct child {
+ /* Information about child process. */
+ char *name; /* argv[0] passed to child. */
+ pid_t pid; /* Child's process ID. */
+
+ /* For sending a reply to the controller when the child dies. */
+ struct rconn *rconn;
+ uint32_t xid; /* Transaction ID used by controller. */
+
+ /* We read up to MAX_OUTPUT bytes of output and send them back to the
+ * controller when the child dies. */
+#define MAX_OUTPUT 4096
+ int output_fd; /* FD from which to read child's output. */
+ uint8_t *output; /* Output data. */
+ size_t output_size; /* Number of bytes of output data so far. */
+};
+
+struct executer {
+ /* Settings. */
+ char *command_acl; /* Command white/blacklist, as shell globs. */
+ char *command_dir; /* Directory that contains commands. */
+
+ /* Children. */
+ struct child children[MAX_CHILDREN];
+ size_t n_children;
+};
+
+/* File descriptors for waking up when a child dies. */
+static int signal_fds[2];
+
+/* File descriptor for /dev/null. */
+static int null_fd = -1;
+
+static void send_child_status(struct rconn *, uint32_t xid, uint32_t status,
+ const void *data, size_t size);
+static void send_child_message(struct rconn *, uint32_t xid, uint32_t status,
+ const char *message);
+
+/* Returns true if 'cmd' is allowed by 'acl', which is a command-separated
+ * access control list in the format described for --command-acl in
+ * secchan(8). */
+static bool
+executer_is_permitted(const char *acl_, const char *cmd)
+{
+ char *acl, *save_ptr, *pattern;
+ bool allowed, denied;
+
+ /* Verify that 'cmd' consists only of alphanumerics plus _ or -. */
+ if (cmd[strspn(cmd, "abcdefghijklmnopqrstuvwxyz"
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-")] != '\0') {
+ VLOG_WARN("rejecting command name \"%s\" that contain forbidden "
+ "characters", cmd);
+ return false;
+ }
+
+ /* Check 'cmd' against 'acl'. */
+ acl = xstrdup(acl_);
+ save_ptr = acl;
+ allowed = denied = false;
+ while ((pattern = strsep(&save_ptr, ",")) != NULL && !denied) {
+ if (pattern[0] != '!' && !fnmatch(pattern, cmd, 0)) {
+ allowed = true;
+ } else if (pattern[0] == '!' && !fnmatch(pattern + 1, cmd, 0)) {
+ denied = true;
+ }
+ }
+ free(acl);
+
+ /* Check the command white/blacklisted state. */
+ if (allowed && !denied) {
+ VLOG_INFO("permitting command execution: \"%s\" is whitelisted", cmd);
+ } else if (allowed && denied) {
+ VLOG_WARN("denying command execution: \"%s\" is both blacklisted "
+ "and whitelisted", cmd);
+ } else if (!allowed) {
+ VLOG_WARN("denying command execution: \"%s\" is not whitelisted", cmd);
+ } else if (denied) {
+ VLOG_WARN("denying command execution: \"%s\" is blacklisted", cmd);
+ }
+ return allowed && !denied;
+}
+
+int
+executer_handle_request(struct executer *e, struct rconn *rconn,
+ struct nicira_header *request)
+{
+ char **argv;
+ char *args;
+ char *exec_file = NULL;
+ int max_fds;
+ struct stat s;
+ size_t args_size;
+ size_t argc;
+ size_t i;
+ pid_t pid;
+ int output_fds[2];
+
+ /* Verify limit on children not exceeded.
+ * XXX should probably kill children when the connection drops? */
+ if (e->n_children >= MAX_CHILDREN) {
+ send_child_message(rconn, request->header.xid, NXT_STATUS_ERROR,
+ "too many child processes");
+ return 0;
+ }
+
+ /* Copy argument buffer, adding a null terminator at the end. Now every
+ * argument is null-terminated, instead of being merely null-delimited. */
+ args_size = ntohs(request->header.length) - sizeof *request;
+ args = xmemdup0((const void *) (request + 1), args_size);
+
+ /* Count arguments. */
+ argc = 0;
+ for (i = 0; i <= args_size; i++) {
+ argc += args[i] == '\0';
+ }
+
+ /* Set argv[*] to point to each argument. */
+ argv = xmalloc((argc + 1) * sizeof *argv);
+ argv[0] = args;
+ for (i = 1; i < argc; i++) {
+ argv[i] = strchr(argv[i - 1], '\0') + 1;
+ }
+ argv[argc] = NULL;
+
+ /* Check permissions. */
+ if (!executer_is_permitted(e->command_acl, argv[0])) {
+ send_child_message(rconn, request->header.xid, NXT_STATUS_ERROR,
+ "command not allowed");
+ goto done;
+ }
+
+ /* Find the executable. */
+ exec_file = xasprintf("%s/%s", e->command_dir, argv[0]);
+ if (stat(exec_file, &s)) {
+ VLOG_WARN("failed to stat \"%s\": %s", exec_file, strerror(errno));
+ send_child_message(rconn, request->header.xid, NXT_STATUS_ERROR,
+ "command not allowed");
+ goto done;
+ }
+ if (!S_ISREG(s.st_mode)) {
+ VLOG_WARN("\"%s\" is not a regular file", exec_file);
+ send_child_message(rconn, request->header.xid, NXT_STATUS_ERROR,
+ "command not allowed");
+ goto done;
+ }
+ argv[0] = exec_file;
+
+ /* Arrange to capture output. */
+ if (pipe(output_fds)) {
+ VLOG_WARN("pipe failed: %s", strerror(errno));
+ send_child_message(rconn, request->header.xid, NXT_STATUS_ERROR,
+ "internal error (pipe)");
+ goto done;
+ }
+
+ pid = fork();
+ if (!pid) {
+ /* Running in child.
+ * XXX should run in new process group so that we can signal all
+ * subprocesses at once? Would also want to catch fatal signals and
+ * kill them at the same time though. */
+ fatal_signal_fork();
+ dup2(null_fd, 0);
+ dup2(output_fds[1], 1);
+ dup2(null_fd, 2);
+ max_fds = get_max_fds();
+ for (i = 3; i < max_fds; i++) {
+ close(i);
+ }
+ if (chdir(e->command_dir)) {
+ printf("could not change directory to \"%s\": %s",
+ e->command_dir, strerror(errno));
+ exit(EXIT_FAILURE);
+ }
+ execv(argv[0], argv);
+ printf("failed to start \"%s\": %s\n", argv[0], strerror(errno));
+ exit(EXIT_FAILURE);
+ } else if (pid > 0) {
+ /* Running in parent. */
+ struct child *child;
+
+ VLOG_INFO("started \"%s\" subprocess", argv[0]);
+ send_child_status(rconn, request->header.xid, NXT_STATUS_STARTED,
+ NULL, 0);
+ child = &e->children[e->n_children++];
+ child->name = xstrdup(argv[0]);
+ child->pid = pid;
+ child->rconn = rconn;
+ child->xid = request->header.xid;
+ child->output_fd = output_fds[0];
+ child->output = xmalloc(MAX_OUTPUT);
+ child->output_size = 0;
+ set_nonblocking(output_fds[0]);
+ close(output_fds[1]);
+ } else {
+ VLOG_WARN("fork failed: %s", strerror(errno));
+ send_child_message(rconn, request->header.xid, NXT_STATUS_ERROR,
+ "internal error (fork)");
+ close(output_fds[0]);
+ close(output_fds[1]);
+ }
+
+done:
+ free(exec_file);
+ free(args);
+ free(argv);
+ return 0;
+}
+
+static void
+send_child_status(struct rconn *rconn, uint32_t xid, uint32_t status,
+ const void *data, size_t size)
+{
+ if (rconn) {
+ struct nx_command_reply *r;
+ struct ofpbuf *buffer;
+
+ r = make_openflow_xid(sizeof *r, OFPT_VENDOR, xid, &buffer);
+ r->nxh.vendor = htonl(NX_VENDOR_ID);
+ r->nxh.subtype = htonl(NXT_COMMAND_REPLY);
+ r->status = htonl(status);
+ ofpbuf_put(buffer, data, size);
+ update_openflow_length(buffer);
+ if (rconn_send(rconn, buffer, NULL)) {
+ ofpbuf_delete(buffer);
+ }
+ }
+}
+
+static void
+send_child_message(struct rconn *rconn, uint32_t xid, uint32_t status,
+ const char *message)
+{
+ send_child_status(rconn, xid, status, message, strlen(message));
+}
+
+/* 'child' died with 'status' as its return code. Deal with it. */
+static void
+child_terminated(struct child *child, int status)
+{
+ struct ds ds;
+ uint32_t ofp_status;
+
+ /* Log how it terminated. */
+ ds_init(&ds);
+ if (WIFEXITED(status)) {
+ ds_put_format(&ds, "normally with status %d", WEXITSTATUS(status));
+ } else if (WIFSIGNALED(status)) {
+ const char *name = NULL;
+#ifdef HAVE_STRSIGNAL
+ name = strsignal(WTERMSIG(status));
+#endif
+ ds_put_format(&ds, "by signal %d", WTERMSIG(status));
+ if (name) {
+ ds_put_format(&ds, " (%s)", name);
+ }
+ }
+ if (WCOREDUMP(status)) {
+ ds_put_cstr(&ds, " (core dumped)");
+ }
+ VLOG_INFO("child process \"%s\" with pid %ld terminated %s",
+ child->name, (long int) child->pid, ds_cstr(&ds));
+ ds_destroy(&ds);
+
+ /* Send a status message back to the controller that requested the
+ * command. */
+ if (WIFEXITED(status)) {
+ ofp_status = WEXITSTATUS(status) | NXT_STATUS_EXITED;
+ } else if (WIFSIGNALED(status)) {
+ ofp_status = WTERMSIG(status) | NXT_STATUS_SIGNALED;
+ } else {
+ ofp_status = NXT_STATUS_UNKNOWN;
+ }
+ if (WCOREDUMP(status)) {
+ ofp_status |= NXT_STATUS_COREDUMP;
+ }
+ send_child_status(child->rconn, child->xid, ofp_status,
+ child->output, child->output_size);
+}
+
+/* Read output from 'child' and append it to its output buffer. */
+static void
+poll_child(struct child *child)
+{
+ ssize_t n;
+
+ if (child->output_fd < 0) {
+ return;
+ }
+
+ do {
+ n = read(child->output_fd, child->output + child->output_size,
+ MAX_OUTPUT - child->output_size);
+ } while (n < 0 && errno == EINTR);
+ if (n > 0) {
+ child->output_size += n;
+ if (child->output_size < MAX_OUTPUT) {
+ return;
+ }
+ } else if (n < 0 && errno == EAGAIN) {
+ return;
+ }
+ close(child->output_fd);
+ child->output_fd = -1;
+}
+
+void
+executer_run(struct executer *e)
+{
+ char buffer[MAX_CHILDREN];
+ size_t i;
+
+ if (!e->n_children) {
+ return;
+ }
+
+ /* Read output from children. */
+ for (i = 0; i < e->n_children; i++) {
+ struct child *child = &e->children[i];
+ poll_child(child);
+ }
+
+ /* If SIGCHLD was received, reap dead children. */
+ if (read(signal_fds[0], buffer, sizeof buffer) <= 0) {
+ return;
+ }
+ for (;;) {
+ int status;
+ pid_t pid;
+
+ /* Get dead child in 'pid' and its return code in 'status'. */
+ pid = waitpid(WAIT_ANY, &status, WNOHANG);
+ if (pid < 0 && errno == EINTR) {
+ continue;
+ } else if (pid <= 0) {
+ return;
+ }
+
+ /* Find child with given 'pid' and drop it from the list. */
+ for (i = 0; i < e->n_children; i++) {
+ struct child *child = &e->children[i];
+ if (child->pid == pid) {
+ poll_child(child);
+ child_terminated(child, status);
+ free(child->name);
+ free(child->output);
+ *child = e->children[--e->n_children];
+ goto found;
+ }
+ }
+ VLOG_WARN("child with unknown pid %ld terminated", (long int) pid);
+ found:;
+ }
+
+}
+
+void
+executer_wait(struct executer *e)
+{
+ if (e->n_children) {
+ size_t i;
+
+ /* Wake up on SIGCHLD. */
+ poll_fd_wait(signal_fds[0], POLLIN);
+
+ /* Wake up when we get output from a child. */
+ for (i = 0; i < e->n_children; i++) {
+ struct child *child = &e->children[i];
+ if (child->output_fd >= 0) {
+ poll_fd_wait(child->output_fd, POLLIN);
+ }
+ }
+ }
+}
+
+void
+executer_rconn_closing(struct executer *e, struct rconn *rconn)
+{
+ size_t i;
+
+ /* If any of our children was connected to 'r', then disconnect it so we
+ * don't try to reference a dead connection when the process terminates
+ * later.
+ * XXX kill the children started by 'r'? */
+ for (i = 0; i < e->n_children; i++) {
+ if (e->children[i].rconn == rconn) {
+ e->children[i].rconn = NULL;
+ }
+ }
+}
+
+static void
+sigchld_handler(int signr UNUSED)
+{
+ write(signal_fds[1], "", 1);
+}
+
+int
+executer_create(const char *command_acl, const char *command_dir,
+ struct executer **executerp)
+{
+ struct executer *e;
+ struct sigaction sa;
+
+ *executerp = NULL;
+ if (null_fd == -1) {
+ /* Create pipe for notifying us that SIGCHLD was invoked. */
+ if (pipe(signal_fds)) {
+ VLOG_ERR("pipe failed: %s", strerror(errno));
+ return errno;
+ }
+ set_nonblocking(signal_fds[0]);
+ set_nonblocking(signal_fds[1]);
+
+ /* Open /dev/null. */
+ null_fd = open("/dev/null", O_RDWR);
+ if (null_fd < 0) {
+ int error = errno;
+ VLOG_ERR("could not open /dev/null: %s", strerror(error));
+ close(signal_fds[0]);
+ close(signal_fds[1]);
+ return error;
+ }
+ }
+
+ /* Set up signal handler. */
+ memset(&sa, 0, sizeof sa);
+ sa.sa_handler = sigchld_handler;
+ sigemptyset(&sa.sa_mask);
+ sa.sa_flags = SA_NOCLDSTOP | SA_RESTART;
+ if (sigaction(SIGCHLD, &sa, NULL)) {
+ VLOG_ERR("sigaction(SIGCHLD) failed: %s", strerror(errno));
+ return errno;
+ }
+
+ e = xcalloc(1, sizeof *e);
+ e->command_acl = xstrdup(command_acl);
+ e->command_dir = (command_dir
+ ? xstrdup(command_dir)
+ : xasprintf("%s/commands", ovs_pkgdatadir));
+ e->n_children = 0;
+ *executerp = e;
+ return 0;
+}
+
+void
+executer_destroy(struct executer *e)
+{
+ if (e) {
+ size_t i;
+
+ free(e->command_acl);
+ free(e->command_dir);
+ for (i = 0; i < e->n_children; i++) {
+ struct child *child = &e->children[i];
+
+ free(child->name);
+ kill(child->pid, SIGHUP);
+ /* We don't own child->rconn. */
+ free(child->output);
+ free(child);
+ }
+ free(e);
+ }
+}
+
+void
+executer_set_acl(struct executer *e, const char *acl, const char *dir)
+{
+ free(e->command_acl);
+ e->command_acl = xstrdup(acl);
+ free(e->command_dir);
+ e->command_dir = xstrdup(dir);
+}