1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 19:26:48 +00:00

fix(console): a serial console worker holds only its own terminal

pty.fork returns an inheritable master descriptor, so each new console
worker, and the ssh it may start, held the terminals of every other open
session until it ended. A worker now closes every descriptor above its own
terminal before it starts.
This commit is contained in:
catborise 2026-10-26 18:50:26 +03:00
parent cbd6317e55
commit 504d86faa3
2 changed files with 28 additions and 0 deletions

View file

@ -52,6 +52,9 @@ def connect(sid, environ, auth):
pid, fd = pty.fork()
if pid == 0:
try:
# pty.fork's master is inheritable: without this each worker would
# hold the other sessions' consoles open
os.closerange(3, os.sysconf("SC_OPEN_MAX"))
os.execv(sys.executable, [sys.executable, CONSOLECALLBACK, str(instance.pk)])
finally:
os._exit(1)

View file

@ -1,6 +1,7 @@
"""socketiod: who may connect, and each session's console kept to itself."""
import os
import tempfile
import time
from importlib.machinery import SourceFileLoader
from importlib.util import module_from_spec, spec_from_loader
@ -129,6 +130,30 @@ class SocketiodTestCase(Base):
self.d.os.kill.assert_called_once_with(pid, self.d.signal.SIGKILL)
class WorkerIsolationTestCase(Base):
"""A real fork: a session's worker holds no other session's console."""
@override_settings(SERIAL_CONSOLE_ENABLED=True)
def test_a_worker_has_only_its_own_terminal(self):
d = load_socketiod()
other = Instance.objects.create(compute=self.compute, name="vm-b", uuid="11111111-0000-0000-0000-000000000002")
UserInstance.objects.create(user=self.user, instance=other)
with tempfile.NamedTemporaryFile("w", suffix=".py", delete=False) as script:
# stands in for consolecallback: prints the descriptors it holds
script.write("import os, time\nprint(sorted(os.listdir('/proc/self/fd')))\ntime.sleep(5)\n")
self.addCleanup(os.unlink, script.name)
with patch.object(d, "CONSOLECALLBACK", script.name), patch.object(d.sio, "start_background_task"), \
patch.object(d.db, "close_old_connections"):
d.connect("first", {}, {"token": self.token()})
d.connect("second", {}, {"token": self.token(vm=other)})
self.addCleanup(lambda: [d.stop(sid) for sid in list(d.sessions)])
output = b""
while b"]" not in output:
output += os.read(d.sessions["second"]["fd"], 1024)
# 0-2: its terminal, 3: the listing itself
self.assertEqual(output.decode().strip(), "['0', '1', '2', '3']")
def _open(fd):
try:
os.fstat(fd)