Collecting from the two EventSource providers differs by tooling, not by
platform: dotnet-trace/EventPipe works the same everywhere, with ETW/PerfView
as the Windows-only alternative. The doc lists the keyword masks and levels
so sessions can enable only the areas of interest.
Assisted-by: Claude:claude-fable-5:Claude Code
ILSpyX had no instrumentation, yet most UI-visible latency bottoms out
here: lazy assembly loads, the first-resolve cascade that metadata-loads
every assembly in a list snapshot, per-module search strategy runs,
analyzer scope scans over all assemblies and their references, bundle/zip
entry extraction, and PDB loading. The provider mirrors the
ICSharpCode.Decompiler design: Start/Stop pairs, keyword gating, and
IsEnabled() guards at every call site; per-entry package extraction is
Verbose because of its volume.
AbstractSearchStrategy.Search is now a non-virtual template method that
wraps the span around a new protected SearchCore, so derived strategies
cannot bypass the instrumentation.
Assisted-by: Claude:claude-fable-5:Claude Code
The events introduced in #2519 timed only the five per-entity DoDecompile
overloads, allocated a Stopwatch and the member's FullName even when no
trace session was attached, and reported whole milliseconds, which rounds
almost every member to zero. Flat one-shot events also gave PerfView and
dotnet-trace no way to show durations or nesting, and the actually
expensive stages (type system initialization, assembly resolution probing,
the IL/AST transform pipelines, whole-project decompilation) were not
instrumented at all.
Start/Stop event pairs let trace viewers derive duration and nesting from
event timestamps, keywords let a session enable only the areas of
interest, and every call site is gated on IsEnabled() so tracing costs a
branch when disabled. Per-transform events are Verbose because of their
volume; unlike the STEP/Stepper mechanism they work in Release builds.
EventSource is in-box for netstandard2.0 and flows over both ETW and
EventPipe, so this stays cross-platform with no new dependency.
Assisted-by: Claude:claude-fable-5:Claude Code
Enum members whose value duplicates an earlier member now reference it
(Item2B = Item2A), [Flags] members combine earlier single-bit members
(All = Item1 | Item2 | Item3) or their complement (NotItem1 = ~Item1)
instead of showing a bare number.
Several guardrails keep the output faithful to how such enums are
written by hand: only previously declared members are referenced (field
row order); a multi-bit value lying entirely within a larger, earlier
member is a field encoding inside that mask, not a flag union, and
stays numeric, as do zero-valued members of [Flags] enums, which
routinely have several unrelated zero members. The ~X form is
suppressed in byte/ushort enum declarations, where the initializer
constant folds in int and would not compile. Enums with unusual
underlying types (bool, native int) keep the plain constant conversion.
With these rules, decompiling System.Private.CoreLib reproduces the
hand-written declarations of TypeAttributes, MethodAttributes,
AttributeTargets and FileAttributes almost verbatim.
Assisted-by: Claude:claude-fable-5:Claude Code
ReduceNesting walks an else-if chain to its innermost if and asks
ShouldReduceNesting whether to extract the else block, which ExtractElseBlock
does by casting the block to Block. A chain with no trailing else reaches this
with a bare Nop, yet the heuristic still approved it (its stats count a Nop as
one statement), so the cast threw InvalidCastException. Take a Block in
ShouldReduceNesting and skip the reduction at the call site when the else is
absent.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Object creation can require an unsafe context solely because the selected
constructor has a pointer parameter. Apply the existing unsafe-signature
check to object creation nodes so the emitted declaration remains compilable.
Assisted-by: Copilot:gpt-5.6-sol:GitHub Copilot CLI
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 86d2918e-5a24-48b4-9a86-41d331ec3720
With recognition on the ILAst and reference substitution during
translation, the four accessor-body patterns in
PatternStatementTransform were only reachable as a fallback, and any
divergence between them and the AutoEventDecompiler verdict produced
inconsistent output. A compiler shape the ILAst matchers do not know now
degrades to explicit accessors with the backing field kept in the
output, which stays compilable. Bodyless events (abstract, extern,
interface members) previously relied on the patterns' no-body clause to
become field-like; since C# cannot express bodyless custom accessors,
DoDecompile now chooses the field-like form for them directly. Also
deletes the orphaned IsEventBackingFieldName helper; the name
association lives in PropertyAndEventBackingFieldLookup.
Assisted-by: Claude:claude-fable-5:Claude Code
Claude-Session: https://claude.ai/code/session_01Btdypgm8utyxqt1Etn2BDi
PatternStatementTransform renamed backing-field identifiers to the event
after the fact, keyed on the metadata name association alone: references
belonging to an event that is not actually automatic were still renamed,
binding them to a custom event that is unusable as a value (the same
defect class as #3858), and the rename bypassed the resolver checks, so
qualifiers were computed for the hidden field instead of the printed
event. ExpressionBuilder.ConvertField now performs the substitution,
keyed on the AutoEventDecompiler verdict whose memo moves into
DecompileRun so that member hiding, the event declaration, and reference
translation all decide from one analysis. Checking the verdict's field
identity also keeps same-typed sibling events apart (#3575), and the
qualifier logic running against the event drops spurious this./type
qualifiers from raise sites.
mcs 2.x accesses a sibling automatic event's backing field directly
inside custom accessors instead of calling the accessor, so the fixture
expects the resulting Delegate.Combine form there.
Assisted-by: Claude:claude-fable-5:Claude Code
Claude-Session: https://claude.ai/code/session_01Btdypgm8utyxqt1Etn2BDi
MemberIsHidden decides field hiding from the metadata name association
alone, which over-approximates: an event whose accessors fail the ILAst
validation is decompiled with explicit accessors, and while a referenced
backing field is re-added through the work list, an unreferenced one was
silently dropped from the output. The type-definition member loop now
consults the same memoized verdict as the event declaration, so the two
decisions agree by construction.
Assisted-by: Claude:claude-fable-5:Claude Code
Claude-Session: https://claude.ai/code/session_01Btdypgm8utyxqt1Etn2BDi
The syntactic accessor-body patterns in PatternStatementTransform sit
downstream of every settings-dependent transform, so each new compiler
shape or settings combination silently broke recognition: with
AggressiveInlining enabled, static events inline the Delegate.Combine
call into CompareExchange positionally, which none of the four patterns
matched, while call sites were still rewritten to the event name from
metadata alone - producing uncompilable output (CS0079).
Recognition now happens in DoDecompile(IEvent) by structurally matching
the ILAst of the accessors, decompiled with a fixed set of settings the
same way RecordDecompiler analyzes method bodies. This makes detection
independent of the user-visible settings by construction. Events that
are not recognized fall back to the classic path unchanged, including
the existing AST patterns.
mcs 2.x compiles the accessors as a compound assignment, evaluating
'this' once via IL 'dup'; the simple-combine matcher accepts that
stack-slot alias.
Assisted-by: Claude:claude-fable-5:Claude Code
Structural recognition of compiler-generated code needs method bodies as
ILAst decompiled with a fixed set of settings, so that recognition does
not depend on user-visible options. RecordDecompiler had this pipeline
as a private helper; hoist it to CSharpDecompiler so other recognizers
can share it, deriving the generic context from the method's declaring
type instead of a captured type definition.
Assisted-by: Claude:claude-fable-5:Claude Code
Claude-Session: https://claude.ai/code/session_01Btdypgm8utyxqt1Etn2BDi
In nested-namespace mode a NamespaceTreeNode's display label is only its
last segment ("Generic"), while the full dotted path
("System.Collections.Generic") is what identifies the namespace in
metadata and to the docs site. Three call sites read the label where they
need the full path, so in nested mode each targets the wrong namespace:
decompiling a namespace node queries an empty one and titles the output
after the last segment; the MSDN URL points at the wrong page; and
scope-search-to-namespace scopes to the wrong name.
These are ds5678's fixes from #3879, reintegrated on top of the eager
namespace rebuild. #3879's other Name -> FullName corrections, in
AssemblyTreeNode.FindNamespaceNode and TreeNodeLocator, are already
covered here by the full-namespace-name and type-handle indexes, so only
the Decompile and search-entry cases carry over.
Assisted-by: Claude:claude-opus-4-8:Claude Code
With "Use nested namespace structure" enabled, most namespaces never
appeared in the tree, and the first expand of a large assembly lagged.
Both come from the same regression: the Avalonia assembly-tree nodes
were written from scratch as lazy scaffolding, not ported from the WPF
design, and lost the single eager build the WPF host used.
A NamespaceTreeNode filters as Recurse/MatchAndRecurse, so the filter
cascade computes its IsHidden as "all children hidden" -- vacuously true
for an empty child set. The lazy build attached each namespace node
while it was still empty, latching intermediate namespaces (those that
hold only sub-namespaces, e.g. System.Collections) hidden and stranding
everything beneath them. The cascade also force-loads every namespace
node's children anyway, so the per-node laziness avoided no work: it
rescanned the whole TypeDefinitions table once per namespace node.
Restore release/10.1's structure: AssemblyTreeNode builds the entire
namespace band in one pass over the module's top-level types, populates
each node before attaching it, and keeps two indexes -- full namespace
name -> node and type handle -> node -- so FindNamespaceNode/FindTypeNode
are O(1) and correct at any nesting depth. TreeNodeLocator.FindTypeNode
(hyperlink clicks, search activation, JumpToType) delegates to that
index instead of walking children by display name, which never matched
in nested mode. NamespaceTreeNode goes back to a dumb label holder and
re-escapes its display label via ILAmbience.EscapeName.
The band is built from the module's type system, like 10.1's, not from
raw metadata: each TypeTreeNode holds the resolved ITypeDefinition it
renders from, so painting a cell no longer re-enters the settings-keyed
type-system cache the way master's lazy node did on every Text/Icon/
Filter read -- each of which rebuilt an effective-settings object and
took its lock. Ordering the pass by full ReflectionName is also what
interleaves a namespace's types and its sub-namespaces into one
alphabetical run (a sub-namespace attaches when its first descendant
type is reached, landing at its own alphabetical slot among the sibling
types); grouping all types ahead of all namespaces was a visible
departure from the WPF order.
Two deliberate departures from a literal 10.1 copy: keep the global-
namespace "-" node, and keep the cached IsPublicAPI getter. Both index
dictionaries are cleared on rebuild so a nested/flat toggle leaves no
stale entries.
Holding resolved entities means the tree has to be rebuilt when a
setting changes the type system. Only one compilation is cached per
module, keyed on the effective decompiler settings, so a language-
version or decompiler-option change drops it and would otherwise leave
every node pointing at a discarded compilation -- stale labels, icons
and filters, and the C# 14 extension-block nodes shown against the wrong
version. AssemblyTreeModel reloads the loaded assemblies when the
computed TypeSystemOptions actually change (Display-only settings never
do, and cost nothing), then restores the selected node from its path --
which re-expands its ancestors on the way to revealing it -- the way
Refresh does. The WPF host got this for free: its modal Options dialog
rebuilt the tree on close, where the Avalonia page applies live.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Solution export reported once per assembly, when that assembly finished.
Nothing was reported before the first one did, so the tab sat on the
indeterminate spinner it starts with for most of the run and then jumped
straight to the end -- exporting two assemblies showed a spinner, 1 of 2,
done. A project that bailed out before decompiling never reported at all,
stranding the bar short of the end for the rest of the export.
Sum the per-project file counts instead: each parallel worker feeds its own
counts into a shared map and the bar reports their total. WholeProjectDecompiler
carries its whole file count on every report, so the total is known from a
project's first written file rather than its last -- measured on two real
assemblies, the bar turns determinate after 245ms instead of 15s, and moves
through 978 files rather than 2 assemblies. Each project closes its share out
in a finally, so bailing out or cancelling still lets the bar reach the end.
The denominator grows over the first second as projects discover their file
counts. The alternative -- enumerating every project's types up front -- delays
the export itself to make the bar look better, which is the wrong trade.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Two gaps in the export paths, both visible from the same selection.
A selection holding an assembly that failed to load was turned away by
TryGetExportableAssemblies, so Ctrl+S fell through to the single-node save
and quietly wrote just the focused assembly -- the rest of the selection
vanished with no report. The predicate now only insists that something in
the selection loaded, and the exporter skips what it cannot decompile and
names it in the status report. That is also what the dialog always assumed:
its "not a valid assembly" row badge was unreachable, because no selection
containing one could get that far.
The dialog asks for an output folder and derived the .sln name from it,
while Save Code lets the user name the file. Now the dialog offers the name
too, in solution mode, defaulting (via the placeholder) to the folder-
derived name the exporter would pick anyway.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Save Code on several assemblies had its own copy of the export flow: its
own selection matcher, its own frozen-tab runner, and a hard-coded
"Exporting solution" tab title -- so the same operation read differently
depending on whether it was started from Save Code or Export Project,
which titles the tab after the assemblies. Route it through ProjectExport
like the single-assembly path already is, leaving one runner and one
matcher behind every flow that decompiles whole assemblies to disk.
Save Code keeps letting the user name the .sln (the Export Project dialog
only asks for a folder and derives the name from it), so the export
options now carry an optional solution file name; unset means the old
folder-derived name.
Assisted-by: Claude:claude-opus-4-8:Claude Code
File -> Save Code decompiled a whole assembly on a bare Task.Run: no
progress bar, no way to cancel, and (for a .csproj) diverging from both
normal decompilation and the dedicated Export Project command, which
already report progress. Route the assembly-save paths through the shared
UI instead:
- The .csproj export reuses the Export Project machinery (ProjectExporter
in a frozen, determinate-progress tab), so a large assembly reports
per-file progress and can be cancelled while the tree stays browsable.
- The single-file save runs behind the same RunWithCancellation overlay
that normal decompilation uses.
Both entry points into the project export compute the tab title in one
place, titling it after the assemblies being exported (their tree-node
labels, joined the way a multi-node decompile tab is) so the tab reads
the same whether reached via Save Code or Export Project.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Roslyn caches a ReadOnlySpan<T> created from an array literal in a
<PrivateImplementationDetails> field on target frameworks without
RuntimeHelpers.CreateSpan (e.g. .NET Framework / netstandard2.0 + System.Memory):
object obj = <PrivateImplementationDetails>.cache;
if (obj == null) {
obj = new char[] { '\r', '\n' };
<PrivateImplementationDetails>.cache = (char[])obj;
}
... new ReadOnlySpan<char>((char[])obj) ...
The decompiled output referenced the compiler-synthesized
<PrivateImplementationDetails> type, whose escaped name is not expressible in C#
and is never declared, so the output failed to recompile (CS0400).
The modern RuntimeHelpers.CreateSpan form was already handled
(TransformRuntimeHelpersCreateSpanInitialization); this adds the analogous
handling for the legacy lazy-cache form, mirroring CachedDelegateInitialization
(which collapses the same lazy-static-field cache for anonymous-method delegates).
Once the cache is collapsed, the existing array-initializer transforms recover
the array literal, so the <PrivateImplementationDetails> reference disappears.
Test: ILPretty/CachedReadOnlySpanInitialization.
MatchSwitchOnCharBlock's case 2 and default paths guarded against a
negative character index, but case 1 (a bare switch on get_Chars) did
not. Crafted IL whose get_Chars/get_Item index is negative - a value no
compiler emits, but valid IL - therefore reached the pattern
reconstruction unchecked. For a length-1 group this silently miscompiled
the switch (it rebuilds the string switch from the char labels without
using the index), turning IL that reads s[-1] into `switch (s)`; for
longer strings it threw IndexOutOfRangeException and aborted the method.
Move the check into MatchGetChars so all three call sites reject a
negative index by construction, and drop the two now-redundant guards.
Same class of unvalidated-integer robustness issue as #3878, in a
different switch-on-string pattern.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The BAML tests were orphaned: in no solution, no CI, and missing their
ICSharpCode.BamlDecompiler reference. Revive them split by platform,
mirroring ILSpy.Tests / ILSpy.Tests.Windows: ILSpy.BamlDecompiler.Tests
(net11.0) holds platform-agnostic tests - including a regression test that
decompiles with the WPF assemblies hidden, covering the missing-assembly
fix - and ILSpy.BamlDecompiler.Tests.Windows (net11.0-windows) holds the
WPF round-trip cases that compile XAML into BAML. Single-TFM projects keep
normal lock files; the Windows one declares all RIDs so its lock is
generatable and verifiable on any host.
Wire the cross-platform project into ILSpy.sln, ILSpy.XPlat.slnf and
ILSpy.Desktop.slnf so it builds and runs on the Linux/macOS legs; the
Windows project stays in ILSpy.sln (its tests execute in the Windows leg).
The Windows job also archives that assembly, whose embedded BAML lets the
decompiler be exercised against real BAML on a host without WPF.
Assisted-by: Claude:claude-opus-4-8:Claude Code
KnownThings hard-required PresentationFramework, PresentationCore and
WindowsBase to be resolvable, so decompiling a WPF binary on a machine
without WPF (Linux/macOS) threw "Could not resolve known assembly" and
aborted the whole BAML decompile.
Mirror MinimalCorlib: when a well-known BAML assembly cannot be resolved,
BamlDecompilerTypeSystem substitutes a synthetic stand-in module that
upholds the invariant KnownThings assumes. The stand-in only materializes
the types the decompiler explicitly seeds, so it stays bounded to the
well-known set and references to other types keep degrading to
UnknownType exactly as before. For the WPF assemblies it also reproduces
the XmlnsDefinitionAttribute mapping so known types still serialize under
the presentation xmlns instead of a clr-namespace fallback.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Added Issue3877 test to PrettyTestRunner and new test case source to verify dictionary initialization with negative capacity. Updated SwitchOnStringTransform to skip processing when a negative dictionary capacity is detected.
When a null literal is the only argument a generic type argument could
be inferred from, and that type argument is an anonymous type, ILSpy
used to drop the type arguments entirely (they cannot be written
explicitly), producing 'Test(null)', which no longer compiles
(CS0411). A conditional expression whose never-taken branch creates an
instance of the anonymous type is the minimal C# expression that gives
a null value that type, so the argument now carries enough information
for type inference.
The instance expression is obtained by translating a synthesized
'newobj' with 'default.value' arguments through the existing pipeline
rather than assembling syntax by hand; the property values are typed
defaults, since the IL only contains ldnull. Anonymous types occurring
inside other constructed types (e.g. arrays) stay unexpressible and
keep the previous output.
Assisted-by: Claude:claude-fable-5:Claude Code
CS8196 also fires when the second reference sits inside a nested call
in another argument of the declaring call, e.g.
OutAndValue(out var a, UseAndReturn(out a)). The existing check already
covers this because it walks all descendants of the sibling arguments;
this fixture pins that behavior.
Assisted-by: Claude:claude-fable-5:Claude Code
Passing the same local as multiple out arguments of one call made
DeclareVariables turn the first use into an implicitly-typed declaration,
producing 'f(out var x, out x)'. Referencing an implicitly-typed out
variable in another argument of the declaring call is rejected by the
compiler (CS8196), because its type is only inferred once overload
resolution of that call has completed. The explicitly-typed form
'f(out int x, out x)' is valid, so fall back to the explicit type in
that case.
Assisted-by: Claude:claude-fable-5:Claude Code
Awaiting a dynamic value lowers GetAwaiter/IsCompleted/GetResult to
dynamic callsites, which async decompilation could not recognize:
await detection ran before DynamicCallSiteTransform, so the await was
emitted as a raw state machine (or crashed in AnalyzeAwaitBlock, #1388).
AnalyzeStateMachine now collapses the awaiter callsites per block, folds
the runtime ICriticalNotifyCompletion branch the compiler emits for an
awaiter not statically known to implement it into the canonical single
call, and re-joins the branch chains the collapse leaves so each dynamic
await sits in one block. DetectAwaitPattern matches the dynamic
GetAwaiter/IsCompleted/GetResult shape and emits `await expr`; a
synthesized dynamic GetResult method gives the await and its local the
dynamic type. DynamicCallSiteTransform also follows callsite targets
spilled into state-machine locals, so an awaited value flowing into a
dynamic callsite (e.g. d.Result = await ..., #1928) decompiles too.
Assisted-by: Claude:claude-fable-5:Claude Code
* Add decompiler architecture document
* Add StatementBuilder section to architecture doc, fix foreach story
* Correct architecture doc per review by the decompiler author
DynamicIsEventInstruction has no ExpressionBuilder visitor, so any is-event
diamond that survives the transform pipeline leaks as an "OpCode not supported"
comment. The collapse only fired for the plain statement form; every other
lowering the compiler emits for "d.Event += b" was missed:
- a copy-of-value temporary between the getmember cache and the diamond (modern
Roslyn emits it for the statement form),
- the result-used shape, where the diamond is a value-if nested inside the
consuming expression (a call argument, or a leave's value when returned),
- the result-returned shape as two leaves, "if (isevent) leave(add)" plus a
fall-through "leave(compound)" rather than an if/else, and
- the same two-leaves shape when the optimizer drops the getmember cache (legacy
csc and Roslyn <= 2.x): the is-event flag then feeds a single branch, inlining
folds it into the condition, and the compound leave is already collapsed, so
the cache-based entry never matched it and the branch leaked. These compilers
only run on Windows CI, so the gap was invisible on Linux.
The descendants walk handles the nested value-if once an over-strict
copy-of-value bounds guard (which never held for the compact return block) is
dropped. The two two-leaves shapes share a skeleton matcher and the add/remove
accessor-name check, and both validate the accessor name and arguments. Re-running
from the collapsed statement instead of the next one avoids indexing past the end
of a block that has shrunk to a single leave.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Failed decompiler-test runs leave their generated fixtures (compiled
test assemblies, generated IL, diff inputs) on the runner where they
are lost, making CI-only failures hard to diagnose. Capture the folder
via upload-artifact's own client-side zip at compression-level 9
instead of a separate 7z step, since 7z is not reliably available on
the macOS runner.
Assisted-by: Claude:claude-fable-5:Claude Code
A stackalloc initializer whose first element is written twice must not be
folded into a 'stackalloc int[] { ... }': doing so drops the earlier store,
which is observable when its value has side effects. HandleSequentialLocAllocInitializer
already rejects this (the extra store trips the LoadCount and elementCount
guards); the sequential-store offset matcher on its own would misclassify the
second offset-0 write. The IL is derived from a C# 'stackalloc int[] { a, b, c }'
disassembly with an extra offset-0 store injected, a shape no compiler emits.
Assisted-by: Claude:claude-fable-5:Claude Code
A second store to an already-written array element must prevent
HandleSimpleArrayInitializer from folding the stores into a collection
initializer: the earlier store would be dropped (a visible change when
its value has side effects), and without the nextMinimumIndex guard the
transform crashes on the colliding index. The nextMinimumIndex check
already enforces this; pin it with a Pretty fixture so the boundary
between a foldable initializer and a duplicate-write sequence stays
covered.
Assisted-by: Claude:claude-fable-5:Claude Code
The bool-plus-out-abortTransform contract encoded three outcomes in two
flags, so false meant either 'sequence ended' or 'reject the transform'
depending on the flag. A three-value enum names each outcome at the
return site, and passing minExpectedOffset by ref makes visible that
only the binary.add path advances the expected offset (the bare ldloc
path previously echoed it back through an out parameter).
Assisted-by: Claude:claude-fable-5:Claude Code
HandleCpblkInitializer already rejects fields with a nil metadata token
before casting to FieldDefinitionHandle; the localloc prefix path did
not, so a crafted assembly could make GetFieldDefinition throw instead
of the transform being skipped.
Assisted-by: Claude:claude-fable-5:Claude Code
The jagged-array branch of DoTransform still assembled the
Block(ArrayInitializer) shape by hand; mapping its sequential values to
indexed tuples lets it use BuildSimpleArrayInitializerBlock like the
other single-dim branches, removing the last inline copy of the pattern.
Assisted-by: Claude:claude-fable-5:Claude Code
A true result meant both 'consumed a prefix' and 'no prefix present',
while false aborted the whole transform. The distinction is unnecessary:
a malformed or absent prefix leaves pos unchanged, so the per-element
stobj scan fails on the initblk/cpblk instruction and rejects the
transform with the same out-state. Aborting on 'no prefix' had also
broken plain constant-length stackalloc initializers (element stores
without any prefix), caught by CS73_StackAllocInitializers.
Assisted-by: Claude:claude-fable-5:Claude Code
HandleSequentialLocAllocInitializer mixed three concerns in one method
body: consuming an optional initblk/cpblk prefix, matching the offset of
each sequential store, and assembling the values array. Moving the first
two into TryHandleLocAllocInitializerPrefix and TryGetSequentialStoreOffset
reduces the method to the scan loop itself. Behavior-preserving; the
break-vs-abort distinction of the offset matcher is kept via an out flag.
Share the repeated block construction used by single-dimensional and multi-dimensional simple array initializers. The helper also avoids the LINQ iterator used for adding initializer stores while keeping the transformation behavior unchanged.
Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
Pre-checking stream.Length before reading is racy (the stream can shrink
between check and read) and not every stream knows its length ahead of
time. Instead, catch EndOfStreamException at each read: a stream too
short for the magic is passed through as not-XALZ, and a truncated
header or payload after a confirmed magic is rejected as
InvalidDataException. stream.Length remains only a sizing hint for the
payload buffer.
Assisted-by: Claude:claude-fable-5:Claude Code
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
WebCilFile builds raw native pointers into the memory-mapped view directly
from section-header fields read out of attacker-controlled metadata. Unlike
PEFile.GetSectionData, which delegates to the bounds-checked PEReader, this
hand-rolled path validated nothing: a crafted section header could produce a
SectionData (and hence a BlobReader) pointing far outside the view, an
out-of-bounds read reachable on normal decompilation through method-body and
field-data RVA resolution. The (int)RawDataSize narrowing cast could also
yield a negative length.
Resolve and bounds-check the raw-data range against the view length before
constructing SectionData, widening the arithmetic to long so crafted uint
fields cannot wrap the range check or narrow into an apparently valid length.
Structural parsing in FromFile now reports a crafted or truncated module as
"not a WebCIL file" (null) rather than letting EndOfStreamException,
OverflowException or BadImageFormatException escape the loader.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Backfills the standard MIT X11 header on hand-written files that never
got one, attributing each to its first-commit author and year from git
history. Code vendored from dotnet/runtime and Humanizr/Humanizer gets
its origin's license lines and a provenance note instead. Generated
files (Resources.Designer.cs, the version-info template), tool-managed
suppression files, and BAML test-case fixtures intentionally stay
header-less.
Assisted-by: Claude:claude-fable-5:Claude Code
New .cs files were inconsistently getting AlphaSierraPapa headers; the
convention is that new code carries the contributing human's copyright,
while existing headers (including vendored third-party ones) are never
rewritten and legacy header-less files stay as they are.
Assisted-by: Claude:claude-fable-5:Claude Code
The C# compiler lowers a foreach over an inline array into a for loop whose
body reads each element through <PrivateImplementationDetails>.InlineArrayElementRef(ref
buffer, i). That helper cannot be named in C#, so the decompiled for loop did
not compile.
Reconstruct the foreach at the AST level instead. Rewriting the unchecked
InlineArrayElementRef helper to the bounds-checked indexer buffer[i] would be
unsound for an out-of-bounds index, so the transform fires only when the loop
bound equals the inline array's length: that proves 0 <= i < length, matching
the exact shape the compiler emits and nothing else. A loop that does not match
keeps the faithful (if unnameable) helper call rather than silently gaining a
bounds check.
Assisted-by: Claude:claude-fable-5:Claude Code