diff --git a/docs/SETUP.md b/docs/SETUP.md index 10d22b9..7c669a2 100644 --- a/docs/SETUP.md +++ b/docs/SETUP.md @@ -39,7 +39,7 @@ Linux 5.14 or newer is required for `MADV_POPULATE_WRITE`, used when Darwin's al The runtime installer requires native `pkg-config` entries for `x11`, `x11-xcb`, `xcb-shm` and `xext`, plus the base Darling SDK. -Run configuration, builds and the per-user runtime installation as your ordinary account. Use sudo only for the base CMake installation and the incremental server installer. Running the whole build or per-user install under sudo can create root-owned files and configure the wrong guest home. `scripts/install-runtime.sh` initializes the default user prefix and creates `~/.darling/private/tmp` with mode `1777`, so the installer can write guest temporary files. It then shuts down Darling before replacing the paired runtime: first exit the game and both clients manually. +Run configuration, builds and the per-user runtime installation as your ordinary account. Use sudo only for the base CMake installation and the incremental server installer. Running the whole build or per-user install under sudo can create root-owned files and configure the wrong guest home. `scripts/install-runtime.sh` initializes the selected user prefix and creates its `private/tmp` directory with mode `1777`, so the installer can write guest temporary files. It then shuts down Darling before replacing the paired runtime: first exit the game and both clients manually. The normal launcher starts neither an Electron inspector nor client API/UI helpers. It clears inherited `DYLD_INSERT_LIBRARIES`, retains Darling runtime library/framework overrides, supplies an absolute log directory to the launcher, and selects the League product. The log-directory/Chromium argument shim, empty-autofill hook and optional native VPX bridge are no longer injected or built by runtime installation. Existing copies on disk are inactive and may remain. Child-process log-directory rewriting and Chromium feature overrides previously supplied by the shim are removed; client startup without those workarounds needs manual validation. The first-time installer uses `--skip-to-install` and omits the installed client's `--app-root`, avoiding an immediate exit or inaccessible intro screen. The project ships no inspector hooks, client API helpers, input automation or client binary patches. Keep account data, logs and captures private. diff --git a/tests/test_setup_portability.py b/tests/test_setup_portability.py index 47ded2b..4b5a70a 100644 --- a/tests/test_setup_portability.py +++ b/tests/test_setup_portability.py @@ -13,8 +13,7 @@ from unittest.mock import patch ROOT = Path(__file__).resolve().parents[1] -VALIDATION_INPUTS = ( - 'RuntimeFrameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libvDSP.dylib', +GUEST_VALIDATION_INPUTS = ( 'libDarlingXShm.dylib', *(f'RuntimeLibraries/{name}.dylib' for name in ( 'libsystem_kernel', 'libsystem_c', 'libsystem_platform', 'libsystem_malloc', @@ -30,6 +29,12 @@ VALIDATION_INPUTS = ( 'RuntimeFrameworks/OpenGL.framework/Versions/A/Libraries/libGL.dylib', 'Library/Audio/Plug-Ins/Components/DarlingRiotAudio.component/Contents/MacOS/CoreAudio', ) +PREFIX_VALIDATION_INPUTS = ( + 'usr/lib/system/libsystem_kernel.dylib', + 'System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libvDSP.dylib', + 'System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/LaunchServices', + 'System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/CarbonCore.framework/Versions/A/CarbonCore', +) class SetupFixture(unittest.TestCase): @@ -159,10 +164,12 @@ exec REAL_INSTALL "$@"'''.replace('REAL_INSTALL', shutil.which('install'))) def prepare_validation(self): self.configure() - for relative in VALIDATION_INPUTS: - path = self.guest / relative - path.parent.mkdir(parents=True, exist_ok=True) - path.write_bytes(b'\xcf\xfa\xed\xfe' + b'\0' * 5000) + for root, inputs in ((self.guest, GUEST_VALIDATION_INPUTS), + (self.prefix, PREFIX_VALIDATION_INPUTS)): + for relative in inputs: + path = root / relative + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(b'\xcf\xfa\xed\xfe' + b'\0' * 5000) self.stub('llvm-objdump', '''printf '%s\\n' "$*" >> "$FIXTURE_LOG" exit "${CHECK_FAIL:-0}"''') @@ -171,6 +178,11 @@ exit "${CHECK_FAIL:-0}"''') source = (ROOT / 'tools/run-riot-client.sh').read_text() self.assertLess(source.index('runtime-paths.sh'), source.index('ensure-darling-runtime.py')) + def test_metal_pair_uses_shared_runtime_paths(self): + source = (ROOT / 'tools/install-riot-metal-pair.sh').read_text() + self.assertIn('runtime-paths.sh', source) + self.assertNotIn('$HOME/.darling', source) + def test_paths_reject_symlink_escape(self): self.configure() outside = self.home / 'outside' @@ -189,7 +201,7 @@ exit "${CHECK_FAIL:-0}"''') self.env['CHECK_FAIL'] = '1' self.assertNotEqual(self.run_script('build/runtime/tools/validate-riot-runtime.py').returncode, 0) self.env.pop('CHECK_FAIL') - vdsp = self.guest / 'RuntimeFrameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libvDSP.dylib' + vdsp = self.prefix / 'System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libvDSP.dylib' vdsp.unlink(missing_ok=True) self.assertNotEqual(self.run_script('build/runtime/tools/validate-riot-runtime.py').returncode, 0) shutil.rmtree(self.guest) @@ -204,6 +216,27 @@ exit "${CHECK_FAIL:-0}"''') self.assertEqual(result.returncode, 0, result.stderr) self.assertIn(str(override), self.log.read_text()) + def test_libs_cli_prefix_revalidates_environment_guest_home(self): + self.configure() + self.env['RIOT_GUEST_HOME'] = str(self.guest) + other_prefix = Path(self.tmp.name) / 'other prefix' + result = self.run_script('build/runtime/tools/install-riot-libs-runtime.py', + '--prefix', str(other_prefix), '--only', 'libsystem_c.dylib') + self.assertNotEqual(result.returncode, 0) + self.assertIn('inside DPREFIX', result.stderr) + + def test_libs_cli_guest_home_overrides_invalid_environment(self): + self.configure() + self.env['RIOT_GUEST_HOME'] = str(self.home / 'outside prefix') + other_prefix = Path(self.tmp.name) / 'other prefix' + artifact = self.root / 'build/riot-libsystem_c.dylib' + artifact.write_bytes(b'\xcf\xfa\xed\xfe' + b'library') + result = self.run_script('build/runtime/tools/install-riot-libs-runtime.py', + '--prefix', str(other_prefix), '--guest-home', '/Users/New Guest', + '--only', 'libsystem_c.dylib') + self.assertEqual(result.returncode, 0, result.stderr) + self.assertTrue((other_prefix / 'Users/New Guest/RuntimeLibraries/libsystem_c.dylib').is_file()) + def test_failed_stop_with_all_compile_outputs_available(self): self.configure() diff --git a/tools/install-riot-libs-runtime.py b/tools/install-riot-libs-runtime.py index e50cbba..4a2a6b6 100644 --- a/tools/install-riot-libs-runtime.py +++ b/tools/install-riot-libs-runtime.py @@ -23,7 +23,7 @@ import shutil import stat import tempfile -from runtime_paths import runtime_paths +from runtime_paths import validate_runtime_paths ROOT = Path(__file__).resolve().parents[1] @@ -189,12 +189,17 @@ def main(argv=None): parser.exit(1, str(error) + "\n") return config = json.loads((ROOT / "build/config.json").read_text()) - prefix, host_guest_home = runtime_paths(config) - prefix = arguments.prefix or prefix - if arguments.prefix and not os.environ.get("RIOT_GUEST_HOME"): - host_guest_home = prefix / config["guest_home"].lstrip("/") + prefix = arguments.prefix or Path(os.environ.get("DPREFIX") or config["prefix"]) if arguments.guest_home: host_guest_home = prefix / arguments.guest_home.lstrip("/") + elif os.environ.get("RIOT_GUEST_HOME"): + host_guest_home = Path(os.environ["RIOT_GUEST_HOME"]) + else: + host_guest_home = prefix / config["guest_home"].lstrip("/") + try: + prefix, host_guest_home = validate_runtime_paths(prefix, host_guest_home) + except (ValueError, OSError) as error: + parser.exit(1, str(error) + "\n") libraries = destinations(host_guest_home, "") if arguments.only: wanted = set(arguments.only.split(",")) diff --git a/tools/install-riot-metal-pair.sh b/tools/install-riot-metal-pair.sh index 115525b..733b376 100755 --- a/tools/install-riot-metal-pair.sh +++ b/tools/install-riot-metal-pair.sh @@ -5,7 +5,7 @@ # `darling shutdown` and `sh scripts/run.sh` to activate. set -eu project_dir=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) -guest_home=${RIOT_GUEST_HOME:-$HOME/.darling@GUEST_HOME@} +. "$project_dir/tools/runtime-paths.sh" metal="$guest_home/RuntimeFrameworks/Metal.framework/Versions/A/Metal" indium="$guest_home/RuntimeLibraries/libindium.dylib" backups="$project_dir/build/metal-pair-backups" diff --git a/tools/runtime_paths.py b/tools/runtime_paths.py index d50e4d4..2efc413 100644 --- a/tools/runtime_paths.py +++ b/tools/runtime_paths.py @@ -3,10 +3,9 @@ import os from pathlib import Path -def runtime_paths(config): - prefix = Path(os.environ.get("DPREFIX") or config["prefix"]) - guest_home = Path(os.environ.get("RIOT_GUEST_HOME") or - str(prefix / config["guest_home"].lstrip("/"))) +def validate_runtime_paths(prefix, guest_home): + """Return an absolute prefix/guest-home pair that cannot escape the prefix.""" + prefix, guest_home = Path(prefix), Path(guest_home) if not prefix.is_absolute() or not guest_home.is_absolute(): raise ValueError("Runtime paths must be absolute host paths") if ".." in guest_home.parts or not guest_home.is_relative_to(prefix) or guest_home == prefix: @@ -16,6 +15,13 @@ def runtime_paths(config): return prefix, guest_home +def runtime_paths(config): + prefix = Path(os.environ.get("DPREFIX") or config["prefix"]) + guest_home = Path(os.environ.get("RIOT_GUEST_HOME") or + str(prefix / config["guest_home"].lstrip("/"))) + return validate_runtime_paths(prefix, guest_home) + + if __name__ == '__main__': import sys try: diff --git a/tools/validate-riot-runtime.py b/tools/validate-riot-runtime.py index 1e5da1a..5176b46 100644 --- a/tools/validate-riot-runtime.py +++ b/tools/validate-riot-runtime.py @@ -9,8 +9,7 @@ import sys from runtime_paths import runtime_paths ROOT = Path(__file__).resolve().parents[1] -REQUIRED = ( - 'RuntimeFrameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libvDSP.dylib', +GUEST_REQUIRED = ( 'libDarlingXShm.dylib', *(f'RuntimeLibraries/{name}' for name in ( 'libsystem_kernel.dylib', 'libsystem_c.dylib', 'libsystem_platform.dylib', @@ -28,10 +27,19 @@ REQUIRED = ( 'RuntimeFrameworks/OpenGL.framework/Versions/A/Libraries/libGL.dylib', 'Library/Audio/Plug-Ins/Components/DarlingRiotAudio.component/Contents/MacOS/CoreAudio', ) +PREFIX_REQUIRED = ( + 'usr/lib/system/libsystem_kernel.dylib', + 'System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/' + 'vecLib.framework/Versions/A/libvDSP.dylib', + 'System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/' + 'LaunchServices.framework/Versions/A/LaunchServices', + 'System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/' + 'CarbonCore.framework/Versions/A/CarbonCore', +) MACHO = (b'\xcf\xfa\xed\xfe', b'\xca\xfe\xba\xbe') -def inputs(guest_home): +def inputs(prefix, guest_home): def enumeration_error(error): raise error @@ -39,7 +47,8 @@ def inputs(guest_home): with path.open('rb') as source: return source.read(4) in MACHO - required = [guest_home / relative for relative in REQUIRED] + required = ([guest_home / relative for relative in GUEST_REQUIRED] + + [prefix / relative for relative in PREFIX_REQUIRED]) for path in required: if not is_macho(path): raise ValueError('Expected installed Mach-O runtime binary: ' + str(path)) @@ -64,8 +73,8 @@ def inputs(guest_home): def main(): try: - _, guest_home = runtime_paths(json.loads((ROOT / 'build/config.json').read_text())) - binaries = inputs(guest_home) + prefix, guest_home = runtime_paths(json.loads((ROOT / 'build/config.json').read_text())) + binaries = inputs(prefix, guest_home) subprocess.run([sys.executable, str(ROOT / 'tools/check-darwin-tlv-calls.py'), *map(str, binaries)], check=True) except (OSError, ValueError, subprocess.CalledProcessError) as error: