Repository navigation
Description
Summary
rom_hub.sandbox.probe() reports the seccomp sandbox as available whenever pyseccomp imports, without ever loading a filter. On a kernel built without seccomp filter support, every seccomp_load then fails with ECANCELED, so every plugin is refused with SandboxRefused, while the host is told sandboxed: true.
In ROMarr this fails silently. Interactive Search returns no plugin results and logs nothing about why. The Plugins page says "Plugins run confined". /api/v1/hub/status reports "sandboxed": true, "acquisition": "ready". ROMarr's built-in fallback never triggers, because it is driven by probe(). ROM_HUB_ALLOW_UNSANDBOXED=1 on the container does not help either: ROMarr's in-process hub.search() and hub.fetch() take allow_unsandboxed from sandbox_state(), not from the environment. The operator is left with no way to use plugins at all.
Environment
- Synology DS920+, DSM 7, kernel
4.4.302+(Synology's own build) - ROMarr 1.6.0 and 1.7.0 Docker images. Both pin rom-hub
8e46348(unchanged in currentmaster). - Docker security options:
[name=apparmor]only. No seccomp.
Evidence
The kernel has no seccomp. There is no Seccomp: line at all:
$ uname -r
4.4.302+
$ grep -i seccomp /proc/self/status
$ docker info --format '{{.SecurityOptions}}'
[name=apparmor]
Loading a minimal filter inside the ROMarr container fails:
$ docker exec -u 1027:65536 romarr python -c "import pyseccomp as s; f=s.SyscallFilter(defaction=s.ALLOW); f.add_rule(s.ERRNO(1),'ptrace'); f.load()"
OSError: [Errno 125] Operation canceled
Every plugin is refused:
$ docker exec -u 1027:65536 romarr rom-hub search "sonic the hedgehog"
! archive-org: SandboxRefused: refusing to run plugin archive-org unsandboxed: seccomp filter load failed: [Errno 125] Operation canceled. ...
(same for all 11 enabled search plugins)
0 of 11 sources responded, 0 results in 0 games
The probe still reports available, and ROMarr believes it:
$ curl -s -H "X-Api-Key: …" http://127.0.0.1:6868/api/v1/hub/status
{"available": true, "sandboxed": true, "sandbox_detail": "seccomp filter available", ..., "acquisition": "ready"}
With the override set by hand on the CLI, the plugins themselves work fine. 11 of 11 sources responded with 151 results:
$ docker exec -u 1027:65536 -e ROM_HUB_ALLOW_UNSANDBOXED=1 romarr rom-hub search "sonic the hedgehog"
11 of 11 sources responded, 151 results in 70 games
Proposed fix
probe() should answer by actually loading the filter in a throwaway child process, because loading is irreversible. The result should be cached. The load step is split out of install() into _load_filter(). That way the child does not call probe() again and recurse, since install() itself starts by probing.
On a kernel with seccomp nothing changes: probe() still returns (True, "seccomp filter available"), and install() still blocks socket(). On a kernel without it, probe() returns (False, "seccomp filter cannot be loaded here: … [Errno 125] Operation canceled"). ROMarr then takes its existing, logged fallback (ROM Hub calls run WITHOUT confinement: …) instead of failing silently.
Tested:
- Kernel with seccomp: probe reports available;
socket()is stillPermissionErrorafterinstall(). - Simulated failing load (ECANCELED): probe reports unavailable, with the reason.
pytest tests -k "sandbox or broker or probe": 74 passed, 5 skipped.- On the DS920+ itself, with the patched
sandbox.pybind-mounted over the bundled one,/api/v1/hub/statusreports"sandboxed": falsewith the real reason, and plugin search and fetch work in ROMarr. A No-Intro Mega Drive dump was fetched throughnointro-archive.
diff --git a/src/rom_hub/sandbox.py b/src/rom_hub/sandbox.py
index 598abd1..0ffab3e 100644
--- a/src/rom_hub/sandbox.py
+++ b/src/rom_hub/sandbox.py
@@ -18,6 +18,8 @@ Scope, stated plainly:
"""
import ctypes
+import functools
+import subprocess
import sys
PR_SET_NO_NEW_PRIVS = 38
@@ -44,13 +46,39 @@ class SandboxUnavailable(Exception):
def probe() -> tuple[bool, str]:
- """Report whether a filter can be installed, and why not if it cannot."""
+ """Report whether a filter can be installed, and why not if it cannot.
+
+ Importable is not the same as loadable. A kernel built without
+ CONFIG_SECCOMP_FILTER (Synology DSM's 4.4 kernel is one) imports pyseccomp
+ fine and then fails every `seccomp_load` with ECANCELED. Reporting that as
+ "available" makes hosts trust a sandbox that cannot exist, and every plugin
+ is then refused at run time with no fallback offered. So the answer comes
+ from actually loading the filter -- in a throwaway child, because loading
+ is irreversible and would confine the caller.
+ """
+ return _probe_cached()
+
+
+@functools.lru_cache(maxsize=1)
+def _probe_cached() -> tuple[bool, str]:
if sys.platform != "linux":
return False, f"seccomp requires linux; this is {sys.platform}"
try:
import pyseccomp # noqa: F401
except ImportError as exc:
return False, f"pyseccomp is not installed: {exc}"
+ try:
+ child = subprocess.run(
+ [sys.executable, "-c",
+ "from rom_hub.sandbox import _load_filter; _load_filter()"],
+ capture_output=True, text=True, timeout=30,
+ )
+ except (OSError, subprocess.SubprocessError) as exc:
+ return False, f"could not test-load the seccomp filter: {exc}"
+ if child.returncode != 0:
+ lines = (child.stderr or "").strip().splitlines()
+ detail = lines[-1] if lines else f"exit status {child.returncode}"
+ return False, f"seccomp filter cannot be loaded here: {detail}"
return True, "seccomp filter available"
@@ -64,11 +92,21 @@ def install() -> None:
child instead, or every later test in the same session that spawns a
subprocess will fail with `PermissionError` once `execve` is blocked.
"""
- available, reason = probe()
- if not available:
- raise SandboxUnavailable(reason)
+ if sys.platform != "linux":
+ raise SandboxUnavailable(f"seccomp requires linux; this is {sys.platform}")
+ _load_filter()
+
+
+def _load_filter() -> None:
+ """Set no_new_privs and load the deny filter on this process.
- import pyseccomp
+ Kept separate from `install` so `probe`'s throwaway child can run it
+ without probing again (which would recurse, one child per level).
+ """
+ try:
+ import pyseccomp
+ except ImportError as exc:
+ raise SandboxUnavailable(f"pyseccomp is not installed: {exc}") from exc
libc = ctypes.CDLL("libc.so.6", use_errno=True)
if libc.prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0:Related: ROMarr
Once this lands, ROMarr needs a rom-hub pin bump to pick it up. Separately, it would help if an operator could explicitly opt in on a host like this one, and if ROMarr showed the refusal reason somewhere visible instead of returning empty results. Happy to open a matching ROMarr issue if you'd prefer it tracked there.
Activity
BlizzHacker commentedon Oct 29, 2026
Thanks for reporting this. The Synology plugin probe fix has now merged into ROM Hub: PR #47, with the ECANCELED regression test in PR #48. The probe now test-loads the filter in a throwaway child process so unsupported seccomp behavior is caught before plugin installation, while the installed plugin still loads through the normal sandbox. Please update ROM Hub in ROMarr, retry enabling the plugin, and let us know whether it works on your DSM/kernel version. If it still fails, share the ROMarr and ROM Hub versions plus redacted logs; please omit credentials and tokens.
BlizzHacker commentedon Oct 29, 2026
The Synology kernel compatibility fix is in ROM Hub's updated plugin host, and its regression test is merged. Please update ROM Hub and the plugin on the Synology system, then try enabling the plugin again. If it still fails, reply with the DSM version, kernel version, plugin name, and a redacted log.
p4xel commentedon Oct 29, 2026
The underlying fix is now merged in ROM Hub: BlizzHacker/rom-hub#47, plus the follow-up test in #48. probe() now test-loads the seccomp filter, so on a kernel without seccomp it correctly reports the sandbox as unavailable and ROMarr takes its logged fallback.
ROMarr 1.8.0 doesn’t include it yet. Both Dockerfile and Dockerfile.browser still pin rom-hub 8e46348, so the 1.8.0 image ships the old sandbox.py. Checked on my DS920+ with a throwaway container:
Without my bind-mounted patch, /api/v1/hub/status reports "sandboxed": true and every plugin is refused. With it, plugins work and the log shows running plugins WITHOUT confinement: … [Errno 125] Operation canceled, as intended.
Could you bump the rom-hub pin to current master (933d19b, which includes #47/#48 and the new MiNERVA catalogue entry)? This issue can be closed after that.
One small thing I noticed while the fix is active: the WITHOUT confinement warning is logged on every plugin call, roughly every 2–3 seconds while the Plugins page is open. Logging it once per process would keep the log readable.
feat: Igir v5.8.0 1G1R front-end, Supportive Library Shell, Fleet Syn…
BlizzHacker commentedon Nov 2, 2026
@p4xel Thanks for spotting that!
We have updated the pinned
BlizzHacker/rom-hubrevision acrossDockerfile,Dockerfile.browser, anddocs/PLUGINS.mdin commit0ecc8d1b59dcbb07d44fb4dae540d82bed8f369atof30123ff9113c2a8067e76e177a2405ca66561ab(which includes theeb7cd82catalog fix as well as the newigirv5.8.0 1G1R Collection Manager plugin).Verified by
tests/test_issues_107_112.py::test_issue_93_rom_hub_pin_updated(PASSED).