use std::path::PathBuf;
use std::process::Stdio;
use std::sync::Arc;
use std::time::Duration;
use base64::Engine;
use base64::engine::general_purpose::STANDARD as B64;
use hyper_util::rt::TokioIo;
use rcgen::{
BasicConstraints, CertificateParams, ExtendedKeyUsagePurpose, IsCa, KeyPair, KeyUsagePurpose,
};
use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer, ServerName, UnixTime};
use rustls::{DigitallySignedStruct, SignatureScheme};
use thiserror::Error;
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::process::{Child, Command};
use tracing::{debug, warn};
use magma_protocol::PluginProtocol;
pub mod schema;
pub mod provider;
pub mod import;
pub use import::import_resource_state;
fn err_chain(e: &dyn std::error::Error) -> String {
let mut s = e.to_string();
let mut src = e.source();
while let Some(inner) = src {
s.push_str(" -> ");
s.push_str(&inner.to_string());
src = inner.source();
}
s
}
#[derive(Clone)]
pub struct H2Channel {
inner: hyper::client::conn::http2::SendRequest<tonic::body::Body>,
close_reason: std::sync::Arc<std::sync::Mutex<Option<String>>>,
}
impl H2Channel {
#[must_use]
pub fn close_reason(&self) -> Option<String> {
self.close_reason.lock().ok().and_then(|g| g.clone())
}
}
#[derive(Debug, Clone, Default)]
pub struct ProviderCrash {
pub lines: Vec<String>,
pub signal: Option<i32>,
}
impl ProviderCrash {
#[must_use]
pub fn crash_site(&self) -> Option<String> {
self.lines
.iter()
.map(|l| l.trim())
.find(|l| l.contains(".go:"))
.map(|l| l.split(" +0x").next().unwrap_or(l).trim().to_string())
}
#[must_use]
pub fn headline(&self) -> Option<&str> {
self.lines
.iter()
.find(|l| l.contains("panic:") || l.contains("[signal"))
.or_else(|| self.lines.first())
.map(String::as_str)
}
}
#[must_use]
pub fn is_crash_line(l: &str) -> bool {
let lower = l.to_ascii_lowercase();
const MARKERS: &[&str] = &[
"panic:",
"signal sigsegv",
"sigsegv",
"fatal error",
"runtime error",
"nil pointer dereference",
"goroutine ",
"[signal ",
];
MARKERS.iter().any(|m| lower.contains(m))
}
const BACKTRACE_WINDOW: usize = 64;
fn classify_crash_capture(l: &str, budget: &mut usize) -> bool {
if is_crash_line(l) {
*budget = BACKTRACE_WINDOW;
return true;
}
if *budget > 0 {
*budget -= 1;
return !l.trim().is_empty();
}
false
}
struct CrashRing {
buf: std::collections::VecDeque<String>,
cap: usize,
}
impl CrashRing {
fn new(cap: usize) -> Self {
Self {
buf: std::collections::VecDeque::new(),
cap: cap.max(1),
}
}
fn push(&mut self, line: String) {
if self.buf.len() >= self.cap {
self.buf.pop_front();
}
self.buf.push_back(line);
}
fn snapshot(&self) -> Vec<String> {
self.buf.iter().cloned().collect()
}
}
type BoxErr = Box<dyn std::error::Error + Send + Sync>;
impl tower::Service<http::Request<tonic::body::Body>> for H2Channel {
type Response = http::Response<hyper::body::Incoming>;
type Error = BoxErr;
type Future = std::pin::Pin<
Box<dyn std::future::Future<Output = Result<Self::Response, Self::Error>> + Send>,
>;
fn poll_ready(
&mut self,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx).map_err(Into::into)
}
fn call(&mut self, req: http::Request<tonic::body::Body>) -> Self::Future {
use http_body_util::{BodyExt, Full};
let mut sender = self.inner.clone();
Box::pin(async move {
let (mut parts, body) = req.into_parts();
if parts.uri.authority().is_none() {
let pq = parts
.uri
.path_and_query()
.map_or("/", http::uri::PathAndQuery::as_str)
.to_string();
if let Ok(uri) = http::Uri::builder()
.scheme("http")
.authority("localhost")
.path_and_query(pq)
.build()
{
parts.uri = uri;
}
}
let bytes = body
.collect()
.await
.map_err(Into::<BoxErr>::into)?
.to_bytes();
let full = tonic::body::Body::new(Full::new(bytes));
let req = http::Request::from_parts(parts, full);
std::future::poll_fn(|cx| sender.poll_ready(cx))
.await
.map_err(Into::<BoxErr>::into)?;
sender.send_request(req).await.map_err(Into::<BoxErr>::into)
})
}
}
async fn h2_channel<IO>(io: IO) -> Result<H2Channel, PluginError>
where
IO: hyper::rt::Read + hyper::rt::Write + Unpin + Send + 'static,
{
use hyper_util::rt::TokioExecutor;
const WIN: u32 = 64 * 1024 * 1024;
let (send_req, conn) = hyper::client::conn::http2::Builder::new(TokioExecutor::new())
.initial_stream_window_size(WIN)
.initial_connection_window_size(WIN)
.max_frame_size(4 * 1024 * 1024)
.handshake::<_, tonic::body::Body>(io)
.await
.map_err(|e| PluginError::Transport(err_chain(&e)))?;
let close_reason = std::sync::Arc::new(std::sync::Mutex::new(None));
let close_reason_w = std::sync::Arc::clone(&close_reason);
tokio::spawn(async move {
if let Err(e) = conn.await {
let chain = err_chain(&e);
debug!("magma-plugin h2 connection closed: {chain}");
if let Ok(mut g) = close_reason_w.lock() {
*g = Some(chain);
}
}
});
Ok(H2Channel {
inner: send_req,
close_reason,
})
}
fn ensure_crypto_provider() {
use std::sync::Once;
static INIT: Once = Once::new();
INIT.call_once(|| {
let _ = rustls::crypto::ring::default_provider().install_default();
});
}
#[derive(Debug)]
struct TrustOnlyPeerVerifier {
trusted_cert_der: Vec<u8>,
}
impl ServerCertVerifier for TrustOnlyPeerVerifier {
fn verify_server_cert(
&self,
end_entity: &CertificateDer<'_>,
_intermediates: &[CertificateDer<'_>],
_server_name: &ServerName<'_>,
_ocsp_response: &[u8],
_now: UnixTime,
) -> Result<ServerCertVerified, rustls::Error> {
if end_entity.as_ref() == self.trusted_cert_der.as_slice() {
Ok(ServerCertVerified::assertion())
} else {
Err(rustls::Error::General(
"magma-plugin: peer cert does not match the trusted handshake cert".into(),
))
}
}
fn verify_tls12_signature(
&self,
_message: &[u8],
_cert: &CertificateDer<'_>,
_dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, rustls::Error> {
Ok(HandshakeSignatureValid::assertion())
}
fn verify_tls13_signature(
&self,
_message: &[u8],
_cert: &CertificateDer<'_>,
_dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, rustls::Error> {
Ok(HandshakeSignatureValid::assertion())
}
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
vec![
SignatureScheme::ECDSA_NISTP256_SHA256,
SignatureScheme::ECDSA_NISTP384_SHA384,
SignatureScheme::ECDSA_NISTP521_SHA512,
SignatureScheme::RSA_PKCS1_SHA256,
SignatureScheme::RSA_PSS_SHA256,
SignatureScheme::RSA_PKCS1_SHA384,
SignatureScheme::RSA_PSS_SHA384,
SignatureScheme::RSA_PKCS1_SHA512,
SignatureScheme::RSA_PSS_SHA512,
SignatureScheme::ED25519,
]
}
}
#[derive(Debug, Error)]
pub enum PluginError {
#[error("provider binary not found: {0:?}")]
BinaryNotFound(PathBuf),
#[error("provider binary not executable: {0:?}")]
NotExecutable(PathBuf),
#[error("magic cookie validation failed (provider rejected handshake)")]
MagicCookieMismatch,
#[error("provider exited before printing handshake: code {0:?}")]
EarlyExit(Option<i32>),
#[error("handshake line malformed: {0}")]
HandshakeMalformed(String),
#[error("unsupported protocol version: requested {requested}, provider offered {offered}")]
UnsupportedProtocol { requested: String, offered: String },
#[error("certificate generation failed: {0}")]
CertGen(String),
#[error("base64 decode error: {0}")]
Base64(String),
#[error("tonic transport error: {0}")]
Transport(String),
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("tls / cert error: {0}")]
Tls(String),
#[error("ImportResourceState RPC error: {0}")]
ImportRpc(String),
#[error("provider rejected import of {type_name} (id {id:?}): {reason}")]
ImportRejected {
type_name: String,
id: String,
reason: String,
},
#[error("imported-state decode error: {0}")]
ImportDecode(String),
}
#[derive(Debug, Clone)]
pub struct ParentIdentity {
pub cert_der: Vec<u8>,
pub cert_pem: String,
pub key_pem: String,
pub key_der: Vec<u8>,
pub base64_cert: String,
}
impl ParentIdentity {
pub fn generate() -> Result<Self, PluginError> {
let mut params = CertificateParams::new(vec!["localhost".to_string()])
.map_err(|e| PluginError::CertGen(e.to_string()))?;
params
.distinguished_name
.push(rcgen::DnType::CommonName, "magma-parent");
params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
params.key_usages = vec![
KeyUsagePurpose::KeyCertSign,
KeyUsagePurpose::DigitalSignature,
KeyUsagePurpose::CrlSign,
];
params.extended_key_usages = vec![
ExtendedKeyUsagePurpose::ClientAuth,
ExtendedKeyUsagePurpose::ServerAuth,
];
let key_pair = KeyPair::generate().map_err(|e| PluginError::CertGen(e.to_string()))?;
let cert = params
.self_signed(&key_pair)
.map_err(|e| PluginError::CertGen(e.to_string()))?;
let cert_pem = cert.pem();
let cert_der = cert.der().to_vec();
let key_pem = key_pair.serialize_pem();
let key_der = key_pair.serialize_der();
let base64_cert = B64.encode(cert_pem.as_bytes());
Ok(Self {
cert_der,
cert_pem,
key_pem,
key_der,
base64_cert,
})
}
}
#[derive(Debug, Clone)]
pub struct HandshakeLine {
pub core_protocol: u32,
pub app_protocol: PluginProtocol,
pub network: String,
pub address: String,
pub proto_type: String,
pub cert_pem_base64: Option<String>,
}
impl HandshakeLine {
pub fn parse(line: &str) -> Result<Self, PluginError> {
let parts: Vec<&str> = line.trim().split('|').collect();
if parts.len() < 5 {
return Err(PluginError::HandshakeMalformed(format!(
"expected ≥5 pipe-separated fields, got {}: {line:?}",
parts.len(),
)));
}
let core_protocol = parts[0]
.parse::<u32>()
.map_err(|e| PluginError::HandshakeMalformed(format!("core_protocol not u32: {e}")))?;
let app_protocol = match parts[1] {
"5" => PluginProtocol::V5,
"6" => PluginProtocol::V6,
other => {
return Err(PluginError::UnsupportedProtocol {
requested: "5 or 6".into(),
offered: other.into(),
});
}
};
Ok(Self {
core_protocol,
app_protocol,
network: parts[2].to_string(),
address: parts[3].to_string(),
proto_type: parts[4].to_string(),
cert_pem_base64: parts.get(5).map(|s| (*s).to_string()),
})
}
pub fn provider_cert_der(&self) -> Option<Result<Vec<u8>, PluginError>> {
self.cert_pem_base64.as_ref().map(|b64| {
let pad_count = (4 - b64.len() % 4) % 4;
let padded = format!("{b64}{}", "=".repeat(pad_count));
B64.decode(&padded)
.map_err(|e| PluginError::Base64(e.to_string()))
})
}
}
#[derive(Debug, Clone)]
pub struct PluginSpec {
pub binary: PathBuf,
pub magic_cookie_key: String,
pub magic_cookie_value: String,
pub accepted_protocols: Vec<PluginProtocol>,
pub min_port: u16,
pub max_port: u16,
pub kill_grace: Duration,
pub secure: bool,
}
impl Default for PluginSpec {
fn default() -> Self {
Self {
binary: PathBuf::new(),
magic_cookie_key: "TF_PLUGIN_MAGIC_COOKIE".into(),
magic_cookie_value: "d602bf8f470bc67ca7faa0386276bbdd4330efaf76d1a219cb4d6991ca9872b2"
.into(),
accepted_protocols: vec![PluginProtocol::V6, PluginProtocol::V5],
min_port: 10_000,
max_port: 25_000,
kill_grace: Duration::from_secs(5),
secure: false,
}
}
}
pub struct Plugin {
process: Child,
handshake: HandshakeLine,
spec: PluginSpec,
identity: ParentIdentity,
channel: Option<H2Channel>,
crash: Arc<std::sync::Mutex<CrashRing>>,
}
impl Plugin {
pub async fn spawn(spec: PluginSpec) -> Result<Self, PluginError> {
if !spec.binary.exists() {
return Err(PluginError::BinaryNotFound(spec.binary.clone()));
}
ensure_crypto_provider();
let identity = ParentIdentity::generate()?;
let mut cmd = Command::new(&spec.binary);
cmd.env(&spec.magic_cookie_key, &spec.magic_cookie_value)
.env("PLUGIN_MIN_PORT", spec.min_port.to_string())
.env("PLUGIN_MAX_PORT", spec.max_port.to_string())
.env(
"PLUGIN_PROTOCOL_VERSIONS",
spec.accepted_protocols
.iter()
.map(|p| p.version_str())
.collect::<Vec<_>>()
.join(","),
)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.kill_on_drop(true);
if spec.secure {
cmd.env("PLUGIN_CLIENT_CERT", &identity.cert_pem);
}
debug!(binary = ?spec.binary, "spawning provider plugin");
let mut child = cmd.spawn()?;
let stdout = child.stdout.take().ok_or_else(|| {
PluginError::Io(std::io::Error::other(
"stdout pipe missing after spawn (Stdio::piped requested above)",
))
})?;
let mut reader = BufReader::new(stdout).lines();
let line = match reader.next_line().await? {
Some(line) => line,
None => {
let status = child.wait().await.ok().and_then(|s| s.code());
return Err(PluginError::EarlyExit(status));
}
};
let handshake = HandshakeLine::parse(&line)?;
debug!(?handshake, "provider handshake received");
let crash = Arc::new(std::sync::Mutex::new(CrashRing::new(256)));
let bin = spec.binary.clone();
if let Some(stderr) = child.stderr.take() {
let bin = bin.clone();
let crash_w = Arc::clone(&crash);
tokio::spawn(async move {
let mut lines = BufReader::new(stderr).lines();
let mut budget = 0usize;
while let Ok(Some(l)) = lines.next_line().await {
if classify_crash_capture(&l, &mut budget) {
tracing::error!(provider = ?bin, stream = "stderr", "{l}");
if let Ok(mut g) = crash_w.lock() {
g.push(l);
}
} else {
tracing::trace!(provider = ?bin, "{l}");
}
}
});
}
let crash_w = Arc::clone(&crash);
tokio::spawn(async move {
let mut budget = 0usize;
while let Ok(Some(l)) = reader.next_line().await {
if classify_crash_capture(&l, &mut budget) {
tracing::error!(provider = ?bin, stream = "stdout", "{l}");
if let Ok(mut g) = crash_w.lock() {
g.push(l);
}
} else {
tracing::trace!(provider = ?bin, stream = "stdout", "{l}");
}
}
});
if !spec.accepted_protocols.contains(&handshake.app_protocol) {
return Err(PluginError::UnsupportedProtocol {
requested: spec
.accepted_protocols
.iter()
.map(|p| p.version_str())
.collect::<Vec<_>>()
.join(","),
offered: handshake.app_protocol.version_str().into(),
});
}
Ok(Self {
process: child,
handshake,
spec,
identity,
channel: None,
crash,
})
}
pub async fn dial(&mut self) -> Result<&H2Channel, PluginError> {
if self.channel.is_none() {
let channel = self.dial_channel().await?;
self.channel = Some(channel);
}
self.channel
.as_ref()
.ok_or_else(|| PluginError::Transport("internal: channel vanished after dial".into()))
}
async fn dial_channel(&self) -> Result<H2Channel, PluginError> {
let network = self.handshake.network.clone();
let address = self.handshake.address.clone();
if !self.spec.secure {
let channel = match network.as_str() {
"tcp" => {
let stream = tokio::net::TcpStream::connect(&address)
.await
.map_err(|e| PluginError::Transport(err_chain(&e)))?;
h2_channel(TokioIo::new(stream)).await?
}
"unix" => {
let stream = tokio::net::UnixStream::connect(&address)
.await
.map_err(|e| PluginError::Transport(err_chain(&e)))?;
h2_channel(TokioIo::new(stream)).await?
}
other => {
return Err(PluginError::Transport(format!(
"unsupported handshake network type: {other:?}",
)));
}
};
return Ok(channel);
}
let provider_cert_der = self.handshake.provider_cert_der().ok_or_else(|| {
PluginError::Transport("provider handshake omitted cert; mTLS impossible".into())
})??;
let parent_cert = CertificateDer::from(self.identity.cert_der.clone());
let parent_key =
PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(self.identity.key_der.clone()));
let verifier = Arc::new(TrustOnlyPeerVerifier {
trusted_cert_der: provider_cert_der,
});
let mut tls_config = rustls::ClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(verifier)
.with_client_auth_cert(vec![parent_cert], parent_key)
.map_err(|e| PluginError::Tls(format!("client auth cert: {e}")))?;
tls_config.alpn_protocols = vec![b"h2".to_vec()];
let tls_config = Arc::new(tls_config);
let connector = tokio_rustls::TlsConnector::from(tls_config);
let server_name = ServerName::try_from("localhost")
.map_err(|e| PluginError::Tls(format!("server_name: {e}")))?;
let channel = match network.as_str() {
"tcp" => {
let stream = tokio::net::TcpStream::connect(&address)
.await
.map_err(|e| PluginError::Transport(err_chain(&e)))?;
let tls = connector
.connect(server_name, stream)
.await
.map_err(|e| PluginError::Tls(err_chain(&e)))?;
h2_channel(TokioIo::new(tls)).await?
}
"unix" => {
let stream = tokio::net::UnixStream::connect(&address)
.await
.map_err(|e| PluginError::Transport(err_chain(&e)))?;
let tls = connector
.connect(server_name, stream)
.await
.map_err(|e| PluginError::Tls(err_chain(&e)))?;
h2_channel(TokioIo::new(tls)).await?
}
other => {
return Err(PluginError::Transport(format!(
"unsupported handshake network type: {other:?} (expected `tcp` or `unix`)",
)));
}
};
Ok(channel)
}
#[must_use]
pub fn handshake(&self) -> &HandshakeLine {
&self.handshake
}
#[must_use]
pub fn parent_identity(&self) -> &ParentIdentity {
&self.identity
}
#[must_use]
pub fn channel(&self) -> Option<&H2Channel> {
self.channel.as_ref()
}
#[must_use]
pub fn crash_lines(&self) -> Vec<String> {
self.crash.lock().map(|g| g.snapshot()).unwrap_or_default()
}
#[must_use]
pub fn crash_summary(&self) -> Option<ProviderCrash> {
let lines = self.crash_lines();
if lines.is_empty() {
None
} else {
Some(ProviderCrash {
lines,
signal: None,
})
}
}
pub fn exit_signal(&mut self) -> Option<i32> {
#[cfg(unix)]
{
use std::os::unix::process::ExitStatusExt;
self.process
.try_wait()
.ok()
.flatten()
.and_then(|s| s.signal())
}
#[cfg(not(unix))]
{
None
}
}
}
impl Drop for Plugin {
fn drop(&mut self) {
let _ = &self.spec.kill_grace;
warn!(handshake = ?self.handshake, "dropping plugin; subprocess will be killed by tokio");
let _ = self.process.start_kill();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_handshake_v6() {
let line = "1|6|tcp|127.0.0.1:42839|grpc|MIIBkTCCATegAwIBAgIBATAK";
let h = HandshakeLine::parse(line).unwrap();
assert_eq!(h.core_protocol, 1);
assert_eq!(h.app_protocol, PluginProtocol::V6);
assert_eq!(h.network, "tcp");
assert_eq!(h.address, "127.0.0.1:42839");
assert_eq!(h.proto_type, "grpc");
assert_eq!(
h.cert_pem_base64.as_deref(),
Some("MIIBkTCCATegAwIBAgIBATAK")
);
}
#[test]
fn parse_handshake_v5_no_cert() {
let line = "1|5|tcp|127.0.0.1:10001|grpc";
let h = HandshakeLine::parse(line).unwrap();
assert_eq!(h.app_protocol, PluginProtocol::V5);
assert!(h.cert_pem_base64.is_none());
}
#[test]
fn parse_handshake_rejects_unknown_protocol() {
let line = "1|99|tcp|127.0.0.1:10001|grpc";
assert!(matches!(
HandshakeLine::parse(line),
Err(PluginError::UnsupportedProtocol { .. })
));
}
#[test]
fn parse_handshake_rejects_malformed() {
let line = "this-is-not-pipe-separated";
assert!(matches!(
HandshakeLine::parse(line),
Err(PluginError::HandshakeMalformed(_))
));
}
#[test]
fn generate_parent_identity() {
let identity = ParentIdentity::generate().unwrap();
assert!(!identity.cert_der.is_empty());
assert!(identity.cert_pem.contains("BEGIN CERTIFICATE"));
assert!(identity.key_pem.contains("PRIVATE KEY"));
assert!(!identity.base64_cert.is_empty());
let decoded = B64.decode(&identity.base64_cert).unwrap();
assert_eq!(decoded, identity.cert_pem.as_bytes());
}
#[test]
fn is_crash_line_matches_the_live_sigsegv_evidence() {
assert!(is_crash_line(
"panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV]"
));
assert!(is_crash_line(
"[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x...]"
));
assert!(is_crash_line("goroutine 1 [running]:"));
assert!(is_crash_line("fatal error: concurrent map writes"));
assert!(is_crash_line("PANIC: something blew up"));
}
#[test]
fn is_crash_line_ignores_ordinary_provider_logs() {
assert!(!is_crash_line(
"2026-06-12T00:00:00Z [INFO] provider: configuring client: host=api.cloudflare.com"
));
assert!(!is_crash_line(
"[DEBUG] ReadDataSource: cloudflare_accounts"
));
assert!(!is_crash_line(""));
assert!(!is_crash_line("the operation did not panic and succeeded"));
}
#[test]
fn crash_ring_evicts_oldest_at_capacity() {
let mut ring = CrashRing::new(2);
ring.push("first".to_string());
ring.push("second".to_string());
ring.push("third".to_string());
assert_eq!(
ring.snapshot(),
vec!["second".to_string(), "third".to_string()]
);
}
#[test]
fn crash_ring_zero_cap_is_clamped_to_one() {
let mut ring = CrashRing::new(0);
ring.push("only".to_string());
assert_eq!(ring.snapshot(), vec!["only".to_string()]);
}
#[tokio::test]
async fn crash_capture_surfaces_panic_from_a_fake_stderr_stream() {
let stderr_bytes = concat!(
"[INFO] provider: starting up\n",
"panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV]\n",
"[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0xabc]\n",
"\n",
"goroutine 17 [running]:\n",
"github.com/cloudflare/terraform-provider-cloudflare/internal/services/zones.(*ZonesDataSource).Read(0xc0001)\n",
"\t/home/runner/work/terraform-provider-cloudflare/internal/services/zones/list_data_source.go:103 +0x2a4\n",
)
.as_bytes()
.to_vec();
let crash = Arc::new(std::sync::Mutex::new(CrashRing::new(256)));
let crash_w = Arc::clone(&crash);
let mut lines = BufReader::new(std::io::Cursor::new(stderr_bytes)).lines();
let mut budget = 0usize;
while let Ok(Some(l)) = lines.next_line().await {
if classify_crash_capture(&l, &mut budget) {
if let Ok(mut g) = crash_w.lock() {
g.push(l);
}
}
}
let captured = crash.lock().unwrap().snapshot();
assert!(
captured
.iter()
.any(|l| l.contains("nil pointer dereference") && l.contains("SIGSEGV")),
"captured crash lines must include the nil-deref panic: {captured:?}"
);
assert!(captured.iter().any(|l| l.contains("goroutine 17")));
assert!(
captured
.iter()
.any(|l| l.contains("list_data_source.go:103")),
"the .go:NNN crash-site frame must be captured: {captured:?}"
);
assert!(
!captured.iter().any(|l| l.contains("starting up")),
"ordinary info logs must NOT be captured as crash lines"
);
let pc = ProviderCrash {
lines: captured,
signal: Some(11),
};
let site = pc.crash_site().expect("crash_site from the .go: frame");
assert!(site.contains("list_data_source.go:103"), "site: {site}");
assert!(!site.contains("+0x"), "PC offset trimmed from site: {site}");
assert!(pc.headline().unwrap().contains("nil pointer dereference"));
}
#[test]
fn classify_crash_capture_window_spans_blank_then_frames() {
let mut budget = 0usize;
assert!(!classify_crash_capture(
"\t/some/file.go:1 +0x0",
&mut budget
));
assert!(classify_crash_capture("panic: boom", &mut budget));
assert!(!classify_crash_capture("", &mut budget)); assert!(classify_crash_capture(
"\t/some/file.go:42 +0x0",
&mut budget
));
}
#[test]
fn crash_site_is_none_without_a_go_frame() {
let pc = ProviderCrash {
lines: vec!["panic: boom".into(), "goroutine 1 [running]:".into()],
signal: None,
};
assert!(pc.crash_site().is_none());
assert_eq!(pc.headline(), Some("panic: boom"));
}
#[test]
fn provider_cert_round_trip() {
let identity = ParentIdentity::generate().unwrap();
let handshake = HandshakeLine {
core_protocol: 1,
app_protocol: PluginProtocol::V6,
network: "tcp".into(),
address: "127.0.0.1:50051".into(),
proto_type: "grpc".into(),
cert_pem_base64: Some(identity.base64_cert.clone()),
};
let decoded = handshake.provider_cert_der().unwrap().unwrap();
assert_eq!(decoded, identity.cert_pem.as_bytes());
}
}