{"record":{"id":"aeb16a11da7242fe","repo":"stamparm/maltrail","slug":"trail-reload-rejected-trails-would-replace-below-the-0-floor","errorCode":null,"errorMessage":"trail reload REJECTED: {} trails would replace {} (below the {:.0}% floor); keeping the current set. If this drop is real, restart the sensor or lower 'TRAIL_RELOAD_MIN_RATIO'","messagePattern":"trail reload REJECTED: (.+?) trails would replace (.+?) \\(below the (.+?)% floor\\); keeping the current set\\. If this drop is real, restart the sensor or lower 'TRAIL_RELOAD_MIN_RATIO'","errorType":"console","errorClass":null,"httpStatus":null,"severity":"error","filePath":"sensor/src/main.rs","lineNumber":558,"sourceCode":"                    }\n                    let mtime = std::fs::metadata(&cfg_reload.trails_file).and_then(|m| m.modified()).ok();\n                    if mtime == last_mtime && !forced {\n                        continue;\n                    }\n                    last_mtime = mtime;\n                    match trails::load_with(&cfg_reload.trails_file, &wl_reload, load_options(&cfg_reload)) {\n                        Ok((db, stats)) => {\n                            // A reload that loses most of the trail set is far more likely to be a\n                            // half-written or truncated file than a real change, and publishing it\n                            // would blind the sensor without any error ever occurring. Keep the\n                            // last known-good store: detection continues on slightly stale trails,\n                            // which beats continuing on almost none.\n                            let current = reg_reload.trail_count.load(Ordering::Relaxed);\n                            let incoming = db.len() as u64;\n                            let floor = (current as f64 * cfg_reload.trail_reload_min_ratio) as u64;\n                            if cfg_reload.trail_reload_min_ratio > 0.0 && current > 0 && incoming < floor {\n                                reg_reload.reloads_rejected.fetch_add(1, Ordering::Relaxed);\n                                output::log_error(\n                                    &format!(\n                                        \"trail reload REJECTED: {} trails would replace {} (below the \\\n                                         {:.0}% floor); keeping the current set. If this drop is real, \\\n                                         restart the sensor or lower 'TRAIL_RELOAD_MIN_RATIO'\",\n                                        thousands(incoming),\n                                        thousands(current),\n                                        cfg_reload.trail_reload_min_ratio * 100.0\n                                    ),\n                                    true,\n                                );\n                            } else {\n                                reg_reload.trail_count.store(incoming, Ordering::Relaxed);\n                                store_reload.publish(db);\n                                reg_reload.trail_generation.store(store_reload.generation(), Ordering::Relaxed);\n                                reg_reload.reloads_ok.fetch_add(1, Ordering::Relaxed);\n                                cprintln!(\"[i] reloaded {} trails\", thousands(stats.loaded as u64));\n                            }\n                        }","sourceCodeStart":540,"sourceCodeEnd":576,"githubUrl":"https://github.com/stamparm/maltrail/blob/77cfb06d7606506d101bbcec0786c77166c4255e/sensor/src/main.rs#L540-L576","documentation":"The Rust sensor's trail hot-reload path refuses to swap in an incoming trails set whose size is below cfg.trail_reload_min_ratio of the current count (e.g. incoming 1,000 vs current 100,000 at a 0.9 floor would wipe out 99% of coverage). It increments reloads_rejected, logs this message, and keeps the current set, treating a sudden collapse as more likely a bad download than a real IOC purge.","triggerScenarios":"Periodic trail refresh where the newly downloaded trails DB (db.len()) contains far fewer trails than the registry's current count, and incoming < floor where floor = current * trail_reload_min_ratio (only when min_ratio > 0 and current > 0).","commonSituations":"Upstream trails feed partially failing and returning a truncated file; network proxy stripping the download; a stale/cached small trails file being served; misconfigured TRAIL_RELOAD_MIN_RATIO so legitimate smaller updates are rejected after a real upstream cleanup.","solutions":["Verify the trails source is healthy and the downloaded file is complete (compare its size/hash against upstream).","If the drop is genuine (upstream legitimately shrank the IOC set), lower TRAIL_RELOAD_MIN_RATIO or set it to 0 to disable the floor.","Restart the sensor to force a full reload that bypasses the ratio check.","Check metrics counters (reloads_rejected) over time to distinguish transient truncation from a persistent change."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// before a reload, sanity-check the incoming set against the floor\nlet floor = (current as f64 * cfg.trail_reload_min_ratio) as u64;\nlet acceptable = cfg.trail_reload_min_ratio == 0.0 || current == 0 || incoming >= floor;\nassert!(acceptable, \"incoming trails {incoming} below floor {floor}\");","typeGuard":"fn passes_reload_floor(current: u64, incoming: u64, min_ratio: f64) -> bool {\n    min_ratio <= 0.0 || current == 0 || incoming >= (current as f64 * min_ratio) as u64\n}","tryCatchPattern":"// treat a rejection as a warning signal, not a crash\nif !passes_reload_floor(current, incoming, ratio) {\n    verify_trails_source_integrity(); // re-download and compare size/hash before retrying\n}","preventionTips":["Monitor reloads_rejected metrics for spikes","Validate downloaded trails file size/hash against upstream before applying","Set TRAIL_RELOAD_MIN_RATIO deliberately; set 0 only when upstream shrinkage is expected"],"tags":["rust","trails","reload","config"],"backgroundTag":"invalid-config-value","analyzedSha":"77cfb06d7606506d101bbcec0786c77166c4255e","analyzedAt":"2026-09-13T03:50:16.010Z","contentChangedAt":"2026-09-13T03:50:16.010Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}