Chromium with WebXR for the Steam Frame

Build script, SteamVR sandbox patch, and a Frame installer that adds
Chromium XR to the Steam library.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
saphidandClaude Opus 5.5 committed 2026-09-26 23:13:40 +10:00
commit 60237408b3
11 files changed
+789

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
*.tar.xz
.DS_Store
__pycache__/
+27
View File
@@ -0,0 +1,27 @@
Copyright (c) 2026, saphid
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+204
View File
@@ -0,0 +1,204 @@
# Chromium with WebXR for the Steam Frame
A build recipe and installer for **Chromium with immersive WebXR on Valve's
Steam Frame** headset. Press the
**Enter VR** button on a WebXR site and it opens in the headset, through
SteamVR, as a full VR experience.
The Chromium that SteamOS offers (Flathub) can't do this. Websites see
`navigator.xr`, but `isSessionSupported("immersive-vr")` returns `false`, so
VR buttons are greyed out or missing. Upstream Chromium only wires up its
OpenXR backend on Windows and Android. This repo builds Chromium with the
in-progress Linux OpenXR changes, adds a small fix so it works with SteamVR,
and installs it on the Frame as a normal app in your Steam library.
## What you can do with it
The web has a lot of VR content that only needs a browser:
- **360° and 3D video.** Travel, nature, concerts, sports and documentaries
from players that support WebXR, shown around you instead of in a flat
window.
- **Games and toys.** WebXR games and experiments made with
[three.js](https://threejs.org/examples/?q=webxr),
[A-Frame](https://aframe.io/) or [Babylon.js](https://www.babylonjs.com/),
with nothing to install.
- **Learning.** Virtual museum tours, space and anatomy explorers, and other
educational experiences built for the web.
- **Art and music.** Painting, sculpting and music toys that run in a page.
- **Building WebXR sites.** Test your own WebXR app on real headset hardware
from a normal URL, with Chromium's DevTools.
## Status
Tested on a Steam Frame (SteamOS 0.3.0, build 20260922.6101926,
SteamVR 2.17.10) with Chromium **156.0.8071.0**, built for arm64:
| | |
|---|---|
| `isSessionSupported("immersive-vr")` | `true` |
| [WebXR Samples](https://immersive-web.github.io/webxr-samples/) Immersive VR Session | shows its scene in the headset |
| three.js [stereo 360 video](https://threejs.org/examples/webxr_vr_video.html) | plays in 3D |
| Launch from the Steam library | opens as its own panel, like any app |
| Controller and hand input inside WebXR pages | not tested yet |
| Frame rate | not measured (looks smooth) |
This is an unofficial, experimental build. See [Limitations](#limitations)
before you use it for anything other than VR sites.
## Requirements
- A Steam Frame with SteamVR, and a way to run commands on it: the terminal
in Desktop Mode, or SSH.
- To build: an **x86-64 Linux machine** with about **90 GB free disk**, git,
Python 3, tmux (or another way to keep a long job running) and a few
hours. No root access is needed. The first build took about 9.5 hours on
a 6-core, 12-thread desktop CPU; rebuilds take minutes.
## Build
On the Linux build machine:
```sh
git clone https://github.com/saphid/chromium-webxr-steam-frame
cd chromium-webxr-steam-frame
mkdir -p ~/chromium-xr
tmux new -d -s chromium-xr 'build/build.sh > ~/chromium-xr/build.log 2>&1'
```
`build.sh` fetches Chromium with the Linux OpenXR changes, applies the patch
in [`patches/`](patches), cross-compiles for arm64 and packs the result into
`~/chromium-xr/chromium-xr-arm64.tar.xz` (about 145 MB).
- Follow progress with `tail -F ~/chromium-xr/stage` (milestones) or
`tail -F ~/chromium-xr/build.log` (everything).
- If it stops (reboot, full disk, network), run the same command again. It
picks up where it left off.
- To build on another disk, set `CHROMIUM_XR_DIR` and use that directory
in place of `~/chromium-xr` in the commands above, including the log path.
- It stops itself if free disk space drops below 12 GB.
## Install on the Frame
Copy the tarball and this repo to the Frame, then run the installer there.
For example, over SSH from the build machine (replace `steamframe` with your
Frame's hostname or IP address):
```sh
scp ~/chromium-xr/chromium-xr-arm64.tar.xz steamos@steamframe:
ssh steamos@steamframe
git clone https://github.com/saphid/chromium-webxr-steam-frame
chromium-webxr-steam-frame/frame/install.sh ~/chromium-xr-arm64.tar.xz
```
The installer:
- unpacks the build into `~/chromium-xr`, after checking the new binary runs;
- installs the `chromium-xr` launcher in `~/.local/bin`;
- adds **Chromium XR** to the Desktop Mode app menu;
- adds **Chromium XR** to your Steam library, without restarting Steam.
Run it again with a newer tarball to update. Your profile
(`~/.config/chromium-xr`) and the Steam shortcut are kept. You can delete
the repo clone afterwards; the installer keeps what it needs to uninstall.
If the Steam shortcut can't be added automatically, add it by hand: in
Desktop Mode, open Steam, choose **Games → Add a Non-Steam Game to My
Library**, and pick Chromium XR.
## Use it
1. Open **Chromium XR** from your Steam library. It appears as a panel in
the headset.
2. Go to a WebXR site, for example the
[WebXR Samples](https://immersive-web.github.io/webxr-samples/).
3. Press the site's **Enter VR** button.
4. Chromium asks **Allow VR?** in the browser panel. Choose *Allow this time*
or *Allow while visiting the site*.
5. To leave VR, use the site's exit button or the Steam button.
The first time you launch it, Steam may show an **External Controller
Translation** notice. It's only information about controller button icons;
choose OK.
From a terminal on the Frame, `chromium-xr https://example.com` opens a
page directly.
## Limitations
- **Part of the sandbox is off.** The launcher passes
`--disable-seccomp-filter-sandbox`, which turns off Chrome's system-call
filter for every process; the namespace sandbox stays on. Without it,
SteamVR refuses the session (details in
[docs/technical-notes.md](docs/technical-notes.md)). Use Chromium XR for VR
sites and keep another browser for everyday browsing.
- **Saved passwords aren't encrypted.** The launcher uses
`--password-store=basic` so startup doesn't stop at a keyring prompt, so
passwords you save are stored unencrypted in `~/.config/chromium-xr`.
- **No automatic updates.** It won't get Chromium security fixes until you
rebuild it.
- **No DRM video.** There's no Widevine, so paid streaming services that
need it won't play.
- **One window at a time per profile.** If Chromium XR is already open,
launching it again opens the page in the existing window.
- **Not a default browser.** It works as one (the desktop entry registers
for web links), but for the reasons above it's better kept for VR.
## Remove it
```sh
~/.local/share/chromium-xr/uninstall.sh # keeps your profile
~/.local/share/chromium-xr/uninstall.sh --remove-profile # also deletes settings and logins
```
This removes the build, the launcher, the menu entry and the Steam shortcut.
## Updating
The Chromium changes are still under review upstream, so this repo pins one
revision of them (`CL_REF` in [`build/build.sh`](build/build.sh), currently
patch set 44 of CL 8132979). To build a newer patch set, set `CL_REF` when
you run the build, for example
`CL_REF=refs/changes/79/8132979/45 build/build.sh`. The script fetches it,
syncs, re-applies the local patch and rebuilds. A newer patch set may need
the patch in [`patches/`](patches) updated. Then install the new tarball on
the Frame as above.
## How it works
- **Chromium changes.** [CL 8441736](https://chromium-review.googlesource.com/c/chromium/src/+/8441736)
(a sandboxed XR process on Linux) and
[CL 8132979](https://chromium-review.googlesource.com/c/chromium/src/+/8132979)
(the OpenXR device provider on Linux, patch set 44), tracked in Chromium
[issue 506004811](https://issues.chromium.org/issues/506004811). Neither
is merged upstream yet. The OpenXR device is behind
`--enable-features=OpenXR`, which the launcher passes.
- **SteamVR fix.** SteamVR's OpenXR runtime asks the kernel who is on the
other end of its socket (`getsockopt(SO_PEERCRED)`). The XR sandbox policy
blocks all `getsockopt` calls, which crashed the XR process.
[`patches/0001-…`](patches/0001-xr-sandbox-allow-getsockopt-SO_PEERCRED.patch)
allows only that one option.
- **Steam integration.** `frame/steam-shortcut.py` adds the shortcut through
the Steam client's local DevTools port, the same API the Steam UI uses.
Steam runs each app as its own panel, so Chromium gets one too.
More detail, including why the seccomp filter is off, is in
[docs/technical-notes.md](docs/technical-notes.md).
## Related work
- [utzcoz/chromium-webxr-linux](https://github.com/utzcoz/chromium-webxr-linux):
a fuller patch series for WebXR over OpenXR on Linux desktops, including
the in-headset permission UI and crash fixes, tested with
[Monado](https://monado.dev/). If you're on an x86-64 Linux PC rather than
the Steam Frame, start there.
- [Chromium issue 506004811](https://issues.chromium.org/issues/506004811):
upstream tracking for WebXR on Linux.
## License
The scripts in this repo are under the [BSD 3-Clause License](LICENSE). The
patch in [`patches/`](patches) modifies Chromium and is under
[Chromium's license](https://chromium.googlesource.com/chromium/src/+/main/LICENSE).
Chromium is a trademark of Google LLC; this project is not affiliated with
Google or Valve.
Executable
+126
View File
@@ -0,0 +1,126 @@
#!/bin/bash
# Cross-compile arm64 Chromium with WebXR over OpenXR on Linux, for the
# Steam Frame. Runs on an x86-64 Linux machine, no root needed.
#
# build/build.sh # checkout, patch, build, package
#
# Needs about 90 GB free disk and several hours (about 9.5 h on a 6-core
# machine for the first build). Run it detached, e.g. in tmux:
# tmux new -d -s chromium-xr 'build/build.sh > ~/chromium-xr/build.log 2>&1'
#
# Progress lines go to $CHROMIUM_XR_DIR/stage. The result is
# $CHROMIUM_XR_DIR/chromium-xr-arm64.tar.xz. Re-running resumes: the
# existing checkout and out/XR are reused, so a rebuild takes minutes.
# Set CL_REF to build a different patch set of CL 8132979 (see README).
set -euo pipefail
W=${CHROMIUM_XR_DIR:-$HOME/chromium-xr}
PATCHES=$(cd "$(dirname "$0")/../patches" && pwd)
# Chromium CL 8132979 (WebXR OpenXR provider on Linux), patch set 44. It sits
# on top of CL 8441736 (the XR process sandbox), so fetching it gets both.
CL_REF=${CL_REF:-refs/changes/79/8132979/44}
mkdir -p "$W"
cd "$W"
stage() { echo "$(date -Is) $*" | tee -a "$W/stage"; }
# Stops the build before the disk fills up.
guard() {
avail=$(df --output=avail -BG "$W" | tail -n 1 | tr -dc 0-9)
if [ "$avail" -lt 12 ]; then stage "ABORT: only ${avail}G free for $W"; return 3; fi
}
# Checks for a file, not the directory, so an interrupted clone is retried.
if [ ! -x depot_tools/gclient ]; then
rm -rf depot_tools
git clone -q https://chromium.googlesource.com/chromium/tools/depot_tools.git
fi
export PATH="$W/depot_tools:$PATH" DEPOT_TOOLS_UPDATE=1 DEPOT_TOOLS_METRICS=0
if [ ! -f .gclient ]; then
cat > .gclient <<'G'
solutions = [{ "name": "src", "url": "https://chromium.googlesource.com/chromium/src.git",
"managed": False, "custom_deps": {}, "custom_vars": { "checkout_nacl": False } }]
target_os = ["linux"]
target_cpu = ["arm64"]
G
fi
# Fetch when there's no checkout yet (or the first fetch was interrupted), or
# when CL_REF names a different patch set than last time.
if ! git -C src rev-parse -q --verify HEAD >/dev/null 2>&1 ||
[ "$(cat "$W/cl-ref" 2>/dev/null)" != "$CL_REF" ]; then
stage "fetch src at $CL_REF"
mkdir -p src
[ -d src/.git ] || git -C src init -q
git -C src remote get-url origin >/dev/null 2>&1 ||
git -C src remote add origin https://chromium.googlesource.com/chromium/src.git
git -C src fetch -q --depth=1 origin "$CL_REF"
# Drops the local patches; they're re-applied below.
git -C src checkout -q --force FETCH_HEAD
echo "$CL_REF" > "$W/cl-ref"
fi
guard
stage "src at $(git -C src log -1 --format='%h %s')"
rev=$(git -C src rev-parse HEAD)
# Sync once per revision. After the patches below are applied, gclient sync
# refuses to run on the modified checkout, so re-runs must skip it.
if [ "$(cat "$W/synced" 2>/dev/null)" != "$rev" ]; then
stage "gclient sync"
gclient sync --nohooks --no-history -D --shallow --revision "src@$rev" -j 8
guard
stage "runhooks"
gclient runhooks
src/build/linux/sysroot_scripts/install-sysroot.py --arch=arm64
echo "$rev" > "$W/synced"
fi
guard
cd src
# Local fixes on top of the CLs. Each is applied once; re-runs skip it.
for p in "$PATCHES"/*.patch; do
if ! git apply --reverse --check "$p" 2>/dev/null; then
git apply "$p"
stage "applied $(basename "$p")"
fi
done
mkdir -p out/XR
# Rewritten on every run: change build settings here, not in out/XR/args.gn.
cat > out/XR/args.gn <<'A'
target_os = "linux"
target_cpu = "arm64"
is_debug = false
is_official_build = false
is_component_build = false
dcheck_always_on = false
symbol_level = 0
blink_symbol_level = 0
v8_symbol_level = 0
proprietary_codecs = true
ffmpeg_branding = "Chrome"
enable_nacl = false
use_remoteexec = false
use_siso = true
treat_warnings_as_errors = false
A
stage "gn gen"
gn gen out/XR
gn args out/XR --list=enable_openxr --short | tee -a "$W/stage"
stage "build"
( while sleep 600; do guard || { pkill -u "$(id -u)" -f "(siso|ninja).*out/XR"; exit 3; }; done ) &
GUARD=$!
trap 'kill $GUARD 2>/dev/null || true' EXIT
autoninja -C out/XR chrome chrome_sandbox chrome_crashpad_handler
stage "package"
cp chrome/app/theme/chromium/product_logo_256.png out/XR/product_logo_256.png
cd out/XR
files=(chrome chrome_sandbox chrome_crashpad_handler *.pak *.bin icudtl.dat locales product_logo_256.png)
# GPU libraries aren't produced by every config; pack the ones that exist.
for f in libEGL.so libGLESv2.so libvk_swiftshader.so libvulkan.so.1 vk_swiftshader_icd.json; do
[ -e "$f" ] && files+=("$f")
done
tar -cJf "$W/chromium-xr-arm64.tar.xz" "${files[@]}"
stage "DONE $(ls -la "$W/chromium-xr-arm64.tar.xz")"
+97
View File
@@ -0,0 +1,97 @@
# Technical notes
What we found getting WebXR working on the Steam Frame, for anyone picking
this up or taking it upstream. Tested with Chromium 156.0.8071.0 (arm64),
SteamOS 0.3.0 (build 20260922.6101926) and SteamVR 2.17.10.
## Why stock Chromium on Linux has no immersive WebXR
Chromium 154 was the first release to compile OpenXR on Linux
(`enable_openxr` includes Linux). But
`content/services/isolated_xr_device/xr_runtime_provider.cc` only creates an
OpenXR device when `ENABLE_OPENXR && IS_WIN`. Nothing on Linux calls the
OpenXR code, so the linker drops it. Flathub's arm64 Chromium contains no
OpenXR loader code at all, and flags such as `--force-webxr-runtime=openxr`
can't bring it back.
The two Gerrit changes this repo builds fill that gap:
- [CL 8132979](https://chromium-review.googlesource.com/c/chromium/src/+/8132979)
creates the OpenXR device on Linux, using the Vulkan graphics binding
(`XR_KHR_vulkan_enable2`). `device::features::kOpenXR` stays off by
default, so it needs `--enable-features=OpenXR`.
- [CL 8441736](https://chromium-review.googlesource.com/c/chromium/src/+/8441736)
runs the XR device service in its own sandbox (`xr_compositing`, with the
`XrProcessPolicy` seccomp policy and a file broker), instead of requiring
`--no-sandbox`.
The build uses patch set 44 of CL 8132979, which sits on top of CL 8441736.
## The SO_PEERCRED crash
With both CLs, the XR utility process crashed inside `xrCreateInstance` on
system call 0xd1 (`getsockopt` on arm64). SteamVR's IPC client calls
`getsockopt(fd, SOL_SOCKET, SO_PEERCRED, …)` to check which process is on the
other end of its socket, and `XrProcessPolicy` refuses every `getsockopt`.
[`patches/0001-xr-sandbox-allow-getsockopt-SO_PEERCRED.patch`](../patches/0001-xr-sandbox-allow-getsockopt-SO_PEERCRED.patch)
allows exactly `SOL_SOCKET`/`SO_PEERCRED` and still returns `EPERM` for
everything else.
## Why the seccomp filter is still off
With the patch, `xrCreateInstance` gets further but fails with
`Unable to init path manager: VRInitError_Init_Internal`. SteamVR's client
reads `/proc/self/status` to find its own process ID. Inside the seccomp
sandbox, file opens go through Chrome's broker process, so `/proc/self` is
the broker's, and SteamVR registers the broker's PID instead of the XR
process's. Granting the broker `/proc/self` doesn't help, because the broker
can only answer for itself.
Fixing this properly needs a change in Chromium: for example, having the
broker client rewrite `/proc/self` to `/proc/<caller pid>`, or opening the
needed `/proc` files before the sandbox seals. Until then the launcher passes
`--disable-seccomp-filter-sandbox`. The namespace sandbox still works:
SteamOS allows unprivileged user namespaces, so the setuid `chrome_sandbox`
isn't needed.
## What a working session looks like
- The page's `isSessionSupported("immersive-vr")` resolves `true`.
- After the **Allow VR?** prompt, `requestSession("immersive-vr")` succeeds.
The first frame has a viewer pose with 2 views and a 2880 × 1440 framebuffer
(1440 × 1440 per eye).
- SteamVR's log (`~/.local/share/Steam/logs/vrserver.txt`) shows the app move
from `VRApplication_OpenXRInstance` to `VRApplication_OpenXRScene`, followed
by controller binding files being created for
`system.generated.openxr.chromium 156.chrome`.
- If nobody is wearing the headset, SteamVR keeps it in standby: the session
stays at `XR_SESSION_STATE_SYNCHRONIZED`, the page sees
`visibilityState: "hidden"`, and only the first frame runs. Put the headset
on to see it.
## Graphics
Chromium's GPU process uses ANGLE on OpenGL, which runs on zink over the
Turnip Vulkan driver (Adreno 750). Chromium's own Vulkan backend stays off;
that doesn't stop the session. The OpenXR runtime uses Vulkan.
## Panels and the Steam library
The Frame's compositor (gamescope) gives every Steam app ID its own SteamVR
overlay, named `valve.steam.desktopgame.<appid>`, which appears as a panel in
the headset. A non-Steam shortcut gets an app ID like any game, so launching
Chromium XR from the library gives it a panel. `frame/steam-shortcut.py` adds
the shortcut through the Steam client's DevTools port (`127.0.0.1:8080`,
page `SharedJSContext`) with `SteamClient.Apps.AddShortcut`, so Steam doesn't
need restarting. (`steam steam://addnonsteamgame/<path>` adds nothing.)
## Debugging
- `chromium-xr --remote-debugging-port=9223 URL` opens DevTools on the Frame's
loopback. It has no password, so close the browser when you're done. If a
VPN such as userspace Tailscale forwards traffic to loopback, other devices
can reach it.
- `chrome://gpu` and `chrome://webxr-internals` show the graphics setup and
the XR runtime Chromium picked.
- SteamVR's logs are in `~/.local/share/Steam/logs/`. `vrserver.txt` shows
the session starting and which app SteamVR bound it to.
+23
View File
@@ -0,0 +1,23 @@
#!/bin/bash
# Start the WebXR Chromium build. frame/install.sh copies this to
# ~/.local/bin/chromium-xr; the Steam library shortcut and the desktop entry
# both run it. Arguments go to Chromium, so `chromium-xr URL` opens a page.
set -euo pipefail
CHROME="${CHROMIUM_XR_HOME:-$HOME/chromium-xr}/chrome"
[[ -x "$CHROME" ]] || { echo "chromium-xr: no build at $CHROME (run frame/install.sh)" >&2; exit 1; }
# --enable-features=OpenXR: the Linux OpenXR device is off by default.
# --ozone-platform=x11: gamescope's X display, where each app is a panel.
# --no-first-run and --password-store=basic: otherwise startup can stop at a
# first-run or keyring prompt you can't see.
# --disable-seccomp-filter-sandbox: with the XR process's seccomp policy on,
# SteamVR sees the wrong process ID and refuses the session (see
# docs/technical-notes.md). The namespace sandbox stays on.
exec "$CHROME" \
--user-data-dir="$HOME/.config/chromium-xr" \
--enable-features=OpenXR \
--ozone-platform=x11 \
--no-first-run --no-default-browser-check --password-store=basic \
--disable-seccomp-filter-sandbox \
"$@"
+61
View File
@@ -0,0 +1,61 @@
#!/bin/bash
# Run on the Steam Frame (Desktop Mode terminal or SSH), as the normal user:
#
# frame/install.sh chromium-xr-arm64.tar.xz
#
# Unpacks the build into ~/chromium-xr, installs the `chromium-xr` launcher
# in ~/.local/bin, adds a desktop menu entry, and adds "Chromium XR" to the
# Steam library. Safe to rerun, e.g. to install a newer build.
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
tarball=${1:-}
[[ -n "$tarball" && -f "$tarball" ]] || { echo "usage: $0 chromium-xr-arm64.tar.xz" >&2; exit 2; }
[[ $(uname -m) == aarch64 ]] || { echo "This is for the Steam Frame (arm64); this machine is $(uname -m)." >&2; exit 1; }
dest=$HOME/chromium-xr
state=$HOME/.local/share/chromium-xr
launcher=$HOME/.local/bin/chromium-xr
echo "Unpacking into $dest"
rm -rf "$dest.new"
mkdir -p "$dest.new"
tar -xJf "$tarball" -C "$dest.new"
# Check the new build runs at all before replacing the old one.
"$dest.new/chrome" --version
rm -rf "$dest"
mv "$dest.new" "$dest"
mkdir -p "$HOME/.local/bin" "$state" "$HOME/.local/share/applications"
install -m 755 "$here/chromium-xr" "$launcher"
# Keep the uninstaller, so removing works after the repo clone is deleted.
install -m 755 "$here/uninstall.sh" "$here/steam-shortcut.py" "$state/"
icon=''
for candidate in "$dest/product_logo_256.png" \
/var/lib/flatpak/exports/share/icons/hicolor/256x256/apps/org.chromium.Chromium.png \
"$HOME/.local/share/flatpak/exports/share/icons/hicolor/256x256/apps/org.chromium.Chromium.png"; do
if [[ -f "$candidate" ]]; then icon=$candidate; break; fi
done
cat > "$HOME/.local/share/applications/chromium-xr.desktop" <<EOF
[Desktop Entry]
Type=Application
Name=Chromium XR
Comment=Chromium with WebXR (immersive VR) through SteamVR
Exec=$launcher %U
Icon=${icon:-web-browser}
Terminal=false
Categories=Network;WebBrowser;
MimeType=text/html;x-scheme-handler/http;x-scheme-handler/https;
EOF
echo "Adding Chromium XR to the Steam library"
if appid=$(python3 "$here/steam-shortcut.py" ensure "Chromium XR" "$launcher" "$HOME" "$icon" "$state/steam-appid"); then
echo "Steam shortcut ready (app id $appid)"
else
echo "Couldn't add the Steam shortcut automatically. In Desktop Mode, open Steam and use" >&2
echo "Games > Add a Non-Steam Game to My Library, then pick Chromium XR." >&2
fi
echo "Done. Open Chromium XR from your Steam library, or run: chromium-xr URL"
+163
View File
@@ -0,0 +1,163 @@
#!/usr/bin/env python3
"""Add, update or remove the "Chromium XR" shortcut in the Steam library.
Talks to the Steam client's built-in DevTools port (127.0.0.1:8080, which
SteamOS starts Steam with), so Steam doesn't need restarting. Python stdlib
only.
steam-shortcut.py ensure NAME EXE START_DIR ICON ID_FILE -> prints the app id
steam-shortcut.py remove NAME ID_FILE
"""
import base64, json, os, socket, struct, sys, urllib.request
DEVTOOLS = 'http://127.0.0.1:8080/json'
def target_ws():
for t in json.load(urllib.request.urlopen(DEVTOOLS, timeout=5)):
if t.get('title') == 'SharedJSContext':
return t['webSocketDebuggerUrl']
sys.exit('SharedJSContext not found: is the Steam client running?')
class WS:
"""Just enough RFC 6455 for one CDP request/response on loopback."""
def __init__(self, url):
host_port, path = url[len('ws://'):].split('/', 1)
host, port = host_port.split(':')
self.s = socket.create_connection((host, int(port)), timeout=20)
key = base64.b64encode(os.urandom(16)).decode()
self.s.sendall((f'GET /{path} HTTP/1.1\r\nHost: {host_port}\r\nUpgrade: websocket\r\n'
f'Connection: Upgrade\r\nSec-WebSocket-Key: {key}\r\n'
'Sec-WebSocket-Version: 13\r\n\r\n').encode())
buf = b''
while b'\r\n\r\n' not in buf:
chunk = self.s.recv(4096)
if not chunk:
raise EOFError('connection closed during the websocket handshake')
buf += chunk
if b' 101 ' not in buf.split(b'\r\n', 1)[0]:
sys.exit('websocket handshake failed')
self.rest = buf.split(b'\r\n\r\n', 1)[1]
def _read(self, n):
while len(self.rest) < n:
chunk = self.s.recv(65536)
if not chunk:
raise EOFError
self.rest += chunk
out, self.rest = self.rest[:n], self.rest[n:]
return out
def send(self, text):
data = text.encode()
mask = os.urandom(4)
n = len(data)
head = bytes([0x81]) + (bytes([0x80 | n]) if n < 126 else
bytes([0x80 | 126]) + struct.pack('>H', n) if n < 65536 else
bytes([0x80 | 127]) + struct.pack('>Q', n))
self.s.sendall(head + mask + bytes(b ^ mask[i % 4] for i, b in enumerate(data)))
def recv(self):
msg = b''
while True:
b0, b1 = self._read(2)
n = b1 & 0x7f
if n == 126:
n = struct.unpack('>H', self._read(2))[0]
elif n == 127:
n = struct.unpack('>Q', self._read(8))[0]
msg += self._read(n)
if b0 & 0x80:
return msg.decode()
def evaluate(js):
ws = WS(target_ws())
ws.send(json.dumps({'id': 1, 'method': 'Runtime.evaluate', 'params': {
'expression': js, 'awaitPromise': True, 'returnByValue': True}}))
while True:
r = json.loads(ws.recv())
if r.get('id') == 1:
break
res = r.get('result', {})
if 'exceptionDetails' in res:
sys.exit('JS error: ' + json.dumps(res['exceptionDetails'])[:500])
return res.get('result', {}).get('value')
def shortcuts():
return evaluate('''(() => appStore.allApps.filter(a => a.app_type === 1073741824)
.map(a => ({appid: a.appid, name: a.display_name})))()''')
def read_id(path):
try:
with open(path) as f:
return int(f.read().strip())
except (OSError, ValueError):
return None
def ensure(name, exe, start_dir, icon, id_file):
saved = read_id(id_file)
apps = shortcuts()
# Match the saved app id first, so renaming the shortcut in Steam
# doesn't make a rerun add a second one.
found = ([a['appid'] for a in apps if a['appid'] == saved] or
[a['appid'] for a in apps if a['name'] == name])
if found:
appid = found[0]
evaluate(f'''(() => {{
SteamClient.Apps.SetShortcutExe({appid}, {json.dumps(exe)});
SteamClient.Apps.SetShortcutStartDir({appid}, {json.dumps(start_dir)});
if ({json.dumps(icon)}) SteamClient.Apps.SetShortcutIcon({appid}, {json.dumps(icon)});
}})()''')
else:
appid = evaluate(f'''(async () => {{
const id = await SteamClient.Apps.AddShortcut({json.dumps(name)}, {json.dumps(exe)}, "", "");
SteamClient.Apps.SetShortcutName(id, {json.dumps(name)});
SteamClient.Apps.SetShortcutStartDir(id, {json.dumps(start_dir)});
if ({json.dumps(icon)}) SteamClient.Apps.SetShortcutIcon(id, {json.dumps(icon)});
return id;
}})()''')
if not isinstance(appid, int) or appid <= 0:
sys.exit(f'Steam did not return a shortcut app id: {appid!r}')
os.makedirs(os.path.dirname(id_file), exist_ok=True)
with open(id_file, 'w') as f:
f.write(f'{appid}\n')
print(appid)
def remove(name, id_file):
saved = read_id(id_file)
apps = shortcuts()
# The saved app id, plus any shortcut with the name (a lost id file, or one
# added by hand).
found = [a['appid'] for a in apps if a['appid'] == saved or a['name'] == name]
for appid in found:
evaluate(f'SteamClient.Apps.RemoveShortcut({appid})')
print(f'removed Steam shortcut {appid}')
if not found:
print('no Steam shortcut to remove')
if saved is not None:
os.remove(id_file)
def main():
if len(sys.argv) == 7 and sys.argv[1] == 'ensure':
action = lambda: ensure(*sys.argv[2:])
elif len(sys.argv) == 4 and sys.argv[1] == 'remove':
action = lambda: remove(*sys.argv[2:])
else:
sys.exit(__doc__)
try:
action()
except (OSError, ValueError, KeyError, EOFError) as e:
sys.exit(f"Couldn't update the Steam shortcut ({type(e).__name__}: {e}). "
"Is the Steam client running?")
if __name__ == '__main__':
main()
+31
View File
@@ -0,0 +1,31 @@
#!/bin/bash
# Run on the Steam Frame to remove what frame/install.sh added: the build,
# the launcher, the menu entry and the Steam shortcut. Your Chromium XR
# profile (~/.config/chromium-xr: settings, logins, history) is kept unless
# you pass --remove-profile.
#
# install.sh keeps a copy of this script in ~/.local/share/chromium-xr, so it
# works even after the repo clone is deleted.
set -euo pipefail
remove_profile=false
[[ $# -le 1 ]] || { echo "usage: $0 [--remove-profile]" >&2; exit 2; }
case "${1:-}" in
'') ;;
--remove-profile) remove_profile=true ;;
*) echo "usage: $0 [--remove-profile]" >&2; exit 2 ;;
esac
here=$(cd "$(dirname "$0")" && pwd)
state=$HOME/.local/share/chromium-xr
python3 "$here/steam-shortcut.py" remove "Chromium XR" "$state/steam-appid" ||
echo "Couldn't remove the Steam shortcut; delete Chromium XR from the library by hand." >&2
rm -rf "$HOME/chromium-xr" "$state"
rm -f "$HOME/.local/bin/chromium-xr" "$HOME/.local/share/applications/chromium-xr.desktop"
if $remove_profile; then
rm -rf "$HOME/.config/chromium-xr"
echo "Chromium XR and its profile removed."
else
echo "Chromium XR removed. Your profile is still in ~/.config/chromium-xr."
fi
@@ -0,0 +1,40 @@
diff --git a/sandbox/policy/linux/bpf_xr_policy_linux.cc b/sandbox/policy/linux/bpf_xr_policy_linux.cc
index 435e13d396..297453f582 100644
--- a/sandbox/policy/linux/bpf_xr_policy_linux.cc
+++ b/sandbox/policy/linux/bpf_xr_policy_linux.cc
@@ -11,6 +11,7 @@
#include "sandbox/linux/system_headers/linux_syscalls.h"
#include "sandbox/policy/linux/sandbox_linux.h"
+using sandbox::bpf_dsl::AllOf;
using sandbox::bpf_dsl::Allow;
using sandbox::bpf_dsl::Arg;
using sandbox::bpf_dsl::Error;
@@ -27,8 +28,8 @@ XrProcessPolicy::~XrProcessPolicy() = default;
ResultExpr XrProcessPolicy::EvaluateSyscall(int system_call_number) const {
switch (system_call_number) {
// The runtime reaches its compositor over an AF_UNIX socket and passes fds
- // with SCM_RIGHTS, neither of which the GPU policy allows. get/setsockopt
- // stay disallowed; add a narrow level/optname restriction if ever needed.
+ // with SCM_RIGHTS, neither of which the GPU policy allows. setsockopt
+ // stays disallowed; getsockopt is limited to SO_PEERCRED below.
#if defined(__NR_getpeername)
case __NR_getpeername:
#endif
@@ -49,6 +50,16 @@ ResultExpr XrProcessPolicy::EvaluateSyscall(int system_call_number) const {
case __NR_get_robust_list:
#endif
return Allow();
+#if defined(__NR_getsockopt)
+ case __NR_getsockopt: {
+ // SteamVR's IPC client checks who is on the other end of its socket
+ // with SO_PEERCRED. Nothing else is readable.
+ const Arg<int> level(1);
+ const Arg<int> optname(2);
+ return If(AllOf(level == SOL_SOCKET, optname == SO_PEERCRED), Allow())
+ .Else(Error(EPERM));
+ }
+#endif
#if defined(__NR_kill)
case __NR_kill: {
// SteamVR probes its sibling processes for liveness with kill(pid, 0).
+14
View File
@@ -0,0 +1,14 @@
# Patches
Local changes applied on top of Chromium
[CL 8132979](https://chromium-review.googlesource.com/c/chromium/src/+/8132979)
and [CL 8441736](https://chromium-review.googlesource.com/c/chromium/src/+/8441736)
by `build/build.sh`, in file-name order.
- `0001-xr-sandbox-allow-getsockopt-SO_PEERCRED.patch`: lets the XR process
call `getsockopt(SOL_SOCKET, SO_PEERCRED)`, which SteamVR's OpenXR runtime
needs. See [docs/technical-notes.md](../docs/technical-notes.md).
These patches modify Chromium source and are under
[Chromium's BSD-style license](https://chromium.googlesource.com/chromium/src/+/main/LICENSE),
Copyright The Chromium Authors.