Skip to content

Commit 5410c8e

Browse files
committed
Fix for hooks getting the wrong 'this' pointer when multiple inheritance is involved
Before we could have cases where a hook thought it had a pointer to a derived class, when it fact it pointed to a base class, so the pointer was effectively garbage. Crimes have been committed to make this work in a way that's backwards compatible, apologies for the pain and suffering this has caused.
1 parent e3c6f9e commit 5410c8e

1 file changed

Lines changed: 151 additions & 12 deletions

File tree

Mods/SML/Source/SML/Public/Patching/NativeHookManager.h

Lines changed: 151 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#include "CoreMinimal.h"
33
#include "Reflection/FunctionThunkGenerator.h"
44
#include "Traits/MemberFunctionPtrOuter.h"
5+
#include <concepts>
56
#include <type_traits>
67

78
SML_API DECLARE_LOG_CATEGORY_EXTERN(LogNativeHookManager, Log, Log);
@@ -163,6 +164,9 @@ struct TCallScope;
163164
template<typename ReturnType, typename... ArgTypes>
164165
class TCallScopeBase
165166
{
167+
template<typename Backend, typename Variant, bool bIsMemberFunction, typename /*ReturnType*/, typename... /*ArgTypes*/>
168+
friend class THookInvokerBase;
169+
166170
using DerivedCallScope = TCallScope<ReturnType(*)(ArgTypes...)>;
167171
static constexpr bool bHasReturnValue = !std::is_void_v<ReturnType>;
168172

@@ -182,7 +186,9 @@ class TCallScopeBase
182186

183187
ReturnType operator()(ArgTypes... Args)
184188
{
185-
if (FunctionList == nullptr || HandlerIndex >= FunctionList->Num())
189+
const ArgsPreprocessor<ArgTypes...> ArgsPreprocessor(*this, Args...);
190+
191+
if (FunctionList == nullptr || static_cast<size_t>(HandlerIndex) >= FunctionList->Num())
186192
{
187193
// Reached the end of the handler list, call the original function.
188194
if constexpr (bHasReturnValue)
@@ -197,8 +203,8 @@ class TCallScopeBase
197203
}
198204
else
199205
{
200-
const size_t CurrentHandlerIndex = HandlerIndex++;
201-
const size_t NextHandlerIndex = HandlerIndex;
206+
const uint32 CurrentHandlerIndex = HandlerIndex++;
207+
const uint32 NextHandlerIndex = HandlerIndex;
202208

203209
// Call the current handler.
204210
const TSharedPtr<HookCallable>& Handler = (*FunctionList)[CurrentHandlerIndex];
@@ -222,11 +228,61 @@ class TCallScopeBase
222228
{
223229
}
224230

231+
// We need a new variable to store ThisAdjustment, but we can't change the layout of this class
232+
// because we need to maintain compatibility with mods that were compiled before this change. We
233+
// also can't make use of existing padding because that won't necessarily be zero-initialized if an
234+
// old mod creates the scope. Fortunately there's a really hacky way around this: HandlerIndex was
235+
// size_t (64-bits) before, but we're obviously not going to have anywhere near that number of
236+
// handlers, so the upper bits of that variable are always going to be zero. It's now restricted to
237+
// a 32-bit value, which leaves the upper 32-bits for ThisAdjustment, The hacky thing about this is
238+
// that we need to make sure that those upper bits are set back to zero whenever we call into old
239+
// code, as it will be doing full 64-bit operations at that address.
240+
static_assert(PLATFORM_LITTLE_ENDIAN && sizeof(size_t) == 8,
241+
"TCallScope changes aren't backwards-compatible on this platform!");
242+
225243
const TArray<TSharedPtr<HookCallable>>* FunctionList;
226-
size_t HandlerIndex = 0;
244+
uint32 HandlerIndex = 0; // Used to be size_t
245+
uint32 ThisAdjustment = 0;
227246
OrigCallable OrigFunc;
228247
bool bForwardCall = true;
229248
std::conditional_t<bHasReturnValue, std::remove_cv_t<ReturnType>, std::monostate> ResultData{};
249+
250+
private:
251+
template<typename... /* ArgTypes */>
252+
struct ArgsPreprocessor
253+
{
254+
FORCEINLINE ArgsPreprocessor(const TCallScopeBase& Scope, const ArgTypes&...)
255+
{
256+
check(Scope.ThisAdjustment == 0);
257+
}
258+
};
259+
260+
// Pre-processing for the 'this' pointer in member functions.
261+
// Technically this will also run for non-member functions that have a pointer as their first
262+
// argument, but in those cases ThisAdjustment will be zero so this won't end up doing anything.
263+
template<typename Pointee, typename... OtherArgTypes>
264+
struct ArgsPreprocessor<Pointee*, OtherArgTypes...>
265+
{
266+
TCallScopeBase& Scope;
267+
const uint32_t SavedThisAdjustment;
268+
269+
FORCEINLINE ArgsPreprocessor(TCallScopeBase& Scope, Pointee*& Ptr, const OtherArgTypes&...)
270+
: Scope(Scope)
271+
, SavedThisAdjustment(Scope.ThisAdjustment)
272+
{
273+
// Handlers un-apply the ThisAdjustment so that they can have sane pointers for their callbacks, but
274+
// we need to re-apply it now as we're about to call into unknown code.
275+
Ptr = reinterpret_cast<Pointee*>(reinterpret_cast<uintptr_t>(Ptr) + SavedThisAdjustment);
276+
277+
// Reset this to zero so it doesn't confuse old mods trying to use HandlerIndex.
278+
Scope.ThisAdjustment = 0;
279+
}
280+
281+
FORCEINLINE ~ArgsPreprocessor()
282+
{
283+
Scope.ThisAdjustment = SavedThisAdjustment;
284+
}
285+
};
230286
};
231287

232288
// non-void return
@@ -353,6 +409,10 @@ struct FNativeHookResult
353409
/// actual address and not some sort of (member/virtual) function pointer as it will be directly
354410
/// jumped to.
355411
void* OriginalFunctionCode;
412+
413+
/// Offset, in bytes, added to the 'this' pointer before the function was called.
414+
/// Only relevant for non-static member functions, should be set to zero otherwise.
415+
ptrdiff_t ThisAdjustment;
356416
};
357417

358418
template<typename TCallable, TCallable OriginalFunction>
@@ -378,13 +438,15 @@ struct TStandardHookBackend
378438
static FNativeHookResult RegisterHook(const TCHAR* DebugSymbolName, decltype(GetNullSampleObject()) SampleObjectInstance = nullptr)
379439
{
380440
FNativeHookResult Result;
441+
const FMemberFunctionPointer OriginalFunctionData = ConvertFunctionPointer(OriginalFunction);
381442

382443
Result.Key = FNativeHookManagerInternal::RegisterHookFunction(
383444
DebugSymbolName,
384-
ConvertFunctionPointer(OriginalFunction),
445+
OriginalFunctionData,
385446
SampleObjectInstance,
386447
(void*)HookFunction,
387448
&Result.OriginalFunctionCode);
449+
Result.ThisAdjustment = static_cast<ptrdiff_t>(OriginalFunctionData.ThisAdjustment);
388450

389451
return Result;
390452
}
@@ -404,13 +466,15 @@ struct TVtableHookBackend
404466
static FNativeHookResult RegisterHook(const TCHAR* DebugSymbolName, const TMemberFunctionPtrOuter_T<TCallable>* SampleObjectInstance)
405467
{
406468
FNativeHookResult Result;
469+
const FMemberFunctionPointer OriginalFunctionData = ConvertFunctionPointer(OriginalFunction);
407470

408471
Result.Key = FNativeHookManagerInternal::RegisterVtableHook(
409472
DebugSymbolName,
410-
ConvertFunctionPointer(OriginalFunction),
473+
OriginalFunctionData,
411474
SampleObjectInstance,
412475
(void*)HookFunction,
413476
&Result.OriginalFunctionCode);
477+
Result.ThisAdjustment = static_cast<ptrdiff_t>(OriginalFunctionData.ThisAdjustment);
414478

415479
return Result;
416480
}
@@ -445,6 +509,7 @@ struct TUFunctionHookBackend
445509
FunctionName,
446510
HookFunction,
447511
(FNativeFuncPtr*)&Result.OriginalFunctionCode);
512+
Result.ThisAdjustment = 0;
448513

