use crate::error::{Error, Result};
use crate::exception::Exception;
use crate::execution_class::ExecutionClass;
use crate::stacktrace::{DebugInfo, ParseStacktrace, Stacktrace, StacktraceEntry};
use regex::Regex;
pub struct JsException;
impl Exception for JsException {
fn parse_exception(stderr: &str) -> Option<ExecutionClass> {
let rexception = Regex::new(r"(?m)^.*?(\S*Error):(?:\s+(.*))?$").unwrap();
let captures = rexception.captures(stderr)?;
let error_type = captures.get(1).unwrap().as_str();
let message = if let Some(message) = captures.get(2) {
message.as_str()
} else {
""
};
Some(ExecutionClass::new((
"NOT_EXPLOITABLE",
error_type,
message,
"",
)))
}
}
pub struct JsStacktrace;
impl ParseStacktrace for JsStacktrace {
fn extract_stacktrace(stream: &str) -> Result<Vec<String>> {
let re = Regex::new(r"(?m)^(?:(?:.*Error:)(?:.|\n)*?|Thrown at:\n*?)((?:\n\s*at \S+.*)+)")
.unwrap();
let Some(cap) = re.captures(stream) else {
return Err(Error::Casr(
"Couldn't find traceback in JS report".to_string(),
));
};
let re = Regex::new(r" +").unwrap();
let stacktrace = cap
.get(1)
.unwrap()
.as_str()
.split('\n')
.filter(|l| !l.is_empty())
.map(|l| l.trim().to_string())
.map(|e| re.replace_all(&e, " ").to_string())
.collect::<Vec<String>>();
Ok(stacktrace)
}
fn parse_stacktrace_entry(entry: &str) -> Result<StacktraceEntry> {
let mut stentry = StacktraceEntry::default();
let re_full = Regex::new(r"^\s*at\s+(.+?)(?:\s+(\[as.*?\])?\s*)\((.*)\)$").unwrap();
let re_without_pars = Regex::new(r"^\s*at\s+(.+?)(?:(?:\s+(\[as.*?\]))?\s*)$").unwrap();
fn parse_location(loc: &str) -> Result<DebugInfo> {
let mut debug_info = DebugInfo::default();
if loc.is_empty() {
return Err(Error::Casr(format!("Couldn't parse location: {loc}")));
}
let mut debug: Vec<String> = loc.split(':').map(|s| s.to_string()).collect();
if debug.len() > 3 {
debug = loc.rsplitn(3, ':').map(|s| s.to_string()).collect();
debug.reverse();
}
if debug[0].contains("://") {
debug[0] = debug[0].rsplit("://").next().unwrap().to_string();
}
if debug[0].is_empty() || !debug[0].starts_with('/') {
debug_info.file = "<anonymous>".to_string();
} else {
debug_info.file = debug[0].to_string();
}
if debug.len() == 1 {
return Ok(debug_info);
}
debug_info.line = if let Ok(line) = debug[1].parse::<u64>() {
line
} else {
return Err(Error::Casr(format!(
"Couldn't parse line number {}: {loc}",
debug[1]
)));
};
if debug.len() == 3 {
debug_info.column = if let Ok(column) = debug[2].parse::<u64>() {
column
} else {
return Err(Error::Casr(format!(
"Couldn't parse column number {}: {loc}",
debug[2]
)));
}
}
Ok(debug_info)
}
fn parse_eval(entry: &str) -> Result<StacktraceEntry> {
let mut stentry = StacktraceEntry::default();
let re = Regex::new(
r"^\s*at eval \(eval at (?:(.+?) (?:(\[as.*?\]) )?\((.*?)\)|(.+?)), (.*?)\)$",
)
.unwrap();
let Some(cap) = re.captures(entry) else {
return Err(Error::Casr(format!(
"Couldn't parse stacktrace line: {entry}"
)));
};
if let Some(function_name) = cap.get(1) {
let debug = function_name.as_str().to_string();
stentry.function = if debug == "<anonymous>" {
"eval".to_string()
} else {
"eval at ".to_string() + debug.as_str()
};
if let Some(method_name) = cap.get(2) {
stentry.function += (" ".to_string() + method_name.as_str()).as_str();
}
let debug = cap.get(3).unwrap().as_str().to_string();
if debug.contains('(') || debug.contains(')') {
stentry.function = "<anonymous>".to_string();
return Ok(stentry);
}
stentry.debug = parse_location(&debug)?;
} else if let Some(location) = cap.get(4) {
stentry.function = "eval".to_string();
stentry.debug = parse_location(location.as_str())?;
}
let debug = cap.get(5).unwrap().as_str().to_string();
let eval_debug = parse_location(&debug)?;
if eval_debug.line >= 3 {
stentry.debug.line += eval_debug.line - 3;
if eval_debug.column != 0 {
stentry.debug.column = eval_debug.column;
}
}
Ok(stentry)
}
if let Some(cap) = re_full.captures(entry) {
if entry.starts_with("at eval") && entry.contains("eval at") {
stentry = parse_eval(entry)?;
} else {
stentry.function = cap.get(1).unwrap().as_str().to_string();
if let Some(method_name) = cap.get(2) {
stentry.function += (" ".to_string() + method_name.as_str()).as_str();
}
let debug = cap.get(3).unwrap().as_str().to_string();
stentry.debug = parse_location(&debug)?;
}
} else if let Some(cap) = re_without_pars.captures(entry) {
let debug = cap.get(1).unwrap().as_str().to_string();
stentry.debug = parse_location(&debug)?;
} else {
return Err(Error::Casr(format!(
"Couldn't parse stacktrace line: {entry}"
)));
}
Ok(stentry)
}
fn parse_stacktrace(entries: &[String]) -> Result<Stacktrace> {
entries
.iter()
.map(String::as_str)
.filter(|entry| !entry.contains("unknown location"))
.map(Self::parse_stacktrace_entry)
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::stacktrace::{CrashLineExt, Filter, tests::safe_init_ignore_stack_frames};
#[test]
fn test_js_stacktrace() {
let stream = r#"Uncaught ReferenceError: var is not defined
at new Uint8Array (<anonymous>)
at Object.decode (/fuzz/node_modules/jpeg-js/lib/decoder.js:1110:13)
at fuzz (/fuzz/FuzzTarget.js:6:14)
at result (/fuzz/node_modules/fuzzer/core/core.ts:335:15)
at Worker.fuzz [as fn] (/home/user/test_js_stacktrace/main.js:1:2017)
at process.<anonymous> (/home/user/.nvm/versions/node/v16.15.1/lib/node_modules/fuzzer/build/src/worker.js:55:30)
at process.emit (node:events:527:28)
at <anonymous>
at bootstrap_node.js:609:3
at file:///home/user/node/offset.js:3:37
at fuzz (/home/user/.nvm/versions/node/v16.15.1/lib/node_modules/jsfuzz/build/src/worker.js:55:30)
at async Loader.import (internal/modules/esm/loader.js:178:24)
at eval (eval at <anonymous> (eval at g (/fuzz/FuzzTarget.js:7:7)), <anonymous>:4:23)
at eval (eval at <anonymous> (file:///home/user/node/offset.js:3:3), <anonymous>:3:7)
at eval (eval at g (/fuzz/FuzzTarget.js:7:7), <anonymous>:8:13)
at eval (/.svelte-kit/runtime/components/layout.svelte:8:41)
at fuzz (/home/user/.nvm/versions/node/v16.15.1/lib/node_modules/@jazzer.js/build/src/worker.js:55:30)
at handler (:3:10)
Uncaught ReferenceError: var is not defined
at Object.decode (/fuzz/node_modules/jpeg-js/lib/decoder.js:1110:13)
at fuzz (/fuzz/FuzzTarget.js:6:14)
at result (/fuzz/node_modules/fuzzer/core/core.ts:335:15)
at Worker.fuzz [as fn] (/home/user/test_js_stacktrace/main.js:1:2017)
at process.<anonymous> (/home/user/.nvm/versions/node/v16.15.1/lib/node_modules/fuzzer/build/src/worker.js:55:30)
at process.emit (node:events:527:28)
at <anonymous>
at bootstrap_node.js:609:3
at file:///home/user/node/offset.js:3:37
at fuzz (/home/user/.nvm/versions/node/v16.15.1/lib/node_modules/jsfuzz/build/src/worker.js:55:30)
at async Loader.import (internal/modules/esm/loader.js:178:24)
at eval (eval at <anonymous> (eval at g (/fuzz/FuzzTarget.js:7:7)), <anonymous>:4:23)
at eval (eval at <anonymous> (file:///home/user/node/offset.js:3:3), <anonymous>:3:7)
at eval (eval at g (/fuzz/FuzzTarget.js:7:7), <anonymous>:8:13)
at eval (/.svelte-kit/runtime/components/layout.svelte:8:41)
at fuzz (/home/user/.nvm/versions/node/v16.15.1/lib/node_modules/@jazzer.js/build/src/worker.js:55:30)
at handler (:3:10)"#;
let raw_stacktrace = &[
"at new Uint8Array (<anonymous>)",
"at Object.decode (/fuzz/node_modules/jpeg-js/lib/decoder.js:1110:13)",
"at fuzz (/fuzz/FuzzTarget.js:6:14)",
"at result (/fuzz/node_modules/fuzzer/core/core.ts:335:15)",
"at Worker.fuzz [as fn] (/home/user/test_js_stacktrace/main.js:1:2017)",
"at process.<anonymous> (/home/user/.nvm/versions/node/v16.15.1/lib/node_modules/fuzzer/build/src/worker.js:55:30)",
"at process.emit (node:events:527:28)",
"at <anonymous>",
"at bootstrap_node.js:609:3",
"at file:///home/user/node/offset.js:3:37",
"at fuzz (/home/user/.nvm/versions/node/v16.15.1/lib/node_modules/jsfuzz/build/src/worker.js:55:30)",
"at async Loader.import (internal/modules/esm/loader.js:178:24)",
"at eval (eval at <anonymous> (eval at g (/fuzz/FuzzTarget.js:7:7)), <anonymous>:4:23)",
"at eval (eval at <anonymous> (file:///home/user/node/offset.js:3:3), <anonymous>:3:7)",
"at eval (eval at g (/fuzz/FuzzTarget.js:7:7), <anonymous>:8:13)",
"at eval (/.svelte-kit/runtime/components/layout.svelte:8:41)",
"at fuzz (/home/user/.nvm/versions/node/v16.15.1/lib/node_modules/@jazzer.js/build/src/worker.js:55:30)",
"at handler (:3:10)",
];
let Some(exception) = JsException::parse_exception(stream) else {
panic!("Couldn't get JS exception");
};
assert_eq!(exception.short_description, "ReferenceError");
assert_eq!(exception.description, "var is not defined");
let trace = raw_stacktrace
.iter()
.map(|e| e.to_string())
.collect::<Vec<String>>();
let bt = JsStacktrace::extract_stacktrace(stream).unwrap();
assert_eq!(bt, trace);
let sttr = JsStacktrace::parse_stacktrace(&bt);
if sttr.is_err() {
panic!("{}", sttr.err().unwrap());
}
let mut stacktrace = sttr.unwrap();
safe_init_ignore_stack_frames();
stacktrace.filter();
let crash_line = stacktrace.crash_line();
if let Ok(crash_line) = crash_line {
assert_eq!(
crash_line.to_string(),
"/fuzz/node_modules/jpeg-js/lib/decoder.js:1110:13"
);
} else {
panic!("{}", crash_line.err().unwrap());
}
assert_eq!(stacktrace.len(), 9);
assert_eq!(
stacktrace[0].debug.file,
"/fuzz/node_modules/jpeg-js/lib/decoder.js".to_string()
);
assert_eq!(stacktrace[0].debug.line, 1110);
assert_eq!(stacktrace[0].debug.column, 13);
assert_eq!(stacktrace[0].function, "Object.decode".to_string());
assert_eq!(stacktrace[1].debug.file, "/fuzz/FuzzTarget.js".to_string());
assert_eq!(stacktrace[1].debug.line, 6);
assert_eq!(stacktrace[1].debug.column, 14);
assert_eq!(stacktrace[1].function, "fuzz".to_string());
assert_eq!(
stacktrace[2].debug.file,
"/fuzz/node_modules/fuzzer/core/core.ts".to_string()
);
assert_eq!(stacktrace[2].debug.line, 335);
assert_eq!(stacktrace[2].debug.column, 15);
assert_eq!(stacktrace[2].function, "result".to_string());
assert_eq!(
stacktrace[3].debug.file,
"/home/user/test_js_stacktrace/main.js".to_string()
);
assert_eq!(stacktrace[3].debug.line, 1);
assert_eq!(stacktrace[3].debug.column, 2017);
assert_eq!(stacktrace[3].function, "Worker.fuzz [as fn]".to_string());
assert_eq!(
stacktrace[4].debug.file,
"/home/user/.nvm/versions/node/v16.15.1/lib/node_modules/fuzzer/build/src/worker.js"
.to_string()
);
assert_eq!(stacktrace[4].debug.line, 55);
assert_eq!(stacktrace[4].debug.column, 30);
assert_eq!(stacktrace[4].function, "process.<anonymous>".to_string());
assert_eq!(
stacktrace[5].debug.file,
"/home/user/node/offset.js".to_string()
);
assert_eq!(stacktrace[5].debug.line, 3);
assert_eq!(stacktrace[5].debug.column, 37);
assert_eq!(stacktrace[5].function, "".to_string());
assert_eq!(
stacktrace[6].debug.file,
"/home/user/node/offset.js".to_string()
);
assert_eq!(stacktrace[6].debug.line, 3);
assert_eq!(stacktrace[6].debug.column, 7);
assert_eq!(stacktrace[6].function, "eval".to_string());
assert_eq!(stacktrace[7].debug.file, "/fuzz/FuzzTarget.js".to_string());
assert_eq!(stacktrace[7].debug.line, 12);
assert_eq!(stacktrace[7].debug.column, 13);
assert_eq!(stacktrace[7].function, "eval at g".to_string());
assert_eq!(
stacktrace[8].debug.file,
"/.svelte-kit/runtime/components/layout.svelte".to_string()
);
assert_eq!(stacktrace[8].debug.line, 8);
assert_eq!(stacktrace[8].debug.column, 41);
assert_eq!(stacktrace[8].function, "eval".to_string());
}
}