Skip to content

Commit

Permalink
Add a test for qrexec-client race condition
Browse files Browse the repository at this point in the history
Check if the service exit code is correctly retrieved even if the
the service terminates at the exact moment the qrexec-client tries to
send some data.
Try to win the race by initially sending SIGSTOP to the qrexec-client
process, and sending SIGCONT only after preparing both local and remote
data streams. qrexec-client will handle local data stream first, at
which point remote socket is already closed.

Similar issue applies to qrexec-client-vm, but since the implementation
is shared, one test is enough.

QubesOS/qubes-issues#9618
  • Loading branch information
marmarek committed Dec 10, 2024
1 parent 66c1519 commit df25090
Showing 1 changed file with 61 additions and 0 deletions.
61 changes: 61 additions & 0 deletions qrexec/tests/socket/daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import time
import itertools
import socket
import signal

import psutil
import pytest
Expand Down Expand Up @@ -664,6 +665,7 @@ def start_client(self, args):
self.client = subprocess.Popen(
cmd,
env=env,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
)
self.addCleanup(self.stop_client)
Expand Down Expand Up @@ -744,6 +746,65 @@ def test_run_vm_command_from_dom0(self):
self.client.wait()
self.assertEqual(self.client.returncode, 42)

def test_run_vm_command_from_dom0_reject_stdin(self):
"""Test if qrexec-client properly returns remote exit code even if
service didn't read all of stdin"""
cmd = "user:command"
target_domain_name = "target_domain"
target_domain_uuid = "d95e1147-2d82-4595-90bb-5a7500cc3196"
target_domain = 42
target_port = 513

target_daemon = self.connect_daemon(
target_domain, target_domain_name, target_domain_uuid
)
self.start_client(["-d", target_domain_name, cmd])
target_daemon.accept()
target_daemon.handshake()

# negotiate_connection_params
self.assertEqual(
target_daemon.recv_message(),
(
qrexec.MSG_EXEC_CMDLINE,
struct.pack("<LL", 0, 0) + cmd.encode() + b"\0",
),
)
target_daemon.send_message(
qrexec.MSG_EXEC_CMDLINE,
struct.pack("<LL", target_domain, target_port),
)

target = self.connect_target(target_domain, target_port)
target.handshake()

self.client.send_signal(signal.SIGSTOP)

# select_loop
target.send_message(qrexec.MSG_DATA_STDOUT, b"stdout data\n")
target.send_message(qrexec.MSG_DATA_STDOUT, b"")
target.send_message(qrexec.MSG_DATA_EXIT_CODE, struct.pack("<L", 42))
target.close()

# ...but still send some data
self.client.stdin.write(b"A" * 4096)
self.client.stdin.write(b"A" * 4096)
self.client.stdin.write(b"A" * 4096)
self.client.stdin.write(b"A" * 4096)
self.client.stdin.write(b"A" * 4096)
self.client.stdin.write(b"A" * 4096)
self.client.stdin.write(b"A" * 4096)
self.client.stdin.write(b"A" * 4096)
self.client.stdin.write(b"A" * 4096)
self.client.stdin.flush()

# and only now let the client process both data streams
self.client.send_signal(signal.SIGCONT)

self.assertEqual(self.client.stdout.read(), b"stdout data\n")
self.client.wait()
self.assertEqual(self.client.returncode, 42)

def test_run_vm_command_from_dom0_with_local_command(self):
cmd = "user:command"
local_cmd = "while read x; do echo input: $x; done; exit 44"
Expand Down

0 comments on commit df25090

Please sign in to comment.