stamparm/maltrail · error

just inserted

Error message

just inserted

What it means

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.

Solutions

  1. Ensure insert and get_mut share the same lock scope with no intervening writes
  2. Use the entry() API so the fetch cannot miss
  3. Confirm the saturation guard returns before reaching the insert when pairs is full

Example fix

// before
self.pairs.insert(key.into(), Accumulator { first_sec: sec, ..Accumulator::default() });
self.pairs.get_mut(key).expect("just inserted")
// after
self.pairs.entry(key.into()).or_insert_with(|| Accumulator { first_sec: sec, ..Accumulator::default() })
Defensive patterns

Strategy: type-guard

Type guard

fn accumulator_mut<'m>(m: &'m mut HashMap<String, Accumulator>, k: &str, sec: u64) -> &'m mut Accumulator {
    m.entry(k.to_owned()).or_insert_with(|| Accumulator { first_sec: sec, ..Accumulator::default() })
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of stamparm/maltrail@77cfb06d76 (2026-09-13). Data as JSON: /api/errors/0825032ea1e553e9. Report an issue: GitHub.

Appendix: source

Thrown at sensor/src/heuristics/dns_tunneling.rs:207

    /// Record one query and say whether the pair has just crossed every threshold.
    ///
    /// `subdomain` is everything below the registered zone; `leading` is its first label.
    pub fn observe(&mut self, key: &str, subdomain: &str, leading: &str, sec: u64) -> Outcome {
        let carrying = leading.chars().count() >= settings::DNS_TUNNELING_MIN_LABEL
            && entropy_x100(leading) >= settings::DNS_TUNNELING_MIN_ENTROPY_X100;

        let entry = match self.pairs.get_mut(key) {
            Some(entry) => entry,
            None => {
                // Refuse rather than evict, like every other accumulator here: the key is chosen
                // by whoever is sending, so eviction under flood would let them push their own
                // earlier evidence out of the window.
                if self.pairs.len() >= super::HEURISTIC_MAX_KEYS {
                    self.saturations += 1;
                    return Outcome::Quiet;
                }
                self.pairs.insert(key.into(), Accumulator { first_sec: sec, ..Accumulator::default() });
                self.pairs.get_mut(key).expect("just inserted")
            }
        };

        entry.queries += 1;
        entry.last_sec = sec;
        entry.bytes += subdomain.len();
        if carrying {
            entry.carrying += 1;
        }
        if entry.names.len() < super::HEURISTIC_MAX_KEYS {
            entry.names.insert(subdomain.into());
        }

        if entry.alerted || !entry.verdict() {
            return Outcome::Quiet;
        }
        entry.alerted = true;
        Outcome::Alert

View on GitHub (pinned to 77cfb06d76)