Two test fixtures carried their own copy of the 32-byte signature, which
would silently drift from the real one. The signature is now an internal
member of SingleFileBundle and ILSpy.Tests gets internals access to the
decompiler assembly, matching what ILSpyX already grants it.
Assisted-by: Claude:claude-fable-5:Claude Code
IsBundle scanned up to but excluding the last position at which a full
signature fits, so a signature occupying the final 32 bytes of the region
was never compared. Windows hid this: a memory-mapped view there reports
the page-rounded region size, leaving trailing zero bytes after the file.
On Linux and macOS the view length is the exact file length, and the
LoadedPackage bundle tests, whose synthetic bundles end with the
signature, failed there with FromBundle returning null. Real bundles keep
apphost code after the signature, which is why this stayed latent.
Assisted-by: Claude:claude-fable-5:Claude Code
SingleFileBundle.IsBundle scans for the 32-byte bundle signature and reads the
8-byte header offset stored immediately before it. That offset only exists in a
genuine bundle, where the signature sits near the end of the file. The scan
started at the first byte, so a crafted file with the signature at offset 0..7
made it read before the start of the buffer. On the production path that buffer
is a page-aligned memory-mapped view, so the read faults on the preceding
unmapped page with an AccessViolationException -- a corrupted-state exception
that bypasses the loader's catch and terminates the process merely from opening
the file. The bundle probe runs on every opened file, so this needs no user
action beyond open.
Skip the backward read unless the match is at least sizeof(long) bytes into the
buffer. While in the same file, bound ReadManifest's FileCount against the bytes
that remain before it pre-sizes the entry array, so a crafted manifest can no
longer request a multi-gigabyte allocation.
Assisted-by: Claude:claude-opus-4-8:Claude Code