]> git.ipfire.org Git - thirdparty/rspamd.git/commitdiff
[Fix] Move binary data from KEYS to ARGV in fuzzy and neural scripts
authorVsevolod Stakhov <vsevolod@rspamd.com>
Tue, 24 Feb 2026 14:39:49 +0000 (14:39 +0000)
committerVsevolod Stakhov <vsevolod@rspamd.com>
Tue, 24 Feb 2026 14:39:49 +0000 (14:39 +0000)
Same expand_keys corruption issue as the Bayes fix: binary data passed
as KEYS gets template-expanded, stripping 0x24 bytes.

fuzzy_update: move operation parameters and raw digest to ARGV, keep
only hash_key, count_key, and shingle keys as KEYS.

neural_save_unlock: move compressed ANN weights, PCA matrix, and all
non-key parameters to ARGV, keep only Redis key names as KEYS.

lualib/lua_fuzzy_redis.lua
lualib/plugins/neural.lua
lualib/redis_scripts/fuzzy_update.lua
lualib/redis_scripts/neural_save_unlock.lua

index e9b35671f41d755370b1d76328408f2b709f9b1e..69055b4a82464505da9fc1f919490eed3a3ccab5 100644 (file)
@@ -76,17 +76,10 @@ local function gen_update_functor(redis_params, update_script_id)
       if upd.op ~= "dup" then
         local hash_key = prefix .. upd.digest
 
-        -- Build KEYS array for the Redis script
+        -- Build KEYS array (only actual Redis key names) and ARGV (parameters)
         local keys = {
           hash_key,
-          upd.op,
-          tostring(upd.flag),
-          tostring(upd.value),
-          tostring(expire),
-          tostring(upd.timestamp),
-          tostring(upd.is_weak),
           count_key,
-          upd.digest,
         }
 
         -- Append shingle keys if present
@@ -96,6 +89,16 @@ local function gen_update_functor(redis_params, update_script_id)
           end
         end
 
+        local args = {
+          upd.op,
+          tostring(upd.flag),
+          tostring(upd.value),
+          tostring(expire),
+          tostring(upd.timestamp),
+          tostring(upd.is_weak),
+          upd.digest,
+        }
+
         local function update_cb(err, _)
           if err then
             logger.errx(rspamd_config, '%s: update script failed: %s', N, err)
@@ -106,7 +109,7 @@ local function gen_update_functor(redis_params, update_script_id)
 
         lua_redis.exec_redis_script(update_script_id,
             { ev_base = ev_base, is_write = true, key = hash_key },
-            update_cb, keys)
+            update_cb, keys, args)
       end
     end
   end
