Skip to content

Linux: use the CEF sandbox where it can work - #3

Merged
zackees merged 6 commits into
obs-rust:masterfrom
vpacher:feat/1092-linux-sandbox
Oct 24, 2026
Merged

zackees merged 6 commits into
obs-rust:masterfrom
vpacher:feat/1092-linux-sandbox

Conversation

@vpacher

@vpacher vpacher commented Oct 22, 2026 •

Copy link
Copy Markdown
Member

Step A of the Linux sandbox work in obs-rust/obs-studio#1092 (the pre-check before CefInitialize), and the log part of B.

B was decided on 2026-10-24 (obs-rust/obs-studio#1092 (comment)): when no sandbox can work, the browser runs without it, the reason is logged and goes to stderr, and the user gets a warning with an "Accept anyway" countdown. This PR does the log and stderr part. The warning dialog follows in separate PRs (a shared frontend helper in obs-studio, then its use here). The .deb packaging (C) is separate too.

Change

  • browser-sandbox.{hpp,cpp}: DecideSandbox, a pure function over a probe (platform supported, root, user namespaces, state of chrome-sandbox), and ProbeSandbox for Linux:
    • user namespaces: a forked child does what Chromium's namespace sandbox does. It unshares user, PID and network namespaces, then maps its own uid and gid. A sysctl alone misses the AppArmor restriction and seccomp filters.
    • chrome-sandbox next to libcef (or at CHROME_DEVEL_SANDBOX): setuid root means owned by root, setuid, executable, and not on a nosuid mount.
  • BrowserInit sets settings.no_sandbox from the decision, and logs the decision and the reason. When no sandbox can work, it runs as before (no_sandbox), so nobody loses their browser sources. Windows and macOS have no probe yet and are unchanged.
Probe Decision
no probe for the platform off (as today)
root off (Chromium exits the process when it runs sandboxed as root)
user namespaces namespace sandbox
no user namespaces, chrome-sandbox setuid root setuid helper
no user namespaces, chrome-sandbox absent or not setuid off (Chromium aborts on a helper that is not setuid)

Tests

  • 67f32cf RED: tests/sandbox_decision.py, 9 cases, all fail against a stub. 48aa373 GREEN: 9/9 (macOS and Linux).

  • d9faaa1 RED: SandboxFallback, 6 more cases in tests/sandbox_decision.py; the 3 fallback cases (root; no user namespaces with a helper that is not setuid; no user namespaces and no helper) fail against a stub. 87ba6a2 GREEN: 15/15.

  • Fallback log line, real module (obs-rust/obs-studio d5960eac6, CEF 8078, Debian 13 LXC):

    • root: [obs-browser]: CEF sandbox off: OBS runs as root. Browser sources run without the sandbox. at error level, so OBS echoes it once to stderr. Renders.
    • normal user: CEF sandbox on: user namespaces are available at info level, as before. Renders.
    • Windows and macOS (no probe): the line stays at info level.
  • tests/sandbox_probe.py prints the probe and the decision in the current environment. Debian 13 LXC container, CEF 7871 chrome-sandbox: root and every helper state gives off; a normal user and every helper state gives namespaces; a normal user with RestrictNamespaces=~user gives off, off (not setuid) and the setuid helper.

  • Real module (obs-rust/obs-studio master 9f2253334, CEF 7871, Browser Source with a JavaScript page, read back through GetSourceScreenshot):

    • root: CEF sandbox off: OBS runs as root, renders, every CEF process has --no-sandbox (as today).
    • normal user: CEF sandbox on: user namespaces are available, renders. The renderers and one utility process have their own user and PID namespace and seccomp-bpf.
    • normal user, RestrictNamespaces=~user: CEF sandbox off: no user namespaces, and chrome-sandbox is not setuid root, renders. With no_sandbox = false alone, OBS aborted here.
  • Ubuntu 24.04.5 VM (kernel 6.8.0-147, kernel.apparmor_restrict_unprivileged_userns = 1, AppArmor on). The test host is a minimal CEF 7871 program: CefInitialize with no_sandbox = false, a separate subprocess binary as with obs-browser-page, and this probe in the main process. The probe agrees with Chromium in every case:

    Case Probe Chromium with the sandbox on
    stock, chrome-sandbox present, not setuid (as the .deb installs it) off aborts: "The SUID sandbox helper binary was found, but is not configured correctly"
    stock, no chrome-sandbox off aborts: "No usable sandbox!"
    stock, no_sandbox = true (as today) off runs
    AppArmor profile with userns on the subprocess binary only off aborts: "No usable sandbox!"
    AppArmor profile with userns on the main binary on runs; the sandboxed zygote has its own user namespace

    On stock Ubuntu 24.04, unshare(CLONE_NEWUSER) itself succeeds and only the uid_map write fails, so the probe has to map the uid, as it does.

    For C on #1092: the user namespaces are created by the process that hosts CEF. An AppArmor profile therefore has to cover the OBS executable, not obs-browser-page.

Refs obs-rust/obs-studio#1092

CEF 8078 (2026-10-24)

Linux x86_64 now uses CEF 8078 (obs-rust/obs-studio#1948). I ran the same tests again. The code of this PR did not change.

  • Real module: obs-rust/obs-studio master d5960eac6, CEF 156.0.8+g426920c+chromium-156.0.8078.48, this PR merged onto the obs-browser pin 7bab463 (no conflicts). Browser source with a JavaScript page, read back through GetSourceScreenshot:
    • root: CEF sandbox off: OBS runs as root, renders, every CEF process has --no-sandbox.
    • normal user: CEF sandbox on: user namespaces are available, renders. The renderers and one utility process have their own user and PID namespace and seccomp-bpf.
    • normal user, RestrictNamespaces=~user: CEF sandbox off: no user namespaces, and chrome-sandbox is not setuid root, renders.
  • Ubuntu 24.04.5 VM (kernel 6.8.0-147, kernel.apparmor_restrict_unprivileged_userns = 1), minimal CEF 8078 host: the probe agrees with Chromium in all five cases, with the same results as the table above.

The decision and the probe do not depend on the CEF version.

🤖 Generated with Claude Code

vpacher and others added 4 commits October 22, 2026 08:35
Add browser-sandbox.{hpp,cpp} with DecideSandbox (a pure function over a
SandboxProbe: platform supported, root, user namespaces, state of
chrome-sandbox) and ProbeSandbox, both stubs for now, and
tests/sandbox_decision.py, which compiles the decision with
tests/sandbox-decision.cpp and runs every case:

- no probe for the platform: off (as today)
- root: off, also with namespaces or a setuid helper (Chromium exits the
  process when it runs sandboxed as root)
- user namespaces: the namespace sandbox, whatever the helper state
- no user namespaces: the setuid helper when it is setuid root; off when
  it is absent or not setuid (Chromium aborts on a helper that is not
  setuid)

All 9 cases fail against the stub.

Refs obs-rust/obs-studio#1092

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DecideSandbox: off without a platform probe and as root; the namespace
sandbox when user namespaces work (Chromium then ignores the helper);
otherwise the setuid helper when chrome-sandbox is setuid root, and off
when it is absent or not setuid. The reason goes to the log.

tests/sandbox_decision.py: 9/9.

Refs obs-rust/obs-studio#1092

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ProbeSandbox (Linux):
- user namespaces: a forked child does what Chromium's namespace sandbox
  does: unshare user, PID and network namespaces, then map its own uid
  and gid. A sysctl alone misses the AppArmor restriction (Ubuntu
  24.04+) and seccomp filters (systemd RestrictNamespaces). The child
  uses only async-signal-safe calls. Skipped as root.
- chrome-sandbox: next to libcef, or at CHROME_DEVEL_SANDBOX; setuid
  root means a regular file owned by root, setuid, executable, not on a
  nosuid mount.

Other platforms: unsupported (the decision is off, as today).

tests/sandbox_probe.py prints the probe and the decision in the current
environment. CT104 (Debian 13 LXC, CEF 7871 chrome-sandbox), 9/9 as
measured on #1092: root off; user namespaces; no user namespaces: off,
off (not setuid), setuid helper.

Refs obs-rust/obs-studio#1092

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
BrowserInit sets settings.no_sandbox from the sandbox decision instead
of always true. The probe looks for chrome-sandbox in the directory of
libcef (dladdr on cef_version_info). The decision and its reason go to
the log:

  [obs-browser]: CEF sandbox on: user namespaces are available
  [obs-browser]: CEF sandbox off: OBS runs as root

When no sandbox can work, obs-browser runs as before (no_sandbox), so
nobody loses their browser sources; the fallback policy (a UI notice, a
"require the sandbox" setting) and the .deb packaging wait for the design
decision on #1092. Windows and macOS have no probe yet and stay as they
were.

CT104 (Debian 13 LXC, CEF 7871, obs-studio master 9f2253334, Browser
Source with a JavaScript page, read back through GetSourceScreenshot):

- root: off, renders; every CEF process has --no-sandbox (as today)
- normal user: on, renders; renderers and one utility process have their
  own user and PID namespace and seccomp-bpf
- normal user, RestrictNamespaces=~user, chrome-sandbox not setuid: off,
  renders. With no_sandbox = false alone, OBS aborted here ("The SUID
  sandbox helper binary was found, but is not configured correctly").

Refs obs-rust/obs-studio#1092

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vpacher and others added 2 commits October 24, 2026 22:44
Add SandboxFallback (a stub for now) and its cases to
tests/sandbox-decision.cpp. The fallback is B in
obs-rust/obs-studio#1092: a probe says the sandbox cannot work, so the
browser runs without it and the user is warned. Without a probe (Windows,
macOS for now) the sandbox is off as before, with no warning.

- fallback: root; no user namespaces and a chrome-sandbox that is not
  setuid root; no user namespaces and no chrome-sandbox
- no fallback: no probe for the platform; user namespaces; a setuid root
  chrome-sandbox

The 3 fallback cases fail against the stub.

Refs obs-rust/obs-studio#1092

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SandboxFallback is true when a probe says the sandbox cannot work. The
plugin then logs the reason at error level, as decided for B in
obs-rust/obs-studio#1092: OBS echoes error lines to stderr on Linux and
macOS, so the line is in the log and on stderr once. The browser source
continues without the sandbox. Without a probe (Windows, macOS for now)
the line stays at info level, as before.

The UI warning with the "Accept anyway" countdown follows in a separate
change.

tests/sandbox_decision.py: 15 of 15 pass.

Refs obs-rust/obs-studio#1092

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vpacher
vpacher marked this pull request as ready for review October 24, 2026 20:49
@zackees
zackees merged commit 8a0a7bf into obs-rust:master Oct 24, 2026
1 check passed
chriscoveries pushed a commit to chriscoveries/obsrust-fork that referenced this pull request Oct 25, 2026
…s) (obs-rust#2378)

Moves plugins/obs-browser from 7bab463c9 to 4583599fe, with:

- obs-rust/obs-browser#3: the CEF sandbox where it can work on Linux
  (obs-rust#1092)
- obs-rust/obs-browser#5: os_event_init results checked in module load
  and ExecuteOnBrowser
- obs-rust/obs-browser#6: WebAudio resumes after the audio service dies
  (obs-rust#2328)
- obs-rust/obs-browser#7: browser audio that arrives too late to mix is
  dropped (obs-rust#2335)
- obs-rust/obs-browser#8: the fallback warning when the sandbox can't
  work (obs-rust#2218)

One bump instead of three (obs-rust#2333, obs-rust#2351 and ours), as agreed on obs-rust#2351.
docs/features.md gets the two audio lines from obs-rust#2333 and obs-rust#2351 as they
are, a line for the sandbox, and the browser on the fallback warning
line, which obs-rust#2238 merged on 2026-10-25.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@vpacher
vpacher deleted the feat/1092-linux-sandbox branch October 25, 2026 12:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants