foot/slave.c
Daniel Eklöf 027a8de6f5
slave: use an event FD to communicate errors after fork before exec
Replace the pipe we used to communicate errors after fork(), but
before exec() to the parent process with an event FD.

The overhead in the kernel is much lower to setup an event fd,
compared to a pipe.
2020-08-01 09:33:43 +02:00

347 lines
8.6 KiB
C

#include "slave.h"
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <unistd.h>
#include <errno.h>
#include <assert.h>
#include <signal.h>
#include <termios.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <sys/eventfd.h>
#include <fcntl.h>
#define LOG_MODULE "slave"
#define LOG_ENABLE_DBG 0
#include "log.h"
#include "terminal.h"
#include "tokenize.h"
static bool
is_valid_shell(const char *shell)
{
FILE *f = fopen("/etc/shells", "r");
if (f == NULL)
goto err;
char *_line = NULL;
size_t count = 0;
while (true) {
errno = 0;
ssize_t ret = getline(&_line, &count, f);
if (ret < 0) {
free(_line);
break;
}
char *line = _line;
{
while (isspace(*line))
line++;
if (line[0] != '\0') {
char *end = line + strlen(line) - 1;
while (isspace(*end))
end--;
*(end + 1) = '\0';
}
}
if (line[0] == '#')
continue;
if (strcmp(line, shell) == 0) {
fclose(f);
return true;
}
}
err:
if (f != NULL)
fclose(f);
return false;
}
enum user_notification_ret_t {UN_OK, UN_NO_MORE, UN_FAIL};
static enum user_notification_ret_t
emit_one_notification(int fd, const struct user_notification *notif)
{
const char *prefix = NULL;
const char *postfix = "\e[m\n";
switch (notif->kind) {
case USER_NOTIFICATION_DEPRECATED:
prefix = "\e[33;1mdeprecated\e[39;21m: ";
break;
case USER_NOTIFICATION_WARNING:
prefix = "\e[33;1mwarning\e[39;21m: ";
break;
case USER_NOTIFICATION_ERROR:
prefix = "\e[31;1merror\e[39;21m: ";
break;
}
assert(prefix != NULL);
if (write(fd, prefix, strlen(prefix)) < 0 ||
write(fd, notif->text, strlen(notif->text)) < 0 ||
write(fd, postfix, strlen(postfix)) < 0)
{
/*
* The main process is blocking and waiting for us to close
* the error pipe. Thus, pts data will *not* be processed
* until we've exec:d. This means we cannot write anymore once
* the kernel buffer is full. Don't treat this as a fatal
* error.
*/
if (errno == EWOULDBLOCK || errno == EAGAIN)
return UN_NO_MORE;
else {
LOG_ERRNO("failed to write user-notification");
return UN_FAIL;
}
}
return UN_OK;
}
static bool
emit_notifications_of_kind(int fd, const user_notifications_t *notifications,
enum user_notification_kind kind)
{
tll_foreach(*notifications, it) {
if (it->item.kind == kind) {
switch (emit_one_notification(fd, &it->item)) {
case UN_OK:
break;
case UN_NO_MORE:
return true;
case UN_FAIL:
return false;
}
}
}
return true;
}
static bool
emit_notifications(int fd, const user_notifications_t *notifications)
{
return
emit_notifications_of_kind(fd, notifications, USER_NOTIFICATION_ERROR) &&
emit_notifications_of_kind(fd, notifications, USER_NOTIFICATION_WARNING) &&
emit_notifications_of_kind(fd, notifications, USER_NOTIFICATION_DEPRECATED);
}
static void
slave_exec(int ptmx, char *argv[], int err_fd, bool login_shell,
const user_notifications_t *notifications)
{
int pts = -1;
const char *pts_name = ptsname(ptmx);
if (grantpt(ptmx) == -1) {
LOG_ERRNO("failed to grantpt()");
goto err;
}
if (unlockpt(ptmx) == -1) {
LOG_ERRNO("failed to unlockpt()");
goto err;
}
close(ptmx);
ptmx = -1;
if (setsid() == -1) {
LOG_ERRNO("failed to setsid()");
goto err;
}
pts = open(pts_name, O_RDWR);
if (pts == -1) {
LOG_ERRNO("failed to open pseudo terminal slave device");
goto err;
}
if (ioctl(pts, TIOCSCTTY, 0) < 0) {
LOG_ERRNO("failed to configure controlling terminal");
goto err;
}
{
struct termios flags;
if (tcgetattr(pts, &flags) < 0) {
LOG_ERRNO("failed to get terminal attributes");
goto err;
}
flags.c_iflag |= IUTF8;
if (tcsetattr(pts, TCSANOW, &flags) < 0) {
LOG_ERRNO("failed to set IUTF8 terminal attribute");
goto err;
}
}
if (tll_length(*notifications) > 0) {
int flags = fcntl(pts, F_GETFL);
if (flags < 0)
goto err;
if (fcntl(pts, F_SETFL, flags | O_NONBLOCK) < 0)
goto err;
if (!emit_notifications(pts, notifications))
goto err;
fcntl(pts, F_SETFL, flags);
}
if (dup2(pts, STDIN_FILENO) == -1 ||
dup2(pts, STDOUT_FILENO) == -1 ||
dup2(pts, STDERR_FILENO) == -1)
{
LOG_ERRNO("failed to dup stdin/stdout/stderr");
goto err;
}
close(pts);
pts = -1;
const char *file;
if (login_shell) {
file = strdup(argv[0]);
char *arg0 = malloc(strlen(argv[0]) + 1 + 1);
arg0[0] = '-';
arg0[1] = '\0';
strcat(arg0, argv[0]);
argv[0] = arg0;
} else
file = argv[0];
execvp(file, argv);
err:
(void)!write(err_fd, &(uint64_t){errno}, sizeof(uint64_t));
if (pts != -1)
close(pts);
if (ptmx != -1)
close(ptmx);
close(err_fd);
_exit(errno);
}
pid_t
slave_spawn(int ptmx, int argc, const char *cwd, char *const *argv,
const char *term_env, const char *conf_shell, bool login_shell,
const user_notifications_t *notifications)
{
int error_fd = eventfd(0, EFD_CLOEXEC);
if (error_fd < 0) {
LOG_ERRNO("failed to create event FD");
return -1;
}
pid_t pid = fork();
switch (pid) {
case -1:
LOG_ERRNO("failed to fork");
close(error_fd);
return -1;
case 0:
/* Child */
if (chdir(cwd) < 0) {
const int _errno = errno;
LOG_ERRNO("failed to change working directory");
(void)!write(error_fd, &(uint64_t){_errno}, sizeof(uint64_t));
_exit(_errno);
}
/* Restore signals */
sigset_t mask;
sigemptyset(&mask);
const struct sigaction sa = {.sa_handler = SIG_DFL};
if (sigaction(SIGINT, &sa, NULL) < 0 ||
sigaction(SIGTERM, &sa, NULL) < 0 ||
sigaction(SIGHUP, &sa, NULL) < 0 ||
sigprocmask(SIG_SETMASK, &mask, NULL) < 0)
{
const int _errno = errno;
LOG_ERRNO_P("failed to restore signals", errno);
(void)!write(error_fd, &(uint64_t){_errno}, sizeof(uint64_t));
_exit(_errno);
}
setenv("TERM", term_env, 1);
char **_shell_argv = NULL;
char **shell_argv = NULL;
if (argc == 0) {
char *shell_copy = strdup(conf_shell);
if (!tokenize_cmdline(shell_copy, &_shell_argv)) {
const int _errno = errno;
free(shell_copy);
(void)!write(error_fd, &(uint64_t){_errno}, sizeof(uint64_t));
_exit(_errno);
}
shell_argv = _shell_argv;
} else {
size_t count = 0;
for (; argv[count] != NULL; count++)
;
shell_argv = malloc((count + 1) * sizeof(shell_argv[0]));
for (size_t i = 0; i < count; i++)
shell_argv[i] = argv[i];
shell_argv[count] = NULL;
}
if (is_valid_shell(shell_argv[0]))
setenv("SHELL", shell_argv[0], 1);
slave_exec(ptmx, shell_argv, error_fd, login_shell, notifications);
assert(false);
break;
default: {
LOG_DBG("slave has PID %d", pid);
uint64_t _errno;
ssize_t ret = read(error_fd, &_errno, sizeof(_errno));
close(error_fd);
if (ret < 0) {
LOG_ERRNO("failed to read from error FD");
return -1;
} else if (ret == sizeof(_errno)) {
LOG_ERRNO_P(
"%s: failed to execute", (int)_errno, argc == 0 ? conf_shell : argv[0]);
return -1;
} else
LOG_DBG("%s: successfully started", conf_shell);
int fd_flags;
if ((fd_flags = fcntl(ptmx, F_GETFD)) < 0 ||
fcntl(ptmx, F_SETFD, fd_flags | FD_CLOEXEC) < 0)
{
LOG_ERRNO("failed to set FD_CLOEXEC on ptmx");
return -1;
}
break;
}
}
return pid;
}