diff --git a/libcasr/src/js.rs b/libcasr/src/js.rs new file mode 100644 index 00000000..afd1accf --- /dev/null +++ b/libcasr/src/js.rs @@ -0,0 +1,281 @@ +//! JS module implements `ParseStacktrace` and `Exception` traits for JS reports. +use crate::stacktrace::ParseStacktrace; + +use crate::error::*; +use crate::stacktrace::*; +use regex::Regex; + +/// Structure provides an interface for processing the stack trace. +pub struct JSStacktrace; + +impl ParseStacktrace for JSStacktrace { + fn extract_stacktrace(stream: &str) -> Result> { + // Get stack trace from JS report. + let re = + Regex::new(r"(?m)^(?:.*Error)(?:.|\n)*?((?:\n(?:\s|\t)+at .*(?:\(.*\))?)+)").unwrap(); + let Some(cap) = re.captures(stream) else { + return Err(Error::Casr( + "Couldn't find traceback in JS report".to_string(), + )); + }; + let stacktrace = cap + .get(1) + .unwrap() + .as_str() + .split('\n') + .filter(|l| !l.is_empty()) + .map(|l| l.to_string()) + .collect::>(); + + Ok(stacktrace) + } + + fn parse_stacktrace_entry(entry: &str) -> Result { + let mut stentry = StacktraceEntry::default(); + let re_full = Regex::new( + r"^(?:\s|\t)*at(?:\s|\t)+(.+?)(?:(?:\s|\t)+(\[as.*?\])?(?:\s|\t)*)\((.*)\)$", + ) + .unwrap(); + let re_without_pars = + Regex::new(r"^(?:\s|\t)*at(?:\s|\t)+(.+?)(?:(?:(?:\s|\t)+(\[as.*?\]))?(?:\s|\t)*)$") + .unwrap(); + + fn parse_location( + entry: &str, + stentry: &mut StacktraceEntry, + loc: &str, + can_be_func: bool, + ) -> Result<()> { + let mut debug: Vec = loc.split(':').map(|s| s.to_string()).collect(); + if debug.len() > 1 { + if debug.len() > 3 { + let tail = debug[debug.len() - 2..].to_vec(); + debug = [debug[..debug.len() - 2].join(":")].to_vec(); + debug.append(&mut tail.to_vec()); + } + stentry.debug.file = debug[0].to_string(); + stentry.debug.line = if let Ok(line) = debug[1].parse::() { + line + } else { + return Err(Error::Casr(format!( + "Couldn't parse line number {}. Entry: {entry}", + debug[1] + ))); + }; + if debug.len() == 3 { + stentry.debug.column = if let Ok(column) = debug[2].parse::() { + column + } else { + return Err(Error::Casr(format!( + "Couldn't parse column number {}. Entry: {entry}", + debug[2] + ))); + } + } + } else if can_be_func { + if debug[0].ends_with(".js") || debug[0] == "native" { + stentry.debug.file = debug[0].to_string(); + } else { + stentry.function = debug[0].to_string(); + } + } else { + stentry.debug.file = debug[0].to_string(); + } + Ok(()) + } + + fn parse_eval(entry: &str, stentry: &mut StacktraceEntry) -> Result<()> { + let re = Regex::new( + r"^(?:\s|\t)*at(?:\s|\t)+eval(?:\s|\t)+\((?:eval(?:\s|\t)+at(?:\s|\t)+(.*?)(?:\s|\t)+(?:(\[as.*?\])(?:\s|\t)+)?)(?:\((.*)\))(?:,(?:\s|\t)+(.*))\)$", + ) + .unwrap(); + let Some(cap) = re.captures(entry) else { + return Err(Error::Casr(format!( + "Couldn't parse stacktrace line: {entry}" + ))); + }; + + let debug = cap.get(1).unwrap().as_str().to_string(); + if debug == "" { + stentry.function = "eval".to_string(); + } else { + stentry.function = "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(')') { + // Irrelevant entry that contains nested evals + // Fill to filter this ectry from stacktrace + // after parsing + stentry.function = "".to_string(); + return Ok(()); + } + parse_location(entry, stentry, &debug, false)?; + + let debug = cap.get(4).unwrap().as_str().to_string(); + let mut eval_stentry = StacktraceEntry::default(); + parse_location(entry, &mut eval_stentry, &debug, false)?; + if eval_stentry.debug.line != 0 { + stentry.debug.line += eval_stentry.debug.line - 3; + if eval_stentry.debug.column != 0 { + stentry.debug.column = eval_stentry.debug.column; + } + } + Ok(()) + } + + if let Some(cap) = re_full.captures(entry) { + if entry.starts_with("at eval") && entry.contains("eval at") { + // Parse eval + parse_eval(entry, &mut stentry)?; + } else { + // at function ([file][:line[:column]]) + // Not eval + 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(); + parse_location(entry, &mut stentry, &debug, false)?; + } + } else if let Some(cap) = re_without_pars.captures(entry) { + let debug = cap.get(1).unwrap().as_str().to_string(); + parse_location(entry, &mut stentry, &debug, true)?; + } else { + return Err(Error::Casr(format!( + "Couldn't parse stacktrace line: {entry}" + ))); + } + + Ok(stentry) + } + + fn parse_stacktrace(entries: &[String]) -> Result { + entries + .iter() + .map(String::as_str) + .filter(|entry| !entry.contains("unknown location")) + .map(Self::parse_stacktrace_entry) + .filter(|entry| { + entry.as_ref().is_ok_and(|x| { + x.debug.file != "native" + && x.debug.file != "" + && x.function != "" + }) + }) + .collect() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_js_stacktrace() { + let raw_stacktrace = &[ + "at new Uint8Array ()", + "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/@jazzer.js/core/core.ts:335:15)", + "at Worker.fuzz [as fn] (/home/pa_darochek/Documents/SandBox/test_js_stacktrace/main.js:1:2017)", + "at process. (/home/pa_darochek/.nvm/versions/node/v16.15.1/lib/node_modules/jsfuzz/build/src/worker.js:55:30)", + "at process.emit (node:events:527:28)", + "at func (:1:2)", + "at ", + "at bootstrap_node.js:609:3", + "at file:///offset.js:3:37", + "at async Loader.import (internal/modules/esm/loader.js:178:24)", + "at eval (eval at (eval at g (/fuzz/FuzzTarget.js:7:7)), :4:23)", + "at eval (eval at (file:///offset.js:3:3), :3:7)", + "at eval (eval at g (/fuzz/FuzzTarget.js:7:7), :8:13)", + "at eval (/.svelte-kit/runtime/components/layout.svelte:8:41)", + ]; + let trace = raw_stacktrace + .iter() + .map(|e| e.to_string()) + .collect::>(); + let sttr = JSStacktrace::parse_stacktrace(&trace); + if sttr.is_err() { + panic!("{}", sttr.err().unwrap()); + } + + let stacktrace = sttr.unwrap(); + 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/@jazzer.js/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/pa_darochek/Documents/SandBox/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/pa_darochek/.nvm/versions/node/v16.15.1/lib/node_modules/jsfuzz/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.".to_string()); + assert_eq!(stacktrace[5].debug.file, "node:events".to_string()); + assert_eq!(stacktrace[5].debug.line, 527); + assert_eq!(stacktrace[5].debug.column, 28); + assert_eq!(stacktrace[5].function, "process.emit".to_string()); + assert_eq!(stacktrace[6].debug.file, "".to_string()); + assert_eq!(stacktrace[6].debug.line, 1); + assert_eq!(stacktrace[6].debug.column, 2); + assert_eq!(stacktrace[6].function, "func".to_string()); + assert_eq!(stacktrace[7].debug.file, "bootstrap_node.js".to_string()); + assert_eq!(stacktrace[7].debug.line, 609); + assert_eq!(stacktrace[7].debug.column, 3); + assert_eq!(stacktrace[7].function, "".to_string()); + assert_eq!( + stacktrace[8].debug.file, + "file:///offset.js".to_string() + ); + assert_eq!(stacktrace[8].debug.line, 3); + assert_eq!(stacktrace[8].debug.column, 37); + assert_eq!(stacktrace[8].function, "".to_string()); + assert_eq!( + stacktrace[9].debug.file, + "internal/modules/esm/loader.js".to_string() + ); + assert_eq!(stacktrace[9].debug.line, 178); + assert_eq!(stacktrace[9].debug.column, 24); + assert_eq!(stacktrace[9].function, "async Loader.import".to_string()); + assert_eq!( + stacktrace[10].debug.file, + "file:///offset.js".to_string() + ); + assert_eq!(stacktrace[10].debug.line, 3); + assert_eq!(stacktrace[10].debug.column, 7); + assert_eq!(stacktrace[10].function, "eval".to_string()); + assert_eq!(stacktrace[11].debug.file, "/fuzz/FuzzTarget.js".to_string()); + assert_eq!(stacktrace[11].debug.line, 12); + assert_eq!(stacktrace[11].debug.column, 13); + assert_eq!(stacktrace[11].function, "eval at g".to_string()); + assert_eq!( + stacktrace[12].debug.file, + "/.svelte-kit/runtime/components/layout.svelte".to_string() + ); + assert_eq!(stacktrace[12].debug.line, 8); + assert_eq!(stacktrace[12].debug.column, 41); + assert_eq!(stacktrace[12].function, "eval".to_string()); + } +} diff --git a/libcasr/src/lib.rs b/libcasr/src/lib.rs index dcb8d22f..2284fb57 100644 --- a/libcasr/src/lib.rs +++ b/libcasr/src/lib.rs @@ -16,6 +16,7 @@ //! * Go //! * Python //! * Java +//! * JavaScript //! //! It could be built with `exploitable` feature for severity estimation crashes //! collected from gdb. To save crash reports as json (.casrep/.sarif) use `serde` feature. @@ -29,6 +30,7 @@ pub mod execution_class; pub mod gdb; pub mod go; pub mod java; +pub mod js; pub mod python; pub mod report; pub mod rust;