{"record":{"id":"5f54df6aee4b0b3a","repo":"stamparm/maltrail","slug":"one-of-the-two-matched","errorCode":null,"errorMessage":"one of the two matched","messagePattern":"one of the two matched","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"sensor/src/process.rs","lineNumber":728,"sourceCode":"    if previous == Some(stamp) {\n        return; // skip bursts\n    }\n\n    st.metrics.trail_lookups += 1;\n    let dst_hit = st.trails.db().get_ip(ep.dst).map(|v| (v.info.to_string(), v.reference.to_string()));\n    let dst_port_hit =\n        st.trails.db().get_ip_port(ep.dst, ep.dst_port).map(|v| (v.info.to_string(), v.reference.to_string()));\n\n    if dst_hit.is_some() || dst_port_hit.is_some() {\n        let previous_logged = st.last_logged_syn.replace(stamp);\n        if previous_logged != Some(stamp) {\n            // IPORT iff the matched key is the addr_port form (not the bare IP).\n            let (trail, info, reference, trail_type) = match dst_port_hit {\n                Some((info, reference)) => {\n                    (ep.dst.addr_port(ep.dst_port).as_str().to_string(), info, reference, TRAIL::IPORT)\n                }\n                None => {\n                    let (info, reference) = dst_hit.expect(\"one of the two matched\");\n                    (ep.dst.render().as_str().to_string(), info, reference, TRAIL::IP)\n                }\n            };\n            let parking_off_web = info.contains(\"parking site\") && !matches!(ep.dst_port, 80 | 443);\n            if !info.contains(\"attacker\") && !parking_off_web {\n                emit_ep(st, sec, usec, ep, PROTO::TCP, trail_type, Field::Text(trail), &info, &reference);\n            }\n        }\n    } else if !ep.dst.is_localhost() {\n        let src_hit = st.trails.db().get_ip(ep.src).map(|v| (v.info.to_string(), v.reference.to_string()));\n        let src_port_hit =\n            st.trails.db().get_ip_port(ep.src, ep.src_port).map(|v| (v.info.to_string(), v.reference.to_string()));\n        if src_hit.is_some() || src_port_hit.is_some() {\n            let previous_logged = st.last_logged_syn.replace(stamp);\n            if previous_logged != Some(stamp) {\n                let (trail, info, reference, trail_type) = match src_port_hit {\n                    Some((info, reference)) => {\n                        (ep.src.addr_port(ep.src_port).as_str().to_string(), info, reference, TRAIL::IPORT)","sourceCodeStart":710,"sourceCodeEnd":746,"githubUrl":"https://github.com/stamparm/maltrail/blob/77cfb06d7606506d101bbcec0786c77166c4255e/sensor/src/process.rs#L710-L746","documentation":"This is a Rust `Option::expect` panic inside the TCP SYN handling path. The code matches on two lookups (`dst_port_hit` and `dst_hit`) against the trails database and asserts that at least one of them matched; if both are `None`, the invariant 'one of the two matched' is broken and the process panics. It exists to avoid re-checking an option the author believed was already proven non-empty.","triggerScenarios":"A TCP SYN packet whose destination IP and destination IP:port both fail to match any entry in the trails database reaches the `match dst_port_hit` arm with `dst_port_hit == None` and `dst_hit == None`; the `expect` then panics. Any desynchronization between the predicate that decided this packet was a 'hit' and the actual two `db()` lookups (e.g. key normalization differences, concurrent db reload) triggers it.","commonSituations":"Running a custom/edited trails feed that removed an IP entry between the filtering pass and the match pass; feeding packets whose addresses are rendered differently by `addr_port` vs `render`; races where the trails DB is swapped/reloaded while packets are in flight.","solutions":["Replace the `expect` with a graceful `None =>` arm that skips emit_ep (and optionally logs) instead of panicking, so a missing trail never kills the sensor thread.","Verify the caller's selection logic guarantees at least one of `dst_port_hit`/`dst_hit` is `Some`; fix the upstream filter if it can admit packets with no trail match.","If the trails DB can be reloaded concurrently, snapshot the lookup results (or hold the guard) so both lookups observe the same DB version.","Add a regression test with a SYN to an IP absent from the trails DB to ensure it is dropped, not panicked on."],"exampleFix":"// before\nNone => {\n    let (info, reference) = dst_hit.expect(\"one of the two matched\");\n    (ep.dst.render().as_str().to_string(), info, reference, TRAIL::IP)\n}\n// after\nNone => match dst_hit {\n    Some((info, reference)) =>\n        (ep.dst.render().as_str().to_string(), info, reference, TRAIL::IP),\n    None => return, // no trail matched; drop instead of panicking\n}","handlingStrategy":"try-catch","validationCode":"// Rust has no try/catch; guard before unwrapping\nif dst_port_hit.is_none() && dst_hit.is_none() { return; } // skip packet, no panic","typeGuard":"fn trail_present(h: &Option<(Info, Ref)>) -> bool { h.is_some() }","tryCatchPattern":"// contain panics at the packet-loop boundary\ncatch_unwind(AssertUnwindSafe(|| handle_syn(st, ep)))\n    .unwrap_or_else(|_| log::warn!(\"syn handler panicked; packet dropped\"));","preventionTips":["Never use expect() for conditions that depend on runtime data; use match/if let","Add tests feeding packets with no matching trails entry","Keep trail lookup and emission in one consistent DB snapshot","Replace paired-lookup invariants with a single lookup returning both forms"],"tags":["rust","panic","expect","network","sensor"],"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"}