mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-07 13:03:42 +08:00
156 lines
6.3 KiB
Python
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)
|