pub mod points_to;
pub mod ssa_summary;
use crate::labels::Cap;
use crate::summary::ssa_summary::SsaFuncSummary;
use crate::symbol::{FuncKey, FuncKind, Lang, normalize_namespace};
use serde::{Deserialize, Deserializer, Serialize};
use smallvec::SmallVec;
use std::collections::{BTreeMap, HashMap};
use std::hash::{Hash, Hasher};
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct SinkSite {
#[serde(default, skip_serializing_if = "String::is_empty")]
pub file_rel: String,
#[serde(default, skip_serializing_if = "is_zero_u32")]
pub line: u32,
#[serde(default, skip_serializing_if = "is_zero_u32")]
pub col: u32,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub snippet: String,
pub cap: Cap,
}
impl SinkSite {
pub(crate) fn dedup_key(&self) -> (&str, u32, u32, u16) {
(self.file_rel.as_str(), self.line, self.col, self.cap.bits())
}
pub fn cap_only(cap: Cap) -> Self {
Self {
file_rel: String::new(),
line: 0,
col: 0,
snippet: String::new(),
cap,
}
}
}
pub struct SinkSiteLocator<'a> {
pub tree: &'a tree_sitter::Tree,
pub bytes: &'a [u8],
pub file_rel: &'a str,
}
impl<'a> SinkSiteLocator<'a> {
pub fn site_for_span(&self, span: (usize, usize), cap: Cap) -> SinkSite {
let byte = span.0;
let point = self
.tree
.root_node()
.descendant_for_byte_range(byte, byte)
.map(|n| n.start_position())
.unwrap_or(tree_sitter::Point { row: 0, column: 0 });
let snippet = line_snippet(self.bytes, byte).unwrap_or_default();
SinkSite {
file_rel: self.file_rel.to_string(),
line: (point.row + 1) as u32,
col: (point.column + 1) as u32,
snippet,
cap,
}
}
}
pub(crate) use crate::utils::snippet::line_snippet;
pub(crate) fn union_sink_sites(existing: &mut SmallVec<[SinkSite; 1]>, incoming: &[SinkSite]) {
for site in incoming {
let key = site.dedup_key();
if !existing.iter().any(|s| s.dedup_key() == key) {
existing.push(site.clone());
}
}
}
pub(crate) fn union_param_sink_sites(
existing: &mut Vec<(usize, SmallVec<[SinkSite; 1]>)>,
incoming: &[(usize, SmallVec<[SinkSite; 1]>)],
) {
for (idx, sites) in incoming {
if let Some((_, ex)) = existing.iter_mut().find(|(i, _)| *i == *idx) {
union_sink_sites(ex, sites);
} else {
existing.push((*idx, sites.clone()));
}
}
}
const SYNTHETIC_DISAMBIG_BIT: u32 = 0x8000_0000;
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct CalleeSite {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub arity: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub receiver: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub qualifier: Option<String>,
#[serde(default, skip_serializing_if = "is_zero_u32")]
pub ordinal: u32,
}
fn is_zero_u32(n: &u32) -> bool {
*n == 0
}
impl CalleeSite {
pub fn bare(name: impl Into<String>) -> Self {
Self {
name: name.into(),
..Default::default()
}
}
}
impl From<String> for CalleeSite {
fn from(name: String) -> Self {
Self {
name,
..Default::default()
}
}
}
impl From<&str> for CalleeSite {
fn from(name: &str) -> Self {
Self {
name: name.to_string(),
..Default::default()
}
}
}
fn deserialize_callee_sites<'de, D>(de: D) -> Result<Vec<CalleeSite>, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum Entry {
Structured(CalleeSite),
Bare(String),
}
let raw: Vec<Entry> = Vec::deserialize(de)?;
Ok(raw
.into_iter()
.map(|e| match e {
Entry::Structured(s) => s,
Entry::Bare(name) => CalleeSite::bare(name),
})
.collect())
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct FuncSummary {
pub name: String,
pub file_path: String,
pub lang: String,
pub param_count: usize,
pub param_names: Vec<String>,
pub source_caps: u16,
pub sanitizer_caps: u16,
pub sink_caps: u16,
#[serde(default)]
pub propagating_params: Vec<usize>,
#[serde(default, skip_serializing)]
pub propagates_taint: bool,
pub tainted_sink_params: Vec<usize>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub param_to_sink: Vec<(usize, SmallVec<[SinkSite; 1]>)>,
#[serde(default, deserialize_with = "deserialize_callee_sites")]
pub callees: Vec<CalleeSite>,
#[serde(default)]
pub container: String,
#[serde(default)]
pub disambig: Option<u32>,
#[serde(default)]
pub kind: FuncKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub module_path: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rust_use_map: Option<BTreeMap<String, String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rust_wildcards: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub hierarchy_edges: Vec<(String, String)>,
}
impl FuncSummary {
#[inline]
pub fn source_caps(&self) -> Cap {
Cap::from_bits_truncate(self.source_caps)
}
#[inline]
pub fn sanitizer_caps(&self) -> Cap {
Cap::from_bits_truncate(self.sanitizer_caps)
}
#[inline]
pub fn sink_caps(&self) -> Cap {
Cap::from_bits_truncate(self.sink_caps)
}
pub fn propagates_any(&self) -> bool {
!self.propagating_params.is_empty() || self.propagates_taint
}
pub fn func_key(&self, scan_root: Option<&str>) -> FuncKey {
FuncKey {
lang: Lang::from_slug(&self.lang).unwrap_or(Lang::Rust),
namespace: normalize_namespace(&self.file_path, scan_root),
container: self.container.clone(),
name: self.name.clone(),
arity: Some(self.param_count),
disambig: self.disambig,
kind: self.kind,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CalleeResolution {
Resolved(FuncKey),
NotFound,
Ambiguous(Vec<FuncKey>),
}
#[derive(Debug, Clone)]
pub struct CalleeQuery<'a> {
pub name: &'a str,
pub caller_lang: Lang,
pub caller_namespace: &'a str,
pub caller_container: Option<&'a str>,
pub receiver_type: Option<&'a str>,
pub namespace_qualifier: Option<&'a str>,
pub receiver_var: Option<&'a str>,
pub arity: Option<usize>,
}
impl<'a> CalleeQuery<'a> {
pub fn has_qualified_hint(&self) -> bool {
self.receiver_type.is_some()
|| self.namespace_qualifier.is_some()
|| self.caller_container.is_some_and(|s| !s.is_empty())
}
}
#[derive(Default)]
pub struct GlobalSummaries {
by_key: HashMap<FuncKey, FuncSummary>,
by_lang_name: HashMap<(Lang, String), Vec<FuncKey>>,
by_lang_qualified: HashMap<(Lang, String), Vec<FuncKey>>,
by_rust_module: HashMap<(String, String), Vec<FuncKey>>,
ssa_by_key: HashMap<FuncKey, SsaFuncSummary>,
bodies_by_key: HashMap<FuncKey, crate::taint::ssa_transfer::CalleeSsaBody>,
auth_by_key: HashMap<FuncKey, crate::auth_analysis::model::AuthCheckSummary>,
hierarchy: Option<crate::callgraph::TypeHierarchyIndex>,
}
impl GlobalSummaries {
pub fn new() -> Self {
Self::default()
}
fn reconcile_func_summary_key(&self, mut key: FuncKey, summary: &FuncSummary) -> FuncKey {
let mut probe: u32 = 0;
loop {
match self.by_key.get(&key) {
Some(existing) if !summaries_compatible(existing, summary) => {
let synth = synthesize_disambig(summary).wrapping_add(probe);
key.disambig = Some(SYNTHETIC_DISAMBIG_BIT | (synth & !SYNTHETIC_DISAMBIG_BIT));
probe = probe.wrapping_add(1);
if probe >= 1024 {
tracing::warn!(
"summary identity collision probe gave up after 1024 attempts; \
falling back to union-merge for {}",
key
);
return key;
}
}
_ => return key,
}
}
}
fn reconcile_ssa_summary_key(&self, mut key: FuncKey, summary: &SsaFuncSummary) -> FuncKey {
let mut probe: u32 = 0;
loop {
let conflict = match self.ssa_by_key.get(&key) {
Some(existing) => !ssa_summaries_compatible(existing, summary, key.arity),
None => !ssa_summary_fits_arity(summary, key.arity),
};
if !conflict {
return key;
}
let synth = synthesize_ssa_disambig(summary).wrapping_add(probe);
key.disambig = Some(SYNTHETIC_DISAMBIG_BIT | (synth & !SYNTHETIC_DISAMBIG_BIT));
probe = probe.wrapping_add(1);
if probe >= 1024 {
tracing::warn!(
"SSA summary identity collision probe gave up after 1024 attempts \
for {}",
key
);
return key;
}
}
}
fn reconcile_body_key(
&self,
mut key: FuncKey,
body: &crate::taint::ssa_transfer::CalleeSsaBody,
) -> FuncKey {
let mut probe: u32 = 0;
loop {
let conflict = match self.bodies_by_key.get(&key) {
Some(existing) => existing.param_count != body.param_count,
None => match key.arity {
Some(a) => a != body.param_count,
None => false,
},
};
if !conflict {
return key;
}
let synth = (body.param_count as u32)
.wrapping_mul(0x9E37_79B9)
.wrapping_add(probe);
key.disambig = Some(SYNTHETIC_DISAMBIG_BIT | (synth & !SYNTHETIC_DISAMBIG_BIT));
probe = probe.wrapping_add(1);
if probe >= 1024 {
tracing::warn!(
"SSA body identity collision probe gave up after 1024 attempts for {}",
key
);
return key;
}
}
}
pub fn insert(&mut self, key: FuncKey, summary: FuncSummary) {
let key = self.reconcile_func_summary_key(key, &summary);
let lang = key.lang;
let name = key.name.clone();
let qualified = key.qualified_name();
let rust_module = if lang == Lang::Rust {
summary.module_path.clone()
} else {
None
};
self.by_key
.entry(key.clone())
.and_modify(|existing| {
existing.source_caps |= summary.source_caps;
existing.sanitizer_caps |= summary.sanitizer_caps;
existing.sink_caps |= summary.sink_caps;
existing.propagates_taint |= summary.propagates_taint;
for &idx in &summary.propagating_params {
if !existing.propagating_params.contains(&idx) {
existing.propagating_params.push(idx);
}
}
for &idx in &summary.tainted_sink_params {
if !existing.tainted_sink_params.contains(&idx) {
existing.tainted_sink_params.push(idx);
}
}
union_param_sink_sites(&mut existing.param_to_sink, &summary.param_to_sink);
for c in &summary.callees {
if !existing.callees.iter().any(|e| {
e.name == c.name
&& e.arity == c.arity
&& e.receiver == c.receiver
&& e.qualifier == c.qualifier
&& e.ordinal == c.ordinal
}) {
existing.callees.push(c.clone());
}
}
})
.or_insert(summary);
let keys = self.by_lang_name.entry((lang, name)).or_default();
if !keys.contains(&key) {
keys.push(key.clone());
}
let q_keys = self.by_lang_qualified.entry((lang, qualified)).or_default();
if !q_keys.contains(&key) {
q_keys.push(key.clone());
}
if let Some(mp) = rust_module {
let mk = self
.by_rust_module
.entry((mp, key.name.clone()))
.or_default();
if !mk.contains(&key) {
mk.push(key);
}
}
}
pub fn get(&self, key: &FuncKey) -> Option<&FuncSummary> {
self.by_key.get(key)
}
pub fn get_for_interop(&self, key: &FuncKey) -> Option<&FuncSummary> {
if let Some(hit) = self.by_key.get(key) {
return Some(hit);
}
if key.disambig.is_some() {
return None;
}
let mut matches = self.by_key.iter().filter(|(k, _)| {
k.lang == key.lang
&& k.namespace == key.namespace
&& k.container == key.container
&& k.name == key.name
&& k.arity == key.arity
&& k.kind == key.kind
});
let first = matches.next()?;
if matches.next().is_some() {
None
} else {
Some(first.1)
}
}
pub fn lookup_same_lang(&self, lang: Lang, name: &str) -> Vec<(&FuncKey, &FuncSummary)> {
self.by_lang_name
.get(&(lang, name.to_string()))
.map(|keys| {
keys.iter()
.filter_map(|k| self.by_key.get(k).map(|v| (k, v)))
.collect()
})
.unwrap_or_default()
}
pub fn lookup_rust_module(
&self,
module_path: &str,
name: &str,
) -> Vec<(&FuncKey, &FuncSummary)> {
self.by_rust_module
.get(&(module_path.to_string(), name.to_string()))
.map(|keys| {
keys.iter()
.filter_map(|k| self.by_key.get(k).map(|v| (k, v)))
.collect()
})
.unwrap_or_default()
}
pub fn lookup_qualified(&self, lang: Lang, qualified: &str) -> Vec<(&FuncKey, &FuncSummary)> {
self.by_lang_qualified
.get(&(lang, qualified.to_string()))
.map(|keys| {
keys.iter()
.filter_map(|k| self.by_key.get(k).map(|v| (k, v)))
.collect()
})
.unwrap_or_default()
}
pub fn merge(&mut self, other: GlobalSummaries) {
for (key, summary) in other.by_key {
self.insert(key, summary);
}
for (key, ssa_sum) in other.ssa_by_key {
self.ssa_by_key.insert(key, ssa_sum);
}
for (key, body) in other.bodies_by_key {
self.bodies_by_key.insert(key, body);
}
for (key, auth_sum) in other.auth_by_key {
self.auth_by_key.insert(key, auth_sum);
}
self.hierarchy = None;
}
pub fn insert_ssa(&mut self, key: FuncKey, summary: SsaFuncSummary) {
let key = if key.arity.is_some() && !ssa_summary_fits_arity(&summary, key.arity) {
let existing_also_overflows = self
.ssa_by_key
.get(&key)
.is_some_and(|existing| !ssa_summary_fits_arity(existing, key.arity));
let existing_present = self.ssa_by_key.contains_key(&key);
if !existing_present || existing_also_overflows {
key
} else {
self.reconcile_ssa_summary_key(key, &summary)
}
} else {
self.reconcile_ssa_summary_key(key, &summary)
};
self.ssa_by_key.insert(key, summary);
}
pub fn get_ssa(&self, key: &FuncKey) -> Option<&SsaFuncSummary> {
self.ssa_by_key.get(key)
}
pub fn insert_auth(
&mut self,
key: FuncKey,
summary: crate::auth_analysis::model::AuthCheckSummary,
) {
self.auth_by_key.insert(key, summary);
}
pub fn get_auth(
&self,
key: &FuncKey,
) -> Option<&crate::auth_analysis::model::AuthCheckSummary> {
self.auth_by_key.get(key)
}
pub fn auth_by_key(
&self,
) -> Option<&HashMap<FuncKey, crate::auth_analysis::model::AuthCheckSummary>> {
if self.auth_by_key.is_empty() {
None
} else {
Some(&self.auth_by_key)
}
}
pub fn auth_len(&self) -> usize {
self.auth_by_key.len()
}
pub fn insert_body(&mut self, key: FuncKey, body: crate::taint::ssa_transfer::CalleeSsaBody) {
let key = self.reconcile_body_key(key, &body);
self.bodies_by_key.insert(key, body);
}
pub fn get_body(&self, key: &FuncKey) -> Option<&crate::taint::ssa_transfer::CalleeSsaBody> {
self.bodies_by_key.get(key)
}
pub fn bodies_by_key(
&self,
) -> Option<&HashMap<FuncKey, crate::taint::ssa_transfer::CalleeSsaBody>> {
if self.bodies_by_key.is_empty() {
None
} else {
Some(&self.bodies_by_key)
}
}
pub fn bodies_len(&self) -> usize {
self.bodies_by_key.len()
}
pub fn resolve_callee_body(
&self,
lang: Lang,
name: &str,
arity_hint: Option<usize>,
caller_namespace: &str,
) -> Option<&crate::taint::ssa_transfer::CalleeSsaBody> {
match self.resolve_callee_key(name, lang, caller_namespace, arity_hint) {
CalleeResolution::Resolved(key) => self.bodies_by_key.get(&key),
CalleeResolution::NotFound | CalleeResolution::Ambiguous(_) => None,
}
}
#[allow(dead_code)] pub fn is_empty(&self) -> bool {
self.by_key.is_empty() && self.ssa_by_key.is_empty() && self.auth_by_key.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = (&FuncKey, &FuncSummary)> {
self.by_key.iter()
}
pub fn snapshot_caps(&self) -> HashMap<FuncKey, (u16, u16, u16, Vec<usize>)> {
self.by_key
.iter()
.map(|(k, s)| {
(
k.clone(),
(
s.source_caps,
s.sanitizer_caps,
s.sink_caps,
s.propagating_params.clone(),
),
)
})
.collect()
}
pub fn snapshot_ssa(&self) -> &HashMap<FuncKey, SsaFuncSummary> {
&self.ssa_by_key
}
pub fn resolve_callee_key_rust(
&self,
callee: &str,
qualifier: Option<&str>,
arity_hint: Option<usize>,
caller_namespace: &str,
use_map: Option<&crate::rust_resolve::RustUseMap>,
) -> CalleeResolution {
use crate::rust_resolve::{resolve_with_use_map, split_module_and_name};
if let Some(um) = use_map
&& let Some(full) = resolve_with_use_map(um, qualifier, callee)
{
let (module_path, name) = split_module_and_name(&full);
if !module_path.is_empty() {
let candidates = self.lookup_rust_module(&module_path, &name);
let filtered: Vec<&FuncKey> = match arity_hint {
Some(a) => candidates
.iter()
.filter(|(k, _)| k.arity == Some(a))
.map(|(k, _)| *k)
.collect(),
None => candidates.iter().map(|(k, _)| *k).collect(),
};
if filtered.len() == 1 {
return CalleeResolution::Resolved(filtered[0].clone());
}
}
}
if let Some(um) = use_map
&& !um.wildcards.is_empty()
{
let mut collected: Vec<FuncKey> = Vec::new();
for w in &um.wildcards {
let prefix = w.strip_prefix("crate::").unwrap_or(w);
if prefix.is_empty() {
continue;
}
for (k, _) in self.lookup_rust_module(prefix, callee) {
if let Some(a) = arity_hint
&& k.arity != Some(a)
{
continue;
}
if !collected.contains(k) {
collected.push(k.clone());
}
}
}
if collected.len() == 1 {
return CalleeResolution::Resolved(collected.remove(0));
}
}
self.resolve_callee_key_with_container(
callee,
Lang::Rust,
caller_namespace,
None,
arity_hint,
)
}
pub fn resolve_callee_key(
&self,
callee: &str,
caller_lang: Lang,
caller_namespace: &str,
arity_hint: Option<usize>,
) -> CalleeResolution {
self.resolve_callee(&CalleeQuery {
name: callee,
caller_lang,
caller_namespace,
caller_container: None,
receiver_type: None,
namespace_qualifier: None,
receiver_var: None,
arity: arity_hint,
})
}
pub fn resolve_callee_key_with_container(
&self,
callee: &str,
caller_lang: Lang,
caller_namespace: &str,
container_hint: Option<&str>,
arity_hint: Option<usize>,
) -> CalleeResolution {
self.resolve_callee(&CalleeQuery {
name: callee,
caller_lang,
caller_namespace,
caller_container: None,
receiver_type: None,
namespace_qualifier: container_hint,
receiver_var: None,
arity: arity_hint,
})
}
pub fn resolve_callee(&self, q: &CalleeQuery<'_>) -> CalleeResolution {
let arity_matches = |k: &FuncKey| match q.arity {
Some(a) => k.arity == Some(a),
None => true,
};
let try_qualified = |container: &str| -> Option<FuncKey> {
if container.is_empty() {
return None;
}
let qual = format!("{container}::{}", q.name);
let candidates: Vec<&FuncKey> = self
.lookup_qualified(q.caller_lang, &qual)
.into_iter()
.map(|(k, _)| k)
.filter(|k| arity_matches(k))
.collect();
match candidates.len() {
0 => None,
1 => Some(candidates[0].clone()),
_ => {
let same_ns: Vec<&FuncKey> = candidates
.iter()
.copied()
.filter(|k| k.namespace == q.caller_namespace)
.collect();
if same_ns.len() == 1 {
Some(same_ns[0].clone())
} else {
None
}
}
}
};
if let Some(rt) = q.receiver_type {
if let Some(key) = try_qualified(rt) {
return CalleeResolution::Resolved(key);
}
let bare: Vec<&FuncKey> = self
.lookup_same_lang(q.caller_lang, q.name)
.into_iter()
.map(|(k, _)| k)
.filter(|k| arity_matches(k))
.collect();
return if bare.is_empty() {
CalleeResolution::NotFound
} else {
CalleeResolution::Ambiguous(bare.into_iter().cloned().collect())
};
}
if let Some(nq) = q.namespace_qualifier
&& let Some(key) = try_qualified(nq)
{
return CalleeResolution::Resolved(key);
}
if let Some(cc) = q.caller_container
&& let Some(key) = try_qualified(cc)
{
return CalleeResolution::Resolved(key);
}
let all_candidates: Vec<&FuncKey> = self
.lookup_same_lang(q.caller_lang, q.name)
.into_iter()
.map(|(k, _)| k)
.collect();
if all_candidates.is_empty() {
return CalleeResolution::NotFound;
}
let arity_filtered: Vec<&FuncKey> = all_candidates
.iter()
.copied()
.filter(|k| arity_matches(k))
.collect();
if arity_filtered.is_empty() {
return CalleeResolution::NotFound;
}
let same_ns: Vec<&FuncKey> = arity_filtered
.iter()
.copied()
.filter(|k| k.namespace == q.caller_namespace)
.collect();
if same_ns.len() == 1 {
return CalleeResolution::Resolved(same_ns[0].clone());
}
if let Some(rv) = q.receiver_var
&& let Some(key) = try_qualified(rv)
{
return CalleeResolution::Resolved(key);
}
let syntactic_bare = q.receiver_type.is_none()
&& q.namespace_qualifier.is_none()
&& q.receiver_var.is_none();
if syntactic_bare {
let empty_container_same_ns: Vec<&FuncKey> = same_ns
.iter()
.copied()
.filter(|k| k.container.is_empty())
.collect();
if empty_container_same_ns.len() == 1 {
return CalleeResolution::Resolved(empty_container_same_ns[0].clone());
}
}
if arity_filtered.len() == 1 {
return CalleeResolution::Resolved(arity_filtered[0].clone());
}
if q.has_qualified_hint() {
return CalleeResolution::Ambiguous(arity_filtered.into_iter().cloned().collect());
}
match same_ns.len() {
1 => CalleeResolution::Resolved(same_ns[0].clone()),
0 => CalleeResolution::Ambiguous(arity_filtered.into_iter().cloned().collect()),
_ => CalleeResolution::Ambiguous(same_ns.into_iter().cloned().collect()),
}
}
pub fn install_hierarchy(&mut self) {
let h = crate::callgraph::TypeHierarchyIndex::build(self);
self.hierarchy = Some(h);
}
pub fn hierarchy(&self) -> Option<&crate::callgraph::TypeHierarchyIndex> {
self.hierarchy.as_ref()
}
pub const MAX_HIERARCHY_FANOUT: usize = 8;
pub fn resolve_callee_widened(&self, q: &CalleeQuery<'_>) -> Vec<FuncKey> {
let arity_matches = |k: &FuncKey| match q.arity {
Some(a) => k.arity == Some(a),
None => true,
};
let single_fallback = || -> Vec<FuncKey> {
match self.resolve_callee(q) {
CalleeResolution::Resolved(k) => vec![k],
_ => Vec::new(),
}
};
let Some(rt) = q.receiver_type.filter(|s| !s.is_empty()) else {
return single_fallback();
};
let Some(h) = self.hierarchy.as_ref() else {
return single_fallback();
};
let subs = h.subs_of(q.caller_lang, rt);
if subs.is_empty() {
return single_fallback();
}
let mut out: Vec<FuncKey> = Vec::new();
let push_unique = |out: &mut Vec<FuncKey>, k: FuncKey| -> bool {
if !out.iter().any(|e| e == &k) {
out.push(k);
true
} else {
false
}
};
let qualified_lookup = |container: &str| -> Vec<FuncKey> {
let qual = format!("{container}::{}", q.name);
self.lookup_qualified(q.caller_lang, &qual)
.into_iter()
.map(|(k, _)| k.clone())
.filter(|k| arity_matches(k))
.collect()
};
for k in qualified_lookup(rt) {
push_unique(&mut out, k);
if out.len() >= Self::MAX_HIERARCHY_FANOUT {
tracing::debug!(
receiver = rt,
method = q.name,
cap = Self::MAX_HIERARCHY_FANOUT,
"hierarchy fan-out cap reached on direct receiver match"
);
return out;
}
}
for sub in subs {
for k in qualified_lookup(sub.as_str()) {
push_unique(&mut out, k);
if out.len() >= Self::MAX_HIERARCHY_FANOUT {
tracing::debug!(
receiver = rt,
method = q.name,
cap = Self::MAX_HIERARCHY_FANOUT,
"hierarchy fan-out cap reached; tail impls dropped"
);
return out;
}
}
}
if out.is_empty() {
return single_fallback();
}
out
}
}
impl std::fmt::Debug for GlobalSummaries {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("GlobalSummaries")
.field("len", &self.by_key.len())
.field("ssa_len", &self.ssa_by_key.len())
.field("bodies_len", &self.bodies_by_key.len())
.field("auth_len", &self.auth_by_key.len())
.finish()
}
}
pub(crate) fn summaries_compatible(a: &FuncSummary, b: &FuncSummary) -> bool {
if a.param_count != b.param_count {
return false;
}
if a.kind != b.kind {
return false;
}
if a.container != b.container {
return false;
}
if !a.file_path.is_empty() && !b.file_path.is_empty() && a.file_path != b.file_path {
return false;
}
if !a.param_names.is_empty() && !b.param_names.is_empty() && a.param_names != b.param_names {
return false;
}
match (&a.module_path, &b.module_path) {
(Some(l), Some(r)) if l != r => return false,
_ => {}
}
true
}
pub(crate) fn synthesize_disambig(summary: &FuncSummary) -> u32 {
let mut h = std::collections::hash_map::DefaultHasher::new();
summary.param_count.hash(&mut h);
summary.param_names.hash(&mut h);
summary.container.hash(&mut h);
summary.kind.hash(&mut h);
summary.file_path.hash(&mut h);
summary.source_caps.hash(&mut h);
summary.sanitizer_caps.hash(&mut h);
summary.sink_caps.hash(&mut h);
summary.module_path.hash(&mut h);
h.finish() as u32
}
fn ssa_summaries_compatible(
existing: &SsaFuncSummary,
new: &SsaFuncSummary,
key_arity: Option<usize>,
) -> bool {
if !ssa_summary_fits_arity(existing, key_arity) {
return false;
}
if !ssa_summary_fits_arity(new, key_arity) {
return false;
}
true
}
fn ssa_summary_fits_arity(summary: &SsaFuncSummary, key_arity: Option<usize>) -> bool {
let arity = match key_arity {
Some(a) => a,
None => return true,
};
let refs = summary
.param_to_return
.iter()
.map(|(i, _)| *i)
.chain(summary.param_to_sink.iter().map(|(i, _)| *i))
.chain(summary.param_to_sink_param.iter().map(|(i, _, _)| *i))
.chain(summary.param_container_to_return.iter().copied())
.chain(
summary
.param_to_container_store
.iter()
.flat_map(|(a, b)| [*a, *b]),
)
.chain(summary.source_to_callback.iter().map(|(i, _)| *i))
.chain(summary.abstract_transfer.iter().map(|(i, _)| *i))
.chain(summary.param_return_paths.iter().map(|(i, _)| *i));
for i in refs {
if i >= arity {
return false;
}
}
if let Some(max) = summary.points_to.max_param_index()
&& (max as usize) >= arity
{
return false;
}
true
}
fn synthesize_ssa_disambig(summary: &SsaFuncSummary) -> u32 {
let mut h = std::collections::hash_map::DefaultHasher::new();
summary.param_to_return.len().hash(&mut h);
summary.param_to_sink.len().hash(&mut h);
summary.source_caps.bits().hash(&mut h);
summary.param_to_sink_param.len().hash(&mut h);
summary.param_container_to_return.len().hash(&mut h);
summary.param_to_container_store.len().hash(&mut h);
summary.receiver_to_sink.bits().hash(&mut h);
summary.receiver_to_return.is_some().hash(&mut h);
summary.return_type.is_some().hash(&mut h);
summary.return_abstract.is_some().hash(&mut h);
summary.source_to_callback.len().hash(&mut h);
summary.abstract_transfer.len().hash(&mut h);
summary.param_return_paths.len().hash(&mut h);
summary.points_to.edges.len().hash(&mut h);
summary.points_to.overflow.hash(&mut h);
summary.points_to.returns_fresh_alloc.hash(&mut h);
h.finish() as u32
}
pub fn merge_summaries(
per_file: impl IntoIterator<Item = FuncSummary>,
scan_root: Option<&str>,
) -> GlobalSummaries {
let mut map = GlobalSummaries::new();
for fs in per_file {
let key = fs.func_key(scan_root);
map.insert(key, fs);
}
map
}
#[cfg(test)]
mod tests;