use serde::{Deserialize, Serialize};
use serde_json::Value;
use super::corpus::{CrashType, InterestingReason};
pub struct CrashDetector {
timeout_ms: u64,
}
impl CrashDetector {
pub fn new(timeout_ms: u64) -> Self {
Self { timeout_ms }
}
pub fn analyze(&self, response: &FuzzResponse) -> CrashAnalysis {
match &response.result {
FuzzResponseResult::Success(value) => self.analyze_success(value),
FuzzResponseResult::Error(e) => self.classify_error(e),
FuzzResponseResult::Timeout => CrashAnalysis::Hang(HangInfo {
timeout_ms: self.timeout_ms,
}),
FuzzResponseResult::ConnectionLost(reason) => CrashAnalysis::Crash(CrashInfo {
crash_type: CrashType::ConnectionDrop,
message: reason.clone(),
stack_trace: None,
}),
FuzzResponseResult::ProcessExit(code) => {
let crash_type = match *code {
139 => CrashType::Segfault, 134 => CrashType::AssertionFailure, 137 => CrashType::OutOfMemory, _ => {
if *code != 0 {
CrashType::Panic
} else {
return CrashAnalysis::None;
}
}
};
CrashAnalysis::Crash(CrashInfo {
crash_type,
message: format!("Process exited with code {}", code),
stack_trace: None,
})
}
}
}
fn analyze_success(&self, value: &Value) -> CrashAnalysis {
if let Some(obj) = value.as_object() {
if obj.contains_key("error") || obj.contains_key("errorCode") {
return CrashAnalysis::Interesting(InterestingReason::UnexpectedSuccess);
}
if let Some(s) = obj.get("message").and_then(|m| m.as_str()) {
if s.contains("panic") || s.contains("stack backtrace") || s.contains("Error:") {
return CrashAnalysis::Interesting(InterestingReason::ProtocolViolation);
}
}
}
CrashAnalysis::None
}
fn classify_error(&self, error: &JsonRpcError) -> CrashAnalysis {
let message = &error.message;
if message.contains("panic")
|| message.contains("panicked at")
|| message.contains("stack backtrace")
|| message.contains("thread 'main' panicked")
{
return CrashAnalysis::Crash(CrashInfo {
crash_type: CrashType::Panic,
message: message.clone(),
stack_trace: Self::extract_stack_trace(message),
});
}
if message.contains("out of memory")
|| message.contains("allocation")
|| message.contains("memory exhausted")
|| message.contains("OOM")
{
return CrashAnalysis::Crash(CrashInfo {
crash_type: CrashType::OutOfMemory,
message: message.clone(),
stack_trace: None,
});
}
if message.contains("assertion failed")
|| message.contains("assert!")
|| message.contains("debug_assert")
{
return CrashAnalysis::Crash(CrashInfo {
crash_type: CrashType::AssertionFailure,
message: message.clone(),
stack_trace: Self::extract_stack_trace(message),
});
}
if message.contains("SIGSEGV")
|| message.contains("segmentation fault")
|| message.contains("invalid memory")
|| message.contains("null pointer")
{
return CrashAnalysis::Crash(CrashInfo {
crash_type: CrashType::Segfault,
message: message.clone(),
stack_trace: Self::extract_stack_trace(message),
});
}
match error.code {
-32700 => CrashAnalysis::Interesting(InterestingReason::ProtocolViolation), -32600 => CrashAnalysis::Interesting(InterestingReason::ProtocolViolation), -32601 => CrashAnalysis::None, -32602 => CrashAnalysis::None, -32603 => {
if message.len() > 100 {
CrashAnalysis::Interesting(InterestingReason::ProtocolViolation)
} else {
CrashAnalysis::None
}
}
code if !((-32099..=-32000).contains(&code) || (-32768..=-32600).contains(&code)) => {
CrashAnalysis::Interesting(InterestingReason::NewErrorCode)
}
_ => CrashAnalysis::None,
}
}
fn extract_stack_trace(message: &str) -> Option<String> {
if let Some(idx) = message.find("stack backtrace:") {
return Some(message[idx..].to_string());
}
if let Some(idx) = message.find("at ") {
let remainder = &message[idx..];
if remainder.contains(".rs:") {
return Some(remainder.to_string());
}
}
None
}
}
#[derive(Debug, Clone)]
pub struct FuzzResponse {
pub result: FuzzResponseResult,
pub response_time_ms: u64,
}
impl FuzzResponse {
pub fn success(value: Value) -> Self {
Self {
result: FuzzResponseResult::Success(value),
response_time_ms: 0,
}
}
pub fn error(code: i32, message: impl Into<String>) -> Self {
Self {
result: FuzzResponseResult::Error(JsonRpcError {
code,
message: message.into(),
data: None,
}),
response_time_ms: 0,
}
}
pub fn timeout() -> Self {
Self {
result: FuzzResponseResult::Timeout,
response_time_ms: 0,
}
}
pub fn connection_lost(reason: impl Into<String>) -> Self {
Self {
result: FuzzResponseResult::ConnectionLost(reason.into()),
response_time_ms: 0,
}
}
pub fn process_exit(code: i32) -> Self {
Self {
result: FuzzResponseResult::ProcessExit(code),
response_time_ms: 0,
}
}
pub fn with_time(mut self, ms: u64) -> Self {
self.response_time_ms = ms;
self
}
pub fn from_jsonrpc(value: &Value) -> Self {
if let Some(result) = value.get("result") {
return Self::success(result.clone());
}
if let Some(error) = value.get("error") {
let code = error.get("code").and_then(|c| c.as_i64()).unwrap_or(-32603) as i32;
let message = error
.get("message")
.and_then(|m| m.as_str())
.unwrap_or("Unknown error")
.to_string();
let data = error.get("data").cloned();
return Self {
result: FuzzResponseResult::Error(JsonRpcError {
code,
message,
data,
}),
response_time_ms: 0,
};
}
Self::error(-32603, "Invalid JSON-RPC response")
}
}
#[derive(Debug, Clone)]
pub enum FuzzResponseResult {
Success(Value),
Error(JsonRpcError),
Timeout,
ConnectionLost(String),
ProcessExit(i32),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcError {
pub code: i32,
pub message: String,
pub data: Option<Value>,
}
#[derive(Debug, Clone)]
pub enum CrashAnalysis {
None,
Crash(CrashInfo),
Hang(HangInfo),
Interesting(InterestingReason),
}
impl CrashAnalysis {
pub fn is_crash(&self) -> bool {
matches!(self, CrashAnalysis::Crash(_))
}
pub fn is_hang(&self) -> bool {
matches!(self, CrashAnalysis::Hang(_))
}
pub fn is_interesting(&self) -> bool {
matches!(self, CrashAnalysis::Interesting(_))
}
}
#[derive(Debug, Clone)]
pub struct CrashInfo {
pub crash_type: CrashType,
pub message: String,
pub stack_trace: Option<String>,
}
#[derive(Debug, Clone)]
pub struct HangInfo {
pub timeout_ms: u64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detect_panic() {
let detector = CrashDetector::new(5000);
let response = FuzzResponse::error(-32603, "thread 'main' panicked at 'assertion failed'");
let analysis = detector.analyze(&response);
assert!(matches!(
analysis,
CrashAnalysis::Crash(CrashInfo {
crash_type: CrashType::Panic,
..
})
));
}
#[test]
fn detect_oom() {
let detector = CrashDetector::new(5000);
let response = FuzzResponse::error(-32603, "out of memory: allocation failed");
let analysis = detector.analyze(&response);
assert!(matches!(
analysis,
CrashAnalysis::Crash(CrashInfo {
crash_type: CrashType::OutOfMemory,
..
})
));
}
#[test]
fn detect_timeout() {
let detector = CrashDetector::new(5000);
let response = FuzzResponse::timeout();
let analysis = detector.analyze(&response);
assert!(matches!(analysis, CrashAnalysis::Hang(_)));
}
#[test]
fn detect_process_exit() {
let detector = CrashDetector::new(5000);
let response = FuzzResponse::process_exit(139);
let analysis = detector.analyze(&response);
assert!(matches!(
analysis,
CrashAnalysis::Crash(CrashInfo {
crash_type: CrashType::Segfault,
..
})
));
}
#[test]
fn normal_error_not_crash() {
let detector = CrashDetector::new(5000);
let response = FuzzResponse::error(-32601, "Method not found");
let analysis = detector.analyze(&response);
assert!(matches!(analysis, CrashAnalysis::None));
}
#[test]
fn from_jsonrpc() {
let success = serde_json::json!({
"jsonrpc": "2.0",
"result": {"data": "test"},
"id": 1
});
let response = FuzzResponse::from_jsonrpc(&success);
assert!(matches!(response.result, FuzzResponseResult::Success(_)));
let error = serde_json::json!({
"jsonrpc": "2.0",
"error": {"code": -32601, "message": "Not found"},
"id": 1
});
let response = FuzzResponse::from_jsonrpc(&error);
assert!(matches!(response.result, FuzzResponseResult::Error(_)));
}
#[test]
fn from_jsonrpc_invalid_response() {
let invalid = serde_json::json!({
"jsonrpc": "2.0",
"id": 1
});
let response = FuzzResponse::from_jsonrpc(&invalid);
assert!(matches!(response.result, FuzzResponseResult::Error(_)));
}
#[test]
fn detect_assertion_failure() {
let detector = CrashDetector::new(5000);
let response = FuzzResponse::error(-32603, "assertion failed: x > 0");
let analysis = detector.analyze(&response);
assert!(matches!(
analysis,
CrashAnalysis::Crash(CrashInfo {
crash_type: CrashType::AssertionFailure,
..
})
));
}
#[test]
fn detect_segfault() {
let detector = CrashDetector::new(5000);
let response = FuzzResponse::error(-32603, "SIGSEGV: segmentation fault");
let analysis = detector.analyze(&response);
assert!(matches!(
analysis,
CrashAnalysis::Crash(CrashInfo {
crash_type: CrashType::Segfault,
..
})
));
}
#[test]
fn detect_null_pointer() {
let detector = CrashDetector::new(5000);
let response = FuzzResponse::error(-32603, "null pointer dereference");
let analysis = detector.analyze(&response);
assert!(matches!(
analysis,
CrashAnalysis::Crash(CrashInfo {
crash_type: CrashType::Segfault,
..
})
));
}
#[test]
fn detect_connection_lost() {
let detector = CrashDetector::new(5000);
let response = FuzzResponse::connection_lost("connection reset by peer");
let analysis = detector.analyze(&response);
assert!(matches!(
analysis,
CrashAnalysis::Crash(CrashInfo {
crash_type: CrashType::ConnectionDrop,
..
})
));
}
#[test]
fn detect_process_exit_abort() {
let detector = CrashDetector::new(5000);
let response = FuzzResponse::process_exit(134);
let analysis = detector.analyze(&response);
assert!(matches!(
analysis,
CrashAnalysis::Crash(CrashInfo {
crash_type: CrashType::AssertionFailure,
..
})
));
}
#[test]
fn detect_process_exit_oom() {
let detector = CrashDetector::new(5000);
let response = FuzzResponse::process_exit(137);
let analysis = detector.analyze(&response);
assert!(matches!(
analysis,
CrashAnalysis::Crash(CrashInfo {
crash_type: CrashType::OutOfMemory,
..
})
));
}
#[test]
fn detect_process_exit_success() {
let detector = CrashDetector::new(5000);
let response = FuzzResponse::process_exit(0);
let analysis = detector.analyze(&response);
assert!(matches!(analysis, CrashAnalysis::None));
}
#[test]
fn detect_panic_with_stack_trace() {
let detector = CrashDetector::new(5000);
let response = FuzzResponse::error(
-32603,
"thread 'main' panicked at 'assertion failed'\nstack backtrace:\n 0: foo::bar\n",
);
let analysis = detector.analyze(&response);
if let CrashAnalysis::Crash(info) = analysis {
assert_eq!(info.crash_type, CrashType::Panic);
assert!(info.stack_trace.is_some());
} else {
panic!("Expected Crash analysis");
}
}
#[test]
fn detect_interesting_parse_error() {
let detector = CrashDetector::new(5000);
let response = FuzzResponse::error(-32700, "Parse error");
let analysis = detector.analyze(&response);
assert!(matches!(
analysis,
CrashAnalysis::Interesting(InterestingReason::ProtocolViolation)
));
}
#[test]
fn detect_interesting_invalid_request() {
let detector = CrashDetector::new(5000);
let response = FuzzResponse::error(-32600, "Invalid Request");
let analysis = detector.analyze(&response);
assert!(matches!(
analysis,
CrashAnalysis::Interesting(InterestingReason::ProtocolViolation)
));
}
#[test]
fn detect_interesting_new_error_code() {
let detector = CrashDetector::new(5000);
let response = FuzzResponse::error(123, "Custom error");
let analysis = detector.analyze(&response);
assert!(matches!(
analysis,
CrashAnalysis::Interesting(InterestingReason::NewErrorCode)
));
}
#[test]
fn detect_interesting_verbose_internal_error() {
let detector = CrashDetector::new(5000);
let long_message = "Internal error: ".to_string() + &"x".repeat(100);
let response = FuzzResponse::error(-32603, &long_message);
let analysis = detector.analyze(&response);
assert!(matches!(
analysis,
CrashAnalysis::Interesting(InterestingReason::ProtocolViolation)
));
}
#[test]
fn analyze_success_with_error_content() {
let detector = CrashDetector::new(5000);
let response = FuzzResponse::success(serde_json::json!({
"error": "Something went wrong",
"errorCode": 500
}));
let analysis = detector.analyze(&response);
assert!(matches!(
analysis,
CrashAnalysis::Interesting(InterestingReason::UnexpectedSuccess)
));
}
#[test]
fn analyze_success_with_panic_message() {
let detector = CrashDetector::new(5000);
let response = FuzzResponse::success(serde_json::json!({
"message": "panic occurred in handler"
}));
let analysis = detector.analyze(&response);
assert!(matches!(
analysis,
CrashAnalysis::Interesting(InterestingReason::ProtocolViolation)
));
}
#[test]
fn analyze_normal_success() {
let detector = CrashDetector::new(5000);
let response = FuzzResponse::success(serde_json::json!({
"tools": []
}));
let analysis = detector.analyze(&response);
assert!(matches!(analysis, CrashAnalysis::None));
}
#[test]
fn crash_analysis_is_crash() {
let analysis = CrashAnalysis::Crash(CrashInfo {
crash_type: CrashType::Panic,
message: "test".to_string(),
stack_trace: None,
});
assert!(analysis.is_crash());
assert!(!analysis.is_hang());
assert!(!analysis.is_interesting());
}
#[test]
fn crash_analysis_is_hang() {
let analysis = CrashAnalysis::Hang(HangInfo { timeout_ms: 5000 });
assert!(!analysis.is_crash());
assert!(analysis.is_hang());
assert!(!analysis.is_interesting());
}
#[test]
fn crash_analysis_is_interesting() {
let analysis = CrashAnalysis::Interesting(InterestingReason::NewCoverage);
assert!(!analysis.is_crash());
assert!(!analysis.is_hang());
assert!(analysis.is_interesting());
}
#[test]
fn fuzz_response_with_time() {
let response = FuzzResponse::success(serde_json::json!({})).with_time(123);
assert_eq!(response.response_time_ms, 123);
}
#[test]
fn from_jsonrpc_error_with_data() {
let error = serde_json::json!({
"jsonrpc": "2.0",
"error": {
"code": -32603,
"message": "Internal error",
"data": {"details": "more info"}
},
"id": 1
});
let response = FuzzResponse::from_jsonrpc(&error);
if let FuzzResponseResult::Error(e) = response.result {
assert!(e.data.is_some());
} else {
panic!("Expected error response");
}
}
}