Guard against OOB read when bundle signature is at file start#3838
Merged
Conversation
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
christophwille
force-pushed
the
christophwille/singlefilebundlefix
branch
from
June 27, 2026 19:11
a1ef245 to
ed4d712
Compare
Contributor
There was a problem hiding this comment.
Pull request overview
This pull request hardens the single-file bundle detection/manifest parsing logic in ICSharpCode.Decompiler against crafted inputs that could otherwise crash the process or trigger excessive memory allocation when opening untrusted files.
Changes:
- Added a guard in
SingleFileBundle.IsBundleto prevent reading the 8-byte header offset from before the start of the buffer when a signature match occurs at file offset 0–7. - Added validation in
SingleFileBundle.ReadManifest(Stream)to reject negative or implausibly largeFileCountvalues before pre-sizing the entry array. - Introduced NUnit tests covering the OOB-read scenario, a valid bundle layout, and the oversized
FileCountcase.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| ICSharpCode.Decompiler/SingleFileBundle.cs | Adds bounds checks to prevent OOB reads during signature scanning and caps FileCount to avoid pathological allocations. |
| ICSharpCode.Decompiler.Tests/SingleFileBundleTests.cs | Adds regression tests to ensure the crash/large-allocation cases are rejected and valid layouts still work. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes an out-of-bounds read in
SingleFileBundle.IsBundlethat can crash theprocess when a crafted file is opened, plus a related unbounded allocation in
SingleFileBundle.ReadManifest.The bug
ICSharpCode.Decompiler/SingleFileBundle.csscans an opened file for the 32-bytesingle-file-bundle signature and then reads the 8-byte header offset stored
immediately before it:
That header offset only exists in a genuine bundle, where the signature lives near
the end of the file. The scan, however, starts at
ptr = data. Because thefile contents are fully attacker-controlled, placing the signature at file
offset 0-7 makes
ptr - sizeof(long)point before the start of the buffer.On the production path the buffer is a page-aligned memory-mapped view
(
LoadedPackage.FromBundle->BundleFileLoader, which runs on every openedfile), so
data - 8lands in the preceding, unmapped page and the read faultswith an
AccessViolationException. That is a corrupted-state exception:on .NET 5+ it is not delivered to managed
catch (Exception)blocks, so theloader pipeline's defensive
catch(LoadedAssembly.LoadAsync) does not catch itand the whole process terminates just from opening a ~30-byte crafted file.
Real-world impact
a write: no memory corruption, no RCE. The read value is only used as a file
offset that is immediately range-checked and is never returned to the attacker,
so there is no practical information disclosure either.
ICSharpCode.Decompilerand
ilspycmdthat decompile untrusted, user-submitted files (malware-analysisbackends, "decompile every upload" services, CI scanners). One crafted file is a
reliable crash-the-worker primitive, and because the crash is an uncatchable
AccessViolationExceptionit defeats thetry/catchsuch services typicallywrap around untrusted parsing. Interactive desktop ILSpy is low-stakes (the user
just reopens the app).
unmapped -- reliable on Windows (64 KB-aligned mappings), layout-dependent on
Linux/macOS (sometimes a silent benign OOB read instead). Reproduced as a real
fatal
AccessViolationException(process exit 134) on the productionmemory-mapped path.
The fix
OOB read: skip the backward read unless the match is at least
sizeof(long)bytes into the buffer (
ptr - data >= sizeof(long)). This changes behavior onlyfor malformed input that would otherwise fault; well-formed bundles (signature
near end of file) are unaffected, and the public API is unchanged.
Unbounded
FileCount(same file, same crafted-bundle chain):ReadManifestfedheader.FileCount = reader.ReadInt32()straight intoImmutableArray.CreateBuilder<Entry>(FileCount)with no bound, so a craftedmanifest (
FileCount = 0x7FFFFFFF) requested ~80 GB before reading any entry.It is now bounded against the bytes that can possibly remain (each entry
occupies >= 1 byte) and throws
InvalidDataExceptioninstead.Tests
New
ICSharpCode.Decompiler.Tests/SingleFileBundleTests.cs(NUnit), writtentest-first (red -> green):
IsBundle_SignatureAtStart_DoesNotReadBeforeBuffer-- places a sentinel wherethe out-of-bounds read would land; pre-fix the scanner returned it as a valid
header offset, post-fix the match is rejected without the read. (Uses the public
unsafe IsBundle(byte*, long, out long)overload with a managed buffer, so theout-of-bounds read stays on the GC heap and the test runner never faults --
deterministic red/green on all platforms.)
IsBundle_ValidLayout_DetectsBundle-- a well-formed signature preceded by itsheader offset is still detected (guards against over-correction).
ReadManifest_HugeFileCount_Throws-- a manifest withFileCount = int.MaxValuenow throws
InvalidDataExceptionrather than attempting a huge allocation.