index f7094e2dcbf86909efed2c6d36eeeec1c45d9110..ab8c49652c0feae5d193a156c0e35331aeaa4171 100644 (file)
@@ -1267,11 +1267,12 @@ local function spawn_train(params)
           redis_save_cb,
           { profile.redis_key,
             redis_ann_prefix(params.rule, params.set.name),
-            ann_data,
+            params.ann_key, -- old key to unlock...
+          },
+          { ann_data,
             profile_serialized,
             tostring(params.rule.ann_expire),
             tostring(os.time()),
-            params.ann_key, -- old key to unlock...
             roc_thresholds_serialized or '',
             pca_data or '',
             providers_meta_serialized or '',
index 83487405ef5d6d75173b11b28413010fecdd7584..f78ad9de7646873f68d593eeaf5d20839e9a05fc 100644 (file)
 -- Handles ADD, DEL, and REFRESH operations including multi-flag merge and shingles
 --
 -- KEYS[1] = hash_key (prefix + digest)
--- KEYS[2] = operation: "add", "del", "refresh"
--- KEYS[3] = flag (string number)
--- KEYS[4] = value (string number)
--- KEYS[5] = expire (string number, seconds)
--- KEYS[6] = timestamp (string number, calendar seconds)
--- KEYS[7] = is_weak ("0" or "1")
--- KEYS[8] = count_key (prefix .. "_count")
--- KEYS[9] = digest (raw bytes, used as value for shingle SETEX)
--- KEYS[10..] = shingle keys (0 or 32 of them)
+-- KEYS[2] = count_key (prefix .. "_count")
+-- KEYS[3..] = shingle keys (0 or 32 of them)
+-- ARGV[1] = operation: "add", "del", "refresh"
+-- ARGV[2] = flag (string number)
+-- ARGV[3] = value (string number)
+-- ARGV[4] = expire (string number, seconds)
+-- ARGV[5] = timestamp (string number, calendar seconds)
+-- ARGV[6] = is_weak ("0" or "1")
+-- ARGV[7] = digest (raw bytes, used as value for shingle SETEX)
 
 local key = KEYS[1]
-local op = KEYS[2]
-local new_flag = tonumber(KEYS[3])
-local new_value = tonumber(KEYS[4])
-local expire = tonumber(KEYS[5])
-local timestamp = KEYS[6]
-local is_weak = tonumber(KEYS[7])
-local count_key = KEYS[8]
-local digest = KEYS[9]
+local count_key = KEYS[2]
+local op = ARGV[1]
+local new_flag = tonumber(ARGV[2])
+local new_value = tonumber(ARGV[3])
+local expire = tonumber(ARGV[4])
+local timestamp = ARGV[5]
+local is_weak = tonumber(ARGV[6])
+local digest = ARGV[7]
 
 if op == "add" then
   -- Multi-flag merge logic: up to 8 flag slots (primary '' + extra '1'..'7')
@@ -145,7 +145,7 @@ if op == "add" then
   redis.call('INCR', count_key)
 
   -- Handle shingles: SETEX each shingle key with expire and digest as value
-  for i = 10, #KEYS do
+  for i = 3, #KEYS do
     redis.call('SETEX', KEYS[i], expire, digest)
   end
 
@@ -153,14 +153,14 @@ elseif op == "del" then
   redis.call('DEL', key)
   redis.call('DECR', count_key)
 
-  for i = 10, #KEYS do
+  for i = 3, #KEYS do
     redis.call('DEL', KEYS[i])
   end
 
 elseif op == "refresh" then
   redis.call('EXPIRE', key, expire)
 
-  for i = 10, #KEYS do
+  for i = 3, #KEYS do
     redis.call('EXPIRE', KEYS[i], expire)
   end
 end
index 0c7eaecbc348d363effe77fd549a0e4c02eae11b..c6d6dc3572629cb2a61d4db1b5d90e5da23798f4 100644 (file)
@@ -1,35 +1,35 @@
 -- Lua script to save and unlock ANN in redis
--- Uses the following keys
+-- Uses the following keys and argv
 -- key1 - prefix for ANN
 -- key2 - prefix for profile
--- key3 - compressed ANN
--- key4 - profile as JSON
--- key5 - expire in seconds
--- key6 - current time
--- key7 - old key
--- key8 - ROC Thresholds
--- key9 - optional PCA
--- key10 - optional providers_meta (JSON)
--- key11 - optional norm_stats (JSON)
-local now = tonumber(KEYS[6])
-redis.call('ZADD', KEYS[2], now, KEYS[4])
-redis.call('HSET', KEYS[1], 'ann', KEYS[3])
-redis.call('HSET', KEYS[1], 'roc_thresholds', KEYS[8])
-if KEYS[9] and KEYS[9] ~= '' then
-  redis.call('HSET', KEYS[1], 'pca', KEYS[9])
+-- key3 - old key
+-- argv1 - compressed ANN
+-- argv2 - profile as JSON
+-- argv3 - expire in seconds
+-- argv4 - current time
+-- argv5 - ROC Thresholds
+-- argv6 - optional PCA
+-- argv7 - optional providers_meta (JSON)
+-- argv8 - optional norm_stats (JSON)
+local now = tonumber(ARGV[4])
+redis.call('ZADD', KEYS[2], now, ARGV[2])
+redis.call('HSET', KEYS[1], 'ann', ARGV[1])
+redis.call('HSET', KEYS[1], 'roc_thresholds', ARGV[5])
+if ARGV[6] and ARGV[6] ~= '' then
+  redis.call('HSET', KEYS[1], 'pca', ARGV[6])
 end
-if KEYS[10] and KEYS[10] ~= '' then
-  redis.call('HSET', KEYS[1], 'providers_meta', KEYS[10])
+if ARGV[7] and ARGV[7] ~= '' then
+  redis.call('HSET', KEYS[1], 'providers_meta', ARGV[7])
 end
-if KEYS[11] and KEYS[11] ~= '' then
-  redis.call('HSET', KEYS[1], 'norm_stats', KEYS[11])
+if ARGV[8] and ARGV[8] ~= '' then
+  redis.call('HSET', KEYS[1], 'norm_stats', ARGV[8])
 end
 redis.call('HDEL', KEYS[1], 'lock')
-redis.call('HDEL', KEYS[7], 'lock')
-redis.call('HSET', KEYS[7], 'obsolete', '1')
-redis.call('EXPIRE', KEYS[7], 600)
-redis.call('EXPIRE', KEYS[1], tonumber(KEYS[5]))
+redis.call('HDEL', KEYS[3], 'lock')
+redis.call('HSET', KEYS[3], 'obsolete', '1')
+redis.call('EXPIRE', KEYS[3], 600)
+redis.call('EXPIRE', KEYS[1], tonumber(ARGV[3]))
 -- expire in 10m, to not face race condition with other rspamd replicas refill deleted keys
-redis.call('EXPIRE', KEYS[7] .. '_spam_set', 600)
-redis.call('EXPIRE', KEYS[7] .. '_ham_set', 600)
+redis.call('EXPIRE', KEYS[3] .. '_spam_set', 600)
+redis.call('EXPIRE', KEYS[3] .. '_ham_set', 600)
 return 1