Skip to content

Commit

Permalink
add casr-msan
Browse files Browse the repository at this point in the history
  • Loading branch information
Pavel Nekrasov committed Feb 24, 2025
1 parent 5c3d818 commit 356c9a9
Show file tree
Hide file tree
Showing 10 changed files with 509 additions and 4 deletions.
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ CASR is maintained by:

CASR is a set of tools that allows you to collect crash reports in different
ways. Use `casr-core` binary to deal with coredumps. Use `casr-san` to analyze
ASAN reports or `casr-ubsan` to analyze UBSAN reports. Try `casr-gdb` to get
ASAN reports or `casr-msan` to analyze
MSAN 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
Expand Down Expand Up @@ -74,6 +75,7 @@ crashes.
It can analyze crashes from different sources:

* AddressSanitizer
* MemorySanitizer
* UndefinedBehaviorSanitizer
* Gdb output

Expand Down Expand Up @@ -145,6 +147,11 @@ Create report from AddressSanitizer output:
$ clang++ -fsanitize=address -O0 -g casr/tests/casr_tests/test_asan_df.cpp -o test_asan_df
$ casr-san -o asan.casrep -- ./test_asan_df

Create report from MemorySanitizer output:

$ clang++ -fsanitize=memory -O0 casr/tests/casr_tests/test_msan.cpp -o test_msan
$ casr-msan -o msan.casrep -- ./test_msan

Create report from UndefinedBehaviorSanitizer output:

$ clang++ -fsanitize=undefined -O0 -g casr/tests/casr_tests/ubsan/test_ubsan.cpp -o test_ubsan
Expand Down
14 changes: 14 additions & 0 deletions casr/src/bin/casr-cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,16 @@ fn build_tree_report(
tree.expand_item(row);
}

if !report.msan_report.is_empty() {
row = tree
.insert_container_item("MsanReport".to_string(), Placement::After, row)
.unwrap();
report.msan_report.iter().for_each(|e| {
tree.insert_item(e.clone(), Placement::LastChild, row);
});
tree.expand_item(row);
}

