From af370088fc178996a46ee508aaa1a2c46318bbf2 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Fri, 26 Dec 2025 23:12:11 +0000 Subject: Move to static trampolines and type-safe hooks For some reason the DLL got a tiny little bit bigger again but that's fine. This will make orig_ calls more efficient in the inline case, and also make it harder to screw up and hook the wrong thing by mistake. Self-explanatory-ish, apart from the fact it's a fairly large API change of course. And it relies on some more bonkers assembler directive hackery. But it works! The only complaint one might have is that the featsetup functions no longer take an explicit string which occasionally yields slightly less perfect error messages, but I've decided this isn't really a problem and makes the API nicer to use. It's a tradeoff, innit. --- src/hook.h | 336 ++++++++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 231 insertions(+), 105 deletions(-) (limited to 'src/hook.h') diff --git a/src/hook.h b/src/hook.h index 26796e2..e4be554 100644 --- a/src/hook.h +++ b/src/hook.h @@ -17,137 +17,263 @@ #ifndef INC_HOOK_H #define INC_HOOK_H +#include "asm.h" #include "intdefs.h" #include "errmsg.h" #include "feature.h" #include "langext.h" -/* - * Replaces a vtable entry with a target function and returns the original - * function. - */ -static inline void *hook_vtable(void **vtable, usize off, void *target) { - void *orig = vtable[off]; - vtable[off] = target; - return orig; -} +#define _HOOK_STR2(x) #x +#define _HOOK_STR(x) _HOOK_STR2(x) +#define _HOOK_CAT2(a, b, c, d) a##b##c##d +#define _HOOK_CAT(a, b, c, d) _HOOK_CAT2(a, b, c, d) +#ifdef MODULE_NAME +#define _HOOK_MODNAME MODULE_NAME +#else +#define _HOOK_MODNAME G // kinda arbitrary thing for "global" +#endif + +#if defined(__GNUC__) || defined(__clang__) +#define _HOOK_UNUSED __attribute((unused)) +#else +#define _HOOK_UNUSED +#endif + +// internal helpers, do not call directly +uchar *_hook_getpos(uchar *); +struct _hook_prep_ret { + void *hookpos; + int inslen; + const char *err; +} _hook_prep(uchar *func, uchar *trampoline); +void _hook_inline_commit(uchar *restrict hookpos, const uchar *restrict target); +void _unhook_inline(uchar *trampoline, int len); + +#define _DEF_TRAMPOLINE_ASM(symb) \ + __asm ( \ + ".pushsection " ASM_RWX_SECTION_STR ", \"" ASM_RWX_SECTION_FLAGS "\"\n" \ + ".globl " symb "\n" \ + symb ":\n" \ + ".space 24\n" \ + ".popsection\n" \ + ); + +// internal macro detail, don't use +#define _DEF_TRAMPOLINE(ftype, name, symb) \ + _DEF_TRAMPOLINE_ASM(symb) \ + typeof(*(typeof(ftype))0) name __asm(symb); /* this declares the function! */ /* - * Puts an original function back after hooking. + * Creates a callable trampoline function backed by a chunk of uninitialised rwx + * memory. Calling this on its own will crash, but the inline hooking system + * can use it to create the wrapper/trampoline function used to call an original + * function from a hook. + * + * Note that it's usually unnecessary to create these manually. Most of the + * time this is handled by DEF_TRAMPOLINE(). */ -static inline void unhook_vtable(void **vtable, usize off, void *orig) { - vtable[off] = orig; -} +#define DEF_TRAMPOLINE(ftype, name) \ + _DEF_TRAMPOLINE(ftype, name, ASM_MANGLE_STR( \ + _HOOK_STR(_HOOK_CAT(_hook_t_, _HOOK_MODNAME, _, name)))) /* - * Finds the correct function prologue location to install an inline hook, and - * tries to initialise a trampoline with sufficient instructions and a jump back - * to enable calling the original function. + * Equivalent to DEF_INLINE_HOOK(), but with a manually-specified trampoline + * function (see DEF_TRAMPOLINE() for how to create one of those). * - * This is a low-level API and in most cases, if doing hooking from inside a - * plugin feature, the hook_inline_featsetup() function should be used instead. - * It automatically performs conventional error logging for both this step and - * the hook_inline_mprot() call below, and returns error codes that are - * convenient for use in a feature INIT function. - * - * When this function succeeds, the returned struct will have the prologue - * member set to the prologue or starting point of the hooked function (which is - * not always the same as the original function pointer). The trampoline - * parameter, being a pointer-to-pointer, is an output parameter to which a - * trampoline pointer will be written. The trampoline is a small run of - * instructions from the original function, followed by a jump back to it, - * allowing the original to be seamlessly called from a hook. - * - * In practically rare cases, this function will fail due to unsupported - * instructions in the function prologue. In such instances, the returned struct - * will have a null prologue, and the second member err, will point to a - * null-terminated string for error logging. In this case, the trampoline - * pointer will remain untouched. + * It is typically unnecessary to use this and DEF_INLINE_HOOK() should be used + * instead. */ -struct hook_inline_prep_ret { - void *prologue; - const char *err; -} hook_inline_prep(void *func, void **trampoline); +#define DEF_INLINE_HOOK_WITHTRAMPOLINE(ftype, name, trampoline) \ + static schar _hook_inslen_##name; \ + static inline struct hook_prep_ret_##name { \ + void *hookpos; \ + const char *err; \ + } hook_prep_##name(typeof(ftype) func) { \ + struct _hook_prep_ret r = _hook_prep((uchar *)func, \ + (uchar *)&trampoline); \ + _hook_inslen_##name = r.inslen; \ + return (struct hook_prep_ret_##name){r.hookpos, r.err}; \ + } \ + static inline void hook_commit_##name(void *hookpos, \ + typeof(ftype) target) { \ + _hook_inline_commit((uchar *)hookpos, (const uchar *)target); \ + } \ + static inline _HOOK_UNUSED void unhook_##name() { \ + _unhook_inline((void *)&trampoline, _hook_inslen_##name); \ + } \ + static inline _HOOK_UNUSED struct hook_featsetup_ret_##name { \ + void *hookpos; \ + int err; \ + } hook_featsetup_##name(typeof(ftype) f) { \ + struct hook_prep_ret_##name ret = hook_prep_##name(f); \ + if_cold (ret.err) { \ + errmsg_warnx("couldn't hook %s function: %s", #name, ret.err); \ + return (struct hook_featsetup_ret_##name){0, FEAT_INCOMPAT}; \ + } \ + if_cold (!hook_inline_mprot(ret.hookpos)) { \ + errmsg_errorsys("couldn't hook %s function: %s", #name, \ + "couldn't make hook point writable"); \ + return (struct hook_featsetup_ret_##name){0, FEAT_FAIL}; \ + } \ + return (struct hook_featsetup_ret_##name){ret.hookpos, 0}; \ + } /* - * This is a small helper function to make the memory page containing a - * function's prologue writable, allowing an inline hook to be inserted with - * hook_inline_commit(). + * Creates a set of inline hooking functions for hooking a particular named + * function. ftype specifies a function pointer type, generally defined first as + * _func as a matter of convention. * - * This is a low-level API and in most cases, if doing hooking from inside a - * plugin feature, the hook_inline_featsetup() function should be used instead. - * It automatically performs conventional error logging for both this step and - * the prior hook_inline_prep() call documented above, and returns error codes - * that are convenient for use in a feature INIT function. - * - * After using hook_inline_prep() to obtain the prologue and an appropriate - * trampoline, call this to unlock the prologue, and then use - * hook_inline_commit() to finalise the hook. In the event that multiple - * functions need to be hooked at once, the commit calls can be batched up at - * the end, removing the need for rollbacks since commitment is guaranteed to - * succeed after all setup is complete. - * - * This function returns true on success, or false if a failure occurs at the - * level of the OS memory protection API. os_lasterror() or errmsg_*sys() can be - * used to report such an error. + * Defines the following functions: + * + * static orig_(); + * + * This is the trampoline function, which is dynamically generated by + * hook_prep_() and allows wrapping the original function while it is + * otherwise redirected to the hook target. + * + * Calling this before the inline hook has been committed will likely crash + * or otherwise result in undefined behaviour. + * + * static inline struct hook_featsetup_ret_ { + * void *hookpos; + * int err; + * } hook_featsetup_(ftype f); + * + * This is a higher-level helper function intended for use in plugin feature + * initialisation (see INIT in feature.h). It combines the efforts of + * hook_prep_() and hook_inline_mprot() (see below), and also performs + * appropriate error logging. + * + * The err member of the returned struct is 0 on success, or a suitable + * feature return code (see FEAT_* in feature.h) on failure, so the status can + * just be returned directly from INIT if nonzero. + * + * On success, the hookpos member of the struct is ready for passing to + * hook_commit_() to finish setting up the hook. + * + * Generally, in feature code, there is no reason to call hook_prep_() + * directly, but it is documented below anyway. + * + * static struct hook_prep_ret_ { + * void *hookpos; + * const char *err; + * } hook_prep_(ftype func); + * + * This finds the correct jump point to pass to hook_inline_mprot() and then + * hook_commit_(), and sets up the trampoline (see orig_() above). + * + * The struct field hookpos, if not null, should be passed to + * hook_commit_() to finish installing the hook. + * + * If hookpos is null, something went wrong, and the null-terminated string + * err will provide a log message. + * + * static void hook_commit_(void *hookpos, ftype target); + * + * This finalises an inline hook to jump to the function pointed to by target. + * hookpos should have been successfully passed to hook_inline_mprot() first, + * otherwise the code will still be read only, likely causing a crash. + * + * static void unhook_() + * + * This undoes an inline hook, allowing the original code to function as it + * did before without further interception. */ -bool hook_inline_mprot(void *func); +#define DEF_INLINE_HOOK(ftype, name) \ + DEF_TRAMPOLINE(ftype, orig_##name) \ + DEF_INLINE_HOOK_WITHTRAMPOLINE(ftype, name, orig_##name) /* - * Finalises an inline hook set up using the hook_inline_prep() and - * hook_inline_mprot() functions above (or the hook_inline_featsetup() helper - * function below). prologue must be the prologue obtained via the - * aforementioned functons and target must be the function that will be jumped - * to in place of the original. It is very important that these functions are - * ABI-compatible lest obvious bad things happen. - * - * The resulting hook can be removed later by calling unhook_inline(). + * Very similar to DEF_INLINE_HOOK, except does not allow calling into the + * original function while the hook is installed. The hook_prep_() and + * hook_featsetup_() functions have simpler return values as a result: + * hook_prep_() cannot fail, stores the hook point internally and just + * returns it as a convenience for passing to hook_mprot(); and + * hook_featsetup_() only returns an error code since the hook point is + * no longer required for hook_commit_(). hook_commit_ also lacks + * the hookpos parameter as a result. */ -void hook_inline_commit(void *restrict prologue, void *restrict target); +#define DEF_INLINE_HOOK_NOTRAMPOLINE(ftype, name) \ + static uchar *_hook_origpos_##name; \ + static uchar _hook_origbytes_##name[5]; \ + static inline void *hook_prep_##name(typeof(ftype) func) { \ + _hook_origpos_##name = _hook_getpos((uchar *)func); \ + *(int *)_hook_origbytes_##name = *(int *)_hook_origpos_##name; \ + _hook_origbytes_##name[4] = ((uchar *)_hook_origpos_##name)[4]; \ + return _hook_origpos_##name; \ + } \ + static inline void hook_commit_##name(typeof(ftype) target) { \ + _hook_inline_commit(_hook_origpos_##name, (const uchar *)target); \ + } \ + static inline _HOOK_UNUSED void unhook_##name() { \ + *(int *)_hook_origpos_##name = *(int *)_hook_origbytes_##name; \ + _hook_origpos_##name[4] = _hook_origbytes_##name[4]; \ + } \ + static inline _HOOK_UNUSED int hook_featsetup_##name(typeof(ftype) f) { \ + void *hookpos = hook_prep_##name(f); \ + if_cold (!hook_inline_mprot(hookpos)) { \ + errmsg_errorsys("couldn't hook %s function: %s", #name, \ + "couldn't make hook point writable"); \ + return FEAT_FAIL; \ + } \ + return 0; \ + } /* - * This is a helper specifically for use in feature INIT code. It doesn't make - * much sense to call it elsewhere. - * - * Combines the functionality of the hook_inline_prep() and hook_inline_mprot() - * functions above, logs to the console on error automatically in a conventional - * format, and returns an error status that can be propagated straight from a - * feature INIT function. - * - * func must point to the original function to be hooked, orig must point to - * your trampoline pointer (which can in turn be used to call the original - * function indirectly from within your hook or elsewhere), and fname should be - * the name of the function for error logging purposes. - * - * If the err member of the returned struct is nonzero, simply return it as-is. - * Otherwise, the prologue member will contain the prologue pointer to pass to - * hook_inline_commit() to finalise the hook. + * Equivalent to DEF_VTABLE_HOOK(), but with a manually specified original + * function pointer which has to have been defined already. + * + * In most cases, DEF_VTABLE_HOOK() should be used instead. This exists mainly + * for space-saving union shenanigans which are done in a very small handful of + * places (and with highly dubious necessity). */ -static inline struct hook_inline_featsetup_ret { - void *prologue; - int err; -} hook_inline_featsetup(void *func, void **orig, const char *fname) { - void *trampoline; - struct hook_inline_prep_ret prep = hook_inline_prep(func, &trampoline); - if_cold (prep.err) { - errmsg_warnx("couldn't hook %s function: %s", fname, prep.err); - return (struct hook_inline_featsetup_ret){0, FEAT_INCOMPAT}; - } - if_cold (!hook_inline_mprot(prep.prologue)) { - errmsg_errorsys("couldn't hook %s function: %s", fname, - "couldn't make prologue writable"); - return (struct hook_inline_featsetup_ret){0, FEAT_FAIL}; +#define DEF_VTABLE_HOOK_WITHORIG(ftype, name, origp) \ + static inline void hook_##name(void **vtable, ssize idx, \ + typeof(ftype) target) { \ + (origp) = (typeof(ftype))vtable[idx]; \ + vtable[idx] = (void *)(target); \ + } \ + static inline void _HOOK_UNUSED unhook_##name(void **vtable, ssize idx) { \ + vtable[idx] = (void *)(origp); \ } - *orig = trampoline; - return (struct hook_inline_featsetup_ret){prep.prologue, 0}; -} /* - * Reverts a function to its original unhooked state. Takes the pointer to the - * callable "original" function, i.e. the trampoline, NOT the initial function - * pointer from before hooking. + * Creates a set of virtual table hooking functions for hooking a particular + * named function. ftype specifies a function pointer type, generally defined + * first as _func as a matter of convention. + * + * Defines the following functions: + * + * static orig_(); + * + * As an implementation detail, this is really a function pointer, but exists + * to allow calling the original function. It is also used to unhook the + * function again later, so generally should not be modified/used to point to + * something else. + * + * static void hook_(void **vtable, ssize idx, ftype target); + * + * Installs a virtual table hook by swapping the function at the given index. + * The virtual table must have first been made writable with os_mprot(). + * + * static void unhook_(void **vtable, ssize idx); + * + * Removes a virtual table hook by swapping back the original function + * pointer. The index must be the same one that was used for hook_(). + */ +#define DEF_VTABLE_HOOK(ftype, name) \ + static typeof(ftype) orig_##name; \ + DEF_VTABLE_HOOK_WITHORIG(ftype, name, orig_##name) + +/* + * This is a small helper function to make a function's hook point - found by + * hook_prep_*() - writable, allowing an inline hook to be inserted with + * hook_commit_*(). + * + * This is a low-level API and in most cases, if doing hooking from inside a + * plugin feature, the hook_inline_featsetup() function should be used instead. */ -void unhook_inline(void *orig); +bool hook_inline_mprot(void *hookpos); #endif -- cgit v1.2.3-54-g00ecf