From: Vsevolod Stakhov Date: Sat, 25 Apr 2026 18:44:16 +0000 (+0100) Subject: [Fix] lua_scanners: emit fail symbol on nil upstream X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=13c5a71ef8606f014a236e622dbf764ca39a76a1;p=thirdparty%2Frspamd.git [Fix] lua_scanners: emit fail symbol on nil upstream After the upstream layer learnt to defer DNS resolution, every scanner's :get_upstream_round_robin() may return nil while the host is still pending (or when every backend has been marked dead). The scanners then crashed in the scan callback on `upstream:get_addr()`, which silently aborted the scan instead of surfacing the failure. Add a shared lua_scanners/common.get_upstream_or_fail(task, rule, maybe_part, reason) helper that selects an upstream and, on nil, emits the configured *_FAIL symbol via yield_result with reason "no upstream available (DNS pending or all dead)". Update every scanner under lualib/lua_scanners/ to use the helper for the initial selection, and to add an inline yield_result+return guard around the retry-on-error sites that pick a different upstream. Cloudmark also has a config-time preload that picks an upstream to detect max message size; that path now logs an error and returns, since there is no task context to report against. --- diff --git a/lualib/lua_scanners/avast.lua b/lualib/lua_scanners/avast.lua index 6bbb046ce7..754c6311ae 100644 --- a/lualib/lua_scanners/avast.lua +++ b/lualib/lua_scanners/avast.lua @@ -83,7 +83,10 @@ end local function avast_check(task, content, digest, rule, maybe_part) local function avast_check_uncached () - local upstream = rule.upstreams:get_upstream_round_robin() + local upstream = common.get_upstream_or_fail(task, rule, maybe_part) + if not upstream then + return + end local addr = upstream:get_addr() local retransmits = rule.retransmits local CRLF = '\r\n' @@ -155,6 +158,11 @@ local function avast_check(task, content, digest, rule, maybe_part) end upstream = rule.upstreams:get_upstream_round_robin() + if not upstream then + common.yield_result(task, rule, + 'no upstream available for retry', 0.0, 'fail', maybe_part) + return + end addr = upstream:get_addr() tcp_opts.upstream = upstream tcp_opts.callback = avast_helo_cb diff --git a/lualib/lua_scanners/clamav.lua b/lualib/lua_scanners/clamav.lua index fc99ab0b91..5a9c339298 100644 --- a/lualib/lua_scanners/clamav.lua +++ b/lualib/lua_scanners/clamav.lua @@ -81,7 +81,10 @@ end local function clamav_check(task, content, digest, rule, maybe_part) local function clamav_check_uncached () - local upstream = rule.upstreams:get_upstream_round_robin() + local upstream = common.get_upstream_or_fail(task, rule, maybe_part) + if not upstream then + return + end local addr = upstream:get_addr() local retransmits = rule.retransmits local header = rspamd_util.pack("c9 c1 >I4", "zINSTREAM", "\0", @@ -98,6 +101,11 @@ local function clamav_check(task, content, digest, rule, maybe_part) -- Select a different upstream! upstream = rule.upstreams:get_upstream_round_robin() + if not upstream then + common.yield_result(task, rule, + 'no upstream available for retry', 0.0, 'fail', maybe_part) + return + end addr = upstream:get_addr() lua_util.debugm(rule.name, task, '%s: error: %s; retry IP: %s; retries left: %s', diff --git a/lualib/lua_scanners/cloudmark.lua b/lualib/lua_scanners/cloudmark.lua index 12a60abf1d..498b1e9b04 100644 --- a/lualib/lua_scanners/cloudmark.lua +++ b/lualib/lua_scanners/cloudmark.lua @@ -55,6 +55,11 @@ end -- Detect cloudmark max size local function cloudmark_preload(rule, cfg, ev_base, _) local upstream = rule.upstreams:get_upstream_round_robin() + if not upstream then + rspamd_logger.errx(ev_base or rspamd_config, + 'cloudmark preload: no upstream available, will retry on next scan') + return + end local addr = upstream:get_addr() local function max_message_size_cb(http_err, code, body, _) if http_err then @@ -258,7 +263,10 @@ end local function cloudmark_check(task, content, digest, rule, maybe_part) local function cloudmark_check_uncached() - local upstream = rule.upstreams:get_upstream_round_robin() + local upstream = common.get_upstream_or_fail(task, rule, maybe_part) + if not upstream then + return + end local addr = upstream:get_addr() local retransmits = rule.retransmits @@ -336,6 +344,11 @@ local function cloudmark_check(task, content, digest, rule, maybe_part) -- Select a different upstream! upstream = rule.upstreams:get_upstream_round_robin() + if not upstream then + common.yield_result(task, rule, + 'no upstream available for retry', 0.0, 'fail', maybe_part) + return + end addr = upstream:get_addr() url = cloudmark_url(rule, addr) diff --git a/lualib/lua_scanners/common.lua b/lualib/lua_scanners/common.lua index f5e760eec2..2cad0a7895 100644 --- a/lualib/lua_scanners/common.lua +++ b/lualib/lua_scanners/common.lua @@ -509,6 +509,26 @@ local function check_metric_results(task, rule) end end +--[[ +Pick a round-robin upstream from `rule.upstreams` and emit `symbol_fail` if +none is currently usable (e.g. all hostnames are still pending DNS resolution +or every backend has been marked dead). Returns the upstream on success or +nil on failure — in the nil case `symbol_fail` has already been recorded, so +the caller should just return. +--]] +local function get_upstream_or_fail(task, rule, maybe_part, reason) + local upstream = rule.upstreams and rule.upstreams:get_upstream_round_robin() + + if not upstream then + yield_result(task, rule, + reason or 'no upstream available (DNS pending or all dead)', + 0.0, 'fail', maybe_part) + return nil + end + + return upstream +end + exports.log_clean = log_clean exports.yield_result = yield_result exports.match_patterns = match_patterns @@ -517,6 +537,7 @@ exports.save_cache = save_cache exports.create_regex_table = create_regex_table exports.check_parts_match = check_parts_match exports.check_metric_results = check_metric_results +exports.get_upstream_or_fail = get_upstream_or_fail setmetatable(exports, { __call = function(t, override) diff --git a/lualib/lua_scanners/dcc.lua b/lualib/lua_scanners/dcc.lua index ed60ae486d..642889d730 100644 --- a/lualib/lua_scanners/dcc.lua +++ b/lualib/lua_scanners/dcc.lua @@ -85,7 +85,10 @@ local function dcc_config(opts) end local function dcc_check(task, content, digest, rule) - local upstream = rule.upstreams:get_upstream_round_robin() + local upstream = common.get_upstream_or_fail(task, rule, nil) + if not upstream then + return + end local addr = upstream:get_addr() local retransmits = rule.retransmits local client = rule.client @@ -144,6 +147,11 @@ local function dcc_check(task, content, digest, rule) -- Select a different upstream! upstream = rule.upstreams:get_upstream_round_robin() + if not upstream then + common.yield_result(task, rule, + 'no upstream available for retry', 0.0, 'fail') + return + end addr = upstream:get_addr() lua_util.debugm(rule.name, task, '%s: error: %s; retry IP: %s; retries left: %s', diff --git a/lualib/lua_scanners/expurgate.lua b/lualib/lua_scanners/expurgate.lua index 8405374a0a..27f6dc9d6b 100644 --- a/lualib/lua_scanners/expurgate.lua +++ b/lualib/lua_scanners/expurgate.lua @@ -145,7 +145,10 @@ local function expurgate_check(task, content, digest, rule, maybe_part) return url end - local upstream = rule.upstreams:get_upstream_round_robin() + local upstream = common.get_upstream_or_fail(task, rule, maybe_part) + if not upstream then + return + end local addr = upstream:get_addr() local retransmits = rule.retransmits @@ -205,6 +208,11 @@ local function expurgate_check(task, content, digest, rule, maybe_part) -- Select a different upstream! upstream = rule.upstreams:get_upstream_round_robin() + if not upstream then + common.yield_result(task, rule, + 'no upstream available for retry', 0.0, 'fail', maybe_part) + return + end addr = upstream:get_addr() url = expurgate_spamd_url(addr) diff --git a/lualib/lua_scanners/fprot.lua b/lualib/lua_scanners/fprot.lua index 5a469c3270..790bbbbf48 100644 --- a/lualib/lua_scanners/fprot.lua +++ b/lualib/lua_scanners/fprot.lua @@ -80,7 +80,10 @@ end local function fprot_check(task, content, digest, rule, maybe_part) local function fprot_check_uncached () - local upstream = rule.upstreams:get_upstream_round_robin() + local upstream = common.get_upstream_or_fail(task, rule, maybe_part) + if not upstream then + return + end local addr = upstream:get_addr() local retransmits = rule.retransmits local scan_id = task:get_queue_id() @@ -100,6 +103,11 @@ local function fprot_check(task, content, digest, rule, maybe_part) -- Select a different upstream! upstream = rule.upstreams:get_upstream_round_robin() + if not upstream then + common.yield_result(task, rule, + 'no upstream available for retry', 0.0, 'fail', maybe_part) + return + end addr = upstream:get_addr() lua_util.debugm(rule.name, task, '%s: error: %s; retry IP: %s; retries left: %s', diff --git a/lualib/lua_scanners/icap.lua b/lualib/lua_scanners/icap.lua index 5328587939..dc10db26c0 100644 --- a/lualib/lua_scanners/icap.lua +++ b/lualib/lua_scanners/icap.lua @@ -166,7 +166,10 @@ end local function icap_check(task, content, digest, rule, maybe_part) local function icap_check_uncached () - local upstream = rule.upstreams:get_upstream_round_robin() + local upstream = common.get_upstream_or_fail(task, rule, maybe_part) + if not upstream then + return + end local addr = upstream:get_addr() local retransmits = rule.retransmits local http_headers = {} @@ -218,6 +221,11 @@ local function icap_check(task, content, digest, rule, maybe_part) -- Select a different upstream! upstream = rule.upstreams:get_upstream_round_robin() + if not upstream then + common.yield_result(task, rule, 'no upstream available for retry', 0.0, + 'fail', maybe_part) + return + end addr = upstream:get_addr() lua_util.debugm(rule.name, task, '%s: retry IP: %s:%s', @@ -239,7 +247,7 @@ local function icap_check(task, content, digest, rule, maybe_part) end end - local function get_req_headers() + local function get_req_headers() local in_client_ip = task:get_from_ip() local in_client_ip_str = in_client_ip:to_string() local req_hlen = 2 diff --git a/lualib/lua_scanners/kaspersky_av.lua b/lualib/lua_scanners/kaspersky_av.lua index 6ae6b620e3..a90c8b3499 100644 --- a/lualib/lua_scanners/kaspersky_av.lua +++ b/lualib/lua_scanners/kaspersky_av.lua @@ -81,7 +81,10 @@ end local function kaspersky_check(task, content, digest, rule, maybe_part) local function kaspersky_check_uncached () - local upstream = rule.upstreams:get_upstream_round_robin() + local upstream = common.get_upstream_or_fail(task, rule, maybe_part) + if not upstream then + return + end local addr = upstream:get_addr() local retransmits = rule.retransmits local fname = string.format('%s/%s.tmp', @@ -117,6 +120,11 @@ local function kaspersky_check(task, content, digest, rule, maybe_part) -- Select a different upstream! upstream = rule.upstreams:get_upstream_round_robin() + if not upstream then + common.yield_result(task, rule, + 'no upstream available for retry', 0.0, 'fail', maybe_part) + return + end addr = upstream:get_addr() lua_util.debugm(rule.name, task, '%s: error: %s; retry IP: %s; retries left: %s', diff --git a/lualib/lua_scanners/kaspersky_se.lua b/lualib/lua_scanners/kaspersky_se.lua index 7bef586e14..d5fcff2561 100644 --- a/lualib/lua_scanners/kaspersky_se.lua +++ b/lualib/lua_scanners/kaspersky_se.lua @@ -110,7 +110,10 @@ local function kaspersky_se_check(task, content, digest, rule, maybe_part) return url end - local upstream = rule.upstreams:get_upstream_round_robin() + local upstream = common.get_upstream_or_fail(task, rule, maybe_part) + if not upstream then + return + end local addr = upstream:get_addr() local retransmits = rule.retransmits @@ -189,6 +192,11 @@ local function kaspersky_se_check(task, content, digest, rule, maybe_part) -- Select a different upstream! upstream = rule.upstreams:get_upstream_round_robin() + if not upstream then + common.yield_result(task, rule, + 'no upstream available for retry', 0.0, 'fail', maybe_part) + return + end addr = upstream:get_addr() url = make_url(addr) diff --git a/lualib/lua_scanners/oletools.lua b/lualib/lua_scanners/oletools.lua index 378e094675..2ccadca6c6 100644 --- a/lualib/lua_scanners/oletools.lua +++ b/lualib/lua_scanners/oletools.lua @@ -89,7 +89,10 @@ end local function oletools_check(task, content, digest, rule, maybe_part) local function oletools_check_uncached () - local upstream = rule.upstreams:get_upstream_round_robin() + local upstream = common.get_upstream_or_fail(task, rule, maybe_part) + if not upstream then + return + end local addr = upstream:get_addr() local retransmits = rule.retransmits local protocol = 'OLEFY/1.0\nMethod: oletools\nRspamd-ID: ' .. task:get_uid() .. '\n\n' @@ -106,6 +109,11 @@ local function oletools_check(task, content, digest, rule, maybe_part) -- Select a different upstream! upstream = rule.upstreams:get_upstream_round_robin() + if not upstream then + common.yield_result(task, rule, + 'no upstream available for retry', 0.0, 'fail', maybe_part) + return + end addr = upstream:get_addr() lua_util.debugm(rule.name, task, '%s: error: %s; retry IP: %s; retries left: %s', diff --git a/lualib/lua_scanners/pyzor.lua b/lualib/lua_scanners/pyzor.lua index 75c1b4a157..a00600ba85 100644 --- a/lualib/lua_scanners/pyzor.lua +++ b/lualib/lua_scanners/pyzor.lua @@ -77,7 +77,10 @@ end local function pyzor_check(task, content, digest, rule) local function pyzor_check_uncached () - local upstream = rule.upstreams:get_upstream_round_robin() + local upstream = common.get_upstream_or_fail(task, rule, nil) + if not upstream then + return + end local addr = upstream:get_addr() local retransmits = rule.retransmits @@ -92,6 +95,11 @@ local function pyzor_check(task, content, digest, rule) -- Select a different upstream! upstream = rule.upstreams:get_upstream_round_robin() + if not upstream then + common.yield_result(task, rule, + 'no upstream available for retry', 0.0, 'fail') + return + end addr = upstream:get_addr() lua_util.debugm(N, task, '%s: retry IP: %s:%s err: %s', diff --git a/lualib/lua_scanners/razor.lua b/lualib/lua_scanners/razor.lua index fcc0a8e3a3..5c91bb8b7e 100644 --- a/lualib/lua_scanners/razor.lua +++ b/lualib/lua_scanners/razor.lua @@ -82,7 +82,10 @@ end local function razor_check(task, content, digest, rule) local function razor_check_uncached () - local upstream = rule.upstreams:get_upstream_round_robin() + local upstream = common.get_upstream_or_fail(task, rule, nil) + if not upstream then + return + end local addr = upstream:get_addr() local retransmits = rule.retransmits @@ -99,6 +102,11 @@ local function razor_check(task, content, digest, rule) -- Select a different upstream! upstream = rule.upstreams:get_upstream_round_robin() + if not upstream then + common.yield_result(task, rule, + 'no upstream available for retry', 0.0, 'fail') + return + end addr = upstream:get_addr() lua_util.debugm(rule.name, task, '%s: retry IP: %s:%s', diff --git a/lualib/lua_scanners/savapi.lua b/lualib/lua_scanners/savapi.lua index 293b8f3b54..f5602815df 100644 --- a/lualib/lua_scanners/savapi.lua +++ b/lualib/lua_scanners/savapi.lua @@ -83,7 +83,10 @@ end local function savapi_check(task, content, digest, rule) local function savapi_check_uncached () - local upstream = rule.upstreams:get_upstream_round_robin() + local upstream = common.get_upstream_or_fail(task, rule, nil) + if not upstream then + return + end local addr = upstream:get_addr() local retransmits = rule.retransmits local fname = string.format('%s/%s.tmp', @@ -205,6 +208,11 @@ local function savapi_check(task, content, digest, rule) -- Select a different upstream! upstream = rule.upstreams:get_upstream_round_robin() + if not upstream then + common.yield_result(task, rule, + 'no upstream available for retry', 0.0, 'fail') + return + end addr = upstream:get_addr() lua_util.debugm(rule.name, task, '%s: error: %s; retry IP: %s; retries left: %s', diff --git a/lualib/lua_scanners/sophos.lua b/lualib/lua_scanners/sophos.lua index d9b64f1a80..6153a5606d 100644 --- a/lualib/lua_scanners/sophos.lua +++ b/lualib/lua_scanners/sophos.lua @@ -80,7 +80,10 @@ end local function sophos_check(task, content, digest, rule, maybe_part) local function sophos_check_uncached () - local upstream = rule.upstreams:get_upstream_round_robin() + local upstream = common.get_upstream_or_fail(task, rule, maybe_part) + if not upstream then + return + end local addr = upstream:get_addr() local retransmits = rule.retransmits local protocol = 'SSSP/1.0\n' @@ -97,6 +100,11 @@ local function sophos_check(task, content, digest, rule, maybe_part) -- Select a different upstream! upstream = rule.upstreams:get_upstream_round_robin() + if not upstream then + common.yield_result(task, rule, + 'no upstream available for retry', 0.0, 'fail', maybe_part) + return + end addr = upstream:get_addr() lua_util.debugm(rule.name, task, '%s: error: %s; retry IP: %s; retries left: %s', diff --git a/lualib/lua_scanners/spamassassin.lua b/lualib/lua_scanners/spamassassin.lua index f425924d5f..cb3fc8b09d 100644 --- a/lualib/lua_scanners/spamassassin.lua +++ b/lualib/lua_scanners/spamassassin.lua @@ -86,7 +86,10 @@ end local function spamassassin_check(task, content, digest, rule) local function spamassassin_check_uncached () - local upstream = rule.upstreams:get_upstream_round_robin() + local upstream = common.get_upstream_or_fail(task, rule, nil) + if not upstream then + return + end local addr = upstream:get_addr() local retransmits = rule.retransmits @@ -114,6 +117,11 @@ local function spamassassin_check(task, content, digest, rule) -- Select a different upstream! upstream = rule.upstreams:get_upstream_round_robin() + if not upstream then + common.yield_result(task, rule, + 'no upstream available for retry', 0.0, 'fail') + return + end addr = upstream:get_addr() lua_util.debugm(rule.N, task, '%s: retry IP: %s:%s', diff --git a/lualib/lua_scanners/vadesecure.lua b/lualib/lua_scanners/vadesecure.lua index 826573a890..6862ab7ede 100644 --- a/lualib/lua_scanners/vadesecure.lua +++ b/lualib/lua_scanners/vadesecure.lua @@ -167,7 +167,10 @@ local function vade_check(task, content, digest, rule, maybe_part) return url end - local upstream = rule.upstreams:get_upstream_round_robin() + local upstream = common.get_upstream_or_fail(task, rule, maybe_part) + if not upstream then + return + end local addr = upstream:get_addr() local retransmits = rule.retransmits @@ -241,6 +244,11 @@ local function vade_check(task, content, digest, rule, maybe_part) -- Select a different upstream! upstream = rule.upstreams:get_upstream_round_robin() + if not upstream then + common.yield_result(task, rule, + 'no upstream available for retry', 0.0, 'fail', maybe_part) + return + end addr = upstream:get_addr() url = vade_url(addr)