Skip to content

Commit

Permalink
Ensure that EOF is propagated to stdout
Browse files Browse the repository at this point in the history
It is critical that EOF from the service gets propagated to
the stdout of qrexec-client-vm or qrexec-client.  Otherwise, the caller
will not recognize that EOF has happened and may wait forever for data
that will never arrive.  In QubesOS/qubes-issues#9169, this caused curl
to hang when reading a plaintext HTTP/1.0 response from tinyproxy, which
relies on EOF to delimit the response.

The bug will trigger iff all of the following conditions are met:

1. The remote side must close the writing side of the connection, either
   by closing or shutting down a socket.
2. If the remote service is executable, it must _not_ exit.
3. qrexec-client-vm or qrexec-client must _not_ get EOF on stdin.
4. Either the remote side does not close its stdin, or the local side
   does not continue to write data.
5. The same file description is used for both stdin and stdout of the
   local qrexec-client or qrexec-client-vm process.
6. qrexec-client or qrexec-client-vm connect the stdin and stdout of the
   remote process to their own stdin and stdout, not the stdin and
   stdout of a locally spawned command.

To fix this bug, add flags to qrexec-client and qrexec-client-vm that
cause the socket passed as file descriptor 0 to be used for both stdin
and stdout.

Fixes: QubesOS/qubes-issues#9169
  • Loading branch information
DemiMarie committed Apr 30, 2024
1 parent 8120940 commit c2f197c
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 28 deletions.
27 changes: 26 additions & 1 deletion agent/qrexec-client-vm.c
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
*/
#include <assert.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <sys/un.h>
Expand Down Expand Up @@ -94,6 +95,7 @@ static void convert_target_name_keyword(char *target)
enum {
opt_no_filter_stdout = 't'+128,
opt_no_filter_stderr = 'T'+128,
opt_use_stdin_socket = 'u'+128,
};

static struct option longopts[] = {
Expand All @@ -104,6 +106,7 @@ static struct option longopts[] = {
{ "no-filter-escape-chars-stderr", no_argument, 0, opt_no_filter_stderr},
{ "agent-socket", required_argument, 0, 'a'},
{ "prefix-data", required_argument, 0, 'p' },
{ "use-stdin-socket", no_argument, 0, opt_use_stdin_socket },
{ "help", no_argument, 0, 'h' },
{ NULL, 0, 0, 0},
};
Expand Down Expand Up @@ -141,6 +144,7 @@ int main(int argc, char **argv)
int inpipe[2], outpipe[2];
int buffer_size = 0;
int opt;
int stdout_fd = 1;
const char *agent_trigger_path = QREXEC_AGENT_TRIGGER_PATH, *prefix_data = NULL;

setup_logging("qrexec-client-vm");
Expand Down Expand Up @@ -198,6 +202,23 @@ int main(int argc, char **argv)
exit(1);
}
break;
case opt_use_stdin_socket:
{
int type;
if (stdout_fd == 0)
errx(2, "--use-stdin-socket passed twice");

Check warning on line 209 in agent/qrexec-client-vm.c

View check run for this annotation

Codecov / codecov/patch

agent/qrexec-client-vm.c#L209

Added line #L209 was not covered by tests
socklen_t len = sizeof(type);
if (getsockopt(0, SOL_SOCKET, SO_TYPE, &type, &len)) {
if (errno == ENOTSOCK)
errx(2, "--use-stdin-socket passed, but stdin not a socket");
err(2, "getsockopt(0, SOL_SOCKET, SO_TYPE)");

Check warning on line 214 in agent/qrexec-client-vm.c

View check run for this annotation

Codecov / codecov/patch

agent/qrexec-client-vm.c#L212-L214

Added lines #L212 - L214 were not covered by tests
}
assert(len == sizeof(type));
if (type != SOCK_STREAM)
errx(2, "stdin was a socket of type %d, not SOCK_STREAM (%d)", type, SOCK_STREAM);

Check warning on line 218 in agent/qrexec-client-vm.c

View check run for this annotation

Codecov / codecov/patch

agent/qrexec-client-vm.c#L218

Added line #L218 was not covered by tests
stdout_fd = 0;
}
break;
case '?':
usage(argv[0], 2);
}
Expand All @@ -209,6 +230,10 @@ int main(int argc, char **argv)
if (argc - optind > 2) {
start_local_process = 1;
}
if (start_local_process && stdout_fd != 1) {
fprintf(stderr, "cannot spawn a local process with --use-stdin-socket\n");
usage(argv[0], 2);

Check warning on line 235 in agent/qrexec-client-vm.c

View check run for this annotation

Codecov / codecov/patch

agent/qrexec-client-vm.c#L234-L235

Added lines #L234 - L235 were not covered by tests
}

