Skip to content

Write every byte of a section: fix the stub's zero-fill and the trimmed size - #46

Merged
uyjulian merged 1 commit into
ps2dev:masterfrom
jhoff:fix-trailing-zeroes
Oct 8, 2026
Merged

uyjulian merged 1 commit into
ps2dev:masterfrom
jhoff:fix-trailing-zeroes

Conversation

@jhoff

@jhoff jhoff commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

A packed program never gets the bytes remove_section_zeroes() trims off the end of a section: two defects combine so nothing writes them, and they keep whatever the previous program left in RAM.

1. originalSize is recorded before the trim. psh.originalSize = section_size runs before remove_section_zeroes() shortens section_size and adds the trimmed count to zeroByteSize. The stub decompresses the shorter stream to [va, va + filesz - trimmed) and starts the zero-fill at va + originalSize = va + filesz, so [va + filesz - trimmed, va + filesz) is written by nothing. (The fill also runs trimmed bytes past the end of the segment.) It is now set after the trim.

2. The stub's zero-fill does nothing. fast_memzero() writes to ptr and size inside its inline asm while declaring them input-only, so GCC may give the output and ptr one register. In the lzma-1d00-stub built by the current toolchain (GCC 15.2.0, ps2max/dev:v20260228, ps2-packer at 9afec04) the loop is:

addu  v0,v0,v1      # end, computed into ptr's register
...
sb    zero,0(v0)    # one store, at ptr + size
sltu  v1,v0,v0      # always 0
bnez  v1,loop
addiu v0,v0,1

One byte stored past the range, then out. Even as written, the compare came before the increment, so the loop stored one byte past the end. ptr is now a read-write operand and the loop flag an early-clobber output, the loop writes exactly [ptr, ptr + size), and the three nops stay so the body is still eight instructions. Same change in main-kmode.c (it shares the code; only the LZMA user-mode stub was exercised below). The patched stub's loop:

addu  v1,v0,v1      # end in its own register
...
sb    zero,0(v0)
addiu v0,v0,1
sltu  a0,v0,v1
bnez  a0,loop
nop

Why it matters. Programs mostly survive because ps2sdk's crt0 clears .bss from _fbss, but the trimmed bytes sit below _fbss. In a ps2sdk program the last word of the data segment is newlib's _impure_ptr, whose top byte is 0x00 for anything linked below 0x01000000, so it is exactly that byte that is lost. A program started with ExecPS2 over another one's memory inherits it, and current libcglue writes through _impure_ptr before main on every boot: _libcglue_timezone_update → IsT10K() → open() returns ENOSYS, because the path ops are installed by a constructor crt0 runs after _libcglue_init, and _open_r stores errno. With a non-zero leftover byte that is a TLB store miss before main. On hardware it is a black screen; PCSX2 logs TLB Miss ... [store] and carries on, so it does not show in the emulator.

Released software has it. The current Open PS2 Loader beta (OPNPS2LD.ELF from v1.2.0-Beta-2245-3e3f34e, itself ps2-packer output) has one section at 0x00100000 whose stream ends a byte short of originalSize: the last word of its image is _impure_ptr at 0x42bf00 (0x0024b8e8, its _impure_data), and the shipped stub never writes its top byte at 0x42bf03. Whether that bites depends on what the launcher left at that address.

Tested

  • Offline: decoding each section and applying the header's fill exactly as the stub does, the shipped packer leaves 1 byte unwritten in each of two real programs (at 0x685b07 and 0x686187); the patched packer leaves none. Repacking with the unpatched packer reproduces the original images byte for byte, so the inputs are the same programs.
  • PCSX2, a program chained by ExecPS2 over another: 9 TLB Miss [store] with the shipped packer, 0 with the patched one, same input, same chainer, same point reached afterwards.
  • Hardware (SCPH-75001): a ps2sdk program chained by ExecPS2 over another whose byte at its tail address is 0x54 died at the handover with the shipped packer (black screen, no network), and the same input repacked with the patched packer came up 2 of 2, by the same chain.

Until a fixed stub is installed, ending the segment in a non-zero byte (a LONG at the end of .sdata in the linker script) keeps the packer from trimming anything.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VdURrNBJ6MvWbY6gZ2VYrz

…ed size

remove_section_zeroes() moves a section's trailing zero bytes from the
compressed stream into zeroByteSize, but originalSize was recorded before the
trim, so the stub's zero-fill started at va + filesz and the trimmed bytes
were written by nothing.

The zero-fill itself did nothing either: fast_memzero's inline asm modified
ptr and size while declaring them input-only, so GCC gave the output and ptr
the same register and the loop compiled to a single store one byte past the
range. Even as written, the compare before the increment stored one byte past
the end. ptr is now a read-write operand, the loop flag an early-clobber
output, and the loop writes exactly [ptr, ptr + size). The three nops stay, so
the loop body is still eight instructions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01VdURrNBJ6MvWbY6gZ2VYrz

@uyjulian uyjulian left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@uyjulian
uyjulian merged commit d43ac51 into ps2dev:master Oct 8, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants