Files
StarPilot/openpilot/selfdrive/ui/layouts/arabesque/tools/browser_session.py
T
2026-10-28 05:08:17 -04:00

782 lines
34 KiB
Python

"""On-demand Firefox owner and isolated worker. The worker uses only the standard library."""
import array
import ctypes as C
import fcntl
import importlib.util
import json
import mmap
import os
from pathlib import Path
import select
import signal
import socket
import stat
import struct
import subprocess
import sys
from threading import Event, Thread
import time
import uuid
ROOT = Path('/data/arabesque-firefox')
RUNTIME = ROOT / 'runtime'
STATE = ROOT / 'state'
BROWSER_UID = 61234
# Linux UAPI constants also work with Python builds made against pre-memfd libc headers.
ADD_SEALS, GET_SEALS, SEALS = 1033, 1034, 7
LEASE_SECONDS = 2.0
MAX_PACKET = 4096
def packet(sock, message, fds=()):
data = json.dumps(message, separators=(',', ':'), ensure_ascii=True).encode('ascii')
if len(data) > MAX_PACKET:
raise ValueError('Browser control packet too large')
rights = [(socket.SOL_SOCKET, socket.SCM_RIGHTS, array.array('i', fds))] if fds else []
if sock.sendmsg([data], rights) != len(data):
raise RuntimeError('Incomplete browser packet')
def receive(sock, fd_count=0):
data, ancillary, flags, _ = sock.recvmsg(MAX_PACKET, socket.CMSG_SPACE(8 * array.array('i').itemsize), socket.MSG_CMSG_CLOEXEC)
fds = []
try:
for level, kind, payload in ancillary:
if (level, kind) != (socket.SOL_SOCKET, socket.SCM_RIGHTS):
raise ValueError('Unexpected browser ancillary data')
values = array.array('i')
values.frombytes(payload)
fds.extend(values)
if not data:
raise EOFError('Browser connection closed')
if flags & (socket.MSG_TRUNC | socket.MSG_CTRUNC) or len(fds) != fd_count:
raise ValueError('Truncated browser packet or unexpected descriptors')
message = json.loads(data)
if not isinstance(message, dict) or not isinstance(message.get('type'), str):
raise ValueError('Invalid browser control message')
return message, fds
except BaseException:
for fd in fds:
os.close(fd)
raise
def credentials(sock):
return struct.unpack('3i', sock.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, struct.calcsize('3i')))
def geometry(width, height, fps):
if type(width) is not int or type(height) is not int or not 320 <= width <= 1600 or not 240 <= height <= 1200:
raise ValueError('Invalid browser geometry')
if type(fps) is not int or not 1 <= fps <= 30:
raise ValueError('Invalid browser frame rate')
return width * height * 4
def sealed_slot(size):
libc = C.CDLL(None, use_errno=True)
try:
create = libc.memfd_create
except AttributeError:
raise RuntimeError('memfd_create is unavailable') from None
create.argtypes, create.restype = [C.c_char_p, C.c_uint], C.c_int
fd = create(b'arabesque-frame', 3) # MFD_CLOEXEC | MFD_ALLOW_SEALING
if fd < 0:
raise OSError(C.get_errno(), 'memfd_create failed')
try:
os.ftruncate(fd, size)
fcntl.fcntl(fd, ADD_SEALS, SEALS)
if fcntl.fcntl(fd, GET_SEALS) != SEALS:
raise RuntimeError('Cannot seal browser frame size')
return fd
except BaseException:
os.close(fd)
raise
def launch_command(unit, address, width, height, fps, capture):
bridge = Path(__file__).resolve().parent
# Mask user/vehicle data, then expose only the immutable runtime, Python and the browser's own state.
binds = {str(RUNTIME), str(Path(sys.prefix).resolve()), str(Path(sys.base_prefix).resolve())}
empty, devices = RUNTIME / 'isolation/empty', RUNTIME / 'isolation/devices'
properties = [
'Type=notify', 'NotifyAccess=main', 'TimeoutStartSec=12', 'WatchdogSec=4', 'WatchdogSignal=SIGTERM', 'TimeoutAbortSec=3',
'KillMode=mixed', 'TimeoutStopSec=3', 'SendSIGKILL=yes', 'Restart=no', 'LimitCORE=0',
'NoNewPrivileges=yes', 'CapabilityBoundingSet=CAP_SETUID CAP_SETGID',
'ProtectSystem=strict', 'PrivateTmp=yes',
'ProtectControlGroups=yes', 'UMask=0077',
# ProtectKernelTunables also installs seccomp; retain its mount restrictions without hiding Firefox's filter.
'InaccessiblePaths=-/proc/kallsyms -/proc/kcore',
'TemporaryFileSystem=/data:ro /persist:ro /mnt:ro /media:ro /cache:ro /rwtmp:ro /var:ro /dev/shm:rw,nosuid,nodev,noexec,mode=1777',
'BindReadOnlyPaths=' + ' '.join(sorted(binds)) + f' {bridge}:/tmp/arabesque-bridge '
+ f'{empty}:/home {empty}:/root {empty}:/run/user {devices}:/dev '
+ '/sys /proc/sys -/proc/acpi -/proc/apm -/proc/asound -/proc/bus -/proc/fs -/proc/irq '
+ '-/proc/latency_stats -/proc/mtrr -/proc/scsi -/proc/sysrq-trigger -/proc/timer_stats',
f'BindPaths={STATE} /dev/null /dev/zero /dev/random /dev/urandom', f'ReadWritePaths={STATE}', 'WorkingDirectory=/tmp',
# This kernel has no CFS bandwidth or pids controller; use fair shares and the per-UID process limit.
'CPUAffinity=0-5', 'CPUSchedulingPolicy=other', 'Nice=10', 'CPUWeight=10', 'MemoryMax=1G', 'LimitNPROC=1024',
'StandardOutput=null', 'StandardError=journal',
]
python = str(Path(sys.executable).absolute())
# Use the real interpreter; a venv symlink may point through the masked /home tree.
python = str(Path(python).resolve())
command = ['sudo', '-n', '/usr/bin/timeout', '--kill-after=1', '15', '/usr/bin/systemd-run', '--quiet', '--collect', f'--unit={unit}']
command += [f'--property={value}' for value in properties]
command += ['/usr/bin/setpriv', f'--reuid={BROWSER_UID}', f'--regid={BROWSER_UID}', '--clear-groups', '--no-new-privs',
python, '-I', '-u', '-c',
"import sys; sys.path.insert(0, '/tmp/arabesque-bridge'); from browser_session import worker_main; worker_main()",
address, str(os.getuid()), str(os.getpid()), os.readlink('/proc/self/ns/mnt'),
str(width), str(height), str(fps), capture]
return command
def preparation_command(unit, address):
bridge = Path(__file__).resolve().parent
properties = [
'Type=notify', 'NotifyAccess=main', 'TimeoutStartSec=12', 'WatchdogSec=4', 'WatchdogSignal=SIGTERM',
'TimeoutAbortSec=3', 'KillMode=mixed', 'TimeoutStopSec=3', 'Restart=no', 'RuntimeMaxSec=900', 'LimitCORE=0',
# /data is owned by comma; only this mkdir runs outside the preparer's capability bound.
f'ExecStartPre=-+/usr/bin/mkdir -m 0755 {ROOT}',
'NoNewPrivileges=yes', 'CapabilityBoundingSet=CAP_CHOWN', 'ProtectSystem=strict', 'ReadWritePaths=/data',
'PrivateTmp=yes', 'ProtectKernelTunables=yes', 'ProtectControlGroups=yes', 'UMask=0022', 'WorkingDirectory=/tmp',
'CPUAffinity=0-5', 'CPUSchedulingPolicy=other', 'Nice=10', 'MemoryMax=1G', 'StandardOutput=null', 'StandardError=journal',
]
command = ['sudo', '-n', '/usr/bin/timeout', '--kill-after=1', '15', '/usr/bin/systemd-run', '--quiet', '--collect', f'--unit={unit}']
command += [f'--property={value}' for value in properties]
command += [str(Path(sys.executable).resolve()), '-I', '-u', '-c',
f'import sys; sys.path.insert(0, {str(bridge)!r}); from browser_session import preparation_main; preparation_main()',
address, str(os.getuid()), str(os.getpid())]
return command
class BrowserSession:
pending_stop = None
def __init__(self, width, height, fps, capture='auto'):
self.width, self.height, self.fps = width, height, fps
self.size = geometry(width, height, fps)
if capture not in ('auto', 'reference', 'shm'):
raise ValueError('Invalid capture mode')
self._capture = capture
self.error, self.status = '', 'Checking isolated Firefox runtime…'
self.ready = False
self._listener = self._socket = self._launcher = None
self._fds, self.buffers, self._frames = [], [], []
self._unit = ''
self._started = self._heartbeat = time.monotonic()
self._preparing = self._next_start = False
self.stopped = Event()
self.stopped.set()
self._start()
def _start(self):
try:
if ROOT.is_symlink() or RUNTIME.is_symlink():
raise RuntimeError('Firefox runtime path must not be a symlink')
self._preparing = not RUNTIME.exists()
paths = (ROOT, RUNTIME, STATE, RUNTIME / 'firefox/firefox')
if self._preparing:
paths = (ROOT,) if ROOT.exists() else ()
for path in paths:
info = path.stat()
if info.st_uid != (BROWSER_UID if path == STATE else 0) or info.st_mode & 0o022:
raise RuntimeError('Firefox runtime has unsafe ownership or permissions')
if not self._preparing and not (RUNTIME / 'ready.json').is_file():
raise RuntimeError('Firefox runtime is not prepared')
self._listener = socket.socket(socket.AF_UNIX, socket.SOCK_SEQPACKET | socket.SOCK_CLOEXEC)
address = 'arabesque-firefox-' + uuid.uuid4().hex
self._listener.bind('\0' + address)
self._listener.listen(1)
self._listener.setblocking(False)
if not self._preparing:
for _ in range(2):
fd = sealed_slot(self.size)
self._fds.append(fd)
self.buffers.append(mmap.mmap(fd, self.size, access=mmap.ACCESS_READ))
self._unit = 'arabesque-firefox-' + uuid.uuid4().hex
self.stopped = Event()
self._started = self._heartbeat = time.monotonic()
self.status = 'Preparing Firefox for first use…' if self._preparing else 'Starting Firefox…'
command = (preparation_command(self._unit, address) if self._preparing else
launch_command(self._unit, address, self.width, self.height, self.fps, self._capture))
self._launcher = subprocess.Popen(command,
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
except (OSError, ValueError, RuntimeError) as e:
self.error = f'{e}. Close Firefox and reopen it to retry.'
self.close()
def send(self, message):
if self._socket is not None:
try:
packet(self._socket, message)
return True
except (OSError, ValueError, RuntimeError) as e:
self.fail(str(e))
return False
def fail(self, error):
self.error = error
self.close()
def poll(self):
if self._next_start:
if not self.stopped.is_set():
return
self._next_start = False
self._start()
if not self._unit:
return
try:
now = time.monotonic()
if self._socket is None:
try:
peer, _ = self._listener.accept()
except BlockingIOError:
if now - self._started > 12 or self._launcher.poll() not in (None, 0):
raise RuntimeError('Isolated Firefox could not start; check the transient unit journal') from None
return
if credentials(peer)[1] != (0 if self._preparing else BROWSER_UID):
peer.close()
raise RuntimeError('Unexpected Firefox worker identity')
self._socket = peer
peer.setblocking(False)
if self._preparing:
packet(peer, {'type': 'prepare'})
else:
packet(peer, {'type': 'init', 'width': self.width, 'height': self.height, 'fps': self.fps}, self._fds)
self._listener.close()
self._listener = None
if not self._preparing and not self.ready and now - self._started > 45:
raise TimeoutError('Firefox startup checks timed out')
if now - self._heartbeat >= 0.25:
if not self.send({'type': 'lease'}):
return
self._heartbeat = now
for _ in range(8):
try:
message, _ = receive(self._socket)
except BlockingIOError:
break
if self._preparing and message['type'] == 'status' and set(message) == {'type', 'message'} and isinstance(message['message'], str):
self.status = message['message'][:512]
elif self._preparing and message == {'type': 'prepared'}:
self.close()
self._next_start = True
self.status = 'Starting Firefox…'
return
elif not self._preparing and message['type'] == 'ready' and set(message) == {'type', 'capture'} and message['capture'] in ('shm', 'reference'):
self.ready = True
self.status = 'MIT-SHM FD' if message['capture'] == 'shm' else 'GetImage reference capture'
elif message['type'] == 'frame' and set(message) == {'type', 'slot'} and type(message['slot']) is int:
slot = message['slot']
if not self.ready or slot not in (0, 1) or slot in self._frames:
raise ValueError('Invalid Firefox frame lease')
self._frames.append(slot)
elif message['type'] == 'error' and set(message) == {'type', 'message'} and isinstance(message['message'], str):
raise RuntimeError(message['message'][:512])
else:
raise ValueError('Unexpected Firefox worker response')
except (OSError, ValueError, RuntimeError, EOFError) as e:
self.fail(str(e))
def take_frame(self):
if not self._frames:
return None
frames, self._frames = self._frames, []
for slot in frames[:-1]:
self.release(slot)
return (frames[-1], self.buffers[frames[-1]]) if self.ready else None
def release(self, slot):
self.send({'type': 'release', 'slot': slot})
def reset_input(self):
self.send({'type': 'reset'})
def close(self):
self._next_start = False
if self._socket is not None:
try:
packet(self._socket, {'type': 'close'})
except OSError:
pass
self._socket.close()
self._socket = None
if self._listener is not None:
self._listener.close()
self._listener = None
if self._unit:
# Reap both commands away from the UI thread; PID 1 stops every grandchild before allowing a resume.
BrowserSession.pending_stop = self.stopped
Thread(target=stop_unit, args=(self._unit, self._launcher, self.stopped), daemon=True, name='firefox-stop').start()
self._unit = ''
if self._launcher is not None:
self._launcher.poll()
self.ready = False
self._frames = []
for buffer in self.buffers:
buffer.close()
self.buffers = []
for fd in self._fds:
os.close(fd)
self._fds = []
def stop_unit(unit, launcher, stopped):
try:
subprocess.run(['sudo', '-n', '/usr/bin/timeout', '--kill-after=1', '4', '/usr/bin/systemctl', 'stop', unit],
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=6, check=False)
except (OSError, subprocess.TimeoutExpired):
pass # EOF, the owner lease and PID 1's watchdog remain independent cleanup paths.
finally:
try:
if launcher is not None:
launcher.wait(timeout=12)
except (OSError, subprocess.TimeoutExpired):
pass
finally:
stopped.set()
def status(pid):
return dict(line.split(':', 1) for line in Path(f'/proc/{pid}/status').read_text().splitlines() if ':' in line)
def no_new_privileges():
libc = C.CDLL(None, use_errno=True)
libc.prctl.argtypes = [C.c_int, C.c_ulong, C.c_ulong, C.c_ulong, C.c_ulong]
libc.prctl.restype = C.c_int
result = libc.prctl(39, 0, 0, 0, 0) # PR_GET_NO_NEW_PRIVS; /proc omits this field on AGNOS's kernel.
if result not in (0, 1):
raise OSError(C.get_errno(), 'Cannot query no-new-privileges')
return result
def check_isolation(host_mount):
current = status('self')
if (os.getresuid() != (BROWSER_UID,) * 3 or os.getresgid() != (BROWSER_UID,) * 3 or os.getgroups() or
any(int(current.get(key, '0').strip(), 16) for key in ('CapEff', 'CapPrm', 'CapInh', 'CapAmb')) or
no_new_privileges() != 1):
raise RuntimeError('Firefox worker privilege drop failed')
# Do not install systemd syscall filters: on kernel 4.9 that would hide whether Firefox itself enabled seccomp.
if current.get('Seccomp', '').strip() != '0':
raise RuntimeError('Cannot independently verify Firefox seccomp with an inherited filter')
if os.readlink('/proc/self/ns/mnt') == host_mount:
raise RuntimeError('Firefox filesystem isolation is missing')
if any(Path(path).exists() for path in ('/data/params', '/persist/comma', str(ROOT / '.isolation-probe'))):
raise RuntimeError('Firefox can see host vehicle data')
mounts = {fields[4]: (fields[5], fields[fields.index('-') + 1]) for line in Path('/proc/self/mountinfo').read_text().splitlines()
if (fields := line.split())}
for path in ('/data', '/persist', '/mnt', '/media', '/cache', '/rwtmp', '/var'):
if path not in mounts or mounts[path][1] != 'tmpfs' or 'ro' not in mounts[path][0].split(','):
raise RuntimeError(f'Firefox host-data mask missing: {path}')
for path in ('/home', '/root', '/run/user'):
if (not os.path.samefile(path, RUNTIME / 'isolation/empty') or os.listdir(path) or
not os.statvfs(path).f_flag & os.ST_RDONLY):
raise RuntimeError(f'Firefox home mask missing: {path}')
if (not os.path.samefile('/dev', RUNTIME / 'isolation/devices') or
set(os.listdir('/dev')) != {'null', 'zero', 'random', 'urandom', 'shm'} or mounts.get('/dev/shm', ('', ''))[1] != 'tmpfs'):
raise RuntimeError('Firefox private device tree is missing')
for name, minor in (('null', 3), ('zero', 5), ('random', 8), ('urandom', 9)):
device = os.stat('/dev/' + name)
if not stat.S_ISCHR(device.st_mode) or device.st_rdev != os.makedev(1, minor):
raise RuntimeError(f'Firefox device binding is invalid: {name}')
if 6 in os.sched_getaffinity(0) or os.sched_getscheduler(0) != os.SCHED_OTHER:
raise RuntimeError('Firefox inherited display scheduling')
for path in ('/sys', '/proc/sys', '/proc/bus', '/proc/fs', '/proc/irq'):
if not os.statvfs(path).f_flag & os.ST_RDONLY:
raise RuntimeError(f'Firefox kernel interface is writable: {path}')
if not os.statvfs(RUNTIME).f_flag & os.ST_RDONLY:
raise RuntimeError('Firefox application is writable')
probe = STATE / '.write-probe'
probe.write_text('ok')
probe.unlink()
def enough_space():
disk = os.statvfs(STATE)
return disk.f_bavail * disk.f_frsize >= max(5 * 1024**3, disk.f_blocks * disk.f_frsize // 10)
def content_sandboxed(parent):
"""Require a Firefox browser-content child, rather than a GPU/utility helper, with active seccomp."""
# AGNOS disables CONFIG_CHECKPOINT_RESTORE, so /proc/task/*/children does not exist.
groups = Path(f'/proc/{parent}/cgroup').read_text().splitlines()
group = next((line[3:] for line in groups if line.startswith('0::')), None)
if group is None:
raise RuntimeError('Firefox process verification requires its unified cgroup')
members = (Path('/sys/fs/cgroup') / group.lstrip('/') / 'cgroup.procs').read_text().split()
inherited_nnp = no_new_privileges() == 1
for pid in members:
try:
# Firefox rewrites its process title into one space-separated argv entry after fork.
args = Path(f'/proc/{pid}/cmdline').read_bytes().replace(b'\0', b' ').split()
info = status(pid)
if b'-parentPid' not in args or args[args.index(b'-parentPid') + 1:][:1] != [str(parent).encode()]:
continue
# This worker launches Firefox directly; no-new-privileges is inherited and cannot be unset by descendants.
child_nnp = info['NoNewPrivs'].strip() == '1' if 'NoNewPrivs' in info else inherited_nnp
if (b'-contentproc' in args and b'-isForBrowser' in args and args[-1] == b'tab' and
info.get('Seccomp', '').strip() == '2' and
child_nnp and info.get('Uid', '').split()[:1] == [str(os.getuid())] and not int(info['CapEff'].strip(), 16)):
return True
except (OSError, ValueError):
continue
return False
def stop_process(process):
if process is not None and process.poll() is None:
process.terminate()
try:
process.wait(timeout=1)
except subprocess.TimeoutExpired:
process.kill()
process.wait(timeout=1)
def stop_firefox(process, x):
if x is not None and process is not None and process.poll() is None:
try:
x.key(ord('q'), ctrl=True)
x.flush(x.connection)
deadline = time.monotonic() + 1.5
while process.poll() is None and time.monotonic() < deadline:
time.sleep(0.02)
except (OSError, RuntimeError, ValueError):
pass # PID 1 still bounds shutdown if Firefox is hung or a dialog prevents quitting.
stop_process(process)
def notify(message):
address = os.environ.get('NOTIFY_SOCKET', '')
if not address:
raise RuntimeError('Firefox transient supervisor is missing')
with socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM | socket.SOCK_CLOEXEC) as sock:
sock.settimeout(0.1)
sock.sendto(message.encode('ascii'), '\0' + address[1:] if address.startswith('@') else address)
def preparation_main():
address, owner_uid, owner_pid = sys.argv[1:]
cancel, finished = Event(), Event()
progress: dict[str, str] = {'status': 'Preparing Firefox for first use…'}
thread = None
sock = socket.socket(socket.AF_UNIX, socket.SOCK_SEQPACKET | socket.SOCK_CLOEXEC)
sock.settimeout(5)
signal.signal(signal.SIGTERM, lambda *_: cancel.set())
signal.signal(signal.SIGINT, lambda *_: cancel.set())
def report(message):
if cancel.is_set():
raise InterruptedError('Firefox preparation cancelled')
progress['status'] = message[:512]
def prepare():
try:
path = Path(__file__).resolve().parents[5] / 'tools/arabesque_firefox/setup.py'
spec = importlib.util.spec_from_file_location('firefox_setup', path)
if spec is None or spec.loader is None:
raise RuntimeError('Firefox preparation is unavailable')
setup = importlib.util.module_from_spec(spec)
spec.loader.exec_module(setup)
setup.ROOT, setup.report = ROOT, report
setup.prepare()
except Exception as e:
progress['error'] = str(e)[:512]
finally:
finished.set()
try:
sock.connect('\0' + address)
if credentials(sock)[:2] != (int(owner_pid), int(owner_uid)):
raise RuntimeError('Firefox preparation owner identity mismatch')
message, _ = receive(sock)
if message != {'type': 'prepare'}:
raise ValueError('Invalid Firefox preparation handshake')
sock.setblocking(False)
notify('READY=1\nWATCHDOG=1')
thread = Thread(target=prepare, daemon=True, name='firefox-prepare')
thread.start()
last_lease = last_watchdog = time.monotonic()
status_sent = ''
while not cancel.is_set():
now = time.monotonic()
if now - last_lease > LEASE_SECONDS:
raise TimeoutError('Firefox preparation owner stopped responding')
if now - last_watchdog >= 1:
notify('WATCHDOG=1')
last_watchdog = now
for _ in range(32):
try:
message, _ = receive(sock)
except BlockingIOError:
break
if message == {'type': 'lease'}:
last_lease = now
elif message == {'type': 'close'}:
cancel.set()
break
else:
raise ValueError('Invalid Firefox preparation command')
if cancel.is_set():
break
if finished.is_set():
if failure := progress.get('error'):
raise RuntimeError(failure)
packet(sock, {'type': 'prepared'})
break
status = progress['status']
if status != status_sent:
packet(sock, {'type': 'status', 'message': status})
status_sent = status
select.select([sock], [], [], 0.1)
except (OSError, ValueError, RuntimeError, EOFError) as e:
print(f'Firefox preparation: {str(e)[:512]}', file=sys.stderr, flush=True)
try:
packet(sock, {'type': 'error', 'message': str(e)[:512]})
except OSError:
pass
finally:
cancel.set()
if thread is not None:
thread.join(timeout=1)
sock.close()
def worker_main():
# This is the first Python code executed after systemd's mount sandbox and setpriv.
from browser_x11 import X11
address, owner_uid, owner_pid, host_mount, width, height, fps, mode = sys.argv[1:]
width, height, fps = int(width), int(height), int(fps)
size = geometry(width, height, fps)
sock = socket.socket(socket.AF_UNIX, socket.SOCK_SEQPACKET | socket.SOCK_CLOEXEC)
sock.settimeout(5)
xvfb = firefox = x = None
buffers, fds = [], []
stopping = False
def terminate(*_):
nonlocal stopping
stopping = True
signal.signal(signal.SIGTERM, terminate)
signal.signal(signal.SIGINT, terminate)
try:
sock.connect('\0' + address)
if credentials(sock)[:2] != (int(owner_pid), int(owner_uid)):
raise RuntimeError('Firefox owner identity mismatch')
message, fds = receive(sock, 2)
if message != {'type': 'init', 'width': width, 'height': height, 'fps': fps}:
raise ValueError('Firefox geometry handshake failed')
check_isolation(host_mount)
if not enough_space():
raise RuntimeError('Insufficient free storage for Firefox')
for fd in fds:
info = os.fstat(fd)
if not stat.S_ISREG(info.st_mode) or info.st_size != size or fcntl.fcntl(fd, GET_SEALS) != SEALS:
raise ValueError('Invalid Firefox shared frame descriptor')
buffers.append(mmap.mmap(fd, size, access=mmap.ACCESS_WRITE))
notify('READY=1\nWATCHDOG=1')
env = {'PATH': '/usr/bin:/bin', 'HOME': str(STATE), 'TMPDIR': '/tmp', 'LANG': 'C.UTF-8',
'LD_LIBRARY_PATH': str(RUNTIME / 'usr/lib/aarch64-linux-gnu'),
'XDG_DATA_DIRS': f'{RUNTIME}/usr/share:/usr/share', 'XDG_CACHE_HOME': str(STATE / 'cache'),
'GSETTINGS_SCHEMA_DIR': str(RUNTIME / 'usr/share/glib-2.0/schemas'), 'GSETTINGS_BACKEND': 'memory',
'GDK_BACKEND': 'x11', 'MOZ_ENABLE_WAYLAND': '0', 'MOZ_CRASHREPORTER_DISABLE': '1',
# Off-road teardown may interrupt startup; keep recovery under our bounded supervisor.
'MOZ_DISABLE_AUTO_SAFE_MODE': '1',
'LIBGL_ALWAYS_SOFTWARE': '1', 'NO_AT_BRIDGE': '1'}
auth = Path('/tmp/firefox.xauth')
auth.touch(mode=0o600)
cookie = os.urandom(16).hex()
# PrivateTmp makes this fixed display name private to this one transient service.
display = ':77'
subprocess.run(['/usr/bin/xauth', '-f', str(auth), 'add', display, '.', cookie], env=env, check=True, timeout=2,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
env.update(DISPLAY=display, XAUTHORITY=str(auth))
os.environ['XAUTHORITY'] = str(auth)
xvfb = subprocess.Popen(['/usr/bin/Xvfb', display, '-screen', '0', f'{width}x{height}x24', '-nolisten', 'tcp',
'-nolisten', 'local', '-auth', str(auth), '-noreset', '-extension', 'GLX'], env=env,
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL)
# -nolisten local disables the shared abstract socket; the filesystem socket stays in PrivateTmp.
for _ in range(100):
if stopping or xvfb.poll() is not None:
raise RuntimeError('Private Xvfb failed to start')
if Path('/tmp/.X11-unix/X77').is_socket():
x = X11('unix/:77', RUNTIME / 'usr/lib/aarch64-linux-gnu')
break
time.sleep(0.02)
if x is None or (x.width, x.height) != (width, height):
raise RuntimeError('Private Xvfb geometry check failed')
x.reference(buffers)
notify('WATCHDOG=1')
segments = []
if mode != 'reference':
try:
segments = [x.attach(fd) for fd in fds]
x.finish(x.wait(x.capture(segments[1])), buffers[1], True)
compare = C.CDLL(None).memcmp
compare.argtypes, compare.restype = [C.c_void_p, C.c_void_p, C.c_size_t], C.c_int
if compare(C.addressof(C.c_char.from_buffer(buffers[0])), C.addressof(C.c_char.from_buffer(buffers[1])), size):
raise RuntimeError('MIT-SHM frame differs from reference GetImage')
except (OSError, RuntimeError, TimeoutError):
if mode == 'shm':
raise
segments = []
x.clear_reference()
profile = STATE / 'profile'
profile.mkdir(mode=0o700, exist_ok=True)
first_run = not (profile / 'prefs.js').exists()
preferences = {'browser.shell.checkDefaultBrowser': False, 'browser.startup.homepage_override.mstone': 'ignore',
'browser.warnOnQuitShortcut': False,
'browser.startup.page': 3,
'browser.cache.disk.capacity': 65536, 'browser.cache.disk.smart_size.enabled': False,
'browser.download.dir': str(STATE / 'downloads'), 'browser.download.folderList': 2,
'gfx.webrender.software': True, 'layers.acceleration.disabled': True,
'media.hardware-video-decoding.enabled': False, 'dom.ipc.processCount': 2}
(STATE / 'downloads').mkdir(mode=0o700, exist_ok=True)
(profile / 'user.js').write_text('\n'.join(f'user_pref({json.dumps(key)}, {json.dumps(value)});' for key, value in preferences.items()))
page = Path('/tmp/firefox-start.html')
page.write_text('''<!doctype html><meta charset="utf-8"><title>Arabesque</title>
<style>body{background:#191327;color:white;font:24px sans-serif;padding:32px}input{font:24px sans-serif}</style>
<h1>Firefox in Arabesque</h1><p>Use the Firefox address bar to browse.</p>
<p>Scroll mode moves pages; Pointer mode drags and selects. Touch Keyboard to type.</p>
<p>Desktop closes Firefox completely.</p><input placeholder="Test typing here">''')
command = [str(RUNTIME / 'firefox/firefox'), '--no-remote', '--new-instance', '--profile', str(profile),
'--width', str(width), '--height', str(height)]
firefox = subprocess.Popen(command + ([page.as_uri()] if first_run else []), env=env,
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL)
sock.setblocking(False)
free, leased, pending = [0, 1], set(), None
dirty, ready = True, False
started = last_lease = last_capture = last_disk = last_watchdog = time.monotonic()
down = set()
while not stopping:
now = time.monotonic()
if now - last_lease > LEASE_SECONDS:
raise TimeoutError('Firefox owner stopped responding')
if now - last_watchdog >= 1:
notify('WATCHDOG=1')
last_watchdog = now
if xvfb.poll() is not None or firefox.poll() is not None:
raise RuntimeError(f'Firefox or Xvfb exited (Firefox={firefox.poll()}, Xvfb={xvfb.poll()})')
if now - last_disk >= 5:
last_disk = now
if not enough_space():
raise RuntimeError('Firefox stopped to preserve free storage')
for _ in range(32):
try:
message, _ = receive(sock)
except BlockingIOError:
break
kind = message['type']
if kind == 'lease' and set(message) == {'type'}:
last_lease = now
elif kind == 'release' and set(message) == {'type', 'slot'} and type(message['slot']) is int and message['slot'] in leased:
slot = message['slot']
leased.remove(slot)
free.append(slot)
elif kind in ('close', 'reset') and set(message) == {'type'}:
for button in down:
x.button(button, False)
down.clear()
stopping |= kind == 'close'
elif kind == 'input' and ready:
inject(x, message, down, width, height)
else:
raise ValueError('Invalid Firefox owner command')
dirty |= x.damaged()
if not ready and now - last_capture >= 0.25:
last_capture = now
if content_sandboxed(firefox.pid) and x.focus_browser():
ready = True
packet(sock, {'type': 'ready', 'capture': 'shm' if segments else 'reference'})
elif now - started > 30:
raise RuntimeError('Firefox content sandbox could not be verified')
if pending:
slot, request, deadline = pending
if reply := x.poll(request):
x.finish(reply, buffers[slot], bool(segments))
packet(sock, {'type': 'frame', 'slot': slot})
leased.add(slot)
pending = None
elif now >= deadline:
raise TimeoutError('Firefox capture stalled')
if ready and dirty and free and pending is None and now - last_capture >= 1 / fps:
slot = free.pop()
pending = slot, x.capture(segments[slot] if segments else 0), now + 2
dirty, last_capture = False, now
x.flush(x.connection)
select.select([sock, x.fd], [], [], min(0.02, 1 / fps) if pending or dirty or not ready else 0.25)
except (OSError, ValueError, RuntimeError, EOFError, subprocess.SubprocessError) as e:
print(f'Firefox bridge: {str(e)[:512]}', file=sys.stderr, flush=True)
try:
packet(sock, {'type': 'error', 'message': str(e)[:512]})
except OSError:
pass
finally:
stop_firefox(firefox, x)
if x is not None:
x.close()
stop_process(xvfb)
for buffer in buffers:
buffer.close()
for fd in fds:
os.close(fd)
sock.close()
def inject(x, message, down, width, height):
if set(message) != {'type', 'event', 'a', 'b'} or not isinstance(message['event'], str):
raise ValueError('Invalid Firefox input packet')
event, a, b = message['event'], message['a'], message['b']
if event == 'motion' and type(a) is int and type(b) is int and 0 <= a < width and 0 <= b < height:
x.motion(a, b)
elif event == 'button' and type(a) is int and a in (1, 3, 4, 5) and type(b) is bool:
if b:
down.add(a)
else:
down.discard(a)
x.button(a, b)
elif event == 'key' and type(a) is int and 0 < a <= 0x0110ffff and type(b) is int and 0 <= b <= 7:
x.key(a, ctrl=bool(b & 1), alt=bool(b & 2), shift=bool(b & 4))
else:
raise ValueError('Invalid Firefox input bounds')
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser(description='Run isolated Firefox and reference/SHM checks without starting the UI.')
parser.add_argument('--capture', choices=('reference', 'shm'), default='reference')
parser.add_argument('--seconds', type=int, default=45)
args = parser.parse_args()
# The diagnostic entry must not bypass the UI's fresh off-road authority gate.
sys.path.insert(0, str(Path(__file__).resolve().parents[6]))
from openpilot.common.params import Params
from openpilot.starpilot.galaxy.settings import LiveContextSource
authority = LiveContextSource(Params(), evidence_wait_ms=1500)
session = None
try:
if not authority.offroad():
raise SystemExit('Firefox checks require fresh off-road authority')
authority.evidence_wait_ms = 0
session = BrowserSession(1440, 650, 20, args.capture)
deadline, frames = time.monotonic() + max(1, min(args.seconds, 120)), 0
while not session.error and time.monotonic() < deadline:
if not authority.offroad():
raise SystemExit('Firefox check stopped: off-road authority lost')
session.poll()
if frame := session.take_frame():
slot, _ = frame
session.release(slot)
frames += 1
time.sleep(0.01)
if session.error or not frames:
raise SystemExit(session.error or 'No Firefox frame received')
print(json.dumps({'capture': session.status, 'frames': frames, 'runtime': 'isolated', 'content_seccomp': True}))
finally:
if session is not None:
session.close()
authority.close()