Re-enabled PDB files.
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 version. 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"
53 #if defined PLATFORM_LINUX
55 # include <sys/mman.h>
60 #if SH_IMPL_VERSION >= 5
61 # if SOURCE_ENGINE >= SE_ORANGEBOX
62 SH_DECL_EXTERN1_void(ConCommand, Dispatch, SH_NOATTRIB, false, const CCommand &);
64 SH_DECL_EXTERN0_void(ConCommand, Dispatch, SH_NOATTRIB, false);
66 #elif SH_IMPL_VERSION == 3
67 extern bool __SourceHook_FHAddConCommandDispatch(void *, bool, class fastdelegate::FastDelegate0<void>);
68 extern bool __SourceHook_FHRemoveConCommandDispatch(void *, bool, class fastdelegate::FastDelegate0<void>);
71 #if SH_IMPL_VERSION >= 4
73 class GenericCommandHooker : public IConCommandLinkListener
79 unsigned int refcount;
81 CVector<HackInfo> vtables;
83 SourceHook::MemFuncInfo dispatch;
85 inline void **GetVirtualTable(ConCommandBase *pBase)
87 return *reinterpret_cast<void***>(reinterpret_cast<char*>(pBase) +
88 dispatch.thisptroffs +
92 inline bool FindVtable(void **ptr, size_t& index)
94 for (size_t i = 0; i < vtables.size(); i++)
96 if (vtables[i].vtable == ptr)
105 void MakeHookable(ConCommandBase *pBase)
107 if (!pBase->IsCommand())
110 ConCommand *cmd = (ConCommand*)pBase;
111 void **vtable = GetVirtualTable(cmd);
114 if (!FindVtable(vtable, index))
117 hack.vtable = vtable;
118 hack.hook = SH_ADD_VPHOOK(ConCommand, Dispatch, cmd, SH_MEMBER(this, &GenericCommandHooker::Dispatch), false);
120 vtables.push_back(hack);
124 vtables[index].refcount++;
128 # if SOURCE_ENGINE >= SE_ORANGEBOX
129 void Dispatch(const CCommand& args)
134 cell_t res = ConsoleDetours::Dispatch(META_IFACEPTR(ConCommand)
135 # if SOURCE_ENGINE >= SE_ORANGEBOX
139 if (res >= Pl_Handled)
140 RETURN_META(MRES_SUPERCEDE);
143 void ReparseCommandList()
145 for (size_t i = 0; i < vtables.size(); i++)
146 vtables[i].refcount = 0;
147 for (ConCommandBaseIterator iter; iter.IsValid(); iter.Next())
148 MakeHookable(iter.Get());
149 CVector<HackInfo>::iterator iter = vtables.begin();
150 while (iter != vtables.end())
152 if ((*iter).refcount)
154 /* Damn it. This event happens AFTER the plugin has unloaded!
155 * There's two options. Remove the hook now and hope SH's memory
156 * protection will prevent a crash. Otherwise, we can wait until
157 * the server shuts down and more likely crash then.
159 * This situation only arises if:
160 * 1) Someone has used AddCommandFilter()
161 * 2) ... on a Dark Messiah server (mm:s new api)
162 * 3) ... and another MM:S plugin that uses ConCommands has unloaded.
164 * Even though the impact is really small, we'll wait until the
165 * server shuts down, so normal operation isn't interrupted.
169 iter = vtables.erase(iter);
173 void UnhookCommand(ConCommandBase *pBase)
175 if (!pBase->IsCommand())
178 ConCommand *cmd = (ConCommand*)pBase;
179 void **vtable = GetVirtualTable(cmd);
182 if (!FindVtable(vtable, index))
184 g_Logger.LogError("Console detour tried to unhook command \"%s\" but it wasn't found", pBase->GetName());
188 assert(vtables[index].refcount > 0);
189 vtables[index].refcount--;
190 if (vtables[index].refcount == 0)
192 SH_REMOVE_HOOK_ID(vtables[index].hook);
193 vtables.erase(vtables.iterAt(index));
198 GenericCommandHooker() : enabled(false)
204 SourceHook::GetFuncInfo(&ConCommand::Dispatch, dispatch);
206 if (dispatch.thisptroffs < 0)
208 g_Logger.LogError("Command filter could not determine ConCommand layout");
212 for (ConCommandBaseIterator iter; iter.IsValid(); iter.Next())
213 MakeHookable(iter.Get());
217 g_Logger.LogError("Command filter could not find any cvars!");
228 for (size_t i = 0; i < vtables.size(); i++)
229 SH_REMOVE_HOOK_ID(vtables[i].hook);
233 virtual void OnLinkConCommand(ConCommandBase *pBase)
240 virtual void OnUnlinkConCommand(ConCommandBase *pBase)
245 ReparseCommandList();
247 UnhookCommand(pBase);
252 * END DVP HOOK VERSION
255 #else /* SH_IMPL_VERSION >= 4 */
258 * BEGIN ENGINE DETOUR VERSION
261 # include <jit/jit_helpers.h>
262 # include <jit/x86/x86_macros.h>
264 class GenericCommandHooker
268 Patch() : applied(false)
274 unsigned char search[16];
288 const char *platform = NULL;
289 # if defined(PLATFORM_WINDOWS)
290 platform = "Windows";
292 void *addrInBase = (void *)g_SMAPI->GetEngineFactory(false);
294 if (!dladdr(addrInBase, &info))
296 g_Logger.LogError("Command filter could not find engine name");
299 if (strstr(info.dli_fname, "engine_i486"))
301 platform = "Linux_486";
303 else if (strstr(info.dli_fname, "engine_i686"))
305 platform = "Linux_686";
307 else if (strstr(info.dli_fname, "engine_amd"))
309 platform = "Linux_AMD";
313 g_Logger.LogError("Command filter could not determine engine (%s)", info.dli_fname);
318 if (!PrepPatch("Cmd_ExecuteString", "CES", platform, &ces))
320 if (!PrepPatch("CGameClient::ExecuteString", "CGC", platform, &cgc))
337 # if !defined PLATFORM_WINDOWS
338 # define PAGE_READWRITE PROT_READ|PROT_WRITE
339 # define PAGE_EXECUTE_READ PROT_READ|PROT_EXEC
341 static inline uintptr_t AddrToPage(uintptr_t address)
343 return (address & ~(uintptr_t(sysconf(_SC_PAGE_SIZE) - 1)));
348 void Protect(void *addr, size_t length, int prot)
350 # if defined PLATFORM_WINDOWS
352 VirtualProtect(addr, length, prot, &ignore);
354 uintptr_t startPage = AddrToPage(uintptr_t(addr));
355 length += (uintptr_t(addr) - startPage);
356 mprotect((void*)startPage, length, prot);
360 void UndoPatch(Patch *patch)
362 if (!patch->applied || patch->detour == NULL)
365 g_pSourcePawn->FreePageMemory(patch->detour);
367 unsigned char *source = (unsigned char *)patch->base + patch->offset;
368 Protect(source, patch->search_len, PAGE_READWRITE);
369 for (size_t i = 0; i < patch->search_len; i++)
370 source[i] = patch->search[i];
371 Protect(source, patch->search_len, PAGE_EXECUTE_READ);
374 void ApplyPatch(Patch *patch)
376 assert(!patch->applied);
379 void *callback = (void*)&ConsoleDetours::Dispatch;
381 /* Bogus assignment to make compiler is doing the right thing. */
382 patch->detour = callback;
384 /* Assemgle the detour. */
386 writer.outbase = NULL;
387 writer.outptr = NULL;
390 /* Need a specific register, or value on stack? */
391 if (patch->regparam != -1)
392 IA32_Push_Reg(&writer, patch->regparam);
393 /* Call real function. */
394 IA32_Write_Jump32_Abs(&writer, IA32_Call_Imm32(&writer, 0), callback);
396 if (patch->regparam != -1)
397 IA32_Pop_Reg(&writer, patch->regparam);
398 /* Copy any saved bytes */
401 for (size_t i = patch->saveat; i < patch->search_len; i++)
403 writer.write_ubyte(patch->search[i]);
406 /* Jump back to the caller. */
407 unsigned char *target = (unsigned char *)patch->base + patch->offset + patch->search_len;
408 IA32_Jump_Imm32_Abs(&writer, target);
409 /* Assemble, if we can. */
410 if (writer.outbase == NULL)
412 length = writer.outptr - writer.outbase;
413 patch->detour = g_pSourcePawn->AllocatePageMemory(length);
414 if (patch->detour == NULL)
416 g_Logger.LogError("Ran out of memory!");
419 g_pSourcePawn->SetReadWrite(patch->detour);
420 writer.outbase = (jitcode_t)patch->detour;
421 writer.outptr = writer.outbase;
429 g_pSourcePawn->SetReadExecute(patch->detour);
431 unsigned char *source = (unsigned char *)patch->base + patch->offset;
432 Protect(source, 6, PAGE_READWRITE);
435 *(void **)&source[2] = &patch->detour;
436 Protect(source, 6, PAGE_EXECUTE_READ);
438 patch->applied = true;
441 bool PrepPatch(const char *signature, const char *name, const char *platform, Patch *patch)
443 /* Get the base address of the function. */
444 if (!g_pGameConf->GetMemSig(signature, &patch->base) || patch->base == NULL)
446 g_Logger.LogError("Command filter could not find signature: %s", signature);
453 /* Get the verification bytes that will be written over. */
454 UTIL_Format(keyname, sizeof(keyname), "%s_Patch_%s", name, platform);
455 if ((value = g_pGameConf->GetKeyValue(keyname)) == NULL)
457 g_Logger.LogError("Command filter could not find key: %s", keyname);
460 patch->search_len = UTIL_DecodeHexString(patch->search, sizeof(patch->search), value);
461 if (patch->search_len < 6)
463 g_Logger.LogError("Error decoding %s value, or not long enough", keyname);
467 /* Get the offset into the function. */
468 UTIL_Format(keyname, sizeof(keyname), "%s_Offset_%s", name, platform);
469 if ((value = g_pGameConf->GetKeyValue(keyname)) == NULL)
471 g_Logger.LogError("Command filter could not find key: %s", keyname);
474 patch->offset = atoi(value);
475 if (patch->offset > 20000)
477 g_Logger.LogError("Command filter %s value is bogus", keyname);
481 /* Get the number of bytes to save from what was written over. */
483 UTIL_Format(keyname, sizeof(keyname), "%s_Save_%s", name, platform);
484 if ((value = g_pGameConf->GetKeyValue(keyname)) != NULL)
486 patch->saveat = atoi(value);
487 if (patch->saveat >= patch->search_len)
489 g_Logger.LogError("Command filter %s value is too large", keyname);
494 /* Get register for parameter, if any. */
495 patch->regparam = -1;
496 UTIL_Format(keyname, sizeof(keyname), "%s_Reg_%s", name, platform);
497 if ((value = g_pGameConf->GetKeyValue(keyname)) != NULL)
499 patch->regparam = atoi(value);
502 /* Everything loaded from gamedata, make sure the patch will succeed. */
503 unsigned char *address = (unsigned char *)patch->base + patch->offset;
504 for (size_t i = 0; i < patch->search_len; i++)
506 if (address[i] != patch->search[i])
508 g_Logger.LogError("Command filter %s has changed (byte %x is not %x sub-offset %d)",
509 name, address[i], patch->search[i], i);
518 static bool dummy_hook_set = false;
523 dummy_hook_set = false;
524 RETURN_META(MRES_SUPERCEDE);
531 * BEGIN THE ACTUALLY GENERIC CODE.
534 static GenericCommandHooker s_GenericHooker;
535 ConsoleDetours g_ConsoleDetours;
537 ConsoleDetours::ConsoleDetours() : triedToEnable(false), isEnabled(false)
541 void ConsoleDetours::OnSourceModAllInitialized()
543 m_pForward = g_Forwards.CreateForwardEx("OnAnyCommand", ET_Hook, 3, NULL, Param_Cell,
544 Param_String, Param_Cell);
547 void ConsoleDetours::OnSourceModShutdown()
549 List<Listener*>::iterator iter = m_Listeners.begin();
550 while (iter != m_Listeners.end())
552 Listener *listener = (*iter);
553 g_Forwards.ReleaseForward(listener->forward);
555 iter = m_Listeners.erase(iter);
558 g_Forwards.ReleaseForward(m_pForward);
559 s_GenericHooker.Disable();
562 bool ConsoleDetours::IsAvailable()
566 isEnabled = s_GenericHooker.Enable();
567 triedToEnable = true;
571 bool ConsoleDetours::AddListener(IPluginFunction *fun, const char *command)
578 m_pForward->AddFunction(fun);
582 const char *str = UTIL_ToLowerCase(command);
584 Listener **plistener = m_CmdLookup.retrieve(str);
585 if (plistener == NULL)
587 listener = new Listener;
588 listener->forward = g_Forwards.CreateForwardEx(NULL, ET_Hook, 3, NULL, Param_Cell,
589 Param_String, Param_Cell);
590 m_CmdLookup.insert(str, listener);
594 listener = *plistener;
596 listener->forward->AddFunction(fun);
603 bool ConsoleDetours::RemoveListener(IPluginFunction *fun, const char *command)
607 return m_pForward->RemoveFunction(fun);
611 const char *str = UTIL_ToLowerCase(command);
613 Listener **plistener = m_CmdLookup.retrieve(str);
615 if (plistener == NULL)
617 listener = *plistener;
618 return listener->forward->RemoveFunction(fun);
622 cell_t ConsoleDetours::InternalDispatch(int client, const CCommand& args)
625 const char *realname = args.Arg(0);
626 size_t len = strlen(realname);
627 for (size_t i = 0; i < len; i++)
629 if (realname[i] >= 'A' && realname[i] <= 'Z')
630 name[i] = tolower(realname[i]);
632 name[i] = realname[i];
636 cell_t result = Pl_Continue;
637 m_pForward->PushCell(client);
638 m_pForward->PushString(name);
639 m_pForward->PushCell(args.ArgC() - 1);
640 m_pForward->Execute(&result, NULL);
642 /* Don't let plugins block this. */
643 if (strcmp(name, "sm") == 0)
644 result = Pl_Continue;
646 if (result >= Pl_Stop)
649 Listener **plistener = m_CmdLookup.retrieve(name);
650 if (plistener == NULL)
652 Listener *listener = *plistener;
653 if (listener->forward->GetFunctionCount() == 0)
656 cell_t result2 = Pl_Continue;
657 listener->forward->PushCell(client);
658 listener->forward->PushString(name);
659 listener->forward->PushCell(args.ArgC() - 1);
660 listener->forward->Execute(&result2, NULL);
662 if (result2 > result)
665 /* "sm" should not have flown through the above. */
666 assert(strcmp(name, "sm") != 0 || result == Pl_Continue);
671 #if SOURCE_ENGINE >= SE_ORANGEBOX
672 cell_t ConsoleDetours::Dispatch(ConCommand *pBase, const CCommand& args)
675 cell_t ConsoleDetours::Dispatch(ConCommand *pBase)
679 g_HL2.PushCommandStack(&args);
680 cell_t res = g_ConsoleDetours.InternalDispatch(g_ConCmds.GetCommandClient(), args);
681 g_HL2.PopCommandStack();
683 #if SH_IMPL_VERSION < 4
684 if (res >= Pl_Handled)
686 /* See bug 4020 - we can't optimize this because looking at the vtable
687 * is probably more expensive, since there's no SH_GET_ORIG_VFNPTR_ENTRY.
689 SH_ADD_HOOK_STATICFUNC(ConCommand, Dispatch, pBase, DummyHook, false);
690 dummy_hook_set = true;
692 SH_REMOVE_HOOK_STATICFUNC(ConCommand, Dispatch, pBase, DummyHook, false);
696 /* Make sure the command gets invoked. See bug 4019 on making this better. */