2 * vim: set ts=4 sw=4 tw=99 noet :
3 * =============================================================================
5 * Copyright (C) 2004-2009 AlliedModders LLC. All rights reserved.
6 * =============================================================================
8 * This program is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU General Public License, version 3.0, as published by the
10 * Free Software Foundation.
12 * This program is distributed in the hope that it will be useful, but WITHOUT
13 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
14 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
17 * You should have received a copy of the GNU General Public License along with
18 * this program. If not, see <http://www.gnu.org/licenses/>.
20 * As a special exception, AlliedModders LLC gives you permission to link the
21 * code of this program (as well as its derivative works) to "Half-Life 2," the
22 * "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software
23 * by the Valve Corporation. You must obey the GNU General Public License in
24 * all respects for all other code used. Additionally, AlliedModders LLC grants
25 * this exception to all derivative works. AlliedModders LLC defines further
26 * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007),
27 * or <http://www.sourcemod.net/license.php>.
33 * On SourceHook v4.3 or lower, there are no DVP hooks. Very sad, right?
34 * Only do this on newer versions. For the older code, we'll do an incredibly
37 * The idea of the "non-hacky" (yeah... no) code is that every unique
38 * ConCommand vtable gets its own DVP hook. We watch for unloading and
39 * loading commands to remove stale hooks from SH.
42 #include "sourcemod.h"
43 #include "sourcemm_api.h"
45 #include "compat_wrappers.h"
46 #include "ConsoleDetours.h"
47 #include "GameConfigs.h"
48 #include "sm_stringutil.h"
49 #include "ConCmdManager.h"
50 #include "HalfLife2.h"
51 #include "ConCommandBaseIterator.h"
54 #if defined PLATFORM_LINUX
56 # include <sys/mman.h>
61 #if SH_IMPL_VERSION >= 5
62 # if SOURCE_ENGINE >= SE_ORANGEBOX
63 SH_DECL_EXTERN1_void(ConCommand, Dispatch, SH_NOATTRIB, false, const CCommand &);
65 SH_DECL_EXTERN0_void(ConCommand, Dispatch, SH_NOATTRIB, false);
68 # if SH_IMPL_VERSION >= 4
69 extern int __SourceHook_FHVPAddConCommandDispatch(void *,bool,class fastdelegate::FastDelegate0<void>,bool);
70 extern int __SourceHook_FHAddConCommandDispatch(void *, bool, class fastdelegate::FastDelegate0<void>);
72 extern bool __SourceHook_FHAddConCommandDispatch(void *, bool, class fastdelegate::FastDelegate0<void>);
74 extern bool __SourceHook_FHRemoveConCommandDispatch(void *, bool, class fastdelegate::FastDelegate0<void>);
77 #if SH_IMPL_VERSION >= 4
79 class GenericCommandHooker : public IConCommandLinkListener
85 unsigned int refcount;
87 CVector<HackInfo> vtables;
89 SourceHook::MemFuncInfo dispatch;
91 inline void **GetVirtualTable(ConCommandBase *pBase)
93 return *reinterpret_cast<void***>(reinterpret_cast<char*>(pBase) +
94 dispatch.thisptroffs +
98 inline bool FindVtable(void **ptr, size_t& index)
100 for (size_t i = 0; i < vtables.size(); i++)
102 if (vtables[i].vtable == ptr)
111 void MakeHookable(ConCommandBase *pBase)
113 if (!pBase->IsCommand())
116 ConCommand *cmd = (ConCommand*)pBase;
117 void **vtable = GetVirtualTable(cmd);
120 if (!FindVtable(vtable, index))
123 hack.vtable = vtable;
124 hack.hook = SH_ADD_VPHOOK(ConCommand, Dispatch, cmd, SH_MEMBER(this, &GenericCommandHooker::Dispatch), false);
126 vtables.push_back(hack);
130 vtables[index].refcount++;
134 # if SOURCE_ENGINE >= SE_ORANGEBOX
135 void Dispatch(const CCommand& args)
140 cell_t res = ConsoleDetours::Dispatch(META_IFACEPTR(ConCommand)
141 # if SOURCE_ENGINE >= SE_ORANGEBOX
145 if (res >= Pl_Handled)
146 RETURN_META(MRES_SUPERCEDE);
149 void ReparseCommandList()
151 for (size_t i = 0; i < vtables.size(); i++)
152 vtables[i].refcount = 0;
153 for (ConCommandBaseIterator iter; iter.IsValid(); iter.Next())
154 MakeHookable(iter.Get());
155 CVector<HackInfo>::iterator iter = vtables.begin();
156 while (iter != vtables.end())
158 if ((*iter).refcount)
163 /* Damn it. This event happens AFTER the plugin has unloaded!
164 * There's two options. Remove the hook now and hope SH's memory
165 * protection will prevent a crash. Otherwise, we can wait until
166 * the server shuts down and more likely crash then.
168 * This situation only arises if:
169 * 1) Someone has used AddCommandFilter()
170 * 2) ... on a Dark Messiah server (mm:s new api)
171 * 3) ... and another MM:S plugin that uses ConCommands has unloaded.
173 * Even though the impact is really small, we'll wait until the
174 * server shuts down, so normal operation isn't interrupted.
178 iter = vtables.erase(iter);
182 void UnhookCommand(ConCommandBase *pBase)
184 if (!pBase->IsCommand())
187 ConCommand *cmd = (ConCommand*)pBase;
188 void **vtable = GetVirtualTable(cmd);
191 if (!FindVtable(vtable, index))
193 g_Logger.LogError("Console detour tried to unhook command \"%s\" but it wasn't found", pBase->GetName());
197 assert(vtables[index].refcount > 0);
198 vtables[index].refcount--;
199 if (vtables[index].refcount == 0)
201 SH_REMOVE_HOOK_ID(vtables[index].hook);
202 vtables.erase(vtables.iterAt(index));
207 GenericCommandHooker() : enabled(false)
213 SourceHook::GetFuncInfo(&ConCommand::Dispatch, dispatch);
215 if (dispatch.thisptroffs < 0)
217 g_Logger.LogError("Command filter could not determine ConCommand layout");
221 for (ConCommandBaseIterator iter; iter.IsValid(); iter.Next())
222 MakeHookable(iter.Get());
226 g_Logger.LogError("Command filter could not find any cvars!");
237 for (size_t i = 0; i < vtables.size(); i++)
238 SH_REMOVE_HOOK_ID(vtables[i].hook);
242 virtual void OnLinkConCommand(ConCommandBase *pBase)
249 virtual void OnUnlinkConCommand(ConCommandBase *pBase)
254 ReparseCommandList();
256 UnhookCommand(pBase);
261 * END DVP HOOK VERSION
264 #else /* SH_IMPL_VERSION >= 4 */
267 * BEGIN ENGINE DETOUR VERSION
270 # include <jit/jit_helpers.h>
271 # include <jit/x86/x86_macros.h>
273 class GenericCommandHooker
277 Patch() : applied(false)
283 unsigned char search[16];
295 static void DelayedActivation(void *inparam)
297 GenericCommandHooker *cdtrs = reinterpret_cast<GenericCommandHooker*>(inparam);
298 /* Safe to re-enter because the frame queue is lock+swapped. */
299 if ((!cdtrs->ces.applied || !cdtrs->cgc.applied) &&
300 g_HL2.PeekCommandStack() != NULL)
302 g_SourceMod.AddFrameAction(GenericCommandHooker::DelayedActivation, cdtrs);
306 if (!cdtrs->ces.applied)
307 cdtrs->ApplyPatch(&cdtrs->ces);
308 if (!cdtrs->cgc.applied)
309 cdtrs->ApplyPatch(&cdtrs->cgc);
314 const char *platform = NULL;
315 # if defined(PLATFORM_WINDOWS)
316 platform = "Windows";
318 void *addrInBase = (void *)g_SMAPI->GetEngineFactory(false);
320 if (!dladdr(addrInBase, &info))
322 g_Logger.LogError("Command filter could not find engine name");
325 if (strstr(info.dli_fname, "engine_i486"))
327 platform = "Linux_486";
329 else if (strstr(info.dli_fname, "engine_i686"))
331 platform = "Linux_686";
333 else if (strstr(info.dli_fname, "engine_amd"))
335 platform = "Linux_AMD";
339 g_Logger.LogError("Command filter could not determine engine (%s)", info.dli_fname);
344 if (!PrepPatch("Cmd_ExecuteString", "CES", platform, &ces))
346 if (!PrepPatch("CGameClient::ExecuteString", "CGC", platform, &cgc))
349 if (g_HL2.PeekCommandStack() != NULL)
351 g_SourceMod.AddFrameAction(GenericCommandHooker::DelayedActivation, this);
369 # if !defined PLATFORM_WINDOWS
370 # define PAGE_READWRITE PROT_READ|PROT_WRITE
371 # define PAGE_EXECUTE_READ PROT_READ|PROT_EXEC
373 static inline uintptr_t AddrToPage(uintptr_t address)
375 return (address & ~(uintptr_t(sysconf(_SC_PAGE_SIZE) - 1)));
380 void Protect(void *addr, size_t length, int prot)
382 # if defined PLATFORM_WINDOWS
384 VirtualProtect(addr, length, prot, &ignore);
386 uintptr_t startPage = AddrToPage(uintptr_t(addr));
387 length += (uintptr_t(addr) - startPage);
388 mprotect((void*)startPage, length, prot);
392 void UndoPatch(Patch *patch)
394 if (!patch->applied || patch->detour == NULL)
397 g_pSourcePawn->FreePageMemory(patch->detour);
399 unsigned char *source = (unsigned char *)patch->base + patch->offset;
400 Protect(source, patch->search_len, PAGE_READWRITE);
401 for (size_t i = 0; i < patch->search_len; i++)
402 source[i] = patch->search[i];
403 Protect(source, patch->search_len, PAGE_EXECUTE_READ);
406 void ApplyPatch(Patch *patch)
408 assert(!patch->applied);
411 void *callback = (void*)&ConsoleDetours::Dispatch;
413 /* Bogus assignment to make compiler is doing the right thing. */
414 patch->detour = callback;
416 /* Assemgle the detour. */
418 writer.outbase = NULL;
419 writer.outptr = NULL;
422 /* Need a specific register, or value on stack? */
423 if (patch->regparam != -1)
424 IA32_Push_Reg(&writer, patch->regparam);
425 /* Call real function. */
426 IA32_Write_Jump32_Abs(&writer, IA32_Call_Imm32(&writer, 0), callback);
428 if (patch->regparam != -1)
429 IA32_Pop_Reg(&writer, patch->regparam);
430 /* Copy any saved bytes */
433 for (size_t i = patch->saveat; i < patch->search_len; i++)
435 writer.write_ubyte(patch->search[i]);
438 /* Jump back to the caller. */
439 unsigned char *target = (unsigned char *)patch->base + patch->offset + patch->search_len;
440 IA32_Jump_Imm32_Abs(&writer, target);
441 /* Assemble, if we can. */
442 if (writer.outbase == NULL)
444 length = writer.outptr - writer.outbase;
445 patch->detour = g_pSourcePawn->AllocatePageMemory(length);
446 if (patch->detour == NULL)
448 g_Logger.LogError("Ran out of memory!");
451 g_pSourcePawn->SetReadWrite(patch->detour);
452 writer.outbase = (jitcode_t)patch->detour;
453 writer.outptr = writer.outbase;
461 g_pSourcePawn->SetReadExecute(patch->detour);
463 unsigned char *source = (unsigned char *)patch->base + patch->offset;
464 Protect(source, 6, PAGE_READWRITE);
467 *(void **)&source[2] = &patch->detour;
468 Protect(source, 6, PAGE_EXECUTE_READ);
470 patch->applied = true;
473 bool PrepPatch(const char *signature, const char *name, const char *platform, Patch *patch)
475 /* Get the base address of the function. */
476 if (!g_pGameConf->GetMemSig(signature, &patch->base) || patch->base == NULL)
478 g_Logger.LogError("Command filter could not find signature: %s", signature);
485 /* Get the verification bytes that will be written over. */
486 UTIL_Format(keyname, sizeof(keyname), "%s_Patch_%s", name, platform);
487 if ((value = g_pGameConf->GetKeyValue(keyname)) == NULL)
489 g_Logger.LogError("Command filter could not find key: %s", keyname);
492 patch->search_len = UTIL_DecodeHexString(patch->search, sizeof(patch->search), value);
493 if (patch->search_len < 6)
495 g_Logger.LogError("Error decoding %s value, or not long enough", keyname);
499 /* Get the offset into the function. */
500 UTIL_Format(keyname, sizeof(keyname), "%s_Offset_%s", name, platform);
501 if ((value = g_pGameConf->GetKeyValue(keyname)) == NULL)
503 g_Logger.LogError("Command filter could not find key: %s", keyname);
506 patch->offset = atoi(value);
507 if (patch->offset > 20000)
509 g_Logger.LogError("Command filter %s value is bogus", keyname);
513 /* Get the number of bytes to save from what was written over. */
515 UTIL_Format(keyname, sizeof(keyname), "%s_Save_%s", name, platform);
516 if ((value = g_pGameConf->GetKeyValue(keyname)) != NULL)
518 patch->saveat = atoi(value);
519 if (patch->saveat >= patch->search_len)
521 g_Logger.LogError("Command filter %s value is too large", keyname);
526 /* Get register for parameter, if any. */
527 patch->regparam = -1;
528 UTIL_Format(keyname, sizeof(keyname), "%s_Reg_%s", name, platform);
529 if ((value = g_pGameConf->GetKeyValue(keyname)) != NULL)
531 patch->regparam = atoi(value);
534 /* Everything loaded from gamedata, make sure the patch will succeed. */
535 unsigned char *address = (unsigned char *)patch->base + patch->offset;
536 for (size_t i = 0; i < patch->search_len; i++)
538 if (address[i] != patch->search[i])
540 g_Logger.LogError("Command filter %s has changed (byte %x is not %x sub-offset %d)",
541 name, address[i], patch->search[i], i);
550 static bool dummy_hook_set = false;
555 dummy_hook_set = false;
556 RETURN_META(MRES_SUPERCEDE);
563 * BEGIN THE ACTUALLY GENERIC CODE.
566 #define FEATURECAP_COMMANDLISTENER "command listener"
568 static GenericCommandHooker s_GenericHooker;
569 ConsoleDetours g_ConsoleDetours;
571 ConsoleDetours::ConsoleDetours() : status(FeatureStatus_Unknown)
575 void ConsoleDetours::OnSourceModAllInitialized()
577 m_pForward = g_Forwards.CreateForwardEx("OnAnyCommand", ET_Hook, 3, NULL, Param_Cell,
578 Param_String, Param_Cell);
579 g_ShareSys.AddCapabilityProvider(NULL, this, FEATURECAP_COMMANDLISTENER);
582 void ConsoleDetours::OnSourceModShutdown()
584 List<Listener*>::iterator iter = m_Listeners.begin();
585 while (iter != m_Listeners.end())
587 Listener *listener = (*iter);
588 g_Forwards.ReleaseForward(listener->forward);
590 iter = m_Listeners.erase(iter);
593 g_Forwards.ReleaseForward(m_pForward);
594 s_GenericHooker.Disable();
597 FeatureStatus ConsoleDetours::GetFeatureStatus(FeatureType type, const char *name)
602 FeatureStatus ConsoleDetours::GetStatus()
604 if (status == FeatureStatus_Unknown)
606 status = s_GenericHooker.Enable() ? FeatureStatus_Available : FeatureStatus_Unavailable;
611 bool ConsoleDetours::AddListener(IPluginFunction *fun, const char *command)
613 if (GetStatus() != FeatureStatus_Available)
618 m_pForward->AddFunction(fun);
622 const char *str = UTIL_ToLowerCase(command);
624 Listener **plistener = m_CmdLookup.retrieve(str);
625 if (plistener == NULL)
627 listener = new Listener;
628 listener->forward = g_Forwards.CreateForwardEx(NULL, ET_Hook, 3, NULL, Param_Cell,
629 Param_String, Param_Cell);
630 m_CmdLookup.insert(str, listener);
634 listener = *plistener;
636 listener->forward->AddFunction(fun);
643 bool ConsoleDetours::RemoveListener(IPluginFunction *fun, const char *command)
647 return m_pForward->RemoveFunction(fun);
651 const char *str = UTIL_ToLowerCase(command);
653 Listener **plistener = m_CmdLookup.retrieve(str);
655 if (plistener == NULL)
657 listener = *plistener;
658 return listener->forward->RemoveFunction(fun);
662 cell_t ConsoleDetours::InternalDispatch(int client, const CCommand& args)
665 const char *realname = args.Arg(0);
666 size_t len = strlen(realname);
667 for (size_t i = 0; i < len; i++)
669 if (realname[i] >= 'A' && realname[i] <= 'Z')
670 name[i] = tolower(realname[i]);
672 name[i] = realname[i];
676 cell_t result = Pl_Continue;
677 m_pForward->PushCell(client);
678 m_pForward->PushString(name);
679 m_pForward->PushCell(args.ArgC() - 1);
680 m_pForward->Execute(&result, NULL);
682 /* Don't let plugins block this. */
683 if (strcmp(name, "sm") == 0)
684 result = Pl_Continue;
686 if (result >= Pl_Stop)
689 Listener **plistener = m_CmdLookup.retrieve(name);
690 if (plistener == NULL)
692 Listener *listener = *plistener;
693 if (listener->forward->GetFunctionCount() == 0)
696 cell_t result2 = Pl_Continue;
697 listener->forward->PushCell(client);
698 listener->forward->PushString(name);
699 listener->forward->PushCell(args.ArgC() - 1);
700 listener->forward->Execute(&result2, NULL);
702 if (result2 > result)
705 /* "sm" should not have flown through the above. */
706 assert(strcmp(name, "sm") != 0 || result == Pl_Continue);
711 #if SOURCE_ENGINE >= SE_ORANGEBOX
712 cell_t ConsoleDetours::Dispatch(ConCommand *pBase, const CCommand& args)
715 cell_t ConsoleDetours::Dispatch(ConCommand *pBase)
719 g_HL2.PushCommandStack(&args);
720 cell_t res = g_ConsoleDetours.InternalDispatch(g_ConCmds.GetCommandClient(), args);
721 g_HL2.PopCommandStack();
723 #if SH_IMPL_VERSION < 4
724 if (res >= Pl_Handled)
726 /* See bug 4020 - we can't optimize this because looking at the vtable
727 * is probably more expensive, since there's no SH_GET_ORIG_VFNPTR_ENTRY.
729 SH_ADD_HOOK_STATICFUNC(ConCommand, Dispatch, pBase, DummyHook, false);
730 dummy_hook_set = true;
732 SH_REMOVE_HOOK_STATICFUNC(ConCommand, Dispatch, pBase, DummyHook, false);
736 /* Make sure the command gets invoked. See bug 4019 on making this better. */