Skip to content

Commit

Permalink
add tty mux functionality to chroot2
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
europaul committed Aug 26, 2024
1 parent d04fab9 commit 07ca022
Show file tree
Hide file tree
Showing 2 changed files with 264 additions and 11 deletions.
264 changes: 261 additions & 3 deletions pkg/xen-tools/initrd/chroot2.c
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
#define _GNU_SOURCE
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <sched.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mount.h>
#include <sys/select.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <termios.h>
#include <unistd.h>
#include <pwd.h>
#include <grp.h>
Expand All @@ -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

Check failure on line 127 in pkg/xen-tools/initrd/chroot2.c

View workflow job for this annotation

GitHub Actions / yetus

codespell: unneded ==> unneeded
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);
Expand Down Expand Up @@ -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;

Expand All @@ -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);
Expand All @@ -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:");
Expand Down
11 changes: 3 additions & 8 deletions pkg/xen-tools/initrd/init-initrd
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 07ca022

Please sign in to comment.