/* This file is part of Darling. Copyright (C) 2017 Lubos Dolezel Darling 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 3 of the License, or (at your option) any later version. Darling 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 Darling. If not, see . */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "shellspawn.h" #include "duct_signals.h" #define DBG 0 int g_serverSocket = -1; struct sigaction sigchld_oldaction; void setupSocket(void); void listenForConnections(void); void spawnShell(int fd); void setupSigchild(void); void restoreSigchild(void); void reapAll(void); int main(int argc, const char** argv) { // shellspawn (daemon) --fork()--> shellspawn (child) --fork()--> exec /bin/bash // in order to read the exit status of the shell process, // we have to allow it to become a zombie, therefore we need to // restore the sigaction of SIGCHLD of the child shellspawn setupSigchild(); setupSocket(); listenForConnections(); if (g_serverSocket != -1) close(g_serverSocket); return 0; } void setupSocket(void) { struct sockaddr_un addr = { .sun_family = AF_UNIX, .sun_path = SHELLSPAWN_SOCKPATH }; g_serverSocket = socket(AF_UNIX, SOCK_STREAM, 0); if (g_serverSocket == -1) { perror("Creating unix socket"); exit(EXIT_FAILURE); } fcntl(g_serverSocket, F_SETFD, FD_CLOEXEC); unlink(SHELLSPAWN_SOCKPATH); if (bind(g_serverSocket, (struct sockaddr*) &addr, sizeof(addr)) == -1) { perror("Binding the unix socket"); exit(EXIT_FAILURE); } chmod(addr.sun_path, 0600); if (listen(g_serverSocket, 16384) == -1) { perror("Listening on unix socket"); exit(EXIT_FAILURE); } } void listenForConnections(void) { int sock; struct sockaddr_un addr; socklen_t len = sizeof(addr); while (true) { sock = accept(g_serverSocket, (struct sockaddr*) &addr, &len); if (sock == -1) break; if (fork() == 0) { restoreSigchild(); fcntl(sock, F_SETFD, FD_CLOEXEC); spawnShell(sock); exit(EXIT_SUCCESS); } else { close(sock); } } } void spawnShell(int fd) { pid_t shell_pid = -1; int shellfd[3] = { -1, -1, -1 }; int pipefd[2]; int rv; struct pollfd pfd[2]; char** argv = NULL; int argc = 2; struct msghdr msg; struct iovec iov; char cmsgbuf[CMSG_SPACE(sizeof(int)) * 3]; int kq; bool read_cmds = true; argv = (char**) malloc(sizeof(char*) * 3); argv[0] = "/bin/bash"; argv[1] = "--login"; char* alloc_exec = NULL; // Read commands from client while (read_cmds) { struct shellspawn_cmd cmd; char* param = NULL; memset(&msg, 0, sizeof(msg)); msg.msg_control = cmsgbuf; msg.msg_controllen = sizeof(cmsgbuf); iov.iov_base = &cmd; iov.iov_len = sizeof(cmd); msg.msg_iov = &iov; msg.msg_iovlen = 1; if (recvmsg(fd, &msg, 0) != sizeof(cmd)) { if (DBG) puts("bad recvmsg"); goto err; } if (cmd.data_length != 0) { param = (char*) malloc(cmd.data_length + 1); if (read(fd, param, cmd.data_length) != cmd.data_length) goto err; param[cmd.data_length] = '\0'; } switch (cmd.cmd) { case SHELLSPAWN_ADDARG: { if (param != NULL) { argv = (char**) realloc(argv, sizeof(char*) * (argc + 1)); argv[argc] = param; if (DBG) printf("add arg: %s\n", param); argc++; } break; } case SHELLSPAWN_SETENV: { if (param != NULL) { if (DBG) printf("set env: %s\n", param); putenv(param); } break; } case SHELLSPAWN_CHDIR: { if (param != NULL) { if (DBG) printf("chdir: %s\n", param); chdir(param); free(param); } break; } case SHELLSPAWN_GO: { struct cmsghdr *cmptr = CMSG_FIRSTHDR(&msg); if (cmptr == NULL) { if (DBG) puts("bad cmptr"); goto err; } if (cmptr->cmsg_level != SOL_SOCKET || cmptr->cmsg_type != SCM_RIGHTS) { if (DBG) puts("bad cmsg level/type"); goto err; } if (cmptr->cmsg_len != CMSG_LEN(sizeof(int) * 3)) { if (DBG) printf("bad cmsg_len: %d\n", cmptr->cmsg_len); goto err; } memcpy(shellfd, CMSG_DATA(cmptr), sizeof(int) * 3); if (DBG) printf("go, fds={ %d, %d, %d }\n", shellfd[0], shellfd[1], shellfd[2]); free(param); read_cmds = false; break; } case SHELLSPAWN_SETUIDGID: { int* ids = (int*) param; if (cmd.data_length < 2*sizeof(int)) { free(param); break; } setuid(ids[0]); setgid(ids[1]); free(param); break; } case SHELLSPAWN_SETEXEC: { argc = 0; argv = realloc(argv, 0); alloc_exec = param; if (DBG) printf("setexec: %s\n", param); break; } } } // Add terminating NULL argv = (char**) realloc(argv, sizeof(char*) * (argc + 1)); argv[argc] = NULL; if (pipe(pipefd) == -1) goto err; setsid(); setpgrp(); close(STDIN_FILENO); close(STDOUT_FILENO); close(STDERR_FILENO); dup2(shellfd[0], STDIN_FILENO); dup2(shellfd[1], STDOUT_FILENO); dup2(shellfd[2], STDERR_FILENO); ioctl(STDIN_FILENO, TIOCSCTTY, STDIN_FILENO); shell_pid = fork(); if (shell_pid == 0) { close(fd); fcntl(pipefd[1], F_SETFD, FD_CLOEXEC); // In future, we may support spawning something else than Bash // and check the provided shell against /etc/shells execv(alloc_exec ? alloc_exec : "/bin/bash", argv); rv = errno; write(pipefd[1], &rv, sizeof(rv)); close(pipefd[1]); exit(EXIT_FAILURE); } if (alloc_exec) { free(alloc_exec); alloc_exec = NULL; } // Check that exec succeeded close(pipefd[1]); // close the write end if (read(pipefd[0], &rv, sizeof(rv)) == sizeof(rv)) { errno = rv; goto err; } close(pipefd[0]); // Now we start passing signals // and check for child process exit kq = kqueue(); { struct kevent changes[2]; EV_SET(&changes[0], fd, EVFILT_READ, EV_ADD | EV_ENABLE, 0, 0, NULL); EV_SET(&changes[1], shell_pid, EVFILT_PROC, EV_ADD | EV_ENABLE, NOTE_EXIT, 0, NULL); if (kevent(kq, changes, 2, NULL, 0, NULL) == -1) goto err; } while (true) { struct kevent ev; if (kevent(kq, NULL, 0, &ev, 1, NULL) <= 0) { if (errno == EINTR) { if (DBG) puts("kevent call interrupted; continuing..."); continue; } if (DBG) puts("kevent fail"); goto err; } if (ev.filter == EVFILT_PROC && (ev.fflags & NOTE_EXIT)) { if (DBG) puts("subprocess exit"); break; } else if (ev.filter == EVFILT_READ) { struct shellspawn_cmd cmd; if (read(fd, &cmd, sizeof(cmd)) != sizeof(cmd)) { if (DBG) puts("Cannot read cmd"); break; } switch (cmd.cmd) { case SHELLSPAWN_SIGNAL: { int linux_signal, darwin_signal; if (cmd.data_length != sizeof(int)) goto err; if (read(fd, &linux_signal, sizeof(int)) != sizeof(int)) goto err; // Convert Linux signal number to Darwin signal number darwin_signal = signum_linux_to_bsd(linux_signal); if (DBG) printf("rcvd signal %d -> %d\n", linux_signal, darwin_signal); if (darwin_signal != 0) { int fg_pid = tcgetpgrp(shellfd[0]); if (fg_pid != -1) { if (DBG) printf("fg_pid = %d\n", fg_pid); kill(fg_pid, darwin_signal); } else kill(-shell_pid, darwin_signal); } break; } default: goto err; } } } // Kill the child process in case it's still running kill(shell_pid, SIGKILL); // Close shell fds for (int i = 0; i < 3; i++) { if (shellfd[i] != -1) close(shellfd[0]); } // Reap the child int wstatus; if (waitpid(shell_pid, &wstatus, 0) != shell_pid) perror("waitpid"); wstatus = WEXITSTATUS(wstatus); // Report exit code back to the client write(fd, &wstatus, sizeof(int)); if (DBG) printf("Shell terminated with exit code %d\n", wstatus); close(fd); reapAll(); return; err: if (DBG) fprintf(stderr, "Error spawning shell: %s\n", strerror(errno)); for (int i = 0; i < 3; i++) { if (shellfd[i] != -1) close(shellfd[0]); } if (shell_pid != -1) kill(shell_pid, SIGKILL); close(fd); reapAll(); } void setupSigchild(void) { struct sigaction sigchld_action = { .sa_handler = SIG_DFL, .sa_flags = SA_NOCLDWAIT }; sigaction(SIGCHLD, &sigchld_action, &sigchld_oldaction); } void restoreSigchild(void) { sigaction(SIGCHLD, &sigchld_oldaction, NULL); } void reapAll(void) { while (waitpid((pid_t)(-1), 0, WNOHANG) > 0); }