qrexec: add configurable waiting for session before starting service
Some services require GUI access. Make qrexec-agent handling this, based on per-service configuration, instead of forcing every caller to call qubes.WaitForSession service first. This is especially important for Disposable VMs, because those are destroyed after a single service call. This needs to be done in qrexec-agent (instead of service script, or qubes-rpc-multiplexer), because agent will behave differently depending on GUI session being available or not. Namely, will use qrexec-fork-server (so the process will be a child of session leader), or will open new session. Service configuration lives in /etc/qubes/rpc-config/SERVICE_NAME, can can contain 'key=value' entries (no space around '=' allowed). Currently the only settings supported is 'wait-for-session', with value either '0' or '1'. QubesOS/qubes-issues#2974
This commit is contained in:
		
							parent
							
								
									2a0c670a53
								
							
						
					
					
						commit
						c8140375fa
					
				| @ -52,17 +52,32 @@ struct _connection_info { | |||||||
|     int connect_port; |     int connect_port; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | /* structure describing a single request waiting for qubes.WaitForSession to
 | ||||||
|  |  * finish */ | ||||||
|  | struct _waiting_request { | ||||||
|  |     int type; | ||||||
|  |     int connect_domain; | ||||||
|  |     int connect_port; | ||||||
|  |     char *cmdline; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
| int max_process_fd = -1; | int max_process_fd = -1; | ||||||
| 
 | 
 | ||||||
| /*  */ | /*  */ | ||||||
| struct _connection_info connection_info[MAX_FDS]; | struct _connection_info connection_info[MAX_FDS]; | ||||||
| 
 | 
 | ||||||
|  | struct _waiting_request requests_waiting_for_session[MAX_FDS]; | ||||||
|  | 
 | ||||||
| libvchan_t *ctrl_vchan; | libvchan_t *ctrl_vchan; | ||||||
| 
 | 
 | ||||||
|  | pid_t wait_for_session_pid = -1; | ||||||
|  | 
 | ||||||
| int trigger_fd; | int trigger_fd; | ||||||
| 
 | 
 | ||||||
| int meminfo_write_started = 0; | int meminfo_write_started = 0; | ||||||
| 
 | 
 | ||||||
|  | void handle_server_exec_request_do(int type, int connect_domain, int connect_port, char *cmdline); | ||||||
|  | 
 | ||||||
| void no_colon_in_cmd() | void no_colon_in_cmd() | ||||||
| { | { | ||||||
|     fprintf(stderr, |     fprintf(stderr, | ||||||
| @ -382,14 +397,187 @@ void register_vchan_connection(pid_t pid, int fd, int domain, int port) | |||||||
|     fprintf(stderr, "No free slot for child %d (connection to %d:%d)\n", pid, domain, port); |     fprintf(stderr, "No free slot for child %d (connection to %d:%d)\n", pid, domain, port); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /* Load service configuration from /etc/qubes/rpc-config/
 | ||||||
|  |  * (QUBES_RPC_CONFIG_DIR), currently only wait-for-session option supported. | ||||||
|  |  * | ||||||
|  |  * Return: | ||||||
|  |  *  1  - config successfuly loaded | ||||||
|  |  *  0  - config not found | ||||||
|  |  *  -1 - other error | ||||||
|  |  */ | ||||||
|  | int load_service_config(const char *service_name, int *wait_for_session) { | ||||||
|  |     char filename[256]; | ||||||
|  |     char config[MAX_CONFIG_SIZE]; | ||||||
|  |     char *config_iter = config; | ||||||
|  |     FILE *config_file; | ||||||
|  |     size_t read_count; | ||||||
|  |     char *current_line; | ||||||
|  | 
 | ||||||
|  |     if (snprintf(filename, sizeof(filename), "%s/%s", | ||||||
|  |                 QUBES_RPC_CONFIG_DIR, service_name) >= (int)sizeof(filename)) { | ||||||
|  |         /* buffer too small?! */ | ||||||
|  |         return -1; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     config_file = fopen(filename, "r"); | ||||||
|  |     if (!config_file) { | ||||||
|  |         if (errno == ENOENT) | ||||||
|  |             return 0; | ||||||
|  |         else { | ||||||
|  |             fprintf(stderr, "Failed to load %s\n", filename); | ||||||
|  |             return -1; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     read_count = fread(config, 1, sizeof(config)-1, config_file); | ||||||
|  | 
 | ||||||
|  |     if (ferror(config_file)) { | ||||||
|  |         fclose(config_file); | ||||||
|  |         return -1; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // config is a text file, should not have \0 inside; but when it has, part
 | ||||||
|  |     // after it will be ignored
 | ||||||
|  |     config[read_count] = 0; | ||||||
|  | 
 | ||||||
|  |     while ((current_line = strsep(&config_iter, "\n"))) { | ||||||
|  |         // ignore comments
 | ||||||
|  |         if (current_line[0] == '#') | ||||||
|  |             continue; | ||||||
|  |         sscanf(current_line, "wait-for-session=%d", wait_for_session); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fclose(config_file); | ||||||
|  |     return 1; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /* Check if requested command/service require GUI session and if so, initiate
 | ||||||
|  |  * waiting process. | ||||||
|  |  * | ||||||
|  |  * Return: | ||||||
|  |  *  - 1 - waiting is needed, caller should register request to be proceeded | ||||||
|  |  *  only after session is started) | ||||||
|  |  *  - 0 - waiting is not needed, caller may proceed with request immediately | ||||||
|  |  */ | ||||||
|  | int wait_for_session_maybe(char *cmdline) { | ||||||
|  |     char *realcmd = index(cmdline, ':'); | ||||||
|  |     char *user, *service_name, *source_domain, *service_argument; | ||||||
|  |     int stdin_pipe[2]; | ||||||
|  |     int wait_for_session = 0; | ||||||
|  | 
 | ||||||
|  |     if (!realcmd) | ||||||
|  |         /* no colon in command line, this will be properly reported later */ | ||||||
|  |         return 0; | ||||||
|  | 
 | ||||||
|  |     /* "nogui:" prefix have priority - do not wait for session */ | ||||||
|  |     if (strncmp(realcmd, NOGUI_CMD_PREFIX, NOGUI_CMD_PREFIX_LEN) == 0) | ||||||
|  |         return 0; | ||||||
|  | 
 | ||||||
|  |     /* extract username */ | ||||||
|  |     user = strndup(cmdline, realcmd - cmdline); | ||||||
|  |     realcmd++; | ||||||
|  | 
 | ||||||
|  |     /* wait for session only for service requests */ | ||||||
|  |     if (strncmp(realcmd, RPC_REQUEST_COMMAND " ", RPC_REQUEST_COMMAND_LEN+1) != 0) { | ||||||
|  |         free(user); | ||||||
|  |         return 0; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     realcmd += RPC_REQUEST_COMMAND_LEN+1; | ||||||
|  |     /* now realcmd contains service name (possibly with argument after '+'
 | ||||||
|  |      * char) and source domain name, after space */ | ||||||
|  |     source_domain = index(realcmd, ' '); | ||||||
|  |     if (!source_domain) { | ||||||
|  |         /* qrexec-rpc-multiplexer will properly report this */ | ||||||
|  |         free(user); | ||||||
|  |         return 0; | ||||||
|  |     } | ||||||
|  |     service_name = strndup(realcmd, source_domain - realcmd); | ||||||
|  |     source_domain++; | ||||||
|  | 
 | ||||||
|  |     /* first try to load config for specific argument */ | ||||||
|  |     switch (load_service_config(service_name, &wait_for_session)) { | ||||||
|  |         case 0: | ||||||
|  |             /* no config for specific argument, try for bare service name */ | ||||||
|  |             service_argument = index(service_name, '+'); | ||||||
|  |             if (!service_argument) { | ||||||
|  |                 /* there was no argument, so no config at all - do not wait for
 | ||||||
|  |                  * session */ | ||||||
|  |                 free(user); | ||||||
|  |                 return 0; | ||||||
|  |             } | ||||||
|  |             /* cut off the argument */ | ||||||
|  |             *service_argument = '\0'; | ||||||
|  | 
 | ||||||
|  |             if (load_service_config(service_name, &wait_for_session) != 1) { | ||||||
|  |                 /* no config, or load error -> no wait for session */ | ||||||
|  |                 free(user); | ||||||
|  |                 return 0; | ||||||
|  |             } | ||||||
|  |             break; | ||||||
|  | 
 | ||||||
|  |         case 1: | ||||||
|  |             /* config loaded */ | ||||||
|  |             break; | ||||||
|  | 
 | ||||||
|  |         case -1: | ||||||
|  |             /* load error -> no wait for session */ | ||||||
|  |             free(user); | ||||||
|  |             return 0; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (!wait_for_session) { | ||||||
|  |         /* setting not set, or set to 0 */ | ||||||
|  |         free(user); | ||||||
|  |         return 0; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /* ok, now we know that service is configured to wait for session */ | ||||||
|  | 
 | ||||||
|  |     if (wait_for_session_pid != -1) { | ||||||
|  |         /* we're already waiting */ | ||||||
|  |         free(user); | ||||||
|  |         return 1; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (pipe(stdin_pipe) == -1) { | ||||||
|  |         perror("pipe for wait-for-session"); | ||||||
|  |         free(user); | ||||||
|  |         return 0; | ||||||
|  |     } | ||||||
|  |     /* start waiting process */ | ||||||
|  |     wait_for_session_pid = fork(); | ||||||
|  |     switch (wait_for_session_pid) { | ||||||
|  |         case 0: | ||||||
|  |             close(stdin_pipe[1]); | ||||||
|  |             dup2(stdin_pipe[0], 0); | ||||||
|  |             execl("/etc/qubes-rpc/qubes.WaitForSession", "qubes.WaitForSession", | ||||||
|  |                     source_domain, (char*)NULL); | ||||||
|  |             exit(1); | ||||||
|  |         case -1: | ||||||
|  |             perror("fork wait-for-session"); | ||||||
|  |             free(user); | ||||||
|  |             return 0; | ||||||
|  |         default: | ||||||
|  |             close(stdin_pipe[0]); | ||||||
|  |             if (write(stdin_pipe[1], user, strlen(user)) == -1) | ||||||
|  |                 perror("write error"); | ||||||
|  |             if (write(stdin_pipe[1], "\n", 1) == -1) | ||||||
|  |                 perror("write error"); | ||||||
|  |             close(stdin_pipe[1]); | ||||||
|  |     } | ||||||
|  |     free(user); | ||||||
|  |     /* qubes.WaitForSession started, postpone request until it report back */ | ||||||
|  |     return 1; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| /* hdr parameter is received from dom0, so it is trusted */ | /* hdr parameter is received from dom0, so it is trusted */ | ||||||
| void handle_server_exec_request(struct msg_header *hdr) | void handle_server_exec_request_init(struct msg_header *hdr) | ||||||
| { | { | ||||||
|     struct exec_params params; |     struct exec_params params; | ||||||
|     int buf_len = hdr->len-sizeof(params); |     int buf_len = hdr->len-sizeof(params); | ||||||
|     char buf[buf_len]; |     char buf[buf_len]; | ||||||
|     pid_t child_agent; |  | ||||||
|     int client_fd; |  | ||||||
| 
 | 
 | ||||||
|     assert(hdr->len >= sizeof(params)); |     assert(hdr->len >= sizeof(params)); | ||||||
| 
 | 
 | ||||||
| @ -398,18 +586,54 @@ void handle_server_exec_request(struct msg_header *hdr) | |||||||
|     if (libvchan_recv(ctrl_vchan, buf, buf_len) < 0) |     if (libvchan_recv(ctrl_vchan, buf, buf_len) < 0) | ||||||
|         handle_vchan_error("read exec cmd"); |         handle_vchan_error("read exec cmd"); | ||||||
| 
 | 
 | ||||||
|     if ((hdr->type == MSG_EXEC_CMDLINE || hdr->type == MSG_JUST_EXEC) && |     buf[buf_len-1] = 0; | ||||||
|             !strstr(buf, ":nogui:")) { | 
 | ||||||
|         int child_socket = try_fork_server(hdr->type, |     if (hdr->type != MSG_SERVICE_CONNECT && wait_for_session_maybe(buf)) { | ||||||
|  |         /* waiting for session, postpone actual call */ | ||||||
|  |         int slot_index = 0; | ||||||
|  |         while (slot_index < MAX_FDS) | ||||||
|  |             if (!requests_waiting_for_session[slot_index].cmdline) | ||||||
|  |                 break; | ||||||
|  |         if (slot_index == MAX_FDS) { | ||||||
|  |             /* no free slots */ | ||||||
|  |             fprintf(stderr, "No free slots for waiting for GUI session, continuing!\n"); | ||||||
|  |         } else { | ||||||
|  |             requests_waiting_for_session[slot_index].type = hdr->type; | ||||||
|  |             requests_waiting_for_session[slot_index].connect_domain = params.connect_domain; | ||||||
|  |             requests_waiting_for_session[slot_index].connect_port = params.connect_port; | ||||||
|  |             requests_waiting_for_session[slot_index].cmdline = strdup(buf); | ||||||
|  |             /* nothing to do now, when we get GUI session, we'll continue */ | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     handle_server_exec_request_do(hdr->type, params.connect_domain, params.connect_port, buf); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void handle_server_exec_request_do(int type, int connect_domain, int connect_port, char *cmdline) { | ||||||
|  |     int client_fd; | ||||||
|  |     pid_t child_agent; | ||||||
|  |     int cmdline_len = strlen(cmdline) + 1; // size of cmdline, including \0 at the end
 | ||||||
|  |     struct exec_params params = { | ||||||
|  |         .connect_domain = connect_domain, | ||||||
|  |         .connect_port = connect_port, | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     if ((type == MSG_EXEC_CMDLINE || type == MSG_JUST_EXEC) && | ||||||
|  |             !strstr(cmdline, ":nogui:")) { | ||||||
|  |         int child_socket; | ||||||
|  | 
 | ||||||
|  |         child_socket = try_fork_server(type, | ||||||
|                 params.connect_domain, params.connect_port, |                 params.connect_domain, params.connect_port, | ||||||
|                 buf, buf_len); |                 cmdline, cmdline_len); | ||||||
|         if (child_socket >= 0) { |         if (child_socket >= 0) { | ||||||
|             register_vchan_connection(-1, child_socket, |             register_vchan_connection(-1, child_socket, | ||||||
|                     params.connect_domain, params.connect_port); |                     params.connect_domain, params.connect_port); | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|     if (hdr->type == MSG_SERVICE_CONNECT && sscanf(buf, "SOCKET%d", &client_fd)) { | 
 | ||||||
|  |     if (type == MSG_SERVICE_CONNECT && sscanf(cmdline, "SOCKET%d", &client_fd)) { | ||||||
|         /* FIXME: Maybe add some check if client_fd is really FD to some
 |         /* FIXME: Maybe add some check if client_fd is really FD to some
 | ||||||
|          * qrexec-client-vm process; but this data comes from qrexec-daemon |          * qrexec-client-vm process; but this data comes from qrexec-daemon | ||||||
|          * (which sends back what it got from us earlier), so it isn't critical. |          * (which sends back what it got from us earlier), so it isn't critical. | ||||||
| @ -429,9 +653,9 @@ void handle_server_exec_request(struct msg_header *hdr) | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /* No fork server case */ |     /* No fork server case */ | ||||||
|     child_agent = handle_new_process(hdr->type, |     child_agent = handle_new_process(type, | ||||||
|             params.connect_domain, params.connect_port, |             params.connect_domain, params.connect_port, | ||||||
|             buf, buf_len); |             cmdline, cmdline_len); | ||||||
| 
 | 
 | ||||||
|     register_vchan_connection(child_agent, -1, |     register_vchan_connection(child_agent, -1, | ||||||
|             params.connect_domain, params.connect_port); |             params.connect_domain, params.connect_port); | ||||||
| @ -471,7 +695,7 @@ void handle_server_cmd() | |||||||
|         case MSG_JUST_EXEC: |         case MSG_JUST_EXEC: | ||||||
|         case MSG_SERVICE_CONNECT: |         case MSG_SERVICE_CONNECT: | ||||||
|             wake_meminfo_writer(); |             wake_meminfo_writer(); | ||||||
|             handle_server_exec_request(&s_hdr); |             handle_server_exec_request_init(&s_hdr); | ||||||
|             break; |             break; | ||||||
|         case MSG_SERVICE_REFUSED: |         case MSG_SERVICE_REFUSED: | ||||||
|             handle_service_refused(&s_hdr); |             handle_service_refused(&s_hdr); | ||||||
| @ -521,6 +745,21 @@ void reap_children() | |||||||
|     int pid; |     int pid; | ||||||
|     int id; |     int id; | ||||||
|     while ((pid = waitpid(-1, &status, WNOHANG)) > 0) { |     while ((pid = waitpid(-1, &status, WNOHANG)) > 0) { | ||||||
|  |         if (pid == wait_for_session_pid) { | ||||||
|  |             for (id = 0; id < MAX_FDS; id++) { | ||||||
|  |                 if (!requests_waiting_for_session[id].cmdline) | ||||||
|  |                     continue; | ||||||
|  |                 handle_server_exec_request_do( | ||||||
|  |                         requests_waiting_for_session[id].type, | ||||||
|  |                         requests_waiting_for_session[id].connect_domain, | ||||||
|  |                         requests_waiting_for_session[id].connect_port, | ||||||
|  |                         requests_waiting_for_session[id].cmdline); | ||||||
|  |                 free(requests_waiting_for_session[id].cmdline); | ||||||
|  |                 requests_waiting_for_session[id].cmdline = NULL; | ||||||
|  |             } | ||||||
|  |             wait_for_session_pid = -1; | ||||||
|  |             continue; | ||||||
|  |         } | ||||||
|         id = find_connection(pid); |         id = find_connection(pid); | ||||||
|         if (id < 0) |         if (id < 0) | ||||||
|             continue; |             continue; | ||||||
|  | |||||||
| @ -21,6 +21,11 @@ | |||||||
| 
 | 
 | ||||||
| #define QREXEC_FORK_SERVER_SOCKET "/var/run/qubes/qrexec-server.%s.sock" | #define QREXEC_FORK_SERVER_SOCKET "/var/run/qubes/qrexec-server.%s.sock" | ||||||
| 
 | 
 | ||||||
|  | // directory for services configuration (for example 'wait-for-session' flag)
 | ||||||
|  | #define QUBES_RPC_CONFIG_DIR "/etc/qubes/rpc-config" | ||||||
|  | // support only very small configuration files,
 | ||||||
|  | #define MAX_CONFIG_SIZE 4096 | ||||||
|  | 
 | ||||||
| int handle_handshake(libvchan_t *ctrl); | int handle_handshake(libvchan_t *ctrl); | ||||||
| void handle_vchan_error(const char *op); | void handle_vchan_error(const char *op); | ||||||
| void do_exec(const char *cmd); | void do_exec(const char *cmd); | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user
	 Marek Marczykowski-Górecki
						Marek Marczykowski-Górecki