{"record":{"id":"0825032ea1e553e9","repo":"stamparm/maltrail","slug":"just-inserted-dns-tunneling","errorCode":null,"errorMessage":"just inserted","messagePattern":"just inserted","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"sensor/src/heuristics/dns_tunneling.rs","lineNumber":207,"sourceCode":"    /// Record one query and say whether the pair has just crossed every threshold.\n    ///\n    /// `subdomain` is everything below the registered zone; `leading` is its first label.\n    pub fn observe(&mut self, key: &str, subdomain: &str, leading: &str, sec: u64) -> Outcome {\n        let carrying = leading.chars().count() >= settings::DNS_TUNNELING_MIN_LABEL\n            && entropy_x100(leading) >= settings::DNS_TUNNELING_MIN_ENTROPY_X100;\n\n        let entry = match self.pairs.get_mut(key) {\n            Some(entry) => entry,\n            None => {\n                // Refuse rather than evict, like every other accumulator here: the key is chosen\n                // by whoever is sending, so eviction under flood would let them push their own\n                // earlier evidence out of the window.\n                if self.pairs.len() >= super::HEURISTIC_MAX_KEYS {\n                    self.saturations += 1;\n                    return Outcome::Quiet;\n                }\n                self.pairs.insert(key.into(), Accumulator { first_sec: sec, ..Accumulator::default() });\n                self.pairs.get_mut(key).expect(\"just inserted\")\n            }\n        };\n\n        entry.queries += 1;\n        entry.last_sec = sec;\n        entry.bytes += subdomain.len();\n        if carrying {\n            entry.carrying += 1;\n        }\n        if entry.names.len() < super::HEURISTIC_MAX_KEYS {\n            entry.names.insert(subdomain.into());\n        }\n\n        if entry.alerted || !entry.verdict() {\n            return Outcome::Quiet;\n        }\n        entry.alerted = true;\n        Outcome::Alert","sourceCodeStart":189,"sourceCodeEnd":225,"githubUrl":"https://github.com/stamparm/maltrail/blob/77cfb06d7606506d101bbcec0786c77166c4255e/sensor/src/heuristics/dns_tunneling.rs#L189-L225","documentation":"Same invariant pattern in DnsTunneling::observe: after the saturation guard, a new Accumulator is inserted into self.pairs and immediately fetched with get_mut().expect(\"just inserted\"). The expect encodes the guarantee that the key just inserted must be present; a panic indicates the map changed between insert and lookup.","triggerScenarios":"Concurrent or intervening mutation of self.pairs between insert and get_mut — e.g. a refactor inserting an eviction/clear call inside the locked region, or replacing the plain insert with logic that can skip insertion.","commonSituations":"Code changes to the observe() hot path (new eviction policies, per-key TTL sweeps) breaking the insert-then-get assumption; not reachable from ordinary DNS query patterns.","solutions":["Ensure insert and get_mut share the same lock scope with no intervening writes","Use the entry() API so the fetch cannot miss","Confirm the saturation guard returns before reaching the insert when pairs is full"],"exampleFix":"// before\nself.pairs.insert(key.into(), Accumulator { first_sec: sec, ..Accumulator::default() });\nself.pairs.get_mut(key).expect(\"just inserted\")\n// after\nself.pairs.entry(key.into()).or_insert_with(|| Accumulator { first_sec: sec, ..Accumulator::default() })","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"fn accumulator_mut<'m>(m: &'m mut HashMap<String, Accumulator>, k: &str, sec: u64) -> &'m mut Accumulator {\n    m.entry(k.to_owned()).or_insert_with(|| Accumulator { first_sec: sec, ..Accumulator::default() })\n}","tryCatchPattern":null,"preventionTips":["Prefer the entry API for insert-or-fetch patterns","Avoid intervening mutations between insert and use","Add debug_assert!(m.contains_key(k)) after inserts during refactors"],"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"}