if (!start_local_process) {
if (replace_chars_stdout == -1 && isatty(1))
Expand Down Expand Up @@ -303,7 +328,7 @@ int main(int argc, char **argv)
} else {
ret = handle_data_client(MSG_SERVICE_CONNECT,
exec_params.connect_domain, exec_params.connect_port,
1, 0, -1, buffer_size, 0, prefix_data);
stdout_fd, 0, -1, buffer_size, 0, prefix_data);
}

close(trigger_fd);
Expand Down
34 changes: 31 additions & 3 deletions daemon/qrexec-client.c
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,16 @@ static _Noreturn void do_exec(const char *prog, const char *username __attribute
PERROR("exec bash");
exit(1);
}
enum {
opt_socket_dir = 'd'+128,
opt_use_stdin_socket = 'u'+128,
};

static struct option longopts[] = {
{ "help", no_argument, 0, 'h' },
{ "socket-dir", required_argument, 0, 'd'+128 },
{ "socket-dir", required_argument, 0, opt_socket_dir },
{ "no-exit-code", no_argument, 0, 'E' },
{ "use-stdin-socket", no_argument, 0, opt_use_stdin_socket },
{ NULL, 0, 0, 0 },
};

Expand All @@ -96,7 +101,8 @@ _Noreturn static void usage(const char *const name)
" -c - connect to existing process (response to trigger service call)\n"
" -w timeout - override default connection timeout of 5s (set 0 for no timeout)\n"
" -k - kill the domain right before exiting\n"
" --socket-dir=PATH - directory for qrexec socket, default: %s\n",
" --socket-dir=PATH - directory for qrexec socket, default: %s\n"
" --use-stdin-socket - use fd 0 (which must be socket) for both stdin and stdout\n",
name ? name : "qrexec-client", QREXEC_DAEMON_SOCKET_DIR);
exit(1);
}
Expand Down Expand Up @@ -217,9 +223,26 @@ int main(int argc, char **argv)
case 'W':
wait_connection_end = 1;
break;
case 'd' + 128:
case opt_socket_dir:
socket_dir = strdup(optarg);
break;
case opt_use_stdin_socket:

Check warning on line 229 in daemon/qrexec-client.c

View check run for this annotation

Codecov / codecov/patch

daemon/qrexec-client.c#L229

Added line #L229 was not covered by tests
{
int type;
if (local_stdin_fd != 1)
errx(2, "--use-stdin-socket passed twice");
socklen_t len = sizeof(type);
if (getsockopt(0, SOL_SOCKET, SO_TYPE, &type, &len)) {
if (errno == ENOTSOCK)
errx(2, "--use-stdin-socket passed, but stdin not a socket");
err(2, "getsockopt(0, SOL_SOCKET, SO_TYPE)");

Check warning on line 238 in daemon/qrexec-client.c

View check run for this annotation

Codecov / codecov/patch

daemon/qrexec-client.c#L231-L238

Added lines #L231 - L238 were not covered by tests
}
assert(len == sizeof(type) && "wrong socket option length?");
if (type != SOCK_STREAM)
errx(2, "stdin was a socket of type %d, not SOCK_STREAM (%d)", type, SOCK_STREAM);
local_stdin_fd = 0;

Check warning on line 243 in daemon/qrexec-client.c

View check run for this annotation

Codecov / codecov/patch

daemon/qrexec-client.c#L240-L243

Added lines #L240 - L243 were not covered by tests
}
break;

Check warning on line 245 in daemon/qrexec-client.c

