diff --git a/agent/qrexec-agent-data.c b/agent/qrexec-agent-data.c index 8e760936..08fb71ff 100644 --- a/agent/qrexec-agent-data.c +++ b/agent/qrexec-agent-data.c @@ -138,22 +138,29 @@ int handle_handshake(libvchan_t *ctrl) static int handle_just_exec(struct qrexec_parsed_command *cmd) { - int fdn, pid; + int fdn, pid, log_fd; if (cmd == NULL) return QREXEC_EXIT_PROBLEM; + char file_path[QUBES_SOCKADDR_UN_MAX_PATH_LEN]; + struct buffer buf = { .data = file_path, .buflen = (int)sizeof(file_path) }; + struct buffer stdin_buffer; + buffer_init(&stdin_buffer); if (cmd->service_descriptor) { int socket_fd; struct buffer stdin_buffer; buffer_init(&stdin_buffer); - int status = find_qrexec_service(cmd, &socket_fd, &stdin_buffer); + int status = find_qrexec_service(cmd, &socket_fd, &stdin_buffer, &buf); if (status == -1) return QREXEC_EXIT_SERVICE_NOT_FOUND; if (status != 0) return QREXEC_EXIT_PROBLEM; if (socket_fd != -1) - return write_all(socket_fd, stdin_buffer.data, stdin_buffer.buflen) ? 0 : QREXEC_EXIT_PROBLEM; + return write_all(socket_fd, stdin_buffer.data, stdin_buffer.buflen) ? + 0 : QREXEC_EXIT_PROBLEM; + } else { + buf.data = NULL; } switch (pid = fork()) { case -1: @@ -161,11 +168,22 @@ static int handle_just_exec(struct qrexec_parsed_command *cmd) return QREXEC_EXIT_PROBLEM; case 0: fdn = open("/dev/null", O_RDWR); - fix_fds(fdn, fdn, fdn); - do_exec(cmd->command, cmd->username); + if (fdn < 0) { + LOG(ERROR, "open /dev/null failed"); + _exit(QREXEC_EXIT_PROBLEM); + } + int other_pid; + log_fd = cmd->service_descriptor ? open_logger(cmd, &other_pid) : fdn; + if (log_fd < 0) + _exit(QREXEC_EXIT_PROBLEM); + fix_fds(fdn, fdn, log_fd); + do_exec(buf.data, cmd->command, cmd->username); default:; } - LOG(INFO, "executed (nowait): %s (pid %d)", cmd->command, pid); + if (buf.data) + LOG(INFO, "executed (nowait): %s %s (pid %d)", buf.data, cmd->command, pid); + else + LOG(INFO, "executed (nowait): %s (pid %d)", cmd->command, pid); return 0; } @@ -261,6 +279,7 @@ static int handle_new_process_common( return 0; } + int logger_pid = 0; req.vchan = data_vchan; req.stdin_buf = &stdin_buf; @@ -280,6 +299,8 @@ static int handle_new_process_common( req.prefix_data.data = NULL; req.prefix_data.len = 0; + if (cmd->service_descriptor != NULL) + req.logger_fd = open_logger(cmd, &logger_pid); exit_code = qrexec_process_io(&req, cmd); diff --git a/agent/qrexec-agent.c b/agent/qrexec-agent.c index b4f41212..1d87132b 100644 --- a/agent/qrexec-agent.c +++ b/agent/qrexec-agent.c @@ -137,7 +137,7 @@ static struct pam_conv conv = { * If dom0 sends overly long cmd, it will probably crash qrexec-agent (unless * process can allocate up to 4GB on both stack and heap), sorry. */ -_Noreturn void do_exec(const char *cmd, const char *user) +_Noreturn void do_exec(const char *prog, const char *cmd, const char *user) { #ifdef HAVE_PAM int retval, status; @@ -172,7 +172,10 @@ _Noreturn void do_exec(const char *cmd, const char *user) exit(1); } /* call QUBESRPC if requested */ - exec_qubes_rpc_if_requested(cmd, environ); + if (prog) { + /* no point in creating a login shell for test environmens */ + exec_qubes_rpc2(prog, cmd, environ, false); + } /* otherwise exec shell */ execl("/bin/sh", "sh", "-c", cmd, NULL); @@ -279,8 +282,10 @@ _Noreturn void do_exec(const char *cmd, const char *user) warn("chdir(%s)", pw->pw_dir); /* call QUBESRPC if requested */ - exec_qubes_rpc_if_requested(cmd, env); - + if (prog) { + /* Set up environment variables for a login shell. */ + exec_qubes_rpc2(prog, cmd, env, true); + } /* otherwise exec shell */ execle(pw->pw_shell, arg0, "-c", cmd, (char*)NULL, env); _exit(QREXEC_EXIT_PROBLEM); @@ -317,8 +322,10 @@ _Noreturn void do_exec(const char *cmd, const char *user) exit(1); #else /* call QUBESRPC if requested */ - exec_qubes_rpc_if_requested(cmd, environ); - + if (prog) { + /* Set up environment variables for a login session. */ + exec_qubes_rpc2(prog, cmd, environ, true); + } /* otherwise exec shell */ execl("/bin/su", "su", "-", user, "-c", cmd, NULL); PERROR("execl"); diff --git a/agent/qrexec-agent.h b/agent/qrexec-agent.h index 83ed3f43..f85064ed 100644 --- a/agent/qrexec-agent.h +++ b/agent/qrexec-agent.h @@ -29,7 +29,7 @@ int handle_handshake(libvchan_t *ctrl); void handle_vchan_error(const char *op); -_Noreturn void do_exec(const char *cmd, const char *user); +_Noreturn void do_exec(const char *prog, const char *cmd, const char *user); /* call before fork() for service handling process (either end) */ void prepare_child_env(void); diff --git a/agent/qrexec-client-vm.c b/agent/qrexec-client-vm.c index 0f03bb8d..e933448b 100644 --- a/agent/qrexec-client-vm.c +++ b/agent/qrexec-client-vm.c @@ -43,7 +43,7 @@ _Noreturn void handle_vchan_error(const char *op) exit(1); } -_Noreturn void do_exec(const char *cmd __attribute__((unused)), char const* user __attribute__((__unused__))) { +_Noreturn void do_exec(const char *prog __attribute__((unused)), const char *cmd __attribute__((unused)), char const* user __attribute__((__unused__))) { LOG(ERROR, "BUG: do_exec function shouldn't be called!"); abort(); } diff --git a/agent/qrexec-fork-server.c b/agent/qrexec-fork-server.c index 9edba38e..34bc1fa4 100644 --- a/agent/qrexec-fork-server.c +++ b/agent/qrexec-fork-server.c @@ -37,7 +37,7 @@ extern char **environ; const bool qrexec_is_fork_server = true; -void do_exec(const char *cmd, const char *user __attribute__((unused))) +void do_exec(const char *prog, const char *cmd, const char *user __attribute__((unused))) { char *shell; @@ -45,7 +45,10 @@ void do_exec(const char *cmd, const char *user __attribute__((unused))) signal(SIGPIPE, SIG_DFL); /* call QUBESRPC if requested */ - exec_qubes_rpc_if_requested(cmd, environ); + if (prog != NULL) { + /* Already in login session. */ + exec_qubes_rpc2(prog, cmd, environ, false); + } /* otherwise, pass it to shell */ shell = getenv("SHELL"); diff --git a/daemon/qrexec-client.c b/daemon/qrexec-client.c index fb20aaf5..f0cbc393 100644 --- a/daemon/qrexec-client.c +++ b/daemon/qrexec-client.c @@ -62,13 +62,18 @@ static void set_remote_domain(const char *src_domain_name) { } /* called from do_fork_exec */ -static _Noreturn void do_exec(const char *prog, const char *username __attribute__((unused))) +static _Noreturn void do_exec(const char *prog, + const char *cmdline, + const char *username __attribute__((unused))) { - /* avoid calling qubes-rpc-multiplexer through shell */ - exec_qubes_rpc_if_requested(prog, environ); + /* avoid calling RPC service through shell */ + if (prog) { + /* qrexec-client is always in a login session. */ + exec_qubes_rpc2(prog, cmdline, environ, false); + } - /* if above haven't executed qubes-rpc-multiplexer, pass it to shell */ - execl("/bin/bash", "bash", "-c", prog, NULL); + /* if above haven't executed RPC service, pass it to shell */ + execl("/bin/bash", "bash", "-c", cmdline, NULL); PERROR("exec bash"); exit(1); } diff --git a/daemon/qrexec-daemon.c b/daemon/qrexec-daemon.c index 3d20849e..9578fdec 100644 --- a/daemon/qrexec-daemon.c +++ b/daemon/qrexec-daemon.c @@ -1131,14 +1131,18 @@ static enum policy_response connect_daemon_socket( } /* called from do_fork_exec */ -static _Noreturn void do_exec(const char *prog, const char *username __attribute__((unused))) +static _Noreturn void do_exec(const char *prog, const char *cmd, const char *username __attribute__((unused))) { - /* avoid calling qubes-rpc-multiplexer through shell */ - exec_qubes_rpc_if_requested(prog, environ); + /* avoid calling RPC command through shell */ + if (prog) { + /* qrexec-daemon is always in a login session already */ + exec_qubes_rpc2(prog, cmd, environ, false); + } - /* if above haven't executed qubes-rpc-multiplexer, pass it to shell */ - execl("/bin/bash", "bash", "-c", prog, NULL); + /* if above haven't executed RPC command, pass it to shell */ + execl("/bin/bash", "bash", "-c", cmd, NULL); PERROR("exec bash"); + /* treat ENOENT as "problem" because bash should always exist */ _exit(QREXEC_EXIT_PROBLEM); } diff --git a/debian/control b/debian/control index 58669b3b..bc12ba57 100644 --- a/debian/control +++ b/debian/control @@ -19,7 +19,7 @@ Homepage: https://www.qubes-os.org Package: qubes-core-qrexec Architecture: any Depends: - libqrexec-utils2 (= ${binary:Version}), + libqrexec-utils4 (= ${binary:Version}), python3-qrexec, ${shlibs:Depends}, ${misc:Depends} @@ -36,7 +36,7 @@ Description: Qubes qrexec agent Agent part of Qubes RPC system. A daemon responsible for starting processes as requested by dom0 or other VMs, according to dom0-enforced policy. -Package: libqrexec-utils2 +Package: libqrexec-utils4 Architecture: any Depends: ${shlibs:Depends}, ${misc:Depends} Breaks: qubes-utils (<< 3.1.4) @@ -47,7 +47,7 @@ Description: Library of common functions of qrexec agent and daemon Package: libqrexec-utils-dev Architecture: any Section: libdevel -Depends: libqrexec-utils2 (= ${binary:Version}), ${misc:Depends} +Depends: libqrexec-utils4 (= ${binary:Version}), ${misc:Depends} Breaks: qubes-utils (<< 3.1.4) Replaces: qubes-utils (<< 3.1.4) Description: Development headers for libqrexec-utils diff --git a/debian/libqrexec-utils2.install b/debian/libqrexec-utils2.install deleted file mode 100644 index 5f64a9a0..00000000 --- a/debian/libqrexec-utils2.install +++ /dev/null @@ -1 +0,0 @@ -usr/lib/libqrexec-utils.so.3* diff --git a/debian/libqrexec-utils2.shlibs b/debian/libqrexec-utils2.shlibs deleted file mode 100644 index f6a013f3..00000000 --- a/debian/libqrexec-utils2.shlibs +++ /dev/null @@ -1 +0,0 @@ -libqrexec-utils 3 libqrexec-utils2 (>= 4.1.0) diff --git a/debian/libqrexec-utils4.install b/debian/libqrexec-utils4.install new file mode 100644 index 00000000..3b2f712c --- /dev/null +++ b/debian/libqrexec-utils4.install @@ -0,0 +1 @@ +usr/lib/libqrexec-utils.so.4* diff --git a/debian/libqrexec-utils4.shlibs b/debian/libqrexec-utils4.shlibs new file mode 100644 index 00000000..68a934d8 --- /dev/null +++ b/debian/libqrexec-utils4.shlibs @@ -0,0 +1 @@ +libqrexec-utils 4 libqrexec-utils4 (>=4.2.18) diff --git a/libqrexec/Makefile b/libqrexec/Makefile index 3838f6a6..6d95c040 100644 --- a/libqrexec/Makefile +++ b/libqrexec/Makefile @@ -10,7 +10,7 @@ CFLAGS += -I. -I../libqrexec -g -O2 -Wall -Wextra -Werror \ LDFLAGS += -pie -Wl,-z,relro,-z,now -shared -SO_VER=3 +SO_VER=4 VCHANLIBS := $(shell pkg-config --libs vchan) LIBDIR ?= /usr/lib INCLUDEDIR ?= /usr/include @@ -22,7 +22,7 @@ endif all: libqrexec-utils.so -libqrexec-utils.so.$(SO_VER): unix-server.o ioall.o buffer.o exec.o txrx-vchan.o write-stdin.o replace.o remote.o process_io.o log.o toml.o vchan_timeout.o +libqrexec-utils.so.$(SO_VER): unix-server.o ioall.o buffer.o exec.o txrx-vchan.o write-stdin.o replace.o remote.o process_io.o log.o toml.o open_logger.o vchan_timeout.o $(CC) $(LDFLAGS) -Wl,-soname,$@ -o $@ $^ $(VCHANLIBS) libqrexec-utils.so: libqrexec-utils.so.$(SO_VER) diff --git a/libqrexec/buffer.c b/libqrexec/buffer.c index 1c49a07e..217d94dd 100644 --- a/libqrexec/buffer.c +++ b/libqrexec/buffer.c @@ -35,12 +35,12 @@ static char *limited_malloc(int len) (total_mem > BUFFER_LIMIT) || (len <= 0)) { LOG(ERROR, "attempt to allocate >BUFFER_LIMIT"); - exit(1); + abort(); } ret = malloc((size_t)len); if (!ret) { PERROR("malloc"); - exit(1); + abort(); } return ret; } @@ -83,11 +83,11 @@ void buffer_append(struct buffer *b, const char *data, int len) assert(data != NULL && "NULL data"); if (b->buflen < 0 || b->buflen > BUFFER_LIMIT) { LOG(ERROR, "buffer_append buflen %d", len); - exit(1); + abort(); } if (len < 0 || len > BUFFER_LIMIT) { LOG(ERROR, "buffer_append %d", len); - exit(1); + abort(); } if (len == 0) return; @@ -108,7 +108,7 @@ void buffer_remove(struct buffer *b, int len) char *qdata = NULL; if (len < 0 || len > b->buflen) { LOG(ERROR, "buffer_remove %d/%d", len, b->buflen); - exit(1); + abort(); } newsize = b->buflen - len; if (newsize > 0) { diff --git a/libqrexec/exec.c b/libqrexec/exec.c index 1c6196c2..9d5653b0 100644 --- a/libqrexec/exec.c +++ b/libqrexec/exec.c @@ -36,6 +36,7 @@ #include #include #include + #include "qrexec.h" #include "libqrexec-utils.h" #include "private.h" @@ -47,44 +48,127 @@ void register_exec_func(do_exec_t *func) { exec_func = func; } -void exec_qubes_rpc_if_requested(const char *prog, char *const envp[]) { - /* avoid calling qubes-rpc-multiplexer through shell */ - if (strncmp(prog, RPC_REQUEST_COMMAND, RPC_REQUEST_COMMAND_LEN) == 0) { - char *prog_copy; - char *tok, *savetok; - char *argv[16]; // right now 6 are used, but allow future extensions - size_t i = 1; +#define STR_LITERAL_SIZE(a) (sizeof("" a) - 1) +#define STARTS_WITH_LITERAL(a, b) (strncmp(a, b, STR_LITERAL_SIZE(b)) == 0) - if (prog[RPC_REQUEST_COMMAND_LEN] != ' ') { - LOG(ERROR, "\"" RPC_REQUEST_COMMAND "\" not followed by space"); - _exit(126); +static bool should_strip_env_var(const char *var) +{ + if (!STARTS_WITH_LITERAL(var, "QREXEC")) + return false; + const char *rest = var + STR_LITERAL_SIZE("QREXEC"); + return !STARTS_WITH_LITERAL(rest, "_SERVICE_PATH=") && + !STARTS_WITH_LITERAL(rest, "_AGENT_PID="); +} + +void exec_qubes_rpc2(const char *program, const char *cmd, char *const envp[], + bool use_shell) { + /* avoid calling RPC service through shell */ + char *prog_copy; + char *tok, *savetok; + const char *argv[6]; + size_t i = 0; +#define MAX_ADDED_ENV_VARS 5 + size_t const extra_env_vars = MAX_ADDED_ENV_VARS; + size_t env_amount = extra_env_vars; + + for (char *const *env = envp; *env; ++env) { + // Set this 0 to 1 if adding new variable settings below, + // to ensure that MAX_ADDED_ENV_VARS is correct. + if (0 && should_strip_env_var(*env)) + continue; + env_amount++; + } +#define EXTEND(...) \ + do { \ + if (iterator >= env_amount) \ + abort(); \ + if (asprintf(&buf[iterator++], __VA_ARGS__) < 0) \ + goto bad_asprintf; \ + } while (0) +#define EXTEND_RAW(arg) \ + do { \ + if (iterator >= env_amount) \ + abort(); \ + buf[iterator++] = (arg); \ + } while (0) + + char **buf = calloc(env_amount + 1, sizeof(char *)); + if (buf == NULL) { + LOG(ERROR, "calloc(%zu, %zu) failed: %m", env_amount, sizeof(char *)); + _exit(QREXEC_EXIT_PROBLEM); + } + size_t iterator = 0; + for (char *const *env = envp; *env; ++env) { + if (!should_strip_env_var(*env)) { + EXTEND_RAW(*env); } + } + + prog_copy = strdup(cmd); + if (!prog_copy) { + PERROR("strdup"); + _exit(QREXEC_EXIT_PROBLEM); + } - prog_copy = strdup(prog + RPC_REQUEST_COMMAND_LEN + 1); - if (!prog_copy) { - PERROR("strdup"); + argv[i++] = program; + tok=strtok_r(prog_copy, " ", &savetok); + while (tok != NULL) { + if (i >= sizeof(argv)/sizeof(argv[0])-1) { + LOG(ERROR, "Too many arguments to %s", RPC_REQUEST_COMMAND); _exit(QREXEC_EXIT_PROBLEM); } - - tok=strtok_r(prog_copy, " ", &savetok); - while (tok != NULL) { - if (i >= sizeof(argv)/sizeof(argv[0])-1) { - LOG(ERROR, "To many arguments to %s", RPC_REQUEST_COMMAND); - _exit(QREXEC_EXIT_PROBLEM); - } - argv[i++] = tok; - tok=strtok_r(NULL, " ", &savetok); + argv[i++] = tok; + tok = strtok_r(NULL, " ", &savetok); + } + argv[i] = NULL; + if (i == 5) { + EXTEND("QREXEC_REQUESTED_TARGET_TYPE=%s", argv[3]); + if (strcmp(argv[3], "name") == 0) { + EXTEND("QREXEC_REQUESTED_TARGET=%s", argv[4]); + } else if (strcmp(argv[3], "keyword") == 0) { + EXTEND("QREXEC_REQUESTED_TARGET_KEYWORD=%s", argv[4]); + } else { + // requested target type unknown or not given, ignore } - argv[i] = NULL; - - argv[0] = getenv("QREXEC_MULTIPLEXER_PATH"); - if (!argv[0]) - argv[0] = QUBES_RPC_MULTIPLEXER_PATH; - execve(argv[0], argv, envp); - bool noent = errno == ENOENT; - PERROR("exec qubes-rpc-multiplexer"); - _exit(noent ? QREXEC_EXIT_SERVICE_NOT_FOUND : QREXEC_EXIT_PROBLEM); + } else if (i == 3) { + EXTEND_RAW("QREXEC_REQUESTED_TARGET_TYPE="); + } else { + LOG(ERROR, "invalid number of arguments: %zu", i); + _exit(QREXEC_EXIT_PROBLEM); } + EXTEND("QREXEC_SERVICE_FULL_NAME=%s", argv[1]); + EXTEND("QREXEC_REMOTE_DOMAIN=%s", argv[2]); + + const char *p = strchr(argv[1], '+'); + argv[1] = NULL; + if (p != NULL) { + EXTEND("QREXEC_SERVICE_ARGUMENT=%s", p + 1); + if (p[1] != '\0') { + argv[1] = p + 1; + argv[2] = NULL; + } + } + assert(iterator <= env_amount); + buf[iterator] = NULL; + if (program[0] != '/') { + LOG(ERROR, "Program to execute not absolute path"); + _exit(QREXEC_EXIT_PROBLEM); + } + if (use_shell) { + argv[3] = argv[0]; + argv[4] = argv[1]; + argv[5] = NULL; + argv[0] = "-sh"; + argv[1] = "-lcunset LESSOPEN LESSCLOSE&&exec \"$@\""; + argv[2] = "sh"; + execve("/bin/sh", (char *const *)argv, buf); + } else { + execve(program, (char *const *)argv, buf); + } + _exit(errno == ENOENT ? QREXEC_EXIT_SERVICE_NOT_FOUND : QREXEC_EXIT_PROBLEM); +bad_asprintf: + PERROR("asprintf"); + _exit(QREXEC_EXIT_PROBLEM); } void fix_fds(int fdin, int fdout, int fderr) @@ -94,7 +178,9 @@ void fix_fds(int fdin, int fdout, int fderr) fdin, fdout, fderr); _exit(125); } - if (dup2(fdin, 0) < 0 || dup2(fdout, 1) < 0 || dup2(fderr, 2) < 0) { + if ((fdin != 0 && dup2(fdin, 0) < 0) || + (fdout != 1 && dup2(fdout, 1) < 0) || + (fderr != 2 && dup2(fderr, 2) < 0)) { PERROR("dup2"); abort(); } @@ -112,7 +198,8 @@ void fix_fds(int fdin, int fdout, int fderr) close(i); } -static int do_fork_exec(const char *user, +static int do_fork_exec(const char *prog, + const char *user, const char *cmdline, int *pid, int *stdin_fd, @@ -144,7 +231,7 @@ static int do_fork_exec(const char *user, fix_fds(inpipe[0], outpipe[1], 2); if (exec_func != NULL) - exec_func(cmdline, user); + exec_func(prog, cmdline, user); abort(); default: retval = 0; @@ -438,7 +525,9 @@ struct qrexec_parsed_command *parse_qubes_rpc_command( } /* Parse service descriptor ("qubes.Service+arg") */ - start = cmd->command + RPC_REQUEST_COMMAND_LEN + 1; + cmd->command += RPC_REQUEST_COMMAND_LEN + 1; + + start = cmd->command; end = strchr(start, ' '); if (!end) { LOG(ERROR, "No space found after service descriptor"); @@ -450,7 +539,7 @@ struct qrexec_parsed_command *parse_qubes_rpc_command( goto err; } - size_t const descriptor_len = (size_t)(end - start); + size_t const descriptor_len = cmd->service_descriptor_len = (size_t)(end - start); if (descriptor_len > MAX_SERVICE_NAME_LEN) { LOG(ERROR, "Command too long (length %zu)", descriptor_len); goto err; @@ -538,7 +627,9 @@ int execute_parsed_qubes_rpc_command( int *stdout_fd, int *stderr_fd, struct buffer *stdin_buffer) { if (cmd->service_descriptor) { // Proper Qubes RPC call - int find_res = find_qrexec_service(cmd, stdin_fd, stdin_buffer); + char file_path[QUBES_SOCKADDR_UN_MAX_PATH_LEN]; + struct buffer buf = { .data = file_path, .buflen = (int)sizeof(file_path) }; + int find_res = find_qrexec_service(cmd, stdin_fd, stdin_buffer, &buf); if (find_res != 0) { assert(find_res < 0); return find_res; @@ -550,12 +641,12 @@ int execute_parsed_qubes_rpc_command( *pid = 0; return 0; } - return do_fork_exec(cmd->username, cmd->command, - pid, stdin_fd, stdout_fd, stderr_fd); + return do_fork_exec(buf.data, cmd->username, cmd->command, + pid, stdin_fd, stdout_fd, stderr_fd); } else { // Legacy qrexec behavior: spawn shell directly - return do_fork_exec(cmd->username, cmd->command, - pid, stdin_fd, stdout_fd, stderr_fd); + return do_fork_exec(NULL, cmd->username, cmd->command, + pid, stdin_fd, stdout_fd, stderr_fd); } } static bool validate_port(const char *port) { @@ -632,11 +723,11 @@ static int qubes_tcp_connect(const char *host, const char *port) int find_qrexec_service( struct qrexec_parsed_command *cmd, - int *socket_fd, struct buffer *stdin_buffer) { + int *socket_fd, struct buffer *stdin_buffer, + struct buffer *path_buffer) { assert(cmd->service_descriptor); + assert(path_buffer->buflen > NAME_MAX); - char file_path[QUBES_SOCKADDR_UN_MAX_PATH_LEN]; - struct buffer path_buffer = { .data = file_path, .buflen = (int)sizeof(file_path) }; const char *qrexec_service_path = getenv("QREXEC_SERVICE_PATH"); if (!qrexec_service_path) qrexec_service_path = QREXEC_SERVICE_PATH; @@ -645,11 +736,11 @@ int find_qrexec_service( struct stat statbuf; int ret = find_file(qrexec_service_path, cmd->service_descriptor, - path_buffer.data, (size_t)path_buffer.buflen, + path_buffer->data, (size_t)path_buffer->buflen, &statbuf); if (ret == -1) ret = find_file(qrexec_service_path, cmd->service_name, - path_buffer.data, (size_t)path_buffer.buflen, + path_buffer->data, (size_t)path_buffer->buflen, &statbuf); if (ret < 0) { if (ret == -1) @@ -662,11 +753,11 @@ int find_qrexec_service( if (S_ISSOCK(statbuf.st_mode)) { /* Socket-based service. */ int s; - if ((s = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) { + if ((s = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0)) == -1) { PERROR("socket"); return -2; } - if (qubes_connect(s, path_buffer.data, strlen(path_buffer.data))) { + if (qubes_connect(s, path_buffer->data, strlen(path_buffer->data))) { PERROR("qubes_connect"); close(s); return -2; @@ -674,7 +765,7 @@ int find_qrexec_service( if (cmd->send_service_descriptor) { /* send part after "QUBESRPC ", including trailing NUL */ - const char *desc = cmd->command + RPC_REQUEST_COMMAND_LEN + 1; + const char *desc = cmd->command; buffer_append(stdin_buffer, desc, strlen(desc) + 1); } @@ -682,9 +773,9 @@ int find_qrexec_service( return 0; } else if (S_ISLNK(statbuf.st_mode)) { /* TCP-based service */ - assert(path_buffer.buflen >= (int)sizeof("/dev/tcp") - 1); - assert(memcmp(path_buffer.data, "/dev/tcp", sizeof("/dev/tcp") - 1) == 0); - char *address = path_buffer.data + (sizeof("/dev/tcp") - 1); + assert(path_buffer->buflen >= (int)sizeof("/dev/tcp") - 1); + assert(memcmp(path_buffer->data, "/dev/tcp", sizeof("/dev/tcp") - 1) == 0); + char *address = path_buffer->data + (sizeof("/dev/tcp") - 1); char *host = NULL, *port = NULL; if (*address == '/') { host = address + 1; @@ -699,7 +790,7 @@ int find_qrexec_service( if (port == NULL) { if (cmd->arg == NULL || *cmd->arg == '\0') { LOG(ERROR, "No or empty argument provided, cannot connect to %s", - path_buffer.data); + path_buffer->data); return -2; } if (host == NULL) { @@ -726,14 +817,15 @@ int find_qrexec_service( } } else { if (cmd->arg != NULL && *cmd->arg != '\0') { - LOG(ERROR, "Unexpected argument %s to service %s", cmd->arg, path_buffer.data); + LOG(ERROR, "Unexpected argument %s to service %s", cmd->arg, + path_buffer->data); return -2; } } if (cmd->send_service_descriptor) { /* send part after "QUBESRPC ", including trailing NUL */ - const char *desc = cmd->command + RPC_REQUEST_COMMAND_LEN + 1; + const char *desc = cmd->command; buffer_append(stdin_buffer, desc, strlen(desc) + 1); } @@ -744,30 +836,30 @@ int find_qrexec_service( return 0; } - if (euidaccess(path_buffer.data, X_OK) == 0) { + if (euidaccess(path_buffer->data, X_OK) == 0) { /* Executable-based service. */ if (!cmd->send_service_descriptor) { LOG(WARNING, "Warning: ignoring skip-service-descriptor=true " "for execute executable service %s", - path_buffer.data); + path_buffer->data); } if (cmd->exit_on_stdout_eof) { LOG(WARNING, "Warning: ignoring exit-on-service-eof=true " "for executable service %s", - path_buffer.data); + path_buffer->data); cmd->exit_on_stdout_eof = false; } if (cmd->exit_on_stdin_eof) { LOG(WARNING, "Warning: ignoring exit-on-client-eof=true " "for executable service %s", - path_buffer.data); + path_buffer->data); cmd->exit_on_stdin_eof = false; } return 0; } LOG(ERROR, "Unknown service type (not executable, not a socket): %.*s", - path_buffer.buflen, path_buffer.data); + path_buffer->buflen, path_buffer->data); return -2; } diff --git a/libqrexec/libqrexec-utils.h b/libqrexec/libqrexec-utils.h index 18d9b141..6009b589 100644 --- a/libqrexec/libqrexec-utils.h +++ b/libqrexec/libqrexec-utils.h @@ -66,7 +66,8 @@ struct qrexec_parsed_command { /* Override to disable "wait for session" */ bool nogui; - /* Command (the part after "user:") */ + /* Command (the part after "user:"). If this is an RPC command + * then the "QUBESRPC " prefix is not included. */ const char *command; /* The below parameters are NULL for legacy (non-"QUBESRPC") commands. */ @@ -104,6 +105,8 @@ struct qrexec_parsed_command { /* Pointer to the argument, or NULL if there is no argument. * Same buffer as "service_descriptor". */ char *arg; + /* length of the command */ + size_t service_descriptor_len; }; /* Parse a command, return NULL on failure. Uses cmd->cmdline @@ -137,16 +140,24 @@ int load_service_config(struct qrexec_parsed_command *cmd_name, __attribute__((visibility("default"))) int load_service_config_v2(struct qrexec_parsed_command *cmd_name); -typedef void (do_exec_t)(const char *cmdline, const char *user); +typedef void (do_exec_t)(const char *program, const char *cmd, const char *user); __attribute__((visibility("default"))) void register_exec_func(do_exec_t *func); -/* - * exec() qubes-rpc-multiplexer if *prog* starts with magic "QUBESRPC" keyword, - * do not return in that case; pass *envp* to execve() as en environment - * otherwise, return false without any action + +/** + * \param program Full path to program to execute. + * \param cmd RPC command, excluding "QUBESRPC " prefix. + * \param envp Environment passed to execve(). + * \param use_shell If true, use a login shell to spawn the program. + * + * Execute *program* as an RPC service or call _exit() on failure. + * *cmd* is used to set the argument (if any) and "QREXEC_*" environment variables. + * Environment variables in *envp* that start with "QREXEC" are ignored, except for + * "QREXEC_SERVICE_PATH" and "QREXEC_AGENT_PID", which are inherited. */ __attribute__((visibility("default"))) -void exec_qubes_rpc_if_requested(const char *prog, char *const envp[]); +_Noreturn void exec_qubes_rpc2(const char *program, const char *cmd, char *const envp[], + bool use_shell); /* Execute `qubes.WaitForSession` service, do not return on success, return -1 * (maybe setting errno) on failure. */ @@ -194,13 +205,17 @@ int execute_parsed_qubes_rpc_command( * the socket, or -1 for executable services. * @param stdin_buffer This buffer will need to be prepended to the child process’s * stdin. + * @param path_buffer This buffer (NUL-terminated) holds the service's path. On + * entry it must be at least NAME_MAX bytes. It will not be freed or reallocated. + * Its contents should be ignored if stdout_fd is not -1. * @return 0 if the implementation is found (and, for sockets, connected to) * successfully, -1 if not found, -2 if problem. */ __attribute__((visibility("default"))) int find_qrexec_service( struct qrexec_parsed_command *cmd, - int *socket_fd, struct buffer *stdin_buffer); + int *socket_fd, struct buffer *stdin_buffer, + struct buffer *path_buffer); /** Suggested buffer size for the path buffer of find_qrexec_service. */ #define QUBES_SOCKADDR_UN_MAX_PATH_LEN 1024 @@ -288,7 +303,7 @@ struct process_io_request { * *local* process. For a *remote* process, stdin_fd is the standard * *output*, stdout_fd is the standard *input*, and stderr_fd must be -1. */ // stderr_fd can be -1 - int stdin_fd, stdout_fd, stderr_fd; + int stdin_fd, stdout_fd, stderr_fd, logger_fd; // 0 if no child process pid_t local_pid; @@ -318,6 +333,9 @@ struct process_io_request { volatile sig_atomic_t *sigusr1; struct prefix_data prefix_data; }; +/** Open an FD to a logger */ +__attribute__((visibility("default"))) +int open_logger(struct qrexec_parsed_command *command, int *pid); /* * Pass IO between vchan and local FDs. See the comments for diff --git a/libqrexec/open_logger.c b/libqrexec/open_logger.c new file mode 100644 index 00000000..a1d378b4 --- /dev/null +++ b/libqrexec/open_logger.c @@ -0,0 +1,38 @@ +#include +#include +#include +#include +#include + +int open_logger(struct qrexec_parsed_command *command, int *pid) +{ + int pipes[2]; + if (pipe2(pipes, O_CLOEXEC)) { + LOG(ERROR, "Cannot create status pipe"); + return -1; + } + char *buf[] = { + "logger", + "-t", + NULL, + NULL, + }; + if (asprintf(buf + 2, "%.*s-%s", (int)command->service_descriptor_len, + command->service_descriptor, command->source_domain) < 0) { + LOG(ERROR, "asprintf() failed"); + return -1; + } + switch ((*pid = fork())) { + case -1: + LOG(ERROR, "Cannot fork logger process"); + return -1; + case 0: + fix_fds(pipes[0], 1, 2); + execvp("logger", buf); + _exit(126); + default: + free(buf[2]); + close(pipes[0]); + return pipes[1]; + } +} diff --git a/libqrexec/process_io.c b/libqrexec/process_io.c index b0524876..4b16baf6 100644 --- a/libqrexec/process_io.c +++ b/libqrexec/process_io.c @@ -96,6 +96,7 @@ int qrexec_process_io(const struct process_io_request *req, int stdin_fd = req->stdin_fd; int stdout_fd = req->stdout_fd; int stderr_fd = req->stderr_fd; + int dup_fd = req->logger_fd; struct buffer *stdin_buf = req->stdin_buf; bool const is_service = req->is_service; @@ -344,7 +345,7 @@ int qrexec_process_io(const struct process_io_request *req, if (prefix.len > 0 || (stdout_fd >= 0 && fds[FD_STDOUT].revents)) { switch (handle_input_v2( vchan, stdout_fd, stdout_msg_type, - &prefix, &remote_buffer)) { + &prefix, &remote_buffer, -1)) { case REMOTE_ERROR: if (!is_service && remote_status == -1) { /* Even if sending fails, still try to read remaining @@ -365,7 +366,7 @@ int qrexec_process_io(const struct process_io_request *req, if (stderr_fd >= 0 && fds[FD_STDERR].revents) { switch (handle_input_v2( vchan, stderr_fd, MSG_DATA_STDERR, - &empty, &remote_buffer)) { + &empty, &remote_buffer, dup_fd)) { case REMOTE_ERROR: handle_vchan_error("send(handle_input stderr)"); break; diff --git a/libqrexec/qrexec.h b/libqrexec/qrexec.h index a8fc06b3..ec0d44a2 100644 --- a/libqrexec/qrexec.h +++ b/libqrexec/qrexec.h @@ -163,7 +163,6 @@ enum { #define QREXEC_AGENT_TRIGGER_PATH "/var/run/qubes/qrexec-agent" #define QREXEC_AGENT_FDPASS_PATH "/var/run/qubes/qrexec-agent-fdpass" #define MEMINFO_WRITER_PIDFILE "/var/run/meminfo-writer.pid" -#define QUBES_RPC_MULTIPLEXER_PATH "/usr/lib/qubes/qubes-rpc-multiplexer" #define QREXEC_DAEMON_SOCKET_DIR "/var/run/qubes" #define QREXEC_POLICY_PROGRAM "/usr/bin/qrexec-policy-exec" #define QREXEC_SERVICE_PATH "/usr/local/etc/qubes-rpc:/etc/qubes-rpc" diff --git a/libqrexec/remote.c b/libqrexec/remote.c index 314bb8af..b831e7a6 100644 --- a/libqrexec/remote.c +++ b/libqrexec/remote.c @@ -150,7 +150,8 @@ int handle_remote_data_v2( int handle_input_v2( libvchan_t *vchan, int fd, int msg_type, struct prefix_data *prefix_data, - const struct buffer *buffer) + const struct buffer *buffer, + int dup_fd) { const size_t max_len = (size_t)buffer->buflen; char *buf = buffer->data; @@ -175,6 +176,8 @@ int handle_input_v2( prefix_data->len -= len; } else { len = read(fd, buf, len); + if (dup_fd != -1 && len > 0) + write_all(dup_fd, buf, (size_t)len); /* If the other side of the socket is a process that is already dead, * read from such socket could fail with ECONNRESET instead of * just 0. */ diff --git a/libqrexec/remote.h b/libqrexec/remote.h index 7d19f7a4..b6a42397 100644 --- a/libqrexec/remote.h +++ b/libqrexec/remote.h @@ -73,9 +73,12 @@ int handle_remote_data_v2( * initialized, and will _not_ be anything meaningful on return. The * buffer pointer and length will not be freed or reallocated, though. * In Rust terms: this is an &mut [MaybeUninit]. + * + * If out_fd is not -1, all data written is duplicated to it. */ int handle_input_v2( libvchan_t *vchan, int fd, int msg_type, struct prefix_data *prefix_data, - const struct buffer *buffer); + const struct buffer *buffer, + int out_fd); #pragma GCC visibility pop diff --git a/qrexec/client.py b/qrexec/client.py index f2d97d8e..01e432bc 100644 --- a/qrexec/client.py +++ b/qrexec/client.py @@ -23,7 +23,6 @@ QREXEC_CLIENT_DOM0 = "/usr/bin/qrexec-client" QREXEC_CLIENT_VM = "/usr/bin/qrexec-client-vm" -RPC_MULTIPLEXER = "/usr/lib/qubes/qubes-rpc-multiplexer" VERSION = None @@ -102,11 +101,6 @@ def make_command(dest, rpcname, arg): if arg is not None: rpcname = f"{rpcname}+{arg}" - if VERSION == "dom0" and dest == "dom0": - # Invoke qubes-rpc-multiplexer directly. This will work for non-socket - # services only. - return [RPC_MULTIPLEXER, rpcname, "dom0"] - if VERSION == "dom0": return [ QREXEC_CLIENT_DOM0, diff --git a/qrexec/tests/socket/agent.py b/qrexec/tests/socket/agent.py index b17812e5..52653a41 100644 --- a/qrexec/tests/socket/agent.py +++ b/qrexec/tests/socket/agent.py @@ -58,25 +58,49 @@ def check_dom0(self, dom0): ), ) - def assertExpectedStdout( - self, target, expected_stdout: bytes, *, exit_code=0 - ): + def assertExpectedStdoutStderr( + self, + target, + expected_stdout: bytes, + expected_stderr: bytes, + *, + exit_code: int = 0, + ) -> None: messages = util.sort_messages(target.recv_all_messages()) self.assertListEqual( - messages[-3:], + messages[-2:], [ - (qrexec.MSG_DATA_STDOUT, b""), (qrexec.MSG_DATA_STDERR, b""), (qrexec.MSG_DATA_EXIT_CODE, struct.pack("> " + shlex.quote(fifo)).encode("ascii", "strict") @@ -219,7 +238,7 @@ def test_just_exec(self): self.assertEqual(f.read(), b"a\n") self.check_dom0(dom0) - def test_just_exec_rpc(self): + def test_003_just_exec_rpc(self): fifo = os.path.join(self.tempdir, "new_file") os.mkfifo(fifo, mode=0o600) util.make_executable_service( @@ -227,7 +246,8 @@ def test_just_exec_rpc(self): "rpc", "qubes.Service", rf"""#!/bin/bash -eu -printf %s\\n "$QREXEC_SERVICE_FULL_NAME" >> {shlex.quote(fifo)} +printf %s "$QREXEC_SERVICE_FULL_NAME" >> {shlex.quote(fifo)} +exit 1 """, ) cmd = b"QUBESRPC qubes.Service+ domX" @@ -238,9 +258,8 @@ def test_just_exec_rpc(self): (qrexec.MSG_DATA_EXIT_CODE, b"\0\0\0\0"), ], ) - with open(fifo, "rb") as f: - self.assertEqual(f.read(), b"qubes.Service+\n") + self.assertEqual(f.read(), b"qubes.Service+") self.check_dom0(dom0) def test_just_exec_rpc_not_found(self): @@ -353,10 +372,8 @@ def trigger_service(self, dom0, client, target_domain_name, service_name): @unittest.skipIf(os.environ.get("SKIP_SOCKET_TESTS"), "socket tests not set up") class TestAgentExecQubesRpc(TestAgentBase): - def execute_qubesrpc( - self, service: str, src_domain_name: str, fail_exec: bool = False - ): - self.start_agent(fail_exec) + def execute_qubesrpc(self, service: str, src_domain_name: str): + self.start_agent() dom0 = self.connect_dom0() @@ -400,11 +417,14 @@ def test_exec_service(self): """\ #!/bin/sh echo "arg: $1, remote domain: $QREXEC_REMOTE_DOMAIN" +printf 'something on stderr' >&2 """, ) target, dom0 = self.execute_qubesrpc("qubes.Service+arg", "domX") target.send_message(qrexec.MSG_DATA_STDIN, b"") - self.assertExpectedStdout(target, b"arg: arg, remote domain: domX\n") + self.assertExpectedStdoutStderr( + target, b"arg: arg, remote domain: domX\n", b"something on stderr" + ) self.check_dom0(dom0) def test_exec_service_keyword(self): @@ -547,14 +567,12 @@ def test_exec_service_not_found(self): """ self._test_exec_service_fail(qrexec.QREXEC_EXIT_SERVICE_NOT_FOUND) - def test_exec_service_bad_multiplexer(self): + def test_exec_service_bad_service(self): """ - RPC multiplexer is junk + Service to execute is junk (-ENOEXEC from execve()) """ util.make_executable_service(self.tempdir, "rpc", "qubes.Service", "") - target, dom0 = self.execute_qubesrpc( - "qubes.Service+arg", "domX", fail_exec=True - ) + target, dom0 = self.execute_qubesrpc("qubes.Service+arg", "domX") target.send_message(qrexec.MSG_DATA_STDIN, b"") messages = util.sort_messages(target.recv_all_messages()) self.assertEqual(messages[0], (qrexec.MSG_DATA_STDOUT, b"")) @@ -695,7 +713,7 @@ def test_exec_null_argument_finds_service_for_empty_argument(self): "rpc", "qubes.Service", """\ -#!/bin/sh +#!/bin/sh -- echo "general service" """, ) @@ -1339,6 +1357,7 @@ def assertStdoutMessages( msg_type, msg_body = target.recv_message() self.assertEqual(msg_type, ty) l = len(msg_body) + self.assertGreater(l, 0) self.assertEqual(msg_body, expected_stdout[bytes_recvd:l]) bytes_recvd += l self.assertEqual(bytes_recvd, expected) diff --git a/qrexec/tests/socket/daemon.py b/qrexec/tests/socket/daemon.py index d561e740..4da11d5b 100644 --- a/qrexec/tests/socket/daemon.py +++ b/qrexec/tests/socket/daemon.py @@ -652,9 +652,6 @@ def start_client(self, args): os.path.join(self.tempdir, "rpc"), ] ) - env["QREXEC_MULTIPLEXER_PATH"] = os.path.join( - ROOT_PATH, "lib", "qubes-rpc-multiplexer" - ) env["QUBES_RPC_CONFIG_PATH"] = os.path.join(self.tempdir, "rpc-config") cmd = [ os.path.join(ROOT_PATH, "daemon", "qrexec-client"), @@ -1000,36 +997,73 @@ def test_exec_service_with_invalid_config_5(self): def test_exec_service_with_invalid_config_6(self): self.exec_service_with_invalid_config(None) - def _test_run_dom0_service_exec(self, nogui): + def assertExpectedStdout( + self, target, expected_stdout: bytes, exit_code: int = 0 + ): + bytes_recvd, expected = 0, len(expected_stdout) + while bytes_recvd < expected: + msg_type, msg_body = target.recv_message() + self.assertEqual(msg_type, qrexec.MSG_DATA_STDOUT) + l = len(msg_body) + self.assertGreater(l, 0) + new_bytes_received = bytes_recvd + l + self.assertEqual( + msg_body, expected_stdout[bytes_recvd:new_bytes_received] + ) + bytes_recvd = new_bytes_received + self.assertEqual(bytes_recvd, expected) + self.assertListEqual( + util.sort_messages(target.recv_all_messages()), + [ + (qrexec.MSG_DATA_STDOUT, b""), + (qrexec.MSG_DATA_EXIT_CODE, struct.pack("