449514
return Result;
450515
}
@@ -474,19 +539,25 @@ class THookInvokerBase
474539
using HandlerBefore = TFunction<HandlerBeforeSignature>;
475540
using HandlerAfter = TFunction<HandlerAfterSignature>;
476541

477-
template<typename... BackendArgTypes>
478-
static auto AddHandlerBefore(HandlerBefore&& InHandler, const TCHAR* DebugSymbolName, BackendArgTypes&&... BackendArgs)
542+
template<std::convertible_to<HandlerBefore> InHandlerType, typename... BackendArgTypes>
543+
static auto AddHandlerBefore(InHandlerType&& InHandler, const TCHAR* DebugSymbolName, BackendArgTypes&&... BackendArgs)
479544
{
480545
InstallHook(DebugSymbolName, Forward<BackendArgTypes>(BackendArgs)...);
481-
const FDelegateHandle DelegateHandle = InternalAddHandler(MoveTemp(InHandler), *HandlersBefore, *HandlerBeforeReferences);
546+
const FDelegateHandle DelegateHandle = InternalAddHandler(
547+
WrapHandler<HandlerBefore>(Forward<InHandlerType>(InHandler)),
548+
*HandlersBefore,
549+
*HandlerBeforeReferences);
482550
return FNativeHookHandle::TDelayedInit<THookInvokerBase>(DelegateHandle, DebugSymbolName);
483551
}
484552

