Test against Valve's own Steam Frame OS from its recovery image

tests/frame-container/frame-image.sh extracts rootfs-A from Valve's Frame
recovery image (steamdeck-images.steamos.cloud/recovery), mounts it read-only
with a throwaway writable layer and starts the image's own sshd, so the iPhone
app can pair with and run its server on the real SteamOS for Frame userland.
Verified: password pairing then key login in the image's sshd log, the power
password check through the image's sudo, status reading SteamOS 0.3.0.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
saphidandClaude Opus 5.5 committed 2026-09-27 16:59:01 +10:00
1 parent b1fa3afd40
commit d65f7130d5
3 files changed
+62 -5

No files matched your search

+4 -3
View File
@@ -78,9 +78,10 @@ screenshots, a file upload (checked on the Frame), a background install job, and
the power password check (a wrong password is refused). The server on the Frame
exits within seconds of the app closing.
Against a stand-in built on Valve's Holo Core aarch64 base
([tests/frame-container](../tests/frame-container)), since Valve publishes no
Frame OS image: pairing with the password (key added with the right
Against Valve's own Steam Frame OS (SteamOS 0.3.0 build 20260922.5152327, the
`rootfs-A` partition of the Frame recovery image, run with its own sshd; see
[tests/frame-container](../tests/frame-container)), and a Holo Core stand-in:
pairing with the password (key added with the right
permissions, host key pinned, password stored nowhere), the power password
check (a wrong or missing password refused; the right one reaches `systemctl`),
a changed host key refused with "Pair with the Frame again", and a wrong
+23 -2
View File
@@ -1,6 +1,27 @@
# A Frame stand-in for testing without the headset
# Testing without the headset
Valve publishes no Steam Frame OS image. This builds the closest thing: Valve and
## Valve's own Frame OS, from its recovery image
Valve publishes a Steam Frame recovery image at
https://steamdeck-images.steamos.cloud/recovery/ (`steamframe-oobe-repair-*.img.bz2`,
~4 GB). `frame-image.sh` extracts its `rootfs-A` partition (btrfs), mounts it
read-only with a throwaway writable layer, and starts the image's own sshd on
port 2223, so the iPhone app can pair with, and run its server on, the real
SteamOS for Frame userland (Python, sudo, sshd, PAM). It needs Linux with btrfs,
e.g. Colima's VM on a Mac:
```sh
colima start --arch aarch64 --vm-type vz
colima ssh -- sudo sh tests/frame-container/frame-image.sh ~/Downloads/steamframe-oobe-repair-<build>.img.bz2
# pair with 127.0.0.1:2223, user steamos, password frame-test-pw
```
The image's kernel is built for the Frame's Qualcomm chip, so this runs its
userland, not the whole OS: no SteamVR, Steam client, battery or Lepton.
## Holo Core stand-in
A lighter option: Valve and
Collabora's [Holo Core aarch64 preview](https://www.collabora.com/news-and-blog/news-and-events/building-an-arch-linux-aarch64-port-for-holo-core.html)
(the Arch Linux ARM64 base the Frame's SteamOS is built on) with the Frame's SSH
surface: a `steamos` user with a password and sudo, OpenSSH taking keys and
+35
View File
@@ -0,0 +1,35 @@
#!/bin/sh
# Run Valve's own Steam Frame OS, from its recovery image, as an SSH target for
# testing (see README.md). Run on a Linux host or VM with btrfs, as root:
# frame-image.sh steamframe-oobe-repair-<build>.img.bz2
# It extracts the rootfs-A partition, mounts it read-only, adds a throwaway
# writable layer, and starts the image's own sshd on port 2223 (user steamos,
# password frame-test-pw). systemctl only records what it's asked.
set -e
command -v bzcat >/dev/null && command -v python3 >/dev/null || {
command -v apt-get >/dev/null && apt-get install -y -qq bzip2 python3 >/dev/null; }
IMG=${1:?recovery .img.bz2}; RAW=${RAW:-$(dirname "$IMG")/frame-rootfs-A.img}
if [ ! -f "$RAW" ]; then
# Partition 3 (rootfs-A) from the image's GPT: start and size in 512-byte sectors.
set -- $(bzcat "$IMG" | head -c 1048576 | python3 -c '
import struct,sys; d=sys.stdin.buffer.read(); e=d[1024+2*128:1024+3*128]
a,b=struct.unpack("<QQ",e[32:48]); print(a, b-a+1)')
bzcat "$IMG" | tail -c +$(( $1 * 512 + 1 )) | head -c $(( $2 * 512 )) > "$RAW"
fi
R=/mnt/frame; O=/var/lib/frame-ovl; M=/srv/frame
mkdir -p $R $O/upper $O/work $M
mountpoint -q $R || mount -o ro -t btrfs "$(losetup -f --show -r "$RAW")" $R
mountpoint -q $M || mount -t overlay overlay -o lowerdir=$R,upperdir=$O/upper,workdir=$O/work $M
for d in proc sys dev dev/pts; do mountpoint -q $M/$d || mount --rbind /$d $M/$d; done
mountpoint -q $M/run || mount -t tmpfs tmpfs $M/run
mountpoint -q $M/tmp || mount -t tmpfs tmpfs $M/tmp
mkdir -p $M/run/sshd $M/home/steamos $M/usr/local/bin
chroot $M chown 1000:1000 /home/steamos
echo 'steamos:frame-test-pw' | chroot $M chpasswd
chroot $M ssh-keygen -A >/dev/null
# No systemd here: systemctl records power requests instead of acting.
printf '#!/bin/sh\necho "systemctl $*" >> /tmp/power-requests.log\n' > $M/usr/local/bin/systemctl
chmod +x $M/usr/local/bin/systemctl
pkill -f "[s]shd -p 2223" 2>/dev/null || true
chroot $M /usr/bin/sshd -p 2223 -E /tmp/sshd.log
echo up