From 07ca022f7a8fdb4408d2890f0dc03e3331740c1a Mon Sep 17 00:00:00 2001 From: Paul Gaiduk Date: Mon, 26 Aug 2024 14:49:03 +0200 Subject: [PATCH] add tty mux functionality to chroot2 This commit adds the ability to control the container's entrypoint process from multiple tty consoles. Mainly it's done to support the simultanious access to the container's console from the serial terminal and the VNC console. The `chroot2` script now accepts two additional argument which are the names of the tty consoles (serial and VNC) to be multiplexed. The script accepts inputs from both consoles and forwards them to the container's entrypoint stdin. The output from the container's entrypoint stdout and stderr is also multiplexed to both consoles. Additionally both consoles forward the common signals such as Ctrl+C, Ctrl+Z, Ctrl+\, Ctrl+D to the container's entrypoint process. Signed-off-by: Paul Gaiduk --- pkg/xen-tools/initrd/chroot2.c | 264 ++++++++++++++++++++++++++++++- pkg/xen-tools/initrd/init-initrd | 11 +- 2 files changed, 264 insertions(+), 11 deletions(-) diff --git a/pkg/xen-tools/initrd/chroot2.c b/pkg/xen-tools/initrd/chroot2.c index 679c3b4ae6c..c25bde098b0 100644 --- a/pkg/xen-tools/initrd/chroot2.c +++ b/pkg/xen-tools/initrd/chroot2.c @@ -1,14 +1,17 @@ #define _GNU_SOURCE #include +#include #include #include #include #include #include #include +#include #include #include #include +#include #include #include #include @@ -24,11 +27,134 @@ struct clone_args { #define STACK_SIZE (8 * 1024 * 1024) static char child_stack[STACK_SIZE]; /* Space for child's stack */ +// Create the input and output pipes to multiplex the main process +int input_pipe_fds[2]; +int output_pipe_fds[2]; + +pid_t child_pid; +struct termios orig_termios_first_tty; +struct termios orig_termios_second_tty; +int fd1, fd2; + +void reset_terminal_mode_first_tty() { + tcsetattr(fd1, TCSANOW, &orig_termios_first_tty); +} + +void reset_terminal_mode_second_tty() { + tcsetattr(fd2, TCSANOW, &orig_termios_second_tty); +} + +void set_raw_mode(int fd, struct termios *orig_termios) { + struct termios raw; + + // Get current terminal settings + tcgetattr(fd, orig_termios); + + // Modify the terminal settings to raw mode + raw = *orig_termios; + raw.c_lflag &= ~(ICANON | IEXTEN | ISIG); + raw.c_iflag &= ~(BRKINT | INPCK | ISTRIP | IXON); + raw.c_cflag |= (CS8); + raw.c_oflag &= ~(OPOST); + raw.c_cc[VMIN] = 1; + raw.c_cc[VTIME] = 0; + + // Set the terminal to raw mode + tcsetattr(fd, TCSANOW, &raw); +} + +void handle_signal(int sig) { + if (child_pid != 0) { + // Print the signal number + printf("Received signal: %d\n", sig); + + // Forward the signal to the child process group + kill(-child_pid, sig); + } +} + +void write_to_all(int *fds, int num_fds, char *buffer, ssize_t bytes_read) { + for (int i = 0; i < num_fds; i++) { + write(fds[i], buffer, bytes_read); + } +} + +void forward_data_to_fds(int input_fd, int *output_fds, int num_output_fds) { + char c; + ssize_t bytes_read = read(input_fd, &c, 1); + if (bytes_read > 0) { + switch (c) { + case '\n': + // Translate newline to carriage return and newline + write_to_all(output_fds, 2, "\r\n", 2); + break; + default: + write_to_all(output_fds, 2, &c, 1); + break; + } + } +} +void handle_input(int from_fd, int to_fd) { + char c; + ssize_t bytes_read = read(from_fd, &c, 1); + if (bytes_read > 0) { + switch (c) { + case 3: // "Ctrl-C" + handle_signal(SIGINT); + break; + case 4: // "Ctrl-D" + handle_signal(SIGQUIT); + break; + case 26: // "Ctrl-Z" + handle_signal(SIGTSTP); + break; + case 28: // "Ctrl-\" + handle_signal(SIGQUIT); + break; + default: + write(to_fd, &c, 1); + break; + } + } +} + static int child_func(void *args) { struct clone_args *parsed_args = args; struct passwd *pws; + /* We are going to redirect the child stdin, stdout and stderr + to the corresponding pipes, close other unneded FDs and + additionally create a new process group for the child and + make it the leader to ensure correct signal handling in + case it's a shell */ + + // Create a new process group and make the child process the leader + if (setpgid(0, 0) == -1) { + perror("setpgid"); + exit(EXIT_FAILURE); + } + + // Close the read end of the output pipe + close(output_pipe_fds[0]); + + // Redirect stdout and stderr to the read end of output pipe + dup2(output_pipe_fds[1], STDOUT_FILENO); + dup2(output_pipe_fds[1], STDERR_FILENO); + + // Close the original write end of the output pipe + close(output_pipe_fds[1]); + + // Close the write end of the input pipe + close(input_pipe_fds[1]); + + // Redirect the read end of the input pipe to stdin + dup2(input_pipe_fds[0], STDIN_FILENO); + + // Close the original read end of the input pipe + close(input_pipe_fds[0]); + + /* Continuing with processing the arguments and exec the child process */ if (chroot(parsed_args->chroot) != 0) err(-1, "chroot(%s) failed:", parsed_args->chroot); @@ -64,7 +190,6 @@ int main(int argc, char **argv) uid_t uid, gid; int wstatus; char *endptr; - pid_t child_pid; struct clone_args args; int fd; @@ -77,13 +202,25 @@ int main(int argc, char **argv) uid = strtol(argv[3], &endptr, 10); gid = strtol(argv[4], &endptr, 10); + // Create the input pipe + if (pipe(input_pipe_fds) == -1) { + perror("input_pipe"); + exit(EXIT_FAILURE); + } + + // Create the output pipe + if (pipe(output_pipe_fds) == -1) { + perror("output_pipe"); + exit(EXIT_FAILURE); + } + args = (struct clone_args) { .chroot = argv[1], .workdir = argv[2], .uid = uid, .gid = gid, - .command = argv[6], - .args = argv + 6, + .command = argv[8], + .args = argv + 8, }; child_pid = clone(child_func, child_stack + STACK_SIZE, CLONE_NEWPID | SIGCHLD, &args); @@ -107,6 +244,127 @@ int main(int argc, char **argv) close(fd); } + + // Open the TTYs without O_NOCTTY + fd1 = open(argv[6], O_RDWR); + if (fd1 == -1) { + perror("open first tty"); + exit(EXIT_FAILURE); + } + + fd2 = open(argv[7], O_RDWR); + if (fd2 == -1) { + perror("open second tty"); + close(fd1); + exit(EXIT_FAILURE); + } + + // Set both tty's to raw mode + set_raw_mode(fd1, &orig_termios_first_tty); + set_raw_mode(fd2, &orig_termios_second_tty); + + // Ensure raw mode is reset on exit + atexit(reset_terminal_mode_first_tty); + atexit(reset_terminal_mode_second_tty); + + // Handle signals + signal(SIGINT, handle_signal); + signal(SIGTERM, handle_signal); + signal(SIGHUP, handle_signal); + signal(SIGQUIT, handle_signal); + + int write_pid = fork(); + if (write_pid == -1) { + perror("fork of write"); + exit(EXIT_FAILURE); + } + + if (write_pid == 0) { // Write process + // Close the write end of the output pipe in the write process + close(output_pipe_fds[1]); + // Close all ends of the input pipe in the write process + close(input_pipe_fds[0]); + close(input_pipe_fds[1]); + + // Put the TTYs in an array + int output_fds[2] = {fd1, fd2}; + + while (1) { + /* In the main loop this process reads from the pipe + and writes to all FDs from output_fds */ + forward_data_to_fds(output_pipe_fds[0], output_fds, 2); + } + + close(fd1); + close(fd2); + close(output_pipe_fds[0]); + } else { // Continuing with parent process (read process) + // Close all ends of the output pipe in the read process + close(output_pipe_fds[0]); + close(output_pipe_fds[1]); + // Close the read end of the input pipe in the read process + close(input_pipe_fds[0]); + + int max_fd = (fd1 > fd2) ? fd1 : fd2; + + while (1) { + /* In the main loop this process waits for input on any FD + and forwards it to the main exec process */ + fd_set read_fds; + FD_ZERO(&read_fds); + FD_SET(fd1, &read_fds); + FD_SET(fd2, &read_fds); + + int select_result = select(max_fd + 1, &read_fds, NULL, NULL, NULL); + if (select_result == -1) { + if (errno == EINTR) { + // Interrupted by a signal, check if the exec process has terminated + int status; + pid_t result = waitpid(child_pid, &status, WNOHANG); + if (result == 0) { + // Exec process is still running + continue; + } else if (result == -1) { + perror("waitpid"); + close(fd1); + close(fd2); + close(input_pipe_fds[1]); + exit(EXIT_FAILURE); + } else { + // Child has terminated, exit the parent process + if (WIFEXITED(status) || WIFSIGNALED(status)) { + close(fd1); + close(fd2); + close(input_pipe_fds[1]); + exit(EXIT_SUCCESS); + } + } + } else { + perror("select"); + close(fd1); + close(fd2); + close(input_pipe_fds[1]); + kill(child_pid, SIGKILL); + exit(EXIT_FAILURE); + } + } + + // Handle input from TTY10 + if (FD_ISSET(fd1, &read_fds)) { + handle_input(fd1, input_pipe_fds[1]); + } + + // Handle input from TTY20 + if (FD_ISSET(fd2, &read_fds)) { + handle_input(fd2, input_pipe_fds[1]); + } + } + + close(fd1); + close(fd2); + close(input_pipe_fds[1]); + } + child_pid = wait(&wstatus); if (child_pid < 0) err(-1, "wait() failed:"); diff --git a/pkg/xen-tools/initrd/init-initrd b/pkg/xen-tools/initrd/init-initrd index 35056d5019c..0fa914be624 100755 --- a/pkg/xen-tools/initrd/init-initrd +++ b/pkg/xen-tools/initrd/init-initrd @@ -207,14 +207,9 @@ echo "Executing with uid gid: $ug" # process. File will be used for the `eve-enter-container` script. pid_file="/mnt/entrypoint-pid" -if grep -q "console=tty0" /proc/cmdline; then - #shellcheck disable=SC2086 - #we have tty0 console primary, so will add output to hvc0 for logging - eval /chroot2 /mnt/rootfs "${WORKDIR:-/}" $ug $pid_file $cmd 2>&1 | tee -i /dev/hvc0 -else - #shellcheck disable=SC2086 - eval /chroot2 /mnt/rootfs "${WORKDIR:-/}" $ug $pid_file $cmd <> /dev/console 2>&1 -fi +#shellcheck disable=SC2086 +# Chroot into the working directory and execute the container entrypoint command in a new PID namespace +eval /chroot2 /mnt/rootfs "${WORKDIR:-/}" $ug $pid_file /dev/tty0 /dev/hvc0 $cmd chroot_ret=$? # Container exited, final lines