From 03b5d4a5076dff44ae8f6be7ca65838445ec911a Mon Sep 17 00:00:00 2001 From: Giovanni Date: Tue, 21 Jul 2026 23:28:09 +0200 Subject: [PATCH] Cache eval-plugin glue methods for the process lifetime, not per message PerMsgStatus::finish() undefined every register_plugin_eval_glue() generated wrapper sub (and any register_generated_rule_method() plugin subs) and cleared their tracking hash after every single message, even though this glue is 100% static per-config -- forcing a fresh eval() compile of every distinct eval-plugin function on every message instead of once per process, the same "compile once, reuse" pattern Check.pm already uses for its rule subs. It also contradicted register_generated_rule_method()'s documented contract, which says these methods are destroyed at Mail::SpamAssassin::finish() (session end), not per-message. Move the cleanup to Mail::SpamAssassin::finish(), alongside the existing finish_tests plugin-hook cleanup. Since that code runs outside the PerMsgStatus package, bare (non-fully-qualified) method names now need explicit qualification to avoid undefining the wrong symbol. Co-Authored-By: Claude Sonnet 5 --- lib/Mail/SpamAssassin.pm | 19 +++++++++++++++++++ lib/Mail/SpamAssassin/PerMsgStatus.pm | 16 ++++++++-------- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/lib/Mail/SpamAssassin.pm b/lib/Mail/SpamAssassin.pm index 472ba75a0c..0d3435a905 100644 --- a/lib/Mail/SpamAssassin.pm +++ b/lib/Mail/SpamAssassin.pm @@ -1661,6 +1661,25 @@ sub finish { $self->call_plugins("finish_tests", { conf => $self->{conf}, main => $self }); + # clean up eval-plugin glue (register_plugin_eval_glue()) and any + # plugin-registered generated rule methods (register_generated_rule_method()), + # left in place across all messages processed by this session -- see + # the comment in PerMsgStatus::finish() for why these aren't cleaned up + # per-message. register_plugin_eval_glue() pushes bare names (defined in + # the PerMsgStatus package); register_generated_rule_method() pushes + # fully-qualified ones -- qualify bare names here since, unlike + # PerMsgStatus::finish()'s original cleanup loop, this code doesn't run + # in the PerMsgStatus package itself. + { no strict 'refs'; + foreach my $method (@Mail::SpamAssassin::PerMsgStatus::TEMPORARY_METHODS) { + my $fqname = $method =~ /::/ ? $method + : "Mail::SpamAssassin::PerMsgStatus::$method"; + undef &{$fqname} if defined &{$fqname}; + } + } + @Mail::SpamAssassin::PerMsgStatus::TEMPORARY_METHODS = (); + %Mail::SpamAssassin::PerMsgStatus::TEMPORARY_EVAL_GLUE_METHODS = (); + $self->{plugins}->finish(); delete $self->{plugins}; if ($self->{bayes_scanner}) { diff --git a/lib/Mail/SpamAssassin/PerMsgStatus.pm b/lib/Mail/SpamAssassin/PerMsgStatus.pm index 5e561042e4..b08b763cc9 100644 --- a/lib/Mail/SpamAssassin/PerMsgStatus.pm +++ b/lib/Mail/SpamAssassin/PerMsgStatus.pm @@ -1959,14 +1959,14 @@ sub finish { $self->report_unsatisfied_actions(); - # Clean up temporary methods - foreach my $method (@TEMPORARY_METHODS) { - if (defined &{$method}) { - undef &{$method}; - } - } - @TEMPORARY_METHODS = (); # clear for next time - %TEMPORARY_EVAL_GLUE_METHODS = (); + # Note: @TEMPORARY_METHODS/%TEMPORARY_EVAL_GLUE_METHODS (eval-plugin glue + # generated by register_plugin_eval_glue(), and any methods a plugin + # registered via register_generated_rule_method()) are intentionally NOT + # cleaned up here. They're static per-config, safe to reuse for every + # message scanned by this process -- same lifetime as Check.pm's own + # compiled rule subs. Cleaning them up per-message forced eval-plugin + # glue to be eval()-recompiled on every single message. They're cleaned + # up once at session end, in Mail::SpamAssassin::finish(). # Delete out all of the members of $self. This will remove any direct # circular references and let the memory get reclaimed while also being more