Merge pull request #9291 from veosett/codex/self-alignment-padding

fix(tools): bound total SELF alignment padding
This commit is contained in:
Dean authored and GitHub committed 2026-10-23 09:40:16 +13:00
commit 44ce348a74
3 files changed
+98 -9

No files matched your search

+2
View File
@@ -12,6 +12,8 @@ Select or drop multiple app ZIP archives to convert them into one ZIP bundle. Th
## Input and conversion
The SELF unwrapper refuses containers requiring more than 64 MiB of total alignment padding before allocating the output buffer. This limit counts padding across file-backed and empty segments, not segment data; a refusal preserves the input and any existing output file.
Use a clean ELF executable. Place its bundled ELF modules in `sce_module/`, `sce_modules/`, or `prx/` beside the input executable, or under the parent directory selected with `--sce-module-path <path>`. The option accepts absolute paths and paths relative to the current working directory; do not include the `sce_module` directory itself. Only files directly inside the module directories are processed. SELF/fSELF containers are skipped with a warning; unwrap them before relinking if they need to be included. `tools/unwrap_self.py <input.self> <output.elf>` does that for a decrypted container: it places the segments at the offsets of the SELF segment records and restores `p_offset % p_align == p_vaddr % p_align`, and it refuses a container whose load segments all look encrypted. The command-line relinker skips an ELF module as a backup copy, with a message, when its file name extends the name of another ELF module in the same directory and both have the same SONAME, such as a dumper's `libX.prx.esbak` or `libX.prxbak` beside an ELF `libX.prx`; a single backup beside a SELF container is still relinked. `prx/` can coexist with either `sce_module/` or `sce_modules/`. Both `sce_module/` and `sce_modules/` present is an error. All three absent is an error unless an explicit `--module-dir` is supplied.
```text
+90 -6
View File
@@ -473,7 +473,7 @@ class UnwrapTest(unittest.TestCase):
def allocate(value):
if isinstance(value, int):
self.assertLessEqual(value, len(container) + unwrap_self.MAX_EMPTY_LOAD_PADDING)
self.assertLessEqual(value, len(container) + unwrap_self.MAX_ALIGNMENT_PADDING)
return bytearray(value)
for previous in (None, b"previous output"):
@@ -486,7 +486,7 @@ class UnwrapTest(unittest.TestCase):
contextlib.redirect_stderr(io.StringIO()) as stderr:
self.assertEqual(unwrap_self.main(), 2)
self.assertEqual(stdout.getvalue(), "")
self.assertIn("empty load alignment requires more than", stderr.getvalue())
self.assertIn("segment alignment requires more than", stderr.getvalue())
self.assertNotIn("Traceback", stderr.getvalue())
if previous is not None:
self.assertEqual(output.read_bytes(), previous)
@@ -494,20 +494,104 @@ class UnwrapTest(unittest.TestCase):
self.assertFalse(output.exists())
self.assertEqual(source.read_bytes(), container)
def test_excessive_file_backed_padding_is_refused_before_allocation(self):
cases = [
([(PT_LOAD, 5, 0x1234, (1 << 40) - 1, 0x100, 1 << 40)],
[(0x2804, 0x800, 0x100)], {0x800: b"x" * 0x100}),
([(PT_LOAD, 5, 0x1234, 0, 0x100, PAGE),
(SCE_COMMENT, 0, 0xB00, 0, 0x20, 1 << 40)],
[(0x2804, 0x800, 0x100)], {0x800: b"x" * 0x100, 0xB00: pattern(9, 0x20)}),
]
source = Path(self.directory) / "input.self"
output = Path(self.directory) / "output.elf"
for segments, records, payloads in cases:
container = build_container(segments, records, payloads)
source.write_bytes(container)
def allocate(value):
if isinstance(value, int):
self.assertLessEqual(value, len(container) + 64 * 1024 * 1024)
return bytearray(value)
for previous in (None, b"previous output"):
with self.subTest(segments=segments, previous=previous):
output.unlink(missing_ok=True)
if previous is not None:
output.write_bytes(previous)
with mock.patch.object(unwrap_self, "bytearray", side_effect=allocate, create=True), \
mock.patch.object(sys, "argv", ["unwrap_self.py", str(source), str(output)]), \
contextlib.redirect_stdout(io.StringIO()) as stdout, \
contextlib.redirect_stderr(io.StringIO()) as stderr:
self.assertEqual(unwrap_self.main(), 2)
self.assertEqual(stdout.getvalue(), "")
self.assertIn("alignment requires more than", stderr.getvalue())
self.assertNotIn("Traceback", stderr.getvalue())
self.assertEqual(source.read_bytes(), container)
if previous is None:
self.assertFalse(output.exists())
else:
self.assertEqual(output.read_bytes(), previous)
def test_alignment_padding_limit_is_cumulative(self):
for empty in (False, True):
with self.subTest(empty=empty):
segments = [(PT_LOAD, 5, 0x1234, 0x80, 0x20, 0x100),
(PT_LOAD, 6, 0x5678, 0x580, 0 if empty else 0x10, 0x1000)]
records = [(0x800, 0x800, 0x20)]
payloads = {0x800: pattern(1, 0x20)}
if not empty:
records.append((0x100800, 0xA00, 0x10))
payloads[0xA00] = pattern(9, 0x10)
source = Path(self.directory) / "input.self"
output = Path(self.directory) / "output.elf"
container = build_container(segments, records, payloads)
source.write_bytes(container)
output.write_bytes(b"previous output")
with mock.patch.object(unwrap_self, "MAX_ALIGNMENT_PADDING", 0x15F):
with self.assertRaisesRegex(unwrap_self.Refused, "alignment requires more than"):
unwrap_self.unwrap(source, output)
self.assertEqual(output.read_bytes(), b"previous output")
self.assertEqual(source.read_bytes(), container)
with mock.patch.object(unwrap_self, "MAX_ALIGNMENT_PADDING", 0x160):
unwrap_self.unwrap(source, output)
data, headers = read_program_headers(output)
for header in headers:
self.assertEqual(header[2] % header[7], header[3] % header[7])
self.assertEqual(data[headers[0][2]:headers[0][2] + 0x20], payloads[0x800])
if not empty:
self.assertEqual(data[headers[1][2]:headers[1][2] + 0x10], payloads[0xA00])
def test_large_alignment_with_small_padding_remains_supported(self):
payload = b"x" * 0x100
segments = [(PT_LOAD, 5, 0x1234, FIRST_REGION + 0x10, len(payload), 1 << 40)]
container = build_container(segments, [(0x2804, 0x800, len(payload))], {0x800: payload})
source = Path(self.directory) / "input.self"
output = Path(self.directory) / "output.elf"
source.write_bytes(container)
with mock.patch.object(unwrap_self, "MAX_ALIGNMENT_PADDING", 0x10):
unwrap_self.unwrap(source, output)
data, headers = read_program_headers(output)
load = headers[0]
self.assertEqual(load[2] % load[7], load[3] % load[7])
self.assertEqual(load[2], FIRST_REGION + 0x10)
self.assertEqual(data[load[2]:load[2] + load[5]], payload)
self.assertEqual(source.read_bytes(), container)
def test_empty_load_padding_limit_counts_only_added_bytes(self):
baseline, output = self.convert(build_container(default_segments(), default_records(), default_payloads()))
backed = [(*segment[:5], 1) for segment in default_segments()]
baseline, output = self.convert(build_container(backed, default_records(), default_payloads()))
self.assertEqual(baseline.returncode, 0, baseline.stderr)
baseline_size = output.stat().st_size
limit = 0x100
for extra in (-1, limit, limit + 1):
with self.subTest(extra=extra):
address = 0x10000 + baseline_size + extra
segments = default_segments() + [(PT_LOAD, 6, 0, address, 0, 0x10000)]
segments = backed + [(PT_LOAD, 6, 0, address, 0, 0x10000)]
container = build_container(segments, default_records(), default_payloads())
source = Path(self.directory) / "input.self"
source.write_bytes(container)
previous = output.read_bytes()
with mock.patch.object(unwrap_self, "MAX_EMPTY_LOAD_PADDING", limit):
with mock.patch.object(unwrap_self, "MAX_ALIGNMENT_PADDING", limit):
if extra <= limit:
unwrap_self.unwrap(source, output)
data, headers = read_program_headers(output)
@@ -515,7 +599,7 @@ class UnwrapTest(unittest.TestCase):
self.assertEqual(headers[3][2], baseline_size + extra)
self.assertEqual(headers[3][2] % headers[3][7], address % headers[3][7])
else:
with self.assertRaisesRegex(unwrap_self.Refused, "empty load alignment requires more than"):
with self.assertRaisesRegex(unwrap_self.Refused, "segment alignment requires more than"):
unwrap_self.unwrap(source, output)
self.assertEqual(output.read_bytes(), previous)
+6 -3
View File
@@ -43,7 +43,7 @@ RECORD_SIZE = 0x20
RECORD_HAS_SEGMENT = 0x800
RECORD_SEGMENT_SHIFT = 20
FIRST_REGION = 0x400
MAX_EMPTY_LOAD_PADDING = 64 * 1024 * 1024
MAX_ALIGNMENT_PADDING = 64 * 1024 * 1024
PT_LOAD = 1
PT_NOTE = 4
ENTROPY_LIMIT = 7.5
@@ -273,9 +273,11 @@ def unwrap(path, output, verbose=False):
regions.sort(key=lambda region: region["old"])
program_offset = struct.unpack_from("<Q", elf_header, 0x20)[0]
cursor = max(FIRST_REGION, program_offset + len(headers) * 56)
padding = 0
for region in regions:
alignment = region["align"] if region["align"] and (region["align"] & (region["align"] - 1)) == 0 else 1
placed = cursor + (region["vaddr"] - cursor) % alignment
padding += placed - cursor
region["new"] = placed
cursor = placed + region["size"]
@@ -283,8 +285,9 @@ def unwrap(path, output, verbose=False):
for header in headers:
if header["type"] == PT_LOAD and header["size"] == 0:
empty_end = max(empty_end, header["vaddr"] % max(header["align"], 1))
if empty_end - cursor > MAX_EMPTY_LOAD_PADDING:
raise Refused("empty load alignment requires more than %d bytes of output padding" % MAX_EMPTY_LOAD_PADDING)
padding += empty_end - cursor
if padding > MAX_ALIGNMENT_PADDING:
raise Refused("segment alignment requires more than %d bytes of output padding" % MAX_ALIGNMENT_PADDING)
cursor = empty_end
result = bytearray(cursor)