#include "libclpex_tty.h" #include #include #include #include #include "../../logger/Logger.h" #include "../FileHandle.h" #define BUFFER_SIZE 1024 // Initialize logger static Logger tty_logger(E_INFO, "lcpex_tty"); std::string tty_user = tty_logger.get_user_name(); std::string tty_group = tty_logger.get_group_name(); void tty_signal_handler(int sig) { std::string command = "tty_signal_handler"; // Command context // Create an instance of Logger (you can change the log level and mask accordingly) // Log the signal handling if (sig == SIGCHLD) { // Log that SIGCHLD was received, but leave the exit status to the parent tty_logger.log_to_json_file("E_INFO", "SIGCHLD received. A child process ended.", tty_user, tty_group, command); } else if (sig == SIGINT) { tty_logger.log_to_json_file("E_FATAL", "SIGINT received. Gracefully terminating...", tty_user, tty_group, command); } else if (sig == SIGTERM) { tty_logger.log_to_json_file("E_FATAL", "SIGTERM received. Terminating...", tty_user, tty_group, command); } else if (sig == SIGSEGV) { tty_logger.log_to_json_file("E_FATAL", "SIGSEGV received. Possible segmentation fault.", tty_user, tty_group, command); } else { tty_logger.log_to_json_file("E_FATAL", "Unhandled signal received", tty_user, tty_group, command); } } // Setup signal registrations void setup_tty_signal_handlers() { struct sigaction sa; sa.sa_handler = tty_signal_handler; // <-- handler function sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART | SA_NOCLDSTOP; std::string command = "setup_tty_signal_handlers"; // Command context // SIGCHLD if (sigaction(SIGCHLD, &sa, nullptr) == -1) { std::string error_message = "Failed to set SIGCHLD handler: " + std::string(strerror(errno)); tty_logger.log_to_json_file("E_FATAL", error_message, tty_user, tty_group, command); // Log to JSON file } // SIGINT if (sigaction(SIGINT, &sa, nullptr) == -1) { std::string error_message = "Failed to set SIGINT handler: " + std::string(strerror(errno)); tty_logger.log_to_json_file("E_FATAL", error_message, tty_user, tty_group, command); // Log to JSON file } // SIGTERM if (sigaction(SIGTERM, &sa, nullptr) == -1) { std::string error_message = "Failed to set SIGTERM handler: " + std::string(strerror(errno)); tty_logger.log_to_json_file("E_FATAL", error_message, tty_user, tty_group, command); // Log to JSON file } // SIGSEGV if (sigaction(SIGSEGV, &sa, nullptr) == -1) { std::string error_message = "Failed to set SIGSEGV handler: " + std::string(strerror(errno)); tty_logger.log_to_json_file("E_FATAL", error_message, tty_user, tty_group, command); // Log to JSON file } } /** * @brief Safely prints a message and exits the program * * @param msg The message to print * @param ttyOrig A pointer to a struct termios representing the original terminal settings * * This function takes a message `msg` and a pointer to a struct termios `ttyOrig` as input. * The function first prints the `msg` to `stderr`. * Then, it calls `ttyResetExit()` with `ttyOrig` as its argument to reset the terminal settings. * Finally, the function exits the program with exit code 1. */ void set_tty_cloexec_flag(int fd) { std::string command = "set_tty_cloexec_flag"; if (fcntl(fd, F_SETFD, FD_CLOEXEC) == -1) { std::string error_message = "fcntl(F_SETFD) failed for fd " + std::to_string(fd); tty_logger.log_to_json_file("E_FATAL", error_message, tty_user, tty_group, command); throw std::runtime_error("fcntl(F_SETFD) failed for fd " + std::to_string(fd)); } } void safe_perror( const char * msg, struct termios * ttyOrig ) { std::string command = "safe_perror"; // Command context tty_logger.log_to_json_file("E_FATAL", msg, tty_user, tty_group, command); // Log the message before exiting std::cerr << msg << std::endl; ttyResetExit( ttyOrig ); throw std::runtime_error(std::string("TTY Error: ") + msg); } /** * @brief Executes the child process * * @param fd_child_stderr_pipe A file descriptor array for the child process's stderr pipe * @param processed_command An array of char pointers representing the command and its arguments to be executed * @param ttyOrig A pointer to a struct termios representing the original terminal settings * @param context_override A flag indicating whether to override the process's execution context * @param context_user The username to use for the execution context if `context_override` is `true` * @param context_group The group name to use for the execution context if `context_override` is `true` * * This function takes an array of file descriptors `fd_child_stderr_pipe` for the child process's stderr pipe, * an array of char pointers `processed_command` representing the command and its arguments to be executed, * a pointer to a struct termios `ttyOrig` representing the original terminal settings, * a flag `context_override` indicating whether to override the process's execution context, * a string `context_user` representing the username to use for the execution context if `context_override` is `true`, * and a string `context_group` representing the group name to use for the execution context if `context_override` is `true`. * * The function first redirects the child process's stderr to the write end of the stderr pipe. * If `context_override` is `true`, the function sets the process's execution context using `set_identity_context()`. * If `context_override` is `false`, the function does nothing. * Finally, the function executes the command specified in `processed_command` using `execvp()`. * If the execution of `execvp()` fails, the function calls `safe_perror()` to print a message and exit the program. */ void run_child_process( int fd_child_stderr_pipe[2], char * processed_command[], struct termios * ttyOrig, bool context_override, std::string context_user, std::string context_group ) { std::string command = "run_child_process_tty"; // Command context // redirect stderr to the write end of the stderr pipe // close the file descriptor STDERR_FILENO if it was previously open, then (re)open it as a copy of while ((dup2(fd_child_stderr_pipe[WRITE_END], STDERR_FILENO) == -1) && (errno == EINTR)) {} // close the write end of the stderr pipe and read end to avoid leaks close( fd_child_stderr_pipe[WRITE_END] ); close( fd_child_stderr_pipe[READ_END] ); // if the user has specified a context override, set the context if ( context_override ) { int context_result = set_identity_context(context_user, context_group); switch (context_result) { case IDENTITY_CONTEXT_ERRORS::ERROR_NONE: break; case IDENTITY_CONTEXT_ERRORS::ERROR_NO_SUCH_USER: tty_logger.log(E_FATAL, "Aborting: context user not found: " + context_user); tty_logger.log_to_json_file("E_FATAL", "Context user not found: " + context_user, tty_user, tty_group, command); _exit(1); break; case IDENTITY_CONTEXT_ERRORS::ERROR_NO_SUCH_GROUP: tty_logger.log(E_FATAL, "Aborting: context group not found: " + context_group); tty_logger.log_to_json_file("E_FATAL", "Context group not found: " + context_group, tty_user, tty_group, command); _exit(1); break; case IDENTITY_CONTEXT_ERRORS::ERROR_SETGID_FAILED: tty_logger.log(E_FATAL, "Aborting: Setting GID failed: " + context_user + "/" + context_group); tty_logger.log_to_json_file("E_FATAL", "Setting GID failed for: " + context_user + "/" + context_group, tty_user, tty_group, command); _exit(1); break; case IDENTITY_CONTEXT_ERRORS::ERROR_SETUID_FAILED: tty_logger.log(E_FATAL, "Aborting: Setting UID failed: " + context_user + "/" + context_group); tty_logger.log_to_json_file("E_FATAL", "Setting UID failed for: " + context_user + "/" + context_group, tty_user, tty_group, command); _exit(1); break; default: tty_logger.log(E_FATAL, "Aborting: Unknown error while setting identity context."); tty_logger.log_to_json_file("E_FATAL", "Unknown error while setting identity context.", tty_user, tty_group, command); _exit(1); break; } } // execute the dang command, print to stdout, stderr (of parent), and dump to file for each!!!! // (and capture exit code in parent) int exit_code = execvp( processed_command[0], processed_command ); tty_logger.log_to_json_file("E_FATAL", "failed on execvp in child", tty_user, tty_group, command); tty_logger.log(E_FATAL, "failed on execvp in child"); _Exit(exit_code); } // this does three things: // - execute a dang string as a subprocess command // - capture child stdout/stderr to respective log files // - TEE child stdout/stderr to parent stdout/stderr int exec_pty( std::string command, FILE * stdout_log_fh, FILE * stderr_log_fh, bool context_override, std::string context_user, std::string context_group, bool environment_supplied ) { try { std::string exec_command = "exec_pty"; // Command context tty_logger.log_to_json_file("E_INFO", "TTY LAUNCHER: " + command, tty_user, tty_group, exec_command); // Setup signal handlers setup_tty_signal_handlers(); // initialize the terminal settings obj struct termios ttyOrig; // if the user chose to supply the environment, then we need to clear the environment // before we fork the process, so that the child process will inherit the environment // from the parent process if ( environment_supplied ) { //clearenv(); } // turn our command string into something execvp can consume char ** processed_command = expand_env( command ); if ( stdout_log_fh == NULL ) { tty_logger.log_to_json_file("E_FATAL", "Error opening STDOUT log file. Aborting.", tty_user, tty_group, exec_command); throw std::runtime_error("Error opening STDOUT log file"); } if ( stderr_log_fh == NULL ) { tty_logger.log_to_json_file("E_FATAL", "Error opening STDERR log file. Aborting.", tty_user, tty_group, exec_command); throw std::runtime_error("Error opening STDERR log file"); } // create the pipes for the child process to write and read from using its stderr int fd_child_stderr_pipe[2]; if ( pipe( fd_child_stderr_pipe ) == -1 ) { std::string error_message = "Failed to create stderr pipe: " + std::string(strerror(errno)); tty_logger.log_to_json_file("E_FATAL", error_message, tty_user, tty_group, exec_command); throw std::runtime_error("Failed to create stderr pipe"); } // using O_CLOEXEC to ensure that the child process closes the file descriptors set_tty_cloexec_flag( fd_child_stderr_pipe[READ_END] ); set_tty_cloexec_flag( fd_child_stderr_pipe[WRITE_END] ); // status result basket for the parent process to capture the child's exit status int status = 616; // start ptyfork integration char slaveName[MAX_SNAME]; int masterFd; struct winsize ws; /* Retrieve the attributes of terminal on which we are started */ if (tcgetattr(STDIN_FILENO, &ttyOrig) == -1){ tty_logger.log_to_json_file("E_FATAL", "tcgetattr failed", tty_user, tty_group, exec_command); close(fd_child_stderr_pipe[READ_END]); // Pipe Leaks on Error Paths close(fd_child_stderr_pipe[WRITE_END]); // Pipe Leaks on Error Paths throw std::runtime_error("tcgetattr failed"); } if (ioctl(STDIN_FILENO, TIOCGWINSZ, &ws) < 0) { tty_logger.log_to_json_file("E_FATAL", "ioctl-TIOCGWINSZ failed", tty_user, tty_group, exec_command); close(fd_child_stderr_pipe[READ_END]); // Pipe Leaks on Error Paths close(fd_child_stderr_pipe[WRITE_END]); // Pipe Leaks on Error Paths throw std::runtime_error("ioctl-TIOCGWINSZ failed"); } pid_t pid = ptyFork( &masterFd, slaveName, MAX_SNAME, &ttyOrig, &ws ); switch( pid ) { case -1: { // fork failed tty_logger.log(E_FATAL, "ptyfork failure: " + std::string(strerror(errno))); tty_logger.log_to_json_file("E_FATAL", "ptyfork failure: " + std::string(strerror(errno)), tty_user, tty_group, exec_command); close(fd_child_stderr_pipe[READ_END]); // Pipe Leaks on Error Paths close(fd_child_stderr_pipe[WRITE_END]); // Pipe Leaks on Error Paths throw std::runtime_error("ptyfork() failed: " + std::string(strerror(errno))); } case 0: { // child process run_child_process( fd_child_stderr_pipe, processed_command, &ttyOrig, context_override, context_user, context_group ); } default: { // parent process // start ptyfork integration ttySetRaw(STDIN_FILENO, &ttyOrig); // RAII guards for file descriptors FdGuard stderr_read_guard(fd_child_stderr_pipe[READ_END]); FdGuard stderr_write_guard(fd_child_stderr_pipe[WRITE_END]); FdGuard master_fd_guard(masterFd); // We don't need WRITE_END in parent, close it now stderr_write_guard.reset(); // Closes and sets fd to -1 // attempt to write to stdout,stderr from child as well as to write each to file char buf[BUFFER_SIZE] = {0}; std::strncpy(buf, command.c_str(), BUFFER_SIZE - 1); buf[BUFFER_SIZE - 1] = '\0'; // contains the byte count of the last read from the pipe ssize_t byte_count; // watched_fds for poll() to wait on struct pollfd watched_fds[3]; // populate the watched_fds array // child in to parent watched_fds[0].fd = STDIN_FILENO; watched_fds[0].events = POLLIN; // child pty to parent (stdout) watched_fds[1].fd = master_fd_guard.get(); watched_fds[1].events = POLLIN; // child stderr to parent watched_fds[2].fd = stderr_read_guard.get(); watched_fds[2].events = POLLIN; // number of files poll() reports as ready int num_files_readable; // loop flag bool break_out = false; // loop until we've read all the data from the child process while ( ! break_out ) { num_files_readable = poll(watched_fds, sizeof(watched_fds) / sizeof(watched_fds[0]), -1); // after the poll() call, add a check to see if both pipes are closed if (!(watched_fds[1].events & POLLIN) && !(watched_fds[2].events & POLLIN)) { break_out = true; } if (num_files_readable == -1) { // error occurred in poll() tty_logger.log(E_FATAL, "poll() failed: " + std::string(strerror(errno))); tty_logger.log_to_json_file("E_FATAL", "poll() failed", tty_user, tty_group, exec_command); ttyResetExit( &ttyOrig); throw std::runtime_error("poll() failed"); } if (num_files_readable == 0) { // poll reports no files readable break_out = true; break; } for (int this_fd = 0; this_fd < 3; this_fd++) { if (watched_fds[this_fd].revents & POLLIN) { // this pipe is readable byte_count = read(watched_fds[this_fd].fd, buf, BUFFER_SIZE); if (byte_count == -1) { if (errno == EAGAIN) { continue; } else { // error reading from pipe tty_logger.log(E_FATAL, "Error while reading: " + std::string(strerror(errno))); tty_logger.log_to_json_file("E_FATAL", "Error while reading from pipe", tty_user, tty_group, exec_command); ttyResetExit( &ttyOrig); stderr_read_guard.reset(-1); master_fd_guard.reset(-1); exit(EXIT_FAILURE); } } else if (byte_count == 0) { // reached EOF on one of the streams but not a HUP // we've read all we can this cycle, so go to the next fd in the for loop continue; } else { // byte count was sane // write to stdout,stderr if (this_fd == 0) { // parent stdin received, write to child pty (stdin) write_all(masterFd, buf, byte_count); } else if (this_fd == 1 ) { // child pty sent some stuff, write to parent stdout and log write_all(stdout_log_fh->_fileno, buf, byte_count); write_all(STDOUT_FILENO, buf, byte_count); } else if ( this_fd == 2 ){ //the child's stderr pipe sent some stuff, write to parent stderr and log write_all(stderr_log_fh->_fileno, buf, byte_count); write_all(STDERR_FILENO, buf, byte_count); } else { // this should never happen tty_logger.log(E_FATAL, "Logic error: unexpected pipe index."); exit(EXIT_FAILURE); } } } if (watched_fds[this_fd].revents & POLLERR) { if (this_fd == 1) { master_fd_guard.reset(-1); } else if (this_fd == 2) { stderr_read_guard.reset(-1); } //close(watched_fds[this_fd].fd); break_out = true; //continue; } // if (watched_fds[this_fd].revents & POLLHUP) { // // this pipe has hung up // close(watched_fds[this_fd].fd); // break_out = true; // break; // } if (watched_fds[this_fd].revents & POLLHUP) { // this pipe has hung up // don't close the file descriptor yet, there might still be data to read // instead, remove the POLLIN event to avoid getting a POLLHUP event in the next poll() call watched_fds[this_fd].events &= ~POLLIN; } } } // wait for child to exit, capture status waitpid(pid, &status, 0); while ((byte_count = read(master_fd_guard.get(), buf, BUFFER_SIZE)) > 0) { write_all(stdout_log_fh->_fileno, buf, byte_count); write_all(STDOUT_FILENO, buf, byte_count); } while ((byte_count = read(stderr_read_guard.get(), buf, BUFFER_SIZE)) > 0) { write_all(stderr_log_fh->_fileno, buf, byte_count); write_all(STDERR_FILENO, buf, byte_count); } // Handle child process status with proper signal handling pid_t child_pid = waitpid(-1, &status, WNOHANG); // Non-blocking wait for child process if (child_pid > 0) { // If a child process has terminated // Check if the child process exited normally if (WIFEXITED(status)) { int exit_code = WEXITSTATUS(status); tty_logger.log(E_INFO, "Child exited with status " + std::to_string(exit_code)); tty_logger.log_to_json_file("E_INFO", "Child exited with status " + std::to_string(exit_code), tty_user, tty_group, exec_command); ttyResetExit( &ttyOrig); return exit_code; } // Check if the child process was terminated by a signal else if (WIFSIGNALED(status)) { int signal_number = WTERMSIG(status); tty_logger.log(E_FATAL, "Process terminated by signal: " + std::to_string(signal_number)); tty_logger.log_to_json_file("E_FATAL", "Process terminated by signal: " + std::to_string(signal_number), tty_user, tty_group, exec_command); // Reset signal handler to default before exiting signal(signal_number, SIG_DFL); ttyResetExit( &ttyOrig); return 128 + signal_number; // POSIX exit code format } // Handle unexpected exit conditions else { tty_logger.log(E_WARN, "Unknown child exit condition."); tty_logger.log_to_json_file("E_WARN", "Unknown child exit condition", tty_user, tty_group, exec_command); } } ttyResetExit( &ttyOrig); if WIFEXITED(status) { return WEXITSTATUS(status); } else { return -617; } } } return 0; } catch (const std::exception& ex) { std::cerr << "[LCPEX TTY ERROR] " << ex.what() << std::endl; return -999; } }