{"record":{"id":"47aab83ad9f3f5a9","repo":"stamparm/maltrail","slug":"just-inserted","errorCode":null,"errorMessage":"just inserted","messagePattern":"just inserted","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"sensor/src/heuristics/dns_exhaustion.rs","lineNumber":81,"sourceCode":"    }\n\n    /// Record `subdomain_part` under `domain` and report what the caller should do.\n    /// `threshold` is passed in so tests can lower it, exactly like the Python test does.\n    pub fn observe(&mut self, domain: &str, subdomain_part: &str, sec: u64, threshold: usize) -> Outcome {\n        // NOTE: membership test, not truthiness - an existing-but-empty set (just cleared\n        // at the 60s boundary) must keep its window start.\n        let window = match self.domains.get_mut(domain) {\n            Some(w) => w,\n            None => {\n                // Bounded: the hourly reset is a TIME bound, and the parent domain is chosen by\n                // whoever sends the query. Refuse new domains at the cap instead of evicting\n                // tracked ones, so a flood cannot push an in-progress window out of memory.\n                if self.domains.len() >= super::HEURISTIC_MAX_KEYS {\n                    self.saturations += 1;\n                    return Outcome::Continue;\n                }\n                self.domains.insert(domain.into(), Window { start: sec, subdomains: StrSet::default() });\n                self.domains.get_mut(domain).expect(\"just inserted\")\n            }\n        };\n\n        if sec.saturating_sub(window.start) > 60 {\n            window.start = sec;\n            window.subdomains.clear();\n            return Outcome::Continue;\n        }\n        if window.subdomains.len() < threshold {\n            // `contains` first: `insert(subdomain_part.into())` would build an owned copy of the\n            // label on EVERY query just to throw it away as a duplicate. Real traffic queries the\n            // same handful of subdomains over and over, so that was an allocation per DNS packet.\n            if !window.subdomains.contains(subdomain_part) {\n                window.subdomains.insert(subdomain_part.into());\n            }\n            return Outcome::Continue;\n        }\n        if !self.exhausted.contains(domain) {","sourceCodeStart":63,"sourceCodeEnd":99,"githubUrl":"https://github.com/stamparm/maltrail/blob/77cfb06d7606506d101bbcec0786c77166c4255e/sensor/src/heuristics/dns_exhaustion.rs#L63-L99","documentation":"Internal invariant assertion in DnsExhaustion::observe: after the HEURISTIC_MAX_KEYS guard, the code inserts a fresh Window for `domain` and immediately retrieves it with get_mut().expect(\"just inserted\"). Failure is impossible under normal concurrency (the whole observe body is behind the same lock), so a panic means the map was mutated between insert and get or the insert silently failed.","triggerScenarios":"The domain entry is removed or overwritten between insert and get_mut — e.g. code added later that clears/retains self.domains inside the same locked section, or a refactor that swaps insert for entry-API misuse.","commonSituations":"Refactoring observe() (e.g. introducing an eviction pass or a second lock scope) that breaks the insert-then-get invariant; never triggered by DNS traffic itself.","solutions":["Verify the insert and get_mut still happen under the same lock scope with no intervening mutation of self.domains","Replace insert+get_mut with the entry() API, which cannot miss","Re-check the HEURISTIC_MAX_KEYS guard still returns before the insert path is reached"],"exampleFix":"// before\nself.domains.insert(domain.into(), Window { start: sec, subdomains: StrSet::default() });\nself.domains.get_mut(domain).expect(\"just inserted\")\n// after\nmatch self.domains.entry(domain.into()) {\n    std::collections::hash_map::Entry::Vacant(v) => v.insert(Window { start: sec, subdomains: StrSet::default() }),\n    std::collections::hash_map::Entry::Occupied(o) => o.into_mut(),\n}","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"fn window_mut<'m>(m: &'m mut HashMap<String, Window>, k: &str, sec: u64) -> &'m mut Window {\n    m.entry(k.to_owned()).or_insert(Window { start: sec, subdomains: StrSet::default() })\n}","tryCatchPattern":null,"preventionTips":["Use entry()/or_insert instead of insert+get_mut","Keep insert and lookup in one lock scope","Never add eviction inside the insert-then-use window"],"tags":["rust","assertion","invariant"],"backgroundTag":"internal-invariant-violation","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"}