Skip to content

[2.1] Keeps error logging on prepared statements for any error message - #9883

Open
albertlast wants to merge 1 commit into
SimpleMachines:release-2.1from
albertlast:2.1/error-log-prepared-only
Open

albertlast wants to merge 1 commit into
SimpleMachines:release-2.1from
albertlast:2.1/error-log-prepared-only

Conversation

@albertlast

@albertlast albertlast commented Nov 12, 2026 •

Copy link
Copy Markdown
Collaborator

Description

The error log is written through a prepared statement so that error messages, whatever they hold, never pass through SMF's query parser. This keeps it that way and closes the two gaps that still lost errors.

When the statement cannot be prepared, the error is dropped. mysqli_stmt_bind_param() was handed false and threw a TypeError. The error is not sent through $smcFunc['db_insert'] instead. Whatever caused the error may have broken that path too, and its own errors would be logged by calling smf_db_error_insert() again. #9872 does fall back to db_insert, and in testing that fallback breaks on both engines:

  • on MySQL it calls $smcFunc without declaring it global, which is a fatal "Value of type null is not callable";
  • on both engines it passes a null IP, which {inet:...} rejects ("The database value you're trying to insert does not exist: ip"). On PostgreSQL that failure is logged through the same broken path and ends in "Error loop." A null IP is what every error from cron, the CLI or a background task has.

What the statement binds is cleaned first. NUL bytes are removed. On a UTF-8 forum:

  • strings are cut to their column with mb_strcut(), so the cut never ends inside a character;
  • invalid UTF-8 is replaced;
  • on MySQL, 4-byte characters such as emoji go through $smcFunc['fix_utf8mb4'], as they do on the query path. A table that is not utf8mb4 rejects them otherwise.

Other character sets keep a plain substr(). Without this, a message longer than its column, or one holding bytes the database rejects, made the insert fail and the error was lost.

PostgreSQL with pooling prepares the unnamed statement every time, as it did before, since any other query may replace it.

Testing

2.1 has no test suite, so this was checked with a script calling $smcFunc['db_error_insert'] against an installed forum. It used 16 kinds of message (quotes, backslashes, SQL, {db_prefix} and %s placeholders, emoji, newlines, HTML, NUL bytes, invalid UTF-8, empty, and 70 KB cut inside 2-, 3- and 4-byte characters), each under an IPv4, an IPv6 and an invalid IP. The MySQL tables were not utf8mb4.

normal prepare fails
release-2.1, MySQL 30 / 48 stored 0 / 48 stored
release-2.1, PostgreSQL 45 / 48 stored 0 / 48 stored
#9872, MySQL 36 / 48 stored fatal: $smcFunc is null
#9872, PostgreSQL 42 / 48 stored "Error loop."
this PR, MySQL 48 / 48 stored 0 / 48 stored, no error, no loop
this PR, PostgreSQL 48 / 48 stored 0 / 48 stored, no error, no loop

The prepare was made to fail with SET GLOBAL max_prepared_stmt_count = 0 on MySQL, and on PostgreSQL by preparing another statement named smf_log_errors first. The script checked that the prepare really failed before it ran.

Issues References (Fixes|Related|Closes)

  1. Fixes mysqli_prepare can return false #7831
  2. Related [2.1] Handle failed prepare statements and oversized data in error logs #9872, which this offers an alternative to
  3. The same change for 3.0 is [3.0] Keeps error logging on prepared statements for any error message #9882

🤖 Generated with Claude Code

The error log is written only through its prepared statement. When that
cannot be prepared, mysqli_stmt_bind_param() was handed false and threw
a TypeError; the error is now dropped instead of being sent through the
regular query path, which may be what failed and whose own errors would
come straight back here.

What the statement binds is cleaned first. On a UTF-8 forum strings are
cut to their column on a character boundary, invalid UTF-8 is replaced
and, on MySQL, 4-byte characters are encoded the way the query path
does for tables that are not utf8mb4. NUL bytes are removed. Without
that a long message, or one holding bytes the database rejects, made
the insert fail and the error was lost.

PostgreSQL with pooling prepares the unnamed statement every time, since
any other query may replace it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
@jdarwood007

Copy link
Copy Markdown
Member

We already have $smcFunc['substr'], which should be handling the work here and shouldn't need to redo it again.

@albertlast

Copy link
Copy Markdown
Collaborator Author

from where you know that this function exists in the time of the error?

@jdarwood007

Copy link
Copy Markdown
Member

Its defined in Load.php during a reloadSettings. So the first thing that SMF calls on any startup. If it doesn't exist, the forum was not loaded properly.

@albertlast

Copy link
Copy Markdown
Collaborator Author

so we are in a error state, provide in this most usable information in this moment instead of "no loaded proplerly" message.

@jdarwood007 jdarwood007 added this to the 2.1.10 milestone Nov 13, 2026
@albertlast

Copy link
Copy Markdown
Collaborator Author

On $smcFunc['substr']: even when it is loaded, it does not do the job here. It counts characters, and the columns are limited in bytes. message, url and backtrace are TEXT, which on MySQL is 65,535 bytes, so 65534 characters of a 3-byte script is about 196 KB and the insert fails. It also splits the string with preg_split(…/u), which returns false for invalid UTF-8, and array_slice() then throws a TypeError inside the error logger.

Both show up in #9872, which uses it, against its current head on MySQL. Long 2-byte, 3-byte and emoji messages were not stored, and invalid UTF-8 threw TypeError: array_slice(): Argument #1 ($array) must be of type array, bool given. Full results: #9872 (comment).

mb_strcut() cuts to a size in bytes without splitting a character, and needs nothing from reloadSettings(). This PR stores all 48 cases on both engines.

@jdarwood007

Copy link
Copy Markdown
Member

I don't think 2.1 required mb to be loaded, I can't recall right now. So I don't know if we can rely on it, but it should explain the hacky logic in the $smcFunc. I think we have to take the L here because of it and the low chance of having many multi byte characters in a error message.

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.

2 participants