From 56a7d7004f481163616927006fb2b680cc7dcc32 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 18:41:04 +0200 Subject: [PATCH] Require a confirmed process handle before accepting target selection --- .../Processes/ProcessOperationStatus.cs | 2 +- .../Processes/ProcessOperationStatusKind.cs | 6 ++-- .../Processes/RuntimeProcessOperations.cs | 27 +++++++++++--- libs/CheatEngine.SDK.Engine/README.md | 3 +- .../RuntimeProcessOperationsTests.cs | 35 +++++++++++++++++-- 5 files changed, 61 insertions(+), 12 deletions(-) diff --git a/libs/CheatEngine.SDK.Engine/Processes/ProcessOperationStatus.cs b/libs/CheatEngine.SDK.Engine/Processes/ProcessOperationStatus.cs index 7a692272..3623bc98 100644 --- a/libs/CheatEngine.SDK.Engine/Processes/ProcessOperationStatus.cs +++ b/libs/CheatEngine.SDK.Engine/Processes/ProcessOperationStatus.cs @@ -53,7 +53,7 @@ public LuaStatus LuaStatus public static ProcessOperationStatus GlobalUnavailable => new(ProcessOperationStatusKind.GlobalUnavailable, LuaStatus.Ok); - /// Gets a status for a result outside the documented process-observation shape. + /// Gets a status for a result outside the documented process selection or observation shape. public static ProcessOperationStatus InvalidResult => new(ProcessOperationStatusKind.InvalidResult, LuaStatus.Ok); /// diff --git a/libs/CheatEngine.SDK.Engine/Processes/ProcessOperationStatusKind.cs b/libs/CheatEngine.SDK.Engine/Processes/ProcessOperationStatusKind.cs index f52f2e99..a631f25b 100644 --- a/libs/CheatEngine.SDK.Engine/Processes/ProcessOperationStatusKind.cs +++ b/libs/CheatEngine.SDK.Engine/Processes/ProcessOperationStatusKind.cs @@ -17,8 +17,8 @@ public enum ProcessOperationStatusKind TargetNotAttached = 2, /// - /// An explicit selection call returned normally, but the immediately observed process was absent or did not match - /// the requested identifier. + /// An explicit selection returned no valid-handle confirmation (nil or false), or its immediately observed + /// process was absent or did not match the requested identifier. /// SelectionNotConfirmed = 3, @@ -28,7 +28,7 @@ public enum ProcessOperationStatusKind /// A protected Lua lookup or invocation failed. ProtectedLuaFailure = 5, - /// The host returned a value outside the documented process-observation shape. + /// The host returned a value outside the documented process selection or observation shape. InvalidResult = 6, /// diff --git a/libs/CheatEngine.SDK.Engine/Processes/RuntimeProcessOperations.cs b/libs/CheatEngine.SDK.Engine/Processes/RuntimeProcessOperations.cs index f0e5cf2d..0217facf 100644 --- a/libs/CheatEngine.SDK.Engine/Processes/RuntimeProcessOperations.cs +++ b/libs/CheatEngine.SDK.Engine/Processes/RuntimeProcessOperations.cs @@ -55,8 +55,9 @@ public static ProcessOperationStatus ObserveCurrent(out CurrentProcessObservatio /// The positive process identifier to select. /// The copied matching target observation only when the returned status is successful. /// - /// The factual protected selection status. A normal openProcess return is not success by itself: success - /// requires the next getOpenedProcessID observation to equal . + /// The factual protected selection status. Success requires openProcess to return boolean + /// , confirming CE's nonzero process handle, and the next getOpenedProcessID + /// observation to equal . A missing or false confirmation refuses selection. /// /// is default or otherwise non-positive. /// The plugin is not enabled or the calling thread has no Lua state. @@ -304,10 +305,26 @@ private static ProcessOperationStatus TryOpenProcess(LuaState state, TargetProce } state.PushInteger(processId.Value); - LuaStatus luaStatus = state.TryCall(1, 0); - return luaStatus.IsOk + LuaStatus luaStatus = state.TryCall(1, 1); + if (!luaStatus.IsOk) + { + return ProcessOperationStatus.ProtectedLuaFailure(luaStatus); + } + + // CE assigns the requested PID before opening its handle. Only its true return confirms both. + LuaType resultType = state.TypeOf(-1); + if (resultType == LuaType.Nil) + { + return ProcessOperationStatus.SelectionNotConfirmed; + } + if (resultType != LuaType.Boolean) + { + return ProcessOperationStatus.InvalidResult; + } + + return state.ToBoolean(-1) ? ProcessOperationStatus.Success - : ProcessOperationStatus.ProtectedLuaFailure(luaStatus); + : ProcessOperationStatus.SelectionNotConfirmed; } private static ProcessOperationStatus TryCallBoolean(LuaState state, LuaRef cache, ReadOnlySpan globalName, diff --git a/libs/CheatEngine.SDK.Engine/README.md b/libs/CheatEngine.SDK.Engine/README.md index 973c1a5a..ab0f2c77 100644 --- a/libs/CheatEngine.SDK.Engine/README.md +++ b/libs/CheatEngine.SDK.Engine/README.md @@ -372,7 +372,8 @@ observations `RuntimeProcessOperations.ObserveTargetArchitecture` and `TryGetCon `RuntimeObservations.TryObserveRuntimeInfo` and `InstructionProfiles.TryObserveCurrent` also read it last and report a difference as `TargetChanged`; instruction operations re-check it before and after their Cheat Engine call. `TargetSelection.ObserveCurrent` and `RuntimeProcessOperations.ObserveCurrent` read it once, before their other probes, -and `RuntimeProcessOperations.SelectAndObserve` reads it once, after `openProcess`, to confirm the selection. +and `RuntimeProcessOperations.SelectAndObserve` first requires `openProcess` to return boolean `true`, confirming +CE's nonzero process handle, then reads the PID once to confirm the selection. A reported PID alone is insufficient. | Fact | CE global | SDK member | When unknown | |-----------------------------------|------------------------------|------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------| diff --git a/tests/CheatEngine.SDK.Engine.Tests/Processes/RuntimeProcessOperationsTests.cs b/tests/CheatEngine.SDK.Engine.Tests/Processes/RuntimeProcessOperationsTests.cs index 0233766b..70780d4a 100644 --- a/tests/CheatEngine.SDK.Engine.Tests/Processes/RuntimeProcessOperationsTests.cs +++ b/tests/CheatEngine.SDK.Engine.Tests/Processes/RuntimeProcessOperationsTests.cs @@ -250,7 +250,7 @@ public void SelectAndObserve_explicit_pid_requires_the_next_ce_observation_to_ma LuaState lua = scope.State; EngineTest.Run(lua, """ opened = 0 - function openProcess(id) opened = id end + function openProcess(id) opened = id return true end function getOpenedProcessID() return opened end function targetIs64Bit() return true end """u8); @@ -274,7 +274,7 @@ public void SelectAndObserve_unconfirmed_or_failing_selection_keeps_outcomes_dis { LuaState lua = mismatchScope.State; EngineTest.Run(lua, """ - function openProcess(_) end + function openProcess(_) return true end function getOpenedProcessID() return 77 end function targetIs64Bit() error('must not be called after a mismatched PID') end """u8); @@ -298,6 +298,37 @@ function targetIs64Bit() error('must not be called after a mismatched PID') end Assert.Equal(0, failureScope.State.Top); } + [Theory] + [InlineData("", ProcessOperationStatusKind.SelectionNotConfirmed)] + [InlineData("return false", ProcessOperationStatusKind.SelectionNotConfirmed)] + [InlineData("return 'true'", ProcessOperationStatusKind.InvalidResult)] + [InlineData("return 1", ProcessOperationStatusKind.InvalidResult)] + [InlineData("return {}", ProcessOperationStatusKind.InvalidResult)] + public void SelectAndObserve_rejects_an_unconfirmed_or_malformed_open_process_result( + string returned, ProcessOperationStatusKind expected) + { + EngineTest.RequireNativeLua(); + using NativeLuaState state = new(); + using HostScope scope = new(state); + LuaState lua = scope.State; + EngineTest.Run(lua, Encoding.UTF8.GetBytes($$""" + pidReads = 0 + bitnessReads = 0 + function openProcess(id) opened = id {{returned}} end + function getOpenedProcessID() pidReads = pidReads + 1 return opened end + function targetIs64Bit() bitnessReads = bitnessReads + 1 return true end + """)); + + ProcessOperationStatus status = RuntimeProcessOperations.SelectAndObserve(new TargetProcessId(42), + out CurrentProcessObservation observation); + + Assert.Equal(expected, status.Kind); + Assert.Equal(default, observation); + Assert.Equal(0, lua.Top); + EngineTest.Run(lua, "assert(pidReads == 0 and bitnessReads == 0, 'unconfirmed selection was observed')"u8); + Assert.Equal(0, lua.Top); + } + [Fact] public void SelectAndObserve_rejects_a_default_process_identifier_before_entering_the_lua_runtime() {