485-
template<typename... BackendArgTypes>
486-
static auto AddHandlerAfter(HandlerAfter&& InHandler, const TCHAR* DebugSymbolName, BackendArgTypes&&... BackendArgs)
553+
template<std::convertible_to<HandlerAfter> InHandlerType, typename... BackendArgTypes>
554+
static auto AddHandlerAfter(InHandlerType&& InHandler, const TCHAR* DebugSymbolName, BackendArgTypes&&... BackendArgs)
487555
{
488556
InstallHook(DebugSymbolName, Forward<BackendArgTypes>(BackendArgs)...);
489-
const FDelegateHandle DelegateHandle = InternalAddHandler(MoveTemp(InHandler), *HandlersAfter, *HandlerAfterReferences);
557+
const FDelegateHandle DelegateHandle = InternalAddHandler(
558+
WrapHandler<HandlerAfter>(Forward<InHandlerType>(InHandler)),
559+
*HandlersAfter,
560+
*HandlerAfterReferences);
490561
return FNativeHookHandle::TDelayedInit<THookInvokerBase>(DelegateHandle, DebugSymbolName);
491562
}
492563

@@ -522,6 +593,7 @@ class THookInvokerBase
522593
HandlersAfter = &HandlerLists->HandlersAfter;
523594
HandlerBeforeReferences = &HandlerLists->HandlerBeforeReferences;
524595
HandlerAfterReferences = &HandlerLists->HandlerAfterReferences;
596+
ThisAdjustment = Result.ThisAdjustment;
525597
OriginalFunctionCode = Result.OriginalFunctionCode;
526598
Key = Result.Key;
527599
}
@@ -538,6 +610,7 @@ class THookInvokerBase
538610
HandlersAfter = nullptr;
539611
HandlerBeforeReferences = nullptr;
540612
HandlerAfterReferences = nullptr;
613+
ThisAdjustment = 0;
541614
OriginalFunctionCode = nullptr;
542615
Key = nullptr;
543616
}
@@ -651,10 +724,76 @@ class THookInvokerBase
651724
};
652725
#endif
653726

