From e4700e9448d23e276e05c70c58922b4559c243b3 Mon Sep 17 00:00:00 2001 From: tommy Date: Sun, 19 Apr 2026 10:26:57 -0400 Subject: [PATCH 1/2] Close bulk re-entry window gB_IsProcessingBatches was only flipped to true once the async SQL result came back, leaving a gap between Command_SendAllWRs and SQL_GetRecords_Callback where a second osdb_get_all_wrs could slip past the guard. Now the flag is set immediately on command entry and explicitly cleared on every failure path in the chain. --- .../scripting/include/offstyledb_bulk.inc | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/addons/sourcemod/scripting/include/offstyledb_bulk.inc b/addons/sourcemod/scripting/include/offstyledb_bulk.inc index 0397d5c..11b1681 100644 --- a/addons/sourcemod/scripting/include/offstyledb_bulk.inc +++ b/addons/sourcemod/scripting/include/offstyledb_bulk.inc @@ -17,12 +17,15 @@ public Action Command_SendAllWRs(int client, int args) if (gB_IsProcessingBatches) { ReplyToCommand(client, "[OSdb] Already processing batches. Please wait for current operation to complete."); - DebugPrint("Bulk operation request denied - already processing batches"); return Plugin_Handled; } + // Flag immediately so a second osdb_get_all_wrs call doesn't slip through + // the guard while /bulk_verification and the SQL query are in flight. + // Every failure path below clears this back to false. + gB_IsProcessingBatches = true; + ReplyToCommand(client, "[OSdb] Requesting bulk verification..."); - DebugPrint("Starting bulk verification request"); RequestBulkVerification(); return Plugin_Handled; @@ -71,19 +74,17 @@ public void Callback_OnBulkVerification(HTTPResponse resp, any value) if (resp.Status != HTTPStatus_OK || resp.Data == null) { LogError("[OSdb] Bulk verification failed: status = %d, data = null", resp.Status); - DebugPrint("Bulk verification failed with status %d", resp.Status); + gB_IsProcessingBatches = false; return; } - DebugPrint("Bulk verification response received successfully"); - JSONObject data = view_as(resp.Data); if (!data.HasKey("key_to_send_times")) { LogError("[OSdb] Bulk verification response missing 'key_to_send_times' field"); - DebugPrint("Bulk verification response missing required field"); delete data; + gB_IsProcessingBatches = false; return; } @@ -106,7 +107,7 @@ void SendRecordDatabase() if (bulkMode == -1) { PrintToServer("[OSdb] Bulk upload disabled (mode = -1), skipping record collection"); - DebugPrint("Bulk upload skipped due to mode = -1"); + gB_IsProcessingBatches = false; return; } @@ -143,16 +144,17 @@ public void SQL_GetRecords_Callback(Database db, DBResultSet results, const char if (results == null) { LogError("[OSdb] SQL_GetRecords_Callback results are null, SQL error: %s", error); + gB_IsProcessingBatches = false; return; } if (results.RowCount == 0) { LogError("[OSdb] SQL_GetRecords_Callback rowcount is 0"); + gB_IsProcessingBatches = false; return; } - gB_IsProcessingBatches = true; gI_CurrentBatch = 0; gI_TotalRecords = 0; From 004c754ea6d4646cd08d6e9302eb860eb8b92c91 Mon Sep 17 00:00:00 2001 From: tommy Date: Sun, 19 Apr 2026 10:30:23 -0400 Subject: [PATCH 2/2] Add TODOs for known remaining issues 100MB pragma, auth-key blanking trick, bulk re-entry race, double file read in style mapping, missing retry on mapping fetch failure --- addons/sourcemod/scripting/include/offstyledb.inc | 7 +++++++ addons/sourcemod/scripting/include/offstyledb_records.inc | 5 +++++ addons/sourcemod/scripting/include/offstyledb_styles.inc | 6 ++++++ 3 files changed, 18 insertions(+) diff --git a/addons/sourcemod/scripting/include/offstyledb.inc b/addons/sourcemod/scripting/include/offstyledb.inc index 8d04988..5823fde 100644 --- a/addons/sourcemod/scripting/include/offstyledb.inc +++ b/addons/sourcemod/scripting/include/offstyledb.inc @@ -11,6 +11,8 @@ #include +// TODO: 100MB is reserved unconditionally for bulk JSON payloads. If bulk +// ever switches to streaming/paging, shrink this. #pragma dynamic 104857600 #pragma newdecls required #pragma semicolon 1 @@ -173,6 +175,11 @@ public void OnAllPluginsLoaded() public void OnConfigsExecuted() { + // TODO: we read the key once and blank the ConVar so it doesn't leak via + // sm_cvar. The strlen guard makes a plugin reload re-read correctly + // (since we blanked the ConVar after), but the interaction is subtle - + // worth replacing with a proper secret-loading path (env var / sealed + // file) if one appears. if (strlen(gS_AuthKey) == 0) { gCV_Authentication.GetString(gS_AuthKey, sizeof(gS_AuthKey)); diff --git a/addons/sourcemod/scripting/include/offstyledb_records.inc b/addons/sourcemod/scripting/include/offstyledb_records.inc index 3611c58..40e5963 100644 --- a/addons/sourcemod/scripting/include/offstyledb_records.inc +++ b/addons/sourcemod/scripting/include/offstyledb_records.inc @@ -91,6 +91,11 @@ void Records_HandleFinish(int client, int style, float time, int jumps, int stra return; } + // TODO: overwrite race - if player A stashed an intent, disconnected, + // player B took the slot and finished all before A's replay save fired, + // A's replay would dispatch B's intent. Requires B to complete a full + // map run in <100ms so practically impossible, but a steamid check + // before dispatch would close it. gPending[client] = intent; DebugPrint("Stashed intent for client %d (isWR=%d)", client, isWR); } diff --git a/addons/sourcemod/scripting/include/offstyledb_styles.inc b/addons/sourcemod/scripting/include/offstyledb_styles.inc index 834622f..728e6ab 100644 --- a/addons/sourcemod/scripting/include/offstyledb_styles.inc +++ b/addons/sourcemod/scripting/include/offstyledb_styles.inc @@ -6,6 +6,9 @@ #pragma newdecls required #pragma semicolon 1 +// TODO: shavit-styles.cfg is opened twice per call - once for HashStyleConfig +// and again below for the base64-encode + POST. Read once, hash the buffer, +// encode the buffer. void GetStyleMapping(bool forceRefresh = false) { DebugPrint("Starting style mapping request (forceRefresh: %s)", forceRefresh ? "true" : "false"); @@ -131,6 +134,9 @@ public void Callback_OnStyleMapping(HTTPResponse resp, any value) if (resp.Status != HTTPStatus_OK || resp.Data == null) { + // TODO: retry on a short backoff instead of waiting for the next map + // change. If a map lasts hours, style submissions stay broken the + // whole time. LogError("[OSdb] Style mapping fetch failed (status=%d). Keeping any existing mapping; will retry on next map change.", resp.Status); // Invalidate the cached hash so the next OnMapEnd tries again even if // shavit-styles.cfg didn't change in between.