View check run for this annotation

Codecov / codecov/patch

daemon/qrexec-client.c#L245

Added line #L245 was not covered by tests
case 'k':
kill = true;
break;
Expand All @@ -241,6 +264,11 @@ int main(int argc, char **argv)
usage(argv[0]);
}

if ((local_cmdline != NULL) && (local_stdin_fd != 1)) {
fprintf(stderr, "ERROR: at most one of -l and --use-stdin-socket can be specified\n");
usage(argv[0]);

Check warning on line 269 in daemon/qrexec-client.c

View check run for this annotation

Codecov / codecov/patch

daemon/qrexec-client.c#L268-L269

Added lines #L268 - L269 were not covered by tests
}

if (strcmp(domname, "dom0") == 0 || strcmp(domname, "@adminvm") == 0) {
if (request_id == NULL) {
fprintf(stderr, "ERROR: when target domain is 'dom0', -c must be specified\n");
Expand Down
3 changes: 2 additions & 1 deletion daemon/qrexec-daemon-common.c
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,8 @@ bool send_service_connect(int s, const char *conn_ident,

#define QREXEC_DATA_MIN_VERSION QREXEC_PROTOCOL_V2

static int local_stdin_fd = 1, local_stdout_fd = 0;
static int local_stdout_fd = 0;
int local_stdin_fd = 1;
static pid_t local_pid = 0;

static volatile sig_atomic_t sigchld = 0;
Expand Down
2 changes: 2 additions & 0 deletions daemon/qrexec-daemon-common.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,5 @@ __attribute__((warn_unused_result))
int handle_agent_handshake(libvchan_t *vchan, bool remote_send_first);
__attribute__((warn_unused_result))
int prepare_local_fds(struct qrexec_parsed_command *command, struct buffer *stdin_buffer);
/* FD for stdout of remote process */
extern int local_stdin_fd;
70 changes: 47 additions & 23 deletions qrexec/tests/socket/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -1173,11 +1173,24 @@ class TestClientVm(unittest.TestCase):
target_domain = 43
target_port = 1024

def assertStdoutMessages(self, target, expected_stdout: bytes, ty=qrexec.MSG_DATA_STDOUT):
bytes_recvd, expected = 0, len(expected_stdout)
while bytes_recvd < expected:
msg_type, msg_body = target.recv_message()
self.assertEqual(msg_type, ty)
l = len(msg_body)
self.assertEqual(msg_body, expected_stdout[bytes_recvd:l])
bytes_recvd += l
self.assertEqual(bytes_recvd, expected)

def setUp(self):
self.tempdir = tempfile.mkdtemp()
self.addCleanup(shutil.rmtree, self.tempdir)

def start_client(self, args):
def start_client(self, args, stdio=subprocess.PIPE):
if stdio is not subprocess.PIPE:
self.assertIsInstance(stdio, socket.socket)
args.insert(0, "--use-stdin-socket")
env = os.environ.copy()
env["LD_LIBRARY_PATH"] = os.path.join(ROOT_PATH, "libqrexec")
env["VCHAN_DOMAIN"] = str(self.domain)
Expand All @@ -1193,8 +1206,8 @@ def start_client(self, args):
self.client = subprocess.Popen(
cmd,
env=env,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stdin=stdio,
stdout=stdio,
stderr=subprocess.PIPE,
pass_fds=(stderr_dup,),
)
Expand All @@ -1219,7 +1232,7 @@ def connect_target_client(self):
self.addCleanup(target_client.close)
return target_client

def run_service(self, *, local_program=None, options=None):
def run_service(self, *, local_program=None, options=None, stdio=subprocess.PIPE):
server = self.connect_server()

args = options or []
Expand All @@ -1228,7 +1241,7 @@ def run_service(self, *, local_program=None, options=None):
if local_program:
args += local_program

self.start_client(args)
self.start_client(args, stdio=stdio)
server.accept()

message_type, data = server.recv_message()
Expand Down Expand Up @@ -1257,6 +1270,30 @@ def test_run_client(self):
self.client.wait()
self.assertEqual(self.client.returncode, 42)

def test_run_client_eof(self):
remote, local = socket.socketpair()
target_client = self.run_service(stdio=remote)
remote.close()
initial_data = b"stdout data\n"
target_client.send_message(qrexec.MSG_DATA_STDOUT, initial_data)
# FIXME: data can be received in multiple messages
self.assertEqual(local.recv(len(initial_data)), initial_data)
initial_data = b"stdin data\n"
local.sendall(initial_data)
self.assertStdoutMessages(target_client, initial_data, qrexec.MSG_DATA_STDIN)
target_client.send_message(qrexec.MSG_DATA_STDOUT, b"")
# Check that EOF got propagated
self.assertEqual(local.recv(1), b"")
# Close local
local.close()
# Check that EOF got propagated on this side too
self.assertEqual(target_client.recv_message(), (qrexec.MSG_DATA_STDIN, b""))
target_client.send_message(
qrexec.MSG_DATA_EXIT_CODE, struct.pack("<L", 42)
)
self.client.wait()
self.assertEqual(self.client.returncode, 42)

def test_run_client_replace_chars(self):
target_client = self.run_service(options=["-t"])
target_client.send_message(
Expand Down Expand Up @@ -1301,10 +1338,7 @@ def test_run_client_with_local_proc(self):
target_client = self.run_service(local_program=["/bin/cat"])
target_client.send_message(qrexec.MSG_DATA_STDOUT, b"stdout data\n")
target_client.send_message(qrexec.MSG_DATA_STDOUT, b"")
self.assertEqual(
target_client.recv_message(),
(qrexec.MSG_DATA_STDIN, b"stdout data\n"),
)
self.assertStdoutMessages(target_client, b"stdout data\n", qrexec.MSG_DATA_STDIN)
self.assertEqual(
target_client.recv_message(), (qrexec.MSG_DATA_STDIN, b"")
)
Expand All @@ -1331,16 +1365,12 @@ def test_stdio_socket(self):
""",
]
)
self.assertEqual(
target.recv_message(), (qrexec.MSG_DATA_STDIN, b"hello world\n")
)
self.assertStdoutMessages(target, b"hello world\n", qrexec.MSG_DATA_STDIN)

target.send_message(qrexec.MSG_DATA_STDOUT, b"stdin\n")
target.send_message(qrexec.MSG_DATA_STDOUT, b"")

self.assertEqual(
target.recv_message(), (qrexec.MSG_DATA_STDIN, b"received: stdin\n")
)
self.assertStdoutMessages(target, b"received: stdin\n", qrexec.MSG_DATA_STDIN)
self.assertEqual(target.recv_message(), (qrexec.MSG_DATA_STDIN, b""))

target.send_message(qrexec.MSG_DATA_EXIT_CODE, struct.pack("<L", 42))
Expand Down Expand Up @@ -1412,10 +1442,7 @@ def test_run_client_vchan_disconnect(self):
target_client = self.run_service()
self.client.stdin.write(b"stdin data\n")
self.client.stdin.flush()
self.assertEqual(
target_client.recv_message(),
(qrexec.MSG_DATA_STDIN, b"stdin data\n"),
)
self.assertStdoutMessages(target_client, b"stdin data\n", qrexec.MSG_DATA_STDIN)
target_client.send_message(qrexec.MSG_DATA_STDOUT, b"stdout data\n")
target_client.send_message(qrexec.MSG_DATA_STDOUT, b"")
self.assertEqual(self.client.stdout.read(), b"stdout data\n")
Expand All @@ -1427,10 +1454,7 @@ def test_run_client_vchan_disconnect(self):
def test_run_client_with_local_proc_disconnect(self):
target_client = self.run_service(local_program=["/bin/cat"])
target_client.send_message(qrexec.MSG_DATA_STDOUT, b"stdout data\n")
self.assertEqual(
target_client.recv_message(),
(qrexec.MSG_DATA_STDIN, b"stdout data\n"),
)
self.assertStdoutMessages(target_client, b"stdout data\n", qrexec.MSG_DATA_STDIN)
target_client.close()
self.client.wait()
self.assertEqual(self.client.returncode, 255)

0 comments on commit c2f197c

Please sign in to comment.