Add an opt-in filter for host desktop-session variables

RCLOL_FILTER_HOST_ENV=1 makes exec-guest.py drop host session metadata
(WAYLAND_DISPLAY, XDG_SESSION_*, XDG_CURRENT_DESKTOP, GNOME/KDE/GDM,
systemd, SSH agent, terminal, toolkit, Flatpak/Snap/AppImage, exported
shell functions) and set SHELL to the guest's /bin/zsh. It is off by
default; RCLOL_KEEP_HOST_ENV lists names to keep anyway.

Unlike the denylist in #61, the filter runs after the launcher sets its
GPU tuning, keeps everything Darling and the host graphics and audio
libraries read (DISPLAY, XAUTHORITY, XDG_RUNTIME_DIR and the XDG base
directories, D-Bus, PulseAudio/PipeWire/ALSA, DRI_PRIME, MESA_*, __GL_*,
__NV_*, VK_*, LD_PRELOAD for gamemoderun, LD_LIBRARY_PATH), and lives in
one place instead of three.

tests/test_exec_guest.py runs in CI; the launcher check also verifies
that MESA_NO_ERROR=1 and __GL_THREADED_OPTIMIZATIONS=0 reach the guest
with the filter on.

Split from #61.

Co-authored-by: DevWebeloper <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
committed 2026-10-18 17:13:24 +02:00
1 parent 6d3e870346
commit 41b5b6fc0f
5 files changed
+157 -3

No files matched your search

