Files
StarPilot/openpilot/tools/arabesque_firefox/test_setup.py
T
2026-10-28 05:08:17 -04:00

156 lines
6.3 KiB
Python

import hashlib
import importlib.util
import io
import json
from pathlib import Path
import shlex
import shutil
import subprocess
import sys
import tarfile
from unittest.mock import Mock
import pytest
spec = importlib.util.spec_from_file_location('firefox_setup', Path(__file__).with_name('setup.py'))
assert spec is not None and spec.loader is not None
setup = importlib.util.module_from_spec(spec)
spec.loader.exec_module(setup)
def test_corrupt_cached_package_is_rejected(tmp_path):
path = tmp_path / 'package.deb'
path.write_bytes(b'good')
package = {'name': path.name, 'size': 4, 'sha256': hashlib.sha256(b'good').hexdigest()}
setup.verify(path, package)
path.write_bytes(b'evil')
with pytest.raises(ValueError, match='checksum'):
setup.verify(path, package)
def test_download_reports_progress_and_reuses_verified_package(tmp_path, monkeypatch):
data = b'verified browser'
package = {'name': 'firefox-test.tar.xz', 'url': 'https://example.invalid/browser',
'size': len(data), 'sha256': hashlib.sha256(data).hexdigest()}
response = io.BytesIO(data)
response.url = package['url']
fetch, progress = Mock(return_value=response), Mock()
monkeypatch.setattr(setup.urllib.request, 'urlopen', fetch)
monkeypatch.setattr(setup, 'report', progress)
path = setup.download(package, tmp_path)
assert path.read_bytes() == data
progress.assert_called_once_with('Downloading Firefox… 100%')
assert setup.download(package, tmp_path) == path
fetch.assert_called_once()
def test_cancelled_download_never_publishes_partial_package(tmp_path, monkeypatch):
data = b'partial'
response = io.BytesIO(data)
response.url = 'https://example.invalid/browser'
package = {'name': 'firefox-test.tar.xz', 'url': response.url, 'size': len(data),
'sha256': hashlib.sha256(data).hexdigest()}
monkeypatch.setattr(setup.urllib.request, 'urlopen', Mock(return_value=response))
monkeypatch.setattr(setup, 'report', Mock(side_effect=InterruptedError('cancelled')))
with pytest.raises(InterruptedError):
setup.download(package, tmp_path)
assert not list(tmp_path.iterdir())
def test_archive_cannot_escape_the_private_runtime(tmp_path):
path = tmp_path / 'browser.tar.xz'
with tarfile.open(path, 'w:xz') as archive:
entry = tarfile.TarInfo('../escaped')
entry.size = 4
archive.addfile(entry, io.BytesIO(b'bad!'))
destination = tmp_path / 'runtime'
destination.mkdir()
with pytest.raises(tarfile.FilterError):
setup.unpack(path, destination)
assert not (tmp_path / 'escaped').exists()
def test_preparation_refuses_unprivileged_host_without_writing(tmp_path, monkeypatch):
monkeypatch.setattr(setup.os, 'geteuid', lambda: 1000)
monkeypatch.setattr(setup, 'ROOT', tmp_path / 'browser')
with pytest.raises(RuntimeError, match='ARM64 AGNOS'):
setup.prepare()
assert not (tmp_path / 'browser').exists()
@pytest.mark.parametrize('existing', ['missing', 'directory', 'symlink', 'unsafe'])
def test_first_use_under_unwritable_data(tmp_path, monkeypatch, existing):
if not shutil.which('unshare') or not shutil.which('setpriv'):
raise pytest.skip.Exception('Linux user namespaces and setpriv are required')
probe = subprocess.run(['unshare', '-Ur', 'true'], capture_output=True, check=False)
if probe.returncode:
raise pytest.skip.Exception('Linux user namespaces are unavailable')
spec = importlib.util.spec_from_file_location(
'browser_session', Path(__file__).resolve().parents[2] / 'selfdrive/ui/layouts/arabesque/tools/browser_session.py')
assert spec is not None and spec.loader is not None
runtime = importlib.util.module_from_spec(spec)
spec.loader.exec_module(runtime)
data = tmp_path / 'data'
data.mkdir()
root = data / 'arabesque-firefox'
target = tmp_path / 'target'
target.mkdir()
(target / 'sentinel').write_text('preserved')
if existing == 'symlink':
root.symlink_to(target)
elif existing != 'missing':
root.mkdir(mode=0o755)
if existing == 'unsafe':
root.chmod(0o777)
data.chmod(0o555)
monkeypatch.setattr(runtime, 'ROOT', root)
command = runtime.preparation_command('test', 'test')
prestart = next((shlex.split(p.removeprefix('--property=ExecStartPre=')) for p in command
if p.startswith('--property=ExecStartPre=')), [])
if prestart:
assert prestart[0].startswith('-+')
prestart[0] = prestart[0][2:]
assert prestart == ['/usr/bin/mkdir', '-m', '0755', str(root)]
caps = next(p.split('=', 2)[2] for p in command if p.startswith('--property=CapabilityBoundingSet='))
caps = ','.join('+' + cap.removeprefix('CAP_').lower() for cap in caps.split())
installer = '''
import importlib.util, os, sys
from pathlib import Path
spec = importlib.util.spec_from_file_location('setup', sys.argv[1])
setup = importlib.util.module_from_spec(spec)
spec.loader.exec_module(setup)
setup.ROOT = Path(sys.argv[2])
setup.platform.machine = lambda: 'aarch64'
read_text = Path.read_text
Path.read_text = lambda path, *a, **kw: '19.8.1' if str(path) == '/VERSION' else read_text(path, *a, **kw)
setup.prepare_locked = lambda manifest: None
assert os.geteuid() == 0
assert int(next(line.split(':')[1] for line in Path('/proc/self/status').read_text().splitlines() if line.startswith('CapEff:')), 16) == 1
setup.prepare()
'''
runner = '''
import json, os, subprocess, sys
prestart = json.loads(sys.argv[1])
if prestart:
subprocess.run(prestart, capture_output=True, check=False)
os.execvp('setpriv', ['setpriv', '--no-new-privs', '--bounding-set=-all,' + sys.argv[2],
sys.executable, '-c', sys.argv[3], sys.argv[4], sys.argv[5]])
'''
try:
result = subprocess.run(['unshare', '-Ur', sys.executable, '-c', runner, json.dumps(prestart), caps, installer,
str(Path(__file__).with_name('setup.py')), str(root)], capture_output=True, text=True, check=False)
if existing in ('missing', 'directory'):
assert result.returncode == 0, result.stderr
assert root.stat().st_mode & 0o777 == 0o755
assert (root / '.prepare.lock').is_file()
else:
assert result.returncode != 0
assert ('Refusing symlink' if existing == 'symlink' else 'Unsafe browser directory ownership') in result.stderr
if existing == 'unsafe':
assert root.stat().st_mode & 0o777 == 0o777
assert (target / 'sentinel').read_text() == 'preserved'
assert len(list(target.iterdir())) == 1
finally:
data.chmod(0o755)