Skip to content

fix(sql): userid in query callbacks, map-proof DB retries, null guards - #67

Merged
Rushaway merged 1 commit into
masterfrom
fix/sql-audit
Oct 29, 2026
Merged

Rushaway merged 1 commit into
masterfrom
fix/sql-audit

Conversation

@Rushaway

@Rushaway Rushaway commented Oct 28, 2026 •

Copy link
Copy Markdown
Member

Summary

  • Raw client index in query callbacks (SprayManager.sp): the ban, spray-hash-ban and NSFW lookups passed the client index as callback data and the callbacks only checked IsValidClient(client). If the player left and someone else took the slot before the result came back, the other player got the first player's spray ban / hash ban / NSFW status. The queries now pass GetClientUserId(client) and the callbacks resolve it with GetClientOfUserId.
  • DB retries killed by a map change (SprayManager.sp): the reconnect timer (ReconnectSQL) and the table-creation retry (RetryTableCreation) used TIMER_FLAG_NO_MAPCHANGE, and nothing reconnects on map start. A failed connect or table creation shortly before a map change left the plugin without a database (bans not loaded or enforced) until a plugin reload. Both flags are dropped.
  • Null handle on NSFW writes (SprayManager.sp): DB_UpdateSprayNSFWStatus / DB_DeleteSprayNSFWStatus called g_hDatabase.Query without a null check. They are reachable from the admin menu/commands and from the SprayManager_MarkSprayAsNSFW/SFW natives (modules/module_api.inc), so they threw native errors while the DB was down. They now return early (the in-memory status still changes for the session).
  • NSFW lookup skipped (SprayManager.sp): UpdateNSFWInfo silently returned when the DB was not ready yet (late load, reconnect), so the player's NSFW flag was never loaded for the session. It now schedules the existing RetryNSFWSprayLookup timer, like the ban and hash lookups do.
  • Charset (SprayManager.sp, modules/functions.inc): the charset was only set with an async SET NAMES inside the table transaction, which does not survive the MySQL driver's auto-reconnect. OnSQLConnected now calls db.SetCharset("utf8mb4") once per new MySQL connection, before any query is queued. SET NAMES stays only as fallback when SetCharset fails.
  • Version 3.2.12 -> 3.2.13.

Test plan

  • Compiles with spcomp 1.12.0.7259 (same deps as CI)
  • CI build passes
  • Spray-ban a player, rejoin: the ban is loaded. Leave right after joining so another player takes the slot: the new player is not banned.
  • Wrong DB credentials, change map during the 10 s retry, fix credentials: "Successfully connected to mySQL database!" shows up without a plugin reload.
  • MySQL down: mark a spray NSFW/SFW from the menu and through the native: no native error in logs.
  • Late load (sm plugins reload SprayManager) with a player whose spray is in spraynsfwlist: the spray is shown as NSFW after the retry.
  • Player name with emoji in a spray ban: stored correctly, also after a MySQL restart (auto-reconnect).

- Pass the userid instead of the raw client index to the ban / spray hash /
  NSFW lookups, resolved with GetClientOfUserId in the callbacks.
- Reconnect and table-creation retry timers no longer die on map change.
- Guard DB_UpdateSprayNSFWStatus / DB_DeleteSprayNSFWStatus against a null handle.
- Retry the NSFW lookup when the DB isn't ready, like the other two lookups.
- Set the charset with SetCharset once per connection, SET NAMES only as fallback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 28, 2026 14:38

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

@WareZ57 WareZ57 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went through this against current master (it sits on top of #66, merges cleanly, CI green) and compiled it with spcomp 1.12.0.7259 + MultiColors/FixSprayExploit: no errors, no warnings.

What I checked:

  • The three lookups now pass GetClientUserId(client) and resolve it with GetClientOfUserId before touching per-client arrays (SprayManager.sp:1054, :1071, :1088, callbacks at :1106, :1140, :1159). The retry paths in the callbacks still use serials, so nothing gets applied to a reused slot anymore.
  • Dropping TIMER_FLAG_NO_MAPCHANGE from ReconnectSQL / RetryTableCreation (:629, :664) is right. Each chain only has one timer at a time (a failed connect arms one reconnect, a failed table transaction arms one retry) so nothing stacks, and the plugin's timers still die on unload.
  • SetCharset runs on the new handle before it's published to g_hDatabase and before the table transaction is queued (:644-649), so nothing else is using the connection yet. SET NAMES stays as the fallback only when it returns false. SQLite is skipped.
  • The null guards in DB_UpdateSprayNSFWStatus / DB_DeleteSprayNSFWStatus cover all callers (menu :948/:987, commands, and both natives in module_api.inc).

One small edge case, not blocking: with the new NSFW retry in UpdateNSFWInfo (:1076-1080), if the DB is down at startup and an admin marks a player's spray SFW in the meantime, the delete is skipped (:1408) and the in-memory flag goes to false. When the DB comes back, the pending RetryNSFWSprayLookup finds the old spraynsfwlist row and OnSQLCheckNSFWSprayHashQuery sets g_bHasNSFWSpray back to true (:1174). It needs a startup outage plus an admin action during that window, so I'm fine leaving it. If you want to cover it, skip the retry once an admin has changed the status in-session.

LGTM.

@Rushaway
Rushaway merged commit 3420066 into master Oct 29, 2026
7 checks passed
@Rushaway
Rushaway deleted the fix/sql-audit branch October 29, 2026 16:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants