From 3d6eb6a501978da3519f50b6ebfb19d8ddb22a0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=96=A4=20ShadowNine=20=F0=9F=92=9B?= Date: Mon, 19 Oct 2026 13:40:46 +0200 Subject: [PATCH 1/2] Verify memory-record parent assignments Refuse unsupported root detach before calling the nil setter, verify supported assignments through fresh lookup and parent ID readback, and preserve started failures without retrying. Keep existing outcome enums for Client compatibility. Validation: 20 focused mutation tests, 740 Engine NativeLua tests, formatting, whitespace, and independent Astra review. --- .../AddressList/AddressListMutations.cs | 238 ++++++++++++++---- .../AddressList/MemoryRecordMutationEffect.cs | 7 +- .../MemoryRecordMutationOutcome.cs | 22 +- .../MemoryRecordMutationProblem.cs | 5 +- .../AddressList/AddressListMutationsTests.cs | 205 +++++++++++++-- 5 files changed, 403 insertions(+), 74 deletions(-) diff --git a/libs/CheatEngine.SDK.Engine/AddressList/AddressListMutations.cs b/libs/CheatEngine.SDK.Engine/AddressList/AddressListMutations.cs index 721ed89a..6359665b 100644 --- a/libs/CheatEngine.SDK.Engine/AddressList/AddressListMutations.cs +++ b/libs/CheatEngine.SDK.Engine/AddressList/AddressListMutations.cs @@ -100,9 +100,17 @@ public static MemoryRecordMutationOutcome Delete(MemoryRecordId recordId) /// Assigns a record's parent after validating the requested hierarchy with the default traversal bound. /// The child record's CE ID in the current list. - /// The new parent's CE ID, or to make the child a root record. + /// + /// The new parent's CE ID, or to require that the child is already a root record. + /// /// A command result that preserves whether assignment was started. /// The plugin is not enabled or the calling thread has no Lua state. + /// + /// Cheat Engine exposes no safe Lua operation for detaching an existing nested record to the address-list root. + /// A request therefore completes only when the protected Parent read already + /// reports a root record; otherwise it is refused with + /// before the setter is called. + /// [RequiresPluginEnabled] public static MemoryRecordMutationOutcome SetParent(MemoryRecordId recordId, MemoryRecordId? parentId) { @@ -111,7 +119,9 @@ public static MemoryRecordMutationOutcome SetParent(MemoryRecordId recordId, Mem /// Assigns a record's parent after validating the requested hierarchy with . /// The child record's CE ID in the current list. - /// The new parent's CE ID, or to make the child a root record. + /// + /// The new parent's CE ID, or to require that the child is already a root record. + /// /// A positive bound for the proposed parent's existing parent chain. /// A command result that preserves whether assignment was started. /// The plugin is not enabled or the calling thread has no Lua state. @@ -139,8 +149,52 @@ public static MemoryRecordMutationOutcome SetParent(MemoryRecordId recordId, Mem bool mutationStarted = false; try { - MemoryRecordMutationOutcome preflight = TryPrepareParentAssignment(state, recordId, parentId, - traversalLimit, out MemoryRecord child, out MemoryRecord parent); + MemoryRecordMutationOutcome preflight = TryGetCurrentList(state, out AddressList list); + if (preflight.Problem != MemoryRecordMutationProblem.None) + { + return preflight; + } + + if (LuaRuntime.CurrentStateIdentity != identity) + { + return NotAttempted(MemoryRecordMutationProblem.RuntimeIdentityChanged); + } + + preflight = TryResolveRecord(state, list, recordId, false, out MemoryRecord child); + if (preflight.Problem != MemoryRecordMutationProblem.None) + { + return preflight; + } + + if (LuaRuntime.CurrentStateIdentity != identity) + { + return NotAttempted(MemoryRecordMutationProblem.RuntimeIdentityChanged); + } + + if (!parentId.HasValue) + { + preflight = TryReadParentId(state, child, out bool hasParent, out _, out bool rootReadChangedIdentity); + if (preflight.Problem != MemoryRecordMutationProblem.None) + { + if (rootReadChangedIdentity) + { + return NotAttempted(MemoryRecordMutationProblem.RuntimeIdentityChanged); + } + + return preflight; + } + + if (LuaRuntime.CurrentStateIdentity != identity) + { + return NotAttempted(MemoryRecordMutationProblem.RuntimeIdentityChanged); + } + + return hasParent + ? NotAttempted(MemoryRecordMutationProblem.GlobalUnavailable) + : Completed(); + } + + preflight = TryResolveRecord(state, list, parentId.Value, true, out MemoryRecord parent); if (preflight.Problem != MemoryRecordMutationProblem.None) { return preflight; @@ -151,18 +205,66 @@ public static MemoryRecordMutationOutcome SetParent(MemoryRecordId recordId, Mem return NotAttempted(MemoryRecordMutationProblem.RuntimeIdentityChanged); } - if (parentId.HasValue) + preflight = ValidateParentChain(state, recordId, parent, traversalLimit, identity); + if (preflight.Problem != MemoryRecordMutationProblem.None) { - parent.Handle.Push(state); + return preflight; } - else + + if (LuaRuntime.CurrentStateIdentity != identity) { - state.PushNil(); + return NotAttempted(MemoryRecordMutationProblem.RuntimeIdentityChanged); } + parent.Handle.Push(state); mutationStarted = true; LuaStatus status = child.Handle.TrySetProperty(state, "Parent"u8); - return status.IsOk ? Completed() : Indeterminate(status); + if (!status.IsOk) + { + return Indeterminate(status); + } + + // Admission prevents state resets; retain this defensive identity check and re-resolve records after callbacks. + if (LuaRuntime.CurrentStateIdentity != identity) + { + return Indeterminate(MemoryRecordMutationProblem.InvalidResult, LuaStatus.Ok); + } + + preflight = TryGetCurrentList(state, out list); + if (preflight.Problem != MemoryRecordMutationProblem.None) + { + return IndeterminateAfterParentSetter(preflight); + } + + if (LuaRuntime.CurrentStateIdentity != identity) + { + return Indeterminate(MemoryRecordMutationProblem.InvalidResult, LuaStatus.Ok); + } + + preflight = TryResolveRecord(state, list, recordId, false, out MemoryRecord rereadChild); + if (preflight.Problem != MemoryRecordMutationProblem.None) + { + return IndeterminateAfterParentSetter(preflight); + } + + if (LuaRuntime.CurrentStateIdentity != identity) + { + return Indeterminate(MemoryRecordMutationProblem.InvalidResult, LuaStatus.Ok); + } + + preflight = TryReadParentId(state, rereadChild, out bool hasActualParent, out MemoryRecordId actualParent, + out _); + if (preflight.Problem != MemoryRecordMutationProblem.None) + { + return IndeterminateAfterParentSetter(preflight); + } + + if (LuaRuntime.CurrentStateIdentity != identity || !hasActualParent || actualParent != parentId.Value) + { + return Indeterminate(MemoryRecordMutationProblem.InvalidResult, LuaStatus.Ok); + } + + return Completed(); } catch (LuaException exception) { @@ -335,38 +437,6 @@ private static MemoryRecordActivationOutcome Activation(MemoryRecordActivationOu return new MemoryRecordActivationOutcome(kind, problem, requested, before, after, asyncProcessing, status); } - private static MemoryRecordMutationOutcome TryPrepareParentAssignment(LuaState state, MemoryRecordId recordId, - MemoryRecordId? parentId, MemoryRecordParentTraversalLimit traversalLimit, out MemoryRecord child, - out MemoryRecord parent) - { - child = default; - parent = default; - MemoryRecordMutationOutcome preflight = TryGetCurrentList(state, out AddressList list); - if (preflight.Problem != MemoryRecordMutationProblem.None) - { - return preflight; - } - - preflight = TryResolveRecord(state, list, recordId, false, out child); - if (preflight.Problem != MemoryRecordMutationProblem.None) - { - return preflight; - } - - if (!parentId.HasValue) - { - return Completed(); - } - - preflight = TryResolveRecord(state, list, parentId.Value, true, out parent); - if (preflight.Problem != MemoryRecordMutationProblem.None) - { - return preflight; - } - - return ValidateParentChain(state, recordId, parent, traversalLimit); - } - private static MemoryRecordMutationOutcome TryResolveRecord(LuaState state, AddressList list, MemoryRecordId id, bool isParent, out MemoryRecord record) { @@ -434,7 +504,7 @@ private static MemoryRecordMutationOutcome TryGetCurrentList(LuaState state, out } private static MemoryRecordMutationOutcome ValidateParentChain(LuaState state, MemoryRecordId childId, - MemoryRecord proposedParent, MemoryRecordParentTraversalLimit traversalLimit) + MemoryRecord proposedParent, MemoryRecordParentTraversalLimit traversalLimit, LuaStateIdentity identity) { HashSet seen = new(); MemoryRecord current = proposedParent; @@ -446,6 +516,11 @@ private static MemoryRecordMutationOutcome ValidateParentChain(LuaState state, M return FromPreflightStatus(status); } + if (LuaRuntime.CurrentStateIdentity != identity) + { + return NotAttempted(MemoryRecordMutationProblem.RuntimeIdentityChanged); + } + if (!MemoryRecordId.TryRead(state, -1, out MemoryRecordId currentId)) { return NotAttempted(MemoryRecordMutationProblem.InvalidResult); @@ -462,6 +537,11 @@ private static MemoryRecordMutationOutcome ValidateParentChain(LuaState state, M return FromPreflightStatus(status); } + if (LuaRuntime.CurrentStateIdentity != identity) + { + return NotAttempted(MemoryRecordMutationProblem.RuntimeIdentityChanged); + } + if (state.IsNil(-1)) { return Completed(); @@ -479,6 +559,65 @@ private static MemoryRecordMutationOutcome ValidateParentChain(LuaState state, M } } + private static MemoryRecordMutationOutcome TryReadParentId(LuaState state, MemoryRecord child, out bool hasParent, + out MemoryRecordId parentId, out bool identityChanged) + { + int top = state.Top; + LuaStateIdentity identity = LuaRuntime.CurrentStateIdentity; + hasParent = false; + parentId = default; + identityChanged = false; + LuaStatus status = child.Handle.TryGetProperty(state, "Parent"u8); + if (!status.IsOk) + { + state.SetTop(top); + return NotAttempted(MemoryRecordMutationProblem.LuaFailure, status); + } + + if (LuaRuntime.CurrentStateIdentity != identity) + { + state.SetTop(top); + identityChanged = true; + return NotAttempted(MemoryRecordMutationProblem.InvalidResult); + } + + if (state.IsNil(-1)) + { + state.SetTop(top); + return Completed(); + } + + if (!MemoryRecord.TryRead(state, -1, out MemoryRecord parent)) + { + state.SetTop(top); + return NotAttempted(MemoryRecordMutationProblem.InvalidResult); + } + + status = parent.Handle.TryGetProperty(state, "ID"u8); + if (!status.IsOk) + { + state.SetTop(top); + return NotAttempted(MemoryRecordMutationProblem.LuaFailure, status); + } + + if (LuaRuntime.CurrentStateIdentity != identity) + { + state.SetTop(top); + identityChanged = true; + return NotAttempted(MemoryRecordMutationProblem.InvalidResult); + } + + if (!MemoryRecordId.TryRead(state, -1, out parentId)) + { + state.SetTop(top); + return NotAttempted(MemoryRecordMutationProblem.InvalidResult); + } + + state.SetTop(top); + hasParent = true; + return Completed(); + } + private static MemoryRecordMutationOutcome FromPreflightStatus(LuaStatus status) { return NotAttempted(MemoryRecordMutationProblem.LuaFailure, status); @@ -502,8 +641,19 @@ private static MemoryRecordMutationOutcome NotAttempted(MemoryRecordMutationProb private static MemoryRecordMutationOutcome Indeterminate(LuaStatus status) { - return new MemoryRecordMutationOutcome(MemoryRecordMutationEffect.Indeterminate, - MemoryRecordMutationProblem.LuaFailure, status); + return Indeterminate(MemoryRecordMutationProblem.LuaFailure, status); + } + + private static MemoryRecordMutationOutcome Indeterminate(MemoryRecordMutationProblem problem, LuaStatus status) + { + return new MemoryRecordMutationOutcome(MemoryRecordMutationEffect.Indeterminate, problem, status); + } + + private static MemoryRecordMutationOutcome IndeterminateAfterParentSetter(MemoryRecordMutationOutcome outcome) + { + return outcome.Problem == MemoryRecordMutationProblem.LuaFailure + ? Indeterminate(MemoryRecordMutationProblem.LuaFailure, outcome.LuaStatus) + : Indeterminate(MemoryRecordMutationProblem.InvalidResult, LuaStatus.Ok); } // Progress of one SetActive command, shared with the catch block of its caller. diff --git a/libs/CheatEngine.SDK.Engine/AddressList/MemoryRecordMutationEffect.cs b/libs/CheatEngine.SDK.Engine/AddressList/MemoryRecordMutationEffect.cs index 87b93818..9bb0690e 100644 --- a/libs/CheatEngine.SDK.Engine/AddressList/MemoryRecordMutationEffect.cs +++ b/libs/CheatEngine.SDK.Engine/AddressList/MemoryRecordMutationEffect.cs @@ -6,9 +6,12 @@ public enum MemoryRecordMutationEffect /// No CE mutation was started. NotAttempted, - /// The CE mutation completed successfully. + /// The CE mutation completed successfully, including any required postcondition read-back. Completed, - /// The CE mutation started but returned a protected failure, so its final host state is unknown. + /// + /// The CE mutation started but returned a protected failure or left its postcondition unconfirmed, so its final + /// host state is unknown. + /// Indeterminate } diff --git a/libs/CheatEngine.SDK.Engine/AddressList/MemoryRecordMutationOutcome.cs b/libs/CheatEngine.SDK.Engine/AddressList/MemoryRecordMutationOutcome.cs index 031b1b69..12e77953 100644 --- a/libs/CheatEngine.SDK.Engine/AddressList/MemoryRecordMutationOutcome.cs +++ b/libs/CheatEngine.SDK.Engine/AddressList/MemoryRecordMutationOutcome.cs @@ -6,16 +6,18 @@ namespace CheatEngine.SDK.Engine.AddressList; /// A structured result from a typed address-list record mutation. /// -/// -/// The result describes the command only. It intentionally does not capture a record snapshot: callers that -/// need a post-command view must obtain a fresh snapshot after a -/// -/// result, and must not merge a snapshot-read failure with the mutation result. -/// -/// -/// means a protected call began and then failed. Do -/// not blindly retry it; CE may have applied part or all of the mutation before reporting the error. -/// +/// +/// The result describes the command only. It intentionally does not capture a record snapshot: callers that need a +/// post-command view must obtain a fresh snapshot after a +/// result, and must not merge a snapshot-read failure with the mutation result. Parent assignment is an exception to +/// the usual command-only shape: it reports only after a fresh +/// child lookup confirms the requested parent identifier. +/// +/// +/// means a protected call started but either failed or left +/// its postcondition unconfirmed. Do not blindly retry it; CE may have applied part or all of the mutation before +/// reporting the error or invalid result. +/// /// [StructLayout(LayoutKind.Sequential)] public readonly struct MemoryRecordMutationOutcome diff --git a/libs/CheatEngine.SDK.Engine/AddressList/MemoryRecordMutationProblem.cs b/libs/CheatEngine.SDK.Engine/AddressList/MemoryRecordMutationProblem.cs index 7b71ff01..5c474518 100644 --- a/libs/CheatEngine.SDK.Engine/AddressList/MemoryRecordMutationProblem.cs +++ b/libs/CheatEngine.SDK.Engine/AddressList/MemoryRecordMutationProblem.cs @@ -27,7 +27,10 @@ public enum MemoryRecordMutationProblem /// The parent walk reached its explicit safety limit. TraversalLimitReached, - /// A required CE global or member was absent or not callable. + /// + /// A required CE global or member was absent or not callable, or Cheat Engine exposes no safe operation for the + /// requested mutation (for example, detaching a nested record to the address-list root). + /// GlobalUnavailable, /// A protected CE access or call raised. diff --git a/tests/CheatEngine.SDK.Engine.Tests/AddressList/AddressListMutationsTests.cs b/tests/CheatEngine.SDK.Engine.Tests/AddressList/AddressListMutationsTests.cs index ee290090..2462a5b1 100644 --- a/tests/CheatEngine.SDK.Engine.Tests/AddressList/AddressListMutationsTests.cs +++ b/tests/CheatEngine.SDK.Engine.Tests/AddressList/AddressListMutationsTests.cs @@ -5,6 +5,7 @@ using CheatEngine.SDK.Engine.Tests.Support; using CheatEngine.SDK.Lua.Calls; using CheatEngine.SDK.Lua.Marshalling; +using CheatEngine.SDK.Lua.Runtime; using CheatEngine.SDK.Lua.State; using CheatEngine.SDK.Tests.Shared.NativeLua; @@ -124,19 +125,172 @@ public void Set_parent_obeys_an_explicit_parent_walk_bound() } [Fact] - public void Set_parent_assigns_a_typed_parent_or_lua_nil_for_root() + public void Set_parent_verifies_the_fresh_parent_after_one_typed_assignment() { using MutationFixture fixture = new(); int top = fixture.State.Top; MemoryRecordMutationOutcome parent = AddressListMutations.SetParent(new MemoryRecordId(1), new MemoryRecordId(2)); - fixture.Execute("assert(assigned == 1)"); - MemoryRecordMutationOutcome root = AddressListMutations.SetParent(new MemoryRecordId(1), null); Assert.True(parent.IsCompleted); - Assert.True(root.IsCompleted); - fixture.Execute("assert(assigned == 2)"); + fixture.RunOnChild("assert(o.props.Parent.ID == parent.ID)"); + fixture.Execute("assert(assigned == 1)"); + Assert.Equal(top, fixture.State.Top); + } + + [Fact] + public void Set_parent_root_request_is_a_verified_no_op_or_an_unavailable_host_operation() + { + using MutationFixture fixture = new(); + int top = fixture.State.Top; + + MemoryRecordMutationOutcome alreadyRoot = AddressListMutations.SetParent(new MemoryRecordId(1), null); + + Assert.True(alreadyRoot.IsCompleted); + fixture.RunOnChild("assert(o.props.Parent == nil)"); + fixture.Execute("assert(assigned == 0)"); + + fixture.RunOnChild("o.props.Parent = parent"); + MemoryRecordMutationOutcome nested = AddressListMutations.SetParent(new MemoryRecordId(1), null); + + Assert.Equal(MemoryRecordMutationEffect.NotAttempted, nested.Effect); + Assert.Equal(MemoryRecordMutationProblem.GlobalUnavailable, nested.Problem); + // This setter models CE's silently ignored nil assignment. The SDK must never call it. + fixture.RunOnChild("assert(o.props.Parent.ID == parent.ID)"); + fixture.Execute("assert(assigned == 0)"); + Assert.Equal(top, fixture.State.Top); + } + + [Fact] + public void Set_parent_root_request_reports_missing_child_without_calling_a_nil_setter() + { + using MutationFixture fixture = new(); + int top = fixture.State.Top; + + MemoryRecordMutationOutcome missing = AddressListMutations.SetParent(new MemoryRecordId(99), null); + + Assert.Equal(MemoryRecordMutationEffect.NotAttempted, missing.Effect); + Assert.Equal(MemoryRecordMutationProblem.RecordNotFound, missing.Problem); + fixture.Execute("assert(assigned == 0)"); + Assert.Equal(top, fixture.State.Top); + } + + [Theory] + [InlineData("assigned = assigned + 1")] + [InlineData("assigned = assigned + 1; o.props.Parent = grandparent")] + public void Set_parent_silent_or_wrong_host_result_is_indeterminate(string setterBody) + { + using MutationFixture fixture = new(); + int top = fixture.State.Top; + fixture.RunOnChild("o.setters.Parent = function(o, v) " + setterBody + " end"); + + MemoryRecordMutationOutcome outcome = + AddressListMutations.SetParent(new MemoryRecordId(1), new MemoryRecordId(2)); + + Assert.Equal(MemoryRecordMutationEffect.Indeterminate, outcome.Effect); + Assert.Equal(MemoryRecordMutationProblem.InvalidResult, outcome.Problem); + fixture.Execute("assert(assigned == 1)"); + Assert.Equal(top, fixture.State.Top); + } + + [Theory] + [InlineData("error('parent read failed')", MemoryRecordMutationProblem.LuaFailure)] + [InlineData("return 42", MemoryRecordMutationProblem.InvalidResult)] + public void Set_parent_post_setter_parent_read_failure_is_indeterminate(string parentGetter, + MemoryRecordMutationProblem expectedProblem) + { + using MutationFixture fixture = new(); + int top = fixture.State.Top; + fixture.RunOnChild("o.setters.Parent = function(o, v) assigned = assigned + 1; o.props.Parent = v; o.getters.Parent = function() " + parentGetter + " end end"); + + MemoryRecordMutationOutcome outcome = + AddressListMutations.SetParent(new MemoryRecordId(1), new MemoryRecordId(2)); + + Assert.Equal(MemoryRecordMutationEffect.Indeterminate, outcome.Effect); + Assert.Equal(expectedProblem, outcome.Problem); + fixture.Execute("assert(assigned == 1)"); + Assert.Equal(top, fixture.State.Top); + } + + [Fact] + public void Set_parent_missing_post_setter_child_is_indeterminate() + { + using MutationFixture fixture = new(); + int top = fixture.State.Top; + fixture.RunOnChild("o.setters.Parent = function(o, v) assigned = assigned + 1; records[1] = nil end"); + + MemoryRecordMutationOutcome outcome = + AddressListMutations.SetParent(new MemoryRecordId(1), new MemoryRecordId(2)); + + Assert.Equal(MemoryRecordMutationEffect.Indeterminate, outcome.Effect); + Assert.Equal(MemoryRecordMutationProblem.InvalidResult, outcome.Problem); + fixture.Execute("assert(assigned == 1)"); + Assert.Equal(top, fixture.State.Top); + } + + [Fact] + public void Set_parent_missing_post_setter_list_is_indeterminate() + { + using MutationFixture fixture = new(); + int top = fixture.State.Top; + fixture.RunOnChild("o.setters.Parent = function(o, v) assigned = assigned + 1; o.props.Parent = v; address_list_lookup = function() return nil end end"); + + MemoryRecordMutationOutcome outcome = + AddressListMutations.SetParent(new MemoryRecordId(1), new MemoryRecordId(2)); + + Assert.Equal(MemoryRecordMutationEffect.Indeterminate, outcome.Effect); + Assert.Equal(MemoryRecordMutationProblem.InvalidResult, outcome.Problem); + fixture.Execute("assert(assigned == 1)"); + Assert.Equal(top, fixture.State.Top); + } + + [Theory] + [InlineData("preflight")] + [InlineData("setter")] + [InlineData("readback")] + [InlineData("root")] + public void Set_parent_admission_rejects_reentrant_resets_and_preserves_the_verified_result(string scenario) + { + using MutationFixture fixture = new(); + int top = fixture.State.Top; + LuaStateIdentity identity = LuaRuntime.CurrentStateIdentity; + int resetAttempts = 0; + bool rootRequest = string.Equals(scenario, "root", StringComparison.Ordinal); + using (FakeHost.InstallReentrantHook(fixture.State, "attempt_reset", () => + { + resetAttempts++; + FakeHost.ReplaceStateGeneration(); + })) + { + switch (scenario) + { + case "preflight": + fixture.RunOnParent("o.getters.Parent = function() attempt_reset(); return nil end"); + break; + case "setter": + fixture.RunOnChild("o.setters.Parent = function(o, v) assigned = assigned + 1; o.props.Parent = v; attempt_reset() end"); + break; + case "readback": + fixture.RunOnChild("o.setters.Parent = function(o, v) assigned = assigned + 1; o.props.Parent = v; o.getters.Parent = function() attempt_reset(); return o.props.Parent end end"); + break; + case "root": + fixture.RunOnChild("o.getters.Parent = function() attempt_reset(); return nil end"); + break; + } + + MemoryRecordMutationOutcome outcome = AddressListMutations.SetParent(new MemoryRecordId(1), + rootRequest ? null : new MemoryRecordId(2)); + + Assert.True(outcome.IsCompleted); + Assert.Equal(MemoryRecordMutationProblem.None, outcome.Problem); + } + + Assert.IsType(FakeHost.TakeReentrantHookFailure()); + Assert.Equal(1, resetAttempts); + Assert.Equal(identity, LuaRuntime.CurrentStateIdentity); + fixture.Execute(rootRequest ? "assert(assigned == 0)" : "assert(assigned == 1)"); + fixture.RunOnChild(rootRequest ? "assert(o.props.Parent == nil)" : "assert(o.props.Parent.ID == parent.ID)"); Assert.Equal(top, fixture.State.Top); } @@ -188,24 +342,21 @@ public MutationFixture(bool parentSetterFails = false, bool destroyFails = false CEObject listObject = FakeHost.CreateObject(State, "Probe"); string childInitializer = parentSetterFails ? "o.props.ID = 1; o.setters.Parent = function(o, v) o.props.Parent = v; assigned = assigned + 1; error('set after side effect') end" - : "o.props.ID = 1; o.setters.Parent = function(o, v) o.props.Parent = v; assigned = assigned + 1 end"; - CEObject childObject = FakeHost.CreateObject(State, destroyFails ? "Stubborn" : "Probe", childInitializer); - CEObject parentObject = FakeHost.CreateObject(State, "Probe", "o.props.ID = 2"); + : "o.props.ID = 1; o.setters.Parent = function(o, v) assigned = assigned + 1; if v ~= nil then o.props.Parent = v end end"; + ChildObject = FakeHost.CreateObject(State, destroyFails ? "Stubborn" : "Probe", childInitializer); + ParentObject = FakeHost.CreateObject(State, "Probe", "o.props.ID = 2"); CEObject grandparentObject = FakeHost.CreateObject(State, "Probe", "o.props.ID = 3"); SetGlobal(State, "list"u8, new EngineAddressList(listObject)); - SetGlobal(State, "child"u8, new MemoryRecord(childObject)); - SetGlobal(State, "parent"u8, new MemoryRecord(parentObject)); + SetGlobal(State, "child"u8, new MemoryRecord(ChildObject)); + SetGlobal(State, "parent"u8, new MemoryRecord(ParentObject)); SetGlobal(State, "grandparent"u8, new MemoryRecord(grandparentObject)); EngineTest.Run(State, """ destroyed = 0 assigned = 0 - function getAddressList() return list end - list.getMemoryRecordByID = function(id) - if id == 1 then return child end - if id == 2 then return parent end - if id == 3 then return grandparent end - return nil - end + address_list_lookup = function() return list end + function getAddressList() return address_list_lookup() end + records = { [1] = child, [2] = parent, [3] = grandparent } + list.getMemoryRecordByID = function(id) return records[id] end child.destroy = function() destroyed = destroyed + 1 end """u8); } @@ -215,6 +366,16 @@ public LuaState State get; } + private CEObject ChildObject + { + get; + } + + private CEObject ParentObject + { + get; + } + public void Dispose() { _scope.Dispose(); @@ -226,6 +387,16 @@ public void Execute(string source) EngineTest.Run(State, Encoding.UTF8.GetBytes(source)); } + public void RunOnChild(string source) + { + FakeHost.RunOnObject(State, ChildObject, source); + } + + public void RunOnParent(string source) + { + FakeHost.RunOnObject(State, ParentObject, source); + } + private static void SetGlobal(LuaState state, ReadOnlySpan name, T value) where T : struct, ILuaMarshaller { From c4480e6c7fa24b58a6b495a378f11cdca042673c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=96=A4=20ShadowNine=20=F0=9F=92=9B?= Date: Mon, 19 Oct 2026 14:02:45 +0200 Subject: [PATCH 2/2] Cover parent read failures and simplify admission checks --- .../AddressList/AddressListMutations.cs | 80 +++---------------- .../AddressList/AddressListMutationsTests.cs | 56 ++++++++++++- 2 files changed, 62 insertions(+), 74 deletions(-) diff --git a/libs/CheatEngine.SDK.Engine/AddressList/AddressListMutations.cs b/libs/CheatEngine.SDK.Engine/AddressList/AddressListMutations.cs index 6359665b..6522f18c 100644 --- a/libs/CheatEngine.SDK.Engine/AddressList/AddressListMutations.cs +++ b/libs/CheatEngine.SDK.Engine/AddressList/AddressListMutations.cs @@ -155,32 +155,17 @@ public static MemoryRecordMutationOutcome SetParent(MemoryRecordId recordId, Mem return preflight; } - if (LuaRuntime.CurrentStateIdentity != identity) - { - return NotAttempted(MemoryRecordMutationProblem.RuntimeIdentityChanged); - } - - preflight = TryResolveRecord(state, list, recordId, false, out MemoryRecord child); + preflight = TryResolveRecord(state, list, recordId, isParent: false, out MemoryRecord child); if (preflight.Problem != MemoryRecordMutationProblem.None) { return preflight; } - if (LuaRuntime.CurrentStateIdentity != identity) - { - return NotAttempted(MemoryRecordMutationProblem.RuntimeIdentityChanged); - } - if (!parentId.HasValue) { - preflight = TryReadParentId(state, child, out bool hasParent, out _, out bool rootReadChangedIdentity); + preflight = TryReadParentId(state, child, out bool hasParent, out _); if (preflight.Problem != MemoryRecordMutationProblem.None) { - if (rootReadChangedIdentity) - { - return NotAttempted(MemoryRecordMutationProblem.RuntimeIdentityChanged); - } - return preflight; } @@ -194,18 +179,13 @@ public static MemoryRecordMutationOutcome SetParent(MemoryRecordId recordId, Mem : Completed(); } - preflight = TryResolveRecord(state, list, parentId.Value, true, out MemoryRecord parent); + preflight = TryResolveRecord(state, list, parentId.Value, isParent: true, out MemoryRecord parent); if (preflight.Problem != MemoryRecordMutationProblem.None) { return preflight; } - if (LuaRuntime.CurrentStateIdentity != identity) - { - return NotAttempted(MemoryRecordMutationProblem.RuntimeIdentityChanged); - } - - preflight = ValidateParentChain(state, recordId, parent, traversalLimit, identity); + preflight = ValidateParentChain(state, recordId, parent, traversalLimit); if (preflight.Problem != MemoryRecordMutationProblem.None) { return preflight; @@ -224,36 +204,20 @@ public static MemoryRecordMutationOutcome SetParent(MemoryRecordId recordId, Mem return Indeterminate(status); } - // Admission prevents state resets; retain this defensive identity check and re-resolve records after callbacks. - if (LuaRuntime.CurrentStateIdentity != identity) - { - return Indeterminate(MemoryRecordMutationProblem.InvalidResult, LuaStatus.Ok); - } - + // Admission prevents supported state transitions. Re-resolve records after callbacks and verify identity before success. preflight = TryGetCurrentList(state, out list); if (preflight.Problem != MemoryRecordMutationProblem.None) { return IndeterminateAfterParentSetter(preflight); } - if (LuaRuntime.CurrentStateIdentity != identity) - { - return Indeterminate(MemoryRecordMutationProblem.InvalidResult, LuaStatus.Ok); - } - - preflight = TryResolveRecord(state, list, recordId, false, out MemoryRecord rereadChild); + preflight = TryResolveRecord(state, list, recordId, isParent: false, out MemoryRecord rereadChild); if (preflight.Problem != MemoryRecordMutationProblem.None) { return IndeterminateAfterParentSetter(preflight); } - if (LuaRuntime.CurrentStateIdentity != identity) - { - return Indeterminate(MemoryRecordMutationProblem.InvalidResult, LuaStatus.Ok); - } - - preflight = TryReadParentId(state, rereadChild, out bool hasActualParent, out MemoryRecordId actualParent, - out _); + preflight = TryReadParentId(state, rereadChild, out bool hasActualParent, out MemoryRecordId actualParent); if (preflight.Problem != MemoryRecordMutationProblem.None) { return IndeterminateAfterParentSetter(preflight); @@ -504,7 +468,7 @@ private static MemoryRecordMutationOutcome TryGetCurrentList(LuaState state, out } private static MemoryRecordMutationOutcome ValidateParentChain(LuaState state, MemoryRecordId childId, - MemoryRecord proposedParent, MemoryRecordParentTraversalLimit traversalLimit, LuaStateIdentity identity) + MemoryRecord proposedParent, MemoryRecordParentTraversalLimit traversalLimit) { HashSet seen = new(); MemoryRecord current = proposedParent; @@ -516,11 +480,6 @@ private static MemoryRecordMutationOutcome ValidateParentChain(LuaState state, M return FromPreflightStatus(status); } - if (LuaRuntime.CurrentStateIdentity != identity) - { - return NotAttempted(MemoryRecordMutationProblem.RuntimeIdentityChanged); - } - if (!MemoryRecordId.TryRead(state, -1, out MemoryRecordId currentId)) { return NotAttempted(MemoryRecordMutationProblem.InvalidResult); @@ -537,11 +496,6 @@ private static MemoryRecordMutationOutcome ValidateParentChain(LuaState state, M return FromPreflightStatus(status); } - if (LuaRuntime.CurrentStateIdentity != identity) - { - return NotAttempted(MemoryRecordMutationProblem.RuntimeIdentityChanged); - } - if (state.IsNil(-1)) { return Completed(); @@ -560,13 +514,11 @@ private static MemoryRecordMutationOutcome ValidateParentChain(LuaState state, M } private static MemoryRecordMutationOutcome TryReadParentId(LuaState state, MemoryRecord child, out bool hasParent, - out MemoryRecordId parentId, out bool identityChanged) + out MemoryRecordId parentId) { int top = state.Top; - LuaStateIdentity identity = LuaRuntime.CurrentStateIdentity; hasParent = false; parentId = default; - identityChanged = false; LuaStatus status = child.Handle.TryGetProperty(state, "Parent"u8); if (!status.IsOk) { @@ -574,13 +526,6 @@ private static MemoryRecordMutationOutcome TryReadParentId(LuaState state, Memor return NotAttempted(MemoryRecordMutationProblem.LuaFailure, status); } - if (LuaRuntime.CurrentStateIdentity != identity) - { - state.SetTop(top); - identityChanged = true; - return NotAttempted(MemoryRecordMutationProblem.InvalidResult); - } - if (state.IsNil(-1)) { state.SetTop(top); @@ -600,13 +545,6 @@ private static MemoryRecordMutationOutcome TryReadParentId(LuaState state, Memor return NotAttempted(MemoryRecordMutationProblem.LuaFailure, status); } - if (LuaRuntime.CurrentStateIdentity != identity) - { - state.SetTop(top); - identityChanged = true; - return NotAttempted(MemoryRecordMutationProblem.InvalidResult); - } - if (!MemoryRecordId.TryRead(state, -1, out parentId)) { state.SetTop(top); diff --git a/tests/CheatEngine.SDK.Engine.Tests/AddressList/AddressListMutationsTests.cs b/tests/CheatEngine.SDK.Engine.Tests/AddressList/AddressListMutationsTests.cs index 2462a5b1..9355b16b 100644 --- a/tests/CheatEngine.SDK.Engine.Tests/AddressList/AddressListMutationsTests.cs +++ b/tests/CheatEngine.SDK.Engine.Tests/AddressList/AddressListMutationsTests.cs @@ -145,14 +145,14 @@ public void Set_parent_root_request_is_a_verified_no_op_or_an_unavailable_host_o using MutationFixture fixture = new(); int top = fixture.State.Top; - MemoryRecordMutationOutcome alreadyRoot = AddressListMutations.SetParent(new MemoryRecordId(1), null); + MemoryRecordMutationOutcome alreadyRoot = AddressListMutations.SetParent(new MemoryRecordId(1), parentId: null); Assert.True(alreadyRoot.IsCompleted); fixture.RunOnChild("assert(o.props.Parent == nil)"); fixture.Execute("assert(assigned == 0)"); fixture.RunOnChild("o.props.Parent = parent"); - MemoryRecordMutationOutcome nested = AddressListMutations.SetParent(new MemoryRecordId(1), null); + MemoryRecordMutationOutcome nested = AddressListMutations.SetParent(new MemoryRecordId(1), parentId: null); Assert.Equal(MemoryRecordMutationEffect.NotAttempted, nested.Effect); Assert.Equal(MemoryRecordMutationProblem.GlobalUnavailable, nested.Problem); @@ -168,7 +168,7 @@ public void Set_parent_root_request_reports_missing_child_without_calling_a_nil_ using MutationFixture fixture = new(); int top = fixture.State.Top; - MemoryRecordMutationOutcome missing = AddressListMutations.SetParent(new MemoryRecordId(99), null); + MemoryRecordMutationOutcome missing = AddressListMutations.SetParent(new MemoryRecordId(99), parentId: null); Assert.Equal(MemoryRecordMutationEffect.NotAttempted, missing.Effect); Assert.Equal(MemoryRecordMutationProblem.RecordNotFound, missing.Problem); @@ -176,6 +176,56 @@ public void Set_parent_root_request_reports_missing_child_without_calling_a_nil_ Assert.Equal(top, fixture.State.Top); } + [Theory] + [InlineData("error('parent read failed')", MemoryRecordMutationProblem.LuaFailure)] + [InlineData("return 42", MemoryRecordMutationProblem.InvalidResult)] + public void Set_parent_root_request_preserves_read_failure_without_starting_assignment(string parentGetter, + MemoryRecordMutationProblem expectedProblem) + { + using MutationFixture fixture = new(); + int top = fixture.State.Top; + fixture.RunOnChild("o.getters.Parent = function() " + parentGetter + " end"); + + MemoryRecordMutationOutcome outcome = AddressListMutations.SetParent(new MemoryRecordId(1), parentId: null); + + Assert.Equal(MemoryRecordMutationEffect.NotAttempted, outcome.Effect); + Assert.Equal(expectedProblem, outcome.Problem); + Assert.Equal(expectedProblem != MemoryRecordMutationProblem.LuaFailure, outcome.LuaStatus.IsOk); + fixture.RunOnChild("assert(o.props.Parent == nil)"); + fixture.Execute("assert(assigned == 0)"); + Assert.Equal(top, fixture.State.Top); + } + + [Theory] + [InlineData(true, "error('parent ID read failed')", MemoryRecordMutationProblem.LuaFailure)] + [InlineData(true, "return 'not an ID'", MemoryRecordMutationProblem.InvalidResult)] + [InlineData(false, "error('parent ID read failed')", MemoryRecordMutationProblem.LuaFailure)] + [InlineData(false, "return 'not an ID'", MemoryRecordMutationProblem.InvalidResult)] + public void Set_parent_parent_id_read_failure_preserves_whether_assignment_started(bool rootRequest, + string idGetter, MemoryRecordMutationProblem expectedProblem) + { + using MutationFixture fixture = new(); + int top = fixture.State.Top; + string when = rootRequest ? "true" : "assigned > 0"; + fixture.RunOnParent("o.getters.ID = function() if " + when + " then " + idGetter + " end; return 2 end"); + if (rootRequest) + { + fixture.RunOnChild("o.props.Parent = parent"); + } + + MemoryRecordMutationOutcome outcome = AddressListMutations.SetParent(new MemoryRecordId(1), + rootRequest ? null : new MemoryRecordId(2)); + + Assert.Equal(rootRequest ? MemoryRecordMutationEffect.NotAttempted : MemoryRecordMutationEffect.Indeterminate, + outcome.Effect); + Assert.Equal(expectedProblem, outcome.Problem); + Assert.Equal(expectedProblem != MemoryRecordMutationProblem.LuaFailure, outcome.LuaStatus.IsOk); + fixture.RunOnParent("o.getters.ID = nil"); + fixture.RunOnChild("assert(o.props.Parent.ID == 2)"); + fixture.Execute(rootRequest ? "assert(assigned == 0)" : "assert(assigned == 1)"); + Assert.Equal(top, fixture.State.Top); + } + [Theory] [InlineData("assigned = assigned + 1")] [InlineData("assigned = assigned + 1; o.props.Parent = grandparent")]