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:
Marek Marczykowski-Górecki 2017-08-05 21:09:01 +02:00
parent 2a0c670a53
commit c8140375fa
No known key found for this signature in database
GPG Key ID: 063938BA42CFA724
2 changed files with 255 additions and 11 deletions

View File

@ -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;

View File

@ -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);