727+
// For some member functions we could end up with a 'this' pointer that has been adjusted to point
728+
// to a base class, which won't be compatible with the derived class pointer that the handler is
729+
// expecting. To work around this, we need to un-adjust the 'this' pointer before calling the
730+
// handler. It's tempting to do this in the hook function itself, we could modify the parameter as
731+
// soon as it's passed in and the new value could be passed on to all of the handlers, but we can't
732+
// do that because we need to be backwards-compatible with old mods; if an old mod is the first to
733+
// register the hook, then they'd define the hook function and they wouldn't know to do that
734+
// adjustment. The only thing that we know we have control over is our own handler, so that's the
735+
// only place where this can be done.
736+
template<typename HandlerType, typename InHandlerType>
737+
static HandlerType WrapHandler(InHandlerType&& InHandler)
738+
{
739+
if constexpr (!bIsMemberFunction)
740+
{
741+
// Non-member functions shouldn't have a ThisAdjustment.
742+
check(ThisAdjustment == 0);
743+
return Forward<InHandlerType>(InHandler);
744+
}
745+
else if (ThisAdjustment == 0)
746+
{
747+
// No adjustment, use the input handler as-is.
748+
return Forward<InHandlerType>(InHandler);
749+
}
750+
else if constexpr (std::is_same_v<HandlerType, HandlerBefore>)
751+
{
752+
// HandlerBefore
753+
return [Handler = Forward<InHandlerType>(InHandler)]
754+
<typename Pointee, typename... OtherArgTypes>
755+
(ScopeType& Scope, Pointee* This, OtherArgTypes&&... OtherArgs)
756+
{
757+
Scope.ThisAdjustment = ThisAdjustment;
758+
This = reinterpret_cast<Pointee*>(reinterpret_cast<uintptr_t>(This) - ThisAdjustment);
759+
Handler(Scope, This, Forward<OtherArgTypes>(OtherArgs)...);
760+
Scope.ThisAdjustment = 0;
761+
};
762+
}
763+
else
764+
{
765+
// HandlerAfter
766+
static_assert(std::is_same_v<HandlerType, HandlerAfter>);
767+
if constexpr (std::is_void_v<ReturnType>)
768+
{
769+
// void return
770+
return [Handler = Forward<InHandlerType>(InHandler)]
771+
<typename Pointee, typename... OtherArgTypes>
772+
(Pointee* This, OtherArgTypes&&... OtherArgs)
773+
{
774+
This = reinterpret_cast<Pointee*>(reinterpret_cast<uintptr_t>(This) - ThisAdjustment);
775+
Handler(This, Forward<OtherArgTypes>(OtherArgs)...);
776+
};
777+
}
778+
else
779+
{
780+
// non-void return
781+
return [Handler = Forward<InHandlerType>(InHandler)]
782+
<typename Pointee, typename... OtherArgTypes>
783+
(const ReturnType& ReturnValue, Pointee* This, OtherArgTypes&&... OtherArgs)
784+
{
785+
This = reinterpret_cast<Pointee*>(reinterpret_cast<uintptr_t>(This) - ThisAdjustment);
786+
Handler(ReturnValue, This, Forward<OtherArgTypes>(OtherArgs)...);
787+
};
788+
}
789+
}
790+
}
791+
654792
static inline HandlersArray<HandlerBefore>* HandlersBefore;
655793
static inline HandlersArray<HandlerAfter>* HandlersAfter;
656794
static inline HandlersMap<HandlerBefore>* HandlerBeforeReferences;
657795
static inline HandlersMap<HandlerAfter>* HandlerAfterReferences;
796+
static inline ptrdiff_t ThisAdjustment;
658797
static inline void* OriginalFunctionCode;
659798
static inline void* Key;
660799
};

0 commit comments

Comments
 (0)