Conversation
…lifetime
`PacketReader(byte[] buff, bool dyn)` takes its pointer inside a `fixed` block
and keeps using it after the block ends, while holding no reference to the
array:
fixed (byte* p = buff)
m_Data = p;
`fixed` pins only for the statement it wraps. Its only caller is
`Packet.GetCompressedReader()`, where the inflated buffer is a local, so from
the moment the reader is returned the GC is free to move or collect it. The
0xDD handler (`Handlers.CompressedGump`) then walks the layout and the string
table through that pointer while allocating heavily in between - `Regex.Split`,
`int.Parse`, and a string per table entry - so a collection landing inside that
window is not unlikely.
The result is either silently wrong gump data or an access violation in JIT
code. An AV is a corrupted-state exception, so the handler's `catch {}` does not
see it and the host process dies. Under ClassicUO that surfaces as the client
exiting with 0xC0000005 and no managed stack trace.
Reproduced against a released 1.9.77.0 binary with a harness that builds a
reader the way GetCompressedReader does, forces a compacting GC, then reads
back: 200/200 trials return the wrong bytes. With this change, 0/200.
Fix: pin with a GCHandle for the reader's lifetime and free it in a finalizer.
The `byte*` constructor - the per-packet hot path, whose buffer is owned and
already pinned by the caller - suppresses finalization, so nothing changes
there.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The finalizer design adds per-packet overhead and retains pinned buffers nondeterministically.
Get a fresh assessment by requesting another Copilot review.
Review effort: Balanced
Findings: 1
Open (1)
What changed in this PR
Fixes dangling pointers in array-backed PacketReader instances used for compressed packet data.
Changes:
- Pins array-backed buffers with
GCHandle. - Adds finalizer-based handle cleanup.
- Suppresses finalization for caller-owned pointer buffers.
| File | Description |
|---|---|
Razor/Network/Packet.cs |
Adds managed-buffer pinning and lifetime cleanup. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+821
to
+824
| ~PacketReader() | ||
| { | ||
| if (m_Pin.IsAllocated) | ||
| m_Pin.Free(); |
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.

The bug
PacketReader(byte[] buff, bool dyn)inRazor/Network/Packet.cstakes its pointer inside afixedblock and keeps using it after the block ends, while holding no reference to the array:fixedpins only for the statement it wraps. Its only caller isPacket.GetCompressedReader(), where the inflated buffer is a local — so from the moment the reader is returned, nothing keeps the array alive or in place and the GC is free to move or collect it.Handlers.CompressedGump(the0xDDhandler) then reads through that pointer while allocating heavily in between —Regex.Splitover the layout, anint.Parseper match, and a string per string-table entry. A collection landing inside that window is not a remote possibility.The result is either silently wrong gump data, or an access violation in JIT code. An AV is a corrupted-state exception, so the handler's
catch {}never sees it and the host process dies. Under ClassicUO that surfaces as the client exiting with0xC0000005and no managed stack trace — intermittent, and more frequent on shards that redraw gumps often.Reproduction
A harness that loads a released
Razor.exeby reflection, builds aPacketReaderthe wayGetCompressedReaderdoes, forces a compacting GC, then reads the buffer back:I first found this from a minidump of a ClassicUO crash: AV reading a misaligned address,
ExceptionAddressin no loaded module (JIT heap),mscorlib.ni.dll+clr.dllon the faulting stack, and the managed heap holding a constructed "Attempted to read or write protected memory" exception. The client log ended mid-0xDD.The fix
Pin the array with a
GCHandlefor the reader's lifetime and free it in a finalizer.The
byte*constructor is the per-packet hot path and its buffer is owned and already pinned by the caller (ClassicUO.OnRecv/OnSendandOSIbuild their readers inside their ownfixedblocks and never let them escape), so it allocates no handle and callsGC.SuppressFinalizeto stay off the finalizer queue. Nothing changes for that path.PacketReader(byte[])is the only place in the codebase where such a pointer escapes — I checked everyfixedblock inRazor/.Builds clean against
master(net472, x64).🤖 Generated with Claude Code