diff --git a/.github/workflows/aarch64.yml b/.github/workflows/aarch64.yml index ecd48192..ea02c069 100644 --- a/.github/workflows/aarch64.yml +++ b/.github/workflows/aarch64.yml @@ -25,7 +25,8 @@ jobs: install: | export CARGO_TERM_COLOR=always export CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse - apt-get update && apt-get install -y gdb pip curl python3.12-dev clang llvm build-essential + apt-get update && apt-get install -y gdb pip curl python3.12-dev clang \ + llvm build-essential lua5.4 curl https://sh.rustup.rs -o rustup.sh && chmod +x rustup.sh && \ ./rustup.sh -y && rm rustup.sh run: | diff --git a/.github/workflows/amd64.yml b/.github/workflows/amd64.yml index bd264b87..24466478 100644 --- a/.github/workflows/amd64.yml +++ b/.github/workflows/amd64.yml @@ -20,7 +20,7 @@ jobs: - name: Run tests run: | sudo apt update && sudo apt install -y gdb pip curl python3.10-dev llvm \ - openjdk-17-jdk ca-certificates gnupg + openjdk-17-jdk ca-certificates gnupg lua5.4 pip3 install atheris sudo mkdir -p /etc/apt/keyrings curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml index 7fae8fa4..3d641a47 100644 --- a/.github/workflows/coverage.yaml +++ b/.github/workflows/coverage.yaml @@ -18,7 +18,7 @@ jobs: - name: Install Dependences run: | sudo apt update && sudo apt install -y gdb pip curl python3.10-dev llvm \ - openjdk-17-jdk ca-certificates gnupg + openjdk-17-jdk ca-certificates gnupg lua5.4 pip3 install atheris sudo mkdir -p /etc/apt/keyrings curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg diff --git a/.github/workflows/darwin-arm64.yml b/.github/workflows/darwin-arm64.yml index 3180fd98..9e24b064 100644 --- a/.github/workflows/darwin-arm64.yml +++ b/.github/workflows/darwin-arm64.yml @@ -20,7 +20,7 @@ jobs: run: | arch -x86_64 /usr/local/bin/brew update arch -x86_64 /usr/local/bin/brew install gdb curl python llvm \ - openjdk ca-certificates gnupg nodejs --overwrite + openjdk ca-certificates gnupg nodejs lua5.4 --overwrite - name: Build run: cargo build --all-features --verbose - name: NPM packages diff --git a/.github/workflows/riscv64.yml b/.github/workflows/riscv64.yml index 9cee8adb..b7141c5d 100644 --- a/.github/workflows/riscv64.yml +++ b/.github/workflows/riscv64.yml @@ -25,8 +25,8 @@ jobs: install: | export CARGO_TERM_COLOR=always export CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse - apt-get update \ - && apt-get install -y gdb pip curl python3-dev clang llvm build-essential + apt-get update && apt-get install -y gdb pip curl python3-dev clang llvm \ + build-essential lua5.4 curl https://sh.rustup.rs -o rustup.sh && chmod +x rustup.sh && \ ./rustup.sh -y && rm rustup.sh run: | diff --git a/.gitignore b/.gitignore index b3cec92b..91217064 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ Cargo.lock */tests/tmp_tests_casr */tests/casr_tests/csharp/*/bin */tests/casr_tests/csharp/*/obj +*.swo *.swp node_modules */node_modules/* diff --git a/README.md b/README.md index a6d6f6d4..fc638cbc 100644 --- a/README.md +++ b/README.md @@ -31,12 +31,12 @@ ASAN reports or `casr-ubsan` to analyze UBSAN reports. Try `casr-gdb` to get reports from gdb. Use `casr-python` to analyze python reports and get report from [Atheris](https://github.com/google/atheris). Use `casr-java` to analyze java reports and get report from -[Jazzer](https://github.com/CodeIntelligenceTesting/jazzer). Use `casr-js` -to analyze JavaScript reports and get report from +[Jazzer](https://github.com/CodeIntelligenceTesting/jazzer). Use `casr-js` to +analyze JavaScript reports and get report from [Jazzer.js](https://github.com/CodeIntelligenceTesting/jazzer.js) or -[jsfuzz](https://github.com/fuzzitdev/jsfuzz). -Use `casr-csharp` to analyze C# reports and get report from -[Sharpfuzz](https://github.com/Metalnem/sharpfuzz). +[jsfuzz](https://github.com/fuzzitdev/jsfuzz). Use `casr-csharp` to analyze C# +reports and get report from [Sharpfuzz](https://github.com/Metalnem/sharpfuzz). +Use `casr-lua` to analyze Lua reports. Crash report contains many useful information: severity (like [exploitable](https://github.com/jfoote/exploitable)) for x86, x86\_64, arm32, aarch64, rv32g, rv64g architectures, @@ -79,12 +79,13 @@ It can analyze crashes from different sources: and program languages: * C/C++ -* Rust +* C# * Go -* Python * Java * JavaScript -* C# +* Lua +* Python +* Rust It could be built with `exploitable` feature for severity estimation crashes collected from gdb. To save crash reports as json use `serde` feature. @@ -169,6 +170,10 @@ Create report from C#: $ casr-csharp -o csharp.casrep -- dotnet run --project casr/tests/casr_tests/csharp/test_casr_csharp/test_casr_csharp.csproj +Create report from Lua: + + $ casr-lua -o lua.casrep -- casr/tests/casr_tests/lua/test_casr_lua.lua + View report: $ casr-cli casr/tests/casr_tests/casrep/test_clustering_san/load_fuzzer_crash-120697a7f5b87c03020f321c8526adf0f4bcc2dc.casrep diff --git a/casr/src/bin/casr-cli.rs b/casr/src/bin/casr-cli.rs index 2676c0b4..3eed3ff7 100644 --- a/casr/src/bin/casr-cli.rs +++ b/casr/src/bin/casr-cli.rs @@ -429,6 +429,16 @@ fn build_tree_report( tree.collapse_item(row); } + if !report.lua_report.is_empty() { + row = tree + .insert_container_item("LuaReport".to_string(), Placement::After, row) + .unwrap(); + report.lua_report.iter().for_each(|e| { + tree.insert_item(e.clone(), Placement::LastChild, row); + }); + tree.collapse_item(row); + } + if !report.java_report.is_empty() { row = tree .insert_container_item("JavaReport".to_string(), Placement::After, row) @@ -649,6 +659,10 @@ fn build_slider_report( select.add_item("PythonReport", report.python_report.join("\n")); } + if !report.lua_report.is_empty() { + select.add_item("LuaReport", report.lua_report.join("\n")); + } + if !report.java_report.is_empty() { select.add_item("JavaReport", report.java_report.join("\n")); } diff --git a/casr/src/bin/casr-lua.rs b/casr/src/bin/casr-lua.rs new file mode 100644 index 00000000..72c318d4 --- /dev/null +++ b/casr/src/bin/casr-lua.rs @@ -0,0 +1,155 @@ +use casr::util; +use libcasr::{ + init_ignored_frames, + lua::LuaException, + report::CrashReport, + severity::Severity, + stacktrace::Filter, + stacktrace::Stacktrace, + stacktrace::{CrashLine, CrashLineExt}, +}; + +use anyhow::{bail, Result}; +use clap::{Arg, ArgAction, ArgGroup}; +use std::path::{Path, PathBuf}; +use std::process::Command; + +fn main() -> Result<()> { + let matches = clap::Command::new("casr-lua") + .version(clap::crate_version!()) + .about("Create CASR reports (.casrep) from Lua reports") + .term_width(90) + .arg( + Arg::new("output") + .short('o') + .long("output") + .action(ArgAction::Set) + .value_parser(clap::value_parser!(PathBuf)) + .value_name("REPORT") + .help( + "Path to save report. Path can be a directory, then report name is generated", + ), + ) + .arg( + Arg::new("stdout") + .action(ArgAction::SetTrue) + .long("stdout") + .help("Print CASR report to stdout"), + ) + .group( + ArgGroup::new("out") + .args(["stdout", "output"]) + .required(true), + ) + .arg( + Arg::new("stdin") + .long("stdin") + .action(ArgAction::Set) + .value_parser(clap::value_parser!(PathBuf)) + .value_name("FILE") + .help("Stdin file for program"), + ) + .arg( + Arg::new("timeout") + .short('t') + .long("timeout") + .action(ArgAction::Set) + .default_value("0") + .value_name("SECONDS") + .help("Timeout (in seconds) for target execution, 0 value means that timeout is disabled") + .value_parser(clap::value_parser!(u64)) + ) + .arg( + Arg::new("ignore") + .long("ignore") + .action(ArgAction::Set) + .value_parser(clap::value_parser!(PathBuf)) + .value_name("FILE") + .help("File with regular expressions for functions and file paths that should be ignored"), + ) + .arg( + Arg::new("strip-path") + .long("strip-path") + .env("CASR_STRIP_PATH") + .action(ArgAction::Set) + .value_name("PREFIX") + .help("Path prefix to strip from stacktrace"), + ) + .arg( + Arg::new("ARGS") + .action(ArgAction::Set) + .num_args(1..) + .last(true) + .required(true) + .help("Add \"-- \" to run"), + ) + .get_matches(); + + init_ignored_frames!("lua"); + if let Some(path) = matches.get_one::("ignore") { + util::add_custom_ignored_frames(path)?; + } + // Get program args. + let argv: Vec<&str> = if let Some(argvs) = matches.get_many::("ARGS") { + argvs.map(|s| s.as_str()).collect() + } else { + bail!("Wrong arguments for starting program"); + }; + + // Get stdin for target program. + let stdin_file = util::stdin_from_matches(&matches)?; + + // Get timeout + let timeout = *matches.get_one::("timeout").unwrap(); + + // Run program. + let mut cmd = Command::new(argv[0]); + if let Some(ref file) = stdin_file { + cmd.stdin(std::fs::File::open(file)?); + } + if argv.len() > 1 { + cmd.args(&argv[1..]); + } + let result = util::get_output(&mut cmd, timeout, true)?; + let stderr = String::from_utf8_lossy(&result.stderr); + + // Create report. + let mut report = CrashReport::new(); + report.executable_path = argv[0].to_string(); + if argv.len() > 1 { + if let Some(fname) = Path::new(argv[0]).file_name() { + let fname = fname.to_string_lossy(); + if fname.starts_with("lua") && !fname.ends_with(".lua") && argv[1].ends_with(".lua") { + report.executable_path = argv[1].to_string(); + } + } + } + report.proc_cmdline = argv.join(" "); + let _ = report.add_os_info(); + let _ = report.add_proc_environ(); + + // Extract lua exception + let Some(exception) = LuaException::new(&stderr) else { + bail!("Lua exception is not found!"); + }; + + // Parse exception + report.lua_report = exception.lua_report(); + report.stacktrace = exception.extract_stacktrace()?; + report.execution_class = exception.severity()?; + if let Ok(crashline) = exception.crash_line() { + report.crashline = crashline.to_string(); + if let CrashLine::Source(debug) = crashline { + if let Some(sources) = CrashReport::sources(&debug) { + report.source = sources; + } + } + } + let stacktrace = exception.parse_stacktrace()?; + if let Some(path) = matches.get_one::("strip-path") { + util::strip_paths(&mut report, &stacktrace, path); + } + + //Output report + util::output_report(&report, &matches, &argv) +} diff --git a/casr/tests/casr_tests/lua/test_casr_lua.lua b/casr/tests/casr_tests/lua/test_casr_lua.lua new file mode 100755 index 00000000..f1db751e --- /dev/null +++ b/casr/tests/casr_tests/lua/test_casr_lua.lua @@ -0,0 +1,23 @@ +#!/bin/env lua + +function f(a, b) + a = a .. 'qwer' + b = b * 123 + c = a / b + return c +end + +function g(a) + a = a .. 'qwer' + b = 123 + c = f(a, b) + return c +end + +function h() + a = 'qwer' + c = g(a) + return c +end + +print(h()) diff --git a/casr/tests/tests.rs b/casr/tests/tests.rs index 1e7fe468..ed79e335 100644 --- a/casr/tests/tests.rs +++ b/casr/tests/tests.rs @@ -22,6 +22,7 @@ lazy_static::lazy_static! { static ref EXE_CASR_SAN: RwLock<&'static str> = RwLock::new(env!("CARGO_BIN_EXE_casr-san")); static ref EXE_CASR_UBSAN: RwLock<&'static str> = RwLock::new(env!("CARGO_BIN_EXE_casr-ubsan")); static ref EXE_CASR_PYTHON: RwLock<&'static str> = RwLock::new(env!("CARGO_BIN_EXE_casr-python")); + static ref EXE_CASR_LUA: RwLock<&'static str> = RwLock::new(env!("CARGO_BIN_EXE_casr-lua")); static ref EXE_CASR_JAVA: RwLock<&'static str> = RwLock::new(env!("CARGO_BIN_EXE_casr-java")); static ref EXE_CASR_JS: RwLock<&'static str> = RwLock::new(env!("CARGO_BIN_EXE_casr-js")); static ref EXE_CASR_CSHARP: RwLock<&'static str> = RwLock::new(env!("CARGO_BIN_EXE_casr-csharp")); @@ -4856,6 +4857,45 @@ fn test_casr_cluster_d_python() { let _ = std::fs::remove_dir_all(&paths[1]); } +#[test] +#[cfg(target_arch = "x86_64")] +fn test_casr_lua() { + let test_dir = abs_path("tests/tmp_tests_casr/test_casr_lua"); + let test_path = abs_path("tests/casr_tests/lua/test_casr_lua.lua"); + let _ = std::fs::remove_dir_all(test_dir); + + let output = Command::new(*EXE_CASR_LUA.read().unwrap()) + .args(["--stdout", "--", &test_path]) + .output() + .expect("failed to start casr-lua"); + + assert!( + output.status.success(), + "Stdout {}.\n Stderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + + let report: Result = serde_json::from_slice(&output.stdout); + if let Ok(report) = report { + let severity_type = report["CrashSeverity"]["Type"].as_str().unwrap(); + let severity_desc = report["CrashSeverity"]["ShortDescription"] + .as_str() + .unwrap() + .to_string(); + + assert_eq!(6, report["Stacktrace"].as_array().unwrap().iter().count()); + assert_eq!(severity_type, "NOT_EXPLOITABLE"); + assert_eq!(severity_desc, "attempt to div a 'string' with a 'number'"); + assert!(report["CrashLine"] + .as_str() + .unwrap() + .contains("test_casr_lua.lua:6")); + } else { + panic!("Couldn't parse json report file."); + } +} + #[test] #[cfg(target_arch = "x86_64")] fn test_casr_js() { diff --git a/docs/usage.md b/docs/usage.md index be98355f..2251f8d8 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -11,15 +11,16 @@ to analyze JavaScript reports and get report from [Jazzer.js](https://github.com/CodeIntelligenceTesting/jazzer.js) or [jsfuzz](https://github.com/fuzzitdev/jsfuzz). Use `casr-csharp` to analyze C# reports and get report from -[Sharpfuzz](https://github.com/Metalnem/sharpfuzz). `casr-afl` can triage -crashes found by [AFL++](https://github.com/AFLplusplus/AFLplusplus) and -AFL-based fuzzer [Sharpfuzz](https://github.com/Metalnem/sharpfuzz). -`casr-libfuzzer` can triage crashes found by -[libFuzzer](https://www.llvm.org/docs/LibFuzzer.html) (libFuzzer, go-fuzz, -Atheris, Jazzer, Jazzer.js, jsfuzz). `casr-dojo` allows to upload new and -unique CASR reports to [DefectDojo](https://github.com/DefectDojo/django-DefectDojo). -`casr-cli` is meant to provide TUI for viewing reports and converting them into -SARIF report. Reports triage (deduplication, clustering) is done by `casr-cluster`. +[Sharpfuzz](https://github.com/Metalnem/sharpfuzz). Use `casr-lua` to analyze +Lua reports. `casr-afl` can triage crashes found by +[AFL++](https://github.com/AFLplusplus/AFLplusplus) and AFL-based fuzzer +[Sharpfuzz](https://github.com/Metalnem/sharpfuzz). `casr-libfuzzer` can triage +crashes found by [libFuzzer](https://www.llvm.org/docs/LibFuzzer.html) +(libFuzzer, go-fuzz, Atheris, Jazzer, Jazzer.js, jsfuzz). `casr-dojo` allows to +upload new and unique CASR reports to +[DefectDojo](https://github.com/DefectDojo/django-DefectDojo). `casr-cli` is +meant to provide TUI for viewing reports and converting them into SARIF report. +Reports triage (deduplication, clustering) is done by `casr-cluster`. ## casr-gdb @@ -232,6 +233,32 @@ Run casr-csharp: $ casr-csharp -o csharp.casrep -- dotnet run --project casr/tests/casr_tests/csharp/test_casr_csharp/test_casr_csharp.csproj +## casr-lua + +Create CASR reports (.casrep) from Lua reports + + Usage: casr-lua [OPTIONS] <--stdout|--output > -- ... + + Arguments: + ... Add "-- " to run + + Options: + -o, --output Path to save report. Path can be a directory, then report + name is generated + --stdout Print CASR report to stdout + --stdin Stdin file for program + -t, --timeout Timeout (in seconds) for target execution, 0 value means that + timeout is disabled [default: 0] + --ignore File with regular expressions for functions and file paths + that should be ignored + --strip-path Path prefix to strip from stacktrace [env: CASR_STRIP_PATH=] + -h, --help Print help + -V, --version Print version + +Run casr-lua: + + $ casr-lua -o lua.casrep -- casr/tests/casr_tests/lua/test_casr_lua.lua + ## casr-core Analyze coredump for security goals and provide detailed report with severity estimation diff --git a/libcasr/fuzz/fuzz_targets/parse_stacktrace.rs b/libcasr/fuzz/fuzz_targets/parse_stacktrace.rs index 468209db..6f765f32 100644 --- a/libcasr/fuzz/fuzz_targets/parse_stacktrace.rs +++ b/libcasr/fuzz/fuzz_targets/parse_stacktrace.rs @@ -4,13 +4,14 @@ use libfuzzer_sys::fuzz_target; use libcasr::{ asan::AsanStacktrace, + csharp::CSharpStacktrace, gdb::GdbStacktrace, go::GoStacktrace, init_ignored_frames, java::JavaStacktrace, - python::PythonStacktrace, js::JsStacktrace, - csharp::CSharpStacktrace, + lua::LuaStacktrace, + python::PythonStacktrace, stacktrace::{CrashLineExt, Filter, ParseStacktrace, Stacktrace}, }; @@ -30,17 +31,17 @@ fuzz_target!(|data: &[u8]| { } } 1 => { - // Go - if let Ok(raw) = GoStacktrace::extract_stacktrace(&s) { - if let Ok(st) = GoStacktrace::parse_stacktrace(&raw) { + // C# + if let Ok(raw) = CSharpStacktrace::extract_stacktrace(&s) { + if let Ok(st) = CSharpStacktrace::parse_stacktrace(&raw) { let _ = st.crash_line(); } } } 2 => { - // Python - if let Ok(raw) = PythonStacktrace::extract_stacktrace(&s) { - if let Ok(st) = PythonStacktrace::parse_stacktrace(&raw) { + // Go + if let Ok(raw) = GoStacktrace::extract_stacktrace(&s) { + if let Ok(st) = GoStacktrace::parse_stacktrace(&raw) { let _ = st.crash_line(); } } @@ -62,9 +63,17 @@ fuzz_target!(|data: &[u8]| { } } 5 => { - // C# - if let Ok(raw) = CSharpStacktrace::extract_stacktrace(&s) { - if let Ok(st) = CSharpStacktrace::parse_stacktrace(&raw) { + // Lua + if let Ok(raw) = LuaStacktrace::extract_stacktrace(&s) { + if let Ok(st) = LuaStacktrace::parse_stacktrace(&raw) { + let _ = st.crash_line(); + } + } + } + 6 => { + // Python + if let Ok(raw) = PythonStacktrace::extract_stacktrace(&s) { + if let Ok(st) = PythonStacktrace::parse_stacktrace(&raw) { let _ = st.crash_line(); } } diff --git a/libcasr/fuzz/init_corpus/lua b/libcasr/fuzz/init_corpus/lua new file mode 100644 index 00000000..31821b31 --- /dev/null +++ b/libcasr/fuzz/init_corpus/lua @@ -0,0 +1,13 @@ +lua: /usr/local/share/lua/5.4/luacheck/parser.lua:28: error +stack traceback: + [C]: in function 'error' + /usr/local/share/lua/5.4/luacheck/parser.lua:28: in function 'luacheck.parser.syntax_error' + /usr/local/share/lua/5.4/luacheck/parser.lua:96: in upvalue 'parse_error' + /usr/local/share/lua/5.4/luacheck/parser.lua:535: in upvalue 'parse_simple_expression' + /usr/local/share/lua/5.4/luacheck/parser.lua:894: in function + (...tail calls...) + /usr/local/share/lua/5.4/luacheck/parser.lua:974: in upvalue 'parse_block' + /usr/local/share/lua/5.4/luacheck/parser.lua:1020: in function 'luacheck.parser.parse' + luacheck_parser_parse.lua:6: in local 'TestOneInput' + luacheck_parser_parse.lua:18: in main chunk + [C]: in ? diff --git a/libcasr/src/constants.rs b/libcasr/src/constants.rs index 6fe55b9f..e8f79020 100644 --- a/libcasr/src/constants.rs +++ b/libcasr/src/constants.rs @@ -37,6 +37,12 @@ pub const STACK_FRAME_FUNCTION_IGNORE_REGEXES_JS: &[&str] = &[ r"^$", ]; +/// Regular expressions for lua functions to be ignored. +pub const STACK_FRAME_FUNCTION_IGNORE_REGEXES_LUA: &[&str] = &[ + // TODO + r"^[^.]$", +]; + /// Regular expressions for python functions to be ignored. pub const STACK_FRAME_FUNCTION_IGNORE_REGEXES_PYTHON: &[&str] = &[ // TODO @@ -256,6 +262,12 @@ pub const STACK_FRAME_FILEPATH_IGNORE_REGEXES_JS: &[&str] = &[ r"node_modules/jsfuzz", ]; +/// Regular expressions for paths to lua files that should be ignored. +pub const STACK_FRAME_FILEPATH_IGNORE_REGEXES_LUA: &[&str] = &[ + // TODO + r"^[^.]$", +]; + /// Regular expressions for paths to python files that should be ignored. pub const STACK_FRAME_FILEPATH_IGNORE_REGEXES_PYTHON: &[&str] = &[ // TODO diff --git a/libcasr/src/lib.rs b/libcasr/src/lib.rs index 0dc1d90f..b6d0fb04 100644 --- a/libcasr/src/lib.rs +++ b/libcasr/src/lib.rs @@ -12,12 +12,13 @@ //! and program languages: //! //! * C/C++ -//! * Rust +//! * C# //! * Go -//! * Python //! * Java //! * JavaScript -//! * C# +//! * Lua +//! * Python +//! * Rust //! //! 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. @@ -34,6 +35,7 @@ pub mod gdb; pub mod go; pub mod java; pub mod js; +pub mod lua; pub mod python; pub mod report; pub mod rust; diff --git a/libcasr/src/lua.rs b/libcasr/src/lua.rs new file mode 100644 index 00000000..bc11adeb --- /dev/null +++ b/libcasr/src/lua.rs @@ -0,0 +1,334 @@ +//! Lua module implements `ParseStacktrace`, `CrashLineExt` and `Severity` traits for Lua reports. +use crate::error::*; +use crate::execution_class::ExecutionClass; +use crate::severity::Severity; +use crate::stacktrace::{CrashLine, CrashLineExt, DebugInfo}; +use crate::stacktrace::{ParseStacktrace, Stacktrace, StacktraceEntry}; + +use regex::Regex; + +/// Structure provides an interface for save parsing lua exception. +#[derive(Clone, Debug)] +pub struct LuaException { + message: String, +} + +impl LuaException { + /// Create new `LuaException` instance from stream + pub fn new(stream: &str) -> Option { + let re = Regex::new(r#"\S+: .+\n\s*stack traceback:\s*\n(?:.*\n)*.+: .+"#).unwrap(); + let mat = re.find(stream)?; + Some(LuaException { + message: mat.as_str().to_string(), + }) + } + /// Extract stack trace from lua exception. + pub fn extract_stacktrace(&self) -> Result> { + LuaStacktrace::extract_stacktrace(&self.message) + } + /// Transform lua exception into `Stacktrace` type. + pub fn parse_stacktrace(&self) -> Result { + LuaStacktrace::parse_stacktrace(&self.extract_stacktrace()?) + } + /// Get lua exception as a vector of lines. + pub fn lua_report(&self) -> Vec { + self.message + .split('\n') + .map(|s| s.trim().to_string()) + .collect() + } +} + +/// Structure provides an interface for processing the stack trace. +pub struct LuaStacktrace; + +impl ParseStacktrace for LuaStacktrace { + fn extract_stacktrace(stream: &str) -> Result> { + let stacktrace = stream + .split('\n') + .map(|l| l.trim().to_string()) + .collect::>(); + let Some(first) = stacktrace + .iter() + .position(|line| line.ends_with("stack traceback:")) + else { + return Err(Error::Casr( + "Couldn't find traceback in lua report".to_string(), + )); + }; + + let re = Regex::new(r#".+: in .+"#).unwrap(); + Ok(stacktrace[first..] + .iter() + .map(|s| s.to_string()) + .filter(|s| re.is_match(s)) + .collect::>()) + } + + fn parse_stacktrace_entry(entry: &str) -> Result { + let mut stentry = StacktraceEntry::default(); + let re = Regex::new(r#"(.+):(\d+): in (\S+ )(\S+)"#).unwrap(); + let Some(cap) = re.captures(entry) else { + return Err(Error::Casr(format!( + "Couldn't parse stacktrace line: {entry}" + ))); + }; + stentry.debug.file = cap.get(1).unwrap().as_str().to_string(); + if let Ok(line) = cap.get(2).unwrap().as_str().parse::() { + stentry.debug.line = line; + } else { + return Err(Error::Casr(format!( + "Couldn't parse stacktrace line number: {entry}" + ))); + }; + stentry.function = if let Some(func) = cap.get(4) { + func.as_str() + .to_string() + .trim_matches('\'') + .trim_start_matches('<') + .trim_end_matches('>') + .to_string() + } else { + return Err(Error::Casr(format!( + "Couldn't parse stacktrace function: {entry}" + ))); + }; + if let Some(appendix) = cap.get(3) { + let appendix = appendix.as_str().to_string(); + if appendix.starts_with("main") { + stentry.function = appendix + &stentry.function; + } + } + Ok(stentry) + } + + fn parse_stacktrace(entries: &[String]) -> Result { + let mut stacktrace = Stacktrace::new(); + for entry in entries.iter() { + if entry.starts_with("[C]:") { + continue; + } + stacktrace.push(Self::parse_stacktrace_entry(entry)?); + } + Ok(stacktrace) + } +} + +impl CrashLineExt for LuaException { + fn crash_line(&self) -> Result { + let lines = self.lua_report(); + let re = Regex::new(r#"\S+: (.+):(\d+):"#).unwrap(); + let mut cap = re.captures(&lines[0]); + if cap.is_none() { + let Some(index) = lines + .iter() + .rposition(|line| line.ends_with("stack traceback:")) + else { + return Err(Error::Casr( + "Couldn't find traceback in lua report".to_string(), + )); + }; + let re = Regex::new(r#"(.+):(\d+):"#).unwrap(); + for line in &lines[index + 1..] { + cap = re.captures(line); + if cap.is_some() { + break; + } + } + } + let Some(cap) = cap else { + return Err(Error::Casr(format!("Crashline is not found: {:?}", lines))); + }; + let file = cap.get(1).unwrap().as_str().to_string(); + let line = cap.get(2).unwrap().as_str(); + let Ok(line) = line.parse::() else { + return Err(Error::Casr(format!( + "Couldn't crashline line number: {line}" + ))); + }; + Ok(CrashLine::Source(DebugInfo { + file, + line, + column: 0, + })) + } +} + +impl Severity for LuaException { + fn severity(&self) -> Result { + let re = Regex::new(r#"\S+:(?: .+:)? (.+)"#).unwrap(); + let lines = self.lua_report(); + let description = lines.first().unwrap(); + let Some(cap) = re.captures(description) else { + return Err(Error::Casr(format!( + "Couldn't parse exception description: {description}" + ))); + }; + let description = cap.get(1).unwrap().as_str().to_string(); + Ok(ExecutionClass::new(( + "NOT_EXPLOITABLE", + &description, + &description, + "", + ))) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_lua_exception() { + let stream = " + lua: (error object is a table value) + stack traceback: + [C]: in function 'error' + /usr/local/share/lua/5.4/luacheck/parser.lua:28: in function 'luacheck.parser.syntax_error' + /usr/local/share/lua/5.4/luacheck/parser.lua:96: in upvalue 'parse_error' + /usr/local/share/lua/5.4/luacheck/parser.lua:535: in upvalue 'parse_simple_expression' + /usr/local/share/lua/5.4/luacheck/parser.lua:894: in function + (...tail calls...) + /usr/local/share/lua/5.4/luacheck/parser.lua:974: in upvalue 'parse_block' + /usr/local/share/lua/5.4/luacheck/parser.lua:1020: in function 'luacheck.parser.parse' + luacheck_parser_parse.lua:6: in local 'TestOneInput' + luacheck_parser_parse.lua:18: in main chunk + [C]: in ? + "; + let exception = LuaException::new(stream); + let Some(exception) = exception else { + panic!("{:?}", exception); + }; + + let lines = exception.lua_report(); + assert_eq!(lines.len(), 13); + + let sttr = exception.extract_stacktrace(); + let Ok(sttr) = sttr else { + panic!("{}", sttr.err().unwrap()); + }; + assert_eq!(sttr.len(), 10); + + let sttr = exception.parse_stacktrace(); + let Ok(sttr) = sttr else { + panic!("{}", sttr.err().unwrap()); + }; + assert_eq!(sttr.len(), 8); + assert_eq!( + sttr[0].debug.file, + "/usr/local/share/lua/5.4/luacheck/parser.lua".to_string() + ); + assert_eq!(sttr[0].debug.line, 28); + assert_eq!(sttr[0].function, "luacheck.parser.syntax_error".to_string()); + assert_eq!( + sttr[1].debug.file, + "/usr/local/share/lua/5.4/luacheck/parser.lua".to_string() + ); + assert_eq!(sttr[1].debug.line, 96); + assert_eq!(sttr[1].function, "parse_error".to_string()); + assert_eq!( + sttr[2].debug.file, + "/usr/local/share/lua/5.4/luacheck/parser.lua".to_string() + ); + assert_eq!(sttr[2].debug.line, 535); + assert_eq!(sttr[2].function, "parse_simple_expression".to_string()); + assert_eq!( + sttr[3].debug.file, + "/usr/local/share/lua/5.4/luacheck/parser.lua".to_string() + ); + assert_eq!(sttr[3].debug.line, 894); + assert_eq!( + sttr[3].function, + "/usr/local/share/lua/5.4/luacheck/parser.lua:886".to_string() + ); + assert_eq!( + sttr[4].debug.file, + "/usr/local/share/lua/5.4/luacheck/parser.lua".to_string() + ); + assert_eq!(sttr[4].debug.line, 974); + assert_eq!(sttr[4].function, "parse_block".to_string()); + assert_eq!( + sttr[5].debug.file, + "/usr/local/share/lua/5.4/luacheck/parser.lua".to_string() + ); + assert_eq!(sttr[5].debug.line, 1020); + assert_eq!(sttr[5].function, "luacheck.parser.parse".to_string()); + assert_eq!(sttr[6].debug.file, "luacheck_parser_parse.lua".to_string()); + assert_eq!(sttr[6].debug.line, 6); + assert_eq!(sttr[6].function, "TestOneInput".to_string()); + assert_eq!(sttr[7].debug.file, "luacheck_parser_parse.lua".to_string()); + assert_eq!(sttr[7].debug.line, 18); + assert_eq!(sttr[7].function, "main chunk".to_string()); + + let crashline = exception.crash_line(); + let Ok(crashline) = crashline else { + panic!("{}", crashline.err().unwrap()); + }; + assert_eq!( + crashline.to_string(), + "/usr/local/share/lua/5.4/luacheck/parser.lua:28" + ); + + let execution_class = exception.severity(); + let Ok(execution_class) = execution_class else { + panic!("{}", execution_class.err().unwrap()); + }; + assert_eq!(execution_class.severity, "NOT_EXPLOITABLE"); + assert_eq!( + execution_class.short_description, + "(error object is a table value)" + ); + assert_eq!( + execution_class.description, + "(error object is a table value)" + ); + assert_eq!(execution_class.explanation, ""); + + let stream = " + custom-lua-interpreter: (command line):1: crash + stack traceback: + some custom error message + stack traceback: + [C]: in function 'error' + (command line):1: in main chunk + [C]: at 0x607f3df872e0 + "; + let exception = LuaException::new(stream); + let Some(exception) = exception else { + panic!("{:?}", exception); + }; + + let lines = exception.lua_report(); + assert_eq!(lines.len(), 7); + + let sttr = exception.extract_stacktrace(); + let Ok(sttr) = sttr else { + panic!("{}", sttr.err().unwrap()); + }; + assert_eq!(sttr.len(), 2); + + let sttr = exception.parse_stacktrace(); + let Ok(sttr) = sttr else { + panic!("{}", sttr.err().unwrap()); + }; + assert_eq!(sttr.len(), 1); + assert_eq!(sttr[0].debug.file, "(command line)".to_string()); + assert_eq!(sttr[0].debug.line, 1); + assert_eq!(sttr[0].function, "main chunk".to_string()); + + let crashline = exception.crash_line(); + let Ok(crashline) = crashline else { + panic!("{}", crashline.err().unwrap()); + }; + assert_eq!(crashline.to_string(), "(command line):1"); + + let execution_class = exception.severity(); + let Ok(execution_class) = execution_class else { + panic!("{}", execution_class.err().unwrap()); + }; + assert_eq!(execution_class.severity, "NOT_EXPLOITABLE"); + assert_eq!(execution_class.short_description, "crash"); + assert_eq!(execution_class.description, "crash"); + assert_eq!(execution_class.explanation, ""); + } +} diff --git a/libcasr/src/python.rs b/libcasr/src/python.rs index bd2b60d6..759a2d9e 100644 --- a/libcasr/src/python.rs +++ b/libcasr/src/python.rs @@ -59,7 +59,7 @@ impl ParseStacktrace for PythonStacktrace { stentry.debug.line = line; } else { return Err(Error::Casr(format!( - "Couldn't parse stacktrace line num: {entry}" + "Couldn't parse stacktrace line number: {entry}" ))); }; stentry.function = cap.get(3).unwrap().as_str().to_string(); diff --git a/libcasr/src/report.rs b/libcasr/src/report.rs index 52ae6e03..646c3898 100644 --- a/libcasr/src/report.rs +++ b/libcasr/src/report.rs @@ -8,6 +8,7 @@ use crate::gdb::GdbStacktrace; use crate::go::GoStacktrace; use crate::java::JavaStacktrace; use crate::js::JsStacktrace; +use crate::lua::LuaStacktrace; use crate::python::PythonStacktrace; use crate::rust::RustStacktrace; use crate::stacktrace::*; @@ -192,6 +193,13 @@ pub struct CrashReport { )] #[cfg_attr(feature = "serde", serde(default))] pub ubsan_report: Vec, + /// Lua report. + #[cfg_attr( + feature = "serde", + serde(rename(serialize = "LuaReport", deserialize = "LuaReport")) + )] + #[cfg_attr(feature = "serde", serde(default))] + pub lua_report: Vec, /// Python report. #[cfg_attr( feature = "serde", @@ -590,6 +598,8 @@ impl CrashReport { JsStacktrace::parse_stacktrace(&self.stacktrace)? } else if !self.csharp_report.is_empty() { CSharpStacktrace::parse_stacktrace(&self.stacktrace)? + } else if !self.lua_report.is_empty() { + LuaStacktrace::parse_stacktrace(&self.stacktrace)? } else { GdbStacktrace::parse_stacktrace(&self.stacktrace)? }; @@ -739,6 +749,14 @@ impl fmt::Display for CrashReport { report += &(self.ubsan_report.join("\n") + "\n"); } + // LuaReport + if !self.lua_report.is_empty() { + report += "\n===LuaReport===\n"; + for e in self.lua_report.iter() { + report += &format!("{e}\n"); + } + } + // PythonReport if !self.python_report.is_empty() { report += "\n===PythonReport===\n"; @@ -878,6 +896,13 @@ mod tests { "/home/hkctkuy/github/casr/casr/tests/tmp_tests_casr/test_casr_ubsan/test_ubsan.cpp:4:29: runtime error: signed integer overflow: 65535 * 32769 cannot be represented in type 'int'".to_string(), "SUMMARY: UndefinedBehaviorSanitizer: signed-integer-overflow /home/hkctkuy/github/casr/casr/tests/tmp_tests_casr/test_casr_ubsan/test_ubsan.cpp:4:29 in".to_string(), ]; + report.lua_report = vec![ + "luajit: (command line):1: crash".to_string(), + "stack traceback:".to_string(), + "[C]: in function 'error'".to_string(), + "(command line):1: in main chunk".to_string(), + "[C]: at 0x607f3df872e0".to_string(), + ]; report.python_report = vec![ " === Uncaught Python exception: ===".to_string(), "TypeError: unhashable type: 'list'".to_string(), @@ -965,6 +990,13 @@ mod tests { "/home/hkctkuy/github/casr/casr/tests/tmp_tests_casr/test_casr_ubsan/test_ubsan.cpp:4:29: runtime error: signed integer overflow: 65535 * 32769 cannot be represented in type 'int'".to_string(), "SUMMARY: UndefinedBehaviorSanitizer: signed-integer-overflow /home/hkctkuy/github/casr/casr/tests/tmp_tests_casr/test_casr_ubsan/test_ubsan.cpp:4:29 in".to_string(), "".to_string(), + "===LuaReport===".to_string(), + "luajit: (command line):1: crash".to_string(), + "stack traceback:".to_string(), + "[C]: in function 'error'".to_string(), + "(command line):1: in main chunk".to_string(), + "[C]: at 0x607f3df872e0".to_string(), + "".to_string(), "===PythonReport===".to_string(), " === Uncaught Python exception: ===".to_string(), "TypeError: unhashable type: 'list'".to_string(), diff --git a/libcasr/src/stacktrace.rs b/libcasr/src/stacktrace.rs index e52ad5f8..c16e4905 100644 --- a/libcasr/src/stacktrace.rs +++ b/libcasr/src/stacktrace.rs @@ -5,12 +5,14 @@ extern crate lazy_static; use crate::constants::{ STACK_FRAME_FILEPATH_IGNORE_REGEXES_CPP, STACK_FRAME_FILEPATH_IGNORE_REGEXES_CSHARP, STACK_FRAME_FILEPATH_IGNORE_REGEXES_GO, STACK_FRAME_FILEPATH_IGNORE_REGEXES_JAVA, - STACK_FRAME_FILEPATH_IGNORE_REGEXES_JS, STACK_FRAME_FILEPATH_IGNORE_REGEXES_PYTHON, - STACK_FRAME_FILEPATH_IGNORE_REGEXES_RUST, STACK_FRAME_FUNCTION_IGNORE_REGEXES_CPP, - STACK_FRAME_FUNCTION_IGNORE_REGEXES_CSHARP, STACK_FRAME_FUNCTION_IGNORE_REGEXES_GO, - STACK_FRAME_FUNCTION_IGNORE_REGEXES_JAVA, STACK_FRAME_FUNCTION_IGNORE_REGEXES_JS, + STACK_FRAME_FILEPATH_IGNORE_REGEXES_JS, STACK_FRAME_FILEPATH_IGNORE_REGEXES_LUA, + STACK_FRAME_FILEPATH_IGNORE_REGEXES_PYTHON, STACK_FRAME_FILEPATH_IGNORE_REGEXES_RUST, + STACK_FRAME_FUNCTION_IGNORE_REGEXES_CPP, STACK_FRAME_FUNCTION_IGNORE_REGEXES_CSHARP, + STACK_FRAME_FUNCTION_IGNORE_REGEXES_GO, STACK_FRAME_FUNCTION_IGNORE_REGEXES_JAVA, + STACK_FRAME_FUNCTION_IGNORE_REGEXES_JS, STACK_FRAME_FUNCTION_IGNORE_REGEXES_LUA, STACK_FRAME_FUNCTION_IGNORE_REGEXES_PYTHON, STACK_FRAME_FUNCTION_IGNORE_REGEXES_RUST, }; + use crate::error::*; use kodama::{linkage, Method}; use regex::Regex; @@ -323,18 +325,14 @@ pub trait Filter { let (funcs, files): (Vec<_>, Vec<_>) = languages .iter() .map(|&x| match x { - "python" => ( - STACK_FRAME_FUNCTION_IGNORE_REGEXES_PYTHON, - STACK_FRAME_FILEPATH_IGNORE_REGEXES_PYTHON, - ), - "rust" => ( - STACK_FRAME_FUNCTION_IGNORE_REGEXES_RUST, - STACK_FRAME_FILEPATH_IGNORE_REGEXES_RUST, - ), "cpp" => ( STACK_FRAME_FUNCTION_IGNORE_REGEXES_CPP, STACK_FRAME_FILEPATH_IGNORE_REGEXES_CPP, ), + "csharp" => ( + STACK_FRAME_FUNCTION_IGNORE_REGEXES_CSHARP, + STACK_FRAME_FILEPATH_IGNORE_REGEXES_CSHARP, + ), "go" => ( STACK_FRAME_FUNCTION_IGNORE_REGEXES_GO, STACK_FRAME_FILEPATH_IGNORE_REGEXES_GO, @@ -347,9 +345,17 @@ pub trait Filter { STACK_FRAME_FUNCTION_IGNORE_REGEXES_JS, STACK_FRAME_FILEPATH_IGNORE_REGEXES_JS, ), - "csharp" => ( - STACK_FRAME_FUNCTION_IGNORE_REGEXES_CSHARP, - STACK_FRAME_FILEPATH_IGNORE_REGEXES_CSHARP, + "lua" => ( + STACK_FRAME_FUNCTION_IGNORE_REGEXES_LUA, + STACK_FRAME_FILEPATH_IGNORE_REGEXES_LUA, + ), + "python" => ( + STACK_FRAME_FUNCTION_IGNORE_REGEXES_PYTHON, + STACK_FRAME_FILEPATH_IGNORE_REGEXES_PYTHON, + ), + "rust" => ( + STACK_FRAME_FUNCTION_IGNORE_REGEXES_RUST, + STACK_FRAME_FILEPATH_IGNORE_REGEXES_RUST, ), &_ => (["^[^.]$"].as_slice(), ["^[^.]$"].as_slice()), }) @@ -474,7 +480,7 @@ pub mod tests { let mut is_inited = INITED_STACKFRAMES_FILTER.write().unwrap(); if !*is_inited { *is_inited = true; - init_ignored_frames!("cpp", "rust", "python", "go", "java", "js", "csharp"); + init_ignored_frames!("cpp", "csharp", "go", "java", "js", "lua", "python", "rust"); } } diff --git a/libcasr/src/ubsan.rs b/libcasr/src/ubsan.rs index 4d014b71..b92c3d66 100644 --- a/libcasr/src/ubsan.rs +++ b/libcasr/src/ubsan.rs @@ -1,11 +1,11 @@ //! UndefinedBehaviorSanitizer module implements `Severity` and `CrashLineExt` traits for UndefinedBehaviorSanitizer warnings. use crate::asan::AsanStacktrace; +use crate::error::{Error, Result}; +use crate::execution_class::ExecutionClass; use crate::severity::Severity; use crate::stacktrace::{CrashLine, CrashLineExt, DebugInfo}; use crate::stacktrace::{ParseStacktrace, StacktraceEntry}; -use crate::error::*; -use crate::execution_class::ExecutionClass; use regex::Regex; /// Structure provides an interface for parsing ubsan runtime error message.