Skip to content

Move static fields out of ref structs to avoid GC use-after-free on IL2CPP - #199

Merged
hadashiA merged 1 commit into
hadashiA:masterfrom
cp-hashimoto-ko-fighter:fix/il2cpp-ref-struct-static-fields
Oct 15, 2026
Merged

hadashiA merged 1 commit into
hadashiA:masterfrom
cp-hashimoto-ko-fighter:fix/il2cpp-ref-struct-static-fields

Conversation

@cp-hashimoto-ko-fighter

Copy link
Copy Markdown
Contributor

Summary

On IL2CPP with Unity's incremental GC enabled, the static arrays held by Utf8YamlEmitter can 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 Utf8YamlEmitter and Utf8YamlTokenizer into a nested static 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++ of Utf8YamlEmitter's static constructor (Unity 6000.2.13f1, iOS):

// class EmitStringAnalyzer: barrier emitted
((EmitStringAnalyzer_..._StaticFields*)il2cpp_codegen_static_fields_for(...))->___whiteSpaces = L_4;
Il2CppCodeGenWriteBarrier((void**)(&(...)->___whiteSpaces), (void*)L_4);

// ref struct Utf8YamlEmitter: no barrier
((Utf8YamlEmitter_..._StaticFields*)il2cpp_codegen_static_fields_for(...))->___FlowSequenceEmpty = L_8;

The same is true for the [ThreadStatic] buffers (stringBufferStatic etc.) and for WriteIndent reassigning whiteSpaces. 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)

_platform_memmove
Buffer.Memcpy
Utf8YamlEmitter.WriteRaw            <- WriteRaw(FlowSequenceEmpty, ...)
Utf8YamlEmitter.EndSequence
CollectionFormatterBase.Serialize
...
YamlSerializer.Serialize

Inspecting the process memory at the crash:

  • All 8 byte[] fields of Utf8YamlEmitter pointed to memory that had been freed and reused: 7 of them now held System.String objects (their "length" field read as UTF-16 text such as "tRes", "esul"), and whiteSpaces held an unrelated byte[].
  • The static field storage itself was intact, and the cctor had completed.
  • At the same moment, the static fields of reference types in the same assembly (EmitStringAnalyzer.whiteSpaces, YamlEmitOptions.Default) were alive and correct.
  • WriteRaw used the bogus length (~3.6 MB) of the "array" and read past it into a guard page.

Changes

  • Utf8YamlEmitter: move the 8 static byte[] fields and the 3 [ThreadStatic] buffers into a nested static class Statics.
  • Utf8YamlTokenizer: move the static stop-byte sets (byte[] / SearchValues<byte>) into a nested static 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

…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>
@hadashiA

Copy link
Copy Markdown
Owner

Makes sense. Thanks for the detailed investigation! I'm going to merge this.

@hadashiA
hadashiA merged commit 4ee2afc into hadashiA:master Oct 15, 2026
1 check passed
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.

2 participants