From 33f5ed9ef58f26c6b3b52cf605dd4311d658a60d Mon Sep 17 00:00:00 2001 From: salmonik Date: Fri, 6 Nov 2026 16:09:55 +0100 Subject: Fix audit findings: crash safety, correctness, rendering, performance Crash/UB: - gate every hook behind an init-stage flag until config/surface/input are ready - publish module maps as immutable atomic snapshots instead of reallocating shared vectors under unlocked readers - validate the %ebp walk in the RandomSeed/RandomFloat hooks against the stack - bounds-check every RecvTable/RecvProp/string dereference with module_range() - preserve the Itanium vtable header when cloning (client, CInput, model render, studio render) - reject negative usercmd sequence numbers; guard config_load against a null section target; cap the bone parent walk; check mprotect; drop PROT_EXEC on the GOT patch; absorb server.so .bss tail Correctness: - filter dormant entities across ESP, aimbot, triggerbot, melee, items and projectiles (slot self-tested against the local player) - reset the server-health latch per map and only latch on valid model indices - bone cache sized for the 128-target scan; set valid=false on every failure; head_position keeps searching past an invalid bone - vischeck steps past glass by probing until clear, and ignores entities owned by the target (held weapons, bone followers) - aimbot: maxs no longer disables it, AABB-aware FOV prefilter, lock hysteresis, lead point re-checked for FOV and visibility - spread: seed hook arms on the no-spread toggle alone; engine toggle only controls the RandomFloat zeroing - resolve m_hOwnerEntity/m_hOwner; NPC lead velocity from the server entity - vector_angles pitch range, point_ground normal test, underwater jump+duck no longer skips combat, semi-auto press/release edge for synthetic fire - item/projectile markers and blast rings refresh origin per frame while discovery stays at 10 Hz; trajectory uses the real m_DmgRadius and honours the blast-radius toggle Rendering/GUI: - one overlay draw per engine frame (FRAME_RENDER_START boundary), fixing alpha stacking and the speed-graph flicker - clipped lines/outlines and ellipsised text instead of drawing past the content pane or shifting over neighbours - dropdown dismissal consumes the click; hover ignores hidden controls - chams: reset AlphaModulate, reference shared materials once, restore prop tint colour; fonts created once per handle - projection uses Source's 4:3 aspect scaling and roll; screen size refreshed each frame; FOV circle gated on the aim toggles, not the bind Performance: - TTL caches for NPC relation, friendly/neutral and damage class - model names memoised per model index - entity pointer -> index cache for the triggerbot (was a 4096-slot walk/tick) Build: - real Makefile with -MMD dependency tracking, debug/release objects and an always-relink rule; build.sh kept as a thin wrapper --- src/bones.cpp | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) (limited to 'src/bones.cpp') diff --git a/src/bones.cpp b/src/bones.cpp index c777067..c4168a9 100644 --- a/src/bones.cpp +++ b/src/bones.cpp @@ -100,7 +100,10 @@ struct BoneEntry { Matrix3x4 mats[MAX_BONES]; }; -static BoneEntry g_entries[64]; +// Sized for the 128-target scan with headroom: failed updates also allocate an +// entry, and 64 slots evicted live NPCs mid-frame. +constexpr int BONE_CACHE = 192; +static BoneEntry g_entries[BONE_CACHE]; static int g_next = 0; static int g_logged = 0; static bool g_renderable_warned = false; @@ -112,14 +115,14 @@ static double bone_now() { } static BoneEntry *find_entry(int idx) { - for (int i = 0; i < 64; i++) + for (int i = 0; i < BONE_CACHE; i++) if (g_entries[i].ent && g_entries[i].idx == idx) return &g_entries[i]; return nullptr; } static BoneEntry *alloc_entry(int idx, void *ent) { - BoneEntry *e = &g_entries[g_next++ % 64]; + BoneEntry *e = &g_entries[g_next++ % BONE_CACHE]; memset(e, 0, sizeof(*e)); e->idx = idx; e->ent = ent; @@ -321,13 +324,17 @@ bool bones_update(int idx, void *ent) { const void *expected = vfunc(g_iface.modelinfo, 2)(g_iface.modelinfo, modelindex); - if (!expected) + if (!expected) { + e->valid = false; return false; + } // IClientUnknown::GetClientRenderable = primary vtable slot 6. void *r = vfunc(ent, 6)(ent); - if (!r) + if (!r) { + e->valid = false; return false; + } // Validate the interface before using SetupBones on it. const void *rmodel = vfunc(r, 9)(r); if (rmodel != expected) { @@ -418,9 +425,14 @@ int bone_nearest_core_parent(int idx, int bone) { return -1; int p = bone_at(e->hdr, bone)->parent; if (!e->humanoid) { - while (p >= 0 && p < e->count && !e->bone_valid[p]) + // Same cycle guard as the core path: a malformed model can self-parent a + // bone whose matrix is invalid, which would spin here forever. + for (int guard = 0; p >= 0 && p < e->count && guard < 64; guard++) { + if (e->bone_valid[p]) + return p; p = bone_at(e->hdr, p)->parent; - return (p >= 0 && p < e->count) ? p : -1; + } + return -1; } for (int guard = 0; p >= 0 && p < e->count && guard < 64; guard++) { if (e->core[p] && e->bone_valid[p]) @@ -487,14 +499,16 @@ bool head_position(int idx, Vector &out) { if (boxes[i].bone < 0 || boxes[i].bone >= e->count) continue; const char *bn = bone_name_raw(e->hdr, boxes[i].bone); - if (bn && head_like_name(bn)) - return hitbox_world(e, boxes[i], out); + // Keep looking: the first name match can be a bone whose matrix failed + // validation, and returning its false result aborted the whole search. + if (bn && head_like_name(bn) && hitbox_world(e, boxes[i], out)) + return true; } } for (int i = 0; i < e->count; i++) { const char *bn = bone_name_raw(e->hdr, i); - if (bn && head_like_name(bn)) - return bone_pos(idx, i, out); + if (bn && head_like_name(bn) && bone_pos(idx, i, out)) + return true; } // Last resort for non-humanoid models with unnamed heads/hitboxes: the // first hitbox is the body (still a valid aim point). -- cgit v1.2.3