mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 02:51:10 +00:00
* Run programs, scripts and AppImages from the desktop Windows programs open with the Proton Steam has chosen, and a Steam game's own launch program is handed to Steam. x86-64 Linux programs and game scripts run under FEX, and an exec the kernel refuses goes to droiddeck-open through libblsession's binfmt.c. Programs on noexec shared storage pass KIO's checks; a folder of Linux programs there, and sharun AppImages, are added by the app once and run from the copy. Executable scripts run without KIO's prompt, mpv plays on the software desktop, FEXServer's socket moves under XDG_RUNTIME_DIR, and Proton runtimes no longer show up as games. * Switch to Desktop from Big Picture without Deck mode Big Picture's Switch to Desktop asks the client's SteamOS manager, which the client only has when started as SteamOS (Deck mode). Without it the request failed and the screen stayed on "Switching to Desktop..." for good. The live compat helper now sees the request on its way to the client and hands it to steamos-session-select, which opens the KDE desktop as Deck mode already did. * Run x86 programs from shared storage where they lie FEX asks for the guest's PROT_EXEC when it maps a program or library, which the kernel refuses on noexec shared storage; FEX never runs those pages itself, so libblsession maps them without it in FEX's processes. An x86 game, its libraries and its start script now run in place instead of being copied in; only native code on noexec storage is still copied. A Steam library's games are never copied, and a Linux game's launch program opened from the desktop goes to Steam like a Windows one.
120 lines
5.6 KiB
Python
120 lines
5.6 KiB
Python
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import socket
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
|
|
SOURCE = Path(__file__).resolve().parents[1] / "linuxfs/preload/net.c"
|
|
|
|
|
|
@unittest.skipUnless(sys.platform.startswith("linux") and shutil.which("cc"), "needs Linux and a C compiler")
|
|
class CdpGuardTest(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.build = tempfile.TemporaryDirectory()
|
|
cls.library = Path(cls.build.name) / "net.so"
|
|
subprocess.run(["cc", "-shared", "-fPIC", "-O2", str(SOURCE), str(SOURCE.parent / "binfmt.c"), "-x", "c", "-", "-ldl",
|
|
"-o", str(cls.library)],
|
|
input=b"int bl_udevmon_stand_in(int fd) { return 0; }", check=True, capture_output=True)
|
|
|
|
@classmethod
|
|
def tearDownClass(cls):
|
|
cls.build.cleanup()
|
|
|
|
def setUp(self):
|
|
self.temp = tempfile.TemporaryDirectory()
|
|
self.addCleanup(self.temp.cleanup)
|
|
self.registry = Path(self.temp.name) / "net"
|
|
self.registry.mkdir()
|
|
|
|
def port(self, ipv6=False):
|
|
with socket.socket(socket.AF_INET6 if ipv6 else socket.AF_INET) as s:
|
|
s.bind(("::1" if ipv6 else "127.0.0.1", 0))
|
|
return s.getsockname()[1]
|
|
|
|
def env(self, port):
|
|
return dict(os.environ, LD_PRELOAD=str(self.library), BL_CDP_GUARD=str(port), BL_SYSV_DIR=self.temp.name)
|
|
|
|
def test_guarded_ipv4_bind_is_loopback_only(self):
|
|
port = self.port()
|
|
program = "import socket; s=socket.socket(); s.bind(('0.0.0.0', %d)); print(s.getsockname()[0])" % port
|
|
result = subprocess.run([sys.executable, "-c", program], env=self.env(port), check=True, capture_output=True, text=True)
|
|
self.assertEqual("127.0.0.1", result.stdout.strip())
|
|
|
|
def test_guarded_ipv6_bind_is_loopback_only(self):
|
|
if not socket.has_ipv6:
|
|
self.skipTest("needs IPv6")
|
|
port = self.port(True)
|
|
program = "import socket; s=socket.socket(socket.AF_INET6); s.bind(('::', %d)); print(s.getsockname()[0])" % port
|
|
result = subprocess.run([sys.executable, "-c", program], env=self.env(port), check=True, capture_output=True, text=True)
|
|
self.assertEqual("::1", result.stdout.strip())
|
|
|
|
def test_other_listeners_are_unchanged(self):
|
|
port = self.port()
|
|
program = "import socket; s=socket.socket(); s.bind(('0.0.0.0', %d)); print(s.getsockname()[0])" % port
|
|
result = subprocess.run([sys.executable, "-c", program], env=self.env(0), check=True, capture_output=True, text=True)
|
|
self.assertEqual("0.0.0.0", result.stdout.strip())
|
|
|
|
def connect(self, address, registered, stale=False, wrong_destination=False):
|
|
port = self.port()
|
|
program = "import socket; s=socket.socket(); s.bind(('0.0.0.0', %d)); s.listen(); print('ready', flush=True); c,_=s.accept(); c.sendall(b'accepted')" % port
|
|
server = subprocess.Popen([sys.executable, "-u", "-c", program], env=self.env(port), stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
try:
|
|
self.assertEqual(b"ready\n", server.stdout.readline())
|
|
with socket.socket() as client:
|
|
client.settimeout(3)
|
|
client.bind((address, 0))
|
|
if registered:
|
|
peer = client.getsockname()[1]
|
|
inode = os.fstat(client.fileno()).st_ino + (1 if stale else 0)
|
|
destination = port + (1 if wrong_destination else 0)
|
|
(self.registry / ("p%d" % peer)).write_text("%d 1 %d %d %d %d %d\n" %
|
|
(os.getpid(), os.getuid(), destination, client.fileno(), inode, socket.AF_INET))
|
|
client.connect(("127.0.0.1", port))
|
|
try:
|
|
return client.recv(8)
|
|
except ConnectionResetError:
|
|
return b""
|
|
finally:
|
|
server.kill()
|
|
server.wait()
|
|
server.stdout.close()
|
|
server.stderr.close()
|
|
|
|
def test_registered_guest_loopback_peer_is_accepted(self):
|
|
self.assertEqual(b"accepted", self.connect("127.0.0.1", True))
|
|
|
|
def test_unregistered_local_peer_is_refused(self):
|
|
self.assertEqual(b"", self.connect("127.0.0.1", False))
|
|
|
|
def test_matching_port_record_cannot_authorize_another_address(self):
|
|
self.assertEqual(b"", self.connect("127.0.0.2", True))
|
|
|
|
def test_stale_socket_record_does_not_authorize_a_reused_port(self):
|
|
self.assertEqual(b"", self.connect("127.0.0.1", True, stale=True))
|
|
|
|
def test_record_for_another_destination_does_not_authorize_cdp(self):
|
|
self.assertEqual(b"", self.connect("127.0.0.1", True, wrong_destination=True))
|
|
|
|
def test_preloaded_guest_connect_still_works(self):
|
|
port = self.port()
|
|
program = "import socket; s=socket.socket(); s.bind(('0.0.0.0', %d)); s.listen(); print('ready', flush=True); c,_=s.accept(); c.sendall(b'accepted')" % port
|
|
server = subprocess.Popen([sys.executable, "-u", "-c", program], env=self.env(port), stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
try:
|
|
self.assertEqual(b"ready\n", server.stdout.readline())
|
|
client = "import socket; s=socket.create_connection(('127.0.0.1', %d), timeout=3); print(s.recv(8).decode())" % port
|
|
result = subprocess.run([sys.executable, "-c", client], env=self.env(port), check=True, capture_output=True, text=True, timeout=5)
|
|
self.assertEqual("accepted", result.stdout.strip())
|
|
finally:
|
|
server.kill()
|
|
server.wait()
|
|
server.stdout.close()
|
|
server.stderr.close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|