Skip to content

Bound XamarinCompressedFileLoader against crafted XALZ headers#3843

Open
christophwille wants to merge 1 commit into
masterfrom
christophwille/99
Open

Bound XamarinCompressedFileLoader against crafted XALZ headers#3843
christophwille wants to merge 1 commit into
masterfrom
christophwille/99

Conversation

@christophwille

Copy link
Copy Markdown
Member

The Xamarin "XALZ" compressed-module loader (XamarinCompressedFileLoader) sized its buffer allocations from an attacker-controlled header field and ignored the partial-read return value. Because the loader is registered first and runs on any file beginning with the XALZ magic, merely opening a crafted file could crash the process or force a huge allocation.

Three concrete defects: the declared uncompressed length is a raw header uint cast to int with no sanity bound, so a tiny file claiming ~2 GB forced a giant ArrayPool.Rent (a decompression-bomb amplification), while a value with the high bit set became negative after the cast and made Rent throw ArgumentOutOfRangeException. The compressed length was taken as the whole file length (header included) and ReadAsync's return value was discarded, so a short read left the tail of the rented buffer as stale pooled bytes fed to the LZ4 decoder. Finally, the output MemoryStream exposed the entire rented buffer, which ArrayPool.Rent may size larger than the data, so PEFile parsed past the real decompressed bytes into leftover pool contents.

The fix bounds the declared length before any allocation (rejecting zero, values above int.MaxValue, or more than an LZ4 block could possibly expand from the payload at its 255x maximum ratio), reads exactly the payload that follows the 12-byte header with ReadExactlyAsync, and slices the output MemoryStream to the length that LZ4Codec.Decode actually reports. Malformed input now surfaces as a catchable InvalidDataException, consistent with the recent bundle-signature and .rsrc resource-tree hardening, instead of crashing or over-allocating. Well-formed Xamarin modules continue to load unchanged.

New tests in ICSharpCode.Decompiler.Tests/XamarinCompressedFileLoaderTests.cs cover the crafted cases (high-bit/negative declared length, an implausibly huge declared length rejected without allocating, and a corrupt LZ4 payload) plus a happy-path round trip that LZ4-compresses a real assembly, wraps it in a valid XALZ header, and asserts it still loads.

🤖 Generated with Claude Code

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens the Xamarin XALZ compressed-module loader to avoid attacker-controlled allocations, partial-read issues, and parsing beyond the actual decompressed bytes when opening crafted files.

Changes:

  • Adds header/payload validation and uses ReadExactlyAsync to avoid partial reads of the compressed payload.
  • Uses the LZ4Codec.Decode returned length to bound the MemoryStream exposed to PEFile.
  • Adds NUnit tests covering malformed headers/payloads and a valid round-trip XALZ load.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
ICSharpCode.ILSpyX/FileLoaders/XamarinCompressedFileLoader.cs Adds validation, exact reads, and bounds the decompressed stream to mitigate crafted XALZ inputs.
ICSharpCode.Decompiler.Tests/XamarinCompressedFileLoaderTests.cs Introduces regression tests for crafted XALZ headers/payloads plus a happy-path load test.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread ICSharpCode.ILSpyX/FileLoaders/XamarinCompressedFileLoader.cs
Comment thread ICSharpCode.ILSpyX/FileLoaders/XamarinCompressedFileLoader.cs
Comment thread ICSharpCode.ILSpyX/FileLoaders/XamarinCompressedFileLoader.cs Outdated
Comment thread ICSharpCode.ILSpyX/FileLoaders/XamarinCompressedFileLoader.cs
The Xamarin XALZ loader sized its buffer allocations from an attacker-controlled
header field and ignored the partial-read length, so merely opening a crafted
file (the loader is registered first and runs on any XALZ-magic input) could
crash or over-allocate. The declared uncompressed length, a raw header uint cast
to int, had no sanity bound: a tiny file claiming ~2 GB forced a giant
ArrayPool.Rent (decompression-bomb amplification), and a high-bit value became
negative and made Rent throw ArgumentOutOfRangeException. The compressed length
was taken as the whole file (header included) and ReadAsync's return value was
discarded, leaving stale pooled bytes in the tail fed to the decoder; the
output MemoryStream then exposed the entire rented buffer, so PEFile parsed past
the real decompressed data into leftover pool contents.

Bound the declared length before renting (reject zero, > int.MaxValue, or more
than an LZ4 block could expand from this payload at its 255x maximum ratio),
read the payload that actually follows the header with ReadExactlyAsync, and
slice the output to the length LZ4Codec.Decode reports. Malformed input now
fails as a catchable InvalidDataException, consistent with the bundle and .rsrc
hardening; well-formed Xamarin modules load exactly as before.

Assisted-by: Claude:claude-opus-4-8:Claude Code
using var fileReader = new BinaryReader(stream, Encoding.UTF8, leaveOpen: true);
// Read compressed file header
if (stream.Length < sizeof(uint))
return null;

@dgrunwald dgrunwald Jun 29, 2026

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.

Useless check (TOCTOU).
Catching the exception is the only proper way of handling IO errors.

Also, some kinds of streams might not know the length ahead-of-time.

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.

3 participants