libcasr 2.13.0

Collect crash reports, triage, and estimate severity.
Documentation
//! 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 regex::Regex;

/// Structure provides an interface for parsing ubsan runtime error message.
#[derive(Clone, Debug)]
pub struct UbsanWarning {
    pub message: String,
}

impl UbsanWarning {
    /// Extract stack trace from ubsan message.
    pub fn extract_stacktrace(&self) -> Result<Vec<String>> {
        AsanStacktrace::extract_stacktrace(&self.message)
    }
    /// Transform stack trace line into StacktraceEntry type.
    pub fn parse_stacktrace_entry(entry: &str) -> Result<StacktraceEntry> {
        AsanStacktrace::parse_stacktrace_entry(entry)
    }
    /// Get ubsan runtime error message as a vector of lines.
    pub fn ubsan_report(&self) -> Vec<String> {
        self.message
            .split('\n')
            .map(|s| s.trim_end().to_string())
            .collect()
    }
}

impl Severity for UbsanWarning {
    fn severity(&self) -> Result<ExecutionClass> {
        let message = self.ubsan_report();
        if message.len() <= 1 {
            return Err(Error::Casr("Malformed ubsan message".to_string()));
        }
        // Get description (from first line)
        let description = message.first().unwrap();
        let re = Regex::new(r".+: runtime error: (.+)").unwrap();
        let Some(cap) = re.captures(description) else {
            return Err(Error::Casr(format!(
                "Couldn't parse error description: {description}"
            )));
        };
        let description = cap.get(1).unwrap().as_str().to_string();
        // Get short description (from last line)
        let short_description = message.last().unwrap();
        let re = Regex::new(r"SUMMARY: UndefinedBehaviorSanitizer: ([A-Za-z_\-\(\)]+)").unwrap();
        let Some(cap) = re.captures(short_description) else {
            return Err(Error::Casr(format!(
                "Couldn't parse ubsan summary: {short_description}"
            )));
        };
        let short_description = cap.get(1).unwrap().as_str().to_string();

        Ok(ExecutionClass::new((
            "NOT_EXPLOITABLE",
            &short_description,
            &description,
            "",
        )))
    }
}

impl CrashLineExt for UbsanWarning {
    fn crash_line(&self) -> Result<CrashLine> {
        /// Get crashline from specified string
        fn get_crash_line(crashline: &String) -> Result<CrashLine> {
            if let Ok(crashline) = UbsanWarning::parse_stacktrace_entry(crashline) {
                if !crashline.debug.file.is_empty() {
                    Ok(CrashLine::Source(crashline.debug))
                } else if !crashline.module.is_empty() && crashline.offset != 0 {
                    Ok(CrashLine::Module {
                        file: crashline.module,
                        offset: crashline.offset,
                    })
                } else {
                    Err(Error::Casr(format!(
                        "Couldn't collect crashline from stack trace: {:?}",
                        crashline
                    )))
                }
            } else {
                let re = Regex::new(r"(.+?):(\d+):(?:(\d+):)? runtime error: ").unwrap();
                let Some(cap) = re.captures(crashline) else {
                    return Err(Error::Casr(format!(
                        "Couldn't parse error crashline: {crashline}"
                    )));
                };
                let file = cap.get(1).unwrap().as_str().to_string();
                let line = cap.get(2).unwrap().as_str().parse::<u64>();
                let Ok(line) = line else {
                    return Err(Error::Casr(format!(
                        "Couldn't parse crashline line: {crashline}"
                    )));
                };
                if let Some(column) = cap.get(3) {
                    let column = column.as_str().parse::<u64>();
                    let Ok(column) = column else {
                        return Err(Error::Casr(format!(
                            "Couldn't parse crashline column: {crashline}"
                        )));
                    };
                    return Ok(CrashLine::Source(DebugInfo { file, line, column }));
                }
                Ok(CrashLine::Source(DebugInfo {
                    file,
                    line,
                    column: 0,
                }))
            }
        }

        let message = self.ubsan_report();
        if message.is_empty() {
            return Err(Error::Casr("Empty ubsan message".to_string()));
        }
        // If there is a stacktrace use first string from stacktrace
        // Or combine with crashline from UbsanWarning first string
        // Else use crashline from UbsanWarning first string
        let header_crashline = get_crash_line(&message[0]);
        if let Some(crashline) = message
            .iter()
            .skip(1)
            .find(|line| line.starts_with("    #0 "))
        {
            let Ok(stack_crashline) = get_crash_line(crashline) else {
                return header_crashline;
            };
            let Ok(header_crashline) = header_crashline else {
                return Ok(stack_crashline);
            };
            let CrashLine::Source(stack_crashline) = stack_crashline else {
                // header_crashline may be a CrashLine::Source
                return Ok(header_crashline);
            };
            let CrashLine::Source(header_crashline) = header_crashline else {
                return Ok(CrashLine::Source(stack_crashline));
            };
            // Get line and column if it's possible
            let line = if header_crashline.line != 0 {
                header_crashline.line
            } else {
                stack_crashline.line
            };
            let column = if header_crashline.column != 0 {
                header_crashline.column
            } else {
                stack_crashline.column
            };
            // Path from header may be not absolute
            Ok(CrashLine::Source(DebugInfo {
                file: stack_crashline.file,
                line,
                column,
            }))
        } else {
            header_crashline
        }
    }
}

