Skip to content

Commit 98ede6a

Browse files
committed
fix(analysis,protocol): track member access operation modes and sync packet IDs
- add MemberAccessOperation (Read/GetAddress/Write) and new TryApply* tracing APIs to fix value-type field mutation tracing for address/write paths in ParamModificationAnalyzer - add unit tests for access-mode behavior and value-type field-write detection - align MessageID definitions/aliases with newer protocol IDs and add DevCommands packet
1 parent 5ee21b4 commit 98ede6a

14 files changed

Lines changed: 455 additions & 137 deletions

File tree

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
using Mono.Cecil;
2+
using OTAPI.UnifiedServerProcess.Core.Analysis.DataModels.MemberAccess;
3+
using OTAPI.UnifiedServerProcess.Core.Analysis.ParameterFlowAnalysis;
4+
using OTAPI.UnifiedServerProcess.Core.Analysis.StaticFieldReferenceAnalysis;
5+
using Xunit;
6+
7+
namespace OTAPI.UnifiedServerProcess.UnitTests
8+
{
9+
public class MemberAccessOperationModeTests
10+
{
11+
[Fact]
12+
public void ParameterTracingChain_ReadRejectsValueTypeMember_ButGetAddressAndWriteAccept() {
13+
using var module = ModuleDefinition.CreateModule("USP.MemberAccess.Mode.Param", ModuleKind.Dll);
14+
CreateContainerTypes(module, out var containerType, out var valueField, out _);
15+
16+
var parameter = new ParameterDefinition("p", ParameterAttributes.None, containerType);
17+
var chain = new ParameterTracingChain(parameter, [], []);
18+
19+
Assert.False(chain.TryApplyMemberAccess(valueField, MemberAccessOperation.Read, out _));
20+
Assert.False(chain.TryExtendTracingWithMemberAccess(valueField, out _));
21+
22+
Assert.True(chain.TryApplyMemberAccess(valueField, MemberAccessOperation.GetAddress, out var byAddress));
23+
Assert.NotNull(byAddress);
24+
Assert.Single(byAddress.ComponentAccessPath);
25+
Assert.Equal(valueField.Name, byAddress.ComponentAccessPath[0].Name);
26+
27+
Assert.True(chain.TryApplyMemberAccess(valueField, MemberAccessOperation.Write, out var byWrite));
28+
Assert.NotNull(byWrite);
29+
Assert.Single(byWrite.ComponentAccessPath);
30+
Assert.Equal(valueField.Name, byWrite.ComponentAccessPath[0].Name);
31+
}
32+
33+
[Fact]
34+
public void ParameterTracingChain_LegacyExtend_EqualsReadMode_ForReferenceMember() {
35+
using var module = ModuleDefinition.CreateModule("USP.MemberAccess.Mode.Param.Legacy", ModuleKind.Dll);
36+
CreateContainerTypes(module, out var containerType, out _, out var referenceField);
37+
38+
var parameter = new ParameterDefinition("p", ParameterAttributes.None, containerType);
39+
var chain = new ParameterTracingChain(parameter, [], []);
40+
41+
bool readOk = chain.TryApplyMemberAccess(referenceField, MemberAccessOperation.Read, out var readResult);
42+
bool legacyOk = chain.TryExtendTracingWithMemberAccess(referenceField, out var legacyResult);
43+
44+
Assert.Equal(readOk, legacyOk);
45+
Assert.NotNull(readResult);
46+
Assert.NotNull(legacyResult);
47+
Assert.Equal(readResult.ComponentAccessPath.Length, legacyResult.ComponentAccessPath.Length);
48+
Assert.Equal(readResult.ComponentAccessPath[0].Name, legacyResult.ComponentAccessPath[0].Name);
49+
}
50+
51+
[Fact]
52+
public void StaticFieldTracingChain_ReadRejectsValueTypeMember_ButGetAddressAndWriteAccept() {
53+
using var module = ModuleDefinition.CreateModule("USP.MemberAccess.Mode.Static", ModuleKind.Dll);
54+
CreateContainerTypes(module, out var containerType, out var valueField, out _);
55+
56+
var host = new TypeDefinition("Tests", "Host",
57+
TypeAttributes.Public | TypeAttributes.Class,
58+
module.TypeSystem.Object);
59+
module.Types.Add(host);
60+
var tracedStaticField = new FieldDefinition("Root", FieldAttributes.Public | FieldAttributes.Static, containerType);
61+
host.Fields.Add(tracedStaticField);
62+
63+
var chain = new StaticFieldTracingChain(tracedStaticField, [], []);
64+
65+
Assert.False(chain.TryApplyMemberAccess(valueField, MemberAccessOperation.Read, out _));
66+
Assert.False(chain.TryExtendTracingWithMemberAccess(valueField, out _));
67+
68+
Assert.True(chain.TryApplyMemberAccess(valueField, MemberAccessOperation.GetAddress, out var byAddress));
69+
Assert.NotNull(byAddress);
70+
Assert.Single(byAddress.ComponentAccessPath);
71+
Assert.Equal(valueField.Name, byAddress.ComponentAccessPath[0].Name);
72+
73+
Assert.True(chain.TryApplyMemberAccess(valueField, MemberAccessOperation.Write, out var byWrite));
74+
Assert.NotNull(byWrite);
75+
Assert.Single(byWrite.ComponentAccessPath);
76+
Assert.Equal(valueField.Name, byWrite.ComponentAccessPath[0].Name);
77+
}
78+
79+
[Fact]
80+
public void StaticFieldTracingChain_LegacyExtend_EqualsReadMode_ForReferenceMember() {
81+
using var module = ModuleDefinition.CreateModule("USP.MemberAccess.Mode.Static.Legacy", ModuleKind.Dll);
82+
CreateContainerTypes(module, out var containerType, out _, out var referenceField);
83+
84+
var host = new TypeDefinition("Tests", "Host",
85+
TypeAttributes.Public | TypeAttributes.Class,
86+
module.TypeSystem.Object);
87+
module.Types.Add(host);
88+
var tracedStaticField = new FieldDefinition("Root", FieldAttributes.Public | FieldAttributes.Static, containerType);
89+
host.Fields.Add(tracedStaticField);
90+
91+
var chain = new StaticFieldTracingChain(tracedStaticField, [], []);
92+
93+
bool readOk = chain.TryApplyMemberAccess(referenceField, MemberAccessOperation.Read, out var readResult);
94+
bool legacyOk = chain.TryExtendTracingWithMemberAccess(referenceField, out var legacyResult);
95+
96+
Assert.Equal(readOk, legacyOk);
97+
Assert.NotNull(readResult);
98+
Assert.NotNull(legacyResult);
99+
Assert.Equal(readResult.ComponentAccessPath.Length, legacyResult.ComponentAccessPath.Length);
100+
Assert.Equal(readResult.ComponentAccessPath[0].Name, legacyResult.ComponentAccessPath[0].Name);
101+
}
102+
103+
private static void CreateContainerTypes(
104+
ModuleDefinition module,
105+
out TypeDefinition containerType,
106+
out FieldDefinition valueField,
107+
out FieldDefinition referenceField) {
108+
109+
var payloadType = new TypeDefinition(
110+
"Tests",
111+
"Payload",
112+
TypeAttributes.Public | TypeAttributes.SequentialLayout | TypeAttributes.Sealed | TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit,
113+
module.ImportReference(typeof(ValueType)));
114+
module.Types.Add(payloadType);
115+
payloadType.Fields.Add(new FieldDefinition("Number", FieldAttributes.Public, module.TypeSystem.Int32));
116+
117+
containerType = new TypeDefinition(
118+
"Tests",
119+
"Container",
120+
TypeAttributes.Public | TypeAttributes.Class,
121+
module.TypeSystem.Object);
122+
module.Types.Add(containerType);
123+
124+
valueField = new FieldDefinition("PayloadField", FieldAttributes.Public, payloadType);
125+
referenceField = new FieldDefinition("ReferenceField", FieldAttributes.Public, module.TypeSystem.Object);
126+
containerType.Fields.Add(valueField);
127+
containerType.Fields.Add(referenceField);
128+
}
129+
}
130+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
using ModFramework;
2+
using Mono.Cecil;
3+
using Mono.Cecil.Cil;
4+
using OTAPI.UnifiedServerProcess.Core;
5+
using OTAPI.UnifiedServerProcess.Core.FunctionalFeatures;
6+
using OTAPI.UnifiedServerProcess.Extensions;
7+
using OTAPI.UnifiedServerProcess.Loggers;
8+
using System;
9+
using System.IO;
10+
using Xunit;
11+
12+
namespace OTAPI.UnifiedServerProcess.UnitTests
13+
{
14+
public class ParamModificationAnalyzerValueTypeMutationTests
15+
{
16+
[Fact]
17+
public void ParamModificationAnalyzer_DetectsValueTypeFieldWriteThroughAddress() {
18+
using var module = CreateModuleWithResolver("USP.ParamModification.ValueTypeWrite");
19+
20+
var valueType = new TypeDefinition(
21+
"Tests",
22+
"Payload",
23+
TypeAttributes.Public | TypeAttributes.SequentialLayout | TypeAttributes.Sealed | TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit,
24+
module.ImportReference(typeof(ValueType)));
25+
module.Types.Add(valueType);
26+
var valueField = new FieldDefinition("Number", FieldAttributes.Public, module.TypeSystem.Int32);
27+
valueType.Fields.Add(valueField);
28+
29+
var holderType = new TypeDefinition(
30+
"Tests",
31+
"Holder",
32+
TypeAttributes.Public | TypeAttributes.Class,
33+
module.TypeSystem.Object);
34+
module.Types.Add(holderType);
35+
var payloadField = new FieldDefinition("PayloadField", FieldAttributes.Public, valueType);
36+
holderType.Fields.Add(payloadField);
37+
38+
var hostType = new TypeDefinition(
39+
"Tests",
40+
"Host",
41+
TypeAttributes.Public | TypeAttributes.Abstract | TypeAttributes.Sealed | TypeAttributes.Class,
42+
module.TypeSystem.Object);
43+
module.Types.Add(hostType);
44+
45+
var mutateMethod = new MethodDefinition(
46+
"Mutate",
47+
MethodAttributes.Public | MethodAttributes.Static,
48+
module.TypeSystem.Void);
49+
mutateMethod.Parameters.Add(new ParameterDefinition("holder", ParameterAttributes.None, holderType));
50+
hostType.Methods.Add(mutateMethod);
51+
52+
var il = mutateMethod.Body.GetILProcessor();
53+
il.Append(il.Create(OpCodes.Ldarg_0));
54+
il.Append(il.Create(OpCodes.Ldflda, payloadField));
55+
il.Append(il.Create(OpCodes.Ldc_I4, 42));
56+
il.Append(il.Create(OpCodes.Stfld, valueField));
57+
il.Append(il.Create(OpCodes.Ret));
58+
59+
var analyzers = new AnalyzerGroups(new NullLogger(), module);
60+
var methodId = mutateMethod.GetIdentifier();
61+
62+
Assert.True(analyzers.ParamModificationAnalyzer.ModifiedParameters.TryGetValue(methodId, out var methodMutations));
63+
Assert.True(methodMutations.TryGetValue(0, out var parameterMutations));
64+
Assert.Contains(parameterMutations.Mutations, m =>
65+
m.ModificationAccessPath.Length == 2
66+
&& m.ModificationAccessPath[0].Name == payloadField.Name
67+
&& m.ModificationAccessPath[1].Name == valueField.Name);
68+
}
69+
70+
private static ModuleDefinition CreateModuleWithResolver(string name) {
71+
var resolver = new DefaultAssemblyResolver();
72+
resolver.AddSearchDirectory(AppContext.BaseDirectory);
73+
resolver.AddSearchDirectory(Path.GetDirectoryName(typeof(object).Assembly.Location)!);
74+
resolver.AddSearchDirectory(Path.GetDirectoryName(typeof(DefaultCollection<>).Assembly.Location)!);
75+
76+
var parameters = new ModuleParameters {
77+
Kind = ModuleKind.Dll,
78+
AssemblyResolver = resolver,
79+
};
80+
return ModuleDefinition.CreateModule(name, parameters);
81+
}
82+
83+
private sealed class NullLogger : ILogger
84+
{
85+
public void Progress(ILoggedComponent sender, int iteration, int progress, int total, string message, int indent = 0) { }
86+
public void Progress(ILoggedComponent sender, int progress, int total, string message, int indent = 0) { }
87+
public void Progress(ILoggedComponent sender, int iteration, int progress, int total, string message, int indent = 0, params object[] args) { }
88+
public void Progress(ILoggedComponent sender, int progress, int total, string message, int indent = 0, params object[] args) { }
89+
public void Debug(ILoggedComponent sender, int indent, string log, params object[] args) { }
90+
public void Info(ILoggedComponent sender, int indent, string log, params object[] args) { }
91+
public void Warn(ILoggedComponent sender, int indent, string log, params object[] args) { }
92+
public void Error(ILoggedComponent sender, int indent, string log, Exception ex, params object[] args) { }
93+
public void Error(ILoggedComponent sender, int indent, string log, params object[] args) { }
94+
public void Fatal(ILoggedComponent sender, int indent, string log, params object[] args) { }
95+
public void Debug(ILoggedComponent sender, string log, params object[] args) { }
96+
public void Info(ILoggedComponent sender, string log, params object[] args) { }
97+
public void Warn(ILoggedComponent sender, string log, params object[] args) { }
98+
public void Error(ILoggedComponent sender, string log, Exception ex, params object[] args) { }
99+
public void Error(ILoggedComponent sender, string log, params object[] args) { }
100+
public void Fatal(ILoggedComponent sender, string log, params object[] args) { }
101+
}
102+
}
103+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace OTAPI.UnifiedServerProcess.Core.Analysis.DataModels.MemberAccess
2+
{
3+
public enum MemberAccessOperation
4+
{
5+
Read,
6+
GetAddress,
7+
Write,
8+
}
9+
}

src/OTAPI.UnifiedServerProcess/Core/Analysis/ParamModificationAnalysis/ParamModificationAnalyzer.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,11 @@ static bool IsAtomicModificationMethod(MethodReference callee) {
251251

252252
void HandleModifyField(Instruction instruction) {
253253
FieldReference field = (FieldReference)instruction.Operand;
254+
MemberAccessOperation operation = instruction.OpCode.Code switch {
255+
Code.Stfld => MemberAccessOperation.Write,
256+
Code.Ldflda => MemberAccessOperation.GetAddress,
257+
_ => MemberAccessOperation.Read,
258+
};
254259
foreach (var path in MonoModCommon.Stack.AnalyzeInstructionArgsSources(method, instruction, jumpSites)) {
255260
foreach (var loadModifyingInstance in MonoModCommon.Stack.AnalyzeStackTopTypeAllPaths(method, path.ParametersSources[0].Instructions.Last(), jumpSites)) {
256261

@@ -267,7 +272,7 @@ void HandleModifyField(Instruction instruction) {
267272
continue;
268273
}
269274

270-
if (!tracedStackData.TryExtendTracingWithMemberAccess(field, out var modified)) {
275+
if (!tracedStackData.TryApplyMemberAccess(field, operation, out var modified)) {
271276
continue;
272277
}
273278

0 commit comments

Comments
 (0)