From 517a7fb552c2e4442a75bd7372b3dd20f3953c75 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sat, 23 May 2026 19:29:04 -0400 Subject: [PATCH 1/3] fix(matchmode): teamnone arg check and captain-disconnect broadcast (#316 FIX-10, FIX-11) FIX-10: Cmd_Teamnone_f guarded missing-arg with `gi.argc() < 1`. argc always returns at least 1 (command name itself) so the check was dead code; missing arg fell through to atoi("")=0, which either failed "Player 0 not found" or worse silently kicked the client at index 0. Change to `< 2`. FIX-11: The esp+matchmode captain-disconnect broadcast in ClientDisconnect fired for every disconnect, not just captains. For a teamless disconnector it read teams[NOTEAM].name (unused slot). Guard with IS_CAPTAIN(ent) and explicit NOTEAM check. Refs #316 --- src/action/a_match.c | 5 ++++- src/action/p_client.c | 16 ++++++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/action/a_match.c b/src/action/a_match.c index 1ba9adb27..d5d71c362 100644 --- a/src/action/a_match.c +++ b/src/action/a_match.c @@ -656,7 +656,10 @@ void Cmd_Teamnone_f(edict_t *ent) return; } - if (gi.argc() < 1) { + /* gi.argc() always returns at least 1 (the command name itself), so the + * previous `< 1` guard was dead code — missing-arg silently fell through + * with playernum=0 from atoi(""). */ + if (gi.argc() < 2) { gi.cprintf(ent, PRINT_HIGH, "You need to provide a playernum for this command\nUse 'playerlist' to get a list of playernums\n"); return; } diff --git a/src/action/p_client.c b/src/action/p_client.c index df182179f..1b27a2724 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -3849,11 +3849,14 @@ qboolean ClientConnect(edict_t * ent, char *userinfo) IRC_printf(IRC_T_SERVER, "%n@%s connected", value, ipaddr_buf); } - // LRCON: Check if reconnecting claimer and restore claim + // LRCON: Check if reconnecting claimer and restore claim. + // Use Q_stricmp to match Lrcon_CheckClaimer's case-insensitive compare; + // otherwise a claimer reconnecting as "admin" (was "Admin") passes the + // permission check at command time but silently fails restore here. value = Info_ValueForKey(userinfo, "name"); if (game.lrcon_config.enabled && lrcon_claimer_name->string && *lrcon_claimer_name->string && - !strcmp(lrcon_claimer_name->string, value) && - !strcmp(lrcon_claimer_ip->string, ipaddr_buf)) { + !Q_stricmp(lrcon_claimer_name->string, value) && + !Q_stricmp(lrcon_claimer_ip->string, ipaddr_buf)) { level.lrcon.claimed = true; Q_strncpyz(level.lrcon.claimer_name, lrcon_claimer_name->string, sizeof(level.lrcon.claimer_name)); @@ -3913,7 +3916,12 @@ void ClientDisconnect(edict_t * ent) if (!ent->client) return; - if (esp->value && matchmode->value) { + /* Only fire the captain-disconnect broadcast for actual captains. + * Previously this ran for every disconnect from an esp+matchmode game, + * including spectators (resp.team == NOTEAM), which would read + * teams[NOTEAM].name — empty/stale slot. */ + if (esp->value && matchmode->value && + ent->client->resp.team != NOTEAM && IS_CAPTAIN(ent)) { char tempmsg[128]; // We have to kill him first before he is removed as captain/leader killPlayer(ent, false); From a734b593ec128b1471d30a4972ec65c910b3abdd Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sat, 23 May 2026 19:58:10 -0400 Subject: [PATCH 2/3] fix(lrcon): consistent comparator and config-path validation (#316 FIX-13, FIX-14) FIX-13: ClientBegin reconnect-restore used strcmp (case-sensitive) for claimer match; Lrcon_CheckClaimer uses Q_stricmp (case-insensitive). A claimer named "Admin" reconnecting as "admin" passed the permission check at command time but silently failed to auto-restore the claim on reconnect. Use Q_stricmp consistently in both paths. FIX-14: ReadLrconConfig used unbounded sprintf and accepted any lrcon_config cvar value, allowing path traversal (lrcon_config "../../../etc/crontab" would open arbitrary paths). Replace with Q_snprintf and reject any value containing "..", "/", "\\", or ":" before use. Refs #316 --- src/action/a_game.c | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/action/a_game.c b/src/action/a_game.c index 64aeff1db..69775c04f 100644 --- a/src/action/a_game.c +++ b/src/action/a_game.c @@ -1660,12 +1660,21 @@ void ReadLrconConfig(void) game.lrcon_config.allowed_cvars_count = 0; game.lrcon_config.modes_count = 0; - // Get config filename from cvar + // Get config filename from cvar. + // Validate value: must be a plain filename within the action/ directory. + // Without this, lrcon_config "../../../etc/crontab" would open arbitrary + // filesystem paths. Also replaces unbounded sprintf with Q_snprintf. lrcon_config_cvar = gi.cvar("lrcon_config", "lrcon.cfg", 0); - if (lrcon_config_cvar->string && *(lrcon_config_cvar->string)) - sprintf(cfgpath, "%s/%s", GAMEVERSION, lrcon_config_cvar->string); - else - sprintf(cfgpath, "%s/%s", GAMEVERSION, "lrcon.cfg"); + { + const char *name = (lrcon_config_cvar->string && *lrcon_config_cvar->string) + ? lrcon_config_cvar->string : "lrcon.cfg"; + if (strstr(name, "..") || strchr(name, '/') || strchr(name, '\\') || + strchr(name, ':')) { + gi.dprintf("LRCON: refusing lrcon_config '%s' — must be a plain filename within action/\n", name); + return; + } + Q_snprintf(cfgpath, sizeof(cfgpath), "%s/%s", GAMEVERSION, name); + } // Try to open config file config_file = fopen(cfgpath, "r"); From 61bdd3e13df523d63370a272d34616848ca66b35 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sat, 23 May 2026 19:58:47 -0400 Subject: [PATCH 3/3] fix(server): build RulesExt cache on first use (#316 FIX-12) The SVC_RulesExt staleness check `svs.realtime - timestamp > CACHE_TIME` is false at startup when both values are zero, so the cache never gets built. chunk_count stays at 0 and all chunk requests return empty. Metadata responses report chunk_count=0, total_size=0. This persisted until svs.realtime exceeded RULESEXT_CACHE_TIME (~5s). Server-browser tools querying a freshly-started server got empty Rules responses for the first 5 seconds of uptime. Mirror the SVC_StatusExt pattern: also build when chunk_count == 0. Refs #316 --- src/server/main.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/server/main.c b/src/server/main.c index 8031a6bd0..3e35abce8 100644 --- a/src/server/main.c +++ b/src/server/main.c @@ -914,8 +914,14 @@ static void SVC_RulesExt(void) return; } - // Check if we need to rebuild the cache - if (svs.realtime - rulesext_cache.timestamp > RULESEXT_CACHE_TIME) { + // Check if we need to rebuild the cache. + // Force a build on first use (chunk_count == 0): otherwise during the + // first RULESEXT_CACHE_TIME (~5s) of uptime, svs.realtime and timestamp + // are both 0 so the staleness check is false and the cache stays empty, + // leaving all chunk replies empty until the threshold passes. + // Mirrors the SVC_StatusExt build-condition. + if (!rulesext_cache.chunk_count || + svs.realtime - rulesext_cache.timestamp > RULESEXT_CACHE_TIME) { len = SV_BuildExtendedRules(rulesext_cache.data, sizeof(rulesext_cache.data)); rulesext_cache.total_size = len; rulesext_cache.chunk_count = (len + RULESEXT_CHUNK_SIZE - 1) / RULESEXT_CHUNK_SIZE;