diff --git a/common/os_calls.c b/common/os_calls.c index ff1182b28..1071ca3b6 100644 --- a/common/os_calls.c +++ b/common/os_calls.c @@ -37,6 +37,7 @@ #define ctid_t id_t #endif #include +#include #include #include #include @@ -2588,6 +2589,23 @@ g_executable_exist(const char *exename) return access(exename, R_OK | X_OK) == 0; } +/*****************************************************************************/ +/* returns boolean, non zero if the socket exists */ +int +g_socket_exist(const char *sockname) +{ + struct stat st; + + if (stat(sockname, &st) == 0) + { + return S_ISSOCK(st.st_mode); + } + else + { + return 0; + } +} + /*****************************************************************************/ /* returns boolean */ int @@ -4200,3 +4218,61 @@ g_qsort(void *base, size_t nitems, size_t size, { qsort(base, nitems, size, compar); } + +/*****************************************************************************/ +struct list * +g_readdir(const char *dir) +{ + DIR *handle; + struct list *result = NULL; + struct dirent *dent; + int saved_errno; + + errno = 0; // See readdir(3) + if ((handle = opendir(dir)) != NULL && + (result = list_create()) != NULL) + { + result->auto_free = 1; + while (1) + { + errno = 0; + dent = readdir(handle); + if (dent == NULL) + { + break; // errno = 0 for end-of-dir, or != 0 for error + } + + // Ignore '.' and '..' + if (dent->d_name[0] == '.' && dent->d_name[1] == '\0') + { + continue; + } + if (dent->d_name[0] == '.' && dent->d_name[1] == '.' && + dent->d_name[2] == '\0') + { + continue; + } + + if (!list_add_strdup(result, dent->d_name)) + { + // Memory allocation failure + errno = ENOMEM; + break; + } + } + } + + saved_errno = errno; + if (errno != 0) + { + list_delete(result); + result = NULL; + } + if (handle != NULL) + { + closedir(handle); + } + errno = saved_errno; + + return result; +} diff --git a/common/os_calls.h b/common/os_calls.h index 31e8933ba..ad54ddca7 100644 --- a/common/os_calls.h +++ b/common/os_calls.h @@ -258,6 +258,7 @@ int g_file_exist(const char *filename); int g_file_readable(const char *filename); int g_directory_exist(const char *dirname); int g_executable_exist(const char *dirname); +int g_socket_exist(const char *dirname); int g_create_dir(const char *dirname); int g_create_path(const char *path); int g_remove_dir(const char *dirname); @@ -428,6 +429,19 @@ void g_qsort(void *base, size_t nitems, size_t size, int (*compar)(const void *, const void *)); +/** + * Returns a list of the filenames contained within a directory + * + * @param dir Name of directory + * @return list of directory entry names + * + * If NULL is returned, further information may be available in errno. No + * other errors are specifically logged. + * The special files '.' and '..' are not returned. + */ +struct list * +g_readdir(const char *dir); + /* glib-style wrappers */ #define g_new(struct_type, n_structs) \ (struct_type *) malloc(sizeof(struct_type) * (n_structs)) diff --git a/instfiles/xrdp.service.in b/instfiles/xrdp.service.in index 52216cc81..72bc2e99f 100644 --- a/instfiles/xrdp.service.in +++ b/instfiles/xrdp.service.in @@ -11,6 +11,13 @@ EnvironmentFile=-@sysconfdir@/default/xrdp ExecStart=@sbindir@/xrdp $XRDP_OPTIONS --nodaemon SystemCallArchitectures=native SystemCallFilter=@system-service +# Uncomment the following line if you wish xrdp connections to survive +# an xrdp restart. +# +# This is not recommended, as the xrdp and xrdp-sesman processes can +# end up with API differences if the restart was caused by an upgrade. It +# may however be useful for some use cases. +#KillMode=process [Install] WantedBy=multi-user.target diff --git a/libipm/ercp.c b/libipm/ercp.c index a5ba61c9e..008583878 100644 --- a/libipm/ercp.c +++ b/libipm/ercp.c @@ -81,6 +81,32 @@ ercp_trans_from_eicp_trans(struct trans *trans, /*****************************************************************************/ +struct trans * +ercp_connect(const char *port, int (*term_func)(void)) +{ + struct trans *t; + + if ((t = trans_create(TRANS_MODE_UNIX, 128, 128)) != NULL) + { + t->is_term = term_func; + + if (trans_connect(t, NULL, port, 3000) != 0) + { + trans_delete(t); + t = NULL; + } + else if (ercp_init_trans(t) != 0) + { + trans_delete(t); + t = NULL; + } + } + + return t; +} + +/*****************************************************************************/ + int ercp_msg_in_check_available(struct trans *trans, int *available) { @@ -182,10 +208,7 @@ ercp_get_session_announce_event(struct trans *trans, if (rv == 0) { - if (display != NULL) - { - *display = i_display; - } + *display = i_display; *uid = (uid_t)i_uid; *type = (enum scp_session_type)i_type; *start_width = i_width; diff --git a/libipm/ercp.h b/libipm/ercp.h index aac0a8d68..0f608fd32 100644 --- a/libipm/ercp.h +++ b/libipm/ercp.h @@ -88,6 +88,16 @@ ercp_trans_from_eicp_trans(struct trans *trans, ttrans_data_in callback_func, void *callback_data); +/** + * Connects to an ERCP endpoint. + * + * @param port Path to UDS object in filesystem + * @param term_func Function to poll during connection for program + * termination, or NULL for none. + */ +struct trans * +ercp_connect(const char *port, + int (*term_func)(void)); /** * Checks an ERCP transport to see if a complete message is @@ -178,7 +188,6 @@ ercp_send_session_announce_event(struct trans *trans, * * @param trans EICP transport * @param[out] display Display used by session. - * Pointer can be NULL if this is already known. * @param[out] uid UID of user logged in to session * @param[out] type Session type * @param[out] start_width Starting width of seenio diff --git a/sesman/Makefile.am b/sesman/Makefile.am index 9ddef8e84..74dc7a85f 100644 --- a/sesman/Makefile.am +++ b/sesman/Makefile.am @@ -32,6 +32,8 @@ xrdp_sesman_SOURCES = \ sesexec_control.h \ session_list.c \ session_list.h \ + sesman_restart.c \ + sesman_restart.h \ sig.c \ sig.h diff --git a/sesman/ercp_process.c b/sesman/ercp_process.c index 4ac95990e..8569b23b7 100644 --- a/sesman/ercp_process.c +++ b/sesman/ercp_process.c @@ -42,9 +42,10 @@ process_session_announce_event(struct session_item *si) { int rv; const char *start_ip_addr; + unsigned int display; rv = ercp_get_session_announce_event(si->sesexec_trans, - NULL, + &display, &si->uid, &si->type, &si->start_width, @@ -53,10 +54,24 @@ process_session_announce_event(struct session_item *si) &si->guid, &start_ip_addr, &si->start_time); + if (rv == 0) + { + // We may already know the display we sent sesexec. If we do, + // check sesexec sent the same value back. + if (si->display >= 0 && display != (unsigned int)si->display) + { + LOG(LOG_LEVEL_ERROR, "Bugcheck: sesman expected display %d, got %u", + si->display, display); + rv = 1; + } + } + if (rv == 0) { snprintf(si->start_ip_addr, sizeof(si->start_ip_addr), "%s", start_ip_addr); + si->display = display; + si->state = E_SESSION_RUNNING; } diff --git a/sesman/libsesman/sesman_config.h b/sesman/libsesman/sesman_config.h index aa8d43dcf..26800cb47 100644 --- a/sesman/libsesman/sesman_config.h +++ b/sesman/libsesman/sesman_config.h @@ -183,8 +183,11 @@ struct config_sesman /** * @var listen_port * @brief Listening port + * + * This string is used to form the restart directory name, so + * can't be the full XRDP_SOCKETS_MAXPATH length. */ - char listen_port[XRDP_SOCKETS_MAXPATH]; + char listen_port[XRDP_SOCKETS_MAXPATH - 10]; /** * @var enable_user_wm * @brief Flag that enables user specific wm diff --git a/sesman/sesexec/Makefile.am b/sesman/sesexec/Makefile.am index 1eae19cb0..ac8a2c429 100644 --- a/sesman/sesexec/Makefile.am +++ b/sesman/sesexec/Makefile.am @@ -29,6 +29,8 @@ xrdp_sesexec_SOURCES = \ env.h \ login_info.c \ login_info.h \ + sesexec_discover.c \ + sesexec_discover.h \ sessionrecord.c \ sessionrecord.h \ xauth.c \ diff --git a/sesman/sesexec/eicp_server.c b/sesman/sesexec/eicp_server.c index 80255fdf0..210d21b75 100644 --- a/sesman/sesexec/eicp_server.c +++ b/sesman/sesexec/eicp_server.c @@ -37,6 +37,7 @@ #include "ercp.h" #include "scp.h" #include "sesexec.h" +#include "sesexec_discover.h" #include "session.h" /******************************************************************************/ @@ -98,12 +99,13 @@ handle_create_session_request(struct trans *self) { int scp_fd; struct session_parameters sp = {0}; - int rv; + int status; - rv = eicp_get_create_session_request(self, &scp_fd, &sp.display, - &sp.type, &sp.width, &sp.height, - &sp.bpp, &sp.shell, &sp.directory); - if (rv == 0) + status = eicp_get_create_session_request( + self, &scp_fd, &sp.display, + &sp.type, &sp.width, &sp.height, + &sp.bpp, &sp.shell, &sp.directory); + if (status == 0) { // Need to talk to the SCP client struct trans *scp_trans; @@ -113,7 +115,7 @@ handle_create_session_request(struct trans *self) { LOG(LOG_LEVEL_ERROR, "Can't create SCP trans"); g_file_close(scp_fd); - rv = 1; + status = 1; } else { @@ -134,39 +136,68 @@ handle_create_session_request(struct trans *self) scp_status = session_start(g_login_info, &sp, &g_session_data); } - // Return the status to the SCP client - rv = scp_send_create_session_response(scp_trans, scp_status, - sp.display, &sp.guid); - trans_delete(scp_trans); - - // Further comms from sesexec is sent over the ERCP protocol + // Further comms to sesman is sent over the ERCP protocol ercp_trans_from_eicp_trans(self, sesexec_ercp_data_in, (void *)self); - if (scp_status == E_SCP_SCREATE_OK) + if (scp_status != E_SCP_SCREATE_OK) { - rv = ercp_send_session_announce_event( - self, - sp.display, - g_login_info->uid, - sp.type, - sp.width, - sp.height, - sp.bpp, - &sp.guid, - g_login_info->ip_addr, - session_get_start_time(g_session_data)); + // Tell sesman the session hasn't started + (void)ercp_send_session_finished_event(self); } - else + else if ((status = ercp_send_session_announce_event( + self, + sp.display, + g_login_info->uid, + sp.type, + sp.width, + sp.height, + sp.bpp, + &sp.guid, + g_login_info->ip_addr, + session_get_start_time(g_session_data))) != 0) + { + // We failed to tell sesman about the new session. This + // probably means sesman has exited in the time between + // asking us to start a session, and our reply. This + // could be many seconds, and a new sesman may well + // have started. + // If we enable the restart functionality at + // this point, we have a race condition that could + // result in a session which sesman doesn't know + // about. The simplest thing to do in this rare situation + // is to abort the session - the user can create a + // new one + LOG(LOG_LEVEL_ERROR, + "sesman appears to have failed - stopping session"); + scp_status = E_SCP_SCREATE_GENERAL_ERROR; + } + else if ((status = sesexec_discover_enable()) != 0) { - rv = ercp_send_session_finished_event(self); - sesexec_terminate_main_loop(1); + // Equally regrettable - we can't make the session + // discoverable, so we'll stop it. We can tell sesman + // about this one however. + LOG(LOG_LEVEL_ERROR, + "unable to make session discoverable" + " - stopping session"); + (void)ercp_send_session_finished_event(self); + scp_status = E_SCP_SCREATE_GENERAL_ERROR; } + + // Return the status to the SCP client. + (void)scp_send_create_session_response(scp_trans, scp_status, + sp.display, &sp.guid); + trans_delete(scp_trans); } + } + if (status != 0) + { + // Kill sesexec, and any active session + sesexec_terminate_main_loop(status); } - return rv; + return 0; } /******************************************************************************/ diff --git a/sesman/sesexec/sesexec.c b/sesman/sesexec/sesexec.c index 67b493e8d..0d9e44088 100644 --- a/sesman/sesexec/sesexec.c +++ b/sesman/sesexec/sesexec.c @@ -38,6 +38,7 @@ #include "ercp_server.h" #include "login_info.h" #include "sesexec.h" +#include "sesexec_discover.h" #include "sesman_config.h" #include "log.h" #include "os_calls.h" @@ -67,6 +68,7 @@ pid_t g_pid; * Module-scope globals */ static struct trans *g_ecp_trans; +static pid_t g_ecp_pid; static int g_terminate_loop = 0; static int g_terminate_status = 0; @@ -119,30 +121,6 @@ process_params(int argc, char **argv, return 0; } -/******************************************************************************/ -#if 0 -static int -sesexec_scp_data_in(struct trans *self) -{ - int rv; - int available; - - rv = scp_msg_in_check_available(self, &available); - - if (rv == 0 && available) - { - struct sesman_con *sc = (struct sesman_con *)self->callback_data; - //if ((rv = scp_process(sc)) != 0) - { - LOG(LOG_LEVEL_ERROR, "%s: scp_process failed", __func__); - } - scp_msg_in_reset(self); - } - - return rv; -} -#endif - /******************************************************************************/ static int sesexec_eicp_data_in(struct trans *self) @@ -233,8 +211,59 @@ sesexec_is_term(void) void sesexec_terminate_main_loop(int status) { - g_terminate_loop = 1; - g_terminate_status = status; + // Only take the first request to terminate the loop + if (!g_terminate_loop) + { + g_terminate_loop = 1; + g_terminate_status = status; + } +} + +/******************************************************************************/ +int +sesexec_set_ecp_transport(struct trans *t) +{ + int rv; + int pid; + int uid; + int gid; + + if (t == NULL) + { + trans_delete(g_ecp_trans); + g_ecp_trans = NULL; + g_ecp_pid = 0; + rv = 0; + } + else if ((rv = g_sck_get_peer_cred(t->sck, &pid, &uid, &gid)) != 0) + { + LOG(LOG_LEVEL_ERROR, "Can't get credentials of sesman socket [%s]", + g_get_strerror()); + } + else if (uid != 0 || gid != 0) + { + LOG(LOG_LEVEL_ERROR, "sesman PID %d is running as UID:GID %d:%d", + pid, uid, gid); + rv = 1; + } + else + { + trans_delete(g_ecp_trans); + g_ecp_trans = t; + g_ecp_pid = pid; + rv = 0; + } + + return rv; +} + +/******************************************************************************/ +int +sesexec_is_ecp_active(void) +{ + return (g_ecp_trans != NULL && + g_ecp_pid != 0 && g_pid_is_active(g_ecp_pid)); + } /******************************************************************************/ @@ -251,6 +280,45 @@ process_sigchld_event(void) } } +/******************************************************************************/ +static void +sesexec_terminate_session_and_wait(void) +{ + session_send_term(g_session_data); + do + { + g_sleep(1000); + // Process SIGCHLD events while waiting for the session + // to exit. + if (g_is_wait_obj_set(g_sigchld_event)) + { + g_reset_wait_obj(g_sigchld_event); + process_sigchld_event(); + } + } + while (session_active(g_session_data)); +} + +/******************************************************************************/ +static void +sesexec_main_loop_cleanup(void) +{ + login_info_free(g_login_info); + + /* This session is no longer discoverable */ + sesexec_discover_disable(); + + /* Don't allow sesexec to terminate with an active + session, as we can't connect to such a session */ + if (session_active(g_session_data)) + { + LOG(LOG_LEVEL_INFO, + "Stopping session on xrdp-sesexec exit"); + sesexec_terminate_session_and_wait(); + } + session_data_free(g_session_data); +} + /******************************************************************************/ /** * @@ -262,7 +330,8 @@ sesexec_main_loop(void) { int error = 0; int robjs_count; - intptr_t robjs[32]; +#define MAX_ROBJS 32 + intptr_t robjs[MAX_ROBJS]; g_terminate_loop = 0; g_terminate_status = 0; @@ -274,11 +343,25 @@ sesexec_main_loop(void) robjs[robjs_count++] = g_term_event; robjs[robjs_count++] = g_sigchld_event; - error = trans_get_wait_objs(g_ecp_trans, robjs, &robjs_count); + // ECP transport may be null if sesman has gone away + if (g_ecp_trans != NULL) + { + error = trans_get_wait_objs(g_ecp_trans, robjs, &robjs_count); + if (error != 0) + { + LOG(LOG_LEVEL_ERROR, "sesexec_main_loop: " + "trans_get_wait_objs(ECP) failed"); + sesexec_terminate_main_loop(error); + continue; + } + } + + // Add any objects from the discover module + error = sesexec_discover_get_wait_objs(robjs, &robjs_count, MAX_ROBJS); if (error != 0) { LOG(LOG_LEVEL_ERROR, "sesexec_main_loop: " - "trans_get_wait_objs(ECP) failed"); + "sesexec_discover_get_wait_objs() failed"); sesexec_terminate_main_loop(error); continue; } @@ -297,21 +380,17 @@ sesexec_main_loop(void) g_reset_wait_obj(g_term_event); if (session_active(g_session_data)) { - // Ask the active session to terminate LOG(LOG_LEVEL_INFO, "sesexec_main_loop: " - "sesexec asked to terminate. " - "Terminating active session"); - session_send_term(g_session_data); + "sesexec asked to terminate with active session."); } else { - // Terminate immediately LOG(LOG_LEVEL_INFO, "sesexec_main_loop: " "sesexec asked to terminate. " "No session is active"); - sesexec_terminate_main_loop(0); - continue; } + sesexec_terminate_main_loop(0); + continue; } if (g_is_wait_obj_set(g_sigchld_event)) /* SIGCHLD */ @@ -326,7 +405,10 @@ sesexec_main_loop(void) { // We've finished the session. Tell sesman and // finish up. - (void)ercp_send_session_finished_event(g_ecp_trans); + if (g_ecp_trans != NULL) + { + (void)ercp_send_session_finished_event(g_ecp_trans); + } session_data_free(g_session_data); g_session_data = NULL; @@ -335,19 +417,49 @@ sesexec_main_loop(void) } } - error = trans_check_wait_objs(g_ecp_trans); + if (g_ecp_trans != NULL) + { + error = trans_check_wait_objs(g_ecp_trans); + if (error != 0) + { + if (g_ecp_trans->status != TRANS_STATUS_UP && + session_active(g_session_data)) + { + // sesman has gone away. We have an active session + // to keep track of, so sesman can pick it up when it + // restarts + LOG(LOG_LEVEL_INFO, "sesexec_main_loop: " + "sesman has exited"); + sesexec_set_ecp_transport(NULL); + } + else + { + // A callback has failed, or sesman has gone away and + // we have no active session + LOG(LOG_LEVEL_ERROR, "sesexec_main_loop: " + "trans_check_wait_objs failed for ECP transport"); + sesexec_terminate_main_loop(error); + } + continue; + } + } + + error = sesexec_discover_check_wait_objs(); if (error != 0) { LOG(LOG_LEVEL_ERROR, "sesexec_main_loop: " - "trans_check_wait_objs failed for ECP transport"); + "sesexec_discover_check_wait_objs failed"); sesexec_terminate_main_loop(error); continue; } + } - login_info_free(g_login_info); + /* close sesman communications immediately */ + sesexec_set_ecp_transport(NULL); return g_terminate_status; +#undef MAX_ROBJS } /******************************************************************************/ @@ -476,6 +588,7 @@ main(int argc, char **argv) else { char text[128]; + struct trans *t; g_pid = g_getpid(); @@ -496,10 +609,10 @@ main(int argc, char **argv) /* Set up an EICP process handler * Errors are logged by this call if necessary */ - g_ecp_trans = eicp_init_trans_from_fd(eicp_fd, - TRANS_TYPE_SERVER, - sesexec_is_term); - if (g_ecp_trans != NULL) + t = eicp_init_trans_from_fd(eicp_fd, + TRANS_TYPE_SERVER, + sesexec_is_term); + if (t != NULL && sesexec_set_ecp_transport(t) == 0) { g_ecp_trans->trans_data_in = sesexec_eicp_data_in; g_ecp_trans->callback_data = NULL; @@ -507,9 +620,8 @@ main(int argc, char **argv) /* start program main loop */ LOG(LOG_LEVEL_INFO, "starting xrdp-sesexec with pid %d", g_pid); error = sesexec_main_loop(); - trans_delete(g_ecp_trans); + sesexec_main_loop_cleanup(); } - g_delete_wait_obj(g_term_event); } config_free(g_cfg); diff --git a/sesman/sesexec/sesexec.h b/sesman/sesexec/sesexec.h index ff6fe7e0f..7d476b2c2 100644 --- a/sesman/sesexec/sesexec.h +++ b/sesman/sesexec/sesexec.h @@ -67,4 +67,24 @@ sesexec_is_term(void); void sesexec_terminate_main_loop(int status); +/** + * Sets the ECP (sesman) transport + * + * @param t Transport supposedly connected to the ECP protocol + * provider (sesman), or NULL to clear the transport + * @return 0 for success + * + * Peer credentials are checked for root:root + */ +int +sesexec_set_ecp_transport(struct trans *t); + +/* + * Is the ECP transport still active? + * + * @result boolean + */ +int +sesexec_is_ecp_active(void); + #endif // SESEXEC_H diff --git a/sesman/sesexec/sesexec_discover.c b/sesman/sesexec/sesexec_discover.c new file mode 100644 index 000000000..952b327c3 --- /dev/null +++ b/sesman/sesexec/sesexec_discover.c @@ -0,0 +1,188 @@ +/** + * xrdp: A Remote Desktop Protocol server. + * + * Copyright (C) Jay Sorg 2004-2024 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * + * @file sesexec_discover.c + * @brief Declare functionality associated with sesman restart support + * @author Matt Burt + * + */ + +#if defined(HAVE_CONFIG_H) +#include +#endif + +#include + +#include "sesexec_discover.h" +#include "trans.h" +#include "sesexec.h" +#include "session.h" +#include "sesman_config.h" +#include "os_calls.h" +#include "ercp.h" +#include "login_info.h" + +/* + * Module-scope globals + */ +static struct trans *g_discover_trans = NULL; + +/*****************************************************************************/ +static int +discover_trans_conn_in(struct trans *trans, struct trans *new_trans) +{ + const struct session_parameters *sp; + int rv = 0; + + if (trans == NULL || new_trans == NULL || trans != g_discover_trans) + { + return 1; + } + + LOG_DEVEL(LOG_LEVEL_DEBUG, "discover_trans_conn_in:"); + + if (sesexec_is_ecp_active()) + { + int pid = 0; + (void)g_sck_get_peer_cred(new_trans->sck, &pid, 0, 0); + + LOG(LOG_LEVEL_WARNING, + "Connection attempt to sesexec PID %d from PID %d" + " while ECP is still active", + g_pid, pid); + + trans_delete(new_trans); + } + else if ((sp = session_get_parameters(g_session_data)) == NULL) + { + // If we haven't got session parameters, we shouldn't be here + LOG(LOG_LEVEL_ERROR, "Bugcheck: Can't get active session params"); + trans_delete(new_trans); + rv = 1; + } + else + { + // Reconnect to sesman and tell it about our existing + // session + ercp_init_trans(new_trans); + new_trans->trans_data_in = sesexec_ercp_data_in; + new_trans->callback_data = (void *)new_trans; + + // Note, this call makes further privilege checks that may still + // fail. If they do however, we wish to carry on running. These + // failed checks will be logged. + if (sesexec_set_ecp_transport(new_trans) == 0) + { + (void)ercp_send_session_announce_event( + new_trans, + sp->display, + g_login_info->uid, + sp->type, + sp->width, + sp->height, + sp->bpp, + &sp->guid, + g_login_info->ip_addr, + session_get_start_time(g_session_data)); + } + } + return rv; +} + +/******************************************************************************/ +int +sesexec_discover_enable(void) +{ + int rv = 1; + if (g_session_data == NULL) + { + LOG(LOG_LEVEL_ERROR, "Cant enable discovery without an active session"); + } + else if (g_discover_trans != NULL) + { + LOG(LOG_LEVEL_ERROR, "Logic error: discovery is already active"); + } + else if ((g_discover_trans = + trans_create(TRANS_MODE_UNIX, 8192, 8192)) == NULL) + { + LOG(LOG_LEVEL_ERROR, "Out of memory enabling discovery"); + } + else + { + char discover_port[XRDP_SOCKETS_MAXPATH]; + + snprintf(discover_port, sizeof(discover_port), "%s.r/%u", + g_cfg->listen_port, + session_get_parameters(g_session_data)->display); + g_discover_trans->is_term = sesexec_is_term; + g_discover_trans->trans_conn_in = discover_trans_conn_in; + if ((rv = trans_listen(g_discover_trans, discover_port)) != 0) + { + LOG(LOG_LEVEL_ERROR, "Transport error enabling discovery [%s]", + g_get_strerror()); + trans_delete(g_discover_trans); + g_discover_trans = NULL; + } + } + + return rv; +} + +/******************************************************************************/ +int +sesexec_discover_disable(void) +{ + trans_delete(g_discover_trans); + g_discover_trans = NULL; + + return 0; +} + +/******************************************************************************/ + +int +sesexec_discover_get_wait_objs(intptr_t robjs[], int *robjs_count, + int max_count) +{ + int rv; + if (g_discover_trans == NULL) + { + rv = 0; + } + else if (*robjs_count >= max_count) + { + rv = 1; + } + else + { + rv = trans_get_wait_objs(g_discover_trans, robjs, robjs_count); + } + + return rv; +} + +/******************************************************************************/ +int +sesexec_discover_check_wait_objs(void) +{ + return (g_discover_trans == NULL) + ? 0 + : trans_check_wait_objs(g_discover_trans); +} diff --git a/sesman/sesexec/sesexec_discover.h b/sesman/sesexec/sesexec_discover.h new file mode 100644 index 000000000..e29fea402 --- /dev/null +++ b/sesman/sesexec/sesexec_discover.h @@ -0,0 +1,77 @@ +/** + * xrdp: A Remote Desktop Protocol server. + * + * Copyright (C) Jay Sorg 2004-2024 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * + * @file sesexec_discover.h + * @brief Declare functionality associated with sesman restart support + * for sesexec + * + * @author Matt Burt + * + */ + +#ifndef SESEXEC_DISCOVER_H +#define SESEXEC_DISCOVER_H + +#include + +/** + * Start the listening object used when sesman restarts + * + * @return != 0 for error + */ +int +sesexec_discover_enable(void); + +/** + * Stop the listening object used when sesman restarts, and deallocate all + * module resources. + * + * @return != 0 for error + */ +int +sesexec_discover_disable(void); + +/** + * Add any file descriptors in use by the module to an array + * + * @param robjs Array to add fds to + * @param[in,out] robjs_count Index where elements are to be added + * @param max_count Max value of robjs_count + * @return != 0 for error + * + * This function can be called before sesexec_discover_enable(), + * in which case it does nothing. + */ +int +sesexec_discover_get_wait_objs(intptr_t robjs[], int *robjs_count, + int max_count); + + +/** + * Check any file descriptors in use by the module for actionable events + * @return != 0 for error + * + * This function can be called before sesexec_discover_enable(), + * in which case it does nothing. + */ +int +sesexec_discover_check_wait_objs(void); + +#endif // SESEXEC_DISCOVER_H diff --git a/sesman/sesexec/session.c b/sesman/sesexec/session.c index 13ac7f359..6641ac8ea 100644 --- a/sesman/sesexec/session.c +++ b/sesman/sesexec/session.c @@ -929,12 +929,22 @@ session_get_start_time(const struct session_data *sd) return (sd == NULL) ? 0 : sd->start_time; } +/******************************************************************************/ +const struct session_parameters * +session_get_parameters(const struct session_data *sd) +{ + return (sd == NULL) ? NULL : &sd->params; +} + /******************************************************************************/ void session_send_term(struct session_data *sd) { if (sd != NULL && sd->win_mgr > 0) { + // Killing the window manager only is appropriate here. + // When we process SIGCHLD for the windowe manager, we + // will kill other processes as appropriate g_sigterm(sd->win_mgr); } } diff --git a/sesman/sesexec/session.h b/sesman/sesexec/session.h index 5cfab1409..a1fdd4abf 100644 --- a/sesman/sesexec/session.h +++ b/sesman/sesexec/session.h @@ -103,10 +103,23 @@ session_active(const struct session_data *sd); * Returns the start time for an active session * * @param sd session_data for this session + * @return session start time */ time_t session_get_start_time(const struct session_data *sd); +/** + * Returns the parameters used to start the session + * + * @param sd session_data for this session + * @return Pointer to parameters + * + * The pointed-to data returned must not be modified in + * any way. + */ +const struct session_parameters * +session_get_parameters(const struct session_data *sd); + /*** * Ask a session to terminate by signalling the window manager * diff --git a/sesman/sesman.c b/sesman/sesman.c index 18cd765f0..4fbd94e8e 100644 --- a/sesman/sesman.c +++ b/sesman/sesman.c @@ -46,6 +46,7 @@ #include "scp.h" #include "scp_process.h" #include "sesexec_control.h" +#include "sesman_restart.h" #include "sig.h" #include "string_calls.h" #include "trans.h" @@ -78,7 +79,7 @@ struct sesman_startup_params }; struct config_sesman *g_cfg; -static tintptr g_term_event = 0; +tintptr g_term_event = 0; static tintptr g_sigchld_event = 0; static tintptr g_reload_event = 0; @@ -87,7 +88,6 @@ static struct trans *g_list_trans; /* Variables used to lock g_list_trans */ static struct lock_uds *g_list_trans_lock; -static struct list *g_con_list = NULL; static int g_pid; /*****************************************************************************/ @@ -202,7 +202,18 @@ static int sesman_listen_test(struct config_sesman *cfg) } /******************************************************************************/ -int +/** + * Close all file descriptors used by sesman. + * + * This is generally used after forking, to make sure the + * file descriptors used by the main process are not disturbed + * + * This call will also :- + * - release all trans objects held by sesman + * - Delete sesman wait objects + * - Call sesman_delete_listening_transport() + */ +static int sesman_close_all(void) { LOG_DEVEL(LOG_LEVEL_TRACE, "sesman_close_all:"); @@ -459,17 +470,10 @@ sesman_main_loop(void) int robjs_count; intptr_t robjs[1024]; - g_con_list = list_create(); - if (g_con_list == NULL) - { - LOG(LOG_LEVEL_ERROR, "sesman_main_loop: list_create failed"); - return 1; - } if (sesman_create_listening_transport(g_cfg) != 0) { LOG(LOG_LEVEL_ERROR, "sesman_main_loop: sesman_create_listening_transport failed"); - list_delete(g_con_list); return 1; } LOG(LOG_LEVEL_INFO, "Sesman now listening on %s", g_cfg->listen_port); @@ -937,7 +941,8 @@ main(int argc, char **argv) } if ((error = pre_session_list_init(MAX_PRE_SESSION_ITEMS)) == 0 && - (error = session_list_init()) == 0) + (error = session_list_init()) == 0 && + (error = sesman_restart_discover_sessions()) == 0) { error = sesman_main_loop(); } diff --git a/sesman/sesman.h b/sesman/sesman.h index 5f7c06d28..6c60de738 100644 --- a/sesman/sesman.h +++ b/sesman/sesman.h @@ -32,20 +32,7 @@ struct trans; /* Globals */ extern struct config_sesman *g_cfg; - -/** - * Close all file descriptors used by sesman. - * - * This is generally used after forking, to make sure the - * file descriptors used by the main process are not disturbed - * - * This call will also :- - * - release all trans objects held by sesman - * - Delete sesman wait objects - * - Call sesman_delete_listening_transport() - */ -int -sesman_close_all(void); +extern tintptr g_term_event; /* * Remove the listening transport diff --git a/sesman/sesman_restart.c b/sesman/sesman_restart.c new file mode 100644 index 000000000..ff7e0cae5 --- /dev/null +++ b/sesman/sesman_restart.c @@ -0,0 +1,288 @@ +/** + * xrdp: A Remote Desktop Protocol server. + * + * Copyright (C) Matt Burt 2024 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * + * @file sesman_restart.c + * @brief Sesman restart definitions + * @author Matt Burt + * + */ + +#if defined(HAVE_CONFIG_H) +#include "config_ac.h" +#endif + +#include + +#include "os_calls.h" +#include "sesman.h" +#include "sesman_config.h" +#include "session_list.h" + +#include "ercp.h" + +#include "sesman_restart.h" +#include "xrdp_sockets.h" + +// Result of calling init_restart_directory() +enum init_restart_dir_status +{ + E_RESTART_DIR_CREATED_OK, ///< All good. Dir created + E_RESTART_DIR_ALREADY_EXISTS, ///< All good. Dir already existed + E_RESTART_DIR_ERROR ///< Not good +}; + +enum +{ + MAX_DISCOVERY_WAIT_TIME = 5000 // Milli-seconds +}; + +/******************************************************************************/ +static enum init_restart_dir_status +init_restart_directory(const char *restart_dir) +{ + enum init_restart_dir_status rv; + + if (g_directory_exist(restart_dir)) + { + rv = E_RESTART_DIR_ALREADY_EXISTS; + } + else + { + // Create the restart directory for the next run + if (g_mkdir(restart_dir) != 0) + { + LOG(LOG_LEVEL_ERROR, "Can't create restart directory %s [%s]", + restart_dir, g_get_strerror()); + rv = E_RESTART_DIR_ERROR; + } + else + { + rv = E_RESTART_DIR_CREATED_OK; + } + } + + if (rv != E_RESTART_DIR_ERROR) + { + // Always set the permissions on the restart directory, whether + // or not we created it + if (g_chown(restart_dir, g_getuid(), g_getuid()) != 0) + { + LOG(LOG_LEVEL_ERROR, "Can't set ownership of '%s' [%s]", + restart_dir, g_get_strerror()); + rv = E_RESTART_DIR_ERROR; + } + else if (g_chmod_hex(restart_dir, 0x700) != 0) + { + LOG(LOG_LEVEL_ERROR, "Can't set permissions on '%s' [%s]", + restart_dir, g_get_strerror()); + rv = E_RESTART_DIR_ERROR; + } + } + return rv; +} + +/******************************************************************************/ +/** + * Attempts to add a sesexec Unix Domain Socket to the process_list + * @param filename Name of UDS + * @return Boolean for success + * + * The credentials of the process on the other end are checked. + * We don't take the max session limit into account when adding sessions here, + * as this could result in orphaned sessions. + */ +static int +add_sesexec_fd_to_session_list(const char *filename) +{ + int status = 0; + struct trans *t = NULL; + + // Check filename is a socket + if (g_socket_exist(filename)) + { + // Try to connect to the session + if ((t = ercp_connect(filename, sesman_is_term)) != NULL) + { + int sesexec_pid; + int sesexec_uid; + int sesexec_gid; + + // Find the credentials of the sesexec process on the other end + if (g_sck_get_peer_cred(t->sck, &sesexec_pid, + &sesexec_uid, &sesexec_gid) == 0) + { + // Don't talk to unprivileged processes. It's a big concern + // if we find one. + if (sesexec_uid != 0 || sesexec_gid != 0) + { + LOG(LOG_LEVEL_ALWAYS, + "Unexpected sesexec owner %d:%d" + " for PID %d listening on %s", + sesexec_uid, sesexec_gid, sesexec_pid, filename); + } + else + { + struct session_item *s_item; + if ((s_item = session_list_new()) != NULL) + { + // Finalise the session for I/O + t->trans_data_in = sesman_ercp_data_in; + t->callback_data = (void *)s_item; + + // Complete the session fields for the + // E_SESSION_STARTING state + s_item->sesexec_trans = t; + s_item->sesexec_pid = sesexec_pid; + s_item->display = -1; + + // Tell the caller we've added one + status = 1; + } + } + } + } + } + + // Clean up an unused transport + if (status == 0) + { + trans_delete(t); + } + + return 1; +} + +/******************************************************************************/ +static int +discover_sessions(const char *restart_dir) +{ + int rv = 0; + struct list *dirnames = g_readdir(restart_dir); + unsigned int start_time = g_get_elapsed_ms(); + unsigned int session_count; + unsigned int elapsed; + int robjs_count; + intptr_t robjs[1024]; + int timeout; + + if (dirnames == NULL) + { + LOG(LOG_LEVEL_ERROR, + "Can't read restart directory to discover sessions [%s]", + g_get_strerror()); + } + else + { + // Iterate over the restart directory, and add any sesexec + // processes we discover to the session list, in + // E_SESSION_STARTING state. + char filename[XRDP_SOCKETS_MAXPATH]; + int i; + + for (i = 0 ; i < dirnames->count; ++i) + { + g_snprintf(filename, sizeof(filename), "%s/%s", + restart_dir, (const char *)dirnames->items[i]); + + (void)add_sesexec_fd_to_session_list(filename); + } + } + + // Process session list messages until either all sessions have + // started (or failed), or we hit a timeout. + while (1) + { + session_count = session_list_get_count_by_state(E_SESSION_STARTING); + elapsed = (g_get_elapsed_ms() - start_time); + if (session_count == 0 || elapsed >= MAX_DISCOVERY_WAIT_TIME) + { + break; + } + + robjs_count = 0; + robjs[robjs_count++] = g_term_event; + (void)session_list_get_wait_objs(robjs, &robjs_count); + + timeout = MAX_DISCOVERY_WAIT_TIME - elapsed; // > 0 + if (g_obj_wait(robjs, robjs_count, NULL, 0, timeout) != 0) + { + /* should not get here */ + g_sleep(100); + continue; + } + + if (g_is_wait_obj_set(g_term_event)) /* term */ + { + LOG(LOG_LEVEL_INFO, "discover_sessions: sesman asked to terminate"); + rv = 1; + break; + } + + (void)session_list_check_wait_objs(); + } + + if (rv == 0) + { + LOG(LOG_LEVEL_INFO, + "Session discovery took %d secs and loaded %u sessions", + elapsed, session_list_get_count_by_state(E_SESSION_RUNNING)); + + if (session_count > 0) + { + LOG(LOG_LEVEL_WARNING, + "%u sessions have not responded at end of discovery", + session_count); + } + } + + return rv; +} + +/******************************************************************************/ +int +sesman_restart_discover_sessions(void) +{ + int rv = 1; + // The restart directory contains Unix Domain sockets, so can't + // exceed XRDP_SOCKETS_MAXPATH in length + char restart_dir[XRDP_SOCKETS_MAXPATH]; + + // sizeof(g_cfg->listen_port) is guaranteed to be smaller than + // XRDP_SOCKETS_MAXPATH + g_snprintf(restart_dir, sizeof(restart_dir), + "%s.r", g_cfg->listen_port); + switch (init_restart_directory(restart_dir)) + { + case E_RESTART_DIR_CREATED_OK: + // Nothing to discover + rv = 0; + break; + + case E_RESTART_DIR_ALREADY_EXISTS: + // Look for sessions from previous run + rv = discover_sessions(restart_dir); + break; + + default: + ; + } + + return rv; +} diff --git a/sesman/sesman_restart.h b/sesman/sesman_restart.h new file mode 100644 index 000000000..bbfa11b49 --- /dev/null +++ b/sesman/sesman_restart.h @@ -0,0 +1,40 @@ +/** + * xrdp: A Remote Desktop Protocol server. + * + * Copyright (C) Matt Burt 2024 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * + * @file sesman_restart.h + * @brief Sesman restart declarations + * @author Matt Burt + * + */ + + +#ifndef SESMAN_RESTART_H +#define SESMAN_RESTART_H + +/** + * Discover sessions from a previous sesman run + * @return 0 for success + * + * Errors are logged + */ +int +sesman_restart_discover_sessions(void); + +#endif // SESMAN_RESTART_H diff --git a/sesman/session_list.c b/sesman/session_list.c index b2c0ea59d..090d441eb 100644 --- a/sesman/session_list.c +++ b/sesman/session_list.c @@ -122,6 +122,24 @@ session_list_get_count(void) return g_session_list->count; } +/******************************************************************************/ +unsigned int +session_list_get_count_by_state(enum session_state state) +{ + unsigned int result = 0; + int i; + for (i = 0 ; i < g_session_list->count ; ++i) + { + struct session_item *si; + si = (struct session_item *)list_get_item(g_session_list, i); + if (si->state == state) + { + ++result; + } + } + return result; +} + /******************************************************************************/ struct session_item * session_list_new(void) diff --git a/sesman/session_list.h b/sesman/session_list.h index b018afad9..d33c56c65 100644 --- a/sesman/session_list.h +++ b/sesman/session_list.h @@ -90,6 +90,14 @@ session_list_cleanup(void); unsigned int session_list_get_count(void); +/** + * @brief Get the number of sessions in a particular state + * @param state to count + * @return session count + */ +unsigned int +session_list_get_count_by_state(enum session_state state); + /** * Allocates a new session on the list * diff --git a/sesman/sig.c b/sesman/sig.c index 8cdd508a6..c7151d8e2 100644 --- a/sesman/sig.c +++ b/sesman/sig.c @@ -58,6 +58,8 @@ sig_sesman_reload_cfg(void) { LOG(LOG_LEVEL_INFO, "sesman listen port changed to %s", cfg->listen_port); + LOG(LOG_LEVEL_WARNING, + "Restarting sesman will now lose active sessions"); /* We have to delete the old port before listening to the new one * in case they overlap in scope */ diff --git a/tests/common/test_os_calls.c b/tests/common/test_os_calls.c index c2a3e09e8..b1c6b8f87 100644 --- a/tests/common/test_os_calls.c +++ b/tests/common/test_os_calls.c @@ -7,6 +7,7 @@ #include #include #include +#include #include "os_calls.h" #include "list.h" @@ -20,6 +21,9 @@ // File for testing ro/rw opens #define RO_RW_FILE "./test_ro_rw" +// Directory for testing files +#define TEST_READDIR "./test_readdir" + /******************************************************************************/ /*** * Gets the number of open file descriptors for the current process */ @@ -487,6 +491,74 @@ START_TEST(test_g_sck_fd_overflow) } END_TEST +/******************************************************************************/ +static int qsort_func_strlist(const void *a, const void *b) +{ + return strcmp(*(const char **)a, *(const char **)b); +} + +START_TEST(test_g_readdir) +{ + int status; + const char *entries[] = { "one", "two", "three", "four", "five", 0}; + unsigned int count = 0; + const char **name; + + // Don't check results of create dir, as we may be re-running this + // after a fail + (void)g_mkdir(TEST_READDIR); + // This should work though + status = g_set_current_dir(TEST_READDIR); + ck_assert_int_eq(status, 0); + + // Create some files in the directory + for (name = entries; *name != NULL; ++name, ++count) + { + int fd = g_file_open_rw(*name); + ck_assert(fd >= 0); + g_file_close(fd); + } + + // Can we read them, and are there the expected number? + struct list *dentries = g_readdir("."); + ck_assert_ptr_ne(dentries, NULL); + ck_assert_ptr_ne(dentries, NULL); + ck_assert_int_eq(dentries->count, count); + + // Sort both lists according to the current locale + qsort(entries, count, sizeof(entries[0]), qsort_func_strlist); + qsort(dentries->items, count, sizeof(dentries->items[0]), + qsort_func_strlist); + + // Check both lists are identical + int i; + for (i = 0 ; i < count; ++i) + { + ck_assert_str_eq(entries[i], (const char *)dentries->items[i]); + } + + // Clean up + list_delete(dentries); + for (i = 0 ; i < count ; ++i) + { + g_file_delete(entries[i]); + } + g_set_current_dir(".."); + status = g_remove_dir(TEST_READDIR); // Returns a boolean(?) + ck_assert_int_ne(status, 0); + + +} +END_TEST + +START_TEST(test_g_readdir_not_dir) +{ + struct list *dentries = g_readdir("NoSuchDirectory"); + ck_assert_ptr_eq(dentries, 0); + ck_assert_int_eq(errno, ENOENT); +} +END_TEST + /******************************************************************************/ Suite * make_suite_test_os_calls(void) @@ -512,6 +584,8 @@ make_suite_test_os_calls(void) tcase_add_test(tc_os_calls, test_g_file_is_open); tcase_add_test(tc_os_calls, test_g_sck_fd_passing); tcase_add_test(tc_os_calls, test_g_sck_fd_overflow); + tcase_add_test(tc_os_calls, test_g_readdir); + tcase_add_test(tc_os_calls, test_g_readdir_not_dir); // Add other test cases in other files suite_add_tcase(s, make_tcase_test_os_calls_signals());