From 6a7e0b3bae04a5a4667267b6a45baddeb66fc570 Mon Sep 17 00:00:00 2001 From: salmonik Date: Wed, 4 Nov 2026 13:19:50 +0100 Subject: Fix No spread on the hl2_complete server build The server RandomSeed GOT slot was hardcoded to 0xce1348, measured on the older bin/server.so. The anniversary branch loads hl2_complete/bin/server.so, whose R_386_JMP_SLOT for RandomSeed is 0xce5348, so the patch hit unrelated writable data and no spread silently did nothing. - Read the relocation from the loaded module's ELF (module_jump_slot) instead of hardcoding, with the two known offsets as fallback. - Override whenever the RandomSeed caller's `this` is the local server player (CBaseEntity::FireBullets) instead of matching a hardcoded return-address range that also moved between builds. - m_takedamage is a char (FIELD_CHARACTER) in the 2013 SDK; resolve it as such and read a byte, so the IMMUNE damage tag actually works. --- src/hl2h.cpp | 50 ++++++++++++++++++++--------- src/hl2h.h | 2 ++ src/modules.cpp | 99 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/server.cpp | 11 +++++-- 4 files changed, 144 insertions(+), 18 deletions(-) (limited to 'src') diff --git a/src/hl2h.cpp b/src/hl2h.cpp index 28b2995..7ce35d3 100644 --- a/src/hl2h.cpp +++ b/src/hl2h.cpp @@ -471,11 +471,12 @@ static bool jumpbug_apply(CUserCmd *cmd, int local, void *ent, int flags, // server RandomSeed diagnostic / override // --------------------------------------------------------------------------- // -// The installed 2024 server build is stripped and its usercmd/server-random-seed -// handling differs from the 2013 SDK source. This hook is installed on the -// server.so GOT slot for RandomSeed (R_386_JUMP_SLOT at vaddr 0xce1348) so we -// can see which seed FireBullets actually uses and optionally force the chosen -// spread seed. Disabled unless HL2H_SEED_TRACE / HL2H_SEED_FORCE are set. +// The server is a stripped build whose usercmd/random-seed handling differs +// from the 2013 SDK source. Hook the server.so PLT slot for RandomSeed so we +// can force the chosen spread seed. The GOT offset is read from the loaded +// module's relocation table at runtime (module_jump_slot): hardcoding it broke +// when Steam switched to the hl2_complete/server.so branch (0xce1348 -> +// 0xce5348). static void (*g_real_random_seed)(int) = nullptr; static uintptr_t g_srv_base = 0; @@ -487,8 +488,10 @@ static void hkdSrvRandomSeed(int seed) { long off = (long)(ret - g_srv_base); static bool trace = getenv("HL2H_SEED_TRACE") != nullptr; static int logs = 0; - bool bullet_site = (off >= 0x216900 && off <= 0x216930); + // The caller's first argument is `this`. Only FireBullets is called on the + // player entity; the CUniformRandomStream wrappers and the constructor-time + // RandomSeed(0) reset have a different object there. void *frame = nullptr; __asm__ volatile("movl %%ebp, %0" : "=r"(frame)); if (frame) @@ -497,7 +500,7 @@ static void hkdSrvRandomSeed(int seed) { bool local = self && g_local_server_player && (uintptr_t)self == g_local_server_player; - if (bullet_site && local && feature_active(FEAT_SPREAD) && + if (local && g_server.contains(ret) && feature_active(FEAT_SPREAD) && spread_no_spread_enabled()) { int pellets = g_spread_pellets; if (pellets < 1) @@ -516,12 +519,10 @@ static void hkdSrvRandomSeed(int seed) { idx++; } - if (trace && (bullet_site || logs < 12)) { - if (logs < 200) { - logs++; - logd("srvseed: seed=%d off=0x%lx bullet=%d self=%p local=%d", seed, off, - (int)bullet_site, self, (int)local); - } + if (trace && logs < 200) { + logs++; + logd("srvseed: seed=%d off=0x%lx self=%p local=%d", seed, off, self, + (int)local); } if (g_real_random_seed) g_real_random_seed(seed); @@ -532,9 +533,28 @@ static void install_seed_hook() { if (g_seed_hook_installed || failed || !g_server.valid()) return; g_srv_base = g_server.ranges[0].lo; - uintptr_t slot = g_srv_base + 0xce1348; // R_386_JUMP_SLOT RandomSeed - if (!g_server.contains(slot)) + uintptr_t reloc = 0, bias = 0; + uintptr_t slot = 0; + if (module_jump_slot(g_server.path.c_str(), "RandomSeed", &reloc, &bias)) { + slot = g_srv_base + reloc - bias; + logf("seed hook: RandomSeed GOT reloc=%#lx bias=%#lx -> %#lx", + (unsigned long)reloc, (unsigned long)bias, (unsigned long)slot); + } else { + // ELF parse failed (deleted/unreadable module); try the builds we know. + static const uintptr_t known[] = {0xce5348, 0xce1348}; + for (size_t i = 0; i < sizeof(known) / sizeof(known[0]) && !slot; i++) { + uintptr_t cand = g_srv_base + known[i]; + if (g_server.contains(cand)) + slot = cand; + } + logf("seed hook: ELF reloc scan failed, fallback slot=%#lx", + (unsigned long)slot); + } + if (!slot || !g_server.contains(slot)) { + failed = true; + logf("seed hook: no writable RandomSeed slot"); return; + } g_real_random_seed = (void (*)(int))dlsym(RTLD_NEXT, "RandomSeed"); if (!g_real_random_seed) diff --git a/src/hl2h.h b/src/hl2h.h index e35e737..b7fc2f5 100644 --- a/src/hl2h.h +++ b/src/hl2h.h @@ -278,6 +278,8 @@ int server_npc_takedamage(int idx); int server_npc_turret_flags(int idx); int server_model_index(void *base); void find_modules(); +bool module_jump_slot(const char *path, const char *name, uintptr_t *vaddr, + uintptr_t *bias); void init_conmsg(); bool feature_active(int feature); bool feature_has_bind(int feature); diff --git a/src/modules.cpp b/src/modules.cpp index 70e2dff..b832694 100644 --- a/src/modules.cpp +++ b/src/modules.cpp @@ -1,5 +1,7 @@ // modules.cpp — /proc/self/maps parsing and module discovery #include "hl2h.h" +#include +#include static bool basename_is(const char *path, const char *base) { size_t pl = strlen(path), bl = strlen(base); @@ -70,3 +72,100 @@ void find_modules() { } fclose(f); } + +// --------------------------------------------------------------------------- +// ELF relocation lookup +// --------------------------------------------------------------------------- +// +// The server RandomSeed hook used to hardcode the GOT offset measured on one +// server.so build. The anniversary branch ships a different server.so +// (hl2_complete/bin/server.so) whose slot moved, so the patch silently landed +// on unrelated data and No spread stopped working. Read the loaded module's +// ELF relocation table instead: find the R_386_JMP_SLOT for a symbol and +// return its virtual address plus the load bias implied by the first PT_LOAD. +bool module_jump_slot(const char *path, const char *name, uintptr_t *out_vaddr, + uintptr_t *out_bias) { + if (!path || !*path || !name) + return false; + FILE *f = fopen(path, "rb"); + if (!f) + return false; + if (fseek(f, 0, SEEK_END) != 0) { + fclose(f); + return false; + } + long sz = ftell(f); + if (sz < (long)sizeof(Elf32_Ehdr) || fseek(f, 0, SEEK_SET) != 0) { + fclose(f); + return false; + } + std::vector buf((size_t)sz); + size_t got = fread(buf.data(), 1, buf.size(), f); + fclose(f); + if (got != buf.size()) + return false; + if (buf.size() < sizeof(Elf32_Ehdr)) + return false; + + const Elf32_Ehdr *eh = (const Elf32_Ehdr *)buf.data(); + if (memcmp(eh->e_ident, ELFMAG, SELFMAG) != 0 || + eh->e_ident[EI_CLASS] != ELFCLASS32) + return false; + if (eh->e_shoff == 0 || eh->e_shentsize != sizeof(Elf32_Shdr)) + return false; + if ((uint64_t)eh->e_shoff + (uint64_t)eh->e_shnum * sizeof(Elf32_Shdr) > + buf.size()) + return false; + const Elf32_Shdr *sh = (const Elf32_Shdr *)(buf.data() + eh->e_shoff); + + uintptr_t bias = 0; + if (eh->e_phoff && eh->e_phnum && + eh->e_phentsize == sizeof(Elf32_Phdr) && + (uint64_t)eh->e_phoff + (uint64_t)eh->e_phnum * sizeof(Elf32_Phdr) <= + buf.size()) { + const Elf32_Phdr *ph = (const Elf32_Phdr *)(buf.data() + eh->e_phoff); + for (int i = 0; i < eh->e_phnum; i++) { + if (ph[i].p_type == PT_LOAD) { + bias = ph[i].p_vaddr; + break; + } + } + } + + for (int s = 0; s < eh->e_shnum; s++) { + if (sh[s].sh_type != SHT_REL || sh[s].sh_link >= eh->e_shnum) + continue; + if ((uint64_t)sh[s].sh_offset + (uint64_t)sh[s].sh_size > buf.size()) + continue; + const Elf32_Shdr &symsh = sh[sh[s].sh_link]; + if (symsh.sh_link >= eh->e_shnum) + continue; + const Elf32_Shdr &strsh = sh[symsh.sh_link]; + if ((uint64_t)symsh.sh_offset + (uint64_t)symsh.sh_size > buf.size() || + (uint64_t)strsh.sh_offset + (uint64_t)strsh.sh_size > buf.size()) + continue; + const Elf32_Rel *rels = (const Elf32_Rel *)(buf.data() + sh[s].sh_offset); + size_t nrels = sh[s].sh_size / sizeof(Elf32_Rel); + const Elf32_Sym *syms = (const Elf32_Sym *)(buf.data() + symsh.sh_offset); + size_t nsyms = symsh.sh_size / sizeof(Elf32_Sym); + const char *strs = (const char *)(buf.data() + strsh.sh_offset); + size_t strn = strsh.sh_size; + for (size_t r = 0; r < nrels; r++) { + unsigned type = ELF32_R_TYPE(rels[r].r_info); + unsigned sym = ELF32_R_SYM(rels[r].r_info); + if (type != R_386_JMP_SLOT || sym >= nsyms) + continue; + if (syms[sym].st_name >= strn) + continue; + const char *sn = strs + syms[sym].st_name; + if (strcmp(sn, name) != 0) + continue; + if (out_vaddr) + *out_vaddr = rels[r].r_offset; + if (out_bias) + *out_bias = bias; + return true; + } + } + return false; +} diff --git a/src/server.cpp b/src/server.cpp index e9fd921..f71b441 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -142,7 +142,11 @@ bool resolve_server_netvars() { g_srv_spawnflags = datamap_offset("m_spawnflags", FIELD_INTEGER, 0x4); g_srv_hacked = datamap_offset("m_bHackedByAlyx", FIELD_BOOLEAN); g_srv_npcstate = datamap_offset("m_NPCState", FIELD_INTEGER); - g_srv_takedamage = datamap_offset("m_takedamage", FIELD_INTEGER); + // CBaseEntity::m_takedamage is a char in the 2013 SDK + // (CNetworkVarForDerived(char, m_takedamage)); older builds used int. + g_srv_takedamage = datamap_offset("m_takedamage", FIELD_CHARACTER); + if (g_srv_takedamage < 0) + g_srv_takedamage = datamap_offset("m_takedamage", FIELD_INTEGER); g_srv_ready = g_srv_health > 0; if (g_srv_ready) { logf("server datamap (%s): health=%d maxhealth=%d lifeste=%d " @@ -272,7 +276,8 @@ int server_npc_state(int idx) { } // TAKEDAMAGE: 0 = DAMAGE_NO, 1 = DAMAGE_EVENTS_ONLY, 2 = DAMAGE_YES, -// 3 = DAMAGE_AIM. 0/1 cannot be damaged at all. +// 3 = DAMAGE_AIM. 0/1 cannot be damaged at all. The field is a char, so read +// a byte even if the older int layout was resolved. int server_npc_takedamage(int idx) { if (!g_srv_ready || g_srv_takedamage < 0) return -1; @@ -280,7 +285,7 @@ int server_npc_takedamage(int idx) { server_entity_health(idx, nullptr, nullptr, &base); if (!base) return -1; - return *(int *)((uintptr_t)base + g_srv_takedamage); + return (int)*(unsigned char *)((uintptr_t)base + g_srv_takedamage); } bool server_npc_has_follow_target(int idx) { -- cgit v1.2.3