From 98c390a75deb59d94424b3d89d4410d12bd2bc56 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Wed, 28 Oct 2026 09:00:17 +0100 Subject: [PATCH] fix: refuse a preload the loader cannot find Runs passed LD_PRELOAD=libfaketime.so.1 by bare name on a host without libfaketime. The loader skipped it silently, so those runs recorded a fixed clock they never had. A preload must now be an absolute path to a file, which is bound into the sandbox and recorded by sha256. --- scripts/core_probe.py | 12 +++++++++++- tests/test_core_probe.py | 12 ++++++++++++ wiki/tools.md | 2 +- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/scripts/core_probe.py b/scripts/core_probe.py index 0d3585ae6..4d3cc7f1c 100644 --- a/scripts/core_probe.py +++ b/scripts/core_probe.py @@ -935,7 +935,8 @@ def run_once(args: argparse.Namespace, system_dir: Path, scratch: Path, state_ou readonly = [Path(__file__), Path(args.core), system_dir, *interpreter_paths()] if args.content: readonly.append(Path(args.content).resolve().parent) - readonly += [Path(value) for _, value in args.env if Path(value).is_absolute() and Path(value).exists()] + readonly += [Path(part) for _, value in args.env for part in value.replace(":", " ").split() + if Path(part).is_absolute() and Path(part).exists()] prefix, isolation = sandbox_argv(writable, readonly) if args.require_sandbox and isolation != SANDBOX: raise ProbeError(f"refused to run the core outside a sandbox: {isolation}") @@ -1157,6 +1158,15 @@ def validate_args(args: argparse.Namespace) -> list[str]: isolation = sandbox_argv([])[1] if isolation != SANDBOX: problems.append(f"--require-sandbox: {isolation}") + for key, value in args.env: + if key != "LD_PRELOAD": + continue + # A library the loader cannot find is skipped without a word, and + # the run would record a fixed clock it never had. Only a file that + # exists is bound into the sandbox and recorded by sha256. + for library in value.replace(":", " ").split(): + if not (Path(library).is_absolute() and Path(library).is_file()): + problems.append(f"--env LD_PRELOAD: {library} is not an absolute path to a file") if args.oracle and not Path(args.oracle).is_file(): problems.append(f"--oracle {args.oracle}: not a file") if args.png_dir and not args.frame_hash_every: diff --git a/tests/test_core_probe.py b/tests/test_core_probe.py index 5181c16df..60a8e1fc8 100644 --- a/tests/test_core_probe.py +++ b/tests/test_core_probe.py @@ -457,6 +457,18 @@ class ResultTests(unittest.TestCase): "--claim", "c", "--method", "m"]) self.assertTrue(any("--observable nope" in p for p in core_probe.validate_args(unknown))) + def test_a_preload_the_loader_cannot_find_is_refused(self) -> None: + # libfaketime.so.1 by bare name, with libfaketime not installed, was + # skipped by the loader: every run recorded a fixed clock it never had. + parser = core_probe.build_parser() + core = sys.executable + for value in ("libfaketime.so.1", "/nonexistent/libfaketime.so.1", f"{core}:libfaketime.so.1"): + args = parser.parse_args(["run", "--core", core, "--env", f"LD_PRELOAD={value}"]) + with self.subTest(value=value): + self.assertTrue(any("LD_PRELOAD" in p for p in core_probe.validate_args(args))) + found = parser.parse_args(["run", "--core", core, "--env", f"LD_PRELOAD={core}"]) + self.assertFalse([p for p in core_probe.validate_args(found) if "LD_PRELOAD" in p]) + @unittest.skipUnless(shutil.which("cc"), "no C compiler") class FakeCoreTests(unittest.TestCase): diff --git a/wiki/tools.md b/wiki/tools.md index a2efa281b..7664e24c4 100644 --- a/wiki/tools.md +++ b/wiki/tools.md @@ -964,7 +964,7 @@ python scripts/core_probe.py rejudge --emulator beetle_psx | `--frames`, `--warmup` | frames timed, and frames run before timing starts | | `--press FRAME:BUTTON[:LEN]` | hold a libretro joypad button on port 0 (`a`, `b`, `start`, `up`...). A menu may need 15 frames or more to see it | | `--joypad-ports N` | ports given a joypad after loading, as RetroArch does (default 2) | -| `--env KEY=VALUE` | environment of the core process. A core that reads the host clock repeats only with the clock fixed, for example `LD_PRELOAD` of libfaketime with `FAKETIME`. A value naming a file is recorded by file name and sha256 | +| `--env KEY=VALUE` | environment of the core process. A core that reads the host clock repeats only with the clock fixed, for example `LD_PRELOAD` of a clock shim. `LD_PRELOAD` must name each library by an absolute path to a file, or the run is refused: the loader skips a library it cannot find without a word. A value naming a file is recorded by file name and sha256 | | `--frame-hash-every K`, `--png-dir` | sha1 of every K-th frame, and those frames as PNG | | `--state-var SECTION.VAR` | a variable of a Mednafen save state, as an observable | | `--state-dir` | keep the save state taken at the end of the first repetition |