Skip to content

Commit 81cb1d1

Browse files
authored
Merge pull request #1 from xphorror/fix/il2cpp-runtime-safety
fix(il2cpp): harden runtime abstractions
2 parents 2719a69 + 683af2f commit 81cb1d1

13 files changed

Lines changed: 645 additions & 72 deletions

‎Android/library/build.gradle‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ plugins {
1010
android {
1111
namespace 'starray.android.modmanager'
1212
compileSdk 35
13-
ndkVersion "27.2.12479018"
13+
ndkVersion "25.2.9519653"
1414

1515
defaultConfig {
1616
minSdk 26
Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
using StArray.ModManager.Il2Cpp;
2+
using StArray.ModManager.RuntimeAbstractions;
3+
4+
namespace StArray.ModManager.Tests;
5+
6+
[TestFixture]
7+
[NonParallelizable]
8+
public sealed class Il2CppRuntimeRegressionTests
9+
{
10+
private RuntimeBackend _originalBackend;
11+
12+
[SetUp]
13+
public void SetUp()
14+
{
15+
_originalBackend = RuntimeManager.Backend;
16+
RuntimeManager.SetBackend(RuntimeBackend.Il2Cpp);
17+
}
18+
19+
[TearDown]
20+
public void TearDown()
21+
{
22+
RuntimeManager.SetBackend(_originalBackend);
23+
}
24+
25+
[Test]
26+
public void CurrentCachesDomainAndBalancesNestedAttachment()
27+
{
28+
var fake = new FakeIl2CppRuntimeApi();
29+
using var scope = Il2CppRuntimeApi.OverrideForTesting(fake);
30+
31+
var first = Il2CppDomain.Current;
32+
var second = Il2CppDomain.Current;
33+
34+
Assert.That(first, Is.Not.Null);
35+
Assert.That(second, Is.SameAs(first));
36+
37+
first!.ThreadAttach();
38+
second!.ThreadAttach();
39+
first.ThreadDetach();
40+
41+
Assert.Multiple(() =>
42+
{
43+
Assert.That(fake.DomainGetCalls, Is.EqualTo(1));
44+
Assert.That(fake.ThreadAttachCalls, Is.EqualTo(1));
45+
Assert.That(fake.ThreadDetachCalls, Is.Zero);
46+
});
47+
48+
second.ThreadDetach();
49+
Assert.That(fake.ThreadDetachCalls, Is.EqualTo(1));
50+
}
51+
52+
[Test]
53+
public void ExistingIl2CppThreadIsNeverDetachedByWrapper()
54+
{
55+
var fake = new FakeIl2CppRuntimeApi { CurrentThread = 0x4444 };
56+
using var scope = Il2CppRuntimeApi.OverrideForTesting(fake);
57+
var domain = Il2CppDomain.Current!;
58+
59+
domain.ThreadAttach();
60+
domain.ThreadDetach();
61+
62+
Assert.Multiple(() =>
63+
{
64+
Assert.That(fake.ThreadAttachCalls, Is.Zero);
65+
Assert.That(fake.ThreadDetachCalls, Is.Zero);
66+
});
67+
}
68+
69+
[Test]
70+
public void CurrentRetriesWhenDomainIsNotReadyYet()
71+
{
72+
var fake = new FakeIl2CppRuntimeApi { Domain = 0 };
73+
using var scope = Il2CppRuntimeApi.OverrideForTesting(fake);
74+
75+
Assert.That(Il2CppDomain.Current, Is.Null);
76+
fake.Domain = 0x100;
77+
78+
Assert.Multiple(() =>
79+
{
80+
Assert.That(Il2CppDomain.Current, Is.Not.Null);
81+
Assert.That(fake.DomainGetCalls, Is.EqualTo(2));
82+
});
83+
}
84+
85+
[Test]
86+
public void Il2CppArrayDataStartsAfterBoundsAndLength()
87+
{
88+
var fake = new FakeIl2CppRuntimeApi { UnboxedObject = 0x1000 };
89+
using var scope = Il2CppRuntimeApi.OverrideForTesting(fake);
90+
91+
var array = new RuntimeArray((nint)0x2000);
92+
93+
Assert.That(array.DataPtr, Is.EqualTo((nint)(0x1000 + nint.Size * 2)));
94+
}
95+
96+
[Test]
97+
public void Il2CppReferenceFieldUsesWriteBarrierAwareSetter()
98+
{
99+
var fake = new FakeIl2CppRuntimeApi { ReferenceField = true };
100+
using var scope = Il2CppRuntimeApi.OverrideForTesting(fake);
101+
var field = new Il2CppField(0x3000);
102+
103+
field.SetValue(0x4000, (nint)0x5000);
104+
105+
Assert.Multiple(() =>
106+
{
107+
Assert.That(fake.ObjectFieldSetCalls, Is.EqualTo(1));
108+
Assert.That(fake.ValueFieldSetCalls, Is.Zero);
109+
Assert.That(fake.LastObject, Is.EqualTo((nint)0x4000));
110+
Assert.That(fake.LastField, Is.EqualTo((nint)0x3000));
111+
Assert.That(fake.LastObjectValue, Is.EqualTo((nint)0x5000));
112+
});
113+
}
114+
115+
[TestCase(0x0e, true)]
116+
[TestCase(0x12, true)]
117+
[TestCase(0x14, true)]
118+
[TestCase(0x1c, true)]
119+
[TestCase(0x1d, true)]
120+
[TestCase(0x0f, false)]
121+
[TestCase(0x11, false)]
122+
public void Il2CppFieldTypeClassificationDoesNotTreatPointersAsObjects(int typeCode, bool expected)
123+
{
124+
Assert.That(NativeIl2CppRuntimeApi.IsDirectReferenceType(typeCode), Is.EqualTo(expected));
125+
}
126+
127+
[Test]
128+
public void Il2CppValueFieldUsesMetadataAwareFieldApi()
129+
{
130+
var fake = new FakeIl2CppRuntimeApi();
131+
using var scope = Il2CppRuntimeApi.OverrideForTesting(fake);
132+
var field = new Il2CppField(0x3000);
133+
134+
field.SetValue(0x4000, 123);
135+
136+
Assert.Multiple(() =>
137+
{
138+
Assert.That(fake.ValueFieldSetCalls, Is.EqualTo(1));
139+
Assert.That(fake.ObjectFieldSetCalls, Is.Zero);
140+
});
141+
}
142+
143+
[Test]
144+
public void RuntimeInvokePropagatesFormattedIl2CppException()
145+
{
146+
var fake = new FakeIl2CppRuntimeApi
147+
{
148+
InvokeException = 0x6000,
149+
ExceptionMessage = "System.InvalidOperationException: failed",
150+
ExceptionStackTrace = "at Game.Run()",
151+
};
152+
using var scope = Il2CppRuntimeApi.OverrideForTesting(fake);
153+
var method = new Il2CppMethod(0x7000);
154+
155+
var exception = Assert.Throws<Il2CppInvocationException>(() => method.Invoke(0x8000));
156+
157+
Assert.Multiple(() =>
158+
{
159+
Assert.That(exception!.NativeException, Is.EqualTo((nint)0x6000));
160+
Assert.That(exception.Message, Does.Contain("System.InvalidOperationException: failed"));
161+
Assert.That(exception.NativeStackTrace, Is.EqualTo("at Game.Run()"));
162+
Assert.That(exception.ToString(), Does.Contain("at Game.Run()"));
163+
});
164+
}
165+
166+
[Test]
167+
public void UnixProbeUsesNoLoadAndBalancesReturnedHandle()
168+
{
169+
var closed = nint.Zero;
170+
171+
var loaded = RuntimeManager.ProbeUnixLibrary(
172+
"libil2cpp.so",
173+
(_, flags) =>
174+
{
175+
Assert.That(flags, Is.EqualTo(RuntimeManager.RtldNow | RuntimeManager.RtldNoLoad));
176+
return 0x9000;
177+
},
178+
handle => closed = handle);
179+
180+
Assert.Multiple(() =>
181+
{
182+
Assert.That(RuntimeManager.RtldNoLoad, Is.EqualTo(0x4));
183+
Assert.That(loaded, Is.True);
184+
Assert.That(closed, Is.EqualTo((nint)0x9000));
185+
});
186+
}
187+
188+
[Test]
189+
public void UnixProbeDoesNotCloseMissingLibrary()
190+
{
191+
var closeCalls = 0;
192+
193+
var loaded = RuntimeManager.ProbeUnixLibrary(
194+
"missing.so",
195+
(_, _) => 0,
196+
_ => closeCalls++);
197+
198+
Assert.Multiple(() =>
199+
{
200+
Assert.That(loaded, Is.False);
201+
Assert.That(closeCalls, Is.Zero);
202+
});
203+
}
204+
205+
private sealed class FakeIl2CppRuntimeApi : IIl2CppRuntimeApi
206+
{
207+
public nint Domain { get; set; } = 0x100;
208+
public nint CurrentThread { get; init; }
209+
public nint UnboxedObject { get; init; }
210+
public bool ReferenceField { get; init; }
211+
public nint InvokeResult { get; init; }
212+
public nint InvokeException { get; init; }
213+
public string ExceptionMessage { get; init; } = "native exception";
214+
public string ExceptionStackTrace { get; init; } = "native stack";
215+
216+
public int ThreadAttachCalls { get; private set; }
217+
public int ThreadDetachCalls { get; private set; }
218+
public int DomainGetCalls { get; private set; }
219+
public int ObjectFieldSetCalls { get; private set; }
220+
public int ValueFieldSetCalls { get; private set; }
221+
public nint LastObject { get; private set; }
222+
public nint LastField { get; private set; }
223+
public nint LastObjectValue { get; private set; }
224+
225+
public nint DomainGet()
226+
{
227+
DomainGetCalls++;
228+
return Domain;
229+
}
230+
public nint ThreadCurrent() => CurrentThread;
231+
232+
public nint ThreadAttach(nint domain)
233+
{
234+
ThreadAttachCalls++;
235+
return 0x200;
236+
}
237+
238+
public void ThreadDetach(nint thread) => ThreadDetachCalls++;
239+
public nint ObjectUnbox(nint obj) => UnboxedObject;
240+
public int FieldGetFlags(nint field) => 0;
241+
public bool IsReferenceField(nint field) => ReferenceField;
242+
public T GetFieldValue<T>(nint obj, nint field, bool isStatic) where T : unmanaged => default;
243+
244+
public void SetFieldValue<T>(nint obj, nint field, bool isStatic, T value) where T : unmanaged
245+
{
246+
ValueFieldSetCalls++;
247+
}
248+
249+
public void SetObjectFieldValue(nint obj, nint field, nint value)
250+
{
251+
ObjectFieldSetCalls++;
252+
LastObject = obj;
253+
LastField = field;
254+
LastObjectValue = value;
255+
}
256+
257+
public nint RuntimeInvoke(nint method, nint obj, nint[]? args, out nint exception)
258+
{
259+
exception = InvokeException;
260+
return InvokeResult;
261+
}
262+
263+
public string FormatException(nint exception) => ExceptionMessage;
264+
public string FormatStackTrace(nint exception) => ExceptionStackTrace;
265+
}
266+
}

‎StArray.ModManager.Tests/PInvokeTests.cs‎

Lines changed: 0 additions & 14 deletions
This file was deleted.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
namespace StArray.ModManager.Tests;
2+
3+
[TestFixture]
4+
public sealed class StubAssemblyGeneratorTests
5+
{
6+
[TestCase("System.String", "RuntimeString")]
7+
[TestCase("System.Int32", "int")]
8+
[TestCase("Unknown.Managed.Type", "nint")]
9+
public void MapsRuntimeTypesToStableStubTypes(string runtimeType, string expected)
10+
{
11+
Assert.That(StubAssemblyGenerator.MapType(runtimeType), Is.EqualTo(expected));
12+
}
13+
14+
[Test]
15+
public void SanitizesIdentifiersAndEscapesGeneratedLiterals()
16+
{
17+
Assert.Multiple(() =>
18+
{
19+
Assert.That(StubAssemblyGenerator.SanitizeIdentifier("1Bad+Name"), Is.EqualTo("_1Bad_Name"));
20+
Assert.That(StubAssemblyGenerator.SanitizeNamespace("Game.Bad+Part"), Is.EqualTo("Game.Bad_Part"));
21+
Assert.That(StubAssemblyGenerator.EscapeString("a\\b\"c"), Is.EqualTo("a\\\\b\\\"c"));
22+
});
23+
}
24+
}

0 commit comments

Comments
 (0)