Repository navigation
Move static fields out of ref structs to avoid GC use-after-free on IL2CPP - #199
Merged
hadashiA merged 1 commit intoOct 15, 2026
Conversation
…L2CPP IL2CPP does not emit GC write barriers for stores into static or thread-static fields declared on a value type (including ref structs). With Unity's incremental GC enabled, objects stored there can be swept while still referenced, which crashed in Utf8YamlEmitter.WriteRaw when the freed FlowSequenceEmpty array had been reused by a string. Keep these fields on a nested static class so IL2CPP emits barriers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner
|
Makes sense. Thanks for the detailed investigation! I'm going to merge this. |
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
On IL2CPP with Unity's incremental GC enabled, the static arrays held by
Utf8YamlEmittercan be garbage-collected while still referenced, which leads to a native crash (EXC_BAD_ACCESS) during serialization.This PR moves the static / thread-static fields of the ref structs
Utf8YamlEmitterandUtf8YamlTokenizerinto a nestedstatic class Statics. There is no behavior change.Cause
IL2CPP emits a GC write barrier (
Il2CppCodeGenWriteBarrier) for stores into static fields declared on reference types, but not for static or thread-static fields declared on value types (including ref structs). From the generated C++ ofUtf8YamlEmitter's static constructor (Unity 6000.2.13f1, iOS):The same is true for the
[ThreadStatic]buffers (stringBufferStaticetc.) and forWriteIndentreassigningwhiteSpaces. If such a store happens during an incremental mark phase, the GC never sees the new reference, and the object gets swept.Observed crash (iOS, VYaml 1.1.0, Unity 6000.2.13f1,
gcIncremental: 1)Inspecting the process memory at the crash:
byte[]fields ofUtf8YamlEmitterpointed to memory that had been freed and reused: 7 of them now heldSystem.Stringobjects (their "length" field read as UTF-16 text such as"tRes","esul"), andwhiteSpacesheld an unrelatedbyte[].EmitStringAnalyzer.whiteSpaces,YamlEmitOptions.Default) were alive and correct.WriteRawused the bogus length (~3.6 MB) of the "array" and read past it into a guard page.Changes
Utf8YamlEmitter: move the 8 staticbyte[]fields and the 3[ThreadStatic]buffers into a nestedstatic class Statics.Utf8YamlTokenizer: move the static stop-byte sets (byte[]/SearchValues<byte>) into a nestedstatic class Statics.Since the fields now belong to a reference type, IL2CPP emits write barriers for them. Field initializers and access patterns are unchanged.
Testing
dotnet build VYaml -c Release: succeeded for all target frameworks, with no new warnings.dotnet test VYaml.Tests: Passed 410 / Skipped 6 / Failed 0.🤖 Generated with Claude Code