svn commit: r1933373 - spamassassin/trunk/lib/Mail/SpamAssassin/Plugin
[email protected] Mon, 27 Apr 2026 06:38:36 -0000
| Newsgroups | gmane.mail.spam.spamassassin.cvs |
|---|---|
| Message-ID | <177727191644.3341570.35887755331939738@svn03-he-fi> |
Author: gbechis
Date: Mon Apr 27 06:38:36 2026
New Revision: 1933373
Log:
prevent corrupted models from being written to filesystem
Modified:
spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/NeuralNetwork.pm
Modified: spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/NeuralNetwork.pm
==============================================================================
--- spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/NeuralNetwork.pm Mon Apr 27 05:16:00 2026 (r1933372)
+++ spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/NeuralNetwork.pm Mon Apr 27 06:38:36 2026 (r1933373)
@@ -44,7 +44,7 @@ use strict;
use warnings;
use re 'taint';
-my $VERSION = 0.8;
+my $VERSION = 0.8.1;
use AI::FANN qw(:all);
use Storable qw(store retrieve);
@@ -524,7 +524,6 @@ sub _text_to_features {
$vocabulary{_ham_count} += $local_doc_increment;
}
- # Prune vocabulary if needed
$self->_prune_vocabulary(\%vocabulary, $vocab_cap);
my $vocab_path;
@@ -865,6 +864,7 @@ sub learn_message {
# Save the model atomically
my $model_saved = 0;
my $tmp_path;
+ my $file_mode = 0666 & ~umask();
eval {
my ($vol, $dir, undef) = File::Spec->splitpath($dataset_path);
my $tmp_dir = File::Spec->catpath($vol, $dir, '');
@@ -874,6 +874,7 @@ sub learn_message {
SUFFIX => '.tmp',
UNLINK => 0,
);
+ chmod($file_mode, $tmp_path) or info("chmod $file_mode on '$tmp_path' failed: $!");
$network->save($tmp_path) or die "model save to temp '$tmp_path' failed";
rename($tmp_path, $dataset_path)
or die "atomic rename '$tmp_path' -> '$dataset_path' failed: $!";
@@ -1299,14 +1300,61 @@ sub _check_neuralnetwork {
if ($model_expired) {
dbg("Model cache expired (age: ${model_age}s, ttl: ${ttl}s), reloading");
}
+
+ my $locker = $self->{main}->{locker};
+ my $got_lock = 0;
+ eval {
+ $got_lock = $locker->safe_lock($dataset_path,
+ $self->{main}->{conf}->{neuralnetwork_lock_timeout});
+ 1;
+ };
+
+ my $file_mode = 0666 & ~umask();
eval {
$self->{neural_model} = AI::FANN->new_from_file($dataset_path);
$self->{_neural_model_load_time} = time();
1;
} or do {
- dbg("Failed to load model for prediction: " . ($@ || 'unknown'));
- return;
+ my $err = $@ || 'unknown';
+ my @stat = stat($dataset_path);
+ my $fsize = @stat ? $stat[7] : 'N/A';
+ dbg("Failed to load model for prediction: $err "
+ . "(path=$dataset_path, size=${fsize}B), attempting vocabulary rebuild");
+
+ # rebuild an in-memory model from vocabulary statistics
+ undef $self->{neural_model};
+ my $rebuilt = eval {
+ $self->_retrain_from_vocabulary($conf, $nn_data_dir,
+ $feature_vectors->[0] ? scalar(@{$feature_vectors->[0]{vec}}) : 0);
+ };
+ if ($rebuilt) {
+ dbg("Vocabulary rebuild succeeded");
+ $self->{neural_model} = $rebuilt;
+ $self->{_neural_model_load_time} = time();
+ # Persist the rebuilt model
+ eval {
+ my ($vol, $dir) = File::Spec->splitpath($dataset_path);
+ my $tmp_dir = File::Spec->catpath($vol, $dir, '');
+ my (undef, $tmp_path) = File::Temp::tempfile(
+ 'fann-XXXXXX', DIR => $tmp_dir, SUFFIX => '.tmp', UNLINK => 0);
+ chmod($file_mode, $tmp_path) or info("chmod $file_mode on '$tmp_path' failed: $!");
+ if ($rebuilt->save($tmp_path)) {
+ rename($tmp_path, $dataset_path)
+ or die "rename failed: $!";
+ dbg("Persisted rebuilt model to '$dataset_path'");
+ } else {
+ unlink $tmp_path;
+ }
+ 1;
+ } or dbg("Could not persist rebuilt model: " . ($@ || 'unknown'));
+ } else {
+ dbg("Vocabulary rebuild failed");
+ $locker->safe_unlock($dataset_path) if $got_lock;
+ return;
+ }
};
+
+ $locker->safe_unlock($dataset_path) if $got_lock;
}
my $network = $self->{neural_model};