You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
css_unban / css_unmute silently fail on SQLite — every caller path
TL;DR
On SQLite (the default Database.Type in the shipped config), css_unban, !unban, css_unmute, css_ungag, css_unsilence all print their cosmetic success message to the admin but never write to the database. The ban/mute row stays status='ACTIVE', no row is inserted into sa_unbans / sa_unmutes, and the player remains rejected on reconnect. The exception is swallowed by a bare catch { }, so nothing surfaces in any log.
MySQL users do not see this because MySqlConnector materialises an INT column as int; SQLite returns INTEGER PRIMARY KEY AUTOINCREMENT as long, and the C# dynamic binder refuses the implicit long→int conversion at runtime.
Reproduced and root-caused against 1.7.9a (eea700b). Fix is two one-line changes; PR follows.
Windows dedicated server (Windows 11), but the bug is platform-independent — the root cause is in the C# dynamic binding semantics, not in any native code.
Then issue css_unban 76561198000000001 from the server console.
Observed
Console issued command `css_unban 76561198000000001` on server `<name>`
Unbanned player with pattern 76561198000000001.
Expected
sa_bans.id=<row> flips to status='UNBANNED', unban_id populated.
sa_unbans gets a new row with matching ban_id.
The player can reconnect.
Actual
sa_bans.id=<row>.status stays 'ACTIVE'.
sa_unbans stays empty.
The player is still rejected with Your client is not allowed to join this server.
No exception in any log (eaten by catch { }).
Caller-format matrix — all four broken identically
Caller
Argument format
Result
Server console
css_unban <SteamID64>
cosmetic success, DB unchanged
Server console
css_unban <IPv4>
cosmetic success, DB unchanged
Server console
css_unban <player_name substring>
cosmetic success, DB unchanged
In-game chat, admin with @css/unban
!unban <SteamID64>
cosmetic success, DB unchanged
css_unban <ban_id> (e.g. css_unban 4) is correctly rejected with Too short pattern to search. — css_unban is a substring lookup with Length > 1, not an id-based command. That part is by design.
Root cause
After temporarily replacing the bare catch { } at Managers/BanManager.cs::UnbanPlayer with catch (Exception ex) { CS2_SimpleAdmin.Instance?.Logger?.LogError(ex, "UnbanPlayer failed for pattern {Pattern}", playerPattern); }, the SA log immediately surfaces:
[EROR] (plugin:CS2-SimpleAdmin (RELEASE)) UnbanPlayer failed for pattern 76561198000000001
Microsoft.CSharp.RuntimeBinder.RuntimeBinderException:
Cannot implicitly convert type 'long' to 'int'.
An explicit conversion exists (are you missing a cast?)
at CallSite.Target(Closure, CallSite, Object)
at System.Dynamic.UpdateDelegates.UpdateAndExecute1[T0,TRet](CallSite site, T0 arg0)
at CS2_SimpleAdmin.Managers.BanManager.UnbanPlayer(String playerPattern, String adminSteamId, String reason)
bansList comes from connection.QueryAsync(...) typed as IEnumerable<dynamic>. On SQLite, the id column is INTEGER PRIMARY KEY AUTOINCREMENT; the SQLite native layer hands every INTEGER value back as a 64-bit integer, and Microsoft.Data.Sqlite/Dapper box it as long. The C# dynamic binder's runtime conversion path rejects the implicit long→int narrowing and throws RuntimeBinderException BEFORE either the INSERT into sa_unbans or the UPDATE on sa_bans runs. The exception is then caught and discarded by the surrounding catch { } at line 343.
MySQL users do not see this because MySqlConnector materialises a SQL INT column as a CLR int, so the implicit conversion succeeds and the loop completes normally.
Two anti-patterns that make this worse than it had to be
Commands/basebans.cs::OnUnbanCommand calls the cosmetic ReplyToCommand($"Unbanned player with pattern {pattern}.") synchronously, then fires the actual DB work into a fire-and-forget Task.Run(...). The admin sees success at the same instant the work starts, with no propagation path for failures. (Same shape used for OnUnwarnCommand, OnWarnCommand, and several others throughout the file.)
Managers/BanManager.cs::UnbanPlayer ends its try block with a bare catch { } that discards every exception type without any logging. (The try-blocks in BanPlayer, AddBanBySteamid, AddBanByIp all use catch (Exception ex) { CS2_SimpleAdmin._logger?.LogError(...); }, so the inconsistency is purely accidental, not by design.) Same pattern in Managers/MuteManager.cs::UnmutePlayer, where the catch does Console.WriteLine(ex) — which writes to the CS2 console scrollback but never makes it into the rolling SA log file.
Together these guarantee that the bug looks like nothing went wrong, from the admin chat reply down to every log file.
Identical bug, second site
Managers/MuteManager.cs::UnmutePlayer line 266 has the same int muteId = mute.id; pattern and throws the same RuntimeBinderException on SQLite. That means css_unmute, css_ungag, css_unsilence, and their !-aliases all silently fail on SQLite too, for exactly the same reason.
I did not separately repro the mute path because the root cause is identical; mentioning it here so the fix covers both sites in one PR.
Suggested fix (PR follows)
Normalise the conversion path to be provider-agnostic:
The (object) cast bypasses the dynamic binder so Convert.ToInt32 resolves the well-defined object overload, which handles both int (MySQL) and long (SQLite) without any runtime binding surprises.
I'd also recommend tightening the catch in UnbanPlayer to match the rest of the file (log the exception via CS2_SimpleAdmin.Instance.Logger). My PR includes that one extra hunk; happy to drop it if you prefer to keep the diff minimal.
Testing notes
SQLite: verified manually against the synthetic ban above. Pre-fix: sa_bans.status='ACTIVE', sa_unbans empty, RuntimeBinderException in SA log. Post-fix: sa_bans.status='UNBANNED', sa_bans.unban_id populated, sa_unbans row inserted with matching ban_id, no exception. Server: Windows 11 dedicated, CS2 latest, CSS# current.
MySQL: not retested by me. The fix preserves the existing MySQL code path semantically — Convert.ToInt32((object)x) on a boxed int returns the same int. Both managers compile clean against net8.0 Release without warnings.
Related prior reports
I searched the tracker before filing this; no prior report names the actual root cause (RuntimeBinderException, long→int, Dapper-on-SQLite), but a few existing issues are almost certainly the same family observed from different surfaces:
css_unban not working #229 (closed) — "css_unban not working" by @dueruem. SQLite, exact same symptom (css_unban does nothing). Reported in November 2024, closed without a code-level diagnosis. The conversation suggested workarounds ("update plugin", "disable multiacc check", "don't use latest release if you don't need sqlite support") rather than a fix. This PR/issue is the root cause for that report.
[BUG?] css_unmute, css_ungag, css_unsilence #142 (closed) — "[BUG?] css_unmute, css_ungag, css_unsilence". Closed as a usage question ("Anything you type is looked up in the database — so you have to enter the exact player nickname or steamid"). With this bug present, even an exact-SteamID lookup against SQLite fails silently for exactly the same reason as css_unban — the user wasn't wrong, the targeting reached the DB but the dynamic binder threw before the write. Almost certainly the same bug, observed on the mute side.
OverflowException in CacheManager.InitializeCacheAsync — sa_players_ips.steamid mapped to int (SteamID64 doesn't fit) #283 (open) — "OverflowException in CacheManager.InitializeCacheAsync — sa_players_ips.steamid mapped to int (SteamID64 doesn't fit)". Different surface, same family: Dapper materialising a SQLite 64-bit integer column into a CLR int target. Not fixed by this PR, but worth pulling forward as a related cleanup — both are symptoms of an implicit "SQLite gives you int-width values" assumption that doesn't hold.
Happy to fold this into a comment on #229 instead of standing up a separate issue if that's preferred; this report exists as a new issue because the root cause is materially new information and the proposed fix touches a second code path (MuteManager) that #229 doesn't mention.
css_unban/css_unmutesilently fail on SQLite — every caller pathTL;DR
On SQLite (the default
Database.Typein the shipped config),css_unban,!unban,css_unmute,css_ungag,css_unsilenceall print their cosmetic success message to the admin but never write to the database. The ban/mute row staysstatus='ACTIVE', no row is inserted intosa_unbans/sa_unmutes, and the player remains rejected on reconnect. The exception is swallowed by a barecatch { }, so nothing surfaces in any log.MySQL users do not see this because
MySqlConnectormaterialises anINTcolumn asint; SQLite returnsINTEGER PRIMARY KEY AUTOINCREMENTaslong, and the C#dynamicbinder refuses the implicitlong→intconversion at runtime.Reproduced and root-caused against
1.7.9a(eea700b). Fix is two one-line changes; PR follows.Environment
build-1.7.9a(commiteea700b)Database.Type = "sqlite", filecs2-simpleadmin.sqlite)Reproduction
Plant a synthetic
ACTIVEban directly insa_bans:Then issue
css_unban 76561198000000001from the server console.Observed
Expected
sa_bans.id=<row>flips tostatus='UNBANNED',unban_idpopulated.sa_unbansgets a new row with matchingban_id.Actual
sa_bans.id=<row>.statusstays'ACTIVE'.sa_unbansstays empty.Your client is not allowed to join this server.catch { }).Caller-format matrix — all four broken identically
css_unban <SteamID64>css_unban <IPv4>css_unban <player_name substring>@css/unban!unban <SteamID64>css_unban <ban_id>(e.g.css_unban 4) is correctly rejected withToo short pattern to search.—css_unbanis a substring lookup withLength > 1, not an id-based command. That part is by design.Root cause
After temporarily replacing the bare
catch { }atManagers/BanManager.cs::UnbanPlayerwithcatch (Exception ex) { CS2_SimpleAdmin.Instance?.Logger?.LogError(ex, "UnbanPlayer failed for pattern {Pattern}", playerPattern); }, the SA log immediately surfaces:The offending line is
Managers/BanManager.cs:334:bansListcomes fromconnection.QueryAsync(...)typed asIEnumerable<dynamic>. On SQLite, theidcolumn isINTEGER PRIMARY KEY AUTOINCREMENT; the SQLite native layer hands everyINTEGERvalue back as a 64-bit integer, andMicrosoft.Data.Sqlite/Dapper box it aslong. The C#dynamicbinder's runtime conversion path rejects the implicitlong→intnarrowing and throwsRuntimeBinderExceptionBEFORE either theINSERTintosa_unbansor theUPDATEonsa_bansruns. The exception is then caught and discarded by the surroundingcatch { }at line 343.MySQL users do not see this because
MySqlConnectormaterialises a SQLINTcolumn as a CLRint, so the implicit conversion succeeds and the loop completes normally.Two anti-patterns that make this worse than it had to be
Commands/basebans.cs::OnUnbanCommandcalls the cosmeticReplyToCommand($"Unbanned player with pattern {pattern}.")synchronously, then fires the actual DB work into a fire-and-forgetTask.Run(...). The admin sees success at the same instant the work starts, with no propagation path for failures. (Same shape used forOnUnwarnCommand,OnWarnCommand, and several others throughout the file.)Managers/BanManager.cs::UnbanPlayerends itstryblock with a barecatch { }that discards every exception type without any logging. (Thetry-blocks inBanPlayer,AddBanBySteamid,AddBanByIpall usecatch (Exception ex) { CS2_SimpleAdmin._logger?.LogError(...); }, so the inconsistency is purely accidental, not by design.) Same pattern inManagers/MuteManager.cs::UnmutePlayer, where thecatchdoesConsole.WriteLine(ex)— which writes to the CS2 console scrollback but never makes it into the rolling SA log file.Together these guarantee that the bug looks like nothing went wrong, from the admin chat reply down to every log file.
Identical bug, second site
Managers/MuteManager.cs::UnmutePlayerline 266 has the sameint muteId = mute.id;pattern and throws the sameRuntimeBinderExceptionon SQLite. That meanscss_unmute,css_ungag,css_unsilence, and their!-aliases all silently fail on SQLite too, for exactly the same reason.I did not separately repro the mute path because the root cause is identical; mentioning it here so the fix covers both sites in one PR.
Suggested fix (PR follows)
Normalise the conversion path to be provider-agnostic:
The
(object)cast bypasses thedynamicbinder soConvert.ToInt32resolves the well-definedobjectoverload, which handles bothint(MySQL) andlong(SQLite) without any runtime binding surprises.I'd also recommend tightening the
catchinUnbanPlayerto match the rest of the file (log the exception viaCS2_SimpleAdmin.Instance.Logger). My PR includes that one extra hunk; happy to drop it if you prefer to keep the diff minimal.Testing notes
sa_bans.status='ACTIVE',sa_unbansempty,RuntimeBinderExceptionin SA log. Post-fix:sa_bans.status='UNBANNED',sa_bans.unban_idpopulated,sa_unbansrow inserted with matchingban_id, no exception. Server: Windows 11 dedicated, CS2 latest, CSS# current.Convert.ToInt32((object)x)on a boxedintreturns the sameint. Both managers compile clean againstnet8.0Release without warnings.Related prior reports
I searched the tracker before filing this; no prior report names the actual root cause (
RuntimeBinderException,long→int, Dapper-on-SQLite), but a few existing issues are almost certainly the same family observed from different surfaces:css_unbandoes nothing). Reported in November 2024, closed without a code-level diagnosis. The conversation suggested workarounds ("update plugin", "disable multiacc check", "don't use latest release if you don't need sqlite support") rather than a fix. This PR/issue is the root cause for that report.css_unban— the user wasn't wrong, the targeting reached the DB but the dynamic binder threw before the write. Almost certainly the same bug, observed on the mute side.inttarget. Not fixed by this PR, but worth pulling forward as a related cleanup — both are symptoms of an implicit "SQLite gives youint-width values" assumption that doesn't hold.OverflowExceptionreports againstCacheManager.InitializeCacheAsync, same root-pattern family. Mentioning for context, not as duplicates of this report.Happy to fold this into a comment on #229 instead of standing up a separate issue if that's preferred; this report exists as a new issue because the root cause is materially new information and the proposed fix touches a second code path (
MuteManager) that #229 doesn't mention.