/// Extract ubsan warnings form stderr
///
/// # Arguments
///
/// * `stderr` - output containing ubsan warnings
///
/// # Return value
///
/// Ubsan warning struct vector
///
pub fn extract_ubsan_warnings(stderr: &str) -> Vec<UbsanWarning> {
    let mut ubsan_warnings: Vec<UbsanWarning> = vec![];
    let re = Regex::new(r"(.+: runtime error: (?:.*\n)*?SUMMARY: UndefinedBehaviorSanitizer: .*)")
        .unwrap();
    for cap in re.captures_iter(stderr) {
        let message = cap[0].to_string();
        ubsan_warnings.push(UbsanWarning { message });
    }
    ubsan_warnings
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_ubsan_parse() {
        let stderr =
            "/tarantool/src/box/sql/build.c:263:17: runtime error: null pointer passed as argument 2, which is declared to never be null
/usr/include/string.h:44:28: note: nonnull attribute specified here
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior /tarantool/src/box/sql/build.c:263:17 in
/tarantool/src/box/sql/vdbeaux.c:1417:6: runtime error: implicit conversion from type 'int' of value -8 (32-bit, signed) to type 'unsigned long' changed the value to 18446744073709551608 (64-bit, unsigned)
    #0 0x14529af in sqlVdbeMakeReady /tarantool/src/box/sql/vdbeaux.c:1417:6
    #1 0xd94ff7 in sql_finish_coding /tarantool/src/box/sql/build.c:109:3
    #2 0x1291e28 in sql_code_ast /tarantool/src/box/sql/tokenize.c:506:3
    #3 0x128f24c in sqlRunParser /tarantool/src/box/sql/tokenize.c:585:2
    #4 0x10d6e5b in sql_stmt_compile /tarantool/src/box/sql/prepare.c:79:4
    #5 0xd01caf in sql_fuzz /tarantool/src/box/sql.c:1730:6
    #6 0x8ced0e in TestOneProtoInput(sql_query::SQLQuery const&) /tarantool/test/fuzz/sql_fuzzer/sql_fuzzer.cc:50:2
    #7 0x8ce0d9 in LLVMFuzzerTestOneInput /tarantool/test/fuzz/sql_fuzzer/sql_fuzzer.cc:38:1
    #8 0x7f4131 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) /llvm-project-llvmorg-14.0.6/compiler-rt/lib/fuzzer/FuzzerLoop.cpp:611:15
    #9 0x7de03c in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) /llvm-project-llvmorg-14.0.6/compiler-rt/lib/fuzzer/FuzzerDriver.cpp:324:6
    #10 0x7e3d8b in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) /llvm-project-llvmorg-14.0.6/compiler-rt/lib/fuzzer/FuzzerDriver.cpp:860:9
    #11 0x80d342 in main /llvm-project-llvmorg-14.0.6/compiler-rt/lib/fuzzer/FuzzerMain.cpp:20:10
    #12 0x7f296f4d7082 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x24082) (BuildId: 1878e6b475720c7c51969e69ab2d276fae6d1dee)
    #13 0x7d895d in _start (/sql_fuzzer+0x7d895d)

SUMMARY: UndefinedBehaviorSanitizer: implicit-integer-sign-change /tarantool/src/box/sql/vdbeaux.c:1417:6 in
Executed sql-out/corpus/7daf7545bad605f9ea192f6523d5427c757e56a4 in 66 ms
***
*** NOTE: fuzzing was not performed, you have only
***       executed the target code on a fixed set of inputs.
***
/tarantool/src/lib/small/include/small/lf_lifo.h:86:59: runtime error: applying non-zero offset 1 to null pointer
    #0 0x3f6a87e in lf_lifo_push /tarantool/src/lib/small/include/small/lf_lifo.h:86:59
    #1 0x3f6a162 in slab_unmap /tarantool/src/lib/small/small/slab_arena.c:275:2
    #2 0x3ebb1da in slab_cache_destroy /tarantool/src/lib/small/small/slab_cache.c:213:4
    #3 0x3c1773d in cord_destroy /tarantool/src/lib/core/fiber.c:1704:2
    #4 0x3c26a42 in fiber_free /tarantool/src/lib/core/fiber.c:2040:2
    #5 0x8cd6fa in teardown() /tarantool/test/fuzz/sql_fuzzer/sql_fuzzer.cc:34:2
    #6 0x7f296fe8df6a  (/lib64/ld-linux-x86-64.so.2+0x11f6a) (BuildId: 4587364908de169dec62ffa538170118c1c3a078)
    #7 0x7f296f4f98a6  (/lib/x86_64-linux-gnu/libc.so.6+0x468a6) (BuildId: 1878e6b475720c7c51969e69ab2d276fae6d1dee)
    #8 0x7f296f4f9a5f in exit (/lib/x86_64-linux-gnu/libc.so.6+0x46a5f) (BuildId: 1878e6b475720c7c51969e69ab2d276fae6d1dee)
    #9 0x7e3f43 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) /llvm-project-llvmorg-14.0.6/compiler-rt/lib/fuzzer/FuzzerDriver.cpp
    #10 0x80d342 in main /llvm-project-llvmorg-14.0.6/compiler-rt/lib/fuzzer/FuzzerMain.cpp:20:10
    #11 0x7f296f4d7082 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x24082) (BuildId: 1878e6b475720c7c51969e69ab2d276fae6d1dee)
    #12 0x7d895d in _start (/sql_fuzzer+0x7d895d)

SUMMARY: UndefinedBehaviorSanitizer: nullptr-with-nonzero-offset /tarantool/src/lib/small/include/small/lf_lifo.h:86:59 in
/openjpeg/src/lib/openjp2/j2k.c:10085:46: runtime error: unsigned integer overflow: 255 - 536870656 cannot be represented in type 'unsigned int'
    #0 0x601e15 in opj_j2k_update_image_data /openjpeg/src/lib/openjp2/j2k.c
    #1 0x5f7cb3 in opj_j2k_decode_tiles /openjpeg/src/lib/openjp2/j2k.c:11743:15
    #2 0x54e785 in opj_j2k_exec /openjpeg/src/lib/openjp2/j2k.c:9031:33

SUMMARY: UndefinedBehaviorSanitizer: unsigned-integer-overflow /openjpeg/src/lib/openjp2/j2k.c:10085:46 in";
        // Check warning extract
        let warnings = extract_ubsan_warnings(stderr);
        assert_eq!(warnings.len(), 4, "{:?}", warnings);

        // Check warning
        let warning = &warnings[0];
        assert_eq!(warning.ubsan_report().len(), 3);

        // Check severity
        let execution_class = warning.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, "undefined-behavior");
        assert_eq!(
            execution_class.description,
            "null pointer passed as argument 2, which is declared to never be null"
        );
        assert_eq!(execution_class.explanation, "");

        // Check crashline
        let crash_line = warning.crash_line();
        if let Ok(crash_line) = crash_line {
            assert_eq!(
                crash_line.to_string(),
                "/tarantool/src/box/sql/build.c:263:17"
            );
        } else {
            panic!("{}", crash_line.err().unwrap());
        }

        // Check warning
        let warning = &warnings[1];
        assert_eq!(warning.ubsan_report().len(), 17);

        // Check stacktrace
        let stacktrace = warning.extract_stacktrace();
        let Ok(stacktrace) = stacktrace else {
            panic!("{}", stacktrace.err().unwrap());
        };
        assert_eq!(stacktrace.len(), 14);

        // Check severity
        let execution_class = warning.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,
            "implicit-integer-sign-change"
        );
        assert_eq!(
            execution_class.description,
            "implicit conversion from type 'int' of value -8 (32-bit, signed) to type 'unsigned long' changed the value to 18446744073709551608 (64-bit, unsigned)"
        );
        assert_eq!(execution_class.explanation, "");

        // Check crashline
        let crash_line = warning.crash_line();
        if let Ok(crash_line) = crash_line {
            assert_eq!(
                crash_line.to_string(),
                "/tarantool/src/box/sql/vdbeaux.c:1417:6"
            );
        } else {
            panic!("{}", crash_line.err().unwrap());
        }

        // Check warning
        let warning = &warnings[2];
        assert_eq!(warning.ubsan_report().len(), 16);

        // Check stacktrace
        let stacktrace = warning.extract_stacktrace();
        let Ok(stacktrace) = stacktrace else {
            panic!("{}", stacktrace.err().unwrap());
        };
        assert_eq!(stacktrace.len(), 13);

        // Check severity
        let execution_class = warning.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,
            "nullptr-with-nonzero-offset"
        );
        assert_eq!(
            execution_class.description,
            "applying non-zero offset 1 to null pointer"
        );
        assert_eq!(execution_class.explanation, "");

        // Check crashline
        let crash_line = warning.crash_line();
        if let Ok(crash_line) = crash_line {
            assert_eq!(
                crash_line.to_string(),
                "/tarantool/src/lib/small/include/small/lf_lifo.h:86:59"
            );
        } else {
            panic!("{}", crash_line.err().unwrap());
        }

        // Check warning
        let warning = &warnings[3];
        assert_eq!(warning.ubsan_report().len(), 6);

        // Check severity
        let execution_class = warning.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,
            "unsigned-integer-overflow"
        );
        assert_eq!(
            execution_class.description,
            "unsigned integer overflow: 255 - 536870656 cannot be represented in type 'unsigned int'"
        );
        assert_eq!(execution_class.explanation, "");

        // Check crashline
        let crash_line = warning.crash_line();
        if let Ok(crash_line) = crash_line {
            assert_eq!(
                crash_line.to_string(),
                "/openjpeg/src/lib/openjp2/j2k.c:10085:46"
            );
        } else {
            panic!("{}", crash_line.err().unwrap());
        }
    }
}