From cff866dcc260d4daee096e4066abda69b65d3dcd Mon Sep 17 00:00:00 2001 From: cake Date: Tue, 20 Jan 2026 00:24:42 +0100 Subject: [PATCH 1/2] Added ptrguard to be used whenever ptrs are being used. --- .../PlayerData/Factories/PlayerDataFactory.cs | 72 ++-------- .../PlayerData/Handlers/GameObjectHandler.cs | 126 ++++++++++++------ LightlessSync/Utils/PtrGuard.cs | 55 ++++++++ LightlessSync/Utils/PtrGuardMemory.cs | 36 +++++ 4 files changed, 183 insertions(+), 106 deletions(-) create mode 100644 LightlessSync/Utils/PtrGuard.cs create mode 100644 LightlessSync/Utils/PtrGuardMemory.cs diff --git a/LightlessSync/PlayerData/Factories/PlayerDataFactory.cs b/LightlessSync/PlayerData/Factories/PlayerDataFactory.cs index 5d304bd..d960e2e 100644 --- a/LightlessSync/PlayerData/Factories/PlayerDataFactory.cs +++ b/LightlessSync/PlayerData/Factories/PlayerDataFactory.cs @@ -127,79 +127,23 @@ public class PlayerDataFactory { nint basePtr = playerPointer; - if (!LooksLikeUserPtr(basePtr)) + if (!PtrGuard.LooksLikePtr(basePtr)) return true; nint drawObjAddr = basePtr + _drawObjectOffset; - if (!TryReadIntPtr(drawObjAddr, out var drawObj)) + if (!PtrGuard.IsReadable(drawObjAddr, (nuint)IntPtr.Size)) + return true; + + if (!PtrGuard.TryReadIntPtr(drawObjAddr, out var drawObj)) + return true; + + if (drawObj != 0 && !PtrGuard.LooksLikePtr(drawObj)) return true; return drawObj == 0; }).ConfigureAwait(false); - private static bool LooksLikeUserPtr(nint p) - { - if (p == 0) return false; - - ulong u = (ulong)p; - - if (u < 0x0000_0001_0000UL) return false; - if (u > 0x0000_7FFF_FFFF_FFFFUL) return false; - if ((u & 0x7UL) != 0) return false; - - return true; - } - - private static bool TryReadIntPtr(nint addr, out nint value) - { - value = 0; - - if (!VirtualReadable(addr)) - return false; - - try - { - value = Marshal.ReadIntPtr(addr); - return true; - } - catch - { - return false; - } - } - - private static bool VirtualReadable(nint addr) - { - if (VirtualQuery(addr, out var mbi, (nuint)Marshal.SizeOf()) == 0) - return false; - - const uint MEM_COMMIT = 0x1000; - const uint PAGE_NOACCESS = 0x01; - const uint PAGE_GUARD = 0x100; - - if (mbi.State != MEM_COMMIT) return false; - if ((mbi.Protect & PAGE_GUARD) != 0) return false; - if (mbi.Protect == PAGE_NOACCESS) return false; - - return true; - } - - [DllImport("kernel32.dll", SetLastError = true)] - private static extern nuint VirtualQuery(nint lpAddress, out MEMORY_BASIC_INFORMATION lpBuffer, nuint dwLength); - - [StructLayout(LayoutKind.Sequential)] - private struct MEMORY_BASIC_INFORMATION - { - public nint BaseAddress; - public nint AllocationBase; - public uint AllocationProtect; - public nuint RegionSize; - public uint State; - public uint Protect; - public uint Type; - } - private static bool IsCacheFresh(CacheEntry entry) => (DateTime.UtcNow - entry.CreatedUtc) <= _characterCacheTtl; diff --git a/LightlessSync/PlayerData/Handlers/GameObjectHandler.cs b/LightlessSync/PlayerData/Handlers/GameObjectHandler.cs index 28b67b6..6ecd223 100644 --- a/LightlessSync/PlayerData/Handlers/GameObjectHandler.cs +++ b/LightlessSync/PlayerData/Handlers/GameObjectHandler.cs @@ -2,11 +2,12 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using LightlessSync.Services; using LightlessSync.Services.Mediator; +using LightlessSync.Utils; using Microsoft.Extensions.Logging; using System.Runtime.CompilerServices; using static FFXIVClientStructs.FFXIV.Client.Game.Character.DrawDataContainer; -using VisibilityFlags = FFXIVClientStructs.FFXIV.Client.Game.Object.VisibilityFlags; using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind; +using VisibilityFlags = FFXIVClientStructs.FFXIV.Client.Game.Object.VisibilityFlags; namespace LightlessSync.PlayerData.Handlers; @@ -177,18 +178,47 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP var prevDrawObj = DrawObjectAddress; string? nameString = null; - Address = _getAddress(); + // Resolve address and validate BEFORE first deref + var nextAddr = _getAddress(); + + // Optional: catch the root cause quickly + // if nextAddr is 32-bit-ish, you're being fed an id/sentinel + if (nextAddr != IntPtr.Zero && !PtrGuard.LooksLikePtr(nextAddr)) + { + Logger.LogWarning("[{this}] _getAddress returned non-pointer: 0x{addr:X}", this, (ulong)nextAddr); + nextAddr = IntPtr.Zero; + } + + // Must be readable at least for a GameObject header before touching it + if (nextAddr != IntPtr.Zero && + !PtrGuard.IsReadable(nextAddr, (nuint)sizeof(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject))) + { + Logger.LogWarning("[{this}] Address not readable: 0x{addr:X}", this, (ulong)nextAddr); + nextAddr = IntPtr.Zero; + } + + Address = nextAddr; if (Address != IntPtr.Zero) { var gameObject = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Address; - DrawObjectAddress = (IntPtr)gameObject->DrawObject; + + var draw = (nint)gameObject->DrawObject; + + if (!PtrGuard.LooksLikePtr(draw) || !PtrGuard.IsReadable(draw, (nuint)sizeof(DrawObject))) + draw = 0; + + DrawObjectAddress = draw; EntityId = gameObject->EntityId; - var chara = (Character*)Address; - nameString = chara->GameObject.NameString; - if (!string.IsNullOrEmpty(nameString) && !string.Equals(nameString, Name, StringComparison.Ordinal)) - Name = nameString; + if (PtrGuard.IsReadable(Address, (nuint)sizeof(Character))) + { + var chara = (Character*)Address; + nameString = chara->GameObject.NameString; + + if (!string.IsNullOrEmpty(nameString) && !string.Equals(nameString, Name, StringComparison.Ordinal)) + Name = nameString; + } } else { @@ -196,22 +226,27 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP EntityId = uint.MaxValue; } - CurrentDrawCondition = IsBeingDrawnUnsafe(); + CurrentDrawCondition = (Address != IntPtr.Zero && DrawObjectAddress != IntPtr.Zero) + ? IsBeingDrawnUnsafe() + : DrawCondition.DrawObjectZero; if (_haltProcessing || !allowPublish) return; bool drawObjDiff = DrawObjectAddress != prevDrawObj; bool addrDiff = Address != prevAddr; - if (Address != IntPtr.Zero && DrawObjectAddress != IntPtr.Zero) + if (Address != IntPtr.Zero && DrawObjectAddress != IntPtr.Zero + && PtrGuard.IsReadable(Address, (nuint)sizeof(Character)) + && PtrGuard.IsReadable(DrawObjectAddress, (nuint)sizeof(DrawObject))) { var chara = (Character*)Address; var drawObj = (DrawObject*)DrawObjectAddress; + var objType = drawObj->Object.GetObjectType(); var isHuman = objType == ObjectType.CharacterBase && ((CharacterBase*)drawObj)->GetModelType() == CharacterBase.ModelType.Human; - nameString ??= ((Character*)Address)->GameObject.NameString; + nameString ??= chara->GameObject.NameString; var nameChange = !string.Equals(nameString, Name, StringComparison.Ordinal); if (nameChange) Name = nameString; @@ -219,32 +254,36 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP if (isHuman) { - var classJob = chara->CharacterData.ClassJob; - if (classJob != _classJob) + if (PtrGuard.IsReadable(DrawObjectAddress, (nuint)sizeof(Human))) { - Logger.LogTrace("[{this}] classjob changed from {old} to {new}", this, _classJob, classJob); - _classJob = classJob; - Mediator.Publish(new ClassJobChangedMessage(this)); + var classJob = chara->CharacterData.ClassJob; + if (classJob != _classJob) + { + Logger.LogTrace("[{this}] classjob changed from {old} to {new}", this, _classJob, classJob); + _classJob = classJob; + Mediator.Publish(new ClassJobChangedMessage(this)); + } + + equipDiff = CompareAndUpdateEquipByteData((byte*)&((Human*)drawObj)->Head); + + ref var mh = ref chara->DrawData.Weapon(WeaponSlot.MainHand); + ref var oh = ref chara->DrawData.Weapon(WeaponSlot.OffHand); + + equipDiff |= CompareAndUpdateMainHand((Weapon*)mh.DrawObject); + equipDiff |= CompareAndUpdateOffHand((Weapon*)oh.DrawObject); + } + else + { + isHuman = false; } - - equipDiff = CompareAndUpdateEquipByteData((byte*)&((Human*)drawObj)->Head); - - ref var mh = ref chara->DrawData.Weapon(WeaponSlot.MainHand); - ref var oh = ref chara->DrawData.Weapon(WeaponSlot.OffHand); - equipDiff |= CompareAndUpdateMainHand((Weapon*)mh.DrawObject); - equipDiff |= CompareAndUpdateOffHand((Weapon*)oh.DrawObject); - - if (equipDiff) - Logger.LogTrace("Checking [{this}] equip data as human from draw obj, result: {diff}", this, equipDiff); } - else + + if (!isHuman) { equipDiff = CompareAndUpdateEquipByteData((byte*)Unsafe.AsPointer(ref chara->DrawData.EquipmentModelIds[0])); - if (equipDiff) - Logger.LogTrace("Checking [{this}] equip data from game obj, result: {diff}", this, equipDiff); } - if (equipDiff && !_isOwnedObject) // send the message out immediately and cancel out, no reason to continue if not self + if (equipDiff && !_isOwnedObject) { Logger.LogTrace("[{this}] Changed", this); return; @@ -252,11 +291,13 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP bool customizeDiff = false; - if (isHuman) + if (isHuman && PtrGuard.IsReadable(DrawObjectAddress, (nuint)sizeof(Human))) { - var gender = ((Human*)drawObj)->Customize.Sex; - var raceId = ((Human*)drawObj)->Customize.Race; - var tribeId = ((Human*)drawObj)->Customize.Tribe; + var human = (Human*)drawObj; + + var gender = human->Customize.Sex; + var raceId = human->Customize.Race; + var tribeId = human->Customize.Tribe; if (_isOwnedObject && ObjectKind == ObjectKind.Player && (gender != Gender || raceId != RaceId || tribeId != TribeId)) @@ -267,15 +308,11 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP TribeId = tribeId; } - customizeDiff = CompareAndUpdateCustomizeData(((Human*)drawObj)->Customize.Data); - if (customizeDiff) - Logger.LogTrace("Checking [{this}] customize data as human from draw obj, result: {diff}", this, customizeDiff); + customizeDiff = CompareAndUpdateCustomizeData(human->Customize.Data); } else { customizeDiff = CompareAndUpdateCustomizeData(chara->DrawData.CustomizeData.Data); - if (customizeDiff) - Logger.LogTrace("Checking [{this}] customize data from game obj, result: {diff}", this, equipDiff); } if ((addrDiff || drawObjDiff || equipDiff || customizeDiff || nameChange) && _isOwnedObject) @@ -289,12 +326,11 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP CurrentDrawCondition = DrawCondition.DrawObjectZero; Logger.LogTrace("[{this}] Changed", this); if (_isOwnedObject && ObjectKind != ObjectKind.Player) - { Mediator.Publish(new ClearCacheForObjectMessage(this)); - } } } + private unsafe bool CompareAndUpdateCustomizeData(Span customizeData) { bool hasChanges = false; @@ -330,7 +366,10 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP private unsafe bool CompareAndUpdateMainHand(Weapon* weapon) { - if ((nint)weapon == nint.Zero) return false; + var p = (nint)weapon; + if (!PtrGuard.LooksLikePtr(p) || !PtrGuard.IsReadable(p, (nuint)sizeof(Weapon))) + return false; + bool hasChanges = false; hasChanges |= weapon->ModelSetId != MainHandData[0]; MainHandData[0] = weapon->ModelSetId; @@ -343,7 +382,10 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP private unsafe bool CompareAndUpdateOffHand(Weapon* weapon) { - if ((nint)weapon == nint.Zero) return false; + var p = (nint)weapon; + if (!PtrGuard.LooksLikePtr(p) || !PtrGuard.IsReadable(p, (nuint)sizeof(Weapon))) + return false; + bool hasChanges = false; hasChanges |= weapon->ModelSetId != OffHandData[0]; OffHandData[0] = weapon->ModelSetId; diff --git a/LightlessSync/Utils/PtrGuard.cs b/LightlessSync/Utils/PtrGuard.cs new file mode 100644 index 0000000..2b08169 --- /dev/null +++ b/LightlessSync/Utils/PtrGuard.cs @@ -0,0 +1,55 @@ +using System.Runtime.InteropServices; +using static LightlessSync.Utils.PtrGuardMemory; + +namespace LightlessSync.Utils +{ + public static partial class PtrGuard + { + private const ulong _minLikelyPtr = 0x0000_0001_0000_0000UL; + private const ulong _maxUserPtr = 0x0000_7FFF_FFFF_FFFFUL; + private const ulong _aligmentPtr = 0x7UL; + + public static bool LooksLikePtr(nint p) + { + if (p == 0) return false; + var u = (ulong)p; + if (u < _minLikelyPtr) return false; + if (u > _maxUserPtr) return false; + if ((u & _aligmentPtr) != 0) return false; + return true; + } + public static bool TryReadIntPtr(nint addr, out nint value) + { + value = 0; + + if (!LooksLikePtr(addr)) + return false; + + return ReadProcessMemory(GetCurrentProcess(), addr, out value, (nuint)IntPtr.Size, out nuint bytesRead) + && bytesRead == (nuint)IntPtr.Size; + } + + public static bool IsReadable(nint addr, nuint size) + { + if (addr == 0 || size == 0) return false; + + if (VirtualQuery(addr, out var mbi, (nuint)Marshal.SizeOf()) == 0) + return false; + + const uint Commit = 0x1000; + const uint NoAccess = 0x01; + const uint PageGuard = 0x100; + + if (mbi.State != Commit) return false; + if ((mbi.Protect & PageGuard) != 0) return false; + if (mbi.Protect == NoAccess) return false; + + ulong start = (ulong)addr; + ulong end = start + size - 1; + ulong r0 = (ulong)mbi.BaseAddress; + ulong r1 = r0 + mbi.RegionSize - 1; + + return start >= r0 && end <= r1; + } + } +} \ No newline at end of file diff --git a/LightlessSync/Utils/PtrGuardMemory.cs b/LightlessSync/Utils/PtrGuardMemory.cs new file mode 100644 index 0000000..ff29c4f --- /dev/null +++ b/LightlessSync/Utils/PtrGuardMemory.cs @@ -0,0 +1,36 @@ +using System.Runtime.InteropServices; + +namespace LightlessSync.Utils +{ + internal static class PtrGuardMemory + { + [StructLayout(LayoutKind.Sequential)] + internal struct MEMORY_BASIC_INFORMATION + { + public nint BaseAddress; + public nint AllocationBase; + public uint AllocationProtect; + public nuint RegionSize; + public uint State; + public uint Protect; + public uint Type; + } + + [DllImport("kernel32.dll", SetLastError = true)] + internal static extern nuint VirtualQuery( + nint lpAddress, + out MEMORY_BASIC_INFORMATION lpBuffer, + nuint dwLength); + + [DllImport("kernel32.dll", SetLastError = true)] + internal static extern bool ReadProcessMemory( + nint hProcess, + nint lpBaseAddress, + out nint lpBuffer, + nuint nSize, + out nuint lpNumberOfBytesRead); + + [DllImport("kernel32.dll")] + internal static extern nint GetCurrentProcess(); + } +} \ No newline at end of file From 22fe9901a4ab4d7894e3b5e8f171fda4fe8f27eb Mon Sep 17 00:00:00 2001 From: cake Date: Tue, 20 Jan 2026 00:25:26 +0100 Subject: [PATCH 2/2] Fixed some issues. --- LightlessSync/PlayerData/Handlers/GameObjectHandler.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/LightlessSync/PlayerData/Handlers/GameObjectHandler.cs b/LightlessSync/PlayerData/Handlers/GameObjectHandler.cs index 6ecd223..359ec20 100644 --- a/LightlessSync/PlayerData/Handlers/GameObjectHandler.cs +++ b/LightlessSync/PlayerData/Handlers/GameObjectHandler.cs @@ -178,18 +178,14 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP var prevDrawObj = DrawObjectAddress; string? nameString = null; - // Resolve address and validate BEFORE first deref var nextAddr = _getAddress(); - // Optional: catch the root cause quickly - // if nextAddr is 32-bit-ish, you're being fed an id/sentinel if (nextAddr != IntPtr.Zero && !PtrGuard.LooksLikePtr(nextAddr)) { Logger.LogWarning("[{this}] _getAddress returned non-pointer: 0x{addr:X}", this, (ulong)nextAddr); nextAddr = IntPtr.Zero; } - // Must be readable at least for a GameObject header before touching it if (nextAddr != IntPtr.Zero && !PtrGuard.IsReadable(nextAddr, (nuint)sizeof(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject))) {