From d757b66e9af0dbde1af0762ddd763e35439aa170 Mon Sep 17 00:00:00 2001 From: Josip Parlov Date: Mon, 19 Oct 2026 09:09:39 +0200 Subject: [PATCH] fix(tools): unwrap SELF dumps that leave out the version segment unwrap_self reads the vaddr-less segments that follow the recorded comment segment back to back from right after its data, and refuses the whole container when one of them runs past the end of the file. Some dumps end right after the last recorded segment and do not store the version segment at all. The sce_module/libc.prx of PPSA15552 is one: its last record, the 0x58-byte comment at 0x13b590, ends at 0x13b5e8, which is the file size, so the 0x1a-byte PT_SCE_VERSION has no bytes and the tool failed with "segment 12 does not fit the file" although every load, dynamic and comment segment is complete. A trailing segment that would run past the end of the file is now emitted without file bytes, the same way as the PT_NOTE the container leaves out. The relinker does not read PT_SCE_VERSION (GuestImageReader skips it). Containers that store the version segment unwrap byte for byte as before. --- tools/tests/test_unwrap_self.py | 25 +++++++++++++++++++++++++ tools/unwrap_self.py | 9 +++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/tools/tests/test_unwrap_self.py b/tools/tests/test_unwrap_self.py index 0d5bf8caf0..aff38128e5 100644 --- a/tools/tests/test_unwrap_self.py +++ b/tools/tests/test_unwrap_self.py @@ -168,6 +168,31 @@ class UnwrapTest(unittest.TestCase): self.assertEqual(data[version[2]:version[2] + version[5]], pattern(13, 0x10)) self.assertEqual(note[5], 0) + def test_a_version_segment_the_container_does_not_store_is_emitted_without_file_bytes(self): + segments = default_segments() + [(SCE_COMMENT, 4, 0x15A0, 0x0, 0x20, 0x10), (PT_NOTE, 4, 0x15C0, 0x0, 0x18, 4), + (SCE_VERSION, 4, 0x15D8, 0x0, 0x10, 0x10)] + records = default_records() + [(0x410004, 0xA80, 0x20), (0x302804, 0xAA0, 0x20)] + for stored in (0, 8): + with self.subTest(stored=stored): + payloads = dict(default_payloads()) + payloads[0xAA0] = pattern(11, 0x20) + if stored: + payloads[0xAC0] = pattern(13, stored) + container = build_container(segments, records, payloads) + self.assertEqual(len(container), 0xAC0 + stored) + result, output = self.convert(container) + self.assertEqual(result.returncode, 0, result.stderr) + data, headers = read_program_headers(output) + loads = [header for header in headers if header[0] == PT_LOAD] + self.assertEqual(data[loads[0][2]:loads[0][2] + loads[0][5]], pattern(1, 0x100)) + self.assertEqual(data[loads[1][2]:loads[1][2] + loads[1][5]], pattern(9, 0x80)) + comment = [header for header in headers if header[0] == SCE_COMMENT][0] + self.assertEqual(data[comment[2]:comment[2] + comment[5]], pattern(11, 0x20)) + version = [header for header in headers if header[0] == SCE_VERSION][0] + self.assertEqual((version[2], version[5]), (0, 0)) + for header in headers: + self.assertLessEqual(header[2] + header[5], len(data)) + def test_declared_offsets_are_used_when_the_container_has_no_records(self): segments = [(PT_LOAD, 1, 0x800, 0x0, 0x40, 0x1000), (PT_LOAD, 4, 0x900, 0x1000, 0x40, 0x1000)] payloads = {0x800: pattern(3, 0x40), 0x900: pattern(5, 0x40)} diff --git a/tools/unwrap_self.py b/tools/unwrap_self.py index 7225820aaa..b03fd9bfe2 100644 --- a/tools/unwrap_self.py +++ b/tools/unwrap_self.py @@ -14,8 +14,10 @@ differ inside a container: mapping of the PT_LOAD that contains them. Segments without a vaddr that follow the last recorded one in the ELF are stored right after it, back to back in ELF order, except a PT_NOTE, which the container leaves out and which - is emitted without file bytes. Other segments without a vaddr carry the shift - of the last placed one. + is emitted without file bytes. Some dumps end right after the recorded ones + and leave out the version segment as well, so one that would run past the + end of the file is emitted without file bytes too. Other segments without a + vaddr carry the shift of the last placed one. - The segments are packed without honouring p_align, while ELF requires p_offset % p_align == p_vaddr % p_align. Every region is copied to an offset that satisfies it, which the relinker enforces on guest modules. @@ -156,6 +158,9 @@ def place_segments(data, headers, records): continue header["source"] = cursor cursor += header["size"] + if cursor > len(data): + header["source"] = 0 + header["size"] = 0 for header in loads: if header["size"] == 0 or header["index"] in placed: continue