+42 -1
View File
@@ -1,8 +1,47 @@
#!/usr/bin/env python3
"""Run a command without the host file descriptors this launcher inherited (anything above stderr)."""
"""Run a command without the host file descriptors this launcher inherited (anything above stderr).
With RCLOL_FILTER_HOST_ENV=1 it also drops host desktop-session variables first (see
host_session_variable). RCLOL_KEEP_HOST_ENV lists names to keep anyway, separated by spaces or commas.
"""
import os
import sys
# Desktop-session, service-manager, terminal and packaging metadata that nothing in the guest reads.
# Everything Darling and the host libraries it loads need is kept: DISPLAY, XAUTHORITY, XCURSOR_*,
# XDG_RUNTIME_DIR, XDG_DATA_DIRS and the other XDG base directories (the Vulkan loader searches
# them), DBUS_SESSION_BUS_ADDRESS, PULSE_*, PIPEWIRE_*, ALSA_*, DRI_PRIME, MESA_*, mesa_glthread,
# __GL_*, __NV_*, __EGL_*, VK_*, LIBGL_*, LD_PRELOAD (gamemoderun) and LD_LIBRARY_PATH.
SESSION_PREFIXES = (
'XDG_SESSION_', 'XDG_SEAT', 'XDG_VTNR', 'XDG_CURRENT_DESKTOP', 'XDG_MENU_PREFIX',
'GNOME_', 'KDE_', 'GDM', 'SSH_', 'SYSTEMD_', 'MEMORY_PRESSURE_', 'QT_', 'GTK_', 'GDK_',
'GIO_', 'GSETTINGS_', 'GJS_', 'FLATPAK_', 'SNAP_', 'KONSOLE_', 'TERM_PROGRAM', 'BASH_FUNC_',
)
SESSION_NAMES = {
'WAYLAND_DISPLAY', 'DESKTOP_SESSION', 'DESKTOP_STARTUP_ID', 'SESSION_MANAGER',
'INVOCATION_ID', 'JOURNAL_STREAM', 'MANAGERPID', 'MANAGERPIDFDID', 'NOTIFY_SOCKET',
'LISTEN_FDS', 'LISTEN_PID', 'LISTEN_FDNAMES', 'COLORTERM', 'VTE_VERSION', 'WINDOWID',
'WINDOWPATH', 'CHROME_DESKTOP', 'NO_AT_BRIDGE', 'MOTD_SHOWN', 'SNAP', 'APPIMAGE', 'APPDIR',
'OWD', 'VIRTUAL_ENV', 'DEBUGINFOD_URLS', 'LS_COLORS', 'COLORFGBG', 'OLDPWD', 'SHLVL',
}
GUEST_SHELL = '/bin/zsh'
def host_session_variable(name):
return name in SESSION_NAMES or name.startswith(SESSION_PREFIXES)
def filter_host_environment(environ):
"""Return the names removed from environ (a mutable mapping), honoring RCLOL_KEEP_HOST_ENV."""
keep = set(environ.get('RCLOL_KEEP_HOST_ENV', '').replace(',', ' ').split())
removed = sorted(name for name in environ if host_session_variable(name) and name not in keep)
for name in removed:
del environ[name]
# The host login shell (for example /bin/fish) does not exist in the guest.
if 'SHELL' not in keep:
environ['SHELL'] = GUEST_SHELL
return removed
def close_inherited_fds():
"""A terminal, IDE or service manager can leak descriptors; the guest must not inherit them."""
@@ -22,6 +61,8 @@ def close_inherited_fds():
def main(argv):
if len(argv) < 2:
sys.exit('usage: exec-guest.py COMMAND [ARGUMENT]...')
if os.environ.get('RCLOL_FILTER_HOST_ENV') == '1':
filter_host_environment(os.environ)
close_inherited_fds()
os.execvp(argv[1], argv[1:])
+13 -2
View File
@@ -36,7 +36,8 @@ if record["command"] == "darling":
return False
record["fds"] = sorted(fd for fd in map(int, os.listdir("/proc/self/fd")) if is_open(fd))
record["flags"] = {{key: os.environ.get(key) for key in
("RCLOL_APP_ARG3", "RCLOL_APP_ARG3_SLOT", "RCLOL_UX_ARG2", "RCLOL_UX_ARG2_SLOT", "RCLOL_UX_ARG1", "RCLOL_APP_ARGS", "RCLOL_APP_ARG2", "DYLD_INSERT_LIBRARIES", "DYLD_LIBRARY_PATH", "DYLD_FRAMEWORK_PATH", "DARLING_XSHM_LIBRARY")}}
("RCLOL_APP_ARG3", "RCLOL_APP_ARG3_SLOT", "RCLOL_UX_ARG2", "RCLOL_UX_ARG2_SLOT", "RCLOL_UX_ARG1", "RCLOL_APP_ARGS", "RCLOL_APP_ARG2", "DYLD_INSERT_LIBRARIES", "DYLD_LIBRARY_PATH", "DYLD_FRAMEWORK_PATH", "DARLING_XSHM_LIBRARY",
"WAYLAND_DISPLAY", "XAUTHORITY", "DRI_PRIME", "MESA_NO_ERROR", "__GL_THREADED_OPTIMIZATIONS", "SHELL")}}
with open({str(log)!r}, "a") as output:
output.write(json.dumps(record) + "\\n")
'''
@@ -116,7 +117,17 @@ with open({str(log)!r}, "a") as output:
subprocess.run(["sh", str(launcher)], env=env, check=True, capture_output=True, text=True)
clean = json.loads(log.read_text().splitlines()[-1])
assert clean["flags"]["DYLD_INSERT_LIBRARIES"] is None
print("LAUNCHER_OFFLINE pass=1 installed=PASS installer=PASS inherited_injection=absent runtime=preserved inherited_fds=closed")
# The opt-in host environment filter drops session metadata but never the launcher's GPU tuning.
env.update({"RCLOL_FILTER_HOST_ENV": "1", "WAYLAND_DISPLAY": "wayland-0", "XAUTHORITY": "/tmp/xauth",
"DRI_PRIME": "1", "SHELL": "/bin/fish"})
env.pop("MESA_NO_ERROR", None)
env.pop("__GL_THREADED_OPTIMIZATIONS", None)
subprocess.run(["sh", str(launcher)], env=env, check=True, capture_output=True, text=True)
filtered = json.loads(log.read_text().splitlines()[-1])["flags"]
assert filtered["WAYLAND_DISPLAY"] is None and filtered["SHELL"] == "/bin/zsh", "host session metadata survived"
assert (filtered["XAUTHORITY"], filtered["DRI_PRIME"]) == ("/tmp/xauth", "1"), "X11/GPU selection lost"
assert (filtered["MESA_NO_ERROR"], filtered["__GL_THREADED_OPTIMIZATIONS"]) == ("1", "0"), "launcher GPU tuning lost"
print("LAUNCHER_OFFLINE pass=1 installed=PASS installer=PASS inherited_injection=absent runtime=preserved inherited_fds=closed host_env_filter=PASS")
if __name__ == "__main__":