Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion tools/package_title_release.py
Original file line number Diff line number Diff line change
Expand Up @@ -831,9 +831,33 @@ def check_diagnostics(ctx, stage):
log('Playtest Diagnostics default %s (%s)' % ('on' if want else 'off', ctx.channel))


def declared_overlay_assets(stage):
"""Hash-bound mod patches are payloads, not standalone disc/BIOS images."""
allowed = set()
for manifest in sorted((Path(stage) / 'mods/bundled').glob('*/*/manifest.toml')):
for overlay in tomllib.loads(read_text(manifest)).get('overlay', []):
rel = PurePosixPath(overlay.get('file', ''))
if (rel.is_absolute() or '..' in rel.parts or not rel.parts or
rel.parts[0] != 'assets' or rel.suffix.lower() != '.bin' or
overlay.get('target') != 'disc_user'):
continue
digest = overlay.get('sha256', '')
expected = overlay.get('expected_sha256', '')
if not all(re.fullmatch(r'[0-9a-fA-F]{64}', h) for h in (digest, expected)):
continue
asset = manifest.parent.joinpath(*rel.parts).resolve()
if not asset.is_relative_to(manifest.parent.resolve()) or not asset.is_file():
fail('declared overlay asset missing or outside package: %s' % rel)
if sha256(asset).lower() != digest.lower():
fail('declared overlay asset hash mismatch: %s' % rel)
allowed.add(asset)
return allowed


def payload_gates(stage, plat, exe_name):
"""Content that must never ship, whatever the title config says."""
stage = Path(stage)
overlay_assets = declared_overlay_assets(stage)
present = [name for name in BUILD_KIT if (stage / name).exists()]
if present:
fail('package contains build-kit/user content: %s' % ', '.join(present))
Expand All @@ -850,7 +874,8 @@ def payload_gates(stage, plat, exe_name):
suffix = path.suffix.lower()
if suffix in DISC_SUFFIXES:
fail('disc/save data in package: %s' % rel)
if suffix in ('.bin', '.rom') and lower != 'bios/openbios.bin':
if (suffix in ('.bin', '.rom') and lower != 'bios/openbios.bin' and
path.resolve() not in overlay_assets):
fail('binary image in package (only bios/openbios.bin may ship): %s' % rel)
if re.match(r'scph.*\.(bin|rom)', leaf):
fail('retail BIOS image in package: %s' % rel)
Expand Down
17 changes: 17 additions & 0 deletions tools/tests/test_package_title_release.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,23 @@ def test_toolchain_sources_are_allowed(self):
(self.stage / 'overlay_toolchain/tcc/examples/fib.c').write_text('int x;')
ptr.payload_gates(self.stage, 'windows', 'Fixture.exe')

def test_hash_bound_disc_overlay_asset(self):
package = self.stage / 'mods/bundled/fixture.patch/1.0.0'
(package / 'assets').mkdir(parents=True)
asset = package / 'assets/warning.bin'
asset.write_bytes(b'authored patch fixture')
(package / 'manifest.toml').write_text(
'[[overlay]]\ntarget="disc_user"\noffset=16\n'
'file="assets/warning.bin"\nsha256="%s"\nexpected_sha256="%s"\n'
% (hashlib.sha256(asset.read_bytes()).hexdigest(), '0' * 64))
ptr.payload_gates(self.stage, 'windows', 'Fixture.exe')
asset.write_bytes(b'tampered')
with self.assertRaisesRegex(ptr.PackagingError, 'hash mismatch'):
ptr.payload_gates(self.stage, 'windows', 'Fixture.exe')

def test_undeclared_mod_binary_is_refused(self):
self.assert_refused('mods/bundled/fixture/1.0.0/assets/disc.bin', message='image')

def test_openbios_required(self):
(self.stage / 'bios/openbios.bin').write_bytes(b'short')
with self.assertRaisesRegex(ptr.PackagingError, 'OpenBIOS'):
Expand Down
Loading