Fix the cross-provider review's findings

- APK reader: cap AndroidManifest.xml, resources.arsc and icon sizes before
  inflating them (APKs can come from install links), and follow resource
  references without cycles and with a result budget.
- Sideloading: reserve devkit-steam (SteamOS's sideloaded-client trampoline);
  the install dialog warns when a name replaces an installed title; a
  manifest's exe may name the program as it is in the archive, above the
  folder the installer steps into.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
saphidandClaude Opus 5.5 committed 2026-09-26 22:35:16 +10:00
1 parent 1580dae42e
commit 39d28790a1
5 files changed
+79 -12

No files matched your search

+20
View File
@@ -134,6 +134,26 @@ class ApkInfo(unittest.TestCase):
with self.assertRaises(frame_apk.ApkError):
self.read(data)
def test_refuses_oversized_members(self):
# An APK from a website mustn't make the server inflate gigabytes.
data = apk({'AndroidManifest.xml': manifest('com.example.big', 0x7f010000, 0x7f010001, 21)})
limit, frame_apk.MAX_MANIFEST = frame_apk.MAX_MANIFEST, 16
try:
with self.assertRaises(frame_apk.ApkError):
self.read(data)
finally:
frame_apk.MAX_MANIFEST = limit
def test_reference_cycles_and_fan_out_are_bounded(self):
res = frame_apk.Resources(b'')
ref = frame_apk.T_REF
res.entries = {1: [('', 0, ref, 1)] * 5} # five references to itself
self.assertEqual(res.values(1), [])
# Five references at each of five hops: 3125 leaves without a budget.
res.entries = {i: [('', 0, ref, i + 1)] * 5 for i in range(1, 6)}
res.entries[6] = [('', 0, frame_apk.T_STRING, 0)]
self.assertEqual(len(res.values(1)), frame_apk.MAX_VALUES)
if __name__ == '__main__':
unittest.main()
+13
View File
@@ -142,6 +142,17 @@ class Targets(unittest.TestCase):
with self.assertRaises(FrameError):
frame_titles._choose(p, '../outside.exe')
def test_exe_path_from_above_the_unwrapped_folder(self):
# A manifest names the program as it is in the archive: Game/B.exe, not B.exe.
p = self.plan({'Game/A.exe': pe(0x8664), 'Game/B.exe': pe(0x8664)}, 'Game')
self.assertEqual(p['unwrapped'], 'Game')
frame_titles._choose(p, 'Game/B.exe')
self.assertEqual(p['target'], 'B.exe')
frame_titles._choose(p, 'Game\\A.exe')
self.assertEqual(p['target'], 'A.exe')
with self.assertRaises(FrameError):
frame_titles._choose(p, 'Game/../../outside.exe')
class Zips(unittest.TestCase):
def setUp(self):
@@ -317,6 +328,8 @@ class Names(unittest.TestCase):
def test_title_id(self):
self.assertEqual(frame_titles.title_id('Hollow Knight: Silksong!'), 'Hollow_Knight_Silksong')
self.assertEqual(frame_titles.title_id('steam'), 'steam-game') # Valve's reserved sideload names
self.assertEqual(frame_titles.title_id('Devkit Steam'), 'Devkit_Steam')
self.assertEqual(frame_titles.title_id('devkit-steam'), 'devkit-steam-game') # the trampoline file
self.assertEqual(frame_titles.title_id('--rm -rf /'), 'rm_-rf')
self.assertEqual(len(frame_titles.title_id('x' * 200)), 64)
with self.assertRaises(FrameError):