Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions src/action/a_game.c
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
5 changes: 4 additions & 1 deletion src/action/a_match.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
16 changes: 12 additions & 4 deletions src/action/p_client.c
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down Expand Up @@ -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);
Expand Down
10 changes: 8 additions & 2 deletions src/server/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down