if !report.ubsan_report.is_empty() {
row = tree
.insert_container_item("UbsanReport".to_string(), Placement::After, row)
Expand Down Expand Up @@ -656,6 +666,10 @@ fn build_slider_report(
select.add_item("AsanReport", report.asan_report.join("\n"));
}

if !report.msan_report.is_empty() {
select.add_item("MsanReport", report.msan_report.join("\n"));
}

if !report.ubsan_report.is_empty() {
select.add_item("UbsanReport", report.ubsan_report.join("\n"));
}
Expand Down
290 changes: 290 additions & 0 deletions casr/src/bin/casr-msan.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
use casr::util;
use libcasr::{
msan::{MsanStacktrace, MsanContext},
constants::{
SIGINFO_SIGABRT, SIGINFO_SIGBUS, SIGINFO_SIGILL, SIGINFO_SIGSEGV, SIGINFO_SIGSYS,
SIGINFO_SIGTRAP,
},
cpp::CppException,
exception::Exception,
execution_class::*,
gdb::*,
init_ignored_frames,
report::CrashReport,
severity::Severity,
stacktrace::*,
};

use anyhow::{bail, Context, Result};
use clap::{Arg, ArgAction, ArgGroup};
use gdb_command::mappings::{MappedFiles, MappedFilesExt};
use gdb_command::stacktrace::StacktraceExt;
use gdb_command::*;
use regex::Regex;

use std::env;
use std::os::unix::process::{CommandExt, ExitStatusExt};
use std::path::PathBuf;
use std::process::Command;

fn main() -> Result<()> {
let matches = clap::Command::new("casr-msan")
.version(clap::crate_version!())
.about("Create CASR reports (.casrep) from MemorySanitizer reports")
.term_width(90)
.arg(
Arg::new("output")
.short('o')
.long("output")
.action(ArgAction::Set)
.value_name("REPORT")
.value_parser(clap::value_parser!(PathBuf))
.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_name("FILE")
.value_parser(clap::value_parser!(PathBuf))
.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_name("FILE")
.value_parser(clap::value_parser!(PathBuf))
.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 and crash line"),
)
.arg(
Arg::new("ARGS")
.action(ArgAction::Set)
.num_args(1..)
.last(true)
.required(true)
.help("Add \"-- ./binary <arguments>\" to run executable"),
)
.get_matches();

// Get program args.
let argv: Vec<&str> = if let Some(argvs) = matches.get_many::<String>("ARGS") {
argvs.map(|s| s.as_str()).collect()
} else {
bail!("Wrong arguments for starting program");
};

init_ignored_frames!("cpp");

if let Some(path) = matches.get_one::<PathBuf>("ignore") {
util::add_custom_ignored_frames(path)?;
}
// Get stdin for target program.
let stdin_file = util::stdin_from_matches(&matches)?;

// Get timeout
let timeout = *matches.get_one::<u64>("timeout").unwrap();

// Set rss limit.
if let Ok(msan_options_str) = env::var("MSAN_OPTIONS") {
let mut msan_options = msan_options_str.clone();
if !msan_options_str.contains("hard_rss_limit_mb") {
msan_options = [msan_options.as_str(), "hard_rss_limit_mb=2048"].join(",");
}
if msan_options.starts_with(',') {
msan_options.remove(0);
}
msan_options = msan_options.replace("symbolize=0", "symbolize=1");
unsafe {
std::env::set_var("MSAN_OPTIONS", msan_options);
}
} else {
unsafe {
std::env::set_var("MSAN_OPTIONS", "hard_rss_limit_mb=2048");
}
}

// Run program with sanitizers.
let mut sanitizers_cmd = Command::new(argv[0]);
if let Some(ref file) = stdin_file {
sanitizers_cmd.stdin(std::fs::File::open(file).unwrap());
}
if argv.len() > 1 {
sanitizers_cmd.args(&argv[1..]);
}
#[cfg(target_os = "macos")]
{
sanitizers_cmd.env("DYLD_NO_PIE", "1");
}
#[cfg(target_os = "linux")]
{
use linux_personality::{Personality, personality};

unsafe {
sanitizers_cmd.pre_exec(|| {
if personality(Personality::ADDR_NO_RANDOMIZE).is_err() {
panic!("Cannot set personality");
}
Ok(())
})
};
}
let sanitizers_result = util::get_output(&mut sanitizers_cmd, timeout, true)?;
let sanitizers_stderr = String::from_utf8_lossy(&sanitizers_result.stderr);

if sanitizers_stderr.contains("Cannot set personality") {
bail!("Cannot set personality (if you are running docker, allow personality syscall in your seccomp profile)");
}

// Create report.
let mut report = CrashReport::new();
report.executable_path = argv[0].to_string();
report.proc_cmdline = argv.join(" ");
let _ = report.add_os_info();
let _ = report.add_proc_environ();
if let Some(mut file_path) = stdin_file.clone() {
file_path = file_path.canonicalize().unwrap_or(file_path);
report.stdin = file_path.display().to_string();
}

let stacktrace: Stacktrace;

// Get MASAN report.
let msan_stderr_list: Vec<String> = sanitizers_stderr
.split('\n')
.map(|l| l.trim_end().to_string())
.collect();
let rmsan_start =
Regex::new(r"==\d+==\s*WARNING: MemorySanitizer:").unwrap();
if let Some(report_start) = msan_stderr_list
.iter()
.position(|line| rmsan_start.is_match(line))
{
// Set MASAN report in casr report.
let report_end = msan_stderr_list.iter().rposition(|s| !s.is_empty()).unwrap() + 1;
report.msan_report = Vec::from(&msan_stderr_list[report_start..report_end]);
let context = MsanContext(report.msan_report.clone());
let severity = context.severity();
if let Ok(severity) = severity {
report.execution_class = severity;
} else {
eprintln!("Couldn't estimate severity. {}", severity.err().unwrap());
}
report.stacktrace = MsanStacktrace::extract_stacktrace(&report.msan_report.join("\n"))?;
} else {
// Get termination signal.
if let Some(signal) = sanitizers_result.status.signal() {
// Get stack trace and mappings from gdb.
match signal as u32 {
SIGINFO_SIGILL | SIGINFO_SIGSYS => {
report.execution_class = ExecutionClass::find("BadInstruction").unwrap();
}
SIGINFO_SIGTRAP => {
report.execution_class = ExecutionClass::find("TrapSignal").unwrap();
}
SIGINFO_SIGABRT => {
report.execution_class = ExecutionClass::find("AbortSignal").unwrap();
}
SIGINFO_SIGBUS | SIGINFO_SIGSEGV => {
eprintln!("Segmentation fault occurred, but there is not enough information available to determine \
exploitability. Try using casr-gdb instead.");
report.execution_class = ExecutionClass::find("AccessViolation").unwrap();
}
_ => {
// "Undefined" is by default in report.
}
}

// Get stack trace and mappings from gdb.
let gdb_result = GdbCommand::new(&ExecType::Local(&argv))
.timeout(timeout)
.stdin(&stdin_file)
.r()
.bt()
.mappings()
.launch()
.with_context(|| "Unable to get results from gdb")?;

let frame = Regex::new(r"^ *#[0-9]+").unwrap();
report.stacktrace = gdb_result[0]
.split('\n')
.filter(|x| frame.is_match(x))
.map(|x| x.to_string())
.collect::<Vec<String>>();
report.proc_maps = gdb_result[1]
.split('\n')
.skip(4)
.map(|x| x.to_string())
.collect::<Vec<String>>();
} else {
// Normal termination.
bail!("Program terminated (no crash)");
}
}

// Get stacktrace to find crash line.
stacktrace = if !report.msan_report.is_empty() {
MsanStacktrace::parse_stacktrace(&report.stacktrace)?
} else {
let mut parsed_stacktrace = GdbStacktrace::parse_stacktrace(&report.stacktrace)?;
if let Ok(mfiles) = MappedFiles::from_gdb(report.proc_maps.join("\n")) {
parsed_stacktrace.compute_module_offsets(&mfiles);
}
parsed_stacktrace
};

// Check for exceptions
if let Some(class) = [CppException::parse_exception]
.iter()
.find_map(|parse| parse(&sanitizers_stderr))
{
report.execution_class = class;
}

// Get crash line.
if let Ok(crash_line) = stacktrace.crash_line() {
report.crashline = crash_line.to_string();
if let CrashLine::Source(debug) = crash_line {
if let Some(sources) = CrashReport::sources(&debug) {
report.source = sources;
}
}
}

if let Some(path) = matches.get_one::<String>("strip-path") {
util::strip_paths(&mut report, &stacktrace, path);
}

util::output_report(&report, &matches, &argv)
}
15 changes: 15 additions & 0 deletions casr/tests/casr_tests/test_msan.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#include <stdio.h>

void set_val(bool &b, const int val) {
if (val > 1) {
b = false;
}
}

int main(const int argc, const char *[]) {
bool b;
set_val(b, argc);
if (b) {
printf("value set\n");
}
}
Loading

0 comments on commit 356c9a9

Please sign in to comment.