Skip to content

fix(callgraph): consumer keys keep an adapter crate's re-export prefix, so owning-crate contracts never match #394

Description

@agustingroh

Summary

When a consumer reaches a crate's API through an adapter crate that re-exports it, the
callee key keeps the adapter's prefix. A contract keyed on the crate that owns the API — which
is the KB convention — cannot match that key, so the call stays unresolved.

The existing facade handling covers the case where the crate being scanned declares the
facade module. It does not cover a consumer of that crate, which is the common case.

Reproducer

Consumer of tokio-native-tls (the standard way to use native-tls with tokio):

// Cargo.toml: tokio-native-tls = "0.3", native-tls = "0.2"
use tokio_native_tls::native_tls::{Protocol, TlsConnector};

fn configure() {
    let mut b = TlsConnector::builder();
    b.min_protocol_version(Some(Protocol::Tlsv12));
}

--export-callgraph emits:

tokio_native_tls::native_tls.TlsConnector.min_protocol_version(?)

(?) with empty parameter_types — the shape an absent contract takes.

Why it cannot resolve

native_tls::TlsConnector is the owning spelling, so a contract for that crate is keyed
native_tls::TlsConnector.min_protocol_version. But rustAuthoredKey
(internal/callgraph/contracts/contracts.go) performs exactly one rewrite — the receiver-type
separator — as its own comment says: "Callers build at most package.Type.method, so one
rewrite always suffices."

emitted:          tokio_native_tls::native_tls.TlsConnector.min_protocol_version
after rewrite:    tokio_native_tls::native_tls::TlsConnector.min_protocol_version
contract key:                       native_tls::TlsConnector.min_protocol_version
                  └── this prefix is never removed ──┘

Different strings, no match.

rustPassThroughReExport (internal/callgraph/rust_type_semantics.go) already resolves
pub mod native_tls { pub use native_tls::*; } (tokio-native-tls 0.3.1 src/lib.rs:382)
through to the owning crate — but it fires on the scanned crate's own module. In a
consumer scan the adapter's source is not parsed, so there is no pub mod to detect and the
prefix survives.

internal/callgraph/rust_reexport_facade_test.go already lists this key shape under absent,
and states the cost: "no contract keyed on the owning crate could ever match it." That
assertion holds for the adapter-scan path; the consumer path reintroduces the same key.

Why it is worth fixing before the next contract lands

There is currently no contract for native-tls in internal/callgraph/contracts/rust/, so
today every such call is unresolved and nothing stands out. The moment a native-tls contract
is added:

Consumer Import Result
direct use native_tls::… resolves
via adapter use tokio_native_tls::native_tls::… still (?)

Contract tests are written with direct imports, so they will pass. The gap is silent exactly
when the contract looks complete.

A second re-export form, not handled at all

tokio-rustls 0.12.0 through 0.26.x re-exports at the crate root instead:

pub use rustls;
pub use webpki;   // through 0.23.x

rustPassThroughReExport's discriminator looks for pub mod <name> { pub use <name>::*; }, so
this bare crate re-export does not match it in either direction. I verified the source shape
across those versions; I did not measure the emitted key for this form, so treat the key
behaviour as inferred by analogy rather than measured.

Possible directions

  1. Strip leading segments at lookup, guarded. Retry the lookup after dropping the leading
    segment. Unguarded this is unsafe: it would make mycrate::des::Des.new match a des
    contract, which is precisely what the current local-module rule prevents. A guard on "the
    leading segment is a declared dependency" helps but is not sufficient, since a dependency
    may have a non-facade module named like a contracted crate.
  2. Declare the facade in the KB. An explicit map consulted at lookup
    (tokio_native_tls::native_tls → native_tls). Precise, auditable, cannot over-match; the
    cost is one small data entry per adapter crate.
  3. Derive it when dependency sources are present. Under --scan-dependencies the adapter's
    src/lib.rs is on disk, so the existing narrow discriminator can run against the dependency
    and rewrite consumer keys. Reuses proven logic; only helps in that mode.

Whatever the route, it is worth making the miss non-silent — an unresolved crypto_call
key whose leading segment is a declared dependency is a detectable signal. Today the only way
to notice is to read (?) by hand.

Incidental finding

library.coordinates is parsed into the KB (contracts.go:59,382,470) but never consulted
during lookup — the index is built solely from contractKey(method, arity). The field reads
like it provides alias resolution and does not, which is worth either wiring up or documenting.

Verification for a fix

The reproducer above should go from (?) to a resolved signature, and the existing absent
cases in rust_reexport_facade_test.go — including the mod des local-module negative — must
stay green.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    contract-gapMissing callgraph type-inference contract for a Tier 0 librustlanguage: rusttype:bugBug fix

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions