Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Nested BINLOG events can still mutate the retained format-description cache before rejection.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
This PR hardens BINLOG execution in stored-program contexts and fixes prepared-statement arena handling.
Changes:
- Defers nested replication cleanup.
- Adds stored-program event checks and PS arena isolation.
- Adds regression coverage.
File summaries
| File | Summary |
|---|---|
sql/rpl_rli.cc |
Defers transaction and table cleanup in sub-statements. |
sql/log_event_server.cc |
Guards nested event execution and PS parsing. |
mysql-test/suite/binlog/t/binlog_base64_flag.test |
Adds regression scenarios. |
mysql-test/suite/binlog/r/binlog_base64_flag.result |
Records expected results. |
Review details
- Files reviewed: 4/4 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
34eda4a to
65b9f94
Compare
There was a problem hiding this comment.
🟡 Changes recommended
The rejection occurs after validation has already mutated the retained format-description state.
Get a fresh assessment by requesting another Copilot review.
Review details
- Files reviewed: 5/5 changed files
- Comments generated: 1
- Review effort level: Balanced
65b9f94 to
1c80fa2
Compare
|
|
… failed.
slave_close_thread_tables() unconditionally called
trans_commit_stmt()/trans_rollback_stmt(), which assert
!thd->in_sub_stmt. A BINLOG statement with malformed base64 payload
executed from an AFTER INSERT trigger hits this: on decode failure,
mysql_client_binlog_statement() sets thd->is_error() and calls
slave_close_thread_tables() from within the trigger's sub-statement.
Guard it with spcont/in_sub_stmt, deferring cleanup to the
enclosing top-level statement. Other callers run only from the
top-level SQL slave applier thread, so this doesn't change their
behavior.
1. BINLOG statement executed from a trigger, SF or SP is disabled
by the patch:
Row events (Rows_log_event) open their target table via a
one-shot check that only fires at the top of a fresh statement.
Inside a trigger, the table is never opened this way. It may be
fixed by reusing query_tables, but:
- find_locked_table() matched only by table name -- could return
the TABLE instance the enclosing statement was actively writing
through, not an idle one. Reusing it would require pre-saving its
state: record[0]/bitmaps/handler/etc. (will deprecate
ER_CANT_UPDATE_USED_TABLE_IN_SF_OR_TRG)
- set_stmt_row_injection()/set_time() calls mutated thd->lex, which
at that point is main_lex -- shared with the enclosing statement,
not something safe to touch.
Statement events (Query_log_event) run the embedded query via
mysql_parse(): it bundles lex_start(), reset_for_next_command() and
parse_sql() as one unit meant for a genuinely new top-level statement,
not a one-off nested parse.
Calling parse_sql() directly instead avoids that, but then we own
everything mysql_parse() was doing for us: a private LEX and a
private Query_arena (or allocations land on main_lex/whatever arena
is currently active, shared with the enclosing statement), plus
calling mysql_execute_command() ourselves afterwards.
In any case, DML for query_tables cannot be done due to
ER_CANT_UPDATE_USED_TABLE_IN_SF_OR_TRG reasons explained above.
2. PS for BINLOG statement still works and needs a leak fix for
statement events:
A BINLOG statement decoding to a Query_log_event, executed via
PREPARE/EXECUTE, leaked its nested query's allocations onto the PS's
own persistent arena: thd->stmt_arena pointed at it while mysql_parse()
ran the decoded query. Second EXECUTE asserted on ROOT_FLAG_READ_ONLY,
since PROTECT_STATEMENT_MEMROOT marks that arena read-only after a
successful execution.
The fix redirects thd->stmt_arena to thd itself for the duration of
the nested mysql_parse() call, so stmt_arena->is_conventional() reads
true and activate_stmt_arena_if_needed() (called e.g. from
save_leaf_tables()) never redirects allocations to the PS's arena in
the first place.
Harmless for what it's protecting: leaf_tables_exec is normally cached
on the persistent arena so a repeatedly-executed statement's
SELECT_LEX doesn't rebuild it every time, but our SELECT_LEX is torn
down and reparsed fresh (due to mysql_parse() semantics) on every
EXECUTE, so there's nothing to cache here regardless of which arena is
used.
ee4c250 to
4d95881
Compare
slave_close_thread_tables()unconditionally calledtrans_commit_stmt()/trans_rollback_stmt(), which assert!thd->in_sub_stmt. ABINLOGstatement with malformed base64 payload executed from anAFTER INSERTtrigger hits this: on decode failure,mysql_client_binlog_statement()setsthd->is_error()and callsslave_close_thread_tables()from within the trigger's sub-statement.Guard it with
spcont/in_sub_stmt, deferring cleanup to the enclosing top-level statement. Other callers run only from the top-level SQL slave applier thread, so this doesn't change their behavior.1.
BINLOGstatement executed from a trigger, SF or SP is disabled by the patchRow events (
Rows_log_event) open their target table via a one-shot check that only fires at the top of a fresh statement. Inside a trigger, the table is never opened this way. It may be fixed by reusingquery_tables, but:find_locked_table()matched only by table name -- could return theTABLEinstance the enclosing statement was actively writing through, not an idle one. Reusing it would require pre-saving its state:record[0]/bitmaps/handler/etc. (will deprecateER_CANT_UPDATE_USED_TABLE_IN_SF_OR_TRG)set_stmt_row_injection()/set_time()calls mutatedthd->lex, which at that point ismain_lex-- shared with the enclosing statement, not something safe to touch.Statement events (
Query_log_event) run the embedded query viamysql_parse(): it bundleslex_start(),reset_for_next_command()andparse_sql()as one unit meant for a genuinely new top-level statement, not a one-off nested parse.Calling
parse_sql()directly instead avoids that, but then we own everythingmysql_parse()was doing for us: a privateLEXand a privateQuery_arena(or allocations land onmain_lex/whatever arena is currently active, shared with the enclosing statement), plus callingmysql_execute_command()ourselves afterwards.In any case, DML for
query_tablescannot be done due toER_CANT_UPDATE_USED_TABLE_IN_SF_OR_TRGreasons explained above.2. PS for
BINLOGstatement still works and needs a leak fix for statement eventsA
BINLOGstatement decoding to aQuery_log_event, executed viaPREPARE/EXECUTE, leaked its nested query's allocations onto the PS's own persistent arena:thd->stmt_arenapointed at it whilemysql_parse()ran the decoded query. SecondEXECUTEasserted onROOT_FLAG_READ_ONLY, sincePROTECT_STATEMENT_MEMROOTmarks that arena read-only after a successful execution.The fix redirects
thd->stmt_arenatothditself for the duration of the nestedmysql_parse()call, sostmt_arena->is_conventional()reads true andactivate_stmt_arena_if_needed()(called e.g. fromsave_leaf_tables()) never redirects allocations to the PS's arena in the first place.Harmless for what it's protecting:
leaf_tables_execis normally cached on the persistent arena so a repeatedly-executed statement'sSELECT_LEXdoesn't rebuild it every time, but ourSELECT_LEXis torn down and reparsed fresh (due tomysql_parse()semantics) on everyEXECUTE, so there's nothing to cache here regardless of which arena is used.Initial attempt for more fixes was made by PR #5692 (now closed as too complex for stable branches)